Merge remote-tracking branch 'origin/master' into refactor/subagent
# Conflicts: # packages/client/ui-plugin-manager/tests/components.client.spec.tsx # packages/subagent/subagent/src/list-children.ts
This commit is contained in:
commit
1829f2f74a
209 files changed
+5926
-1591
No files matched your search
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.md
|
||||
2026-08-25-session-observations-and-projection-owned-client-state.md: 0ff8cbf50c18285092432850ce5222430f5acf0d
|
||||
2026-08-25-session-observations-and-projection-owned-client-state.zh.md: f3fca078d089a9ee1e329fdbb661653053053a91
|
||||
2026-08-25-session-observations-and-projection-owned-client-state.md: 8573ea89fae6f52a9d28cc75261aea529df4f075
|
||||
2026-08-25-session-observations-and-projection-owned-client-state.zh.md: 55c477e994df72ef8f6b6e4bb97b8b35e5783c9b
|
||||
+2
-2
@@ -108,11 +108,11 @@ These distinctions prevent one overloaded `undefined` from representing cache mi
|
||||
| Follow opening baseline | Complete for the Host composition | Exact opening cursor | Capability absent |
|
||||
| Projection frame | One whole key | Event sequence carried by the frame | Not applicable |
|
||||
|
||||
The Client stores one row per key with its sequence number. A newer hint, baseline, or frame replaces a row; an equal or older input is ignored. Reconnect can therefore replace the event window without rolling back a projection frame that was already accepted at a later sequence.
|
||||
The Client stores one row per key. A row the connected Host computed carries its sequence number: a newer baseline or frame replaces it, an equal or older input is ignored, and reconnect can therefore replace the event window without rolling back a projection frame that was already accepted at a later sequence. A list hint viewed from the persisted cache carries no comparable sequence number and yields to every Host-sequenced write ([projection cache listing identity and cached rows](2026-09-19-projection-cache-listing-identity-and-cached-rows.md)).
|
||||
|
||||
The list view reads the same per-Session store as the opened Session. Hints can populate title, preset, and other list presentation before follow completes; the opening baseline then converges that state without creating a second summary-only authority.
|
||||
|
||||
The per-Session Client projection store accepts list hints, the follow baseline, and later whole-value frames under one higher-sequence-wins rule. It never folds Session events. A baseline or frame may advance a hinted value, while an older cut cannot overwrite a newer row.
|
||||
The per-Session Client projection store accepts list hints, the follow baseline, and later whole-value frames. Higher-sequence-wins applies among Host-sequenced values; cached list hints sit below all of them. It never folds Session events. A baseline or frame may advance a hinted value, while an older cut cannot overwrite a newer sequenced row.
|
||||
|
||||
Data that is not derived from one Session remains outside projections. `session/modelCatalog` owns the Host-generation model catalog, and `agentPresets/list` owns the configurable preset roster. A selector combines the relevant catalog with the Session's `modelSelection` or `agentPreset` projection only when both inputs are ready. During refresh it may retain the last complete catalog; before the first complete pair it reports loading instead of rendering a guessed name or availability verdict.
|
||||
|
||||
|
||||
+2
-2
@@ -108,11 +108,11 @@ Projection 的三种交付状态含义不同:
|
||||
| Follow opening baseline | 对当前 Host composition 完整 | 精确 opening cursor | Capability 不存在 |
|
||||
| Projection frame | 单个完整 key | Frame 携带的 event sequence | 不适用 |
|
||||
|
||||
Client 为每个 key 保存带 sequence number 的一行。更新的 hint、baseline 或 frame 会替换 row;相同或更旧的输入被忽略。因此 reconnect 可以替换 event window,而不会回退已经在更晚 sequence 接受的 projection frame。
|
||||
Client 为每个 key 保存一行。由建连 Host 算出的行带 sequence number:更新的 baseline 或 frame 会替换它,相同或更旧的输入被忽略,因此 reconnect 可以替换 event window,而不会回退已经在更晚 sequence 接受的 projection frame。从持久化缓存看出来的 list hint 不带可比的 sequence number,让位于所有 Host 排序过的写入([Projection cache 只读面按 lifecycle 身份匹配,客户端 store 区分 cached 与 sequenced 行](2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md))。
|
||||
|
||||
List view 与已打开 Session 读取同一个 per-Session store。Hints 可以在 follow 完成前填充 title、preset 和其他 list presentation;opening baseline 随后收敛这份状态,而不会建立第二套 summary-only authority。
|
||||
|
||||
每个 Session 的 Client projection store 按一条 higher-sequence-wins 规则接收 list hints、follow baseline 和后续 whole-value frame。它从不折叠 Session event。Baseline 或 frame 可以推进 hinted value,较旧切面不能覆盖较新的 row。
|
||||
每个 Session 的 Client projection store 接收 list hints、follow baseline 和后续 whole-value frame。higher-sequence-wins 只在 Host 排序过的值之间生效;cached list hint 排在它们全部之下。它从不折叠 Session event。Baseline 或 frame 可以推进 hinted value,较旧切面不能覆盖较新的 sequenced row。
|
||||
|
||||
不由单个 Session 派生的数据不进入 projection。`session/modelCatalog` 持有当前 Host generation 的 model catalog,`agentPresets/list` 持有可配置 preset roster。Selector 只在相应 catalog 与 Session 的 `modelSelection` 或 `agentPreset` projection 均就绪后组合两者。刷新时可以保留上一份完整 catalog;第一次获得完整输入前显示 loading,而不是展示猜测的名称或可用性结论。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-02-projcache-cross-version-read-compat.md
|
||||
2026-09-02-projcache-cross-version-read-compat.md: 82f7f26d5cd0484f5adcd51db17748f41a298294
|
||||
2026-09-02-projcache-cross-version-read-compat.zh.md: 58705ec8f15fbffff6af31872a69f1f19ef5e3c5
|
||||
2026-09-02-projcache-cross-version-read-compat.md: 160c5c796bacc6f672b06acc0df4246d7008c52b
|
||||
2026-09-02-projcache-cross-version-read-compat.zh.md: cff7742f6c1f887b905a2c139e4157782aef677c
|
||||
+1
-1
@@ -39,7 +39,7 @@ Declared read compatibility — reads tolerate vouched-for older versions, write
|
||||
5. **Identity matching is stricter than structural admission**: an absent `formatVersion` never matches a current Session, so predecessor rows cannot seed a projection and refold from the authoritative log. Once the format matches, `identityMatches` normalizes absent lineage to unseeded (`?? false` / `?? 0`): exact for an unseeded session, while a seeded expectation fails the match. Poisoned v5 homes therefore boot safely, but their unbound rows are not exposed as current values.
|
||||
6. **Schema-validation backstop: `invalidRecords: 'backup-and-skip'` (declared by this domain only)**. A stored record that still fails to parse beyond read compatibility no longer refuses the whole domain: the domain layer calls the backend's `KvUnit.backupRecord` (json per-record implementation = rename the document to `<key>.json.bak.<YYYYMMDDHHmm>`, bytes kept, never read again), prints the concrete failure with `logger.error` (domain, table, key, destination, zod cause), and continues the open with the record absent; the next cold read rebuilds and rewrites that session's cache. **The policy is an explicit per-domain declaration and the default stays fail-loud** — other domains still refuse the whole load on invalid stored data, and a backend without `backupRecord` (single layout, row stores) also falls back to fail-loud. Naming history: quarantine → backup-and-skip (user ruling: the word must carry both "back up" and "skip", sharing its root with the `.bak` suffix; skip-backup was rejected because the CLI `--skip-X` convention reads it as "do not back up"). For this domain it supersedes the reset/destroy recovery path of the [2026-07-28 storage recovery proposal](../../proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md), which stays live for authoritative and whole-medium damage.
|
||||
|
||||
7. **A predecessor title is a listing hint, not a fold shortcut**: Session-list startup remains metadata/cache-only and never opens cold log bodies. The log header is authoritative; a lifecycle-matching checkpoint is a durable-prefix witness that may lag but cannot lead the log. `cachedPredecessorTitle` therefore exposes only a predecessor `title` row that still passes the current title unit's `stateVersion` and schema. Both adjacent Session-format edges preserve title text. The hint uses `asOfSeq: -1` rather than the stored row sequence because a cardinality-changing log migration can remap that coordinate. Other predecessor rows remain hidden, and `hydratePrepared`/`coldSnapshot` retain strict format identity, because normalizers can change values such as `blank` or `lastPromptAt` even when storage is physically consistent.
|
||||
7. **A predecessor title is a listing hint, not a fold shortcut**: Session-list startup remains metadata/cache-only and never opens cold log bodies. The log header is authoritative; a lifecycle-matching checkpoint is a durable-prefix witness that may lag but cannot lead the log. `cachedPredecessorTitle` therefore exposes only a predecessor `title` row that still passes the current title unit's `stateVersion` and schema. Both adjacent Session-format edges preserve title text. The hint carries the stored title row's own watermark as `asOfSeq`; because a cardinality-changing log migration can remap that coordinate, the Session list labels the block `kind: 'cached'` and the client never compares it with the connected Session's seqs ([listing identity and cached rows](2026-09-19-projection-cache-listing-identity-and-cached-rows.md)). Other predecessor rows remain hidden, and `hydratePrepared`/`coldSnapshot` retain strict format identity, because normalizers can change values such as `blank` or `lastPromptAt` even when storage is physically consistent.
|
||||
|
||||
### v3-v6 → v7 disposition
|
||||
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ v4→v5 的唯一实质差异是 identity 新增两个 lineage 字段;v6 只
|
||||
5. **identity 匹配比结构准入更严格**:缺失 `formatVersion` 的记录绝不匹配当前 Session,因此前代行不能播种投影,而会从权威日志重新折叠。格式匹配后,`identityMatches` 才把缺失 lineage 归一化为 unseeded(`?? false` / `?? 0`):对 unseeded 会话精确,对 seeded 期望则匹配失败。v5 投毒 home 因而可以安全启动,但其未绑定行不会作为当前值暴露。
|
||||
6. **schema 校验兜底:`invalidRecords: 'backup-and-skip'`(仅本域声明)**。读兼容之外仍然解析失败的存量记录不再让整个域拒开:domain 层调用后端的 `KvUnit.backupRecord`(json per-record 实现=把文档改名为 `<key>.json.bak.<YYYYMMDDHHmm>`,字节留档、不再被读取),用 `logger.error` 打印具体失败信息(域名、表、键、移动去向、zod 失败原因),随后当该记录不存在继续启动;下一次冷读会重建并重写该会话的缓存。**该策略是域级显式声明,缺省仍为 fail-loud**——其他业务域的存量数据校验失败照旧整域拒载;后端没有 `backupRecord` 能力(single 布局、行存储)时也回退 fail-loud。命名沿革:quarantine → backup-and-skip(用户裁决:词要同时含"备份"与"跳过"两义,且与 `.bak` 后缀同源;skip-backup 因 CLI `--skip-X` 惯例存在"不备份"反读而弃用)。对本域而言,该策略取代了 [2026-07-28 存储恢复提案](../../proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md)中 reset/destroy 的恢复途径;该提案对权威介质与整介质损坏仍然有效。
|
||||
|
||||
7. **predecessor title 是列表 hint,而不是 fold shortcut**:Session list 启动保持 metadata/cache-only,绝不打开冷 log body。log header 是权威来源;生命周期匹配的 checkpoint 是 durable prefix witness,可以落后但不能领先日志。因此 `cachedPredecessorTitle` 只公开仍通过当前 title unit `stateVersion` 与 schema 的 predecessor `title` row。两条相邻 Session format edge 都保留 title 文本。该 hint 使用 `asOfSeq: -1`,而不使用存储 row 的序号,因为改变日志事件数量的迁移会重新映射该坐标。其他 predecessor row 继续隐藏,`hydratePrepared`/`coldSnapshot` 仍要求严格格式 identity,因为即使物理存储一致,normalizer 仍可能改变 `blank` 或 `lastPromptAt` 等值。
|
||||
7. **predecessor title 是列表 hint,而不是 fold shortcut**:Session list 启动保持 metadata/cache-only,绝不打开冷 log body。log header 是权威来源;生命周期匹配的 checkpoint 是 durable prefix witness,可以落后但不能领先日志。因此 `cachedPredecessorTitle` 只公开仍通过当前 title unit `stateVersion` 与 schema 的 predecessor `title` row。两条相邻 Session format edge 都保留 title 文本。该 hint 的 `asOfSeq` 是存储 title row 自己的水位;因为改变日志事件数量的迁移会重新映射该坐标,Session list 把该 block 标为 `kind: 'cached'`,客户端不会拿它与建连 Session 的 seq 比较([只读面按 lifecycle 身份匹配与 cached 行](2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md))。其他 predecessor row 继续隐藏,`hydratePrepared`/`coldSnapshot` 仍要求严格格式 identity,因为即使物理存储一致,normalizer 仍可能改变 `blank` 或 `lastPromptAt` 等值。
|
||||
|
||||
### v3-v6 → v7 处置
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-15-client-session-references.md
|
||||
2026-09-15-client-session-references.md: 00f87cfb6f358218060a72199888690c3e46a091
|
||||
2026-09-15-client-session-references.zh.md: 7947a8a405d295eb6324ac33f06c0f2bc062c91b
|
||||
2026-09-15-client-session-references.md: 162ef08875e5b0591b3b4099c9bdcadb18e7d175
|
||||
2026-09-15-client-session-references.zh.md: 3af8932f7c002b3b63de21c689ae91e3017a164e
|
||||
@@ -73,7 +73,7 @@ The Controller owns source counts alongside each live generation's references. F
|
||||
|
||||
Each available Session list row exposes a read-only `retainedBy` record from source key to positive reference count. An unretained row has an empty record; zero-count keys are absent. Acquisition failure and release update that projection, including removal of the final key. The source counts remain local facts when Host metadata is refreshed; Host responses cannot overwrite them.
|
||||
|
||||
The generation owns the counts even if its Session has not reached the Host catalog or its catalog metadata has been removed. `byId` synthesizes a local fallback row for every live generation so Provider and ownership consumers can resolve it; `ids` remains the Host-list membership and ordering. A fallback row is not Host metadata. Every row's `retainedBy` projection uses the live generation's counts. Neither the reference objects nor the counts are persisted or sent to the Host.
|
||||
The generation owns the counts even if its Session has not reached the Host catalog or its catalog metadata has been removed. `byId` contains Host summaries, projected child-catalog rows, and fallback rows for retained subagents with a known direct-parent address. Acquisition publishes a missing subagent row synchronously so Provider discovery does not depend on parent-catalog arrival; title projections and reference-source counts continue to update that row. Ordinary retained Sessions do not synthesize fallback rows. `ids` remains the Host-list membership and ordering. Consumers of an unlisted generation use its binding and `retainInfo` directly. Every row's `retainedBy` projection uses the live generation's counts. Neither the reference objects nor the counts are persisted or sent to the Host.
|
||||
|
||||
For example, `retainedBy = { mainView: 1, gateway: 2 }` means that the main view and two Host invocations own three references. Releasing the main-view reference removes only `mainView`; the Gateway invocations continue to own the generation. Source names in this example are consumer keys, not a closed Controller enum.
|
||||
|
||||
@@ -141,7 +141,7 @@ Scoped business objects capture their binding before awaiting work. They do not
|
||||
|
||||
### Main-area navigation and presentation
|
||||
|
||||
`uiWorkspace.openSession`, `openWorkspace`, `forkSession`, and `startSession` remain the navigation entry points. They accept or resolve explicit targets, change the main view, and return the main area to Conversation according to existing navigation policy. `retain` itself never navigates. There is no `registerNavigation` receiver protocol or second navigation service introduced by reference ownership.
|
||||
`uiWorkspace.openSession`, `openWorkspace`, and `startSession` are the navigation entry points. They accept or resolve explicit targets, change the main view, and return the main area to Conversation according to existing navigation policy. Sidebar `forkSession` creates and renames the child without retaining it or changing selection. `retain` itself never navigates. There is no `registerNavigation` receiver protocol or second navigation service introduced by reference ownership.
|
||||
|
||||
The existing `uiWorkspace` implementation directly owns the main-area reference and target. Its navigation methods update that owner rather than calling a receiver registered by Conversation. `ui-session` derives the root Provider binding from the source marker; the main Conversation and associated right Sidebar only inherit the Provider and neither depend on `uiWorkspace` nor see the main reference. An independent Sidebar Conversation establishes a nested Provider from its own reference and overrides only that subtree's binding. The main reference is not a global standard prop, subtree Hook, or default value for ID-based lookup.
|
||||
|
||||
@@ -160,7 +160,7 @@ Source metadata does not change when DOM focus moves or when a global panel hide
|
||||
|
||||
Conversation retains its `hero`, `settling`, and `active` composition and existing history-loading and `openError` handling. Acquisition adds no outer loading/error phase presentation, extra composer-hiding condition, Retry button, or replacement Sidebar recovery panel. Existing error handlers continue to handle their errors; call sites without error presentation gain none. Promise rejection and correct reference release do not imply an additional UI handler.
|
||||
|
||||
Workspace connection and fork preserve their existing navigation guards and panel-switch invalidation. Direct Session opening gains no additional global-navigation cancellation policy. Agent Team refresh preserves its originating-selection validity condition instead of starting a global navigation token before refresh. Reference acquisition does not broaden cancellation to unrelated navigation or running operations, and local cancellation does not roll back Host effects.
|
||||
Workspace connection preserves its navigation guards and panel-switch invalidation. Sidebar fork does not supersede an in-flight navigation. Direct Session opening gains no additional global-navigation cancellation policy. Agent Team refresh preserves its originating-selection validity condition instead of starting a global navigation token before refresh. Reference acquisition does not broaden cancellation to unrelated navigation or running operations, and local cancellation does not roll back Host effects.
|
||||
|
||||
### Presets and creation flows
|
||||
|
||||
@@ -221,6 +221,6 @@ Consumers must keep references until their real completion point; an unreleased
|
||||
|
||||
Final release discards non-persisted binding-owned state, including loaded history pages, Chat scroll anchors, preview wrapping, Files expansion, composer attachments, and undo history. Only persisted Session-keyed Store values or a generation kept alive by another reference survive a view switch; the runtime does not clear those persisted values.
|
||||
|
||||
Every public `retain`, including the temporary references used by Session rename and fork-title assignment, starts the generation's shared initial history opening. These metadata operations await `reference.ready` before using the Session and therefore pay that history I/O for a cold generation.
|
||||
Every public `retain`, including the temporary reference used by ordinary Session rename, starts the generation's shared initial history opening. Fork-title assignment instead sends rename directly and applies its returned projection without a Client reference or history I/O. A metadata-only fork operation therefore cannot create a temporary list row or switch the main view.
|
||||
|
||||
The catalog, UI status, and view target have separate owners and can publish independently. Their consumers must not infer a lifecycle transition solely from notification order. The main view remains an ordinary reference owner, while its navigation and presentation rules stay in UI rather than the reference allocator.
|
||||
@@ -73,7 +73,7 @@ Controller 将来源计数与每个活跃代的引用共同管理。每个来源
|
||||
|
||||
每条已有 Session 列表记录公开只读 `retainedBy`,由来源键映射到正数引用计数。未被持有的记录使用空对象;零计数键不存在。获取失败与释放更新该投影,包括删除最后一个来源键。Host 元数据刷新时,来源计数仍是本地事实,Host 响应不能覆盖它。
|
||||
|
||||
即使 Session 尚未进入 Host 目录或其目录元数据已被移除,计数仍由对应代持有。`byId` 为每个活跃代合成本地兜底记录,以便 Provider 与所有权使用方解析它;`ids` 仍表示 Host 列表成员关系与顺序。兜底记录不是 Host 元数据。每条记录的 `retainedBy` 投影均使用活跃代的计数。引用对象与计数均不持久化,也不发送给 Host。
|
||||
即使 Session 尚未进入 Host 目录或其目录元数据已被移除,计数仍由对应代持有。`byId` 包含 Host 摘要、子会话目录投影行,以及已保留且具有明确直接父子地址的 subagent 兜底行。获取引用时同步发布缺失的 subagent 行,使 Provider 发现不依赖父目录到达;标题投影和引用来源计数继续更新该行。普通被保留 Session 不合成兜底行。`ids` 仍表示 Host 列表成员关系与顺序。未列出 generation 的使用方直接读取其 binding 与 `retainInfo`。每条记录的 `retainedBy` 投影均使用活跃代的计数。引用对象与计数均不持久化,也不发送给 Host。
|
||||
|
||||
例如,`retainedBy = { mainView: 1, gateway: 2 }` 表示主视图与两个 Host 调用持有三份引用。释放主视图引用只移除 `mainView`;Gateway 调用继续持有该代。示例中的来源名是使用方键,不是 Controller 内封闭的枚举。
|
||||
|
||||
@@ -141,7 +141,7 @@ Conversation 的引用属于其视图所有者,不属于 Chat、Trajectory 或
|
||||
|
||||
### 主区域导航与展示
|
||||
|
||||
`uiWorkspace.openSession`、`openWorkspace`、`forkSession` 和 `startSession` 仍是导航入口。它们接受或解析明确目标,改变主视图,并按既有导航策略将主区域返回 Conversation。`retain` 本身从不导航。引用所有权不引入 `registerNavigation` 接收者协议,也不引入第二个导航服务。
|
||||
`uiWorkspace.openSession`、`openWorkspace` 和 `startSession` 是导航入口。它们接受或解析明确目标,改变主视图,并按既有导航策略将主区域返回 Conversation。侧边栏 `forkSession` 创建并重命名子会话,不 retain 子会话,也不改变选择。`retain` 本身从不导航。引用所有权不引入 `registerNavigation` 接收者协议,也不引入第二个导航服务。
|
||||
|
||||
现有 `uiWorkspace` 实现直接持有来源为 `mainView` 的主区域引用与目标。导航方法直接更新该所有者,不调用 Conversation 注册的接收者。`ui-session` 根据来源标记把该引用对应的 binding 注入根 Provider;主 Conversation 和关联右栏只继承 Provider,既不依赖 `uiWorkspace`,也看不到主引用。独立 Sidebar Conversation 以自己的 reference 建立嵌套 Provider,并覆盖本子树的 binding。主引用不是全局标准 prop、子树 Hook 或按 ID 查询的默认值。
|
||||
|
||||
@@ -160,7 +160,7 @@ DOM 焦点移动或全局面板隐藏仍被持有的视图时,来源元数据
|
||||
|
||||
Conversation 保留 `hero`、`settling`、`active` 组合与既有历史加载和 `openError` 处理。获取引用不增加外层 loading/error 阶段展示、额外隐藏 composer 的条件、Retry 按钮或替换 Sidebar 内容的恢复面板。已有错误处理方继续处理自己的错误;没有错误展示的调用点不增加展示。Promise 拒绝与正确释放引用不意味着额外增加 UI 处理方。
|
||||
|
||||
Workspace 连接和 fork 保持既有导航检查与面板切换失效规则。直接打开 Session 不增加全局导航取消策略。Agent Team 刷新保留发起时选择仍然有效的条件,不在刷新前启动全局导航 token。引用获取不扩大取消范围,不影响无关导航或正在执行的操作;本地取消不回滚 Host 效果。
|
||||
Workspace 连接保持既有导航检查与面板切换失效规则。侧边栏 fork 不替代尚未完成的导航。直接打开 Session 不增加全局导航取消策略。Agent Team 刷新保留发起时选择仍然有效的条件,不在刷新前启动全局导航 token。引用获取不扩大取消范围,不影响无关导航或正在执行的操作;本地取消不回滚 Host 效果。
|
||||
|
||||
### 预设与创建流程
|
||||
|
||||
@@ -221,6 +221,6 @@ Gateway 持有本地引用,直到 handler 使用与回复结算均结束。Con
|
||||
|
||||
最后一份 reference 释放后,未持久化的 binding 自有状态会被丢弃,包括已加载的历史页、Chat 滚动锚点、预览换行、Files 展开状态、composer 附件和 undo 历史。只有持久化的 Session-keyed Store 值或由另一份 reference 保活的 generation 能跨视图切换保留;runtime 不会清除这些持久值。
|
||||
|
||||
每次公开 `retain` 都会启动该 generation 共享的首次历史打开,包括 Session 重命名和 fork 标题设置所用的临时引用。这些元数据操作在使用 Session 前等待 `reference.ready`,因此冷 generation 会承担这次历史 I/O。
|
||||
每次公开 `retain` 都会启动该 generation 共享的首次历史打开,包括普通 Session 重命名所用的临时引用。Fork 标题设置直接发送 rename 并应用返回的投影,不获取 Client 引用,也不读取历史。因此,仅修改元数据的 fork 操作不会创建列表临时行或切换主视图。
|
||||
|
||||
目录、UI 状态与视图目标由不同所有者管理,可以独立发布。使用方不能仅从通知顺序推断生命周期变化。主视图仍是普通引用所有者,其导航与展示规则留在 UI,不进入引用分配器。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-session-row-menu-actions-slot.md
|
||||
2026-09-17-session-row-menu-actions-slot.md: 034144b2b7c9d260f41565d3835687e52353c381
|
||||
2026-09-17-session-row-menu-actions-slot.zh.md: 6ad15d263fd41a0ad97a386506c8f581290d032f
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Session row actions as slot lists
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-17-session-row-menu-actions-slot.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Session row's "..." menu and its hover buttons were closed lists owned by `ui-workspace`: the browser built the menu `items` by row state, rendered the pin and archive buttons itself, and dispatched every verb through callbacks it threaded down the tree, together with the toast and rename dialog those verbs needed. A client plugin could add a sidebar control of its own, but not an action beside the shipped ones without editing the owning package or copying the menu interaction.
|
||||
|
||||
## Decision
|
||||
|
||||
`ui-workspace` declares two root-scoped `list` slots under its `sidebar.workspaces` registration — `sidebar.workspaces.session.menu.item` for the menu rows and `sidebar.workspaces.session.row.action` for the hover buttons — and registers its own actions into them from `apply` exactly as a client plugin would: `pin` (menu 100, button 200), `rename` (200), `fork` (300), `archive` (menu 400, button 100). The lists are the menu and the button strip: entries render in ascending `order`, a plugin action lands wherever its order says, and reusing a shipped id at another `priority` shadows that action through the registry's ordinary cell rules. There is no separate "extension group": the owner draws no separator, and a group start is the entry's own `separatorBefore`.
|
||||
|
||||
An entry receives only the row identity, `{ sessionId, displayTitle }`, and owns everything else. It reads the Host state it cares about through hooks its own face injects (`usePinned` and `useArchived`, Sets derived once per Workspace snapshot) and decides its own visibility from it: pin renders nothing on an archived row because the Host keeps the two sets exclusive; archive never consults pin. Its whole behavior lives in its registration's `inject` face, so a component only calls one callback: `pinSession` delegates to `uiWorkspace.pinSession`, which performs the Host pin and then fronts the Session in its accounts' saved orders with `pinSessionOrder`, reading the memberships current at completion from the object-layer snapshots (`pin-order.ts`), beside `archiveSession`, which the same service already owned; the archive action's callback adds the notice. The service writes view order through the viewing-store instance the apply creates and hands the browser's registration as its handle, the pattern `ui-layout` uses for its layout store. The registry-global pin and archive sets reach the entries as Sets through the faces' `hooks`, derived once per Workspace snapshot, so a row reads its own membership with one lookup.
|
||||
|
||||
The surfaces an action raises outlive the row menu, so they are this package's `shell.overlay` entries: the rename dialog and the row-action notice. An action reaches its surface through the plugin's own apply-closure observable, injected as a callback on the action side (`requestSessionRename`, `notify`) and as a `hooks` source on the surface side (`useRenameRequest`, `useToast`). The browser raises the same rename request from a title double-click and the same notice when an archived row is clicked; the archived notice's second action switches the archived filter to "show" through the same store instance, since the browser's view-options menu is no longer in the loop.
|
||||
|
||||
The shipped menu entries render `MenuItemButton`, the `ui-primitives` row component for menus whose rows are components: a plain row, markup and styling only. Any `role="menuitem"` button joins the menu's keyboard walk, submenu exclusivity, and focus return, because `Menu` decides all three from the DOM (the arrow walk queries the rows; submenu collapse and the post-selection focus return run on the list's own bubbles, for data rows and component rows alike) — a `cordis-client-runner` closure, which cannot import the primitive, renders such a button directly. Closing is the owner's state for both kinds of row: data rows through the owner's `onSelect`, component rows through the slot-level `useMenuOpenState` hook — the declaration carries `inject: { hooks: { menuOpenState } }`, `SessionNodeItem` passes its `[open, setOpen]` pair as the occurrence's `hookContext`, and the factory hands the pair back. That is the channel `sidebar.right.pane.tab` already uses for `useTabInfo`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**A React context between `Menu` and the row component.** Rejected: business components see zero React contexts ([client rules](../../../../packages/client/AGENTS.md)); the Menu-to-row relationship is parent-to-child through the slot system, so its control travels through the slot channels.
|
||||
|
||||
**Owner-supplied row capabilities.** Passing `pinned`/`archived` and `pin`/`archive`/`rename`/`fork` callbacks to entries, through owner props or a slot-level hook, keeps the browser the owner of every verb and makes the shipped actions thin wrappers over it. Rejected: these are not capabilities of a menu; each action's behavior is that action's own, and an entry can read the Host state it needs directly.
|
||||
|
||||
**A dedicated slot appended below the shipped rows.** Rejected: it makes the shipped rows a privileged group a plugin can only follow, and it needs a second rendering path in the primitive for what is one ordered list.
|
||||
|
||||
**Entries that compute their own view-order writes.** A pin entry subscribing to the whole Session list and Workspace snapshot to assemble `pinSessionOrder`'s inputs re-renders every visible row on every list change and puts ordering detail into a menu row. Rejected: the injected callback owns the write and reads the snapshots once, at completion.
|
||||
|
||||
**Declared shared stores for the rename request and the notice.** The request and the notice are one plugin's transient facts, so they travel as apply-closure observables through the entries' own inject faces.
|
||||
|
||||
## Consequences
|
||||
|
||||
Plugins add, reorder, or shadow Session row actions in the menu and in the hover strip without editing `ui-workspace`, and the shipped actions follow the same rules and the same registration path. `WorkspaceBrowser` no longer threads action callbacks, renders a toast, or hosts the rename dialog; `Menu` grew `children` and made `items`/`onSelect` optional; `shell.overlay` gained two `ui-workspace` entries. The archived notice applies the "show archived" filter directly instead of opening the view-options menu.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Session 行 action 即 slot 列表
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-17-session-row-menu-actions-slot.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Session 行的 "..." 菜单和行尾悬停按钮原本是 `ui-workspace` 持有的封闭列表:browser 按行状态拼菜单 `items`、自己渲染置顶和归档按钮、把每个动作的回调一路传下去,连这些动作需要的 toast 和重命名对话框也由它持有。客户端插件可以自己添加侧边栏控件,却无法在不修改 owner 包或复制菜单交互的情况下把 action 放到内置项旁边。
|
||||
|
||||
## 决策
|
||||
|
||||
`ui-workspace` 在其 `sidebar.workspaces` 注册项下声明两个 root-scoped `list` slot——菜单行用 `sidebar.workspaces.session.menu.item`,悬停按钮用 `sidebar.workspaces.session.row.action`——并在 `apply` 里以客户端插件的同一方式注册自己的 action:`pin`(菜单 100、按钮 200)、`rename`(200)、`fork`(300)、`archive`(菜单 400、按钮 100)。列表就是菜单和按钮条:条目按 `order` 升序渲染,插件 action 落在其 order 所指的位置,以另一个 `priority` 复用内置 id 则按注册表的普通 cell 规则遮蔽。没有单独的"扩展分组":owner 不画分隔线,分组起点是条目自己的 `separatorBefore`。
|
||||
|
||||
条目只接收行身份 `{ sessionId, displayTitle }`,其余一切自己负责。它用自己 face 注入的 hook 读自己关心的 Host 状态(`usePinned` 与 `useArchived`,每次 Workspace 快照只派生一次的 Set),并据此决定是否显示:Host 规定两个集合互斥,所以 pin 在归档行上不渲染;archive 从不参考 pin。整套行为放在注册项自己的 `inject` face 里,组件只调一个回调:`pinSession` 委托给 `uiWorkspace.pinSession`——先做 Host 置顶,成功后用 `pinSessionOrder` 把会话推到其记账保存顺序的最前,成员关系在完成时刻从对象层快照读取(`pin-order.ts`)——和这个服务原本就持有的 `archiveSession` 并列;归档 action 的回调在此之上加上提示。服务经 apply 自己创建、并作为 handle 交给 browser 注册项的那个 viewing store 实例写入视图顺序,与 `ui-layout` 处理 layout store 的方式相同。置顶与归档集合以 Set 形式经 face 的 `hooks` 到达条目,每次 Workspace 快照变化只重建一次,行读自己那一位只需一次查找。
|
||||
|
||||
action 发起的浮层必须活得比行菜单久,所以它们是本包注册在 `shell.overlay` 的条目:重命名对话框和行 action 提示。action 经本插件 apply 闭包里的观察源到达自己的浮层:action 一侧注入为回调(`requestSessionRename`、`notify`),浮层一侧注入为 `hooks` 源(`useRenameRequest`、`useToast`)。browser 在标题双击时发出同一个重命名请求,在点击归档行时发出同一种提示;归档提示的第二个动作经同一个 store 实例把归档筛选切到"显示",因为 browser 的视图选项菜单已不在链路上。
|
||||
|
||||
本包自己的菜单条目渲染 `MenuItemButton`——`ui-primitives` 为"行是组件"的菜单提供的行组件,只是一行普通 markup 与样式。任何 `role="menuitem"` 的按钮都能加入菜单的键盘走位、子菜单互斥与焦点归还,因为 `Menu` 从 DOM 上统一决定这三件事(方向键走位查询行元素;子菜单收起与选中后的焦点归还都在列表自己的冒泡上执行,数据行与组件行一视同仁)——无法 import 该 primitive 的 `cordis-client-runner` 闭包直接渲染这样一个按钮。两类行的关闭都是 owner 的状态:数据行通过 owner 的 `onSelect`,组件行通过 slot 级 `useMenuOpenState` hook——声明带 `inject: { hooks: { menuOpenState } }`,`SessionNodeItem` 把自己的 `[open, setOpen]` 作为该次渲染的 `hookContext` 传入,factory 把这一对原样交回。这正是 `sidebar.right.pane.tab` 用于 `useTabInfo` 的通道。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**在 `Menu` 与行组件之间放一个 React context。** 否决:业务组件不接触任何 React context([client 规则](../../../../packages/client/AGENTS.md));Menu 与行是经 slot 系统建立的父子关系,控制权就该走 slot 的通道。
|
||||
|
||||
**由 owner 提供行能力。** 把 `pinned`/`archived` 和 `pin`/`archive`/`rename`/`fork` 回调经 owner props 或 slot 级 hook 交给条目,会让 browser 继续拥有每个动作,内置 action 沦为薄封装。否决:这些不是菜单的能力,每个 action 的行为属于它自己,需要的 Host 状态条目可以直接读。
|
||||
|
||||
**在内置行下方追加一个专用 slot。** 否决:它把内置行变成插件只能跟在后面的特权分组,还要在 primitive 里为本来只是一个有序列表的东西开第二条渲染路径。
|
||||
|
||||
**让条目自己算置顶顺序写入。** pin 条目为了拼 `pinSessionOrder` 的输入订阅整个 Session 列表和 Workspace 快照,列表一变所有可见行都重渲染,排序细节还进了菜单行。否决:写入归注入的回调,在完成时刻读一次快照。
|
||||
|
||||
**用声明式共享 store 传重命名请求与提示。** 请求与提示是本插件自己的瞬时事实,所以经条目自己的 inject face 以 apply 闭包观察源传递。
|
||||
|
||||
## 后果
|
||||
|
||||
插件无需修改 `ui-workspace` 即可在菜单和悬停条里添加、重排或遮蔽 Session 行 action,内置 action 遵循相同规则、走相同注册路径。`WorkspaceBrowser` 不再传递 action 回调、渲染 toast 或承载重命名对话框;`Menu` 新增 `children`,`items`/`onSelect` 变为可选;`shell.overlay` 多了两个 `ui-workspace` 条目。归档提示直接应用"显示已归档"筛选,不再打开视图选项菜单。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.md
|
||||
2026-09-19-projection-cache-listing-identity-and-cached-rows.md: 61b82e5003ba4b634230941f5d3df65e65491e25
|
||||
2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md: ef49d90bc68ca501f7496adbac1d1aa8a5d706a7
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
# Agent Note: Projection cache read-only face matches the lifecycle identity; the client store separates cached and sequenced rows
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
After a Host process restart, every forked session (`SessionHeader.isSeeded === true`) shows no title in the sidebar session list, `@` reference completion shows only the session id, and list ordering degrades to creation time. Opening the session once restores all of it. On one development machine on 2026-09-19, 44 of 241 projcache records were seeded, all 44 carried a valid `title` row, and the list never read any of them.
|
||||
|
||||
This is neither cache corruption nor a version mismatch. The projcache record's domain version is 7 and its `identity.formatVersion` is 4; those are the cache's own on-disk format generation and the Session log format generation the fold ran under, and both are current. The defect is on the read path.
|
||||
|
||||
### 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 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. All three header-only cache consumers grew the same guard:
|
||||
|
||||
| Consumer | Guard | Fallback | Consequence |
|
||||
|---|---|---|---|
|
||||
| `packages/api/session-controller/src/list.ts` `projectionsFor` | `header.isSeeded ? undefined : cachedSnapshot(header, 0) ?? cachedPredecessorTitle(header, 0)` | none | no title, no `sessionListMetadata`; blank falls back to `false`, ordering falls back to `createdAt` |
|
||||
| `packages/context/session-reference/src/index.ts` `projectedLabels` | same | none | `@` completion labels by id; the title cannot be searched |
|
||||
| `packages/subagent/subagent/src/list-children.ts` `resolveColdIdentity` | same | `observeSession` reads the body | correct result, one body read per seeded child |
|
||||
|
||||
When the guard landed (#3346, 2026-09-01), `list.ts` still had `probeSmallCold`: on a cache miss it read the body when the log file was at most `DEFAULT_COLD_BLANK_PROBE_MAX_BYTES = 1024` bytes. Its purpose was blank-session detection; it covered only logs under 1KB and never applied to an ordinary fork. #3400 (2026-09-03) removed it and the list returned to metadata plus cache only. #4320 (2026-09-19) made forking a first-class feature (`packages/core/session/src/fork.ts`, fork at any seq); every fork is `isSeeded: true` with an exact cut, seeded sessions went from a handful to dozens, all of them cold after a restart, and the gap became visible at once.
|
||||
|
||||
### The cache's two uses and where validation lives
|
||||
|
||||
The read and write faces of `SessionProjectionCache` and their callers:
|
||||
|
||||
| Face | What it does with the record | Writes back | Callers |
|
||||
|---|---|---|---|
|
||||
| `write(session)` | writes the checkpoint at three mandatory points plus a throttle | yes | the cache's own listeners |
|
||||
| `hydratePrepared(session, events)` | uses `rows` as the fold starting point and applies events from `row.seq + 1`; the result lands in live cells | the next checkpoint | `session-query/src/observation.ts` |
|
||||
| `coldSnapshot(meta, cut, events)` | fold starting point plus write-back | yes | no production caller |
|
||||
| `cachedSnapshot(meta, cut, keys?)` | `viewCheckpoint`: each row passes `ver` and `stateSchema`, is `view`ed, and returned | no | `list.ts`, `session-reference`, `list-children` |
|
||||
| `cachedPredecessorTitle(meta, cut)` | the same, `title` only, allowing an older `formatVersion` | no | `list.ts` |
|
||||
|
||||
Identity validation lives entirely inside the cache (`recordFor` → `identityMatches`; `viewCheckpoint` checks `ver` and schema per row). `list.ts` validates nothing itself; it merely declines to call when it cannot supply a cut.
|
||||
|
||||
spec.ts and the README describe the purpose of the identity check with one verb: preventing "seed state folded from an unrelated log", "cannot seed the caller". That guards the fold face. The read-only face has never seeded anything.
|
||||
|
||||
### The client store cannot guarantee that connected data replaces hints
|
||||
|
||||
Each session has exactly one `ProjectionValueStore` on the client (`manager.projectionStores`; its merge rules were recorded in [Session observations and projection-owned client state](2026-08-25-session-observations-and-projection-owned-client-state.md)). The block delivered by the list, the history first-page baseline on open, the control baseline, push frames, and rename results all write into that one object, and `useProjection` reads it. All writers are peers under one higher-seq-wins rule: `apply` drops a new value when `seq <= row.seq`, and `seed` clears omitted keys only when `row.seq <= cut`. A list hint whose seq is equal or higher survives untouched after the session is opened. The hint's seq comes from a disk record; after a crash-repair truncation it can be numerically higher than the connected cursor, which is exactly the case where the cache is wrong and the connection is right. Comparing seqs against a hint is the wrong tool.
|
||||
|
||||
## Decision
|
||||
|
||||
### Two read faces; the cut is compared only on the fold face
|
||||
|
||||
| Face | Identity | Who can supply it | Methods |
|
||||
|---|---|---|---|
|
||||
| read-only face | lifecycle identity: `formatVersion + createdAt + cwd + isSeeded`, all from the header | any header-only caller | `cachedSnapshot(header, keys?)`, `cachedPredecessorTitle(header)` |
|
||||
| fold face | lifecycle identity plus `inheritedEventCount` | callers holding the Session or its body | `hydratePrepared`, `write`, `coldSnapshot`, all through `recordFor` |
|
||||
|
||||
The two read-only methods no longer take a cut parameter. `inheritedEventCount` is still written into every record and the fold face still compares it on full equality; the original design expectation, that records folded under different fork cuts never seed each other, is unchanged.
|
||||
|
||||
In the block the read-only face returns, `asOfSeq` is the lowest watermark among the served rows, the stored record's own position. A header can vouch neither for the cut nor for the comparability of that watermark with the current log, so comparability is not expressed by this number: the Session list adds an independent field `kind` to each summary's `projections` block (`SessionProjectionHints`). A block a cold session's row viewed from projcache is `cached`; a block the Host's live registry computed for an attached session is `sequenced`. The two fields are independent facts: `kind` names the sequence space `asOfSeq` belongs to, and `asOfSeq` is the watermark within that space.
|
||||
|
||||
### Why the read-only face may skip the cut
|
||||
|
||||
- Within one `formatVersion`, a log's cut is determined by `(id, createdAt, cwd, isSeeded)`: it is fixed at fork creation, and only a cardinality-changing format migration can alter it, which necessarily bumps `formatVersion`. The cut is a derived fact of the lifecycle, not an independent coordinate.
|
||||
- What the read-only face actually did until now: unseeded callers passed the constant 0 (true by construction) and seeded callers never called. The cut was never compared against any real fact on this face. Removing it changes the rejection set not at all: a record from another lifecycle (any of the four fields differing) is still rejected, unseeded results are identical, and seeded lifecycles go from unaddressable to addressable.
|
||||
- The read-only face never seeds and never writes back; its output is a pure function of the stored record, the cut plays no part in it, and it flows nowhere.
|
||||
- The one new exposure: a record whose four fields all match but whose cut differs. The system cannot produce it; only copying a session directory by hand and editing files can. Even then the list shows that record's values until the session is opened, and nothing enters any fold.
|
||||
|
||||
### Consumers
|
||||
|
||||
| Location | Change |
|
||||
|---|---|
|
||||
| `list.ts` `projectionsFor` | drop the `isSeeded` branch and `SessionLogOffset(0)`; every cold row reads `cachedSnapshot(header) ?? cachedPredecessorTitle(header)` |
|
||||
| `session-reference` `projectedLabels` | same symptom, same change |
|
||||
| `list-children.ts` `resolveColdIdentity` | follows the signature only; the `!header.isSeeded` guard and the body fallback stay, because it needs the cut to classify a seq as inherited or owned, which is fold semantics |
|
||||
|
||||
### The client store separates cached and sequenced rows
|
||||
|
||||
Rows in `ProjectionValueStore` come in two kinds, named for the property the rule depends on: whether the value carries a seq comparable within this connection.
|
||||
|
||||
| Row | Meaning | Carries |
|
||||
|---|---|---|
|
||||
| `cached` | viewed from the persisted checkpoint without a Session; its seq is not comparable with this connection | `value` only |
|
||||
| `sequenced` | computed by the Host for this Session within this connection; seqs share one space | `value` and `seq` |
|
||||
|
||||
Rules:
|
||||
|
||||
| Write | Against a `cached` row | Against a `sequenced` row |
|
||||
|---|---|---|
|
||||
| `applyCached(values)` | replaces | ignored |
|
||||
| `seed(baseline)` | discards every `cached` row first, then writes the block; omitted keys clear under `seq <= cut` | higher-seq-wins |
|
||||
| `apply(key, value, seq)` | replaces | higher-seq-wins |
|
||||
|
||||
Seq comparison happens only within one Host connection: `handleConnected` clears the whole store, then the list writes cached rows again and opening a session seeds again. From cached to sequenced is an unconditional replacement without any seq comparison.
|
||||
|
||||
Writer classification:
|
||||
|
||||
| Entry | Row kind |
|
||||
|---|---|
|
||||
| the `projections` block of each summary in the `session.list` response (`manager.refreshList`) | by the block's `kind`: `cached` for a cold session, `sequenced` for a live one |
|
||||
| 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 |
|
||||
| push frames (the `projection` frame handled by `manager`) | sequenced |
|
||||
| the `title` after a successful rename (`session.ts`) | sequenced |
|
||||
|
||||
The `seed` and `apply` signatures are unchanged. The client routes by `kind`: a `sequenced` block goes through `apply` key by key, a `cached` block through `applyCached`, which does not read its `asOfSeq`.
|
||||
|
||||
### What stays untouched
|
||||
|
||||
- The Session format, the physical header line, `fromHeaderLine`, persistence, `SessionPersistenceSnapshot`.
|
||||
- Hydration and the checkpoint write path; `recordFor` keeps full equality on all five fields.
|
||||
- `coldSnapshot` stays; having no production caller is not a reason to delete it here.
|
||||
- Old records (domain v4/v5, lacking `isSeeded` and `inheritedEventCount`) keep missing for seeded sessions until the session is opened and rewritten as a v7 record.
|
||||
- The list still delivers every row with a wire view (`contextBreakdown`, `turnOutline`, and so on); narrowing that is out of scope.
|
||||
|
||||
### Worst case, walked through
|
||||
|
||||
1. The list serves a wrong record; the sidebar shows a wrong title, held in a cached row.
|
||||
2. The session is opened; for the instant before the first-page baseline arrives the wrong value is still shown. Every pre-population has this window.
|
||||
3. The first-page baseline `seed`s: every cached row is discarded unconditionally, then the block is written. Nothing of the wrong value survives, whatever its seq.
|
||||
4. A later list refresh writes cached values that hit sequenced rows and are ignored; the wrong value does not come back.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Write the cut back into the physical header line.** Header-only reads would obtain the cut directly and the read-only face would keep its identity. The cost is another Session format generation right after V4 landed, and reversing #3346's decision that the exact cut does not pose as body-free metadata. Rejected: the header is not changed for this.
|
||||
|
||||
**An index of each session's cut maintained by persistence or session-query.** A new durable index with its own consistency maintenance, serving one display read. Rejected.
|
||||
|
||||
**Restore a bounded body probe.** The `probeSmallCold` idea #3400 removed, or a client-side asynchronous `refreshProjections` for visible seeded sessions. It breaks the list's zero-I/O principle, and the historical 1KB threshold shows it never covered an ordinary fork. Rejected.
|
||||
|
||||
**Only the `asOfSeq: -1` sentinel, without store tiers.** One server-side change makes hints always lose under the seq rule. It rests on a convention: when a hint's seq equals or exceeds the baseline cut (crash-repair truncation), `apply` keeps the old row and the wrong value survives until the next frame. The user requires connected data to replace hints unconditionally, so the rule belongs in the store rather than in a seq convention.
|
||||
|
||||
**Express cached through `asOfSeq: -1` and let the client route on the sentinel.** The read-only face always emits `-1` and the client treats every list block as cached. Rejected: one field would carry two meanings, and the second could only be inferred by convention; a live session's list block carries a real seq comparable within the connection, and treating every block as cached demoted those too. The PR review reproduced the regression: a delayed control baseline at a lower cut overwrote a newer list value. The independent field `kind` replaces the sentinel, and `asOfSeq` keeps each source's own watermark.
|
||||
|
||||
**Remove the cut from the cache identity entirely.** The fold face needs it: `restore` continues applying from the cached row's state, and `schedule`, `subagentCatalog`, `permissions.seeded`, and owned/inherited classification encode the cut; an error would be written back and persisted. Rejected.
|
||||
|
||||
**Naming the row kinds `hint` / `authoritative`.** Those words speak of trust, while the rule depends on seq comparability. Renamed `cached` / `sequenced` so the names state the property the rule uses.
|
||||
|
||||
## Consequences
|
||||
|
||||
Bought:
|
||||
|
||||
- After a restart, forked sessions immediately show their title, `sessionListMetadata` (blank, lastPromptAt), and every other cached wire value in the sidebar, ordered by the last prompt time.
|
||||
- `@` completion shows and searches forks by title.
|
||||
- Connected data replaces list hints unconditionally, independent of seq ordering.
|
||||
- The two levels of the cache identity have names: the lifecycle identity answers "is this the same log", the fold identity answers "may this continue a fold".
|
||||
|
||||
Paid:
|
||||
|
||||
- `cachedSnapshot` and `cachedPredecessorTitle` change signature; three callers change with them.
|
||||
- `SessionProjectionHints` gains the required field `kind`; every producer of a list summary and every test fixture that builds one carries it.
|
||||
- A hand-crafted record with the same four fields and a different cut is displayed in the list until the session is opened.
|
||||
- Keys the baseline omits clear together with their hints: when a Host does not mount `schedule`, the schedule mark the list hinted disappears after the session opens. Under "connected data is the truth" this is the correct behavior.
|
||||
- Old records still miss for seeded sessions until an open rewrites them.
|
||||
|
||||
## Testing
|
||||
|
||||
- `session-projection-cache/tests/cache.spec.ts`: a seeded cold header obtains every version-matching row through `cachedSnapshot(header)` at the row's watermark; an unseeded header is refused for the same id's seeded record; `coldSnapshot` continues from the row when the cut matches, refolds the whole log when it differs, and throws for an unseeded call with a nonzero cut; `cachedPredecessorTitle(header)` serves only `title` from a seeded record of an older `formatVersion`; rows with differing watermarks form one block whose `asOfSeq` is the lowest row.
|
||||
- `session-projection-cache/tests/fixtures.spec.ts`: archived v3 to v6 records still expose only the predecessor title; lineage-less archives still miss for seeded callers.
|
||||
- `api/session-controller/tests/session-cold.host.spec.ts`: a seeded cold summary carries `kind: 'cached'`, `title`, and `sessionListMetadata`, `updatedAt` takes `lastPromptAt`, the cache is queried, and the body is not read.
|
||||
- `api/session-controller/tests/session-projections.host.spec.ts`: a real fork made through `sessions.fork` is checkpointed into projcache, the whole Context is disposed, and a Host restarted over the same storage root reads the `kind: 'cached'` `title` and `sessionListMetadata` through `session.list` from the header alone; `inspect` and `open` are never called.
|
||||
- `api/session-controller/tests/projection-store.client.spec.ts`: `applyCached` fills empty keys only and never displaces a sequenced row; every sequenced write (including a frame at cursor `-1` and a baseline at a lower cut) replaces cached rows; a baseline discards all cached rows before clearing omitted keys; face subscribers are notified on cached fills and their discard. Manager path: a `cached` list block at any watermark is replaced by a control baseline at a lower cut, and a later list refresh cannot bring it back; a `sequenced` list block merges under higher-seq-wins, so a delayed baseline at a lower cut neither overwrites nor clears it while a higher-seq frame still advances it.
|
||||
- `api/session-controller/tests/manager.client.spec.ts`: a `cached` `api-session/added` block is replaced by a control baseline at the same cursor; a `cached` list block does not displace an existing sequenced title.
|
||||
- `api/session-controller/tests/inbox-projection.client.spec.ts`: unchanged; a live session's `sequenced` list block still outranks a delayed control baseline at a lower cut.
|
||||
- `context/session-reference/tests/session-reference.spec.ts`: a seeded cold session is labeled and searchable by its cached title, a session without a cache record is still labeled by id, and neither reads a log.
|
||||
- `subagent/subagent/tests/list-children.spec.ts`: unchanged; seeded children still go through body observation.
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
# Agent Note: Projection cache 只读面按 lifecycle 身份匹配,客户端 store 区分 cached 与 sequenced 行
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-19-projection-cache-listing-identity-and-cached-rows.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
Host 进程重启后,所有 fork 出来的会话(`SessionHeader.isSeeded === true`)在侧边栏列表里没有 title,`@` 引用补全只显示会话 id,列表排序退化到创建时间。打开一次之后恢复。2026-09-19 的一台开发机上,241 条 projcache 记录里有 44 条 seeded,44 条的 `title` 行全部合法,列表一次都没读到。
|
||||
|
||||
这不是缓存损坏,也不是版本失配。projcache 记录的 domain version 是 7,`identity.formatVersion` 是 4,两者分别是缓存自身的磁盘格式代际和 fold 所依据的 Session 日志格式代际,都是当前值。问题在读取路径。
|
||||
|
||||
### 机制
|
||||
|
||||
缓存记录(记录格式与前代恢复见 [投影缓存前代恢复与 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。
|
||||
|
||||
于是 header-only 的读取拿不到 cut:JSONL 后端 `fromHeaderLine` 对 header-only 读取硬编码 `inheritedEventCount: 0`,`SessionPersistenceSnapshot` 只有 header、revision 和可选的 eventCount。三个只读 header 的缓存消费者都因此加了同一个守卫:
|
||||
|
||||
| 消费者 | 守卫 | 兜底 | 后果 |
|
||||
|---|---|---|---|
|
||||
| `packages/api/session-controller/src/list.ts` `projectionsFor` | `header.isSeeded ? undefined : cachedSnapshot(header, 0) ?? cachedPredecessorTitle(header, 0)` | 无 | 无 title,无 `sessionListMetadata`,blank 判定退到 `false`,排序退到 `createdAt` |
|
||||
| `packages/context/session-reference/src/index.ts` `projectedLabels` | 同 | 无 | `@` 补全按 id 显示,搜不到 title |
|
||||
| `packages/subagent/subagent/src/list-children.ts` `resolveColdIdentity` | 同 | `observeSession` 读正文 | 结果正确,每个 seeded 子代一次正文读 |
|
||||
|
||||
守卫存在时(#3346,2026-09-01),`list.ts` 还有 `probeSmallCold`:缓存 miss 且日志文件不超过 `DEFAULT_COLD_BLANK_PROBE_MAX_BYTES = 1024` 字节时读正文补全。它的目的是识别 blank 会话,只覆盖不到 1KB 的日志,对正常 fork 从未生效。#3400(2026-09-03)删掉它,列表回到纯元数据加缓存。#4320(2026-09-19)把 fork 变成一等功能(`packages/core/session/src/fork.ts`,任意 seq fork),每个 fork 都是 `isSeeded: true` 加精确 cut,seeded 会话数量从个位数变成几十条,重启后全部冷,问题集中显形。
|
||||
|
||||
### 缓存的两种用途与校验位置
|
||||
|
||||
`SessionProjectionCache` 的读写面和调用方:
|
||||
|
||||
| 面 | 对记录做什么 | 写回 | 调用方 |
|
||||
|---|---|---|---|
|
||||
| `write(session)` | 三个强制点加节流写 checkpoint | 有 | 缓存内部监听 |
|
||||
| `hydratePrepared(session, events)` | 把 `rows` 当 fold 起点,从 `row.seq + 1` 继续 apply,结果进 live cell | 后续 checkpoint 写回 | `session-query/src/observation.ts` |
|
||||
| `coldSnapshot(meta, cut, events)` | fold 起点加回写 | 有 | 无生产调用方 |
|
||||
| `cachedSnapshot(meta, cut, keys?)` | `viewCheckpoint`:每行过 `ver` 和 `stateSchema`,`view` 后返回 | 无 | `list.ts`、`session-reference`、`list-children` |
|
||||
| `cachedPredecessorTitle(meta, cut)` | 同上,只看 `title`,允许更旧的 `formatVersion` | 无 | `list.ts` |
|
||||
|
||||
身份校验全部在缓存内部(`recordFor` → `identityMatches`;`viewCheckpoint` 逐行校验 `ver` 与 schema)。`list.ts` 自己不校验,只是给不出 cut 时不调。
|
||||
|
||||
spec.ts 与 README 描述身份检查的目的时用的都是同一个动词:防止 "seed state folded from an unrelated log"、"cannot seed the caller"。它防的是 fold 面。只读面从未做过 seed。
|
||||
|
||||
### 客户端 store 无法保证建连数据覆盖提示
|
||||
|
||||
客户端每个会话只有一个 `ProjectionValueStore`(`manager.projectionStores`;合并规则原记录于 [Session observation 与 projection 所有的客户端状态](2026-08-25-session-observations-and-projection-owned-client-state.zh.md)),列表下发的 block、打开会话的 history 首页基线、control 基线、推送 frame、rename 结果全部写进同一个对象,`useProjection` 读的也是它。所有写入者平等,只有一条 higher-seq-wins 规则:`apply` 在 `seq <= row.seq` 时丢弃新值,`seed` 清理没带的键时只清 `row.seq <= cut` 的行。一个 seq 相等或偏高的列表提示会在会话打开后原样留在格子里。列表提示的 seq 来自磁盘记录,crash-repair 截断日志后它可以数字上高于建连 cursor,而那正是缓存错、建连对的情形。对提示做 seq 比较是用错了工具。
|
||||
|
||||
## Decision
|
||||
|
||||
### 缓存分成两个读面,cut 只在 fold 面比对
|
||||
|
||||
| 面 | 身份 | 谁能提供 | 方法 |
|
||||
|---|---|---|---|
|
||||
| 只读面 | lifecycle 身份:`formatVersion + createdAt + cwd + isSeeded`,全部来自 header | 任何 header-only 调用方 | `cachedSnapshot(header, keys?)`、`cachedPredecessorTitle(header)` |
|
||||
| fold 面 | lifecycle 身份加 `inheritedEventCount` | 握有 Session 或正文的调用方 | `recordFor` 所服务的 `hydratePrepared`、`write`、`coldSnapshot` |
|
||||
|
||||
只读面的两个方法不再接受 cut 参数。`inheritedEventCount` 继续写入记录,fold 面继续全等比对它;"不同 fork cut 的记录不能互相当种子"这条原始设计预期不变。
|
||||
|
||||
只读面返回的 block 里,`asOfSeq` 是所服务各行中最低的水位,也就是存储记录自己的位置。header 既作证不了 cut,也作证不了这个水位与当前日志的可比性,所以可比性不由这个数字表达:Session list 给每条摘要的 `projections` block(`SessionProjectionHints`)加独立字段 `kind`,冷会话从 projcache 看出来的 block 标 `cached`,活会话由 Host live registry 算出的 block 标 `sequenced`。两个字段各管各的:`kind` 说明 `asOfSeq` 属于哪个序列空间,`asOfSeq` 是那个空间里的水位。
|
||||
|
||||
### 只读面为什么可以不比 cut
|
||||
|
||||
- 在同一个 `formatVersion` 下,一条日志的 cut 由 `(id, createdAt, cwd, isSeeded)` 唯一决定:fork 创建时写死,只有改变事件基数的格式迁移能改它,而那种迁移必然改 `formatVersion`。cut 是 lifecycle 的派生事实,不是独立坐标。
|
||||
- 只读面今天实际发生的是:unseeded 传常量 0(构造上恒真),seeded 直接不调。cut 在只读面上从未和任何真实事实比过。拿掉它,拒绝集合一个都不变:另一个 lifecycle 的记录(四字段任一不同)照样被拒;unseeded 结果相同;seeded 从不可寻址变成可寻址。
|
||||
- 只读面没有 seed、没有写回,输出是存储记录的纯函数,cut 不参与计算,也不流向任何地方。
|
||||
- 唯一多出来的暴露面:一份四字段全同、只有 cut 不同的记录。系统自身产生不出它,只有手工复制会话目录再改文件才行。就算出现,后果是列表显示它的值直到会话被打开,仍然不进任何 fold。
|
||||
|
||||
### 消费者
|
||||
|
||||
| 位置 | 改法 |
|
||||
|---|---|
|
||||
| `list.ts` `projectionsFor` | 删 `isSeeded` 分支和 `SessionLogOffset(0)`,冷会话统一 `cachedSnapshot(header) ?? cachedPredecessorTitle(header)` |
|
||||
| `session-reference` `projectedLabels` | 同一症状,同一改法 |
|
||||
| `list-children.ts` `resolveColdIdentity` | 只跟随签名,`!header.isSeeded` 守卫和正文兜底原样保留;它需要 cut 做 inherited/owned 分类,属于 fold 语义 |
|
||||
|
||||
### 客户端 store 区分 cached 与 sequenced 行
|
||||
|
||||
`ProjectionValueStore` 的行分两类,按"这个值有没有本连接可比的 seq"命名:
|
||||
|
||||
| 行 | 含义 | 携带 |
|
||||
|---|---|---|
|
||||
| `cached` | 从持久化 checkpoint 只读看出来的值,没有 Session 参与,seq 与本连接不可比 | 只有 `value` |
|
||||
| `sequenced` | 本连接内由 Host 对该 Session 算出的值,seq 在同一空间可比 | `value` 加 `seq` |
|
||||
|
||||
规则:
|
||||
|
||||
| 写入 | 遇到 `cached` 行 | 遇到 `sequenced` 行 |
|
||||
|---|---|---|
|
||||
| `applyCached(values)` | 覆盖 | 忽略 |
|
||||
| `seed(baseline)` | 整表丢弃 `cached` 行,再按 block 写入;block 没带的键按 `seq <= cut` 清理 | higher-seq-wins |
|
||||
| `apply(key, value, seq)` | 覆盖 | higher-seq-wins |
|
||||
|
||||
seq 比较只在同一条 Host 连接内发生:`handleConnected` 先整表 `clear()`,再由列表重新写 cached、打开会话重新 seed。从 cached 到 sequenced 是无条件替换,不看 seq。
|
||||
|
||||
写入口归类:
|
||||
|
||||
| 入口 | 行类型 |
|
||||
|---|---|
|
||||
| `session.list` 响应里每条摘要的 `projections` block(`manager.refreshList`) | 按 block 的 `kind`:冷会话 `cached`,活会话 `sequenced` |
|
||||
| `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 |
|
||||
| 推送 frame(`manager` 处理 `projection` frame) | sequenced |
|
||||
| rename 成功后的 `title`(`session.ts`) | sequenced |
|
||||
|
||||
`seed` 与 `apply` 签名不变。客户端按 `kind` 分流:`sequenced` block 逐键 `apply`,`cached` block 走 `applyCached`,不读它的 `asOfSeq`。
|
||||
|
||||
### 不动的部分
|
||||
|
||||
- Session 格式、物理 header 行、`fromHeaderLine`、persistence、`SessionPersistenceSnapshot`。
|
||||
- hydration 与 checkpoint 写路径,`recordFor` 的五字段全等。
|
||||
- `coldSnapshot` 保留,不因没有生产调用方而删除。
|
||||
- 老记录(domain v4/v5,缺 `isSeeded` 与 `inheritedEventCount`)对 seeded 会话继续 miss,等会话打开后被 v7 记录改写。
|
||||
- 列表下发全部带 wire 视图的行(`contextBreakdown`、`turnOutline` 等)这一点不在本次范围。
|
||||
|
||||
### 最坏情形推演
|
||||
|
||||
1. 列表用了一份错误记录,侧边栏显示错 title,格子里是 cached 行。
|
||||
2. 点进会话,首页基线到达前一瞬仍是错值。这是任何预填充都有的窗口。
|
||||
3. 首页基线 `seed`:所有 cached 行先被无条件丢弃,再按 block 写入。错值零残留,与 seq 大小无关。
|
||||
4. 之后列表刷新,cached 写入遇到 sequenced 行直接忽略,错值不会被刷回来。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**把 cut 写回物理 header 行。** header-only 读取能直接拿到 cut,只读面无需改身份。代价是 Session 格式再升一版,V4 刚落地;并且要推翻 #3346"精确 cut 不伪装成 body-free metadata"的决定。否决:不为此改 header。
|
||||
|
||||
**由 persistence 或 session-query 维护每个会话 cut 的索引。** 新增一套 durable 索引及其一致性维护,只为服务一个展示读取。否决。
|
||||
|
||||
**恢复有界正文探测。** #3400 删掉的 `probeSmallCold` 思路,或由客户端对可见 seeded 会话异步 `refreshProjections`。违背列表零 I/O 原则,且历史阈值 1KB 说明它从未覆盖过正常 fork。否决。
|
||||
|
||||
**只用 `asOfSeq: -1` 哨兵,不给 store 分层。** 服务端一处改动即可让提示在 seq 规则下永远落败。但它靠约定成立:一旦提示 seq 与基线 cut 相等或偏高(crash-repair 截断),`apply` 保留旧行,错值存活到下一帧。用户要求建连数据无条件覆盖提示,规则要写进 store 而不是靠 seq 约定模拟。
|
||||
|
||||
**用 `asOfSeq: -1` 表达 cached,客户端按哨兵分流。** 只读面统一输出 `-1`,客户端把所有列表 block 当 cached。否决:一个字段同时承担两个含义,另一个含义只能靠约定推断;活会话的列表 block 带本连接可比的真实 seq,一律当 cached 就把它们也降级了,PR 评审复现了延迟到达、cut 更低的 control 基线覆盖更新列表值的回归。改为独立字段 `kind`,`asOfSeq` 保持各自来源的水位。
|
||||
|
||||
**把 cut 从缓存身份里彻底删掉。** fold 面需要它:`restore` 从缓存行状态继续 apply,`schedule`、`subagentCatalog`、`permissions.seeded` 与 owned/inherited 归属都编码了 cut,错误会被写回并持久化。否决。
|
||||
|
||||
**行类型命名 `hint` / `authoritative`。** 说的是可信度,而规则依据的是 seq 可比性。改为 `cached` / `sequenced`,名字直接说明规则用到的性质。
|
||||
|
||||
## Consequences
|
||||
|
||||
买到的:
|
||||
|
||||
- 重启后 fork 会话在侧边栏立即有 title、`sessionListMetadata`(blank、lastPromptAt)和其余带 wire 的缓存值,排序按最后一次提问时间。
|
||||
- `@` 补全对 fork 按 title 显示和搜索。
|
||||
- 建连后的数据无条件替换列表提示,不再依赖 seq 大小。
|
||||
- 缓存身份的两级含义有了名字:lifecycle 身份回答"是不是同一份日志",fold 身份回答"能不能当续算起点"。
|
||||
|
||||
付出的:
|
||||
|
||||
- `cachedSnapshot` 与 `cachedPredecessorTitle` 签名变化,三个调用方同步修改。
|
||||
- `SessionProjectionHints` 新增必填字段 `kind`,所有产出列表摘要的地方和构造摘要的测试夹具都要带上。
|
||||
- 手工构造的同四字段、不同 cut 的记录会在列表上显示到会话被打开为止。
|
||||
- 基线没带的键连提示一起清掉:某个 Host 未挂载 `schedule` 时,打开会话后列表提示过的 schedule 标记消失。按"建连数据是真值"这是正确行为。
|
||||
- 老记录对 seeded 会话仍然 miss,直到打开重写。
|
||||
|
||||
## Testing
|
||||
|
||||
- `session-projection-cache/tests/cache.spec.ts`:seeded 冷 header 通过 `cachedSnapshot(header)` 取到全部版本匹配的行,`asOfSeq` 为行水位;unseeded header 对同一 id 的 seeded 记录被拒;`coldSnapshot` 对 cut 一致的记录从行续算、对 cut 不一致的记录整段重新 fold,对 unseeded 且 cut 非 0 的调用抛错;`cachedPredecessorTitle(header)` 对 seeded 的更旧 `formatVersion` 记录只给 `title`;不同 watermark 的多行合成一个 block,`asOfSeq` 取最低行。
|
||||
- `session-projection-cache/tests/fixtures.spec.ts`:归档的 v3 到 v6 记录继续只透出 predecessor title;无 lineage 字段的归档对 seeded 调用方继续 miss。
|
||||
- `api/session-controller/tests/session-cold.host.spec.ts`:seeded 冷会话摘要携带 `kind: 'cached'`、`title` 与 `sessionListMetadata`,`updatedAt` 取 `lastPromptAt`,缓存确实被查询,正文没有被读。
|
||||
- `api/session-controller/tests/session-projections.host.spec.ts`:用 `sessions.fork` 造一个真实 fork 并写入 projcache,销毁整个 Context 后在同一存储根上重启 Host,只凭 header 通过 `session.list` 读到 `kind: 'cached'` 的 `title` 与 `sessionListMetadata`,`inspect` / `open` 一次都没被调用。
|
||||
- `api/session-controller/tests/projection-store.client.spec.ts`:`applyCached` 只填空位、不覆盖 sequenced 行;任何 sequenced 写入(包括 cursor `-1` 的 frame 和 cut 更低的基线)替换 cached 行;基线先丢弃全部 cached 行再清理没带的键;面订阅在 cached 填充与丢弃时都收到通知。manager 路径:`cached` 列表 block 的水位再高,也被 cut 更低的 control 基线替换,之后的列表刷新拿不回来;`sequenced` 列表 block 按 higher-seq-wins,cut 更低的延迟基线既不覆盖也不清掉它,更高 seq 的 frame 仍能推进它。
|
||||
- `api/session-controller/tests/manager.client.spec.ts`:`api-session/added` 的 `cached` block 被同 cursor 的 control 基线替换;`cached` 列表 block 不覆盖已有的 sequenced title。
|
||||
- `api/session-controller/tests/inbox-projection.client.spec.ts`:不变,活会话的 `sequenced` 列表 block 仍压过延迟到达、cut 更低的 control 基线。
|
||||
- `context/session-reference/tests/session-reference.spec.ts`:seeded 冷会话按缓存 title 标注并可按 title 搜到,没有缓存记录的会话仍按 id 标注,两者都不读日志。
|
||||
- `subagent/subagent/tests/list-children.spec.ts`:不变,seeded 子代仍走正文观察。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-17-durable-web-queue-recovery.md
|
||||
2026-08-17-durable-web-queue-recovery.md: c235cc47d5a68623249765753c2757948721ff6e
|
||||
2026-08-17-durable-web-queue-recovery.zh.md: 2b224b5e1d63905e738dfda0772a3ea578c9aef3
|
||||
2026-08-17-durable-web-queue-recovery.md: 5fbfdf0e62c1034fbea138016cc2979489de474f
|
||||
2026-08-17-durable-web-queue-recovery.zh.md: 2aaffd6bc571aab449d9429758fdf45bb4dd8d4c
|
||||
@@ -18,7 +18,7 @@ The registry folds committed splices before `Session.append()` returns; each Age
|
||||
|
||||
The generic session-projection carrier is the only Web transport. It sends higher-seq `session/projection` values, includes the complete values block in each `session.follow` opening snapshot, folds detached cold logs, and uses the projection cache when valid. There is no Host-owned `queue` projection, placement vocabulary, handoff list, dedicated queue frame, or live-Agent reconnect enumeration.
|
||||
|
||||
A synchronous subscription to ready Host generations discards every retained projection value and watermark before refreshing queries and restarting the control stream, including cold Sessions absent from the process-local control baseline. The first control stream waits for generation readiness; a baseline cannot arrive before invalidation and then be erased by a later Cordis `connection/reset` notification. Observable faces retain their identities and subscriptions. A list request from an earlier generation cannot publish values or settle the current request; history and list values from the new generation may therefore establish a lower durable seq without losing to unpersisted state. Within a generation, all incoming baselines obey higher-seq-wins, so a delayed control baseline cannot remove or overwrite newer list or history values.
|
||||
A synchronous subscription to ready Host generations discards every retained projection value and watermark before refreshing queries and restarting the control stream, including cold Sessions absent from the process-local control baseline. The first control stream waits for generation readiness; a baseline cannot arrive before invalidation and then be erased by a later Cordis `connection/reset` notification. Observable faces retain their identities and subscriptions. A list request from an earlier generation cannot publish values or settle the current request; history and list values from the new generation may therefore establish a lower durable seq without losing to unpersisted state. Within a generation, all Host-sequenced values obey higher-seq-wins, so a delayed control baseline cannot remove or overwrite newer history values or a live Session's list values; a cached list block viewed from the projection cache yields to the baseline instead ([listing identity and cached rows](../architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.md)).
|
||||
|
||||
The client Session binding retains `inbox` in its generic per-session projection store and does not copy it into `SessionSnapshot`. QueueDock reads `next-turn` directly. ChatView reads user-origin `next-step` messages directly and ignores injected context. Claiming removes a pending value through the durable splice; a later `user/message` is rendered through the ordinary conversation projection.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Inbox 接受消息时会记录规范化的 `agent/inbox/spliced` 事件,但 We
|
||||
|
||||
通用会话投影传输层是唯一 Web 传输。它发送 seq 更高的 `session/projection` 值,在每次 `session.follow` 的起始快照中包含完整 values 块,折叠已分离的冷日志,并在缓存有效时使用投影缓存。系统不存在 Host 拥有的 `queue` 投影、placement 词汇、handoff 列表、专用 queue 帧或枚举 live Agent 的重连逻辑。
|
||||
|
||||
对已就绪 Host generation 的同步订阅会先丢弃所有保留的投影值及其水位,再刷新查询并重新打开 control stream,其中也包括进程本地 control baseline 中没有列出的冷 Session。首次 control stream 会等待 generation 就绪;baseline 不会先于旧状态清理到达,再被较晚的 Cordis `connection/reset` 通知清除。Observable face 保留自身标识及订阅。较早 generation 的 list 请求不能发布值或使当前请求结束,因此新 generation 的历史与 list 值可以建立较低的持久 seq,而不会被尚未持久化的状态挡住。同一 generation 内,所有收到的 baseline 都遵循较高 seq 优先,因此延迟到达的 control baseline 不能删除或覆盖较新的 list 或 history 值。
|
||||
对已就绪 Host generation 的同步订阅会先丢弃所有保留的投影值及其水位,再刷新查询并重新打开 control stream,其中也包括进程本地 control baseline 中没有列出的冷 Session。首次 control stream 会等待 generation 就绪;baseline 不会先于旧状态清理到达,再被较晚的 Cordis `connection/reset` 通知清除。Observable face 保留自身标识及订阅。较早 generation 的 list 请求不能发布值或使当前请求结束,因此新 generation 的历史与 list 值可以建立较低的持久 seq,而不会被尚未持久化的状态挡住。同一 generation 内,所有 Host 排序过的值都遵循较高 seq 优先,因此延迟到达的 control baseline 不能删除或覆盖较新的 history 值或活 Session 的 list 值;从 projection cache 看出来的 cached list block 则让位于该 baseline([只读面按 lifecycle 身份匹配与 cached 行](../architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md))。
|
||||
|
||||
客户端 Session binding 在通用逐会话投影存储中保留 `inbox`,不会把它复制进 `SessionSnapshot`。QueueDock 直接读取 `next-turn`。ChatView 直接读取用户来源的 `next-step` 消息,并忽略注入上下文。认领操作通过持久 splice 移除待处理值;后续 `user/message` 由普通会话投影渲染。
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-19-declared-session-attachment-carriers.md
|
||||
2026-09-19-declared-session-attachment-carriers.md: 1619624800dc5e1ee8d6a4f4b64b84a6c80078c2
|
||||
2026-09-19-declared-session-attachment-carriers.zh.md: bd66b1c80469e2adbd7becc1b015d390978becc6
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Declared Session attachment carriers
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-19-declared-session-attachment-carriers.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Session attachment authorization and ZIP export inferred attachment ownership from common payload field names. An unknown ignorable event could therefore authorize an image read or export file bytes. Compaction content under `summary` and `rawOutput` was outside that field-name scan.
|
||||
|
||||
## Decision
|
||||
|
||||
Both consumers select content fields by built-in event type. They inspect completed assistant blocks and direct content blocks, including flat V4 tool-role messages. Other content-block fields and unknown event payloads remain opaque, even when they contain objects resembling image or file references.
|
||||
|
||||
The controller applies this rule before reading image bytes. The exporter applies it before reading image or file bytes and retains the complete logical log. Historical logs reach these readers after persistence converts them to current logical events. Title requests are excluded because V4 requires a single text block. Compaction uses its declared content fields: the basic producer currently emits text, while other valid compaction producers may supply attachments there.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Scan common keys on every event.** This lets a field name grant attachment access without a defined content meaning. It also misses valid content stored under other declared fields.
|
||||
|
||||
**Interpret historical wrappers in attachment readers.** Historical conversion belongs to the adjacent format packages. Current attachment readers use flat V4 content and do not maintain a second interpretation of old blocks.
|
||||
|
||||
## Consequences
|
||||
|
||||
Plugins that store attachments only in custom events cannot use these built-in readers for those references. A plugin needs a supported content occurrence or its own reader. Adding a built-in content carrier requires updating both readers and their acceptance and refusal tests. Historical codec inventories remain version-owned; changing this policy must not change what a released decoder recognizes.
|
||||
|
||||
The change affects attachment lookup and archive membership, not Session encoding or the model-visible message history.
|
||||
|
||||
## Verification
|
||||
|
||||
Controller and archive tests cover declared carriers, flat tool results, opaque plugin payloads, and misleading extra fields. Refusal tests assert that attachment storage is never read; archive tests also compare the exported log text.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Session 声明的附件载体
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-19-declared-session-attachment-carriers.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Session 附件授权与 ZIP 导出按常见载荷字段名推断附件归属。因此,未知可忽略事件也能授权图片读取或导出文件字节。压缩事件的 `summary` 与 `rawOutput` 内容不在这套字段名扫描范围内。
|
||||
|
||||
## 决策
|
||||
|
||||
两个消费者均按内置事件类型选择内容字段。它们检查已完成的 assistant 块与直接内容块,包括扁平的 V4 tool 角色消息。其他内容块字段与未知事件载荷保持不透明,即使其中含有类似图片或文件引用的对象。
|
||||
|
||||
控制器在读取图片字节前应用此规则。导出器在读取图片或文件字节前应用此规则,并保留完整逻辑日志。历史日志由持久化层转换成当前逻辑事件后交给这些读取器。标题请求不在附件载体之列,因为 V4 要求它只含一个文本块。压缩事件使用其声明的内容字段:基础生产者目前输出文本,其他合法压缩生产者可以在这些位置提供附件。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**扫描所有事件的常见键。** 这使字段名在没有明确内容语义时授予附件访问权,也会遗漏其他已声明字段中的有效内容。
|
||||
|
||||
**在附件读取器中解释历史包装块。** 历史转换由相邻格式包负责。当前附件读取器使用扁平的 V4 内容,不维护第二套旧块解释。
|
||||
|
||||
## 影响
|
||||
|
||||
仅在自定义事件中存储附件的插件不能通过这些内置读取器读取相关引用。插件需要使用受支持的内容位置,或提供自己的读取器。新增内置内容载体时,需要同步更新两个读取器及其接纳和拒绝测试。历史编解码器的清单仍归各版本所有;调整此策略不能改变已发布解码器识别的内容。
|
||||
|
||||
此变更影响附件查找与归档成员,不影响 Session 编码或模型可见消息历史。
|
||||
|
||||
## 验证
|
||||
|
||||
控制器和归档测试覆盖声明的载体、扁平工具结果、不透明插件载荷及误导性额外字段。拒绝测试断言不会读取附件存储;归档测试还比较导出的日志文本。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-05-agent-teams.md
|
||||
2026-08-05-agent-teams.md: d108d4f9833aca2a24c84d18d895e7ec63c0bfc2
|
||||
2026-08-05-agent-teams.zh.md: abf2ce3716e44effc8e34c0911cd9561d815aa08
|
||||
2026-08-05-agent-teams.md: 45c7e599e03bb26dc9224c74a6d3bc0ab8a74dcc
|
||||
2026-08-05-agent-teams.zh.md: dbb2931bdc40da5ddf2c8091dd8c401029e0445f
|
||||
@@ -40,6 +40,8 @@ Fresh children have no inherited conversation. Fork children capture the Lead's
|
||||
|
||||
Peer communication is a Lead-log mailbox. `team/message/queued` is appended and flushed before delivery. The target message carries the stable message id and sender identity in both durable source metadata and a short model-visible prefix. A target receipt is acknowledged with `team/message/delivered` only after its pending inbox item or recorded user message is flushed. Immediate admission is serialized per target in queued-log order, recovery retries queued-minus-delivered in the same order, and delivery folds live or persisted target inbox/history state before cold resume. Every current-version Team payload is runtime-validated before entering replay state. The Team runtime tracks dispatch and asynchronous acknowledgement work from synchronous admission until settlement; disposal closes admission and awaits both before removing the service. Current waiters wake only after the owning Team event flush succeeds.
|
||||
|
||||
Unknown mailbox content stays as decoded JSON during event projection and checkpoint admission. Validation inspects its type without rebuilding its fields, because generic object parsing can omit a valid own `__proto__` key. Locally declared content variants undergo structural validation; opaque plugin fields do not gain Team semantics. Cache invalidation is necessary because parser correction alone cannot recover keys omitted from cached state. The [Team documentation](../../../../packages/experimental/agent-team/README.md) owns the restore behavior.
|
||||
|
||||
`send_message` always attempts Steer delivery. A running target receives the message at the nearest step boundary; an inactive target starts a turn if loaded or cold-resumes otherwise. Success means the message is already durable even when temporary delivery failure leaves it queued. The mechanism provides process-local retry and target-Session de-duplication, not a cross-process exactly-once claim. The [Team Steer messaging decision](../../archived/simplification/2026-08-30-team-send-message-steer.md) owns the single-tool scheduling rationale.
|
||||
|
||||
Shared tasks are complete snapshots with Team-local ids and monotonic revisions. Every mutation carries `expectedRevision`. Any member creates, reads, or claims a ready unowned task; the owner or Lead edits and transitions it, while only the Lead assigns another member. Numeric task ids remain within the safe-integer allocation range, and exhaustion fails without reusing an id. Dependencies must name non-deleted tasks and form a complete DAG. Deleted tasks are retained tombstones. `writeScopes` are normalized path prefixes that produce overlap diagnostics but never block claim or authorize a write.
|
||||
|
||||
@@ -40,6 +40,8 @@ fresh child 不继承对话。fork child 只捕获一次 Lead 已完成 turn 前
|
||||
|
||||
Peer 通讯使用 Lead 日志 mailbox。投递前先追加并 flush `team/message/queued`。target message 会在持久 source metadata 与短模型可见前缀中同时携带稳定 message id 和 sender identity。只有 pending inbox 条目或已记录用户消息完成 flush,Lead 日志才写入 `team/message/delivered` acknowledgement。即时准入按 target 和 queued 日志顺序串行化,恢复按同一顺序重试 queued-minus-delivered,并在冷恢复前折叠 live 或 persisted target 的 inbox/历史状态。每个当前版本 Team payload 都会经过运行时验证后才进入 replay state。Team runtime 从同步准入到 settlement 全程跟踪 dispatch 与异步 acknowledgement 工作;dispose 会关闭准入,并在移除服务前等待两者。当前 waiter 只在所属 Team event flush 成功后被唤醒。
|
||||
|
||||
事件投影和 checkpoint 准入期间,未知 mailbox 内容保持为已解码 JSON。校验只检查其 type,不重建字段,因为通用对象解析可能省略有效的自有 `__proto__` 键。本地声明的内容变体接受结构校验;不透明的插件字段不获得 Team 语义。缓存失效是必要的,因为只修正解析器无法恢复缓存状态中省略的键。[Team 文档](../../../../packages/experimental/agent-team/README.zh.md)拥有恢复行为的说明。
|
||||
|
||||
`send_message` 始终尝试 Steer 投递。running target 在最近的步骤边界收到消息,inactive target 在已加载时启动一个轮次,否则冷恢复。即使临时投递失败让消息保持 queued,成功也表示消息已经持久化。该机制提供进程内重试与 target Session 去重,不宣称跨进程 exactly-once。[Team Steer 消息决策](../../archived/simplification/2026-08-30-team-send-message-steer.md)负责单工具调度的理由。
|
||||
|
||||
共享 task 是带 Team-local id 与单调 revision 的完整快照。每次变更都携带 `expectedRevision`。任意 member 可以创建、读取或 claim ready 且无 owner 的任务;Owner 或 Lead 可以编辑和转换;只有 Lead 可以分配给另一个 member。数字 task id 保持在安全整数分配范围内;该范围耗尽时会失败,不会复用 id。依赖必须指向未删除任务,并形成完整 DAG。删除任务保留为 tombstone。`writeScopes` 是规范化路径前缀,只产生重叠诊断,绝不会阻止 claim 或授予写权限。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.md
|
||||
2026-09-07-deepseek-messages-adapter.md: caec59feb3a84140427bd3ae8b738274c3bc5c47
|
||||
2026-09-07-deepseek-messages-adapter.zh.md: 030c91cb9a5f566ce09892bb054b63b1d243f6bb
|
||||
2026-09-07-deepseek-messages-adapter.md: b79c2526461843a9534d6a997029281a584e0bed
|
||||
2026-09-07-deepseek-messages-adapter.zh.md: 49a967828e22803296ae3d4c8eef943b9ec8e3c5
|
||||
@@ -18,6 +18,8 @@ Assistant blocks remain the durable model-visible content. A versioned `ReplayEn
|
||||
|
||||
Both protocols prefer Files references for deterministic request images and share upload caching, refresh, quota recovery, and attachment offload. The Files client retains the selected protocol and configured endpoint: Messages follows the [exact `/v1` root rule](../bug-fix/2026-09-15-messages-v1-base-url.md), while Chat Completions appends `/files`. Messages Files requests carry the required beta header. Cached ids remain scoped by the resolved Files root and credential, so equivalent `/v1` and unversioned Messages roots share uploads. Messages metadata omits expiry, so local reuse is bounded from the original upload time without asserting remote deletion. A Files-resolution failure rebuilds the complete request under the independent inline-image budget; caller cancellation stops it. The shared image policy preserves the 128 MiB retained-image budget, 20 MiB inline base64 budget, and oldest-prefix offload in both requests and token measurement.
|
||||
|
||||
JSON syntax failure after a successful HTTP response does not establish a transport failure. Files decoding uses the existing `INVALID_RESPONSE` code with operation context instead of relabeling the failure as `TRANSPORT` or adding a retry; the [provider README](../../../../packages/llm/llm-deepseek/README.md) owns the error fields.
|
||||
|
||||
System updates use the existing [route capability](2026-09-02-in-history-system-prompt-replacement.md) when explicitly declared for an endpoint/model. Messages retains the initial top-level system and emits later snapshots as native system turns after the corresponding user/tool-result turn, preserving previously sent prefixes. This placement differs from the loop's system-before-user admission; serialization changes neither the durable log nor conversation-turn order. Undeclared routes consolidate the latest snapshot at the top level, including direct compaction calls. Capability inference from protocol or model names is insufficient because support and update semantics depend on the deployed endpoint.
|
||||
|
||||
Web always displays DeepSeek without a protocol selector. Both protocols share `baseURL` and `apiKeyEnv`, with no nested per-protocol configuration map. Without an endpoint override, resolution uses the selected protocol’s official default; Messages uses `https://api.deepseek.com/anthropic`. Switching retains existing endpoint overrides, whose compatibility belongs to the deployment. Messages follows the exact `/v1` root rule rather than inferring compatibility from other version-like suffixes. One model catalog includes `deepseek-flash` text/image and in-history system capabilities and retains the V4 entries. Explicit `chat-completions` remains supported with its own official default and appends `/chat/completions` without adding a version segment.
|
||||
|
||||
@@ -18,6 +18,8 @@ Status: implemented
|
||||
|
||||
两种协议均优先为确定性请求图片使用 Files 引用,并共享上传缓存、刷新、配额恢复和附件卸载。Files 客户端保留所选协议与已配置端点:Messages 遵循[严格匹配 `/v1` 的根地址规则](../bug-fix/2026-09-15-messages-v1-base-url.zh.md),Chat Completions 则追加 `/files`。Messages Files 请求携带必需的 beta 标头。缓存 id 按解析后的 Files 根地址和凭据限定作用域,因此等价的 `/v1` 与无版本 Messages 根地址可以复用上传。Messages 元数据不含过期时间,因此本地复用从原始上传时间起受限,但不宣称远端文件已删除。Files 解析失败会按独立的内联图片预算重建完整请求;调用方取消则停止请求。共享图片策略在请求与 token 计量中保留 128 MiB 的保留图片预算、20 MiB 的内联 base64 预算,以及最旧前缀卸载。
|
||||
|
||||
成功 HTTP 响应之后的 JSON 语法错误不足以证明传输故障。Files 解码复用已有的 `INVALID_RESPONSE` code 并补充操作上下文,不将其改标为 `TRANSPORT` 或添加重试;错误字段由 [provider README](../../../../packages/llm/llm-deepseek/README.zh.md) 维护。
|
||||
|
||||
系统提示词更新在端点与模型显式声明支持时,使用现有[路由能力](2026-09-02-in-history-system-prompt-replacement.zh.md)。Messages 保留初始顶层 system,在对应的用户或工具结果轮次之后,将后续快照发送为原生 system 轮次,保留此前发送的前缀。这个位置不同于循环先 system、后 user 的接纳顺序;序列化既不改写持久化日志,也不改变对话轮次的顺序。未声明能力的路由将最新快照归并到顶层,直接压缩调用也如此。仅凭协议或模型名称推断能力并不充分,因为支持情况和更新语义取决于实际部署的端点。
|
||||
|
||||
Web 始终显示 DeepSeek,不提供协议选择器。两个协议共用 `baseURL` 与 `apiKeyEnv`,没有嵌套的协议配置表。未提供地址覆盖时使用当前协议的官方默认值;Messages 为 `https://api.deepseek.com/anthropic`。切换协议保留已有端点覆盖,部署者负责其兼容性。Messages 遵循严格匹配 `/v1` 的根地址规则,不根据其他版本式后缀推断兼容性。模型目录只维护一份,包含 `deepseek-flash` 的文本/图片和历史内 system 更新能力,也保留 V4 条目。显式 `chat-completions` 仍受支持,并使用自己的官方默认值,追加 `/chat/completions` 而不增加版本段。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md
|
||||
2026-09-09-web-sidebar-terminal.md: 5757aed3702dbcc7752e99912714bd1356a96206
|
||||
2026-09-09-web-sidebar-terminal.zh.md: 39870d1ba26cbc1d655b35a3d2dba61692d0fdb9
|
||||
2026-09-09-web-sidebar-terminal.md: 5ed920d744f847cc97b08645592f0be221f18b8d
|
||||
2026-09-09-web-sidebar-terminal.zh.md: c3ceb0214c2f4db42b2fefb8b65619e198d9dc7e
|
||||
@@ -16,7 +16,7 @@ The application theme supplies terminal default colors. The body reads resolved
|
||||
|
||||
`api-terminal-controller` owns user terminals by Session and exposes the `terminal` Remote namespace. `ui-sidebar-terminal` registers native right-sidebar tabs, xterm.js rendering and FitAddon sizing. The terminal guide card has a primary action for the remembered available shell and a separate installed-shell menu. Selecting a menu item records its path and opens a new terminal immediately; discovery alone allocates no process. Each tab owns its startup and close lifecycle. Host discovery verifies the configured candidates, with the execution default first; creation accepts only a currently discovered path. The browser remembers the last selected shell path in origin-scoped localStorage and falls back to the current default if that path is unavailable. The terminal type declares independent instances, so ordinary page deduplication cannot collapse separate processes when opening or docking tabs. The existing sidebar controls open additional tabs; double-clicking a tab title renames its terminal. Terminal processes use the composed subprocess provider; [user-terminal permissions](../architecture/2026-09-16-user-terminal-permissions.md) govern their execution permissions independently of the Agent. Shell resolution occurs during discovery and creation; reading limits and reconnecting an existing process do not depend on the default executable remaining available. Interactive shell configuration supplies Tab completion and optional inline suggestions.
|
||||
|
||||
Close and replacement remove the tab synchronously and run process cleanup in the background. The Client first records the unfinished close request under a terminal-specific localStorage key; success removes it, and startup retries requests that remain. A cleanup failure produces a lightweight notification with a retry action without reopening the tab. Independent keys prevent another window from overwriting unrelated cleanup requests. Collapse, tab/Session switching, floating, fullscreen and browser disconnection preserve the process. Component cleanup and `TabDomain.signal` only detach browser work because the same lifetime can end during plugin reload. Failed process cleanup retains ownership, including failures after allocation but before create publication. Session owner disposal and Host plugin disposal also clean up terminals. A definitive missing-Session response retires its saved close request because the Session owns process cleanup; transport failures remain retryable. Client plugin disposal awaits every detached stream so a replacement plugin does not inherit unfinished Client cleanup.
|
||||
Close and replacement remove the tab synchronously and run process cleanup in the background. The Client first records the unfinished close request under a terminal-specific localStorage key; success removes it, and startup retries requests that remain. Cleanup failures have no notification or manual retry action. Host terminals missing from the saved layout are not reopened automatically and have no UI recovery entry. Independent keys prevent another window from overwriting unrelated cleanup requests. Collapse, tab/Session switching, floating, fullscreen and browser disconnection preserve the process. Component cleanup and `TabDomain.signal` only detach browser work because the same lifetime can end during plugin reload. Failed process cleanup retains ownership, including failures after allocation but before create publication. Session owner disposal and Host plugin disposal also clean up terminals. A definitive missing-Session response retires its saved close request because the Session owns process cleanup; transport failures remain retryable. Client plugin disposal awaits every detached stream so a replacement plugin does not inherit unfinished Client cleanup.
|
||||
|
||||
[Sidebar layout persistence and provider recovery](../architecture/2026-09-14-sidebar-layout-provider-recovery.md) owns browser reload: layout and tab identities are restored before the terminal provider reconnects its views. Listing still takes the Session ID directly because history can outlive its Agent and terminal owner; an offline Session has no retained terminals to restore. Only a new view may allocate a process; a recovered target that disappears reports an error. The Host remains authoritative for process state, titles and screen contents.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Web 用户需要在 Session 旁使用交互式 shell 检查工作区和运行命
|
||||
|
||||
`api-terminal-controller` 按 Session 管理用户终端并提供 `terminal` Remote namespace。`ui-sidebar-terminal` 注册原生右侧栏标签页,使用 xterm.js 渲染和 FitAddon 测量尺寸。终端开始页卡片的主操作打开上次选择且仍可用的 shell,独立菜单提供已安装 shell。选择菜单项会记录路径并立即打开新终端;仅探测 shell 不分配进程。每个标签页拥有自己的启动和关闭生命周期。Host 探测会验证配置的候选,并把执行环境默认项放在首位;创建只接受当前探测返回的路径。浏览器在当前站点 localStorage 中记住上次选择的 shell 路径,该路径不可用时回到当前默认项。终端类型声明独立实例,因此打开或停靠标签页时,普通页面的去重规则不会合并不同进程。已有侧栏控件负责打开更多标签页,双击标签页标题可重命名终端。终端进程使用组合的 subprocess provider;[用户终端权限](../architecture/2026-09-16-user-terminal-permissions.zh.md)规定其独立于 Agent 的执行权限。shell 在探测和创建时解析;读取限制和重新连接已有进程不依赖默认可执行文件仍然可用。交互式 shell 配置提供 Tab 补全和可选的内联建议。
|
||||
|
||||
关闭和替换会同步移除标签页,并在后台清理进程。Client 先以终端独立的 localStorage key 保存未完成的关闭请求;成功后删除,启动时重试剩余请求。清理失败时显示带重试操作的轻量通知,不重新打开标签页。独立 key 避免其他窗口覆盖无关的清理请求。折叠、切换标签页或 Session、浮动、全屏和浏览器断线均保留进程。组件清理和 `TabDomain.signal` 只停止浏览器工作,因为插件重新加载也会结束这些生命周期。进程清理失败时保留所有权,包括分配完成但 create 尚未发布时的失败。Session owner 和 Host 插件卸载也会清理终端。 明确的 Session 不存在响应会清除已保存的关闭请求,因为进程清理由 Session 负责;传输失败仍可重试。Client 插件卸载等待所有断开的流结束,避免替换插件继承未完成的 Client 清理。
|
||||
关闭和替换会同步移除标签页,并在后台清理进程。Client 先以终端独立的 localStorage key 保存未完成的关闭请求;成功后删除,启动时重试剩余请求。清理失败没有通知或手动重试操作。保存布局之外的 Host 终端不会自动重新打开,也没有 UI 恢复入口。独立 key 避免其他窗口覆盖无关的清理请求。折叠、切换标签页或 Session、浮动、全屏和浏览器断线均保留进程。组件清理和 `TabDomain.signal` 只停止浏览器工作,因为插件重新加载也会结束这些生命周期。进程清理失败时保留所有权,包括分配完成但 create 尚未发布时的失败。Session owner 和 Host 插件卸载也会清理终端。 明确的 Session 不存在响应会清除已保存的关闭请求,因为进程清理由 Session 负责;传输失败仍可重试。Client 插件卸载等待所有断开的流结束,避免替换插件继承未完成的 Client 清理。
|
||||
|
||||
[侧栏布局持久化与 provider 恢复](../architecture/2026-09-14-sidebar-layout-provider-recovery.zh.md)负责浏览器刷新:先恢复布局和标签身份,再由 terminal provider 重连视图。列表仍直接使用 Session ID,因为历史记录可以比 Agent 和终端 owner 存活更久;离线 Session 没有需要恢复的保留终端。只有新视图可以分配进程,恢复目标消失时显示错误。进程状态、标题和屏幕内容仍以 Host 为准。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-18-session-pin-and-sidebar-archive.md
|
||||
2026-09-18-session-pin-and-sidebar-archive.md: eb0c82ba84b1ebc94b23bff4d613ec6de9565d71
|
||||
2026-09-18-session-pin-and-sidebar-archive.zh.md: d971c7b4ff2b6de6a0f49c41974b7654843b29ea
|
||||
2026-09-18-session-pin-and-sidebar-archive.md: 262f7b76d6ac7eee21a646eedc93b7d936c86675
|
||||
2026-09-18-session-pin-and-sidebar-archive.zh.md: 86ddf4b58781c5974b93562396c3205cfdd0f9dc
|
||||
@@ -18,7 +18,7 @@ The `dsh-workspace` registry stores registry-global pin and archive sets as Sess
|
||||
|
||||
Each browser-local account has one complete Session sequence, including pinned and archived members. Workspace membership bounds each account; registry-global pin and archive sets never add a Session to an unrelated Workspace. Grouped, Ungrouped, and flat views use the same rules.
|
||||
|
||||
- Existing members retain their positions. Missing pinned members prepend in the pin array's order. Other missing members append by descending Session `updatedAt`, with missing archived members last. Supplementation never interleaves existing positions or duplicates an id.
|
||||
- Existing members retain their positions. Missing pinned members prepend in the pin array's order. New ordinary forks enter immediately before their source, including nested forks, without inheriting its pin. Saved members keep their relative order, and visible pinned rows still lead ordinary forks. Other missing members append by descending Session `updatedAt`, with missing archived members last. Supplementation never duplicates an id.
|
||||
- Supplementation is an in-memory projection, not an automatic storage migration. A Session-order write records the complete supplemented sequence, including hidden archived rows. Filtering alone never rewrites Session positions.
|
||||
- A successful Pin action marks the Session pinned and moves its position to the front of its complete account sequence. Completion uses current saved positions and the latest Workspace membership, preserving reorders completed while the request was pending. Unpin removes the mark without restoring an earlier position. Both pinned and ordinary drags edit the same complete sequence; display indices never replace its hidden members.
|
||||
- The current blank New Session precedes both partitions; other pinned Sessions lead unpinned Sessions. Manual preserves the complete sequence's relative order within both partitions. Last updated sorts both partitions strictly by descending `updatedAt`, with Session id as the tie-break. Pin records carry only Session identities, not timestamps.
|
||||
|
||||
@@ -18,7 +18,7 @@ Status: implemented
|
||||
|
||||
每份浏览器本地排序记录包含一条完整会话序列,包括置顶和归档成员。每份序列受 Workspace 成员关系约束,全局置顶与归档集合不会把会话补进无关的 Workspace。分组、Ungrouped 和平铺视图使用同一套规则。
|
||||
|
||||
- 已有成员保留位置。缺失的置顶成员按置顶数组中的顺序补到头部。其他缺失成员按会话 `updatedAt` 降序追加,缺失的归档成员放在最后。补齐不穿插已有位置,也不重复加入同一个 id。
|
||||
- 已有成员保留位置。缺失的置顶成员按置顶数组中的顺序补到头部。新增的普通 fork 紧邻来源之前插入,包括连续 fork,不继承来源的置顶。已保存成员保持相对顺序,可见置顶行仍排在普通 fork 之前。其他缺失成员按会话 `updatedAt` 降序追加,缺失的归档成员放在最后。补齐不重复加入同一个 id。
|
||||
- 补齐是内存派生,不自动迁移存储。一次会话顺序写入保存补齐后的完整序列,包括隐藏的归档行。单纯切换筛选不重写会话位置。
|
||||
- 置顶成功后,会话被标记为置顶,同时移到所属完整序列的首位。完成时使用当前保存的位置与最新 Workspace 成员关系,保留请求等待期间完成的重排序。取消置顶只移除标记,不恢复更早的位置。置顶行与普通行的拖拽都修改同一条完整序列,显示下标不会覆盖其中的隐藏成员。
|
||||
- 当前空白新会话排在两个分区之前,其余会话再按置顶区、非置顶区排列。手动模式下,两区分别保持完整序列中的相对顺序。最近更新模式下,两区都严格按 `updatedAt` 降序排序,以会话 id 打破平局。置顶记录只包含会话身份,不包含时间戳。
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-19-evidence-led-simplification-surveys.md
|
||||
2026-09-19-evidence-led-simplification-surveys.md: c217f80239690ee2f7310ca8e97956ec06fb75c7
|
||||
2026-09-19-evidence-led-simplification-surveys.zh.md: 1b1a11731967c87ab54aa9eba82253f90bb2abde
|
||||
@@ -0,0 +1,66 @@
|
||||
# Agent Note: Evidence-led simplification surveys
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-19-evidence-led-simplification-surveys.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Unused-symbol searches can miss working behavior whose implementation costs more than its required outcome, and can misclassify public extension APIs without fixed repository callers as dead. The previous simplification skill also combined discovery with lengthy archive procedures, omitted application and Python paths from its consumer examples, and suggested a proposal outline without the mandatory `Alternatives considered` section.
|
||||
|
||||
## Decision
|
||||
|
||||
The [simplification skill](../../../skills/dsh-find-simplifications/SKILL.md) traces a complete producer-to-outcome path, asks which distinctions change consumer actions, considers explicit reductions in supported behavior, and searches for maintained copies that an authoritative read can replace. It separates composition availability from deployment policy and counts removed configuration, lifecycle, tests, and documentation together with source code.
|
||||
|
||||
Production use changes the required trade-off evidence; it does not automatically veto a proposal or authorize implementation. Dynamic plugin discovery, installed consumers, generated runtime assets, profiles, applications, and Python participate in consumer tracing. Protected adapters and persistence designs, released data, trust boundaries, independent invariant observations, synchronous publication, cancellation, and quiescent disposal remain constraints.
|
||||
|
||||
The entry point separates decision criteria from an [optional historical reference](../../../skills/dsh-find-simplifications/references/historical-patterns.md) pairing mechanisms with counterexamples and residual obligations. Archive mechanics remain at their existing owner, and the proposal skeleton uses the canonical headings.
|
||||
|
||||
## Historical evidence
|
||||
|
||||
The author sampled 26 of the 100 English implemented or archived simplification notes: 11 implemented and 15 archived. All nine rejected simplification notes supplied counterexamples. The sample covers producerless variants, inert request knobs, explicit inputs, public projections, derived data, shared composition, lifecycle ownership, and dependency replacements; it is purposeful, not random or exhaustive.
|
||||
|
||||
The frozen comparison inputs contained 2,222 whitespace-delimited words in the baseline entry, 1,372 in the revised entry, and 897 in its reference; the hashes below identify those measured artifacts. The reference teaches questions rather than current deletion instructions. An adapter-rejected knob differs from a working extension API; a simpler public status can retain internal residency distinctions; reading at feedback time changes the observation promise; replacing a dependency can lose deterministic clocks or necessary teardown. Archived records were read without following or repairing outbound links. No old note was edited, archived, or deleted.
|
||||
|
||||
## Bounded survey evidence
|
||||
|
||||
The source snapshot is master after #4618. Each arm uses fresh agents with the same inherited model and reasoning effort, identical owner domains, a time cap, and at most four submissions per domain. Other outputs remain hidden from discovery agents. The runtime domain covers `core`, `session`, `session-query`, `api`, `sdk`, and Python; execution covers `shell`, `subprocess`, `terminal`, `jobs`, `fs`, `mcp`, and `boot`.
|
||||
|
||||
A third matched domain, covering `llm`, `compaction`, `context`, `skill`, and `extensions`, was added after the initial runtime results to broaden coverage. It is exploratory, uses a shorter equal cap, and remains separate from the initially declared two-domain comparison. The study stops after these three domains.
|
||||
|
||||
The skill author derived the revision from historical notes while isolated from current candidates. The coordinator received accidental early baseline hints, but accepted the independent author's revision unchanged before revised runs. Candidate packets hid arm labels from a separate reviewer; the coordinator's integration was not blind. The frozen inputs had SHA-256 values `9830af2272a1a1c3832078f31a432c8abe0335a013bc6c248c072b388520e798` for the baseline entry, `3b101d16ae098855c9214ad7fed631a2f079b6078c83b49829ae87305f631a87` for the revised entry, and `9a6b674d1eeadb691481ea09838b211b8e94c94e5a8cd928726246b1c097b2b5` for its reference.
|
||||
|
||||
After these runs, review replaced only the skill phrase “earns its place” with “is justified”; no discovery criterion changed and no new discovery run was performed. Each prompt requested exact source/search evidence, producer and consumer classification, note overlap, net reduction, the strongest counterargument, and acceptance requirements. Review applied those same criteria to every submission, distinguished local edits from durable decisions, and did not treat either public discoverability or a line-deletion count as a verdict by itself.
|
||||
|
||||
| Version | Domain | Cap | Submitted | Durable as submitted | Durable after refinement | Local | Deferred | Elapsed |
|
||||
|---|---|---|---:|---:|---:|---:|---:|---|
|
||||
| Baseline | Runtime | 12m | 3 | 1 | 2 | 0 | 0 | 6m 30s |
|
||||
| Baseline | Execution | 12m | 3 | 3 | 0 | 0 | 0 | 8m 40s |
|
||||
| Revised | Runtime | 12m | 2 | 1 | 0 | 1 | 0 | 5m 05s |
|
||||
| Revised | Execution | 12m | 3 | 2 | 0 | 1 | 0 | 8m 19s |
|
||||
| Baseline | Model/context, exploratory | 10m | 2 | 2 | 0 | 0 | 0 | 6m 02s |
|
||||
| Revised | Model/context, exploratory | 10m | 2 | 0 | 0 | 1 | 1 | 7m 51s |
|
||||
|
||||
The baseline yields six durable proposals as submitted and two after refinement. The revised skill yields three durable proposals and three local cleanups, with one proposed behavior reduction deferred. Python RPC narrowing and filesystem-invariant omission overlap. The combined set contains nine distinct durable proposals; the revised version adds [configuration-only HMR](../../proposed/simplification/2026-09-19-config-only-hmr.md). This sample does not show higher durable-proposal yield from the revision.
|
||||
|
||||
Review narrowed the [query API proposal](../../proposed/simplification/2026-09-19-trim-session-query-convenience-api.md) to retain `filterEvents`, whose literal substring behavior differs from FTS. The [prompt-event proposal](../../proposed/simplification/2026-09-19-retire-prompt-registry-change-event.md) explicitly retires an active extension promise instead of claiming per-step assembly makes notifications redundant. Both refined baseline findings remain useful and are retained.
|
||||
|
||||
The three revised-only local findings were a mirrored provider reference, copied filesystem results, and duplicate assembler ordering state. They were counted separately and were not written as active design proposals.
|
||||
|
||||
Review deferred replacing automatic Session-reference sizing with a fixed byte default. The [current budget decision](../bug-fix/2026-09-05-session-reference-model-budget.md) fixes lost useful context on large models; best-effort spill retrieval does not replace inline context. No new workload evidence justified restoring the problematic default. The proposal's deletion inventory was accurate, but that alone did not overcome the current requirement.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep caller counts as the main filter.** This misses producer support, dynamic consumers, and useful explicit behavior reductions. More symbol matches do not establish which obligations can disappear.
|
||||
|
||||
**Encode historical successes as a deletion checklist.** Architecture and consumers change. The reference uses mechanisms and counterexamples without making frozen records current authority.
|
||||
|
||||
**Select the revision by proposal count.** The observed count does not support superiority. Its demonstrated benefit is complementary discovery and explicit classification; the stronger union includes findings from both versions.
|
||||
|
||||
## Consequences
|
||||
|
||||
The skill changes survey guidance, not runtime behavior. The nine proposals still require scoped implementation and relevant tests; their future implementation checks were not run. Three local opportunities remain evaluation findings, and the unsupported budget reversal is excluded from the proposal set.
|
||||
|
||||
There is one discovery run per version/domain, no randomized order or repetition, unequal actual elapsed time within equal caps, and subjective review. Source reductions are estimates, not implemented diffs. These results establish neither statistical superiority, reproducible speedup, nor exhaustive coverage.
|
||||
|
||||
The [dependency](2026-07-26-dependencies-over-hand-rolling.md), [prose](2026-08-09-concrete-prose-names-actors-and-recorded-facts.md), [archive](2026-07-26-frozen-agent-note-archive.md), and [performance-workflow](2026-09-06-evidence-driven-performance-skill.md) decisions retain their independent responsibilities. This note adds simplification-specific discovery and evaluation criteria without superseding those owners.
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
# Agent Note: 以证据指导简化调研
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-19-evidence-led-simplification-surveys.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
仅搜索未使用的符号,可能漏掉实现成本超出必要结果的有效行为,也可能将没有固定仓库调用方的公共扩展 API 误判为死代码。原简化 skill(技能)还将发现候选与冗长的归档流程混在一起,消费方示例遗漏应用与 Python 路径,建议的提案结构也缺少必需的 `Alternatives considered` 章节。
|
||||
|
||||
## 决策
|
||||
|
||||
[简化 skill](../../../skills/dsh-find-simplifications/SKILL.md) 追踪从生产方到结果的完整路径,判断哪些区分会改变消费方动作,考虑明确收缩受支持行为,并寻找可以用权威读取替代的持续维护副本。它区分组合可用性与部署策略,并在源码之外合计删除的配置、生命周期、测试和文档成本。
|
||||
|
||||
生产使用会改变所需的取舍证据,但不会自动否决提案,也不构成实施授权。动态插件发现、已安装消费方、生成的运行时产物、profile、应用和 Python 都纳入消费方追踪。受保护的适配器与持久化设计、已发布数据、信任边界、独立 invariant 观察、同步发布、取消及达到静止状态的释放仍是约束。
|
||||
|
||||
入口将判断标准与[可选历史参考页](../../../skills/dsh-find-simplifications/references/historical-patterns.md) 分开,后者将机制与反例、残留义务配对。归档操作仍由原有归属处负责,提案结构使用规范标题。
|
||||
|
||||
## 历史证据
|
||||
|
||||
作者从 100 份英文已实现或归档简化记录中抽样 26 份:11 份已实现、15 份归档。全部九份已否决简化记录提供反例。样本涵盖无生产方变体、无效请求开关、显式输入、公共投影、派生数据、共享组合、生命周期所有权和依赖替换;这是有目的的抽样,并非随机或穷尽审查。
|
||||
|
||||
冻结的比较输入按空白分隔计数:基线入口为 2,222 词,新版入口为 1,372 词,参考页为 897 词;下方哈希标识这些已测量产物。参考页提供问题,而非当前删除指令。所有适配器都拒绝的开关不同于有效扩展 API;更简单的公共状态可以保留内部驻留差别;反馈时读取会改变观察承诺;依赖替换可能丢失确定性时钟或必要的释放行为。阅读归档记录时未追踪或修复其出站链接。没有编辑、归档或删除任何旧记录。
|
||||
|
||||
## 有界调研证据
|
||||
|
||||
源码快照固定为 #4618 之后的 master。每组使用继承相同模型和推理强度的全新 agent(智能体),获得相同归属领域、时间上限及每域最多四个提交项。发现候选的 agent 不能读取其他输出。运行时域覆盖 `core`、`session`、`session-query`、`api`、`sdk` 和 Python;执行域覆盖 `shell`、`subprocess`、`terminal`、`jobs`、`fs`、`mcp` 和 `boot`。
|
||||
|
||||
初始运行时结果产生后,增加第三个配对领域,覆盖 `llm`、`compaction`、`context`、`skill` 和 `extensions`,以扩大覆盖。该领域属于探索性补充,采用较短但双方相同的时间上限,与最初声明的两域比较分开。调研在这三个领域后结束。
|
||||
|
||||
skill 作者与当前候选隔离,根据历史记录撰写修订。协调者意外提前收到少量基线提示,但在新版运行前原样接受独立作者的修订。交给独立审查方的候选材料隐藏了分组标签;协调者整合并非盲审。冻结输入的 SHA-256 为:基线入口 `9830af2272a1a1c3832078f31a432c8abe0335a013bc6c248c072b388520e798`,新版入口 `3b101d16ae098855c9214ad7fed631a2f079b6078c83b49829ae87305f631a87`,新版参考页 `9a6b674d1eeadb691481ea09838b211b8e94c94e5a8cd928726246b1c097b2b5`。
|
||||
|
||||
这些运行结束后,评审仅将 skill 中的「earns its place」改为「is justified」;没有改变发现标准,也没有重新运行发现测试。每份提示都要求精确源码/搜索证据、生产方与消费方分类、记录重叠、净减少量、最强反对理由和验收要求。审查对每个提交项使用相同标准,区分局部编辑与长期决策,既不单凭公共可发现性,也不单凭删除行数作结论。
|
||||
|
||||
| 版本 | 领域 | 上限 | 提交数 | 原样可用的长期提案 | 调整后可用的长期提案 | 局部项 | 暂缓 | 耗时 |
|
||||
|---|---|---|---:|---:|---:|---:|---:|---|
|
||||
| 基线 | 运行时 | 12m | 3 | 1 | 2 | 0 | 0 | 6m 30s |
|
||||
| 基线 | 执行 | 12m | 3 | 3 | 0 | 0 | 0 | 8m 40s |
|
||||
| 新版 | 运行时 | 12m | 2 | 1 | 0 | 1 | 0 | 5m 05s |
|
||||
| 新版 | 执行 | 12m | 3 | 2 | 0 | 1 | 0 | 8m 19s |
|
||||
| 基线 | 模型/上下文,探索性 | 10m | 2 | 2 | 0 | 0 | 0 | 6m 02s |
|
||||
| 新版 | 模型/上下文,探索性 | 10m | 2 | 0 | 0 | 1 | 1 | 7m 51s |
|
||||
|
||||
基线产生六个原样可用的长期提案,另有两个调整后可用。新版 skill 产生三个长期提案和三个局部清理项,一项行为收缩被暂缓。Python RPC 收缩和文件系统 invariant 省略相互重叠。合并集合包含九个互不重复的长期提案;新版增加了[仅保留配置更新的 HMR](../../proposed/simplification/2026-09-19-config-only-hmr.zh.md)。该样本未显示新版的长期提案产出更高。
|
||||
|
||||
审查缩小了[查询 API 提案](../../proposed/simplification/2026-09-19-trim-session-query-convenience-api.zh.md),保留其字面子串语义不同于 FTS 的 `filterEvents`。[提示事件提案](../../proposed/simplification/2026-09-19-retire-prompt-registry-change-event.zh.md) 明确撤回当前扩展承诺,而非声称逐步骤组装让通知多余。这两项经调整的基线发现仍有价值,因此保留。
|
||||
|
||||
三个仅新版发现的局部项分别是镜像的提供方引用、复制的文件系统结果,以及重复的组装器顺序状态。它们单独计数,没有写成当前设计提案。
|
||||
|
||||
审查暂缓了将自动 Session 引用大小改为固定字节默认值的提案。[当前预算决策](../bug-fix/2026-09-05-session-reference-model-budget.zh.md) 修复了大模型丢失有用上下文的问题;尽力而为的 spill 检索不能替代内联上下文。没有新的工作负载证据支持恢复有问题的默认值。提案的删除清单准确,但仅此不足以推翻当前要求。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**继续以调用数量为主要筛选依据。** 这会遗漏生产方支持、动态消费方及有价值的明确行为收缩。更多符号匹配不能证明哪些义务可以删除。
|
||||
|
||||
**将历史成功案例编码为删除清单。** 架构与消费方会变化。参考页使用机制与反例,不将冻结记录视为当前权威。
|
||||
|
||||
**按提案数量选择修订。** 观察到的数量不支持优越性结论。已展示的收益是互补发现和明确分类;更有价值的合并集合包含两个版本的发现。
|
||||
|
||||
## 后果
|
||||
|
||||
skill 改变调研指导,不改变运行时行为。九份提案仍需限定范围的实现与相关测试;本次未运行其未来实施检查。三个局部机会保留为评估发现,缺少依据的预算逆转排除在提案集合之外。
|
||||
|
||||
每个版本/领域只有一次发现运行,没有随机顺序或重复试验,相同上限内的实际耗时不等,审查带有判断性。源码减少量是估算,并非实现 diff。这些结果不能证明统计优越性、可复现的提速或穷尽覆盖。
|
||||
|
||||
[依赖](2026-07-26-dependencies-over-hand-rolling.zh.md)、[措辞](2026-08-09-concrete-prose-names-actors-and-recorded-facts.zh.md)、[归档](2026-07-26-frozen-agent-note-archive.zh.md)和[性能工作流](2026-09-06-evidence-driven-performance-skill.zh.md)决策保留各自独立职责。本记录增加简化专用的发现与评估标准,不取代这些归属处。
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md
|
||||
2026-09-03-minimal-profiles-persistent-shell-only.md: d58210cb890e4264b02d591f213045b592ca6bfc
|
||||
2026-09-03-minimal-profiles-persistent-shell-only.zh.md: 481ef479c2dc01ae503f621df976529aeb6bdbe0
|
||||
2026-09-03-minimal-profiles-persistent-shell-only.md: 2bc4ef7ada0767d96c0ac448e19e13e8e06a945c
|
||||
2026-09-03-minimal-profiles-persistent-shell-only.zh.md: d54e5734ac04825e2d28f59d9104a01b5caf9fb7
|
||||
+1
-1
@@ -16,7 +16,7 @@ The shipped minimal compositions expose exactly one platform-selected persistent
|
||||
|
||||
The standalone editor package remains available for explicit custom compositions. A trusted user-authored preset or higher profile patch must insert the editor into the Cordis tree with a filesystem provider in the same service scope; the shipped `minimal` and `sdk-minimal` defaults never insert it. The [Python SDK guide](../../../../docs/user/guide/python-sdk.md#opt-in-to-str_replace_editor) provides an executable patch example.
|
||||
|
||||
The shared [persistent Bash consumer](../../../../packages/shell/tool-bash-persistent/README.md#model-experience) uses the one-shot shell's command-status wording while retaining its persistent state. Settled commands append `[Command finished with exit code N]`, including success; timeout output includes `[Command timed out or OOM]` and the shell-reset notice. Trailing newlines are removed before the status trailer. Both minimal Bash descriptions state that network access depends on the task environment. Explicit compositions using this consumer share its output behavior; the persistent PowerShell description and output remain unchanged.
|
||||
The shared [persistent Bash consumer](../../../../packages/shell/tool-bash-persistent/README.md#model-experience) uses the one-shot shell's command-status wording while retaining its persistent state. Settled commands append `[Command finished with exit code N]`, including success; timeout output includes `[Command timed out or OOM]` and the shell-reset notice. Trailing newlines are removed before the status trailer. Both minimal Bash descriptions state that network access depends on the task environment. Explicit compositions using this consumer share its output behavior. Persistent Bash and PowerShell settle cancelled commands only after shell cleanup; the existing ToolRuntime then publishes `ABORTED` instead of stringifying the user's cancellation object. Queued cancelled calls send no command, and execution or cleanup failures retain their errors.
|
||||
|
||||
Exact composition tests assert the single tool and the absence of a preset-local filesystem service. The `sdk-minimal` bundle test and built config dump assert that its row and dependency allowlists contain neither `fs-local` nor `dsh-tool-str-replace-editor`. Web and packaged-Python model-visible snapshots pin the one-tool schema roster. SDK profile smoke tests execute the guide's editor patch and verify file creation and viewing.
|
||||
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ Status: implemented
|
||||
|
||||
独立 editor 包仍可用于显式自定义组合。受信任的用户自定义 preset 或更高优先级的 profile patch 必须将 editor 插入 Cordis tree,并在同一服务作用域内提供文件系统后端;随附的 `minimal` 与 `sdk-minimal` 默认组合不会插入它。[Python SDK 指南](../../../../docs/user/guide/python-sdk.zh.md#opt-in-to-str_replace_editor)提供可执行的 patch 示例。
|
||||
|
||||
共享的[持久 Bash 消费方](../../../../packages/shell/tool-bash-persistent/README.zh.md#model-experience)保留跨调用状态,并采用单次 shell 的命令状态文案。完成的命令追加 `[Command finished with exit code N]`,成功时也包含该标记;超时输出包含 `[Command timed out or OOM]` 和 shell 重置说明。追加状态标记前会移除输出末尾的全部换行。两份极简 Bash 描述都说明网络访问取决于任务环境。使用该消费方的显式组合共享相同的输出行为;持久 PowerShell 的描述与输出保持不变。
|
||||
共享的[持久 Bash 消费方](../../../../packages/shell/tool-bash-persistent/README.zh.md#model-experience)保留跨调用状态,并采用单次 shell 的命令状态文案。完成的命令追加 `[Command finished with exit code N]`,成功时也包含该标记;超时输出包含 `[Command timed out or OOM]` 和 shell 重置说明。追加状态标记前会移除输出末尾的全部换行。两份极简 Bash 描述都说明网络访问取决于任务环境。使用该消费方的显式组合共享相同的输出行为。持久 Bash 和 PowerShell 在 shell 清理完成后才结束已取消命令,由现有 ToolRuntime 发布 `ABORTED`,不把用户取消对象转成字符串。排队期间被取消的调用不发送命令,执行或清理失败仍保留各自的错误。
|
||||
|
||||
精确组合测试会断言单工具清单以及 preset 内不存在文件系统服务。`sdk-minimal` bundle 测试与构建后配置转储会断言配置项和依赖 allowlist 都不含 `fs-local` 或 `dsh-tool-str-replace-editor`。Web 与打包 Python 的模型可见快照会固定单工具 schema 清单。SDK profile 冒烟测试会执行指南中的 editor patch,并验证文件创建和查看。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md
|
||||
2026-06-19-acp-snapshot-tests.md: d6fd6f74342fda3e1f3c686376f521bf82546e85
|
||||
2026-06-19-acp-snapshot-tests.zh.md: dfd09f9d9036763c5fd98393f078bf1772aa0beb
|
||||
2026-06-19-acp-snapshot-tests.md: 3a21d09609d45b574d7f19c3704a44dde5a3d17c
|
||||
2026-06-19-acp-snapshot-tests.zh.md: e9f3682289a05e4e54db2de3adcd3bee7042ef1b
|
||||
@@ -57,7 +57,7 @@ A snapshot run asserts **two** normalized outputs, because the harness's externa
|
||||
|
||||
The surfaces are complementary: stdout covers the minimal automation wire, while JSONL covers loop, tool, and boundary structure that the wire intentionally omits.
|
||||
|
||||
Normalization replaces session, cwd, protocol-id, timestamp, path, and process volatility; fixture projection omits body sequence/time envelopes without changing payload references. Record and refresh also store a generated workspace and its filesystem-resolved aliases as `{{cwd}}` in the replay fixture, so platform temp roots and random basenames do not affect recordings; authored temp paths and cwd values under an explicit `workspaceParent` remain literal. Scenarios constrain real bash use to stable commands. The stdout expected output remains wire-shaped JSONL and every raw line must parse as JSON. Ordinary Vitest snapshot updates write only the stdout expected output; the explicit `record` and `refresh` modes own replay-fixture writes.
|
||||
Normalization replaces session, cwd, protocol-id, timestamp, path, and process volatility; fixture projection omits body sequence/time envelopes without changing payload references. The shared snapshot writer expands top-level `sourceEventSeqs` ranges before saving, so record and refresh produce canonical fixture bytes independently of replay normalization. Record and refresh also store a generated workspace and its filesystem-resolved aliases as `{{cwd}}` in the replay fixture, so platform temp roots and random basenames do not affect recordings; authored temp paths and cwd values under an explicit `workspaceParent` remain literal. Scenarios constrain real bash use to stable commands. The stdout expected output remains wire-shaped JSONL and every raw line must parse as JSON. Ordinary Vitest snapshot updates write only the stdout expected output; the explicit `record` and `refresh` modes own replay-fixture writes.
|
||||
|
||||
### Isolation: normalization now, sandbox later
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ Log 从持久 Assistant settlement 与显式标记的 compaction call 推导 chu
|
||||
|
||||
两个表面互补:stdout 覆盖精简的自动化协议格式,JSONL 覆盖协议格式有意省略的循环、工具和边界结构。
|
||||
|
||||
规范化会替换会话、cwd、协议 id、时间戳、路径和进程易变值;fixture 投影会省略正文序号/时间 envelope,而不修改 payload 引用。录制与刷新还会在回放 fixture 中将生成的 workspace 及其文件系统解析出的别名存储为 `{{cwd}}`,使平台临时根目录和随机 basename 不影响录制结果;手工编写的临时路径与显式 `workspaceParent` 下的 cwd 值仍保留字面值。场景把真实 bash 使用限制在稳定命令上。stdout 预期输出仍是符合协议格式的 JSONL,每个原始行都必须可解析为 JSON。普通 Vitest 快照更新只写入 stdout 预期输出;回放 fixture 的写入由显式 `record` 和 `refresh` 模式负责。
|
||||
规范化会替换会话、cwd、协议 id、时间戳、路径和进程易变值;fixture 投影会省略正文序号/时间 envelope,而不修改 payload 引用。共享快照写入器在保存前展开顶层 `sourceEventSeqs` 区间,使录制与刷新直接产出规范 fixture 字节,无需依赖回放时的规范化。录制与刷新还会在回放 fixture 中将生成的 workspace 及其文件系统解析出的别名存储为 `{{cwd}}`,使平台临时根目录和随机 basename 不影响录制结果;手工编写的临时路径与显式 `workspaceParent` 下的 cwd 值仍保留字面值。场景把真实 bash 使用限制在稳定命令上。stdout 预期输出仍是符合协议格式的 JSONL,每个原始行都必须可解析为 JSON。普通 Vitest 快照更新只写入 stdout 预期输出;回放 fixture 的写入由显式 `record` 和 `refresh` 模式负责。
|
||||
|
||||
### 隔离:当前靠归一化,后续可加沙箱
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-09-19-config-only-hmr.md
|
||||
2026-09-19-config-only-hmr.md: a5f447a8a426e9f2831a1292cae1132acab99023
|
||||
2026-09-19-config-only-hmr.zh.md: 24651aa310994b3fec4d925a84fe5d88ae6a5851
|
||||
@@ -0,0 +1,36 @@
|
||||
# Agent Note: Limit DSH HMR to live profile configuration
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-09-19-config-only-hmr.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
[DSH HMR](../../../../packages/boot/hmr/src/index.ts) owns both serialized profile configuration refresh and running JavaScript module replacement. Module replacement adds Node-loader internals, dependency traversal, cache backup, old/new plugin fibers, rollback, module watches, and import-error formatting. The [base composition](../../../../packages/bundle/base/cordis.patch.yml) uses `root: []`; other shipped profiles disable or omit HMR. The default product requires configuration refresh, while source replacement serves an explicit custom-profile opt-in.
|
||||
|
||||
That opt-in is real supported behavior: [profile tests](../../../../apps/cli/tests/profile-hmr.spec.ts), module tests, and a [built CLI test](../../../../apps/cli/tests/built-bin.e2e.ts) exercise it, and generated inspection APIs advertise its methods and events. This proposal trades developer continuity for a smaller maintained subsystem; it does not classify the code as unreachable.
|
||||
|
||||
## Proposal
|
||||
|
||||
Keep `watchConfig`, `runExclusive`, exact profile/home patch and manifest watches, application-readiness coordination, serialized refresh, and disposal drainage. Require a process restart for source-module, arbitrary Include, and framework-dependency changes; remove automatic framework-change calls to `loader.exit()`. Keep browser Client loading independent.
|
||||
|
||||
Remove the module watcher and dispatch, dependency graph analysis, module-cache/fiber replacement and restoration, module-only state/options, `baseDir`, `getOuterStack`, `getLinked`, and `hmr/change`/`hmr/reload`. Remove the [import-error formatter](../../../../packages/boot/hmr/src/error.ts), module-only dependencies, and dedicated module/error tests. Keep Chokidar and configuration watcher options actually used by [exact watches](../../../../packages/boot/hmr/src/watch-config.ts). Roughly 400 identified source lines and 372 dedicated test lines belong to the removable module paths; retained configuration code and mixed tests must be counted separately in implementation.
|
||||
|
||||
Update the [profile-management decision](../../implemented/architecture/2026-09-14-current-profile-plugin-management.md) and [single-launcher decision](../../implemented/architecture/2026-08-22-single-dsh-application-launcher.md) only where they promise module replacement or its coordination. Their configuration, installation, and launcher responsibilities remain. The [non-transactional Loader decision](../../implemented/simplification/2026-09-09-nontransactional-loader.md) motivates avoiding duplicated rollback but does not already authorize withdrawing source HMR.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep opt-in source replacement.** Plugin authors can edit code without restarting an active Session, and failed replacements restore old plugins. That capability is useful; this proposal deliberately gives it up to remove loader-version coupling and replacement-state ownership from DSH.
|
||||
|
||||
**Remove only rollback or delegate replacement elsewhere.** Partial removal can leave cached modules and live fibers from different generations. A wrapper around another HMR implementation retains the public behavior and coordination burden. Neither establishes the complete deletion proposed here.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Live profile/home patch and bundle-list changes, Plugin Manager serialization, readiness, self-removal, disposal drainage, and recovery after a later valid edit retain coverage.
|
||||
- Source and arbitrary Include changes take effect on restart. Removed source options receive an explicit migration/configuration error; update shipped `root: []` rows rather than silently ignoring obsolete options.
|
||||
- Remove module-only APIs, events, dependencies, generated declarations, and tests. Preserve the remaining public queue/watch APIs and package installation/restart semantics.
|
||||
- Run focused HMR configuration/watch/coordination and Plugin Manager tests, app-boot reload coverage, and a real built CLI profile-refresh scenario. Replace source-replacement expectations with the agreed restart behavior and update required assembled snapshots and documentation.
|
||||
|
||||
## Risks
|
||||
|
||||
Custom profiles and installed plugins lose live server-code replacement and its failure restoration. Restarting can interrupt long-lived development work; acceptance therefore requires agreement to this product trade-off. Removing shared configuration coordination or recreating source replacement in another package would defeat the proposal.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Agent Note: 将 DSH HMR 限定为实时 profile 配置更新
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-09-19-config-only-hmr.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[DSH HMR](../../../../packages/boot/hmr/src/index.ts) 同时拥有串行 profile 配置刷新和运行中的 JavaScript 模块替换。模块替换增加了 Node loader 内部接口、依赖遍历、缓存备份、新旧插件 fiber、回滚、模块监听与导入错误格式化。[基础组合](../../../../packages/bundle/base/cordis.patch.yml) 使用 `root: []`;其他随产品发布的 profile 禁用或省略 HMR(热模块替换)。默认产品需要配置刷新,而源码替换服务于自定义 profile 的显式启用。
|
||||
|
||||
该选项是真实受支持的行为:[profile 测试](../../../../apps/cli/tests/profile-hmr.spec.ts)、模块测试和[构建后 CLI 测试](../../../../apps/cli/tests/built-bin.e2e.ts) 都覆盖它,生成的检查 API 也展示其方法和事件。本提案以开发连续性换取更小的维护子系统,并不将代码判定为不可达。
|
||||
|
||||
## 提案
|
||||
|
||||
保留 `watchConfig`、`runExclusive`、精确的 profile/home 补丁与 manifest(元数据清单)监听、应用就绪协调、串行刷新和释放排空。源码模块、任意 Include 和框架依赖的修改要求重启进程;删除框架变化时对 `loader.exit()` 的自动调用。浏览器 Client 加载保持独立。
|
||||
|
||||
删除模块监听与分发、依赖图分析、模块缓存/fiber 替换与恢复、仅供模块使用的状态/选项、`baseDir`、`getOuterStack`、`getLinked` 及 `hmr/change`/`hmr/reload`。删除[导入错误格式化器](../../../../packages/boot/hmr/src/error.ts)、仅供模块使用的依赖和模块/错误专用测试。保留 Chokidar 及[精确监听](../../../../packages/boot/hmr/src/watch-config.ts) 实际使用的配置监听选项。已定位约 400 行源码和 372 行专用测试属于可删除模块路径;实现时必须单独计算保留的配置代码和混合测试。
|
||||
|
||||
仅修改[profile 管理决策](../../implemented/architecture/2026-09-14-current-profile-plugin-management.zh.md) 与[单一启动器决策](../../implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md) 中承诺模块替换及其协调的部分。其配置、安装与启动器职责保留。[非事务式 Loader 决策](../../implemented/simplification/2026-09-09-nontransactional-loader.zh.md) 为避免重复回滚提供理由,但尚未授权撤回源码 HMR。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留可选源码替换。** 插件作者可以在不重启活动 Session 的情况下编辑代码,失败替换会恢复旧插件。这项能力有价值;本提案有意放弃它,以移除 DSH 对 loader 版本的耦合及替换状态所有权。
|
||||
|
||||
**仅删除回滚或将替换委托到别处。** 部分删除可能让缓存模块和活动 fiber 属于不同代。包装另一个 HMR 实现仍保留公共行为及协调负担。这两种方式都不能实现此处提出的完整删除。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 实时 profile/home 补丁与组合包列表变化、Plugin Manager 串行化、就绪、自身移除、释放排空及后续有效编辑后的恢复仍有覆盖。
|
||||
- 源码和任意 Include 修改在重启后生效。删除的源码选项给出明确迁移/配置错误;更新随产品发布的 `root: []` 项,不静默忽略过时选项。
|
||||
- 删除仅供模块使用的 API、事件、依赖、生成声明与测试。保留剩余公共队列/监听 API,以及包安装/重启语义。
|
||||
- 运行聚焦的 HMR 配置/监听/协调及 Plugin Manager 测试、app-boot 重载覆盖与真实构建后 CLI(命令行界面)profile 刷新场景。将源码替换预期改为已同意的重启行为,并更新所需的组装快照与文档。
|
||||
|
||||
## 风险
|
||||
|
||||
自定义 profile 和已安装插件会失去实时服务端代码替换及其失败恢复。重启可能中断长时间开发工作,因此接受该提案需要同意此产品取舍。删除共享配置协调,或在另一个包重建源码替换,都会违背提案目的。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-09-19-omit-fs-payload-invariant.md
|
||||
2026-09-19-omit-fs-payload-invariant.md: e91fa1784aa99869e3d457f7fc25a330fbc43088
|
||||
2026-09-19-omit-fs-payload-invariant.zh.md: ba81baed5f7d7051ef22218d9d59180ac87c8a21
|
||||
@@ -0,0 +1,34 @@
|
||||
# Agent Note: Omit the filesystem payload-only invariant companion
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-09-19-omit-fs-payload-invariant.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The [filesystem invariant](../../../../packages/fs/fs/src/invariant.ts) checks non-empty target strings, a non-empty present version, and an observation discriminant. It reads one dispatch payload, retains no history, and compares no provider, policy, filesystem, or independently mutable state. Its [tests](../../../../packages/fs/fs/tests/invariant.spec.ts) construct malformed payloads directly. Publishing this check costs a companion module, export/build wiring, invariant dependencies, compiler references, and dedicated tests.
|
||||
|
||||
Current guidance needs an explicit decision: the [independent-observation rule](../../implemented/simplification/2026-08-28-omit-unneeded-invariant-companions.md) requires observations that can independently diverge, while the [runtime-contract examples](../../implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md) specifically endorse filesystem target/version validity. The later rule retains the earlier note's semantic authority, so chronology alone does not resolve this conflict.
|
||||
|
||||
## Proposal
|
||||
|
||||
Apply the independent-observation criterion to this concrete companion and remove it. The event producers are independent plugins, but checking the format of each single payload does not compare their observations. Keep typed filesystem events and the real [observation-policy consumer](../../../../packages/fs/fs-observation-policy/src/index.ts), along with parser, wire, read, edit, and write validation at their existing owners.
|
||||
|
||||
Remove the 48-line companion, its 55-line dedicated test, `./invariant` export/publication, invariant-only dependencies and compiler references. Add the package-specific omission reason to both fs READMEs. Amend the earlier representative-check row when implementing this decision, retaining its other semantic examples and the shared invariant registration machinery. This proposal leaves those current owners unchanged until acceptance.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep payload validation as an explicit policy exception.** It can detect malformed third-party emissions early, but this would deliberately broaden the stated independent-observation criterion. The proposal prefers input validation at its owner and reserves companions for relational checks.
|
||||
|
||||
**Add a second state source to justify the companion.** Rejected without a concrete missing invariant. Duplicating policy state or exposing new API solely to retain this diagnostic increases the maintenance burden.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Resolve the named policy conflict in the active owning notes; do not claim this is already an uncontested mechanical omission.
|
||||
- No companion source, test, export, artifact entry, invariant-only dependency, or compiler reference remains; package topology checks accept the explained omission.
|
||||
- Real file-tool and observation-policy tests retain stale-version, absent-target, alias, actor-routing, and world-state assertions. The events and consumers remain.
|
||||
- Run focused fs and observation-policy tests, relevant file-tool integration checks, source/built invariant-topology checks, build/hygiene, doc-sync, and lint. Do not replace the module with equivalent generic payload-validation machinery.
|
||||
|
||||
## Risks
|
||||
|
||||
Malformed trusted-plugin emissions lose this opt-in early diagnostic. The check's removal does not justify weakening validation of external data, nor removing other companions that compare lifecycle, durable history, or independently assembled values. If payload-format diagnostics are intentionally part of the companion policy, retain this module and clarify that exception instead.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Agent Note: 省略仅校验载荷的文件系统 invariant 配套模块
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-09-19-omit-fs-payload-invariant.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[文件系统 invariant](../../../../packages/fs/fs/src/invariant.ts) 检查目标字符串非空、present 版本非空及观察值的判别标签。它只读取一次分发的载荷,不保留历史,也不与提供方、策略、文件系统或独立可变状态比较。其[测试](../../../../packages/fs/fs/tests/invariant.spec.ts) 直接构造格式错误的载荷。发布该检查需要配套模块、导出/构建配置、invariant 依赖、编译器引用和专用测试。
|
||||
|
||||
当前指导需要显式决策:[独立观察规则](../../implemented/simplification/2026-08-28-omit-unneeded-invariant-companions.zh.md) 要求能够独立分歧的观察值,而[运行时约定示例](../../implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md) 明确认可文件系统目标/版本有效性检查。后者的语义权威仍被较新的规则保留,因此不能仅凭时间先后解决冲突。
|
||||
|
||||
## 提案
|
||||
|
||||
将独立观察标准应用于这个具体配套模块,并删除它。事件生产方来自独立插件,但逐一检查单个载荷的格式,并没有比较它们的观察值。保留类型化文件系统事件和实际的[观察策略消费方](../../../../packages/fs/fs-observation-policy/src/index.ts),以及现有归属处的解析器、wire、读取、编辑和写入校验。
|
||||
|
||||
删除 48 行配套模块、55 行专用测试、`./invariant` 导出/发布项、仅供 invariant 使用的依赖和编译器引用。在 fs 的两个 README 中补充包特有的省略理由。实现本决策时修改较早的代表性检查行,保留其他语义示例及共享 invariant 注册机制。本提案获接受前不修改这些当前归属文档。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**将载荷校验保留为显式策略例外。** 它能提前发现第三方格式错误的事件,但这会有意扩大已声明的独立观察标准。提案倾向于在输入归属处校验,并将配套模块留给关系检查。
|
||||
|
||||
**添加第二个状态源以证明配套模块有必要。** 在没有具体缺失 invariant 的情况下否决。仅为保留此诊断而复制策略状态或暴露新 API,会增加维护负担。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 在当前归属 Agent Notes 中解决已指出的策略冲突;不将其宣称为已有共识的机械省略。
|
||||
- 不再保留配套源码、测试、导出、产物条目、仅供 invariant 使用的依赖或编译器引用;包拓扑检查接受有说明的省略。
|
||||
- 真实文件工具和观察策略测试保留过期版本、目标缺失、别名、执行方路由及外部状态断言。事件及其消费方保留。
|
||||
- 运行聚焦的 fs 与观察策略测试、相关文件工具集成检查、源码/构建产物 invariant 拓扑检查、build/hygiene、doc-sync(文档同步门禁)和 lint。不用等价的通用载荷校验机制替换该模块。
|
||||
|
||||
## 风险
|
||||
|
||||
来自受信任插件的格式错误事件会失去这项可选的早期诊断。删除该检查不构成削弱外部数据校验的理由,也不意味着可以删除比较生命周期、持久化历史或独立组装值的其他配套模块。如果载荷格式诊断本来就是配套模块策略的一部分,应保留模块并澄清该例外。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-09-19-provider-only-instruction-reads.md
|
||||
2026-09-19-provider-only-instruction-reads.md: 26d004c136cff8deb155007cf8d7743bc8bba95a
|
||||
2026-09-19-provider-only-instruction-reads.zh.md: 82f3451f96578f1ee596bf9291a1a3c935d5f698
|
||||
@@ -0,0 +1,37 @@
|
||||
# Agent Note: Read instruction files only through a filesystem provider
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-09-19-provider-only-instruction-reads.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
[Instruction discovery](../../../../packages/context/agent-instructions/src/files.ts) maintains provider and direct-Node paths for metadata probes, root-marker discovery, and bounded reads. `nodeStatFile`, `nodeTextChunks`, and optional provider/target fields support the latter. The exported `discoverBaselineInstructionFiles` wrapper selects it; `loadBaselineInstructions` permits it when callers omit a provider.
|
||||
|
||||
Every repository production caller supplies a provider. The [plugin](../../../../packages/context/agent-instructions/src/index.ts) reads `ctx.get('fs')` and returns when absent; [reconciliation](../../../../packages/context/agent-instructions/src/state.ts) requires `FileSystem`. Direct-Node consumers are tests. Maintaining both paths therefore adds separate error and cancellation coverage without serving a shipped product path.
|
||||
|
||||
## Proposal
|
||||
|
||||
Require `FileSystem` throughout discovery and loading, including both exported wrappers and `findProjectRoot`. Remove `nodeStatFile`, `nodeTextChunks`, Node-only missing-path classification, the Node branch of `existsAsMarker`, and the `statFile` selector. Provider discovery supplies required targets; bounded reads consume those targets through `streamText`.
|
||||
|
||||
Keep the plugin's missing-provider no-op and the pure `renderAgentInstructions` API. Preserve candidate precedence, source and rendered-output byte budgets, cancellation, symlink following, and error semantics: unavailable candidates cannot imply removal, while root-marker failures remain errors. The separate `skill-filesystem` owner retains its existing providerless behavior.
|
||||
|
||||
Migrate direct helper tests to explicitly owned local or controlled providers. The [test suite](../../../../packages/context/agent-instructions/tests/agent-instructions.spec.ts) already mounts both. Preserve unique assertions and consolidate duplicate Node-mocking cases into provider coverage. Expected deletion is roughly 50–65 production lines before signature/documentation updates, plus redundant test machinery; measure the actual reduction during implementation.
|
||||
|
||||
The [symlink decision](../../implemented/feature/2026-07-21-follow-instruction-symlinks.md) remains active because its accepted behavior survives. At implementation, update its references to the two implementations and the [package README](../../../../packages/context/agent-instructions/README.md), including bilingual counterparts. The [archived workspace-context note](../../archived/feature/2026-06-24-workspace-context.md) remains frozen; no existing note changes accompany this proposal.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep providerless convenience exports.** They let external callers load instructions without constructing a provider, but require a second I/O implementation that production bypasses. This proposal deliberately gives up that convenience; repository searches do not establish external non-use.
|
||||
|
||||
**Construct a local provider implicitly.** This preserves the omitted argument while hiding filesystem selection and lifecycle ownership. Callers should supply the implementation they intend to use.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- This owner's discovery/read helpers require a provider and contain no direct-Node I/O fallback; providerless plugin composition still loads nothing.
|
||||
- Focused tests preserve symlink, absence/unavailability, marker-error, budget, cancellation, resume, nested-touch, and compaction-restoration behavior.
|
||||
- The workspace-context resume process expectation and `ptc-workspace-context` scenario retain their text and event order. Typecheck, affected builds, and documentation checks pass.
|
||||
|
||||
## Risks
|
||||
|
||||
External callers omitting the provider face an intentional pre-stable API change. Identify such requirements before implementation. Keep filesystem extension paths intact, and do not replace useful provider tests with a new convenience wrapper that recreates the removed fallback.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Agent Note: 仅通过文件系统提供方读取指令文件
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-09-19-provider-only-instruction-reads.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[指令发现](../../../../packages/context/agent-instructions/src/files.ts)为元数据探测、根标记发现和有界读取维护了提供方与直接 Node 两条路径。`nodeStatFile`、`nodeTextChunks` 和可选的提供方/目标字段支持后者。导出的 `discoverBaselineInstructionFiles` 包装函数选择该路径;`loadBaselineInstructions` 则允许调用方通过省略提供方来使用它。
|
||||
|
||||
仓库中的每个生产调用方都传入提供方。[插件](../../../../packages/context/agent-instructions/src/index.ts)读取 `ctx.get('fs')`,缺失时直接返回;[协调逻辑](../../../../packages/context/agent-instructions/src/state.ts)要求传入 `FileSystem`。直接 Node 路径的消费方都是测试。因此,维护两条路径增加了独立的错误与取消覆盖,却没有服务于已交付的产品路径。
|
||||
|
||||
## 提案
|
||||
|
||||
在整个发现与加载链路中要求传入 `FileSystem`,包括两个导出的包装函数和 `findProjectRoot`。移除 `nodeStatFile`、`nodeTextChunks`、仅供 Node 使用的路径缺失分类、`existsAsMarker` 的 Node 分支以及 `statFile` 选择器。提供方发现过程给出必需的目标;有界读取通过 `streamText` 消费这些目标。
|
||||
|
||||
保留插件缺少提供方时不执行操作的行为,以及纯函数 `renderAgentInstructions` API。保留候选优先级、源文件与渲染输出的字节预算、取消、符号链接跟随和错误语义:候选不可用不能表示应移除已有内容,根标记失败仍是错误。独立的 `skill-filesystem` 所有者保留其现有的无提供方行为。
|
||||
|
||||
让直接调用辅助函数的测试改用显式管理生命周期的本地或受控提供方。[测试套件](../../../../packages/context/agent-instructions/tests/agent-instructions.spec.ts)已经挂载了这两类实现。保留独有断言,把重复的 Node mock 场景合并到提供方覆盖中。预计在更新签名和文档之前删除约 50–65 行生产源码,另加冗余测试机制;实施时测量实际减少量。
|
||||
|
||||
[符号链接决策](../../implemented/feature/2026-07-21-follow-instruction-symlinks.zh.md)所接受的行为继续存在,因此该说明保持现行状态。实施时,更新其中对两种实现的引用和[包 README](../../../../packages/context/agent-instructions/README.zh.md),包括双语对侧。[已归档的工作区上下文说明](../../archived/feature/2026-06-24-workspace-context.md)保持冻结;本提案不附带任何既有说明变更。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**保留无提供方的便利导出。** 它们让外部调用方无需构造提供方便可加载指令,但也要求保留生产路径不使用的第二套 I/O 实现。本提案有意放弃这种便利;仓库搜索不能确定外部无人使用。
|
||||
|
||||
**隐式构造本地提供方。** 这会保留省略参数的调用方式,却隐藏文件系统选择和生命周期归属。调用方应传入它们打算使用的实现。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 本所有者的发现/读取辅助函数要求提供方,且不含直接 Node I/O 回退;无提供方的插件组合仍不加载任何内容。
|
||||
- 针对性测试保留符号链接、缺失/不可用、标记错误、预算、取消、恢复、嵌套访问和压缩(compaction)后恢复的行为。
|
||||
- 工作区上下文恢复的进程预期输出和 `ptc-workspace-context` 场景保持原有文本及事件顺序。类型检查、受影响构建和文档检查通过。
|
||||
|
||||
## 风险
|
||||
|
||||
省略提供方的外部调用方面临有意的稳定前 API 变更。实施前确认这类需求。保留文件系统扩展路径,也不要用重新创建已移除回退的新便利包装函数替换有用的提供方测试。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-09-19-prune-stream-visibility-helpers.md
|
||||
2026-09-19-prune-stream-visibility-helpers.md: e44716fe15eb0ff09020ff4417b1dd9f4e9c29f4
|
||||
2026-09-19-prune-stream-visibility-helpers.zh.md: 60d010f260a6cadc99a15bde164bb0717dd7502a
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Prune unused stream visibility helpers
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-09-19-prune-stream-visibility-helpers.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The [compact Assistant stream implementation](../../../../packages/llm/llm/src/assistant-stream.ts) exports five visibility queries: `isVisibleChunk`, `chunkHasVisibleText`, `runFirstVisibleTime`, `assistantStreamHasVisibleContent`, and `assistantStreamHasVisibleText`. Repository searches find no production caller outside that family. Its other consumers are the [unit tests](../../../../packages/llm/llm/tests/assistant-stream.spec.ts), package documentation, and the [record-reader decision](../../implemented/architecture/2026-09-06-embedded-stream-record-readers.md). The package root and published `./assistant-stream` entry expose the functions to external callers, whose usage this search cannot establish.
|
||||
|
||||
The retained first-token reader has current consumers in [Session Stats](../../../../packages/session/session-stats/src/projection.ts) and [Trajectory](../../../../packages/client/ui-trajectory/src/client/trajectory-assistant-definition.ts). Their timing requirements do not require the separate whitespace-sensitive visibility classification.
|
||||
|
||||
## Proposal
|
||||
|
||||
Remove the five exported visibility queries and their exclusive private helpers, `hasNonWhitespace` and `blockIsVisible`. This deletes approximately 77 source lines, including local documentation, without adding replacement machinery. Retain all first-token, raw-chunk, text-joining, and assembly readers, the accumulator, and validating expansion. Preserve the direct and library-backed LLM adapters and their extension APIs.
|
||||
|
||||
Remove dedicated visibility tests and only the obsolete assertions inside mixed reader tests. Keep the mixed fixtures and assertions that verify first-token timing, early exit, raw chunks, text joining, and assembly equivalence.
|
||||
|
||||
The record-reader decision is only partially superseded: its allocation rationale and used readers remain current. When implementing this proposal, update that active note, the [package README](../../../../packages/llm/llm/README.md), and the [LLM subsystem page](../../../../docs/subsystems/llm-streaming.md) with their bilingual counterparts. Do not delete the owning note or alter archived records while adding this proposal.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep a complete reader family for external consumers.** This preserves convenient, tested visibility queries, but keeps public functions and dedicated tests without a demonstrated repository consumer. A named external requirement would justify retaining or reintroducing them.
|
||||
|
||||
**Replace visibility queries with stream expansion.** There is no production caller to migrate. Adding replacements would retain an unused API and introduce the allocations the record readers avoid.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- The five names disappear from source exports and current API documentation; no production caller gains an expansion or replacement visibility query.
|
||||
- Focused stream-reader, Session Stats, and Trajectory timing checks pass; mixed tests retain their unrelated assertions.
|
||||
- Typecheck, affected published-entry builds, and documentation checks pass. Session formats, model input, transcript content, and first-token timing remain unchanged.
|
||||
|
||||
## Risks
|
||||
|
||||
This deliberately contracts exported pre-stable APIs. An external import may break even though repository consumers are absent. Refresh consumer evidence before implementation and assess any identified external requirement explicitly; do not claim ecosystem-wide non-use.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Agent Note: 精简未使用的流可见性辅助函数
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-09-19-prune-stream-visibility-helpers.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[紧凑 Assistant 流实现](../../../../packages/llm/llm/src/assistant-stream.ts)导出五个可见性查询:`isVisibleChunk`、`chunkHasVisibleText`、`runFirstVisibleTime`、`assistantStreamHasVisibleContent` 和 `assistantStreamHasVisibleText`。仓库搜索未找到这组函数之外的生产调用方。其余消费方是[单元测试](../../../../packages/llm/llm/tests/assistant-stream.spec.ts)、包文档和[记录读取器决策](../../implemented/architecture/2026-09-06-embedded-stream-record-readers.zh.md)。包根入口和发布的 `./assistant-stream` 入口向外部调用方暴露这些函数;本次搜索无法确定外部使用情况。
|
||||
|
||||
保留的首 token 读取器已有 [Session Stats](../../../../packages/session/session-stats/src/projection.ts) 和 [Trajectory](../../../../packages/client/ui-trajectory/src/client/trajectory-assistant-definition.ts) 消费方。它们的计时要求不依赖单独区分空白文本的可见性分类。
|
||||
|
||||
## 提案
|
||||
|
||||
移除这五个导出的可见性查询及其专用私有辅助函数 `hasNonWhitespace` 和 `blockIsVisible`。这会删除约 77 行源码(含局部文档),无需新增替代机制。保留所有首 token、原始分片、文本拼接和组装读取器,以及累加器和带校验的展开操作。保留直接实现和基于库实现的两个 LLM(大语言模型)适配器及其扩展 API。
|
||||
|
||||
移除专门的可见性测试;混合读取器测试只删除过时断言。保留验证首 token 计时、提前退出、原始分片、文本拼接和组装等价性的混合 fixture(测试前置数据)与断言。
|
||||
|
||||
记录读取器决策只被部分替代:其分配开销方面的理由和仍被使用的读取器继续有效。实施本提案时,更新该现行说明、[包 README](../../../../packages/llm/llm/README.zh.md) 和 [LLM 子系统页面](../../../../docs/subsystems/llm-streaming.zh.md)及其双语对侧。添加本提案时不删除所属说明,也不修改归档记录。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**为外部消费方保留完整的读取器组。** 这能保留方便且经过测试的可见性查询,但也会在缺少已确认仓库消费方的情况下保留公共函数和专门测试。明确的外部需求可以成为保留或重新引入它们的理由。
|
||||
|
||||
**用流展开替代可见性查询。** 当前没有需要迁移的生产调用方。新增替代实现会继续保留未使用的 API,并引入记录读取器原本避免的分配开销。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 源码导出和现行 API 文档中不再包含这五个名称;生产调用方不新增展开操作或替代可见性查询。
|
||||
- 针对流读取器、Session Stats 和 Trajectory 计时的检查通过;混合测试保留无关断言。
|
||||
- 类型检查、受影响发布入口的构建和文档检查通过。Session 格式、模型输入、transcript(文本记录)内容和首 token 计时保持不变。
|
||||
|
||||
## 风险
|
||||
|
||||
这会有意收缩导出的稳定前 API。即使仓库中没有消费方,外部导入仍可能失效。实施前重新核对消费方证据,并明确评估发现的外部需求;不得宣称整个生态均未使用这些函数。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-09-19-python-sdk-directional-client.md
|
||||
2026-09-19-python-sdk-directional-client.md: 287126d65cc9e81e240c2b18081b2935c107760a
|
||||
2026-09-19-python-sdk-directional-client.zh.md: caf9b7c73a00b8865b6d189fe512f96afb20dad9
|
||||
@@ -0,0 +1,34 @@
|
||||
# Agent Note: Remove the Python SDK's unused reverse RPC direction
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-09-19-python-sdk-directional-client.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The [Python client](../../../../python/sdk/src/deepseek_harness/client.py) keeps an incoming-request queue, close-time wake-up, and `next_request`, `respond`, `respond_error`, and `notify` methods. The [SDK server](../../../../packages/sdk/server/src/server.ts) receives requests and emits notifications; it declares no server-originated request or client-notification handler. Searches across Python sources, examples, and runtime profiles found the reverse direction only in client implementation and [synthetic client tests](../../../../python/sdk/tests/test_client.py). The exported `IncomingRequest` model consequently has no supported runtime producer.
|
||||
|
||||
This is a narrower continuation of the [directional JSON-RPC proposal](../../rejected/simplification/2026-07-19-make-jsonrpc-directional.md), whose rejection explicitly permits transport narrowing separately. Its completion redesign remains rejected. Its claim about the shared TypeScript transport is now inapplicable: the [Codex wire adapter](../../../../packages/subagent/subagent-codex/src/wire.ts) uses requests and notifications in both directions.
|
||||
|
||||
## Proposal
|
||||
|
||||
Remove the four Python methods, `_requests`, and `IncomingRequest` from the client, models, and exports. Retain an explicit guard for an unexpected frame containing both `id` and `method` before response matching, so a server request cannot settle an unrelated pending client request. Keep client requests, response validation, incoming notification subscriptions, and shutdown unchanged.
|
||||
|
||||
Remove the dedicated synthetic reverse-request test. Change tests that send client notifications only to trigger fixture behavior to use a request and response instead; retain their filter-exception and concurrent-write assertions. This removes one queue and its lifetime, four public methods, one public model, and roughly three dozen source lines without adding a replacement transport.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep a generic Python peer for external plugins.** Custom runtimes could use the methods, but no SDK method describes their request ownership, cancellation, or delivery protocol. The proposal accepts this pre-stable API loss; a future bidirectional SDK feature must supply its actual method and lifecycle requirements.
|
||||
|
||||
**Narrow the shared TypeScript transport too.** Rejected because the Codex adapter is a current bidirectional consumer. The shared implementation and its tests remain.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Python sources and examples have no reverse-direction API or request queue; documentation and exports describe the retained client role.
|
||||
- An unexpected server request reusing a pending client id cannot complete its waiter. Notification filtering, sibling delivery, final draining, malformed-frame handling, serialized writes, and shutdown still pass their existing tests.
|
||||
- Prompt submission still returns the durable enqueue receipt; no per-prompt completion promise or response-based turn result is introduced.
|
||||
- Run the Python client suite and installed-runtime SDK smoke and snapshot checks, plus documentation and lint checks. Preserve the TypeScript transport and released Session fixtures.
|
||||
|
||||
## Risks
|
||||
|
||||
External Python callers using custom runtime plugins lose these methods. Ignoring an unsupported request does not fulfill a hypothetical server's response expectation; this is deliberate withdrawal of that direction, not protocol parity. Revisit the proposal if a supported server-originated method lands before implementation.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Agent Note: 删除 Python SDK 未使用的反向 RPC
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-09-19-python-sdk-directional-client.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[Python 客户端](../../../../python/sdk/src/deepseek_harness/client.py) 维护入站请求队列、关闭时的唤醒,以及 `next_request`、`respond`、`respond_error` 和 `notify` 方法。[SDK 服务端](../../../../packages/sdk/server/src/server.ts) 接收请求并发送通知,没有定义服务端主动发起的请求或客户端通知处理器。搜索 Python 源码、示例和运行时 profile 后,仅在客户端实现和[合成客户端测试](../../../../python/sdk/tests/test_client.py) 中发现反向通信。因此,导出的 `IncomingRequest` 模型没有受支持运行时的生产方。
|
||||
|
||||
这是[定向 JSON-RPC 提案](../../rejected/simplification/2026-07-19-make-jsonrpc-directional.zh.md) 的缩小范围续案;该提案的否决原因明确允许单独重提传输收缩。其完成语义重设计仍被否决。它对共享 TypeScript 传输的判断已不适用:[Codex wire 适配器](../../../../packages/subagent/subagent-codex/src/wire.ts) 在两个方向上都使用请求和通知。
|
||||
|
||||
## 提案
|
||||
|
||||
从客户端、模型和导出中删除四个 Python 方法、`_requests` 和 `IncomingRequest`。在匹配响应前,保留对同时含有 `id` 和 `method` 的意外帧的显式保护,避免服务端请求完成无关的待处理客户端请求。保持客户端请求、响应校验、入站通知订阅和关闭行为。
|
||||
|
||||
删除专门的合成反向请求测试。仅为触发 fixture(测试前置数据)行为而发送客户端通知的测试改用请求和响应,同时保留过滤器异常与并发写入断言。该变更删除一个队列及其生命周期、四个公共方法、一个公共模型和约三十多行源码,不增加替代传输实现。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留通用 Python 对等端供外部插件使用。** 自定义运行时可能使用这些方法,但没有 SDK 方法规定其请求所有权、取消或交付协议。提案接受这一稳定前 API 损失;未来双向 SDK 功能必须提供实际的方法与生命周期要求。
|
||||
|
||||
**同时收缩共享 TypeScript 传输。** 否决,因为 Codex 适配器是当前双向消费方。共享实现及其测试保留。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- Python 源码和示例不再包含反向 API 或请求队列;文档和导出描述保留的客户端角色。
|
||||
- 复用待处理客户端 id 的意外服务端请求不能完成其等待方。通知过滤、同级交付、末尾排空、错误帧处理、串行写入和关闭仍通过现有测试。
|
||||
- 提交提示仍返回持久化入队回执;不引入单条提示的完成承诺或基于响应的轮次结果。
|
||||
- 运行 Python 客户端测试、已安装运行时的 SDK 冒烟与快照检查,以及文档和 lint 检查。保留 TypeScript 传输和已发布 Session fixture。
|
||||
|
||||
## 风险
|
||||
|
||||
使用自定义运行时插件的外部 Python 调用方会失去这些方法。忽略不支持的请求无法满足假想服务端的响应期待;这是有意撤回该方向,而非保持协议等价。如果受支持的服务端主动请求方法先于实现落地,需要重新评估提案。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-09-19-retire-app-bin-boot-helpers.md
|
||||
2026-09-19-retire-app-bin-boot-helpers.md: c67013bfae8569d0f750b3c8be5e56c57a2a66aa
|
||||
2026-09-19-retire-app-bin-boot-helpers.zh.md: 5ef68ff3d8666e8b74d36c50eacbcf5048a4438a
|
||||
@@ -0,0 +1,36 @@
|
||||
# Agent Note: Retire app-boot helpers used only by fixtures
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-09-19-retire-app-bin-boot-helpers.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
[App boot](../../../../packages/boot/app-boot/src/index.ts) exports `resolveConfigPath`, including a basename-specific replay substitution, and `loadEnv`, which reads one optional `.env`. The supported [CLI launcher](../../../../apps/cli/src/bin.ts) uses `loadLayeredEnv`; [profile boot](../../../../apps/cli/src/profile-boot.ts) supplies an absolute path directly. Neither production path calls the older helpers.
|
||||
|
||||
An exact-symbol search across packages, apps, and scripts finds ten `resolveConfigPath` calls in eight fixture drivers, all passing `undefined` for replay mode. The only non-unit `loadEnv` caller is the [Loader smoke fixture](../../../../packages/test-support/loader-smoke/tests/fixtures/headless-driver.ts). The [snapshot launcher](../../../../packages/test-support/session-snapshot/src/launcher.ts) separately owns replay patch selection. The old helpers consequently retain two public APIs, unused production replay policy, and dedicated tests for fixture-only needs.
|
||||
|
||||
The [shared app-bin glue record](../../archived/simplification/2026-07-04-share-app-bin-boot-glue.md) explains the original shared-bin use. The active [single-launcher decision](../../implemented/architecture/2026-08-22-single-dsh-application-launcher.md) replaces those supported entry paths. This is partial supersession: `boot` and fail-loud handling still have owners, and the archived record remains frozen.
|
||||
|
||||
## Proposal
|
||||
|
||||
Delete `resolveConfigPath` and `loadEnv` from the production export. Use Node's `path.resolve` in the eight fixture drivers. Keep optional one-directory `.env` handling local to the sole fixture that needs it, including its absent-file and warning behavior; do not introduce a replacement production package.
|
||||
|
||||
Remove helper-specific tests and update app-boot's package description, README pair, and `boot` JSDoc. Keep `boot`, `loadLayeredEnv`, shipped profile composition, and snapshot replay selection. The identified removal is 39 production source/JSDoc lines and about 73 dedicated test lines, less the small fixture-local environment handling and builtin imports; the estimate is not an implemented diff.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep public conveniences for low-level embedders.** They can be useful, but current fixtures request only path resolution and one optional file read. Maintaining a second production replay selector and obsolete app-bin guidance is disproportionate to those uses.
|
||||
|
||||
**Replace every use with `loadLayeredEnv`.** Rejected because layered discovery, filtering, and precedence differ from reading one fixture-local `.env`. Sharing a name does not establish equivalent behavior.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Exact-name searches find no live helper imports or calls. All ten path replacements remain absolute and select the same files.
|
||||
- The sole environment-loading fixture preserves missing-file, present-file, and warning behavior; helper-only tests disappear without dropping retained boot coverage.
|
||||
- Run focused app-boot and affected fixture-entry tests, including built entry checks where required, a shipped profile smoke, and a keyless replay scenario.
|
||||
- Update README pairs and package/JSDoc descriptions; run build, relevant hygiene, typecheck, doc-sync, and lint. Add no compatibility aliases or second replay selector.
|
||||
|
||||
## Risks
|
||||
|
||||
Out-of-tree callers importing these pre-stable helpers must use the retained profile APIs or own their low-level path/environment policy. Accidentally switching the fixture to layered environment discovery would change its test inputs; preserve that distinction explicitly.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Agent Note: 删除仅供 fixture 使用的 app-boot 辅助函数
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-09-19-retire-app-bin-boot-helpers.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[App boot](../../../../packages/boot/app-boot/src/index.ts) 导出 `resolveConfigPath`,其中包含按文件名替换重放配置的逻辑,以及读取单个可选 `.env` 的 `loadEnv`。受支持的 [CLI 启动器](../../../../apps/cli/src/bin.ts) 使用 `loadLayeredEnv`;[profile boot](../../../../apps/cli/src/profile-boot.ts) 直接提供绝对路径。这两条生产路径都不调用旧辅助函数。
|
||||
|
||||
在 packages、apps 和 scripts 中搜索精确符号,可找到八个 fixture(测试前置数据)驱动器中的十次 `resolveConfigPath` 调用,重放模式全部传入 `undefined`。唯一非单元测试的 `loadEnv` 调用方是 [Loader 冒烟 fixture](../../../../packages/test-support/loader-smoke/tests/fixtures/headless-driver.ts)。[快照启动器](../../../../packages/test-support/session-snapshot/src/launcher.ts) 单独拥有重放补丁选择。因此,旧辅助函数仅为 fixture 的需要保留了两个公共 API、生产中未使用的重放策略及专用测试。
|
||||
|
||||
[共享 app-bin 胶水代码记录](../../archived/simplification/2026-07-04-share-app-bin-boot-glue.md) 解释了最初的共享可执行入口用途。当前[单一启动器决策](../../implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md) 已替代这些受支持入口。这属于部分取代:`boot` 和明确失败处理仍有归属,归档记录保持冻结。
|
||||
|
||||
## 提案
|
||||
|
||||
从生产导出中删除 `resolveConfigPath` 和 `loadEnv`。八个 fixture 驱动器改用 Node 的 `path.resolve`。仅在需要它的 fixture 内保留读取单目录可选 `.env` 的处理,包括文件缺失与警告行为;不引入替代生产包。
|
||||
|
||||
删除辅助函数专用测试,并更新 app-boot 的包描述、README 配对及 `boot` JSDoc。保留 `boot`、`loadLayeredEnv`、随产品发布的 profile 组合和快照重放选择。已定位可删除 39 行生产源码/JSDoc 和约 73 行专用测试,需扣除少量 fixture 本地环境处理及内置模块导入;这是估算,尚非实现 diff。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**为底层嵌入方保留公共便利函数。** 它们可能有用,但当前 fixture 仅需要路径解析和单个可选文件读取。为这些用途维护第二套生产重放选择器及过时的 app-bin 指南,成本过高。
|
||||
|
||||
**将所有调用替换为 `loadLayeredEnv`。** 否决,因为分层发现、过滤和优先级不同于读取单个 fixture 本地 `.env`。名称相近不能证明行为等价。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 精确名称搜索不再发现有效的辅助函数导入或调用。十处路径替换仍为绝对路径并选择相同文件。
|
||||
- 唯一加载环境的 fixture 保持文件缺失、文件存在和警告行为;删除辅助函数专用测试时不丢失保留的 boot 覆盖。
|
||||
- 运行聚焦的 app-boot 和受影响 fixture 入口测试,包括所需的构建产物入口检查、随产品发布的 profile 冒烟,以及无密钥重放场景。
|
||||
- 更新 README 配对及包/JSDoc 描述;运行 build、相关 hygiene、typecheck、doc-sync(文档同步门禁)和 lint。不添加兼容别名或第二个重放选择器。
|
||||
|
||||
## 风险
|
||||
|
||||
导入这些稳定前辅助函数的仓库外调用方,需要改用保留的 profile API,或自行拥有底层路径与环境策略。意外将 fixture 改为分层环境发现会改变其测试输入;必须明确保留这一差别。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-09-19-retire-prompt-registry-change-event.md
|
||||
2026-09-19-retire-prompt-registry-change-event.md: 1103675525e30c7f6c345a1fe92f07cbd0bcce4a
|
||||
2026-09-19-retire-prompt-registry-change-event.zh.md: a49df2eebad7134b7b038ee2e6ab54610bd4c635
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Retire the prompt registry change event
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-09-19-retire-prompt-registry-change-event.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
[`SystemPrompt`](../../../../packages/core/system-prompt/src/index.ts) emits `system-prompt/change` when providers register or dispose. Repository searches find no executing product listener, but generated Host discovery advertises the event. Tests maintain notification counts and rollback after listener exceptions. This is a public extension with maintenance obligations, even without a first-party subscriber.
|
||||
|
||||
The active [Remote event delivery decision](../../implemented/architecture/2026-08-10-remote-event-delivery.md) explicitly retains this extension despite having no shipped consumer. Per-step assembly predates that promise and serves a different purpose: rebuilding model input does not replace push observation of registry changes.
|
||||
|
||||
## Proposal
|
||||
|
||||
Retire `system-prompt/change` and accept the loss of prompt-registry push observation for installed and dynamically authored Host plugins. Remove its declaration, emitter, current documentation, generated discovery entries, and notification-specific tests. No currently supported product path requires this extension.
|
||||
|
||||
Make [`ScopedLayers`](../../../../packages/core/scope/src/store.ts)'s constructor notification callback optional and omit it for `SystemPrompt`; do not supply a no-op callback. Remove the existing no-op arguments in [jobs-local](../../../../packages/jobs/jobs-local/src/index.ts) and [mcp-resources](../../../../packages/mcp/mcp-resources/src/index.ts) when updating this shared constructor, and update jobs-local's adjacent notification explanation. Preserve shared effects, action rollback, undo ordering, layer reclamation, and `tools/change` with its production consumers. Keep prompt-provider evaluation, scope shadowing, and assembly invariants.
|
||||
|
||||
Implementation must amend the active Remote event delivery note to state that this proposal supersedes only its promise to retain `system-prompt/change`. Remove that event from the retained-extension statement and link the implemented retirement decision. Keep the Remote delivery note active and cross-linked: its forwarding policy and other extensions remain independently useful. Do not rewrite or archive that entire decision.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the extension for future dashboards or plugins.** Push observation could support a real consumer, but none is identified in supported product paths. Retention preserves notification and listener-failure behavior that the package otherwise does not need.
|
||||
|
||||
**Treat per-step assembly as proof of compatibility.** Rejected because model-input reconstruction and registry notifications are different behaviors. This proposal deliberately changes the extension promise rather than claiming the earlier reconstruction decision already removed it.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Remove the event from source and current generated inventories, and amend the active retention promise in the implementation change.
|
||||
- Remove notification-count and notification-failure tests; retain provider-membership, disposer, duplicate-registration, shadowing, and HMR coverage. Keep mixed tests' independent assertions.
|
||||
- Run focused scope, tools, system-prompt, jobs-local, and mcp-resources tests, loop request-reconstruction coverage, a relevant keyless recording, catalog generation, typecheck, lint, and doc-sync. Prompt/log output remains unchanged.
|
||||
|
||||
## Risks
|
||||
|
||||
Installed plugins may depend on notifications or on listener exceptions rejecting registration. They lose both behaviors; repository searches cannot establish their absence. A future push consumer needs an explicit registration-observation requirement and ownership design before reintroducing an event. Source savings are small; the benefit is retiring the public notification and failure obligation.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Agent Note: 停用提示词注册表变更事件
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-09-19-retire-prompt-registry-change-event.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[`SystemPrompt`](../../../../packages/core/system-prompt/src/index.ts) 在提供方注册或 dispose(资源释放)时发出 `system-prompt/change`。仓库搜索未找到执行的产品监听者,但生成的 Host 发现目录会展示该事件。测试维护通知次数及监听者抛出异常后的回滚行为。即使没有第一方订阅者,它仍是具有维护义务的公开扩展。
|
||||
|
||||
活动的 [Remote 事件投递决策](../../implemented/architecture/2026-08-10-remote-event-delivery.zh.md) 明确保留了这一扩展,尽管没有随产品提供的消费方。逐步骤组装早于该承诺,且用途不同:重新构建模型输入不能替代注册表变更的推送观测。
|
||||
|
||||
## 提案
|
||||
|
||||
停用 `system-prompt/change`,接受已安装及动态编写的 Host 插件失去提示词注册表推送观测的代价。删除其声明、发出点、当前文档、生成的发现条目和通知专属测试。当前受支持的产品路径均不依赖此扩展。
|
||||
|
||||
将 [`ScopedLayers`](../../../../packages/core/scope/src/store.ts) 构造函数的通知回调改为可选,并让 `SystemPrompt` 省略它;不提供无操作回调。更新此共享构造函数时,同时删除 [jobs-local](../../../../packages/jobs/jobs-local/src/index.ts) 和 [mcp-resources](../../../../packages/mcp/mcp-resources/src/index.ts) 中现有的无操作实参,并更新 jobs-local 相邻的通知说明。保留共享 effect、操作回滚、撤销顺序、层回收,以及 `tools/change` 及其生产消费方。保留提示词提供方求值、作用域覆盖和组装 invariant。
|
||||
|
||||
实现时必须修订活动的 Remote 事件投递记录,说明本提案只取代其中保留 `system-prompt/change` 的承诺。从保留扩展的陈述中移除该事件,并链接到已实现的停用决策。让 Remote 投递记录保持活动并相互链接:其转发策略和其他扩展仍具有独立价值。不要重写或归档整个决策。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**为未来仪表盘或插件保留扩展。** 推送观测可能服务于真实消费方,但受支持的产品路径中尚未找到这样的消费方。保留它会维持这个包原本不需要的通知及监听者失败行为。
|
||||
|
||||
**将逐步骤组装视为兼容性证据。** 不采用,因为模型输入重建和注册表通知是不同的行为。本提案有意改变扩展承诺,而不声称早先的重建决策已经删除了它。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 从源代码和当前生成目录中删除该事件,并在实现变更中修订活动记录中的保留承诺。
|
||||
- 删除通知次数和通知失败测试;保留提供方成员关系、disposer、重复注册、作用域覆盖及 HMR(热模块替换)覆盖。保留混合测试中的独立断言。
|
||||
- 运行针对作用域、工具、系统提示词、jobs-local 和 mcp-resources 的测试、循环请求重建覆盖、相关无密钥录制回放、目录生成、类型检查、lint 和 doc-sync(文档同步门禁)。提示词和日志输出保持不变。
|
||||
|
||||
## 风险
|
||||
|
||||
已安装插件可能依赖通知,或依赖监听者异常使注册失败。这两种行为都会消失;仓库搜索无法确认此类插件不存在。未来的推送消费方需要先明确注册观测需求和所有权设计,才能重新引入事件。源代码节省不多;收益在于停用公开通知及其失败义务。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-09-19-shell-env-key-declarations.md
|
||||
2026-09-19-shell-env-key-declarations.md: 7b74e55e6b8602cb488d16040d57fe41d16e14da
|
||||
2026-09-19-shell-env-key-declarations.zh.md: 03d7ed9fdee1c1181c529c2b2dd26ceb7c791ef2
|
||||
@@ -0,0 +1,34 @@
|
||||
# Agent Note: Keep shell-environment declarations focused on key ownership
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-09-19-shell-env-key-declarations.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The [shell-environment registry](../../../../packages/shell/shell-env/src/index.ts) requires a description for every declared variable and exposes `list()` to enumerate it. Runtime collection uses only contributor identity, declared keys, and resolved values. [Bash](../../../../packages/shell/tool-bash/src/index.ts) and [PowerShell](../../../../packages/shell/tool-pwsh/src/index.ts) call `collect`; the [Web bundle](../../../../packages/bundle/web-app/src/index.ts) contributes `DSH_WEB_URL`. Searches found no fixed production caller of `list()` or consumer of its descriptions.
|
||||
|
||||
The method is nevertheless discoverable through the [Cordis inspection provider](../../../../packages/extensions/tool-cordis/src/providers.ts), so this is an explicit API contraction, not unreachable-code removal. The [original identity-and-log-location record](../../archived/feature/2026-07-10-agent-session-identity-and-log-location.md) anticipated diagnostics and future prompt/UI consumers. The current TODO and [README limitation](../../../../packages/shell/shell-env/README.md) still describe an incomplete catalog that omits built-in variables.
|
||||
|
||||
## Proposal
|
||||
|
||||
Represent a contributor's declaration as an explicit readonly collection of keys. Remove description objects, `BashEnvVariable`, `BashEnvVariableInfo`, `list()`, description-only validation, and the exhaustive-catalog TODO. Update the Web contributor, README pair, and generated service/type declarations together.
|
||||
|
||||
Keep `register`, `collect`, reserved keys, ownership conflicts, undeclared-output refusal, deterministic environment output, and effect disposal. The [PowerShell parity decision](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) continues to own the shared registry and both shell consumers. The proposal removes one public method, two metadata types, and their validation/enumeration tests; roughly 32 identified source lines disappear before the small key-membership adaptation.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Finish the diagnostics catalog.** That would add built-in metadata and a consumer capability without an identified current product requirement. The proposal instead gives up declarative environment descriptions and enumeration; actual shell environment values remain available through execution.
|
||||
|
||||
**Keep descriptions after deleting `list()`.** Rejected because they would have no remaining reader while still burdening every contributor. Preserve the key ownership declaration independently of presentation metadata.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Recheck fixed callers, recordings, generic inspection, and current product requirements before implementation; retain the API if an actual diagnostics consumer now needs it.
|
||||
- Contributors declare keys without description objects. Duplicate names, reserved keys, cross-contributor conflicts, undeclared outputs, and disposal retain their behavior.
|
||||
- Bash and PowerShell receive equivalent environment maps, including Web URL and Session identity; no secret-handling or ambient-environment policy changes.
|
||||
- Run registry, Web contributor, and shell integration tests; regenerate and validate inspection catalogs and any affected recorded inspection output; update both READMEs and run typecheck, doc-sync, and lint.
|
||||
|
||||
## Risks
|
||||
|
||||
Model-written and external plugins can already discover and call `list()`. They lose enumeration without resolver execution and must change contributor declarations. This pre-stable API loss is acceptable only while the anticipated diagnostics capability remains unowned; `collect()` is not advertised as an equivalent diagnostic replacement.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Agent Note: 让 shell 环境声明只负责键所有权
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-09-19-shell-env-key-declarations.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[shell 环境注册表](../../../../packages/shell/shell-env/src/index.ts) 要求每个声明变量提供描述,并暴露 `list()` 枚举它。运行时收集仅使用贡献方身份、声明键和解析后的值。[Bash](../../../../packages/shell/tool-bash/src/index.ts) 与 [PowerShell](../../../../packages/shell/tool-pwsh/src/index.ts) 调用 `collect`;[Web 组合包](../../../../packages/bundle/web-app/src/index.ts) 贡献 `DSH_WEB_URL`。搜索未发现 `list()` 的固定生产调用方或其描述的消费方。
|
||||
|
||||
但该方法可通过 [Cordis 检查提供方](../../../../packages/extensions/tool-cordis/src/providers.ts) 发现,因此这是显式 API 收缩,而非删除不可达代码。[最初的身份与日志位置记录](../../archived/feature/2026-07-10-agent-session-identity-and-log-location.md) 预期它会服务于诊断以及未来提示/UI 消费方。当前 TODO 与 [README 限制](../../../../packages/shell/shell-env/README.zh.md) 仍描述一个未包含内置变量的不完整目录。
|
||||
|
||||
## 提案
|
||||
|
||||
将贡献方声明表示为显式只读键集合。删除描述对象、`BashEnvVariable`、`BashEnvVariableInfo`、`list()`、仅针对描述的校验,以及完整目录 TODO。同时更新 Web 贡献方、README 配对与生成的服务/类型声明。
|
||||
|
||||
保留 `register`、`collect`、保留键、所有权冲突、拒绝未声明输出、确定性的环境输出和 effect 释放。[PowerShell 对齐决策](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.zh.md) 继续拥有共享注册表及两个 shell 消费方。提案删除一个公共方法、两个元数据类型及其校验/枚举测试;在少量键成员判定适配前,已定位可删除约 32 行源码。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**补完诊断目录。** 这会在没有明确当前产品需求的情况下增加内置元数据及消费能力。提案选择放弃声明式环境描述和枚举;实际 shell 环境值仍可通过执行获得。
|
||||
|
||||
**删除 `list()` 后保留描述。** 否决,因为描述将没有剩余读取方,却仍给每个贡献方增加负担。键所有权声明应独立于展示元数据保留。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 实现前重新检查固定调用方、记录、通用检查与当前产品要求;若已有实际诊断消费方需要,则保留 API。
|
||||
- 贡献方声明键而无需描述对象。重复名称、保留键、跨贡献方冲突、未声明输出及释放保持原有行为。
|
||||
- Bash 与 PowerShell 获得等价的环境映射,包括 Web URL 和 Session 身份;不改变密钥处理或继承环境策略。
|
||||
- 运行注册表、Web 贡献方与 shell 集成测试;重新生成并校验检查目录及受影响的检查输出记录;更新两个 README,运行 typecheck、doc-sync(文档同步门禁)和 lint。
|
||||
|
||||
## 风险
|
||||
|
||||
模型编写的插件与外部插件已经能够发现和调用 `list()`。它们会失去不执行解析器的枚举能力,并需要修改贡献方声明。只有预期的诊断能力仍无归属时,这种稳定前 API 损失才可接受;不能将 `collect()` 宣称为等价诊断替代。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-09-19-trim-session-query-convenience-api.md
|
||||
2026-09-19-trim-session-query-convenience-api.md: 1a3d558c9be5e16b5e9e84a59a9731b55174114c
|
||||
2026-09-19-trim-session-query-convenience-api.zh.md: 63c3ae309edee8366eae4d5db10cc59f0b72e843
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Trim unused SessionQuery convenience methods
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-09-19-trim-session-query-convenience-api.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
[`SessionQueryEngine`](../../../../packages/session-query/session-query/src/index.ts) maintains `readSession`, `readTitle`, `readTitleSnapshot`, and `listEvents` without executing first-party product callers. Exact-name and call-site searches find internal delegation, tests, and generated discovery metadata. Current consumers use retained observations, batch titles, surface reads, and traces. The generated catalog exposes these methods to plugin authors but does not execute the advertised operations.
|
||||
|
||||
These entry points add replay-validation and result-copying paths, wrappers, public types, and endpoint-specific tests. Their removal can reduce maintained API obligations without replacing the shared query implementation.
|
||||
|
||||
## Proposal
|
||||
|
||||
Remove the four methods, the unreferenced [`SessionLogSnapshot`](../../../../packages/session-query/session-query/src/types.ts) result type, and [`eventRecords`](../../../../packages/session-query/session-query/src/tracing.ts). Remove their imports and current documentation, including the `SessionLogSnapshot` type-equivalence block and explanatory paragraph in both subsystem pages. Remove its hand-maintained entry in the [type-equivalence manifest](../../../../scripts/type-equiv.manifest.json) and its `LINK_MAP` entry in the [catalog generator](../../../../scripts/gen-cordis-catalog.ts), then regenerate discovery metadata. The selected definitions occupy approximately eighty source lines before import and metadata cleanup; no replacement service is needed.
|
||||
|
||||
Retain `filterEvents` and `_filterEvents`, together with their semantic extraction and filtering helpers. Retain `observeSession`, `readTitleSnapshots`, `readSurface`, `readEvent`, listing, search, and tracing. Preserve shared corpus resolution and its existing ownership and failure behavior.
|
||||
|
||||
The [historical unified-query decision](../../archived/architecture/2026-07-23-unified-session-query-service.md) explains the retained single-service topology. This proposal removes selected convenience operations, not that topology. No active note is fully superseded; keep archived records frozen and link this narrower decision from affected current documentation.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Retain all documented methods for external callers.** This avoids migration for installed plugins, but keeps four APIs without a current product caller. The pre-stable API policy permits a deliberate removal with explicit migration costs.
|
||||
|
||||
**Remove `filterEvents` too.** Rejected because its provider-independent literal substring scan differs from FTS token matching. The [SQLite query documentation](../../../../packages/session-query/session-query-sqlite/README.md) explicitly recommends this operation; removing it would withdraw a distinct query capability.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Remove the four methods and orphan definitions from source, package READMEs, subsystem declarations, and generated catalogs; retain literal substring queries.
|
||||
- Preserve live-source precedence, corruption refusal, cancellation, detached results, cold reads, and batch-title behavior through retained endpoints. Move relevant assertions from removed-method tests instead of deleting their shared guarantees; account for `readSession`'s additional replay validation.
|
||||
- Run focused query, SQLite-query, tool-query, and affected context-reference tests, relevant keyless query recordings, catalog generation, typecheck, lint, and doc-sync. Released Session generations remain unchanged.
|
||||
|
||||
## Risks
|
||||
|
||||
Repository searches cannot enumerate installed or dynamically authored plugins. Such callers lose complete-log and lightweight-record convenience operations and must adopt retained APIs with their actual ownership and error semantics. A demonstrated consumer requiring a removed operation would justify reconsidering that operation separately.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Agent Note: 精简未使用的 SessionQuery 便利方法
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-09-19-trim-session-query-convenience-api.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[`SessionQueryEngine`](../../../../packages/session-query/session-query/src/index.ts) 维护了 `readSession`、`readTitle`、`readTitleSnapshot` 和 `listEvents`,但没有执行这些方法的第一方产品调用方。精确名称和调用点搜索只找到内部委托、测试和生成的发现元数据。当前消费方使用可保留的观测、批量标题、模型可见事件读取和追踪。生成的目录向插件作者展示这些方法,但不执行其中列出的操作。
|
||||
|
||||
这些入口增加了回放验证和结果复制路径、包装层、公开类型及入口专属测试。删除它们可以减少需要维护的 API 义务,无须替换共享查询实现。
|
||||
|
||||
## 提案
|
||||
|
||||
删除这四个方法、不再被引用的 [`SessionLogSnapshot`](../../../../packages/session-query/session-query/src/types.ts) 结果类型和 [`eventRecords`](../../../../packages/session-query/session-query/src/tracing.ts)。删除相关导入及当前文档,包括两个子系统页面中的 `SessionLogSnapshot` 类型等价代码块和说明段落。删除[类型等价 manifest](../../../../scripts/type-equiv.manifest.json) 中手工维护的条目,以及[目录生成器](../../../../scripts/gen-cordis-catalog.ts) 中的 `LINK_MAP` 条目,然后重新生成发现元数据。选定定义在清理导入和元数据之前约占八十行源代码;无须新增替代服务。
|
||||
|
||||
保留 `filterEvents` 和 `_filterEvents`,以及语义提取和过滤辅助函数。保留 `observeSession`、`readTitleSnapshots`、`readSurface`、`readEvent`、列举、搜索和追踪。保留共享会话集合解析及其现有所有权和失败行为。
|
||||
|
||||
[历史上的统一查询决策](../../archived/architecture/2026-07-23-unified-session-query-service.md) 解释了保留的单服务结构。本提案删除选定的便利操作,不改变该结构。没有活动记录被完全取代;保持归档记录冻结,并从受影响的当前文档链接到这一较窄的决策。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**为外部调用方保留所有已记录的方法。** 这样可以避免已安装插件迁移,但会保留四个没有当前产品调用方的 API。尚未稳定的 API 策略允许在明确迁移代价后有意删除它们。
|
||||
|
||||
**同时删除 `filterEvents`。** 不采用,因为其提供方无关的字面子串扫描不同于 FTS 的 token 匹配。[SQLite 查询文档](../../../../packages/session-query/session-query-sqlite/README.zh.md) 明确推荐该操作;删除它会撤销一项独立的查询能力。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 从源代码、包 README、子系统声明和生成目录中删除这四个方法及失去引用的定义;保留字面子串查询。
|
||||
- 通过保留的入口维持实时来源优先、损坏拒绝、取消、结果独立复制、冷读取和批量标题行为。将被删方法测试中的相关断言迁移出去,不删除其共享保证;明确处理 `readSession` 额外执行的回放验证。
|
||||
- 运行针对查询、SQLite 查询、查询工具及受影响的会话引用的测试,相关无密钥查询录制回放、目录生成、类型检查、lint 和 doc-sync(文档同步门禁)。已发布的 Session 代际文件保持不变。
|
||||
|
||||
## 风险
|
||||
|
||||
仓库搜索无法枚举已安装或动态编写的插件。这些调用方将失去完整日志和轻量事件记录的便利操作,必须按照保留 API 实际的所有权和错误语义迁移。若出现确实需要某个被删操作的消费方,可以单独重新考虑该操作。
|
||||
@@ -1,157 +1,67 @@
|
||||
---
|
||||
name: dsh-find-simplifications
|
||||
description: 'Use when working in the deepseek-harness repo to find non-obvious simplification candidates, remove redundant comments or implementation-heavy documentation, write proposed Agent Notes or inline TODO/FIXME/XXX notes, audit or coalesce superseded Agent Notes, or fold worthwhile simplification ideas from another PR; especially for dead, duplicated, speculative, over-built, added-then-removed, or hand-rolled-where-a-dependency-exists surfaces.'
|
||||
description: Find evidence-backed simplifications in DeepSeek Harness code, APIs, configuration, tests, and prose; write or consolidate proposals, identify small inline cleanups, or assess simplifications from another branch. Use for removing dead, duplicated, speculative, or unnecessarily maintained behavior and infrastructure.
|
||||
---
|
||||
|
||||
# Finding DeepSeek Harness Simplifications
|
||||
|
||||
This skill helps turn a broad "find things to simplify" request into evidence-backed Agent Notes that remove or collapse existing harness surface area. It is guidance, not a checklist: follow the code, keep judgment active, and prefer a few well-proven candidates over a pile of thin guesses.
|
||||
Find changes that remove maintained obligations: APIs, representations, lifecycle states, configuration paths, dependencies, tests, or documentation. Prefer a few well-supported candidates over a count of deletions. This is guidance, not a checklist; keep the user's scope and distinguish a survey from permission to implement its proposals.
|
||||
|
||||
## Start With Repo Context
|
||||
## Establish scope and constraints
|
||||
|
||||
- Read `AGENTS.md`, especially the pre-release stance and the conventions (including the tests-are-not-golden-truth and Agent Notes-are-not-golden-truth doctrines), plus [docs/defensive-patterns.md](../../../docs/defensive-patterns.md) and [docs/testing.md](../../../docs/testing.md).
|
||||
- Skim [docs/architecture.md](../../../docs/architecture.md) before judging anything under `packages/`; simplifications that fight the service map or event taxonomy need extra evidence.
|
||||
- Use the Agent Note tree and its [rules](../../notes/README.md) to understand intentional architecture. The most relevant implemented examples are [drop mutable session summary](../../notes/archived/simplification/2026-06-19-drop-mutable-session-summary.md), [handle-based session persistence](../../notes/implemented/architecture/2026-08-27-handle-based-session-persistence.md), [JSONL-only first-party Session persistence](../../notes/implemented/simplification/2026-08-30-jsonl-only-session-persistence.md), [capability seams](../../notes/implemented/architecture/2026-06-13-capability-seams.md), and the twin-adapter Agent Notes.
|
||||
- Treat dual LLM adapters as intentional by default. Session persistence is different: JSONL is the sole first-party provider, while the backend-neutral service remains available to out-of-tree providers. Do not propose deleting an LLM twin or the persistence seam as "low effort" unless the user explicitly overrides that constraint. Removing an unused method or hook inside a protected seam can still be valid if it does not collapse the protected design.
|
||||
Read `AGENTS.md`, [architecture](../../../docs/architecture.md) before judging packages, [defensive patterns](../../../docs/defensive-patterns.md), and [testing policy](../../../docs/testing.md). Consult the relevant active Agent Notes for intentional decisions; use the [note rules](../../notes/README.md) for new records. Archived notes are historical examples, not current authority: never inspect, verify, or repair their outbound links.
|
||||
|
||||
## What Counts As A Strong Candidate
|
||||
The two LLM adapters are intentional. JSONL is the sole first-party Session persistence provider, while the backend-neutral Service Definition supports out-of-tree providers. Do not propose deleting an LLM twin or the persistence seam unless the user explicitly overrides that constraint. Unused members within a protected design remain candidates when their removal preserves its purpose.
|
||||
|
||||
A strong simplification removes, folds, or demotes something real and has clear evidence that the current design costs more than it buys:
|
||||
For broad requests, divide independent domains among subagents: loop and persistence; model/tool assembly; Host, Client, and SDKs; subprocess and execution providers; composition, scripts, tests, and documentation. Give each agent a bounded domain and require consumer evidence and rejected alternatives. Inspect substantial production machinery as well as obvious unused symbols; do not stop after the first promising deletion.
|
||||
|
||||
- A public method, event, config knob, registry notification, helper, package, durable event, or test artifact has no production consumer.
|
||||
- Tests or docs are the only consumers, and the behavior they pin is not load-bearing.
|
||||
- Two representations mirror the same fact, especially across durable session events and transient `agent/*` events.
|
||||
- A seam has methods every implementation must support but no consumer uses.
|
||||
- A separate package exists only for test/demo/support code and adds publish or dependency overhead.
|
||||
- A feature implements speculative product generality: multi-session/session-load, background job rosters, live registry invalidation, mid-turn steering, tool-owned UI rendering, and similar designs with no product owner.
|
||||
- An invariant, rollback path, set of expected outputs, or special-case test exists only to protect an unused API.
|
||||
- Hand-rolled code reimplements what a well-maintained external package or a Node builtin at the engine floor already provides, and the swap would delete the implementation plus its dedicated tests ([dependency policy](../../notes/implemented/process/2026-07-26-dependencies-over-hand-rolling.md)).
|
||||
- The simplified behavior may differ slightly, but the new behavior is still reasonable and easier to explain.
|
||||
## Search for removable obligations
|
||||
|
||||
Thin candidates are not enough for an Agent Note: deleting one typo, running `knip` once, removing an intentionally documented backend/adapter, or flagging "this looks complex" without call-site proof.
|
||||
Use these questions to guide discovery. Read the relevant section of [historical patterns](references/historical-patterns.md) when a candidate needs calibration; the examples illustrate decisions, not a current deletion inventory.
|
||||
|
||||
### Audit invariant companions
|
||||
- **Does a declared feature have a complete effect path?** Trace producer, transformations, provider or endpoint support, and observable result. A field copied everywhere may still have no setter, no reader, or only implementations that reject it. Search constructors and discriminant emitters as well as callers.
|
||||
- **Which distinctions change a consumer's action?** Several internal states may require one model-facing status or one usable identifier. Keep internal distinctions that control authorization, residency, durability, or ownership; remove public distinctions that only invite an unusable choice.
|
||||
- **Could a smaller explicit behavior remove the subsystem?** Fixed intervals, caller-supplied time zones, or reported activation failure can eliminate generalized calendars, persisted defaults, or automatic rollback. Name the capability given up and the machinery deleted. A production caller makes this a behavior decision, not an automatic rejection; implementation must remain within the user's authorization.
|
||||
- **Can a consumer read the authoritative value when needed?** Look for copied histories, promotion ledgers, invalidation events followed by reads, and feature caches beside a shared projection stream. Derivation or reuse may delete subscriptions, retained data, stale-response handling, and recovery paths together. Establish the required time of observation first.
|
||||
- **Is composition being mistaken for policy?** Optional service presence, a provider method, or a sibling tool can express availability without deciding deployment behavior. Explicit configuration or a caller-intent operation may remove registries and dependency checks. Preserve distinct operations when their ownership or completion timing differs.
|
||||
- **What owns the complete maintenance cost?** Trace pass-through configuration, duplicate application trees, demo launchers used by products, resolver-only workspaces, and installation helpers with their own upgrade lifecycle. Compare the entire removed system with the replacement, including residual glue; moving complexity or adding a gate to keep duplicate definitions equal does not remove it.
|
||||
|
||||
Treat an invariant companion as useful only when it compares independently produced observations that can diverge. Remove empty installers and checks that merely inspect service presence, plugin metadata, fixed examples, or the result of calling the same mutation they claim to verify. For every omission, remove the export, build entry, invariant-only compiler reference or dependency, and companion-only test, then record the package-specific reason in both package READMEs. Keep a companion when it compares distinct event producers, durable history, or independently mutable data, even if the package also validates inputs synchronously.
|
||||
## Prove reachability and the trade-off
|
||||
|
||||
## Survey Broadly
|
||||
Start with `rg`, then read the matches. Search exact symbols, property reads and writes, discriminants, event and wire strings, config keys, package names, and both `.method(` and `method(` forms. Tests and declarations can show a contract without proving a shipped producer or consumer.
|
||||
|
||||
Use parallel subagents when the user asks for breadth or many candidates. Give each agent a domain and require evidence, not guesses. Useful domains:
|
||||
Include `packages/*/*/src`, `apps/`, `python/`, runtime scripts, shipped profiles and overlays, package manifests and exports, loader resolution, generated runtime assets, and installed-package consumers. Classify examples and fixtures by their actual entry point rather than directory name. Follow generated catalogs to their consumers: a catalog used by model-written plugin mounts is a dynamic product API, while a documentation-only listing is not a fixed caller. Public third-party extension paths can be intentional despite zero repository call sites.
|
||||
|
||||
- Agent loop and session log: turn/step boundaries, steering, abort/cancel, durable events, replay, load/resume.
|
||||
- ACP automation and human UI APIs: prompt settlement and teardown on the protocol side; transcript rendering and interaction state on the UI side.
|
||||
- LLM/tools/system prompt: stream/generate APIs, assemblers, registries, tool schema defaults, presentation hooks.
|
||||
- Bash and tool execution: foreground/background split, job ownership, output spill files, executor methods.
|
||||
- Packages/examples/scripts/tests: package splits, static inventories, redundant snapshot expected outputs, support packages.
|
||||
For each candidate, record the current owner, effective producer/consumer path, what disappears, what remains, and the strongest reason to retain it. Distinguish:
|
||||
|
||||
If subagents are unavailable, simulate the same breadth yourself. Do not let the first good candidate stop the survey.
|
||||
- removal of unreachable or unread behavior;
|
||||
- a narrower public or product behavior with an explicit loss;
|
||||
- a protected obligation or insufficient evidence.
|
||||
|
||||
Start with the largest production-code deltas. A broad simplification audit that stops after obvious unused symbols can miss the files where duplicated lifecycle or defensive machinery carries most of the cost.
|
||||
Reject a candidate when it breaks a retained obligation, merely relocates the same complexity, or has no meaningful reduction. Read rejection reasons at the level of the proposed change: a stale inventory or a mixed proposal's rejected behavior does not settle every independent item. Refresh the evidence before re-proposing one. Small local improvements belong in actionable `TODO`/`FIXME`/`XXX` comments under the [urgency rules](../../../docs/development.md), not standalone design records.
|
||||
|
||||
## Simplify Prose With The Code
|
||||
## Preserve ownership and failure semantics
|
||||
|
||||
Treat comments and documentation as maintained surface area. Apply [dsh-prose-standard](../dsh-prose-standard/SKILL.md) when a survey includes prose.
|
||||
For copies, freezes, validators, and callback captures, name the value's origin, next owner, and trust boundary. Typed same-process calls ordinarily borrow readonly values; parsers, config loaders, queues, model/tool JSON, durable files, workers, processes, and wire decoders own or validate their inputs. Hostile-getter or callback-replacement tests do not establish a requirement by themselves. A frozen root does not prove its descendants immutable, and equal ids do not prove object identity.
|
||||
|
||||
- Delete comments that restate code or explain behavior owned elsewhere; keep required local contracts.
|
||||
- Keep docs at their owning level; omit implementation details and rare cases unless they change a maintained contract.
|
||||
For asynchronous machinery, map each promise, flag, cancellation path, disposer, and reservation to its owner and transition. Collapse mechanisms only when they express the same fact. Preserve synchronous publication and rollback, callback containment, first-terminal-outcome arbitration, worker/process ownership, and dispose-to-quiescence where required. Durable start/end markers can record real facts even when transcript renderers ignore them; dropping interrupted work is a capability loss, not redundant-log cleanup.
|
||||
|
||||
## Audit Trust And Lifecycle Boundaries
|
||||
An invariant companion is justified by comparing independently produced observations that can diverge. Checks of service presence, plugin metadata, fixed examples, or the same mutation's return value do not qualify. Removing a companion includes its export, build entry, invariant-only compiler references, dependencies, and tests; record the package-specific omission reason in both READMEs. Keep checks comparing independent event producers, durable history, or mutable data.
|
||||
|
||||
For every defensive copy, freeze, validator, and callback capture, name where the value came from and who owns it next. Same-process typed service/plugin calls ordinarily borrow readonly values; parsers, config loaders, queues, model/tool JSON, durable files, workers, processes, and wire decoders own or validate their data. Tests built around hostile getters, fake typed objects, callback replacement, or mutation after a same-process handoff are evidence of a potentially speculative contract, not automatic justification for keeping it.
|
||||
## Replace infrastructure only for a net reduction
|
||||
|
||||
For complex asynchronous code, draw the ownership graph and map each sentinel, readiness promise, cancellation path, disposer, and state flag to a distinct owner or transition. When several mechanisms mirror the same liveness or settlement fact, propose one transaction or lifecycle controller instead. Preserve separate machinery where it protects synchronous publication and rollback, callback containment, first-terminal-outcome arbitration, worker/process ownership, or dispose-to-quiescence.
|
||||
Follow the [dependency policy](../../notes/implemented/process/2026-07-26-dependencies-over-hand-rolling.md). Search existing dependencies and Node builtins at the engine floor before adding a package. Identify the exact implementation and dedicated tests the replacement deletes, its maintenance and transitive cost, and every residual behavior the glue must preserve.
|
||||
|
||||
## Hand-Rolled Code Versus A Dependency
|
||||
Exercise the mismatches most likely to defeat the replacement: protocol truncation, cancellation and teardown, configured bounds, deterministic test clocks, platform packaging, and source versus built entry points. A library that covers framing but requires rebuilding the same framing to enforce a size limit is not a simplification. An accepted edge-behavior change belongs in the proposal and relevant tests. Do not re-prove a dependency's generic specification while omitting tests for the harness-specific shim.
|
||||
|
||||
Introducing a dependency is a valid simplification move, not a policy exception: the [dependency policy](../../notes/implemented/process/2026-07-26-dependencies-over-hand-rolling.md) owns the bar. When surveying, ask of protocol parsers, framers, retry/backoff loops, glob matchers, diff engines, and similar infrastructure: does a well-maintained npm package or a Node builtin at the repo's engine floor already do this?
|
||||
## Record, consolidate, and validate
|
||||
|
||||
Prove a dependency-swap candidate like any other, plus:
|
||||
Use [dsh-prose-standard](../dsh-prose-standard/SKILL.md) when prose is in scope. Remove code-restating or remotely owned explanations while preserving local behavior, timing, ownership, and failure obligations. Update affected READMEs, JSDoc, model-visible catalogs, configuration, snapshots, and generated files with their owning change.
|
||||
|
||||
- Read the hand-rolled implementation and name the exact surface the package covers; residual semantics the package does not cover count against the swap and stay in the Agent Note.
|
||||
- Check the package's health honestly (maintenance, adoption, transitive footprint) and prefer builtins when the engine floor has them.
|
||||
- Check the Agent Note tree first: schemastery, vendored Cordis, the twin adapters, and other recorded seams are settled — a swap that collapses one needs to beat the recorded rationale, not just cite the policy.
|
||||
- Weigh net deletion: implementation plus dedicated tests plus docs, minus the glue that remains. A wrapper that relocates the same complexity is not a win.
|
||||
A substantial proposal uses the mandatory note skeleton: `Problem`, `Proposal`, `Alternatives considered`, `Acceptance criteria`, and `Risks`. Include concrete consumer evidence, the removed maintenance cost, the capability given up, and observable acceptance conditions. An implemented decision uses the [implemented format](../../notes/README.md#the-body-skeleton) instead. Update an existing owner when the decision is the same; do not create duplicate notes to preserve candidate counts.
|
||||
|
||||
## Prove Or Reject Each Candidate
|
||||
Every new note requires a scoped supersession check through [dsh-archive-agent-notes](../dsh-archive-agent-notes/SKILL.md). That workflow owns retention, consolidation, triplet deletion, and frozen archive mechanics. A code survey does not imply a repository-wide note audit. Preserve partial supersessions and current durable, wire, compatibility, or rejected-alternative obligations.
|
||||
|
||||
For every symbol or behavior, classify consumers before writing:
|
||||
When folding another branch, compare its independent diff against the target base, port only supported non-overlapping proposals, and consolidate overlapping rationale. Closing another PR requires authorization or clear ownership of that housekeeping.
|
||||
|
||||
- Production corpus: `packages/*/src`, `examples/*/src`, `examples/**/*.yml`, runtime scripts, and loader/config paths.
|
||||
- Non-production corpus: tests, README/docs, Agent Notes, snapshots, generated expected outputs, and comments.
|
||||
- Ambiguous corpus: examples and scripts that may be product smoke paths. Inspect usage before classifying.
|
||||
|
||||
Use `rg` first. Good searches include the exact symbol, event name, package name, config key, method name with both `.name(` and `name(`, and any wire strings. Then read the call sites, public interfaces, dynamic event names, tests, docs, and Cordis loader paths.
|
||||
|
||||
Reject or downgrade a candidate when:
|
||||
|
||||
- A production caller exists and the simplification would be a feature decision rather than a cleanup.
|
||||
- The API is explicitly justified by an implemented Agent Note or a hard-won defensive pattern, and the new evidence does not beat that reason.
|
||||
- The removal would force unrelated churn without actually reducing the public API or required behavior.
|
||||
- The idea is correct but tiny. Add a targeted TODO/FIXME/XXX instead, using the urgency semantics in [docs/development.md](../../../docs/development.md).
|
||||
|
||||
## Coalesce Superseded Agent Notes
|
||||
|
||||
Audit the Agent Note tree when the user asks to reduce or coalesce it, or when the simplification being implemented makes an owning note obsolete. Do not expand every code-simplification survey into a repository-wide note audit.
|
||||
|
||||
Use [`dsh-archive-agent-notes`](../dsh-archive-agent-notes/SKILL.md) for retention judgment and archive mechanics. Implemented notes that only describe small UI adjustments or purely mechanical changes are deleted directly; other completed notes with historical decision value but little future guidance move as frozen triplets to `archived/{kind}`; proposed notes are never archived; rejected notes that no longer prevent a tempting mistake are deleted. Do not edit an archived note while simplifying current prose or code.
|
||||
|
||||
Follow the deletion rule in the [Agent Note rules](../../notes/README.md#when-to-write-one); do not duplicate or weaken it here. For each candidate chain:
|
||||
|
||||
1. Identify the current owner from shipped code, configuration, generated catalogs, package docs, newer Agent Notes, and inbound links; dates and titles are discovery hints, not proof.
|
||||
2. Classify the old note as fully or partially superseded. Any surviving behavior, current contract, durable format, compatibility obligation, or independently current rejected alternative makes it partial. Rationale that can be transferred to the current owner does not by itself make supersession partial.
|
||||
3. For full supersession, move every unique rationale, alternative, consequence, shipped verification evidence, and named coverage gap into the current owner. An inventory that only describes deleted implementation mechanics is not one of those decision facts.
|
||||
4. Repair every inbound link, then delete the English note, Chinese counterpart, and consistency record together.
|
||||
5. Search exact filenames, symbols, config keys, event names, and wire strings after the edit. Keep partial supersessions cross-linked and current.
|
||||
|
||||
An added-then-removed feature is a common full-supersession case. Let the removal note own the history only when the feature is absent from production code, configuration, schemas, durable or wire formats, migration, and compatibility behavior; no current documentation presents it as available; and no test exercises it as supported behavior. Removal rationale and tests that enforce absence may remain. Preserve why the feature originally existed, why that motivation no longer justified it, alternatives to full removal, the capability given up, conditions for reintroduction, and evidence that removal is complete. Old tests and implementation mechanics that verified only the deleted behavior are not current verification evidence.
|
||||
|
||||
Reject consolidation when the removal is only one transport, default, implementation, or presentation of a feature; when persisted data or compatibility handling survives; or when the removal note does not yet carry enough rationale to prevent accidental reintroduction. A current negative design decision may legitimately need its own note even though the removed implementation is gone.
|
||||
|
||||
## Write The Agent Note
|
||||
|
||||
Create one file per durable proposal under `.agents/notes/<lifecycle>/<class>/yyyy-mm-dd-topic.md`, following the lifecycle and classification rules in `.agents/notes/README.md`. Keep prose paragraphs on one physical line and use relative Markdown links.
|
||||
|
||||
Prefer this structure, adjusting when the idea needs it:
|
||||
|
||||
- `# Agent Note: <action-oriented title>`
|
||||
- `Status: proposed`
|
||||
- `## Problem`: name the current API, cite the relevant files, and state the consumer evidence. Separate production callers from tests/docs.
|
||||
- `## Proposal`: say exactly what to remove, fold, demote, or rehome. Include tests, docs, READMEs, JSDoc, event-taxonomy, snapshot, and generated-file cleanup when relevant.
|
||||
- `## Why not keep it?` or `## What we give up`: make the strongest counterargument legible.
|
||||
- `## Acceptance criteria`: observable end state and gates.
|
||||
- `## Risks`: public API changes, behavior changes, future product wants, and why the tradeoff is still reasonable.
|
||||
|
||||
Be concrete enough that an implementing PR can follow the trail. Avoid vague "simplify this package" Agent Notes. When a proposal overlaps an existing Agent Note, consolidate the useful details into the existing one rather than creating a duplicate.
|
||||
|
||||
## Inline TODO Notes
|
||||
|
||||
Use inline TODO/FIXME/XXX only for small, local cleanups that are clearly useful but not durable design decisions. Keep them short and actionable:
|
||||
|
||||
- Name the smell with a stable tag, e.g. `TODO(double-default)` or `XXX(unused-default)`.
|
||||
- Explain why it is safe to revisit and what action would simplify it.
|
||||
- Do not add TODOs for speculative complaints or for behavior that needs an Agent Note-level decision.
|
||||
|
||||
## When Folding Another PR Or Branch
|
||||
|
||||
Diff the sibling branch against `origin/master`, not against the current PR branch, so you see its independent contribution. For each item:
|
||||
|
||||
- Port non-overlapping Agent Notes or TODOs that meet the quality bar.
|
||||
- Consolidate overlapping material into the existing Agent Note that owns the topic.
|
||||
- Do not port duplicate or lower-confidence proposals just to preserve the count.
|
||||
- Update the PR body so reviewers see the true candidate count and scope.
|
||||
- Close the duplicate PR only when the user asked you to, or when you clearly own that housekeeping.
|
||||
|
||||
## Validation And PR Hygiene
|
||||
|
||||
For docs-only Agent Note work, run at least `pnpm run doc-sync`, `pnpm run lint`, and `git diff --check`. For code comments or skill changes, also run the relevant validator when one exists. Select any other evidence from the outgoing diff; the pre-push hook contributes typecheck only.
|
||||
|
||||
When opening or updating a PR, summarize:
|
||||
|
||||
- How many Agent Notes and inline notes were added, consolidated, retained as partial supersessions, or deleted.
|
||||
- The main areas surveyed.
|
||||
- What was intentionally excluded.
|
||||
- Which checks passed.
|
||||
|
||||
For each consolidation group, name the old and current owners, state the evidence for full supersession, and explain why deletion is safe. If an added-then-removed scan finds no qualifying note, report that result and the representative partial cases retained.
|
||||
|
||||
Use a draft PR while the survey is still expanding; mark ready only when the candidate set, review responses, and validation are settled.
|
||||
For docs-only Agent Note work, run `pnpm run doc-sync`, `pnpm run lint`, and `git diff --check`; for skill or comment edits, include the applicable validator. Select other checks through [dsh-pre-push-checks](../dsh-pre-push-checks/SKILL.md). Report the surveyed areas, supported candidates, meaningful rejections or deferrals, notes added/consolidated/retained/removed, and commands actually run. Use a draft PR while the survey is expanding; do not describe an unverified search as exhaustive.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Historical simplification patterns
|
||||
|
||||
Use these examples to calibrate a candidate whose value is not established by an unused-symbol search. They describe recorded decisions, not current removal opportunities. Archived sources are frozen snapshots: read the cited note itself, never follow or repair its outbound links. Verify current owners and released-data obligations before applying a pattern.
|
||||
|
||||
## Require a complete effect path
|
||||
|
||||
A declaration, serializer branch, and test can form convincing-looking support without an effective feature. Trace both ends: something must produce the value, and an implementation must honor it. The [producerless-variants decision](../../../notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md) removed reserved union members and cache hints without emitters. The [inert-request-knobs decision](../../../notes/archived/simplification/2026-07-04-drop-inert-request-knobs.md) distinguished knobs rejected by every adapter from working request fields available to hooks. Deletion included declarations, forwarding, rejection-only tests, and generated documentation.
|
||||
|
||||
Zero fixed callers is weaker evidence than unreachability. The [skill-registration rejection](../../../notes/rejected/simplification/2026-07-12-prune-unused-skill-registry-api.md) preserves a deliberate third-party registration path. For catalogued services, inspect generic plugin invocation and returned values before calling them dead. A working extension point and a field no provider can honor require different verdicts.
|
||||
|
||||
## Reduce distinctions where consumers decide
|
||||
|
||||
Map every public variant to the consumer action it enables. The [model agent-availability decision](../../../notes/implemented/simplification/2026-09-15-model-agent-availability-and-team-targets.md) gives models one usable Team target and projects multiple residency states to the same availability, while internal routing retains Session identity and residency. The [capability-neutral sandbox context](../../../notes/archived/simplification/2026-07-31-capability-neutral-sandbox-policy-context.md) replaces two approximate capability registries with truthful conditional policy wording; exact tool availability remains in tool schemas. The gain includes registration methods, disposal state, invalidation, and combination tests.
|
||||
|
||||
Do not erase distinct durable facts merely because one renderer ignores them. The [durable step-boundary rejection](../../../notes/rejected/simplification/2026-06-20-drop-durable-step-boundaries.md) retains explicit start and completion facts needed for empty failed steps, crash repair, and inspection. Simplify the projection only when the underlying distinction remains necessary.
|
||||
|
||||
## Trade general behavior for a smaller explicit promise
|
||||
|
||||
A used feature can still be an expensive way to satisfy the required outcome. The [fixed-rate Schedule decision](../../../notes/archived/simplification/2026-08-09-bounded-fixed-rate-schedule.md) gives up calendar recurrence and missed-occurrence replay, removing a parser, evaluator, cross-record gate, durable variants, and backlog states. The [non-transactional Loader decision](../../../notes/implemented/simplification/2026-09-09-nontransactional-loader.md) gives up automatic plugin rollback while keeping failure reporting and consumer-owned activation checks. Moving generic rollback to another package would retain its cost.
|
||||
|
||||
State the loss as clearly as the deletion. The [interrupted-turn truncation rejection](../../../notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.md) rejects a simpler recovery algorithm because an unfinished turn can contain substantial real work. Convenience, smaller unions, or fewer tests alone do not justify losing required data or behavior. A survey can propose the trade-off without implying permission to implement it.
|
||||
|
||||
## Make assumptions explicit at their consuming operation
|
||||
|
||||
Look for a local default that has acquired shared storage, synchronization, or confirmation machinery. The [explicit Schedule time-zone decision](../../../notes/implemented/simplification/2026-08-09-explicit-schedule-time-zone.md) keeps browser facts on the originating request and requires an explicit zone in the scheduling operation, removing Session defaults and cross-tab conflict state. The [injected-content decision](../../../notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md) leaves framing with the caller and removes envelope controls threaded through unrelated layers. Both reduce the number of owners that must interpret one assumption.
|
||||
|
||||
Do not move a requirement past the place that must enforce it. The [Host-only Remote input-validation decision](../../../notes/implemented/simplification/2026-09-15-host-only-remote-input-validation.md) removes duplicate Client schema execution but retains Host validation before lookup and business invocation, plus local arity and Context binding checks. Its documented cost is later malformed-input failure and the loss of Client parsing as accidental redaction.
|
||||
|
||||
## Derive at use or reuse an existing delivery owner
|
||||
|
||||
For each cache or event subscription, ask when the consumer needs the value and whether an authoritative source already retains it. [Feedback telemetry](../../../notes/archived/simplification/2026-08-06-buffer-free-feedback-telemetry.md) reads the canonical Session prefix at feedback time, deleting a continuously copied pre-feedback buffer and its listeners. [Workspace recency](../../../notes/implemented/simplification/2026-09-10-derived-workspace-recency.md) sorts current summaries instead of persisting promotion history; manual ordering remains independent user state. [Web subagent projections](../../../notes/implemented/simplification/2026-09-08-web-subagent-catalog-projections.md) reuse a shared stream and projection store, removing feature notifications, repeated reads, and a second membership cache.
|
||||
|
||||
The timing promise determines whether this works. Feedback-time redaction differs from capture-time redaction; manual placement cannot be derived from activity timestamps; an initial historical read may remain necessary when a stream baseline is absent. Name such residual obligations instead of declaring all caches redundant.
|
||||
|
||||
## Remove accidental composition owners
|
||||
|
||||
Inspect actual launch and resolution paths before trusting names such as demo, support, or example. The [shared-base-config decision](../../../notes/archived/simplification/2026-07-29-shared-base-config-overlays.md) removes duplicate compositions and pass-through settings; actual Loader behavior determines overlay placement. The [owner-local profiles and guides decision](../../../notes/archived/simplification/2026-08-24-owner-local-profile-tests-and-guides.md) removes a resolver-only workspace by assigning tests, guides, and shipped assets to their owners. The [merged subagent service decision](../../../notes/archived/simplification/2026-07-26-merge-subagent-control-service.md) uses explicit deployment policy instead of inferring it from provider or sibling-tool presence, while preserving optional dependencies internally.
|
||||
|
||||
A smaller package count is not sufficient. Separate caller intents may have different ownership and completion timing; a retained Service Definition may serve independent providers. Prove the replacement's real composition and dependency behavior, and account for all remaining configuration and lifecycle glue.
|
||||
|
||||
## Use rejected experiments to test parity claims
|
||||
|
||||
The [SSE parser replacement](../../../notes/archived/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md) deletes generic framing and its conformance tests while retaining the provider-specific termination shim and documenting stricter truncated-tail behavior. In contrast, the [timer-promises rejection](../../../notes/rejected/simplification/2026-07-26-builtin-timer-promises-for-hand-rolled-sleeps.md) records that a nominally equivalent builtin defeated deterministic fake-clock tests for a small deletion. The [dependency-swap audit](../../../notes/rejected/simplification/2026-07-26-dependency-swaps-rejected-by-nih-audit.md) distinguishes replaceable generic code from configured bounds, security rules, lifecycle ownership, and deployment behavior a library does not implement.
|
||||
|
||||
A past rejection is a scoped falsifier, not a permanent package blacklist. Revisit it only with new evidence addressing the failed premise, and measure the implementation, glue, tests, dependencies, and supported behavior together.
|
||||
@@ -20,6 +20,7 @@ interface SessionSummary {
|
||||
readonly origin?: 'subagent'
|
||||
readonly cwd?: string
|
||||
readonly projections?: {
|
||||
readonly kind: 'cached' | 'sequenced'
|
||||
readonly asOfSeq: number
|
||||
readonly values: Readonly<Record<string, unknown>>
|
||||
}
|
||||
@@ -260,7 +261,9 @@ export function createAssembledRemote(options: AssembledRemoteOptions = {}): Ass
|
||||
running: false,
|
||||
blank: true,
|
||||
cwd,
|
||||
projections: structuredClone(blankSessionProjections),
|
||||
// The created Session is live on this fixture Host: its list block is
|
||||
// sequenced, like the block the real live registry would serve.
|
||||
projections: { kind: 'sequenced', ...structuredClone(blankSessionProjections) },
|
||||
}
|
||||
sessions.push(summary)
|
||||
records.set(sessionId, [])
|
||||
|
||||
@@ -5,11 +5,17 @@ import { tmpdir } from 'node:os'
|
||||
import { cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { chromium, type Page } from 'playwright'
|
||||
import { expect, it, onTestFailed, onTestFinished } from 'vitest'
|
||||
import { launchWebScaffold, watchConsole, captureStableAria, compareOrRefreshGolden, webSnapshotMode } from './scaffold.ts'
|
||||
import { saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, launchWebScaffold,
|
||||
seedSession, watchConsole, webSnapshotMode,
|
||||
} from './scaffold.ts'
|
||||
import { newEnglishPage, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
const FIXTURE = fileURLToPath(new URL('./fixtures/plugins/fixture-live-client', import.meta.url))
|
||||
const EXPECTED = fileURLToPath(new URL('./expected/client-plugin-live', import.meta.url))
|
||||
const SESSION_ACTION_EXPECTED = join(EXPECTED, 'session-actions.expected.md')
|
||||
const SESSION_SEED = fileURLToPath(new URL('../../../snapshots/web/seeded-history/session.v3.jsonl', import.meta.url))
|
||||
const SESSION_TITLE = 'Session action extension'
|
||||
|
||||
async function openInventory(page: Page, url: string) {
|
||||
await page.goto(url, { waitUntil: 'load' })
|
||||
@@ -20,6 +26,68 @@ async function openInventory(page: Page, url: string) {
|
||||
return dialog
|
||||
}
|
||||
|
||||
it('places dynamic Session menu rows by order among the shipped ones and removes them with their fiber', async () => {
|
||||
const scaffold = await launchWebScaffold({ extraInstallAnchors: [join(FIXTURE, 'package.json')] })
|
||||
onTestFinished(() => scaffold.close())
|
||||
const workspace = await scaffold.ctx.workspaceRegistry.create(scaffold.workspaceCwd)
|
||||
const sessionId = await seedSession(scaffold, await readFile(SESSION_SEED, 'utf8'), 'session-menu-actions-web-e2e')
|
||||
await workspace.attachSession(sessionId)
|
||||
await scaffold.ctx.sessionController.rename({ sessionId, title: SESSION_TITLE })
|
||||
const entryId = await scaffold.ctx.loader.create({ name: '@fixture/live-client' })
|
||||
const browser = await chromium.launch()
|
||||
try {
|
||||
const page = await newEnglishPage(browser)
|
||||
const console = watchConsole(page)
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-session-menu-actions'))
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
const row = page.getByRole('treeitem').filter({ has: page.getByText(SESSION_TITLE, { exact: true }) })
|
||||
await row.waitFor({ timeout: 20_000 })
|
||||
const trigger = row.getByRole('button', { name: `Session actions for ${SESSION_TITLE}` })
|
||||
|
||||
await row.hover()
|
||||
await trigger.focus()
|
||||
await trigger.click()
|
||||
const menu = page.getByRole('menu')
|
||||
await menu.waitFor()
|
||||
expect(await menu.getByRole('menuitem').allTextContents()).toEqual([
|
||||
'Pin session', 'Rename', 'Fork session', 'Archive session', 'Export session', 'Copy session ID',
|
||||
])
|
||||
expect(await menu.getByRole('separator').count()).toBe(1)
|
||||
await compareOrRefreshGolden(
|
||||
SESSION_ACTION_EXPECTED,
|
||||
await captureStableAria(page, '[role="menu"]', scaffold.workspaceCwd),
|
||||
webSnapshotMode(),
|
||||
)
|
||||
|
||||
await menu.getByRole('menuitem', { name: 'Export session' }).click()
|
||||
await expect.poll(() => menu.count()).toBe(0)
|
||||
expect(await page.evaluate(() => ({
|
||||
action: document.documentElement.dataset.sessionAction,
|
||||
id: document.documentElement.dataset.sessionActionId,
|
||||
title: document.documentElement.dataset.sessionActionTitle,
|
||||
}))).toEqual({ action: 'fixture.export-session', id: sessionId, title: SESSION_TITLE })
|
||||
|
||||
scaffold.ctx.loader.remove(entryId)
|
||||
await expect.poll(() => page.evaluate(
|
||||
() => document.documentElement.dataset.liveDisposals,
|
||||
)).toBe('1')
|
||||
await row.hover()
|
||||
await trigger.click()
|
||||
await menu.waitFor()
|
||||
expect(await menu.getByRole('menuitem').allTextContents()).toEqual([
|
||||
'Pin session', 'Rename', 'Fork session', 'Archive session',
|
||||
])
|
||||
expect(await menu.getByRole('separator').count()).toBe(0)
|
||||
expect(console.pageErrors).toEqual([])
|
||||
await assertFixtureInventory(EXPECTED, [
|
||||
'bootstrap-rebuild.expected.md', 'enabled.expected.md', 'recovered.expected.md',
|
||||
'session-actions.expected.md',
|
||||
])
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
}, 120_000)
|
||||
|
||||
it('synchronizes two pages, disposes effects and restores an offline page from the latest graph without navigation', async () => {
|
||||
const scaffold = await launchWebScaffold({
|
||||
extraInstallAnchors: [join(FIXTURE, 'package.json')],
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
- menu:
|
||||
- menuitem "Pin session"
|
||||
- menuitem "Rename"
|
||||
- menuitem "Fork session"
|
||||
- menuitem "Archive session"
|
||||
- separator
|
||||
- menuitem "Export session"
|
||||
- menuitem "Copy session ID"
|
||||
@@ -3,6 +3,7 @@ window.__ModuleLoader__.load({
|
||||
id: '@fixture/live-client',
|
||||
factory(require) {
|
||||
const React = require('react')
|
||||
const { MenuItemButton } = require('@deepseek-ai/dsh-client-ui-primitives')
|
||||
const style = document.createElement('style')
|
||||
style.dataset.plugin = '@fixture/live-client'
|
||||
style.textContent = '[data-live-client] { color: rgb(12, 34, 56); position: absolute; bottom: 20px; right: 20px; }'
|
||||
@@ -13,8 +14,14 @@ window.__ModuleLoader__.load({
|
||||
const counters = document.documentElement.dataset
|
||||
counters.liveMounts = String(Number(counters.liveMounts ?? 0) + 1)
|
||||
ctx.effect(() => ctx.locale.register('fixtureLive', {
|
||||
zh: { active: '动态插件已启用', configSummary: '示例配置项', configForm: '动态插件配置', configField: '问候语', configSave: '保存' },
|
||||
en: { active: 'Live plugin enabled', configSummary: 'An example setting', configForm: 'Live plugin configuration', configField: 'Greeting', configSave: 'Save' },
|
||||
zh: {
|
||||
active: '动态插件已启用', configSummary: '示例配置项', configForm: '动态插件配置', configField: '问候语', configSave: '保存',
|
||||
exportSession: '导出会话', copySessionId: '复制会话 ID',
|
||||
},
|
||||
en: {
|
||||
active: 'Live plugin enabled', configSummary: 'An example setting', configForm: 'Live plugin configuration', configField: 'Greeting', configSave: 'Save',
|
||||
exportSession: 'Export session', copySessionId: 'Copy session ID',
|
||||
},
|
||||
}))
|
||||
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
||||
name: 'shell.overlay', id: 'fixture-live-client', locale: 'fixtureLive',
|
||||
@@ -34,6 +41,35 @@ window.__ModuleLoader__.load({
|
||||
},
|
||||
React.createElement('label', null, t('configField'), React.createElement('input', { name: 'greeting', defaultValue: 'hello' })),
|
||||
React.createElement('button', { type: 'submit' }, t('configSave')))))
|
||||
const actionInjected = () => ({
|
||||
selectAction(action, sessionId, displayTitle) {
|
||||
counters.sessionAction = action
|
||||
counters.sessionActionId = sessionId
|
||||
counters.sessionActionTitle = displayTitle
|
||||
},
|
||||
})
|
||||
const registerSessionAction = (id, order, label, separatorBefore) => ctx.slots.register({
|
||||
name: 'sidebar.workspaces.session.menu.item', id, order, locale: 'fixtureLive', inject: actionInjected,
|
||||
}, ({ sessionId, displayTitle, useMenuOpenState, selectAction, t }) => {
|
||||
const [, setMenuOpen] = useMenuOpenState()
|
||||
return React.createElement(
|
||||
MenuItemButton,
|
||||
{
|
||||
separatorBefore,
|
||||
onSelect: () => {
|
||||
setMenuOpen(false)
|
||||
selectAction(id, sessionId, displayTitle)
|
||||
},
|
||||
},
|
||||
t(label),
|
||||
)
|
||||
})
|
||||
ctx.slots.inject('sidebar.workspaces.session.menu.item', function* () {
|
||||
// The shipped rows end at archive (400); these follow as one group,
|
||||
// opened by the export row's hairline.
|
||||
yield registerSessionAction('fixture.export-session', 500, 'exportSession', true)
|
||||
yield registerSessionAction('fixture.copy-session-id', 600, 'copySessionId', false)
|
||||
})
|
||||
ctx.effect(() => {
|
||||
const ping = () => { counters.liveHits = String(Number(counters.liveHits ?? 0) + 1) }
|
||||
window.addEventListener('dsh-fixture-ping', ping)
|
||||
|
||||
@@ -396,9 +396,10 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
||||
// The child row is published before its inherited title rename settles;
|
||||
// wait for that second RPC projection before freezing the ARIA tree.
|
||||
await expect.poll(
|
||||
() => page.locator('[role="treeitem"][aria-selected="true"]').textContent(),
|
||||
() => page.locator('[role="treeitem"]').allTextContents(),
|
||||
{ timeout: 10_000 },
|
||||
).toContain('Use the read tool twice (2)')
|
||||
).toEqual(expect.arrayContaining([expect.stringContaining('Use the read tool twice (2)')]))
|
||||
expect(await sourceRow.textContent()).toContain('Use the read tool twice (1)')
|
||||
const tree = await captureStableAria(
|
||||
page,
|
||||
'[role="tree"][aria-label="Sessions"]',
|
||||
|
||||
@@ -409,6 +409,8 @@ describe.skipIf(process.platform === 'win32')('Web sidebar terminal', () => {
|
||||
await openTerminal(page)
|
||||
const firstTab = await page.locator('[data-dockkit-tab][aria-selected="true"]').getAttribute('data-dockkit-tab')
|
||||
const firstProcess = processIdentity(0)
|
||||
// Busy activity isolates layout recovery from unattended idle reclamation.
|
||||
vi.spyOn(handles[0]!, 'inspectActivity').mockResolvedValue({ state: 'busy', revision: 1 })
|
||||
await command(page, "printf 'WINDOW_A\\n'")
|
||||
await expect.poll(() => page.locator('.xterm-rows:visible').innerText()).toContain('WINDOW_A')
|
||||
await openTerminal(second)
|
||||
@@ -433,11 +435,11 @@ describe.skipIf(process.platform === 'win32')('Web sidebar terminal', () => {
|
||||
await expect.poll(() => bindings()).toHaveLength(1)
|
||||
expect(saved).toContain((await bindings())[0])
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await expect.poll(() => page.locator('.xterm-rows:visible').innerText()).toContain('WINDOW_A')
|
||||
await command(page, "printf 'WINDOW_A_RECONNECTED\\n'")
|
||||
await expect.poll(() => page.locator('.xterm-rows:visible').innerText()).toContain('WINDOW_A_RECONNECTED')
|
||||
expect(handles).toHaveLength(2)
|
||||
await page.getByText('Ready for terminal input.').waitFor()
|
||||
// The second window saved an empty layout; disabled recovery does not recreate the first window's tab.
|
||||
expect(await page.locator('[data-sidebar-terminal]:visible').count()).toBe(0)
|
||||
expect(alive(firstProcess)).toBe(true)
|
||||
expect(handles).toHaveLength(2)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(secondErrors.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
@@ -86,7 +86,7 @@ async function waitForCacheRow(
|
||||
header: SessionHeader,
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + 10_000
|
||||
while (scaffold.ctx.sessionProjectionCache.cachedSnapshot(header, SessionLogOffset(0)) === undefined) {
|
||||
while (scaffold.ctx.sessionProjectionCache.cachedSnapshot(header) === undefined) {
|
||||
if (Date.now() >= deadline) throw new Error(`cache row for "${header.id}" did not land`)
|
||||
await new Promise<void>(resolve => setTimeout(resolve, 10))
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
||||
config-catalog.md: e7b759ad04ba1c8222359f935e3834d4fe81295e
|
||||
config-catalog.zh.md: 9d39b4821cd6fb223db0a028de3376a6e3804b4b
|
||||
config-catalog.md: df0cadde3f488c69278dcee3bb29252f0279ddc4
|
||||
config-catalog.zh.md: 6cb3680db988fe8f1a76cbed8986fff5d8ca715e
|
||||
@@ -2232,7 +2232,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/session/session-projection-cache/src/index.ts:63`](../packages/session/session-projection-cache/src/index.ts)
|
||||
Source: [`packages/session/session-projection-cache/src/index.ts:75`](../packages/session/session-projection-cache/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-session-query-sqlite"></a>
|
||||
|
||||
@@ -3058,7 +3058,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/shell/tool-bash-persistent/src/index.ts:435`](../packages/shell/tool-bash-persistent/src/index.ts)
|
||||
Source: [`packages/shell/tool-bash-persistent/src/index.ts:443`](../packages/shell/tool-bash-persistent/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tool-fs"></a>
|
||||
|
||||
@@ -3239,7 +3239,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/shell/tool-pwsh-persistent/src/index.ts:472`](../packages/shell/tool-pwsh-persistent/src/index.ts)
|
||||
Source: [`packages/shell/tool-pwsh-persistent/src/index.ts:480`](../packages/shell/tool-pwsh-persistent/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tool-ralph"></a>
|
||||
|
||||
@@ -3372,7 +3372,7 @@ export interface Config {
|
||||
|
||||
Depends on: [`AgentOptions`](subsystems/core.md)
|
||||
|
||||
Source: [`packages/subagent/tool-subagent/src/index.ts:43`](../packages/subagent/tool-subagent/src/index.ts)
|
||||
Source: [`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tool-terminal"></a>
|
||||
|
||||
|
||||
@@ -3060,7 +3060,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/shell/tool-bash-persistent/src/index.ts:435`](../packages/shell/tool-bash-persistent/src/index.ts)
|
||||
来源:[`packages/shell/tool-bash-persistent/src/index.ts:443`](../packages/shell/tool-bash-persistent/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tool-fs"></a>
|
||||
|
||||
@@ -3241,7 +3241,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/shell/tool-pwsh-persistent/src/index.ts:472`](../packages/shell/tool-pwsh-persistent/src/index.ts)
|
||||
来源:[`packages/shell/tool-pwsh-persistent/src/index.ts:480`](../packages/shell/tool-pwsh-persistent/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tool-ralph"></a>
|
||||
|
||||
@@ -3374,7 +3374,7 @@ export interface Config {
|
||||
|
||||
Depends on: [`AgentOptions`](subsystems/core.zh.md)
|
||||
|
||||
来源:[`packages/subagent/tool-subagent/src/index.ts:43`](../packages/subagent/tool-subagent/src/index.ts)
|
||||
来源:[`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tool-terminal"></a>
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
|
||||
event-producer-consumer.md: 9f9d2e2172ec0af51306b24060cd0e12d1b6efee
|
||||
event-producer-consumer.zh.md: 4e46a064d1471d1e8802afc04a86772515fe3b09
|
||||
event-producer-consumer.md: 3fbbb8f62adbb1e3e77f629cdc89c6c3c85a524e
|
||||
event-producer-consumer.zh.md: 1032f9c0df45db33b36554876b679f368d58841b
|
||||
@@ -21,11 +21,11 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:600`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:580`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:607`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:586`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:593`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:614`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:594`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:621`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:600`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:607`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
|
||||
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
|
||||
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:600`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:580`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:607`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:586`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:593`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:614`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:594`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:621`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:600`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:607`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
|
||||
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
|
||||
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/persistence-catalog.md
|
||||
persistence-catalog.md: 67f282f1f4c0df52b02d1f85f50ea3eb904934e3
|
||||
persistence-catalog.zh.md: 17c8b8074a46f47c0a744cad731cccdf1c465eac
|
||||
persistence-catalog.md: f42ec2873da8eb6a48a71a012dd53acb810336bd
|
||||
persistence-catalog.zh.md: 9df37e6570f1a2871bb959e670cdcca9cc61317c
|
||||
@@ -4074,7 +4074,7 @@ Sources: [`packages/llm/llm/src/message.ts:24`](../packages/llm/llm/src/message.
|
||||
|
||||
SHA-256: `f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b`
|
||||
|
||||
Sources: [`packages/api/session-controller/src/types.ts:86`](../packages/api/session-controller/src/types.ts)
|
||||
Sources: [`packages/api/session-controller/src/types.ts:100`](../packages/api/session-controller/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
@@ -5312,7 +5312,7 @@ Array of `number`.
|
||||
|
||||
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
|
||||
|
||||
Sources: [`packages/api/session-controller/src/types.ts:380`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:10`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
Sources: [`packages/api/session-controller/src/types.ts:394`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:10`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
|
||||
`string`
|
||||
|
||||
@@ -7219,7 +7219,7 @@ Sources: [`packages/core/tools/src/index.ts:34`](../packages/core/tools/src/inde
|
||||
|
||||
SHA-256: `6a4f72e2e179e17b922f2a9392c0e1c8f8c707f32494372454850a3eb184a6e7`
|
||||
|
||||
Sources: [`packages/api/session-controller/src/types.ts:385`](../packages/api/session-controller/src/types.ts)
|
||||
Sources: [`packages/api/session-controller/src/types.ts:399`](../packages/api/session-controller/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
|
||||
@@ -4076,7 +4076,7 @@ SHA-256: `719c51f67074f97034a6c94297fe1f4b5d3c8d392c57586dab732aa5398c7a91`
|
||||
|
||||
SHA-256: `f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b`
|
||||
|
||||
来源:[`packages/api/session-controller/src/types.ts:86`](../packages/api/session-controller/src/types.ts)
|
||||
来源:[`packages/api/session-controller/src/types.ts:100`](../packages/api/session-controller/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
@@ -5314,7 +5314,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78`
|
||||
|
||||
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
|
||||
|
||||
来源:[`packages/api/session-controller/src/types.ts:380`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:10`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
来源:[`packages/api/session-controller/src/types.ts:394`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:10`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
|
||||
`string`
|
||||
|
||||
@@ -7221,7 +7221,7 @@ SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543`
|
||||
|
||||
SHA-256: `6a4f72e2e179e17b922f2a9392c0e1c8f8c707f32494372454850a3eb184a6e7`
|
||||
|
||||
来源:[`packages/api/session-controller/src/types.ts:385`](../packages/api/session-controller/src/types.ts)
|
||||
来源:[`packages/api/session-controller/src/types.ts:399`](../packages/api/session-controller/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
|
||||
Generated
+3
-3
@@ -44192,7 +44192,7 @@
|
||||
"packages/workflow/workflow/src/types.ts#WorkflowRunId"
|
||||
],
|
||||
"sources": [
|
||||
"packages/api/session-controller/src/types.ts:380",
|
||||
"packages/api/session-controller/src/types.ts:394",
|
||||
"packages/attachment/attachment/src/brand.ts:6",
|
||||
"packages/compaction/compaction/src/brand.ts:4",
|
||||
"packages/core/session/src/types.ts:20",
|
||||
@@ -57718,7 +57718,7 @@
|
||||
},
|
||||
"names": [],
|
||||
"sources": [
|
||||
"packages/api/session-controller/src/types.ts:385"
|
||||
"packages/api/session-controller/src/types.ts:399"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -86472,7 +86472,7 @@
|
||||
"packages/api/session-controller/src/types.ts#ModelSelection"
|
||||
],
|
||||
"sources": [
|
||||
"packages/api/session-controller/src/types.ts:86"
|
||||
"packages/api/session-controller/src/types.ts:100"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/session-projection.md
|
||||
session-projection.md: cb3ac37d593a2b5aae6725854c31a3a39ae3152b
|
||||
session-projection.zh.md: 5051dab59316840267d16d201a17717742565059
|
||||
session-projection.md: 8580b94c4d7dd36f942ff10d77e13d2dc70b7e8b
|
||||
session-projection.zh.md: b98d348a36c51aab973bc3397ddbf744860245a4
|
||||
@@ -122,20 +122,24 @@ The persisted projection cache service. Opens the `session_projcache` domain at
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* The zero-I/O listing read: whole values viewed straight from the stored
|
||||
* rows (version-matching keys only), each cut carried with its watermark so
|
||||
* a client value store can seed under its higher-seq-wins rule — as stale
|
||||
* as the last durable checkpoint but never wrong, and never from an
|
||||
* unrelated log (the caller's header is the identity witness). Fresher
|
||||
* paths (the history tail baseline) supersede these values whenever a
|
||||
* session is actually opened.
|
||||
* rows (version-matching keys only) of the record bound to the caller's
|
||||
* lifecycle. The header is the only identity witness a listing holds, so
|
||||
* this face matches the lifecycle identity (`formatVersion`, `createdAt`,
|
||||
* `cwd`, `isSeeded`) and not the inherited cut: within one format
|
||||
* generation the cut is fixed at fork time, so it distinguishes no
|
||||
* lifecycle the other fields do not, and a viewed value never seeds a fold.
|
||||
* The view is as stale as the last durable checkpoint but never wrong and
|
||||
* never from an unrelated log. Its `asOfSeq` is the lowest watermark among
|
||||
* the served rows: the stored record's own position, which the header
|
||||
* cannot relate to the log the caller later opens. The Session list
|
||||
* therefore labels the block as cached, and the client lets every value the
|
||||
* connected Session produces supersede it whatever this number says.
|
||||
* @param meta - the listed session's header (identity witness; no log read).
|
||||
* @param inheritedEventCount - exact inherited prefix length that completes
|
||||
* the checkpoint identity.
|
||||
* @param keys - optional projection keys required by the caller's audience.
|
||||
* @returns the cut (`asOfSeq` = lowest served-row watermark), or
|
||||
* `undefined` when no usable row exists for this lifecycle.
|
||||
* @returns the viewed block, or `undefined` when no usable row exists for
|
||||
* this lifecycle at the current Session format.
|
||||
*/
|
||||
cachedSnapshot( meta: SessionHeader, inheritedEventCount: SessionLogOffset, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot | undefined
|
||||
cachedSnapshot( meta: SessionHeader, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot | undefined
|
||||
|
||||
/**
|
||||
* Read only a predecessor checkpoint's title as a zero-I/O listing hint.
|
||||
@@ -146,15 +150,13 @@ cachedSnapshot( meta: SessionHeader, inheritedEventCount: SessionLogOffset, keys
|
||||
* fact from this Session. The registry still requires the current title
|
||||
* projection's row version and schema. No other predecessor projection is
|
||||
* exposed: format normalization can change their current meaning, and the
|
||||
* strict {@link cachedSnapshot} / hydration paths continue to reject them.
|
||||
* {@link cachedSnapshot} / hydration paths continue to reject them.
|
||||
* @param meta - authoritative listed Session header.
|
||||
* @param inheritedEventCount - exact inherited cut completing the lifecycle identity.
|
||||
* @returns a title-only checkpoint view with `asOfSeq: -1`, or `undefined`
|
||||
* when the record is current, newer, unrelated, missing, or incompatible
|
||||
* with the title unit. The sentinel avoids reusing a sequence that a
|
||||
* cardinality-changing Session migration may have remapped.
|
||||
* @returns a title-only block at the stored title row's watermark, or
|
||||
* `undefined` when the record is current, newer, unrelated, missing, or
|
||||
* incompatible with the title unit.
|
||||
*/
|
||||
cachedPredecessorTitle( meta: SessionHeader, inheritedEventCount: SessionLogOffset, ): ProjectionSnapshot | undefined
|
||||
cachedPredecessorTitle(meta: SessionHeader): ProjectionSnapshot | undefined
|
||||
|
||||
/**
|
||||
* Hydrate projection cells for an already-prepared Session without another
|
||||
|
||||
@@ -122,20 +122,24 @@ The persisted projection cache service. Opens the `session_projcache` domain at
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* The zero-I/O listing read: whole values viewed straight from the stored
|
||||
* rows (version-matching keys only), each cut carried with its watermark so
|
||||
* a client value store can seed under its higher-seq-wins rule — as stale
|
||||
* as the last durable checkpoint but never wrong, and never from an
|
||||
* unrelated log (the caller's header is the identity witness). Fresher
|
||||
* paths (the history tail baseline) supersede these values whenever a
|
||||
* session is actually opened.
|
||||
* rows (version-matching keys only) of the record bound to the caller's
|
||||
* lifecycle. The header is the only identity witness a listing holds, so
|
||||
* this face matches the lifecycle identity (`formatVersion`, `createdAt`,
|
||||
* `cwd`, `isSeeded`) and not the inherited cut: within one format
|
||||
* generation the cut is fixed at fork time, so it distinguishes no
|
||||
* lifecycle the other fields do not, and a viewed value never seeds a fold.
|
||||
* The view is as stale as the last durable checkpoint but never wrong and
|
||||
* never from an unrelated log. Its `asOfSeq` is the lowest watermark among
|
||||
* the served rows: the stored record's own position, which the header
|
||||
* cannot relate to the log the caller later opens. The Session list
|
||||
* therefore labels the block as cached, and the client lets every value the
|
||||
* connected Session produces supersede it whatever this number says.
|
||||
* @param meta - the listed session's header (identity witness; no log read).
|
||||
* @param inheritedEventCount - exact inherited prefix length that completes
|
||||
* the checkpoint identity.
|
||||
* @param keys - optional projection keys required by the caller's audience.
|
||||
* @returns the cut (`asOfSeq` = lowest served-row watermark), or
|
||||
* `undefined` when no usable row exists for this lifecycle.
|
||||
* @returns the viewed block, or `undefined` when no usable row exists for
|
||||
* this lifecycle at the current Session format.
|
||||
*/
|
||||
cachedSnapshot( meta: SessionHeader, inheritedEventCount: SessionLogOffset, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot | undefined
|
||||
cachedSnapshot( meta: SessionHeader, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot | undefined
|
||||
|
||||
/**
|
||||
* Read only a predecessor checkpoint's title as a zero-I/O listing hint.
|
||||
@@ -146,15 +150,13 @@ cachedSnapshot( meta: SessionHeader, inheritedEventCount: SessionLogOffset, keys
|
||||
* fact from this Session. The registry still requires the current title
|
||||
* projection's row version and schema. No other predecessor projection is
|
||||
* exposed: format normalization can change their current meaning, and the
|
||||
* strict {@link cachedSnapshot} / hydration paths continue to reject them.
|
||||
* {@link cachedSnapshot} / hydration paths continue to reject them.
|
||||
* @param meta - authoritative listed Session header.
|
||||
* @param inheritedEventCount - exact inherited cut completing the lifecycle identity.
|
||||
* @returns a title-only checkpoint view with `asOfSeq: -1`, or `undefined`
|
||||
* when the record is current, newer, unrelated, missing, or incompatible
|
||||
* with the title unit. The sentinel avoids reusing a sequence that a
|
||||
* cardinality-changing Session migration may have remapped.
|
||||
* @returns a title-only block at the stored title row's watermark, or
|
||||
* `undefined` when the record is current, newer, unrelated, missing, or
|
||||
* incompatible with the title unit.
|
||||
*/
|
||||
cachedPredecessorTitle( meta: SessionHeader, inheritedEventCount: SessionLogOffset, ): ProjectionSnapshot | undefined
|
||||
cachedPredecessorTitle(meta: SessionHeader): ProjectionSnapshot | undefined
|
||||
|
||||
/**
|
||||
* Hydrate projection cells for an already-prepared Session without another
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/slots.md
|
||||
slots.md: c441557092d0273c7beebd222ed7ca329b02c0df
|
||||
slots.zh.md: 48aafad0042e06f7a6945be4768249dfa4b5aab1
|
||||
slots.md: 1e820fd880163d565a72733372fa4f2cc70a62b5
|
||||
slots.zh.md: c5bec8e752b9d9e1922f608449ca3249d0477062
|
||||
@@ -116,7 +116,9 @@ root
|
||||
│ ├─ sidebar.panellist
|
||||
│ ├─ sidebar.footer.action
|
||||
│ ├─ sidebar.workspaces
|
||||
│ │ └─ sidebar.workspaces.directoryFlow
|
||||
│ │ ├─ sidebar.workspaces.directoryFlow
|
||||
│ │ ├─ sidebar.workspaces.session.menu.item
|
||||
│ │ └─ sidebar.workspaces.session.row.action
|
||||
│ └─ sidebar.settings
|
||||
│ ├─ settings.trigger
|
||||
│ ├─ settings.header
|
||||
|
||||
@@ -116,7 +116,9 @@ root
|
||||
│ ├─ sidebar.panellist
|
||||
│ ├─ sidebar.footer.action
|
||||
│ ├─ sidebar.workspaces
|
||||
│ │ └─ sidebar.workspaces.directoryFlow
|
||||
│ │ ├─ sidebar.workspaces.directoryFlow
|
||||
│ │ ├─ sidebar.workspaces.session.menu.item
|
||||
│ │ └─ sidebar.workspaces.session.row.action
|
||||
│ └─ sidebar.settings
|
||||
│ ├─ settings.trigger
|
||||
│ ├─ settings.header
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/api/session-controller/README.md
|
||||
README.md: 228ed88ed20b9f9581496468e47079afd1542e4f
|
||||
README.zh.md: edebf8bdda249cf0ea6797db7dd77fcfa973074a
|
||||
README.md: c9f63d462c6e291abda7312c66c8fffcd7109c57
|
||||
README.zh.md: f6d03b45b6bf9d3e809e70154adacd59ce341721
|
||||
@@ -27,7 +27,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
History pages and follow opening snapshots carry one `{ type: 'event', event: SessionWireEvent }` record per durable Session event. The Client retains each accepted record as one durable `SessionEventLikeEntry`; Assistant token boundaries remain inside the compact stream on `assistant/message` or `assistant/attempt`. Tool arguments, result content, failures, and `tool/result.data.meta` pass through unchanged; the controller does not resolve a Tool definition, run a presenter, or attach UI data.
|
||||
|
||||
The Client journal validates exact V3 event envelopes before publishing follow snapshots, live entries, or history pages. It reuses the browser-safe Session validators for required surface markers, exact replacement endpoints, earlier unique source seqs, embedded Assistant provider metadata, request-header omissions, and tool-error consistency. Invalid records fail without field stripping or normalization; range membership and source existence remain durable-log checks on the Host.
|
||||
The Client journal validates current Session event envelopes before publishing follow snapshots, live entries, or history pages. It reuses the browser-safe Session validators for required surface markers, exact replacement endpoints, earlier unique source seqs, embedded Assistant provider metadata, request-header omissions, and tool-error consistency. Invalid records fail without field stripping or normalization; range membership and source existence remain durable-log checks on the Host.
|
||||
|
||||
Each endpoint states its activation policy. List reads only stored headers and projection-cache rows: it never calls per-session stat or opens a cold Session body. A current-format cache identity may supply every list hint; a lifecycle-matching predecessor cache may supply only its version-compatible title as a stale display fact, never as an authoritative fold seed. Search, attachment, history pages, log following, skill discovery, and workspace-path opening can inspect persistence without activating an Agent; `canOpenWorkspacePath()` reports native-opening availability without addressing a Session. Cancellation requires live state; queue mutation, model, rename, prompt, and file-reference operations may resolve or resume an ordinary Session. Prompt rejects content with neither non-whitespace text nor an attachment before resolving the Agent or appending Session events; queue edits accept only non-empty text content. Prompt admission consumes opaque receipts from the injected [`fileUploads`](../../client/file-upload/README.md) Host service and resolves every same-Agent receipt before sending the complete ordered content list through `ctx.attachments`. Prompt retries whose `requestId` is already queued or logged return the original acceptance without inserting another message. Create and fork are the only operations that create a new Agent directly. The service applies one preset-aware resume policy and subagent ownership fence to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces. Queue mutation has one narrow exception: a live child whose current projected identity is continuable and comes from its own non-seed suffix accepts the ordinary Edit, Remove, and QueueDock Steer actions across both inbox destinations. One-shot, missing, unknown, corrupt, seed-only, or cold children remain rejected without resume. The skill catalog uses a live Agent when present or the recorded preset's standing scope when cold, so listing never starts an Agent. The authenticated delivery routes use `workspaceDesktop()` for the serving Host name and file-manager behavior. `openWorkspacePath({ path, action: "reveal" })` delegates file-manager navigation to the native adapter; omitting `action` opens the file-type association, including HTML and SVG. Both operations require the composed filesystem to map the requested Host path to the same canonical process path; unmapped remote paths are refused before a native command runs. `session.projections` reads one complete baseline through a live-preferred Session observation without activating an Agent. It returns null for a missing Session and serves any registered projection keys. The Client exposes shared values and explicit-read state through `projectionsBySession`; domains select their own keys. Session-list summaries carry `agentAvailable`, updated through existing summary and status events independently of durable projections. Initial reads and live projection frames use the same sequence ordering.
|
||||
|
||||
@@ -39,7 +39,7 @@ Client list refreshes retain unchanged row objects and reuse the items array whe
|
||||
|
||||
Projection reads retain loading and failure state independently of their values. Reconnect cancels reads from the previous connection and reloads previously requested projections; live membership updates arrive through the control stream. Parent availability comes from Host summaries and remains unknown until a summary or a successful list baseline establishes it. Address lookup resolves projected children without selecting them or creating scopes.
|
||||
|
||||
The Client adapter exposes `SessionEventStream`, a Gateway `RemoteJournalStream` bound to one ordinary or direct-subagent address. It opens follow before the initial page, publishes only contiguous `replace`, `prepend`, `append`, and `settle-assistant` changes, and repairs reconnect or sequence gaps through a tail page. Backwards paging has two verbs: `loadOlder()` pulls one 50-message page, and `loadThrough(seq)` — the turn-jump loader — loops 200-message pages until the window covers the target seq, lowering a shared target on repeated calls, stopping on a page that makes no progress, and reporting busy through the same `loadingOlder` snapshot bit. The Web adapter explicitly opts into cursorless Assistant frames: each opening carries the active attempt's `startedAfterSeq`, `nextIndex`, and compact stream, and every stream member becomes a Client-only `assistant/live-chunk` entry ordered between durable cursors. The Host captures a follower-local arrival ordinal with that baseline and suppresses buffered frames at or before the cut; a replacement Agent may restart frame revision at one. A durable `assistant/message` or `assistant/attempt` arriving after an active opening stays staged only when its seq follows `startedAfterSeq` and its Turn and Step match; the matching end type, seq, and index publishes one named settlement delta that retires the attempt's transient rows and adds the durable entry while earlier same-step retries remain visible. Revision, dense-index, or settlement gaps for a known attempt reopen follow, while a controller that missed the start ignores unknown-attempt frames and publishes their durable settlement normally. An abandoned end publishes a settlement delta without a durable entry so its transient rows retire immediately. A durable gap-repair page has no Assistant baseline, so its held notification reopens follow once for a paired page and baseline. Every history record covers exactly its event seq. A business, persistence, or unresolved continuity failure terminates the stream, while only physical carrier loss selects automatic resumption. `SessionControlStream` is a Gateway `RemoteSnapshotStream`; every generation opens with a complete process-local baseline, so reconnect replaces jobs and projection state instead of treating transient values as durable events. At each ready Host generation, a synchronous Client subscription clears retained projection values and watermarks before refreshing queries and restarting the control stream, including Sessions absent from the control baseline. The first control stream waits for generation readiness, so its opening values cannot precede invalidation. Outstanding list responses from the previous generation cannot republish those values. Within a generation, a delayed control baseline cannot overwrite or clear newer list, history, or live values. The durable `inbox` projection carries both pending lists through the same cold-read and reconnect path as other projections. Client Agent contexts provide the identity used by the independent [`fileUpload`](../../client/file-upload/README.md) service; Session objects expose lifecycle, prompt, queue, and history operations rather than file transfer.
|
||||
The Client adapter exposes `SessionEventStream`, a Gateway `RemoteJournalStream` bound to one ordinary or direct-subagent address. It opens follow before the initial page, publishes only contiguous `replace`, `prepend`, `append`, and `settle-assistant` changes, and repairs reconnect or sequence gaps through a tail page. Backwards paging has two verbs: `loadOlder()` pulls one 50-message page, and `loadThrough(seq)` — the turn-jump loader — loops 200-message pages until the window covers the target seq, lowering a shared target on repeated calls, stopping on a page that makes no progress, and reporting busy through the same `loadingOlder` snapshot bit. The Web adapter explicitly opts into cursorless Assistant frames: each opening carries the active attempt's `startedAfterSeq`, `nextIndex`, and compact stream, and every stream member becomes a Client-only `assistant/live-chunk` entry ordered between durable cursors. The Host captures a follower-local arrival ordinal with that baseline and suppresses buffered frames at or before the cut; a replacement Agent may restart frame revision at one. A durable `assistant/message` or `assistant/attempt` arriving after an active opening stays staged only when its seq follows `startedAfterSeq` and its Turn and Step match; the matching end type, seq, and index publishes one named settlement delta that retires the attempt's transient rows and adds the durable entry while earlier same-step retries remain visible. Revision, dense-index, or settlement gaps for a known attempt reopen follow, while a controller that missed the start ignores unknown-attempt frames and publishes their durable settlement normally. An abandoned end publishes a settlement delta without a durable entry so its transient rows retire immediately. A durable gap-repair page has no Assistant baseline, so its held notification reopens follow once for a paired page and baseline. Every history record covers exactly its event seq. A business, persistence, or unresolved continuity failure terminates the stream, while only physical carrier loss selects automatic resumption. `SessionControlStream` is a Gateway `RemoteSnapshotStream`; every generation opens with a complete process-local baseline, so reconnect replaces jobs and projection state instead of treating transient values as durable events. At each ready Host generation, a synchronous Client subscription clears retained projection values and watermarks before refreshing queries and restarting the control stream, including Sessions absent from the control baseline. The first control stream waits for generation readiness, so its opening values cannot precede invalidation. Outstanding list responses from the previous generation cannot republish those values. Within a generation, a delayed control baseline cannot overwrite or clear newer sequenced values, whether they arrived through a live Session's list block, a history page, or a frame; a cached list block, viewed from the projection cache for a cold Session, yields to the connected Session's baseline whatever watermark it carries. The durable `inbox` projection carries both pending lists through the same cold-read and reconnect path as other projections. Client Agent contexts provide the identity used by the independent [`fileUpload`](../../client/file-upload/README.md) service; Session objects expose lifecycle, prompt, queue, and history operations rather than file transfer.
|
||||
|
||||
The Session object also carries local submission echoes: `session.beginSubmission` inserts one into `SessionSnapshot.pendingSubmissions` synchronously, before the caller serializes and prompts, so a conversation UI can show the message on the submit click's own frame. The echo stores ordered image previews and durable file references. Session derives its `transcript`, `queued`, or `steering` placement from the current running state and requested delivery mode, then retains that placement while serialization is in flight. The prompt's `requestId` is the correlation identity: the Host echoes it as the durable user source's `rpcId`, including pending messages in the `inbox` projection. An echo retires one animation frame after its durable event or queue occurrence is observed, immediately when its identified prompt fails or is abandoned, and as failed on disposal. Each retirement fires `onRetire` exactly once; an observed retirement includes the ordered durable attachment references so the composer can release successful cards while preserving failed drafts. Echoes are Client memory only; reload and reconnect rebuild the conversation from durable events alone.
|
||||
|
||||
@@ -54,12 +54,14 @@ A resume blocked by an existing write handle returns `session/writer-held` with
|
||||
|
||||
Queue edits replace pending content with non-empty text only.
|
||||
|
||||
Attachment authorization reads declared content fields of built-in Session events and completed assistant stream blocks, including flat V4 tool-role messages. Unknown event payloads and unrelated fields cannot authorize attachment reads.
|
||||
|
||||
<a id="client-references"></a>
|
||||
## Client references
|
||||
|
||||
`sessions.retain(target, { source, signal? })` immediately acquires one exact Client generation and starts its shared initial history opening. The target is a known Session id or a durable direct-parent subagent address; the Host validates an explicit address when history opens. The returned reference supports idempotent `release()` and `Symbol.dispose`; its `ready` Promise follows the shared `Session.open()` result and resolves to the exact binding when that attempt settles, including when a Remote failure is represented by `openState: 'error'`. It rejects when `Session.open()` rejects, its waiter is cancelled, or the reference is released early. Cancelling one waiter does not cancel another owner's opening. `sessions.using(target, options, operation)` waits for that settlement, holds its reference until the callback settles, and propagates rejected readiness and callback failures.
|
||||
|
||||
References keep local Session data, scoped Contexts, and history streams alive, not Host Agents. Final release withdraws the generation before teardown; later acquisition can create a new generation with the same id. `binding(id)` and `scope(id)` only borrow an existing generation. `retainInfo(id)` observes stable read-only source counts independently of catalog membership and performs no history I/O. Consumer source keys are declaration-merge extensible; navigation and completion acknowledgement belong to UI consumers, not this Controller. See [Client Session references](../../../.agents/notes/implemented/architecture/2026-09-15-client-session-references.md) for ownership and teardown rules.
|
||||
References keep local Session data, scoped Contexts, and history streams alive, not Host Agents. Ordinary Session retention never adds a catalog row. Retained subagents with a known direct-parent address keep a fallback row and notify list readers even before the parent catalog arrives; these rows do not join Host-list membership. Fork-title assignment sends rename directly and applies the returned title projection without retaining the child or opening its history. Final release withdraws the generation before teardown; later acquisition can create a new generation with the same id. `binding(id)` and `scope(id)` only borrow an existing generation. `retainInfo(id)` observes stable read-only source counts independently of catalog membership and performs no history I/O. Consumer source keys are declaration-merge extensible; navigation and completion acknowledgement belong to UI consumers, not this Controller. See [Client Session references](../../../.agents/notes/implemented/architecture/2026-09-15-client-session-references.md) for ownership and teardown rules.
|
||||
|
||||
<a id="session-media-references"></a>
|
||||
## Session media references
|
||||
|
||||
@@ -27,7 +27,7 @@ kind: "package-reference"
|
||||
|
||||
历史页与 follow opening 快照为每个持久 Session 事件携带一条 `{ type: 'event', event: SessionWireEvent }` record。Client 把每条已接受 record 保留为一个持久 `SessionEventLikeEntry`;Assistant token 边界保留在 `assistant/message` 或 `assistant/attempt` 的紧凑流内。工具参数、结果内容、失败信息和 `tool/result.data.meta` 原样通过;控制器不解析工具定义、不运行展示转换器,也不附加 UI 数据。
|
||||
|
||||
Client journal 在发布 follow 快照、live entry 或历史页之前验证精确的 V3 事件 envelope。它复用浏览器安全的 Session validator,检查必需的 surface marker、精确的 replacement endpoint、更早且唯一的 source seq、内嵌 Assistant 提供方元数据、request header 可选字段的省略规则以及工具错误一致性。无效 record 直接失败,不删除字段或归一化;范围成员与来源存在性仍由 Host 的持久日志检查。
|
||||
Client journal 在发布 follow 快照、live entry 或历史页之前验证当前 Session 事件 envelope。它复用浏览器安全的 Session validator,检查必需的 surface marker、精确的 replacement endpoint、更早且唯一的 source seq、内嵌 Assistant 提供方元数据、request header 可选字段的省略规则以及工具错误一致性。无效 record 直接失败,不删除字段或归一化;范围成员与来源存在性仍由 Host 的持久日志检查。
|
||||
|
||||
每个 endpoint 都声明自己的激活策略。列表只读取持久化 header 与 projection cache row,绝不调用逐 Session stat 或打开冷 Session body。当前格式 cache identity 可以提供全部列表 hint;生命周期匹配的 predecessor cache 只能提供版本兼容的 title,作为可能过时的展示事实,绝不能作为权威 fold seed。搜索、附件、历史页、日志跟随、skill 发现和工作区路径打开可以在不激活 Agent 的情况下检查 persistence;`canOpenWorkspacePath()` 无需指定 Session 即可报告原生打开能力。取消要求 live 状态;queue 变更、模型、重命名、prompt 和文件引用操作可以解析或恢复普通 Session。提示词会在解析 Agent 或追加 Session 事件前,拒绝既没有非空白文本也没有附件的 content;queue edit 只接受非空文本 content。prompt 准入从注入的 [`fileUploads`](../../client/file-upload/README.zh.md) Host 服务取得不透明凭证,在把完整有序内容列表交给 `ctx.attachments` 前解析每个属于同一 Agent 的凭证。`requestId` 已进入 queue 或日志时,prompt 重试直接返回原来的接受结果,不会重复插入消息。只有 create 与 fork 会直接创建新 Agent。该服务把同一套感知 preset 的恢复策略和 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。Queue 变更只有一个狭窄例外:当前 projection identity 为 continuable 且来自自身非 seed suffix 的在线 child,可以在两个 inbox 目标上使用普通 Edit、Remove 与 QueueDock Steer action。One-shot、缺失、未知、损坏、仅含 seed identity 或冷 child 继续被拒绝,且不会恢复。skill 目录优先使用已有 live Agent,否则使用所记录 preset 的常驻 scope,因此列表查询绝不会启动 Agent。经过鉴权的文件交付路由通过 `workspaceDesktop()` 获取提供服务的 Host 名称和文件管理器行为。`openWorkspacePath({ path, action: "reveal" })` 将文件管理器导航委托给原生适配器;省略 `action` 时按文件类型关联打开,包括 HTML 和 SVG。两种操作都要求当前文件系统将请求的 Host 路径映射到同一个规范进程路径;无法映射的远端路径会在执行原生命令之前被拒绝。 `session.projections` 通过一次 live-preferred Session observation 读取完整基线,不激活 Agent。Session 不存在时返回 null,并可提供任意已注册的 projection key。Client 通过 `projectionsBySession` 暴露共享值和显式读取状态,由领域选择自身的 key。Session 列表摘要携带 `agentAvailable`,通过已有摘要与状态事件更新,与持久化 projection 相互独立。初始读取与实时 projection 帧使用相同的序号排序规则。
|
||||
|
||||
@@ -39,7 +39,7 @@ Client 列表刷新保留未变化的行对象,并在顺序和值均相同时
|
||||
|
||||
投影读取独立于其值保留加载与失败状态。重连会取消上一连接的读取,并重新加载已请求的投影;实时成员更新通过 control stream 到达。父 Agent 可用性来自 Host 摘要,在收到对应摘要或成功的列表 baseline 前保持未知。地址查找直接解析投影中的子项,不会选择它们或创建 scope。
|
||||
|
||||
Client 适配器提供 `SessionEventStream`,即绑定到一个普通 Session 或 direct subagent address 的 Gateway `RemoteJournalStream`。它在读取首个 page 前打开 follow,只发布连续的 `replace`、`prepend`、`append` 与 `settle-assistant` 变更,并通过 tail page 修复重连或 seq 缺口。向后分页有两个动词:`loadOlder()` 拉一页 50 条消息,而 `loadThrough(seq)`——轮次跳转加载器——按每页 200 条消息循环拉取直到窗口覆盖目标 seq,重复调用会下调共享目标,遇到无进展的页即停止,忙碌状态复用同一个 `loadingOlder` 快照位。Web 适配器显式选择接收无 cursor 的 Assistant frame:每个 opening 携带活跃 attempt 的 `startedAfterSeq`、`nextIndex` 与紧凑 stream,每个 stream member 都成为排在持久 cursor 之间的 Client-only `assistant/live-chunk` 条目。Host 会随该 baseline 捕获 follower 本地到达序号,并抑制该 cut 及之前的 buffered frame;replacement Agent 可以从 revision 一重新开始。活跃 opening 之后到达的持久 `assistant/message` 或 `assistant/attempt` 只有在其 seq 晚于 `startedAfterSeq` 且轮次与步骤匹配时才会保持暂存;匹配的 end type、seq 与 index 会发布一个具名 settlement delta,删除该 attempt 的瞬态 row、加入持久条目,并保留同一步骤中更早的 retry。已知 attempt 的 revision、密集 index 或 settlement 缺口会重新打开 follow;若 controller 错过 start,则忽略 unknown-attempt frame,并正常发布其持久 settlement。Abandoned end 会发布不含持久条目的 settlement delta,使瞬态 row 立即退出。持久缺口修复 page 不携带 Assistant baseline,因此 held notification 会重新打开 follow 一次,以取得配对的 page 与 baseline。每条历史 record 只覆盖自身的事件 seq。业务、persistence 或无法恢复的连续性错误会终止 stream,只有物理载体断开才触发自动恢复。`SessionControlStream` 是 Gateway `RemoteSnapshotStream`;每代都以完整的进程本地 baseline 开始,因此重连会替换 jobs 和 projection 状态,而不会把瞬态值当作 durable event。每次 Host generation 就绪时,同步的 Client 订阅会先清除保留的投影值及其水位,再刷新查询并重新打开 control stream,其中也包括 control baseline 中没有列出的 Session。首次 control stream 会等待 generation 就绪,确保其 opening 值不会先于旧状态清理到达。上一代尚未完成的 list 响应无法重新发布这些值。同一 generation 内,延迟到达的 control baseline 不能覆盖或清除较新的 list、history 或 live 值。持久 `inbox` 投影通过与其他投影相同的冷读取和重连路径传输两份待处理列表。Client Agent 上下文提供独立 [`fileUpload`](../../client/file-upload/README.zh.md) 服务使用的身份;Session 对象提供生命周期、prompt、queue 与历史操作,不提供文件传输。
|
||||
Client 适配器提供 `SessionEventStream`,即绑定到一个普通 Session 或 direct subagent address 的 Gateway `RemoteJournalStream`。它在读取首个 page 前打开 follow,只发布连续的 `replace`、`prepend`、`append` 与 `settle-assistant` 变更,并通过 tail page 修复重连或 seq 缺口。向后分页有两个动词:`loadOlder()` 拉一页 50 条消息,而 `loadThrough(seq)`——轮次跳转加载器——按每页 200 条消息循环拉取直到窗口覆盖目标 seq,重复调用会下调共享目标,遇到无进展的页即停止,忙碌状态复用同一个 `loadingOlder` 快照位。Web 适配器显式选择接收无 cursor 的 Assistant frame:每个 opening 携带活跃 attempt 的 `startedAfterSeq`、`nextIndex` 与紧凑 stream,每个 stream member 都成为排在持久 cursor 之间的 Client-only `assistant/live-chunk` 条目。Host 会随该 baseline 捕获 follower 本地到达序号,并抑制该 cut 及之前的 buffered frame;replacement Agent 可以从 revision 一重新开始。活跃 opening 之后到达的持久 `assistant/message` 或 `assistant/attempt` 只有在其 seq 晚于 `startedAfterSeq` 且轮次与步骤匹配时才会保持暂存;匹配的 end type、seq 与 index 会发布一个具名 settlement delta,删除该 attempt 的瞬态 row、加入持久条目,并保留同一步骤中更早的 retry。已知 attempt 的 revision、密集 index 或 settlement 缺口会重新打开 follow;若 controller 错过 start,则忽略 unknown-attempt frame,并正常发布其持久 settlement。Abandoned end 会发布不含持久条目的 settlement delta,使瞬态 row 立即退出。持久缺口修复 page 不携带 Assistant baseline,因此 held notification 会重新打开 follow 一次,以取得配对的 page 与 baseline。每条历史 record 只覆盖自身的事件 seq。业务、persistence 或无法恢复的连续性错误会终止 stream,只有物理载体断开才触发自动恢复。`SessionControlStream` 是 Gateway `RemoteSnapshotStream`;每代都以完整的进程本地 baseline 开始,因此重连会替换 jobs 和 projection 状态,而不会把瞬态值当作 durable event。每次 Host generation 就绪时,同步的 Client 订阅会先清除保留的投影值及其水位,再刷新查询并重新打开 control stream,其中也包括 control baseline 中没有列出的 Session。首次 control stream 会等待 generation 就绪,确保其 opening 值不会先于旧状态清理到达。上一代尚未完成的 list 响应无法重新发布这些值。同一 generation 内,延迟到达的 control baseline 不能覆盖或清除较新的 sequenced 值,无论它们来自活 Session 的 list block、history page 还是 frame;冷 Session 从 projection cache 看出来的 cached list block 则不论水位都让位于建连 Session 的 baseline。持久 `inbox` 投影通过与其他投影相同的冷读取和重连路径传输两份待处理列表。Client Agent 上下文提供独立 [`fileUpload`](../../client/file-upload/README.zh.md) 服务使用的身份;Session 对象提供生命周期、prompt、queue 与历史操作,不提供文件传输。
|
||||
|
||||
Session 对象还承载本地提交回显:`session.beginSubmission` 在调用方序列化与提示词之前,同步把一条回显写入 `SessionSnapshot.pendingSubmissions`,会话 UI 因此能在点击提交的当帧显示消息。回显按顺序存放图片预览与持久文件引用。Session 根据当前运行状态与请求的投递模式推导其 `transcript`、`queued` 或 `steering` 位置,并在序列化期间保留该位置。提示词的 `requestId` 是关联标识:Host 把它回显为 durable user source 的 `rpcId`,`inbox` 投影中的待处理消息也保留同一 source。回显在观察到其 durable event 或 queue occurrence 后延迟一个动画帧退休,带标识的提示词失败或被放弃时立即退休,销毁时按 failed 退休。每次退休恰好触发一次 `onRetire`;observed 退休还会携带有序的持久附件引用,让 composer 释放成功卡片并保留失败草稿。回显只存在于 Client 内存;刷新与重连只从持久事件重建会话。
|
||||
|
||||
@@ -54,12 +54,14 @@ Fork 复制 `atSeq` 所选的精确事件前缀,包含切点事件,允许在
|
||||
|
||||
队列编辑仅允许用非空文本替换待处理内容。
|
||||
|
||||
附件授权读取内置 Session 事件声明的内容字段与已完成的 assistant 流块,包括扁平的 V4 tool 角色消息。未知事件载荷与无关字段不能授权附件读取。
|
||||
|
||||
<a id="client-references"></a>
|
||||
## Client 引用
|
||||
|
||||
`sessions.retain(target, { source, signal? })` 立即获取一个精确 Client generation 的引用,并启动其共享的首次历史打开。目标是已知 Session id 或持久的直接父子 subagent 地址;Host 在打开历史时校验显式地址。返回引用支持幂等的 `release()` 和 `Symbol.dispose`;其 `ready` Promise 跟随共享的 `Session.open()` 结果,并在该次尝试结算时解析为确切 binding,包括 Remote failure 以 `openState: 'error'` 表示的情况。仅当 `Session.open()` 拒绝、等待方取消或引用提前释放时,`ready` 才拒绝。取消一个等待方不会取消其他 owner 的打开。`sessions.using(target, options, operation)` 等待该次结算,持有引用直到回调结束,并传播被拒绝的就绪与回调失败。
|
||||
|
||||
引用保活本地会话数据、作用域 Context 和历史流,不保活 Host Agent。最后一个引用释放时,generation 先退出可访问映射,再执行清理;后续获取可以为同一 id 创建新 generation。`binding(id)` 和 `scope(id)` 只借用已有 generation。`retainInfo(id)` 独立于目录成员关系观察稳定的只读来源计数,不执行历史 I/O。消费方来源键可通过声明合并扩展;导航和完成确认属于 UI 消费方,不属于本控制器。所有权与清理规则见 [Client 会话引用](../../../.agents/notes/implemented/architecture/2026-09-15-client-session-references.zh.md)。
|
||||
引用保活本地会话数据、作用域 Context 和历史流,不保活 Host Agent。普通 Session 不因保留引用而添加目录行。已保留且具有明确直接父子地址的 subagent 即使尚未收到父目录,也会保留兜底行并通知列表读取方;这些行不加入 Host 列表成员集合。Fork 标题设置直接发送 rename 并应用返回的标题投影,不 retain 子会话,也不打开其历史。最后一个引用释放时,generation 先退出可访问映射,再执行清理;后续获取可以为同一 id 创建新 generation。`binding(id)` 和 `scope(id)` 只借用已有 generation。`retainInfo(id)` 独立于目录成员关系观察稳定的只读来源计数,不执行历史 I/O。消费方来源键可通过声明合并扩展;导航和完成确认属于 UI 消费方,不属于本控制器。所有权与清理规则见 [Client 会话引用](../../../.agents/notes/implemented/architecture/2026-09-15-client-session-references.zh.md)。
|
||||
|
||||
<a id="session-media-references"></a>
|
||||
## 会话媒体引用
|
||||
|
||||
@@ -7,11 +7,14 @@ import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
|
||||
import type {
|
||||
SessionControlBaseline,
|
||||
SessionControlFrame,
|
||||
SessionProjectionHints,
|
||||
SessionRenameValue,
|
||||
SessionSummary,
|
||||
SessionJob as JobView,
|
||||
} from '../../types.ts'
|
||||
import { mergeOrderedBaseline } from '../ordered-baseline.ts'
|
||||
import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client'
|
||||
import { assertNever } from '@deepseek-ai/dsh-util-values'
|
||||
import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { SessionListEntry, TitledSessionSummary } from './lineage.ts'
|
||||
import { flattenLineage } from './lineage.ts'
|
||||
@@ -375,18 +378,12 @@ export class SessionManager {
|
||||
session.handleBlank(s.blank)
|
||||
session.handleRunning(s.running)
|
||||
}
|
||||
// Seed each row's projection baseline into the per-session value
|
||||
// store (cold titles surface without opening the session). Per-key
|
||||
// apply, not seed(): the list block is a partial baseline — the
|
||||
// cold cache serves only version-matching keys — so an absent key
|
||||
// must not clear; higher-seq-wins still keeps a stale list block
|
||||
// from overwriting a newer push frame or tail baseline.
|
||||
// Land each row's projection block in the per-session value store
|
||||
// (cold titles surface without opening the session). A list block is
|
||||
// partial — only the keys its source holds — so an absent key never
|
||||
// clears; how the present keys merge depends on the block's kind.
|
||||
for (const s of result.value.items) {
|
||||
const block = s.projections
|
||||
if (block === undefined) continue
|
||||
const store = this.projectionStore(s.sessionId)
|
||||
const values = block.values as Record<string, unknown>
|
||||
for (const key of Object.keys(values)) store.apply(key, values[key], sessionSeqCursor(block.asOfSeq))
|
||||
if (s.projections !== undefined) this.applyListBlock(s.sessionId, s.projections)
|
||||
}
|
||||
} else {
|
||||
this.listState = 'error'
|
||||
@@ -502,6 +499,20 @@ export class SessionManager {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a Session and update its title projection without opening its history.
|
||||
* @param sessionId - Session to rename.
|
||||
* @param title - raw title text for Host normalization.
|
||||
* @returns the accepted title and event position, or the Remote failure.
|
||||
*/
|
||||
async rename(sessionId: SessionId, title: string): Promise<RemoteResult<SessionRenameValue>> {
|
||||
const result = await this.remote.session.rename({ sessionId, title })
|
||||
if (result.ok) {
|
||||
this.projectionStore(sessionId).apply('title', result.value.title, SessionSeq(result.value.seq))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a Host summary, replacing live state and filling missing metadata.
|
||||
* Local create/fork placeholders only fill metadata on an existing row.
|
||||
@@ -589,12 +600,30 @@ export class SessionManager {
|
||||
handleSessionAdded(summary: SessionSummary): void {
|
||||
this.mergeSummary(summary)
|
||||
this.sessions.get(summary.sessionId)?.handleBlank(summary.blank)
|
||||
const projections = summary.projections
|
||||
if (projections !== undefined) {
|
||||
const store = this.projectionStore(summary.sessionId)
|
||||
for (const [key, value] of Object.entries(projections.values)) {
|
||||
store.apply(key, value, sessionSeqCursor(projections.asOfSeq))
|
||||
if (summary.projections !== undefined) this.applyListBlock(summary.sessionId, summary.projections)
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge one list-surface projection block by the sequence space it declares.
|
||||
* A `sequenced` block came from the Host's live registry for an attached
|
||||
* Session, so each key lands under higher-seq-wins against that Session's
|
||||
* baselines and frames. A `cached` block was viewed from the persisted
|
||||
* checkpoint by a header-only listing: its watermark is not comparable with
|
||||
* this connection's seqs, so it only fills keys no sequenced row holds.
|
||||
*/
|
||||
private applyListBlock(sessionId: SessionId, block: SessionProjectionHints): void {
|
||||
const store = this.projectionStore(sessionId)
|
||||
switch (block.kind) {
|
||||
case 'sequenced': {
|
||||
const seq = sessionSeqCursor(block.asOfSeq)
|
||||
for (const [key, value] of Object.entries(block.values)) store.apply(key, value, seq)
|
||||
return
|
||||
}
|
||||
case 'cached':
|
||||
store.applyCached(block.values)
|
||||
return
|
||||
default:
|
||||
assertNever(block.kind, 'session list projection block kind')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
/**
|
||||
* Generic per-session projection value store (push model; see the
|
||||
* session-projection subsystem page, docs/subsystems/session-projection.md):
|
||||
* the host is the only computation site; the client holds finished
|
||||
* whole values per key — `key → { value, seq }` — seeded by a follow opening
|
||||
* baseline and updated by Session Controller `projection` frames,
|
||||
* under the single rule **higher seq wins**. No client-side domain folding
|
||||
* the host is the only computation site; the client holds finished whole
|
||||
* values per key in one of two row kinds. A `sequenced` row
|
||||
* (`{ value, seq }`) comes from the connected Host — a follow opening
|
||||
* baseline, a Session Controller `projection` frame, a list block the Host
|
||||
* computed for an attached Session — and merges under **higher seq wins**
|
||||
* against other sequenced rows. A `cached` row (`{ value }`) comes from the
|
||||
* session list's view of the persisted checkpoint, carries no comparable
|
||||
* seq, and yields to every sequenced write. No client-side domain folding
|
||||
* exists: a domain ships projection support with zero client code. Per-key
|
||||
* bare observable faces feed `useProjection` (ui-renderer binds them).
|
||||
*/
|
||||
@@ -52,11 +56,18 @@ export interface ProjectionsBaseline {
|
||||
values: Readonly<Record<string, unknown>>
|
||||
}
|
||||
|
||||
/** One key's row: the latest finished value and the seq it is consistent with. */
|
||||
interface Row {
|
||||
value: unknown
|
||||
seq: SessionSeqCursor
|
||||
}
|
||||
/**
|
||||
* One key's row. A `sequenced` row was computed by the Host for this Session
|
||||
* within the current connection generation and carries the seq it is
|
||||
* consistent with; seqs of sequenced rows are comparable with each other. A
|
||||
* `cached` row was viewed from the persisted projection checkpoint by a
|
||||
* header-only listing without a Session: its origin cannot vouch that its
|
||||
* stored seq is comparable with the log the client later opens, so it carries
|
||||
* none and yields to every sequenced write.
|
||||
*/
|
||||
type Row =
|
||||
| { kind: 'cached'; value: unknown }
|
||||
| { kind: 'sequenced'; value: unknown; seq: SessionSeqCursor }
|
||||
|
||||
/** Per-key notification channel: the bare face plus its batching notifier. */
|
||||
interface Channel {
|
||||
@@ -66,10 +77,14 @@ interface Channel {
|
||||
|
||||
/**
|
||||
* One session's projection values. Framework semantics, uniform across every
|
||||
* key: a baseline seeds rows at its cut, a push frame updates one row, and in
|
||||
* both paths a lower-or-equal seq within the Host generation loses. A replayed
|
||||
* frame cannot regress a value; a stale baseline cannot overwrite a newer
|
||||
* frame. A key the store has never seen reads `undefined` (capability absent). Faces are identity-stable
|
||||
* key. Sequenced writes (a baseline seeds rows at its cut, a push frame
|
||||
* updates one row) compare seqs among themselves: a lower-or-equal seq within
|
||||
* the Host generation loses, so a replayed frame cannot regress a value and a
|
||||
* stale baseline cannot overwrite a newer frame. Cached writes (the session
|
||||
* list's zero-I/O block) only fill keys no sequenced row holds, and a baseline
|
||||
* discards every cached row before it seeds, regardless of seq: the connected
|
||||
* Session is the truth and a cached value never outranks it. A key the store
|
||||
* has never seen reads `undefined` (capability absent). Faces are identity-stable
|
||||
* per key (create-on-demand, cached) so the React side binds each exactly
|
||||
* once; the store-level channel (`subscribeAny`) serves coarse consumers (the
|
||||
* manager's list projection reads the `title` key).
|
||||
@@ -133,27 +148,54 @@ export class ProjectionValueStore {
|
||||
*/
|
||||
apply(key: string, value: unknown, seq: SessionSeqCursor): void {
|
||||
const row = this.rows.get(key)
|
||||
if (row !== undefined && seq <= row.seq) return // higher seq wins; replays and stale frames drop
|
||||
this.rows.set(key, { value, seq })
|
||||
// higher seq wins among sequenced rows; replays and stale frames drop. A
|
||||
// cached row has no comparable seq and always yields.
|
||||
if (row?.kind === 'sequenced' && seq <= row.seq) return
|
||||
this.rows.set(key, { kind: 'sequenced', value, seq })
|
||||
this.changed(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed from a history tail page's projections block: every carried key
|
||||
* lands under the same seq rule as frames; a key the block omits is
|
||||
* capability-absent as of the cut — its row clears unless a newer frame
|
||||
* already superseded the cut (a stale baseline can neither overwrite nor
|
||||
* clear newer values).
|
||||
* Fill keys from a session-list block the Host labeled `cached`: a zero-I/O
|
||||
* view of the persisted checkpoint. A cached value lands only where no
|
||||
* sequenced row exists: a connected Session has already answered for such
|
||||
* a key, and the list's view of the persisted checkpoint cannot be newer
|
||||
* than it.
|
||||
* @param values - whole values by key viewed from the persisted checkpoint.
|
||||
*/
|
||||
applyCached(values: Readonly<Record<string, unknown>>): void {
|
||||
for (const key of Object.keys(values)) {
|
||||
if (this.rows.get(key)?.kind === 'sequenced') continue
|
||||
this.rows.set(key, { kind: 'cached', value: values[key] })
|
||||
this.changed(key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed from a history tail page's projections block. Every cached row is
|
||||
* discarded first, regardless of seq: the block comes from the connected
|
||||
* Session, and a value viewed from the persisted checkpoint never outranks
|
||||
* it. Then every carried key lands under the same seq rule as frames, and a
|
||||
* key the block omits is capability-absent as of the cut — its row clears
|
||||
* unless a newer frame already superseded the cut (a stale baseline can
|
||||
* neither overwrite nor clear newer sequenced values).
|
||||
* @param baseline - the response's projections block.
|
||||
*/
|
||||
seed(baseline: ProjectionsBaseline): void {
|
||||
for (const [key, row] of this.rows) {
|
||||
if (row.kind !== 'cached') continue
|
||||
this.rows.delete(key)
|
||||
this.changed(key)
|
||||
}
|
||||
// Erased walk: the framework crosses the open key space; per-key typing
|
||||
// is re-established at the consumer (useProjection's map lookup).
|
||||
const values = baseline.values as Record<string, unknown>
|
||||
for (const key of Object.keys(values)) this.apply(key, values[key], baseline.asOfSeq)
|
||||
for (const [key, row] of this.rows) {
|
||||
if (Object.hasOwn(values, key)) continue
|
||||
if (row.seq > baseline.asOfSeq) continue
|
||||
// Every cached row was deleted above; the kind test only narrows the
|
||||
// type so `row.seq` is readable.
|
||||
if (row.kind === 'sequenced' && row.seq > baseline.asOfSeq) continue
|
||||
this.rows.delete(key)
|
||||
this.changed(key)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export interface SessionSummary {
|
||||
export interface SessionListState {
|
||||
/** Host list order; every id has a matching byId row in the same snapshot. */
|
||||
ids: SessionId[]
|
||||
/** Host/catalog rows plus local fallback rows for live Client generations; only `ids` expresses Host-list membership. */
|
||||
/** Host/catalog rows plus retained subagent fallbacks; only `ids` expresses Host-list membership. */
|
||||
byId: Record<SessionId, SessionSummary>
|
||||
/** Arrival lifecycle projected 1:1 from the manager snapshot (see SessionListPhase): empty-with-ready means "truly no sessions". */
|
||||
phase: SessionListPhase
|
||||
@@ -470,14 +470,8 @@ export class ClientSessions implements ISessions {
|
||||
this.projectList()
|
||||
const childId = result.value.sessionId
|
||||
if (sourceTitle !== undefined) {
|
||||
const reference = this.retain(childId, { source: 'controllerOperation' })
|
||||
try {
|
||||
await reference.ready
|
||||
const renamed = await reference.binding.session.rename(increasedForkTitle(sourceTitle))
|
||||
if (!renamed.ok) throw new Error(`fork child rename failed: ${renamed.error.code}: ${renamed.error.message}`)
|
||||
} finally {
|
||||
reference.release()
|
||||
}
|
||||
const renamed = await this.manager.rename(childId, increasedForkTitle(sourceTitle))
|
||||
if (!renamed.ok) throw new Error(`fork child rename failed: ${renamed.error.code}: ${renamed.error.message}`)
|
||||
}
|
||||
return childId
|
||||
}
|
||||
@@ -558,7 +552,9 @@ export class ClientSessions implements ISessions {
|
||||
if (count === 0) this.retireScope(id, record)
|
||||
else this.publishRetention(id)
|
||||
})
|
||||
if (this.list.getSnapshot().byId[id] === undefined) this.projectList()
|
||||
if (this.list.getSnapshot().byId[id] === undefined && this.manager.subagentAddress(id) !== undefined) {
|
||||
this.projectList()
|
||||
}
|
||||
this.publishRetention(id)
|
||||
return reference
|
||||
}
|
||||
@@ -667,15 +663,22 @@ export class ClientSessions implements ISessions {
|
||||
}
|
||||
for (const [id, record] of this.scopes) {
|
||||
if (byId[id] !== undefined) continue
|
||||
const address = this.manager.subagentAddress(id)
|
||||
if (address === undefined) continue
|
||||
const previous = previousById[id]
|
||||
const snapshot = record.session.getSnapshot()
|
||||
const address = this.manager.subagentAddress(id)
|
||||
const projectionValues = this.manager.projectionValues(id)
|
||||
const projectedTitle = projectionValues?.title
|
||||
const title = typeof projectedTitle === 'string' && projectedTitle !== '' ? projectedTitle : previous?.title
|
||||
byId[id] = {
|
||||
...(previous ?? { id, displayTitle: id, updatedAt: 0 }),
|
||||
running: snapshot.running,
|
||||
retainedBy: record.retention.retainedBy,
|
||||
blank: snapshot.blank,
|
||||
...(address === undefined ? {} : { parentId: address.parentSessionId, origin: 'subagent' }),
|
||||
parentId: address.parentSessionId,
|
||||
origin: 'subagent',
|
||||
...(projectionValues === undefined ? {} : { projectionValues }),
|
||||
...(title === undefined ? {} : { title, displayTitle: title }),
|
||||
}
|
||||
}
|
||||
this.list.set({ ids, byId, phase, projectionsBySession, jobsBySession })
|
||||
|
||||
Loaded 100 of 209 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user