` 命名由其 Host 工作区解析相对或绝对路径的 Session([语法](../../../../packages/util/workspace-path/README.zh.md))。其它任何 scheme——`sidebar://guide`——是导航地址:它命名一个 tab 而非数据,模型对它回答 `none`([tab 类型与导航](../../../../packages/client/ui-sidebar-right/README.zh.md))。
### 服务
@@ -55,7 +55,7 @@ type UseResource = (address: string) => ResourceSnap
每个地址一条记录。持有只控制观察的启停,不控制底层文件或 Session 的生灭。持有者是 hook 的订阅者加 pin;第一个持有者在 `AbortController` 下打开提供方的流,之后的持有者共享它并立刻读到最新值,最后一个释放时中止流并把快照重置为空闲——有提供方注册时为 `loading`,否则为 `none`。地址已被持有时到达的提供方会打开该地址的流;离开的提供方中止它,地址读作 `none`。记录在页面存续期内保留,使 `source(address)` 在 React 渲染到订阅的窗口与 StrictMode 重挂载之间保持引用稳定,否则重建记录会让每次渲染重订阅、重开流。
-右侧 Sidebar 的 Tab 域在每条打开的 tab 记录存续期内钉住其地址,所以切 tab 卸载正文不关流、切回读到最新值;撤销恢复的记录是一次新的钉住,模型已放掉的资源会重新读取([tab 类型与导航](2026-09-05-sidebar-tab-types-and-navigation.zh.md))。`openResource(address)` 只收资源地址;引导页与文件树这类页面按 kind 打开,从不进入资源模型。
+右侧 Sidebar 的 Tab 域在每条打开的 tab 记录存续期内钉住其地址,所以切 tab 卸载正文不关流、切回读到最新值;撤销恢复的记录是一次新的钉住,模型已放掉的资源会重新读取([tab 类型与导航](../../../../packages/client/ui-sidebar-right/README.zh.md))。`openResource(address)` 只收资源地址;引导页与文件树这类页面按 kind 打开,从不进入资源模型。
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-09-05-package-manifest-types.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-05-package-manifest-types.i18n.yaml
deleted file mode 100644
index 1cbd6713ec..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-05-package-manifest-types.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-05-package-manifest-types.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-05-package-manifest-types.md
-/agent-note-package-manifest-declaration-ownership:
- en: 26d50551c7d49cf8
- zh: eac4a93af773c945
-/agent-note-package-manifest-declaration-ownership/problem:
- en: bdc0fcaf27081889
- zh: 45eb034edd0bed82
-/agent-note-package-manifest-declaration-ownership/decision:
- en: da43234e14fa9bd5
- zh: 0389015e086f4e25
-/agent-note-package-manifest-declaration-ownership/alternatives-considered:
- en: bf99e75ffab5cb45
- zh: 398f5934858aaca5
-/agent-note-package-manifest-declaration-ownership/consequences:
- en: ce7c3df526622bf2
- zh: 91d51dff77371cb0
diff --git a/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.i18n.yaml
deleted file mode 100644
index d7f7c0dc3b..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.i18n.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-05-shared-client-control-primitives.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.md
-/agent-note-shared-client-control-primitives:
- en: 53e5c771078f4383
- zh: 59e4cc45d06adbee
-/agent-note-shared-client-control-primitives/problem:
- en: 7a711c1cafe0be88
- zh: 7275b36fdd5094f6
-/agent-note-shared-client-control-primitives/decision:
- en: cc335d6820b9ab1c
- zh: e375bababc0fccdb
-/agent-note-shared-client-control-primitives/finding-the-duplicates:
- en: f50ed2aea00a34c5
- zh: a8a27c1d1e0177a8
-/agent-note-shared-client-control-primitives/what-stays-local:
- en: e4fb0a84b9924d5a
- zh: 30545265b2159f04
-/agent-note-shared-client-control-primitives/alternatives-considered:
- en: f77deaa327bc5395
- zh: be8450b665be04bf
-/agent-note-shared-client-control-primitives/testing:
- en: 578f6bad674e4c40
- zh: 0a8f2e72bb4d1f61
-/agent-note-shared-client-control-primitives/consequences:
- en: f94dd6f669d00276
- zh: a67de4bd3f242232
diff --git a/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.md b/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.md
deleted file mode 100644
index db8cb9a8bc..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.md
+++ /dev/null
@@ -1,73 +0,0 @@
-# Agent Note: Shared client control primitives
-
-Status: implemented
-
-English | [中文](2026-09-05-shared-client-control-primitives.zh.md)
-
-## Problem
-
-Client feature plugins compose through slots and never import one another's values, so `@deepseek-ai/dsh-client-ui-primitives` is their only channel for sharing a React component. A control that grows inside one feature package is invisible to the next package that needs the same thing, and copying its markup and CSS is the cheapest move available. Three families had diverged that way. A 36×20 toggle switch existed only inside `ui-settings-plugins`. Read-only capsule badges were declared five separate times across `ui-agent-preset`, `ui-settings-plugins`, and `ui-settings-plugin-inventory`, with two different corner radii and separately authored palettes. A plugin-phase status dot was reimplemented in `ui-settings-plugin-inventory` next to the shared `StateDot` it duplicates.
-
-Neither half of the fix was available to an author. No checklist told them to look in `ui-primitives` before writing a control, and the package gave them nothing to look at: its README named six source files while the package exported more than forty symbols.
-
-## Decision
-
-**A control that a second client package needs lives in `ui-primitives`.** The rule is guidance for authors, not a gate: a feature package may still write its own component when its need is genuinely specific, and the [what stays local](#what-stays-local) list below records the cases from this change. What the rule forbids is the copy — when a control already exists, the author either uses it or lifts the deliberate difference into a prop.
-
-`Tag` is the read-only capsule badge. Its geometry is fixed at the size `ui-agent-preset` established: `999px` radius, `1px 8px` padding, 11px text on a 17px line, 500 weight, `inline-flex`, no wrapping. A single closed `TagTone` union selects the palette, and every member exists because a shipped call site needed it: `outline` and `solid` from the agent-preset section, `neutral` and `quiet` from the plugin settings fields, and `success`, `info`, `warning`, and `danger` from the plugin inventory's enablement tags. The component carries no copy of its own, as every Cordis-free primitive must.
-
-`Switch` is the two-state toggle, at the 36×20 track and 16px thumb `ui-settings-plugins` established. `label` is required and has no default, so a render site cannot omit the accessible name; `title` carries a lock reason where a deployment disables the control.
-
-`StateDotState` includes `idle` for a tracked subject with no activity in progress. The plugin inventory's `pending` and `unloading` phases need that semantic member; without it those two phases lose their marker entirely. The five-state union remains safe for consumers because each package produces a `StateDotState` from its own closed status union and none switches over `StateDotState` itself. The current solid-dot palette and rotating `ongoing` loader are owned by the later [StateDot visual language decision](../feature/2026-09-17-state-dot-visual-language.md).
-
-**The component catalog in the `ui-primitives` README is what makes the rule usable.** It lists every exported component with its purpose and the case it is wrong for, and it names the three pairs that are easy to confuse: `Tag` against `Pill`, `DisclosureRow` against card-shaped disclosure, and the package-internal `FoldToggle` against the exported surface. `Pill` is the selectable capsule button — it takes `active` and `onClick` and drives view switchers and filters; `Tag` is the read-only badge and takes neither. The component comments use the same distinction.
-
-The rule is stated in [packages/client/AGENTS.md](../../../../packages/client/AGENTS.md) as the first step of the new-component checklist, and [docs/web-styling.md](../../../../docs/web-styling.md) points at the catalog so an author arriving from the styling side reaches it too.
-
-## Finding the duplicates
-
-A name-based search undercounts. `.badge`, `.tag`, `.chip`, and `.configTag` miss a capsule named for its role rather than its appearance — `PluginCard`'s unsaved marker is `.pending`. Geometry is the reliable probe: search CSS Modules rules carrying both `border-radius: 999px` and `padding: 1px 8px`. That signature belongs to `Tag` and the deliberately local broken badge below; the unsaved marker keeps only its `flex: none` placement class.
-
-
-## What stays local
-
-A mechanical search groups these controls with the promoted three. They stay in their own packages, because the grouping is superficial:
-
-- **`ui-trajectory`'s toolbar toggle** carries `role="switch"` but is an 88px labelled control with an inline track, and it currently renders `hidden`. It is not the same widget as `Switch`.
-- **`ui-plan`'s mode chip** and **`ui-conversation`'s `ReferenceChip`** are interactive: the first is a warning-toned button with hover, focus, disabled, and a close affordance; the second is a Lexical atom node with its own truncation. Neither is a read-only badge.
-- **`ui-trajectory`'s cell tag** and **`ui-user-questions`'s recommendation badge** use their own geometry — a 6px radius at table density, and a 6px radius at 600 weight on the sidebar accent. Forcing either into the capsule baseline would change a deliberate design, not an accidental one.
-- **`TerminalBlock`'s exit-status pill** stays a static `Pill`. It sits on a 24px command line at the pill's own geometry, and `Tag`'s 11px capsule would not fit that row. The read-only/selectable split is the usual guide, but size decides this one, and the catalog says so.
-- **`ui-agent-preset`'s broken badge** shares the capsule geometry but carries a solid error fill that no second site uses, and it is the hover anchor for a tooltip element of its own. `Tag` would have to keep a palette override in the feature stylesheet and depend on cross-file CSS ordering to win it.
-
-## Alternatives considered
-
-**A `SettingsCard` primitive.** Rejected. `.card` appears in fifteen packages, but only three carry settings-page semantics, and those three differ in behaviour rather than appearance: `ui-settings-plugins`' `PluginCard` stages edits and collapses only after a Host-confirmed save, `ui-agent-preset`'s cards are selectable, and the plugin inventory's are read-only. `PluginCard`'s own header comment already records why it cannot use the shared disclosure row. A single component would have to accept all three behaviours through props that no caller uses more than one of.
-
-**A gate rejecting `role="switch"` or a `.switch` rule outside `ui-primitives`.** Rejected. The goal is that authors reuse what exists, not that they are prevented from building. A gate would fail a package with a legitimately specific control — `ui-trajectory`'s labelled toolbar toggle is exactly that case — and the cost of the false rejection lands on the author least able to argue with it. The catalog plus the checklist step address the actual failure, which is that authors did not know the control existed.
-
-**Preserving every current appearance behind extra `Tag` props.** Rejected. The plugin inventory's 5px radius and the agent-preset capsule are the same kind of tag in two shapes, and neither difference was decided. Keeping both would fix an accident into a public union and leave the next author to guess which one to pick.
-
-**Extending `Pill` instead of adding `Tag`.** Rejected. `Pill` is 24px tall on a 12px radius with 12px text; the badge baseline is denser and rounder. Merging them would produce one component whose size depends on whether `onClick` is present, and would erase the read-only/selectable distinction that the catalog needs in order to answer "which one do I want".
-
-**A two-axis `variant × tone` API for `Tag`.** Rejected. Three variants against six tones describes eighteen combinations of which eight ship, and it lets a caller request combinations with no defined appearance. The flat eight-member union maps each value to exactly one shipped appearance.
-
-**Rendering no dot for `pending` and `unloading` instead of adding `idle`.** Rejected. Those phases need a compact inactive or transitional marker; dropping it would remove information from the inventory in a change whose purpose is to consolidate presentation.
-
-**Putting the reuse rule in the root `AGENTS.md`.** Rejected. The rule governs `packages/client` alone, and the root file sits exactly at its 1950-word ceiling in `scripts/doc-budgets.manifest.json`, so stating it there would have to displace an unrelated repository-wide rule.
-
-## Testing
-
-`Tag`, `Switch`, and the extended `StateDot` carry component specs in `packages/client/ui-primitives/tests`, inside the per-file 100% coverage gate. `StateDot`'s palette is pinned by reading its stylesheet: CSS Modules resolve to class-name maps in the component suites, so a state whose color rule is missing renders on the inherited color and no render assertion notices.
-
-The migrated render sites keep package specs for their role, accessible name, state mapping, and interaction. Web ARIA snapshots cover the assembled presentations — `Switch` keeps `role="switch"` with `aria-checked`, and the inventory's phase dot keeps its `role="img"` name on a wrapper because `StateDot` is `aria-hidden`.
-
-The component and stylesheet specs pin the capsule geometry, StateDot artwork, animation, tokens, and font weight; Web snapshots pin accessible structure. Manual visual inspection remains responsible for the rendered result across themes.
-
-## Consequences
-
-- A new client control now has one place to check and one place to add, and the catalog makes the check a single file read rather than a `grep` over forty exports.
-- `TagTone` is eight members wide because eight appearances shipped. Adding a ninth requires a render site that needs it, not a symmetry argument.
-- The plugin inventory uses capsule tags; the disabled tag uses the `neutral` tone's visible `--dsw-alias-bg-module-platform` fill. Its phase markers use the shared `StateDot`, including the rotating loader for active transitions. The unconfigured-secret badge in the plugin settings fields uses the baseline 500 weight.
-- The plugin inventory palette contains no literal `#b45309`. The conditional tag uses the real `--dsw-alias-state-warn-primary` token rather than the nonexistent `--dsw-alias-state-warning-primary` alias and its hardcoded fallback.
-- The rule cannot be checked mechanically. A future author can still copy a control, and only review will catch it. That is the accepted cost of not gating: the alternative rejects legitimate work, and the packages that stay local above are proof that legitimate work exists.
-- `Tag` and `Switch` are shared across settings and preset packages; future consumers extend these primitives only when an existing prop cannot express a deliberate difference.
diff --git a/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.zh.md b/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.zh.md
deleted file mode 100644
index 6d4b04a21b..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.zh.md
+++ /dev/null
@@ -1,73 +0,0 @@
-# Agent Note: Shared client control primitives
-
-Status: implemented
-
-[English](2026-09-05-shared-client-control-primitives.md) | 中文
-
-## Problem
-
-客户端功能插件通过 slot 组合,彼此从不 import 对方的值,因此 `@deepseek-ai/dsh-client-ui-primitives` 是它们共享 React 组件的唯一通道。在某个功能包内部长出来的控件,对下一个需要同样东西的包不可见,而复制它的标记与 CSS 是当时最省事的做法。三类控件已经这样分叉:36×20 的开关只存在于 `ui-settings-plugins`;只读胶囊徽章在 `ui-agent-preset`、`ui-settings-plugins`、`ui-settings-plugin-inventory` 里被分别声明了五次,带着两种圆角和各自编写的配色;插件相位状态点在 `ui-settings-plugin-inventory` 里被重新实现,就在它所复制的共享 `StateDot` 旁边。
-
-修复所需的两半,作者当时一半也拿不到。没有任何清单要求他们在写控件前先看 `ui-primitives`,而这个包也没给他们可看的东西:README 只列了六个源文件,包却导出了四十多个符号。
-
-## Decision
-
-**第二个客户端包需要的控件,住在 `ui-primitives`。** 这条规则是对作者的引导,不是门禁:功能包在需求确实特殊时仍可以自己写组件,下面的[保留在原包的控件](#what-stays-local)记录了本次改动中的这些例子。规则禁止的是复制——控件已经存在时,作者要么用它,要么把那处有意的差异提升成一个 prop。
-
-`Tag` 是只读胶囊徽章。几何锁定在 `ui-agent-preset` 确立的尺寸:`999px` 圆角、`1px 8px` 内边距、11px 字号配 17px 行高、500 字重、`inline-flex`、不换行。单个闭合的 `TagTone` 联合选择配色,每个成员都因为有已发布的调用点需要它而存在:`outline` 与 `solid` 来自 agent preset 区块,`neutral` 与 `quiet` 来自插件设置字段,`success`、`info`、`warning`、`danger` 来自插件清单的启用标签。与所有 Cordis-free 原语一样,这个组件不自带任何文案。
-
-`Switch` 是双态开关,采用 `ui-settings-plugins` 确立的 36×20 轨道与 16px 滑块。`label` 必填且没有默认值,渲染点无法省略无障碍名称;`title` 在部署禁用该控件时承载锁定原因。
-
-`StateDotState` 包含 `idle`,表示一个被跟踪的对象当前没有进行中的活动。插件清单的 `pending` 与 `unloading` 相位需要这个语义成员;没有 `idle`,这两个相位会彻底失去标记。五态联合对消费方保持安全,因为每个包都从自己的闭合状态联合**产生** `StateDotState`,没有任何一个对 `StateDotState` 本身做 switch。当前的纯色点配色与旋转 `ongoing` loading 由后续的 [StateDot 视觉语言决策](../feature/2026-09-17-state-dot-visual-language.zh.md)负责。
-
-**`ui-primitives` README 里的组件目录,是这条规则得以可用的前提。** 它列出每个导出组件的用途,以及它不适用的场合,并点名三组容易混淆的配对:`Tag` 与 `Pill`、`DisclosureRow` 与卡片式折叠、包内的 `FoldToggle` 与对外导出面。`Pill` 是可选中的胶囊按钮——它接受 `active` 与 `onClick`,用于视图切换与筛选器;`Tag` 是只读徽章,两者都不接受。组件注释使用同一套区分。
-
-规则写在 [packages/client/AGENTS.md](../../../../packages/client/AGENTS.md) 新组件清单的第一步,[docs/web-styling.md](../../../../docs/web-styling.zh.md) 指向该目录,让从样式一侧进来的作者也能到达。
-
-## 如何找出重复
-
-按名字搜会漏。`.badge`、`.tag`、`.chip`、`.configTag` 找不到以角色而非外观命名的胶囊——`PluginCard` 的未保存标记叫 `.pending`。几何特征才是可靠探针:搜索同时带有 `border-radius: 999px` 与 `padding: 1px 8px` 的 CSS Modules 规则。这个特征属于 `Tag` 与下面那个有意保留在原包的损坏徽章;未保存标记只保留自己的 `flex: none` 布局类。
-
-
-## 保留在原包的控件
-
-机械搜索会把下面这些控件归到被提升的那三类里。它们留在各自的包中,因为那种归并只是表面相似:
-
-- **`ui-trajectory` 的工具栏开关**带 `role="switch"`,但它是 88px 宽的带标签控件、轨道内联,而且当前渲染为 `hidden`。它和 `Switch` 不是同一个部件。
-- **`ui-plan` 的模式 chip** 与 **`ui-conversation` 的 `ReferenceChip`** 都是可交互的:前者是警告色调的按钮,带 hover、focus、disabled 与关闭操作;后者是带自有截断逻辑的 Lexical 原子节点。两者都不是只读徽章。
-- **`ui-trajectory` 的单元格 tag** 与 **`ui-user-questions` 的推荐徽章**使用各自的几何——一个是表格密度下的 6px 圆角,一个是侧栏强调色上 600 字重的 6px 圆角。把它们塞进胶囊基准,改掉的是有意的设计,不是意外的分歧。
-- **`TerminalBlock` 的退出状态胶囊**保持为静态 `Pill`。它落在 24px 的命令行上、用的是 pill 自己的几何,`Tag` 的 11px 胶囊放不进那一行。只读与可选中的区分是通常的判据,但这一处由尺寸决定,目录里也是这么写的。
-- **`ui-agent-preset` 的损坏徽章**共用胶囊几何,但带着没有第二处使用的实底错误色填充,而且它是自带提示元素的悬停锚点。改用 `Tag` 就得在功能包样式表里保留一份配色覆盖,并依赖跨文件 CSS 顺序来让它生效。
-
-## Alternatives considered
-
-**做一个 `SettingsCard` 原语。** 否决。`.card` 出现在十五个包里,但只有三处带设置页语义,而这三处的差异在行为而不在外观:`ui-settings-plugins` 的 `PluginCard` 暂存编辑,且只在 Host 确认保存后才折叠;`ui-agent-preset` 的卡片可选中;插件清单的卡片是只读的。`PluginCard` 自己的头注释已经记录了它为何不能使用共享折叠行。单个组件将不得不通过 props 接纳全部三种行为,而没有任何一个调用方会用到其中一种以上。
-
-**加门禁,拒绝 `ui-primitives` 之外的 `role="switch"` 或 `.switch` 规则。** 否决。目标是让作者复用已有的东西,不是阻止他们构建。门禁会挂掉一个确实有特殊控件的包——`ui-trajectory` 的带标签工具栏开关正是这种情况——而误拒的代价落在最没法跟它讲道理的作者身上。目录加清单条目针对的是真正的失败原因:作者不知道那个控件存在。
-
-**用额外的 `Tag` props 保留现有的每一种外观。** 否决。插件清单的 5px 圆角与 agent preset 的胶囊是同一类标签的两种形状,两处差异都不是决定出来的。两个都留会把一次意外固化进公开联合,并且让下一个作者去猜该挑哪个。
-
-**扩展 `Pill` 而不是新增 `Tag`。** 否决。`Pill` 高 24px、圆角 12px、字号 12px;徽章基准更密也更圆。合并会产生一个尺寸取决于是否传了 `onClick` 的组件,并抹掉只读与可选中的区分——而目录正需要这个区分来回答"我该用哪个"。
-
-**给 `Tag` 设计 `variant × tone` 双轴 API。** 否决。三个 variant 乘六个 tone 描述十八种组合,其中八种会发布,而且它允许调用方请求没有定义外观的组合。扁平的八成员联合把每个值映射到恰好一种已发布外观。
-
-**让 `pending` 与 `unloading` 不渲染点,而不是新增 `idle`。** 否决。这些阶段需要紧凑的未活动或过渡标记;在一个目的是统一呈现的改动里把它删掉,等于从清单中拿走信息。
-
-**把复用规则写进根 `AGENTS.md`。** 否决。这条规则只管 `packages/client`,而根文件正好处在 `scripts/doc-budgets.manifest.json` 给它的 1950 词上限上,写在那里就必须挤掉一条不相干的仓库级规则。
-
-## Testing
-
-`Tag`、`Switch` 与扩展后的 `StateDot` 在 `packages/client/ui-primitives/tests` 中各有组件测试,处于每文件 100% 覆盖率门禁之内。`StateDot` 的配色通过读取其样式表来钉住:CSS Modules 在组件测试里解析为类名映射,因此某个状态缺了配色规则时会落到继承色上,而任何渲染断言都不会察觉。
-
-迁移后的渲染点保留包级测试,覆盖 role、无障碍名称、状态映射与交互。Web ARIA 快照覆盖装配后的呈现——`Switch` 保留带 `aria-checked` 的 `role="switch"`,而由于 `StateDot` 是 `aria-hidden`,插件清单的相位点把 `role="img"` 名称保留在外层包裹元素上。
-
-组件与样式表测试固定胶囊几何、StateDot 图稿、动画、token 与字重;Web 快照固定无障碍结构。明暗主题下的最终渲染仍由人工视觉检查负责。
-
-## Consequences
-
-- 新的客户端控件现在有一处可查、一处可加,而目录把这次查询变成读一个文件,而不是在四十多个导出上 `grep`。
-- `TagTone` 有八个成员,因为发布了八种外观。加第九个需要一个真的需要它的渲染点,而不是一个对称性论证。
-- 插件清单使用胶囊标签;"已停用"标签使用 `neutral` 色调下可见的 `--dsw-alias-bg-module-platform` 填充。相位标记使用共享 `StateDot`,活动转换采用旋转 loading。插件设置字段中的"未配置"徽章使用基准 500 字重。
-- 插件清单调色板不含 `#b45309` 字面量。`conditional` 标签使用真实的 `--dsw-alias-state-warn-primary` token,而不是不存在的 `--dsw-alias-state-warning-primary` 别名及其硬编码兜底色。
-- 这条规则无法被机械检查。将来的作者仍然可以复制一个控件,只有评审能拦住。这是不设门禁所接受的代价:另一条路会拒绝正当的工作,而上面那份保留在原包的清单就是正当工作确实存在的证据。
-- `Tag` 与 `Switch` 由多个设置和预设包共享;只有现有 prop 无法表达明确差异时,后续消费方才扩展这些原语。
diff --git a/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.i18n.yaml
deleted file mode 100644
index f7b369d628..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.i18n.yaml
+++ /dev/null
@@ -1,43 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-05-sidebar-tab-types-and-navigation.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md
-/agent-note-right-sidebar-tab-types-and-navigation:
- en: 2860d8acc82e4a85
- zh: 7b09ac61a684e36d
-/agent-note-right-sidebar-tab-types-and-navigation/problem:
- en: 1db34c36cf2dc2bd
- zh: 4d4e8d3f09ac18c6
-/agent-note-right-sidebar-tab-types-and-navigation/decision:
- en: 905fd6fbedad540f
- zh: 5420c0dff38012d8
-/agent-note-right-sidebar-tab-types-and-navigation/decision/the-type-registry-ctx-sidebarrighttabs:
- en: 9c0a993b39b2c92f
- zh: 0e0c008e425f47e4
-/agent-note-right-sidebar-tab-types-and-navigation/decision/bodies-and-titles-keyed-slot-seats-under-the-definition-s-id:
- en: 9e80f2d213b394fe
- zh: 79f2e5c504b9863e
-/agent-note-right-sidebar-tab-types-and-navigation/decision/tab-occurrence-information:
- en: 774c3bf80a48ffbe
- zh: c513245789aa8efd
-/agent-note-right-sidebar-tab-types-and-navigation/decision/navigation-ctx-sidebarright:
- en: b32bad4c3b8c2cc8
- zh: 57ba7b8d3451e478
-/agent-note-right-sidebar-tab-types-and-navigation/decision/addresses:
- en: eeb1e7e7478a2f2c
- zh: bddbc5bc78245f63
-/agent-note-right-sidebar-tab-types-and-navigation/decision/entry-points:
- en: 6386bde2654f9a0a
- zh: 700439be6a01e162
-/agent-note-right-sidebar-tab-types-and-navigation/alternatives-considered:
- en: 91fbca006e8de5bc
- zh: f48d3ce1dd7b63bf
-/agent-note-right-sidebar-tab-types-and-navigation/consequences:
- en: 3cdb1159ee55d851
- zh: dcd068005e678a89
-/agent-note-right-sidebar-tab-types-and-navigation/testing:
- en: 49449a6c8465c098
- zh: 1f476cc1226aa9cd
-/agent-note-right-sidebar-tab-types-and-navigation/deferred:
- en: fa27b7684e38329b
- zh: d270e0cdcab73732
diff --git a/.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.i18n.yaml
index 561af11c1a..38d42e7d93 100644
--- a/.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.i18n.yaml
@@ -9,14 +9,14 @@
en: 55d969715a48c9b3
zh: 755dab7523cd01c2
/agent-note-workspace-file-service/decision:
- en: 6ab2a1f635cfc87c
- zh: 369e3ed9b6cdc159
+ en: 17137a4e49e56470
+ zh: ed08cf3d2d7348c8
/agent-note-workspace-file-service/decision/package-topology:
- en: 59c3648960630ff8
- zh: 1891f8659e660e45
+ en: 5a8d81c95c031663
+ zh: 5bdeebe3b5e759c9
/agent-note-workspace-file-service/decision/the-workspacefiles-file-operations:
- en: efdecd9cf80c03e7
- zh: 5ea40e48ab0559a2
+ en: 434a2f6b3fab4756
+ zh: ea83124d00c6a79c
/agent-note-workspace-file-service/decision/paths-on-the-wire:
en: 7b5a96adc08177bc
zh: ae4692e5e62e7de0
@@ -36,11 +36,11 @@
en: 0d42d4716dde706f
zh: 4e95b8af2df232b6
/agent-note-workspace-file-service/decision/related-notes:
- en: 6e8cfa03b5222af6
- zh: efdf8f17757fc378
+ en: ef015f605f7b00ab
+ zh: eee9c79faebf3dff
/agent-note-workspace-file-service/alternatives-considered:
- en: 3d598eb6d03d05e5
- zh: 7dac0b7af471a651
+ en: 8eee658526db4465
+ zh: 4a655accf6a5c08a
/agent-note-workspace-file-service/consequences:
en: 246bb7aed62f6c9b
zh: debf6d4129ee477b
diff --git a/.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.md b/.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.md
index 7deb0dac14..02937e54e2 100644
--- a/.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.md
+++ b/.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.md
@@ -12,11 +12,11 @@ Two constraints frame the service. File reads through `ctx.fs` use the Session's
## Decision
-`packages/api/workspace-files` (`@deepseek-ai/dsh-api-workspace-files`) owns the Host `ctx.workspaceFiles` service, the `workspaceFiles` Remote namespace, and the Client `file` provider that turns `stat` and `changes` into live metadata for the [resource model](2026-09-05-client-resource-model.md); [dual-face packaging](2026-09-07-workspace-files-dual-face-package.md) governs their package organization. File methods resolve relative paths from the workspace root but inherit the Session filesystem backend's read authority; `list` and `changes` remain workspace-scoped. The [workspace file read authority](2026-09-09-workspace-file-read-authority.md) owns this split and its security consequences. Results name files by their absolute path in the filesystem's execution world, and content is bounded by page, byte window, or complete-file cap. The byte window rides on a new `dsh-fs` seam, `FileSystem.readByteRange`, implemented by every provider. The Session Controller carries no workspace-file code.
+`packages/api/workspace-files` (`@deepseek-ai/dsh-api-workspace-files`) owns the Host `ctx.workspaceFiles` service, the `workspaceFiles` Remote namespace, and the Client `file` provider that turns `stat` and `changes` into live metadata for the [resource model](2026-09-05-client-resource-model.md); [dual-face packaging](../../../../packages/api/workspace-files/README.md) governs their package organization. File methods resolve relative paths from the workspace root but inherit the Session filesystem backend's read authority; `list` and `changes` remain workspace-scoped. The [workspace file read authority](2026-09-09-workspace-file-read-authority.md) owns this split and its security consequences. Results name files by their absolute path in the filesystem's execution world, and content is bounded by page, byte window, or complete-file cap. The byte window rides on a new `dsh-fs` seam, `FileSystem.readByteRange`, implemented by every provider. The Session Controller carries no workspace-file code.
### Package topology
-[dual-face packaging](2026-09-07-workspace-files-dual-face-package.md) supersedes this note's choice of separate Host and Client packages; the file service, paging, and change-feed decisions here remain in force. The [workspace file read authority](2026-09-09-workspace-file-read-authority.md) supersedes the original workspace-containment choice for file methods. Host and Client compile in separate leaf configurations, share wire types, and the Client does not import the Host runtime entry.
+[dual-face packaging](../../../../packages/api/workspace-files/README.md) supersedes this note's choice of separate Host and Client packages; the file service, paging, and change-feed decisions here remain in force. The [workspace file read authority](2026-09-09-workspace-file-read-authority.md) supersedes the original workspace-containment choice for file methods. Host and Client compile in separate leaf configurations, share wire types, and the Client does not import the Host runtime entry.
| Face | Package | Files | Depends on |
|---|---|---|---|
@@ -41,7 +41,7 @@ Every Host method takes `WorkspaceFileScope` first. The `workspaceFileScope` Gat
- **`read`** returns one window of lines, `WorkspaceFileText = WorkspaceFileStat & { offset, text, lines, eof }`; `lines` counts the page's lines, so a page holding one empty line (`text: ''`, `lines: 1`) and a page past the end (`lines: 0`) read differently. `range.offset` is the 1-based first line and defaults to 1; `range.limit` is the largest number of lines and defaults to `maxLines`, which it may not exceed. Lines end at `\n` and a final `\n` terminates the last line rather than opening an empty one; `text` joins the page's lines with `\n` and carries no terminator; `eof` is true when the page includes the last line, and an offset past the end returns an empty page with `eof` true. The pager walks `streamText`, counts the lines before the window without keeping them, admits each in-window segment against `maxBytes` before buffering it, and returns at the first character past the window, so a file of any size costs one page of memory. The `version` and `bytes` on a page are the stat's, taken before the stream.
- **`readBytes`** returns native bytes with file metadata, offset, and EOF. The required options object selects a complete read under `maxFileBytes` when `range` is absent, or a bounded window under `maxBytes` when present. `range: {}` uses offset zero and the configured window cap; windows can read files above the complete-file cap. The [binary transfer decision](2026-09-17-workspace-file-binary-transfer.md) owns the Remote response encoding.
- **`baseFile`** selects target resolution independently of `range`: the target `path` must be relative and resolves from the base file's directory. The Host applies regular-file checks to both files and preserves backend read authority. [Document Preview](2026-09-08-document-preview-operations.md) owns content loading and address semantics.
-- **`list`** returns `WorkspaceDirectoryListing { path, entries, truncated }`: the listed directory as a workspace path relative to the root (empty for the root), its direct children in the backend's stable name order as `{ name, type, size? }`, and whether `maxEntries` cut the list. `type` is `file`, `directory`, or `other`; a symlink child reports the type of what it points to and a dangling one is `other`. A child the listing reports as a directory opens through its resolved target, including a Windows junction, and the listing then names that target; [directory link listing](../bug-fix/2026-09-18-windows-directory-junction-listing.md) owns the rule. Dotfiles are listed; nothing is filtered.
+- **`list`** returns `WorkspaceDirectoryListing { path, entries, truncated }`: the listed directory as a workspace path relative to the root (empty for the root), its direct children in the backend's stable name order as `{ name, type, size? }`, and whether `maxEntries` cut the list. `type` is `file`, `directory`, or `other`; a symlink child reports the type of what it points to and a dangling one is `other`. A child the listing reports as a directory opens through its resolved target, including a Windows junction, and the listing then names that target; [directory link listing](../../../../packages/api/workspace-files/README.md) owns the rule. Dotfiles are listed; nothing is filtered.
- **`changes`** yields `WorkspaceFileWatchFrame`: `{ kind: 'ready' }` after the observation queue is registered and the workspace root resolves, followed by `{ kind: 'change', change }`. The `WorkspaceFileChange` payload is `{ absolutePath, version }` for a present file or `{ absolutePath, absent: true }` for one observed gone. Its source is `fs/observed` inside the workspace root, never an OS watcher. Observations after the first pull are queued, including during root resolution; cancellation or plugin disposal ends the generation.
### Paths on the wire
@@ -107,13 +107,13 @@ The Client export registers one `ResourceProvider<'file'>` into `ctx.resources`
### Related notes
-The [resource model](2026-09-05-client-resource-model.md) owns `ctx.resources`, `useResource`, the `dsh-resource:///…` address grammar, and the reasoning for one resource per address; the [text preview and file tree](../feature/2026-09-05-sidebar-text-preview-and-file-tree.md) are the shipped consumers of `read`, `list`, and the `file` provider; the [right Sidebar docking infrastructure](../feature/2026-09-04-right-sidebar-docking-infrastructure.md) is the surface they open into; [workspace file links](../feature/2026-07-31-web-workspace-file-links.md) is where serving files over HTTP was rejected. Anyone extending this system reaches the same five methods through `remote.workspaceFiles` and the same `file` resource through `useResource<'file'>`; the wire types are published as `@deepseek-ai/dsh-api-workspace-files/types`. The [workspace file read authority](2026-09-09-workspace-file-read-authority.md) owns Host read access and the HTML security trade-off; [Document Preview](2026-09-08-document-preview-operations.md) owns content loading and per-tab freshness.
+The [resource model](2026-09-05-client-resource-model.md) owns `ctx.resources`, `useResource`, the `dsh-resource:///…` address grammar, and the reasoning for one resource per address; the [text preview and file tree](../../../../packages/client/ui-sidebar-documentpreview/README.md) are the shipped consumers of `read`, `list`, and the `file` provider; the [right Sidebar docking infrastructure](../feature/2026-09-04-right-sidebar-docking-infrastructure.md) is the surface they open into; [historical workspace file links](../../archived/feature/2026-07-31-web-workspace-file-links.md) is where serving files over HTTP was rejected. Anyone extending this system reaches the same five methods through `remote.workspaceFiles` and the same `file` resource through `useResource<'file'>`; the wire types are published as `@deepseek-ai/dsh-api-workspace-files/types`. The [workspace file read authority](2026-09-09-workspace-file-read-authority.md) owns Host read access and the HTML security trade-off; [Document Preview](2026-09-08-document-preview-operations.md) owns content loading and per-tab freshness.
## Alternatives considered
**Keeping the workspace file endpoint on the Session Controller.** The first form: one `read` under a total byte cap, registered as a sub-plugin of the Session Controller because that is where the wire entry already was. Rejected because a Workspace File service is its own capability — reading and statting files plus listing and observing the workspace — and those queries belong together, while the Session Controller's concern is session lifecycle. The move also let the service grow to five methods without the Controller's file gaining a second purpose.
-**A dual-face package with reverse UI dependencies.** The split-package choice followed two project-reference cycles after `api/remotes` referenced the Client leaf: the resource model imported Remote assembly for result types, and the file provider imported Sidebar UI for its parameter map. TypeScript rejected these cycles with `TS6202`. [dual-face packaging](2026-09-07-workspace-files-dual-face-package.md) supersedes that split: result types come directly from the protocol package, and Sidebar parameter registration belongs to the text preview; both root aggregates retain explicit compiler entries.
+**A dual-face package with reverse UI dependencies.** The split-package choice followed two project-reference cycles after `api/remotes` referenced the Client leaf: the resource model imported Remote assembly for result types, and the file provider imported Sidebar UI for its parameter map. TypeScript rejected these cycles with `TS6202`. [dual-face packaging](../../../../packages/api/workspace-files/README.md) supersedes that split: result types come directly from the protocol package, and Sidebar parameter registration belongs to the text preview; both root aggregates retain explicit compiler entries.
**Serving navigable workspace documents.** Rejected on origin grounds; the [binary transfer](2026-09-17-workspace-file-binary-transfer.md) returns multipart data while HTML remains isolated in a Blob iframe.
diff --git a/.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.zh.md b/.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.zh.md
index fe1dd00000..60b9597225 100644
--- a/.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.zh.md
@@ -12,11 +12,11 @@ Web 客户端需要从一个未必在 Host 机器上的浏览器查看会话工
## Decision
-`packages/api/workspace-files`(`@deepseek-ai/dsh-api-workspace-files`)同时拥有 Host 服务 `ctx.workspaceFiles`、`workspaceFiles` Remote 命名空间,以及将 `stat` 与 `changes` 转成[资源模型](2026-09-05-client-resource-model.zh.md)实时元数据的 Client `file` 提供者;包组织方式由[双面包组织](2026-09-07-workspace-files-dual-face-package.zh.md)规定。文件方法从工作区根解析相对路径,但继承 Session 文件系统后端的读取权限;`list` 与 `changes` 仍限于工作区。[工作区文件读取权限](2026-09-09-workspace-file-read-authority.zh.md)拥有这一分层及其安全后果。结果以文件在文件系统执行环境中的绝对路径命名文件,内容则受页、字节窗口或整文件上限约束。字节窗口依托 `dsh-fs` 新增的 seam `FileSystem.readByteRange`,由每个提供者实现。Session Controller 不再携带任何工作区文件代码。
+`packages/api/workspace-files`(`@deepseek-ai/dsh-api-workspace-files`)同时拥有 Host 服务 `ctx.workspaceFiles`、`workspaceFiles` Remote 命名空间,以及将 `stat` 与 `changes` 转成[资源模型](2026-09-05-client-resource-model.zh.md)实时元数据的 Client `file` 提供者;包组织方式由[双面包组织](../../../../packages/api/workspace-files/README.zh.md)规定。文件方法从工作区根解析相对路径,但继承 Session 文件系统后端的读取权限;`list` 与 `changes` 仍限于工作区。[工作区文件读取权限](2026-09-09-workspace-file-read-authority.zh.md)拥有这一分层及其安全后果。结果以文件在文件系统执行环境中的绝对路径命名文件,内容则受页、字节窗口或整文件上限约束。字节窗口依托 `dsh-fs` 新增的 seam `FileSystem.readByteRange`,由每个提供者实现。Session Controller 不再携带任何工作区文件代码。
### 包拓扑
-[双面包组织](2026-09-07-workspace-files-dual-face-package.zh.md)取代本记录中把 Host 与 Client 分成两个包的组织选择;这里的文件服务、分页和变更流约定保持不变。[工作区文件读取权限](2026-09-09-workspace-file-read-authority.zh.md)取代文件方法原有的工作区包含选择。Host 与 Client 分别编译在两个叶配置中,共享线路类型,Client 不导入 Host 运行时入口。
+[双面包组织](../../../../packages/api/workspace-files/README.zh.md)取代本记录中把 Host 与 Client 分成两个包的组织选择;这里的文件服务、分页和变更流约定保持不变。[工作区文件读取权限](2026-09-09-workspace-file-read-authority.zh.md)取代文件方法原有的工作区包含选择。Host 与 Client 分别编译在两个叶配置中,共享线路类型,Client 不导入 Host 运行时入口。
| 面 | 包 | 文件 | 依赖 |
|---|---|---|---|
@@ -41,7 +41,7 @@ Web 客户端需要从一个未必在 Host 机器上的浏览器查看会话工
- **`read`** 返回一个行窗口 `WorkspaceFileText = WorkspaceFileStat & { offset, text, lines, eof }`;`lines` 计页内行数,使只含一个空行的页(`text: ''`、`lines: 1`)与越过文件末尾的页(`lines: 0`)可区分。`range.offset` 是 1 起算的首行,缺省 1;`range.limit` 是最多行数,缺省 `maxLines` 且不得超过。行以 `\n` 结束,末尾的 `\n` 终止最后一行而不是开启一空行;`text` 以 `\n` 连接本页各行且不带终止符;页含最后一行时 `eof` 为 true,越过末尾的 offset 返回 `eof` 为 true 的空页。切页器沿 `streamText` 前进,数过窗口前的行而不保留,把每个窗内片段先按 `maxBytes` 核准再缓冲,并在越过窗口的第一个字符处返回,因此任意大小的文件只花一页内存。页上的 `version` 与 `bytes` 来自流之前的那次 stat。
- **`readBytes`** 返回原生字节、文件元数据、偏移量和 EOF。必填选项对象未传 `range` 时,在 `maxFileBytes` 上限内完整读取;传入时,在 `maxBytes` 上限内读取有界窗口。`range: {}` 使用零偏移和配置的窗口上限;窗口可读取超过完整文件上限的文件。[二进制传输决议](2026-09-17-workspace-file-binary-transfer.zh.md)负责 Remote 响应编码。
- **`baseFile`** 独立于 `range` 选择目标解析方式:目标 `path` 必须为相对路径,从基准文件所在目录解析。Host 对两个文件执行普通文件检查,并保留后端读取权限。[Document Preview](2026-09-08-document-preview-operations.zh.md)负责内容加载与地址语义。
-- **`list`** 返回 `WorkspaceDirectoryListing { path, entries, truncated }`:被列目录相对根的工作区路径(根为空串)、其直接子项按后端的稳定名序以 `{ name, type, size? }` 给出,以及 `maxEntries` 是否截断了列表。`type` 为 `file`、`directory` 或 `other`;符号链接子项报告其指向目标的类型,悬空者为 `other`。列表报告为目录的子项经其解析目标打开,Windows 目录联接亦然,此时列表以该目标命名;[目录链接列举](../bug-fix/2026-09-18-windows-directory-junction-listing.zh.md)拥有该规则。dotfile 照常列出,不做任何过滤。
+- **`list`** 返回 `WorkspaceDirectoryListing { path, entries, truncated }`:被列目录相对根的工作区路径(根为空串)、其直接子项按后端的稳定名序以 `{ name, type, size? }` 给出,以及 `maxEntries` 是否截断了列表。`type` 为 `file`、`directory` 或 `other`;符号链接子项报告其指向目标的类型,悬空者为 `other`。列表报告为目录的子项经其解析目标打开,Windows 目录联接亦然,此时列表以该目标命名;[目录链接列举](../../../../packages/api/workspace-files/README.zh.md)拥有该规则。dotfile 照常列出,不做任何过滤。
- **`changes`** 产出 `WorkspaceFileWatchFrame`:在观察队列注册且工作区根解析完成后先发 `{ kind: 'ready' }`,随后为 `{ kind: 'change', change }`。载荷 `WorkspaceFileChange` 对存在的文件为 `{ absolutePath, version }`,对消失的文件为 `{ absolutePath, absent: true }`。来源是工作区根内的 `fs/observed`,不监视操作系统。首次拉取后的观察都会排队,包括根解析期间的观察;取消或插件释放会结束该代流。
### 线路上的路径
@@ -107,13 +107,13 @@ Client 导出向 `ctx.resources` 注册一个 `ResourceProvider<'file'>`,存
### 相关记录
-[资源模型](2026-09-05-client-resource-model.zh.md)拥有 `ctx.resources`、`useResource`、`dsh-resource:///…` 地址语法以及"每个地址一份资源"的推理;[文本预览与文件树](../feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md)是 `read`、`list` 与 `file` 提供者随包交付的消费方;[右侧 Sidebar 停靠基础设施](../feature/2026-09-04-right-sidebar-docking-infrastructure.zh.md)是它们打开进去的界面;[工作区文件链接](../feature/2026-07-31-web-workspace-file-links.zh.md)是经 HTTP 供文件被否决之处。任何在这套体系上扩展的人都经 `remote.workspaceFiles` 触达同样的五个方法、经 `useResource<'file'>` 触达同样的 `file` 资源;线路类型以 `@deepseek-ai/dsh-api-workspace-files/types` 发布。[工作区文件读取权限](2026-09-09-workspace-file-read-authority.zh.md)负责 Host 读取权限与 HTML 安全取舍;[Document Preview](2026-09-08-document-preview-operations.zh.md)负责内容加载和逐 tab 新鲜度。
+[资源模型](2026-09-05-client-resource-model.zh.md)拥有 `ctx.resources`、`useResource`、`dsh-resource:///…` 地址语法以及"每个地址一份资源"的推理;[文本预览与文件树](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md)是 `read`、`list` 与 `file` 提供者随包交付的消费方;[右侧 Sidebar 停靠基础设施](../feature/2026-09-04-right-sidebar-docking-infrastructure.zh.md)是它们打开进去的界面;[历史工作区文件链接](../../archived/feature/2026-07-31-web-workspace-file-links.md)是经 HTTP 供文件被否决之处。任何在这套体系上扩展的人都经 `remote.workspaceFiles` 触达同样的五个方法、经 `useResource<'file'>` 触达同样的 `file` 资源;线路类型以 `@deepseek-ai/dsh-api-workspace-files/types` 发布。[工作区文件读取权限](2026-09-09-workspace-file-read-authority.zh.md)负责 Host 读取权限与 HTML 安全取舍;[Document Preview](2026-09-08-document-preview-operations.zh.md)负责内容加载和逐 tab 新鲜度。
## Alternatives considered
**把工作区文件端点留在 Session Controller 上。** 最初形态:总字节上限之下的一个 `read`,作为 Session Controller 的子插件注册,因为线路入口本来就在那里。被否,因为 Workspace File 服务是自己的能力——读取和 stat 文件,并列举和观察工作区——这些查询应归于一处,而 Session Controller 关心的是会话生命周期。搬出也让服务长到五个方法而不给 Controller 的文件添第二重目的。
-**带有反向 UI 依赖的双面包。** 拆包选择源于 `api/remotes` 引用 Client 叶子后形成的两条工程引用环:资源模型为了结果类型引用 Remote 装配,文件提供者为了 Sidebar 参数表引用右栏 UI。TypeScript 以 `TS6202` 拒绝这些环。[双面包组织](2026-09-07-workspace-files-dual-face-package.zh.md)取代拆包选择:结果类型直接取自协议包,Sidebar 参数注册移至文本预览;保留两个根聚合中的显式编译入口。
+**带有反向 UI 依赖的双面包。** 拆包选择源于 `api/remotes` 引用 Client 叶子后形成的两条工程引用环:资源模型为了结果类型引用 Remote 装配,文件提供者为了 Sidebar 参数表引用右栏 UI。TypeScript 以 `TS6202` 拒绝这些环。[双面包组织](../../../../packages/api/workspace-files/README.zh.md)取代拆包选择:结果类型直接取自协议包,Sidebar 参数注册移至文本预览;保留两个根聚合中的显式编译入口。
**提供可导航的工作区文档。** 因源隔离问题不采用;[二进制传输](2026-09-17-workspace-file-binary-transfer.zh.md)返回 multipart 数据,HTML 仍隔离在 Blob iframe 中。
diff --git a/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.i18n.yaml
deleted file mode 100644
index f95de9ad8f..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.i18n.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-06-embedded-stream-record-readers.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.md
-/agent-note-embedded-assistant-stream-consumers-read-compact-records:
- en: ddc3fed8aa76c1b8
- zh: 409dcb67ef28080e
-/agent-note-embedded-assistant-stream-consumers-read-compact-records/problem:
- en: 34ceceb99e246ae6
- zh: 0a18314b78557205
-/agent-note-embedded-assistant-stream-consumers-read-compact-records/decision:
- en: ad50509c41a7dacf
- zh: c8170641203a3184
-/agent-note-embedded-assistant-stream-consumers-read-compact-records/decision/measurements:
- en: 2eedf644fa07e35a
- zh: cb691c75fbeda518
-/agent-note-embedded-assistant-stream-consumers-read-compact-records/alternatives-considered:
- en: 66b6007d582f7521
- zh: a33aa19f7c912fa2
-/agent-note-embedded-assistant-stream-consumers-read-compact-records/consequences:
- en: 05cf037a97b4254d
- zh: 1a45fae2447678c2
diff --git a/.agents/notes/implemented/architecture/2026-09-07-sidebar-responsive-tab-info.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-07-sidebar-responsive-tab-info.i18n.yaml
deleted file mode 100644
index dd4a23fe04..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-07-sidebar-responsive-tab-info.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-07-sidebar-responsive-tab-info.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-07-sidebar-responsive-tab-info.md
-/agent-note-responsive-sidebar-and-injected-tab-information:
- en: 63ced560202cf1a3
- zh: 1ec49cd5ae68f909
-/agent-note-responsive-sidebar-and-injected-tab-information/problem:
- en: f9824b1d6f93d329
- zh: 36bc474f646a1a25
-/agent-note-responsive-sidebar-and-injected-tab-information/decision:
- en: 1a055f79cb0afcde
- zh: 78467751791d42d1
-/agent-note-responsive-sidebar-and-injected-tab-information/alternatives-considered:
- en: c05a3ca9a201f01a
- zh: b1b30e134a92c8ed
-/agent-note-responsive-sidebar-and-injected-tab-information/consequences:
- en: 852e827be572c626
- zh: 125ac90be15a5187
diff --git a/.agents/notes/implemented/architecture/2026-09-07-workspace-files-dual-face-package.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-07-workspace-files-dual-face-package.i18n.yaml
deleted file mode 100644
index 2d222100c4..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-07-workspace-files-dual-face-package.i18n.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-07-workspace-files-dual-face-package.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-07-workspace-files-dual-face-package.md
-/agent-note-workspace-files-as-one-dual-face-api-package:
- en: fee20bed1c4f69d2
- zh: 8897bf7430250d46
-/agent-note-workspace-files-as-one-dual-face-api-package/problem:
- en: 057ac188f6269740
- zh: 4ed1c2f6c3015efb
-/agent-note-workspace-files-as-one-dual-face-api-package/decision:
- en: 36fdbb0b44b1bb5e
- zh: 87a7a9b45231cf28
-/agent-note-workspace-files-as-one-dual-face-api-package/alternatives-considered:
- en: 45a93fced31f5980
- zh: 9283fbda48828416
-/agent-note-workspace-files-as-one-dual-face-api-package/consequences:
- en: d4558eea4ac3a00b
- zh: e625747fd469f146
-/agent-note-workspace-files-as-one-dual-face-api-package/verification:
- en: 4c49a2ec778623e8
- zh: 94eacb1ee8e4d36d
diff --git a/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.i18n.yaml
index 6e302c76a3..40b064454f 100644
--- a/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.i18n.yaml
@@ -3,8 +3,8 @@
# 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-desktop-bundled-runtime-and-external-plugins.md
/agent-note-bundle-the-desktop-runtime-and-retain-external-plugins:
- en: f4c2a14c7f0dbd02
- zh: 5c9b9b68885f3ee0
+ en: 6a87387a952b6d28
+ zh: 5fd41d2dd5545637
/agent-note-bundle-the-desktop-runtime-and-retain-external-plugins/problem:
en: fe97fe6ba33cb23a
zh: c83a1f023c4e352d
@@ -12,14 +12,14 @@
en: e01df5ba880c2972
zh: e62074a6a9ba5685
/agent-note-bundle-the-desktop-runtime-and-retain-external-plugins/package-ownership:
- en: 37cae6b28fd359ef
- zh: 63118e361fa8f84c
+ en: f683605bf4655db8
+ zh: de09d610ee1d4193
/agent-note-bundle-the-desktop-runtime-and-retain-external-plugins/transactions-and-upgrades:
- en: 5bdc450be55832fb
- zh: 3312fc9e5497e139
+ en: 53d48f71fcc1c52e
+ zh: 7d61923484dbec1c
/agent-note-bundle-the-desktop-runtime-and-retain-external-plugins/alternatives-considered:
- en: 1558bba473382cbb
- zh: b6a6467866ee254d
+ en: 67b3ba2b37e92f88
+ zh: 785083e5de098af8
/agent-note-bundle-the-desktop-runtime-and-retain-external-plugins/consequences:
en: 6dfa581ec6879c5b
zh: f6c735ad6a00b2b3
diff --git a/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.md b/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.md
index 61fa9eafb7..a1aeb68f2d 100644
--- a/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.md
+++ b/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.md
@@ -6,7 +6,7 @@ English | [中文](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md
Plugin management and native recovery follow the [shared Web wrapper decision](2026-09-10-desktop-web-wrapper.md).
-The [Electron runtime decision](2026-09-11-desktop-electron-node-runtime.md) supersedes the separate upstream Node executable; other decisions in this note remain applicable.
+Desktop uses Electron as its Node runtime under the [current runtime reference](../../../../apps/desktop/README.md); other decisions in this note remain applicable.
## Problem
@@ -26,7 +26,7 @@ The resource descriptor records the exact release, Node version, platform, archi
The [Desktop file policy](../../../../apps/desktop/scripts/runtime-file-policy.ts) applies after production npm installation and before native signing or descriptor generation. npm publication lists serve library consumers and can include declarations, maps, tests, and native build inputs; they do not identify the files needed by the Desktop process. The Desktop copy omits declarations and recognized source maps because Host execution uses JavaScript and generated Typert artifacts. The Host inherits the user environment. Published npm packages and external plugin directories retain their own files. Source debugger navigation is a development-package capability.
-Package-specific exclusions remove Domino tests, fs-ext compilation outputs, Koffi's Windows import library, and non-target node-pty prebuilds and debug symbols. The policy retains native executable dependencies, node-pty's ConPTY source distribution, licenses, and unrecognized assets; broad `src`, `test`, `.ts`, or `.map` exclusions could remove executable code or runtime data. Copy tests preserve sentinel assets and seal the filtered inventory; the Electron [payload smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs) verifies PTY output, native file seeking, FFI, image conversion, and HTML parsing. Runtime preparation still verifies every retained byte and boots the complete Host with an external plugin.
+Package-specific exclusions remove Domino tests, fs-ext compilation outputs, Koffi's Windows import library, and non-target node-pty prebuilds and debug symbols. The policy retains native executable dependencies, licenses, and unrecognized assets; node-pty's ConPTY source distribution is retained only for Windows targets; broad `src`, `test`, `.ts`, or `.map` exclusions could remove executable code or runtime data. Copy tests preserve sentinel assets and seal the filtered inventory; the Electron [payload smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs) verifies PTY output, native file seeking, FFI, image conversion, and HTML parsing. Runtime preparation still verifies every retained byte and boots the complete Host with an external plugin.
The shared profile runner supplies missing installation and selected-bundle dependencies through the runtime resolution. pnpm-installed packages take precedence. Runtime resolution delegates to the selected package paths, so Host and plugin imports reaching the same export share its module instance. Distinct ESM and CommonJS conditional exports remain distinct entry points; runtime resolution does not merge a package’s dual implementations.
@@ -44,11 +44,11 @@ The shared [plugin manager](../../../../packages/boot/plugin-manager/README.md)
The shared Web plugin manager owns package mutations and activation; Electron retains profile preparation and native recovery. The [Web wrapper decision](2026-09-10-desktop-web-wrapper.md) owns these responsibilities.
-The [immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-start.md) owns direct Host startup. The native recovery dialog can disable third-party bundles and back up the profile patch when the Web application cannot start. Installed plugin files remain available for repair.
+The [immediate-window reference](../../../../apps/desktop/README.md) owns direct Host startup. The native recovery dialog can disable third-party bundles and back up the profile patch when the Web application cannot start. Installed plugin files remain available for repair.
## Alternatives considered
-Full runtime verification belongs to packaging. Startup reads the resource descriptor and checks shared package records and required Host entries. The [release-validation decision](2026-09-09-desktop-build-release-validation.md) assigns release and target compatibility checks to packaging. Startup neither enumerates nor hashes installed runtime files. Reading every file before backend loading adds I/O proportional to distribution size; unusable modules instead fail when loaded. Build-time verification rejects changed, missing, extra, or linked files against the recorded inventory.
+Full runtime verification belongs to packaging. Startup reads the resource descriptor and checks shared package records and required Host entries. The [release-validation reference](../../../../apps/desktop/packaging/README.md#desktop-runtime-preparation) assigns release and target compatibility checks to packaging. Startup neither enumerates nor hashes installed runtime files. Reading every file before backend loading adds I/O proportional to distribution size; unusable modules instead fail when loaded. Build-time verification rejects changed, missing, extra, or linked files against the recorded inventory.
- **Install the bundled offline seed at startup.** This preserves an ordinary pnpm installation procedure but repeats core extraction and installation on every affected machine. Materialized resources remove that work at the cost of more application files and release-builder responsibility.
- **Force host dependency versions into plugins.** This unnecessarily couples ordinary plugin dependencies to the host. The runtime resolution supplies missing packages while pnpm-owned entries retain independent versions.
diff --git a/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md b/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md
index 8d94d58235..73728d1feb 100644
--- a/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md
@@ -6,7 +6,7 @@ Status: implemented
插件管理和原生恢复遵循[共享 Web 薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)。
-[Electron 运行时决策](2026-09-11-desktop-electron-node-runtime.zh.md)替代独立上游 Node 可执行文件的选择;本文其他决策仍然适用。
+Desktop 按[当前运行时参考](../../../../apps/desktop/README.zh.md)使用 Electron 作为 Node 运行时;本文其他决策仍然适用。
## 问题
@@ -26,7 +26,7 @@ Desktop 初始化时安装核心依赖图,会重复发布构建器已经完成
[桌面文件规则](../../../../apps/desktop/scripts/runtime-file-policy.ts)在生产 npm 依赖安装之后、原生签名或描述文件生成之前执行。npm 发布列表服务于库的使用者,可以包含声明、map、测试和原生构建输入,不能直接表示桌面进程需要哪些文件。桌面副本排除声明和已识别的 source map,因为 Host 执行 JavaScript 和生成的 Typert 产物。Host 继承用户环境。已发布的 npm 包和外部插件目录保留各自的文件。源码调试导航由开发包提供。
-包专用排除项包括 Domino 测试、fs-ext 编译产物、Koffi 的 Windows 导入库,以及非目标平台的 node-pty 预构建文件和调试符号。规则保留原生可执行依赖、node-pty 的 ConPTY 源分发内容、许可证和未知资源;宽泛排除 `src`、`test`、`.ts` 或 `.map` 可能移除可执行代码或运行时数据。复制测试保留哨兵资源并封存过滤后的清单;Electron 的[产物 smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs)验证 PTY 输出、原生文件定位、FFI、图像转换和 HTML 解析。运行时准备仍会验证每个保留字节,并携带外部插件启动完整 Host。
+包专用排除项包括 Domino 测试、fs-ext 编译产物、Koffi 的 Windows 导入库,以及非目标平台的 node-pty 预构建文件和调试符号。规则保留原生可执行依赖、许可证和未知资源;node-pty 的 ConPTY 源分发内容仅在 Windows 目标中保留;宽泛排除 `src`、`test`、`.ts` 或 `.map` 可能移除可执行代码或运行时数据。复制测试保留哨兵资源并封存过滤后的清单;Electron 的[产物 smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs)验证 PTY 输出、原生文件定位、FFI、图像转换和 HTML 解析。运行时准备仍会验证每个保留字节,并携带外部插件启动完整 Host。
共享 profile runner 通过 runtime resolution 补全安装包与选中 bundle 缺失的依赖。pnpm 安装的包优先。运行时解析委托给所选包路径,因此 Host 与插件导入同一导出时共享其模块实例。不同的 ESM 与 CommonJS 条件导出仍是不同入口;运行时解析不会合并包的双重实现。
@@ -44,11 +44,11 @@ profile manifest 分别记录 pnpm 安装的依赖及已启用 bundle 列表。
共享 Web 插件管理器负责包变更和激活;Electron 保留 profile 准备和原生恢复。[Web 薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)负责这些职责。
-[立即显示窗口决策](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)负责直接启动 Host。Web 应用无法启动时,原生恢复对话框可禁用第三方 bundle 并备份 profile patch。已安装插件文件保留以供修复。
+[立即显示窗口参考](../../../../apps/desktop/README.zh.md)负责直接启动 Host。Web 应用无法启动时,原生恢复对话框可禁用第三方 bundle 并备份 profile patch。已安装插件文件保留以供修复。
## 考虑过的替代方案
-完整运行时验证属于打包流程。启动读取资源描述文件,并检查共享包记录及必要的 Host 入口。[发布验证决策](2026-09-09-desktop-build-release-validation.zh.md)将发布与目标兼容性检查交给打包流程。启动既不枚举已安装运行时文件,也不计算其哈希。后端加载前读取每个文件会增加与分发体积成正比的 I/O;不可用模块改由加载时失败暴露。构建时验证按记录清单拒绝内容变化、缺失、多余或链接文件。
+完整运行时验证属于打包流程。启动读取资源描述文件,并检查共享包记录及必要的 Host 入口。[发布验证参考](../../../../apps/desktop/packaging/README.zh.md#desktop-runtime-preparation)将发布与目标兼容性检查交给打包流程。启动既不枚举已安装运行时文件,也不计算其哈希。后端加载前读取每个文件会增加与分发体积成正比的 I/O;不可用模块改由加载时失败暴露。构建时验证按记录清单拒绝内容变化、缺失、多余或链接文件。
- **启动时安装内置离线 seed。** 这保留普通 pnpm 安装流程,但会在每台受影响机器上重复核心解压与安装。物化资源消除了这部分工作,代价是更多应用文件和发布构建器责任。
- **强制插件使用 Host 依赖版本。** 这会让普通插件依赖不必要地耦合于 Host。runtime resolution 提供缺失的包,pnpm 拥有的条目保留独立版本。
diff --git a/.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.i18n.yaml
index 9de126a4e4..98414d1dd2 100644
--- a/.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.i18n.yaml
@@ -9,8 +9,8 @@
en: 8578b584bda3abdc
zh: 85dcf44aab2cfd51
/agent-note-desktop-welcome-window-material/decision:
- en: df7f9dc0c48769ab
- zh: 5e667c736dd58a1c
+ en: 1affa7f1fccc945f
+ zh: e5aea959e7886a59
/agent-note-desktop-welcome-window-material/alternatives-considered:
en: cf42e200b1d0f00a
zh: de96b5647f5ffb23
diff --git a/.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.md b/.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.md
index e3961481f8..e285a57699 100644
--- a/.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.md
+++ b/.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.md
@@ -12,7 +12,7 @@ The desktop welcome design blurs the desktop behind its entire window. Chromium
The [Electron welcome window](../../../../apps/desktop/src/welcome-window.ts) owns native material and window controls. Its independently bundled React renderer supplies the entry, account sign-in states and API-key form. It shares the `StateDot` loading component with the Web UI without booting the Web plugin graph. The Desktop build emits local JavaScript and CSS under `lib/welcome`, included in packaged applications; the document keeps its network-denying content security policy. A narrow preload supplies typed shell copy, a write-only key operation, and a skip operation; IPC rejects other windows and subframes. Each sandboxed preload is bundled independently because Electron’s restricted require cannot load sibling chunks. The Electron main process authenticates through the shared Web launch URL and resolves the official provider's reference through the existing settings and credential RPC methods. The [thin Web wrapper](2026-09-10-desktop-web-wrapper.md) owns the HTTP server; onboarding adds no Host endpoint or child IPC operation. Responses contain metadata or a safe outcome, never a key or private provider diagnostics.
-Cold startup opens the entry when neither an account credential nor a model API key is configured. Saving enters the workspace after persistence succeeds; skipping enters without saving a draft or a credential-entry completion flag. The next process launch checks credentials again. The Desktop preload marker suppresses the automatic Web credential step and welcome notice; settings and explicit API-key editing remain available. The [Desktop onboarding decision](../feature/2026-09-16-desktop-onboarding.md) owns the separate device-local introduction after account login. Other native shells can disable only the credential step through the Models Host plugin's `credentialOnboarding` page-injection value. The module graph carries package identities rather than arbitrary Host config, so a Host row alone does not configure its Client half. The [account provider](2026-09-14-deepseek-account-login.md) supplies login and sign-out state while preserving independently stored API keys.
+Cold startup opens the entry when neither an account credential nor a model API key is configured. Saving enters the workspace after persistence succeeds; skipping enters without saving a draft or a credential-entry completion flag. The next process launch checks credentials again. The Desktop preload marker suppresses the automatic Web credential step and welcome notice; settings and explicit API-key editing remain available. The [Desktop onboarding reference](../../../../packages/client/ui-settings-account/README.md) owns the separate device-local introduction after account login. Other native shells can disable only the credential step through the Models Host plugin's `credentialOnboarding` page-injection value. The module graph carries package identities rather than arbitrary Host config, so a Host row alone does not configure its Client half. The [account provider](2026-09-14-deepseek-account-login.md) supplies login and sign-out state while preserving independently stored API keys.
The generated project follows declared workspace dependencies because pnpm’s hoist index alone can omit configured plugins.
diff --git a/.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.zh.md b/.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.zh.md
index 17504368e8..e708b53447 100644
--- a/.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.zh.md
@@ -12,7 +12,7 @@ Status: implemented
[Electron 欢迎窗口](../../../../apps/desktop/src/welcome-window.ts)负责原生材质和窗口控件。独立打包的 React 渲染器提供入口、账号登录状态和 API Key 表单。它与 Web UI 共用 `StateDot` 加载组件,不启动 Web 插件图。Desktop 构建将本地 JavaScript 和 CSS 输出到 `lib/welcome`,并纳入应用安装包;文档继续使用禁止网络访问的内容安全策略。窄接口 preload 提供强类型桌面文案、仅写密钥操作和跳过操作;IPC 拒绝其他窗口和子 frame。每个沙箱 preload 都独立打包,因为 Electron 受限的 require 无法加载同目录中的拆分模块。Electron 主进程通过共享 Web 启动 URL 认证,并通过现有设置和凭证 RPC 方法解析官方提供方的引用。[Web 薄壳](2026-09-10-desktop-web-wrapper.zh.md)负责 HTTP 服务;引导不增加 Host 端点或子进程 IPC 操作。响应只包含元数据或安全结果,不包含密钥或提供方的私有诊断。
-冷启动在账号凭证与模型 API Key 均未配置时打开入口。保存会在持久化成功后进入工作区;跳过会直接进入,不保存草稿或凭证入口完成标记。下次进程启动时会重新检查凭证。Desktop preload 标记抑制自动 Web 凭证步骤和欢迎须知;设置与显式 API Key 编辑仍然可用。[Desktop 引导决策](../feature/2026-09-16-desktop-onboarding.zh.md)负责账号登录后独立的设备本地介绍流程。其他原生壳可以通过 Models Host 插件注入页面的 `credentialOnboarding` 值仅禁用凭证步骤。模块图传递的是包标识,而不是任意 Host 配置,因此单独配置 Host 行不会配置其 Client 半部。[账号提供方](2026-09-14-deepseek-account-login.zh.md)提供登录与退出登录状态,同时保留独立存储的 API Key。
+冷启动在账号凭证与模型 API Key 均未配置时打开入口。保存会在持久化成功后进入工作区;跳过会直接进入,不保存草稿或凭证入口完成标记。下次进程启动时会重新检查凭证。Desktop preload 标记抑制自动 Web 凭证步骤和欢迎须知;设置与显式 API Key 编辑仍然可用。[Desktop 引导参考](../../../../packages/client/ui-settings-account/README.zh.md)负责账号登录后独立的设备本地介绍流程。其他原生壳可以通过 Models Host 插件注入页面的 `credentialOnboarding` 值仅禁用凭证步骤。模块图传递的是包标识,而不是任意 Host 配置,因此单独配置 Host 行不会配置其 Client 半部。[账号提供方](2026-09-14-deepseek-account-login.zh.md)提供登录与退出登录状态,同时保留独立存储的 API Key。
生成的开发项目沿已声明的 workspace 依赖建立链接,因为单独依赖 pnpm 的提升索引会漏掉配置插件。
diff --git a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml
index ec7374517f..064ed4f762 100644
--- a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml
@@ -9,8 +9,8 @@
en: a35075e6babe3f7a
zh: 3a64ac57761e1009
/agent-note-document-preview-and-file-addresses/decision:
- en: d0a2923efefbcfe1
- zh: 34f71d092c0e706b
+ en: 75b40970e401f2e3
+ zh: 970b2cc044ea17f4
/agent-note-document-preview-and-file-addresses/alternatives-considered:
en: 8e9616e954df488c
zh: 10801341ec53bbe8
diff --git a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md
index 220db6db04..55845a7718 100644
--- a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md
+++ b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md
@@ -16,9 +16,9 @@ Document Preview separates resource observation from content reads. The [resourc
Readable files use `dsh-resource://file/session//`. The path may be workspace-relative or absolute; an encoded absolute path retains its leading slash. `fileAddressFor` always emits this Session-address form. The provider and Preview RPC take the Session only from that address, never from the current selection, first holder, or owning tab. A Session-less `absolute` URI cannot be read; the provider reports `workspace-file/unknown-workspace`. Session authorization is a file-protocol rule, not an additional Resource identity.
-[Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.md) owns format selection and loading policy. Metadata registers with `ctx.documentPreviews`; components register separately into the keyed `sidebar.right.tab.document` Slot. Extension registrations precede builtins, then longer suffixes and registration order decide. The toolbar lists supported alternatives and remembers a manual choice per tab. Plain text remains available for unknown extensions and text-compatible renderers. Registered binary suffixes determine text compatibility independently of loading mode; HTML and SVG remain outside those suffixes and retain source viewing. A single candidate renders no viewer control. Registered binary suffixes omit plain text; known binary suffixes without a renderer show an unsupported state without reading the file ([sidebar preview polish](../feature/2026-09-11-sidebar-document-preview-polish.md)). The child receives accumulated text or complete native bytes, the original resource address, and the standard `useResource` and `useTabInfo` hooks. Preview calls `ctx.remote.workspaceFiles.read` for text and `ctx.remote.workspaceFiles.readBytes` for native bytes, with `baseFile` for related resources. Refresh remains per tab, with no resource reload, shared `changed` acknowledgement, extra resource wrapper, or content Session.
+[Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.md) owns format selection and loading policy. Metadata registers with `ctx.documentPreviews`; components register separately into the keyed `sidebar.right.tab.document` Slot. Extension registrations precede builtins, then longer suffixes and registration order decide. The toolbar lists supported alternatives and remembers a manual choice per tab. Plain text remains available for unknown extensions and text-compatible renderers. Registered binary suffixes determine text compatibility independently of loading mode; HTML and SVG remain outside those suffixes and retain source viewing. A single candidate renders no viewer control. Registered binary suffixes omit plain text; known binary suffixes without a renderer show an unsupported state without reading the file ([sidebar preview polish](../../../../packages/client/ui-sidebar-documentpreview/README.md)). The child receives accumulated text or complete native bytes, the original resource address, and the standard `useResource` and `useTabInfo` hooks. Preview calls `ctx.remote.workspaceFiles.read` for text and `ctx.remote.workspaceFiles.readBytes` for native bytes, with `baseFile` for related resources. Refresh remains per tab, with no resource reload, shared `changed` acknowledgement, extra resource wrapper, or content Session.
-Markdown and code reuse the incremental primitives with cumulative paged text. HTML, PDF, and images read complete `Uint8Array` data; Host transport uses the [binary workspace file transfer](2026-09-17-workspace-file-binary-transfer.md). Published buffers are borrowed read-only and never persist into layout or Session JSON. PDF.js runs in an owned Worker with version-matched bundled font and decoder data, and copies input before transfer to preserve Preview's retained buffer. With [Developer tools](../feature/2026-09-17-developer-tools-settings.md) enabled, HTML runs in a Blob iframe with `sandbox="allow-scripts"`, without same-origin, popup, form, download, or top-navigation privileges. The browser retains its normal external-network rules. Bounded static local JS/CSS reads stay in the parent; the opaque frame creates its own asset Blobs, because it cannot load parent-origin Blobs. PNG, JPEG, GIF, WebP, BMP, ICO, and SVG use image-specific Blob URLs in an `
` static-image context. An image wider than the pane scales down to its width at its aspect ratio; a smaller image keeps its intrinsic CSS-pixel dimensions centred by auto margins, and a taller image extends the shared scroller's vertical range ([sidebar preview polish](../feature/2026-09-11-sidebar-document-preview-polish.md)). The renderer provides no zoom or drag-to-pan. SVG markup never enters the application DOM or an iframe, so scripts remain inert and cannot reach the parent page. Replacing HTML or an image revokes its root Blob URL.
+Markdown and code reuse the incremental primitives with cumulative paged text. HTML, PDF, and images read complete `Uint8Array` data; Host transport uses the [binary workspace file transfer](2026-09-17-workspace-file-binary-transfer.md). Published buffers are borrowed read-only and never persist into layout or Session JSON. PDF.js runs in an owned Worker with version-matched bundled font and decoder data, and copies input before transfer to preserve Preview's retained buffer. With [Developer tools](../../../../packages/client/ui-sidebar-documentpreview/README.md) enabled, HTML runs in a Blob iframe with `sandbox="allow-scripts"`, without same-origin, popup, form, download, or top-navigation privileges. The browser retains its normal external-network rules. Bounded static local JS/CSS reads stay in the parent; the opaque frame creates its own asset Blobs, because it cannot load parent-origin Blobs. PNG, JPEG, GIF, WebP, BMP, ICO, and SVG use image-specific Blob URLs in an `
` static-image context. An image wider than the pane scales down to its width at its aspect ratio; a smaller image keeps its intrinsic CSS-pixel dimensions centred by auto margins, and a taller image extends the shared scroller's vertical range ([sidebar preview polish](../../../../packages/client/ui-sidebar-documentpreview/README.md)). The renderer provides no zoom or drag-to-pan. SVG markup never enters the application DOM or an iframe, so scripts remain inert and cannot reach the parent page. Replacing HTML or an image revokes its root Blob URL.
PDF.js's official TextLayerBuilder owns selection boundaries and copy normalization over the width-fitted canvas, with shared page cleanup and a component-owned resize observer. Responsive sizing uses the CSS `scale` property independently of PDF.js's page rotation and translation transforms. Its end-of-content marker and stacking rules constrain selection in blank regions; line-break highlighting is suppressed. Per-page cancellation uses the builder's cleanup rather than aborting the first page's signal, because the official selection listeners are shared across pages.
diff --git a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md
index 7b838711e5..2945cc823d 100644
--- a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md
@@ -16,9 +16,9 @@ Document Preview 将资源观察与内容读取分开。[资源模型](2026-09-0
可读取的文件使用 `dsh-resource://file/session//`。路径可以相对工作区,也可以是绝对路径;编码后的绝对路径保留前导斜杠。`fileAddressFor` 始终生成这种 Session 地址。提供方与 Preview RPC 只从该地址取 Session,不取当前选择、首个持有者或 tab 所属 Session。不带 Session 的 `absolute` URI 无法读取;提供方报告 `workspace-file/unknown-workspace`。Session 授权是文件协议规则,不是额外的 Resource 身份。
-[Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md) 负责格式选择和加载策略。元数据通过 `ctx.documentPreviews` 注册;组件单独注册到 keyed `sidebar.right.tab.document` Slot。扩展注册优先于内置注册,其次比较后缀长度和注册顺序。工具栏列出受支持的候选,按 tab 记住手动选择。未知扩展名及文本兼容的渲染器仍可使用纯文本。文本兼容性由注册的二进制后缀决定,与加载方式无关;HTML 和 SVG 不属于二进制后缀,保留源码查看。只有一个候选时不显示查看器控件。注册声明为二进制的后缀不提供纯文本;已知二进制后缀没有注册渲染器时不发起读取,并显示不支持预览的空态([侧边栏预览打磨](../feature/2026-09-11-sidebar-document-preview-polish.zh.md))。子组件收到累积文本或完整原生字节、原始资源地址,以及标准 `useResource` 和 `useTabInfo` 钩子。Preview 通过 `ctx.remote.workspaceFiles.read` 读取文本,通过 `ctx.remote.workspaceFiles.readBytes` 读取原生字节,关联资源使用 `baseFile`。刷新仍按 tab 独立进行,不引入资源 reload、共享 `changed` 确认、额外资源包装层或内容 Session。
+[Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md) 负责格式选择和加载策略。元数据通过 `ctx.documentPreviews` 注册;组件单独注册到 keyed `sidebar.right.tab.document` Slot。扩展注册优先于内置注册,其次比较后缀长度和注册顺序。工具栏列出受支持的候选,按 tab 记住手动选择。未知扩展名及文本兼容的渲染器仍可使用纯文本。文本兼容性由注册的二进制后缀决定,与加载方式无关;HTML 和 SVG 不属于二进制后缀,保留源码查看。只有一个候选时不显示查看器控件。注册声明为二进制的后缀不提供纯文本;已知二进制后缀没有注册渲染器时不发起读取,并显示不支持预览的空态([侧边栏预览打磨](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md))。子组件收到累积文本或完整原生字节、原始资源地址,以及标准 `useResource` 和 `useTabInfo` 钩子。Preview 通过 `ctx.remote.workspaceFiles.read` 读取文本,通过 `ctx.remote.workspaceFiles.readBytes` 读取原生字节,关联资源使用 `baseFile`。刷新仍按 tab 独立进行,不引入资源 reload、共享 `changed` 确认、额外资源包装层或内容 Session。
-Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML、PDF 和图片读取完整 `Uint8Array` 数据;Host 传输使用[工作区文件二进制传输](2026-09-17-workspace-file-binary-transfer.zh.md)。发布后的缓冲区只读借用,绝不持久化进布局或 Session JSON。PDF.js 在自有 Worker 中运行,字体和解码数据以相同版本随包发布,转移输入前先复制,以保留 Preview 的缓冲区。开启[开发者工具](../feature/2026-09-17-developer-tools-settings.zh.md)时,HTML 在 Blob iframe 中运行,设置 `sandbox="allow-scripts"`,不授予同源、弹窗、表单、下载或顶层导航权限。浏览器保持正常的外部网络规则。有上限的静态本地 JS/CSS 读取由父页面负责;不透明源 iframe 创建自己的资源 Blob,因为它不能加载父源创建的 Blob。PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 使用图片专用 Blob URL,在 `
` 静态图片上下文中渲染。比面板宽的图片按纵横比缩小到面板宽度;较小的图片保留固有 CSS 像素尺寸并由 auto margin 居中,较高的图片扩展共享滚动区的纵向范围([侧边栏预览打磨](../feature/2026-09-11-sidebar-document-preview-polish.zh.md))。渲染器不提供缩放或拖拽平移。SVG 标记绝不进入应用 DOM 或 iframe,因此脚本保持不可执行,也无法访问父页面。替换 HTML 或图片时会撤销其根 Blob URL。
+Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML、PDF 和图片读取完整 `Uint8Array` 数据;Host 传输使用[工作区文件二进制传输](2026-09-17-workspace-file-binary-transfer.zh.md)。发布后的缓冲区只读借用,绝不持久化进布局或 Session JSON。PDF.js 在自有 Worker 中运行,字体和解码数据以相同版本随包发布,转移输入前先复制,以保留 Preview 的缓冲区。开启[开发者工具](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md)时,HTML 在 Blob iframe 中运行,设置 `sandbox="allow-scripts"`,不授予同源、弹窗、表单、下载或顶层导航权限。浏览器保持正常的外部网络规则。有上限的静态本地 JS/CSS 读取由父页面负责;不透明源 iframe 创建自己的资源 Blob,因为它不能加载父源创建的 Blob。PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 使用图片专用 Blob URL,在 `
` 静态图片上下文中渲染。比面板宽的图片按纵横比缩小到面板宽度;较小的图片保留固有 CSS 像素尺寸并由 auto margin 居中,较高的图片扩展共享滚动区的纵向范围([侧边栏预览打磨](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md))。渲染器不提供缩放或拖拽平移。SVG 标记绝不进入应用 DOM 或 iframe,因此脚本保持不可执行,也无法访问父页面。替换 HTML 或图片时会撤销其根 Blob URL。
PDF.js 官方 TextLayerBuilder 在适配宽度的 canvas 上负责选择边界和复制规范化,共享页面清理,并使用由组件拥有的 resize observer。响应式尺寸适配使用独立的 CSS `scale` 属性,与 PDF.js 的页面旋转和平移变换组合。其内容结束标记和堆叠规则限制空白区域中的选择;换行高亮被抑制。逐页取消使用 builder 的清理操作,而不 abort 第一页的信号,因为官方选择监听器跨页面共享。
diff --git a/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml
deleted file mode 100644
index d16bba3786..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-08-global-main-panels.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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
-/agent-note-global-main-panels-without-default-ui-additions:
- en: df950323f9032f3b
- zh: d08172be0c9ca71c
-/agent-note-global-main-panels-without-default-ui-additions/problem:
- en: da6aa479acac3baf
- zh: ed6316ea9953ff99
-/agent-note-global-main-panels-without-default-ui-additions/decision:
- en: e732d64ac12c0ac5
- zh: e8ff462ae93bf259
-/agent-note-global-main-panels-without-default-ui-additions/alternatives-considered:
- en: 18c8461d961490ee
- zh: a26b37ff3aa15bbc
-/agent-note-global-main-panels-without-default-ui-additions/consequences:
- en: 31db82c87448a292
- zh: 1923a60d2f0845f9
diff --git a/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.i18n.yaml
index a8255aa82a..47d3a65a12 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.i18n.yaml
@@ -9,8 +9,8 @@
en: 03e6b75365b29406
zh: cc880a33cf2303d0
/agent-note-awaited-per-agent-initialization/decision:
- en: f1db71d1fdb4388e
- zh: 6c5a43a770f4cffd
+ en: 48cdb570d3feb222
+ zh: 904a1169540e3111
/agent-note-awaited-per-agent-initialization/alternatives-considered:
en: 466a9ebbb13f68c6
zh: ab88fb34786d2c2b
diff --git a/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.md b/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.md
index 27353225a8..40dcec53a8 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.md
+++ b/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.md
@@ -16,7 +16,7 @@ AgentLoop holds its existing maintenance activity through setup and creation dis
Creation dispatch retains the scope and Session while listeners await. Disposal cancels initialization and joins the dispatch before releasing those resources. A listener must not await its own Agent's idle state or its owner's disposal, because each waits for that listener to finish. Background work on another Agent follows that Agent's own initialization lifecycle.
-This decision owns asynchronous creation timing. The [scope runtime decision](2026-07-12-agent-scope-runtime-design.md) retains registry identity and teardown ownership, while the [interception decision](../feature/2026-06-30-interception-extension-points.md) retains policy and tool-event semantics.
+This decision owns asynchronous creation timing. The [scope runtime reference](../../../../packages/core/agent-loop/README.md) retains registry identity and teardown ownership, while the [interception reference](../../../../docs/tool-execution-pipeline.md) retains policy and tool-event semantics.
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.zh.md b/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.zh.md
index f664e89da0..43b0bfa7d1 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.zh.md
@@ -16,7 +16,7 @@ AgentLoop 在 setup 与创建分发期间保留现有的维护活动。初始化
创建分发在监听器等待期间保留作用域与 Session。Dispose(资源释放)会取消初始化,并在释放这些资源前等待分发结束。监听器不得等待自身 Agent 的空闲状态或自身所有者的 dispose,因为两者都要等待该监听器完成。另一个 Agent 上的后台工作遵循该 Agent 自己的初始化生命周期。
-本决策拥有异步创建时序。[作用域运行时决策](2026-07-12-agent-scope-runtime-design.zh.md)继续拥有注册表身份与 teardown 归属,[拦截决策](../feature/2026-06-30-interception-extension-points.zh.md)继续拥有策略与工具事件语义。
+本决策拥有异步创建时序。[作用域运行时参考](../../../../packages/core/agent-loop/README.zh.md)继续拥有注册表身份与 teardown 归属,[拦截参考](../../../../docs/tool-execution-pipeline.zh.md)继续拥有策略与工具事件语义。
## 考虑过的替代方案
diff --git a/.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.i18n.yaml
index 51149ea2fb..f7082b3940 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.i18n.yaml
@@ -9,8 +9,8 @@
en: 4548e7fbdbee5b5a
zh: 0078762ea92fa180
/agent-note-consumer-owned-startup-strictness/decision:
- en: eb2b83f5f65c6b6a
- zh: b85a9aa4645c9a20
+ en: 1f11e04162e19ad1
+ zh: d102e300eba9a2ea
/agent-note-consumer-owned-startup-strictness/alternatives-considered:
en: 1b67e062213184cd
zh: 91cc8a015de2bfe4
@@ -18,5 +18,5 @@
en: 283dec1b336c540d
zh: 2ed358079072cfff
/agent-note-consumer-owned-startup-strictness/testing:
- en: d8cafae30ea1347b
- zh: de47e4e7ba863261
+ en: c38221069ce98fd3
+ zh: 9a2f91f539eba4d4
diff --git a/.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.md b/.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.md
index bbe975e13b..cf17823e72 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.md
+++ b/.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.md
@@ -18,7 +18,7 @@ The audit treats a throwing `disabled` expression as an entry failure, not a dis
The audit runs only during initial application boot. Later config HMR remains best effort and keeps the failed candidate visible for repair.
-This policy governs [Web host boot](2026-07-24-web-config-tree-boot-and-transport-layering.md), including its [client plugin roster](2026-07-23-client-plugin-loading-model.md). [Per-session presets](2026-09-18-declarative-agent-presets.md) own a separate strict subtree audit.
+This policy governs [Web host boot](../../../../packages/boot/app-boot/README.md), including its [client plugin roster](2026-07-23-client-plugin-loading-model.md). [Per-session presets](2026-09-18-declarative-agent-presets.md) own a separate strict subtree audit.
## Alternatives considered
@@ -36,6 +36,6 @@ The compact terminal report keeps the failing plugins visible; a separate file r
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.
+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](../../../../packages/boot/app-boot/tests/user-patches.spec.ts): unit tests isolate reconciliation failures, while the process tests also require the shipped launcher, native watcher, and bounded shutdown to work together.
The matrix enables Chokidar's `awaitWriteFinish` to acknowledge stable file contents before each reload; otherwise its short change-event suppression window can discard the next test edit. Native events remain required, and assertions wait for observed activation or failure rather than a fixed settling sleep. This is explicit test configuration, not evidence for the default watcher timing.
diff --git a/.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.zh.md b/.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.zh.md
index 4bd2bfe026..c8bda5cb20 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.zh.md
@@ -18,7 +18,7 @@ Required id 为 `agent-loop`、`webserver`、`modules`、`connection`、`headles
该审计只在应用首次启动时运行。之后的 config HMR 仍采用 best effort,并保留 failed candidate 供后续修复。
-该策略适用于 [Web host 启动](2026-07-24-web-config-tree-boot-and-transport-layering.zh.md),包括其 [client 插件名册](2026-07-23-client-plugin-loading-model.zh.md)。[按会话的 preset](2026-09-18-declarative-agent-presets.zh.md)持有独立的严格子树审计。
+该策略适用于 [Web host 启动](../../../../packages/boot/app-boot/README.zh.md),包括其 [client 插件名册](2026-07-23-client-plugin-loading-model.zh.md)。[按会话的 preset](2026-09-18-declarative-agent-presets.zh.md)持有独立的严格子树审计。
## 考虑过的替代方案
@@ -36,6 +36,6 @@ Required id 为 `agent-loop`、`webserver`、`modules`、`connection`、`headles
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)互补:单元测试隔离配置协调失败,进程测试还要求随附启动器、原生监听器和有界关闭流程协同工作。
+[Web 进程矩阵](../../../../apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts)分别验证启动时和原生补丁文件修改后的 optional 与 required 失败。经过认证的 HTTP 请求和插件生命周期文件区分可用应用与仅存活的进程。这些无需密钥的进程检查与[受控事件投递单元测试](../../../../packages/boot/app-boot/tests/user-patches.spec.ts)互补:单元测试隔离配置协调失败,进程测试还要求随附启动器、原生监听器和有界关闭流程协同工作。
矩阵启用 Chokidar 的 `awaitWriteFinish`,在每次重载前确认文件内容已稳定;否则它的短暂 change 事件抑制窗口可能丢弃下一次测试编辑。测试仍然依赖原生事件,并等待观察到激活或失败,而不是固定时长的休眠。这是显式测试配置,不能证明默认监听器的时序行为。
diff --git a/.agents/notes/implemented/architecture/2026-09-09-desktop-build-release-validation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-09-desktop-build-release-validation.i18n.yaml
deleted file mode 100644
index 2516370b2e..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-09-desktop-build-release-validation.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-09-desktop-build-release-validation.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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-desktop-build-release-validation.md
-/agent-note-verify-desktop-release-compatibility-during-packaging:
- en: 4183cb362d89a8fb
- zh: ede5e45ba6245b6b
-/agent-note-verify-desktop-release-compatibility-during-packaging/problem:
- en: ae3f1e31ba929c0e
- zh: 1a42684e41531547
-/agent-note-verify-desktop-release-compatibility-during-packaging/decision:
- en: 352372f213c43a76
- zh: 54dc9826254e56f2
-/agent-note-verify-desktop-release-compatibility-during-packaging/alternatives-considered:
- en: 515cec0d22c8373c
- zh: e3a5b3058b6316c2
-/agent-note-verify-desktop-release-compatibility-during-packaging/consequences:
- en: 5193e3ce82faa984
- zh: 1bd7a31152e3b71c
diff --git a/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.i18n.yaml
deleted file mode 100644
index ecc422ab61..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-09-desktop-immediate-window-and-direct-start.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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-desktop-immediate-window-and-direct-start.md
-/agent-note-show-the-desktop-window-before-starting-the-host:
- en: aed1efe0e21399a1
- zh: dd45a0ed4bfa731e
-/agent-note-show-the-desktop-window-before-starting-the-host/problem:
- en: bd8f14bc0beaac9e
- zh: 20b0521f27ad231f
-/agent-note-show-the-desktop-window-before-starting-the-host/decision:
- en: 6ac14061a85b3da6
- zh: ec35f11c15b34be8
-/agent-note-show-the-desktop-window-before-starting-the-host/alternatives-considered:
- en: 4d476b25f3647e1b
- zh: 4a1b0252005c8c22
-/agent-note-show-the-desktop-window-before-starting-the-host/consequences:
- en: 98119090f2133d33
- zh: 53ad4539d677ee4e
diff --git a/.agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.i18n.yaml
deleted file mode 100644
index 6c34e86274..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.i18n.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-09-plugin-management-in-the-web-sidebar.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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-plugin-management-in-the-web-sidebar.md
-/agent-note-plugin-management-moves-to-the-web-sidebar:
- en: efdb6c8f366dd2c2
- zh: 5c55e2882753d481
-/agent-note-plugin-management-moves-to-the-web-sidebar/problem:
- en: b0d11556425397b0
- zh: 00968088bec13102
-/agent-note-plugin-management-moves-to-the-web-sidebar/decision:
- en: eb93a33f64907b80
- zh: 4073f2da219ad798
-/agent-note-plugin-management-moves-to-the-web-sidebar/alternatives-considered:
- en: e5cea1a5e97884be
- zh: d5544ae935a48f0a
-/agent-note-plugin-management-moves-to-the-web-sidebar/consequences:
- en: 74c0051937f03444
- zh: 256b38ff34549a4d
-/agent-note-plugin-management-moves-to-the-web-sidebar/testing:
- en: 9d08eb02abc00595
- zh: 56c7d26016f9deec
diff --git a/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.i18n.yaml
index c1cc88bcdd..8d31b237e6 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.i18n.yaml
@@ -6,8 +6,8 @@
en: 6a4338564c01ad91
zh: 241248cff9bd4cfc
/agent-note-add-immutable-profile-resolution-generations/problem:
- en: 8e192c176fe48c4c
- zh: adb965d650088eb0
+ en: 907a40c722a6faff
+ zh: a471d7034d8c231f
/agent-note-add-immutable-profile-resolution-generations/decision:
en: a3022a87337058c5
zh: 1d53a08a25ed6eb3
@@ -21,8 +21,8 @@
en: 53a9e92698ce9a5d
zh: 6124d446e8d4a532
/agent-note-add-immutable-profile-resolution-generations/decision/immutable-generations:
- en: cd4e64bd80f448a2
- zh: 585e1fbd8721ae99
+ en: e10b224e0e68b701
+ zh: d43fbdf37e6bfc54
/agent-note-add-immutable-profile-resolution-generations/decision/shared-esm-and-commonjs-rule:
en: 0e0575a04a081b52
zh: 94c74334939e6d13
@@ -33,8 +33,8 @@
en: 99308bd758859e02
zh: 35f1e7719fe80d2e
/agent-note-add-immutable-profile-resolution-generations/decision/additive-package-changes:
- en: 718dfa7826783f2a
- zh: 21c3d5fdfae9cb99
+ en: 39d27cff0d419259
+ zh: 6cbdc88a450614d0
/agent-note-add-immutable-profile-resolution-generations/decision/filesystem-and-runtime-carriers:
en: b0607713102a093d
zh: 2de9c33bfaa0e3e2
@@ -45,8 +45,8 @@
en: 2426173c58a17a48
zh: 0423b3700273accd
/agent-note-add-immutable-profile-resolution-generations/verification:
- en: fbb17aa8fe62b1e8
- zh: 2ed091fdfd995fc8
+ en: 74cb43ea2ccdb334
+ zh: b966bbed43a83c82
/agent-note-add-immutable-profile-resolution-generations/consequences:
- en: 54cae20b2734a07e
- zh: caa334f8f7a40452
+ en: 457a7d1e82b45fe0
+ zh: 800036e083a7674c
diff --git a/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md b/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md
index 649c6c358f..08aeffa559 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md
+++ b/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md
@@ -8,7 +8,7 @@ English | [中文](2026-09-09-profile-resolution-generations.zh.md)
A profile loads plugin rows from its own package project, while Harness packages and packages carried by selected bundles can live outside that project's ordinary dependency tree. Bridging the trees through shared symlinks, profile-owned links, or packaged-executable proxy packages persists package selections across processes and installations. Those files require reconciliation and locking, expose generated proxy manifests to metadata readers, and cannot represent a process-local change atomically.
-The runtime design keeps installation-first, ordered-bundle, and local-before-fallback precedence. It covers imports performed by plugin modules as well as Loader row imports and works in the main thread and Harness-owned Workers. Generation replacement preserves existing package mappings and local package names, permits linked-root membership changes, and never mutates a live table entry by entry.
+The runtime design keeps installation-first, ordered-bundle, and local-before-fallback precedence. It covers imports performed by plugin modules as well as Loader row imports and works in the main thread and Harness-owned Workers. Generation replacement preserves retained package identities, permits removing profile packages and changing linked-root membership, and never mutates a live table entry by entry.
## Decision
@@ -75,7 +75,7 @@ Linked-root membership is immutable within a generation. A successor may add or
The router retains the real target of each successfully published link name for its lifetime, solely to validate successors. Removing a root does not erase that record or keep its directory intercepted. Re-adding the same name and target is allowed; a different target is rejected because Node caches real paths. Failed publication changes neither the current generation nor the recorded targets.
-The launcher constructs one startup generation. The service accepts a complete successor, but no package-manager transaction invokes replacement in this implementation.
+The launcher constructs one startup generation. `ProfileRuntimeResolution` retains only the installation anchor, Harness home, and optional profile directory as recomputation inputs; successors reread disk selection and layers rather than inheriting a supplied profile's synthetic layers. A generation without a profile directory can recompute installation packages. The [package refresh decision](2026-09-30-profile-package-refresh-and-manifest-invalidation.md) assigns successor publication to Plugin Manager operations.
### Shared ESM and CommonJS rule
@@ -105,9 +105,9 @@ New Workers inherit the latest published generation. Existing Workers keep the g
### Additive package changes
-A caller adding a package completes its pnpm transaction before constructing a successor generation. Replacement rejects any generation that changes the directory or version of an existing package. The caller publishes an additive successor before mounting the new Loader row; this implementation does not provide that package transaction. A mount failure may leave the package installed but inactive.
+A caller adding a package completes its pnpm transaction before constructing a successor generation. Replacement rejects any generation that changes the directory or version of a retained package. Plugin Manager publishes the successor before mounting the new Loader row. A mount failure may leave the package installed but inactive.
-Changing or removing an existing runtime package mapping, or removing a recorded profile-local package name, requires process restart because Node's ESM Module Map, CommonJS cache, existing object references, and running Workers can retain the old module identity. These restrictions do not prohibit removing a linked root from the interception scope. Generation replacement does not claim to unload modules.
+A retained package keeps its normalized directory, version, and scope; installation entries also keep their declaring anchor and cannot be removed. Violating these restrictions requires process restart because Node's ESM Module Map, CommonJS cache, existing object references, and running Workers can retain the old module identity. A profile entry may change its declaring anchor: deselecting the first bundle that reaches a shared dependency can leave another bundle selecting the same physical package. Profile-scoped mappings and profile-local package names may be removed; the caller stops their plugins before deleting package files. A profile mapping moves to another directory only through removal and a new installation; a linked root may point to another target ([replacement Note](2026-10-01-replace-changed-package-modules-on-recomposition.md)). These restrictions do not prohibit removing a linked root from the interception scope. Generation replacement does not claim to unload modules.
### Filesystem and runtime carriers
@@ -142,7 +142,7 @@ Behavior tests exercise root order, transitive and peer dependencies, local and
## Verification
- One eager computation supplies the runtime resolution; startup neither writes nor retires module-resolution data.
-- [Generation tests](../../../../packages/boot/app-boot/tests/profile-resolution.spec.ts) cover installation and selected-bundle graphs with ordinary directories and recursive symlinks, including different dependency versions beside logical and real anchors. They also cover linked-root removal, same-target restoration, overlapping roots, native misses, new requests from loaded modules, and relink rejection after removal.
+- [Generation tests](../../../../packages/boot/app-boot/tests/profile-resolution.spec.ts) cover installation and selected-bundle graphs with ordinary directories and recursive symlinks, including different dependency versions beside logical and real anchors. They also cover same-package profile declarer changes, retained-identity rejection, captured-directory recomputation, linked-root removal, same-target restoration, overlapping roots, native misses, new requests from loaded modules, and relink rejection after removal.
- [Source-launch tests](../../../../apps/cli/tests/source-launch.compat.spec.ts) and [built-bin tests](../../../../apps/cli/tests/built-bin.e2e.ts) run both profile layouts through the real CLI. They assert ESM/CJS versions, loaded paths, per-format dependency identity, and consistent Tools/AgentLoop module instances with an accessible scheduler key.
- Pkg and Electron carriers select runtime resolution; Electron executes its Host in Node mode from the ASAR-backed dsh tree while native executable entries remain unpacked.
- ESM and CommonJS adapters share one router and delegate final resolution to Node without `module.registerHooks` or `_findPath` replacement.
@@ -152,4 +152,4 @@ Behavior tests exercise root order, transitive and peer dependencies, local and
## Consequences
-Runtime startup avoids disk mutation and proxy manifests while retaining package-precedence rules. Real-directory anchors align dependency discovery with default Node loading and tsx workspace mapping, including cases where a logical symlink path would select another version. The implementation accepts the maintenance cost of Node Internal compatibility tests and an early, self-contained bootstrap in each owned Worker; it provides no disk-only backend or dual comparison mode. Package mappings and local package names remain additive; linked-root membership may change without unloading modules.
+Runtime startup avoids disk mutation and proxy manifests while retaining package-precedence rules. Real-directory anchors align dependency discovery with default Node loading and tsx workspace mapping, including cases where a logical symlink path would select another version. The implementation accepts the maintenance cost of Node Internal compatibility tests and an early, self-contained bootstrap in each owned Worker; it provides no disk-only backend or dual comparison mode. Profile package records and linked-root membership may be removed without unloading modules; retained package identities stay unchanged.
diff --git a/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md b/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md
index 4e84cc1f1b..cf63626c6a 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md
@@ -8,7 +8,7 @@ Status: implemented
profile 从自己的包项目加载插件配置项,而 Harness 包和所选 bundle 携带的包可能位于该项目普通依赖树之外。通过共享 symlink、profile 自有链接或打包可执行文件的代理包连接两棵依赖树,会让选包结果跨进程和安装版本持续存在。这些文件需要协调和锁来维护,并向元数据读取方暴露生成的代理 manifest(元数据清单),也无法原子表示进程内变更。
-运行时设计保留安装优先、有序 bundle 和本地包优先于 fallback 的顺序。它覆盖插件模块内部的 import 以及 Loader 配置项的 import,并在主线程和 Harness 自有 Worker 中工作。generation 替换保留既有包映射和本地包名,允许调整 linked root 集合,不会逐项修改正在使用的表。
+运行时设计保留安装优先、有序 bundle 和本地包优先于 fallback 的顺序。它覆盖插件模块内部的 import 以及 Loader 配置项的 import,并在主线程和 Harness 自有 Worker 中工作。generation 替换保持保留包的身份不变,允许移除 profile 包和调整 linked root 集合,不会逐项修改正在使用的表。
## Decision
@@ -75,7 +75,7 @@ linked root 集合在一个 generation 内不可变。后继 generation 可以
解析器在自身生命周期内保留每个已成功发布的 link 名称对应的真实目标,仅用于校验后继 generation。移除 root 不会抹掉该记录,也不会让其目录继续被拦截。同名、同目标可以重新加入;不同目标会被拒绝,因为 Node 缓存真实路径。发布失败时,当前 generation 和已记录目标均不变。
-launcher 只构造启动 generation。服务接受完整的后继 generation,但本实现没有包管理器事务调用替换操作。
+launcher 只构造启动 generation。`ProfileRuntimeResolution` 仅保留安装锚点、Harness home 和可选的 profile 目录作为重算输入;后继代重新读取磁盘上的选择与配置层,不继承传入 profile 的合成层。没有 profile 目录的 generation 可以重算安装包。[包操作刷新决策](2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md)规定由 Plugin Manager 的操作发布后继代。
### ESM 与 CommonJS 共用规则
@@ -105,9 +105,9 @@ runtime resolution 列出它提供的包;Loader entries 组成活动插件列
### 只增加包的变更
-添加包的调用方先完成 pnpm 事务,再构造下一代。替换操作会拒绝改变任何既有 package name 的目录或版本。调用方先发布只增加映射的后继 generation,再挂载新的 Loader 配置项;本实现不提供该包事务。挂载失败可以留下已安装但未启用的包。
+添加包的调用方先完成 pnpm 事务,再构造下一代。替换操作会拒绝改变任何保留包的目录或版本。Plugin Manager 先发布后继 generation,再挂载新的 Loader 配置项。挂载失败可以留下已安装但未启用的包。
-修改或删除既有运行时包映射,或删除已记录的 profile 本地包名,需要重启,因为 Node 的 ESM Module Map、CommonJS cache、现存对象引用和运行中的 Worker 都可能保留旧模块 identity。这些限制不禁止从拦截范围移除 linked root。generation 换代不声称卸载模块。
+保留包的规范化目录、版本和作用域必须不变;安装条目还必须保留声明锚点,且不能移除。违反这些限制需要重启,因为 Node 的 ESM Module Map、CommonJS cache、现存对象引用和运行中的 Worker 都可能保留旧模块 identity。profile 条目可以改变声明锚点:取消选入最先找到共享依赖的组合包后,另一个组合包仍可能选中同一物理包。profile 范围的映射和 profile 本地包名可以移除;调用方在删除包文件之前先停止相关插件。profile 映射只能通过卸载和重新安装移到其他目录;linked root 可以指向其他目标([替换 Note](2026-10-01-replace-changed-package-modules-on-recomposition.zh.md))。这些限制不禁止从拦截范围移除 linked root。generation 换代不声称卸载模块。
### 文件系统与运行时载体
@@ -142,7 +142,7 @@ generation 构造发生在启动或显式更新阶段,不属于单次 resolve
## Verification
- 一次 eager 计算供应 runtime resolution;启动既不写入也不退休模块解析数据。
-- [Generation 测试](../../../../packages/boot/app-boot/tests/profile-resolution.spec.ts)覆盖普通目录和递归软链接下的安装图与所选 bundle 图,包括逻辑锚点和真实锚点旁存在不同依赖版本的情况。测试还覆盖 linked root 移除、同目标恢复、重叠 root、原生缺包、已加载模块的新请求,以及移除后重新链接不同目标时的拒绝。
+- [Generation 测试](../../../../packages/boot/app-boot/tests/profile-resolution.spec.ts)覆盖普通目录和递归软链接下的安装图与所选 bundle 图,包括逻辑锚点和真实锚点旁存在不同依赖版本的情况。测试还覆盖同包 profile 声明者变更、保留包身份变更拒绝、捕获目录后的重算、linked root 移除、同目标恢复、重叠 root、原生缺包、已加载模块的新请求,以及移除后重新链接不同目标时的拒绝。
- [源码启动测试](../../../../apps/cli/tests/source-launch.compat.spec.ts)与[构建入口测试](../../../../apps/cli/tests/built-bin.e2e.ts)通过真实 CLI 运行两种 profile 布局,断言 ESM/CJS 版本、加载路径、各模块格式内的依赖身份,以及一致的 Tools/AgentLoop 模块实例和可访问的 scheduler 键。
- pkg 与 Electron 载体选择 runtime 解析;Electron 以 Node 模式从 ASAR 承载的 dsh 依赖树执行 Host,原生可执行条目保持 unpacked。
- ESM 与 CommonJS 适配器共享同一个路由器,并把最终解析委托给 Node,不使用 `module.registerHooks` 或替换 `_findPath`。
@@ -152,4 +152,4 @@ generation 构造发生在启动或显式更新阶段,不属于单次 resolve
## Consequences
-runtime 启动避免磁盘修改和代理 manifest,同时保留包优先级规则。真实目录锚点让依赖发现对齐 Node 默认加载与 tsx workspace 映射,也覆盖软链接逻辑路径会选中另一版本的情况。实现需要持续维护 Node Internal 兼容测试,并在每个自有 Worker 中尽早执行自包含 bootstrap;它不提供纯磁盘后端或 dual 对比模式。包映射和本地包名仍只允许新增;linked root 集合可以变化,而不卸载模块。
+runtime 启动避免磁盘修改和代理 manifest,同时保留包优先级规则。真实目录锚点让依赖发现对齐 Node 默认加载与 tsx workspace 映射,也覆盖软链接逻辑路径会选中另一版本的情况。实现需要持续维护 Node Internal 兼容测试,并在每个自有 Worker 中尽早执行自包含 bootstrap;它不提供纯磁盘后端或 dual 对比模式。profile 包记录和 linked root 集合可以移除,而不卸载模块;保留包的身份保持不变。
diff --git a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.i18n.yaml
index 3d1c6c968a..55defc8dc9 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.i18n.yaml
@@ -9,8 +9,8 @@
en: ee5703dcce5d81c5
zh: 6c1aec04723d211b
/agent-note-workspace-file-read-authority/decision:
- en: 92dbdf6087e00f04
- zh: 2c8720a081d492f9
+ en: b4b88d397452d083
+ zh: 14069d83e001082d
/agent-note-workspace-file-read-authority/alternatives-considered:
en: 329e575be02d7392
zh: a78f428b36d40f59
diff --git a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.md b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.md
index 140816e8c4..5550cfa003 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.md
+++ b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.md
@@ -14,7 +14,7 @@ Workspace Files serves both file content and workspace navigation. Applying work
`list` and `changes` remain workspace-scoped because they expose workspace navigation and observation rather than a named file read. `list` rejects a directory outside the root, and `changes` filters observations through the backend's workspace-containment predicate.
-`readBytes` with `baseFile` resolves a relative path from the base file's directory. A `..` path may therefore read JavaScript or CSS outside the workspace when the Session backend permits it. With [Developer tools](../feature/2026-09-17-developer-tools-settings.md) enabled, Document Preview packages bounded, statically declared local scripts and stylesheets into an HTML Blob iframe with `sandbox="allow-scripts"`; the opaque origin blocks parent access, but the browser retains normal network access. This exposure is an intentional security trade-off for rendering static generated HTML.
+`readBytes` with `baseFile` resolves a relative path from the base file's directory. A `..` path may therefore read JavaScript or CSS outside the workspace when the Session backend permits it. With [Developer tools](../../../../packages/client/ui-sidebar-documentpreview/README.md) enabled, Document Preview packages bounded, statically declared local scripts and stylesheets into an HTML Blob iframe with `sandbox="allow-scripts"`; the opaque origin blocks parent access, but the browser retains normal network access. This exposure is an intentional security trade-off for rendering static generated HTML.
The [Workspace Files service](2026-09-05-workspace-files-service.md) owns paging, file checks, listing, and observation. [Document Preview](2026-09-08-document-preview-operations.md) owns which related files are packaged and the iframe sandbox.
diff --git a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.zh.md b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.zh.md
index 75ffee32c4..f12c11ac9b 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.zh.md
@@ -14,7 +14,7 @@ Workspace Files 同时提供文件内容与工作区导航。对所有操作应
`list` 与 `changes` 仍限于工作区,因为它们暴露工作区导航和观察,而不是读取一个具名文件。`list` 拒绝根外目录,`changes` 通过后端的工作区包含判定过滤观察。
-带 `baseFile` 的 `readBytes` 从基准文件所在目录解析相对路径。因此,只要 Session 后端允许,`..` 路径就可以读取工作区外的 JavaScript 或 CSS。开启[开发者工具](../feature/2026-09-17-developer-tools-settings.zh.md)时,Document Preview 把有界、静态声明的本地脚本与样式表打包进带 `sandbox="allow-scripts"` 的 HTML Blob iframe;不透明源阻止访问父应用,但浏览器保留正常网络访问。这种暴露是为渲染静态生成 HTML 而有意接受的安全取舍。
+带 `baseFile` 的 `readBytes` 从基准文件所在目录解析相对路径。因此,只要 Session 后端允许,`..` 路径就可以读取工作区外的 JavaScript 或 CSS。开启[开发者工具](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md)时,Document Preview 把有界、静态声明的本地脚本与样式表打包进带 `sandbox="allow-scripts"` 的 HTML Blob iframe;不透明源阻止访问父应用,但浏览器保留正常网络访问。这种暴露是为渲染静态生成 HTML 而有意接受的安全取舍。
[Workspace Files 服务](2026-09-05-workspace-files-service.zh.md)负责分页、文件检查、列举和观察。[Document Preview](2026-09-08-document-preview-operations.zh.md)负责选择要打包的关联文件及 iframe sandbox。
diff --git a/.agents/notes/implemented/architecture/2026-09-10-command-identities-and-composer-file-action.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-10-command-identities-and-composer-file-action.i18n.yaml
deleted file mode 100644
index 85ab4e3c03..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-10-command-identities-and-composer-file-action.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-10-command-identities-and-composer-file-action.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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-command-identities-and-composer-file-action.md
-/agent-note-command-identities-and-composer-owned-file-action:
- en: 96db2d4653b47858
- zh: b914d79ab95b0e51
-/agent-note-command-identities-and-composer-owned-file-action/problem:
- en: ad2c360593eb7a42
- zh: 503a9c31a5eb55e7
-/agent-note-command-identities-and-composer-owned-file-action/decision:
- en: c78d13ee6a59c04d
- zh: 53fc322d351e437b
-/agent-note-command-identities-and-composer-owned-file-action/alternatives-considered:
- en: 94ccabd1fe224408
- zh: bfa28806f0218ce8
-/agent-note-command-identities-and-composer-owned-file-action/consequences:
- en: 08f25ff5e0d227cf
- zh: a26663e5b7d9feeb
diff --git a/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.i18n.yaml
deleted file mode 100644
index 50d4ce7a3d..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-10-compact-tool-details.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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-compact-tool-details.md
-/agent-note-compact-tool-details-from-recorded-results:
- en: 7495c26039484440
- zh: d1907caf1f139054
-/agent-note-compact-tool-details-from-recorded-results/problem:
- en: e4ce66f2cdb08516
- zh: 3f45374f23fa8f3b
-/agent-note-compact-tool-details-from-recorded-results/decision:
- en: 668e06f5146a78de
- zh: 4af1edf74eb8121c
-/agent-note-compact-tool-details-from-recorded-results/alternatives-considered:
- en: 1f0bf45e4ecc0bfa
- zh: c46b173dc7ee180b
-/agent-note-compact-tool-details-from-recorded-results/consequences:
- en: e02acb07b752c285
- zh: b29f9a5ad44ff10f
diff --git a/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.md b/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.md
deleted file mode 100644
index 6c1f29c555..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Agent Note: Compact Tool Details from Recorded Results
-
-Status: implemented
-
-English | [中文](2026-09-10-compact-tool-details.zh.md)
-
-## Problem
-
-Goal, todo, and schedule results contain a small set of user-facing facts that take more effort to read as JSON. Dedicated expanded bodies need to fit the existing conversation rows without adding a second presentation registry or live state to historical calls.
-
-## Decision
-
-The `ui-tool` package registers goal and schedule toolviews through the existing keyed slot and shares one package-internal fields/list component with the todo view. Successful, recognized results display recorded values; incomplete calls, errors, malformed values, and unsupported result formats retain generic input/output. Schedule domain errors, including uncertain persistence and an unknown deletion target, retain their original result.
-
-Tool rows keep their disclosure and Inspect interaction. Checklist marks are static and have accessible status labels. Schedule times use absolute dates with the viewer's time zone; states come from the recorded result. Goal details distinguish an active goal awaiting continuation from an armed goal. These display values do not subscribe to the current goal, todo, or schedule projection.
-
-Todo changes compare each root or nested call with its preceding durable `todo/write`, so later writes cannot alter an earlier card. The conversation assembler repairs predecessors when older history loads. Until the predecessor is known, the card shows the submitted list without inferred additions or removals.
-
-This scoped visual change is an exception to the visual-equivalence requirement in [Client-derived tool presentation](2026-08-23-client-derived-tool-presentation.md). Its raw-event ownership, generic fallback, and single keyed registry remain authoritative.
-
-## Alternatives considered
-
-**A shared presenter framework:** the existing keyed slot already dispatches tools. Reusing only the detail body keeps the abstraction local until another package needs it.
-
-**A dashboard inside each card:** interactive controls, progress graphics, and live states add visual weight and can misrepresent what a historical call returned. Compact read-only fields and lists preserve the conversation's hierarchy.
-
-## Consequences
-
-Registered tools gain readable details without changing Host tools, Session events, or public Client exports. Tool-specific adapters interpret recorded JSON and text; unrecognized formats retain generic output. The authored tool-details Session replay covers their assembled rendering.
diff --git a/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.zh.md b/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.zh.md
deleted file mode 100644
index d0822a48f1..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.zh.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Agent Note: Compact Tool Details from Recorded Results
-
-Status: implemented
-
-[English](2026-09-10-compact-tool-details.md) | 中文
-
-## 问题
-
-目标、任务清单和定时任务的结果包含少量面向用户的事实,以 JSON 阅读时较费力。专用展开内容需要融入现有对话行,不能为历史调用新增一套呈现注册表或实时状态。
-
-## 决策
-
-`ui-tool` 包通过现有按键分发的插槽注册目标和定时任务视图,并与任务清单视图共用一个包内字段/列表组件。成功且可识别的结果显示记录值;未完成调用、错误、格式异常和不支持的结果格式保留通用输入/输出。定时任务领域错误(包括持久化不确定和删除目标不存在)保留原始结果。
-
-工具行保留展开和 Inspect 交互。任务清单标记是静态的,并带有可访问的状态标签。定时任务使用查看者时区的绝对日期,状态取自记录的结果。目标详情区分等待继续的活动目标和已启动的目标。这些显示值不订阅当前目标、任务清单或定时任务投影。
-
-任务清单的变化将每个根调用或嵌套调用与之前的持久化 `todo/write` 比较,因此后续写入不会改变较早的卡片。对话组装器在加载更早的历史时修复前序记录。前序记录尚不明确时,卡片展示提交的列表,不推断新增或删除。
-
-这项有限的视觉调整是[客户端派生工具呈现](2026-08-23-client-derived-tool-presentation.zh.md)中视觉等价要求的例外。该决策中的原始事件所有权、通用回退和唯一按键注册表仍然有效。
-
-## 考虑过的替代方案
-
-**共享呈现框架:**现有按键插槽已负责工具分发。仅复用详情内容,可以在其他包出现需求前将抽象限制在包内。
-
-**在每张卡片中嵌入仪表盘:**交互控件、进度图形和实时状态会增加视觉负担,也可能歪曲历史调用返回的事实。紧凑的只读字段和列表保留了对话的层级。
-
-## 影响
-
-已注册的工具获得可读详情,无需改变 Host 工具、Session 事件或公开的 Client 导出。工具专用适配器解读记录中的 JSON 和文本;未识别的格式保留通用输出。手写的 tool-details Session 回放覆盖这些工具组装后的渲染。
diff --git a/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.i18n.yaml
index 2de0be6d61..5f22b758a9 100644
--- a/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.i18n.yaml
@@ -3,14 +3,14 @@
# 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
/agent-note-run-desktop-through-the-shared-web-application:
- en: 1803d1966797a555
- zh: 6ab64a673a217d34
+ en: aa8d39aff5589d4e
+ zh: f2d72d070f879f15
/agent-note-run-desktop-through-the-shared-web-application/problem:
en: 085b445846af76c1
zh: fdddf72401292bbd
/agent-note-run-desktop-through-the-shared-web-application/decision:
- en: 7767ab2839440970
- zh: 9a998e72b728fd17
+ en: 6a51259a49377680
+ zh: d18f7010ced738e7
/agent-note-run-desktop-through-the-shared-web-application/alternatives-considered:
en: 9c5dfbf9cc788040
zh: 631f851909dd7234
diff --git a/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.md b/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.md
index 4f43cc6507..d2c286c955 100644
--- a/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.md
+++ b/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.md
@@ -4,7 +4,7 @@ Status: implemented
English | [中文](2026-09-10-desktop-web-wrapper.zh.md)
-The [Electron runtime decision](2026-09-11-desktop-electron-node-runtime.md) supersedes the separate upstream Node executable; other decisions in this note remain applicable.
+Desktop uses Electron as its Node runtime under the [current runtime reference](../../../../apps/desktop/README.md); other decisions in this note remain applicable.
## Problem
@@ -12,7 +12,7 @@ Separate Desktop composition and request transport require their own configurati
## Decision
-The private Desktop Host invokes the CLI's shared profile runner against the independently owned Desktop profile. The complete Web composition owns authentication, HTTP routes, client assets, RPC, and response streaming. Electron loads packaged static Web assets before the child is ready. Child IPC carries readiness, structured boot injections, and shutdown. The [immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-start.md) owns local-document HTTP forwarding and authenticated WebSocket access; Web retains application dispatch and stream framing.
+The private Desktop Host invokes the CLI's shared profile runner against the independently owned Desktop profile. The complete Web composition owns authentication, HTTP routes, client assets, RPC, and response streaming. Electron loads packaged static Web assets before the child is ready. Child IPC carries readiness, structured boot injections, and shutdown. The [immediate-window reference](../../../../apps/desktop/README.md) owns local-document HTTP forwarding and authenticated WebSocket access; Web retains application dispatch and stream framing.
The shared runner owns profile and Harness-home patches, proxy setup, telemetry defaults, the runtime resolution, configuration reload, and application lifecycle. Desktop initializes profiles from the shared Web template and uses its Plugin Manager in the main application. Electron owns windows, menus, native directory selection, recovery, and release updates.
diff --git a/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.zh.md b/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.zh.md
index bc41b4f8ef..dfe352d078 100644
--- a/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.zh.md
@@ -4,7 +4,7 @@ Status: implemented
[English](2026-09-10-desktop-web-wrapper.md) | 中文
-[Electron 运行时决策](2026-09-11-desktop-electron-node-runtime.zh.md)替代独立上游 Node 可执行文件的选择;本文其他决策仍然适用。
+Desktop 按[当前运行时参考](../../../../apps/desktop/README.zh.md)使用 Electron 作为 Node 运行时;本文其他决策仍然适用。
## Problem
@@ -12,7 +12,7 @@ Status: implemented
## Decision
-私有 Desktop Host 针对独立归属的 Desktop profile 调用 CLI 的共享 profile runner。完整 Web 组合负责认证、HTTP 路由、客户端资源、RPC 与响应流。Electron 在子进程就绪前加载打包静态 Web 资源。子进程 IPC 承载就绪、结构化启动注入与关闭。[立即显示窗口决策](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)规定本地文档 HTTP 转发与认证 WebSocket 访问;Web 保留应用分派与流帧处理。
+私有 Desktop Host 针对独立归属的 Desktop profile 调用 CLI 的共享 profile runner。完整 Web 组合负责认证、HTTP 路由、客户端资源、RPC 与响应流。Electron 在子进程就绪前加载打包静态 Web 资源。子进程 IPC 承载就绪、结构化启动注入与关闭。[立即显示窗口参考](../../../../apps/desktop/README.zh.md)规定本地文档 HTTP 转发与认证 WebSocket 访问;Web 保留应用分派与流帧处理。
共享 runner 负责 profile 与 Harness-home patch、代理设置、遥测默认值、runtime resolution、配置重载和应用生命周期。Desktop 从共享 Web 模板初始化 profile,并在主应用中使用其插件管理器。Electron 负责窗口、菜单、原生目录选择、恢复和发布更新。
diff --git a/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.i18n.yaml
index 6a7a9b2152..aadac74410 100644
--- a/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.i18n.yaml
@@ -6,8 +6,8 @@
en: 13b817438040d321
zh: 490038f3c93551c7
/agent-note-durable-image-offload-events/problem:
- en: 93c2241e0f40fca1
- zh: fe3528cff00e07b3
+ en: 352f3f7912a08a3e
+ zh: bf3a16a5bd2c3a84
/agent-note-durable-image-offload-events/decision:
en: e1cbaf46e6a6a7be
zh: c3640a44c9be69af
diff --git a/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.md b/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.md
index e80c4ae17b..1115a66fab 100644
--- a/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.md
+++ b/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.md
@@ -6,7 +6,7 @@ English | [中文](2026-09-10-image-offload-events.zh.md)
## Problem
-A request can exceed an image-capable route's byte or image-count budget. Recomputing the omitted prefix for each request lets a route switch, inline fallback, or compaction restore previously omitted images. The model-visible image set then depends on unlogged request preparation. The [unified image pipeline](../feature/2026-08-20-unified-image-request-pipeline.md) owns attachment normalization and request versions; durable omission needs its own recorded decision.
+A request can exceed an image-capable route's byte or image-count budget. Recomputing the omitted prefix for each request lets a route switch, inline fallback, or compaction restore previously omitted images. The model-visible image set then depends on unlogged request preparation. The [unified image pipeline](../../../../packages/attachment/attachment-local/README.md) owns attachment normalization and request versions; durable omission needs its own recorded decision.
## Decision
diff --git a/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.zh.md b/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.zh.md
index 2aa2b8b1fb..a64c898215 100644
--- a/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-10-image-offload-events.zh.md
@@ -6,7 +6,7 @@ Status: implemented
## 问题
-请求可能超过图片路由的字节或图片数量预算。每次请求重新计算省略前缀,会让路由切换、内联回退或压缩恢复先前省略的图片。模型可见的图片集合因此取决于未记录的请求准备过程。[统一图片流水线](../feature/2026-08-20-unified-image-request-pipeline.zh.md)负责附件归一化与请求版本,持久省略需要单独记录决策。
+请求可能超过图片路由的字节或图片数量预算。每次请求重新计算省略前缀,会让路由切换、内联回退或压缩恢复先前省略的图片。模型可见的图片集合因此取决于未记录的请求准备过程。[统一图片流水线](../../../../packages/attachment/attachment-local/README.zh.md)负责附件归一化与请求版本,持久省略需要单独记录决策。
## 决策
diff --git a/.agents/notes/implemented/architecture/2026-09-10-public-package-manifest.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-10-public-package-manifest.i18n.yaml
index 493f5002ab..4434bd49e6 100644
--- a/.agents/notes/implemented/architecture/2026-09-10-public-package-manifest.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-10-public-package-manifest.i18n.yaml
@@ -9,8 +9,8 @@
en: e4bafc2b26cae6d2
zh: f8226b200b627c55
/agent-note-public-package-manifest-fields/decision:
- en: 9351e430c86c51fe
- zh: 486496885b77824a
+ en: 8586a9687da8e327
+ zh: 2dcd1c1fdb856412
/agent-note-public-package-manifest-fields/alternatives-considered:
en: 656ef2c1e7853d01
zh: 21c525fda71c6def
diff --git a/.agents/notes/implemented/architecture/2026-09-10-public-package-manifest.md b/.agents/notes/implemented/architecture/2026-09-10-public-package-manifest.md
index 2d35a3ad17..4341d99d4e 100644
--- a/.agents/notes/implemented/architecture/2026-09-10-public-package-manifest.md
+++ b/.agents/notes/implemented/architecture/2026-09-10-public-package-manifest.md
@@ -14,7 +14,7 @@ Plugin authors need npm identity, runtime requirements, and DSH declarations fro
Runtime requirements live at top-level `engines`: `dsh`, `node`, and `npm` are optional version strings, and other engine names are allowed. `dsh.manifestVersion` identifies declaration format `1`. Format and DSH compatibility declarations are not enforced by current installers or loaders.
-The image packer owns `configTrees`, and the workspace catalog generator owns Session migration declarations. Their existing on-disk keys remain readable by those internal tools, but the public manifest types do not expose them. This scope refines the [shared declaration ownership decision](2026-09-05-package-manifest-types.md), whose package placement and dependency rules remain active.
+The image packer owns `configTrees`, and the workspace catalog generator owns Session migration declarations. Their existing on-disk keys remain readable by those internal tools, but the public manifest types do not expose them.
Each consumer owns JSON parsing, field validation, default resolution, and adaptation to runtime data. Interfaces do not validate parsed JSON. A helper belongs in the shared package only when multiple consumers need the same validation or normalization; getters that repeat property access add no shared policy.
diff --git a/.agents/notes/implemented/architecture/2026-09-10-public-package-manifest.zh.md b/.agents/notes/implemented/architecture/2026-09-10-public-package-manifest.zh.md
index 7dc03e5160..e4695b1ca7 100644
--- a/.agents/notes/implemented/architecture/2026-09-10-public-package-manifest.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-10-public-package-manifest.zh.md
@@ -14,7 +14,7 @@ Status: implemented
运行时要求位于顶层 `engines`:`dsh`、`node` 和 `npm` 均为可选版本字符串,也允许其他 engine 名称。`dsh.manifestVersion` 标识声明格式 `1`。当前安装器和加载器不强制检查格式与 DSH 兼容性声明。
-镜像打包器拥有 `configTrees`,工作区目录生成器拥有 Session 迁移声明。这些内部工具仍可读取既有磁盘字段,但公共 manifest 类型不暴露这些字段。此范围细化了[共享声明归属决策](2026-09-05-package-manifest-types.zh.md),后者的包位置与依赖规则仍然有效。
+镜像打包器拥有 `configTrees`,工作区目录生成器拥有 Session 迁移声明。这些内部工具仍可读取既有磁盘字段,但公共 manifest 类型不暴露这些字段。
各消费方负责 JSON 解析、字段校验、默认值解析和运行时数据适配。接口不会校验已解析的 JSON。只有多个消费方需要相同校验或归一化时,helper 才属于共享包;重复属性访问的 getter 不提供共享策略。
diff --git a/.agents/notes/implemented/architecture/2026-09-10-windows-native-installer-pages.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-10-windows-native-installer-pages.i18n.yaml
deleted file mode 100644
index 30d6654c2d..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-10-windows-native-installer-pages.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-10-windows-native-installer-pages.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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-windows-native-installer-pages.md
-/agent-note-native-windows-installer-pages:
- en: 2170ba78b248756f
- zh: 6e5852f4685fbc81
-/agent-note-native-windows-installer-pages/problem:
- en: 5d148a332b151928
- zh: a79cdfd0f1556c58
-/agent-note-native-windows-installer-pages/decision:
- en: 3357e9aca4862152
- zh: c2fa86534f9c8737
-/agent-note-native-windows-installer-pages/alternatives-considered:
- en: a5b1e11a1b30ab99
- zh: 8495f05dc87b831a
-/agent-note-native-windows-installer-pages/consequences:
- en: 16243ca0777d1ff8
- zh: e35cb7f33a756b47
diff --git a/.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.i18n.yaml
deleted file mode 100644
index 65fd19d68d..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-11-desktop-electron-node-runtime.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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
-/agent-note-use-electron-as-the-desktop-node-runtime:
- en: 06290013c52b169d
- zh: 5e876829cf708f34
-/agent-note-use-electron-as-the-desktop-node-runtime/problem:
- en: 67e580ac6d9298d9
- zh: bb2bd7486090bde4
-/agent-note-use-electron-as-the-desktop-node-runtime/decision:
- en: 3cac5d877b18179d
- zh: 0376bce7b099e0a6
-/agent-note-use-electron-as-the-desktop-node-runtime/consequences:
- en: b407d0ce648beea5
- zh: 7ab329aa1d7a237c
-/agent-note-use-electron-as-the-desktop-node-runtime/alternatives-considered:
- en: 880e255d56b45345
- zh: 5952d9685c564284
diff --git a/.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.md b/.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.md
deleted file mode 100644
index 71516bb930..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Agent Note: Use Electron as the Desktop Node runtime
-
-Status: implemented
-
-English | [中文](2026-09-11-desktop-electron-node-runtime.zh.md)
-
-## Problem
-
-Shipping an upstream Node executable alongside Electron duplicates the JavaScript runtime. Desktop needs one runtime for its Host and package scripts without requiring users to install Node.
-
-## Decision
-
-Desktop runs the shared Web Host and bundled pnpm through its own Electron executable with `ELECTRON_RUN_AS_NODE=1`. It ships no separate upstream Node executable. The target Electron distribution supplies both the packaging input and the runtime used to prepare and verify production dependencies; release metadata records its actual Node version. Development uses the installed Electron distribution.
-
-This supersedes the separate-Node choice in the [packaging decision](2026-08-25-electron-desktop-packaging-and-updates.md) and [bundled-runtime decision](2026-09-08-desktop-bundled-runtime-and-external-plugins.md). Their independent plugin storage and ordinary resource-directory layout remain applicable. The [Web wrapper](2026-09-10-desktop-web-wrapper.md) retains the shared profile runner and HTTP transport.
-
-## Consequences
-
-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.
-
-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.
-
-## Alternatives considered
-
-A separate Node executable decouples the Host from Electron's runtime but adds another binary, download, signature, and version selection. Electron RunAsNode removes that duplication. Moving production packages into ASAR is a separate change involving native modules, package resolution, and subprocess paths; the Host continues to load ordinary resource files.
diff --git a/.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.zh.md b/.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.zh.md
deleted file mode 100644
index 162908392b..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.zh.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Agent Note: 使用 Electron 作为 Desktop 的 Node 运行时
-
-Status: implemented
-
-[English](2026-09-11-desktop-electron-node-runtime.md) | 中文
-
-## 问题
-
-在 Electron 之外携带上游 Node 可执行文件会重复分发 JavaScript 运行时。Desktop 需要让 Host 和包脚本共用一个运行时,无需用户安装 Node。
-
-## 决策
-
-Desktop 通过自己的 Electron 可执行文件运行共享 Web Host 和内置 pnpm,并设置 `ELECTRON_RUN_AS_NODE=1`。应用不携带独立的上游 Node 可执行文件。目标 Electron 分发包同时作为打包输入,以及准备和验证生产依赖的运行时;发布元数据记录其实际 Node 版本。开发模式使用已安装的 Electron 分发包。
-
-此决策替代[打包决策](2026-08-25-electron-desktop-packaging-and-updates.zh.md)和[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)中的独立 Node 选择。独立插件存储和普通资源目录布局仍然适用。[Web 薄壳](2026-09-10-desktop-web-wrapper.zh.md)保留共享 profile runner 和 HTTP 传输。
-
-## 后果
-
-Host 和 pnpm 启动时传入 `--expose-internals`:内置 Cordis 加载器使用 Node 内部 ESM 加载器,而其原生 builtin 访问器无法在 Electron 44 中找到所需符号。显式参数使加载器可用,无需修改 Cordis。RunAsNode fuse 保持启用。
-
-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。
-
-## 考虑过的替代方案
-
-独立 Node 可执行文件可以让 Host 与 Electron 运行时分离,但会增加另一份二进制文件、下载、签名和版本选择。Electron RunAsNode 消除这一重复。将生产包移入 ASAR 是另一项涉及原生模块、包解析和子进程路径的改动;Host 继续加载普通资源文件。
diff --git a/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.i18n.yaml
deleted file mode 100644
index 5879ae942a..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-11-node-office-kit.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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-node-office-kit.md
-/agent-note-node-office-conversion-with-independently-packaged-engines:
- en: 04142abd8eef58bc
- zh: 1a2fe733f74e88ef
-/agent-note-node-office-conversion-with-independently-packaged-engines/problem:
- en: ac8ace90fa115f0d
- zh: 18b9977031cbadeb
-/agent-note-node-office-conversion-with-independently-packaged-engines/decision:
- en: 61d2e96b5b3782c5
- zh: 49780b672e681b0e
-/agent-note-node-office-conversion-with-independently-packaged-engines/alternatives-considered:
- en: 70315a46124cbde7
- zh: ee258ed0ef7effb0
-/agent-note-node-office-conversion-with-independently-packaged-engines/consequences:
- en: bb32631217be9743
- zh: 1ea86613ade03732
diff --git a/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.i18n.yaml
index 552e27d0a6..90c7dfd954 100644
--- a/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.i18n.yaml
@@ -9,11 +9,11 @@
en: bd3ad5505204ec0b
zh: 770b6089c760d99f
/agent-note-plugin-owned-message-projections/decision:
- en: 2b33d4a1cfba0f53
- zh: 55347f3ecf234862
+ en: 50c02960b1f087c6
+ zh: fa3f5c18b897eab9
/agent-note-plugin-owned-message-projections/alternatives-considered:
- en: c6e4c770739dca68
- zh: 7bfcb3dda06a22a1
+ en: b0da1fbf24bc506a
+ zh: c4e35896599f824c
/agent-note-plugin-owned-message-projections/consequences:
en: c2f3ff0b9639bec5
zh: 357f90d9a3423751
diff --git a/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.md b/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.md
index 9e1eca9256..739d27644b 100644
--- a/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.md
+++ b/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.md
@@ -16,7 +16,7 @@ The `@messageProjection` tag on the owning `SessionEventMap` member generates th
The compaction-image-offload plugin registers its definition through a fiber-owned `ctx.sessions.registerMessageProjection()` effect. Each event has one registered owner. Removing a definition invalidates pending committed decisions and cached reads that used it; a replacement definition requires restoring the session. Rejected, uncommitted candidates retain no interpreter dependency.
-Detached readers pass definitions explicitly. The current Session format catalog assembles the same browser-safe plugin exports for persistence validation and offline queries. This static assembly does not mount recovery listeners. Live sessions use the registered composition, while compaction invariants borrow that composition's definitions. No process-global registry or import-time registration is involved.
+Detached readers pass definitions explicitly. The current Session format catalog assembles the same browser-safe plugin exports for persistence validation and offline queries. This static assembly does not mount recovery listeners. Live sessions use the registered composition. No process-global registry or import-time registration is involved.
The image event payload and required-on-read semantics remain owned by the image-offload note. Its indexes, selection policy, retry behavior, and SDK recordings are unchanged; this decision partially supersedes that note's core-owned interpretation.
@@ -26,7 +26,7 @@ The image event payload and required-on-read semantics remain owned by the image
**Use the ordinary session-projection registry.** Its folds expose domain state after commit. They cannot veto an invalid append or supply canonical `deriveMessages()` content without another integration point.
-**Transform only the outgoing request.** Compaction, fork, offline replay, and request invariants also derive messages. A send-time transformation does not cover these consumers.
+**Transform only the outgoing request.** Compaction, fork, and offline replay also derive messages. A send-time transformation does not cover these consumers.
## Consequences
diff --git a/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.zh.md b/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.zh.md
index b26b5d4549..74c85825bf 100644
--- a/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-11-plugin-owned-message-projections.zh.md
@@ -16,7 +16,7 @@ Status: implemented
compaction-image-offload 插件通过 fiber 拥有的 `ctx.sessions.registerMessageProjection()` effect 注册处理器。每种事件只有一个注册者。移除处理器后,尚未应用的已提交决策和使用过它的缓存读取都会失效。替换处理器需要重新恢复会话。被拒绝且未提交的候选事件不会保留对处理器的依赖。
-独立读取器显式传入处理器。当前 Session 格式目录装配相同的浏览器安全插件导出,供持久数据校验和离线查询使用。这项静态装配不挂载恢复监听器。实时会话使用已注册的组合,压缩 invariant 借用该组合的处理器。不使用进程全局注册表,也不在导入模块时注册。
+独立读取器显式传入处理器。当前 Session 格式目录装配相同的浏览器安全插件导出,供持久数据校验和离线查询使用。这项静态装配不挂载恢复监听器。实时会话使用已注册的组合。不使用进程全局注册表,也不在导入模块时注册。
图片事件载荷和读取时必须识别的语义仍由图片省略记录负责。索引、选图策略、重试行为和 SDK 录制保持不变。本决策部分取代该记录中由核心负责解释事件的职责划分。
@@ -26,7 +26,7 @@ compaction-image-offload 插件通过 fiber 拥有的 `ctx.sessions.registerMess
**使用普通的 session-projection 注册表。** 它的折叠在提交后公开领域状态。没有额外集成点,它无法否决无效追加,也无法提供标准 `deriveMessages()` 内容。
-**只转换发出的请求。** 压缩、分叉、离线回放和请求 invariant 也会派生消息。发送时转换无法覆盖这些消费方。
+**只转换发出的请求。** 压缩、分叉和离线回放也会派生消息。发送时转换无法覆盖这些消费方。
## 影响
diff --git a/.agents/notes/implemented/architecture/2026-09-11-posix-ssh-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-11-posix-ssh-runtime.i18n.yaml
index 423de99127..6c236fcf0f 100644
--- a/.agents/notes/implemented/architecture/2026-09-11-posix-ssh-runtime.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-11-posix-ssh-runtime.i18n.yaml
@@ -9,8 +9,11 @@
en: f67f6add03a36e85
zh: 656be288f8b2e1d2
/agent-note-posix-ssh-execution-providers/decision:
- en: b2d1e370d008f0f3
- zh: 3e98919538d3fb78
+ en: bc22949098fc3028
+ zh: 76618180bc0bac56
+/agent-note-posix-ssh-execution-providers/decision/helper-deployment:
+ en: c479fdb58ed617f1
+ zh: 17cfefa778e3fd71
/agent-note-posix-ssh-execution-providers/alternatives-considered:
en: 4efffb3edba91689
zh: 40100a70e5f55a42
@@ -18,8 +21,8 @@
en: f29359ab89feb084
zh: db7505b8d4a5351a
/agent-note-posix-ssh-execution-providers/verification:
- en: 822c7c029a77f3c0
- zh: 4a2eaaf3372cba8b
+ en: ee72e88fd40d6769
+ zh: 56f0e0c8f3365973
/agent-note-posix-ssh-execution-providers/deferred-work:
en: f300f112c9a09f7e
zh: b4708a0b7f596420
diff --git a/.agents/notes/implemented/architecture/2026-09-11-posix-ssh-runtime.md b/.agents/notes/implemented/architecture/2026-09-11-posix-ssh-runtime.md
index d54600faa6..3bd1c340d1 100644
--- a/.agents/notes/implemented/architecture/2026-09-11-posix-ssh-runtime.md
+++ b/.agents/notes/implemented/architecture/2026-09-11-posix-ssh-runtime.md
@@ -22,12 +22,20 @@ Each stream receives a fresh 256-bit TLS pre-shared key through private administ
Collected stdout and stderr carry bounded tail snapshots through handlers that only update output observations. Capture continues while a snapshot is waiting for transport. Final snapshots preserve raw-byte offsets, and completed spill files use the local provider’s retained-output storage after connection disposal.
-Readiness verifies the installed helper digest and, when PTC is configured, the installed Node bootstrap digest. These checks pin expected deployment artifacts; they do not authenticate a malicious remote operating system. The helper disables Node debugger activation through `SIGUSR1`: file-effect confinement can still permit same-user signals, which must not expose the helper's unrestricted filesystem and process services. File-effect confinement delegates to the remote local sandbox provider and retains its full/partial disclosure and platform limitations.
+Readiness verifies the installed helper script or executable digest and, when a script deployment supplies a PTC bootstrap, that bootstrap's digest. Executable deployments embed the PTC worker and require no separate bootstrap. These checks pin expected deployment artifacts; they do not authenticate a malicious remote operating system. The helper disables Node debugger activation through `SIGUSR1`: file-effect confinement can still permit same-user signals, which must not expose the helper's unrestricted filesystem and process services. File-effect confinement delegates to the remote local sandbox provider and retains its full/partial disclosure and platform limitations.
-Path canonicalization belongs where the files exist. The shared policy resolver preserves absolute execution-world spelling; enforcing providers resolve symlinks and `..` on their own filesystem. Headless records and validates cwd through `ctx.fs`. Host-path projection remains unavailable for SSH, so Node execution requires an explicitly installed remote bootstrap.
+Path canonicalization belongs where the files exist. The shared policy resolver preserves absolute execution-world spelling; enforcing providers resolve symlinks and `..` on their own filesystem. Headless records and validates cwd through `ctx.fs`. Host-path projection remains unavailable for SSH; PTC uses an explicitly installed remote bootstrap for script deployments or the helper executable's embedded worker.
A lost connection invalidates pending operations without reconnect or replay. Helper EOF, signals and a heartbeat lease initiate remote native cleanup. The client cannot turn lease expiry into an observed successful termination: an interrupted mutation or launch can have an unknown outcome. Administrative request deadlines, consumer execution deadlines and cleanup remain separate owners.
+### Helper deployment
+
+The explicit `launch.kind` selects remote `node-script` or `executable` invocation independently of the local Harness carrier. A local `process.pkg` flag cannot describe the remote installation. Protocol 2 includes the remote runtime kind in its readiness response and rejects protocol or launch-kind mismatches, so a client cannot apply script bootstrap rules to an embedded worker. Client and helper protocol upgrades must be deployed together.
+
+The executable embeds Node, the helper, managed subprocess runners and the PTC worker. It reuses the [SDK executable packaging decision](2026-07-10-single-file-executable-sdk-runtime-distribution.md): `pkg --sea` preserves a package tree and dynamic module resolution instead of maintaining a separate static plugin registry. That record owns the comparison with bare Node SEA. The embedded runtime serves these private entry points; project commands still depend on the remote machine's development tools.
+
+The four release targets cover Linux and macOS on x64 and arm64, matching the POSIX providers and their supported native resources. Packaging does not add Windows or musl support. Landlock and macOS PTY launchers remain real files beside the executable because the operating system cannot execute paths inside pkg's virtual filesystem. The installation unit is therefore the complete archive directory, with native resources and integrity metadata, rather than the executable alone. The [helper runtime reference](../../../../packages/ssh/ssh-helper-runtime/README.md) owns the build matrix, deployment baselines and archive verification commands.
+
## Alternatives considered
**Frame every stream over SSH exec stdout/stdin.** This avoids Unix-socket forwarding, but requires per-stream credits, queue bounds and independent EOF semantics inside the application protocol. Independent SSH channels use the transport’s existing flow control and keep program output away from administrative framing.
@@ -48,7 +56,7 @@ The portable-consumer decision remains active; this note supplies its SSH realiz
## Verification
-Required protocol and lifecycle evidence covers malformed frames, bounds, reservation cancellation, authenticated stream publication and disconnect errors. Native SSH acceptance must exercise remote file guards and symlink identity, Bash confinement, fd 7 binary traffic, control progress under paused output, terminal operations, LSP and Node execution. Live checks require an explicitly configured disposable remote workspace; keyless tests do not provision one.
+Required protocol and lifecycle evidence covers malformed frames, bounds, reservation cancellation, authenticated stream publication and disconnect errors. Native SSH acceptance must exercise remote file guards and symlink identity, Bash confinement, fd 7 binary traffic, control progress under paused output, terminal operations, LSP and Node execution. External-host checks require an explicitly configured disposable remote workspace. Packaged Linux acceptance provisions a disposable OpenSSH container without Node; native-host checks own confinement evidence.
Security evidence requires both a same-user connector and a replaced socket listener that cannot claim a reserved stream, learn its key, alter another run or receive its plaintext output. Process-lifecycle evidence distinguishes direct exit from managed-range quiescence and tests cancellation both before launch publication and after the payload starts. Source and built profile checks verify the installed helper/bootstrap arrangement without transferring host credentials to program environments.
diff --git a/.agents/notes/implemented/architecture/2026-09-11-posix-ssh-runtime.zh.md b/.agents/notes/implemented/architecture/2026-09-11-posix-ssh-runtime.zh.md
index a6b3a84c37..5ad2d3c471 100644
--- a/.agents/notes/implemented/architecture/2026-09-11-posix-ssh-runtime.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-11-posix-ssh-runtime.zh.md
@@ -22,12 +22,20 @@ SSH 提供经过认证的字节通道及逐通道流量控制,但普通 exec
收集的 stdout 与 stderr 通过仅更新输出观测的处理器传递有界尾部快照。快照等待传输时,输出捕获继续进行。最终快照保留原始字节偏移,已完成的 spill 文件在连接释放后使用本地提供方的保留输出存储。
-就绪流程验证已安装辅助程序的摘要,并在配置 PTC 时验证已安装 Node 引导程序的摘要。这些检查固定预期部署产物,不用于认证恶意远端操作系统。辅助程序禁用通过 `SIGUSR1` 启动 Node 调试器:文件效果限制仍可能允许同用户信号,这些信号不得暴露辅助程序不受限的文件系统和进程服务。文件效果限制委托给远端本地沙箱提供方,保留其完整/部分执行披露及平台限制。
+就绪流程验证已安装辅助脚本或可执行文件的摘要;脚本部署提供 PTC 引导程序时,还验证该引导程序的摘要。可执行文件部署内嵌 PTC 工作进程,无需单独的引导程序。这些检查固定预期部署产物,不用于认证恶意远端操作系统。辅助程序禁用通过 `SIGUSR1` 启动 Node 调试器:文件效果限制仍可能允许同用户信号,这些信号不得暴露辅助程序不受限的文件系统和进程服务。文件效果限制委托给远端本地沙箱提供方,保留其完整/部分执行披露及平台限制。
-路径规范化属于文件实际存在的位置。共享策略解析器保留执行环境中的绝对路径写法;执行限制的提供方在自己的文件系统上解析符号链接与 `..`。headless 通过 `ctx.fs` 记录和验证 cwd。SSH 不提供主机路径投影,因此 Node 执行需要显式安装的远端引导程序。
+路径规范化属于文件实际存在的位置。共享策略解析器保留执行环境中的绝对路径写法;执行限制的提供方在自己的文件系统上解析符号链接与 `..`。headless 通过 `ctx.fs` 记录和验证 cwd。SSH 不提供主机路径投影;PTC 在脚本部署中使用显式安装的远端引导程序,在可执行文件部署中使用辅助程序内嵌的工作进程。
连接丢失会使待处理操作失效,不进行重连或重放。辅助进程 EOF、信号及心跳租期会启动远端原生清理。客户端不能把租期到期当作观察到的成功终止:被中断的修改或启动可能结果未知。管理请求截止时限、消费方执行截止时限及清理分别由各自归属方负责。
+### 辅助程序部署
+
+显式的 `launch.kind` 选择远端的 `node-script` 或 `executable` 调用方式,与本地 Harness 的运行载体相互独立。本地 `process.pkg` 标志无法描述远端安装。协议 2 在就绪响应中包含远端运行时类型,并拒绝协议版本或启动类型不匹配,避免客户端把脚本引导规则用于内嵌工作进程。客户端与辅助程序的协议升级必须一起部署。
+
+可执行文件内嵌 Node、辅助程序、托管子进程运行器与 PTC 工作进程。它复用 [SDK 可执行文件打包决策](2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md):`pkg --sea` 保留包目录树与动态模块解析,无需维护独立的静态插件注册表。该记录负责与裸 Node SEA 的方案比较。内嵌运行时服务于这些私有入口;项目命令仍依赖远端机器的开发工具。
+
+四个发布目标覆盖 Linux 和 macOS 的 x64 与 arm64,对应 POSIX 提供方及其支持的原生资源。打包不会增加 Windows 或 musl 支持。Landlock 与 macOS PTY 启动器保留为可执行文件旁的真实文件,因为操作系统无法执行 pkg 虚拟文件系统中的路径。因此,安装单元是包含原生资源与完整性元数据的完整归档目录,而非单个可执行文件。[辅助程序运行时参考](../../../../packages/ssh/ssh-helper-runtime/README.zh.md)负责构建矩阵、部署基线及归档验证命令。
+
## 考虑过的替代方案
**在 SSH exec stdout/stdin 上为所有流分帧。** 这避免了 Unix 套接字转发,但要求应用协议自行实现逐流额度、队列上限及独立 EOF 语义。独立 SSH 通道使用传输层既有流控,并将程序输出与管理帧分开。
@@ -48,7 +56,7 @@ SSH 提供经过认证的字节通道及逐通道流量控制,但普通 exec
## 验证
-必要的协议及生命周期证据覆盖畸形消息、上限、预留取消、经过认证的流发布及断连错误。原生 SSH 验收必须覆盖远端文件保护与符号链接身份、Bash 限制、fd 7 二进制流、输出暂停时的控制进展、终端操作、LSP 及 Node 执行。实时检查要求显式配置的可丢弃远端工作区;无密钥测试不创建远端环境。
+必要的协议及生命周期证据覆盖畸形消息、上限、预留取消、经过认证的流发布及断连错误。原生 SSH 验收必须覆盖远端文件保护与符号链接身份、Bash 限制、fd 7 二进制流、输出暂停时的控制进展、终端操作、LSP 及 Node 执行。外部主机检查要求显式配置的可丢弃远端工作区。打包产物的 Linux 验收会创建未安装 Node 的可丢弃 OpenSSH 容器;原生主机检查负责限制执行的证据。
安全证据要求同时尝试同用户连接与替换套接字监听端点,并确认其无法占用预留流、获知流密钥、修改另一执行或收到其明文输出。进程生命周期证据区分直接退出与托管进程范围静止,并测试启动发布前及程序启动后的取消。源码与构建后配置组合检查验证已安装辅助程序/引导安排,不向程序环境传递主机凭据。
diff --git a/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.i18n.yaml
index 834d15576d..d7f60ead90 100644
--- a/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.i18n.yaml
@@ -9,17 +9,17 @@
en: 66ac3d7ce7eb8e15
zh: abb9d41b71900bbf
/agent-note-sandboxed-node-execution-for-ptc/decision:
- en: cf072f53de09faca
- zh: 9edd5c4a2bfaae5a
+ en: d6b9c22603a4f933
+ zh: f324b91830078d9a
/agent-note-sandboxed-node-execution-for-ptc/decision/resolved-inputs-and-policy:
- en: e93df995efab569a
- zh: 850daf02d1207028
+ en: 3aef37bb4f8a8e53
+ zh: a7b477324d486ab3
/agent-note-sandboxed-node-execution-for-ptc/decision/control-and-lifetime:
en: 2c7a694b04951772
zh: 133b95b0477b127f
/agent-note-sandboxed-node-execution-for-ptc/decision/resource-limits:
- en: 84c73cf3ff33bc03
- zh: a228080683d6252c
+ en: 3f1d8a30f6d48243
+ zh: faa7c01d7c959893
/agent-note-sandboxed-node-execution-for-ptc/alternatives-considered:
en: 8ffcc2b32a2243d7
zh: 81d48d38947abc76
diff --git a/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.md b/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.md
index f851775460..1259a617ad 100644
--- a/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.md
+++ b/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.md
@@ -12,7 +12,7 @@ The [PTC foundation](../feature/2026-06-15-ptc.md) remains responsible for regis
## Decision
-`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.
+`dsh-ptc-runtime-node` in its default `per-call` mode 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 after a per-call execution.
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.
@@ -22,7 +22,7 @@ The host preserves `ELECTRON_RUN_AS_NODE` for child startup; the bootstrap remov
File mode, observed denial and enforcement completeness travel in `PtcRunResult.sandbox` separately from the program outcome. Restricted execution fails if the required sandbox backend cannot launch. Full access is an explicit policy mode. Program success does not prove full enforcement, and neither the `process` descriptor nor the extra control pipe claims multi-tenant isolation.
-The private Python provider keeps its existing execution implementation and configured wall deadline. Its resolver accepts cwd but rejects an explicit file policy or per-call timeout override; a capability descriptor never silently grants unsupported protection.
+The [experimental Python provider](../../../../packages/experimental/ptc-runtime-python/README.md) shares the resolved file policy and sandbox diagnostics while retaining its local Unix execution and configured wall deadline. Per-call timeout overrides remain unsupported.
### Control and lifetime
@@ -34,6 +34,8 @@ Program completion, timeout, cancellation and protocol failure all close executi
The default elapsed deadline is 120 seconds, capped at 600 seconds by default. Trusted service consumers may request `timeoutMs: null` to disable this timer; [workflow sandbox reuse](2026-09-13-workflow-ptc-sandbox-reuse.md) owns that caller-controlled lifetime. Omitted and numeric requests, including model-facing `run_code`, retain the numeric defaults and caps. An enabled deadline includes runtime setup and nested tool or approval waits. V8 old-generation memory, serialized outer output and control traffic have separate configured bounds. The heap limit excludes native allocations and descendant memory, and elapsed time is not a process-tree CPU budget.
+The opt-in [in-memory execution mode](2026-09-23-best-effort-persistent-ptc.md) retains disposable memory under the same file policy; [retained-process permissions](2026-10-08-persistent-ptc-sandbox.md) govern policy changes and temporary escalation.
+
## Alternatives considered
**Keep the worker and add tool checks.** Tool checks cannot intercept direct Node imports or establish OS confinement. Keeping a worker inside a confined supervisor process preserves worker metering but adds another execution lifetime without supplying a process-tree CPU limit.
diff --git a/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.zh.md b/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.zh.md
index 94f3a37eb7..0074fdf578 100644
--- a/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.zh.md
@@ -12,7 +12,7 @@ Node worker 隔离 JavaScript 状态,但不应用调用 Session 的 OS 沙箱
## 决策
-`dsh-ptc-runtime-node` 在一个全新 Node 进程中运行每个程序。Host 解析执行选择,通过与 Bash 相同的 `ctx.sandbox` 提供方约束启动,并将进程生命周期交给 `ctx.subprocess`。子进程以直接 Node API、空模型环境和 Host 提供的异步绑定求值可擦除 TypeScript。本提供方不保留 worker 或持久内核。
+`dsh-ptc-runtime-node` 的默认 `per-call` 模式在一个全新 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 二进制文件;普通运行时测试无需此依赖即可覆盖环境过滤。
@@ -22,7 +22,7 @@ Host 在子进程启动时保留 `ELECTRON_RUN_AS_NODE`;bootstrap 在求值前
文件模式、观察到的拒绝与强制完整性通过 `PtcRunResult.sandbox` 独立于程序结果传递。所需沙箱后端无法启动时,受限执行失败。完整访问是显式策略模式。程序成功不证明完整强制能力,`process` 描述符与额外控制管道也不声明多租户隔离。
-私有 Python 提供方保留现有执行实现与配置的经过时间截止。其解析器接受 cwd,但拒绝显式文件策略或单次 timeout 覆盖;能力描述符不会静默授予不受支持的保护。
+[实验性 Python 提供方](../../../../packages/experimental/ptc-runtime-python/README.zh.md)共享已解析文件策略与沙箱诊断,同时保留本地 Unix 执行和配置的经过时间截止。仍不支持单次 timeout 覆盖。
### 控制与生命周期
@@ -34,6 +34,8 @@ Host 在子进程启动时保留 `ELECTRON_RUN_AS_NODE`;bootstrap 在求值前
默认经过时间截止为 120 秒,默认上限为 600 秒。可信服务消费方可以请求 `timeoutMs: null` 来禁用该定时器;[工作流沙箱复用](2026-09-13-workflow-ptc-sandbox-reuse.zh.md)负责这种由调用方控制的生命周期。省略 timeout 或使用数值的请求(包括面向模型的 `run_code`)保留数值默认值与上限。启用的截止包括运行时准备以及嵌套工具或审批等待。V8 老生代内存、序列化外层输出与控制通信具有独立配置的上限。堆限制不包含原生分配和后代内存,经过时间也不是进程树 CPU 预算。
+显式启用的[内存保留执行模式](2026-09-23-best-effort-persistent-ptc.zh.md)在相同文件策略下保留可丢弃内存;[常驻进程权限](2026-10-08-persistent-ptc-sandbox.zh.md)负责策略切换和临时提权。
+
## 考虑过的替代方案
**保留 worker 并增加工具检查。** 工具检查不能拦截直接 Node 导入,也不能建立 OS 约束。在受限监督进程中保留 worker 可以保留 worker 计量,但会增加一层执行生命周期,仍不能提供进程树 CPU 限制。
diff --git a/.agents/notes/implemented/architecture/2026-09-11-windows-directory-installation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-11-windows-directory-installation.i18n.yaml
deleted file mode 100644
index 35bfbf6d4b..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-11-windows-directory-installation.i18n.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-11-windows-directory-installation.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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-windows-directory-installation.md
-/agent-note-replace-windows-application-directories-after-staging:
- en: 7cdd071dd6ad760f
- zh: eb8510c6c09f0068
-/agent-note-replace-windows-application-directories-after-staging/problem:
- en: fa0015dca8cd9e85
- zh: a826baa59c0a3bd1
-/agent-note-replace-windows-application-directories-after-staging/decision:
- en: 5bec9a233d21bb35
- zh: 50e8ba778b14b48e
-/agent-note-replace-windows-application-directories-after-staging/consequences:
- en: 3562fbc117967fb3
- zh: 6600b28ea6202f68
-/agent-note-replace-windows-application-directories-after-staging/alternatives-considered:
- en: 0971baacd045ddef
- zh: c908624104e242f9
-/agent-note-replace-windows-application-directories-after-staging/verification:
- en: 650fdccdf35ad9d3
- zh: 6069b9acb5f03878
diff --git a/.agents/notes/implemented/architecture/2026-09-12-ptc-runtime-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-12-ptc-runtime-vocabulary.i18n.yaml
deleted file mode 100644
index ac5f77829d..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-12-ptc-runtime-vocabulary.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-12-ptc-runtime-vocabulary.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-12-ptc-runtime-vocabulary.md
-/agent-note-ptc-runtime-vocabulary:
- en: 4e585bb88c918c48
- zh: 1ee36889df928e84
-/agent-note-ptc-runtime-vocabulary/problem:
- en: 1281bbda56bc41e0
- zh: 719dc99572bd7921
-/agent-note-ptc-runtime-vocabulary/decision:
- en: b3ca07cb6a92f358
- zh: f3b763124b483563
-/agent-note-ptc-runtime-vocabulary/alternatives-considered:
- en: 38af2c476cc408bc
- zh: 9f1ebd14829250e3
-/agent-note-ptc-runtime-vocabulary/consequences:
- en: bb68774fb0a4bcc3
- zh: b866e8763b3a4f74
diff --git a/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.i18n.yaml
new file mode 100644
index 0000000000..49a9f432ee
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-09-13-session-working-directory.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-13-session-working-directory.md
+/agent-note-session-working-directory:
+ en: 4d5b74f6b55a2270
+ zh: 8323ea3fbdf8fd8d
+/agent-note-session-working-directory/problem:
+ en: 31fba384d90ce7d2
+ zh: f8b814868f3280c3
+/agent-note-session-working-directory/decision:
+ en: 9ccde1c5e635b9c0
+ zh: 4dc76229703d3ef7
+/agent-note-session-working-directory/alternatives-considered:
+ en: f0995feb9b6bcce8
+ zh: 97417085fd17f70f
+/agent-note-session-working-directory/consequences:
+ en: 2dd3a4de72ee6d74
+ zh: be5d8a80c8eaac6e
diff --git a/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.md b/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.md
new file mode 100644
index 0000000000..8151c0e85a
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.md
@@ -0,0 +1,47 @@
+# Agent Note: Session working directory
+
+Status: implemented
+
+English | [中文](2026-09-13-session-working-directory.zh.md)
+
+## Problem
+
+A fixed Session creation directory cannot represent a task that changes checkout during its conversation. Independent tools resolving paths from different sources can read and write different projects, while changing the process directory also affects unrelated Sessions. Putting mutable directory information in the system prompt changes the reusable prompt prefix.
+
+## Decision
+
+`dsh-working-directory` owns one logged effective directory per Session. Its projection initializes from the immutable original project and folds `working-directory/change`; consumers read that owner instead of deriving execution paths from the header or sandbox root. Directory changes validate through the filesystem provider, serialize within the Session, and publish only after the new directory is available.
+
+The `dsh-base` composition mounts `working_directory({ cd? })` globally. The tool reads or changes the Session directory, so every Agent preset can select or recover it, including presets without file-editing tools. The Web composition keeps action tools in preset scopes.
+
+Required user-context snapshots carry the current value; each committed change attempts to queue a notice through the ordinary Agent inbox. Cancellation or disposal may discard unadmitted notices while the directory event survives for subsequent requests. The original project is the recovery destination when the selected directory disappears. Missing recovery destinations produce an error. Write permissions remain independent.
+
+Subagents capture the parent's current directory or an explicit override and commit their selected directory after inherited history. Existing processes, terminals, and persistent shells keep their process-local directory. Project instructions, skills, and completion follow the effective directory without changing Session identity, grouping, or runtime composition.
+
+Live Session skill catalogs and file completion validate through the same directory owner. These reads can commit recovery and queue its notice before discovery, keeping discovered files and subsequent operations tied to the committed Session directory. Cold skill catalogs read the recorded projection without activating an Agent.
+
+Web sidebar terminal environment lookup reads the effective directory without filesystem validation or recovery, so a missing directory does not prevent reconnecting to a retained terminal. New terminal creation validates and recovers through the directory owner before spawning; the process keeps that directory independently of later Session changes.
+
+Playwright MCP and Chrome DevTools MCP validate the effective directory when acquiring a Session's MCP connection and use it to launch the server. Later directory changes do not restart the retained server or change its process directory.
+
+File links and command directory labels use result metadata captured when each operation runs, so historical rows remain correct after later directory changes.
+
+The [prompt-variables note](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) retains strict interpolation, route-variable ownership, and tool-guidance ownership. The [environment-suffix note](../../archived/bug-fix/2026-09-06-environment-prompt-suffix.md) retains ordered prefix/suffix placement for deployment, Harness source, and Web guidance. This decision owns directory state and placement in user context.
+
+## Alternatives considered
+
+**Rewrite the Session header.** The header is immutable identity metadata used by storage, grouping, and permission roots. Rewriting it would conflate current execution state with the original project.
+
+**Let worktree tools own directory state.** Ordinary directory changes, filesystem tools, and subagents need the same value. A Git-specific owner would duplicate selection and recovery.
+
+**Change the process directory or synchronize every shell.** Concurrent Sessions and existing subprocesses have independent lifetimes. A Session change affects new directory-based operations, while a shell's `cd` remains local.
+
+**Keep directory text in the system prompt.** Mutable operational context belongs after retained history, using the same user-context mechanism as permission state.
+
+## Consequences
+
+The runtime exposes one directory owner and preserves the original project. Consumers migrate together, including filesystem, shell, instructions, skills, subagents, and both SDKs. Directory existence can change outside the harness, so validation and logged recovery occur at use. Required runtime context remains visible even when optional context is suppressed.
+
+`{{cwd}}` has no loop-provided value; custom personas that depend on that built-in fail strict interpolation. Deployments remove those references using the [system-prompt migration guidance](../../../../packages/core/system-prompt/README.md). When both the current directory and its recovery destination are unavailable, prompt assembly rejects model turns; the [directory service recovery guidance](../../../../packages/session/working-directory/README.md#known-limitations-and-deferred-work) requires restoring the directory or selecting an existing absolute directory through the SDK or host service.
+
+Verification covers independent Sessions, relative changes, replay, cancellation, recovery, tool disposal, user-context rendering, provider inheritance, and real directory-based operations.
diff --git a/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.zh.md b/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.zh.md
new file mode 100644
index 0000000000..46246a9d67
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.zh.md
@@ -0,0 +1,47 @@
+# Agent Note: Session working directory
+
+Status: implemented
+
+[English](2026-09-13-session-working-directory.md) | 中文
+
+## Problem
+
+固定的 Session 创建目录无法表示在对话中切换检出目录的任务。各工具从不同来源解析路径时,可能读写不同项目;改变进程目录还会影响其他 Session。把可变目录信息放入系统提示词会改变可复用的提示词前缀。
+
+## Decision
+
+`dsh-working-directory` 拥有每个 Session 的一个日志化有效目录。投影从不可变的原始项目初始化,并折叠 `working-directory/change`;消费者读取该服务,而不是从 header 或沙箱根目录派生执行路径。目录变更通过文件系统 provider 验证,在 Session 内串行执行,并只在新目录可用后发布。
+
+`dsh-base` 组合在全局挂载 `working_directory({ cd? })`。该工具读取或改变 Session 目录,使每个 Agent preset 都能选择或恢复它,包括没有文件编辑工具的 preset。Web 组合把操作工具放在 preset 作用域中。
+
+必需的用户上下文快照携带当前值;每次已提交变更都会尝试通过普通 Agent 收件箱排入通知。取消或销毁可能丢弃尚未进入请求的通知,但目录事件仍然保留,供后续请求使用。所选目录消失时恢复到原始项目。恢复目录也消失时返回错误。写权限保持独立。
+
+子代理捕获父代理当前目录或显式覆盖值,并在继承历史之后提交选定目录。已有进程、终端和持久 shell 保留进程局部目录。项目指令、技能和补全跟随有效目录,不改变 Session 身份、归属或运行时组合。
+
+活跃 Session 的技能目录与文件补全通过同一个目录 owner 验证目录。这些读取可在发现操作之前提交恢复并排入通知,使发现的文件与后续操作关联到已提交的 Session 目录。冷态技能目录读取已记录的投影,不激活 Agent。
+
+Web 侧栏终端的环境查询读取有效目录,不验证文件系统或执行恢复,因此目录缺失不会阻止重新连接保留的终端。新终端在创建进程前通过目录 owner 验证和恢复目录;后续 Session 目录变更不会改变该进程的目录。
+
+Playwright MCP 和 Chrome DevTools MCP 在获取 Session 的 MCP 连接时验证有效目录,并在该目录启动服务器。后续目录变更不会重启保留的服务器,也不会改变其进程目录。
+
+文件链接与命令目录标签使用各次操作执行时记录的结果元数据,因此后续目录变更不会使历史记录指向错误的位置。
+
+[提示词变量记录](2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md)保留严格插值、路由变量归属与工具指导归属。[环境后缀记录](../../archived/bug-fix/2026-09-06-environment-prompt-suffix.md)保留部署、Harness 源码与 Web 指导的有序前缀/后缀位置。本决策拥有目录状态及其用户上下文位置。
+
+## Alternatives considered
+
+**改写 Session header。** header 是存储、归属与权限根目录使用的不可变身份元数据。改写它会混淆当前执行状态与原始项目。
+
+**让 worktree 工具拥有目录状态。** 普通目录变更、文件系统工具和子代理需要同一个值。Git 专用 owner 会重复实现选择与恢复。
+
+**改变进程目录或同步所有 shell。** 并发 Session 与已有子进程具有独立生命周期。Session 变更影响新的目录相关操作,shell 内的 `cd` 则保持局部。
+
+**把目录文本留在系统提示词中。** 可变操作上下文应位于保留历史之后,复用权限状态的用户上下文机制。
+
+## Consequences
+
+运行时提供一个目录 owner,并保留原始项目。消费者一起迁移,包括文件系统、shell、指令、技能、子代理和两种 SDK。目录可能在 harness 之外消失,因此在使用时执行验证和日志化恢复。即使可选上下文被禁用,必需运行时上下文也仍然可见。
+
+`{{cwd}}` 没有循环提供的值;依赖该内置变量的自定义 persona 会触发严格插值错误。部署方按照[系统提示词迁移说明](../../../../packages/core/system-prompt/README.zh.md)移除这些引用。当前目录与恢复目标均不可用时,提示词组装会拒绝模型轮次;[目录服务恢复说明](../../../../packages/session/working-directory/README.zh.md#known-limitations-and-deferred-work)要求恢复目录,或通过 SDK 或 Host 服务选择现有的绝对目录。
+
+验证覆盖 Session 相互独立、相对路径变更、回放、取消、恢复、工具卸载、用户上下文渲染、provider 继承和真实目录操作。
diff --git a/.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.i18n.yaml
index 46f602667d..ccb643fcc4 100644
--- a/.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.i18n.yaml
@@ -9,8 +9,8 @@
en: 42235162e945e015
zh: 4edc5633720cd6cc
/agent-note-reuse-the-ptc-node-sandbox-for-workflows/decision:
- en: b55a2a59b8039cdd
- zh: fd6bbe987a49266f
+ en: 5112e6adc51b9c40
+ zh: 06277d1d753a9b6b
/agent-note-reuse-the-ptc-node-sandbox-for-workflows/alternatives-considered:
en: 56d9a25713227146
zh: 2cd89bf6ccc3b3bd
diff --git a/.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.md b/.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.md
index 9454ffef28..297ff20f3e 100644
--- a/.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.md
+++ b/.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.md
@@ -22,7 +22,7 @@ Progress uses one binding call at a time. The first batch starts synchronously;
The Node bootstrap keeps its control pipe open after sending the terminal frame until the host closes it. Unawaited binding replies may still be in flight, so eager child-side close would let an `EPIPE` race an already completed program.
-The [dynamic-workflows decision](../feature/2026-07-05-dynamic-workflows.md) retains the script, structured-output, event and tool semantics; this note supersedes only its execution substrate and trust realization. The [sandboxed Node PTC decision](2026-09-11-sandboxed-node-ptc-runtime.md) retains execution and control guarantees; the explicit null deadline extends its service options. The [agent-scope runtime design](2026-07-12-agent-scope-runtime-design.md#workflow-children-are-pending-starts-or-published-records) retains pending-start and child-cleanup ownership.
+The [dynamic-workflows reference](../../../../packages/workflow/workflow/README.md) retains the script, structured-output, event and tool semantics; this note supersedes only its execution substrate and trust realization. The [sandboxed Node PTC decision](2026-09-11-sandboxed-node-ptc-runtime.md) retains execution and control guarantees; the explicit null deadline extends its service options. The [agent-scope runtime design](../../../../packages/core/agent-loop/README.md) retains pending-start and child-cleanup ownership.
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.zh.md b/.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.zh.md
index c517060657..48dd4e4262 100644
--- a/.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.zh.md
@@ -22,7 +22,7 @@ VM 定义辅助 API,以及协作式并发、agent 总数和条目上限。它
Node 引导程序发送终态帧后保持控制管道打开,直到 Host 将其关闭。未被等待的绑定回复可能仍在传输,因此子进程提前关闭会让 `EPIPE` 与已经完成的程序结果发生竞争。
-[动态工作流决策](../feature/2026-07-05-dynamic-workflows.zh.md)保留脚本、结构化输出、事件与工具语义;本文只取代其执行基底与信任实现。[沙箱化 Node PTC 决策](2026-09-11-sandboxed-node-ptc-runtime.zh.md)保留执行与控制保证;显式 null 截止扩展其服务选项。[agent 作用域运行时设计](2026-07-12-agent-scope-runtime-design.zh.md#workflow-children-are-pending-starts-or-published-records)保留待启动与子 agent 清理的归属规则。
+[动态工作流参考](../../../../packages/workflow/workflow/README.zh.md)保留脚本、结构化输出、事件与工具语义;本文只取代其执行基底与信任实现。[沙箱化 Node PTC 决策](2026-09-11-sandboxed-node-ptc-runtime.zh.md)保留执行与控制保证;显式 null 截止扩展其服务选项。[agent 作用域运行时设计](../../../../packages/core/agent-loop/README.zh.md)保留待启动与子 agent 清理的归属规则。
## 曾考虑的替代方案
diff --git a/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.i18n.yaml
index 4bef95dc24..b348c71538 100644
--- a/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.i18n.yaml
@@ -15,8 +15,8 @@
en: 3c2dd2c948ffb51b
zh: d019197df0ee3dcf
/agent-note-deepseek-account-browser-login/consequences:
- en: a6eff4a0350e9018
- zh: 2e3200ca089023ab
+ en: 3c5681b492cd34cf
+ zh: d7707fed9241f9bc
/agent-note-deepseek-account-browser-login/verification:
en: 881edcb454b941a6
zh: 7dc5a9b7a066f623
diff --git a/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.md b/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.md
index 70ed73f265..2e08d36606 100644
--- a/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.md
+++ b/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.md
@@ -46,7 +46,7 @@ Profile and recharge-wallet queries use independent getProfile and getBalance op
Deployment authentication uses explicit Host-only requestHeaders on the configured Platform origin. The provider rejects redirects and reserved-header overrides so deployment cookies cannot replace account authorization or follow a browser destination. Environment-specific authentication protocols remain outside the account provider.
-The bundled account UI is Desktop-only: its preload bridge enables the account launcher, settings and sign-in onboarding. Plain Web retains API-key onboarding and the standard Settings launcher without account subscription or login. The Host protocol accepts login_source (desktop or web) in auth_init; the bundled UI sends desktop. The backend accepts localhost callbacks. DSH preserves the browser-supplied localhost hostname and port without DNS resolution or conversion to an IP literal.
+The bundled account UI mounts the account launcher, settings and sign-in onboarding in both Desktop and plain Web. The Host sends login_source (desktop or web) in auth_init so Platform can choose the completion experience; the UI sends desktop when the Desktop preload bridge is present and web otherwise. Web failures close the authorization tab while the original tab receives Host state; no Web UI return URL is used. The backend accepts localhost callbacks. DSH preserves the browser-supplied localhost hostname and port without DNS resolution or conversion to an IP literal.
The macOS development launcher registers an isolated, ad-hoc-signed application bundle for `dsh://open`. The bundle retains the workspace entry and development paths for Launch Services cold starts without copying credentials or changing the package-manager-owned Electron application. Protocol registration targets the latest launched development or packaged application.
diff --git a/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.zh.md b/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.zh.md
index 5b0df92064..235fcba920 100644
--- a/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.zh.md
@@ -46,7 +46,7 @@ DSH 授权通过 x-dsh-auth-token 请求头鉴权 Platform、推理和 Files 请
开发环境认证通过配置的平台来源上的显式 Host 请求头 requestHeaders 完成。提供者拒绝重定向和保留请求头覆盖,防止开发环境 Cookie 替换账号授权或跟随浏览器跳转地址。特定环境的认证协议不属于账号提供者。
-内置账号界面仅用于 Desktop:preload 桥接启用账号入口、设置和登录引导。普通 Web 保留 API Key 引导及标准设置入口,不订阅账号状态或显示登录。Host 协议在 auth_init 中接受 login_source(desktop 或 web);内置界面发送 desktop。后端接受 localhost 回调。DSH 保留浏览器提供的 localhost 主机名和端口,不做 DNS 解析或 IP 字面量转换。
+内置账号界面在 Desktop 和普通 Web 中都挂载账号入口、设置和登录引导。Host 在 auth_init 中发送 login_source(desktop 或 web),以便 Platform 选择完成页体验;存在 Desktop preload 桥接时界面发送 desktop,否则发送 web。Web 登录失败时关闭授权标签页,原标签页通过 Host 状态获知结果;不使用 Web UI 返回 URL。后端接受 localhost 回调。DSH 保留浏览器提供的 localhost 主机名和端口,不做 DNS 解析或 IP 字面量转换。
macOS 开发启动器为 `dsh://open` 注册独立、经临时签名的应用包。该应用包保留工作区入口和开发路径,使 Launch Services 能够冷启动;凭证不会被复制,也不修改包管理器安装的 Electron 应用。协议注册指向最近启动的开发版或打包版应用。
diff --git a/.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.i18n.yaml
index a0da60252b..175c7d9ac3 100644
--- a/.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.i18n.yaml
@@ -9,8 +9,8 @@
en: 2fc4ac7bdd4a3474
zh: 21ebdd3ac9264132
/agent-note-independent-libreoffice-kit-ownership/decision:
- en: 31200a9be3940dc0
- zh: 8e22be1d160ec3e4
+ en: 0d8e05bcb940f88f
+ zh: b5b55f4ee82cf6a3
/agent-note-independent-libreoffice-kit-ownership/alternatives-considered:
en: 55175bb30630bf0c
zh: 788853ddd5f7a4e9
diff --git a/.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md b/.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md
index 13efc41e56..50e4ab6e0c 100644
--- a/.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md
+++ b/.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md
@@ -14,7 +14,7 @@ The `deepseek-harness/libreoffice-kit` repository owns the reusable `@deepseek-a
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 upstream API declares platform engines as optional dependencies. The [platform engine reference](../../../../packages/document/office-to-pdf/README.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.
diff --git a/.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md b/.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md
index 720ca2ba3f..222197ab5b 100644
--- a/.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md
@@ -14,7 +14,7 @@ LibreOffice 编译、源码补丁、平台资格验证和大型二进制发布
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 凭据。
+上游 API 将平台引擎声明为可选依赖。[平台引擎参考](../../../../packages/document/office-to-pdf/README.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 的精确包标识;无关包和改变后的许可条款仍被拒绝。
diff --git a/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.i18n.yaml
index 76754c3084..c92c021186 100644
--- a/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.i18n.yaml
@@ -9,8 +9,8 @@
en: f9721b730c7e70c7
zh: 367e7839912cb726
/agent-note-default-deepseek-session-log-upload/decision:
- en: ec3b05fb70ae490f
- zh: 59b0691e327855b4
+ en: b72ef475a41b1a23
+ zh: b17a68367679730b
/agent-note-default-deepseek-session-log-upload/alternatives-considered:
en: 4cc4b61d76f84ff3
zh: bdd29ea66037f744
diff --git a/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.md b/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.md
index 232ab867b1..42327124fd 100644
--- a/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.md
+++ b/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.md
@@ -9,7 +9,9 @@ Ordinary DeepSeek requests do not contain the complete canonical Session traject
## Decision
-`session-log-deepseek.Config.enabled` defaults to `true` in every process. An explicit `enabled: false` disables the contribution. The plugin does not inspect test-runner or snapshot environment variables.
+`session-log-deepseek.Config.enabled` defaults to `true` in every process. A resolved `enabled: false` disables the contribution; configuration precedence determines which value takes effect. The plugin does not inspect test-runner or snapshot environment variables.
+
+The `enabled` value is read for each request through a volatile reference so a saved preference takes effect without restarting the plugin. Profile writes can override bundle defaults under the [profile-owned configuration rules](2026-09-19-profile-owned-live-configuration.md); home patches and command-line overlays still reject conflicting writes. Disabling upload leaves the acceptance watermark unchanged because it records provider acceptance, not user eligibility. Re-enabling therefore sends the unaccepted suffix, including events recorded while disabled. Already prepared requests retain their payload.
This supersedes only the opt-in default in the [request-extension decision](2026-08-21-deepseek-llm-api-request-extensions.md); that note still owns field serialization, destinations, acceptance, and retry semantics. The headless and ACP corpus base patches and Web scaffold explicitly disable upload. Later scenario patches can enable it. The SDK text-turn recording omits the setting and exercises the shipped default, including durable acceptance events.
diff --git a/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.zh.md b/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.zh.md
index 0fd8210e46..74bce2350d 100644
--- a/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.zh.md
@@ -9,7 +9,9 @@ Status: implemented
## 决策
-`session-log-deepseek.Config.enabled` 在所有进程中均默认为 `true`。显式设置 `enabled: false` 可关闭贡献。插件不读取测试运行器或快照环境变量。
+`session-log-deepseek.Config.enabled` 在所有进程中均默认为 `true`。最终生效的 `enabled: false` 会关闭贡献;配置优先级决定采用哪个值。插件不读取测试运行器或快照环境变量。
+
+每次请求通过 volatile 引用读取 `enabled`,因此保存的偏好无需重启插件即可生效。按照 [profile 所有的配置规则](2026-09-19-profile-owned-live-configuration.zh.md),profile 写入可覆盖组合包默认值;home patch 和命令行 overlay 仍会拒绝冲突写入。关闭上传不改变接受水位,因为它记录提供方的接受结果,而不是用户是否允许上传。重新开启后会发送尚未接受的后缀,包括关闭期间记录的事件。已准备的请求保留原有载荷。
本决策仅取代[请求扩展决策](2026-08-21-deepseek-llm-api-request-extensions.zh.md)中的主动启用默认策略;该记录仍负责字段序列化、目的地址、接受与重试语义。headless 和 ACP 语料的基础 patch 以及 Web scaffold 显式关闭上传。后续场景 patch 可以启用上传。SDK text-turn 录制省略该设置,验证产品默认值及持久接受事件。
diff --git a/.agents/notes/implemented/architecture/2026-09-14-sidebar-layout-provider-recovery.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-14-sidebar-layout-provider-recovery.i18n.yaml
deleted file mode 100644
index 621d099284..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-14-sidebar-layout-provider-recovery.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-14-sidebar-layout-provider-recovery.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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-sidebar-layout-provider-recovery.md
-/agent-note-sidebar-layout-persistence-and-provider-recovery:
- en: 78dee80210908b7d
- zh: 01a490734740a523
-/agent-note-sidebar-layout-persistence-and-provider-recovery/problem:
- en: 8adb7105e89be803
- zh: 96c4e92e85b6980e
-/agent-note-sidebar-layout-persistence-and-provider-recovery/decision:
- en: aa09cb90f663dfe8
- zh: edaabb76aa6dc8de
-/agent-note-sidebar-layout-persistence-and-provider-recovery/alternatives-considered:
- en: 4a9b1234b0289ae7
- zh: 81854e0e9d32b6da
-/agent-note-sidebar-layout-persistence-and-provider-recovery/consequences:
- en: 4daf813b59205ca3
- zh: 917c2b975c9a67dd
diff --git a/.agents/notes/implemented/architecture/2026-09-15-client-session-references.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-15-client-session-references.i18n.yaml
index d2b3a75106..f1173f2864 100644
--- a/.agents/notes/implemented/architecture/2026-09-15-client-session-references.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-15-client-session-references.i18n.yaml
@@ -12,8 +12,8 @@
en: a15c7c13f167e5ac
zh: 63bcc2ef60c2f344
/agent-note-client-session-references-reference-sources-and-ui-status/decision/scope-and-ownership:
- en: d9d52caab1468205
- zh: 1b7665610bb025c6
+ en: 3f04f9c2b7b0fc2b
+ zh: ebfcbac3de8463a3
/agent-note-client-session-references-reference-sources-and-ui-status/decision/addresses-bindings-and-references:
en: 3743abc8bca00348
zh: d79e6a9311f0a2fe
@@ -30,8 +30,8 @@
en: 04802baa0147fec1
zh: b86037bf2e4bc936
/agent-note-client-session-references-reference-sources-and-ui-status/decision/main-area-navigation-and-presentation:
- en: c46c9ca54ea9eaad
- zh: 918dc8eebff5edf3
+ en: 508268f083a644a9
+ zh: 9a3c0931d49fcedc
/agent-note-client-session-references-reference-sources-and-ui-status/decision/presets-and-creation-flows:
en: 70ef26a3cd399b14
zh: 5c81515ff896f91a
diff --git a/.agents/notes/implemented/architecture/2026-09-15-client-session-references.md b/.agents/notes/implemented/architecture/2026-09-15-client-session-references.md
index ca4055bd9e..83f2ec688b 100644
--- a/.agents/notes/implemented/architecture/2026-09-15-client-session-references.md
+++ b/.agents/notes/implemented/architecture/2026-09-15-client-session-references.md
@@ -30,7 +30,7 @@ Client Session objects, Agent-scoped Client Contexts, references, consumer-sourc
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.
+The [blank Session decision](2026-09-17-process-local-blank-sessions.md) owns creation and reuse; this decision owns explicit Provider references and adoption.
### Addresses, bindings, and references
@@ -156,7 +156,7 @@ The main view privately persists its target identity and subagent address under
| 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.
+Source metadata does not change when DOM focus moves or when a global panel hides a retained view. The [global main-panel design](../../../../packages/client/ui-layout/README.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.
diff --git a/.agents/notes/implemented/architecture/2026-09-15-client-session-references.zh.md b/.agents/notes/implemented/architecture/2026-09-15-client-session-references.zh.md
index b8c6c313f6..542def8a76 100644
--- a/.agents/notes/implemented/architecture/2026-09-15-client-session-references.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-15-client-session-references.zh.md
@@ -30,7 +30,7 @@ Client Session 对象、Agent 作用域的 Client 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 与收养语义理由。
+[空白 Session 决策](2026-09-17-process-local-blank-sessions.zh.md)负责创建与复用;本决策负责显式 Provider 引用与收养。
### 地址、binding 与引用
@@ -156,7 +156,7 @@ Conversation 的引用属于其视图所有者,不属于 Chat、Trajectory 或
| Chat/Trajectory 恢复 | 为显式选中的主目标恢复视图;独立绑定的视图保留自己的状态 |
| Cordis inventory 面板 | 使用不区分 current/other 的单一列表;runner 不提供主区域选择的公开 getter |
-DOM 焦点移动或全局面板隐藏仍被持有的视图时,来源元数据不变。[全局主面板设计](../../implemented/architecture/2026-09-08-global-main-panels.zh.md)拥有面板选择与布局;Session 引用所有权不替代它。
+DOM 焦点移动或全局面板隐藏仍被持有的视图时,来源元数据不变。[全局主面板设计](../../../../packages/client/ui-layout/README.zh.md)拥有面板选择与布局;Session 引用所有权不替代它。
Conversation 保留 `hero`、`settling`、`active` 组合与既有历史加载和 `openError` 处理。获取引用不增加外层 loading/error 阶段展示、额外隐藏 composer 的条件、Retry 按钮或替换 Sidebar 内容的恢复面板。已有错误处理方继续处理自己的错误;没有错误展示的调用点不增加展示。Promise 拒绝与正确释放引用不意味着额外增加 UI 处理方。
diff --git a/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.i18n.yaml
index b5ebc209af..8d942f2a1d 100644
--- a/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.i18n.yaml
@@ -9,8 +9,8 @@
en: 9cf7ba7dfd36aee0
zh: 356fdbb01870981e
/agent-note-native-desktop-fatal-recovery/decision:
- en: a3b9aa3659117524
- zh: 6f25cb295c37cddd
+ en: 02d07a5458e01321
+ zh: de9f480b7196f2c0
/agent-note-native-desktop-fatal-recovery/alternatives-considered:
en: 624d62bc0b73c37a
zh: 81c3d7cd66782f73
diff --git a/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.md b/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.md
index fc1564b02e..75364ae8ee 100644
--- a/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.md
+++ b/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.md
@@ -16,7 +16,7 @@ The first report claims presentation before awaiting the dialog. Later reports r
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. Backend state remains in the main process, and native recovery directly owns disabling all third-party bundles. Desktop has no profile reset or emergency recovery document. A fatal backend failure requires one of the native recovery actions rather than an in-process retry.
-This supersedes recovery-page and reset behavior in the [immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-start.md), whose immediate visibility and direct Host startup rationale remain active. The [Web wrapper decision](2026-09-10-desktop-web-wrapper.md) owns shared plugin management and native profile preparation.
+The [Desktop reference](../../../../apps/desktop/README.md) defines immediate window visibility and direct Host startup; this decision owns native fatal recovery. The [Web wrapper decision](2026-09-10-desktop-web-wrapper.md) owns shared plugin management and native profile preparation.
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.zh.md b/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.zh.md
index ceda54b414..f5c1775278 100644
--- a/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.zh.md
@@ -16,7 +16,7 @@ Electron 在每个应用进程中提供一次原生致命错误对话框。明
Web 文档保留在原位。宿主回调负责启动失败展示,共享启动页保留加载动画;普通浏览器启动仍显示自身的失败报告。只有主应用框架可以上报 Web 启动失败。后端状态保留在主进程中,原生恢复直接负责禁用全部第三方 bundle。Desktop 不提供 profile 重置或应急恢复文档。后端致命故障必须通过原生恢复操作处理,不在当前进程中重试。
-本决策取代[立即显示窗口决策](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)中的恢复页面和重置行为;其立即显示与直接启动 Host 的理由仍有效。[Web 薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)负责共享插件管理和原生 profile 准备。
+[Desktop 参考](../../../../apps/desktop/README.zh.md)定义立即显示窗口与直接启动 Host;本决策负责原生致命故障恢复。[Web 薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)负责共享插件管理和原生 profile 准备。
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.i18n.yaml
index 9283aaa9e4..d1e00c2cd5 100644
--- a/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.i18n.yaml
@@ -9,14 +9,14 @@
en: edefff7cd986a509
zh: a270839fa86976a2
/agent-note-guided-plugin-installation/decision:
- en: b661fb786e7182c5
- zh: 4cdeb584639fda23
+ en: 30f1e636973df06a
+ zh: 0f874b88a5d4d7f5
/agent-note-guided-plugin-installation/alternatives-considered:
en: be64b541f4d81e7c
zh: 8600531c08ab4288
/agent-note-guided-plugin-installation/consequences:
- en: e4536b20f3587497
- zh: a8eeb1d3c578b785
+ en: bc1b116dd15ff420
+ zh: e886e84cc3081feb
/agent-note-guided-plugin-installation/testing:
en: 1e121dca5f7dc572
zh: bcdb1b6482cc1af6
diff --git a/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.md b/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.md
index 68bd519b7c..c0e9e68325 100644
--- a/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.md
+++ b/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.md
@@ -14,7 +14,7 @@ The install dialog put a spec straight into `pnpm add` and showed pnpm's termina
**A failed or cancelled installation restores the profile files.** `installBundle` snapshots `package.json` and `pnpm-lock.yaml` before pnpm runs and puts them back when pnpm fails, when the run is cancelled, or when the package pnpm added declares no bundle patch. This reverses, for installations only, the [manager's decision](2026-09-14-current-profile-plugin-management.md) to retain partial changes: an installation that did not produce a usable bundle must not leave a dependency the page cannot show. Removals keep that decision. Downloaded files can stay under `node_modules` and the pnpm store.
-**A failed run is classified where the facts are.** `classifyInstallFailure` reads how the run ended and what pnpm printed — its `ERR_PNPM_*` codes and Node's errno names — into `packageResult.kind`. A run the manager terminated is classified `timeout` whatever exit status the signal left behind, so installation restores the files and removal reports failure instead of reading it as a success ([run bounds](../bug-fix/2026-09-23-bounded-pnpm-runs.md)). The client shows the kind as one line and folds pnpm's output behind the details. Parsing the log is confined to this one function with a fixture-driven test.
+**A failed run is classified where the facts are.** `classifyInstallFailure` reads how the run ended and what pnpm printed — its `ERR_PNPM_*` codes and Node's errno names — into `packageResult.kind`. A run the manager terminated is classified `timeout` whatever exit status the signal left behind, so installation restores the files and removal reports failure instead of reading it as a success ([run bounds](../../../../packages/boot/plugin-manager/README.md)). The client shows the kind as one line and folds pnpm's output behind the details. Parsing the log is confined to this one function with a fixture-driven test.
**GitHub connection failures have a short budget before installation.** The Host runs a read-only `git ls-remote` check with the profile's Git and proxy configuration and a default `githubConnectionTimeoutMs` of 5000 ms. Credential helpers and prompts are disabled for this check, so authentication cannot consume the connection budget. Only network failures and timeouts prevent pnpm from starting; pnpm still owns authentication, HTTPS-to-SSH fallback, ref resolution and the full installation. The budget does not limit downloads or builds. The existing failure kind and `failedAt: 'spec-host'` let the Client show a connection error or timeout and offer a domestic registry for a replacement package spec. Cancellation and manager disposal terminate the check and its descendants before settling.
@@ -38,7 +38,7 @@ The install dialog put a spec straight into `pnpm add` and showed pnpm's termina
## Consequences
-`inspect`, `cancelInstall`, and the `plugin-manager/changed`, `plugin-manager/install-log`, and `plugin-manager/install-state` events join the manager's Remote; `listBundles` carries titles, rows, and overrides; `ChangeResult` gains `cancelled`, `bundle`, and `packageResult.kind`; the config gains `pnpmCommand`, `inspectTimeoutMs` and `githubConnectionTimeoutMs`. The dialog is four screens over one subject card.
+`inspect`, `cancelInstall`, and the `plugin-manager/changed`, `plugin-manager/install-log`, and `plugin-manager/install-state` events join the manager's Remote; `listBundles` carries titles, rows, and overrides; `ChangeResult` gains `cancelled`, `bundle`, `version`, and `packageResult.kind`; the config gains `pnpmCommand`, `inspectTimeoutMs` and `githubConnectionTimeoutMs`. The dialog is four screens over one subject card.
## Testing
diff --git a/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.zh.md b/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.zh.md
index f3cbefedeb..0d914869a3 100644
--- a/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.zh.md
@@ -14,7 +14,7 @@ Status: implemented
**失败或被取消的安装恢复 profile 文件。** `installBundle` 在 pnpm 运行前快照 `package.json` 与 `pnpm-lock.yaml`,在 pnpm 失败、运行被取消、或 pnpm 装入的包没有声明组合包 patch 时把它们放回去。这只针对安装反转了[管理器的决定](2026-09-14-current-profile-plugin-management.zh.md)中"保留部分改动"的部分:没有产出可用组合包的安装,不能留下一个页面无法显示的依赖。删除仍沿用那个决定。已下载文件可能留在 `node_modules` 与 pnpm 缓存中。
-**失败在事实所在处分类。** `classifyInstallFailure` 依据运行的结束方式和 pnpm 的输出——它的 `ERR_PNPM_*` 码和 Node 的 errno 名——给出 `packageResult.kind`。被管理器终止的运行,不论信号留下什么退出状态都归类为 `timeout`,因此安装会还原文件,删除则报告失败而不会读成成功([运行上界](../bug-fix/2026-09-23-bounded-pnpm-runs.zh.md))。客户端把 kind 显示成一句话,把 pnpm 输出折叠进详情。对日志的解析只存在于这一个函数,用夹具驱动的测试钉住。
+**失败在事实所在处分类。** `classifyInstallFailure` 依据运行的结束方式和 pnpm 的输出——它的 `ERR_PNPM_*` 码和 Node 的 errno 名——给出 `packageResult.kind`。被管理器终止的运行,不论信号留下什么退出状态都归类为 `timeout`,因此安装会还原文件,删除则报告失败而不会读成成功([运行上界](../../../../packages/boot/plugin-manager/README.zh.md))。客户端把 kind 显示成一句话,把 pnpm 输出折叠进详情。对日志的解析只存在于这一个函数,用夹具驱动的测试钉住。
**GitHub 连接失败在安装前有单独的短时限。** 宿主通过只读的 `git ls-remote` 检查连接,沿用 profile 的 Git 与代理配置,`githubConnectionTimeoutMs` 默认为 5000 毫秒。这次检查禁用凭据助手和认证提示,避免认证过程占用连接预算。只有网络失败与超时会阻止 pnpm 启动;认证、HTTPS 到 SSH 的回退、ref 解析及完整安装仍由 pnpm 负责。时限不约束下载或构建。已有的失败类型与 `failedAt: 'spec-host'` 让客户端显示连接失败或超时,并提供国内镜像供用户输入另一个包名。取消安装或销毁管理器会先终止检查及其子进程,再结束操作。
@@ -38,7 +38,7 @@ Status: implemented
## 后果
-`inspect`、`cancelInstall` 以及 `plugin-manager/changed`、`plugin-manager/install-log`、`plugin-manager/install-state` 事件加入管理器的 Remote;`listBundles` 携带标题、行与覆盖项;`ChangeResult` 增加 `cancelled`、`bundle` 与 `packageResult.kind`;配置增加 `pnpmCommand`、`inspectTimeoutMs` 与 `githubConnectionTimeoutMs`。对话框是围绕同一张主题卡的四个画面。
+`inspect`、`cancelInstall` 以及 `plugin-manager/changed`、`plugin-manager/install-log`、`plugin-manager/install-state` 事件加入管理器的 Remote;`listBundles` 携带标题、行与覆盖项;`ChangeResult` 增加 `cancelled`、`bundle`、`version` 与 `packageResult.kind`;配置增加 `pnpmCommand`、`inspectTimeoutMs` 与 `githubConnectionTimeoutMs`。对话框是围绕同一张主题卡的四个画面。
## 测试
diff --git a/.agents/notes/implemented/architecture/2026-09-15-narrow-pi-ai-runtime-imports.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-15-narrow-pi-ai-runtime-imports.i18n.yaml
deleted file mode 100644
index 7e6309f2da..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-15-narrow-pi-ai-runtime-imports.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-15-narrow-pi-ai-runtime-imports.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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-narrow-pi-ai-runtime-imports.md
-/agent-note-narrow-pi-ai-runtime-imports:
- en: d29340bbc4bf0e5b
- zh: 12a39c4efe53b580
-/agent-note-narrow-pi-ai-runtime-imports/problem:
- en: 4b2048b356064ada
- zh: 9926d7c54da73852
-/agent-note-narrow-pi-ai-runtime-imports/decision:
- en: d425c01ec05ee7f2
- zh: 8d58d6db913720c4
-/agent-note-narrow-pi-ai-runtime-imports/alternatives-considered:
- en: c663ce67f3831d3f
- zh: de280dc149d5a6a0
-/agent-note-narrow-pi-ai-runtime-imports/consequences:
- en: 9707284a83c1294a
- zh: c53be621aac00fc9
diff --git a/.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.i18n.yaml
deleted file mode 100644
index c20b10b6fe..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-15-platform-office-engines.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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-platform-office-engines.md
-/agent-note-one-office-engine-per-platform:
- en: 5ba8eea6deb11824
- zh: 7d1cb289fa278c74
-/agent-note-one-office-engine-per-platform/problem:
- en: 803030fa18966fef
- zh: bb1bbd185860f533
-/agent-note-one-office-engine-per-platform/decision:
- en: 8cea2cb889666af7
- zh: 346adb1e99787c78
-/agent-note-one-office-engine-per-platform/alternatives-considered:
- en: d406d0a9f4dd503a
- zh: b2685e11d706799e
-/agent-note-one-office-engine-per-platform/consequences:
- en: 7e236c479975eee6
- zh: 94a8e0e8883bd324
diff --git a/.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.i18n.yaml
index 0c59a93381..e2da7be945 100644
--- a/.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.i18n.yaml
@@ -9,8 +9,8 @@
en: 44615a7a0939054e
zh: b8efa721321ba0d7
/agent-note-creator-mode-installs-persistent-plugin-bundles/decision:
- en: 3530a89a72e35bdd
- zh: 3601941563747b5d
+ en: c0d74db1b2b16576
+ zh: b8713a6aadf007f9
/agent-note-creator-mode-installs-persistent-plugin-bundles/alternatives-considered:
en: af527d8aabdfd59c
zh: 34388da992d2ab56
diff --git a/.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.md b/.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.md
index 04ccbb83fe..5723eba63e 100644
--- a/.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.md
+++ b/.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.md
@@ -12,7 +12,7 @@ Agents need to install capabilities and use them during the same conversation. G
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.
+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 reference](../../../../packages/extensions/cordis-host-runner/README.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.
diff --git a/.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.zh.md b/.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.zh.md
index 91afa00cd3..33c1fd867f 100644
--- a/.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.zh.md
@@ -12,7 +12,7 @@ agent 需要安装能力并在同一段对话中使用。生成代码工具在
创造模式启用现有的 `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)继续规定锁、包安装和部分失败行为。
+模型可见两个只读 Cordis 检查工具,不再提供生成代码的 define/run/stop/undefine 和动态自省工具 API。现有运行时和 Client 消费者保留其服务;历史会话卡片仍然可读。这仅取代[自引用工具集参考](../../../../packages/extensions/cordis-host-runner/README.zh.md)中的模型侧变更流程;其运行时所有权和沙箱依据仍有独立价值。[profile 事务决策](2026-09-14-current-profile-plugin-management.zh.md)继续规定锁、包安装和部分失败行为。
除非用户指定其他目标,视觉创建请求默认通过已安装的 Client 插件显示在当前 Web 页面。开发 skill 提供最小包和由 effect 管理的 Client 注册示例。已知所需 API 后结束探查,在可选视觉优化之前先安装能工作的初版。有条件时使用已连接页面验证。浏览器认证或操作系统设置不是安装插件的前提,mock 预览不能证明应用内结果。
diff --git a/.agents/notes/implemented/architecture/2026-09-16-desktop-cos-upload-transport.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-16-desktop-cos-upload-transport.i18n.yaml
deleted file mode 100644
index 8a90598c63..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-16-desktop-cos-upload-transport.i18n.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-16-desktop-cos-upload-transport.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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-desktop-cos-upload-transport.md
-/agent-note-upload-desktop-releases-through-the-tencent-cos-sdk:
- en: acead237faaa9390
- zh: ca5d23451f3b4577
-/agent-note-upload-desktop-releases-through-the-tencent-cos-sdk/problem:
- en: 74f8515cdbb2a0aa
- zh: a00d1b08db6d6158
-/agent-note-upload-desktop-releases-through-the-tencent-cos-sdk/decision:
- en: 54cef8eacccce2e8
- zh: 8bad9ce5c932e0dd
-/agent-note-upload-desktop-releases-through-the-tencent-cos-sdk/testing:
- en: c15a405f365ca499
- zh: ee59d1b593473ee7
-/agent-note-upload-desktop-releases-through-the-tencent-cos-sdk/alternatives-considered:
- en: faae1f132d6cd404
- zh: 1683143fbe6b07ce
-/agent-note-upload-desktop-releases-through-the-tencent-cos-sdk/consequences:
- en: 2453f558b12db1ec
- zh: 15d77bfb842f9b31
diff --git a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.i18n.yaml
index 8bc4eaeaa5..a59c5e9371 100644
--- a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.i18n.yaml
@@ -9,11 +9,11 @@
en: 2c774d05ae75784f
zh: 619ba7f281b13233
/agent-note-host-owned-scheduled-messages/decision:
- en: 4a38d9bd933241b0
- zh: b246c74f68e92a25
+ en: 1210b3975fd26bb3
+ zh: 3322e6d0aa63fdb0
/agent-note-host-owned-scheduled-messages/alternatives-considered:
en: 21d7ec9a9ff3cbf2
zh: 93d03b8b3012aa7e
/agent-note-host-owned-scheduled-messages/consequences:
- en: a48f13f89a1944b1
- zh: f8fa677122ce2e12
+ en: 3ae0cd93ed95431e
+ zh: 25a0a2bc25863651
diff --git a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.md b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.md
index 3d90e8ad7e..cd295da249 100644
--- a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.md
+++ b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.md
@@ -14,13 +14,13 @@ The existing Schedule package owns one Host storage domain containing each task'
The task domain declares the whole-unit layout. Tasks are authoritative, so the JSON backend rejects an unreadable file, malformed document, unsupported version, or invalid task as a complete load failure instead of interpreting it as deletion. Failed recovery leaves `schedule.json` unchanged, while an initially absent file opens as an empty domain; repairing the file preserves task identity. `Service.init` waits for both storage validation and runtime setup, management methods wait for the same initialization result, and an unloading owner releases late-acquired storage without starting dispatch. Dispatch admission is separate from initialization: an ancestor can close Agent admission after a task mutation commits but before runtime cleanup starts. That refusal is logged without automatic retry and does not turn the persisted mutation into a failed management result. Invalid storage and failed cleanup registration still reject startup.
-This supersedes the Session-log authority, live-only activation, and read-only catalog decisions in the [Session-local reminders note](../feature/2026-08-05-durable-web-schedule.md). Its explicit absolute-time validation, fixed-rate arithmetic, and distinction between queue admission and model completion remain applicable. Historical `schedule/change` creation, fold, and invariant types remain restricted to After, At, and Every through `LegacyScheduleRecord`; a separate Host decoder accepts the current `ScheduleRecord`, including Daily, Weekly, and Cron. The Host record decoder requires a stored `title` that is non-empty after trimming, already trimmed, and at most 120 characters, and the strict task schema rejects the whole domain open instead of dropping such a task. The historical change decoder tolerates an absent `title` so an already-written Session log stays readable, and it applies the same three checks when the member is present. The storage domain remains version 1. Historical events do not populate that domain, and existing `at` tasks are not converted into daily or weekly rules. Loading a Session with active legacy reminders warns that they must be recreated.
+This replaces the earlier Session-log authority, live-only activation, and read-only catalog decisions. The [Schedule reference](../../../../packages/schedule/schedule/README.md) owns explicit absolute-time validation, fixed-rate arithmetic, and the distinction between queue admission and model completion. Historical `schedule/change` creation and fold types remain restricted to After, At, and Every through `LegacyScheduleRecord`; a separate Host decoder accepts the current `ScheduleRecord`, including Daily, Weekly, and Cron. The Host record decoder requires a stored `title` that is non-empty after trimming, already trimmed, and at most 120 characters, and the strict task schema rejects the whole domain open instead of dropping such a task. The historical change decoder tolerates an absent `title` so an already-written Session log stays readable, and it applies the same three checks when the member is present. The storage domain remains version 1. Historical events do not populate that domain, and existing `at` tasks are not converted into daily or weekly rules. Loading a Session with active legacy reminders warns that they must be recreated.
A plugin-sourced `followup()` synchronously appends the message to the Session inbox. Successful Session flush acknowledges durable delivery. One task-record put stores `lastDelivery`, appends a delivery record with the occurrence target, acknowledgment time, actual message id, and instruction snapshot, and updates the one-shot's `inactive` status or the recurring task's next target and status. Task state and its saved history therefore share one commit. Inactive tasks remain stored and never schedule another delivery; explicit deletion stops future delivery and removes the row together with its saved records. These records acknowledge inbox delivery, not user receipt or model execution completion.
New tasks initialize an empty `deliveryHistory`. A task without that optional field exposes its existing latest receipt, if any, and marks earlier records unavailable. Reading never rewrites the old task or invents the lost history or instruction snapshot. Its next acknowledged delivery preserves that available receipt and appends the new one. Strict decoding rejects duplicate message ids and disagreement between the latest receipt and the last history entry. History follows append order, not wall-clock sorting, so clock rollback does not reverse acknowledged deliveries.
-A recurring task contributes its latest due occurrence once, including after Host restart. Every remains creation-aligned until an interval edit establishes a new anchor; Daily retains a local time and explicit IANA zone, and Weekly adds a non-empty ISO weekday set stored as unique ascending numbers. Every, Daily, Weekly, and Cron use `renderRecurringReminderBatchFraming` and share one follow-up message and its real message id when due for the same Session. A recurring task with no representable next target remains inactive with its last receipt. A stopped Host does not send messages.
+A recurring task contributes its latest due occurrence once, including after Host restart. Every remains creation-aligned until an interval edit establishes a new anchor; Daily retains a local time and explicit IANA zone, and Weekly adds a non-empty ISO weekday set stored as unique ascending numbers. Every, Daily, Weekly, and Cron use `renderRecurringReminderBatchFraming` and share one follow-up message and its real message id when due for the same Session. The fixed text both renderers emit is the [delivered-reminder origin line](2026-09-24-scheduled-message-framing.md) decision. A recurring task with no representable next target remains inactive with its last receipt. A stopped Host does not send messages.
Daily is a fourth mutually exclusive creation selector, Weekly a fifth, and Cron a sixth, none a conversion of `every_seconds` or `at`. Weekly selects every local date whose ISO weekday belongs to its stored set; the decoder rejects an unnormalized stored set, and the selector rejects an empty, repeated, out-of-range, or non-integer one. Their first targets are strictly future. Missing local times and dates are skipped; overlaps select the earlier instant once per date. The next target is after the delivery decision and on a local date later than the selected occurrence's date. Only UTC targets are restricted to years 0001–9999; local dates may cross into year 0 or 10000 at the edges. After Session restoration, the runtime rechecks due members against the current clock so a rollback does not admit future tasks.
@@ -58,7 +58,7 @@ Session inbox persistence and the task put occur in separate persistence systems
**Keep the mock's hourly choice out of the stored vocabulary.** The approved mock lists 每小时整点 beside the daily, weekly and one-shot choices. Hourly and fixed-rate intervals are the same elapsed-time rule, so the card maps that menu slot onto the existing interval kind instead of adding a persisted kind. A dedicated hourly kind would need a new durable variant, its decoder, its target resolution, and its tool schema, all to express what an interval already expresses.
-Task discovery and timers do not depend on loaded Sessions. The after, absolute-time, and fixed-rate selectors retain their semantics alongside Daily, Weekly, and Cron; reading existing saved tasks does not rewrite them. The Host must have its ordinary Session persistence and recovery services available, and enabling the optional experimental bundle `@deepseek-ai/dsh-experimental-schedule-bundle` mounts Schedule; the Web composition carries its row disabled. Explicit deletion stops future delivery and removes the task from `list` and `catalog` without deleting conversation messages; the removed row takes its saved receipts with it. History is pruned to the configured window and record cap when an acknowledgment is appended, loaded into Host memory, and rewritten with the task on each acknowledgment. Pagination limits the returned record count; the plugin configuration (`deliveryHistoryDays`, default 30, and `deliveryHistoryRecords`, default 200) limits storage growth. Earlier overwritten receipts and deliveries whose task write failed cannot be reconstructed. Pause and execution-history operations remain unsupported.
+Task discovery and timers do not depend on loaded Sessions. The after, absolute-time, and fixed-rate selectors retain their semantics alongside Daily, Weekly, and Cron; reading existing saved tasks does not rewrite them. The Host must have its ordinary Session persistence and recovery services available, and the shipped Web composition mounts the Schedule service for the whole deployment. Explicit deletion stops future delivery and removes the task from `list` and `catalog` without deleting conversation messages; the removed row takes its saved receipts with it. History is pruned to the configured window and record cap when an acknowledgment is appended, loaded into Host memory, and rewritten with the task on each acknowledgment. Pagination limits the returned record count; the plugin configuration (`deliveryHistoryDays`, default 30, and `deliveryHistoryRecords`, default 200) limits storage growth. Earlier overwritten receipts and deliveries whose task write failed cannot be reconstructed. Pause and execution-history operations remain unsupported.
Required coverage includes startup from stored tasks, missing-status normalization, cold Session restoration, latest-only catch-up, inactive-task retention without repeat dispatch, real message receipts, deletion across restart, removal of a deleted task's saved deliveries, queue/persistence failures, and disposal. Wall-clock obligations include gaps and skipped dates, earlier-only overlaps, fractional seconds, UTC-range edges, committed-target and alias preservation, and post-restoration clock rollback. Weekly adds weekday normalization and validation, next-target selection across a gap and an overlap, and stored set preservation. Historical decoders must reject Daily, Weekly, and Cron while Host storage accepts them. Client coverage includes every recurring kind, exact rule time, zone, and weekday set, All/Enabled/Inactive status filters, retained inactive details and original-Session navigation, authoritative refresh after confirmed deletion and notification, empty lists, reconnect, and late responses. Presentation coverage includes the page's New action starting a Session, the transcript card's narrowing of persisted result JSON including an incomplete or replayed result, the header clock's direct open for exactly one task and its count label, the idle-only row mark with the hover-card two-task limit and omission line. History coverage includes legacy byte preservation, append and restart, immutable sent instructions, strict latest-receipt consistency, stable cursor pagination, binding checks, failed persistence, and late page replies during refresh or task changes. Cross-store crash duplication remains an explicit limitation rather than a completion claim.
diff --git a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.zh.md b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.zh.md
index f7b9a8f889..c5ee3ac455 100644
--- a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.zh.md
@@ -14,13 +14,13 @@ Status: implemented
任务 domain 声明整 unit 布局。任务是权威数据,因此 JSON 后端会把文件不可读、文档损坏、版本不受支持或任务非法作为整批加载失败,而不是解释为删除。恢复失败不会改写 `schedule.json`,初始不存在的文件则作为空 domain 打开;修复该文件后会保留任务身份。`Service.init` 等待存储校验和运行时初始化,管理方法等待同一初始化结果;拥有方卸载时释放迟到的存储资源,而不开始投递。投递调度准入与初始化分开:任务变更提交后、运行时清理开始前,祖先可能关闭 Agent 准入。此类拒绝会记录日志而不自动重试,不会将已持久化的变更变成失败的管理结果。存储非法和清理注册失败仍拒绝启动。
-此决策替代[会话内提醒记录](../feature/2026-08-05-durable-web-schedule.zh.md)中的 Session 日志权威来源、仅 live 时激活和只读目录决策。其中显式绝对时间校验、固定速率计算,以及区分排队接纳与模型完成的决策仍然适用。历史 `schedule/change` 的创建、fold 和 invariant 类型通过 `LegacyScheduleRecord` 保持为 After、At 和 Every;独立的宿主解码器接受包含 Daily、Weekly 与 Cron 的当前 `ScheduleRecord`。宿主记录解码器要求已存储的 `title` 去除首尾空白后非空、本身已去除首尾空白且不超过 120 个字符,严格的 task schema 会拒绝整个 domain 的打开,而不是丢弃这样的任务。历史变更解码器容忍缺失的 `title`,以便已写入的 Session 日志仍可读取;该成员存在时,同样按这三项检查校验。storage domain 保持版本 1。历史事件不会填充该 domain,已有 `at` 任务也不会转换为每日或每周规则。加载含有活动旧提醒的 Session 时,会警告需要重新创建提醒。
+本决策替代先前的 Session 日志权威来源、仅 live 时激活和只读目录决策。[Schedule 参考](../../../../packages/schedule/schedule/README.zh.md)负责显式绝对时间校验、固定速率计算,以及区分排队接纳与模型完成。历史 `schedule/change` 的创建与 fold 类型通过 `LegacyScheduleRecord` 保持为 After、At 和 Every;独立的宿主解码器接受包含 Daily、Weekly 与 Cron 的当前 `ScheduleRecord`。宿主记录解码器要求已存储的 `title` 去除首尾空白后非空、本身已去除首尾空白且不超过 120 个字符,严格的 task schema 会拒绝整个 domain 的打开,而不是丢弃这样的任务。历史变更解码器容忍缺失的 `title`,以便已写入的 Session 日志仍可读取;该成员存在时,同样按这三项检查校验。storage domain 保持版本 1。历史事件不会填充该 domain,已有 `at` 任务也不会转换为每日或每周规则。加载含有活动旧提醒的 Session 时,会警告需要重新创建提醒。
插件来源的 `followup()` 同步将消息追加到 Session 收件箱。Session flush 成功后确认持久投递。一次任务记录 put 存储 `lastDelivery`,追加包含发生目标、确认时间、真实消息 id 和指令快照的发送记录,并更新一次性任务的 `inactive` 状态或重复任务的下一目标和状态。因此,任务状态与其保存的历史共用一次提交。未运行任务仍保留在存储中,且不会再次安排投递;显式删除会阻止后续投递,并把该行连同其已保存记录一并移除。这些记录确认收件箱投递,不表示用户收到消息或模型执行完成。
新任务初始化空的 `deliveryHistory`。缺少该可选字段的任务会展示已有的最近一次回执(如果存在),并标明更早记录不可用。读取不会重写旧任务,也不会虚构已丢失的历史或指令快照。下一次确认投递时,保留已有回执并追加新记录。严格解码拒绝重复消息 id,以及最近一次回执与历史末项不一致的情况。历史按追加顺序排列,不按墙钟排序,因此时钟回拨不会颠倒已确认投递的顺序。
-重复任务只投递一次最新到期发生时点,Host 重启后也一样。Every 保持与创建时间对齐,直到修改间隔后建立新的起算时点;Daily 保留本地时间与显式 IANA 时区,Weekly 另存非空 ISO 星期集合(唯一且升序)。Every、Daily、Weekly 与 Cron 均使用 `renderRecurringReminderBatchFraming`,同一 Session 的到期任务共用一条 follow-up 消息及其真实消息 id。没有可表示的下一目标时,重复任务保留未运行状态及最近一次回执。停止的 Host 不会发送消息。
+重复任务只投递一次最新到期发生时点,Host 重启后也一样。Every 保持与创建时间对齐,直到修改间隔后建立新的起算时点;Daily 保留本地时间与显式 IANA 时区,Weekly 另存非空 ISO 星期集合(唯一且升序)。Every、Daily、Weekly 与 Cron 均使用 `renderRecurringReminderBatchFraming`,同一 Session 的到期任务共用一条 follow-up 消息及其真实消息 id。两个渲染器输出的固定文本见[投递提醒的来源行](2026-09-24-scheduled-message-framing.zh.md)决策。没有可表示的下一目标时,重复任务保留未运行状态及最近一次回执。停止的 Host 不会发送消息。
Daily 是互斥的第四个创建选择器,Weekly 是第五个,Cron 是第六个,三者都不是对 `every_seconds` 或 `at` 的转换。Weekly 选择本地日期 ISO 星期属于其所存集合的每个日期;解码器拒绝未规范化的已存集合,选择器拒绝空、重复、超出范围或非整数的集合。它们的首个目标严格晚于当前时间。缺失的本地时间或日期被跳过;重叠时间选择较早时点,每个日期一次。下一目标晚于投递判断时刻,且其本地日期晚于所选发生时点的日期。仅 UTC 目标限制在 0001–9999 年;边缘处的本地日期可以进入第 0 年或第 10000 年。Session 恢复后,运行时依据当前时钟重新核对到期成员,避免时钟回拨时接纳未来任务。
@@ -58,7 +58,7 @@ Session 收件箱持久化和任务 put 发生在两个持久化系统中。收
**不把 mock 的「每小时整点」引入持久词表。** 设计稿把它与每天、每周、仅一次并列。每小时整点与固定间隔在语义上是同一种按经过时间计量的规则,因此详情里的该项直接映射到既有的间隔类型,而不是新增一个持久种类。若真要新增,需要新的持久变体、解码器、目标解析与工具 schema,而它表达的能力现有间隔已经覆盖。
-任务发现和 timer 不依赖已加载的 Session。延迟、绝对时间和固定速率选择器在与 Daily、Weekly、Cron 并存时保留自身语义;读取已有任务不会重写它们。Host 必须提供普通 Session 持久化和恢复服务,而挂载 Schedule 需要启用可选实验 bundle `@deepseek-ai/dsh-experimental-schedule-bundle`;Web 组合以禁用状态携带其条目。显式删除会阻止后续投递,并将任务从 `list` 和 `catalog` 中移除,不删除对话消息;被移除的行会带走其已保存回执。历史在追加确认时按配置的窗口与条数上限裁剪,随后加载到 Host 内存,并在每次确认时随任务一起重写。分页限制返回记录数量,不限制存储增长或指令字节数。此前被覆盖的回执,以及任务写入失败的投递记录,无法重建。暂停和执行历史操作仍不受支持。
+任务发现和 timer 不依赖已加载的 Session。延迟、绝对时间和固定速率选择器在与 Daily、Weekly、Cron 并存时保留自身语义;读取已有任务不会重写它们。Host 必须提供普通 Session 持久化和恢复服务,而随发行版交付的 Web 组合为整个部署挂载 Schedule 服务。显式删除会阻止后续投递,并将任务从 `list` 和 `catalog` 中移除,不删除对话消息;被移除的行会带走其已保存回执。历史在追加确认时按配置的窗口与条数上限裁剪,随后加载到 Host 内存,并在每次确认时随任务一起重写。分页限制返回记录数量,不限制存储增长或指令字节数。此前被覆盖的回执,以及任务写入失败的投递记录,无法重建。暂停和执行历史操作仍不受支持。
必要覆盖范围包括从已存储任务启动、缺失状态规范化、冷 Session 恢复、仅补发最新发生时点、保留未运行任务且不重复投递、真实消息回执、重启后的删除结果、删除会移除已保存发送记录、排队或持久化失败,以及销毁。钟表规则的验证要求包括缺口和跳过的日期、仅选择较早时点的重叠、小数秒、UTC 范围边缘、已提交目标和别名保留,以及恢复后的时钟回拨。Weekly 另需覆盖星期集合的规范化与校验、跨缺口与重叠的下一目标选择,以及已存集合的保留。历史解码器必须拒绝 Daily、Weekly 与 Cron,而宿主存储接受它们。客户端覆盖范围包括每种重复规则、精确规则时间、时区和星期集合、全部/已开启/未运行状态筛选、保留未运行任务详情和原 Session 导航、确认删除和通知后的权威刷新、空列表、重新连接和晚到响应。呈现覆盖范围包括页面“新建”操作打开新会话、transcript 卡片从不完整或回放的结果 JSON 中收窄任务、页头时钟在恰好一个任务时直接打开并给出数量标签、仅空闲行标记及其悬浮卡片两个任务上限与遗漏行。历史覆盖范围包括旧文件字节保留、追加与重启、不可变的发送指令、最近回执一致性严格校验、稳定游标分页、绑定校验、持久化失败,以及刷新或切换任务期间晚到的分页响应。跨存储崩溃导致的重复投递仍是明确限制,不能被表述为完成保证。
diff --git a/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.i18n.yaml
deleted file mode 100644
index 209838a431..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.i18n.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-16-plugin-configuration-on-the-plugins-page.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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
-/agent-note-plugin-configuration-on-the-plugins-page:
- en: e3cb8f2cfd31ee99
- zh: 75c7c498bb5b29e4
-/agent-note-plugin-configuration-on-the-plugins-page/problem:
- en: 9ca845902f1df3a2
- zh: 41e3157a843a511e
-/agent-note-plugin-configuration-on-the-plugins-page/decision:
- en: ccee8068ea810fcb
- zh: c87c4e6c44b33a67
-/agent-note-plugin-configuration-on-the-plugins-page/consequences:
- en: d3d131d6c46bc32e
- zh: eb7c06f119dfbe8d
-/agent-note-plugin-configuration-on-the-plugins-page/alternatives-considered:
- en: 14c6db09f23c2005
- zh: 979730121cd837c3
-/agent-note-plugin-configuration-on-the-plugins-page/testing:
- en: b58fdf0d5922f52b
- zh: 0b999255d2b9b832
diff --git a/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.i18n.yaml
deleted file mode 100644
index de00746f9a..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-16-size-neutral-product-icon-weights.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# 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-size-neutral-product-icon-weights.md
-/agent-note-size-neutral-product-icon-weights:
- en: f0d742af33e3297e
- zh: 5db6cdc26635d843
-/agent-note-size-neutral-product-icon-weights/problem:
- en: aae2bf56f8695ee8
- zh: 491fa683d7cc580f
-/agent-note-size-neutral-product-icon-weights/decision:
- en: c0680b384d4198c8
- zh: a34f4481b7784b4f
-/agent-note-size-neutral-product-icon-weights/alternatives-considered:
- en: bda83cdc7a7c59a9
- zh: d3592cdf068556ea
-/agent-note-size-neutral-product-icon-weights/consequences:
- en: bceee31f65a32e69
- zh: d8fad0a01f1a7e7a
diff --git a/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.md b/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.md
deleted file mode 100644
index 3850fcbddf..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Agent Note: Size-neutral product icon weights
-
-Status: implemented
-
-English | [中文](2026-09-16-size-neutral-product-icon-weights.zh.md)
-
-## Problem
-
-Shared product-icon exports encoded one historical render size in each symbol name even though every component accepted a `size` prop. Consumers also thickened selected icons through local CSS or separate drawings, so emphasis had no named library choice and could drift between features.
-
-## Decision
-
-Every shared product glyph exports a size-neutral `Regular` and `Medium` component. `Regular` renders the supplied one-pixel artwork, while `Medium` renders the same paths with a 1.3px inherited stroke; filled regions remain unchanged. The `size` prop controls rendered dimensions, and the former numeric suffix survives only as the default size when a glyph has no 16px source.
-
-`LinkIconRegular` and `LinkIconMedium`, `ReferenceIconRegular` and `ReferenceIconMedium`, and the three `PermissionIcon*Regular`/`PermissionIcon*Medium` pairs follow the same rule. Repository consumers use a named weight instead of a numeric export.
-
-Medium weight is reserved for deliberate emphasis: new-Session controls, Settings trigger and navigation icons, Appearance choices, the composer add button, and clickable artifact-link glyphs. Other existing consumers use Regular. The composer command menu uses `PermissionIconFullAccessRegular` for the permission command because that row names permission elevation rather than the current mode.
-
-## Alternatives considered
-
-**Keep pixel sizes in export names.** Rejected because the suffix duplicated the runtime `size` prop and created separate names for identical geometry.
-
-**Expose one component with a `weight` prop.** Rejected because call sites would hide the chosen weight inside props; explicit component names keep visual emphasis searchable and reviewable.
-
-**Override stroke width in consumer CSS.** Rejected because source SVG elements can carry their own stroke widths, and local overrides would recreate inconsistent weighting outside the library.
-
-## Consequences
-
-The icon API is intentionally breaking while pre-stable: every consumer names `Regular` or `Medium`, and former 14px call sites pass an explicit size when they share geometry with a 16px export. New product glyphs add both weights from one geometry definition. Fill-only artwork exposes both names for API consistency even when stroke weight does not change its appearance. The hierarchy choices above remain explicit at their render sites rather than becoming global size or opacity overrides.
diff --git a/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.zh.md b/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.zh.md
deleted file mode 100644
index 84031677e9..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.zh.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Agent Note: 尺寸无关的产品图标线重
-
-Status: implemented
-
-[English](2026-09-16-size-neutral-product-icon-weights.md) | 中文
-
-## 问题
-
-共享产品图标的导出名称包含一个历史渲染尺寸,但每个组件已经接受 `size` prop。消费方还会通过本地 CSS 或独立图稿加粗部分图标,视觉强调没有具名的图标库选项,因而可能在不同功能间漂移。
-
-## 决策
-
-每个共享产品图形都导出尺寸无关的 `Regular` 和 `Medium` 组件。`Regular` 渲染提供的 1px 图稿,`Medium` 使用相同路径并继承 1.3px 描边;填充区域保持不变。`size` prop 控制渲染尺寸;仅当图稿没有 16px 来源时,原数字后缀才作为默认尺寸保留。
-
-`LinkIconRegular` 与 `LinkIconMedium`、`ReferenceIconRegular` 与 `ReferenceIconMedium`,以及三组 `PermissionIcon*Regular`/`PermissionIcon*Medium` 遵循同一规则。仓库内消费方使用具名线重,不再使用带数字的导出名。
-
-Medium 线重只用于明确强调的场景:新建会话控件、设置入口与导航图标、外观选项、输入框加号按钮,以及可点击产物链接的图标。其他既有消费方使用 Regular。编辑器加号菜单中的权限命令使用 `PermissionIconFullAccessRegular`,因为该行表示权限提升,而不是当前权限模式。
-
-## 考虑过的替代方案
-
-**在导出名中保留像素尺寸。** 否决,因为后缀重复了运行时 `size` prop,并为相同几何创建了多个名称。
-
-**只导出一个带 `weight` prop 的组件。** 否决,因为调用点会把线重选择隐藏在 prop 中;显式组件名便于搜索和审查视觉强调。
-
-**在消费方 CSS 中覆盖描边宽度。** 否决,因为源 SVG 元素可能自带描边宽度,本地覆盖会在图标库外重新产生不一致线重。
-
-## 后果
-
-图标 API 在预稳定阶段有意进行破坏性调整:每个消费方明确选择 `Regular` 或 `Medium`;原 14px 调用点与 16px 导出共享几何时会显式传入尺寸。新增产品图形从同一份几何定义同时提供两种线重。仅填充的图稿也为 API 一致性导出两个名称,即使线重不会改变其外观。上述层级选择继续由对应渲染点显式声明,不会变成全局尺寸或透明度覆盖。
diff --git a/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.i18n.yaml
new file mode 100644
index 0000000000..35d2cdc77b
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-09-17-experimental-cua-object-api.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.md
+/agent-note-native-cua-objects-independent-of-evaluator-ownership:
+ en: 1ea218552c24946e
+ zh: 83524cf60abec6c8
+/agent-note-native-cua-objects-independent-of-evaluator-ownership/problem:
+ en: a22ddcfb339e503d
+ zh: a1f4fadb46fce1e7
+/agent-note-native-cua-objects-independent-of-evaluator-ownership/decision:
+ en: 2780d1b0f5ce9ea8
+ zh: ff986f8c1c90300f
+/agent-note-native-cua-objects-independent-of-evaluator-ownership/alternatives-considered:
+ en: 3d0bc77e3c432cb9
+ zh: 50a20eda1811f16c
+/agent-note-native-cua-objects-independent-of-evaluator-ownership/consequences:
+ en: 528c644d5810e60b
+ zh: 9649dfc3b1172a6d
diff --git a/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.md b/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.md
new file mode 100644
index 0000000000..f7ee24f6bc
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.md
@@ -0,0 +1,43 @@
+# Agent Note: Native CUA objects independent of evaluator ownership
+
+Status: implemented
+
+English | [中文](2026-09-17-experimental-cua-object-api.zh.md)
+
+## Problem
+
+Computer-use programs need consistent app and window operations. Coupling those operations to a CUA-specific interpreter makes input handling depend on unrelated choices about variable persistence, imports and cancellation.
+
+## Decision
+
+The shared `cua` library owns application/window objects, consent, and logged dispatch. `cua-macos` owns the Swift helper, its build, and its local connection lifecycle. Providers pass a prepared executable and a connection factory to the shared installer, keeping platform code out of object consumers without adding another runtime registry.
+
+The experimental CUA package owns a native-app subset of the Codex-style object interface and selected MIT-licensed macOS behavior adapted from Cua Driver in a Swift helper. A source manifest records the adapted upstream files and local implementation. The helper is locally compiled; the package has no external Cua Driver runtime dependency. The existing [provider registration](2026-09-12-computer-use-provider-registration.md) prevents competing computer-use providers within one composition.
+
+The facade accepts an operation dispatcher. Its tool adapter resolves the current tool binding for each operation, preserving normal authorization, logging and image admission. The execution host owns variable persistence and completion serialization. CUA defines no persistence directive, worker or run_code replacement.
+
+Native process ownership follows the [shared helper decision](2026-10-09-cua-shared-native-helper.md), with isolated Agent connections. App handles bind process identity and kernel start time; replacing the process or helper never revives old handles. AX snapshots are scoped to an app and optional window, so independent window observations remain usable. Coordinate actions select an owned window; the Host maps [screenshot coordinates](2026-10-13-cua-hover-routes-and-coordinates.md) to native window-local points. Keyboard input remains app-scoped because macOS delivers it to the app's current focus.
+
+The operation schema owns shared data declarations. Host-only limits and launch continuations remain separate from model parameters, and generated native field/enum tables preserve independent native admission without hand-maintained copies. Native code continues to own system calls, permissions and cancellation. AX formatting consumes immutable observations; the walker retains sole ownership of indices and AX references.
+
+Clipboard paste is distinct from Unicode key input: Return and Tab key events can trigger actions instead of inserting clipboard content. Pasteboard access runs on the AppKit main actor and retains promised data until a reader requests it. AX command completion alone does not establish that the application has fetched the data. Restoration checks ownership before replacing contents; the caller still verifies the application result. After successful command dispatch, `pasteReadTimeoutMs` bounds waiting for a data request, with a default of 1000 ms. Expiry returns `PASTE_NOT_CONSUMED`, closes the data provider against late delivery, and restores the clipboard only while still owned. The failure reports no read within that interval; it does not prove the application will never process its queued Paste command. A delayed command may read restored clipboard contents, so callers inspect the target and never retry automatically. Foreground-dependent consumption was observed in the installed ChatGPT/Codex application (`com.openai.codex`); other applications need their own validation. Guidance presents this as an example and applies recovery instructions to any `PASTE_NOT_CONSUMED` result.
+
+CUA registers its own Harness tool and returns operation values directly. Screenshot capture requires a resolved image-capable model and the shared attachment service. The tool admits each screenshot once and renders its durable image reference; the object API, combined observation and image sink retain the same `CuaScreenshot` containing encoded `bytes`, `mimeType`, `pixelWidth`, `pixelHeight` and `windowId` from the validated screenshot result. The bridge preserves this metadata without another upload or image emission. Callers use the reported format and dimensions when saving or declaring images.
+
+## Alternatives considered
+
+Depending on the complete upstream driver would simplify upgrades but does not meet the requirement for locally owned implementation. Copying browser and other platform code would add unsupported scope. The JSON operation protocol allows another native implementation without changing object consumers.
+
+A CUA-specific persistent evaluator duplicates execution lifecycle policy and prevents independent iteration on PTC. Requiring callers to manage raw app and snapshot tokens alone loses reusable object semantics. The facade and small tool adapter provide those semantics while leaving the evaluator outside this package.
+
+Reusing the MCP result adapter would impose MCP envelopes and wire validation on native CUA operations. Direct Harness tool output and shared attachment admission provide the needed logging and images without a second protocol.
+
+**Fixing the screenshot format** would require otherwise unnecessary re-encoding and discard the attachment pipeline’s pass-through optimization. A format convention alone still forces callers to guess, and cannot tell them when encoding policy changes. Returning the already computed format and dimensions preserves pass-through and makes each result describe its actual bytes.
+
+**Waiting until the full native-operation timeout** hides successful command dispatch behind a generic timeout. A separate configurable read budget reports the missing acknowledgement without treating slow applications as unsupported. Requiring every application to be foreground would claim a prerequisite that the available observations do not establish.
+
+## Consequences
+
+The package remains opt-in and macOS-only. Observations do not lock the desktop, and input success records dispatch rather than task completion. Callers must verify effects and must not retry uncertain input automatically. Native fixture tests verify actual window state separately from protocol and model-transcript tests. Persistent execution, binary distribution, permission onboarding and real-model task success remain separately owned work.
+
+Source builds retain a content-addressed executable per compiled generation. The [shared helper decision](2026-10-09-cua-shared-native-helper.md) owns capture-client identity and cross-Host lifetime. Retained source generations avoid replacing a live executable, while the package payload remains separately published for distribution.
diff --git a/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.zh.md b/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.zh.md
new file mode 100644
index 0000000000..c9ac90817d
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-17-experimental-cua-object-api.zh.md
@@ -0,0 +1,43 @@
+# Agent Note:独立于执行器管理的原生 CUA 对象
+
+Status: implemented
+
+[English](2026-09-17-experimental-cua-object-api.md) | 中文
+
+## 问题
+
+Computer-use 程序需要一致的应用和窗口操作。将这些操作与 CUA 专用解释器绑定,会使输入处理依赖变量持久化、导入和取消等独立设计。
+
+## 决策
+
+共享的 `cua` 库负责应用与窗口对象、授权和已记录的派发。`cua-macos` 负责 Swift helper、构建和本地连接生命周期。提供方将准备好的可执行文件和连接工厂交给共享安装函数,使对象调用方无需包含平台代码,也无需增加另一套运行时注册表。
+
+实验 CUA 包提供 Codex 风格的原生应用对象接口,并在 Swift 辅助进程中改写 Cua Driver 中选取的 MIT 许可 macOS 行为。源码清单记录改写的上游文件和本地实现。辅助进程在本地编译,本包不依赖外部 Cua Driver 运行时。已有的 [provider 注册机制](2026-09-12-computer-use-provider-registration.zh.md)防止同一组合中出现竞争 provider。
+
+对象接口接收操作分派器。工具适配模块每次操作都获取当前工具绑定,保留标准授权、日志和图片接入。执行宿主负责变量持久化和最终表达式值的序列化。CUA 不定义持久化指令、执行 worker 或 run_code 替代实现。
+
+原生进程归属遵循[共享 helper 决策](2026-10-09-cua-shared-native-helper.zh.md),Agent 连接保持隔离。应用句柄绑定进程身份及内核启动时间,替换进程或辅助进程不会使旧句柄重新有效。AX 快照按应用和可选窗口保存,不同窗口的观察可以分别使用。坐标操作选择属于该应用的窗口;Host 按[截图坐标规则](2026-10-13-cua-hover-routes-and-coordinates.zh.md)换算为原生窗口内 point。键盘输入保持应用范围,因为 macOS 将其送往应用的当前焦点。
+
+操作 schema 负责共享数据声明。Host 专属限制和启动续接与模型参数分开,生成的原生字段、枚举表保留独立原生校验,同时避免手工维护副本。系统调用、权限和取消仍由原生代码负责。AX 格式化只消费不可变观察数据,遍历器独自负责索引与 AX 引用。
+
+剪贴板粘贴与 Unicode 按键输入分别实现,因为 Return 和 Tab 按键可能触发动作,而不是插入剪贴板内容。剪贴板访问使用 AppKit 主线程,保留延迟提供的数据直到读取方请求。AX 命令完成本身不能证明应用已取得数据。恢复前检查剪贴板归属;调用方仍需验证应用中的结果。 命令成功分派后,`pasteReadTimeoutMs` 限制等待数据请求的时间,默认 1000 毫秒。到期返回 `PASTE_NOT_CONSUMED`,关闭数据提供方以阻止迟到的数据交付,并仅在仍持有剪贴板时恢复原内容。该失败只说明这段时间内没有读取,不证明应用以后不会处理已排队的 Paste 命令。迟到的命令可能读取恢复后的剪贴板内容,因此调用方必须检查目标,不能自动重试。已安装的 ChatGPT/Codex 应用(`com.openai.codex`)已观察到依赖前台才能读取的行为,其他应用需要分别验证。指引将其写为例子,并对任何 `PASTE_NOT_CONSUMED` 结果提供恢复步骤。
+
+CUA 注册自己的 Harness 工具,直接返回操作结果。截图要求解析到支持图片输入的模型,并提供共享附件服务。工具只接入一次截图,再渲染持久图片引用;对象 API、组合观察和图片输出回调保留同一个 `CuaScreenshot`,其中的编码 `bytes`、`mimeType`、`pixelWidth`、`pixelHeight` 和 `windowId` 来自已校验的截图结果。桥接保留这些元数据,不再次上传或输出图片。调用方落盘或声明图片时使用返回的格式和尺寸。
+
+## 考虑过的替代方案
+
+依赖完整上游驱动可以简化升级,但不符合本地维护实现的要求。复制浏览器及其他平台代码会引入尚不支持的范围。JSON 操作协议允许替换原生实现,而不改变对象调用方。
+
+CUA 专用持久执行器会重复实现执行生命周期规则,使 PTC 无法独立演进。仅让调用方管理原始应用和快照 token,又会失去可复用的对象语义。对象接口和轻量工具适配模块提供这些语义,并将执行器留在本包之外。
+
+复用 MCP 结果适配器会给原生 CUA 操作引入 MCP 信封和协议校验。直接使用 Harness 工具输出与共享附件接入,即可取得所需的日志和图片能力,无需再经过一种协议。
+
+**固定截图格式**会要求原本不需要的重新编码,并失去附件处理的原样直通优化。仅约定格式仍要求调用方猜测,也无法告知编码策略的变化。返回已经算出的格式和尺寸,既保留直通优化,也让每个结果说明其实际字节。
+
+**等到原生操作的总超时**会用一条通用超时掩盖命令已经成功分派的事实。独立、可配置的读取等待上限能报告缺失的读取确认,而不将响应慢的应用认定为不支持粘贴。要求所有应用都必须位于前台,则超出了现有观察能支持的结论。
+
+## 后果
+
+本包需要显式启用,且只提供 macOS 实现。观察不会锁定桌面,输入成功只记录分派完成,不能证明任务完成。调用方必须验证效果,不能自动重试结果不确定的输入。原生测试通过实际窗口状态验证效果,与协议和模型消息测试分别进行。持久执行、二进制分发、权限引导及真实模型任务成功率是分别负责的工作。
+
+源码构建按编译产物的内容摘要保留可执行文件。[共享 helper 决策](2026-10-09-cua-shared-native-helper.zh.md)负责捕获客户端身份与跨 Host 生命期。源码各代副本避免替换运行中的可执行文件,包内产物仍单独发布。
diff --git a/.agents/notes/implemented/architecture/2026-09-17-frame-shell-leading-window-chrome-seat.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-17-frame-shell-leading-window-chrome-seat.i18n.yaml
deleted file mode 100644
index 16d3ced836..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-17-frame-shell-leading-window-chrome-seat.i18n.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-17-frame-shell-leading-window-chrome-seat.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-frame-shell-leading-window-chrome-seat.md
-/agent-note-frame-owned-shell-leading-window-chrome-seat:
- en: 3e2cfa57af6b53e7
- zh: 78fe101ad11a6330
-/agent-note-frame-owned-shell-leading-window-chrome-seat/problem:
- en: 09b5defb78287f7e
- zh: b7333ce5077914a2
-/agent-note-frame-owned-shell-leading-window-chrome-seat/decision:
- en: 04853eeed03a00c5
- zh: 9542acfaa6258842
-/agent-note-frame-owned-shell-leading-window-chrome-seat/alternatives-considered:
- en: 81e997e43d185834
- zh: 38eda1f9a4bac318
-/agent-note-frame-owned-shell-leading-window-chrome-seat/consequences:
- en: 7d95c4c155f8e3c3
- zh: b3f88fe2fb7a77f0
-/agent-note-frame-owned-shell-leading-window-chrome-seat/testing:
- en: 2fcc157c9c4f0b81
- zh: 3b0e6135b1c10db1
diff --git a/.agents/notes/implemented/architecture/2026-09-17-frame-shell-leading-window-chrome-seat.md b/.agents/notes/implemented/architecture/2026-09-17-frame-shell-leading-window-chrome-seat.md
deleted file mode 100644
index cc3ef57690..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-17-frame-shell-leading-window-chrome-seat.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Agent Note: Frame-owned shell.leading window-chrome seat
-
-Status: implemented
-
-English | [中文](2026-09-17-frame-shell-leading-window-chrome-seat.zh.md)
-
-## Problem
-
-The [macOS hidden-titlebar work](../feature/2026-09-13-macos-hidden-titlebar-vibrancy.md) hid the collapsed sidebar column entirely on darwin and moved the reopen and New Session controls into a conversation-header slot (`conversation.session.header.leading`), shown via CSS while `data-sidebar-collapsed` was published. Only the Conversation had that seat: with any other main panel selected (the plugin manager, or any future global panel), a collapsed window kept the floating traffic lights over the panel's content with no reopen control anywhere on screen. Each new panel would have had to grow its own leading seat and repeat the same clearance geometry.
-
-## Decision
-
-The window chrome belongs to the frame, not to one panel. ui-layout declares a fifth root-scoped child slot, `shell.leading` (single), and AppFrame renders its seat as a frame-level box over the columns — mounted only on the darwin desktop while `sidebarCollapsed`, the full-hide state that leaves window chrome without a home (Windows' zero-width collapse keeps its fixed caption controls, so the seat must not mount a second set there). The seat sits at the frame's top-left (left 88px clears the hiddenInset traffic lights, top 11px centers the 28px controls on the conversation title row, z-index 15 above column content and below frame overlays) and carries `-webkit-app-region: no-drag`, subtracting itself from any drag band beneath it.
-
-Under the same collapsed condition the frame publishes `--dsh-frame-leading-clearance: 160px` — the inline band the traffic lights plus the seat's two controls occupy, measured from the frame's left edge. Window fullscreen hides the traffic lights (the desktop preload mirrors the state onto `html[data-fullscreen]`): the seat moves to `left: 12px` into their vacated band and the clearance drops to `84px`. The conversation title row is the one consumer: it pads `max(0px, clearance - 20px)` (its header already pads 20px). Entry pages that start below the window strip in both sidebar states (the plugin manager's page head) instead pad their top by `--dsh-frame-top-clearance` (48px, published on the darwin frame unconditionally), which clears the strip vertically rather than indenting inline.
-
-ui-sidebar registers `HeaderLeadingControls` into `shell.leading` instead of the conversation seat, reusing the shell's inject face and locale; the component renders unconditionally because the frame owns the mount condition, and its platform check and CSS collapse-gating are deleted. This also drops ui-sidebar's dependency on ui-conversation. The original `conversation.session.header.leading` slot is gone per the pre-stable API rule (every consumer updated, no compatibility shim); the conversation header now exposes its own sessionless `conversation.header.leading` seat (root-scoped, rendered without a Session) as a public extension point, empty in the shipped composition because the window-chrome controls live in `shell.leading`.
-
-## Alternatives considered
-
-**Keeping the conversation seat and adding one per panel.** Every main panel would repeat the seat markup, the drag-region subtraction, and the traffic-light geometry, and a panel that forgot would strand a collapsed window without a reopen control. One frame-owned seat makes the guarantee structural.
-
-**Mounting the occupant always and gating visibility in CSS (the prior mechanism).** This kept dead DOM and a no-drag subtraction in the header on every platform and every collapse state, and it could not help non-conversation panels at all. A frame-level TSX mount keeps the condition, the seat geometry, and the clearance publication in one file.
-
-**Gating the seat on rightbar fullscreen as well.** Unnecessary: the fullscreen right panel (z-index 40) covers the seat (15) visually, and it already opts its whole box out of window dragging, so the seat's geometry cannot puncture it.
-
-**Publishing clearance as a fixed padding each consumer hardcodes.** A raw `160px` in each panel would silently desynchronize from the seat's real band; one custom property published under exactly the mount condition keeps consumers correct when the geometry changes.
-
-## Consequences
-
-- Every current and future main panel keeps the reopen and New Session controls while the sidebar is hidden, at zero per-panel cost; the conversation opts into `--dsh-frame-leading-clearance`, and entry pages take the unconditional `--dsh-frame-top-clearance` instead.
-- `conversation.session.header.leading` no longer exists in the client catalog; `shell.leading` carries the window chrome as a public root-scoped seat, while the conversation header's sessionless `conversation.header.leading` remains a separate public seat with no shipped occupant.
-- ui-sidebar no longer depends on ui-conversation (inject list, devDependency, and tsconfig reference removed).
-- The mount condition names darwin explicitly: Windows' `data-windows-titlebar` collapse also yields a zero-width column but keeps its own fixed caption controls, which a frame seat would duplicate. A platform that later hides its column without leaving its own chrome opts in by widening the condition.
-
-## Testing
-
-The ui-layout app-frame spec pins the seat's mount and unmount across the darwin collapse and its absence beside the 56px rail; the apply spec pins the five-slot declaration and teardown. The ui-sidebar apply spec pins the `shell.leading` registration (component, locale, shared inject face) and its removal on dispose; the sidebar-root spec pins the two controls' actions.
diff --git a/.agents/notes/implemented/architecture/2026-09-17-frame-shell-leading-window-chrome-seat.zh.md b/.agents/notes/implemented/architecture/2026-09-17-frame-shell-leading-window-chrome-seat.zh.md
deleted file mode 100644
index c652f313d4..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-17-frame-shell-leading-window-chrome-seat.zh.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Agent Note: 框架持有的 shell.leading 窗口 chrome 座
-
-Status: implemented
-
-[English](2026-09-17-frame-shell-leading-window-chrome-seat.md) | 中文
-
-## Problem
-
-[macOS 隐藏标题栏工作](../feature/2026-09-13-macos-hidden-titlebar-vibrancy.zh.md)在 darwin 上把收起的侧边栏整列隐藏,并把重新打开与 New Session 控件移入会话头部的 slot(`conversation.session.header.leading`),在 `data-sidebar-collapsed` 发布期间由 CSS 显示。但只有会话界面有这个座:选中任何其他主面板(插件管理器或未来的全局面板)时,收起的窗口只剩悬浮的红绿灯压在面板内容上,屏幕上没有任何重新打开控件。每个新面板都得自建 leading 座并重复同一套避让几何。
-
-## Decision
-
-窗口 chrome 属于框架,不属于某个面板。ui-layout 声明第五个 root 作用域子 slot `shell.leading`(single),AppFrame 把它的座渲染为覆盖各列的框架级盒子——仅在 darwin 桌面且 `sidebarCollapsed` 时挂载,即让窗口 chrome 无处安放的全隐藏状态(Windows 的零宽收起保留其固定 caption 控件,该座在那里挂载会出现第二组控件)。座位于框架左上角(left 88px 避开 hiddenInset 红绿灯,top 11px 让 28px 控件对齐会话标题行中线,z-index 15 高于列内容、低于框架浮层),并带 `-webkit-app-region: no-drag`,把自己从下方任何拖拽带中减去。
-
-在同一收起条件下框架发布 `--dsh-frame-leading-clearance: 160px`——红绿灯加座内两个控件占据的行内带宽,自框架左边缘起量。窗口全屏隐藏红绿灯(桌面 preload 把状态镜像到 `html[data-fullscreen]`):座移入其空出的带宽(`left: 12px`),避让量降到 `84px`。会话标题行是唯一消费者:它 pad `max(0px, clearance - 20px)`(其头部已 pad 20px)。两种侧栏状态下都始于窗口顶带之下的入口页(插件管理器页头)改用 `--dsh-frame-top-clearance`(48px,darwin 框架上无条件发布)做顶部内边距,纵向避开顶带而非行内缩进。
-
-ui-sidebar 把 `HeaderLeadingControls` 改注册到 `shell.leading` 而非会话座,复用 shell 的 inject face 与 locale;因挂载条件归框架所有,组件无条件渲染,其平台判断与 CSS 收起门控被删除,同时解除 ui-sidebar 对 ui-conversation 的依赖。原 `conversation.session.header.leading` slot 按 pre-stable API 规则不复存在(更新所有消费者,不留兼容垫层);会话头现在自带无会话可用的 `conversation.header.leading` 座(root 作用域、无 Session 也渲染)作为公开扩展点,因窗口 chrome 控件已住进 `shell.leading`,出厂组合中它保持空置。
-
-## Alternatives considered
-
-**保留会话座并为每个面板各加一个。** 每个主面板都要重复座标记、拖拽区减除与红绿灯几何,漏掉的面板会让收起的窗口失去重新打开控件。框架持有的单一座使该保证成为结构性的。
-
-**始终挂载占用方、由 CSS 控制可见性(先前机制)。** 它在所有平台与所有收起状态下都保留死 DOM 和头部的 no-drag 减除,且完全帮不到非会话面板。框架级 TSX 挂载把条件、座几何与避让量发布集中在一个文件。
-
-**同时以右栏全屏为座的门控条件。** 不必要:全屏右面板(z-index 40)在视觉上盖住座(15),且它已让整个盒子退出窗口拖拽,座的几何无法穿透它。
-
-**把避让量作为各消费者硬编码的固定内边距。** 各面板写死 `160px` 会与座的真实带宽悄然失同步;在恰为挂载条件下发布一个自定义属性,几何变化时消费者仍保持正确。
-
-## Consequences
-
-- 侧边栏隐藏时,现有与未来的每个主面板都免费获得重新打开与 New Session 控件;会话接入 `--dsh-frame-leading-clearance`,入口页改用无条件发布的 `--dsh-frame-top-clearance`。
-- `conversation.session.header.leading` 从客户端目录中消失;`shell.leading` 作为公开 root 作用域座承载窗口 chrome,会话头无会话可用的 `conversation.header.leading` 则是另一个公开座,出厂组合中无占用者。
-- ui-sidebar 不再依赖 ui-conversation(inject 列表、devDependency 与 tsconfig 引用均已移除)。
-- 挂载条件显式点名 darwin:Windows 的 `data-windows-titlebar` 收起同样得到零宽列,但保留自己的固定 caption 控件,框架座会与之重复。未来某平台若隐藏整列且不留自己的 chrome,通过放宽该条件接入。
-
-## Testing
-
-ui-layout 的 app-frame spec 钉住 darwin 收起下座的挂载/卸载及 56px 控制栏旁座的缺席;apply spec 钉住五 slot 声明与拆除。ui-sidebar 的 apply spec 钉住 `shell.leading` 注册(组件、locale、共享 inject face)及 dispose 时的移除;sidebar-root spec 钉住两个控件的动作。
diff --git a/.agents/notes/implemented/architecture/2026-09-17-native-v4-read-validation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-17-native-v4-read-validation.i18n.yaml
index cfe428cf55..ecb3c391ff 100644
--- a/.agents/notes/implemented/architecture/2026-09-17-native-v4-read-validation.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-17-native-v4-read-validation.i18n.yaml
@@ -9,11 +9,11 @@
en: cfb527c1c2ca661a
zh: b61a906330cdaf97
/agent-note-mandatory-native-v4-relationship-validation/decision:
- en: 14feaf74a58d5609
- zh: 8aea80c89110e638
+ en: 534efaae5eb9beb0
+ zh: ccb3734458248383
/agent-note-mandatory-native-v4-relationship-validation/alternatives-considered:
- en: d6296e00a6b30d48
- zh: 9f7074c69fb42e5f
+ en: 96a37d9ba0252630
+ zh: fdf2d085805f0010
/agent-note-mandatory-native-v4-relationship-validation/consequences:
en: b76aaca1c35dcc6c
zh: b933c2518c64c60c
diff --git a/.agents/notes/implemented/architecture/2026-09-17-native-v4-read-validation.md b/.agents/notes/implemented/architecture/2026-09-17-native-v4-read-validation.md
index b111e9f1dd..385377db23 100644
--- a/.agents/notes/implemented/architecture/2026-09-17-native-v4-read-validation.md
+++ b/.agents/notes/implemented/architecture/2026-09-17-native-v4-read-validation.md
@@ -16,7 +16,7 @@ The V4 format restorer and current JSONL scanner share one mandatory cross-event
An unfinished tail may retain an open turn, step, tool call, or compaction. A closing event must settle the relationships it closes. An inherited unfinished compaction ends at its `session/end-seed` marker; it does not constrain the child's lifecycle. Repair message identities retain their historical numeric suffix after earlier migrations change sequence coordinates. Fork repairs retain their separately validated native identities.
-Installed Session adoption continues to own event envelopes, message roles and metadata, canonical request headers, and surface replacement reference coverage. Optional runtime invariants provide diagnostics; their installation is not required for durable admission. Native system-message admission also validates coordinates, identity, content blocks, and known image/tool-call fields before physical recovery can discard rows. Additional JSON fields and unknown nonempty content tags remain intact; retired tool-result wrappers are refused. Retired `request/header.header.system` and required `tool/code-dispatch*` tags remain hard refusals before recovery; obsolete ignorable PTC records remain opaque. Physical framing alone does not establish semantic validity.
+Installed Session adoption continues to own event envelopes, message roles and metadata, canonical request headers, and surface replacement reference coverage. Native system-message admission also validates coordinates, identity, content blocks, and known image/tool-call fields before physical recovery can discard rows. Additional JSON fields and unknown nonempty content tags remain intact; retired tool-result wrappers are refused. Retired `request/header.header.system` and required `tool/code-dispatch*` tags remain hard refusals before recovery; obsolete ignorable PTC records remain opaque. Physical framing alone does not establish semantic validity.
A retired tool-result wrapper outside its owning `tool/result` row would require changing the event, role, or source to represent it in V4. The adjacent migration lifts only the canonical owned wrapper; target restoration refuses wrappers remaining in interpreted content. Native row admission applies the same refusal before recoverable tail suppression. The check follows declared message, content, and Assistant-stream slots instead of recursively inspecting JSON, so tool arguments, replay state, schema parameters, nested extensions, and unknown ignorable payloads retain their recorded values.
@@ -26,8 +26,6 @@ A retired tool-result wrapper outside its owning `tool/result` row would require
**Project V4 back into a frozen V3 validation view.** This couples current acceptance to retired representations and invites new fields to disappear in the projection. Frozen generations remain independently readable, while the current restorer interprets its own fields.
-**Rely on optional runtime invariants.** Detached readers and restoration paths may not install diagnostic companions. A required relationship must be checked before publication or consumption of the restored Session.
-
## Consequences
The native restorer maintains one bounded state machine per read and rejects contradictions before successor publication. Frozen V0–V3 validators and committed Session generations remain unchanged. Public catalog regressions cover invalid relationships alongside partial tails, inherited cuts, immutable unknown attribution, and complete tool exchanges; these checks enforce the declared V4 representation without changing its schemas. Breaking changes require a successor; compatible additions use new acknowledgements, and implementation fixes preserve the declared meaning.
diff --git a/.agents/notes/implemented/architecture/2026-09-17-native-v4-read-validation.zh.md b/.agents/notes/implemented/architecture/2026-09-17-native-v4-read-validation.zh.md
index 20f1dc53b6..169a80ecf7 100644
--- a/.agents/notes/implemented/architecture/2026-09-17-native-v4-read-validation.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-17-native-v4-read-validation.zh.md
@@ -16,7 +16,7 @@ V4 格式恢复器与当前 JSONL 扫描器共用同一强制跨事件关系校
未完成的尾部可以保留打开的 turn、step、工具调用或压缩事务。关闭事件必须结清它关闭的关系。继承的未完成压缩事务在其 `session/end-seed` 标记处结束,不约束子 Session 的生命周期。较早迁移改变序号坐标后,修复消息身份仍保留历史数字后缀。fork 修复保留经过独立校验的原生身份。
-已安装的 Session 接纳器继续负责事件封装、消息角色与元数据、规范请求头,以及 surface 替换的引用覆盖。可选运行时不变式提供诊断;持久化准入不要求安装它们。原生 system 消息准入还会在物理恢复可能丢弃行之前,校验坐标、身份、内容块及已知 image/tool-call 字段。额外 JSON 字段和非空的未知内容标签保持不变,退役的 tool-result 包装会被拒绝。退役的 `request/header.header.system` 和必需的 `tool/code-dispatch*` 标签仍在恢复前强制拒绝;标记为可忽略的过时 PTC 记录保持不透明。仅通过物理分帧不能证明语义有效。
+已安装的 Session 接纳器继续负责事件封装、消息角色与元数据、规范请求头,以及 surface 替换的引用覆盖。原生 system 消息准入还会在物理恢复可能丢弃行之前,校验坐标、身份、内容块及已知 image/tool-call 字段。额外 JSON 字段和非空的未知内容标签保持不变,退役的 tool-result 包装会被拒绝。退役的 `request/header.header.system` 和必需的 `tool/code-dispatch*` 标签仍在恢复前强制拒绝;标记为可忽略的过时 PTC 记录保持不透明。仅通过物理分帧不能证明语义有效。
退役的 tool-result 包装若不属于对应的 `tool/result` 行,就需要改变事件、角色或来源才能在 V4 中表示。相邻迁移只提升规范的自有包装;目标恢复器拒绝解释内容中剩余的包装。原生行准入在可恢复尾部被忽略之前执行同样的拒绝。检查沿声明的消息、内容和 Assistant stream 字段进行,不递归扫描 JSON,因此工具参数、replay state、schema 参数、嵌套扩展和未知可忽略载荷保留其记录值。
@@ -26,8 +26,6 @@ V4 格式恢复器与当前 JSONL 扫描器共用同一强制跨事件关系校
**将 V4 投影回冻结的 V3 校验视图。** 这会将当前准入与退役表示绑定,并容易让新增字段在投影中消失。冻结代际仍可独立读取,当前恢复器解释自己的字段。
-**依赖可选运行时不变式。** 分离读取器和恢复路径可能不安装诊断组件。必需关系必须在发布或消费恢复后的 Session 之前完成校验。
-
## Consequences
原生恢复器为每次读取维护独立状态机,在发布后继文件之前拒绝矛盾。冻结的 V0–V3 校验器及已提交的 Session 代际保持不变。公开目录回归覆盖无效关系、部分尾部、继承切分点、未知归属的不可变保存,以及完整工具交互;这些检查落实已声明的 V4 表示,不改变其 schema。破坏性变更要求后继版本;兼容新增使用新的确认记录,实现修复须保留声明的含义。
diff --git a/.agents/notes/implemented/architecture/2026-09-17-plugin-detail-page-extension-slots.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-17-plugin-detail-page-extension-slots.i18n.yaml
deleted file mode 100644
index 5b7f7e9b6b..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-17-plugin-detail-page-extension-slots.i18n.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-17-plugin-detail-page-extension-slots.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-plugin-detail-page-extension-slots.md
-/agent-note-plugin-detail-pages-as-an-extension-point:
- en: 71b432dfc4475c08
- zh: a9f9a7e272c32093
-/agent-note-plugin-detail-pages-as-an-extension-point/problem:
- en: 39f7e246e6ab3beb
- zh: dba3ba5206d81d8d
-/agent-note-plugin-detail-pages-as-an-extension-point/decision:
- en: df2cba037b6bcb4d
- zh: 82d0a5dc61a1716c
-/agent-note-plugin-detail-pages-as-an-extension-point/alternatives-considered:
- en: cdcc8710be8ab1be
- zh: d82781a3c353984f
-/agent-note-plugin-detail-pages-as-an-extension-point/consequences:
- en: 0567cb3fbfbdb4f8
- zh: 256811c9a7271da1
-/agent-note-plugin-detail-pages-as-an-extension-point/testing:
- en: 8957db157d2f743e
- zh: fb24db99bb5a55aa
diff --git a/.agents/notes/implemented/architecture/2026-09-17-process-local-blank-sessions.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-17-process-local-blank-sessions.i18n.yaml
index 25c6bed4ad..5441a65985 100644
--- a/.agents/notes/implemented/architecture/2026-09-17-process-local-blank-sessions.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-17-process-local-blank-sessions.i18n.yaml
@@ -9,14 +9,14 @@
en: 837ebb0071a5611b
zh: 23d0f0713889ead6
/agent-note-writer-acquisition-for-blank-session-reuse/decision:
- en: 932bc5c0be3721de
- zh: 000425c5f053e74e
+ en: a54db0d4a97aa9b8
+ zh: a52d257b657fe99b
/agent-note-writer-acquisition-for-blank-session-reuse/alternatives-considered:
en: 2a67a53f7f396ba8
zh: 6df6ccc071fb0de2
/agent-note-writer-acquisition-for-blank-session-reuse/consequences:
- en: 2e1f1894337689a2
- zh: d0cab07cc48083ee
+ en: 8307612b7476e8f3
+ zh: 87fa39e58d33d4f1
/agent-note-writer-acquisition-for-blank-session-reuse/verification:
- en: 4680e845e2ab294e
- zh: ea5890c5ffb09831
+ en: 4367d55ae9dce5c1
+ zh: c610626f4e15eb77
diff --git a/.agents/notes/implemented/architecture/2026-09-17-process-local-blank-sessions.md b/.agents/notes/implemented/architecture/2026-09-17-process-local-blank-sessions.md
index 58d222185a..d6be46b3ed 100644
--- a/.agents/notes/implemented/architecture/2026-09-17-process-local-blank-sessions.md
+++ b/.agents/notes/implemented/architecture/2026-09-17-process-local-blank-sessions.md
@@ -10,11 +10,11 @@ Two Hosts sharing Session storage can select the same blank Session when opening
## Decision
-The Session catalog includes persisted blanks. Startup restoration selects the saved blank; ordinary Workspace navigation selects the first eligible unarchived member in catalog order. Explicit-id `session.create` resumes that Session and acquires its writer before opening history. Only `session/writer-held` triggers creation of a new Session. Other errors propagate. Concurrent Workspace connects in one Client share the acquisition and fallback. Later navigation cancels a pending startup selection.
+The Session catalog includes persisted blanks. Startup restoration selects the saved blank; ordinary Workspace navigation selects the first eligible unarchived member in catalog order. Explicit-id `session.create` names that Session; the Host resumes it and acquires its writer before opening history, and creates a new Session when another owner holds the writer ([reuse and preset rules](../bug-fix/2026-09-29-reused-blank-runs-requested-preset.md)). Other errors propagate. A Workspace connect in one Client joins the latest in-flight connect to that Workspace only when both request the same preset. Later navigation cancels a pending startup selection.
The Agent retains the acquired handle throughout its lifetime. Checking ownership and releasing a probe lock would leave a race before resume. No PID or process identity enters persisted data or the Remote schema.
-This decision extends the blank-reuse policy in the [Client Session scope decision](2026-07-25-web-client-session-scope-and-provide-channel.md); its real-Session slash support and Provider adoption rationale remain active.
+This decision extends the blank-reuse policy in the [Client Session scope reference](2026-09-15-client-session-references.md); its real-Session slash support and Provider adoption rationale remain active.
## Alternatives considered
@@ -28,8 +28,8 @@ This decision extends the blank-reuse policy in the [Client Session scope decisi
## Consequences
-A Host can reuse its own live blank or reclaim an unowned persisted blank with its slash state. Separate Hosts create distinct blanks while existing writers remain held. A held selected blank causes fresh creation even when another blank is free; navigation does not search for another candidate. No cleanup is added. Opening the same established conversation from two Hosts can still encounter writer contention. Unknown projection hints remain visible without cold body scans; automatic reuse requires known blank metadata. A non-contention acquisition failure aborts New Session and is currently reported only to the console; hidden blanks have no recovery action in this flow.
+A Host can reuse its own live blank or reclaim an unowned persisted blank with its slash state. Separate Hosts create distinct blanks while existing writers remain held. A held selected blank causes fresh creation even when another blank is free; navigation does not search for another candidate. No cleanup is added. Opening the same established conversation from two Hosts can still encounter writer contention. Unknown projection hints remain visible without cold body scans; automatic reuse requires known blank metadata. A non-contention acquisition failure aborts the navigation; a hero Workspace pick or explicit draft preparation reports it as a notice and startup restoration only to the console, and a hidden blank that fails this way has no recovery action in this flow.
## Verification
-Navigation tests cover saved selection, single-candidate contention fallback, non-contention failures, overlapping acquisition, and superseding navigation. A shipped Web-composition test holds a real persistence writer, verifies fresh creation despite another free blank, and reclaims the released selected blank with its plan state. The fresh-round-trip recorded Session scenario reloads its blank before submitting the recorded prompt.
+Navigation tests cover saved selection, opening the Session the Host returns for a held candidate, non-contention failures, overlapping acquisition, and superseding navigation. A shipped Web-composition test holds a real persistence writer, verifies fresh creation despite another free blank, and reclaims the released selected blank with its plan state. The fresh-round-trip recorded Session scenario reloads its blank before submitting the recorded prompt.
diff --git a/.agents/notes/implemented/architecture/2026-09-17-process-local-blank-sessions.zh.md b/.agents/notes/implemented/architecture/2026-09-17-process-local-blank-sessions.zh.md
index 0f3fdf0509..d477e9770c 100644
--- a/.agents/notes/implemented/architecture/2026-09-17-process-local-blank-sessions.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-17-process-local-blank-sessions.zh.md
@@ -10,11 +10,11 @@ Status: implemented
## Decision
-Session 列表包含持久化空白会话。启动恢复选择已保存的空白会话;普通 Workspace 导航按列表顺序选择第一个符合条件且未归档的成员。显式 ID 的 `session.create` 会恢复该 Session,并在打开历史前取得写锁。只有 `session/writer-held` 会触发新建 Session。其他错误返回调用方。同一 Client 的并发 Workspace 连接共享获取与回退过程。后续导航会取消尚未完成的启动选择。
+Session 列表包含持久化空白会话。启动恢复选择已保存的空白会话;普通 Workspace 导航按列表顺序选择第一个符合条件且未归档的成员。显式 ID 的 `session.create` 指明该 Session;Host 恢复它并在打开历史前取得写锁,写锁被其他写者持有时改为新建 Session([复用与 preset 规则](../bug-fix/2026-09-29-reused-blank-runs-requested-preset.zh.md))。其他错误返回调用方。同一 Client 中的 Workspace 连接仅在与该 Workspace 最近一次进行中的连接请求相同 preset 时与之合并。后续导航会取消尚未完成的启动选择。
Agent 在整个生命周期内持有取得的句柄。检查占用后释放探测锁,会在恢复前留下竞态。持久化数据与 Remote schema 均不引入 PID 或进程身份。
-本决策扩展 [Client Session scope 决策](2026-07-25-web-client-session-scope-and-provide-channel.zh.md)中的空白会话复用策略;其真实 Session 支持 slash 以及 Provider 收养的理由仍然有效。
+本决策扩展 [Client Session scope 参考](2026-09-15-client-session-references.zh.md)中的空白会话复用策略;其真实 Session 支持 slash 以及 Provider 收养的理由仍然有效。
## Alternatives considered
@@ -28,8 +28,8 @@ Agent 在整个生命周期内持有取得的句柄。检查占用后释放探
## Consequences
-Host 可以复用自己的活动空白会话,也可以接管无人占用的持久化空白会话并保留其 slash 状态。现有写锁被持有时,不同 Host 创建不同的空白会话。所选空白会话被占用时,即使另有空闲空白会话也会新建;导航不再寻找其他候选。不增加清理逻辑。两个 Host 打开同一个已有对话时仍可能遇到写锁争用。未知投影提示仍可见且不扫描冷日志正文;自动复用要求已知的空白元数据。非锁争用的获取错误会中止新建会话,目前只报告到控制台;此流程没有针对隐藏空白会话的恢复操作。
+Host 可以复用自己的活动空白会话,也可以接管无人占用的持久化空白会话并保留其 slash 状态。现有写锁被持有时,不同 Host 创建不同的空白会话。所选空白会话被占用时,即使另有空闲空白会话也会新建;导航不再寻找其他候选。不增加清理逻辑。两个 Host 打开同一个已有对话时仍可能遇到写锁争用。未知投影提示仍可见且不扫描冷日志正文;自动复用要求已知的空白元数据。非锁争用的获取错误会中止该次导航;在 hero 中选择工作区或显式准备草稿时以提示展示该错误,启动恢复只报告到控制台,以此方式失败的隐藏空白会话在此流程中没有恢复操作。
## Verification
-导航测试覆盖已保存选择、单候选锁争用回退、其他错误、重叠获取以及后续导航替代。实际 Web 组合测试持有真实持久化写锁,验证另有空闲空白会话时仍新建,并接管已释放的所选空白会话及其 plan 状态。fresh-round-trip 的录制 Session 场景在提交已录制的提示词前刷新空白会话。
+导航测试覆盖已保存选择、为被占用候选打开 Host 返回的 Session、其他错误、重叠获取以及后续导航替代。实际 Web 组合测试持有真实持久化写锁,验证另有空闲空白会话时仍新建,并接管已释放的所选空白会话及其 plan 状态。fresh-round-trip 的录制 Session 场景在提交已录制的提示词前刷新空白会话。
diff --git a/.agents/notes/implemented/architecture/2026-09-17-session-row-menu-actions-slot.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-17-session-row-menu-actions-slot.i18n.yaml
deleted file mode 100644
index 40bc5939fe..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-17-session-row-menu-actions-slot.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-17-session-row-menu-actions-slot.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-session-row-menu-actions-slot.md
-/agent-note-session-row-actions-as-slot-lists:
- en: 6c14eadc272cbe5f
- zh: d0fb9907cddcae5a
-/agent-note-session-row-actions-as-slot-lists/problem:
- en: 1afb986570a4bb49
- zh: 22862e82275038b7
-/agent-note-session-row-actions-as-slot-lists/decision:
- en: 7a353dd9c0642f56
- zh: e503a953cccfdc47
-/agent-note-session-row-actions-as-slot-lists/alternatives-considered:
- en: 19fc0c951e9e49b5
- zh: 9010c3fb55b71ce0
-/agent-note-session-row-actions-as-slot-lists/consequences:
- en: cfc90eb041452a48
- zh: 882748ceb29f21ea
diff --git a/.agents/notes/implemented/architecture/2026-09-17-settings-pages-as-companion-packages.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-17-settings-pages-as-companion-packages.i18n.yaml
deleted file mode 100644
index 34faaaba25..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-17-settings-pages-as-companion-packages.i18n.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-17-settings-pages-as-companion-packages.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-settings-pages-as-companion-packages.md
-/agent-note-settings-pages-as-companion-packages:
- en: ada5eadeb6e921e4
- zh: 1cc804b1720c2c98
-/agent-note-settings-pages-as-companion-packages/problem:
- en: 8c23fffd3cef6ea8
- zh: 6072e1e4197afd7c
-/agent-note-settings-pages-as-companion-packages/decision:
- en: d0610e0445b8fdb7
- zh: 5e8af1fce960cae5
-/agent-note-settings-pages-as-companion-packages/alternatives-considered:
- en: cc29e94d925d84b4
- zh: bdaec6d6ff2d511a
-/agent-note-settings-pages-as-companion-packages/consequences:
- en: ef4733505d43fb1e
- zh: 320475e4b5a0f5e0
-/agent-note-settings-pages-as-companion-packages/testing:
- en: 92bef8e1c609359b
- zh: c4f2016243441b1e
diff --git a/.agents/notes/implemented/architecture/2026-09-17-shared-office-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-17-shared-office-runtime.i18n.yaml
deleted file mode 100644
index 4aa201536b..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-17-shared-office-runtime.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-17-shared-office-runtime.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-shared-office-runtime.md
-/agent-note-shared-office-runtime-for-desktop-and-sdk:
- en: 28af688a50a2d5c6
- zh: ee73d6dd4fab9184
-/agent-note-shared-office-runtime-for-desktop-and-sdk/problem:
- en: 4d08aed08c8aff27
- zh: d618b4ac68832867
-/agent-note-shared-office-runtime-for-desktop-and-sdk/decision:
- en: f92ce88d82a076ed
- zh: 341e486effad4b6d
-/agent-note-shared-office-runtime-for-desktop-and-sdk/alternatives-considered:
- en: d85334a384a32a7d
- zh: 61c370390d9fdcf9
-/agent-note-shared-office-runtime-for-desktop-and-sdk/consequences:
- en: f236fec61d99ba16
- zh: 859ee94e05a95aa4
diff --git a/.agents/notes/implemented/architecture/2026-09-17-web-feature-routes-and-route-gate.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-17-web-feature-routes-and-route-gate.i18n.yaml
deleted file mode 100644
index 735a46bdc2..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-17-web-feature-routes-and-route-gate.i18n.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-17-web-feature-routes-and-route-gate.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-web-feature-routes-and-route-gate.md
-/agent-note-feature-routes-follow-the-document-base-and-a-gate-keeps-them-there:
- en: ca746e8d558cfc31
- zh: 3524931b0bf9991f
-/agent-note-feature-routes-follow-the-document-base-and-a-gate-keeps-them-there/problem:
- en: d4a1124ae3b6f6a8
- zh: 4814da5d08947bc7
-/agent-note-feature-routes-follow-the-document-base-and-a-gate-keeps-them-there/decision:
- en: 5b89e02010c2da46
- zh: d11e67d86ef27908
-/agent-note-feature-routes-follow-the-document-base-and-a-gate-keeps-them-there/alternatives-considered:
- en: d90dbd0c95914085
- zh: 6256f4fd76a7cb55
-/agent-note-feature-routes-follow-the-document-base-and-a-gate-keeps-them-there/consequences:
- en: 94530852d7746ddb
- zh: ad2383c465053822
-/agent-note-feature-routes-follow-the-document-base-and-a-gate-keeps-them-there/testing:
- en: 95c952092857cb53
- zh: 232cd731afcd3288
diff --git a/.agents/notes/implemented/architecture/2026-09-18-agent-teams-single-bundle.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-18-agent-teams-single-bundle.i18n.yaml
deleted file mode 100644
index 83ed8c5841..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-18-agent-teams-single-bundle.i18n.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-18-agent-teams-single-bundle.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-18-agent-teams-single-bundle.md
-/agent-note-enable-agent-teams-with-one-bundle:
- en: c595b526791a8dfd
- zh: 61f4bf6948e41bee
-/agent-note-enable-agent-teams-with-one-bundle/problem:
- en: 8dd802c775d24e42
- zh: 53cf2818dd93181e
-/agent-note-enable-agent-teams-with-one-bundle/decision:
- en: d513c3239b59b2ee
- zh: 7692b7ae43befc9d
-/agent-note-enable-agent-teams-with-one-bundle/alternatives-considered:
- en: 32952c4e1b6f486d
- zh: 6c9b91eaf0460d30
-/agent-note-enable-agent-teams-with-one-bundle/consequences:
- en: be92cb1ce7c86cb6
- zh: 0da06f76df72f97e
-/agent-note-enable-agent-teams-with-one-bundle/verification:
- en: 8fc694c4c84401f1
- zh: 6540462f6896bef7
diff --git a/.agents/notes/implemented/architecture/2026-09-18-chat-navigation-performance.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-18-chat-navigation-performance.i18n.yaml
deleted file mode 100644
index d8ea21a542..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-18-chat-navigation-performance.i18n.yaml
+++ /dev/null
@@ -1,31 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-18-chat-navigation-performance.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-18-chat-navigation-performance.md
-/agent-note-bound-turn-navigation-work-and-batch-historical-jumps:
- en: c37ea37bfa045a00
- zh: 88a75b8739e99fb5
-/agent-note-bound-turn-navigation-work-and-batch-historical-jumps/problem:
- en: c3ca28379b414fc5
- zh: 4a25378cef3f2259
-/agent-note-bound-turn-navigation-work-and-batch-historical-jumps/decision:
- en: a15c7c13f167e5ac
- zh: a15c7c13f167e5ac
-/agent-note-bound-turn-navigation-work-and-batch-historical-jumps/decision/fixed-pitch-virtual-rail:
- en: 79e61c0dbffe6f01
- zh: 47dd4758d115f8bb
-/agent-note-bound-turn-navigation-work-and-batch-historical-jumps/decision/two-layer-transcript-navigation:
- en: 022a8ad83ac099f1
- zh: b4393e39bfdd401d
-/agent-note-bound-turn-navigation-work-and-batch-historical-jumps/decision/one-publication-per-historical-jump:
- en: 510e80edc58ebdea
- zh: cfbff46bff1e43bb
-/agent-note-bound-turn-navigation-work-and-batch-historical-jumps/decision/theoretical-work-model:
- en: 6786e39e812ad355
- zh: eca00072cf880a08
-/agent-note-bound-turn-navigation-work-and-batch-historical-jumps/alternatives-considered:
- en: f590762936560d5c
- zh: 0fc836bf1841733e
-/agent-note-bound-turn-navigation-work-and-batch-historical-jumps/consequences:
- en: 5f50c3d4a539f48c
- zh: 5aee80a99d52233e
diff --git a/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.i18n.yaml
index 1f07fe4f8b..455daa7fff 100644
--- a/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.i18n.yaml
@@ -9,8 +9,8 @@
en: 725f773bb350218e
zh: dd8fbc09ef48208d
/agent-note-declarative-agent-presets-and-retained-revisions/decision:
- en: 2a2489303fc76fc0
- zh: 1a760e9a5edfccc8
+ en: 88edb925b556300c
+ zh: c6f05b641513d768
/agent-note-declarative-agent-presets-and-retained-revisions/alternatives-considered:
en: 09f40eab4659e9f6
zh: 7a70da0d0ae6845b
diff --git a/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.md b/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.md
index bf900bc4dc..38fa045ef1 100644
--- a/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.md
+++ b/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.md
@@ -16,7 +16,7 @@ The registry owns each revision's scope and Loader tree. Agents link their own s
The Web editor and `agent_preset` tool accept only child plugin YAML. Saving writes the active profile's user patch, preserving display metadata and unrelated configuration. Revision checks and profile locking prevent overwriting concurrent saves; validation rejects a higher-priority override that would defeat the edit. Persistence and activation outcomes are reported separately. Tool writes require approval or full access because configuration executes in the Host.
-Session data continues to record the preset identity. Restart resolves that identity against current configuration and rejects a missing definition. Old executable revisions are process-local, not serialized. The saved user default and blank-session selection remain; `selectedDefault` is what an unnamed new session resolves, and the Client's Developer tools preference decides only whether the choice is shown.
+Session data continues to record the preset identity. Restart resolves that identity against current configuration and rejects a missing definition; reusing a blank Session for a new conversation does not resolve it ([decision](../bug-fix/2026-09-29-reused-blank-runs-requested-preset.md)). Old executable revisions are process-local, not serialized. New sessions without an explicit preset resolve `selectedDefault`; Client choice filtering and default correction follow [the preset UI policy](../../../../packages/client/ui-agent-preset/README.md).
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.zh.md b/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.zh.md
index 21611da37f..4da53eb366 100644
--- a/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.zh.md
@@ -16,7 +16,7 @@ Preset 目录重复承担 Cordis 配置的所有权。独立的发现、元数
Web 编辑器与 `agent_preset` 工具只接受子插件 YAML。保存写入当前 profile 的用户补丁,保留显示元数据和无关配置。版本检查和 profile 锁防止覆盖并发保存;验证拒绝会使编辑失效的更高优先级覆盖。持久化与激活结果分别报告。配置在 Host 中执行,因此工具写入需要批准或完全访问权限。
-会话数据继续记录 preset 标识。重启时根据当前配置解析该标识,定义缺失则拒绝恢复。旧的可执行代际仅保留在进程内,不会序列化。保存的用户默认值与空白会话选择保持不变:未指定 preset 的新会话解析 `selectedDefault`,客户端的开发者工具偏好只决定是否展示这项选择。
+会话数据继续记录 preset 标识。重启时根据当前配置解析该标识,定义缺失则拒绝恢复;为新对话复用空白会话时不解析该标识([决策](../bug-fix/2026-09-29-reused-blank-runs-requested-preset.zh.md))。旧的可执行代际仅保留在进程内,不会序列化。未显式指定 preset 的新会话解析 `selectedDefault`;客户端选项过滤和默认值修正遵循[预设 UI 策略](../../../../packages/client/ui-agent-preset/README.zh.md)。
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-09-18-localized-package-metadata.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-18-localized-package-metadata.i18n.yaml
deleted file mode 100644
index 828454ccca..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-18-localized-package-metadata.i18n.yaml
+++ /dev/null
@@ -1,25 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-18-localized-package-metadata.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-18-localized-package-metadata.md
-/agent-note-plugin-owned-localized-display-metadata:
- en: b78ee8e580f068ea
- zh: 17dd59c9b6273897
-/agent-note-plugin-owned-localized-display-metadata/problem:
- en: 4022a58baf82e77d
- zh: 8439268ed4df9b69
-/agent-note-plugin-owned-localized-display-metadata/decision:
- en: f6c3e24681977f48
- zh: 84ae52ec62f4af96
-/agent-note-plugin-owned-localized-display-metadata/existing-decisions:
- en: a84f44d6a3179d47
- zh: 8fb62f08bec5f75d
-/agent-note-plugin-owned-localized-display-metadata/alternatives-considered:
- en: abbdb1b8ab360380
- zh: edfe6c06b1d67fd7
-/agent-note-plugin-owned-localized-display-metadata/verification:
- en: 8541a2431321ef61
- zh: 77599e0855d93da9
-/agent-note-plugin-owned-localized-display-metadata/consequences:
- en: 2a4547c9657038df
- zh: 1a4bebd06d2b9e38
diff --git a/.agents/notes/implemented/architecture/2026-09-18-profile-plugin-host-runtime-instances.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-18-profile-plugin-host-runtime-instances.i18n.yaml
deleted file mode 100644
index 4325837109..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-18-profile-plugin-host-runtime-instances.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-18-profile-plugin-host-runtime-instances.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-18-profile-plugin-host-runtime-instances.md
-/agent-note-profile-plugins-share-the-installation-s-host-runtime-instances:
- en: 04d5248dde932469
- zh: 877d263ca4b0beef
-/agent-note-profile-plugins-share-the-installation-s-host-runtime-instances/problem:
- en: f9c4830ba4be20bc
- zh: be46323e2eae9247
-/agent-note-profile-plugins-share-the-installation-s-host-runtime-instances/decision:
- en: bcd92c487ebfab32
- zh: c50478b864562f0d
-/agent-note-profile-plugins-share-the-installation-s-host-runtime-instances/alternatives-considered:
- en: 1443d5fc869ffcf9
- zh: 76b21473d2892e7f
-/agent-note-profile-plugins-share-the-installation-s-host-runtime-instances/consequences:
- en: 075067e84219ae67
- zh: 035c60f3e11783d5
diff --git a/.agents/notes/implemented/architecture/2026-09-19-concrete-web-bind-address.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-19-concrete-web-bind-address.i18n.yaml
new file mode 100644
index 0000000000..f729428b68
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-19-concrete-web-bind-address.i18n.yaml
@@ -0,0 +1,22 @@
+# Bilingual-pair consistency record for 2026-09-19-concrete-web-bind-address.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-19-concrete-web-bind-address.md
+/agent-note-bind-one-concrete-web-address:
+ en: f70f2b6b24ef16dd
+ zh: 64100dbc55eee33d
+/agent-note-bind-one-concrete-web-address/problem:
+ en: ffc265e61eadc31b
+ zh: fcc70f0a8817914c
+/agent-note-bind-one-concrete-web-address/decision:
+ en: 5fe4f2ddba2ee2da
+ zh: bb140a16f056d4b4
+/agent-note-bind-one-concrete-web-address/alternatives-considered:
+ en: 0e4d475347796821
+ zh: e621e606bc5b1045
+/agent-note-bind-one-concrete-web-address/consequences:
+ en: ba7a839bca09992d
+ zh: a11171005405d1e2
+/agent-note-bind-one-concrete-web-address/testing:
+ en: 4bb8f4bf4ad943c3
+ zh: 2e24d56d01b52574
diff --git a/.agents/notes/implemented/architecture/2026-09-19-concrete-web-bind-address.md b/.agents/notes/implemented/architecture/2026-09-19-concrete-web-bind-address.md
new file mode 100644
index 0000000000..3f6be5132e
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-19-concrete-web-bind-address.md
@@ -0,0 +1,35 @@
+# Agent Note: Bind one concrete Web address
+
+Status: implemented
+
+English | [中文](2026-09-19-concrete-web-bind-address.zh.md)
+
+## Problem
+
+The Web listener accepted `127.0.0.1` or `0.0.0.0`. A wildcard bind exposed every interface of the machine, so the bundle sampled the LAN IPv4 addresses at bind time into a `webRuntime` service and appended them to `trustedHosts`, granting authorities for interfaces the operator never named and mixing inferred reachability into configured trust. A container or workstation that should expose exactly one address had no way to say so, IPv6 addresses were not accepted at all, and a browser dialing the exposed address sent it as `Host` only to be refused unless the sampled list happened to contain it.
+
+## Decision
+
+`webserver.config.host` accepts one concrete IPv4 or IPv6 address literal, loopback by default; the unspecified address in every spelling (`0.0.0.0`, `::`, `::ffff:0.0.0.0`, and their expansions) is rejected when the config loads, and the shipped `dsh web` command refuses the same spellings for `--host` before any consumer activates. An IPv6 `%zone` is kept for `listen`; classification through the exported `isWildcardHost` and `isLoopbackHost` reads the address alone.
+
+The browser Host/Origin fence accepts the listener's own bind IP literal as `Host` on any port, independently of `trustedHosts` and ignoring the zone. A browser dialing that literal presents it as `Host`, the literal is no rebinding target, and the Origin and cross-site checks still apply. DNS names resolving to the address, other interfaces, and proxy authorities still require `--trusted-host`. Connection reads the invocation authorities directly from the `webStartup` provider; the `webRuntime` service and LAN sampling no longer exist.
+
+The advertised URL uses IPv4 for mapped loopback binds, brackets other IPv6 binds, drops a loopback zone, and requires `--public-url` for a zoned non-loopback bind because a zone cannot appear in a URL. A non-loopback bind serves plain HTTP and warns at startup even when an HTTPS `--public-url` fronts it. `web-runtime` is a required startup entry, so a bind failure fails startup instead of leaving a silent listener-less process. Desktop pins `--host 127.0.0.1`, the address Electron dials.
+
+## Alternatives considered
+
+**Keep sampling LAN addresses into `trustedHosts`.** Rejected: the listener already owns the concrete bind address. Sampling grants authorities for other interfaces and mixes inferred reachability with configured trust; direct bind-IP acceptance preserves that distinction without another runtime service.
+
+**Accept the bind IP only on the bound port.** Rejected: a port-less `trustedHosts` entry already matches any port, a proxy on the same address may listen elsewhere, and the port adds nothing to the rebinding defense the Host check provides.
+
+**Keep `0.0.0.0` with a warning.** Rejected: binding every interface exposes remote code execution to every network the machine joins, and the operator can name each address that should be exposed.
+
+## Consequences
+
+An operator exposes exactly the interfaces named by `--host` and reaches the listener through that literal without further configuration; every other remote authority is a `--trusted-host` decision. Compositions that bound `0.0.0.0` fail at load with a message naming the rejected spelling. IPv6-only hosts are supported end to end, including the printed and opened URL.
+
+The [native HTTPS listener decision](2026-09-23-native-https-web-listener.md) supersedes this note's HTTP-only transport assumption. The non-loopback warning applies only without TLS; bind-address validation and authority admission remain unchanged.
+
+## Testing
+
+`packages/host/webserver/tests/webserver.spec.ts` classifies wildcard and loopback spellings and rejects wildcard config; `packages/bundle/web-app/tests/startup.spec.ts` refuses wildcard `--host` before consumers activate; `packages/client/connection/tests/api-request-trust.host.spec.ts` and `node-half.host.spec.ts` admit the bind literal on any port without admitting other authorities; `packages/bundle/web-app/tests/web-app.spec.ts` covers the bracketed IPv6 URL and the plain-HTTP warning; `apps/cli/tests/profiles/web/tests/public-url.expected.e2e.ts` boots the built profile on a non-loopback and a zoned loopback address; `web-failure-matrix.expected.e2e.ts` and `packages/boot/app-boot/tests/app-boot.spec.ts` report the required `web-runtime` entry.
diff --git a/.agents/notes/implemented/architecture/2026-09-19-concrete-web-bind-address.zh.md b/.agents/notes/implemented/architecture/2026-09-19-concrete-web-bind-address.zh.md
new file mode 100644
index 0000000000..4df6b8384f
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-19-concrete-web-bind-address.zh.md
@@ -0,0 +1,35 @@
+# Agent Note: 绑定一个具体的 Web 地址
+
+Status: implemented
+
+[English](2026-09-19-concrete-web-bind-address.md) | 中文
+
+## 问题
+
+Web 监听器只接受 `127.0.0.1` 或 `0.0.0.0`。通配绑定会暴露机器的每个接口,因此 bundle 在绑定时把 LAN IPv4 地址采样进 `webRuntime` 服务并追加到 `trustedHosts`,为操作者从未点名的接口授予 authority,并把推断的可达性混入配置的信任。应当只暴露一个地址的容器或工作站无法表达这一点,IPv6 地址完全不被接受,浏览器拨号已暴露地址时把它作为 `Host` 发送,却只在采样列表恰好包含它时才被接受。
+
+## 决策
+
+`webserver.config.host` 接受一个具体的 IPv4 或 IPv6 地址字面量,默认回环;未指定地址的所有拼写(`0.0.0.0`、`::`、`::ffff:0.0.0.0` 及其展开形式)在配置加载时被拒绝,随附的 `dsh web` 命令在任何消费者激活前对 `--host` 拒绝同样的拼写。IPv6 的 `%zone` 保留给 `listen`;通过导出的 `isWildcardHost` 与 `isLoopbackHost` 分类时只读取地址本身。
+
+浏览器 Host/Origin 栅栏接受监听器自身的绑定 IP 字面量作为任意端口上的 `Host`,独立于 `trustedHosts` 并忽略 zone。拨号该字面量的浏览器把它作为 `Host` 呈现,该字面量不是重绑目标,Origin 与跨站检查仍然生效。解析到该地址的 DNS 名称、其他接口与代理 authority 仍需 `--trusted-host`。Connection 直接从 `webStartup` 提供方读取调用 authority;`webRuntime` 服务与 LAN 采样不再存在。
+
+公告 URL 对 mapped 回环绑定使用 IPv4,为其他 IPv6 绑定加方括号、剥离回环 zone,并因 zone 不能出现在 URL 中而要求带 zone 的非回环绑定显式提供 `--public-url`。非回环绑定提供明文 HTTP,即使前置 HTTPS 的 `--public-url` 也会在启动时警告。`web-runtime` 是必需的启动条目,因此绑定失败会使启动失败,而不是留下一个没有监听器的静默进程。Desktop 固定 `--host 127.0.0.1`,即 Electron 拨号的地址。
+
+## 备选方案
+
+**继续把采样的 LAN 地址加入 `trustedHosts`。** 否决:监听器已经持有具体绑定地址。采样会授权其他接口的 authority,并把推断的可达性混入配置的信任;直接接受绑定 IP 可以保留两者的区分,无需另设运行时服务。
+
+**只在绑定端口上接受绑定 IP。** 否决:不带端口的 `trustedHosts` 条目本就匹配任意端口,同一地址上的代理可能监听其他端口,端口对 Host 检查提供的重绑防御没有增益。
+
+**保留 `0.0.0.0` 并给出警告。** 否决:绑定所有接口会向机器加入的每个网络暴露远程代码执行,而操作者可以逐个点名应暴露的地址。
+
+## 影响
+
+操作者只暴露 `--host` 点名的接口,并无需额外配置即可通过该字面量访问监听器;其他每个远程 authority 都是一个 `--trusted-host` 决定。曾绑定 `0.0.0.0` 的组合在加载时失败,并给出点名被拒绝拼写的消息。纯 IPv6 主机得到端到端支持,包括打印与打开的 URL。
+
+[原生 HTTPS 监听器决策](2026-09-23-native-https-web-listener.zh.md)取代本说明仅支持 HTTP 传输的前提。非回环警告只适用于未启用 TLS 的监听器;绑定地址校验与 authority 接纳保持不变。
+
+## 测试
+
+`packages/host/webserver/tests/webserver.spec.ts` 分类通配与回环拼写并拒绝通配配置;`packages/bundle/web-app/tests/startup.spec.ts` 在消费者激活前拒绝通配 `--host`;`packages/client/connection/tests/api-request-trust.host.spec.ts` 与 `node-half.host.spec.ts` 在任意端口上接纳绑定字面量而不接纳其他 authority;`packages/bundle/web-app/tests/web-app.spec.ts` 覆盖带方括号的 IPv6 URL 与明文 HTTP 警告;`apps/cli/tests/profiles/web/tests/public-url.expected.e2e.ts` 在非回环地址与带 zone 的回环地址上启动已构建的 profile;`web-failure-matrix.expected.e2e.ts` 与 `packages/boot/app-boot/tests/app-boot.spec.ts` 报告必需的 `web-runtime` 条目。
diff --git a/.agents/notes/implemented/architecture/2026-09-19-lossless-projection-checkpoint-json.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-19-lossless-projection-checkpoint-json.i18n.yaml
deleted file mode 100644
index 93be98ebc5..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-19-lossless-projection-checkpoint-json.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-19-lossless-projection-checkpoint-json.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-19-lossless-projection-checkpoint-json.md
-/agent-note-lossless-projection-checkpoint-json:
- en: 2b7b5f9e5da030df
- zh: 76d65ddc85776939
-/agent-note-lossless-projection-checkpoint-json/problem:
- en: 26e613246f31ee5a
- zh: e0aa1e2f63bce802
-/agent-note-lossless-projection-checkpoint-json/decision:
- en: 01161e9c42142971
- zh: e05e52d2af7fc4c5
-/agent-note-lossless-projection-checkpoint-json/alternatives-considered:
- en: 1d64345d3d72ed5f
- zh: 4c9d3410cfd815b3
-/agent-note-lossless-projection-checkpoint-json/consequences:
- en: 94b619dc13cfff0b
- zh: c432227d3333175f
diff --git a/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.i18n.yaml
index 599afcc6f0..0fb431067d 100644
--- a/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.i18n.yaml
@@ -15,5 +15,5 @@
en: 4089464ad1fb60bd
zh: 3168e9e4a205145d
/agent-note-keep-live-configuration-in-the-owning-profile/consequences:
- en: 6033aaa12b2a521b
- zh: 43934413c97670de
+ en: 41bb170694c785d6
+ zh: 17419fdd6d937dce
diff --git a/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.md b/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.md
index 7acf109869..65cc8f60b1 100644
--- a/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.md
+++ b/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.md
@@ -24,6 +24,6 @@ The rejected alternative is retaining settings as a second store behind a config
## Consequences
-This supersedes only the non-secret settings tier in [configuration source ownership](2026-08-04-configuration-source-ownership.md); its environment restrictions and credential ordering remain applicable. [Volatile references](2026-09-18-volatile-config-references.md) own reference lifetime and ordinary update behavior.
+This supersedes only the non-secret settings tier in [configuration source ownership](2026-08-04-configuration-source-ownership.md); its environment restrictions remain applicable, and [stored credential precedence](2026-10-09-stored-credential-precedence.md) supersedes its credential ordering. [Volatile references](2026-09-18-volatile-config-references.md) own reference lifetime and ordinary update behavior.
Real profile tests cover multiple instances, persisted restart, invalid edits, stale revisions, higher-layer rejection, secret redaction, and configuration expressions. Browser tests exercise form writes and reset through the assembled application. Runtime reference identity is checked across live edits.
diff --git a/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.zh.md b/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.zh.md
index c9b88462a5..9ce0952aab 100644
--- a/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.zh.md
@@ -24,6 +24,6 @@ Cordis config patch 替换完整 entry config。编辑会保留普通字段、
## 后果
-本决定只替代 [配置来源所有权](2026-08-04-configuration-source-ownership.zh.md) 中的非秘密 settings 层;其中的环境限制和凭据优先级仍适用。[Volatile 引用](2026-09-18-volatile-config-references.zh.md) 负责引用生命周期和普通更新行为。
+本决定只替代 [配置来源所有权](2026-08-04-configuration-source-ownership.zh.md) 中的非秘密 settings 层;其中的环境限制仍适用,其凭据优先级已由[存储凭据优先级](2026-10-09-stored-credential-precedence.zh.md)替代。[Volatile 引用](2026-09-18-volatile-config-references.zh.md) 负责引用生命周期和普通更新行为。
真实 profile 测试覆盖多实例、持久化重启、非法编辑、过期修订、高优先级层拒绝、秘密脱敏及配置表达式。浏览器测试通过完整应用执行表单写入和重置,并检查实时编辑前后的运行时引用身份。
diff --git a/.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.i18n.yaml
index 8892007905..88254d458d 100644
--- a/.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.i18n.yaml
@@ -51,8 +51,8 @@
en: d0bed60317e6e3a8
zh: 5714160678adc049
/agent-note-resolve-profile-modules-along-the-ancestor-chain-with-an-interception-layer/decision/part-3-developer-integration-guide/2-development-mode-the-plugin-repository-is-outside-the-profile-tree:
- en: d7ec5a747ebcb93e
- zh: 7e1d0724973b32c1
+ en: 22259a568693c641
+ zh: d81d15671398d033
/agent-note-resolve-profile-modules-along-the-ancestor-chain-with-an-interception-layer/alternatives-considered:
en: 5466618bb5e3d4d9
zh: 7d0b6d727e33d0d1
diff --git a/.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.md b/.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.md
index 6dfb5511af..bb99cb2435 100644
--- a/.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.md
+++ b/.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.md
@@ -135,7 +135,7 @@ The plugin's own third-party dependencies are hoisted to `$DSH_HOME/profiles/` there, or link dsh packages to a local source repository and start dsh from that repository. Both make the running dsh and the repository's copies identical, but linked plugins do not require either layout.
diff --git a/.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.zh.md b/.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.zh.md
index 2474a9b040..4af29f7255 100644
--- a/.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-19-profile-resolution-lookup-order.zh.md
@@ -135,7 +135,7 @@ ESM 与 CommonJS 两个适配器调用同一个路由函数,主线程与 Harne
`npm link`,或 `dsh plugin add ../my-plugin` 这类裸目录路径(pnpm 按 `link:` 处理),会让 profile 里的条目成为指向插件仓库的软链接,仓库目录成为 linked root。插件以真实路径加载。查找到其 manifest 的 peer 位置时,匹配的运行时条目由当前运行的安装提供,无论 dsh 来自 npm 全局安装、Desktop 内置还是源码仓启动。该位置的 devDependency 副本服务编译器,普通请求不会加载它。link 指向无 manifest 的 `src` 目录时,也可以通过同一条祖先查询使用父目录的 peer。
-manifest 的写法与 harness 自身的包相同:需要与宿主共享实例的 dsh 包同时声明在 `peerDependencies` 与 `devDependencies`,peer 让 `R/node_modules` 上的位置被占据,dev 副本给编译器和独立测试使用;第三方依赖以及 `@deepseek-ai/dsh-brand`、`@deepseek-ai/dsh-util-values` 这类无状态 dsh 工具包放在 `dependencies`。修改 `peerDependencies` 后,重新加载插件时的新解析会使用新声明;已加载模块的生命周期仍由 Node 和 Cordis 管理。
+manifest 的写法与 harness 自身的包相同:需要与宿主共享实例的 dsh 包同时声明在 `peerDependencies` 与 `devDependencies`,peer 让 `R/node_modules` 上的位置被占据,dev 副本给编译器和独立测试使用;第三方依赖以及 `@deepseek-ai/dsh-brand`、`@deepseek-ai/dsh-util-values` 这类无状态 dsh 工具包放在 `dependencies`。修改 `peerDependencies` 后,重新加载插件时的新解析会使用新声明;已加载模块的生命周期仍由 Node 和 Cordis 管理。版本相同也不会让不同目录中的副本共享模块局部状态。仅把 scope 标记改成 `Symbol.for` 同样无法共享 scope 父关系、carrier key 或注册表;承载身份的运行时必须解析到 Host 实例。
另两种可用布局:插件仓库自装 `@deepseek-ai/dsh` 并从仓库里启动 `pnpm exec dsh --profile `;或把 dsh 包 link 到本机源码仓并从源码仓启动。两者让运行中的 dsh 与仓库里的副本本来就是同一份,但 linked 插件不要求使用这两种布局。
diff --git a/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.i18n.yaml
index 938a007c31..41376c7c73 100644
--- a/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.i18n.yaml
@@ -9,8 +9,8 @@
en: 2e7f99c81fbc941f
zh: b0fac7cd5eb86ffe
/agent-note-projection-cache-read-only-face-matches-the-lifecycle-identity-the-client-store-separates-cached-and-sequenced-rows/problem/mechanism:
- en: 36d24e92eedf8cea
- zh: 5dafc5758ee0f380
+ en: 2594af08bbc79dba
+ zh: cdebd5d14d99cf3c
/agent-note-projection-cache-read-only-face-matches-the-lifecycle-identity-the-client-store-separates-cached-and-sequenced-rows/problem/the-cache-s-two-uses-and-where-validation-lives:
en: 055ead18162ca1cd
zh: 9046bf04229e45d1
@@ -30,8 +30,8 @@
en: 743f2dceda3072aa
zh: f4d53ddc13a4127e
/agent-note-projection-cache-read-only-face-matches-the-lifecycle-identity-the-client-store-separates-cached-and-sequenced-rows/decision/the-client-store-separates-cached-and-sequenced-rows:
- en: 8a4e9e9ac2ada06f
- zh: b5884535cd934f72
+ en: dd1cc7ac96a5a25d
+ zh: bdea08d20f166e65
/agent-note-projection-cache-read-only-face-matches-the-lifecycle-identity-the-client-store-separates-cached-and-sequenced-rows/decision/what-stays-untouched:
en: 1cefba02bbfb2647
zh: 5a96942565ab9d72
diff --git a/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.md b/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.md
index bfdd8bda22..f0235d8040 100644
--- a/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.md
+++ b/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.md
@@ -12,7 +12,7 @@ This is neither cache corruption nor a version mismatch. The projcache record's
### Mechanism
-A cache record (record format and predecessor recovery: [Projection-cache predecessor recovery and Session-format binding](2026-09-02-projcache-cross-version-read-compat.md)) is bound to a lifecycle identity: `formatVersion + createdAt + cwd + isSeeded + inheritedEventCount`, matched by `identityMatches` on full equality. Since #3346, `inheritedEventCount` (the length of the event prefix a fork inherits, the cut below) no longer appears in the logical header: the header keeps only the `isSeeded` bit and the exact cut follows the body. Since Session format v2 (#3398) the physical header line no longer stores `seedLength` either; the reader derives the cut from the seq of the `session/end-seed {inherited: true}` marker in the body.
+A cache record (record format and predecessor recovery: [Projection-cache predecessor recovery and Session-format binding](../../../../packages/session/session-projection-cache/README.md)) is bound to a lifecycle identity: `formatVersion + createdAt + cwd + isSeeded + inheritedEventCount`, matched by `identityMatches` on full equality. Since #3346, `inheritedEventCount` (the length of the event prefix a fork inherits, the cut below) no longer appears in the logical header: the header keeps only the `isSeeded` bit and the exact cut follows the body. Since Session format v2 (#3398) the physical header line no longer stores `seedLength` either; the reader derives the cut from the seq of the `session/end-seed {inherited: true}` marker in the body.
A header-only read therefore cannot obtain the cut: the JSONL backend's `fromHeaderLine` hard-codes `inheritedEventCount: 0` for header-only reads, and `SessionPersistenceSnapshot` carries only the header, the revision, and an optional eventCount. The Session-list and reference consumers grew the same guard:
@@ -97,7 +97,7 @@ Writer classification:
| the `projections` block of an `api-session/added` summary (`manager.handleSessionAdded`) | by the block's `kind`; the summary comes from a live session, so in practice `sequenced` |
| the history first page's `projections` (`projections.seed` in `session.ts`) | sequenced |
| the control baseline, live sessions only (`manager.replaceControlBaseline`) | sequenced |
-| the `session.projections` result of `refreshProjections` (a body observation) | sequenced |
+| the `session.projections` result of `refreshProjections` | sequenced for an exact read; cached only when [migration is required](2026-09-29-explicit-session-history-preparation.md) |
| push frames (the `projection` frame handled by `manager`) | sequenced |
| the `title` after a successful rename (`session.ts`) | sequenced |
diff --git a/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md b/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md
index 470c96385c..326e6f028b 100644
--- a/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md
@@ -12,7 +12,7 @@ Host 进程重启后,所有 fork 出来的会话(`SessionHeader.isSeeded ===
### 机制
-缓存记录(记录格式与前代恢复见 [投影缓存前代恢复与 Session 格式绑定](2026-09-02-projcache-cross-version-read-compat.zh.md))绑定一份 lifecycle identity:`formatVersion + createdAt + cwd + isSeeded + inheritedEventCount`,`identityMatches` 做全等匹配。其中 `inheritedEventCount`(fork 继承的事件前缀长度,下称 cut)从 #3346 起不再出现在逻辑 header 里:header 只保留 `isSeeded` 这一位,精确 cut 跟随正文。Session 格式 v2 起(#3398),物理 header 行也不再存 `seedLength`,reader 从正文里 `session/end-seed {inherited: true}` 标记的 seq 推出 cut。
+缓存记录(记录格式与前代恢复见 [投影缓存前代恢复与 Session 格式绑定](../../../../packages/session/session-projection-cache/README.zh.md))绑定一份 lifecycle identity:`formatVersion + createdAt + cwd + isSeeded + inheritedEventCount`,`identityMatches` 做全等匹配。其中 `inheritedEventCount`(fork 继承的事件前缀长度,下称 cut)从 #3346 起不再出现在逻辑 header 里:header 只保留 `isSeeded` 这一位,精确 cut 跟随正文。Session 格式 v2 起(#3398),物理 header 行也不再存 `seedLength`,reader 从正文里 `session/end-seed {inherited: true}` 标记的 seq 推出 cut。
于是 header-only 的读取拿不到 cut:JSONL 后端 `fromHeaderLine` 对 header-only 读取硬编码 `inheritedEventCount: 0`,`SessionPersistenceSnapshot` 只有 header、revision 和可选的 eventCount。Session 列表与引用消费者都因此加了同一个守卫:
@@ -97,7 +97,7 @@ seq 比较只在同一条 Host 连接内发生:`handleConnected` 先整表 `cl
| `api-session/added` 摘要的 `projections` block(`manager.handleSessionAdded`) | 按 block 的 `kind`;该摘要来自活会话,实际为 `sequenced` |
| history 首页 `projections`(`session.ts` 的 `projections.seed`) | sequenced |
| control 基线,仅活会话(`manager.replaceControlBaseline`) | sequenced |
-| `refreshProjections` 的 `session.projections` 结果(正文观察) | sequenced |
+| `refreshProjections` 的 `session.projections` 结果 | 准确读取为 sequenced;仅[需要迁移](2026-09-29-explicit-session-history-preparation.zh.md)时为 cached |
| 推送 frame(`manager` 处理 `projection` frame) | sequenced |
| rename 成功后的 `title`(`session.ts`) | sequenced |
diff --git a/.agents/notes/implemented/architecture/2026-09-19-remote-duplex-stream.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-19-remote-duplex-stream.i18n.yaml
index eba3d7545a..a883f81d59 100644
--- a/.agents/notes/implemented/architecture/2026-09-19-remote-duplex-stream.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-19-remote-duplex-stream.i18n.yaml
@@ -9,14 +9,14 @@
en: 70a969dce3283824
zh: 18c61bf840a6518a
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/problem/current-state:
- en: 8fb06638898de59e
- zh: 79ed0cfe79e40be1
-/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/problem/three-unrelated-uplink-mechanisms:
- en: 386be7b691ab63e7
- zh: 935e0db22ae1cf5d
-/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/problem/concrete-costs:
- en: f374976b43645d15
- zh: 26b2c7aaafa41fba
+ en: 2de84c3fc0a3552e
+ zh: da9cd565588bf2dd
+/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/problem/uplink-consumers:
+ en: 403103ef08958529
+ zh: 2813d9d38c2c55b7
+/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/problem/consumer-responsibilities:
+ en: 28f8a3112d6ec5fe
+ zh: c70d68233e3f6b7f
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/problem/relation-to-background-jobs:
en: 5e67475010d52ddb
zh: ce23aad2d4aa0d67
@@ -30,8 +30,8 @@
en: 943208904785f7eb
zh: bda1969132a16101
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/decision/terms:
- en: 401703adbd9cc1c2
- zh: 56d960c0b6fdff57
+ en: a22cd84756773075
+ zh: 49efa0c5a37d4b57
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/decision/type-signatures:
en: 5f9cd00890b6f4c3
zh: 600ecba7e60b63f6
@@ -42,8 +42,8 @@
en: a7180d3c194cf9d9
zh: 0dd7833e0e5d0a71
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/decision/peerscope:
- en: 202aa6e8e8b04bd6
- zh: b1fc897c09074a7c
+ en: 182634406ed2b25b
+ zh: 0bd5900a39802224
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/decision/client-face:
en: 9f10347a73402b8b
zh: b939c7a254a8e579
@@ -75,8 +75,8 @@
en: 46a0c0b3ce1e5671
zh: 7743707de42b1d1d
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/decision/in-process-and-worker-carriers:
- en: ec339e57dc2c0603
- zh: 3570be8c094ea24c
+ en: df4ae60d8403cdbb
+ zh: 70c6024a2a9a1e84
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/decision/relation-to-events:
en: a011793e5a4400ed
zh: df8b20115f0348b8
@@ -102,8 +102,8 @@
en: 26dcd8bebdc8d974
zh: e74bcef7c549e70c
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/placement:
- en: 41128746334320c7
- zh: 558a676340aae3a2
+ en: ecfaff2b9b2ef021
+ zh: 85b392d79364e0c3
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/key-sequences:
en: 130e4f9ec7296a10
zh: 2a5aa1302a8db61a
@@ -126,17 +126,17 @@
en: b42ab0932f8b31b4
zh: 7fed8cb5b6e36788
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/alternatives-considered:
- en: 04e72789417d6e3a
- zh: b57c5cc68dff45a5
+ en: b0046c9f7c12a876
+ zh: f3c37b466746b0fd
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/consequences:
- en: 07524e0c2dc22ad5
- zh: 5bf4f27160d78b43
+ en: dbc54ce699ef8e71
+ zh: 83144f07ae8f8759
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/testing:
- en: 5c8188930b9d4fdb
- zh: 1d94e6563cedde13
+ en: 06fbeed7b82ded32
+ zh: 66accb4bd2bf64a2
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/deferred:
- en: 3b99a327464edc3e
- zh: dcffdc75369925dc
+ en: 8368c592f30af97f
+ zh: 2ff89acb7b3ed477
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/related:
- en: f8995f478dc1d478
- zh: 96621e80a4d3cefc
+ en: 475d212ae1f9c1c9
+ zh: 21fe6c6cdb900a6b
diff --git a/.agents/notes/implemented/architecture/2026-09-19-remote-duplex-stream.md b/.agents/notes/implemented/architecture/2026-09-19-remote-duplex-stream.md
index ea8e8cebcc..2d1c04de1e 100644
--- a/.agents/notes/implemented/architecture/2026-09-19-remote-duplex-stream.md
+++ b/.agents/notes/implemented/architecture/2026-09-19-remote-duplex-stream.md
@@ -8,41 +8,41 @@ English | [中文](2026-09-19-remote-duplex-stream.zh.md)
### Current state
-`dsh-api-gateway` multiplexes every Typert Remote stream over one WebSocket at the fixed path `/api/remote.mux` (`packages/api/gateway/src/stream-protocol.ts`). A Host method decorated with `@Remote({ mode: 'stream' })` that returns an `Iterable` or `AsyncIterable` becomes one Host-to-Client logical stream; the optional final `signal: AbortSignal` is the only reserved parameter, never enters the wire arguments, and is appended by the Gateway after the decoded business parameters (`packages/api/gateway/src/index.ts`). The generated method the Client receives returns a bare `AsyncIterable`.
+`dsh-api-gateway` multiplexes Typert Remote streams over one WebSocket at `/api/remote.mux`. A Host method declares both directions with `@Remote({ mode: 'stream' })` and `RemoteStream`; the generated Client method returns `RemoteStreamHandle`. Existing `Iterable` and `AsyncIterable` return types declare downlink-only methods. The optional final `signal: AbortSignal` is supplied by the Gateway and does not enter the wire arguments.
-There are only five wire frames:
+Both directions share one `streamId`:
| Direction | Frame | Definition |
| --- | --- | --- |
| Client to Host | `{ type: 'open', streamId, endpoint, payload }` | `stream-protocol.ts` |
+| Client to Host | `{ type: 'item', streamId, value? }` | `stream-protocol.ts` |
+| Client to Host | `{ type: 'end', streamId }` | `stream-protocol.ts` |
| Client to Host | `{ type: 'cancel', streamId }` | `stream-protocol.ts` |
| Host to Client | `{ type: 'item', streamId, value? }` | `stream-protocol.ts` |
| Host to Client | `{ type: 'error', streamId, error: { code, message, details } }` | `stream-protocol.ts` |
| Host to Client | `{ type: 'end', streamId }` | `stream-protocol.ts` |
-The Client can send only open and cancel. The parser `parseRemoteStreamClientMessage` (`stream-protocol.ts`) throws on any other frame, and a Host connection that receives one closes the whole socket with 1008 (`stream-server.ts`). In short, the Gateway today supports only "a backend iterable sent to the frontend". Every feature that needs the frontend to send data to the backend continuously takes its own detour.
+The Client handle's `send()` supplies uplink items, `end()` half-closes the uplink, and `dispose()` cancels the logical stream. The Host reads decoded items through `this.ctx.invocation.uplink()`; the invocation also carries the admitted Peer and cancellation signal.
-A Host method also has no notion of who initiated this call: `InvokeRemoteRequest = { namespace, method, args, signal }` (`packages/api/gateway/src/types.ts`) has no caller slot, `RemoteStreamOpener` has the signature `(endpoint, payload, signal)` (`stream-server.ts`), and the WebSocket checks the cookie once, at the handshake.
+The transport owns validation, cancellation, and its bounded inbox. Consumer protocols own input authority, acknowledgements, and recovery after disconnection.
-### Three unrelated uplink mechanisms
+### Uplink consumers
| Feature | Uplink path | Location | Cost |
| --- | --- | --- | --- |
-| Web terminal keystrokes | One unary RPC `terminal.write(agent, id, attachmentId, data)` per xterm `onData`; the Client serializes them through a promise chain and caps its own byte budget | `packages/api/terminal-controller/src/client/model.ts`; Host `src/index.ts` | One HTTP round trip and one authentication per keystroke; two carriers correlated by hand through `attachmentId`; `inputFull` when the budget is exhausted |
+| Web terminal input and resize | `terminal.attach` carries `TerminalInput` uplink items and `TerminalFrame` downlink items; the Client pipelines commands and releases its raw-input byte budget on `ack` | `packages/api/terminal-controller/src/client/model.ts`; Host `src/terminal.ts` | The current attachment owns input; `inputFull` when the Client budget is exhausted; input is never replayed after reconnection |
| File upload | A `Blob` or `ReadableStream` goes to the native `POST /api/session/uploadFileBinary` outside the Remote layer, with `duplex: 'half'` | `packages/client/file-upload/src/client/runtime.ts`; route `src/index.ts` | Bypasses Typert descriptors; no type projection |
| Answers to approvals and questions | The Host sends waterfall frames down the `$events` stream, the Client answers through a separate unary RPC `$events/result`, correlated by `clientId` plus `eventId` | `packages/api/gateway/src/client/remote-events.ts`; Host `index.ts` | A second correlation key; `index.ts` records the race this creates |
-These three mechanisms solve the same problem: the Client wants to write into a logical stream that is already open.
+These consumers need Client-to-Host data while a related operation remains active. Web terminals use the duplex stream; file upload and event answers retain their own transports.
-### Concrete costs
+### Consumer responsibilities
-- **Aligning two carriers.** The terminal uses `attachmentId` to correlate the `follow` stream with the `write` RPC and to decide who owns input. Approvals use `clientId` to correlate the `$events` stream with the `$events/result` RPC.
-- **No ordering guarantee.** A unary RPC travels over HTTP and is unordered relative to the WebSocket downlink; the terminal can preserve order only by serializing writes on the Client.
-- **Separate authentication and lookup per uplink.** Every `write(agent, id, attachmentId, data)` resolves `agent`, looks up the terminal, and validates the attachment.
-- **No EOF.** The Client cannot express "I am done writing"; a half-close needs yet another RPC.
-- **Cancellation does not cover both halves.** Cancelling the downlink stream does not cancel queued uplink writes, and vice versa.
-- **Backpressure implemented three times.** The downlink pulls the iterator at the pace of the socket write callback (`stream-server.ts`); the terminal uses a bounded follower that makes a slow consumer fail explicitly (`terminal-controller/src/stream.ts`); the uplink relies on the Client capping `maxInputBytes` itself.
-- **The documentation is already stale.** `docs/api-gateway.md:160` still says Remote handles one request and one result, a sentence that lags the stream mode the README records.
+- **Input authority.** A terminal retains its controlling attachment as a Host-local follower. Snapshot and state frames report each attachment's `writable` state; older attachments keep receiving output after control transfers.
+- **Ordering and acknowledgement.** Terminal writes and resizes share a consecutive command sequence and the terminal's operation queue. An `ack` reports provider acceptance or a control refusal; it never authorizes replay after disconnection.
+- **Independent lifetimes.** Cancelling `attach` stops its input and output delivery, drops unstarted commands, and joins an in-flight input operation. The PTY lifetime is owned separately by retention and explicit close.
+- **Coordinated byte limits.** The Gateway exposes its complete-frame inbox capacity and this stream's JSON envelope cost through `invocation.uplinkWindow`. The terminal publishes that window in its snapshot and uses provider acknowledgements to release in-flight capacity. Writes and resizes wait in a bounded Client queue when the window is full. `maxInputBytes` limits raw UTF-8 input; `maxBufferedBytes` separately limits the complete encoded input retained by the Client and output retained by each follower.
+- **Remaining separate carriers.** Approval answers still correlate `$events` with the `$events/result` RPC through `clientId` and `eventId`. Moving this protocol onto the duplex stream remains deferred.
### Relation to background jobs
@@ -66,7 +66,7 @@ Every Remote stream accepts a Client uplink. The method's return type `RemoteStr
Client Host
───────────────────────────────── ───────────────────────────────────────────
const s = remote.job.attach(req, signal)
- │ open { streamId, endpoint, payload } ───────▶ opener(endpoint, payload, uplink, peer, control)
+ │ open { streamId, endpoint, payload } ───────▶ opener(endpoint, payload, uplink, peer, control, window)
│ │ prepareInvocation
│ s.send(v1) → item { streamId, v1 } ───────▶ inbox.push(v1) │ invocation = { request, service, peer, signal, uplink() }
│ s.send(v2) → item { streamId, v2 } ───────▶ inbox.push(v2) │ receiverContext.extend({ invocation }).get(service)
@@ -88,7 +88,7 @@ Every Remote stream accepts a Client uplink. The method's return type `RemoteStr
| Half-close | One direction has ended while the other remains open |
| Item | One business value in one direction; every item is validated by the codec independently |
| inbox | The bounded uplink queue the Host keeps per logical stream |
-| Peer | The party the connection layer admitted, one `PeerScope`; this Host has exactly one Peer, the operator |
+| Peer | The party one connection speaks for, one `PeerScope` carrying its principal; every browser connection shares `connection.browserPeer`, and in-process code creates its own through `connection.createPeer()` ([Peer principals](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.md)) |
| Invocation context | `RemoteInvocation`: the request, receiving service, Peer, cancellation signal, and uplink entry of this call |
### Type signatures
@@ -135,10 +135,11 @@ export interface RemoteInvocation {
}
/** Cordis service key of the receiving service. */
readonly service: string
- /** The Peer that initiated the call. In-process carriers and unadmitted calls are the operator. */
+ /** The Peer that initiated the call; every carrier names it explicitly. */
readonly peer: PeerScope
/** Carrier cancellation: Client cancel, socket closure, uplink failure. */
readonly signal: AbortSignal
+ readonly uplinkWindow?: RemoteUplinkWindow | undefined
/**
* The uplink items of this call. Can be taken once; the second call throws. With an uplink codec
* in the descriptor each item is decoded to In; without one, items are delivered as unknown with
@@ -179,24 +180,30 @@ The mechanism relies on two existing Cordis behaviors:
/** The opaque identity of one Peer. */
export type PeerId = Branded<'PeerId'>
-/** One Peer's session on this Host: opened and released by the admitting side; ctx owns connection-lifetime registrations. */
+/** One Peer's session on this Host: opened and released by its creator; ctx owns connection-lifetime registrations. */
export interface PeerScope {
readonly id: PeerId
readonly ctx: Context
+ /** Caller identity fixed at creation. */
+ readonly principal: Principal
+ /** Aborted once the Peer is disposed. */
+ readonly signal: AbortSignal
+ /** Participation in forwarded interactions; independent of RBAC. */
+ readonly interactions: 'answer' | 'none'
dispose(): Promise
}
```
-This Host has exactly one Peer: the operator. `@deepseek-ai/dsh-client-connection` creates it with `createScope(connectionCtx, peer)` when the service applies, the same mechanism an Agent uses to create its own scope: the Peer object is the ScopeKey, `peer.ctx` carries connection-lifetime effects, and `dispose()` runs with the Connection's release and quiets the fiber. It is exposed as `connection.operator`.
+`@deepseek-ai/dsh-client-connection` implements the one Peer class, `Peer(owner, { principal, interactions? })`, with `createScope(owner, peer)`, the same mechanism an Agent uses to create its own scope: the Peer object is the ScopeKey, `peer.ctx` carries connection-lifetime effects, `peer.signal` aborts when the scope ends, and `dispose()` quiets the fiber. Connection creates `connection.browserPeer` with the owner principal when the service applies; [Peer principals and method-level Remote access control](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.md) owns principals, `createPeer()`, and access checks.
| Member | Semantics |
| --- | --- |
-| `connection.operator: PeerScope` | The only Peer; lives and dies with the Connection |
-| `connection.admit(request): PeerAdmission` | Runs the existing `requestRejection`: returns `{ rejection: 401 \| 403 }` when refused, otherwise `{ peer: operator }`. Future admitters plug in here |
+| `connection.browserPeer: PeerScope` | The Peer every authenticated browser connection shares; owner principal; lives and dies with the Connection |
+| `connection.admit(request): PeerAdmission` | Checks Host/Origin and signed browser or API Cookies; returns `{ rejection: 401 \| 403 }` or the credential's live Peer; connect publishes the API Peer and its lifecycle socket releases it |
-The Gateway's `handleUpgrade(req, socket, head, peer)` binds an opener with a fixed Peer to that socket and registers socket closure in `peer.ctx.effect`; when the scope is already inactive, it closes the socket directly with 1001 instead. When `InvokeRemoteRequest.peer?` or the `peer` parameter of `RemoteStreamOpener` is absent, the answer is the operator, so existing in-process callers change nothing; in a composition without a Connection, the operator stand-in the Gateway builds for itself is likewise created through `createScope`, with the same `dispose()` contract. Looking up a Peer by id, associating a carrier with a second Peer, and Peer open and close events all wait until the first consumer that needs multiple Peers appears.
+The Gateway's `handleUpgrade(req, socket, head, peer)` binds an opener with a fixed Peer to that socket and registers socket closure in `peer.ctx.effect`; when the scope is already inactive, it closes the socket directly with 1001 instead. `InvokeRemoteRequest.peer` and the `peer` parameter of `RemoteStreamOpener` and `wireStream.open` are required, so every in-process caller names its Peer, and the Gateway creates none. Looking up a Peer by id and Peer open and close events wait until the first consumer that needs them appears.
-The Peer carries no access model: who the Peer is and what it may do are attached by business plugins on their own through `peer.ctx`.
+The Peer carries its principal; what that principal may call is decided by the roles, bindings, and policies that [Peer principals and method-level Remote access control](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.md) defines.
### Client face
@@ -311,7 +318,7 @@ Credit frames wait until a consumer that needs sustained high-volume uplink appe
| Worker tunnel (`packages/experimental/webworker-runtime`) | The page adds `stream-uplink-item` / `stream-uplink-end` frames; `serveStream` assembles them into an `uplink` handed to `seams.openStream`. Same-origin trusted boundary: no inbox limit, and items after `end` are dropped |
| `remote-mock` (`packages/test-support/remote-mock`) | `StreamHandle.uplink: AsyncIterable`; `rpc.open` passes it through |
-An in-process carrier with no `peer` means the operator.
+An in-process carrier passes its Peer explicitly; the in-page Host of the worker tunnel passes `connection.browserPeer`.
### Relation to `$events`
@@ -325,7 +332,7 @@ An in-process carrier with no `peer` means the operator.
// types.ts
export type RemoteStream = AsyncIterable & { readonly [STREAM_UPLINK]?: In }
export type PeerId = Branded<'PeerId'>
-export interface PeerScope { readonly id: PeerId; readonly ctx: Context; dispose(): Promise }
+export interface PeerScope { readonly id: PeerId; readonly ctx: Context; readonly principal: Principal; readonly signal: AbortSignal; dispose(): Promise }
export interface RemoteInvocation { … } // see Host face
declare module '@deepseek-ai/cordis' { interface Context { readonly invocation: RemoteInvocation | undefined } }
@@ -362,13 +369,13 @@ export interface InvokeRemoteRequest {
readonly args: Readonly>
/** Uplink items; absent is equivalent to an iterator that ends immediately. */
readonly uplink?: AsyncIterable
- /** The Peer that initiated the call; absent means the operator. */
- readonly peer?: PeerScope
+ /** The Peer that initiated the call; required on every path. */
+ readonly peer: PeerScope
readonly signal?: AbortSignal
}
export interface TypertGatewayWireStream {
- open: (endpoint: string, payload: unknown, uplink: AsyncIterable, peer: PeerScope | undefined, signal: AbortSignal) => Promise>
+ open: (endpoint: string, payload: unknown, uplink: AsyncIterable, peer: PeerScope, signal: AbortSignal) => Promise>
failure: (error: unknown) => RemoteStreamFailure
}
@@ -379,6 +386,7 @@ export type RemoteStreamOpener = (
uplink: AsyncIterable,
peer: PeerScope,
control: AbortController,
+ window: RemoteUplinkWindow,
) => Promise>
// index.ts Config addition
@@ -394,7 +402,7 @@ const signal = request.signal ?? NEVER_ABORTED_SIGNAL
const invocation = new GatewayInvocation(
{ namespace: request.namespace, method: request.method, args: request.args },
descriptor.service,
- request.peer ?? this.operatorPeer(),
+ request.peer,
signal,
new UplinkDecoder(request.uplink ?? EMPTY_ASYNC_ITERABLE, descriptor.uplink?.codec, endpoint, control),
)
@@ -444,8 +452,8 @@ cancel : missing → ignore; otherwise control.abort(new Error('Remote stream ca
| `dsh-typert-protocol` (`types.ts`, `index.ts`) | `RemoteStream`, `RemoteStreamHandle`, `PeerId`, `PeerScope`, `RemoteInvocation`, the `ctx.invocation` declaration merge, `InvocationDescriptor.uplink`; the decorator recognizes only `mode: 'stream'`; `TypertRemoteService` registers the `invocation` accessor when constructed |
| `dsh-typert-registry` | Load-time validation that `uplink.codec` is a valid strict codec |
| `dsh-typert-generator` (`model.ts`, `analyzer.ts`, `emitter.ts`) | `remoteResultType` recognizes `RemoteStream` and generates the `uplink` boundary; the descriptor emits `uplink: { codec }`; the generated Client signature returns `RemoteStreamHandle`; the parameter loop accepts only business parameters and a final `signal` |
-| `dsh-client-connection` (`operator-peer.ts`, `rpc.ts`, `rpc-host.ts`, `index.ts`) | The operator `PeerScope`; `connection.operator` and `admit`; the `/api` and upgrade routes obtain the Peer through `admit`; the optional `uplink` of `rpc.open` |
-| `dsh-api-gateway` Host (`stream-protocol.ts`, `stream-server.ts`, `types.ts`, `index.ts`) | `item` / `end` frames; `UplinkInbox`; `Config.streamInboxBytes`; `GatewayInvocation` and `extend({ invocation })`; `UplinkDecoder`; `handleUpgrade(req, socket, head, peer)`; `operatorPeer()` |
+| `dsh-client-connection` (`peer.ts`, `rpc.ts`, `rpc-host.ts`, `index.ts`) | The `Peer` class; `connection.browserPeer` and `admit`; the `/api` and upgrade routes obtain the Peer through `admit`; the optional `uplink` of `rpc.open` |
+| `dsh-api-gateway` Host (`stream-protocol.ts`, `stream-server.ts`, `types.ts`, `index.ts`) | `item` / `end` frames; `UplinkInbox`; `Config.streamInboxBytes`; `GatewayInvocation` and `extend({ invocation })`; `UplinkDecoder`; `handleUpgrade(req, socket, head, peer)` |
| `dsh-api-gateway` Client (`client/index.ts`, `client/stream-client.ts`) | `invokeStream` returns the handle; the uplink pump is driven by the handle's queue |
| `dsh-webworker-runtime`, `dsh-remote-mock` | The worker tunnel's two uplink frames and the `serveStream` assembly; `StreamHandle.uplink`; the direct proxy returns the real handle |
| Documentation | The READMEs of gateway, the api group, connection, protocol, and generator; `docs/api-gateway`; the type-equiv blocks of `docs/subsystems/typert`; the config and Cordis catalogs |
@@ -560,7 +568,7 @@ for await (const reply of stream) replies.push(reply) // ['> a', '> b']
**Putting the Client's `send` on the `AsyncIterable` the generated method returns.** Hanging methods on an `AsyncIterable` is unconventional; the handle type `RemoteStreamHandle` extends it explicitly.
-**Copying the Access mechanism.** Access control is business semantics, served by `vouch` and the `remote/invoke` waterfall; this Note only needs "whose stream is this", and `PeerScope` suffices.
+**Copying the Access mechanism.** Access control is business semantics, served by `vouch` and the `remote/invoke` waterfall; this Note only needs "whose stream is this", and `PeerScope` suffices. Method-level access control later moved to [Peer principals and method-level Remote access control](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.md), which supersedes this part.
**Keeping unary RPC for the uplink.** One HTTP round trip, one authentication, and one lookup per uplink; two carriers with no ordering relation that only Client-side serialization can order; no EOF; cancellation that does not cover both halves; a hand-made correlation key per feature.
@@ -574,10 +582,11 @@ for await (const reply of stream) replies.push(reply) // ['> a', '> b']
- **Bought**: one stream with two directions, so interactive scenarios such as terminal keystrokes, approval answers, and background-job stdin no longer each build their own "one stream plus one unary"; a Host method knows its caller; the uplink and downlink types are declared in one place, the return type; uplink items come from the browser and are validated strictly per item at the Host, while downlink items are Host-produced typed values that pass straight through; the generated output and wire frames of existing `AsyncIterable` methods and unary methods are completely unchanged.
- **`this.ctx.invocation` is `undefined` outside a Remote call**, so a method that reads it must handle the optional; a service method invoked only directly in process correctly reads `undefined`.
+- **Web terminals use `attach`** for screen output and input commands, with provider acknowledgements and per-attachment writable state; their Client retains the current generation's handle and never replays input.
- **The inbox limit applies to every stream**: a method that does not read its uplink fails as a whole when it receives many uplink frames. This is a deliberate explicit failure, recorded in the README.
-- **The operator is the only Peer**, and `admit` is the only admission point; the first consumer that needs a second Peer must add opening, association, and events in `dsh-client-connection`.
+- **Every browser connection shares `browserPeer`**, and `admit` is the only network admission point; in-process code creates further Peers with `connection.createPeer()`.
- **No cross-direction ordering guarantee between uplink and downlink**; a protocol that needs request-response pairing carries its own sequence numbers.
-- **Upper-layer protocols do not yet expose `send`**: consumers of `$stream`, snapshot, and journal use only the downlink today; the object they receive is already the handle.
+- **Upper-layer supervisors expose only the downlink**: `$stream`, snapshot and journal do not expose `send`; a domain that writes uplink retains its generated physical handle for that generation.
- **The worker tunnel behaves differently from the WebSocket carrier**: no inbox limit, and items after `end` are dropped, because page to worker is a same-origin trusted boundary.
## Testing
@@ -588,22 +597,20 @@ Retention cases in `gateway-stream.host.spec.ts` and `gateway.client.spec.ts` co
- Gateway Host: exact-key validation in frame parsing; `UplinkInbox` buffering during opening, `item` after `end`, overflow, ignored unknown `streamId`, and cleanup on socket closure; `UplinkDecoder` delivery with and without a codec and abort on decode failure; `uplink()` taken only once; the `extend({ invocation })` view and the origin of `peer`; `cancellableStream` closing the uplink before `return()`.
- Gateway Client: the handle's `send` (JSON validation, queued before open, throws after termination), idempotent `end`, `dispose` driving the `cancel` frame and ending later reads; the pump's three exits; the pump stopping as soon as a terminal frame arrives.
- typert: the analyzer's handling of `RemoteStream`, `RemoteStream`, `never`, and the existing `AsyncIterable`; decorator options; registry load-time validation.
-- connection: the three outcomes of `admit`, 401, 403, and the operator; the operator reaching handlers through the bridge and the shared handler; release with the Connection.
+- connection: the three outcomes of `admit`, 401, 403, and `browserPeer`; `browserPeer` reaching handlers through the bridge and the shared handler; release with the Connection.
- remote-mock and the worker tunnel: uplink pass-through; the direct proxy returning the real handle.
- Existing stream and unary method tests pass without changed expectations.
## Deferred
- `$stream` / snapshot / journal expose the handle's `send` to upper-layer protocols.
-- Web terminal: one `attach` stream replaces `follow` plus `write` plus `resize`, and `attachmentId` retires.
- Duplex `$events`: waterfall answers become uplink items on the same `streamId`, and `$events/result` and `clientId` retire.
- Credit frames: wait for a consumer that needs sustained high-volume uplink; the inbox limit is the initial credit.
-- The `streamInboxBytes` default of 262144 is far more than keyboard and stdin need; if the first consumer is terminal paste, it may be aligned with the `maxInputBytes` of `terminal-controller`.
- A Host-initiated half-close frame (the Host stops reading the uplink but keeps writing the downlink): no consumer; not added.
- `uplink()` on unary methods: it can read items that arrive while the method runs, semantically valid but of doubtful use; allowed, noted in the README.
## Related
-- [Remote event delivery](2026-08-10-remote-event-delivery.md): the unary `$events/result` answer path; the duplex `$events` item under Deferred partially supersedes it once it lands.
+- [Remote event delivery](../../../../packages/api/remotes/README.md): the unary `$events/result` answer path; the duplex `$events` item under Deferred partially supersedes it once it lands.
- [Session history and event transport](2026-08-18-session-history-and-event-transport.md): the `$stream()` supervisor and the journal / snapshot protocols above it, which this note leaves unchanged.
-- [Web sidebar terminal](../feature/2026-09-09-web-sidebar-terminal.md): the unary terminal `write` and `attachmentId`; the `attach` stream under Deferred replaces it once it lands.
+- [Web sidebar terminal](../feature/2026-09-09-web-sidebar-terminal.md): the `attach` stream carries input commands, provider acknowledgements and per-attachment writable state; reconnect never replays input.
diff --git a/.agents/notes/implemented/architecture/2026-09-19-remote-duplex-stream.zh.md b/.agents/notes/implemented/architecture/2026-09-19-remote-duplex-stream.zh.md
index 2e119bf054..bc219b5d5e 100644
--- a/.agents/notes/implemented/architecture/2026-09-19-remote-duplex-stream.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-19-remote-duplex-stream.zh.md
@@ -8,41 +8,41 @@ Status: implemented
### 现状
-`dsh-api-gateway` 在一条 WebSocket 上复用全部 Typert Remote 流,路径固定为 `/api/remote.mux`(`packages/api/gateway/src/stream-protocol.ts`)。一个 Host 方法用 `@Remote({ mode: 'stream' })` 修饰并返回 `Iterable` 或 `AsyncIterable`,就成为一条 Host 到 Client 的逻辑流;可选的末位 `signal: AbortSignal` 是唯一的保留参数,不进线路参数,由网关在解码后的业务参数之后追加(`packages/api/gateway/src/index.ts`)。客户端拿到的生成方法返回一个裸 `AsyncIterable`。
+`dsh-api-gateway` 在 `/api/remote.mux` 的一条 WebSocket 上复用 Typert Remote 流。Host 方法通过 `@Remote({ mode: 'stream' })` 和 `RemoteStream` 声明两个方向;生成的 Client 方法返回 `RemoteStreamHandle`。既有的 `Iterable` 和 `AsyncIterable` 返回类型声明仅下行的方法。可选的末位 `signal: AbortSignal` 由 Gateway 提供,不进入线路参数。
-线路帧只有五种:
+两个方向共用一个 `streamId`:
| 方向 | 帧 | 定义 |
| --- | --- | --- |
| Client 到 Host | `{ type: 'open', streamId, endpoint, payload }` | `stream-protocol.ts` |
+| Client 到 Host | `{ type: 'item', streamId, value? }` | `stream-protocol.ts` |
+| Client 到 Host | `{ type: 'end', streamId }` | `stream-protocol.ts` |
| Client 到 Host | `{ type: 'cancel', streamId }` | `stream-protocol.ts` |
| Host 到 Client | `{ type: 'item', streamId, value? }` | `stream-protocol.ts` |
| Host 到 Client | `{ type: 'error', streamId, error: { code, message, details } }` | `stream-protocol.ts` |
| Host 到 Client | `{ type: 'end', streamId }` | `stream-protocol.ts` |
-客户端能发的只有打开与取消。解析器 `parseRemoteStreamClientMessage`(`stream-protocol.ts`)对其他任何帧抛错,Host 连接收到后以 1008 关闭整个 socket(`stream-server.ts`)。结论:网关今天只支持"后端 iterable 发到前端"。需要前端向后端持续发数据的功能,各自绕路。
+Client 句柄的 `send()` 提供上行项,`end()` 半关闭上行,`dispose()` 取消逻辑流。Host 通过 `this.ctx.invocation.uplink()` 读取解码后的项;调用上下文同时携带获准接入的 Peer 和取消信号。
-Host 方法也没有"这次调用是谁发起的"这个概念:`InvokeRemoteRequest = { namespace, method, args, signal }`(`packages/api/gateway/src/types.ts`)没有调用者槽位,`RemoteStreamOpener` 的签名是 `(endpoint, payload, signal)`(`stream-server.ts`),WebSocket 只在握手时校一次 cookie。
+传输层负责校验、取消和有界 inbox。消费者协议负责输入权限、确认以及断线后的恢复。
-### 三套各自为政的上行机制
+### 上行消费者
| 功能 | 上行方式 | 位置 | 代价 |
| --- | --- | --- | --- |
-| Web 终端按键 | 每次 xterm `onData` 一个 unary RPC `terminal.write(agent, id, attachmentId, data)`,客户端用 promise 链串行化并自限字节预算 | `packages/api/terminal-controller/src/client/model.ts`;Host `src/index.ts` | 一次按键一次 HTTP 往返与鉴权;两条载体靠 `attachmentId` 手工关联;满了报 `inputFull` |
+| Web 终端输入与尺寸调整 | `terminal.attach` 承载上行 `TerminalInput` 和下行 `TerminalFrame`;Client 连续发送命令,并在收到 `ack` 后释放原始输入字节预算 | `packages/api/terminal-controller/src/client/model.ts`;Host `src/terminal.ts` | 当前附件拥有输入权;Client 预算耗尽时报 `inputFull`;重连后绝不重放输入 |
| 文件上传 | `Blob` 或 `ReadableStream` 走 Remote 层之外的原生 `POST /api/session/uploadFileBinary`,`duplex: 'half'` | `packages/client/file-upload/src/client/runtime.ts`;路由 `src/index.ts` | 不经 Typert 描述符,没有类型投影 |
| 审批与提问的回答 | Host 在 `$events` 流上下发 waterfall 帧,Client 用一个独立的 unary RPC `$events/result` 回答,靠 `clientId` 加 `eventId` 关联 | `packages/api/gateway/src/client/remote-events.ts`;Host `index.ts` | 第二套关联键;`index.ts` 记录了由此产生的竞态 |
-这三套机制解决的是同一个问题:一条已经打开的逻辑流,客户端想往里写。
+这些消费者都需要在相关操作仍进行时从 Client 向 Host 传送数据。Web 终端使用双工流;文件上传和事件回答保留各自的传输方式。
-### 具体代价
+### 消费者职责
-- **两条载体对齐**。终端要用 `attachmentId` 把 `follow` 流与 `write` RPC 关联起来,并用它决定谁拥有输入权。审批要用 `clientId` 把 `$events` 流与 `$events/result` RPC 关联起来。
-- **没有顺序保证**。unary RPC 走 HTTP,与 WebSocket 下行互不排序;终端只能在客户端串行化写入来自保顺序。
-- **每次上行单独鉴权与查找**。`write(agent, id, attachmentId, data)` 每次解析 `agent`、查终端、校验 attachment。
-- **没有 EOF**。客户端无法表达"我这边写完了",半关闭要再加一个 RPC。
-- **取消不覆盖两半**。取消下行流不会取消排队中的上行写;反之亦然。
-- **背压各写一套**。下行按 socket 写回调节拍拉迭代器(`stream-server.ts`);终端用有界 follower 让慢消费者显式失败(`terminal-controller/src/stream.ts`);上行靠客户端自限 `maxInputBytes`。
-- **文档已经过时**。`docs/api-gateway.md:160` 仍写着 Remote 只处理一请求一结果,这一句已经落后于 README 记录的 stream 模式。
+- **输入权限**。终端通过 Host 本地的 follower 持有控制附件。快照和状态帧报告各附件的 `writable` 状态;控制权转移后,旧附件继续接收输出。
+- **顺序与确认**。终端输入和尺寸调整共用连续命令序列及终端操作队列。`ack` 报告提供者已接受操作或输入权限拒绝,绝不授权断线后重放。
+- **独立生命周期**。取消 `attach` 会停止其输入和输出传递、丢弃尚未开始的命令,并等待在途输入操作收尾。PTY 的生命周期由保留机制和显式关闭独立管理。
+- **协调字节限额**。Gateway 通过 `invocation.uplinkWindow` 提供完整帧 inbox 容量及本流的 JSON 封装开销。终端在快照中公布窗口,并通过提供者确认释放在途额度。窗口满时,输入和尺寸调整命令在有界 Client 队列中等待。`maxInputBytes` 限制原始 UTF-8 输入;`maxBufferedBytes` 分别限制 Client 保留的完整编码输入及每个 follower 保留的输出。
+- **仍独立的载体**。审批回答仍通过 `clientId` 和 `eventId` 将 `$events` 与 `$events/result` RPC 关联。将该协议迁入双工流仍属后续工作。
### 与后台 job 的关系
@@ -66,7 +66,7 @@ Host 方法也没有"这次调用是谁发起的"这个概念:`InvokeRemoteReq
Client Host
───────────────────────────────── ───────────────────────────────────────────
const s = remote.job.attach(req, signal)
- │ open { streamId, endpoint, payload } ───────▶ opener(endpoint, payload, uplink, peer, control)
+ │ open { streamId, endpoint, payload } ───────▶ opener(endpoint, payload, uplink, peer, control, window)
│ │ prepareInvocation
│ s.send(v1) → item { streamId, v1 } ───────▶ inbox.push(v1) │ invocation = { request, service, peer, signal, uplink() }
│ s.send(v2) → item { streamId, v2 } ───────▶ inbox.push(v2) │ receiverContext.extend({ invocation }).get(service)
@@ -88,7 +88,7 @@ Host 方法也没有"这次调用是谁发起的"这个概念:`InvokeRemoteReq
| 半关闭 | 一个方向已结束而另一个方向仍开放 |
| 项 | 一个方向上的一个业务值;每项独立经 codec 校验 |
| inbox | Host 侧每逻辑流一个的有界上行队列 |
-| Peer | 连接层被接纳的一方,一个 `PeerScope`;本 Host 只有操作者一个 Peer |
+| Peer | 一条连接代表的一方,一个携带 principal 的 `PeerScope`;所有浏览器连接共享 `connection.browserPeer`,进程内代码经 `connection.createPeer()` 自建([Peer 身份属性](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md)) |
| 调用上下文 | `RemoteInvocation`:本次调用的请求、接收服务、Peer、取消信号与上行入口 |
### 类型签名
@@ -135,10 +135,11 @@ export interface RemoteInvocation {
}
/** Cordis service key of the receiving service. */
readonly service: string
- /** The Peer that initiated the call. In-process carriers and unadmitted calls are the operator. */
+ /** The Peer that initiated the call; every carrier names it explicitly. */
readonly peer: PeerScope
/** Carrier cancellation: Client cancel, socket closure, uplink failure. */
readonly signal: AbortSignal
+ readonly uplinkWindow?: RemoteUplinkWindow | undefined
/**
* The uplink items of this call. Can be taken once; the second call throws. With an uplink codec
* in the descriptor each item is decoded to In; without one, items are delivered as unknown with
@@ -179,24 +180,30 @@ declare module '@deepseek-ai/cordis' {
/** The opaque identity of one Peer. */
export type PeerId = Branded<'PeerId'>
-/** One Peer's session on this Host: opened and released by the admitting side; ctx owns connection-lifetime registrations. */
+/** One Peer's session on this Host: opened and released by its creator; ctx owns connection-lifetime registrations. */
export interface PeerScope {
readonly id: PeerId
readonly ctx: Context
+ /** Caller identity fixed at creation. */
+ readonly principal: Principal
+ /** Aborted once the Peer is disposed. */
+ readonly signal: AbortSignal
+ /** Participation in forwarded interactions; independent of RBAC. */
+ readonly interactions: 'answer' | 'none'
dispose(): Promise
}
```
-本 Host 只有一个 Peer:操作者。`@deepseek-ai/dsh-client-connection` 在服务 apply 时用 `createScope(connectionCtx, peer)` 建它,与 Agent 建自己的 scope 是同一机制:Peer 对象就是 ScopeKey,`peer.ctx` 承接连接期效果,`dispose()` 随 connection 释放让 fiber 静默。它以 `connection.operator` 暴露。
+`@deepseek-ai/dsh-client-connection` 实现唯一的 Peer 类 `Peer(owner, { principal, interactions? })`,用 `createScope(owner, peer)` 建立,与 Agent 建自己的 scope 是同一机制:Peer 对象就是 ScopeKey,`peer.ctx` 承接连接期效果,scope 结束时 `peer.signal` 中止,`dispose()` 让 fiber 静默。Connection 在服务 apply 时以 owner principal 创建 `connection.browserPeer`;principal、`createPeer()` 与访问检查归 [Peer 身份属性与 Remote 方法级访问控制](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md)。
| 成员 | 语义 |
| --- | --- |
-| `connection.operator: PeerScope` | 唯一的 Peer,随 connection 生灭 |
-| `connection.admit(request): PeerAdmission` | 过既有的 `requestRejection`:被拒返回 `{ rejection: 401 \| 403 }`,否则返回 `{ peer: operator }`。将来的接纳方在这里接入 |
+| `connection.browserPeer: PeerScope` | 所有通过认证的浏览器连接共享的 Peer;principal 为 owner;随 connection 生灭 |
+| `connection.admit(request): PeerAdmission` | 检查 Host/Origin 与浏览器或 API 签名 Cookie;返回 `{ rejection: 401 \| 403 }` 或凭据对应的存活 Peer;API Peer 在 connect 时发布,并由其生命周期 socket 负责释放 |
-网关 `handleUpgrade(req, socket, head, peer)` 为该 socket 绑一个固定 Peer 的 opener,并在 `peer.ctx.effect` 里登记 socket 关闭;scope 已失效时改为直接以 1001 关闭 socket。`InvokeRemoteRequest.peer?` 与 `RemoteStreamOpener` 的 `peer` 形参缺席时按操作者作答,现有进程内调用方零改动;没有 connection 的组合里网关自建的操作者替身同样经 `createScope` 建立,`dispose()` 契约一致。按 id 查 Peer、把载体关联到第二个 Peer、Peer 的开关事件,都等第一个需要多 Peer 的消费者出现时再加。
+网关 `handleUpgrade(req, socket, head, peer)` 为该 socket 绑一个固定 Peer 的 opener,并在 `peer.ctx.effect` 里登记 socket 关闭;scope 已失效时改为直接以 1001 关闭 socket。`InvokeRemoteRequest.peer`、`RemoteStreamOpener` 与 `wireStream.open` 的 `peer` 形参均为 required,每个进程内调用方都显式给出 Peer,网关不自建 Peer。按 id 查 Peer、Peer 的开关事件,都等第一个需要它们的消费者出现时再加。
-Peer 不携带访问模型:Peer 是谁、能做什么,由业务插件通过 `peer.ctx` 自行附加。
+Peer 携带自己的 principal;该 principal 能调用什么,由 [Peer 身份属性与 Remote 方法级访问控制](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md) 定义的 role、binding 与 policy 判定。
### Client 面
@@ -311,7 +318,7 @@ credit 帧留待需要持续大流量上行的消费者出现时再加;inbox
| worker 隧道(`packages/experimental/webworker-runtime`) | 页面新增 `stream-uplink-item` / `stream-uplink-end` 帧;`serveStream` 把它们组装成 `uplink` 交给 `seams.openStream`。同源可信边界:没有 inbox 上限,`end` 之后的项丢弃 |
| `remote-mock`(`packages/test-support/remote-mock`) | `StreamHandle.uplink: AsyncIterable`;`rpc.open` 透传 |
-进程内载体的 `peer` 缺席即操作者。
+进程内载体显式传入 Peer;worker 隧道的页内 Host 传 `connection.browserPeer`。
### 与 `$events` 的关系
@@ -325,7 +332,7 @@ credit 帧留待需要持续大流量上行的消费者出现时再加;inbox
// types.ts
export type RemoteStream = AsyncIterable & { readonly [STREAM_UPLINK]?: In }
export type PeerId = Branded<'PeerId'>
-export interface PeerScope { readonly id: PeerId; readonly ctx: Context; dispose(): Promise }
+export interface PeerScope { readonly id: PeerId; readonly ctx: Context; readonly principal: Principal; readonly signal: AbortSignal; dispose(): Promise }
export interface RemoteInvocation { … } // see Host face
declare module '@deepseek-ai/cordis' { interface Context { readonly invocation: RemoteInvocation | undefined } }
@@ -362,13 +369,13 @@ export interface InvokeRemoteRequest {
readonly args: Readonly>
/** Uplink items; absent is equivalent to an iterator that ends immediately. */
readonly uplink?: AsyncIterable
- /** The Peer that initiated the call; absent means the operator. */
- readonly peer?: PeerScope
+ /** The Peer that initiated the call; required on every path. */
+ readonly peer: PeerScope
readonly signal?: AbortSignal
}
export interface TypertGatewayWireStream {
- open: (endpoint: string, payload: unknown, uplink: AsyncIterable, peer: PeerScope | undefined, signal: AbortSignal) => Promise>
+ open: (endpoint: string, payload: unknown, uplink: AsyncIterable, peer: PeerScope, signal: AbortSignal) => Promise>
failure: (error: unknown) => RemoteStreamFailure
}
@@ -379,6 +386,7 @@ export type RemoteStreamOpener = (
uplink: AsyncIterable,
peer: PeerScope,
control: AbortController,
+ window: RemoteUplinkWindow,
) => Promise>
// index.ts Config addition
@@ -394,7 +402,7 @@ const signal = request.signal ?? NEVER_ABORTED_SIGNAL
const invocation = new GatewayInvocation(
{ namespace: request.namespace, method: request.method, args: request.args },
descriptor.service,
- request.peer ?? this.operatorPeer(),
+ request.peer,
signal,
new UplinkDecoder(request.uplink ?? EMPTY_ASYNC_ITERABLE, descriptor.uplink?.codec, endpoint, control),
)
@@ -444,8 +452,8 @@ cancel : missing → ignore; otherwise control.abort(new Error('Remote stream ca
| `dsh-typert-protocol`(`types.ts`、`index.ts`) | `RemoteStream`、`RemoteStreamHandle`、`PeerId`、`PeerScope`、`RemoteInvocation`、`ctx.invocation` 声明合并、`InvocationDescriptor.uplink`;修饰器只认 `mode: 'stream'`;`TypertRemoteService` 构造时注册 `invocation` accessor |
| `dsh-typert-registry` | 加载校验 `uplink.codec` 为有效的严格 codec |
| `dsh-typert-generator`(`model.ts`、`analyzer.ts`、`emitter.ts`) | `remoteResultType` 识别 `RemoteStream` 并生成 `uplink` boundary;描述符输出 `uplink: { codec }`;生成的 Client 签名返回 `RemoteStreamHandle`;参数循环只认业务参数与末位 `signal` |
-| `dsh-client-connection`(`operator-peer.ts`、`rpc.ts`、`rpc-host.ts`、`index.ts`) | 操作者 `PeerScope`;`connection.operator` 与 `admit`;`/api` 与升级路由经 `admit` 取 Peer;`rpc.open` 的可选 `uplink` |
-| `dsh-api-gateway` Host(`stream-protocol.ts`、`stream-server.ts`、`types.ts`、`index.ts`) | `item` / `end` 帧;`UplinkInbox`;`Config.streamInboxBytes`;`GatewayInvocation` 与 `extend({ invocation })`;`UplinkDecoder`;`handleUpgrade(req, socket, head, peer)`;`operatorPeer()` |
+| `dsh-client-connection`(`peer.ts`、`rpc.ts`、`rpc-host.ts`、`index.ts`) | `Peer` 类;`connection.browserPeer` 与 `admit`;`/api` 与升级路由经 `admit` 取 Peer;`rpc.open` 的可选 `uplink` |
+| `dsh-api-gateway` Host(`stream-protocol.ts`、`stream-server.ts`、`types.ts`、`index.ts`) | `item` / `end` 帧;`UplinkInbox`;`Config.streamInboxBytes`;`GatewayInvocation` 与 `extend({ invocation })`;`UplinkDecoder`;`handleUpgrade(req, socket, head, peer)` |
| `dsh-api-gateway` Client(`client/index.ts`、`client/stream-client.ts`) | `invokeStream` 返回句柄;上行泵由句柄的队列驱动 |
| `dsh-webworker-runtime`、`dsh-remote-mock` | worker 隧道的两种上行帧与 `serveStream` 组装;`StreamHandle.uplink`;直接代理返回真句柄 |
| 文档 | gateway、api 组、connection、protocol、generator 的 README;`docs/api-gateway`;`docs/subsystems/typert` 的 type-equiv 块;config 与 Cordis 目录 |
@@ -560,7 +568,7 @@ for await (const reply of stream) replies.push(reply) // ['> a', '> b']
**客户端 `send` 放在生成方法返回的 `AsyncIterable` 上。** `AsyncIterable` 上挂方法不常规;句柄类型 `RemoteStreamHandle` 显式扩展它。
-**照搬 Access 机制。** 访问控制是业务语义,`vouch` 与 `remote/invoke` waterfall 为它服务;本 Note 只需要"这条流是谁的",`PeerScope` 就够。
+**照搬 Access 机制。** 访问控制是业务语义,`vouch` 与 `remote/invoke` waterfall 为它服务;本 Note 只需要"这条流是谁的",`PeerScope` 就够。方法级访问控制后来归入 [Peer 身份属性与 Remote 方法级访问控制](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md),该部分由其 supersede。
**继续用 unary RPC 做上行。** 每次上行一次 HTTP 往返、一次鉴权、一次查找;两条载体没有顺序关系,只能靠客户端串行化;没有 EOF;取消不覆盖两半;每个功能各造一套关联键。
@@ -574,10 +582,11 @@ for await (const reply of stream) replies.push(reply) // ['> a', '> b']
- **买到的**:一条流两个方向,终端按键、审批回答、后台任务 stdin 这类交互式场景不再各造一套"一条流加一个 unary";Host 方法知道调用者;上行类型与下行类型在返回类型里一处声明,上行项来自浏览器,在 Host 逐项严格校验,下行项是 Host 自产的类型值,直接透传;既有 `AsyncIterable` 方法与 unary 方法的生成物与线路帧完全不变。
- **`this.ctx.invocation` 在非 Remote 调用下是 `undefined`**,方法读它要处理可选;一个只被进程内直接调用的服务方法读到 `undefined` 是正确行为。
+- **Web 终端使用 `attach`** 传输屏幕输出和输入命令,并提供提供者确认和附件专属可写状态;Client 保留当前代次的句柄,绝不重放输入。
- **inbox 上限对所有流生效**:一个不读上行的方法收到大量上行帧会整流失败。这是刻意的显式失败,README 记明。
-- **Peer 只有操作者一个**,`admit` 是唯一的接纳点;第一个需要第二个 Peer 的消费者要在 `dsh-client-connection` 加上打开、关联与事件。
+- **所有浏览器连接共享 `browserPeer`**,`admit` 是唯一的网络接纳点;进程内代码经 `connection.createPeer()` 创建其他 Peer。
- **上行与下行之间没有跨方向顺序保证**;需要请求应答配对的协议自带序号。
-- **上层协议尚未透出 `send`**:`$stream`、snapshot、journal 的消费者今天只用下行;它们拿到的对象已经是句柄。
+- **上层监督器仅暴露下行**:`$stream`、snapshot 和 journal 不暴露 `send`;需要写入上行的领域保留该代次的生成物理句柄。
- **worker 隧道与 WebSocket 载体行为不同**:没有 inbox 上限,`end` 之后的项丢弃,因为页面到 worker 是同源可信边界。
## 测试
@@ -588,22 +597,20 @@ for await (const reply of stream) replies.push(reply) // ['> a', '> b']
- 网关 Host:帧解析的精确键校验;`UplinkInbox` 的 opening 缓冲、`end` 后 `item`、超限、未知 `streamId` 忽略、socket 关闭时的收尾;`UplinkDecoder` 有无 codec 两种交付与解码失败中止;`uplink()` 只能取一次;`extend({ invocation })` 视图与 `peer` 的来源;`cancellableStream` 先关 uplink 再 `return()`。
- 网关 Client:句柄的 `send`(JSON 校验、open 前入队、终止后抛错)、`end` 幂等、`dispose` 驱动 `cancel` 帧并结束后续读取;泵的三种退出;终止帧到达即停泵。
- typert:analyzer 对 `RemoteStream`、`RemoteStream`、`never` 与既有 `AsyncIterable` 的处理;修饰器选项;registry 加载校验。
-- connection:`admit` 的 401、403 与操作者三种结果;操作者经桥与共享处理器到达;随 connection 释放。
+- connection:`admit` 的 401、403 与 `browserPeer` 三种结果;`browserPeer` 经桥与共享处理器到达;随 connection 释放。
- remote-mock、worker 隧道:上行透传;直接代理返回真句柄。
- 既有 stream 与 unary 方法的测试不改期望值即通过。
## 后续
- `$stream` / snapshot / journal 把句柄的 `send` 透出给上层协议。
-- Web 终端:一条 `attach` 流取代 `follow` 加 `write` 加 `resize`,`attachmentId` 退场。
- 双工 `$events`:waterfall 的回答成为同一 `streamId` 上的上行项,`$events/result` 与 `clientId` 退场。
- credit 帧:等需要持续大流量上行的消费者出现;inbox 上限就是初始授信。
-- `streamInboxBytes` 默认值 262144 对键盘与 stdin 远超需要;若第一个消费者是终端粘贴,可能与 `terminal-controller` 的 `maxInputBytes` 对齐。
- Host 发起的半关闭帧(Host 不再读上行但继续写下行):没有消费者,不加。
- unary 方法的 `uplink()`:能读到方法运行期间到达的项,语义成立但用处存疑;允许,README 注明。
## 相关
-- [Remote 事件投递](2026-08-10-remote-event-delivery.zh.md):`$events/result` 的 unary 回答路径;「后续」里的双工 `$events` 落地后部分取代它。
+- [Remote 事件投递](../../../../packages/api/remotes/README.zh.md):`$events/result` 的 unary 回答路径;「后续」里的双工 `$events` 落地后部分取代它。
- [会话历史与事件传输](2026-08-18-session-history-and-event-transport.zh.md):`$stream()` 监督器与其上的 journal / snapshot 协议,本 Note 不改它们。
-- [Web 侧边栏终端](../feature/2026-09-09-web-sidebar-terminal.zh.md):终端按键的 unary `write` 与 `attachmentId`;「后续」里的 `attach` 流落地后取代它。
+- [Web 侧边栏终端](../feature/2026-09-09-web-sidebar-terminal.zh.md):`attach` 流承载输入命令、提供者确认和附件专属可写状态;重连绝不重放输入。
diff --git a/.agents/notes/implemented/architecture/2026-09-19-timed-user-question-two-settlements.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-19-timed-user-question-two-settlements.i18n.yaml
new file mode 100644
index 0000000000..596b6ca28b
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-19-timed-user-question-two-settlements.i18n.yaml
@@ -0,0 +1,43 @@
+# Bilingual-pair consistency record for 2026-09-19-timed-user-question-two-settlements.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-19-timed-user-question-two-settlements.md
+/agent-note-timed-user-questions-as-two-settlements:
+ en: 5b6fa73158a11213
+ zh: b55c32342d6e6692
+/agent-note-timed-user-questions-as-two-settlements/problem:
+ en: f665127a9b5b8225
+ zh: b19845f62310f3a2
+/agent-note-timed-user-questions-as-two-settlements/decision:
+ en: ce869bf2eed04e81
+ zh: 5b1fcf44c44c6137
+/agent-note-timed-user-questions-as-two-settlements/decision/modes-and-scope:
+ en: adccc5dc758ad8f6
+ zh: 50304d68db42e8b1
+/agent-note-timed-user-questions-as-two-settlements/decision/ownership:
+ en: a28e37fda6e9eaff
+ zh: 9ca622e09ab24b4d
+/agent-note-timed-user-questions-as-two-settlements/decision/foreground-wait-and-claim-lifetime:
+ en: 447713bbdf1ab4d8
+ zh: 2757678ae47822aa
+/agent-note-timed-user-questions-as-two-settlements/decision/durable-state-and-late-answers:
+ en: 93f3def953d10a2e
+ zh: e1106340e0043254
+/agent-note-timed-user-questions-as-two-settlements/decision/card-state-and-delivery-races:
+ en: 3dd7321bd37c51bf
+ zh: 2d1f058732bd6d0f
+/agent-note-timed-user-questions-as-two-settlements/decision/local-countdown-and-panel-actions:
+ en: 3a33f2583e30e32a
+ zh: 0ad7e8d1e47dc510
+/agent-note-timed-user-questions-as-two-settlements/decision/reply-node-and-grouping:
+ en: d87d7432c37405a7
+ zh: 49f1bbf53856851a
+/agent-note-timed-user-questions-as-two-settlements/alternatives-considered:
+ en: 1a070542bfa40e68
+ zh: 82e32964da912dd8
+/agent-note-timed-user-questions-as-two-settlements/verification:
+ en: 1d19d196c363a273
+ zh: 7cb0475f0cb554d1
+/agent-note-timed-user-questions-as-two-settlements/consequences:
+ en: 0580e85cc12439c5
+ zh: a4b151d2c9e5b747
diff --git a/.agents/notes/implemented/architecture/2026-09-19-timed-user-question-two-settlements.md b/.agents/notes/implemented/architecture/2026-09-19-timed-user-question-two-settlements.md
new file mode 100644
index 0000000000..014912c9a8
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-19-timed-user-question-two-settlements.md
@@ -0,0 +1,116 @@
+# Agent Note: Timed user questions as two settlements
+
+Status: implemented
+
+English | [中文](2026-09-19-timed-user-question-two-settlements.zh.md)
+
+## Problem
+
+A question can block an agent even when useful work does not depend on the answer. Releasing the tool call after a wait must not discard the question: the user may answer after the agent has continued or after reopening the Session. A pending result is neither a skipped answer nor permission to proceed with work that requires approval.
+
+The foreground tool result and the eventual answer have different lifetimes. A `user-questions/request` waterfall settles once; keeping it alive after returning pending creates a second answer path that outlives its caller. Countdown, focus, and editing also have different owners from durable question state.
+
+## Decision
+
+Foreground answers use the existing Remote Event waterfall. A timeout ends that request and returns pending; a later answer uses a business RPC that steers a durable user message. The claim stream controls the foreground wait only. It neither transports answers nor replaces the Remote Event.
+
+### Modes and scope
+
+- The default `mode: legacy` retains the blocking schema and `ask()` path. Explicit `mode: timed` adds `timeout`, using the configured default of 120 seconds when omitted.
+- A positive integer timeout selects `askTimed()`; `-1` selects blocking `ask()`. A native blocking call made with the timed schema still participates in timed-question projection and review; it is not identical to a legacy call.
+- Plan review retains `ask()`, its `plan-review` intent, and `BAD_INTENT` validation. This decision does not move plan review to another package.
+- Human interaction requires the exact live runtime root when an Agent is supplied. A stale instance fails with `CALLER_NOT_LIVE`; an owned child fails with `DELEGATED_CALLER`. Durable Session lineage does not prevent a resumed runtime root from asking.
+
+### Ownership
+
+| Owner | Responsibility |
+|---|---|
+| `tool-ask-user` | Model schema, timeout validation, service selection, and pending-result text. |
+| `UserQuestionService` | Foreground waits and Client claims, unattended deadlines, late-answer validation and steering, and the durable question projection. |
+| Gateway and Remote runtime | Existing request delivery, settlement, cancellation, and Remote stream transport; no question-specific timer or event-name branch. |
+| `ui-user-questions` | One card per Session and call, answer-channel selection, local countdown and drafts, and the reply Definition and renderer. |
+| `ui-chat` | Ordinary process/Turn folding. |
+| Question-specific tool row | Pending/answered presentation and panel actions, composed in `QuestionToolRow` without adding `rowAction` to generic `ToolRow`. |
+
+The [question service](../../../../packages/interaction/user-questions/README.md), [tool](../../../../packages/interaction/tool-ask-user/README.md), and [questions UI](../../../../packages/client/ui-user-questions/README.md) own their package contracts.
+
+### Foreground wait and claim lifetime
+
+`TimedQuestionWait` owns a private cancellation signal, the original Host deadline, and Client claims. The Host runs a timer only while no answer UI holds a claim. Delivery to a transport queue is not a claim. Losing the last claim restores the original deadline, with immediate expiry if it has passed.
+
+A timed request carries `wait: { callId, timed: true }`. Before attaching its answer channel, the Client opens `attachWait`, a business Remote stream that yields one Host-computed `remainingMs` frame and stays open for the claim's lifetime. Focus, edits, and heartbeats do not travel through this stream.
+
+The Client retains its claim through delivery of the waterfall outcome to the Host. Returning an answer locally is not delivery: releasing the claim then could let an overdue Host timer beat that answer. The Host closes the claim when the foreground request settles. Explicit delegation releases the claim before `next()`; plugin teardown delegates and cancels its stream.
+
+Client countdown expiry rejects the waterfall with `ASK_TIMED_OUT`. Unattended Host expiry aborts only the private wait signal with the same business error. `askTimed()` maps only that error to pending, maps parent cancellation to `ASK_ABORTED`, and propagates other failures. `NO_PROVIDER` waits on the same bounded wait lifetime, not a permanently unresolved promise. Foreground completion releases timers, claims, and the wait registry entry.
+
+### Durable state and late answers
+
+The `userQuestions` projection reconstructs question state from existing Session events. For native calls, it recognises the timed schema by the `timeout` parameter recorded in `request/header`; legacy calls are not tracked. A valid `tool/call` opens a question. Its `tool/result` makes it continued for pending or `TOOL_OUTCOME_UNKNOWN`, settles it with a valid answer batch, or removes it for another result or failure. A PTC sub-call enters the projection when its `tool/ptc-dispatch` result is pending, since only `run_code` appears in the model request header.
+
+A continued question accepts `answer(agent, callId, answer)`. The method returns `false` for an unknown or non-continued call, throws `REPLY_QUEUED` while another reply awaits admission, and throws `BAD_ANSWER` unless the batch names each question exactly once. Accepted answers use `agent.steer(createUserMessage(...))`, source `{ kind: 'user-question-reply', callId, outcome: 'answered' }`, and JSON text containing `answer_to_pending_question`, the call id, original questions, and answers. Remote Agent resolution from the Session id resumes a root when needed; the `answer()` body does not own that resume operation.
+
+The inbox message is the durable queued reply. A per-Session reservation covers the interval from queuing through claim and admission; claiming a restored inbox reply creates the reservation again. Admission as `user/message` settles the question, while discard or a Turn that ends without admission releases the reservation for another answer. Settled answer batches remain in the projection so the original tool row can show late answers even though its own result remains pending. Unusable recorded reply text settles with an empty batch.
+
+No new Session event type or wait-state log is needed. The message-source extension requires the repository's persistence-type acknowledgement. Hiding a panel does not manufacture a reply.
+
+### Card state and delivery races
+
+`QuestionCards` shares one `PendingQuestion` between the live request and the continued projection, keyed by Session and `callId`. A forwarded request keeps its card through the claim's first frame even before the projection lists it. A blocking request without a call id has its own per-request card with a reload-unique key, so an old persisted draft cannot attach to another request. An open card submits through its waterfall; without that channel it disables submission. A continued card uses the late-answer RPC.
+
+A waterfall submission has no delivery acknowledgement. The Gateway can discard an outcome that loses a settlement race. The card therefore retains its draft and stays busy until projection reconciliation. If the question becomes continued during submission, the controls re-arm with a resubmit hint; the user can submit the retained draft through RPC. An accepted RPC submission steers a reply toward the nearest step without settling the question, and the panel closes. A current tool wait must finish before that step can read the reply; if the steer is discarded, the question can be reopened and answered again. This is not automatic retry or a guarantee that every local submission reaches the model.
+
+A tool-call-keyed card is removed when its call is absent from the active projection and no forwarded request remains. Removal closes the card and clears its draft. Neither a local submission nor a transport cancel frame alone decides that the question is answered. An unkeyed blocking card ends with its waterfall; closing that blocking panel rejects with `ASK_CANCELLED`.
+
+### Local countdown and panel actions
+
+The Client computes `Date.now() + remainingMs`. Manual focus on a pristine answer surface pauses its countdown, and blur resumes the preserved remainder. Focus before the claim handshake remains effective when the deadline arrives; blur before the handshake does not create a timer. First edit or Take time freezes the countdown, with that disposition saved beside the browser-local draft.
+
+Autofocus applies only when the answer channel is ready, no countdown is active, and the card is not locked. Hiding a timed-call panel leaves its claim and countdown running; its tool row can reopen it. Once a late reply enters the Inbox, the tool row displays its submitted answers and “View answers” expands the read-only transcript while admission is pending. A settled call opens a read-only panel with recorded answers. Discarding the queued reply restores the answer action.
+
+### Reply node and grouping
+
+The questions UI identifies a late reply from `source.kind`, then parses its recorded JSON for question-and-answer presentation. Pasting identical JSON in an ordinary user message does not identify it as a reply. Unreadable recorded payloads retain their raw-text presentation.
+
+The question-reply Definition claims the appended reply message on the Chat target through an [exclusive match](2026-10-12-exclusive-conversation-matches.md), so the ordinary message Definition builds no `turn-trigger` or context projection for it and the reply renders once, including for unscoped historical input.
+
+The reply participates in ordinary process grouping and whole-Turn folding. It is not independently visible: reading it can require expanding both its Turn and process group. Turn disclosure stays before the group it controls, including a reply that opens the Turn.
+
+This decision adds no independent-layout flag or user-message structure change. The reply declares no group-tree position, so it follows the default placement of process content.
+
+## Alternatives considered
+
+**Unconditional Host deadline or Gateway-owned question timers.** An unconditional deadline overrides local editing and Take time. Putting the exception into pending Remote Events or branching on the question event in Remote dispatch makes generic transport own business policy. Claims keep that policy in the question service.
+
+**Keep the waterfall alive after returning pending.** The request outlives the tool caller, and late waterfall answers compete with the RPC. Ending the foreground request gives each settlement one answer path.
+
+**Delegate every timed request immediately.** Dispatching to an intentionally empty answerer set wastes the existing blocking-answer path and requires special handling of `NO_PROVIDER`. Delegation is for an unavailable answerer, not the timed mode itself.
+
+**Persist a whole-value wait/focus/edit stream or use Host focus leases.** Durable question status is derivable from tool and admitted user events. Focus and editing are local UI state; logging them or synchronising focus leases adds Host state without making multiple Clients share one editing session.
+
+**Reclassify JSON inside generic `MessageItem`, borrow the relay form, or use the default context row.** Parsing text to identify the business message confuses its source with its content; the relay form describes another agent's message, and a generic context row exposes JSON. A dedicated source, Definition, and renderer own the question presentation.
+
+**Independent reply layout through generic node metadata.** It broadens a question feature into a conversation-infrastructure change. Ordinary folding is sufficient for this decision; placing the reply as user input through its group-tree position is separate work.
+
+**A log-only close event or a manufactured dismissal message on panel close.** A log-only close hides the user's decision from the model and adds another durable mechanism. Manufacturing a reply treats hiding the panel as an answer. Panel close therefore leaves a timed question answerable.
+
+**A dedicated late-reply Session event beside the inbox message.** The inbox splice records the queued reply and the admitted user message records its delivery. A third event duplicates those facts and adds another persistence type.
+
+## Verification
+
+- [Service tests](../../../../packages/interaction/user-questions/tests/user-questions.spec.ts) and [wait tests](../../../../packages/interaction/user-questions/tests/timed-wait.spec.ts) cover claimed and unclaimed waits, last-claim disconnect, cancellation, timeout error mapping, continued-only answers, and exact answer-id validation.
+- [Projection tests](../../../../packages/interaction/user-questions/tests/projection.spec.ts) distinguish legacy and timed schemas and cover native and PTC pending calls, other failures, resume repair, queued reply cancellation, and admitted settlement.
+- [Client listener tests](../../../../packages/client/ui-user-questions/tests/browser-plugin.client.spec.ts) and [composer tests](../../../../packages/client/ui-user-questions/tests/user-questions-composer.client.spec.tsx) cover claim retention through Host acceptance, focus before handshake, indefinite waits, draft retention, RPC resubmission, and panel actions.
+- [Grouping tests](../../../../packages/client/ui-chat/tests/process-groups.client.spec.ts) and [browser replay](../../../../apps/web/tests/question-composer.e2e.ts) cover ordinary collapsed placement, disclosure ordering, and the original tool row's read-only late-answer panel.
+- Legacy round-trip, cancellation, plan-review intent, and Session-remount regressions remain covered by their owning tests. Live multi-Client delivery races and real Host restart remain integration coverage gaps beyond isolated lifecycle and projection tests.
+
+## Consequences
+
+The model can continue independent work while the question remains answerable. Business-owned timing and reply rendering reuse the existing transport and conversation APIs, at the cost of local rather than globally coordinated waiting and ordinary folded visibility.
+
+- Take time affects only one Client. Another Client can answer or expire the shared waterfall; local focus does not establish a global hold.
+- A terminal claim-stream failure can reject the foreground question rather than produce pending. No unattended-wait fallback is provided for that failure.
+- Host and Client clocks need not align, but Client system-clock changes can move expiry and transport latency can delay the local countdown's start.
+- Unanswered continued questions can remain after reopening a Session. Panel close records nothing, and visibility follows normal folding rather than an always-visible reply guarantee.
+- A queued late reply can be canceled before the agent admits it. The question stays answerable until an admitted `user/message` records the answer.
+- A PTC sub-call interrupted before its `tool/ptc-dispatch` result is recorded cannot be reconstructed from the parent `run_code` failure, so it cannot take a late answer after recovery.
diff --git a/.agents/notes/implemented/architecture/2026-09-19-timed-user-question-two-settlements.zh.md b/.agents/notes/implemented/architecture/2026-09-19-timed-user-question-two-settlements.zh.md
new file mode 100644
index 0000000000..91ddd5c6b4
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-19-timed-user-question-two-settlements.zh.md
@@ -0,0 +1,116 @@
+# Agent Note: 计时用户提问的两次结算
+
+Status: implemented
+
+[English](2026-09-19-timed-user-question-two-settlements.md) | 中文
+
+## Problem
+
+即使还有不依赖回答的工作可做,问题仍可能阻塞 agent(智能体)。等待后放行工具调用不能丢弃问题:用户可能在 agent 继续执行后,或重新打开 Session 后才回答。pending 结果既不是跳过回答,也不是开展需要批准的工作的授权。
+
+前台工具结果与最终回答的生命周期不同。`user-questions/request` waterfall(瀑布式事件)只结算一次;返回 pending 后仍保留它,会产生一条活过调用方的第二回答路径。倒计时、聚焦和编辑的归属也不同于持久问题状态。
+
+## Decision
+
+前台回答继续使用现有 Remote Event waterfall。超时结束该请求并返回 pending;迟到回答通过业务 RPC steer 一条持久用户消息。接手 stream 只控制前台等待,既不传递回答,也不替代 Remote Event。
+
+### 模式与范围
+
+- 默认 `mode: legacy` 保留阻塞式 schema 和 `ask()` 路径。显式 `mode: timed` 增加 `timeout`,省略时使用配置默认值 120 秒。
+- 正整数超时选择 `askTimed()`;`-1` 选择阻塞式 `ask()`。timed schema 发出的原生阻塞调用仍参与计时问题投影和答案查看,并不等同于 legacy 调用。
+- 计划评审保留 `ask()`、`plan-review` 意图及 `BAD_INTENT` 校验。本决策不把计划评审迁移到其他包。
+- 提供 Agent 时,人机交互要求它是运行时中确切的存活根实例。陈旧实例以 `CALLER_NOT_LIVE` 失败;被其他 agent 拥有的子实例以 `DELEGATED_CALLER` 失败。持久 Session 的血缘关系不妨碍恢复后的运行时根实例提问。
+
+### 职责归属
+
+| 归属方 | 职责 |
+|---|---|
+| `tool-ask-user` | 模型 schema、超时校验、服务选择和 pending 结果文本。 |
+| `UserQuestionService` | 前台等待与 Client 接手、无人接手时的 deadline、迟到回答校验与 steering(中途引导),以及持久问题投影。 |
+| Gateway 与 Remote 运行时 | 现有请求投递、结算、取消及 Remote stream 传输;不包含问题专属计时器或按事件名分支。 |
+| `ui-user-questions` | 每个 Session 和调用的一张卡片、回答通道选择、本地倒计时与草稿,以及回复 Definition 和渲染器。 |
+| `ui-chat` | 普通过程/轮次折叠。 |
+| 问题专用工具行 | pending/已回答呈现与面板操作,在 `QuestionToolRow` 中组合,不向通用 `ToolRow` 添加 `rowAction`。 |
+
+[问题服务](../../../../packages/interaction/user-questions/README.zh.md)、[工具](../../../../packages/interaction/tool-ask-user/README.zh.md)和[问题 UI](../../../../packages/client/ui-user-questions/README.zh.md)分别拥有各包的约定。
+
+### 前台等待与接手生命周期
+
+`TimedQuestionWait` 拥有专属取消 signal、原始 Host deadline 和 Client 接手记录。Host 只在没有回答 UI 接手时运行计时器。投递到传输队列不算接手。最后一个接手方离开后恢复原 deadline,已经过期则立即到期。
+
+计时请求携带 `wait: { callId, timed: true }`。Client 在挂接回答通道前打开业务 Remote stream `attachWait`,接收一帧由 Host 计算的 `remainingMs`,并在接手期间保持 stream 打开。聚焦、编辑和心跳不经过该 stream。
+
+Client 将接手保留到 waterfall 结果交付给 Host。本地返回回答不等于交付:此时释放接手会让已经到期的 Host 计时器抢在回答之前结算。Host 在前台请求结算时关闭接手。显式委托在 `next()` 前释放接手;插件拆卸会委托请求并取消自己的 stream。
+
+Client 倒计时到期以 `ASK_TIMED_OUT` 拒绝 waterfall。无人接手时,Host 到期只以同一业务错误中止等待专属 signal。`askTimed()` 只把该错误映射为 pending,把父级取消映射为 `ASK_ABORTED`,其余失败继续传播。`NO_PROVIDER` 等待同一个有界等待生命周期,而不是永不结算的 Promise。前台完成时释放计时器、接手记录及等待注册项。
+
+### 持久状态与迟到回答
+
+`userQuestions` 投影从现有 Session 事件重建问题状态。对原生调用,它通过 `request/header` 记录的 `timeout` 参数识别 timed schema,不跟踪 legacy 调用。有效的 `tool/call` 打开问题。其 `tool/result` 在 pending 或 `TOOL_OUTCOME_UNKNOWN` 时将问题标为已继续,在有效回答批次时结算并保存答案,其他结果或失败则移除问题。PTC 子调用在 `tool/ptc-dispatch` 结果为 pending 时进入投影,因为模型请求头只列出 `run_code`。
+
+已继续的问题接受 `answer(agent, callId, answer)`。未知或非已继续调用返回 `false`;另一条回复等待准入时抛出 `REPLY_QUEUED`;回答批次没有恰好包含每个问题一次时抛出 `BAD_ANSWER`。接受的回答通过 `agent.steer(createUserMessage(...))` 送入 agent,来源为 `{ kind: 'user-question-reply', callId, outcome: 'answered' }`,JSON 文本包含 `answer_to_pending_question`、调用 id、原问题及答案。Remote 根据 Session id 解析 Agent 时按需恢复根实例;恢复操作不属于 `answer()` 方法体。
+
+inbox 消息是持久的排队回复。逐 Session 预约覆盖从排队、领取到准入的期间;领取恢复后的 inbox 回复时会重建预约。回复作为 `user/message` 准入时问题结算;丢弃回复或 Turn 结束而未准入时释放预约,允许再次回答。已结算答案批次保留在投影中,让原工具行即使自身结果仍是 pending,也能显示迟到回答。无法使用的已记录回复文本以空答案批次结算。
+
+无需新增 Session 事件类型或等待状态日志。消息来源扩展需要仓库的持久化类型变更确认。隐藏面板不会伪造回复。
+
+### 卡片状态与投递竞争
+
+`QuestionCards` 按 Session 和 `callId`,让实时请求与已继续投影共享一个 `PendingQuestion`。转发请求在投影列出问题之前,仍会让卡片存活至接手 stream 的首帧。没有调用 id 的阻塞请求拥有独立的逐请求卡片,并使用跨重载唯一的键,避免旧草稿附着到另一请求。开放卡片通过 waterfall 提交;缺少该通道时禁用提交。已继续卡片使用迟到回答 RPC。
+
+waterfall 提交没有送达确认。Gateway 可能丢弃输掉结算竞争的结果,因此卡片保留草稿并保持忙碌,等待投影同步。若提交期间问题变为已继续,控件带重提提示重新可用,用户可以通过 RPC 提交保留的草稿。RPC 接受提交后会将回复作为 steer 送往最近一步,但不会立即结算问题,同时关闭面板。当前工具等待须先结束,该步才能读取回复;若 steer 被丢弃,可以重新打开问题并再次回答。这不是自动重试,也不保证每次本地提交都能到达模型。
+
+按工具调用标识的卡片在调用不属于活动投影且没有转发中的请求时移除。移除会关闭卡片并清理草稿。本地提交和传输取消帧都不能单独决定问题已经回答。无调用标识的阻塞卡片随 waterfall 结束;关闭这种阻塞面板以 `ASK_CANCELLED` 拒绝请求。
+
+### 本地倒计时与面板操作
+
+Client 计算 `Date.now() + remainingMs`。手动聚焦未编辑的回答区会暂停倒计时,失焦后从保留的剩余时间继续。接手握手前的聚焦在 deadline 到达时仍生效;握手前失焦不会制造计时器。首次编辑或「慢慢回答」冻结倒计时,该状态与浏览器本地草稿一起保存。
+
+只有回答通道就绪、没有活动倒计时且卡片未锁定时才自动聚焦。隐藏计时调用的面板会保留接手和倒计时,工具行可以重新打开面板。迟到回复进入 Inbox 后,工具行显示已提交的答案;等待准入期间,「查看回答」展开只读的行内记录。已结算调用打开只读面板,显示记录的答案。排队回复被丢弃后,回答操作恢复可用。
+
+### 回复节点与分组
+
+问题 UI 通过 `source.kind` 识别迟到回复,再解析已记录的 JSON 来呈现问答。普通用户消息粘贴相同 JSON 不会被识别为回复。无法读取的已记录载荷保留原始文本呈现。
+
+问答回复 Definition 通过[独占匹配](2026-10-12-exclusive-conversation-matches.zh.md)在 Chat target 上认领追加的回复消息,普通消息 Definition 因此不再为它建立 `turn-trigger` 或 context 投影,回复只显示一次;无轮次归属的历史输入同样如此。
+
+回复参与普通过程分组和整轮折叠,不保证独立可见:阅读时可能需要展开所属轮次与过程分组。轮次折叠控件始终在所控制的分组之前,包括由回复开启轮次的情况。
+
+本决策不添加独立布局标记或用户消息结构变更。回复不声明分组树位置,因此按过程内容的默认位置放置。
+
+## Alternatives considered
+
+**无条件 Host deadline 或 Gateway 拥有问题计时器。** 无条件 deadline 会覆盖本地编辑和「慢慢回答」。把例外放入 pending Remote Events,或在 Remote 派发中按问题事件分支,会让通用传输拥有业务策略。接手机制将策略留在问题服务中。
+
+**返回 pending 后继续保留 waterfall。** 请求活过工具调用方,迟到的 waterfall 回答与 RPC 竞争。结束前台请求让每次结算各有一条回答路径。
+
+**立即委托所有计时请求。** 向故意留空的回答方集合派发请求,浪费现有阻塞回答路径,还要求特殊处理 `NO_PROVIDER`。委托表示回答方不可用,不代表计时模式本身。
+
+**持久化整值等待/聚焦/编辑流,或使用 Host 聚焦租约。** 持久问题状态可从工具事件与已准入的用户事件推导。聚焦和编辑属于本地 UI 状态;记录它们或同步聚焦租约会增加 Host 状态,却不能让多个 Client 共享同一编辑会话。
+
+**在通用 `MessageItem` 内重新分类 JSON、借用 relay form,或使用默认 context 行。** 解析文本来识别业务消息混淆了来源与内容;relay form 表示其他 agent 的消息,通用 context 行则会暴露 JSON。专用来源、Definition 和渲染器拥有问题呈现。
+
+**通过通用节点元数据让回复独立布局。** 这会把问题功能扩大为对话基础设施变更。普通折叠足以满足本决策;通过分组树位置把回复放置为用户输入是单独的工作。
+
+**面板关闭时写仅入日志的关闭事件,或伪造放弃消息。** 仅入日志会让模型看不到用户决定,还增加另一套持久机制。伪造回复则把隐藏面板当成回答。因此关闭面板仍让计时问题保持可回答。
+
+**在 inbox 消息之外增加专用迟到回复 Session 事件。** inbox 插入记录排队回复,准入的用户消息记录其交付。第三个事件重复这些事实,并增加另一种持久化类型。
+
+## 验证
+
+- [服务测试](../../../../packages/interaction/user-questions/tests/user-questions.spec.ts)与[等待测试](../../../../packages/interaction/user-questions/tests/timed-wait.spec.ts)覆盖有人与无人接手、最后接手方断开、取消、超时错误映射、仅接受已继续问题的回答,以及答案 id 的精确校验。
+- [投影测试](../../../../packages/interaction/user-questions/tests/projection.spec.ts)区分 legacy 与 timed schema,并覆盖原生及 PTC pending 调用、其他失败、恢复修复、排队回复取消和准入后结算。
+- [Client 监听器测试](../../../../packages/client/ui-user-questions/tests/browser-plugin.client.spec.ts)与[composer 测试](../../../../packages/client/ui-user-questions/tests/user-questions-composer.client.spec.tsx)覆盖接手保留到 Host 接受结果、握手前聚焦、无限期等待、草稿保留、RPC 重提及面板操作。
+- [分组测试](../../../../packages/client/ui-chat/tests/process-groups.client.spec.ts)与[浏览器回放](../../../../apps/web/tests/question-composer.e2e.ts)覆盖普通折叠位置、折叠控件顺序,以及原工具行的迟到答案只读面板。
+- legacy 问答往返、取消、计划评审意图及 Session 重新挂载的回归仍由各自测试覆盖。真实多 Client 投递竞争与真实 Host 重启仍是独立生命周期和投影测试之外的集成覆盖缺口。
+
+## Consequences
+
+模型可以继续独立工作,问题同时保持可回答。业务层拥有的计时与回复渲染复用现有传输和对话 API,代价是等待由本地而非全局协调,可见性遵循普通折叠。
+
+- 「慢慢回答」只影响一个 Client。其他 Client 可以回答或让共享 waterfall 到期,本地聚焦不构成全局暂停。
+- 接手 stream 的终止性失败可能让前台问题失败,而不是产生 pending。不为该失败提供无人接手等待的 fallback。
+- Host 与 Client 时钟不必对齐,但 Client 系统时钟变化可能移动到期时间,传输延迟可能推迟本地倒计时的开始。
+- 无人回答的已继续问题可以在重新打开 Session 后继续存在。关闭面板不写记录,可见性遵循普通折叠,而不是保证回复始终可见。
+- 排队的迟到回复可能在 agent 准入前被取消。只有已准入的 `user/message` 记录答案,问题在此之前保持可回答。
+- PTC 子调用若在记录 `tool/ptc-dispatch` 结果前中断,无法从父 `run_code` 的失败中重建,因此恢复后不能接受迟到回答。
diff --git a/.agents/notes/implemented/architecture/2026-09-20-sidebar-retained-tab-layout.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-20-sidebar-retained-tab-layout.i18n.yaml
index 641b4a5806..26bf24ff02 100644
--- a/.agents/notes/implemented/architecture/2026-09-20-sidebar-retained-tab-layout.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-20-sidebar-retained-tab-layout.i18n.yaml
@@ -21,8 +21,8 @@
en: a34c228751a1a329
zh: 8e8b962b55e7c68e
/agent-note-stable-sidebar-tab-mounting-and-session-retention/decision/retention-versus-recovery:
- en: aabfa99401aa0f73
- zh: 465ebf723c4393aa
+ en: 61e4a975fea17a9a
+ zh: 911f4d1caab1efd0
/agent-note-stable-sidebar-tab-mounting-and-session-retention/alternatives-considered:
en: 84cfe7613657bfdb
zh: 4a0a499eb5d36d22
diff --git a/.agents/notes/implemented/architecture/2026-09-20-sidebar-retained-tab-layout.md b/.agents/notes/implemented/architecture/2026-09-20-sidebar-retained-tab-layout.md
index 3c1e23ad93..574667c79f 100644
--- a/.agents/notes/implemented/architecture/2026-09-20-sidebar-retained-tab-layout.md
+++ b/.agents/notes/implemented/architecture/2026-09-20-sidebar-retained-tab-layout.md
@@ -51,7 +51,7 @@ The [Desktop Browser decision](../feature/2026-09-20-desktop-browser-webview.md)
| Application restart or window reload | Show the saved title and URL; load only after an explicit restore or address submission. |
| Guest crash | Retry through the provider; page memory and native history are not durable. |
-[Layout and provider recovery](2026-09-14-sidebar-layout-provider-recovery.md) remains independent: persisted tab identity, placement and address support cold starts, not DOM serialization. A hidden page may continue scripts, media and networking.
+[Layout and provider recovery](../../../../packages/client/ui-sidebar-right/README.md) remains independent: persisted tab identity, placement and address support cold starts, not DOM serialization. A hidden page may continue scripts, media and networking.
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-09-20-sidebar-retained-tab-layout.zh.md b/.agents/notes/implemented/architecture/2026-09-20-sidebar-retained-tab-layout.zh.md
index 000174a0c3..a2a88a209b 100644
--- a/.agents/notes/implemented/architecture/2026-09-20-sidebar-retained-tab-layout.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-20-sidebar-retained-tab-layout.zh.md
@@ -51,7 +51,7 @@ Tab 提供 Body 保活持有,而不独立申请会话引用。View 的稳定 `
| 应用重启或窗口刷新 | 展示保存的标题和 URL,显式恢复或提交地址后才加载。 |
| guest 崩溃 | 通过提供方重试;页面内存和原生 history 不持久化。 |
-[布局与提供方恢复](2026-09-14-sidebar-layout-provider-recovery.zh.md)仍然独立:持久化的 Tab 身份、位置和地址支持冷启动,而不是序列化 DOM。隐藏页面可能继续执行脚本、播放媒体或联网。
+[布局与提供方恢复](../../../../packages/client/ui-sidebar-right/README.zh.md)仍然独立:持久化的 Tab 身份、位置和地址支持冷启动,而不是序列化 DOM。隐藏页面可能继续执行脚本、播放媒体或联网。
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-09-21-conversation-build-groups.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-21-conversation-build-groups.i18n.yaml
index f715253877..bbac97a7ad 100644
--- a/.agents/notes/implemented/architecture/2026-09-21-conversation-build-groups.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-21-conversation-build-groups.i18n.yaml
@@ -15,8 +15,8 @@
en: d7cf1bd544f163ee
zh: 2433f3faa5f5abcb
/agent-note-definition-owned-grouping-with-generic-node-group-assembly/decision/related-decisions:
- en: d8307dcaf2a9ac72
- zh: 4852b1e2a4d09af0
+ en: 0889aeb088ed31b2
+ zh: 539f03253b48c59b
/agent-note-definition-owned-grouping-with-generic-node-group-assembly/definition-input-state-and-registration:
en: 9fb89bddbfd75627
zh: 950c319db123c637
diff --git a/.agents/notes/implemented/architecture/2026-09-21-conversation-build-groups.md b/.agents/notes/implemented/architecture/2026-09-21-conversation-build-groups.md
index bccb48b0e4..39a31ae902 100644
--- a/.agents/notes/implemented/architecture/2026-09-21-conversation-build-groups.md
+++ b/.agents/notes/implemented/architecture/2026-09-21-conversation-build-groups.md
@@ -35,7 +35,7 @@ The grouping foundation provides a separate, Node-input `ConversationGroupDefini
| Record | Relationship |
|---|---|
| [Business Node assembly](2026-08-09-client-conversation-node-assembly.md) | Retains event matching, Contexts, Locations, one business Node per Context, and target Builders; grouping adds another input category. |
-| [Chat scroll and footer](../bug-fix/2026-09-22-chat-scroll-follow-and-footer-geometry.md) | Owns clipping and independent nested following without changing Group Definition membership. |
+| [Chat scroll and footer](../../../../packages/client/ui-chat/README.md) | Owns clipping and independent nested following without changing Group Definition membership. |
Grouping preserves the independent Node-assembly decisions. Cross-View navigation and `toolCallFocus` remain separate responsibilities; Group references carry no View handles or resource-navigation policy.
diff --git a/.agents/notes/implemented/architecture/2026-09-21-conversation-build-groups.zh.md b/.agents/notes/implemented/architecture/2026-09-21-conversation-build-groups.zh.md
index 5d2ec9e684..b469ad12ca 100644
--- a/.agents/notes/implemented/architecture/2026-09-21-conversation-build-groups.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-21-conversation-build-groups.zh.md
@@ -35,7 +35,7 @@ Chat Builder 汇总整个目标的节点及索引。在其中固定创建 Chat
| 记录 | 关系 |
|---|---|
| [业务节点组装](2026-08-09-client-conversation-node-assembly.zh.md) | 保留事件匹配、Context、Location、每个 Context 一个业务 Node 及目标 Builder,分组增加另一种输入类别。 |
-| [Chat 滚动与页脚](../bug-fix/2026-09-22-chat-scroll-follow-and-footer-geometry.zh.md) | 负责裁剪与独立的嵌套跟随,不改变 Group Definition 成员关系。 |
+| [Chat 滚动与页脚](../../../../packages/client/ui-chat/README.zh.md) | 负责裁剪与独立的嵌套跟随,不改变 Group Definition 成员关系。 |
分组保留独立的节点组装决策。跨 View 导航与 `toolCallFocus` 仍是独立职责,Group 引用不携带 View 句柄或资源导航策略。
diff --git a/.agents/notes/implemented/architecture/2026-09-21-creator-skills-progressive-disclosure.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-21-creator-skills-progressive-disclosure.i18n.yaml
deleted file mode 100644
index 4a59755b21..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-21-creator-skills-progressive-disclosure.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-21-creator-skills-progressive-disclosure.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-21-creator-skills-progressive-disclosure.md
-/agent-note-creator-skills-disclose-progressively-and-may-read-sources:
- en: 374f4a41f3026e59
- zh: 4e7f90d18b03fd14
-/agent-note-creator-skills-disclose-progressively-and-may-read-sources/problem:
- en: 1808717550a5e7ba
- zh: e18dd5a813a2398d
-/agent-note-creator-skills-disclose-progressively-and-may-read-sources/decision:
- en: b83fb74f03ca75a7
- zh: e4f6b1c73ef96787
-/agent-note-creator-skills-disclose-progressively-and-may-read-sources/alternatives-considered:
- en: d36d4f610925060a
- zh: c0ed56b6c631789e
-/agent-note-creator-skills-disclose-progressively-and-may-read-sources/consequences:
- en: ed908afe74adf3bc
- zh: 62a3cfba359dfbb6
diff --git a/.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.i18n.yaml
index 55a0c25bdd..d2564b05ce 100644
--- a/.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.i18n.yaml
@@ -9,8 +9,8 @@
en: feb9d491ad4a9016
zh: 291f0e6c913b0b19
/agent-note-desktop-close-shortcuts-follow-page-focus/decision:
- en: c587d88575b6ce7e
- zh: 2287cf4d6d2e7587
+ en: f076fb4b2098899d
+ zh: 6f136e9d11714e02
/agent-note-desktop-close-shortcuts-follow-page-focus/alternatives-considered:
en: 10c7bb2fc87bdd16
zh: 796ecde8bcf09d34
diff --git a/.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.md b/.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.md
index dc97680644..ef53a03de7 100644
--- a/.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.md
+++ b/.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.md
@@ -14,7 +14,7 @@ The sidebar page owner resolves close against live focus and captures the target
The macOS File menu routes Close Page or Window through the same owner and displays the accepted single-key binding. Native closure requires the current configuration revision, a focused and enabled product window, and inactive shortcut recording. The existing window lifecycle applies without a shortcut-specific confirmation: macOS keeps the application alive after its last window closes; Windows and Linux quit and stop the Host.
-This decision owns File-menu and close behavior in place of the close-role choice in [the standard macOS menu note](../bug-fix/2026-09-16-desktop-window-menus.md). That note retains the rationale for native Window and application hide commands. [Shortcut preference persistence](2026-09-20-device-local-shortcut-preferences.md) owns accepted bindings and revision publication.
+This decision owns File-menu and close behavior; the [Desktop reference](../../../../apps/desktop/README.md) documents native Window and application hide commands. [Shortcut preference persistence](2026-09-20-device-local-shortcut-preferences.md) owns accepted bindings and revision publication.
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.zh.md b/.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.zh.md
index 6ef60c084f..298c493bb7 100644
--- a/.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.zh.md
@@ -14,7 +14,7 @@ Status: implemented
macOS File 菜单通过同一个 owner 路由“关闭页面或窗口”,并显示已接受的单键绑定。原生关闭要求配置 revision 仍为当前值、产品窗口已聚焦且启用,并且没有在录制快捷键。现有窗口生命周期直接生效,不增加快捷键专用确认:macOS 最后一个窗口关闭后保留应用;Windows 和 Linux 退出并停止 Host。
-本决策接管[标准 macOS 菜单记录](../bug-fix/2026-09-16-desktop-window-menus.zh.md)中 close role 选择所涉及的 File 菜单和关闭行为。该记录保留原生 Window 与应用隐藏命令的理由。[快捷键偏好持久化](2026-09-20-device-local-shortcut-preferences.zh.md)负责已接受绑定及 revision 发布。
+本决策负责 File 菜单与关闭行为;[Desktop 参考](../../../../apps/desktop/README.zh.md)说明原生 Window 与应用隐藏命令。[快捷键偏好持久化](2026-09-20-device-local-shortcut-preferences.zh.md)负责已接受绑定及 revision 发布。
## 考虑过的替代方案
diff --git a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml
index 521567dd7e..448629299d 100644
--- a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml
@@ -9,11 +9,11 @@
en: 2ba6c43b20a3d347
zh: 5647179ddaa38ad9
/agent-note-ship-experimental-capabilities-as-optional-bundles/decision:
- en: fdd54ede661f7a83
- zh: a41ed1a872bb8ba3
+ en: 6c8583bdefc5e36c
+ zh: 0928b4f2876bbfe4
/agent-note-ship-experimental-capabilities-as-optional-bundles/alternatives-considered:
- en: a65d84004e48b3ef
- zh: 17221f08e7a8a89b
+ en: d6f40bf02df37ab7
+ zh: e5b85ce9d41e8754
/agent-note-ship-experimental-capabilities-as-optional-bundles/consequences:
- en: e7fb2a93a89ba9b6
- zh: 37ed9f0a3ca44583
+ en: 38d27583533cd028
+ zh: 9bf11f5489c1aa43
diff --git a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md
index b7139f6d9a..dbdd104da0 100644
--- a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md
+++ b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md
@@ -10,9 +10,13 @@ The Web plugin page offered two optional bundles, Agent Teams and voice input. A
## Decision
-`OPTIONAL_BUNDLES` lists Agent Teams, voice input, Auto review, and the Schedule bundle. Every optional bundle declares `icon` and exports `./locale/*.json` with `meta.title` and `meta.description`, so the Official group renders a localized title, description, and image; `verify-default-product-isolation` rejects an optional bundle without them. The Inspector's `cordis.patch.yml` names the package instead of a built-artifact path, so the same file is the bundle patch and the `--patch` overlay for a built Web launch.
+`OPTIONAL_BUNDLES` owns the installation's default-off selections, including maintained optional capabilities and experimental capabilities. Every bundle declares `icon` and exports `./locale/*.json` with `meta.title` and `meta.description`; the Official group displays that metadata. A bundle's presentation does not determine its [experimental status](../../../../packages/experimental/README.md#status). The isolation check validates admission and still rejects experimental dependencies of nonexperimental bundles.
-An optional bundle is a runtime dependency of the installation, so its dependency graph is downloaded by every `dsh` install. The list therefore admits only packages whose graph is already in the installation's closure; Auto review adds nothing. The Inspector stays available through explicit installation and does not appear in the default plugin list. The browser-use and computer-use providers stay explicit compositions: Playwright MCP, Chrome DevTools MCP, and the native Cua Driver runtime binaries add about 85 MB and 21 packages to every install whether or not the bundle is enabled, and a shipped Cua Driver MCP switch would offer a capability whose executable the installation does not carry. Those provider packages keep locale display metadata for their component rows. Two more packages stay out for other reasons: `ptc-runtime-python` replaces the PTC runtime, and `workflow-ptc` rejects a non-TypeScript runtime at load while the Web presets carry `workflow-ptc` rows a bundle patch cannot reach; `browser-use-stagehand-native` requires a native model name and API key at schema validation with no configuration form on the page.
+An optional bundle's dependencies are downloaded with every `dsh` installation. [Reasoning translation](2026-10-05-anonymous-reasoning-translation.md) uses three experimental packages without adding third-party runtime dependencies. Admission therefore considers installation cost and whether a switch supplies a usable composition without an additional configuration form. Browser and computer providers remain explicit compositions because they add substantial runtimes or require an external executable. Stagehand requires model credentials; Python PTC replaces a core provider and conflicts with TypeScript workflow consumers. Demonstrative hooks, webhooks, and mods stay outside the list. [Session Inspector](../feature/2026-09-24-session-inspector.md) owns the Inspector's distinct activation behavior.
+
+Bundles insert ordinary Host rows; optional model tools register in the global tool layer, so every preset, including minimal, receives them. Plugin management, profile composition, and reload need no bundle-specific behavior. Search, Ralph, and terminals supply their own isolated services in a Host group. Search opens one in-memory index on demand; the Host search policy stays unchanged. Badge skills and title providers act on the Host. Switching a bundle registers or removes its tools for live Agents, and later user patch layers retain precedence. A replacement title provider may register once the previous provider's disposal starts; the previous provider's late results never commit.
+
+Claude Code and Codex install on demand through the separate `ON_DEMAND_BUNDLES` catalog; the [on-demand native-bundle decision](2026-10-05-official-on-demand-bundles.md) owns the cost, version, and offline-discovery trade-offs.
## Alternatives considered
@@ -20,6 +24,10 @@ An optional bundle is a runtime dependency of the installation, so its dependenc
**Mount the registration-only `computer-use` and `browser-use` services in `dsh-base`.** The shared composition would carry rows that only an optional provider bundle needs; a provider bundle can insert the service row from its own patch and dependencies.
+**Register preset extensions at runtime.** A separate registration path would need its own ordering, module resolution, generation updates, and inspection behavior. Profile patches keep one effective composition for boot, dumps, validation, and user overrides.
+
+**Insert rows into named presets with a `preset` patch operation.** Limiting tools to full presets required a second patch language across profile composition, reload, plugin management, and the plugin page, plus retained-generation removal checks. Global tools reach the same Agents through the existing Host composition.
+
## Consequences
-The Official group carries four optional-bundle entries, each tagged experimental by its experimental name. The runtime dependency closure of the installation is unchanged. A browser-use or computer-use provider still needs its Service Definition and the provider in a profile patch or composition.
+Official discovery does not imply experimental status or a different plugin lifecycle. Switching a bundle off keeps its installed files; failed activation retains the existing saved-selection behavior. Bundle rows are ordinary Host rows with their usual switches. Individual and combined composition tests cover the Host rows and every preset's tool catalog; live switching and recorded Sessions cover activation and model-visible behavior.
diff --git a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md
index af4048bc5e..6c9df974e8 100644
--- a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md
@@ -10,9 +10,13 @@ Web 插件页只提供两个可选 bundle:Agent Teams 与语音输入。Auto r
## 决策
-`OPTIONAL_BUNDLES` 列出 Agent Teams、语音输入、Auto review 与 Schedule bundle。每个可选 bundle 都声明 `icon` 并导出带 `meta.title` 与 `meta.description` 的 `./locale/*.json`,官方分组因此能渲染本地化标题、描述和图片;`verify-default-product-isolation` 会拒绝缺少这些声明的可选 bundle。Inspector 的 `cordis.patch.yml` 直接写包名而非构建产物路径,所以同一文件既是 bundle patch,也是已构建 Web 启动的 `--patch` overlay。
+`OPTIONAL_BUNDLES` 管理安装随附且默认关闭的选项,包括维护中的可选能力与实验性能力。每个 bundle 声明 `icon`,并导出带 `meta.title` 与 `meta.description` 的 `./locale/*.json`;官方分组显示这些元数据。Bundle 的展示方式不决定其[实验性状态](../../../../packages/experimental/README.zh.md#status)。隔离检查校验准入,并继续拒绝非实验 bundle 的实验性依赖。
-可选 bundle 是安装的运行时依赖,其依赖图会随每次 `dsh` 安装一起下载。因此名单只接纳依赖图已在安装闭包内的包;Auto review 不增加任何依赖。Inspector 保留显式安装方式,不在默认插件列表中显示。浏览器操作与电脑操作提供方仍保持显式组合:Playwright MCP、Chrome DevTools MCP 与原生 Cua Driver 运行时二进制会让每次安装多出约 85 MB、21 个包,无论 bundle 是否启用;而随附的 Cua Driver MCP 开关会提供一个安装本身并不携带其可执行文件的能力。这些提供方包保留 locale 显示元数据,供组件行使用。另有两个包因其他原因不纳入:`ptc-runtime-python` 会替换 PTC 运行时,而 `workflow-ptc` 在加载时拒绝非 TypeScript 运行时,且 Web preset 内含 bundle patch 无法触及的 `workflow-ptc` 行;`browser-use-stagehand-native` 在 schema 校验时就要求原生模型名称与 API 密钥,而插件页没有对应的配置表单。
+可选 bundle 的依赖随每次 `dsh` 安装下载。[思考过程翻译](2026-10-05-anonymous-reasoning-translation.zh.md)使用三个实验包,不增加第三方运行时依赖。因此准入需要考虑安装成本,以及开关能否在不增加配置表单的情况下提供可用组合。浏览器与电脑提供方仍使用显式组合,因为它们会增加较大的运行时或要求外部可执行文件。Stagehand 需要模型凭据;Python PTC 替换核心提供方,并与 TypeScript workflow 消费方冲突。演示性钩子、webhook 与模组不进入名单。[Session Inspector](../feature/2026-09-24-session-inspector.zh.md) 管理 Inspector 特有的激活行为。
+
+Bundle 插入普通 Host 行;可选模型工具注册到全局工具层,因此包括 minimal 在内的所有预设都会获得它们。插件管理、profile 组合与重载无需针对 bundle 的特殊行为。搜索、Ralph 与终端在 Host 组内提供自己的隔离服务。搜索按需打开一个内存索引;Host 搜索策略保持不变。徽章技能与标题提供方作用于 Host。切换 bundle 会为运行中的 Agent 注册或移除其工具,后续用户 patch 层继续优先。前一个标题提供方开始清理后,替代提供方即可注册;前一个提供方的迟到结果不会提交。
+
+Claude Code 与 Codex 通过独立的 `ON_DEMAND_BUNDLES` 目录按需安装,其成本、版本与离线发现取舍由[按需原生 bundle 决策](2026-10-05-official-on-demand-bundles.zh.md)管理。
## 考虑过的替代方案
@@ -20,6 +24,10 @@ Web 插件页只提供两个可选 bundle:Agent Teams 与语音输入。Auto r
**在 `dsh-base` 中挂载仅负责注册的 `computer-use` 与 `browser-use` 服务。** 共享组合将携带只有可选提供方 bundle 才需要的行;提供方 bundle 可以从自己的 patch 和依赖插入服务行。
+**在运行时注册预设扩展。** 独立注册路径需要自己的排序、模块解析、代际更新与检查行为。Profile patch 为启动、配置导出、校验和用户覆盖保留同一个有效组合。
+
+**用 `preset` patch 操作向指定预设插入行。** 只把工具限定在完整预设中,需要在 profile 组合、重载、插件管理和插件页中引入第二套 patch 语言,并为保留代际增加删除检查。全局工具通过现有 Host 组合即可到达同样的 Agent。
+
## 后果
-官方分组包含四个可选 bundle 条目,每项都因实验包名而带实验性标签。安装的运行时依赖闭包保持不变。浏览器操作或电脑操作提供方仍需在 profile patch 或组合中同时挂载其 Service Definition 与提供方。
+官方发现不表示实验性状态,也不引入不同的插件生命周期。关闭 bundle 保留已安装文件;激活失败保留现有的已保存选择行为。Bundle 行是普通 Host 行,使用常规开关。独立与组合测试覆盖 Host 行及每个预设的工具目录;实时切换与已记录 Session 覆盖激活和模型可见行为。
diff --git a/.agents/notes/implemented/architecture/2026-09-21-live-config-inspect-provider.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-21-live-config-inspect-provider.i18n.yaml
deleted file mode 100644
index a9e885fe52..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-21-live-config-inspect-provider.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-21-live-config-inspect-provider.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-21-live-config-inspect-provider.md
-/agent-note-live-plugin-config-inspection-replaces-the-host-builtin-provider:
- en: f57edbb8b3fa60ae
- zh: 8d0fa0eb73bbdaf1
-/agent-note-live-plugin-config-inspection-replaces-the-host-builtin-provider/problem:
- en: e01cc31ce200b24b
- zh: 7462a52c3e59ec87
-/agent-note-live-plugin-config-inspection-replaces-the-host-builtin-provider/decision:
- en: a7d21366030c31ac
- zh: 047299df13e504b7
-/agent-note-live-plugin-config-inspection-replaces-the-host-builtin-provider/alternatives-considered:
- en: a19dba57a4108fc6
- zh: 2e64893e8bcf6513
-/agent-note-live-plugin-config-inspection-replaces-the-host-builtin-provider/consequences:
- en: efa454977043c76d
- zh: d6bf61ac47310d3e
diff --git a/.agents/notes/implemented/architecture/2026-09-22-fatal-diagnostics-and-crash-reports.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-22-fatal-diagnostics-and-crash-reports.i18n.yaml
index 567f3220b2..fae09a79ee 100644
--- a/.agents/notes/implemented/architecture/2026-09-22-fatal-diagnostics-and-crash-reports.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-22-fatal-diagnostics-and-crash-reports.i18n.yaml
@@ -12,8 +12,8 @@
en: ac461e0ea1702ecc
zh: 745def8581dc7974
/agent-note-fatal-diagnostics-and-desktop-crash-reports/relationship-to-earlier-decisions:
- en: 157e52fc4036044a
- zh: e474d9c9a36f1381
+ en: eced414858f08ef9
+ zh: 40639593b0d5bb6f
/agent-note-fatal-diagnostics-and-desktop-crash-reports/relationship-to-the-desktop-logging-design:
en: 7dc438912d10574a
zh: 07d05f9f17043d67
diff --git a/.agents/notes/implemented/architecture/2026-09-22-fatal-diagnostics-and-crash-reports.md b/.agents/notes/implemented/architecture/2026-09-22-fatal-diagnostics-and-crash-reports.md
index 01cbb24fc8..0cf5f64b89 100644
--- a/.agents/notes/implemented/architecture/2026-09-22-fatal-diagnostics-and-crash-reports.md
+++ b/.agents/notes/implemented/architecture/2026-09-22-fatal-diagnostics-and-crash-reports.md
@@ -28,7 +28,7 @@ Four decisions ship together.
## Relationship to earlier decisions
-This partially supersedes the [native Desktop fatal recovery](2026-09-15-desktop-native-fatal-recovery.md) note: the crash report file, not the Electron console, is now the record the dialog points to, and the dialog detail carries the report-path line; that note's dialog ownership, button set, and profile recovery remain active. The `uncaughtException` handling extends the release mechanism of the [fail-loud release](../bug-fix/2026-07-31-fail-loud-releases-the-terminal.md) note to synchronous throws.
+This partially supersedes the [native Desktop fatal recovery](2026-09-15-desktop-native-fatal-recovery.md) note: the crash report file, not the Electron console, is now the record the dialog points to, and the dialog detail carries the report-path line; that note's dialog ownership, button set, and profile recovery remain active. The `uncaughtException` handling extends the release mechanism of the [fail-loud release](../../../../packages/boot/app-boot/README.md) note to synchronous throws.
## Relationship to the Desktop logging design
diff --git a/.agents/notes/implemented/architecture/2026-09-22-fatal-diagnostics-and-crash-reports.zh.md b/.agents/notes/implemented/architecture/2026-09-22-fatal-diagnostics-and-crash-reports.zh.md
index 38f6a6802b..c3513bbddd 100644
--- a/.agents/notes/implemented/architecture/2026-09-22-fatal-diagnostics-and-crash-reports.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-22-fatal-diagnostics-and-crash-reports.zh.md
@@ -28,7 +28,7 @@ macOS 上,启动审计把一组 Web 客户端行列为 `import failed (see con
## 与既有决定的关系
-本 note 部分取代 [Desktop 原生致命恢复](2026-09-15-desktop-native-fatal-recovery.zh.md):对话框指向的记录现在是崩溃报告文件而非 Electron 控制台,对话框详情多出报告路径一行;该 note 对对话框归属、按钮集合与 profile 恢复的决定仍然有效。`uncaughtException` 的处理把 [fail-loud release](../bug-fix/2026-07-31-fail-loud-releases-the-terminal.zh.md) note 的 release 机制扩展到同步抛出。
+本 note 部分取代 [Desktop 原生致命恢复](2026-09-15-desktop-native-fatal-recovery.zh.md):对话框指向的记录现在是崩溃报告文件而非 Electron 控制台,对话框详情多出报告路径一行;该 note 对对话框归属、按钮集合与 profile 恢复的决定仍然有效。`uncaughtException` 的处理把 [fail-loud release](../../../../packages/boot/app-boot/README.zh.md) note 的 release 机制扩展到同步抛出。
## 与 Desktop 日志体系设计的关系
diff --git a/.agents/notes/implemented/architecture/2026-09-22-tool-call-three-phases.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-22-tool-call-three-phases.i18n.yaml
index 48da0f768d..c1523d3b70 100644
--- a/.agents/notes/implemented/architecture/2026-09-22-tool-call-three-phases.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-22-tool-call-three-phases.i18n.yaml
@@ -12,17 +12,17 @@
en: a15c7c13f167e5ac
zh: 63bcc2ef60c2f344
/agent-note-tool-calls-own-preparation-dispatch-and-results-by-callid/decision/one-tool-owns-three-stages:
- en: ea65da88c3df35bb
- zh: a71afb4683a75290
+ en: c4bac1088a4fd3bc
+ zh: 549348deae5485c1
/agent-note-tool-calls-own-preparation-dispatch-and-results-by-callid/decision/start-marks-an-initialization-candidate:
- en: 696a180246e23391
- zh: aeb2b1fdcfdf9631
+ en: 7771032e6ad227e7
+ zh: 313d32ab5312d965
/agent-note-tool-calls-own-preparation-dispatch-and-results-by-callid/decision/converge-on-final-state-without-replaying-preparation:
en: 603cbcf3379ad4f7
zh: e34d87ea348c756b
/agent-note-tool-calls-own-preparation-dispatch-and-results-by-callid/decision/shared-rows-reuse-their-existing-presentation:
- en: 24536588cd0c979b
- zh: 992d4c4657d6d034
+ en: a5741c967212250b
+ zh: de6288916a054c6e
/agent-note-tool-calls-own-preparation-dispatch-and-results-by-callid/decision/relationship-to-existing-records:
en: bd4984f66c507785
zh: 4d2372fdbc4c3cec
@@ -30,5 +30,5 @@
en: 505095bec889700e
zh: b77c492de3e8641a
/agent-note-tool-calls-own-preparation-dispatch-and-results-by-callid/consequences:
- en: 3cf36b54240013da
- zh: 67e663b431bc67d2
+ en: dd0e4561c50b1188
+ zh: 0ec83177ef570a78
diff --git a/.agents/notes/implemented/architecture/2026-09-22-tool-call-three-phases.md b/.agents/notes/implemented/architecture/2026-09-22-tool-call-three-phases.md
index f43eaf1a68..a99545b58b 100644
--- a/.agents/notes/implemented/architecture/2026-09-22-tool-call-three-phases.md
+++ b/.agents/notes/implemented/architecture/2026-09-22-tool-call-three-phases.md
@@ -18,7 +18,7 @@ The [Tool Definition](../../../../packages/client/ui-chat/src/client/conversatio
| Stage | Creation or update evidence | Available data and presentation |
|---|---|---|
-| `preparing` | A live delta with a call ID and tool name | Call identity, name, and time; no complete arguments, and one non-expandable row. An opt-in Hook can read the raw argument prefix. |
+| `preparing` | A live delta with a call ID and tool name | Call identity, name, time, and a lazy partial-argument view; one non-expandable row. |
| `start` | `tool/call` | Complete arguments; enables the tool's existing call presentation. |
| `result` | `tool/result` | The result and any complete arguments paired within the loaded window. |
@@ -28,7 +28,7 @@ An argument block ending does not trigger dispatch presentation. Showing prepara
The [Conversation Definition](../../../../packages/client/ui-conversation/src/client/contract/conversation.ts) retains its match/start/update interface and existing roles. Durable events and transient deltas may both match as start. The earliest start in the current event sequence initializes State; every later Match, including another start for the same ID, goes through update.
-A named tool delta and `tool/call` are both initialization candidates. A live delta can create preparation; a historical `tool/call` creates the dispatched stage directly. Matching still reads only the current event, without Context lookup, additional roles, registries, or separate caches.
+Every tool delta and `tool/call` are initialization candidates. A named live delta creates the preparing row; a historical `tool/call` creates the dispatched stage directly. Matching still reads only the current event, without Context lookup, additional roles, registries, or separate caches.
Successful stream settlement publishes the final Assistant message while retaining transient deltas until `step/end`. Each `tool/call` updates its existing callId; the Assistant keeps its live ordering anchor until the same cleanup. Failed, interrupted, or abandoned streams retire their deltas immediately. On retirement the Assembler reselects starts and recomputes State from remaining Matches. Undispatched preparations hide without synthetic execution results.
@@ -40,9 +40,9 @@ Groups include Tool nodes from preparation onward and classify and count them by
### Shared rows reuse their existing presentation
-[ToolRow](../../../../packages/client/ui-tool/src/client/tool/components/ToolRow.tsx) reuses the existing icon, title, and row component across all stages. The shared row model selects the tool title and combines any generic tool-name prefix with the available argument summary. Preparation has no complete arguments or result, and the shared argument parser returns null without parsing JSON. The existing slot Hook binding optionally exposes a call-scoped raw prefix; it adds no preparation registration mechanism. ToolRow prevents preparation rows from expanding.
+[ToolRow](../../../../packages/client/ui-tool/src/client/tool/components/ToolRow.tsx) reuses the existing icon, title, and row component across all stages. The shared row model selects the tool title and combines any generic tool-name prefix with the available argument summary. All stages expose the [lazy argument view](2026-09-24-preparing-tool-arguments.md); cards requiring dispatched arguments still return no call during preparation. ToolRow prevents preparation rows from expanding.
-Read, read_image, write/edit, search, web, todo, question, details, and the generic fallback use this path. Custom rows such as Bash, Skill, Present, and Cordis keep their own preparation branches. No automatic/custom registration declaration or second tool-classification list in ToolTree is introduced.
+Read, read_image, write/edit, search, web, todo, question, details, and the generic fallback use this path. Bash shares its component across stages; Skill, Present, and Cordis retain their own preparation branches. No automatic/custom registration declaration or second tool-classification list in ToolTree is introduced.
### Relationship to existing records
@@ -60,8 +60,8 @@ This decision replaces the [business-node assembly](2026-08-09-client-conversati
## Consequences
-- A tool may appear before its arguments are complete, but preparation neither implies execution nor exposes interactions requiring arguments.
+- A tool may appear before its arguments are complete, but preparation never implies execution. Interactions use only fields already available.
- Multiple starts with one ID belong to one lifecycle. Independent calls must use distinct IDs; a second-start error no longer detects identity reuse.
-- The shared row primitive is reused across stages. Write/edit separate preparing and dispatched components so only the former subscribes to raw arguments; custom rows may also switch internal components by stage.
-- Partial JSON parsing and `useToolCallDelta` are not provided. `useToolCallArgumentsPartial` exposes the existing Step's raw prefix without a second accumulator; write/edit use only its length for preparation progress.
+- The shared row primitive is reused across stages. Write/edit and Bash keep their component identity; custom rows may switch internal components by stage.
+- Partial-argument access and its update policy follow the lazy argument view; no separate raw-prefix Hook is required.
- Assembly tests separately cover live preparation, withdrawal, active-stream reconstruction, durable history, and paged convergence. Component tests cover argument-free presentation and stage changes; a recorded session covers browser preparation and reload results.
diff --git a/.agents/notes/implemented/architecture/2026-09-22-tool-call-three-phases.zh.md b/.agents/notes/implemented/architecture/2026-09-22-tool-call-three-phases.zh.md
index accbd2e966..9bea117dd3 100644
--- a/.agents/notes/implemented/architecture/2026-09-22-tool-call-three-phases.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-22-tool-call-three-phases.zh.md
@@ -18,7 +18,7 @@ Status: implemented
| 阶段 | 创建或更新依据 | 可用数据与展示 |
|---|---|---|
-| `preparing` | 带调用 ID 和工具名的实时 delta | 调用身份、名称和时间;不提供完整参数,只显示不可展开的一行。可选钩子可读取原始参数前缀。 |
+| `preparing` | 带调用 ID 和工具名的实时 delta | 调用身份、名称、时间和懒计算的部分参数视图;显示不可展开的一行。 |
| `start` | `tool/call` | 完整参数;启用工具原有的调用展示。 |
| `result` | `tool/result` | 调用结果,以及当前窗口内可配对的完整参数。 |
@@ -28,7 +28,7 @@ Status: implemented
沿用 [Conversation Definition](../../../../packages/client/ui-conversation/src/client/contract/conversation.ts) 的 `match/start/update` 接口和既有角色。持久事件与瞬态 delta 都可以匹配为 start;当前事件序列中最早的 start 初始化 State,后续 Match,包括同 ID 的其他 start,都交给 update。
-Tool 的具名 delta 和 `tool/call` 都是创建入口。实时先收到 delta,就从 preparing 创建;历史先收到 `tool/call`,就从 start 创建。匹配函数仍然只读取当前事件,不查询 Context,不增加角色、注册表或旁路缓存。
+Tool 的每个 delta 和 `tool/call` 都是创建入口。具名实时 delta 创建准备行;历史 `tool/call` 直接创建派发阶段。匹配函数仍然只读取当前事件,不查询 Context,不增加角色、注册表或旁路缓存。
成功流结束时发布最终 Assistant 消息,瞬态 delta 保留到 `step/end`。每个 `tool/call` 更新已有 callId;Assistant 的实时排序锚点也保留到同一次清理。失败、中断或废弃的流立即移除 delta。清理时,Assembler 从剩余 Match 重新选择 start 并重算 State;未派发的准备节点隐藏,不合成执行结果。
@@ -40,9 +40,9 @@ Group 从准备阶段起就聚合 Tool 节点,按工具名分类和计数。As
### 通用行复用现有外壳
-[ToolRow](../../../../packages/client/ui-tool/src/client/tool/components/ToolRow.tsx) 在三个阶段复用既有图标、标题和行组件。公共行模型选择工具标题,并组合通用工具名前缀与已有参数摘要。准备阶段没有完整参数或结果,共享参数解析入口直接返回 null,不解析 JSON。既有 slot 钩子绑定可选地提供当前调用的原始前缀,不增加准备态注册机制;ToolRow 禁止准备行展开。
+[ToolRow](../../../../packages/client/ui-tool/src/client/tool/components/ToolRow.tsx) 在三个阶段复用既有图标、标题和行组件。公共行模型选择工具标题,并组合通用工具名前缀与已有参数摘要。所有阶段都提供[懒计算参数视图](2026-09-24-preparing-tool-arguments.zh.md);需要完整派发参数的卡片在准备阶段仍返回无调用。ToolRow 禁止准备行展开。
-read、read_image、write/edit、search、web、todo、question、details 和通用回退使用这条路径。Bash、Skill、Present、Cordis 等自定义外壳保留自己的准备态分支。不增加自动/自管注册声明,不在 ToolTree 维护另一份工具分类名单。
+read、read_image、write/edit、search、web、todo、question、details 和通用回退使用这条路径。Bash 在各阶段共用组件;Skill、Present、Cordis 保留自己的准备态分支。不增加自动/自管注册声明,不在 ToolTree 维护另一份工具分类名单。
### 与已有记录的关系
@@ -60,8 +60,8 @@ read、read_image、write/edit、search、web、todo、question、details 和通
## 影响
-- 工具可以在参数完整前出现,但准备展示不表示工具已经执行,也不暴露依赖参数的交互。
+- 工具可以在参数完整前出现,但准备展示不表示工具已经执行;交互只使用已可用的字段。
- 同一 ID 的多个 start 被视为同一个生命周期;业务必须为独立调用提供不同 ID,不能再依赖第二个 start 报错检测身份复用。
-- 各阶段复用通用行基础组件。write/edit 分离准备态与派发后组件,只有前者订阅原始参数;自定义外壳也可按阶段切换内部组件。
-- 不提供部分 JSON 解析或 `useToolCallDelta`。`useToolCallArgumentsPartial` 暴露既有 Step 的原始前缀,不另建累积器;write/edit 只使用其长度展示准备进度。
+- 各阶段复用通用行基础组件。write/edit 和 Bash 保持组件身份;自定义外壳可按阶段切换内部组件。
+- 部分参数的读取与更新策略遵循懒计算参数视图,不需要独立的原始前缀钩子。
- 组装测试分别覆盖实时准备态、撤回、活跃流重建、持久历史与分页收敛;组件测试检查无参数展示和阶段切换,录制会话覆盖浏览器中的准备态与重载结果。
diff --git a/.agents/notes/implemented/architecture/2026-09-23-best-effort-persistent-ptc.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-23-best-effort-persistent-ptc.i18n.yaml
new file mode 100644
index 0000000000..615a8d43cf
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-23-best-effort-persistent-ptc.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-09-23-best-effort-persistent-ptc.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-23-best-effort-persistent-ptc.md
+/agent-note-node-ptc-execution-modes-and-in-memory-state:
+ en: 078a89753b06cd6a
+ zh: 4004e5e4bb42f2cc
+/agent-note-node-ptc-execution-modes-and-in-memory-state/problem:
+ en: e42db40b879fdbf4
+ zh: 6017da9c4da17835
+/agent-note-node-ptc-execution-modes-and-in-memory-state/decision:
+ en: d2d08c54c6520f02
+ zh: 520a9b3421ff5c65
+/agent-note-node-ptc-execution-modes-and-in-memory-state/alternatives-considered:
+ en: 396157a9fc293198
+ zh: a061f05b4423e26f
+/agent-note-node-ptc-execution-modes-and-in-memory-state/consequences:
+ en: 9b9172f4aafe907f
+ zh: 022a6bd52d7caf05
diff --git a/.agents/notes/implemented/architecture/2026-09-23-best-effort-persistent-ptc.md b/.agents/notes/implemented/architecture/2026-09-23-best-effort-persistent-ptc.md
new file mode 100644
index 0000000000..153a63b87e
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-23-best-effort-persistent-ptc.md
@@ -0,0 +1,37 @@
+# Agent Note: Node PTC execution modes and in-memory state
+
+Status: implemented
+
+English | [中文](2026-09-23-best-effort-persistent-ptc.zh.md)
+
+## Problem
+
+Interactive automation retains application objects, imported modules and helper functions between model calls. Recreating every object for every program prevents consumers from using ordinary JavaScript references across observations and actions.
+
+## Decision
+
+The existing `@deepseek-ai/dsh-ptc-runtime-node` plugin selects its execution mode through `executionMode`. The default `per-call` mode starts a fresh process under the configured file policy. Opt-in `persistent-in-memory` mode keeps a Node REPL process per explicit, object-identity state owner. Both modes provide the same `ctx.ptcRuntime` service. PTC uses the live Agent as owner and awaits release during its context disposal. Changing the configuration reloads the provider and releases its processes. The [sandboxed Node decision](2026-09-11-sandboxed-node-ptc-runtime.md) owns per-call confinement; the [PTC foundation](../feature/2026-06-15-ptc.md) owns tool dispatch and logged model requests.
+
+Mode names describe state retention rather than a process implementation or source syntax. `per-call` retains state for one invocation; `persistent-in-memory` retains it across invocations until the process or owner ends. `durable` names the deferred ability to save and restore execution state across restarts. It is not an accepted configuration value: a future implementation must define its recovery point, recoverable values and external-resource handling before exposing that mode. The [retained-process permission decision](2026-10-08-persistent-ptc-sandbox.md) owns confinement, policy changes and temporary escalation.
+
+One npm package and one plugin entry keep existing installation and loading paths stable. Communication, binding proxies, JSON encoding and output limits remain package-local modules. The Node provider delegates retained process ownership to an internal execution object, which registers no additional Cordis service. Per-call and in-memory execution keep their distinct lifecycle rules. `programFormat` describes source evaluation independently of retention: a REPL need not retain state. The runtime owns syntax, completion and retention instructions through `executionInstructions`; tools own call arguments, SDK declarations and image attachment. Workflow uses `programFormat` to wrap its program and refuses restricted file policies when the selected runtime cannot enforce them.
+
+The Session records submitted programs, nested tool calls and returned output. It does not serialize the REPL heap or rerun earlier programs after restart, resume or fork: replaying automation could repeat external side effects. A missing variable is a normal program error, and the model-visible instructions require recreating variables and reacquiring resources. No environment identifier or new durable Session event is required because the provider exposes no unlogged model input.
+
+Cancellation, deadline expiry, protocol or output-limit failure, owner disposal and process exit discard the environment. Ordinary exceptions retain earlier mutations. A retained function uses the active evaluation's host bindings; asynchronous work from an earlier evaluation cannot acquire a later evaluation's tool authority or output sink.
+
+## Alternatives considered
+
+Separate npm providers plus a shared library allow independent distribution, at the cost of another package, dependency declarations and cross-package exports for shared implementation details. The two Node modes have no current need for separate release schedules or independent dependencies. A second plugin subpath permits separate loading but asks deployments to replace plugin entries to select an execution policy. A configuration field expresses that selection while preserving the existing entry. Independent distribution or substantially different dependencies can justify revisiting this choice.
+
+A new Cordis service for common helpers would require another registration and injected dependency without adding an independently replaceable runtime capability. Internal functions and classes provide the required reuse. A common execution superclass would also couple fresh-process cleanup with owner-retained lifetimes; the provider shares mechanisms without merging those lifecycles.
+
+Serializing arbitrary JavaScript objects cannot preserve native resources and closures. Replaying prior programs can repeat writes and input actions. Both would imply recovery guarantees that in-memory execution does not offer.
+
+A realm inside the Harness process shares process lifetime with model code and cannot terminate a synchronous loop independently. A managed child process permits cancellation and resource cleanup while retaining imports and variables during healthy use. Process ownership alone does not provide a file sandbox or multi-tenant isolation.
+
+## Consequences
+
+Selecting `persistent-in-memory` retains execution state; the [permission decision](2026-10-08-persistent-ptc-sandbox.md) defines its file restrictions. Nested tools keep their normal authorization and logging. Memory and native handles may disappear without a Harness restart. The model must recover from reference or resource errors; the runtime never retries a program automatically.
+
+An idle owner retains a process with no expiration timer. Callers release owners explicitly; ownerless service calls use temporary processes. Programs use REPL statements and a final expression rather than top-level return, and calls for one retained owner cannot overlap. Tool schemas and program guidance follow the selected mode.
diff --git a/.agents/notes/implemented/architecture/2026-09-23-best-effort-persistent-ptc.zh.md b/.agents/notes/implemented/architecture/2026-09-23-best-effort-persistent-ptc.zh.md
new file mode 100644
index 0000000000..a4c4a52828
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-23-best-effort-persistent-ptc.zh.md
@@ -0,0 +1,37 @@
+# Agent Note: Node PTC 执行模式与内存状态
+
+Status: implemented
+
+[English](2026-09-23-best-effort-persistent-ptc.md) | 中文
+
+## 问题
+
+交互式自动化需要在模型调用之间保留应用对象、导入模块和辅助函数。如果每次执行都重新创建对象,消费者就无法在观察与操作之间继续使用普通 JavaScript 引用。
+
+## 决策
+
+现有 `@deepseek-ai/dsh-ptc-runtime-node` 插件通过 `executionMode` 选择执行模式。默认的 `per-call` 模式按配置的文件策略启动新进程。显式启用的 `persistent-in-memory` 模式为每个以对象身份标识的状态所有者保留一个 Node REPL 进程。两种模式都提供同一个 `ctx.ptcRuntime` 服务。PTC 使用存活的 Agent 作为所有者,在其上下文销毁时等待释放。修改配置会重载提供方并释放其进程。[Node 沙箱决策](2026-09-11-sandboxed-node-ptc-runtime.zh.md)负责逐次执行的文件约束,[PTC 基础决策](../feature/2026-06-15-ptc.zh.md)负责工具分派与模型请求日志。
+
+模式名称描述状态保留方式,不绑定进程实现或代码语法。`per-call` 只为一次调用保留状态;`persistent-in-memory` 跨调用保留状态,直到进程或所有者结束。`durable` 用于命名未来跨重启保存与恢复执行状态的能力。它不是可接受的配置值:实现必须先明确恢复点、可恢复的值以及外部资源的处理方式,才能公开这个模式。[常驻进程权限决策](2026-10-08-persistent-ptc-sandbox.zh.md)负责文件约束、权限切换与临时提权。
+
+一个 npm 包和一个插件入口保持现有安装与加载路径稳定。通信、绑定代理、JSON 编码和输出限制继续作为包内模块复用。Node 提供方将保留进程的所有权交给内部执行对象,该对象不注册额外的 Cordis 服务。逐次执行和内存保留各自管理不同的生命周期。`programFormat` 描述代码求值形式,与状态保留独立:REPL 不必跨调用保留状态。运行时通过 `executionInstructions` 负责语法、完成值和状态保留指引;工具层负责调用参数、SDK 声明和图片附加。工作流根据 `programFormat` 包装程序;所选运行时无法执行受限文件策略时,工作流拒绝执行。
+
+Session 记录提交的程序、嵌套工具调用和返回输出。它不序列化 REPL 堆,也不在重启、恢复或派生后重新执行旧程序,因为重放自动化可能重复外部操作。缺失变量产生普通程序错误,模型指引要求重新创建变量并获取资源。提供方没有引入日志之外的模型输入,因此不需要环境标识或新的持久 Session 事件。
+
+取消、截止时间到期、协议失败、输出超限、所有者销毁和进程退出都会丢弃环境。普通异常保留此前发生的修改。保留的函数使用当前执行的宿主绑定;较早执行中的异步任务不能获取后续执行的工具权限或向其输出。
+
+## 考虑过的替代方案
+
+将提供方拆成独立 npm 包并增加共享库,可以分别分发,但需要维护额外的包、依赖声明和共享实现的跨包导出。两种 Node 模式目前没有不同发布节奏或独立依赖的需求。第二个插件子路径允许独立加载,但要求部署配置通过替换插件入口来选择执行策略。配置字段直接表达这个选择,并保留已有入口。需要独立分发或出现明显不同的依赖时,可以重新评估拆包。
+
+为公共辅助代码新增 Cordis 服务,会增加注册和注入依赖,却没有提供独立替换的运行能力。内部函数和类已经能够完成复用。共同的执行基类还会把新进程清理与按所有者保留进程的生命周期耦合起来,因此提供方共用处理机制,各自管理生命周期。
+
+序列化任意 JavaScript 对象无法保留原生资源和闭包。重放旧程序可能重复写入和输入操作。这两种做法都会暗示内存保留模式没有提供的恢复保证。
+
+Harness 进程内的独立执行上下文与模型代码共享进程生命周期,无法单独终止同步死循环。托管子进程可以在正常使用时保留导入和变量,同时支持取消与资源清理。仅有进程所有权不能提供文件沙箱或多租户隔离。
+
+## 影响
+
+选择 `persistent-in-memory` 会保留执行状态;[权限决策](2026-10-08-persistent-ptc-sandbox.zh.md)定义其文件限制。嵌套工具仍遵循原有授权和日志规则。即使 Harness 没有重启,内存和原生句柄也可能丢失。模型需要从引用或资源错误中恢复,运行时不会自动重试程序。
+
+空闲所有者保留一个没有过期计时器的进程。调用方显式释放所有者,没有所有者的服务调用使用临时进程。程序使用 REPL 语句和末尾表达式,不支持顶层 return;同一保留所有者的调用不能重叠。工具 schema 和程序指引跟随所选模式。
diff --git a/.agents/notes/implemented/architecture/2026-09-23-desktop-close-to-background-and-quit-confirmation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-23-desktop-close-to-background-and-quit-confirmation.i18n.yaml
index 28b2efa813..11a37ac266 100644
--- a/.agents/notes/implemented/architecture/2026-09-23-desktop-close-to-background-and-quit-confirmation.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-23-desktop-close-to-background-and-quit-confirmation.i18n.yaml
@@ -3,8 +3,8 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-23-desktop-close-to-background-and-quit-confirmation.md
/agent-note-hide-the-desktop-window-on-close-and-confirm-interruptible-quits:
- en: 9e029938c58bf143
- zh: f60f9cf2067a02ee
+ en: 92bb54cd9fe6b168
+ zh: b0952405c6066416
/agent-note-hide-the-desktop-window-on-close-and-confirm-interruptible-quits/problem:
en: 782e934449eae28c
zh: 0941465dff3a26d9
diff --git a/.agents/notes/implemented/architecture/2026-09-23-desktop-close-to-background-and-quit-confirmation.md b/.agents/notes/implemented/architecture/2026-09-23-desktop-close-to-background-and-quit-confirmation.md
index 67c7bf92e0..937cff4f32 100644
--- a/.agents/notes/implemented/architecture/2026-09-23-desktop-close-to-background-and-quit-confirmation.md
+++ b/.agents/notes/implemented/architecture/2026-09-23-desktop-close-to-background-and-quit-confirmation.md
@@ -4,7 +4,7 @@ Status: implemented
English | [中文](2026-09-23-desktop-close-to-background-and-quit-confirmation.zh.md)
-Window timing and shutdown ownership otherwise follow [showing the window before the Host starts](2026-09-09-desktop-immediate-window-and-direct-start.md); the task rule reuses the [update restart check](2026-08-25-electron-desktop-packaging-and-updates.md). This partially supersedes the [standard macOS window menus](../bug-fix/2026-09-16-desktop-window-menus.md) decision: ⌘W no longer destroys the window through Electron's role, and Dock activation reopens the hidden window when no window is visible instead of only when none exists. That note's menu declarations stay in force.
+Window timing and shutdown ownership otherwise follow [showing the window before the Host starts](../../../../apps/desktop/README.md); the task rule reuses the [update restart check](2026-08-25-electron-desktop-packaging-and-updates.md). ⌘W does not destroy the window through Electron's role, and Dock activation reopens the hidden window when no window is visible instead of only when none exists. The native menu declarations in the [Desktop reference](../../../../apps/desktop/README.md) stay in force.
## Problem
diff --git a/.agents/notes/implemented/architecture/2026-09-23-desktop-close-to-background-and-quit-confirmation.zh.md b/.agents/notes/implemented/architecture/2026-09-23-desktop-close-to-background-and-quit-confirmation.zh.md
index e5a59bcd0b..02633e6e04 100644
--- a/.agents/notes/implemented/architecture/2026-09-23-desktop-close-to-background-and-quit-confirmation.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-23-desktop-close-to-background-and-quit-confirmation.zh.md
@@ -4,7 +4,7 @@ Status: implemented
[English](2026-09-23-desktop-close-to-background-and-quit-confirmation.md) | 中文
-窗口时序和关闭归属仍遵循[在 Host 启动前显示窗口](2026-09-09-desktop-immediate-window-and-direct-start.zh.md);任务判定复用[更新重启检查](2026-08-25-electron-desktop-packaging-and-updates.zh.md)。本记录部分取代[标准 macOS 窗口菜单](../bug-fix/2026-09-16-desktop-window-menus.zh.md)决策:⌘W 不再通过 Electron 的 role 销毁窗口,Dock 激活在没有可见窗口时重新打开隐藏窗口,而不再只在没有任何窗口时才创建。该记录中的菜单声明仍然有效。
+窗口时序和关闭归属仍遵循[在 Host 启动前显示窗口](../../../../apps/desktop/README.zh.md);任务判定复用[更新重启检查](2026-08-25-electron-desktop-packaging-and-updates.zh.md)。⌘W 不通过 Electron 的 role 销毁窗口,Dock 激活在没有可见窗口时重新打开隐藏窗口,而不再只在没有任何窗口时才创建。[Desktop 参考](../../../../apps/desktop/README.zh.md)中的原生菜单声明仍然有效。
## 问题
diff --git a/.agents/notes/implemented/architecture/2026-09-23-native-https-web-listener.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-23-native-https-web-listener.i18n.yaml
new file mode 100644
index 0000000000..0aad921cce
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-23-native-https-web-listener.i18n.yaml
@@ -0,0 +1,22 @@
+# Bilingual-pair consistency record for 2026-09-23-native-https-web-listener.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-23-native-https-web-listener.md
+/agent-note-native-https-on-the-web-listener:
+ en: bdb6e7794af113f0
+ zh: b9f325b0cbe8d34d
+/agent-note-native-https-on-the-web-listener/problem:
+ en: 685a00e6f84f642d
+ zh: 404e463f56ec5a1f
+/agent-note-native-https-on-the-web-listener/decision:
+ en: 7f9ea8a4f2577d2f
+ zh: e9b2a1fc6944428e
+/agent-note-native-https-on-the-web-listener/alternatives-considered:
+ en: 44c051936dd2529e
+ zh: 7588669ec4c302ef
+/agent-note-native-https-on-the-web-listener/consequences:
+ en: 6a8be07b24f957ba
+ zh: fbf48476e092dfc8
+/agent-note-native-https-on-the-web-listener/testing:
+ en: f92f7ec224dba94d
+ zh: 0f0e0304c834f0af
diff --git a/.agents/notes/implemented/architecture/2026-09-23-native-https-web-listener.md b/.agents/notes/implemented/architecture/2026-09-23-native-https-web-listener.md
new file mode 100644
index 0000000000..123f762559
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-23-native-https-web-listener.md
@@ -0,0 +1,41 @@
+# Agent Note: Native HTTPS on the Web listener
+
+Status: implemented
+
+English | [中文](2026-09-23-native-https-web-listener.zh.md)
+
+## Problem
+
+A directly reached Web listener needs to encrypt launch tokens and session cookies without requiring a separate reverse proxy. Advertising an HTTPS URL does not encrypt the listener or establish certificate trust.
+
+## Decision
+
+WebServer owns optional TLS through `tls: { certFile, keyFile }`, exposed by `dsh web` as paired `--tls-cert` and `--tls-key` flags. Certificate and unencrypted private-key PEM files are loaded before binding; unreadable, empty, invalid, or mismatched material fails activation without an HTTP fallback. HTTP remains the default. Both transports share routing, upgrades, and disposal; disposal also owns incomplete TLS handshakes.
+
+`webServer.protocol` is the source for the bind-address URL, the non-loopback plain-HTTP warning, and Connection's `Secure` cookie attribute. Neither forwarded headers nor `publicUrl` control encryption or that attribute. Explicit Host-port grants normalize under the listener's scheme, while Origin comparison uses the Origin's HTTP(S) scheme so HTTPS proxies can retain HTTP upstreams. TLS grants no additional Host authority and does not replace authentication.
+
+The API authority check does not equate a proxy's upstream transport with the browser page's scheme. An HTTPS listener can accept a matching HTTP Origin, but `sec-fetch-site: cross-site` still rejects the request and authentication remains required. Schemeful `SameSite=Strict` cookies prevent cross-scheme browser credential delivery; this is not support for serving a public browser frontend over HTTP with a TLS upstream.
+
+HTTP cookie audiences retain their existing spelling so enabling TLS support does not invalidate unrelated HTTP sessions. HTTPS cookie names and signed audiences include the `https:` scheme, preventing replay of an HTTP credential on any HTTPS port even if the cookie is renamed. Default ports follow URL normalization without a separate port-443 spelling.
+
+Certificate issuance, trust, renewal, and replacement belong to the operator. Files are read once per activation; replacement requires a listener reload or process restart. The certificate must cover the hostname or IP the TLS client dials. The default port remains 3080; `--port` selects another port, subject to operating-system permission.
+
+## Alternatives considered
+
+**Require a TLS-terminating proxy.** This remains supported, but requires another service for a directly reached listener.
+
+**Derive transport or cookie security from `publicUrl` or forwarding headers.** An advertisement does not describe the receiving socket, and request headers cannot establish trusted proxy identity. Native listener state keeps this decision independent of proxy policy.
+
+**Issue, renew, or generate certificates.** This would add certificate lifecycle and trust-distribution responsibilities. An automatically generated certificate would not establish browser trust.
+
+**Fall back to HTTP on certificate errors.** This would expose credentials despite an explicit request for encryption; activation must fail instead.
+
+## Consequences
+
+Direct HTTPS needs no TLS-terminating proxy. An HTTPS proxy in front of an HTTP listener still owns external cookie rewriting. An HTTPS listener emits `Secure` cookies, so public browser-facing deployments must retain HTTPS rather than depend on local-browser exceptions for HTTP. Deployment procedures belong to the [public deployments guide](../../../../docs/user/guide/public-deployments.md).
+
+This partially supersedes the HTTP-only assumptions in [browser launch-token authentication](2026-08-24-browser-token-authentication.md) and [concrete bind addresses](2026-09-19-concrete-web-bind-address.md). Those notes remain active for cookie payloads and lifetimes, and bind admission respectively.
+
+## Testing
+
+The WebServer suite covers verified HTTPS, invalid material, and disposal. Connection covers listener-owned cookie attributes, cross-protocol credential replay, and exact-port grants; Web startup rejects unpaired flags. The built CLI's `public-url.expected.e2e.ts` authenticates over verified TLS with a `Secure` cookie while retaining HTTP and proxy cases; its failure matrix verifies unreadable TLS material exits without opening a listener. Desktop's startup suite covers matching WS/WSS credentials and refusal on a different transport.
diff --git a/.agents/notes/implemented/architecture/2026-09-23-native-https-web-listener.zh.md b/.agents/notes/implemented/architecture/2026-09-23-native-https-web-listener.zh.md
new file mode 100644
index 0000000000..c34ad19b31
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-23-native-https-web-listener.zh.md
@@ -0,0 +1,41 @@
+# Agent Note: Web 监听器原生 HTTPS
+
+Status: implemented
+
+[English](2026-09-23-native-https-web-listener.md) | 中文
+
+## Problem
+
+浏览器直接访问的 Web 监听器需要在不依赖独立反向代理的情况下加密启动 token 与会话 cookie。公告 HTTPS URL 不会加密监听器,也不会建立证书信任。
+
+## Decision
+
+WebServer 通过 `tls: { certFile, keyFile }` 拥有可选 TLS,由 `dsh web` 的成对 `--tls-cert` 与 `--tls-key` flag 暴露。证书与不带口令的私钥 PEM 文件在绑定前加载;材料不可读、为空、无效或不匹配时激活失败,绝不回退到 HTTP。HTTP 仍为默认值。两种传输共用路由、升级与释放;释放也负责未完成的 TLS 握手。
+
+`webServer.protocol` 决定绑定地址 URL、非 loopback 明文 HTTP 警告及 Connection 的 `Secure` cookie 属性。转发 header 与 `publicUrl` 都不控制加密或该属性。显式 Host 端口授权按监听器的 scheme 归一化,Origin 比较则按 Origin 的 HTTP(S) scheme 进行,以允许 HTTPS 代理保留 HTTP 上游。TLS 不授予额外的 Host authority,也不替代认证。
+
+API authority 校验不会把代理的上游传输等同于浏览器页面的 scheme。HTTPS 监听器可接受 authority 匹配的 HTTP Origin,但 `sec-fetch-site: cross-site` 仍会拒绝请求,且请求仍须认证。区分 scheme 的 `SameSite=Strict` cookie 阻止浏览器跨 scheme 携带凭据;这不表示支持以 HTTP 向公开浏览器前端提供页面、同时使用 TLS 上游。
+
+HTTP cookie audience 保留既有写法,避免启用 TLS 支持时使无关的 HTTP 会话失效。HTTPS cookie 名称与签名 audience 包含 `https:` scheme,因此即使修改 cookie 名称,HTTP 凭据也不能在任何 HTTPS 端口上重放。默认端口遵循 URL 归一化,不为 443 端口另设写法。
+
+证书签发、信任、续期与更换归操作者负责。每次激活只读取一次文件;更换后需重新加载监听器或重启进程。证书必须覆盖 TLS 客户端拨号的 hostname 或 IP。默认端口仍为 3080;`--port` 可选择其他端口,但受操作系统权限约束。
+
+## Alternatives considered
+
+**要求终止 TLS 的代理。** 仍然支持,但会为直接访问的监听器增加一个服务。
+
+**根据 `publicUrl` 或转发 header 推导传输或 cookie 安全属性。** 公告不描述接收请求的 socket,请求 header 也不能建立可信代理身份。原生监听器状态使这一决策独立于代理策略。
+
+**签发、续期或生成证书。** 这会增加证书生命周期与信任分发职责。自动生成证书本身不能建立浏览器信任。
+
+**证书错误时回退到 HTTP。** 这会在操作者明确要求加密时暴露凭据;激活必须失败。
+
+## Consequences
+
+直接 HTTPS 不需要终止 TLS 的代理。HTTP 监听器前的 HTTPS 代理仍负责外部 cookie 重写。HTTPS 监听器签发 `Secure` cookie,因此公开部署的浏览器侧必须保持 HTTPS,而不应依赖本地浏览器对 HTTP 的例外。部署步骤归[公开部署指南](../../../../docs/user/guide/public-deployments.zh.md)所有。
+
+本决策部分取代[浏览器启动 token 认证](2026-08-24-browser-token-authentication.zh.md)与[具体绑定地址](2026-09-19-concrete-web-bind-address.zh.md)中仅支持 HTTP 的前提。这些说明分别继续定义 cookie 载荷与有效期及绑定准入,保持活跃。
+
+## Testing
+
+WebServer 套件覆盖校验证书的 HTTPS、无效材料与释放。Connection 覆盖由监听器决定的 cookie 属性、跨协议凭据重放和精确端口授权;Web 启动拒绝未配对的 flag。已构建 CLI 的 `public-url.expected.e2e.ts` 使用 `Secure` cookie 经校验证书的 TLS 完成认证,并保留 HTTP 与代理用例;失败矩阵验证 TLS 材料不可读时进程退出且不打开监听器。Desktop 启动套件覆盖匹配的 WS/WSS 凭据及不同传输下的拒绝。
diff --git a/.agents/notes/implemented/architecture/2026-09-23-schedule-local-menu.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-23-schedule-local-menu.i18n.yaml
deleted file mode 100644
index 5bd54753a0..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-23-schedule-local-menu.i18n.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-23-schedule-local-menu.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-23-schedule-local-menu.md
-/agent-note-own-the-schedule-detail-menus-inside-the-schedule-client:
- en: a9ed8f9cf0bdfd66
- zh: ff11822efe6b48b7
-/agent-note-own-the-schedule-detail-menus-inside-the-schedule-client/problem:
- en: 076e6ca799bf2045
- zh: d3f87030015cdf3c
-/agent-note-own-the-schedule-detail-menus-inside-the-schedule-client/decision:
- en: c35ab9fa2549866d
- zh: 01855f87b884dc93
-/agent-note-own-the-schedule-detail-menus-inside-the-schedule-client/alternatives-considered:
- en: 3648e55bb76a4381
- zh: 83c98e0789a6df9c
-/agent-note-own-the-schedule-detail-menus-inside-the-schedule-client/consequences:
- en: f6380c38e35bce37
- zh: 1379ff1763d0eb10
-/agent-note-own-the-schedule-detail-menus-inside-the-schedule-client/testing:
- en: f1de601175678a9a
- zh: 58fddec29a462b98
diff --git a/.agents/notes/implemented/architecture/2026-09-23-schedule-local-menu.md b/.agents/notes/implemented/architecture/2026-09-23-schedule-local-menu.md
deleted file mode 100644
index fd3266bbc4..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-23-schedule-local-menu.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# Agent Note: Own the Schedule detail menus inside the Schedule client
-
-Status: implemented
-
-English | [中文](2026-09-23-schedule-local-menu.zh.md)
-
-## Problem
-
-The Automation tasks detail needs a menu whose list carries a pinned control: the Time zone row's search field must stay above the scrolling zone rows. The shared `Menu` in `@deepseek-ai/dsh-client-ui-primitives` renders rows, separators, labels, a selection mark, and a keyboard walk, but it has no header slot. The Delivery records tab shows a saved record's message id behind an info glyph, and the reader must be able to read and copy that id.
-
-Serving either need by extending `ui-primitives` puts one feature's behavior into the package every client plugin consumes. The same branch had already removed unrelated `ui-layout` and `ui-sidebar-right` edits after review asked why a Schedule change touched them, and a shared primitive extended for a single caller still costs every future reader of that primitive.
-
-## Decision
-
-`packages/client/ui-schedule` owns its menu. `src/client/TaskMenu.tsx` with `TaskMenu.module.css` holds a schedule-local copy that renders rows, separators, labels, disabled and destructive rows, a trailing `selectedId` mark, the pointer and keyboard walk, portaled placement, the pinned `header` slot, and the `data-menu-field` focus handover. It imports the shared `useAnchoredPosition` and `useDismissOnOutsidePointer` hooks, which are the placement and dismissal behavior any anchored panel needs, and it imports no runtime value from `ui-primitives`. The four detail menus use it.
-
-The Delivery records tab names and copies a record's message id through an info button. The button's accessible name states the action and the record (`Copy message ID: `), the shared `Tooltip` states the id on hover and on keyboard focus, and activation copies it through the shared `writeClipboard` helper and reports `Copied` in a status region beside the glyph. `Tooltip` therefore keeps its master shape: the mode that held one bubble open under the pointer is gone, and the Delivery records view does not use the `HoverCard` that opens on the pointer alone.
-
-`packages/client/ui-primitives` returns to master except the clock glyph's stroke. The client bundle purity gate in `packages/client/tsdown.client.ts` is the neighboring rule for the other direction of this boundary: a client plugin may import another plugin's package only as types or as a module-table request, so shared runtime vocabulary travels through an inline-safe layer or a cordis service rather than a package edge.
-
-## Alternatives considered
-
-**Add a `header` prop to the shared `Menu`.** This is what the branch first did: an optional slot plus the `data-menu-field` focus handover, with tests in the shared package. It gives the smallest diff and keeps one menu implementation, and it was rejected because the only caller is the Schedule detail: `ui-primitives` would carry API that exists for one feature, and every client plugin's bundle would read a header contract none of them use.
-
-**Add an `interactive` mode to the shared `Tooltip`.** Shipped first and then removed: a grace period between anchor and bubble, pointer hold, click toggling, and selectable text, with six tests. It lost because the shared tooltip already opens on focus and the shared clipboard helper already performs the copy, so the mode added a second way to hold one bubble open for a single caller.
-
-**Name and copy the message id through `HoverCard`.** Shipped second and then replaced: the card holds while the pointer rests on it, selects its text, and copies on activation. It lost because `HoverCard` opens on `onPointerEnter` alone, so the already-focusable glyph showed a keyboard reader nothing and offered no copy path.
-
-**Render the zone search field outside the menu list.** A search input above the trigger, or a separate row in the rule card, needs no primitive change. It was rejected because the field would scroll away from the list it filters or sit in a different surface than the rows it filters.
-
-**Build the zone picker on the schedule-local `PickerPopover`.** The Date and Time pickers already use that panel, so the zone list could reuse it. It was rejected because `PickerPopover` positions and dismisses a panel but renders no list semantics: the keyboard walk, the row roles, and the selected-row mark would have to be rebuilt inside the Schedule client anyway, which is the copy this note describes with more surface than the menu it replaces.
-
-## Consequences
-
-The Schedule client carries a second menu implementation: 346 lines of component, 189 lines of CSS, and 403 lines of tests, in exchange for `ui-primitives` returning to master except the clock glyph's stroke. The copy is trimmed to what the detail uses, and the absent capabilities stay absent: submenus, component-rendered rows, a pinned footer, multiple selection ids, the fill selection mode, dense and compact spacing, mount-time autofocus, a caller-supplied anchor rect, pointer-leave closing, and a side parameter. A later Schedule menu that needs one of them extends the copy or revisits this decision.
-
-Copying is an activation, not a text selection: the tooltip shows the id but does not accept the pointer, so the desktop gesture that copies the id is a click on the glyph. That is the capability the removed interactive mode had and this shape gives up. Shared behavior stays shared: placement, dismissal, focus handover, the duplication gate's exclusion markers naming both owners, and per-file coverage all apply to the copy. The copied region carries `jscpd:ignore-start` and `jscpd:ignore-end` with the two owners named, following the [cross-package value dependencies note](../../archived/process/2026-08-23-client-cross-package-value-dependencies.md); the schedule-specific header, handover, and Tab semantics sit outside that region.
-
-The copy places its list with `useAnchoredPosition`, which clamps every edge by the same 12px margin, while the shared menu's top edge used the frame-top clearance helper. A list that must clamp upward against the top of a macOS window can therefore sit about 12px lower than the shared menu did in the same situation; the stylesheet still reserves the clearance for the list's own maximum height, so the common case is unchanged.
-
-## Testing
-
-`packages/client/ui-schedule/tests/task-menu.client.spec.tsx` pins the pointer and keyboard walk, the `header` field's Home/End ownership, the `data-menu-field` handover after placement, the selected-row mark, disabled rows, portal placement, outside dismissal, and focus return; the three menu-header tests that lived in `ui-primitives` moved there with the capability. `delivery-history.client.spec.tsx` pins the tooltip that keyboard focus raises, the button's accessible name, the clipboard write, and the copy confirmation that clears. The coverage gate reports 100% per file for `packages/client/ui-schedule/src`, and the duplication gate reports no clones across `packages/client/ui-schedule` and `packages/client/ui-primitives`.
diff --git a/.agents/notes/implemented/architecture/2026-09-23-schedule-local-menu.zh.md b/.agents/notes/implemented/architecture/2026-09-23-schedule-local-menu.zh.md
deleted file mode 100644
index 924f14f5ad..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-23-schedule-local-menu.zh.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# Agent Note: Schedule 详情菜单由 Schedule 客户端自己拥有
-
-Status: implemented
-
-[English](2026-09-23-schedule-local-menu.md) | 中文
-
-## Problem
-
-Automation tasks 详情需要一个列表顶部钉住控件的菜单:Time zone 行的搜索框必须停在滚动时区行之上。`@deepseek-ai/dsh-client-ui-primitives` 里的共享 `Menu` 会渲染行、分隔线、标题行、选中标记和键盘走位,但没有 header 槽。Delivery records 页把一条已保存记录的 message id 放在 info 图标背后,读者要能读到并复制这个 id。
-
-用扩展 `ui-primitives` 来满足其中任一需求,都会把单个功能的行为放进每个 client 插件都消费的包里。同一条分支此前已经因为 review 质询“Schedule 的改动为什么碰它们”而撤掉了无关的 `ui-layout` 与 `ui-sidebar-right` 编辑;而一个只为单一调用方扩展的共享原语,对之后每一个读它的人都是成本。
-
-## Decision
-
-`packages/client/ui-schedule` 自己拥有菜单。`src/client/TaskMenu.tsx` 与 `TaskMenu.module.css` 保存一份 schedule 本地副本,渲染行、分隔线、标题行、disabled 与 dangerous 行、`selectedId` 尾随标记、指针与键盘走位、portal 定位、钉住的 `header` 槽,以及 `data-menu-field` 焦点交接。它导入共享的 `useAnchoredPosition` 与 `useDismissOnOutsidePointer` hook——任何锚定面板都需要的定位与关闭行为——不从 `ui-primitives` 导入任何运行时值。详情的四个菜单都改用它。
-
-Delivery records 页用一个 info 按钮给出并复制记录的消息 id。该按钮的可访问名称同时说明动作与记录(`Copy message ID: `),共享 `Tooltip` 在悬停与键盘聚焦时给出该 id,激活时通过共享的 `writeClipboard` 复制,并在图标旁用 status 区域报告 `Copied`。因此 `Tooltip` 保持 master 形态:那个让气泡在指针下保持展开的模式已不存在,Delivery records 页也不使用只在指针下打开的 `HoverCard`。
-
-`packages/client/ui-primitives` 除时钟图标描边外回到 master。`packages/client/tsdown.client.ts` 的 client bundle purity gate 是这条边界另一方向上的相邻规则:client 插件只能以类型或 module-table request 的形式导入另一个插件的包,因此共享运行时词汇要么走 inline-safe 层,要么走 cordis service,而不是包与包之间的边。
-
-## Alternatives considered
-
-**给共享 `Menu` 加 `header` prop。** 这是分支最初的做法:一个可选槽加 `data-menu-field` 焦点交接,并在共享包里带测试。它的 diff 最小、只保留一份菜单实现;被否决是因为唯一的调用方是 Schedule 详情:`ui-primitives` 会背上只为单个功能存在的 API,而每个 client 插件的 bundle 都要读一份没人使用的 header 契约。
-
-**给共享 `Tooltip` 加 `interactive` 模式。** 先落地后撤除:锚点与气泡之间的宽限期、指针停留、点击切换、可选中的文字,外加六个测试。被否决是因为共享 tooltip 本来就支持聚焦展开、共享剪贴板助手本来就负责复制,这个模式只是为单一调用方多出了一种保持气泡展开的方式。
-
-**用 `HoverCard` 给出并复制消息 id。** 第二个落地又被替换的版本:指针停在卡片上时保持展开、文字可选中、激活即复制。被否决是因为 `HoverCard` 只在 `onPointerEnter` 打开,于是那个本来就可聚焦的图标对键盘读者什么都不显示,也没有复制入口。
-
-**把时区搜索框渲染在菜单列表之外。** 放在触发器上方的输入框,或规则卡里的独立一行,都不需要改原语。被否决是因为该输入框会与它所过滤的列表分离滚动,或落在与它过滤的行不同的界面层上。
-
-**用 schedule 本地的 `PickerPopover` 实现时区选择器。** Date 与 Time picker 已经用它。被否决是因为 `PickerPopover` 只负责定位与关闭面板,不渲染列表语义:键盘走位、行 role、选中行标记都要在 Schedule 客户端里重建,那正是本 note 描述的副本,而且比它所替代的菜单要维护更多东西。
-
-## Consequences
-
-Schedule 客户端多出一份菜单实现:346 行组件、189 行 CSS、403 行测试,换来 `ui-primitives` 除时钟图标描边外回到 master。副本按详情实际使用裁剪,缺失的能力保持缺失:submenu、组件渲染行、钉住的 footer、多个 selectedId、fill 选中模式、dense 与 compact 间距、挂载时 autofocus、调用方提供锚点矩形、指针离开即关闭,以及 side 参数。日后某个 Schedule 菜单需要其中一项时,要么扩展副本,要么重新审视这个决定。
-
-复制是一次激活,而不是选中文本:tooltip 给出 id 但不接受指针,所以桌面端复制该 id 的手势是点击图标;这正是被撤掉的交互模式所具备、而当前形态放弃的能力。共享行为仍是共享的:定位、关闭、焦点交接、点名两个 owner 的 duplication gate 排除标记,以及 per-file 覆盖率都适用于这份副本。被拷贝的区域带 `jscpd:ignore-start` 与 `jscpd:ignore-end` 并点名两个 owner,遵循 [cross-package value dependencies note](../../archived/process/2026-08-23-client-cross-package-value-dependencies.md);schedule 专有的 header、交接与 Tab 语义不在该区域内。
-
-副本用 `useAnchoredPosition` 定位列表,它对四条边都用同一个 12px margin,而共享菜单的顶边用的是 frame-top clearance helper。因此在 macOS 窗口顶部需要向上钳制的列表,可能比共享菜单原先的位置低约 12px;样式表仍为该列表自身的最大高度保留 clearance,常见情形不变。
-
-## Testing
-
-`packages/client/ui-schedule/tests/task-menu.client.spec.tsx` 钉住指针与键盘走位、`header` 输入框对 Home/End 的独占、落位后的 `data-menu-field` 交接、选中行标记、disabled 行、portal 定位、外部关闭与焦点回返;原先位于 `ui-primitives` 的三个菜单 header 测试随该能力一起迁入。`delivery-history.client.spec.tsx` 钉住键盘聚焦展开的工具提示、按钮的可访问名称、剪贴板写入,以及会自行清除的复制确认。覆盖率门禁对 `packages/client/ui-schedule/src` 报告 per-file 100%,duplication 门禁在 `packages/client/ui-schedule` 与 `packages/client/ui-primitives` 上报告 0 克隆。
diff --git a/.agents/notes/implemented/architecture/2026-09-24-bounded-session-log-upload.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-bounded-session-log-upload.i18n.yaml
index 64f1ca3e65..cc7f108d4c 100644
--- a/.agents/notes/implemented/architecture/2026-09-24-bounded-session-log-upload.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-24-bounded-session-log-upload.i18n.yaml
@@ -15,5 +15,5 @@
en: 88985b4e1f5c7a4f
zh: 32617f7ff75ee825
/agent-note-bounded-deepseek-session-log-upload/consequences:
- en: e19fb6136ffd2250
- zh: 01dacd7a44c3f837
+ en: 288834c514df377a
+ zh: a33a4af2b4d9ebf4
diff --git a/.agents/notes/implemented/architecture/2026-09-24-bounded-session-log-upload.md b/.agents/notes/implemented/architecture/2026-09-24-bounded-session-log-upload.md
index b980765eed..27ae7e4117 100644
--- a/.agents/notes/implemented/architecture/2026-09-24-bounded-session-log-upload.md
+++ b/.agents/notes/implemented/architecture/2026-09-24-bounded-session-log-upload.md
@@ -25,6 +25,6 @@ This supersedes complete-suffix delivery and fail-closed serialization in the [r
## Consequences
-A backlog drains at up to 8 MiB per accepted request by default. At the end-to-end rate measured for the 269 MiB upload, about 8 MiB/s, an 8 MiB field adds about 1 s to each request while a backlog drains, and a 0.86 GB backlog drains in about 100 requests. In the same 96,341-event Session, the median per-step append is 11 KiB and the largest event is 414 KiB, so ordinary steps stay far below the limit and only an unusual event or a small configured limit reaches the oversized-event path. While a backlog drains, the endpoint receives a `throughSeq` behind the Session's current tip. Preparation failures, acceptance failures, and provider rejections still fail the model request.
+A backlog drains at up to 8 MiB per accepted request by default. At the end-to-end rate measured for the 269 MiB upload, about 8 MiB/s, an 8 MiB field adds about 1 s to each request while a backlog drains, and a 0.86 GB backlog drains in about 100 requests. In the same 96,341-event Session, the median per-step append is 11 KiB and the largest event is 414 KiB, so ordinary steps stay far below the limit and only an unusual event or a small configured limit reaches the oversized-event path. While a backlog drains, the endpoint receives a `throughSeq` behind the Session's current tip. Provider rejections still fail the model request; preparation and acceptance failures are logged and do not.
Plugin tests cover the exact byte boundary, UTF-8 counting, backlog draining across accepted requests, the oversized-event warning with its unchanged watermark, an event past the string limit, and the 8 MiB default through plugin configuration. Adapter tests cover the base-body fallback through the provider plugin's logger and through an adapter without a reporter.
diff --git a/.agents/notes/implemented/architecture/2026-09-24-bounded-session-log-upload.zh.md b/.agents/notes/implemented/architecture/2026-09-24-bounded-session-log-upload.zh.md
index 13dd90ed0a..8e2d6d4785 100644
--- a/.agents/notes/implemented/architecture/2026-09-24-bounded-session-log-upload.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-24-bounded-session-log-upload.zh.md
@@ -25,6 +25,6 @@ DeepSeek 适配器把合并后请求正文的序列化失败视为扩展失败
## 后果
-默认情况下,积压按每次已接受请求最多 8 MiB 补传。按那次 269 MiB 上传实测的端到端速度(约 8 MiB/s),补传期间 8 MiB 的字段让每次请求多约 1 秒,0.86 GB 的积压约 100 次请求补完。在同一个 96,341 条事件的会话中,每步追加量的中位数是 11 KiB,最大单条事件是 414 KiB,因此普通步骤远低于上限,只有异常事件或很小的配置上限才会走到超限事件路径。积压补传期间,端点收到的 `throughSeq` 会落后于会话当前末尾。准备失败、接受失败与提供方拒绝仍会使模型请求失败。
+默认情况下,积压按每次已接受请求最多 8 MiB 补传。按那次 269 MiB 上传实测的端到端速度(约 8 MiB/s),补传期间 8 MiB 的字段让每次请求多约 1 秒,0.86 GB 的积压约 100 次请求补完。在同一个 96,341 条事件的会话中,每步追加量的中位数是 11 KiB,最大单条事件是 414 KiB,因此普通步骤远低于上限,只有异常事件或很小的配置上限才会走到超限事件路径。积压补传期间,端点收到的 `throughSeq` 会落后于会话当前末尾。提供方拒绝仍会使模型请求失败;准备失败与接受失败只记录告警,不会使请求失败。
插件测试覆盖精确字节边界、UTF-8 计数、跨已接受请求的积压补传、超限事件告警及其不变的水位、超过字符串上限的事件,以及经插件配置生效的 8 MiB 默认值。适配器测试覆盖经提供方插件 logger 报告的基础正文回退,以及没有 reporter 的适配器上的同一回退。
diff --git a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.i18n.yaml
new file mode 100644
index 0000000000..71c61113a6
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-09-24-deepseek-account-remote-model-catalog.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.md
+/agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version:
+ en: e6d82714ac89a7ae
+ zh: 54c14278e749e7e1
+/agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version/problem:
+ en: 2eeb916eb65c66e7
+ zh: 7bf5c0abb8bb61e4
+/agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version/decision:
+ en: 42e800c4cd3b6ebe
+ zh: 06fabe5a9681fa27
+/agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version/alternatives-considered:
+ en: 7c7880fdb0cc95f6
+ zh: 2b8a539192626789
+/agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version/consequences:
+ en: 352ece6c2decaaf7
+ zh: 1d0c3620fd26a35c
diff --git a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.md b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.md
new file mode 100644
index 0000000000..75265db1ce
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.md
@@ -0,0 +1,36 @@
+# Agent Note: DeepSeek account models come from the account listing and gate by client version
+
+Status: implemented
+
+English | [中文](2026-09-24-deepseek-account-remote-model-catalog.zh.md)
+
+## Problem
+
+The account route's catalog was deployment configuration: a build advertised the models it shipped with, a new model reached users only through a release, and nothing kept an older build from requesting a model it cannot drive. The account service publishes per-account model metadata at `GET /models`, and that listing can name models an older build does not understand.
+
+## Decision
+
+- [`llm-deepseek-account`](../../../../packages/llm/llm-deepseek-account/README.md) loads the listing lazily for each credential, waits for it once up to `modelsTimeoutMs`, refreshes it in the background after `modelsMaxAgeMs`, keeps the last good listing when a load fails or returns no model, and serves the bundled catalog before any listing loads. The cache lives in memory and is keyed by endpoint and token fingerprint, so per-account rollouts stay separate.
+- A listed model is available when its `client_requirements["deepseek-harness"].min_version` is at most the current DeepSeek Harness version in semantic-version order. Without that declaration, only ids the built-in catalog knows are available; the others stay hidden, because a client cannot tell which future version supports them. A request for a refused listed model fails before network I/O with `CLIENT_UPGRADE_REQUIRED` or `MODEL_UNSUPPORTED`.
+- Listed capabilities replace bundled ones field by field; the bundled entry keeps client-side knowledge such as tool-update mode and image budgets; `modelOverrides` applies last and reuses the `models` entry fields and validation. `max_output_tokens` bounds the request default instead of becoming it.
+- The shared transport gains only two provider hooks, `connect` and `admit`; listing, gating, and merging stay in the account package.
+
+## Alternatives considered
+
+**Server-side filtering by client version only.** Codex sends `client_version` and lets its backend drop models. An older build then cannot show that a newer version supports a model, and a listing that omits a declaration would expose an unknown model.
+
+**Default parameters for unknown models.** Reasonix and Pi drive unknown ids with generic defaults, and MiniMax treats that as a defect. A model that needs client changes would run with the wrong behavior.
+
+**Whole-listing validation.** Codex, ZCode, and Kimi reject the whole listing on one bad field. One malformed row would then send every account back to the bundled catalog.
+
+**Persisting the last good listing.** A durable cache adds a persistence format and its migration duty; the bundled catalog already covers offline starts.
+
+**Keeping `models` with merge-by-id semantics.** The API-key route's `models` replaces its catalog, so the same field name with merge semantics would invert its meaning between sibling routes.
+
+**Generalizing the shared transport.** Per-model effort levels, an output-capability field, and catalog-change announcements would widen the Messages package for one consumer. The listed effort levels match the bundled defaults, and a model that needs different ones needs a release, which the gate enforces.
+
+## Consequences
+
+- An older build never offers a model it does not know unless the service declares a minimum version that build satisfies, and the service must declare `min_version` for a future build to show that an upgrade enables a model.
+- A modified client can still request any id; the service enforces version floors only if it rejects such requests itself.
+- The first account access after sign-in may wait up to `modelsTimeoutMs`; an offline start uses bundled capabilities until a listing loads; a listing change during a running process reaches the Web picker at its next catalog refresh.
diff --git a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.zh.md
new file mode 100644
index 0000000000..7199091c73
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.zh.md
@@ -0,0 +1,36 @@
+# Agent Note: DeepSeek 账号模型来自账号列表,并按客户端版本门控
+
+Status: implemented
+
+[English](2026-09-24-deepseek-account-remote-model-catalog.md) | 中文
+
+## 问题
+
+账号路由的目录原本是部署配置:每个构建只提供它发布时自带的模型,新模型只能随发版到达用户,也没有办法阻止旧构建请求它驱动不了的模型。账号服务在 `GET /models` 上按账号发布模型元数据,这份列表可能包含旧构建不认识的模型。
+
+## 决策
+
+- [`llm-deepseek-account`](../../../../packages/llm/llm-deepseek-account/README.zh.md) 按凭据懒加载列表,首次访问最多等待 `modelsTimeoutMs`,超过 `modelsMaxAgeMs` 后在后台刷新;获取失败或返回的列表中没有模型时保留上一份,还没有加载到任何列表时使用内置目录。缓存只在内存中,以端点和 token 指纹为键,因此按账号灰度互不干扰。
+- 列出的模型在其 `client_requirements["deepseek-harness"].min_version` 按 semver 顺序不高于当前 DeepSeek Harness 版本时可用。没有这个声明时,只有内置目录认识的 id 可用,其余保持隐藏,因为客户端无法判断将来哪个版本会支持它们。请求被拒绝的已列出模型时,在网络请求之前以 `CLIENT_UPGRADE_REQUIRED` 或 `MODEL_UNSUPPORTED` 失败。
+- 列表中的能力逐字段覆盖内置能力;内置条目保留工具更新模式、图片预算等客户端知识;`modelOverrides` 最后叠加,复用 `models` 条目的字段与校验。`max_output_tokens` 只限制请求默认值,不直接成为默认值。
+- 共享传输层只新增 `connect` 与 `admit` 两个提供方钩子;列表、门控与合并都留在账号包中。
+
+## 考虑过的替代方案
+
+**只在服务端按客户端版本过滤。** Codex 发送 `client_version`,由后端去掉模型。这样旧构建无法提示新版本支持某个模型,而且列表漏掉声明时会暴露未知模型。
+
+**未知模型套用默认参数。** Reasonix 和 Pi 用通用默认值驱动未知 id,MiniMax 把这种做法当作缺陷。需要客户端改动的模型会以错误的方式运行。
+
+**整份列表严格校验。** Codex、ZCode 和 Kimi 会因为一个坏字段拒绝整份列表。一条格式错误的记录就会让所有账号退回内置目录。
+
+**持久化上一次成功的列表。** 持久缓存要新增持久化格式并承担迁移责任;内置目录已经覆盖离线启动。
+
+**保留 `models` 并改为按 id 合并。** API key 路由的 `models` 会替换整个目录,同名字段在兄弟路由上改成合并语义会让含义相反。
+
+**扩展共享传输层。** 按模型的推理档位、输出能力字段和目录变更通知,会为一个使用方扩大 Messages 包的范围。列表中的推理档位与内置默认值一致;需要不同档位的模型需要发版适配,门控正是用来保证这一点。
+
+## 影响
+
+- 旧构建永远不会提供它不认识的模型,除非服务端声明了该构建满足的最低版本;服务端必须声明 `min_version`,将来的构建才能提示升级后可用。
+- 改过的客户端仍然可以请求任意 id;只有服务端自己拒绝这类请求时,版本下限才会被强制执行。
+- 登录后的第一次账号访问最多等待 `modelsTimeoutMs`;离线启动在加载到列表之前使用内置能力;进程运行中途的列表变化要等 Web 选择器下一次刷新目录时才会显示。
diff --git a/.agents/notes/implemented/architecture/2026-09-24-preparing-tool-arguments.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-preparing-tool-arguments.i18n.yaml
new file mode 100644
index 0000000000..fcc4c5d557
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-preparing-tool-arguments.i18n.yaml
@@ -0,0 +1,31 @@
+# Bilingual-pair consistency record for 2026-09-24-preparing-tool-arguments.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-24-preparing-tool-arguments.md
+/agent-note-incremental-tool-argument-scanning-and-argument-order-during-preparation:
+ en: 07f6420c67ea2700
+ zh: b9b53250e050cf6e
+/agent-note-incremental-tool-argument-scanning-and-argument-order-during-preparation/problem:
+ en: c97b3edd4ef1d063
+ zh: 5c8b6065e662f326
+/agent-note-incremental-tool-argument-scanning-and-argument-order-during-preparation/decision:
+ en: a15c7c13f167e5ac
+ zh: 63bcc2ef60c2f344
+/agent-note-incremental-tool-argument-scanning-and-argument-order-during-preparation/decision/a-lazily-computed-argument-view-lives-in-dsh-util-values:
+ en: eb004194b0c08464
+ zh: d09c6353ed7138de
+/agent-note-incremental-tool-argument-scanning-and-argument-order-during-preparation/decision/parsing-happens-in-the-tool-definition-not-in-react:
+ en: 518206f94d82993d
+ zh: c6cab3b2f71dd18b
+/agent-note-incremental-tool-argument-scanning-and-argument-order-during-preparation/decision/argument-declaration-order:
+ en: 0fd7c9f0b8cf6a28
+ zh: 7d410403283b9996
+/agent-note-incremental-tool-argument-scanning-and-argument-order-during-preparation/alternatives-considered:
+ en: 7a033f81c557c58f
+ zh: 2ec66b0db6cf5d9e
+/agent-note-incremental-tool-argument-scanning-and-argument-order-during-preparation/verification:
+ en: c1e09085a137be1c
+ zh: ec79806f84f59dec
+/agent-note-incremental-tool-argument-scanning-and-argument-order-during-preparation/consequences:
+ en: f1c44f9d58d36920
+ zh: bb297337882ec407
diff --git a/.agents/notes/implemented/architecture/2026-09-24-preparing-tool-arguments.md b/.agents/notes/implemented/architecture/2026-09-24-preparing-tool-arguments.md
new file mode 100644
index 0000000000..60a9bc6a15
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-preparing-tool-arguments.md
@@ -0,0 +1,88 @@
+# Agent Note: Incremental tool-argument scanning and argument order during preparation
+
+Status: implemented
+
+English | [中文](2026-09-24-preparing-tool-arguments.zh.md)
+
+## Problem
+
+[Three-stage tool calls](2026-09-22-tool-call-three-phases.md) make tool identity visible before `tool/call`. A raw argument prefix alone cannot supply field-aware presentation: write/edit need a complete path and decoded content length, while bash/run_code need the description before the command or program finishes streaming.
+
+The order in which the model generates arguments also decides what can appear first. On 2026-09-24, 310 controlled requests against the real DeepSeek V4 API exercised bash/run_code/write/edit:
+
+| Variant | Change | bash first key is description | run_code first key is description |
+|---|---|---|---|
+| Baseline | — | 0/20 | 1/20 |
+| `properties` order only | description moved first | 0/20 | 0/20 |
+| `properties` + `required` together | Same plus reordered `required` | Forced tool_choice 10/10; free calling Pro 7/10, Flash 0/10 | Forced 10/10; free 20/20 |
+| Together + one ordering instruction in the description text | Appends "Provide `description` before `command` in the arguments." | Free calling Flash 19/20, Pro 20/20 | 20/20 |
+
+In these samples, reordering `required` changes argument order where reordering `properties` alone does not; V4 Flash's free bash calls also need the explicit instruction. The `dsh-tools` schema DSL generates `required` in property declaration order, so changing the declaration order in source reorders both. The probe uses a minimal system prompt, not the complete Harness prompt.
+
+## Decision
+
+### A lazily computed argument view lives in `dsh-util-values`
+
+`PartialArguments` is a tool-independent lazy reader. It retains separate argument fragments and a top-level key/value range index, or borrows an already parsed PTC payload. Reads index only newly received boundaries; unrequested values are neither decoded, counted, nor parsed. Requested fields materialize from their own ranges, without a per-tool field specification or registry.
+
+The boundary scanner tracks quotes, backslash parity, and matching nested brackets across fragments. Key names are decoded for lookup, while value contents are deferred. `complete()` reports a closing delimiter, not validated content; `invalid` reports errors already found by indexing or a content read. Content errors do not prevent indexing subsequent fields. Formal tool-input validation remains outside this presentation reader.
+
+Requested unescaped lengths derive directly from offsets. Escaped strings maintain content cursors only for requested length, text, or bounded-prefix reads; a limit query stops once its answer is known. Closed strings use native decoding when their full text is first requested. Arbitrary late reads require retaining the original fragments until authoritative complete text replaces them.
+
+The view judges change itself: `append(delta)` only retains the fragment, and `refresh()` compares remembered answers at publication time. With nothing read yet, it does not scan or report a change. Only refresh advances an existing answer's comparison baseline; an intervening read cannot suppress another consumer's update. The business states its granularity as it reads: `stringLength('content', { step: 1024 })` makes only a kilobyte crossing count, `text('description')` makes every character count, and `textPrefix()` limits decoded text to the requested UTF-16 prefix. An `offset` includes completed strings when edit progress combines old and new text.
+
+### Parsing happens in the Tool Definition, not in React
+
+The [Tool Definition](../../../../packages/client/ui-chat/src/client/conversation-nodes/tool.ts) matches every `tool-call-delta` carrying an id as a start candidate: the earliest opens the Context whether or not it is named, later ones fold as updates. The named delta creates the preparing root with a fresh streaming view; every delta only calls `args.append(delta)`. At the `animation-frame` publication point, `buildViewNode()` refreshes observed answers and replaces the root only when they change; otherwise it reuses the currently published root. The Context and its published Node own these identities without a separate preparation cache. A stream whose arguments precede the name is not scanned: a view opened late sees a non-object prefix, turns invalid, and reports no fields.
+
+Every stage's block exposes `name` and `args`; legacy `argsRaw` and result `call: { name, argsRaw } | null` remain available. `block-end` and `tool/call` reconcile argument fragments against their authoritative full text with length and exact per-fragment comparisons. Matching text seals the same reader and preserves its indexed fields; missing or conflicting deltas create a new `fromText()` view. No cumulative string or probabilistic hash is needed. PTC readers use `fromObject(payload)`, results reuse the started reader, and an unpaired result uses the shared empty view. Card models continue to read finalized `argsRaw`.
+
+Assistant blocks retain tool identity and first-token timing, not argument deltas. Complete argument text comes from `block-end` or the durable message. Parameter-only deltas preserve Assistant State, while the Tool Definition owns preparing reads and notifications.
+
+The Assembler shares one immutable Match per input and lifecycle role, including its Location, while each Definition retains independent State. Empty dependency sets require no replay work.
+
+Rows use the same readers across stages: read/write/edit show an openable path once `file_path` closes; write/edit show decoded content length in 1024-character units while content streams; bash/pwsh/run_code show the description prefix. Chat group detail reads the same view using its existing field priority. Without usable detail it stays empty while fields can still arrive, then falls back to the tool name once `closed()` is true, for every category.
+
+Group detail normalizes an initial 512-unit decoded prefix and expands it only when whitespace or multi-unit grapheme clusters prevent deciding the 160-cluster result. A filled prefix remains unchanged as its field grows, while a later higher-priority field still republishes the detail. String-length checks do not materialize the full text. Detail already within 160 UTF-16 units requires no grapheme traversal.
+
+Third-party tools receive the same view without registration or a separate subscription. Write/edit and Bash share their component across stages. The mutable argument reader is a narrow exception to JSON-compatible owner data: its Definition publishes a new block reference when an observed answer changes. Scan caches use private fields so read history does not affect structural comparison of equal sources.
+
+### Argument declaration order
+
+`tool-bash` and `tool-pwsh` declare `description` before `command` and include the ordering instruction. Both `parameters` declarations of `run_code` put `description` before `code` and share a parameter description requesting that generation order for TypeScript and Python. Write/edit declare `file_path` first and ask for it before `content` or `old_string`/`new_string` in the path parameter description. Read retains its leading `file_path`; other tools retain their argument order.
+
+## Alternatives considered
+
+**Parse inside a hook closure (a selector subscribing to the Step source).** Only the subscribing row sees the parsed result, so group titles need another subscription and each row owns parser lifetime. Definition-owned views share parsed fields across consumers.
+
+**Keeping a per-tool field specification in the Definition (built-in table plus a runtime registry).** The Definition is a pure function without ctx, so a runtime registry could only reach it through a factory closure, and a third-party tool would register in two places, the slot and the registry; the specification only ever said which fields keep text, information that on-demand decoding from the raw text no longer needs.
+
+**Third-party `partial-json`.** It reparses the cumulative input each frame and does not expose string completion separately from the decoded value.
+
+**Eagerly parse every field.** Large fields such as `content` usually need only a length; eagerly materializing their text adds decoding and memory costs before a consumer asks for it. Observing the entire key list also republishes a block when an unused argument appears; detail keeps field-specific observations.
+
+**Slice the normalized field for a truncated detail.** A V8 substring can retain the entire field. Joining the bounded selection of grapheme clusters keeps the displayed prefix independent of that source.
+
+**Reorder `properties` only.** Measured 0/40 effective; the model follows the `required` order.
+
+## Verification
+
+- Parser tests cover lazy scanning, observed-answer changes, empty and repeated keys, split escapes, non-string values, invalid input, sealed views, and read-independent structural equality.
+- Row and grouping tests cover streamed paths and descriptions, content length, shared row identity, name fallback only when arguments cannot grow, and at most 161 clusters per truncation pass without changing whitespace or Unicode handling.
+- Negative controls restore read-time acknowledgement, repeated Match allocation, and short-text segmentation; four focused tests fail at their corresponding assertions and pass after restoration. Definitions retain independent State; Turn and Step records remain Session-owned.
+- Additional controls eagerly parse unread containers, accept same-length conflicting text, or rebuild Assistant State for parameter-only deltas. Each fails its owning assertion; the boundary-index implementation passes after restoration.
+
+The required [conversation-fold benchmark](../../../../benchmarks/conversation-fold/conversation-fold.bench.client.ts) drives the real Tool Definition, Assembler, Chat groups, and published write-row reads with 16-character fragments, flushing every 64 fragments. Three fresh compiled Node workers report all samples and their median; setup and forced GC are outside timing, and retained heap is measured with the Assembler and argument view still reachable. Row reads validate decoded progress and the file path; the worker also reports process peak RSS.
+
+Local Chat-only measurements on 2026-09-29 compare the pre-feature baseline with the optimized implementation for 512 KiB write content and 32,769 body deltas. On Linux x64, AMD EPYC 7763, Node 24.18.0, median unprofiled processing time is 212.16 → 176.12 ms, cumulative allocation including collected objects is 132.06 → 76.65 MiB, and retained heap is 24.89 → 21.84 MiB. These measurements exclude model, network, browser rendering, and non-Chat targets; they do not establish whole-page or CI-runner latency.
+
+Match calls remain 393,228; update calls rise from 98,307 to 131,076 because the Tool Definition consumes every argument delta. The baseline displays raw-length write progress rather than decoded argument reads.
+
+The CI budgets remain 750/375 ms and 37.5/10 MiB: reference expectations of 300/150 ms use the shared 2× time scale and 1.25× headroom, while 30/8 MiB heap expectations use headroom only. Historical cumulative-indexing/full-segmentation controls take 3984.9–4035.5 ms for write and 1471.2–1494.9 ms for bash, exceeding those unchanged budgets.
+
+## Consequences
+
+- Observed argument changes replace preparing blocks; unread calls retain their block reference. Views retain source text, and group headers can request fields independently of tool rows. Started and settled views scan on first read without publishing updates.
+- Schema order and description instructions encourage label-first generation but do not guarantee it. Rows show usable fields as they arrive; a late path delays its display and the accompanying content progress.
+- Presentation remains Client-derived; argument scanning adds no Session event or persistence format.
+- A stream whose name arrives after its arguments gives up scanning; that call's preparing row falls back to the title alone.
diff --git a/.agents/notes/implemented/architecture/2026-09-24-preparing-tool-arguments.zh.md b/.agents/notes/implemented/architecture/2026-09-24-preparing-tool-arguments.zh.md
new file mode 100644
index 0000000000..653b517aed
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-preparing-tool-arguments.zh.md
@@ -0,0 +1,88 @@
+# Agent Note: 准备阶段的工具参数增量扫描与参数顺序
+
+Status: implemented
+
+[English](2026-09-24-preparing-tool-arguments.md) | 中文
+
+## 问题
+
+[三阶段工具调用](2026-09-22-tool-call-three-phases.zh.md)让工具身份在 `tool/call` 之前就可见。只有原始参数前缀不足以按字段展示:write/edit 需要完整路径和解码后的内容长度,bash/run_code 需要在命令或程序流入结束前显示描述。
+
+模型生成参数的顺序也决定了什么能先显示。2026-09-24 用真实 DeepSeek V4 API 对 bash/run_code/write/edit 做了 310 次受控请求:
+
+| 变体 | 改了什么 | bash 首键为 description | run_code 首键为 description |
+|---|---|---|---|
+| 现状 | — | 0/20 | 1/20 |
+| 只调 `properties` 顺序 | description 提前 | 0/20 | 0/20 |
+| `properties` + `required` 同调 | 同上并调 `required` | 强制 tool_choice 10/10;自由调用 pro 7/10、flash 0/10 | 强制 10/10;自由 20/20 |
+| 同调 + 描述文本加一句顺序指令 | 追加「Provide `description` before `command` in the arguments.」 | 自由调用 flash 19/20、pro 20/20 | 20/20 |
+
+在这些样本中,单调 `properties` 无效时,调整 `required` 能改变参数顺序;V4 Flash 自由调用 bash 还需要显式指令。`dsh-tools` 的 schema DSL 按属性声明顺序生成 `required`,所以改源码声明顺序会同时调整两者。探针使用极简系统提示词,不是完整 Harness 提示词。
+
+## 决策
+
+### 懒计算的参数视图住在 `dsh-util-values`
+
+`PartialArguments` 是一个与工具无关的懒读器。它分别保留参数分片和顶层键值范围索引,或借用已解析的 PTC 参数。读取只索引新收到的边界;未请求的值不解码、不计数、不解析。请求字段时仅从该字段范围生成值,不需要每工具字段规格或注册表。
+
+边界扫描器跨分片跟踪引号、反斜杠奇偶和嵌套括号配对。键名为查找而解码,值内容延后处理。`complete()` 表示结束分隔符已到达,不代表内容已校验;`invalid` 报告索引或内容读取已经发现的错误。内容错误不阻止后续字段建立索引。正式工具入参校验仍位于此展示读器之外。
+
+被请求的无转义字符串长度直接由偏移量计算。转义字符串仅为已请求的长度、文本或有界前缀维护内容游标;上限查询在答案确定后停止。完整字符串首次被请求全文时使用原生解码。支持任意晚到的读取,需要保留原分片,直到权威完整文本替换它们。
+
+变化由视图自己判定:`append(delta)` 只保留分片,`refresh()` 在发布时比较已观察的答案。没人读过时,不扫描也不报告变化。只有 refresh 推进已有答案的比较基线;中途读取不能吞掉其他消费方的更新。业务在读的时候说明粒度——`stringLength('content', { step: 1024 })` 让跨 KB 才算变化,`text('description')` 让每个字符都算变化,`textPrefix()` 将解码文本限制在请求的 UTF-16 前缀内。edit 进度合计新旧文本时,`offset` 计入已完成的字符串。
+
+### 解析在 Tool Definition 层,不在 React
+
+[Tool Definition](../../../../packages/client/ui-chat/src/client/conversation-nodes/tool.ts) 把每个带 id 的 `tool-call-delta` 都匹配为 start 候选:最早的一个打开 Context(无论是否带名),后续折叠为 update。带名 delta 创建 preparing 根并挂上一个新的流式视图;每个 delta 只做 `args.append(delta)`。在 `animation-frame` 发布点,`buildViewNode()` 刷新已观察答案,仅在变化时替换根块,否则复用当前已发布根块。Context 和已发布 Node 持有这些身份,不另建准备阶段缓存。参数在名字之前到达的流不扫描:迟开的视图看到非对象前缀即 invalid、零字段。
+
+三个阶段的块都提供 `name` 和 `args`,原有 `argsRaw` 和 result 的 `call: { name, argsRaw } | null` 仍可使用。`block-end` 和 `tool/call` 通过长度及逐分片精确比较,将流式参数与权威全文核对。文本一致时封存原读器并保留字段索引,分片缺失或冲突时创建新的 `fromText()` 视图。不需要累加字符串或概率性 hash。PTC 读器使用 `fromObject(payload)`,结果复用已派发读器,未配对结果使用共享空视图。卡片模型继续读取最终 `argsRaw`。
+
+Assistant 块保留工具身份和首 token 时间,不保留参数 delta。完整参数文本来自 `block-end` 或持久消息。纯参数 delta 保持 Assistant State 不变,preparing 读取与通知由 Tool Definition 负责。
+
+Assembler 为同一输入和生命周期角色共享一个不可变 Match 及其 Location,各 Definition 仍保留独立 State。空依赖集合无需执行回放。
+
+工具行在各阶段使用相同读器:read/write/edit 在 `file_path` 闭合后显示可打开的路径;write/edit 在内容流入时按 1024 字符单位显示解码后长度;bash/pwsh/run_code 显示描述前缀。Chat 分组详情按既有字段优先级读取同一视图。没有可用详情时,只要字段还可能到达就留空;`closed()` 为真后才回退工具名,所有类别一致。
+
+分组详情先规范化 512 单元的解码前缀,仅当空白或多单元字素簇使其无法判定 160 字素簇的结果时才扩大前缀。前缀填满后,字段继续增长不会改变它,但稍后出现的更高优先级字段仍会发布新详情。字符串长度检查不会物化全文。详情不超过 160 个 UTF-16 单元时,无需遍历字素。
+
+三方工具无需注册或独立订阅就能取得同一视图。write/edit 和 Bash 在各阶段共用组件。可变参数读器是 owner 数据要求 JSON 兼容的一项限定例外:已读取答案变化时,所属 Definition 发布新的块引用。扫描缓存使用私有字段,同源视图的结构比较不受读取历史影响。
+
+### 参数声明顺序
+
+`tool-bash`、`tool-pwsh` 在 `command` 前声明 `description`,并包含顺序指令。`run_code` 两处 `parameters` 将 `description` 放在 `code` 前,共用的参数描述要求 TypeScript 和 Python 都按此顺序生成。write/edit 首先声明 `file_path`,并在路径参数描述中要求先生成它,再生成 `content` 或 `old_string`/`new_string`。read 保留首位的 `file_path`;其他工具保留原有参数顺序。
+
+## 考虑过的替代方案
+
+**在 hook 闭包里解析(选择器订阅 Step source)。** 只有订阅行能看到解析结果,分组标题需要另一份订阅,每行还要负责解析器生命周期。Definition 持有的视图让消费方共享已解析字段。
+
+**在 Definition 里维护每工具字段规格(内建表 + 运行时注册表)。** Definition 是纯函数、拿不到 ctx,运行时注册表只能经工厂闭包注入,三方工具要在坑位与注册表两处登记;而规格表本质只是"哪些字段保留文本",懒计算按需从原始文本切出原文后这条信息不再需要。
+
+**第三方 `partial-json`。** 每帧重解析累计输入,且不独立于解码值暴露字符串完成状态。
+
+**提前解析所有字段。** `content` 等大字段通常只需要长度;在消费方请求前构造全文,会增加解码和内存成本。观察整个键列表还会在无关参数出现时重新发布块,因此详情保持逐字段观察。
+
+**从规范化字段切出截断详情。** V8 的子字符串可能保留整个字段。拼接有界数量的字素簇,使展示前缀不再依赖该原文。
+
+**只调 `properties` 顺序。** 实测 0/40 生效;模型跟随 `required` 顺序。
+
+## 验证
+
+- 解析器测试覆盖懒扫描、已读取答案变化、空键与重复键、跨片转义、非字符串值、非法输入、封存视图,以及不受读取历史影响的结构相等。
+- 工具行和分组测试覆盖流式路径与描述、内容长度、共用行身份、参数不再增长后才回退工具名,以及不改变空白或 Unicode 处理语义的每次截断遍历最多 161 个字素簇。
+- 负控恢复读取时确认变化、重复分配 Match 和短文本字素遍历;四项聚焦测试在对应断言失败,恢复后通过。各 Definition 保留独立 State,Turn 与 Step 记录仍归各 Session 所有。
+- 另有负控提前解析未读容器、接受同长度的冲突文本,或为纯参数 delta 重建 Assistant State。每项都在所属断言失败,恢复边界索引实现后通过。
+
+必跑的 [conversation-fold benchmark](../../../../benchmarks/conversation-fold/conversation-fold.bench.client.ts) 使用真实 Tool Definition、Assembler、Chat 分组及发布后的 write 行读取,以 16 字符分片输入,每 64 片 flush。三个独立的已编译 Node worker 报告全部样本及中位数;准备工作和显式 GC 不计时,保留 Assembler 与参数视图后测量驻留堆。行读取校验解码进度与文件路径;worker 还报告进程峰值 RSS。
+
+2026-09-29 的本地 Chat 专项测量,以 512 KiB write 内容和 32,769 个正文 delta 对比功能改造前基线与优化实现。在 Linux x64、AMD EPYC 7763、Node 24.18.0 上,无探针处理耗时中位数为 212.16 → 176.12 ms,包含已回收对象的累计分配为 132.06 → 76.65 MiB,驻留堆为 24.89 → 21.84 MiB。测量不含模型、网络、浏览器渲染和非 Chat 目标,不代表整页或 CI runner 延迟。
+
+Match 调用保持 393,228 次;update 从 98,307 增至 131,076 次,因为 Tool Definition 消费每个参数 delta。基线按原文长度显示 write 进度,不读取解码后的参数。
+
+CI 预算保持 750/375 ms 和 37.5/10 MiB:300/150 ms 参考预期使用共享的 2× 时间倍率和 1.25× 余量,30/8 MiB 堆预期只使用余量。历史累计索引/全文分段负控中,write 耗时为 3984.9–4035.5 ms,bash 为 1471.2–1494.9 ms,均超过未调整的预算。
+
+## 影响
+
+- 已读取参数变化时替换准备态块,无人读取的调用保留块引用。视图保留源文本,分组标题可以独立于工具行请求字段。已派发和已结算视图在首次读取时扫描,不发布更新。
+- schema 顺序和描述指令引导模型先生成展示标签,但不保证生成顺序。工具行在字段可用时显示它;路径晚到会推迟路径及其后的内容进度展示。
+- 展示仍由 Client 派生;参数扫描不新增 Session 事件或持久化格式。
+- 名字晚于参数到达的流放弃扫描,该调用的准备行退回只显示标题。
diff --git a/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.i18n.yaml
new file mode 100644
index 0000000000..01a5ad7732
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.i18n.yaml
@@ -0,0 +1,22 @@
+# Bilingual-pair consistency record for 2026-09-24-preset-scoped-schedule-tools.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.md
+/agent-note-preset-scoped-schedule-tools:
+ en: c7dae89a43489b21
+ zh: f85a88544d7e2726
+/agent-note-preset-scoped-schedule-tools/problem:
+ en: 27b4d1d25cd1c178
+ zh: 3ddc4d9a09b6182d
+/agent-note-preset-scoped-schedule-tools/decision:
+ en: 3c4708981d34217d
+ zh: cd0c07dc0888f3db
+/agent-note-preset-scoped-schedule-tools/alternatives-considered:
+ en: 5cfe619038339caa
+ zh: c2d2651d304d867c
+/agent-note-preset-scoped-schedule-tools/consequences:
+ en: d7c7e6b8e6268559
+ zh: 801e5be70e00cc90
+/agent-note-preset-scoped-schedule-tools/testing:
+ en: d42cb866907a96b5
+ zh: 6e66f8babe744bcb
diff --git a/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.md b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.md
new file mode 100644
index 0000000000..169f3bea52
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.md
@@ -0,0 +1,34 @@
+# Agent Note: Preset-scoped Schedule tools
+
+Status: implemented
+
+English | [中文](2026-09-24-preset-scoped-schedule-tools.zh.md)
+
+## Problem
+
+`@deepseek-ai/dsh-schedule` registered `schedule_create`, `schedule_list`, `schedule_update`, and `schedule_delete` itself, attaching them to every live root Agent from an `agent/created` listener whose only filter was membership in `ctx.agents.roots()`. That predicate says nothing about the Agent preset, so `minimal` — composed for a capability-poor agent — carried all four schemas and paid their fixed request-context token cost. Storage, delivery, and the preset mechanism were each correct; the availability decision sat in the package that owns the storage service.
+
+## Decision
+
+`@deepseek-ai/dsh-tool-schedule` (`packages/schedule/tool-schedule`) contributes the four tools as a preset-level Consumer. It declares `inject = ['tools']` and registers the definitions on `ctx.tools` from inside `ctx.inject(['schedule'], …)`, so the mounting scope is the one that owns them and the registration waits for the Host Schedule service in that same scope. The shipped Web profile mounts the row in its `standard`, `cordis`, and `ptc` presets and omits it from `minimal`. Cordis effect ownership disposes the definitions with the mount, and a composition that never resolves `schedule` registers none of them. The [Web bundle](../../../../packages/bundle/web-app/README.md) owns the `schedule` and `ui-schedule` rows the shipped composition mounts; this record owns the reminder tools' preset placement.
+
+`@deepseek-ai/dsh-schedule` keeps the version-1 storage domain, the Host timer and serialized queue, Host delivery through the Session controller, the Automation page's backing reads, and the `ctx.schedule` interface. `dsh-tool-schedule` is the model-facing consumer of that interface: it checks selector and identity constraints before the service call, reads `exec.agent` for the Session binding, and maps failures that are not `ScheduleInputError` to `internal_error` so storage details never reach the model.
+
+## Alternatives considered
+
+**Keep registration in the Host service behind a `Config` flag.** An `exposeTools` field would move a composition choice into the storage plugin, where no preset can state it, and every preset would still need an edit to change the outcome.
+
+**Register the tools whenever `ctx.schedule` is present.** The shipped Web composition mounts the `schedule` Host service for the whole deployment, `minimal` included, so that condition restores the coupling between storage and model surface that this decision removes.
+
+## Consequences
+
+- `minimal` request headers and tool lists carry no reminder tool schema.
+- Each preset that mounts the row pays the four schemas' fixed token cost, and tool availability is read from the composition instead of inferred from the Host service's presence.
+- A deployment can mount `dsh-schedule` for storage and delivery without granting its agents model-driven reminder management.
+- `dsh-schedule` injects no `ctx.tools` and registers no model-facing tool.
+- A delegated child inherits the preset's four reminder schemas. `subagentCallerRefusal` in `dsh-tool-schedule` returns `{ code: 'subagent_session', message: 'A delegated subagent cannot use reminders.' }` from each tool when the caller's delegation depth is above zero. The [preset composition decision](../simplification/2026-10-09-preset-owned-subagent-composition.md) removes the redundant child filter; visibility does not authorize execution.
+- A Session a delegated child owns can never receive a delivered reminder, so `ScheduleService.create` and `ScheduleService.update` refuse it with `subagent_session`. The refusal reads the delegation depth — the accounting the delegation cap itself enforces, carried by the persisted session header across a cold resume. The rule sits in the service, so other consumers reach it too — the Automation surface included.
+
+## Testing
+
+`packages/schedule/tool-schedule/tests/tool-schedule.spec.ts` pins the four definitions and their error mapping, including `refuses every tool for a delegated child caller`. `apps/cli/tests/web-agent-presets.e2e.ts` asserts the `minimal` preset's tool list, and `snapshots/web/minimal-preset/tool-schemas.expected.json` records its tool table without reminder schemas. `packages/schedule/schedule/tests/subagent-ownership.spec.ts` pins the delegation-depth refusal, and `scripts/optional-bundles.spec.ts` asserts that both delegation rows in the three presets omit `toolFilter`. The Web preset e2e also checks equal parent/child tool schemas and refusal from all four reminder executors for fresh and seeded children. `snapshots/web/subagent-fork-schedule` records all four refusals in a seeded fork through the shipped Web composition and shares the standard preset's prompt and schema pins.
diff --git a/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.zh.md b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.zh.md
new file mode 100644
index 0000000000..75b41d8b9e
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.zh.md
@@ -0,0 +1,34 @@
+# Agent Note: 按 preset 提供的 Schedule 工具
+
+Status: implemented
+
+[English](2026-09-24-preset-scoped-schedule-tools.md) | 中文
+
+## Problem
+
+`@deepseek-ai/dsh-schedule` 自己注册 `schedule_create`、`schedule_list`、`schedule_update` 和 `schedule_delete`,在 `agent/created` 监听器中把它们附加到每个 live 根 Agent,唯一的过滤条件是 `ctx.agents.roots()` 的成员资格。该判据不涉及 Agent preset,因此为能力克制而组合的 `minimal` 也携带全部四个 schema,并承担其固定的请求上下文 token 成本。存储、投递和 preset 机制各自都是正确的;可用性决策落在了拥有存储服务的包里。
+
+## Decision
+
+`@deepseek-ai/dsh-tool-schedule`(`packages/schedule/tool-schedule`)以 preset 级 Consumer 的身份贡献这四个工具。它声明 `inject = ['tools']`,并在 `ctx.inject(['schedule'], …)` 内通过 `ctx.tools` 注册这些定义,因此拥有它们的是挂载它的作用域,而注册会等待同一作用域中的宿主 Schedule 服务。发布版 Web profile 在其 `standard`、`cordis` 和 `ptc` preset 中挂载该行,`minimal` 不挂载。Cordis 的 effect 所有权随挂载卸载而释放这些定义,而始终未解析出 `schedule` 的组合不会注册其中任何一个。[Web bundle](../../../../packages/bundle/web-app/README.zh.md)负责随发行版交付的组合挂载的 `schedule` 与 `ui-schedule` 两行;本记录负责提醒工具的 preset 归属。
+
+`@deepseek-ai/dsh-schedule` 保留版本 1 storage domain、宿主定时器与串行队列、经由 Session controller 的宿主投递、自动化任务页面的读取来源,以及 `ctx.schedule` 接口。`dsh-tool-schedule` 是该接口面向模型的消费者:它在调用服务前校验选择器与身份约束,从 `exec.agent` 读取 Session 绑定,并把非 `ScheduleInputError` 的失败映射为 `internal_error`,使存储细节不会到达模型。
+
+## Alternatives considered
+
+**继续在宿主服务中通过 `Config` 开关注册。** `exposeTools` 这类字段会把组合选择放进存储插件,而任何 preset 都无法在那里声明它,且每个 preset 仍要各自编辑才能改变结果。
+
+**只要 `ctx.schedule` 存在就注册这些工具。** 随发行版交付的 Web 组合为整个部署挂载 `schedule` 宿主服务,包括 `minimal`,因此该条件会恢复本决策所移除的、存储与模型界面之间的耦合。
+
+## Consequences
+
+- `minimal` 的请求头与工具列表不含任何提醒工具 schema。
+- 挂载该行的每个 preset 承担四个 schema 的固定 token 成本,工具可用性取自组合,而不是由宿主服务是否存在推断。
+- 部署可以只为存储与投递挂载 `dsh-schedule`,而不授予其 Agent 由模型驱动的提醒管理能力。
+- `dsh-schedule` 不注入 `ctx.tools`,也不注册任何面向模型的工具。
+- 被委派的子 agent 继承 preset 的四个提醒 schema。调用方委派深度大于零时,`dsh-tool-schedule` 中的 `subagentCallerRefusal` 从每个工具返回 `{ code: 'subagent_session', message: 'A delegated subagent cannot use reminders.' }`。[preset 组装决策](../simplification/2026-10-09-preset-owned-subagent-composition.zh.md)移除了重复的子级过滤;可见性不授予执行权限。
+- 被委派子 agent 拥有的 Session 永远收不到投递的提醒,因此 `ScheduleService.create` 与 `ScheduleService.update` 会以 `subagent_session` 拒绝它。该拒绝读取委派深度——委派封顶本身执行的记账,由持久化 session header 承载并跨冷恢复保留。这条规则在服务层,因此其它消费方(包括自动化任务页面)也会命中。
+
+## Testing
+
+`packages/schedule/tool-schedule/tests/tool-schedule.spec.ts` 固定这四个定义及其错误映射,包括 `refuses every tool for a delegated child caller`。`apps/cli/tests/web-agent-presets.e2e.ts` 断言 `minimal` preset 的工具列表,`snapshots/web/minimal-preset/tool-schemas.expected.json` 记录其不含提醒工具 schema 的工具表。`packages/schedule/schedule/tests/subagent-ownership.spec.ts` 固定委派深度拒绝,`scripts/optional-bundles.spec.ts` 断言三个 preset 的两行委派行都不含 `toolFilter`。Web preset e2e 还检查父子工具 schema 相等,且四个提醒执行器对新建和带种子的子级均拒绝执行。`snapshots/web/subagent-fork-schedule` 通过出货 Web 组合记录带种子 fork 中的四次拒绝,并共享 standard preset 的提示和 schema 预期。
diff --git a/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-time-context.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-time-context.i18n.yaml
new file mode 100644
index 0000000000..1ecbec2c31
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-time-context.i18n.yaml
@@ -0,0 +1,22 @@
+# Bilingual-pair consistency record for 2026-09-24-preset-scoped-time-context.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-24-preset-scoped-time-context.md
+/agent-note-preset-scoped-time-context:
+ en: dabc6e224d2af71c
+ zh: 81dafffcf9dcb0e2
+/agent-note-preset-scoped-time-context/problem:
+ en: 6b0d2bf60e97809f
+ zh: 8a32ad297b908ebb
+/agent-note-preset-scoped-time-context/decision:
+ en: 650d78ef725cd741
+ zh: 95dc34667246d2b5
+/agent-note-preset-scoped-time-context/alternatives-considered:
+ en: 3db88ad95898e9ee
+ zh: aa211bd55052331c
+/agent-note-preset-scoped-time-context/consequences:
+ en: 3d92e8f922a8d812
+ zh: 78078ba2c38f6727
+/agent-note-preset-scoped-time-context/testing:
+ en: 16a4911143027f71
+ zh: f945e85204aebf7e
diff --git a/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-time-context.md b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-time-context.md
new file mode 100644
index 0000000000..c10d4c0f43
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-time-context.md
@@ -0,0 +1,37 @@
+# Agent Note: Preset-scoped time context
+
+Status: implemented
+
+English | [中文](2026-09-24-preset-scoped-time-context.zh.md)
+
+## Problem
+
+A row in `packages/bundle/web-app/cordis.patch.yml` serves every preset in the profile, so a `time-context` row placed there would append one user-role message per eligible step to every preset, `minimal` included. The `minimal` preset composes only a persona and a persistent shell: it declares no reminder tool, and nothing in it turns the reading into a scheduled target, so that message would have no consumer in its composition.
+
+## Decision
+
+`packages/bundle/web-app/presets/standard.patch.yml`, `ptc.patch.yml`, and `cordis.patch.yml` each declare `time-context` among their Agent-context rows; `minimal.patch.yml` does not. `packages/bundle/web-app/cordis.patch.yml` carries no `time-context` row; it inserts `schedule` and `ui-schedule`. `packages/bundle/web-app/package.json` keeps declaring `@deepseek-ai/dsh-time-context`, which `verify-cordis-config` requires for the bare plugin name `presets/cordis.patch.yml` contributes.
+
+The boundary this states is that an injection carrying time belongs to the preset that consumes it. A preset decides whether its Agents receive a clock reading, so the reading travels with the reminder tools that consume it; the `schedule` Host service row and the `ui-schedule` client row are preset-independent surfaces, so the [Web bundle](../../../../packages/bundle/web-app/README.md) inserts them for the whole deployment. That composition owns which Host rows and client surfaces the `web` profile mounts; this record owns the reading's preset placement.
+
+The plugin is unchanged: it still appends the sampled instant, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message, at the configured minimum interval.
+
+## Alternatives considered
+
+**Keep the row in the Host row list.** `minimal` would keep receiving a model-visible message that nothing in its composition consumes, contradicting the rule that moved the reminder tools to preset declarations ([preset-scoped Schedule tools](2026-09-24-preset-scoped-schedule-tools.md)).
+
+**Declare the row in `minimal` too.** That would make the placement uniform across presets, but it keeps the same unused durable message in the capability-poor composition, so it pays the cost the change removes.
+
+**Give the Schedule Host service the clock instead.** The service validates an offset-bearing `at` value or an explicit `time_zone`; it reads no browser, Session, process, or model context, and the reading reaches the model only as prompt content. The Host service row therefore does not need the injection, and moving clock sampling into storage would couple two independently owned decisions.
+
+## Consequences
+
+- A `minimal` session appends no time-context message, so its model receives no clock reading and must ask for any date or time the user leaves unqualified.
+- `standard`, `cordis`, and `ptc` sessions keep the reading at the shipped 10-minute minimum interval, and the four reminder tools stay in the same three presets.
+- The reading is model-visible and durable, so it replays, compacts, and appears in exported Session logs; `snapshots/web/minimal-preset/session.v4.jsonl` no longer records one.
+- A profile patch layer cannot disable or reconfigure the row by id: `applyEntryPatches` reaches a loaded entry or a group's children, and a preset's declared plugins sit inside the preset row's `config.plugins`. Changing the row means restating that row, which is how the Web editor saves preset edits.
+- A preset mount reaches that preset's subagents, so a `standard`, `cordis`, or `ptc` child receives the reading its parent receives; the reminder tools in those presets stay denied to that child and refuse its calls.
+
+## Testing
+
+`apps/web/tests/schedule-after.e2e.ts` pins the shipped composition: the `schedule` and `ui-schedule` rows, `time-context` (`@deepseek-ai/dsh-time-context`) and `tool-schedule` (`@deepseek-ai/dsh-tool-schedule`) declared enabled once each in the `standard`, `ptc`, and `cordis` presets and absent from `minimal`, and no `time-context` row. Its every-step overlay restates `preset-standard`'s declared plugins instead of patching a row by id. `apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts` asserts the shipped composition carries the `schedule` and `ui-schedule` entries and no `time-context` entry.
diff --git a/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-time-context.zh.md b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-time-context.zh.md
new file mode 100644
index 0000000000..19cfe7c916
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-time-context.zh.md
@@ -0,0 +1,37 @@
+# Agent Note: 按 preset 归属的时间上下文
+
+Status: implemented
+
+[English](2026-09-24-preset-scoped-time-context.md) | 中文
+
+## Problem
+
+`packages/bundle/web-app/cordis.patch.yml` 中的一行会服务于 profile 中的每个 preset,因此把 `time-context` 行放在那里,会为每个 preset(包括 `minimal`)的每个符合条件的步骤追加一条 user 角色消息。`minimal` preset 只组合 persona 与 persistent shell:它不声明提醒工具,其中也没有任何东西会把该读数变成定时目标,因此那条消息在它的组合中没有消费者。
+
+## Decision
+
+`packages/bundle/web-app/presets/standard.patch.yml`、`ptc.patch.yml` 与 `cordis.patch.yml` 各自在其 Agent 上下文行中声明 `time-context`;`minimal.patch.yml` 不声明。`packages/bundle/web-app/cordis.patch.yml` 不含 `time-context` 行;它插入 `schedule` 与 `ui-schedule`。`packages/bundle/web-app/package.json` 继续声明 `@deepseek-ai/dsh-time-context`,这是 `verify-cordis-config` 对 `presets/cordis.patch.yml` 贡献的裸包名的要求。
+
+这里确立的边界是:携带时间的注入归属于消费它的 preset。由 preset 决定其 Agent 是否收到时钟读数,因此该读数与消费它的提醒工具同行;`schedule` 宿主服务行与 `ui-schedule` 客户端行是与 preset 无关的界面,因此由 [Web bundle](../../../../packages/bundle/web-app/README.zh.md)为整个部署插入它们。该组合负责 `web` profile 挂载哪些宿主行与客户端界面;本记录负责该读数的 preset 归属。
+
+插件本身没有改动:它仍按配置的最小间隔追加采样瞬时、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长。
+
+## Alternatives considered
+
+**把该行留在宿主行列表。** 这样 `minimal` 会继续收到其组合中无任何消费者使用的模型可见消息,与把提醒工具改为 preset 声明所依据的规则相矛盾([按 preset 提供的 Schedule 工具](2026-09-24-preset-scoped-schedule-tools.zh.md))。
+
+**在 `minimal` 中也声明该行。** 这会让各 preset 的归属看起来一致,但会在能力贫乏的组合中保留同一条无人使用的持久消息,因此它仍要付出本改动所消除的成本。
+
+**把时钟交给 Schedule 宿主服务。** 该服务校验带偏移量的 `at` 值或显式 `time_zone`;它不读取浏览器、Session、进程或模型上下文,读数只作为提示词内容到达模型。因此宿主服务行不需要该注入,把时钟采样搬进存储也会把两个各自独立归属的决策耦合起来。
+
+## Consequences
+
+- `minimal` 会话不再追加 time-context 消息,其模型收不到时钟读数,因此用户未明确限定的任何日期或时间都必须询问。
+- `standard`、`cordis` 与 `ptc` 会话保持发布版的 10 分钟最小间隔读数,且四个提醒工具仍留在这三个 preset 中。
+- 读数对模型可见且持久,因此它参与回放、压缩并出现在导出的 Session 日志中;`snapshots/web/minimal-preset/session.v4.jsonl` 不再记录它。
+- profile patch 层无法按 id 禁用或改配该行:`applyEntryPatches` 只能触达已加载的条目或某个 group 的子项,而 preset 声明的插件位于 preset 行的 `config.plugins` 内。改动该行意味着重述该行,这也是 Web 编辑器保存 preset 编辑的机制。
+- preset 挂载会覆盖该 preset 的子 agent,因此 `standard`、`cordis` 或 `ptc` 的子 agent 会收到与其父级相同的读数;这些 preset 中的提醒工具仍对该子 agent 保持 deny,并拒绝其调用。
+
+## Testing
+
+`apps/web/tests/schedule-after.e2e.ts` 固化随发行版交付的组合:`schedule` 与 `ui-schedule` 两行,`time-context`(`@deepseek-ai/dsh-time-context`)与 `tool-schedule`(`@deepseek-ai/dsh-tool-schedule`)在 `standard`、`ptc` 与 `cordis` preset 中各声明一次且未禁用,也不在 `minimal` 中,且不含 `time-context` 行。其每步读数 overlay 通过重述 `preset-standard` 声明的插件实现,而不是按 id patch 该行。`apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts` 断言随发行版交付的组合含 `schedule` 与 `ui-schedule` 条目,且不含 `time-context` 条目。
diff --git a/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.i18n.yaml
deleted file mode 100644
index ca6497cb81..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.i18n.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Bilingual-pair consistency record for 2026-09-24-schedule-opt-in-optional-bundle.md (docs/i18n/README.md): per heading
-# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
-# After editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.md
-/agent-note-schedule-as-an-opt-in-optional-bundle:
- en: 8c2381fd778a9888
- zh: 8877255cb980eea1
-/agent-note-schedule-as-an-opt-in-optional-bundle/problem:
- en: bb23c2f7f93a462b
- zh: 9fb600fc8e203323
-/agent-note-schedule-as-an-opt-in-optional-bundle/decision:
- en: 483d1dd51b9cc073
- zh: b069e2e2385c8132
-/agent-note-schedule-as-an-opt-in-optional-bundle/alternatives-considered:
- en: 8e5f4d8fa8c2e78d
- zh: 3353c7042c6a1d6b
-/agent-note-schedule-as-an-opt-in-optional-bundle/consequences:
- en: 36cd0eb71abeedbe
- zh: 7c134095cc19fcfd
diff --git a/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.md b/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.md
deleted file mode 100644
index 865f97848d..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Agent Note: Schedule as an opt-in optional bundle
-
-Status: implemented
-
-English | [中文](2026-09-24-schedule-opt-in-optional-bundle.zh.md)
-
-## Problem
-
-Schedule adds four tool schemas to every live root Agent request and one durable clock message per eligible step, so the shipped Web composition must not mount it by default. A person using the shipped Web profile still needs a product-surface switch to turn it on; a profile patch layer or a `--patch` overlay is a configuration file that person cannot reach.
-
-## Decision
-
-`packages/bundle/web-app/cordis.patch.yml` carries none of `time-context`, `schedule`, and `ui-schedule`, so the shipped Web composition exposes no Schedule surface: no `schedule_*` tools, no Session reminder catalog, no Automation tasks page, and no per-step clock reading.
-
-`@deepseek-ai/dsh-experimental-schedule-bundle` (`packages/experimental/schedule-bundle/`) inserts the three rows in its `cordis.patch.yml` and depends on their packages, as every other optional bundle inserts the rows it ships. `OPTIONAL_BUNDLES` in `packages/boot/app-boot/src/profile.ts` names the package, and `apps/cli` declares it as a runtime dependency, so every installation ships it switched off. The [experimental-as-optional-bundles decision](2026-09-21-experimental-capabilities-as-optional-bundles.md) owns the `OPTIONAL_BUNDLES` conventions and the localized `icon` and `meta.title` / `meta.description` metadata the Web Plugins page renders in its Official group.
-
-Enabling the bundle inserts `time-context` (a per-step clock reading with the sampled instant, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message), `schedule` (durable reminders plus `schedule_create`, `schedule_list`, `schedule_update`, and `schedule_delete` on live root Agents), and `ui-schedule` (the Session reminder catalog and the Automation tasks page). The bundle carries `time-context` with `schedule` because a reminder request states a wall-clock target: the sampled instant is what lets the model turn a request such as "tomorrow at nine" into an offset-bearing `at` value. The [Schedule subsystem](../../../../docs/subsystems/schedule.md) owns the durable records, delivery, and management operations; the [Web bundle](../../../../packages/bundle/web-app/README.md) owns the composition the bundle adds the rows to. Disabling the bundle restores the shipped composition, and the Host stops scheduling while the `schedule` row is absent; stored task records remain in the Schedule domain.
-
-## Alternatives considered
-
-**Ship Schedule on by default.** Every Web session then pays four tool schemas in each request header and one durable user message per eligible step, and a conversation that never creates a reminder pays both costs. The arrangement also left the capability without an off switch on the product surface.
-
-**Keep a `--patch` overlay file.** An overlay is a launch-time argument rather than a product-surface switch, so a person using the shipped Web profile cannot reach it. Re-declaring the rows through its `insert` list does not override them: `applyEntryPatches` appends that list without de-duplicating ids, and the Loader collapses the composed list to one entry per id with the last declaration winning, so the overlay replaces the Web bundle's rows instead of setting fields on them.
-
-**Keep the rows in the Web composition with `disabled: true` and switch them on with id-targeted patches.** The bundle would then set fields on existing rows, but the plugin manager lists only the rows a bundle inserts, so the bundle's page reported no components and needed a second listing path for overridden rows, while every other optional bundle inserts its rows. A profile that also declares one of the ids resolves the same way as for any other bundle: the Loader keeps the last declaration.
-
-**Lock the three rows to the bundle switch.** A bundle manifest field that makes the plugin manager refuse to switch the rows one by one adds a plugin-manager concept only this bundle would use. The bundle's page offers a switch per row, as for every other bundle, and the bundle README records that the three rows work only together.
-
-## Consequences
-
-- A default `dsh web` session carries no Schedule tool schemas, no Session reminder catalog, no Automation tasks page, and no per-step clock reading. An installation that wants them enables the optional bundle from the Plugins page or lists the package in a profile's `dsh.profile.bundles`.
-- An enabled installation adds four tool schemas to every live root Agent and one durable clock message per eligible step; the reading is model-visible and durable, so it replays, compacts, and appears in exported Session logs like any other user message.
-- The Plugins page lists the three rows under the bundle with the titles their packages' `locale/*.json` declare, their state, and, while the bundle is on, a switch per row; switching one of them off leaves Schedule without that part.
-- A profile patch or `--patch` overlay that targets one of the three ids matches no row while the bundle is not selected, and the loader warns `patch: entry not found` for it; selecting the bundle replaces switching the rows on by id.
-- The switch is configuration-only: `src/index.ts` is an empty module, the patch carries the runtime content, and the package owns no mutable runtime state, so it publishes no invariant companion.
diff --git a/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.zh.md b/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.zh.md
deleted file mode 100644
index a90516606a..0000000000
--- a/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.zh.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Agent Note: Schedule 作为按需开启的可选 bundle
-
-Status: implemented
-
-[English](2026-09-24-schedule-opt-in-optional-bundle.md) | 中文
-
-## Problem
-
-Schedule 会给每个活跃根 Agent 的请求增加四个工具 schema,并在每个符合条件的步骤追加一条持久时钟消息,因此随发行版交付的 Web 组合不能默认挂载它。使用随发行版 Web profile 的人仍需要产品界面上的开关来打开它;profile patch 层或 `--patch` overlay 都是这个人无法触及的配置文件。
-
-## Decision
-
-`packages/bundle/web-app/cordis.patch.yml` 不含 `time-context`、`schedule` 与 `ui-schedule` 中的任何一行,因此随发行版交付的 Web 组合不提供任何定时能力:没有 `schedule_*` 工具、没有 Session 提醒目录、没有自动化任务页面,也没有逐步时钟读数。
-
-`@deepseek-ai/dsh-experimental-schedule-bundle`(`packages/experimental/schedule-bundle/`)在其 `cordis.patch.yml` 中插入这三行并依赖它们的包,与其他可选 bundle 插入各自随附的行相同。`packages/boot/app-boot/src/profile.ts` 的 `OPTIONAL_BUNDLES` 列出该包,`apps/cli` 将其声明为运行时依赖,因此每次安装都随包携带且默认关闭。[实验性能力作为可选 bundle 的决策](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)负责 `OPTIONAL_BUNDLES` 的约定,以及 Web 插件管理页在“官方”分组中渲染的本地化 `icon` 与 `meta.title` / `meta.description` 元数据。
-
-启用该 bundle 会插入 `time-context`(每个步骤的时钟读数,包含采样瞬时、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长)、`schedule`(持久提醒,以及每个活跃根 Agent 上的 `schedule_create`、`schedule_list`、`schedule_update` 与 `schedule_delete`)与 `ui-schedule`(Session 提醒目录与自动化任务页面)。该 bundle 把 `time-context` 与 `schedule` 一起携带,因为提醒请求给出的是挂钟时间目标:采样瞬时正是让模型把“明天九点”这类请求转换成带偏移量的 `at` 值的依据。持久记录、投递与管理操作由 [Schedule 子系统](../../../../docs/subsystems/schedule.zh.md)负责;该 bundle 向其加入这些行的组合由 [Web bundle](../../../../packages/bundle/web-app/README.zh.md) 负责。禁用该 bundle 会恢复随发行版交付的组合,`schedule` 行不存在期间 Host 不再调度;已存储的任务记录保留在 Schedule domain 中。
-
-## Alternatives considered
-
-**默认发布 Schedule。** 那样每个 Web 会话都要在每个请求头中带上四个工具 schema,并在每个符合条件的步骤追加一条持久 user 消息,从不创建提醒的对话也要承担这两项成本。这种安排还让该能力在产品界面上没有关闭开关。
-
-**保留一个 `--patch` overlay 文件。** overlay 是启动时的参数,而不是产品界面上的开关,因此使用随发行版 `web` profile 的人无法触及它。通过它的 `insert` 列表重新声明这些条目并不能覆盖它们:`applyEntryPatches` 追加该列表时不对 id 去重,而 Loader 会把组合出的列表按 id 收敛为一个条目、最后一个声明生效,因此该 overlay 会替换 Web Bundle 的条目,而不是给它们设值。
-
-**把这三行以 `disabled: true` 留在 Web 组合中,用按 id 定位的 patch 打开。** 这样 bundle 只是给已有的行设值,但插件管理器只列出 bundle 插入的行,因此该 bundle 的页面显示不含任何组件,需要为被覆盖的行另建一条列出路径;而其他可选 bundle 都插入各自的行。某个 profile 若也声明了其中某个 id,处理方式与任何其他 bundle 相同:Loader 保留最后一份声明。
-
-**把这三行锁定在 bundle 开关上。** 让插件管理器拒绝逐行开关的 bundle 清单字段,会给插件管理器增加一个只有这个 bundle 使用的概念。该 bundle 的页面像其他所有 bundle 一样为每行提供开关,由 bundle README 说明这三行只能一起工作。
-
-## Consequences
-
-- 默认的 `dsh web` 会话不带 Schedule 工具 schema、Session 提醒目录、自动化任务页面与逐步时钟读数。需要它们的安装可从插件管理页启用该可选 bundle,或把该包列进 profile 的 `dsh.profile.bundles`。
-- 启用后的安装会给每个活跃根 Agent 增加四个工具 schema,并在每个符合条件的步骤追加一条持久时钟消息;该读数对模型可见且持久,因此它与其他 user 消息一样参与回放、压缩,并出现在导出的 Session 日志中。
-- 插件管理页在该 bundle 下列出这三行,显示各自包的 `locale/*.json` 声明的标题、它们的状态,并在 bundle 打开时为每行提供开关;关掉其中一行会让 Schedule 缺少这一部分。
-- 未选中该 bundle 时,按 id 定位这三行之一的 profile 补丁或 `--patch` overlay 匹配不到任何条目,加载器会为它报告 `patch: entry not found` 警告;选中该 bundle 即可取代按 id 打开这些行。
-- 该开关只涉及配置:`src/index.ts` 是空模块,运行时内容由 patch 承载,包本身不拥有可变的运行时状态,因此不发布不变量伴随模块。
diff --git a/.agents/notes/implemented/architecture/2026-09-24-scheduled-message-framing.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-scheduled-message-framing.i18n.yaml
new file mode 100644
index 0000000000..8dcfb5a6a2
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-scheduled-message-framing.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-09-24-scheduled-message-framing.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-24-scheduled-message-framing.md
+/agent-note-delivered-reminders-are-framed-as-scheduled-messages-from-the-user:
+ en: f524c3b0ee74c586
+ zh: ec4e85f0fd853532
+/agent-note-delivered-reminders-are-framed-as-scheduled-messages-from-the-user/problem:
+ en: c4156e2a8a3ad379
+ zh: 992b76509f83d61f
+/agent-note-delivered-reminders-are-framed-as-scheduled-messages-from-the-user/decision:
+ en: b9b203da394f58e5
+ zh: ac511458d60dab49
+/agent-note-delivered-reminders-are-framed-as-scheduled-messages-from-the-user/alternatives-considered:
+ en: 8074a2b9b9f5d782
+ zh: 1604b7ee5d34375e
+/agent-note-delivered-reminders-are-framed-as-scheduled-messages-from-the-user/consequences:
+ en: 85e03b541bfce57c
+ zh: 77decbb804f6a0b9
diff --git a/.agents/notes/implemented/architecture/2026-09-24-scheduled-message-framing.md b/.agents/notes/implemented/architecture/2026-09-24-scheduled-message-framing.md
new file mode 100644
index 0000000000..906edabec9
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-scheduled-message-framing.md
@@ -0,0 +1,29 @@
+# Agent Note: Delivered reminders are framed as scheduled messages from the user
+
+Status: implemented
+
+English | [中文](2026-09-24-scheduled-message-framing.zh.md)
+
+## Problem
+
+Due reminders reach their original Session as user-role messages with producer kind `schedule`. Before this decision, both renderers opened that message with an instruction about how to treat its payload: one-shot delivery asked the model to present `reminder_prompt_json` as untrusted reminder content and not as new user instructions, and recurring batch delivery asked the same for every `reminder_prompt` in `reminders_json`. The message therefore asserted that its own content was untrusted instead of naming where it came from.
+
+## Decision
+
+`renderReminderFraming` and `renderRecurringReminderBatchFraming` in [`packages/schedule/schedule/src/domain.ts`](../../../../packages/schedule/schedule/src/domain.ts) both emit one shared fixed line, `SCHEDULED_MESSAGE_FRAMING`: `This is a scheduled message from the user`. It is the block's second line, after the bracketed marker (`[SCHEDULE REMINDER]`, `[SCHEDULE REMINDER BATCH]`), and the dynamic field lines are unchanged: a one-shot message still appends `schedule_id_json`, `occurrence_at`, and `reminder_prompt_json`, and a recurring batch still appends `reminders_json`.
+
+The fixed line states the origin of the message: a schedule bound to that Session. A model reading a user-role message whose fixed block carries it reads the following reminder payload (`reminder_prompt_json` in a one-shot message, `reminders_json` in a batch) as content the user scheduled, and the Host cannot attest that the user typed it: `schedule_create` and `schedule_update` let the model write the prompt, and a model-written prompt can carry text the model read from the web, files, or command output. The previous line marked that payload untrusted, and this decision drops that instruction, so the change accepts the residual risk that model-authored text arrives under a user-origin line. Reintroduction condition: a demonstrated injection through a model-written prompt restores a clause that scopes the payload as reminder text rather than a new instruction, and the five pinned framing assertions in the package specs plus `expectReminderFraming` in the Web e2e are the check that such a change is deliberate. Framing-line forgery is closed by encoding: the schedule id and prompt are JSON strings, the occurrence instant is a validated canonical RFC 3339 value, and each occupies one line, so a prompt carrying embedded newlines or an `occurrence_at:`-style line cannot add framing lines to the block. The [Host-owned scheduled messages](2026-09-16-host-schedule-storage.md) note keeps the rest of its decision, including delivery through the Session controller and the saved delivery records.
+
+## Alternatives considered
+
+**Keep the untrusted-content instruction.** It asserted that the prompt was untrusted input, which the delivered message does not need: the prompt is written in the Session the schedule is bound to — by the model through `schedule_create` and `schedule_update`, or by the user in the Web task detail — and the delivery is an ordinary user-role message there.
+
+**Change only the recurring batch line.** One-shot and recurring delivery would then disagree about the origin of the same kind of message, and each later wording change would need both renderers edited separately. Both paths now read one constant.
+
+## Consequences
+
+Model-visible reminder text changes for both delivery paths, so a Session log recorded before this decision reconstructs the previous framing, and the Web notice that renders a delivered message shows the new line when expanded. Nothing else moves: no session event, storage field, client code, protocol, or durable format changes, and the delivered text remains reconstructable from the log.
+
+The prompt text carries no authorship guarantee. It is written in the Session the schedule is bound to, by the model through `schedule_create` and `schedule_update` or by the user in the Web task detail, and a model-written reminder can quote text the model read from the web, files, or command output. The task's Session binding scopes which Session's model reaches the task; it is not caller authorization and says nothing about where the prompt content came from. The Decision holds the accepted risk and its reintroduction condition.
+
+No recorded-session snapshot covers a delivered reminder, and this change adds none: no committed fixture contains a `schedule` message, and `snapshots/web/schedule-catalog` records catalog queries only. Two harness capabilities are missing for such a fixture: no scenario patch controls the wall clock that `packages/schedule/schedule/src/runtime.ts` reads for delivery, and the snapshot harness seeds Sessions rather than the schedule storage domain that holds Host tasks, so those rows exist only where a Web test writes them by hand. This change records the missing harness support instead of a fixture. The delivered text is pinned by the five full-string assertions in `packages/schedule/schedule/tests/{domain,daily,weekly,cron}.spec.ts` and by `expectReminderFraming` in `apps/web/tests/schedule-after.e2e.ts`, which asserts the line in the assembled request for the one-shot and recurring paths.
diff --git a/.agents/notes/implemented/architecture/2026-09-24-scheduled-message-framing.zh.md b/.agents/notes/implemented/architecture/2026-09-24-scheduled-message-framing.zh.md
new file mode 100644
index 0000000000..7aed47c1d0
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-24-scheduled-message-framing.zh.md
@@ -0,0 +1,29 @@
+# Agent Note: 投递的提醒以「用户安排的定时消息」为固定文本
+
+Status: implemented
+
+[English](2026-09-24-scheduled-message-framing.md) | 中文
+
+## Problem
+
+到期的提醒以生产者 kind 为 `schedule` 的 user-role 消息进入其原 Session。在本决策之前,两个渲染器都用一句关于如何处理其载荷的固定指令开篇:单次投递要求模型把 `reminder_prompt_json` 作为不可信的提醒内容呈现、而不是新的用户指令;周期批次投递对 `reminders_json` 中的每个 `reminder_prompt` 提出同样的要求。于是这条消息断言自身内容不可信,而没有说明它来自哪里。
+
+## Decision
+
+[`packages/schedule/schedule/src/domain.ts`](../../../../packages/schedule/schedule/src/domain.ts) 中的 `renderReminderFraming` 与 `renderRecurringReminderBatchFraming` 都输出同一条固定行,即模块常量 `SCHEDULED_MESSAGE_FRAMING`:`This is a scheduled message from the user`;它是该块的第二行,位于方括号标记(`[SCHEDULE REMINDER]`、`[SCHEDULE REMINDER BATCH]`)之后。动态字段行不变:单次消息仍追加 `schedule_id_json`、`occurrence_at`、`reminder_prompt_json`,周期批次仍追加 `reminders_json`。
+
+这条固定行说明消息的来源:绑定到该 Session 的一条定时任务。模型读到固定块中带有它的 user-role 消息时,会把随后的提醒载荷(单次消息的 `reminder_prompt_json`、批次的 `reminders_json`)当作用户安排的内容,而 Host 无法证明该文本由用户键入:`schedule_create` 与 `schedule_update` 允许模型写入提示词,模型写入的提示词可以带上模型从网页、文件或命令输出中读到的文本。旧固定行把该载荷标记为不可信,本决策有意去掉这一指令,因此接受「模型生成的文本以用户来源行投递」这一残留风险。再引入条件:一旦出现经由模型写入提示词完成注入的实例,就恢复一句把载荷限定为提醒文本、而非新指令的说明;包内五处固定断言与 Web e2e 的 `expectReminderFraming` 是「此类改动必须是有意为之」的检查。固定行无法被动态字段伪造,这由编码保证:schedule id 与提示词是 JSON 字符串,发生时点是经校验的规范 RFC 3339 值,且各自只占一行,因此带有换行或形如 `occurrence_at:` 的提示词无法向该块增加固定行。[Host 拥有的定时消息](2026-09-16-host-schedule-storage.zh.md) 记下的其余决策不变,包括通过 Session controller 投递以及保存的投递记录。
+
+## Alternatives considered
+
+**保留「不可信内容」指令。** 它断言提示词是不可信输入,而投递的消息并不需要这一断言:提示词写在绑定该定时任务的 Session 内——由模型通过 `schedule_create` 与 `schedule_update` 写入,或由用户在 Web 任务详情中写入——投递本身就是该 Session 中的普通 user-role 消息。
+
+**只改周期批次那一行。** 单次与周期投递会对同类消息的来源给出不同说法,此后每次改措辞都要分别改两个渲染器。现在两条路径读同一个常量。
+
+## Consequences
+
+两个投递路径的模型可见提醒文本都改变,因此在该决策之前记录的 Session 日志重建出的是旧的固定文本;Web 中渲染该投递消息的通知在展开时显示新的一行。其余不动:没有 session 事件、存储字段、客户端代码、协议或持久化格式变化,投递文本仍可从日志重建。
+
+提示词文本不携带作者保证。它写在绑定该定时任务的 Session 内——由模型通过 `schedule_create` 与 `schedule_update` 写入,或由用户在 Web 任务详情中写入——模型创建的提醒可以把模型从网页、文件或命令输出中读到的文本原样写入。任务的 Session 绑定限定的是哪个 Session 的模型能访问该任务;它不是调用方鉴权,也不说明提示词内容的来源。本决策中记下该残留风险与再引入条件。
+
+没有录制会话快照覆盖提醒投递,本次也没有新增:已提交夹具中没有 `schedule` 消息,`snapshots/web/schedule-catalog` 只记录任务查询。构成该夹具还缺两项 harness 能力:没有场景补丁能控制 `packages/schedule/schedule/src/runtime.ts` 读取的墙上时钟;快照 harness 播种的是 Session,而不是承载 Host 任务的 schedule storage domain,那些任务行只在 Web 测试里手工写入。因此本次记录缺失的 harness 支持,而不新增夹具。投递文本改由 `packages/schedule/schedule/tests/{domain,daily,weekly,cron}.spec.ts` 中五处整串断言,以及 `apps/web/tests/schedule-after.e2e.ts` 中在装配后请求上断言该行、覆盖单次与周期两条路径的 `expectReminderFraming` 固定。
diff --git a/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.i18n.yaml
index 15f69188be..c72fdec87f 100644
--- a/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.i18n.yaml
@@ -9,8 +9,8 @@
en: 20f659a15fb55352
zh: 49d39c95da1b8aa5
/agent-note-session-log-otlp-strings-and-bounded-requests/decision:
- en: 0f0866232326658b
- zh: aa608d17ba712d8a
+ en: 6e18311171c724bd
+ zh: 0214676ea3ee769c
/agent-note-session-log-otlp-strings-and-bounded-requests/alternatives-considered:
en: 0dc933b18a1a3573
zh: 77655740322c4e45
diff --git a/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.md b/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.md
index 57fbe72dc5..686df954fd 100644
--- a/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.md
+++ b/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.md
@@ -14,7 +14,7 @@ The shared `otel` Cordis plugin owns ordinary-event and Session-log transport im
Session logs use a separate provider and byte/count queue. Each record is measured once with the SDK serializer including a full envelope; the sum conservatively bounds a combined request. Greedy packing avoids full-candidate serialization and recursive re-encoding. Single oversized records receive one diagnostic and do not block later events. One HTTP request owns the transport slot until settlement, even after its watchdog fires; shutdown expiry stops the remaining queue. Scope name/version and explicit transport options remain owned by the backend.
-The [telemetry revival](../feature/2026-07-23-session-telemetry-otel-revival.md) remains authoritative for redaction, capture, and best-effort handoff. This decision replaces its SDK-only queue/batching rule: SDK batch deadlines cannot own an operation split into multiple HTTP requests. The shared Session-log channel schedules complete requests; the SDK still owns transport/retry. Feedback authorization and the separate DeepSeek model-request contribution remain unchanged.
+The [telemetry revival](../../../../packages/session/session-telemetry/README.md) remains authoritative for redaction, capture, and best-effort handoff. This decision replaces the earlier SDK-only queue/batching rule: SDK batch deadlines cannot own an operation split into multiple HTTP requests. The shared Session-log channel schedules complete requests; the SDK still owns transport/retry. Feedback authorization and the separate DeepSeek model-request contribution remain unchanged.
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.zh.md b/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.zh.md
index f6e8306978..947f5cf129 100644
--- a/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.zh.md
@@ -14,7 +14,7 @@ Session 日志包含嵌套 JSON 和大型工具结果。产品采集端接收带
Session 日志使用独立 provider 和字节/条数队列。每条记录使用 SDK 序列化器计量一次,包含完整 envelope;其总和保守约束合并请求大小。顺序组包避免候选批次的完整序列化和递归重复编码。单条超限记录只产生一次诊断,不阻塞后续事件。每个 HTTP 请求在结束前始终占有传输槽位,即使其监测期限已经触发;关闭期限到期后停止剩余队列。scope 名称/版本和显式传输选项仍由后端拥有。
-[遥测恢复决策](../feature/2026-07-23-session-telemetry-otel-revival.zh.md) 继续约束脱敏、捕获和尽力交接。本决策替代其仅使用 SDK 队列/聚合的规则:SDK 批次期限无法拥有拆成多个 HTTP 请求的操作。共享 Session 日志通道调度完整请求,SDK 仍负责传输和重试。反馈授权和独立的 DeepSeek 模型请求附加路径不变。
+[遥测恢复参考](../../../../packages/session/session-telemetry/README.zh.md) 继续约束脱敏、捕获和尽力交接。本决策替代先前仅使用 SDK 队列/聚合的规则:SDK 批次期限无法拥有拆成多个 HTTP 请求的操作。共享 Session 日志通道调度完整请求,SDK 仍负责传输和重试。反馈授权和独立的 DeepSeek 模型请求附加路径不变。
## 考虑过的替代方案
diff --git a/.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.i18n.yaml
new file mode 100644
index 0000000000..a5e3035bd0
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-09-26-cua-application-consent.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-26-cua-application-consent.md
+/agent-note-application-consent-for-native-computer-use:
+ en: d570ebf4790311f9
+ zh: a6ec87cb3ae3d0f1
+/agent-note-application-consent-for-native-computer-use/problem:
+ en: b32a7ce6483b9b48
+ zh: ec0938ad032b99d8
+/agent-note-application-consent-for-native-computer-use/decision:
+ en: 92247ff599c1103f
+ zh: 26845420a9711c83
+/agent-note-application-consent-for-native-computer-use/alternatives-considered:
+ en: 4693dcd217c4fedb
+ zh: 1dc9bf9067314006
+/agent-note-application-consent-for-native-computer-use/consequences:
+ en: 0146a69ab2086edc
+ zh: 4257bc1ab4432c3f
diff --git a/.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.md b/.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.md
new file mode 100644
index 0000000000..3fb91170e5
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.md
@@ -0,0 +1,38 @@
+# Agent Note: Application consent for native Computer Use
+
+Status: implemented
+
+English | [中文](2026-09-26-cua-application-consent.zh.md)
+
+## Problem
+
+macOS privileges authorize an execution host, not a user's choice of target application. Retained app and window objects can outlive that choice. Concurrent permission decisions must not restore access after the user revokes it.
+
+## Decision
+
+The CUA executor owns application consent for every object-API and native-tool dispatch. It uses the existing User Questions service for human decisions and the provider's existing profile configuration for saved grants. General tool approval and operating-system privileges remain separate requirements. Arbitrary persistent PTC code and separately authorized shell tools retain their own execution authority.
+
+A saved identity combines the canonical executable path with a verified designated signing requirement, represented by a SHA-256 digest. The path prevents authorization from following a copied application, and the signing requirement distinguishes replacements while allowing matching signed updates. Unverifiable and ad-hoc applications receive task access only, tied to a process instance and Agent connection lifetime. Labels do not establish identity.
+
+Cold application selection prepares an executable identity and handle without launching. The same consent decision guards the private launch continuation, and the helper checks the executable again before launch and binds the resulting process afterward. Signed selections retain the normal designated-requirement identity; an unverifiable executable uses a content identity for this pending selection and cannot create a saved grant. Preparing metadata before asking avoids starting an application the user refuses.
+
+Selections of the same application within one task are serialized, so a pending question does not produce competing decisions. Saved-grant additions read the current configuration inside one mutation queue. Refusals prevent another prompt within the task; an explicitly restored saved grant takes precedence at dispatch.
+
+Removing a saved grant invalidates Agent connections retaining the application's handles and cancels active operations; Agent/provider disposal awaits native connection cleanup. Reauthorization uses a fresh connection, so old app and window snapshots cannot become valid again. The [native object decision](2026-09-17-experimental-cua-object-api.md) continues to own process identity, coordinate semantics, and separation from the evaluator.
+
+## Alternatives considered
+
+**Trust model instructions or hide controls.** Retained objects and direct tool calls can still reach native operations. The executor must enforce the decision at dispatch.
+
+**Persist application names or unsigned paths.** A replacement can reuse both. Durable grants require a verifiable signing identity; task-only access supports unsigned development applications without that promise.
+
+**Add an approval database or Session control stream.** Profile settings already own user configuration, and User Questions already owns interactive answers.
+
+**Keep the native context after revocation.** Its application lookup can return an earlier handle and retain window snapshots. Closing the Agent connection discards its native context through existing transport cleanup; preserving unrelated handles would require a native per-application retirement protocol.
+
+## Consequences
+
+Revoking one application also invalidates other application handles on the same Agent connection. Those applications require fresh selection; saved grants remain reusable, while task grants expire. Other tasks with independent connections remain unaffected unless they also hold access to the revoked application.
+
+
+Application decisions and failure results are exercised through policy regressions and the recorded CUA scenario. Desktop binary distribution, signing, and packaged-host permissions remain outside the source-launch workflow.
diff --git a/.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.zh.md b/.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.zh.md
new file mode 100644
index 0000000000..73d4640db7
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-26-cua-application-consent.zh.md
@@ -0,0 +1,38 @@
+# Agent Note: 原生 Computer Use 的应用授权
+
+Status: implemented
+
+[English](2026-09-26-cua-application-consent.md) | 中文
+
+## 问题
+
+macOS 权限授权执行宿主,不能表达用户允许操作哪些目标应用。保留的应用和窗口对象可能比用户的许可存活更久。并发授权决定不能在用户撤销后恢复访问。
+
+## 决策
+
+CUA 执行器在每次对象 API 和原生工具调用时检查应用授权。人工决定复用 User Questions 服务,长期许可保存在 provider 的现有 profile 配置中。通用工具审批和操作系统权限仍是独立要求。任意 persistent PTC 代码和另行授权的 shell 工具保留各自的执行权限。
+
+长期身份由规范化可执行文件路径和经过验证的签名指定要求组成,以 SHA-256 摘要表示。路径阻止许可跟随应用副本,签名要求区分替换程序,并允许符合相同签名要求的更新。签名不可验证或使用 ad-hoc 签名的应用只能获得任务授权,许可绑定进程实例和 Agent 连接生命周期。显示名称不用于确定身份。
+
+选择尚未运行的应用时,先准备可执行文件身份和句柄,不立即启动。同一授权决定保护内部启动续接,helper 在启动前再次核对可执行文件,并在启动后绑定实际进程。已签名应用沿用签名指定要求身份;签名不可验证的程序使用本次待启动选择的内容身份,不能生成长期许可。先准备元数据再询问,可以避免启动用户拒绝的应用。
+
+同一任务对同一应用的选择串行执行,避免未完成的问题产生相互冲突的决定。长期许可的新增在同一写入队列内读取当前配置。拒绝会阻止任务内的再次询问;用户显式恢复的长期许可在执行检查时优先于旧拒绝记录。
+
+删除长期许可会使保留目标应用句柄的 Agent 连接失效,并取消活动操作;Agent 或提供方销毁时等待对应的原生连接清理。重新授权使用新连接,因此旧应用对象和窗口快照不能重新生效。[原生对象决策](2026-09-17-experimental-cua-object-api.zh.md)继续负责进程身份、坐标语义和与执行器分离的设计。
+
+## 考虑过的替代方案
+
+**依赖模型指引或隐藏控件。** 保留的对象和直接工具调用仍能到达原生操作。执行器必须在派发时检查用户决定。
+
+**保存应用名称或未签名路径。** 替换程序可以复用二者。长期许可需要可验证的签名身份;任务授权支持未签名的开发应用,但不承诺跨进程复用。
+
+**新增审批数据库或 Session 控制流。** Profile settings 已负责用户配置,User Questions 已负责交互回答。
+
+**撤销后保留原生状态。** 原生应用查找可能返回旧句柄并保留窗口快照。关闭 Agent 连接可以通过现有传输清理释放其原生状态;保留其他应用的句柄则需要新增按应用销毁状态的原生协议。
+
+## 影响
+
+撤销一个应用也会使同一 Agent 连接中其他应用的句柄失效。这些应用需要重新选择;长期许可仍可复用,任务许可随之失效。其他任务使用独立连接,除非也持有被撤销应用的许可,否则不受影响。
+
+
+授权决定和失败结果由策略回归测试及 CUA 录制会话验证。桌面安装包的二进制分发、签名和打包宿主权限不属于源码启动流程。
diff --git a/.agents/notes/implemented/architecture/2026-09-27-progressive-office-skills.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-27-progressive-office-skills.i18n.yaml
new file mode 100644
index 0000000000..01cda717dd
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-27-progressive-office-skills.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-09-27-progressive-office-skills.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-27-progressive-office-skills.md
+/agent-note-progressive-office-skills-with-ordinary-shell-execution:
+ en: 42e2f062c37e9ef1
+ zh: 5cd3d421adec1cac
+/agent-note-progressive-office-skills-with-ordinary-shell-execution/problem:
+ en: a9984a594cdac98d
+ zh: 38768e7699bc8d39
+/agent-note-progressive-office-skills-with-ordinary-shell-execution/decision:
+ en: d057c558d6b9803c
+ zh: acf3fdbc4cd7cee3
+/agent-note-progressive-office-skills-with-ordinary-shell-execution/alternatives-considered:
+ en: 8bca955255e6d982
+ zh: bc313a4bcab3f6f9
+/agent-note-progressive-office-skills-with-ordinary-shell-execution/consequences:
+ en: cd4ff2423b2f5f9e
+ zh: 63135aeae9c46415
diff --git a/.agents/notes/implemented/architecture/2026-09-27-progressive-office-skills.md b/.agents/notes/implemented/architecture/2026-09-27-progressive-office-skills.md
new file mode 100644
index 0000000000..a89b896fe6
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-27-progressive-office-skills.md
@@ -0,0 +1,32 @@
+# Agent Note: Progressive Office skills with ordinary shell execution
+
+Status: implemented
+
+English | [中文](2026-09-27-progressive-office-skills.zh.md)
+
+## Problem
+
+Combined authoring and rendering instructions spend context on operations unrelated to ordinary editing. Rendering tool activation adds a second discovery path and durable state even though shell tools already own execution permissions and process management. A missing renderer can prevent loading otherwise usable editing guidance.
+
+## Decision
+
+
+The [Office provider](../../../../packages/skill/skill-office/README.md) lists a compact router and six format-specific authoring or operation skills. The model loads full instructions by name and selects rendering for appearance-focused changes unless the user explicitly skips it. Rendering uses a private, randomly named temporary directory inside the task workspace and removes it after inspection. Skills invoke the shared Python checker and configured LibreOffice Kit CLI through ordinary Bash or PowerShell. SDK and Desktop own their Office composition separately; ordinary Web and the shared base do not mount these plugins.
+
+Carriers own interpreter and resource locations. Operation skills append the configured Node and CLI paths only when their bodies are loaded; authoring skills use the selected Python and shared checker resource. Loading instructions does not prepare engines or authoring payloads. Ordinary shell execution retains its existing permissions and process management.
+
+Office dependency ranges allow patch updates; the root lock records the resolved adapter and engine versions.
+
+## Alternatives considered
+
+**Keep a dedicated renderer tool.** A small typed schema constrains rendering arguments, but requires its own execution configuration, scoped registration, projection, and activation lifecycle. The CLI operation skills accept shell quoting and model-selected arguments in exchange for reusing the existing approval, sandbox, cancellation, and output mechanisms.
+
+**Hide subskills from the catalog.** This requires a generic visibility policy independent of invocation permission. Listing seven short summaries preserves the existing skill API while full instruction bodies remain demand-loaded.
+
+**Automatically inject guides for file extensions.** Extensions do not distinguish text edits, recalculation, conversion, and requested visual checks. Model-selected routing keeps unrelated operations out of context and permits explicit direct invocation.
+
+## Consequences
+
+Instruction loading appends ordinary logged skill results; completion does not erase them. Historical tool records remain readable and existing declaration reconciliation handles resumed sessions without a Session format change or agent-loop modification. A dedicated renderer would need a new independently justified user or security requirement before reintroduction.
+
+Verification covers catalog entries, denied and explicit invocation, authoring without executable paths, native/PTC requests, resume and fork, ordinary and minimal profile composition, resource packaging, context budgets, and real-model operation selection. Rendering failures preserve usable sources and stop visual checks; successful commands alone cannot establish usable images.
diff --git a/.agents/notes/implemented/architecture/2026-09-27-progressive-office-skills.zh.md b/.agents/notes/implemented/architecture/2026-09-27-progressive-office-skills.zh.md
new file mode 100644
index 0000000000..0aeb95eb1d
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-27-progressive-office-skills.zh.md
@@ -0,0 +1,32 @@
+# Agent Note: Office 技能渐进加载与普通 shell 执行
+
+Status: implemented
+
+[English](2026-09-27-progressive-office-skills.md) | 中文
+
+## 问题
+
+合并的创作和渲染说明会为普通编辑无关的操作消耗上下文。渲染工具激活增加第二条发现路径和持久状态,而 shell 工具已经负责执行权限及进程管理。缺少渲染器也可能阻止加载原本可用的编辑指南。
+
+## 决策
+
+
+[Office 提供者](../../../../packages/skill/skill-office/README.zh.md) 列出精简路由和六个各格式创作或操作技能。模型按名称加载完整说明;以外观为目标的修改会选择渲染,除非用户明确要求跳过。渲染使用任务工作区内随机命名的私有临时目录,并在检查后删除。技能通过普通 Bash 或 PowerShell 调用共享 Python 检查器和配置的 LibreOffice Kit CLI。SDK 和 Desktop 分别拥有 Office 组合;普通 Web 和共享 base 不挂载这些插件。
+
+载体负责解释器和资源位置。操作技能仅在加载正文时附加配置的 Node 与 CLI 路径;创作技能使用选定的 Python 和共享检查器资源。加载说明不准备引擎或创作 payload。普通 shell 执行保留现有权限和进程管理。
+
+Office 依赖范围允许补丁版本更新;根锁文件记录实际解析的适配器与引擎版本。
+
+## 考虑过的替代方案
+
+**保留专属渲染工具。** 小型类型化 schema 能约束渲染参数,但需要专属执行配置、作用域注册、投影和激活生命周期。CLI 操作技能接受 shell 引用和模型选择参数的成本,以复用现有审批、沙箱、取消及输出机制。
+
+**从目录隐藏子技能。** 这需要独立于调用权限的通用可见性策略。列出七条简短摘要可保留现有技能 API,完整说明仍按需加载。
+
+**按文件扩展名自动注入指南。** 扩展名无法区分文本编辑、重计算、转换和用户要求的视觉检查。模型选路使无关操作不进入上下文,并允许显式直接调用。
+
+## 影响
+
+说明加载追加普通的已记录技能结果;操作完成不会擦除这些结果。历史工具记录仍可读取,恢复会话使用现有声明更新机制,无需修改 Session 格式或 agent loop。重新引入专属渲染器需要新的、独立成立的用户或安全要求。
+
+验证覆盖目录条目、拒绝及显式调用、无可执行文件路径时的创作、native/PTC 请求、恢复和 fork、常规及 minimal 组合、资源打包、上下文预算和真实模型操作选择。渲染失败时保留可用源文件并停止视觉检查;命令成功本身不能证明图片可用。
diff --git a/.agents/notes/implemented/architecture/2026-09-29-explicit-session-history-preparation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-29-explicit-session-history-preparation.i18n.yaml
new file mode 100644
index 0000000000..ddd6d80912
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-29-explicit-session-history-preparation.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-09-29-explicit-session-history-preparation.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-29-explicit-session-history-preparation.md
+/agent-note-open-the-session-when-migration-is-required:
+ en: d409772933407317
+ zh: ac258a6d1743965f
+/agent-note-open-the-session-when-migration-is-required/problem:
+ en: ab63c8127d68d294
+ zh: d3f99489c96ff1d4
+/agent-note-open-the-session-when-migration-is-required/decision:
+ en: 32627d744a6e7410
+ zh: 2836a95ee5390111
+/agent-note-open-the-session-when-migration-is-required/alternatives-considered:
+ en: baea781018a49252
+ zh: bcc452029bf229fd
+/agent-note-open-the-session-when-migration-is-required/consequences:
+ en: 979eebb53a6bf711
+ zh: 013743b0b7a000ce
diff --git a/.agents/notes/implemented/architecture/2026-09-29-explicit-session-history-preparation.md b/.agents/notes/implemented/architecture/2026-09-29-explicit-session-history-preparation.md
new file mode 100644
index 0000000000..84b30b6cb4
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-29-explicit-session-history-preparation.md
@@ -0,0 +1,43 @@
+# Agent Note: Open the Session when migration is required
+
+Status: implemented
+
+English | [中文](2026-09-29-explicit-session-history-preparation.zh.md)
+
+## Problem
+
+Expanding a subagent catalog or forking from the sidebar can trigger an old Session's format migration.
+
+## Decision
+
+The WorkspaceBrowser row-menu fork request carries `allowMigration: false`, which the Host handles by checking `stat` format information before calling the original fork flow. Only a source format requiring migration is refused, with an app-wide Toast offering to open the original Session. That action only navigates and does not retry fork. Neither an absent live Session nor unopened Client history justifies refusal.
+
+Other fork callers omit that option and allow automatic migration.
+
+`session.projections` also checks the stored format first; current-format cold history still uses the original observation flow for exact projections. Success returns `kind: sequenced` and its real `asOfSeq`; required migration returns `kind: migration-required` hints. The Client retains that read state, and cached values cannot replace sequenced values published by history or control.
+
+The `formatStatus` returned by `stat` carries the migration library's existing classification; `header` remains the current logical format. It describes the stored format, not migration-task progress or prepared in-memory history.
+
+When migration is required and `subagentCatalog` is absent, Web marks the Session's row “Migration required” and keeps its history-opening actions without descendant expansion or changing its subagent mode. Opening the Session supplies its catalog through history and control projections. Available catalogs remain usable, including empty catalogs. Session-list lineage resolves navigation addresses, not missing catalog membership. Ordinary loading, cancellation, and storage errors do not become this migration branch.
+
+Create/adopt, full-text search, and ordinary history opening allow automatic migration.
+
+This decision adds a migration exception to [catalog projection reads](../../archived/simplification/2026-09-08-web-subagent-catalog-projections.md). Exact reads and leases in [Session observations](2026-08-25-session-observations-and-projection-owned-client-state.md), and identity checks and cached/sequenced ordering in [projection-cache reads](2026-09-19-projection-cache-listing-identity-and-cached-rows.md), still apply.
+
+## Alternatives considered
+
+**Treat every cold Session as needing migration.** Residency and stored format are different; this would reject otherwise valid forks and discard catalog information available on disk.
+
+**Globally forbid forks that automatically migrate on the Host.** The sidebar's interaction choice does not govern other callers, so its request explicitly carries the restriction.
+
+**Automatically open and retry the sidebar fork.** Opening is an explicit user action, and fork does not run automatically after migration.
+
+**Derive catalogs from Session-list parent ids.** This adds a second membership derivation; explicit Session opening obtains the catalog.
+
+**Give cached hints `asOfSeq: -1`.** Hints have no event cut in current history and cannot participate in sequence comparisons.
+
+## Consequences
+
+Sessions requiring migration use the existing opening flow to prepare history; Sessions that need no migration remain cold-readable and forkable. Catalog hints may lag the log and yield to subsequent exact projections.
+
+A separate follow-redirection proposal discusses future single-Session migration progress; this decision does not implement that flow.
diff --git a/.agents/notes/implemented/architecture/2026-09-29-explicit-session-history-preparation.zh.md b/.agents/notes/implemented/architecture/2026-09-29-explicit-session-history-preparation.zh.md
new file mode 100644
index 0000000000..31925f7a9f
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-29-explicit-session-history-preparation.zh.md
@@ -0,0 +1,43 @@
+# Agent Note: 需要迁移时引导打开会话
+
+Status: implemented
+
+[English](2026-09-29-explicit-session-history-preparation.md) | 中文
+
+## 问题
+
+展开 subagent 目录或从侧栏 fork 可能触发旧会话的格式迁移。
+
+## 决定
+
+WorkspaceBrowser 行菜单的 fork 请求携带 `allowMigration: false`,由 Host 在调用原 fork 流程前检查 `stat` 的格式信息。只有源格式需要迁移时才拒绝,并显示带“打开原会话”操作的应用级 Toast。该操作只导航,不自动重发 fork。没有 live Session 或 Client 尚未打开历史不构成拒绝理由。
+
+其他 fork 调用者省略该选项,允许自动迁移。
+
+`session.projections` 同样先检查存储格式,当前格式冷历史继续通过原观察流程读取准确投影。成功时返回 `kind: sequenced` 和真实 `asOfSeq`;需要迁移时返回 `kind: migration-required` 的缓存提示。Client 保留这一读取状态,缓存值不能覆盖 history 或 control 已发布的有序值。
+
+`stat` 返回的 `formatStatus` 携带迁移库已有的分类;`header` 仍是当前逻辑格式。它描述存储格式,不描述迁移任务进度或内存准备缓存。
+
+在需要迁移且缺少 `subagentCatalog` 时,Web 将该会话行标为“需要迁移”,保留打开历史的操作,但不能展开下级,也不改变子会话的 mode。打开会话后,历史与 control 投影提供其目录。已有目录仍可使用,包括空目录。会话列表的谱系用于解析导航地址,不用于补齐缺失的目录成员。普通加载、取消和存储错误不转换成这一迁移分支。
+
+create/adopt、全文搜索和普通历史打开允许自动迁移。
+
+本决策为[目录投影查询](../../archived/simplification/2026-09-08-web-subagent-catalog-projections.md)增加迁移例外。[会话观察](2026-08-25-session-observations-and-projection-owned-client-state.zh.md)的准确读取和租约,以及[投影缓存读取](2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md)的身份校验与缓存/有序分层继续适用。
+
+## 考虑过的替代方案
+
+**把所有冷会话都视作需要迁移。** 是否驻留与存储格式不同;这样会拒绝可以正常完成的 fork,并丢失能够从磁盘读取的目录信息。
+
+**Host 全局禁止自动迁移的 fork。** 侧栏的交互选择不适用于其他调用者,因此限制由该请求显式携带。
+
+**侧栏 fork 自动打开并重试。** 打开会话由用户明确选择,fork 不在迁移后自动执行。
+
+**按会话列表中的父 id 派生目录。** 这需要第二套成员派生逻辑;显式打开会话即可获取目录。
+
+**让缓存提示携带 `asOfSeq: -1`。** 提示没有当前历史的事件切点,不能参与序号比较。
+
+## 影响
+
+需要迁移的会话通过现有打开流程完成准备;无需迁移的会话仍可冷读和 fork。目录的缓存提示可能落后于日志,后续准确投影替换它们。
+
+后续单会话迁移进度由独立的 follow 重定向提案讨论,本决策不实现该流程。
diff --git a/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.i18n.yaml
new file mode 100644
index 0000000000..aa017c7b03
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.i18n.yaml
@@ -0,0 +1,37 @@
+# Bilingual-pair consistency record for 2026-09-30-profile-package-refresh-and-manifest-invalidation.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.md
+/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration:
+ en: ea66719b3dcdd21c
+ zh: 893364551594ddaf
+/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/problem:
+ en: e59e3f5c61158701
+ zh: 2ced78e3c84f604e
+/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/decision:
+ en: a15c7c13f167e5ac
+ zh: a15c7c13f167e5ac
+/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/decision/responsibilities:
+ en: d53a850a2deabc08
+ zh: 01c2f735f2a9562a
+/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/decision/runtime-resolution:
+ en: 42a3ed903fbb5483
+ zh: 7e1f6df1137794c3
+/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/decision/plugin-manager:
+ en: 7ec1ad2a691bbd12
+ zh: 01057bbe0240056a
+/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/decision/hmr-package-configuration-expiry:
+ en: c7de8dafe47a5587
+ zh: 6010a70d8451edd1
+/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/decision/future-work:
+ en: 8b5faf72418a43dd
+ zh: 3d676c9b54697a2a
+/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/alternatives-considered:
+ en: f04a27b3525180b0
+ zh: 9f3a01d0400c5a88
+/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/verification:
+ en: 939cedceed200cb2
+ zh: c7bb02d0da400c8d
+/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/consequences:
+ en: 1b9e31b4e773fac5
+ zh: c140ce07fc64bd55
diff --git a/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.md b/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.md
new file mode 100644
index 0000000000..83612801d2
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.md
@@ -0,0 +1,117 @@
+# Agent Note: Refresh runtime resolution after package operations and expire HMR package configuration
+
+Status: implemented
+
+English | [中文](2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md)
+
+## Problem
+
+Two pieces of Host state did not follow changes while the process ran:
+
+- **Runtime resolution was built only at startup.** After a GUI install or enablement, nothing published the latest generation, so a bundle's private dependencies never reached the profile fallback and its bare plugins could not be found. After disablement or removal, a new generation could not remove profile-scoped entries or profile-local names, so stale records and package metadata remained. A newly installed package directory is new, and Node holds no stale cache for it; only the table was missing.
+- **Package configuration and module evaluation have separate caches.** Clearing a plugin's module cache does not refresh the package.json fields that Node uses for exports, main, imports, and format detection. A package.json can also be imported as a JSON module, in which case its consumers need ordinary module reloads.
+
+## Decision
+
+### Responsibilities
+
+| Owner | Does | Does not |
+|---|---|---|
+| resolver (`app-boot/src/profile-resolution/resolver.ts`) | `replace()` allows removing `scope: profile` entries and profile-local names, and changing a retained profile entry's version in place | Touch any Node cache |
+| plugin-manager | Without HMR, calls `pluginPackages.refresh()` at fixed points of package operations; with HMR, leaves publication to its recomposition | Construct resolutions or manage module and package-configuration caches |
+| HMR (`packages/boot/hmr`) | Expires package configuration and preserves ordinary reloads for manifests loaded as JSON modules; replaces plugins whose loaded package files changed during recomposition ([replacement Note](2026-10-01-replace-changed-package-modules-on-recomposition.md)) | Reload plugins for configuration-only manifests |
+
+### Runtime resolution
+
+The [generation Note](2026-09-09-profile-resolution-generations.md) owns package identity and Worker inheritance rules.
+
+Validation of a new generation in `replace()`:
+
+- `scope: profile` entries may be removed.
+- Profile-local package names may be removed.
+- Retained profile entries keep their normalized directory and scope, but may change declaring anchor when another selected bundle supplies the same package, and may change version when the package is upgraded in place ([replacement Note](2026-10-01-replace-changed-package-modules-on-recomposition.md)). Installation mappings and declaring anchors remain unchanged. The profile scope cannot change; a new local name cannot override an existing entry. A published link name may select another real directory; Node's cached real paths of the previous one are relocated.
+
+### Plugin Manager
+
+| Operation | Publication point |
+|---|---|
+| Install, enable, disable or remove with HMR | Inside HMR's recomposition, after the rows the generation drops stopped and before the generation applies ([replacement Note](2026-10-01-replace-changed-package-modules-on-recomposition.md)) |
+| Install a new package or enable a bundle without HMR | After the profile files are saved, before reconciliation |
+| Remove without HMR | After pnpm remove succeeds |
+| Disable without HMR, an install over an installed package, a failed or cancelled install, a failed removal | No publication; an install over a package the spec names is refused before pnpm runs |
+
+`createRuntimeResolution()` returns a `ProfileRuntimeResolution`, which privately keeps the installation anchor, Harness home, and profile directory it was computed from. Its `computeLatestResolution()` rereads that profile with the same inputs and returns a new object for the latest generation, leaving the original unchanged; Workers receive only the table fields. `PluginPackages.refresh()` calls the current resolution's `computeLatestResolution()` and then `replace()`; a resolution constructed as plain data cannot be refreshed and makes the call throw. app-boot's `recomposeProfile()` first stops the rows the generation no longer lists, so their plugins dispose while their packages are still mapped, then publishes the latest generation once; a table that is already published is not published again.
+
+Without HMR, deselecting a startup bundle does not stop its plugins. While such a bundle remains deselected, later package operations keep the existing runtime table instead of publishing its removal. Other no-HMR installations and removals of non-running bundles still publish normally.
+
+A successful package operation is not rolled back when publication is rejected afterwards. The result reports the failed runtime application and retains the successful disk changes. Recomputing uses the captured profile directory on disk, not synthetic in-memory layers; a computed resolution without a profile recomputes installation packages only.
+
+### HMR package-configuration expiry
+
+In HMR's change dispatch, a changed file named `package.json` outside `node_modules` goes to `PackageManifests.invalidate()`. A manifest in the host dependency graph still requests a host reload; one loaded as a JSON module still reloads its consumers. A configuration-only manifest schedules no module reload. Configuration-owned paths stay with their dedicated watcher. Source changes in the same batch reload afterwards.
+
+`invalidate(manifest)` records the directory as expired. Afterwards:
+
+- package.json reads, scope lookups, type lookups, and nearest-manifest lookups below that directory read the current file. Ownership follows the real directory when a consumer reaches the package through a link.
+- ESM `ResolveCache` is cleared. Its entries do not record every consulted manifest, and a package entry may resolve outside the package directory.
+- CommonJS `_pathCache` is cleared for the same reason. Unrelated requests recompute their resolution without unloading their modules.
+- CommonJS loads using the default resolver pass the freshly resolved filename to the native loader. Its private request alias cannot select an older entry; cached module instances remain intact.
+
+Configuration invalidation alone leaves loaded modules unchanged. Loader entries retain their raw import results before export normalization. HMR matches imported Node module objects to cached ModuleJob namespaces and reloads the loaded URLs. All matching entry records, including disabled entries, update only after a successful reload and remain unchanged on failure. Stopping and restarting a Loader entry resolves its package name again and selects the new entry.
+
+Entry names are scoped by configuration-tree base URL and retain all distinct imported namespaces. An uninitialized entry contributes the name without erasing loaded namespaces; name-based resolution is used only when none is recorded, or for `cordis:` builtins. Dependency analysis considers every recorded module. Entries sharing a plugin runtime use one replacement operation, while each Loader entry selects its replacement by its original namespace. Module imports and export normalization finish before the old runtime is removed. Failed imports or activation restore the previous modules and plugin implementations and clean up partially activated replacements.
+
+An instance without a Loader entry has no recorded module identity. If a shared runtime's replacement modules have different plugin callbacks, HMR reports an ambiguity error and rolls back instead of assigning that instance to an arbitrary module.
+
+Package invalidation lives in `packages/boot/hmr/src/package-manifest.ts`, which encapsulates its Node internal interfaces. `index.ts` dispatches manifest changes and locates loaded entries by module identity. HMR's `node_modules` exclusion is unchanged.
+
+| Interface | Action |
+|---|---|
+| modules binding `readPackageJSON`, `getPackageScopeConfig`, `getPackageType` | Replaced: paths in an expired directory read the current file; other calls reach the native method |
+| `package_json_reader.getNearestParentPackageJSON` | Replaced: expired directories bypass its JS cache; a lookup without a manifest returns the native absent result |
+| The ESM Loader's `ResolveCache` instance | The prototype `get` is replaced for one lookup to obtain the instance and restored at once; the resolution cache is then cleared |
+| CommonJS `Module._pathCache` | The request-to-filename cache is cleared |
+| CommonJS `Module._load` | Default-resolver requests load by resolved filename; builtins and registered resolve hooks keep the original path |
+
+The binding's `getNearestParentPackageJSON` is called only by `package_json_reader` and is not replaced. Each HMR instance owns a separate configuration cache. Its replacements are installed at the first expiry and restored when the service is disposed, alongside watcher and reload-queue cleanup.
+
+### Future Work
+
+- A retained profile mapping that moves to another directory waits for a restart; Plugin Manager moves a package by removing and installing it. In-place replacement inside `node_modules` is handled by the [replacement Note](2026-10-01-replace-changed-package-modules-on-recomposition.md).
+- TSX versions using an asynchronous loader thread keep that thread's package configuration outside these hooks. Synchronizing it is deferred; this change does not provide general Worker cache synchronization.
+- CommonJS private-request-cache refresh with registered synchronous resolve hooks is deferred. Those requests retain their original loader behavior.
+- Disposing HMR restores the native readers, whose previous cached configuration can become visible again. Preserving invalidation state across HMR replacement is deferred.
+
+## Alternatives considered
+
+**Read current disk on every lookup (#4703).** Resolution follows the disk, but module caches stay and running plugins do not reload. Same-URL contents do not take effect, and different URLs load a second version beside the first in one process. It also maintains its own entry resolver, bypasses custom bare-name hooks, and does not refresh runtime resolution, so GUI-installed private dependencies still cannot be found.
+
+**Reread every observed manifest when runtime resolution publishes (#5496).** This places package-configuration expiry in table publication, decoupled from module caches: overwriting an installed package reports restart, yet the next unrelated publication makes new imports load the new version while the old plugin still runs. Package configuration is content and belongs with module caches, in HMR.
+
+**Let HMR reload packages inside `node_modules`.** It requires finding affected plugins over a module graph that includes `node_modules`, preventing duplicate evaluation of shared libraries and Cordis, and handling missing dynamic-import edges, module side effects, and Workers. It is not done here; the [replacement Note](2026-10-01-replace-changed-package-modules-on-recomposition.md) adds it at profile recomposition.
+
+**Reload every plugin for a package.json change.** Configuration-only manifests do not require module evaluation. Manifests actually imported as JSON modules retain ordinary dependency-driven reloads.
+
+**Re-import reloaded plugins by package name.** It would let an entry rename follow a source reload, but changes `partialReload`'s rule that the loaded URL is the reload unit. Entry renames already take effect when the Loader entry restarts, so this is not changed.
+
+**Keep one namespace per entry name.** Entries with the same name and base URL can have loaded different modules before and after a manifest change. Keeping only the first or last namespace loses a live module; an uninitialized entry must not erase another entry's imported namespace.
+
+## Verification
+
+| Coverage | Location |
+|---|---|
+| A new generation removes profile entries and local names and still rejects retained-entry changes | `packages/boot/app-boot/tests/profile-resolution.spec.ts` |
+| GUI install, deferred enablement, disablement, removal, overwrite, failure, and cancellation, with and without HMR | `packages/boot/plugin-manager/tests/package-reload.spec.ts` |
+| Captured-directory recomputation, computed installation-only refresh, and plain-data rejection | `packages/boot/app-boot/tests/profile-resolution.spec.ts`, `packages/boot/app-boot/tests/profile-resolution-service.spec.ts` |
+| Sequential no-HMR operations, shared dependency declarers, and disk-success/publication-failure outcomes | `packages/boot/plugin-manager/tests/package-reload.spec.ts` |
+| Expired exports, main, imports, type, scope, and nearest manifests; native-reader parity; actual CommonJS loads and retained module instances; the `node_modules` boundary; restoration | `packages/boot/hmr/tests/package-manifest.spec.ts` |
+| Configuration-only manifests, JSON-module and host reloads, source reloads, same-name and shared-runtime entries, import and activation rollback, entry restarts, and the `node_modules` exclusion | `packages/boot/hmr/tests/package-manifest-dispatch.spec.ts` |
+
+Tests need no API key and make no model calls.
+
+## Consequences
+
+- With HMR, supported package operations publish the updated private dependencies and remove unused mappings after plugins stop. Without HMR, a pending startup-bundle disablement defers later publications until restart, preserving running plugins' mappings.
+- In the supported thread, package.json configuration reads use the changed fields; JSON modules retain their own reload behavior. The separate module, loader-thread, and Worker limitations above still apply.
+- Package configuration below expired directories is parsed in JavaScript and must keep the native reader's field and error semantics; tests compare it with the native reader on Node 22, 24, and 26.
+- The implementation depends on several Node internal interfaces, and these tests must run again for Node upgrades.
diff --git a/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md b/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md
new file mode 100644
index 0000000000..6f19b5fa4a
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md
@@ -0,0 +1,117 @@
+# Agent Note: 包操作刷新 runtime resolution 与 HMR 包配置失效
+
+Status: implemented
+
+[English](2026-09-30-profile-package-refresh-and-manifest-invalidation.md) | 中文
+
+## Problem
+
+Host 运行期间有两处状态没有随变化更新:
+
+- **runtime resolution 只在启动时构造。** GUI 安装或启用 bundle 后,没有人发布最新代,bundle 的私有依赖进不了 profile fallback,它的 bare 插件找不到。停用、卸载后,新一代又不允许移除 profile 范围的条目和本地包名,旧记录和包元数据一直留着。新安装的包目录本身是新的,Node 对它没有旧缓存,缺的只是这张表。
+- **包配置与模块求值使用独立缓存。** 清理插件的模块缓存不会刷新 Node 用于 exports、main、imports 和格式判断的 package.json 字段。package.json 也可以被作为 JSON 模块导入,此时其消费方需要普通的模块重载。
+
+## Decision
+
+### 职责
+
+| 负责方 | 做什么 | 不做什么 |
+|---|---|---|
+| resolver(`app-boot/src/profile-resolution/resolver.ts`) | `replace()` 允许移除 `scope: profile` 条目和 profile 本地包名,并允许原地更改保留的 profile 条目的版本 | 不碰 Node 的任何缓存 |
+| plugin-manager | 没有 HMR 时,在包操作的确定时点调用 `pluginPackages.refresh()`;有 HMR 时,把发布交给其重新组合 | 不构造 resolution;不管模块缓存与包配置缓存 |
+| HMR(`packages/boot/hmr`) | 使包配置失效,并保留作为 JSON 模块加载的 manifest 的普通重载;重新组合时替换已加载包文件发生变化的插件([替换说明](2026-10-01-replace-changed-package-modules-on-recomposition.zh.md)) | 不因仅作为配置的 manifest 重载插件 |
+
+### runtime resolution
+
+包身份与 Worker 继承规则由[代际说明](2026-09-09-profile-resolution-generations.zh.md)定义。
+
+`replace()` 对新一代的校验:
+
+- `scope: profile` 的条目可以移除。
+- profile 本地包名可以移除。
+- 保留的 profile 条目保持规范化目录与作用域不变;另一个已选 bundle 提供同一包时,可以更换声明锚点;包被原地升级时,可以更改版本([替换说明](2026-10-01-replace-changed-package-modules-on-recomposition.zh.md))。安装范围的映射与声明锚点保持不变。profile 范围不能变;新本地包名不能覆盖已有条目。已发布的 link 名称可以选中其他真实目录;Node 缓存的原目录真实路径会被重新定位。
+
+### plugin-manager
+
+| 操作 | 发布时点 |
+|---|---|
+| 有 HMR 时的安装、启用、停用或卸载 | 在 HMR 的重新组合内,新一代丢弃的行停止之后、新一代应用之前([替换说明](2026-10-01-replace-changed-package-modules-on-recomposition.zh.md)) |
+| 没有 HMR 时安装新包或启用 bundle | 保存 profile 文件之后,reconciliation 之前 |
+| 没有 HMR 时卸载 | pnpm remove 成功后 |
+| 没有 HMR 时停用、覆盖已安装的包、安装失败或取消、卸载失败 | 不发布;spec 指明已安装的包时,覆盖安装在 pnpm 运行之前被拒绝 |
+
+`createRuntimeResolution()` 返回 `ProfileRuntimeResolution`,它私有保存构造时的 installAnchor、home 和 profile 目录;`computeLatestResolution()` 用同一组输入重新读取 profile,返回表示最新代的新对象,原实例不变;Worker 只收到表数据。`PluginPackages.refresh()` 调用当前 resolution 的 `computeLatestResolution()`,再调用 `replace()`;以纯数据构造的 resolution 无法刷新,调用时抛错。app-boot 的 `recomposeProfile()` 先停止新一代不再列出的行,使其插件在包仍有映射时完成释放,再发布一次最新一代;已发布的表不会再次发布。
+
+没有 HMR 时,取消启动 bundle 的选择不会停止它的插件。只要该 bundle 仍未选中,后续包操作就保留当前 runtime 表,不发布其移除。其他无 HMR 的安装和未运行 bundle 卸载仍正常发布。
+
+包操作成功后若发布被拒绝,不回滚已完成的包操作。结果报告运行时应用失败,并保留成功的磁盘变更。重新计算使用捕获的磁盘 profile 目录,不继承内存合成 layers;没有 profile 的已计算 resolution 只重算安装范围的包。
+
+### HMR 包配置失效
+
+HMR 的文件变化分派中,文件名为 `package.json` 且不在 `node_modules` 内的变化,交给 `PackageManifests.invalidate()`。位于宿主依赖图中的 manifest 仍请求宿主重载;作为 JSON 模块加载的 manifest 仍重载消费方。仅作为配置的 manifest 不安排模块重载。配置专属路径仍由其独立 watcher 处理。同一批的源码变化随后重载。
+
+`invalidate(manifest)` 把该目录登记为已失效,之后:
+
+- 位于该目录下的 package.json 读取、scope 查询、type 查询、最近 package.json 查询,都按当前磁盘解析。消费方经由链接路径访问时,按真实目录判断归属。
+- 清空 ESM `ResolveCache`。其条目不记录查询过的全部 manifest,包入口也可能解析到包目录之外。
+- 基于相同原因清空 CommonJS `_pathCache`。无关请求重新计算解析结果,不卸载其模块。
+- 使用默认 resolver 的 CommonJS 加载把重新解析的文件名交给原生 loader。其私有请求别名不再选中旧入口,已缓存的模块实例保持不变。
+
+仅使配置失效不会改变已加载模块。Loader entry 保留导出规范化之前的原始导入结果。HMR 将已导入的 Node 模块对象与缓存中 ModuleJob 的模块命名空间按对象身份匹配,重载已加载 URL。所有匹配的 entry 记录,包括已停用的 entry,仅在重载成功后更新,失败时保持不变。停止并重新启动 Loader entry 时,会重新按包名解析并选择新入口。
+
+entry 名称以配置树 base URL 为作用域,并保留所有不同的已导入命名空间。尚未初始化的 entry 只登记名称,不抹去已加载的命名空间;仅在没有记录命名空间或入口为 `cordis:` 内置模块时按名称解析。依赖分析考虑每个已记录模块。共享插件运行时的 entry 使用同一次替换操作,但每个 Loader entry 按自身原命名空间选择替换实现。模块导入与导出规范化在移除旧运行时之前完成。导入或激活失败时,恢复原来的模块和插件实现,并清理已部分激活的替换实例。
+
+没有 Loader entry 的实例不具备已记录的模块身份。如果共享运行时的替换模块具有不同的插件回调,HMR 会报告歧义错误并回滚,而不是将该实例随意分配给某个模块。
+
+包配置失效实现在 `packages/boot/hmr/src/package-manifest.ts`,封装其使用的 Node internal 接口。`index.ts` 分派 manifest 变更,并按模块身份定位已加载 entry。HMR 的 `node_modules` 排除规则不变。
+
+| 接口 | 动作 |
+|---|---|
+| modules binding 的 `readPackageJSON`、`getPackageScopeConfig`、`getPackageType` | 替换:路径位于已失效目录时按当前磁盘解析,其余调用原生方法 |
+| `package_json_reader.getNearestParentPackageJSON` | 替换:已失效目录绕过它的 JS 缓存;没有 manifest 时返回原生的缺失结果 |
+| ESM Loader 的 `ResolveCache` 实例 | 临时替换原型的 `get`,取得实例后立即恢复,再清空解析缓存 |
+| CJS `Module._pathCache` | 清空请求到文件名的缓存 |
+| CJS `Module._load` | 默认 resolver 的请求按解析后的文件名加载;内置模块及注册的 resolve hooks 保留原路径 |
+
+binding 的 `getNearestParentPackageJSON` 只被 `package_json_reader` 自己调用,不替换。每个 HMR 实例拥有独立的配置缓存,在第一次失效时安装 hook,并在服务销毁时恢复,同时清理 watcher 和重载队列。
+
+### 后续工作
+
+- 移动到其他目录的保留 profile 映射需要等待重启;Plugin Manager 通过卸载再安装来移动包。`node_modules` 内的原地替换由[替换说明](2026-10-01-replace-changed-package-modules-on-recomposition.zh.md)处理。
+- 使用异步 loader 线程的 TSX 版本,其线程内包配置不受这些 hook 管理。线程同步留待后续;本次不提供通用 Worker 缓存同步。
+- 注册了同步 resolve hooks 时,CommonJS 私有请求缓存刷新留待后续;这些请求保留原有 loader 行为。
+- 销毁 HMR 会恢复原生 reader,其此前缓存的配置可能重新可见。跨 HMR 替换保留失效状态留待后续。
+
+## Alternatives considered
+
+**每次查找都读当前磁盘(#4703)。** 解析能跟上磁盘,但不清模块缓存,也不重载运行中的插件。同一 URL 时新内容不生效,不同 URL 时新旧版本在同一进程并存。它还要维护一份入口解析实现,并且绕过自定义 bare-name hook。它也没有刷新 runtime resolution,GUI 安装的私有依赖依然找不到。
+
+**在 runtime resolution 发布时全量重读已见过的 manifest(#5496)。** 它把包配置失效放进了绑定表发布,和模块缓存脱了钩:覆盖已安装的包时报需要重启,但下一次无关的发布会让新导入拿到新版本,旧插件仍在运行。包配置缓存属于内容,应该和模块缓存由同一方负责,也就是 HMR。
+
+**让 HMR 支持 `node_modules` 内的包重载。** 这需要沿包含 `node_modules` 的模块图确定受影响插件,防止共享库和 Cordis 被重复求值,还要处理动态 import 漏边、模块副作用和 Worker。本次不做;[替换说明](2026-10-01-replace-changed-package-modules-on-recomposition.zh.md)在 profile 重新组合时加入了这一能力。
+
+**package.json 变化时重载所有插件。** 仅作为配置的 manifest 不要求模块求值。实际作为 JSON 模块导入的 manifest 保留普通的依赖驱动重载。
+
+**按插件名重新 import 被重载的插件。** 这能让入口改名随源码重载生效,但会改变 `partialReload` 以已加载 URL 为重载单位的规则。入口改名在 Loader entry 重启时已经生效,所以本次不改。
+
+**每个 entry 名称只保留一个命名空间。** 名称和 base URL 相同的 entry,可能在 manifest 变化前后加载了不同模块。只保留最早或最后一个命名空间会遗漏仍在使用的模块;尚未初始化的 entry 也不能抹去其他 entry 的已导入命名空间。
+
+## Verification
+
+| 覆盖面 | 位置 |
+|---|---|
+| 新一代可移除 profile 条目与本地包名、仍拒绝修改保留条目 | `packages/boot/app-boot/tests/profile-resolution.spec.ts` |
+| GUI 安装、延迟启用、停用、卸载、覆盖、失败、取消,有无 HMR | `packages/boot/plugin-manager/tests/package-reload.spec.ts` |
+| 捕获目录后的重新计算、无 profile 的已计算 resolution 刷新、纯数据拒绝 | `packages/boot/app-boot/tests/profile-resolution.spec.ts`、`packages/boot/app-boot/tests/profile-resolution-service.spec.ts` |
+| 无 HMR 连续操作、共享依赖声明来源、磁盘成功但发布失败 | `packages/boot/plugin-manager/tests/package-reload.spec.ts` |
+| 失效后的 exports、main、imports、type、scope、最近 package.json,原生 reader 对照,实际 CommonJS 加载及旧模块实例保留,node_modules 边界,恢复 | `packages/boot/hmr/tests/package-manifest.spec.ts` |
+| 配置专用 manifest、JSON 模块与宿主重载、源码重载、同名及共享运行时的 entry、导入与激活失败回滚、entry 重启和 node_modules 排除规则 | `packages/boot/hmr/tests/package-manifest-dispatch.spec.ts` |
+
+测试不需要 API key,不调用模型。
+
+## Consequences
+
+- 有 HMR 时,支持的包操作发布更新后的私有依赖,并在插件停止后移除不再需要的映射。没有 HMR 时,待生效的启动 bundle 停用会将后续发布延迟到重启,保留运行中插件的映射。
+- 在支持的线程内,package.json 配置读取使用更新后的字段;JSON 模块保留自己的重载行为。上述模块、loader 线程及 Worker 的独立限制仍然适用。
+- 包配置读取改由 JS 解析 package.json,需要与原生读取保持字段和错误语义一致;测试在 Node 22、24、26 上与原生读取逐项比较。
+- 依赖多个 Node internal 接口,Node 升级时需要重跑这些测试。
diff --git a/.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.i18n.yaml
new file mode 100644
index 0000000000..16a39c6917
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.i18n.yaml
@@ -0,0 +1,34 @@
+# Bilingual-pair consistency record for 2026-09-30-structured-draft-initialization.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.md
+/agent-note-initial-draft-content-and-structured-reference-restoration:
+ en: 88202480bcf49e10
+ zh: 3f2aff8ad2be0faa
+/agent-note-initial-draft-content-and-structured-reference-restoration/problem:
+ en: 6aa396e12264ee48
+ zh: 56849a18acaf830a
+/agent-note-initial-draft-content-and-structured-reference-restoration/decision:
+ en: 9cf2afd9d5e663e9
+ zh: ad59b1bec6ec4666
+/agent-note-initial-draft-content-and-structured-reference-restoration/decision/data-model:
+ en: 7f33895364a9f5cc
+ zh: f1c79ea15f5c151e
+/agent-note-initial-draft-content-and-structured-reference-restoration/decision/classes-and-methods:
+ en: ca81423a57f898a0
+ zh: 98962cc1c6bce113
+/agent-note-initial-draft-content-and-structured-reference-restoration/decision/data-flow:
+ en: dd57e639e9328f1d
+ zh: 5e16cb2e3a73597c
+/agent-note-initial-draft-content-and-structured-reference-restoration/decision/asynchronous-skill-matching:
+ en: 3b6a99c417384470
+ zh: b62951995eda2fb0
+/agent-note-initial-draft-content-and-structured-reference-restoration/alternatives-considered:
+ en: eddaec471425fafc
+ zh: b206ecdb12e8c33d
+/agent-note-initial-draft-content-and-structured-reference-restoration/verification:
+ en: 341f4f36936f686a
+ zh: 479fbe04e810f640
+/agent-note-initial-draft-content-and-structured-reference-restoration/consequences:
+ en: ff4f9527db80f790
+ zh: f3e57093c058f643
diff --git a/.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.md b/.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.md
new file mode 100644
index 0000000000..678129340f
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.md
@@ -0,0 +1,88 @@
+# Agent Note: Initial draft content and structured reference restoration
+
+Status: implemented
+
+English | [中文](2026-09-30-structured-draft-initialization.zh.md)
+
+## Problem
+
+New-task entry points need to place unsent content in the composer. A draft contains not only text but also file, folder, and Session references the user has selected. Saving only their text projection loses the chips' source, target, and presentation fields when the input object is disposed. Filling the composer after mounting also cannot guarantee that the first input-model read contains the complete document.
+
+## Decision
+
+Keep the existing `SessionInputShell`, `DraftEditorRuntime`, Conversation store, and Session reference lifetime. Plain text and structured drafts enter through the input model. Initial import finishes before `InputHub.shellFor()` returns, without React effects restoring text. Existing input objects are not restored again. React mounts the same editor and binds the existing persistence callback.
+
+The second parameter of `startSession(workspaceId?, options?)` contains `prompt` and `clearPreviousDraft`. `prompt` accepts only a string; structured drafts remain internal to restoration and Workspace transfer. Omitting options preserves the existing flow. Without clearing, existing content is preserved and only an empty draft adopts the prompt. Explicit clearing replaces the target Session's text document, without deleting attachments or changing other Sessions. Prefilling does not send, execute commands, or change `blank`.
+
+### Data model
+
+`DraftSnapshot` contains `text` and `references`. Each reference retains `offset`, `length`, `source`, `ref`, `label`, `appearance`, `clipboardText`, and its existing invalid flag. Positions use JavaScript string coordinates. Reference spans must be ordered, non-overlapping, and equal the corresponding `clipboardText`. Snapshots exclude Lexical NodeKeys, DOM, runtime occurrenceIds, functions, and temporary highlights.
+
+A string represents input without structured references; an ambiguous `@` query never selects a file automatically. The public `SessionInput.setDraft` and `InputActions.setDraft` methods accept only text. Internal structured restoration reuses the editor's reference restoration without listing directories or reading files. Plain text and file, folder, and Session chips all pass through the registered editor transforms and projection.
+
+The existing `dsh.conversation.` key remains. Readers accept both legacy strings and structured `draft` values; new writes store structured values. The original Conversation store still writes the complete record, without a second writer for that key. Reference changes must reach persistence even when the visible text does not change.
+
+### Classes and methods
+
+| Location | Change and responsibility |
+| --- | --- |
+| `UiWorkspace` / `UiWorkspaceService.startSession` | The second parameter is `StartSessionOptions`; the existing `beforeOpen` preparation point passes initialization to the exact target binding. Creation, reuse, and navigation cancellation remain unchanged |
+| `SessionInputResolver` / `InputHub.requestDraftInitialization` | Accept the binding and options and delegate to that binding's input object, without looking up a global current Session |
+| `InputHub.shellFor` | After registering a new object and its cleanup, read the Session's saved draft, import it through the input model, and return. Reused objects are not restored again |
+| `readConversationDraft(sessionId)` in `stores.ts` / `parseStoredDraft` in `draft.ts` | Read the original storage key, accept legacy strings, and check structured reference fields and positions without creating a store or another writer |
+| `SessionInputShell.setDraft` | The concrete internal method accepts a string or `DraftSnapshot` and imports one document through the editor; public input methods accept only text. Neither path simulates keyboard events or submits |
+| `SessionInputShell.requestDraftInitialization` | Apply clear, preserve, and prefill rules to the restored model; refuse changes during submission without clearing first |
+| `SessionInputShell.draftSnapshot` / `persistCurrentDraft()` | Cache snapshots without runtime IDs by content revision; `InputActions.persistDraft()` delegates explicit saving, while `bindDraftPersistence(write)` only binds the callback |
+| `DraftEditorRuntime.restoreDraft` | Rebuild ReferenceChipNodes from semantic reference fields; existing failed-send restoration uses the same implementation |
+| `SessionInputShell.refreshLexiconSubscription()` / `DraftEditorRuntime.refreshLexiconSubscription()` | `InputHub` owns the Session-scope `inject` callback that connects the lexicon after caching the shell, reconnects when the service arrives or is replaced, and unsubscribes on disposal. Both connection and catalog updates rescan the current editor without writing old text back; `setDraft` does not establish the subscription |
+| `ConversationStoreState` / `createConversationStore` | Support structured input and legacy strings in draft storage, keeping View state and the original key |
+| `DefaultConversationViews` | Remove restoration and prefill effects, retaining persistence binding and unbinding. Initial saving reads the owner's current snapshot, never text captured during render |
+| The existing `selectWorkspace` callback in `apply.ts` | Keep the existing cross-Workspace draft-transfer rules, passing `draftSnapshot` instead of plain text so transfer retains references |
+
+There is no new draft service, storage key, general task framework, or Session creation entry point. SlotRenderer, Host Session logs, command execution, and file-discovery RPCs are unchanged. `InputActions` remains the shared input-action entry; components receive no separate content-initialization callback.
+
+The call parameters are `startSession(workspaceId?: WorkspaceId, options?: StartSessionOptions)`, `requestDraftInitialization(binding: SessionBinding, options: DraftInitializationOptions)`, `SessionInputShell.requestDraftInitialization(options)`, and the internal `SessionInputShell.setDraft(input: string | DraftSnapshot)`. `StartSessionOptions` aliases `DraftInitializationOptions`, whose fields are `prompt?: string` and `clearPreviousDraft?: boolean`. A shallow copy captures these values before asynchronous creation, and the result addresses only the retained target binding. Initialization distinguishes applied, preserved, and submission-blocked outcomes. Refusal does not clear first; superseded navigation does not write a draft.
+
+### Data flow
+
+```text
+stored draft / startSession options
+ → DraftInput
+ → SessionInputShell.setDraft
+ → DraftEditorRuntime → Lexical → InputState
+ → InputHub.shellFor returns
+ → React mounts the editor
+
+user edits / input actions
+ → editor → projection
+ → DraftSnapshot
+ → Conversation store
+```
+
+### Asynchronous skill matching
+
+Skills remain editable text, not file chips. A pending lexicon never locks the draft. Slash names remain ordinary text until a catalog supplies a matching name; an absent provider, unavailable catalog, or unmatched name produces no skill highlight. The existing subscription rescans the current document on arrival instead of saving and restoring the text present when the request began. This changes neither text nor structured references, does not take the caret, and adds no user Undo step. Commands retain their existing validation and execution path at submission.
+
+## Alternatives considered
+
+**Calling setDraft after mounting.** The first model read and render can still be empty, and later restoration can overwrite a clear. Initialization cannot depend on React-effect ordering.
+
+**Saving only clipboardText and searching references again.** Text does not identify a reference's source and presentation fields. Searching candidates does not restore the user's previous selection; existing structure should be restored directly.
+
+**Waiting for all remote lexicons before allowing input.** File references already have their data, and skill highlighting is not an editing prerequisite. Blocking the draft on those queries adds unnecessary waiting.
+
+**Splitting persistence keys or adding a draft-management service.** The existing input owner and store can perform the work by sharing the input representation and import path.
+
+## Verification
+
+The core model invariants are semantic equivalence after import, export, and JSON round trips without comparing runtime node IDs; a complete document on the first input-object read; and stable draft snapshots across selection or skill-highlight changes. [Document-model cases](../../../../packages/client/ui-conversation/tests/draft-document.client.spec.ts) cover strings, empty drafts, multiline Unicode text, file/folder/Session references, duplicates, invalid flags, placeholder sanitization, and invalid stored spans. [Input-lifecycle cases](../../../../packages/client/ui-conversation/tests/draft-hub.client.spec.ts) cover first reads, reuse, lexicon-service arrival, unloading, reconnection, and retired bindings. Initialization is refused during submission, and lexicon rescans add no Undo step.
+
+[Navigation cases](../../../../packages/client/ui-workspace/tests/workspaces-service.client.spec.ts) check exact retained targets, option copying before asynchronous creation, superseded requests without draft writes, and refusal without changing selection. [Assembly cases](../../../../packages/client/ui-conversation/tests/apply-inject.client.spec.tsx) check that existing Workspace transfer retains structured references. The related regression run passed 708 cases in 44 files; after the shared draft module moved to its owning location, the three affected model and assembly files passed another 66 cases.
+
+The [browser e2e](../../../../apps/web/tests/draft-initialization.e2e.ts) observes the shipped Workspace API through the existing module-loader test instrumentation and calls public `startSession` with text options, without an additional test plugin. Structured fixtures enter through the existing saved-draft restoration path. The cases cover absent, empty, and nonempty prompts; explicit and inherited Workspaces; preservation and clearing; creation and reuse; repeated real-UI Session switching and editing; reload; legacy strings; and Workspace-picker transfer. Both orders of delayed creation responses preserve the newer target and leave the other draft unchanged. A delayed real skill-catalog response rematches current text without replacing chips, selection, or Undo history. Assertions inspect chip fields, file previews, and persisted snapshots. Before sending, Host logs contain neither `user/message` nor `turn/start`.
+
+## Consequences
+
+Missing reference information in legacy strings cannot be reconstructed losslessly, so they restore as plain text without searching and guessing files. Attachment File objects, upload state, cross-browser synchronization, multiple independent editors, and persistent Undo history are outside this change.
+
+This decision shares input ownership with the [editor-isolation proposal](../../proposed/architecture/2026-09-14-composer-model-and-draft-editor.md) without implementing its multi-editor goal. [Reference previews](../../../../packages/client/ui-input-trigger/README.md) and [Session reference lifetimes](2026-09-15-client-session-references.md) continue to govern source routing and generation cleanup independently; neither is superseded.
diff --git a/.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.zh.md b/.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.zh.md
new file mode 100644
index 0000000000..07c185a969
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.zh.md
@@ -0,0 +1,88 @@
+# Agent Note: 草稿初始内容与结构化引用恢复
+
+Status: implemented
+
+[English](2026-09-30-structured-draft-initialization.md) | 中文
+
+## 问题
+
+新任务入口需要把一段尚未发送的内容放入输入框。草稿不仅包含文字,也包含用户已经选定的文件、目录和 Session 引用;只保存引用的文字投影,会在输入对象销毁后丢失胶囊的来源、目标和展示字段。页面挂载后的填字也不能保证第一次读取输入模型时已经得到完整内容。
+
+## 决策
+
+保留现有 `SessionInputShell`、`DraftEditorRuntime`、Conversation store 和 Session 引用生命周期。普通文字与结构化草稿统一经过输入模型导入;初始导入在 `InputHub.shellFor()` 返回对象前完成,不由 React effect 恢复文字。已存在的输入对象不重新恢复。React 只挂载同一编辑器,并绑定现有持久化回调。
+
+`startSession(workspaceId?, options?)` 的第二参数保留 `prompt` 与 `clearPreviousDraft`。`prompt` 仅接受字符串;结构化草稿只用于内部恢复和工作区携带。无选项时维持原流程。不清空时保留已有内容,空稿才采用 prompt;显式清空作用于目标 Session 的文字文档,不删除附件,也不修改其他 Session。预填不自动发送、执行命令或改变 `blank`。
+
+### 数据模型
+
+`DraftSnapshot` 由 `text` 和 `references` 组成。每个引用保留 `offset`、`length`、`source`、`ref`、`label`、`appearance`、`clipboardText` 及已有的无效标记;位置使用 JavaScript 字符串坐标。引用位置必须有序、不重叠,且对应的正文片段与 `clipboardText` 一致。不保存 Lexical NodeKey、DOM、运行时 occurrenceId、函数或临时高亮。
+
+字符串等价于不含引用结构的输入;不会据模糊的 `@` 查询擅自选中某个文件。公开的 `SessionInput.setDraft` 与 `InputActions.setDraft` 仅接受文本。内部结构化恢复直接复用编辑器已有的引用恢复能力,无需查询文件目录或读取文件内容。恢复普通文字、文件胶囊、目录胶囊和 Session 胶囊都经过已注册的编辑器变换和投影流程。
+
+现有 `dsh.conversation.` key 保留,`draft` 可读取旧字符串,也可读取新的结构化值;新写入保存结构化值。仍由原 Conversation store 写整个记录,不增加第二个同 key 写入者。引用变化即使没有改变显示文字,也必须进入持久化快照。
+
+### Class 与方法
+
+| 位置 | 改动与职责 |
+| --- | --- |
+| `UiWorkspace`/`UiWorkspaceService.startSession` | 第二参数类型为 `StartSessionOptions`;在原 `beforeOpen` 准备点向准确的目标 binding 传入初始化请求。创建、复用与导航取消路径不变 |
+| `SessionInputResolver`/`InputHub.requestDraftInitialization` | 接收 binding 和 options,交给该 binding 的输入对象,不重新查找全局当前 Session |
+| `InputHub.shellFor` | 新对象登记及清理接线后,读取该 Session 保存的草稿,通过输入模型导入,然后返回;复用对象不再恢复 |
+| `stores.ts` 的 `readConversationDraft(sessionId)`/`draft.ts` 的 `parseStoredDraft` | 只读原存储 key;兼容字符串并检查结构化引用的位置和字段,不创建 store 或增加写入者 |
+| `SessionInputShell.setDraft` | 具体类的内部方法接受字符串或 `DraftSnapshot`,向编辑器发送一次文档导入;公开输入方法仅接受文本。两条路径均不模拟键盘事件、不提交内容 |
+| `SessionInputShell.requestDraftInitialization` | 在已经恢复的模型上执行清空/保留/填入规则;提交中拒绝修改,失败前不先清稿 |
+| `SessionInputShell.draftSnapshot`/`persistCurrentDraft()` | 按内容修订号缓存不含运行时 ID 的快照;`InputActions.persistDraft()` 委托显式保存方法;`bindDraftPersistence(write)` 仍只绑定回调 |
+| `DraftEditorRuntime.restoreDraft` | 接受引用的语义字段,重建 ReferenceChipNode;既有发送失败恢复继续使用同一实现 |
+| `SessionInputShell.refreshLexiconSubscription()`/`DraftEditorRuntime.refreshLexiconSubscription()` | `InputHub` 拥有 Session scope 的 `inject` 回调,在缓存输入对象后连接词典,服务到达或替换时重接,销毁时退订。连接和词典更新都会重扫当前 editor,不覆盖旧文本;`setDraft` 不负责建立订阅 |
+| `ConversationStoreState`/`createConversationStore` | draft 支持兼容旧字符串的结构化输入,View 状态及原 key 保持 |
+| `DefaultConversationViews` | 删除恢复/预填 effect;保留持久化回调的绑定和解绑。初始保存只能读取 owner 的当前快照,不能把 render 时的旧文字写回 |
+| `apply.ts` 中现有 `selectWorkspace` 回调 | 沿用原有跨工作区携带规则,但传递 `draftSnapshot` 而不是纯文本,防止携带时再次丢失引用 |
+
+不新增草稿服务、存储 key、通用任务框架或另一套 Session 创建入口;不改 SlotRenderer、Host Session 日志、命令执行和文件发现 RPC。`InputActions` 保留统一输入操作入口,组件不单独注入另一条内容初始化回调。
+
+调用参数依次为 `startSession(workspaceId?: WorkspaceId, options?: StartSessionOptions)`、`requestDraftInitialization(binding: SessionBinding, options: DraftInitializationOptions)`、`SessionInputShell.requestDraftInitialization(options)`,以及内部的 `SessionInputShell.setDraft(input: string | DraftSnapshot)`。`StartSessionOptions` 直接引用 `DraftInitializationOptions`,字段为 `prompt?: string` 和 `clearPreviousDraft?: boolean`;进入异步创建前浅拷贝这些值,结果只作用于最终保留的 binding。初始化结果区分已应用、保留原稿和提交期间拒绝;拒绝不先清空,导航被后续操作取消时不写稿。
+
+### 数据流
+
+```text
+stored draft / startSession options
+ → DraftInput
+ → SessionInputShell.setDraft
+ → DraftEditorRuntime → Lexical → InputState
+ → InputHub.shellFor returns
+ → React mounts the editor
+
+user edits / input actions
+ → editor → projection
+ → DraftSnapshot
+ → Conversation store
+```
+
+### 异步 skill 匹配
+
+skill 是可编辑文字,不转为文件胶囊。词典尚未返回时不锁草稿;斜杠名称先保持普通文字,词典提供匹配名称后才高亮。提供方缺席、词典不可用或名称不匹配时不显示 skill 高亮。数据到达后通过现有订阅机制重匹配当前文档,而不是保存请求发出时的文字并回写。此过程不修改正文和结构化引用,不抢光标、不增加用户 Undo 步骤。命令仍只在用户提交时走既有校验和执行路径。
+
+## 考虑过的替代方案
+
+**挂载后调用 setDraft。** 首次模型读取与显示仍可能为空,清空还可能被稍后的旧稿恢复覆盖;初始化责任不能依赖 React effect 的先后顺序。
+
+**只保存 clipboardText,再重新搜索引用。** 文字不足以表达引用的来源和展示字段;搜索候选也不等于恢复用户已经作出的选择。已有结构应直接恢复。
+
+**等待所有远程词典再开放输入。** 文件引用已有数据,skill 高亮也不是编辑前提;让网络查询阻塞草稿会引入不必要的等待。
+
+**拆分持久化 key 或新增草稿管理服务。** 现有输入 owner 和 store 已能承担职责,只需统一输入数据与导入路径。
+
+## 验证
+
+数据模型的核心约束是:有效草稿经过导入、导出和 JSON 往返后语义相等,运行时节点 ID 不参与比较;初次获取输入对象即具有完整文档;选区和 skill 高亮变化不改变草稿快照。[文档模型用例](../../../../packages/client/ui-conversation/tests/draft-document.client.spec.ts)覆盖字符串、空稿、多行与 Unicode、文件/目录/Session 引用、重复引用、无效标记、占位字符净化和非法存储跨度;[输入生命周期用例](../../../../packages/client/ui-conversation/tests/draft-hub.client.spec.ts)验证首次读取、复用、词典服务到达/卸载/重连和旧 binding 失效。提交中拒绝初始化,词典重匹配不增加 Undo 步骤。
+
+[导航用例](../../../../packages/client/ui-workspace/tests/workspaces-service.client.spec.ts)验证初始化作用于准确的保留对象、参数在异步创建前复制、导航取消不写稿、拒绝时保持原选择。[组合用例](../../../../packages/client/ui-conversation/tests/apply-inject.client.spec.tsx)验证现有工作区携带路径不把引用变成纯文本。相关回归共 44 个文件、708 项通过;共享草稿模块归位后,受影响的三个模型与组合文件再验证 66 项通过。
+
+[浏览器 e2e](../../../../apps/web/tests/draft-initialization.e2e.ts)通过已有模块加载观测方式取得实际 Workspace API,调用公开 `startSession` 传入文本选项,不新增测试插件。结构化 fixture 经既有已存草稿路径恢复。用例覆盖缺省、空字符串和非空提示词,明确传入及沿用当前工作区,保留与清空、创建与复用、真实界面的 Session 多轮切换和编辑、刷新、旧字符串及工作区选择器携带。延迟创建响应的两种返回顺序都保持较新的目标选择,并且不改动另一份草稿。延迟返回的真实 skill 目录只重匹配当前文字,不替换胶囊、选区或 Undo 历史。断言检查胶囊字段、文件预览和持久化快照。未点击发送前,Host 日志没有 `user/message` 或 `turn/start`。
+
+## 后果与边界
+
+旧字符串草稿缺失的引用信息不能无损推断,按普通文字恢复;不为兼容旧数据自动查询并猜测文件。附件文件对象、上传状态、跨浏览器同步、多个独立编辑器和完整 Undo 持久化不在本次范围。
+
+本决策与[编辑器隔离方案](../../proposed/architecture/2026-09-14-composer-model-and-draft-editor.zh.md)共享输入 owner 的归属,但不实现其多编辑器目标;[引用预览](../../../../packages/client/ui-input-trigger/README.zh.md)和[Session 引用生命周期](2026-09-15-client-session-references.zh.md)继续各自约束来源路由和代次清理,不被本次决策取代。
diff --git a/.agents/notes/implemented/architecture/2026-10-01-replace-changed-package-modules-on-recomposition.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-01-replace-changed-package-modules-on-recomposition.i18n.yaml
new file mode 100644
index 0000000000..576319b255
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-01-replace-changed-package-modules-on-recomposition.i18n.yaml
@@ -0,0 +1,34 @@
+# Bilingual-pair consistency record for 2026-10-01-replace-changed-package-modules-on-recomposition.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-01-replace-changed-package-modules-on-recomposition.md
+/agent-note-replace-plugins-whose-loaded-package-files-changed-during-profile-recomposition:
+ en: 72fe06d4e304418d
+ zh: bc3c96e957889bc1
+/agent-note-replace-plugins-whose-loaded-package-files-changed-during-profile-recomposition/problem:
+ en: c43fb0d2df54b28f
+ zh: 6636548449663fc5
+/agent-note-replace-plugins-whose-loaded-package-files-changed-during-profile-recomposition/decision:
+ en: a15c7c13f167e5ac
+ zh: a15c7c13f167e5ac
+/agent-note-replace-plugins-whose-loaded-package-files-changed-during-profile-recomposition/decision/one-recomposition-path:
+ en: f2874c3fc946cf7a
+ zh: 4c32b9f94e1f9709
+/agent-note-replace-plugins-whose-loaded-package-files-changed-during-profile-recomposition/decision/detecting-changed-modules:
+ en: 1fedf2dde87f41bf
+ zh: c2249267d1c2bc78
+/agent-note-replace-plugins-whose-loaded-package-files-changed-during-profile-recomposition/decision/replacing-them:
+ en: 6fe2cbe5aa3a9786
+ zh: fe30fbf672ea8c21
+/agent-note-replace-plugins-whose-loaded-package-files-changed-during-profile-recomposition/decision/plugin-manager-and-runtime-resolution:
+ en: 794ed33204d91014
+ zh: b08daa2a43cbe3b3
+/agent-note-replace-plugins-whose-loaded-package-files-changed-during-profile-recomposition/alternatives-considered:
+ en: a67d0c6bc4957060
+ zh: 32e02ab45b8e71cc
+/agent-note-replace-plugins-whose-loaded-package-files-changed-during-profile-recomposition/consequences:
+ en: 2cb3f39c6a910685
+ zh: e14938c868390f39
+/agent-note-replace-plugins-whose-loaded-package-files-changed-during-profile-recomposition/verification:
+ en: d4750abb810faac2
+ zh: 78c9c4d86d719b9b
diff --git a/.agents/notes/implemented/architecture/2026-10-01-replace-changed-package-modules-on-recomposition.md b/.agents/notes/implemented/architecture/2026-10-01-replace-changed-package-modules-on-recomposition.md
new file mode 100644
index 0000000000..f599d86407
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-01-replace-changed-package-modules-on-recomposition.md
@@ -0,0 +1,86 @@
+# Agent Note: Replace plugins whose loaded package files changed during profile recomposition
+
+Status: implemented
+
+English | [中文](2026-10-01-replace-changed-package-modules-on-recomposition.zh.md)
+
+## Problem
+
+Node caches an evaluated module by file URL. A profile installs packages with pnpm's hoisted layout, so a package lives at `node_modules/` whatever its version. Overwriting, upgrading or reinstalling it keeps every file URL, and a later import or Loader entry restart returns the cached module of the previous version. The [package-refresh Note](2026-09-30-profile-package-refresh-and-manifest-invalidation.md) refreshes runtime resolution and package configuration but leaves this module cache; Plugin Manager therefore reported overwrites as `restart-required`, and a reinstall at the same path after removal reported `applied` while the previous version kept running. A `link:` development package hits the same cache whenever its build output changes, unless its directory is listed in HMR's source watch roots.
+
+## Decision
+
+### One recomposition path
+
+`Hmr.reconcileProfile()` is the profile recomposition used by HMR's profile watches and by Plugin Manager; the Config Editor still recomposes through app-boot directly. It runs inside `runExclusive()`. Plugin Manager only edits profile files and runs pnpm, and refuses to install over an installed package it can name before pnpm runs; another version is a removal and an installation, each with its own recomposition. The steps of a recomposition are:
+
+1. Read the profile patches and compare loaded modules below the profile directory and its linked package targets with disk.
+2. Plan the replacement as below; a patch read or comparison failure throws or is reported before any module changes.
+3. Evict the replaceable changed modules and their importers. A changed module whose importers reach the process entry point's static graph or a plugin that no Loader entry owns stays loaded with its importers; the other changes proceed.
+4. Stop the Loader entries running the replaced plugins by deleting their runtimes, so the Loader does not record them as disabled.
+5. Call app-boot's `recomposeProfile()` once with the entries that ran before the stops. It records the activation state, stops the rows the generation no longer lists by removing them from their groups, publishes the runtime resolution, runs the compatibility preflight against the published packages, and applies the generation. The Loader imports the current files when it starts an entry: rows the generation keeps start once with the new code and their new configuration; rows it disables, refuses or removes stay stopped and never run the new code; an entry it inserts links against the current files. Stopped entries the update did not try to start, below an unchanged Include or outside the profile composition, start afterwards; an entry whose import failed during the update is imported once more. Activation is judged: a stopped entry that failed afterwards fails the operation, while an entry waiting for a service is a warning, since it starts once a provider appears. If no generation applied, the published resolution and the stopped rows are restored, and the stopped entries start again.
+6. Evict the loaded modules below the directories of the mappings the recomposition dropped, so a removed or deselected package, including its dynamically imported dependencies, is imported afresh when it returns. A bundle switched off while its package stays mapped keeps its unchanged modules.
+
+### Detecting changed modules
+
+[`ModuleFiles`](../../../../packages/boot/hmr/src/module-files.ts) records inode, size and modification time for the loaded ES and CommonJS modules below the profile directory and the real directories of its linked packages. Modules of the dsh installation are outside this scope. Change time is excluded: pnpm imports packages by hard link from its store, and adding a link elsewhere changes a loaded file's change time without changing its content. Records are taken at application readiness, after each recomposition, and for modules a source reload evaluated again. A recomposition records the identities it read before replacing anything, so a write during the recomposition stays detectable; modules first loaded during it are recorded after settlement to avoid an unnecessary restart during the next installation. An edit between first evaluation and that record can be missed; the timestamp fallback still covers modules first loaded outside recomposition. If the readiness observation fails, the fallback starts at HMR construction time, not the Unix epoch. A module is stale when its file disappeared or its identity differs. A module first loaded after the last recording has no record; it is stale when its file was modified at or after that recording, less a 2-second filesystem timestamp resolution, so a reinstall between loading and the next comparison is not missed. Version numbers are not consulted: lockfiles record no content identity for `file:` and `link:` dependencies, and file identity covers registry, tarball, directory and link installs alike.
+
+### Replacing them
+
+A stale module whose importer closure reaches the process entry point's static graph, including installed packages, stays loaded and is reported. The remaining stale modules and their static importers, across ES module links and CommonJS `require` children, form the affected set, and every manifest from a stale file up to its profile root or linked target expires through `PackageManifests.invalidate()`. The whole affected set is evicted without a backup before recomposition, so every entry that imports during recomposition reaches the current files. A plugin registered outside a Loader entry has no owner that can start its replacement, so the changes its modules import stay pending; plugins are read from the ES module namespace and from cached CommonJS exports, so a CommonJS plugin registered through `require` keeps its cache too. HMR imports no replacement code itself, so admission is the Loader's ordinary activation: a disabled, removed or refused row never evaluates the new module, and dynamic `disabled` expressions and nested Includes are evaluated as at startup. Every recomposition imports again each enabled entry whose last import failed, after expiring the package configuration below the profile roots and evicting the modules whose links or evaluation failed, which Node keeps cached, so a corrected file or a later-installed dependency takes effect without a restart. A plugin counts as owned only when an affected plugin's own Loader entry fiber starts it; a changed plugin another plugin registers through a dynamic import stays pending. Unchanged dependencies of a replaced plugin are not re-evaluated, so shared libraries keep one instance. Source reloads through `partialReload()` keep their own in-place replacement with rollback.
+
+`reconcileProfile()` returns `pending`, each stale module that loads only after a restart with its reason (`host-module`, `unmanaged-plugin`, or `module-comparison` when planning failed). Pending modules stay stale, including modules first loaded after the last recording, so the next recomposition retries them. A module first loaded during a recomposition whose file was modified after the comparison also stays stale: activation may have evaluated it before that write. A failure while listing or comparing modules keeps the previous records and sets the profile-wide `restartReason: module-comparison`, even without known module URLs. Neither prevents recomposition. An activation failure, including a replacement that fails to import, is thrown with the Loader's import error; an unchanged failure is returned as a warning by later recompositions. Plugin Manager reports a thrown failure as `failed` with that diagnostic, and turns `pending` into `restart-required` with its `restartReason`, unless the operation reports `overridden`. The Web page localizes the reason, and the agent tool derives a `nextStep` sentence from it, so no surface parses a diagnostic string.
+
+The [packed worker loader](../../../../packages/experimental/webworker-runtime/src/module-system/module-loader.ts) stores evaluated modules in a private map and its `require` exposes no cache. HMR cannot replace them through Node APIs and reports a restart requirement. Native Node still exposes its caches when using asynchronous module hooks.
+
+### Plugin Manager and runtime resolution
+
+Plugin Manager refuses to install over an installed package it can name before pnpm runs, so a new version or location is a removal and an installation: the removal stops the plugins, evicts their modules and drops the mappings, and the installation maps the package afresh. Runtime resolution accepts a retained profile mapping whose version changes while its normalized directory and scope stay, as a shared dependency upgraded by another installation does, and rejects one that moved to another directory. A linked root may point to another directory; a file Node's cached link real path selects below the previous target, for an importer outside it, is relocated below the current one. Rejected publications, including changes to installation mappings or local overrides, leave the resolution and the composition unchanged and report `restart-required` with reason `package-moved`.
+
+## Alternatives considered
+
+**A separate package-replacement call from Plugin Manager.** Plugin Manager knows which package it installed, but not which Loader entries import it, and row additions, removals and configuration changes in the new bundle patch must apply in the same step. A second operation would split one profile change into two transactions with an undefined order. Recomposition already reads the new profile; comparing loaded files there covers Plugin Manager, CLI installs followed by a recomposition, and link builds.
+
+**Compare package versions from the profile manifest or lockfile.** The profile manifest stores specs, not resolved versions, and `pnpm update` can change a version without changing it. Lockfiles carry no content identity for `file:` and `link:` dependencies, and a same-version reinstall of a tarball changes content without changing the version.
+
+**Let app-boot call HMR from inside its recomposition.** Hooks for the steps before applying and before judging would split the order of one operation between two owners. HMR instead stops everything it replaces first and passes the stopped entries to one app-boot call.
+
+**Expose the recomposition's stages to HMR.** Separate calls to record activation, prepare after publication, apply, restore and drop mappings let HMR insert its stops between them, but they leave callers ordering obligations that the types cannot express. Stopping before the call costs a restart of the stopped plugins when preparation fails.
+
+**Publish twice, keeping the dropped mappings while the generation applies.** A temporary table with both the new and the dropped mappings lets one Loader update stop and start entries, but it is a third generation that must stay recomputable when the update fails. Stopping the dropped rows first lets one publication follow them.
+
+**Overwrite an installed bundle in one operation.** Stopping the bundle before pnpm and starting it afterwards, or detecting moved packages from the loaded module files, keeps one call for an upgrade, but needs a restart path for every pnpm failure, a selection restored in its order, and a release rule for moved directories. Refusing the overwrite leaves a removal and an installation, which the existing operations already run in this order.
+
+**Preload the replacement and keep the previous plugin when it fails to load.** HMR would import new code before stopping the previous runtime and restore the previous caches on failure. Preloading must not evaluate a row the generation disables, removes or refuses, so HMR would decide admission before the Loader does; dynamic `disabled` expressions and nested Includes cannot be decided without activation, so those changes would need a restart. Rollback must also remove new importers of restored modules, and the previous code would still receive the new configuration. A restart would load the same broken files, so keeping the previous plugin only delays a failure that needs a fix anyway.
+
+**Restart replaced plugins in place before recomposition.** New code would start with the previous configuration, which an upgraded version may reject, and a row the new composition disables or refuses, for example an incompatible version left on disk by a failed install, would briefly run.
+
+**Replace running plugins after recomposition.** Recomposition would restart the previous code with the new configuration, which the previous version may reject, so a successful upgrade would be reported as a failure; an entry inserted during recomposition would also link against the previous copy of a changed dependency still cached for a running plugin.
+
+**Stop affected plugins before pnpm runs.** This prevents previous code from lazily loading files pnpm has already replaced, but adds downtime for the whole install and holds the HMR queue during pnpm.
+
+**Re-evaluate every dependency of a replaced plugin.** Source reloads reload a plugin's non-`node_modules` dependency tree. Doing the same below `node_modules` would create second instances of unchanged shared libraries and Cordis packages used by other plugins.
+
+**Record identities in a module load hook.** `module.registerHooks()` could record each file at load time and close the window for modules loaded between recordings, but it adds a hook to every module load and interacts with TSX and the vendored Loader's internals. The timestamp rule only causes an extra reload in that window.
+
+## Consequences
+
+- Upgrades through a removal and an installation, and packages whose files changed in place, take effect without a restart when the replacement loads and starts; a replacement that fails to load or start stops the previous plugin and reports `failed` with the error, and it starts once its files or dependencies are corrected.
+- Each recomposition stats every loaded file of the profile; the module graph is walked when a file changed, and the host graph is collected on first use.
+- Dynamic `import()` edges are not followed: an importer keeps running while its next dynamic import evaluates the new file. Existing Workers and the TSX loader thread keep their caches.
+- Replaced Loader entries stop before recomposition and start once afterwards; the previous version does not return.
+- Loader entries outside the profile composition restart with replaced code without a new compatibility check ([limitation](../../../../packages/boot/hmr/README.md#known-limitations-and-deferred-work)).
+- `ModuleFiles` belongs to one HMR instance. A recomposition that restarts the `hmr` entry itself starts a new instance whose first observation records the current files, so changes pending at that point are no longer reported or retried.
+- `pending` describes the whole profile, so a host module brought into scope by a linked workspace package makes later unrelated operations report `restart-required` until the process restarts.
+- A file replaced by a hard link to an older store file keeps an older modification time; when its module was first loaded after the last recording, the change is not detected.
+- A change confined to `package.json` is not detected. HMR's profile watches do not recompose for dependency-only manifest changes, so installs performed outside Plugin Manager wait for the next recomposition or restart.
+
+## Verification
+
+| Coverage | Location |
+|---|---|
+| In-place upgrades with bundle patch changes, configurations the previous version rejects, inserted entries and disabled rows, plugins without a Loader entry, shared-dependency importers, linked packages, hard links, CommonJS `require` children and their failures, failed replacement and retry, nested groups and Includes, dynamic disabling, same-path reinstall, late-loaded modules, host-graph modules, comparison and recomposition failures, eviction of packages a recomposition stopped using or whose mappings it dropped, and the restart of stopped entries after a rejected publication or failed preparation | `packages/boot/hmr/tests/package-reload.spec.ts` |
+| Refused overwrites, removal then installation with HMR, failed installation, relinked and link-to-registry reinstalls, unmanaged plugins, comparison failures | `packages/boot/plugin-manager/tests/package-reload.spec.ts`, `packages/boot/plugin-manager/tests/manager.spec.ts` |
+| In-place profile mapping version change, retained directory and scope rejection including a relinked path inside the profiles tree, relinked roots and native lookups relocated below the current target | `packages/boot/app-boot/tests/profile-resolution.spec.ts` |
+
+Tests need no API key and make no model calls.
diff --git a/.agents/notes/implemented/architecture/2026-10-01-replace-changed-package-modules-on-recomposition.zh.md b/.agents/notes/implemented/architecture/2026-10-01-replace-changed-package-modules-on-recomposition.zh.md
new file mode 100644
index 0000000000..35ba5c98e9
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-01-replace-changed-package-modules-on-recomposition.zh.md
@@ -0,0 +1,86 @@
+# Agent Note: profile 重新组合时替换已加载包文件发生变化的插件
+
+Status: implemented
+
+[English](2026-10-01-replace-changed-package-modules-on-recomposition.md) | 中文
+
+## Problem
+
+Node 按文件 URL 缓存已求值的模块。profile 使用 pnpm 的 hoisted 布局安装包,因此无论版本如何,包都位于 `node_modules/`。覆盖、升级或重装包时所有文件 URL 保持不变,之后的导入或 Loader entry 重启返回的是旧版本的缓存模块。[包刷新说明](2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md)刷新 runtime resolution 和包配置,但不处理这一模块缓存;因此 Plugin Manager 把覆盖报告为 `restart-required`,而删除后在同一路径重装时报告 `applied`,旧版本却仍在运行。`link:` 开发包的构建输出变化时也会命中同一缓存,除非其目录列在 HMR 的源码监听根目录中。
+
+## Decision
+
+### 唯一的重新组合路径
+
+`Hmr.reconcileProfile()` 是 HMR 的 profile 监听和 Plugin Manager 共用的 profile 重新组合入口;Config Editor 仍直接通过 app-boot 重新组合。它在 `runExclusive()` 内运行。Plugin Manager 只编辑 profile 文件并运行 pnpm;对于在运行 pnpm 之前能确定包名的已安装包,它拒绝覆盖安装;安装另一个版本需要先卸载再安装,每一步各有一次重新组合。一次重新组合的步骤如下:
+
+1. 读取 profile patch,将 profile 目录及其已链接包目标之下的已加载模块与磁盘比较。
+2. 按下文规划替换;patch 读取或比较失败时,在任何模块变化之前抛出错误或报告失败。
+3. 逐出可替换的已变化模块及其导入方。若已变化模块的导入方可到达进程入口的静态依赖图,或可到达没有 Loader entry 拥有的插件,该模块连同其导入方保持加载;其余变化照常处理。
+4. 通过删除运行时停止运行被替换插件的 Loader entry,使 Loader 不把它们记录为已停用。
+5. 以停止之前正在运行的条目调用一次 app-boot 的 `recomposeProfile()`。它记录激活状态,把新一代组合不再列出的行从其所在组中移除,以停止这些行,发布 runtime resolution,基于已发布的包运行兼容性预检,然后应用新一代组合。Loader 在启动条目时导入当前文件:新一代组合保留的行以新代码和新配置启动一次;停用、拒绝或删除的行保持停止,不会运行新代码;新插入的条目链接当前文件。更新未尝试启动的已停止条目(位于未变化 Include 之下或 profile 组合之外)随后启动;更新期间导入失败的条目会再导入一次。随后判定激活结果:之后失败的已停止条目使操作失败;等待服务的条目只是警告,因为出现提供方后它即启动。若没有新一代组合被应用,已发布的 resolution 和已停止的行被恢复,已停止的条目重新启动。
+6. 逐出重新组合删除的映射所在目录之下的已加载模块,使已删除或取消选入的包连同其动态导入的依赖在重新加入时重新导入。关闭组合包而其包仍有映射时,它未变化的模块保持加载。
+
+### 检测已变化的模块
+
+[`ModuleFiles`](../../../../packages/boot/hmr/src/module-files.ts) 为 profile 目录及其已链接包的真实目录之下已加载的 ES 和 CommonJS 模块记录 inode、大小和修改时间。dsh 安装目录中的模块不在此范围内。不记录状态变更时间:pnpm 通过硬链接从其 store 导入包,在其他位置添加链接会改变已加载文件的状态变更时间,而内容不变。记录发生在应用就绪时和每次重新组合之后;源码重载重新求值模块时,也为这些模块记录。重新组合记录的是它在替换任何模块之前读取的文件身份,因此重新组合期间发生的写入仍可被检测到;在重新组合期间首次加载的模块在结算后记录,避免下一次安装时发生不必要的重启。首次求值与记录之间的修改可能漏检;时间戳兜底仍覆盖重新组合之外首次加载的模块。就绪时的观察失败时,兜底从 HMR 构造时间开始,而非 Unix 纪元。文件已消失或身份不同的模块视为陈旧。在上次记录之后才首次加载的模块没有记录;若其文件的修改时间不早于该次记录时间减去 2 秒文件系统时间戳精度,则视为陈旧,因此不会漏掉在加载与下一次比较之间发生的重装。比较不参考版本号:lockfile 不为 `file:` 和 `link:` 依赖记录内容标识,而文件身份同样覆盖 registry、tarball、目录和 link 安装。
+
+### 替换
+
+导入方闭包可到达进程入口静态依赖图(包括已安装包)的陈旧模块保持加载,并被报告。其余陈旧模块及其静态导入方(沿 ES 模块链接和 CommonJS `require` 子模块)构成受影响集合,从每个陈旧文件到其 profile 根目录或链接目标的各级 manifest 都通过 `PackageManifests.invalidate()` 失效。整个受影响集合在重新组合之前被逐出且不保留备份,因此重新组合期间执行导入的每个条目都读取当前文件。在 Loader entry 之外注册的插件没有能启动其替换版本的所有者,因此其模块导入的变化保持待处理;插件从 ES 模块命名空间和已缓存的 CommonJS exports 中读取,因此通过 `require` 注册的 CommonJS 插件同样保留其缓存。HMR 本身不导入替换代码,因此是否允许加载由 Loader 的普通激活决定:停用、删除或被拒绝的行永远不会对新模块求值,动态 `disabled` 表达式和嵌套 Include 按启动时的方式求值。每次重新组合都会先让 profile 根目录下的包配置失效,并逐出链接或求值失败的模块(Node 会在缓存中保留这些模块),再次导入上次导入失败的每个已启用条目,因此修正后的文件或之后安装的依赖无需重启即生效。只有当受影响插件自身的 Loader entry fiber 启动某个插件时,该插件才算有所有者;由另一个插件通过动态导入注册的已变化插件保持待处理。被替换插件中未变化的依赖不会重新求值,因此共享库保持单一实例。通过 `partialReload()` 进行的源码重载保留其自身带回滚的原地替换。
+
+`reconcileProfile()` 返回 `pending`,即每个只有重启后才能加载的陈旧模块及其原因(`host-module`、`unmanaged-plugin`,规划失败时为 `module-comparison`)。待处理模块保持陈旧,包括在上次记录之后才首次加载的模块,因此下一次重新组合会重试它们。在重新组合期间首次加载、且其文件在比较之后被修改的模块同样保持陈旧:激活可能在该写入之前已对其求值。列出或比较模块时发生的失败会保留原有记录,并设置 profile 级 `restartReason: module-comparison`,即使无法确定模块 URL。两种情况都不会阻止重新组合。激活失败(包括替换版本导入失败)会连同 Loader 的导入错误一起抛出;未变化的失败在后续重新组合中作为警告返回。Plugin Manager 把抛出的失败报告为带该诊断的 `failed`,把 `pending` 转为带其 `restartReason` 的 `restart-required`,除非操作报告 `overridden`。Web 页面本地化该原因,agent 工具据此生成一句 `nextStep`,因此没有任何界面解析诊断字符串。
+
+[打包 worker loader](../../../../packages/experimental/webworker-runtime/src/module-system/module-loader.ts) 在私有映射中保存已求值模块,其 `require` 不暴露缓存。HMR 无法通过 Node API 替换它们,因此报告需要重启。原生 Node 使用异步模块 hook 时仍暴露其缓存。
+
+### Plugin Manager 与 runtime resolution
+
+对于在运行 pnpm 之前能确定包名的已安装包,Plugin Manager 拒绝覆盖安装,因此新版本或新位置需要先卸载再安装:卸载停止插件、逐出其模块并删除映射,安装再重新映射该包。runtime resolution 接受保留的 profile 映射在规范化目录和作用域不变时更改版本,即共享依赖被另一次安装升级的情形;移动到其他目录的映射被拒绝。linked root 可以指向其他目录;对于原目标之外的导入方,Node 缓存的链接真实路径若选中原目标之下的文件,该文件会被重新定位到当前目标之下。被拒绝的发布,包括对安装范围映射或本地覆盖的更改,不改变 resolution 和组合,并报告原因是 `package-moved` 的 `restart-required`。
+
+## Alternatives considered
+
+**由 Plugin Manager 单独调用包替换。** Plugin Manager 知道自己安装了哪个包,但不知道哪些 Loader entry 导入了它;新组合包 patch 中的行增加、删除和配置变更也必须在同一步应用。第二个操作会把一次 profile 变更拆成两个顺序未定义的事务。重新组合本来就会读取新的 profile;在此处比较已加载文件,同时覆盖 Plugin Manager、之后执行重新组合的 CLI 安装以及 link 构建。
+
+**比较 profile manifest 或 lockfile 中的包版本。** profile manifest 存储的是 spec,不是解析后的版本,`pnpm update` 可以在不修改 spec 的情况下改变版本。lockfile 不为 `file:` 和 `link:` 依赖记录内容标识,而同版本重装 tarball 会改变内容却不改变版本。
+
+**让 app-boot 在其重新组合内部调用 HMR。** 为应用之前和判定之前的步骤提供 hook,会把一次操作的顺序拆分给两个所有者。HMR 改为先停止它替换的所有内容,再把已停止的条目传给一次 app-boot 调用。
+
+**向 HMR 暴露重新组合的各个阶段。** 分别调用记录激活状态、发布后准备、应用、恢复和丢弃映射,可让 HMR 在其间插入停止操作,但会给调用方留下类型无法表达的顺序义务。在调用之前停止插件,代价是准备失败时需要重新启动已停止的插件。
+
+**发布两次,在应用新一代组合期间保留被丢弃的映射。** 同时包含新映射和被丢弃映射的临时表,可以让一次 Loader 更新既停止又启动条目,但它是第三个 generation,更新失败时必须仍能重新计算。先停止被丢弃的行,之后只需发布一次。
+
+**在一次操作中覆盖已安装的组合包。** 在 pnpm 运行之前停止该组合包、之后再启动,或根据已加载模块文件检测已移动的包,可以让升级只需一次调用;但这需要为每种 pnpm 失败提供重启路径、按原顺序恢复选择列表,并为已移动的目录制定释放规则。拒绝覆盖安装后只剩卸载和安装,现有操作已按此顺序执行。
+
+**预加载替换版本,并在其加载失败时保留原插件。** HMR 将在停止旧运行时之前导入新代码,并在失败时恢复旧缓存。预加载不得对新一代组合停用、删除或拒绝的行求值,因此 HMR 要先于 Loader 决定是否允许加载;动态 `disabled` 表达式和嵌套 Include 不经激活就无法判定,因此这些变化需要重启。回滚还必须删除已恢复模块的新导入方,而旧代码仍会收到新配置。重启会加载相同的损坏文件,因此保留原插件只是推迟一个无论如何都需要修复的失败。
+
+**在重新组合之前原地重启被替换的插件。** 新代码会先以旧配置启动,而升级后的版本可能拒绝旧配置;新组合停用或拒绝的行(例如安装失败后留在磁盘上的不兼容版本)也会短暂运行。
+
+**在重新组合之后替换运行中的插件。** 重新组合会以新配置重启旧代码,而旧版本可能拒绝新配置,导致一次成功的升级被报告为失败;重新组合期间插入的条目也会链接到仍为运行中插件缓存的、已变化依赖的旧副本。
+
+**在 pnpm 运行前停止受影响插件。** 这能防止旧代码惰性加载 pnpm 已经替换的文件,但会让整个安装期间插件不可用,并在 pnpm 运行期间占用 HMR 队列。
+
+**重新求值被替换插件的全部依赖。** 源码重载会重载插件在 `node_modules` 之外的依赖树。若对 `node_modules` 之下做同样处理,会为其他插件使用的、未变化的共享库和 Cordis 包创建第二个实例。
+
+**在模块加载 hook 中记录文件身份。** `module.registerHooks()` 可以在加载时记录每个文件,消除两次记录之间加载的模块的检测空窗,但这会给每次模块加载增加一个 hook,并与 TSX 和 vendored Loader 的内部机制相互影响。时间戳规则在该空窗内只会导致一次多余的重载。
+
+## Consequences
+
+- 通过卸载再安装进行的升级,以及文件被原地更改的包,在替换版本成功加载并启动时无需重启即生效;替换版本加载或启动失败时,原插件停止并报告 `failed` 及该错误,其文件或依赖被修正后它即启动。
+- 每次重新组合对 profile 的所有已加载文件执行一次 stat;有文件变化时才遍历模块图,宿主依赖图在首次使用时收集。
+- 不跟踪动态 `import()` 边:导入方继续运行,而其下一次动态导入会求值新文件。已存在的 Worker 和 TSX loader 线程保留各自的缓存。
+- 被替换的 Loader entry 在重新组合之前停止,之后启动一次;旧版本不会恢复。
+- profile 组合之外的 Loader entry 以替换后的代码重启,不重新进行兼容性检查([限制](../../../../packages/boot/hmr/README.zh.md#known-limitations-and-deferred-work))。
+- `ModuleFiles` 属于单个 HMR 实例。重启 `hmr` 条目本身的重新组合会启动一个新实例,其首次观察记录当前文件,因此此时待处理的变化不再被报告或重试。
+- `pending` 描述整个 profile,因此由已链接工作区包纳入范围的宿主模块,会使后续无关操作也报告 `restart-required`,直到进程重启。
+- 若文件被替换为指向较旧 store 文件的硬链接,它保留较旧的修改时间;当其模块在上次记录之后才首次加载时,该变化不会被检测到。
+- 仅限于 `package.json` 的变化不会被检测到。HMR 的 profile 监听不会因仅涉及依赖的 manifest 变化而重新组合,因此在 Plugin Manager 之外执行的安装要等到下一次重新组合或重启。
+
+## Verification
+
+| 覆盖面 | 位置 |
+|---|---|
+| 伴随组合包 patch 变更的原地升级、旧版本拒绝的配置、新插入的条目和停用行、没有 Loader entry 的插件、共享依赖的导入方、已链接包、硬链接、CommonJS `require` 子模块及其失败、替换失败与重试、嵌套 group 和 Include、动态停用、同路径重装、延后加载的模块、宿主依赖图中的模块、比较失败与重新组合失败、逐出重新组合不再使用或已删除其映射的包,以及发布被拒绝或准备失败后已停止条目的重启 | `packages/boot/hmr/tests/package-reload.spec.ts` |
+| 被拒绝的覆盖安装、有 HMR 时先卸载再安装、安装失败、重新链接和从链接改为 registry 的重装、非托管插件、比较失败 | `packages/boot/plugin-manager/tests/package-reload.spec.ts`、`packages/boot/plugin-manager/tests/manager.spec.ts` |
+| profile 映射原地更改版本、保留目录和作用域的拒绝(包括 profiles 树内重新链接的路径)、重新链接的 root 以及被重新定位到当前目标之下的原生查询 | `packages/boot/app-boot/tests/profile-resolution.spec.ts` |
+
+测试不需要 API key,不调用模型。
diff --git a/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.i18n.yaml
new file mode 100644
index 0000000000..c8cf21b92d
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-10-05-anonymous-reasoning-translation.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.md
+/agent-note-anonymous-translation-remains-separate-from-model-history:
+ en: ab8fee99b1f6d828
+ zh: 0e324a42eebc1403
+/agent-note-anonymous-translation-remains-separate-from-model-history/problem:
+ en: 5e10347fa87f98b5
+ zh: cf3569631af8d66f
+/agent-note-anonymous-translation-remains-separate-from-model-history/decision:
+ en: 3c4745cc96d89ffd
+ zh: 939d5cd494346311
+/agent-note-anonymous-translation-remains-separate-from-model-history/alternatives-considered:
+ en: 083d50f8564b0378
+ zh: 50af332e64ad06f3
+/agent-note-anonymous-translation-remains-separate-from-model-history/consequences:
+ en: 57c774c6fb14a1b4
+ zh: 5a05393e6f1d25b1
diff --git a/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.md b/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.md
new file mode 100644
index 0000000000..1e67961bf1
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.md
@@ -0,0 +1,31 @@
+# Agent Note: Anonymous translation remains separate from model history
+
+Status: implemented
+
+English | [中文](2026-10-05-anonymous-reasoning-translation.zh.md)
+
+## Problem
+
+Readers need reasoning in their chosen language without another model credential or replacement of the original assistant output. Translation needs an owner for external requests, cancellation, and failures independent of Agent execution.
+
+## Decision
+
+The experimental translator owns explicit Google or Bing routing, provider-language normalization, and bounded network requests. Its Service Definition and both providers share one package because their current consumers use the same operation and lifetime. The optional GUI consumer owns settings, paragraph scheduling, per-disclosure caches, and translated display. Its authenticated Remote calls the translator; the agent loop and Session event types remain independent.
+
+Bing is the configured default. Enabling the bundle permits transmission of expanded reasoning to the chosen service, disclosed in plugin metadata and settings. Failure never selects another provider. Original remains available; translated text neither replaces persisted assistant output nor enters model input. The [optional-bundle admission policy](2026-09-21-experimental-capabilities-as-optional-bundles.md) keeps the feature disabled until selected.
+
+The GUI registers a translation wrapper in the standard `conversation.chat.reasoning.body` Slot and calls the official `conversation.chat.reasoning.content` Factory for original or translated text. The default body is also an ordinary registration; the host supplies no inline Markdown fallback or plugin action state. The translation wrapper owns its toolbar and passes its localized labels to the Factory, which supplies default labels when callers omit them and fixes compact typography. The [reasoning Body Slot and Content Factory design](2026-10-09-reasoning-content-slot-factory.md) supersedes this record's body composition mechanism; external requests remain separate from model history. Consumers do not replace the assistant renderer or alter Session projection. Only expanded bodies start translation. Collapsing, changing selection, or unloading aborts outstanding work and excludes late results.
+
+## Alternatives considered
+
+**Translate through the conversation's LLM provider.** This adds model requests and depends on model credentials, pricing, and routing. Anonymous translation has separate availability and requires none of those credentials.
+
+**Rewrite reasoning in the Session or provider stream.** This changes the original trajectory and can affect model reasoning passback. Display translation preserves the original model history.
+
+**Silently try another endpoint after failure.** This sends potentially private text to another operator without an explicit selection. Retry uses the same provider; switching providers is a user action.
+
+## Consequences
+
+Anonymous translation adds no third-party translation library or model-token usage. Anonymous endpoints provide no developer availability guarantee. Both providers send source text in POST request bodies. Translations may alter Markdown or code formatting and do not translate collapsed summaries.
+
+The translator retains Session-bound results under the [shared machine-translation persistence decision](2026-10-05-persistent-machine-translation.md); its stateless calls do not record results.
diff --git a/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.zh.md b/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.zh.md
new file mode 100644
index 0000000000..4c2bb2854c
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.zh.md
@@ -0,0 +1,31 @@
+# Agent Note: 免登录翻译独立于模型历史
+
+Status: implemented
+
+[English](2026-10-05-anonymous-reasoning-translation.md) | 中文
+
+## 问题
+
+读者需要用指定语言阅读思考内容,同时无需额外模型凭据,也不替换原始 assistant 输出。翻译需要独立于 Agent 执行的所有者,负责外部请求、取消与失败。
+
+## 决策
+
+实验性 translator 负责显式 Google 或 Bing 路由、Provider 语言标签映射及有界网络请求。Service Definition 与两种 Provider 共用一个包,因为当前消费者使用相同操作与生命周期。可选 GUI 消费者负责设置、段落调度、展开组件内缓存及译文展示。其经过身份验证的 Remote 调用 translator;agent loop 与 Session 事件类型保持独立。
+
+配置的默认服务为 Bing。启用 Bundle 允许将展开的思考内容发送给所选服务,插件元数据与设置公开说明接收方。失败时不选择其他 Provider。原文始终可访问;译文不替换持久化 assistant 输出,也不进入模型输入。[可选 Bundle 准入规则](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md) 保证功能在显式选择前保持关闭。
+
+GUI 向标准 `conversation.chat.reasoning.body` Slot 注册翻译包装,并调用官方 `conversation.chat.reasoning.content` Factory 展示原文或译文。默认正文同样是普通注册项;宿主不提供内联 Markdown fallback 或插件操作状态。翻译包装自行维护工具栏,并将自己的本地化 labels 传入 Factory;Factory 在调用者省略 labels 时提供默认文案,并固定紧凑排版。[思考正文的 Slot 与 Factory 设计](2026-10-09-reasoning-content-slot-factory.zh.md)取代本记录的正文组合方式,外部请求与模型历史隔离的决策保持不变。消费者不替换 assistant 渲染器,也不改变 Session projection。只有展开内容才开始翻译。折叠、切换设置或卸载都会中止未完成工作并忽略迟到结果。
+
+## 考虑过的替代方案
+
+**通过对话使用的 LLM Provider 翻译。** 这会增加模型请求,并依赖模型凭据、定价与路由。免登录翻译有独立的可用性,且无需这些凭据。
+
+**重写 Session 或 Provider 流中的思考内容。** 这会改变原始轨迹,并可能影响思考内容回传。展示翻译保留原始模型历史。
+
+**失败后静默尝试另一端点。** 这会在没有显式选择的情况下将潜在私有文本发送给另一个运营方。重试使用同一 Provider;切换 Provider 是用户操作。
+
+## 后果
+
+匿名翻译不增加第三方翻译库或模型 token 消耗。免登录端点不保证开发者可用性。两种服务均通过 POST 请求体发送原文。译文可能改变 Markdown 或代码格式,也不翻译折叠摘要。
+
+translator 按[统一的机器翻译持久化决策](2026-10-05-persistent-machine-translation.zh.md)保留绑定 Session 的结果;无状态调用不记录结果。
diff --git a/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.i18n.yaml
new file mode 100644
index 0000000000..3060e831f9
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-10-05-official-on-demand-bundles.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.md
+/agent-note-discover-official-bundles-without-installing-their-runtimes:
+ en: 1f2387f10628638b
+ zh: c835a8ee615e9da6
+/agent-note-discover-official-bundles-without-installing-their-runtimes/problem:
+ en: a590bcef6c9760c0
+ zh: 19e6f61c2c7d8d3b
+/agent-note-discover-official-bundles-without-installing-their-runtimes/decision:
+ en: f0fabc0d4fbd1178
+ zh: cf19938a7a7dea93
+/agent-note-discover-official-bundles-without-installing-their-runtimes/alternatives-considered:
+ en: 42cb0f808a145871
+ zh: 225012615d1ecdf5
+/agent-note-discover-official-bundles-without-installing-their-runtimes/consequences:
+ en: 293286dcd7e3325e
+ zh: c6c15212e85d050b
diff --git a/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.md b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.md
new file mode 100644
index 0000000000..a67acd25f9
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.md
@@ -0,0 +1,37 @@
+# Agent Note: Discover Official bundles without installing their runtimes
+
+Status: implemented
+
+English | [中文](2026-10-05-official-on-demand-bundles.zh.md)
+
+## Problem
+
+Native subagent integrations bring their own runtimes. Shipping those dependencies with every DSH installation charges that cost to people who never delegate to them, while requiring a package name hides the integrations from the Plugins page.
+
+## Decision
+
+`ON_DEMAND_BUNDLES` admits the Claude Code and Codex bundles to an installation-owned catalog. The catalog embeds their package-owned localized metadata and icons, so discovery works offline without resolving or importing either package. Official identifies project maintenance; optional delivery and experimental maturity remain independent properties.
+
+Selection uses the ordinary third-party bundle installer, profile dependency, Host row patches, compatibility evaluation, progress, cancellation, registry fallback, and build-script approval. Native authentication and configuration remain provider-owned.
+
+The running installation owns the target. Published applications offer the exact DSH release version and save an exact registry dependency pin. Source Web and development Desktop offer ordinary `link:` dependencies into the same checkout as the running CLI, using its prepared workspace dependencies and peers. The source target follows the actual installation, never the current working directory or a prerelease suffix; an incomplete checkout does not fall back to the registry. A development checkout can share a version string with different published code, so replacing an installed registry copy or another checkout’s link compares the source as well as the version. Replacement is explicit, preserves selection, and reports required restarts; startup does not download providers. [Plugin Manager](../../../../packages/boot/plugin-manager/README.md#use-this-package) owns the operational details.
+
+Every Official package remains in the DSH release family. Packed DSH dependencies, optional dependencies, and peers use the exact family version. Publication prerequisites place catalog packages before the DSH package advertising them without adding their runtimes to DSH production dependencies. Catalog verification rejects missing metadata, unsupported membership, and stale generated content.
+
+The [shipped optional-bundle policy](../process/2026-09-15-shipped-optional-bundles.md) still owns lightweight installation-provided bundles. The [optional-composition decision](2026-09-21-experimental-capabilities-as-optional-bundles.md) owns shared patch behavior and global optional tools. Native bundles insert ordinary Host rows, so any profile can select them; profiles that keep the package without its bundle mount the same rows explicitly. The [identity compatibility decision](2026-10-06-web-desktop-plugin-identity-compatibility.md) owns retained names, selectors, and deferred naming cleanup.
+
+## Alternatives considered
+
+**Ship the native runtimes with every installation.** This makes first activation offline, but increases the default download for unused integrations.
+
+**Query the registry to build the discovery list.** This avoids embedded metadata, but makes discovery depend on network availability and does not bind the offered package version to the running application.
+
+**Give Official packages a separate installer or activation registry.** A second path would duplicate package-operation and profile-composition behavior. Installation-owned discovery data can reuse the existing third-party mechanisms.
+
+**Always install the registry package with the running version.** Version equality identifies a published release, but cannot identify unreleased source changes. It can deliver an older provider API and a provider-only patch to a development Host that expects current tool contributions.
+
+**Build a separate development registry or artifact cache.** Ordinary local links already keep provider code and its dependencies in the running checkout. A second delivery system would add lifecycle and invalidation work without improving that development promise.
+
+## Consequences
+
+Registry delivery requires a reachable registry; development delivery requires a prepared, built checkout and normal rebuild/restart discipline. Equal source version strings do not certify identical build outputs. Off only deselects; Remove remains separate and refuses while the bundle's rows are in use. Activation failure can retain the saved dependency and selection; there is no rollback. Offline metadata, release pins, source-target selection, same-version source mismatches, clean installed artifacts, and release ordering have dedicated checks. Native-provider authentication is not part of catalog discovery.
diff --git a/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.zh.md b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.zh.md
new file mode 100644
index 0000000000..0949f6b72c
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.zh.md
@@ -0,0 +1,37 @@
+# Agent Note: 发现 Official bundle 而不安装其运行时
+
+Status: implemented
+
+[English](2026-10-05-official-on-demand-bundles.md) | 中文
+
+## 问题
+
+原生子智能体集成携带自己的运行时。把这些依赖放进每次 DSH 安装,会让从不向它们委派任务的用户也承担成本;要求用户知道包名,又会让插件页无法展示这些集成。
+
+## 决策
+
+`ON_DEMAND_BUNDLES` 将 Claude Code 和 Codex bundle 纳入安装方维护的目录。目录内嵌包自身拥有的本地化元信息和图标,因此发现过程离线可用,无需解析或导入这两个包。Official 表示项目维护;可选交付与实验成熟度仍是独立属性。
+
+选择操作复用普通第三方 bundle 的安装器、profile 依赖、Host 行 patch、兼容性判断、进度、取消、注册表回退和构建脚本审批。原生认证与配置仍由提供方拥有。
+
+当前运行的安装决定目标。已发布应用提供精确的 DSH 发布版本,并保存精确的注册表依赖版本。源码 Web 和开发 Desktop 提供指向当前 CLI 所在 checkout 的普通 `link:` 依赖,使用其中已准备好的工作区依赖和 peer。源码目标跟随实际安装位置,不由当前工作目录或预发布版本后缀决定;不完整的 checkout 不回退到注册表。开发 checkout 可以与内容不同的已发布代码共用版本字符串,因此替换已安装的注册表副本或其他 checkout 的链接时,同时比较来源和版本。替换需显式触发,保留选择状态,并报告是否需要重启;启动时不下载提供方。[插件管理器](../../../../packages/boot/plugin-manager/README.zh.md#use-this-package)拥有操作细节。
+
+所有 Official 包仍属于 DSH 发布家族。打包后的 DSH 依赖、可选依赖与 peer 均使用家族精确版本。发布前置关系要求先发布目录包,再发布宣传它们的 DSH 包,同时不把这些运行时加入 DSH 生产依赖。目录校验拒绝缺失的元信息、不受支持的成员和过期生成内容。
+
+[随附可选 bundle 策略](../process/2026-09-15-shipped-optional-bundles.zh.md)仍管理安装随附的轻量 bundle。[可选组合决策](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)管理共享 patch 行为与全局可选工具。原生 bundle 插入普通 Host 行,因此任何 profile 都可以选择它们;保留包但不选择其 bundle 的 profile 显式挂载相同的行。[身份兼容决策](2026-10-06-web-desktop-plugin-identity-compatibility.zh.md)管理保留名称、选择器和暂缓的命名清理。
+
+## 考虑过的替代方案
+
+**每次安装都附带原生运行时。** 首次激活可离线完成,但会为未使用的集成增加默认下载量。
+
+**查询注册表构建发现列表。** 可以避免内嵌元信息,但发现过程依赖网络可用性,且无法把提供的包版本绑定到运行中的应用。
+
+**为 Official 包增加独立安装器或激活注册表。** 第二条路径会重复包操作和 profile 组合行为。安装方维护的发现数据可以复用已有第三方机制。
+
+**始终安装与当前版本相同的注册表包。** 版本相同可以标识已发布版本,但不能标识尚未发布的源码修改。这可能向期待当前工具贡献的开发 Host 交付旧提供方 API 和仅含提供方的 patch。
+
+**建立独立的开发注册表或产物缓存。** 普通本地链接已能让提供方代码及其依赖来自当前 checkout。第二套交付系统会增加生命周期和失效处理,而不改善这一开发保证。
+
+## 后果
+
+注册表交付要求注册表可达;开发交付要求准备并构建 checkout,遵循正常的重新构建和重启流程。源码版本字符串相同不证明构建输出相同。关闭仅取消选择;移除保持独立,并在 bundle 的行仍在使用时拒绝执行。激活失败可以保留已保存的依赖和选择,不加入回滚。离线元信息、发布版本锁定、源码目标选择、同版本来源差异、干净安装产物和发布顺序有专门检查。原生提供方认证不属于目录发现过程。
diff --git a/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.i18n.yaml
new file mode 100644
index 0000000000..33da8f55cb
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-10-05-paid-native-flash-reasoning-translation.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.md
+/agent-note-paid-native-flash-translation-uses-independent-queries:
+ en: 37a2416d68e5a73d
+ zh: ae439aa6ec6a2feb
+/agent-note-paid-native-flash-translation-uses-independent-queries/problem:
+ en: 1029e393c5794564
+ zh: 6044cd7da7fc0947
+/agent-note-paid-native-flash-translation-uses-independent-queries/decision:
+ en: be443b5b9ed2d0a4
+ zh: 399c3c196a3a5858
+/agent-note-paid-native-flash-translation-uses-independent-queries/alternatives-considered:
+ en: e5ec698f302c8ae8
+ zh: b3a7d4f037ca6bdb
+/agent-note-paid-native-flash-translation-uses-independent-queries/consequences:
+ en: 74d2e9a7f22b6555
+ zh: 249e56f0c7071fe3
diff --git a/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.md b/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.md
new file mode 100644
index 0000000000..948ae90f1b
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.md
@@ -0,0 +1,27 @@
+# Agent Note: Paid native Flash translation uses independent queries
+
+Status: implemented
+
+English | [中文](2026-10-05-paid-native-flash-reasoning-translation.zh.md)
+
+## Problem
+
+Readers may choose model-based translation in exchange for additional charges. Reusing their conversation request would inherit its history and generation controls, while routing through an arbitrary configured model would conceal the translation's billing and receiver choices.
+
+## Decision
+
+Paid translation is an explicit alternative to the [default anonymous path](2026-10-05-anonymous-reasoning-translation.md). Only active built-in `deepseek-account` and `deepseek-official` owners with configured credentials and the exact `deepseek-flash` catalog ID qualify. Display names and versions do not participate, and pi-ai routes are excluded. Each fragment receives one fresh text-only query, a simple translation instruction and disabled thinking; no previous fragment or main history is supplied.
+
+The original Session keeps `plugin:translator/request`, the [shared translation request record](2026-10-05-persistent-machine-translation.md), with exact model-visible input in optional `metadata.modelRequest` before dispatch. The existing `appendPluginRecord` writer snapshots its JSON payload and marks it ignorable without replacing main history. Read-only cache lookup precedes native admission and needs no live Session. A cache miss uses the active Session writer and flushes before dispatch; the GUI joins normal Session activation when needed, with its ordinary startup and resume effects. Format migration retains these informational records on a best-effort basis. Translation does not supply the main Session identity to the native request extensions, so a fragment cannot authorize uploading its full conversation log. The configured paid choice is enforced by the Host before billing.
+
+## Alternatives considered
+
+**Reuse the conversation's model call.** Its history, model and thinking settings are unrelated to a translation fragment, and session-log delivery could transmit more than the selected text. A fresh native Flash call makes these choices explicit.
+
+**Offer every model or version label.** This requires additional receiver and billing decisions. The stable `deepseek-flash` ID supports future versions while preserving the requested built-in route restriction.
+
+**Replace the original assistant output.** Translated text is retained separately as a shared result record; original reasoning remains the source of main model history.
+
+## Consequences
+
+Paid queries consume additional input and output tokens. The GUI discloses billing for uncached fragments. Reopening reuses persisted successful results, including when native credentials or model configuration are unavailable. Failures never choose another route. Local audit retention is independent of provider log delivery, and anonymous translation remains the default.
diff --git a/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.zh.md b/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.zh.md
new file mode 100644
index 0000000000..9feb7e8dff
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.zh.md
@@ -0,0 +1,27 @@
+# Agent Note: 付费原生 Flash 翻译使用独立查询
+
+Status: implemented
+
+[English](2026-10-05-paid-native-flash-reasoning-translation.md) | 中文
+
+## 问题
+
+读者可选择模型翻译并承担额外费用。复用对话请求会继承其历史及生成控制,而路由到任意已配置模型会隐藏翻译的计费及接收方选择。
+
+## 决策
+
+付费翻译是[默认匿名路径](2026-10-05-anonymous-reasoning-translation.zh.md)的显式替代选项。只有内置 `deepseek-account` 与 `deepseek-official` 所有者处于活动状态、凭据已配置,且目录包含 ID 恰好为 `deepseek-flash` 的模型时才符合条件。显示名称和版本不参与匹配,pi-ai 路由被排除。每个片段执行一次新的纯文本查询,使用简单翻译指令并关闭思考,不携带此前片段或主历史。
+
+原始 Session 在派发前通过实验性记录 `plugin:translator/request` 在可选 `metadata.modelRequest` 中保留精确的模型可见请求,采用[统一的翻译请求记录](2026-10-05-persistent-machine-translation.zh.md)。现有 `appendPluginRecord` 写入器快照其 JSON 载荷并标为可忽略,不替换主历史。只读缓存查找先于原生准入,且不要求 Session 已激活。缓存未命中时使用已激活 Session 的写入器,在派发前刷新;GUI 按需等待正常 Session 激活,保留普通启动与恢复行为。格式迁移以尽力而为的方式保留这些信息性记录。翻译不向原生请求扩展提供主 Session 身份,因此单个片段不能授权上传完整对话日志。Host 在计费前强制要求已配置的付费选择。
+
+## 考虑过的替代方案
+
+**复用对话的模型调用。** 其历史、模型及思考设置与翻译片段无关,Session 日志派送还可能传输超出所选文本的内容。新的原生 Flash 调用使这些选择明确。
+
+**提供所有模型或版本标签。** 这需要额外的接收方及计费决策。稳定的 `deepseek-flash` ID 支持未来版本,同时保留所要求的内置路由限制。
+
+**替换原始 assistant 输出。** 译文通过统一的结果记录单独保留;原始思考仍是主模型历史的来源。
+
+## 后果
+
+付费查询消耗额外的输入和输出 token。GUI 说明未缓存片段的计费。重新展开复用已持久化的成功结果,即使原生凭据或模型配置不可用。失败时不选择其他路由。本地审计保留独立于提供方日志派送,匿名翻译仍为默认选项。
diff --git a/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.i18n.yaml
new file mode 100644
index 0000000000..d54e1be33d
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-10-05-persistent-machine-translation.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.md
+/agent-note-machine-translation-results-use-shared-session-records:
+ en: 0587db1f8b7b3c6d
+ zh: 63af01050024d4d1
+/agent-note-machine-translation-results-use-shared-session-records/problem:
+ en: 4194ba9786778876
+ zh: 6b99613a4d0dd5d2
+/agent-note-machine-translation-results-use-shared-session-records/decision:
+ en: fb8800dede33177b
+ zh: b3f5098e54d5d982
+/agent-note-machine-translation-results-use-shared-session-records/alternatives-considered:
+ en: 3f251877332d74cc
+ zh: 61991d5fd4704a99
+/agent-note-machine-translation-results-use-shared-session-records/consequences:
+ en: c51bbb4ec6b7f20b
+ zh: 856dbfdf47509d90
diff --git a/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.md b/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.md
new file mode 100644
index 0000000000..0826274392
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.md
@@ -0,0 +1,35 @@
+# Agent Note: Machine translation results use shared Session records
+
+Status: implemented
+
+English | [中文](2026-10-05-persistent-machine-translation.zh.md)
+
+## Problem
+
+Readers need a saved translation when they reopen reasoning or restart the application. A disclosure-only cache repeats external requests, can change an already displayed result and can incur repeated model charges. Storing only model inputs leaves successful translations unavailable after restart.
+
+## Decision
+
+The translator owns `plugin:translator/request` and `plugin:translator/result` for every provider. A request records its selected provider, exact source text, source and target languages, and recipe identity. Optional metadata carries provider-owned request details. A result references the request's Session sequence and retains the completed translated text. Requests are durable before dispatch, and results are durable before they return to the GUI.
+
+The browser binds displayed translations to the disclosure's Session, and the Host validates the configured provider and explicit target language. The authenticated Client supplies the original text and Session identity; the Host does not verify that the text belongs to that Session. Successful records are reusable across disclosure lifetimes and restarts. Identity includes the provider and recipe, so changing the receiver, language or translation settings cannot substitute another result. Identical concurrent translations wait for one result; distinct fragments can run in parallel.
+
+Read-only lookup precedes provider work. New records use the active Session's existing writer through `appendPluginRecord` and targeted flush. An uncached inactive Session rejects before provider work; the GUI consumer joins normal Session controller activation and retries once.
+
+These records accompany the [original reasoning](2026-10-05-anonymous-reasoning-translation.md) and never replace its events or model input. They use the existing experimental record writer and best-effort format-migration retention. Provider availability gates new requests; a saved result remains readable without another inference. Stateless translator calls remain available without a Session.
+
+## Alternatives considered
+
+**Keep only disclosure-local results.** Reopening loses the displayed value and sends the same text again, with possible repeat charges.
+
+**Give anonymous and model translations different result formats.** The reader needs the same source-to-translation relationship from every provider. Provider-owned audit details belong in optional metadata.
+
+**Replace original assistant reasoning.** A translation is derived data. Keeping it alongside the original preserves model history and access to the source.
+
+**Open a separate writer for historical translations.** This competes with normal Session activation and requires coordination between two writer owners. The GUI already activates opened conversations; using its existing controller keeps that ownership in one place.
+
+## Consequences
+
+The Session retains translated text and request identity in addition to original reasoning. Only completed persisted results supply cached text; failed or incomplete attempts do not. A completion that has already entered the append-only log may survive a late caller cancellation. Records do not introduce released Session schema history, and future format migrations may drop experimental state.
+
+Saved results remain readable without activation. A new translation may wait for normal Session activation, including preset startup hooks, resume markers, interrupted-turn repair, and any required generation migration. These ordinary lifecycle effects can append context; translation does not bypass them. Cache reads leave older generations unpublished. The translator owns no second writer and requires no core lifecycle changes.
diff --git a/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.zh.md b/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.zh.md
new file mode 100644
index 0000000000..bf9b9b2c56
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.zh.md
@@ -0,0 +1,35 @@
+# Agent Note: 机器翻译结果使用统一的 Session 记录
+
+Status: implemented
+
+[English](2026-10-05-persistent-machine-translation.md) | 中文
+
+## 问题
+
+读者在重新展开思考内容或重启应用后仍需要已保存的译文。只在展开区域缓存会重复外部请求、可能改变已显示结果,也可能重复产生模型费用。仅存储模型输入无法在重启后恢复成功译文。
+
+## 决策
+
+translator 对所有 Provider 统一使用 `plugin:translator/request` 和 `plugin:translator/result`。请求记录所选 Provider、精确原文、源语言和目标语言,以及翻译规则标识。可选 metadata 保存 Provider 自有请求细节。结果引用请求的 Session 序号并保留完成后的译文。请求在派发前持久化,结果在返回 GUI 前持久化。
+
+浏览器将展示翻译绑定到展开区域的 Session,Host 校验配置的提供方和显式目标语言。已认证的 Client 提供原文与 Session 身份;Host 不校验原文是否属于该 Session。成功记录可跨展开组件生命周期和重启复用。身份包含 Provider 和翻译规则,因此改变接收方、语言或翻译设置不会混用其他结果。相同的并发翻译等待同一结果,不同片段可并行执行。
+
+只读查找先于 Provider 调用。新记录通过已激活 Session 的现有写入器调用 `appendPluginRecord` 并定向刷新。未激活 Session 缓存未命中时,在 Provider 调用前拒绝;GUI 消费者等待 Session controller 的正常激活,再重试一次。
+
+这些记录与[原始思考内容](2026-10-05-anonymous-reasoning-translation.zh.md)一同保留,从不替换其事件或模型输入。它们使用现有的实验性记录写入器,格式迁移采用尽力保留语义。Provider 可用性限制新请求,已保存结果无需再次推理即可读取。不指定 Session 时,translator 仍支持无状态调用。
+
+## 考虑过的替代方案
+
+**只保留展开区域内的结果。** 重新展开会丢失已展示值并再次发送同样的文本,可能重复计费。
+
+**匿名翻译与模型翻译使用不同的结果格式。** 读者对所有 Provider 都需要相同的原文到译文关系。Provider 自有审计细节属于可选 metadata。
+
+**替换原始 assistant 思考内容。** 翻译属于派生数据。与原文并存可以保留模型历史和原文访问能力。
+
+**为历史翻译另开写入器。** 这会与正常 Session 激活竞争,并需要协调两个写入所有者。GUI 已经会激活打开的对话;使用其现有 controller 可将所有权集中在一处。
+
+## 后果
+
+Session 在原始思考内容之外保留译文和请求身份。只有完成并持久化的结果提供缓存译文,失败或未完成尝试不提供。已进入 append-only 日志的完成记录可能在调用者迟到取消后保留。记录不引入已发布的 Session schema 历史,未来格式迁移可能丢弃实验性状态。
+
+已保存结果无需激活即可读取。新翻译可能等待正常 Session 激活,包括 preset 启动钩子、恢复标记、中断轮次修复及所需的 generation 迁移。这些普通生命周期行为可能追加上下文;翻译不会绕过它们。缓存读取不会发布旧 generation。translator 不拥有第二个写入器,也不需要修改核心生命周期。
diff --git a/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.i18n.yaml
new file mode 100644
index 0000000000..0d4737ef26
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-10-05-shared-agents-root-instructions.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.md
+/agent-note-shared-agents-root-joins-the-user-global-instruction-chain:
+ en: d29282678b243512
+ zh: f81e134f59df5491
+/agent-note-shared-agents-root-joins-the-user-global-instruction-chain/problem:
+ en: 8fd7583122a2b082
+ zh: 032acf0c713780f7
+/agent-note-shared-agents-root-joins-the-user-global-instruction-chain/decision:
+ en: 69008d5fd1bb091b
+ zh: a7c4d0334834e6c7
+/agent-note-shared-agents-root-joins-the-user-global-instruction-chain/alternatives-considered:
+ en: 825c2470ba83573e
+ zh: d0ffd4fea4f8f0d0
+/agent-note-shared-agents-root-joins-the-user-global-instruction-chain/consequences:
+ en: cc30674b9b41d0f3
+ zh: 36ac52fb078dbec4
diff --git a/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.md b/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.md
new file mode 100644
index 0000000000..5687130961
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.md
@@ -0,0 +1,37 @@
+# Agent Note: Shared agents root joins the user-global instruction chain
+
+Status: implemented
+
+English | [中文](2026-10-05-shared-agents-root-instructions.zh.md)
+
+## Problem
+
+DSH loads one user-global instruction file, `$DSH_HOME/AGENTS.md`. People who run several agent tools keep the instructions they share across those tools in the shared agent configuration root (`~/.agents/AGENTS.md`, or `$DSH_AGENTS_HOME`) that `dsh-skill-filesystem` already scans for skills. Those instructions reached DSH only when the person copied or symlinked them into the harness home, which duplicates one source of truth into DSH-owned data and leaves no way to tell which copy a session loaded.
+
+## Decision
+
+The user-global instruction scope has two ordered roots: `$DSH_HOME/AGENTS.md` first, then `/AGENTS.md`. The instruction loader resolves the harness home from `$DSH_HOME` or `~/.dsh`, and `agentsHome` from `$DSH_AGENTS_HOME` or `~/.agents`. `resolveAgentsHome` and `agentsHomeDisplay` in `dsh-home-paths` own that resolution for both `dsh-agent-instructions` and `dsh-skill-filesystem`, so the shared root has one resolver whose tilde expansion and blank-environment handling match the harness home.
+
+The two files are candidates of one candidate group. They deduplicate by trimmed content, so identical copies render once from the harness home, and an unobservable candidate preserves the group's last-good state. A duplicate of budget-retained content keeps being reconciled, so deleting or changing the retained candidate promotes it; a byte-budget omission leaves that candidate and its content duplicates unprobed. The harness-home file keeps the `user-global` scope key that earlier sessions recorded; the shared file carries `agents-global`, and reconciliation probes both roots on resume, so an external edit, a removal, or a newly created file appears as a change notice instead of a new baseline.
+
+A user-global scope directory is an internal reconciliation key, so it stays out of model-visible prose: a global delta states `These user-global instructions apply to all work.` instead of naming a scope directory, and a project path whose first component would collide with one renders with a leading `.` so it keeps its own scope.
+
+`USER_GLOBAL_DIRECTORIES` fixes the root order, and discovery, baseline reconciliation, and deduplication all iterate it. The harness home therefore decides content duplicates and keeps its position in the baseline, while distinct content from the shared root follows it.
+
+## Alternatives considered
+
+**Replace the harness-home file with the shared root.** The harness home owns DSH-specific configuration, and its `AGENTS.md` is the user-global file the package documents, tests, and renders today. Removing it would drop a DSH-only layer for anyone who shares instructions across tools, and sessions that recorded `user-global` state would lose their referent.
+
+**Load the shared root only when the harness-home file is absent.** The two files then alternate as the session-visible user-global file, which makes a DSH-specific file silently suppress the shared one instead of composing with it.
+
+**Load the shared file as a second candidate name in the harness home.** `instructionFileCandidates` names files within one directory; the two roots are different directories, so the option cannot express this chain without redefining candidate semantics.
+
+**Rely on path identity instead of content for the duplicate case.** The common layout is `$DSH_HOME/AGENTS.md` symlinked to `~/.agents/AGENTS.md`, which is two paths for one file. Path identity drops neither, so the same text would render twice under two display labels.
+
+## Consequences
+
+- Shared instructions load without a copy or symlink, and the symlinked layout still renders once, from the harness home.
+- A session that started before this change receives one transition when the shared file exists with content that differs from the harness-home file.
+- `dsh-agent-instructions` exposes no `dshHome` or `agentsHome` config field; its two roots are process policy. `dsh-skill-filesystem` retains its explicit home fields, which select that provider's own skill roots.
+- The two user-global files share one fate on probe failure: the group preserves its last-good state together, because one digest decides which member renders.
+- `packages/context/agent-instructions/tests/agent-instructions.spec.ts` pins the two-root order, content deduplication, duplicate promotion after the retained candidate changes or disappears, the global delta wording, the `replace` and `remove` transitions, `$DSH_AGENTS_HOME` resolution, the `~/.agents` display label, and the escaped project directory name.
diff --git a/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.zh.md b/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.zh.md
new file mode 100644
index 0000000000..bea805d075
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.zh.md
@@ -0,0 +1,37 @@
+# Agent Note: 共享 agents 根目录加入用户全局指令链
+
+Status: implemented
+
+[English](2026-10-05-shared-agents-root-instructions.md) | 中文
+
+## 问题
+
+DSH 只加载一个用户全局指令文件:`$DSH_HOME/AGENTS.md`。同时使用多个 agent 工具的人,会把跨工具共享的指令放在共享 agent 配置根目录(`~/.agents/AGENTS.md`,或 `$DSH_AGENTS_HOME`)——`dsh-skill-filesystem` 已经在该根目录下扫描 skill。这些指令只有被复制或 symlink 到 harness home 之后才能进入 DSH,这会把唯一真源复制进 DSH 自有数据,也让人无法判断会话加载的是哪一份。
+
+## 决策
+
+用户全局指令 scope 有两个有序根目录:先是 `$DSH_HOME/AGENTS.md`,然后是 `/AGENTS.md`。指令加载器从 `$DSH_HOME` 或 `~/.dsh` 解析 harness home,从 `$DSH_AGENTS_HOME` 或 `~/.agents` 解析 `agentsHome`。`dsh-home-paths` 中的 `resolveAgentsHome` 与 `agentsHomeDisplay` 为 `dsh-agent-instructions` 和 `dsh-skill-filesystem` 共同负责该解析,因此共享根目录只有一个解析器,其波浪号展开与空白环境变量处理都与 harness home 一致。
+
+这两个文件是同一个候选组中的候选:按去除首尾空白后的内容去重,完全相同的副本只从 harness home 渲染一次,候选不可观测时保留该组最后一次成功状态。预算保留内容的重复候选仍参与对账,因此保留的候选被删除或改变时它会被提升为可见;字节预算省略会让该候选及其内容重复项不再被探测。harness home 文件保留更早会话记录过的 `user-global` scope key;共享文件使用 `agents-global`。对账在恢复会话时会探测两个根目录,因此外部编辑、移除或新建文件都会表现为变更通知,而不会重新生成基线。
+
+用户全局 scope 目录是对账用的内部 key,因此不会进入模型可见文本:全局增量声明 `These user-global instructions apply to all work.`,而不指名 scope 目录;首段路径与 scope 目录同名的项目路径会带前导 `.` 渲染,从而保留自己的 scope。
+
+根目录顺序由 `USER_GLOBAL_DIRECTORIES` 固定,发现、基线对账与去重都按它迭代。因此由 harness home 决定内容重复项,并在基线中保持自己的位置;共享根目录中内容不同的文件排在其后。
+
+## 备选方案
+
+**用共享根目录替换 harness home 文件。** harness home 存放 DSH 专有配置,其 `AGENTS.md` 是该包当前记录、测试并渲染的用户全局文件。移除它会剥夺跨工具共享指令者的 DSH 专有层,也会让记录了 `user-global` 状态的会话失去指涉对象。
+
+**仅当 harness home 文件不存在时才加载共享根目录。** 这样两个文件会轮流充当会话可见的用户全局文件,使 DSH 专有文件静默压制共享文件,而不是与之组合。
+
+**把共享文件作为 harness home 中的第二个候选名加载。** `instructionFileCandidates` 命名的是同一个目录内的文件;这两个根目录是不同目录,因此该选项无法表达这条指令链,除非重新定义候选语义。
+
+**重复项改用路径标识而非内容判断。** 常见布局是把 `$DSH_HOME/AGENTS.md` symlink 到 `~/.agents/AGENTS.md`,这是同一个文件的两个路径。路径标识无法丢弃其中任何一个,同一段文本会以两个显示标签渲染两次。
+
+## 影响
+
+- 共享指令无需复制或 symlink 即可加载;symlink 布局仍然只从 harness home 渲染一次。
+- 在此变更之前启动的会话,若共享文件存在且内容与 harness home 文件不同,会收到一次变更通知。
+- `dsh-agent-instructions` 不暴露 `dshHome` 或 `agentsHome` 配置字段;它的两个根目录属于进程策略。`dsh-skill-filesystem` 保留显式 home 字段,用于选择该提供方自身的 skill 根目录。
+- 探测失败时两个用户全局文件共享同一命运:整个组一起保留最后一次成功状态,因为决定哪个成员渲染的是同一个 digest。
+- `packages/context/agent-instructions/tests/agent-instructions.spec.ts` 固定了两个根目录的顺序、内容去重、保留候选改变或消失后的重复项提升、全局增量措辞、`replace` 与 `remove` 变更、`$DSH_AGENTS_HOME` 解析、`~/.agents` 显示标签,以及转义后的项目目录名。
diff --git a/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.i18n.yaml
new file mode 100644
index 0000000000..b22fc768ec
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.i18n.yaml
@@ -0,0 +1,22 @@
+# Bilingual-pair consistency record for 2026-10-06-web-desktop-plugin-identity-compatibility.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.md
+/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation:
+ en: 45d8ead4ca3db272
+ zh: 4ef8e729d5d7e8c1
+/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/problem:
+ en: 8112c1c059d1cf10
+ zh: 852d23c56a93a555
+/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/decision:
+ en: 90d74300f045bf6e
+ zh: 088e88944478ca2a
+/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/deferred-naming-cleanup:
+ en: 4c428f65ef611d07
+ zh: 1fdc5467b0203d08
+/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/alternatives-considered:
+ en: 65af55b5775c9bc5
+ zh: 2ea75b3fd33ac0c2
+/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/consequences:
+ en: e8e96b0354f1a3c6
+ zh: ad1bfe3600e14ff1
diff --git a/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.md b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.md
new file mode 100644
index 0000000000..c39f289a39
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.md
@@ -0,0 +1,40 @@
+# Agent Note: Preserve Web and Desktop plugin identities across experimental consolidation
+
+Status: implemented
+
+English | [中文](2026-10-06-web-desktop-plugin-identity-compatibility.zh.md)
+
+## Problem
+
+Moving a capability into the experimental directory changes its ownership and support classification. Renaming its published package or an existing configuration row additionally changes how installed profiles refer to that capability. Those identity changes can make a working Web or Desktop customization require edits unrelated to the capability's behavior.
+
+## Decision
+
+The moved packages retain the published names declared by the [experimental naming policy](../../../../scripts/experimental-package-policy.ts). That typed directory-to-name map is the sole maintained inventory of naming exceptions. Package manifests, consumers, installation checks, and bundler checks agree with it; a missing, moved, or duplicated exception is rejected. [Experimental status](../../../../packages/experimental/README.md#status) remains independent of spelling, Official discovery, and explicit selection.
+
+Existing badge, Ralph, and native delegation row IDs remain valid Host row selectors. New isolation groups and newly introduced capabilities retain their new IDs. Later user layers still replace configuration or disable rows. Selecting an optional bundle is still explicit: a patch targeting an absent row does not select its bundle.
+
+Compatibility protection covers Web and Desktop profiles, including existing native provider package identities, selections, configuration, and authentication. Native bundles insert ordinary Host rows, so Headless, SDK, and ACP compositions can select them too; separate provider-only compatibility wrappers are unnecessary. Public APIs remain pre-stable, and new output metadata stays required unless an actual consumer needs an accommodation.
+
+## Deferred naming cleanup
+
+| Retained identity | Unconstrained cleanup | Why it is deferred |
+|---|---|---|
+| Published names in the naming-exception map | Give every experimental package the `@deepseek-ai/dsh-experimental-` prefix | Existing dependencies, imports, module declarations, and profile files use the established names |
+| Existing badge, Ralph, and native-tool row IDs | Give optional contributions consistently distinct `optional-*` selectors | Existing user overrides use those IDs; the ownership move does not require a new selector |
+
+These naming changes are deferred, not prerequisites for experimental classification. A later cleanup requires an explicit release and migration decision, an inventory of affected Web/Desktop configurations, and installed-profile upgrade evidence. Directory moves or pre-stable API status alone do not retire these exceptions. The package list stays in the typed policy rather than being copied into this note or package READMEs.
+
+## Alternatives considered
+
+**Rename every package and row during consolidation.** Uniform spelling makes the new ownership visible, but adds dependency and configuration migration to a change that can preserve those references.
+
+**Publish aliases or keep duplicate compatibility rows.** Parallel identities need continuing resolution and removal rules and can register the same capability twice. Retaining the established identity avoids that additional mechanism.
+
+**Keep provider-only bundles through additional wrapper packages.** Native bundles insert ordinary Host rows that compose into any profile. Extra delivery packages would preserve compositions outside the selected compatibility scope and increase catalog and release maintenance.
+
+## Consequences
+
+The repository carries a bounded naming-exception map and tests that classify retained names through direct imports, aliases, transitive dependencies, and packed or bundled artifacts. An unprefixed name does not grant admission to the default product. Existing row identities have Web/Desktop composition coverage for user overrides and disabled state. No general alias or automatic profile migration mechanism is introduced.
+
+The [classification decision](../process/2026-10-05-product-package-classification.md), [optional-composition decision](2026-09-21-experimental-capabilities-as-optional-bundles.md), and [on-demand discovery decision](2026-10-05-official-on-demand-bundles.md) retain their separate responsibilities. Released Session generations and persisted hook event names remain protected independently of this profile compatibility scope.
diff --git a/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.zh.md b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.zh.md
new file mode 100644
index 0000000000..4b58743cb5
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.zh.md
@@ -0,0 +1,40 @@
+# Agent Note: 在实验能力整合中保留 Web 和 Desktop 插件身份
+
+Status: implemented
+
+[English](2026-10-06-web-desktop-plugin-identity-compatibility.md) | 中文
+
+## 问题
+
+把能力迁入 experimental 目录会改变其归属和支持分类。重命名已发布的包或既有配置行,还会改变已安装 profile 引用该能力的方式。这些身份变化可能要求正常工作的 Web 或 Desktop 自定义配置进行与能力行为无关的修改。
+
+## 决策
+
+迁移的包保留[实验包命名策略](../../../../scripts/experimental-package-policy.ts)声明的已发布名称。这个带类型的目录到包名映射是命名例外唯一手工维护的清单。包清单、消费者、安装检查和打包检查与其保持一致;缺失、迁移到其他位置或重复归属的例外会被拒绝。[实验状态](../../../../packages/experimental/README.zh.md#status)仍独立于名称拼写、Official 发现和显式选择。
+
+既有徽章、Ralph 和原生委派行 ID 作为 Host 行选择器保持有效。新增隔离组和新引入的能力保留各自的新 ID。后续用户层仍可替换配置或禁用行。可选 bundle 仍须显式选择:指向缺失行的 patch 不会自动选择其 bundle。
+
+兼容性保护覆盖 Web 和 Desktop profile,包括既有原生提供方的包身份、选择状态、配置和认证。原生 bundle 插入普通 Host 行,因此 Headless、SDK 和 ACP 组合也可以选择它们;不需要单独的仅提供方兼容包装包。公共 API 仍处于稳定前阶段,新增输出元信息保持必填,除非实际消费者需要适配。
+
+## 暂缓的命名清理
+
+| 保留的身份 | 不受兼容性约束时的清理 | 暂缓原因 |
+|---|---|---|
+| 命名例外映射中的已发布名称 | 为每个实验包使用 `@deepseek-ai/dsh-experimental-` 前缀 | 既有依赖、导入、模块声明和 profile 文件使用原有名称 |
+| 既有徽章、Ralph 和原生工具行 ID | 为可选贡献统一使用独立的 `optional-*` 选择器 | 既有用户覆盖使用这些 ID;归属迁移不要求新的选择器 |
+
+这些命名变更被暂缓,不是实验分类的前提。后续清理需要明确的发布与迁移决策、受影响 Web/Desktop 配置清单,以及已安装 profile 的升级验证。仅目录迁移或公共 API 处于稳定前阶段,不足以删除这些例外。包清单保留在带类型的策略中,不复制到本记录或各包 README。
+
+## 考虑过的替代方案
+
+**在整合时重命名所有包和配置行。** 统一拼写可以体现新归属,但会为本可保留引用的变更加上依赖和配置迁移。
+
+**发布别名或保留重复兼容行。** 并行身份需要持续维护解析和移除规则,还可能重复注册同一能力。保留既有身份可以避免增加这种机制。
+
+**通过额外包装包保留仅提供方 bundle。** 原生 bundle 插入普通 Host 行,可组合到任何 profile。额外交付包会保留选定兼容范围之外的组合,并增加目录和发布维护成本。
+
+## 后果
+
+仓库维护范围明确的命名例外映射,并测试通过直接导入、别名、传递依赖及打包产物识别这些保留名称。没有实验前缀的名称不因此获准进入默认产品。既有行身份具有覆盖用户配置和禁用状态的 Web/Desktop 组合测试。不引入通用别名或自动 profile 迁移机制。
+
+[分类决策](../process/2026-10-05-product-package-classification.zh.md)、[可选组合决策](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)和[按需发现决策](2026-10-05-official-on-demand-bundles.zh.md)继续承担各自独立的职责。已发布 Session 代际和持久化 hook 事件名的保护独立于此 profile 兼容范围。
diff --git a/.agents/notes/implemented/architecture/2026-10-08-desktop-shell-configuration.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-08-desktop-shell-configuration.i18n.yaml
new file mode 100644
index 0000000000..7bd206e1f8
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-08-desktop-shell-configuration.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-10-08-desktop-shell-configuration.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-08-desktop-shell-configuration.md
+/agent-note-desktop-shell-configuration-independent-of-the-host:
+ en: 860c8afa57227b89
+ zh: bec6a19319050aed
+/agent-note-desktop-shell-configuration-independent-of-the-host/problem:
+ en: 9f652292b62cd015
+ zh: 14a97b5a4884c008
+/agent-note-desktop-shell-configuration-independent-of-the-host/decision:
+ en: e58311d3d5ebb6b6
+ zh: b4c791fcfde7b986
+/agent-note-desktop-shell-configuration-independent-of-the-host/alternatives-considered:
+ en: 349210f3cf5f168b
+ zh: 019acfc3c3d71b4b
+/agent-note-desktop-shell-configuration-independent-of-the-host/consequences:
+ en: a523f43819051d4b
+ zh: 69926bc3761195ea
diff --git a/.agents/notes/implemented/architecture/2026-10-08-desktop-shell-configuration.md b/.agents/notes/implemented/architecture/2026-10-08-desktop-shell-configuration.md
new file mode 100644
index 0000000000..9531ff4bff
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-08-desktop-shell-configuration.md
@@ -0,0 +1,29 @@
+# Agent Note: Desktop shell configuration independent of the Host
+
+Status: implemented
+
+English | [中文](2026-10-08-desktop-shell-configuration.zh.md)
+
+## Problem
+
+Desktop updates and native recovery can run before the Host starts or while it is unavailable. Depending on Host configuration prevents these functions from resolving their preferences when needed. A shared settings page does not identify which process owns each preference.
+
+## Decision
+
+Electron main owns `userData/desktop/settings.json` independently of Host startup and Cordis evaluation. The test authentication popup switch uses this file; the [Desktop reference](../../../../apps/desktop/README.md#local-desktop-settings) owns its fields and behavior.
+
+Ownership follows runtime dependencies: Electron-owned functions needing configuration before Host startup or while the Host is unavailable use shell configuration. Host plugin settings, including shared Web/Desktop preferences, continue to use Cordis. Displaying a setting in Desktop alone does not justify shell ownership. A unified settings interface can use separate storage owners.
+
+The `desktop` subdirectory groups shell configuration without relocating existing files. The [shortcut decision](2026-09-20-device-local-shortcut-preferences.md) retains ownership of `userData/keybindings.json` and its persistence semantics. The [shared Web application decision](2026-09-10-desktop-web-wrapper.md) still governs Host composition and profile configuration.
+
+## Alternatives considered
+
+**Put every preference in Cordis.** This requires a working Host. Reading `cordis.patch.yml` directly in Electron duplicates configuration evaluation; a patch alone omits plugin defaults and other configuration layers.
+
+**Move every setting shown in Desktop.** Interface placement does not imply a Host-independent requirement. This splits shared Web/Desktop preferences and moves configuration away from its consuming plugins.
+
+## Consequences
+
+Shell and Host configuration have separate owners. Each setting needs documented ownership and effective timing. Independent shell configuration supports additional Host-independent functions without introducing a second application backend.
+
+Existing settings are candidates for individual evaluation when their functionality changes or a Host-independent use is required. This is an evaluation direction, not a scheduled migration or predefined list. A migration must identify a single authoritative source, transfer existing values and define when changes take effect, without ambiguous precedence between two stores. Shared Host-managed settings remain in Cordis unless their ownership requirements change.
diff --git a/.agents/notes/implemented/architecture/2026-10-08-desktop-shell-configuration.zh.md b/.agents/notes/implemented/architecture/2026-10-08-desktop-shell-configuration.zh.md
new file mode 100644
index 0000000000..3dac97f51a
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-08-desktop-shell-configuration.zh.md
@@ -0,0 +1,29 @@
+# Agent Note: Desktop 壳层配置独立于 Host
+
+Status: implemented
+
+[English](2026-10-08-desktop-shell-configuration.md) | 中文
+
+## Problem
+
+Desktop 更新和原生恢复可以在 Host 启动前或不可用时运行。依赖 Host 配置会使这些功能无法在需要时解析偏好。统一设置页面不能决定每项偏好由哪个进程负责。
+
+## Decision
+
+Electron 主进程负责 `userData/desktop/settings.json`,不依赖 Host 启动或 Cordis 求值。测试鉴权弹窗开关使用此文件;[Desktop 参考文档](../../../../apps/desktop/README.zh.md#local-desktop-settings)统一说明字段和行为。
+
+配置归属按运行依赖划分:由 Electron 负责且需要在 Host 启动前或不可用时读取配置的功能,使用壳层配置。Host 插件设置,包括 Web/Desktop 共用偏好,继续使用 Cordis。仅仅展示在 Desktop 中,不构成归属壳层的理由。统一设置界面可以使用不同的存储归属。
+
+`desktop` 子目录集中存放壳层配置,不迁移已有文件。[快捷键决策](2026-09-20-device-local-shortcut-preferences.zh.md)继续负责 `userData/keybindings.json` 及其持久化语义。[共享 Web 应用决策](2026-09-10-desktop-web-wrapper.zh.md)仍约束 Host 组合和 profile 配置。
+
+## Alternatives considered
+
+**所有偏好都放入 Cordis。** 这需要可用的 Host。由 Electron 直接读取 `cordis.patch.yml` 会重复实现配置求值;单独的 patch 不包含插件默认值和其他配置层。
+
+**迁移所有在 Desktop 展示的设置。** 界面位置不意味着需要独立于 Host。这会拆分 Web/Desktop 共用偏好,并让配置脱离实际使用它的插件。
+
+## Consequences
+
+壳层与 Host 配置分别有自己的归属。每项设置需说明归属和生效时机。独立壳层配置支持更多不依赖 Host 的功能,但不引入第二套应用后端。
+
+现有设置在功能调整或需要独立于 Host 使用时,可逐项评估。这是评估方向,不是已排期的迁移计划或预设清单。实际迁移需明确唯一配置来源、迁移已有值并定义生效时机,避免两套存储间存在含糊的优先级。共用的 Host 设置在归属要求不变时继续使用 Cordis。
diff --git a/.agents/notes/implemented/architecture/2026-10-08-persistent-ptc-sandbox.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-08-persistent-ptc-sandbox.i18n.yaml
new file mode 100644
index 0000000000..262e03bb22
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-08-persistent-ptc-sandbox.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-10-08-persistent-ptc-sandbox.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-08-persistent-ptc-sandbox.md
+/agent-note-retained-ptc-processes-follow-file-policy:
+ en: ef07a82c85e490e1
+ zh: b1662d1970d2b83b
+/agent-note-retained-ptc-processes-follow-file-policy/problem:
+ en: b8cb574032d4e6e4
+ zh: 6a1a25dec5f96086
+/agent-note-retained-ptc-processes-follow-file-policy/decision:
+ en: 5109a4bb5be39867
+ zh: 2dcc602aba9eae61
+/agent-note-retained-ptc-processes-follow-file-policy/alternatives-considered:
+ en: 10bebc7f982cf7f4
+ zh: 9247a2ba57921a11
+/agent-note-retained-ptc-processes-follow-file-policy/consequences:
+ en: b3ddf273158d3754
+ zh: 916356f4074b3622
diff --git a/.agents/notes/implemented/architecture/2026-10-08-persistent-ptc-sandbox.md b/.agents/notes/implemented/architecture/2026-10-08-persistent-ptc-sandbox.md
new file mode 100644
index 0000000000..2c4235fcc3
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-08-persistent-ptc-sandbox.md
@@ -0,0 +1,31 @@
+# Agent Note: Retained PTC processes follow file policy
+
+Status: implemented
+
+English | [中文](2026-10-08-persistent-ptc-sandbox.zh.md)
+
+## Problem
+
+Retaining JavaScript variables does not require broader file access. A process that outlives one execution can also outlive a one-time approval or a Session policy change. File-policy changes must invalidate retained environments and begin stopping their file handles and background work.
+
+## Decision
+
+`persistent-in-memory` starts its Node process through the same sandbox provider as `per-call`. A retained owner uses one resolved mode, workspace root and Session identity until release. The [memory decision](2026-09-23-best-effort-persistent-ptc.md) owns retained values, failure recovery and package composition; this note owns file authority.
+
+The [PTC-owned cleanup decision](2026-10-09-ptc-owned-policy-cleanup.md) supersedes this note’s permission-setter completion and cleanup-ownership decision. Permission changes commit synchronously; PTC owns immediate cancellation and waits internally before subsequent execution. The tools package keeps Agent ownership and omits the owner for temporary escalations.
+
+An approved one-time escalation omits the state owner and receives a fresh temporary environment. Its process is released before the call returns. It cannot access standing variables, and its declarations do not survive. The standing environment keeps its original policy and variables. Nested tools continue to apply their own permissions and approvals.
+
+## Alternatives considered
+
+**Unconfined retained execution.** Omitting confinement simplifies a temporary prototype but couples variable retention to full host file access. The existing process sandbox supports long-lived processes, so this grant is unnecessary.
+
+**Changing the running process policy.** Existing backends apply restrictions at launch. Arbitrary retained objects, open handles and background work cannot be transferred into another policy with the existing interface. Releasing the environment prevents later evaluations from reusing those resources.
+
+**Coordinating through permission presets.** File-policy changes also have direct callers. PTC observes committed Session mode events from both paths, independently of user-facing preset selection.
+
+**Keeping an elevated environment.** A one-time approval cannot authorize future programs or idle background work. Omitting the state owner makes the approval lifetime explicit while preserving the standing environment.
+
+## Consequences
+
+Normal calls retain variables without changing file permissions. Changing sandbox mode discards variables. Escalated programs must be self-contained. Runtime consumers must release a retained owner before supplying a different policy without a Session mode change; the provider rejects inconsistent reuse. The sandbox inherits the existing backends’ file-effect coverage and platform limitations.
diff --git a/.agents/notes/implemented/architecture/2026-10-08-persistent-ptc-sandbox.zh.md b/.agents/notes/implemented/architecture/2026-10-08-persistent-ptc-sandbox.zh.md
new file mode 100644
index 0000000000..d841702941
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-08-persistent-ptc-sandbox.zh.md
@@ -0,0 +1,31 @@
+# Agent Note: 保留状态的 PTC 进程遵循文件策略
+
+Status: implemented
+
+[English](2026-10-08-persistent-ptc-sandbox.md) | 中文
+
+## 问题
+
+保留 JavaScript 变量不需要更大的文件访问权限。跨调用存活的进程也可能在单次审批结束或 Session 策略变化后继续运行。文件策略变化必须使保留环境失效,并开始停止其文件句柄和后台工作。
+
+## 决策
+
+`persistent-in-memory` 与 `per-call` 通过同一个沙箱提供方启动 Node 进程。保留状态的所有者在释放前使用固定的已解析权限模式、工作区根目录和 Session 身份。[内存决策](2026-09-23-best-effort-persistent-ptc.zh.md)负责保留值、失败恢复和包组成;本文负责文件权限。
+
+[PTC 自行等待清理的决策](2026-10-09-ptc-owned-policy-cleanup.zh.md)替代本文关于权限设置完成时点和清理归属的决策。权限变化同步提交;PTC 负责立即取消,并在后续执行前内部等待清理。工具包保留 Agent 所有权,在临时提权时省略所有者。
+
+单次获准的提权省略状态所有者,使用全新的临时环境。该次调用返回前释放进程,无法访问原环境的变量,也不会保留新声明。原环境保持原权限和变量。嵌套工具继续应用各自的权限和审批规则。
+
+## 考虑过的替代方案
+
+**不限制保留状态的执行环境。** 省略沙箱能简化临时原型,却把变量保留与完整宿主文件访问权限绑定。现有进程沙箱支持长期运行的进程,因此这项授权没有必要。
+
+**修改运行中进程的策略。** 现有后端在启动时应用限制。现有接口无法把任意保留对象、打开的句柄和后台工作转移到另一策略。释放环境可以防止后续执行复用这些资源。
+
+**通过权限预设协调清理。** 文件策略也有直接调用方。PTC 观察两类调用方提交的 Session 模式事件,无需依赖面向用户的预设选择。
+
+**保留提权环境。** 单次审批不能授权未来程序或空闲期间的后台工作。省略状态所有者明确了审批生效的范围,同时保留原环境。
+
+## 影响
+
+普通调用保留变量,同时维持原文件权限。切换沙箱模式会丢弃变量。提权程序必须包含执行所需的完整代码。未发生 Session 模式变化时,运行时消费者在传入不同策略前必须释放保留状态的所有者;提供方拒绝策略不一致的复用。沙箱继承现有后端的文件约束范围与平台限制。
diff --git a/.agents/notes/implemented/architecture/2026-10-09-composer-command-chip.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-09-composer-command-chip.i18n.yaml
new file mode 100644
index 0000000000..5071ac4a0f
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-composer-command-chip.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-10-09-composer-command-chip.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-09-composer-command-chip.md
+/agent-note-claimed-commands-as-atomic-composer-chips:
+ en: 66d4f149698760c8
+ zh: de219f394dfd4a4c
+/agent-note-claimed-commands-as-atomic-composer-chips/problem:
+ en: 831c97e0de131639
+ zh: ea35b4b179ff33f0
+/agent-note-claimed-commands-as-atomic-composer-chips/decision:
+ en: 6ac53f6cd1307897
+ zh: 2f1dbe762b52f18e
+/agent-note-claimed-commands-as-atomic-composer-chips/alternatives-considered:
+ en: 9c694a21237d7772
+ zh: b35ba7bf8b73b679
+/agent-note-claimed-commands-as-atomic-composer-chips/consequences:
+ en: 089365d6a4be0ebb
+ zh: b30384c717a7ea1d
diff --git a/.agents/notes/implemented/architecture/2026-10-09-composer-command-chip.md b/.agents/notes/implemented/architecture/2026-10-09-composer-command-chip.md
new file mode 100644
index 0000000000..8445cb44be
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-composer-command-chip.md
@@ -0,0 +1,41 @@
+# Agent Note: Claimed commands as atomic composer chips
+
+Status: implemented
+
+English | [中文](2026-10-09-composer-command-chip.zh.md)
+
+## Problem
+
+The [Lexical composer decision](../../archived/architecture/2026-08-20-web-composer-lexical-editor.md) kept a claimed command as literal leading text. A TextNode transform styled the token and split typed overflow back out of it, and the submit machine held the claim while the draft string started with the token. Every edit at the token boundary ran through that transform. Lexical merges same-style neighbours before the overflow node's own transform runs, so the split and the merge looped until Lexical aborted the update with error #14 and the DOM diverged from the editor state (issue #4490). PR #5480 cleared the copied style for the reported gesture; pasting an image after a claim and IME composition reach the same transform and remained unverified. The plain-text reference entity also needed a precedence rule to leave the claimed token alone.
+
+## Decision
+
+A claim replaces the leading trigger span with `CommandChipNode`, an atomic inline `DecoratorNode`, followed by the token's argument separator as an ordinary text node.
+
+- The node carries the `CommandClaim`, including its submit closure. History restores nodes with their fields, so undoing a chip deletion restores the claim. JSON export writes the token text; JSON import yields a plain text node because the closure has no JSON form.
+- The detect projection counts the chip as one U+FFFC, so trigger detection never re-reads the command name. The clipboard projection is the token without its separator. The chip is not a reference occurrence. `EditorProjection.command` is the claim of a chip that opens the document.
+- Outside a submit transaction, the machine's claim follows the document: `draft-changed` carries the leading chip's claim, so deleting or displacing the chip releases the claim and a history restore re-enters claimed. A failed command submission settles to the same rule, so it returns to claimed exactly when a chip still leads; a claim adjudicated from typed text at Enter has no chip and returns to plain.
+- Node transforms keep the chip on the leading seat. A chip with a preceding sibling or inside a later block becomes its literal token, which releases the claim as typing before the old token did. The chip's own transform sees sibling insertions and moves between blocks; a paragraph transform sees a block inserted before the chip's block, so no update walks the whole document.
+- Deletion takes two steps. A character deletion that reaches the chip from a collapsed caret marks it selected (`data-selected` on its host; the root's `data-composer-chip-selected` hides the caret), and the next deletion in the same direction removes it whole. Deletions are decided both on the character-deletion command and on deletion input events, because iOS applies a ranged `deleteContentBackward` without that command. A key whose dispatch produced no character deletion, a click, blur, or a caret that leaves the chip clears the mark, so platform deletion shortcuts such as macOS Ctrl+H keep it. The editor selection stays a collapsed range and `isKeyboardSelectable()` stays false for the reason recorded on reference chips. Escape still does not release a claim.
+- The persisted draft records the leading chip's claim token (`DraftSnapshot.command`), never the claim. On restore the input asks the trigger pipeline's `resolveClaim`, which polls the optional source hook `matchClaim`: it awaits the source's catalog and answers only a claim, never invoking or executing the command, unlike Enter's `matchEnter`. The chip is rebuilt only at the revision the restore produced and merged into the seed's history entry; an earlier edit, an unclaimed token, or a catalog failure keeps the literal token, which Enter still adjudicates.
+- `claim-decor.ts` and the text-reference precedence rule are deleted.
+
+The `slash/input-*` events, `CommandClaim`, `InputState.claim`, and trigger sources keep their fields. `CommandClaim.token` still ends with the separator, and command arguments are still read from the clipboard text after it.
+
+## Alternatives considered
+
+**Keep literal text and repair each gesture.** Every gesture that reaches the boundary node needs its own proof, as #5480 showed, and the precedence rule with text references remains.
+
+**Reuse `ReferenceChipNode`.** Reference chips are occurrences serialized through their owner codec at submit; a command is not model content and must not enter `occurrences`.
+
+**Put the separator inside the chip text.** Lexical appends a managed line break after an inline decorator that ends its block, so the blank-arguments hint rendered by `p:last-child::after` would move to the next line. A separate text node keeps the hint on the chip's line.
+
+**Select the chip with a NodeSelection.** The plain-text binding's arrow, typing, and deletion handlers ignore non-range selections, so each of those gestures would need its own handling at the chip.
+
+**Keep claims in a shell map keyed by NodeKey.** The map duplicates the field the node already carries and needs pruning whenever history is cleared.
+
+## Consequences
+
+- Backspace no longer edits the command name character by character to reopen the menu. At the chip's edge, two Backspace or Delete presses remove the chip and leave the arguments as plain text.
+- A reload restores the chip after the command catalog loads; until then, and when restoration does not apply, the draft shows the literal token.
+- Whole-chip deletion through a real key event is observable only in the browser lane because jsdom lacks `Selection.modify`; `lifecycle-chrome.e2e.ts` pins the separator, select, and delete presses.
diff --git a/.agents/notes/implemented/architecture/2026-10-09-composer-command-chip.zh.md b/.agents/notes/implemented/architecture/2026-10-09-composer-command-chip.zh.md
new file mode 100644
index 0000000000..268e0a7209
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-composer-command-chip.zh.md
@@ -0,0 +1,41 @@
+# Agent Note: 已认领命令作为输入框原子 chip
+
+Status: implemented
+
+[English](2026-10-09-composer-command-chip.md) | 中文
+
+## 问题
+
+[Lexical 输入框决策](../../archived/architecture/2026-08-20-web-composer-lexical-editor.md)把已认领命令保留为行首字面文本。一个 TextNode transform 给 token 加样式,并把输入进去的溢出文字拆出来;提交状态机在草稿字符串以 token 开头时保持认领。token 交界处的每次编辑都会经过这个 transform。Lexical 会在溢出节点自身的 transform 执行前合并样式相同的相邻节点,于是拆分与合并循环往复,直到 Lexical 以错误码 14 中止这次更新,DOM 与编辑器状态随之分叉(issue #4490)。PR #5480 针对报告的手势清除了被复制的样式;认领后粘贴图片和 IME 组字会进入同一个 transform,仍未验证。纯文本引用实体也需要一条优先级规则来避开已认领的 token。
+
+## 决策
+
+认领把行首触发区间替换为 `CommandChipNode`(一个原子 inline `DecoratorNode`),其后是作为普通文本节点的 token 参数分隔符。
+
+- 节点携带 `CommandClaim`,包括其 submit 闭包。历史记录恢复节点时连同字段一起恢复,因此撤销 chip 删除会恢复认领。JSON 导出写入 token 文本;闭包没有 JSON 形式,因此 JSON 导入得到纯文本节点。
+- 检测投影把 chip 计为一个 U+FFFC,触发检测不会再读到命令名。剪贴板投影是不含分隔符的 token。chip 不是引用 occurrence。`EditorProjection.command` 是位于文档开头的 chip 所携带的认领。
+- 在提交事务之外,状态机的认领跟随文档:`draft-changed` 携带行首 chip 的认领,因此删除或挤开 chip 会释放认领,历史恢复会重新进入 claimed。命令提交失败后按同一规则结算,只要行首仍有 chip 就回到 claimed;Enter 时从输入文本裁决出的认领没有 chip,回到 plain。
+- 节点 transform 让 chip 保持在行首位置。前面有兄弟节点或位于后续块中的 chip 会变回字面 token,并像在旧 token 前输入那样释放认领。chip 自身的 transform 覆盖兄弟节点插入和跨块移动;段落 transform 覆盖在 chip 所在块之前插入块的情况,因此没有任何更新需要遍历整个文档。
+- 删除分两步。从折叠光标出发、方向触及 chip 的字符删除先把它标记为选中(宿主元素上的 `data-selected`;根元素的 `data-composer-chip-selected` 隐藏光标),同方向的下一次删除才整体移除它。字符删除命令和删除类输入事件都会参与判断,因为 iOS 处理带范围的 `deleteContentBackward` 时不经过该命令。分发过程中没有产生字符删除的按键、点击、失焦或光标离开 chip 会清除标记,因此 macOS Ctrl+H 这类平台删除快捷键会保留它。编辑器选区保持为折叠的 range,`isKeyboardSelectable()` 仍为 false,原因与引用 chip 记录的相同。Escape 仍不释放认领。
+- 持久化的草稿记录行首 chip 的认领 token(`DraftSnapshot.command`),从不保存认领本身。恢复时输入框调用触发管线的 `resolveClaim`,它轮询可选的 source 钩子 `matchClaim`:等待 source 的目录就绪,只应答认领,与 Enter 的 `matchEnter` 不同,从不调用或执行命令。chip 只在恢复产生的那个版本上重建,并合入种子的历史条目;在此之前发生的编辑、无人认领的 token 或目录加载失败都会保留字面 token,Enter 仍会对其裁决。
+- 删除 `claim-decor.ts` 和文本引用的优先级规则。
+
+`slash/input-*` 事件、`CommandClaim`、`InputState.claim` 与触发源的字段保持不变。`CommandClaim.token` 仍以分隔符结尾,命令参数仍从其后的剪贴板文本读取。
+
+## 考虑过的替代方案
+
+**保留字面文本并逐个修复手势。** 如 #5480 所示,每个触及交界节点的手势都需要单独证明,与文本引用之间的优先级规则也仍然存在。
+
+**复用 `ReferenceChipNode`。** 引用 chip 是在提交时经 owner codec 序列化的 occurrence;命令不是模型内容,不能进入 `occurrences`。
+
+**把分隔符放进 chip 文本。** inline decorator 位于块末尾时,Lexical 会在其后追加受管换行,由 `p:last-child::after` 渲染的空参数提示会移到下一行。独立的文本节点让提示留在 chip 所在行。
+
+**用 NodeSelection 选中 chip。** 纯文本绑定的方向键、输入和删除处理器都会忽略非 range 选区,这些手势在 chip 处都需要单独处理。
+
+**在 shell 中用按 NodeKey 索引的表保存认领。** 该表重复了节点已携带的字段,并且每次清空历史时都需要修剪。
+
+## 影响
+
+- Backspace 不再逐字编辑命令名并重新打开菜单。在 chip 边缘按两次 Backspace 或 Delete 会删除 chip,参数保留为普通文字。
+- 刷新后,命令目录加载完成即恢复 chip;在此之前以及恢复不适用时,草稿显示字面 token。
+- 通过真实按键整体删除 chip 只能在浏览器 lane 中观察,因为 jsdom 缺少 `Selection.modify`;`lifecycle-chrome.e2e.ts` 固定了删除分隔符、选中、删除这三次按键。
diff --git a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.i18n.yaml
new file mode 100644
index 0000000000..04a53081d6
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-10-09-cua-shared-native-helper.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.md
+/agent-note-shared-native-cua-process-with-agent-scoped-state:
+ en: 65056aee19bf6b22
+ zh: 9823fa0c33497ff9
+/agent-note-shared-native-cua-process-with-agent-scoped-state/problem:
+ en: 386864a282e5ad58
+ zh: a4ea75ec70eb7778
+/agent-note-shared-native-cua-process-with-agent-scoped-state/decision:
+ en: ebf231a6aad109bb
+ zh: 53b553e679569da1
+/agent-note-shared-native-cua-process-with-agent-scoped-state/alternatives-considered:
+ en: 883f71387f599160
+ zh: 4b5f1450be799397
+/agent-note-shared-native-cua-process-with-agent-scoped-state/consequences:
+ en: 518a4abf2c4c95be
+ zh: 72bf2b9817975368
diff --git a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.md b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.md
new file mode 100644
index 0000000000..1bc795dd52
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.md
@@ -0,0 +1,25 @@
+# Agent Note: Shared native CUA process with Agent-scoped state
+
+Status: implemented
+
+English | [中文](2026-10-09-cua-shared-native-helper.zh.md)
+
+## Problem
+
+On macOS 26.6.2, replayd derives capture-client identity from the executable path. Two live helpers at the same path repeatedly replace each other's connections even when their screenshot requests are sequential. Byte-identical executables at distinct real paths complete sequential and concurrent captures; two symlinks to one executable reproduce the failure. A per-request lock cannot isolate retained system connections.
+
+## Decision
+
+One user shares a native service per executable content digest across Agents and Hosts. A version lock elects the service; an additional canonical-executable-path lock prevents different builds published at one path from running together. Locks remain held until process exit. The managed startup command hands service lifetime to its client connections, and the last connection's completed cleanup ends the service. The diagnostic stdin/stdout entry proxies to this same service.
+
+Each Agent connection owns its application handles, AX snapshots, limits and cancellation state. An app handle grants no exclusive use of the application. A FIFO orders complete desktop operations, including clipboard transactions, while response writes are independent. Agent disposal retires only its connection. The [object API decision](2026-09-17-experimental-cua-object-api.md) retains evaluator independence and coordinate semantics; the [consent decision](2026-09-26-cua-application-consent.md) retains application authorization.
+
+Cancellation and timeout first stop one request and wait for acknowledgement. A timed-out screenshot retains the single system-RPC slot until the real callback completes; new screenshots fail with `CAPTURE_BUSY` instead of accumulating requests. Late pixels are discarded, while other AX and input operations remain available. Ending an await or creating a wrapper does not prove that the system connection has recovered. An operation or cleanup that cannot stop can invalidate the process, making every old handle unusable. Uncertain input is never replayed automatically.
+
+## Alternatives considered
+
+Separate executable copies per Agent avoid the observed identity collision but multiply system capture clients. One shared service preserves a single capture client while retaining Agent state isolation. An in-process Swift binding changes crash containment without removing the system RPC. The [continuous-hover decision](2026-10-13-cua-continuous-hover.md) keeps individual operations serialized without cross-call application ownership.
+
+## Consequences
+
+Two Hosts can race to start the service and receive the same instance. A may retain its app object while B screenshots and changes the same application; A reads fresh AX afterward. An Agent closing cannot retire another Agent's handles. Recovery must distinguish request, snapshot, handle, capture-backend and process failure. A lost system callback occupies a bounded capture slot; reliable in-process backend reset requires separate platform evidence. The historical DeepSeek failures have matching RPC errors, but their competing-client identity was not recovered from retained logs.
diff --git a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.zh.md b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.zh.md
new file mode 100644
index 0000000000..c40d277837
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.zh.md
@@ -0,0 +1,25 @@
+# Agent Note:共享原生 CUA 进程并隔离 Agent 状态
+
+Status: implemented
+
+[English](2026-10-09-cua-shared-native-helper.md) | 中文
+
+## 问题
+
+在 macOS 26.6.2 上,replayd 按可执行文件路径生成捕获客户端标识。同一路径的两个活动 helper 即使依次截图,也会反复替换对方的连接。相同字节的可执行文件位于不同实际路径时,交替和并发截图均成功;两个符号链接指向同一文件仍会失败。逐请求加锁不能隔离仍存活的系统连接。
+
+## 决策
+
+同一用户的多个 Agent 和 Host 按可执行文件内容摘要共享原生服务。版本锁选出服务实例,规范化可执行路径上的另一把锁阻止发布在同一路径的不同构建同时运行。进程退出才释放锁。受管启动命令将服务生命期交给客户端连接,最后一条连接清理完成后服务退出。诊断用 stdin/stdout 入口也代理到同一个服务。
+
+每条 Agent 连接独立持有应用句柄、AX 快照、资源限制和取消状态。持有 app 句柄不代表独占应用。FIFO 队列串行执行包含剪贴板事务的完整桌面操作,响应写入独立进行。Agent 销毁只释放自己的连接。[对象接口决策](2026-09-17-experimental-cua-object-api.zh.md)继续负责执行器独立性及坐标语义,[授权决策](2026-09-26-cua-application-consent.zh.md)继续负责应用许可。
+
+取消或超时先停止单次请求并等待确认。截图超时后,唯一的系统 RPC 槽位保留到真实回调结束;新截图返回 `CAPTURE_BUSY`,不继续堆积请求。迟到图片被丢弃,其他 AX 与输入操作仍可执行。结束 await 或创建包装对象不能证明系统连接已恢复。操作或清理无法停止时,才可能使进程失效,届时所有旧句柄均不可用。结果不确定的输入不会自动重放。
+
+## 考虑过的替代方案
+
+每个 Agent 使用独立的可执行文件副本可以避开已观察到的标识冲突,但会增加系统捕获客户端数量。共享服务保留一个捕获客户端,同时隔离 Agent 状态。进程内 Swift 绑定会改变崩溃隔离方式,不能消除系统 RPC。[连续悬停决策](2026-10-13-cua-continuous-hover.zh.md)保留单次操作串行执行,不分配跨调用的应用所有权。
+
+## 后果
+
+两个 Host 同时启动服务时可连接同一个实例。A 可以保留 app 对象,B 对同一应用截图并修改,之后 A 重新读取 AX。一个 Agent 关闭不会释放另一个 Agent 的句柄。恢复需要区分请求、快照、句柄、截图后端和进程失效。系统回调丢失会占用有界的捕获槽位,可靠的进程内后端重置需要另外取得平台验证。历史 DeepSeek 失败具有相同 RPC 错误,但保留日志未能确定当时另一个冲突客户端的身份。
diff --git a/.agents/notes/implemented/architecture/2026-10-09-message-node-factories-and-delegates.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-09-message-node-factories-and-delegates.i18n.yaml
new file mode 100644
index 0000000000..ac38c709dc
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-message-node-factories-and-delegates.i18n.yaml
@@ -0,0 +1,22 @@
+# Bilingual-pair consistency record for 2026-10-09-message-node-factories-and-delegates.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-09-message-node-factories-and-delegates.md
+/agent-note-domain-image-slots-and-a-shared-gallery-factory:
+ en: bce6358fb4211261
+ zh: bfebd3bbdb1affb1
+/agent-note-domain-image-slots-and-a-shared-gallery-factory/problem:
+ en: ec6ffd7f737a1f56
+ zh: 5e368f87ce52e28c
+/agent-note-domain-image-slots-and-a-shared-gallery-factory/decision:
+ en: b807777fd48560eb
+ zh: e7b1625bd4448ce1
+/agent-note-domain-image-slots-and-a-shared-gallery-factory/alternatives-considered:
+ en: db14fd6d0e2ce78b
+ zh: c29a19cde692a1dd
+/agent-note-domain-image-slots-and-a-shared-gallery-factory/verification:
+ en: d82e036edb981e7c
+ zh: 5a05e9a2e6489d21
+/agent-note-domain-image-slots-and-a-shared-gallery-factory/consequences:
+ en: ac387ffb5a316920
+ zh: ab584aaafe554278
diff --git a/.agents/notes/implemented/architecture/2026-10-09-message-node-factories-and-delegates.md b/.agents/notes/implemented/architecture/2026-10-09-message-node-factories-and-delegates.md
new file mode 100644
index 0000000000..054dbe24ac
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-message-node-factories-and-delegates.md
@@ -0,0 +1,54 @@
+# Agent Note: Domain image Slots and a shared gallery Factory
+
+Status: implemented
+
+English | [中文](2026-10-09-message-node-factories-and-delegates.zh.md)
+
+## Problem
+
+User messages, assistant output, Trajectory records, and tool results need independently replaceable image presentation. Their official entries can share one gallery, but sharing that implementation must not merge their replacement positions or hide the caller's image-reading authority.
+
+## Decision
+
+Each domain declares its own image Slot and supplies the framework's `renderSlot` seat to the component that renders it. The official attachment entries use one reusable Content Factory, following the [Body Slot and Content Factory division](2026-10-09-reasoning-content-slot-factory.md).
+
+| Entry | Declaration owner and renderer |
+| --- | --- |
+| `conversation.chat.user.images` Slot | The `conversation.chat.user.content` Factory declares it; `UserContent` renders it for user messages, pending steering, and local echoes. |
+| `conversation.chat.message.images` Slot | The Assistant node registration declares it; `AssistantMarkdown` renders consecutive output-image groups. |
+| `conversation.trajectory.images` Slot | The Trajectory view registration declares it; record attachments and tool-output blocks render it through the ledger's Slot seat. |
+| `tool.call.images` Slot | The `read_image` tool-view registration declares it; `ToolRow` renders the result image. |
+| `conversation.images.content` Factory | The attachment plugin registers this root-scoped gallery, implemented by `MessageImagesContent`. |
+
+The attachment plugin fills all four image Slots with the same `MessageImages` React adapter at priority 100. A smaller priority replaces only the selected domain's entry; disposing that replacement restores the official entry. Each adapter invokes `conversation.images.content` with explicit image references, `loadImage`, and layout parameters. The shared Factory neither declares the domain Slots nor selects a domain from a scene parameter.
+
+The image callers use their declared Slot directly; no image-entry Factory exists solely to forward one Slot call. User text remains a separate shared `conversation.user.text` Factory with its own keyed node Slot, as specified by the [user-text decision](2026-10-10-user-text-json-nodes.md).
+
+`AssistantNodeView` passes its Slot seat and `loadImage` to `AssistantMarkdown`. User wrappers pass the loader through `UserStyleBubble` to `UserContent`. Trajectory passes its Slot seat and loader through the ledger. These paths preserve component responsibilities, image order, and keys without custom ReactNode-producing image callbacks.
+
+Conversation owns read authorization, per-Session URL caching, and cleanup. The shared gallery never searches for a global current Session: persistent reads use the loader supplied by the caller, while submission-echo previews retain their existing browser URLs. Gallery layout, retries, thumbnails, and lightbox behavior remain in the attachment package.
+
+`ChatView` keeps the existing `MarkdownDelegateProvider` with its wrapped file opener, external-link action, and file-image capabilities. Native-image composition does not replace Markdown image handling or its failure feedback.
+
+`MessageImagesContentInput` accepts optional complete `MessageImageLabels`. Supplied labels are used unchanged; when omitted, the Content Factory's locale seat supplies the default copy.
+
+## Alternatives considered
+
+**One shared image Slot for every caller.** A replacement intended for one domain would also affect the others; shared implementation does not require shared replacement identity.
+
+**Import the gallery component across feature packages.** This bypasses standard Slot replacement and the reusable Factory entry point.
+
+**Pass custom image-rendering functions or supply the loader through a private Context.** Framework Slot seats already express rendering authority, while explicit loader props preserve the caller's Session authority.
+
+## Verification
+
+- User and output images use separate Slots; replacing either leaves the other and its mounted gallery untouched.
+- Each official image entry has priority 100; a priority-0 replacement wins and disposal restores the official gallery.
+- All four defaults call the same Content Factory with their original image data, authorized loader, and layout choices.
+- Source order, keys, cached previews, failed-load retries, and lightbox interactions retain their existing behavior.
+- The Content Factory accepts complete custom labels and resolves default labels when the caller omits them.
+- Unloading the attachment implementation removes its entries without removing Slots declared by the owning domains.
+
+## Consequences
+
+Independent Slots allow domain-specific presentation without copying the gallery. Domain callers must forward the framework Slot seat to their plain React descendants and must supply an authorized loader at each image call. A Factory and an ordinary Slot have different lifetimes and ownership; a reusable implementation does not own every position where it appears.
diff --git a/.agents/notes/implemented/architecture/2026-10-09-message-node-factories-and-delegates.zh.md b/.agents/notes/implemented/architecture/2026-10-09-message-node-factories-and-delegates.zh.md
new file mode 100644
index 0000000000..03869f2dbb
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-message-node-factories-and-delegates.zh.md
@@ -0,0 +1,54 @@
+# Agent Note: 领域图片 Slot 与共享画廊 Factory
+
+Status: implemented
+
+[English](2026-10-09-message-node-factories-and-delegates.md) | 中文
+
+## 问题
+
+用户消息、助手输出、Trajectory 记录和工具结果需要可独立替换的图片展示。官方条目可以共享同一画廊,但共享实现不能合并各自的替换位置,也不能隐藏调用方的图片读取授权。
+
+## 决策
+
+各领域声明自己的图片 Slot,并把框架 `renderSlot` 席位交给实际渲染它的组件。官方附件条目复用一个 Content Factory,沿用 [Body Slot 与 Content Factory 的分工](2026-10-09-reasoning-content-slot-factory.zh.md)。
+
+| 入口 | 声明所有者与渲染组件 |
+| --- | --- |
+| `conversation.chat.user.images` Slot | `conversation.chat.user.content` Factory 声明,由 `UserContent` 为用户消息、待处理 steering 和本地回显渲染。 |
+| `conversation.chat.message.images` Slot | Assistant 节点注册声明,由 `AssistantMarkdown` 渲染连续的输出图片组。 |
+| `conversation.trajectory.images` Slot | Trajectory 视图注册声明,记录附件及工具输出块通过账本的 Slot 席位渲染。 |
+| `tool.call.images` Slot | `read_image` 工具视图注册声明,由 `ToolRow` 渲染结果图片。 |
+| `conversation.images.content` Factory | 附件插件注册此 root 作用域画廊,由 `MessageImagesContent` 实现。 |
+
+附件插件以优先级 100 为四个图片 Slot 填入同一个 `MessageImages` React 适配组件。较小的优先级只替换选定领域的条目;替换条目释放后恢复官方条目。适配组件显式向 `conversation.images.content` 传入图片引用、`loadImage` 和布局参数。共享 Factory 不声明各领域的 Slot,也不通过场景参数选择领域。
+
+图片调用方直接使用其声明的 Slot,不设置仅转发一次 Slot 调用的图片入口 Factory。用户文本仍使用独立共享的 `conversation.user.text` Factory 及其 keyed 节点 Slot,详见[用户文本决策](2026-10-10-user-text-json-nodes.zh.md)。
+
+`AssistantNodeView` 将 Slot 席位与 `loadImage` 传给 `AssistantMarkdown`。用户包装层通过 `UserStyleBubble` 将 loader 传给 `UserContent`。Trajectory 通过账本传递自己的 Slot 席位与 loader。这些路径保持组件职责、图片顺序和 key,不使用自制的 ReactNode 图片渲染回调。
+
+Conversation 拥有读取授权、逐 Session URL 缓存与回收。共享画廊不查找全局当前 Session:持久图片使用调用方提供的 loader 读取,本地提交回显预览保留已有浏览器 URL。画廊布局、重试、缩略图和灯箱行为仍由附件包负责。
+
+`ChatView` 保留既有 `MarkdownDelegateProvider` 及其包装后的文件打开动作、外链动作和文件图片能力。原生图片组合不替代 Markdown 图片处理及失败反馈。
+
+`MessageImagesContentInput` 接受可选的完整 `MessageImageLabels`。提供 labels 时原样使用,省略时由 Content Factory 的 locale 席位提供默认文案。
+
+## 考虑过的替代方案
+
+**所有调用方共用一个图片 Slot。** 面向某个领域的替换也会影响其他领域;共享实现不要求共享替换身份。
+
+**跨功能包导入画廊组件。** 绕过标准 Slot 替换机制和可复用的 Factory 入口。
+
+**传递自制图片渲染函数,或通过私有 Context 供应 loader。** 框架 Slot 席位已经表达渲染权限,显式 loader props 则保留调用方的 Session 授权。
+
+## 验证
+
+- 用户与输出图片使用独立 Slot;替换任意一方时,另一方及其已挂载画廊不受影响。
+- 各官方图片条目优先级为 100;优先级 0 的替换条目胜出,释放后恢复官方画廊。
+- 四个默认条目携带原有图片数据、授权 loader 和布局选项调用同一个 Content Factory。
+- 来源顺序、key、缓存预览、读取失败重试及灯箱交互保持既有行为。
+- Content Factory 接受完整自定义 labels,调用方省略时解析默认 labels。
+- 卸载附件实现会移除其条目,但不移除各领域声明的 Slot。
+
+## 影响
+
+独立 Slot 支持各领域定制展示而无需复制画廊。领域调用方需要将框架 Slot 席位传给普通 React 后代组件,并在每次图片调用中提供授权 loader。Factory 与普通 Slot 的生命周期和所有权不同;可复用实现不拥有它出现的所有位置。
diff --git a/.agents/notes/implemented/architecture/2026-10-09-operator-scoped-connect-handshake.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-09-operator-scoped-connect-handshake.i18n.yaml
new file mode 100644
index 0000000000..10847a2adb
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-operator-scoped-connect-handshake.i18n.yaml
@@ -0,0 +1,31 @@
+# Bilingual-pair consistency record for 2026-10-09-operator-scoped-connect-handshake.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-09-operator-scoped-connect-handshake.md
+/agent-note-peer-scoped-connect-handshake:
+ en: 90137ec146d1a60e
+ zh: 3dbb0c96426beb36
+/agent-note-peer-scoped-connect-handshake/problem:
+ en: 3658160e034e0e58
+ zh: 980d922aded8713b
+/agent-note-peer-scoped-connect-handshake/decision:
+ en: 414dfe949a53c4d9
+ zh: 7e6efdd57f2eb240
+/agent-note-peer-scoped-connect-handshake/decision/admission-and-ownership:
+ en: 95ad4c8c433fdc9b
+ zh: 379ccd5e077d7dee
+/agent-note-peer-scoped-connect-handshake/decision/accepted-retention:
+ en: f58c1b39855740da
+ zh: 99343095a357fa3f
+/agent-note-peer-scoped-connect-handshake/decision/events-and-client-scope:
+ en: e0bdf2f82d79d2a8
+ zh: 5df84ee473916161
+/agent-note-peer-scoped-connect-handshake/alternatives-considered:
+ en: bcc97e06d4565be1
+ zh: d11e9e9680b26c03
+/agent-note-peer-scoped-connect-handshake/verification:
+ en: 81ce03ab4e1fd0d4
+ zh: c2729e7e2db56f67
+/agent-note-peer-scoped-connect-handshake/consequences:
+ en: 91f5f80f733c174c
+ zh: 76cbb31b0d2e8764
diff --git a/.agents/notes/implemented/architecture/2026-10-09-operator-scoped-connect-handshake.md b/.agents/notes/implemented/architecture/2026-10-09-operator-scoped-connect-handshake.md
new file mode 100644
index 0000000000..9211aa6d43
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-operator-scoped-connect-handshake.md
@@ -0,0 +1,55 @@
+# Agent Note: Peer-scoped connect handshake
+
+Status: implemented
+
+English | [中文](2026-10-09-operator-scoped-connect-handshake.zh.md)
+
+## Problem
+
+Automatically subscribing automation clients to forwarded events lets method-only callers receive and answer human interactions. CLI and MCP callers need independent participation and cleanup without changing browser authentication or assigning per-browser identities.
+
+## Decision
+
+Browsers retain their existing signed Cookie, root launch-token exchange, shared `connection.browserPeer` with owner principal and `interactions: 'answer'`, and recovery behavior. Only API callers create independent Peers through `GET /connect?token=&interactions=answer|none`. The endpoint is API-only, defaults to `none`, rejects invalid or repeated declarations with 400, and retains Host/Origin checks and token authentication. It returns 302 with the Remote Mux destination and an API Cookie.
+
+### Admission and ownership
+
+- Connect publishes the API Peer before returning. Its signed API Cookie carries the authority, Peer id, and issue/expiry interval; interaction participation belongs to the live Peer, not another credential field. Browser v1 Cookies remain valid and always select the shared Peer. API v2 Cookies select only an existing independent Peer.
+- HTTP validates the Cookie and finds its Peer without waiting for a WebSocket. Connect, HTTP, and WebSocket require no cross-transport readiness ordering.
+- One API lifecycle WebSocket owns the Peer. Duplicate attachment is refused; closure removes the Peer before asynchronous disposal completes. Later HTTP and reconnect attempts using that API Cookie are refused. A fresh connect is caller-owned.
+- Already-admitted unary requests retain their request-level cancellation semantics. Closing the lifecycle socket does not cancel independent HTTP requests or accepted Session work.
+- Browser sockets never own the shared Peer. Refresh, concurrent tabs, repeated launch-URL visits, and Host restart require no per-browser Peer restoration or credential reuse machinery.
+
+### Accepted retention
+
+An API caller may exit after connect without establishing its WebSocket. If it has performed no other work, its Peer retains only lightweight identity, scope, and registry state, not a socket, subscription, or background job. It may remain until Host shutdown. No initial-connect deadline, idle sweep, or in-memory expiry timer exists; Cookie expiration is checked on admission independently of record retention. Repeated abandoned connects accumulate lightweight records, an accepted cost for authenticated local callers. Actual operations still own cleanup of resources they acquire.
+
+### Events and Client scope
+
+The Client creates forwarded-event machinery only for `answer`. The Host checks `peer.signal.aborted` first, then `peer.interactions` at both `$events` and `$events/result`: `none` refuses both regardless of principal. An answering owner receives forwarded waterfalls and may reply only through the same Peer as the stream; an answering non-owner receives only RBAC-authorized broadcasts and cannot reply to waterfalls. Only active owner answering streams count as live answering clients. Browsers remain one shared answering party; distinguishing browser credentials from each other is deliberately unsupported.
+
+Non-page Clients supply HTTP and WebSocket transports using the same API Cookie and Host base URL. Passive clients open their lifecycle socket independently of unary calls and may call generated raw stream methods. They do not use `$stream()`, the browser-generation recovery helper: it rejects immediately in `none` mode. Raw stream failure is terminal; the caller owns any new connect and retry. Existing answering-client generation behavior and Host fact delivery remain unchanged; there is no additional API recovery subsystem.
+
+## Alternatives considered
+
+**Independent Peers for browsers too.** This adds credential identities, restart reconstruction, and repeated-login reuse to preserve tabs. Browser callers can remain one party, so the original browser model stays intact.
+
+**Publish only after upgrade or require Mux readiness before unary.** Both reject or delay valid HTTP that arrives first. Connect is the publication point.
+
+**Deadlines and pending/bound activation states.** They reclaim lightweight idle records at the cost of timers and transition races. Unused Peers may remain until shutdown.
+
+**Adapt browser stream recovery to passive callers.** This spreads interaction mode into generation-aware stream consumers and their tests. Raw streams already support CLI/MCP calls; passive callers manage failure themselves.
+
+**Client-only events switch or Host-global configuration.** Neither enforces independent participation. The authenticated Peer supplies the mode checked by the Host.
+
+## Verification
+
+Connection tests cover API publication, Cookie verification, idle retention, socket ownership, and unchanged browser admission. The [Loader tests](../../../../packages/api/gateway/tests/operator-connect.host.spec.ts) cover HTTP before WebSocket, duplicate attachment, prompt removal during slow cleanup, unary cancellation independence, and event refusal/ownership. [Node Client tests](../../../../packages/api/gateway/tests/authenticated-node.client.spec.ts) cover authenticated transports, delayed socket establishment, and raw streams without passive recovery.
+
+The [recorded approval scenario](../../../../apps/web/tests/approval-composer.e2e.ts) adds a passive caller alongside the answering browser. Full replay requires a usable command sandbox for its file-writing operation; local environments without one cannot validate that final step. No Session format or model-visible input changes.
+
+## Consequences
+
+Unconnected API records need not be promptly reclaimed; resource-heavy additions to idle Peers require revisiting that choice. API Cookies cannot restore a Peer after its socket closes or the Host restarts. Browser Cookie format and authentication behavior remain unchanged, with no reauthentication migration. API and browser Cookies use the same authority-specific name, so API callers keep their own Cookie storage rather than exchanging credentials in a shared browser jar. Any launch-token holder may request `answer`; this is interaction participation, not user isolation.
+
+The [duplex-stream decision](2026-09-19-remote-duplex-stream.md) retains Peer scopes and stream framing. The [browser authentication decision](2026-08-24-browser-token-authentication.md) retains browser token, signature, authority, and lifetime semantics; only independent API admission is added here. Both records remain active.
diff --git a/.agents/notes/implemented/architecture/2026-10-09-operator-scoped-connect-handshake.zh.md b/.agents/notes/implemented/architecture/2026-10-09-operator-scoped-connect-handshake.zh.md
new file mode 100644
index 0000000000..e7d8f7a4ea
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-operator-scoped-connect-handshake.zh.md
@@ -0,0 +1,55 @@
+# Agent Note: 以 Peer 为作用域的建连握手
+
+Status: implemented
+
+[English](2026-10-09-operator-scoped-connect-handshake.md) | 中文
+
+## 问题
+
+让自动化客户端自动订阅转发事件,会使只需调用方法的客户端也收到并回答人机交互。CLI 与 MCP 调用方需要独立参与和清理机制,而不改变浏览器认证或为每个浏览器赋予独立身份。
+
+## 决策
+
+浏览器保留现有签名 Cookie、根路径 launch token 交换、共享 `connection.browserPeer` 和恢复行为。只有 API 调用方通过 `GET /connect?token=&interactions=answer|none` 创建独立 Peer。该端点仅供 API 使用,默认 `none`,非法或重复声明返回 400,并保留 Host/Origin 检查和 token 认证。它返回 302、Remote Mux 目标地址和 API Cookie。
+
+### 接纳与所有权
+
+- Connect 在返回前发布 API Peer。签名 API Cookie 包含 authority、Peer 标识和签发/过期区间;交互参与模式属于有效 Peer,不再重复写入凭证字段。浏览器 v1 Cookie 保持有效,始终选择共享 Peer;API v2 Cookie 只选择已存在的独立 Peer。
+- HTTP 校验 Cookie 并查找 Peer,不等待 WebSocket。Connect、HTTP 与 WebSocket 不要求跨传输的就绪顺序。
+- 一条 API 生命周期 WebSocket 拥有 Peer。重复关联被拒绝;关闭时在异步释放完成前移除 Peer。之后使用该 API Cookie 的 HTTP 与重连请求被拒绝,调用方负责重新 connect。
+- 已受理的 unary 请求保留请求级取消语义。生命周期连接关闭不会取消独立 HTTP 请求或已接受的 Session 工作。
+- 浏览器 socket 不拥有共享 Peer。刷新、多标签页、重复打开启动 URL 和 Host 重启不需要每浏览器 Peer 的恢复或凭证复用机制。
+
+### 接受的保留行为
+
+API 调用方可能在 connect 后退出,始终没有建立 WebSocket。如果它没有执行其他工作,Peer 仅保留轻量身份、作用域与登记状态,不持有 socket、订阅或后台任务,可以保留到 Host 退出。不设置首次建联期限、空闲扫描或内存过期定时器;Cookie 过期在接纳时检查,与记录保留无关。反复放弃 connect 会累积轻量记录,这是已认证本地调用方场景下接受的成本。实际操作仍负责清理其获取的资源。
+
+### 事件与 Client 范围
+
+Client 仅在 `answer` 模式创建转发事件机制。Host 首先检查 `peer.signal.aborted`,随后在 `$events` 与 `$events/result` 两处检查 `peer.interactions`:无论 principal 是什么,`none` 都被拒绝。回答型 owner 收到转发的 waterfall,且只能从拥有事件流的同一 Peer 回答;回答型非 owner 仅收到经 RBAC 授权的广播,不能回答 waterfall。只有未取消的 owner 回答流计入在线回答客户端。浏览器仍是同一个共享回答方;本设计明确不区分不同浏览器凭证的身份。
+
+非页面 Client 提供使用同一 API Cookie 与 Host 基址的 HTTP 和 WebSocket 传输。被动客户端独立于 unary 调用打开生命周期连接,也可以调用生成的原始流方法。它们不使用浏览器代际恢复辅助方法 `$stream()`:该方法在 `none` 模式立即拒绝。原始流失败直接结束;调用方负责重新 connect 和重试。既有回答型客户端的代际行为与 Host facts 下发不变,不另建 API 恢复系统。
+
+## 备选方案
+
+**也为浏览器创建独立 Peer。** 为保留多标签页行为,这需要凭证身份、重启重建和重复登录复用。浏览器调用方可以保持为同一方,因此保留原模型。
+
+**升级后才发布,或在 unary 前等待 Mux 就绪。** 两者都会拒绝或延迟先到达的有效 HTTP。Connect 才是发布时机。
+
+**建联期限和 pending/bound 激活状态。** 它们以定时器和状态转换竞争为代价回收轻量空闲记录。未使用的 Peer 可以留到退出。
+
+**为被动调用方适配浏览器流恢复。** 这会把交互模式传入依赖代际的流消费者及其测试。原始流已支持 CLI/MCP 调用,被动调用方自行处理失败。
+
+**仅客户端事件开关或 Host 全局配置。** 两者都不能强制独立参与规则。由已认证 Peer 提供 Host 检查的模式。
+
+## 验证
+
+Connection 测试覆盖 API 发布、Cookie 校验、闲置保留、连接所有权和不变的浏览器接纳。[Loader 测试](../../../../packages/api/gateway/tests/operator-connect.host.spec.ts)覆盖 HTTP 先于 WebSocket、重复关联、慢清理期间及时移除、unary 取消独立性和事件拒绝/所有权。[Node Client 测试](../../../../packages/api/gateway/tests/authenticated-node.client.spec.ts)覆盖认证传输、延迟建联和不使用被动恢复的原始流。
+
+[录制审批场景](../../../../apps/web/tests/approval-composer.e2e.ts)在回答页面旁增加一个被动调用方。完整回放需要可用命令 sandbox 执行写文件;缺少该后端的本地环境无法验证最后一步。本次不改变 Session 格式或模型可见输入。
+
+## 后果
+
+未建联的 API 记录不要求及时回收;若为空闲 Peer 增加重资源,必须重新评估这一取舍。连接关闭或 Host 重启后,API Cookie 不能恢复 Peer。浏览器 Cookie 格式与认证行为保持不变,无需重新认证迁移。API 与浏览器 Cookie 使用同一 authority 专属名称,因此 API 调用方维护自己的 Cookie 存储,不在共享浏览器 Cookie jar 中交换凭证。任何 launch token 持有者都可以请求 `answer`;这是交互参与,不是用户隔离。
+
+[双工流决策](2026-09-19-remote-duplex-stream.zh.md)继续定义 Peer 作用域和流帧格式。[浏览器认证决策](2026-08-24-browser-token-authentication.zh.md)保留浏览器 token、签名、authority 与有效期语义;本记录只增加独立 API 接纳。两份既有记录继续保留。
diff --git a/.agents/notes/implemented/architecture/2026-10-09-ptc-owned-policy-cleanup.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-09-ptc-owned-policy-cleanup.i18n.yaml
new file mode 100644
index 0000000000..da9d7f6c81
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-ptc-owned-policy-cleanup.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-10-09-ptc-owned-policy-cleanup.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-09-ptc-owned-policy-cleanup.md
+/agent-note-ptc-owns-policy-change-cleanup-waits:
+ en: dfe28bee0ab79ff6
+ zh: dd68c51b4cbfbc3f
+/agent-note-ptc-owns-policy-change-cleanup-waits/problem:
+ en: 5620ff525d762114
+ zh: ddef3a3624cab79b
+/agent-note-ptc-owns-policy-change-cleanup-waits/decision:
+ en: 4389c330d95f1588
+ zh: 97e4b9cf8b69f48e
+/agent-note-ptc-owns-policy-change-cleanup-waits/alternatives-considered:
+ en: a51d7f5c5b8fd7dc
+ zh: 4480107a4d8cff70
+/agent-note-ptc-owns-policy-change-cleanup-waits/consequences:
+ en: f83009b6406bafa1
+ zh: c774eb1895bf37e1
diff --git a/.agents/notes/implemented/architecture/2026-10-09-ptc-owned-policy-cleanup.md b/.agents/notes/implemented/architecture/2026-10-09-ptc-owned-policy-cleanup.md
new file mode 100644
index 0000000000..470568cc3f
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-ptc-owned-policy-cleanup.md
@@ -0,0 +1,27 @@
+# Agent Note: PTC owns policy-change cleanup waits
+
+Status: implemented
+
+English | [中文](2026-10-09-ptc-owned-policy-cleanup.zh.md)
+
+## Problem
+
+Stopping a retained Node process and its nested tools can take longer than recording a new file policy. Making permission setters await that cleanup spreads PTC lifecycle requirements into unrelated callers, including Webhook Session creation, and adds cancellation windows to those callers.
+
+## Decision
+
+Permission setters commit synchronously. Success means the selected policy is recorded; it does not guarantee that operations admitted under the old policy have completely stopped. This replaces the completion requirement and cleanup ownership in the [retained-process sandbox decision](2026-10-08-persistent-ptc-sandbox.md), whose confinement and one-time escalation rules still apply.
+
+PTC observes committed Session mode changes. The tool bridge immediately cancels affected complete calls, including pending approvals and nested tools. A later PTC call waits for canceled predecessors to settle before resolving the latest standing policy. The Node provider immediately invalidates affected processes, including idle environments, starts cleanup, and retains the cleanup Promise internally. A later program waits for that cleanup; failed cleanup remains retryable and prevents process reuse. Unchanged modes preserve environments. Tool-registry and provider unload remove their observers and await owned work.
+
+## Alternatives considered
+
+**Wait in permission setters.** This provides a definite completion point for old-authority work, but no current permission consumer requires that guarantee. It introduces a general resource registry and asynchronous API migration for PTC-specific cleanup. The completion guarantee belongs to PTC execution and disposal instead.
+
+**Clean up only on the next PTC call.** An idle environment can retain background work indefinitely when no further call occurs. Policy changes begin cancellation immediately; only waiting is deferred.
+
+**Reconfigure an existing process.** The sandbox applies restrictions at launch. Open handles, retained objects and background work require releasing the old environment before another policy can execute there.
+
+## Consequences
+
+Permission changes return without process-cleanup latency. Old operations may remain active while cancellation finishes, and cleanup failure does not roll back the recorded policy. Subsequent PTC execution waits for cleanup and uses the latest policy. Webhook creation keeps its synchronous permission step, so that step adds no cancellation window before prompt admission. Regression coverage holds cleanup with explicit barriers, changes policy again while a call waits, checks idle cancellation without another call, and verifies cleanup retry and owner unload.
diff --git a/.agents/notes/implemented/architecture/2026-10-09-ptc-owned-policy-cleanup.zh.md b/.agents/notes/implemented/architecture/2026-10-09-ptc-owned-policy-cleanup.zh.md
new file mode 100644
index 0000000000..9d852a0eb6
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-ptc-owned-policy-cleanup.zh.md
@@ -0,0 +1,27 @@
+# Agent Note: PTC 自行等待策略变化后的清理
+
+Status: implemented
+
+[English](2026-10-09-ptc-owned-policy-cleanup.md) | 中文
+
+## 问题
+
+停止保留的 Node 进程及其嵌套工具,可能比记录新文件策略耗时更长。让权限设置等待这些清理,会把 PTC 生命周期要求传给 Webhook Session 创建等无关调用方,并为这些调用方增加取消窗口。
+
+## 决策
+
+权限设置同步提交。成功表示所选策略已经记录,不保证按旧策略接纳的操作已完全停止。这替代[保留进程沙箱决策](2026-10-08-persistent-ptc-sandbox.zh.md)中的完成要求和清理归属;其中的沙箱限制与单次提权规则继续适用。
+
+PTC 观察已提交的 Session 模式变化。工具桥接立即取消受影响的完整调用,包括等待中的审批和嵌套工具。后续 PTC 调用先等待已取消的调用结束,再解析最新常设策略。Node 提供方立即使受影响的进程失效,包括空闲环境,开始清理并在内部保留清理 Promise。后续程序等待清理完成;清理失败可重试,且阻止复用进程。模式不变时保留环境。工具注册表和提供方卸载时移除各自监听器,并等待所拥有的工作结束。
+
+## 考虑过的替代方案
+
+**在权限设置中等待。** 这可以明确旧权限下的工作完成时点,但当前权限调用方不需要该保证。为此增加通用资源登记和异步 API 迁移,会扩大 PTC 清理的影响范围。完成保证由 PTC 执行和销毁负责。
+
+**等下次 PTC 调用再清理。** 没有后续调用时,空闲环境的后台工作可能一直存在。策略变化立即开始取消,只有等待被推迟。
+
+**重新配置已有进程。** 沙箱在启动时应用限制。打开的句柄、保留对象和后台工作要求先释放旧环境,才能在其中按另一策略执行。
+
+## 影响
+
+权限变化返回时无需等待进程清理。取消完成前,旧操作可能仍在运行;清理失败不会回滚已记录的策略。后续 PTC 执行等待清理并使用最新策略。Webhook 创建保留同步权限步骤,该步骤不会在提交任务前增加取消窗口。回归测试通过显式信号阻塞清理,覆盖等待期间再次改变策略、没有后续调用时取消空闲环境、清理重试及所有者卸载。
diff --git a/.agents/notes/implemented/architecture/2026-10-09-reasoning-content-slot-factory.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-09-reasoning-content-slot-factory.i18n.yaml
new file mode 100644
index 0000000000..ba86de4f69
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-reasoning-content-slot-factory.i18n.yaml
@@ -0,0 +1,31 @@
+# Bilingual-pair consistency record for 2026-10-09-reasoning-content-slot-factory.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-09-reasoning-content-slot-factory.md
+/agent-note-standard-reasoning-body-slot-and-reusable-content-factory:
+ en: 55862b96f6731c81
+ zh: 425ab1ff7043a606
+/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/problem:
+ en: 91000d9dd4a47647
+ zh: 6146424ac5c3987b
+/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/decision:
+ en: 0a45affd1b125113
+ zh: 3af28a27b7721bc7
+/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/decision/registration-and-rendering:
+ en: 0af348d91eedc16e
+ zh: a06d6f59cc271e94
+/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/decision/data-state-and-lifecycle:
+ en: e220ebf28a113e0b
+ zh: dfedd85ac5d76ccb
+/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/decision/translation-plugin-integration:
+ en: 145a1ae617a49f43
+ zh: b2eb8443bb1515a7
+/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/alternatives-considered:
+ en: 89adfc6fbad716f3
+ zh: 098cca9e73c5eeaf
+/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/verification:
+ en: 23e6e98d176f08f4
+ zh: 2b89720b8914552b
+/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/consequences:
+ en: 60a32f46ff947154
+ zh: 6498e791d4c2c166
diff --git a/.agents/notes/implemented/architecture/2026-10-09-reasoning-content-slot-factory.md b/.agents/notes/implemented/architecture/2026-10-09-reasoning-content-slot-factory.md
new file mode 100644
index 0000000000..b1c3f5c7d6
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-reasoning-content-slot-factory.md
@@ -0,0 +1,102 @@
+# Agent Note: Standard reasoning Body Slot and reusable Content Factory
+
+Status: implemented
+
+English | [中文](2026-10-09-reasoning-content-slot-factory.zh.md)
+
+## Problem
+
+Reasoning bodies need to let third parties replace or wrap their presentation while reusing the official Markdown renderer, localized labels, and compact typography. An optional renderer parameter that makes the host choose between direct rendering and a chain fallback leaves the default body outside the registration system and forces plugins to maintain another Markdown call.
+
+The render position and the base implementation serve different purposes: the position accepts plugin registrations and lets the framework select one, while any component can call the implementation directly. Calling the base implementation must not repeat plugin selection for that position or require a Session.
+
+## Decision
+
+The design uses existing [Component Factories](2026-09-10-component-factories-and-local-slots.md) and ordinary Slots without adding framework dispatch machinery.
+
+| Name | Type | Owner and responsibility |
+| --- | --- | --- |
+| `conversation.chat.reasoning.body` | `single`, `session` Slot | Body position declared by the Assistant node registration; receives only original text and streaming state when expanded. |
+| `conversation.chat.reasoning.content` | `root` Factory | Standard reasoning content provided by `ui-chat`; accepts text, streaming state, and optional labels, supplies default labels, and fixes `variant="compact"`. |
+
+The Factory receives no Session identity, reads no Session, and owns no translation state. The current Factory API requires `scope`; `root` expresses the absence of a Session dependency, while each render position still has an independent React instance. The Chat plugin registers the Factory independently of any Body occupant, so replacing a body does not remove the base implementation.
+
+### Registration and rendering
+
+1. `ui-chat` registers the Content Factory with component props derived through `FactoryComponentPropsOf`. When the caller omits labels, the Factory constructs stable defaults from Chat's locale seat.
+2. The Assistant node registration declares the Body Slot. An expanded `ReasoningRow` only calls `renderSlot`; it accepts no optional renderer, supplies no inline fallback, and does not check whether a plugin is enabled.
+3. The official default Body is an ordinary registration with `priority: 100`. It forwards only `text` and `running` to the Content Factory.
+4. Third parties wait for the Body declaration through `slots.inject` and register their Body with a smaller priority value. They may call the Content Factory directly and add their own controls around it without importing private official React components.
+5. When a third party unregisters, the framework selects the official default registration again. Multiple third-party registrations follow existing single-slot priorities; they do not form middleware or render multiple bodies together.
+
+```text
+ReasoningRow
+ └─ conversation.chat.reasoning.body
+ ├─ 官方默认 Body ───────────┐
+ └─ 第三方 Body → 自有控件 ─┤
+ └─ conversation.chat.reasoning.content
+ └─ MarkdownText
+```
+
+The body render position:
+
+```tsx ignore-check
+
+ {renderSlot('conversation.chat.reasoning.body', { text, running })}
+
+```
+
+The default Body and third-party wrappers use the same reusable implementation; a third party passes labels when it needs custom copy:
+
+```tsx ignore-check
+renderFactorySlot('conversation.chat.reasoning.content', {
+ text: displayText,
+ running,
+ labels,
+})
+```
+
+### Data, state, and lifecycle
+
+- `ReasoningBodyOwnerProps` contains only readonly `text: string` and `running: boolean`. `ReasoningContentInput` also accepts `labels?: MarkdownLabels`; supplied labels are used unchanged without field-level merging, and omission selects the official defaults. Callers own the localization and reference stability of custom labels.
+- The Body's `text` is always the original reasoning. A third party selects original or derived display text and passes that value to the Factory without rewriting the Session or Chat projection.
+- `ReasoningRow` retains its existing disclosure state, summary, shimmer, and title behavior. Plugin-specific selection state, buttons, and styles stay inside the plugin's Body wrapper; the official implementation supplies no `setHeaderAction`, header-action type, or corresponding state.
+- Registrations follow Cordis effect lifetimes. Collapsing the Body declaration removes its contributions through `slots.inject`; unloading the Chat plugin removes the Content Factory.
+- Factory components and the default Body have stable module-level identities. Text changes update props instead of rebuilding component types or keys. Plugin replacement or HMR may remount components according to existing framework semantics; unmounted components have no state-retention guarantee.
+
+### Translation plugin integration
+
+The [anonymous translation design](2026-10-05-anonymous-reasoning-translation.md) retains its external-request, privacy, failure, and cancellation decisions; this design supersedes its body composition mechanism. The translation plugin registers in the standard Body Slot, renders original/translation selection, status, and retry controls in its own toolbar, and wraps the official Content Factory with its own state and optional custom labels rather than calling `MarkdownText` directly.
+
+Translation requests, splitting, caching, persistence, and settings remain unchanged. The translation toggle belongs to the plugin body rather than the official Think title. The design adds no Slot outside the body, does not handle user-message masking, and does not change the general Markdown parser.
+
+## Alternatives considered
+
+**Translation-specific chain and host fallback.** The default content is not an ordinary registration, the host maintains a second render path, and third parties cannot directly reuse the complete official body implementation.
+
+**Expose only the Body Slot.** This permits replacement, but wrapping plugins still have to copy the official Markdown call, labels, and variant.
+
+**Provide only the Content Factory.** This permits implementation reuse but supplies no actual body position for third-party replacement registrations.
+
+**Export React components across plugins.** This bypasses existing Factory loading, lifecycle, and type derivation and adds feature-plugin runtime dependencies.
+
+**Name the Factory as general Markdown.** This implementation fixes reasoning-specific compact typography and supplies Chat defaults; it is not a general entry point for all Markdown uses.
+
+**Make the official implementation maintain plugin header actions.** The default body has no such requirement; a dedicated setter makes the host own plugin state, cleanup, and button styles.
+
+**Force official labels in the Factory.** Default copy must not prevent caller customization; an optional parameter provides an override without replacing the whole body implementation.
+
+## Verification
+
+- The body host only calls `conversation.chat.reasoning.body`, with no old `reasoning-body` chain, optional renderer, or inline Markdown fallback.
+- Both the default Body and translation Body render through `conversation.chat.reasoning.content`; omitted labels use official defaults, and custom labels reach Markdown unchanged.
+- The Factory works without a Session Provider, and two render positions do not share component-local state.
+- The official implementation has no header-action type, setter, state, or dedicated styles; the translation wrapper supplies its own selection and retry controls.
+- Enabling, disabling, or hot-reloading the translation plugin selects the corresponding Body; default output, original-text access, streaming, disclosure, and cancellation behavior remain consistent.
+- Unit and assembly tests cover registration disposal, a Session-independent Factory, default and custom labels, shared implementation reuse, and independent occurrences; existing browser replay covers default bodies and translation switching.
+
+## Consequences
+
+The old Slot name, chain-selector registrations, and header-action callback are unsupported, so third parties must migrate. Test fixtures and the Client inspect catalog must follow the new declarations rather than offer obsolete registration examples.
+
+The Body remains an exclusive replacement position. This design does not automatically compose multiple text-transformation plugins or turn display transformations into redaction or model-input modification.
diff --git a/.agents/notes/implemented/architecture/2026-10-09-reasoning-content-slot-factory.zh.md b/.agents/notes/implemented/architecture/2026-10-09-reasoning-content-slot-factory.zh.md
new file mode 100644
index 0000000000..73986249ef
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-reasoning-content-slot-factory.zh.md
@@ -0,0 +1,102 @@
+# Agent Note: 思考正文的标准 Slot 与可复用 Content Factory
+
+Status: implemented
+
+[English](2026-10-09-reasoning-content-slot-factory.md) | 中文
+
+## 问题
+
+思考正文需要允许第三方替换或包装展示,同时复用官方的 Markdown、本地化文案和紧凑排版。将扩展做成可选的 renderer 参数,再由宿主选择直接渲染或 chain fallback,会让默认正文存在于注册体系之外,插件也只能另行维护 Markdown 调用。
+
+渲染位置与基础实现是两种不同的能力:位置允许多个插件注册并由框架选中一个,实现则应允许任何组件直接调用。调用基础实现不应再次触发该位置的插件选择,也不应要求 Session。
+
+## 决策
+
+沿用现有 [Component Factory](2026-09-10-component-factories-and-local-slots.zh.md) 和普通 Slot,不增加框架分发机制。
+
+| 名称 | 类型 | 所有者与职责 |
+| --- | --- | --- |
+| `conversation.chat.reasoning.body` | `single`、`session` Slot | Assistant 节点注册声明的正文位置;展开时只接收原始文本与流式状态。 |
+| `conversation.chat.reasoning.content` | `root` Factory | `ui-chat` 提供的标准思考正文;接收文本、流式状态及可选 labels,提供默认文案并固定 `variant="compact"`。 |
+
+Factory 不接收 Session 身份,不读取 Session,不持有翻译状态。现有 Factory API 要求填写 `scope`,使用 `root` 表达无 Session 依赖;每个调用位置仍有独立的 React 实例。Factory 由 Chat 插件注册,而非依附于某个 Body occupant,第三方替换正文不会撤掉基础实现。
+
+### 注册与调用
+
+1. `ui-chat` 注册 Content Factory,组件 props 由 `FactoryComponentPropsOf` 推导。调用者未传 labels 时,Factory 使用 Chat 的 locale seat 构造稳定的默认文案。
+2. Assistant 节点注册声明 Body Slot。`ReasoningRow` 展开时只调用 `renderSlot`,不接收可选 renderer,不提供内联 fallback,也不判断插件是否启用。
+3. 官方以 `priority: 100` 注册普通默认 Body。该组件只将 `text`、`running` 传给 Content Factory。
+4. 第三方通过 `slots.inject` 等待 Body 声明,以较小的 priority 注册自己的 Body。第三方可直接调用 Content Factory,并在外层增加自己的控件;无需导入官方私有 React 组件。
+5. 第三方注销后,框架重新选中官方默认注册项。多个第三方注册遵循现有 single 优先级,不形成 middleware,也不同时渲染多份正文。
+
+```text
+ReasoningRow
+ └─ conversation.chat.reasoning.body
+ ├─ 官方默认 Body ───────────┐
+ └─ 第三方 Body → 自有控件 ─┤
+ └─ conversation.chat.reasoning.content
+ └─ MarkdownText
+```
+
+正文调用位置:
+
+```tsx ignore-check
+
+ {renderSlot('conversation.chat.reasoning.body', { text, running })}
+
+```
+
+默认 Body 与第三方包装都使用同一个可复用实现;第三方需要自定义文案时传入 labels:
+
+```tsx ignore-check
+renderFactorySlot('conversation.chat.reasoning.content', {
+ text: displayText,
+ running,
+ labels,
+})
+```
+
+### 数据、状态与生命周期
+
+- `ReasoningBodyOwnerProps` 仅包含只读的 `text: string` 与 `running: boolean`。`ReasoningContentInput` 另外允许 `labels?: MarkdownLabels`;传入的文案原样使用,不做字段级合并,省略时才使用官方默认文案。调用者负责自定义文案的本地化与引用稳定性。
+- Body 的 `text` 始终是原始思考。第三方选择原文或派生显示文本后,只将该值交给 Factory,不改写 Session 或 Chat projection。
+- 折叠状态、摘要、流光和标题由 `ReasoningRow` 保持现有行为。插件自己的切换状态、按钮及样式留在其 Body 包装中;官方不提供 `setHeaderAction`、标题动作类型或相应状态。
+- 注册沿用 Cordis effect 生命周期。Body 声明撤销时,其贡献随 `slots.inject` 清理;Content Factory 在 Chat 插件卸载时撤销。
+- Factory 组件与默认 Body 使用模块级稳定身份,文本变化作为 props 更新,不以文本重建组件类型或 key。插件替换或 HMR 可按现有框架语义重新挂载,不承诺保留被卸载组件的本地状态。
+
+### 翻译插件接入
+
+[免登录翻译设计](2026-10-05-anonymous-reasoning-translation.zh.md)保留外部请求、隐私、失败与取消决策;其正文组合方式由本方案取代。翻译插件注册到标准 Body Slot,在自己的工具栏内渲染原文/译文切换、状态及重试按钮,使用自己的状态和可选自定义 labels 包装官方 Content Factory,不直接调用 `MarkdownText`。
+
+翻译请求、分片、缓存、持久化和设置保持不变。翻译切换按钮属于插件正文,不再由官方思考标题承载;不增加正文外的 Slot,不处理用户消息遮挡,也不改动通用 Markdown 解析器。
+
+## 考虑过的替代方案
+
+**翻译专用 chain 与宿主 fallback。** 默认内容不是普通注册项,宿主需要维护第二条渲染路径,第三方无法直接复用完整的官方正文实现。
+
+**只开放 Body Slot。** 可以替换正文,但第三方包装时仍需复制官方 Markdown 调用、labels 与 variant。
+
+**只提供 Content Factory。** 可以复用实现,但缺少允许第三方注册替换的实际正文位置。
+
+**跨插件导出 React 组件。** 绕过现有 Factory 的加载、生命周期与类型推导,增加 feature-plugin 运行时依赖。
+
+**将 Factory 命名为通用 Markdown。** 该实现固定思考正文的紧凑排版,并提供 Chat 默认文案,不是所有 Markdown 场景的通用入口。
+
+**官方替插件维护标题动作。** 官方默认正文没有该需求;保留专用 setter 会让宿主承担插件的状态、清理与按钮样式。
+
+**Factory 强制使用官方 labels。** 默认文案不应阻止调用者定制;可选参数即可表达覆盖,无需替换整套正文实现。
+
+## 验证
+
+- 正文宿主只调用 `conversation.chat.reasoning.body`,没有旧 `reasoning-body` chain、可选 renderer 或内联 Markdown fallback。
+- 官方默认 Body 和翻译 Body 都通过 `conversation.chat.reasoning.content` 渲染;未传 labels 使用官方默认文案,传入自定义 labels 则原样交给 Markdown。
+- Factory 在没有 Session Provider 的调用位置也能使用;两个调用位置不共享组件本地状态。
+- 官方不存在标题动作类型、setter、状态或专用样式;翻译包装自行提供切换与重试控件。
+- 翻译插件启用、停用及 HMR 后,框架选中对应 Body;默认输出、原文访问、流式展示、折叠和取消行为保持一致。
+- 单元与装配测试覆盖注册撤销、无 Session Factory、labels 缺省与覆盖、默认与第三方共用实现及双实例;已有浏览器回放覆盖默认正文和翻译切换。
+
+## 后果
+
+旧 Slot 名称、chain selector 注册及标题动作回调不再受支持,第三方需要迁移。测试夹具与 Client inspect catalog 必须同步到新声明,避免仍向插件开发者提供旧注册方式。
+
+Body 仍是独占替换位置。此方案不解决多个正文转换插件的自动组合,也不将显示变换提升为脱敏或模型输入修改能力。
diff --git a/.agents/notes/implemented/architecture/2026-10-09-stored-credential-precedence.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-09-stored-credential-precedence.i18n.yaml
new file mode 100644
index 0000000000..544d0c2c04
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-stored-credential-precedence.i18n.yaml
@@ -0,0 +1,22 @@
+# Bilingual-pair consistency record for 2026-10-09-stored-credential-precedence.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-09-stored-credential-precedence.md
+/agent-note-stored-credentials-take-precedence-over-the-launch-environment:
+ en: 5253af6605ba0fac
+ zh: 160ac751c664a502
+/agent-note-stored-credentials-take-precedence-over-the-launch-environment/problem:
+ en: 878c68dcfe5b3771
+ zh: 8a24d16118155245
+/agent-note-stored-credentials-take-precedence-over-the-launch-environment/decision:
+ en: 7cd89bda9f793431
+ zh: 93d26a948eea63f3
+/agent-note-stored-credentials-take-precedence-over-the-launch-environment/per-run-overrides:
+ en: 7479ffc82280a1fb
+ zh: ce3d42489191ab2b
+/agent-note-stored-credentials-take-precedence-over-the-launch-environment/alternatives-considered:
+ en: 268add7fd8415280
+ zh: 0765cbfd3fc7b57c
+/agent-note-stored-credentials-take-precedence-over-the-launch-environment/consequences:
+ en: 2d71fd527b3060c4
+ zh: fafed884315b9782
diff --git a/.agents/notes/implemented/architecture/2026-10-09-stored-credential-precedence.md b/.agents/notes/implemented/architecture/2026-10-09-stored-credential-precedence.md
new file mode 100644
index 0000000000..75997d9db0
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-stored-credential-precedence.md
@@ -0,0 +1,45 @@
+# Agent Note: Stored credentials take precedence over the launch environment
+
+Status: implemented
+
+English | [中文](2026-10-09-stored-credential-precedence.zh.md)
+
+The [headless environment authentication](2026-10-12-headless-environment-auth.md) decision partially supersedes this note's per-run override policy: the shipped headless profile omits the store. The ordering below remains the store's contract when mounted.
+
+## Problem
+
+[Configuration source ownership](2026-08-04-configuration-source-ownership.md) ranked the inherited process environment above `$DSH_HOME/.credentials.yaml`, and `credentials-local` refused `set` and `unset` for a name the launching shell exported. Two kinds of stored credential then had opposite precedence. pi-ai's resolvers read the credential record stored under `llm-pi-ai/` before any environment variable, so a stored OAuth grant or `api-key` record won over an exported key. A key that the Models page stored under a reference lost to the same exported name. Which credential a request used therefore depended on the provider family, and the Models page disabled its key field whenever the name was exported (#5527).
+
+## Decision
+
+**A stored credential wins for every provider.** `LocalCredentialProvider` resolves a reference in this order:
+
+```text
+$DSH_HOME/.credentials.yaml (provider-managed, written by set/unset)
+> inherited process environment (fallback; the provider never writes it)
+> /.env (fallback; the provider never writes it)
+> $DSH_HOME/.env (fallback; the provider never writes it)
+```
+
+A save always writes the winning layer, so `describe` reports `writable: true` for every reference, and `set` and `unset` never refuse a name because it is exported. `resolve` and `describe` still report the supplying layer as `source` (`file`, `env`, `project-env`, or `user-env`). `authContextFrom` in `llm-pi-ai` answers pi-ai's `env()` from the credential seam before the launch environment, so a pi-ai resolver reads its stored record first, then a stored reference, then the fallbacks in the order above.
+
+The non-secret ordering and the environment restrictions in configuration source ownership are unchanged. This note supersedes only its credential ordering.
+
+## Per-run overrides
+
+The previous order existed so that CI, containers, and a one-shot `DEEPSEEK_API_KEY=… dsh` could override a credential without editing machine state. A fresh CI or container harness home has no `.credentials.yaml`, so its exported secret is still the effective value. A harness home holds a stored key only because someone saved it there, through the Models page or by editing the file. To use an exported value for that name, the user deletes the `refs` entry, or launches with a separate `DSH_HOME`.
+
+## Alternatives considered
+
+**Keep the launch environment on top.** It keeps per-run overrides without touching the store, but it keeps the opposite precedence of stored records and stored references, and the Models page cannot save or replace a key while the launching shell exports the name.
+
+**Refuse a write while only the launch environment supplies the name.** Under the new order such a write takes effect, because the stored value outranks the environment. Refusing it would block the first save of a key for every exported name, and it would keep the read-only field state that the new order removes.
+
+**Warn when a stored value hides an exported variable.** The provider would compare layers on every resolution and report the result for users who keep both values on purpose, such as a shell profile that exports a key for other tools. The Models page help text states the order, and `describe` reports the supplying layer.
+
+## Consequences
+
+- A key left in `.credentials.yaml` overrides an exported variable with the same name, including a CI secret on a machine whose harness home stores a value for that name. The [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-alpha.2/stored-credentials-precedence/guide.md) tells affected users how to keep the exported account.
+- The Models page can always save, replace, or remove a key, and it does not disable the key field for an exported name.
+- There is no per-run flag above the store. A one-run override takes a separate `DSH_HOME`.
+- `packages/credentials/credentials-local/tests/local.spec.ts` covers a store over a launch environment value and its return after `unset`; `packages/llm/llm-pi-ai/tests/auth.spec.ts` covers a stored credential that wins over every launch layer.
diff --git a/.agents/notes/implemented/architecture/2026-10-09-stored-credential-precedence.zh.md b/.agents/notes/implemented/architecture/2026-10-09-stored-credential-precedence.zh.md
new file mode 100644
index 0000000000..3846944c8d
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-09-stored-credential-precedence.zh.md
@@ -0,0 +1,45 @@
+# Agent Note: 存储的凭据优先于启动环境
+
+Status: implemented
+
+[English](2026-10-09-stored-credential-precedence.md) | 中文
+
+[Headless 环境认证](2026-10-12-headless-environment-auth.zh.md)决策部分取代本文的单次覆盖策略:随附 headless profile 不挂载存储。存储挂载时仍遵循下述顺序。
+
+## 问题
+
+[配置来源所有权](2026-08-04-configuration-source-ownership.zh.md) 把继承的进程环境排在 `$DSH_HOME/.credentials.yaml` 之前,并且 `credentials-local` 会拒绝对启动 shell 已导出的名字执行 `set` 和 `unset`。于是两类存储的凭据具有相反的优先级。pi-ai 的解析器先读取存储在 `llm-pi-ai/` 下的凭据记录,再读取任何环境变量,因此存储的 OAuth 授权或 `api-key` 记录会压过导出的密钥;而 Models 页面以引用形式存储的密钥会输给同名的导出变量。一次请求使用哪个凭据因此取决于提供方类别,而且只要该名字被导出,Models 页面就会禁用其密钥输入框(#5527)。
+
+## 决策
+
+**存储的凭据对每个提供方都优先。** `LocalCredentialProvider` 按以下顺序解析引用:
+
+```text
+$DSH_HOME/.credentials.yaml (provider-managed, written by set/unset)
+> inherited process environment (fallback; the provider never writes it)
+> /.env (fallback; the provider never writes it)
+> $DSH_HOME/.env (fallback; the provider never writes it)
+```
+
+保存总是写入胜出的那一层,因此 `describe` 对每个引用都报告 `writable: true`,`set` 与 `unset` 也不会因为名字已被导出而拒绝。`resolve` 与 `describe` 仍以 `source`(`file`、`env`、`project-env` 或 `user-env`)报告提供值的层。`llm-pi-ai` 中的 `authContextFrom` 先从凭据 seam、再从启动环境回答 pi-ai 的 `env()`,因此 pi-ai 解析器依次读取其存储的记录、存储的引用,再按上面的顺序读取后备来源。
+
+配置来源所有权中的非秘密顺序与环境限制保持不变。本 Note 只替代其中的凭据优先级。
+
+## 按次覆盖
+
+之前的顺序让 CI、容器和一次性的 `DEEPSEEK_API_KEY=… dsh` 无需修改机器状态就能覆盖凭据。全新的 CI 或容器 harness home 中没有 `.credentials.yaml`,因此其导出的机密仍是生效值。harness home 中存有密钥,只可能是有人通过 Models 页面或直接编辑该文件存入的。要对该名字使用导出的值,用户应删除对应的 `refs` 条目,或使用单独的 `DSH_HOME` 启动。
+
+## 考虑过的替代方案
+
+**保持启动环境在最上层。** 它无需改动存储就能保留按次覆盖,但会保留存储记录与存储引用之间相反的优先级,而且只要启动 shell 导出了该名字,Models 页面就无法保存或替换密钥。
+
+**只有启动环境提供该名字时拒绝写入。** 在新顺序下这样的写入会生效,因为存储值优先于环境。拒绝它会阻止为每个已导出的名字首次保存密钥,也会保留新顺序已经去掉的只读输入框状态。
+
+**存储值遮蔽导出变量时发出警告。** 提供方需要在每次解析时比较各层,并向有意同时保留两个值的用户报告结果,例如在 shell 配置文件中为其他工具导出密钥的用户。Models 页面的帮助文本说明了该顺序,`describe` 也会报告提供值的层。
+
+## 后果
+
+- 留在 `.credentials.yaml` 中的密钥会覆盖同名的导出变量,包括在 harness home 存有该名字的机器上运行的 CI 机密。[升级指南](../../../../docs/upgrade-guide/v0.2.1-alpha.2/stored-credentials-precedence/guide.zh.md)说明受影响的用户如何继续使用导出的账户。
+- Models 页面总能保存、替换或删除密钥,也不会因名字被导出而禁用密钥输入框。
+- 存储之上没有按次生效的标志。一次运行的覆盖需要使用单独的 `DSH_HOME`。
+- `packages/credentials/credentials-local/tests/local.spec.ts` 覆盖在启动环境值之上存储、以及 `unset` 后恢复该值;`packages/llm/llm-pi-ai/tests/auth.spec.ts` 覆盖存储的凭据压过每个启动层。
diff --git a/.agents/notes/implemented/architecture/2026-10-10-user-text-json-nodes.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-10-user-text-json-nodes.i18n.yaml
new file mode 100644
index 0000000000..41d9dfe377
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-10-user-text-json-nodes.i18n.yaml
@@ -0,0 +1,40 @@
+# Bilingual-pair consistency record for 2026-10-10-user-text-json-nodes.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-10-user-text-json-nodes.md
+/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh:
+ en: 6185ac67a459beba
+ zh: 994bf90f05c0fd5a
+/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/problem:
+ en: 42e3752efebedde5
+ zh: 75e23c5762efbb1f
+/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/decision:
+ en: 71f5b33f58cd2066
+ zh: 8365506795c0b158
+/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/decision/rule-roles-and-original-coordinates:
+ en: ed0fa9306f0605cb
+ zh: 7240e135f6d1b46b
+/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/decision/one-reconciliation-loop:
+ en: acaeb2dccfac3f07
+ zh: 2170ec2b858627fc
+/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/decision/query-cache-and-traversal:
+ en: 55dee64de32bef9a
+ zh: 69eb625451f55268
+/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/decision/actual-facts-dependencies:
+ en: 11bc718324d22c03
+ zh: 371576c1b2a90d62
+/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/decision/facts-text-and-registration-refresh:
+ en: 47bc3f32b9aa22de
+ zh: 59ffd06c800c00af
+/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/decision/output-ownership-and-publication:
+ en: e076cb57a1fdcfc1
+ zh: 988c02c6cf751f16
+/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/verification-requirements:
+ en: 78127c1f18eacf8a
+ zh: fcc2ad5db1279356
+/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/alternatives-considered:
+ en: 439e5ecff6701938
+ zh: 9861805f2a19439c
+/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/consequences:
+ en: 13bd26532d0353fa
+ zh: d6a4997b51af09d5
diff --git a/.agents/notes/implemented/architecture/2026-10-10-user-text-json-nodes.md b/.agents/notes/implemented/architecture/2026-10-10-user-text-json-nodes.md
new file mode 100644
index 0000000000..3b01c313f0
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-10-user-text-json-nodes.md
@@ -0,0 +1,91 @@
+# Agent Note: User-text candidate scheduling and Facts-driven refresh
+
+Status: implemented
+
+English | [中文](<2026-10-10-user-text-json-nodes.zh.md>)
+
+## Problem
+
+User text contains references owned by independent businesses. Recognition must follow original-text positions, resolve collisions deterministically, and reuse unaffected matching work when Facts change. The evaluator must expose its scheduling behavior to pure logic tests without depending on React, browser timing, or elapsed-time benchmarks.
+
+## Decision
+
+The composition is business registration, lazy candidate matching, a single reconciliation loop, readonly JSON nodes, and business-owned keyed renderers. Initialization and Facts refresh use the same algorithm; they differ only in which query caches remain valid.
+
+### Rule roles and original coordinates
+
+Every rule provides an ID, finite order, and synchronous `match(text, from, context)`. Each call returns its nearest eligible node with `start >= from`, or no candidate anywhere in the remaining original text. It returns one node, not all matches or a replacement document. The original string and UTF-16 offsets remain intact: a search cursor does not create a new word, line, or quoted-token boundary. Matching depends only on original text and Facts, not previously selected node payloads.
+
+Each matcher resolves its own alternative spellings by earliest start and then its documented same-start choice; Session labels choose the longer match. A matcher cannot replace a consumed prefix with the remainder of an overlapping candidate. Each business owns its grammar, payload, and renderer; the generic evaluator has no Session, file, skill, or command branches.
+
+There is one rule type and one candidate protocol. A candidate contains either a business node using the rule ID or a Text node preserving the original substring with an optional `data.display`. Text-producing rules compete by the same start, order, and stable ID as business rules. Order only breaks same-start ties; no rule gets precedence over an earlier position.
+
+### One reconciliation loop
+
+The evaluator keeps original text, the previous successful segmentation, registered definitions, per-rule Facts reads, and query results. Nonempty text initially has one Text covering `[0, text.length)`; empty text has no nodes. Initialization makes every rule invalid, including rules that read no Facts.
+
+Each reconciliation starts its result cursor at zero. Every matcher obtains one candidate through the same cache-aware query operation. The evaluator selects the smallest candidate start, breaks same-start ties by ascending rule order and stable ID, emits the preceding raw Text gap, emits the winner, and advances the cursor to the winner's end. No candidate means the remaining raw text is emitted and recognition ends.
+
+Only a consumed candidate or a candidate whose start is before the new cursor needs another query. A cached candidate beginning at or after the cursor remains eligible. A no-candidate result remains eligible as the cursor advances. All ranges are nonempty half-open original-text ranges; candidates cannot move the cursor backward or exceed the original string.
+
+Previously accepted nodes are reusable output, not a complete list of possible matches: a losing candidate can become visible when an earlier winner disappears. The evaluator retains queries for this purpose instead of treating an old Text node or the absence of a business node as unconditional evidence of no match.
+
+### Query cache and traversal
+
+A query record contains its original `from` offset and either the next candidate, no candidate, or a failure. If a query from `q` returned a candidate starting at `s`, its result is valid for any requested cursor `c` with `q <= c <= s`. A no-candidate result is valid for every `c >= q`. Neither result proves anything before `q`. A failed query is reusable only at the same requested offset while its dependencies remain valid.
+
+During a reconciliation, each matcher has one active candidate. The evaluator also retains the sorted query records already produced for that matcher across reconciliations; it does not enumerate all possible matches eagerly. Forward cursors walk these records monotonically. Queries generated in newly exposed gaps are merged with retained records in source order, with one record per query offset. Unsearched intervals require matching rather than an invented negative result.
+
+The result cursor and matcher search cursors are distinct. Starting result reconciliation at zero does not require unchanged matchers to scan from zero again: their recorded queries answer valid requests. Each matcher must implement a forward search rather than repeatedly materializing every match in the remaining text. Internal scans, candidate selection, and result comparison still have costs; the scheduling guarantees concern actual matcher calls, not a claim that arbitrary regular expressions are linear.
+
+### Actual Facts dependencies
+
+`context.read(key)` returns the borrowed readonly value from the current Facts snapshot and records the read, including `undefined`. The reader is valid only during its owning synchronous call and cannot be retained. Businesses receive neither a whole Facts object, a manual `dependsOn` list, nor previous-output/cache objects.
+
+A matcher's dependency set is the union of reads for the queries retained in its current cache generation, including losing candidates, no-match queries, and failed calls. Changed fields are detected with `Object.is` at field granularity. A new outer Facts object alone does not invalidate matching. When a rule is invalidated, its old queries and read set are discarded; subsequent calls establish the replacement read set, so obsolete conditional dependencies do not remain permanently attached.
+
+Text-producing rules use the same query cache and dependency generation as every other rule. Updating a whitespace switch invalidates only its readers; it does not trigger an unconditional pass over the completed nodes.
+
+### Facts, text, and registration refresh
+
+A Facts update invalidates only rules that read changed fields. Their first query starts at original offset zero; all other rules retain their query records. The same reconciliation loop then compares fresh candidates with reusable ones. Refreshing a rule does not give it priority, and a new first candidate does not hide the removal of an earlier old winner.
+
+A fresh candidate confined to previously plain text can split that Text without invoking unrelated matchers whose cached queries still cover the new cursors. A fresh candidate that loses to an earlier unchanged winner is discarded without invalidating that winner. When a fresh winner covers another candidate, only expired heads advance. When a winner disappears or shrinks, cached losing candidates can compete again; any previously unsearched interval causes a query at the newly exposed cursor. All changes use the same cursor loop and query-coverage rules.
+
+Several changed Facts fields form one snapshot and one invalidation set. All affected matchers participate in the same reconciliation; one update is not committed before processing the next. Equivalent refreshed results reuse previous nodes and, when all elements remain identical, the entire result array. Reconciliation settles the complete document before publishing its result.
+
+Original-text changes discard all query caches and supply one full-range Text as the previous segmentation. New or replaced rule definitions start with empty caches; unchanged definitions retain their independent queries. Removed rules lose their dependencies and candidates. Reconciliation after additions, removals, or order changes uses the same selection logic and can expose formerly blocked candidates.
+
+### Output, ownership, and publication
+
+Nodes retain `{ id, start, end, data }`. Business nodes use their matcher's ID; Text retains the exact source substring in `data.text` and optional display text in `data.display`. Output is ordered, nonoverlapping, and completely covers the original text. JSON compatibility and readonly ownership are TypeScript obligations: the evaluator neither freezes borrowed objects nor deep-copies Facts or payloads for isolation.
+
+The document evaluator owns all caches; the registry retains neither evaluators nor document subscriptions. Separate text instances do not share state through a global cache. Only fully successful evaluation publishes a new result. A matcher failure carries the rule ID and cause and publishes no partial segmentation; its actual reads remain available for dependency-driven retry. Reentrant evaluation and escaped synchronous read contexts are rejected.
+
+`conversation.user.text` remains a session-scoped Factory owning `conversation.user.text.node`, a keyed inline Slot. React consumes final nodes and forwards `raw` and `interactive` to the business renderer. Session, skill, file/folder, and command matching remain owned by their respective plugins, and actions remain in each renderer's Injection. No matching work or cache subscription moves into React.
+
+`collapseWhitespace` and `trimWhitespace` each control one ordinary matcher and default to off. Collapse offers whitespace runs as Text candidates with display `" "`; trim offers original leading/trailing whitespace runs with display `""`. Trim has lower order than collapse, so it wins their same-start edge collisions. An earlier reference candidate consumes its whole range and naturally excludes internal whitespace candidates. Every Text candidate retains its exact raw substring; emoji and spaces between references remain intact. `interactive` and CSS `ellipsis` remain React Props, not matching Facts. Queue uses full original text with CSS ellipsis; native attachments remain in their existing cards and image Slots, outside text recognition.
+
+## Verification requirements
+
+Pure logic tests assert both the final nodes and each matcher's ordered calls with their `from` offsets. They use deterministic input and spies rather than clocks or performance thresholds.
+
+- Initialization calls every matcher, including Facts-free and no-match rules; empty input has explicit terminal-query behavior.
+- Earlier starts beat stronger order values; same-start ties use order and stable ID, and each matcher preserves its own longest-match choice.
+- A future candidate and a no-match answer are not requested again during forward traversal; only the winner and overlapping heads advance.
+- Unread Facts changes and new outer Facts objects with unchanged field values cause no matcher calls.
+- One or several changed fields rerun exactly their readers; missing fields, losing candidates, no-match results, failures, and conditional read-set replacement remain covered.
+- Equivalent refreshes preserve node/array identity and do not invoke unrelated matchers; payload-only changes preserve unaffected nodes.
+- Insertions in plain text, losing new candidates, same-start winner changes, extending/shrinking matches, and removal of an early winner assert the exact affected query offsets and counts.
+- Removing or shrinking a winner exposes cached losers and unsearched gaps; an old tail no-match result cannot answer a query before its recorded origin.
+- Batched Facts updates equal a fresh evaluation under the same snapshot and do not give the first refreshed matcher precedence.
+- Text and rule-definition changes, registration/disposal, range validation, synchronous failure containment, Context lifetime, and independent evaluator state have deterministic tests.
+- Real Session/file/skill/command combinations preserve full quoted references, original word boundaries, and same-start precedence; whitespace candidates use exactly the same scheduling and cache rules.
+
+## Alternatives considered
+
+**Ordered whole-document business transforms.** A business can consume later text before an earlier reference from another business is considered, so fixed type order does not express leftmost recognition.
+
+## Consequences
+
+Field-level invalidation can require a matcher to traverse the document again; it does not promise per-element Facts invalidation. Query history consumes memory proportional to the distinct recorded search positions for the current text and definitions. Reconciliation and JSON identity alignment can still traverse output without invoking matchers. The performance evidence is the exact scheduling count and query offsets, while semantic evidence compares refreshed output with a fresh evaluator. This mechanism requires no browser end-to-end or elapsed-time benchmark tests.
diff --git a/.agents/notes/implemented/architecture/2026-10-10-user-text-json-nodes.zh.md b/.agents/notes/implemented/architecture/2026-10-10-user-text-json-nodes.zh.md
new file mode 100644
index 0000000000..c689d56cc9
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-10-user-text-json-nodes.zh.md
@@ -0,0 +1,91 @@
+# Agent Note: 用户文本候选调度与 Facts 驱动刷新
+
+Status: implemented
+
+[English](<2026-10-10-user-text-json-nodes.md>) | 中文
+
+## 问题
+
+用户文本包含独立业务拥有的引用。识别必须按原文位置推进,确定性地处理冲突,并在 Facts 变化时复用无关匹配工作。计算器必须能通过纯逻辑测试展示调度行为,不依赖 React、浏览器时序或耗时基准。
+
+## 决策
+
+采用业务注册、按需候选匹配、单一协调循环、只读 JSON 节点和业务拥有的 keyed renderer。初始化与 Facts 刷新使用同一个算法,只在查询缓存的有效性上存在差别。
+
+### 规则职责与原始坐标
+
+所有规则都提供 ID、有限的 order 和同步 `match(text, from, context)`。每次调用返回 `start >= from` 的最近有效节点,或确认剩余原文中没有候选。它返回一个节点,不返回全部匹配或整份替换文档。原始字符串及 UTF-16 坐标保持不变:搜索游标不创造新的词首、行首或带引号词元边界。匹配只依赖原文和 Facts,不依赖先前选中节点的 payload。
+
+每个匹配器先按最早起点处理自身不同写法,再按该语法规定处理同起点选择;Session 标签选择较长匹配。匹配器不能把被消费前缀之后的半截重叠候选变成新匹配。语法、payload 和 renderer 均归对应业务所有,通用计算器不包含 Session、文件、skill 或命令分支。
+
+只有一种规则类型和一个候选协议。候选既可以包含使用规则 ID 的业务节点,也可以包含保留原文子串、携带可选 `data.display` 的 Text 节点。产生 Text 的规则与业务规则按相同的起点、order 和稳定 ID 竞争。Order 仅裁定同起点冲突,任何规则都不优先于更早的位置。
+
+### 单一协调循环
+
+计算器保留原文、上一次成功分段、注册定义、逐规则 Facts 读取记录和查询结果。非空原文最初对应一个覆盖 `[0, text.length)` 的 Text,空原文没有节点。初始化使所有规则失效,包括不读取 Facts 的规则。
+
+每次协调都从结果游标零开始,各匹配器通过同一个带缓存的查询操作获取一个候选。计算器选择起点最小的候选,同起点按规则 order 升序和稳定 ID 裁定,输出前面的原文 Text 空隙,输出赢家,再将游标推进到赢家末尾。全部没有候选时,输出剩余原文并结束识别。
+
+只有被消费的候选或起点早于新游标的候选需要再次查询。缓存候选从新游标或其后开始时仍然有效;无候选结论在游标前进时保持有效。所有范围均是原文中的非空半开区间;候选不能使游标倒退或超出原文。
+
+先前采纳的节点是可复用输出,不是所有可能匹配的完整清单;更早的赢家消失后,落败候选可能重新可见。计算器为此保留查询,而不能将旧 Text 节点或缺少某类业务节点无条件解释为没有匹配。
+
+### 查询缓存与遍历
+
+查询记录包含原始 `from` 偏移,以及下一个候选、无候选或失败。若从 `q` 查询得到起点为 `s` 的候选,则在请求游标 `c` 满足 `q <= c <= s` 时可以复用;无候选结论可用于所有 `c >= q`。两者都不能证明 `q` 之前的情况。失败查询只在请求偏移相同且依赖仍有效时复用。
+
+一次协调过程中,每个匹配器只有一个活动候选。计算器同时保留该匹配器跨次协调已经产生的有序查询记录,不预先枚举所有可能匹配。前进游标单调遍历这些记录;新暴露空隙中的查询按原文顺序与旧记录合并,每个查询偏移只保留一条记录。未搜索区间必须执行匹配,不能虚构无匹配结论。
+
+结果游标与匹配器搜索游标不同。结果协调从零开始,不代表无变化的匹配器需要从零重新扫描,它们的查询记录可以回答有效请求。各匹配器必须实现向前搜索,不能每次调用都重新收集剩余文本中的全部匹配。内部扫描、候选选择及结果比较仍有成本;调度保证针对实际匹配器调用,不宣称任意正则表达式都是线性的。
+
+### 实际 Facts 依赖
+
+`context.read(key)` 返回本次 Facts 快照中借用的只读值,并记录读取,包括 `undefined`。读取器仅在所属同步调用期间有效,不得保留。业务既不获得完整 Facts 对象,也不维护手工 `dependsOn` 列表或接收上一轮输出/缓存对象。
+
+匹配器依赖集合是当前缓存代次所保留查询读取字段的并集,包括落败候选、无匹配查询和失败调用。字段变化按 `Object.is` 比较,外层 Facts 对象换引用不单独使匹配失效。规则失效时丢弃旧查询及读取集合,后续调用建立新的集合,因此已经不再读取的条件依赖不会永久残留。
+
+产生 Text 的规则与其他规则共用查询缓存和依赖代次。空白开关更新只使它的读取者失效,不触发对已完成节点的无条件遍历。
+
+### Facts、原文与注册刷新
+
+Facts 更新只使实际读取变化字段的规则失效。它们的首次查询从原文零开始,其他规则保留查询记录;随后由同一个协调循环比较新候选和可复用候选。规则刷新不赋予额外优先权,新首个候选也不能掩盖更早旧赢家的消失。
+
+新候选完全位于原有普通文字内时,可以拆分该 Text;无关匹配器的缓存仍覆盖新游标时,不调用它们。新候选输给更早的有效旧赢家时,丢弃新候选,不使旧赢家失效。新赢家覆盖其他候选时,只推进过期的候选头。赢家消失或缩短时,缓存的落败候选可以重新竞争;此前未搜索的区间在新暴露游标处触发查询。所有变化都使用同一个游标循环和查询覆盖规则。
+
+多个 Facts 字段变化形成同一份快照和一个失效集合,所有受影响匹配器共同参与一次协调,不先提交某个更新再处理另一个。刷新结果等价时复用旧节点,所有元素相同则复用整个结果数组。协调在整份文档确定后发布结果。
+
+原文变化会丢弃全部查询缓存,并以覆盖全文的 Text 作为上一版分段。新增或替换的规则定义从空缓存开始,无变化定义保留各自独立查询。规则移除时删除其依赖和候选。新增、移除或顺序变化后的协调仍使用相同选择逻辑,并可暴露此前被遮挡的候选。
+
+### 输出、所有权与发布
+
+节点保留 `{ id, start, end, data }`。业务节点使用所属匹配器 ID;Text 的 `data.text` 保留准确原文子串,`data.display` 可保存显示文字。输出有序、非重叠并完整覆盖原文。JSON 兼容性与只读所有权由 TypeScript 协议约束;计算器既不冻结借用对象,也不为隔离而深拷贝 Facts 或 payload。
+
+文档计算器拥有全部缓存;注册表不持有计算器或文档订阅,不同文本实例不通过全局缓存共享状态。只有完整成功的求值才能发布新结果。匹配器失败时保留规则 ID 与 cause,不发布部分分段;实际读取仍参与依赖驱动的重试。禁止重入求值以及逃逸的同步读取 Context。
+
+`conversation.user.text` 保持 session 作用域 Factory,拥有 keyed inline Slot `conversation.user.text.node`。React 消费最终节点,将 `raw` 和 `interactive` 交给业务 renderer。Session、skill、文件/目录和命令匹配仍归各插件所有,动作仍由各 renderer 的 Injection 提供,匹配与缓存订阅不进入 React。
+
+`collapseWhitespace` 与 `trimWhitespace` 分别控制一个普通匹配器,缺省均关闭。Collapse 将空白串作为显示值为 `" "` 的 Text 候选;trim 将原文开头/末尾的空白串作为显示值为 `""` 的候选。Trim 的 order 小于 collapse,因此在同起点的边缘空白冲突中胜出。更早的引用候选消费自身完整范围,自然排除内部空白候选。每个 Text 候选保留准确的原文子串,emoji 及引用之间的空格保持不变。`interactive` 和 CSS `ellipsis` 仍为 React Props,不是匹配 Facts。Queue 使用完整原文和 CSS 省略,原生附件保留既有卡片及图片 Slot,不进入文本识别。
+
+## 验证要求
+
+纯逻辑测试同时断言最终节点,以及每个匹配器有序的调用和 `from` 偏移。使用确定性输入与 spy,不依赖时钟或性能阈值。
+
+- 初始化调用所有匹配器,包括不读 Facts 和始终无匹配的规则;空输入具有明确的终止查询行为。
+- 较早起点胜过更强 order;同起点按 order 和稳定 ID 裁定,各匹配器保留自身最长匹配选择。
+- 向前遍历不重复查询未来候选及无匹配结论,只推进赢家和重叠候选头。
+- 未读取 Facts 字段变化,或字段值不变而外层 Facts 对象换引用,不调用匹配器。
+- 一个或多个字段变化只重跑实际读取者;覆盖缺失字段、落败候选、无匹配结果、失败和条件依赖集合替换。
+- 等价刷新保留节点/数组引用且不调用无关匹配器;仅 payload 变化保留无关节点。
+- 普通文字中插入、落败的新候选、同起点换赢家、匹配扩张/缩短、较早赢家移除,均断言准确的受影响查询偏移与次数。
+- 赢家移除或缩短后暴露缓存落败者及未搜索空隙;旧尾部的无匹配结论不能回答其查询起点之前的问题。
+- 批量 Facts 更新等于相同快照下的新建计算器结果,不赋予先刷新匹配器额外优先权。
+- 原文及规则定义变化、注册/注销、范围校验、同步失败隔离、Context 生命周期和计算器状态独立均有确定性测试。
+- 真实 Session/文件/skill/命令组合保留完整带引号引用、原始词边界和同起点优先关系;空白候选使用完全相同的调度和缓存规则。
+
+## 备选方案
+
+**按顺序处理整篇文档的业务变换。** 某业务可能在另一业务更早的引用获得考虑前消费后面的文本,因此固定类型顺序不能表达最左优先识别。
+
+## 影响
+
+字段级失效仍可能要求某个匹配器重新遍历文档,不承诺 Facts 字段内部逐元素失效。查询历史的内存量与当前原文、定义下记录的不同搜索位置成正比。协调及 JSON 引用对齐可能遍历输出而不调用匹配器。性能证据是准确的调度次数和查询偏移,语义证据是刷新结果与全新计算器的等价性。本机制不需要浏览器端到端测试或耗时基准测试。
diff --git a/.agents/notes/implemented/architecture/2026-10-11-chat-group-tree-exchanges.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-11-chat-group-tree-exchanges.i18n.yaml
new file mode 100644
index 0000000000..b34903faef
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-11-chat-group-tree-exchanges.i18n.yaml
@@ -0,0 +1,25 @@
+# Bilingual-pair consistency record for 2026-10-11-chat-group-tree-exchanges.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-11-chat-group-tree-exchanges.md
+/agent-note-chat-group-tree-positions-and-exchanges:
+ en: b26212eda152d6c6
+ zh: a801a75ee0c07911
+/agent-note-chat-group-tree-positions-and-exchanges/problem:
+ en: 84858bc0e1e268ab
+ zh: 39ebb2979968d6cc
+/agent-note-chat-group-tree-positions-and-exchanges/decision:
+ en: ca3153ae96994baa
+ zh: de6c7d925559efbb
+/agent-note-chat-group-tree-positions-and-exchanges/why-mark-the-opening-rather-than-the-notice:
+ en: db8d77cd773ad646
+ zh: ab2f03b8fc58e952
+/agent-note-chat-group-tree-positions-and-exchanges/alternatives-considered:
+ en: fd9152c3f5edb035
+ zh: 259337cdcc5969a5
+/agent-note-chat-group-tree-positions-and-exchanges/consequences:
+ en: 2a98bb21bb1315d1
+ zh: be4a320e9b427a79
+/agent-note-chat-group-tree-positions-and-exchanges/testing:
+ en: e2e3982718e16ef4
+ zh: 4f3802946e51c55c
diff --git a/.agents/notes/implemented/architecture/2026-10-11-chat-group-tree-exchanges.md b/.agents/notes/implemented/architecture/2026-10-11-chat-group-tree-exchanges.md
new file mode 100644
index 0000000000..0fffb84e71
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-11-chat-group-tree-exchanges.md
@@ -0,0 +1,56 @@
+# Agent Note: Chat group-tree positions and exchanges
+
+Status: implemented
+
+English | [中文](2026-10-11-chat-group-tree-exchanges.zh.md)
+
+## Problem
+
+On next message folding kept only the last Turn of the loaded timeline expanded. A Turn woken by a notice (a settled subagent, goal round, schedule, or background job) is not submitted by this Client, yet its arrival made the preceding Turn historical and folded it. No own-input driver computed the exact remaining room for that fold, so the scroll layer reserved generic bottom space and a reader following the tail saw blank space. In the recorded Session the notice woke the next Turn 13–15 ms after the previous Turn ended; the minimal reproduction left 596 px blank that had not recovered after five seconds.
+
+## Decision
+
+A notice is neither body text nor a user message: folding and bottom advancement ignore it. The bottom changes from the last Turn to the latest exchange.
+
+- **Group-tree positions.** `ChatConversationViewNode.groupTree` declares a Node's position in the group tree, typed `ChatGroupTree = 'exchange' | 'input' | 'turn' | 'process'` and ordered outermost first; a Node at a position also has the effect of every inner position. `exchange` opens an exchange. `input` makes the Node an input of its Turn: before the Turn's process it is the opening input, after it an interleaved input that keeps the whole Turn open. `turn` keeps the Node outside the Turn's process disclosure, `process` keeps it inside the disclosure but outside step runs, and a Node without a position joins the surrounding step run. `isAtOrOutside(node, position)` is the single reader of that order: process grouping reads `process`, whole-Turn folding reads `turn`, Turn-input detection reads `input`, and exchanges read `exchange`.
+- **Exchanges.** A Turn holding an `exchange` Node opens an exchange, which lasts until the next opening Turn. The business message Definition marks only human input claimed in Step 1 (`user` or `steering`) as `exchange`; later human input and trigger notices stand at `input` and never open an exchange.
+- **Built-in positions.** `turn-error`, `turn-max-tokens`, `turn-tail`, and the `turn-process` control stand at `turn`; `model-retry` stands at `process`. Two placement rules stay with ui-chat's own `assistant-step` Node: its reasoning row joins the step run while its reply row stands outside it, and the Turn's process specification keeps the finalized answer's reply outside the disclosure because which step answers changes as later steps arrive.
+- **Mechanism.** `GroupTreeSegments(position)` splits the loaded Turns at the Turns holding a Node at that position. The latest segment starts at the last opening Turn, or at the first loaded Turn when the window holds none. It counts per Turn incrementally without scanning loaded Nodes, and a start move returns only the Turns that entered or left the latest segment. The Chat Builder's `bottomSource` is "the Node is visible and belongs to the latest exchange".
+
+As a result, a notice-woken Turn no longer folds earlier Turns, so no room is reserved; the human's next ordinary message folds the whole exchange through the own-input path.
+
+## Why mark the opening rather than the notice
+
+The host claims the waking input right after `turn/start`, assembles the system prompt, and only then logs `step/start` and `user/message`; the recorded Session spent 187 ms in that gap. Marking only the notice leaves the new Turn untagged during the gap, so the bottom still advances and the previous Turn folds and then reopens. Marking the human opening and letting unmarked Turns continue the exchange keeps the gap inert.
+
+## Alternatives considered
+
+**Tag the notice as a continuation.** The 187 ms gap above makes it fold and reopen, and a claim that mixes a notice with human input needs extra classification.
+
+**Name opening kinds in a list (`user`, `steering`).** A kind cannot separate Step 1 steering from mid-Turn steering, so a human steering into a notice-woken Turn would wrongly fold the previous exchange.
+
+**Publish Turn-level Location data.** Only one Context may publish a key for a Turn, while opening input comes from several message Contexts; aggregating per Turn would repeat the inbox claim classification in a new Definition and read every Turn on each update.
+
+**Declare a business role (`input`, `notice`) on Nodes.** That places business semantics in the grouping mechanism; group-tree positions describe structure only, and each business Definition chooses its position.
+
+**Correct the scroll room for folds not caused by own input.** Notices should not fold at all, and correcting the room would not remove that unnecessary fold. Folds caused by human input without an echo on this page are handled by [one fold motion for collapsing on the next message](2026-10-13-chat-single-fold-motion.md).
+
+**Read Turn inputs at `turn`.** Terminal errors, max-token notices, and the completed-turn footer also stand outside the process disclosure, so reading `turn` would treat them as opening input; Turn inputs need their own position.
+
+**Derive the exchange from an `input` in Step 1.** A trigger notice is its Turn's opening input but must not open an exchange. Deriving the exchange would drop trigger notices from `input` and change how notice-woken Turns order their process rows.
+
+## Consequences
+
+- Notice-woken Turns no longer cause a fold, reserved room, or blank space.
+- External Definitions place their Chat rows through `groupTree`. Apart from the two `assistant-step` rules, ui-chat names no kind when it groups, folds, or detects Turn inputs, and built-in kinds keep the placement their former kind lists gave them.
+- Under On next message, a long run of notice-woken Turns (up to 31 goal-mode Turns in sampled Sessions) stays expanded until the human types again.
+- When the loaded window holds no opening Turn, every loaded Turn belongs to the latest exchange.
+- Human input from another tab folds when its `user/message` arrives, about 200 ms later than the former `turn/start` fold; its landing follows [one fold motion for collapsing on the next message](2026-10-13-chat-single-fold-motion.md).
+
+## Testing
+
+- `packages/client/ui-chat/tests/group-tree-segments.client.spec.ts`: position order, segmentation, count fallback, and the empty window.
+- `packages/client/ui-chat/tests/chat-node-bottom.client.spec.ts`: latest-exchange membership and notification scope.
+- `packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts`: a real assembler across an opening Turn, mid-Turn steering, the pre-Step gap, a notice wake, and an idle steer, with non-opening inputs at `input`.
+- `packages/client/ui-chat/tests/process-groups.client.spec.ts`: a Node at any position splits the step run regardless of its kind, and a position change regroups.
+- `apps/web/tests/notice-wake-tail.e2e.ts`: after a long Turn a notice wakes the next Turn; the long Turn stays expanded through the pre-Step gap and after the notice reply, and a following reader sees the notice and the reply.
diff --git a/.agents/notes/implemented/architecture/2026-10-11-chat-group-tree-exchanges.zh.md b/.agents/notes/implemented/architecture/2026-10-11-chat-group-tree-exchanges.zh.md
new file mode 100644
index 0000000000..395faed58b
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-11-chat-group-tree-exchanges.zh.md
@@ -0,0 +1,56 @@
+# Agent Note: Chat 分组树位置与交互段
+
+Status: implemented
+
+[English](2026-10-11-chat-group-tree-exchanges.md) | 中文
+
+## Problem
+
+「下次发送时收起」原本只让已加载时间线的最后一个 Turn 保持展开。由通知唤醒的 Turn(子 agent 完成、Goal Round、定时任务、后台作业)不是本客户端提交的,它一出现,上一轮就被当作历史折叠。这次折叠没有自己输入的驱动来计算精确的剩余补白,滚动层只能按通用规则预留底部空间,正在跟尾的读者因此看到一片空白。录制的真实 Session 中,通知在上一轮结束后 13–15 ms 唤醒新轮次;最小复现里空白高 596 px,五秒后仍未恢复。
+
+## Decision
+
+通知不算正文,也不算用户消息:折叠和推进底部时都忽略它。底部从"最后一个 Turn"改为"最新交互段"。
+
+- **分组树位置。** `ChatConversationViewNode.groupTree` 声明节点在分组树中的位置,类型为 `ChatGroupTree = 'exchange' | 'input' | 'turn' | 'process'`,由外到内有序;站在某个位置的节点同时具有所有内层位置的效果。`exchange` 开启一个交互段。`input` 表示节点是所在 Turn 的输入:出现在该轮过程之前即为开场输入,之后出现即为中途输入,使整个 Turn 保持展开。`turn` 让节点留在 Turn 的整轮折叠之外,`process` 让节点留在整轮折叠之内、步骤组之外,没有位置的节点并入相邻的步骤组。`isAtOrOutside(node, position)` 是读取这一顺序的唯一入口:过程分组读 `process`,整轮折叠读 `turn`,轮次输入判断读 `input`,交互段读 `exchange`。
+- **交互段。** 含 `exchange` 节点的 Turn 开启一个交互段,交互段持续到下一个开启 Turn。业务层的 message Definition 只给 Step 1 中被认领的真人输入(`user` 或 `steering`)标 `exchange`;之后的真人输入和触发通知位于 `input`,不开启交互段。
+- **内置位置。** `turn-error`、`turn-max-tokens`、`turn-tail` 与 `turn-process` 控件位于 `turn`;`model-retry` 位于 `process`。ui-chat 自己的 `assistant-step` 节点保留两条放置规则:推理行并入步骤组、回复行在步骤组之外;该轮的过程规格让最终答案的回复留在整轮折叠之外,因为哪一步是最终答案会随后续步骤到来而变化。
+- **机制。** `GroupTreeSegments(position)` 按含该位置节点的 Turn 切分已加载的 Turn,最新一段从最后一个开启 Turn 开始,窗口中没有开启 Turn 时从第一个已加载 Turn 开始。它按 Turn 增量计数,不扫描已加载节点;起点移动时只返回进入或离开最新一段的 Turn。Chat Builder 的 `bottomSource` 为"节点可见且属于最新交互段"。
+
+结果是:通知唤醒的 Turn 不再折叠前面的 Turn,没有折叠就没有补白;真人下一次普通输入时,整个交互段经由自己输入的路径一起折叠。
+
+## 为什么标开头而不是标通知
+
+Host 在 `turn/start` 之后立即认领唤醒输入,然后组装系统提示,再写 `step/start` 和 `user/message`。录制的 Session 中这段间隔为 187 ms。只给通知打标的话,这段时间新 Turn 还没有标签,底部照样推进,上一轮会先折叠再展开。把标签放在真人开头上、未标的 Turn 默认续接,空白间隔里什么都不会发生。
+
+## Alternatives considered
+
+**给通知打"续接"标签。** 上面那 187 ms 的间隔会让它先折后展;同一次认领里通知和真人输入并存时还要额外判断。
+
+**按 kind 写一张开启名单(`user`、`steering`)。** kind 区分不了 Step 1 的 steering 与 Turn 中途的 steering,真人往通知唤醒的 Turn 里插话时会错误地折叠前一个交互段。
+
+**发布到 Turn 级 Location data。** 同一 Turn 的同一个 key 只允许一个 Context 发布,而开启输入来自多个消息 Context;改成按 Turn 汇总就得在新的 Definition 里重复 inbox 认领分类,并且每次都要读取所有 Turn。
+
+**节点声明业务角色(`input`、`notice`)。** 这会把业务语义放进分组机制;分组树位置只描述结构,业务 Definition 自行决定位置。
+
+**修正非自身输入折叠时的滚动补白。** 通知本来就不应触发折叠,修补白不能消除这次不必要的折叠。本页没有回显的真人输入引起的折叠,由[「下条消息时收起」只用一条折叠动画](2026-10-13-chat-single-fold-motion.zh.md)处理。
+
+**在 `turn` 位置上判断轮次输入。** 终止错误、最大 token 提示和已完成轮次页脚同样在整轮折叠之外,读 `turn` 会把它们误当作开场输入;轮次输入需要自己的位置。
+
+**由 Step 1 中的 `input` 推导交互段。** 触发通知是所在 Turn 的开场输入,却不能开启交互段。推导交互段就得把触发通知移出 `input`,从而改变通知唤醒的 Turn 中过程行的排列。
+
+## Consequences
+
+- 通知唤醒的 Turn 不再产生折叠、补白和空白。
+- 外部 Definition 通过 `groupTree` 放置自己的 Chat 行。除 `assistant-step` 的两条规则外,ui-chat 在分组、折叠和判断轮次输入时不点名任何 kind,内置 kind 保持原 kind 名单给出的放置。
+- 「下次发送时收起」下,一长串通知唤醒的 Turn(抽样会话中 Goal 模式最长 31 轮)会一直展开,直到真人再次输入。
+- 已加载窗口里没有开启 Turn 时,所有已加载 Turn 都属于最新交互段。
+- 另一个标签页里的真人输入在它的 `user/message` 到达时才折叠,比原来在 `turn/start` 时晚约 200 ms;其落点按[「下条消息时收起」只用一条折叠动画](2026-10-13-chat-single-fold-motion.zh.md)决定。
+
+## Testing
+
+- `packages/client/ui-chat/tests/group-tree-segments.client.spec.ts`:位置顺序、切分、计数回退与空窗口。
+- `packages/client/ui-chat/tests/chat-node-bottom.client.spec.ts`:最新交互段的成员与通知范围。
+- `packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts`:真实 assembler 下的开轮、中途 steering、Step 1 前的间隔、通知唤醒与空闲 steer,非开启输入位于 `input`。
+- `packages/client/ui-chat/tests/process-groups.client.spec.ts`:任意位置上的节点不论 kind 都会切开步骤组,位置变化会触发重新分组。
+- `apps/web/tests/notice-wake-tail.e2e.ts`:长 Turn 之后由通知唤醒,在 Step 1 前的间隔和通知回复之后,上一轮都保持展开,跟尾读者看到通知与回复。
diff --git a/.agents/notes/implemented/architecture/2026-10-12-exclusive-conversation-matches.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-12-exclusive-conversation-matches.i18n.yaml
new file mode 100644
index 0000000000..e1f4c7c1e9
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-12-exclusive-conversation-matches.i18n.yaml
@@ -0,0 +1,22 @@
+# Bilingual-pair consistency record for 2026-10-12-exclusive-conversation-matches.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-12-exclusive-conversation-matches.md
+/agent-note-exclusive-conversation-matches:
+ en: 6f78605cc85cf7f2
+ zh: f73e26bb3f9d1896
+/agent-note-exclusive-conversation-matches/problem:
+ en: 38ed7541d4cb7019
+ zh: 98ed08f8c0c7c5ba
+/agent-note-exclusive-conversation-matches/decision:
+ en: f2bfa3d0cc6b429a
+ zh: c1dd501c79086d71
+/agent-note-exclusive-conversation-matches/alternatives-considered:
+ en: efd209a2c0e97381
+ zh: 8b6f10ff4de49449
+/agent-note-exclusive-conversation-matches/consequences:
+ en: 414ae7349bd4cc03
+ zh: 9861b3e72bdfc548
+/agent-note-exclusive-conversation-matches/testing:
+ en: a88ed18065da4d28
+ zh: 5bf4448a8b1f4763
diff --git a/.agents/notes/implemented/architecture/2026-10-12-exclusive-conversation-matches.md b/.agents/notes/implemented/architecture/2026-10-12-exclusive-conversation-matches.md
new file mode 100644
index 0000000000..5dc353f598
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-12-exclusive-conversation-matches.md
@@ -0,0 +1,52 @@
+# Agent Note: Exclusive Conversation matches
+
+Status: implemented
+
+English | [中文](2026-10-12-exclusive-conversation-matches.zh.md)
+
+## Problem
+
+Every event Definition matches each event independently, and a target may hold a generic Definition that presents a whole event family. When a business Definition presents one kind of that family itself, both Definitions match the same event and each builds a Context and a Node. Nothing let the business Definition say that the event belongs to it on that target.
+
+The late question reply is the shipped case. Chat's generic `input-message` Definition matches every appended `user/message`, and the questions UI's `question-reply` Definition matches the reply message it presents. Chat process grouping therefore dropped the duplicate `turn-trigger` presentation by message id, in both the Turn-owned and unscoped paths, and anchored the Turn control to the first group member so it stayed above the reply. The duplicate Node remained in the Node Store, and the generic Definition kept treating the reply as context or a trigger notice.
+
+## Decision
+
+A match result may claim its event on the Definition's target:
+
+```ts
+interface ConversationMatchResult {
+ readonly id: string
+ readonly role: 'start' | 'update'
+ readonly exclusive?: true
+}
+```
+
+- **Dispatch.** `ConversationNodeAssembler.dispatchInput()` evaluates every match of one event into two reused arrays before accepting any, then settles claims. Other Definitions of a claimed target build no Context for the event, and that target's unmatched-event fallback does not run. Definitions of other targets and Definitions without a target still receive it. The outcome is independent of registration order. Live append and window collection share this dispatch.
+- **Failures.** Two claims on one target, or a claim from a Definition without a target, throw during dispatch and name the event, its `seq`, and the claiming Definition kinds.
+- **Scope.** A claim belongs to one event and comes from `match()`, so a Definition claims only the events it recognizes. The generic Definition declares nothing.
+- **Current use.** `question-reply` returns `exclusive: true` for a recorded `user-question-reply` message. Chat no longer aggregates by message id, and the reply's placement is unchanged.
+
+## Alternatives considered
+
+**Definition precedence (`specific` / `default`).** A generic Definition would declare itself overridable, and dispatch would become a priority order rather than a statement of which Definition presents an event. A claim needs no change to the generic Definition, and two claims fail instead of silently ranking.
+
+**Declare human sources in a registry and render the reply through user-text rules.** The reply would stay a generic message Node with customized text. Chat's message and Inbox Definitions would need a service-backed predicate, registry changes would have to reassemble every bound Conversation, and the text rule would need the message source as a fact. A claim keeps the reply in its owning Definition.
+
+**Mark human-authored sources in durable data.** A generic field on the message source would let the generic Definition classify the reply, but it changes a persisted type, and replies recorded by released builds do not carry it.
+
+**Skip the reply kind in `input-message`.** The generic Chat Definition would name a plugin's source kind.
+
+**Deduplicate in the grouping output.** This is the replaced mechanism: the duplicate Node stays stored and visible to every other Chat reader, and each grouping path repeats the aggregation.
+
+## Consequences
+
+- A claimed event has exactly one Context per claimed target, so no hidden duplicate remains in the Node Store.
+- A conflicting claim fails at dispatch rather than at registration, because claims are decided per event from event data.
+- Dispatch makes one extra pass over an event's matches, usually one to three, and allocates nothing for events without a claim.
+- A plugin that presents part of a generic Definition's event family on one target uses a claim; it does not need the generic Definition's cooperation.
+
+## Testing
+
+- `packages/client/ui-conversation/tests/conversation-assembler.client.spec.ts` ("exclusive matches"): a claim withholds the event from other Definitions of its target in either registration order and on live append; other targets, target-less Definitions, and no fallback; two claims and a target-less claim throw.
+- `packages/client/ui-user-questions/tests/question-reply.client.spec.ts`: the reply match claims its event, and an assembler holding both message Definitions produces one Chat Node for the reply.
diff --git a/.agents/notes/implemented/architecture/2026-10-12-exclusive-conversation-matches.zh.md b/.agents/notes/implemented/architecture/2026-10-12-exclusive-conversation-matches.zh.md
new file mode 100644
index 0000000000..021b1cc918
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-12-exclusive-conversation-matches.zh.md
@@ -0,0 +1,52 @@
+# Agent Note: Conversation 独占匹配
+
+Status: implemented
+
+[English](2026-10-12-exclusive-conversation-matches.md) | 中文
+
+## Problem
+
+每个事件 Definition 各自独立地匹配每个事件,同一个 target 上可能有一个呈现整类事件的通用 Definition。业务 Definition 自己呈现这类事件中的一种时,两个 Definition 会命中同一个事件,各自建立 Context 和 Node。业务 Definition 没有办法声明这个事件在该 target 上归它呈现。
+
+迟到的问题回答是已上线的实例。Chat 的通用 `input-message` Definition 匹配每条追加的 `user/message`,问题 UI 的 `question-reply` Definition 也匹配它要呈现的回答消息。Chat 过程分组因此在有轮次归属和无轮次归属两条路径上都按消息 id 丢掉重复的 `turn-trigger` 呈现,并把整轮控件锚到组首成员,让它留在回答之上。重复的 Node 仍留在 Node Store 中,通用 Definition 也仍把回答当作 context 或触发通知。
+
+## Decision
+
+match 结果可以在 Definition 的 target 上认领它的事件:
+
+```ts
+interface ConversationMatchResult {
+ readonly id: string
+ readonly role: 'start' | 'update'
+ readonly exclusive?: true
+}
+```
+
+- **派发。** `ConversationNodeAssembler.dispatchInput()` 先把一个事件的全部 match 求入两个复用数组,再结算认领,之后才逐个接收。被认领 target 上的其他 Definition 不为该事件建立 Context,该 target 的未匹配事件 fallback 也不运行;其他 target 的 Definition 与没有 target 的 Definition 照常接收。结果与注册顺序无关。实时追加和窗口收集共用这一派发。
+- **失败。** 同一 target 被认领两次,或没有 target 的 Definition 发起认领,都会在派发时抛错,错误信息包含事件、其 `seq` 与认领方 Definition 的 kind。
+- **范围。** 认领只对一个事件生效,由 `match()` 返回,因此 Definition 只认领它识别出的事件;通用 Definition 不需要声明任何东西。
+- **当前用法。** `question-reply` 对已记录的 `user-question-reply` 消息返回 `exclusive: true`。Chat 不再按消息 id 聚合,回答的放置不变。
+
+## Alternatives considered
+
+**Definition 优先级(`specific` / `default`)。** 通用 Definition 得声明自己可被覆盖,派发也变成优先级排序,而不是声明由哪个 Definition 呈现事件。认领不要求改动通用 Definition,两个认领冲突时直接失败,而不是静默排序。
+
+**用注册表声明真人来源,并通过用户文本规则渲染回答。** 回答会保持为通用消息 Node,只定制文本。Chat 的 message 与 Inbox Definition 需要一个依赖 service 的判断函数,注册表变化必须重新组装所有已绑定的 Conversation,文本规则还要把消息来源当作 fact 读取。认领让回答留在它自己的 Definition 中。
+
+**在持久数据中标记真人撰写的来源。** 消息来源上的通用字段可以让通用 Definition 自行分类回答,但这会改动持久化类型,已发布版本记录的回答也不带这个字段。
+
+**在 `input-message` 中跳过回答的 kind。** 通用的 Chat Definition 会点名插件的来源 kind。
+
+**在分组输出层去重。** 这是被替换的机制:重复 Node 仍留在存储里,对其他所有 Chat 读取方可见,每条分组路径都要重复这一聚合。
+
+## Consequences
+
+- 被认领的事件在每个被认领 target 上恰好只有一个 Context,Node Store 中不再残留隐藏的重复节点。
+- 认领冲突在派发时失败,而不是在注册时失败,因为认领由事件数据逐个决定。
+- 派发对每个事件的 match 多遍历一次,通常一到三个;没有认领的事件不产生额外分配。
+- 插件在一个 target 上呈现通用 Definition 事件族中的一部分时,使用认领即可,不需要通用 Definition 配合。
+
+## Testing
+
+- `packages/client/ui-conversation/tests/conversation-assembler.client.spec.ts`("exclusive matches"):两种注册顺序与实时追加下,认领都会对同 target 的其他 Definition 屏蔽该事件;其他 target 与无 target 的 Definition 照常接收,fallback 不运行;两次认领与无 target 认领会抛错。
+- `packages/client/ui-user-questions/tests/question-reply.client.spec.ts`:回答的 match 认领其事件;同时持有两个消息 Definition 的 assembler 只为回答产生一个 Chat Node。
diff --git a/.agents/notes/implemented/architecture/2026-10-12-headless-environment-auth.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-12-headless-environment-auth.i18n.yaml
new file mode 100644
index 0000000000..df5212392d
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-12-headless-environment-auth.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-10-12-headless-environment-auth.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-12-headless-environment-auth.md
+/agent-note-headless-jobs-omit-the-interactive-credential-store:
+ en: c768db5a7e821bf0
+ zh: 423b3e423d071ddc
+/agent-note-headless-jobs-omit-the-interactive-credential-store/problem:
+ en: 829df36d87975148
+ zh: 171d29ef2f2e5079
+/agent-note-headless-jobs-omit-the-interactive-credential-store/decision:
+ en: af275ffea713f76b
+ zh: 12a3bacfcb147df7
+/agent-note-headless-jobs-omit-the-interactive-credential-store/alternatives-considered:
+ en: f6f8dfde4789adc8
+ zh: ced62f3d1ec402e6
+/agent-note-headless-jobs-omit-the-interactive-credential-store/consequences:
+ en: 1c1a699dc9f5681f
+ zh: 4a72b09d24f9a005
diff --git a/.agents/notes/implemented/architecture/2026-10-12-headless-environment-auth.md b/.agents/notes/implemented/architecture/2026-10-12-headless-environment-auth.md
new file mode 100644
index 0000000000..0a497c44e7
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-12-headless-environment-auth.md
@@ -0,0 +1,30 @@
+# Agent Note: Headless jobs omit the interactive credential store
+
+Status: implemented
+
+English | [中文](2026-10-12-headless-environment-auth.zh.md)
+
+## Problem
+
+The [stored credential precedence](2026-10-09-stored-credential-precedence.md) decision makes GUI saves effective even when the shell exports a key for another application. A headless job sharing that Harness home can then use the GUI account instead of the key explicitly supplied to the job. Changing reference precedence alone would leave pi-ai provider records and saved grants ahead of the environment.
+
+## Decision
+
+The shipped headless bundle disables the base `credentials`, `authorization` and `deepseek-account` rows. API-key adapters already support compositions without the credentials service and resolve through the launch snapshot: process environment, invocation-directory `.env`, then Harness-home `.env`. The profile does not read, migrate, write or delete `.credentials.yaml`. Dependent authorization and account rows are disabled explicitly so ordinary jobs do not emit pending-service warnings.
+
+This partially supersedes the per-run override policy in stored credential precedence for the shipped headless profile. Other profiles and an explicitly restored headless store retain stored-first resolution. The headless [README](../../../../packages/bundle/headless/README.md#credentials) owns the restoration patch.
+
+## Alternatives considered
+
+**Put the environment first in every profile.** It prevents a GUI save from replacing a key left in the launching environment and restores inconsistent reference and provider-record precedence.
+
+**Disable only the credentials row.** It achieves environment selection but leaves authorization and the account provider pending, producing warnings on every launch.
+
+**Add a second precedence mode to the store.** It introduces configuration and record-handling policy into the shared provider when existing adapter fallbacks and profile overrides already supply the required behavior.
+
+## Consequences
+
+- Headless ignores saved API keys, provider records and grants. A provider requiring stored sign-in or the DeepSeek account route fails until the user restores stored authentication; restoring it also restores saved-first precedence.
+- Environment and external provider credential chains remain available. Sessions, settings and tools stay composed; omission of the store does not deny filesystem reads by tools.
+- Web, SDK and ACP retain the credential store. Their authentication, login and refresh implementations are unchanged.
+- Profile-level process tests cover all three environment layers, named and native pi-ai authentication, missing keys, ignored malformed storage, restoration and preservation of the document. Existing headless transcripts and real-model workspace tests cover the remaining composition.
diff --git a/.agents/notes/implemented/architecture/2026-10-12-headless-environment-auth.zh.md b/.agents/notes/implemented/architecture/2026-10-12-headless-environment-auth.zh.md
new file mode 100644
index 0000000000..e6e0b8b870
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-12-headless-environment-auth.zh.md
@@ -0,0 +1,30 @@
+# Agent Note: Headless 任务不挂载交互式凭据存储
+
+Status: implemented
+
+[English](2026-10-12-headless-environment-auth.md) | 中文
+
+## 问题
+
+[已保存凭据优先级](2026-10-09-stored-credential-precedence.zh.md)的决策保证 GUI 保存的密钥生效,即使 shell 为其他应用导出了密钥。共享同一个 Harness home 的 headless 任务可能因此使用 GUI 的账户,而不是显式传给任务的密钥。只调整引用优先级仍会让 pi-ai 提供方记录和已保存 grant 排在环境变量之前。
+
+## 决策
+
+随附 headless 组合包禁用 base 中的 `credentials`、`authorization` 和 `deepseek-account` 行。API 密钥适配器已经支持不挂载凭据服务的组合,按启动快照解析:进程环境、运行命令的目录 `.env`、Harness home `.env`。该 profile 不读取、迁移、写入或删除 `.credentials.yaml`。显式禁用依赖它的 authorization 和 account 行,避免普通任务输出等待服务的警告。
+
+这部分取代了已保存凭据优先级中针对随附 headless profile 的单次覆盖策略。其他 profile,以及显式恢复存储的 headless,仍按已保存凭据优先解析。headless [README](../../../../packages/bundle/headless/README.zh.md#credentials)负责恢复 patch 的用法。
+
+## 考虑过的替代方案
+
+**在所有 profile 中让环境变量优先。** 这会使 GUI 保存的密钥无法替换启动环境中遗留的密钥,并恢复引用与提供方记录之间不一致的优先级。
+
+**只禁用 credentials 行。** 这能选用环境变量,但 authorization 与 account 提供方会保持 pending,每次启动都产生警告。
+
+**给存储增加第二种优先级模式。** 现有适配器后备与 profile 覆盖已经满足需求;这种做法却会把额外配置和记录处理策略引入共享提供方。
+
+## 后果
+
+- Headless 忽略已保存 API 密钥、提供方记录和 grant。依赖已保存登录凭据的提供方或 DeepSeek account 路由会失败,直到用户恢复存储认证;恢复后也恢复已保存凭据优先。
+- 环境变量与提供方外部凭据链仍然可用。会话、settings 与工具继续组合;不挂载存储不会禁止工具读取文件系统。
+- Web、SDK 与 ACP 保留凭据存储;其认证、登录和刷新实现不变。
+- Profile 进程测试覆盖三个环境层、具名与原生 pi-ai 认证、缺失密钥、忽略畸形存储、恢复存储,以及文件保持原样。现有 headless 录制与真实模型工作区测试覆盖组合中的其余行为。
diff --git a/.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.i18n.yaml
new file mode 100644
index 0000000000..fbf95020c0
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.i18n.yaml
@@ -0,0 +1,91 @@
+# Bilingual-pair consistency record for 2026-10-12-peer-principal-and-remote-access.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.md
+/agent-note-peer-principals-and-method-level-remote-access-control:
+ en: 0c0fd0f567bd5c89
+ zh: 464e34d05737a7bc
+/agent-note-peer-principals-and-method-level-remote-access-control/problem:
+ en: f5b70ee2c0a3e32a
+ zh: 68776a916cb41f11
+/agent-note-peer-principals-and-method-level-remote-access-control/decision:
+ en: a15c7c13f167e5ac
+ zh: 63bcc2ef60c2f344
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/scope:
+ en: 86061e5578d06871
+ zh: 08eb605fcd38329d
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/two-layers-principal-and-peer:
+ en: 6663ea33561eee82
+ zh: 722fd3b8fae0ef90
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/peer-model:
+ en: d9ac7c94e40c2494
+ zh: fa6f5e6690b2fb90
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/identity-types:
+ en: 31430c5b95295ead
+ zh: 8f4ba35b4406558d
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/peer-lifetime-and-revocation:
+ en: a01ac57fc71a84f0
+ zh: 3e488ef3da5e4b8c
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/method-level-access-control:
+ en: a55516339b0d49dd
+ zh: 3281df9ea7878fc0
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/method-level-access-control/correspondence-with-rbac:
+ en: a78a2410eb52f777
+ zh: a868bf1b0e690c9f
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/method-level-access-control/resources-and-resource-readers-deepseek-ai-dsh-typert-protocol-deepseek-ai-dsh-typert-registry:
+ en: 625b20552962e268
+ zh: 1136c0fd7cdc6901
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/method-level-access-control/endpoint-resources:
+ en: ec1677d8e338e115
+ zh: 6bedd163f208eaa9
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/method-level-access-control/roles-bindings-and-policies-deepseek-ai-dsh-api-remote-access:
+ en: e57f396239ea9f56
+ zh: ddb26278b1521ceb
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/method-level-access-control/example-share-viewer:
+ en: 3c911f57f0f6ca4b
+ zh: 1509781eeda4c922
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/method-level-access-control/host-access-check-order:
+ en: 6c58dc8777e107ef
+ zh: bc14605c96e4d8f3
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/method-level-access-control/forwarded-events:
+ en: 5fe34ee5ec31942a
+ zh: ef39b020b0539b8f
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/method-level-access-control/list-and-aggregate-methods:
+ en: 7865e159528e2d30
+ zh: 1399d54632f4d926
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/method-level-access-control/access-snapshot-stream:
+ en: 41d7ac1faf2448d7
+ zh: 6d523895a9abae68
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/method-level-access-control/client-face:
+ en: 7be28cd7e19e5377
+ zh: 30b5aee957271d6c
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/security-invariants:
+ en: 2094587c320b6ccc
+ zh: 3a1d2a6b343500f4
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/recording-the-message-source:
+ en: 2ee3abed24d418f4
+ zh: 25653dd24b95c220
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/code-review-rule-and-documentation:
+ en: e9a2e0d1534b8fe7
+ zh: 085813c0b2b3c0d8
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/out-of-scope-and-future-work:
+ en: a84e07716719e3b5
+ zh: 9649ccb4a035a1df
+/agent-note-peer-principals-and-method-level-remote-access-control/decision/relation-to-existing-notes:
+ en: 305ec45a7cd85ed7
+ zh: d2cd4209a56b1d0c
+/agent-note-peer-principals-and-method-level-remote-access-control/alternatives-considered:
+ en: 130df2981ba550df
+ zh: e9a0a89af6fb50a3
+/agent-note-peer-principals-and-method-level-remote-access-control/consequences:
+ en: 89452c3c3f042e75
+ zh: 9bcad00a60c65303
+/agent-note-peer-principals-and-method-level-remote-access-control/consequences/verification:
+ en: 048067eb02e6bf8a
+ zh: 12508b59f6b206b8
+/agent-note-peer-principals-and-method-level-remote-access-control/consequences/costs-and-limits:
+ en: 1e254cec680b1a37
+ zh: 06a31e057ba965aa
+/agent-note-peer-principals-and-method-level-remote-access-control/consequences/migration-list-for-pr-5945:
+ en: f904c62b68d46bb5
+ zh: 34eb45b97302badb
diff --git a/.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.md b/.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.md
new file mode 100644
index 0000000000..324606f3ab
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.md
@@ -0,0 +1,675 @@
+# Agent Note: Peer principals and method-level Remote access control
+
+Status: implemented
+
+English | [中文](2026-10-12-peer-principal-and-remote-access.zh.md)
+
+## Problem
+
+A Remote method on the Host could not tell whom one call spoke for, and could not restrict callable methods by caller. The [`PeerScope`](../../../../packages/typert/protocol/src/types.ts) contract stated "Who the Peer is and what it may do are not recorded here"; [Connection](../../../../packages/client/connection/src/rpc-host.ts) had a single operator Peer, `admit()` returned it for every authenticated request, and the `/api` route and the interceptor's Fetch handler used it unconditionally instead of the admission result; [Gateway](../../../../packages/api/gateway/src/index.ts) silently fell back to `operatorPeer()` when a caller named no Peer.
+
+The first non-owner caller is a liveshare viewer. Viewer requests reach the owner's liveshare Host plugin through the Relay, and the plugin dispatched them in the owner's process as the operator, so the owner's Session treated a viewer's prompt as the owner's own input. PR #3854 pushed liveshare semantics into session-controller for this: the request body carries `origin`, every prompt writes a required `prompt/origin` event correlated by requestId, the controller folds origins, `serializeAdmission` extends to every prompt, and a new `api-session/external-admission` hook appears; liveshare maintains its own `READ_ENDPOINTS` allowlist and trims list/follow results; the frontend `clientWritePolicy` lives in ui-conversation, with three ways of reading it across five packages and seven places that hide UI by whether a callback was injected.
+
+One general mechanism is needed:
+
+- In-process Host code assigns the caller identity; no wire field can set or override it.
+- Every call path names its Peer explicitly; there is no implicit fallback.
+- Method-level permission is computed and enforced in one place on the Host; the frontend reads the same result to show or hide entries and follows rule changes automatically.
+- A prompt records its caller with the user message, so a later check can tell who initiated each turn.
+
+## Decision
+
+### Scope
+
+| Part | Content |
+|---|---|
+| Peer identity | `PeerScope.principal` and `PeerScope.signal`; one Peer implementation `Peer` (renamed from `OperatorPeer`) with an options constructor; removal of the operator concept and every implicit Peer fallback; `connection.browserPeer`, `connection.createPeer()`, and `fetch(request, peer)`; a disposed Peer is always refused with `gateway/peer-disposed`; the `user-rpc` source records `principal?` |
+| Method-level access control | `resources` readers on `@Remote`, `TypertResourceMap` resource kinds, and the `typert.resources` registry (typert-protocol, typert-registry); Gateway resolution of calls and forwarded events to resources; new package `@deepseek-ai/dsh-api-remote-access` (Host and Client faces): RBAC decisions over roles, bindings, and policies through `decide(request)`, list filtering through `allows()`, the first snapshot in the index and the access snapshot stream, and access hooks |
+| Documentation | One code-review rule and terminology |
+
+### Two layers: principal and peer
+
+| Layer | Concept | Example | Cardinality |
+|---|---|---|---|
+| Authorization | principal: the caller identity that permission decisions read | `share-viewer{shareId, actor}` proven by a share token; the local user `{ kind: 'owner' }` | one principal has N peers |
+| Connection | peer: the lifetime scope of one connection | a browser connection, one connection of a liveshare viewer | one peer per connection |
+
+| Concern | Layer |
+|---|---|
+| Method-level decisions, access hooks, access snapshots | principal |
+| Recording `source.principal` | principal |
+| Subscriptions, streams, connection-lifetime registrations (`peer.ctx`) | peer |
+| Revocation | both: after revocation the policy gives that principal no binding, so its calls are all refused; the creator disposes every peer under it |
+
+The creator owns the principal→peers table; liveshare, for example, keeps one object per share grant that records every peer opened under it.
+
+### Peer model
+
+There is one Peer concept and one implementation; the operator concept is removed.
+
+```text
+// @deepseek-ai/dsh-typert-protocol
+export interface PeerScope {
+ readonly id: PeerId
+ /** Connection-lifetime registrations; disposed with the Peer. */
+ readonly ctx: Context
+ /** Caller identity fixed at creation by in-process Host code; no wire field can set it. */
+ readonly principal: Principal
+ /** Aborted once the Peer is disposed; Remote streams that speak for the Peer end with it. */
+ readonly signal: AbortSignal
+ /** Forwarded human-interaction participation; independent of authorization. */
+ readonly interactions: 'answer' | 'none'
+ dispose(): Promise
+}
+
+// @deepseek-ai/dsh-client-connection
+export class Peer implements PeerScope {
+ constructor(owner: Context, options: { readonly principal: Principal; readonly interactions?: 'answer' | 'none' })
+}
+
+interface HostConnectionHandle {
+ /** The Peer every authenticated browser connection shares; principal { kind: 'owner' }. */
+ readonly browserPeer: PeerScope
+ /** Network admission: the admitted Peer or the refusal status. */
+ admit(request: ConnectionTrustRequest): PeerAdmission
+ /** In-process only; disposed with the Context that read `ctx.connection`, or earlier by dispose(). */
+ createPeer(options: { readonly principal: Principal; readonly interactions?: 'answer' | 'none' }): PeerScope
+}
+
+interface ConnectionFetchHandler {
+ fetch(request: Request, peer: PeerScope): Promise
+}
+bridge(req, res, apiHandler, peer, maxRequestBodyBytes?)
+
+// @deepseek-ai/dsh-api-gateway
+interface InvokeRemoteRequest { …; readonly peer: PeerScope } // was `peer?`
+wireStream.open(endpoint, payload, uplink, peer: PeerScope, signal) // was `PeerScope | undefined`
+```
+
+| Rule | Detail |
+|---|---|
+| Every path names its Peer | Network paths take it from `admit()`; in-process paths receive it from the caller. Gateway deletes `operatorPeer()` and `request.peer ?? …` and never creates a Peer |
+| Browser connections share `browserPeer` | Every browser connection shares one Peer, and every use site names `connection.browserPeer` explicitly; there is no fallback variable. The in-page Host (webworker-runtime) also passes `browserPeer` explicitly |
+| In-process Host automation | Code that calls Remote methods creates its own Peer with `createPeer({ principal })`. No production caller exists yet, so no fixed Host Peer is pre-provisioned |
+| Multiple Peers | Each `createPeer()` returns an independent Peer; liveshare is to create one per viewer connection (wired on the #3854 side) |
+| Interaction participation | `Peer(owner, { principal, interactions? })` and `createPeer()` default omitted `interactions` to `answer`. `none` refuses `$events` and `$events/result` for every principal, even owner; this is independent of method-level RBAC. Answering owner Peers receive waterfalls, and non-owner Peers receive only RBAC-authorized broadcasts and cannot answer |
+| Exact Fetch routes | Exact routes such as file-upload, present-open, media-references, session-log-export, and inspector bypass the Gateway; the shared Fetch handler answers 403 to every non-owner Peer |
+
+### Identity types
+
+```text
+// @deepseek-ai/dsh-typert-protocol
+
+/**
+ * Merge-extensible caller identities. Each driver declares its own entry.
+ * Every entry is a JSON value with a unique literal `kind`, carries no
+ * credential, and is safe to persist in a Session log.
+ */
+export interface PrincipalMap {
+ owner: { readonly kind: 'owner' }
+}
+export type Principal = PrincipalMap[keyof PrincipalMap]
+
+/**
+ * @param invocation - `ctx.invocation`; `undefined` outside a Remote call.
+ * @returns the caller's principal when it is not the owner; `undefined` for the
+ * owner and for trusted in-process code outside a Remote call.
+ */
+export function principalOf(invocation: RemoteInvocation | undefined): Principal | undefined
+```
+
+The single read point is `this.ctx.invocation.peer.principal`; an owner test is written inline as `principal.kind === 'owner'`, and an unknown kind is never the owner. External drivers extend `PrincipalMap` by declaration merging in their own package; liveshare's `share-viewer{ shareId, actor }` is declared on the #3854 side, and liveshare itself is unchanged here.
+
+### Peer lifetime and revocation
+
+| Event | Behavior |
+|---|---|
+| Gateway dispatch | The first step checks `peer.signal.aborted` and refuses a disposed Peer with `gateway/peer-disposed`, on HTTP, WebSocket, and in-process paths alike; wire paths check before they read the payload, so a malformed payload from a disposed Peer gets the same code |
+| Streams when a Peer is disposed | The signal a Remote stream method observes joins `peer.signal`, and so does the `$events` stream lifetime; a WebSocket still closes with 1001 |
+| Unary calls when a Peer is disposed | A unary call already running is not cancelled; later calls are refused |
+| Revocation | After a business package revokes a grant, its policy gives that principal no binding, and the business package calls `invalidate(principal)`; the creator disposes every peer in its principal→peers table |
+
+### Method-level access control
+
+Resource types and the `resources` readers of `@Remote` live in `@deepseek-ai/dsh-typert-protocol`; resource kinds live in typert beside lookups and Contexts: `TypertResourceMap` declares the kinds, `typert.resources` holds each kind's default wire field, and lookups and Host Context adapters declare the resource kind their wire identity names. The Gateway resolves one call or forwarded event to the resources it touches. Decisions, list filtering, and the access snapshot live in the new package `@deepseek-ai/dsh-api-remote-access` (`packages/api/remote-access`), which reads no argument field. The Gateway and domain packages depend on remote-access only for types and inject `remoteAccess`; their only value dependency is `dsh-typert-protocol`, so `package-dependency-policy` gains no remote-access entry. Business packages define and register roles, bindings, and policies; liveshare is the first user. Apart from the built-in `global`, no resource-kind name appears in remote-access or the Gateway, which follows the [`packages/api` carries generic orchestration only](../../../../docs/defensive-patterns/remote-and-api.md) review rule.
+
+#### Correspondence with RBAC
+
+| This design | RBAC / IAM term | Meaning |
+|---|---|---|
+| principal | Subject / Principal | The caller identity, one entry of `PrincipalMap` |
+| Peer | Session (RBAC session) | One connection of a principal; one principal has N Peers |
+| owner | Superuser | Skips every check |
+| action | Action / Permission | A Remote endpoint `ns/method`, or a forwarded event `event:`; the Gateway still calls it an endpoint internally |
+| resource | Resource / Object | `{ kind: 'global' }`, or `{ kind, id }` of a declared resource kind |
+| role | Role | An action set over one resource kind; a plain value of the business package |
+| binding | RoleBinding (role assignment) | The principal holds a role on a resource, optionally with a condition |
+| condition | Condition (IAM Conditions) | Attached to a binding; allows by the request's action, resource, and arguments, or refuses with a reason |
+| policy | Allow policy | The bindings and optional `explain` a business package gives for one non-owner principal kind |
+| explain | Denial explanation | Names a refusal reason when no binding allows; never changes the decision |
+| Gateway | PEP (Policy Enforcement Point) | Resolves resources and enforces the decision |
+| remote-access | PDP (Policy Decision Point) | Computes decisions, list filtering, and access snapshots |
+
+| Rule | Content |
+|---|---|
+| Default deny | A non-owner is allowed only when the role of some binding explicitly lists the action; an action no role lists, a resource without a binding, and a principal kind without a policy are all refused. Every Remote method is under access control; there is no separate accessibility marker |
+| No inheritance between resources | `global` is not a parent of other resources: a grant on `global` implies no grant on any Session or Workspace, and a grant on a Workspace implies no grant on its Sessions |
+| A `global` grant is an explicit global grant | A call that resolves to no resource touches `global`. Putting an action in a `global` role lets that principal call it without resource restriction, which is a high-risk grant; once the method is allowed, it can still decide each item inside with `allows(resource)` |
+
+#### Resources and resource readers (`@deepseek-ai/dsh-typert-protocol`, `@deepseek-ai/dsh-typert-registry`)
+
+```text
+/** Merge-extensible resource kinds: the key is the kind, the value declares its identity type. */
+export interface TypertResourceMap {}
+// @deepseek-ai/dsh-session/types: session: TypertResource
+// @deepseek-ai/dsh-workspace/types: workspace: TypertResource
+
+// Runtime registries in typert
+ctx.typert.resources.register(kind, { field }): TypertDisposer
+ctx.typert.lookups.register(key, { parameter, wire, resource?, … }) // resource: the kind its wire id names
+ctx.typert.contexts.registerHost(key, { wire, resource?, … }) // a TypertHostContextAdapter
+
+export interface TypertHostContextAdapter {
+ readonly wire: string
+ readonly wireTypeSymbol: string
+ /** Resource kind the wire identity names; access checks read the identity from args before resolve(). */
+ readonly resource?: TypertResourceKind
+ resolve(id: Wire): Context | undefined | Promise
+}
+
+export type RemoteAccessResourceKind = 'global' | TypertResourceKind
+export type RemoteAccessResource =
+ | { readonly kind: 'global' }
+ | { readonly kind: K; readonly id: TypertResourceId } // for each declared kind K
+export type RemoteAccessResourceOf = Extract
+
+/** Per-kind readers, called with the method's positional wire arguments. */
+export type TypertResourceReaders = {
+ readonly [K in TypertResourceKind]?: (...args: WireArgs) => Id | readonly Id[] | undefined // Id = TypertResourceId
+}
+
+export interface RemoteMethodOptions {
+ readonly name?: string
+ readonly mode?: 'stream'
+ readonly resources?: TypertResourceReaders
+}
+
+export function Remote(
+ option: string | RemoteMethodOptions>>,
+): (method: (this: This, ...args: Args) => Result,
+ context: ClassMethodDecoratorContext Result>) => void
+
+export interface RemoteScopeOptions {
+ readonly name?: string
+ readonly resources?: TypertResourceReaders
+}
+
+export function RemoteScope(
+ key: Extract,
+ option?: string | RemoteScopeOptions>>,
+): (method: (this: This, ...args: Args) => Result,
+ context: ClassMethodDecoratorContext Result>) => void
+```
+
+| Rule | Content |
+|---|---|
+| Syntax | `@Remote({ name: 'page', resources: { session: (request) => request.address.kind === 'session' ? request.address.sessionId : request.address.childSessionId } })`; a Context method writes `@RemoteScope('agent')` and gets its Session resource from the Agent Context's `resource: 'session'`, and writes `@RemoteScope('agent', { resources: { workspace: (request) => request.workspaceId } })` when it needs another resource. Most methods write no `resources` and get their resources from automatic resolution |
+| Resource kinds | The lower-level packages that declare the identity types merge the resource kinds: `session: TypertResource` in `@deepseek-ai/dsh-session/types` and `workspace: TypertResource` in `@deepseek-ai/dsh-workspace/types`. session-controller references the commands package, so a merge in session-controller would make commands reference session-controller and form a project-reference cycle. remote-access builds in `global`, which has no id and stays out of `TypertResourceMap`; typert reserves the name, and `typert.resources.register('global', …)` throws |
+| Default fields and declarations | The domain that owns a kind registers its default wire field when its controller is constructed: the `SessionController` constructor calls `typert.resources.register('session', { field: 'sessionId' })` and the `WorkspaceController` constructor registers `workspace` → `workspaceId`; registering a kind twice throws. The Agent, Session, and `workspaceFileScope` lookups declare `resource: 'session'`, and the agent package's Agent Context declares `resource: 'session'` |
+| Resolution precedence | Each resource kind takes, in order: ① that kind's reader in the method's `resources`; ② every lookup parameter that declares that kind as its `resource` contributes its wire identity, and an omitted optional lookup parameter contributes none; when the Context of a Context method declares that kind as its `resource`, the Context's wire identity contributes too; ③ that kind's default field: a top-level named argument of that name when one exists, otherwise that field of the first object argument. A default field names one identity, so an array there is an invalid value |
+| Argument values | Resource resolution and the `args` of `decide()` read the named arguments validated against their Typert-compiled types: each JSON argument is decoded once by its codec, which drops object fields its type does not declare, and dispatch reuses that value, so a caller cannot change the resources with an undeclared field; lookup parameters and the Context identity keep their wire identities, and no lookup or Agent is resolved at that point |
+| Resolution result | The resources of every kind together are the resources the call touches; with none, the call touches `[global]`; when resources resolve, only those are used and `global` is not added |
+| Readers | Called with the method's positional arguments: the named arguments mapped by the descriptor back to the method's parameter order (the validated value in a JSON position, the wire identity in a lookup position, and the call's signal in the cancellation position); return one id, an id array (every one must be allowed), or `undefined` (no resource of that kind) |
+| Invalid values | A reader that throws or returns an empty array, or a read value (including a Context's wire identity) that is not a non-empty string, makes the resources invalid: the Gateway refuses the call with `remote-access/denied` (reason `resource-invalid`) without asking `decide()` |
+| Completeness | `resources` and automatic resolution must cover every resource the method touches. A call that resolves no resource falls to `global`, which only a `global` role can allow, so it is refused by default; when only some resources resolve, the rest go unchecked and a principal granted on the resolved resources can reach them, which the method author must prevent |
+| Type inference | The parameter types of `@Remote` and `@RemoteScope` readers are inferred from the decorated method: the factory parameter uses `NoInfer`, so `Args` is inferred only from the returned decorator; lookup parameters map to wire types through `TypertLookupMap` (for a method whose first parameter is `agent: Agent`, the reader's first parameter is `SessionId`). A missing property, more parameters than the method has, and a `global` key fail compilation |
+| Storage | `resources` readers live on the class prototype with the other `@Remote` / `@RemoteScope` metadata, are recorded on instance construction, and are safe across duplicate installations; they do not enter Typert artifacts |
+| Context methods | `@RemoteScope(key, option?)` matches `@Remote`: a string is the export name and an object is `{ name?, resources? }`. A Context's wire identity takes part in resolution like a lookup parameter that declares a `resource`, with the same precedence, and a `resources` reader for that kind overrides it; the Gateway reads that identity directly from args before `resolveReceiverContext`, so no Agent is activated. A Context that declares no `resource` contributes no resource from its identity, and the method resolves by the ordinary rules |
+
+#### Endpoint resources
+
+The table lists the endpoints #3854 viewers use and where their resources come from. Only `session/page` and `session/follow` need explicit `resources`; the rest rely on a lookup's `resource` or the default field:
+
+| Endpoint | Resource | Resolution source | Filtering inside the method |
+|---|---|---|---|
+| `workspace/follow` | `global` | no resource | yes: Workspaces and Sessions |
+| `session/control` | `global` | no resource | yes: Sessions |
+| `session/list` | `global` (`_request` has no `sessionId`) | no resource | yes: Session rows |
+| `session/modelCatalog` | `global` | no resource | no |
+| `session/canOpenWorkspacePath` | `global` | no resource | no |
+| `llm/listConfigurableProviders` | `global` | no resource | no |
+| `llm/listProviders` | `global` | no resource | no |
+| `commands/list` | session: `args.agentId` (the Agent lookup's wire field) | Agent lookup `resource: 'session'` | no |
+| `goals/get` | session: `args.agentId` | Agent lookup `resource: 'session'` | no |
+| `session/attachment` | session: `args.request.sessionId` | default field | no |
+| `skills/list` | session: `args.request.sessionId` | default field | no |
+| `session/fork` | session: `args.request.sessionId` | default field | no |
+| `session/prompt` | session: `args.request.sessionId` | default field | no |
+| `session/cancel` | session: `args.request.sessionId` | default field | no |
+| `session/projections` | session: `args.request.sessionId` | default field | no |
+| `session/page` | session: `args.request.address.sessionId` for a Session address; `args.request.address.childSessionId` for a subagent address | `resources` | no |
+| `session/follow` | same as above | `resources` | no |
+| `remoteAccess/follow` | `global` | no resource; remote-access allows it for every principal | no |
+
+A subagent address reads the child transcript, so its resource is the child Session. A Client rereads the projections of known Sessions after a reconnect, including the parent of a subagent Session; a reader without a grant on that parent has its projection read refused. Whether a grant on a parent Session covers its subagent children is the policy's decision, that is, whether it gives a binding for the child Session; the generic layer derives no parent resource.
+
+Methods that need no non-owner access declare nothing, and a method no role lists is refused. The request fields of subagent `prompt` are `parentSessionId` / `childSessionId`, so automatic resolution finds no Session and resolves it to `global`; it must therefore stay out of every `global` role. `session/search` filters its hits per Session inside the method. `subagents/list` is not a Remote endpoint (`SubagentRuntime.list()` carries no `@Remote`).
+
+#### Roles, bindings, and policies (`@deepseek-ai/dsh-api-remote-access`)
+
+```text
+/** Remote endpoint `ns/method`, or `event:` for one forwarded event; endpoint segments never contain `:`. */
+export type RemoteAccessAction = RemoteAccessEndpoint | `event:${string}`
+
+/** A named action set over one resource kind; a plain value owned by the business package. */
+export interface RemoteAccessRole {
+ readonly name: string
+ readonly resource: K
+ readonly actions: readonly RemoteAccessAction[]
+}
+
+/** Answer of decide() and of a condition. */
+export type RemoteAccessDecision =
+ | { readonly kind: 'allow' }
+ | { readonly kind: 'deny'; readonly reason: string }
+
+/** Refines one binding for one action on its resource; `args` only for a concrete call. */
+export type RemoteAccessCondition = (request: {
+ readonly action: RemoteAccessAction
+ readonly resource: RemoteAccessResourceOf
+ readonly args?: Readonly>
+}) => RemoteAccessDecision
+
+/** One role on one resource of the role's kind; the resource type follows from the role. */
+export type RemoteAccessBinding = {
+ [K in RemoteAccessResourceKind]: {
+ readonly role: RemoteAccessRole
+ readonly resource: RemoteAccessResourceOf
+ readonly condition?: RemoteAccessCondition
+ }
+}[RemoteAccessResourceKind]
+
+/** One access question; the Gateway passes only valid resources, [global] when none. */
+export interface RemoteAccessRequest {
+ readonly principal: P
+ readonly action: RemoteAccessAction
+ /** Allow only when the action is allowed on every listed resource. */
+ readonly resources: readonly RemoteAccessResource[]
+ /** Named arguments of a concrete call, validated against their Typert-compiled types with lookup parameters as wire identities; absent for events, snapshots, and list filtering. */
+ readonly args?: Readonly>
+}
+
+/** Business rules for one non-owner principal kind; synchronous over in-memory state. */
+export interface RemoteAccessPolicy {
+ bindings(principal: P): readonly RemoteAccessBinding[]
+ /** Reason for a request no binding allows; never changes the decision. */
+ explain?(request: RemoteAccessRequest
): string | undefined
+}
+
+/** details of `remote-access/denied` */
+export interface RemoteAccessDeniedDetails {
+ readonly action: string
+ readonly reason: string
+}
+
+class RemoteAccessService { // ctx.remoteAccess; Remote namespace remoteAccess
+ registerPolicy>(kind: K, policy: RemoteAccessPolicy): () => void
+ invalidate(principal?: Principal): void
+ decide(request: RemoteAccessRequest): RemoteAccessDecision // Gateway-only
+ allows(resource: RemoteAccessResource): boolean // reads the accessing ctx's invocation
+ snapshotFor(principal: Principal): RemoteAccessBaseline // a page's opening baseline
+ follow(signal): AsyncIterable // remoteAccess/follow
+}
+```
+
+| Rule | Content |
+|---|---|
+| Owner | The Gateway does not resolve resources or call `decide()` for the owner, and reads no policy |
+| Registration | `registerPolicy(kind, policy)` returns a disposer, and a business package calls it inside `ctx.effect()`. Each non-owner principal kind has at most one policy, and a second registration for the same kind throws; `bindings()` and `explain()` receive principals typed by the kind |
+| Built-in allow | `remoteAccess/follow` is allowed for every principal, so every Peer can read its own snapshot |
+| Decision | A principal kind without a policy is refused with `no-allow`. Otherwise `bindings(principal)` is read, and a linear scan finds for each requested resource one binding whose resource is equal (the same `kind` and, for a non-`global` resource, the same `id`), whose role's `actions` contains the action, and that has no condition or whose condition allows; the request is allowed only when every resource has one (AND). A condition runs only for a binding whose resource is equal and whose role contains the action |
+| Refusal reason | The first resource not allowed decides the reason, in this precedence: when matching bindings exist on that resource but all their conditions refuse, the first condition's reason; otherwise the return value of `explain(request)`; without `explain`, or when it returns `undefined`, `no-allow`. `explain` runs only after the decision is already a refusal and never changes it |
+| Failure isolation | When `bindings()`, a condition, or `explain()` throws, the decision is refused with `policy-failed`; in a snapshot computation, a throwing `bindings()` leaves that principal no grant and a throwing condition excludes that action. A faulty rule therefore never breaks the shared `$events` source, a snapshot stream, or page rendering; after each `invalidate()` or policy change, the first failure of each policy is logged once |
+| Synchronous | `bindings()`, conditions, and `explain()` return synchronously from state the business package preloads in memory, so list filtering can decide per item and snapshots can be computed |
+| Evaluation timing | Each `decide()`, each `allows()`, and each snapshot computation calls `bindings()` once, with no caching; time-dependent state (such as expiry) applies immediately, and a long stream (such as `workspace/follow` or `session/control`) reflects added or removed bindings at its next per-item decision; snapshots are recomputed only after `invalidate()`, so a business package also calls `invalidate()` when a time event such as expiry happens |
+| Notification | When a business state change (grant, revocation, mode switch) alters a binding or a condition's answer, the business package must call `invalidate(principal?)`; snapshots are recomputed only after `invalidate()` and policy registration or removal |
+| `remote-access/denied` | Only a call refusal becomes an error, with details `{ action, reason }`, where reason is the reason a condition or `explain` gave, `no-allow`, `policy-failed`, or the `resource-invalid` the Gateway gives for invalid resources; a refused event is simply not pushed |
+| Required service | The Gateway declares `static inject = ['typert', 'remoteAccess']`; a composition that mounts the Gateway mounts remote-access, which [`bundle/base`](../../../../packages/bundle/base/cordis.patch.yml) assembles right after `typert-gateway`; the plugin manager protects it like the Gateway, so it cannot be disabled from the Plugins page |
+
+#### Example: share viewer
+
+The `share-viewer` principal that liveshare declares shows that the derivation from business state to bindings stays in the business package; `shares` in the example stands for the share state the business package holds in memory:
+
+```text
+const globalReader = { name: 'share:global-reader', resource: 'global', actions: ['session/list', 'session/control', 'workspace/follow', 'session/modelCatalog', 'llm/listProviders', 'commands/list'] }
+const sessionReader = { name: 'share:session-reader', resource: 'session', actions: ['session/page', 'session/follow', 'session/list', 'session/control', 'workspace/follow', 'commands/list', 'event:api-session/status', 'event:api-session/added'] }
+const workspaceReader = { name: 'share:workspace-reader', resource: 'workspace', actions: ['workspace/follow'] }
+const forker = { name: 'share:forker', resource: 'session', actions: ['session/fork'] }
+const writer = { name: 'share:writer', resource: 'session', actions: ['session/prompt', 'session/cancel'] }
+
+ctx.effect(() => ctx.remoteAccess.registerPolicy('share-viewer', {
+ bindings(viewer) {
+ const share = shares.serving(viewer) // undefined once stopped, expired, or no longer admitted
+ if (share === undefined) return []
+ const source = { kind: 'session', id: share.sessionId }
+ return [
+ { role: globalReader, resource: { kind: 'global' } },
+ { role: sessionReader, resource: source },
+ ...(share.allowsFork ? [{ role: forker, resource: source }] : []),
+ ...(share.mode === 'write' ? [{ role: writer, resource: source, condition: sourceWrite(share) }] : []),
+ ...shares.forksOf(viewer).flatMap(child => [
+ { role: sessionReader, resource: { kind: 'session', id: child.id } },
+ ...(child.revoked ? [] : [{ role: writer, resource: { kind: 'session', id: child.id }, condition: childPrompt(child) }]),
+ ]),
+ ...shares.workspacesShowing(viewer).map(id => ({ role: workspaceReader, resource: { kind: 'workspace', id } })),
+ ]
+ },
+ explain({ principal, action }) {
+ return shares.refusal(principal) // 'share-expired', 'share-stopped', …
+ ?? (action === 'session/prompt' ? 'share-not-writable' : undefined)
+ },
+}))
+
+// Without args (snapshots, list filtering) a condition answers for the action alone.
+const sourceWrite = share => () => share.confined ? allow : deny('source-write-unavailable')
+const childPrompt = child => ({ action, args }) =>
+ action === 'session/prompt' && args !== undefined && !child.allowsPrompt() ? deny('fork-prompt-refused') : allow
+```
+
+| Owned by | Content |
+|---|---|
+| Business package | The action lists of roles; the derivation from share state to bindings (stop, expiry, admission, mode, the viewer's own forks, revocation, Workspaces containing visible Sessions); per-request conditions; refusal reasons for the viewer page |
+| Generic layer | Resource resolution, AND across resources, role and action matching, condition calls, reason precedence, list filtering, snapshot computation, and failure isolation |
+
+That viewer's snapshot contains grants on `global`, the source Session, each of its own forks, and each visible Workspace; `session/list` appears in both a `global` role and a session role, see [List and aggregate methods](#list-and-aggregate-methods).
+
+#### Host access-check order
+
+Every call path (HTTP `/api`, WebSocket streams, in-process `fetch(request, peer)` / `invoke` / `stream` / `wireStream.open`) passes through one Gateway sequence:
+
+| Step | Action | Failure |
+|---|---|---|
+| 1 | Admission yields the Peer: network paths from Connection `admit()` (`browserPeer`), in-process paths from the caller. The Gateway checks `peer.signal` before it reads the wire payload | Failed network admission answers HTTP 401 / 403 without entering the Gateway; a disposed Peer: `gateway/peer-disposed` |
+| 2 | Resolve the endpoint and validate the argument field set (`resolveDescriptor`, `assertExactArguments`), then validate the JSON arguments against their Typert-compiled types, decoding each once | `gateway/invocation-unavailable`, `gateway/ambiguous-endpoint`, `gateway/definition-unavailable`; extra or missing fields: `gateway/arguments-invalid`; a JSON argument failing validation: `gateway/input-invalid` (for owners and non-owners alike, before the access check) |
+| 3 | Owner pass-through: for `principal.kind === 'owner'` go straight to step 5 | — |
+| 4 | Non-owner: the Gateway resolves resources from the arguments validated in step 2 (`resources` readers, then lookup parameters and the `resource` a Context declares, then default fields; lookup and Context identities are wire identities, and neither a lookup nor the Context is resolved), then asks `remoteAccess.decide({ principal, action: endpoint, resources, args })` with the validated arguments as `args`; invalid resources are refused with `resource-invalid` without asking `decide()`; a `deny` decision becomes the thrown error | `remote-access/denied` with details `{ action, reason }` |
+| 5 | Dispatch: `resolveReceiverContext` (which can activate an Agent), lookup resolution (JSON arguments reuse the step 2 values), and the method call | Existing codes unchanged, such as `gateway/lookup-not-found`, `gateway/context-not-found`, `gateway/input-invalid`, `gateway/service-unavailable`, `gateway/method-unavailable`, and business errors |
+
+The access check (steps 3–4) precedes lookup and Context resolution, so a refused call activates no Session and causes no other side effect. The only new Gateway error code is `gateway/peer-disposed`; remote-access declares the refusal code `remote-access/denied`.
+
+Gateway-owned endpoints: a non-owner may subscribe to `$events` but never receives waterfall frames it could answer; `$events/result` from a non-owner is refused with the existing `gateway/invocation-unavailable`; both run the step-1 check.
+
+#### Forwarded events
+
+An `emit` entry of `API_REMOTE_FORWARDED_EVENTS` may declare `resources`: readers keyed by resource kind and typed by that event's own parameters through `TypertForwardableEventEntry`, for example `{ event: 'api-session/added', mode: 'emit', resources: { session: (summary) => summary.sessionId } }`. API Remotes attaches the readers to each forwarded frame; for every non-owner `$events` stream the Gateway resolves the resources once per frame and pushes the frame only when `remoteAccess.decide({ principal, action: 'event:', resources })` allows it.
+
+| Rule | Content |
+|---|---|
+| Declared events | `api-session/activity`, `api-session/added`, `api-session/error`, `api-session/removed`, and `api-session/status` declare their Session |
+| Undeclared resources | An event without `resources`, or with an empty `resources: {}`, touches `global` and reaches a non-owner only when a `global` role lists `event:` |
+| Missing resource | An event carries every resource it declares, so a reader returning `undefined` makes the event's resources invalid, and the Gateway delivers it to no non-owner without asking `decide()`; a call missing a resource touches `global` instead |
+| Whole or nothing | An allowed event arrives with its payload unchanged; no payload is trimmed or projected per Peer |
+| One resource per event | A notification carries at most one resource; collection changes travel on follow streams (`session/follow`, `session/control`, `workspace/follow`), whose methods decide each item inline through `allows()` |
+| Waterfalls | Non-owners never receive waterfall frames, and no policy decides them |
+| Non-owner stream facts | A non-owner `$events` stream opens with the same `host` as an owner stream, including `home`; `hasLiveClient()` counts only owner streams, the ones that can answer waterfalls |
+
+
+#### List and aggregate methods
+
+Calls of list, paging, and aggregate methods resolve to `global` and are allowed by a `global` role; inside, the method calls `this.ctx.remoteAccess.allows(resource)` for every item to decide whether the current call's action is granted on that resource (without args). A list action therefore appears both in a `global` role (allowing the call) and in resource roles (allowing each item): `session/list` must be granted on `global` to be called, and each Session row also requires `session/list` granted on that Session. `allows()` reads the invocation on the Context that accessed the service; it is always true for the owner and for in-process code outside a Remote call, which are not filtered.
+
+| Method | Filtered items |
+|---|---|
+| `session/list` | Session rows (`session` resource) |
+| `session/search` | search hits (`session` resource) |
+| `session/control` | baseline projections and projection increments (`session` resource) |
+| `workspace/follow` | Workspaces in the baseline, `upsert` frames, and `order` ids (`workspace` resource), the Sessions inside Workspaces, and the archived / pinned Session sets (`session` resource). The stream tracks the Workspaces it delivered; when one is no longer allowed at its next change or in a new order, it sends a `replace` increment with the complete filtered state instead of a `remove`, because the Client never resurrects a removed id, and the Workspace reappears through an ordinary `upsert` once access returns. A `remove` frame reports a deletion and reaches only a caller that received that Workspace |
+
+#### Access snapshot stream
+
+```text
+// Remote: remoteAccess/follow (stream; every Peer may read its own snapshot)
+type RemoteAccessFrame =
+ | { readonly type: 'baseline'; readonly owner: true }
+ | { readonly type: 'baseline'; readonly owner: false; readonly grants: readonly RemoteAccessGrant[] }
+ | { readonly type: 'upsert'; readonly grant: RemoteAccessGrant }
+ | { readonly type: 'remove'; readonly resource: RemoteAccessResource }
+
+interface RemoteAccessGrant {
+ readonly resource: RemoteAccessResource // global, or one declared-kind resource
+ readonly actions: readonly string[] // endpoint actions allowed on this resource without args, sorted
+}
+
+/** Window global carrying a page's opening baseline. */
+export const REMOTE_ACCESS_SNAPSHOT_KEY = '__DSH_REMOTE_ACCESS__'
+```
+
+| Behavior | Detail |
+|---|---|
+| First snapshot | `snapshotFor(principal)` computes a page's opening baseline. remote-access adds the global `REMOTE_ACCESS_SNAPSHOT_KEY`, set to the owner baseline `{ type: 'baseline', owner: true }`, to every index the webServer renders, through `webserver/index-inject`. A business package that renders a non-owner page (such as liveshare on the owner Host) places an assignment of that global to `snapshotFor(principal)` after every index injection row; the later assignment wins, and the Client reads the global when it mounts |
+| Subscription | Each Peer subscribes to one access snapshot stream, whose baseline replaces the injected one |
+| Computation | All bindings of the policy for the principal's kind are merged by equal resource; each resource's `actions` are the endpoint actions of those bindings' roles that the same decision as a call allows (one resource, without args, conditions called without args); only a resource with at least one allowed endpoint action gets a grant. Decisions and snapshots share one source: for the same principal, action, and resource (without arguments), the snapshot answer equals `decide()` |
+| Endpoint actions only | `event:` actions do not enter the snapshot; the frontend decides entry visibility from endpoint actions only |
+| Increments | `invalidate()` or a policy registration or removal recomputes and pushes `upsert` / `remove`, and a resource whose allowed set becomes empty pushes `remove`; unchanged resources push nothing |
+| Lifetime | A generation ends when its signal aborts or when the remote-access service unloads, so a reloaded service answers every reader from a fresh baseline; the Client treats the end as a carrier loss and reconnects |
+| Owner | Receives only `{ owner: true }`, so every frontend answer is true |
+| Wire type | `actions` is `readonly string[]`: Typert cannot generate a codec for template-literal types |
+
+Access hooks judge only by action and resource and never look at call arguments; at call time a condition may also read args for a finer decision, and the Host result prevails.
+
+#### Client face
+
+The Client uses framework standard-hook injection and no React context. Components receive hooks only through props and import no remote-access value.
+
+```text
+// @deepseek-ai/dsh-api-remote-access/client
+interface ClientRemoteAccess { // Client service ctx.remoteAccess
+ source(kind: TypertResourceKind, id: string, action: string): HostObservable
+}
+GlobalStandardProps.useRemoteAccess: (action: RemoteEndpoint) => boolean // remote-access; the global resource
+
+// @deepseek-ai/dsh-client-ui-session (injects remoteAccess)
+GlobalStandardProps.useSessionAccess: (sessionId: SessionId, action: RemoteEndpoint) => boolean
+SessionStandardProps.useAccess: (action: RemoteEndpoint) => boolean
+SessionMaybeStandardProps.useAccess: (action: RemoteEndpoint) => boolean | undefined
+
+// @deepseek-ai/dsh-client-ui-workspace (injects remoteAccess)
+GlobalStandardProps.useWorkspaceAccess: (workspaceId: WorkspaceId, action: RemoteEndpoint) => boolean
+
+// RemoteEndpoint = Extract
+```
+
+| Concern | Decision |
+|---|---|
+| Snapshot and mount | The remote-access Client plugin starts from the baseline the index injected (read synchronously at startup and validated), follows the snapshot stream, applies each baseline in one pass, provides the Client service `ctx.remoteAccess.source(kind, id, action)` (an identity-stable boolean source), and injects `useRemoteAccess` on the `global` resource. The entry module imports the generated `/remote` contribution and hands it with the injected baseline to `mountAccess(ctx, contribution, initial)`, which mounts it through `ctx.remote.$mount` (unit specs pass a fake contribution), following experimental client-ui-cot-translation, voice-input, and claude-code-mods; a central mount in api/remotes would form the project-reference cycle ui-session → api/remotes → remote-access → ui-session |
+| Session resource | ui-session gives the bound Session's answer through `uiSession.provide({ keyedHooks: ['access'], resolve })`; the framework binds the Session as it does for `useProjection`. `useSessionAccess` is injected through `provideRoot` |
+| Workspace resource | ui-workspace injects `useWorkspaceAccess` through `provideRoot` |
+| Distinct names | The `global` answer is `useRemoteAccess` and the Session answer is `useAccess`, so neither shadows the other; in a session-maybe scope without a Session, `useAccess` is `undefined`, which callers treat as not allowed, and a `global` decision reads `useRemoteAccess` |
+| Two-argument calls | Framework keyed hooks take a standard second argument: `KeyedStandardSource` is `(key, member?) => source`; a string second argument makes the hook read the resolver's `(key, member)` source without a selector, and the resolver returns the same source for the same arguments. Existing `(key, selector?, equal?)` and `(selector, equal?)` calls are unchanged. Curried forms are forbidden |
+| Return value | Every answer on a resource is a boolean; action types come from the `ctx.remote` namespaces and methods |
+| No fallback | Asking a Session resource about a non-session method (such as `useAccess('session/create')`) reads the decision on that Session resource and never falls back to the `global` answer |
+| No snapshot | True for the owner, also after its stream fails terminally; a non-owner page without an injected baseline, or whose stream fails terminally, answers false, and the terminal failure is reported with its error and the advice to reload; a reconnect keeps the last snapshot; a malformed injected baseline fails the mount; the Host still checks every call. Hooks never issue requests |
+| Example consumers | The ui-workspace Session row menu's fork entry reads `useSessionAccess(sessionId, 'session/fork')` and is not rendered when it is false; the ui-chat turn tail's branch button reads `useAccess('session/fork')` on the Session resource and is not rendered when it is false. Replacing #3854's `clientWritePolicy` stays with #3854 |
+| Tests | `@deepseek-ai/dsh-client-test-runtime` exports `allowAllAccess` props and `allowAllAccessPlugin`; TestClient drops the remote-access row as it drops api-remotes and installs the allow-all service, and SlotTestRuntime installs it before ui-session |
+
+### Security invariants
+
+| Invariant | Basis and enforcement point |
+|---|---|
+| Only in-process Host code supplies a principal, when it creates a Peer | `PeerScope.principal` is read-only; `createPeer()` has no route |
+| No wire field can set or override a principal | Principals never come from args, the envelope, headers, the query, or cookies. Extra top-level args fields fail `assertExactArguments`; the payload must be exactly `{ args }`; [`clientRequestSchema`](../../../../packages/client/connection/src/rpc-schema.ts) drops unknown envelope fields; a same-named field inside a request body is business data |
+| Browser network admission yields the owner | Connections that pass [browser token authentication](../../implemented/architecture/2026-08-24-browser-token-authentication.md) receive `browserPeer` |
+| No implicit Peer | Neither the Gateway nor Connection supplies a default Peer; required parameters expose a missing Peer at compile time |
+| A disposed Peer cannot dispatch | `gateway/peer-disposed` |
+| A non-owner cannot call without authorization | A call is allowed only when a binding's role explicitly lists the action and its resource matches; a kind without a policy is refused; the Gateway depends on `remoteAccess` and does not start without it |
+| A `global` grant implies no resource grant | Resources do not inherit; `allows()` inside a method decides each item independently |
+| An unknown principal kind is not the owner | Only `kind === 'owner'` is the owner |
+| Non-owners answer no waterfall | `$events` gives non-owners no waterfall frame, and `$events/result` refuses them |
+| A non-owner receives only authorized notifications | Each forwarded frame is decided by `event:` and its resources, and reaches a non-owner only when allowed |
+
+### Recording the message source
+
+The `MessageSourceMap` entry in the [session-controller types](../../../../packages/api/session-controller/src/types.ts) gains an optional field:
+
+```text
+'user-rpc': {
+ kind: 'user'
+ rpcId: SessionRequestId
+ clientTimeZone?: string
+ /** Non-owner caller that submitted this prompt; absent means the owner. */
+ principal?: Principal
+}
+```
+
+Two producers build a `kind: 'user'` + `rpcId` user-message source inside a Remote call. Both call `principalOf(this.ctx.invocation)` in the Remote method and write `principal` only when it is defined:
+
+| Producer | Location |
+|---|---|
+| `SessionController.prompt()` builds the source and `SessionCommandController.prompt(request, source)` only delivers it | [`session-controller/src/index.ts`](../../../../packages/api/session-controller/src/index.ts), [`commands.ts`](../../../../packages/api/session-controller/src/commands.ts) |
+| subagent `prompt()` (`BrowserPromptSource`); no role lists it, so a non-owner reaches it only through in-process Host code, not through the Gateway | [`subagent/src/index.ts`](../../../../packages/subagent/subagent/src/index.ts) |
+
+Business code does not pass a principal down as a function parameter: the identity is read once from the invocation in the Remote method, and the built source travels down. The Agent inbox has no interception point that could add the field on enqueue (`followup` / `steer` accept built messages, and in-process producers such as the SDK and scheduling run outside Remote calls), so each producer records it, and the code-review rule covers new producers.
+
+| Fact | Detail |
+|---|---|
+| `kind` is unchanged | Still `'user'`; existing `kind === 'user'` checks are unaffected |
+| Owner messages are unchanged | An owner call writes no field, so its source is byte-identical to the source before principals existed; a message without the field (including every older Session) is the owner's |
+| Not part of model requests | LLM adapters read only the model source ([`llm-deepseek/src/replay.ts`](../../../../packages/llm/llm-deepseek/src/replay.ts)) |
+| Session format | `SESSION_FORMAT_VERSION` is not bumped; per the [persistence type-change cookbook](../../../../docs/cookbook/reviewing-persistence-type-changes.md) the change records an acknowledgement |
+
+Example consumer use, outside this Note's scope: tool guards and pre-step rechecks read `source.principal` of the user messages in a turn and judge by the non-owner caller when any message carries one; a turn mixing owner and viewer messages takes the strictest.
+
+### Code-review rule and documentation
+
+The review rule's home is the "`@Remote` marks capability methods" section of [Remote and API rules](../../../../docs/defensive-patterns/remote-and-api.md): `@Remote({ resources })` and automatic resolution must cover every resource a method touches; aggregate results filter every item through `allows()`; the caller identity is read only from `invocation.peer.principal`, never from arguments; every call path names its Peer explicitly; business code does not pass a principal as a function parameter.
+
+| Document | Content |
+|---|---|
+| [`docs/subsystems/typert.md`](../../../../docs/subsystems/typert.md), [`docs/api-gateway.md`](../../../../docs/api-gateway.md) | `PeerScope`, `RemoteInvocation`, `InvokeRemoteRequest`, explicit Peers, `resources` on `@Remote`, and the access-check order |
+| READMEs of connection, gateway, typert-protocol, typert-registry, api-remotes, remote-access, session-controller, workspace-controller, ui-session, ui-workspace, ui-slots, and client-test-runtime | Peer / `browserPeer` in place of the operator; the remote-access role, binding, and policy contract; access hooks |
+
+### Out of scope and future work
+
+- The API-only `/connect` handshake, API cookies, one Peer per API connection, and WebSocket lifetimes are described in [Peer-scoped connect handshake](2026-10-09-operator-scoped-connect-handshake.md).
+- Multiple browser Peers (such as one per tab): every browser connection shares `browserPeer`.
+- Interaction participation is attached to each Peer. A non-owner with `interactions: 'answer'` still receives only RBAC-authorized broadcast frames and cannot answer owner waterfalls; per-principal approval handling remains outside this mechanism.
+- `agent-preset/selected` and the other forwarded events declare no resources and are decided on `global`; when its viewers need a Session event, #3854 declares that event's resources and lists `event:` in a role.
+- Liveshare integration (declaring `share-viewer`, one Peer per viewer connection, defining roles and registering a policy, removing `origin` / `prompt/origin` / `READ_ENDPOINTS`) happens on the #3854 side.
+- `session/cancel` ownership checks, the fork `childSessionId`, and `ToolExecutionInput.turn` do not change.
+- Precise resource recognition (for example, the Typert generator locating request fields by branded id type and writing them into descriptors): this change uses the `resource` that lookups and Contexts declare, default fields, and `resources` readers, and resource resolution can later be replaced without changing the policy interface.
+- Answering 403 to every non-owner on exact Fetch routes is temporary; each route is to declare its own access rule later.
+- The resource declaration of subagent `prompt` is open: its request fields are `parentSessionId` / `childSessionId`, so it currently resolves to `global`. The producer records `source.principal` for in-process callers; no role lists it, so non-owners cannot reach it through the Gateway.
+- Predefined and aggregate roles: when a second non-owner principal kind appears, domain packages declare the generic roles a method belongs to (such as `session:viewer`) with `@Remote({ roles })`, and each policy binds those roles; roles are currently defined by the business package as explicit action lists.
+
+### Relation to existing Notes
+
+- [Remote duplex stream](../../implemented/architecture/2026-09-19-remote-duplex-stream.md): partially superseded. The replaced parts are "the Peer records no identity or permission", "this Host has exactly one Peer, the operator", "an in-process carrier without a Peer means the operator", and its alternative stating that "the Access mechanism is business semantics"; the rest (the duplex stream, uplink, and `ctx.invocation` binding) stands. Both Notes stay active and cross-linked, and that Note's current facts are updated.
+- [Browser token authentication](../../implemented/architecture/2026-08-24-browser-token-authentication.md): unaffected. Its rejection of per-method privilege concerns owner authentication; an authenticated browser is still the owner and is not checked per method.
+- [Typert Remote method calls](../../implemented/architecture/2026-08-02-typert-remote-method-calls.md): unaffected; `@Remote` and `@RemoteScope` gain optional `resources` readers (the second argument of `@RemoteScope` may be `{ name?, resources? }`), read only by the Gateway's access check, and change neither endpoint naming, call modes, nor generated artifacts.
+- [Producer-owned message sources](../../implemented/architecture/2026-09-09-producer-owned-message-sources.md): unaffected; the producer that builds a source writes `principal`.
+
+## Alternatives considered
+
+**A separate `prompt/origin` event correlated by requestId (the #3854 approach).** Every prompt writes a required event, so even ordinary Sessions become unreadable by older versions; correlation serializes every prompt and maintains an origin fold, and messages outside the fold window lose their initiator.
+
+**Principal and peer as parallel invocation fields.** One caller's identity would have two sources; every admission point would have to produce and pass both, and a path that passes only one would mismatch them.
+
+**Keep "the Peer records no identity".** When business plugins attach identity through `peer.ctx`, every reader must know who attached it and how to read it, and the wire admission path cannot guarantee the attachment happened.
+
+**Keep a default-Peer fallback (`request.peer ?? …`).** An implicit default silently grants owner identity to a call that forgot its Peer; a required parameter turns the omission into a compile error.
+
+**A new source kind.** Every existing `kind === 'user'` check would need review, and one miss makes viewer messages disappear from titles, outlines, queues, or chat rendering.
+
+**Identity in a request-body field.** Any caller able to build a request body could forge or override it.
+
+**React context for frontend resources.** Business components read no React context; the framework binds standard hooks per resource, and components receive them only through props.
+
+**Driver-maintained endpoint allowlists that trim results (the #3854 approach).** The allowlist and the result trimming both live in the driver, the Gateway does not enforce them, and the snapshot and the frontend each derive their own; the generic layer enforces role action lists, and `allows()` inside the method performs list filtering.
+
+**Business packages implement `decide()` / `abstain` providers whose answers are merged.** Each business package hand-writes action × resource branches plus a separate resource list for snapshots, and the two stay consistent only by hand; a policy gives only bindings, decisions and snapshots are computed from the same bindings, and each principal kind has one policy, so no merge rule is needed.
+
+**A separate `@RemoteAccess` marker.** Role action lists already guarantee default deny, so a marker is only a second consent; a method that resolves no resource falls to `global`, and only an explicit global grant allows it. Resource readers describe the method, so they live on `@Remote`.
+
+**Calling resource-less calls `page` or `host`.** `page` describes a frontend location, not a resource; `host` reads easily as the parent of every resource on this Host. `global` means a grant tied to no resource.
+
+**Resource inheritance (GCP style: a grant on `global` or on a Workspace covers lower resources).** List methods allowed by a `global` grant must decide each item, and inheritance would make any `global` grant imply every Session; without inheritance, a binding grants each resource explicitly.
+
+**A role registry and predefined aggregate roles.** The only non-owner principal today is managed by one business package, so roles can be plain values of that package; aggregate roles would let methods a new package declares enter a share viewer's reach automatically, so they wait for a second non-owner principal kind.
+
+**Conditions attached to a role's actions.** A role stays reusable plain data; a refinement by business state belongs to the binding that grants the role to a principal.
+
+**Refusal reasons from conditions only.** Without a binding there is no condition to give a reason, so a viewer page could not tell "share expired" from "share not writable"; `explain` gives a reason only after the decision is already a refusal and never changes it.
+
+**A global resource-resolver registry (domain packages register recognizers by field name).** Field recognition separates from the method, nothing flags a method signature change, and same-named fields in other packages are misread; resource resolution uses a default convention on the method plus per-method `resources`, and a domain package registers only its own kind's default field.
+
+**Dot-path resource declarations (`'request.address.sessionId'`).** Types cannot constrain a path; the parameter types of `resources` readers are inferred from the decorated method, so a typo fails compilation.
+
+**Generator recognition of resource fields by branded id type.** Requires changing Typert artifacts and the descriptor format, and SRC descriptors carry no types; kept as future work.
+
+**A central mount of the remote-access generated contribution in api/remotes.** The remote-access Client depends on ui-session and ui-session depends on api/remotes, so a central mount forms a project-reference cycle; remote-access mounts its own contribution instead.
+
+**Decisions inside the Gateway (a `check()` with rules written in the Gateway).** The Gateway would carry access-control semantics and extra error codes; it resolves resources as it resolves lookups and calls `remoteAccess.decide()` before dispatch, and the rules and refusal code belong to remote-access.
+
+**Asynchronous policy decisions.** List filtering and snapshot computation need synchronous answers; business packages preload asynchronous state into memory.
+
+**On-demand permission queries from the frontend.** Request volume grows with the number of entries and cannot follow rule changes; the Host pushes a snapshot stream and the frontend reads it synchronously.
+
+**An access check after lookup resolution.** A lookup can activate an Agent, so an unauthorized call would cause side effects first.
+
+**Resource fields registered in remote-access (`registerScope`).** The access package would recognize argument fields although lookups already know which wire field names a Session; typert holds resource kinds beside lookups and Contexts, so the Gateway resolves calls and events in one place and remote-access reads no argument field.
+
+**Trimming forwarded event payloads per Peer.** A per-event projection would repeat list filtering inside every notification; an event carries at most one resource and is delivered whole or not at all, while collection changes travel on follow streams that filter inline.
+
+**Answering true until the first snapshot arrives.** A non-owner page would briefly show entries it cannot use; the index carries the first snapshot, so a page without one has no access.
+
+## Consequences
+
+### Verification
+
+| Case | Behavior | Test location |
+|---|---|---|
+| The wire tries to set a principal (top-level args, a payload sibling, the envelope, inside the request body) | The first two fail argument or payload validation; the rest are dropped or treated as business data; `invocation.peer` remains the admitted Peer | Gateway host spec |
+| Every call path names its Peer | `InvokeRemoteRequest.peer`, the `wireStream.open` peer, `fetch(request, peer)`, and the `bridge` peer are required; the Gateway has no fallback; the `/api` route passes `admission.peer` | typecheck, Connection host spec |
+| Browser connections and `createPeer()` | `browserPeer` has the owner principal; each `createPeer()` returns an independent Peer released with the Context that read Connection; exact routes answer 403 to non-owners | Connection host spec |
+| Disposed Peers | Later unary, stream, and `$events` dispatches fail with `gateway/peer-disposed`, also when the wire payload is malformed; its streams end; a running unary call completes; other Peers are unaffected | Gateway host spec |
+| Check order | The owner calls any method normally without reading a policy; a method no role lists refuses non-owners with `remote-access/denied` (`no-allow`); the access check precedes lookups, so a refused call activates no Session | Gateway host spec |
+| Policy registration | One policy per non-owner principal kind, and a second registration throws; once the disposer withdraws it, that kind is refused with `no-allow`; it is withdrawn with its fiber | remote-access host spec |
+| Decisions | Every resource needs a matching binding (AND); a role without the action, an unequal resource, or a refusing condition does not allow; the refusal reason follows condition > `explain` > `no-allow`; `explain` never changes the decision; conditions receive args for calls but not for snapshots and list filtering; a throwing `bindings()`, condition, or `explain()` refuses with `policy-failed`, each policy is logged once between `invalidate()` calls, and on the event path the owner stream and the shared event source continue; `remoteAccess/follow` is allowed for every principal | remote-access host spec, Gateway host spec |
+| Resource resolution | A default field (top-level or first object argument) yields its resource; a lookup parameter that declares that kind as its `resource` yields its wire identity; a Context method gets its Session resource from the Agent Context identity in args without activating the Agent, and may declare other resources through `resources`; a `resources` reader overrides its kind, an array yields several resources, and `undefined` none; with nothing the call is `[global]`; a throw, an empty array, or a value that is not a non-empty string (including a Context identity) is invalid, refused by the Gateway with `resource-invalid` without asking `decide()`; a Context that declares no `resource` resolves by the ordinary rules; resources and `args` read the validated arguments, so an undeclared nested field changes no resource and no condition sees it, and a JSON argument failing validation returns `gateway/input-invalid` before the access check | Gateway host spec (gateway-access) |
+| Resource registry | `typert.resources` registers one default field per kind, rejects a second registration and `global`, and withdraws the registration with its fiber; a configured lookup resolver keeps the provider's `resource`; a Host Context adapter keeps its `resource` | typert-registry spec |
+| Forwarded events | A non-owner `$events` stream receives an event, whole, only when `event:` is granted on its resources; an event that declares no resources is decided on `global`; an event whose reader returns `undefined` does not reach non-owners, and `decide()` is not asked; the owner receives all | Gateway host spec (gateway-access) |
+| First snapshot | `snapshotFor()` returns the owner baseline or the principal's grants; remote-access injects the owner baseline into every rendered index; the Client starts from the injected baseline and rejects a malformed one | remote-access host and client specs |
+| Type inference | The parameter types of `resources` readers are inferred from the method; lookup positions have wire types; a missing property, more parameters than the method has, and a `global` key fail compilation; a binding whose resource kind differs from its role's fails compilation | typert-protocol spec, remote-access spec (type tests) |
+| List filtering | `session/list`, `session/search`, `session/control`, and `workspace/follow` return non-owners only items for which `allows()` is true; `workspace/follow` sends a `replace` increment when a delivered Workspace loses visibility, and the Workspace reappears once access returns; owner results are unchanged | session-controller / workspace-controller host spec |
+| Access snapshots | The allowed set in a snapshot equals the Host decision for the same principal, action, and resource without arguments; it contains endpoint actions only; a resource with no allowed action gets no grant, and one whose allowed set becomes empty is removed; `invalidate()` and policy registration or removal push increments; a wait that a change ends leaves no abort listener behind; unloading the service ends every open snapshot stream; the owner receives only `{ owner: true }` | remote-access host spec |
+| Access hooks | `source()`, `useRemoteAccess`, `useAccess`, `useSessionAccess`, and `useWorkspaceAccess` follow the snapshot; `useAccess` on a Session resource reads that Session's answer; the owner answers true; a non-owner without a snapshot or after its snapshot stream fails answers false; hooks issue no requests | remote-access client spec, ui-session / ui-workspace client spec |
+| Two-argument keyed hooks | A string second argument reads the `(key, member)` source; existing call forms are unchanged | ui-renderer client spec |
+| A non-owner dispatches `session/prompt` | The recorded user message's `source.principal` equals that principal, and a cold restore reads the same value | session-controller host spec |
+| An in-process non-owner invocation of subagent `prompt` | The child Session's recorded user message has `source.principal` equal to that principal | subagent host spec |
+| Owner messages | The source is byte-identical to the source before this change; existing keyless recorded-session snapshots are unchanged | `pnpm run test:snapshot` needs no re-recording |
+| Persistence record | `verify-persistence-changes` passes | doc-sync |
+
+The SDK projections are unaffected: the TypeScript SDK server builds its own `{ kind: 'user' }` messages ([`sdk/server/src/server.ts`](../../../../packages/sdk/server/src/server.ts)) outside the Gateway and does not project the source; the Python SDK does not read the user source.
+
+### Costs and limits
+
+- The change spans protocol, Connection, Gateway, two domain controllers, subagent, a new package, and the frontend, with breaking API renames; it necessarily conflicts with pr-5945 in Connection and the Gateway, handled by the migration list.
+- The Gateway depends on `remoteAccess`: a custom composition that mounts the Gateway without remote-access leaves the Gateway waiting.
+- Session logs store viewer identities and travel with exported or shared Sessions, so no principal kind may carry a credential.
+- Every browser connection shares one Peer, so disposal cannot distinguish tabs until multiple browser Peers exist.
+- The default field convention reads by field name: a method that gains a same-named field touches that resource, which review must watch; precise recognition is future work.
+- A method that resolves only some resources lets a principal exceed its grant: unresolved resources go unchecked, and the method author ensures `resources` is complete.
+- Role actions are strings and are not checked against loaded endpoints: a misspelled or renamed action never matches and shows up as a refusal.
+- `api-session/added` carries the whole `SessionSummary`, so a non-owner allowed to receive it also learns the Session's `parentSessionId`.
+- A granted Session's cwd and tool paths already contain the owner's absolute paths, so the Gateway does not hide `home` from non-owner `$events` streams separately.
+- `workspace/follow` notices a Workspace whose visibility a policy revoked only at that Workspace's next change or the next order change, and then sends the non-owner a `replace` increment; an access change alone sends no frame.
+
+### Migration list for pr-5945
+
+| File | Migration |
+|---|---|
+| `packages/client/connection/src/peer.ts` | Replaces `operator-peer.ts`; construct `OperatorPeer` as `new Peer(owner, { principal })` |
+| `packages/client/connection/src/rpc-host.ts` | `this.operator` becomes `browserPeer` with principal `{ kind: 'owner' }` |
+| `packages/client/connection/src/rpc.ts` | `HostConnectionHandle` merges `admitConnection` with `browserPeer` / `createPeer` |
+| `packages/api/gateway/src/index.ts` | Replace `peer ?? this.operatorPeer()` where `$events` opens with an explicit Peer; reuse `gateway/peer-disposed` |
+| The operator-connect Note, upgrade guide, and subsystem changes | Rewrite operator terms as Peer / `browserPeer`; an API connection's Peer has principal `{ kind: 'owner' }` |
diff --git a/.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md b/.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md
new file mode 100644
index 0000000000..afbd49de7f
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md
@@ -0,0 +1,675 @@
+# Agent Note: Peer 身份属性与 Remote 方法级访问控制
+
+Status: implemented
+
+[English](2026-10-12-peer-principal-and-remote-access.md) | 中文
+
+## 问题
+
+Host 上的 Remote 方法此前无法得知本次调用代表谁,也无法按调用方限制可调用的方法。[`PeerScope`](../../../../packages/typert/protocol/src/types.ts) 的契约曾写明「Who the Peer is and what it may do are not recorded here」;[Connection](../../../../packages/client/connection/src/rpc-host.ts) 只有一个 operator Peer,`admit()` 对每个通过认证的请求都返回它,`/api` 路由与 interceptor 的 Fetch handler 固定使用它而不使用准入结果;[Gateway](../../../../packages/api/gateway/src/index.ts) 在调用方未给出 Peer 时隐式回退到 `operatorPeer()`。
+
+首个非 owner 调用方是 liveshare 读者。读者请求经 Relay 到达 owner 的 liveshare Host 插件,插件此前在 owner 进程内以 operator 身份分派,owner 的 Session 因此把读者的 prompt 当作 owner 本人的输入。PR #3854 为此把 liveshare 语义塞进 session-controller:请求体携带 `origin`,每条 prompt 都写 required 的 `prompt/origin` 事件并按 requestId 关联,controller 内做来源 fold,`serializeAdmission` 扩到全部 prompt,新增 `api-session/external-admission` 钩子;liveshare 自己维护接口白名单 `READ_ENDPOINTS` 并裁剪 list/follow 结果;前端的 `clientWritePolicy` 定义在 ui-conversation,5 个包三种读法,7 处靠「是否注入某回调」隐藏界面。
+
+需要一套通用机制:
+
+- 调用方身份由 Host 进程内代码赋予,wire 上任何字段都无法设置或覆盖。
+- 每条调用路径都显式给出 Peer,不存在隐式兜底。
+- 方法级权限在后端一处计算、一处把关;前端读同一份结果决定入口显隐,规则变化时自动同步。
+- prompt 投递时身份随用户消息落盘,供之后按 turn 判定发起者。
+
+## 决策
+
+### 范围
+
+| 部分 | 内容 |
+|---|---|
+| Peer 身份 | `PeerScope.principal` 与 `PeerScope.signal`;唯一 Peer 实现 `Peer`(`OperatorPeer` 改名)与 options 构造;删除 operator 概念与所有隐式 Peer 兜底;`connection.browserPeer`、`connection.createPeer()` 与 `fetch(request, peer)`;已 dispose 的 Peer 一律以 `gateway/peer-disposed` 拒绝;`user-rpc` source 落盘 `principal?` |
+| 方法级访问控制 | `@Remote` 的 `resources` 读取函数、`TypertResourceMap` 资源种类与 `typert.resources` 注册表(typert-protocol、typert-registry);Gateway 把调用与转发事件解析为资源;新包 `@deepseek-ai/dsh-api-remote-access`(Host 面 + Client 面):按 role、binding 与 policy 的 RBAC 判定 `decide(request)`、列表过滤 `allows()`、index 中的首份快照与访问快照流、access hooks |
+| 文档 | 一条代码评审标准与术语 |
+
+### 两层概念:principal 与 peer
+
+| 层 | 概念 | 例子 | 数量关系 |
+|---|---|---|---|
+| 授权层 | principal:调用方身份,权限判定的依据 | 一份分享授权 token 证明的 `share-viewer{shareId, actor}`;本机用户 `{ kind: 'owner' }` | 一个 principal 对应 N 个 peer |
+| 连接层 | peer:一条连接的生命周期作用域 | 浏览器连接、liveshare 读者的一条连接 | 每条连接一个 peer |
+
+| 事项 | 归属层 |
+|---|---|
+| 方法级权限判定、access hooks、访问快照 | principal |
+| `source.principal` 落盘 | principal |
+| 订阅、stream、连接期注册(`peer.ctx`) | peer |
+| 撤销授权 | 两层:撤权后 policy 不为该 principal 给出 binding,其调用一律被拒;创建方 dispose 其名下全部 peer |
+
+principal→peers 的对应表由创建方持有,例如 liveshare 每份授权一个对象,记录该授权下建立的全部 peer。
+
+### Peer 模型
+
+只有 Peer 一个概念、一个实现,删除 operator 概念。
+
+```text
+// @deepseek-ai/dsh-typert-protocol
+export interface PeerScope {
+ readonly id: PeerId
+ /** Connection-lifetime registrations; disposed with the Peer. */
+ readonly ctx: Context
+ /** Caller identity fixed at creation by in-process Host code; no wire field can set it. */
+ readonly principal: Principal
+ /** Aborted once the Peer is disposed; Remote streams that speak for the Peer end with it. */
+ readonly signal: AbortSignal
+ /** Forwarded human-interaction participation; independent of authorization. */
+ readonly interactions: 'answer' | 'none'
+ dispose(): Promise
+}
+
+// @deepseek-ai/dsh-client-connection
+export class Peer implements PeerScope {
+ constructor(owner: Context, options: { readonly principal: Principal; readonly interactions?: 'answer' | 'none' })
+}
+
+interface HostConnectionHandle {
+ /** The Peer every authenticated browser connection shares; principal { kind: 'owner' }. */
+ readonly browserPeer: PeerScope
+ /** Network admission: the admitted Peer or the refusal status. */
+ admit(request: ConnectionTrustRequest): PeerAdmission
+ /** In-process only; disposed with the Context that read `ctx.connection`, or earlier by dispose(). */
+ createPeer(options: { readonly principal: Principal; readonly interactions?: 'answer' | 'none' }): PeerScope
+}
+
+interface ConnectionFetchHandler {
+ fetch(request: Request, peer: PeerScope): Promise
+}
+bridge(req, res, apiHandler, peer, maxRequestBodyBytes?)
+
+// @deepseek-ai/dsh-api-gateway
+interface InvokeRemoteRequest { …; readonly peer: PeerScope } // was `peer?`
+wireStream.open(endpoint, payload, uplink, peer: PeerScope, signal) // was `PeerScope | undefined`
+```
+
+| 规则 | 说明 |
+|---|---|
+| 每条路径显式给出 Peer | 网络路径由 `admit()` 的结果给出;进程内路径由调用方传入。Gateway 删除 `operatorPeer()` 与 `request.peer ?? …`,不再自建 Peer |
+| 浏览器连接共享 `browserPeer` | 所有浏览器连接共享一个 Peer,每个使用处都显式引用 `connection.browserPeer`,不存在兜底变量;页内 Host(webworker-runtime)同样显式传 `browserPeer` |
+| Host 进程内自动化 | 需要调用 Remote 方法时用 `createPeer({ principal })` 创建自己的 Peer。目前没有这类生产调用方,因此不预置固定的 Host Peer |
+| 多 Peer | `createPeer()` 每次返回独立 Peer;liveshare 将用它为读者的每条连接建一个 Peer(在 #3854 侧接入) |
+| 交互参与 | `Peer(owner, { principal, interactions? })` 与 `createPeer()` 省略 interactions 时默认 `answer`。`none` 对所有 principal 拒绝 `$events` 与 `$events/result`;交互模式不赋予方法权限。回答型 owner 能接收并回答 waterfall,非 owner 只能接收 RBAC 授权的广播,不能回答 |
+| exact Fetch route | file-upload、present-open、media-references、session-log-export、inspector 等 exact route 不经 Gateway;共享 Fetch handler 对非 owner Peer 一律返回 403 |
+
+### 身份类型
+
+```text
+// @deepseek-ai/dsh-typert-protocol
+
+/**
+ * Merge-extensible caller identities. Each driver declares its own entry.
+ * Every entry is a JSON value with a unique literal `kind`, carries no
+ * credential, and is safe to persist in a Session log.
+ */
+export interface PrincipalMap {
+ owner: { readonly kind: 'owner' }
+}
+export type Principal = PrincipalMap[keyof PrincipalMap]
+
+/**
+ * @param invocation - `ctx.invocation`; `undefined` outside a Remote call.
+ * @returns the caller's principal when it is not the owner; `undefined` for the
+ * owner and for trusted in-process code outside a Remote call.
+ */
+export function principalOf(invocation: RemoteInvocation | undefined): Principal | undefined
+```
+
+读取入口统一为 `this.ctx.invocation.peer.principal`;owner 判断就地写 `principal.kind === 'owner'`,未知 kind 永远不是 owner。外部 driver 在自己的包内用声明合并扩展 `PrincipalMap`;liveshare 的 `share-viewer{ shareId, actor }` 在 #3854 侧声明,此处不改 liveshare。
+
+### Peer 生命周期与撤权
+
+| 事件 | 行为 |
+|---|---|
+| Gateway 分派 | 第一步检查 `peer.signal.aborted`,已 dispose 即以 `gateway/peer-disposed` 拒绝。对 HTTP、WebSocket 与进程内路径都生效;wire 路径在读取 payload 之前检查,已 dispose 的 Peer 携带畸形 payload 也得到同一错误码 |
+| Peer dispose 时的 stream | Remote stream 方法看到的 signal 并入 `peer.signal`,`$events` 流的生命周期同样并入;WebSocket 照旧以 1001 关闭 |
+| Peer dispose 时的 unary | 已在执行的 unary 调用不被取消;之后的调用被拒绝 |
+| 撤销授权 | 业务包撤权后 policy 不为该 principal 给出 binding,业务包调用 `invalidate(principal)`;创建方按 principal→peers 表 dispose 全部 peer |
+
+### 方法级访问控制
+
+资源类型与 `@Remote` 的 `resources` 读取函数在 `@deepseek-ai/dsh-typert-protocol`;资源种类在 typert 中与 lookups、Contexts 并列:`TypertResourceMap` 声明种类,`typert.resources` 保存每个种类的默认 wire 字段,lookup 与 Host Context adapter 声明其 wire 身份所属的资源种类。Gateway 把一次调用或一个转发事件解析为它访问的资源。判定、列表过滤与访问快照在新包 `@deepseek-ai/dsh-api-remote-access`(`packages/api/remote-access`),该包不读取任何参数字段。Gateway 与领域包对 remote-access 只做 type-only 依赖并 inject `remoteAccess`,值依赖只有 `dsh-typert-protocol`,不为 remote-access 新增 `package-dependency-policy` 条目。role、binding 与 policy 由业务包定义和注册,liveshare 为首个使用方。除内置的 `global` 外,remote-access 与 Gateway 中不出现任何资源种类名,符合 [`packages/api` 只承载通用编排](../../../../docs/defensive-patterns/remote-and-api.zh.md) 的评审规则。
+
+#### 与 RBAC 的对应
+
+| 本设计 | RBAC / IAM 术语 | 含义 |
+|---|---|---|
+| principal | Subject / Principal | 调用方身份,`PrincipalMap` 的一个条目 |
+| Peer | Session(RBAC 会话) | principal 的一条连接;一个 principal 对应 N 个 Peer |
+| owner | 超级用户 | 跳过全部检查 |
+| action | Action / Permission | Remote 端点 `ns/method`,或转发事件 `event:`;Gateway 内部仍称端点 |
+| resource | Resource / Object | `{ kind: 'global' }`,或某个已声明资源种类的 `{ kind, id }` |
+| role | Role | 限定一种资源种类的 action 集合,是业务包的普通值 |
+| binding | RoleBinding(角色指派) | principal 在某个资源上拥有某个 role,可附 condition |
+| condition | Condition(IAM Conditions) | 挂在 binding 上,按请求的 action、资源与参数放行或带原因拒绝 |
+| policy | Allow policy | 业务包为一种非 owner principal kind 给出 binding 与可选 `explain` |
+| explain | 拒绝说明 | 没有 binding 放行时给出拒绝原因,不改变结论 |
+| Gateway | PEP(Policy Enforcement Point) | 解析资源、执行结论 |
+| remote-access | PDP(Policy Decision Point) | 计算结论、列表过滤与访问快照 |
+
+| 规则 | 内容 |
+|---|---|
+| 默认拒绝 | 非 owner 只有在某个 binding 的 role 显式列出该 action 时才被放行;role 未列出的 action、没有 binding 的资源、没有 policy 的 principal kind 一律被拒。所有 Remote 方法都受访问控制,没有单独的可访问标记 |
+| 资源之间无继承 | `global` 不是其他资源的上级:`global` 上的授权不蕴含任何会话或工作区上的授权,工作区上的授权也不蕴含其中会话的授权 |
+| `global` 授权即显式全局授权 | 调用解析不出任何资源即访问 `global`。把某个 action 放进 `global` 级 role,等于允许该 principal 不受资源限制地调用它,属高危授权;方法被放行后仍可在内部用 `allows(resource)` 逐项细判 |
+
+#### 资源与资源读取(`@deepseek-ai/dsh-typert-protocol`、`@deepseek-ai/dsh-typert-registry`)
+
+```text
+/** Merge-extensible resource kinds: the key is the kind, the value declares its identity type. */
+export interface TypertResourceMap {}
+// @deepseek-ai/dsh-session/types: session: TypertResource
+// @deepseek-ai/dsh-workspace/types: workspace: TypertResource
+
+// Runtime registries in typert
+ctx.typert.resources.register(kind, { field }): TypertDisposer
+ctx.typert.lookups.register(key, { parameter, wire, resource?, … }) // resource: the kind its wire id names
+ctx.typert.contexts.registerHost(key, { wire, resource?, … }) // a TypertHostContextAdapter
+
+export interface TypertHostContextAdapter {
+ readonly wire: string
+ readonly wireTypeSymbol: string
+ /** Resource kind the wire identity names; access checks read the identity from args before resolve(). */
+ readonly resource?: TypertResourceKind
+ resolve(id: Wire): Context | undefined | Promise
+}
+
+export type RemoteAccessResourceKind = 'global' | TypertResourceKind
+export type RemoteAccessResource =
+ | { readonly kind: 'global' }
+ | { readonly kind: K; readonly id: TypertResourceId } // for each declared kind K
+export type RemoteAccessResourceOf = Extract
+
+/** Per-kind readers, called with the method's positional wire arguments. */
+export type TypertResourceReaders = {
+ readonly [K in TypertResourceKind]?: (...args: WireArgs) => Id | readonly Id[] | undefined // Id = TypertResourceId
+}
+
+export interface RemoteMethodOptions {
+ readonly name?: string
+ readonly mode?: 'stream'
+ readonly resources?: TypertResourceReaders
+}
+
+export function Remote(
+ option: string | RemoteMethodOptions>>,
+): (method: (this: This, ...args: Args) => Result,
+ context: ClassMethodDecoratorContext