Bläddra i källkod

Merge remote-tracking branch 'origin/master' into codex/pr-3111-review

# Conflicts:
#	scripts/ci-workflow.spec.ts
creatixchu 3 veckor sedan
förälder
incheckning
16a7ead9ab
100 ändrade filer med 1072 tillägg och 240 borttagningar
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
  2. 0 0
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
  3. 0 0
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md
  7. 6 0
      .agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.i18n.yaml
  8. 70 0
      .agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.md
  9. 70 0
      .agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.zh.md
  10. 6 0
      .agents/notes/implemented/architecture/2026-08-26-models-page-extension-slots.i18n.yaml
  11. 33 0
      .agents/notes/implemented/architecture/2026-08-26-models-page-extension-slots.md
  12. 33 0
      .agents/notes/implemented/architecture/2026-08-26-models-page-extension-slots.zh.md
  13. 6 0
      .agents/notes/implemented/bug-fix/2026-08-26-composer-gesture-editable-gate.i18n.yaml
  14. 25 0
      .agents/notes/implemented/bug-fix/2026-08-26-composer-gesture-editable-gate.md
  15. 25 0
      .agents/notes/implemented/bug-fix/2026-08-26-composer-gesture-editable-gate.zh.md
  16. 6 0
      .agents/notes/implemented/bug-fix/2026-08-26-pi-ai-grant-payload-json-image.i18n.yaml
  17. 29 0
      .agents/notes/implemented/bug-fix/2026-08-26-pi-ai-grant-payload-json-image.md
  18. 29 0
      .agents/notes/implemented/bug-fix/2026-08-26-pi-ai-grant-payload-json-image.zh.md
  19. 6 0
      .agents/notes/implemented/bug-fix/2026-08-26-question-drafts-survive-session-switch.i18n.yaml
  20. 39 0
      .agents/notes/implemented/bug-fix/2026-08-26-question-drafts-survive-session-switch.md
  21. 39 0
      .agents/notes/implemented/bug-fix/2026-08-26-question-drafts-survive-session-switch.zh.md
  22. 2 2
      .agents/notes/implemented/feature/2026-08-18-settings-font-size-control.i18n.yaml
  23. 0 1
      .agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md
  24. 0 1
      .agents/notes/implemented/feature/2026-08-18-settings-font-size-control.zh.md
  25. 6 0
      .agents/notes/implemented/feature/2026-08-26-web-trigger-menu-presentation-polish.i18n.yaml
  26. 35 0
      .agents/notes/implemented/feature/2026-08-26-web-trigger-menu-presentation-polish.md
  27. 35 0
      .agents/notes/implemented/feature/2026-08-26-web-trigger-menu-presentation-polish.zh.md
  28. 2 2
      .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml
  29. 2 2
      .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md
  30. 2 2
      .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md
  31. 2 2
      .agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml
  32. 4 3
      .agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md
  33. 4 3
      .agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md
  34. 6 7
      .github/workflows/build-exe-for-python-sdk.yml
  35. 1 1
      apps/web/tests/command-image-envelope.expected.e2e.ts
  36. 1 1
      apps/web/tests/expected/github-ready-review/conversation.expected.md
  37. 1 1
      apps/web/tests/expected/goal-command-presentation/ui.expected.md
  38. 1 1
      apps/web/tests/expected/markdown-cjk-strong/ui.expected.md
  39. 1 1
      apps/web/tests/expected/markdown-images/ui.expected.md
  40. 1 1
      apps/web/tests/expected/markdown-inline-code-links/ui.expected.md
  41. 1 1
      apps/web/tests/expected/math-rendering/ui.expected.md
  42. 8 7
      apps/web/tests/expected/reference-composer/menu.expected.md
  43. 1 1
      apps/web/tests/expected/reference-composer/order.expected.md
  44. 1 1
      apps/web/tests/expected/skill-user-invoke/ui.expected.md
  45. 1 1
      apps/web/tests/expected/stats-paged-history/ui.expected.md
  46. 1 1
      apps/web/tests/expected/steer-all/mid-steer.expected.md
  47. 1 1
      apps/web/tests/expected/steer-all/settled.expected.md
  48. 1 1
      apps/web/tests/goal-bar.e2e.ts
  49. 3 3
      apps/web/tests/image-display.expected.e2e.ts
  50. 4 4
      apps/web/tests/onboarding-deepseek-config.e2e.ts
  51. 7 7
      apps/web/tests/permission-policy-context.e2e.ts
  52. 37 28
      apps/web/tests/preview-boot.e2e.ts
  53. 15 0
      apps/web/tests/question-composer.e2e.ts
  54. 13 13
      apps/web/tests/reference-composer.e2e.ts
  55. 17 0
      apps/web/tests/settings-chrome.e2e.ts
  56. 1 1
      apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md
  57. 1 1
      apps/web/tests/startup-auto-selection.e2e.ts
  58. 2 2
      apps/web/tests/startup-rpc-budget.e2e.ts
  59. 3 3
      apps/web/tests/subagent-conversation.e2e.ts
  60. 1 1
      apps/web/tests/subagent-interrupt-ui.e2e.ts
  61. 1 3
      apps/web/tests/submission-echo.e2e.ts
  62. 12 2
      apps/web/tests/support.ts
  63. 2 2
      docs/capability-seams.i18n.yaml
  64. 12 2
      docs/capability-seams.md
  65. 12 2
      docs/capability-seams.zh.md
  66. 2 2
      docs/config-catalog.i18n.yaml
  67. 3 2
      docs/config-catalog.md
  68. 2 1
      docs/config-catalog.zh.md
  69. 2 2
      docs/event-producer-consumer.i18n.yaml
  70. 4 4
      docs/event-producer-consumer.md
  71. 4 4
      docs/event-producer-consumer.zh.md
  72. 2 2
      docs/module-graph.i18n.yaml
  73. 30 18
      docs/module-graph.md
  74. 30 18
      docs/module-graph.zh.md
  75. 2 2
      docs/subsystems/credentials.i18n.yaml
  76. 43 3
      docs/subsystems/credentials.md
  77. 43 3
      docs/subsystems/credentials.zh.md
  78. 2 2
      docs/subsystems/persistence.i18n.yaml
  79. 1 1
      docs/subsystems/persistence.md
  80. 1 1
      docs/subsystems/persistence.zh.md
  81. 2 2
      docs/subsystems/settings.i18n.yaml
  82. 50 0
      docs/subsystems/settings.md
  83. 50 0
      docs/subsystems/settings.zh.md
  84. 2 2
      docs/subsystems/slots.i18n.yaml
  85. 2 0
      docs/subsystems/slots.md
  86. 2 0
      docs/subsystems/slots.zh.md
  87. 2 2
      docs/subsystems/workspace.i18n.yaml
  88. 34 0
      docs/subsystems/workspace.md
  89. 34 0
      docs/subsystems/workspace.zh.md
  90. 0 5
      knip.json
  91. 2 2
      packages/api/README.i18n.yaml
  92. 2 1
      packages/api/README.md
  93. 3 2
      packages/api/README.zh.md
  94. 2 2
      packages/api/remotes/README.i18n.yaml
  95. 1 1
      packages/api/remotes/README.md
  96. 1 1
      packages/api/remotes/README.zh.md
  97. 2 0
      packages/api/remotes/package.json
  98. 15 3
      packages/api/remotes/src/client/index.ts
  99. 3 0
      packages/api/remotes/tsconfig.client.json
  100. 0 27
      packages/api/session-controller/tests/fake-api.client.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
-2026-07-10-single-file-executable-sdk-runtime-distribution.md: 8731528b9ae600bb8bfe12738669f3a84c11a06b
-2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 755b4bd7ddbe9b88b4f40b8a3ae7419f746b8dde
+2026-07-10-single-file-executable-sdk-runtime-distribution.md: c152345772826ec4e2dbfd238726c429418c7897
+2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: ea5e457afd761cb5071f8b584ef10fa7ffaa8210

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md


+ 2 - 2
.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-web-config-plane.md
-2026-07-30-web-config-plane.md: d5bd9c05e8352536c5c6f8b265db7dbd56a4fb84
-2026-07-30-web-config-plane.zh.md: 3c7f801766b1a6c197cc208a3c4a030b8aaac771
+2026-07-30-web-config-plane.md: c817071ed17554d06249aa6893ed759bea5d72d0
+2026-07-30-web-config-plane.zh.md: 0b15e329340051d0f63e8a5b1f8c70a29a2138d2

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md

@@ -12,7 +12,7 @@ The request-level configuration seam made LLM adapter configuration restart-free
 
 ## Decision
 
-**Wire domains on the compiled RPC map, rejections as codes, owner events forwarded verbatim.** `settings.describe/openDocument/update/replace/mutate`, `credentials.describe/set/unset`, `llm.providers`, and `llm.models` join `RpcMethodMap`, so the compiler-locked wiring sites keep schema, handler, and client in lockstep. Seam rejections fold into `settings-rejected {ns}` / `credential-rejected {ref}` business errors, while clients subscribe to forwarded settings, credentials, and LLM owner events and converge without polling ([forwarded Remote events](2026-08-10-remote-event-delivery.md)). Connection authenticates settings reads, native actions, writes, `pickDirectory`, `openPath`, and every other Host operation with one browser session; Host/Origin failures still return 403 before identity is checked.
+**Configuration calls use their owning wire implementation, rejections as codes, and owner events forwarded verbatim.** `@deepseek-ai/dsh-api-settings-controller` owns generated Remote methods for `settings/describe`, `settings/update`, `settings/replace`, `settings/mutate`, and `credentials/describe|set|unset`; `settings.openDocument` and the `llm.*` methods remain in `RpcMethodMap`. Provider absence retains the configuration API's actionable `internal` diagnostic, while seam rejections retain `settings-rejected {ns}` / `settings-conflict {ns, expected, actual}` / `credential-rejected {ref}`. Clients subscribe to forwarded settings, credentials, and LLM owner events and converge without polling ([forwarded Remote events](2026-08-10-remote-event-delivery.md)). Connection authenticates generated Remote methods and API Proxy fallbacks with the same browser session; Host/Origin failures still return 403 before identity is checked.
 
 **`describe()` grows layers and structural secret redaction.** `SettingsDescriptor` carries `base`/`user` beside the effective value, so the form marks "overridden" by presence in the user layer, not value inequality (an override *equal* to the base is still an override). `describe({ redactSecrets: true })` — mandatory at every wire face — strips `role('secret')` subtrees from all three layers via a pure structural walk of the schema (object/dict/array containers; a secret-role subtree is one opaque leaf) and enumerates the stripped slots as `{path, set}`, so a page can render write-only inputs without ever receiving a value.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 ## 决策
 
-**wire 领域挂上编译期 RPC 映射,拒绝落为错误码,owner 事件原样转发。**`settings.describe/openDocument/update/replace/mutate`、`credentials.describe/set/unset`、`llm.providers` 与 `llm.models` 一同加入 `RpcMethodMap`,由编译器锁定的接线位点让 schema、处理器与客户端保持步调一致。seam 侧拒绝折叠为业务错误,客户端则订阅转发的 settings、credentials 与 LLM owner 事件,无需轮询即可收敛(见[转发的 Remote 事件](2026-08-10-remote-event-delivery.zh.md))。Connection 用一个浏览器会话认证 settings 读取、原生操作、写入、`pickDirectory`、`openPath` 与其他所有 Host 操作;Host/Origin 失败仍会在身份校验前返回 403。
+**配置调用使用其所属的 wire 实现,拒绝落为错误码,owner 事件原样转发。**`@deepseek-ai/dsh-api-settings-controller` 持有 `settings/describe`、`settings/update`、`settings/replace`、`settings/mutate` 与 `credentials/describe|set|unset` 的生成 Remote 方法;`settings.openDocument` 和 `llm.*` 方法仍位于 `RpcMethodMap`。provider 缺失时保留配置 API 可操作的 `internal` 诊断,seam 拒绝则保留 `settings-rejected {ns}`/`settings-conflict {ns, expected, actual}`/`credential-rejected {ref}`。Client 订阅转发的 settings、credentials 与 LLM owner 事件,无需轮询即可收敛(见[转发的 Remote 事件](2026-08-10-remote-event-delivery.zh.md))。Connection 使用同一个浏览器会话认证生成的 Remote 方法与 API Proxy 回退;Host/Origin 失败仍会在身份校验前返回 403。
 
 **`describe()` 增加分层与结构化 secret 脱敏。**`SettingsDescriptor` 在生效值之外携带 `base`/`user`,表单据此按「字段是否出现在用户层」来标记「已覆盖」,而非按值是否不等(与 base *相等*的覆盖仍然是覆盖)。`describe({ redactSecrets: true })`——在每个 wire 面都强制启用——经由对 schema 的纯结构遍历(object/dict/array 容器;secret 角色子树整体是一个不透明叶节点)从全部三层剥除 `role('secret')` 子树,并把剥除的槽位枚举为 `{path, set}`,页面因此不必收到任何值就能渲染只写输入框。
 

+ 6 - 0
.agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.md
+2026-08-25-persistence-latency-and-page-size.md: 27eb58cc551f01c48361a3af3224eb8b12592a00
+2026-08-25-persistence-latency-and-page-size.zh.md: 24ab1835cc313cd617d665a0c52a399d505069ea

+ 70 - 0
.agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.md

@@ -0,0 +1,70 @@
+# Agent Note: Persistence compression latency and SQLite page size
+
+Status: implemented
+
+English | [中文](2026-08-25-persistence-latency-and-page-size.zh.md)
+
+## Problem
+
+The physical persistence optimizations need to reduce retained storage without moving disproportionate work into full writes, reads, or session forks. The original 105-session corpus showed that JSONL level-19 compression made full writes and forks more than twice as slow. The earlier SQLite page-size experiment predated shared-dictionary row compression and showed negligible savings, so it did not establish the best page size for the current row distribution.
+
+The decision needs evidence from more varied sessions, including long event streams and payloads outside the original corpus. The expanded corpus contains 501 real sessions, 16,153,332 logical events, and 2,002,145,570 bytes of serialized event data.
+
+## Decision
+
+### Storage encoding stays physical and independently decodable
+
+JSONL stores strictly increasing `sourceEventSeqs` as mixed scalar values and inclusive ranges; other orders remain verbatim. SQLite stores the same arrays as tagged zigzag-delta or `(start, count)` varints, choosing the smaller encoding. Both readers restore the original `number[]` before exposing an event.
+
+SQLite uses an internal integer `sessions.id` and keeps the public session id once in `sessions.session_key`, so event rows and their primary key do not repeat a text identifier. Each `events.data` value remains independently decodable: the writer tries level-3 Zstandard with the packaged 64 KiB raw-content dictionary and retains SQLite text when compression is not smaller. The dictionary bytes are part of schema 19 and a test pins their SHA-256 digest; replacing them requires another schema-version bump.
+
+### JSONL uses the standard Zstandard level
+
+The JSONL writer keeps one checksummed Zstandard frame per durable append batch but uses the compressor's standard level. Lossless `sourceEventSeqs` range encoding remains active. Frames stay independently decodable for suffix reads and torn-tail recovery; only the expensive level-19 search is removed.
+
+### New SQLite databases use 64 KiB pages
+
+The SQLite provider sets `page_size=65536` before initializing a pristine schema-19 database. An established schema-19 database retains its current page size because SQLite ignores the pragma after allocation.
+
+The page size is part of schema 19's fixed physical layout and is applied through the package's closed SQL resources like the other fixed SQLite pragmas.
+
+### Expanded benchmark
+
+Each candidate was rebuilt five times from the same 501-session corpus with 512-event append batches. Their order rotates between rounds so every candidate occupies each run position once. Each build runs three complete and suffix-read sweeps. For each displayed metric, the highest and lowest build are discarded and the remaining three values are averaged. Complete and suffix read times cover one sweep over all sessions, and fork time covers all 501 sessions.
+
+| Backend | Stored size | Full write | Full read | Suffix read | Fork |
+| --- | ---: | ---: | ---: | ---: | ---: |
+| JSONL `master` | 172.43 MB | 200.902 s | 8.033 s | 24.479 s | 72.670 s |
+| JSONL with provenance ranges | 148.15 MB (-14.1%) | 197.281 s (-1.8%) | 7.799 s (-2.9%) | 24.582 s (+0.4%) | 72.308 s (-0.5%) |
+| JSONL with provenance ranges and level 19 | 130.22 MB (-24.5%) | 329.442 s (+64.0%) | 7.764 s (-3.3%) | 24.454 s (-0.1%) | 166.177 s (+128.7%) |
+| SQLite `master` (schema 17) | 438.31 MB | 69.632 s | 8.211 s | 0.546 s | 64.290 s |
+| SQLite with all physical optimizations and 64 KiB pages | 233.18 MB (-46.8%) | 87.656 s (+25.9%) | 9.155 s (+11.5%) | 0.575 s (+5.3%) | 79.417 s (+23.5%) |
+
+Relative to standard-level frames with provenance ranges, level 19 saves another 12.1% of the JSONL bytes but increases full-write time by 67.0% and fork time by 129.8%. Its complete and suffix reads change by -0.4% and -0.5%. The extra search therefore benefits retained size without improving the latency-sensitive operations enough to offset its repeated encoding cost.
+
+An otherwise identical SQLite build isolates the page-size effect: 4 KiB pages use 256.97 MB and 64 KiB pages use 233.18 MB (-9.26%). The `events` table's unused page bytes fall from 30.25 MB to 6.95 MB, while the index changes from 5.92 MB to 6.03 MB. In the paired run, full write, full read, and suffix read change by -0.5%, -0.4%, and -3.8%; fork changes by -14.8%. The space gain therefore comes from better large-row page utilization rather than a smaller index or omitted data, without a measured latency regression.
+
+## Alternatives considered
+
+**Keep JSONL level 19.** Rejected. On the expanded corpus it saves another 12.1% relative to default-level frames but increases full-write time by 67.0% and fork time by 129.8%, while complete and suffix reads differ by less than 1%. Default-level frames plus provenance ranges retain a 14.1% size reduction relative to master without a material latency regression.
+
+**Compress one whole JSONL log as a single frame.** Rejected. It improves cross-batch compression but makes suffix reads decompress from the start and removes batch-local torn-tail recovery.
+
+**Keep 4 KiB SQLite pages.** Rejected for pristine databases. The current compressed-row distribution retains 9.26% more bytes because large compressed records leave more unusable space across 4 KiB B-tree pages. Existing databases keep their page size to avoid a historical rewrite.
+
+**Remove ROWID from `events`.** Rejected. The composite primary key becomes the table B-tree key and repeats through internal pages; the 105-session comparison produced a larger database than ordinary ROWID tables.
+
+**Deduplicate event content.** Rejected. Message restatements and tool arguments can be reconstructed only under assumptions that compaction, retries, and pruning may invalidate. Physical compression preserves every event without adding reconstruction semantics.
+
+**Use per-session SQLite files or DuckDB.** Rejected for the hot store. Per-session files lose cross-session queries, while DuckDB's OLAP write model fits cold batch analysis rather than durable append batches and low-latency suffix reads.
+
+## Consequences
+
+JSONL keeps the low-cost provenance optimization without the level-19 write and fork penalty. SQLite exchanges approximately 5–26% more time across the measured operations for a 46.8% retained-size reduction; its full write remains materially faster than JSONL, and its suffix read remains much faster. Its complete read and fork are slightly slower than default-level JSONL on this expanded corpus.
+
+New SQLite databases use 64 KiB WAL frames and cache pages. Small databases may reserve more bytes for sparsely populated schema and metadata pages, while the measured multi-session workload gains substantially better `events` page utilization. Schema 19 rejects every other schema version rather than migrating it.
+
+## Related
+
+- [sqlite-physical-chunk-row-compression](2026-08-18-sqlite-physical-chunk-row-compression.md) — owns the packed row model; its earlier page-size conclusion applies to the pre-dictionary layout.
+- [zstandard-jsonl-session-logs](2026-07-19-zstandard-jsonl-session-logs.md) — owns the checksummed frame-per-batch container and the standard compressor-level policy restored here.

+ 70 - 0
.agents/notes/implemented/architecture/2026-08-25-persistence-latency-and-page-size.zh.md

@@ -0,0 +1,70 @@
+# Agent Note: 持久化压缩延迟与 SQLite page size
+
+Status: implemented
+
+[English](2026-08-25-persistence-latency-and-page-size.md) | 中文
+
+## 问题
+
+物理持久化优化需要减少保留存储,同时不能把不成比例的工作转移到完整写入、读取或会话 fork。原有的 105 会话语料显示,JSONL level-19 压缩会让完整写入与 fork 耗时增加一倍以上。此前的 SQLite page-size 实验早于共享字典行压缩,所得空间收益可以忽略,因此无法确定当前行分布的最佳 page size。
+
+该决策需要来自更多样会话的证据,包括长事件流与原语料之外的 payload。扩展后的语料包含 501 个真实会话、16,153,332 个逻辑事件与 2,002,145,570 字节序列化事件数据。
+
+## 决策
+
+### 存储编码保持为物理层行为并可独立解码
+
+JSONL 把严格递增的 `sourceEventSeqs` 存为标量值与闭区间的混合数组,其他顺序保持原样。SQLite 把同一数组存为带 tag 的 zigzag-delta 或 `(start, count)` varint,并选择更小的编码。两个读取方都会在暴露事件前还原原始 `number[]`。
+
+SQLite 使用内部整数 `sessions.id`,并只在 `sessions.session_key` 中保留一次公开会话 id,使事件行及其主键不再重复文本标识。每个 `events.data` 值仍可独立解码:写入方尝试用打包的 64 KiB raw-content 字典执行 level-3 Zstandard 压缩,结果不更小时保留 SQLite 文本。字典字节属于 schema 19,测试固定其 SHA-256 摘要;替换字典需要再次提升 schema 版本。
+
+### JSONL 使用 Zstandard 标准级别
+
+JSONL 写入方继续为每个持久 append 批次写入一个带 checksum 的 Zstandard frame,但使用压缩器的标准级别。无损 `sourceEventSeqs` 区间编码继续生效。各 frame 仍可独立解码,以支持后缀读取与撕裂尾部恢复;只移除昂贵的 level-19 搜索。
+
+### 新建 SQLite 数据库使用 64 KiB page
+
+SQLite 提供方在初始化全新 schema-19 数据库前设置 `page_size=65536`。SQLite 在 page 已分配后会忽略该 pragma,因此已有 schema-19 数据库保留其当前 page size。
+
+Page size 属于 schema 19 的固定物理布局,并与其他固定 SQLite pragma 一样通过包内封闭的 SQL 资源应用。
+
+### 扩展基准
+
+每个候选方案都从同一份 501 会话语料独立重建五次,每个 append 批次包含 512 个事件。各轮轮换执行顺序,使每个候选方案在每个运行位置各出现一次。每次重建执行三轮完整读取与后缀读取。下表中的每项指标都去掉最高与最低的一次重建,再平均其余三次。完整读取与后缀读取耗时覆盖对全部会话的一轮扫描,fork 耗时覆盖全部 501 个会话。
+
+| 后端 | 存储大小 | 完整写入 | 完整读取 | 后缀读取 | Fork |
+| --- | ---: | ---: | ---: | ---: | ---: |
+| JSONL `master` | 172.43 MB | 200.902 s | 8.033 s | 24.479 s | 72.670 s |
+| JSONL + 来源区间 | 148.15 MB (-14.1%) | 197.281 s (-1.8%) | 7.799 s (-2.9%) | 24.582 s (+0.4%) | 72.308 s (-0.5%) |
+| JSONL + 来源区间 + level 19 | 130.22 MB (-24.5%) | 329.442 s (+64.0%) | 7.764 s (-3.3%) | 24.454 s (-0.1%) | 166.177 s (+128.7%) |
+| SQLite `master`(schema 17) | 438.31 MB | 69.632 s | 8.211 s | 0.546 s | 64.290 s |
+| SQLite + 全部物理优化 + 64 KiB page | 233.18 MB (-46.8%) | 87.656 s (+25.9%) | 9.155 s (+11.5%) | 0.575 s (+5.3%) | 79.417 s (+23.5%) |
+
+相对使用来源区间的标准级别 frame,level 19 可再减少 12.1% 的 JSONL 字节,但会让完整写入增加 67.0%、fork 增加 129.8%;完整读取与后缀读取分别变化 -0.4% 与 -0.5%。因此,更深入的搜索只改善保留体积,无法通过延迟敏感操作的收益抵消反复付出的编码成本。
+
+其余条件相同的 SQLite 重建可单独观察 page-size 影响:4 KiB page 使用 256.97 MB,64 KiB page 使用 233.18 MB(-9.26%)。`events` 表的 page 内未使用字节从 30.25 MB 降至 6.95 MB,索引则从 5.92 MB 变为 6.03 MB。在该成对运行中,完整写入、完整读取与后缀读取分别变化 -0.5%、-0.4% 与 -3.8%,fork 变化 -14.8%。因此,空间收益来自更高的大记录 page 利用率,而不是索引缩小或数据省略,并且没有测得延迟退化。
+
+## 考虑过的替代方案
+
+**保留 JSONL level 19。** 不予采用。在扩展语料上,它相对默认级别 frame 可再减少 12.1%,却让完整写入增加 67.0%、fork 增加 129.8%,而完整读取与后缀读取的差异都不足 1%。默认级别 frame 配合来源区间后,相对 master 仍能缩小 14.1%,且没有实质性延迟退化。
+
+**把整份 JSONL 日志压成单个 frame。** 不予采用。该方案可改善跨批次压缩,但后缀读取必须从头解压,也会失去按批次恢复撕裂尾部的能力。
+
+**新建 SQLite 数据库继续使用 4 KiB page。** 不予采用。当前压缩行分布会在 4 KiB B-tree page 之间留下更多不可用空间,使保留字节增加 9.26%。已有数据库保留其 page size,避免改写历史数据。
+
+**从 `events` 移除 ROWID。** 不予采用。复合主键会成为表 B-tree 键并在内部 page 中重复;105 会话对比所得数据库大于使用普通 ROWID 的表。
+
+**对事件内容去重。** 不予采用。消息复述与工具参数只能在依赖重建假设时删除,而 compaction、重试和修剪可能让这些假设失效。物理压缩保留每个事件,不增加重建语义。
+
+**使用逐会话 SQLite 文件或 DuckDB。** 不用于热存储。逐会话文件会失去跨会话查询,DuckDB 的 OLAP 写入模型则更适合冷批量分析,而不是持久 append 批次与低延迟后缀读取。
+
+## 后果
+
+JSONL 保留低成本来源优化,同时避开 level-19 的写入与 fork 代价。SQLite 以实测各项操作约 5–26% 的额外耗时换取 46.8% 的保留体积缩减;其完整写入仍明显快于 JSONL,后缀读取也仍快得多。在这份扩展语料上,完整读取与 fork 略慢于默认级别 JSONL。
+
+新建 SQLite 数据库使用 64 KiB WAL frame 与 cache page。小型数据库可能为稀疏的 schema 与元数据 page 预留更多字节,而实测的多会话工作负载显著改善了 `events` page 利用率。Schema 19 会拒绝其他所有 schema 版本,而不是迁移它们。
+
+## 相关资料
+
+- [sqlite-physical-chunk-row-compression](2026-08-18-sqlite-physical-chunk-row-compression.zh.md) — 定义打包行模型;其此前的 page-size 结论适用于共享字典之前的布局。
+- [zstandard-jsonl-session-logs](2026-07-19-zstandard-jsonl-session-logs.zh.md) — 定义带 checksum 的按批次 frame 容器,以及本笔记恢复的标准压缩级别策略。

+ 6 - 0
.agents/notes/implemented/architecture/2026-08-26-models-page-extension-slots.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-26-models-page-extension-slots.md
+2026-08-26-models-page-extension-slots.md: 9983c669b7838de00d49a634a54e5d115ef4945a
+2026-08-26-models-page-extension-slots.zh.md: 639654ea4b912d43a95f0a9295969523f4e1c5f8

+ 33 - 0
.agents/notes/implemented/architecture/2026-08-26-models-page-extension-slots.md

@@ -0,0 +1,33 @@
+# Agent Note: Models-page extension slots
+
+Status: implemented
+
+English | [中文](2026-08-26-models-page-extension-slots.zh.md)
+
+## Problem
+
+Provider sign-in for the pi-ai catalog (GitHub Copilot, OpenAI accounts) is moving out of the product into an optional out-of-tree plugin for provider terms-of-service reasons. The plugin needs its sign-in button and attempt UI inside the Models page's provider cards — the surface where a user meets a provider — but `ui-settings-models` rendered its cards from closed code: the only integration path was editing this package, which an external plugin cannot do, and the page's one open seam (`settings.section`) can only add a whole separate page.
+
+## Decision
+
+`ui-settings-models` declares two SlotMap seats in `src/client/slot-contract.ts`, claims them as `children` of its `settings.section` registration, and re-exports their types from `./client` so an out-of-tree plugin can merge them with a type-only import.
+
+`settings.models.provider-card` is `keyed` with `entryKey = ConfigurableProviderView.settingsNs`: one registration under an adapter family's settings namespace receives every card of that family — shipped catalog routes, adopted directory rows, and hand-declared routes alike — while the section never interprets the key. The key domain stays the open string space (no `keyProps` table) because hand-declared route ids are user-chosen at runtime. The seat dispatches on every card that shows a directory row: a saved row's card, its first-run setup posture, and the add-provider draft (its dormant row, `configured: false` in practice), which is where sign-in matters most — the user has just met the provider and holds no key. The hand-declared draft card has no directory row before saving and dispatches nothing. Owner props carry the row's `ConfigurableProviderView`, its `configured` join, and its confirmed api-key credential state (`keyConfigured`, which the first consumer uses to withhold sign-in beside a stored key); nothing more has a current consumer.
+
+`settings.models.footer` is a `list` seat after the rows and the add controls, for section-level extension content such as orphaned-record management.
+
+Without registrants both seats render nothing, so the shipped page is pixel-identical to before.
+
+## Alternatives considered
+
+**A `list` seat with self-filtering registrants instead of keying.** Every registrant would render (and return null) on every card, and two plugins could silently interleave UI inside one family's cards. Keying by namespace gives one accountable extension owner per adapter family and zero wasted dispatches, and reuses the exact pairing rationale of `settings.plugin.item`.
+
+**Keying by provider route id.** Route ids are dynamic — hand-declared routes are named by users at runtime — so a plugin could not register ahead of the rows it wants and would have to churn registrations as the directory changes.
+
+**A `chain` seat replacing the whole card body.** No current consumer needs to replace the editor; the sign-in surface is additive. A takeover contract would also make the section's layout a compatibility surface. A chain can still be added later without disturbing these seats.
+
+**Keeping the sign-in UI wired inside `ui-settings-models` (the pre-plugin design).** Ships the terms-of-service-sensitive surface in the product, which is the outcome this extension point exists to avoid.
+
+## Consequences
+
+An out-of-tree plugin can now integrate per-family card UI into the Models page with no product edits; `llm-pi-ai-oauth` is the first consumer. The cost is a public contract: `ProviderCardExtrasOwnerProps` exposes `ConfigurableProviderView` at the `./client` boundary, and the dispatch sites (saved card, setup posture, add draft, footer) become behavior extensions rely on. Per adapter family the keyed cell renders one owner at a time: a second registration under the same namespace at the same priority is refused by the registry, while a different priority deliberately shadows it (the lowest-priority entry renders) — the slot kit's standard override channel, never a silent merge.

+ 33 - 0
.agents/notes/implemented/architecture/2026-08-26-models-page-extension-slots.zh.md

@@ -0,0 +1,33 @@
+# Agent Note: Models 页扩展插槽
+
+Status: implemented
+
+[English](2026-08-26-models-page-extension-slots.md) | 中文
+
+## Problem
+
+出于提供方服务条款的考虑,pi-ai catalog 的提供方登录(GitHub Copilot、OpenAI 账号)正从产品中移出,改由一个可选的仓库外插件承担。该插件需要把登录按钮与登录过程 UI 放进 Models 页的提供方卡片——用户与提供方相遇的界面——但 `ui-settings-models` 的卡片由封闭代码渲染:唯一的集成途径是修改本包,外部插件做不到;页面仅有的开放缝隙(`settings.section`)只能新增一整个独立页面。
+
+## Decision
+
+`ui-settings-models` 在 `src/client/slot-contract.ts` 声明两个 SlotMap 席位,在其 `settings.section` 注册中以 `children` 认领它们,并从 `./client` 再导出其类型,使仓库外插件通过 type-only import 即可获得类型合并。
+
+`settings.models.provider-card` 为 `keyed`,`entryKey = ConfigurableProviderView.settingsNs`:以某适配器家族的 settings namespace 注册一次,即可收到该家族的全部卡片——内置 catalog 路由、从目录采纳的行、手工声明的路由一视同仁——而分区从不解释这个 key。键域保持开放字符串空间(不设 `keyProps` 表),因为手工声明的路由 id 由用户在运行时命名。该席位在每张展示目录行的卡片上分发:已保存行的卡片、其首次运行 setup 形态、以及「添加提供方」草稿卡(其休眠行,实际为 `configured: false`)——草稿卡正是登录价值最大的时刻:用户刚遇到该提供方、手中还没有密钥。手工声明的草稿卡在保存前没有目录行,不分发。Owner props 携带该行的 `ConfigurableProviderView`、其 `configured` 合并结果与已确认的 api-key 凭据状态(`keyConfigured`,首个消费者用它在已存密钥旁抑制登录入口);更多字段没有现役消费者。
+
+`settings.models.footer` 为 `list` 席位,位于行列表与新增控件之后,承载孤儿记录管理这类分区级扩展内容。
+
+没有注册方时两个席位均不渲染,产品页面与之前逐像素一致。
+
+## Alternatives considered
+
+**用 `list` 席位、由注册方自行筛选,替代按键分发。**每个注册方都会在每张卡片上渲染(再返回 null),且两个插件可能在同一家族的卡片里静默交错 UI。按 namespace 分发让每个适配器家族有唯一可问责的扩展所有者、零浪费分发,并完全复用 `settings.plugin.item` 的配对理由。
+
+**按提供方路由 id 分发。**路由 id 是动态的——手工声明的路由由用户在运行时命名——插件无法先于目标行注册,还得随目录变化反复重注册。
+
+**用 `chain` 席位整体接管卡片。**没有现役消费者需要替换编辑器;登录界面是加法。接管契约还会让分区布局成为兼容面。将来仍可在不动这两个席位的前提下追加 chain。
+
+**把登录 UI 继续织在 `ui-settings-models` 里(插件化之前的设计)。**会把服务条款敏感的界面随产品发布,而这正是本扩展点要避免的结果。
+
+## Consequences
+
+仓库外插件现在无需改动产品即可把按家族的卡片 UI 集成进 Models 页;`llm-pi-ai-oauth` 是首个消费者。代价是一份公开契约:`ProviderCardExtrasOwnerProps` 在 `./client` 边界暴露 `ConfigurableProviderView`,且各分发位点(已保存卡片、setup 形态、新增草稿、footer)成为扩展方依赖的行为。每个适配器家族的 keyed 单元格同一时刻只渲染一个所有者:同一 namespace 下同 priority 的第二次注册会被注册表拒绝,不同 priority 则是刻意的遮蔽(最低 priority 的条目渲染)——这是 slot 套件的标准覆盖通道,绝非静默合并。

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-08-26-composer-gesture-editable-gate.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-26-composer-gesture-editable-gate.md
+2026-08-26-composer-gesture-editable-gate.md: 9b01a79d8d8a546641eaf87040d7a19fc5b4d5bc
+2026-08-26-composer-gesture-editable-gate.zh.md: b5dc8d36c7b195a152d32e92fac7ae36eea880d9

+ 25 - 0
.agents/notes/implemented/bug-fix/2026-08-26-composer-gesture-editable-gate.md

@@ -0,0 +1,25 @@
+# Agent Note: Composer e2e gestures gate on the contenteditable attribute
+
+Status: implemented
+
+English | [中文](2026-08-26-composer-gesture-editable-gate.zh.md)
+
+## Problem
+
+Two Playwright gesture semantics silently changed when the composer became a Lexical `contenteditable` `<div>`, and both bit only under CI load. While the input machine is adjudicating or submitting a send — and in every locked state — the composer renders read-only by flipping `contenteditable` to `"false"` on the same element. On that element `fill()` throws immediately (`Element is not an <input>, <textarea> or [contenteditable] element`) instead of waiting through actionability, and `expect.poll(() => input.isEnabled())` is a no-op guard: Playwright's enablement check ignores both `aria-disabled` and `contenteditable` on a `<div>`, so it reports `true` throughout the read-only window. The exposed race is only a few frames wide — the permission-policy scenario stayed green for weeks until a Remote-routed subagent refactor stretched submit settling enough for CI to land inside it.
+
+## Decision
+
+Composer e2e gestures go through `writeComposerDraft` in `apps/web/tests/support.ts`, which waits for the editable attribute on the gesture's own target (`input.and(page.locator('[contenteditable="true"]'))`) before acting and replaces the draft with per-key strokes. Scenario code that must wait for the composer to reopen after a submit gates on the `contenteditable` attribute, never on `isEnabled()`.
+
+## Alternatives considered
+
+- **Waiting inside each scenario** instead of inside the helper: rejected — every new scenario re-discovers the trap the hard way, and the fix that motivated this note was already the second such site.
+- **Keeping `fill()` and polling `aria-disabled`** before each call: rejected — it leaves the dropped-edit race `fill()` has directly after trigger-menu and chip interactions (Lexical's internal selection lags the DOM selection inside one task), which the per-key helper also covers.
+- **Making the product surface tolerate `fill()`** (accepting synthetic edits while read-only): rejected — the read-only window is deliberate UI truth during submit adjudication; loosening it for tests would change user-visible behavior.
+
+## Consequences
+
+- A bare `input.fill(...)` against `[data-composer-input]` is a latent CI race even when it passes locally; the helper is the supported gesture.
+- `isEnabled()` on the composer asserts nothing. Existing polls of it guard nothing and read as coverage they do not provide.
+- A running turn by itself keeps the composer editable — that is what queueing types into — so the gate waits only through submit adjudication and locked states, not for turn completion.

+ 25 - 0
.agents/notes/implemented/bug-fix/2026-08-26-composer-gesture-editable-gate.zh.md

@@ -0,0 +1,25 @@
+# Agent Note: Composer e2e 手势以 contenteditable 属性为门
+
+Status: implemented
+
+[English](2026-08-26-composer-gesture-editable-gate.md) | 中文
+
+## 问题
+
+composer 变为 Lexical `contenteditable` `<div>` 后,两个 Playwright 手势语义悄然改变,且都只在 CI 高负载下咬人。输入机在裁决或发送一次提交期间——以及所有 locked 状态下——composer 通过把同一元素的 `contenteditable` 翻成 `"false"` 呈现只读。在该元素上 `fill()` 立即抛错(`Element is not an <input>, <textarea> or [contenteditable] element`)而不再经 actionability 等待;`expect.poll(() => input.isEnabled())` 则是无效护栏:Playwright 的 enablement 检查对 `<div>` 同时无视 `aria-disabled` 与 `contenteditable`,整个只读窗口内一律报 `true`。暴露的竞态只有几帧宽——permission-policy 场景绿了数周,直到 subagent 控制 Remote 化把提交 settle 拉长,CI 才落进窗口。
+
+## 决策
+
+composer 的 e2e 手势统一走 `apps/web/tests/support.ts` 的 `writeComposerDraft`:动作前在手势自身的目标上等待可编辑属性(`input.and(page.locator('[contenteditable="true"]'))`),再以逐键击键替换草稿。场景代码若需等待提交后 composer 重新开放,一律以 `contenteditable` 属性为门,永不使用 `isEnabled()`。
+
+## 曾考虑的替代方案
+
+- **在各场景内各自等待**而不是收进 helper:否决——每个新场景都会以最痛的方式重新发现这个陷阱,而促成本 note 的修复本身已是第二个踩点。
+- **保留 `fill()`、每次调用前 poll `aria-disabled`**:否决——这仍留着 `fill()` 在触发菜单与 chip 交互之后的丢编辑竞态(单 task 内 Lexical 内部 selection 落后于 DOM selection),逐键 helper 同时覆盖了它。
+- **让产品表面容忍 `fill()`**(只读期间接受合成编辑):否决——只读窗口是提交裁决期间刻意的 UI 事实;为测试放松它会改变用户可见行为。
+
+## 后果
+
+- 对 `[data-composer-input]` 裸写 `input.fill(...)` 即使本地全绿也是潜伏的 CI 竞态;helper 是受支持的手势。
+- 对 composer 调用 `isEnabled()` 断言不了任何东西。既有的此类 poll 不护任何路径,却读起来像提供了覆盖。
+- turn 运行本身保持 composer 可编辑——排队输入正是打进这里——因此该门只等待提交裁决与 locked 状态,不等待 turn 完成。

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-08-26-pi-ai-grant-payload-json-image.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-26-pi-ai-grant-payload-json-image.md
+2026-08-26-pi-ai-grant-payload-json-image.md: 9eb5e5ffd9ccbc18d409bd50e05061006bdca094
+2026-08-26-pi-ai-grant-payload-json-image.zh.md: 8329b4acf062a073dfe78ec10fe03852e7a64fd9

+ 29 - 0
.agents/notes/implemented/bug-fix/2026-08-26-pi-ai-grant-payload-json-image.md

@@ -0,0 +1,29 @@
+# Agent Note: pi-ai grant payloads store their JSON image
+
+Status: implemented
+
+English | [中文](2026-08-26-pi-ai-grant-payload-json-image.zh.md)
+
+## Problem
+
+A GitHub Copilot sign-in against github.com failed at its commit step: `credentials-local: record "llm-pi-ai/github-copilot" payload holds a value JSON cannot represent`. pi-ai's Copilot credential carries its optional members as explicit `undefined` (`enterpriseUrl: undefined` when no Enterprise domain was given — idiomatic JavaScript that `JSON.stringify` would simply drop), and `llm-pi-ai`'s store bridge committed the credential object verbatim as the grant payload. The credential store's validator rightly refuses `undefined` as unrepresentable, so every grant whose flow left an optional member unset failed to store, and the sign-in reported failure after the provider had already authorized it.
+
+## Decision
+
+`toRecord` in `packages/llm/llm-pi-ai/src/auth.ts` stores the JSON image of a grant credential: `jsonImage` drops explicitly-undefined members of plain objects and renders undefined array entries as `null`, exactly as `JSON.stringify` would. Everything else — non-finite numbers, foreign-prototype objects — passes through untouched, so a genuinely unstorable value still fails loud at the store's validator rather than being silently reshaped. Reading back is unchanged: an absent member and an explicitly-undefined one are indistinguishable to pi-ai's consumers, which access optional members by property read.
+
+## Testing
+
+`tests/auth.spec.ts` writes the Copilot-shaped grant (explicit `undefined` member, nested drop, array hole) through the real `LocalCredentialProvider` and asserts the stored payload is the JSON image; a second case proves the fail-loud path survives by committing a `Date`-valued member and asserting the store's refusal reaches the caller.
+
+## Alternatives considered
+
+**`JSON.parse(JSON.stringify(credential))`.** Rejected: it also renders `NaN`/`Infinity` as `null` and runs `toJSON` methods, silently reshaping exactly the values the strict validator exists to refuse loudly.
+
+**Relaxing the store validator to skip `undefined` members.** Rejected: the seam stores payloads it never reads or reshapes, and every producer relies on byte-faithful round-trips; normalization belongs to the producer that knows its library's idiom, not to the store every plugin shares.
+
+**Fixing pi-ai upstream to omit unset members.** Out of this repository's hands and version-fragile: any future flow reintroducing the idiom would break sign-in again. The bridge owning the translation makes the harness robust against the whole class.
+
+## Consequences
+
+Grants from every pi-ai flow store regardless of which optional members the flow left unset. The bridge now owns a one-way normalization: a payload read back lacks members that were explicitly `undefined` at write time, which is indistinguishable from their absence for property access, and remains the documented JSON semantics.

+ 29 - 0
.agents/notes/implemented/bug-fix/2026-08-26-pi-ai-grant-payload-json-image.zh.md

@@ -0,0 +1,29 @@
+# Agent Note: pi-ai grant payload 落盘其 JSON 像
+
+Status: implemented
+
+[English](2026-08-26-pi-ai-grant-payload-json-image.md) | 中文
+
+## Problem
+
+一次面向 github.com 的 GitHub Copilot 登录在提交环节失败:`credentials-local: record "llm-pi-ai/github-copilot" payload holds a value JSON cannot represent`。pi-ai 的 Copilot 凭据以显式 `undefined` 携带可选成员(未填 Enterprise 域名时为 `enterpriseUrl: undefined`——这是 `JSON.stringify` 会直接丢弃的 JavaScript 惯用写法),而 `llm-pi-ai` 的存储桥接把凭据对象原样作为 grant payload 提交。凭据存储的校验器正当地拒绝 `undefined` 为不可表示,于是所有流程留有未填可选成员的 grant 都无法落盘,提供方已经完成授权之后登录却报失败。
+
+## Decision
+
+`packages/llm/llm-pi-ai/src/auth.ts` 的 `toRecord` 改为落盘 grant 凭据的 JSON 像:`jsonImage` 丢弃普通对象里显式为 undefined 的成员,把数组中的 undefined 条目渲染为 `null`,与 `JSON.stringify` 完全一致。其余一切——非有限数、异种原型对象——原样透传,因此真正不可存储的值仍会在存储校验器处大声失败,而不是被静默改写。读回不变:成员缺失与显式 undefined 对以属性读取访问可选成员的 pi-ai 消费方不可区分。
+
+## Testing
+
+`tests/auth.spec.ts` 经真实 `LocalCredentialProvider` 写入 Copilot 形状的 grant(显式 `undefined` 成员、嵌套丢弃、数组空洞),断言落盘 payload 为 JSON 像;第二个用例提交带 `Date` 成员的 grant 并断言存储的拒绝到达调用方,证明 fail-loud 路径仍在。
+
+## Alternatives considered
+
+**`JSON.parse(JSON.stringify(credential))`。**否决:它还会把 `NaN`/`Infinity` 渲染为 `null` 并执行 `toJSON` 方法,把严格校验器本要大声拒绝的值静默改写掉。
+
+**放宽存储校验器、跳过 undefined 成员。**否决:seam 存储的 payload 它从不读取或改写,所有生产方都依赖逐字节往返;归一化属于了解自家库惯用法的生产方,而不是所有插件共享的存储。
+
+**在 pi-ai 上游修掉未填成员。**不在本仓库掌控内且随版本脆弱:将来任何流程重新引入该惯用法都会再次弄坏登录。由桥接层拥有这次翻译,让 harness 对整类问题免疫。
+
+## Consequences
+
+所有 pi-ai 流程的 grant 无论留空哪些可选成员都能落盘。桥接层现在拥有一次单向归一化:写入时显式为 undefined 的成员在读回时缺失,这对属性访问不可区分,且正是文档化的 JSON 语义。

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-08-26-question-drafts-survive-session-switch.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-26-question-drafts-survive-session-switch.md
+2026-08-26-question-drafts-survive-session-switch.md: 2279c4efd5e79c51a0e2e2347f21fae4b0a47b7a
+2026-08-26-question-drafts-survive-session-switch.zh.md: af275a704bef723cb3ed6fde83138a2be5b5cb3f

+ 39 - 0
.agents/notes/implemented/bug-fix/2026-08-26-question-drafts-survive-session-switch.md

@@ -0,0 +1,39 @@
+# Agent Note: Question drafts survive Session switches
+
+Status: implemented
+
+English | [中文](2026-08-26-question-drafts-survive-session-switch.zh.md)
+
+## Problem
+
+`conversation.composer` is a strict Session-scoped slot, so selecting another Session unmounts its question entry. The generic `QuestionFlow` kept its current question index, selected labels, custom text, and skip flags in React component state. A still-pending request therefore returned with empty answers after an A → B → A Session switch even though the pending carrier remained owned by Session A.
+
+The draft is transient presentation state: it must follow its Session within the current page, but it must not become mutable state on the pending business carrier or a user preference synchronized through Host settings.
+
+## Decision
+
+The question entry declares a non-persisted `createQuestionDraftStore` handle when it registers into `conversation.composer`. The renderer owns one instance per Session scope and retains that instance across selection changes, so remounting the same Session reads the same progress.
+
+The store holds at most one request identity and one progress value: current question index plus one selected/custom/skipped draft per question. `QuestionFlow` reads the stored value only when the local pending-request key and question count match. A new request therefore renders empty immediately and its first write atomically replaces the older value instead of accumulating request records. Successful answer and cancellation settlements clear only their matching request key, so a stale completion cannot erase a later draft.
+
+Busy state, failure feedback, collapse state, and focus bookkeeping remain component-local because they describe the mounted interaction rather than the unfinished answer. The `plan-review` presentation has no multi-question draft and does not read the store.
+
+This realizes the existing [Session-scope rule](../architecture/2026-07-25-web-client-session-scope-and-provide-channel.md) that remount-surviving state belongs in a Session-bound source, while retaining the [Host-backed preference decision](2026-08-06-host-backed-web-preferences.md): drafts remain page-local and never enter settings, `localStorage`, or disk. The answer semantics from [multi-select custom composition](2026-07-30-multi-select-custom-answer-composition.md) are unchanged.
+
+## Testing
+
+The store test pins keyed replacement and stale-cleanup isolation. The component test unmounts and remounts the strict entry over one store instance and requires its page, selected option, and custom text to return. The keyless assembled Web scenario types both answer forms, switches to a new Session, returns to the waiting Session, snapshots the restored composer, and submits the restored values through the real question waterfall.
+
+## Alternatives considered
+
+**Keep the state in `QuestionFlow`.** Rejected because a strict Session switch deliberately destroys that React instance; a component-local key cannot outlive the unmount it is intended to identify.
+
+**Put mutable drafts on `PendingQuestion`.** Rejected because the carrier represents pending request settlement, not React presentation state, and mutations there would bypass the Slot store's subscribed read/write surface and lifecycle ownership.
+
+**Use a module-level map keyed by Session and request.** Rejected because plugin reload and Session pruning would not own its disposal, and completed request entries could accumulate independently of the renderer's scope lifecycle.
+
+**Persist drafts through Host settings or browser storage.** Rejected because switching Sessions within one page needs remount continuity, not cross-page or cross-process durability. Persistence would synchronize transient answer text beyond the interaction that owns it.
+
+## Consequences
+
+Unsubmitted generic-question answers survive ordinary Session navigation in the current page, including the current question and explicit skips. They still reset after a page reload, Session-scope prune, or replacement pending-request identity. The per-Session memory cost is bounded to one request progress value and is released with the Slot store's Session scope.

+ 39 - 0
.agents/notes/implemented/bug-fix/2026-08-26-question-drafts-survive-session-switch.zh.md

@@ -0,0 +1,39 @@
+# Agent Note: 提问草稿在 Session 切换后保留
+
+Status: implemented
+
+[English](2026-08-26-question-drafts-survive-session-switch.md) | 中文
+
+## Problem
+
+`conversation.composer` 是严格按 Session 划分 scope 的 slot,因此选择另一个 Session 会卸载其提问条目。通用 `QuestionFlow` 把当前题号、已选标签、自定义文本和跳过标记保存在 React 组件状态中。因此,即使待处理载体仍归 Session A 所有,一个仍在等待的请求经过 A → B → A 的 Session 切换后,也会以空答案重新出现。
+
+草稿是临时呈现状态:它必须在当前页面内跟随所属 Session,但不能变成待处理业务载体上的可变状态,也不能成为通过 Host settings 同步的用户偏好。
+
+## Decision
+
+提问条目注册到 `conversation.composer` 时声明一个非持久化的 `createQuestionDraftStore` handle。renderer 为每个 Session scope 拥有一个实例,并在选择切换期间保留该实例,因此重新挂载同一 Session 时会读到相同进度。
+
+store 最多保存一个请求标识和一个进度值:当前题号,以及每道题各一份 selected/custom/skipped 草稿。只有本地待处理请求 key 与题目数量都相符时,`QuestionFlow` 才读取已存值。因此,新请求会立即渲染为空,并在首次写入时原子替换旧值,而不会累积请求记录。成功回答和取消落定后只清除与自身相符的请求 key,因此过期的完成动作不会删除较新的草稿。
+
+忙碌状态、失败提示、折叠状态和焦点记录仍留在组件本地,因为它们描述当前已挂载交互,而不是未完成的答案。`plan-review` 呈现界面没有多题草稿,也不读取该 store。
+
+这落实了既有的 [Session scope 规则](../architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md):需要跨重新挂载保留的状态应归 Session 绑定的数据源;同时保留[由 Host 持久化偏好的决策](2026-08-06-host-backed-web-preferences.zh.md):草稿仍只存在于当前页面,从不进入 settings、`localStorage` 或磁盘。[多选自定义答案组合](2026-07-30-multi-select-custom-answer-composition.zh.md)规定的答案语义保持不变。
+
+## Testing
+
+store 测试固定按 key 替换和过期清理隔离。组件测试在同一个 store 实例上卸载并重新挂载严格 Session 条目,并要求题号、已选选项和自定义文本全部恢复。无密钥的组装 Web 场景会输入两种答案、切换到新 Session、返回仍在等待的 Session、对恢复后的编辑器生成快照,再经真实提问 waterfall 提交恢复的值。
+
+## Alternatives considered
+
+**继续把状态留在 `QuestionFlow`。** 不采用,因为严格 Session 切换会刻意销毁该 React 实例;组件本地 key 无法比其试图标识的卸载过程活得更久。
+
+**把可变草稿放进 `PendingQuestion`。** 不采用,因为载体表示待处理请求的落定过程,而不是 React 呈现状态;在其中做变更还会绕过 Slot store 提供的订阅读写界面和生命周期归属。
+
+**使用按 Session 和请求建立索引的模块级 map。** 不采用,因为 plugin 重载与 Session 裁剪不拥有其清理过程,已完成请求的条目还可能脱离 renderer 的 scope 生命周期不断累积。
+
+**通过 Host settings 或浏览器存储持久化草稿。** 不采用,因为同一页面内切换 Session 需要的是跨重新挂载连续性,而不是跨页面或跨进程耐久性。持久化会把临时答案文本同步到拥有它的交互之外。
+
+## Consequences
+
+未提交的通用提问答案现在能在当前页面的普通 Session 导航中保留,包括当前题号和显式跳过状态。刷新页面、Session scope 被裁剪或待处理请求标识被替换后,草稿仍会重置。每个 Session 的内存成本被限制为一个请求进度值,并随 Slot store 的 Session scope 一起释放。

+ 2 - 2
.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md
-2026-08-18-settings-font-size-control.md: 34ee7f28f58a0a5a5dc8cd39bc4cecf5e081359f
-2026-08-18-settings-font-size-control.zh.md: 2c2ea714d50ca1f934d78ede6175935338488331
+2026-08-18-settings-font-size-control.md: 778893a2c0943f1d3af8905a27efb526071e073b
+2026-08-18-settings-font-size-control.zh.md: 71ff3b556622e7091891f8e18f5eefcbda2b0851

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 1
.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 1
.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.zh.md


+ 6 - 0
.agents/notes/implemented/feature/2026-08-26-web-trigger-menu-presentation-polish.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-26-web-trigger-menu-presentation-polish.md
+2026-08-26-web-trigger-menu-presentation-polish.md: 62582794cf46719a4ede3627164b4dda8c1d372d
+2026-08-26-web-trigger-menu-presentation-polish.zh.md: 627f0fb2ce152bf3a505cea892eeda484fcd29e4

+ 35 - 0
.agents/notes/implemented/feature/2026-08-26-web-trigger-menu-presentation-polish.md

@@ -0,0 +1,35 @@
+# Agent Note: Web trigger menu presentation polish
+
+Status: implemented
+
+English | [中文](2026-08-26-web-trigger-menu-presentation-polish.zh.md)
+
+## Problem
+
+The Web composer's `/` and `@` trigger menu carried several presentation defects that made the reference flow harder to read and operate. Candidate rows spelled their kind as a localized text prefix (`Folder · name/`, `Session · label`) that duplicated the section title and pushed the name right. Pointer hover used a CSS `:hover` tint while keyboard navigation drove the reducer-owned highlight, so two rows could look focused at once. The drillable-folder affordance was a raw `›` text glyph, unlike every other chevron in the composer, and nothing told the user that Tab drills into the highlighted folder. The pending-source state was a bare "Loading…" text row. The editable `@dir/` text a drill leaves behind rendered a folder icon before the `@`, visually double-marking a token that is not a settled chip. The composer placeholders never mentioned that `/` and `@` exist ([#3080](https://github.com/deepseek-harness/deepseek-harness/issues/3080)).
+
+## Decision
+
+Candidate rows lead with a domain icon instead of a text prefix: `InputTriggerCandidate.icon` narrows from `string` to the closed union `InputTriggerCandidateIcon` (`file | folder | session`), the menu view maps it to `ReferenceIcon`, and `ui-reference` emits bare names (`folderx/`, session label). The `candidate.file`/`candidate.folder`/`candidate.session` locale keys are deleted; the session section title is `对话`/`Sessions`. The menu spans the composer card edge to edge (`left: 0; right: 0`), and a pending source renders two breathing skeleton bars in item cell metrics instead of the loading text row.
+
+Pointer and keyboard share one highlight, last input wins: a `hover` MenuEvent parks the reducer-owned highlight on a ready row, `MenuView` routes it from `onMouseMove` (not `mouseenter`, so keyboard-scrolling rows under a resting pointer cannot steal the highlight back), and the CSS `:hover` tint is gone.
+
+The drill affordance on the highlighted folder row is the library `IconChevronRightOutline14` in the quiet `--dsw-alias-label-caption` tint the access-mode chevron uses, preceded by a localized "Browse folder" caption and a `Tab` keycap that reveal only while the row holds the shared highlight.
+
+A token still carrying its trigger character is editable text, not a settled chip: the text-ref decoration colors it and nothing more, and the domain icon belongs exclusively to the settled `ReferenceChipNode`. The former appearance channel (scan `appearance` field, `TextRefNode.__appearance`, `data-ref-appearance` DOM attribute, CSS `::before` icon) is deleted end to end.
+
+Composer placeholders advertise both triggers (`描述你想要构建的内容… / 调用指令 @ 文件或会话` / `Describe what you want to build... / commands, @ files or sessions`), and the zh copy for commands is unified from 命令 to 指令 across `ui-chat`, `ui-conversation`, `ui-goal`, and `ui-input-trigger`.
+
+## Alternatives considered
+
+**Keep the CSS `:hover` tint alongside the keyboard highlight.** Rejected: two rows can look focused at once while `aria-activedescendant` names only one, and Enter acts on the keyboard row while the eye may rest on the hovered one.
+
+**Route hover from `mouseenter`.** Rejected: when arrow keys scroll new rows under a stationary pointer, each row entering the pointer re-fires `mouseenter` and steals the highlight the user just moved; `mousemove` fires only on real pointer motion.
+
+**Keep the folder icon on the editable `@dir/` text.** Rejected: the icon before the trigger character double-marks the token and erases the visual distinction between "still editable text" and "settled chip"; reserving the icon for the chip makes the two states readable at a glance.
+
+**Show the Tab hint on every drillable row.** Rejected: idle rows carrying persistent keycaps add noise; the hint teaches the key exactly when it applies — while that row is the one Tab would act on.
+
+## Consequences
+
+The kind information every row used to spell in text now rides the icon and section title; a future candidate kind must extend `InputTriggerCandidateIcon` and pick an icon rather than pass an arbitrary string. Pointer motion round-trips through the reducer (`hover` is a no-op for the already-highlighted row, so mousemove storms do not churn state). Drill discoverability rests on the highlight: an idle folder row shows only its chevron until hovered or reached by keys. Deferred follow-ups — settle-on-space for exact-match tokens, candidate description content, back navigation after a drill, `name` vs `name/` labels, and reference search latency — are tracked in [#3154](https://github.com/deepseek-harness/deepseek-harness/issues/3154).

+ 35 - 0
.agents/notes/implemented/feature/2026-08-26-web-trigger-menu-presentation-polish.zh.md

@@ -0,0 +1,35 @@
+# Agent Note:Web 触发菜单呈现打磨
+
+Status: implemented
+
+[English](2026-08-26-web-trigger-menu-presentation-polish.md) | 中文
+
+## Problem
+
+Web composer 的 `/` 与 `@` 触发菜单存在多处呈现缺陷,使引用流程更难阅读和操作。候选行用本地化文字前缀标注类型(`Folder · name/`、`Session · label`),既与 section 标题重复又把名称挤向右侧。指针悬停用 CSS `:hover` 着色,而键盘导航驱动 reducer 持有的高亮,两行可能同时呈现焦点态。可下钻文件夹的操作标记是裸文本 `›`,与 composer 中其他 chevron 不一致,且没有任何提示告诉用户 Tab 可以下钻高亮的文件夹。来源加载中状态是一行"正在加载…"文字。下钻留下的可编辑 `@dir/` 文本在 `@` 前渲染文件夹图标,对一个并非 settled chip 的 token 形成视觉双重标记。composer 的 placeholder 从未提及 `/` 和 `@` 的存在([#3080](https://github.com/deepseek-harness/deepseek-harness/issues/3080))。
+
+## Decision
+
+候选行以领域图标开头,不再用文字前缀:`InputTriggerCandidate.icon` 从 `string` 收窄为封闭联合 `InputTriggerCandidateIcon`(`file | folder | session`),菜单视图将其映射到 `ReferenceIcon`,`ui-reference` 只输出裸名称(`folderx/`、session 标签)。删除 `candidate.file`/`candidate.folder`/`candidate.session` 三个 locale 键;session section 标题改为 `对话`/`Sessions`。菜单与 composer 卡片左右等宽(`left: 0; right: 0`),加载中的来源渲染两条与候选行同尺寸的呼吸骨架条,替代加载文字行。
+
+指针与键盘共享单一高亮,后到者优先:新增 `hover` MenuEvent 把 reducer 持有的高亮停在某个就绪行上,`MenuView` 从 `onMouseMove` 路由(不用 `mouseenter`,避免键盘滚动把新行送到静止指针下时抢回高亮),CSS `:hover` 着色整体移除。
+
+高亮文件夹行上的下钻标记改为库内 `IconChevronRightOutline14`,使用访问模式 chevron 同款的弱色 `--dsw-alias-label-caption`,左侧为本地化"进入目录"文字加 `Tab` 键帽提示,仅当该行持有共享高亮时显示。
+
+仍带触发符的 token 是可编辑文本而非 settled chip:text-ref 装饰只做染色,领域图标专属于 settled 的 `ReferenceChipNode`。原有的 appearance 通道(扫描的 `appearance` 字段、`TextRefNode.__appearance`、`data-ref-appearance` DOM 属性、CSS `::before` 图标)端到端删除。
+
+composer placeholder 同时提示两个触发符(`描述你想要构建的内容… / 调用指令 @ 文件或会话` / `Describe what you want to build... / commands, @ files or sessions`),并将 `ui-chat`、`ui-conversation`、`ui-goal`、`ui-input-trigger` 中命令的中文文案统一为"指令"。
+
+## Alternatives considered
+
+**保留 CSS `:hover` 着色与键盘高亮并存。** 被否:两行可能同时呈现焦点态,而 `aria-activedescendant` 只指向一行;Enter 作用于键盘行,视线却可能停在悬停行上。
+
+**从 `mouseenter` 路由悬停。** 被否:方向键把新行滚动到静止指针下方时,每个进入指针的行都会重新触发 `mouseenter`,抢走用户刚移走的高亮;`mousemove` 只在指针真实移动时触发。
+
+**保留可编辑 `@dir/` 文本上的文件夹图标。** 被否:触发符前的图标对 token 形成双重标记,抹掉了"仍可编辑的文本"与"settled chip"之间的视觉区分;图标专属于 chip 才能让两种状态一眼可辨。
+
+**在所有可下钻行上常驻 Tab 提示。** 被否:空闲行常驻键帽增加噪音;提示恰好在其生效时出现——该行正是 Tab 将作用的行。
+
+## Consequences
+
+过去每行用文字拼写的类型信息现在由图标和 section 标题承载;未来新增候选类型必须扩展 `InputTriggerCandidateIcon` 并选定图标,而非传任意字符串。指针移动经 reducer 往返(`hover` 对已高亮行是 no-op,mousemove 风暴不会搅动状态)。下钻的可发现性依赖高亮:空闲文件夹行在被悬停或键盘到达前只显示 chevron。延后的跟进项——精确匹配 token 的空格 settle、候选 description 内容、下钻后的回退导航、`name` 与 `name/` 标签、引用搜索延迟——记录在 [#3154](https://github.com/deepseek-harness/deepseek-harness/issues/3154)。

+ 2 - 2
.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md
-2026-08-08-native-windows-pull-request-ci.md: 98f48029a86a8b07e53cd4498b27d637508e450b
-2026-08-08-native-windows-pull-request-ci.zh.md: b2dba91e4a88d0da637521aae2de937672b869a3
+2026-08-08-native-windows-pull-request-ci.md: 8faec10fdc8538c994954213ac477a2ed52e800c
+2026-08-08-native-windows-pull-request-ci.zh.md: f692aa85e33e2e02d04cd72d9316ed4fc139aced

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 2 - 2
.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 2 - 2
.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md


+ 2 - 2
.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md
-2026-08-10-unary-apiproxy-remote-migration.md: cd019ef10c6d584a98b185ac50856a3fe63b8bd0
-2026-08-10-unary-apiproxy-remote-migration.zh.md: e6842cc82cfd7f850bb58e9d640ec23394287427
+2026-08-10-unary-apiproxy-remote-migration.md: b63946b581d3a2afcd159e3b1c0ef44f824aa35c
+2026-08-10-unary-apiproxy-remote-migration.zh.md: 92f40fc79f2be44855620b6b6915798834284747

+ 4 - 3
.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md

@@ -29,7 +29,8 @@ Large BFF methods remain in `dsh-host-apiproxy`. A method leaves this migration
 | `session.rename` | `ctx.remote.sessionTitle` in `@deepseek-ai/dsh-session-title` | `SessionTitleService.rename(Session, title)` | Direct `@Remote`; Client maps `eventSeq` to its title projection sequence. |
 | `command.list`, `command.execute` | `ctx.remote.commands` in `@deepseek-ai/dsh-commands` | `CommandRuntime.list(Agent)`, `execute(Agent, line, signal)` | Direct `@Remote`; Client maps `undefined` to unmatched and preserves caller cancellation. |
 | `llm.providers` | `ctx.remote.llm` in `@deepseek-ai/dsh-llm` | `LlmRuntime.listProviders()`, `listConfigurableProviders()` | Direct `@Remote` on both reads; the Client joins registration and configuration-directory rows. |
-| `credentials.describe`, `credentials.set`, `credentials.unset` | `ctx.remote.credentials` in `@deepseek-ai/dsh-credentials-local` | `LocalCredentialProvider.describe(ref)`, `set(ref, value)`, `unset(ref)` | Direct `@Remote`; Client batches `describe` calls when its UI requests several refs. |
+| `credentials.describe`, `credentials.set`, `credentials.unset` | `ctx.remote.credentials` in `@deepseek-ai/dsh-api-settings-controller` | `CredentialsController.describe(refs)`, `set(ref, value)`, `unset(ref)` | The controller preserves batch size, reference validation, field projection, provider-absence diagnostics, and provider refusal mapping without adding wire behavior to the abstract Definition. |
+| `settings.describe`, `settings.update`, `settings.replace`, `settings.mutate` | `ctx.remote.settings` in `@deepseek-ai/dsh-api-settings-controller` | `SettingsController.describe()`, `update(ns, patch, expectedRevision)`, `replace(ns, section, expectedRevision)`, `mutate(ns, ops, expectedRevision)` | The controller preserves redaction, all three write operations, optimistic revision checks, provider-absence diagnostics, and failure details. |
 | `agentPreset.read`, `agentPreset.copy`, `agentPreset.remove` | `ctx.remote.agentPresets` in `@deepseek-ai/dsh-agent-presets` | `readDocument(id)`, `copy(from, id, name?)`, `remove(id)` | `copy` and `remove` are direct; `readDocument` combines stored content with metadata from one live discovery. |
 | `subagent.interrupt` | `ctx.remote.subagents` in `@deepseek-ai/dsh-subagent` | `interruptByParent(targetSessionId, parentSessionId)` | Adapter constructs the internal user-authority variant without resolving or resuming either Agent. |
 | `workspace.list`, `workspace.insertSessionBefore`, `workspace.archiveSession` | `ctx.remote.workspace` in `@deepseek-ai/dsh-workspace` | `snapshot()`, `insertSessionBefore(workspaceId, sessionId, before?)`, `archiveSession(sessionId)` | Registry adapters detach mutable entities and return the settled workspace or archive snapshot. |
@@ -44,7 +45,7 @@ The Remote API deliberately follows Service names rather than preserving dotted
 | Session transcript | `session.history`, `attachment`, `subagent.history` | Cold/live logs, pagination, projections, presenters, and attachment authorization. |
 | Agent model selection | `session.models`, `selectModel` | Per-Agent state, model validation, and default persistence are BFF policy. |
 | Agent input and control | `session.prompt`, `updateQueue`, `cancel` | Image admission, Inbox mutation, and endpoint-specific live-only semantics. |
-| Configuration Remote | `settings.describe`, `openDocument`, `update`, `replace`, `mutate` | Namespace exposure, redaction, revision checks, and native opening are product policy. |
+| Native settings document | `settings.openDocument` | Host path resolution, document preparation, and native opening remain product policy in API Proxy. |
 | Session skill catalog | `skill.list` | Cold Sessions must not resume; preset standing scope and presenter filtering are BFF joins. |
 | Host runtime information | `host.describe` | Version, cwd, default model, and attached count combine several Host owners. |
 | Host path opening | `host.openPath`, `agentPreset.openDocument` | Native desktop authority and cancellation belong to the Host composition. |
@@ -72,7 +73,7 @@ Methods whose signatures contain only branded ids do not invoke Typert object lo
 
 ## Client and error behavior
 
-Generated Remote methods return business values and throw an Error whose `cause` contains the existing RPC failure. Client business services own adaptation to their current result/store interfaces. They must settle successful results immediately exactly as the existing services do so event frames remain idempotent replays rather than the only update path.
+Generated Remote methods return `RemoteResult` values. Client business services adapt them to their current stores and settle successful results immediately exactly as the existing services do, so event frames remain idempotent replays rather than the only update path. The migration preserves domain validation, provider-absence diagnostics, business error codes, structured details, and successful values; only endpoint addressing, the Remote result envelope, and the separately accepted timeout behavior differ from API Proxy transport.
 
 Resolver-owned `session-not-found` and `agent-busy` errors remain stable because the shared resolver raises `TypertLookupFailure`. Ordinary business exceptions become the Gateway's existing `internal` RPC failure. A selected Client consumer may migrate only if it does not branch on a more specific legacy business error code; if implementation finds such a branch, that RPC leaves this set unless the business package gains a transport-independent typed failure.
 

+ 4 - 3
.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md

@@ -29,7 +29,8 @@ API Proxy 还包含一些不以业务方法为约定的 BFF 操作:Session 生
 | `session.rename` | `ctx.remote.sessionTitle`,位于 `@deepseek-ai/dsh-session-title` | `SessionTitleService.rename(Session, title)` | 直接使用 `@Remote`;Client 将 `eventSeq` 映射到自身的标题投影序列。 |
 | `command.list`、`command.execute` | `ctx.remote.commands`,位于 `@deepseek-ai/dsh-commands` | `CommandRuntime.list(Agent)`、`execute(Agent, line, signal)` | 直接使用 `@Remote`;Client 将 `undefined` 映射为未匹配结果,并保留调用方的取消行为。 |
 | `llm.providers` | `ctx.remote.llm`,位于 `@deepseek-ai/dsh-llm` | `LlmRuntime.listProviders()`、`listConfigurableProviders()` | 两项读取都直接使用 `@Remote`;Client 关联注册行与配置目录行。 |
-| `credentials.describe`、`credentials.set`、`credentials.unset` | `ctx.remote.credentials`,位于 `@deepseek-ai/dsh-credentials-local` | `LocalCredentialProvider.describe(ref)`、`set(ref, value)`、`unset(ref)` | 直接使用 `@Remote`;当 UI 请求多个 ref 时,Client 批量发起 `describe` 调用。 |
+| `credentials.describe`、`credentials.set`、`credentials.unset` | `ctx.remote.credentials`,位于 `@deepseek-ai/dsh-api-settings-controller` | `CredentialsController.describe(refs)`、`set(ref, value)`、`unset(ref)` | controller 保留批量上限、引用校验、字段投影、provider 缺失诊断与 provider 拒绝映射,不给抽象 Definition 增加 wire 行为。 |
+| `settings.describe`、`settings.update`、`settings.replace`、`settings.mutate` | `ctx.remote.settings`,位于 `@deepseek-ai/dsh-api-settings-controller` | `SettingsController.describe()`、`update(ns, patch, expectedRevision)`、`replace(ns, section, expectedRevision)`、`mutate(ns, ops, expectedRevision)` | controller 保留脱敏、三种写入操作、乐观 revision 校验、provider 缺失诊断与失败 details。 |
 | `agentPreset.read`、`agentPreset.copy`、`agentPreset.remove` | `ctx.remote.agentPresets`,位于 `@deepseek-ai/dsh-agent-presets` | `readDocument(id)`、`copy(from, id, name?)`、`remove(id)` | `copy` 和 `remove` 直接暴露现有方法;`readDocument` 将存储的内容与一次实时发现取得的元数据组合。 |
 | `subagent.interrupt` | `ctx.remote.subagents`,位于 `@deepseek-ai/dsh-subagent` | `interruptByParent(targetSessionId, parentSessionId)` | 适配器构造内部的用户权限变体,不解析也不恢复任一 Agent。 |
 | `workspace.list`、`workspace.insertSessionBefore`、`workspace.archiveSession` | `ctx.remote.workspace`,位于 `@deepseek-ai/dsh-workspace` | `snapshot()`、`insertSessionBefore(workspaceId, sessionId, before?)`、`archiveSession(sessionId)` | 注册表适配器分离可变实体,并返回已完成更新的 workspace 或归档快照。 |
@@ -44,7 +45,7 @@ Remote API 有意采用服务名称,而不保留旧 RPC 的点分名称。例
 | Session transcript | `session.history`、`attachment`、`subagent.history` | cold/live 日志、分页、投影、呈现器和附件授权。 |
 | Agent 模型选择 | `session.models`、`selectModel` | 各 Agent 的状态、模型校验和默认值持久化属于 BFF 策略。 |
 | Agent 输入与控制 | `session.prompt`、`updateQueue`、`cancel` | 图片准入、Inbox 变更和端点特有的仅限 live 语义。 |
-| 配置 Remote | `settings.describe`、`openDocument`、`update`、`replace`、`mutate` | namespace 暴露、脱敏、修订检查和原生打开操作属于产品策略。 |
+| 原生 settings 文档 | `settings.openDocument` | Host 路径解析、文档准备和原生打开仍属于 API Proxy 中的产品策略。 |
 | Session skill 目录 | `skill.list` | 不得恢复冷 Session;preset 的常驻 scope 和呈现器过滤属于 BFF 关联操作。 |
 | Host 运行时信息 | `host.describe` | 版本、cwd、默认模型和当前已附加的 Session 数量来自多个 Host 所有者。 |
 | Host 路径打开 | `host.openPath`、`agentPreset.openDocument` | 原生桌面权限和取消属于 Host 组合。 |
@@ -72,7 +73,7 @@ Lookup 策略作用于整个 key,而非特定端点。提示词输入、队列
 
 ## Client 与错误行为
 
-生成的 Remote 方法返回业务值,并抛出一个 Error,其 `cause` 包含现有的 RPC 失败。Client 业务服务负责适配到当前的结果/store 接口。它们必须与现有服务一样让成功结果立即生效,使事件帧仍是幂等回放,而非唯一的更新路径。
+生成的 Remote 方法返回 `RemoteResult` 值。Client 业务服务负责把它们适配到现有 store,并与既有服务一样让成功结果立即生效,使事件帧仍是幂等回放,而非唯一的更新路径。迁移保留领域校验、provider 缺失诊断、业务错误码、结构化 details 与成功值;只有 endpoint 寻址、Remote 结果信封和另行接受的超时行为不同于 API Proxy 传输。
 
 Resolver 拥有的 `session-not-found` 和 `agent-busy` 错误保持稳定,因为共享 resolver 会抛出 `TypertLookupFailure`。普通业务异常会变成 Gateway 现有的 `internal` RPC 失败。只有在选定的 Client 消费方不根据更具体的旧版业务错误码进行分支时,才能迁移该调用;如果实现过程中发现这种分支,除非业务包新增与传输无关的类型化失败,否则该 RPC 将退出此集合。
 

+ 6 - 7
.github/workflows/build-exe-for-python-sdk.yml

@@ -4,9 +4,10 @@ name: Build single-exe
 # .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.
 # A full target run retains one SDK wheel and four runtime wheels; subset
 # dispatch retains the SDK wheel and selected runtime wheels. Bare executables
-# and source closures are test inputs. Run manually, label a PR `build-exe`
-# (remove and reapply to rerun), or call it from the Python release workflow.
-# Checkout uses the triggering ref, so dispatch needs no separate ref input.
+# and source closures are test inputs. Run manually or call it from the Python
+# release workflow. There is no `pull_request` trigger: a label trigger would
+# list gray skipped checks on every unrelated PR label event. Checkout uses the
+# triggering ref, so dispatch needs no separate ref input.
 on:
   workflow_call:
     inputs:
@@ -39,8 +40,6 @@ on:
         type: string
         required: false
         default: ''
-  pull_request:
-    types: [labeled]
 
 concurrency:
   # Keep the called workflow distinct from its caller's concurrency group;
@@ -62,7 +61,7 @@ jobs:
   # construct the matrix before the dependent jobs.
   plan:
     name: plan targets
-    if: inputs.ci || inputs.release || github.event_name == 'workflow_dispatch' || github.event.label.name == 'build-exe'
+    if: inputs.ci || inputs.release || github.event_name == 'workflow_dispatch'
     runs-on: ubuntu-latest
     timeout-minutes: 5
     outputs:
@@ -89,7 +88,7 @@ jobs:
       - name: Compute matrix from targets input
         id: plan
         env:
-          # Label runs and blank dispatch inputs build all targets.
+          # Blank dispatch inputs build all targets.
           TARGETS: ${{ inputs.targets || 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64' }}
         run: |
           set -euo pipefail

+ 1 - 1
apps/web/tests/command-image-envelope.expected.e2e.ts

@@ -21,7 +21,7 @@ async function freshComposer(): Promise<HTMLElement> {
   fireEvent.click(start)
   return await waitFor(() => {
     const surface = document.querySelector<HTMLElement>(
-      '[data-composer-input][data-placeholder="Describe what you want to build"]',
+      '[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]',
     )
     if (surface === null) throw new Error('composer surface missing')
     return surface

+ 1 - 1
apps/web/tests/expected/github-ready-review/conversation.expected.md

@@ -42,7 +42,7 @@
 - button "Branch into a new conversation":
   - img
 - text: {{clock}} Ran for {{duration}}
-- textbox "Message the agent"
+- textbox "Message or run a task... / commands, @ files or sessions"
 - button "Commands":
   - img
 - 'button "Access mode, current: Read Only"': Read Only

+ 1 - 1
apps/web/tests/expected/goal-command-presentation/ui.expected.md

@@ -14,7 +14,7 @@
   - img
   - img
   - text: "goal No goal is currently set. Usage: /goal [<objective>|clear|edit <objective>|pause|resume]"
-- textbox "Message the agent"
+- textbox "Message or run a task... / commands, @ files or sessions"
 - button "Commands":
   - img
 - 'button "Access mode, current: Workspace Write"': Workspace Write

+ 1 - 1
apps/web/tests/expected/markdown-cjk-strong/ui.expected.md

@@ -45,7 +45,7 @@
 - button "Branch into a new conversation":
   - img
 - text: {{clock}} Ran for {{duration}}
-- textbox "Message the agent"
+- textbox "Message or run a task... / commands, @ files or sessions"
 - button "Commands":
   - img
 - 'button "Access mode, current: Workspace Write"': Workspace Write

+ 1 - 1
apps/web/tests/expected/markdown-images/ui.expected.md

@@ -24,7 +24,7 @@
 - button "Branch into a new conversation":
   - img
 - text: {{clock}} Ran for {{duration}}
-- textbox "Message the agent"
+- textbox "Message or run a task... / commands, @ files or sessions"
 - button "Commands":
   - img
 - 'button "Access mode, current: Workspace Write"': Workspace Write

+ 1 - 1
apps/web/tests/expected/markdown-inline-code-links/ui.expected.md

@@ -36,7 +36,7 @@
 - button "Branch into a new conversation":
   - img
 - text: {{clock}} Ran for {{duration}}
-- textbox "Message the agent"
+- textbox "Message or run a task... / commands, @ files or sessions"
 - button "Commands":
   - img
 - 'button "Access mode, current: Workspace Write"': Workspace Write

+ 1 - 1
apps/web/tests/expected/math-rendering/ui.expected.md

@@ -40,7 +40,7 @@
 - button "Branch into a new conversation":
   - img
 - text: {{clock}} Ran for {{duration}}
-- textbox "Message the agent"
+- textbox "Message or run a task... / commands, @ files or sessions"
 - button "Commands":
   - img
 - 'button "Access mode, current: Workspace Write"': Workspace Write

+ 8 - 7
apps/web/tests/expected/reference-composer/menu.expected.md

@@ -1,9 +1,10 @@
 - listbox "Trigger suggestions":
   - text: Files & folders
-  - option "Folder · folderx/ folderx Browse folder" [selected]:
-    - text: Folder · folderx/ folderx
-    - button "Browse folder": ›
-  - option "File · reference.txt reference.txt"
-  - text: Session conversations
-  - option "Session · Reference order target reference-order-target-session · {{cwd}} · {{timestamp}}"
-  - option "Session · Research notes reference-source-session · {{cwd}} · {{timestamp}}"
+  - option "folderx/ folderx Browse folder" [selected]:
+    - text: folderx/ folderx
+    - button "Browse folder":
+      - img
+  - option "reference.txt reference.txt"
+  - text: Sessions
+  - option "Reference order target reference-order-target-session · {{cwd}} · {{timestamp}}"
+  - option "Research notes reference-source-session · {{cwd}} · {{timestamp}}"

+ 1 - 1
apps/web/tests/expected/reference-composer/order.expected.md

@@ -13,7 +13,7 @@
 - button "Session recall Research notes":
   - img
   - text: Session recall Research notes
-- textbox "Message the agent"
+- textbox "Message or run a task... / commands, @ files or sessions"
 - button "Commands":
   - img
 - 'button "Access mode, current: Workspace Write"': Workspace Write

+ 1 - 1
apps/web/tests/expected/skill-user-invoke/ui.expected.md

@@ -34,7 +34,7 @@
 - button "Branch into a new conversation":
   - img
 - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
-- textbox "Message the agent"
+- textbox "Message or run a task... / commands, @ files or sessions"
 - button "Commands":
   - img
 - 'button "Access mode, current: Workspace Write"': Workspace Write

+ 1 - 1
apps/web/tests/expected/stats-paged-history/ui.expected.md

@@ -375,7 +375,7 @@
 - text: 7/25 {{clock}} Ran for {{duration}}
 - button "Back to bottom":
   - img
-- textbox "Message the agent"
+- textbox "Message or run a task... / commands, @ files or sessions"
 - button "Commands":
   - img
 - 'button "Access mode, current: Workspace Write"': Workspace Write

+ 1 - 1
apps/web/tests/expected/steer-all/mid-steer.expected.md

@@ -31,7 +31,7 @@
 - text: "Interjection: include the word ORANGE in your final reply."
 - button "Copy":
   - img
-- textbox "Message the agent"
+- textbox "Message or run a task... / commands, @ files or sessions"
 - button "Commands":
   - img
 - 'button "Access mode, current: Workspace Write"': Workspace Write

+ 1 - 1
apps/web/tests/expected/steer-all/settled.expected.md

@@ -44,7 +44,7 @@
 - button "Branch into a new conversation":
   - img
 - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
-- textbox "Message the agent"
+- textbox "Message or run a task... / commands, @ files or sessions"
 - button "Commands":
   - img
 - 'button "Access mode, current: Workspace Write"': Workspace Write

+ 1 - 1
apps/web/tests/goal-bar.e2e.ts

@@ -45,7 +45,7 @@ describe('web e2e: goal bar clear convergence', () => {
     onTestFailed(() => saveFailureShot(page, 'web-e2e-goal-bar-clear'))
     // Startup reuses the fixture workspace's blank session, keeping this
     // command independent of alpha's running replay and pending question.
-    const input = page.locator('[data-composer-input][data-placeholder="Describe what you want to build"]')
+    const input = page.locator('[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]')
     await input.waitFor({ timeout: 10_000 })
     await input.fill('/goal guard rapid clear clicks')
     await input.press('Enter')

+ 3 - 3
apps/web/tests/image-display.expected.e2e.ts

@@ -90,7 +90,7 @@ it('accepts pasted images into the composer rail in order and removes them', asy
   // this assembled lane pins the intake chain over the built graph.
   const textarea = await waitFor(() => {
     const surface = document.querySelector<HTMLElement>(
-      '[data-composer-input][data-placeholder="Describe what you want to build"]',
+      '[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]',
     )
     if (surface === null) throw new Error('composer surface missing')
     return surface
@@ -165,7 +165,7 @@ it('accepts a whole-page drop under the limits-labeled overlay and refuses an ov
   fireEvent.click(start)
   const textarea = await waitFor(() => {
     const surface = document.querySelector<HTMLElement>(
-      '[data-composer-input][data-placeholder="Describe what you want to build"]',
+      '[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]',
     )
     if (surface === null) throw new Error('composer surface missing')
     return surface
@@ -220,7 +220,7 @@ it('renders a host dimension rejection with the projected 2000px limit', async (
 
   const textarea = await waitFor(() => {
     const surface = document.querySelector<HTMLElement>(
-      '[data-composer-input][data-placeholder="Describe what you want to build"]',
+      '[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]',
     )
     if (surface === null) throw new Error('composer surface missing')
     return surface

+ 4 - 4
apps/web/tests/onboarding-deepseek-config.e2e.ts

@@ -141,7 +141,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
     // Regression pin for the reload flash: both steps are satisfied, yet each
     // must load private facts before deciding not to show. Dialog chrome lives
     // inside each visible branch, so the deciding window paints and blocks
-    // nothing. Holding settings.describe widens that window from loopback
+    // nothing. Holding settings/describe widens that window from loopback
     // RTT scale to a deterministic hundreds of milliseconds, removing all
     // timing dependence from the sampler assertions below.
     //
@@ -162,7 +162,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
         if (document.getElementById('root')?.inert === true) sightings.push('inert')
       }, 8)
     })
-    // EVERY settings.describe issued before the release is held — not just
+    // EVERY settings/describe issued before the release is held — not just
     // the first — so the pin cannot silently collapse back to loopback
     // timing if a second boot-time consumer of the join ever appears.
     let released = false
@@ -171,7 +171,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
       released = true
       for (const resolve of heldRoutes.splice(0)) resolve()
     }
-    await page.route('**/api/settings.describe', async (route) => {
+    await page.route('**/api/settings/describe', async (route) => {
       if (!released) await new Promise<void>((resolve) => { heldRoutes.push(resolve) })
       await route.continue()
     })
@@ -182,7 +182,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
     await page.waitForTimeout(600)
     releaseDescribe()
     await page.waitForTimeout(400)
-    await page.unroute('**/api/settings.describe')
+    await page.unroute('**/api/settings/describe')
     acknowledgeReloadConnectionLoss(tripwire, warningsBefore)
     expect(await page.evaluate(() =>
       (window as unknown as { __takeoverSightings: string[] }).__takeoverSightings)).toEqual([])

+ 7 - 7
apps/web/tests/permission-policy-context.e2e.ts

@@ -15,7 +15,7 @@ import {
   assertFinalWorkspaceSnapshot, assertFixtureInventory, fixtureUserPrompts, launchWebScaffold, recordFixture,
   watchConsole, webSnapshotMode, type WebScaffold,
 } from './scaffold.ts'
-import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
+import { connectFreshWorkspace, newEnglishPage, saveFailureShot, writeComposerDraft } from './support.ts'
 
 const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/permission-policy-context', import.meta.url))
 const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/permission-policy-context/session.jsonl', import.meta.url))
@@ -94,26 +94,26 @@ describe('web e2e: current sandbox policy reaches the model before tools', () =>
       expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual(PROMPTS)
     }
 
-    const input = page.locator('[data-composer-input]').first()
+    const input = page.locator('[data-composer-input][contenteditable="true"]').first()
     let sessionId: Awaited<ReturnType<WebScaffold['whenTurnSettled']>> | undefined
     for (const [index, preset] of ['read-only', 'danger-full-access', 'workspace-write'].entries()) {
-      await input.fill(`/permission ${preset}`)
+      await writeComposerDraft(page, input, `/permission ${preset}`)
       await input.press('Enter')
       await page.getByRole('button', { name: `Access mode, current: ${PRESET_LABELS[index]}` })
         .waitFor({ timeout: 10_000 })
 
       const settled = scaffold.whenTurnSettled()
-      await input.fill(PROMPTS[index] as string)
+      await writeComposerDraft(page, input, PROMPTS[index] as string)
       await input.press('Enter')
       sessionId = await settled
-      await page.locator('[data-composer-input][contenteditable="true"]').first().waitFor({ timeout: 10_000 })
+      await input.waitFor({ timeout: 10_000 })
     }
 
-    await input.fill('/permission read-only')
+    await writeComposerDraft(page, input, '/permission read-only')
     await input.press('Enter')
     await page.getByRole('button', { name: 'Access mode, current: Read Only' }).waitFor({ timeout: 10_000 })
     const settled = scaffold.whenTurnSettled()
-    await input.fill(PROMPTS[3])
+    await writeComposerDraft(page, input, PROMPTS[3])
     await input.press('Enter')
     sessionId = await settled
 

+ 37 - 28
apps/web/tests/preview-boot.e2e.ts

@@ -310,25 +310,13 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
     const configureLater = page.getByRole('button', { name: 'Configure later' })
     await configureLater.waitFor({ timeout: 30_000 })
     await configureLater.click()
-    await page.locator('[data-composer-input][data-placeholder="Describe what you want to build"]')
+    await page.locator('[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]')
       .waitFor({ timeout: 30_000 })
 
     const exercised = await page.evaluate(async () => {
       type Result<T> = { result: { ok: true; value: T } | { ok: false; error: { code: string; message: string } } }
       interface PreviewApi {
-        host: { createDirectory(payload: { path: string; name: string }): Promise<Result<{ path: string }>> }
         skills: { list(payload: { sessionId: string }): Promise<Result<{ skills: unknown[] }>> }
-        settings: {
-          describe(payload: object): Promise<Result<{ namespaces: Array<{ ns: string; revision: number }> }>>
-          update(payload: { ns: string; patch: object; expectedRevision: number }): Promise<Result<unknown>>
-        }
-        credentials: {
-          set(payload: { ref: string; value: string }): Promise<Result<unknown>>
-          unset(payload: { ref: string }): Promise<Result<unknown>>
-          describe(payload: { refs: string[] }): Promise<Result<{
-            credentials: Record<string, { configured: boolean }>
-          }>>
-        }
       }
       interface PreviewTransport {
         fetch(input: string, init: RequestInit): Promise<Response>
@@ -349,33 +337,54 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
       const sessionId = sessions.result.value.items[0]?.sessionId
       if (sessionId === undefined) throw new Error('workspace adoption created no Session')
 
+      // Remote namespaces answer over the same unary carrier; the args object
+      // keys every wire parameter by its name.
+      const remote = async <T>(endpoint: string, args: object): Promise<T> => {
+        const answered = await transport.fetch(`/api/${endpoint}`, {
+          method: 'POST',
+          headers: { 'content-type': 'application/json' },
+          body: JSON.stringify({
+            type: 'client-request', rpcId: `preview-${endpoint.replace('/', '-')}`,
+            method: endpoint, payload: { args },
+          }),
+        })
+        const body = await answered.json() as Result<T>
+        if (!body.result.ok) throw new Error(`${endpoint} failed: ${body.result.error.message}`)
+        return body.result.value
+      }
       const api = transport.createApiClient()
       const skills = await api.skills.list({ sessionId })
       if (!skills.result.ok) throw new Error(`skill.list failed: ${skills.result.error.message}`)
       const createDirectory = async (path: string, name: string): Promise<void> => {
-        const created = await api.host.createDirectory({ path, name })
-        if (!created.result.ok) throw new Error(`host.createDirectory failed: ${created.result.error.message}`)
+        await remote<string>('directoryPicker/createDirectory', { path, name })
         await new Promise((resolve) => { setTimeout(resolve, 250) })
         const refreshed = await api.skills.list({ sessionId })
         if (!refreshed.result.ok) throw new Error(`skill.list refresh failed: ${refreshed.result.error.message}`)
       }
       await createDirectory('/dsh/workspace/.agents/skills', 'runtime-created')
-      const settings = await api.settings.describe({})
-      if (!settings.result.ok) throw new Error(`settings.describe failed: ${settings.result.error.message}`)
-      const shell = settings.result.value.namespaces.find(namespace => namespace.ns === 'shell')
-      if (shell === undefined) throw new Error('settings.describe omitted the shell namespace')
-      const updated = await api.settings.update({ ns: 'shell', patch: { timeoutMs: 61_000 }, expectedRevision: shell.revision })
-      if (!updated.result.ok) throw new Error(`settings.update failed: ${updated.result.error.message}`)
-      const stored = await api.credentials.set({ ref: 'PREVIEW_TEST_SECRET', value: 'worker-only' })
-      if (!stored.result.ok) throw new Error(`credentials.set failed: ${stored.result.error.message}`)
-      const credentials = await api.credentials.describe({ refs: ['PREVIEW_TEST_SECRET'] })
-      if (!credentials.result.ok) throw new Error(`credentials.describe failed: ${credentials.result.error.message}`)
-      const removed = await api.credentials.unset({ ref: 'PREVIEW_TEST_SECRET' })
-      if (!removed.result.ok) throw new Error(`credentials.unset failed: ${removed.result.error.message}`)
+      // Settings and credentials both answer over the Remote carrier, so this
+      // half of the sweep posts the generated endpoints directly like the
+      // session read above.
+      const settings = await remote<{ namespaces: { ns: string; revision: number }[] }>(
+        'settings/describe', {},
+      )
+      const shell = settings.namespaces.find(namespace => namespace.ns === 'shell')
+      if (shell === undefined) throw new Error('settings/describe omitted the shell namespace')
+      await remote('settings/update', {
+        ns: 'shell',
+        patch: { timeoutMs: 61_000 },
+        expectedRevision: shell.revision,
+      })
+      await remote('credentials/set', { ref: 'PREVIEW_TEST_SECRET', value: 'worker-only' })
+      const credentials = await remote<Record<string, { configured: boolean }>>(
+        'credentials/describe',
+        { refs: ['PREVIEW_TEST_SECRET'] },
+      )
+      await remote('credentials/unset', { ref: 'PREVIEW_TEST_SECRET' })
       await new Promise((resolve) => { setTimeout(resolve, 250) })
       return {
         skillCount: skills.result.value.skills.length,
-        credentialConfigured: credentials.result.value.credentials.PREVIEW_TEST_SECRET?.configured,
+        credentialConfigured: credentials.PREVIEW_TEST_SECRET?.configured,
       }
     })
     expect(exercised.skillCount).toBeGreaterThan(0)

+ 15 - 0
apps/web/tests/question-composer.e2e.ts

@@ -195,6 +195,21 @@ describe('web e2e: resident question composer round trip', () => {
     expect(await blue.getAttribute('aria-checked')).toBe('true')
     expect(await custom.inputValue()).toBe('Include accessibility notes')
     if (MODE !== 'record') {
+      // A strict Session-slot switch remounts the composer. Open a fresh blank
+      // Session, then return to the still-waiting request and require its
+      // Session-scoped store to restore both option and free-text drafts.
+      const originalRow = page.locator('[role="treeitem"]')
+        .filter({ hasText: 'Use the ask_user_question tool' }).first()
+      await page.getByRole('button', { name: 'New session', exact: true }).last().click()
+      await page.getByText('New Session', { exact: true }).waitFor({ timeout: 15_000 })
+      await expect.poll(() => composer.count(), { timeout: 10_000 }).toBe(0)
+      await originalRow.click()
+      await composer.waitFor({ timeout: 15_000 })
+      expect(await blue.getAttribute('aria-checked')).toBe('true')
+      expect(await custom.inputValue()).toBe('Include accessibility notes')
+
+      // This golden now owns the composed state after a real A -> B -> A
+      // Session cycle, not merely the state before the remount.
       const snapshot = await captureStableAria(page, '[data-question-key]', scaffold.workspaceCwd)
       await compareOrRefreshGolden(COMPOSED_EXPECTED, snapshot, MODE)
     }

+ 13 - 13
apps/web/tests/reference-composer.e2e.ts

@@ -150,14 +150,14 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
     const snapshot = await captureStableAria(page, '[role="listbox"]', scaffold.workspaceCwd)
     await compareOrRefreshGolden(MENU_EXPECTED, snapshot, MODE)
     expect(snapshot).toContain('Files & folders')
-    expect(snapshot).toContain('Session conversations')
+    expect(snapshot).toContain('Sessions')
     expect(snapshot).not.toContain('text: reference Files & folders')
-    expect(snapshot).toContain('File \u00b7 reference.txt')
-    expect(snapshot).toContain('Session \u00b7 Research notes')
+    expect(snapshot).toContain('reference.txt')
+    expect(snapshot).toContain('Research notes')
     expect(snapshot).not.toContain('text: Subagents')
 
     await input.fill('@reference')
-    await menu.getByRole('option', { name: /File \u00b7 reference\.txt/ }).click()
+    await menu.getByRole('option', { name: /reference\.txt/ }).click()
     // The pick lands an atomic chip: a real DOM capsule carrying the domain
     // icon and the label (the canonical reference text lives on the node and
     // expands on submit; the surface text is the label plus the separator).
@@ -167,7 +167,7 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
     await expect.poll(() => input.textContent()).toBe('reference.txt ')
 
     await input.fill('@Research')
-    await menu.getByRole('option', { name: /Session \u00b7 Research notes/ }).click()
+    await menu.getByRole('option', { name: /Research notes/ }).click()
     const sessionReference = page.locator('[data-composer-chip]').last()
     await expect.poll(() => sessionReference.textContent()).toBe('Research notes')
     await expect.poll(() => sessionReference.locator('svg').count()).toBe(1)
@@ -183,7 +183,7 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
     const menu = page.getByRole('listbox', { name: 'Trigger suggestions' })
 
     await input.fill('@reference')
-    await menu.getByRole('option', { name: /File · reference\.txt/ }).click()
+    await menu.getByRole('option', { name: /reference\.txt/ }).click()
     await expect.poll(() => input.locator('[data-composer-chip]').count()).toBe(1)
 
     // The #2813 gesture: collapse the caret to the document start, directly
@@ -192,7 +192,7 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
     await page.keyboard.press('ControlOrMeta+A')
     await page.keyboard.press('ArrowLeft')
     await page.keyboard.type('@Research')
-    await menu.getByRole('option', { name: /Session · Research notes/ }).click()
+    await menu.getByRole('option', { name: /Research notes/ }).click()
 
     // Both chips survive the boundary insert: the session chip lands ahead of
     // the intact file chip.
@@ -212,7 +212,7 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
     const menu = page.getByRole('listbox', { name: 'Trigger suggestions' })
 
     await input.fill('@reference')
-    await menu.getByRole('option', { name: /File · reference\.txt/ }).click()
+    await menu.getByRole('option', { name: /reference\.txt/ }).click()
     await expect.poll(() => input.locator('[data-composer-chip]').count()).toBe(1)
 
     // First ArrowLeft crosses the trailing space; the second steps across the
@@ -250,7 +250,7 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
     // as an atomic chip — folder glyph, no trigger character, one unit.
     await writeComposerDraft(page, input, '@folderx')
     // First folder query on this page: allow the Host index a cold start.
-    await menu.getByRole('option', { name: /Folder · folderx\// }).waitFor({ timeout: 60_000 })
+    await menu.getByRole('option', { name: /^folderx\// }).waitFor({ timeout: 60_000 })
     await page.keyboard.press('Enter')
     const chip = input.locator('[data-composer-chip]').last()
     await expect.poll(() => chip.textContent()).toBe('folderx/')
@@ -260,18 +260,18 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
     // Tab drills: the literal descent text stays editable and the open menu
     // lists the folder's children.
     await writeComposerDraft(page, input, '@folderx')
-    await menu.getByRole('option', { name: /Folder · folderx\// }).waitFor()
+    await menu.getByRole('option', { name: /^folderx\// }).waitFor()
     await page.keyboard.press('Tab')
     await expect.poll(() => input.textContent()).toBe('@folderx/')
-    await menu.getByRole('option', { name: /File · child\.txt/ }).waitFor()
+    await menu.getByRole('option', { name: /child\.txt/ }).waitFor()
 
     // The row chevron drills the same way by pointer.
     await writeComposerDraft(page, input, '@folderx')
-    const row = menu.getByRole('option', { name: /Folder · folderx\// })
+    const row = menu.getByRole('option', { name: /^folderx\// })
     await row.waitFor()
     await row.getByRole('button', { name: 'Browse folder' }).click()
     await expect.poll(() => input.textContent()).toBe('@folderx/')
-    await menu.getByRole('option', { name: /File · child\.txt/ }).waitFor()
+    await menu.getByRole('option', { name: /child\.txt/ }).waitFor()
     await page.keyboard.press('Escape')
 
     expect(tripwire.pageErrors).toEqual([])

+ 17 - 0
apps/web/tests/settings-chrome.e2e.ts

@@ -351,7 +351,19 @@ describe('web e2e: settings modal and General preferences', () => {
     const readFontSize = async (target: Page = page): Promise<string> => await target.evaluate(
       () => document.body.style.getPropertyValue('--dsh-content-font-size'),
     )
+    // The secondary tier resolved by the real engine: a probe element's
+    // font-size forces min/max/calc evaluation, which the CSS-text specs
+    // cannot exercise. Setting −1 at ≤14, setting −2 above.
+    const readSecondaryFontSize = async (): Promise<string> => await page.evaluate(() => {
+      const probe = document.createElement('div')
+      probe.style.fontSize = 'var(--dsh-content-font-size-secondary, 13px)'
+      document.body.appendChild(probe)
+      const size = getComputedStyle(probe).fontSize
+      probe.remove()
+      return size
+    })
     expect(await readFontSize()).toBe('14px')
+    expect(await readSecondaryFontSize()).toBe('13px')
     await page.getByRole('button', { name: '设置', exact: true }).click()
     const dialog = page.getByRole('dialog', { name: '设置' })
     await dialog.waitFor({ timeout: 10_000 })
@@ -360,9 +372,13 @@ describe('web e2e: settings modal and General preferences', () => {
     const increase = dialog.getByRole('button', { name: '增大字号' })
     await increase.click()
     await dialog.getByText('15', { exact: true }).waitFor({ timeout: 5_000 })
+    // 15 is the piecewise boundary: the secondary tier holds at 13px (−2)
+    // where the ≤14 branch would have given 14px (−1).
+    await expect.poll(readSecondaryFontSize, { timeout: 5_000 }).toBe('13px')
     await increase.click()
     await dialog.getByText('16', { exact: true }).waitFor({ timeout: 5_000 })
     await expect.poll(readFontSize, { timeout: 5_000 }).toBe('16px')
+    await expect.poll(readSecondaryFontSize, { timeout: 5_000 }).toBe('14px')
     await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
       .toMatch(/ui-theme:\n(?:\s+\w+: .*\n)*?\s+fontSize: 16/)
     await page.keyboard.press('Escape')
@@ -375,6 +391,7 @@ describe('web e2e: settings modal and General preferences', () => {
     acknowledgeReloadConnectionLoss(tripwire, warningStart)
     await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
     await expect.poll(readFontSize, { timeout: 5_000 }).toBe('16px')
+    expect(await readSecondaryFontSize()).toBe('14px')
 
     // Restore the default for the specs that follow (and the dialog golden).
     await page.getByRole('button', { name: '设置', exact: true }).click()

+ 1 - 1
apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md

@@ -24,7 +24,7 @@
 - button "Copy"
 - code: "const first: number = 1 const second = \"two\" let tail"
 - status: Deep diving...
-- textbox "Message the agent"
+- textbox "Message or run a task... / commands, @ files or sessions"
 - button "Commands":
   - img
 - 'button "Access mode, current: Workspace Write"': Workspace Write

+ 1 - 1
apps/web/tests/startup-auto-selection.e2e.ts

@@ -130,7 +130,7 @@ describe('web e2e: startup auto-selection', () => {
       expect(await page.locator('[data-composer-input]').first().isVisible()).toBe(true)
 
       releaseOpening()
-      await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build"]')
+      await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]')
         .waitFor({ timeout: 15_000 })
       acknowledgeReloadConnectionLoss(tripwire, warningsBefore)
 

+ 2 - 2
apps/web/tests/startup-rpc-budget.e2e.ts

@@ -1,4 +1,4 @@
-// Cold boot may issue at most two settings.describe calls regardless of client
+// Cold boot may issue at most two settings/describe calls regardless of client
 // plugin count. No model call or replay fixture is involved.
 import type { Browser, Page } from 'playwright'
 import { chromium } from 'playwright'
@@ -38,7 +38,7 @@ describe('startup RPC budget', () => {
     // absorbs the first-connection reset wave the budget must include.
     await page.getByRole('textbox', { name: 'Choose workspace' }).waitFor({ timeout: 30_000 })
     await page.waitForTimeout(3000)
-    const describeCount = calls.filter(method => method === 'settings.describe').length
+    const describeCount = calls.filter(method => method === 'settings/describe').length
     expect(describeCount, `startup /api calls:\n${calls.join('\n')}`).toBe(DESCRIBE_BUDGET)
   })
 })

+ 3 - 3
apps/web/tests/subagent-conversation.e2e.ts

@@ -393,7 +393,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
       expect(await page.locator('[data-composer-seat]').evaluate(element =>
         getComputedStyle(element).visibility)).toBe('hidden')
       releaseCatalog()
-      const input = page.getByRole('textbox', { name: 'Message the agent' })
+      const input = page.getByRole('textbox', { name: 'Message or run a task... / commands, @ files or sessions' })
       await input.waitFor({ timeout: 15_000 })
       await expect.poll(() => input.isEnabled(), { timeout: 15_000 }).toBe(true)
       acknowledgeReloadConnectionLoss(tripwire, warningStart)
@@ -417,7 +417,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
         resolveEnded()
       })
     })
-    const input = page.getByRole('textbox', { name: 'Message the agent' })
+    const input = page.getByRole('textbox', { name: 'Message or run a task... / commands, @ files or sessions' })
     await input.fill(FOLLOWUP)
     await input.press('Enter')
     await expect.poll(
@@ -500,7 +500,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
       .click()
     await page.getByRole('button', { name: '3 subagents' }).hover()
     await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click()
-    await page.getByRole('textbox', { name: 'Message the agent' }).waitFor()
+    await page.getByRole('textbox', { name: 'Message or run a task... / commands, @ files or sessions' }).waitFor()
     const forkResponse = page.waitForResponse(response =>
       new URL(response.url()).pathname === '/api/session/fork')
     await page.getByRole('button', { name: 'Branch into a new conversation' }).last().click()

+ 1 - 1
apps/web/tests/subagent-interrupt-ui.e2e.ts

@@ -259,7 +259,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co
       .getByRole('button').first().click()
     await page.getByRole('button', { name: /1 subagent/ }).click()
     await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click()
-    const input = page.getByRole('textbox', { name: 'Message the agent' })
+    const input = page.getByRole('textbox', { name: 'Message or run a task... / commands, @ files or sessions' })
     await input.waitFor({ timeout: 15_000 })
     expect(await input.isDisabled()).toBe(false)
 

+ 1 - 3
apps/web/tests/submission-echo.e2e.ts

@@ -21,9 +21,7 @@ it('paints the submission echo on the send keystroke and swaps it for the durabl
   fireEvent.click(start)
 
   const composer = await waitFor(() => {
-    const surface = document.querySelector<HTMLElement>(
-      '[data-composer-input][data-placeholder="Describe what you want to build"]',
-    )
+    const surface = document.querySelector<HTMLElement>('[data-composer-input]')
     if (surface === null) throw new Error('composer surface missing')
     return surface
   }, { timeout: 10_000 })

+ 12 - 2
apps/web/tests/support.ts

@@ -83,7 +83,7 @@ export async function connectFreshWorkspace(page: Page, root: string, name = 'wo
   await dialog.getByRole('button', { name: 'Open', exact: true }).click()
   // The pick connected the workspace: the blank session's live composer
   // replaces the locked placeholder and enables.
-  await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build"]')
+  await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]')
     .waitFor({ timeout: 15_000 })
 }
 
@@ -106,7 +106,7 @@ export async function connectFreshWorkspaceZh(page: Page, root: string, name = '
   await pathInput.fill(join(root, name))
   await pathInput.press('Enter')
   await dialog.getByRole('button', { name: '打开', exact: true }).click()
-  await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="描述你想要构建的内容"]')
+  await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="描述你想要构建的内容… / 调用指令 @ 文件或会话"]')
     .waitFor({ timeout: 15_000 })
 }
 
@@ -117,6 +117,15 @@ export async function connectFreshWorkspaceZh(page: Page, root: string, name = '
  * selection, and the batched edit lands on a null selection and is silently
  * dropped, leaving the previous draft in place. Real keystrokes leave room for
  * `selectionchange` between keys, which is also what a user's typing does.
+ *
+ * Waits for the surface to be editable first. While the input machine is
+ * adjudicating or submitting a send — and in every locked state (removed
+ * session, no workspace, an owner block) — the composer renders read-only
+ * with `contenteditable="false"` on the same element. `fill()` throws
+ * immediately on that element, and `isEnabled()` reports `true` for a
+ * `<div>` regardless of the attribute — so a gesture directly after a
+ * submit must gate on the attribute, not on enablement. A running turn by
+ * itself keeps the composer editable (that is what queueing types into).
  * @param page - the page under test.
  * @param input - the `[data-composer-input]` surface locator.
  * @param text - the replacement draft; `''` clears the draft. Must not
@@ -127,6 +136,7 @@ export async function writeComposerDraft(
   input: ReturnType<Page['locator']>,
   text: string,
 ): Promise<void> {
+  await input.and(page.locator('[contenteditable="true"]')).waitFor({ timeout: 15_000 })
   await input.click()
   await page.keyboard.press('ControlOrMeta+A')
   if (text === '') await page.keyboard.press('Backspace')

+ 2 - 2
docs/capability-seams.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/capability-seams.md
-capability-seams.md: 2ee2d1b989c4eb25b245cfd528b2fa1ba3bb0272
-capability-seams.zh.md: 7ecda568ef4df4d360414ee872d2b0241ebe1dce
+capability-seams.md: 1e7e6e39d307a9e72b5d57420bde99f51063f64d
+capability-seams.zh.md: e33a1e6da7f71838c48b961f93389ba1a089f57f

+ 12 - 2
docs/capability-seams.md

@@ -38,8 +38,12 @@ flowchart LR
   pkg_invariants["invariants"]
   pkg_message_feedback["message-feedback"]
   svc_sessionController["ctx.sessionController<br/>Host Session Remote controller"]
+  pkg_api_settings_controller["api-settings-controller"]
+  svc_credentialsController["ctx.credentialsController<br/>Host credential-surface Remote controller"]
+  svc_settingsController["ctx.settingsController<br/>Host settings-surface Remote controller"]
   pkg_api_workspace_controller["api-workspace-controller"]
   svc_workspaceController["ctx.workspaceController<br/>Host Workspace Remote controller"]
+  svc_directoryPickerController["ctx.directoryPickerController<br/>Host directory-picking Remote controller"]
   svc_invariants["ctx.invariants<br/>Package-owned invariant registry"]
   pkg_scope["scope"]
   pkg_typert_registry["typert-registry"]
@@ -217,6 +221,9 @@ flowchart LR
   pkg_agent_presets --> svc_agentPresets
   pkg_api_gateway --> svc_typertGateway
   pkg_api_session_controller --> svc_sessionController
+  pkg_api_settings_controller --> svc_credentialsController
+  pkg_api_settings_controller --> svc_settingsController
+  pkg_api_workspace_controller --> svc_directoryPickerController
   pkg_api_workspace_controller --> svc_workspaceController
   pkg_attachment --> svc_attachments
   pkg_attachment_local --> svc_attachments
@@ -348,7 +355,7 @@ flowchart LR
   svc_credentials --> pkg_llm_deepseek
   svc_credentials --> pkg_llm_pi_ai
   svc_deepseekLlmApiExtensions --> pkg_llm_deepseek
-  svc_directoryPicker --> pkg_host_apiproxy
+  svc_directoryPicker --> pkg_api_workspace_controller
   svc_dynamicCordisRunner --> pkg_tool_cordis
   svc_e2b --> pkg_fs_e2b
   svc_e2b --> pkg_subprocess_e2b
@@ -458,7 +465,10 @@ flowchart LR
 | `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. |
 | `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | Owns append-only Session instances and emits the durable session event feed. |
 | `ctx.sessionController` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | Owns Session commands, cold reads, durable-event following, live control state, and Agent activation policy; apiProxy reuses its inspection and Agent-resolution operations for Session-aware domains. |
+| `ctx.credentialsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | Projects the credential-reference seam onto the generated Remote namespace: batch fan-out, view projection, and refusal mapping live here, not on the seam Definition. |
+| `ctx.settingsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | Projects the user-settings seam onto the generated Remote namespace: the read is always redacted and every refusal is classified here, not on the seam Definition. |
 | `ctx.workspaceController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | Owns Workspace commands and reconnect-safe Workspace state delivery through the generated Remote namespace. |
+| `ctx.directoryPickerController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | Carries the picking seam onto the wire: capability gating, cancellation, and the seam-coded failures a browser directory flow discriminates on. |
 | `ctx.invariants` | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures. |
 | `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges. |
 | `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier. |
@@ -506,7 +516,7 @@ flowchart LR
 | `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | - | Producers (background bash, PTY sends, and subagent delegations) register running work; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry. |
 | `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
 | `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. |
-| `ctx.directoryPicker` | `seam` | [`host-directory-picker`](../packages/host/directory-picker) | [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | [`host-apiproxy`](../packages/host/apiproxy) | - | Discriminated interaction capability: the native backend opens one OS chooser on the host display, the browse backend serves listing/creation primitives for the in-app browser; dual-face backends fill ui-workspace directory-flow slots from their browser halves (no wire advertisement). |
+| `ctx.directoryPicker` | `seam` | [`host-directory-picker`](../packages/host/directory-picker) | [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | [`api-workspace-controller`](../packages/api/workspace-controller) | - | Discriminated interaction capability: the native backend opens one OS chooser on the host display, the browse backend serves listing/creation primitives for the in-app browser; dual-face backends fill ui-workspace directory-flow slots from their browser halves (no wire advertisement). |
 | `ctx.webServer` | `core` | [`host-webserver`](../packages/host/webserver) | - | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-hmr`](../packages/client/hmr) | - | Plain node:http carrier: named-route registry, index transform taps, and the static dist fallback; web-transport plugins register their own routes. |
 | `ctx.clientModules` | `core` | [`client-modules`](../packages/client/modules) | - | [`client-hmr`](../packages/client/hmr) | - | Composes the __DSH_BOOT__ entry graph from an incremental dsh.client scan, serves plugin bundles, and notifies rebuilt/graph-changed subscribers. |
 | `ctx.workflowEngine` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | [`tool-workflow`](../packages/workflow/tool-workflow), [`tool-ralph`](../packages/workflow/tool-ralph) | - | One engine per context, as in bash, with no named-provider registry; the general workflow and fixed Ralph consumers start runs whose agent() calls fan out through ctx.subagents. |

+ 12 - 2
docs/capability-seams.zh.md

@@ -40,8 +40,12 @@ flowchart LR
   pkg_invariants["invariants"]
   pkg_message_feedback["message-feedback"]
   svc_sessionController["ctx.sessionController<br/>Host Session Remote controller"]
+  pkg_api_settings_controller["api-settings-controller"]
+  svc_credentialsController["ctx.credentialsController<br/>Host credential-surface Remote controller"]
+  svc_settingsController["ctx.settingsController<br/>Host settings-surface Remote controller"]
   pkg_api_workspace_controller["api-workspace-controller"]
   svc_workspaceController["ctx.workspaceController<br/>Host Workspace Remote controller"]
+  svc_directoryPickerController["ctx.directoryPickerController<br/>Host directory-picking Remote controller"]
   svc_invariants["ctx.invariants<br/>Package-owned invariant registry"]
   pkg_scope["scope"]
   pkg_typert_registry["typert-registry"]
@@ -219,6 +223,9 @@ flowchart LR
   pkg_agent_presets --> svc_agentPresets
   pkg_api_gateway --> svc_typertGateway
   pkg_api_session_controller --> svc_sessionController
+  pkg_api_settings_controller --> svc_credentialsController
+  pkg_api_settings_controller --> svc_settingsController
+  pkg_api_workspace_controller --> svc_directoryPickerController
   pkg_api_workspace_controller --> svc_workspaceController
   pkg_attachment --> svc_attachments
   pkg_attachment_local --> svc_attachments
@@ -350,7 +357,7 @@ flowchart LR
   svc_credentials --> pkg_llm_deepseek
   svc_credentials --> pkg_llm_pi_ai
   svc_deepseekLlmApiExtensions --> pkg_llm_deepseek
-  svc_directoryPicker --> pkg_host_apiproxy
+  svc_directoryPicker --> pkg_api_workspace_controller
   svc_dynamicCordisRunner --> pkg_tool_cordis
   svc_e2b --> pkg_fs_e2b
   svc_e2b --> pkg_subprocess_e2b
@@ -460,7 +467,10 @@ flowchart LR
 | `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 |
 | `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 |
 | `ctx.sessionController` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | 负责 Session 命令、冷读取、持久事件跟随、实时控制状态与 Agent 激活策略;apiProxy 在需要 Session 上下文的领域中复用其检查和 Agent 解析操作。 |
+| `ctx.credentialsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | 把凭据引用 seam 投影到生成的 Remote namespace:批量扇出、视图投影与拒绝映射都在这里,而不在 seam Definition 上。 |
+| `ctx.settingsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | 把用户设置 seam 投影到生成的 Remote namespace:读取一律脱敏,所有拒绝在这里分类,而不在 seam Definition 上。 |
 | `ctx.workspaceController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | 通过生成的 Remote namespace 负责 Workspace 命令和可在重连后收敛的 Workspace 状态投递。 |
+| `ctx.directoryPickerController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | 把选目录 seam 送上线:能力门禁、取消传播,以及浏览器目录流程用于分支判断的 seam 错误码。 |
 | `ctx.invariants` | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | 配套子路径注册所属包本地的检查;该服务负责选择、唯一性、子 fiber,以及标明所属包的失败。 |
 | `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | 插件直接或通过 dsh-typert-loader 注册实时 zod 贡献;API 网关消费调用描述符和提供方,其他运行时消费方则在各自边界查询 schema 与反射元数据。 |
 | `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | 将生成的 Remote 描述符与实时 Cordis 服务关联,解析已注册的身份,并通过共享的 Connection RPC 载体提供一元调用。 |
@@ -508,7 +518,7 @@ flowchart LR
 | `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | - | 生产方(后台 bash、PTY 发送和 subagent 委派)登记正在运行的工作;tool-jobs 是面向模型的控制器,用于读取、列出和终止这些工作;jobs-local 是进程本地注册表。 |
 | `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | 搜索和抓取提供方注册到同一个 ctx.web seam;tool-web 负责稳定的面向模型名称。 |
 | `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | 后端保存过大的工具文本,并返回面向模型的定位信息和取回提示;spill-policy 是 tools/post-execute 消费方,负责决定何时 spill。 |
-| `ctx.directoryPicker` | `seam` | [`host-directory-picker`](../packages/host/directory-picker) | [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | [`host-apiproxy`](../packages/host/apiproxy) | - | 带判别标记的交互能力:原生后端在 Host 显示设备上打开一个操作系统选择器,浏览后端为应用内浏览器提供列表与创建原语;双端后端通过其浏览器侧填充 ui-workspace 目录流程的 slot(不通过协议发布)。 |
+| `ctx.directoryPicker` | `seam` | [`host-directory-picker`](../packages/host/directory-picker) | [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | [`api-workspace-controller`](../packages/api/workspace-controller) | - | 带判别标记的交互能力:原生后端在 Host 显示设备上打开一个操作系统选择器,浏览后端为应用内浏览器提供列表与创建原语;双端后端通过其浏览器侧填充 ui-workspace 目录流程的 slot(不通过协议发布)。 |
 | `ctx.webServer` | `core` | [`host-webserver`](../packages/host/webserver) | - | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-hmr`](../packages/client/hmr) | - | 普通的 node:http 载体:具名路由注册表、索引转换 tap,以及静态 dist 回退;Web 传输插件注册自己的路由。 |
 | `ctx.clientModules` | `core` | [`client-modules`](../packages/client/modules) | - | [`client-hmr`](../packages/client/hmr) | - | 通过增量 `dsh.client` 扫描组合 __DSH_BOOT__ 入口图,提供插件组合包,并通知重建/图变更订阅方。 |
 | `ctx.workflowEngine` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | [`tool-workflow`](../packages/workflow/tool-workflow), [`tool-ralph`](../packages/workflow/tool-ralph) | - | 每个上下文使用一个引擎,与 bash 相同,且没有具名提供方注册表;通用工作流与固定 Ralph 消费方启动运行,其中的 agent() 调用通过 ctx.subagents 扇出。 |

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: e771c16e7b0407b55806bae37caf689a9cae530d
-config-catalog.zh.md: 8f0d597b3bbbf339dad56470ee547ae51bb6494b
+config-catalog.md: 7f85b870bd9604ed983b0d8a251a3ee4511a52b7
+config-catalog.zh.md: 1ab6838e4cea77a7d98a2227aca6e8ac47d84fcd

+ 3 - 2
docs/config-catalog.md

@@ -796,7 +796,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/host/apiproxy/src/index.ts:41`](../packages/host/apiproxy/src/index.ts)
+Source: [`packages/host/apiproxy/src/index.ts:42`](../packages/host/apiproxy/src/index.ts)
 
 <a id="deepseek-aidsh-host-directory-picker-browse"></a>
 
@@ -1307,7 +1307,7 @@ export interface ReplayModelConfig {
 
 Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
 
-Source: [`packages/test-support/llm-replay/src/index.ts:914`](../packages/test-support/llm-replay/src/index.ts)
+Source: [`packages/test-support/llm-replay/src/index.ts:918`](../packages/test-support/llm-replay/src/index.ts)
 
 <a id="deepseek-aidsh-llm-retry"></a>
 
@@ -3319,6 +3319,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
 - `@deepseek-ai/dsh-agent` ([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts))
 - `@deepseek-ai/dsh-api-gateway` — requires `typert` ([`packages/api/gateway/src/index.ts`](../packages/api/gateway/src/index.ts))
 - `@deepseek-ai/dsh-api-remotes` — requires `typertGateway` ([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts))
+- `@deepseek-ai/dsh-api-settings-controller` ([`packages/api/settings-controller/src/index.ts`](../packages/api/settings-controller/src/index.ts))
 - `@deepseek-ai/dsh-api-workspace-controller` — requires `typert` · `workspaceRegistry` ([`packages/api/workspace-controller/src/index.ts`](../packages/api/workspace-controller/src/index.ts))
 - `@deepseek-ai/dsh-authorization` — requires `credentials` ([`packages/credentials/authorization/src/index.ts`](../packages/credentials/authorization/src/index.ts))
 - `@deepseek-ai/dsh-client-locale` ([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts))

+ 2 - 1
docs/config-catalog.zh.md

@@ -1309,7 +1309,7 @@ export interface ReplayModelConfig {
 
 依赖:[`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
 
-来源:[`packages/test-support/llm-replay/src/index.ts:914`](../packages/test-support/llm-replay/src/index.ts)
+来源:[`packages/test-support/llm-replay/src/index.ts:918`](../packages/test-support/llm-replay/src/index.ts)
 
 <a id="deepseek-aidsh-llm-retry"></a>
 
@@ -3321,6 +3321,7 @@ export interface Config {
 - `@deepseek-ai/dsh-agent`([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts))
 - `@deepseek-ai/dsh-api-gateway` — 需要 `typert`([`packages/api/gateway/src/index.ts`](../packages/api/gateway/src/index.ts))
 - `@deepseek-ai/dsh-api-remotes` — 需要 `typertGateway`([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts))
+- `@deepseek-ai/dsh-api-settings-controller`([`packages/api/settings-controller/src/index.ts`](../packages/api/settings-controller/src/index.ts))
 - `@deepseek-ai/dsh-api-workspace-controller` — 需要 `typert` · `workspaceRegistry`([`packages/api/workspace-controller/src/index.ts`](../packages/api/workspace-controller/src/index.ts))
 - `@deepseek-ai/dsh-authorization` — 需要 `credentials`([`packages/credentials/authorization/src/index.ts`](../packages/credentials/authorization/src/index.ts))
 - `@deepseek-ai/dsh-client-locale`([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts))

+ 2 - 2
docs/event-producer-consumer.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
-event-producer-consumer.md: 6083f1e7adcfd79de881c0fa61b3f971611d8d95
-event-producer-consumer.zh.md: 5d3ae6194b830571b9b361b6dc48c13f2c0dc14e
+event-producer-consumer.md: 9bd351a204ceb6ab262d2f2b9a0276c44be20cc5
+event-producer-consumer.zh.md: 453d22632dbcc73edec67a44759f1de42490be88

+ 4 - 4
docs/event-producer-consumer.md

@@ -35,8 +35,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `cordis/inspect-query-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:397`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
 | `cordis/request-run` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:367`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
 | `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:373`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
-| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:87`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
-| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:75`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
+| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
+| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
 | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
 | `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
 | `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) |
@@ -49,8 +49,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
 | `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
-| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
-| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
+| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:105`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
+| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:92`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
 | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
 | `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:178`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
 | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:152`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |

+ 4 - 4
docs/event-producer-consumer.zh.md

@@ -37,8 +37,8 @@
 | `cordis/inspect-query-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:397`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
 | `cordis/request-run` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:367`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
 | `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:373`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
-| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:87`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
-| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:75`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
+| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
+| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
 | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
 | `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
 | `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) |
@@ -51,8 +51,8 @@
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
 | `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
-| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
-| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
+| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:105`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
+| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:92`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
 | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
 | `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:178`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
 | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:152`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |

+ 2 - 2
docs/module-graph.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/module-graph.md
-module-graph.md: e28dc23d3baab10a5bffc6014339a6796daa845d
-module-graph.zh.md: e32626810d7208cf534a164558dc0fec0dc3bf28
+module-graph.md: 93170bd4d76d768d1cf5f1bc5efdf94d1ce2453a
+module-graph.zh.md: ac7f7e70d2364469ce5de5bf0734457e8e4dfb3e

+ 30 - 18
docs/module-graph.md

@@ -108,6 +108,7 @@ flowchart TD
     pkg_api_gateway["api-gateway"]
     pkg_api_remotes["api-remotes"]
     pkg_api_session_controller["api-session-controller"]
+    pkg_api_settings_controller["api-settings-controller"]
     pkg_api_workspace_controller["api-workspace-controller"]
   end
   subgraph group_attachment["packages/attachment"]
@@ -410,8 +411,6 @@ flowchart TD
   pkg_anonymous_user_id --> pkg_brand
   pkg_anonymous_user_id --> pkg_home_paths
   pkg_anonymous_user_id --> pkg_invariants
-  pkg_settings --> pkg_brand
-  pkg_settings --> pkg_invariants
   pkg_storage_domain --> pkg_invariants
   pkg_storage_domain --> pkg_storage
   pkg_storage_json --> pkg_invariants
@@ -438,10 +437,6 @@ flowchart TD
   pkg_credentials_local --> pkg_home_paths
   pkg_credentials_local --> pkg_invariants
   pkg_credentials_local --> pkg_launch_environment
-  pkg_settings_file --> pkg_atomic_write
-  pkg_settings_file --> pkg_home_paths
-  pkg_settings_file --> pkg_invariants
-  pkg_settings_file --> pkg_settings
   pkg_session --> pkg_brand
   pkg_session --> pkg_invariants
   pkg_session --> pkg_llm
@@ -504,6 +499,9 @@ flowchart TD
   pkg_session_persistence --> pkg_timeout
   pkg_session_projection --> pkg_invariants
   pkg_session_projection --> pkg_session
+  pkg_settings --> pkg_brand
+  pkg_settings --> pkg_invariants
+  pkg_settings --> pkg_session
   pkg_session_snapshot --> pkg_invariants
   pkg_session_snapshot --> pkg_session
   pkg_llm_retry --> pkg_agent
@@ -537,6 +535,11 @@ flowchart TD
   pkg_web_search_deepseek --> pkg_web
   pkg_spill_local --> pkg_invariants
   pkg_spill_local --> pkg_spill
+  pkg_api_settings_controller --> pkg_credentials
+  pkg_api_settings_controller --> pkg_invariants
+  pkg_api_settings_controller --> pkg_session
+  pkg_api_settings_controller --> pkg_settings
+  pkg_api_settings_controller --> pkg_typert_protocol
   pkg_file_reference --> pkg_agent
   pkg_file_reference --> pkg_invariants
   pkg_file_reference --> pkg_typert_protocol
@@ -605,6 +608,10 @@ flowchart TD
   pkg_session_title --> pkg_llm
   pkg_session_title --> pkg_session
   pkg_session_title --> pkg_session_projection
+  pkg_settings_file --> pkg_atomic_write
+  pkg_settings_file --> pkg_home_paths
+  pkg_settings_file --> pkg_invariants
+  pkg_settings_file --> pkg_settings
   pkg_shell --> pkg_invariants
   pkg_shell --> pkg_sandbox
   pkg_shell --> pkg_settings
@@ -1083,10 +1090,12 @@ flowchart TD
   pkg_client_connection --> pkg_commands
   pkg_client_connection --> pkg_credentials
   pkg_client_connection --> pkg_host_apiproxy
+  pkg_client_connection --> pkg_host_directory_picker
   pkg_client_connection --> pkg_host_webserver
   pkg_client_connection --> pkg_invariants
   pkg_client_connection --> pkg_llm
   pkg_client_connection --> pkg_session
+  pkg_client_connection --> pkg_settings
   pkg_client_connection --> pkg_tool_todo
   pkg_compaction_basic --> pkg_agent
   pkg_compaction_basic --> pkg_commands
@@ -1256,6 +1265,7 @@ flowchart TD
   pkg_api_session_controller --> pkg_workspace
   pkg_api_workspace_controller --> pkg_api_gateway
   pkg_api_workspace_controller --> pkg_client_connection
+  pkg_api_workspace_controller --> pkg_host_directory_picker
   pkg_api_workspace_controller --> pkg_invariants
   pkg_api_workspace_controller --> pkg_session
   pkg_api_workspace_controller --> pkg_storage_domain
@@ -1290,6 +1300,7 @@ flowchart TD
   pkg_api_remotes --> pkg_agent_presets
   pkg_api_remotes --> pkg_api_gateway
   pkg_api_remotes --> pkg_api_session_controller
+  pkg_api_remotes --> pkg_api_settings_controller
   pkg_api_remotes --> pkg_api_workspace_controller
   pkg_api_remotes --> pkg_commands
   pkg_api_remotes --> pkg_cordis_host_runner
@@ -1332,7 +1343,6 @@ flowchart TD
   pkg_client_ui_settings_plugin_inventory --> pkg_client_ui_settings
   pkg_client_ui_settings_plugin_inventory --> pkg_invariants
   pkg_client_ui_settings_plugins --> pkg_api_remotes
-  pkg_client_ui_settings_plugins --> pkg_client_connection
   pkg_client_ui_settings_plugins --> pkg_client_locale
   pkg_client_ui_settings_plugins --> pkg_client_ui_renderer
   pkg_client_ui_settings_plugins --> pkg_client_ui_settings
@@ -1388,6 +1398,7 @@ flowchart TD
   pkg_client_ui_sidebar --> pkg_client_ui_session
   pkg_client_ui_sidebar --> pkg_client_ui_workspace
   pkg_client_ui_sidebar --> pkg_invariants
+  pkg_client_ui_workspace --> pkg_api_remotes
   pkg_client_ui_workspace --> pkg_api_session_controller
   pkg_client_ui_workspace --> pkg_api_workspace_controller
   pkg_client_ui_workspace --> pkg_client_connection
@@ -1398,6 +1409,7 @@ flowchart TD
   pkg_client_ui_workspace --> pkg_client_ui_sidebar
   pkg_client_ui_workspace --> pkg_invariants
   pkg_client_ui_workspace --> pkg_session
+  pkg_client_ui_workspace --> pkg_typert_protocol
   pkg_client_ui_workspace --> pkg_util_workspace_path
   pkg_client_ui_agent_preset --> pkg_agent_presets
   pkg_client_ui_agent_preset --> pkg_api_remotes
@@ -1425,7 +1437,7 @@ flowchart TD
   pkg_client_ui_brand_official --> pkg_client_ui_renderer
   pkg_client_ui_brand_official --> pkg_client_ui_sidebar
   pkg_client_ui_brand_official --> pkg_invariants
-  pkg_client_ui_directory_picker_browse --> pkg_client_connection
+  pkg_client_ui_directory_picker_browse --> pkg_api_remotes
   pkg_client_ui_directory_picker_browse --> pkg_client_locale
   pkg_client_ui_directory_picker_browse --> pkg_client_ui_renderer
   pkg_client_ui_directory_picker_browse --> pkg_client_ui_workspace
@@ -1612,7 +1624,6 @@ flowchart TD
   pkg_client_ui_model_selection --> pkg_typert_protocol
   pkg_client_ui_permission_presets --> pkg_api_remotes
   pkg_client_ui_permission_presets --> pkg_api_session_controller
-  pkg_client_ui_permission_presets --> pkg_client_connection
   pkg_client_ui_permission_presets --> pkg_client_locale
   pkg_client_ui_permission_presets --> pkg_client_ui_commands
   pkg_client_ui_permission_presets --> pkg_client_ui_input_trigger
@@ -1724,7 +1735,6 @@ flowchart TD
 | [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/runtime-diagnostics/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) |
 | [`anonymous-user-id`](../packages/identity/anonymous-user-id) | `identity` | [`brand`](../packages/util/brand), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
 | [`storage-json`](../packages/storage/storage-json) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
 | [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
@@ -1734,7 +1744,6 @@ flowchart TD
 | [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment) |
-| [`settings-file`](../packages/settings/settings-file) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
 | [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`typert-protocol`](../packages/typert/protocol) |
 | [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
 | [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
@@ -1754,6 +1763,7 @@ flowchart TD
 | [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
 | [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
+| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
 | [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings) |
@@ -1761,6 +1771,7 @@ flowchart TD
 | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
 | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
 | [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
+| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
 | [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) |
 | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
@@ -1776,6 +1787,7 @@ flowchart TD
 | [`session-stats`](../packages/session/session-stats) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
 | [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`session-title`](../packages/session/session-title) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
+| [`settings-file`](../packages/settings/settings-file) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
 | [`shell`](../packages/shell/shell) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) |
 | [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
@@ -1854,7 +1866,7 @@ flowchart TD
 | [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
 | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
-| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tool-todo`](../packages/todo/tool-todo) |
+| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`host-apiproxy`](../packages/host/apiproxy), [`host-directory-picker`](../packages/host/directory-picker), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`tool-todo`](../packages/todo/tool-todo) |
 | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) |
 | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
@@ -1877,28 +1889,28 @@ flowchart TD
 | [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
 | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
 | [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
-| [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
+| [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`host-directory-picker`](../packages/host/directory-picker), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
 | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
 | [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
 | [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
-| [`api-remotes`](../packages/api/remotes) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval), [`user-questions`](../packages/interaction/user-questions) |
+| [`api-remotes`](../packages/api/remotes) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-settings-controller`](../packages/api/settings-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval), [`user-questions`](../packages/interaction/user-questions) |
 | [`client-ui-session`](../packages/client/ui-session) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-ui-renderer`](../packages/client/ui-renderer), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
 | [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
 | [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
 | [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-locale`](../packages/client/locale), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-workspace`](../packages/client/ui-workspace), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`util-crypto`](../packages/util/crypto), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
 | [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`api-workspace-controller`](../packages/api/workspace-controller), [`client-locale`](../packages/client/locale), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`util-workspace-path`](../packages/util/workspace-path) |
+| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`util-workspace-path`](../packages/util/workspace-path) |
 | [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`agent-presets`](../packages/preset/agent-presets), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`client-ui-approval`](../packages/client/ui-approval) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`client-ui-brand-official`](../packages/client/ui-brand-official) | `client` | [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native) | `client` | [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`client-ui-jobs`](../packages/client/ui-jobs) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants) |
@@ -1918,7 +1930,7 @@ flowchart TD
 | [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`client-ui-message-feedback`](../packages/client/ui-message-feedback) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`client-ui-model-selection`](../packages/client/ui-model-selection) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
-| [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) |
+| [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) |
 | [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`api-remotes`](../packages/api/remotes), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`util-workspace-path`](../packages/util/workspace-path) |
 | [`client-ui-workflow-run`](../packages/client/ui-workflow-run) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
 | [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |

+ 30 - 18
docs/module-graph.zh.md

@@ -110,6 +110,7 @@ flowchart TD
     pkg_api_gateway["api-gateway"]
     pkg_api_remotes["api-remotes"]
     pkg_api_session_controller["api-session-controller"]
+    pkg_api_settings_controller["api-settings-controller"]
     pkg_api_workspace_controller["api-workspace-controller"]
   end
   subgraph group_attachment["packages/attachment"]
@@ -412,8 +413,6 @@ flowchart TD
   pkg_anonymous_user_id --> pkg_brand
   pkg_anonymous_user_id --> pkg_home_paths
   pkg_anonymous_user_id --> pkg_invariants
-  pkg_settings --> pkg_brand
-  pkg_settings --> pkg_invariants
   pkg_storage_domain --> pkg_invariants
   pkg_storage_domain --> pkg_storage
   pkg_storage_json --> pkg_invariants
@@ -440,10 +439,6 @@ flowchart TD
   pkg_credentials_local --> pkg_home_paths
   pkg_credentials_local --> pkg_invariants
   pkg_credentials_local --> pkg_launch_environment
-  pkg_settings_file --> pkg_atomic_write
-  pkg_settings_file --> pkg_home_paths
-  pkg_settings_file --> pkg_invariants
-  pkg_settings_file --> pkg_settings
   pkg_session --> pkg_brand
   pkg_session --> pkg_invariants
   pkg_session --> pkg_llm
@@ -506,6 +501,9 @@ flowchart TD
   pkg_session_persistence --> pkg_timeout
   pkg_session_projection --> pkg_invariants
   pkg_session_projection --> pkg_session
+  pkg_settings --> pkg_brand
+  pkg_settings --> pkg_invariants
+  pkg_settings --> pkg_session
   pkg_session_snapshot --> pkg_invariants
   pkg_session_snapshot --> pkg_session
   pkg_llm_retry --> pkg_agent
@@ -539,6 +537,11 @@ flowchart TD
   pkg_web_search_deepseek --> pkg_web
   pkg_spill_local --> pkg_invariants
   pkg_spill_local --> pkg_spill
+  pkg_api_settings_controller --> pkg_credentials
+  pkg_api_settings_controller --> pkg_invariants
+  pkg_api_settings_controller --> pkg_session
+  pkg_api_settings_controller --> pkg_settings
+  pkg_api_settings_controller --> pkg_typert_protocol
   pkg_file_reference --> pkg_agent
   pkg_file_reference --> pkg_invariants
   pkg_file_reference --> pkg_typert_protocol
@@ -607,6 +610,10 @@ flowchart TD
   pkg_session_title --> pkg_llm
   pkg_session_title --> pkg_session
   pkg_session_title --> pkg_session_projection
+  pkg_settings_file --> pkg_atomic_write
+  pkg_settings_file --> pkg_home_paths
+  pkg_settings_file --> pkg_invariants
+  pkg_settings_file --> pkg_settings
   pkg_shell --> pkg_invariants
   pkg_shell --> pkg_sandbox
   pkg_shell --> pkg_settings
@@ -1085,10 +1092,12 @@ flowchart TD
   pkg_client_connection --> pkg_commands
   pkg_client_connection --> pkg_credentials
   pkg_client_connection --> pkg_host_apiproxy
+  pkg_client_connection --> pkg_host_directory_picker
   pkg_client_connection --> pkg_host_webserver
   pkg_client_connection --> pkg_invariants
   pkg_client_connection --> pkg_llm
   pkg_client_connection --> pkg_session
+  pkg_client_connection --> pkg_settings
   pkg_client_connection --> pkg_tool_todo
   pkg_compaction_basic --> pkg_agent
   pkg_compaction_basic --> pkg_commands
@@ -1258,6 +1267,7 @@ flowchart TD
   pkg_api_session_controller --> pkg_workspace
   pkg_api_workspace_controller --> pkg_api_gateway
   pkg_api_workspace_controller --> pkg_client_connection
+  pkg_api_workspace_controller --> pkg_host_directory_picker
   pkg_api_workspace_controller --> pkg_invariants
   pkg_api_workspace_controller --> pkg_session
   pkg_api_workspace_controller --> pkg_storage_domain
@@ -1292,6 +1302,7 @@ flowchart TD
   pkg_api_remotes --> pkg_agent_presets
   pkg_api_remotes --> pkg_api_gateway
   pkg_api_remotes --> pkg_api_session_controller
+  pkg_api_remotes --> pkg_api_settings_controller
   pkg_api_remotes --> pkg_api_workspace_controller
   pkg_api_remotes --> pkg_commands
   pkg_api_remotes --> pkg_cordis_host_runner
@@ -1334,7 +1345,6 @@ flowchart TD
   pkg_client_ui_settings_plugin_inventory --> pkg_client_ui_settings
   pkg_client_ui_settings_plugin_inventory --> pkg_invariants
   pkg_client_ui_settings_plugins --> pkg_api_remotes
-  pkg_client_ui_settings_plugins --> pkg_client_connection
   pkg_client_ui_settings_plugins --> pkg_client_locale
   pkg_client_ui_settings_plugins --> pkg_client_ui_renderer
   pkg_client_ui_settings_plugins --> pkg_client_ui_settings
@@ -1390,6 +1400,7 @@ flowchart TD
   pkg_client_ui_sidebar --> pkg_client_ui_session
   pkg_client_ui_sidebar --> pkg_client_ui_workspace
   pkg_client_ui_sidebar --> pkg_invariants
+  pkg_client_ui_workspace --> pkg_api_remotes
   pkg_client_ui_workspace --> pkg_api_session_controller
   pkg_client_ui_workspace --> pkg_api_workspace_controller
   pkg_client_ui_workspace --> pkg_client_connection
@@ -1400,6 +1411,7 @@ flowchart TD
   pkg_client_ui_workspace --> pkg_client_ui_sidebar
   pkg_client_ui_workspace --> pkg_invariants
   pkg_client_ui_workspace --> pkg_session
+  pkg_client_ui_workspace --> pkg_typert_protocol
   pkg_client_ui_workspace --> pkg_util_workspace_path
   pkg_client_ui_agent_preset --> pkg_agent_presets
   pkg_client_ui_agent_preset --> pkg_api_remotes
@@ -1427,7 +1439,7 @@ flowchart TD
   pkg_client_ui_brand_official --> pkg_client_ui_renderer
   pkg_client_ui_brand_official --> pkg_client_ui_sidebar
   pkg_client_ui_brand_official --> pkg_invariants
-  pkg_client_ui_directory_picker_browse --> pkg_client_connection
+  pkg_client_ui_directory_picker_browse --> pkg_api_remotes
   pkg_client_ui_directory_picker_browse --> pkg_client_locale
   pkg_client_ui_directory_picker_browse --> pkg_client_ui_renderer
   pkg_client_ui_directory_picker_browse --> pkg_client_ui_workspace
@@ -1614,7 +1626,6 @@ flowchart TD
   pkg_client_ui_model_selection --> pkg_typert_protocol
   pkg_client_ui_permission_presets --> pkg_api_remotes
   pkg_client_ui_permission_presets --> pkg_api_session_controller
-  pkg_client_ui_permission_presets --> pkg_client_connection
   pkg_client_ui_permission_presets --> pkg_client_locale
   pkg_client_ui_permission_presets --> pkg_client_ui_commands
   pkg_client_ui_permission_presets --> pkg_client_ui_input_trigger
@@ -1726,7 +1737,6 @@ flowchart TD
 | [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/runtime-diagnostics/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) |
 | [`anonymous-user-id`](../packages/identity/anonymous-user-id) | `identity` | [`brand`](../packages/util/brand), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
 | [`storage-json`](../packages/storage/storage-json) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
 | [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
@@ -1736,7 +1746,6 @@ flowchart TD
 | [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment) |
-| [`settings-file`](../packages/settings/settings-file) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
 | [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`typert-protocol`](../packages/typert/protocol) |
 | [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
 | [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
@@ -1756,6 +1765,7 @@ flowchart TD
 | [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
 | [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
+| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
 | [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings) |
@@ -1763,6 +1773,7 @@ flowchart TD
 | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
 | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
 | [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
+| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
 | [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) |
 | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
@@ -1778,6 +1789,7 @@ flowchart TD
 | [`session-stats`](../packages/session/session-stats) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
 | [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`session-title`](../packages/session/session-title) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
+| [`settings-file`](../packages/settings/settings-file) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
 | [`shell`](../packages/shell/shell) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) |
 | [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
@@ -1856,7 +1868,7 @@ flowchart TD
 | [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
 | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
-| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tool-todo`](../packages/todo/tool-todo) |
+| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`host-apiproxy`](../packages/host/apiproxy), [`host-directory-picker`](../packages/host/directory-picker), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`tool-todo`](../packages/todo/tool-todo) |
 | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) |
 | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
@@ -1879,28 +1891,28 @@ flowchart TD
 | [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
 | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
 | [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
-| [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
+| [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`host-directory-picker`](../packages/host/directory-picker), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
 | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
 | [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
 | [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
-| [`api-remotes`](../packages/api/remotes) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval), [`user-questions`](../packages/interaction/user-questions) |
+| [`api-remotes`](../packages/api/remotes) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-settings-controller`](../packages/api/settings-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval), [`user-questions`](../packages/interaction/user-questions) |
 | [`client-ui-session`](../packages/client/ui-session) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-ui-renderer`](../packages/client/ui-renderer), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
 | [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
 | [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
 | [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-locale`](../packages/client/locale), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-workspace`](../packages/client/ui-workspace), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`util-crypto`](../packages/util/crypto), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
 | [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`api-workspace-controller`](../packages/api/workspace-controller), [`client-locale`](../packages/client/locale), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`util-workspace-path`](../packages/util/workspace-path) |
+| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`util-workspace-path`](../packages/util/workspace-path) |
 | [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`agent-presets`](../packages/preset/agent-presets), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`client-ui-approval`](../packages/client/ui-approval) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`client-ui-brand-official`](../packages/client/ui-brand-official) | `client` | [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native) | `client` | [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`client-ui-jobs`](../packages/client/ui-jobs) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants) |
@@ -1920,7 +1932,7 @@ flowchart TD
 | [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`client-ui-message-feedback`](../packages/client/ui-message-feedback) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`client-ui-model-selection`](../packages/client/ui-model-selection) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
-| [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) |
+| [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) |
 | [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`api-remotes`](../packages/api/remotes), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`util-workspace-path`](../packages/util/workspace-path) |
 | [`client-ui-workflow-run`](../packages/client/ui-workflow-run) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
 | [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |

+ 2 - 2
docs/subsystems/credentials.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/credentials.md
-credentials.md: 9955f3ae6c6991ba5abbbec593ca5ccd12ef8773
-credentials.zh.md: 5565fe89431da42cd50fabca48bcc53e0810c800
+credentials.md: 60cd388fc18fe239b9ee5207d77d3b5561b7a3b3
+credentials.zh.md: 5ca47fac6c36c14eb1ee0aa8a6cb6a8b5e3eb9c4

+ 43 - 3
docs/subsystems/credentials.md

@@ -34,13 +34,17 @@ interface ResolvedCredential {
 `describe(ref)` answers configuration surfaces without ever exposing a value: whether the reference resolves, from which layer, and whether `set` would currently succeed. The local provider reports a reference supplied by the live process environment as `writable: false` — a write would appear to succeed while resolution kept returning the shadowing value, so the seam rejects it and the UI can render the reference read-only up front.
 
 ```ts type-equiv
-/** Source and writability facts for one reference, safe for configuration UIs — never the value. */
+/**
+ * Source and writability facts for one reference, safe for configuration UIs —
+ * never the value. The view has no slot a value could ride in, which is what
+ * lets the whole read half cross the Remote wire.
+ */
 interface CredentialInfo {
-  /** Whether {@link CredentialProvider.resolve} would currently return a value. */
+  /** Whether resolving the reference would currently return a value. */
   configured: boolean
   /** Source layer currently supplying the value; absent while unconfigured. */
   source?: string
-  /** Whether {@link CredentialProvider.set} would currently succeed for this reference. */
+  /** Whether the active provider can write this reference. */
   writable: boolean
 }
 ```
@@ -212,6 +216,42 @@ abstract deleteRecord(key: CredentialKey): Promise<void>
 
 Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credentials/credentials/src/index.ts)
 
+<a id="ctxcredentialscontroller--credentialscontroller"></a>
+
+### `ctx.credentialsController` — `CredentialsController`
+
+Host service backing the generated `ctx.remote.credentials` namespace. It carries every wire obligation the credential seam itself does not: the batch fan-out bound, the field-by-field view projection, the reference-grammar guard, and the refusal mapping. Secret values cross in one direction only — no method here returns one.
+
+```ts cordis-catalog
+/**
+ * Describe several references for one configuration surface. Batched because
+ * a settings page describes every reference its rows name at once, and one
+ * round trip keeps those rows from settling separately.
+ * @param refs - reference names, at most {@link MAX_DESCRIBE_REFS}; a name outside the grammar rejects the whole call as `bad-request`.
+ * @returns one view per requested name, keyed by that name.
+ * @throws TypertRemoteFailure when the request is invalid or no credential provider is mounted.
+ */
+@Remote async describe(refs: string[]): Promise<Record<string, CredentialInfo>>
+
+/**
+ * Store one value from a configuration surface. The value crosses the wire in
+ * this direction only: no read path returns it.
+ * @param ref - reference name to store under.
+ * @param value - the non-empty secret value.
+ * @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
+ */
+@Remote async set(ref: string, value: string): Promise<void>
+
+/**
+ * Remove one reference from a configuration surface.
+ * @param ref - reference name to remove.
+ * @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
+ */
+@Remote async unset(ref: string): Promise<void>
+```
+
+Source: [`packages/api/settings-controller/src/credentials.ts`](../../packages/api/settings-controller/src/credentials.ts)
+
 <a id="authorization-events"></a>
 
 ### `authorization/*` events

+ 43 - 3
docs/subsystems/credentials.zh.md

@@ -34,13 +34,17 @@ interface ResolvedCredential {
 `describe(ref)` 在绝不暴露值的前提下回应配置界面:引用当前是否可解析、来自哪一层、`set` 当前能否成功。本地提供方把由当前进程环境供值的引用报告为 `writable: false`——那样的写入会表面成功而解析持续返回遮蔽值,因此 seam 直接拒绝,界面也得以提前把该引用渲染为只读。
 
 ```ts type-equiv
-/** Source and writability facts for one reference, safe for configuration UIs — never the value. */
+/**
+ * Source and writability facts for one reference, safe for configuration UIs —
+ * never the value. The view has no slot a value could ride in, which is what
+ * lets the whole read half cross the Remote wire.
+ */
 interface CredentialInfo {
-  /** Whether {@link CredentialProvider.resolve} would currently return a value. */
+  /** Whether resolving the reference would currently return a value. */
   configured: boolean
   /** Source layer currently supplying the value; absent while unconfigured. */
   source?: string
-  /** Whether {@link CredentialProvider.set} would currently succeed for this reference. */
+  /** Whether the active provider can write this reference. */
   writable: boolean
 }
 ```
@@ -212,6 +216,42 @@ abstract deleteRecord(key: CredentialKey): Promise<void>
 
 Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credentials/credentials/src/index.ts)
 
+<a id="ctxcredentialscontroller--credentialscontroller"></a>
+
+### `ctx.credentialsController` — `CredentialsController`
+
+Host service backing the generated `ctx.remote.credentials` namespace. It carries every wire obligation the credential seam itself does not: the batch fan-out bound, the field-by-field view projection, the reference-grammar guard, and the refusal mapping. Secret values cross in one direction only — no method here returns one.
+
+```ts cordis-catalog
+/**
+ * Describe several references for one configuration surface. Batched because
+ * a settings page describes every reference its rows name at once, and one
+ * round trip keeps those rows from settling separately.
+ * @param refs - reference names, at most {@link MAX_DESCRIBE_REFS}; a name outside the grammar rejects the whole call as `bad-request`.
+ * @returns one view per requested name, keyed by that name.
+ * @throws TypertRemoteFailure when the request is invalid or no credential provider is mounted.
+ */
+@Remote async describe(refs: string[]): Promise<Record<string, CredentialInfo>>
+
+/**
+ * Store one value from a configuration surface. The value crosses the wire in
+ * this direction only: no read path returns it.
+ * @param ref - reference name to store under.
+ * @param value - the non-empty secret value.
+ * @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
+ */
+@Remote async set(ref: string, value: string): Promise<void>
+
+/**
+ * Remove one reference from a configuration surface.
+ * @param ref - reference name to remove.
+ * @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
+ */
+@Remote async unset(ref: string): Promise<void>
+```
+
+Source: [`packages/api/settings-controller/src/credentials.ts`](../../packages/api/settings-controller/src/credentials.ts)
+
 <a id="authorization-events"></a>
 
 ### `authorization/*` events

+ 2 - 2
docs/subsystems/persistence.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/persistence.md
-persistence.md: 402836cb727fb99d92cea5e2a0d242b010aad2c9
-persistence.zh.md: 424fc928d5a8ef18b403ea31e5a5d26d3fd3fdc7
+persistence.md: f73b9ab01c232c4d4fec5aa51e5250c60b9337da
+persistence.zh.md: 061c29f6b54c41137e3c764e9a7804f411f63f17

+ 1 - 1
docs/subsystems/persistence.md

@@ -233,7 +233,7 @@ interface SessionPersistenceSnapshot {
 All implement the same abstract `SessionPersistence` (locate/create/append/prepare/load/inspect/readFrom/list/listSnapshots over `SessionEvent`, with optional cancellation on observation methods) and pass the shared `runPersistenceContract` suite:
 
 - **[dsh-session-persistence-jsonl](../../packages/session/session-persistence-jsonl)** — an append-only logical JSONL log per session, stored as checksummed concatenated Zstandard frames by default or raw lines by configuration, with crash-safe atomic writes, interrupted-turn recovery, and a read/replay path.
-- **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)** — an opt-in `node:sqlite` backend using schema 18 to store exact same-block delta runs in bounded physical `text-chunks`, `reasoning-chunks`, and `tool-call-chunks` rows. It reconstructs the complete logical event stream before returning it, packs only newly durable batches, and rejects older schemas rather than migrating them.
+- **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)** — an opt-in `node:sqlite` backend using schema 19 to store exact same-block delta runs in bounded physical `text-chunks`, `reasoning-chunks`, and `tool-call-chunks` rows. It reconstructs the complete logical event stream before returning it, packs only newly durable batches, and rejects older schemas rather than migrating them.
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 

+ 1 - 1
docs/subsystems/persistence.zh.md

@@ -233,7 +233,7 @@ interface SessionPersistenceSnapshot {
 两者都实现同一个抽象 `SessionPersistence`(在 `SessionEvent` 上执行 locate/create/append/prepare/load/inspect/readFrom/list/listSnapshots,观察方法可选支持取消),并通过共享的 `runPersistenceContract` 套件:
 
 - **[dsh-session-persistence-jsonl](../../packages/session/session-persistence-jsonl)**——逐会话仅追加的逻辑 JSONL 日志,默认存储为带 checksum 的连续 Zstandard frame,也可配置为原始行;支持崩溃安全的原子写入、被中断轮次的恢复以及读取/回放路径。
-- **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)**:一个可选启用的 `node:sqlite` 后端,使用 schema 18 把同一分片块中字段完全匹配的 delta 连续段存为有界物理 `text-chunks`、`reasoning-chunks` 与 `tool-call-chunks` 行。它在返回前重建完整逻辑事件流,只打包新增的持久批次,并拒绝旧 schema,而不是执行迁移。
+- **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)**:一个可选启用的 `node:sqlite` 后端,使用 schema 19 把同一分片块中字段完全匹配的 delta 连续段存为有界物理 `text-chunks`、`reasoning-chunks` 与 `tool-call-chunks` 行。它在返回前重建完整逻辑事件流,只打包新增的持久批次,并拒绝旧 schema,而不是执行迁移。
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 

+ 2 - 2
docs/subsystems/settings.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/settings.md
-settings.md: bfda7b897031337090f78e692a881a3d8e4cb625
-settings.zh.md: 06e82d6d25ea95ac82072ac4fa0e93d90cc4d948
+settings.md: 59bd06a6e7a8659097c726b85db13caa46dd461f
+settings.zh.md: a6bbdbce633a8b51167615f7838e55b698d48829

+ 50 - 0
docs/subsystems/settings.md

@@ -254,6 +254,56 @@ async mutate(ns: SettingsNamespace, ops: readonly SettingsPathOp[], expectedRevi
 
 Source: [`packages/settings/settings/src/index.ts`](../../packages/settings/settings/src/index.ts)
 
+<a id="ctxsettingscontroller--settingscontroller"></a>
+
+### `ctx.settingsController` — `SettingsController`
+
+Host service backing the generated `ctx.remote.settings` namespace. Every remote read uses `redactSecrets: true`, so a `role('secret')` field cannot ride a response. Writes expose the settings service's merge, replacement, and path-addressed operations, and classify every provider refusal as `settings-conflict` or `settings-rejected` with the service's message.
+
+```ts cordis-catalog
+/**
+ * Describe every registered namespace for a configuration page: redacted
+ * layered values plus the serialized schema the page renders its form from.
+ * @returns provider writability, local-document presence, and one view per namespace.
+ * @throws TypertRemoteFailure when no settings provider is mounted.
+ */
+@Remote describe(): SettingsDescribeValue
+
+/**
+ * Merge a patch into one namespace's stored user section.
+ * @param ns - namespace key to write.
+ * @param patch - fields to merge into the user section.
+ * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
+ * @returns the namespace's redacted view after the write.
+ * @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
+ */
+@Remote update( ns: string, patch: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
+
+/**
+ * Replace one namespace's stored user section wholesale.
+ * @param ns - namespace key to write.
+ * @param section - complete replacement user section.
+ * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
+ * @returns the namespace's redacted view after the write.
+ * @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
+ */
+@Remote replace( ns: string, section: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
+
+/**
+ * Apply path-addressed edits to one namespace's user section, resolved against
+ * the section as stored rather than against whatever the caller last read,
+ * then answer with that namespace's new redacted view.
+ * @param ns - namespace key to write.
+ * @param ops - the edits to apply, in order.
+ * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
+ * @returns the namespace's redacted view after the write.
+ * @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
+ */
+@Remote async mutate( ns: string, ops: SettingsPathOpView[], expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
+```
+
+Source: [`packages/api/settings-controller/src/index.ts`](../../packages/api/settings-controller/src/index.ts)
+
 <a id="settings-events"></a>
 
 ### `settings/*` events

+ 50 - 0
docs/subsystems/settings.zh.md

@@ -254,6 +254,56 @@ async mutate(ns: SettingsNamespace, ops: readonly SettingsPathOp[], expectedRevi
 
 Source: [`packages/settings/settings/src/index.ts`](../../packages/settings/settings/src/index.ts)
 
+<a id="ctxsettingscontroller--settingscontroller"></a>
+
+### `ctx.settingsController` — `SettingsController`
+
+Host service backing the generated `ctx.remote.settings` namespace. Every remote read uses `redactSecrets: true`, so a `role('secret')` field cannot ride a response. Writes expose the settings service's merge, replacement, and path-addressed operations, and classify every provider refusal as `settings-conflict` or `settings-rejected` with the service's message.
+
+```ts cordis-catalog
+/**
+ * Describe every registered namespace for a configuration page: redacted
+ * layered values plus the serialized schema the page renders its form from.
+ * @returns provider writability, local-document presence, and one view per namespace.
+ * @throws TypertRemoteFailure when no settings provider is mounted.
+ */
+@Remote describe(): SettingsDescribeValue
+
+/**
+ * Merge a patch into one namespace's stored user section.
+ * @param ns - namespace key to write.
+ * @param patch - fields to merge into the user section.
+ * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
+ * @returns the namespace's redacted view after the write.
+ * @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
+ */
+@Remote update( ns: string, patch: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
+
+/**
+ * Replace one namespace's stored user section wholesale.
+ * @param ns - namespace key to write.
+ * @param section - complete replacement user section.
+ * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
+ * @returns the namespace's redacted view after the write.
+ * @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
+ */
+@Remote replace( ns: string, section: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
+
+/**
+ * Apply path-addressed edits to one namespace's user section, resolved against
+ * the section as stored rather than against whatever the caller last read,
+ * then answer with that namespace's new redacted view.
+ * @param ns - namespace key to write.
+ * @param ops - the edits to apply, in order.
+ * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
+ * @returns the namespace's redacted view after the write.
+ * @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
+ */
+@Remote async mutate( ns: string, ops: SettingsPathOpView[], expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
+```
+
+Source: [`packages/api/settings-controller/src/index.ts`](../../packages/api/settings-controller/src/index.ts)
+
 <a id="settings-events"></a>
 
 ### `settings/*` events

+ 2 - 2
docs/subsystems/slots.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/slots.md
-slots.md: 6eb61780ca2f06ebc38a0fcf2638a7fafcd5ceee
-slots.zh.md: e5a25763d382c228525f3da24694e41dd09737e0
+slots.md: 8e115e30aed68e543eca2f1aac6e28ad9f57cf73
+slots.zh.md: 3894b69d8d020b4bb67ce325d389ab3f20cfcc9a

+ 2 - 0
docs/subsystems/slots.md

@@ -123,6 +123,8 @@ root
 │     ├─ settings.onboarding
 │     └─ settings.section
 │        ├─ settings.general.item
+│        ├─ settings.models.provider-card
+│        ├─ settings.models.footer
 │        └─ settings.plugins.tab
 │           └─ settings.plugin.item
 ├─ conversation

+ 2 - 0
docs/subsystems/slots.zh.md

@@ -123,6 +123,8 @@ root
 │     ├─ settings.onboarding
 │     └─ settings.section
 │        ├─ settings.general.item
+│        ├─ settings.models.provider-card
+│        ├─ settings.models.footer
 │        └─ settings.plugins.tab
 │           └─ settings.plugin.item
 ├─ conversation

+ 2 - 2
docs/subsystems/workspace.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/workspace.md
-workspace.md: af2a8d4c5abbde04764e26751ba178fa99b2b3ef
-workspace.zh.md: f54444f052c8faffc7670468d5423123cd3aeeac
+workspace.md: 82bbe310d490653bae5d88f41d1fcbe76833b431
+workspace.zh.md: 9bf63b7a90f9136173fbec16b55a22821fa380e5

+ 34 - 0
docs/subsystems/workspace.md

@@ -149,6 +149,40 @@ abstract capability(): DirectoryPickerCapability
 
 Source: [`packages/host/directory-picker/src/index.ts`](../../packages/host/directory-picker/src/index.ts)
 
+<a id="ctxdirectorypickercontroller--directorypickercontroller"></a>
+
+### `ctx.directoryPickerController` — `DirectoryPickerController`
+
+Host service backing the generated `ctx.remote.directoryPicker` namespace. The seam it exports is abstract and therefore never a Loader entry of its own, so this controller carries the wire verbs: one composed backend serves either the native chooser or the browse primitives, and a verb the composition cannot serve is refused rather than approximated.
+
+```ts cordis-catalog
+/**
+ * Open the host's OS chooser for a Remote caller.
+ * @param signal - caller lifetime; abort terminates the chooser.
+ * @returns the chosen absolute path, or null when the operator cancels.
+ */
+@Remote('pick') async pick(signal: AbortSignal): Promise<string | null>
+
+/**
+ * List one directory level for a Remote caller's in-app browser.
+ * @param path - absolute directory to list; absent lists the home directory.
+ * @param signal - caller lifetime; abort stops the backend's scan instead of
+ *   letting it outlive a disconnected caller.
+ * @returns the level's listing with its ancestry.
+ */
+@Remote('list') async list(path: string | undefined, signal: AbortSignal): Promise<DirectoryListing>
+
+/**
+ * Create one child directory for a Remote caller's in-app browser.
+ * @param path - absolute existing parent directory.
+ * @param name - single non-blank path segment.
+ * @returns the created directory's absolute path.
+ */
+@Remote('createDirectory') async createDirectory(path: string, name: string): Promise<string>
+```
+
+Source: [`packages/api/workspace-controller/src/directory-picker.ts`](../../packages/api/workspace-controller/src/directory-picker.ts)
+
 <a id="ctxworkspacecontroller--workspacecontroller"></a>
 
 ### `ctx.workspaceController` — `WorkspaceController`

+ 34 - 0
docs/subsystems/workspace.zh.md

@@ -149,6 +149,40 @@ abstract capability(): DirectoryPickerCapability
 
 Source: [`packages/host/directory-picker/src/index.ts`](../../packages/host/directory-picker/src/index.ts)
 
+<a id="ctxdirectorypickercontroller--directorypickercontroller"></a>
+
+### `ctx.directoryPickerController` — `DirectoryPickerController`
+
+Host service backing the generated `ctx.remote.directoryPicker` namespace. The seam it exports is abstract and therefore never a Loader entry of its own, so this controller carries the wire verbs: one composed backend serves either the native chooser or the browse primitives, and a verb the composition cannot serve is refused rather than approximated.
+
+```ts cordis-catalog
+/**
+ * Open the host's OS chooser for a Remote caller.
+ * @param signal - caller lifetime; abort terminates the chooser.
+ * @returns the chosen absolute path, or null when the operator cancels.
+ */
+@Remote('pick') async pick(signal: AbortSignal): Promise<string | null>
+
+/**
+ * List one directory level for a Remote caller's in-app browser.
+ * @param path - absolute directory to list; absent lists the home directory.
+ * @param signal - caller lifetime; abort stops the backend's scan instead of
+ *   letting it outlive a disconnected caller.
+ * @returns the level's listing with its ancestry.
+ */
+@Remote('list') async list(path: string | undefined, signal: AbortSignal): Promise<DirectoryListing>
+
+/**
+ * Create one child directory for a Remote caller's in-app browser.
+ * @param path - absolute existing parent directory.
+ * @param name - single non-blank path segment.
+ * @returns the created directory's absolute path.
+ */
+@Remote('createDirectory') async createDirectory(path: string, name: string): Promise<string>
+```
+
+Source: [`packages/api/workspace-controller/src/directory-picker.ts`](../../packages/api/workspace-controller/src/directory-picker.ts)
+
 <a id="ctxworkspacecontroller--workspacecontroller"></a>
 
 ### `ctx.workspaceController` — `WorkspaceController`

+ 0 - 5
knip.json

@@ -76,11 +76,6 @@
         "tests/**/*.ts"
       ]
     },
-    "packages/api/workspace-controller": {
-      "ignoreDependencies": [
-        "zod"
-      ]
-    },
     "packages/client/ui-approval": {
       "entry": [
         "tests/**/*.spec.tsx"

+ 2 - 2
packages/api/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/api/README.md
-README.md: 1d9bcc49684ee557bc62b8ba2bbe79918c5de5ca
-README.zh.md: 2aae832f8dc9ece0f15283efded7c9c05b5bff88
+README.md: aaea6ef47aabdc8baa63ef7a875de065bcae94b9
+README.zh.md: 8e87f97c413620ede4ee160384d00f18382cfdf7

+ 2 - 1
packages/api/README.md

@@ -29,9 +29,10 @@ The packages below provide the Remote layer; the package READMEs own the exhaust
 | [`remotes/`](remotes/README.md) | Chooses which Host capabilities and events the Client can consume. | — |
 | [`gateway/`](gateway/README.md) | Carries typed unary calls, multiplexed streams, and forwarded Host events. | `ctx.typertGateway` / `ctx.remote` |
 | [`session-controller/`](session-controller/README.md) | Owns Session commands, history streams, live control state, and Agent/Session identity policy. | `ctx.sessionController` / `ctx.remote.session` |
+| [`settings-controller/`](settings-controller/README.md) | Owns the configuration-surface reads and writes over the settings-domain seams. | `ctx.settingsController`, `ctx.credentialsController` / `ctx.remote.settings`, `ctx.remote.credentials` |
 | [`workspace-controller/`](workspace-controller/README.md) | Owns Workspace mutations and the complete Client Workspace projection. | `ctx.workspaceController` / `ctx.remote.workspace` |
 
-Remote calls run Client → Host over the application's shared Connection. API Gateway owns Remote transport, while the two controller packages own Session and Workspace behavior. Endpoints without a Remote definition fall through to the application's API Proxy.
+Remote calls run Client → Host over the application's shared Connection. API Gateway owns Remote transport, while the controller packages own Session, configuration-surface, and Workspace behavior. Endpoints without a Remote definition fall through to the application's API Proxy.
 
 -----
 

+ 3 - 2
packages/api/README.zh.md

@@ -22,16 +22,17 @@ kind: "package-group"
 <a id="packages"></a>
 ## 包
 
-下面两个包共同提供 Remote 层;穷尽式约定以各包 README 为准。
+下面这些包共同提供 Remote 层;穷尽式约定以各包 README 为准。
 
 | 包 | 职责 | ctx key |
 |---|---|---|
 | [`remotes/`](remotes/README.zh.md) | 决定 Client 可以消费哪些 Host 能力与事件。 | — |
 | [`gateway/`](gateway/README.zh.md) | 承载带类型的单次调用、多路复用 stream 与转发的 Host 事件。 | `ctx.typertGateway` / `ctx.remote` |
 | [`session-controller/`](session-controller/README.zh.md) | 拥有 Session 命令、历史 stream、实时控制状态与 Agent/Session 身份策略。 | `ctx.sessionController` / `ctx.remote.session` |
+| [`settings-controller/`](settings-controller/README.zh.md) | 拥有 settings 域各 seam 之上的配置界面读写。 | `ctx.settingsController`、`ctx.credentialsController` / `ctx.remote.settings`、`ctx.remote.credentials` |
 | [`workspace-controller/`](workspace-controller/README.zh.md) | 拥有 Workspace 变更与完整 Client Workspace 投影。 | `ctx.workspaceController` / `ctx.remote.workspace` |
 
-Remote 调用沿 Client → Host 方向运行在应用共享的 Connection 之上。API Gateway 拥有 Remote 传输,两个 controller 包分别拥有 Session 与 Workspace 行为。没有 Remote 定义的 endpoint 会回退到应用的 API Proxy。
+Remote 调用沿 Client → Host 方向运行在应用共享的 Connection 之上。API Gateway 拥有 Remote 传输,各 controller 包分别拥有 Session、配置界面与 Workspace 行为。没有 Remote 定义的 endpoint 会回退到应用的 API Proxy。
 
 -----
 

+ 2 - 2
packages/api/remotes/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/api/remotes/README.md
-README.md: 7817f30c531abffeb9156e1afaf8c6fe0697431e
-README.zh.md: 4529ee60289fc43978f9526d4abb3e2cbf073687
+README.md: 50361d1a6ae2f0b3b53a5bd928429414ab12bbce
+README.zh.md: 5545198cce2e43513af73adfc9d4277831adae92

+ 1 - 1
packages/api/remotes/README.md

@@ -27,7 +27,7 @@ Two-sided BFF for Host Remote capabilities selected by this application. The Hos
 
 [`@deepseek-ai/dsh-api-session-controller`](../session-controller/README.md) owns Agent and Session identity policy, including the Typert lookup resolvers used by other namespaces. This package only selects and mounts that generated Session contribution; it does not duplicate activation policy.
 
-The Client assembly mounts Commands, Goal, dynamic Cordis, file and Session references, read-only Host plugin inventory, message feedback, Session Controller, and Workspace Controller contributions. Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, streams, and cancellation. The Client entry consumes the shared `TypertClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation.
+The Client assembly mounts Commands, credentials, settings, Goal, dynamic Cordis, file and Session references, read-only Host plugin inventory, message feedback, Session Controller, and Workspace Controller contributions. Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, streams, and cancellation. The Client entry consumes the shared `TypertClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation.
 
 This package owns no physical transport or Host service discovery. It projects the application selection into generated Remote contributions and an independent Host event source per Client; API Gateway owns endpoints, carriers, cancellation, and reconnection. Its Client face can be reused by Web or a future TUI that provides the same React-free `ctx.remote` contract.
 

+ 1 - 1
packages/api/remotes/README.zh.md

@@ -27,7 +27,7 @@ kind: "package-reference"
 
 [`@deepseek-ai/dsh-api-session-controller`](../session-controller/README.zh.md) 拥有 Agent 与 Session 身份策略,包括供其他 namespace 使用的 Typert lookup resolver。本包只选择并挂载生成的 Session contribution,不复制激活策略。
 
-Client 组合挂载 Commands、Goal、动态 Cordis、文件与 Session 引用、只读 Host 插件清单、消息反馈、Session Controller 和 Workspace Controller contribution。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪 namespace Service、直接与作用域方法、调用、流与取消。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
+Client 组合挂载 Commands、凭据、settings、Goal、动态 Cordis、文件与 Session 引用、只读 Host 插件清单、消息反馈、Session Controller 和 Workspace Controller contribution。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪 namespace Service、直接与作用域方法、调用、流与取消。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
 
 本包不拥有物理传输或 Host 服务发现。它只把应用选择投影为生成的 Remote contribution 和唯一的 Host Cordis event source;API Gateway 负责 endpoint、carrier、取消与重连。Web 或未来的 TUI 只要提供同一份不依赖 React 的 `ctx.remote` 约定,均可复用其 Client face。
 

+ 2 - 0
packages/api/remotes/package.json

@@ -63,6 +63,7 @@
     "@deepseek-ai/dsh-agent-presets": "workspace:^",
     "@deepseek-ai/dsh-api-gateway": "workspace:^",
     "@deepseek-ai/dsh-api-session-controller": "workspace:^",
+    "@deepseek-ai/dsh-api-settings-controller": "workspace:^",
     "@deepseek-ai/dsh-api-workspace-controller": "workspace:^",
     "@deepseek-ai/dsh-commands": "workspace:^",
     "@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
@@ -85,6 +86,7 @@
     "@deepseek-ai/dsh-agent-presets": "workspace:^",
     "@deepseek-ai/dsh-api-gateway": "workspace:^",
     "@deepseek-ai/dsh-api-session-controller": "workspace:^",
+    "@deepseek-ai/dsh-api-settings-controller": "workspace:^",
     "@deepseek-ai/dsh-api-workspace-controller": "workspace:^",
     "@deepseek-ai/dsh-commands": "workspace:^",
     "@deepseek-ai/dsh-cordis-host-runner": "workspace:^",

+ 15 - 3
packages/api/remotes/src/client/index.ts

@@ -3,6 +3,7 @@
 import type { Context } from '@deepseek-ai/cordis'
 import agentPresetsRemote from '@deepseek-ai/dsh-agent-presets/remote'
 import commandsRemote from '@deepseek-ai/dsh-commands/remote'
+import settingsControllerRemote from '@deepseek-ai/dsh-api-settings-controller/remote'
 import goalsRemote from '@deepseek-ai/dsh-goal/remote'
 import dynamicRemote from '@deepseek-ai/dsh-cordis-host-runner/remote'
 import fileReferencesRemote from '@deepseek-ai/dsh-file-reference/remote'
@@ -18,6 +19,7 @@ export type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client'
 export type { PluginInventorySnapshot } from '@deepseek-ai/dsh-host-plugin-inventory/types'
 export type {} from '@deepseek-ai/dsh-agent-presets/remote'
 export type {} from '@deepseek-ai/dsh-commands/remote'
+export type {} from '@deepseek-ai/dsh-api-settings-controller/remote'
 export type {} from '@deepseek-ai/dsh-file-reference/remote'
 export type {} from '@deepseek-ai/dsh-goal/remote'
 export type {} from '@deepseek-ai/dsh-host-plugin-inventory/remote'
@@ -53,10 +55,10 @@ export type {} from '@deepseek-ai/dsh-api-session-controller/types'
  */
 export type {
   ConfigurableProviderView, ConnectionHandle, ConnectionSinks, ContentBlock,
-  CredentialView, DirectoryListing, DiscoveredModelView, IApiClient,
+  DiscoveredModelView, IApiClient,
   MessageId, ModelCatalog, ModelCatalogFailure, ModelProviderGroup, ModelReasoningEffort, ModelSelection,
   RpcError, RpcId, RpcRequest, RpcResponse, RpcResult, SessionId,
-  SettingsNamespaceView, SettingsPathOpView, SkillEntry, StreamChunk,
+  SkillEntry, StreamChunk,
 } from '@deepseek-ai/dsh-client-connection/client'
 export type {} from '@deepseek-ai/dsh-api-gateway/client'
 export type {} from '@deepseek-ai/dsh-cordis-host-runner/remote'
@@ -102,6 +104,13 @@ export type {
 // reason: a Client contribution names what it sends without importing a Host
 // package, and this assembly is where both planes legitimately meet.
 export type { JsonValue } from '@deepseek-ai/dsh-session/types'
+// Credential state vocabulary for the credentials namespace (values never ride it).
+export type { CredentialInfo } from '@deepseek-ai/dsh-credentials/types'
+// Redacted namespace vocabulary for the settings namespace (secrets never ride
+// it). It travels with its seam, whose `./types` the Client face already reads.
+export type {
+  SettingsDescribeValue, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
+} from '@deepseek-ai/dsh-settings/types'
 // Reference-discovery result vocabulary for the fileReferences and
 // sessionReferenceResolver namespaces.
 export type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types'
@@ -112,6 +121,8 @@ export type ClientFailure =
   | import('@deepseek-ai/dsh-client-connection/client').RpcError
   | import('@deepseek-ai/dsh-agent-presets/types').AgentPresetError
   | import('@deepseek-ai/dsh-api-session-controller/types').SessionError
+  | import('@deepseek-ai/dsh-api-settings-controller/types').CredentialError
+  | import('@deepseek-ai/dsh-api-settings-controller/types').SettingsError
   | import('@deepseek-ai/dsh-subagent/client').SubagentControlError
   | import('@deepseek-ai/dsh-api-workspace-controller/types').WorkspaceError
 
@@ -139,7 +150,8 @@ export async function apply(ctx: Context): Promise<() => Promise<void>> {
   const disposers: Array<() => Promise<void>> = []
   try {
     for (const contribution of [
-      agentPresetsRemote, commandsRemote, goalsRemote, dynamicRemote, fileReferencesRemote,
+      agentPresetsRemote, commandsRemote, settingsControllerRemote, goalsRemote, dynamicRemote,
+      fileReferencesRemote,
       pluginInventoryRemote, messageFeedbackRemote, sessionReferencesRemote,
       subagentsRemote, sessionRemote, workspaceRemote,
     ]) {

+ 3 - 0
packages/api/remotes/tsconfig.client.json

@@ -66,6 +66,9 @@
     {
       "path": "../session-controller/tsconfig.client.json"
     },
+    {
+      "path": "../settings-controller"
+    },
     {
       "path": "../workspace-controller/tsconfig.client.json"
     },

+ 0 - 27
packages/api/session-controller/tests/fake-api.client.ts

@@ -167,23 +167,9 @@ export class FakeApiClient implements IApiClient {
     () => Promise.resolve(ok({
       version: '0-fake', cwd: '/f', attachedSessions: 0, home: '/h', canOpenPath: true,
     }))
-  onPickDirectory: (payload: unknown) => Promise<RpcResponse<{ path: string | null }>> =
-    () => Promise.resolve(ok({ path: null }))
   onOpenPath: (payload: unknown) => Promise<RpcResponse<{ opened: true }>> =
     () => Promise.resolve(ok({ opened: true as const }))
 
-  onListDirectory: (payload: unknown) => Promise<RpcResponse<{
-    path: string
-    home: string
-    crumbs: { name: string; path: string; hidden: boolean }[]
-    entries: { name: string; path: string; hidden: boolean }[]
-    truncated: boolean
-  }>> =
-    () => Promise.resolve(ok({ path: '/home/fake', home: '/home/fake', crumbs: [{ name: '/', path: '/', hidden: false }], entries: [], truncated: false }))
-
-  onCreateDirectory: (payload: unknown) => Promise<RpcResponse<{ path: string }>> =
-    () => Promise.resolve(ok({ path: '/home/fake/new' }))
-
   private readonly followConns = new Map<SessionId, ValueStreamConn<SessionFollowFrame>[]>()
   private readonly controlConns: ValueStreamConn<SessionControlFrame>[] = []
   private readonly workspaceConns: ValueStreamConn<WorkspaceFollowFrame>[] = []
@@ -210,9 +196,6 @@ export class FakeApiClient implements IApiClient {
 
   readonly host: IApiClient['host'] = {
     describe: (payload: unknown) => this.record('host.describe', payload, this.onDescribe(payload)),
-    pickDirectory: (payload: unknown) => this.record('host.pickDirectory', payload, this.onPickDirectory(payload)),
-    listDirectory: (payload: unknown) => this.record('host.listDirectory', payload, this.onListDirectory(payload)),
-    createDirectory: (payload: unknown) => this.record('host.createDirectory', payload, this.onCreateDirectory(payload)),
     openPath: (payload: unknown) => this.record('host.openPath', payload, this.onOpenPath(payload)),
   }
 
@@ -251,17 +234,7 @@ export class FakeApiClient implements IApiClient {
   }
 
   readonly settings: IApiClient['settings'] = {
-    describe: payload => this.record('settings.describe', payload, Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [] }))),
     openDocument: payload => this.record('settings.openDocument', payload, Promise.resolve(ok({ opened: true as const }))),
-    update: payload => this.record('settings.update', payload, Promise.resolve(ok({ ns: 'fake', schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 0 }))),
-    replace: payload => this.record('settings.replace', payload, Promise.resolve(ok({ ns: 'fake', schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 0 }))),
-    mutate: payload => this.record('settings.mutate', payload, Promise.resolve(ok({ ns: 'fake', schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 0 }))),
-  }
-
-  readonly credentials: IApiClient['credentials'] = {
-    describe: payload => this.record('credentials.describe', payload, Promise.resolve(ok({ credentials: {} }))),
-    set: payload => this.record('credentials.set', payload, Promise.resolve(ok({}))),
-    unset: payload => this.record('credentials.unset', payload, Promise.resolve(ok({}))),
   }
 
   readonly llm: IApiClient['llm'] = {

Vissa filer visades inte eftersom för många filer har ändrats