Selaa lähdekoodia

Merge master into PR 4317 and adapt Subagent configuration page

Dudu-0223 2 päivää sitten
vanhempi
sitoutus
fae11e6dc1
100 muutettua tiedostoa jossa 1584 lisäystä ja 355 poistoa
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
  2. 2 0
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
  3. 2 0
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml
  5. 18 14
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md
  6. 18 14
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md
  7. 2 2
      .agents/notes/implemented/architecture/2026-08-20-client-session-conversation-ownership.i18n.yaml
  8. 28 27
      .agents/notes/implemented/architecture/2026-08-20-client-session-conversation-ownership.md
  9. 28 27
      .agents/notes/implemented/architecture/2026-08-20-client-session-conversation-ownership.zh.md
  10. 2 2
      .agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml
  11. 2 2
      .agents/notes/implemented/architecture/2026-09-08-global-main-panels.md
  12. 2 2
      .agents/notes/implemented/architecture/2026-09-08-global-main-panels.zh.md
  13. 2 2
      .agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.i18n.yaml
  14. 4 2
      .agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.md
  15. 4 2
      .agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.zh.md
  16. 2 2
      .agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.i18n.yaml
  17. 4 0
      .agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.md
  18. 4 0
      .agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.zh.md
  19. 2 2
      .agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.i18n.yaml
  20. 1 1
      .agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.md
  21. 1 1
      .agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.zh.md
  22. 2 2
      .agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.i18n.yaml
  23. 2 0
      .agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.md
  24. 2 0
      .agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.zh.md
  25. 2 2
      .agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.i18n.yaml
  26. 1 1
      .agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.md
  27. 1 1
      .agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.zh.md
  28. 6 0
      .agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.i18n.yaml
  29. 29 0
      .agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md
  30. 29 0
      .agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md
  31. 6 0
      .agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.i18n.yaml
  32. 37 0
      .agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.md
  33. 37 0
      .agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.zh.md
  34. 6 0
      .agents/notes/implemented/architecture/2026-09-15-client-session-references.i18n.yaml
  35. 226 0
      .agents/notes/implemented/architecture/2026-09-15-client-session-references.md
  36. 226 0
      .agents/notes/implemented/architecture/2026-09-15-client-session-references.zh.md
  37. 2 2
      .agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.i18n.yaml
  38. 4 2
      .agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.md
  39. 4 2
      .agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.zh.md
  40. 3 3
      .agents/notes/implemented/architecture/2026-09-15-platform-office-engines.i18n.yaml
  41. 31 0
      .agents/notes/implemented/architecture/2026-09-15-platform-office-engines.md
  42. 31 0
      .agents/notes/implemented/architecture/2026-09-15-platform-office-engines.zh.md
  43. 6 0
      .agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.i18n.yaml
  44. 31 0
      .agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.md
  45. 31 0
      .agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.zh.md
  46. 6 0
      .agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.i18n.yaml
  47. 39 0
      .agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.md
  48. 39 0
      .agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.zh.md
  49. 6 0
      .agents/notes/implemented/architecture/2026-09-16-user-terminal-permissions.i18n.yaml
  50. 29 0
      .agents/notes/implemented/architecture/2026-09-16-user-terminal-permissions.md
  51. 29 0
      .agents/notes/implemented/architecture/2026-09-16-user-terminal-permissions.zh.md
  52. 6 0
      .agents/notes/implemented/bug-fix/2026-09-16-desktop-window-menus.i18n.yaml
  53. 35 0
      .agents/notes/implemented/bug-fix/2026-09-16-desktop-window-menus.md
  54. 35 0
      .agents/notes/implemented/bug-fix/2026-09-16-desktop-window-menus.zh.md
  55. 6 0
      .agents/notes/implemented/bug-fix/2026-09-16-messages-historical-tool-input.i18n.yaml
  56. 29 0
      .agents/notes/implemented/bug-fix/2026-09-16-messages-historical-tool-input.md
  57. 29 0
      .agents/notes/implemented/bug-fix/2026-09-16-messages-historical-tool-input.zh.md
  58. 6 0
      .agents/notes/implemented/bug-fix/2026-09-16-session-writer-held-feedback.i18n.yaml
  59. 25 0
      .agents/notes/implemented/bug-fix/2026-09-16-session-writer-held-feedback.md
  60. 25 0
      .agents/notes/implemented/bug-fix/2026-09-16-session-writer-held-feedback.zh.md
  61. 6 0
      .agents/notes/implemented/bug-fix/2026-09-16-windows-subprocess-console-visibility.i18n.yaml
  62. 29 0
      .agents/notes/implemented/bug-fix/2026-09-16-windows-subprocess-console-visibility.md
  63. 29 0
      .agents/notes/implemented/bug-fix/2026-09-16-windows-subprocess-console-visibility.zh.md
  64. 6 0
      .agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.i18n.yaml
  65. 31 0
      .agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.md
  66. 31 0
      .agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.zh.md
  67. 2 2
      .agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml
  68. 3 3
      .agents/notes/implemented/feature/2026-07-06-sandbox.md
  69. 3 3
      .agents/notes/implemented/feature/2026-07-06-sandbox.zh.md
  70. 2 2
      .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.i18n.yaml
  71. 9 64
      .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
  72. 13 68
      .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md
  73. 2 2
      .agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.i18n.yaml
  74. 1 1
      .agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.md
  75. 1 1
      .agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.zh.md
  76. 2 2
      .agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.i18n.yaml
  77. 1 1
      .agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md
  78. 1 1
      .agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md
  79. 2 2
      .agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.i18n.yaml
  80. 3 2
      .agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.md
  81. 3 2
      .agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.zh.md
  82. 2 2
      .agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.i18n.yaml
  83. 1 1
      .agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.md
  84. 1 1
      .agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.zh.md
  85. 2 2
      .agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.i18n.yaml
  86. 2 2
      .agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.md
  87. 2 2
      .agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.zh.md
  88. 2 2
      .agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.i18n.yaml
  89. 2 2
      .agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md
  90. 2 2
      .agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.zh.md
  91. 6 0
      .agents/notes/implemented/feature/2026-09-11-turn-changed-files-card.i18n.yaml
  92. 53 0
      .agents/notes/implemented/feature/2026-09-11-turn-changed-files-card.md
  93. 53 0
      .agents/notes/implemented/feature/2026-09-11-turn-changed-files-card.zh.md
  94. 2 2
      .agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.i18n.yaml
  95. 3 3
      .agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.md
  96. 3 3
      .agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.zh.md
  97. 0 27
      .agents/notes/implemented/feature/2026-09-14-model-image-input-settings.md
  98. 0 27
      .agents/notes/implemented/feature/2026-09-14-model-image-input-settings.zh.md
  99. 6 0
      .agents/notes/implemented/feature/2026-09-15-bundled-office-skills.i18n.yaml
  100. 31 0
      .agents/notes/implemented/feature/2026-09-15-bundled-office-skills.md

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

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
-2026-07-10-single-file-executable-sdk-runtime-distribution.md: 665364e6d39a78f7ac497198f18ed9aa160a4cbd
-2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 8b39df71c615dc59f3ef1bf622a5736a70b085b3
+2026-07-10-single-file-executable-sdk-runtime-distribution.md: 56c9770966647e97763e0eaf339e4589fba673fb
+2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 04351105a56304020f5696cf1ce82d7a83ed2ced

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 2 - 0
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 2 - 0
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md


+ 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: 620102f9f5e7fd76f38bf0031b856b26c2a7840d
-2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 3f00658c672908ba92627416ba5d5db381c0d9cd
+2026-07-25-web-client-session-scope-and-provide-channel.md: 6126d06e48111a551a0dc57247659a3086986c66
+2026-07-25-web-client-session-scope-and-provide-channel.zh.md: e7bf4aa6952b4fd7b187c91e2ac5d64aac32cabb

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

@@ -19,6 +19,8 @@ Hard constraints: the host is the single source of truth; every registration goe
 
 ## Decision
 
+The reference-owned lifetime and Provider targeting now follow [Client Session references](2026-09-15-client-session-references.md). This note retains the blank-Session and adoption rationale and describes their current realization.
+
 ### The parity model: client and host share one root state axis
 
 Host-side `session.create(workspaceId)` produces Session + Agent + cwd in one piece (an atomic bundle, never split); the client side is the mirror of that birth — the instant a session row enters the list mirror, the client mints its Agent scope (actx + provide + the full input surface mounted):
@@ -48,13 +50,13 @@ id→ctx handoff is allowed in only three kinds of places (business providers ne
 - Root coordination services self-addressing: from a projection's sessionId back to the actx via `sessions.scope(id)`.
 - Root untagged listeners: looking up their own store by the payload's sessionId.
 
-### Scope lifecycle: anchored to the list mirror — birth is entering view, death is prune
+### Scope lifecycle: anchored to explicit references
 
-Session instances share the scope's lifecycle; liveness eligibility = host-listed (one criterion, shared by mint and prune):
+Session instances share the scope's lifecycle, while the catalog reports discoverability without retaining a generation:
 
-- Birth = a session row entering client view (the list baseline pull / the local `create()` echo / the `host/session-added` frame); a lazy first resolve mints the scope (resolution is a pure function, render-safe).
-- One prune tears down three things together: the Session instance, the scope fiber (cascading through every consumer hung on the actx), and the session-keyed slot store. The staged session (= `list.current`) is the exception: removed while still on stage, it keeps a frozen read-only view, torn down only once the stage moves away.
-- Reopening = lazily rebuilding the instance + `open()` pulling history (the host session log is the durable truth).
+- Birth = the first explicit `sessions.retain(target, options)`; it synchronously returns a reference and mints the Session binding and scope before history is ready.
+- Final release withdraws the exact generation before tearing down its Session instance, scope fiber (cascading through every consumer hung on the actx), and session-keyed slot store. Catalog removal does not end a generation while references remain.
+- Reopening = a later retain lazily rebuilding the generation and exposing history readiness through `reference.ready` (the Host Session log is the durable truth).
 - Remaining TODO: approval/question frames never enter history and cannot be recovered across a prune (the manager-level pendingBuffers cover only the never-instantiated window).
 
 ### The blank bit: the empty session's visible projection, conversion, and reuse
@@ -67,7 +69,7 @@ A session "materialized but with no first prompt" is governed by the summary-der
   - The sender's own tab: the **successful response** to the first `prompt()` flips false (acceptance proves the user/message is already in the host log — this flip is confirmation, not optimism; `onEngaged` synchronously updates the list mirror, converting the current `New Session` row in place to an ordinary title, adding no list row). A rejected first prompt keeps the session blank: aligned with host authority, still shown as `New Session`, keeping its connectWorkspace reuse eligibility while it remains a Workspace member.
   - Other tabs: the `host/session-status (running:true)` frame flips it — a blank session never runs, so the first running necessarily means no longer blank;
   - Reconnect alignment: `session.list`'s summary.blank is authoritative, so a tab that missed frames aligns naturally on its next pull; a stale blank:true can never mark a converted session back to blank.
-- List discipline: the store retains every row; the Workspace browser's grouping, flat view, search, and counts share one visible projection — every non-blank session shows, while blank sessions show only the one with `session.id === sessions.current`, its title forced to `New Session`. After a Workspace switch, the old blank entity stays in the mirror but is hidden from the list while the target Workspace's current blank shows; the user-visible surface therefore holds at most one blank row globally.
+- List discipline: the store retains every row; the Workspace browser's grouping, flat view, search, and counts share one visible projection — every non-blank session shows, while blank sessions show only the row retained by the `mainView` source, with its title forced to `New Session`. After a Workspace switch, the old blank entity stays in the mirror but is hidden from the list while the target Workspace's main blank shows; the user-visible surface therefore holds at most one blank row globally.
 - The residue ledger takes zero GC: after a refresh, blank sessions come back with the bit intact and are reused on the next same-workspace connect while they remain members, so the ordinary single-tab path keeps at most one per workspace; after a host restart, blanks leave no disk trace and simply evaporate; the extra empty shells from multi-tab races only become non-current hidden rows, digested by later reuse, with no coordination.
 
 ### connectWorkspace: the sole entry point of New Session
@@ -77,23 +79,25 @@ A session "materialized but with no first prompt" is governed by the summary-der
 - The reuse arm: the list mirror is searched for `blank && cwd == workspace.path && sessionIds.includes(id)` — the host's own membership rule, never cwd alone. A cwd match without the account slot (a CLI/TUI session birthed at the host cwd, or a deleted/recreated registration) would open a session no grouping surface can show under this Workspace, so it falls through to the create arm instead (see the [membership reuse fix](../../archived/bug-fix/2026-08-05-workspace-blank-session-reuse-membership.md)); a hit returns that id directly, creating nothing.
 - The create arm: on a miss, `session.create({workspaceId})` returns the new id.
 - An unknown workspaceId fails loud (never silently creating somewhere else).
-- The resolution guarantee (one contract for both arms): when the promise resolves, the returned id is already in the list store and `sessions.binding(id)` resolves synchronously — `SessionRuntime.create` projects the list synchronously after RPC success before resolving, so a draft mover can write text into the new scope's machine before open, without waiting for a notifier flush.
-- The caller takes the id and does its own `sessions.open`; sending the first prompt is an ordinary `session.prompt` — the session already exists, a failure is an ordinary prompt failure, the draft text is still in the machine, and a retry is simply sending again.
-- The global New Session button defaults to `recentWorkspaceId`: first comparing each Workspace's newest Session `updatedAt`, falling back to the Workspace `createdAt` when it has no Sessions, and keeping host order on ties; only with no Workspace at all does it `sessions.clear()` into the no-session view. Create actions inside a Workspace group still hit that Workspace explicitly.
+- The resolution guarantee (one contract for both arms): when the promise resolves, the returned id is already in the list store. The view owner then retains it synchronously, so a draft mover can write text through that binding before history readiness without waiting for a notifier flush.
+- The caller takes the id and installs a `mainView` reference; sending the first prompt is an ordinary `session.prompt` — the Session already exists, a failure is an ordinary prompt failure, the draft text is still in the machine, and a retry is simply sending again.
+- The global New Session button defaults to `recentWorkspaceId`: first comparing each Workspace's newest Session `updatedAt`, falling back to the Workspace `createdAt` when it has no Sessions, and keeping Host order on ties; only with no Workspace at all does it clear the main-view reference into the no-Session view. Create actions inside a Workspace group still hit that Workspace explicitly.
 - At startup the runtime subscribes to the first complete baseline: a successfully restored current session is kept in place; otherwise it automatically calls `connectWorkspace(recentWorkspaceId)` and opens the returned blank session. The policy settles only once; a later user-initiated clear is never overridden by auto-selection again, and a connect failure waits for the next baseline projection to retry.
-- Re-picking the Workspace in the blank Hero also goes through `connectWorkspace`; when the target id differs from the current one, the current input machine's non-empty draft moves to the target scope first, then `sessions.open(nextId)`. The old blank entity is not deleted — it merely leaves the list by no longer being current.
+- Re-picking the Workspace in the blank Hero also goes through `connectWorkspace`; when the target id differs from the main one, `ui-workspace` retains the target, moves the current input machine's non-empty draft through the preparation callback, and then publishes the new main reference. The old blank entity is not deleted — it merely leaves the list when its `mainView` reference is released.
 
-### Per-session provisioning: the `sessions.provide` standard-kit channel
+### Per-session provisioning: the `uiSession.provide` standard-kit channel
 
-The sole provisioning path by which session slot components fetch their own session data. Plugins declare a fixed key map through the static descriptor `sessions.provide({hooks, props, resolve})` (a duplicate key throws at registration); `resolve(binding)` materializes values for a specific session and tears them down with the scope. ui-renderer's `standardKit` single loop binds the hooks compartment into `use<Name>` selector hooks (`observableHook`→uSES, anti-tearing) and passes the props compartment through as-is.
+The sole provisioning path by which Session slot components fetch their own Session data. Plugins declare a fixed key map through the static descriptor `uiSession.provide({hooks, props, resolve})` (a duplicate key throws at registration); `resolve(binding)` materializes values for a specific binding and tears them down with its scope. ui-renderer's `standardKit` single loop binds the hooks compartment into `use<Name>` selector hooks (`observableHook`→uSES, anti-tearing) and passes the props compartment through as-is.
 
 Slot scope is the closed set `root | session-maybe | session`:
 
 - `root` receives only the global standard kit, with no session identity or provisioning.
-- `session-maybe` follows the current session with ADOPTION identity (the only behavior — there is no hold-identity-forever mode): an incarnation born session-less keeps its React instance across the arrival of the FIRST session (the blank shell adopts it — no remount, the DOM survives), and from then on behaves exactly like a strict session entry — switching to a different session remounts, and dropping back to no-session remounts into a fresh blank incarnation that will adopt again. Component-local per-session state therefore clears by construction; state that must survive a switch belongs in session-bound sources (machine, store, hooks). With no session, `sessionId`, the results of `useSession`/`useInput`, and `inputActions` may all be absent. The unkeyed root `SessionMaybeProvider` drives these updates by subscribing to the runtime's atomic `currentProvide` projection — selection moves and provider-roster changes publish through the same source, so a roster change under a stable current id republishes the mounted bundle instead of stranding entries on an obsolete hook/prop schema — while `SessionMaybeProvideInfo` uses the static key map to retain the complete hook/prop shape even with no session; the per-entry adoption bookkeeping (incarnation-counter key) lives in the renderer's `SessionMaybeEntry`.
+- `session-maybe` inherits the nearest `SessionProvider` binding with ADOPTION identity: an incarnation born Session-less keeps its React instance when that Provider receives its first binding, then remounts when the Provider switches generation or returns to absence. Component-local per-Session state clears when the Provider switches generation. Across a switch, only persisted Store values survive generation retirement; binding-owned sources survive only when another reference keeps that generation alive. With no binding, `sessionId`, the results of `useSession`/`useInput`, and `inputActions` may all be absent. Provider-roster changes rematerialize the mounted binding without changing its identity, while the per-entry adoption bookkeeping lives in the renderer's `SessionMaybeEntry`.
 - `session` guarantees that `sessionId`, every hook source, and every prop exist; each strict entry's error boundary is keyed by `sessionId`, so switching sessions recreates that entry and its session store.
 
-`conversation` is the resident `session-maybe` shell: `ConversationRoot`, HeroShell, the Workspace picker, the root-owned scrollport and composer stack, and the overlay chain's fallback frame retain their React instances across the no-session → blank-session switch. Two strict entries fill fixed regions without reparenting that tree: `conversation.session.header` carries breadcrumb/tabs/actions above the scrollport, while `conversation.session` carries the view ring and draft mirror inside it; both share the same session-scoped chat store. The composer bar (`conversation.composer.bar`) is itself `session-maybe`: with no session its machine faces and message actions are inert, while the whole dashed card opens the existing Workspace picker by pointer and its read-only textarea does the same through Enter or Space. The same instance — textarea included — goes live when a session appears; the remaining input slots stay strict `session` and dispatch nothing until then. The blank → engaging/active transition never rebuilds the InputBar on a phase flip.
+`conversation` is the resident `session-maybe` shell under its owning `SessionProvider`: `ConversationRoot`, HeroShell, the Workspace picker, the scrollport and composer stack, and the overlay chain's fallback frame retain their React instances across the no-Session → blank-Session switch. Two strict entries fill fixed regions without reparenting that tree: `conversation.session.header` carries breadcrumb/tabs/actions above the scrollport, while `conversation.session` carries the view ring and draft mirror inside it; both share the same Session-scoped chat store. The composer bar (`conversation.composer.bar`) is itself `session-maybe`: with no Session its machine faces and message actions are inert, while the whole dashed card opens the existing Workspace picker by pointer and its read-only textarea does the same through Enter or Space. The same instance — textarea included — goes live when a binding appears; the remaining input slots stay strict `session` and dispatch nothing until then. The blank → engaging/active transition never rebuilds the InputBar on a phase flip.
+
+Blank Sessions retain the header's leading and corner slots so navigation controls, including the right-sidebar opener, are available before the first message. Title, actions, utilities, and View tabs remain hidden in the blank phase. The header still requires a selected Session; the Files and Terminal entries use that Session's workspace and execution services without requiring a recorded Turn.
 
 - The runtime's first built-in entry: the `'session'` hook — `useSession` itself rides the same mechanism, no special-casing.
 - Concurrent discipline: the render plane reads only from the hooks compartment (uSES consistency guarantee); props-compartment callbacks are used only in event-handler space; descriptor resolution is render-safe (idempotent caching, with prune reaping residue from abandoned renders).

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

@@ -19,6 +19,8 @@ web client 只有一张全局会话面:slot 全部从根上下文渲染,插
 
 ## 决策
 
+[Client Session 引用](2026-09-15-client-session-references.zh.md)现已定义引用所有的生命周期与 Provider 定位。本 Note 保留 blank Session 与收养语义的理由,并描述它们的当前实现。
+
 ### 对等模型:client 与 host 同一根状态轴
 
 host 侧 `session.create(workspaceId)` 一体产出 Session + Agent + cwd(作为不可拆分的原子整体);client 侧就是这次出生的镜像——会话行进入 list mirror 的瞬间,client 为它铸 Agent scope(actx + provide + 输入面全套挂上):
@@ -48,13 +50,13 @@ id→ctx 换乘只许三类位置(业务提供方永不换乘):
 - root 协调服务自寻址:从投影的 sessionId 经 `sessions.scope(id)` 找回 actx。
 - root untagged listener:按 payload 的 sessionId 查自有 store。
 
-### scope 生命周期:挂靠 list mirror,出生即视野、死亡即 prune
+### scope 生命周期:挂靠显式引用
 
-Session 实例与 scope 同生命周期,存活资格 = host listed(一个判据,mint 与 prune 共用)
+Session 实例与 scope 同生命周期;catalog 只报告可发现性,不持有 generation
 
-- 出生 = 会话行进入 client 视野(list 基线拉取 / `create()` 本地回声 / `host/session-added` 帧),lazy 首次 resolve 铸 scope(resolution 纯函数、渲染安全)
-- prune 一次同拆三样:Session 实例、scope fiber(级联挂在 actx 上的一切消费方)、会话键控 slot store。暂存会话(= `list.current`)例外:被移除仍在台上时保留冻结只读视图,stage 移走才拆
-- 重开 = lazy 重建实例 + `open()` 拉 history(host 会话日志是持久真相)。
+- 出生 = 第一次显式调用 `sessions.retain(target, options)`;它同步返回 reference,并在历史就绪前铸造 Session binding 与 scope
+- 最后一份 reference 释放时,Controller 先撤下确切 generation,再拆除其 Session 实例、scope fiber(级联挂在 actx 上的一切消费方)与会话键控 slot store。仍有 reference 时,catalog 移除不会结束 generation
+- 重开 = 后续 retain 惰性重建 generation,并通过 `reference.ready` 暴露历史就绪结果(Host Session 日志是持久真相)。
 - 遗留 TODO:approval/question 帧不进 history,跨 prune 不可恢复(manager 级 pendingBuffers 只覆盖「从未实例化」窗口)。
 
 ### blank 位:空会话的可见投影、转正与复用
@@ -67,7 +69,7 @@ Session 实例与 scope 同生命周期,存活资格 = host listed(一个判
   - 发送方本地:首次 `prompt()` 的**成功响应**翻 false(受理即证明用户消息已入 host 日志——此点翻转是确证而非乐观;`onEngaged` 同步更新列表镜像,当前 `New Session` 行原地转为普通标题,不新增列表行)。首条提示词被拒则会话保持 blank:与 host 权威对齐、继续显示为 `New Session`、在仍为该工作区成员时保持 connectWorkspace 复用资格。
   - 其他端:`host/session-status (running:true)` 帧翻转——blank 会话从不 running,首次 running 必然已非 blank;
   - 重连对齐:`session.list` 的 summary.blank 是权威,错过帧的端下次拉取自然对齐;陈旧的 blank:true 不能把已转正的会话重新标回 blank。
-- 列表纪律:store 保留全部行;Workspace browser 的分组、平铺、搜索和计数共用同一可见投影——所有非 blank 会话都显示,blank 会话只显示 `session.id === sessions.current` 的一条,并强制标题为 `New Session`。切换 Workspace 后,旧 blank 实体仍在镜像中但从列表隐藏,目标 Workspace 的 current blank 显示;因此用户可见面全局至多一条 blank 行。
+- 列表纪律:store 保留全部行;Workspace browser 的分组、平铺、搜索和计数共用同一可见投影——所有非 blank 会话都显示,blank 会话只显示由 `mainView` 来源持有的一行,并强制标题为 `New Session`。切换 Workspace 后,旧 blank 实体仍在镜像中但从列表隐藏,目标 Workspace 的 blank 显示;因此用户可见面全局至多一条 blank 行。
 - 残留账零 GC:刷新后 blank 会话带位回来,下次同 workspace 且仍为成员时复用,普通单端路径使每个 workspace 至多保留一个;host 重启后 blank 无盘痕自然蒸发;多 tab 竞态多出的空壳只会成为非 current 隐藏行,后续复用消化,不做协调。
 
 ### connectWorkspace:New Session 的唯一入口
@@ -77,23 +79,25 @@ Session 实例与 scope 同生命周期,存活资格 = host listed(一个判
 - 复用臂:list mirror 中找 `blank && cwd == workspace.path && sessionIds.includes(id)`——host 自己的成员规则,绝不只按 cwd。没有账户槽位的 cwd 匹配(CLI(命令行界面)/TUI 在 host cwd 创建的会话,或已删除/重建的注册)会打开一个任何分组表面都无法显示在该工作区下的会话,因此落到新建臂(见[成员复用修复](../../archived/bug-fix/2026-08-05-workspace-blank-session-reuse-membership.md));命中直接返回该 id,不新建。
 - 新建臂:未命中则 `session.create({workspaceId})`,返回新 id。
 - 未知 workspaceId fail loud(不静默创建到别处)。
-- 解析保证(两臂同约定):promise resolve 时返回的 id 已在 list store 且 `sessions.binding(id)` 同步可解析——`SessionRuntime.create` 在 RPC 成功后同步投影列表再 resolve,使 draft 搬运方可以在 open 之前往新 scope 的 machine 写文本,不等 notifier flush。
-- 调用方拿 id 自行 `sessions.open`;首条提示词发送就是普通 `session.prompt`——会话本来就在,失败即普通提示词失败,draft 文本还在 machine 里,重试即再次发送。
-- 全局 New Session 按钮默认取 `recentWorkspaceId`:先比较各 Workspace 内 Session 的最新 `updatedAt`,无 Session 时回退 Workspace `createdAt`,同值保持 Host 顺序;只有完全没有 Workspace 时才 `sessions.clear()` 进入无会话视图。Workspace 分组内的创建动作仍显式命中该 Workspace。
+- 解析保证(两臂同约定):promise resolve 时返回的 id 已在 list store。视图 owner 随后同步 retain,因此 draft 搬运方可以在历史就绪前通过该 binding 写入文本,无需等待 notifier flush。
+- 调用方拿 id 安装一份 `mainView` reference;首条提示词发送就是普通 `session.prompt`——Session 本来就在,失败即普通提示词失败,draft 文本还在 machine 里,重试即再次发送。
+- 全局 New Session 按钮默认取 `recentWorkspaceId`:先比较各 Workspace 内 Session 的最新 `updatedAt`,无 Session 时回退 Workspace `createdAt`,同值保持 Host 顺序;只有完全没有 Workspace 时才释放主视图 reference,进入无 Session 视图。Workspace 分组内的创建动作仍显式命中该 Workspace。
 - 运行时启动时订阅首次完整基线:若已有恢复成功的 current 会话则保持不动,否则自动 `connectWorkspace(recentWorkspaceId)` 并 open 返回的 blank 会话。该策略只结算一次;之后用户主动 clear 不会再次被自动选择覆盖,连接失败则等下一次基线投影重试。
-- blank Hero 中改选 Workspace 也走 `connectWorkspace`;若目标 id 与当前 id 不同,先把当前 input machine 的非空 draft 搬到目标 scope,再 `sessions.open(nextId)`。旧 blank 实体不删除,只因不再 current 而从列表隐藏。
+- blank Hero 中改选 Workspace 也走 `connectWorkspace`;若目标 id 与主视图 id 不同,`ui-workspace` 先 retain 目标,通过 preparation callback 搬运当前 input machine 的非空 draft,再发布新的主 reference。旧 blank 实体不删除,只因其 `mainView` reference 被释放而从列表隐藏。
 
-### 逐会话供数:`sessions.provide` 标准件通道
+### 逐会话供数:`uiSession.provide` 标准件通道
 
-会话 slot 组件「自己拿会话数据」的唯一供数路径。插件以静态描述符 `sessions.provide({hooks, props, resolve})` 声明固定键表(重名 key 注册时 throw),`resolve(binding)` 在确定会话下物化值并随 scope 拆;ui-renderer `standardKit` 统一循环把 hooks 格绑成 `use<Name>` 选择器钩子(`observableHook`→uSES,防 tearing)、props 格原样透传。
+Session slot 组件「自己拿 Session 数据」的唯一供数路径。插件以静态描述符 `uiSession.provide({hooks, props, resolve})` 声明固定键表(重名 key 注册时 throw),`resolve(binding)` 在确定 binding 下物化值并随其 scope 拆;ui-renderer `standardKit` 统一循环把 hooks 格绑成 `use<Name>` 选择器钩子(`observableHook`→uSES,防 tearing)、props 格原样透传。
 
 slot scope 是闭集 `root | session-maybe | session`:
 
 - `root` 只拿全局标准件,不接收会话身份或供数。
-- `session-maybe` 以**收养(adoption)身份语义**跟随 current 会话(唯一行为——不存在「永久保持实例」模式):空态出生的化身在**第一个**会话到来时保持 React 实例(空壳收养它——不重挂,DOM 存活);此后行为与严格会话 entry 完全一致——切到不同会话重挂,跌回无会话也重挂为崭新的空态化身(之后再次收养)。因此组件本地的逐会话状态**由构造保证**随切换清零;需要活过切换的状态必须住会话绑定的源(machine、store、hooks)。无会话时 `sessionId`、`useSession`/`useInput` 的选择结果及 `inputActions` 均可缺省。根部无 key 的 `SessionMaybeProvider` 通过订阅运行时的原子 `currentProvide` 投影驱动这条更新——选择移动和提供方名册变化经同一 source 发布,current id 不变时的名册变化也会重发已挂载 bundle,而不是把 entry 困在过期的钩子/prop 形状上——`SessionMaybeProvideInfo` 靠静态键表在无会话时仍保留完整钩子/prop 形状;逐 entry 的收养记账(化身计数 key)住在 renderer 的 `SessionMaybeEntry`。
+- `session-maybe` 以**收养(adoption)身份语义**继承最近 `SessionProvider` 的 binding:空态出生的化身在该 Provider 第一次收到 binding 时保持 React 实例,此后 Provider 切换 generation 或回到空态时重挂。Provider 切换 generation 时,组件本地的逐 Session 状态会清零。切换过程中,只有持久化 Store 值能活过 generation 退休;只有另一份 reference 保活该 generation 时,binding 自有 source 才能保留。无 binding 时,`sessionId`、`useSession`/`useInput` 的结果与 `inputActions` 均可缺省。Provider roster 变化会重新物化已挂载 binding,但不改变其 identity;逐 entry 的收养记账住在 renderer 的 `SessionMaybeEntry`。
 - `session` 保证 `sessionId`、所有钩子 source 与 props 均存在;每个严格 entry 的错误边界以 `sessionId` 为 key,切换会话会重建该 entry 及其会话 store。
 
-`conversation` 是 `session-maybe` 的常驻外壳:`ConversationRoot`、HeroShell、Workspace picker、root 持有的 scrollport 与 composer stack,以及 overlay chain 的 fallback 外框,在无会话 → blank 会话的切换中保持 React 实例。两个严格 session entry 只填入固定区域,不改变该树的父级:`conversation.session.header` 在 scrollport 上方承载 breadcrumb/tab/action,`conversation.session` 在其内部承载 view ring 与 draft mirror;二者共享同一个 session scope chat store。composer bar(`conversation.composer.bar`)本身即为 `session-maybe`:无 session 时,其 machine faces 和消息动作保持惰性,整张虚线卡片可经指针打开现有 Workspace picker,只读 textarea 也可通过 Enter 或 Space 打开。session 出现后同一实例(含 textarea)转为 live;其余输入 slot 保持严格 `session`,在此之前不派发任何内容。blank → engaging/active 的 InputBar 不因 phase 翻转而重建。
+`conversation` 是其 owner `SessionProvider` 下的 `session-maybe` 常驻外壳:`ConversationRoot`、HeroShell、Workspace picker、scrollport 与 composer stack,以及 overlay chain 的 fallback 外框,在无 Session → blank Session 的切换中保持 React 实例。两个严格 session entry 只填入固定区域,不改变该树的父级:`conversation.session.header` 在 scrollport 上方承载 breadcrumb/tab/action,`conversation.session` 在其内部承载 view ring 与 draft mirror;二者共享同一个 Session scope chat store。composer bar(`conversation.composer.bar`)本身即为 `session-maybe`:无 Session 时,其 machine faces 和消息动作保持惰性,整张虚线卡片可经指针打开现有 Workspace picker,只读 textarea 也可通过 Enter 或 Space 打开。binding 出现后同一实例(含 textarea)转为 live;其余输入 slot 保持严格 `session`,在此之前不派发任何内容。blank → engaging/active 的 InputBar 不因 phase 翻转而重建。
+
+blank Session 保留 header 的 leading 与 corner slot,让右侧栏展开入口等导航控件在首条消息之前即可使用。标题、actions、utilities 和 View tabs 在 blank phase 中继续隐藏。header 仍要求已选中的 Session;Files 与 Terminal 入口使用该 Session 的工作区和执行服务,无需已有 Turn 记录。
 
 - 运行时内建第一条:`'session'` 钩子——`useSession` 本身走同一机制,无特判。
 - Concurrent 纪律:渲染平面只从 hooks 格读(uSES 一致性保证);props 格回调只在事件 handler 空间用;描述符解析 render-safe(幂等缓存、废弃渲染残留由 prune 收尸)。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-20-client-session-conversation-ownership.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-20-client-session-conversation-ownership.md
-2026-08-20-client-session-conversation-ownership.md: 89f27f745c13e070b7f9794d98747bc73bda3b5a
-2026-08-20-client-session-conversation-ownership.zh.md: 79be1b898a1fb5a75e5438d55d739d84775fe6d9
+2026-08-20-client-session-conversation-ownership.md: 0d2f19cff1d4625d678a25c87bcc55fc3f2bfad4
+2026-08-20-client-session-conversation-ownership.zh.md: fdb2420efff88e0a5a33dba79bebf0d072280453

+ 28 - 27
.agents/notes/implemented/architecture/2026-08-20-client-session-conversation-ownership.md

@@ -38,7 +38,7 @@ Client Session and Workspace objects belong to `api/session-controller/client` a
 
 The React adapters for Session and Workspace belong to `client/ui-session` and `client/ui-workspace`. The Store engine belongs to `client/store`; the Slot registry, scope materialization, and observable-to-hook binding belong to `client/ui-renderer`.
 
-The system has no aggregate `client/runtime` package and no replacement central facade. [Session history and event transport](2026-08-18-session-history-and-event-transport.md) defines Session history, Remote streams, pagination cursors, and reconnect continuity; this note starts from the Client objects and sources published by Controllers.
+The system has no aggregate `client/runtime` package and no replacement central facade. [Session history and event transport](2026-08-18-session-history-and-event-transport.md) defines history continuity. [Client Session references](2026-09-15-client-session-references.md) owns reference acquisition, exact-generation lifetimes, main-area ownership, and unified UI status; this note owns the layering and source-registration rules.
 
 ## Layering principles
 
@@ -55,9 +55,10 @@ Each standard hook belongs to the `ui-*` package closest to its data semantics.
 | Hook | Owner | Source |
 | --- | --- | --- |
 | `useSessions` | `client/ui-session` | Session Controller global list |
-| `useSession` | `client/ui-session` | Current Session snapshot |
-| `useProjection` | `client/ui-session` | Current Session keyed projection |
-| `useSessionPendingInteraction` | `client/ui-session` | Aggregated pending domains |
+| `useSession` | `client/ui-session` | Bound Session snapshot |
+| `useProjection` | `client/ui-session` | Bound Session keyed projection |
+| `useSessionStatus` | `client/ui-session` | Running, effective pending request, and unread completion |
+| `useSessionRetainInfo` | `client/ui-session` | Read-only Controller reference-source counts |
 | `useWorkspaces` | `client/ui-workspace` | Workspace Controller list |
 | `useConversation` | `client/ui-conversation` | Conversation binding snapshot |
 | `useChat` | `client/ui-chat` | `chat` target source |
@@ -77,10 +78,10 @@ Adding a target does not add a branch to the renderer or Session Controller. The
 
 | Package | Owns | Explicitly does not own |
 | --- | --- | --- |
-| `api/session-controller/client` | Session objects, list, selection, commands, projections, queue, event windows, and Agent Contexts | Conversation targets, React, Slots, Workspace |
+| `api/session-controller/client` | Session objects, catalog, references, source counts, commands, projections, event windows, and Agent Contexts | Navigation, completion reminders, Conversation targets, React, Slots, Workspace |
 | `api/workspace-controller/client` | Workspace objects, ordering, archive state, commands, and snapshots | React, Session navigation policy, directory UI |
-| `client/ui-session` | Session scope, standard sources, `SessionProvider`, and pending-interaction aggregation | Session transport, Conversation assembly, Approval/Question results |
-| `client/ui-workspace` | Workspace hook, browser UI, and cross-Controller navigation policy | Workspace transport, copies of Session data |
+| `client/ui-session` | Explicit Session scope, standard sources, `SessionProvider`, and unified UI status | Session transport, reference ownership, Conversation assembly, Approval/Question results |
+| `client/ui-workspace` | Workspace hook, browser UI, main-area reference, and cross-Controller navigation policy | Workspace transport, copies of Session data |
 | `client/ui-conversation` | Conversation core, registries, bindings, shell, input, composer, queue, and View navigation | Session transport, Chat/Trajectory snapshots |
 | `client/ui-chat` | Chat target, Node definitions, renderers, selection, details, and locale | Session lifecycle, generic View navigation, Trajectory, historical-image cache |
 | `client/ui-trajectory` | Trajectory target, event-record projection, and inspection view | Session snapshots, Chat snapshots |
@@ -117,9 +118,9 @@ Session data reaches the UI through this path:
        useChat            useTrajectory
 ```
 
-Workspace data enters the Workspace Controller from `ctx.remote.workspace`, then `ui-workspace` exposes it as `useWorkspaces`. For cross-domain navigation, `ui-workspace` temporarily reads the Session Controller and issues a selection or command.
+Workspace data enters the Workspace Controller from `ctx.remote.workspace`, then `ui-workspace` exposes it as `useWorkspaces`. `ui-workspace` reads explicit targets for cross-domain navigation and owns the main-area reference without making it a default business Context.
 
-Approval and Question arrive from the Host waterfall through `ctx.remote.$on` at their respective UI owners. Each owner publishes a Pending object; `ui-session.pendingInteractions` then supplies that same object to Session navigation state and Conversation composer selection.
+Approval and Question arrive from the Host waterfall through `ctx.remote.$on` at their respective UI owners. Each owner publishes a Pending object; `ui-session.sessionStatus` supplies the same effective object to Workspace indicators and Conversation composer selection.
 
 ## Session Controller Client
 
@@ -142,7 +143,7 @@ Whether a field derives from an event, control frame, or local command does not
 
 The Session Controller exposes three distinct read faces:
 
-1. The global Session list and current-selection source, used by navigation and `useSessions`.
+1. The Session catalog and local ownership sources, used by `useSessions` and read-only reference metadata consumers.
 2. A logical binding for each Session containing `sessionId`, a `SessionSnapshot` source, commands, and projection sources.
 3. A Conversation-facing `SessionEventSource` used only by the Conversation assembly core.
 
@@ -160,9 +161,9 @@ Initial open, reconnect, gap repair, and updates whose continuity cannot be prov
 
 ### Session binding lifecycle
 
-Each Session binding owns a Cordis Context and Fiber. The Session Controller creates and releases the binding.
+Each live Session generation owns a Cordis Context and Fiber. The Controller creates its binding on acquisition and retires it on final reference release or root disposal.
 
-Objects that depend on a Session register cleanup through `binding.ctx.effect()`. Releasing a binding cleans up Conversation bindings, UI materializations, and scoped Slot stores without a dedicated `onBindingRelease` or `onRelease` callback protocol.
+Objects that depend on a Session register cleanup through `binding.ctx.effect()`. Generation retirement cleans up Conversation bindings, UI materializations, and scoped Slot stores without a dedicated `onBindingRelease` or `onRelease` callback protocol.
 
 This cleanup does not require the Session Controller to know the roster of upper-layer consumers.
 
@@ -172,12 +173,12 @@ This cleanup does not require the Session Controller to know the roster of upper
 
 `client/ui-session` is the sole Session adapter between the Session Controller and the React/Slot system. It provides `ctx.uiSession` and:
 
-- observes the Session list, current selection, and per-Session bindings;
+- observes the Session catalog, local reference metadata, and explicitly supplied bindings;
 - installs the session and session-maybe scope adapters;
 - supplies `SessionProvider` rendering semantics;
 - supplies built-in Session snapshot, projection, and sessionId sources;
 - accepts Session-scoped source contributions from other domain packages;
-- aggregates pending interactions registered by business packages.
+- aggregates domain-owned pending interactions with running and completion-reminder facts in `sessionStatus`.
 
 It does not own Session transport, event folding, Conversation targets, or concrete business results.
 
@@ -193,12 +194,12 @@ The runtime rejects undeclared, missing, or duplicate standard props. `ui-sessio
 
 session and session-maybe use the same adapter with different binding semantics:
 
-- a strict session scope refuses to render without a current binding;
+- a strict session scope refuses to render without an explicitly supplied binding;
 - session-maybe uses a stable absent binding to preserve hook call order;
-- changing the current Session rebuilds the strict Session subtree under the `sessionId` key;
-- root and session-maybe entries may remain mounted across Session changes.
+- changing the exact Context generation remounts a bound subtree, including same-id replacement;
+- an unbound session-maybe entry adopts its first binding without remounting; root entries have no Session binding.
 
-Each real materialized binding retains the Controller binding's Context. `ui-session` removes the cache entry and withdraws the current binding through `binding.ctx.effect()`.
+Each UI materialization borrows the Controller binding's Context. `ui-session` removes that generation's cache entry and publishes absence through `binding.ctx.effect()`; it does not retain the Session.
 
 Changing the contribution roster rematerializes existing bindings and publishes a new source set. Source identity remains stable within one binding lifetime, as required by `useSyncExternalStore` caching.
 
@@ -206,9 +207,9 @@ Changing the contribution roster rematerializes existing bindings and publishes
 
 `SessionProvider` is a standard seat derived by `PropsRenderSlots` from a session-scoped child declaration, not a React Context imported directly by business components.
 
-It accepts ordinary `ReactNode` children rather than a `(sessionId) => ReactNode` render function; callers wrap `renderSlot('details', {})` directly.
+It accepts ordinary `ReactNode` children and a required `session={reference | undefined}`. The Provider borrows the caller-owned reference without acquiring or releasing it; callers wrap `renderSlot('details', {})` directly.
 
-Session identity comes from the scope binding and standard `sessionId` prop. The Provider handles only the absent branch and subtree isolation by Session identity; components do not obtain Session data through a Provider callback.
+Session identity reaches components through the explicit scope binding and standard `sessionId` prop. An absent Provider stays unbound, and neither nested Providers nor root entries fall back to a main-area Session.
 
 ### Pending interactions
 
@@ -220,7 +221,7 @@ Concurrent objects with the same key are rejected; replacement requests use a ne
 
 `ui-session` selects each Session's effective object using domain precedence. Higher precedence wins; at equal precedence, the later valid object in traversal order wins.
 
-The aggregate is published as `pendingInteractions: ObservableSnapshot<ReadonlyMap<SessionId, SessionPendingInteraction>>`; `useSessionPendingInteraction` is its React read face.
+The pending aggregate is private to `ui-session`; its effective request appears unchanged as `sessionStatus.getSnapshot().get(id)?.pendingInteraction`. `useSessionStatus` is the public UI read face.
 
 Session navigation state and composer takeover read the same effective object. They do not maintain separate status maps or takeover rosters.
 
@@ -242,7 +243,7 @@ These combined facts do not enter `WorkspaceSnapshot`:
 
 `client/ui-workspace` registers the Workspace list source as the root standard source `workspaces`, from which the renderer provides `useWorkspaces`.
 
-Initial selection, blank-Session reuse, new-session navigation, concurrent-create coalescing, and navigation after archival are UI navigation policy. That policy may read both `ctx.workspaces` and `ctx.sessions` at decision time, but it issues only Controller commands and selection actions and does not publish a combined snapshot.
+Initial restoration, blank-Session reuse, new-session navigation, concurrent-create coalescing, and navigation after archival are UI policy. `ui-workspace` may read both Controllers, but it keeps the main target and reference in its own navigation owner instead of writing UI selection into a Controller snapshot.
 
 Directory pickers, directory browsing, and `openPath` are separate directory capabilities and do not enter the Workspace Controller.
 
@@ -286,7 +287,7 @@ The shell phase is a pure composition of Session lifecycle and Conversation targ
 
 ### Input and composer
 
-The composer chain belongs to `ui-conversation`; a concrete takeover belongs to its business package. `ConversationRoot` reads the current Session's effective object through `useSessionPendingInteraction` and supplies it to chain selectors as `ComposerChainProps.pendingInteraction`.
+The composer chain belongs to `ui-conversation`; a concrete takeover belongs to its business package. `ConversationRoot` reads its bound Session's effective request through `useSessionStatus` and supplies it to chain selectors as `ComposerChainProps.pendingInteraction`.
 
 A selector is a pure function of owner currency. Its non-null result reaches the selected component as `matched`. A stable composer entry and the default composer remain mounted together, while the chain selects one effective presentation.
 
@@ -334,7 +335,7 @@ The Gateway requires only that Remote Event arguments and results are valid JSON
 
 ### One pending projection
 
-The Sidebar and composer consume the same `pendingInteractions` snapshot. Navigation displays approval, plan-review, or question state from the effective object's `kind`; each composer entry selects its own panel by object identity.
+The Sidebar and composer consume the same effective `sessionStatus` pending request. Navigation displays approval, plan-review, or question state from its `kind`; each composer entry selects its own panel by object identity.
 
 The same request identity drives both UI surfaces. A request that replaces another request of the same type uses a new key, so selectors and subscribers observe the identity change.
 
@@ -366,7 +367,7 @@ Stores hold viewing and interaction state such as drafts, View selection, Chat s
 
 When one plugin provides both a source and a Slot entry, it registers the source first and the entry second. Reverse Cordis disposal then removes the entry before the source, so a mounted entry never briefly loses a required hook.
 
-Releasing a Session binding cleans up UI materialization and scoped Stores through `binding.ctx.effect()`. Releasing a plugin fiber cleans up sources, listeners, and Slot entries through registration disposers.
+Final Session-reference release cleans up UI materialization and scoped Stores through `binding.ctx.effect()`. Releasing a plugin fiber cleans up sources, listeners, and Slot entries through registration disposers.
 
 Every disposer is idempotent and depends on no implicit callback outside the Cordis lifecycle.
 
@@ -386,7 +387,7 @@ UI components do not receive `ctx`. Cross-package collaboration uses Cordis serv
 
 Before adding state, choose its sole owner from its consumption semantics: Host communication, commands, and entity lifecycle belong to an API Controller; data assembled from Session events but independent of a target belongs to the Conversation core; projections serving only one View belong to that target package; drafts, selections, and panel state belong to the UI package that owns the interaction.
 
-The same fact must not be retained simultaneously in a Controller snapshot, Conversation snapshot, and Store. A cross-domain decision reads multiple sources and immediately issues a command; it does not create a joined snapshot or cache another domain's object.
+The same fact must not be retained simultaneously in a Controller snapshot, Conversation snapshot, and Store. Cross-domain navigation reads sources at decision time. A UI-owned status source may compose independent running, pending-request, and completion-reminder facts, but must preserve domain ownership and object identity rather than duplicate those domains' state.
 
 These are signs of incorrect ownership: a Controller imports React; the renderer branches on business types; a component traverses Session events; a Store holds Session or Workspace entities; changing one target requires changing the Session Controller.
 
@@ -421,7 +422,7 @@ A target must not use another target's snapshot as its data source. Optional col
 5. When it can handle the request, create the Pending object, publish it through the publication function, await its result, and remove it in `finally`.
 6. Test concurrent keys, precedence, user cancellation, transport abort, plugin disposal, and delegation without a Session.
 
-A request does not register Slots, declare child Slots, mutate the Session snapshot, or create a separate state index. Sidebar and composer both read one effective object from `useSessionPendingInteraction`.
+A request does not register Slots, declare child Slots, mutate the Session snapshot, or create a separate state index. Sidebar and composer read the same effective object from `useSessionStatus`.
 
 ### Review checks
 

+ 28 - 27
.agents/notes/implemented/architecture/2026-08-20-client-session-conversation-ownership.zh.md

@@ -38,7 +38,7 @@ Session 与 Workspace 的 Client 对象分别归 `api/session-controller/client`
 
 Session 与 Workspace 的 React 适配分别归 `client/ui-session` 和 `client/ui-workspace`。Store engine 归 `client/store`,Slot registry、scope materialization 和 observable-to-hook 绑定归 `client/ui-renderer`。
 
-系统不提供聚合式 `client/runtime` package,也不设置替代它的总控 facade。Session history、Remote stream、分页 cursor 和重连连续性由 [Session 历史与事件传输](2026-08-18-session-history-and-event-transport.zh.md) 定义;本 Note 从 Controller 发布的 Client 对象与 source 开始
+系统没有聚合式 `client/runtime` 包,也没有替代的中央 facade。[Session 历史与事件传输](2026-08-18-session-history-and-event-transport.zh.md)定义历史连续性。[Client 会话引用](2026-09-15-client-session-references.zh.md)拥有引用获取、精确代际生命周期、主区域所有权和统一 UI 状态;本篇拥有分层与数据源注册规则
 
 ## 分层原则
 
@@ -55,9 +55,10 @@ UI 层可以同时读取多个 Controller 做一次导航决定,但不得把
 | Hook | Owner | Source |
 | --- | --- | --- |
 | `useSessions` | `client/ui-session` | Session Controller 全局列表 |
-| `useSession` | `client/ui-session` | 当前 Session snapshot |
-| `useProjection` | `client/ui-session` | 当前 Session keyed projection |
-| `useSessionPendingInteraction` | `client/ui-session` | pending domain 聚合结果 |
+| `useSession` | `client/ui-session` | 已绑定会话快照 |
+| `useProjection` | `client/ui-session` | 已绑定会话的键控投影 |
+| `useSessionStatus` | `client/ui-session` | 运行状态、有效待处理请求和未读完成提醒 |
+| `useSessionRetainInfo` | `client/ui-session` | 控制器的只读引用来源计数 |
 | `useWorkspaces` | `client/ui-workspace` | Workspace Controller 列表 |
 | `useConversation` | `client/ui-conversation` | Conversation binding snapshot |
 | `useChat` | `client/ui-chat` | `chat` target source |
@@ -77,10 +78,10 @@ UI 层可以同时读取多个 Controller 做一次导航决定,但不得把
 
 | Package | 拥有内容 | 明确不拥有 |
 | --- | --- | --- |
-| `api/session-controller/client` | Session 对象、列表、选择、命令、projection、queue、事件窗口和 Agent Context | Conversation target、React、Slot、Workspace |
+| `api/session-controller/client` | 会话对象、目录、引用、来源计数、命令、投影、事件窗口与 Agent Context | 导航、完成提醒、Conversation target、React、Slot、Workspace |
 | `api/workspace-controller/client` | Workspace 对象、顺序、归档、命令和 snapshot | React、Session 导航策略、目录 UI |
-| `client/ui-session` | Session scope、标准 source、`SessionProvider`、pending interaction 聚合 | Session transport、Conversation 组装、Approval/Question 结果 |
-| `client/ui-workspace` | Workspace hook、浏览器 UI 和跨 Controller 导航策略 | Workspace transport、Session 数据副本 |
+| `client/ui-session` | 显式会话作用域、标准数据源、`SessionProvider` 与统一 UI 状态 | 会话传输、引用所有权、Conversation 组装、Approval/Question 结果 |
+| `client/ui-workspace` | Workspace 钩子、浏览器 UI、主区域引用与跨控制器导航策略 | Workspace 传输、会话数据副本 |
 | `client/ui-conversation` | Conversation core、registry、binding、shell、input、composer、queue 和 View 导航 | Session transport、Chat/Trajectory snapshot |
 | `client/ui-chat` | Chat target、Node definitions、renderer、selection、details 和 locale | Session 生命周期、通用 View 导航、Trajectory、历史图片 cache |
 | `client/ui-trajectory` | Trajectory target、事件记录投影和检查视图 | Session snapshot、Chat snapshot |
@@ -117,9 +118,9 @@ Session 数据按以下路径进入 UI:
        useChat            useTrajectory
 ```
 
-Workspace 数据从 `ctx.remote.workspace` 进入 Workspace Controller,再由 `ui-workspace` 暴露为 `useWorkspaces`;需要跨域导航时,`ui-workspace` 临时读取 Session Controller 并发出选择或命令
+Workspace 数据由 `ctx.remote.workspace` 进入 Workspace 控制器,再由 `ui-workspace` 通过 `useWorkspaces` 提供。`ui-workspace` 为跨领域导航读取显式目标并持有主区域引用,不把它变为默认业务 Context
 
-Approval 与 Question 从 Host waterfall 经 `ctx.remote.$on` 到达各自 UI owner。Owner 发布 Pending 对象,`ui-session.pendingInteractions` 再把同一对象送往 Session 导航状态和 Conversation composer selection
+Approval 与 Question 通过 `ctx.remote.$on` 从 Host waterfall 到达各自的 UI owner。各 owner 发布 Pending 对象;`ui-session.sessionStatus` 向 Workspace 标识和 Conversation composer 选择提供同一个有效对象
 
 ## Session Controller Client
 
@@ -142,7 +143,7 @@ Approval 与 Question 从 Host waterfall 经 `ctx.remote.$on` 到达各自 UI ow
 
 Session Controller 对外提供三个互不替代的读取面:
 
-1. 全局 Session list 与 current selection source,供导航和 `useSessions` 使用。
+1. 会话目录与本地所有权数据源,由 `useSessions` 和只读引用元数据消费方使用。
 2. 每个 Session 的逻辑 binding,包含 `sessionId`、`SessionSnapshot` source、commands 与 projection sources。
 3. Conversation-facing `SessionEventSource`,只供 Conversation assemble core 使用。
 
@@ -160,9 +161,9 @@ Session Controller 对外提供三个互不替代的读取面:
 
 ### Session binding 生命周期
 
-每个 Session binding 持有自己的 Cordis Context 与 Fiber。Session Controller 创建 binding,也负责释放它
+每个活跃会话 generation 持有 Cordis Context 与 Fiber。控制器在获取引用时创建绑定,并在最后一份引用释放或根销毁时结束该绑定
 
-依赖 Session 的对象把清理注册到 `binding.ctx.effect()`。Binding 释放会触发 Conversation binding、UI materialization 和 scoped Slot store 的清理,不存在额外的 `onBindingRelease` 或 `onRelease` 回调协议。
+依赖会话的对象通过 `binding.ctx.effect()` 注册清理。generation 结束会清理 Conversation 绑定、UI 物化结果和作用域 Slot 存储,无需单独的 `onBindingRelease` 或 `onRelease` 回调协议。
 
 这种清理方式不要求 Session Controller 了解上层消费者名册。
 
@@ -172,12 +173,12 @@ Session Controller 对外提供三个互不替代的读取面:
 
 `client/ui-session` 是 Session Controller 与 React/Slot 系统之间唯一的 Session adapter。它提供 `ctx.uiSession`,并负责:
 
-- 观察 Session list、current selection 和 per-Session binding
+- 观察会话目录、本地引用元数据和显式提供的绑定
 - 安装 session 与 session-maybe scope adapter;
 - 提供 `SessionProvider` 的呈现语义;
 - 内建 session snapshot、projection 和 sessionId source;
 - 接收其他领域 package 的 Session-scoped source contribution;
-- 聚合业务 package 注册的 pending interaction
+- 在 `sessionStatus` 中组合领域持有的待处理交互、运行事实和完成提醒
 
 它不拥有 Session transport、event folding、Conversation target 或具体业务结果。
 
@@ -193,12 +194,12 @@ Session Controller 对外提供三个互不替代的读取面:
 
 session 与 session-maybe 使用同一个 adapter,但绑定语义不同:
 
-- strict session scope 在没有 current binding 时拒绝渲染;
+- 严格会话作用域在没有显式提供的绑定时拒绝渲染;
 - session-maybe 使用稳定 absent binding,保持 hook 调用顺序;
-- current Session 切换以 `sessionId` 为 key 重建严格 Session subtree
-- root 与 session-maybe entry 可以跨 Session 切换常驻
+- 精确 Context generation 改变时重新挂载已绑定子树,同一 id 的替代 generation 也如此
+- 未绑定的 session-maybe 条目无需重新挂载即可接纳首个绑定;root 条目没有会话绑定
 
-每个真实 materialized binding 保留 Controller binding 的 Context。`ui-session` 通过 `binding.ctx.effect()` 删除缓存项并撤销 current binding
+每个 UI 物化结果借用控制器绑定的 Context。`ui-session` 通过 `binding.ctx.effect()` 移除该 generation 的缓存项并发布空值,不会 retain 会话
 
 Contribution roster 变化会重建已 materialize 的 binding 并发布新的 source 集合。同一 binding 生命周期内,source identity 保持稳定,以满足 `useSyncExternalStore` 的缓存要求。
 
@@ -206,9 +207,9 @@ Contribution roster 变化会重建已 materialize 的 binding 并发布新的 s
 
 `SessionProvider` 是 `PropsRenderSlots` 根据 session-scoped child 声明派生的标准席,不是业务 component 直接 import 的 React Context。
 
-它接收普通 `ReactNode` children,不接收 `(sessionId) => ReactNode` render function;调用方直接用它包裹 `renderSlot('details', {})`。
+它接收普通 `ReactNode` children 和必填的 `session={reference | undefined}`。Provider 借用调用方持有的引用,不获取或释放它;调用方直接包住 `renderSlot('details', {})`。
 
-Session identity 通过 scope binding 和标准 `sessionId` prop 提供。Provider 只负责 absent branch 与按 Session identity 隔离 subtree,组件不得借助 Provider 回调取得 Session 数据
+会话身份通过显式作用域绑定和标准 `sessionId` prop 到达组件。空 Provider 保持未绑定,嵌套 Provider 和 root 条目都不会回退到主区域会话
 
 ### Pending interaction
 
@@ -220,7 +221,7 @@ Session identity 通过 scope binding 和标准 `sessionId` prop 提供。Provid
 
 `ui-session` 使用各 domain 的 precedence 选出每个 Session 当前生效的对象。较高 precedence 胜出,相同 precedence 下后遍历到的有效对象胜出。
 
-聚合结果发布为 `pendingInteractions: ObservableSnapshot<ReadonlyMap<SessionId, SessionPendingInteraction>>`,`useSessionPendingInteraction` 是其 React 读取面
+待处理聚合是 `ui-session` 的私有实现;其有效请求原样出现在 `sessionStatus.getSnapshot().get(id)?.pendingInteraction` 中。`useSessionStatus` 是公开 UI 读取接口
 
 Session 导航状态和 composer takeover 必须读取同一个 effective object,不得分别维护 status map 或 takeover roster。
 
@@ -242,7 +243,7 @@ Session 导航状态和 composer takeover 必须读取同一个 effective object
 
 `client/ui-workspace` 把 Workspace list source 注册为 root 标准 source `workspaces`,renderer 由此提供 `useWorkspaces`。
 
-初始选择、blank Session 复用、新建导航、并发创建合并和归档后导航属于 UI navigation policy。该 policy 可以在决定时同时读取 `ctx.workspaces` 与 `ctx.sessions`,但只调用 Controller command 和 selection action,不发布联合 snapshot
+启动恢复、空白会话复用、新会话导航、并发创建合并与归档后的导航属于 UI 策略。`ui-workspace` 可以读取两个控制器,但把主目标和引用保存在自己的导航 owner 中,不向控制器快照写入 UI 选择
 
 目录 picker、目录浏览和 `openPath` 属于独立目录能力,不进入 Workspace Controller。
 
@@ -286,7 +287,7 @@ Shell phase 由 Session lifecycle 与 Conversation target activity 纯合成。S
 
 ### Input 与 composer
 
-Composer chain 属于 `ui-conversation`,具体 takeover 属于业务 package。`ConversationRoot` 从 `useSessionPendingInteraction` 读取当前 Session 的 effective object,并作为 `ComposerChainProps.pendingInteraction` 交给 chain selector。
+composer chain 属于 `ui-conversation`,具体接管属于业务包。`ConversationRoot` 通过 `useSessionStatus` 读取已绑定会话的有效请求,并作为 `ComposerChainProps.pendingInteraction` 提供给 chain selector。
 
 Selector 是 owner currency 的纯函数,非 null 结果作为 `matched` 传给获选 component。Stable composer entry 与默认 composer 可以同时常驻,chain 只选择一个有效呈现。
 
@@ -334,7 +335,7 @@ Gateway 只要求 Remote Event 参数和结果是合法 JSON 传输值,不复
 
 ### 单一 pending 投影
 
-Sidebar 与 composer 使用相同 `pendingInteractions` snapshot。导航根据 effective object 的 `kind` 显示审批、计划审阅或问题状态,composer entry 根据对象实例选择自己的面板。
+Sidebar 与 composer 消费 `sessionStatus` 中同一个有效待处理请求。导航根据其 `kind` 显示审批、计划审阅或问题状态;每个 composer 条目按对象身份选择自己的面板。
 
 同一请求 identity 同时驱动两处 UI。新请求替换同类型旧请求时使用新 key,因此 selector 与订阅者都观察到身份变化。
 
@@ -366,7 +367,7 @@ Store 只承载 draft、View selection、Chat selection、inspection request 和
 
 一个 plugin 同时提供 source 与 Slot entry 时,先注册 source,再注册 entry。Cordis 反向 disposal 先移除 entry,再移除 source,仍挂载的 entry 因而不会短暂失去必需 hook。
 
-Session binding 释放通过 `binding.ctx.effect()` 清理 UI materialization 与 scoped store。Plugin fiber 释放通过 registration disposer 清理 source、listener 和 Slot entry
+最后一份会话引用释放后,通过 `binding.ctx.effect()` 清理 UI 物化结果和作用域存储。插件 fiber 释放通过注册 disposer 清理数据源、监听器和 Slot 条目
 
 所有 disposer 都可重复调用,不依赖 Cordis 生命周期以外的隐式回调。
 
@@ -386,7 +387,7 @@ UI component 不接收 `ctx`。跨 package 协作使用 Cordis service、standar
 
 新增状态前先按消费语义确定唯一 owner:Host 通信、命令和实体生命周期归 API Controller;由 Session events 形成且与 target 无关的数据归 Conversation core;只服务一种 View 的投影归对应 target package;草稿、选择和面板状态归拥有该交互的 UI package。
 
-同一事实不得同时保存在 Controller snapshot、Conversation snapshot 和 Store。需要跨域决策时读取多个 source 并立即发出 command,不创建联合 snapshot,也不缓存另一领域的对象副本
+同一个事实不能同时保存在控制器快照、Conversation 快照和存储中。跨领域导航在决策时读取数据源。UI 持有的状态数据源可以组合独立的运行、待处理请求和完成提醒事实,但必须保留领域归属与对象身份,不能复制这些领域的状态
 
 以下信号表示 owner 选择错误:Controller 开始 import React;renderer 出现业务类型分支;组件遍历 Session events;Store 保存 Session 或 Workspace 实体;一个 target 的变化要求修改 Session Controller。
 
@@ -421,7 +422,7 @@ Target 不得读取另一个 target 的 snapshot 作为自己的数据源。可
 5. 可处理时创建 Pending 对象,使用 publication function 发布,等待结果,并在 `finally` 中移除。
 6. 测试并发 key、precedence、用户取消、transport abort、plugin disposal 和无 Session delegation。
 
-单次请求不得注册 Slot、声明 child Slot、修改 Session snapshot 或另建状态索引。Sidebar 与 composer 都从 `useSessionPendingInteraction` 读取同一个 effective object
+请求不注册 Slot、不声明子 Slot、不修改会话快照,也不创建独立状态索引。Sidebar 与 composer 从 `useSessionStatus` 读取同一个有效对象
 
 ### Review 检查点
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-08-global-main-panels.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-09-08-global-main-panels.md
-2026-09-08-global-main-panels.md: 6aa7df2dcdd28b94cc0084a0044764efb2e749e2
-2026-09-08-global-main-panels.zh.md: bbb10b2ef5784748a165ad54c907fac2a63c9b17
+2026-09-08-global-main-panels.md: 63530a6b498bd8099cf627d82c888846d3d96767
+2026-09-08-global-main-panels.zh.md: 776787c3de3b79ca7948dcf949ac0393f87e7808

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-08-global-main-panels.md

@@ -10,13 +10,13 @@ Plugins need application-wide views that do not belong to a Session. A Session-s
 
 ## Decision
 
-The layout declares a root-scoped keyed `main` slot. The reserved `conversation` key belongs to the Conversation plugin, whose `main.conversation` child retains optional-Session binding. Other main entries receive no implicit Session binding.
+The layout declares a root-scoped keyed `main` slot. The reserved `conversation` key belongs to Conversation. `ui-session` derives the root Session binding from `uiWorkspace`'s `mainView` ownership marker; `main.conversation` and its associated right Sidebar inherit that Provider binding. Other main entries receive no implicit Session binding. [Client Session references](2026-09-15-client-session-references.md) owns acquisition and source metadata; this note owns global panel selection.
 
 The sidebar owns the root-scoped `sidebar.panellist` list. Each list entry supplies its icon and an id matching its main entry; its string or locale-aware label provides plain visible text, the accessible name, and the collapsed tooltip. The shipped composition registered no panel entry when this landed, so an empty list has no DOM or spacing; the web bundle's plugin manager now registers the first one ([plugin management moves to the Web sidebar](2026-09-09-plugin-management-in-the-web-sidebar.md)). Selection validates the live main entry and rejects a missing key without replacing the current panel.
 
 One eagerly created root store is shared by the renderer and layout controller. Its `panelInfo` and `layoutInfo` objects preserve independent references. The framework supplies `usePanelInfo`; individual rows and main content subscribe to their required selection values, while AppFrame reads only layout information. The right Sidebar's root controller decides whether to mount its Session subtree and reports the resulting track requirements to the frame.
 
-`uiWorkspace.openSession(id)` selects the Session before returning the main area to the Conversation, including when the same Session is selected again. `openWorkspace` and `forkSession` use the layout's `beginNavigation()` abort signal and their own service lifetime to commit only the latest navigation. The Workspace preparation callback moves drafts synchronously only while the request remains current. Supersession prevents a late UI commit, not Session creation. Panel navigation neither cancels the retained Session nor writes a Session event.
+`uiWorkspace.openSession(target)` acquires the explicit target before replacing its main reference and returning the main area to Conversation. `openWorkspace` and `forkSession` keep the layout's existing `beginNavigation()` signal and service lifetime. `openWorkspace` runs its existing synchronous preparation after acquisition and before replacing the main reference. Supersession prevents a late UI commit, not Session creation. Direct Session opening adds no global navigation cancellation. Panel navigation neither releases the retained main Session nor writes a Session event.
 
 DOM focus is not navigation selection. Search and directory-picker controls can receive focus while the global panel and its selected sidebar row remain visible; opening a Session changes the main selection.
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-08-global-main-panels.zh.md

@@ -10,13 +10,13 @@ Status: implemented
 
 ## 决策
 
-布局声明 root 作用域的 keyed `main` slot。保留的 `conversation` key 属于 Conversation 插件,其 `main.conversation` 子 slot 保留可选的会话绑定。其他主面板条目不获得隐式会话绑定。
+布局声明 root 作用域的 keyed `main` slot。保留的 `conversation` key 属于 Conversation。`ui-session` 根据 `uiWorkspace` 的 `mainView` 所有权标记得出根 Session binding;`main.conversation` 及其关联右 Sidebar 继承该 Provider binding。其他主面板条目不获得隐式会话绑定。[Client 会话引用](2026-09-15-client-session-references.zh.md)拥有引用获取与来源元数据;本篇拥有全局面板选择。
 
 侧栏拥有 root 作用域的 `sidebar.panellist` list。每个 list 条目提供图标,以及与主面板条目匹配的 id;字符串或随语言变化的标签提供普通可见文字、无障碍名称和折叠提示。本决定落地时默认组合不注册面板条目,因此空列表没有 DOM 或间距;现在 web bundle 的插件管理器注册了第一个条目([插件管理移到 Web 侧栏](2026-09-09-plugin-management-in-the-web-sidebar.zh.md))。选中操作检查实时主面板条目,对缺失的 key 报错而不替换当前面板。
 
 渲染器与布局控制器共享一个直接创建的 root 存储。其 `panelInfo` 和 `layoutInfo` 对象保持独立的引用。框架提供 `usePanelInfo`;各行和中央内容订阅所需的选中态值,AppFrame 仅读取布局信息。右侧 Sidebar 的 root 控制器决定是否挂载其会话子树,并把最终所需的列宽报告给框架。
 
-`uiWorkspace.openSession(id)` 先选中会话,再将中央区域切回 Conversation,包括再次选中同一个会话的情况。`openWorkspace` 和 `forkSession` 使用布局的 `beginNavigation()` abort signal 与自身 service 生命周期,只提交最新导航。工作区准备回调仅在请求仍有效时同步搬移草稿。请求过期会阻止晚到的 UI 提交,但不阻止会话创建。面板导航既不取消保留的会话,也不写入会话事件。
+`uiWorkspace.openSession(target)` 先获取显式目标,再替换主引用并让中央区域返回 Conversation。`openWorkspace` 和 `forkSession` 保留布局既有的 `beginNavigation()` 信号与服务生命周期。`openWorkspace` 在获取完成后、替换主引用前执行既有的同步准备动作。请求被替代会阻止迟到的 UI 提交,不阻止会话创建。直接打开会话不增加全局导航取消。面板导航既不释放所持主会话,也不写入会话事件。
 
 DOM 焦点不是导航选中态。搜索和目录选择控件可以获得焦点,同时保留全局面板及其侧栏行的选中态;打开会话才改变中央区域的选中态。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.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-09-09-consumer-owned-startup-strictness.md
-2026-09-09-consumer-owned-startup-strictness.md: e009e66ead25ef0a5e6001d33663e32bc04d19d2
-2026-09-09-consumer-owned-startup-strictness.zh.md: 58c364056f5b0dc41e018cd5488be983662401d4
+2026-09-09-consumer-owned-startup-strictness.md: 2f16078c7051b4038c1d48120b500f0fcd465aef
+2026-09-09-consumer-owned-startup-strictness.zh.md: c0fe61e5f708f79f6a5b903f3157ee358c3086bc

+ 4 - 2
.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.md

@@ -28,11 +28,13 @@ This policy governs [Web host boot](2026-07-24-web-config-tree-boot-and-transpor
 
 ## Consequences
 
-Stable required entry ids are part of application assembly. Renaming one requires updating the list and its tests. Optional plugin failures remain visible in Loader state and stderr without tearing down active siblings. Required failures use the same detailed import, activation, or pending-service diagnostic before app-boot disposes the root.
+Stable required entry ids are part of application assembly. Renaming one requires updating the list and its tests. Optional plugin failures remain visible in Loader state and stderr without tearing down active siblings. Required failures combine every inactive entry into one diagnostic, separating failed plugins from pending services and marking required entries. `StartupError` retains the original failures as its cause after app-boot disposes the root. The CLI prints its message once and exits with code 1, avoiding duplicate wrapper stacks while preserving plugin stacks, nested causes, and aggregate members. The CLI saves original errors, inactive-entry metadata, and startup warning/error records in a unique report directly under `$DSH_HOME/logs/`. This preserves import errors and error properties that the concise terminal output omits. Failed writes fall back to the full report on stderr and keep exit code 1. Unrelated exceptions remain unhandled.
+
+The compact terminal report keeps the failing plugins visible; a separate file retains raw diagnostics without the default logger buffer's record limit. Raw error values remain intact, so a sharing warning accompanies the report rather than silently redacting fields. An independent exporter lifetime covers asynchronous application disposal. The CLI awaits stderr completion before explicitly exiting, because failed plugins can leave stdin or other handles open.
 
 ## Testing
 
-App-boot unit tests cover absent and disabled required ids, optional import failure, config evaluation failure, synchronous and asynchronous `apply()` failure, pending dependencies, and required failure teardown. The built Web-profile acceptance serves the full UI with optional failures and exits nonzero without readiness when the required HTTP port is occupied or `modules` or `connection` cannot activate.
+App-boot unit tests cover absent and disabled required ids, optional import failure, config evaluation failure, synchronous and asynchronous `apply()` failure, pending dependencies, and required failure teardown. Unit expectations pin diagnostic grouping, preservation of original error objects and import logs, exporter cleanup, complete diagnostic values, private file creation, concurrent report names, and failed-write fallback. The built Web-profile acceptance asserts a single port-conflict stack without Node wrapper output, verifies the saved diagnostic file and its stderr fallback, serves the full UI with optional failures and exits nonzero without readiness when the required HTTP port is occupied or `modules` or `connection` cannot activate.
 
 The [Web process matrix](../../../../apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts) independently exercises optional and required failures at startup and after native patch-file edits. Authenticated HTTP requests and plugin lifecycle files distinguish a usable application from a surviving process. These keyless process checks complement the [controlled-delivery unit tests](../testing/2026-09-09-user-patch-hmr-test-delivery.md): unit tests isolate reconciliation failures, while the process tests also require the shipped launcher, native watcher, and bounded shutdown to work together.
 

+ 4 - 2
.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.zh.md

@@ -28,11 +28,13 @@ Required id 为 `agent-loop`、`webserver`、`modules`、`connection`、`headles
 
 ## 后果
 
-稳定的 required entry id 是应用 assembly 的一部分。重命名时必须同步更新 list 与测试。Optional plugin failure 会保留在 Loader state 和 stderr 中,但不会拆卸 active sibling。Required failure 使用相同的详细 import、activation 或 pending-service 诊断,然后由 app-boot 拆卸 root。
+稳定的 required entry id 是应用 assembly 的一部分。重命名时必须同步更新 list 与测试。Optional plugin failure 会保留在 Loader state 和 stderr 中,但不会拆卸 active sibling。Required failure 将所有 inactive entry 合并到一份诊断中,区分失败插件与等待服务的插件,并标记 required entry。App-boot 拆卸 root 后,`StartupError` 仍以 cause 保留原始失败。CLI 仅输出其消息一次,并以退出码 1 结束,避免重复的包装堆栈,同时保留插件堆栈、嵌套原因和聚合错误成员。CLI 将原始错误、未激活条目的元数据及启动警告、错误记录保存到直接位于 `$DSH_HOME/logs/` 下的唯一报告中,保留简洁终端输出省略的导入错误和错误属性。写入失败时,完整报告回退到 stderr,退出码仍为 1。其他异常继续作为未处理异常抛出。
+
+简洁的终端报告突出失败插件;单独文件保存原始诊断,不受默认 logger 缓冲区记录数限制。原始错误值保持完整,因此报告附带分享提醒,不会静默脱敏字段。独立的 exporter 生命周期覆盖应用的异步资源释放。CLI 等待 stderr 写入完成后明确退出,因为失败插件可能留下 stdin 或其他打开的句柄。
 
 ## 测试
 
-App-boot 单元测试覆盖缺失和禁用的 required id、optional import failure、config evaluation failure、同步和异步 `apply()` failure、pending dependency,以及 required failure teardown。构建后的 Web-profile acceptance 会在 optional failure 存在时继续提供完整 UI,并在 required HTTP port 被占用或 `modules`、`connection` 无法激活时以非零码退出,且不报告就绪。
+App-boot 单元测试覆盖缺失和禁用的 required id、optional import failure、config evaluation failure、同步和异步 `apply()` failure、pending dependency,以及 required failure teardown。单元预期输出固定诊断分组、原始错误对象与导入日志的保留、exporter 清理、完整诊断值、私有文件创建、并发报告命名以及写入失败回退行为。构建后的 Web-profile acceptance 断言端口冲突堆栈只输出一次且不包含 Node 包装输出,验证已保存的诊断文件及其 stderr 回退,并会在 optional failure 存在时继续提供完整 UI,并在 required HTTP port 被占用或 `modules`、`connection` 无法激活时以非零码退出,且不报告就绪。
 
 [Web 进程矩阵](../../../../apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts)分别验证启动时和原生补丁文件修改后的 optional 与 required 失败。经过认证的 HTTP 请求和插件生命周期文件区分可用应用与仅存活的进程。这些无需密钥的进程检查与[受控事件投递单元测试](../testing/2026-09-09-user-patch-hmr-test-delivery.zh.md)互补:单元测试隔离配置协调失败,进程测试还要求随附启动器、原生监听器和有界关闭流程协同工作。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.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-09-10-desktop-web-wrapper.md
-2026-09-10-desktop-web-wrapper.md: 13b2a36643198f649d6c4eb56df3e01aa98b50ac
-2026-09-10-desktop-web-wrapper.zh.md: de1f5e671c88ea03d6bd36e88692c81ff6f58404
+2026-09-10-desktop-web-wrapper.md: 445c574f86d6e465763603e7d447d92ffefb4f99
+2026-09-10-desktop-web-wrapper.zh.md: a41ce3c2f18237ddf580777e88fe05507998982f

+ 4 - 0
.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.md

@@ -26,8 +26,12 @@ Shared `initProfile` creates missing profile files and preserves existing conten
 
 This partially supersedes the private composition and portless transport in the [packaging decision](2026-08-25-electron-desktop-packaging-and-updates.md). That design avoided listening ports and used framed byte pipes to avoid Base64 expansion and cross-version V8 serialization. Shared HTTP gives up the portless guarantee and assigns serving and authentication to the existing Web implementation. Release identity, signing, process ownership, and native shell features remain active decisions.
 
+Native directory selection in the local application uses a narrow preload IPC call to Electron’s window-owned dialog. Main admits only the current application window’s main frame at `dsh-app://app`; shell, remote, and child frames cannot request it. Concurrent requests share the pending dialog and destroyed windows discard selections. Web backend selection and Host browse are shared.
+
 ## Alternatives considered
 
+**Use the Host OS chooser in Electron.** The Host’s macOS AppleScript dialog has no Electron parent window and cannot reliably follow application focus. Electron owns the local dialog while Web keeps its Host chooser; cancellation and errors do not launch a second chooser.
+
 **Maintain a second backend composition and carrier.** This permits a portless application, but every Web route, reload behavior, authentication change, and stream capability needs a Desktop implementation or explicit omission. Reintroduction requires a desktop product requirement that cannot use the Web implementation and justifies that continuing cost.
 
 **Merge CLI and Desktop plugin installations.** Shared boot code does not require shared executable dependencies. Separate installations allow independently qualified releases and plugin versions while their existing data owners govern shared sessions and settings.

+ 4 - 0
.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.zh.md

@@ -26,8 +26,12 @@ App-boot 负责已安装依赖发现、安装目录优先的 bundle 声明解析
 
 本记录部分取代[打包决策](2026-08-25-electron-desktop-packaging-and-updates.zh.md)中的私有组合与无端口传输。该设计避免监听端口,并使用分帧字节管道避免 Base64 膨胀与跨版本 V8 序列化。共享 HTTP 放弃无端口保证,将服务与认证交给已有 Web 实现。发布身份、签名、进程归属及原生壳功能仍是有效决策。
 
+本地应用的原生目录选择通过窄 preload IPC 调用 Electron 的窗口所属对话框。Main 仅接受当前应用窗口中位于 `dsh-app://app` 的主框架请求;shell、远程页面和子框架均不能调用。并发请求共用待完成的对话框,窗口销毁后丢弃选择结果。Web 后端选择和 Host 浏览由共享实现负责。
+
 ## Alternatives considered
 
+**在 Electron 中使用 Host 操作系统选择器。** Host 的 macOS AppleScript 对话框没有 Electron 父窗口,无法可靠跟随应用焦点。Electron 负责本地对话框,Web 保留 Host 选择器;取消和错误不会启动第二个选择器。
+
 **维护第二套后端组合与传输。** 这允许应用不监听端口,但每项 Web 路由、重载行为、认证变化和流式能力都需要 Desktop 实现或明确省略。只有无法使用 Web 实现、且足以承担持续维护成本的桌面产品需求,才支持重新引入这种方案。
 
 **合并 CLI 与 Desktop 插件安装。** 共享启动代码不要求共享可执行依赖。独立安装允许分别验收发布与插件版本,共享会话和设置则仍由已有数据归属方负责。

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.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-09-11-desktop-electron-node-runtime.md
-2026-09-11-desktop-electron-node-runtime.md: 6743bbfcc9acffb04d28ef9f1540d516b10dd2fc
-2026-09-11-desktop-electron-node-runtime.zh.md: 73622e66183eb7ed94f654e861cb061e02be7a3d
+2026-09-11-desktop-electron-node-runtime.md: 71516bb930f8eee22c1c6a988b6dab596a53a215
+2026-09-11-desktop-electron-node-runtime.zh.md: 162908392b98ee6a666ca414b602207ad8344937

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.md

@@ -18,7 +18,7 @@ This supersedes the separate-Node choice in the [packaging decision](2026-08-25-
 
 Host and pnpm launches pass `--expose-internals`: the bundled Cordis loader uses Node's internal ESM loader, while its native builtin accessor cannot locate the required symbol in Electron 44. The explicit flag makes that loader available without modifying Cordis. The RunAsNode fuse remains enabled.
 
-Package-script environments prepend a small `node` shell launcher that forwards arguments to the current Electron executable. This supports shell lifecycle scripts without a system Node installation. On Windows it is `node.cmd`, not a replacement `node.exe`; third-party code that directly spawns the literal `node` executable without a shell must use `process.execPath` or provide its own runtime. Child processes inherit RunAsNode; worker threads inherit the Host's arguments. Desktop does not emulate upstream OpenSSL behavior or rebuild arbitrary third-party native addons automatically.
+The Host inherits the caller PATH without Desktop’s private `bin` directory, so PTC and agent shells cannot resolve internal launchers through that directory. `DSH_DESKTOP_NODE_EXECUTABLE` is injected only for package installation. Package-script environments prepend a small `node` shell launcher that forwards arguments to the current Electron executable. This supports shell lifecycle scripts without a system Node installation. On Windows it is `node.cmd`, not a replacement `node.exe`; third-party code that directly spawns the literal `node` executable without a shell must use `process.execPath` or provide its own runtime. Child processes inherit RunAsNode; worker threads inherit the Host's arguments. Desktop does not emulate upstream OpenSSL behavior or rebuild arbitrary third-party native addons automatically.
 
 Electron's Node patches and native ABI are release compatibility obligations. The packaged native smoke exercises pnpm shell scripts without system Node on PATH, terminal output through the Windows shell, Koffi, Sharp, and HTML conversion. The Host smoke loads an external plugin sharing Cordis and serves its route through the real Web application. Platform signing and installed-application qualification remain required; Windows results do not establish macOS compatibility. Windows token signing runs serially and retains the first failure, preventing queued tasks from repeating a rejected PIN.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.zh.md

@@ -18,7 +18,7 @@ Desktop 通过自己的 Electron 可执行文件运行共享 Web Host 和内置
 
 Host 和 pnpm 启动时传入 `--expose-internals`:内置 Cordis 加载器使用 Node 内部 ESM 加载器,而其原生 builtin 访问器无法在 Electron 44 中找到所需符号。显式参数使加载器可用,无需修改 Cordis。RunAsNode fuse 保持启用。
 
-包脚本环境在 PATH 前添加一个小型 `node` shell 启动器,把参数转发给当前 Electron 可执行文件。这支持没有系统 Node 的 shell 生命周期脚本。Windows 上它是 `node.cmd`,并非替代的 `node.exe`;第三方代码若绕过 shell 直接启动名为 `node` 的可执行文件,必须使用 `process.execPath` 或提供自己的运行时。子进程继承 RunAsNode;worker 线程继承 Host 参数。Desktop 不模拟上游 OpenSSL 行为,也不自动重编译任意第三方原生扩展。
+Host 继承调用者的 PATH,不加入 Desktop 私有的 `bin` 目录,因此 PTC 和 agent shell 不会通过该目录解析内部启动器。`DSH_DESKTOP_NODE_EXECUTABLE` 仅为包安装注入。包脚本环境在 PATH 前添加一个小型 `node` shell 启动器,把参数转发给当前 Electron 可执行文件。这支持没有系统 Node 的 shell 生命周期脚本。Windows 上它是 `node.cmd`,并非替代的 `node.exe`;第三方代码若绕过 shell 直接启动名为 `node` 的可执行文件,必须使用 `process.execPath` 或提供自己的运行时。子进程继承 RunAsNode;worker 线程继承 Host 参数。Desktop 不模拟上游 OpenSSL 行为,也不自动重编译任意第三方原生扩展。
 
 Electron 的 Node 补丁和原生 ABI 属于发布兼容性责任。打包原生 smoke 在 PATH 不含系统 Node 的情况下验证 pnpm shell 脚本,并验证 Windows shell 终端输出、Koffi、Sharp 和 HTML 转换。Host smoke 加载共享 Cordis 的外部插件,通过真实 Web 应用提供其路由。各平台仍需完成签名和已安装应用验收;Windows 结果不能证明 macOS 兼容性。Windows Token 签名串行执行并保留首次失败,阻止排队任务重复提交被拒绝的 PIN。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.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-09-11-sandboxed-node-ptc-runtime.md
-2026-09-11-sandboxed-node-ptc-runtime.md: 242b3cfedb49b7ab60c47c6ee03005ddb821bb33
-2026-09-11-sandboxed-node-ptc-runtime.zh.md: d1ab47550e49129ee3e1fefbdfa54cda397374a3
+2026-09-11-sandboxed-node-ptc-runtime.md: f851775460c5bd01760d31552bde8c691807ec06
+2026-09-11-sandboxed-node-ptc-runtime.zh.md: 94f3a37eb702a97161c1d08cec531fcffb3460f2

+ 2 - 0
.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.md

@@ -14,6 +14,8 @@ The [PTC foundation](../feature/2026-06-15-ptc.md) remains responsible for regis
 
 `dsh-ptc-runtime-node` runs each program in one fresh Node process. The host resolves execution choices, confines the launch through the same `ctx.sandbox` provider as Bash, and gives process lifetime to `ctx.subprocess`. The child evaluates erasable TypeScript with direct Node APIs, an empty model environment and host-provided asynchronous bindings. No worker or persistent kernel remains inside this provider.
 
+The host preserves `ELECTRON_RUN_AS_NODE` for child startup; the bootstrap removes it from the native environment before evaluation, and model-visible `process.env` stays empty. Nested Electron launches require their own explicit Node-mode selection. Desktop uses Electron as its Node executable; removing this selector launches Electron's application path instead of the PTC bootstrap. Sandbox permission changes cannot repair that launch mismatch. The macOS Desktop regression uses real Electron to verify binding writes, direct workspace writes, and rejection of writes outside the workspace under restricted policy. It requires an installed Electron binary; ordinary runtime tests cover environment filtering without that dependency.
+
 ### Resolved inputs and policy
 
 `PtcRuntime.resolve(request)` validates supported options and supplies a complete `PtcRunSpec`; `run(spec)` does not introduce defaults. PTC passes the calling Session's cwd and resolved standing policy. Direct runtime callers receive deployment defaults through the same resolver. The filesystem and subprocess providers share one execution world, and bootstrap paths cross through the filesystem's explicit host-file mapping or a configured preinstalled bootstrap.

+ 2 - 0
.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.zh.md

@@ -14,6 +14,8 @@ Node worker 隔离 JavaScript 状态,但不应用调用 Session 的 OS 沙箱
 
 `dsh-ptc-runtime-node` 在一个全新 Node 进程中运行每个程序。Host 解析执行选择,通过与 Bash 相同的 `ctx.sandbox` 提供方约束启动,并将进程生命周期交给 `ctx.subprocess`。子进程以直接 Node API、空模型环境和 Host 提供的异步绑定求值可擦除 TypeScript。本提供方不保留 worker 或持久内核。
 
+Host 在子进程启动时保留 `ELECTRON_RUN_AS_NODE`;bootstrap 在求值前将其从原生环境中删除,模型可见的 `process.env` 仍为空。嵌套启动 Electron 需要自行显式选择 Node 模式。桌面端使用 Electron 作为 Node 可执行文件;删除此选择变量会启动 Electron 应用路径,而不是 PTC bootstrap。更改沙箱权限无法修复这一启动模式不匹配。macOS 桌面端回归测试使用真实 Electron 验证绑定写入、直接工作区写入以及受限策略对工作区外写入的拒绝。该测试需要已安装的 Electron 二进制文件;普通运行时测试无需此依赖即可覆盖环境过滤。
+
 ### 已解析输入与策略
 
 `PtcRuntime.resolve(request)` 验证支持的选项并补全 `PtcRunSpec`;`run(spec)` 不引入默认值。PTC 传入调用 Session 的 cwd 与已解析常设策略。直接运行时调用方通过同一解析器取得部署默认值。文件系统与子进程提供方共享一个执行世界,bootstrap 路径通过文件系统的显式宿主文件映射或配置的预安装 bootstrap 传递。

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.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-09-14-current-profile-plugin-management.md
-2026-09-14-current-profile-plugin-management.md: a59a79022bce88dcee7b1629ec35c9759489e34a
-2026-09-14-current-profile-plugin-management.zh.md: 7c3e8d470fcf87fb32cda8d9f68dd2ffe4b7fe15
+2026-09-14-current-profile-plugin-management.md: 8d4d4c40f033e490f09ebf8d367e1fb621037a6d
+2026-09-14-current-profile-plugin-management.zh.md: a8dce606d00321bd96e643a9ba842581c961d6c1

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.md

@@ -16,7 +16,7 @@ Configuration watches use Chokidar write stabilization by default. Its ordinary
 
 Profile files remain the persisted state: entry toggles edit only `disabled` in the last override matching the entry id and any module-name assertion, appending when none matches, and bundle toggles edit the ordered string list. Dependency updates do not reactivate retained disabled bundles. A service removal first applies the composition without the bundle and waits for old fibers to finish before deleting the dependency. Saved configuration, pnpm completion and runtime activation have separate outcomes; a failed removal preserves the actual partial state and a diagnostic path, while a failed or cancelled installation restores the profile files it snapshotted.
 
-This extends the [profile bundle composition decision](2026-08-05-profile-plugin-bundles.md). Profiles without HMR keep their process composition, and Desktop package management remains shell-owned. Web controls and explicitly enabled agent tools call the same service. Management operations return results to callers without adding messages to live Agents. The agent tool is disabled by default in the base bundle and shipped presets. The browser-only worker preview has no host package installer; its module-proxy table refuses `execa` calls explicitly while retaining the management module for inventory discovery.
+This extends the [profile bundle composition decision](2026-08-05-profile-plugin-bundles.md). Profiles without HMR keep their process composition, and Desktop package management remains shell-owned. Web controls and explicitly enabled agent tools call the same service. Management operations return results to callers without adding messages to live Agents. The agent tool is enabled in Creator mode and disabled by default in the base bundle and other shipped presets. The browser-only worker preview has no host package installer; its module-proxy table refuses `execa` calls explicitly while retaining the management module for inventory discovery.
 
 CLI calls inherit the terminal and authentication environment; service calls retain the subprocess credential scrub and bounded diagnostics. Management records carry error codes and parameters for locale-owned Web presentation. Reconciliation compares entry identity, fiber identity, configuration and diagnostics before and after updating: unchanged inactive entries remain warnings, while newly affected failures reject the operation. Explicit enablement targets must activate.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.zh.md

@@ -16,7 +16,7 @@ Web 和 Agent 控件需要修改运行中的 profile,同时避免另建包安
 
 profile 文件保持为持久状态:条目开关只修改最后一条符合条目 id 及模块名称断言的覆盖项中的 `disabled`,没有匹配项时追加,组合包开关修改有序字符串列表。更新依赖不会重新激活保留的已停用组合包。service 删除组合包时,先应用去掉该组合包的配置,等待旧 fiber 完成卸载后再删除依赖。已保存配置、pnpm 完成状态与运行时激活分别报告;失败的删除保留实际的部分状态与诊断路径,失败或被取消的安装则恢复它快照的 profile 文件。
 
-这扩展了[profile 组合包决策](2026-08-05-profile-plugin-bundles.zh.md)。startup profile 保留进程组合,Desktop 包管理仍由 shell 持有。Web 控件与显式启用的 Agent 工具调用同一 service。管理操作向调用方返回结果,不向存活 Agent 添加消息。base 组合包和内置预设默认禁用该 Agent 工具。纯浏览器 worker 预览没有宿主包安装器;其模块代理表明确拒绝 `execa` 调用,同时保留管理模块用于清单发现。
+这扩展了[profile 组合包决策](2026-08-05-profile-plugin-bundles.zh.md)。startup profile 保留进程组合,Desktop 包管理仍由 shell 持有。Web 控件与显式启用的 Agent 工具调用同一 service。管理操作向调用方返回结果,不向存活 Agent 添加消息。创造模式启用该 Agent 工具;base 组合包和其他内置预设默认禁用。纯浏览器 worker 预览没有宿主包安装器;其模块代理表明确拒绝 `execa` 调用,同时保留管理模块用于清单发现。
 
 CLI 调用继承终端和认证环境;service 调用保留子进程凭据清理与有界诊断。管理结果提供错误码和参数,由 Web 词典呈现文案。重载前后比较 entry、fiber、配置与诊断:未变化的已有故障保留为警告,本次影响到的新故障使操作失败。显式启用的目标必须成功激活。
 

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md
+2026-09-14-independent-libreoffice-kit.md: 13efc41e56a4810273fbb6d9314eeda6053dbe92
+2026-09-14-independent-libreoffice-kit.zh.md: 720ca2ba3f57546825ae14b5b5f0b086fbb7f287

+ 29 - 0
.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md

@@ -0,0 +1,29 @@
+# Agent Note: Independent LibreOffice kit ownership
+
+Status: implemented
+
+English | [中文](2026-09-14-independent-libreoffice-kit.zh.md)
+
+## Problem
+
+LibreOffice compilation, source patches, platform qualification, and large binary releases have a different maintenance cycle from Harness plugins. Keeping them in the application workspace expands routine CI and couples engine repairs to monorepo package rules.
+
+## Decision
+
+The `deepseek-harness/libreoffice-kit` repository owns the reusable `@deepseek-ai/libreoffice-kit` Node API, its Worker, font handling, engine selection, build recipes, patches, tests, and releases. The API has no Cordis dependency. Harness owns the adapter from its `OfficeToPdf` service to this API, Session authorization, conversion lifetime, transport, Web UI, and application packaging.
+
+Kit releases run independently of Harness releases. The kit repository qualifies and publishes the Node API and engine npm packages at a shared version, starting at `0.0.1`. Harness consumes an exact npm version and commits its dependency resolution in `pnpm-lock.yaml`; Harness releases neither build nor publish kit packages.
+
+The upstream API declares platform engines as optional dependencies. The [platform engine decision](2026-09-15-platform-office-engines.md) supersedes the original required-WASM fallback policy. Desktop installs the kit as an external npm dependency. Python sidecars keep the Worker, selected engine, and their dependency closure on the real filesystem, outside the executable’s virtual filesystem. Conversion requires neither downloads nor GitHub credentials.
+
+The kit repository owns engine qualification and corresponding source materials. MPL-2.0 declarations, source availability, and redistribution notices accompany the API and engines; Harness retains these materials when packaging them. The notices check accepts the exact API, WASM, macOS ARM64/x64, and Windows ARM64/x64 package identities only at MPL-2.0; unrelated packages and changed license terms still reject.
+
+## Alternatives considered
+
+**Co-locate the public API and Core build in Harness.** This synchronizes source changes but makes application maintenance own long engine builds and special package rules. The Cordis provider is the application integration point; the reusable conversion API belongs with its engine tests.
+
+**Prepare GitHub Release archives before installation.** This requires separate authentication, hashes, decompression, workspace overrides, and distribution repacking. Published npm packages use the application's ordinary dependency installation and platform selection.
+
+## Consequences
+
+Changing the kit requires qualifying a release in its own repository and updating the Harness dependency versions and lockfile. A missing required npm package fails installation; Harness does not compile an engine to recover. Native and WASM conversion smokes validate the installed packages, while Desktop and Python checks cover application packaging.

+ 29 - 0
.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md

@@ -0,0 +1,29 @@
+# Agent Note: 独立 LibreOffice kit 的归属
+
+Status: implemented
+
+[English](2026-09-14-independent-libreoffice-kit.md) | 中文
+
+## Problem
+
+LibreOffice 编译、源码补丁、平台资格验证和大型二进制发布的维护周期不同于 Harness 插件。将它们放在应用工作区会扩大常规 CI,并让引擎修复依赖 monorepo 包规则。
+
+## Decision
+
+`deepseek-harness/libreoffice-kit` 仓库维护可复用的 `@deepseek-ai/libreoffice-kit` Node API、Worker、字体处理、引擎选择、构建配方、补丁、测试和发布。API 不依赖 Cordis。Harness 负责将自己的 `OfficeToPdf` 服务适配到此 API,以及 Session 授权、转换生命周期、传输、Web UI 和应用打包。
+
+kit 发布流程独立于 Harness 发布流程。kit 仓库验证并以统一版本发布 Node API 和引擎 npm 包,起始版本为 `0.0.1`。Harness 消费精确的 npm 版本,并在 `pnpm-lock.yaml` 中提交依赖解析结果;Harness 发布既不构建也不发布 kit 包。
+
+上游 API 将平台引擎声明为可选依赖。[平台引擎决策](2026-09-15-platform-office-engines.zh.md)取代最初强制携带 WASM 回退引擎的策略。Desktop 将 kit 作为外部 npm 依赖安装。Python sidecar 将 Worker、所选引擎及其依赖闭包保留在真实文件系统中,位于可执行文件的虚拟文件系统之外。转换无需下载或 GitHub 凭据。
+
+kit 仓库负责引擎资格验证和对应源码材料。API 和引擎携带 MPL-2.0 声明、可访问源码及再分发声明;Harness 打包时保留这些材料。再分发声明检查仅在许可为 MPL-2.0 时接受 API、WASM、macOS ARM64/x64 和 Windows ARM64/x64 的精确包标识;无关包和改变后的许可条款仍被拒绝。
+
+## Alternatives considered
+
+**将公开 API 和 Core 构建放在 Harness。** 这能同步源码变更,却让应用维护承担耗时的引擎构建和特殊包规则。Cordis provider 是应用集成点;可复用转换 API 应与引擎测试放在一起。
+
+**安装前准备 GitHub Release 归档。** 这需要单独的鉴权、哈希、解压、工作区 overrides 和分发重打包。已发布的 npm 包使用应用常规的依赖安装与平台选择流程。
+
+## Consequences
+
+更新 kit 需要在其独立仓库验证并发布新版本,再更新 Harness 依赖版本和锁文件。必需的 npm 包缺失时安装失败;Harness 不通过编译引擎恢复。原生和 WASM 转换冒烟验证已安装的包,Desktop 与 Python 检查覆盖应用打包。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.md
+2026-09-15-bounded-office-conversion.md: 75e61ce0c49dafac68d4cf3a653d13cbee096e4c
+2026-09-15-bounded-office-conversion.zh.md: a2accf02ec058fa90b83e36bc761a2b7637eaef9

+ 37 - 0
.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.md

@@ -0,0 +1,37 @@
+# Agent Note: Bounded shared Office conversion
+
+Status: implemented
+
+English | [中文](2026-09-15-bounded-office-conversion.zh.md)
+
+## Problem
+
+Office preview and explicit document inspection can request the same conversion. A completed-result cache alone leaves source reads, queued payloads, and concurrent readers unbounded. Speculation can also occupy capacity needed by a user, and canceling one consumer must not destroy another consumer's conversion.
+
+## Decision
+
+The `office-to-pdf` service returns complete PDF bytes. Page rasterization and user presentation remain separate consumers, so conversion naming does not imply image rendering or preview UI.
+
+The [Host provider](../../../../packages/document/office-to-pdf/README.md) owns a shared conversion queue and transient content cache. Authorized source metadata enters admission before source bytes are loaded. The source callback receives reserved byte capacity and returns its read version; changed sources fail without publishing aliases. Exact source bytes and Office extension determine the digest. Each converter lifetime adds a generation so engine/font/configuration replacement invalidates reuse.
+
+A bounded source-version index avoids repeated reads after authorization; the digest remains the identity for sharing conversion across distinct paths. Ready PDFs use an entry/byte-bounded LRU. Queued jobs contain metadata and deferred callbacks. Reader, queue, source-byte, and conversion limits also apply before work completes. Each active source locator belongs to live readers, so cancellation cannot grow retained source metadata independently of reader admission. Unknown source sizes reserve the input cap; cancellation retains active capacity until actual read/conversion cleanup settles.
+
+Foreground preview and explicit QA requests precede background work. Disabling background conversions rejects speculative readers before they can join queued or running jobs, while completed alias hits remain available. Removing a queued foreground blocker immediately admits eligible background work. Queued priority follows live readers: a foreground join promotes a prewarm, and the final foreground cancellation demotes it and admits eligible work. Full queues evict queued speculation for foreground admission. Background concurrency reserves a foreground slot when total concurrency permits it. A speculative admission remains occupied through settlement, so promotion cannot admit additional speculation into capacity reserved for user work. Shared readers cancel independently, including readers joined after content hashing. The cache owns private output bytes and returns a copy to each caller.
+
+## Alternatives considered
+
+**Cache only completed PDFs.** This cannot bound pending source buffers, engine work, or response fanout, and separate consumers still duplicate conversion.
+
+**Use source metadata as the final cache identity.** Metadata is useful before reading, but distinct authorized paths can contain identical bytes. Content hashing provides cross-path reuse without treating a path as document content.
+
+**Cancel the entire conversion when one reader leaves.** An open preview can share work with speculation or explicit QA. Only the final reader owns cancellation of shared work.
+
+**Build a second prewarm or QA converter.** Independent queues duplicate resource ownership and cannot prioritize shared foreground work.
+
+**Separate service-definition and provider packages for the sole LibreOffice implementation.** They evolve together and have no independent alternative implementation. One `office-to-pdf` package supplies the mountable service without duplicated package, dependency, and release configuration. Native and WASM engine selection remains inside the kit; a second independent implementation can justify extracting an interface from actual consumer needs.
+
+## Consequences
+
+The cache is transient and cannot bypass source authorization. Oversized PDFs can be returned without retention, and failed or canceled conversions are retried on a later explicit request. Source reservations measure binary bytes; Remote base64 expansion, engine RSS, caller-retained output, and PDF.js page memory remain outside those limits. With one configured conversion slot, foreground work waits for an already-running background conversion to finish.
+
+Controlled source and engine completions verify pre-read admission, content joining, priority, cancellation isolation, delayed resource release, LRU/alias limits, stale versions, and converter replacement. Loader composition and native conversion checks exercise the shared provider independently of presentation consumers.

+ 37 - 0
.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.zh.md

@@ -0,0 +1,37 @@
+# Agent Note: 有界的共享 Office 转换
+
+Status: implemented
+
+[English](2026-09-15-bounded-office-conversion.md) | 中文
+
+## 问题
+
+Office 预览和显式文档检查可能请求相同转换。仅缓存已完成结果无法限制源读取、排队载荷和并发读取方。推测工作也可能占用用户需要的容量,而取消一个消费者不能破坏另一个消费者的转换。
+
+## 决策
+
+`office-to-pdf` 服务返回完整 PDF 字节。页面栅格化和用户展示由独立消费方负责,因此转换命名不隐含图片渲染或预览 UI。
+
+[宿主提供方](../../../../packages/document/office-to-pdf/README.zh.md)拥有共享转换队列和临时内容缓存。已授权的源文件元数据在加载字节之前进入准入流程。源回调接收预留的字节容量并返回读取版本;源文件变化会导致失败,不发布别名。确切的源字节和 Office 扩展名决定摘要。每个转换器生命周期附加代次,因此引擎、字体或配置替换会使复用失效。
+
+有界的源版本索引在授权后避免重复读取;摘要仍是不同路径间共享转换的身份。已就绪 PDF 使用按条目与字节限制的 LRU。排队任务包含元数据和延迟回调。读取方、队列、源字节与转换限制在工作完成前也适用。每个在途源定位信息归属于活跃读取方,因此取消操作不能让保留的源元数据脱离读取方准入限制增长。未知源大小预留输入上限;取消后仍保留活动容量,直至实际读取、转换与清理结束。
+
+前台预览和显式 QA 请求优先于后台工作。禁用后台转换时,推测读取方在加入排队或运行任务前即被拒绝,但仍可命中已完成的别名缓存。移除排队的前台阻塞任务后,符合条件的后台工作立即准入。排队优先级取决于活跃读取方:前台加入会提升预热,最后一个前台读取方取消后则恢复后台优先级,并准入符合条件的工作。队列满时为前台准入移除排队推测工作。总并发允许时,后台并发为前台预留一个槽位。推测工作的准入名额保留到工作结束,因此提权不会把为用户预留的容量用于更多推测工作。共享读取方独立取消,包括内容哈希后加入的读取方。缓存拥有私有输出字节,并为各调用方返回副本。
+
+## 考虑过的替代方案
+
+**仅缓存已完成 PDF。** 无法限制待完成源缓冲区、引擎工作和响应扇出,不同消费者仍会重复转换。
+
+**以源元数据作为最终缓存身份。** 元数据在读取前有用,但不同的已授权路径可能包含相同字节。内容哈希提供跨路径复用,而不把路径当作文档内容。
+
+**一个读取方离开就取消整个转换。** 打开的预览可能与推测工作或显式 QA 共享工作。只有最后一个读取方拥有共享工作取消权。
+
+**另建预热或 QA 转换器。** 独立队列重复拥有资源,无法优先调度共享前台工作。
+
+**为唯一的 LibreOffice 实现拆分服务定义和提供方包。** 两者共同演化,没有独立的替代实现。单个 `office-to-pdf` 包直接提供可挂载服务,避免重复维护包、依赖和发布配置。原生与 WASM 引擎选择仍由 kit 负责;若出现独立的第二种实现,再根据实际消费者提取接口。
+
+## 后果
+
+缓存为临时数据,不能绕过源授权。超出缓存上限的 PDF 可返回而不保留;失败或取消的转换在后续显式请求时重试。源预留按二进制字节计量;Remote base64 膨胀、引擎 RSS、调用方保留的输出和 PDF.js 页面内存不计入这些限制。仅配置一个转换槽位时,前台工作等待已运行的后台转换结束。
+
+受控的源读取和引擎完成验证读取前准入、内容合并、优先级、取消隔离、延迟资源释放、LRU 与别名限额、过期版本及转换器替换。Loader 组合与原生转换检查独立于展示消费者验证共享提供方。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-15-client-session-references.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-15-client-session-references.md
+2026-09-15-client-session-references.md: 2d727809b237ca919296dac82a3b201ba2dd8fbf
+2026-09-15-client-session-references.zh.md: 34fa6526a57f43fd7636c68c7ed72bb982a129f2

+ 226 - 0
.agents/notes/implemented/architecture/2026-09-15-client-session-references.md

@@ -0,0 +1,226 @@
+# Agent Note: Client Session references, reference sources, and UI status
+
+Status: implemented
+
+English | [中文](2026-09-15-client-session-references.zh.md)
+
+## Problem
+
+The Session catalog, live Client objects, views, and asynchronous operations have different lifetimes. Catalog membership does not establish ongoing use. A borrowed binding cannot protect asynchronous work or distinguish successive Client generations with the same Session id. A global current Session makes independently bound components act on another view's Session.
+
+A reference count identifies ongoing use but not its consumers. Main-area highlighting, Sidebar ownership, and background operations need consumer-source information. Pending interactions and completion reminders also need one UI read interface so Workspace and Conversation do not independently combine the same status.
+
+Client history access and Host Agent execution have independent lifetimes. History opening can fail; local Context acquisition need not read history. Explicit ownership must preserve navigation, loading, error handling, and recovery behavior without adding unrelated UI policy.
+
+## Decision
+
+### Scope and ownership
+
+Client Session objects, Agent-scoped Client Contexts, references, consumer-source metadata, UI status, and explicit Provider composition use the ownership rules below. Host Session and Agent lifetimes, SlotFactory, activity dashboards, durable Session formats, and both SDKs' Host protocols remain independent.
+
+| Owner | Responsibility |
+| --- | --- |
+| Client Session Controller | Catalog, live generations, references, source counts, bindings, history windows, and existing Session control state |
+| `ui-session` | Explicit Session Provider integration and the unified UI status source |
+| View or operation | Its own reference, target, source identifier, and release point |
+| Workspace UI | Main-area target and reference, navigation, persisted target, and creation flow |
+| UI composition boundary | Supplies an owner-provided reference to one explicit `SessionProvider` |
+| Conversation and Sidebar subtrees | Consume only their Provider's Session, never a main-area reference or global selection |
+| Client Gateway | Invocation-lifetime Context ownership while handling a Host event |
+
+The [Client layering design](../../implemented/architecture/2026-08-20-client-session-conversation-ownership.md) defines one-way data, adapter, renderer, and presentation dependencies. Reference-source bookkeeping does not give the Controller a dependency on UI packages.
+
+This decision partially supersedes the list-selected scope lifecycle in the [Web Client Session scope and provide-channel decision](2026-07-25-web-client-session-scope-and-provide-channel.md); that note retains the blank-Session and adoption rationale under explicit Provider ownership.
+
+### Addresses, bindings, and references
+
+`SessionTarget` is a known `SessionId` or a durable direct-parent `SubagentAddress`. It identifies what to acquire; it owns nothing. The Controller resolves an explicit address without requiring a preloaded parent catalog, while the Host validates its parent, child, and mode when history opens. Child discovery and an already known child address remain distinct from retaining the child. Navigation uses the same target representation rather than adding a separate navigation address.
+
+`SessionBinding` is the shared Client generation: `sessionId`, Session face, event source, and scoped Context. Multiple references to the same live generation share this binding. A new generation with the same id has a different binding and Context.
+
+`SessionReference` owns one use of one exact generation. It exposes read-only `sessionId` and `binding`, a `ready` Promise for the shared initial history opening, plus idempotent `release()` and `Symbol.dispose`. `ready` resolves to the exact binding when the corresponding `Session.open()` attempt resolves, including its stateful Remote-failure result. Reading `binding` after release or generation disposal fails. Retaining, releasing, or inspecting a reference does not create durable Sessions or start, stop, or retain a Host Agent.
+
+`SessionBinding` is a borrowed value and owns no lifetime. Only unreleased `SessionReference` objects contribute to source and total reference counts. Synchronous code may borrow a binding within its owner's reference lifetime; work that outlives that owner must acquire its own reference.
+
+| API | Result and caller obligation |
+| --- | --- |
+| `sessions.retain(target, options)` | `SessionReference`; returns immediately, and the caller owns the reference until release |
+| `sessions.using<T>(target, options, operation)` | `Promise<T>`; awaits `reference.ready`, awaits `operation(reference)`, releases its reference, and returns the operation's result |
+| `sessions.retainInfo(id)` | Stable read-only observable of local reference counts; no acquisition, scope creation, or history I/O |
+| `sessions.scope(id)` / `sessions.binding(id)` | Borrow an existing live generation or return `undefined`; neither opens nor extends its lifetime |
+| `sessions.sessionOf(ctx)` | Returns the matching live Session face or `undefined`; an ended Context cannot resolve to a same-id replacement |
+| `sessions.create(...)` / `sessions.fork(...)` | Existing Host operations returning a Session identity; retaining and displaying it remain explicit |
+
+`SessionRetainOptions` contains required `source: SessionReferenceSource` and optional `signal: AbortSignal`. Both acquisition methods accept `target: SessionTarget` and these options. Source keys are consumer-defined and declaration-merge extensible; there is no runtime source-registration protocol or default main-view source. A source is a usage label, not another Session address or a permission to act on it.
+
+### Acquisition, failure, and release
+
+Acquisition synchronously resolves the target, creates or retains the local Session, Context, fiber, and binding, records one reference and its source contribution, starts the generation's shared initial history opening, and returns the reference. Concurrent acquisitions share that opening but receive independent references and readiness waits. `reference.ready` resolves to the still-live exact binding when the shared `Session.open()` attempt resolves.
+
+Unknown Session-id addressing fails before a reference is returned; an explicit subagent address is validated by the Host during opening. A thrown opening failure, caller cancellation, reference release, or generation retirement rejects that reference's `ready`; one cancelled waiter does not cancel another owner's shared opening. A Remote failure represented by `openState: 'error'` follows `Session.open()` and resolves readiness, leaving the error renderable through the binding. The caller still owns a returned reference until release, while `sessions.using` releases its reference when readiness or its operation fails.
+
+`using reference = sessions.retain(target, options)` releases on exit from the enclosing scope; code that requires settlement of the initial history attempt awaits `reference.ready`. `sessions.using` is the callback helper: it waits for that settlement before invoking an operation, waits for its returned value or Promise, and then releases. Returned values must not rely on the helper's released reference remaining usable; a longer-lived consumer acquires its own reference. The helper propagates rejected readiness and operation failures without a fallback result, retry, or error presentation. Model-selection generation checks and error-state updates remain in ModelSelection.
+
+A fulfilled `ready` Promise means the initial `Session.open()` attempt settled; it does not assert `openState: 'open'` or promise an uninterrupted connection. Stateful opening failures and later stream failures remain observable through existing Session state. Callers retain their existing handling and presentation of those errors.
+
+Release removes only that reference and its source contribution. Final release withdraws the generation's admission and id mappings before disposing the Session and fiber. A later retain can create a fresh generation immediately; cleanup of the old generation cannot remove the replacement. `release()` initiates local cleanup synchronously, while root disposal awaits outstanding asynchronous teardown.
+
+The owning Client root invalidates all references during shutdown. It refuses new acquisitions, withdraws live mappings, and joins scoped cleanup and Session stream teardown. References cannot keep a disposed Client root alive. Catalog removal alone does not dispose a referenced generation.
+
+### Reference sources and Session list records
+
+The Controller owns source counts alongside each live generation's references. For every source, the count equals that generation's unreleased references bearing the source. A Session can have several sources, and a source can hold several references. Sources have no special lifecycle behavior in the allocator.
+
+Each available Session list row exposes a read-only `retainedBy` record from source key to positive reference count. An unretained row has an empty record; zero-count keys are absent. Acquisition failure and release update that projection, including removal of the final key. The source counts remain local facts when Host metadata is refreshed; Host responses cannot overwrite them.
+
+The generation owns the counts even if its Session has not reached the Host catalog or its catalog metadata has been removed. `byId` synthesizes a local fallback row for every live generation so Provider and ownership consumers can resolve it; `ids` remains the Host-list membership and ordering. A fallback row is not Host metadata. Every row's `retainedBy` projection uses the live generation's counts. Neither the reference objects nor the counts are persisted or sent to the Host.
+
+For example, `retainedBy = { mainView: 1, gateway: 2 }` means that the main view and two Host invocations own three references. Releasing the main-view reference removes only `mainView`; the Gateway invocations continue to own the generation. Source names in this example are consumer keys, not a closed Controller enum.
+
+`current` means main-view occupancy: a row has that marker exactly when its `mainView` source count is positive. It is one use of the general source record, not a separate `sessions.current` value or Session-selection service. Other consumers can derive their own markers from their source keys. The reference system does not impose a globally unique consumer or select one Session from multiple sources.
+
+The main-area owner manages its own target and reference transitions. The list does not infer selection from how many Providers happen to be mounted. Source records report actual ownership, including temporary overlap during acquisition; UI navigation remains responsible for its target, without assigning selection authority to the allocator.
+
+Window-level consumers may inspect source markers. Session business operations still use their supplied scope or explicit reference; they cannot find `mainView` in the catalog to recover a missing operation target. A background reference is ownership evidence, not evidence that a user viewed the Session.
+
+`SessionRetainInfo` contains `referenceCount` and the read-only `retainedBy` record. `sessions.retainInfo(id)` observes that local ownership independently of catalog membership and remains stable across same-id generation replacement. An identity without a live generation has zero references and an empty source record; reading this value does not assert that the identity exists on the Host.
+
+`ui-session` exposes `useSessionRetainInfo(sessionId, selector)` for an explicit identity and `useSessionRetainInfo(selector)` for the Session bound by the surrounding Provider. An unbound scope supplies absence to the latter form, never the main area's Session. The renderer constructs both forms from the same bare retain-info source. Consumers test the `mainView` source count to recognize the former current-Session role; other source keys remain equally queryable. Reading or subscribing does not retain the Session.
+
+### Unified UI Session status
+
+`ui-session` owns a React-free `sessionStatus` source and exposes it through the standard `useSessionStatus` hook. The snapshot is indexed by Session identity and supplies one UI status record per known Session. It combines these independent facts rather than reducing them to one mutually exclusive phase:
+
+| Field | Value | Meaning and owner |
+| --- | --- | --- |
+| `running` | `boolean` or `undefined` | Latest known Session running fact; absence of a baseline is not confirmed idle |
+| `pendingInteraction` | `SessionPendingInteraction` or `undefined` | Effective domain-owned request, or absence when no request is pending |
+| `completionUnread` | `boolean` | UI reminder for an observed stop that has not been acknowledged |
+
+Source counts remain authoritative in `SessionListState.byId[id].retainedBy`; UI status reads them for acknowledgement policy without owning another reference registry. Titles, Workspace associations, history, queues, and projection data remain with their existing owners.
+
+Pending domains retain `SessionPendingInteractionMap`, request identities, precedence, publication disposers, and teardown delegation. The unified status includes the same effective request object; it does not copy requests or create a second pending registry. Workspace status indicators and Conversation composer selection read `useSessionStatus` instead of separate `useSessionPendingInteraction` and `useCompletedSessionIds` hooks.
+
+Completion tracking subscribes to the existing `api-session/status` events so a running-to-idle transition is not lost in batched catalog snapshots. Catalog snapshots establish initial and reconnect baselines. A pending empty catalog is not evidence that Sessions disappeared. The update rules are:
+
+- An initial idle baseline does not create a completion reminder.
+- Observing running clears an earlier reminder and records the running baseline.
+- A known running-to-idle transition sets `completionUnread` only when the Session lacks main-view ownership.
+- Acquiring main-view ownership clears the reminder; retaining from an unrelated source does not.
+- Releasing main-view ownership does not manufacture a reminder for an earlier stop.
+- Removing a Session clears its completion reminder and running baseline; pending-request teardown stays with the request's domain.
+
+The reminder denotes an observed stop, not successful task completion or completion of a particular queued message. Main-view ownership preserves selection-based acknowledgement even while a global panel temporarily hides the Conversation. The design has no `ui-session/view-presence` event, mounted-Provider index, or implicit acknowledgement from Provider mount.
+
+The Session Controller publishes running and reference-source facts but owns no completion-reminder set, `consumeCompletion` method, or pending-interaction presentation. Reference acquisition does not execute completion-reminder business logic.
+
+### Explicit Providers and consumer lifetimes
+
+The single `SessionProvider` can inherit an outer binding or override its subtree with explicit `session={reference | undefined}`. It neither acquires nor releases ownership. The root Provider resolves its main binding from the `mainView` ownership marker without maintaining another current value; sibling and nested Providers affect only their own subtrees. Explicit absence stays absent instead of falling back to the main area.
+
+The Provider injects the selected binding without keying its whole body. Strict `session` entries remount when the binding Context changes. A blank `session-maybe` entry adopts its first binding without remounting; after adoption, another binding Context or a return to absence starts a new component incarnation.
+
+`uiWorkspace` owns the main-area reference with source `mainView`, and `ui-session` derives the root Provider from that reference's ownership marker on the Session record. Conversation, right Sidebar, preset, command, input, and model components beneath a Provider consume only its standard bound data. They do not read the main-area reference or a global selection, and they do not reinterpret an ID as a binding from another Provider.
+
+Each Provider occurrence establishes an independent rendering scope from its supplied `SessionReference`. Two references may identify different Sessions or share one `SessionBinding`; different bindings isolate business and view data, while Providers for the same binding share Session, Conversation, input, and other Session-level data but retain separate component-local state. The main area and Sidebar can mount two Conversations concurrently without either Provider replacement or teardown redirecting the other subtree.
+
+`ui-session` reuses one stable business observable per active `SessionBinding`. Generation caches for Conversation assembly, input shells, command popups, input-trigger controllers, and model directories use binding identity instead of Session IDs. Caches that must enumerate live values use `WeakMapWithValues<SessionBinding, Value>`, whose weak key table and strong value set provide identity lookup and value iteration. The container performs no cleanup; subscriptions, controllers, URLs, and other resources still release deterministically through `binding.ctx.effect()`. Provider-occurrence view state ends with that rendering scope, while final generation cleanup belongs to the binding Context.
+
+Descriptor changes assemble replacement sources before publication without recreating Session generations. A Provider validates its reference while reading it; a released reference, a reference from another Controller, or a reference that no longer matches an active binding cannot establish a scope.
+
+| Consumer | Acquisition and release |
+| --- | --- |
+| Main Conversation | `uiWorkspace` acquires the navigation target; `ui-session` establishes the root Provider from the `mainView` marker, and the Conversation subtree consumes only its Provider binding |
+| Associated right Sidebar | `RightbarRoot` inherits the root Provider without reading the main-area reference |
+| Independent Session view | Its owner retains the target and establishes a Provider from its own reference alongside the main area |
+| Host event handler in the Client | Holds a local Context reference through handler and reply settlement |
+
+Conversation references belong to their view owners, not Chat, Trajectory, or an individual action. Chat, Trajectory, commands, input, uploads, image reads, and model selection borrow the same binding during the Provider lifetime rather than acquiring a reference per action. Switching or closing that Conversation can end its in-flight local work. Sidebar-tab layout and resource ownership remain separate; only a Sidebar view that hosts a Conversation needs its own Session reference. Empty layouts and guide placeholders retain nothing.
+
+Scoped business objects capture their binding before awaiting work. They do not reinterpret an old Context or directory as a new generation with the same id. ModelSelection borrows its Provider binding while retaining its own selection-generation and error rules. Editor-detach callbacks may overlap scope teardown; optional trigger and popup resolution returns absence for that retired Context rather than resolving another generation.
+
+### Main-area navigation and presentation
+
+`uiWorkspace.openSession`, `openWorkspace`, `forkSession`, and `startSession` remain the navigation entry points. They accept or resolve explicit targets, change the main view, and return the main area to Conversation according to existing navigation policy. `retain` itself never navigates. There is no `registerNavigation` receiver protocol or second navigation service introduced by reference ownership.
+
+The existing `uiWorkspace` implementation directly owns the main-area reference and target. Its navigation methods update that owner rather than calling a receiver registered by Conversation. `ui-session` derives the root Provider binding from the source marker; the main Conversation and associated right Sidebar only inherit the Provider and neither depend on `uiWorkspace` nor see the main reference. An independent Sidebar Conversation establishes a nested Provider from its own reference and overrides only that subtree's binding. The main reference is not a global standard prop, subtree Hook, or default value for ID-based lookup.
+
+The main view privately persists its target identity and subagent address under `dsh.sessions.current`, never a reference. Startup restoration, initial Workspace selection, and clearing an archived main target remain UI responsibilities. Archiving or removing catalog metadata does not revoke independent references held by other consumers.
+
+| UI behavior | Final rule |
+| --- | --- |
+| Session-list highlight and blank-row treatment | Derive main-area occupancy from `retainedBy.mainView`, not the number of mounted Session Providers |
+| New Session Workspace | Explicit Workspace first, then the main Session's Workspace under the existing lookup rule, then the existing recent-Workspace policy |
+| Onboarding | Evaluate absence or blankness of the main-area Session, not all historical Sessions |
+| Browser document title | Conversation shows Session title plus product title; a global panel shows product title |
+| Chat/Trajectory restoration | Restore the view for the explicitly selected main target; independently bound views keep their own state |
+| Cordis inventory panel | One list without current/other grouping; no public runner getter for main-area selection |
+
+Source metadata does not change when DOM focus moves or when a global panel hides a retained view. The [global main-panel design](../../implemented/architecture/2026-09-08-global-main-panels.md) owns panel selection and layout; Session reference ownership does not replace it.
+
+Conversation retains its `hero`, `settling`, and `active` composition and existing history-loading and `openError` handling. Acquisition adds no outer loading/error phase presentation, extra composer-hiding condition, Retry button, or replacement Sidebar recovery panel. Existing error handlers continue to handle their errors; call sites without error presentation gain none. Promise rejection and correct reference release do not imply an additional UI handler.
+
+Workspace connection and fork preserve their existing navigation guards and panel-switch invalidation. Direct Session opening gains no additional global-navigation cancellation policy. Agent Team refresh preserves its originating-selection validity condition instead of starting a global navigation token before refresh. Reference acquisition does not broaden cancellation to unrelated navigation or running operations, and local cancellation does not roll back Host effects.
+
+### Presets and creation flows
+
+Preset directories and deployment defaults may be shared. A bound Session's preset is read or changed through its Provider binding; preset controllers are cached by `SessionBinding` rather than managed by one root current-Session follower. The hero preset seat uses the `session-maybe` Provider: it shows the creation-flow choice without a Session and operates on the exact bound blank Session after one arrives. Header labels read the same Provider-bound Session projection.
+
+A preset chosen before Session creation remains in the main Conversation's `session-maybe` preset surface. After Workspace creation or reuse establishes the main Provider over a blank Session, that surface applies the choice to its Provider-bound Session. When Settings changes the default preset or picker setting, the preset service selects the blank Session whose established Provider binding carries `mainView` ownership and updates that Session. Non-blank main Sessions, independent Sidebar Providers, and other background references remain unchanged; preset subtrees do not read the main reference or use a global current follower to find their target.
+
+### Host-event Context ownership and Typert
+
+A validated Host waterfall identity can arrive before catalog discovery. The Client Context resolver must remain synchronous. It acquires a local generation reference with the Gateway's source and returns `TypertOwnedValue<Context>` without opening history or refreshing child catalogs. A handler that needs history separately acquires a public reference and awaits its `ready` Promise, or uses `sessions.using`.
+
+Gateway owns the local reference until both handler use and reply settlement end. Context acquisition failures retain the existing report-and-delegate behavior, while handler failures produce rejected replies. Cancellation reaches the handler through its existing signal and suppresses late replies without releasing a Context still in use. Plugin shutdown joins the active connection generation's outstanding handlers; Connection starts no replacement generation until that source settles.
+
+`TypertOwnedValue` carries a value and its disposer through the generic Gateway. It has no additional reference count and does not teach Gateway about Session-specific ownership. Independently bundled Client modules share the owned-value marker. Client outgoing `identity(ctx)` remains synchronous; Host Context resolution and Host lifecycle are unchanged.
+
+## Alternatives considered
+
+**Catalog membership as ownership.** Discovery data cannot establish ongoing view or operation use, and some Context identities arrive before catalog membership.
+
+**Implicit main Session plus an explicit alternative.** Two target-resolution rules make reusable components depend on where they render. Explicit Providers supply the target, while generic source records serve window-level observation.
+
+**Provider subtrees reading `mainSession`.** A component would then have both a Provider target and a window-level target, so an independent Sidebar Conversation could be redirected by a main-area change. The main reference participates only in Provider assembly; subtrees consume their Provider.
+
+**Session ID as a generation-cache key.** Final release allows a replacement generation with the same ID to appear before old cleanup ends. An ID key can reuse the old object or let old cleanup remove the replacement. Business caches use weak binding identity, and the binding Context still owns resource cleanup.
+
+**Asynchronous `retain` that resolves only after history opens.** It delays Provider installation and main-view navigation until history arrives, so the existing loading state cannot render immediately. A synchronous reference separates ownership from its explicit `ready` result.
+
+**History I/O in synchronous Context resolution.** Host-event dispatch needs a scoped lifetime, not necessarily a history window; coupling them delays or blocks handlers before catalog discovery.
+
+**A dedicated current flag.** One consumer-specific flag cannot describe Sidebar and background ownership. A main-view marker is derivable from the general per-source counts.
+
+**Completion state in Session Controller, or separate pending and completion hooks.** Completion acknowledgement is UI policy. One UI status source composes independent facts while retaining domain-owned pending objects and Controller-owned running facts.
+
+**Provider presence as acknowledgement or release.** Mounting is neither an owner's lifetime nor selection-based acknowledgement. It cannot decide which background or temporarily hidden uses remain alive or count as viewed.
+
+**Bare ids in Providers or a global binding revision.** An id does not express generation ownership, and global refresh invalidates unrelated Session consumers.
+
+**A reference for every action.** The Provider already defines the Conversation's usage lifetime. Reacquiring for every click fragments view ownership into fine-grained sources. Only work that must outlive the Provider acquires another reference.
+
+**Navigation registration, new cancellation policies, and acquisition-specific recovery UI.** Reference ownership requires explicit targets, release, and failure propagation, not additional navigation or recovery behavior.
+
+**Client references retaining Host Agents.** History access and Host execution are independent uses; a long-lived Client view cannot define Host business-operation lifetime.
+
+## Verification
+
+- Acquisition tests cover shared initial opening, ordinary and unexpected open failures, independent waiter cancellation, exact-generation replacement, and root teardown reaching quiescence.
+- Source tests cover multiple sources, several references from one source, failed acquisition rollback, idempotent release, catalog refresh/removal, and late release of an ended generation without changing its replacement.
+- Retain-info hook tests cover explicit identities, Provider-bound defaults, unbound and nested scopes, source updates across generations, and reads that create neither references nor history requests.
+- Scope and Gateway tests cover synchronous Context acquisition before catalog discovery without history I/O, reported acquisition delegation, rejected handler replies, ownership through cancellation and settlement, and shared owned-value markers across bundles.
+- UI status tests cover pending precedence and teardown, event transitions lost by snapshot batching, initial and reconnect baselines, main-source acknowledgement, unrelated-source retention, and no Provider-presence event.
+- View tests cover explicit Providers for two different Sessions and for repeated uses of one Session, scoped presets, descriptor updates, and Sidebar occurrence/adoption lifetimes without new recovery controls.
+- Assembled browser scenarios preserve main-area highlighting, blank rows, onboarding, Workspace defaults, titles, and the defined navigation/error behavior. Existing recorded model turns remain the behavioral input when only Client ownership changes.
+- The public `retain` and `using` consumers, generated API catalogs, package contracts, and type checks agree on options and ownership. The helper keeps references through callback settlement and propagates both acquisition and operation failures.
+
+## Consequences
+
+Consumers must keep references until their real completion point; an unreleased reference still leaks within a live Client root. Retaining after final release creates a new generation and can reopen history. Source counts describe ownership, not visibility, task success, or authority; using the main marker as a business fallback would recreate implicit current-Session coupling.
+
+Final release discards non-persisted binding-owned state, including loaded history pages, Chat scroll anchors, preview wrapping, Files expansion, composer attachments, and undo history. Only persisted Session-keyed Store values or a generation kept alive by another reference survive a view switch; the runtime does not clear those persisted values.
+
+Every public `retain`, including the temporary references used by Session rename and fork-title assignment, starts the generation's shared initial history opening. These metadata operations await `reference.ready` before using the Session and therefore pay that history I/O for a cold generation.
+
+The catalog, UI status, and view target have separate owners and can publish independently. Their consumers must not infer a lifecycle transition solely from notification order. The main view remains an ordinary reference owner, while its navigation and presentation rules stay in UI rather than the reference allocator.

+ 226 - 0
.agents/notes/implemented/architecture/2026-09-15-client-session-references.zh.md

@@ -0,0 +1,226 @@
+# Agent Note: Client Session 引用、引用来源与 UI 状态
+
+Status: implemented
+
+[English](2026-09-15-client-session-references.md) | 中文
+
+## 问题
+
+Session 目录、活跃 Client 对象、视图与异步操作具有不同生命周期。目录成员关系不能证明仍在使用。借用的 binding 无法保护异步工作,也无法区分同一 Session ID 的不同 Client 代。全局 current Session 会使独立绑定的组件操作另一个视图的 Session。
+
+引用计数能表明仍在使用,但不能识别使用方。主区域高亮、Sidebar 所有权与后台操作需要使用方来源信息。待处理交互与完成提醒也需要统一的 UI 读取接口,避免 Workspace 和 Conversation 各自组合相同状态。
+
+Client 历史访问与 Host Agent(智能体)执行具有独立生命周期。历史打开可能失败;获取本地 Context 不一定需要读取历史。显式所有权必须保持导航、加载、错误处理与恢复行为,不添加无关 UI 策略。
+
+## 决策
+
+### 范围与所有权
+
+Client Session 对象、Agent 作用域的 Client Context、引用、使用方来源元数据、UI 状态与显式 Provider 组合遵循下述所有权规则。Host Session 和 Agent 生命周期、SlotFactory、activity 列表视图、持久 Session 格式及两个 SDK 的 Host 协议保持独立。
+
+| 所有者 | 职责 |
+| --- | --- |
+| Client Session Controller | 目录、活跃代、引用、来源计数、binding、历史窗口与既有 Session 控制状态 |
+| `ui-session` | 显式 Session Provider 集成与统一 UI 状态来源 |
+| 视图或操作 | 自己的引用、目标、来源标识与释放时机 |
+| Workspace UI | 主区域目标与引用、导航、持久目标及创建流程 |
+| UI 组合边界 | 将所有者提供的引用交给一个明确的 `SessionProvider` |
+| Conversation 与 Sidebar 子树 | 只消费所在 Provider 的 Session,不读取主区域引用或全局选择 |
+| Client Gateway | 处理 Host 事件时调用期的 Context 所有权 |
+
+[Client 分层设计](../../implemented/architecture/2026-08-20-client-session-conversation-ownership.zh.md)定义数据、适配器、渲染器与展示层的单向依赖。引用来源统计不会使 Controller 依赖 UI 包。
+
+本决策部分取代 [Web Client Session scope 与 provide channel 决策](2026-07-25-web-client-session-scope-and-provide-channel.zh.md)中由 list 选择驱动的 scope 生命周期;后者保留显式 Provider 所有权下的 blank Session 与收养语义理由。
+
+### 地址、binding 与引用
+
+`SessionTarget` 是已知 `SessionId` 或持久的直接父子 `SubagentAddress`。它标识要获取的目标,不持有任何对象。Controller 解析显式地址时不要求预先加载 parent catalog,Host 则在打开历史时校验其 parent、child 与 mode。子会话发现与已知子会话地址均不等同于持有该子会话。导航使用同一种目标表示,不增加另一套导航地址。
+
+`SessionBinding` 是共享的 Client 代,包含 `sessionId`、Session 接口、事件来源与作用域 Context。同一活跃代的多个引用共享这个 binding。同 ID 的新一代具有不同的 binding 和 Context。
+
+`SessionReference` 持有某个确切代的一次使用。它公开只读 `sessionId` 和 `binding`、表示共享首次历史打开的 `ready` Promise,以及幂等 `release()` 与 `Symbol.dispose`。对应 `Session.open()` 尝试解析时,`ready` 解析为该确切 binding,包括以状态表示的 Remote failure 结果。引用释放或其代清理后,读取 `binding` 会失败。获取、释放或查看引用均不创建持久 Session,也不启动、停止或持有 Host Agent。
+
+`SessionBinding` 是借用值,不构成持有。只有未释放的 `SessionReference` 计入来源与总引用数。同步代码可以在拥有者引用的生命周期内借用 binding;需要越过该生命周期的工作必须拥有自己的引用。
+
+| API | 返回结果与调用方义务 |
+| --- | --- |
+| `sessions.retain(target, options)` | `SessionReference`;立即返回,调用方持有该引用直到释放 |
+| `sessions.using<T>(target, options, operation)` | `Promise<T>`;依次等待 `reference.ready` 与 `operation(reference)`,释放自己的引用,并返回操作结果 |
+| `sessions.retainInfo(id)` | 本地引用计数的稳定只读 observable,不获取引用、不创建作用域,也不执行历史 I/O |
+| `sessions.scope(id)` / `sessions.binding(id)` | 借用已存在的活跃代或返回 `undefined`,不打开它,也不延长其生命周期 |
+| `sessions.sessionOf(ctx)` | 返回匹配的活跃 Session 接口或 `undefined`;已结束的 Context 不能解析为同 ID 替代代 |
+| `sessions.create(...)` / `sessions.fork(...)` | 既有 Host 操作,返回 Session 身份;持有与展示仍需显式执行 |
+
+`SessionRetainOptions` 包含必填 `source: SessionReferenceSource` 与可选 `signal: AbortSignal`。两个获取方法均接受 `target: SessionTarget` 和这些 options。来源键由使用方定义,通过声明合并扩展;不提供运行时来源注册协议,也不默认使用主视图来源。来源是使用标签,不是另一种 Session 地址,也不授予操作权限。
+
+### 获取、失败与释放
+
+获取操作同步解析目标,创建或持有本地 Session、Context、fiber 和 binding,记录一份引用及其来源计数,启动该代共享的首次历史打开,然后返回引用。并发获取共享这次打开,但分别获得独立引用与就绪等待。共享的 `Session.open()` 尝试解析时,`reference.ready` 解析为仍然有效的确切 binding。
+
+未知 Session id 的寻址在返回引用前失败;显式 subagent 地址由 Host 在打开期间校验。抛出的打开失败、调用方取消、引用释放或该代结束会拒绝该引用的 `ready`;取消一个等待方不会取消其他所有者共享的打开操作。以 `openState: 'error'` 表示的 Remote failure 跟随 `Session.open()` 语义并解析就绪,错误仍可通过 binding 渲染。调用方持有已返回的引用直到释放,而 `sessions.using` 会在就绪或操作失败时释放自己的引用。
+
+`using reference = sessions.retain(target, options)` 在所在作用域退出时释放;需要等待首次历史尝试结算的代码等待 `reference.ready`。`sessions.using` 是面向回调调用方的辅助方法:它先等待该次结算,再调用操作并等待操作返回的普通值或 Promise,最后释放。返回值不能依赖辅助方法已经释放的引用仍可使用;需要更长生命周期的使用方自行获取引用。辅助方法传播被拒绝的就绪与操作失败,不提供兜底结果、重试或错误展示。模型选择的代际检查与错误状态更新保留在 ModelSelection。
+
+`ready` Promise 成功表示首次 `Session.open()` 尝试已经结算;它不保证 `openState: 'open'`,也不保证连接永不中断。以状态表示的打开失败与之后的流失败继续通过既有 Session 状态公开。调用方保留对这些错误的既有处理与展示。
+
+释放只移除该引用及其来源计数。最后释放在清理 Session 与 fiber 前撤回该代的准入和 ID 映射。后续 retain 可以立即创建新一代;旧代清理不能移除替代代。`release()` 同步发起本地清理,根清理等待尚未结束的异步释放工作。
+
+所属 Client 根在关闭时使全部引用失效。它拒绝新的获取,撤回活跃映射,并汇合作用域清理与 Session 流释放。引用不能使已清理的 Client 根继续存活。仅移除目录记录不会清理仍被引用持有的代。
+
+### 引用来源与 Session 列表记录
+
+Controller 将来源计数与每个活跃代的引用共同管理。每个来源的计数等于该代尚未释放且带有该来源的引用数。同一 Session 可以有多个来源,同一来源可以持有多份引用。分配器不赋予任何来源特殊生命周期行为。
+
+每条已有 Session 列表记录公开只读 `retainedBy`,由来源键映射到正数引用计数。未被持有的记录使用空对象;零计数键不存在。获取失败与释放更新该投影,包括删除最后一个来源键。Host 元数据刷新时,来源计数仍是本地事实,Host 响应不能覆盖它。
+
+即使 Session 尚未进入 Host 目录或其目录元数据已被移除,计数仍由对应代持有。`byId` 为每个活跃代合成本地兜底记录,以便 Provider 与所有权使用方解析它;`ids` 仍表示 Host 列表成员关系与顺序。兜底记录不是 Host 元数据。每条记录的 `retainedBy` 投影均使用活跃代的计数。引用对象与计数均不持久化,也不发送给 Host。
+
+例如,`retainedBy = { mainView: 1, gateway: 2 }` 表示主视图与两个 Host 调用持有三份引用。释放主视图引用只移除 `mainView`;Gateway 调用继续持有该代。示例中的来源名是使用方键,不是 Controller 内封闭的枚举。
+
+`current` 表示主视图占用:记录的 `mainView` 来源计数为正数时,就具有这个标记。这只是通用来源记录的一种用途,不是独立的 `sessions.current` 值或 Session 选择服务。其他使用方可以从自己的来源键派生标记。引用系统不要求全局唯一使用方,也不从多个来源中选择一个 Session。
+
+主区域所有者管理自己的目标与引用切换。列表不根据恰好挂载了多少 Provider 推断选择。来源记录反映实际所有权,包括获取期间的短暂重叠;UI 导航仍负责自己的目标,不把选择权交给分配器。
+
+窗口级使用方可以查看来源标记。Session 业务操作仍使用传入的作用域或显式引用,不得从目录中查找 `mainView` 来补齐缺失的操作目标。后台引用只能证明所有权,不能证明用户查看过 Session。
+
+`SessionRetainInfo` 包含 `referenceCount` 与只读 `retainedBy` 记录。`sessions.retainInfo(id)` 独立于目录成员关系观察本地所有权,并在同 ID 换代时保持稳定。没有活跃代的身份具有零引用和空来源记录;读取该值不代表该身份在 Host 上存在。
+
+`ui-session` 通过 `useSessionRetainInfo(sessionId, selector)` 读取明确身份,通过 `useSessionRetainInfo(selector)` 读取外围 Provider 绑定的 Session。未绑定作用域向后一种形式提供缺失值,不回退到主区域 Session。两种形式均由渲染器从相同的裸 retain-info 来源构造。使用方检查 `mainView` 来源计数以识别原 current-Session 角色,其他来源键也可同等查询。读取或订阅不会持有 Session。
+
+### 统一的 UI Session 状态
+
+`ui-session` 拥有不依赖 React 的 `sessionStatus` 来源,通过标准 `useSessionStatus` 钩子公开。快照按 Session 身份索引,为每个已知 Session 提供一条 UI 状态记录。它组合下列独立事实,不将它们压缩成互斥的单一阶段:
+
+| 字段 | 取值 | 含义与所有者 |
+| --- | --- | --- |
+| `running` | `boolean` 或 `undefined` | 最新已知的 Session 运行事实;缺少基线不表示已确认 idle |
+| `pendingInteraction` | `SessionPendingInteraction` 或 `undefined` | 领域拥有的有效请求;没有待处理请求时缺失 |
+| `completionUnread` | `boolean` | 已观察到停止、但尚未确认的 UI 提醒 |
+
+来源计数以 `SessionListState.byId[id].retainedBy` 为准;UI 状态读取它以执行确认策略,不维护另一套引用注册表。标题、Workspace 关联、历史、队列和投影数据保留在各自的既有所有者中。
+
+待处理领域保留 `SessionPendingInteractionMap`、请求身份、优先级、发布清理函数与卸载委托。统一状态包含同一个有效请求对象,不复制请求,也不创建第二套待处理注册表。Workspace 状态指示器与 Conversation composer 选择读取 `useSessionStatus`,不再分别读取 `useSessionPendingInteraction` 和 `useCompletedSessionIds` 钩子。
+
+完成跟踪订阅已有 `api-session/status` 事件,避免 running 到 idle 的变化在合批目录快照中丢失。目录快照建立初始与重连基线。pending 状态下的空目录不能证明 Session 已消失。更新规则如下:
+
+- 初始 idle 基线不产生完成提醒。
+- 观察到 running 时清除旧提醒,并记录运行基线。
+- 从已知 running 变为 idle 时,仅在 Session 没有主视图持有的情况下设置 `completionUnread`。
+- 获得主视图持有时清除提醒;无关来源的 retain 不清除提醒。
+- 释放主视图持有不会为更早的一次停止补造提醒。
+- 移除 Session 时清除其完成提醒与运行基线;待处理请求的清理仍属于请求领域。
+
+提醒表示观察到的一次停止,不代表任务成功,也不代表某条排队消息完成。即使全局面板暂时隐藏 Conversation,主视图所有权仍保持基于选择的确认语义。本设计不提供 `ui-session/view-presence` 事件、已挂载 Provider 索引或 Provider 挂载时的隐式确认。
+
+Session Controller 发布运行与引用来源事实,但不拥有完成提醒集合、`consumeCompletion` 方法或待处理交互展示。引用获取不执行完成提醒业务逻辑。
+
+### 显式 Provider、并行 Conversation 与缓存身份
+
+唯一的 `SessionProvider` 可以继承外层 binding,也可以用显式 `session={reference | undefined}` 覆盖本子树。它不获取或释放所有权。根 Provider 从 `mainView` 所有权标记解析主 binding,不维护另一份 current;并列或嵌套 Provider 只影响各自子树。显式缺失保持缺失,不回退到主区域。
+
+Provider 注入选定 binding,但不为整个 body 设置 key。严格 `session` entry 在 binding Context 改变时重新挂载。空白 `session-maybe` entry 接受首个 binding 时不重新挂载;接受后,切换到另一个 binding Context 或回到缺失状态会创建新的组件 incarnation。
+
+`uiWorkspace` 持有来源为 `mainView` 的主区域引用,`ui-session` 从该引用在 Session 记录中的所有权标记建立根 Provider。Provider 下的 Conversation、右 Sidebar、preset、命令、输入与模型组件只能读取 Provider 绑定的标准数据。它们不得读取主区域引用或全局选择,也不得按 Session ID 重新寻找一个可能属于其他 Provider 的 binding。
+
+每个 Provider occurrence 以传入的 `SessionReference` 建立独立渲染作用域。两个引用可以指向不同 Session,也可以共享同一 `SessionBinding`;不同 binding 的业务与观看数据完全分离,同一 binding 的 Provider 共享 Session、Conversation、输入等 Session 级数据,但保留各自的组件局部状态。主区域与 Sidebar 可以同时挂载两个 Conversation,任一 Provider 的替换或卸载不改变另一棵子树的目标。
+
+`ui-session` 为每个活跃 `SessionBinding` 复用稳定的业务 observable。Conversation assembly、input shell、command popup、input-trigger controller 与 model directory 等代际缓存以 binding 为弱键,不再以 Session ID 为键;需要枚举活跃值的缓存使用 `util-values` 的 `WeakMapWithValues<SessionBinding, Value>`,由弱键表和强值集合共同维护身份查询与值遍历。该容器不执行清理;订阅、控制器、URL 和其他资源仍通过 `binding.ctx.effect()` 确定性释放。Provider occurrence 的观看状态随该 Provider 的渲染作用域释放,最终 generation 清理由 binding Context 负责。
+
+Descriptor 变化先组装替代来源再发布,不重建 Session 代。Provider 在读取 reference 时校验它仍有效;已经释放、来自其他 Controller 或不再对应活跃 binding 的 reference 不能创建作用域。
+
+| 使用方 | 获取与释放 |
+| --- | --- |
+| 主 Conversation | `uiWorkspace` 获取导航目标;`ui-session` 从 `mainView` 标记建立根 Provider,Conversation 子树只消费 Provider 绑定 |
+| 关联右 Sidebar | `RightbarRoot` 继承根 Provider,不读取主区域引用 |
+| 独立 Session 视图 | 自己的所有者持有目标,并以自己的引用建立 Provider;与主区域同时运行 |
+| Client 中的 Host 事件 handler | 持有本地 Context 引用直到 handler 与回复结算 |
+
+Conversation 的引用属于其视图所有者,不属于 Chat、Trajectory 或某次点击。Chat、Trajectory、命令、输入、上传、图片读取与模型选择在 Provider 生命周期内借用同一 binding;它们不按行动重复获取引用。切换或关闭该 Conversation 可以结束仍在途的本地工作。Sidebar tab 的布局与资源所有权保持独立;只有承载 Conversation 的 Sidebar 视图所有者需要自己的 Session 引用。空布局与 guide 占位页不持有引用。
+
+作用域业务对象从 Provider 捕获自己的 binding,不把旧 Context 或目录重新解释成同 ID 的新一代。ModelSelection 借用 Provider binding,并保留自己的选择代际与错误规则。编辑器脱离回调可能与作用域清理重叠;可选 trigger 和 popup 对已结束的 Context 返回空值,不解析其他 generation。
+
+### 主区域导航与展示
+
+`uiWorkspace.openSession`、`openWorkspace`、`forkSession` 和 `startSession` 仍是导航入口。它们接受或解析明确目标,改变主视图,并按既有导航策略将主区域返回 Conversation。`retain` 本身从不导航。引用所有权不引入 `registerNavigation` 接收者协议,也不引入第二个导航服务。
+
+现有 `uiWorkspace` 实现直接持有来源为 `mainView` 的主区域引用与目标。导航方法直接更新该所有者,不调用 Conversation 注册的接收者。`ui-session` 根据来源标记把该引用对应的 binding 注入根 Provider;主 Conversation 和关联右栏只继承 Provider,既不依赖 `uiWorkspace`,也看不到主引用。独立 Sidebar Conversation 以自己的 reference 建立嵌套 Provider,并覆盖本子树的 binding。主引用不是全局标准 prop、子树 Hook 或按 ID 查询的默认值。
+
+主视图在 `dsh.sessions.current` 下私下持久化目标身份与子会话地址,不保存引用。启动恢复、初始 Workspace 选择与归档主目标后的清空仍属于 UI。归档或移除目录元数据不撤销其他使用方持有的独立引用。
+
+| UI 行为 | 最终规则 |
+| --- | --- |
+| Session 列表高亮与空白记录处理 | 从 `retainedBy.mainView` 派生主区域占用,不按已挂载 Session Provider 的数量判断 |
+| New Session 的 Workspace | 优先使用显式 Workspace,其次按既有查找规则使用主 Session 的 Workspace,最后使用既有最近 Workspace 策略 |
+| Onboarding | 判断主区域 Session 是否缺失或为空白,不判断所有历史 Session |
+| 浏览器文档标题 | Conversation 显示 Session 标题与产品标题;全局面板显示产品标题 |
+| Chat/Trajectory 恢复 | 为显式选中的主目标恢复视图;独立绑定的视图保留自己的状态 |
+| Cordis inventory 面板 | 使用不区分 current/other 的单一列表;runner 不提供主区域选择的公开 getter |
+
+DOM 焦点移动或全局面板隐藏仍被持有的视图时,来源元数据不变。[全局主面板设计](../../implemented/architecture/2026-09-08-global-main-panels.zh.md)拥有面板选择与布局;Session 引用所有权不替代它。
+
+Conversation 保留 `hero`、`settling`、`active` 组合与既有历史加载和 `openError` 处理。获取引用不增加外层 loading/error 阶段展示、额外隐藏 composer 的条件、Retry 按钮或替换 Sidebar 内容的恢复面板。已有错误处理方继续处理自己的错误;没有错误展示的调用点不增加展示。Promise 拒绝与正确释放引用不意味着额外增加 UI 处理方。
+
+Workspace 连接和 fork 保持既有导航检查与面板切换失效规则。直接打开 Session 不增加全局导航取消策略。Agent Team 刷新保留发起时选择仍然有效的条件,不在刷新前启动全局导航 token。引用获取不扩大取消范围,不影响无关导航或正在执行的操作;本地取消不回滚 Host 效果。
+
+### 预设与创建流程
+
+预设目录与部署默认值可以共享。已绑定 Session 的预设通过 Provider 的 binding 读取或修改;预设控制器按 `SessionBinding` 缓存,不由根级 current-Session 跟随器管理。Hero 的 preset seat 使用 `session-maybe` Provider:没有 Session 时显示创建流程选择,绑定空白 Session 后操作该确切 Session。标题标签读取同一 Provider 绑定 Session 的投影。
+
+Session 创建前选择的 preset 保留在主 Conversation 的 `session-maybe` preset surface 中。Workspace 创建或复用空白 Session 并建立主 Provider 后,该 surface 将选择应用到 Provider 绑定的 Session。设置页修改默认 preset 或 picker 设置时,preset 服务从 Provider 已建立的 binding 缓存中选择带 `mainView` 所有权标记的空白 Session,并更新该 Session。非空白主 Session、Sidebar 的独立 Provider 与其他后台引用均不受该设置动作影响;preset 子树不读取主引用,也不通过全局 current follower 寻找目标。
+
+### Host 事件 Context 所有权与 Typert
+
+经校验的 Host waterfall(瀑布式事件)身份可以先于目录发现到达。Client Context 解析器必须保持同步。它带上 Gateway 的来源获取本地代引用,返回 `TypertOwnedValue<Context>`,不打开历史,也不刷新子目录。需要历史的 handler 另行获取公开引用并等待其 `ready` Promise,或使用 `sessions.using`。
+
+Gateway 持有本地引用,直到 handler 使用与回复结算均结束。Context 获取失败保留既有的记录错误并委托语义,handler 失败产生拒绝回复。取消通过已有 signal 到达 handler,并抑制晚回复,但不会在 Context 仍被使用时提前释放。插件关闭汇合当前连接代的在途 handler;Connection 只在该来源结算后启动替代代。
+
+`TypertOwnedValue` 跨通用 Gateway 传递值及其清理操作。它没有额外引用计数,也不要求 Gateway 理解 Session 专用所有权。独立 Client bundle 共享 owned-value 标记。Client 发请求的 `identity(ctx)` 保持同步;Host Context 解析与 Host 生命周期不变。
+
+## 考虑过的替代方案
+
+**以目录成员关系作为所有权。** 发现数据无法证明视图或操作仍在使用,而且部分 Context 身份先于目录成员关系到达。
+
+**隐式主 Session 加显式替代路径。** 两套目标解析规则会使可复用组件依赖其渲染位置。显式 Provider 提供目标,通用来源记录服务于窗口级观察。
+
+**Provider 子树继续读取 `mainSession`。** 同一组件会同时拥有 Provider 与窗口级目标,Sidebar 的独立 Conversation 也会被主区域变化重定向。主引用只参与 Provider 装配,子树只读取 Provider。
+
+**以 Session ID 为代际缓存键。** 最终释放允许同 ID 新代在旧清理结束前出现,ID 键会复用旧对象或让旧清理删除新对象。业务缓存使用弱引用的 binding 身份,资源清理由 binding Context 负责。
+
+**只在历史打开后解析的异步 `retain`。** 它会把 Provider 安装与主视图导航推迟到历史到达之后,导致既有加载状态无法立即渲染。同步引用把所有权与显式的 `ready` 结果分开。
+
+**在同步 Context 解析中执行历史 I/O。** Host 事件派发需要作用域生命周期,不一定需要历史窗口;耦合两者会在目录发现之前延迟或阻止 handler。
+
+**专用 current 标志。** 一种使用方专用标志无法描述 Sidebar 与后台所有权。主视图标记可以从通用的按来源计数中派生。
+
+**Session Controller 中的完成状态,或独立的待处理与完成提醒钩子。** 完成确认属于 UI 策略。统一 UI 状态来源组合独立事实,同时保留领域拥有的待处理对象与 Controller 拥有的运行事实。
+
+**以 Provider 呈现决定确认或释放。** 挂载既不等于所有者生命周期,也不等于基于选择的确认。它无法决定哪些后台或暂时隐藏的使用仍应存活,或应当算作已查看。
+
+**Provider 中的裸 ID 或全局 binding revision。** ID 不表达代际所有权,全局刷新会使无关 Session 使用方失效。
+
+**每个行动各自获取引用。** Provider 已经定义 Conversation 的使用生命周期;为每次点击重复持有会把视图所有权拆成大量细粒度来源。只有明确需要越过 Provider 生命周期的工作才另行持有引用。
+
+**导航注册、新的取消策略与获取专用恢复 UI。** 引用所有权要求显式目标、释放与失败传播,不要求额外导航或恢复行为。
+
+**Client 引用持有 Host Agent。** 历史访问与 Host 执行是独立使用需求,长时间打开的 Client 视图不能定义 Host 业务操作生命周期。
+
+## 验证
+
+- 获取测试覆盖共享首次打开、普通与意外打开失败、独立等待方取消、确切代替换,以及根清理达到完全停稳。
+- 来源测试覆盖多个来源、同来源多份引用、获取失败回滚、幂等释放、目录刷新或移除,以及旧代的晚释放不改变替代代。
+- Retain-info 钩子测试覆盖明确身份、Provider 绑定默认值、未绑定与嵌套作用域、跨代来源更新,以及读取不创建引用或请求历史。
+- 作用域与 Gateway 测试覆盖目录发现前不执行历史 I/O 的同步 Context 获取、获取失败的记录与委托、handler 的拒绝回复、取消与结算期间的所有权,以及跨 bundle 共享的 owned-value 标记。
+- UI 状态测试覆盖待处理优先级与清理、会被快照合批丢失的事件变化、初始与重连基线、主来源确认、无关来源持有,以及不存在 Provider 呈现事件。
+- 视图验证覆盖两个不同 Session 和同一 Session 多次使用的并行显式 Provider、互不重定向的 Conversation、独立 Slot store、作用域 preset、descriptor 更新,以及没有新恢复控件的 Sidebar 生命周期。
+- 实际组合浏览器场景保持主区域高亮、空白记录、onboarding、Workspace 默认值、标题与约定的导航或错误行为。只有 Client 所有权改变时,已有模型轮次录制仍作为行为输入。
+- 公开 `retain`、`using`、Provider、生成 API 目录、包约定与类型检查对 options 和所有权保持一致。Provider 子树不读取主区域引用,独立所有者各自释放自己的引用。
+
+## 后果
+
+使用方必须把引用保留到真实结束点;在活跃 Client 根内,未释放引用仍会泄漏。最后释放后再次 retain 会创建新一代,并可能重新打开历史。来源计数描述所有权,不描述可见性、任务成功或操作权限;以主标记作为业务兜底会重新引入隐式 current-Session 耦合。
+
+最后一份 reference 释放后,未持久化的 binding 自有状态会被丢弃,包括已加载的历史页、Chat 滚动锚点、预览换行、Files 展开状态、composer 附件和 undo 历史。只有持久化的 Session-keyed Store 值或由另一份 reference 保活的 generation 能跨视图切换保留;runtime 不会清除这些持久值。
+
+每次公开 `retain` 都会启动该 generation 共享的首次历史打开,包括 Session 重命名和 fork 标题设置所用的临时引用。这些元数据操作在使用 Session 前等待 `reference.ready`,因此冷 generation 会承担这次历史 I/O。
+
+目录、UI 状态与视图目标由不同所有者管理,可以独立发布。使用方不能仅从通知顺序推断生命周期变化。主视图仍是普通引用所有者,其导航与展示规则留在 UI,不进入引用分配器。

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.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-09-15-desktop-native-fatal-recovery.md
-2026-09-15-desktop-native-fatal-recovery.md: b2a0dfff1a630d1eedaa898e4a4c8144ff7ae776
-2026-09-15-desktop-native-fatal-recovery.zh.md: 0fee729e3be791d10966050d21482c5132b14fe6
+2026-09-15-desktop-native-fatal-recovery.md: 63ec81991f3e396075fe4ed09a8ddaa87c865862
+2026-09-15-desktop-native-fatal-recovery.zh.md: 05dbad80064706d384eeac6537da073a8f365e7e

+ 4 - 2
.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.md

@@ -12,7 +12,7 @@ A recovery document depends on the renderer and preload whose failure can preven
 
 Electron owns one native fatal dialog per application process. Explicit main-window creation, document-load, preload, renderer, Web initialization, and backend failures enter this path. Ordinary requests and package operations retain their local error handling; the Host restarts after failed package writes, and a failed Host restart enters native recovery; expected cancellation and shutdown do not enter recovery. No elapsed-time heuristic classifies a slow startup as fatal.
 
-The first report claims presentation before awaiting the dialog. Later reports remain in logs. The Electron console retains the complete reported diagnostic. The dialog bounds the first diagnostic to its final eight lines and limits the complete detail to 1,200 UTF-16 code units, including truncation notice and reinstall advice, because native dialogs cannot scroll. The dialog offers exit, restart, or disabling third-party bundles followed by a whole-application restart. Disabling writes activation metadata under the existing profile transaction lock after Host shutdown, without requiring runtime initialization or deleting installed files. An explicit recovery-operation failure is presented separately and does not count as another automatic fatal report.
+The first report claims presentation before awaiting the dialog. Later reports remain in logs. The Electron console retains the complete reported diagnostic. The dialog bounds the first diagnostic to its final eight lines and limits the complete detail to 1,200 UTF-16 code units, including truncation notice and reinstall advice, because native dialogs cannot scroll. The dialog offers exit, restart, or disabling third-party bundles followed by a whole-application restart. Recovery calls the shared app-boot `sanitizeProfile` function under the existing profile transaction lock after Host shutdown. The function restores caller-supplied bundles and renames the profile patch to a unique backup without parsing it, requiring runtime initialization, or deleting installed files. Callers own profile shutdown and write exclusion; Desktop is the current production caller. The home-level patch remains unchanged. An explicit recovery-operation failure is presented separately and does not count as another automatic fatal report.
 
 The Web document stays in place. A carrier callback owns startup failure presentation while the shared boot page retains its spinner; ordinary browser boot still renders its own failure report. Only the primary application frame may report a Web boot failure. The plugin window exposes package operations only; backend state remains in the main process, and native recovery directly owns disabling all third-party bundles. Desktop has no profile reset, plugin-window recovery controls, or emergency recovery document. A fatal backend failure requires one of the native recovery actions rather than an in-process retry.
 
@@ -22,6 +22,8 @@ This supersedes recovery-page and reset behavior in the [immediate-window decisi
 
 A Web modal depends on client initialization, while a second recovery document adds renderer resources and preload recovery paths. Native dialogs remain usable when those components fail. Automatically resetting configuration or restarting on every report can delete user configuration or create restart loops; explicit actions preserve user control.
 
+**Disable bundles while retaining the active profile patch.** A malformed patch or a patch that inserts a broken plugin can still prevent startup. Renaming preserves the user’s exact bytes for manual repair while removing that layer from startup; unique backup names preserve earlier recovery attempts.
+
 ## Consequences
 
-Recovery cannot report a killed or crashed Electron main process, and a silent startup hang has no automatic timeout prompt. Invalid profile JSON can prevent disabling plugins; exit and restart remain available after the operation reports its failure. Focused lifecycle tests cover fatal signals, cancellation, first-report deduplication, and shutdown ordering; locale expectations record dialog diagnostics and actions, and boot tests retain ordinary browser failure presentation.
+Recovery cannot report a killed or crashed Electron main process, and a silent startup hang has no automatic timeout prompt. A malformed home-level patch still blocks startup after profile recovery and requires manual repair. Backups accumulate in the profile directory without automatic pruning; users remove them when no longer needed. Invalid profile JSON can prevent disabling plugins; exit and restart remain available after the operation reports its failure. Focused lifecycle tests cover fatal signals, cancellation, first-report deduplication, and shutdown ordering; locale expectations record dialog diagnostics and actions, and boot tests retain ordinary browser failure presentation.

+ 4 - 2
.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 Electron 在每个应用进程中提供一次原生致命错误对话框。明确的主窗口创建、文档加载、preload、渲染器、Web 初始化和后端失败进入此路径。普通请求和包操作保留局部错误处理;包写入失败后会重新启动 Host,Host 重启失败进入原生恢复;预期取消和关闭不进入恢复。不通过耗时推断慢启动为致命故障。
 
-首次报告在等待对话框前取得展示权。后续报告保留在日志中。Electron 控制台保留完整的已报告诊断。原生对话框无法滚动,因此仅显示首次诊断末尾八行,并将包含截断提示和重装建议的完整详情限制为 1,200 个 UTF-16 代码单元。对话框提供退出、重启或禁用第三方 bundle 后重启整个应用。禁用操作等待 Host 关闭后,在已有 profile 事务锁内写入启用元数据,不要求运行时初始化,也不删除安装文件。显式恢复操作失败会单独展示,不计为另一次自动致命报告。
+首次报告在等待对话框前取得展示权。后续报告保留在日志中。Electron 控制台保留完整的已报告诊断。原生对话框无法滚动,因此仅显示首次诊断末尾八行,并将包含截断提示和重装建议的完整详情限制为 1,200 个 UTF-16 代码单元。对话框提供退出、重启或禁用第三方 bundle 后重启整个应用。恢复操作等待 Host 关闭后,在已有 profile 事务锁内调用共享 app-boot `sanitizeProfile` 函数。该函数恢复调用方指定的 bundle,并将 profile patch 重命名为唯一备份,无需解析 patch、初始化运行时或删除安装文件。调用方负责 profile 关闭并排除并发写入;当前生产调用方是 Desktop。home 级 patch 保持不变。显式恢复操作失败会单独展示,不计为另一次自动致命报告。
 
 Web 文档保留在原位。宿主回调负责启动失败展示,共享启动页保留加载动画;普通浏览器启动仍显示自身的失败报告。只有主应用框架可以上报 Web 启动失败。插件窗口只暴露包操作;后端状态保留在主进程中,原生恢复直接负责禁用全部第三方 bundle。Desktop 不提供 profile 重置、插件窗口恢复控件或应急恢复文档。后端致命故障必须通过原生恢复操作处理,不在当前进程中重试。
 
@@ -22,6 +22,8 @@ Web 文档保留在原位。宿主回调负责启动失败展示,共享启动
 
 Web 模态框依赖客户端初始化,而第二份恢复文档会增加渲染器资源和 preload 恢复路径。原生对话框在这些组件失败时仍然可用。自动重置配置或每次报告都重启可能删除用户配置或形成重启循环;显式操作保留用户控制权。
 
+**禁用 bundle,但保留生效的 profile patch。** 损坏的 patch 或插入故障插件的 patch 仍可阻止启动。重命名保留用户原始内容供手动修复,同时将该层移出启动过程;唯一备份名保留此前恢复操作的备份。
+
 ## Consequences
 
-恢复功能无法报告 Electron 主进程被终止或崩溃,静默启动挂起也没有自动超时提示。无效的 profile JSON 可能阻止禁用插件;操作报告失败后仍可退出和重启。定向生命周期测试覆盖致命信号、取消、首次报告去重和关闭顺序;语言预期记录对话框诊断和操作,启动测试保留普通浏览器失败展示。
+恢复功能无法报告 Electron 主进程被终止或崩溃,静默启动挂起也没有自动超时提示。损坏的 home 级 patch 在 profile 恢复后仍会阻止启动,需要手动修复。备份在 profile 目录中累积,不会自动清理;用户在不再需要时删除。无效的 profile JSON 可能阻止禁用插件;操作报告失败后仍可退出和重启。定向生命周期测试覆盖致命信号、取消、首次报告去重和关闭顺序;语言预期记录对话框诊断和操作,启动测试保留普通浏览器失败展示。

+ 3 - 3
.agents/notes/implemented/feature/2026-09-14-model-image-input-settings.i18n.yaml → .agents/notes/implemented/architecture/2026-09-15-platform-office-engines.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-09-14-model-image-input-settings.md
-2026-09-14-model-image-input-settings.md: ef328400332aa58c02a450ead0195eff124c5fdf
-2026-09-14-model-image-input-settings.zh.md: 70ec427b138026124cad2ffdb1fc5f60597abe8a
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-15-platform-office-engines.md
+2026-09-15-platform-office-engines.md: 4272054672c6163f22b910ab64b6472a9dbdfb4c
+2026-09-15-platform-office-engines.zh.md: d0a317333049734415ab9c0c05a5624b63b27447

+ 31 - 0
.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.md

@@ -0,0 +1,31 @@
+# Agent Note: One Office engine per platform
+
+Status: implemented
+
+English | [中文](2026-09-15-platform-office-engines.zh.md)
+
+## Problem
+
+Installing WASM beside a usable native engine adds a second LibreOffice payload to application downloads and installed resources. The original fallback policy in [independent kit ownership](2026-09-14-independent-libreoffice-kit.md) requires that extra payload even on fixed-platform Desktop and Python distributions.
+
+## Decision
+
+Harness selects one engine from the installed kit API’s `optionalDependencies`. A declared `@deepseek-ai/libreoffice-kit-${platform}-${arch}` requires that native package; other targets require the shared WASM package. The supported native target set belongs to the kit release, not an operating-system branch in Harness. A missing declared native package is an incomplete installation and never selects WASM. The provider has no direct WASM dependency.
+
+Python sidecar assembly copies only the selected engine and its dependency closure. Wheel packaging and runtime lookup select from the same kit manifest; the relocated conversion smoke checks the selected backend. Desktop filters its npm installation to that engine before signing and integrity sealing. Engine compilation, resolver compatibility, npm platform metadata, qualification, and publication remain in the kit repository.
+
+## Alternatives considered
+
+**Keep WASM beside every native engine.** This tolerates a missing optional native package but increases every distribution with native support. Fixed-platform distributions require their declared native engine to be present and tested instead.
+
+**Remove WASM on every platform.** Targets without a released native engine still need conversion. The shared WASM package serves those targets without introducing a new native release target.
+
+**Make the Python Office sidecar optional.** The runtime carries the shared `dsh` CLI and its Web profile as well as the default SDK profile. Requiring the target engine gives the installed wheel a complete shipped profile set and reports an incomplete payload before launch. SDK and headless users also pay the engine download and installed-size cost.
+
+## Consequences
+
+Distributions with a declared native target omit WASM assets. Other targets retain WASM resource and font requirements. The pinned kit declares macOS/Windows ARM64 and x64 native packages, so current Linux distributions select WASM; a kit release can add a native Linux target without changing Harness’s selection rule. This does not expand Harness’s supported release platforms. Harness sidecar, wheel, and runtime-resolution tests cover both declared native targets and WASM selection, including missing native packages. New package bytes require kit qualification and matching dependency integrity records before publication.
+
+A new engine package identity also requires updates to `LIBREOFFICE_PACKAGES` in `scripts/gen-third-party-notices.ts`, any applicable `minimumReleaseAgeExclude` entry in `pnpm-workspace.yaml`, and the package list in the [kit ownership note](2026-09-14-independent-libreoffice-kit.md). The license allowlist remains explicit.
+
+The [public Python release workflow](../../../../.github/workflows/python-release.yml) rejects any wheel at or above 100,000,000 bytes. Selecting one engine reduces payload size but does not establish that a runtime wheel meets this limit; npm engine publication and local conversion are separate from wheel upload eligibility.

+ 31 - 0
.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.zh.md

@@ -0,0 +1,31 @@
+# Agent Note: 每个平台使用一个 Office 引擎
+
+Status: implemented
+
+[English](2026-09-15-platform-office-engines.md) | 中文
+
+## Problem
+
+在可用的原生引擎之外安装 WASM,会为应用下载和安装资源增加第二份 LibreOffice 载荷。[独立 kit 归属](2026-09-14-independent-libreoffice-kit.zh.md)中最初的回退策略要求固定平台的 Desktop 和 Python 分发物也携带这份额外载荷。
+
+## Decision
+
+Harness 从已安装 kit API 的 `optionalDependencies` 中选择一个引擎。声明了 `@deepseek-ai/libreoffice-kit-${platform}-${arch}` 的目标要求该原生包,其余目标要求共享 WASM 包。受支持的原生目标集合由 kit 发布版本决定,不由 Harness 中的操作系统分支决定。已声明的原生包缺失表示安装不完整,不会改选 WASM。provider 不直接依赖 WASM。
+
+Python sidecar 组装仅复制所选引擎及其依赖闭包。wheel 打包和运行时查找依据同一份 kit 清单选择;迁移目录后的转换冒烟检查所选后端。Desktop 在签名和完整性封装前将 npm 安装结果过滤为该引擎。引擎编译、解析器兼容性、npm 平台元数据、资格验证与发布仍由 kit 仓库负责。
+
+## Alternatives considered
+
+**在每个原生引擎旁保留 WASM。** 这能容忍可选原生包缺失,却会增大每个支持原生引擎的分发物。固定平台分发物要求其已声明的原生引擎存在且经过测试。
+
+**在所有平台移除 WASM。** 没有已发布原生引擎的目标仍需转换能力。共享 WASM 包为这些目标提供转换,无需引入新的原生发布目标。
+
+**将 Python Office 伴随目录设为可选。** 运行时承载共享的 `dsh` CLI 及其 Web profile,而不只有默认 SDK profile。强制携带目标引擎使已安装 wheel 具备完整的内置 profile 集合,并在启动前报告载荷不完整。SDK 和 headless 用户也承担引擎的下载与安装体积。
+
+## Consequences
+
+声明了原生目标的分发物省去 WASM 资源,其余目标保留 WASM 的资源和字体要求。锁定的 kit 声明了 macOS/Windows ARM64 和 x64 原生包,因此当前 Linux 分发物选择 WASM;kit 发布版本可以新增 Linux 原生目标,无需修改 Harness 的选择规则。这不扩展 Harness 支持的发布平台。Harness 的 sidecar、wheel 和运行时解析测试覆盖已声明原生目标与 WASM 选择,包括原生包缺失。新包字节在发布前需要 kit 资格验证和匹配的依赖完整性记录。
+
+新增引擎包标识还需要更新 `scripts/gen-third-party-notices.ts` 中的 `LIBREOFFICE_PACKAGES`、`pnpm-workspace.yaml` 中适用的 `minimumReleaseAgeExclude` 条目,以及 [kit 归属记录](2026-09-14-independent-libreoffice-kit.zh.md)中的包列表。许可证允许列表仍使用明确的包标识。
+
+[公开 Python 发布工作流](../../../../.github/workflows/python-release.yml)拒绝任何大于等于 100,000,000 字节的 wheel。只选择一个引擎会减少载荷,但不能据此认定运行时 wheel 已满足此限制;npm 引擎发布、本地转换与 wheel 上传资格是不同的验证。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.md
+2026-09-16-creator-persistent-plugin-management.md: 04ccbb83fe6d2f5c460f53100a9087cce119ea4d
+2026-09-16-creator-persistent-plugin-management.zh.md: 91afa00cd3f0adb8b21a52ee958bac61bc0912fc

+ 31 - 0
.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.md

@@ -0,0 +1,31 @@
+# Agent Note: Creator mode installs persistent plugin bundles
+
+Status: implemented
+
+English | [中文](2026-09-16-creator-persistent-plugin-management.zh.md)
+
+## Problem
+
+Agents need to install capabilities and use them during the same conversation. Generated-code tools create a second plugin lifecycle alongside ordinary installed bundles.
+
+## Decision
+
+Creator mode enables the existing `plugin_manager` tool. Agents author packages and Loader YAML patches as workspace files, then install them through `install_bundle`. An MCP connection is a configuration-only bundle inserting the installed `dsh-mcp-client`; a UI bundle includes a Host entry and a Client artifact. Profile locking, package installation, enablement and HMR remain owned by the existing manager. The entire management tool requires `danger-full-access` or approval for one call: profile changes can load code with host permissions and affect other sessions. Each execution, including inventory reads, uses the shared sandbox escalation helper and approval service before accessing the manager. Under lower sandbox modes, `ask` requests approval and `never` denies; a full-access session needs no additional approval. A grant does not change session permissions, but its profile changes persist. This keeps shell-equivalent Host access explicit without requiring a permanent session-wide elevation. Human-operated Web and CLI controls retain their existing behavior.
+
+The model sees two read-only Cordis inspection tools. Generated-code define/run/stop/undefine and dynamic self-inspection tool APIs are absent. Existing runtime and Client consumers retain their services; historical session cards remain readable. This supersedes only the model-facing mutation workflow in the [self-referential toolset decision](../feature/2026-07-08-self-referential-cordis-toolset.md); its runtime ownership and sandbox rationale remain independently relevant. The [profile transaction decision](2026-09-14-current-profile-plugin-management.md) continues to govern locking, package installation, and partial failures.
+
+Visual creation requests default to an installed Client plugin rendered in the current Web page unless the user names another destination. The development skill supplies a minimal package and effect-owned Client registration. Discovery ends when the required APIs are known; a working first version is installed before optional visual refinement. Verification uses the connected page where available. Browser authentication or operating-system setup is not a prerequisite for plugin installation, and a mock preview cannot establish an in-app result.
+
+Desktop supplies its bundled pnpm entry and Electron Node invocation through launcher-owned profile facts. Plugin Manager uses that invocation for installation, removal, and registry inspection, retaining the ordinary profile transaction and activation behavior. Its environment applies only to package subprocesses; ordinary Host subprocesses do not inherit the private Node launcher path. CLI profiles retain their configured PATH command.
+
+Historical Cordis sessions declare `retired-tools` coverage at their exact format version, including when it equals the current writer. They are immutable replay inputs; their tool call/result data is compared after persistence and their cards render without registering retired tools. This coverage does not count toward migration coverage. The retained runner write APIs remain for programmatic and browser consumers; removing them requires replacing those consumers together.
+
+## Alternatives considered
+
+Generic entry CRUD and an MCP-specific management API duplicate operations expressible as bundle files plus existing installation and enablement. They are unnecessary for prompt-driven installation. Moving generated-code versioning into Plugin Manager retains two lifecycles without providing ordinary package persistence.
+
+## Consequences
+
+Selected bundles affect all sessions in the profile and survive restart. HMR activates new bundles on live profiles; installed package replacement requires restart. The agent reports saved state separately from activation and verifies the requested capability. Side effects belong to the plugin lifecycle, including stylesheet cleanup.
+
+A built Web profile test installs an MCP bundle, checks existing and new Creator sessions, restarts the process, and verifies tool disposal after bundle removal. A recorded session replays manager enablement of a configured MCP entry followed by an actual local MCP request without model credentials. Historical-card tests retain the removed tools' saved presentation.

+ 31 - 0
.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.zh.md

@@ -0,0 +1,31 @@
+# Agent Note: 创造模式安装持久化插件组合包
+
+Status: implemented
+
+[English](2026-09-16-creator-persistent-plugin-management.md) | 中文
+
+## Problem
+
+agent 需要安装能力并在同一段对话中使用。生成代码工具在普通已安装组合包之外引入了第二套插件生命周期。
+
+## Decision
+
+创造模式启用现有的 `plugin_manager` 工具。agent 将包和 Loader YAML patch 写入工作区文件,再通过 `install_bundle` 安装。MCP 连接是插入已安装 `dsh-mcp-client` 的纯配置组合包;UI 组合包包含 Host 入口和 Client 产物。profile 锁、包安装、启停和 HMR 继续由现有管理器负责。 整个管理工具要求 `danger-full-access` 或单次调用的批准:profile 变更能以宿主权限加载代码,并影响其他会话。每次执行(包括查询列表)都会先使用共享沙箱提权函数和审批服务,再访问管理器。在较低沙箱模式下,`ask` 请求审批,`never` 拒绝;完整权限会话无需额外审批。批准不改变会话权限,但本次操作的 profile 变更会持久化。这让与 shell 等价的宿主访问需要明确授权,同时不必永久提升整个会话的权限。由用户直接操作的 Web 和 CLI 控件保持原有行为。
+
+模型可见两个只读 Cordis 检查工具,不再提供生成代码的 define/run/stop/undefine 和动态自省工具 API。现有运行时和 Client 消费者保留其服务;历史会话卡片仍然可读。这仅取代[自引用工具集决策](../feature/2026-07-08-self-referential-cordis-toolset.zh.md)中的模型侧变更流程;其运行时所有权和沙箱依据仍有独立价值。[profile 事务决策](2026-09-14-current-profile-plugin-management.zh.md)继续规定锁、包安装和部分失败行为。
+
+除非用户指定其他目标,视觉创建请求默认通过已安装的 Client 插件显示在当前 Web 页面。开发 skill 提供最小包和由 effect 管理的 Client 注册示例。已知所需 API 后结束探查,在可选视觉优化之前先安装能工作的初版。有条件时使用已连接页面验证。浏览器认证或操作系统设置不是安装插件的前提,mock 预览不能证明应用内结果。
+
+Desktop 通过启动器提供的 profile 信息传入内置 pnpm 入口与 Electron Node 调用方式。Plugin Manager 使用该调用完成安装、移除和 registry 检查,保留通常的 profile 事务与激活行为。其环境仅应用于包管理子进程;普通 Host 子进程不继承私有 Node 启动器路径。CLI profile 保留其配置的 PATH 命令。
+
+历史 Cordis 会话以准确的格式版本声明 `retired-tools` 覆盖,即使该版本等于当前 writer 也如此。它们是不可改写的重放输入;持久化后的工具调用及结果数据会进行比较,卡片渲染不注册已移除工具。此覆盖不计入迁移覆盖。保留的 runner 写入 API 供程序和浏览器消费者使用;移除时必须一起替换这些消费者。
+
+## Alternatives considered
+
+通用条目增删改查和 MCP 专用管理 API 重复了组合包文件及现有安装、启停操作能够表达的能力,提示驱动的安装不需要这些接口。将生成代码的版本管理搬入 Plugin Manager 会保留两套生命周期,且无法提供普通包的持久化方式。
+
+## Consequences
+
+已选择的组合包影响 profile 中的所有会话,并在重启后保留。HMR 在实时 profile 中激活新组合包;替换已安装的包需要重启。agent 分别报告保存状态和激活结果,并验证所需能力。副作用归属于插件生命周期,包括样式表清理。
+
+构建后的 Web profile 测试安装 MCP 组合包,检查现有和新建创造模式会话,重启进程,并验证移除组合包后的工具释放。录制会话无需模型凭据即可回放管理器启用已配置 MCP 条目及后续真实本地 MCP 请求。历史卡片测试保留已移除工具的已保存展示。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.md
+2026-09-16-plugin-configuration-on-the-plugins-page.md: fb330bee44373d20c7aeea8cee521bf8def025d4
+2026-09-16-plugin-configuration-on-the-plugins-page.zh.md: 65c1f5a8bf4654b395ab4bfcf8015ae7c23a6acc

+ 39 - 0
.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.md

@@ -0,0 +1,39 @@
+# Agent Note: Plugin configuration on the Plugins page
+
+Status: implemented
+
+English | [中文](2026-09-16-plugin-configuration-on-the-plugins-page.zh.md)
+
+## Problem
+
+A plugin's settings lived in Settings, on the Plugins section's configuration tab: four collapsible cards, one per host-plane namespace, beside a read-only inventory tab. The sidebar's Plugins page listed and switched bundles but could not open a plugin's settings, so two surfaces split one object between them, and a bundle installed from outside the repository had no place for a form of its own at all.
+
+## Decision
+
+**The Plugins page hosts configuration; Settings keeps the inventory.** The page declares three slots as children of its `main` entry. `plugins.item` (list) lists an official plugin in the Official group by its `label`. `plugins.bundle.config` (keyed by the bundle's package name) renders on the bundle's page between its description and its rows. `plugins.row.config` (keyed by `<package name>#<row id>`) gives that row a configure control that opens a page headed by the row id. Every entry is rendered in two views the page passes as owner props: `summary` for the one-liner under the title, `page` for the form. The page draws the title, icon, and crumb, projects the three ledgers into one observable (`configLedgerSource`) beside its store, and never names a configurable plugin.
+
+**Attribution is declared by the registrant, in the slot name and key.** No manifest field, no registration metadata, and no owner information from the settings service: an official page registers without a bundle, a bundle's page with its package name, a row's page with the bundle and the row id its patch declares. A community bundle that ships no browser half has no configuration page; the page renders no generic form from a settings schema.
+
+**Only a save writes.** The form's discard control and its unsaved marker are gone: leaving a page drops the staged edits, which the form does on unmount. A save still writes through the client settings scope with the revision fence.
+
+**The four host-plane pages register from `ui-settings-plugins` while the Host serves their namespaces.** The package keeps the Settings section as the **Built-in plugins** shell around the inventory tab and registers each page through `ctx.slots.inject` when the shared settings mirror shows the namespace, disposing it when the namespace goes, so a deployment that does not compose the owning plugin shows no trace of it. The `settings.plugin.item` slot is retired.
+
+**The Official group.** The optional bundles the installation ships open the group, tagged **Beta** where the feature is one (Agent Teams), with no official tag; the configuration pages follow. Auto review leaves `OPTIONAL_BUNDLES` and the CLI's dependencies: it is a published experimental package the install guide names as its example.
+
+## Consequences
+
+- A bundle's browser half registers a form with one slot registration and its own dictionary; the bundle's patch must declare the row under the id in the key, and the registration exists while the row that carries the bundle's browser half is on: `dsh-client-modules` attaches that half to the row whose specifier is the bare package name, so a row-level page keyed to a subpath row disappears with the root row, not with its own.
+- The four pages, their forms, and the settings write path are unchanged; `ui-settings-plugins` keeps its name for the section it still owns while its pages live on the Plugins page.
+- Settings lists the inventory only; the settings goldens that carried the Plugins nav entry and the configuration tab were re-recorded.
+
+## Alternatives considered
+
+**Registration metadata on the slot entry.** A slot-declared `meta` share (description, bundle, row) would let one `plugins.item` slot carry every case, at the cost of a new concept in `ui-slots`; the three slots say the same with the API that exists.
+
+**A generic form from the settings schema.** `describe()` already ships each namespace's schemastery schema, so a community namespace without a browser half could get a default form. Deferred: the official pages are curated, a generic form would expose internal namespaces without an allowlist, and attributing a namespace to a bundle needs an owner in the descriptor.
+
+**Keeping the cards in Settings and linking to them from the page.** Cheapest, but the review asked for the form on the plugin's own page.
+
+## Testing
+
+`ui-plugin-manager` unit tests cover the ledger projection, the Official group's cards and pages, a bundle's form, and a row's page; `ui-settings-plugins` tests cover registration per served namespace and its withdrawal, and the form's save-only behavior. The `plugin-config` web lane edits the shell page through the real wire and opens a community fixture's row page after switching its bundle on; the `plugin-manager` lane records the new Official group.

+ 39 - 0
.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.zh.md

@@ -0,0 +1,39 @@
+# Agent Note:插件页上的插件配置
+
+状态:已实现
+
+[English](2026-09-16-plugin-configuration-on-the-plugins-page.md) | 中文
+
+## 问题
+
+插件的设置原本放在设置里、插件分区的配置标签页上:四张可折叠卡片,每个宿主平面命名空间一张,旁边是只读的清单标签页。侧栏的插件页能列出并启停组合包,却打不开插件的设置,于是同一个对象被两个界面各持一半;仓库之外安装的组合包更是完全没有地方放自己的表单。
+
+## 决策
+
+**插件页承载配置;设置只保留清单。** 页面在其 `main` 条目下声明三个子 slot。`plugins.item`(list)按 `label` 把官方插件列在官方分组里。`plugins.bundle.config`(以组合包的包名为键)渲染在组合包页面的描述与行之间。`plugins.row.config`(以 `<包名>#<行 id>` 为键)给这一行一个配置控件,打开以行 id 为标题的页面。每个条目都按页面以 owner props 传入的两种视图渲染:`summary` 是标题下的一句话简介,`page` 是表单。页面负责画标题、图标与面包屑,把三份账本投影成一个可观察对象(`configLedgerSource`)绑在 store 旁边,自身从不点名任何可配置插件。
+
+**归属由注册方在 slot 名与键里声明。** 不加 manifest 字段,不加注册元数据,也不从 settings 服务取 owner 信息:官方页面不带组合包注册,组合包的页面带自己的包名注册,行的页面带组合包与其 patch 声明的行 id 注册。没有浏览器半侧的社区组合包就没有配置页;页面不会从 settings schema 渲染通用表单。
+
+**只有保存才写入。** 表单的放弃控件与未保存标记都去掉了:离开页面即丢弃暂存修改,表单在卸载时执行。保存仍经由客户端 settings scope,带 revision 栅栏。
+
+**四个宿主平面页面由 `ui-settings-plugins` 在 Host 服务其命名空间期间注册。** 该包把设置分区保留为清单标签页外面的**内置插件**外壳,在共享的 settings 镜像显示某个命名空间时通过 `ctx.slots.inject` 注册对应页面,命名空间消失时销毁,因此没有组装该插件的部署不会留下它的痕迹。`settings.plugin.item` slot 退役。
+
+**官方分组。** 安装随附的可选组合包开启这个分组,属于 beta 功能的(Agent Teams)带 **Beta** 标签,不再有官方标签;配置页排在其后。Auto review 离开 `OPTIONAL_BUNDLES` 与 CLI 的依赖:它是一个已发布的实验包,安装引导拿它作示例。
+
+## 后果
+
+- 组合包的浏览器半侧用一次 slot 注册加自己的词典就能提供表单;组合包的 patch 必须以键里的 id 声明这一行,注册只在承载组合包浏览器半侧的那一行开启期间存在:`dsh-client-modules` 把该半侧挂在说明符恰为包名的那一行上,因此键指向子路径行的行级页面随根行消失,而不随它自己的行。
+- 四个页面、它们的表单与 settings 写入路径不变;`ui-settings-plugins` 保留其名字,因为它仍拥有那个分区,尽管它的页面住在插件页上。
+- 设置只列出清单;带有插件导航项与配置标签页的设置类 golden 已重录。
+
+## 考虑过的替代方案
+
+**slot 条目上的注册元数据。** 由 slot 声明的 `meta` 份额(描述、组合包、行)能让单个 `plugins.item` slot 覆盖所有情况,代价是 `ui-slots` 多一个概念;三个 slot 用现有 API 表达了同样的信息。
+
+**从 settings schema 生成通用表单。** `describe()` 已经把每个命名空间的 schemastery schema 发给浏览器,没有浏览器半侧的社区命名空间本可获得一份默认表单。延期:官方页面是精选的,通用表单会在没有白名单的情况下暴露内部命名空间,而把命名空间归属到组合包还需要 descriptor 带 owner。
+
+**把卡片留在设置里、从插件页链接过去。** 最省事,但 review 要求表单在插件自己的页面上。
+
+## 测试
+
+`ui-plugin-manager` 单测覆盖账本投影、官方分组的卡片与页面、组合包的表单以及行的页面;`ui-settings-plugins` 单测覆盖按被服务的命名空间注册与撤下,以及表单只保存不放弃的行为。`plugin-config` web lane 通过真实链路编辑 shell 页面,并在开启社区夹具的组合包后打开其行的页面;`plugin-manager` lane 记录新的官方分组。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-16-user-terminal-permissions.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-16-user-terminal-permissions.md
+2026-09-16-user-terminal-permissions.md: f45b96419c34f8ebddc30431b42c9859c46de6b9
+2026-09-16-user-terminal-permissions.zh.md: 62bb152a1b899d026c8d7bddb350317ba12f14cf

+ 29 - 0
.agents/notes/implemented/architecture/2026-09-16-user-terminal-permissions.md

@@ -0,0 +1,29 @@
+# Agent Note: User-terminal permissions
+
+Status: implemented
+
+English | [中文](2026-09-16-user-terminal-permissions.zh.md)
+
+## Problem
+
+Users need to run commands themselves while keeping the Agent restricted. Sharing the Agent's sandbox mode forces a user to widen Agent access for manual work, and retaining an interactive shell prevents later mode changes because its process confinement cannot follow a new Session setting.
+
+## Decision
+
+The Web sidebar terminal runs directly through the Session's subprocess provider with the execution environment's system-user permissions. It neither confines the shell through the Agent sandbox nor requests Agent approval. Operating-system permissions, container isolation and the provider's credential-environment scrubbing continue to apply. Session identity owns access, process cleanup and the initial directory; sandbox policy supplies only the configured directory fallback when the Session has no cwd.
+
+Agent permission changes leave user terminals running. Agent-owned shell and terminal tools retain their own sandbox enforcement. User terminal input and output create no model input or Session events.
+
+This decision supersedes only the shared sandbox policy and mode-switch restriction in the [Web sidebar terminal decision](../feature/2026-09-09-web-sidebar-terminal.md). That note remains active for process ownership, transport, screen recovery and shell selection. OpenCode's `packages/core/src/pty.ts` and `packages/core/src/pty/pty.node.ts` provide adjacent evidence: its interactive terminal creates a PTY directly with the selected shell and working directory.
+
+## Alternatives considered
+
+**Inherit Agent permissions.** One Session mode describes both processes, but users must also grant the Agent access needed only for manual commands. Persistent user shells then obstruct changes to Agent permissions.
+
+**Add a separate terminal permission selector.** The product treats this terminal as a user-operated system shell. Another selector adds policy state and process-restart semantics without a current requirement; deployment and operating-system controls already determine the execution environment.
+
+## Consequences
+
+Access to the Web terminal grants command execution as the subprocess provider's system user, including writes outside the Session workspace where that user has permission. It does not grant root or escape a container. Session ownership remains useful for grouping and cleanup without implying Agent authority over user actions.
+
+Controller tests pin direct shell launch across all Agent sandbox modes and continued ownership during mode changes. The recorded Web permission-policy scenario keeps one real user PTY open across read-only, full-access and workspace-write transitions, verifies writes inside and outside the workspace, and retains the Agent's read-only denial and approval assertions. The Bash browser assertions run on macOS and Linux; Windows retains the portable controller checks and Agent-policy replay.

+ 29 - 0
.agents/notes/implemented/architecture/2026-09-16-user-terminal-permissions.zh.md

@@ -0,0 +1,29 @@
+# Agent Note: User-terminal permissions
+
+Status: implemented
+
+[English](2026-09-16-user-terminal-permissions.md) | 中文
+
+## 问题
+
+用户需要在限制 Agent(智能体)权限的同时亲自运行命令。共享 Agent 的沙箱模式会迫使用户为了手动操作而扩大 Agent 权限;保留交互式 shell 又会阻止后续模式切换,因为已有进程的沙箱限制无法跟随新的 Session 设置改变。
+
+## 决策
+
+Web 侧栏终端直接通过 Session 的 subprocess provider 运行,使用执行环境中系统用户的权限。它不通过 Agent 沙箱限制 shell,也不请求 Agent 审批。操作系统权限、容器隔离和 provider 对环境凭据的清除仍然生效。Session 标识负责访问范围、进程清理和初始目录;sandbox policy 仅在 Session 没有 cwd 时提供配置的默认目录。
+
+改变 Agent 权限时,用户终端继续运行。Agent 使用的 shell 和 terminal 工具保留各自的沙箱限制。用户终端的输入和输出不产生模型输入或 Session 事件。
+
+本决策仅取代 [Web 侧栏终端决策](../feature/2026-09-09-web-sidebar-terminal.zh.md)中的共享沙箱策略和模式切换限制。原记录继续负责进程所有权、传输、屏幕恢复和 shell 选择。OpenCode 的 `packages/core/src/pty.ts` 和 `packages/core/src/pty/pty.node.ts` 提供相邻实现依据:其交互式终端使用选定的 shell 和工作目录直接创建 PTY。
+
+## 考虑过的替代方案
+
+**继承 Agent 权限。** 一个 Session 模式可以描述两类进程,但用户必须同时授予 Agent 仅用于手动命令的权限。持久用户 shell 随之阻碍 Agent 权限切换。
+
+**增加独立的终端权限选择器。** 产品将此终端视为用户操作的系统 shell。另一个选择器会增加策略状态和进程重启语义,目前没有对应需求;部署和操作系统控制已经确定执行环境。
+
+## 影响
+
+访问 Web 终端即可作为 subprocess provider 的系统用户执行命令,包括在该用户有权限时写入 Session 工作区之外的路径。它不会授予 root 权限或逃逸容器。Session 所有权继续用于分组和清理,不意味着 Agent 决定用户操作的权限。
+
+Controller 测试覆盖全部 Agent 沙箱模式下的直接 shell 启动,以及模式改变后的持续所有权。录制的 Web 权限策略场景在只读、完全访问和工作区写入之间切换时保持同一个真实用户 PTY,验证工作区内外的写入,并保留 Agent 的只读拒绝和审批断言。Bash 浏览器断言在 macOS 和 Linux 运行;Windows 保留可移植的 controller 检查和 Agent 策略回放。

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-09-16-desktop-window-menus.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-16-desktop-window-menus.md
+2026-09-16-desktop-window-menus.md: 85ac42aa287b1cd9e595ade7cb44b965dea787dd
+2026-09-16-desktop-window-menus.zh.md: 1376b90058ceafa7e378b4fd543d261064f5d8aa

+ 35 - 0
.agents/notes/implemented/bug-fix/2026-09-16-desktop-window-menus.md

@@ -0,0 +1,35 @@
+# Agent Note: Desktop standard macOS window menus
+
+Status: implemented
+
+English | [中文](2026-09-16-desktop-window-menus.zh.md)
+
+## Problem
+
+The Desktop shell replaces Electron's default application menu with a custom template that listed only the application and Edit menus. Electron builds only the roles a template declares, so macOS lost the File and Window menus and the application hide commands the default template supplies, including Close Window (⌘W), Minimize (⌘M), and Hide (⌘H). None of those shortcuts did anything in the Desktop application while every comparable macOS application responds to them (issue #4374).
+
+## Decision
+
+On macOS the template declares `{ role: 'fileMenu' }` before the Edit menu and `{ role: 'windowMenu' }` after it, and a separator-delimited run of `hide`, `hideOthers`, and `unhide` before Quit in the application submenu. Those roles contribute only the standard items; no Services submenu, window list, or other macOS default is declared. Windows and Linux keep the application and Edit menus.
+
+Electron's role labels are English string literals inside Electron (`lib/browser/api/menu-item-roles.ts`, `filemenu`, `windowmenu`, `close`, `minimize`, `hide`) with no locale lookup, and Electron re-applies them to the native menu items before the menu is shown, so a non-English Desktop shows them in English; the existing Edit menu behaves the same way. No custom close, minimize, or hide code is added: ⌘W destroys the window through Electron's own role.
+
+## Alternatives considered
+
+**Bind ⌘W to minimizing or hiding the window.** macOS reserves Minimize for ⌘M and Hide for ⌘H, and comparable applications close the front window with ⌘W. Binding another command to the reported shortcut would contradict the platform convention the change follows.
+
+**Declare only the Window menu.** That menu supplies Minimize and Zoom but no Close, leaving ⌘W unbound.
+
+**Add a bare `{ role: 'close' }` item to the application submenu.** It avoids a File menu containing a single command, but places a window command among the app-wide Plugins, Updates, Hide, and Quit items where no macOS application puts it.
+
+**Declare the File and Window menus on every platform.** The same template declares Ctrl+W there; with one window, closing it invokes `window-all-closed` and quits the application, turning a window shortcut into an unrequested quit path.
+
+**Recreate the main window on Dock activation regardless of other windows.** The plugin window can outlive the main window, so gating `activate` on `mainWindow` instead of `BrowserWindow.getAllWindows()` would let a Dock click always restore the main window. That changes window lifecycle beyond restoring the suppressed platform commands, so the existing `activate` condition stays.
+
+## Consequences
+
+macOS regains the window and application commands the custom menu suppressed, at the cost of four menu roles. The labels stay English on a non-English Desktop.
+
+## Testing
+
+A `apps/desktop/tests/main-startup.spec.ts` case pins the declared menu roles per platform, including the macOS hide commands. Role-based menu items execute natively, so a programmatic `click()` and the vitest Electron mock cannot exercise the shortcuts; a real Electron 44 run of the same template showed the standard File, Window, and application items present only after this change, with the same English labels while `app.getLocale()`, `getSystemLocale()`, and `getPreferredSystemLanguages()` all reported `zh-CN`.

+ 35 - 0
.agents/notes/implemented/bug-fix/2026-09-16-desktop-window-menus.zh.md

@@ -0,0 +1,35 @@
+# Agent Note: Desktop standard macOS window menus
+
+Status: implemented
+
+[English](2026-09-16-desktop-window-menus.md) | 中文
+
+## 问题
+
+Desktop shell 用自定义模板替换了 Electron 的默认应用菜单,该模板此前只列出应用菜单和 Edit 菜单。Electron 只构建模板中声明的 role,因此 macOS 失去了默认模板提供的 File、Window 菜单和应用隐藏命令,包括 Close Window(⌘W)、Minimize(⌘M)和 Hide(⌘H)。这些快捷键在 Desktop 应用中都没有任何效果,而同类的 macOS 应用都会响应它们(#4374)。
+
+## 决策
+
+macOS 上的模板在 Edit 菜单之前声明 `{ role: 'fileMenu' }`,在其之后声明 `{ role: 'windowMenu' }`,并在应用子菜单的 Quit 之前声明由分隔符隔开的 `hide`、`hideOthers` 和 `unhide`。这些 role 只提供标准菜单项;不声明 Services 子菜单、窗口列表或其他 macOS 默认项。Windows 和 Linux 保留应用菜单和 Edit 菜单。
+
+这些 role 的标签是 Electron 内部的英文常量(`lib/browser/api/menu-item-roles.ts` 的 `filemenu`、`windowmenu`、`close`、`minimize`、`hide`),没有语言查询,而且 Electron 在菜单显示前会把这些标签重新写到本机菜单项上,因此在非英文的 Desktop 上它们仍是英文;现有的 Edit 菜单同样如此。这里不新增任何自定义的关闭、最小化或隐藏代码:⌘W 通过 Electron 自身的 role 销毁窗口。
+
+## 考虑过的替代方案
+
+**把 ⌘W 绑定为最小化或隐藏窗口。** macOS 把最小化留给 ⌘M、把隐藏留给 ⌘H,同类应用都用 ⌘W 关闭最前窗口。给这个快捷键绑定其他命令会违背本改动所遵循的平台惯例。
+
+**只声明 Window 菜单。** 该菜单提供 Minimize 和 Zoom,但不提供关闭,⌘W 仍然没有绑定。
+
+**在应用子菜单里直接加一个 `{ role: 'close' }` 项。** 这样可以避免只有一个命令的 File 菜单,但会把窗口命令放到 Plugins、Updates、Hide、Quit 这些应用级命令中间,没有 macOS 应用这样排布。
+
+**在所有平台都声明 File 和 Window 菜单。** 同一份模板在那些平台会把 Ctrl+W 声明为关闭;应用只有一个窗口,关闭它会触发 `window-all-closed` 并退出应用,把窗口快捷键变成用户没有要求的退出路径。
+
+**让 Dock 激活时无论其他窗口是否存在都重建主窗口。** 插件窗口可以比主窗口存活更久,因此把 `activate` 的判断从 `BrowserWindow.getAllWindows()` 改为 `mainWindow` 能让点击 Dock 总可以恢复主窗口。但这超出了恢复被压掉的平台命令,属于窗口生命周期的行为变更,因此保留现有的 `activate` 判断。
+
+## 影响
+
+macOS 恢复了自定义菜单压掉的窗口和应用命令,代价是四个菜单 role。在非英文的 Desktop 上这些标签仍是英文。
+
+## 测试
+
+`apps/desktop/tests/main-startup.spec.ts` 中的一个用例固定了各平台声明的菜单 role,包括 macOS 的隐藏命令。基于 role 的菜单项由本机执行,因此程序化的 `click()` 和 vitest 的 Electron mock 都无法触达这些快捷键;用真实 Electron 44 运行同一模板显示,只有在本改动之后才出现标准的 File、Window 和应用菜单项,并且在 `app.getLocale()`、`getSystemLocale()` 和 `getPreferredSystemLanguages()` 都报告 `zh-CN` 时标签保持不变。

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-09-16-messages-historical-tool-input.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-16-messages-historical-tool-input.md
+2026-09-16-messages-historical-tool-input.md: 201b0f21ad041395a4c4101d6919b2878e22c9ab
+2026-09-16-messages-historical-tool-input.zh.md: bcc637982420124b401ef65c668b068394b6d915

+ 29 - 0
.agents/notes/implemented/bug-fix/2026-09-16-messages-historical-tool-input.md

@@ -0,0 +1,29 @@
+# Agent Note: Replay malformed historical tool input through Messages
+
+Status: implemented
+
+English | [中文](2026-09-16-messages-historical-tool-input.zh.md)
+
+## Problem
+
+Chat Completions retains tool arguments as strings, including malformed JSON from failed calls. Switching that history to Messages requires an object for each `tool_use.input`. Rejecting one historical argument blocks every later request containing it, even after a successful tool retry; a summarization request containing the same call also fails.
+
+## Decision
+
+The [Messages serializer](../../../../packages/llm/llm-deepseek/src/protocols/messages/serialize.ts) follows the [pi-ai history conversion](../../../../packages/llm/llm-pi-ai/src/replay.ts): malformed JSON and non-object values become `{}` only in the outgoing historical tool input. Call ids, names, results, and original Session records remain intact. This applies with valid, absent, or unusable native replay metadata and does not execute the historical call again.
+
+This supersedes the historical argument rejection in the [Messages adapter decision](../feature/2026-09-07-deepseek-messages-adapter.md). New Messages responses still require valid object arguments before successful completion; output-limit truncation retains its existing pruning behavior. No Session event, persistence type, or protocol configuration changes.
+
+## Alternatives considered
+
+**Reject malformed history.** A failed call can remain relevant evidence without preventing all subsequent model requests.
+
+**Repair or overwrite stored arguments.** Guessing missing quotes or retaining a parsed prefix can change the requested operation. Request-only empty input preserves the original evidence and requires no migration.
+
+**Drop the call.** Its result still cites the call id; keeping both preserves the tool exchange without inventing arguments.
+
+## Consequences
+
+Messages continuation can omit unusable historical parameters without losing the call identity or result. The model sees `{}` rather than the original malformed text, and the fallback is silent, matching pi-ai. Original arguments remain available in the Session log; this does not claim lossless provider input or repair invalid newly generated calls.
+
+Verification covers object-only conversion, failed results followed by user input, JSON round trips, both valid and degraded replay metadata, a [recorded Session](../../../../snapshots/session/deepseek-messages-invalid-tool-history/snapshot.yml) through the shipped headless profile and real Messages serializer, and a credential-gated live Messages continuation.

+ 29 - 0
.agents/notes/implemented/bug-fix/2026-09-16-messages-historical-tool-input.zh.md

@@ -0,0 +1,29 @@
+# Agent Note: 通过 Messages 回放非法历史工具输入
+
+Status: implemented
+
+[English](2026-09-16-messages-historical-tool-input.md) | 中文
+
+## 问题
+
+Chat Completions 将工具参数保留为字符串,其中可能包含失败调用产生的非法 JSON。将这段历史切换到 Messages 时,每个 `tool_use.input` 都必须是对象。拒绝一条历史参数就会阻断包含它的所有后续请求,即使工具重试已经成功;包含同一调用的摘要请求也会失败。
+
+## 决策
+
+[Messages 序列化器](../../../../packages/llm/llm-deepseek/src/protocols/messages/serialize.ts) 遵循 [pi-ai 历史转换](../../../../packages/llm/llm-pi-ai/src/replay.ts)的做法:只在发出的历史工具输入中,将非法 JSON 和非对象值替换为 `{}`。调用 ID、名称、结果和原始 Session 记录保持不变。原生回放元数据有效、缺失或不可用时均采用此规则,也不会重新执行历史调用。
+
+这取代了 [Messages 适配器决策](../feature/2026-09-07-deepseek-messages-adapter.zh.md)中的历史参数拒绝规则。新生成的 Messages 响应在成功完成前仍要求工具参数是有效对象;达到输出上限时仍按现有规则裁剪。不改变 Session 事件、持久化类型或协议配置。
+
+## 考虑过的替代方案
+
+**拒绝非法历史。** 失败调用可以继续作为相关证据保留,而不必阻断所有后续模型请求。
+
+**修复或覆盖已存参数。** 猜测缺失的引号或保留部分解析结果可能改变请求的操作。只在请求中使用空输入可以保留原始证据,也不需要迁移。
+
+**删除调用。** 对应结果仍引用调用 ID;同时保留调用和结果,可以保留工具交互而不编造参数。
+
+## 后果
+
+Messages 可以在省略不可用历史参数的同时继续会话,并保留调用身份和结果。模型看到的是 `{}`,而不是原始非法文本;该兜底与 pi-ai 一样不产生诊断。原始参数仍可在 Session 日志中查阅;这不保证提供方输入无损,也不修复新生成的非法调用。
+
+验证覆盖仅接受对象的转换、失败结果后的用户输入、JSON 往返、有效与降级的回放元数据、通过已发布 headless profile 和真实 Messages 序列化器运行的[录制 Session](../../../../snapshots/session/deepseek-messages-invalid-tool-history/snapshot.yml),以及需要凭据的真实 Messages 续接。

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-09-16-session-writer-held-feedback.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-16-session-writer-held-feedback.md
+2026-09-16-session-writer-held-feedback.md: 922987996c4abd32857cd0f54cec4394566cf97f
+2026-09-16-session-writer-held-feedback.zh.md: 6a1aa7993a8f36c50cb5fed0180d6523af44c177

+ 25 - 0
.agents/notes/implemented/bug-fix/2026-09-16-session-writer-held-feedback.md

@@ -0,0 +1,25 @@
+# Agent Note: Session writer contention feedback
+
+Status: implemented
+
+English | [中文](2026-09-16-session-writer-held-feedback.zh.md)
+
+## Problem
+
+A Session can retain its write handle while its Agent is idle. Another Host cannot resume that Session, but a generic internal-error toast gives the user no recovery guidance. The holder can also belong to the same process, so contention alone does not identify another running application.
+
+## Decision
+
+Session Controller reports `session/writer-held` with `{ sessionId }` when resume fails with `SessionAlreadyOwnedError` and no reusable Agent exists. Clients discriminate by code, following the [Remote failure vocabulary](../architecture/2026-08-28-ctx-remote-failure-vocabulary.md). The controller recognizes the persistence error by its Error name, matching Session Query's optional-dependency handling; loading the controller requires no persistence implementation or error-class identity.
+
+Send and model-selection failures show localized guidance that another DSH instance may hold the Session and suggest quitting other instances before retrying. Model selection returns the original Remote result to both UI entries; error classification does not depend on a later read of shared directory state. The [write-lease decision](../feature/2026-08-31-cross-process-session-write-lease.md) continues to own locking and release semantics; this feedback neither takes ownership nor retries writes automatically.
+
+## Alternatives considered
+
+**A free-text reason under `session/agent-busy`.** A second string discriminator loses the code-to-details type relationship and mixes write contention with prompt admission failures.
+
+**A separate directory flag or a required persistence peer.** A flag duplicates the operation's failure and can become stale after a catalog refresh. Making persistence mandatory solely to identify its error removes support for deployments without persistence.
+
+## Consequences
+
+The wire gains one typed failure code without changing stored Session data. Recovery guidance cannot identify which process owns the handle. Host tests cover the error name and ordinary failures with and without persistence; Client tests cover both locales and operation-local model failures. The keyless `queue-actions` Web scenario pins the writer-held toast and preserved draft through the shipped composition. `verify-optional-dependency-imports` guards module loading against optional value imports.

+ 25 - 0
.agents/notes/implemented/bug-fix/2026-09-16-session-writer-held-feedback.zh.md

@@ -0,0 +1,25 @@
+# Agent Note: 会话写句柄占用反馈
+
+Status: implemented
+
+[English](2026-09-16-session-writer-held-feedback.md) | 中文
+
+## Problem
+
+Agent 空闲时,Session 仍可能持有写句柄。另一个 Host 无法恢复该会话,但通用内部错误 toast 没有提供恢复指引。持有者也可能属于同一个进程,因此仅凭占用不能确定是另一个正在运行的应用。
+
+## Decision
+
+当恢复因 `SessionAlreadyOwnedError` 失败且不存在可复用 Agent 时,Session Controller 返回 `session/writer-held`,携带 `{ sessionId }`。Client 遵循 [Remote 失败词汇](../architecture/2026-08-28-ctx-remote-failure-vocabulary.zh.md),按 code 判别。Controller 按 Error 名称识别持久化错误,与 Session Query 的可选依赖处理一致;加载 Controller 不需要持久化实现或错误类身份相同。
+
+发送和模型选择失败显示本地化指引,说明可能有其他 DSH 实例占用 Session,并建议退出其他实例后重试。模型选择将原始 Remote 结果返回给两个 UI 入口;错误分类不依赖稍后读取共享目录状态。[写租约决策](../feature/2026-08-31-cross-process-session-write-lease.zh.md) 继续拥有锁定和释放语义;此反馈既不抢占写权限,也不自动重试写入。
+
+## Alternatives considered
+
+**在 `session/agent-busy` 下使用自由文本 reason。** 第二个字符串判别值失去 code 与 details 的类型关系,并将写占用与提示词接纳失败混在一起。
+
+**单独的目录标志或必需的持久化 peer。** 标志重复记录操作失败,且可能在目录刷新后过时。仅为识别错误而强制依赖持久化,会移除对无持久化部署的支持。
+
+## Consequences
+
+Wire 新增一个有类型的失败码,不改变已存储的 Session 数据。恢复指引无法确定哪个进程持有句柄。Host 测试覆盖错误名称,以及有、无持久化时的普通失败;Client 测试覆盖两种语言和随模型操作返回的失败。无密钥的 `queue-actions` Web 场景通过随附组合固定写占用 toast 和保留草稿的输出。`verify-optional-dependency-imports` 防止可选依赖的值导入破坏模块加载。

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-09-16-windows-subprocess-console-visibility.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-16-windows-subprocess-console-visibility.md
+2026-09-16-windows-subprocess-console-visibility.md: 281525e960b049d5871cde9f5da5c8f928654440
+2026-09-16-windows-subprocess-console-visibility.zh.md: 04abf7154b4b384ca521b9d232c7dfb398f5ed99

+ 29 - 0
.agents/notes/implemented/bug-fix/2026-09-16-windows-subprocess-console-visibility.md

@@ -0,0 +1,29 @@
+# Agent Note: Hide Windows subprocess console windows at creation
+
+Status: implemented
+
+English | [中文](2026-09-16-windows-subprocess-console-visibility.zh.md)
+
+## Problem
+
+PTC runtime and shell calls share the Windows subprocess provider. Its ordinary Job runner omits window hiding, and native targets supply standard handles without a startup visibility flag. Desktop execution can therefore flash console windows for short-lived commands.
+
+## Decision
+
+The private Node Job runner uses `windowsHide: true`. Native ordinary and restricted-token process creation supplies `STARTF_USESHOWWINDOW` and `SW_HIDE` alongside standard handles before target code runs. Console inheritance, Job assignment before resume, and pipe ownership stay intact. No operation hides an existing parent console or promises to suppress windows explicitly opened by the command.
+
+The [ACL sandbox decision](../feature/2026-08-08-windows-acl-restricted-token-sandbox.md) still owns restricted-token policy and console-isolation limits. Initial window visibility does not require adding `CREATE_NO_WINDOW` or `CREATE_NEW_CONSOLE` to restricted creation.
+
+## Alternatives considered
+
+**Hide only the outer runner.** Native target creation is independent of Node's launch options, so its initial visibility also needs an explicit setting.
+
+**Remove consoles from every process.** Restricted-token creation with console-isolation flags has a recorded DLL initialization failure. Startup visibility preserves the existing console attachment rules instead.
+
+**Hide the window after PowerShell starts.** A window can become visible before the script executes; creation-time settings avoid that interval.
+
+## Consequences
+
+Ordinary subprocess startup suppresses incidental console windows without changing tool output or process cleanup. Native Windows tests inspect console visibility in a descendant and in both ACL modes, alongside existing stream, control-pipe, and Job-lifetime tests. A missing console is valid; the tests do not require one to exist. Startup-parameter tests pin the creation-time guarantee that a final visibility observation alone cannot establish.
+
+Session recordings cannot observe native console windows and their transcripts are unchanged. Windows native tests own this regression; browser screenshots cannot establish the absence of desktop windows.

+ 29 - 0
.agents/notes/implemented/bug-fix/2026-09-16-windows-subprocess-console-visibility.zh.md

@@ -0,0 +1,29 @@
+# Agent Note: 在创建时隐藏 Windows 子进程控制台窗口
+
+Status: implemented
+
+[English](2026-09-16-windows-subprocess-console-visibility.md) | 中文
+
+## 问题
+
+PTC 运行时和 shell 调用共用 Windows 子进程提供方。其普通 Job runner 未设置窗口隐藏,原生目标只提供标准句柄而没有启动可见性标志。因此,Desktop 执行短命令时可能闪现控制台窗口。
+
+## 决策
+
+私有 Node Job runner 使用 `windowsHide: true`。普通和受限令牌原生进程创建在目标代码运行前,将 `STARTF_USESHOWWINDOW` 和 `SW_HIDE` 与标准句柄一起传入。控制台继承、恢复线程前分配 Job 和管道归属保持不变。任何操作都不隐藏已有父控制台,也不承诺抑制命令显式打开的窗口。
+
+[ACL 沙箱决策](../feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md) 仍负责受限令牌策略和控制台隔离限制。设置初始窗口可见性不需要向受限进程创建添加 `CREATE_NO_WINDOW` 或 `CREATE_NEW_CONSOLE`。
+
+## 考虑过的替代方案
+
+**仅隐藏外层 runner。** 原生目标创建独立于 Node 启动选项,因此也需要明确设置初始可见性。
+
+**移除所有进程的控制台。** 受限令牌配合控制台隔离标志存在已记录的 DLL 初始化失败。启动可见性设置保留现有控制台附着规则。
+
+**PowerShell 启动后再隐藏窗口。** 窗口可能在脚本执行前已经可见;创建时设置可避免这一间隔。
+
+## 后果
+
+普通子进程启动抑制附带控制台窗口,不改变工具输出或进程清理。原生 Windows 测试检查后代进程和两种 ACL 模式的控制台可见性,并配合现有流、控制管道和 Job 生命周期测试。没有控制台也是有效状态;测试不要求控制台必须存在。启动参数测试固定创建时的保证,单独观察最终可见性无法确认这一点。
+
+会话录制无法观察原生控制台窗口,转录内容也没有变化。此回归由 Windows 原生测试负责;浏览器截图无法确认桌面窗口不存在。

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.i18n.yaml

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

+ 31 - 0
.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.md

@@ -0,0 +1,31 @@
+# Agent Note: Compact Markdown for Thinking
+
+Status: implemented
+
+English | [中文](2026-09-17-thinking-markdown.zh.md)
+
+## Problem
+
+Chat Thinking contains model-authored Markdown, but literal rendering exposes headings, emphasis markers, and code fences. The shared answer typography makes headings larger and more prominent than the secondary reasoning text. Trajectory applies that answer typography to the same reasoning.
+
+## Decision
+
+[MarkdownText](../../../../packages/client/ui-primitives/src/markdown/MarkdownText.tsx) owns a compact presentation variant used by Chat Thinking and Trajectory thinking details. It keeps the secondary content font size, line height, and tertiary color throughout. Heading levels retain semantic elements but share the same 600 weight and size; paragraphs, lists, quotes, and code use compact spacing. Links keep a resting dotted underline with tertiary color as a secondary-content exception to the [shared link styling](../feature/2026-09-04-web-clickable-link-styles.md); code retains its monospace font and background.
+
+Tables and formulas remain enabled through the existing parser. Their containers constrain horizontal overflow, and formula text inherits the secondary font size. Inline formulas retain native KaTeX baselines; enclosing text blocks own overflow so short formulas do not create scrollbars. Compact code banners stay in normal flow, keeping the [Thinking disclosure header](../feature/2026-08-03-web-sticky-collapsible-headers.md) above the scrolling content without a second sticky band.
+
+Chat passes its running state to the existing incremental Markdown renderer. The collapsed summary remains a separate single-line text projection. The parser, frozen-block cache, stored reasoning, and Session format do not change. Trajectory pins Thinking to its inspector’s fixed 13px/20px tier, independent of Chat’s content-size setting. Its answer output retains its existing typography and its [Thinking disclosure behavior](../feature/2026-09-09-ptc-trajectory-code-inspection.md).
+
+## Alternatives considered
+
+**Use answer typography unchanged.** Large headings and generous block spacing give reasoning more visual weight than the answer.
+
+**Style Markdown separately in each consumer.** Chat and Trajectory would need to track the same renderer elements independently; the primitive owns those elements and their compact presentation.
+
+**Disable tables and formulas.** The existing renderer already supports them. Bounded overflow and inherited font sizing preserve useful content without another parsing mode.
+
+## Consequences
+
+Thinking supports structured reading while retaining its secondary emphasis. Semantic headings remain available to assistive technology even though their visible hierarchy is flattened. The shared variant applies to both views; future Markdown element changes must preserve its typography as well as the default answer typography.
+
+Markdown soft line breaks collapse like answer prose; hard breaks and separate paragraphs require Markdown syntax. Streaming freezing operates on completed blocks, so an unfinished long paragraph remains in the mutable tail and is parsed again as it grows.

+ 31 - 0
.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.zh.md

@@ -0,0 +1,31 @@
+# Agent Note: Thinking 的紧凑 Markdown
+
+Status: implemented
+
+[English](2026-09-17-thinking-markdown.md) | 中文
+
+## 问题
+
+Chat Thinking 包含模型编写的 Markdown,但纯文本渲染直接显示标题、强调标记和代码围栏。共享回答排版会让标题比次级推理文本更大、更醒目。Trajectory 对同一推理内容使用了回答排版。
+
+## 决策
+
+[MarkdownText](../../../../packages/client/ui-primitives/src/markdown/MarkdownText.tsx) 拥有紧凑展示变体,供 Chat Thinking 和 Trajectory 思考详情使用。内容沿用次级字号、行高和 tertiary 颜色。各级标题保留语义元素,但统一使用 600 字重和相同字号;段落、列表、引用和代码采用紧凑间距。链接保留默认点状下划线与 tertiary 颜色,作为[共享链接样式](../feature/2026-09-04-web-clickable-link-styles.zh.md)的次级内容例外;代码保留等宽字体和底色。
+
+表格和公式继续通过既有解析器启用。其容器限制横向溢出,公式文本继承次级字号。行内公式保留 KaTeX 原生基线,由外层文本块承担溢出处理,短公式不会产生滚动条。紧凑代码栏保持正常文档流,让 [Thinking 折叠标题](../feature/2026-08-03-web-sticky-collapsible-headers.zh.md) 位于滚动内容上方,无需第二条 sticky 栏。
+
+Chat 将运行状态传给既有增量 Markdown 渲染器。折叠摘要仍是独立的单行文本投影。解析器、冻结块缓存、存储的推理内容和 Session 格式均保持不变。Trajectory 将 Thinking 固定在检查器的 13px/20px 层级,与 Chat 的内容字号设置无关。其回答输出保留既有排版及其 [Thinking 折叠行为](../feature/2026-09-09-ptc-trajectory-code-inspection.zh.md)。
+
+## 曾考虑的替代方案
+
+**直接使用回答排版。** 大标题和宽松块间距让推理比回答更醒目。
+
+**由每个使用方单独设置 Markdown 样式。** Chat 和 Trajectory 将需要分别跟踪相同的渲染元素;基元拥有这些元素及其紧凑展示。
+
+**禁用表格和公式。** 既有渲染器已支持二者。限制溢出和继承字号可以保留有用内容,无需增加另一种解析模式。
+
+## 后果
+
+Thinking 支持结构化阅读,同时保持次级强调。虽然视觉层级被统一,辅助技术仍可使用语义标题。共享变体同时用于两个视图;后续 Markdown 元素变更必须同时保留紧凑排版与默认回答排版。
+
+Markdown 软换行与回答正文一样折叠;硬换行和独立段落需要相应 Markdown 语法。流式冻结以完整块为单位,因此尚未结束的长段落仍处于可变尾部,并随增长重新解析。

+ 2 - 2
.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-06-sandbox.md
-2026-07-06-sandbox.md: 9f7139c8088df35ac87ddd66120dd8d6dc8e6e35
-2026-07-06-sandbox.zh.md: b8d8b1feb0db91431d0e71bea42299eafbc6856f
+2026-07-06-sandbox.md: 45a1c9c028f127c67e56f4436088852f59e10140
+2026-07-06-sandbox.zh.md: 6ad51a039583949849c853fab2f7c2a72d6d9db7

+ 3 - 3
.agents/notes/implemented/feature/2026-07-06-sandbox.md

@@ -86,7 +86,7 @@ The model sees the current effective file policy in the owner-derived `sandbox:p
 
 `ctx.sandboxPolicy.resolve()` stamps the complete execution policy — explicit escalation mode > session override > configured default, with `SessionHeader.cwd` > configured fallback root — before the executor runs. `SandboxBashExecutor.resolve()` retains that policy on the spec, or supplies the deployment fallback for a direct agentless caller, so `run()`/`start()` never read mutable session state. Per-process wrap facts are keyed by the returned `ShellProcess`; `onProcessDone()` receives spawn failure out of band from stderr classification and stamps that handle before `done` resolves, so overlapping processes retain their own modes and runner dialects.
 
-When a confining executor is mounted, `bash` advertises paired `sandbox_permissions` and `justification` fields. The schema exposes the full closed escalation vocabulary because effective mode is per-session; execution rejects any target that is not strictly wider than that call's effective mode. Approval resolves before execution. `allowed-once` stamps the granted mode onto only that request, while `rejected`, `cancelled`, `unavailable`, a missing approval service, or a missing agent all fail closed with distinct results. No grant is persisted.
+When a confining executor is mounted, `bash` advertises paired `sandbox_permissions` and `justification` fields. The schema exposes the full closed escalation vocabulary because effective mode is per-session; execution accepts a repeated effective mode without approval and rejects narrower or unsupported targets ([same-mode requests](2026-09-16-sandbox-same-mode.md)). Approval resolves before execution. `allowed-once` stamps the granted mode onto only that request, while `rejected`, `cancelled`, `unavailable`, a missing approval service, or a missing agent all fail closed with distinct results. No grant is persisted.
 
 Escalation is a same-turn retry of the denied command with the narrowest sufficient `sandbox_permissions` and a `justification`; the approval prompt is the consent step. It must be grounded in an actual denial, except when the session already observed the same denied access, and a disabled or rejected approval ends that command. The retry, approval decision, and result use existing tool and approval events. `dsh-tool-bash` owns the ask because the executor Service Definition has neither the agent nor call id required for user interaction.
 
@@ -164,7 +164,7 @@ Each phase gets its full design when picked up, validated against the code at th
 What shipped pins — the tiers in Testing hold each:
 
 - A denied command retried with `sandbox_permissions` + `justification` prompts the user through the composed answerer chain; a grant runs THAT call under the wider mode (result facts say so) while every other call keeps its own effective mode; every non-grant outcome produces its distinct error text and executes nothing.
-- The escalation fields exist exactly when the mounted executor confines; a request that is not strictly wider than the call's effective mode fails closed with its own text and prompts no one; a deployment with no ApprovalService fails escalating calls closed and leaves plain calls untouched.
+- The escalation fields exist exactly when the mounted executor confines; a repeated effective mode succeeds without approval; narrower or unsupported targets fail closed without prompting; a deployment with no ApprovalService fails escalating calls closed and leaves plain calls untouched.
 - One sourced policy-context message states the complete current sandbox and approval policies atomically; the whole exchange — context messages, headers, knob events, approval notices, approvals, and results — reconstructs from the session log alone, with no policy bookkeeping events beyond the two knob events.
 - One preset selection records only changed knob values, while a no-op selection records nothing; the next pre-step upserts both current values atomically, and a committed sandbox switch is honored by the next call's stamp.
 - A resumed session's overrides enter its first new policy-context message with no catch-up state; a composition default changed while the process was down likewise appears in that message.
@@ -182,7 +182,7 @@ Costs and accepted limits:
 - **Runner attribution uses an in-band protocol.** Exit status plus stderr cannot cryptographically identify the writer, so a confined child can mimic a fatal runner line and status to cause an availability/diagnostic false attribution. The conjunction and exact notice exclusion reduce accidental matches; this is not a sandbox bypass because the child is already confined.
 - **The launcher is a workspace dependency in source and an npm dependency after publication.** The main repository tests reviewed C source, native CI builds, and byte-pinned local tarballs together before publishing the same package family; the real-kernel e2e legs vouch for behavior through those installed bytes.
 - **The model may over-ask.** Escalating without denial grounding, or picking `danger-full-access` where `workspace-write` suffices: the description steers and the enum forces the ladder, but the human prompt is the actual gate; the `approval/asked` reasons make over-asking auditable, and a `prepend` policy answerer can auto-reject patterns a deployment never wants.
-- **The advertised target set is static while the effective mode is per-session** (schemas are registry-global) — a session already at the widest mode is still offered the fields. Harmless by construction: the strict-wider check at execution, not the enum, is the safety boundary — a non-widening request fails with its own text and never prompts anyone.
+- **The advertised target set is static while the effective mode is per-session** (schemas are registry-global) — a session already at the widest mode is still offered the fields. Harmless by construction: the strict-wider check at execution, not the enum, is the safety boundary — a same-mode request needs no approval, and narrower or unsupported targets fail without prompting.
 - **A granted escalation is not a working sandbox.** An unavailable backend still fails closed even for a granted escalation to a confining mode — at `confine()` when the platform has no chain or every probe fails, through the spawn channel when the selected executable cannot start, or through a structured rule when a started runner refuses — while a granted `danger-full-access` run never touches the provider at all: there the grant, not the probe, is the authority.
 - **Runtime-context history is append-only.** A policy switch appends a complete superseding snapshot after retained history, preserving the stable system-and-conversation prefix; unchanged state adds no message.
 - **Older policy snapshots remain in history.** Each full snapshot explicitly supersedes earlier runtime-context snapshots, so replay and compaction need only retain the latest materialized message.

+ 3 - 3
.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md

@@ -86,7 +86,7 @@ Landlock launcher 源码和包家族位于 `native/system`,与 harness 消费
 
 `ctx.sandboxPolicy.resolve()` 在执行器运行前盖章完整执行策略——显式升级模式 > 会话覆盖 > 配置默认值,且 `SessionHeader.cwd` > 配置的后备根目录。`SandboxBashExecutor.resolve()` 在 spec 上保留该策略,或为直接的无 agent 调用方提供部署后备值,使 `run()`/`start()` 永不读取可变会话状态。每进程包装事实以返回的 `ShellProcess` 为键;`onProcessDone()` 会通过 stderr 分类之外的通道接收 spawn 失败,并在 `done` 结算前给该句柄盖章,因此重叠进程各自保留自己的模式和 runner 方言。
 
-当约束执行器被挂载时,`bash` 公布配对的 `sandbox_permissions` 和 `justification` 字段。schema 暴露完整的封闭升级词汇,因为有效模式是按会话的;执行拒绝任何不严格宽于该调用有效模式的目标。批准在执行之前解析。`allowed-once` 仅将授权模式盖章到该请求上,而 `rejected`、`cancelled`、`unavailable`、缺失的 approval 服务或缺失的 agent 都以各自不同的结果文本失败关闭。授权不持久化。
+当约束执行器被挂载时,`bash` 公布配对的 `sandbox_permissions` 和 `justification` 字段。schema 暴露完整的封闭升级词汇,因为有效模式是按会话的;执行允许重复有效模式且无需审批,拒绝更窄或不支持的目标([同模式请求](2026-09-16-sandbox-same-mode.zh.md))。批准在执行之前解析。`allowed-once` 仅将授权模式盖章到该请求上,而 `rejected`、`cancelled`、`unavailable`、缺失的 approval 服务或缺失的 agent 都以各自不同的结果文本失败关闭。授权不持久化。
 
 升级是对被拒绝命令的同轮次重试,使用最窄的足够 `sandbox_permissions` 和一个 `justification`;批准提示词是同意步骤。它必须基于实际的拒绝,除非会话已观察到相同的被拒绝访问;禁用或被拒绝的批准终结该命令。重试、批准决策和结果使用既有的工具和批准事件。`dsh-tool-bash` 拥有请求动作,因为执行器 Service Definition 既没有 agent 也没有用户交互所需的 call id。
 
@@ -164,7 +164,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自能力边
 已交付并固定的内容——测试中的各层级分别保障:
 
 - 被拒绝的命令以 `sandbox_permissions` + `justification` 重试时,通过组合的应答器链提示用户;授权使该次调用在更宽模式下运行(结果事实如此报告),而其他所有调用保持各自的有效模式;每种非授权结果产生各自不同的错误文本且不执行任何内容。
-- 升级字段恰好在已挂载的执行器约束时存在;不严格宽于调用有效模式的请求以自身文本失败关闭且不提示任何人;没有 ApprovalService 的部署对升级调用失败关闭,对普通调用不影响。
+- 升级字段恰好在已挂载的执行器约束时存在;重复有效模式无需审批即可成功;更窄或不支持的目标不提示任何人并失败关闭;没有 ApprovalService 的部署对升级调用失败关闭,对普通调用不影响。
 - 一条带来源的策略上下文消息会以原子方式声明完整的当前沙箱策略与批准策略;整个交互——上下文消息、header、旋钮事件、批准通知、批准与结果——仅从会话日志即可重建,除两个旋钮事件外没有策略簿记事件。
 - 一次 preset 选择只记录发生变化的旋钮值,而无操作的选择不记录任何内容;下一次 pre-step 会原子 upsert 两个当前值,已提交的沙箱切换由下一次调用的盖章兑现。
 - 恢复会话的覆盖项会进入其首条新策略上下文消息,无需追赶状态;进程停止期间变更的组合默认值也会出现在该消息中。
@@ -182,7 +182,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自能力边
 - **Runner 归因使用带内协议。** 退出状态与 stderr 无法以密码学方式识别写入者,因此受限子进程可以模仿 runner 的致命诊断行和状态,造成可用性或诊断误归因。多项证据的合取与精确通知排除减少了意外匹配;这不是沙箱绕过,因为子进程已经受到限制。
 - **launcher 在源码中是 workspace 依赖,发布后是 NPM 依赖。** 主仓库会在发布同一个包家族之前,一起测试经审查的 C 源码、原生 CI 构建和字节固定的本地 tarball;真实内核 e2e 测试环节会验证这些安装字节的实际行为。
 - **模型可能过度请求。** 在没有拒绝依据的情况下升级,或在 `workspace-write` 足够时选择 `danger-full-access`:描述引导且枚举强制阶梯,但人的提示词是实际门控;`approval/asked` 原因使过度请求可审计,且 `prepend` 策略应答器可以自动拒绝部署永远不想要的模式。
-- **公布的目标集是静态的,而有效模式是按会话的**(schema 是注册表全局的)——已处于最宽模式的会话仍被提供这些字段。构造上无害:执行时的严格放宽检查(而非枚举)是安全边界——非放宽请求以自身文本失败且不提示任何人
+- **公布的目标集是静态的,而有效模式是按会话的**(schema 是注册表全局的)——已处于最宽模式的会话仍被提供这些字段。构造上无害:执行时的严格放宽检查(而非枚举)是安全边界——同模式请求无需审批,更窄或不支持的目标不提示任何人并失败
 - **授权的升级不等于可工作的沙箱。** 不可用的后端即使对授权升级到约束模式也仍然失败关闭——平台没有链或所有探测失败时在 `confine()` 阶段失败,所选可执行文件无法启动时通过 spawn 通道失败,已启动的 runner 拒绝时则通过结构化规则失败——而授权的 `danger-full-access` 运行根本不触及提供方:此时授权(而非探测)是权威。
 - **运行时上下文历史仅追加。** 策略切换会在保留的历史之后追加一份用于取代先前快照的完整快照,从而保留稳定的系统与对话前缀;状态不变时不添加消息。
 - **旧策略快照仍保留在历史中。** 每份完整快照都会明确取代更早的运行时上下文快照,因此回放与压缩(compaction)只需保留最新具体化的消息。

+ 2 - 2
.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
-2026-07-08-self-referential-cordis-toolset.md: d8ae7d26666964c24245021249473d4160b35220
-2026-07-08-self-referential-cordis-toolset.zh.md: d6fac5b59d1d057ea11b607cba115ceb1c37664a
+2026-07-08-self-referential-cordis-toolset.md: 278c71051de1fb897c8735b39dff0b66c43a5626
+2026-07-08-self-referential-cordis-toolset.zh.md: 8a8e197e197fb6b53b52426d5ad9a33fd09e78c9

+ 9 - 64
.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md

@@ -1,4 +1,4 @@
-# Agent Note: The self-referential cordis toolset
+# Agent Note: Cordis runtime inspection and runner isolation
 
 Status: implemented
 
@@ -6,79 +6,24 @@ English | [中文](2026-07-08-self-referential-cordis-toolset.zh.md)
 
 ## Problem
 
-Everything in this harness is a cordis plugin, but the agent running inside that plugin runtime cannot see or touch it: it cannot enumerate the services and events around it, cannot extend itself with a new tool mid-session, and cannot compose capabilities it invents. Handing the model that power is worth exploring — a self-referential agent that inspects and modifies its own runtime — but it raises three correctness problems at once, and the design is about answering them rather than the raw "let the model run code" mechanic.
-
-First, model-written registration must be validated where it happens: a malformed tool schema has to fail at registration, not when a later request tries to assemble it into a prompt. Second, model-written code has to call service APIs whose source it has never seen — guessed method signatures and, worse, guessed return-value shapes cost many steps of blind probing. Third, everything the model mounts must be fully disposable, by the model on demand and by the ordinary plugin lifecycle when the host plugin reloads, or a long session accretes orphaned listeners and tools.
+Runtime API discovery must describe the APIs a plugin can actually call. Process-local generated definitions also need registration validation and complete effect disposal; isolating JavaScript globals alone does not constrain the authority of injected services.
 
 ## Decision
 
-The toolset ships as [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/extensions/tool-cordis/README.md), with its runnable overlay and usage in the [runtime Cordis guide](../../../../docs/user/develop/practice/dynamic-cordis.md). It gives the model three tools over the live Cordis runtime in the current DSH process: inspect it, mount an in-memory temporary Plugin, and unmount that Plugin to quiescence.
-
-The vm isolates accidental global pollution, and the context façade hides framework internals. Neither restricts the authority of exposed services: a temporary Plugin can call `ctx.shell` with the host executor's privileges and reach the real filesystem and web services. It runs in the shared DSH runtime and may affect other sessions in that process. This is an opt-in development tool with bash-equivalent trust, not a security boundary or product default.
-
-### The three tools
-
-| Tool | Contract |
-|---|---|
-| `cordis_inspect` | Read-only report over the live current-process runtime, one Markdown section per `what` value (omit `what` for all sections). `plugins` lists every live fiber; `temporary` lists only the temporary Plugins created by `cordis_mount`. An exact `name` with `what: "api"` or `what: "events"` narrows to one source-documented target. |
-| `cordis_mount` | Evaluates `code` now as an async JavaScript-function body in a `node:vm` sandbox and saves it nowhere. The returned Plugin is mounted under the internal `cordis-dynamic` group and tracked under a fresh process-local id (`dyn-1`, `dyn-2`, …). |
-| `cordis_unmount` | Unmounts one `cordis_mount` temporary Plugin by id and returns only after every owned tool, listener, service, timer, and effect reaches quiescence. It cannot remove Loader, configured, or installed Plugins. |
-
-`cordis_inspect` sections are `services` (every provided ctx service and owning fiber), `plugins` (every live plugin fiber), `tools` (what the model can call), `temporary` (the `cordis_mount` subset with id, running/pending state, provided and awaited services, and lifetime), `api` (live service signatures and referenced types), and `events` (harness events with dispatch mode and signature). Temporary Plugins remain active across later turns and disappear after `cordis_unmount`, toolset unload, or DSH restart; they are never restored automatically. Broad `api` and `events` reports omit full JSDoc to stay compact; an exact `name` returns one service or event with its original method/declaration JSDoc. A name is invalid with other sections, unknown targets fail, and an API target must be live. The model-facing tool descriptions carry the operational rules needed at call time; [the generated tool catalog](../../../../docs/tool-catalog.md) is their exhaustive rendering.
-
-### Sandbox semantics
-
-Mount code runs as an async-function body in a fresh vm realm. Its documented API steers file, network, process, and timer access through Cordis services so mounts remain inspectable and disposable. Host-realm helpers still make Node escape possible, consistent with the trusted posture. `vmTimeoutMs` bounds only synchronous evaluation.
-
-Sandbox globals are deliberately small: a tagged write-through `console` (`[cordis:<id>] …` on the host stdout/stderr, so a listener that fires long after the mount call still lands somewhere the user sees), the `harness.defineTool` / `harness.registerTool` registration pair, the encoding primitives fresh vm contexts lack (`btoa`/`atob` as host closures over `Buffer` — a sanctioned exception, `Buffer` itself is never exposed — plus `TextEncoder`/`TextDecoder`), and callable traps over the withheld Node APIs (`require`, `setTimeout`/`setInterval`/`setImmediate`/`clearTimeout`/`clearInterval`, `fetch`) that throw a redirect naming the cordis alternative. Only function-shaped globals are trapped; `process` and `Buffer` stay `undefined` so a `typeof` feature probe stays inert rather than detonating a throwing accessor.
-
-Mount code crosses the vm boundary through three controls. Dual-realm `instanceof` recognizes both host and vm objects. `harness.defineTool` rebuilds the output schema/projectors in the host realm, snapshots the body value as host-owned JSON, and lets the registry enforce the [canonical tool-output contract](../architecture/2026-07-20-canonical-tool-output-contract.md) before observation. The mounted plugin receives a whitelist context façade, not a raw or pass-through `Context`; framework plumbing and context-valued returns are rejected. Service reads require a declared `inject`, preserving Cordis activation and unload semantics. `ctx.tools.get` exposes only the schema view, so mounted code cannot bypass `ToolRuntime.execute` by calling a definition directly.
-
-The boundary normalizes unambiguous JSON-Schema forms into `ParameterSchemaSpec`, preserving `integer`, raw object openness, and required arrays. Direct DSL object nodes must declare `additionalProperties`; invalid vocabulary fails with the accepted alternatives. Parse, TypeScript, missing-return, Node-API, and duplicate-tool errors include the relevant source line or corrective contract without narrating implementation internals.
-
-### The internal group and temporary-Plugin lifecycle
+`cordis_inspect_list` and `cordis_inspect_query` expose read-only runtime discovery. Generated catalogs retain source-owned declarations and JSDoc, intersected with live providers. The catalog generator rejects freshness drift; detailed queries avoid charging every request for complete API declarations.
 
-Every temporary Plugin is a child of one internal `cordis-dynamic` group beneath the tool plugin, so ordinary fiber disposal handles toolset reload and unload. `cordis_mount` awaits settlement; startup failure disposes the fiber before returning an error. A settled pending Plugin remains visible with its missing injections. `cordis_unmount` awaits the Plugin fiber's disposal.
+The Host and Client runners retain their programmatic lifecycle and browser consumers. A Host definition evaluates in a fresh vm realm and receives a context façade: service access requires declared injection, framework internals are hidden, and registrations belong to the definition's fiber. Tool output normalization crosses back into the Host realm before validation. Disposal awaits the fiber's owned effects. The vm prevents accidental global pollution; injected filesystem, shell, and network services still have real authority, so it is not a security boundary.
 
-Temporary Plugins exist only in process memory. They create no Plugin file, install no package, change no `cordis.yml` or personal/project configuration, do not survive restart, and have no automatic save, promote, or install path. Keeping an experiment means asking the Agent to implement a normal project Plugin or installable profile bundle through the regular development workflow.
-
-### Cross-mount composition via provide/inject
-
-Mounts relate to each other through ordinary cordis service semantics, with their ids as the lifecycle handles: mount A calls `ctx.provide('foo', value)`, mount B declares `inject: ['foo']` and activates the moment `foo` exists; mounted first, B stays pending and names the missing service; unmounting A sends B back to pending (its registrations unwound) and a later re-provide re-runs B's `apply` through a fresh sandbox façade; a duplicate provide fails loud with the owning fiber named. One realm caveat: a service value provided by a mount is a vm-realm object — method calls on it work from anywhere, but consumers must not assume host prototypes on it.
-
-### The generated API catalog
-
-`cordis_inspect` serves API and event data from a generated catalog rather than a duplicated table. The generator reuses the Cordis catalog AST scan and emits service summaries, signatures, original service-method and event JSDoc, event modes, referenced type declarations, and the inherited context API. Ambiguous type names are omitted and oversized declarations are marked as truncated.
-
-Freshness is gated like every generated artifact: `pnpm run verify-cordis-api` (in `doc-sync`) regenerates in memory and fails on any diff, so a JSDoc or public-signature edit cannot ship without regenerating the catalog the model reads. At runtime the inspect tool intersects the catalog with the live runtime rather than dumping it: broad reports render live catalogued services as summary + signatures, live services without a catalog entry (mount-provided ones) as name + owning fiber, catalogued services with no live provider tersely, and then the referenced type shapes. Exact-name reports render one live service or event with the original JSDoc immediately before each signature; keeping that detail opt-in avoids charging its token cost on exploratory listings.
-
-### Configuration, rendering, and observability
-
-The plugin exposes one config field, validated by schemastery and documented in [the config catalog](../../../../docs/config-catalog.md): `vmTimeoutMs` (default 5000), the millisecond bound on the synchronous portion of code evaluation. The current model-facing names are `cordis_inspect`, `cordis_mount`, and `cordis_unmount`; the internal `cordis-dynamic` group name and `dyn-` id prefix remain structural vocabulary. All three tools render as `generic` cards per [the tool cookbook](../../../../docs/cookbook/adding-a-tool.md): inspect is `read`, mount is `execute` carrying code as `rawInput`, and unmount is `delete`. Web conversation rows preserve those generic mechanics while giving the tools the action titles `Inspect`, `Mount temporary Plugin`, and `Unmount temporary Plugin` plus one shared Cordis accent; the mount row retains the shared JavaScript expansion and syntax highlighting.
-
-Model-visible ⟺ logged holds with no new session event type: mount and unmount are visible through their logged `tool/call` / `tool/result` pairs, and any changed tool set is logged by the full changed request header emitted when schemas change between steps. Temporary Plugins are process memory, not session state: session resume rehydrates conversation history but never recreates them.
+Definitions remain process-local. Restart and session resume do not recreate them from historical calls. The [Creator persistent plugin decision](../architecture/2026-09-16-creator-persistent-plugin-management.md) owns agent-authored installation, approval, and profile persistence. Shipped model tools do not create or mutate runner definitions.
 
 ## Alternatives considered
 
-**A structured per-capability registration tool instead of `cordis_mount`.** The most tempting alternative is a `cordis_register_tool` with explicit `name` / `description` / `parameters` / `code` fields (and siblings `cordis_register_listener`, `cordis_register_service`, …) rather than a single "mount a plugin" primitive. It was rejected because its one real win — no plugin boilerplate for the single commonest case — does not pay for its costs, while a single mount primitive answers every capability at once.
-
-| Dimension | Structured per-capability tools | Single `cordis_mount` |
-|---|---|---|
-| Schema correctness | `parameters` is still model-written JSON needing unified-schema validation, merely one step earlier | The same validation runs at the sandbox boundary, with the same instructive errors |
-| The code field | An `execute` body is still model-written JS in a vm; the realm and service-call correctness problems are unchanged | One sandbox, one normalization path, one guarded registration |
-| Capability coverage | Tools only; listeners, services, `inject` relations each need another structured tool — an API that grows without bound | One vocabulary (a cordis plugin) covers every effect, present and future |
-| Cross-mount composition | Not expressible in a tool-registration payload | Native `provide`/`inject`, ordinary cordis semantics |
-| Inspectability | Registers something the plugin list cannot show as a plugin | What the model mounts is exactly what `cordis_inspect` renders |
-| Model ergonomics | Wins for the single most common case (no plugin boilerplate) | Mitigated by the canonical recipe in the mount description plus boundary errors that teach the fix |
-
-The correctness investment therefore goes where it pays for every capability at once: the generated API catalog surfaced through `cordis_inspect`, and sandbox-boundary validation whose error messages teach the correct call. A structured registration tool remains addable later as sugar that synthesizes mount code; nothing here forecloses it.
-
-**A hand-maintained service/event reference in the tool.** The first cut of the inspect tool carried a hand-written table of service method signatures. It was replaced by the generated `api-catalog.ts` because a hand table drifts from the JSDoc the moment a signature changes and nothing gates the drift, whereas the generated artifact is freshness-checked against the same AST the docs use.
+**Hand-maintained API tables.** Rejected because they drift independently from service declarations; generated catalogs and freshness checks share one source.
 
-**A new `cordis/mount` session event.** A durable event recording each mount's source and name has clear precedent (`hook/invoked`, `compaction/start`). Rejected: mount and unmount are already visible as `tool/call` / `tool/result` pairs and the tool-set change is already logged as a full changed request header, so a dedicated event would only duplicate the record. It remains addable if an audit use case needs the mount source and name outside the tool call.
+**Treat the vm as a security sandbox.** Rejected because services exposed through the façade can reach the Host's real resources. Restricted globals improve lifecycle correctness, not permission enforcement.
 
-**A hardened / capability-restricted sandbox.** Trapping Node built-ins and handing mount code a whitelist façade rather than the raw context might suggest an intent to sandbox for safety. It is explicitly not that: the traps and the façade narrow the *API* mount code sees — steering it onto cordis services and away from leak-prone Node built-ins and framework internals — for correctness and to close the unguarded-context escape, but the capabilities the façade exposes (`ctx.shell`, `ctx.fs`, `ctx.web`) reach the real runtime, so it is not a security boundary. A real one (separate process, permission prompts) was out of scope for a dev/opt-in toolset and would fight the entire point — handing the model the live runtime.
+**Recreate definitions from session logs.** Rejected because replaying source would execute historical side effects. Historical cards display persisted source and outcomes without restoring a live definition.
 
 ## Consequences
 
-The toolset is a deliberate opt-in with a fully-privileged `ctx`, so a deployment adopts it as consciously as a bash tool. Several facts follow that the tool descriptions warn the model about directly: a waterfall listener (e.g. `tools/pre-execute`) that returns without calling `next()` short-circuits the chain, so a mounted listener can stop the agent's own tool dispatch ([waterfall semantics](../../../../docs/cordis-primer.md#cordis-waterfall-semantics)); mount code runs inside a tool call of the current turn, so awaiting anything that resolves only after the turn deadlocks; `vmTimeoutMs` bounds synchronous evaluation only; and mounts do not survive session resume.
+Inspection remains usable without Creator or Plugin Manager. Runner lifecycle tests cover guarded registration, startup failure cleanup, and awaited disposal. Existing browser consumers retain their lifecycle APIs; removing their write-side registry and activation machinery requires a coordinated replacement of those consumers, rather than deleting historical rendering or assuming session data recreates live state.

+ 13 - 68
.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md

@@ -1,84 +1,29 @@
-# Agent Note: 自引用 cordis 工具集
+# Agent Note:Cordis 运行时检查与 runner 隔离
 
 Status: implemented
 
 [English](2026-07-08-self-referential-cordis-toolset.md) | 中文
 
-## 问题
+## Problem
 
-本 harness 中的一切都是 cordis 插件,但运行在该插件运行时内部的 agent(智能体)既看不到也碰不到它:它无法枚举周围的服务和事件,无法在会话中途为自己添加新工具,也无法组合自己发明的能力。赋予模型这种能力值得探索——一个能审视并修改自身运行时的自引用 agent——但这同时引发三个正确性问题,本设计的核心正是回答这些问题,而非单纯的「让模型执行代码」机制
+运行时 API 发现必须描述插件实际能调用的 API。进程内生成定义还需要注册校验和完整的 effect 释放;仅隔离 JavaScript 全局变量并不能限制注入服务的权限
 
-第一,模型编写的注册必须在注册发生时就完成校验:格式错误的工具 schema 必须在注册时失败,而不是等到后续请求尝试将其组装进提示词时才报错。第二,模型编写的代码需要调用它从未见过源码的服务 API——靠猜测方法签名、更糟糕的是猜测返回值结构,会消耗大量盲目试探的步骤。第三,模型挂载的一切都必须完全可释放:模型可以按需释放,普通的插件生命周期在宿主插件重载时也会释放,否则长会话会积累遗留的监听器和工具。
+## Decision
 
-## 决策
+`cordis_inspect_list` 和 `cordis_inspect_query` 提供只读运行时发现。生成目录保留源码中的声明和 JSDoc,并按存活 provider 筛选。目录生成器拒绝过期产物;精确查询避免每次请求都承担完整 API 声明的 token 成本。
 
-该工具集以 [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/extensions/tool-cordis/README.zh.md) 发布,其可运行 overlay 与用法位于[运行时 Cordis 指南](../../../../docs/user/develop/practice/dynamic-cordis.zh.md)。它为模型提供三个工具,用于操作当前 DSH 进程中的活跃 Cordis 运行时:检查该运行时、挂载一个仅存于内存的临时插件,再将该插件卸载至完全停稳
+Host 和 Client runner 保留程序侧生命周期和浏览器消费者。Host 定义在新的 vm realm 中求值,并接收 context façade:服务访问需要声明注入,框架内部结构不可见,注册归定义的 fiber 所有。工具输出先回到 Host realm 进行归一化,再接受校验。释放会等待 fiber 所有的 effect。vm 防止意外污染全局变量;注入的文件系统、shell 和网络服务仍有真实权限,因此它不是安全边界
 
-vm 隔离了意外的全局污染,上下文门面隐藏了框架内部细节。但二者都不限制已暴露服务的权限:临时插件可以调用 `ctx.shell` 以宿主执行器的权限运行命令,也能访问真实的文件系统和网络服务。它运行在共享 DSH 运行时中,可能影响同一进程的其他会话。这是一个需要显式启用的开发工具,信任等级与 bash 相当,不是安全边界,也不是产品默认配置
+定义仅存在于进程中。重启和会话恢复不会从历史调用重建它们。[Creator 持久化插件决策](../architecture/2026-09-16-creator-persistent-plugin-management.zh.md)负责 agent 安装、审批和 profile 持久化。内置模型工具不创建或修改 runner 定义
 
-### 三个工具
+## Alternatives considered
 
-| 工具 | 约定 |
-|---|---|
-| `cordis_inspect` | 当前进程活跃运行时的只读报告,每个 `what` 值对应一个 Markdown 小节(省略 `what` 则输出全部小节)。`plugins` 列出全部存活 fiber,`temporary` 只列 `cordis_mount` 创建的临时插件。精确 `name` 搭配 `what: "api"` 或 `what: "events"` 可收窄到一个带源码文档的目标。 |
-| `cordis_mount` | 立即在 `node:vm` 沙箱中把 `code` 作为异步 JavaScript 函数体求值,且不保存到任何位置。返回的插件挂在内部 `cordis-dynamic` 分组下,并用新的进程内 id(`dyn-1`、`dyn-2`……)跟踪。 |
-| `cordis_unmount` | 按 id 卸载一个 `cordis_mount` 临时插件,并只在其自有工具、监听器、服务、定时器和其他 effect 完全停稳后返回。它不能删除 Loader、已配置或已安装的插件。 |
+**手写 API 表。** 拒绝,因为它会独立于服务声明漂移;生成目录与新鲜度检查共享同一源码。
 
-`cordis_inspect` 的小节是 `services`(每个已提供的 ctx 服务及所属 fiber)、`plugins`(全部存活插件 fiber)、`tools`(模型可调用的工具)、`temporary`(`cordis_mount` 子集,包含 id、running/pending 状态、提供与等待的服务和生命周期)、`api`(活跃服务签名及其引用类型)和 `events`(harness 事件及分发模式和签名)。临时插件可跨后续轮次保持活跃,并在 `cordis_unmount`、工具集卸载或 DSH 重启后消失;系统绝不会自动恢复它们。宽泛的 `api` 和 `events` 报告省略完整 JSDoc;精确 `name` 返回一个服务或事件及其原始 JSDoc。其他小节不能搭配 name,未知目标会失败,而 API 目标必须处于活跃状态。面向模型的工具描述包含调用时所需的操作规则;[生成的工具目录](../../../../docs/tool-catalog.zh.md)是这些规则的完整呈现
+**将 vm 视为安全沙箱。** 拒绝,因为 façade 暴露的服务可以访问 Host 的真实资源。受限全局变量改善生命周期正确性,不执行权限控制。
 
-### 沙箱语义
+**从会话日志重建定义。** 拒绝,因为重放源码会执行历史副作用。历史卡片展示持久化源码和结果,不恢复运行中的定义。
 
-挂载代码以异步函数体的形式在一个新的 vm realm 中运行。其文档化的 API 将文件、网络、进程和定时器访问引导至 Cordis 服务,使挂载保持可审视和可释放。宿主 realm 的辅助手段仍然使 Node 逃逸成为可能,这与信任姿态一致。`vmTimeoutMs` 仅约束同步执行部分。
+## Consequences
 
-沙箱全局变量刻意精简:一个带标签的直写 `console`(在宿主 stdout/stderr 上输出 `[cordis:<id>] …`,这样在挂载调用之后很久才触发的监听器输出仍能落到用户可见的地方)、`harness.defineTool`/`harness.registerTool` 注册对、新 vm 上下文缺少的编码原语(`btoa`/`atob` 作为基于 `Buffer` 的宿主闭包——这是一个明确允许的例外,`Buffer` 本身从不暴露——加上 `TextEncoder`/`TextDecoder`),以及对未暴露的 Node API 设置的可调用陷阱(`require`、`setTimeout`/`setInterval`/`setImmediate`/`clearTimeout`/`clearInterval`、`fetch`),这些陷阱会抛出一条重定向消息指明 cordis 替代方案。只有函数形态的全局变量才设陷阱;`process` 和 `Buffer` 保持 `undefined`,这样 `typeof` 特性探测仍然无害,而不会触发会抛出异常的访问器。
-
-挂载代码通过三道控制跨越 vm 边界。双 realm `instanceof` 同时识别宿主和 vm 对象。`harness.defineTool` 在宿主 realm 中重建输出 schema/投影器,将工具体返回值快照为宿主自有的 JSON,并让注册表在观测前强制执行[规范工具输出约定](../architecture/2026-07-20-canonical-tool-output-contract.zh.md)。挂载的插件接收的是一个白名单上下文门面,而非原始或透传的 `Context`;框架内部机制和以上下文为值的返回会被拒绝。服务读取需要声明 `inject`,保留 Cordis 的激活与卸载语义。`ctx.tools.get` 仅暴露 schema 视图,因此挂载代码无法绕过 `ToolRuntime.execute` 直接调用定义。
-
-边界将无歧义的 JSON Schema 形式规范化为 `ParameterSchemaSpec`,同时保留 `integer`、原始对象开放性和 required 数组。直接使用 DSL 的对象节点必须声明 `additionalProperties`;无效词汇会报错并给出可接受的替代方案。解析错误、TypeScript 错误、缺少 return、Node API 误用和重复工具名等错误信息包含相关源码行或纠正性约定,不叙述实现内部细节。
-
-### 内部分组与临时插件生命周期
-
-每个临时插件都是工具插件下方内部 `cordis-dynamic` 分组的子节点,因此普通的 fiber 释放即可处理工具集重载和卸载。`cordis_mount` 会等待 settlement;启动失败时在返回错误前释放 fiber。已 settle 但处于 pending 状态的插件仍然可见,并列出其缺失的注入。`cordis_unmount` 等待插件 fiber 的释放完成。
-
-临时 Plugin 只存在于进程内存中。它不会创建 Plugin 文件、安装 package、修改 `cordis.yml` 或个人/项目配置、跨重启存续,也不存在自动保存、转正式或安装路径。若要保留实验结果,应让 Agent 通过常规开发流程实现普通的项目 Plugin 或可安装的 profile 组合包。
-
-### 通过 provide/inject 实现跨挂载组合
-
-挂载之间通过普通的 cordis 服务语义相互关联,以各自的 id 作为生命周期句柄:挂载 A 调用 `ctx.provide('foo', value)`,挂载 B 声明 `inject: ['foo']` 并在 `foo` 存在的瞬间激活;如果 B 先挂载,它保持 pending 状态并列出缺失的服务;卸载 A 使 B 回到 pending(其注册被撤销),之后重新 provide 会通过一个新的沙箱门面重新运行 B 的 `apply`;重复 provide 会明确报错并指出拥有该服务的 fiber。一个 realm 注意事项:由挂载 provide 的服务值是 vm realm 对象——从任何地方调用其方法都能工作,但消费方不得假设它具有宿主原型。
-
-### 生成的 API 目录
-
-`cordis_inspect` 从生成的目录提供 API 和事件数据,而非维护一份重复的表格。生成器复用 Cordis 目录的 AST 扫描,输出服务摘要、签名、原始服务方法与事件 JSDoc、事件模式、引用的类型声明以及继承的上下文 API。有歧义的类型名被省略,过大的声明被标记为截断。
-
-新鲜度像所有生成产物一样受门禁约束:`pnpm run verify-cordis-api`(在 `doc-sync` 中)在内存中重新生成并在有任何 diff 时失败,因此 JSDoc 或公开签名变更如果不重新生成模型读取的目录就无法合入。运行时 inspect 工具将目录与活跃运行时取交集而非直接转储:宽泛报告把有目录条目的活跃服务渲染为摘要 + 签名,把没有目录条目的活跃服务(挂载提供的)渲染为名称 + 所属 fiber,简要列出有目录条目但无活跃提供方的服务,再附上引用的类型结构。精确名称报告渲染一个活跃服务或事件,并把原始 JSDoc 紧靠在每个签名之前;让该细节按需出现,避免探索性列表承担其 token 成本。
-
-### 配置、渲染与可观测性
-
-该插件暴露一个配置字段,由 schemastery 校验并记录在[配置目录](../../../../docs/config-catalog.zh.md)中:`vmTimeoutMs`(默认 5000),代码同步求值部分的毫秒上限。当前面向模型的名称是 `cordis_inspect`、`cordis_mount` 和 `cordis_unmount`;内部 `cordis-dynamic` 分组名和 `dyn-` id 前缀仍是结构性词汇。三个工具均按[工具实操手册](../../../../docs/cookbook/adding-a-tool.zh.md)渲染为 `generic` 卡片:inspect 为 `read`,mount 为携带代码 `rawInput` 的 `execute`,unmount 为 `delete`。Web 对话行保留这些通用机制,同时为各工具设置操作标题 `Inspect`、`Mount temporary Plugin` 和 `Unmount temporary Plugin` 以及统一的 Cordis 强调色;mount 行仍使用共用的 JavaScript 展开视图和语法高亮。
-
-「模型可见 ⟺ 已记录」成立,且无需新的会话事件类型:mount 与 unmount 通过已记录的 `tool/call`/`tool/result` 对可见,当步骤之间的 schema 发生变化时,系统发出的完整 request header 会记录工具集的任何变化。临时插件属于进程内存,而非会话状态:恢复持久化会话只会重建对话历史,绝不会重新创建它们。
-
-## 曾考虑的替代方案
-
-**用结构化的逐能力注册工具替代 `cordis_mount`。** 最具吸引力的替代方案是一个带有显式 `name`/`description`/`parameters`/`code` 字段的 `cordis_register_tool`(以及配套工具 `cordis_register_listener`、`cordis_register_service`……),而非单一的「挂载一个插件」原语。否决原因:它唯一的真正优势——对最常见的单一场景免去插件样板代码——不足以抵偿其代价,而单一的 mount 原语能一次性覆盖所有能力。
-
-| 维度 | 结构化逐能力工具 | 单一 `cordis_mount` |
-|---|---|---|
-| schema 正确性 | `parameters` 仍然是模型编写的 JSON,需要统一 schema 校验,只是提前了一步 | 同样的校验在沙箱边界运行,同样的指导性错误信息 |
-| 代码字段 | `execute` 函数体仍然是 vm 中模型编写的 JS;realm 和服务调用的正确性问题不变 | 一个沙箱、一条规范化路径、一处受保护的注册 |
-| 能力覆盖面 | 仅限工具;监听器、服务、`inject` 关系各需另一个结构化工具——API 无限增长 | 一套词汇(cordis 插件)覆盖当前和未来的所有效果 |
-| 跨挂载组合 | 在工具注册载荷中无法表达 | 原生 `provide`/`inject`,普通的 cordis 语义 |
-| 可审视性 | 注册的东西无法在插件列表中显示为插件 | 模型挂载的正是 `cordis_inspect` 渲染的 |
-| 模型易用性 | 对最常见的单一场景有优势(无插件样板) | 通过 mount 描述中的规范示例加边界错误信息教会正确调用来缓解 |
-
-因此正确性投入放在能一次性为所有能力带来回报的地方:通过 `cordis_inspect` 呈现的生成 API 目录,以及沙箱边界校验(其错误信息教会正确的调用方式)。结构化注册工具日后仍可作为语法糖添加,由它合成 mount 代码;本设计不排斥这一可能。
-
-**在工具中手工维护服务/事件参考。** inspect 工具的第一版携带了一份手写的服务方法签名表。它被生成的 `api-catalog.ts` 取代,因为手写表在签名变化的瞬间就会与 JSDoc 脱节且没有门禁约束这种漂移,而生成产物的新鲜度由文档使用的同一套 AST 检查。
-
-**新增 `cordis/mount` 会话事件。**一个持久事件记录每次挂载的源码和名称,有明确先例(`hook/invoked`、`compaction/start`)。不予采纳:挂载和卸载已经作为 `tool/call`/`tool/result` 对可见,工具集变化已经作为完整的变更 request header 被记录,因此专用事件只会重复记录。如果审计用例需要在工具调用之外取得挂载的源码和名称,日后仍可添加。
-
-**加固的/能力受限的沙箱。** 对 Node 内置模块设陷阱并向挂载代码提供白名单门面而非原始上下文,可能暗示意图是为安全而沙箱化。这里明确不是:陷阱和门面收窄的是挂载代码所见的 *API*——将其引导至 cordis 服务、远离易泄漏的 Node 内置模块和框架内部——目的是正确性和封堵未受保护的上下文逃逸,但门面暴露的能力(`ctx.shell`、`ctx.fs`、`ctx.web`)触及真实运行时,因此它不是安全边界。真正的安全边界(独立进程、权限提示)超出了一个开发/显式启用工具集的范围,且会与其核心目的——将活跃运行时交给模型——相冲突。
-
-## 后果
-
-该工具集是刻意的显式启用设计,具有完整权限的 `ctx`,因此部署方采用它的意识程度应与 bash 工具相当。以下几个事实由工具描述直接告知模型:一个 waterfall(瀑布式事件)监听器(如 `tools/pre-execute`)如果不调用 `next()` 就返回,会短路整条链,因此一个挂载的监听器可以阻止 agent 自身的工具分发([waterfall 语义](../../../../docs/cordis-primer.zh.md#cordis-waterfall-semantics));挂载代码在当前轮次的工具调用内运行,因此 await 任何只在该轮次结束后才 resolve 的东西会导致死锁;`vmTimeoutMs` 仅约束同步执行;挂载不会在会话恢复后存活。
+检查能力不依赖 Creator 或 Plugin Manager。Runner 生命周期测试覆盖受保护注册、启动失败清理和等待释放。现有浏览器消费者保留生命周期 API;移除其写入注册表与激活机制,需要协调替换这些消费者,不能删除历史渲染或假定会话数据会重建运行状态。

+ 2 - 2
.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.md
-2026-08-03-web-sticky-collapsible-headers.md: 6e8b4b94c9c6aa40bc7190e6705b84caa8189c52
-2026-08-03-web-sticky-collapsible-headers.zh.md: d4e1f69539db6870ae60d5c4b05cd52c7d129360
+2026-08-03-web-sticky-collapsible-headers.md: 5bbe91681e656a8278b9d65f12e20a4d9dc779d0
+2026-08-03-web-sticky-collapsible-headers.zh.md: c75846fa7f5ca1088dab530b63243553e50f9579

+ 1 - 1
.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.md

@@ -14,7 +14,7 @@ The disclosure header of each uncapped block sticks to the conversation scroll c
 
 Both blocks already scroll against the shared conversation scroll container (`[data-conversation-scroll]`), not an inner box, so `position: sticky; top: 0` on the header pins it against that container. A base-token background masks the prose that scrolls under the pinned header.
 
-The pinned header's stacking rank differs by block, because their bodies differ. The Think body is plain text with no sticky descendant, so `z-index: 1` suffices. The compaction body renders markdown, and a fenced code block in the summary pins its own banner at `z-index: 6` (`packages/client/ui-primitives/src/markdown/CodeBlock.module.css`) with a Copy control inside it; the compaction header therefore uses `z-index: 7` and holds that banner below its own band, so the header never covers the Copy control and the banner never covers the toggle. The band's height is one component-local measurement (`--dsh-compaction-header-height` on `.compactionRow`) shared by the toggle's `height` and the banner's `top`; the banner rule wins over CodeBlock's `top: 0` by specificity, because the two sheets load in their own packages. The pinned header also overrides its hover fill to the opaque `--dsw-alias-interactive-bg-hover-solid` token and squares its corners for as long as the row is open: the default translucent hover token would let the scrolling prose show through the moment the pointer lands on the toggle, and the base 6px radius would leave the same prose visible at the corners. `:hover` raises the rule's specificity above the later base hover rule, so declaration order does not decide the winner. Two other elements take `z-index: 7` to clear the same code banners: the composer seat and the turn-navigation rail slot (`TurnNavigator.module.css`, ui-chat). Their order among equal ranks is stated once, in the composer seat's comment (`packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css`).
+The pinned header's stacking rank differs by block, because their bodies differ. The Think body uses [compact Markdown](../bug-fix/2026-09-17-thinking-markdown.md) with code banners in normal flow, so `z-index: 1` suffices. The compaction body renders markdown, and a fenced code block in the summary pins its own banner at `z-index: 6` (`packages/client/ui-primitives/src/markdown/CodeBlock.module.css`) with a Copy control inside it; the compaction header therefore uses `z-index: 7` and holds that banner below its own band, so the header never covers the Copy control and the banner never covers the toggle. The band's height is one component-local measurement (`--dsh-compaction-header-height` on `.compactionRow`) shared by the toggle's `height` and the banner's `top`; the banner rule wins over CodeBlock's `top: 0` by specificity, because the two sheets load in their own packages. The pinned header also overrides its hover fill to the opaque `--dsw-alias-interactive-bg-hover-solid` token and squares its corners for as long as the row is open: the default translucent hover token would let the scrolling prose show through the moment the pointer lands on the toggle, and the base 6px radius would leave the same prose visible at the corners. `:hover` raises the rule's specificity above the later base hover rule, so declaration order does not decide the winner. Two other elements take `z-index: 7` to clear the same code banners: the composer seat and the turn-navigation rail slot (`TurnNavigator.module.css`, ui-chat). Their order among equal ranks is stated once, in the composer seat's comment (`packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css`).
 
 The rules are scoped so only the two uncapped blocks are affected; the capped tool rows keep their existing behavior, since stacking sticky headers across a run of tool rows would pile them at the top. The Think rule is `packages/client/ui-chat/src/client/chat/ReasoningRow.module.css` `.root[data-expanded] [data-open] [data-disclosure-row]` — gated on `DisclosureRow`'s `data-open` so a collapsed Think row never sticks, and scoped under the Think row's own root so no tool-call variant is touched. The compaction rule is `packages/client/ui-chat/src/client/chat/MessageItem.module.css` `.compactionRow:has(.compactionBody) .compactionButton` — the body sibling exists in the DOM only while open, so `:has()` gates the stick on the open state.
 

+ 1 - 1
.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.zh.md

@@ -14,7 +14,7 @@ Status: implemented
 
 这两个块本来就是相对共享的会话滚动容器(`[data-conversation-scroll]`)滚动,而非某个内层框,所以在标题上加 `position: sticky; top: 0` 就把它钉在该容器上。一个 base token 背景遮住在钉住的标题下方滚过的正文。
 
-钉住的标题的层叠级别按块而异,因为两者正文不同。Think 正文是纯文本、无 sticky 后代,`z-index: 1` 就够。压缩正文渲染 markdown,摘要里的围栏代码块会把自己的 banner 钉在 `z-index: 6`(`packages/client/ui-primitives/src/markdown/CodeBlock.module.css`),banner 里带一个 Copy 控件;因此压缩标题用 `z-index: 7`,并让那个 banner 停在自己的标题带下方,这样标题永远不会盖住 Copy 控件,banner 也不会盖住折叠按钮。标题带高度是一个组件局部量(`.compactionRow` 上的 `--dsh-compaction-header-height`),由折叠按钮的 `height` 与 banner 的 `top` 共用;banner 规则靠特异性压过 CodeBlock 的 `top: 0`,因为两张样式表分属不同的包。钉住的标题还把 hover 底覆盖为不透明的 `--dsw-alias-interactive-bg-hover-solid` token,并在整行展开期间都保持直角:默认的半透明 hover token 会在指针落到折叠按钮准备折叠的瞬间让滚动的正文透出,而基础的 6px 圆角会让同样的正文在四角露出来。`:hover` 把该规则的特异性抬到文件更靠后的 hover 基础规则之上,所以胜负不由声明顺序决定。另外还有两个元素为了避开同一批代码 banner 取 `z-index: 7`:输入框座与轮次导航轨道槽(`TurnNavigator.module.css`,ui-chat)。同级之间的先后顺序只写在一处:`packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css` 中输入框座的注释。
+钉住的标题的层叠级别按块而异,因为两者正文不同。Think 正文使用[紧凑 Markdown](../bug-fix/2026-09-17-thinking-markdown.zh.md),代码栏处于正常文档流中,`z-index: 1` 就够。压缩正文渲染 markdown,摘要里的围栏代码块会把自己的 banner 钉在 `z-index: 6`(`packages/client/ui-primitives/src/markdown/CodeBlock.module.css`),banner 里带一个 Copy 控件;因此压缩标题用 `z-index: 7`,并让那个 banner 停在自己的标题带下方,这样标题永远不会盖住 Copy 控件,banner 也不会盖住折叠按钮。标题带高度是一个组件局部量(`.compactionRow` 上的 `--dsh-compaction-header-height`),由折叠按钮的 `height` 与 banner 的 `top` 共用;banner 规则靠特异性压过 CodeBlock 的 `top: 0`,因为两张样式表分属不同的包。钉住的标题还把 hover 底覆盖为不透明的 `--dsw-alias-interactive-bg-hover-solid` token,并在整行展开期间都保持直角:默认的半透明 hover token 会在指针落到折叠按钮准备折叠的瞬间让滚动的正文透出,而基础的 6px 圆角会让同样的正文在四角露出来。`:hover` 把该规则的特异性抬到文件更靠后的 hover 基础规则之上,所以胜负不由声明顺序决定。另外还有两个元素为了避开同一批代码 banner 取 `z-index: 7`:输入框座与轮次导航轨道槽(`TurnNavigator.module.css`,ui-chat)。同级之间的先后顺序只写在一处:`packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css` 中输入框座的注释。
 
 规则被限定作用域,只影响这两个不封顶的块;封顶的工具行保持原有行为,因为让一连串工具行的 sticky 标题层层堆叠会把它们全挤在顶部。Think 规则是 `packages/client/ui-chat/src/client/chat/ReasoningRow.module.css` 的 `.root[data-expanded] [data-open] [data-disclosure-row]`,用 `DisclosureRow` 的 `data-open` 门控,折叠的 Think 行绝不钉住,并限定在 Think 行自己的 root 之下,不触及任何工具调用 variant。压缩规则是 `packages/client/ui-chat/src/client/chat/MessageItem.module.css` 的 `.compactionRow:has(.compactionBody) .compactionButton`,正文兄弟节点只在展开时存在于 DOM,所以 `:has()` 就以展开状态门控钉住。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md
-2026-08-08-windows-acl-restricted-token-sandbox.md: a70fd8839c371c11b6660229146306f7c5aa642a
-2026-08-08-windows-acl-restricted-token-sandbox.zh.md: 892eabf17c63b7ceaa9bb8b21ac8c6e71a2f6112
+2026-08-08-windows-acl-restricted-token-sandbox.md: 9834e9b559f0a8045b0d36dd2dcfba864238996b
+2026-08-08-windows-acl-restricted-token-sandbox.zh.md: d349b504ca6a1054237d5ca6aa61547c2a0dcdad

+ 1 - 1
.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md

@@ -32,7 +32,7 @@ The landstrip evaluation was rejected before implementation (not battle-tested;
 
 ## Consequences
 
-Bought: write-only confinement with no new OS floor (`CreateRestrictedToken` predates the mxc releases by two decades), reads/network/process visibility untouched exactly as the mode vocabulary requires, and fail-closed errors carrying the API name and exact Win32 code. Sessions share the intentionally standing workspace capability but not their revocable temp capabilities; restart residue cannot block or authorize a resumed session. Cost: enforcement is structurally partial because Everyone-granted writes and NTFS hard-link aliases cannot be path-confined by this token shape; no read-side or network isolation; console isolation unavailable (hidden-console children die with `STATUS_DLL_INIT_FAILED`; children share the host console); standing workspace ACE mutations (the reuse cache, plus inert residue when a workspace is renamed) and random temp litter after an unclean shutdown until OS hygiene reclaims it; EAGER full-tree workspace propagation (`SetNamedSecurityInfoW` walks every descendant immediately — tens of seconds on large workspaces), paid once per workspace per machine; CIM unavailable in both confined modes (Authenticated Users is absent, closing the C:\-root tree-creation escape); FAT-class non-ACL targets still writable; NULL-DACL directories not identity-preserving under a grant/revoke round trip; `whoami` and token-inspection cmdlets failing under the restricted token; read-only pwsh entering ConstrainedLanguage while workspace-write remains FullLanguage absent host policy; and named-pipe opens remaining denied, so libuv piped-stdio grandchildren fail with EPERM while inherited/ignored stdio and anonymous pipes work. The package README owns these operational limits.
+Bought: write-only confinement with no new OS floor (`CreateRestrictedToken` predates the mxc releases by two decades), reads/network/process visibility untouched exactly as the mode vocabulary requires, and fail-closed errors carrying the API name and exact Win32 code. Sessions share the intentionally standing workspace capability but not their revocable temp capabilities; restart residue cannot block or authorize a resumed session. Cost: enforcement is structurally partial because Everyone-granted writes and NTFS hard-link aliases cannot be path-confined by this token shape; no read-side or network isolation; console isolation unavailable (children created with `CREATE_NO_WINDOW` or `CREATE_NEW_CONSOLE` die with `STATUS_DLL_INIT_FAILED`; [startup visibility](../bug-fix/2026-09-16-windows-subprocess-console-visibility.md) preserves console inheritance); standing workspace ACE mutations (the reuse cache, plus inert residue when a workspace is renamed) and random temp litter after an unclean shutdown until OS hygiene reclaims it; EAGER full-tree workspace propagation (`SetNamedSecurityInfoW` walks every descendant immediately — tens of seconds on large workspaces), paid once per workspace per machine; CIM unavailable in both confined modes (Authenticated Users is absent, closing the C:\-root tree-creation escape); FAT-class non-ACL targets still writable; NULL-DACL directories not identity-preserving under a grant/revoke round trip; `whoami` and token-inspection cmdlets failing under the restricted token; read-only pwsh entering ConstrainedLanguage while workspace-write remains FullLanguage absent host policy; and named-pipe opens remaining denied, so libuv piped-stdio grandchildren fail with EPERM while inherited/ignored stdio and anonymous pipes work. The package README owns these operational limits.
 
 ## Testing
 

+ 1 - 1
.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md

@@ -32,7 +32,7 @@ landstrip 评估在实现前已被否决(未经实战检验;自建 launcher
 
 ## 后果
 
-所得:仅写隔离、不引入新的 OS 版本下限(`CreateRestrictedToken` 比 mxc 的版本早二十年)、读/网络/进程可见性完全不受影响(与模式词汇表一致),且 fail-closed 错误携带 API 名与精确 Win32 错误码。会话共享有意常驻的工作区能力,但不共享各自可回收的临时能力;重启残留既不能阻塞恢复的会话,也不能向其授权。所失:强制执行在结构上只能是部分的,因为此令牌形态无法把 Everyone 授予的写入与 NTFS 硬链接别名限制在路径边界内;无读侧或网络隔离;控制台隔离不可用(隐藏控制台子进程以 `STATUS_DLL_INIT_FAILED` 死亡;子进程共享宿主控制台);工作区常驻 ACE 改动(复用缓存,以及工作区改名后的失效残留)与异常关闭后遗留的随机临时目录垃圾,直到 OS 卫生机制将其回收;工作区授权采用急切的全树传播(`SetNamedSecurityInfoW` 立即遍历每个后代——大型工作区上耗时数十秒),每台机器每个工作区只付一次;CIM 在两种受限模式下均不可用(Authenticated Users 不存在,从而关闭 C:\-root 建树逃逸);FAT 类无 ACL 目标仍可写;NULL-DACL 目录在 grant/revoke 往返下不保持身份;`whoami` 与令牌检查 cmdlet 在受限令牌下失败;read-only pwsh 会进入 ConstrainedLanguage,而在没有主机策略时 workspace-write 保持 FullLanguage;named pipe 打开仍被拒绝,因此 libuv 管道 stdio 的孙进程以 EPERM 失败,而继承/忽略的 stdio 与匿名管道可用。包 README 负责记录这些运行限制。
+所得:仅写隔离、不引入新的 OS 版本下限(`CreateRestrictedToken` 比 mxc 的版本早二十年)、读/网络/进程可见性完全不受影响(与模式词汇表一致),且 fail-closed 错误携带 API 名与精确 Win32 错误码。会话共享有意常驻的工作区能力,但不共享各自可回收的临时能力;重启残留既不能阻塞恢复的会话,也不能向其授权。所失:强制执行在结构上只能是部分的,因为此令牌形态无法把 Everyone 授予的写入与 NTFS 硬链接别名限制在路径边界内;无读侧或网络隔离;控制台隔离不可用(通过 `CREATE_NO_WINDOW` 或 `CREATE_NEW_CONSOLE` 创建的子进程以 `STATUS_DLL_INIT_FAILED` 死亡;[启动可见性](../bug-fix/2026-09-16-windows-subprocess-console-visibility.zh.md)保留控制台继承);工作区常驻 ACE 改动(复用缓存,以及工作区改名后的失效残留)与异常关闭后遗留的随机临时目录垃圾,直到 OS 卫生机制将其回收;工作区授权采用急切的全树传播(`SetNamedSecurityInfoW` 立即遍历每个后代——大型工作区上耗时数十秒),每台机器每个工作区只付一次;CIM 在两种受限模式下均不可用(Authenticated Users 不存在,从而关闭 C:\-root 建树逃逸);FAT 类无 ACL 目标仍可写;NULL-DACL 目录在 grant/revoke 往返下不保持身份;`whoami` 与令牌检查 cmdlet 在受限令牌下失败;read-only pwsh 会进入 ConstrainedLanguage,而在没有主机策略时 workspace-write 保持 FullLanguage;named pipe 打开仍被拒绝,因此 libuv 管道 stdio 的孙进程以 EPERM 失败,而继承/忽略的 stdio 与匿名管道可用。包 README 负责记录这些运行限制。
 
 ## 测试
 

+ 2 - 2
.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.md
-2026-09-04-web-clickable-link-styles.md: 6fa61945143853a2812468638dafd23ebcd456dc
-2026-09-04-web-clickable-link-styles.zh.md: 0b15a6c72df9151ffb79f7fb41d582e191e79be6
+2026-09-04-web-clickable-link-styles.md: c4e506294c87c6b959cb449403edce09daa11062
+2026-09-04-web-clickable-link-styles.zh.md: 01258674b4b8194ad5e0597cde61cc2186406d49

+ 3 - 2
.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.md

@@ -13,8 +13,9 @@ Clickable artifact links in the chat transcript wore four different costumes: ma
 One link language across the transcript's clickable-link surfaces — markdown anchors (including reference links, mailto, and URL-promoted inline code), prose file mentions, web search source links and the fetch URL, produced-file chips, and workflow member links:
 
 - Color comes through a dedicated `--dsw-alias-link` alias in `design-platform.css` (light `deepseek-500`, dark `deepseek-400`), decoupled from `state-business-primary`; links render at `font-weight: 500` with no underline at rest and `underline dotted` at 3px offset on hover/focus.
-- A leading category glyph — the new `LinkIcon` in ui-primitives with kinds `url` (globe), `folder`, `code`, `image`, `document`, and `other` (paper) — renders `currentColor` only; `classifyLinkPath` folds the [shared detailed file-type classification](2026-09-08-shared-file-type-icons.md) into those six link categories, and code, web, and data extensions share the code glyph by design. Two anchor shapes carry no glyph: workflow member links (an in-app member view fits no file or URL category) and anchors wrapping only images (a badge or thumbnail — a dangling globe beside the picture leads no text). Inline glyphs sit at 1.1em with a −0.25em baseline offset; the flex-centered produced-file glyphs instead nudge 1.2px down because the 22px text box carries its glyphs below box center.
+- A leading category glyph — the new `LinkIcon` in ui-primitives with kinds `url`, `folder`, `code`, `image`, `document`, and `other` (paper) — renders `currentColor` only; the `url` glyph is the globe, or the destination site's own mark when its host is a known site ([known-site link marks](2026-09-16-known-site-link-marks.md)); `classifyLinkPath` folds the [shared detailed file-type classification](2026-09-08-shared-file-type-icons.md) into those six link categories, and code, web, and data extensions share the code glyph by design. Two anchor shapes carry no glyph: workflow member links (an in-app member view fits no file or URL category) and anchors wrapping only images (a badge or thumbnail — a dangling globe beside the picture leads no text). Inline glyphs sit at 1.1em with a −0.25em baseline offset; the flex-centered produced-file glyphs instead nudge 1.2px down because the 22px text box carries its glyphs below box center.
 - Produced-file chips drop the grey pill and the 96px cap: plain link-blue text at natural width that shrinks with ellipsis only when the row overflows; the container-query bands still budget 96px per chip when choosing how many chips to show.
+- [Compact Thinking Markdown](../bug-fix/2026-09-17-thinking-markdown.md) retains tertiary text color and a resting dotted underline to preserve secondary emphasis.
 - Deliberately untouched: ToolRow's grey dotted file links, and the grey "Show in folder" action (it gains the folder glyph but keeps its grey style).
 - In the same pass, the inline-code chip tint moved from `neutral-bluish-100` to `neutral-50` (dark: `neutral-800`) and gained a 0.5px l1 border.
 
@@ -23,7 +24,7 @@ Coverage: a LinkIcon unit spec (one distinct glyph per kind, classification tabl
 ## Alternatives considered
 
 - **Colored Word/Excel/PPT/PDF link glyphs.** Rejected: fixed brand fills break the icon set's currentColor-only rule, so those extensions fold into the single outline `document` glyph. The larger file-card primitive uses distinct current-color silhouettes instead.
-- **Per-extension link icons.** Collapsed to six categories: more glyphs than the eye can parse at 14px adds noise. The 28px `FileTypeIcon` owns the more detailed file identities, and per-site favicons remain possible later behind the same `url` category.
+- **Per-extension link icons.** Collapsed to six categories: more glyphs than the eye can parse at 14px adds noise. The 28px `FileTypeIcon` owns the more detailed file identities, and per-site marks live behind the same `url` category ([known-site link marks](2026-09-16-known-site-link-marks.md)).
 - **Keeping links on `state-business-primary`.** Darker link blues (blue-600/650/700 were auditioned and reverted) would have dragged focus rings and state dots along; the dedicated alias localizes any future tuning to one line.
 - **Glyphs on ToolRow path links.** Rejected: tool rows keep their quieter grey dotted affordance, and leading glyphs there would stack icons in already dense rows.
 

+ 3 - 2
.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.zh.md

@@ -13,8 +13,9 @@ Status: implemented
 会话记录的可点击链接表面——Markdown 锚点(含引用式链接、mailto、被提升为链接的 inline code)、正文文件引用、网页搜索来源链接与抓取 URL、产物 chips、workflow 成员链接——统一为一套链接语言:
 
 - 颜色经由 `design-platform.css` 中专用的 `--dsw-alias-link` 别名(亮色 `deepseek-500`,暗色 `deepseek-400`),与 `state-business-primary` 解耦;链接以 `font-weight: 500` 呈现,默认无下划线,hover/focus 时为 3px offset 的 `underline dotted`。
-- 前置分类图标——ui-primitives 新增的 `LinkIcon`,kind 为 `url`(地球)、`folder`、`code`、`image`、`document`、`other`(纸张)——只渲染 `currentColor`;`classifyLinkPath` 把[共享精细文件类型分类](2026-09-08-shared-file-type-icons.zh.md)折叠进这六种链接类别,代码、网页、数据扩展名按设计共用 code 图形。两类锚点不带图标:workflow 成员链接(应用内成员视图不属于任何文件或 URL 类别)和只包图片的锚点(徽章或缩略图——图片旁悬着的地球没有可引导的文字)。行内图标为 1.1em、基线偏移 −0.25em;flex 居中的产物图标则下移 1.2px,因为 22px 文字盒的字形低于盒中心。
+- 前置分类图标——ui-primitives 新增的 `LinkIcon`,kind 为 `url`、`folder`、`code`、`image`、`document`、`other`(纸张)——只渲染 `currentColor`;`url` 图形为地球,目的地主机属于已知站点时则为该站点自己的标记([已知站点链接标记](2026-09-16-known-site-link-marks.zh.md));`classifyLinkPath` 把[共享精细文件类型分类](2026-09-08-shared-file-type-icons.zh.md)折叠进这六种链接类别,代码、网页、数据扩展名按设计共用 code 图形。两类锚点不带图标:workflow 成员链接(应用内成员视图不属于任何文件或 URL 类别)和只包图片的锚点(徽章或缩略图——图片旁悬着的地球没有可引导的文字)。行内图标为 1.1em、基线偏移 −0.25em;flex 居中的产物图标则下移 1.2px,因为 22px 文字盒的字形低于盒中心。
 - 产物 chips 去掉灰色药丸和 96px 上限:纯链接蓝文字按自然宽度展示,仅当整行溢出时才收缩出省略号;容器查询档位在决定展示几个 chip 时仍按每个 96px 预算。
+- [紧凑 Thinking Markdown](../bug-fix/2026-09-17-thinking-markdown.zh.md) 保留 tertiary 文字色和默认点状下划线,以保持次级强调。
 - 刻意不动:ToolRow 的灰色点线文件链接,以及灰色的「在文件夹中显示」操作(它获得文件夹图标但保持灰色样式)。
 - 同一批次中,inline code 底色从 `neutral-bluish-100` 换到 `neutral-50`(暗色:`neutral-800`),并新增 0.5px l1 描边。
 
@@ -23,7 +24,7 @@ Status: implemented
 ## 备选方案
 
 - **彩色 Word/Excel/PPT/PDF 链接图形。** 否决:固定品牌填充违反图标集 currentColor-only 规则,因此这些扩展名并入单一的 outline `document` 图形。较大的文件卡片 primitive 改用各自不同的 current-color 轮廓。
-- **每个扩展名一个链接图标。** 收敛为六个类别:14px 下超出肉眼可分辨数量的图形只会增加噪音。28px 的 `FileTypeIcon` 拥有更精细的文件身份,按站点的 favicon 以后仍可在同一 `url` 类别之下引入
+- **每个扩展名一个链接图标。** 收敛为六个类别:14px 下超出肉眼可分辨数量的图形只会增加噪音。28px 的 `FileTypeIcon` 拥有更精细的文件身份;按站点标记位于同一 `url` 类别之下([已知站点链接标记](2026-09-16-known-site-link-marks.zh.md))
 - **链接继续用 `state-business-primary`。** 更深的链接蓝(试过 blue-600/650/700 又回退)会连带焦点环和状态点;专用别名把未来的调色收敛到一行。
 - **给 ToolRow 路径链接加图形。** 否决:工具行保持更安静的灰色点线示能,在已经很密的行里加前置图形会造成图标堆叠。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.md
-2026-09-07-deepseek-messages-adapter.md: 6b7aff250aacd4bb7d0af3b4e68ee5b2002c13e3
-2026-09-07-deepseek-messages-adapter.zh.md: 829e2189cb48a2acaa1ec27f7ccade0f163b58ad
+2026-09-07-deepseek-messages-adapter.md: c667ec3afdd9956282a532e748ad2e2488d26f5b
+2026-09-07-deepseek-messages-adapter.zh.md: 0e3425e41d8a9585a1638eedeefcf6ead76a5a78

+ 1 - 1
.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.md

@@ -14,7 +14,7 @@ The [DeepSeek adapter](../../../../packages/llm/llm-deepseek/README.md) serves m
 
 The adapter follows the [DeepSeek compatibility documentation](https://api-docs.deepseek.com/zh-cn/guides/anthropic_api) and [Anthropic streaming protocol](https://platform.claude.com/docs/en/build-with-claude/streaming). The pi-ai Anthropic implementation informed the handling of adjacent user messages, cumulative usage, fragmented tool arguments, and optional thinking signatures. DeepSeek effort uses `output_config.effort`; an Anthropic thinking token budget does not control DeepSeek effort. Both protocols forward explicit `temperature` values; DeepSeek accepts that parameter with thinking enabled and ignores its value, so callers retain their existing thinking configuration.
 
-Assistant blocks remain the durable model-visible content. A versioned `ReplayEnvelope` stores only the protocol format, model identity, aligned block kinds, and signatures absent from those blocks. Same-model Messages continuation restores signatures verbatim, including empty signatures; foreign history carries no invented signature. Unusable metadata follows the existing [replay degradation rule](../architecture/2026-07-14-provider-routed-llm-adapters.md): the request omits signatures with a warning while preserving durable content; content validation such as tool argument parsing still fails explicitly. This keeps provider replay data opaque to the loop while preserving it through Session persistence and block pruning.
+Assistant blocks remain the durable model-visible content. A versioned `ReplayEnvelope` stores only the protocol format, model identity, aligned block kinds, and signatures absent from those blocks. Same-model Messages continuation restores signatures verbatim, including empty signatures; foreign history carries no invented signature. Unusable metadata follows the existing [replay degradation rule](../architecture/2026-07-14-provider-routed-llm-adapters.md): the request omits signatures with a warning while preserving durable content; historical tool arguments use the [empty-input fallback](../bug-fix/2026-09-16-messages-historical-tool-input.md) when Messages cannot represent them. This keeps provider replay data opaque to the loop while preserving it through Session persistence and block pruning.
 
 Both protocols prefer Files references for deterministic request images and share upload caching, refresh, quota recovery, and attachment offload. The Files client retains the selected protocol and configured endpoint: Messages follows the [exact `/v1` root rule](../bug-fix/2026-09-15-messages-v1-base-url.md), while Chat Completions appends `/files`. Messages Files requests carry the required beta header. Cached ids remain scoped by the resolved Files root and credential, so equivalent `/v1` and unversioned Messages roots share uploads. Messages metadata omits expiry, so local reuse is bounded from the original upload time without asserting remote deletion. A Files-resolution failure rebuilds the complete request under the independent inline-image budget; caller cancellation stops it. The shared image policy preserves the 128 MiB retained-image budget, 20 MiB inline base64 budget, and oldest-prefix offload in both requests and token measurement.
 

+ 1 - 1
.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.zh.md

@@ -14,7 +14,7 @@ Status: implemented
 
 适配器遵循 [DeepSeek 兼容文档](https://api-docs.deepseek.com/zh-cn/guides/anthropic_api) 和 [Anthropic 流协议](https://platform.claude.com/docs/en/build-with-claude/streaming)。pi-ai 的 Anthropic 实现为相邻用户消息、累计用量、工具参数分片和可选思考签名的处理提供参考。DeepSeek 通过 `output_config.effort` 设置思考强度;Anthropic 思考 token 预算不控制 DeepSeek 思考强度。两种协议都转发显式 `temperature` 值;DeepSeek 在启用思考时接受该参数但忽略其值,因此调用方可以保留已有思考配置。
 
-助手内容块保留持久化的模型可见内容。带版本的 `ReplayEnvelope` 仅保存协议格式、模型标识、对齐的块类型以及内容块未包含的签名。同模型续接原样恢复签名,包括空签名;外部历史不生成虚构签名。不可用的元数据遵循现有[回放降级规则](../architecture/2026-07-14-provider-routed-llm-adapters.zh.md):请求省略签名并记录警告,保留持久化内容;工具参数等内容校验仍会正常报错。提供者回放数据对循环保持不透明,同时能够随 Session 持久化和内容块裁剪保留。
+助手内容块保留持久化的模型可见内容。带版本的 `ReplayEnvelope` 仅保存协议格式、模型标识、对齐的块类型以及内容块未包含的签名。同模型续接原样恢复签名,包括空签名;外部历史不生成虚构签名。不可用的元数据遵循现有[回放降级规则](../architecture/2026-07-14-provider-routed-llm-adapters.zh.md):请求省略签名并记录警告,保留持久化内容;Messages 无法表示历史工具参数时使用[空输入兜底](../bug-fix/2026-09-16-messages-historical-tool-input.zh.md)。提供者回放数据对循环保持不透明,同时能够随 Session 持久化和内容块裁剪保留。
 
 两种协议均优先为确定性请求图片使用 Files 引用,并共享上传缓存、刷新、配额恢复和附件卸载。Files 客户端保留所选协议与已配置端点:Messages 遵循[严格匹配 `/v1` 的根地址规则](../bug-fix/2026-09-15-messages-v1-base-url.zh.md),Chat Completions 则追加 `/files`。Messages Files 请求携带必需的 beta 标头。缓存 id 按解析后的 Files 根地址和凭据限定作用域,因此等价的 `/v1` 与无版本 Messages 根地址可以复用上传。Messages 元数据不含过期时间,因此本地复用从原始上传时间起受限,但不宣称远端文件已删除。Files 解析失败会按独立的内联图片预算重建完整请求;调用方取消则停止请求。共享图片策略在请求与 token 计量中保留 128 MiB 的保留图片预算、20 MiB 的内联 base64 预算,以及最旧前缀卸载。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.md
-2026-09-08-present-workspace-source-files.md: 650c16fe5a614ce1ccf118744a5f0e8cb5d19f4b
-2026-09-08-present-workspace-source-files.zh.md: 61bca82c83ff42600a7017039a6be4c3a4605769
+2026-09-08-present-workspace-source-files.md: e1bc2bbc56d423741ca53550af2879fed3f01ecf
+2026-09-08-present-workspace-source-files.zh.md: e4f21172b36a5fef50e44a79adca71011ee90a3b

+ 2 - 2
.agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.md

@@ -10,13 +10,13 @@ Users need to open and edit the files produced in their workspace, including she
 
 ## Decision
 
-The [present tool](../../../../packages/fs/tool-present/README.md) declares existing regular source files under the [Session filesystem access policy](2026-09-09-present-filesystem-access.md). It records paths and optional descriptions without reading or copying contents. The [deliverables plugin](../../../../packages/client/ui-deliverables/README.md) opens current workspace sources in the Host's default application. Edits are visible on the next open; deletion or movement makes the declaration unavailable. File-content preservation and copy-on-write storage are deferred until a persistence design owns them.
+The [present tool](../../../../packages/deliverables/tool-present/README.md) declares existing regular source files under the [Session filesystem access policy](2026-09-09-present-filesystem-access.md). It records paths and optional descriptions without reading or copying contents. The [deliverables plugin](../../../../packages/client/ui-deliverables/README.md) opens current workspace sources in the Host's default application. Edits are visible on the next open; deletion or movement makes the declaration unavailable. File-content preservation and copy-on-write storage are deferred until a persistence design owns them.
 
 The tool description requires `present` after writing a file the user asked to receive and before the final response, including files created through Bash or code execution. A prose path reference does not replace the call. The recorded [SVG delivery scenario](../../../../snapshots/web/present-svg/snapshot.yml) uses a user request that does not name `present`, and checks the resulting file, delivery event, and card. Its UI snapshot covers the expanded Chat transcript; navigation and composer controls belong to their own scenarios, so unrelated chrome changes cannot invalidate file-delivery expectations.
 
 The tool remains an ordinary package with shared filesystem and tool error classes. Its pure type entry owns the delivery event without importing Host code into the browser. The `standard`, `ptc`, and `cordis` presets mount it; `minimal` retains its two tools. Each plugin instance correlates its executions with successful final `tools/result` notifications before appending `deliverables/presented`. Native and nested calls share this rule. A later enclosing program failure does not revoke a completed nested declaration; blocked results publish none, and same-name scoped replacements cannot publish another instance's results.
 
-An authenticated POST selects a declaration by viewed Session, event sequence, and original file index. The event carries no owning Session ID; relative paths in inherited history resolve against the viewed Session's workspace. The Host verifies regular-file existence and Host-path mapping before native opening. Route disposal cancels and awaits pending commands. The “Files changed” row lists successful file-tool mutations and retains its separate text-preview behavior. Its Chinese label is “本轮文件改动”; neither label implies final delivery.
+An authenticated POST selects a declaration by viewed Session, event sequence, and original file index. The event carries no owning Session ID; relative paths in inherited history resolve against the viewed Session's workspace. The Host verifies regular-file existence and Host-path mapping before native opening. Route disposal cancels and awaits pending commands. The [changed-files card](2026-09-11-turn-changed-files-card.md) lists the turn's recorded changes beside the delivery cards; neither surface implies final delivery.
 
 File cards use the same split-control pattern as the Session header. The card and the left Open segment preview the source in the right Sidebar; the chevron opens the standard menu for default-app and file-manager actions. The Host selects the file in Finder or Explorer, or opens its containing folder through the default Linux file manager. Both native actions resolve the same saved declaration and verify the Session filesystem and Host path; neither accepts a browser-supplied replacement path. Host-derived desktop metadata keeps remote-browser labels and availability honest, and the route enforces the configured availability on each native gesture. One delivery spans the row; multiple deliveries use at most two columns, retain every declaration, and collapse after the first four cards until the user expands the list. Desktop metadata is invalidated with the connection generation so an old Host cannot keep native actions disabled or supply the wrong file-manager labels. Old metadata requests are cancelled and cannot replace the new generation’s response.
 

+ 2 - 2
.agents/notes/implemented/feature/2026-09-08-present-workspace-source-files.zh.md

@@ -10,13 +10,13 @@ Status: implemented
 
 ## 决策
 
-[present 工具](../../../../packages/fs/tool-present/README.zh.md)声明交付[Session 文件系统访问策略](2026-09-09-present-filesystem-access.zh.md)允许的已有普通源文件。它记录路径和可选说明,不读取或复制内容。[交付插件](../../../../packages/client/ui-deliverables/README.zh.md)使用 Host 默认应用打开当前工作区源文件。下次打开会看到编辑后的内容;删除或移动文件会使声明不可用。文件内容保留与写时复制存储延期到有持久化设计负责时实现。
+[present 工具](../../../../packages/deliverables/tool-present/README.zh.md)声明交付[Session 文件系统访问策略](2026-09-09-present-filesystem-access.zh.md)允许的已有普通源文件。它记录路径和可选说明,不读取或复制内容。[交付插件](../../../../packages/client/ui-deliverables/README.zh.md)使用 Host 默认应用打开当前工作区源文件。下次打开会看到编辑后的内容;删除或移动文件会使声明不可用。文件内容保留与写时复制存储延期到有持久化设计负责时实现。
 
 工具说明要求在写好用户要求接收的文件后、最终回复前调用 `present`,包括通过 Bash 或代码执行创建的文件。正文中的路径引用不能替代调用。录制的 [SVG 交付场景](../../../../snapshots/web/present-svg/snapshot.yml)使用未提及 `present` 的用户请求,检查生成文件、交付事件和卡片。其 UI 快照覆盖展开后的 Chat 对话内容;导航和输入框控件由各自场景负责,避免无关界面改动使文件交付预期失效。
 
 工具保持为普通包,共享文件系统和工具错误类型。其纯类型入口拥有交付事件,不向浏览器导入 Host 代码。`standard`、`ptc` 与 `cordis` preset 挂载工具;`minimal` 保持两个工具。每个插件实例将其执行与成功的最终 `tools/result` 通知关联,再追加 `deliverables/presented`。原生与嵌套调用遵循同一规则。外层程序随后失败不会撤销已完成的嵌套声明;被阻止的结果不发布声明,同名作用域替换也不能发布其他实例的结果。
 
-经过认证的 POST 按当前查看的 Session、事件序号和原始文件索引选择声明。事件不携带所属 Session ID;继承历史中的相对路径按当前查看的 Session 工作区解析。Host 在原生打开前检查普通文件是否存在,并验证 Host 路径映射。路由释放时取消并等待进行中的命令。“本轮文件改动”行列出成功的文件工具修改,并保留独立的文本预览行为。其英文标签为“Files changed”;两个标签均不表示最终交付。
+经过认证的 POST 按当前查看的 Session、事件序号和原始文件索引选择声明。事件不携带所属 Session ID;继承历史中的相对路径按当前查看的 Session 工作区解析。Host 在原生打开前检查普通文件是否存在,并验证 Host 路径映射。路由释放时取消并等待进行中的命令。[改动文件卡片](2026-09-11-turn-changed-files-card.zh.md)在交付卡片旁列出本轮记录的改动;两个表面均不表示最终交付。
 
 文件卡片采用与 Session 顶栏相同的分段控件。点击卡片或左侧“打开”区域会在右侧 Sidebar 预览源文件;右侧箭头打开包含默认应用与文件管理器操作的标准菜单。Host 在 Finder 或文件资源管理器中选中文件,或通过 Linux 默认文件管理器打开所在文件夹。两个原生操作都解析同一份已保存声明并验证 Session 文件系统与 Host 路径;均不接受浏览器提供的替代路径。来自 Host 的桌面信息使远程浏览器中的文案和可用性保持准确,路由在每次原生操作时执行配置的可用性检查。单个交付占满整行;多个交付每行最多两列,并保留所有声明,前四张卡片之后的内容在用户展开列表前保持收起。 桌面元数据随连接代次失效,避免旧主机信息让原生操作持续禁用或显示错误的文件管理器名称。旧元数据请求会被取消,不能覆盖新代次的响应。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md
-2026-09-09-web-sidebar-terminal.md: 2649610e776029b10b11fc4ea87d75a24a58d32b
-2026-09-09-web-sidebar-terminal.zh.md: 9a3fd828ff52142cb945ff14d84d13c9e8522ecf
+2026-09-09-web-sidebar-terminal.md: 5757aed3702dbcc7752e99912714bd1356a96206
+2026-09-09-web-sidebar-terminal.zh.md: 39870d1ba26cbc1d655b35a3d2dba61692d0fdb9

+ 2 - 2
.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md

@@ -14,7 +14,7 @@ Guide entries declare stable ids within their provider. A keyed `sidebar.right.t
 
 The application theme supplies terminal default colors. The body reads resolved CSS tokens, and updates xterm only when those colors change. Public OSC parser observers retain indexed and default-color overrides separately from the DSH defaults; resets remove the corresponding override before restoring the current theme. Observers delegate queries and color handling to xterm. xterm's minimum contrast adjustment improves text legibility without remapping ANSI backgrounds. The DOM cursor reads the rendered cell background after each render and uses a contrasting fill through scoped CSS variables, so cursor movement never resets the palette. Browser checks cover indexed, true-color and inverse cells, light/dark switching, OSC retention and reset, and blinking cursor styles.
 
-`api-terminal-controller` owns user terminals by Session and exposes the `terminal` Remote namespace. `ui-sidebar-terminal` registers native right-sidebar tabs, xterm.js rendering and FitAddon sizing. The terminal guide card has a primary action for the remembered available shell and a separate installed-shell menu. Selecting a menu item records its path and opens a new terminal immediately; discovery alone allocates no process. Each tab owns its startup and close lifecycle. Host discovery verifies the configured candidates, with the execution default first; creation accepts only a currently discovered path. The browser remembers the last selected shell path in origin-scoped localStorage and falls back to the current default if that path is unavailable. The terminal type declares independent instances, so ordinary page deduplication cannot collapse separate processes when opening or docking tabs. The existing sidebar controls open additional tabs; double-clicking a tab title renames its terminal. Terminal processes use the composed subprocess provider and Session sandbox policy. Shell resolution occurs during discovery and creation; reading limits and reconnecting an existing process do not depend on the default executable remaining available. Interactive shell configuration supplies Tab completion and optional inline suggestions.
+`api-terminal-controller` owns user terminals by Session and exposes the `terminal` Remote namespace. `ui-sidebar-terminal` registers native right-sidebar tabs, xterm.js rendering and FitAddon sizing. The terminal guide card has a primary action for the remembered available shell and a separate installed-shell menu. Selecting a menu item records its path and opens a new terminal immediately; discovery alone allocates no process. Each tab owns its startup and close lifecycle. Host discovery verifies the configured candidates, with the execution default first; creation accepts only a currently discovered path. The browser remembers the last selected shell path in origin-scoped localStorage and falls back to the current default if that path is unavailable. The terminal type declares independent instances, so ordinary page deduplication cannot collapse separate processes when opening or docking tabs. The existing sidebar controls open additional tabs; double-clicking a tab title renames its terminal. Terminal processes use the composed subprocess provider; [user-terminal permissions](../architecture/2026-09-16-user-terminal-permissions.md) govern their execution permissions independently of the Agent. Shell resolution occurs during discovery and creation; reading limits and reconnecting an existing process do not depend on the default executable remaining available. Interactive shell configuration supplies Tab completion and optional inline suggestions.
 
 Close and replacement remove the tab synchronously and run process cleanup in the background. The Client first records the unfinished close request under a terminal-specific localStorage key; success removes it, and startup retries requests that remain. A cleanup failure produces a lightweight notification with a retry action without reopening the tab. Independent keys prevent another window from overwriting unrelated cleanup requests. Collapse, tab/Session switching, floating, fullscreen and browser disconnection preserve the process. Component cleanup and `TabDomain.signal` only detach browser work because the same lifetime can end during plugin reload. Failed process cleanup retains ownership, including failures after allocation but before create publication. Session owner disposal and Host plugin disposal also clean up terminals. A definitive missing-Session response retires its saved close request because the Session owns process cleanup; transport failures remain retryable. Client plugin disposal awaits every detached stream so a replacement plugin does not inherit unfinished Client cleanup.
 
@@ -44,7 +44,7 @@ The latest attachment controls input and dimensions; other attachments remain re
 
 ## Consequences
 
-A kept-open terminal retains a process and bounded screen memory. Reload restores the sidebar layout and reconnects Host-retained terminals; Host restart does not restore processes. An exited shell remains visible without automatic respawn. Background cleanup may outlive its tab, and unavailable browser storage limits retry recovery to the current page. Native PTY support and descendant cleanup guarantees remain provider-specific. One writable attachment avoids competing resize and input streams, while explicit takeover permits recovery from another page. Changing sandbox mode requires closing retained terminals first.
+A kept-open terminal retains a process and bounded screen memory. Reload restores the sidebar layout and reconnects Host-retained terminals; Host restart does not restore processes. An exited shell remains visible without automatic respawn. Background cleanup may outlive its tab, and unavailable browser storage limits retry recovery to the current page. Native PTY support and descendant cleanup guarantees remain provider-specific. One writable attachment avoids competing resize and input streams, while explicit takeover permits recovery from another page. User terminals remain open when the Session sandbox mode changes.
 
 The implementation retains the Agent-terminal and portable-execution notes because their ownership and provider decisions remain independently useful; neither is superseded by browser terminals.
 

+ 2 - 2
.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.zh.md

@@ -14,7 +14,7 @@ Web 用户需要在 Session 旁使用交互式 shell 检查工作区和运行命
 
 应用主题提供终端的默认颜色。终端正文读取解析后的 CSS 令牌,仅在颜色变化时更新 xterm。公开的 OSC 解析观察器将索引色和默认颜色覆盖与 DSH 默认值分开保存;重置命令先删除对应覆盖,再恢复当前主题。观察器将查询和颜色处理交给 xterm。xterm 的最小对比度调整改善文字可读性,同时不重新映射 ANSI 背景色。DOM 光标在每次渲染后读取单元格实际背景,通过局部 CSS 变量使用有足够对比度的填充色,因此光标移动不会重置调色板。浏览器检查覆盖索引色、真彩色、反色单元格、明暗主题切换、OSC 保留和重置,以及闪烁光标样式。
 
-`api-terminal-controller` 按 Session 管理用户终端并提供 `terminal` Remote namespace。`ui-sidebar-terminal` 注册原生右侧栏标签页,使用 xterm.js 渲染和 FitAddon 测量尺寸。终端开始页卡片的主操作打开上次选择且仍可用的 shell,独立菜单提供已安装 shell。选择菜单项会记录路径并立即打开新终端;仅探测 shell 不分配进程。每个标签页拥有自己的启动和关闭生命周期。Host 探测会验证配置的候选,并把执行环境默认项放在首位;创建只接受当前探测返回的路径。浏览器在当前站点 localStorage 中记住上次选择的 shell 路径,该路径不可用时回到当前默认项。终端类型声明独立实例,因此打开或停靠标签页时,普通页面的去重规则不会合并不同进程。已有侧栏控件负责打开更多标签页,双击标签页标题可重命名终端。终端进程使用组合的 subprocess provider 和 Session sandbox policy。shell 在探测和创建时解析;读取限制和重新连接已有进程不依赖默认可执行文件仍然可用。交互式 shell 配置提供 Tab 补全和可选的内联建议。
+`api-terminal-controller` 按 Session 管理用户终端并提供 `terminal` Remote namespace。`ui-sidebar-terminal` 注册原生右侧栏标签页,使用 xterm.js 渲染和 FitAddon 测量尺寸。终端开始页卡片的主操作打开上次选择且仍可用的 shell,独立菜单提供已安装 shell。选择菜单项会记录路径并立即打开新终端;仅探测 shell 不分配进程。每个标签页拥有自己的启动和关闭生命周期。Host 探测会验证配置的候选,并把执行环境默认项放在首位;创建只接受当前探测返回的路径。浏览器在当前站点 localStorage 中记住上次选择的 shell 路径,该路径不可用时回到当前默认项。终端类型声明独立实例,因此打开或停靠标签页时,普通页面的去重规则不会合并不同进程。已有侧栏控件负责打开更多标签页,双击标签页标题可重命名终端。终端进程使用组合的 subprocess provider;[用户终端权限](../architecture/2026-09-16-user-terminal-permissions.zh.md)规定其独立于 Agent 的执行权限。shell 在探测和创建时解析;读取限制和重新连接已有进程不依赖默认可执行文件仍然可用。交互式 shell 配置提供 Tab 补全和可选的内联建议。
 
 关闭和替换会同步移除标签页,并在后台清理进程。Client 先以终端独立的 localStorage key 保存未完成的关闭请求;成功后删除,启动时重试剩余请求。清理失败时显示带重试操作的轻量通知,不重新打开标签页。独立 key 避免其他窗口覆盖无关的清理请求。折叠、切换标签页或 Session、浮动、全屏和浏览器断线均保留进程。组件清理和 `TabDomain.signal` 只停止浏览器工作,因为插件重新加载也会结束这些生命周期。进程清理失败时保留所有权,包括分配完成但 create 尚未发布时的失败。Session owner 和 Host 插件卸载也会清理终端。 明确的 Session 不存在响应会清除已保存的关闭请求,因为进程清理由 Session 负责;传输失败仍可重试。Client 插件卸载等待所有断开的流结束,避免替换插件继承未完成的 Client 清理。
 
@@ -44,7 +44,7 @@ Web 用户需要在 Session 旁使用交互式 shell 检查工作区和运行命
 
 ## 影响
 
-保留终端会保留进程和有界屏幕内存。刷新恢复侧栏布局并重连 Host 保留的终端;Host 重启不恢复进程。shell 退出后保持可见,不自动重启。后台清理可能比标签页存活更久,浏览器存储不可用时只能在当前页面保留重试能力。原生 PTY 支持和后代进程清理保证仍由 provider 决定。单一可写连接避免竞争的输入和尺寸流,显式接管允许从另一页面恢复操作。改变 sandbox mode 前需要关闭保留的终端
+保留终端会保留进程和有界屏幕内存。刷新恢复侧栏布局并重连 Host 保留的终端;Host 重启不恢复进程。shell 退出后保持可见,不自动重启。后台清理可能比标签页存活更久,浏览器存储不可用时只能在当前页面保留重试能力。原生 PTY 支持和后代进程清理保证仍由 provider 决定。单一可写连接避免竞争的输入和尺寸流,显式接管允许从另一页面恢复操作。Session 的沙箱模式改变时,用户终端保持打开
 
 Agent 终端和可移植执行环境两篇记录仍保留,其所有权与 provider 决策继续独立有效,不被浏览器终端取代。
 

+ 6 - 0
.agents/notes/implemented/feature/2026-09-11-turn-changed-files-card.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-11-turn-changed-files-card.md
+2026-09-11-turn-changed-files-card.md: 0c41783c6d5d02fecd0c85298aed1f07f5c8a65f
+2026-09-11-turn-changed-files-card.zh.md: f417b31b899529b664664926c832c28f4b1be6ba

+ 53 - 0
.agents/notes/implemented/feature/2026-09-11-turn-changed-files-card.md

@@ -0,0 +1,53 @@
+# Agent Note: Turn changed-files card
+
+Status: implemented
+
+English | [中文](2026-09-11-turn-changed-files-card.zh.md)
+
+## Problem
+
+After a turn, users want to see which files the model changed and by how much. The Web turn tail listed only the paths of successful `write`, `edit`, and `str_replace_editor` calls, without line counts, and missed every file a shell command changed; the per-call diff cards in the message flow answered the question one call at a time.
+
+## Decision
+
+The Host [workspace-changes](../../../../packages/deliverables/workspace-changes/README.md) plugin summarizes each top-level turn's changed files, announces the summary with a `workspace/changes` Session event that carries only the turn number, and serves the summary through its `workspaceChanges` service until the Session is disposed; the [deliverables plugin](../../../../packages/client/ui-deliverables/README.md) reads the served summary and renders it as the changed-files card in place of the mutation-call row. The event is log-only and never model-visible.
+
+The recorder snapshots the working tree with git at turn start and turn end: `add --all` into a private index seeded from the repository's index, then `write-tree`. Both write into a temporary object directory owned by the Session while the repository's object store is attached as a read-only alternate, so the user's repository gains no objects; keeping every captured byte outside the workspace follows the workspace change journal POC (#2973) and costs nothing measurable because the stat cache lives in the index. The two tree ids are diffed with `diff-tree -r -M --numstat`, so the summary contains exactly the turn's changes — the user's earlier uncommitted work, staged or not, is part of the baseline — and commits the model makes mid-turn cannot hide changes. The repository's own index, objects, work tree, and refs are never modified. On a 10k-file repository one snapshot costs about 60 ms and the diff about 10 ms; the baseline runs concurrently with the first model request, and tool execution waits for it.
+
+Git is the default executable on `PATH`; no environment plugin is consulted. Outside any repository, or without git, no snapshot is taken and the summary lists the file-tool edits alone, with the working directory as the workspace, so the card still appears but misses shell edits. Nested repositories and submodules are gitlinks and are not descended into.
+
+Changes outside snapshot coverage come from whole-file captures: before a `write`, `edit`, or mutating `str_replace_editor` call runs, the recorder copies the named file into the Session's temporary directory, once per path per turn, and copies it again at turn end; ignored files, files outside the work tree, and every file-tool edit without a snapshot are listed from a line comparison of the two copies ([comparison decision](2026-09-15-changed-file-diff-preview.md)). Files under the temporary directories are omitted unless they lie inside the working directory; a file left in `/tmp` needs `present` to reach the user. Shell edits outside coverage are a known limitation.
+
+The list sorts by a display path in code-unit order: the path relative to the working directory, `../` for repository files above it, `~` under the home directory, otherwise absolute; parent and absolute paths therefore lead without a separate group. The card shows the total count with summed added and deleted lines in its header, three rows before a fold, and a collapse control at the bottom once expanded. Each row opens the turn's review tab in the right Sidebar on that file and the header opens it on the first file ([review decision](2026-09-15-changed-file-diff-preview.md)); with a Host desktop the review tab offers the default-application open.
+
+The recorder appends inside the turn on `agent/turn-stopping` and again after `turn/end` only when tool results settled after the last record, so aborted, failed, and steered turns are covered. The Client keeps the latest announcement per turn and reads its summary once through the authenticated summary route.
+
+The log deliberately carries nothing but the turn number. Summaries, snapshot trees, and captured copies live only as long as the Session in the Host process: the summaries in the recorder, the objects and copies in a temporary directory removed on disposal. A conversation reopened after a Host restart has no card for its earlier turns. The product decision is that a card whose content the Host can no longer open should not appear at all, so the card's lifetime equals the content's lifetime rather than the log's. Content is not kept across restarts ([comparison decision](2026-09-15-changed-file-diff-preview.md)).
+
+## Alternatives considered
+
+**Extending the mutation-call row with hunk counts** kept a second, weaker implementation beside the per-call diff cards and still missed shell edits; git sees every write regardless of the tool.
+
+**Diffing against `HEAD` at turn end** needs one command but attributes the user's uncommitted work to the turn.
+
+**A git tag or `stash create` per turn** leaves refs in the user's repository or omits untracked files; a tree written through a private index does neither.
+
+**A pure-JavaScript git or a bundled binary for hosts without git** adds megabytes and a platform matrix for users who mostly run without the card; until git exists the card lists file-tool edits only.
+
+**Summing the hunks the file tools persist** for uncovered files was the first implementation; it counted a repeatedly edited line more than once and could not show a whole-file comparison, so the recorder now copies the whole file at first touch instead ([comparison decision](2026-09-15-changed-file-diff-preview.md)).
+
+**Recording the file list and counts in the event** was the first implementation: the card would then render from the log forever, while the content it opens would not survive. It was replaced by the announcement-only event so that the card and its content share one lifetime.
+
+**A snapshot object store under the Harness home with a byte bound** was the first implementation's placement: one store per repository, shared by every Session, discarded when it outgrew the bound. It survived Host restarts that the summaries no longer do, needed two configuration fields, and made a Session's snapshot fail when another Session discarded the store; a per-Session temporary directory removes all three.
+
+**An environment-provider seam for locating git** was raised by the team but not settled; the plugin uses `PATH` and keeps its lookup in one place.
+
+**A shadow repository for working directories outside any repository** — a git directory under the Harness home with the work tree pointing at the working directory — would add shell edits to those users' card without adding a `.git`, but a shadow repository has no `.gitignore`, and a configured exclude list cannot reliably keep build outputs, caches, and dependency trees out of every project layout. It is deferred until that exclude policy is settled; the recorder already treats the repository as an input, so adding the tier changes only where the snapshot goes.
+
+## Consequences
+
+Every turn with tool results costs two snapshots and one diff on the Host, and writes blob and tree objects for the changed files into the Session's temporary directory, which disposal removes. Edits the user makes during a turn are attributed to it. Every file-tool edit also copies its whole file into that directory once per turn.
+
+The Web bundle alone mounts the recorder, so headless, SDK, and ACP logs are unchanged; recorded Web scenarios gain the event and the card whenever a turn changes a file; one dedicated scenario seeds a git repository so the card also carries a shell edit, while the others list their file-tool writes alone. The card replaces the Chinese and English "Files changed" row; prose file mentions still resolve against mutation-call paths and deliveries.
+
+Focused tests cover repositories with real git, directories outside any repository, coverage classification of captured paths, an interrupted turn overlapped by the next, ordering, caps, disposal, the macOS stub, the changed-file route, the card's fold and gesture states, and a Loader composition. The keyless Web scenario replays the recorder end to end, including a created file the repository ignores.

+ 53 - 0
.agents/notes/implemented/feature/2026-09-11-turn-changed-files-card.zh.md

@@ -0,0 +1,53 @@
+# Agent Note: 本轮改动文件卡片
+
+Status: implemented
+
+[English](2026-09-11-turn-changed-files-card.md) | 中文
+
+## Problem
+
+一轮结束后,用户想看到模型改了哪些文件、改了多少。Web 的轮尾只列出成功的 `write`、`edit` 与 `str_replace_editor` 调用的路径,没有行数,也漏掉所有 shell 命令改动的文件;消息流中逐调用的 diff 卡片只能一次回答一个调用。
+
+## Decision
+
+Host 侧的 [workspace-changes](../../../../packages/deliverables/workspace-changes/README.zh.md) 插件汇总每个顶层轮次改动的文件,用只带轮号的 `workspace/changes` Session 事件宣告,并通过 `workspaceChanges` 服务提供摘要直到 Session 释放;[产出物插件](../../../../packages/client/ui-deliverables/README.zh.md)读取提供的摘要并渲染为改动文件卡片,取代修改调用行。该事件仅写日志,模型永远看不到。
+
+记录器在轮次开始和结束时用 git 对工作树做快照:以仓库 index 为种子在私有 index 上执行 `add --all`,再执行 `write-tree`。两者都写入 Session 自己拥有的临时对象目录,仓库自己的对象库以只读 alternate 挂接,因此用户仓库不会多出任何对象;把捕获的每个字节都放在工作区外学自工作区变更日志 POC(#2973),而且因为 stat 缓存在 index 里,它对耗时没有可测量的影响。两个 tree id 用 `diff-tree -r -M --numstat` 比较,因此摘要恰好只包含本轮的改动——用户此前未提交的工作,无论是否暂存,都属于基线——模型在轮中做的提交也藏不住改动。仓库自己的 index、对象、工作树和 ref 从不被修改。在一万个文件的仓库上,一次快照约 60 ms,比较约 10 ms;基线与首次模型请求并行,工具执行会等待它。
+
+git 使用 `PATH` 上的默认可执行文件,不询问任何环境插件。不在任何仓库内或没有 git 时,不做快照,摘要只列文件工具的编辑,并以工作目录作为工作区,因此卡片仍会出现,但缺少 shell 的改动。嵌套仓库和 submodule 是 gitlink,不会深入。
+
+快照覆盖之外的改动来自整文件捕获:在 `write`、`edit` 或有修改作用的 `str_replace_editor` 调用运行之前,记录器把所指文件复制到 Session 的临时目录,每轮每个路径一次,轮次结束时再复制一次;被忽略的文件、工作树之外的文件,以及没有快照时的每一次文件工具编辑,都由两份副本的逐行对比列出([对比决定](2026-09-15-changed-file-diff-preview.zh.md))。临时目录下的文件被省略,除非它们位于工作目录内;留在 `/tmp` 里的文件需要 `present` 才能到达用户。覆盖之外的 shell 编辑是已知限制。
+
+列表按展示路径的码元顺序排序:相对工作目录的路径,仓库内位于其上的文件为 `../`,家目录下为 `~`,其余为绝对路径;上级路径与绝对路径因此自然排在最前,不需要单独分组。卡片标题显示总数与增删行数合计,折叠前显示三行,展开后底部有收起控件。每一行在右侧 Sidebar 打开本轮的 review tab 并选中该文件,标题则在第一个文件上打开它([review 决定](2026-09-15-changed-file-diff-preview.zh.md));Host 有桌面时,review tab 提供用默认应用打开。
+
+记录器在 `agent/turn-stopping` 时于轮内追加事件,并且仅当最后一次记录之后仍有工具结果结束时在 `turn/end` 后再次追加,因此中止、失败和被转向的轮次都被覆盖。Client 保留每轮最新的宣告,并通过经过认证的摘要路由读取一次摘要。
+
+日志有意只携带轮号。摘要、快照树与捕获的副本只在 Host 进程内随 Session 存活:摘要在记录器里,对象和副本在释放时删除的临时目录里。Host 重启后重新打开的对话,先前轮次没有卡片。产品上的决定是,Host 已经打不开内容的卡片根本不该出现,所以卡片的寿命等于内容的寿命,而不是日志的寿命。内容不跨重启保留([对比决定](2026-09-15-changed-file-diff-preview.zh.md))。
+
+## Alternatives considered
+
+**给修改调用行补上 hunk 计数**会在逐调用 diff 卡片旁保留第二个更弱的实现,且仍然漏掉 shell 编辑;git 不管工具是什么都能看到每次写入。
+
+**轮末直接与 `HEAD` 比较**只需一条命令,但会把用户未提交的工作算到本轮。
+
+**每轮打一个 git tag 或 `stash create`**会在用户仓库里留下 ref,或者遗漏未跟踪文件;通过私有 index 写出的树两者都不会发生。
+
+**为没有 git 的主机引入纯 JavaScript 的 git 或捆绑二进制**会为大多数不使用卡片的用户增加数 MB 体积和一套平台矩阵;在 git 出现之前卡片只列文件工具的编辑。
+
+**累加文件工具持久化的 hunk** 是未覆盖文件的第一版实现;它会把反复编辑的行计算多次,也给不出整文件对比,因此记录器改为在首次触碰时复制整个文件([对比决定](2026-09-15-changed-file-diff-preview.zh.md))。
+
+**把文件列表和行数记进事件**是第一版实现:卡片从此可以永远从日志渲染,而它打开的内容却活不了那么久。它被只做宣告的事件取代,让卡片和内容共用一个寿命。
+
+**Harness home 下带字节上限的快照对象库**是第一版实现的放置方式:每个仓库一个、所有 Session 共用、超过上限就丢弃。它能活过 Host 重启,而摘要已经不再需要这一点;它还需要两个配置字段,并且一个 Session 丢弃对象库会让另一个 Session 的快照失败。每个 Session 一个临时目录把这三点一起去掉。
+
+**用于定位 git 的环境提供者接缝**由团队提出但尚未定案;插件使用 `PATH` 并把查找收敛在一处。
+
+**为不在任何仓库内的工作目录建影子仓库**,即把 git 目录放在 Harness home 下、工作树指向工作目录,可以让这些用户在不多出 `.git` 的情况下也看到 shell 改动,但影子仓库没有 `.gitignore`,一份配置的排除列表无法在所有项目布局里可靠地挡住构建产物、缓存和依赖目录。它暂缓到排除策略定下来为止;记录器已经把仓库当作输入,加这一档只改变快照写到哪里。
+
+## Consequences
+
+每个有工具结果的轮次在 Host 上花费两次快照和一次比较,并把改动文件的 blob 与 tree 对象写入 Session 的临时目录,释放时一并删除。用户在轮次进行中的编辑会被算到该轮。每次文件工具编辑还会把整个文件复制进该目录,每轮一次。
+
+只有 Web bundle 挂载记录器,因此 headless、SDK 与 ACP 日志不变;录制的 Web 场景只要某一轮改动了文件就会新增该事件与卡片;一个专门的场景种入 git 仓库,让卡片也带上 shell 的改动,其余场景只列它们的文件工具写入。卡片取代了中英文的“本轮文件改动”行;正文文件提及仍按修改调用路径与交付解析。
+
+聚焦测试用真实 git 覆盖仓库、不在任何仓库内的目录、对已捕获路径的覆盖范围分类、被下一轮压上的中断轮次、排序、上限、释放、macOS 桩程序、改动文件路由、卡片的折叠与手势状态,以及一次 Loader 组合。无密钥的 Web 场景端到端回放记录器,其中包括一个被仓库忽略的新建文件。

+ 2 - 2
.agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.md
-2026-09-14-desktop-primary-runtime.md: 17d489e6c788c786cefcdddf5f1983ac48522bfe
-2026-09-14-desktop-primary-runtime.zh.md: f8be37801452ebf4f4623a1ad0342de770e21ad5
+2026-09-14-desktop-primary-runtime.md: d4dfd5fae400737de5ae852c05e2b6f117dc78f1
+2026-09-14-desktop-primary-runtime.zh.md: e64b6a53d6270f922e477fd844d611f8249581a4

+ 3 - 3
.agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.md

@@ -10,11 +10,11 @@ Desktop agents need predictable Python data-processing libraries and an independ
 
 ## Decision
 
-Desktop ships Python, Node.js, pnpm, numpy and pandas as one release-bound payload. The path-query tool installs the payload from application resources into the fixed Harness-home directory and returns absolute paths. It does not change PATH, environment variables or package-manager configuration. pnpm uses its native global-install rules.
+Desktop ships Python, Node.js, pnpm, data-processing libraries and Office authoring libraries as one release-bound payload. The path-query tool installs the payload from application resources into the fixed Harness-home directory and returns absolute paths and the bundled Python distribution versions. The version report excludes packages added by users. It does not change PATH, environment variables or package-manager configuration. pnpm uses its native global-install rules.
 
-The application version and component versions live in `runtime.json`, not the directory name. Installation publishes a completed staged copy and retains the previous directory until replacement succeeds. Matching releases reuse installed files; upgrades replace user-added Python dependencies inside the managed tree. The Desktop single-instance owner and the tool's shared installation promise serialize normal installation requests.
+The application version, component versions, Python distribution versions and locked-input digest live in `runtime.json`, not the directory name. The digest covers the selected target's interpreter and wheel archives, shared wheel inputs, Python distribution versions, pnpm version and assembly format; other targets do not invalidate it. Key order within the selected target, wheel records and distribution map, plus wheel-entry order, participates in this identity; top-level lock key order does not. Extraction or assembly changes that alter payload bytes without changing locked inputs require an explicit format bump. Installation publishes a completed staged copy and retains the previous directory until replacement succeeds. Matching payloads reuse installed files; replacements also replace user-added Python dependencies inside the managed tree. Older manifests without a digest remain readable and differ from the current payload. Distribution names use PEP 503 normalization; duplicate normalized names are rejected, and present numpy/pandas distribution versions must agree with their component versions. The Desktop single-instance owner and the tool's shared installation promise serialize normal installation requests.
 
-Node downloads and hash-verifies the complete locked wheel set and unpacks these library-only archives into site-packages. This avoids build-host Python and pip version selection without implementing dependency resolution or general wheel installation. Wheels with `.data` installation directories are rejected; command-line entry-point wrappers are outside this library payload. Native smoke executes the final payload after temporary files are removed, so interpreter links must survive relocation.
+Node downloads and hash-verifies the complete locked wheel set and unpacks library files into site-packages. This avoids build-host Python and pip version selection without implementing dependency resolution or general wheel installation. Auxiliary scripts, including XlsxWriter's VBA extraction script, remain under the wheel's `.data/scripts` directory; command-line entry-point wrappers are outside this library payload. Other `.data` installation schemes are rejected. Native smoke executes the final payload after temporary files are removed, checking the exact locked distribution set plus bundled pip, Python and pinned wheel versions, dependency completeness and editable Office document round trips, so interpreter links must survive relocation. Smoke checks disable bytecode writes to keep validation artifacts out of the shipped payload.
 
 macOS grants `com.apple.security.cs.allow-jit` only to the standalone Node executable. Hardened-runtime signing without that entitlement prevents V8 from allocating its code region. Interpreter and library smoke checks run after signing as well as after staging cleanup; a valid signature alone does not establish executable behavior.
 

+ 3 - 3
.agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.zh.md

@@ -10,11 +10,11 @@ Desktop 代理需要在没有开发环境的机器上获得确定的 Python 数
 
 ## Decision
 
-Desktop 将 Python、Node.js、pnpm、numpy 和 pandas 作为绑定应用版本的产物交付。路径查询工具从应用资源将产物安装到 Harness home 下的固定目录,并返回绝对路径。它不修改 PATH、环境变量或包管理器配置。pnpm 使用原生全局安装规则。
+Desktop 将 Python、Node.js、pnpm、数据处理库和 Office 创作库作为绑定应用版本的产物交付。路径查询工具从应用资源将产物安装到 Harness home 下的固定目录,并返回绝对路径与内置 Python 分发包版本。版本报告不包含用户自行添加的包。它不修改 PATH、环境变量或包管理器配置。pnpm 使用原生全局安装规则。
 
-应用版本和组件版本记录在 `runtime.json` 中,不放在目录名里。安装发布完整的暂存副本,并在替换成功前保留之前的目录。同版本复用已安装文件;升级替换受管目录内用户添加的 Python 依赖。Desktop 单实例所有者和工具共享的安装 Promise 串行处理正常安装请求。
+应用版本、组件版本、Python 分发包版本和锁定输入摘要记录在 `runtime.json` 中,不放在目录名里。摘要涵盖所选目标的解释器与 wheel 压缩包、共享 wheel 输入、Python 分发包版本、pnpm 版本和组装格式;其他目标不会使其失效。所选目标、wheel 记录及分发包映射内部的键顺序,以及 wheel 条目顺序参与该身份计算;锁文件顶层键的顺序不参与。解压或组装逻辑在锁定输入不变时改变产物字节,必须显式提升格式版本。安装发布完整的暂存副本,并在替换成功前保留之前的目录。相同产物复用已安装文件;替换也会移除受管目录内用户添加的 Python 依赖。不含摘要的旧清单仍可读取,并与当前产物区分。分发包名称按 PEP 503 归一化,归一化后重复的名称会被拒绝;清单包含 numpy/pandas 分发包版本时,必须与相应组件版本一致。Desktop 单实例所有者和工具共享的安装 Promise 串行处理正常安装请求。
 
-Node 下载并校验完整锁定 wheel 集的哈希,将这些仅含库的压缩包解压到 site-packages。这避免选择构建主机的 Python 和 pip 版本,也无需实现依赖解析或通用 wheel 安装。含 `.data` 安装目录的 wheel 会被拒绝;命令行入口包装器不属于该库产物。本机 smoke 在临时文件删除后执行最终产物,因此解释器链接必须在迁移后仍有效。
+Node 下载并校验完整锁定 wheel 集的哈希,将库文件解压到 site-packages。这避免选择构建主机的 Python 和 pip 版本,也无需实现依赖解析或通用 wheel 安装。XlsxWriter 的 VBA 提取脚本等辅助脚本保留在 wheel 的 `.data/scripts` 目录中;命令行入口包装器不属于该库产物。其他 `.data` 安装方案会被拒绝。本机 smoke 在临时文件删除后执行最终产物,检查精确锁定的分发包集合与内置 pip、Python 与固定 wheel 版本、依赖完整性及可编辑 Office 文档的写入和读取,因此解释器链接必须在迁移后仍有效。Smoke 检查禁用字节码写入,避免把验证产物纳入分发内容。
 
 macOS 仅向独立 Node 可执行文件授予 `com.apple.security.cs.allow-jit`。缺少此权限的强化运行时签名会阻止 V8 分配代码区域。解释器和库的 smoke 检查在签名后以及暂存清理后执行;签名有效本身不能证明程序可运行。
 

+ 0 - 27
.agents/notes/implemented/feature/2026-09-14-model-image-input-settings.md

@@ -1,27 +0,0 @@
-# Agent Note: Model image-input settings
-
-Status: implemented
-
-English | [中文](2026-09-14-model-image-input-settings.zh.md)
-
-## Problem
-
-Models settings can edit a model id without exposing the input capabilities that determine whether image attachments are accepted. A custom vision model can therefore appear in the picker while retaining a text-only declaration.
-
-## Decision
-
-Each model row exposes image input under Model options. Supported declares text and image; Not supported declares text only; Default removes the model's explicit input field. DeepSeek writes `inputModalities`, whose absent value means text only. Pi-ai writes `input`, whose absent or empty value inherits the installed model catalog or provider default. Opening a row preserves that inheritance without materializing an override.
-
-The shared field replaces one drafted row and preserves unrelated metadata. Selecting text only or default for DeepSeek also removes its image request limits, because the adapter rejects those limits without image input. Saving uses the existing catalog-array settings mutation and adapter validation. Configuration declares an upstream capability; it does not add image processing to a text-only model.
-
-## Alternatives considered
-
-**Keep `input` editable only in the settings document.** The [earlier pi-ai modality decision](../../archived/architecture/2026-08-12-pi-ai-route-default-input-modalities.md) kept this field outside the model-list editor. That leaves users who add custom vision models through the UI unable to enable their image input there. Per-row editing supplies that configuration while the default choice preserves catalog inheritance.
-
-**A two-state switch.** Treating an absent pi-ai declaration as disabled would misrepresent inherited vision support and encourage overwriting catalog defaults. The explicit Default choice preserves the adapter's existing resolution rules.
-
-**Keep image limits when disabling DeepSeek images.** This leaves a configuration that the adapter refuses to save. Clearing the image-specific limits makes the selected text-only state valid while preserving unrelated model fields.
-
-## Consequences
-
-Users can configure image input for DeepSeek and custom pi-ai model rows through the same control. Restoring defaults can change effective capabilities when the installed catalog or provider defaults change. DeepSeek image limits must be configured again after disabling images. Provider routing and the [catalog recovery rules](../bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.md) remain owned by their existing decisions.

+ 0 - 27
.agents/notes/implemented/feature/2026-09-14-model-image-input-settings.zh.md

@@ -1,27 +0,0 @@
-# Agent Note:模型图片输入设置
-
-Status: implemented
-
-[English](2026-09-14-model-image-input-settings.md) | 中文
-
-## 问题
-
-模型设置可以编辑模型 ID,却没有展示决定图片附件是否被接受的输入能力。因此,自定义视觉模型虽然出现在选择器中,仍可能保留仅文本的声明。
-
-## 决策
-
-每个模型行在「模型选项」下提供图片输入设置。「支持」声明文本和图片;「不支持」声明仅文本;「默认」移除模型的显式输入字段。DeepSeek 写入 `inputModalities`,缺省时表示仅文本。Pi-ai 写入 `input`,缺省或空数组时继承已安装模型目录或提供方默认值。打开模型行保留这种继承,不会生成覆盖值。
-
-共享字段替换一个草稿模型行并保留无关元数据。DeepSeek 选择仅文本或默认时,还会移除图片请求限制,因为适配器在没有图片输入时拒绝这些限制。保存使用现有的模型目录数组设置变更和适配器校验。配置声明上游能力,不会为仅文本模型增加图片处理能力。
-
-## 考虑过的替代方案
-
-**仅允许在设置文档中编辑 `input`。** [早期 pi-ai 输入模态决策](../../archived/architecture/2026-08-12-pi-ai-route-default-input-modalities.md)将该字段留在模型列表编辑器之外。这使通过 UI 添加自定义视觉模型的用户无法在同一界面启用图片输入。逐行编辑提供了该配置,而默认选项保留模型目录继承。
-
-**两态开关。** 将缺省的 pi-ai 声明视为禁用,会错误表达继承的视觉能力,并促使用户覆盖模型目录的默认值。显式「默认」选项保留适配器现有的解析规则。
-
-**禁用 DeepSeek 图片时保留图片限制。** 这会留下适配器拒绝保存的配置。清除图片专属限制,使选择的仅文本状态有效,同时保留无关模型字段。
-
-## 影响
-
-用户可以通过相同控件配置 DeepSeek 和自定义 pi-ai 模型行的图片输入。恢复默认值后,有效能力可能随已安装模型目录或提供方默认值变化。禁用图片后,DeepSeek 图片限制需要重新配置。提供方路由和[模型目录恢复规则](../bug-fix/2026-09-07-pi-ai-settings-catalog-recovery.zh.md)仍由现有决策负责。

+ 6 - 0
.agents/notes/implemented/feature/2026-09-15-bundled-office-skills.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-15-bundled-office-skills.md
+2026-09-15-bundled-office-skills.md: 089ca0326a7c53d4ee023608010efd49a8616db2
+2026-09-15-bundled-office-skills.zh.md: cd10aa863b1149594e9eb6b6a8e479511424f20d

+ 31 - 0
.agents/notes/implemented/feature/2026-09-15-bundled-office-skills.md

@@ -0,0 +1,31 @@
+# Agent Note: Bundled Office skills with structural verification
+
+Status: implemented
+
+English | [中文](2026-09-15-bundled-office-skills.zh.md)
+
+## Problem
+
+Office tasks need format-specific editing guidance and dependable file checks. Requiring users to install interpreters, package managers, or rendering command-line tools interrupts ordinary document delivery. Structural heuristics can also reject valid merged tables, multi-section documents, or Chinese text without observing an actual layout defect.
+
+## Decision
+
+The [Office provider](../../../../packages/skill/skill-office/README.md) contributes three independently discoverable skills at the bundled rank. The default workflow uses `load_workspace_dependencies` and its Python executable; explicit user and AGENTS.md environment choices take precedence. A configurable absolute asset root lets Desktop expose Python-readable resources outside its application archive. Registration validates the required resources and YAML descriptions; loaded instructions exclude the metadata. Disposal removes every candidate.
+
+One standard-library checker recognizes Transitional and Strict OOXML namespaces, validates ZIP/XML integrity and internal relationships, reports format-specific structure, and checks only explicit text or count assertions. Corrupt or encrypted ZIP members produce the same JSON package-failure report as other invalid documents. DOCX table summaries count logical grid columns, including merged cells. Section geometry is reported rather than judged against the final section; font filenames do not establish glyph coverage. XLSX formula counts never imply recalculation. Text assertions follow section and note references and worksheet string indices, so retained headers, unused note definitions, comments, glossary entries, and unused strings cannot satisfy requested wording.
+
+Desktop mounts the skill provider and runtime query independently of document rendering. Word uses python-docx, PowerPoint creation and editing use python-pptx, and Excel uses openpyxl and pandas. The managed payload and the ordinary creation examples require neither a rendering engine nor a separate presentation authoring library.
+
+Office skills and the bundled-runtime query remain registered without a rendering service. Visual inspection depends on the active model accepting images and a rendering tool being available. Otherwise the skills complete structural and content checks and deliver with the unverified visual scope stated. `present` refers to the current workspace source file; it does not preserve a private copy.
+
+## Alternatives considered
+
+**Require a plan and a local renderer before every delivery.** Simple edits do not need a fixed planning artifact, and models without image input cannot judge rendered pages. Mandatory renderer installation would turn an optional quality signal into a dependency unrelated to many requests.
+
+**Judge layout using package structure and font-name guesses.** Merged cells, different section widths, font substitution, and application layout rules prevent those observations from establishing rendered correctness. The checker reports facts and leaves visual judgments to actual images.
+
+**Share one undifferentiated Office skill.** Format-specific discovery avoids loading spreadsheet formula guidance for a Word edit or presentation instructions for a cell update. The deterministic checker remains shared because all three formats use the same package relationship rules.
+
+## Consequences
+
+The provider supplies reusable instructions without selecting or installing a deployment runtime. The checker is portable wherever the supported Python standard library works, but it cannot establish Office rendering fidelity, advanced feature preservation, or formula results. Loader and disposal tests cover resource relocation, activation failures, and absent rendering services. A recorded Session pins the Office catalog and loaded instruction body. Checker tests cover structural failures and JSON diagnostics; native payload smoke executes the copied checker with the bundled interpreter.

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä