Kaynağa Gözat

Merge remote-tracking branch 'origin/master' into feat/session-turn-stats-display

Yif 1 ay önce
ebeveyn
işleme
d6b30db252
100 değiştirilmiş dosya ile 535 ekleme ve 319 silme
  1. 6 0
      .agents/notes/archived/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml
  2. 1 0
      .agents/notes/archived/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
  3. 1 0
      .agents/notes/archived/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md
  4. 6 0
      .agents/notes/archived/architecture/2026-08-04-websocket-downlink-carrier.i18n.yaml
  5. 1 0
      .agents/notes/archived/architecture/2026-08-04-websocket-downlink-carrier.md
  6. 1 0
      .agents/notes/archived/architecture/2026-08-04-websocket-downlink-carrier.zh.md
  7. 6 0
      .agents/notes/archived/manifest.json
  8. 2 2
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
  9. 10 10
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
  10. 10 10
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md
  11. 2 2
      .agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.i18n.yaml
  12. 2 2
      .agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md
  13. 2 2
      .agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.zh.md
  14. 2 2
      .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml
  15. 5 5
      .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md
  16. 5 5
      .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md
  17. 2 2
      .agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.i18n.yaml
  18. 3 3
      .agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md
  19. 3 3
      .agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.zh.md
  20. 2 2
      .agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.i18n.yaml
  21. 3 3
      .agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md
  22. 3 3
      .agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.zh.md
  23. 2 2
      .agents/notes/implemented/architecture/2026-07-24-web-config-tree-boot-and-transport-layering.i18n.yaml
  24. 3 3
      .agents/notes/implemented/architecture/2026-07-24-web-config-tree-boot-and-transport-layering.md
  25. 3 3
      .agents/notes/implemented/architecture/2026-07-24-web-config-tree-boot-and-transport-layering.zh.md
  26. 2 2
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml
  27. 1 1
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md
  28. 1 1
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md
  29. 2 2
      .agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.i18n.yaml
  30. 1 1
      .agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.md
  31. 1 1
      .agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.zh.md
  32. 2 2
      .agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.i18n.yaml
  33. 1 1
      .agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.md
  34. 1 1
      .agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.zh.md
  35. 2 2
      .agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.i18n.yaml
  36. 4 4
      .agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.md
  37. 4 4
      .agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.zh.md
  38. 2 2
      .agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.i18n.yaml
  39. 2 2
      .agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md
  40. 2 2
      .agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md
  41. 2 2
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml
  42. 3 3
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.md
  43. 3 3
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md
  44. 2 2
      .agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml
  45. 2 2
      .agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md
  46. 2 2
      .agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md
  47. 2 2
      .agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.i18n.yaml
  48. 3 3
      .agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md
  49. 3 3
      .agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.zh.md
  50. 2 2
      .agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.i18n.yaml
  51. 1 1
      .agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.md
  52. 1 1
      .agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.zh.md
  53. 2 2
      .agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml
  54. 1 1
      .agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md
  55. 1 1
      .agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md
  56. 2 2
      .agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.i18n.yaml
  57. 9 9
      .agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.md
  58. 9 9
      .agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.zh.md
  59. 2 2
      .agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.i18n.yaml
  60. 5 6
      .agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.md
  61. 5 6
      .agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.zh.md
  62. 3 3
      .agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml
  63. 65 0
      .agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md
  64. 65 0
      .agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md
  65. 2 2
      .agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.i18n.yaml
  66. 1 1
      .agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.md
  67. 1 1
      .agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.zh.md
  68. 2 2
      .agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.i18n.yaml
  69. 6 6
      .agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.md
  70. 6 6
      .agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.zh.md
  71. 2 2
      .agents/notes/implemented/architecture/2026-08-24-browser-token-authentication.i18n.yaml
  72. 1 1
      .agents/notes/implemented/architecture/2026-08-24-browser-token-authentication.md
  73. 1 1
      .agents/notes/implemented/architecture/2026-08-24-browser-token-authentication.zh.md
  74. 2 2
      .agents/notes/implemented/architecture/2026-08-24-cordis-runtime-tree-inspection.i18n.yaml
  75. 1 1
      .agents/notes/implemented/architecture/2026-08-24-cordis-runtime-tree-inspection.md
  76. 1 1
      .agents/notes/implemented/architecture/2026-08-24-cordis-runtime-tree-inspection.zh.md
  77. 3 3
      .agents/notes/implemented/architecture/2026-08-25-rename-code-mode-to-ptc.i18n.yaml
  78. 37 0
      .agents/notes/implemented/architecture/2026-08-25-rename-code-mode-to-ptc.md
  79. 37 0
      .agents/notes/implemented/architecture/2026-08-25-rename-code-mode-to-ptc.zh.md
  80. 2 2
      .agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.i18n.yaml
  81. 1 1
      .agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.md
  82. 1 1
      .agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.zh.md
  83. 2 2
      .agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.i18n.yaml
  84. 1 1
      .agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.md
  85. 1 1
      .agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.zh.md
  86. 2 2
      .agents/notes/implemented/architecture/2026-08-27-inspector-development-mount.i18n.yaml
  87. 4 8
      .agents/notes/implemented/architecture/2026-08-27-inspector-development-mount.md
  88. 4 8
      .agents/notes/implemented/architecture/2026-08-27-inspector-development-mount.zh.md
  89. 0 46
      .agents/notes/implemented/bug-fix/2026-08-07-code-mode-executor-collapse.md
  90. 0 46
      .agents/notes/implemented/bug-fix/2026-08-07-code-mode-executor-collapse.zh.md
  91. 3 3
      .agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.i18n.yaml
  92. 46 0
      .agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.md
  93. 46 0
      .agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.zh.md
  94. 2 2
      .agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.i18n.yaml
  95. 1 1
      .agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.md
  96. 1 1
      .agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.zh.md
  97. 2 2
      .agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.i18n.yaml
  98. 1 1
      .agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md
  99. 1 1
      .agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md
  100. 2 2
      .agents/notes/implemented/bug-fix/2026-08-18-tool-row-file-open-failure.i18n.yaml

+ 6 - 0
.agents/notes/archived/architecture/2026-07-19-gui-layering-and-rpc-protocol.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/archived/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
+2026-07-19-gui-layering-and-rpc-protocol.md: b27d8d024612d890819bfca9b43c0c81464dfdd3
+2026-07-19-gui-layering-and-rpc-protocol.zh.md: 3cf4ba6421c7332c1f8cebb61656a1546f3ad45f

+ 1 - 0
.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md → .agents/notes/archived/architecture/2026-07-19-gui-layering-and-rpc-protocol.md

@@ -1,6 +1,7 @@
 # Agent Note: GUI layering and the RPC protocol — host/client layering by capability provider, the four-quadrant message model, and the fetch carrier
 
 Status: implemented
+Archived: 2026-08-27
 
 English | [中文](2026-07-19-gui-layering-and-rpc-protocol.zh.md)
 

+ 1 - 0
.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md → .agents/notes/archived/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md

@@ -1,6 +1,7 @@
 # Agent Note: GUI 分层与 RPC 协议——host/client 按能力提供方分层、四象限消息模型与 fetch 载体
 
 Status: implemented
+Archived: 2026-08-27
 
 [English](2026-07-19-gui-layering-and-rpc-protocol.md) | 中文
 

+ 6 - 0
.agents/notes/archived/architecture/2026-08-04-websocket-downlink-carrier.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/archived/architecture/2026-08-04-websocket-downlink-carrier.md
+2026-08-04-websocket-downlink-carrier.md: 5edcdd95cf2845d455a61930a9fc00e7e57e72eb
+2026-08-04-websocket-downlink-carrier.zh.md: 213697effb8655583e7c420e58acfd171261a8d8

+ 1 - 0
.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md → .agents/notes/archived/architecture/2026-08-04-websocket-downlink-carrier.md

@@ -1,6 +1,7 @@
 # Agent Note: WebSocket carrier for browser downlinks
 
 Status: implemented
+Archived: 2026-08-27
 
 English | [中文](2026-08-04-websocket-downlink-carrier.zh.md)
 

+ 1 - 0
.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.zh.md → .agents/notes/archived/architecture/2026-08-04-websocket-downlink-carrier.zh.md

@@ -1,6 +1,7 @@
 # Agent Note: 浏览器下行 WebSocket 载体
 
 Status: implemented
+Archived: 2026-08-27
 
 [English](2026-08-04-websocket-downlink-carrier.md) | 中文
 

+ 6 - 0
.agents/notes/archived/manifest.json

@@ -25,6 +25,9 @@
     "architecture/2026-07-05-windows-fs-permissions.i18n.yaml": "sha256:7e61ee9bbd9de4bf3285a6f250d9625bd062e5fb90279dbffd64c820f1f7fe6b",
     "architecture/2026-07-05-windows-fs-permissions.md": "sha256:03734da511eae3b0736f7cad73d9da76ae2f69f9d5ed09089b0121ccb135a861",
     "architecture/2026-07-05-windows-fs-permissions.zh.md": "sha256:454848057ea905fe76c88d17264e71e71fb685f08f82088de6976878372865c3",
+    "architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml": "sha256:855477999c84236430dc9308e16797eb21658a67a72b8537315c6964ff0c0c0a",
+    "architecture/2026-07-19-gui-layering-and-rpc-protocol.md": "sha256:3517f37e98e74865dced37d5e1559d443e8fa827031c8335e99a1e910586e9ac",
+    "architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md": "sha256:8181386d957fa6d6b3eb9b05d29adb10804b5a926853425415d368cc7fceaefa",
     "architecture/2026-07-22-tui-interactive-extension-service.i18n.yaml": "sha256:1b4822af5c8d642b73e3a0b04fb0a1dea9f50d0147046fbef53f5e49c030fb91",
     "architecture/2026-07-22-tui-interactive-extension-service.md": "sha256:ca6b2774f4821e66f7c8397f20fcd34926728ded853fa48cbe451db7a8d2f883",
     "architecture/2026-07-22-tui-interactive-extension-service.zh.md": "sha256:5b060c7626ee796c27108be7467a5e4be0677d7525d383336e7ec31ddce5c303",
@@ -43,6 +46,9 @@
     "architecture/2026-07-28-dsh-native-typescript-source-launch.i18n.yaml": "sha256:af071e07bce5d9bc8f3df65fed9dcd9b3779a98c5864badbd530363bda021b55",
     "architecture/2026-07-28-dsh-native-typescript-source-launch.md": "sha256:1b56e3454277ace713e2a01c4da538c756c45bf633fd24d7b16443d584afac5d",
     "architecture/2026-07-28-dsh-native-typescript-source-launch.zh.md": "sha256:8c0f97472c2c89d2c19ae5cfa68c6e67f32b50960b08b60b46496f78ea6ffad1",
+    "architecture/2026-08-04-websocket-downlink-carrier.i18n.yaml": "sha256:b9d742d068a0e36df2f3030f6a04a3638f3461f7e10b50ed0cd6bd5e85de5019",
+    "architecture/2026-08-04-websocket-downlink-carrier.md": "sha256:b9be27a4cda8abd410c6e8b728c571f96b4891eb003c5200e9a0ffbbc9145b42",
+    "architecture/2026-08-04-websocket-downlink-carrier.zh.md": "sha256:118b71b33710a7a3d28375c48b42c1ec19993ca7e13f286dd6ea64f934456f46",
     "architecture/2026-08-11-plugin-settings-tabs.i18n.yaml": "sha256:0365da2b317fc5f94dd190064198565f4c624afc91d2e62161ab9170f79d11bc",
     "architecture/2026-08-11-plugin-settings-tabs.md": "sha256:fdd92cfe55b6c4cd31b3f768dd46a2ecf129a04c9818249cbdd33857cf722bbf",
     "architecture/2026-08-11-plugin-settings-tabs.zh.md": "sha256:8993df1a0178aba1ea35c460ee67c522900344a4b386287bba9dfac2bfb87efa",

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.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-12-agent-scope-runtime-design.md
-2026-07-12-agent-scope-runtime-design.md: 9d70b8048b1d2bb50290d34158d9deb329d5e15e
-2026-07-12-agent-scope-runtime-design.zh.md: 278bcede47fee9f67d3d2d2d7135e5357c120161
+2026-07-12-agent-scope-runtime-design.md: 7aebac35d1a5477f0b1d3857e983680ea38bd9f5
+2026-07-12-agent-scope-runtime-design.zh.md: c636bf6f51c58aa5d9f1e3b6f9988b4f4ab5b86c

+ 10 - 10
.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md

@@ -74,7 +74,7 @@ The receiver is a small carrier rather than a transparent proxy for the domain o
 
 Scope-aware registries use `ScopedLayers` to own one eager global aggregate and lazily created identity-keyed aggregates. A read resolves the global layer and at most one exact local layer; it never creates state or traverses parentage. Registration visibility and Cordis effect ownership derive from the same context, and reclamation waits until the concrete layer's complete aggregate is empty ([decision](2026-07-12-scoped-layers-store.md)).
 
-Each service retains its domain rule. Named command and prompt views use the shared insertion-ordered shadow merge; tools keep a richer resolver because restrictions filter globals before local tools are added and the reserved Code Mode transport is inserted separately. Prompt variables and tool guards retain live iteration, while tool-provider membership is materialized per assembly. Scope supplies storage lifecycle and named shadowing, not a universal registry view.
+Each service retains its domain rule. Named command and prompt views use the shared insertion-ordered shadow merge; tools keep a richer resolver because restrictions filter globals before local tools are added and the reserved PTC mode transport is inserted separately. Prompt variables and tool guards retain live iteration, while tool-provider membership is materialized per assembly. Scope supplies storage lifecycle and named shadowing, not a universal registry view.
 
 ### Fused dispatch helpers prevent subject drift
 
@@ -206,7 +206,7 @@ Tool presentation and execution share one private resolver. Prompt assembly rema
 
 ### One resolver defines the tool view
 
-The private resolver applies the current presentation mode, live global restrictions, exact local overlay, and local shadowing. Schemas, lookup, execution, Code Mode SDK generation, and restriction validation all use that resolver or its pre-restriction global-name view.
+The private resolver applies the current presentation mode, live global restrictions, exact local overlay, and local shadowing. Schemas, lookup, execution, PTC mode SDK generation, and restriction validation all use that resolver or its pre-restriction global-name view.
 
 The [subagent composition-controls Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md#tool-filtering-is-one-live-global-view-rule) owns the user-visible allow/deny semantics. The implementation requirement is agreement: a filtered-away global cannot remain executable through a different lookup path, and a locally shadowed definition is the same definition presented and executed.
 
@@ -214,11 +214,11 @@ The [subagent composition-controls Agent Note](../feature/2026-07-12-subagent-pe
 
 ### Tool execution owns identity and boundary materialization
 
-The registry assigns every execution a fresh branded `Symbol` token. Nested Code Mode calls carry the outer token as `parent`, so structured output can correlate an inner capture with its enclosing `run_code` result by identity.
+The registry assigns every execution a fresh branded `Symbol` token. Nested PTC mode calls carry the outer token as `parent`, so structured output can correlate an inner capture with its enclosing `run_code` result by identity.
 
 A fresh registry-assigned Symbol provides collision-free execution identity without a WeakSet membership registry. Callers cannot supply the execution's own token through `ToolExecutionInput`; they only receive the pipeline-owned `ToolExecution` after the registry creates it. This is a trusted typed contract, not a runtime defense against arbitrary casts or JavaScript callers.
 
-Arguments are materialized once where model/tool JSON enters the pipeline. Pre-, around-, and post-execute listeners operate on the typed execution and decisions. Call ID correlation, approval, monotonic guards, and Code Mode nesting remain explicit relational checks.
+Arguments are materialized once where model/tool JSON enters the pipeline. Pre-, around-, and post-execute listeners operate on the typed execution and decisions. Call ID correlation, approval, monotonic guards, and PTC mode nesting remain explicit relational checks.
 
 After post-execute or outer pipeline normalization, the registry losslessly snapshots the candidate result, converting a snapshot failure into an ordinary error, invokes the call's snapshotted optional `ToolDefinition.finalizeContent` callback, then materializes and freezes the accepted final result once. The callback may replace only content, so structured error identity, contexts, and metadata remain registry-owned even when a tool enforces a last-mile result bound. Every synchronous `tools/result` observer receives that exact committed object, and observer failures are contained individually. An outer pipeline or candidate-snapshot failure is normalized before final content, so observers can discard staged work against the same authoritative boundary.
 
@@ -226,9 +226,9 @@ After post-execute or outer pipeline normalization, the registry losslessly snap
 
 SystemPrompt first resolves the global-plus-agent sections, variables, and tool providers into a deterministic registry contribution. The scope-filtered `system-prompt/assemble` waterfall may then reorder, replace, add, or remove any section, variable, or schema. Its returned assembly is authoritative; there is no later restoration pass and no finality metadata on ordinary prompt sections, tool definitions, or provider results.
 
-This is a trusted same-process extension point, not an authority boundary. A listener that changes Code Mode's `run_code` schema or `tools:sdk` instructions, or a structured child's capture schema or instruction, owns preserving a coherent protocol in the assembly it returns. ToolRuntime still reserves `run_code` against ordinary tool registration and restriction because those are registry invariants, but assembly middleware remains free to transform the final model-visible surface.
+This is a trusted same-process extension point, not an authority boundary. A listener that changes PTC mode's `run_code` schema or `tools:sdk` instructions, or a structured child's capture schema or instruction, owns preserving a coherent protocol in the assembly it returns. ToolRuntime still reserves `run_code` against ordinary tool registration and restriction because those are registry invariants, but assembly middleware remains free to transform the final model-visible surface.
 
-Scope solves the real isolation problem directly. Structured-output contributions register in the child's exact scope, while Code Mode derives its transport and SDK from the same resolved tool view. A second named-protection system would need another ownership and collision rule across arbitrary schema providers—including providers that intentionally contribute duplicate names—without creating a new trust boundary.
+Scope solves the real isolation problem directly. Structured-output contributions register in the child's exact scope, while PTC mode derives its transport and SDK from the same resolved tool view. A second named-protection system would need another ownership and collision rule across arbitrary schema providers—including providers that intentionally contribute duplicate names—without creating a new trust boundary.
 
 ### Structured output commits only authoritative outcomes
 
@@ -236,11 +236,11 @@ Structured output combines child-scoped composition with a two-phase execution c
 
 For a native call, the observer deletes the stage and commits its value only when that exact execution's final result succeeds. A post-execute block or outer pipeline failure therefore cannot leave a captured value behind.
 
-For a Code Mode SDK call, the inner successful result records `{ parentToken, value }` rather than committing. The observer waits for the `run_code` execution whose token matches `parentToken` and commits only if that outer final result also succeeds. Program failure, runtime abort, or outer post-policy denial discards the pending value.
+For a PTC mode SDK call, the inner successful result records `{ parentToken, value }` rather than committing. The observer waits for the `run_code` execution whose token matches `parentToken` and commits only if that outer final result also succeeds. Program failure, runtime abort, or outer post-policy denial discards the pending value.
 
 Once a value is pending or committed, a scoped monotonic guard denies later tool calls. The successful structured-output execution calls `exec.concludeTurn()`, so its own immutable result carries `concludesTurn: true` and the loop ends the tool loop at that step. A schema-validation failure remains an ordinary `INVALID_ARGS` tool error and leaves the child able to retry within the same turn.
 
-Pure Code Mode's registry contribution omits `structured_output` from native wire schemas and exposes it through the generated SDK. The assembly waterfall may deliberately change that presentation; execution still validates against the child-scoped definition, and the listener owns the consistency of any alternate model-visible route it creates.
+Pure PTC mode's registry contribution omits `structured_output` from native wire schemas and exposes it through the generated SDK. The assembly waterfall may deliberately change that presentation; execution still validates against the child-scoped definition, and the listener owns the consistency of any alternate model-visible route it creates.
 
 ### Three execution boundaries are deliberately one-way
 
@@ -332,7 +332,7 @@ The plugin does not police trusted setup by scanning registries or reject prompt
 
 The event catalog, service catalog, producer/consumer matrix, configuration catalog, module graph, tool catalog, type-equivalence blocks, and scoped-event resolver map are generated or freshness-gated from source. The [TypeScript semantic-gates Agent Note](../process/2026-07-14-typescript-program-backed-semantic-gates.md) owns Program construction, semantic event discovery, and resolver-generation rules.
 
-Behavioral tests pin scoped routing and disposal, final-entry collision cleanup, publication rollback, ordered quiescence, durable pre/post-commit behavior, live tool filtering across presentation and execution, cooperative prompt assembly, structured-output commit in native and Code Mode, async subagent startup and signal cancellation, worker terminal arbitration, ACP settlement, and process teardown.
+Behavioral tests pin scoped routing and disposal, final-entry collision cleanup, publication rollback, ordered quiescence, durable pre/post-commit behavior, live tool filtering across presentation and execution, cooperative prompt assembly, structured-output commit in native and PTC mode, async subagent startup and signal cancellation, worker terminal arbitration, ACP settlement, and process teardown.
 
 ## Alternatives considered
 
@@ -385,7 +385,7 @@ The implementation is smaller and its proof follows the same shape as its owners
 
 Scope-aware services still maintain global and identity-keyed maps, and operations must carry their real agent explicitly. Async create/resume and subagent start require callers to await ownership transfer and dispose returned handles.
 
-A trusted `system-prompt/assemble` listener can remove or replace Code Mode and structured-output protocol pieces. This is deliberate: the listener owns final composition and must preserve any protocol the deployment expects to remain usable.
+A trusted `system-prompt/assemble` listener can remove or replace PTC mode and structured-output protocol pieces. This is deliberate: the listener owns final composition and must preserve any protocol the deployment expects to remain usable.
 
 The design trusts typed plugins in the same process. It does not defend against arbitrary casts, stateful getters, mutation that violates readonly contracts, or a plugin deliberately using ambient service access outside the supported composition API.
 

+ 10 - 10
.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md

@@ -76,7 +76,7 @@ Receiver 是一个小型载体而非领域对象的透明代理。需要 agent 
 
 作用域感知的注册表使用 `ScopedLayers`,拥有一个即时创建的全局 aggregate 和按标识键惰性创建的 aggregate。读取解析全局 layer 和至多一个精确局部 layer;它不创建状态,也从不遍历父级链。注册可见性与 Cordis effect 所有权都从同一个上下文派生,而回收会等待具体 layer 的完整 aggregate 变空(见[决策](2026-07-12-scoped-layers-store.zh.md))。
 
-每个服务保留其领域规则。命名 command 和提示词视图使用共享的、保持插入顺序的 shadow 合并;工具保留更丰富的 resolver,因为限制会在加入局部工具前过滤全局工具,保留的 Code Mode transport 则单独插入。提示词变量和工具 guard 保持实时迭代,而工具提供方成员关系按每次 assembly 物化。Scope 提供存储生命周期和命名遮蔽,而非通用的注册表视图。
+每个服务保留其领域规则。命名 command 和提示词视图使用共享的、保持插入顺序的 shadow 合并;工具保留更丰富的 resolver,因为限制会在加入局部工具前过滤全局工具,保留的 PTC mode transport 则单独插入。提示词变量和工具 guard 保持实时迭代,而工具提供方成员关系按每次 assembly 物化。Scope 提供存储生命周期和命名遮蔽,而非通用的注册表视图。
 
 ### 融合 dispatch 辅助函数防止主体漂移
 
@@ -210,7 +210,7 @@ Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造
 
 ### 一个解析器定义工具视图
 
-私有解析器应用当前展示模式、活跃的全局限制、精确的局部叠加和局部遮蔽。Schema、查找、执行、Code Mode SDK 生成和限制验证都使用该解析器或其限制前的全局名称视图。
+私有解析器应用当前展示模式、活跃的全局限制、精确的局部叠加和局部遮蔽。Schema、查找、执行、PTC mode SDK 生成和限制验证都使用该解析器或其限制前的全局名称视图。
 
 [subagent 组合控制 Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md#tool-filtering-is-one-live-global-view-rule) 拥有用户可见的 allow/deny 语义。实现要求是一致性:被过滤掉的全局工具不能通过另一条查找路径仍可执行,局部遮蔽的定义就是被展示和执行的同一个定义。
 
@@ -218,11 +218,11 @@ Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造
 
 ### 工具执行拥有标识和边界物化
 
-注册表为每次执行分配一个新的带品牌的 `Symbol` token。嵌套的 Code Mode 调用将外层 token 作为 `parent` 携带,因此结构化输出可以通过标识将内层捕获与其外层 `run_code` 结果关联。
+注册表为每次执行分配一个新的带品牌的 `Symbol` token。嵌套的 PTC mode 调用将外层 token 作为 `parent` 携带,因此结构化输出可以通过标识将内层捕获与其外层 `run_code` 结果关联。
 
 注册表分配的新 Symbol 提供无碰撞的执行标识,无需 WeakSet 成员注册表。调用方无法通过 `ToolExecutionInput` 提供执行自身的 token;它们仅在注册表创建后接收流水线拥有的 `ToolExecution`。这是一个可信的类型化约定,而非针对任意强制转换或 JavaScript 调用方的运行时防御。
 
-参数在模型/工具 JSON 进入流水线时一次性物化。Pre-、around- 和 post-execute 监听器操作类型化的 execution 和决策。Call ID 关联、审批、单调守卫和 Code Mode 嵌套仍然是显式的关系检查。
+参数在模型/工具 JSON 进入流水线时一次性物化。Pre-、around- 和 post-execute 监听器操作类型化的 execution 和决策。Call ID 关联、审批、单调守卫和 PTC mode 嵌套仍然是显式的关系检查。
 
 在 post-execute 或外层流水线完成规范化后,注册表先为候选结果创建无损快照,并将快照失败转为普通错误;随后调用在本次调用创建时已快照的可选 `ToolDefinition.finalizeContent` 回调,最后一次性物化并冻结被接受的最终结果。该回调只能替换内容,因此即使工具强制最后一道结果上限,结构化错误标识、上下文与元数据仍由注册表拥有。每个同步的 `tools/result` 观察者接收该确切的已提交对象,观察者失败被逐个隔离。外层流水线失败或候选快照失败会在最终内容处理之前被规范化,因此观察者可以丢弃针对同一权威边界的暂存工作。
 
@@ -230,9 +230,9 @@ Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造
 
 SystemPrompt 首先将全局加 agent 的段、变量和工具提供方解析为确定性的注册表贡献。作用域过滤的 `system-prompt/assemble` waterfall 随后可以重排、替换、添加或移除任何段、变量或 schema。其返回的组装结果即为权威;没有后续的恢复步骤,普通提示词段、工具定义或提供方结果上也没有终态元数据。
 
-这是一个可信的同进程扩展点,而非权限边界。修改 Code Mode 的 `run_code` schema 或 `tools:sdk` 指令,或结构化子级的捕获 schema 或指令的监听器,有责任在其返回的组装中保持协议的一致性。ToolRuntime 仍然保留 `run_code` 不受普通工具注册和限制影响,因为那些是注册表不变式,但 assembly 中间件仍然可以自由变换最终的模型可见表面。
+这是一个可信的同进程扩展点,而非权限边界。修改 PTC mode 的 `run_code` schema 或 `tools:sdk` 指令,或结构化子级的捕获 schema 或指令的监听器,有责任在其返回的组装中保持协议的一致性。ToolRuntime 仍然保留 `run_code` 不受普通工具注册和限制影响,因为那些是注册表不变式,但 assembly 中间件仍然可以自由变换最终的模型可见表面。
 
-Scope 直接解决了真正的隔离问题。结构化输出贡献注册在子级的精确作用域中,而 Code Mode 从同一个已解析的工具视图派生其传输和 SDK。第二套命名保护系统需要另一套所有权和碰撞规则来覆盖任意 schema 提供方(包括有意贡献重复名称的提供方),却不创建新的信任边界。
+Scope 直接解决了真正的隔离问题。结构化输出贡献注册在子级的精确作用域中,而 PTC mode 从同一个已解析的工具视图派生其传输和 SDK。第二套命名保护系统需要另一套所有权和碰撞规则来覆盖任意 schema 提供方(包括有意贡献重复名称的提供方),却不创建新的信任边界。
 
 <a id="structured-output-commits-only-authoritative-outcomes"></a>
 
@@ -242,11 +242,11 @@ Scope 直接解决了真正的隔离问题。结构化输出贡献注册在子
 
 对于原生调用,观察者仅在该确切执行的最终结果成功时才删除暂存并提交其值。因此 post-execute 阻止或外层流水线失败不会留下已捕获的值。
 
-对于 Code Mode SDK 调用,内层成功结果记录 `{ parentToken, value }` 而非提交。观察者等待 token 匹配 `parentToken` 的 `run_code` 执行,仅在该外层最终结果也成功时才提交。程序失败、运行时中止或外层 post-policy 拒绝会丢弃待定值。
+对于 PTC mode SDK 调用,内层成功结果记录 `{ parentToken, value }` 而非提交。观察者等待 token 匹配 `parentToken` 的 `run_code` 执行,仅在该外层最终结果也成功时才提交。程序失败、运行时中止或外层 post-policy 拒绝会丢弃待定值。
 
 一旦值处于待定或已提交状态,作用域单调守卫拒绝后续工具调用。成功的结构化输出执行会调用 `exec.concludeTurn()`,因此其自身不可变结果携带 `concludesTurn: true`,循环在该步骤结束工具循环。Schema 验证失败仍然是普通的 `INVALID_ARGS` 工具错误,子级可以在同一轮次内重试。
 
-纯 Code Mode 的注册表贡献从原生 wire schema 中省略 `structured_output`,并通过生成的 SDK 暴露它。Assembly waterfall 可以有意改变该展示;执行仍然针对子作用域定义进行验证,监听器拥有其创建的任何替代模型可见路由的一致性。
+纯 PTC mode 的注册表贡献从原生 wire schema 中省略 `structured_output`,并通过生成的 SDK 暴露它。Assembly waterfall 可以有意改变该展示;执行仍然针对子作用域定义进行验证,监听器拥有其创建的任何替代模型可见路由的一致性。
 
 <a id="three-execution-boundaries-are-deliberately-one-way"></a>
 
@@ -342,7 +342,7 @@ TypeScript 无法管控 JavaScript 强制转换、直接 Cordis dispatch、进
 
 事件目录、服务目录、生产者/消费方矩阵、配置目录、模块图、工具目录、type-equiv 块和作用域事件解析器映射都是从源码生成或受新鲜度门禁约束的。[TypeScript 语义门禁 Agent Note](../process/2026-07-14-typescript-program-backed-semantic-gates.zh.md) 拥有 Program 构造、语义事件发现和解析器生成规则。
 
-行为测试固定了作用域路由和 dispose、最终写入注册表时的碰撞清理、发布回滚、有序完全停稳、持久化前/后提交行为、跨展示和执行的活跃工具过滤、协作式提示词组装、原生和 Code Mode 中的结构化输出提交、异步 subagent 启动和信号取消、worker 终端仲裁、ACP 结算和进程拆除。
+行为测试固定了作用域路由和 dispose、最终写入注册表时的碰撞清理、发布回滚、有序完全停稳、持久化前/后提交行为、跨展示和执行的活跃工具过滤、协作式提示词组装、原生和 PTC mode 中的结构化输出提交、异步 subagent 启动和信号取消、worker 终端仲裁、ACP 结算和进程拆除。
 
 ## 曾考虑的替代方案
 
@@ -395,7 +395,7 @@ Worker 消息、进程死亡和持久化输入确实跨越所有权和序列化
 
 作用域感知服务仍然维护全局和按标识键索引的映射,操作必须显式携带其真实 agent。异步创建/恢复和 subagent start 要求调用方等待所有权转移并 dispose 返回的句柄。
 
-可信的 `system-prompt/assemble` 监听器可以移除或替换 Code Mode 和结构化输出协议片段。这是有意为之:监听器拥有最终组合,必须保持部署期望仍可用的任何协议。
+可信的 `system-prompt/assemble` 监听器可以移除或替换 PTC mode 和结构化输出协议片段。这是有意为之:监听器拥有最终组合,必须保持部署期望仍可用的任何协议。
 
 该设计信任同进程中的类型化插件。它不防御任意强制转换、有状态 getter、违反 readonly 约定的修改,或插件有意在支持的组合 API 之外使用环境服务访问。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.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-19-cooperative-tool-cancellation.md
-2026-07-19-cooperative-tool-cancellation.md: 781202688a5cbcd7076ee694fc7dd9489683d8e8
-2026-07-19-cooperative-tool-cancellation.zh.md: ec35734eef91c5c774d1be814b221e9fdb8f65fa
+2026-07-19-cooperative-tool-cancellation.md: 5ca2b44b24a4af189df27c6f724021a7bf290e2b
+2026-07-19-cooperative-tool-cancellation.zh.md: d6d49fb44c5d5629729a8b5b4bad07ea9b2f7ee9

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md

@@ -16,7 +16,7 @@ Cancellation can arrive before policy, during approval, inside an around-dispatc
 
 `ToolExecutionInput.signal` is a required readonly `AbortSignal`. `ToolExecution.signal` and `ToolRunContext.signal` are therefore required and readonly as well. Every typed caller supplies the signal it owns; the registry provides no overload, default controller, never-abort sentinel, or convenience execution path.
 
-`ToolDefinition.execute(args, exec)` keeps its existing signature. `defineTool()` contextually types `exec.signal` as a required `AbortSignal`, so every registered TypeScript tool can observe or forward cancellation without a cast. First-party direct callers and nested Code Mode dispatches pass their current operation signal explicitly.
+`ToolDefinition.execute(args, exec)` keeps its existing signature. `defineTool()` contextually types `exec.signal` as a required `AbortSignal`, so every registered TypeScript tool can observe or forward cancellation without a cast. First-party direct callers and nested PTC mode dispatches pass their current operation signal explicitly.
 
 The registry trusts this typed same-process contract. It does not perform runtime `AbortSignal` validation or add hostile-input tests for an omitted or malformed signal. Validation remains at parser/config, model/tool JSON, durable/file, worker, process, and wire boundaries; untyped JavaScript that violates the TypeScript interface has no compatibility contract.
 
@@ -46,7 +46,7 @@ This decision requires cancellation at the tool invocation boundary only. Making
 
 ## Verification
 
-[`execution-signal-types.spec.ts`](../../../../packages/core/tools/tests/execution-signal-types.spec.ts) proves the required exact signal types, readonly observer and tool views, mutable-but-required around-dispatch view, and `defineTool()` inference. [`tools.spec.ts`](../../../../packages/core/tools/tests/tools.spec.ts) covers pre-aborted materialization, phase skipping, policy and wrapper races, body invocation classification, caller-signal fusion, error precedence, context retention, and quiescent drainage. [`tool-calls.spec.ts`](../../../../packages/core/agent-loop/tests/tool-calls.spec.ts) and [`contract-regressions.spec.ts`](../../../../packages/core/agent-loop/tests/contract-regressions.spec.ts) cover balanced durable results for undispatched siblings. [`code-mode.spec.ts`](../../../../packages/core/tools/tests/code-mode.spec.ts) and first-party integration suites cover explicit forwarding, while [`timeout-policy.spec.ts`](../../../../packages/guard/timeout-policy/tests/timeout-policy.spec.ts) preserves timeout ownership.
+[`execution-signal-types.spec.ts`](../../../../packages/core/tools/tests/execution-signal-types.spec.ts) proves the required exact signal types, readonly observer and tool views, mutable-but-required around-dispatch view, and `defineTool()` inference. [`tools.spec.ts`](../../../../packages/core/tools/tests/tools.spec.ts) covers pre-aborted materialization, phase skipping, policy and wrapper races, body invocation classification, caller-signal fusion, error precedence, context retention, and quiescent drainage. [`tool-calls.spec.ts`](../../../../packages/core/agent-loop/tests/tool-calls.spec.ts) and [`contract-regressions.spec.ts`](../../../../packages/core/agent-loop/tests/contract-regressions.spec.ts) cover balanced durable results for undispatched siblings. [`ptc.spec.ts`](../../../../packages/core/tools/tests/ptc.spec.ts) and first-party integration suites cover explicit forwarding, while [`timeout-policy.spec.ts`](../../../../packages/guard/timeout-policy/tests/timeout-policy.spec.ts) preserves timeout ownership.
 
 No registry test can prove that arbitrary third-party same-process code observes the signal or stops in bounded time. Capability tests continue to prove cancellation and quiescence at the boundary that owns each side effect.
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.zh.md

@@ -16,7 +16,7 @@ Status: implemented
 
 `ToolExecutionInput.signal` 是必填且只读的 `AbortSignal`,因此 `ToolExecution.signal` 和 `ToolRunContext.signal` 也都是必填且只读。每个类型化调用方显式提供自己持有的信号;注册表不提供重载、默认控制器、永不中止哨兵或便捷执行路径。
 
-`ToolDefinition.execute(args, exec)` 保持现有签名。`defineTool()` 会把 `exec.signal` 上下文推断为必填的 `AbortSignal`,因此每个已注册的 TypeScript 工具都能在无需类型断言的情况下观察或转发取消。所有第一方直接调用方和 Code Mode 嵌套调度都会显式传入当前操作的信号。
+`ToolDefinition.execute(args, exec)` 保持现有签名。`defineTool()` 会把 `exec.signal` 上下文推断为必填的 `AbortSignal`,因此每个已注册的 TypeScript 工具都能在无需类型断言的情况下观察或转发取消。所有第一方直接调用方和 PTC mode 嵌套调度都会显式传入当前操作的信号。
 
 注册表信任这份类型化同进程约定。它不在运行时校验 `AbortSignal`,也不为缺失或畸形信号添加敌意输入测试。校验仍位于解析器与配置、模型与工具 JSON、持久化与文件、worker、进程和协议边界;违反 TypeScript 接口的无类型 JavaScript 不享有兼容性约定。
 
@@ -46,7 +46,7 @@ Status: implemented
 
 ## 验证
 
-[`execution-signal-types.spec.ts`](../../../../packages/core/tools/tests/execution-signal-types.spec.ts) 证明必填的精确信号类型、观察者与工具的只读视图、环绕调度可替换但不可删除的视图,以及 `defineTool()` 推断。[`tools.spec.ts`](../../../../packages/core/tools/tests/tools.spec.ts) 覆盖进入时已中止的物化与阶段跳过、策略和包装层竞态、工具主体调用分类、调用方信号融合、错误优先级、上下文保留和完全停稳。[`tool-calls.spec.ts`](../../../../packages/core/agent-loop/tests/tool-calls.spec.ts) 与 [`contract-regressions.spec.ts`](../../../../packages/core/agent-loop/tests/contract-regressions.spec.ts) 覆盖为未调度的同批调用补齐持久化结果。[`code-mode.spec.ts`](../../../../packages/core/tools/tests/code-mode.spec.ts) 和第一方集成测试覆盖显式转发,[`timeout-policy.spec.ts`](../../../../packages/guard/timeout-policy/tests/timeout-policy.spec.ts) 保持超时归属。
+[`execution-signal-types.spec.ts`](../../../../packages/core/tools/tests/execution-signal-types.spec.ts) 证明必填的精确信号类型、观察者与工具的只读视图、环绕调度可替换但不可删除的视图,以及 `defineTool()` 推断。[`tools.spec.ts`](../../../../packages/core/tools/tests/tools.spec.ts) 覆盖进入时已中止的物化与阶段跳过、策略和包装层竞态、工具主体调用分类、调用方信号融合、错误优先级、上下文保留和完全停稳。[`tool-calls.spec.ts`](../../../../packages/core/agent-loop/tests/tool-calls.spec.ts) 与 [`contract-regressions.spec.ts`](../../../../packages/core/agent-loop/tests/contract-regressions.spec.ts) 覆盖为未调度的同批调用补齐持久化结果。[`ptc.spec.ts`](../../../../packages/core/tools/tests/ptc.spec.ts) 和第一方集成测试覆盖显式转发,[`timeout-policy.spec.ts`](../../../../packages/guard/timeout-policy/tests/timeout-policy.spec.ts) 保持超时归属。
 
 任何注册表测试都无法证明任意第三方同进程代码会观察信号或在有界时间内停止。各能力的测试仍需在拥有相应副作用的边界证明取消与完全停稳。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.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-19-gui-web-client-architecture.md
-2026-07-19-gui-web-client-architecture.md: 4448fd5c6871d67b30b71cfe4377682639704235
-2026-07-19-gui-web-client-architecture.zh.md: e8a8121a1a495db7f5392e288f3bcada92c70495
+2026-07-19-gui-web-client-architecture.md: 703bf2b873eee8afc7e13f89acba99b06fc98745
+2026-07-19-gui-web-client-architecture.zh.md: d61cc8119929b3efc912221df3343918e4851308

+ 5 - 5
.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md

@@ -4,7 +4,7 @@ Status: implemented
 
 English | [中文](2026-07-19-gui-web-client-architecture.zh.md)
 
-> Division of labor: the channel-independent layering model and RPC protocol (message model / type system / contract face / client base class) are in the [layering and RPC protocol note](2026-07-19-gui-layering-and-rpc-protocol.md); this document = the browser side: how the client cordis tree loads, how UI plugins compose through slots and services, and how the React-free object layer feeds React through immutable snapshots.
+> Division of labor: the historical channel-independent layering model and RPC protocol are recorded in the [archived layering and RPC protocol note](../../archived/architecture/2026-07-19-gui-layering-and-rpc-protocol.md); this document = the browser side: how the client cordis tree loads, how UI plugins compose through slots and services, and how the React-free object layer feeds React through immutable snapshots.
 
 ## Problem
 
@@ -17,7 +17,7 @@ Both ends run cordis. The host is a cordis plugin tree; the browser runs a secon
 ```
 ┌─ Host ─────────────────────────┐   ┌─ Browser ─────────────────────────────────────────┐
 │ sessions/agents/SessionLog     │   │ client cordis root ctx                             │
-│ apiproxy: RPC + mux/host 双流  │◀─▶│  ├ vendored Loader + ctx.modules(内核,壳静态持有)│
+│ Connection + Gateway: RPC/events│◀─▶│  ├ vendored Loader + ctx.modules(内核,壳静态持有)│
 │ webserver:                     │   │  ├ immediately entries: connection/runtime/        │
 │  ├ GET /plugins/<id>/client.js │   │  │   ui-theme/i18n(fetch bundle,boot 预拉)       │
 │  └ GET / 注入 __DSH_BOOT__ 图  │   │  ├ lazy entries: layout/sidebar/                   │
@@ -42,7 +42,7 @@ Implementation homes: registry core and the props-share types live in `packages/
 
 ## Services and scope addressing
 
-A service is a plugin's only API toward other plugins (UI components and injection faces are not APIs; a plugin nobody calls mounts no service — ui-trajectory is the minimal-plugin exemplar: no ctx service, only view-slot registrations). The roster: `ctx.connection` (api client + stream handles), `ctx.slots` (registry wrapper emitting `slots/changed`, render entry, renderer installation contract), `ctx.sessions` (list store, current-session state, scope tree), `ctx.loader`, `ctx.theme`, `ctx.i18n`, `ctx.layout` (cross-plugin view navigation), `ctx.conversation` (send/cancel/startSession). Viewing state that used to live in service stores (panel widths, selection, drafts) now lives in entry-declared stores per the [slot system standard](2026-07-22-slot-type-chain-implementation.md).
+A service is a plugin's only API toward other plugins (UI components and injection faces are not APIs; a plugin nobody calls mounts no service — ui-trajectory is the minimal-plugin exemplar: no ctx service, only view-slot registrations). The roster: `ctx.connection` (RPC transport + generation state), `ctx.slots` (registry wrapper emitting `slots/changed`, render entry, renderer installation contract), `ctx.sessions` (list store, current-session state, scope tree), `ctx.loader`, `ctx.theme`, `ctx.i18n`, `ctx.layout` (cross-plugin view navigation), `ctx.conversation` (send/cancel/startSession). Viewing state that used to live in service stores (panel widths, selection, drafts) now lives in entry-declared stores per the [slot system standard](2026-07-22-slot-type-chain-implementation.md).
 
 There is no component registration model besides slots — the former view and tool rings both dissolved into it. Conversation views are entries of the `'conversation.view'` list slot ui-conversation declares, tab metadata rides the registration options (`id`/`order`/`label`), and per-view chrome lives inside the view components themselves. Final Chat business Nodes dispatch through the keyed/session `'conversation.chat.node'` slot; ui-tool owns its `tool-call` entry, recursively renders the supplied `subCalls`, and declares the keyed/session `'tool.call.toolview'` child slot. The key space stays runtime-open (SlotMap declares slots, never keys), and roots and descendants dispatch by `entryKey: toolName` with `GenericToolCard` as the fallback. Business packages register atomic views through `ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({ name: 'tool.call.toolview', key: '<tool>' }, Row))`; the declaration is the load and reload dependency ([decision](2026-08-05-slot-declaration-injection.md)). ui-conversation separately delegates the selected call's details body through `'conversation.details.tool'`, so ui-tool's card models remain the single presentation owner without making conversation import Tool components. The target-neutral event and view registries are data assembly seams rather than parallel component registries ([decision](2026-08-09-client-conversation-node-assembly.md)).
 
@@ -53,7 +53,7 @@ There is no component registration model besides slots — the former view and t
 Frames enter, snapshots exit, the Conversation assembler sits between — React-free (zero React imports, grep-assertable):
 
 ```
-mux/host frames (ConnectionController pump, injected sinks)
+$events frames (ConnectionController pump, injected sinks)
         │
         ▼
 SessionManager.handleMuxEnvelope / handleHostEnvelope
@@ -73,7 +73,7 @@ Notifier 微任务合批 ──► ConversationSnapshot 缓存 ──uSES──
 - **SessionManager** (manager.ts): instance cluster + frame entry + the session list. sessionId-bearing frames go only to existing instances (a mux broadcast must not instantiate every session); approval/question `requested` frames are the exception — they never land in history, so they buffer in `pendingBuffers` and replay on instantiation.
 - **Notifier** (notifier.ts): two channels chosen by change source. `markDirty()` (default; frame-driven changes always) batches per microtask — N changes, one notification, one re-render; the flush rebuilds the snapshot cache before notifying. `notifyNow()` (only direct echoes of user gestures) rebuilds and notifies in the same tick — controlled inputs roll the DOM back and jump the caret if their echo defers to a microtask. Frame-driven code using notifyNow collapses batching back to per-frame renders; banned.
 - **ConversationNodeAssembler** (`runtime/src/client/conversation/`): the Session-owned incremental engine runs independently registered Definitions over raw events. `match(event)` selects `(kind, id)` without Context scans; start/update build Definition state; engine-computed Locations carry Turn/Step closure; backward Context reads record dependencies repaired by later prepends; `buildViewNode(target)` materializes only dirty Contexts. The Chat builder preserves structural order and per-key value identity, `useSession` selectors isolate consumption, and Assistant token publication coalesces to one animation frame. The [Conversation Node decision](2026-08-09-client-conversation-node-assembly.md) owns assembly, while [Tool presentation ownership](2026-08-08-client-tool-presentation-ownership.md) owns recursive Tool rendering.
-- **ConnectionController** (in `packages/client/connection`): opens the mux/host streams, pumps with for-await, reconnects with exponential backoff (500ms doubling to 10s, jitter, unlimited) behind a generation fence; sinks are injected one-way (the Controller does not know Session). Reconnect = rebuild: `onConnected` → list refresh + per-open-session resync. The object layer faces only `IApiClient`; Web carriage uses HTTP POST for the two client→server quadrants and [one WebSocket per logical stream](2026-08-04-websocket-downlink-carrier.md) for the two server→client quadrants, while the client class family remains the layering note's territory.
+- **ConnectionController** (in `packages/client/connection`): opens the `$events` Remote stream, pumps with for-await, and reconnects with exponential backoff (500ms doubling to 10s, jitter, unlimited) behind a generation fence; sinks are injected one-way (the Controller does not know Session). Reconnect = rebuild: `onConnected` → list refresh + per-open-session resync. The object layer calls generated namespaces through `ctx.remote`; Web carriage uses HTTP POST for unary Remote calls and API Gateway's WebSocket mux for logical streams, while Connection owns request transport and generations.
 
 ## The React face (`packages/client/ui-renderer`)
 

+ 5 - 5
.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md

@@ -4,7 +4,7 @@ Status: implemented
 
 [English](2026-07-19-gui-web-client-architecture.md) | 中文
 
-> 分工线:通道无关的分层模型与 RPC 协议(消息模型/类型体系/约定面/客户端基类)见 [分层与 RPC 协议笔记](2026-07-19-gui-layering-and-rpc-protocol.zh.md);本篇 = 浏览器侧:client cordis 树如何装载、UI 插件如何经 slot 与服务组合、React-free 对象层如何以不可变快照供给 React。
+> 分工线:历史上的通道无关分层模型与 RPC 协议见[已归档的分层与 RPC 协议笔记](../../archived/architecture/2026-07-19-gui-layering-and-rpc-protocol.md);本篇 = 浏览器侧:client cordis 树如何装载、UI 插件如何经 slot 与服务组合、React-free 对象层如何以不可变快照供给 React。
 
 ## Problem
 
@@ -17,7 +17,7 @@ Status: implemented
 ```
 ┌─ Host ─────────────────────────┐   ┌─ Browser ─────────────────────────────────────────┐
 │ sessions/agents/SessionLog     │   │ client cordis root ctx                             │
-│ apiproxy: RPC + mux/host 双流  │◀─▶│  ├ vendored Loader + ctx.modules(内核,壳静态持有)│
+│ Connection + Gateway: RPC/events│◀─▶│  ├ vendored Loader + ctx.modules(内核,壳静态持有)│
 │ webserver:                     │   │  ├ immediately entries: connection/runtime/        │
 │  ├ GET /plugins/<id>/client.js │   │  │   ui-theme/i18n(fetch bundle,boot 预拉)       │
 │  └ GET / 注入 __DSH_BOOT__ 图  │   │  ├ lazy entries: layout/sidebar/                   │
@@ -42,7 +42,7 @@ slot 体系有自己的笔记——[slot 体系标准](2026-07-22-slot-type-chai
 
 ## 服务与 scope 寻址
 
-服务是插件对其他插件的唯一 API(UI 组件与注入面都不是 API;无人调用的插件不挂服务——ui-trajectory 即最小插件样板:无 ctx 服务,只做视图 slot 注册)。名册:`ctx.connection`(api client + 流句柄)、`ctx.slots`(注册表包装层,发 `slots/changed`,渲染入口,渲染器安装约定)、`ctx.sessions`(列表 store、当前会话状态、scope 树)、`ctx.loader`、`ctx.theme`、`ctx.i18n`、`ctx.layout`(跨插件视图导航)、`ctx.conversation`(send/cancel/startSession)。过去住在服务 store 里的观看态(面板宽、选中、草稿)现按 [slot 体系标准](2026-07-22-slot-type-chain-implementation.zh.md) 住 entry 声明的 store。
+服务是插件对其他插件的唯一 API(UI 组件与注入面都不是 API;无人调用的插件不挂服务——ui-trajectory 即最小插件样板:无 ctx 服务,只做视图 slot 注册)。名册:`ctx.connection`(RPC 传输 + generation 状态)、`ctx.slots`(注册表包装层,发 `slots/changed`,渲染入口,渲染器安装约定)、`ctx.sessions`(列表 store、当前会话状态、scope 树)、`ctx.loader`、`ctx.theme`、`ctx.i18n`、`ctx.layout`(跨插件视图导航)、`ctx.conversation`(send/cancel/startSession)。过去住在服务 store 里的观看态(面板宽、选中、草稿)现按 [slot 体系标准](2026-07-22-slot-type-chain-implementation.zh.md) 住 entry 声明的 store。
 
 slot 之外不存在第二种组件注册模型——原视图环与工具环都已溶解进来。会话视图即 ui-conversation 声明的 `'conversation.view'` list slot entry,tab 元数据随注册 options(`id`/`order`/`label`)走,per-view chrome 住视图组件自身。最终 Chat 业务 Node 通过 keyed/session `'conversation.chat.node'` slot 分发;ui-tool 拥有其中的 `tool-call` entry,递归渲染传入的 `subCalls`,并声明 keyed/session `'tool.call.toolview'` 子 slot。key 空间仍在运行时开放(SlotMap 声明 slot、从不声明 key),root 与任意深度的后代都按 `entryKey: toolName` 分发,以 `GenericToolCard` 兜底。业务包通过 `ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({ name: 'tool.call.toolview', key: '<tool>' }, Row))` 注册原子视图;声明本身就是加载与重载依赖([决策](2026-08-05-slot-declaration-injection.zh.md))。ui-conversation 还通过 `'conversation.details.tool'` 委托 selected call 的详情正文,使 ui-tool 的 card model 保持为唯一展示所有者,同时避免 conversation 导入 Tool 组件。与 target 无关的事件注册表和视图注册表是数据组装 seam,不是平行组件注册表([决策](2026-08-09-client-conversation-node-assembly.zh.md))。
 
@@ -53,7 +53,7 @@ slot 之外不存在第二种组件注册模型——原视图环与工具环都
 帧从这里进、快照从这里出、Conversation assembler 坐在中间——React-free(零 React import,grep 可断言):
 
 ```
-mux/host frames (ConnectionController pump, injected sinks)
+$events frames (ConnectionController pump, injected sinks)
         │
         ▼
 SessionManager.handleMuxEnvelope / handleHostEnvelope
@@ -73,7 +73,7 @@ Notifier 微任务合批 ──► ConversationSnapshot 缓存 ──uSES──
 - **SessionManager**(manager.ts):实例簇 + 帧总入口 + 会话列表。带 sessionId 的帧只投已存在实例(mux 广播不得把每个会话都实例化);例外是审批/问答 `requested` 帧——它们不落 history、open 无法回补,故缓冲进 `pendingBuffers`,实例化时回放。
 - **Notifier**(notifier.ts):两条通知通道,按变更来源取用。`markDirty()`(默认;帧驱动一律用它)按微任务合批——N 次变更、一次通知、一次重渲染;flush 先重建快照缓存再通知。`notifyNow()`(仅用户手势的直接回响)同 tick 重建并通知——受控输入的回响若延到微任务,DOM 会回滚、光标跳尾。帧驱动代码用 notifyNow 会让合批塌回逐帧渲染;禁。
 - **ConversationNodeAssembler**(`runtime/src/client/conversation/`):Session 拥有的增量引擎在原始事件上运行各自独立注册的 Definition。`match(event)` 无须扫描 Context 即可选出 `(kind, id)`;start/update 构造 Definition state;引擎计算的 Location 携带 Turn/Step 关闭信息;向前查询 Context 时记录依赖,并由后续 prepend 修复;`buildViewNode(target)` 只物化 dirty Context。Chat builder 保留结构顺序和 per-key value identity,`useSession` selector 负责消费隔离,Assistant token 发布则合并到每个 animation frame 一次。[Conversation Node 决策](2026-08-09-client-conversation-node-assembly.zh.md)拥有组装边界,[Tool 展示所有权](2026-08-08-client-tool-presentation-ownership.zh.md)拥有 Tool 递归渲染。
-- **ConnectionController**(在 `packages/client/connection`):开 mux/host 双流、for-await 泵入,代际围栏之内指数退避重连(500ms 翻倍至 10s 封顶、抖动、无限重试);sinks 单向注入(Controller 不认识 Session)。重连 = 重建:`onConnected` → 列表刷新 + 各已打开会话 resync。对象层只面向 `IApiClient`;Web 承载以 HTTP POST 载两个 client→server 象限、以[每逻辑流一条 WebSocket](2026-08-04-websocket-downlink-carrier.zh.md)载两个 server→client 象限,客户端类族归分层笔记属地。
+- **ConnectionController**(位于 `packages/client/connection`):打开 `$events` Remote 流、通过 for-await 泵入,并在 generation 围栏内指数退避重连(500ms 翻倍至 10s 封顶、抖动、无限重试);sink 单向注入,Controller 不认识 Session。重连即重建:`onConnected` → 列表刷新 + 各已打开会话 resync。对象层通过 `ctx.remote` 调用生成的命名空间;Web 载体以 HTTP POST 承载 Remote 一元调用,以 API Gateway 的 WebSocket mux 承载逻辑流,Connection 则拥有请求传输与 generation。
 
 ## React 面(`packages/client/ui-renderer`)
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.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-20-canonical-tool-output-contract.md
-2026-07-20-canonical-tool-output-contract.md: 0b3bd788fd1ab63aa6929827ef82e5ba732e5324
-2026-07-20-canonical-tool-output-contract.zh.md: bbcd519de8d02df14be931b44f8dc44fc06b8f6a
+2026-07-20-canonical-tool-output-contract.md: 52b56a44a747a07e13e05511d6d40a85fcd9696d
+2026-07-20-canonical-tool-output-contract.zh.md: a78defb2b45143d46fed2bedf760df12744621a5

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md

@@ -6,7 +6,7 @@ English | [中文](2026-07-20-canonical-tool-output-contract.zh.md)
 
 ## Problem
 
-Tool bodies previously authored model-facing `ContentBlock[]` directly, optionally wrapping it with opaque `meta`. Native function calling therefore had a usable human projection, but a programmatic caller had no stable domain value: Code Mode flattened the blocks back into a string, dynamic tools repeated the content shape, and policy could replace presentation without any way to distinguish that change from replacing the operation's result. Several capability seams already returned richer provider values only to discard them at their model-facing tool boundary.
+Tool bodies previously authored model-facing `ContentBlock[]` directly, optionally wrapping it with opaque `meta`. Native function calling therefore had a usable human projection, but a programmatic caller had no stable domain value: PTC mode flattened the blocks back into a string, dynamic tools repeated the content shape, and policy could replace presentation without any way to distinguish that change from replacing the operation's result. Several capability seams already returned richer provider values only to discard them at their model-facing tool boundary.
 
 The durable session contract made that presentation authoritative for replay, but persisting every rich intermediate value would enlarge logs, expose implementation data to compaction and migration, and incorrectly turn an execution-local API into session format. The foundation instead needs one typed value during execution and an explicit projection into the existing durable/model-facing content.
 
@@ -34,7 +34,7 @@ type ToolExecutionResult =
 
 `tools/post-execute` has two mutually exclusive successful projections. Replacing `content` changes only Native/model presentation and preserves the canonical value and metadata. Replacing `value` revalidates the replacement and recomputes both presentation projections. A block removes the value and becomes a failure. Content replacement is therefore not a confidentiality mechanism: policy that must prevent programmatic access blocks the call or replaces the value.
 
-Canonical values are execution-local. The agent loop persists `tool/result` with only `content`, `error`, and optional `meta`; Code Mode's `tool/code-dispatch` persists the sub-call's rendered `content` and `isError`. Neither event stores the canonical intermediate value, so replay reproduces presentation but cannot reconstruct the programmatic result. When a tool declares `presentationMeta`, it is computed only for a direct surface call; a nested Code dispatch gets no metadata or result card. The outer `run_code` card instead reads final post-policy content and declares no presentation metadata. Generic and tool-owned spill projections similarly skip nested dispatches, whose canonical value never enters model context.
+Canonical values are execution-local. The agent loop persists `tool/result` with only `content`, `error`, and optional `meta`; PTC mode's `tool/code-dispatch` persists the sub-call's rendered `content` and `isError`. Neither event stores the canonical intermediate value, so replay reproduces presentation but cannot reconstruct the programmatic result. When a tool declares `presentationMeta`, it is computed only for a direct surface call; a nested Code dispatch gets no metadata or result card. The outer `run_code` card instead reads final post-policy content and declares no presentation metadata. Generic and tool-owned spill projections similarly skip nested dispatches, whose canonical value never enters model context.
 
 The first-party tools preserve their existing Native text while returning domain DTOs:
 
@@ -66,7 +66,7 @@ MCP bridges preserve protocol blocks through `McpResult<{...}> = { content: Json
 
 ## Alternatives considered
 
-- **Return rendered text to Code Mode:** rejected because callers would continue scraping prose for job ids, mount ids, paths, and structured provider results.
+- **Return rendered text to PTC mode:** rejected because callers would continue scraping prose for job ids, mount ids, paths, and structured provider results.
 - **Persist canonical values on `tool/result`:** rejected because nested execution values are not model history, need not survive replay, and would create a session-format and storage commitment unrelated to Native reconstruction.
 - **Let tools return both value and content:** rejected because two author-owned results can disagree and policy cannot state which one is authoritative. The renderer makes presentation a deterministic projection of the validated value.
 - **Treat content replacement as value redaction:** rejected because presentation and programmatic access are different consumers; hiding only the former would create a false security boundary.

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.zh.md

@@ -6,7 +6,7 @@ Status: implemented
 
 ## 问题
 
-工具主体过去直接编写面向模型的 `ContentBlock[]`,并可选择将其与不透明的 `meta` 包装在一起。因此,Native 模式的 Function Calling(函数调用)虽然拥有可供人阅读的投影,但程序化调用方没有稳定的领域值:Code Mode 会将内容块重新展平为字符串,动态工具会重复定义内容形态,策略也可以替换展示内容,却无法区分这项变更究竟是替换展示,还是替换操作结果。多个能力 seam 已经返回了信息更丰富的提供方值,却又在面向模型的工具边界丢弃这些值。
+工具主体过去直接编写面向模型的 `ContentBlock[]`,并可选择将其与不透明的 `meta` 包装在一起。因此,Native 模式的 Function Calling(函数调用)虽然拥有可供人阅读的投影,但程序化调用方没有稳定的领域值:PTC mode 会将内容块重新展平为字符串,动态工具会重复定义内容形态,策略也可以替换展示内容,却无法区分这项变更究竟是替换展示,还是替换操作结果。多个能力 seam 已经返回了信息更丰富的提供方值,却又在面向模型的工具边界丢弃这些值。
 
 持久会话约定将这份展示内容视为回放时的权威来源,但如果持久化每一个信息丰富的中间值,就会扩大日志、使实现数据进入压缩(compaction)和迁移流程,还会错误地把执行期本地 API 变成会话格式的一部分。因此,系统底层需要在执行期间保留一个类型化值,并显式将其投影为现有的持久化内容和模型可见内容。
 
@@ -34,7 +34,7 @@ type ToolExecutionResult =
 
 `tools/post-execute` 为成功结果提供两种互斥的投影方式。替换 `content` 只改变 Native/模型展示,并保留规范值和元数据。替换 `value` 会重新校验替代值,并重新计算两份展示投影。阻止操作会移除值并转为失败。因此,替换内容并不是保密机制:必须阻止程序化访问的策略,应当阻止调用或替换值。
 
-规范值仅存在于执行期间。agent loop(智能体循环)持久化的 `tool/result` 只包含 `content`、`error` 和可选的 `meta`;Code Mode 的 `tool/code-dispatch` 持久化子调用渲染后的 `content` 与 `isError`。两个事件都不存储规范中间值,因此回放可以重现展示,却无法重建程序化结果。当工具声明 `presentationMeta` 时,系统只会为直接的外层调用计算它;嵌套 Code 分发没有元数据或结果卡片。外层 `run_code` 卡片则读取最终的 post-policy 内容,并且不声明展示元数据。通用以及工具自有的 spill 投影同样跳过嵌套分发,因为它们的规范值永远不会进入模型上下文。
+规范值仅存在于执行期间。agent loop(智能体循环)持久化的 `tool/result` 只包含 `content`、`error` 和可选的 `meta`;PTC mode 的 `tool/code-dispatch` 持久化子调用渲染后的 `content` 与 `isError`。两个事件都不存储规范中间值,因此回放可以重现展示,却无法重建程序化结果。当工具声明 `presentationMeta` 时,系统只会为直接的外层调用计算它;嵌套 Code 分发没有元数据或结果卡片。外层 `run_code` 卡片则读取最终的 post-policy 内容,并且不声明展示元数据。通用以及工具自有的 spill 投影同样跳过嵌套分发,因为它们的规范值永远不会进入模型上下文。
 
 第一方工具在保持现有 Native 文本不变的同时返回领域 DTO:
 
@@ -66,7 +66,7 @@ MCP 桥接层通过 `McpResult<{...}> = { content: JsonValue[]; structuredConten
 
 ## 备选方案
 
-- **向 Code Mode 返回渲染后的文本:**不予采纳。调用方仍需从自然语言中提取 job id、挂载 id、路径和结构化提供方结果。
+- **向 PTC mode 返回渲染后的文本:**不予采纳。调用方仍需从自然语言中提取 job id、挂载 id、路径和结构化提供方结果。
 - **在 `tool/result` 上持久化规范值:**不予采纳。嵌套执行值不属于模型历史记录,无需在回放后继续存在;持久化还会引入与 Native 重建无关的会话格式和存储承诺。
 - **允许工具同时返回值和内容:**不予采纳。由作者分别维护的两份结果可能互相矛盾,策略也无法说明哪一份才是权威结果。渲染器会根据已校验值确定性地产生展示。
 - **将内容替换视为值脱敏:**不予采纳。展示内容和程序化访问面向不同消费方;只隐藏前者会制造虚假的安全边界。

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.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-23-client-plugin-loading-model.md
-2026-07-23-client-plugin-loading-model.md: 0fe4e86410f3b313ec5a31099d5a6ed1f828585b
-2026-07-23-client-plugin-loading-model.zh.md: 386b0edb722d8cedd9325c941f9b392b8cdc8ae2
+2026-07-23-client-plugin-loading-model.md: 21bad78792c6b5aad48b51f454f6c08c0400ad72
+2026-07-23-client-plugin-loading-model.zh.md: 2758f28f3bd34131ece3bed74152fbfe0b36174e

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md

@@ -53,8 +53,8 @@ What happens between `dsh web` starting and the UI appearing? Three stages: the
 **Host side — compose the graph.**
 
 1. The composing app (`apps/cli`) ships the roster as ordinary rows in its `cordis.yml` config tree — client plugin packages are entry rows like every host plugin, including the always-mounted `client-hmr` row. A roster row that fails to import is caught by `assertEntriesLoaded`; a row whose fiber rejects is reported with its original stack by `assertEntriesActivated` ([host boot decision](2026-07-24-web-config-tree-boot-and-transport-layering.md)).
-2. The `dsh-client-modules` node half (the package is dual-face: its browser half is the module table) scans loader entries' package.json `dsh.client` declarations and composes `window.__DSH_BOOT__`: `{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }], batches: [{ phase, url, rev, entries }] }`. The row's three optional fields come from manifests, never hand-copied. Composition orders requested dynamic rows before their consumers, rejects synchronous request cycles, and assigns every row to exactly one initial batch. It refuses declared plugins without built `./client` bundles and groups their package/path rows under one required source-build instruction; malformed declaration fields also fail activation, and the Host audit reports either error from the FAILED fiber.
-3. Scanning is incremental per package — there is no full-rescan code path. Each cordis `internal/plugin` emission marks the fiber's entry name dirty (entry-less fibers drop O(1)); a microtask flush reconciles each dirty name against live loader entries, with package metadata (including the negative "not a client package" verdict) cached per name forever and bundle re-hashing reachable only through `rebuilt(id)`. The activation pass seeds the same dirty set from current entries and flushes synchronously, so first scan and steady state share one implementation. Initial rows receive an opaque process nonce plus sequence without hashing their artifacts; startup combo revisions hash the combined script inputs plus indexed map, and the rows plus batch descriptors hash into `graph.rev`. The graph types are single-sourced in the modules package's `./client` export — the webserver knows nothing about the graph, while modules registers the combo route and contributes structured index-injection rows.
+2. The `dsh-client-modules` node half (the package is dual-face: its browser half is the module table) resolves each live Loader entry through the same `name` and owning-tree `baseUrl` inputs that imported its Host face, then reads the nearest owning package.json `dsh.client` declaration and composes `window.__DSH_BOOT__`: `{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }], batches: [{ phase, url, rev, entries }] }`. The manifest package name is the browser module identity even when an overlay names a relative source or built entry file. Distinct active Loader sources resolving to one package name fail composition; after one source unloads, the surviving source supplies the row without a fiber restart. The row's three optional fields come from manifests, never hand-copied. Composition orders requested dynamic rows before their consumers, rejects synchronous request cycles, and assigns every row to exactly one initial batch. It refuses declared plugins without built `./client` bundles and groups their package/path rows under one required source-build instruction; malformed declaration fields also fail activation, and the Host audit reports either error from the FAILED fiber.
+3. Scanning is incremental per package — there is no full-rescan code path. Each cordis `internal/plugin` emission marks the fiber's entry name dirty (entry-less fibers drop O(1)); a microtask flush reconciles each dirty name against live loader entries, with package metadata (including the negative "not a client package" verdict) cached per entry name and owning-tree base URL for the process lifetime and bundle re-hashing reachable only through `rebuilt(id)`. The activation pass seeds the same dirty set from current entries and flushes synchronously, so first scan and steady state share one implementation. Initial rows receive an opaque process nonce plus sequence without hashing their artifacts; startup combo revisions hash the combined script inputs plus indexed map, and the rows plus batch descriptors hash into `graph.rev`. The graph types are single-sourced in the modules package's `./client` export — the webserver knows nothing about the graph, while modules registers the combo route and contributes structured index-injection rows.
 
 Why is the roster yml rows and not a scan? Because which plugins compose into a deployment is a composition decision, not a package property — a package declaring `dsh.client` in the repo does not mean this deployment mounts it, so discovery-by-scan cannot make that call; the node half scans only what the tree actually mounted.
 
@@ -98,7 +98,7 @@ One governance implementation runs on both sides of the wire; the browser-specif
 
 Costs accepted: the vendored Loader carries idle machinery in the browser (EntryTree persistence is a no-op, groups/isolation unused); every plugin edit in dev pays a bundle rebuild plus fiber remount; graph `inject` rows guide factory arrival but service availability remains the activation authority, so a mismatch appears at the settled sweep; the static UI libraries keep direct value exports; every bundle gains a source-map artifact; and external-script failures provide only coarse URL diagnostics instead of the HTTP status available to an explicit fetch. The Host retains per-plugin bundle/map snapshots, generated one-resource responses, current startup combo responses, and one previous startup generation, so memory scales as several copies of the composed client artifacts. This retained state keeps URLs immutable and lets an in-flight request finish across one HMR recomposition.
 
-Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordis.patch.yml`); `mountWebPlugins` and the `CLIENT_PACKAGES` constant are gone, and recomposing a deployment means swapping the yml/overlay. The graph composer lives in the `dsh-client-modules` node half, while the parser-preloaded client face bootstraps the browser module table. The webserver remains a plain route-registration plugin; `/api/*` binding belongs to the connection node half over `api-gateway` (`dsh-host-apiproxy` providing `ctx.apiProxy`), and the dev bundle watch plus SSE channel belongs to the hmr node half.
+Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordis.patch.yml`); `mountWebPlugins` and the `CLIENT_PACKAGES` constant are gone, and recomposing a deployment means swapping the yml/overlay. The graph composer lives in the `dsh-client-modules` node half, while the parser-preloaded client face bootstraps the browser module table. The webserver remains a plain route-registration plugin; `/api/*` binding, browser authentication, RPC envelopes, and exact Fetch routes belong to the Connection node half, while Remote dispatch belongs to API Gateway and the dev bundle watch plus SSE channel belongs to the hmr node half.
 
 ## Alternatives considered
 

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.zh.md

@@ -53,8 +53,8 @@ Host 会快照每个已构建插件产物,并把每个调度阶段的有序 ro
 **host 侧——组合这张图。**
 
 1. 负责组合的 app(`apps/cli`)把名册作为普通行放进它的 `cordis.yml` 配置树——client 插件包与每个 host 插件一样是 entry 行,包括无条件挂载的 `client-hmr` 行。名册行 import 失败由 `assertEntriesLoaded` 捕获;fiber reject 的行则由 `assertEntriesActivated` 报告原始 stack([host boot 决策](2026-07-24-web-config-tree-boot-and-transport-layering.zh.md))。
-2. `dsh-client-modules` 的 node 半(该包是双面的:浏览器半就是模块表)扫描 loader entry 的 package.json `dsh.client` 声明,组合出 `window.__DSH_BOOT__`:`{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }], batches: [{ phase, url, rev, entries }] }`。Row 的三个可选字段都来自 manifest,永不人肉抄写。组合会把被请求的动态图 row 排到消费者之前、拒绝同步请求环,并把每个 row 恰好分配给一个初始批次。它会拒绝没有已构建 `./client` bundle 的已声明插件,并把它们的 package/path 行归到一条源码构建要求下;畸形声明字段同样会让激活失败,Host 检查会从 FAILED fiber 报告这两类错误。
-3. 扫描是单包增量——不存在全量重扫代码路径。每次 cordis `internal/plugin` 发射把该 fiber 的 entry 名标脏(无 entry 的 fiber O(1) 丢弃);微任务 flush 把每个脏名对账 live loader entries,包元数据(含「非 client 包」的否定结论)按名永久缓存,bundle 重哈希只经 `rebuilt(id)` 可达。激活趟从当前 entries 灌同一脏集合并同步 flush,初扫与稳态共享一条实现。初始 row 使用不透明的进程 nonce 加序号,不对其产物求哈希;启动 combo revision 对合并脚本输入及 indexed map 求哈希,row 与批次描述再共同哈希进 `graph.rev`。图类型单源在 modules 包的 `./client` 出口——webserver 对图一无所知;modules 会注册 combo 路由并贡献结构化 index 注入行。
+2. `dsh-client-modules` 的 node 半(该包是双面的:浏览器半就是模块表)使用 Host face import 时相同的 `name` 与所属 tree `baseUrl` 解析每个 live Loader entry,再读取最近归属 package.json 的 `dsh.client` 声明并组合出 `window.__DSH_BOOT__`:`{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }], batches: [{ phase, url, rev, entries }] }`。即使 overlay 指向相对的 source 或 built entry 文件,manifest 包名仍是浏览器模块身份。若不同的 active Loader source 解析到同一包名,组合会失败;一个来源卸载后,仍存活的来源无需重启 fiber 即可提供该 row。Row 的三个可选字段都来自 manifest,永不人肉抄写。组合会把被请求的动态图 row 排到消费者之前、拒绝同步请求环,并把每个 row 恰好分配给一个初始批次。它会拒绝没有已构建 `./client` bundle 的已声明插件,并把它们的 package/path 行归到一条源码构建要求下;畸形声明字段同样会让激活失败,Host 检查会从 FAILED fiber 报告这两类错误。
+3. 扫描是单包增量——不存在全量重扫代码路径。每次 cordis `internal/plugin` 发射把该 fiber 的 entry 名标脏(无 entry 的 fiber O(1) 丢弃);微任务 flush 把每个脏名对账 live loader entries,包元数据(含「非 client 包」的否定结论)按 entry 名与所属 tree base URL 缓存至进程结束,bundle 重哈希只经 `rebuilt(id)` 可达。激活趟从当前 entries 灌同一脏集合并同步 flush,初扫与稳态共享一条实现。初始 row 使用不透明的进程 nonce 加序号,不对其产物求哈希;启动 combo revision 对合并脚本输入及 indexed map 求哈希,row 与批次描述再共同哈希进 `graph.rev`。图类型单源在 modules 包的 `./client` 出口——webserver 对图一无所知;modules 会注册 combo 路由并贡献结构化 index 注入行。
 
 为什么名册是 yml 行而不是扫描?因为哪些插件组合进一次部署是组合决策,不是包属性——一个在仓库中声明了 dsh.client 的包,不代表这次部署要挂载它,扫描发现无从替人做这个决定;node 半只扫描配置树实际挂载了的东西。
 
@@ -98,7 +98,7 @@ Wire 两侧运行同一份治理实现;浏览器特有层只包含一套模块
 
 接受的代价:vendored Loader 在浏览器里背着闲置机件(EntryTree 持久化是 no-op,分组/隔离未用);开发期每次修改插件都要付一次 bundle 重建加 fiber 重挂;graph `inject` row 指导 factory 到达,但服务可用性仍是激活权威,因此不匹配会在 settled 扫描时浮出;静态 UI 库保留直接实体导出;每个 bundle 多出一份 sourcemap 产物,外部 script 失败也只能给出粗粒度 URL 诊断,不能像显式 fetch 那样报告 HTTP 状态。Host 会保留逐插件 bundle/map 快照、生成的单资源响应、当前启动 combo 响应及上一代启动响应,因此内存会随组合出的客户端产物增长为数份副本。这组保留状态使 URL 保持不可变,并让进行中的请求跨越一次 HMR 重组后仍能完成。
 
-名册位于 web 组合包的配置树(`packages/bundle/web-app/cordis.patch.yml`);`mountWebPlugins` 与 `CLIENT_PACKAGES` 常量已消失,重组一次部署等于替换 yml/overlay。Graph 组合器位于 `dsh-client-modules` node 半,由 parser 预载的 client face 则自举浏览器模块表。Webserver 继续作为朴素路由注册插件;`/api/*` 绑定属于 connection node 半,并经 `api-gateway`(由 `dsh-host-apiproxy` 提供 `ctx.apiProxy`);开发期 bundle 监视与 SSE 通道属于 hmr node 半。
+名册位于 web 组合包的配置树(`packages/bundle/web-app/cordis.patch.yml`);`mountWebPlugins` 与 `CLIENT_PACKAGES` 常量已消失,重组一次部署等于替换 yml/overlay。Graph 组合器位于 `dsh-client-modules` node 半,由 parser 预载的 Client face 则自举浏览器模块表。Webserver 继续作为朴素路由注册插件;`/api/*` 绑定、浏览器认证、RPC envelope 与精确 Fetch 路由属于 Connection node 半,Remote 分发属于 API Gateway,开发期 bundle 监视与 SSE 通道属于 HMR node 半。
 
 ## Alternatives considered
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-24-web-config-tree-boot-and-transport-layering.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-24-web-config-tree-boot-and-transport-layering.md
-2026-07-24-web-config-tree-boot-and-transport-layering.md: 3d1ccc2a0f71411d496466288934d9038425e7cf
-2026-07-24-web-config-tree-boot-and-transport-layering.zh.md: c2409e128dfdbd8550bb7052a7e0f67a40fe1d1f
+2026-07-24-web-config-tree-boot-and-transport-layering.md: c44f68bb65758ec4879da8bded60f27f2f11e0bc
+2026-07-24-web-config-tree-boot-and-transport-layering.zh.md: 80f385a0a76a427c8f604a0df76decf77d2e62b0

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-24-web-config-tree-boot-and-transport-layering.md

@@ -16,9 +16,9 @@ English | [中文](2026-07-24-web-config-tree-boot-and-transport-layering.zh.md)
 
 **Boot glue is a class pair.** `AppCLIEntry` (apps/cli) and `AppWebEntry` (the shell kernel) hold only what must exist independently of cordis: argv facts, the composed patch set, the parsed boot manifest, the module system instance, loading-page handles — everything else lives in plugins. `AppCLIEntry.run()` is three stages: layered env (ambient > cwd `.env` > `$DSH_HOME/.env`, closing the defect above) → patch composition → Loader include boot plus the activation audit. `AppWebEntry.run()` mirrors it browser-side: parse `window.__DSH_BOOT__` into a `BootManifest` (two views: npm-package rows for the module table, cordis-plugin rows for entry composition; malformed wire throws), build the module system, render the loading page, prefetch the `immediately` tier in parallel with Context/Loader setup, **await the prefetch before creating entries** (materialization is `tree.import`'s synchronous require, unprotected by fiber inject waiting; cross-package require edges such as i18n → runtime/client need every immediately-tier factory registered first — an empirically found 10–25% boot race otherwise), adopt the modules entry, create the graph rows, settle, sweep.
 
-**Config sources have one declaration place each.** Bundle yml values are engineering defaults, Settings sections are writable user preferences, CLI flags address their owning launcher rows, and env values enter through yml `!!js` expressions. Patches replace a row's config wholesale. The resolved frontend `distIndex` uses that patch channel as an assembly fact. The transport-independent provider/model default belongs to `ctx.agentDefaultModel`; the [direct headless entry point](2026-08-09-headless-direct-core-entry-point.md) and the Web gateway consume the same state.
+**Config sources have one declaration place each.** Bundle yml values are engineering defaults, Settings sections are writable user preferences, CLI flags address their owning launcher rows, and env values enter through yml `!!js` expressions. Patches replace a row's config wholesale. The resolved frontend `distIndex` uses that patch channel as an assembly fact. The transport-independent provider/model default belongs to `ctx.agentDefaultModel`; the [direct headless entry point](2026-08-09-headless-direct-core-entry-point.md) and the Session Controller consume the same state.
 
-**The transport splits five ways.** `dsh-host-apiproxy` is the gateway plugin (`api-gateway` row): it default-exports `ApiProxyService`, configures only `{nativeOpen?}`, consumes the base layer's entry-point-neutral `ctx.agentDefaultModel`, provides `ctx.apiProxy`, remains transport-agnostic, and registers no routes. `dsh-host-webserver` is a plain route-registration plugin: `WebServer` provides `ctx.webServer` (`register(route) → disposer` with duplicate-pattern throw, `renderIndex` rendering — structured `webserver/index-inject` rows, then raw `tapIndex` transforms in registration order — and `port`), listens on activation, answers per-request failures with 400 and logging, and knows no harness concepts. Its socket-backed Node HTTP entry may apply configured gzip through maintained middleware without adding a response-writing service method or changing route owners; the Web Worker tunnel carries identity bytes. The connection node half owns the `/api` binding from `ctx.apiProxy` through `toFetchHandler`. The modules node half (`ClientModuleRegistry`, providing `ctx.clientModules`) owns incremental package scanning, the bundle route, the boot injection rows, and `onRebuilt`/`onGraphChanged` notification. The hmr node half owns dev reload through `fs.watchFile` membership and the `/plugins/events` SSE route.
+**Transport responsibilities have explicit owners.** `dsh-client-connection` owns the `/api` route, request and response envelopes, browser authentication, Host/Origin checks, exact Fetch route registration, and the shared Typert interceptor seat. `dsh-api-gateway` owns typed Remote dispatch and the multiplexed WebSocket. `dsh-host-webserver` is a plain route-registration plugin: `WebServer` provides `ctx.webServer` (`register(route) → disposer` with duplicate-pattern throw, `renderIndex` rendering — structured `webserver/index-inject` rows, then raw `tapIndex` transforms in registration order — and `port`), listens on activation, answers per-request failures with 400 and logging, and knows no harness concepts. Its socket-backed Node HTTP entry may apply configured gzip through maintained middleware without adding a response-writing service method or changing route owners; the Web Worker tunnel carries identity bytes. The modules node half (`ClientModuleRegistry`, providing `ctx.clientModules`) owns incremental package scanning, the bundle route, the boot injection rows, and `onRebuilt`/`onGraphChanged` notification. The hmr node half owns dev reload through `fs.watchFile` membership and the `/plugins/events` SSE route.
 
 **Package export discipline.** The modules package exposes exactly `.` (node half) and `./client` (the complete browser half: `ClientModuleSystem`, `parseBootManifest`, the adoption plugin face) — no bespoke subpaths; wire types re-export through the root for host-side consumers. The adoption handshake: the kernel writes the constructed instance to `window.__DSH_MODULES__` before cordis exists; the `./client` apply reads the slot (missing = loud throw) and provides `ctx.modules`.
 
@@ -33,7 +33,7 @@ English | [中文](2026-07-24-web-config-tree-boot-and-transport-layering.zh.md)
 | Rejected | One-line reason |
 |---|---|
 | Dedicated `dsh-host-profile` receiver package | User model state belongs to the Settings-backed `ctx.agentDefaultModel`; an extra Host receiver would duplicate ownership and exclude direct entry points |
-| Runtime `assembly` shim plugin providing an `apiHandler` service | Existed only because `createApiProxy` lived in runtime; moving it into apiproxy made the gateway self-hosting, and `toFetchHandler` is a pure function the binding side calls |
+| Runtime `assembly` shim plugin providing an `apiHandler` service | Connection already composes Remote interception and feature-owned exact Fetch routes into one handler at the transport edge |
 | Full-rescan + incremental scan coexisting | Two implementations, two semantics; the single per-package path covers the activation pass too |
 | A bespoke `./impl` export on the modules package | Non-uniform exports; the standard `./client` carries the whole browser half |
 | dev overlay / `cordis.dev.yml` | One yml; `!!js` cannot conditionalize row existence, and `--dev` appending one row is the entire difference |

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-24-web-config-tree-boot-and-transport-layering.zh.md

@@ -16,9 +16,9 @@ Status: implemented
 
 **boot 胶水由两个类组成。** `AppCLIEntry`(apps/cli)与 `AppWebEntry`(壳内核)只持有那些必须独立于 cordis、提前存在的东西:argv 事实、合成的 patch 集、解析出的 boot manifest(元数据清单)、模块系统实例、loading 页句柄——其余一律进插件。`AppCLIEntry.run()` 三段:分层 env(ambient > cwd `.env` > `$DSH_HOME/.env`,顺手关掉上述缺陷)→ patch 合成 → Loader include boot 加 activation audit。`AppWebEntry.run()` 在浏览器侧镜像它:把 `window.__DSH_BOOT__` 解析成 `BootManifest`(双视角:npm 包行给模块表、cordis 插件行给 entry 组合;畸形 wire 大声抛)、建模块系统、渲染 loading 页、immediately 层预取与 Context/Loader 准备并行、**create entry 之前等预取齐**(物化是 `tree.import` 的同步 require,不受 fiber inject 等待保护;i18n → runtime/client 这类跨包 require 边要求 immediately 层工厂全部注册完——否则有实测 10–25% 的 boot 竞态)、收编 modules entry、逐一创建图行、settle、sweep。
 
-**每个配置源有唯一声明位置。** 组合包 yml 值是工程默认,Settings 分节是可写的用户偏好,CLI(命令行界面)flags 面向其归属的启动器配置行,env 值则通过 yml `!!js` 表达式进入。patch 会整体替换一行的 config。解析后的前端 `distIndex` 通过同一条 patch 通道作为组装事实传递。与传输无关的提供方/模型默认值归 `ctx.agentDefaultModel` 所有;[直接 headless 入口](2026-08-09-headless-direct-core-entry-point.zh.md)与 Web 网关消费同一份状态。
+**每个配置源有唯一声明位置。** 组合包 yml 值是工程默认,Settings 分节是可写的用户偏好,CLI(命令行界面)flags 面向其归属的启动器配置行,env 值则通过 yml `!!js` 表达式进入。patch 会整体替换一行的 config。解析后的前端 `distIndex` 通过同一条 patch 通道作为组装事实传递。与传输无关的提供方/模型默认值归 `ctx.agentDefaultModel` 所有;[直接 headless 入口](2026-08-09-headless-direct-core-entry-point.zh.md)与 Session Controller 消费同一份状态。
 
-**传输五分。** `dsh-host-apiproxy` 是网关插件(`api-gateway` 行):默认导出 `ApiProxyService`,只配置 `{nativeOpen?}`,消费 base 层不偏向特定入口的 `ctx.agentDefaultModel`,provide `ctx.apiProxy`,保持传输无关且不注册路由。`dsh-host-webserver` 是朴素的路由注册插件:`WebServer` provide `ctx.webServer`(`register(route) → disposer`、重复 pattern 即抛、`renderIndex` 渲染——先结构化 `webserver/index-inject` 行、后原始 `tapIndex` 按注册序应用——与 `port`),激活即 listen,单请求失败时答 400 并记日志,且不认识任何 harness 概念。其基于 socket 的 Node HTTP 入口可以通过受维护的中间件应用已配置的 gzip,无需新增响应写出服务方法或改变 route 所有者;Web Worker 隧道传递 identity 字节。connection node 半拥有从 `ctx.apiProxy` 经 `toFetchHandler` 绑定到 `/api` 的逻辑。modules node 半(`ClientModuleRegistry`,provide `ctx.clientModules`)拥有单包增量扫描、bundle 路由、启动注入行与 `onRebuilt`/`onGraphChanged` 通知。HMR(热模块替换) node 半通过 `fs.watchFile` membership 与 `/plugins/events` SSE 路由拥有开发期重载。
+**传输职责各有明确 owner。** `dsh-client-connection` 持有 `/api` 路由、请求与响应 envelope、浏览器认证、Host/Origin 检查、精确 Fetch 路由注册以及共享 Typert interceptor 席位。`dsh-api-gateway` 持有类型化 Remote 分发和多路复用 WebSocket。`dsh-host-webserver` 是朴素的路由注册插件:`WebServer` provide `ctx.webServer`(`register(route) → disposer`、重复 pattern 即抛、`renderIndex` 渲染——先结构化 `webserver/index-inject` 行、后原始 `tapIndex` 按注册序应用——与 `port`),激活即 listen,单请求失败时答 400 并记日志,且不认识任何 harness 概念。其基于 socket 的 Node HTTP 入口可以通过受维护的中间件应用已配置的 gzip,无需新增响应写出服务方法或改变 route owner;Web Worker 隧道传递 identity 字节。modules node 半(`ClientModuleRegistry`,provide `ctx.clientModules`)持有单包增量扫描、bundle 路由、启动注入行与 `onRebuilt`/`onGraphChanged` 通知。HMR(热模块替换)node 半通过 `fs.watchFile` membership 与 `/plugins/events` SSE 路由持有开发期重载。
 
 **包出口纪律。** modules 包只暴露 `.`(node 半)与 `./client`(完整浏览器半:`ClientModuleSystem`、`parseBootManifest`、收编插件面)——不设专用子路径;wire 类型经根出口 re-export 给 host 侧消费方。收编握手:内核在 cordis 之前把建好的实例写入 `window.__DSH_MODULES__`;`./client` 的 apply 读取该槽位(缺少时显式抛错)并 provide `ctx.modules`。
 
@@ -33,7 +33,7 @@ Status: implemented
 | 弃案 | 一行理由 |
 |---|---|
 | 专门的 `dsh-host-profile` 受体包 | 用户模型状态归 Settings 支撑的 `ctx.agentDefaultModel` 所有;额外的 Host 受体会重复归属,并排除直接入口 |
-| 运行时里的 `assembly` 垫层插件(provide `apiHandler`) | 它的存在只因 `createApiProxy` 住运行时;本体迁入 apiproxy 后网关可自承载,且 `toFetchHandler` 是绑定方自己调的纯函数 |
+| 运行时里的 `assembly` 垫层插件(provide `apiHandler`) | Connection 已在传输边缘把 Remote interception 与功能自有的精确 Fetch 路由组合成一个 handler |
 | 全量重扫与增量扫描并存 | 两条实现两份语义;单包路径足以覆盖激活初扫 |
 | modules 包特设 `./impl` 出口 | 出口不统一;标准 `./client` 承载完整浏览器半 |
 | dev overlay / `cordis.dev.yml` | 一套 yml;`!!js` 无法条件化行存在性,`--dev` 追加一行就是全部差异 |

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.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-25-web-client-session-scope-and-provide-channel.md
-2026-07-25-web-client-session-scope-and-provide-channel.md: 6f159dc16e0e4063f9077c31829a10caca98eae0
-2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 2cb082dce75f1845e52a52289a8e3eaa1a000931
+2026-07-25-web-client-session-scope-and-provide-channel.md: 1fa442e8db2d8b2d2ec66730700c9c88dceddbae
+2026-07-25-web-client-session-scope-and-provide-channel.zh.md: ea9a6e247402e6a2d15fb4bfc0ebd6e65fc021df

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md

@@ -107,7 +107,7 @@ Slot scope is the closed set `root | session-maybe | session`:
 
 - The summary `blank` column and the `host/session-added` frame's `blank` field (see the blank bit above).
 - The SSE frame `host/commands-changed` (a pure invalidation signal); the client routes it into the typed events `commands/changed` and `connection/reset` (broadcast after each connection generation is established; wire-derived caches uniformly treat prior state as stale). The commands frame and its typed client event were later replaced by verbatim forwarding of `commands/change` through `ctx.remote.$on` ([forwarded Remote events](2026-08-10-remote-event-delivery.md)); `connection/reset` is unchanged, and the invalidation-not-diffing contract this bullet states still holds.
-- `command.list/execute` and `skill.list` are uniformly single-addressed by `sessionId` (a session always has an Agent; `agentFor`'s resume semantics come ready-made); the command-surface narrative lives in the [command surfaces note](2026-07-25-web-command-surfaces-and-assembly.md).
+- `command.list/execute` and `skills/list` are uniformly single-addressed by `sessionId` (a session always has an Agent; `agentFor`'s resume semantics come ready-made); the command-surface narrative lives in the [command surfaces note](2026-07-25-web-command-surfaces-and-assembly.md).
 - The `session.create` request shape: workspaceId/cwd as either-or, plus an optional caller-preallocated sessionId (a same-id same-cwd retry is idempotent; a different cwd reports `session-conflict`).
 
 ## Alternatives considered

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md

@@ -107,7 +107,7 @@ slot scope 是闭集 `root | session-maybe | session`:
 
 - summary `blank` 列与 `host/session-added` 帧 `blank` 字段(见上文 blank 位)。
 - SSE(Server-Sent Events)帧 `host/commands-changed`(纯失效信号);client 路由为类型事件 `commands/changed` 与 `connection/reset`(连接代建立后广播,wire 派生缓存一律视旧态为陈旧)。 该 commands 帧及其类型化 client 事件后来被「`commands/change` 经 `ctx.remote.$on` 原样转发」取代([转发的 Remote 事件](2026-08-10-remote-event-delivery.zh.md));`connection/reset` 不变;本条陈述的「失效而非差分」契约依然成立。
-- `command.list/execute`、`skill.list` 一律 `sessionId` 单址(会话恒有 Agent,`agentFor` 的恢复语义现成);命令面叙述见[命令业务面 note](2026-07-25-web-command-surfaces-and-assembly.zh.md)。
+- `command.list/execute`、`skills/list` 一律 `sessionId` 单址(会话恒有 Agent,`agentFor` 的恢复语义现成);命令面叙述见[命令业务面 note](2026-07-25-web-command-surfaces-and-assembly.zh.md)。
 - `session.create` 请求形状:workspaceId/cwd 二选一 + 可选调用方预分配 sessionId(同 id 同 cwd 重试幂等,异 cwd 报 `session-conflict`)。
 
 ## 考虑过的替代方案

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.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-25-web-command-surfaces-and-assembly.md
-2026-07-25-web-command-surfaces-and-assembly.md: a3628a7d99a6ab33652c67f6188933dea213194b
-2026-07-25-web-command-surfaces-and-assembly.zh.md: eef93d750c502f5c034b6604774de31d75d6d342
+2026-07-25-web-command-surfaces-and-assembly.md: 943b68416d896c674783156b05997840c6df3255
+2026-07-25-web-command-surfaces-and-assembly.zh.md: c35d47202894af99377932df57e5de118186c1ce

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.md

@@ -28,7 +28,7 @@ The pipeline was ready but command knowledge had no landing spot: host-side `ctx
 
 ### Reference sources (seeing only projections plus their own apply closures, on the root ctx)
 
-- **ui-skill**: `skill.list({sessionId})` addresses by session (the host resolves the project root from the session header); the directory cache is single-flight keyed by sessionId, prewarmed at birth by the `warm` hook and fully cleared by `connection/reset`. A pick produces a text outcome (the literal `/name ` text, the plain-text-reference decision); `lexicon` supplies the roster from CatalogFetch's settled snapshot (`undefined` while not warm), and `subscribeLexicon` notifies per-session listeners on settle and on invalidation. No match hook (references never enter command adjudication). Skill references ride ordinary prompts as literal text (outside the command plane; tool-skill unchanged, with the session-prefix directory providing the cooperative association).
+- **ui-skill**: `skills/list({sessionId})` addresses by session (the host resolves the project root from the session header); the directory cache is single-flight keyed by sessionId, prewarmed at birth by the `warm` hook and fully cleared by `connection/reset`. A pick produces a text outcome (the literal `/name ` text, the plain-text-reference decision); `lexicon` supplies the roster from CatalogFetch's settled snapshot (`undefined` while not warm), and `subscribeLexicon` notifies per-session listeners on settle and on invalidation. No match hook (references never enter command adjudication). Skill references ride ordinary prompts as literal text (outside the command plane; tool-skill unchanged, with the session-prefix directory providing the cooperative association).
 - **ui-subagent**: candidates are zero-RPC (the sessions.list snapshot filtered by parentId/running); a pick produces a text outcome (the literal `@name ` text); `lexicon` derives from the same snapshot and `subscribeLexicon` forwards the list store's change feed (the model-side representation awaits its business workstream).
 
 ### Fixture command routing and assembly

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.zh.md

@@ -28,7 +28,7 @@ Status: implemented
 
 ### 引用源(只见投影 + 自家 apply 闭包的 root ctx)
 
-- **ui-skill**:`skill.list({sessionId})` 按会话寻址(host 从会话 header 解析项目根);目录缓存按 sessionId 键控 single-flight,`warm` 钩子出生预热、`connection/reset` 全清。pick 产出 text outcome(`/name ` 原文,纯文本引用决策);`lexicon` 从 CatalogFetch 的 settled 快照给名录(未热 `undefined`),`subscribeLexicon` 在 settle 与失效时按会话通知监听者。无 match 钩子(引用不进命令裁决)。skill 引用以原文随普通提示词走(命令平面之外;tool-skill 不变,会话前缀目录提供协作关联)。
+- **ui-skill**:`skills/list({sessionId})` 按会话寻址(host 从会话 header 解析项目根);目录缓存按 sessionId 键控 single-flight,`warm` 钩子出生预热、`connection/reset` 全清。pick 产出 text outcome(`/name ` 原文,纯文本引用决策);`lexicon` 从 CatalogFetch 的 settled 快照给名录(未热 `undefined`),`subscribeLexicon` 在 settle 与失效时按会话通知监听者。无 match 钩子(引用不进命令裁决)。skill 引用以原文随普通提示词走(命令平面之外;tool-skill 不变,会话前缀目录提供协作关联)。
 - **ui-subagent**:候选零 RPC(sessions.list 快照按 parentId/running 过滤);pick 产出 text outcome(`@name ` 原文);`lexicon` 同快照派生,`subscribeLexicon` 转发 list store 的变更通道(模型侧表示待业务立项)。
 
 ### fixture 命令路由与装配

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.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-28-api-browser-trust-boundary.md
-2026-07-28-api-browser-trust-boundary.md: 4401dc8e361281b1239eb84319fc5353948bc217
-2026-07-28-api-browser-trust-boundary.zh.md: c1db2632a6019585ab6a948bcb0f4f2ef853b8a7
+2026-07-28-api-browser-trust-boundary.md: 397c93e084f84579ce3f90654f514428e696ffcd
+2026-07-28-api-browser-trust-boundary.zh.md: 6d3896fd2c1fd6c607b9d92376eee76ab03df3c7

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.md

@@ -12,7 +12,7 @@ The web GUI host serves `/api` over plain loopback HTTP (default `127.0.0.1:3080
 
 Enforce browser trust once, at the carrier, for the entire `/api` prefix — two halves:
 
-- **Media-type fence (dsh-host-apiproxy)**: every `/api` POST must declare `application/json`, else 415 before parsing. Cross-site "simple" requests thereby stop existing: any cross-site attempt is forced into a CORS preflight this server never answers.
+- **Media-type fence (dsh-client-connection)**: every `/api` POST must declare `application/json`, else 415 before parsing. Cross-site "simple" requests thereby stop existing: any cross-site attempt is forced into a CORS preflight this server never answers.
 - **Authority fence (dsh-client-connection, `src/api-request-trust.ts`)**: every request must present a `Host` that is loopback or matches a `trustedHosts` entry (exact on `host:port`, any port on port-less entries, WHATWG-normalized; rebinding defense). Deliberately no shortcut for unmarked requests: over plain HTTP a browser attaches neither `Origin` nor Fetch-Metadata to reads (EventSource, images, navigations — those headers go only to trustworthy destinations), so an unmarked request may be a rebound browser read whose response the page can read, and Host is the one header rebinding cannot forge; non-browser clients pass via loopback, the derived LAN IP literals, or a declared authority. An attached `Origin` must equal the Host authority; `sec-fetch-site: cross-site` is refused outright. A `trustedHosts` entry that is not a bare, canonical authority fails the plugin load — WHATWG parsing would otherwise quietly authorize the hostname inside a typo or broaden an exact-port grant. `host.pickDirectory` loses its bespoke guard and rides the same fence.
 
 Reachability is the webserver binding's policy (`host: 127.0.0.1 | 0.0.0.0`), and this fence is a confused-deputy defense rather than identity. Connection applies the separate [browser token authentication](2026-08-24-browser-token-authentication.md) after the fence. The fence does not inspect peer socket addresses: binding expresses reachability, `trustedHosts` names accepted authorities, and the socket address adds nothing the Host/Origin checks need.

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.zh.md

@@ -12,7 +12,7 @@ Web GUI 宿主以纯 loopback HTTP 提供 `/api`(默认 `127.0.0.1:3080`;CLI
 
 在载体层对整个 `/api` 前缀一次性执行浏览器信任检查——分为两部分:
 
-- **媒体类型栅栏(dsh-host-apiproxy)**:每个 `/api` POST 必须声明 `application/json`,否则在解析前以 415 拒绝。跨站「简单请求」由此不复存在:任何跨站尝试都被逼进一次本服务器从不应答的 CORS 预检。
+- **媒体类型栅栏(dsh-client-connection)**:每个 `/api` POST 必须声明 `application/json`,否则在解析前以 415 拒绝。跨站「简单请求」由此不复存在:任何跨站尝试都被逼进一次本服务器从不应答的 CORS 预检。
 - **权威栅栏(dsh-client-connection,`src/api-request-trust.ts`)**:每个请求的 `Host` 都必须是回环地址,或与某个 `trustedHosts` 条目匹配(带端口的 `host:port` 条目精确匹配,不带端口的条目匹配任意端口,均经 WHATWG 归一化;rebinding 防御)。刻意不为无标记请求开捷径:明文 HTTP 下浏览器的读取(EventSource、图片、导航——这些头只发给可信目标)既不带 `Origin` 也不带 Fetch-Metadata,因此无标记请求可能是被重绑页面发起且响应可被读走的读取,而 Host 是重绑唯一伪造不了的请求头;非浏览器客户端经由回环地址、推导的 LAN IP 字面量或已声明的权威通过。若带 `Origin` 则必须与 Host 权威完全一致;`sec-fetch-site: cross-site` 一律拒绝。不是单纯规范化 authority 的 `trustedHosts` 条目会导致插件加载失败——否则 WHATWG 解析会悄悄授权笔误里的 hostname,或放大精确端口授权。`host.pickDirectory` 失去专属守卫,与其他请求同栅而行。
 
 可达性由 webserver 的绑定配置(`host: 127.0.0.1 | 0.0.0.0`)控制,这道栅栏是混淆代理人防御,而不是身份。Connection 在栅栏之后应用独立的[浏览器令牌认证](2026-08-24-browser-token-authentication.zh.md)。栅栏不检查对端 socket 地址:绑定表达可达性,`trustedHosts` 点名接受的 authority,socket 地址提供不了 Host/Origin 校验需要的额外信息。

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.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-28-directory-picker-capability-seam.md
-2026-07-28-directory-picker-capability-seam.md: 423fec3ad517e645f1cdee3513bad312a56989a8
-2026-07-28-directory-picker-capability-seam.zh.md: f652157fc152dee44a50ab8b55cc6120d92a1a26
+2026-07-28-directory-picker-capability-seam.md: 10ab8e393d5d33da6a09af5caa4024f02c57165c
+2026-07-28-directory-picker-capability-seam.zh.md: 8764dc208bef1ead3fc1ef608c4d3b2b37400602

+ 4 - 4
.agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.md

@@ -10,9 +10,9 @@ The web GUI's "Open local folder" flow was hardwired to one interaction: `host.p
 
 ## Decision
 
-A three-package capability seam in `packages/host/` — `directory-picker` (Service Definition), `directory-picker-native`, `directory-picker-browse` (backends) — with one contract method: `capability()` returns a **discriminated union**, `{ kind: 'native', pick(signal) }` or `{ kind: 'browse', list(path?), createDirectory(path, name) }`. The gateway (`dsh-host-apiproxy`) injects `directoryPicker`, serves the matching RPCs, and answers `directory-picker-unavailable` for the other kind. The union is discriminated because the backends differ in *interaction shape* — flattening them into one method set would force every backend to fake the other's shape.
+A three-package capability seam in `packages/host/` — `directory-picker` (Service Definition), `directory-picker-native`, `directory-picker-browse` (backends) — has one contract method: `capability()` returns a **discriminated union**, `{ kind: 'native', pick(signal) }` or `{ kind: 'browse', list(path?), createDirectory(path, name) }`. `DirectoryPickerController` in `dsh-api-workspace-controller` injects `directoryPicker`, serves the matching generated Remote methods, and answers `directory-picker-unavailable` for the other kind. The union is discriminated because the backends differ in *interaction shape* — flattening them into one method set would force every backend to fake the other's shape.
 
-**The client side is slot-composed, not advertisement-branched.** ui-workspace's two trigger surfaces each declare a `single` directory-flow hole (`conversation.hero.workspace.directoryFlow` / `sidebar.workspaces.directoryFlow`; two keys because a hole has exactly one declaring slot entry — same owner contract, same occupant). Backend packages are **dual-face**: the browser half registers the matching interaction into both holes — `-native` a renderless occupant driving `host.pickDirectory`, `-browse` the in-app Select Workspace Directory dialog. The hole's owner conversation (`open`/`busy`/`onPicked`/`onCancel`/`onError`) carries the whole exchange: ui-workspace keeps the trigger (menu entry rendered only while the hole is occupied) and the adoption (`createWorkspace({path})`, retryable error dialog, Choose again), the occupant owns everything between `open` and the picked path. One `cordis.yml` row therefore swaps the host capability and the client flow together; a mismatch is impossible by construction, and mounting two flow packages fails at client load (`single` hole). The earlier `host.describe.directoryPicker` advertisement and the client's kind branching are deleted — with composition wiring both sides, a wire fact for the client to branch on had no remaining consumer. The hole registry (`ctx.slots.entries`) replaces it as the per-menu-open occupancy read.
+**The client side is slot-composed, not advertisement-branched.** ui-workspace's two trigger surfaces each declare a `single` directory-flow hole (`conversation.hero.workspace.directoryFlow` / `sidebar.workspaces.directoryFlow`; two keys because a hole has exactly one declaring slot entry — same owner contract, same occupant). Backend packages are **dual-face**: the browser half registers the matching interaction into both holes — `-native` a renderless occupant driving `directoryPicker/pick`, `-browse` the in-app Select Workspace Directory dialog. The hole's owner conversation (`open`/`busy`/`onPicked`/`onCancel`/`onError`) carries the whole exchange: ui-workspace keeps the trigger (menu entry rendered only while the hole is occupied) and the adoption (`createWorkspace({path})`, retryable error dialog, Choose again), the occupant owns everything between `open` and the picked path. One `cordis.yml` row therefore swaps the host capability and the client flow together; a mismatch is impossible by construction, and mounting two flow packages fails at client load (`single` hole). The earlier `host.describe.directoryPicker` advertisement and the client's kind branching are deleted — with composition wiring both sides, a wire fact for the client to branch on had no remaining consumer. The hole registry (`ctx.slots.entries`) replaces it as the per-menu-open occupancy read.
 
 Placement and policy rulings folded into this decision:
 
@@ -31,7 +31,7 @@ Placement and policy rulings folded into this decision:
 
 - **Extend `ctx.fs` with browse methods.** Rejected: authority-domain coupling above; also a listing-for-display contract (hidden flags, crumbs, home anchor) does not belong on a storage seam.
 - **One uniform Service Definition method set (`pick(): path`).** Rejected: an in-app browser cannot be served behind a single host-side call — the browsing loop lives in the client and needs primitives on the wire; the native chooser cannot implement primitives. The interaction difference is irreducible, hence the discriminant.
-- **Direct stdlib calls inside apiproxy (no seam).** Rejected: keeps the gateway the only swap point (source edits), loses fixture/test backends, and contradicts the plugin doctrine that motivated the work.
+- **Direct stdlib calls inside the API adapter (no seam).** Rejected: keeps the adapter the only swap point (source edits), loses fixture/test backends, and contradicts the plugin doctrine that motivated the work.
 - **Adopting a file-manager/drive-enumeration dependency.** Rejected per the survey above; recorded here as the dependency policy requires.
 - **A flip-label show-hidden toggle ("Hide hidden files").** Rejected: a flipping action label is ambiguous between state and action and doubles the negative; the fixed label with a pressed presentation states both at once.
 - **Pure relatedTarget blur cancellation (no mousedown suppression).** Rejected: Safari does not focus buttons on pointer down, so a click's focusout carries a null `relatedTarget` and would cancel the editor before the click lands; editing-scoped mousedown suppression plus the card-anchored relatedTarget guard covers pointer and keyboard paths together.
@@ -43,6 +43,6 @@ Placement and policy rulings folded into this decision:
 ## Consequences
 
 - `cordis.yml` chooses the interaction; `apps/cli` mounts the [`-auto` chooser](../feature/2026-07-29-directory-picker-adaptive-default.md), which resolves the host's situation at boot and mounts `-native` or `-browse` itself, one row still swapping backend and UI together; composing a backend row directly pins the interaction.
-- The wire gains `host.listDirectory`/`host.createDirectory` and four error codes; the connection fixture serves a deterministic browse tree and a deterministic `pickDirectory` path for keyless assembled tests.
+- The wire exposes generated `directoryPicker/list` and `directoryPicker/createDirectory` methods with four error codes; the Connection fixture serves a deterministic browse tree and `directoryPicker/pick` result for keyless assembled tests.
 - A future interaction (or an Electron provider of the `native` interaction) is one dual-face backend package — no gateway surgery, no ui-workspace edits.
 - `ApiProxyDefaults.pickDirectory` (test-only injection) is gone; tests provide a stub `ctx.directoryPicker` like any other service.

+ 4 - 4
.agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.zh.md

@@ -10,9 +10,9 @@ web GUI 的「打开本地文件夹」流程被焊死在一种交互上:`host.
 
 ## 决策
 
-在 `packages/host/` 落一个三包能力 seam——`directory-picker`(Service Definition)、`directory-picker-native`、`directory-picker-browse`(后端)——唯一约定方法 `capability()` 返回**可辨识联合**:`{ kind: 'native', pick(signal) }` 或 `{ kind: 'browse', list(path?), createDirectory(path, name) }`。网关(`dsh-host-apiproxy`)注入 `directoryPicker`,提供对应的 RPC,另一种 kind 的调用以 `directory-picker-unavailable` 应答。联合之所以可辨识,是因为后端差异在**交互形态**——压平成统一方法集会逼每个后端伪装另一方的形态。
+在 `packages/host/` 落一个三包能力 seam——`directory-picker`(Service Definition)、`directory-picker-native`、`directory-picker-browse`(后端)——唯一约定方法 `capability()` 返回**可辨识联合**:`{ kind: 'native', pick(signal) }` 或 `{ kind: 'browse', list(path?), createDirectory(path, name) }`。`dsh-api-workspace-controller` 中的 `DirectoryPickerController` 注入 `directoryPicker`,提供匹配的生成 Remote 方法,另一种 kind 的调用以 `directory-picker-unavailable` 应答。联合之所以可辨识,是因为后端差异在**交互形态**——压平成统一方法集会逼每个后端伪装另一方的形态。
 
-**client 侧靠 slot 组合,而非按广播分支。** ui-workspace 的两个触发表层各自声明一个 `single` 目录流洞(`conversation.hero.workspace.directoryFlow`/`sidebar.workspaces.directoryFlow`;之所以是两个 key,是因为一个洞只有一个声明它的 slot entry——owner 约定相同、占用者相同)。后端包是**双面包**:浏览器一侧把匹配的交互注册进两个洞——`-native` 是驱动 `host.pickDirectory` 的无渲染占用者,`-browse` 是应用内的选择工作区目录对话框。洞的 owner 会话(`open`/`busy`/`onPicked`/`onCancel`/`onError`)承载整个交换:ui-workspace 保留触发(菜单入口仅在洞被占用时渲染)与接纳(`createWorkspace({path})`、可重试的错误对话框、重新选择),占用者持有从 `open` 到所选路径之间的一切。因此一行 `cordis.yml` 同时切换宿主能力与 client 流程;错配在构造上不可能,同时挂两个流程包会在 client 加载期失败(`single` 洞)。早先的 `host.describe.directoryPicker` 广播与客户端 kind 分支被删除——组合已经接好两侧后,供客户端分支用的 wire 事实不再有任何消费者。洞注册表(`ctx.slots.entries`)取而代之,成为每次打开菜单的占用读取。
+**client 侧靠 slot 组合,而非按广播分支。** ui-workspace 的两个触发表层各自声明一个 `single` 目录流洞(`conversation.hero.workspace.directoryFlow`/`sidebar.workspaces.directoryFlow`;之所以是两个 key,是因为一个洞只有一个声明它的 slot entry——owner 约定相同、占用者相同)。后端包是**双面包**:浏览器一侧把匹配的交互注册进两个洞——`-native` 是驱动 `directoryPicker/pick` 的无渲染占用者,`-browse` 是应用内的选择工作区目录对话框。洞的 owner 会话(`open`/`busy`/`onPicked`/`onCancel`/`onError`)承载整个交换:ui-workspace 保留触发(菜单入口仅在洞被占用时渲染)与接纳(`createWorkspace({path})`、可重试的错误对话框、重新选择),占用者持有从 `open` 到所选路径之间的一切。因此一行 `cordis.yml` 同时切换宿主能力与 client 流程;错配在构造上不可能,同时挂两个流程包会在 client 加载期失败(`single` 洞)。早先的 `host.describe.directoryPicker` 广播与客户端 kind 分支被删除——组合已经接好两侧后,供客户端分支用的 wire 事实不再有任何消费者。洞注册表(`ctx.slots.entries`)取而代之,成为每次打开菜单的占用读取。
 
 并入本决策的位置与策略裁决:
 
@@ -31,7 +31,7 @@ web GUI 的「打开本地文件夹」流程被焊死在一种交互上:`host.
 
 - **给 `ctx.fs` 增加浏览方法。** 否决:上述权限域耦合;且面向展示的列举约定(hidden 标志、面包屑、home 锚点)不属于存储 seam。
 - **统一的 Service Definition 方法集(`pick(): path`)。** 否决:应用内浏览器无法藏在一次宿主侧调用后面——浏览循环在客户端,需要协议上的原语;而原生选择器实现不了原语。交互差异不可约,故用判别标签。
-- **apiproxy 里直接调标准库(不建 seam)。** 否决:换装点仍是改网关源码,失去 fixture(测试前置数据)/测试后端,与促成这项工作的插件教义相悖。
+- **API adapter 里直接调标准库(不建 seam)。** 否决:换装点仍是改 adapter 源码,失去 fixture(测试前置数据)/测试后端,与促成这项工作的插件教义相悖。
 - **引入文件管理器/盘符枚举依赖。** 按上文调研否决;依赖政策要求记录于此。
 - **动作标签随状态翻转的「显示隐藏」开关(「隐藏隐藏文件」)。** 否决:会翻转的动作标签在状态与动作之间有歧义,还把否定叠了两层;固定标签加按下态呈现一次说清两者。
 - **纯 relatedTarget 失焦取消(不做 mousedown 抑制)。** 否决:Safari 在指针按下时不给按钮聚焦,点击触发的 focusout 因而携带空 `relatedTarget`,会在点击落地前就取消编辑器;编辑期作用的 mousedown 抑制加上锚定卡片的 relatedTarget 守卫才能同时覆盖指针与键盘路径。
@@ -43,6 +43,6 @@ web GUI 的「打开本地文件夹」流程被焊死在一种交互上:`host.
 ## 后果
 
 - `cordis.yml` 决定交互形态;`apps/cli` 挂 [`-auto` 选择器](../feature/2026-07-29-directory-picker-adaptive-default.zh.md),它在启动时判定宿主处境并自行挂载 `-native` 或 `-browse`,一行仍同时切换后端与 UI;直接组合某个后端行即固定交互。
-- 协议新增 `host.listDirectory`/`host.createDirectory` 与四个错误码;connection fixture 提供确定性浏览树与确定性 `pickDirectory` 路径供无密钥组装测试使用。
+- 协议公开生成的 `directoryPicker/list` 与 `directoryPicker/createDirectory` 方法及四个错误码;Connection fixture 提供确定性浏览树与 `directoryPicker/pick` 结果供无密钥组装测试使用。
 - 未来的新交互(或提供 `native` 交互的 Electron 提供方)只是一个双面后端包——无需网关手术,也不动 ui-workspace。
 - `ApiProxyDefaults.pickDirectory`(仅测试注入)删除;测试像提供其他服务一样提供 stub `ctx.directoryPicker`。

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.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-29-projected-token-usage-and-request-context.md
-2026-07-29-projected-token-usage-and-request-context.md: 063f2300f378f6f7763bce87b11add5da3093230
-2026-07-29-projected-token-usage-and-request-context.zh.md: 37b8741d09e9ec56f6b9f273e05460b2deb4f6f9
+2026-07-29-projected-token-usage-and-request-context.md: 75a05e5a0e8f0183fef1e7d80701ce6d81041cd6
+2026-07-29-projected-token-usage-and-request-context.zh.md: 7cce5989d719156f1d66c48937780ff8aed02a42

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md

@@ -40,7 +40,7 @@ The non-atomicity is deliberate, not a defect. A consumer that genuinely needs a
 
 **An atomic request-boundary snapshot delivered as a transient mux frame (implemented, then rejected).** An earlier revision emitted `session/model-request`: one non-replayable frame carrying `contextTokens` and `contextWindow` measured at the same `agent/model-request` boundary. Being the only non-replayable class on the mux stream is what broke it. Host and mux are independent SSE streams with no cross-stream ordering, so a request emitted before a removal could arrive after `host/session-removed` and revive a dead session's telemetry, while a legitimate request for a new lifecycle reusing the same id could be fenced by a late removal. `session/subscribed` is not lifecycle proof — it says a queue began subscribing to an id, not that a new in-memory session replaced an older one — and `lastSeq` is a durable watermark two lifecycles can share. A correct fix required a monotonic lifecycle generation on the frame, on subscription, and on removal, plus a client watermark comparison.
 
-That cost bought a worse display: occupancy went blank after every reconnect and never moved while a conversation grew. It also made ApiProxy a measurement site calling the O(surface) `measure()` on every request, and expressed reconnect state through a synthetic `cancelled` open error the UI had to special-case.
+That cost bought a worse display: occupancy went blank after every reconnect and never moved while a conversation grew. It also made the transport adapter a measurement site calling the O(surface) `measure()` on every request, and expressed reconnect state through a synthetic `cancelled` open error the UI had to special-case.
 
 **Fold the loaded node window in React.** Cannot survive pagination or compaction, and makes a presentation package reconstruct log semantics.
 
@@ -58,4 +58,4 @@ Token totals stay stable across pagination, compaction, replay, restart, and rec
 
 Occupancy is approximate in the ways documented above. It is available immediately after restore or reconnect, since both fields are durable, at the cost of describing the last recorded request rather than an exact current boundary.
 
-Each session log gains one small `request/context` record per route or advertised-capacity change. Token-meter is the canonical owner of durable usage semantics, including retry-attempt separation in the cumulative projection and the reusable exact attempt/Turn fold; Web Chat only selects a complete loaded Turn and renders the fold result. The TUI retains its live per-step map because it does not mount the generic projection seam, and the standalone browser fixture mirrors the unit. ApiProxy carries no token-specific code, owns no per-session metrics cache, and performs no measurement. The browser keeps two generic projection values and no connection-local telemetry, and streaming text deltas still do not force the stats line to recompute.
+Each session log gains one small `request/context` record per route or advertised-capacity change. Token-meter is the canonical owner of durable usage semantics, including retry-attempt separation in the cumulative projection and the reusable exact attempt/Turn fold; Web Chat only selects a complete loaded Turn and renders the fold result. The TUI retains its live per-step map because it does not mount the generic projection seam, and the standalone browser fixture mirrors the unit. Connection and API Gateway carry no token-specific code, own no per-session metrics cache, and perform no measurement. The browser keeps two generic projection values and no connection-local telemetry, and streaming text deltas still do not force the stats line to recompute.

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md

@@ -40,7 +40,7 @@ Web `StatsLine` 通过标准 `useProjection` 席位读取两者。窗口内节
 
 **以临时 mux 帧交付请求边界上的原子快照(已实现,随后否决)。** 较早的一个修订版会发出 `session/model-request`:一个不可回放的帧,携带在同一个 `agent/model-request` 边界测得的 `contextTokens` 与 `contextWindow`。真正让它失效的,是它成了 mux 流上唯一的不可回放类别。Host 流与 mux 流是两条独立的 SSE(Server-Sent Events)流,彼此之间没有顺序保证:在移除之前发出的请求可能在 `host/session-removed` 之后才到达,让一个已死会话的遥测数据复活;而复用同一 id 的新生命周期的合法请求,又可能被一条迟到的移除拦下。`session/subscribed` 不能证明生命周期:它只说明某个队列开始订阅某个 id,而不说明新的内存会话替换了较早的会话;`lastSeq` 则是两个生命周期可以共用的持久水位线。正确的修法需要在帧上、订阅上和移除上都带一个单调递增的生命周期代次,再加上一次客户端水位线比较。
 
-这份代价换来的是更差的显示:占用率在每次重连后变为空白,而且会话增长期间从不移动。它还把 ApiProxy 变成一个测量点,每个请求都要调用 O(surface) 的 `measure()`,并通过一个 UI 必须特殊处理的、连接打开时的合成 `cancelled` 错误来表达重连状态。
+这份代价换来的是更差的显示:占用率在每次重连后变为空白,而且会话增长期间从不移动。它还把传输适配器变成一个测量点,每个请求都要调用 O(surface) 的 `measure()`,并通过一个 UI 必须特殊处理的、连接打开时的合成 `cancelled` 错误来表达重连状态。
 
 **在 React 中归并已加载的节点窗口。** 无法跨分页或压缩保留数据,还会迫使展示包重建日志语义。
 
@@ -58,4 +58,4 @@ token 总量在分页、压缩、回放、重启和重连期间保持稳定,
 
 占用率在上文记录的意义上是近似值。由于两个字段都是持久的,它在恢复或重连后立即可用;代价是它描述的是最后一条已记录的请求,而不是精确的当前边界。
 
-每个会话日志会为每次路由或已公布容量变化增加一条小型 `request/context` 记录。token-meter 是持久用量语义的正典所有方,包括累计投影中的重试 attempt 分离,以及可复用的精确 attempt/Turn fold;Web Chat 只选择已完整加载的 Turn 并渲染 fold 结果。TUI 未挂载通用投影 seam,因此保留自己的实时逐步骤 map,而独立浏览器 fixture(测试前置数据)会镜像该单元。ApiProxy 不携带任何 token 专用代码,不拥有逐会话指标缓存,也不执行测量。浏览器只保留两个通用投影值,不保留连接本地的遥测数据;流式文本增量仍不会迫使统计行重新计算。
+每个会话日志会为每次路由或已公布容量变化增加一条小型 `request/context` 记录。token-meter 是持久用量语义的正典所有方,包括累计投影中的重试 attempt 分离,以及可复用的精确 attempt/Turn fold;Web Chat 只选择已完整加载的 Turn 并渲染 fold 结果。TUI 未挂载通用投影 seam,因此保留自己的实时逐步骤 map,而独立浏览器 fixture(测试前置数据)会镜像该单元。Connection 与 API Gateway 不携带任何 token 专用代码,不拥有逐会话指标缓存,也不执行测量。浏览器只保留两个通用投影值,不保留连接本地的遥测数据;流式文本增量仍不会迫使统计行重新计算。

+ 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: c817071ed17554d06249aa6893ed759bea5d72d0
-2026-07-30-web-config-plane.zh.md: 0b15e329340051d0f63e8a5b1f8c70a29a2138d2
+2026-07-30-web-config-plane.md: 81b501db529bf1b2974fd4541045991c5a8bf087
+2026-07-30-web-config-plane.zh.md: 3f02a17e4826bb35ecfd45da25c4b0170be270cb

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

@@ -12,11 +12,11 @@ The request-level configuration seam made LLM adapter configuration restart-free
 
 ## Decision
 
-**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.
+**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`, `settings/openSettingsDocument`, `settings/openAgentPresetDirectory`, and `credentials/describe|set|unset`; `@deepseek-ai/dsh-llm` owns `llm/listProviders`, `llm/listConfigurableProviders`, and `llm/discoverModels`. 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.
 
-**The Host identifies and opens the local settings document.** The settings seam exposes optional `documentPath` provider metadata and a `prepareDocument()` operation; `settings-file` returns its fully resolved custom or `$DSH_HOME/settings.yaml` filename and exclusively creates an absent empty document with owner-only permissions, while non-file providers retain the base `undefined`. The browser-authenticated `settings.describe` response carries only the boolean `hasDocument` capability beside the redacted namespace views. `ui-settings-general` registers a `settings.action` entry only on loopback pages, shows it only after the metadata confirms that a provider-owned local document can be prepared, and invokes pathless `settings.openDocument`; the Host resolves the provider path again before a text-document handoff (`open -t` on macOS so an arbitrary YAML file association cannot redirect the gesture, `xdg-open` on desktop Linux, `Invoke-Item` on Windows, and `wslpath -w` followed by that Windows handoff on WSL). Generic workspace paths retain the default intent, including its browser preference for browser-renderable documents. The browser neither derives `$DSH_HOME` nor receives a filesystem target; non-loopback pages retain the Client policy that makes no Host settings read for this action.
+**The Host identifies and opens the local settings document.** The settings seam exposes optional `documentPath` provider metadata and a `prepareDocument()` operation; `settings-file` returns its fully resolved custom or `$DSH_HOME/settings.yaml` filename and exclusively creates an absent empty document with owner-only permissions, while non-file providers retain the base `undefined`. The browser-authenticated `settings/describe` response carries only the boolean `hasDocument` capability beside the redacted namespace views. `ui-settings-general` registers a `settings.action` entry only on loopback pages, shows it only after the metadata confirms that a provider-owned local document can be prepared, and invokes pathless `settings/openSettingsDocument`; the Host resolves the provider path again before a text-document handoff (`open -t` on macOS so an arbitrary YAML file association cannot redirect the gesture, `xdg-open` on desktop Linux, `Invoke-Item` on Windows, and `wslpath -w` followed by that Windows handoff on WSL). Generic workspace paths retain the default intent, including its browser preference for browser-renderable documents. The browser neither derives `$DSH_HOME` nor receives a filesystem target; non-loopback pages retain the Client policy that makes no Host settings read for this action.
 
 **The llm seam declares configurability and announces topology.** `registerConfigurableProviders()` is an all-or-nothing, fiber-scoped directory of `{provider, displayName, settingsNs, settingsPath}` — the addressing a config page needs to open the right settings subtree for a route that may not exist yet; `listConfigurableProviders()` merges with live routes in the wire handler so undeclared live routes still report active. The zero-payload `'llm/adapters-updated'` event fires from all four registration/unregistration commit points with contained listener dispatch (INVARIANT rethrow), following the settings/commands precedent. `llm-deepseek`'s route renamed to `deepseek-official` because the pi-ai catalog legitimately owns `deepseek` as an aggregator entry; pre-release stance, no alias.
 
@@ -32,7 +32,7 @@ The request-level configuration seam made LLM adapter configuration restart-free
 - **Storing the typed key as a literal `apiKey` setting** — the single API key input requirement could have written the literal into the profile, but every UI removal path rebuilds the user section from the *redacted* layers, so any reset or row deletion would silently drop stored sibling keys; deriving a reference keeps the input single-field while keeping `settings.yaml` secret-free and every replace safe.
 - **A `models` bridge plugin owning provider configuration** — same rejection as in the request-level seam note: per-plugin namespaces plus a four-field directory declaration give the UI everything it needs; the bridge's unified dict re-imports the adapter-mapping indirection.
 - **Page-side polling instead of pushed frames** — the mux already carries `host/commands-changed`; three more frames cost one shape each and make a second tab, an external `settings.yaml` edit, and a settings-born route converge at event speed.
-- **Hard-coding `$DSH_HOME/settings.yaml` or returning `documentPath` through `host.openPath` in the browser** — rejected because `settings-file.path` may select another YAML/JSON document, non-file providers have no Host path, and a general path request makes the browser the authority for a local filesystem target. Provider preparation is the authoritative source, and the Host-owned operation feeds the existing opener.
+- **Hard-coding `$DSH_HOME/settings.yaml` or returning `documentPath` through `session/openWorkspacePath` in the browser** — rejected because `settings-file.path` may select another YAML/JSON document, non-file providers have no Host path, and a general path request makes the browser the authority for a local filesystem target. Provider preparation is the authoritative source, and the Host-owned operation feeds the existing opener.
 
 ## Consequences
 

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

@@ -12,11 +12,11 @@ Status: implemented
 
 ## 决策
 
-**配置调用使用其所属的 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。
+**配置调用使用其所属的 wire 实现,拒绝落为错误码,owner 事件原样转发。**`@deepseek-ai/dsh-api-settings-controller` 持有 `settings/describe`、`settings/update`、`settings/replace`、`settings/mutate`、`settings/openSettingsDocument`、`settings/openAgentPresetDirectory` 与 `credentials/describe|set|unset` 的生成 Remote 方法;`@deepseek-ai/dsh-llm` 持有 `llm/listProviders`、`llm/listConfigurableProviders` 与 `llm/discoverModels`。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}`,页面因此不必收到任何值就能渲染只写输入框。
 
-**Host 识别并打开本地设置文档。** settings seam 暴露可选的 `documentPath` 提供方元数据和 `prepareDocument()` 操作;`settings-file` 返回已完全解析的自定义文件名或 `$DSH_HOME/settings.yaml` 文件名,并在文档缺失时以仅属主可访问的权限独占创建空文档,非文件提供方则保留基类的 `undefined`。经浏览器认证的 `settings.describe` 响应会在脱敏 namespace 视图旁只携带布尔型 `hasDocument` 能力。`ui-settings-general` 只在回环页面注册一条 `settings.action` 条目,只有元数据确认可准备好一份由提供方持有的本地文档后才显示,并调用无路径参数的 `settings.openDocument`;Host 会在文本文档交接前再次解析提供方路径(macOS 上使用 `open -t`,使任意 YAML 文件关联无法重定向这次操作;桌面 Linux 上使用 `xdg-open`;Windows 上使用 `Invoke-Item`;WSL 上先执行 `wslpath -w`,再使用同一 Windows 交接)。通用 Workspace 路径仍保留默认意图,包括针对浏览器可渲染文档的浏览器偏好。浏览器既不推导 `$DSH_HOME`,也不会收到文件系统目标;非 loopback 页面保留 Client 策略,不为这项操作发起 Host settings 读取。
+**Host 识别并打开本地设置文档。** settings seam 暴露可选的 `documentPath` 提供方元数据和 `prepareDocument()` 操作;`settings-file` 返回已完全解析的自定义文件名或 `$DSH_HOME/settings.yaml` 文件名,并在文档缺失时以仅属主可访问的权限独占创建空文档,非文件提供方则保留基类的 `undefined`。经浏览器认证的 `settings/describe` 响应会在脱敏 namespace 视图旁只携带布尔型 `hasDocument` 能力。`ui-settings-general` 只在回环页面注册一条 `settings.action` 条目,只有元数据确认可准备好一份由 provider 持有的本地文档后才显示,并调用无路径参数的 `settings/openSettingsDocument`;Host 会在文本文档交接前再次解析 provider 路径(macOS 上使用 `open -t`,使任意 YAML 文件关联无法重定向这次操作;桌面 Linux 上使用 `xdg-open`;Windows 上使用 `Invoke-Item`;WSL 上先执行 `wslpath -w`,再使用同一 Windows 交接)。通用 Workspace 路径仍保留默认意图,包括针对浏览器可渲染文档的浏览器偏好。浏览器既不推导 `$DSH_HOME`,也不会收到文件系统目标;非 loopback 页面保留 Client 策略,不为这项操作发起 Host settings 读取。
 
 **llm seam 声明可配置性并公布拓扑。**`registerConfigurableProviders()` 是一个全有或全无、以 fiber 为作用域的目录,条目为 `{provider, displayName, settingsNs, settingsPath}`——这正是配置页要为一条可能尚不存在的路由打开正确设置子树时所需要的寻址;`listConfigurableProviders()` 在 wire 处理器里与存活路由合并,未声明的存活路由因此仍报告为激活。零负载的 `'llm/adapters-updated'` 事件从全部四个注册/注销提交点触发,listener 派发带异常隔离(INVARIANT 重抛),沿用 settings/commands 的先例。`llm-deepseek` 的路由重命名为 `deepseek-official`,因为 pi-ai catalog 名正言顺地拥有 `deepseek` 这个聚合器条目;依预发布立场,不设别名。
 
@@ -32,7 +32,7 @@ Status: implemented
 - **把键入的密钥存成字面 `apiKey` 设置**——单个 API 密钥输入框的需求本可以把字面量直接写进 profile,但 UI 的每条删除路径都会从*脱敏后的*各层重建用户分节,任何重置或整行删除都会静默丢掉已存储的兄弟密钥;派生引用让输入保持单字段,同时让 `settings.yaml` 不含机密、每一次 replace 都安全。
 - **由 `models` 桥接插件持有提供方配置**——与请求级 seam note 相同的否决理由:按插件划分的 namespace 加上四字段的目录声明已经给了 UI 需要的一切;桥接层的统一字典会把适配器映射那层间接重新引进来。
 - **页面侧轮询而非推送帧**——mux 已经承载 `host/commands-changed`;再加三个帧,每个只需增加一种形状,就能让第二个标签页、外部的 `settings.yaml` 编辑和由设置催生的路由都以事件速度收敛。
-- **在浏览器中硬编码 `$DSH_HOME/settings.yaml`,或经 `host.openPath` 回传 `documentPath`**——否决,因为 `settings-file.path` 可能选择另一份 YAML/JSON 文档、非文件提供方没有 Host 路径,而且通用路径请求会让浏览器成为本地文件系统目标的权威。提供方的准备操作才是权威来源,由 Host 持有的操作会把结果交给现有打开器。
+- **在浏览器中硬编码 `$DSH_HOME/settings.yaml`,或经 `session/openWorkspacePath` 回传 `documentPath`**——否决,因为 `settings-file.path` 可能选择另一份 YAML/JSON 文档、非文件提供方没有 Host 路径,而且通用路径请求会让浏览器成为本地文件系统目标的权威。提供方的准备操作才是权威来源,由 Host 持有的操作会把结果交给现有打开器。
 
 ## 后果
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.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-31-code-runtime-portable-identifier-seam.md
-2026-07-31-code-runtime-portable-identifier-seam.md: 6d75f0b0872a5d4b92403f597f5551276e2d7c9f
-2026-07-31-code-runtime-portable-identifier-seam.zh.md: 98f4665f9c9964701cdeb108e411ae08121af9e0
+2026-07-31-code-runtime-portable-identifier-seam.md: 2011b0f6bc8209e628227ddf486aa1143a63688a
+2026-07-31-code-runtime-portable-identifier-seam.zh.md: 36af33366d004fedc6b1077a937d6519de743638

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md

@@ -6,7 +6,7 @@ English | [中文](2026-07-31-code-runtime-portable-identifier-seam.zh.md)
 
 ## Problem
 
-The code-runtime seam promises that a binding-namespace list valid on one backend is valid on every backend, so a Code Mode consumer can hand the same bindings to any registered runtime without knowing its language. The first backend, `dsh-code-runtime-worker-thread`, privately owned the identifier rules that enforce part of that promise: an `IDENTIFIER` regex that allowed the JS-only `$`, a `RESERVED_WORDS` set holding only ECMAScript keywords, and a `RESERVED_ERROR_PROPERTIES` set of three JS `Error` slots. Those rules described the worker's own language, not the seam's portability contract.
+The code-runtime seam promises that a binding-namespace list valid on one backend is valid on every backend, so a PTC mode consumer can hand the same bindings to any registered runtime without knowing its language. The first backend, `dsh-code-runtime-worker-thread`, privately owned the identifier rules that enforce part of that promise: an `IDENTIFIER` regex that allowed the JS-only `$`, a `RESERVED_WORDS` set holding only ECMAScript keywords, and a `RESERVED_ERROR_PROPERTIES` set of three JS `Error` slots. Those rules described the worker's own language, not the seam's portability contract.
 
 A second backend written against a different language (CPython) would either re-declare its own rules — letting `lambda` pass the worker and fail Python, or `$tools` pass the worker and fail every non-JS backend — or import the worker's, inverting the dependency so a Service Provider reached into a sibling Service Provider. Neither keeps the portability promise real: it would hold only for the backend a caller happened to test against.
 
@@ -25,7 +25,7 @@ The constants live in the Service Definition even though the worker is the only
 
 ## Scope
 
-This decision delivers only the Service Definition extension and the worker's adoption of it. The `py-types` renderer and Code Mode language dispatch are owned by the [language-dispatch note](../feature/2026-07-31-code-mode-language-dispatch.md); a Python backend does not exist yet. The Service Definition README keeps its worker-only wording for that reason: linking to a `dsh-code-runtime-python` README that does not exist would break the dead-link gate.
+This decision delivers only the Service Definition extension and the worker's adoption of it. The `py-types` renderer and PTC mode language dispatch are owned by the [language-dispatch note](../feature/2026-07-31-ptc-language-dispatch.md); a Python backend does not exist yet. The Service Definition README keeps its worker-only wording for that reason: linking to a `dsh-code-runtime-python` README that does not exist would break the dead-link gate.
 
 `RESERVED_BINDING_GLOBALS` encodes the Python bootstrap's concrete design ahead of the backend itself: it seeds exactly `__builtins__`/`__name__` and wraps the program under `__dsh_main__`. A Python backend that seeds any additional module global (`__doc__`, `__loader__`, `__spec__`, `__file__`, `__package__`, …) MUST widen this set in the same change, exactly as adding a language widens `PORTABLE_RESERVED_WORDS` — a name the bootstrap seeds but the set omits is the portability split this contract exists to prevent.
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md

@@ -6,7 +6,7 @@ Status: implemented
 
 ## Problem
 
-code-runtime seam 承诺:在一个后端上有效的绑定命名空间列表,在每个后端上都有效,因此 Code Mode 消费方可以把同一组绑定交给任何已注册的运行时,而不必知道它的语言。首个后端 `dsh-code-runtime-worker-thread` 私自拥有了执行这项承诺一部分的标识符规则:一个允许 JS 专有 `$` 的 `IDENTIFIER` 正则、一个只含 ECMAScript 关键字的 `RESERVED_WORDS` 集合,以及一个含三个 JS `Error` 槽位的 `RESERVED_ERROR_PROPERTIES` 集合。这些规则描述的是 worker 自身的语言,而非 seam 的可移植性约定。
+code-runtime seam 承诺:在一个后端上有效的绑定命名空间列表,在每个后端上都有效,因此 PTC mode 消费方可以把同一组绑定交给任何已注册的运行时,而不必知道它的语言。首个后端 `dsh-code-runtime-worker-thread` 私自拥有了执行这项承诺一部分的标识符规则:一个允许 JS 专有 `$` 的 `IDENTIFIER` 正则、一个只含 ECMAScript 关键字的 `RESERVED_WORDS` 集合,以及一个含三个 JS `Error` 槽位的 `RESERVED_ERROR_PROPERTIES` 集合。这些规则描述的是 worker 自身的语言,而非 seam 的可移植性约定。
 
 一个针对不同语言(CPython)编写的第二后端,要么重新声明自己的规则——让 `lambda` 通过 worker 却在 Python 上失败,或让 `$tools` 通过 worker 却在每个非 JS 后端上失败——要么导入 worker 的规则,从而反转依赖,使一个 Service Provider 伸手进入另一个兄弟 Service Provider。二者都无法让可移植承诺成真:它只对调用方恰好测试过的那个后端成立。
 
@@ -25,7 +25,7 @@ Service Definition 同时把可移植标识符子集收窄为 `[A-Za-z_][A-Za-z0
 
 ## Scope
 
-本决策只交付 Service Definition 扩展与 worker 对它的采用。`py-types` 渲染器与 Code Mode 的语言分发归[语言分发 note](../feature/2026-07-31-code-mode-language-dispatch.zh.md) 所有;Python 后端尚不存在。Service Definition README 因此保留仅描述 worker 的措辞:链接到一个不存在的 `dsh-code-runtime-python` README 会破坏死链 gate。
+本决策只交付 Service Definition 扩展与 worker 对它的采用。`py-types` 渲染器与 PTC mode 的语言分发归[语言分发 note](../feature/2026-07-31-ptc-language-dispatch.zh.md) 所有;Python 后端尚不存在。Service Definition README 因此保留仅描述 worker 的措辞:链接到一个不存在的 `dsh-code-runtime-python` README 会破坏死链 gate。
 
 `RESERVED_BINDING_GLOBALS` 先于后端本身编码了 Python bootstrap 的具体设计:它恰好 seed `__builtins__`/`__name__`,并把程序包装在 `__dsh_main__` 之下。任何 seed 额外模块 global(`__doc__`、`__loader__`、`__spec__`、`__file__`、`__package__` 等)的 Python 后端必须在同一改动中扩宽此集合,正如新增一门语言即扩宽 `PORTABLE_RESERVED_WORDS`——bootstrap 会 seed 却不在集合中的名称,正是本约定要防止的可移植性分裂。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.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-08-03-per-session-agent-presets.md
-2026-08-03-per-session-agent-presets.md: 97352a0e3376ce1fe26bac62b88c2c674202adc7
-2026-08-03-per-session-agent-presets.zh.md: b62baf3e3097ba99247c2e86cc3fb5b7e43e2fab
+2026-08-03-per-session-agent-presets.md: 9d5fffbd4d69713fe733235cc0352bc93c9ce55c
+2026-08-03-per-session-agent-presets.zh.md: 406d546828489ccd172205cde7d4b5e0ba96a39b

+ 3 - 3
.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md

@@ -55,9 +55,9 @@ Which preset an unnamed session gets is a user setting (`agent-presets.default`)
 
 **Authoring a preset is an RPC, and a privileged one.** A composition is a file, but "edit it on the filesystem" is not a browser affordance, so the roster gained `read`/`write`/`remove` beside `select`. Connection authenticates authoring, `list`, `select`, and the complete Host API with one browser session: a composition names the plugins a session runs, so reading one is reconnaissance and writing one is arbitrary capability, while choosing a preset grants nothing `session.create` with `agentPreset` did not already grant. The capability is not the preset's to grant either: the deployment's own default already carries `bash` and the filesystem tools, so any caller that may start a session at all can already run commands as this process. Containment is a property of the id (`[a-z0-9][a-z0-9-]*`), checked before it becomes a directory name rather than by inspecting the joined path afterwards; the text is parsed with the loader's own schema and dialect, so a save cannot leave a file no session could load. Shipped presets are refused for writes and deletes, because the deployment's copy is what a broken local preset is compared against — which also makes "duplicate, then edit" the authoring path rather than an afterthought.
 
-**A service with a consumer outside the agent plane cannot move into a preset.** The aggressive split moved the `subagents` registry and its spawn/fork backends into the delegation group's entry-local realm, and `dsh web` then failed to boot: `dsh-host-apiproxy` is a HOST row that injects `subagents` to answer the browser's cross-session queries (`listChildren`, `followup`), so it waited forever for a service only sessions now provided. A per-session copy is wrong twice over — a provider name registers once, so the second session would have collided anyway. The registry and every shared backend, including the [fixed Codex and Claude Code product providers](2026-08-10-product-subagent-providers-in-shared-host.md), are host-plane; a preset contributes whichever delegation TOOLS its agent should see, and those tools resolve the host registry. `workflows` stays entry-local because nothing outside an agent reads it. Grepping injectors is what should have caught this and did not: the search has to include the host packages, not just the agent-plane ones.
+**A service with a consumer outside the agent plane cannot move into a preset.** The aggressive split moved the `subagents` registry and its spawn/fork backends into the delegation group's entry-local realm, and `dsh web` then failed to boot: `SubagentRuntime` is a Host row that exposes the browser's cross-session Remote queries (`list`, `prompt`), so it waited forever for a service only sessions provided. A per-session copy is wrong twice over — a provider name registers once, so the second session would have collided anyway. The registry and every shared backend, including the [fixed Codex and Claude Code product providers](2026-08-10-product-subagent-providers-in-shared-host.md), are host-plane; a preset contributes whichever delegation TOOLS its agent should see, and those tools resolve the host registry. `workflows` stays entry-local because nothing outside an agent reads it. Grepping injectors is what should have caught this and did not: the search has to include the host packages, not just the agent-plane ones.
 
-**A real-composition test that disables a host row cannot audit that row.** The web composition test disabled `api-gateway` — the api-proxy itself — as a row with side effects, which is exactly the row whose pending injection would have named the break. It now boots with the api-proxy enabled and the browse directory picker substituted, so the boot audit covers the whole host-plane injection graph; only the port, the asset tree, and the telemetry exporter stay off.
+**A real-composition test that disables a host row cannot audit that row.** The web composition test keeps API Gateway and the domain Remote services active while disabling the transport-only webserver, Connection, Session export, asset, and telemetry rows. With the browse directory picker substituted, its startup audit covers the host-plane service graph without binding a port.
 
 **A preset's package names must resolve from the harness, not from the preset.** `EntryTree.import()` resolves a row against its own tree's `baseUrl`, which `Include` sets to the composition's directory. That is right for a relative specifier and fatal for a package name: a locally authored preset lives under the user's home, where Node's upward `node_modules` walk never reaches the installed harness, so every `@deepseek-ai/dsh-*` row fails to import and the whole preset is unmountable. The shipped presets hid this — they sit inside the install. The mount records the host composition's base before plugging the subtree and sends bare specifiers there, leaving relative paths resolving from the preset so its own files still travel with it. The real-composition test writing a preset into a temp root is what found it.
 
@@ -67,7 +67,7 @@ Which preset an unnamed session gets is a user setting (`agent-presets.default`)
 
 **The choice belongs to the screen where it still works.** The composer seat spent almost its whole life disabled, since the preset is fixed once a turn has run. It moved to the new-session screen beside the workspace picker, where the pick is *staged*: that screen precedes the session it applies to, and the stage lands when a session becomes current and is still blank — covering both the session a workspace connect creates and the blank one it reuses, which riding `sessions.create` would miss. It is spent on first use, matching the workspace picker beside it. What a running session runs is then a read-only label in its header: a control there would promise a switch the host refuses outright.
 
-**A preset multiplies a cost the host was already paying: nothing disposes an agent.** Measured against the shipped compositions with `--expose-gc`, one live agent holds ~0.17 MB on `minimal` and ~1.31 MB on `standard`/`cordis`, mounting in ~38 ms and ~135 ms; the first agent of a process costs ~7 MB more as Node imports the modules, which every later mount then shares. Growth is strictly linear — 10, 30 and 50 agents give the same per-agent delta — and disposal reclaims essentially all of it (50 `standard` agents held 57.8 MB and returned it). So the object graph does not leak; the lifecycle does. `dsh-host-apiproxy` discards the `AgentHandle` it creates, `archiveSession` only edits the workspace registry, `AgentRegistry` has no eviction, and the sole disposal site in the host is the JSON-RPC server's own shutdown. A web host therefore retains every session it has touched, at ~1.3 MB each once presets are composed rather than ~0.2 MB before. Note that pruning the mount registry does not help here: it drops records whose fiber `uid` has cleared, and an agent that never dies never clears one.
+**A preset multiplies a cost the host was already paying: nothing disposes an agent.** Measured against the shipped compositions with `--expose-gc`, one live agent holds ~0.17 MB on `minimal` and ~1.31 MB on `standard`/`cordis`, mounting in ~38 ms and ~135 ms; the first agent of a process costs ~7 MB more as Node imports the modules, which every later mount then shares. Growth is strictly linear — 10, 30 and 50 agents give the same per-agent delta — and disposal reclaims essentially all of it (50 `standard` agents held 57.8 MB and returned it). So the object graph does not leak; the lifecycle does. `ApiSessionAgentController` discards the `AgentHandle` returned by the registry, `archiveSession` only edits the workspace registry, `AgentRegistry` has no eviction, and the sole disposal site in the host is the JSON-RPC server's own shutdown. A web host therefore retains every session it has touched, at ~1.3 MB each once presets are composed rather than ~0.2 MB before. Note that pruning the mount registry does not help here: it drops records whose fiber `uid` has cleared, and an agent that never dies never clears one.
 
 - Remaining TODO: idle agent eviction — dispose after the session is persisted and re-mount on resume. It belongs to the host that owns the handle, not to this seam.
 

+ 3 - 3
.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.zh.md

@@ -56,9 +56,9 @@ Status: implemented
 
 **创作 preset 是一次 RPC,而且是特权 RPC。** 组装是一个文件,但“去文件系统里改它”并不是浏览器能提供的操作,因此名单在 `select` 之外新增了 `read`/`write`/`remove`。Connection 用一个浏览器会话认证创作操作、`list`、`select` 与完整 Host API:组装指明一个会话所运行的插件,因此读取它是侦察,写入它是任意能力;选择 preset 则没有授予 `session.create` 携带 `agentPreset` 时尚未拥有的能力。这份能力也不由 preset 授予:部署自带的默认 preset 本就带着 `bash` 与文件系统工具,因此任何被允许开启会话的调用方,早已能以本进程的身份执行命令。约束是 id 自身的性质(`[a-z0-9][a-z0-9-]*`),在它成为目录名之前就检查,而不是事后再去审视拼接出的路径;文本使用 loader 自身的 schema 与方言解析,因此保存不会留下任何会话都无法加载的文件。随部署提供的 preset 拒绝写入与删除,因为部署自带的那一份正是用来对照有问题的本地 preset 的——这也让“先复制、再编辑”成为创作路径本身,而非事后补充。
 
-**在 agent 平面之外还有消费方的服务,不能搬进 preset。** 激进拆分把 `subagents` 注册表连同 spawn/fork 后端一起搬进了 delegation 组的 entry-local realm,于是 `dsh web` 直接起不来:`dsh-host-apiproxy` 是宿主行,它注入 `subagents` 来回答浏览器的跨会话查询(`listChildren`、`followup`),因而永远等待一个此刻只有会话才提供的服务。按会话各一份在两个层面上都是错的——provider 名只能注册一次,第二个会话本来也会相撞。注册表与所有共享后端,包括[固定的 Codex 与 Claude Code 产品 provider](2026-08-10-product-subagent-providers-in-shared-host.zh.md),都属于宿主平面;preset 只贡献自己的 agent 应看见的委派**工具**,这些工具解析宿主注册表。`workflows` 保持 entry-local,因为 agent 之外没有任何东西读它。本该拦下它的是「检索注入方」这一步,而它没拦住:检索必须覆盖宿主包,而不只是 agent 平面的包。
+**在 agent 平面之外还有消费方的服务,不能搬进 preset。** 激进拆分把 `subagents` 注册表连同 spawn/fork 后端一起搬进了 delegation 组的 entry-local realm,于是 `dsh web` 直接起不来:`SubagentRuntime` 是 Host 行,它公开浏览器的跨会话 Remote 查询(`list`、`prompt`),因而永远等待一个只有会话才提供的服务。按会话各一份在两个层面上都是错的——provider 名只能注册一次,第二个会话本来也会相撞。注册表与所有共享后端,包括[固定的 Codex 与 Claude Code 产品 provider](2026-08-10-product-subagent-providers-in-shared-host.zh.md),都属于宿主平面;preset 只贡献自己的 agent 应看见的委派**工具**,这些工具解析宿主注册表。`workflows` 保持 entry-local,因为 agent 之外没有任何东西读它。本该拦下它的是「检索注入方」这一步,而它没拦住:检索必须覆盖宿主包,而不只是 agent 平面的包。
 
-**真实组装测试若禁用了某个宿主行,就无法审计该行。** web 组装测试把 `api-gateway`——也就是 api-proxy 本身——当作「有外部副作用的行」禁用了,而它恰恰是那个会以 pending 注入点名此次断裂的行。现在它在启用 api-proxy、并替换为 browse 目录选择器的前提下引导,启动审计因此覆盖整个宿主平面的注入图;只有端口、资源目录与遥测导出器仍然关闭。
+**真实组装测试若禁用了某个宿主行,就无法审计该行。** web 组装测试保持 API Gateway 与各业务 Remote 服务启用,同时禁用只承载传输的 webserver、Connection、Session export、资源与遥测行。替换为 browse 目录选择器后,其启动审计无需绑定端口即可覆盖 Host 平面的服务图。
 
 **preset 的包名必须从 harness 解析,而非从 preset 解析。** `EntryTree.import()` 按行所属树的 `baseUrl` 解析,而 `Include` 把它设为组装文件所在的目录。这对相对标识符是对的,对包名却是致命的:本地创作的 preset 位于用户主目录之下,Node 向上查找 `node_modules` 永远够不到已安装的 harness,因此每一个 `@deepseek-ai/dsh-*` 行都会导入失败,整个 preset 无法挂载。随部署提供的 preset 掩盖了这一点——它们本就在安装目录之内。挂载在插入子树之前先记录宿主组装的基址,并把裸标识符送往那里,同时让相对路径继续从 preset 解析,使它自带的文件仍随它一同迁移。发现它的正是那个把 preset 写入临时根目录的真实组装测试。
 
@@ -68,7 +68,7 @@ Status: implemented
 
 **这个选择属于它仍然可用的那个界面。** composer 座位几乎一生都处于禁用状态,因为一旦跑过一个轮次,preset 即固定。它移到了新建会话界面、工作区选择器旁边,选择在那里是**暂存**的:该界面先于它要应用到的会话存在,暂存值在某个会话成为当前会话且仍为空白时落地——这既覆盖工作区连接新建的会话,也覆盖它复用的那个空白会话,而搭 `sessions.create` 的便车会漏掉后者。它一经使用即被清空,与旁边的工作区选择器一致。至于运行中的会话在跑什么,则是其标题旁的一个只读标签:在那里放控件,等于承诺一次宿主会断然拒绝的切换。
 
-**preset 放大的是宿主本来就在付的代价:没有任何东西会 dispose 一个 agent。** 用 `--expose-gc` 对随附组装实测:一个存活的 agent 在 `minimal` 上约占 0.17 MB、在 `standard`/`cordis` 上约 1.31 MB,挂载耗时分别约 38 ms 与 135 ms;进程里第一个 agent 另需约 7 MB,那是 Node 首次 import 模块的一次性成本,此后每次挂载共享。增长严格线性——10、30、50 个的单个增量一致——且 dispose 后基本全额回收(50 个 `standard` 占住 57.8 MB,释放后全部归还)。所以对象图并不泄漏,缺的是生命周期。`dsh-host-apiproxy` 创建后直接丢弃 `AgentHandle`,`archiveSession` 只改工作区注册表,`AgentRegistry` 没有驱逐机制,而宿主里唯一一处 dispose 是 JSON-RPC 服务器自身的关停。于是一个 web 宿主会留住它接触过的每一个会话,组装 preset 之后每个约 1.3 MB,而在此之前约 0.2 MB。注意:剪枝挂载注册表在这里没有用——它丢弃的是 fiber `uid` 已清空的记录,而永不死亡的 agent 永远不会清空它。
+**preset 放大的是宿主本来就在付的代价:没有任何东西会 dispose 一个 agent。** 用 `--expose-gc` 对随附组装实测:一个存活的 agent 在 `minimal` 上约占 0.17 MB、在 `standard`/`cordis` 上约 1.31 MB,挂载耗时分别约 38 ms 与 135 ms;进程里第一个 agent 另需约 7 MB,那是 Node 首次 import 模块的一次性成本,此后每次挂载共享。增长严格线性——10、30、50 个的单个增量一致——且 dispose 后基本全额回收(50 个 `standard` 占住 57.8 MB,释放后全部归还)。所以对象图并不泄漏,缺的是生命周期。`ApiSessionAgentController` 会丢弃注册表返回的 `AgentHandle`,`archiveSession` 只改工作区注册表,`AgentRegistry` 没有驱逐机制,而宿主里唯一一处 dispose 是 JSON-RPC 服务器自身的关停。于是一个 web 宿主会留住它接触过的每一个会话,组装 preset 之后每个约 1.3 MB,而在此之前约 0.2 MB。注意:剪枝挂载注册表在这里没有用——它丢弃的是 fiber `uid` 已清空的记录,而永不死亡的 agent 永远不会清空它。
 
 - 遗留 TODO:idle agent 驱逐——会话持久化后 dispose,恢复时重新挂载。它属于持有 handle 的那个宿主,不属于本 seam。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.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-08-04-draft-provider-endpoint-interrogation.md
-2026-08-04-draft-provider-endpoint-interrogation.md: 0132dba5888faa1b9e6a4e59b45ee56a259eeef7
-2026-08-04-draft-provider-endpoint-interrogation.zh.md: 8cea5d2809611696606113b2e38578f1b54b4e09
+2026-08-04-draft-provider-endpoint-interrogation.md: a93c5f79a0e4f824be2f1e96853204d27cd22845
+2026-08-04-draft-provider-endpoint-interrogation.zh.md: f8e9002b5267a9d13425601eaefff29e45662699

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.md

@@ -47,4 +47,4 @@ What it costs: the wire gained a third secret-carrying payload, so the configura
 
 ## Testing
 
-`packages/llm/llm/tests/topology.spec.ts` covers the registry: one offer per namespace, disposal with the fiber, normalization that drops duplicate and unusable ids without inventing capacities, and the `NO_DISCOVERY`/`INVALID_DISCOVERY` refusals. `packages/llm/llm-pi-ai/tests/discovery.spec.ts` drives the probe against local HTTP servers — a listing with and without disclosed capacities, a preserved deployment path, an absent credential, a configured route supplying its own where the draft has none and a typed key winning over it, a catalog route answering without resolving one at all, dropped rows, 401/403 versus a server fault, a non-listing and a non-JSON body, an unreachable endpoint, caller cancellation, an unsupported protocol, and the size ceiling in both its declared-length and streamed forms. `packages/host/apiproxy/tests/api-proxy-config.spec.ts` covers the RPC over a real proxy: the draft reaching its namespace whole, absent fields staying absent, no namespace or credential being written, and a failure surfacing as `model-discovery-failed` with the credential absent from the serialized error.
+`packages/llm/llm/tests/topology.spec.ts` covers the registry: one offer per namespace, disposal with the fiber, normalization that drops duplicate and unusable ids without inventing capacities, and the `NO_DISCOVERY`/`INVALID_DISCOVERY` refusals. `packages/llm/llm-pi-ai/tests/discovery.spec.ts` drives the probe against local HTTP servers — a listing with and without disclosed capacities, a preserved deployment path, an absent credential, a configured route supplying its own where the draft has none and a typed key winning over it, a catalog route answering without resolving one at all, dropped rows, 401/403 versus a server fault, a non-listing and a non-JSON body, an unreachable endpoint, caller cancellation, an unsupported protocol, and the size ceiling in both its declared-length and streamed forms. `packages/api/settings-controller/tests/` covers the RPC over a real proxy: the draft reaching its namespace whole, absent fields staying absent, no namespace or credential being written, and a failure surfacing as `model-discovery-failed` with the credential absent from the serialized error.

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.zh.md

@@ -47,4 +47,4 @@ pi-ai 提供了 `createProvider({ fetchModels })` 加上 `Models.refresh()` 与
 
 ## Testing
 
-`packages/llm/llm/tests/topology.spec.ts` 覆盖注册表:每个 namespace 一份、随 fiber dispose(资源释放)、丢弃重复与不可用 id 且不凭空补容量的归一化,以及 `NO_DISCOVERY`/`INVALID_DISCOVERY` 两种拒绝。`packages/llm/llm-pi-ai/tests/discovery.spec.ts` 针对本地 HTTP 服务器驱动探测——含与不含公布容量的列表、被保留的部署路径、无凭据、草稿没带密钥时已配置路由自行取用凭据且键入的密钥压过它、catalog 路由完全不解析凭据即作答、被丢弃的行、401/403 与服务器故障之别、非列表与非 JSON 响应、不可达端点、调用方取消、不支持的协议,以及尺寸上限的「声明长度」与「流式」两种形态。`packages/host/apiproxy/tests/api-proxy-config.spec.ts` 在真实 proxy 上覆盖该 RPC:草稿完整抵达其 namespace、缺席字段保持缺席、没有 namespace 或凭据被写入,以及失败以 `model-discovery-failed` 呈现且序列化后的错误里不含凭据。
+`packages/llm/llm/tests/topology.spec.ts` 覆盖注册表:每个 namespace 一份、随 fiber dispose(资源释放)、丢弃重复与不可用 id 且不凭空补容量的归一化,以及 `NO_DISCOVERY`/`INVALID_DISCOVERY` 两种拒绝。`packages/llm/llm-pi-ai/tests/discovery.spec.ts` 针对本地 HTTP 服务器驱动探测——含与不含公布容量的列表、被保留的部署路径、无凭据、草稿没带密钥时已配置路由自行取用凭据且键入的密钥压过它、catalog 路由完全不解析凭据即作答、被丢弃的行、401/403 与服务器故障之别、非列表与非 JSON 响应、不可达端点、调用方取消、不支持的协议,以及尺寸上限的「声明长度」与「流式」两种形态。`packages/api/settings-controller/tests/` 在真实 proxy 上覆盖该 RPC:草稿完整抵达其 namespace、缺席字段保持缺席、没有 namespace 或凭据被写入,以及失败以 `model-discovery-failed` 呈现且序列化后的错误里不含凭据。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.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-08-09-client-conversation-node-assembly.md
-2026-08-09-client-conversation-node-assembly.md: 12069d129227cce13eb5f9f39e636921d4bf9efa
-2026-08-09-client-conversation-node-assembly.zh.md: 957c2b291293761ff2417f60093b7962bb75bbdf
+2026-08-09-client-conversation-node-assembly.md: e37f2ab3bdf9f3943cb1bcd193d5ea578a0b2c19
+2026-08-09-client-conversation-node-assembly.zh.md: be23b321b710f2f56e3e7af870ba554adb355cef

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md

@@ -359,7 +359,7 @@ Conversation tests cover every built-in Chat Definition, Assistant Step data, Tu
 
 Slot type/runtime tests pin required parent-provided common inject, the `hookContext` type, Hook isolation across Node contexts, stable factory/Hook identity, and the absence of business-renderer rerenders for unrelated Session publications. Existing entry-owned Observable Hook tests continue to pin the path that does not use a contextual factory.
 
-Assembled Web snapshots, GUI tests, and browser scenarios cover the real plugin graph. Browser evidence compares Assistant streaming→settled, Bash running→settled, and Code Mode root + nested subcalls against master layout.
+Assembled Web snapshots, GUI tests, and browser scenarios cover the real plugin graph. Browser evidence compares Assistant streaming→settled, Bash running→settled, and PTC mode root + nested subcalls against master layout.
 
 History-path tests cover complete replace, non-overlapping prepend, complete-range deduplication, partial-overlap rejection, empty-page `hasMore` convergence, and scalar live append. Scalar and packed representations of the same Assistant history produce equal Chat and Trajectory State, timing boundaries, and final Nodes; one packed run remains one Match through replace, prepend, Location replay, and registry rebuild.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md

@@ -359,7 +359,7 @@ Conversation tests 覆盖全部内建 Chat Definition、Assistant Step data、Tu
 
 Slot type/runtime tests 固定父注册必须提供声明的 common inject、`hookContext` 类型、不同 Node context 的 Hook 隔离、factory/Hook identity 稳定,以及无关 Session publication 不重渲染业务 renderer。原 entry-owned Observable Hook 测试继续固定未使用 contextual factory 的路径。
 
-Assembled Web snapshot、GUI 和浏览器场景覆盖真实 plugin graph。浏览器证据比较 Assistant streaming→settled、Bash running→settled 以及 Code Mode root + nested subcalls 与 master 的布局。
+Assembled Web snapshot、GUI 和浏览器场景覆盖真实 plugin graph。浏览器证据比较 Assistant streaming→settled、Bash running→settled 以及 PTC mode root + nested subcalls 与 master 的布局。
 
 历史链路验证同时覆盖完整 replace、非重叠 prepend、完整 range 去重、部分重叠拒绝、空页 `hasMore` 收敛和 scalar live append。相同 Assistant 历史的 scalar 与 packed 表示产生相同 Chat/Trajectory State、timing boundary 与最终 Node;一个 packed run 在 replace、prepend、Location replay 与 registry rebuild 中始终只保留一个 Match。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.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-08-09-headless-direct-core-entry-point.md
-2026-08-09-headless-direct-core-entry-point.md: 9c17b8d418924c38174b4d958fd54b057b118019
-2026-08-09-headless-direct-core-entry-point.zh.md: d95978a832d52b26b1139cabb4b23ade93ce0da3
+2026-08-09-headless-direct-core-entry-point.md: ca3effd6c054ba70bb2f5d4db0794386c37cbcab
+2026-08-09-headless-direct-core-entry-point.zh.md: 4da5c9539358dd275bcde18c23907c31d35d2519

+ 9 - 9
.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.md

@@ -6,21 +6,21 @@ English | [中文](2026-08-09-headless-direct-core-entry-point.zh.md)
 
 ## Problem
 
-The `headless` product contract is one local task with final assistant text on stdout, a success-sensitive exit code, no listening port, and the stderr reasoning projection owned by [headless reasoning progress](../feature/2026-08-21-headless-reasoning-progress.md). A composition containing Workspace Host services, ApiProxy, HTTP, the Web runtime, or browser plugins contradicts that contract and makes local completion depend on an unrelated transport tree.
+The `headless` product contract is one local task with final assistant text on stdout, a success-sensitive exit code, no listening port, and the stderr reasoning projection owned by [headless reasoning progress](../feature/2026-08-21-headless-reasoning-progress.md). A composition containing Workspace Host services, browser RPC, HTTP, the Web runtime, or browser plugins contradicts that contract and makes local completion depend on an unrelated transport tree.
 
 The direct entry point still needs the same deployment model state as Web-created Agents. A separate provider/model default would give one deployment two answers, while deriving completion before the Agent and Session persistence are quiescent permits stdout and the exit code to observe incomplete state.
 
 ## Decision
 
-The shipped `headless` profile contains `dsh-base` and `dsh-headless`. The base supplies the disabled module-HMR default; the headless bundle supplies its persona and tool mode, mounts the Code Mode worker explicitly, and inserts `headless-runner` without overriding that policy. Its tree contains no `@deepseek-ai/dsh-host-*` package, ApiProxy, HTTP server, Web runtime, or browser client. Code Mode and Session persistence are one-shot Agent capabilities independent of Web presentation.
+The shipped `headless` profile contains `dsh-base` and `dsh-headless`. The base supplies the disabled module-HMR default; the headless bundle supplies its persona and tool mode, mounts the PTC mode worker explicitly, and inserts `headless-runner` without overriding that policy. Its tree contains no browser Connection, HTTP server, Web runtime, or browser client. PTC mode and Session persistence are one-shot Agent capabilities independent of Web presentation.
 
 `headless-runner` is a direct core entry point. After Loader settlement, it reads `ctx.agentDefaultModel.currentSelection()`, creates a fresh persisted Agent through `ctx.agents.create`, installs that `ModelSelection` in the Agent scope, waits for startup quiescence, anchors the Session sequence, submits one ordinary user message, and waits for quiescence again. It awaits `ctx.sessions.flush`, folds its durable event interval for the last non-empty assistant text and final `turn/end` reason, writes the text plus one newline to stdout, and requests bounded launcher shutdown with exit 0 exactly when the reason is `completed`. [Headless reasoning progress](../feature/2026-08-21-headless-reasoning-progress.md) owns the live stderr projection; a terminal `error` reason writes its durable code and message there, and unexpected driver failures also use stderr and exit 1.
 
-`@deepseek-ai/dsh-agent-default-model` owns the transport-independent default used for an Agent without a session-local selection. `AgentDefaultModelConfig` provides `ctx.agentDefaultModel` and registers the `agent-default-model` Settings section. Composition config supplies `{provider, model}`; user settings may also supply `reasoningEffort`. `currentSelection()` returns the live complete selection and `saveSelection()` writes it as a complete section, so a selection without an effort clears any stored effort. `dsh-base` supplies the composition entry. Direct and ApiProxy entry points consume this service; ApiProxy alone owns session-local precedence, model validation, and persistence of accepted Web selections.
+`@deepseek-ai/dsh-agent-default-model` owns the transport-independent default used for an Agent without a session-local selection. `AgentDefaultModelConfig` provides `ctx.agentDefaultModel` and registers the `agent-default-model` Settings section. Composition config supplies `{provider, model}`; user settings may also supply `reasoningEffort`. `currentSelection()` returns the live complete selection and `saveSelection()` writes it as a complete section, so a selection without an effort clears any stored effort. `dsh-base` supplies the composition entry. Direct creation and Session Controller Remote calls consume this service; the Session Controller owns session-local precedence, model validation, and persistence of accepted Web selections.
 
 `loadProfile` recognizes the exact installation-owned headless tuple (`dsh-base`, `dsh-web-app`, `dsh-headless`) and normalizes it to the shipped headless template while preserving every other manifest field. Extra, missing, or reordered bundle lists are user-owned and remain untouched.
 
-This note owns the headless transport and completion contracts; [headless reasoning progress](../feature/2026-08-21-headless-reasoning-progress.md) owns successful stderr output. [Apps own their command lines](2026-08-06-app-owned-command-line.md) owns the current `dsh --profile headless` grammar; the former [`dsh run` decision](../../archived/feature/2026-08-08-dsh-run-headless-command.md) records the superseded launcher-owned grammar, [GUI layering and RPC protocol](2026-07-19-gui-layering-and-rpc-protocol.md) owns browser gateway boundaries, [web config-tree boot and transport layering](2026-07-24-web-config-tree-boot-and-transport-layering.md) owns the Web tree, and [the default model follows the picker](../feature/2026-08-07-default-model-follows-the-picker.md) owns persistence of the shared Agent default.
+This note owns the headless transport and completion contracts; [headless reasoning progress](../feature/2026-08-21-headless-reasoning-progress.md) owns successful stderr output. [Apps own their command lines](2026-08-06-app-owned-command-line.md) owns the current `dsh --profile headless` grammar; the former [`dsh run` decision](../../archived/feature/2026-08-08-dsh-run-headless-command.md) records the superseded launcher-owned grammar, [web config-tree boot and transport layering](2026-07-24-web-config-tree-boot-and-transport-layering.md) owns the Web tree, and [the default model follows the picker](../feature/2026-08-07-default-model-follows-the-picker.md) owns persistence of the shared Agent default.
 
 ## Verification
 
@@ -31,14 +31,14 @@ Package tests use the real Session store and Agent registry around a scripted Ag
 | Alternative | Contract mismatch |
 |---|---|
 | Keep `dsh-web-app` but suppress its observation line | The process still opens a port and carries the Host, Web, and browser trees. |
-| Build a Host-only one-shot bundle around ApiProxy | ApiProxy is a client protocol gateway; a local one-shot entry point has no client boundary. |
-| Use `InProcessApiClient` for product-level protocol coverage | Product execution would depend on an unrelated protocol solely to exercise that protocol. |
+| Build a Host-only one-shot bundle around browser RPC | A local one-shot entry point has no client boundary. |
+| Use the in-process Connection carrier for product-level protocol coverage | Product execution would depend on an unrelated protocol solely to exercise that protocol. |
 | Give headless a separate provider/model config | Direct and Web creation would have independent defaults and persistence. |
-| Omit Code Mode and Session persistence | Both capabilities belong to one-shot Agent execution rather than Web presentation. |
+| Omit PTC mode and Session persistence | Both capabilities belong to one-shot Agent execution rather than Web presentation. |
 | Normalize every tuple containing Web and headless bundles | Bundle lists are an extension surface; only the exact installation-owned tuple is safe to classify. |
 
 ## Consequences
 
-`dsh --profile headless` provides a local Agent task rather than browser observation, Host APIs, or HTTP. Users who need those capabilities choose `dsh web`. Text-only successful runs leave stderr empty, reasoned runs stream the provider-reported content there, completion follows durable flush, and the persisted Session remains available to later tooling. Its initial user message records `source.kind: 'user'` and therefore carries no ApiProxy `rpcId`.
+`dsh --profile headless` provides a local Agent task rather than browser observation, Host APIs, or HTTP. Users who need those capabilities choose `dsh web`. Text-only successful runs leave stderr empty, reasoned runs stream the provider-reported content there, completion follows durable flush, and the persisted Session remains available to later tooling. Its initial user message records `source.kind: 'user'` and therefore carries no browser request id.
 
-ApiProxy carrier coverage stays in the ApiProxy package. Custom one-shot profiles may include Host or Web bundles explicitly, while the shipped profile and the recognized installation-owned tuple are Web-free.
+Connection carrier coverage stays in the Connection package. Custom one-shot profiles may include Host or Web bundles explicitly, while the shipped profile and the recognized installation-owned tuple are Web-free.

+ 9 - 9
.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.zh.md

@@ -6,21 +6,21 @@ Status: implemented
 
 ## 问题
 
-`headless` 的产品约定是一个本地任务:最终 assistant 文本写入 stdout,退出状态反映成功与否,不打开监听端口,并由 [headless 推理进度](../feature/2026-08-21-headless-reasoning-progress.zh.md)负责 stderr 推理投影。包含 Workspace Host 服务、ApiProxy、HTTP、Web 运行时或浏览器插件的组合违背这一约定,也使本地完成状态依赖无关的传输树。
+`headless` 的产品约定是一个本地任务:最终 assistant 文本写入 stdout,退出状态反映成功与否,不打开监听端口,并由 [headless 推理进度](../feature/2026-08-21-headless-reasoning-progress.zh.md)负责 stderr 推理投影。包含 Workspace Host 服务、浏览器 RPC、HTTP、Web 运行时或浏览器插件的组合违背这一约定,也使本地完成状态依赖无关的传输树。
 
 直接入口仍需要与 Web 所创建 Agent 相同的部署模型状态。独立的提供方/模型默认值会让同一部署产生两种答案,而在 Agent 与会话持久化完全停稳之前推导完成状态,会让 stdout 与退出状态观察到不完整状态。
 
 ## 决策
 
-随附的 `headless` profile 包含 `dsh-base` 与 `dsh-headless`。base 提供默认禁用模块 HMR(热模块替换)的策略;headless 组合包提供自身的 persona 与工具模式、显式挂载 Code Mode worker,并在不覆盖该策略的情况下插入 `headless-runner`。其插件树不包含任何 `@deepseek-ai/dsh-host-*` 包、ApiProxy、HTTP server、Web 运行时或浏览器客户端。Code Mode 与会话持久化均为独立于 Web 呈现的一次性 Agent 能力。
+随附的 `headless` profile 包含 `dsh-base` 与 `dsh-headless`。base 提供默认禁用模块 HMR(热模块替换)的策略;headless 组合包提供自身的 persona 与工具模式、显式挂载 PTC mode worker,并在不覆盖该策略的情况下插入 `headless-runner`。其插件树不包含浏览器 Connection、HTTP server、Web 运行时或浏览器客户端。PTC mode 与会话持久化均为独立于 Web 呈现的一次性 Agent 能力。
 
 `headless-runner` 是直接使用核心服务的入口。Loader 完全加载后,它读取 `ctx.agentDefaultModel.currentSelection()`,通过 `ctx.agents.create` 创建一个新的持久化 Agent,在 Agent 作用域中安装该 `ModelSelection`,等待启动工作完全停稳,锚定会话事件序号,提交一条普通用户消息,再次等待完全停稳。随后,它等待 `ctx.sessions.flush`,折叠自身持有的持久事件区间,以取得最后一条非空 assistant 文本和最终 `turn/end` 结束原因,将文本连同一个换行写入 stdout,并且仅在结束原因为 `completed` 时请求启动器以退出状态 0 有界关闭。[Headless 推理进度](../feature/2026-08-21-headless-reasoning-progress.zh.md)负责实时 stderr 投影;结束原因为 `error` 时,其持久化错误码与消息写入 stderr,驱动器的意外失败也写入 stderr 并以 1 退出。
 
-`@deepseek-ai/dsh-agent-default-model` 拥有与传输无关的默认值,供没有会话级选择的 Agent 使用。`AgentDefaultModelConfig` 提供 `ctx.agentDefaultModel` 并注册 `agent-default-model` Settings 分节。组合配置提供 `{provider, model}`,用户设置还可以提供 `reasoningEffort`。`currentSelection()` 返回当前的完整选择,`saveSelection()` 则写入完整分节,因此不含强度的选择会清除已存强度。`dsh-base` 提供组合条目。直接入口与 ApiProxy 入口均消费该服务;只有 ApiProxy 负责会话级优先级、模型校验与已接受 Web 选择的持久化。
+`@deepseek-ai/dsh-agent-default-model` 拥有与传输无关的默认值,供没有会话级选择的 Agent 使用。`AgentDefaultModelConfig` 提供 `ctx.agentDefaultModel` 并注册 `agent-default-model` Settings 分节。组合配置提供 `{provider, model}`,用户设置还可以提供 `reasoningEffort`。`currentSelection()` 返回当前的完整选择,`saveSelection()` 则写入完整分节,因此不含强度的选择会清除已存强度。`dsh-base` 提供组合条目。直接创建与 Session Controller Remote 调用均消费该服务;Session Controller 负责会话级优先级、模型校验与已接受 Web 选择的持久化。
 
 `loadProfile` 识别安装过程拥有的精确 headless 元组(`dsh-base`、`dsh-web-app`、`dsh-headless`),将其规范化为随附的 headless 模板,并保留 manifest(元数据清单)的其他所有字段。带额外项、缺少项或顺序不同的组合包列表归用户所有,保持不变。
 
-本 Agent Note 负责 headless 的传输与完成约定;[headless 推理进度](../feature/2026-08-21-headless-reasoning-progress.zh.md)负责成功运行时的 stderr 输出。[应用持有自己的命令行](2026-08-06-app-owned-command-line.zh.md)负责当前的 `dsh --profile headless` 语法;原 [`dsh run` 决策](../../archived/feature/2026-08-08-dsh-run-headless-command.md)记录已被取代的启动器持有语法,[GUI 分层与 RPC 协议](2026-07-19-gui-layering-and-rpc-protocol.zh.md)负责浏览器网关边界,[Web 配置树启动与传输分层](2026-07-24-web-config-tree-boot-and-transport-layering.zh.md)负责 Web 插件树,[默认模型跟随选择器](../feature/2026-08-07-default-model-follows-the-picker.zh.md)负责共享 Agent 默认值的持久化。
+本 Agent Note 负责 headless 的传输与完成约定;[headless 推理进度](../feature/2026-08-21-headless-reasoning-progress.zh.md)负责成功运行时的 stderr 输出。[应用持有自己的命令行](2026-08-06-app-owned-command-line.zh.md)负责当前的 `dsh --profile headless` 语法;原 [`dsh run` 决策](../../archived/feature/2026-08-08-dsh-run-headless-command.md)记录已被取代的启动器持有语法,[Web 配置树启动与传输分层](2026-07-24-web-config-tree-boot-and-transport-layering.zh.md)负责 Web 插件树,[默认模型跟随选择器](../feature/2026-08-07-default-model-follows-the-picker.zh.md)负责共享 Agent 默认值的持久化。
 
 ## 验证
 
@@ -31,14 +31,14 @@ Status: implemented
 | 替代方案 | 约定不匹配之处 |
 |---|---|
 | 保留 `dsh-web-app`,但隐藏观察行 | 进程仍会打开端口并携带 Host、Web 与浏览器插件树。 |
-| 围绕 ApiProxy 构建纯 Host 一次性组合包 | ApiProxy 是客户端协议网关,而本地一次性入口没有客户端边界。 |
-| 使用 `InProcessApiClient` 实现产品级协议覆盖 | 产品执行会仅为测试无关协议而依赖该协议。 |
+| 围绕浏览器 RPC 构建纯 Host 一次性组合包 | 本地一次性入口没有客户端边界。 |
+| 使用进程内 Connection carrier 实现产品级协议覆盖 | 产品执行会仅为测试无关协议而依赖该协议。 |
 | 为 headless 单独提供提供方/模型配置 | 直接创建与 Web 创建会拥有彼此独立的默认值和持久化。 |
-| 省略 Code Mode 与会话持久化 | 两项能力都属于一次性 Agent 执行,而不是 Web 呈现。 |
+| 省略 PTC mode 与会话持久化 | 两项能力都属于一次性 Agent 执行,而不是 Web 呈现。 |
 | 规范化所有包含 Web 与 headless 组合包的元组 | 组合包列表是扩展面;只有精确的安装过程所属元组可以安全分类。 |
 
 ## 后果
 
-`dsh --profile headless` 提供本地 Agent 任务,而不是浏览器观察、Host API 或 HTTP。需要这些能力的用户选择 `dsh web`。没有推理内容的成功运行会保持 stderr 为空,有推理内容的运行则在那里流式输出提供方报告的内容;完成结果在持久化 flush 后推导,持久化会话仍可供后续工具使用。初始用户消息记录 `source.kind: 'user'`,因此不携带 ApiProxy `rpcId`。
+`dsh --profile headless` 提供本地 Agent 任务,而不是浏览器观察、Host API 或 HTTP。需要这些能力的用户选择 `dsh web`。没有推理内容的成功运行会保持 stderr 为空,有推理内容的运行则在那里流式输出提供方报告的内容;完成结果在持久化 flush 后推导,持久化会话仍可供后续工具使用。初始用户消息记录 `source.kind: 'user'`,因此不携带浏览器 request id。
 
-ApiProxy 载体覆盖保留在 ApiProxy 包中。自定义一次性 profile 可以显式包含 Host 或 Web 组合包;随附 profile 与可识别的安装过程所属元组均不含 Web。
+Connection carrier 覆盖保留在 Connection 包中。自定义一次性 profile 可以显式包含 Host 或 Web 组合包;随附 profile 与可识别的安装过程所属元组均不含 Web。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.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-08-10-remote-event-delivery.md
-2026-08-10-remote-event-delivery.md: 5e6e04bdf2c6b685bbf10f05ede9c96ea8104429
-2026-08-10-remote-event-delivery.zh.md: d744b92d47f5778397b519fa09d4b91f620dcd7e
+2026-08-10-remote-event-delivery.md: 4b9c2f224e36fb97f798492c999726eb55bad214
+2026-08-10-remote-event-delivery.zh.md: 4ca1f8c244df4e985d1223b6c468b450be943abf

+ 5 - 6
.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.md

@@ -70,9 +70,9 @@ $on<Event extends TypertRemoteEvent>(event: Event, listener: TypertClientEventLi
 
 **The contract exposes only the consumer verb.** `ClientRemoteService` registers the one internal `$events` pump as a Connection generation source when it activates, independently of whether any `$on` subscription exists. Browsers open `$events` through the shared Remote mux; in-process compositions open the same logical stream through `connection.rpc.open`. Decoding, exact item validation, and Cordis dispatch are private Gateway Client implementation. `TypertClientRemote` exposes no producer operation, so a business plugin cannot synthesize a Host event.
 
-Each time the Host opens `$events`, the API Remotes source factory installs every allowlist listener synchronously. Gateway then yields the opening `{ type: 'ready' }` before iterating the event source. `ConnectionController` waits for that ready item and `host.describe` in parallel and publishes `connected` only after both succeed, so baseline reads cannot race ahead of incremental listeners.
+Each time the Host opens `$events`, the API Remotes source factory installs every allowlist listener synchronously. Gateway then yields the opening `{ type: 'ready', clientId, host: { home } }` before iterating the event source. `ConnectionController` publishes `connected` only after that item arrives, so baseline reads cannot race ahead of incremental listeners.
 
-A physical mux disconnect ends the logical stream with `RemoteStreamCarrierError`. A Host Remote stream error, unexpected normal completion, non-ready opening item, or malformed event item also ends the current generation. Connection withdraws that generation's `hostDescription` and reopens `$events` and `host.describe` after backoff; Gateway mux only rebuilds the physical WebSocket. Ordinary events are not replayed. State whose correctness requires recovery must provide a query, cursor, or opening baseline and cannot treat `$on` as a reliable journal.
+A physical mux disconnect ends the logical stream with `RemoteStreamCarrierError`. A Host Remote stream error, unexpected normal completion, non-ready opening item, or malformed event item also ends the current generation. Connection withdraws that generation and reopens `$events` after backoff; Gateway mux only rebuilds the physical WebSocket. Ordinary events are not replayed. State whose correctness requires recovery must provide a query, cursor, or opening baseline and cannot treat `$on` as a reliable journal.
 
 The Client dispatches on a Cordis key private to each Remote instance. Ordinary `emit` uses `parallel()` and contains listener failures; Agent-scoped `waterfall` uses `waterfall()` on the resolved Agent Context and allows a result, rejection, or `next()` delegation. Both registration kinds belong to the calling fiber, and Host events do not trigger same-named Client-local events.
 
@@ -132,13 +132,13 @@ cancel    { type, eventId }
 
 The Client opens internal logical stream `$events` with payload `{ args: {} }`. Gateway rejects extra parameters, a missing Host source, and duplicate source registration. Withdrawing a source aborts every stream opened by that registration. Each Client stream owns an independent queue and allowlist listener set in `api/remotes`, so disconnecting one Client neither consumes nor withdraws another Client's events.
 
-The Client requires an opening `ready` item with a non-empty `clientId`; every later item is checked for exact fields by discriminant. An ordinary `emit` with an unknown but structurally valid event name is dropped when there is no subscriber. Waterfalls use `eventId` to correlate `$events/result` and `agentId` to select a Client Agent Context. The Client returns only values representable as lossless JSON; transport does not reinterpret business fields.
+The Client requires an opening `ready` item with a non-empty `clientId` and `host.home`; every later item is checked for exact fields by discriminant. The ready item establishes the Connection generation and supplies the stable Host path-display fact. An ordinary `emit` with an unknown but structurally valid event name is dropped when there is no subscriber. Waterfalls use `eventId` to correlate `$events/result` and `agentId` to select a Client Agent Context. The Client returns only values representable as lossless JSON; transport does not reinterpret business fields.
 
 `$events` is an internal Gateway endpoint. It does not enter a generated Typert Remote descriptor or become `ctx.remote.<namespace>`. Application selection exists only in the API Remotes allowlist and Host source; Gateway owns registration, payload validation, and physical transport only.
 
 ### The `apps/web` browser e2e belongs to the Host face
 
-The `apps/web/tests/**` e2e files typecheck in root `tsconfig.host.json`: they boot a real harness in process and directly access `ctx.apiProxy`, Host `SessionStore.get/create/flush`, and `ctx.sessionProjectionCache`. Driving a browser at runtime does not place a file in the Client TypeScript program. Moving these tests to the Client aggregate produces 21 errors because one program cannot hold both faces' merges for the same Context key.
+The `apps/web/tests/**` e2e files typecheck in root `tsconfig.host.json`: they boot a real harness in process and directly access `ctx.connection`, Host `SessionStore.get/create/flush`, and `ctx.sessionProjectionCache`. Driving a browser at runtime does not place a file in the Client TypeScript program. Moving these tests to the Client aggregate produces errors because one program cannot hold both faces' merges for the same Context key.
 
 This implies one build rule needed by the design: importing a value or type from a Client package in those tests brings that package's whole project and all its project references into the Host build graph. Four consumers (`ui-settings-general`, `ui-settings-models`, `ui-permission`, and `ui-commands`) reference API Remotes' Client face, which cannot compile until Host tsdown generates `@deepseek-ai/dsh-goal/remote`. That forms a build-order cycle: Host tsc needs API Remotes Client, which needs generated `goal/remote`, which Host tsdown emits after Host tsc.
 
@@ -153,11 +153,10 @@ The few required Client symbols are mirrored on the test side: `scaffold.ts` exp
 | `api/remotes` | `src/remote-events.ts` (mode-bearing allowlist value) and `src/types.ts` (key projection and selection) belong to both faces; Host registers each Client source and validates JSON before queueing; Client continues to compose generated Remote contributions |
 | Root `tsconfig.base.json` | Adds source-plane `paths` entries for `dsh-settings/types`, `dsh-credentials/types`, and `dsh-api-remotes/types` |
 | `dsh-commands` / `dsh-settings` / `dsh-credentials` | Moves each `interface Events` member to the owner's Client-safe `./types`; settings and credentials add that export, move brands and pure types with it, retain constructors in index, and include `lib/types/**/*.js` in published files |
-| `host/apiproxy` | Contains no `HostFrame`, `events.host()`, or other Host downlink carrier; API Proxy does not participate in Host events or Connection generation |
 | `dsh-session` | Exposes `isJsonValue` for validation of every event argument by the API Remotes Host source |
 | `client/runtime` | Removes the bridge from Host frames to the Remote subscription table; it only publishes `connection/reset` after a Connection generation is established |
 | Consumers | Client plugins subscribe directly through `ctx.remote.$on(...)`, import owner event declarations type-only, and inject `'remote'` |
-| `client/connection` | Provides the one generation-source registration point; `ConnectionController` combines `$events` ready with `host.describe`, and the fixture emits events from the same source |
+| `client/connection` | Provides the one generation-source registration point; `ConnectionController` publishes the Host facts from `$events` ready, and the fixture emits events from the same source |
 | `apps/web/tests` + `apps/cli` | Mirrors Client symbols on the test side as described above and removes 15 Client project references from `apps/cli/tsconfig.json` |
 
 ## Alternatives considered

+ 5 - 6
.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.zh.md

@@ -70,9 +70,9 @@ $on<Event extends TypertRemoteEvent>(event: Event, listener: TypertClientEventLi
 
 **契约只公开消费动词。**`ClientRemoteService` 激活时就把内部唯一的 `$events` pump 注册为 Connection generation source,与当前有无 `$on` 订阅无关;浏览器通过共享 Remote mux 打开 `$events`,进程内组合通过 `connection.rpc.open` 打开同一 logical stream。解码、精确 item 校验和订阅表派发都是 Gateway Client 的私有实现,`TypertClientRemote` 不暴露生产方方法,因此业务插件不能伪造一条 Host 事件。
 
-每次 Host 打开 `$events` 时,API Remotes source factory 先同步挂载所有 allowlist listener,Gateway 随后产出首项 `{ type: 'ready' }`,再开始迭代事件 source。`ConnectionController` 并行等待该 ready 与 `host.describe`,只有两者都成功才发布 `connected` 并允许 baseline 读取。这个顺序保证 baseline 不会跑在增量 listener 前面。
+每次 Host 打开 `$events` 时,API Remotes source factory 先同步挂载所有 allowlist listener,Gateway 随后产出首项 `{ type: 'ready', clientId, host: { home } }`,再开始迭代事件 source。`ConnectionController` 只有在该项到达后才发布 `connected`,因此 baseline 读取不会跑在增量 listener 前面。
 
-物理 mux 断开会让 logical stream 以 `RemoteStreamCarrierError` 结束;Host 返回的 Remote stream error、意外正常结束、非 ready 首项或畸形事件项也会结束当前 generation。Connection 撤回该 generation 的 `hostDescription`,在退避后重开 `$events` 和 `host.describe`;Gateway mux 只负责重建物理 WebSocket。转发事件不重放;凡正确性依赖恢复的状态,owner 必须另有查询、cursor 或 opening baseline,不能把 `$on` 当作可靠日志。
+物理 mux 断开会让 logical stream 以 `RemoteStreamCarrierError` 结束;Host 返回的 Remote stream error、意外正常结束、非 ready 首项或畸形事件项也会结束当前 generation。Connection 撤回该 generation,在退避后重开 `$events`;Gateway mux 只负责重建物理 WebSocket。转发事件不重放;凡正确性依赖恢复的状态,owner 必须另有查询、cursor 或 opening baseline,不能把 `$on` 当作可靠日志。
 
 Client 以 Remote 实例私有 Cordis key 分发。普通 `emit` 使用 `parallel()` 并隔离 listener 失败;Agent-scoped `waterfall` 在解析出的 Agent Context 上使用 `waterfall()`,允许结果、拒绝或 `next()` 委托。两类注册都归属调用方 fiber,且 Host 事件不会触发 Client 本地同名事件。
 
@@ -132,13 +132,13 @@ cancel    { type, eventId }
 
 Client 以 endpoint `$events` 和 payload `{ args: {} }` 打开 internal logical stream。Gateway 拒绝额外参数、缺失 Host source 和重复 source 注册;source 被撤回时会中止所有由该注册打开的 stream。每个 Client stream 在 `api/remotes` 中拥有独立队列与一组 allowlist listener,因此一个 Client 断开不会消费或撤销另一个 Client 的事件。
 
-Client 要求首项是带非空 `clientId` 的 `ready`;后续 item 按 discriminant 精确校验字段。普通 `emit` 的未知但结构合法事件名在没有订阅者时静默丢弃。waterfall 通过 `eventId` 关联 `$events/result`,并由 `agentId` 选择 Client Agent Context;Client 只回传可无损表示为 JSON 的结果,不在 transport 层重复解释业务字段。
+Client 要求首项是带非空 `clientId` 与 `host.home` 的 `ready`;后续 item 按 discriminant 精确校验字段。ready 项建立 Connection generation,并提供稳定的 Host 路径显示信息。普通 `emit` 的未知但结构合法事件名在没有订阅者时静默丢弃。waterfall 通过 `eventId` 关联 `$events/result`,并由 `agentId` 选择 Client Agent Context;Client 只回传可无损表示为 JSON 的结果,不在 transport 层重复解释业务字段。
 
 `$events` 是 Gateway 内部 endpoint,不进入生成的 Typert Remote descriptor,也不成为 `ctx.remote.<namespace>`。应用选择仍只存在于 `api/remotes` 的 allowlist 和 Host source;Gateway 只拥有注册、payload 校验与物理传输。
 
 ### apps/web 的 browser e2e 属于 Host 面
 
-`apps/web/tests/**` 那批 e2e 在**根 `tsconfig.host.json`** 做类型检查:它们在进程内起真 harness、直接摸 `ctx.apiProxy`、host `SessionStore.get/create/flush`、`ctx.sessionProjectionCache`。**运行时用浏览器 ≠ 类型上属于 client 程序**——把它们搬进 client 聚合会立刻报 21 条错,因为一个 program 装不下两个 face 对同一个 Context key 的合并。
+`apps/web/tests/**` 那批 e2e 在**根 `tsconfig.host.json`** 做类型检查:它们在进程内起真 harness、直接访问 `ctx.connection`、Host `SessionStore.get/create/flush` 与 `ctx.sessionProjectionCache`。**运行时用浏览器 ≠ 类型上属于 Client 程序**——把它们搬进 Client 聚合会报错,因为一个 program 装不下两个 face 对同一个 Context key 的合并。
 
 由此得到一条对本设计要紧的连带纪律:**这些测试从客户端包 import 值或类型,会把该包的整个 project——以及它引用的每个 project——拖进 Host 构建图**。`ui-settings-general`/`ui-settings-models`/`ui-permission`/`ui-commands` 四个消费者 references `api/remotes` 的 client face,而该 face 必须等 host tsdown 生成 `@deepseek-ai/dsh-goal/remote` 才能编译,于是形成构建期死锁:host tsc → api/remotes client face → `goal/remote` → host tsdown → 排在 host tsc 之后。
 
@@ -153,11 +153,10 @@ Client 要求首项是带非空 `clientId` 的 `ready`;后续 item 按 discrim
 | `api/remotes` | `src/remote-events.ts`(带 mode 的名单值)与 `src/types.ts`(键投影 + selection)双列进两个 face;Host 半注册每 Client source,并在入队前校验 JSON;Client 半继续组合生成的 Remote contribution |
 | 根 `tsconfig.base.json` | 加 `dsh-settings/types`、`dsh-credentials/types`、`dsh-api-remotes/types` 三条 `paths`,全部指向**源**平面 |
 | `dsh-commands` / `dsh-settings` / `dsh-credentials` | `interface Events` 子块移入各自 client-safe 的 `./types`(settings/credentials 新建该出口,brand 与纯类型一并移入,index 继续 re-export 并留住构造器;`files` 补 `lib/types/**/*.js`) |
-| `host/apiproxy` | 不包含 `HostFrame`、`events.host()` 或其他 Host 下行 carrier;API Proxy 不参与 Host 事件或 Connection generation |
 | `dsh-session` | `isJsonValue` 供 `api/remotes` Host source 校验每个事件参数 |
 | `client/runtime` | 删除 Host frame 到 Remote subscription table 的桥;只继续在 Connection generation 建立后发布 `connection/reset` |
 | 消费方 | Client 插件直接订阅 `ctx.remote.$on(...)`,type-only 引入 owner 事件声明并把 `'remote'` 加进 `inject` |
-| `client/connection` | 提供唯一 generation source 注册位;`ConnectionController` 以 `$events` ready 与 `host.describe` 组成世代握手,fixture 也从同一 source 产生事件 |
+| `client/connection` | 提供唯一 generation source 注册位;`ConnectionController` 发布 `$events` ready 携带的 Host 信息,fixture 也从同一 source 产生事件 |
 | `apps/web/tests` + `apps/cli` | 客户端符号镜像(见上节);`apps/cli/tsconfig.json` 删 15 条 client 工程引用 |
 
 ## 备选方案

+ 3 - 3
.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.i18n.yaml → .agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml

@@ -1,6 +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-04-websocket-downlink-carrier.md
-2026-08-04-websocket-downlink-carrier.md: 420a0d30f31cca58448adc0afd46a5d6d5e9107f
-2026-08-04-websocket-downlink-carrier.zh.md: 5f277a4ed33ffe97b51587fef960746b93eff1c1
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md
+2026-08-10-unary-apiproxy-remote-migration.md: b98c7ee95b61ec00a5cab3106e812a6f17fc0a15
+2026-08-10-unary-apiproxy-remote-migration.zh.md: 74bca8fd72f3e41075f5a44eba116fe127343bb2

+ 65 - 0
.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md

@@ -0,0 +1,65 @@
+# Agent Note: Place unary browser operations on owning Remote services
+
+Status: implemented
+
+English | [中文](2026-08-10-unary-apiproxy-remote-migration.zh.md)
+
+## Problem
+
+The Host API Proxy duplicated simple unary operations across business Services, API Proxy interfaces, Zod schemas, route tables, client stubs, and Client callers. [Typert Remote calls](2026-08-02-typert-remote-method-calls.md) already let a business package own this class of call, but moving an endpoint without its lifecycle and projection policy could change observable behavior.
+
+Agent-bound calls require particular care. Shared lookup policy reuses live Agents, resumes ordinary cold Sessions with their recorded presets, deduplicates concurrent resumes, and rejects subagent-owned identities. Skill listing instead must inspect a Session without activating its Agent. Settings and preset operations keep their Host-owned document paths out of browser requests; Session file links preserve their caller-resolved path behavior.
+
+## Decision
+
+Simple unary operations live on their natural business Remote owner. The business package owns the Remote signature and Host adaptation; `@deepseek-ai/dsh-api-remotes/client` selects its generated contribution; the Client package owns presentation joins. Connection owns the transport envelope and exact Fetch route registry, and no API Proxy service remains.
+
+| Former API Proxy operation | Destination | Owner and preserved behavior |
+|---|---|---|
+| `session.rename` | `sessionTitle/rename` | `SessionTitleService` resolves the Session through the shared lookup policy and returns the title event sequence. |
+| `command.list`, `command.execute` | `commands/list`, `commands/execute` | `CommandRuntime` preserves Agent lookup, unmatched commands, and caller cancellation. |
+| `llm.providers` | `llm/listProviders`, `llm/listConfigurableProviders` | `LlmRuntime` owns provider facts; Clients join live and configurable rows. |
+| `llm.discoverModels` | `llm/discoverModels` | `LlmRuntime` preserves provider discovery, cancellation, and sanitized failures. |
+| `llm.models` | `session/modelCatalog` | `SessionController` owns the Host-generation catalog, default selection, and isolated provider failures. |
+| `credentials.describe`, `credentials.set`, `credentials.unset` | `credentials/describe`, `credentials/set`, `credentials/unset` | `CredentialsController` preserves reference validation, field projection, provider diagnostics, and refusal mapping. |
+| `settings.describe`, `settings.update`, `settings.replace`, `settings.mutate` | Equivalent `settings/*` methods | `SettingsController` preserves redaction, mutation semantics, revision checks, and provider failures. |
+| `settings.openDocument` | `settings/openSettingsDocument` | `SettingsController` prepares the provider-owned document and opens it with text-editor intent. |
+| `agentPreset.read`, `agentPreset.copy`, `agentPreset.remove` | Equivalent `agentPresets/*` methods | `AgentPresetService` owns document reads, copies, and removals. |
+| `agentPreset.openDocument` | `settings/openAgentPresetDirectory` | `SettingsController` resolves the preset directory and returns its path when native opening is unavailable. |
+| `subagent.interrupt` | `subagents/interruptByParent` | The subagent service preserves parent authority without activating either Agent. |
+| `workspace.list`, `workspace.insertSessionBefore`, `workspace.archiveSession` | Equivalent `workspace/*` methods | The Workspace registry owns detached snapshots and serialized mutations. |
+| `skill.list` | `skills/list` | `SessionSkillCatalog` observes the Session and its recorded preset, uses a live Agent only when one already exists, and never activates an Agent for listing. |
+| `fileReferences/list` | `fileReferences/list` | `SessionFileReferences` supplies the Session Controller's established Agent lookup to the provider; cold lookup behavior remains unchanged. |
+| `host.openPath` | `session/openWorkspacePath` | The Session-aware Client resolves relative paths against the known workspace before `SessionController` hands them to the native opener. |
+| `host.describe` | `$events` ready frame plus capability queries | API Remotes sends the Host home with generation readiness; Settings and Session controllers report their native-open capabilities when the corresponding page appears. Unused process metadata is not sent. |
+| `session.export` | `GET`/`HEAD /api/session.export` | `session-log-export` registers an exact Connection Fetch route and streams the ZIP without a JSON Remote envelope. |
+
+The shared Agent and Session resolver remains the authority for endpoints that accept those objects. It provides the same live reuse, cold restoration, concurrent deduplication, preset setup, persistence failures, and subagent ownership fence that legacy API Proxy calls used. `TypertLookupFailure` preserves resolver-owned RPC errors instead of collapsing them into `internal`.
+
+The native path implementation lives in `@deepseek-ai/dsh-native-command`. Settings controllers select Host-owned targets, while Session-aware Clients resolve workspace paths before calling `SessionController`; the utility only performs platform detection, WSL translation, browser preference, text-editor intent, and shell-free command execution.
+
+## Browser authentication
+
+Connection authenticates the complete `/api` request before choosing a Typert endpoint or exact Fetch route. Remote calls and Session-log downloads therefore require the same browser session and Host/Origin checks.
+
+## Verification
+
+Focused Host and Client tests cover Remote calls, lookup and no-activation policy, native opening, error projection, and removal of legacy routes. The repository build generates and consumes the selected Remote contributions before building the Web application.
+
+## Alternatives considered
+
+**Keep simple calls in the API Proxy.** Rejected because it preserves duplicate interfaces, schemas, route rows, stubs, and result projections after a business owner exists.
+
+**Keep `host.describe`.** Rejected because one bootstrap call coupled Connection readiness to unrelated process and business facts. The generation-ready frame carries the only lifecycle fact needed immediately, and capability-owning pages query their domains when shown.
+
+**Publish every business capability in the generation-ready frame.** Rejected because those values have no common update lifecycle. Only the stable Host home belongs to Connection; each business owner answers its own current capability.
+
+**Represent Session export as a Remote.** Rejected because the browser download manager consumes a streamed HTTP response rather than a JSON result. An exact registered Fetch route keeps ownership in the feature package without adding a second gateway.
+
+**Put native opening in one controller.** Rejected because Session and Settings select different authorized targets. A Host utility avoids controller-to-controller imports without making the browser authoritative for filesystem targets.
+
+## Consequences
+
+Business owners and Client consumers each define one side of a unary operation, while Connection owns authentication, transport, response envelopes, exact Fetch routes, and generation state. Removing the legacy client timeout is the accepted observable transport change; business results, cancellation, lifecycle policy, filtering, and native-path authority remain owned by their existing domains.
+
+Generated Remote artifacts and the explicit API Remotes assembly become required whenever a Remote signature or selected package changes.

+ 65 - 0
.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md

@@ -0,0 +1,65 @@
+# Agent Note: 将一元浏览器操作放到所属 Remote 服务
+
+Status: implemented
+
+[English](2026-08-10-unary-apiproxy-remote-migration.md) | 中文
+
+## 问题
+
+Host API Proxy 曾在业务 Service、API Proxy interface、Zod schema、路由表、Client stub 与 Client 调用方之间重复定义简单一元操作。[Typert Remote 调用](2026-08-02-typert-remote-method-calls.zh.md)已经允许业务包持有这类调用,但如果迁移 endpoint 时没有一并保留生命周期与投影策略,就会改变可观察行为。
+
+与 Agent 绑定的调用需要格外谨慎。共享 lookup 策略会复用 live Agent、用记录的 preset 恢复普通冷 Session、对并发恢复去重,并拒绝由 subagent 持有的 identity。skill 列表则必须检查 Session 而不激活 Agent。Settings 与 preset 操作不会把 Host 持有的文档路径放进浏览器请求;Session 文件链接保留由调用方解析路径的行为。
+
+## 决策
+
+简单一元操作归属其自然的业务 Remote owner。业务包持有 Remote 签名与 Host 适配;`@deepseek-ai/dsh-api-remotes/client` 选择其生成贡献;Client 包持有呈现联接。Connection 持有传输 envelope 与精确 Fetch 路由注册表,不再存在 API Proxy 服务。
+
+| 原 API Proxy 操作 | 目标 | Owner 与保留行为 |
+|---|---|---|
+| `session.rename` | `sessionTitle/rename` | `SessionTitleService` 通过共享 lookup 策略解析 Session,并返回标题事件序号。 |
+| `command.list`、`command.execute` | `commands/list`、`commands/execute` | `CommandRuntime` 保留 Agent lookup、未匹配命令与调用方取消。 |
+| `llm.providers` | `llm/listProviders`、`llm/listConfigurableProviders` | `LlmRuntime` 持有 provider 事实;Client 联接 live 与 configurable 行。 |
+| `llm.discoverModels` | `llm/discoverModels` | `LlmRuntime` 保留 provider 发现、取消与净化后的失败。 |
+| `llm.models` | `session/modelCatalog` | `SessionController` 持有 Host generation 的目录、默认选择与隔离后的 provider 失败。 |
+| `credentials.describe`、`credentials.set`、`credentials.unset` | `credentials/describe`、`credentials/set`、`credentials/unset` | `CredentialsController` 保留引用校验、字段投影、provider 诊断与拒绝映射。 |
+| `settings.describe`、`settings.update`、`settings.replace`、`settings.mutate` | 对应的 `settings/*` 方法 | `SettingsController` 保留脱敏、mutation 语义、revision 校验与 provider 失败。 |
+| `settings.openDocument` | `settings/openSettingsDocument` | `SettingsController` 准备 provider 持有的文档,并按文本编辑器意图打开。 |
+| `agentPreset.read`、`agentPreset.copy`、`agentPreset.remove` | 对应的 `agentPresets/*` 方法 | `AgentPresetService` 持有文档读取、复制与删除。 |
+| `agentPreset.openDocument` | `settings/openAgentPresetDirectory` | `SettingsController` 解析 preset 目录,并在原生打开不可用时返回其路径。 |
+| `subagent.interrupt` | `subagents/interruptByParent` | subagent 服务保留 parent 权限,且不激活任何一方的 Agent。 |
+| `workspace.list`、`workspace.insertSessionBefore`、`workspace.archiveSession` | 对应的 `workspace/*` 方法 | Workspace registry 持有脱离可变对象的 snapshot 与串行 mutation。 |
+| `skill.list` | `skills/list` | `SessionSkillCatalog` 观察 Session 及其记录的 preset,仅在 live Agent 已存在时使用它,列表查询绝不激活 Agent。 |
+| `fileReferences/list` | `fileReferences/list` | `SessionFileReferences` 向 provider 提供 Session Controller 的既有 Agent lookup;冷 lookup 行为保持不变。 |
+| `host.openPath` | `session/openWorkspacePath` | Session-aware Client 先基于已知 workspace 解析相对路径,再由 `SessionController` 交给原生打开器。 |
+| `host.describe` | `$events` ready frame 与 capability 查询 | API Remotes 随 generation readiness 发送 Host home;Settings 与 Session controller 在对应页面显示时报告各自的原生打开能力。不发送无人使用的进程元数据。 |
+| `session.export` | `GET`/`HEAD /api/session.export` | `session-log-export` 注册精确的 Connection Fetch 路由,并在没有 JSON Remote envelope 的情况下流式传输 ZIP。 |
+
+共享 Agent 与 Session resolver 仍是接收这些对象的 endpoint 的权威。它提供与旧 API Proxy 调用相同的 live 复用、冷恢复、并发去重、preset setup、持久化失败与 subagent ownership fence。`TypertLookupFailure` 保留 resolver 持有的 RPC error,而不把它们归并为 `internal`。
+
+原生路径实现在 `@deepseek-ai/dsh-native-command` 中。Settings controller 选择 Host 持有的目标,Session-aware Client 则在调用 `SessionController` 前解析 workspace 路径;该工具仅负责平台探测、WSL 转换、浏览器偏好、文本编辑器意图与无 shell 命令执行。
+
+## 浏览器认证
+
+Connection 在选择 Typert endpoint 或精确 Fetch 路由前认证完整的 `/api` 请求。因此 Remote 调用与 Session 日志下载要求相同的浏览器会话和 Host/Origin 校验。
+
+## 验证
+
+聚焦的 Host 与 Client 测试覆盖 Remote 调用、lookup 与不激活策略、原生打开、错误投影和 legacy 路由移除。仓库构建会先生成并消费所选 Remote contribution,再构建 Web 应用。
+
+## 考虑过的替代方案
+
+**将简单调用留在 API Proxy。** 否决,因为业务 owner 已存在后,这仍会保留重复的 interface、schema、路由行、stub 与结果投影。
+
+**保留 `host.describe`。** 否决,因为一次 bootstrap 调用会把 Connection readiness 与互不相关的进程和业务事实耦合起来。generation-ready frame 只携带立即需要的生命周期事实,各 capability owner 页面在显示时查询自己的当前能力。
+
+**在 generation-ready frame 中发布所有业务 capability。** 否决,因为这些值没有共同的更新生命周期。只有稳定的 Host home 属于 Connection;各业务 owner 回答自己的当前 capability。
+
+**把 Session export 表示为 Remote。** 否决,因为浏览器下载管理器消费流式 HTTP 响应,而不是 JSON 结果。精确注册的 Fetch 路由让功能包持有该行为,同时不引入第二个 gateway。
+
+**把原生打开操作放入某个 controller。** 否决,因为 Session 与 Settings 选择不同的授权目标。Host 工具可以避免 controller 间导入,同时不让浏览器成为文件系统目标的权威。
+
+## 后果
+
+业务 owner 与 Client consumer 各自定义一元操作的一侧,而 Connection 持有认证、传输、响应 envelope、精确 Fetch 路由与 generation 状态。删除 legacy Client timeout 是已接受的可观察传输变化;业务结果、取消、生命周期策略、过滤与原生路径权限仍由既有领域持有。
+
+每当 Remote 签名或所选包发生变化,都必须更新生成的 Remote 产物和显式 API Remotes assembly。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.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-08-17-settings-describe-mirror.md
-2026-08-17-settings-describe-mirror.md: a3774699ff328a44aed192a16dea0fa19d03c83c
-2026-08-17-settings-describe-mirror.zh.md: 1fad68ae6346d656cc7868121eac14fdf845f8dc
+2026-08-17-settings-describe-mirror.md: 81be16c83e61f8c44b4903b4cc57e26d80fe065e
+2026-08-17-settings-describe-mirror.zh.md: 6c37992b7de7c2c225bb2eb965f6f758b8be990e

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.md

@@ -30,5 +30,5 @@ The cold-boot budget is pinned at two reads by `apps/web/tests/startup-rpc-budge
 - Startup `settings.describe` went 15 → 2, and a new preference-owning plugin adds zero reads.
 - Every derived surface shows the same document revision at any moment; the per-reader guards (`refreshWelcomeIfLoaded`, `refreshPermissionIfLoaded`, `refreshDocumentIfLoaded`) and their subscriptions are gone.
 - The mirror refreshes on every document commit regardless of namespace, so an external settings edit now costs one background read even while no settings surface is open — the price of surfaces that open already fresh. The per-namespace `ns !== spec.namespace` filters are gone with the per-scope subscriptions.
-- `credentials.describe` (3 startup calls), `agentPreset.list` (2), and `llm.providers` are separate sources and stay direct; the same mirror pattern fits them if they ever need it.
+- `credentials/describe` (3 startup calls), `agentPresets/list` (2), `llm/listProviders`, and `llm/listConfigurableProviders` are separate sources and stay direct; the same mirror pattern fits them if they ever need it.
 - A new direct `settings.describe` caller in client code is a budget regression; the e2e's failure message says to grep for callers outside `ui-settings`.

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.zh.md

@@ -30,5 +30,5 @@ Status: implemented
 - 启动期 `settings.describe` 从 15 次降到 2 次,新增持有偏好设置的插件带来零次新增读取。
 - 任一时刻每个派生面看到的都是同一份文档 revision;各读取方的防护(`refreshWelcomeIfLoaded`、`refreshPermissionIfLoaded`、`refreshDocumentIfLoaded`)及其订阅随之消失。
 - 镜像对任何命名空间的文档提交都会刷新,因此在没有任何设置表面打开时,一次外部设置编辑现在也花费一次后台读取——这是「表面打开即新鲜」的代价。随着各 scope 订阅的删除,按命名空间的 `ns !== spec.namespace` 过滤一并消失。
-- `credentials.describe`(启动 3 次)、`agentPreset.list`(2 次)与 `llm.providers` 是另外的数据源,保持直连;若将来需要,同一镜像模式对它们同样适用。
+- `credentials/describe`(启动 3 次)、`agentPresets/list`(2 次)、`llm/listProviders` 与 `llm/listConfigurableProviders` 是另外的数据源,保持直连;若将来需要,同一镜像模式对它们同样适用。
 - 客户端代码中新增直连 `settings.describe` 调用即是预算回归;e2e 的失败信息会提示在 `ui-settings` 之外 grep 调用方。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.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-08-18-session-history-and-event-transport.md
-2026-08-18-session-history-and-event-transport.md: 5f4aba19d147eae3f49fcefc9a8006d0f557dc6c
-2026-08-18-session-history-and-event-transport.zh.md: dcf7e7ffc5ad325756b1fb37dd5ad60d2f0b3147
+2026-08-18-session-history-and-event-transport.md: 8f26b2977dceeb2085bf270ae603cd21d48157f5
+2026-08-18-session-history-and-event-transport.zh.md: 10edeff16695cac265f2026b300eb206838a53e4

+ 6 - 6
.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.md

@@ -70,11 +70,11 @@ In-process `connection.rpc.open` uses the same logical endpoint semantics while
 
 The Gateway-internal `$events` logical stream is the sole generation source for `ConnectionHandle`. It does not depend on whether any business `$on` subscription exists, so connection health does not vary with the number of UI listeners.
 
-The Host event source installs incremental listeners synchronously before returning its first frame. Gateway then sends `{ type: 'ready' }` with a `clientId`; this frame proves that the current generation can receive increments.
+The Host event source installs incremental listeners synchronously before returning its first frame. Gateway then sends `{ type: 'ready', clientId, host: { home } }`; this frame proves that the current generation can receive increments and carries the stable Host path-display fact.
 
-`ConnectionController` waits for `$events` readiness and `host.describe` in parallel. It publishes `connected` only after both complete, so a Session or Workspace baseline cannot be read before Host incremental listeners are ready.
+`ConnectionController` publishes `connected` only after `$events` readiness, so a Session or Workspace baseline cannot be read before Host incremental listeners are ready.
 
-Unexpected normal completion of `$events`, a Host error, a malformed opening frame, or a carrier failure ends the current Connection generation. Connection withdraws `hostDescription`, then re-establishes `$events` and `host.describe` after backoff.
+Unexpected normal completion of `$events`, a Host error, a malformed opening frame, or a carrier failure ends the current Connection generation. Connection withdraws the generation, then re-establishes `$events` after backoff.
 
 Gateway stream generation, Connection generation, and a Session business open epoch are three independent counters: the first identifies physical replacement of one logical stream, the second identifies a Host-availability handshake, and the last prevents an obsolete Session open from writing into current state.
 
@@ -136,7 +136,7 @@ If a page request is canceled with its physical carrier generation, the journal
 
 `packages/api/session-controller` provides Host `ctx.sessionController` and the generated `ctx.remote.session` namespace.
 
-It owns Session list, search, create, selectModel, rename, fork, prompt, attachment, updateQueue, cancel, page, follow, and control. The Host-generation model catalog is exposed separately through `llm.models` because it is not Session-specific.
+It owns Session list, search, create, selectModel, rename, fork, prompt, attachment, updateQueue, cancel, page, follow, and control. The Host-generation model catalog is exposed separately through `session/modelCatalog` because it is not Session-specific.
 
 The package separates agent, commands, control, history, and list controllers internally, but Session identity resolution, activation policy, subagent ownership, and Remote error projection have one public owner.
 
@@ -332,7 +332,7 @@ API Proxy carries only independent business APIs it owns. Session, Workspace, Re
 
 Gateway mux tests pin connection without logical streams, idle residency, configurable Ping/Pong without application messages, initial-failure and disconnect recovery, active-stream carrier failure, cancellation, and no reconnect after disposal.
 
-Connection tests pin missing, duplicate, and withdrawn generation sources; the race between `$events` ready and `host.describe`; and description withdrawal and rebuilding after generation failure.
+Connection tests pin missing, duplicate, and withdrawn generation sources, readiness timeout, and generation withdrawal and rebuilding after failure.
 
 `RemoteStream` tests pin single consumption, retry reset after opening acceptance, generation-only `restart()`, no retry for terminal errors, and disposal quiescence.
 
@@ -378,4 +378,4 @@ Remote waterfalls preserve first claim across multiple Clients, continuation of
 
 This decision extends the allowlist and single Cordis-signature design from [Remote event delivery](2026-08-10-remote-event-delivery.md): ordinary notifications use `emit`, while Agent-scoped async waterfalls use the same `ctx.remote.$on` surface with explicit `waterfall` mode. It creates no second invocation map.
 
-This decision takes over the Session, Workspace, and Host-event carriers retained by [simple unary API Proxy migration](../../proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md) while preserving the complete jobs snapshot, process-local lifecycle, and “observation does not resume an Agent” semantics required by [background job display](../feature/2026-08-08-web-background-job-display.md).
+This decision takes over the Session, Workspace, and Host-event carriers retained by [simple unary API Proxy migration](2026-08-10-unary-apiproxy-remote-migration.md) while preserving the complete jobs snapshot, process-local lifecycle, and “observation does not resume an Agent” semantics required by [background job display](../feature/2026-08-08-web-background-job-display.md).

+ 6 - 6
.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.zh.md

@@ -70,11 +70,11 @@ Host 按配置的 `websocketHeartbeatIntervalMs` 间隔(默认 30 秒)向每
 
 Gateway 内部 `$events` logical stream 是 `ConnectionHandle` 唯一的 generation source。它不依赖是否已有业务 `$on` 订阅,因此连接健康状态不会随 UI listener 数量变化。
 
-Host event source 在返回首帧前同步安装增量 listener。Gateway 随后发送带 `clientId` 的 `{ type: 'ready' }`,该帧证明当前 generation 已经能够接收增量。
+Host event source 在返回首帧前同步安装增量 listener。Gateway 随后发送 `{ type: 'ready', clientId, host: { home } }`;该 frame 证明当前 generation 已经能够接收增量,并携带稳定的 Host 路径显示信息。
 
-`ConnectionController` 并行等待 `$events` ready 与 `host.describe`。两者都完成后才发布 `connected`,所以 Session 或 Workspace baseline 不会在 Host 增量 listener 就绪前开始读取。
+`ConnectionController` 只有在 `$events` ready 后才发布 `connected`,所以 Session 或 Workspace baseline 不会在 Host 增量 listener 就绪前开始读取。
 
-`$events` 正常意外结束、Host 错误、畸形首帧或 carrier 失败都会结束当前 Connection generation。Connection 撤回 `hostDescription`,退避后重新建立 `$events` 与 `host.describe`。
+`$events` 正常意外结束、Host 错误、畸形首帧或 carrier 失败都会结束当前 Connection generation。Connection 撤回该 generation,退避后重新建立 `$events`。
 
 Gateway stream、Connection generation 与 Session 业务 open epoch 是三个独立计数:前者表示某条 logical stream 的物理替换,第二个表示 Host 可用性握手,最后一个防止已淘汰的 Session open 写回当前状态。
 
@@ -136,7 +136,7 @@ repair 期间旧 window 保持可读;page 与期间积累的 live entries 拼
 
 `packages/api/session-controller` 提供 Host `ctx.sessionController` 与生成的 `ctx.remote.session` namespace。
 
-它拥有 Session list、search、create、selectModel、rename、fork、prompt、attachment、updateQueue、cancel、page、follow 与 control。Host generation 的 model catalog 通过独立的 `llm.models` 公开,因为它不属于特定 Session。
+它拥有 Session list、search、create、selectModel、rename、fork、prompt、attachment、updateQueue、cancel、page、follow 与 control。Host generation 的 model catalog 通过独立的 `session/modelCatalog` 公开,因为它不属于特定 Session。
 
 包内的 agent、commands、control、history 与 list controller 分开实现,但 Session 身份解析、激活策略、subagent ownership 和 Remote 错误投影只有一个公开 owner。
 
@@ -332,7 +332,7 @@ API Proxy 只承接自身拥有的独立业务 API,不是 Session、Workspace
 
 Gateway mux 测试固定无 logical stream 时建连、空闲常驻、可配置且不产生应用消息的 Ping/Pong、初始失败与断线重连、活动 stream carrier failure、取消和 dispose 后不再重连。
 
-Connection 测试固定 generation source 缺失、重复注册、撤回、`$events` ready 与 `host.describe` 的竞争,以及 generation 失败后的 description 撤回和重建。
+Connection 测试固定 generation source 缺失、重复注册、撤回、ready 超时,以及 generation 失败后的撤回和重建。
 
 `RemoteStream` 测试固定单 consumer、opening acceptance 后清零 retry、`restart()` 只替换 generation、terminal error 不重试和 dispose quiescence。
 
@@ -378,4 +378,4 @@ Remote waterfall 保留多 Client 首个 claim、全体 `next` 后继续 Host ch
 
 本决定扩展[Remote 事件投递](2026-08-10-remote-event-delivery.zh.md)的 allowlist 与单一 Cordis 签名设计:普通通知继续使用 `emit`,Agent-scoped async waterfall 使用同一 `ctx.remote.$on` 面和显式 `waterfall` mode;不建立第二套 invocation map。
 
-本决定接管[简单一元 API Proxy 迁移](../../proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md)中保留的 Session、Workspace 与 Host event carrier,并保留[后台任务展示](../feature/2026-08-08-web-background-job-display.zh.md)所要求的完整 jobs snapshot、进程内生命周期和“观察不恢复 Agent”语义。
+本决定接管[简单一元 API Proxy 迁移](2026-08-10-unary-apiproxy-remote-migration.zh.md)中保留的 Session、Workspace 与 Host event carrier,并保留[后台任务展示](../feature/2026-08-08-web-background-job-display.zh.md)所要求的完整 jobs snapshot、进程内生命周期和“观察不恢复 Agent”语义。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-24-browser-token-authentication.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-08-24-browser-token-authentication.md
-2026-08-24-browser-token-authentication.md: c75f561d9529296ff668791c29453e522f309cc3
-2026-08-24-browser-token-authentication.zh.md: d4a5059619fefda9d9060e9879d10c0a2197f8f3
+2026-08-24-browser-token-authentication.md: cdfed1f250de6def39e386cf9fcd2e536c6c97e4
+2026-08-24-browser-token-authentication.zh.md: 29f3a6c0a2d83ed1c17b9c0bc5988c9472741f3f

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-24-browser-token-authentication.md

@@ -24,7 +24,7 @@ The shipped CLI continues to reject `--host 0.0.0.0`. Authentication does not im
 
 ## Verification
 
-Unit coverage pins process-token retention across Connection reloads, one secret load per activation, synchronous verification without credential-provider reads, cookie attributes, HMAC and payload validation, authority and lifetime checks, record deletion taking effect on the next activation, invalid durable records, and cleanup of obsolete token URLs backed by valid cookies. Host transport suites pin uniform 401/403 behavior for API Proxy, generic RPC, Typert Remote HTTP, and WebSocket upgrade paths. The frontend real-composition test boots credentials, Connection, webserver, and static serving through Loader and proves token exchange before index reads while static assets remain public. Packed-worker tests prove portable cookie encoding and worker-local retry for both authentication and trust rejection. A real-CLI test starts `dsh web` twice on one port with a temporary `DSH_HOME`, proves that forged `Host: localhost` is unauthenticated, calls `host.describe` with the exchanged cookie, observes a new process token, and reuses the old cookie after restart.
+Unit coverage pins process-token retention across Connection reloads, one secret load per activation, synchronous verification without credential-provider reads, cookie attributes, HMAC and payload validation, authority and lifetime checks, record deletion taking effect on the next activation, invalid durable records, and cleanup of obsolete token URLs backed by valid cookies. Host transport suites pin uniform 401/403 behavior for generic RPC, Typert Remote HTTP, exact Fetch routes, and WebSocket upgrade paths. The frontend real-composition test boots credentials, Connection, webserver, and static serving through Loader and proves token exchange before index reads while static assets remain public. Packed-worker tests prove portable cookie encoding and worker-local retry for both authentication and trust rejection. A real-CLI test starts `dsh web` twice on one port with a temporary `DSH_HOME`, proves that forged `Host: localhost` is unauthenticated, calls `settings/describe` with the exchanged cookie, observes a new process token, and reuses the old cookie after restart.
 
 ## Alternatives considered
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-24-browser-token-authentication.zh.md

@@ -24,7 +24,7 @@ HMAC 密钥是 `ctx.credentials` 中位于 `client-connection/browser-session` 
 
 ## 验证
 
-单元覆盖 Connection 重载时保留进程令牌、每次激活只加载一次密钥、无需读取凭据提供方的同步校验、cookie 属性、HMAC 与 payload 校验、authority 与有效期校验、记录删除在下一次激活时生效、无效持久记录,以及用有效 cookie 清理过时令牌 URL。Host 传输套件固定 API Proxy、通用 RPC、Typert Remote HTTP 和 WebSocket upgrade 路径上一致的 401/403 行为。frontend 真实组合测试经 Loader 启动 credentials、Connection、webserver 与静态服务,证明读取 index 前完成令牌交换,同时静态资产仍公开。打包 worker 测试证明 cookie 编码可移植,并覆盖认证与信任拒绝后的 worker 本地重试。真实 CLI 测试在临时 `DSH_HOME` 上用同一端口两次启动 `dsh web`,证明伪造 `Host: localhost` 仍未认证,以交换所得 cookie 调用 `host.describe`,观测新的进程令牌,并在重启后复用旧 cookie。
+单元覆盖 Connection 重载时保留进程令牌、每次激活只加载一次密钥、无需读取凭据提供方的同步校验、cookie 属性、HMAC 与 payload 校验、authority 与有效期校验、记录删除在下一次激活时生效、无效持久记录,以及用有效 cookie 清理过时令牌 URL。Host 传输套件固定通用 RPC、Typert Remote HTTP、精确 Fetch 路由和 WebSocket upgrade 路径上一致的 401/403 行为。frontend 真实组合测试经 Loader 启动 credentials、Connection、webserver 与静态服务,证明读取 index 前完成令牌交换,同时静态资产仍公开。打包 worker 测试证明 cookie 编码可移植,并覆盖认证与信任拒绝后的 worker 本地重试。真实 CLI 测试在临时 `DSH_HOME` 上用同一端口两次启动 `dsh web`,证明伪造 `Host: localhost` 仍未认证,以交换所得 cookie 调用 `settings/describe`,观测新的进程令牌,并在重启后复用旧 cookie。
 
 ## 曾考虑的替代方案
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-24-cordis-runtime-tree-inspection.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-08-24-cordis-runtime-tree-inspection.md
-2026-08-24-cordis-runtime-tree-inspection.md: 9784018a050bdb27e791a44e1cd342a31cec42c0
-2026-08-24-cordis-runtime-tree-inspection.zh.md: 05b9b0d4387447b7c8df05b7c7e771e96c767bd9
+2026-08-24-cordis-runtime-tree-inspection.md: 2cd63aa41567adc19d52d584874583183aef3cdc
+2026-08-24-cordis-runtime-tree-inspection.zh.md: 88cf2b344d4feef10f3632918539d3cb98750fc9

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-24-cordis-runtime-tree-inspection.md

@@ -42,7 +42,7 @@ The identities are intentionally distinct:
 - `NodeId` is assigned per DevTools connection when a node enters that frontend's document. It remains stable while the corresponding backend node is retained and is discarded when that node leaves the tree, on the rare full-document fallback, or when the connection closes.
 - `RemoteObjectId` is assigned by the selected Runtime session when `DOM.resolveNode` exposes the live object. It remains scoped to that DevTools connection and object group.
 
-`sourceId` identifies one Client runtime instance and remains stable across its automatic transport reconnects; `generation` identifies one WebSocket admission. Disconnect removes the synthetic context from the Console with `Runtime.executionContextDestroyed`. Reconnection announces a fresh CDP execution-context id because the destroyed id and its RemoteObjects cannot be reused, but this does not imply that the browser's underlying JavaScript realm was recreated.
+`sourceId` identifies one browser-tab Client runtime and is retained in that tab's `sessionStorage`, so automatic transport reconnects and page refreshes reuse it. Before opening the transport, a Client with Web Locks claims that id for its page lifetime; a simultaneously live tab copied from the same storage state cannot claim it and persists a fresh id instead. Browsers without Web Locks retain storage-backed refresh identity but cannot arbitrate copied live tabs. `generation` identifies one WebSocket admission and always rotates. Disconnect removes the synthetic context from the Console with `Runtime.executionContextDestroyed`. Reconnection announces a fresh CDP execution-context id because the destroyed id and its RemoteObjects cannot be reused, but this does not imply that the browser's underlying JavaScript realm was recreated.
 
 Standard CDP does not place a `RemoteObjectId` field on `DOM.Node`. `DOM.Node` carries `nodeId` and `backendNodeId`; `DOM.resolveNode` returns the corresponding `Runtime.RemoteObject`, and `DOM.requestNode` performs the reverse mapping. The implementation keeps these three CDP identities correlated without adding non-standard DOM fields.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-24-cordis-runtime-tree-inspection.zh.md

@@ -42,7 +42,7 @@ collector 从 root、注册表中的每个 live Fiber,以及每个 event hook
 - `NodeId` 在节点进入某个 frontend document 时按 DevTools 连接分配;对应 backend node 被保留期间保持稳定,并在节点离开树、少见的整 document fallback 或连接关闭时丢弃。
 - `RemoteObjectId` 在 `DOM.resolveNode` 暴露实时对象时由选定的 Runtime session 分配;它只属于该 DevTools 连接和 object group。
 
-`sourceId` 标识一个 Client runtime instance,并在自动重连 transport 时保持稳定;`generation` 标识一次 WebSocket 接纳。断联通过 `Runtime.executionContextDestroyed` 从 Console 移除 synthetic context。重连会发布新的 CDP execution-context id,因为已销毁的 id 及其 RemoteObject 不能复用;这并不表示浏览器底层 JavaScript realm 被重新创建。
+`sourceId` 标识一个浏览器 tab 的 Client runtime,并保存在该 tab 的 `sessionStorage` 中,因此自动重连 transport 与页面刷新都会复用它。Client 在打开 transport 前会在 Web Locks 可用时独占该 id,直至页面结束;从同一存储状态复制出的另一个同时存活 tab 无法取得该锁,因而会持久化一个新 id。缺少 Web Locks 的浏览器仍保留基于存储的刷新身份,但无法仲裁复制出的 live tab。`generation` 标识一次 WebSocket 接纳且每次都会轮换。断联通过 `Runtime.executionContextDestroyed` 从 Console 移除 synthetic context。重连会发布新的 CDP execution-context id,因为已销毁的 id 及其 RemoteObject 不能复用;这并不表示浏览器底层 JavaScript realm 被重新创建。
 
 标准 CDP 不会在 `DOM.Node` 上放置 `RemoteObjectId` 字段。`DOM.Node` 携带 `nodeId` 与 `backendNodeId`;`DOM.resolveNode` 返回对应的 `Runtime.RemoteObject`,`DOM.requestNode` 执行反向映射。实现会关联这三类 CDP 身份,而不添加非标准 DOM 字段。
 

+ 3 - 3
.agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.i18n.yaml → .agents/notes/implemented/architecture/2026-08-25-rename-code-mode-to-ptc.i18n.yaml

@@ -1,6 +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-07-26-code-dispatch-ui-foundation.md
-2026-07-26-code-dispatch-ui-foundation.md: 2b919dfb978ef3df0e65d2e0e410e2bd07c75a46
-2026-07-26-code-dispatch-ui-foundation.zh.md: 33599c5689973d05ca6ec98fbf919cea6fc1cd4d
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-25-rename-code-mode-to-ptc.md
+2026-08-25-rename-code-mode-to-ptc.md: 9f53b9b5d8c3581d5c2dfe0174ad1c279cf47ba3
+2026-08-25-rename-code-mode-to-ptc.zh.md: 56a9e5ec3ca660fd36d21f9c4dbcb1d5cbd5fbf9

+ 37 - 0
.agents/notes/implemented/architecture/2026-08-25-rename-code-mode-to-ptc.md

@@ -0,0 +1,37 @@
+# Agent Note: Rename code-mode to ptc — the transport is named PTC, user-facing copy says PTC mode
+
+Status: implemented
+
+English | [中文](2026-08-25-rename-code-mode-to-ptc.zh.md)
+
+## Problem
+
+The tool-registry presentation mode that exposes tools through a generated SDK and the `run_code` transport shipped under the name Code Mode, while the client preset that selects it already shipped as "PTC mode" (locale `presetPtcName: 'PTC mode'`, zh `PTC 模式`). One feature had two names: config values, plugin and event names, files, and documentation said `code`/`code-mode`, and the user-facing name said "PTC mode". A pre-release rename must update every reference together — no compatibility aliases.
+
+## Decision
+
+The feature is renamed to PTC (programmatic tool calls). Code identifiers use `ptc` — the transport is not a sibling of plan-mode, so the identifier does not carry `-mode`. User-facing prose keeps "PTC mode" (EN) / "PTC 模式" (zh), matching the shipped preset name.
+
+Renamed in this PR:
+
+- config value `tools.mode: 'code'` → `'ptc'` (`ToolPresentationMode` and the zod unions in `dsh-tools` and `dsh-agent-tool-presentation`)
+- preset directory `presets/code/` → `presets/ptc/` (preset id `ptc`)
+- source and test files `code-mode.ts` → `ptc.ts` and friends; root demo `demo:code-mode` → `demo:ptc` (`scripts/demo-ptc.mjs`)
+- the dispatch waterfall `tools/code-dispatch-log` → `tools/ptc-dispatch-log` and types `CodeDispatch*` → `PtcDispatch*`
+- prompt rule `tools:code-only` → `tools:ptc-only`
+- prose "Code Mode" → "PTC mode" / "PTC 模式" in docs, READMEs, and the eight implemented Agent Notes whose topic names the feature (those files were renamed in place)
+
+Deferred to the stacked persistence PR: the session-persistent vocabulary — the durable event types `tool/code-dispatch` / `tool/code-dispatch-start`, the logged plugin name `tools-code-mode`, and the sub-call id segment `:code:`. That PR is blocked until the `SESSION_FORMAT_VERSION` v0→v1 migration lands with it.
+
+Kept unchanged: `run_code` and its `code` parameter (they name the program payload, not the mode), `CodeSdkLanguage`, `CodeRunFailedError`, the `dsh-code-runtime*` package family, the third-party `codex-code-mode-host` binary name, and every frozen archived note.
+
+## Alternatives considered
+
+- **`ptc-mode` identifiers** — rejected: PTC is a tool-presentation transport, not a mode in the plan-mode sense, and the identifier should not claim that kinship.
+- **Surface-only rename** — rejected: the pre-release stance updates every reference together.
+- **Renaming `run_code` too** — rejected: the tool name describes running a program, not the mode, and is model-facing API surface.
+- **Renaming the durable event vocabulary in this PR** — rejected: renaming `tool/code-dispatch*` without a format bump would make pre-rename session logs unreadable; that rename belongs to the stacked persistence PR that lands together with the v0→v1 migration.
+
+## Consequences
+
+Configs with `mode: code` and preset ids `code` are unsupported on this build. The session-persistent vocabulary still says `tool/code-dispatch*`, `tools-code-mode`, and `:code:`, so existing session logs load unchanged and no `SESSION_FORMAT_VERSION` bump is needed yet. The stacked persistence PR renames that vocabulary and is blocked until the v0→v1 migration lands with it (the version mechanics are the [session-event-vocabulary note](../simplification/2026-08-25-fail-closed-session-event-vocabulary.md)). Keyless snapshot refreshes carry this PR's vocabulary; the persistence PR refreshes the dispatch-bearing fixtures. The shipped decision this note renames is [the PTC foundation note](../feature/2026-06-15-ptc.md).

+ 37 - 0
.agents/notes/implemented/architecture/2026-08-25-rename-code-mode-to-ptc.zh.md

@@ -0,0 +1,37 @@
+# Agent Note: 将 code-mode 重命名为 ptc——传输层命名为 PTC,用户文案使用 PTC mode
+
+Status: implemented
+
+[English](2026-08-25-rename-code-mode-to-ptc.md) | 中文
+
+## 问题
+
+通过生成的 SDK 与 `run_code` 传输层向模型呈现工具的注册表模式,发布时用的名字是 Code Mode;而选择该模式的客户端预设早已以「PTC mode」发布(locale `presetPtcName: 'PTC mode'`,中文 `PTC 模式`)。同一功能有两套名字:配置值、插件与事件名、文件与文档写的是 `code`/`code-mode`,用户可见的名字却是「PTC mode」。预发布阶段的更名必须一次性更新所有引用——不加任何兼容别名。
+
+## 决策
+
+该功能更名为 PTC(programmatic tool calls,程序化工具调用)。代码标识符使用 `ptc`——该传输层并非 plan-mode 的同类模式,因此标识符不带 `-mode`。用户可见文案沿用「PTC mode」(英文)/「PTC 模式」(中文),与已发布的预设名一致。
+
+本 PR 完成的重命名包括:
+
+- 配置值 `tools.mode: 'code'` → `'ptc'`(`ToolPresentationMode` 以及 `dsh-tools`、`dsh-agent-tool-presentation` 中的 zod union)
+- 预设目录 `presets/code/` → `presets/ptc/`(预设 id 为 `ptc`)
+- 源文件与测试文件 `code-mode.ts` → `ptc.ts` 等;根 demo `demo:code-mode` → `demo:ptc`(`scripts/demo-ptc.mjs`)
+- 分发 waterfall `tools/code-dispatch-log` → `tools/ptc-dispatch-log`,类型 `CodeDispatch*` → `PtcDispatch*`
+- 提示词规则 `tools:code-only` → `tools:ptc-only`
+- 文档、README 与八个以该功能命名的 implemented Agent Note 中的文案 "Code Mode" → "PTC mode"/"PTC 模式"(这些 Note 文件一并就地改名)
+
+延后到堆叠的持久化 PR:会话持久词汇——持久事件类型 `tool/code-dispatch`/`tool/code-dispatch-start`、日志中的插件名 `tools-code-mode`、子调用 id 段 `:code:`。该 PR 被阻塞,直到 `SESSION_FORMAT_VERSION` v0→v1 迁移与其一同落地。
+
+保持不变:`run_code` 及其 `code` 参数(它们描述程序载荷,而非模式)、`CodeSdkLanguage`、`CodeRunFailedError`、`dsh-code-runtime*` 包族、第三方二进制名 `codex-code-mode-host`,以及所有冻结的 archived Note。
+
+## 备选方案
+
+- **使用 `ptc-mode` 标识符**——否决:PTC 是工具呈现传输层,不是 plan-mode 意义上的模式,标识符不应宣示这种亲缘关系。
+- **仅重命名表面**——否决:预发布立场要求一次性更新所有引用。
+- **连 `run_code` 一起改名**——否决:该工具名描述的是运行程序,不是模式,而且是对模型可见的 API 表面。
+- **在本 PR 中一并重命名持久事件词汇**——否决:在没有格式版本提升的情况下重命名 `tool/code-dispatch*` 会让更名前的会话日志无法读取;该重命名属于与 v0→v1 迁移一同落地的堆叠持久化 PR。
+
+## 后果
+
+配置中写 `mode: code`、预设 id 为 `code`,在本构建上不再受支持。会话持久词汇仍为 `tool/code-dispatch*`、`tools-code-mode` 与 `:code:`,因此既有会话日志照常读取,无需 `SESSION_FORMAT_VERSION` 提升。堆叠的持久化 PR 负责重命名该词汇,并被阻塞到 v0→v1 迁移与其一同落地(版本机制见 [session event 词汇 Note](../simplification/2026-08-25-fail-closed-session-event-vocabulary.zh.md))。无密钥的 snapshot refresh 携带本 PR 的词汇;持久化 PR 刷新包含分发的夹具。本 Note 所更名的已发布决策是 [PTC 基础 Note](../feature/2026-06-15-ptc.zh.md)。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.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-08-25-session-observations-and-projection-owned-client-state.md
-2026-08-25-session-observations-and-projection-owned-client-state.md: e47f2fc75ecbca51d01af077f6c6ab98f4e275f9
-2026-08-25-session-observations-and-projection-owned-client-state.zh.md: 527a4eb6b6765cba95d6067f2be60bff8f31a559
+2026-08-25-session-observations-and-projection-owned-client-state.md: 492640385215b059761b17a057328cc5c6d24bff
+2026-08-25-session-observations-and-projection-owned-client-state.zh.md: 0b892a9cae2c999b4472dd46f19068e2b4139e60

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.md

@@ -114,7 +114,7 @@ The list view reads the same per-Session store as the opened Session. Hints can
 
 The per-Session Client projection store accepts list hints, the follow baseline, and later whole-value frames under one higher-sequence-wins rule. It never folds Session events. A baseline or frame may advance a hinted value, while an older cut cannot overwrite a newer row.
 
-Data that is not derived from one Session remains outside projections. `llm.models` owns the Host-generation model catalog, and `agentPreset.list` owns the configurable preset roster. A selector combines the relevant catalog with the Session's `modelSelection` or `agentPreset` projection only when both inputs are ready. During refresh it may retain the last complete catalog; before the first complete pair it reports loading instead of rendering a guessed name or availability verdict.
+Data that is not derived from one Session remains outside projections. `session/modelCatalog` owns the Host-generation model catalog, and `agentPresets/list` owns the configurable preset roster. A selector combines the relevant catalog with the Session's `modelSelection` or `agentPreset` projection only when both inputs are ready. During refresh it may retain the last complete catalog; before the first complete pair it reports loading instead of rendering a guessed name or availability verdict.
 
 Client-local interaction state also remains local: loading and error status, an open menu, an in-flight selection, and a staged choice for a not-yet-created Session are not replayable Session facts. Once a choice applies to a Session, its durable event and projection become authoritative.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.zh.md

@@ -114,7 +114,7 @@ List view 与已打开 Session 读取同一个 per-Session store。Hints 可以
 
 每个 Session 的 Client projection store 按一条 higher-sequence-wins 规则接收 list hints、follow baseline 和后续 whole-value frame。它从不折叠 Session event。Baseline 或 frame 可以推进 hinted value,较旧切面不能覆盖较新的 row。
 
-不由单个 Session 派生的数据不进入 projection。`llm.models` 拥有当前 Host generation 的 model catalog,`agentPreset.list` 拥有可配置 preset roster。Selector 只在相应 catalog 与 Session 的 `modelSelection` 或 `agentPreset` projection 均就绪后组合两者。刷新时可以保留上一份完整 catalog;第一次获得完整输入前显示 loading,而不是展示猜测的名称或可用性结论。
+不由单个 Session 派生的数据不进入 projection。`session/modelCatalog` 持有当前 Host generation 的 model catalog,`agentPresets/list` 持有可配置 preset roster。Selector 只在相应 catalog 与 Session 的 `modelSelection` 或 `agentPreset` projection 均就绪后组合两者。刷新时可以保留上一份完整 catalog;第一次获得完整输入前显示 loading,而不是展示猜测的名称或可用性结论。
 
 Client 本地交互状态也继续留在本地:loading 和 error 状态、打开的菜单、进行中的选择,以及为尚未创建 Session 暂存的选择都不是可回放 Session 事实。选择一旦应用到 Session,其持久事件与 projection 就成为权威。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.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-08-25-sparse-first-party-prompt-section-orders.md
-2026-08-25-sparse-first-party-prompt-section-orders.md: 2bf2e7441b449a6cfbd5b845f1d7e97b3fab09ae
-2026-08-25-sparse-first-party-prompt-section-orders.zh.md: 624ed51f4d40848c72091097900ef05ec35fdd2e
+2026-08-25-sparse-first-party-prompt-section-orders.md: 4b2568f18d104a77d00f91bb98f64047ecd85fa9
+2026-08-25-sparse-first-party-prompt-section-orders.zh.md: 4dfb0d0bd87ff5f245c28f3dbab6a48ba898f924

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.md

@@ -22,7 +22,7 @@ The allocation preserves the established first-party sequence except for two del
 |---|---|
 | Product opening | `harness:identity` −1000, `harness:source` −900, `app:web-surface` −800, `deployment:persona` 0 |
 | Work modes | `plan:policy` 500, `team:policy` 600 |
-| Invocation prelude | `tools:code-only` 800, `context:file-reference` 900 |
+| Invocation prelude | `tools:ptc-only` 800, `context:file-reference` 900 |
 | Local tools | `tool:bash` 1000, `tool:pwsh` 1010, `tool:read` 1100, `tool:write` 1200, `tool:edit` 1300, `tool:glob` 1400, `tool:grep` 1500, `tool:jobs` 1600, `tool:pty` 1700 |
 | Higher-level tools | `tool:web_search` 2000, `tool:web_fetch` 2100, `tool:lsp` 2200, `tool:session-query` 2300, `tool:goal` 2400, `tool:cordis` 2500, `tool:workflow` 2600, `tool:ralph` 2700, continuable-subagent guidance 2800, `tool:report` 2900 |
 | Generated protocol | `tools:sdk` 5000 |

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.zh.md

@@ -22,7 +22,7 @@ Status: implemented
 |---|---|
 | 产品开场 | `harness:identity` −1000、`harness:source` −900、`app:web-surface` −800、`deployment:persona` 0 |
 | 工作模式 | `plan:policy` 500、`team:policy` 600 |
-| 调用前置说明 | `tools:code-only` 800、`context:file-reference` 900 |
+| 调用前置说明 | `tools:ptc-only` 800、`context:file-reference` 900 |
 | 本地工具 | `tool:bash` 1000、`tool:pwsh` 1010、`tool:read` 1100、`tool:write` 1200、`tool:edit` 1300、`tool:glob` 1400、`tool:grep` 1500、`tool:jobs` 1600、`tool:pty` 1700 |
 | 高层工具 | `tool:web_search` 2000、`tool:web_fetch` 2100、`tool:lsp` 2200、`tool:session-query` 2300、`tool:goal` 2400、`tool:cordis` 2500、`tool:workflow` 2600、`tool:ralph` 2700、可继续运行的 subagent 指导 2800、`tool:report` 2900 |
 | 生成协议 | `tools:sdk` 5000 |

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-27-inspector-development-mount.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-08-27-inspector-development-mount.md
-2026-08-27-inspector-development-mount.md: 0e5f0af52306ebb553e4fb911696cfc3fae087ee
-2026-08-27-inspector-development-mount.zh.md: 252e52692df2ad983e6da9ee9640c5ae6603f20f
+2026-08-27-inspector-development-mount.md: a8660c664f6fba49369af075bc998171a454a842
+2026-08-27-inspector-development-mount.zh.md: 3dd82cc89e4c172c3ff9a4f8dc4cafd47c9fd847

+ 4 - 8
.agents/notes/implemented/architecture/2026-08-27-inspector-development-mount.md

@@ -10,21 +10,17 @@ English | [中文](2026-08-27-inspector-development-mount.zh.md)
 
 ## Decision
 
-The inspector package owns a development overlay, `packages/experimental/inspector/cordis.patch.yml`, holding a single `insert` of the `experimental-inspector` row. A launch selects it through the generic overlay flag; `pnpm run demo:inspector` is the shorthand for `pnpm dsh web --patch ./packages/experimental/inspector/cordis.patch.yml`.
+The inspector package owns two development overlays. `packages/experimental/inspector/cordis.source.patch.yml` inserts `./src/index.ts` for the tsx source launch behind `pnpm run demo:inspector`. `packages/experimental/inspector/cordis.patch.yml` inserts `./lib/index.js` for `node apps/cli/lib/bin.js web --patch ./packages/experimental/inspector/cordis.patch.yml` after `pnpm run build`.
 
-The overlay contributes only the row; the row's module resolves from the profile plane at entry import:
-
-- A source launch (`pnpm dsh`, tsx) resolves the workspace package through the tsconfig `paths` facade and needs no installation.
-- A built launch (`node apps/cli/lib/bin.js`) needs the package importable from the profile first: `dsh plugin --profile web add link:<absolute package path>`, once per profile. `link:` keeps dependency resolution inside the real package directory; `file:` re-installs the package's `workspace:^` dependencies in the profile and fails with `ERR_PNPM_WORKSPACE_PKG_NOT_FOUND`.
-
-A launch whose profile cannot import the package fails loud at entry import (`Cannot find package '@deepseek-ai/dsh-experimental-inspector' imported from <profile dir>`); nothing is skipped silently.
+Each relative entry resolves from its overlay file's directory through the Loader's normal owning-tree `baseUrl`. The source launch therefore reaches TypeScript directly, while the built launch reaches the package artifact; neither path reads or modifies profile-installed plugin state. A missing source or built entry fails loud during Loader import rather than skipping the Inspector.
 
 ## Consequences
 
-Published packages carry no trace of the inspector: no manifest entry, no composition row, no launcher flag. Mounting stays a per-launch choice — the same service without the overlay never loads the package — and every layer the launch composes is declared in a config file. The cost is launch-mode asymmetry: a built launch needs the one-time profile `link:` install, and the overlay must be named on every invocation, which `pnpm run demo:inspector` absorbs for the common case.
+Published packages carry no trace of the inspector: no manifest entry, no composition row, no launcher flag. Mounting stays a per-launch choice — the same service without an overlay never loads the package — and every layer the launch composes is declared in a config file. The source shorthand names its overlay automatically; a built launch names the built overlay explicitly and requires current `lib/` artifacts.
 
 ## Alternatives considered
 
 - A `disabled: !!js` row in the shipped web-app patch: the dependency gate and npm publication both force the private package into the published manifest.
 - A `--inspector` launcher flag mounting the package as an extra bundle layer: the launcher owns neither app flags nor plugin package names.
 - An optional `peerDependencies` entry on `dsh-web-app` plus a dynamic `ctx.loader.create` from its glue plugin: it writes a never-published name into a published manifest and mounts a row no config layer declares.
+- One bare-package overlay for both launch modes: source resolution can use the workspace facade, but built resolution would require persistent profile installation state unrelated to the launch command.

+ 4 - 8
.agents/notes/implemented/architecture/2026-08-27-inspector-development-mount.zh.md

@@ -10,21 +10,17 @@ Status: implemented
 
 ## Decision
 
-inspector 包自有一份开发 overlay,`packages/experimental/inspector/cordis.patch.yml`,只含一个 `insert` 的 `experimental-inspector` 行。启动通过通用 overlay flag 选它;`pnpm run demo:inspector` 是 `pnpm dsh web --patch ./packages/experimental/inspector/cordis.patch.yml` 的简写。
+inspector 包自有两份开发 overlay。`packages/experimental/inspector/cordis.source.patch.yml` 为 `pnpm run demo:inspector` 背后的 tsx 源码启动插入 `./src/index.ts`;`packages/experimental/inspector/cordis.patch.yml` 为执行过 `pnpm run build` 后的 `node apps/cli/lib/bin.js web --patch ./packages/experimental/inspector/cordis.patch.yml` 插入 `./lib/index.js`。
 
-overlay 只贡献这一行;行的模块在 entry import 时从 profile 平面解析:
-
-- 源码启动(`pnpm dsh`,tsx)经 tsconfig `paths` 门面解析 workspace 包,无需任何安装。
-- built 启动(`node apps/cli/lib/bin.js`)需先让包可从 profile import:`dsh plugin --profile web add link:<包目录绝对路径>`,每个 profile 一次。`link:` 让依赖解析留在真实包目录内;`file:` 会在 profile 里重装该包的 `workspace:^` 依赖并以 `ERR_PNPM_WORKSPACE_PKG_NOT_FOUND` 失败。
-
-profile 无法 import 该包的启动会在 entry import 处响亮失败(`Cannot find package '@deepseek-ai/dsh-experimental-inspector' imported from <profile dir>`);不存在静默跳过。
+两个相对 entry 都通过 Loader 常规的所属 tree `baseUrl` 从各自 overlay 文件目录解析。源码启动因此直接读取 TypeScript,built 启动读取包产物;两条路径都不读取或修改 profile 已安装插件状态。源码或 built entry 缺失时,Loader import 会响亮失败,不会跳过 Inspector。
 
 ## Consequences
 
-已发布的包不携带 inspector 的任何痕迹:没有 manifest 条目、没有组合行、没有 launcher flag。挂载保持按次启动选择——不带 overlay 的同一服务永远不会加载该包——且启动组合的每一层都由 config 文件声明。代价是启动方式不对称:built 启动需要一次性 profile `link:` 安装,且每次调用都要点名 overlay,常见场景由 `pnpm run demo:inspector` 吸收。
+已发布的包不携带 inspector 的任何痕迹:没有 manifest 条目、没有组合行、没有 launcher flag。挂载保持按次启动选择——不带 overlay 的同一服务永远不会加载该包——且启动组合的每一层都由 config 文件声明。源码快捷命令会自动指定对应 overlay;built 启动需显式指定 built overlay,并要求 `lib/` 产物为当前版本。
 
 ## Alternatives considered
 
 - 随货 web-app patch 里放 `disabled: !!js` 行:依赖门禁与 npm 发布都会把 private 包逼进已发布 manifest。
 - `--inspector` launcher flag 把包挂成额外 bundle 层:launcher 既不拥有 app flag 也不拥有插件包名。
 - `dsh-web-app` 上加 optional `peerDependencies` 并由其 glue 插件动态 `ctx.loader.create`:向已发布 manifest 写入永不发布的名字,且挂载的行不在任何 config 层声明。
+- 两种启动模式共用一份 bare-package overlay:源码解析可以使用 workspace 门面,但 built 解析会依赖与本次启动命令无关的持久 profile 安装状态。

+ 0 - 46
.agents/notes/implemented/bug-fix/2026-08-07-code-mode-executor-collapse.md

@@ -1,46 +0,0 @@
-# Agent Note: Code Mode collapses the executor, not just the wire
-
-Status: implemented
-
-English | [中文](2026-08-07-code-mode-executor-collapse.zh.md)
-
-## Problem
-
-`mode: 'code'` collapsed only the announcement surface, not the execution surface. `wireSchemas()` sent the model exactly one tool — `run_code` — but the executor resolved every call through `get()`, which returns the full visible map plus the reserved transport. A model that emitted a native tool name (`write`, `read`, `bash`, `subagent`, …) bypassed `run_code` entirely: the call traversed the normal pipeline and executed, even though no schema for it had ever been advertised. Providers do not intercept unadvertised tool names, so schema omission enforced nothing.
-
-The package contract names this exact anti-pattern: schema omission is not enforcement when a direct caller can bypass it; denial must be tested through the executor.
-
-## Decision
-
-`ToolRuntime` resolves callable definitions through a new private `resolveExecution(name, scope, nested)` that applies the mode collapse at the operation boundary that owns it. When `modeFor(scope)` resolves to `code`, a model-direct call (`nested = false`) may only name the reserved `run_code` transport; every native name resolves to `undefined` and surfaces as the executor's existing `UNKNOWN_TOOL` error, whose message names the route back through `run_code` because the name IS declared to this model (an already-aborted caller signal keeps the cancellation contract: `ABORTED_BEFORE_DISPATCH`, with the visible tool's finalizer applied). The effective scope mode includes declarations inherited from an agent preset, so its wire schema and execution permissions remain aligned. A collapsed call terminates at `createExecution` — the first stage of `prepare` — BEFORE the extensible policy pipeline, so `tools/pre-execute` listeners, approval `ask`, and guards never observe a call that is deterministically denied; a human is never prompted to approve it. A nested sub-dispatch (`nested = true` — a `parent` token set, which only the `run_code` SDK binding sets in production code) may call any visible tool, so programs keep every binding the generated SDK declared.
-
-Four execution-path lookups — `executionMode`, `dispatchToolBody`, `postExecute`, `normalizeDispatchResult` — go through `resolveExecution`. `createExecution` applies the same collapse via the shared `collapses(name, nested)` predicate so it can distinguish a collapsed call from a genuinely unknown name before the policy pipeline. The public registry view (`get`) and SDK projection (`schemas`) keep their semantics: presentation, inspection, and binding enumeration still see the full visible set. The wire (`wireSchemas`) and the executor now agree. A collapsed call with non-JSON-serializable arguments reports the parameter `TypeError` (the invalid-args contract), not `UNKNOWN_TOOL` — the body still never runs and policy still does not.
-
-The collapse is a security-relevant invariant, so acceptance is pinned through the executor: a model-direct native call under `code` returns `UNKNOWN_TOOL`, the same tool via an SDK sub-dispatch succeeds, and `native`/`both` direct calls plus `run_code` itself are unchanged. The base [Code Mode foundation](../feature/2026-06-15-code-mode.md) owns the transport design this note layers the execution boundary onto.
-
-## Alternatives considered
-
-### Filter `get()` / the registry view by mode
-
-The view is consumed by presenters, `tool-cordis` inspection, and the SDK binder; collapsing it would hide from the program surface tools that must still bind, and would change the public resolution contract for every consumer, not just the executor.
-
-### Filter at the agent-loop entry
-
-The loop is not the only executor caller, and the distinction that matters (model-direct vs transport sub-dispatch) rides on the execution input, not at the loop boundary. An entry filter would also re-encode mode semantics the registry already owns.
-
-### Reject via a shipped guard
-
-Guards are an optional plugin extension; a security invariant must not depend on a deployment composing the right plugin. The registry owns the mode decision and must enforce it itself.
-
-### Keep schema omission only (status quo)
-
-No provider guarantees interception of unadvertised names; the reported session proves it does not happen.
-
-## Consequences
-
-- `mode: 'code'` now enforces what it announces: a model-direct native call becomes `UNKNOWN_TOOL`, which the model can correct by routing through `run_code` (a pre-aborted call still resolves `ABORTED_BEFORE_DISPATCH`, per the cancellation contract).
-- `both` and `native` behavior is unchanged; SDK sub-dispatches are unchanged (the `parent` token is the discriminator).
-- A collapsed call is rejected at `prepare`, BEFORE the extensible policy pipeline: pre-execute listeners, approval `ask`, and guards never observe it. `executionMode` also fails closed (`exclusive`), so scheduling has no observable difference.
-- Native-tool guidance sections (`tool:read`, `tool:write`, `tool:bash`, etc.) remain in the system prompt because they describe capabilities available through the generated SDK as well as native function calls, and several carry cross-tool routing policy (`read` over `bash cat`, `read` before `write` for the default fs-observation-policy, `subagent` over `workflow`) that no single tool description can hold. The executor collapse, not prompt filtering, prevents model-direct native calls.
-- The prompt STATES the collapse, in the `tools:code-only` section ordered ahead of first-party per-tool guidance. Those sections name their tool without qualifying how it is reached, so a model that read only them emitted a native call, received `UNKNOWN_TOOL` for a tool the same prompt declared, and concluded the deployment was inconsistent rather than correcting itself. The denial carries the route for the same reason. The TypeScript SDK section repeats the distinction next to the generated declarations, labels them as program-only bindings, and states that only separately supplied tool schemas grant direct-call availability. Because the declaration list can otherwise be read as native tool availability, the section emits a complete `run_code` call around `tools.bash(...)` when the current `bash` parameter schema accepts the example arguments. `both` renders the rule empty: its native calls do execute, so stating it there would be false — which is why `both-mode-turn` no longer shares `code-mode-turn`'s expected prompt.
-- Any future composite transport that sets a `parent` token opts its sub-dispatches into the full table, matching the nested-call semantics the token already documents.

+ 0 - 46
.agents/notes/implemented/bug-fix/2026-08-07-code-mode-executor-collapse.zh.md

@@ -1,46 +0,0 @@
-# Agent Note: Code Mode 塌缩执行器而非仅通告面
-
-Status: implemented
-
-[English](2026-08-07-code-mode-executor-collapse.md) | 中文
-
-## 问题
-
-`mode: 'code'` 只塌缩了通告面,没有塌缩执行面。`wireSchemas()` 只向模型发送一个工具——`run_code`——但执行器通过 `get()` 解析所有调用,而 `get()` 返回完整的可见工具表外加保留的传输工具。模型一旦发出原生工具名(`write`、`read`、`bash`、`subagent` 等),就能完全绕过 `run_code`:调用照常走完整流水线并执行成功,尽管它的 schema 从未被通告过。模型提供方不拦截未通告的工具名,因此不发 schema 等于没有约束。
-
-包契约点名了这个反模式:当直接调用方可以绕过时,schema 省略不算强制执行;拒绝必须经执行器验证。
-
-## 决策
-
-`ToolRuntime` 通过新增的私有 `resolveExecution(name, scope, nested)` 解析可执行定义,在拥有该决策的操作边界上应用模式塌缩。当 `modeFor(scope)` 解析为 `code` 时,模型直呼(`nested = false`)只允许命名保留的 `run_code` 传输工具;任何原生名字都解析为 `undefined`,并以执行器既有的 `UNKNOWN_TOOL` 错误呈现,其消息会指出改走 `run_code` 的正确路径——因为这个名字对当前模型而言是**已声明过**的(已中止的调用方 signal 保留取消契约:`ABORTED_BEFORE_DISPATCH`,并应用可见工具的 finalizer)。有效的 scope 模式包括从 agent preset 继承的声明,因此其 wire schema 与执行权限保持一致。被塌缩的调用在 `createExecution`(`prepare` 的第一阶段)即终止——在可扩展策略流水线之前,因此 `tools/pre-execute` 监听器、approval `ask` 与 guard 永远不会观察到一个注定被拒绝的调用,人类也不会被提示去批准它。嵌套子调用(`nested = true`——即设置了 `parent` token,生产代码中只有 `run_code` SDK 绑定会设置)可以调用任意可见工具,因此程序保留生成 SDK 声明的全部绑定。
-
-执行链路的四处查表——`executionMode`、`dispatchToolBody`、`postExecute`、`normalizeDispatchResult`——改走 `resolveExecution`。`createExecution` 通过共享的 `collapses(name, nested)` 谓词应用同一塌缩,以便在策略流水线之前区分被塌缩的调用与真正未知的名字。公共注册表视图(`get`)与 SDK 投影(`schemas`)语义不变:展示、检查与绑定枚举仍看到完整可见集合。通告(`wireSchemas`)与执行器现在一致。带非 JSON 可序列化参数的塌缩调用报告参数 `TypeError`(invalid-args 契约),而非 `UNKNOWN_TOOL`——函数体仍不会运行,策略也不会执行。
-
-塌缩是安全相关的不变量,因此验收经执行器钉死:`code` 模式下模型直呼原生工具返回 `UNKNOWN_TOOL`;同一工具经 SDK 子调用成功;`native`/`both` 模式直呼与 `run_code` 本身行为不变。本 note 把执行边界叠加在基础 [Code Mode 基础](../feature/2026-06-15-code-mode.zh.md) 之上,传输设计由后者拥有。
-
-## 备选方案
-
-### 按模式过滤 `get()` / 注册表视图
-
-视图被展示方、`tool-cordis` 检查与 SDK 绑定消费;塌缩视图会从程序表面隐藏仍必须绑定的工具,并改变所有消费者的公共解析契约,而不只是执行器。
-
-### 在 agent-loop 入口过滤
-
-loop 不是唯一的执行器调用方,且真正要紧的区分(模型直呼 vs 传输子调用)挂在执行输入上,不在 loop 边界。入口过滤还会重复编码注册表已经拥有的模式语义。
-
-### 通过内置 guard 拒绝
-
-guard 是可选的插件扩展;安全不变量不能依赖部署恰好组装了正确的插件。模式决策归注册表所有,必须由它自己执行。
-
-### 只保留 schema 省略(维持现状)
-
-没有提供方保证拦截未通告的名字;被报告的会话证明拦截不会发生。
-
-## 后果
-
-- `mode: 'code'` 现在兑现其通告:模型直呼原生工具变为 `UNKNOWN_TOOL`,模型可以通过改走 `run_code` 自行纠正(已中止的调用仍按取消契约解析为 `ABORTED_BEFORE_DISPATCH`)。
-- `both` 与 `native` 行为不变;SDK 子调用不变(判别信号是 `parent` token)。
-- 被塌缩的调用在 `prepare` 阶段即被拒绝——在可扩展策略流水线之前:pre-execute 监听器、approval `ask` 与 guard 永远不会观察到它。`executionMode` 同样 fail-closed(`exclusive`),调度无可观察差异。
-- 原生工具指引段(`tool:read`、`tool:write`、`tool:bash` 等)保留在系统提示词中,因为它们同时描述了通过生成 SDK 及原生函数调用可用的能力,其中若干段还承载着任何单个工具描述都装不下的跨工具路由策略(`read` 优先于 `bash cat`、默认 fs-observation-policy 要求先 `read` 再 `write`、一两个委派用 `subagent` 而非 `workflow`)。防止模型直呼原生工具的是执行器塌缩,而非提示词过滤。
-- 提示词会**声明**这条塌缩,位于 first-party 逐工具指导之前的 `tools:code-only` 段。那些段只写出工具名而不限定其可达方式,因此只读到它们的模型会发出原生调用,为一个同一份提示词刚刚声明过的工具收到 `UNKNOWN_TOOL`,进而判定部署不一致,而不是自行纠正。拒绝信息给出正确路径也是同一原因。TypeScript SDK 段在生成声明旁再次区分两者,将其标为只能在程序内使用的绑定,并说明只有单独提供的工具 schema 才赋予直呼权限。声明列表可能被误读为原生工具可用性,因此当当前 `bash` 参数 schema 接受示例参数时,该段会给出以 `run_code` 包住 `tools.bash(...)` 的完整调用。`both` 下该规则渲染为空:它的原生调用确实会执行,在那里声明就是假话——这也是 `both-mode-turn` 不再与 `code-mode-turn` 共用期望提示词的原因。
-- 未来任何设置 `parent` token 的组合传输,其子调用自动走全表,与该 token 已有的嵌套调用语义一致。

+ 3 - 3
.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.i18n.yaml → .agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.i18n.yaml

@@ -1,6 +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-07-26-code-dispatch-log-spill.md
-2026-07-26-code-dispatch-log-spill.md: a4b8deee86b1f6102e48e34079a93a74dfcfa288
-2026-07-26-code-dispatch-log-spill.zh.md: 5e53d761367ddce7747109e49c7facea9bcdae49
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.md
+2026-08-07-ptc-executor-collapse.md: e034d30ff54673ecef98faf5c632b92c2d39aa2f
+2026-08-07-ptc-executor-collapse.zh.md: 03f93a323533b8507043c2fa8653b4c51c5fd56e

+ 46 - 0
.agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.md

@@ -0,0 +1,46 @@
+# Agent Note: PTC mode collapses the executor, not just the wire
+
+Status: implemented
+
+English | [中文](2026-08-07-ptc-executor-collapse.zh.md)
+
+## Problem
+
+`mode: 'ptc'` collapsed only the announcement surface, not the execution surface. `wireSchemas()` sent the model exactly one tool — `run_code` — but the executor resolved every call through `get()`, which returns the full visible map plus the reserved transport. A model that emitted a native tool name (`write`, `read`, `bash`, `subagent`, …) bypassed `run_code` entirely: the call traversed the normal pipeline and executed, even though no schema for it had ever been advertised. Providers do not intercept unadvertised tool names, so schema omission enforced nothing.
+
+The package contract names this exact anti-pattern: schema omission is not enforcement when a direct caller can bypass it; denial must be tested through the executor.
+
+## Decision
+
+`ToolRuntime` resolves callable definitions through a new private `resolveExecution(name, scope, nested)` that applies the mode collapse at the operation boundary that owns it. When `modeFor(scope)` resolves to `ptc`, a model-direct call (`nested = false`) may only name the reserved `run_code` transport; every native name resolves to `undefined` and surfaces as the executor's existing `UNKNOWN_TOOL` error, whose message names the route back through `run_code` because the name IS declared to this model (an already-aborted caller signal keeps the cancellation contract: `ABORTED_BEFORE_DISPATCH`, with the visible tool's finalizer applied). The effective scope mode includes declarations inherited from an agent preset, so its wire schema and execution permissions remain aligned. A collapsed call terminates at `createExecution` — the first stage of `prepare` — BEFORE the extensible policy pipeline, so `tools/pre-execute` listeners, approval `ask`, and guards never observe a call that is deterministically denied; a human is never prompted to approve it. A nested sub-dispatch (`nested = true` — a `parent` token set, which only the `run_code` SDK binding sets in production code) may call any visible tool, so programs keep every binding the generated SDK declared.
+
+Four execution-path lookups — `executionMode`, `dispatchToolBody`, `postExecute`, `normalizeDispatchResult` — go through `resolveExecution`. `createExecution` applies the same collapse via the shared `collapses(name, nested)` predicate so it can distinguish a collapsed call from a genuinely unknown name before the policy pipeline. The public registry view (`get`) and SDK projection (`schemas`) keep their semantics: presentation, inspection, and binding enumeration still see the full visible set. The wire (`wireSchemas`) and the executor now agree. A collapsed call with non-JSON-serializable arguments reports the parameter `TypeError` (the invalid-args contract), not `UNKNOWN_TOOL` — the body still never runs and policy still does not.
+
+The collapse is a security-relevant invariant, so acceptance is pinned through the executor: a model-direct native call under `ptc` returns `UNKNOWN_TOOL`, the same tool via an SDK sub-dispatch succeeds, and `native`/`both` direct calls plus `run_code` itself are unchanged. The base [PTC mode foundation](../feature/2026-06-15-ptc.md) owns the transport design this note layers the execution boundary onto.
+
+## Alternatives considered
+
+### Filter `get()` / the registry view by mode
+
+The view is consumed by presenters, `tool-cordis` inspection, and the SDK binder; collapsing it would hide from the program surface tools that must still bind, and would change the public resolution contract for every consumer, not just the executor.
+
+### Filter at the agent-loop entry
+
+The loop is not the only executor caller, and the distinction that matters (model-direct vs transport sub-dispatch) rides on the execution input, not at the loop boundary. An entry filter would also re-enable PTC mode semantics the registry already owns.
+
+### Reject via a shipped guard
+
+Guards are an optional plugin extension; a security invariant must not depend on a deployment composing the right plugin. The registry owns the mode decision and must enforce it itself.
+
+### Keep schema omission only (status quo)
+
+No provider guarantees interception of unadvertised names; the reported session proves it does not happen.
+
+## Consequences
+
+- `mode: 'ptc'` now enforces what it announces: a model-direct native call becomes `UNKNOWN_TOOL`, which the model can correct by routing through `run_code` (a pre-aborted call still resolves `ABORTED_BEFORE_DISPATCH`, per the cancellation contract).
+- `both` and `native` behavior is unchanged; SDK sub-dispatches are unchanged (the `parent` token is the discriminator).
+- A collapsed call is rejected at `prepare`, BEFORE the extensible policy pipeline: pre-execute listeners, approval `ask`, and guards never observe it. `executionMode` also fails closed (`exclusive`), so scheduling has no observable difference.
+- Native-tool guidance sections (`tool:read`, `tool:write`, `tool:bash`, etc.) remain in the system prompt because they describe capabilities available through the generated SDK as well as native function calls, and several carry cross-tool routing policy (`read` over `bash cat`, `read` before `write` for the default fs-observation-policy, `subagent` over `workflow`) that no single tool description can hold. The executor collapse, not prompt filtering, prevents model-direct native calls.
+- The prompt STATES the collapse, in the `tools:ptc-only` section ordered ahead of first-party per-tool guidance. Those sections name their tool without qualifying how it is reached, so a model that read only them emitted a native call, received `UNKNOWN_TOOL` for a tool the same prompt declared, and concluded the deployment was inconsistent rather than correcting itself. The denial carries the route for the same reason. The TypeScript SDK section repeats the distinction next to the generated declarations, labels them as program-only bindings, and states that only separately supplied tool schemas grant direct-call availability. Because the declaration list can otherwise be read as native tool availability, the section emits a complete `run_code` call around `tools.bash(...)` when the current `bash` parameter schema accepts the example arguments. `both` renders the rule empty: its native calls do execute, so stating it there would be false — which is why `both-mode-turn` no longer shares `ptc-turn`'s expected prompt.
+- Any future composite transport that sets a `parent` token opts its sub-dispatches into the full table, matching the nested-call semantics the token already documents.

+ 46 - 0
.agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.zh.md

@@ -0,0 +1,46 @@
+# Agent Note: PTC mode 塌缩执行器而非仅通告面
+
+Status: implemented
+
+[English](2026-08-07-ptc-executor-collapse.md) | 中文
+
+## 问题
+
+`mode: 'ptc'` 只塌缩了通告面,没有塌缩执行面。`wireSchemas()` 只向模型发送一个工具——`run_code`——但执行器通过 `get()` 解析所有调用,而 `get()` 返回完整的可见工具表外加保留的传输工具。模型一旦发出原生工具名(`write`、`read`、`bash`、`subagent` 等),就能完全绕过 `run_code`:调用照常走完整流水线并执行成功,尽管它的 schema 从未被通告过。模型提供方不拦截未通告的工具名,因此不发 schema 等于没有约束。
+
+包契约点名了这个反模式:当直接调用方可以绕过时,schema 省略不算强制执行;拒绝必须经执行器验证。
+
+## 决策
+
+`ToolRuntime` 通过新增的私有 `resolveExecution(name, scope, nested)` 解析可执行定义,在拥有该决策的操作边界上应用模式塌缩。当 `modeFor(scope)` 解析为 `ptc` 时,模型直呼(`nested = false`)只允许命名保留的 `run_code` 传输工具;任何原生名字都解析为 `undefined`,并以执行器既有的 `UNKNOWN_TOOL` 错误呈现,其消息会指出改走 `run_code` 的正确路径——因为这个名字对当前模型而言是**已声明过**的(已中止的调用方 signal 保留取消契约:`ABORTED_BEFORE_DISPATCH`,并应用可见工具的 finalizer)。有效的 scope 模式包括从 agent preset 继承的声明,因此其 wire schema 与执行权限保持一致。被塌缩的调用在 `createExecution`(`prepare` 的第一阶段)即终止——在可扩展策略流水线之前,因此 `tools/pre-execute` 监听器、approval `ask` 与 guard 永远不会观察到一个注定被拒绝的调用,人类也不会被提示去批准它。嵌套子调用(`nested = true`——即设置了 `parent` token,生产代码中只有 `run_code` SDK 绑定会设置)可以调用任意可见工具,因此程序保留生成 SDK 声明的全部绑定。
+
+执行链路的四处查表——`executionMode`、`dispatchToolBody`、`postExecute`、`normalizeDispatchResult`——改走 `resolveExecution`。`createExecution` 通过共享的 `collapses(name, nested)` 谓词应用同一塌缩,以便在策略流水线之前区分被塌缩的调用与真正未知的名字。公共注册表视图(`get`)与 SDK 投影(`schemas`)语义不变:展示、检查与绑定枚举仍看到完整可见集合。通告(`wireSchemas`)与执行器现在一致。带非 JSON 可序列化参数的塌缩调用报告参数 `TypeError`(invalid-args 契约),而非 `UNKNOWN_TOOL`——函数体仍不会运行,策略也不会执行。
+
+塌缩是安全相关的不变量,因此验收经执行器钉死:`ptc` 模式下模型直呼原生工具返回 `UNKNOWN_TOOL`;同一工具经 SDK 子调用成功;`native`/`both` 模式直呼与 `run_code` 本身行为不变。本 note 把执行边界叠加在基础 [PTC mode 基础](../feature/2026-06-15-ptc.zh.md) 之上,传输设计由后者拥有。
+
+## 备选方案
+
+### 按模式过滤 `get()` / 注册表视图
+
+视图被展示方、`tool-cordis` 检查与 SDK 绑定消费;塌缩视图会从程序表面隐藏仍必须绑定的工具,并改变所有消费者的公共解析契约,而不只是执行器。
+
+### 在 agent-loop 入口过滤
+
+loop 不是唯一的执行器调用方,且真正要紧的区分(模型直呼 vs 传输子调用)挂在执行输入上,不在 loop 边界。入口过滤还会重复编码注册表已经拥有的模式语义。
+
+### 通过内置 guard 拒绝
+
+guard 是可选的插件扩展;安全不变量不能依赖部署恰好组装了正确的插件。模式决策归注册表所有,必须由它自己执行。
+
+### 只保留 schema 省略(维持现状)
+
+没有提供方保证拦截未通告的名字;被报告的会话证明拦截不会发生。
+
+## 后果
+
+- `mode: 'ptc'` 现在兑现其通告:模型直呼原生工具变为 `UNKNOWN_TOOL`,模型可以通过改走 `run_code` 自行纠正(已中止的调用仍按取消契约解析为 `ABORTED_BEFORE_DISPATCH`)。
+- `both` 与 `native` 行为不变;SDK 子调用不变(判别信号是 `parent` token)。
+- 被塌缩的调用在 `prepare` 阶段即被拒绝——在可扩展策略流水线之前:pre-execute 监听器、approval `ask` 与 guard 永远不会观察到它。`executionMode` 同样 fail-closed(`exclusive`),调度无可观察差异。
+- 原生工具指引段(`tool:read`、`tool:write`、`tool:bash` 等)保留在系统提示词中,因为它们同时描述了通过生成 SDK 及原生函数调用可用的能力,其中若干段还承载着任何单个工具描述都装不下的跨工具路由策略(`read` 优先于 `bash cat`、默认 fs-observation-policy 要求先 `read` 再 `write`、一两个委派用 `subagent` 而非 `workflow`)。防止模型直呼原生工具的是执行器塌缩,而非提示词过滤。
+- 提示词会**声明**这条塌缩,位于 first-party 逐工具指导之前的 `tools:ptc-only` 段。那些段只写出工具名而不限定其可达方式,因此只读到它们的模型会发出原生调用,为一个同一份提示词刚刚声明过的工具收到 `UNKNOWN_TOOL`,进而判定部署不一致,而不是自行纠正。拒绝信息给出正确路径也是同一原因。TypeScript SDK 段在生成声明旁再次区分两者,将其标为只能在程序内使用的绑定,并说明只有单独提供的工具 schema 才赋予直呼权限。声明列表可能被误读为原生工具可用性,因此当当前 `bash` 参数 schema 接受示例参数时,该段会给出以 `run_code` 包住 `tools.bash(...)` 的完整调用。`both` 下该规则渲染为空:它的原生调用确实会执行,在那里声明就是假话——这也是 `both-mode-turn` 不再与 `ptc-turn` 共用期望提示词的原因。
+- 未来任何设置 `parent` token 的组合传输,其子调用自动走全表,与该 token 已有的嵌套调用语义一致。

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.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/bug-fix/2026-08-12-onboarding-reads-every-provider.md
-2026-08-12-onboarding-reads-every-provider.md: 1f247a6c93257c24052f55eb4297ec3c9c3df06d
-2026-08-12-onboarding-reads-every-provider.zh.md: fc6e43195a46eaea881f8b4bee3219b5e583b284
+2026-08-12-onboarding-reads-every-provider.md: 43895d6bc317f13ece91a48f9b44c0e8da705dc8
+2026-08-12-onboarding-reads-every-provider.zh.md: 67f12dd9e90435f61776d0eac048d588fcdd7252

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.md

@@ -24,7 +24,7 @@ Each card kind now owns its own close handler. `closeSetup` records the provider
 
 ## Alternatives considered
 
-- **Deriving readiness from the model catalog (`llm.models`) instead of the join.** It answers "can the user talk to something" most directly, but it costs a per-provider listing round trip on a surface that already holds the join, and a provider whose listing fails transiently would re-open onboarding.
+- **Deriving readiness from the model catalog (`session/modelCatalog`) instead of the join.** It answers "can the user talk to something" most directly, but it costs a per-provider listing round trip on a surface that already holds the join, and a provider whose listing fails transiently would re-open onboarding.
 - **Requiring `row.configured` in `providerUsable`.** It reads as the stricter check, and would exclude exactly the routes a deployment mounts through `cordis.yml` without a configurable-provider declaration — live routes serving models that this page cannot configure. Registration, not configurability, is what makes a provider usable.
 - **Only adding the dismissal, leaving the card auto-opening.** It fixes the Cancel button and nothing else: a user with a working provider would still be handed the DeepSeek form on every visit to Models, which is the same misreading in a quieter form.
 - **Persisting the dismissal to settings.** A durable "do not ask about DeepSeek" flag is a second fact about first-run state that can disagree with the join. The credential itself already ends the posture permanently, and every other card on this page is session-local.

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.zh.md

@@ -24,7 +24,7 @@ Status: implemented
 
 ## Alternatives considered
 
-- **从模型目录(`llm.models`)而非联接推导就绪状态。** 它最直接地回答「用户有没有能对话的东西」,但会在一个已经持有联接的界面上多花每提供方一次列举往返,而且某个提供方列举的瞬时失败会让引导重新弹出。
+- **从模型目录(`session/modelCatalog`)而非联接推导就绪状态。** 它最直接地回答「用户有没有能对话的东西」,但会在一个已经持有联接的界面上多花每提供方一次列举往返,而且某个提供方列举的瞬时失败会让引导重新弹出。
 - **在 `providerUsable` 中要求 `row.configured`。** 它读起来更严格,却会恰好排除部署通过 `cordis.yml` 挂载、没有可配置提供方声明的那些路由——它们是正在提供模型、只是这个页面配置不了的存活路由。使一个提供方可用的是注册,不是可配置性。
 - **只加关闭状态,保留卡片自动展开。** 那只修好取消按钮,别的什么都没修:已有可用提供方的用户每次进入 Models 仍会被塞一张 DeepSeek 表单,那是同一个误读的安静版本。
 - **把关闭状态持久化到 settings。** 一个「别再问 DeepSeek」的持久标志,是关于首次运行状态的第二个事实,可能与联接互相矛盾。凭据本身已经永久结束该姿态,而这个页面上其他每一张卡片都是会话内的。

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.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/bug-fix/2026-08-13-bounded-cold-blank-verification.md
-2026-08-13-bounded-cold-blank-verification.md: bf8d167d742001ce65b3e96713a9603adb19603e
-2026-08-13-bounded-cold-blank-verification.zh.md: 1418301b8cb3c1452cbed2bdaf974949126079e6
+2026-08-13-bounded-cold-blank-verification.md: cd50d29f0b5d417077d5d848415607885c39474a
+2026-08-13-bounded-cold-blank-verification.zh.md: 851b1fb35126a42623e251bf790dde2029189b36

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md

@@ -12,7 +12,7 @@ The same cold list used the JSONL artifact mtime for `updatedAt`. Opening a Sess
 
 ## Decision
 
-`dsh-host-apiproxy` registers `sessionListMetadata`, a projection containing `blank` and `lastPromptAt`. The attached summary folds the same functions directly over the live log. `blank` changes only from true to false on `turn/start`; `lastPromptAt` changes only on a `user/message` whose source kind is `user`.
+`dsh-api-session-controller` registers `sessionListMetadata`, a projection containing `blank` and `lastPromptAt`. The attached summary folds the same functions directly over the live log. `blank` changes only from true to false on `turn/start`; `lastPromptAt` changes only on a `user/message` whose source kind is `user`.
 
 A cold summary trusts cached `blank: false`, because a checkpoint prefix containing `turn/start` remains non-blank. Cached `blank: true` and a cache miss do not prove the current log is blank. When persistence exposes a physical artifact through `locate()` and its observed size is at most the `coldBlankProbeMaxBytes` eligibility threshold (default 1 KiB per Session), the gateway calls `readFrom(id, 0)` and folds exact list metadata from the stored prefix. Files above the threshold, backends without a location, vanished artifacts, and failed reads all produce `blank: false`, keeping the Session visible.
 

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md

@@ -12,7 +12,7 @@ Web 会话树会隐藏空白 Session,并把当前选中的空白项复用为 N
 
 ## Decision
 
-`dsh-host-apiproxy` 注册 `sessionListMetadata` 投影,其中包含 `blank` 与 `lastPromptAt`。已附加摘要直接用同一组函数折叠实时日志。`blank` 只在 `turn/start` 时从 true 单调变为 false;`lastPromptAt` 只在来源 kind 为 `user` 的 `user/message` 上更新。
+`dsh-api-session-controller` 注册 `sessionListMetadata` 投影,其中包含 `blank` 与 `lastPromptAt`。已附加摘要直接用同一组函数折叠实时日志。`blank` 只在 `turn/start` 时从 true 单调变为 false;`lastPromptAt` 只在来源 kind 为 `user` 的 `user/message` 上更新。
 
 冷摘要信任缓存的 `blank: false`,因为已包含 `turn/start` 的 checkpoint 前缀会始终保持非空。缓存的 `blank: true` 和 cache miss 都无法证明当前日志为空。当 persistence 通过 `locate()` 暴露物理工件,且其观测大小不超过 `coldBlankProbeMaxBytes` 资格阈值(默认每个 Session 1 KiB)时,网关调用 `readFrom(id, 0)`,从已存前缀折叠精确列表元数据。超过阈值的文件、不提供位置的后端、已消失的工件和读取失败都产生 `blank: false`,让 Session 保持可见。
 

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-18-tool-row-file-open-failure.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/bug-fix/2026-08-18-tool-row-file-open-failure.md
-2026-08-18-tool-row-file-open-failure.md: e36552395b992e688fad35b3163b92c9f6189e43
-2026-08-18-tool-row-file-open-failure.zh.md: f09c41585a538b408f6258f64583f63c0fa57b0d
+2026-08-18-tool-row-file-open-failure.md: c1887c834933199d5acf9035632cc83d470777d7
+2026-08-18-tool-row-file-open-failure.zh.md: 8a851b4b1bd40c66de92316967df612f828c8b38

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor