Skip to content

feat(desktop): default new profiles to Agent mode - #2426

Open
mt-hub8 wants to merge 10 commits into
tutti-os:mainfrom
mt-hub8:codex/new-user-os-immersive-default
Open

feat(desktop): default new profiles to Agent mode#2426
mt-hub8 wants to merge 10 commits into
tutti-os:mainfrom
mt-hub8:codex/new-user-os-immersive-default

Conversation

@mt-hub8

@mt-hub8 mt-hub8 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Make fresh-profile initialization an authoritative tuttid rule: daemon defaults enable workspace.standaloneAgentMode, and writeMode: initializeIfAbsent atomically inserts a complete preference row or returns the existing row unchanged.
  • Preserve all initialized profiles without migration or backfill: explicit Agent and OS selections remain unchanged, and the historical missing key still resolves to OS mode.
  • Separate initial-read failure from initialization failure. A confirmed fresh identity keeps Agent as its in-memory fallback, while an ambiguous write is reconciled by reading the authoritative row again.
  • Pass the selected primary window kind explicitly through the workspace-launch application core to the native adapter.
  • Ensure composer-default and session-launch-mode patch writers bootstrap the same complete fresh default instead of materializing a partial preference row.

Behavior

Preference state Startup result Write behavior
No row (initialized: false) Agent Atomic create-if-absent with Agent enabled
Existing, explicit true Agent Existing row returned unchanged
Existing, explicit false OS Existing row returned unchanged
Existing, mode key absent OS (historical fallback) Existing row returned unchanged
Initial GET fails OS (safe historical fallback) No initialization attempted
Initialization outcome is ambiguous Authoritative reread result No blind replacement

The ambiguous-write fallback answers three questions in order:

  1. Did the first GET confirm that this identity had no stored preferences? If not, never assume it is new.
  2. Did the reconciliation GET find an initialized row? If yes, use that row, including a concurrent OS selection.
  3. Is the row still absent or temporarily unreadable after a confirmed-fresh result? Keep the fresh Agent candidate for this process and retry durable initialization on the next launch.

Architecture

  • PUT /v1/preferences/desktop remains backward compatible: omitted/replace keeps the existing full-update behavior; initializeIfAbsent performs the atomic create-only operation.
  • For initializeIfAbsent, tuttid applies its Agent workspace-mode default to the normalized candidate while preserving every other flag. Desktop consumes the daemon's uninitialized response instead of defining a second fresh-mode default.
  • SQLite uses one full INSERT ... ON CONFLICT DO NOTHING, then returns the authoritative stored row. It does not overwrite another initializer or concurrent preference writer.
  • No schema migration or existing-row backfill is introduced.
  • This changes Desktop startup product policy, not Agent Host session/turn/goal/runtime-operation lifecycle semantics.

Verification

  • Negative controls covered both revisions: the original storage assertions failed before the Agent default/common row initializer existed, and the service-policy assertion failed when a caller supplied false before tuttid enforced its own fresh-mode default.
  • Real SQLite coverage verifies create-if-absent, preservation of an existing historical row, and patch-first initialization for both supported patch writers.
  • Service/API coverage verifies daemon-owned mode enforcement, preservation of unrelated flags, atomic-mode routing, no notification for a losing initializer, generated request mapping, and invalid mode rejection.
  • A real daemon black-box test crosses HTTP, generated API binding, service policy, and SQLite for missing/explicit-false fresh inputs and preservation of an existing OS row.
  • Desktop recovery coverage verifies initial GET failure, failure before commit, response loss after commit, and preservation of a concurrently created OS preference.
  • Vertical startup coverage begins with preference resolution and observes the actual windowKind delivered to the launch adapter for fresh, explicit true, explicit false, historical-missing, and read-error states.
  • pnpm check:changed -- --base upstream/main --push-ready: passed 18/18 selected lanes; the repository pre-push gate subsequently passed all 56 selected lanes.
  • pnpm --filter @tutti-os/desktop build: passed, including renderer CSS contract verification.
  • cd services/tuttid && go build ./...: passed.
  • Manual isolated macOS fresh-user launch from the earlier revision persisted {"workspace.standaloneAgentMode":true}, opened view=agent, and retained the selected local project.

Latest review follow-up (4c3a16c)

  • Added a narrow main-to-renderer workspaceUiMode bootstrap that is independent of the current window view. Initialized daemon state overrides it; uninitialized or failed hydration retains it, so unrelated full writes preserve the mode already selected by main.
  • Unified the daemon complete-row update-channel default with Desktop and SQLite on stable. Existing rows remain untouched, and packaged RC alignment remains version-driven.
  • Replaced the serial concurrency claim with a deterministic real-SQLite two-writer test using the sole writer connection as a gate and DB wait-count evidence. It asserts exactly one creator, identical authoritative returns, one complete winning candidate, and passes under the race detector.
  • Executed negative controls for both the renderer bootstrap and SQLite conflict behavior; each new regression test failed under the corresponding faulty implementation and passed after restoration.
  • Final local changed-aware state passed 56/56 push-ready lanes after isolated rerun of a pre-existing rich-text timeout; Desktop tests/typecheck/build, focused Go tests, race coverage, and daemon build also passed.

Platform impact and remaining native gates

The preference decision is platform-neutral, but making the Agent-only shell the fresh-profile default changes the native startup path. This macOS execution host cannot run genuine Windows or Linux GUI startup tests. The following are therefore mandatory release acceptance gates, not optional follow-ups, and are recorded in docs/architecture/desktop-windows.md:

  • Windows packaged build: isolated empty state opens Agent; selecting OS and relaunching restores the OS workspace window.
  • Linux packaged build under the supported X11/Xwayland path: repeat the isolated-state and persisted-OS relaunch checks.
  • macOS: repeat both checks and verify activation with no open windows follows the persisted mode.

The Windows CI build and adapter jobs for the new head are running; they provide build/platform-adapter confidence but do not replace the native GUI acceptance gate.

Checklist

  • Focused change with daemon-owned product policy and Desktop-owned presentation wiring.
  • OpenAPI changed before regenerating Go and TypeScript clients.
  • Durable architecture and local-state documentation updated.
  • Windows impact assessed; remaining native gates are explicit and mandatory.
  • No README or CONTRIBUTING source changed, so multilingual documentation synchronization is not applicable.
  • Commit follows Conventional Commits and includes DCO sign-off.

@tutti-pr-review-bot
tutti-pr-review-bot Bot requested a review from a team August 17, 2026 08:59

@devRickyyy devRickyyy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not find a direct backfill or destructive write for already initialized profiles: explicit true, explicit false, and the legacy absent key remain unchanged on the normal upgrade path.

The cross-cutting concern is that fresh-profile initialization is still a client-side GET followed by a full PUT. The SQLite upsert overwrites feature_flags_json and most other preference columns on conflict, so the new default is neither create-only nor atomic. Besides the first-writer inconsistency called out inline, a concurrent full preference writer could have its configuration replaced by Desktop defaults. An authoritative daemon-side initialize-if-absent operation would address both problems.

Comment thread apps/desktop/src/main/desktopHostPreferences.ts Outdated
Comment thread apps/desktop/src/main/desktopHostPreferences.ts Outdated
Comment thread apps/desktop/src/main/desktopHostPreferences.test.ts
Comment thread docs/architecture/desktop-windows.md Outdated
@mt-hub8
mt-hub8 force-pushed the codex/new-user-os-immersive-default branch from cefbc70 to cf6f42f Compare August 17, 2026 10:58
@tutti-pr-review-bot
tutti-pr-review-bot Bot requested a review from a team August 17, 2026 10:58
Initialize desktop preferences atomically in tuttid so concurrent writers cannot overwrite existing configuration. Preserve explicit and legacy OS modes, reconcile uncertain initialization outcomes, and pass the selected window kind explicitly into startup.

Add SQLite, API, recovery, and vertical startup coverage, and document the required native platform acceptance gates.

Signed-off-by: man tuo <2428184991@qq.com>
@mt-hub8
mt-hub8 force-pushed the codex/new-user-os-immersive-default branch from cf6f42f to 7e304f7 Compare August 17, 2026 10:59
Signed-off-by: man tuo <2428184991@qq.com>
Signed-off-by: man tuo <2428184991@qq.com>

@devRickyyy devRickyyy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复查最新提交后补充 3 个问题:两处配置/口径风险,以及一处并发测试覆盖与 PR 描述不一致。

Comment thread apps/desktop/src/main/desktopHostPreferences.ts Outdated
Comment thread services/tuttid/data/workspace/sqlite_preferences.go Outdated
Comment thread apps/desktop/src/main/host/desktopWorkspaceLaunch.test.ts
Carry the main process workspace mode into renderer bootstrap state so failed first-run persistence cannot be replaced by an unrelated OS-mode write. Align daemon-created preference rows with the stable desktop update default and prove first-writer-wins behavior with real concurrent SQLite writers.

Signed-off-by: man tuo <2428184991@qq.com>
@tutti-pr-review-bot
tutti-pr-review-bot Bot requested a review from a team August 17, 2026 14:55
@mt-hub8

mt-hub8 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

CI note for head 4c3a16c: Go Lint and TypeScript Test Shard 3/3 both failed during Set up job before repository commands ran. GitHub could not download pnpm/action-setup after three attempts (429/502/503 from codeload.github.com). Local push-ready validation reached 56/56 after the unrelated rich-text timeout rerun. This contributor token does not have permission to rerun Actions, so a maintainer needs to rerun the failed jobs.

Copy link
Copy Markdown
Contributor

复查当前 head fe728b7 后,原先的 atomic initialize、main→renderer bootstrap 和真实 SQLite 并发测试已经补齐;这些部分我认为方向是对的。结合启动/交互调用链,仍建议在合入前处理下面几个问题。

[P2] patch-first 仍会用另一套上下文完成整个 profile 初始化

健康启动中,main 会 await preference 初始化后才打开业务窗口,因此 composer/session patch 不会抢在正常初始化之前。但在初始 GET 或 initialize PUT 失败的降级路径中,main 会用内存 fallback 继续开窗口;daemon 恢复后,用户修改 composer defaults 或 local/worktree mode,就可能让 patch 成为第一笔成功写入。

这两个 SQLite patch 路径使用 DefaultDesktopPreferences() 创建完整行,而 Desktop fresh candidate 包含运行时上下文。当前至少存在这些差异:

  • Desktop locale 使用系统 fallback,daemon 默认是 en
  • Desktop minimize animation 是 genie,daemon 默认是 scale
  • packaged RC 的运行时 channel 需要按安装版本对齐,而 daemon 完整默认行是 stable

因此同一个空 profile 在初始化失败后,最终完整配置可能取决于用户先改语言、composer defaults,还是 session launch mode。这个问题不是健康启动主路径,但从当前 recovery 设计和 UI 入口看是可达的。

按本 PR 的最小范围,我更建议:composer/session patch 遇到缺失 preference row 时返回明确的 not-initialized / failed-precondition,让 Desktop 完成初始化后重试。如果产品/API 必须支持 patch-first,则需要明确的 provisional-profile 或真正 canonical initialization contract,不能让局部 patch 静默决定整行其余字段。

[P2] composer patch-first 创建完整行后没有发布完整权威状态

PatchAgentComposerDefaultsForTarget 的 store 现在可能通过 insertDesktopPreferencesIfAbsent 创建整条记录,但 service 只发布 PublishAgentComposerDefaultsChanged,不会发布 preferences.desktop.updated,也不会调用完整 preference change observers。Session-launch patch 则会发布完整 preferences。

所以 composer patch-first 成功后,DB 可能已经是 initialized/Agent/default row,但当前 main 和 renderer 仍保留启动 fallback 的另一套 snapshot;后续 full write 还可能再次覆盖这些字段。

如果保留 patch-first,建议让 store/service 返回 created + authoritative preferences,并在首次创建时发布完整 desktop update/observer。若采用上面的“缺失行拒绝 patch”方案,这个问题会自然消失。

[P2] 未持久化 fallback 不应写 installed-version alignment marker

初始化失败、复读仍不存在时,line 510 仍调用 alignUpdateChannelWithInstalledVersion(options, freshPreferences)。该函数可能记录“当前安装版本已经完成 channel 对齐”,即使 preference row 实际没有写入。

一个可达结果是:packaged RC 的内存 fallback 为 RC并记录 marker,随后 patch-first 用 daemon stable 默认创建行;下次启动因为 marker 已存在而不再把 stable 修正回 RC。建议未确认持久化时只返回内存 candidate,不写 alignment marker。

建议补一条垂直回归

请把现在分散的 recovery、renderer bootstrap、patch-first 测试串成一个真实序列:

空 profile
→ initialize 写前失败
→ 窗口继续打开
→ 用户修改 composer 或 session launch mode
→ 重启
→ 断言 workspace mode、locale、minimizeAnimation、updateChannel

这不是要求本 PR 顺手统一所有 Go/TS defaults;重点是让“缺失记录由谁完成初始化”只有一种可预测语义。另因 Agent mode 会持久化为 true,代码回滚不会自动恢复受影响用户到 OS,PR 中列出的 Windows/Linux/macOS packaged native smoke 仍应作为发布门禁。

Make daemon field-specific patches fail closed when the desktop preferences row is missing. Recover through a typed single-flight Desktop initialization path before renderer mutations, gate migration markers on confirmed durability, and cover the failed-startup-to-restart sequence.

Signed-off-by: man tuo <2428184991@qq.com>
@mt-hub8

mt-hub8 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

@devRickyyy 感谢复查。最新 head 05eacb7ee 已包含修复提交 c836836fd,本轮提出的 3 个 P2 和垂直回归建议均已处理:

  1. patch-first 不再决定完整 profile 上下文

    • SQLite 的 composer-default/session-launch-mode patch 在 preference row 缺失时返回 ErrDesktopPreferencesNotInitialized,不再用 daemon defaults 创建完整行。
    • Desktop renderer 的 mutation 入口先通过 typed IPC 调用 main 的 single-flight ensureInitialized;只有拿到已持久化的权威 preference row 后才执行 patch。
  2. composer patch-first 的状态/事件分叉已消除

    • patch 无法再创建缺失行,因此不存在“DB 已初始化但只发布局部 composer 事件”的路径。
    • service/store 回归测试同时断言缺失行时无局部写入、无 publisher/observer 副作用。
  3. 未持久化 fallback 不再写 installed-version alignment marker

    • 初始化失败后的 Agent candidate 只保留在内存中;migration/alignment 以及 marker 写入只从确认 initialized:true 的 durable snapshot 执行。
    • 回归测试覆盖首次初始化失败时 marker 不存在,以及 mutation 前恢复初始化成功后 marker 才落盘。
  4. 补充了垂直回归序列

    • desktopPreferencesInitialization.integration.test.ts 覆盖:空 profile → 首次 initialize 写前失败 → renderer 保持 Agent 且 DB/marker 仍为空 → session launch patch 触发 durable initialize → patch 成功 → 重启 main + renderer。
    • 最终断言 Agent mode、locale、minimizeAnimation、updateChannel、worktree launch mode 全部保持;测试负向控制曾移除 renderer guard,能够在“patch 必须晚于 durable initialization”处按预期失败。

验证结果:

  • 当前 GitHub head 的 Go Tests、Go Lint、TypeScript Tests/Lint/Typecheck、三个 TS shard、Windows daemon adapters 和 Windows x64 package 均已通过。
  • 本地 Desktop 全量测试:1760 passed、0 failed、2 skipped。
  • 本地 typecheck:37 packages passed。

本次 main merge没有改动上述 26 个修复文件。请基于当前 head 05eacb7ee 复审;当前唯一未通过的 required check 是等待 @tutti-os/tutti-rd 对当前 head 的批准。

@devRickyyy

Copy link
Copy Markdown
Contributor

@mt-hub8 這個 PR 翻默認的那一刻目前沒有任何埋點——發布後將無法區分「被默認分配到 Agent 的新用戶」和「自己切到 Agent 的用戶」,首批 cohort 的歸因會永久丟失(我們已在數據上驗證過自選擇會把留存對比污染 +10pp 級)。

我在 #2457 補了分配蓋章事件,建議你直接吸收進本 PR、同一個版本車發布(單獨發會錯過首批 cohort):

  • 內容:initializeIfAbsent 真創建行之後 Track settings.workspace_ui_mode_initialized(param workspace_ui_mode,取自權威存儲行),wiring 注入 + 三例測試 + 文檔,4 files / +113,無其他行為改動。
  • 吸收方式:單個 commit,基於本 PR 的 head(05eacb7)可乾淨 cherry-pick:
git fetch git@github.com:tutti-os/tutti.git agent/workspace-ui-mode-init-analytics
git cherry-pick 58cd1c2d19e536f6e7b4d8728bd2f6ae5c3b2cbe

吸收後我會關掉 #2457。如果你傾向保持本 PR 純淨、讓 #2457 作為 stacked PR 跟在後面合,也行——但請確保兩者進同一個 release,rc 車正在切(v0.2.27-rc.3)。

Report settings.workspace_ui_mode_initialized only when initializeIfAbsent actually creates the preference row, carrying workspace_ui_mode from the authoritative stored row. The creation result limits invocation to at most once while delivery remains best-effort. This stamps the assigned default so release cohorts can separate default-assigned Agent users from self-selected users and later escapes reported by settings.workspace_ui_mode_changed.

(cherry picked from commit 58cd1c2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: man tuo <2428184991@qq.com>
@mt-hub8

mt-hub8 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

已按 #2457 的首选路径将埋点提交 58cd1c2d 吸收到当前 head c4e169935,并保留原作者、来源 SHA 与 DCO 签署。

本次新增:

  • tuttid 仅在 initializeIfAbsent 实际创建 desktop preference row 时调用 settings.workspace_ui_mode_initialized
  • workspace_ui_mode 从 store 返回的权威行派生为 agent / os,不信任可能输掉并发竞争的 caller candidate。
  • 已有行、replace write 均不上报;后续主动切换继续由 settings.workspace_ui_mode_changed 记录。
  • 文档口径从不准确的 durable “exactly once” 收紧为真实语义:row creation 限制 daemon invocation 至多一次,但 analytics disabled、进程/transport 失败可丢失且不重放;该事件表示首次 preference 初始化,不等同于 account registration。
  • 服务测试覆盖 authoritative Agent、authoritative OS、已有行和 replace 四个分区。OS 负向控制将生产映射临时改为 Agent 后,测试按预期在 workspace_ui_mode = agent, want os 处失败,恢复后通过。

验证:

  • go test ./service/preferences -count=1 通过。
  • pnpm check:changed -- --push-ready --base upstream/main 最终 38/38 lanes 通过。
  • 首轮唯一失败是与 upstream/main 内容完全一致的 agent-daemon 时序测试;两个精确用例单独通过,failed-only 重跑后整 lane 通过。

当前新 head 会重新触发 CI 与 External PR Review Gate;请 @tutti-os/tutti-rd 基于 c4e169935 重新审批。

@mt-hub8

mt-hub8 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Remote CI follow-up for c4e1699358f6392abfc5a0b55cb257298be0726b:

  • All Go, TypeScript, lint, typecheck, tooling, and the dedicated Windows Daemon Adapters workflow passed.
  • Windows Desktop Alpha failed only because TestTuttidBlackBoxWorkspaceTerminalWebSocketExitFrameCarriesExitCode timed out waiting 20s for daemon health while startup was still materializing built-in/runtime assets.
  • The exact same integration test passed on the same PR merge commit in the parallel Windows Daemon Adapters workflow (services/tuttid/integration completed successfully in 15.278s), and the failing test file is unchanged from upstream/main. This is consistent with runner/startup timing variance, not the preferences analytics change.
  • I attempted to rerun the failed job, but external-contributor permissions cannot rerun repository Actions.

Could a maintainer please rerun the failed Windows Desktop Alpha job and re-approve the external PR gate for the current head?

Signed-off-by: man tuo <2428184991@qq.com>
@mt-hub8

mt-hub8 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Latest-head status (4f4d440959a2c7fd3453509607af82db29c178dc):

  • Synced the branch with current upstream/main using a DCO-signed merge commit.
  • Local push-ready validation passed: 19/19 lanes.
  • Fresh remote run passed all 12 code/platform checks, including Go Tests, Go Lint, all TypeScript checks, Windows Daemon Adapters, and Windows Desktop Alpha.
  • The prior Windows health timeout did not reproduce and is confirmed as runner/startup timing variance.

The only remaining blocker is the external-contributor review gate. Please have @tutti-os/tutti-rd approve the current head.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants