mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
Merge master into fix/windows-mandatory-update-overlay
This commit is contained in:
commit
12f0608cad
1807 files changed
+216426
-52813
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/bug-fix/2026-09-11-session-controller-fork-turn-cut.md
|
||||
2026-09-11-session-controller-fork-turn-cut.md: 0c8d45c861df964a62586827e80c613704ed17f0
|
||||
2026-09-11-session-controller-fork-turn-cut.zh.md: 729a42a596264ac618608d26cd52a1ad836b58db
|
||||
2026-09-11-session-controller-fork-turn-cut.md: 9223acd4c0c7bbcc84a0bfeb72f3b57d4bf828e2
|
||||
2026-09-11-session-controller-fork-turn-cut.zh.md: cae022ae97896c0b43633edb310afb305d690b4a
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
# Agent Note: Session Controller forks stop at the selected turn end
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-09-16
|
||||
|
||||
English | [中文](2026-09-11-session-controller-fork-turn-cut.zh.md)
|
||||
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
# Agent Note: Session Controller 分叉截到选中轮次的结束事件
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-09-16
|
||||
|
||||
English | [中文](2026-09-11-session-controller-fork-turn-cut.md)
|
||||
[English](2026-09-11-session-controller-fork-turn-cut.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
@@ -754,6 +754,9 @@
|
||||
"bug-fix/2026-09-16-movable-mandatory-update-window.i18n.yaml": "sha256:e526ce14ae9557e24364e3686778b130090445ef2152d976b7cc93e76eac6401",
|
||||
"bug-fix/2026-09-16-movable-mandatory-update-window.md": "sha256:67546cea0b3de2b3e32957a508dbde097d91dd919fe067c8dbecb2c65b50c366",
|
||||
"bug-fix/2026-09-16-movable-mandatory-update-window.zh.md": "sha256:20a0aade6ee71c89e84d70e23c89bb51e8a38c674d103dd43022feabb8d27be3",
|
||||
"bug-fix/2026-09-11-session-controller-fork-turn-cut.i18n.yaml": "sha256:4442529ced1c8e48d30f7b50c65ff03eb92dce9815beb438cedbf29f234bce47",
|
||||
"bug-fix/2026-09-11-session-controller-fork-turn-cut.md": "sha256:a996f24a896c7fc5d81e015db3eae27d8a46c853287572e815623ce35eae1b12",
|
||||
"bug-fix/2026-09-11-session-controller-fork-turn-cut.zh.md": "sha256:6486f0eacf5067ef9850dc32f1c27b2109a7d4e9e3c498366f5c38742bb53ac5",
|
||||
"feature/2026-06-14-acp-agent-client-protocol.i18n.yaml": "sha256:006795baa43ae962a8d125cc0f1e9f134bc2ee9fb758b6e7669e3fa0126e1918",
|
||||
"feature/2026-06-14-acp-agent-client-protocol.md": "sha256:6828c0af74bb3fb96206ca6b21c0e56a000b50e4744aad4bc2c05092f3a5a31b",
|
||||
"feature/2026-06-14-acp-agent-client-protocol.zh.md": "sha256:ba104e841a1fb84edbd3b6c8119d50445b7785255a7a8d13bb9ac8a2cb4d2e69",
|
||||
|
||||
+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-06-11-content-block-vocabulary.md
|
||||
2026-06-11-content-block-vocabulary.md: d7d3f6b43a3f65d1421f026e5b6c2cc1ba1eadd2
|
||||
2026-06-11-content-block-vocabulary.zh.md: ed4f915dff6dcb6dbc91400f9bfa5384253aea7b
|
||||
2026-06-11-content-block-vocabulary.md: 917dd5225fdc4ab391e9e4629769abbb68095731
|
||||
2026-06-11-content-block-vocabulary.zh.md: 9d228eb34dafbdebab44b45dc3b290c9327d3b2b
|
||||
@@ -10,7 +10,7 @@ The harness needs one internal language for messages that the loop, session log,
|
||||
|
||||
## Decision
|
||||
|
||||
Own the vocabulary: messages are arrays of typed content blocks (`text`, `reasoning`, `tool-call`, `tool-result`), with the union derived from the merge-extensible `ContentBlockMap` so plugins add block types via declaration merging. The same merge-extensible-map pattern types every "stringly" field (`MessageSource`, `FinishReason`, `TurnTrigger`, `TurnEndReason`). Streaming is a raw chunk protocol; `BlockAssembler` is the single shared assembly implementation. Adapters translate to provider wire formats — mapping cost lives in adapters, where it belongs.
|
||||
Own the vocabulary: user, assistant, and system messages are arrays of typed content blocks (`text`, `reasoning`, `tool-call`), with the union derived from the merge-extensible `ContentBlockMap` so plugins add block types via declaration merging. Tool results are first-class tool-role messages whose own content holds result blocks; released Session format v3 retains the historical `tool-result` content block inside a user-role wrapper, and the adjacent V3-to-V4 edge lifts that wrapper into the first-class role. The same merge-extensible-map pattern types every "stringly" field (`MessageSource`, `FinishReason`, `TurnTrigger`, `TurnEndReason`). Streaming is a raw chunk protocol; `BlockAssembler` is the single shared assembly implementation. Adapters translate to provider wire formats — mapping cost lives in adapters, where it belongs.
|
||||
|
||||
In-session context injection (`context/message`) and mid-turn steering originally rendered as tagged user-role envelopes (the system-reminder pattern) rather than a new role, so adapters carry zero burden. Both now project as plain user content with no wrapper; see [the injected-content-envelope Agent Note](../simplification/2026-07-20-unwrap-injected-content-envelopes.md). Live-adapter validation confirms this rendering for current DeepSeek behavior; a future provider-specific mismatch belongs in that adapter rather than a new canonical role.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ harness 需要一套统一的内部消息语言,供 agent loop(智能体循
|
||||
|
||||
## 决策
|
||||
|
||||
自主拥有词汇:消息是类型化内容块的数组(`text`、`reasoning`、`tool-call`、`tool-result`),其联合类型派生自可合并扩展的 `ContentBlockMap`,插件通过声明合并添加新的块类型。同一可合并扩展映射模式为所有「字符串化」字段提供类型(`MessageSource`、`FinishReason`、`TurnTrigger`、`TurnEndReason`)。流式输出采用原始分片协议;`BlockAssembler` 是唯一的共享组装实现。适配器负责转换为提供方的协议格式(wire format)——映射成本留在适配器中,正是它该在的地方。
|
||||
自主拥有词汇:user、assistant 与 system 消息是类型化内容块的数组(`text`、`reasoning`、`tool-call`),其联合类型派生自可合并扩展的 `ContentBlockMap`,插件通过声明合并添加新的块类型。Tool result 是一等 tool-role message,结果块属于该 message 自己的 content;已发布的 Session format v3 仍将历史 `tool-result` content block 保存在 user-role 包装中,邻接的 V3 到 V4 边会将该包装提升为一等角色。同一可合并扩展映射模式为所有「字符串化」字段提供类型(`MessageSource`、`FinishReason`、`TurnTrigger`、`TurnEndReason`)。流式输出采用原始分片协议;`BlockAssembler` 是唯一的共享组装实现。适配器负责转换为提供方的协议格式(wire format)——映射成本留在适配器中,正是它该在的地方。
|
||||
|
||||
会话内上下文注入(`context/message`)和轮次中途 steering(中途引导)最初渲染为带标签的 user-role 信封(system-reminder 模式),而非引入新角色,因此适配器无需承担额外负担。如今两者都投影为无包装的普通用户内容;见[注入内容信封 Agent Note](../simplification/2026-07-20-unwrap-injected-content-envelopes.zh.md)。实际适配器验证已确认此渲染方式符合当前 DeepSeek 的行为;如果未来某提供方出现不兼容,应在该适配器内处理,而非引入新的规范角色。
|
||||
|
||||
|
||||
+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-07-20-routed-model-context-and-compaction-policy.md
|
||||
2026-07-20-routed-model-context-and-compaction-policy.md: 26d8a4a85c284e50b9635017f0dd0bf905a4427f
|
||||
2026-07-20-routed-model-context-and-compaction-policy.zh.md: 8656200a4b25c2ca70160e8f5949a7518e390168
|
||||
2026-07-20-routed-model-context-and-compaction-policy.md: d276354cb21f45eaec1c4e3202c7be1770745efb
|
||||
2026-07-20-routed-model-context-and-compaction-policy.zh.md: b6e2a0b914f78e19675d80b86d8d962e8ba8c80f
|
||||
+5
-5
@@ -16,7 +16,7 @@ Neither obvious configuration owner is sufficient. Compact-basic is optional and
|
||||
|
||||
`LlmAdapter.resolveModel(provider, model, signal?)` returns aggregate metadata for one exact route, with optional `LlmModelContext` under its `context` field. `LlmRuntime.resolveModelInfo()` selects the registered route owner, validates a positive integer `contextWindow`, and returns detached metadata. The query is independent of `listModels()`: an unlisted dynamic model may have capacity metadata, and an absent `context` means only that the adapter cannot describe capacity.
|
||||
|
||||
The hand-rolled DeepSeek adapter accepts optional `contextWindow` on each configured model plus an adapter-wide `defaultContextWindow`. Exact model capacity wins; an entry without capacity and an unlisted pass-through id inherit the adapter default, or omit `context` when it is absent. The two built-in model entries each publish an exact 256,000-token capacity. The pi-ai adapter resolves capacity from the same catalog descriptor that authoritatively resolves the request model.
|
||||
The hand-rolled DeepSeek adapter accepts optional `contextWindow` on each configured model plus an adapter-wide `defaultContextWindow`. Exact model capacity wins; an entry without capacity and an unlisted pass-through id inherit the adapter default, or omit `context` when it is absent. The built-in model entries each declare a 1,000,000-token context capacity. The pi-ai adapter resolves capacity from the same catalog descriptor that authoritatively resolves the request model.
|
||||
|
||||
### Token measurement remains model-agnostic
|
||||
|
||||
@@ -26,17 +26,17 @@ The hand-rolled DeepSeek adapter accepts optional `contextWindow` on each config
|
||||
|
||||
Compact-basic owns consumer policy. Top-level fields define defaults; `modelPolicies` contains partial overrides keyed by the exact `{ provider, model }` pair. Duplicate targets and unknown or invalid fields fail plugin load. `thresholdRatio` defaults to `0.8`, and retention defaults to `retainRatio: 0.16`; callers may use an absolute `retainTokens` instead, but the two retention forms are mutually exclusive. After inheritance, a ratio retention that is not below its threshold ratio also fails plugin load because no model capacity can make that policy valid.
|
||||
|
||||
For proactive pressure, compaction-basic reads the latest durable request route, resolves its adapter capacity and exact-target policy, and scales ratios into a `ResolvedCompactSpec`. It performs this resolution on every check, so a provider or model switch in one session changes capacity and policy immediately. An absolute retained budget that is not below the scaled threshold fails when the target capacity first makes that comparison possible.
|
||||
For proactive pressure, compaction-basic reads the latest durable route and resolves its adapter capacity `W`, request output reservation `O`, and exact-target policy. The trigger is `floor(min(W × thresholdRatio, W − O − headroomTokens))`, with default headroom of 65,536 tokens. The window fraction limits history growth while the second cap leaves extra room after a full output for compaction input and generation; it does not bound arbitrary tool-result growth. Retention scales `W − O` independently. `O` is the effective envelope’s `maxTokens`, else the adapter’s per-request default, else zero. Resolution runs on every check, so route or output-cap changes immediately affect policy. A nonpositive pressure budget or retention at least as large as the trigger fails when target capacity makes that comparison possible.
|
||||
|
||||
The same exact-target override can select summarization provider/model, summarization output cap, convergence retries, and overflow retry cap. These are compaction concerns and never enter an LLM provider.
|
||||
|
||||
### Target-specific pressure failures preserve optional composition
|
||||
|
||||
An adapter that lacks capacity metadata remains a valid LLM route. Manual proactive pressure fails with a target-specific configuration error; the automatic listener warns once per exact route and continues with full history. The same per-route suppression applies when resolved capacity exposes an invalid absolute retention budget, while unrelated operational failures remain independently visible. Canonical provider-confirmed overflow does not need capacity metadata: it bypasses the proactive threshold and normal retention budget, attempts one maximal balanced reduction, and preserves the original provider error unless replacement proves progress.
|
||||
An adapter that lacks capacity metadata remains a valid LLM route. Manual proactive pressure fails with a target-specific configuration error; the automatic listener warns once per exact route and continues with full history. The same suppression applies when output plus headroom exhausts capacity or retention reaches the trigger, while unrelated operational failures remain independently visible. Canonical provider-confirmed overflow does not need capacity metadata: it bypasses the proactive threshold and normal retention budget, attempts one maximal balanced reduction, and preserves the original provider error unless replacement proves progress.
|
||||
|
||||
## Testing
|
||||
|
||||
Service tests cover detached context metadata, invalid adapter output, catalog independence, and default absence. Adapter tests cover DeepSeek exact/default/unlisted resolution, invalid capacities, and pi-ai exact descriptor resolution. Compact tests cover ratio scaling, exact provider/model overrides, load-time rejection of invalid merged ratios, runtime absolute-budget validation, same-model-id provider switches, target-specific warning suppression, and capacity-independent overflow recovery. Loader fixtures reject the removed token-meter capacity setting, and examples configure capacity on adapters.
|
||||
Service tests cover detached context metadata, invalid adapter output, catalog independence, and default absence. Adapter tests cover DeepSeek exact/default/unlisted resolution, invalid capacities, and pi-ai exact descriptor resolution. Compact tests cover both trigger caps, default and overridden headroom, envelope output caps and adapter fallback, independent retention scaling, invalid budgets, exact-target overrides, provider switches, warning suppression, and capacity-independent overflow recovery. The keyless `compaction-output-reserve` headless scenario records a nonzero request cap, proactive compaction before the next step, and replacement history. Loader fixtures reject the removed token-meter capacity setting, and examples configure capacity on adapters.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -52,6 +52,6 @@ Service tests cover detached context metadata, invalid adapter output, catalog i
|
||||
- The same compaction-basic instance safely handles different windows, provider switches, and identical model ids under different providers without consulting discovery metadata.
|
||||
- LLM-only and meter-only compositions remain valid; loading compaction-basic adds no reverse dependency from adapters.
|
||||
- DeepSeek deployments may set exact per-model capacities, or use `defaultContextWindow` for entries without capacity and unlisted pass-through ids.
|
||||
- Ratio defaults scale naturally across models, while exact-target absolute retention remains available for deployment-specific behavior.
|
||||
- Ratio defaults scale naturally across models and exclude the output tokens each request reserves, while exact-target absolute retention remains available for deployment-specific behavior.
|
||||
|
||||
This note supersedes the global-capacity and no-model-policy parts of the [replay token meter service Agent Note](../../archived/architecture/2026-07-15-replay-token-meter-service.md). Its single-fold measurement decision remains unchanged.
|
||||
+5
-5
@@ -16,7 +16,7 @@ Status: implemented
|
||||
|
||||
`LlmAdapter.resolveModel(provider, model, signal?)` 返回一条精确路由的聚合元数据,其中可选的 `LlmModelContext` 位于 `context` 字段下。`LlmRuntime.resolveModelInfo()` 选择已注册的路由所属方,验证 `contextWindow` 为正整数,并返回与适配器内部状态分离的元数据。该查询独立于 `listModels()`:不在目录中的动态模型也可以拥有容量元数据,而缺少 `context` 只表示适配器无法描述容量。
|
||||
|
||||
手写 DeepSeek 适配器允许每个已配置模型提供可选 `contextWindow`,并支持适配器级 `defaultContextWindow`。精确模型容量优先;未提供容量的模型项与未列出的透传 id 会继承适配器默认值,若默认值也不存在则省略 `context`。两个内置模型项都公开精确的 256,000 token 容量。pi-ai 适配器从同一个目录描述符解析容量,该描述符也用于权威解析请求模型。
|
||||
手写 DeepSeek 适配器允许每个已配置模型提供可选 `contextWindow`,并支持适配器级 `defaultContextWindow`。精确模型容量优先;未提供容量的模型项与未列出的透传 id 会继承适配器默认值,若默认值也不存在则省略 `context`。内置模型项各自声明 1,000,000 token 的上下文容量。pi-ai 适配器从同一个目录描述符解析容量,该描述符也用于权威解析请求模型。
|
||||
|
||||
### Token 计量保持模型无关
|
||||
|
||||
@@ -26,17 +26,17 @@ Status: implemented
|
||||
|
||||
Compact-basic 拥有消费方策略。顶层字段定义默认值;`modelPolicies` 包含以精确 `{ provider, model }` 组合为键的部分覆盖。重复目标、未知字段或无效字段都会让插件加载失败。`thresholdRatio` 默认为 `0.8`,保留策略默认为 `retainRatio: 0.16`;调用方也可以改用绝对 `retainTokens`,但两种保留形式互斥。完成继承后,如果保留比例不小于阈值比例,插件也会加载失败,因为任何模型容量都无法让该策略有效。
|
||||
|
||||
对于主动压力检查,compaction-basic 读取最新持久请求路由,解析其适配器容量与精确目标策略,再把比例缩放为 `ResolvedCompactSpec`。每次检查都会重新解析,因此同一会话切换提供方或模型后,容量与策略会立即变化。若绝对保留预算不小于缩放后的阈值,系统会在目标容量首次允许比较两者时失败。
|
||||
对于主动压力检查,compaction-basic 读取最新持久路由,解析适配器容量 `W`、请求输出预留 `O` 和精确目标策略。触发阈值为 `floor(min(W × thresholdRatio, W − O − headroomTokens))`,默认余量为 65,536 tokens。窗口比例限制历史增长,第二项则在一次完整输出后为压缩输入与生成留出额外空间;它无法限制任意工具结果的增长。保留预算独立按 `W − O` 缩放。`O` 取自生效信封的 `maxTokens`,否则取适配器按请求的默认值,再回退到零。每次检查都会重新解析,因此路由或输出上限变化会立即影响策略。若压力预算非正,或保留预算不低于触发阈值,会在目标容量允许比较时失败。
|
||||
|
||||
同一精确目标覆盖还可以选择摘要提供方/模型、摘要输出上限、收敛重试次数与溢出重试上限。这些都属于压缩问题,不会进入任何 LLM 提供方。
|
||||
|
||||
### 目标专用压力错误仍保留可选组合
|
||||
|
||||
缺少容量元数据的适配器仍是有效 LLM 路由。手动主动压力检查会返回目标专用配置错误;自动监听器按精确路由只警告一次,并继续保留完整历史。当已解析容量暴露出无效的绝对保留预算时,系统也按路由抑制重复警告;其他运行故障仍会各自对外可见。提供方已经确认的规范化溢出不需要容量元数据:它绕过主动阈值与普通保留预算,尝试一次最大且平衡的缩减,并在替换无法证明进展时保留原始提供方错误。
|
||||
缺少容量元数据的适配器仍是有效 LLM 路由。手动主动压力检查会返回目标专用配置错误;自动监听器按精确路由只警告一次,并继续保留完整历史。当输出预留与余量耗尽容量,或保留预算达到触发阈值时,也会抑制重复警告;其他运行故障仍会各自对外可见。提供方已经确认的规范化溢出不需要容量元数据:它绕过主动阈值与普通保留预算,尝试一次最大且平衡的缩减,并在替换无法证明进展时保留原始提供方错误。
|
||||
|
||||
## 测试
|
||||
|
||||
服务测试覆盖与适配器内部状态分离的上下文元数据、无效适配器输出、目录独立性与默认缺失行为。适配器测试覆盖 DeepSeek 的精确容量、默认容量、未列出模型解析及无效容量,以及 pi-ai 的精确描述符解析。压缩测试覆盖比例缩放、精确提供方/模型覆盖、加载期拒绝无效合并比例、运行时校验绝对预算、相同模型 id 的提供方切换、目标专用警告抑制与不依赖容量的溢出恢复。Loader fixture(测试前置数据)会拒绝已经移除的 token-meter 容量设置,示例则在适配器上配置容量。
|
||||
服务测试覆盖与适配器内部状态分离的上下文元数据、无效适配器输出、目录独立性与默认缺失行为。适配器测试覆盖 DeepSeek 的精确容量、默认容量、未列出模型解析及无效容量,以及 pi-ai 的精确描述符解析。压缩测试覆盖两项阈值上限、默认和覆盖余量、信封输出上限与适配器回退、独立保留预算缩放、无效预算、精确目标覆盖、提供方切换、警告抑制和不依赖容量的溢出恢复。无需密钥的 `compaction-output-reserve` headless 场景记录非零请求上限、下一步之前的主动压缩和替换后的历史。Loader fixture(测试前置数据)会拒绝已经移除的 token-meter 容量设置,示例则在适配器上配置容量。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -52,6 +52,6 @@ Compact-basic 拥有消费方策略。顶层字段定义默认值;`modelPolici
|
||||
- 同一个 compaction-basic 实例无需查询发现元数据,就能安全处理不同窗口、提供方切换,以及不同提供方下的相同模型 id。
|
||||
- 仅 LLM 与仅 meter 的组合仍然有效;加载 compaction-basic 不会让适配器产生反向依赖。
|
||||
- DeepSeek 部署可以设置精确的逐模型容量,也可以让未提供容量的模型项与未列出的透传 id 使用 `defaultContextWindow`。
|
||||
- 比例默认值会随模型自然缩放,同时仍可按精确目标使用绝对保留值,以满足部署专用行为。
|
||||
- 比例默认值会随模型自然缩放,并扣除每次请求预留的输出 token;同时仍可按精确目标使用绝对保留值,以满足部署专用行为。
|
||||
|
||||
本记录取代[回放式 token 计量服务 Agent Note](../../archived/architecture/2026-07-15-replay-token-meter-service.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/architecture/2026-07-28-identified-immutable-message-values.md
|
||||
2026-07-28-identified-immutable-message-values.md: 9d2b22a5a1fc87ea7b0ff8eb24b2e8c4b86543e1
|
||||
2026-07-28-identified-immutable-message-values.zh.md: 6b36cfec0a24196ca526b0ffb21180967aa640eb
|
||||
2026-07-28-identified-immutable-message-values.md: 73c9c14479117cf654d10b3da23d155cca77054d
|
||||
2026-07-28-identified-immutable-message-values.zh.md: 7a5b9d6524beff01ac8a9e2020f75093271b7ab3
|
||||
+1
-1
@@ -16,7 +16,7 @@ This made identity a routing side effect rather than a message invariant. Produc
|
||||
|
||||
`createMessage(input)` is the canonical role-generic creation boundary. It mints a `MessageId`, detaches the supplied role, content, and source, and deep-freezes the complete value before returning it. `createUserMessage({ content, source })` fixes the user role for prompt and context producers. `createAssistantMessage({ content, source })` fixes both the assistant role and the model source kind, so model-output producers supply only content plus provider, model, and optional replay state. All creation helpers exclude an input id so callers cannot accidentally present creation as import. `freezeMessage(message)` is the separate import or transformation boundary: it detaches and deep-freezes a message whose identity already exists, without minting a replacement.
|
||||
|
||||
The message helpers live in `dsh-llm` beside the base message vocabulary because their complete contracts depend only on that vocabulary. They use `dsh-brand`'s stateless `brandString()` constructor for `MessageId` and `dsh-util-values`'s shared `deepFreeze()` implementation after detaching input with `structuredClone()`. `createToolResultMessage()` belongs with the other creation helpers: it couples a tool call id to the exact user-role tool-result block and source without depending on session state or events. `dsh-session` consumes complete messages rather than owning their construction.
|
||||
The message helpers live in `dsh-llm` beside the base message vocabulary because their complete contracts depend only on that vocabulary. They use `dsh-brand`'s stateless `brandString()` constructor for `MessageId` and `dsh-util-values`'s shared `deepFreeze()` implementation after detaching input with `structuredClone()`. `createToolResultMessage()` belongs with the other creation helpers: it couples a tool call id to a tool-role message carrying the result content, error state, and source without depending on session state or events. `dsh-session` consumes complete messages rather than owning their construction.
|
||||
|
||||
The `Agent` interface accepts a complete `UserMessage` through `followup`, `steer`, and `inject`. These operations never allocate or return identity; they freeze an imported value whose id the caller already holds. Inbox claims and `agent/pre-step` receive that message directly. A content rewrite creates a frozen replacement with the same id, while an additional context is a separately created `UserMessage` with its own id.
|
||||
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ harness 曾存在多种形似消息的表示,各自采用不同的标识规则
|
||||
|
||||
`createMessage(input)` 是角色通用的规范创建边界。它会生成 `MessageId`,将传入的角色、内容和来源与调用方对象解除引用关系,并在返回完整值前将其深度冻结。`createUserMessage({ content, source })` 为提示词和上下文生产方固定 user 角色。`createAssistantMessage({ content, source })` 同时固定 assistant 角色与模型来源类别,因此模型输出生产方只需提供内容,以及提供方、模型和可选的回放状态。所有创建辅助函数的输入都不包含 id,因此调用方不会意外地把新消息的创建伪装成已有消息的导入。`freezeMessage(message)` 是独立的导入或转换边界:它会将已有标识的消息与调用方对象解除引用关系并深度冻结,不会生成替代标识。
|
||||
|
||||
消息辅助函数位于基础消息词汇旁的 `dsh-llm` 中,因为它们的完整约定只依赖该词汇。它们使用 `dsh-brand` 的无状态 `brandString()` 构造函数生成 `MessageId`,并在通过 `structuredClone()` 分离输入后使用 `dsh-util-values` 的共享 `deepFreeze()` 实现。`createToolResultMessage()` 与其他创建辅助函数同属此处:它将工具调用 id 与确切的 user-role 工具结果块及来源耦合起来,不依赖会话状态或事件。`dsh-session` 只消费完整消息,不负责构造它们。
|
||||
消息辅助函数位于基础消息词汇旁的 `dsh-llm` 中,因为它们的完整约定只依赖该词汇。它们使用 `dsh-brand` 的无状态 `brandString()` 构造函数生成 `MessageId`,并在通过 `structuredClone()` 分离输入后使用 `dsh-util-values` 的共享 `deepFreeze()` 实现。`createToolResultMessage()` 与其他创建辅助函数同属此处:它将工具调用 id 与携带结果内容、错误状态及来源的 tool-role 消息耦合起来,不依赖会话状态或事件。`dsh-session` 只消费完整消息,不负责构造它们。
|
||||
|
||||
`Agent` 接口通过 `followup`、`steer` 和 `inject` 接收完整的 `UserMessage`。这些操作绝不会分配或返回标识;它们会冻结导入的值,而调用方已经持有该值的 id。inbox 领取和 `agent/pre-step` 会直接接收该消息。改写内容时会创建具有相同 id 的冻结替代值,而每个附加上下文都是单独创建的 `UserMessage`,拥有自己的 id。
|
||||
|
||||
|
||||
+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-07-29-dsh-source-launch-tsx-esm.md
|
||||
2026-07-29-dsh-source-launch-tsx-esm.md: 5401b0257faa69eaf76b11e7b51d06afd7ec2b8c
|
||||
2026-07-29-dsh-source-launch-tsx-esm.zh.md: f32fa3ece91bf7b9829d7012ceb090cabaa833a9
|
||||
2026-07-29-dsh-source-launch-tsx-esm.md: 700577cb9b137d67f20baeb639f99f4c0877011d
|
||||
2026-07-29-dsh-source-launch-tsx-esm.zh.md: 4d322d3dcf49442ee62f82203f17bd2ab16467fe
|
||||
@@ -14,11 +14,13 @@ Startup latency also mattered: the off-thread `module.register()` hooks worker s
|
||||
|
||||
## Decision
|
||||
|
||||
The `dsh` TUI, Web, and headless source launches run `node --import tsx/esm`: tsx's ESM-only hook owns both TypeScript transformation and tsconfig `paths` projection. The root `dsh` script uses that vector directly from the repository root; artifact generation is a separate operation under the [source-launch/build separation decision](../../archived/simplification/2026-08-12-separate-source-launch-from-build.md). The CJS hook stays off because the CLI source graph is ESM-only; measured runtime launch to the TUI banner is ~0.7s versus ~1.1s under the full tsx default and ~0.75s under the removed native chain.
|
||||
The `dsh` CLI source launches run `node --import tsx/esm`: tsx's ESM-only hook owns both TypeScript transformation and tsconfig `paths` projection. The root `dsh` script uses that vector directly from the repository root; artifact generation is a separate operation under the [source-launch/build separation decision](../../archived/simplification/2026-08-12-separate-source-launch-from-build.md). The CJS hook stays off because the CLI source graph is ESM-only; the implementation-time measurement to the TUI banner was ~0.7s versus ~1.1s under the full tsx default and ~0.75s under the removed native chain.
|
||||
|
||||
`scripts/tspath-loader.ts` and `apps/cli/src/tsconfig-paths-loader.ts` are deleted. With them went the loader's runtime rule of mapping a workspace import only for declared runtime dependencies — tsx applies the `paths` map unconditionally. Declaration completeness now rests on the static gates alone: `verify-cordis-config` for configured bare plugins, and workspace constraints for manifests. (That runtime rule found real bugs: `dsh-plan-mode` and `dsh-tool-jobs` imported `@deepseek-ai/dsh-llm` while declaring it only in devDependencies; since fixed.)
|
||||
tsx owns workspace `paths` mapping without checking whether the importer declares each package as a runtime dependency. Declaration completeness rests on the static gates: `verify-cordis-config` for configured bare plugins, and workspace constraints for manifests. The removed repo-owned paths loaders enforced those declarations at runtime and caught imports of `@deepseek-ai/dsh-llm` declared only in devDependencies; tsx does not provide that check.
|
||||
|
||||
The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`apps/cli/tests/source-launch.compat.spec.ts`): a keyless piped-stdio launch of the exact production runtime vector asserting the non-zero-exit TTY refusal. Any future Node change to module hooks or TypeScript handling turns this gate red instead of breaking developers' `pnpm dsh`.
|
||||
tsx skips the paths map when the importer URL contains `/node_modules/`. The [runtime resolution](2026-09-09-profile-resolution-generations.md) therefore records real declaring-package anchors, including recursive dependencies. This lets workspace fallback imports resolve to `src/` consistently, rather than mixing built `lib/` providers with source callers whose module-local Symbols differ. Packages without a workspace mapping retain ordinary export resolution.
|
||||
|
||||
The node-compat CI matrix runs `dsh-source-launch-smoke` (`apps/cli/tests/source-launch.compat.spec.ts`) without a build: keyless launches assert the required-profile diagnostic and profile dependency resolution without mixed Tools/AgentLoop `src/` and `lib/` instances. The source and built-bin suites share ordinary-directory and npm-link profile cases; the latter launches plain Node against built exports. The required build-backed smoke also runs `apps/cli/tests/profiles/headless/tests/source-tool.built.e2e.ts`: it checks real headless tool dispatch through tsx after the native addon and generated typert contributors are available, while still requiring core workspace modules to load from source. Future changes to module hooks or TypeScript handling are checked through these real entry paths.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -35,4 +37,4 @@ The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`
|
||||
- One launch vector across the whole engines range, including future Node lines that change native TypeScript support; the smoke gate enforces it per matrix line.
|
||||
- TypeScript transformation is delegated to tsx/esbuild again, reversing the prior note's goal of proving Node-native transformation; that goal is unreachable while vendored sources use non-erasable syntax and Node ships no transform mode.
|
||||
- The runtime declared-dependency enforcement in source launches is gone; undeclared workspace imports now surface only through static gates or built-mode resolution failures.
|
||||
- Runtime launch improves ~0.4s over the full tsx default; ACP keeps `--import tsx` because its graph was not audited for CJS-hook dependence and its launch latency is not on the interactive path.
|
||||
- Every CLI source profile uses the same ESM-only hook, including ACP. Workspace runtime entries reached by this launch must expose ESM exports; the launcher does not install a CJS TypeScript transform hook.
|
||||
@@ -14,11 +14,13 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
`dsh` 的 TUI、Web 与无头源码启动运行 `node --import tsx/esm`:由 tsx 的 ESM-only 钩子同时负责 TypeScript 转换与 tsconfig `paths` 投影。根目录的 `dsh` 脚本直接从仓库根目录使用同一启动方式;产物生成是独立操作,由[源码启动与构建分离决策](../../archived/simplification/2026-08-12-separate-source-launch-from-build.md)规定。CJS 钩子保持关闭,因为 CLI(命令行界面)源码图是纯 ESM;实测运行时启动至 TUI banner 耗时约 0.7s,对比完整 tsx 默认形态约 1.1s、已移除的原生链约 0.75s。
|
||||
`dsh` CLI(命令行界面)源码启动运行 `node --import tsx/esm`:由 tsx 的 ESM-only 钩子同时负责 TypeScript 转换与 tsconfig `paths` 投影。根目录的 `dsh` 脚本直接从仓库根目录使用同一启动方式;产物生成是独立操作,由[源码启动与构建分离决策](../../archived/simplification/2026-08-12-separate-source-launch-from-build.md)规定。CJS 钩子保持关闭,因为 CLI 源码图是纯 ESM;实现时测得启动至 TUI banner 耗时约 0.7s,对比完整 tsx 默认形态约 1.1s、已移除的原生链约 0.75s。
|
||||
|
||||
`scripts/tspath-loader.ts` 与 `apps/cli/src/tsconfig-paths-loader.ts` 已删除。随之消失的还有该 loader「仅为已声明运行时依赖映射 workspace import」的运行时规则——tsx 无条件应用 `paths` 映射。声明完整性现在仅由静态门禁保障:配置的裸插件走 `verify-cordis-config`,manifest(元数据清单)走 workspace constraints。(该运行时规则确实发现过真实缺陷:`dsh-plan-mode` 与 `dsh-tool-jobs` 导入 `@deepseek-ai/dsh-llm` 却只声明在 devDependencies;后已修复。)
|
||||
tsx 负责 workspace `paths` 映射,不检查导入方是否把每个包声明为运行时依赖。声明完整性由静态门禁保障:配置的裸插件走 `verify-cordis-config`,manifest(元数据清单)走 workspace constraints。已移除的仓库自有 paths loader 曾在运行时强制这些声明,并发现过将 `@deepseek-ai/dsh-llm` 仅声明在 devDependencies 中的 import;tsx 不提供这项检查。
|
||||
|
||||
node-compat CI 矩阵(Node 22.19 与 26)新增 `dsh-source-launch-smoke`(`apps/cli/tests/source-launch.compat.spec.ts`):以精确的生产运行时启动向量做 keyless 管道 stdio 启动,断言进程会因 TTY 拒绝而以非零状态退出。未来 Node 对模块钩子或 TypeScript 处理的任何改动都会让该门禁变红,而不是破坏开发者的 `pnpm dsh`。
|
||||
导入方 URL 含有 `/node_modules/` 时,tsx 会跳过 paths 映射。因此,[runtime resolution](2026-09-09-profile-resolution-generations.zh.md)记录声明包的真实锚点,也覆盖递归依赖。这让 workspace fallback import 一致解析到 `src/`,避免混用构建后的 `lib/` 提供方与模块本地 Symbol 不同的源码调用方。没有 workspace 映射的包仍使用普通导出解析。
|
||||
|
||||
node-compat CI 矩阵在不执行 build 的情况下运行 `dsh-source-launch-smoke`(`apps/cli/tests/source-launch.compat.spec.ts`):无密钥启动断言必需 profile 的诊断,以及不混用 Tools/AgentLoop `src/` 与 `lib/` 实例的 profile 依赖解析。源码与构建入口测试共享普通目录和 npm-link profile 用例;后者通过普通 Node 加载构建后的导出。必跑的 build-backed smoke 还运行 `apps/cli/tests/profiles/headless/tests/source-tool.built.e2e.ts`:在 native addon 和生成的 typert 贡献文件就绪后,通过 tsx 检查真实 headless 工具派发,同时仍要求核心 workspace 模块从源码加载。模块钩子或 TypeScript 处理的后续变化由这些真实入口检查。
|
||||
|
||||
## 备选方案
|
||||
|
||||
@@ -35,4 +37,4 @@ node-compat CI 矩阵(Node 22.19 与 26)新增 `dsh-source-launch-smoke`(`
|
||||
- 整个 engines 范围(包括未来改变原生 TypeScript 支持的 Node 版本线)只有一个启动向量;冒烟门禁按矩阵行强制执行。
|
||||
- TypeScript 转换重新委托给 tsx/esbuild,逆转了前一篇 Agent Note「证明 Node 原生转换可用」的目标;在 vendor 源码使用不可擦除语法且 Node 不再提供 transform 模式的情况下,该目标不可达。
|
||||
- 源码启动中的运行时依赖声明强制不复存在;未声明的 workspace import 现在只能通过静态门禁或构建模式的解析失败暴露。
|
||||
- 运行时启动相比完整 tsx 默认形态快约 0.4s;ACP(Agent Client Protocol)保留 `--import tsx`,因为它的依赖图尚未就 CJS 钩子依赖性做审计,且其启动延迟不在交互路径上。
|
||||
- 每个 CLI 源码 profile 都使用相同的 ESM-only 钩子,包括 ACP(Agent Client Protocol)。该启动方式访问的 workspace 运行时入口必须提供 ESM 导出;启动器不安装 CJS TypeScript 转换钩子。
|
||||
@@ -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-05-profile-plugin-bundles.md
|
||||
2026-08-05-profile-plugin-bundles.md: 48786a9c1ccaceb5f16c9eefed01e556cf759e6d
|
||||
2026-08-05-profile-plugin-bundles.zh.md: c88cbf98e4276549a6fae6a5b1253d3d838a64ca
|
||||
2026-08-05-profile-plugin-bundles.md: 0607f1c9a2414cc16515967b4520da8a325853fd
|
||||
2026-08-05-profile-plugin-bundles.zh.md: 9d25d3bea6337c472d161c721e44a66dca8470c9
|
||||
@@ -14,14 +14,14 @@ Everything becomes a **profile**: a directory `$DSH_HOME/profiles/<name>` with a
|
||||
|
||||
The default Profile templates use `@deepseek-ai/dsh-base` as the shared core for `web`, `headless`, `sdk`, and `acp`, with one mode bundle above it. The [standalone `sdk-minimal` profile](../../../../packages/bundle/sdk-minimal/README.md) instead lists one bundle that owns its complete explicit tree. Generic `dsh --profile <name>` hands its remaining arguments to that profile's command-line startup row: Web owns its flag family, headless owns its task positional, and the protocol profiles accept no app options. Patch overlays use launcher-owned `--patch`. A new, non-shipped target can use `--from-default-profile <template>` to copy one default template's bundle list and patch-reload policy before boot or config dump. This creates an independent profile with empty dependencies and an empty user patch: it neither reads a local profile named by the template nor records an inheritance relationship. The launcher claims the complete target directory exclusively, so existing state and concurrent creators fail without modification. `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes a base-backed profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](../../archived/architecture/2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract.
|
||||
|
||||
Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory, so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them. Bare plugin names in patch rows use the [immutable profile resolution generation](2026-09-09-profile-resolution-generations.md), which applies the same installation-first and ordered-bundle rules in memory; retained link and dual modes can materialize the same result.
|
||||
Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory, so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them. Bare plugin names in patch rows use the [immutable runtime resolution](2026-09-09-profile-resolution-generations.md), which applies installation-first and ordered-bundle rules in memory and expands dependencies from real package directories.
|
||||
|
||||
Two supporting refactors: the webserver's built-in static dist serving became the single-owner **fallback seat** (`registerFallback`/`applyIndexTaps`), with the SPA server extracted to `@deepseek-ai/dsh-host-frontend-static` so the web bundle owns its dist as composition, not launcher code; and the personal-overlay machinery of the [dsh CLI personal-config decision](../../archived/feature/2026-07-20-dsh-cli-personal-config.md) (`loadPersonalPatches`, `$DSH_HOME/config.yaml`) was retargeted to the per-profile and home-level `cordis.patch.yml` layers (`loadOptionalPatches`, `watchUserPatches` taking a filename), superseding that note's entry modes and file location while keeping its Harness-home root, patch semantics, and fail-loud parsing.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Dependency-scan plus partial `patchOrder`** (the original sketch): scanning `dependencies` for bundles and ordering unlisted ones alphabetically has two sources of truth and an implicit tie-break; one explicit ordered `dsh.profile.bundles` list is smaller and fully deterministic. A raw `pnpm add` inside the profile installs a library without activating any patch — explicit, no spooky scan.
|
||||
- **`link:` entries for in-box bundles**: pnpm cannot version, install, or update a `link:` into the installation, it embeds a machine path in a user file, and it breaks when the installation moves. The two-anchor resolution plus healed symlink fallback gives the same guarantee ("bundles come from the installation") without ceremony.
|
||||
- **`link:` entries for in-box bundles**: pnpm cannot version, install, or update a `link:` into the installation, it embeds a machine path in a user file, and it breaks when the installation moves. Two-anchor resolution plus runtime fallback keeps bundles installation-owned without persisting those links.
|
||||
- **A pre-boot `context` module in the bundle manifest** for boot-time values (dist path, flag facts): rejected in favor of pure plugins — the glue is ordinary rows and app-owned startup services, so the composition stays fully dumpable and the manifest stays data-only. The launcher-provided host slots (`ctx.cmdlineArgs`, `ctx.appExit`, and the environment snapshot) are provided in `boot()`'s `prepare` hook, before any config-tree entry mounts.
|
||||
- **Transitive bundle auto-application**: only direct `dsh.profile.bundles` entries contribute layers; a meta-bundle wanting to re-export another bundle's patch must do so explicitly in its own patch file.
|
||||
- **Dynamic template inheritance or cloning a local profile**: recording a parent would require merge and upgrade rules for bundle membership, dependencies, and user patches, while copying local state would duplicate machine-specific choices. Template-based creation copies only installation-owned defaults once.
|
||||
|
||||
@@ -14,14 +14,14 @@ Status: implemented
|
||||
|
||||
默认 Profile 模板为 `web`、`headless`、`sdk` 与 `acp` 使用 `@deepseek-ai/dsh-base` 作为共享核心,并在其上叠加一个模式组合包。[独立 `sdk-minimal` profile](../../../../packages/bundle/sdk-minimal/README.zh.md)则只列出一个拥有完整显式配置树的组合包。通用的 `dsh --profile <name>` 把剩余参数交给该 profile 的命令行启动行:Web 持有自己的 flag 家族,headless 持有任务位置参数,协议 profile 不接受应用选项。patch overlay 使用启动器持有的 `--patch`。新的非内置目标可以使用 `--from-default-profile <template>`,在启动或配置 dump 之前复制一个默认模板的 bundle 列表与 patch 重载策略。这会创建依赖为空、用户 patch 为空的独立 profile:它既不读取与模板同名的本地 profile,也不记录继承关系。launcher 会以独占方式领取完整的目标目录,因此既有状态和并发创建者都会在不作修改的情况下失败。`dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化一个以 base 为基础的 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](../../archived/architecture/2026-08-09-headless-direct-core-entry-point.md)负责 headless 组合约定。
|
||||
|
||||
解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析,因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们。patch 行中的裸插件名称使用[不可变 profile resolution generation](2026-09-09-profile-resolution-generations.zh.md),在内存中应用相同的安装优先与有序 bundle 规则;保留的 link 与 dual 模式可以物化同一结果。
|
||||
解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析,因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们。patch 行中的裸插件名称使用[不可变 runtime resolution](2026-09-09-profile-resolution-generations.zh.md),在内存中应用安装优先与有序 bundle 规则,并从包的真实目录展开依赖。
|
||||
|
||||
两项配套重构:webserver 内置的静态 dist 服务改为单一所有者的**回退席位**(`registerFallback`/`applyIndexTaps`),SPA 服务器提取到 `@deepseek-ai/dsh-host-frontend-static`,使 web 组合包以组合的方式持有自己的 dist,而不是靠启动器代码;[dsh CLI 个人配置决策](../../archived/feature/2026-07-20-dsh-cli-personal-config.md)的个人 overlay 机制(`loadPersonalPatches`、`$DSH_HOME/config.yaml`)改为面向逐 profile 与 home 级的 `cordis.patch.yml` 层(`loadOptionalPatches`、接受文件名的 `watchUserPatches`),取代该笔记的各入口模式与文件位置,同时保留其 Harness home 根目录、patch 语义与响亮失败的解析。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **依赖扫描加部分 `patchOrder`**(最初的草案):扫描 `dependencies` 找出组合包、未列出者按字母序排列,会产生两个真源和一条隐式决胜规则;一份显式有序的 `dsh.profile.bundles` 列表更小、完全确定。在 profile 内直接 `pnpm add` 只会安装一个库,不激活任何 patch——行为显式,没有暗中扫描。
|
||||
- **内置组合包使用 `link:` 条目**:pnpm 无法对指向安装目录的 `link:` 做版本管理、安装或更新,它会把机器路径嵌进用户文件,并且在安装目录移动后失效。双锚点解析加上每次启动修复的符号链接回退提供了同样的保证(「组合包来自安装目录」),且没有这些繁文缛节。
|
||||
- **内置组合包使用 `link:` 条目**:pnpm 无法对指向安装目录的 `link:` 做版本管理、安装或更新,它会把机器路径嵌进用户文件,并且在安装目录移动后失效。双锚点解析加运行时 fallback 可以让组合包保持由安装持有,无需持久化这些链接。
|
||||
- **在组合包 manifest 中放一个启动前 `context` 模块**承载启动期取值(dist 路径、flag 事实):否决,改用纯插件——粘合逻辑就是普通配置行和由应用持有的启动服务,因此组合始终可完整 dump,manifest 保持纯数据。启动器提供的宿主 slot(`ctx.cmdlineArgs`、`ctx.appExit` 与环境快照)在任何配置树条目挂载之前,于 `boot()` 的 `prepare` 钩子中提供。
|
||||
- **组合包的传递式自动应用**:只有直接列在 `dsh.profile.bundles` 中的条目才贡献层;想重新导出另一个组合包 patch 的元组合包,必须在自己的 patch 文件中显式完成。
|
||||
- **动态模板继承或克隆本地 profile**:记录父级会要求为 bundle 成员关系、依赖和用户 patch 制定合并与升级规则,而复制本地状态会重复机器特定选择。基于模板的创建只会一次性复制安装自有的默认值。
|
||||
|
||||
+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-10-session-log-version-mechanism.md
|
||||
2026-08-10-session-log-version-mechanism.md: 513b126d3b00841f71893715cc296cb67619543e
|
||||
2026-08-10-session-log-version-mechanism.zh.md: 919bfb077c7037da2ebf7eb81b75745d6b4fe4d0
|
||||
2026-08-10-session-log-version-mechanism.md: 9cb0940f767aa27d5a5106fb78b528c1e3e2695b
|
||||
2026-08-10-session-log-version-mechanism.zh.md: e1424f24e5dd74b243fe1f0aebceb5d420b9b8c3
|
||||
@@ -18,6 +18,8 @@ Session logs must be upgradable after release, and the runtime that ships first
|
||||
|
||||
**A per-event `ignorable` marker covers vocabulary growth, so ordinary event additions never bump the version.** The event vocabulary is decided by which plugins are mounted, which a single version integer cannot describe. A reader meeting an unrecognized event type refuses to interpret the log unless the event carries `ignorable: true` in its envelope. The default is *required*: forgetting the marker over-refuses a resumable session (an inconvenience), while a default of ignorable would make the same mistake silently resume a gutted one (a safety failure). The architecture makes this sound: model-visible content flows only through the four `surfaceOp`-marked surface event types plus the `request/header`/`request/context` folds, so the dangerous unknowns are exactly the non-surface events that change how the rest of the log is read (`session/end-seed` is the existing example).
|
||||
|
||||
The [accepted compatibility checkpoint](../../../../docs/session-format-status.md#finalization-record) prevents an accepted transition from reusing its header increase for later breaking changes. Backward-compatible evolution uses new acknowledgement records at the same Session version; breaking changes require a higher writer. The checkpoint does not change the unknown-event protocol.
|
||||
|
||||
### Writer and publication authority
|
||||
|
||||
`SESSION_FORMAT_VERSION` owns the checkout writer number; the [release-status reference](../../../../docs/session-format-status.md) owns one bilingual `latestReleasedVersion` and `evidenceTag` record. Publication changes independently of source development, so status is derived by comparing those facts rather than maintaining a second `released` boolean. General documentation links to these authorities; fixed-version contracts and historical evidence keep their explicit numbers.
|
||||
|
||||
@@ -18,6 +18,8 @@ Session log 在发布后必须能升级格式,而最先发布的运行时决
|
||||
|
||||
**逐事件的 `ignorable` 标记吸收词汇表增长,普通的新增事件永远不用升版本。**事件词汇表由挂载了哪些插件决定,单个版本整数描述不了它。读取器遇到不认识的事件类型时拒绝解读日志,除非该事件的信封带 `ignorable: true`。默认为必需:忘写标记的后果是把一个本可恢复的会话拒绝过头(体验问题),而默认可忽略会让同样的疏忽静默恢复出残缺会话(安全事故)。架构保证了这条规则成立:模型可见内容只经四种带 `surfaceOp` 标记的 surface 事件加 `request/header`、`request/context` 折叠进入重建,危险的未知事件恰好是那些不进 surface 但改变日志其余部分解读方式的事件(`session/end-seed` 是现存例子)。
|
||||
|
||||
[已接受兼容性检查点](../../../../docs/session-format-status.zh.md#finalization-record)阻止已接受转换复用原来的头版本递增来授权后续破坏性变更。向后兼容的演进在相同 Session 版本上使用新的确认记录;破坏性变更要求更高的写入器版本。检查点不改变未知事件协议。
|
||||
|
||||
### 写入器与发布真源
|
||||
|
||||
`SESSION_FORMAT_VERSION` 拥有工作区写入器版本号;[发布状态参考](../../../../docs/session-format-status.zh.md)拥有唯一的双语 `latestReleasedVersion` 与 `evidenceTag` 记录。发布状态独立于源码开发而变化,因此通过比较这两个事实推导状态,而不另行维护 `released` 布尔值。一般文档链接到这些真源;固定版本约定与历史证据保留明确版本号。
|
||||
|
||||
+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-18-experimental-agent-teams-packages.md
|
||||
2026-08-18-experimental-agent-teams-packages.md: a1d29b5e96b185a5539d664e19f2f5cc342a6aba
|
||||
2026-08-18-experimental-agent-teams-packages.zh.md: f4c4eb481ae6ad6319ddac50eda82fa9b9feebf7
|
||||
2026-08-18-experimental-agent-teams-packages.md: 92eba148b328135301b196c9a17e49ee23ddb3f6
|
||||
2026-08-18-experimental-agent-teams-packages.zh.md: 6f4d004a78e32e2c27ad65648c446b3cd54c07ad
|
||||
+1
-1
@@ -20,7 +20,7 @@ The generic caller-reserved continuable child identity and selective direct-chil
|
||||
|
||||
The [single-bundle decision](2026-09-18-agent-teams-single-bundle.md) supersedes the separate Host and Web bundle composition. This note retains publication, dependency isolation, and promotion rationale. The current Team profile remains opt-in and disables the global continuable-child controls whose model-visible names overlap the Team tools; the [optional-bundle decision](../process/2026-09-15-shipped-optional-bundles.md) owns its inclusion in the installation.
|
||||
|
||||
Profile startup resolves selected bundles before computing the [immutable profile resolution generation](2026-09-09-profile-resolution-generations.md). The generation retains installation-first precedence, traverses each explicit bundle root completely in profile order, and keeps pnpm-managed profile packages authoritative. Runtime mode enforces the result in memory; retained link and dual modes materialize the same result as shared and profile-owned projections. A private profile layer can therefore carry experimental plugin rows without adding those plugins to a release app, requiring profile users to install transitive packages directly, weakening packaged-runtime module identity, or changing another profile's resolution.
|
||||
Profile startup resolves selected bundles before computing the [immutable runtime resolution](2026-09-09-profile-resolution-generations.md). The runtime resolution retains installation-first precedence, traverses each explicit bundle root completely in profile order, and keeps pnpm-managed profile packages authoritative. The runtime interception enforces the result in memory without materializing fallback links. A private profile layer can therefore carry experimental plugin rows without adding those plugins to a release app, requiring profile users to install transitive packages directly, weakening packaged-runtime module identity, or changing another profile's resolution.
|
||||
|
||||
Experimental status changes compatibility and support expectations, not publication for these four packages. They retain the repository's ordinary documentation, invariant, lifecycle, security, unit, real-composition, and snapshot requirements. Promotion still requires review of the public contracts, limitations, test evidence, runtime dependents, and a named owner accepting stable-package obligations.
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ dsh 打包与发布集合以及本地基线发布器包含这四个 Agent Teams
|
||||
|
||||
[单组合包决策](2026-09-18-agent-teams-single-bundle.zh.md)取代了独立 Host 与 Web 组合包的组合方式。本记录保留发布、依赖隔离与 promotion 的理由。当前 Team profile 仍须显式启用,并禁用模型可见名称与 Team 工具重叠的全局 continuable-child control;[可选组合包决策](../process/2026-09-15-shipped-optional-bundles.zh.md)负责其随安装交付的规则。
|
||||
|
||||
profile 启动会先解析所选 bundle,再计算[不可变 profile resolution generation](2026-09-09-profile-resolution-generations.zh.md)。generation 保留安装优先顺序,按 profile 顺序完整遍历每个显式 bundle 根,并让 pnpm 管理的 profile 包保持优先。runtime 模式在内存中强制该结果;保留的 link 与 dual 模式把同一结果物化为共享和 profile 自有投影。因此,私有 profile 层可以携带实验性 plugin 配置行,而无需把这些 plugin 加入发布 app、要求 profile 用户直接安装传递依赖、破坏 packaged-runtime 的模块身份,或改变其他 profile 的解析结果。
|
||||
profile 启动会先解析所选 bundle,再计算[不可变 runtime resolution](2026-09-09-profile-resolution-generations.zh.md)。runtime resolution 保留安装优先顺序,按 profile 顺序完整遍历每个显式 bundle 根,并让 pnpm 管理的 profile 包保持优先。运行时拦截在内存中强制该结果,不物化 fallback 链接。因此,私有 profile 层可以携带实验性 plugin 配置行,而无需把这些 plugin 加入发布 app、要求 profile 用户直接安装传递依赖、破坏 packaged-runtime 的模块身份,或改变其他 profile 的解析结果。
|
||||
|
||||
对这四个包而言,实验性状态改变兼容性与支持预期,而不阻止发布。这些包仍须满足仓库的一般文档、不变式、生命周期、安全、单元测试、真实组合测试和快照要求。promotion 前仍须评审公开约定、限制、测试证据、运行时依赖方,并由一名具名 owner 接受稳定包义务。
|
||||
|
||||
|
||||
+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-24-browser-token-authentication.md
|
||||
2026-08-24-browser-token-authentication.md: cdfed1f250de6def39e386cf9fcd2e536c6c97e4
|
||||
2026-08-24-browser-token-authentication.zh.md: 29f3a6c0a2d83ed1c17b9c0bc5988c9472741f3f
|
||||
2026-08-24-browser-token-authentication.md: 47d6afe1bf4cd8169eb4e8ec92a30bf0fee32c8a
|
||||
2026-08-24-browser-token-authentication.zh.md: 13ecf55e41c7bb1455177bdd6a87a3a83d94ca6f
|
||||
@@ -12,9 +12,9 @@ The Web Host runs tool-capable Sessions with the current operating-system user's
|
||||
|
||||
`dsh-client-connection` authenticates the complete Host API before dispatch. Every API Proxy method, Remote unary call, generic Connection channel, and Remote WebSocket stream requires the same browser session; endpoint ownership and method names do not alter authority. The existing Host/Origin checks run first and retain their DNS-rebinding and cross-site-request role, returning 403 when they fail. A trusted Host without a valid browser session receives 401. The browser-trust rules remain owned by the [carrier-level browser trust decision](2026-07-28-api-browser-trust-boundary.md).
|
||||
|
||||
Each Host process generates a random launch token, retained by the root application context across Connection hot reloads. `dsh-web-app` prints and opens the normal root URL with that token in the query once per process. `frontend-static` asks Connection to authorize index responses: only `GET /?token=...` exchanges the process token for a cookie, then redirects to clean `/`; the token is not accepted on API paths or in an Authorization header. An obsolete token paired with a valid cookie redirects to clean `/`. Missing and invalid credentials receive one minimal 401 response. Static non-index assets remain public.
|
||||
Each Host process generates a random launch token, retained by the root application context across Connection hot reloads. `dsh-web-app` prints and opens the normal root URL with that token in the query once per process. `frontend-static` asks Connection to authorize index responses: only `GET /?token=...` exchanges the process token for a cookie, then redirects to clean `./`; the token is not accepted on API paths or in an Authorization header. An obsolete token paired with a valid cookie redirects to clean `./`. Missing and invalid credentials receive one minimal 401 response. Static non-index assets remain public.
|
||||
|
||||
The cookie is a signed, authority-bound bearer. Its deterministic name and signed payload both include the normalized hostname plus port, so one Harness home can run independent Web ports without cookie collisions. The payload carries safe-integer issue and expiry times under an absolute lifetime; `cookieMaxAgeDays` defaults to 30. The cookie is host-only, `Path=/`, `HttpOnly`, and `SameSite=Strict`. It omits `Secure` because the shipped server uses loopback HTTP. There is no logout operation or reverse-proxy-specific handling.
|
||||
The cookie is a signed, authority-bound bearer. Its deterministic name and signed payload both include the normalized hostname plus port, so one Harness home can run independent Web ports without cookie collisions. The payload carries safe-integer issue and expiry times under an absolute lifetime; `cookieMaxAgeDays` defaults to 30. The cookie is host-only, `Path=/`, `HttpOnly`, and `SameSite=Strict`. It omits `Secure` because the shipped server uses loopback HTTP. There is no logout operation.
|
||||
|
||||
The HMAC secret is a versioned `grant` record at `client-connection/browser-session` in `ctx.credentials`; the local provider stores it in `$DSH_HOME/.credentials.yaml`. Connection loads or creates the record during activation and retains the secret for synchronous request verification. An active Connection continues using its loaded secret if the durable record changes; the next activation loads the replacement or creates a missing record, so deleting the record and restarting the process revokes every existing cookie. Invalid owner payloads fail loud instead of being replaced. The launch token itself is never persisted and changes on every process start, while an unexpired cookie remains valid across restarts on the same authority.
|
||||
|
||||
|
||||
+2
-2
@@ -12,9 +12,9 @@ Web Host 以当前操作系统用户的权限运行具有工具能力的 Session
|
||||
|
||||
`dsh-client-connection` 在分发前认证完整 Host API。每个 API Proxy 方法、Remote 一元调用、通用 Connection channel 和 Remote WebSocket stream 都要求同一个浏览器会话;endpoint 所有权与方法名称不改变 authority。既有 Host/Origin 校验先执行,继续负责 DNS rebinding 和跨站请求防御,失败时返回 403。Host 可信但没有有效浏览器会话时返回 401。浏览器信任规则仍由[载体级浏览器信任决策](2026-07-28-api-browser-trust-boundary.zh.md)持有。
|
||||
|
||||
每个 Host 进程生成随机启动令牌,并由应用根 context 跨 Connection 热重载保留。`dsh-web-app` 每个进程只打印并打开一次 query 中带该令牌的普通根 URL。`frontend-static` 请求 Connection 授权 index 响应:只有 `GET /?token=...` 会把进程令牌交换为 cookie,再重定向到干净的 `/`;API 路径和 Authorization header 都不接受该令牌。过时令牌如果同时带有有效 cookie,会重定向到干净的 `/`。缺失与无效凭据得到同一份最小 401 响应。非 index 静态资产保持公开。
|
||||
每个 Host 进程生成随机启动令牌,并由应用根 context 跨 Connection 热重载保留。`dsh-web-app` 每个进程只打印并打开一次 query 中带该令牌的普通根 URL。`frontend-static` 请求 Connection 授权 index 响应:只有 `GET /?token=...` 会把进程令牌交换为 cookie,再重定向到干净的 `./`;API 路径和 Authorization header 都不接受该令牌。过时令牌如果同时带有有效 cookie,会重定向到干净的 `./`。缺失与无效凭据得到同一份最小 401 响应。非 index 静态资产保持公开。
|
||||
|
||||
cookie 是签名且绑定 authority 的 bearer。确定性名称与签名 payload 都包含规范化 hostname 和 port,因此同一 Harness home 可以在不同 Web port 运行而不发生 cookie 冲突。payload 在绝对有效期内携带安全整数形式的签发与过期时间;`cookieMaxAgeDays` 默认为 30。cookie 是 host-only、`Path=/`、`HttpOnly`、`SameSite=Strict`。随附服务器使用 loopback HTTP,因此不设置 `Secure`。这里没有 logout 操作或反向代理专用处理。
|
||||
cookie 是签名且绑定 authority 的 bearer。确定性名称与签名 payload 都包含规范化 hostname 和 port,因此同一 Harness home 可以在不同 Web port 运行而不发生 cookie 冲突。payload 在绝对有效期内携带安全整数形式的签发与过期时间;`cookieMaxAgeDays` 默认为 30。cookie 是 host-only、`Path=/`、`HttpOnly`、`SameSite=Strict`。随附服务器使用 loopback HTTP,因此不设置 `Secure`。这里没有 logout 操作。
|
||||
|
||||
HMAC 密钥是 `ctx.credentials` 中位于 `client-connection/browser-session` 的版本化 `grant` 记录;本地提供方将其存入 `$DSH_HOME/.credentials.yaml`。Connection 在激活期间加载或创建该记录,并保留密钥以同步校验请求。持久记录发生变化后,当前 Connection 继续使用已加载的密钥;下一次激活会加载替换记录或创建缺失记录,因此删除记录并重启进程会撤销全部既有 cookie。无效 owner payload 会明确失败,而不是被覆盖。启动令牌本身绝不持久化并在每次进程启动时变化;未过期 cookie 则能在相同 authority 上跨重启继续有效。
|
||||
|
||||
|
||||
+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-electron-desktop-packaging-and-updates.md
|
||||
2026-08-25-electron-desktop-packaging-and-updates.md: c4940942a8d5188faf1846158873c052a43e1fc1
|
||||
2026-08-25-electron-desktop-packaging-and-updates.zh.md: 011624980f6f7239a33fb2b6c6803646b7d41dd3
|
||||
2026-08-25-electron-desktop-packaging-and-updates.md: f58e8dc471faeb717dd79d32df4afc1b5cc3458f
|
||||
2026-08-25-electron-desktop-packaging-and-updates.zh.md: 2d2d5c68f28974b606693d5b6727c864c92a0763
|
||||
+2
@@ -40,6 +40,8 @@ The Desktop Host exposes the shared Web plugin manager for its reserved profile
|
||||
|
||||
The renderer uses `nodeIntegration: false`, `contextIsolation: true`, and `sandbox: true`. Preload provides boot readiness and failure reporting, native directory selection, theme synchronization, and Windows menu and appearance adapters. It exposes no raw `ipcRenderer`, filesystem access, shell commands, or pnpm arguments. Electron menus and native dialogs use typed English or Chinese copy with English fallback; Windows follows the main document’s language. The shared Web plugin manager owns its client copy.
|
||||
|
||||
Update confirmations use the shell static origin because they must remain available without a ready Web Host. Their packaged documents and assets retain the same method and path restrictions as other local static assets.
|
||||
|
||||
## Filesystem layout
|
||||
|
||||
```text
|
||||
|
||||
+2
@@ -40,6 +40,8 @@ Desktop Host 为保留 profile 提供共享 Web 插件管理器,并通过启
|
||||
|
||||
渲染进程使用 `nodeIntegration: false`、`contextIsolation: true` 和 `sandbox: true`。Preload 提供启动就绪与失败报告、原生目录选择、主题同步,以及 Windows 菜单和外观适配。它不暴露原始 `ipcRenderer`、文件系统访问、shell 命令或 pnpm 参数。Electron 菜单与原生对话框使用类型化中英文文案,并以英文回退;Windows 跟随主文档语言。共享 Web 插件管理器负责自身客户端文案。
|
||||
|
||||
更新确认使用 shell 静态 origin,因为 Web Host 尚未就绪时也必须可用。其打包文档和资源保留与其他本地静态资源相同的方法和路径限制。
|
||||
|
||||
## 文件系统布局
|
||||
|
||||
```text
|
||||
|
||||
+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-08-28-subprocess-native-containment.md
|
||||
2026-08-28-subprocess-native-containment.md: 64fd59e13ea38d7f6e066d5dadec4be830c9c0af
|
||||
2026-08-28-subprocess-native-containment.zh.md: 59e51c5a5c19f55058eb3881928d4f218a22a59c
|
||||
2026-08-28-subprocess-native-containment.md: 88acee2d41fab878a0e50380cbb87eaece8e7296
|
||||
2026-08-28-subprocess-native-containment.zh.md: 53406f99531953a0330abf08be150da845f37aab
|
||||
@@ -46,6 +46,8 @@ Linux falls back before target execution when the exact bootstrap, modern readab
|
||||
|
||||
Normal Cordis disposal starts direct-result and range observation independently, requests termination, and waits for every owned range. Consumer teardown does not inspect an ordinary PID; it retains the original operation or startup error while attempting terminate and final wait, and preserves cleanup failures in the consumer's existing error order. A confirmed empty range permanently disables later signalling against stale identities.
|
||||
|
||||
Native Windows fixtures retain the same Job-owned termination and final wait on success and failure. A descendant PID is observation-only: issuing `taskkill` after its exit can terminate an unrelated process that has reused the number, including another concurrent test worker.
|
||||
|
||||
During a JavaScript-observable host exit, `LocalSubprocessRuntime` synchronously force-terminates every still-live handle without promises or timers. Linux sends the existing direct fallback kill and the exact scope kill; Windows kills the runner so its only Job handle closes; PTY fallback scans remain best effort. Per-handle failures are contained and do not change the host's exit result. Termination modes in which JavaScript cannot run remain outside this listener's guarantee.
|
||||
|
||||
## Existing decisions and supersession
|
||||
|
||||
@@ -46,6 +46,8 @@ selector 是 per-spawn locator 或 sentinel,不是凭据或持久格式。Linu
|
||||
|
||||
正常 Cordis dispose 会独立启动 direct-result 与 range observation、请求终止,并等待每个自有 range。消费方 teardown 不检查普通 PID;它会保留原始 operation 或 startup error,同时尝试 terminate 与 final wait,并按消费方既有错误顺序保留 cleanup failure。range 一旦被确认为空,就会永久禁止后续向陈旧 identity 发送信号。
|
||||
|
||||
原生 Windows fixture 在成功和失败时都通过同一个 Job owner 请求终止并完成最终等待。后代 PID 仅用于观察:进程退出后再执行 `taskkill`,可能终止已复用该编号的无关进程,包括并发运行的其他测试 worker。
|
||||
|
||||
在 JavaScript 可观察的 host exit 期间,`LocalSubprocessRuntime` 会同步强制终止每个仍存活的句柄,不使用 Promise 或 timer。Linux 会发送既有 direct fallback kill 与准确 scope kill;Windows 会终止 runner,使其唯一 Job handle 关闭;PTY fallback 扫描仍是 best effort。每个句柄的失败相互隔离,也不改变宿主退出结果。JavaScript 无法运行的终止形态不属于该 listener 的保证。
|
||||
|
||||
## Existing decisions and supersession
|
||||
|
||||
+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-31-released-session-format-migrations.md
|
||||
2026-08-31-released-session-format-migrations.md: d566459af64f4177ed0135813e75aaf77480823c
|
||||
2026-08-31-released-session-format-migrations.zh.md: f82b406117691197d808111f1c8aa4c722d4bab2
|
||||
2026-08-31-released-session-format-migrations.md: 273b51ac7ce9c507c2322499788dc2084b857415
|
||||
2026-08-31-released-session-format-migrations.zh.md: 69132ec1763ad12b51480e150c3968aa1dcacf61
|
||||
+46
-1
@@ -59,6 +59,7 @@ JSONL record
|
||||
→ v0-to-v1 stage
|
||||
→ v1-to-v2 stage
|
||||
→ v2-to-v3 stage
|
||||
→ v3-to-v4 stage
|
||||
→ current event collector
|
||||
```
|
||||
|
||||
@@ -66,6 +67,8 @@ The chain contains no `flatMap`, spread expansion, intermediate event array, or
|
||||
|
||||
### Adjacent version ownership
|
||||
|
||||
The [V3-to-V4 specification](../../../../packages/session/session-format-v3-to-v4/README.md#v3-to-v4-specification) specifies parent catalog completion: it advances the header version, preserves admitted events and inherited cuts, and appends missing own catalog entries from child evidence. It reuses the frozen V3 codec from V2→V3. Generation-aware delivery validation prevents a historical acknowledgement from acquiring a current watermark meaning solely through the header change; earlier edges retain their own source-admission policies.
|
||||
|
||||
The [V2-to-V3 delivery guards](../../../../packages/session/session-format-v2-to-v3/README.md#delivery-guards) prevent a marker ignored in the source generation from becoming an active upload watermark merely because the header changes. Python release smoke checks generated logs against the source `SESSION_FORMAT_VERSION` independently of generation-neutral golden comparison, so coherent filenames and headers cannot conceal an outdated writer.
|
||||
|
||||
The [V2-to-V3 README](../../../../packages/session/session-format-v2-to-v3/README.md#v2-to-v3-specification) is the single specification for that edge's transformations, preservation, and refusal; its separate [native admission section](../../../../packages/session/session-format-v2-to-v3/README.md#native-v3-admission) prevents current-only capabilities from being mistaken for historical transformations. The released V2 codec remains owned by V1→V2 and is reused, not copied. The [system-prompt](2026-09-02-system-prompt-as-surface-node.md), [PTC](../feature/2026-06-15-ptc.md), and [canonical-envelope](2026-09-06-v3-canonical-session-envelopes.md) notes retain their independent rationale, not duplicate conversion specifications. The [format-version cookbook](../../../../docs/cookbook/adding-a-session-format-version.md) owns package wiring, current consumers, snapshot successors, and validation commands.
|
||||
@@ -76,7 +79,7 @@ Preset renames cover the creation header and every selection event because the l
|
||||
|
||||
A source inherited count can be unknown before EOF: V2 derives it from seed markers, and V1→V2 can change cardinality. The chain passes that absence to the next stage instead of fabricating a count. The [V2-to-V3 inheritance rules](../../../../packages/session/session-format-v2-to-v3/README.md#sequence-references) support this case; older stages that require a header-supplied count still refuse when it is absent. This permits seeded multi-hop restoration without retaining an intermediate artifact array.
|
||||
|
||||
The [version and release-status reference](../../../../docs/session-format-status.md) owns the published-format record and identifies the code’s writer authority. Released formats retain their semantics; committed generations remain byte-preserved during migration. A subsequent structural change requires the next adjacent edge under the [versioning rule](2026-08-10-session-log-version-mechanism.md), not an amendment to a released conversion. Ordinary event additions follow that rule’s required-event refusal mechanism rather than automatically allocating a version. A current-format file does not rerun its incoming migration; integration tests use isolated disposable homes and unchanged historical inputs.
|
||||
The [version and release-status reference](../../../../docs/session-format-status.md) owns the published-format record and identifies the code’s writer authority. Released formats retain their semantics; committed generations remain byte-preserved during migration. A breaking change to the released target format requires the next adjacent edge under the [versioning rule](2026-08-10-session-log-version-mechanism.md). Incoming-converter fixes affect later historical conversions, not existing target files; the [cookbook](../../../../docs/cookbook/adding-a-session-format-version.md) owns compatibility review and alpha support policy. Ordinary event additions follow that rule’s required-event refusal mechanism rather than automatically allocating a version. Integration tests use isolated disposable homes and unchanged historical inputs.
|
||||
|
||||
The [committed-corpus inventory](../../../../packages/test-support/llm-replay/tests/session-format-corpus-inventory.ts) identifies deliberately unsupported historical conversions by source path, generation, and exact refusal reason. Retaining those artifacts must not force chronology-changing migration or permit a blanket skip: every listed artifact must still raise the typed migration refusal, and unlisted artifacts must restore. Native current-generation fixtures cannot be classified as unsupported, because they do not traverse an incoming edge. Headerless test-harness protocol examples remain a separate explicit class. The corpus test checks source bytes after both successful and refused restoration; it does not rewrite historical evidence to satisfy the current reader.
|
||||
|
||||
@@ -96,10 +99,14 @@ Current encoding is record based. The provider serializes about 1 MiB of plainte
|
||||
|
||||
The shipped `lib/worker.cjs` bundles its JavaScript workspace dependencies so each fresh verifier avoids resolving and compiling their runtime module graph. This is safe because the Worker communicates through plain request/result messages and shares no service or class identity with its host. The Host build applies the existing TypeScript and Typert transforms; the Client pass skips this Node-only package instead of replacing its worker with untransformed source. Native add-ons remain external. Verification, scheduler admission, termination, and durable publication still complete before writable open returns. The built-worker smoke copies the package manifest and worker into an isolated temporary package with ambient module paths removed, accepts a valid generation, and rejects an incorrect event count.
|
||||
|
||||
The [browser preview packer](../../../../packages/experimental/webworker-packer/README.md) prepares current successors for bundled Session data in Node, using same-root direct-child descriptors as parent catalog evidence. The browser host does not implement `node:worker_threads`, so this preparation preserves both immutable fixture generations and runtime JSONL’s Worker verification requirement.
|
||||
|
||||
Preparation forwards cancellation through source reads and observes it at the existing approximately 500 ms Decode yield boundary. Once `publish()` starts, encode, Worker verification, and publication do not receive caller cancellation and run to settlement; write open checks its caller signal again afterward. A published generation is never rolled back.
|
||||
|
||||
The Stage pipeline ends at one prepared current artifact. [Historical Session read preparation](2026-09-05-read-only-session-migration-preparation.md) defines how read open consumes that artifact immediately while write open performs encode, verification, and publication before returning append access.
|
||||
|
||||
The bulk V4 migration command shares one JSONL persistence Context across a bounded job queue, retaining the backend’s two-Worker verification limit and cache for reusing recently decoded logs. A child’s concurrent publication can change its parent’s selected child generation; the ordinary revision check refuses that attempt. The command defers only this source-change error for one sequential retry after the initial queue drains, collecting fresh evidence without relaxing publication checks or retrying unrelated failures.
|
||||
|
||||
### Durable format and publication rules
|
||||
|
||||
Canonical filenames encode physical format generation: v0 is `session.jsonl[.zstd]` and positive generations use `session.vN.jsonl[.zstd]`. Migration never moves, replaces, or deletes a committed generation and writes only the final current target; intermediate versions exist only as stage state.
|
||||
@@ -122,6 +129,44 @@ Existing write handles retain the process-local claim and kernel-backed cross-pr
|
||||
| Verification repeats on the main thread | At most two complete-generation Workers | Keeps verification CPU off the main thread |
|
||||
| Production and fixture migration use different APIs | Catalog `createRestore()` with explicit policies | One decoder/chain implementation |
|
||||
|
||||
### Parent catalog prerequisites
|
||||
|
||||
V3→V4 completes parent child discovery from retained direct-child headers and own descriptors. Storage supplies compact descriptor evidence to the catalog assembly, which binds it into the V3→V4 stage factory; the Stage waits for the parent's final inherited cut before validating catalog payloads and deriving available own discovery facts. Nested seed markers discard inherited catalog candidates without interpreting their payloads. An existing catalog entry survives an unavailable descriptor, including a child that failed before its first step. Descriptor v1 denotes continuable mode; v2/v3 explicitly record mode. Available unambiguous discovery fields must agree with the parent; absent entries require one supported descriptor. Missing, unsupported, or multiple descriptors preserve the logs without backfilling that child, as recorded in [incomplete child evidence](../bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.md). JSONL rechecks related membership and source revisions when preparing, reusing, and publishing. An unsupported generation or unreadable header elsewhere in that scan refuses migration because it cannot prove unrelatedness. Source drift invalidates preparation; read open retries once automatically, while write open refuses publication. Diagnostics retain the offending child path and distinguish unsupported migration evidence from malformed child data. The storage decoder classifies physical JSON and compression failures before the collector adds child context; filesystem reads and cancellation keep their original errors. The V0–V3-only catalog avoids recursive child migration. Current V4 reads skip that scan but apply the same own-catalog field, uniqueness, and current delivery ownership checks as strict restoration. See the [format specification](../../../../packages/session/session-format-v3-to-v4/README.md).
|
||||
|
||||
Raw parent/child migration tests retain 24 historical creation-time conflicts as rejection evidence. Detached controls align only the child creation time and prove event-preserving restoration, including a published child without a descriptor. Headless/ACP and SDK snapshot adapters validate live parent/child clocks before normalization and preserve their equality during refresh; committed predecessor generations remain unchanged.
|
||||
|
||||
Historical public revisions combine the parent’s physical revision with a fingerprint of every selected canonical path and its filesystem revision. Parent-only tokens cannot identify catalog changes supplied by children; a root fingerprint preserves metadata-only `stat`/`list` and includes unreadable or unsupported members without a new persistent index. Unrelated changes also invalidate historical caches, and obtaining the token costs a root scan. Current tokens stay file-local. The preparation memo keeps physical parent revisions and separately validates child membership and revisions.
|
||||
|
||||
The generic format interfaces carry only the artifact being restored. Parent-specific catalog assembly replaces the V3→V4 declaration with a closure over the collected child evidence; it reuses the generated codec, migration, and validation inventory. The assembly compiles one short migration chain per parent preparation, with independent stage state per restore. Binding at assembly keeps child evidence out of generic restore options and stage inputs without moving discovery or source revalidation into the migration. Static header and native current reads need no child evidence; an unbound historical body restore refuses rather than assuming an empty child set.
|
||||
|
||||
<a id="catalog-scan-measurements"></a>
|
||||
### Catalog scan measurements
|
||||
|
||||
The package-local [diagnostic](../../../../packages/session/session-persistence-jsonl/tests/catalog-migration.perf.ts) measures one raw V3 parent, four direct children with 1,000 events each, and 0/100/1,000 unrelated header-only V3 Sessions. Each sample creates its own temporary corpus and Cordis context in a fresh process, runs stat → cold read → memo read → write publication → current warm read, and disposes the context and files. It uses built workspace exports under plain Node; fixture creation is outside the measured intervals. These are filesystem-cache-warm observations after fixture creation, not disk-cold latency or a performance gate.
|
||||
|
||||
On macOS arm64 with Node v26.0.0, all three samples per size are below in milliseconds. Read-only opens do not publish: membership scanning remains proportional to corpus size even when decoding is memoized. The 1,000-unrelated medians are 84.14 ms for stat, 237.97 ms for cold read, 110.98 ms for memo read, 268.46 ms for publication, and 0.41 ms for the same-process current read. These measurements document the cost; they establish no speedup claim or cross-host threshold.
|
||||
|
||||
| Unrelated Sessions | Historical stat | Cold read | Memo read | Publish | Current warm read |
|
||||
|---:|---:|---:|---:|---:|---:|
|
||||
| 0 | 5.17 | 19.00 | 1.36 | 42.88 | 0.47 |
|
||||
| 0 | 3.30 | 14.87 | 1.45 | 35.75 | 0.41 |
|
||||
| 0 | 3.18 | 16.71 | 1.50 | 34.51 | 0.47 |
|
||||
| 100 | 29.44 | 77.48 | 17.01 | 60.66 | 0.43 |
|
||||
| 100 | 12.42 | 42.71 | 13.69 | 60.19 | 0.43 |
|
||||
| 100 | 11.19 | 41.06 | 12.61 | 57.18 | 0.39 |
|
||||
| 1000 | 84.14 | 237.97 | 109.86 | 284.96 | 0.48 |
|
||||
| 1000 | 99.41 | 259.88 | 123.50 | 268.46 | 0.41 |
|
||||
| 1000 | 75.40 | 236.64 | 110.98 | 247.69 | 0.39 |
|
||||
|
||||
Run from the repository root after building the runtime:
|
||||
|
||||
```sh
|
||||
pnpm run build:lib:host
|
||||
cd packages/session/session-persistence-jsonl
|
||||
node --input-type=module -e 'import { build } from "tsdown"; await build({ config: false, entry: ["tests/catalog-migration.perf.ts"], outDir: ".artifacts/perf", tsconfig: false, dts: false, deps: { neverBundle: [/^@deepseek-ai\//], onlyBundle: false } })'
|
||||
node .artifacts/perf/catalog-migration.perf.mjs
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
The migration specification requires evidence for transformations, preservation, and refusal separately. Direct-edge and native V3 tests cannot establish seeded multi-hop publication: preceding assistant-stream folding changes source coordinates before V3 inserts system events. Tests through the real catalog and JSONL provider therefore need raw and compressed V0/V1 inputs, mapped references and inherited cuts, publish/reopen equivalence, unchanged predecessor bytes, and no intermediate generations. Coverage percentages alone cannot prove those cross-stage relationships; combined assertions must compare the resulting history and refusal effects.
|
||||
|
||||
+46
-1
@@ -59,6 +59,7 @@ JSONL record
|
||||
→ v0-to-v1 stage
|
||||
→ v1-to-v2 stage
|
||||
→ v2-to-v3 stage
|
||||
→ v3-to-v4 stage
|
||||
→ current event collector
|
||||
```
|
||||
|
||||
@@ -66,6 +67,8 @@ Chain 中不存在 `flatMap`、spread expansion、中间 event array 或 schedul
|
||||
|
||||
### 相邻版本所有权
|
||||
|
||||
[V3 到 V4 规范](../../../../packages/session/session-format-v3-to-v4/README.zh.md#v3-to-v4-specification)规定父目录补全:它推进 header 版本,保留已接纳的事件与继承切点,并根据子 Session 证据追加缺失的自身目录记录。它复用 V2→V3 中冻结的 V3 codec。按 generation 校验 delivery 可防止历史确认仅因 header 变化而获得当前水位含义;更早的 migration edge 保留各自的来源准入策略。
|
||||
|
||||
[V2 到 V3 投递保护](../../../../packages/session/session-format-v2-to-v3/README.zh.md#delivery-guards)防止源代中被忽略的标记仅因头部变化就成为有效上传水位。Python 发布冒烟测试独立于跨代 golden 比较,按源代码中的 `SESSION_FORMAT_VERSION` 检查生成日志,因此文件名与 header 自洽不能掩盖过期 writer。
|
||||
|
||||
[V2 到 V3 README](../../../../packages/session/session-format-v2-to-v3/README.zh.md#v2-to-v3-specification)是该迁移边转换、保留与拒绝规则的单一规范真源;单列的[原生准入章节](../../../../packages/session/session-format-v2-to-v3/README.zh.md#native-v3-admission)避免将仅当前版本支持的能力误认为历史转换。已发布 V2 codec 仍归 V1→V2 所有,并被复用而非复制。[系统提示词](2026-09-02-system-prompt-as-surface-node.zh.md)、[PTC](../feature/2026-06-15-ptc.zh.md)和[规范信封](2026-09-06-v3-canonical-session-envelopes.zh.md)记录保留各自独立依据,而非重复转换规范。[格式版本实操手册](../../../../docs/cookbook/adding-a-session-format-version.zh.md)负责包接线、当前消费方、快照后继代际与验证命令。
|
||||
@@ -76,7 +79,7 @@ Chain 中不存在 `flatMap`、spread expansion、中间 event array 或 schedul
|
||||
|
||||
源继承数量在 EOF 前可能未知:V2 从种子标记推导它,而 V1→V2 可以改变事件数量。迁移链将这种缺失传递给下一个 Stage,而不伪造数量。[V2 到 V3 继承规则](../../../../packages/session/session-format-v2-to-v3/README.zh.md#sequence-references)支持此情况;需要 header 提供数量的旧 Stage 仍在数量缺失时拒绝。这使有种子的多跳恢复无需保留中间产物数组。
|
||||
|
||||
[版本与发布状态参考](../../../../docs/session-format-status.zh.md)拥有已发布格式记录,并指明代码中的写入器真源。已发布格式保留其语义;迁移期间已提交代际的字节保持不变。后续结构性变更必须按[版本规则](2026-08-10-session-log-version-mechanism.zh.md)添加下一条相邻迁移边,而非修改已发布转换。普通事件新增遵循该规则的必需事件拒绝机制,而非自动分配版本。当前格式文件不会重新执行入边迁移;集成测试使用隔离、可丢弃的 home 和未变更的历史输入。
|
||||
[版本与发布状态参考](../../../../docs/session-format-status.zh.md)拥有已发布格式记录,并指明代码中的写入器真源。已发布格式保留其语义;迁移期间已提交代际的字节保持不变。对已发布目标格式的破坏性变更必须按[版本规则](2026-08-10-session-log-version-mechanism.zh.md)添加下一条相邻迁移边。传入转换器修复影响之后的历史转换,不影响已有目标格式文件;[cookbook](../../../../docs/cookbook/adding-a-session-format-version.zh.md)拥有兼容性评审和 alpha 支持政策。普通事件新增遵循该规则的必需事件拒绝机制,而非自动分配版本。集成测试使用隔离、可丢弃的 home 和未变更的历史输入。
|
||||
|
||||
[已提交语料清单](../../../../packages/test-support/llm-replay/tests/session-format-corpus-inventory.ts) 按源路径、代际与精确拒绝原因标识有意不支持的历史转换。保留这些产物不能迫使迁移改变时序,也不能允许统一跳过:每个清单中的产物仍必须抛出类型化迁移拒绝,未列入的产物必须还原。原生当前代际 fixture 不经过入边,因此不能被归为不支持。没有版本 header 的测试框架协议示例保持为独立的显式类别。语料测试在还原成功和拒绝后都检查源字节;它不通过改写历史证据来满足当前 reader。
|
||||
|
||||
@@ -96,10 +99,14 @@ Current encode 以单条 record 为单位。Provider 在主线程每个 slice
|
||||
|
||||
发布的 `lib/worker.cjs` 将 JavaScript workspace 依赖一起打包,使每个新 verifier 无需解析并编译它们的运行时模块图。Worker 只通过普通 request/result 消息通信,与 host 不共享 service 或 class identity,因此可以这样处理。Host build 应用现有 TypeScript 与 Typert 转换;Client pass 跳过这个 Node-only package,不会用未经转换的源代码覆盖 worker。Native add-on 保持 external。Verification、scheduler admission、termination 与 durable publication 仍在 writable open 返回前完成。Built-worker 冒烟测试把 package manifest 与 worker 复制到隔离的临时 package,移除环境中的模块搜索路径,接受有效 generation,并拒绝错误的 event count。
|
||||
|
||||
[浏览器 preview 打包器](../../../../packages/experimental/webworker-packer/README.zh.md)在 Node 中为随包 Session 数据准备当前代际后继,并使用同一根目录中直属子 Session 的 descriptor 作为父目录证据。浏览器 host 未实现 `node:worker_threads`,因此打包阶段的准备同时保留不可变 fixture 代际与运行时 JSONL 的 Worker 校验要求。
|
||||
|
||||
Preparation 会把 cancellation 传给 source read,并在现有的约 500 ms Decode yield 边界观察它。`publish()` 一旦开始,encode、Worker verification 与 publication 不接收 caller cancellation,并运行到终态;write open 会在之后再次检查 caller signal。已经发布的 generation 绝不会回滚。
|
||||
|
||||
Stage pipeline 终止于一份 prepared current artifact。[历史 Session 只读迁移准备](2026-09-05-read-only-session-migration-preparation.zh.md)定义 read open 如何立即消费该 artifact,以及 write open 如何在返回 append 权限前完成 encode、verification 与 publication。
|
||||
|
||||
批量 V4 迁移命令在有界任务队列中共享一个 JSONL persistence Context,保留 backend 的双 Worker 校验上限,以及复用近期已解码日志的缓存。子 Session 并发发布可能改变父 Session 选中的子代际;普通修订检查会拒绝这次尝试。命令只把这种源变化错误延后到初始队列清空后串行重试一次,重新收集证据,不放宽发布检查,也不重试无关失败。
|
||||
|
||||
### Durable format 与 publication 规则
|
||||
|
||||
规范文件名编码 physical format generation:v0 使用 `session.jsonl[.zstd]`,正 generation 使用 `session.vN.jsonl[.zstd]`。Migration 不会移动、覆盖或删除任何 committed generation,并且只写最终 current target;中间版本只存在于 stage state。
|
||||
@@ -122,6 +129,44 @@ POSIX publication 使用 hard-link creation 加目录 sync;Windows 使用 no-o
|
||||
| 主线程重复执行完整 verification | 最多两个 complete-generation Worker | Verification CPU 不占用主线程 |
|
||||
| Production 与 fixture 使用不同 migration API | Catalog `createRestore()` 加显式 policy | 只保留一套 decoder/chain 实现 |
|
||||
|
||||
### 父目录前置事实
|
||||
|
||||
V3→V4 使用保留的直属子 Session header 和自身 descriptor 补齐父 Session 的子代理发现记录。存储将紧凑的 descriptor 证据提供给 catalog 组装处,由其绑定到 V3→V4 stage 工厂;Stage 等待父 Session 的最终继承截点确定后,才校验目录 payload 并导出可获得的自身发现事实。嵌套的 seed 标记丢弃继承目录候选,不解释其 payload。已有目录记录允许 descriptor 不可用,包括在首次 step 之前失败的子 Session。Descriptor v1 表示 continuable 模式;v2/v3 显式记录模式。可获得且明确的发现字段必须与父目录一致;缺失记录要求一个受支持的 descriptor。Descriptor 缺失、版本不受支持或存在多个时,保留日志但不补填对应子 Session,理由见[不完整子目录证据](../bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.zh.md)。JSONL 在准备、复用和发布时重新检查关联成员与来源修订。扫描中其他任何不支持代际或不可读 header 都会拒绝迁移,因为无法证明它与父 Session 无关。来源变化使准备缓存失效;只读打开自动重试一次,写打开则拒绝发布。诊断保留出错子日志的路径,并区分不支持的迁移证据与畸形子数据。存储解码器先将物理 JSON 与压缩错误归类,再由收集器补充子日志信息;文件系统读取与取消保留原始错误。仅支持 V0–V3 的目录避免递归迁移子 Session。当前 V4 读取跳过该扫描,但会执行与严格恢复相同的自身目录字段、唯一性及当前投递归属检查。见[格式规范](../../../../packages/session/session-format-v3-to-v4/README.zh.md)。
|
||||
|
||||
原始父子迁移测试保留 24 个历史创建时间冲突作为拒绝证据。独立的内存对照只对齐子创建时间,证明恢复保留事件,包括已发布但没有 descriptor 的子 Session。Headless/ACP 与 SDK 快照适配器在规范化之前校验实时父子时钟,并在刷新时保持两者相等;已提交的前代文件保持不变。
|
||||
|
||||
历史格式的公开修订号组合父日志物理修订号与每个所选规范路径及其文件系统修订号的指纹。仅父日志令牌无法标识子日志提供的目录变化;全库指纹让 `stat`/`list` 保持只读元数据,也纳入不可读或不支持的成员,无需新增持久化索引。无关变化也会使历史缓存失效,获取令牌需要扫描根目录。当前格式令牌仍只取决于自身文件。准备缓存保留父日志物理修订号,并独立校验子成员集合与修订。
|
||||
|
||||
通用格式接口只传递正在恢复的 artifact。每个父 Session 的 catalog 组装将 V3→V4 声明替换为捕获已收集子 Session 证据的闭包,并复用生成的 codec、迁移与校验清单。每次父 Session 准备只编译一条短迁移链,每次恢复拥有独立的 stage 状态。在组装处绑定证据,使通用恢复选项和 stage 输入不必携带子 Session 证据,同时保留存储对发现与来源重验的所有权。静态 header 和原生当前格式读取不需要子 Session 证据;未绑定的历史正文恢复会拒绝,不会假定子 Session 集合为空。
|
||||
|
||||
<a id="catalog-scan-measurements"></a>
|
||||
### 目录扫描测量
|
||||
|
||||
包内[诊断脚本](../../../../packages/session/session-persistence-jsonl/tests/catalog-migration.perf.ts)测量一个原始 V3 父日志、四个各有 1,000 条事件的直属子日志,以及 0/100/1,000 个只有 header 的无关 V3 Session。每个样本在新进程中创建独立临时日志库与 Cordis 上下文,按 stat → 冷读 → 缓存读取 → 写入发布 → 当前格式热读顺序执行,最后释放上下文并删除文件。脚本在普通 Node 下使用已构建的工作区导出;创建夹具不计入测量区间。这些结果是夹具创建后的文件系统缓存热态观察,不代表磁盘冷读延迟,也不是性能门禁。
|
||||
|
||||
以下为 macOS arm64、Node v26.0.0 下各规模的全部三个样本,单位为毫秒。只读打开不发布后继:即使解码结果已缓存,成员扫描仍与日志库规模成正比。1,000 个无关日志时,中位数分别为 stat 84.14 ms、冷读 237.97 ms、缓存读取 110.98 ms、发布 268.46 ms,以及同进程当前格式读取 0.41 ms。这些测量记录成本,不证明性能提升,也不设跨主机阈值。
|
||||
|
||||
| 无关 Session | 历史 stat | 冷读 | 缓存读取 | 发布 | 当前格式热读 |
|
||||
|---:|---:|---:|---:|---:|---:|
|
||||
| 0 | 5.17 | 19.00 | 1.36 | 42.88 | 0.47 |
|
||||
| 0 | 3.30 | 14.87 | 1.45 | 35.75 | 0.41 |
|
||||
| 0 | 3.18 | 16.71 | 1.50 | 34.51 | 0.47 |
|
||||
| 100 | 29.44 | 77.48 | 17.01 | 60.66 | 0.43 |
|
||||
| 100 | 12.42 | 42.71 | 13.69 | 60.19 | 0.43 |
|
||||
| 100 | 11.19 | 41.06 | 12.61 | 57.18 | 0.39 |
|
||||
| 1000 | 84.14 | 237.97 | 109.86 | 284.96 | 0.48 |
|
||||
| 1000 | 99.41 | 259.88 | 123.50 | 268.46 | 0.41 |
|
||||
| 1000 | 75.40 | 236.64 | 110.98 | 247.69 | 0.39 |
|
||||
|
||||
在仓库根目录构建运行时后执行:
|
||||
|
||||
```sh
|
||||
pnpm run build:lib:host
|
||||
cd packages/session/session-persistence-jsonl
|
||||
node --input-type=module -e 'import { build } from "tsdown"; await build({ config: false, entry: ["tests/catalog-migration.perf.ts"], outDir: ".artifacts/perf", tsconfig: false, dts: false, deps: { neverBundle: [/^@deepseek-ai\//], onlyBundle: false } })'
|
||||
node .artifacts/perf/catalog-migration.perf.mjs
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
迁移规范要求分别提供转换、保留与拒绝的证据。直接迁移边和原生 V3 测试不能证明有种子的多跳发布:前代 assistant 流折叠会在 V3 插入系统事件前改变源坐标。因此,经过真实目录与 JSONL 提供方的测试需要原始及压缩的 V0/V1 输入、映射后的引用和继承切点、发布/重新打开等价性、前代字节不变,以及不产生中间代。覆盖率百分比本身不能证明这些跨阶段关系;组合断言必须比较结果历史与拒绝效果。
|
||||
|
||||
+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-01-parent-owned-subagent-catalog.md
|
||||
2026-09-01-parent-owned-subagent-catalog.md: 20e2dd035b41612592e7baeb55f89322dff02848
|
||||
2026-09-01-parent-owned-subagent-catalog.zh.md: 6ec33e8be8d4373ba98a9934178a67e207ffb0bb
|
||||
2026-09-01-parent-owned-subagent-catalog.md: 536ec33148bd675a974622b98ee23de812691dc0
|
||||
2026-09-01-parent-owned-subagent-catalog.zh.md: ec78ff9e4b7d54a7309baf21c383ec5ff83a19c8
|
||||
@@ -44,6 +44,6 @@ Current-writer snapshot expectations include catalog facts even when replay inpu
|
||||
|
||||
## Consequences
|
||||
|
||||
Session observations and client snapshots expose the direct-child list through `projections.values.subagentCatalog`. The projection change feed publishes a complete list when catalog state changes. Each view costs O(D), so D creations can incur O(D²) cumulative view work; this follows the existing projection mechanism. Direct-child and descendant listing still use the Session corpus and child identity projection.
|
||||
Session observations and client snapshots expose the direct-child list through `projections.values.subagentCatalog`. The projection change feed publishes a complete list when catalog state changes. Each view costs O(D), so D creations can incur O(D²) cumulative view work; this follows the existing projection mechanism. `listChildren()` reads the parent projection through one live-preferred Session observation; descendant listing uses the Session corpus and child identity projection. [Web projection consumption](../simplification/2026-09-08-web-subagent-catalog-projections.md) owns browser loading and synchronization.
|
||||
|
||||
Backends that do not know the required event refuse the log under the existing Session event mechanism. Catalog projection does not reconstruct missing parent facts by scanning old child logs.
|
||||
[Creation metadata exclusion](../simplification/2026-09-16-subagent-catalog-membership-only.md) records why model configuration is absent from catalog facts.
|
||||
+2
-2
@@ -44,6 +44,6 @@ snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clo
|
||||
|
||||
## 后果
|
||||
|
||||
Session 观察和客户端快照通过 `projections.values.subagentCatalog` 暴露直接子级列表。目录状态变化时,projection 变更通知发布完整列表。每次视图计算成本为 O(D),因此 D 次创建的累计视图工作量可能为 O(D²);这沿用既有 projection 机制。直接子级和后代列表仍使用 Session 语料库与子级身份 projection。
|
||||
Session observation 与 client snapshot 通过 `projections.values.subagentCatalog` 暴露直接子级列表。目录状态变化时,projection change feed 发布完整列表。每次 view 的成本为 O(D),因此 D 次创建可能累计产生 O(D²) 的 view 工作量;这沿用现有 projection 机制。`listChildren()` 通过一次 live-preferred Session observation 读取父 projection;后代枚举使用 Session 语料库与子身份 projection。[Web projection 消费决策](../simplification/2026-09-08-web-subagent-catalog-projections.zh.md) 说明浏览器加载与同步。
|
||||
|
||||
不认识该 required event 的 backend 会按既有 Session event 机制拒绝日志。目录投影不通过扫描旧子级日志来重建缺失的父级事实。
|
||||
[创建元数据排除决策](../simplification/2026-09-16-subagent-catalog-membership-only.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/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-06-v3-canonical-session-envelopes.md
|
||||
2026-09-06-v3-canonical-session-envelopes.md: 17ec331cc1ea21f31d0b51eab86d22725b6deef8
|
||||
2026-09-06-v3-canonical-session-envelopes.zh.md: a18bb3b4d71c164e3fba7288cc686a1cb7c7b4df
|
||||
2026-09-06-v3-canonical-session-envelopes.md: 68d4386b9e06025fd44d6d9054a95fb51ec8bc3e
|
||||
2026-09-06-v3-canonical-session-envelopes.zh.md: d3bfc17fa263f8edb0b6f73ce59c626d772dbc1e
|
||||
@@ -14,7 +14,7 @@ Session format V3 uses one canonical event envelope. Every `system/message`, `us
|
||||
|
||||
`SurfaceOp` is exactly `'append'` or `{ op: 'replace', startSeq, endSeq }`, with `SessionSeq` endpoints and no aliases or extra keys. Both endpoints precede the replacing event and identify an inclusive span in current surface order, not numeric sequence order. Session acceptance additionally verifies current membership, ordered endpoints, complete cited coverage, and content-only single-node tool-result replacement. Compaction payload fields such as `shadowedRange.start/end` and fold-result fields retain their own names; this is not a recursive payload rename.
|
||||
|
||||
Current acceptance rejects every `request/header.header.system` and exactly empty `tools: []` or `adapterDefaults: {}`. System prompts belong to `system/message`; `request/header` remains the non-history request snapshot. Writers omit the two empty optionals. Whitespace-only system content, `config.stop: []`, nested header/source/data extras, and nested tool schema values remain intact. A `tool/result` with `data.error` requires `message.content[0].isError === true`; a failed result need not carry error identity. Neither current reads nor migration infer an error outcome from contradictory metadata.
|
||||
Current acceptance rejects every `request/header.header.system` and exactly empty `tools: []` or `adapterDefaults: {}`. System prompts belong to `system/message`; `request/header` remains the non-history request snapshot. Writers omit the two empty optionals. Whitespace-only system content, `config.stop: []`, nested header/source/data extras, and nested tool schema values remain intact. A `tool/result` with `data.error` requires `message.isError === true`; a failed result need not carry error identity. Neither current reads nor migration infer an error outcome from contradictory metadata.
|
||||
|
||||
### Validation ownership
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ Session 格式 V3 使用一种规范事件信封。每个 `system/message`、`us
|
||||
|
||||
`SurfaceOp` 恰好为 `'append'` 或 `{ op: 'replace', startSeq, endSeq }`,端点使用 `SessionSeq`,不接受别名或额外键。两个端点都早于替换事件,并按当前 surface 顺序而非数值序号顺序标识闭区间。Session 接纳还验证当前成员关系、端点顺序、完整引用覆盖与仅修改内容的单节点工具结果替换。`shadowedRange.start/end` 等压缩(compaction)载荷字段与折叠结果字段保留各自名称;这不是对载荷进行递归重命名。
|
||||
|
||||
当前接纳拒绝任何 `request/header.header.system` 以及恰好为空的 `tools: []` 或 `adapterDefaults: {}`。系统提示词属于 `system/message`;`request/header` 仍是请求非历史状态的快照。写入方省略两个空可选字段。仅含空白的系统内容、`config.stop: []`、嵌套 header/source/data 扩展与嵌套工具 schema 值保持原样。带有 `data.error` 的 `tool/result` 要求 `message.content[0].isError === true`;失败结果不必携带错误身份。当前读取与迁移均不会根据矛盾元数据推断错误结果。
|
||||
当前接纳拒绝任何 `request/header.header.system` 以及恰好为空的 `tools: []` 或 `adapterDefaults: {}`。系统提示词属于 `system/message`;`request/header` 仍是请求非历史状态的快照。写入方省略两个空可选字段。仅含空白的系统内容、`config.stop: []`、嵌套 header/source/data 扩展与嵌套工具 schema 值保持原样。带有 `data.error` 的 `tool/result` 要求 `message.isError === true`;失败结果不必携带错误身份。当前读取与迁移均不会根据矛盾元数据推断错误结果。
|
||||
|
||||
### 校验所有权
|
||||
|
||||
|
||||
+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-08-desktop-bundled-runtime-and-external-plugins.md
|
||||
2026-09-08-desktop-bundled-runtime-and-external-plugins.md: a5f9266e0e7a8def3f0ff6fc271bddde3db30163
|
||||
2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md: 2cc9fd635102f08e6ca4afb4f086a0cf6b8b05f0
|
||||
2026-09-08-desktop-bundled-runtime-and-external-plugins.md: 61fa9eafb72133d4cbba1b65193275cc8474f7e5
|
||||
2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md: 8d94d5823526ab4e615dab13f8453c8b6da5d8b6
|
||||
+8
-8
@@ -28,17 +28,17 @@ The [Desktop file policy](../../../../apps/desktop/scripts/runtime-file-policy.t
|
||||
|
||||
Package-specific exclusions remove Domino tests, fs-ext compilation outputs, Koffi's Windows import library, and non-target node-pty prebuilds and debug symbols. The policy retains native executable dependencies, node-pty's ConPTY source distribution, licenses, and unrecognized assets; broad `src`, `test`, `.ts`, or `.map` exclusions could remove executable code or runtime data. Copy tests preserve sentinel assets and seal the filtered inventory; the Electron [payload smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs) verifies PTY output, native file seeking, FFI, image conversion, and HTML parsing. Runtime preparation still verifies every retained byte and boots the complete Host with an external plugin.
|
||||
|
||||
The shared profile runner projects missing installation and selected-bundle dependencies within the Desktop profile. pnpm-installed packages take precedence. Links resolve to real package directories under normal Node resolution, so Host and plugin imports reaching the same export share its module instance. Distinct ESM and CommonJS conditional exports remain distinct entry points; a link cannot merge a package’s dual implementations.
|
||||
The shared profile runner supplies missing installation and selected-bundle dependencies through the runtime resolution. pnpm-installed packages take precedence. Runtime resolution delegates to the selected package paths, so Host and plugin imports reaching the same export share its module instance. Distinct ESM and CommonJS conditional exports remain distinct entry points; runtime resolution does not merge a package’s dual implementations.
|
||||
|
||||
External plugins use normal Node package resolution. Desktop does not recursively check peer versions, duplicate packages, linked packages, or ancestor dependency resolution. These checks duplicate package-manager and loader responsibilities and reject pnpm-supported installation sources. Shared fallback links supply missing packages, but a plugin can resolve another installed copy; incompatible plugins may fail during Host startup and require recovery through the independent shell UI.
|
||||
External plugins use normal Node package resolution. Desktop does not recursively check peer versions, duplicate packages, linked packages, or ancestor dependency resolution. These checks duplicate package-manager and loader responsibilities and reject pnpm-supported installation sources. The runtime resolution supplies missing packages, but a plugin can resolve another installed copy; incompatible plugins may fail during Host startup and require recovery through the independent shell UI.
|
||||
|
||||
The profile manifest records pnpm-installed dependencies separately from its enabled bundle list. Disabling a plugin preserves its package, lockfile entry, and user configuration. The [thin-wrapper decision](2026-09-10-desktop-web-wrapper.md) assigns initialization, bundle reconciliation, and module links to shared app-boot helpers; Desktop holds no separate link ledger or runtime-state identity.
|
||||
The profile manifest records pnpm-installed dependencies separately from its enabled bundle list. Disabling a plugin preserves its package, lockfile entry, and user configuration. The [thin-wrapper decision](2026-09-10-desktop-web-wrapper.md) assigns initialization, bundle reconciliation, and runtime module resolution to shared app-boot helpers; Desktop holds no separate link ledger or runtime-state identity.
|
||||
|
||||
## Transactions and upgrades
|
||||
|
||||
First launch creates profile metadata and host links without running pnpm, preserving unrelated files. Compatible release changes or application relocation refresh links in place. Node version, platform, or architecture changes preserve installed plugins; pnpm and the loader report installation and compatibility failures.
|
||||
First launch creates profile metadata without running pnpm, preserving unrelated files. Each launch computes the runtime resolution from the current installation, including after a compatible release change or application relocation. Node version, platform, or architecture changes preserve installed plugins; pnpm and the loader report installation and compatibility failures.
|
||||
|
||||
Native canonical paths identify shared package directories. Windows launchers can vary path casing without moving the application; string equality would trigger unnecessary profile preparation. Profile cleanup explicitly unlinks every nested directory link before removing real directories. A Windows fixture under Electron 44 reproduces recursive `fs.rmSync` deleting files through a nested junction, while bundled upstream Node 24.17 preserves them. Cleanup qualification therefore includes the real Electron runtime; Node-only tests do not establish target preservation.
|
||||
Native canonical paths identify shared package directories. Windows launchers can vary path casing without moving the application; string equality would trigger unnecessary profile preparation.
|
||||
|
||||
The shared [plugin manager](../../../../packages/boot/plugin-manager/README.md) owns supported package specifications, bundle validation, activation, and installation failure handling. The bundled pnpm reads normal user and profile settings. Development uses the same Web manager against a separate Desktop profile, with workspace packages supplied by the development runtime.
|
||||
|
||||
@@ -51,9 +51,9 @@ The [immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-s
|
||||
Full runtime verification belongs to packaging. Startup reads the resource descriptor and checks shared package records and required Host entries. The [release-validation decision](2026-09-09-desktop-build-release-validation.md) assigns release and target compatibility checks to packaging. Startup neither enumerates nor hashes installed runtime files. Reading every file before backend loading adds I/O proportional to distribution size; unusable modules instead fail when loaded. Build-time verification rejects changed, missing, extra, or linked files against the recorded inventory.
|
||||
|
||||
- **Install the bundled offline seed at startup.** This preserves an ordinary pnpm installation procedure but repeats core extraction and installation on every affected machine. Materialized resources remove that work at the cost of more application files and release-builder responsibility.
|
||||
- **Force host dependency versions into plugins.** This unnecessarily couples ordinary plugin dependencies to the host. Shared fallback supplies missing packages while pnpm-owned entries retain independent versions.
|
||||
- **Use hardlinks.** They cannot represent directories, may not cross volumes, share writable bytes, and retain old inodes after application replacement. Directory symlinks and Windows junctions express the intended package target.
|
||||
- **Use `NODE_PATH` or preserve symlink paths.** These do not provide uniform ESM resolution or shared module identity. Normal package lookup through explicit links is directly testable.
|
||||
- **Force host dependency versions into plugins.** This unnecessarily couples ordinary plugin dependencies to the host. The runtime resolution supplies missing packages while pnpm-owned entries retain independent versions.
|
||||
- **Use hardlinks.** They cannot represent directories, may not cross volumes, share writable bytes, and retain old inodes after application replacement. Runtime resolution requires no filesystem projections.
|
||||
- **Use `NODE_PATH` or preserve symlink paths.** These do not provide uniform ESM resolution or shared module identity. The runtime resolution gives ESM and CommonJS the same package selection.
|
||||
- **Keep core packages in ASAR.** Ordinary `extraResources` preserves native loading and subprocess paths. ASAR requires separate package-resolution qualification.
|
||||
|
||||
## Consequences
|
||||
|
||||
+8
-8
@@ -28,17 +28,17 @@ Desktop 初始化时安装核心依赖图,会重复发布构建器已经完成
|
||||
|
||||
包专用排除项包括 Domino 测试、fs-ext 编译产物、Koffi 的 Windows 导入库,以及非目标平台的 node-pty 预构建文件和调试符号。规则保留原生可执行依赖、node-pty 的 ConPTY 源分发内容、许可证和未知资源;宽泛排除 `src`、`test`、`.ts` 或 `.map` 可能移除可执行代码或运行时数据。复制测试保留哨兵资源并封存过滤后的清单;Electron 的[产物 smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs)验证 PTY 输出、原生文件定位、FFI、图像转换和 HTML 解析。运行时准备仍会验证每个保留字节,并携带外部插件启动完整 Host。
|
||||
|
||||
共享 profile runner 在 Desktop profile 内补全安装包与选中 bundle 缺失的依赖。pnpm 安装的包优先。链接通过正常 Node 解析指向真实包目录,因此 Host 与插件导入同一导出时共享其模块实例。不同的 ESM 与 CommonJS 条件导出仍是不同入口;链接不能合并包的双重实现。
|
||||
共享 profile runner 通过 runtime resolution 补全安装包与选中 bundle 缺失的依赖。pnpm 安装的包优先。运行时解析委托给所选包路径,因此 Host 与插件导入同一导出时共享其模块实例。不同的 ESM 与 CommonJS 条件导出仍是不同入口;运行时解析不会合并包的双重实现。
|
||||
|
||||
外部插件使用正常 Node 包解析。Desktop 不递归检查 peer 版本、重复包、链接包或上级目录依赖解析。这些检查重复包管理器及加载器的职责,并拒绝 pnpm 支持的安装来源。共享补全链接提供缺失的包,但插件可以解析到另一份已安装副本;不兼容插件可能在 Host 启动时失败,需要通过独立壳 UI 恢复。
|
||||
外部插件使用正常 Node 包解析。Desktop 不递归检查 peer 版本、重复包、链接包或上级目录依赖解析。这些检查重复包管理器及加载器的职责,并拒绝 pnpm 支持的安装来源。runtime resolution 提供缺失的包,但插件可以解析到另一份已安装副本;不兼容插件可能在 Host 启动时失败,需要通过独立壳 UI 恢复。
|
||||
|
||||
profile manifest 分别记录 pnpm 安装的依赖及已启用 bundle 列表。禁用插件保留其包、锁文件条目及用户配置。[薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)将初始化、bundle 协调和模块链接交给共享 app-boot helper;Desktop 不保存独立链接账本或运行时状态身份。
|
||||
profile manifest 分别记录 pnpm 安装的依赖及已启用 bundle 列表。禁用插件保留其包、锁文件条目及用户配置。[薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)将初始化、bundle 协调和运行时模块解析交给共享 app-boot helper;Desktop 不保存独立链接账本或运行时状态身份。
|
||||
|
||||
## 事务与升级
|
||||
|
||||
首次启动创建 profile 元数据和宿主链接,不运行 pnpm,并保留无关文件。兼容的发布变化或应用移动会直接刷新链接。Node 版本、平台或架构变化时保留已安装插件;安装和兼容性错误由 pnpm 与加载器报告。
|
||||
首次启动创建 profile 元数据,不运行 pnpm,并保留无关文件。每次启动都从当前安装计算 runtime resolution,包括兼容的发布变化或应用移动之后。Node 版本、平台或架构变化时保留已安装插件;安装和兼容性错误由 pnpm 与加载器报告。
|
||||
|
||||
共享包目录使用原生规范路径识别。Windows 启动器可能改变路径大小写而不移动应用;字符串相等判断会触发不必要的 profile 准备。profile 清理在移除真实目录前,显式解除每一个嵌套目录链接。Windows 夹具在 Electron 44 下复现了递归 `fs.rmSync` 沿嵌套 junction 删除目标文件,而内置上游 Node 24.17 会保留它们。因此清理验收包含真实 Electron 运行时;仅在 Node 下测试不能证明目标文件会保留。
|
||||
共享包目录使用原生规范路径识别。Windows 启动器可能改变路径大小写而不移动应用;字符串相等判断会触发不必要的 profile 准备。
|
||||
|
||||
共享[插件管理器](../../../../packages/boot/plugin-manager/README.zh.md)负责支持的包规格、bundle 验证、激活和安装失败处理。内置 pnpm 读取正常的用户和 profile 设置。开发模式使用相同的 Web 管理器操作独立 Desktop profile,工作区包由开发运行时提供。
|
||||
|
||||
@@ -51,9 +51,9 @@ profile manifest 分别记录 pnpm 安装的依赖及已启用 bundle 列表。
|
||||
完整运行时验证属于打包流程。启动读取资源描述文件,并检查共享包记录及必要的 Host 入口。[发布验证决策](2026-09-09-desktop-build-release-validation.zh.md)将发布与目标兼容性检查交给打包流程。启动既不枚举已安装运行时文件,也不计算其哈希。后端加载前读取每个文件会增加与分发体积成正比的 I/O;不可用模块改由加载时失败暴露。构建时验证按记录清单拒绝内容变化、缺失、多余或链接文件。
|
||||
|
||||
- **启动时安装内置离线 seed。** 这保留普通 pnpm 安装流程,但会在每台受影响机器上重复核心解压与安装。物化资源消除了这部分工作,代价是更多应用文件和发布构建器责任。
|
||||
- **强制插件使用 Host 依赖版本。** 这会让普通插件依赖不必要地耦合于 Host。共享模块补全提供缺失的包,pnpm 拥有的条目保留独立版本。
|
||||
- **使用硬链接。** 它不能表示目录,可能无法跨卷,共享可写字节,并在应用替换后保留旧 inode。目录软链接和 Windows junction 能表达预期的包目标。
|
||||
- **使用 `NODE_PATH` 或保留软链接路径。** 它们不能提供统一的 ESM 解析或共享模块身份。通过明确链接进行正常包查找可以直接测试。
|
||||
- **强制插件使用 Host 依赖版本。** 这会让普通插件依赖不必要地耦合于 Host。runtime resolution 提供缺失的包,pnpm 拥有的条目保留独立版本。
|
||||
- **使用硬链接。** 它不能表示目录,可能无法跨卷,共享可写字节,并在应用替换后保留旧 inode。运行时解析不需要文件系统投影。
|
||||
- **使用 `NODE_PATH` 或保留软链接路径。** 它们不能提供统一的 ESM 解析或共享模块身份。runtime resolution 为 ESM 与 CommonJS 提供相同的选包结果。
|
||||
- **把核心包留在 ASAR。** 普通 `extraResources` 保留原生加载和子进程路径。ASAR 需要单独验证包解析。
|
||||
|
||||
## 影响
|
||||
|
||||
+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-09-plugin-management-in-the-web-sidebar.md
|
||||
2026-09-09-plugin-management-in-the-web-sidebar.md: d2a3329e7c878f25ad5e50d98fc2e3ab39722887
|
||||
2026-09-09-plugin-management-in-the-web-sidebar.zh.md: aae035b3a93aec72e84e2792334c42c54d2bca62
|
||||
2026-09-09-plugin-management-in-the-web-sidebar.md: ba359aeefe3883445fd5f3e3dc164262e0613219
|
||||
2026-09-09-plugin-management-in-the-web-sidebar.zh.md: 91664deeb422c1dc6c34f826a85f398183128d1c
|
||||
+4
-4
@@ -10,9 +10,9 @@ Installed packages belong to the running profile, while Settings is a modal over
|
||||
|
||||
## Decision
|
||||
|
||||
**Management is a sidebar entry; configuration stays in Settings.** `ui-plugin-manager` registers a `sidebar.panellist` entry and the `main` panel it opens under `plugins`. The page manages the profile's bundles and their rows through the [plugin manager](2026-09-14-current-profile-plugin-management.md) Remote, displays install output and confirms uninstalls. The page lists installed bundles only. The Settings Plugins section keeps the global configuration cards beside the read-only Plugin list tab, where the installation's own bundles (`dsh-base`, `dsh-web-app`) are inspected; both of that tab's groups start collapsed, and the tab carries no management controls of its own.
|
||||
**Management is a sidebar entry.** `ui-plugin-manager` registers a `sidebar.panellist` entry and the `main` panel it opens under `plugins`. The page manages the profile's bundles and their rows through the [plugin manager](2026-09-14-current-profile-plugin-management.md) Remote, displays install output and confirms uninstalls. It lists installed bundles and installation-provided optional bundles. Settings keeps the read-only plugin inventory, including the installation's own bundles (`dsh-base`, `dsh-web-app`); both inventory groups start collapsed, and the inventory carries no management controls. Configuration placement follows the [plugin configuration decision](2026-09-16-plugin-configuration-on-the-plugins-page.md).
|
||||
|
||||
**One store follows Host state.** The manager controller joins `listBundles` with `listPlugins` into one view per bundle, decides availability from the inventory's `managementAvailable`, refreshes after management operations, on `plugin-manager/changed`, and on reconnect, and keeps installation progress under the owning job. Configuration cards use the existing global settings bindings.
|
||||
**One store follows Host state.** The manager controller joins `listBundles` with `listPlugins` into one view per bundle, decides availability from the inventory's `managementAvailable`, refreshes after management operations, on `plugin-manager/changed`, and on reconnect, and keeps installation progress under the owning job. Configuration forms use the existing global settings bindings.
|
||||
|
||||
**Installation results belong to a request.** The dialog generates a fresh request id for every install or retry and filters Host progress, logs, and responses by that id. A cancellation acknowledgement can arrive before the original add response, so that response cannot settle a subsequent retry. Cancellation uses the manager's explicit cleanup acknowledgement; local RPC cancellation and connection loss never imply that pnpm has stopped. The application phase closes the cancellation window.
|
||||
|
||||
@@ -20,11 +20,11 @@ Installed packages belong to the running profile, while Settings is a modal over
|
||||
|
||||
**A settings section that opens the management page.** Rejected: the dialog covers the main column, so such an entry would have to close Settings to show the page.
|
||||
|
||||
**Configuration on the plugin's page.** Rejected for now, for the reasons in the decision; it becomes a link from the plugin's page once Settings can be opened on one section.
|
||||
**Configuration placement.** The [plugin configuration decision](2026-09-16-plugin-configuration-on-the-plugins-page.md) owns this choice and its alternatives; sidebar navigation and installation-request ownership remain independent of it.
|
||||
|
||||
## Consequences
|
||||
|
||||
The web bundle's panel list is no longer empty: the **Plugins** entry sits between New Session and the workspaces. The Settings Plugins section keeps two tabs: the configuration page and the read-only Plugin list. `apps/web/tests/plugin-manager.e2e.ts` reaches the manager through the sidebar, and the `plugin-config` and `settings-chrome` scenarios and goldens follow.
|
||||
The web bundle's **Plugins** sidebar entry opens profile management. Settings exposes the read-only inventory, while plugin forms live on the Plugins page. `apps/web/tests/plugin-manager.e2e.ts` reaches the manager through the sidebar; `plugin-config` and `settings-chrome` exercise the forms and read-only inventory respectively.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
# Agent Note:插件管理移到 Web 侧栏
|
||||
# Agent Note: 插件管理移到 Web 侧栏
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -10,9 +10,9 @@ Status: implemented
|
||||
|
||||
## 决定
|
||||
|
||||
**管理位于侧栏,配置保留在设置中。** `ui-plugin-manager` 在 `plugins` 下注册 `sidebar.panellist` 入口与它打开的 `main` 面板。页面通过[插件管理器](2026-09-14-current-profile-plugin-management.zh.md)的 Remote 管理 profile 的组合包及其行、展示安装输出,并确认卸载。页面只列出已安装的组合包。设置的插件分区保留全局配置卡片,旁边是只读的「插件列表」标签页,随安装提供的组合包(`dsh-base`、`dsh-web-app`)在那里查看;该标签页的两个分组默认收起,且不带任何管理控件。
|
||||
**管理位于侧栏。** `ui-plugin-manager` 在 `plugins` 下注册 `sidebar.panellist` 入口与它打开的 `main` 面板。页面通过[插件管理器](2026-09-14-current-profile-plugin-management.zh.md)的 Remote 管理 profile 的组合包及其行、展示安装输出,并确认卸载。它列出已安装的组合包与安装随附的可选组合包。设置保留只读插件清单,包括随安装提供的组合包(`dsh-base`、`dsh-web-app`);清单的两个分组默认收起,且不带管理控件。配置的位置遵循[插件配置页决策](2026-09-16-plugin-configuration-on-the-plugins-page.zh.md)。
|
||||
|
||||
**一个 store 跟随 Host 状态。** 管理器控制器把 `listBundles` 与 `listPlugins` 合成每个组合包一份视图,按清单的 `managementAvailable` 判定可用性,在管理操作后、收到 `plugin-manager/changed` 时以及重连后刷新,并按所属 job 保存安装进度。配置卡片使用原有全局 settings 绑定。
|
||||
**一个 store 跟随 Host 状态。** 管理器控制器把 `listBundles` 与 `listPlugins` 合成每个组合包一份视图,按清单的 `managementAvailable` 判定可用性,在管理操作后、收到 `plugin-manager/changed` 时以及重连后刷新,并按所属 job 保存安装进度。配置表单使用原有全局 settings 绑定。
|
||||
|
||||
**安装结果属于一次请求。** 每次安装或重试都生成新的请求 ID,对话框按该 ID 筛选 Host 进度、日志和返回结果。取消确认可能先于原 add 响应到达,因此旧响应不能结束随后的重试。取消采用管理器的明确清理确认;本地 RPC 取消和断线均不表示 pnpm 已停止。进入配置应用阶段后关闭取消窗口。
|
||||
|
||||
@@ -20,11 +20,11 @@ Status: implemented
|
||||
|
||||
**用一个设置分区打开管理页。** 否决:对话框盖住主列,这样的入口必须先关掉设置才能显示页面。
|
||||
|
||||
**把配置放在插件页面上。** 暂不采纳,理由见决定;等设置能按分区打开后,它会变成插件页面上的一个链接。
|
||||
**配置的位置。** 该选择及其替代方案由[插件配置页决策](2026-09-16-plugin-configuration-on-the-plugins-page.zh.md)维护;侧栏导航与安装请求的归属独立于它。
|
||||
|
||||
## 后果
|
||||
|
||||
web bundle 的面板列表不再为空:**插件**入口位于新建会话与工作区之间。设置的「插件」分区保留两个标签页:配置页与只读的「插件列表」。`apps/web/tests/plugin-manager.e2e.ts` 经侧栏到达管理器,`plugin-config` 与 `settings-chrome` 的场景与 golden 随之更新。
|
||||
web bundle 的**插件**侧栏入口打开 profile 管理。设置提供只读清单,插件表单位于插件页。`apps/web/tests/plugin-manager.e2e.ts` 经侧栏到达管理器;`plugin-config` 与 `settings-chrome` 分别验证表单和只读清单。
|
||||
|
||||
## 测试
|
||||
|
||||
|
||||
+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-09-producer-owned-message-sources.md
|
||||
2026-09-09-producer-owned-message-sources.md: 83f8cb9368b516b7b6fde8717f9e5f8e9d4f7d4e
|
||||
2026-09-09-producer-owned-message-sources.zh.md: f5cdfc48286f7b92883938c8e316a7751580f52c
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: Producer-owned message sources at the V4 edge
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-09-producer-owned-message-sources.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Released V3 plugin attribution uses the shared wrapper `{ kind: 'plugin', plugin: '<producer>' }` plus the producer's context-form fields, so consumers had to treat one synthetic kind specially instead of switching on the producer identity. Once tool results became first-class tool-role messages and the current generation moved to V4, the format layer also had to keep reading released V3 files whose rows still carry the wrapper.
|
||||
|
||||
## Decision
|
||||
|
||||
Message sources are producer-owned: `kind` identifies the producer, with declarations contributed through `MessageSourceMap`. The V3-to-V4 migration uses its fixed producer table: `@deepseek-ai/dsh-system-prompt` becomes `system-prompt` for system-role messages and `runtime-context` otherwise; `compact` becomes `compact-checkpoint`; `tools-code-mode` and `tools-ptc` become `ptc-mode`; `dsh-compaction-basic` becomes `compact-basic`; known same-name producers retain their kind. Unknown plugin names receive `plugin:` before the complete original name, while direct source kinds retain their original names. The prefix keeps a plugin named `user` separate from the human-user kind; this edge leaves direct attribution unchanged. Original context-form fields and other own JSON metadata survive; only the old wrapper's identity fields receive their specified conversion. Rename lookup uses own keys, so `__proto__` and `constructor` remain data.
|
||||
|
||||
Native V4 source admission rejects the retired `kind: 'plugin'` wrapper in every declared durable message slot, including inbox and title-request messages. This refusal applies before recoverable scanning can discard a suffix. Complete source-slot validation runs in the shared known-event pass used by both the format catalog and native JSONL scanner, before either exposes a restored artifact or handle. Unknown nonempty attribution kinds and their extra JSON fields survive without their producer installed. Native compaction and title relationship checks use the producer kinds directly; the [mandatory validation decision](2026-09-17-native-v4-read-validation.md) owns common lifecycle rules.
|
||||
|
||||
The source rewrite is owned by the V3-to-V4 migration stage and runs once while historical events become V4 events. The current V4 codec retains only the released physical framing and native V4 admission; it never sends a V4 row through the released V3 validator and has no source-conversion view in its encode, decode, restore, or row-admission paths.
|
||||
|
||||
Recorded current V4 fixtures and writer, notification, and stream-json sidecars are producer-owned; released V2/V3 fixtures and sidecars keep plugin attribution and convert at read time through the migration. The [released session-format migrations](2026-08-31-released-session-format-migrations.md) note owns the versioning rules this edge follows.
|
||||
|
||||
The [source-attribution policy](2026-09-17-persistence-attribution-policy.md) distinguishes durable source declarations from request-only user inputs and records the narrowly scoped compatibility promise for new attribution kinds. Frozen migration rules retain historical producer kinds even when no active producer declares them.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the released plugin wrapper as the current shape.** Consumers would keep switching on one synthetic kind and the producer identity stays an untyped string; the current semantic validators would keep a second, unreleased source vocabulary.
|
||||
|
||||
**Rewrite released rows only at scan time instead of migrating them.** Scan admission is deliberately shape-only and refuses, never repairs; a scan-time rewrite would blur released generations with current rows and leave the current artifact with version-inappropriate source identities.
|
||||
|
||||
**Reject plugin-kind rows as ordinary corruption in the scanner.** That would let a stale released row silently truncate a current Session instead of surfacing a refusal, and would misclassify rows that full decoding still needs to diagnose.
|
||||
|
||||
## Consequences
|
||||
|
||||
The recorded corpus for this migration applies the frozen rename table, while current-generation validation consumes producer-owned kinds directly. Web context labels render the producer kind (`runtime-context`) instead of the released plugin string. The shared rewriter reports malformed released sources as `SessionFormatError`, so a malformed historical source is a migration refusal rather than a silent payload loss.
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Agent Note: V4 边缘上的生产者归属消息源
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-09-producer-owned-message-sources.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
已发布 V3 的插件归属使用共享包装 `{ kind: 'plugin', plugin: '<producer>' }` 外加生产者的上下文形态字段,消费方因此必须特殊对待一个合成 kind,而不是直接按生产者身份切换。工具结果成为一等 tool-role 消息、当前世代进入 V4 之后,格式层还必须继续读取仍带包装的已发布 V3 文件。
|
||||
|
||||
## Decision
|
||||
|
||||
消息源由生产者拥有:`kind` 标识生产者,各方通过 `MessageSourceMap` 提供声明。V3-to-V4 迁移使用固定生产者表:`@deepseek-ai/dsh-system-prompt` 在 system-role 消息上变成 `system-prompt`,否则变成 `runtime-context`;`compact` 变成 `compact-checkpoint`;`tools-code-mode` 与 `tools-ptc` 变成 `ptc-mode`;`dsh-compaction-basic` 变成 `compact-basic`;已知同名生产者保留 kind。未知插件名在完整原名之前添加 `plugin:`,直接 source kind 则保留原名。该前缀将名为 `user` 的插件与人类用户 kind 区分开;本边保持直接归属不变。原上下文形态字段及其他自有 JSON 元数据保留;只有旧包装的身份字段执行指定转换。重命名查找使用自有键,因此 `__proto__` 和 `constructor` 仍是数据。
|
||||
|
||||
原生 V4 消息源准入在每个声明的持久化消息槽位拒绝退役的 `kind: 'plugin'` 包装,包括 inbox 和标题请求消息。可恢复扫描丢弃后缀之前就会执行此拒绝。完整消息源槽位校验在格式目录和原生 JSONL 扫描器共用的已知事件校验中执行,先于两者公开恢复产物或句柄。即使生产者未安装,未知的非空归属 kind 及额外 JSON 字段仍原样保留。原生压缩和标题关系校验直接使用生产者 kind;[强制校验决策](2026-09-17-native-v4-read-validation.zh.md)负责通用生命周期规则。
|
||||
|
||||
来源重写由 V3 到 V4 迁移 Stage 负责,在历史事件转换为 V4 事件时只执行一次。当前 V4 编解码器只保留已发布的物理分帧和原生 V4 准入;它不会让 V4 行进入已发布 V3 校验器,编码、解码、恢复和行准入路径也没有来源转换视图。
|
||||
|
||||
录制的当前 V4 fixtures 与 writer、notification、stream-json 侧车是生产者归属的;已发布的 V2/V3fixtures 与侧车保留 plugin 归属,读取时经迁移转换。[released session-format migrations](2026-08-31-released-session-format-migrations.zh.md)笔记拥有本边所遵循的版本规则。
|
||||
|
||||
[消息源归属策略](2026-09-17-persistence-attribution-policy.zh.md)区分持久化 source 声明与仅用于请求的 user 输入,并记录新增归属 kind 的受限兼容性承诺。即使没有活动生产者声明,冻结迁移规则仍保留历史生产者 kind。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**保留 released plugin 包装作为当前形态。** 消费方将继续特殊对待一个合成 kind,生产者身份仍是无类型字符串;当前语义校验器也会保留第二套非发布的消息源词汇。
|
||||
|
||||
**只在扫描时重写 released 行而不是迁移它们。** 扫描准入刻意只查形态并拒绝、从不修复;扫描时重写会模糊 released 世代与当前行的界限,使当前产物保留与版本不符的来源身份。
|
||||
|
||||
**在 scanner 中把 plugin-kind 行当作普通损坏拒绝。** 这会让过期的 released 行静默截断当前 Session,而不是给出拒绝;也会把完整解码仍需要诊断的行错误分类。
|
||||
|
||||
## Consequences
|
||||
|
||||
本迁移的记录语料库应用冻结重命名表,而当前代校验器直接消费生产者自有 kind。Web 上下文标签渲染生产者 kind(`runtime-context`)而不是 released plugin 字符串。共享重写器将畸形历史来源报告为 `SessionFormatError`,因此迁移会拒绝它,而不会静默丢失载荷。
|
||||
+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-09-profile-resolution-generations.md
|
||||
2026-09-09-profile-resolution-generations.md: ad6f05ba88b25ea52da97834cf4638c8feb961ba
|
||||
2026-09-09-profile-resolution-generations.zh.md: ffa9abc5a6a04bc9553878946fa3fb422328312c
|
||||
2026-09-09-profile-resolution-generations.md: ced79eefc8c46a43cabd5b6a79db2644526c022f
|
||||
2026-09-09-profile-resolution-generations.zh.md: 98c509488aaeaa8d1b5e722bf6e3ea6346d45c46
|
||||
+60
-30
@@ -8,27 +8,65 @@ English | [中文](2026-09-09-profile-resolution-generations.zh.md)
|
||||
|
||||
A profile loads plugin rows from its own package project, while Harness packages and packages carried by selected bundles can live outside that project's ordinary dependency tree. Bridging the trees through shared symlinks, profile-owned links, or packaged-executable proxy packages persists package selections across processes and installations. Those files require reconciliation and locking, expose generated proxy manifests to metadata readers, and cannot represent a process-local change atomically.
|
||||
|
||||
The runtime design preserves the existing selection rules rather than introducing a second package policy. It covers imports performed by plugin modules as well as Loader row imports and works in the main thread and Harness-owned Workers. Generation replacement accepts only additive package sets and never mutates a live table entry by entry.
|
||||
The runtime design keeps installation-first, ordered-bundle, and local-before-fallback precedence. It covers imports performed by plugin modules as well as Loader row imports and works in the main thread and Harness-owned Workers. Generation replacement accepts only additive package sets and never mutates a live table entry by entry.
|
||||
|
||||
## Decision
|
||||
|
||||
Profile startup computes one immutable `ResolutionGeneration` from the same dependency traversal that supplies the disk module fallback. The launcher defaults to runtime mode, which installs the generation into Node's ESM and CommonJS resolvers without materializing fallback links. Plain Node callers and tests can explicitly select link mode to materialize the generation or dual mode to materialize and verify it. `PluginPackages.replace()` publishes a complete additive successor with one reference replacement.
|
||||
Profile startup computes one immutable `RuntimeResolution` and installs it into Node's ESM and CommonJS resolvers. Runtime is the only resolution backend; there is no mode selector or disk materializer. `PluginPackages.replace()` publishes a complete additive successor with one reference replacement.
|
||||
|
||||
### One selection algorithm
|
||||
|
||||
The package traversal remains in `@deepseek-ai/dsh-app-boot` beside profile loading. The disk materializer and the runtime resolver consume one pure plan; neither owns a copy of the precedence algorithm. Ordinary Node callers can select link, dual, or runtime mode, while an omitted mode selects runtime. Packaged executables and the Electron Host select runtime mode because their dependency trees may live in a virtual filesystem; dual remains an internal comparison path.
|
||||
The package traversal belongs to `@deepseek-ai/dsh-app-boot` beside profile loading. Ordinary Node, source launches, packaged executables, and the Electron Host consume the same runtime resolution and interception.
|
||||
|
||||
The installation manifest is the first root. Its graph traverses `dependencies` followed by `peerDependencies` breadth-first, resolving each edge from the manifest that declares it. The first installed package reached under a name owns that name. Selected bundle roots then run in profile order, with each earlier root's complete graph taking precedence over every later root. Names supplied by the installation are reserved, and bundle package roots themselves do not become plugin fallbacks. Missing declared packages are skipped as before.
|
||||
The installation manifest is the first root. Its graph traverses `dependencies` followed by `peerDependencies` breadth-first, resolving each edge from the manifest that declares it. The first installed package reached under a name owns that name. Selected bundle roots then run in profile order, with each earlier root's complete graph taking precedence over every later root. Names supplied by the installation are reserved, and bundle package roots themselves do not become plugin fallbacks. Missing declared packages are skipped.
|
||||
|
||||
Profile-local and plugin-private `node_modules` entries stay outside the fallback entries, and Node checks them before the virtual fallback position. The generation records only installed direct profile package names for a no-I/O native fast path. Each fallback entry records the package name, version, selected lookup directory, declaring manifest anchor, and scope needed to rerun Node's native resolution from the selected package and validate that a successor preserves existing mappings.
|
||||
During dependency expansion, every installation root, selected bundle root, and recursive dependency manifest uses its package's real directory as the lookup anchor. Ancestor `node_modules` searches therefore follow the location from which Node normally executes the package, not the location of a symlink pointing to it. The same declaring anchor is recorded for runtime delegation. This can change the selected version or remove an otherwise discoverable dependency when logical and real ancestors differ; it is not merely a spelling change to stored paths.
|
||||
|
||||
The existing `healProfilesModuleFallback()` remains as the disk materializer for the same computed result, which permits direct comparison without rewriting the selection rules. Named profile launches use it only in explicit link or dual mode. Runtime mode computes the generation without materializing it, while dual mode materializes and installs that generation for comparison.
|
||||
Profile-local and plugin-private `node_modules` entries take precedence over runtime resolution entries. The runtime resolution records installed direct profile package names for a no-I/O native fast path. Each entry records the package name, version, selected lookup directory, declaring manifest anchor, and scope needed for native resolution and successor validation.
|
||||
|
||||
The profile-scope entries of the runtime resolution remain necessary for dependencies that the profile cannot find through its own ancestor directories. They expand installed manifest dependencies into the active profile's runtime resolution, not a separate graph for each plugin. It neither downloads packages nor scans source imports. Runtime hooks consume that table after native local candidates have been considered.
|
||||
|
||||
### Ordinary and linked package examples
|
||||
|
||||
In both layouts below, the profile selects `my-bundle`, whose manifest declares `bridge`; only `bridge` declares `leaf`. Neither the installation nor the profile's direct packages supplies `leaf`, and `bridge` has no private `node_modules/leaf`. The paths illustrate the two layouts exercised by the [shared CLI profile tests](../../../../apps/cli/tests/profiles/headless/tests/profile-resolution.ts).
|
||||
|
||||
With ordinary installed directories, `bridge` searches its containing bundle's `node_modules` and finds version 1.0.0. The unrelated version 2.0.0 is not on that search path:
|
||||
|
||||
```text
|
||||
/case/home/profiles/headless/node_modules/my-bundle/
|
||||
node_modules/bridge/
|
||||
node_modules/leaf/ # 1.0.0
|
||||
/case/dependencies/node_modules/leaf/ # 2.0.0
|
||||
```
|
||||
|
||||
With an npm-link-style bundle and a linked transitive dependency, the bundle expands from `/case/work/my-bundle`, and the next dependency expansion starts at `/case/dependencies/bridge`. That expansion finds version 2.0.0 beside the real package:
|
||||
|
||||
```text
|
||||
/case/home/profiles/headless/node_modules/my-bundle -> /case/npm-global/node_modules/my-bundle
|
||||
/case/npm-global/node_modules/my-bundle -> /case/work/my-bundle
|
||||
/case/work/my-bundle/node_modules/bridge -> /case/dependencies/bridge
|
||||
/case/work/my-bundle/node_modules/leaf/ # 1.0.0
|
||||
/case/dependencies/node_modules/leaf/ # 2.0.0
|
||||
```
|
||||
|
||||
| Profile package layout | Declaring directory used to resolve `leaf` | `tsx/esm` source launch | Plain Node `lib` launch |
|
||||
|---|---|---|---|
|
||||
| Ordinary directories | `/case/home/profiles/headless/node_modules/my-bundle/node_modules/bridge` | 1.0.0 | 1.0.0 |
|
||||
| Linked bundle and bridge | `/case/dependencies/bridge` | 2.0.0 | 2.0.0 |
|
||||
|
||||
ESM `import` and CommonJS `require` select these versions. Within each module format, importing `leaf` through profile fallback and importing it inside `bridge` reaches the same module instance. A higher-priority native profile package or an installation-reserved fallback name still wins according to the selection rules; the fallback does not force every importer to use version 2.0.0.
|
||||
|
||||
### Source and built module identity
|
||||
|
||||
The [source launcher](2026-07-29-dsh-source-launch-tsx-esm.md) uses tsx's ESM-only hook. tsx skips tsconfig `paths` for an importer URL containing `/node_modules/`. A logical workspace symlink used as a fallback declaring anchor can therefore select built `lib/` exports, while imports from the resulting real workspace files select `src/` through the paths map. Real declaring anchors let workspace imports follow the source map consistently; packages without a matching workspace mapping keep ordinary package-export resolution.
|
||||
|
||||
For example, `@deepseek-ai/dsh-tools` creates its scheduler key with `Symbol()`. A Tools instance loaded from `lib/` cannot expose that scheduler through a key imported from the separate `src/` module instance. Source launches keep Tools and AgentLoop in `src/`; plain Node launches keep them in `lib/`. The scheduler retains its local Symbol; correct imports share one module instance.
|
||||
|
||||
### Immutable generations
|
||||
|
||||
A resolver registration holds one `current` generation. Each synchronous resolution captures that reference once. Generation construction reads every required manifest before publication; an error leaves the current generation unchanged. Successful publication replaces one reference, and in-flight calls may finish against the generation they captured.
|
||||
A runtime interception holds one `current` generation. Each synchronous resolution captures that reference once. Generation construction reads every required manifest before publication; an error leaves the current generation unchanged. Successful publication replaces one reference, and in-flight calls may finish against the generation they captured.
|
||||
|
||||
Selection and package-metadata caches belong to a generation. Publishing a successor invalidates them by making the old generation unreachable after its callers finish; update code does not mutate or clear individual entries. A generation hit and a successful native selection can be cached, but a generation miss is rescanned so a profile-local package installed after the miss becomes visible as it does in link mode. Calls with explicit CommonJS paths or non-default conditions never reuse a default-resolution cache entry.
|
||||
Selection and package-metadata caches belong to a generation. Publishing a successor invalidates them by making the old generation unreachable after its callers finish; update code does not mutate or clear individual entries. A generation hit and a successful native selection can be cached, but a generation miss is rescanned so a profile-local package installed after the miss becomes visible through native lookup. Calls with explicit CommonJS paths or non-default conditions never reuse a default-resolution cache entry.
|
||||
|
||||
The launcher constructs one startup generation. The service accepts an additive successor, but no package-manager transaction invokes replacement in this implementation.
|
||||
|
||||
@@ -36,7 +74,7 @@ The launcher constructs one startup generation. The service accepts an additive
|
||||
|
||||
The resolver uses `node-addon-require-builtin` to read `internal/modules/esm/loader` and `internal/modules/cjs/loader`. The ESM adapter wraps the per-thread singleton `CascadedLoader` resolve methods. The CommonJS adapter wraps the internal builtin's `Module._resolveFilename`; that `Module` is the same object exported by `node:module`.
|
||||
|
||||
Both adapters call one routing function. It ignores builtins, relative or absolute paths, URLs, parents outside the profile scope, and explicit calls outside the supported lookup. A `#imports` request uses Node's mapping from its owning manifest; an external bare target follows the same local, generation, and after-fallback package order with the request's conditions, while Node retains exact target resolution. For a scoped bare request, a package self-reference keeps the original parent even when an npm alias gives its installed directory another name. A profile-local or plugin-private package also keeps the original parent when Node resolves the requested entry before the virtual shared-fallback position; a CommonJS package directory without `exports` does not suppress the fallback when only its requested subpath is absent. Otherwise the router uses a generation hit through that entry's declaring anchor or continues native lookup after the virtual fallback. Explicit CommonJS path lists apply the same insertion rule independently to each path in caller order.
|
||||
Both adapters call one routing function. It ignores builtins, relative or absolute paths, URLs, parents outside the profile scope, and explicit calls outside the supported lookup. A `#imports` request uses Node's mapping from its owning manifest; an external bare target follows the same local, interception, and native-after-interception package order with the request's conditions, while Node retains exact target resolution. For a scoped bare request, a package self-reference keeps the original parent even when an npm alias gives its installed directory another name. A profile-local or plugin-private package also keeps the original parent when Node resolves the requested entry before the virtual shared-fallback position; a CommonJS package directory without `exports` does not suppress the fallback when only its requested subpath is absent. Otherwise the router uses an interception hit through that entry's declaring anchor or continues native lookup after the virtual fallback. Explicit CommonJS path lists apply the same insertion rule independently to each path in caller order.
|
||||
|
||||
The adapters call the captured native resolver after routing. Node remains responsible for exports, import and require conditions, main files, subpaths, extensions, native caches, and error codes. Routed ESM failures replace the internal lookup anchor in Node's diagnostic with the original importer. A selected package's invalid export or missing target does not trigger another same-name candidate. CommonJS does not replace `_findPath` or reproduce `_resolveFilename`.
|
||||
|
||||
@@ -44,11 +82,11 @@ The guarantee covers Node's default `import`, `import()`, `import.meta.resolve`,
|
||||
|
||||
### Active plugin list and package metadata
|
||||
|
||||
The resolution generation lists available fallback packages; Loader entries form the active plugin list. Consumers keep using Loader's existing entry lifecycle and filter the entries relevant to their own scope. Consumers that need package metadata pass a specifier and owning tree base URL to a lightweight `app-boot` service without requiring a `./package.json` export. An installed generation is authoritative, including a miss; a service created without a generation retains native lookup for low-level embedders.
|
||||
The runtime resolution lists the packages it supplies; Loader entries form the active plugin list. Consumers keep using Loader's existing entry lifecycle and filter the entries relevant to their own scope. Consumers that need package metadata pass a specifier and owning tree base URL to a lightweight `app-boot` service without requiring a `./package.json` export. An installed runtime resolution is authoritative, including a miss; a service created without one retains native lookup for low-level embedders.
|
||||
|
||||
The resolver does not expose `imported(entry)` and does not observe ModuleJobs, wrap Entry methods, associate fibers with import calls, replace registry or tree methods, or adapt HMR transactions. A repeated query uses the same generation and therefore cannot drift from the route used for the import. Non-Node importers that need package metadata must explicitly implement the same deterministic resolver interface.
|
||||
|
||||
The implementation lives under `app-boot/src/profile-resolution/`. `service.ts` provides the long-lived `ctx.pluginPackages` and owns the main-thread resolver and Worker-generation lifetimes; `resolver.ts` implements generation lookup and the Node Internal adapters; `worker-bootstrap.ts` installs an inherited generation in one thread. Existing profile selection and disk materialization remain in `profile.ts`. Workers reference the bootstrap only through the public `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap` export.
|
||||
The implementation lives under `app-boot/src/profile-resolution/`. `service.ts` provides the long-lived `ctx.pluginPackages` and owns the main-thread interception and the Worker resolution lifetime; `resolver.ts` implements runtime resolution lookup and the Node Internal adapters; `worker-bootstrap.ts` installs the inherited runtime resolution in one thread. Profile selection and runtime resolution construction remain in `profile.ts`. Workers reference the bootstrap only through the public `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap` export.
|
||||
|
||||
The service definition and provider remain together in `app-boot` because profile boot owns the resolver lifetime. Extracting a separate capability seam becomes warranted when a launcher-independent provider or independently evolving consumers require it.
|
||||
|
||||
@@ -64,21 +102,11 @@ A caller adding a package completes its pnpm transaction before constructing a s
|
||||
|
||||
Replacing, upgrading, or removing an already loaded package requires process restart because Node's ESM Module Map, CommonJS cache, existing object references, and running Workers can retain the old module identity. Generation replacement does not claim to unload modules.
|
||||
|
||||
### Disk migration
|
||||
### Filesystem and runtime carriers
|
||||
|
||||
Runtime-only launch paths do not create, update, or retire symlinks and proxy packages. The resolver treats the legacy shared fallback and `.dsh-module-fallback` projections as virtual insertion positions: a generation hit uses the table target, while a miss skips those old positions before continuing native ancestor lookup. During a dual phase, the launcher materializes and installs the same generation; tests disable each backend in turn and compare their targets.
|
||||
The resolver does not create, update, or remove fallback symlinks and proxy packages. Runtime resolution entries occupy their package names at `$DSH_HOME/profiles/node_modules`; every other name sees that directory as an ordinary ancestor, and symlinks anywhere in the profile tree are ordinary filesystem content without dedicated recognition. Profile-local package metadata and bundle dependency discovery follow the same Node lookup; ordinary pnpm-installed packages retain native precedence. The [lookup-order Note](2026-09-19-profile-resolution-lookup-order.md) records the complete order. Writable profile state and package-manager transactions remain outside the resolver.
|
||||
|
||||
Legacy disk state remains available to link-only launches, old processes, and rollback without participating in runtime-only selection. Removing that state is a separate maintenance operation outside this change.
|
||||
|
||||
### Mode behavior
|
||||
|
||||
Link, dual, and runtime modes use the same generation schema and dependency-selection policy. Link mode persists the computed result, runtime mode installs it only in the process, and dual mode requires Node's materialized result to equal the generation route.
|
||||
|
||||
The `dsh` launcher selects runtime mode when an ordinary Node caller omits `resolutionMode`. A pkg executable always selects runtime mode, and the Electron Host explicitly selects runtime mode in both development and packaged builds before any profile row mounts. Plain Node tests and low-level embedders can explicitly select link, dual, or runtime.
|
||||
|
||||
Runtime mode requires a supported Node Internal loader interface and does not create, update, or retire fallback links. Dual mode retains link writes and fails when Node's disk result differs from the generation. Writable profile state and package-manager transactions remain outside the resolver.
|
||||
|
||||
Pkg and Electron carriers force runtime resolution. The Electron Host runs through the Electron executable with `ELECTRON_RUN_AS_NODE=1`; packaged builds read the dsh tree from ASAR and map executable ASAR entries to electron-builder's unpacked tree. Their runtime resolvers do not create, update, or remove legacy resolution links.
|
||||
Runtime resolution requires a supported Node Internal loader interface. The Electron Host runs through the Electron executable with `ELECTRON_RUN_AS_NODE=1`; packaged builds read the dsh tree from ASAR and map executable ASAR entries to electron-builder's unpacked tree. Pkg and Electron use the same runtime resolution mechanism as ordinary Node launches.
|
||||
|
||||
### Performance and verification
|
||||
|
||||
@@ -86,11 +114,13 @@ Generation construction is startup or update work, not resolve work, and its abs
|
||||
|
||||
One-off local measurements taken during implementation ran built JavaScript under plain Node in fresh processes and compared it with a process that installed no hook. The measurement script and results are not committed, and these figures are not a benchmark or CI budget. Seven alternating rounds covered outside, profile-local, and fallback imports through dynamic import, `import.meta.resolve`, require, and `require.resolve`. Across Node 22.19, 24.18, and 26.8, the largest positive hot-path median was 4.5%. On Node 24.18, a 256-package cold workload regressed by at most 11.2% and generation construction took 16.027 ms median; the 32-package local `require.resolve` case added 1.033 ms across the batch (+34.7%) from fixed startup cost.
|
||||
|
||||
Behavior tests compare the runtime generation with the disk materializer over the same package trees, then exercise root order, transitive and peer dependencies, local and external precedence, exports and subpath errors, conditions, and explicit CommonJS options. The Node compatibility matrix runs the resolver, service, and bootstrap specifications across the supported internal-loader variants. Worker tests verify environment-data publication and bootstrap installation with mocked thread and native-loader interfaces; they do not launch a built Worker. Generation tests prove failed construction does not publish partial state and successful replacement is atomic.
|
||||
Behavior tests exercise root order, transitive and peer dependencies, local and external precedence, exports and subpath errors, conditions, and explicit CommonJS options. The Node compatibility matrix runs the resolver, service, and bootstrap specifications across the supported internal-loader variants. Worker tests verify environment-data publication and bootstrap installation with mocked thread and native-loader interfaces; they do not launch a built Worker. Generation tests prove failed construction does not publish partial state and successful replacement is atomic.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep disk projections permanently.** This preserves native lookup without process hooks, but retains cross-process mutation, stale generations, proxy manifests, writer locks, and packaged-runtime divergence. A bounded dual migration remains useful because both backends consume the same generation.
|
||||
**Keep disk projections permanently.** This preserves native lookup without process hooks, but retains cross-process mutation, stale generations, proxy manifests, writer locks, and packaged-runtime divergence. Retaining link and dual as comparison modes would also keep the materializer and its lifecycle without a supported launcher that needs them.
|
||||
|
||||
**Preserve logical symlink anchors.** This keeps lookup under the symlink's ancestors, but can select a different dependency from the one imported by the real package and can suppress tsx's workspace paths map. Canonical anchors keep eager dependency discovery and runtime delegation aligned with the package's execution location.
|
||||
|
||||
**Expand the dependency graph lazily during resolve.** This spreads manifest reads and errors across first-use calls, changes timing from the disk implementation, complicates Worker startup, and makes the hot path depend on graph size. Complete generation construction is easier to compare and replace atomically.
|
||||
|
||||
@@ -104,15 +134,15 @@ Behavior tests compare the runtime generation with the disk materializer over th
|
||||
|
||||
## Verification
|
||||
|
||||
- One eager computation supplies the retained disk materializer and runtime generation.
|
||||
- Link-only, dual, and runtime-only tests consume the same generation; runtime startup neither writes nor retires module-resolution data.
|
||||
- One eager computation supplies the runtime resolution; startup neither writes nor retires module-resolution data.
|
||||
- [Generation tests](../../../../packages/boot/app-boot/tests/profile-resolution.spec.ts) cover installation and selected-bundle graphs with ordinary directories and recursive symlinks, including different dependency versions beside logical and real anchors.
|
||||
- [Source-launch tests](../../../../apps/cli/tests/source-launch.compat.spec.ts) and [built-bin tests](../../../../apps/cli/tests/built-bin.e2e.ts) run both profile layouts through the real CLI. They assert ESM/CJS versions, loaded paths, per-format dependency identity, and consistent Tools/AgentLoop module instances with an accessible scheduler key.
|
||||
- Pkg and Electron carriers select runtime resolution; Electron executes its Host in Node mode from the ASAR-backed dsh tree while native executable entries remain unpacked.
|
||||
- ESM and CommonJS adapters share one router and delegate final resolution to Node without `module.registerHooks` or `_findPath` replacement.
|
||||
- Production metadata lookup does not record Loader import results or wrap Entry, registry, tree, or HMR methods.
|
||||
- The Node compatibility matrix runs main-thread resolver specifications across supported loader interfaces; service and bootstrap specifications cover Worker environment-data and installation interfaces without launching a built Worker.
|
||||
- One-off built plain-Node measurements produced the hot and cold observations above against no-hook Node; the script and results are not committed evidence.
|
||||
- Package READMEs, architecture references, generated catalogs, and the bilingual pair describe the shipped implementation.
|
||||
|
||||
## Consequences
|
||||
|
||||
Runtime startup avoids disk mutation and proxy manifests while preserving the existing package-selection algorithm. It accepts the maintenance cost of Node Internal compatibility tests and an early, self-contained bootstrap in each owned Worker. Runtime is the ordinary Node launcher default, link and dual remain explicit comparison options, and pkg plus Electron carriers force runtime resolution without the resolver retiring old links. Generation replacement remains additive until the product owns module-cache invalidation and Worker restart.
|
||||
Runtime startup avoids disk mutation and proxy manifests while retaining package-precedence rules. Real-directory anchors align dependency discovery with default Node loading and tsx workspace mapping, including cases where a logical symlink path would select another version. The implementation accepts the maintenance cost of Node Internal compatibility tests and an early, self-contained bootstrap in each owned Worker; it provides no disk-only backend or dual comparison mode. Generation replacement remains additive until the product owns module-cache invalidation and Worker restart.
|
||||
+61
-31
@@ -6,29 +6,67 @@ Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
profile 从自己的包项目加载插件配置项,而 Harness 包和所选 bundle 携带的包可能位于该项目普通依赖树之外。通过共享 symlink、profile 自有链接或打包可执行文件的代理包连接两棵依赖树,会让选包结果跨进程和安装版本持续存在。这些文件需要协调和锁来维护,并向元数据读取方暴露生成的代理 manifest,也无法原子表示进程内变更。
|
||||
profile 从自己的包项目加载插件配置项,而 Harness 包和所选 bundle 携带的包可能位于该项目普通依赖树之外。通过共享 symlink、profile 自有链接或打包可执行文件的代理包连接两棵依赖树,会让选包结果跨进程和安装版本持续存在。这些文件需要协调和锁来维护,并向元数据读取方暴露生成的代理 manifest(元数据清单),也无法原子表示进程内变更。
|
||||
|
||||
运行时设计保留现有选包规则,不另建一套包策略。它覆盖插件模块内部的 import 以及 Loader 配置项的 import,并在主线程和 Harness 自有 Worker 中工作。generation 替换只接受新增包的集合,不会逐项修改正在使用的表。
|
||||
运行时设计保留安装优先、有序 bundle 和本地包优先于 fallback 的顺序。它覆盖插件模块内部的 import 以及 Loader 配置项的 import,并在主线程和 Harness 自有 Worker 中工作。generation 替换只接受新增包的集合,不会逐项修改正在使用的表。
|
||||
|
||||
## Decision
|
||||
|
||||
profile 启动从磁盘 module fallback 使用的同一套依赖遍历生成一个不可变 `ResolutionGeneration`。launcher 默认使用 runtime 模式,把 generation 安装到 Node 的 ESM 与 CommonJS 解析器,不物化 fallback 链接。普通 Node 调用方和测试可以显式选择 link 模式以物化 generation,或选择 dual 模式以物化并校验它。`PluginPackages.replace()` 通过一次引用替换发布完整的新增型后继 generation。
|
||||
profile 启动生成一个不可变 `RuntimeResolution`,并将其安装到 Node 的 ESM 与 CommonJS 解析器。runtime 是唯一的解析后端,不提供模式选择器或磁盘物化器。`PluginPackages.replace()` 通过一次引用替换发布完整的新增型后继 generation。
|
||||
|
||||
### 唯一选包算法
|
||||
|
||||
包遍历继续放在 `@deepseek-ai/dsh-app-boot` 的 profile 加载代码旁。磁盘 materializer 和运行时解析器消费同一个纯计划;两者都不持有另一份优先级算法。普通 Node 调用方可以选择 link、dual 或 runtime 模式,省略模式时使用 runtime。打包可执行文件与 Electron Host 会选择 runtime,因为其依赖树可能位于虚拟文件系统;dual 保留为内部对比路径。
|
||||
包遍历属于 `@deepseek-ai/dsh-app-boot`,与 profile 加载代码放在一起。普通 Node、源码启动、打包可执行文件和 Electron Host 消费相同的 runtime resolution 与拦截。
|
||||
|
||||
安装 manifest 是第一个根。它按 BFS 依次遍历 `dependencies` 和 `peerDependencies`,每条边从声明它的 manifest 解析,同名包由第一次找到的已安装包占有。所选 bundle 随后按 profile 顺序逐根遍历;每个较早根的完整依赖图优先于所有较晚根。安装闭包中的名称被保留,bundle 包根本身不成为插件 fallback。与旧行为相同,已声明但未安装的包会被跳过。
|
||||
安装 manifest 是第一个根。它按 BFS 依次遍历 `dependencies` 和 `peerDependencies`,每条边从声明它的 manifest 解析,同名包由第一次找到的已安装包占有。所选 bundle 随后按 profile 顺序逐根遍历;每个较早根的完整依赖图优先于所有较晚根。安装闭包中的名称被保留,bundle 包根本身不成为插件 fallback。已声明但未安装的包会被跳过。
|
||||
|
||||
profile 本地和插件私有 `node_modules` 不进入 fallback entries,由 Node 在虚拟 fallback 位置之前选择。generation 只记录已安装的 profile 直接包名用于 native 快速分流;每个 fallback 记录包名、版本、旧规则选中的查找目录、声明该边的 manifest 锚点和作用域,足以从选定包重新进入 Node 原生解析并验证换代保持既有映射。
|
||||
依赖展开时,每个安装根、所选 bundle 根和递归依赖 manifest 都以所属包的真实目录作为查找锚点。因此,祖先 `node_modules` 查找沿 Node 通常执行该包的位置进行,而不是沿指向该包的软链接位置进行。同一声明锚点会被记录下来,供运行时委托解析使用。逻辑路径与真实路径的祖先不同时,这可能改变所选版本,也可能使原本能发现的依赖不再被收录;它不只是对已存路径换一种写法。
|
||||
|
||||
旧 `healProfilesModuleFallback()` 保留为相同纯计算结果的磁盘 materializer,便于直接比较并避免重写旧规则。具名 profile 启动只在显式 link 或 dual 模式下调用它。runtime 模式只计算 generation 而不物化,dual 模式会物化并安装该 generation 进行比较。
|
||||
profile 本地和插件私有 `node_modules` 条目优先于 runtime resolution 条目。runtime resolution 记录已安装的 profile 直接包名,用于无 I/O 的原生快速分流。每个条目记录包名、版本、选定的查找目录、声明该边的 manifest 锚点和作用域,供原生解析与后继 generation 校验使用。
|
||||
|
||||
对于 profile 无法通过自身祖先目录找到的依赖,runtime resolution 的 profile 作用域条目仍然必要。它们将已安装 manifest 中的依赖展开到当前 profile 的 runtime resolution,而不是为每个插件建立独立依赖图。它既不下载包,也不扫描源码 import。运行时钩子在考虑原生本地候选后使用这张表。
|
||||
|
||||
### 普通安装与软链接示例
|
||||
|
||||
下面两种布局中,profile 都选中 `my-bundle`,其 manifest 声明 `bridge`;只有 `bridge` 声明 `leaf`。安装包集合和 profile 的直接包都不提供 `leaf`,`bridge` 也没有私有 `node_modules/leaf`。这些路径示意了[共享 CLI(命令行界面)profile 测试](../../../../apps/cli/tests/profiles/headless/tests/profile-resolution.ts)覆盖的两种布局。
|
||||
|
||||
普通安装目录中,`bridge` 向上查找所属 bundle 的 `node_modules`,找到版本 1.0.0。另一个版本 2.0.0 不在这条查找路径上:
|
||||
|
||||
```text
|
||||
/case/home/profiles/headless/node_modules/my-bundle/
|
||||
node_modules/bridge/
|
||||
node_modules/leaf/ # 1.0.0
|
||||
/case/dependencies/node_modules/leaf/ # 2.0.0
|
||||
```
|
||||
|
||||
bundle 采用 npm-link 风格链接,且其传递依赖也被软链接时,bundle 从 `/case/work/my-bundle` 展开,下一层依赖则从 `/case/dependencies/bridge` 开始展开。该次展开找到真实包旁边的版本 2.0.0:
|
||||
|
||||
```text
|
||||
/case/home/profiles/headless/node_modules/my-bundle -> /case/npm-global/node_modules/my-bundle
|
||||
/case/npm-global/node_modules/my-bundle -> /case/work/my-bundle
|
||||
/case/work/my-bundle/node_modules/bridge -> /case/dependencies/bridge
|
||||
/case/work/my-bundle/node_modules/leaf/ # 1.0.0
|
||||
/case/dependencies/node_modules/leaf/ # 2.0.0
|
||||
```
|
||||
|
||||
| Profile 包布局 | 解析 `leaf` 时使用的声明目录 | `tsx/esm` 源码启动 | 普通 Node `lib` 启动 |
|
||||
|---|---|---|---|
|
||||
| 普通目录 | `/case/home/profiles/headless/node_modules/my-bundle/node_modules/bridge` | 1.0.0 | 1.0.0 |
|
||||
| bundle 与 bridge 均为软链接 | `/case/dependencies/bridge` | 2.0.0 | 2.0.0 |
|
||||
|
||||
ESM `import` 和 CommonJS `require` 都选中这些版本。在每种模块格式内部,通过 profile fallback 导入 `leaf` 与在 `bridge` 内部导入它,会得到同一个模块实例。优先级更高的原生 profile 包,或安装集合已保留的同名 fallback,仍按选包规则优先;fallback 不会强制所有导入方都使用版本 2.0.0。
|
||||
|
||||
### 源码与构建产物的模块身份
|
||||
|
||||
[源码启动器](2026-07-29-dsh-source-launch-tsx-esm.zh.md)使用 tsx 的 ESM-only 钩子。导入方 URL 含有 `/node_modules/` 时,tsx 会跳过 tsconfig `paths`。因此,把 workspace 软链接的逻辑路径用作 fallback 声明锚点,可能选中构建后的 `lib/` 导出,而这些模块在真实 workspace 路径中发起的 import 又通过 paths 映射选中 `src/`。真实声明锚点让 workspace import 一致遵循源码映射;没有匹配 workspace 映射的包仍使用普通包导出解析。
|
||||
|
||||
例如,`@deepseek-ai/dsh-tools` 使用 `Symbol()` 创建 scheduler 键。从 `lib/` 加载的 Tools 实例,无法通过另一个 `src/` 模块实例导入的键暴露该 scheduler。源码启动让 Tools 与 AgentLoop 都位于 `src/`;普通 Node 启动让两者都位于 `lib/`。scheduler 保留模块本地的 Symbol;正确的 import 共享同一个模块实例。
|
||||
|
||||
### 不可变 generation
|
||||
|
||||
一个解析器 registration 持有一个 `current` generation。每个同步 resolve 在入口只捕获一次该引用,完整调用只读该引用。generation 构造在发布前读取所有必需 manifest;失败时当前 generation 不变。发布成功只替换一个引用,执行中的调用可以继续使用它已捕获的 generation。
|
||||
一个 runtime interception 持有一个 `current` generation。每个同步 resolve 在入口只捕获一次该引用,完整调用只读该引用。generation 构造在发布前读取所有必需 manifest;失败时当前 generation 不变。发布成功只替换一个引用,执行中的调用可以继续使用它已捕获的 generation。
|
||||
|
||||
选包缓存和包元数据缓存归 generation 所有。发布下一代后,旧 generation 在调用方退出后自然不可达,不逐项清理缓存。generation 命中和原生解析成功结果可以缓存,但 generation 未命中会重新扫描,因此未命中后安装的 profile 本地包会像 link 模式一样变为可见。显式 CommonJS paths 或非默认 conditions 不得复用默认解析缓存。
|
||||
选包缓存和包元数据缓存归 generation 所有。发布下一代后,旧 generation 在调用方退出后自然不可达,不逐项清理缓存。generation 命中和原生解析成功结果可以缓存,但 generation 未命中会重新扫描,因此未命中后安装的 profile 本地包会通过原生查找变为可见。显式 CommonJS paths 或非默认 conditions 不得复用默认解析缓存。
|
||||
|
||||
launcher 只构造启动 generation。服务接受新增型后继 generation,但本实现没有包管理器事务调用替换操作。
|
||||
|
||||
@@ -36,7 +74,7 @@ launcher 只构造启动 generation。服务接受新增型后继 generation,
|
||||
|
||||
resolver 使用 `node-addon-require-builtin` 读取 `internal/modules/esm/loader` 和 `internal/modules/cjs/loader`。ESM 适配器包装每线程单例 `CascadedLoader` 的 resolve 方法。CommonJS 适配器包装内部 builtin 导出的 `Module._resolveFilename`;该 `Module` 与 `node:module` 导出的对象相同。
|
||||
|
||||
两个适配器调用同一个路由函数。builtin、相对或绝对路径、URL、profile 作用域外 parent 和支持的查找以外的显式调用都直接委托原生实现。`#imports` 请求使用所属 manifest 中的 Node 映射;外部 bare target 按相同 conditions 遵循本地包、generation 和 after-fallback 的选包顺序,精确 target 解析仍由 Node 负责。对于作用域内的 bare request,package self-reference 保留原 parent,即使 npm alias 使安装目录使用另一个名称。Node 能在虚拟共享 fallback 之前从 profile 本地包或插件私有包解析到所请求入口时,也保留原 parent;没有 `exports` 的 CommonJS 包目录仅缺少所请求 subpath 时,不会压过 fallback。其他请求在 generation 命中时通过该条目的声明锚点解析,未命中时从虚拟 fallback 之后继续原生查找。显式 CommonJS path 列表按调用方顺序,对每个 path 独立应用相同的插入规则。
|
||||
两个适配器调用同一个路由函数。builtin、相对或绝对路径、URL、profile 作用域外 parent 和支持的查找以外的显式调用都直接委托原生实现。`#imports` 请求使用所属 manifest 中的 Node 映射;外部 bare target 按相同 conditions 遵循本地包、interception 和 native-after-interception 的选包顺序,精确 target 解析仍由 Node 负责。对于作用域内的 bare request,package self-reference 保留原 parent,即使 npm alias 使安装目录使用另一个名称。Node 能在虚拟共享 fallback 之前从 profile 本地包或插件私有包解析到所请求入口时,也保留原 parent;没有 `exports` 的 CommonJS 包目录仅缺少所请求 subpath 时,不会压过 fallback。其他请求在 interception 命中时通过该条目的声明锚点解析,未命中时从虚拟 fallback 之后继续原生查找。显式 CommonJS path 列表按调用方顺序,对每个 path 独立应用相同的插入规则。
|
||||
|
||||
适配器完成路由后调用捕获的原生 resolver。exports、import/require conditions、main、subpath、扩展名、原生缓存和错误码仍归 Node 处理。路由后的 ESM 失败会把 Node 诊断中的内部查找锚点替换为原始 importer。选中包的无效 export 或缺失目标不会触发另一个同名候选。CommonJS 不替换 `_findPath`,也不复制 `_resolveFilename`。
|
||||
|
||||
@@ -44,11 +82,11 @@ resolver 使用 `node-addon-require-builtin` 读取 `internal/modules/esm/loader
|
||||
|
||||
### 活动插件列表与包元数据
|
||||
|
||||
resolution generation 列出可用 fallback 包;Loader entries 组成活动插件列表,两者不能合并。消费方继续使用 Loader 原有 entry 生命周期,并按自身 scope 过滤相关 entries。需要 package metadata 的消费方将 specifier 和所属树的 base URL 交给 app-boot 中的轻量服务,无需 package 导出 `./package.json`。安装 generation 后,即使查询未命中也以 generation 为准;底层嵌入方只安装服务而不提供 generation 时,服务保留 Node 原生查找。
|
||||
runtime resolution 列出它提供的包;Loader entries 组成活动插件列表,两者不能合并。消费方继续使用 Loader 原有 entry 生命周期,并按自身 scope 过滤相关 entries。需要 package metadata 的消费方将 specifier 和所属树的 base URL 交给 app-boot 中的轻量服务,无需 package 导出 `./package.json`。安装 runtime resolution 后,即使查询未命中也以它为准;底层嵌入方只安装服务而不提供 runtime resolution 时,服务保留 Node 原生查找。
|
||||
|
||||
解析器不提供 `imported(entry)`,不观察 ModuleJob,不包装 Entry 方法,不把 fiber 与 import 调用关联,也不替换 registry、tree 或 HMR 方法。重复查询读取同一个 generation,因此不会偏离 import 使用的路线。需要包元数据的非 Node importer 必须显式实现同一个确定性 resolver 接口,不能把调用来源推断重新引入 Node 主路径。
|
||||
|
||||
实现集中在 `app-boot/src/profile-resolution/`。`service.ts` 提供长期存在的 `ctx.pluginPackages`,并拥有主线程 resolver 与 Worker generation 的生命周期;`resolver.ts` 实现 generation 查询和 Node Internal 适配器;`worker-bootstrap.ts` 在线程内安装继承的 generation。旧 profile 选包和磁盘 materialize 逻辑留在 `profile.ts`。Worker 只通过 `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap` 公开入口引用 bootstrap。
|
||||
实现集中在 `app-boot/src/profile-resolution/`。`service.ts` 提供长期存在的 `ctx.pluginPackages`,并拥有主线程拦截与 Worker runtime resolution 的生命周期;`resolver.ts` 实现 runtime resolution 查询和 Node Internal 适配器;`worker-bootstrap.ts` 在线程内安装继承的 runtime resolution。profile 选包和 runtime resolution 构造留在 `profile.ts`。Worker 只通过 `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap` 公开入口引用 bootstrap。
|
||||
|
||||
服务定义与提供方继续放在 `app-boot`,因为 profile boot 拥有 resolver 生命周期。出现与 launcher 无关的提供方或需要独立演进的消费方时,再抽出单独的能力 seam。
|
||||
|
||||
@@ -64,21 +102,11 @@ resolution generation 列出可用 fallback 包;Loader entries 组成活动插
|
||||
|
||||
替换、升级或删除已加载包需要重启,因为 Node 的 ESM Module Map、CommonJS cache、现存对象引用和运行中的 Worker 都可能保留旧模块 identity。generation 换代不声称卸载模块。
|
||||
|
||||
### 磁盘迁移
|
||||
### 文件系统与运行时载体
|
||||
|
||||
runtime-only 启动流程不创建、更新或退休 symlink 和代理包。resolver 把旧共享 fallback 和 `.dsh-module-fallback` 投影视为虚拟插入位置:generation 命中时使用表中目标,未命中时越过旧位置继续原生祖先查找。dual 阶段物化并安装同一个 generation;测试分别禁用一个后端并比较目标。
|
||||
解析器不创建、更新或删除 fallback 软链接与代理包。runtime resolution 条目占据 `$DSH_HOME/profiles/node_modules` 上各自的包名位置;其余包名把该目录当作普通祖先,profile 树内任何位置的软链接都是普通文件系统内容,没有专门识别。profile 本地包元数据与 bundle 依赖展开遵循同一套 Node 查找;正常由 pnpm 安装的包保留原生优先级。完整顺序见[查找顺序 Note](2026-09-19-profile-resolution-lookup-order.zh.md)。可写 profile 状态和包管理器事务不属于解析器。
|
||||
|
||||
旧磁盘状态继续供 link-only 启动、旧进程和回滚使用,但不参与 runtime-only 选择。清理旧链接是本次变更之外的独立维护操作。
|
||||
|
||||
### 模式行为
|
||||
|
||||
link、dual 与 runtime 模式使用同一种 generation schema 和依赖选择策略。link 模式持久化计算结果,runtime 模式只在进程内安装,dual 模式要求 Node 的磁盘结果与 generation 路由一致。
|
||||
|
||||
普通 Node 调用方省略 `resolutionMode` 时,`dsh` launcher 选择 runtime 模式。pkg 可执行文件始终选择 runtime,Electron Host 在开发与打包构建中也会在挂载任何 profile 条目前显式选择 runtime。普通 Node 测试与底层嵌入方可以显式选择 link、dual 或 runtime。
|
||||
|
||||
runtime 模式要求受支持的 Node Internal loader 接口,并且不会创建、更新或退休 fallback 链接。dual 模式保留链接写入,并在 Node 的磁盘结果与 generation 不同时失败。可写 profile 状态和包管理器事务不属于 resolver。
|
||||
|
||||
pkg 与 Electron 载体强制使用 runtime 解析。Electron Host 通过设置 `ELECTRON_RUN_AS_NODE=1` 的 Electron 可执行文件运行;打包构建从 ASAR 读取 dsh 依赖树,并把 ASAR 中的可执行条目映射到 electron-builder 的 unpacked 目录。它们的运行时解析器不会创建、更新或删除旧解析链接。
|
||||
运行时解析要求受支持的 Node Internal loader 接口。Electron Host 通过设置 `ELECTRON_RUN_AS_NODE=1` 的 Electron 可执行文件运行;打包构建从 ASAR 读取 dsh 依赖树,并把 ASAR 中的可执行条目映射到 electron-builder 的 unpacked 目录。pkg 与 Electron 使用和普通 Node 启动相同的 runtime resolution 机制。
|
||||
|
||||
### 性能与验证
|
||||
|
||||
@@ -86,11 +114,13 @@ generation 构造发生在启动或显式更新阶段,不属于单次 resolve
|
||||
|
||||
实现期间的一次性本地测量用 plain Node 在全新进程中执行构建后的 JavaScript,并与完全没有安装 hook 的进程比较。测量脚本和结果未提交,这些数据不是 benchmark 或 CI 预算。七轮交替顺序覆盖 outside、profile-local 和 fallback 的 dynamic import、`import.meta.resolve`、require、`require.resolve`。Node 22.19、24.18 和 26.8 的热路径中位数最大正向回退为 4.5%。Node 24.18 的 256 包 cold workload 最大回退为 11.2%,generation 构造中位数为 16.027 ms;32 包本地 `require.resolve` 因固定启动成本在整批增加 1.033 ms(+34.7%)。
|
||||
|
||||
行为测试在同一包树上比较运行时 generation 与磁盘 materializer,再覆盖根顺序、传递依赖和 peer、本地与外层优先级、exports 与 subpath 错误、conditions 和显式 CommonJS options。Node 兼容矩阵会在受支持的内部 loader 变体上运行 resolver、service 和 bootstrap 规格。Worker 测试通过 mock 线程与 native loader 接口验证 environment data 发布和 bootstrap 安装,但不会启动构建后的 Worker。generation 测试证明构造失败不发布部分状态,成功换代只做原子引用替换。
|
||||
行为测试覆盖根顺序、传递依赖和 peer、本地与外层优先级、exports 与 subpath 错误、conditions 和显式 CommonJS options。Node 兼容矩阵会在受支持的内部 loader 变体上运行解析器、服务和 bootstrap 规格。Worker 测试通过 mock 线程与原生 loader 接口验证 environment data 发布和 bootstrap 安装,但不会启动构建后的 Worker。generation 测试证明构造失败不发布部分状态,成功换代只做原子引用替换。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**永久保留磁盘投影。** 这能在没有进程 hook 时沿用原生查找,但仍有跨进程写入、陈旧 generation、代理 manifest、写锁和打包运行时差异。迁移期 dual 模式仍有价值,因为两个后端消费同一个 generation。
|
||||
**永久保留磁盘投影。** 这能在没有进程钩子时沿用原生查找,但仍有跨进程写入、陈旧 generation、代理 manifest、写锁和打包运行时差异。把 link 与 dual 保留为对比模式,也会留下没有受支持启动器需要的物化器及其生命周期。
|
||||
|
||||
**保留软链接逻辑锚点。** 这会沿软链接的祖先查找,但可能选中与真实包内部 import 不同的依赖,也可能抑制 tsx 的 workspace paths 映射。规范化锚点让 eager 依赖发现与运行时委托解析都对齐包的执行位置。
|
||||
|
||||
**在 resolve 时惰性扩展依赖图。** 这会把 manifest 读取和错误分散到首次使用,改变磁盘实现的时机,使 Worker 启动更复杂,并让热路径成本随依赖图变化。完整构造 generation 更容易比较和原子替换。
|
||||
|
||||
@@ -104,15 +134,15 @@ generation 构造发生在启动或显式更新阶段,不属于单次 resolve
|
||||
|
||||
## Verification
|
||||
|
||||
- 一次 eager 计算同时供应保留的磁盘 materializer 和运行时 generation。
|
||||
- link-only、dual 和 runtime-only 测试消费同一个 generation;runtime 启动既不写入也不退休模块解析数据。
|
||||
- 一次 eager 计算供应 runtime resolution;启动既不写入也不退休模块解析数据。
|
||||
- [Generation 测试](../../../../packages/boot/app-boot/tests/profile-resolution.spec.ts)覆盖普通目录和递归软链接下的安装图与所选 bundle 图,包括逻辑锚点和真实锚点旁存在不同依赖版本的情况。
|
||||
- [源码启动测试](../../../../apps/cli/tests/source-launch.compat.spec.ts)与[构建入口测试](../../../../apps/cli/tests/built-bin.e2e.ts)通过真实 CLI 运行两种 profile 布局,断言 ESM/CJS 版本、加载路径、各模块格式内的依赖身份,以及一致的 Tools/AgentLoop 模块实例和可访问的 scheduler 键。
|
||||
- pkg 与 Electron 载体选择 runtime 解析;Electron 以 Node 模式从 ASAR 承载的 dsh 依赖树执行 Host,原生可执行条目保持 unpacked。
|
||||
- ESM 与 CommonJS 适配器共享同一个路由器,并把最终解析委托给 Node,不使用 `module.registerHooks` 或替换 `_findPath`。
|
||||
- 生产 package metadata 查询不记录 Loader import 结果,也不包装 Entry、registry、tree 或 HMR 方法。
|
||||
- Node 兼容矩阵会在受支持的 loader 接口上运行主线程 resolver 规格;service 和 bootstrap 规格覆盖 Worker environment data 与安装接口,但不会启动构建后的 Worker。
|
||||
- 一次性 plain Node 构建产物测量得到上述相对无 hook Node 的热路径和 cold 观察结果;脚本与结果并未提交为证据。
|
||||
- package README、架构引用、生成目录和双语文档对描述已交付实现。
|
||||
|
||||
## Consequences
|
||||
|
||||
runtime 启动避免磁盘修改和代理 manifest,同时保留既有选包算法。代价是持续维护 Node Internal 兼容测试,并在每个自有 Worker 中最早执行自包含 bootstrap。runtime 是普通 Node launcher 的默认值,link 与 dual 保留为显式对比选项;pkg 与 Electron 载体强制使用 runtime,解析器不退休旧链接。在产品拥有模块缓存失效和 Worker 重启前,generation 替换只能新增映射。
|
||||
runtime 启动避免磁盘修改和代理 manifest,同时保留包优先级规则。真实目录锚点让依赖发现对齐 Node 默认加载与 tsx workspace 映射,也覆盖软链接逻辑路径会选中另一版本的情况。实现需要持续维护 Node Internal 兼容测试,并在每个自有 Worker 中尽早执行自包含 bootstrap;它不提供纯磁盘后端或 dual 对比模式。在产品拥有模块缓存失效和 Worker 重启前,generation 替换只能新增映射。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.md
|
||||
2026-09-10-desktop-web-wrapper.md: 05210e2ab85f0afe6d0275e25f31ce8addf11332
|
||||
2026-09-10-desktop-web-wrapper.zh.md: 84e72c9261250b103245f974e1a0546a4898e9ca
|
||||
2026-09-10-desktop-web-wrapper.md: 4f43cc650777596c807cf79463edc7a9131db44b
|
||||
2026-09-10-desktop-web-wrapper.zh.md: bc41b4f8eff1dca7fc3dd9b934dac257a9d10a6b
|
||||
@@ -14,7 +14,7 @@ Separate Desktop composition and request transport require their own configurati
|
||||
|
||||
The private Desktop Host invokes the CLI's shared profile runner against the independently owned Desktop profile. The complete Web composition owns authentication, HTTP routes, client assets, RPC, and response streaming. Electron loads packaged static Web assets before the child is ready. Child IPC carries readiness, structured boot injections, and shutdown. The [immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-start.md) owns local-document HTTP forwarding and authenticated WebSocket access; Web retains application dispatch and stream framing.
|
||||
|
||||
The shared runner owns profile and Harness-home patches, proxy setup, telemetry defaults, module fallbacks, configuration reload, and application lifecycle. Desktop initializes profiles from the shared Web template and uses its Plugin Manager in the main application. Electron owns windows, menus, native directory selection, recovery, and release updates.
|
||||
The shared runner owns profile and Harness-home patches, proxy setup, telemetry defaults, the runtime resolution, configuration reload, and application lifecycle. Desktop initializes profiles from the shared Web template and uses its Plugin Manager in the main application. Electron owns windows, menus, native directory selection, recovery, and release updates.
|
||||
|
||||
The [bundled-runtime decision](2026-09-08-desktop-bundled-runtime-and-external-plugins.md) retains separate runtime and plugin storage, bundled pnpm and explicit package ownership. The public CLI continues to reject the reserved Desktop profile. Electron profile preparation and native recovery remain available without a working Host.
|
||||
|
||||
@@ -22,7 +22,7 @@ Independent package ownership prevents CLI and Desktop from modifying each other
|
||||
|
||||
The shared Web plugin manager owns installation, activation, errors, and restart requirements. Electron has no separate plugin-management renderer, preload, shell asset route, plugin IPC, or package-mutation runner. Packaging explicitly selects the main entry and application preload so stale build outputs cannot restore the removed bridge.
|
||||
|
||||
Shared `initProfile` creates missing profile files and preserves existing content. The Host’s `healIsolatedProfileModuleFallback` is the sole owner of installation and bundle projections; package operations use shared `unlinkProfileModuleFallback` to detach only its own links before pnpm. pnpm-managed directories retain priority. Desktop maintains no second runtime-state, lockfile hash, or link reconciliation mechanism. One-time cleanup of `desktop-runtime-state.json` removes only matching recorded links and retires that metadata.
|
||||
Shared `initProfile` creates missing profile files and preserves existing content. The Host computes its runtime resolution with `createRuntimeResolution` and installs it through `PluginPackages`, without writing fallback links. pnpm-managed directories retain priority. Desktop maintains no second runtime-state, lockfile hash, or link reconciliation mechanism.
|
||||
|
||||
This partially supersedes the private composition and portless transport in the [packaging decision](2026-08-25-electron-desktop-packaging-and-updates.md). That design avoided listening ports and used framed byte pipes to avoid Base64 expansion and cross-version V8 serialization. Shared HTTP gives up the portless guarantee and assigns serving and authentication to the existing Web implementation. Release identity, signing, process ownership, and native shell features remain active decisions.
|
||||
|
||||
@@ -36,7 +36,7 @@ Native directory selection in the local application uses a narrow preload IPC ca
|
||||
|
||||
**Merge CLI and Desktop plugin installations.** Shared boot code does not require shared executable dependencies. Separate installations allow independently qualified releases and plugin versions while their existing data owners govern shared sessions and settings.
|
||||
|
||||
**Keep a Desktop link ledger and manifest reconciler.** These duplicate shared profile mechanisms and can reject otherwise usable installations when derived metadata drifts. A single fallback owner can protect pnpm directories without maintaining release identity in the plugin profile.
|
||||
**Keep a Desktop link ledger and manifest reconciler.** These duplicate shared profile mechanisms and can reject otherwise usable installations when derived metadata drifts. A single owner of the runtime resolution can protect pnpm directories without maintaining release identity in the plugin profile.
|
||||
|
||||
**Keep a separate native plugin-management page.** It can operate while the Web Host is unavailable, but duplicates package operations, renderer IPC, localization, and backend restart handling. Native recovery already supports disabling third-party bundles and backing up the profile patch without the Host. Reintroduction requires a repair operation that this recovery cannot provide.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
私有 Desktop Host 针对独立归属的 Desktop profile 调用 CLI 的共享 profile runner。完整 Web 组合负责认证、HTTP 路由、客户端资源、RPC 与响应流。Electron 在子进程就绪前加载打包静态 Web 资源。子进程 IPC 承载就绪、结构化启动注入与关闭。[立即显示窗口决策](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)规定本地文档 HTTP 转发与认证 WebSocket 访问;Web 保留应用分派与流帧处理。
|
||||
|
||||
共享 runner 负责 profile 与 Harness-home patch、代理设置、遥测默认值、模块补全、配置重载和应用生命周期。Desktop 从共享 Web 模板初始化 profile,并在主应用中使用其插件管理器。Electron 负责窗口、菜单、原生目录选择、恢复和发布更新。
|
||||
共享 runner 负责 profile 与 Harness-home patch、代理设置、遥测默认值、runtime resolution、配置重载和应用生命周期。Desktop 从共享 Web 模板初始化 profile,并在主应用中使用其插件管理器。Electron 负责窗口、菜单、原生目录选择、恢复和发布更新。
|
||||
|
||||
[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)保留独立运行时与插件存储、内置 pnpm,以及明确的包归属。公开 CLI 继续拒绝保留的 Desktop profile。Electron profile 准备和原生恢复在 Host 不可用时仍可执行。
|
||||
|
||||
@@ -22,7 +22,7 @@ Status: implemented
|
||||
|
||||
共享 Web 插件管理器负责安装、激活、错误和重启要求。Electron 不提供独立插件管理渲染器、preload、shell 资源路由、插件 IPC 或包变更执行器。打包明确选择主入口和应用 preload,避免旧构建产物重新带入已删除的桥接。
|
||||
|
||||
共享 `initProfile` 创建缺失的 profile 文件并保留现有内容。Host 的 `healIsolatedProfileModuleFallback` 是安装包与 bundle 投影的唯一归属方;包操作在 pnpm 前通过共享 `unlinkProfileModuleFallback` 仅分离它自己拥有的链接。pnpm 管理的目录保持优先。Desktop 不维护第二套运行时状态、锁文件哈希或链接协调机制。`desktop-runtime-state.json` 的一次性清理仅移除与记录匹配的链接,并清除该元数据。
|
||||
共享 `initProfile` 创建缺失的 profile 文件并保留现有内容。Host 通过 `createRuntimeResolution` 计算 runtime resolution,并通过 `PluginPackages` 安装它,不写入 fallback 链接。pnpm 管理的目录保持优先。Desktop 不维护第二套运行时状态、锁文件哈希或链接协调机制。
|
||||
|
||||
本记录部分取代[打包决策](2026-08-25-electron-desktop-packaging-and-updates.zh.md)中的私有组合与无端口传输。该设计避免监听端口,并使用分帧字节管道避免 Base64 膨胀与跨版本 V8 序列化。共享 HTTP 放弃无端口保证,将服务与认证交给已有 Web 实现。发布身份、签名、进程归属及原生壳功能仍是有效决策。
|
||||
|
||||
@@ -36,7 +36,7 @@ Status: implemented
|
||||
|
||||
**合并 CLI 与 Desktop 插件安装。** 共享启动代码不要求共享可执行依赖。独立安装允许分别验收发布与插件版本,共享会话和设置则仍由已有数据归属方负责。
|
||||
|
||||
**保留 Desktop 链接账本与 manifest 协调器。** 这些机制重复共享 profile 逻辑,并可能因派生元数据漂移而拒绝原本可用的安装。单一模块补全归属方可以保护 pnpm 目录,无需在插件 profile 中维护发布身份。
|
||||
**保留 Desktop 链接账本与 manifest 协调器。** 这些机制重复共享 profile 逻辑,并可能因派生元数据漂移而拒绝原本可用的安装。单一 runtime resolution 归属方可以保护 pnpm 目录,无需在插件 profile 中维护发布身份。
|
||||
|
||||
**保留独立原生插件管理页。** 它可以在 Web Host 不可用时运行,但重复维护包操作、渲染器 IPC、本地化和后端重启处理。原生恢复已能在没有 Host 时禁用第三方 bundle 并备份 profile 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/architecture/2026-09-10-image-offload-events.md
|
||||
2026-09-10-image-offload-events.md: f540dc98cee8daf3e360b14234a2ac21d0f53aab
|
||||
2026-09-10-image-offload-events.zh.md: 868fca59ba5fc1941de752127f61b091aedb234f
|
||||
2026-09-10-image-offload-events.md: e80c4ae17b148d9eb46517b92111a8d768160def
|
||||
2026-09-10-image-offload-events.zh.md: 2aa2b8b1fbffd117b5d716aba28755a02a155bb8
|
||||
@@ -14,7 +14,7 @@ A request can exceed an image-capable route's byte or image-count budget. Recomp
|
||||
|
||||
Summary requests bypass the agent error waterfall. `dsh-compaction-basic` preserves the full `LlmFailure`, checks cancellation and selection stability, and dispatches synchronous `compaction/summary-error` with the selected event seqs. The same image-offload plugin records omissions only within that selection. Synchronous recovery keeps the stability check and decision adjacent. The backend then re-derives input and pricing, including the shrink baseline. Each retry requires additional omitted occurrences; exhaustion delegates the failure. A later summary failure or cancellation preserves any recorded omissions, so command errors do not claim the conversation is unchanged.
|
||||
|
||||
The event payload is `{ targets: [{ seq, imageIndexes }] }`. Each target identifies a current `user/message` or `tool/result` node. Its strictly increasing, zero-based indexes enumerate every image in depth-first content order, including nested tool results and previously omitted images. Equal attachment ids remain distinct occurrences. Exact selections remain unambiguous after positional replacements reorder the surface; an attachment id or a raw-log prefix cannot identify the selected set.
|
||||
The event payload is `{ targets: [{ seq, imageIndexes }] }`. Each target identifies a current `user/message` or `tool/result` node. Its strictly increasing, zero-based indexes enumerate every image in depth-first content order, including tool-role result content and previously omitted images. Equal attachment ids remain distinct occurrences. Exact selections remain unambiguous after positional replacements reorder the surface; an attachment id or a raw-log prefix cannot identify the selected set.
|
||||
|
||||
The event has no `surfaceOp`. It neither creates a message nor replaces a message node. The plugin's pure projection validates all targets before Session commits the event and rejects missing or shadowed nodes, output images, duplicate targets, invalid indexes, and already omitted occurrences. Reconstruction derives immutable message copies with `ImageBlock.offloaded: true` at the recorded positions. Original events, message ids, sources, and unaffected blocks remain unchanged. Resume, fork, and replay apply the same selections from the log.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
摘要请求不经过 agent 错误 waterfall。`dsh-compaction-basic` 保留完整的 `LlmFailure`,检查取消和选区稳定性,再携带所选事件序号触发同步的 `compaction/summary-error`。同一个图片省略插件只在选区内记录省略。同步恢复使稳定性检查与决策相邻执行。后端随后重新派生输入并计价,包括判断摘要是否缩短输入的基准。每次重试都必须继续省略图片,无图可省略时委派失败。后续摘要失败或取消仍保留已经记录的省略,因此命令错误不声称会话未变。
|
||||
|
||||
事件载荷为 `{ targets: [{ seq, imageIndexes }] }`。每个目标指向当前的 `user/message` 或 `tool/result` 节点。索引从零开始且严格递增,按内容深度优先顺序枚举所有图片,包括嵌套工具结果和先前省略的图片。相同附件 ID 的多次出现分别计数。位置替换改变 surface 顺序后,明确选择仍没有歧义,附件 ID 或原始日志前缀无法标识这个集合。
|
||||
事件载荷为 `{ targets: [{ seq, imageIndexes }] }`。每个目标指向当前的 `user/message` 或 `tool/result` 节点。索引从零开始且严格递增,按内容深度优先顺序枚举所有图片,包括 tool-role 结果内容和先前省略的图片。相同附件 ID 的多次出现分别计数。位置替换改变 surface 顺序后,明确选择仍没有歧义,附件 ID 或原始日志前缀无法标识这个集合。
|
||||
|
||||
事件不带 `surfaceOp`,不创建消息,也不替换消息节点。插件的纯投影在 Session 提交事件前校验全部目标,拒绝缺失或已被遮蔽的节点、输出图片、重复目标、无效索引和已被省略的位置。重建过程派生不可变消息副本,将记录的位置标为 `ImageBlock.offloaded: true`。原始事件、消息 ID、来源和未受影响的内容块保持不变。恢复、分叉和回放从日志应用相同的选择。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-10-public-package-manifest.md
|
||||
2026-09-10-public-package-manifest.md: 0e6c1650a6b04e98aeba1aa6ef45df78e3f2cb44
|
||||
2026-09-10-public-package-manifest.zh.md: 9b7d15f479ca1aab7bea655b2dee43625728a442
|
||||
2026-09-10-public-package-manifest.md: 2d35a3ad17c5aa62df4a5eccde351f325936a84f
|
||||
2026-09-10-public-package-manifest.zh.md: 7dc03e5160b3837ea07e19469b1d8a0a4f64ebe6
|
||||
@@ -14,7 +14,7 @@ Plugin authors need npm identity, runtime requirements, and DSH declarations fro
|
||||
|
||||
Runtime requirements live at top-level `engines`: `dsh`, `node`, and `npm` are optional version strings, and other engine names are allowed. `dsh.manifestVersion` identifies declaration format `1`. Format and DSH compatibility declarations are not enforced by current installers or loaders.
|
||||
|
||||
The image packer owns `configTrees`, the workspace catalog generator owns Session migration declarations, and app-boot owns generated module-fallback metadata. Their existing on-disk keys remain readable by those internal tools, but the public manifest types do not expose them. This scope refines the [shared declaration ownership decision](2026-09-05-package-manifest-types.md), whose package placement and dependency rules remain active.
|
||||
The image packer owns `configTrees`, and the workspace catalog generator owns Session migration declarations. Their existing on-disk keys remain readable by those internal tools, but the public manifest types do not expose them. This scope refines the [shared declaration ownership decision](2026-09-05-package-manifest-types.md), whose package placement and dependency rules remain active.
|
||||
|
||||
Each consumer owns JSON parsing, field validation, default resolution, and adaptation to runtime data. Interfaces do not validate parsed JSON. A helper belongs in the shared package only when multiple consumers need the same validation or normalization; getters that repeat property access add no shared policy.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
运行时要求位于顶层 `engines`:`dsh`、`node` 和 `npm` 均为可选版本字符串,也允许其他 engine 名称。`dsh.manifestVersion` 标识声明格式 `1`。当前安装器和加载器不强制检查格式与 DSH 兼容性声明。
|
||||
|
||||
镜像打包器拥有 `configTrees`,工作区目录生成器拥有 Session 迁移声明,app-boot 拥有生成的模块后备元数据。这些内部工具仍可读取既有磁盘字段,但公共 manifest 类型不暴露这些字段。此范围细化了[共享声明归属决策](2026-09-05-package-manifest-types.zh.md),后者的包位置与依赖规则仍然有效。
|
||||
镜像打包器拥有 `configTrees`,工作区目录生成器拥有 Session 迁移声明。这些内部工具仍可读取既有磁盘字段,但公共 manifest 类型不暴露这些字段。此范围细化了[共享声明归属决策](2026-09-05-package-manifest-types.zh.md),后者的包位置与依赖规则仍然有效。
|
||||
|
||||
各消费方负责 JSON 解析、字段校验、默认值解析和运行时数据适配。接口不会校验已解析的 JSON。只有多个消费方需要相同校验或归一化时,helper 才属于共享包;重复属性访问的 getter 不提供共享策略。
|
||||
|
||||
|
||||
+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-14-web-document-relative-app-routes.md
|
||||
2026-09-14-web-document-relative-app-routes.md: fbf30253bbfcc23eb09847f93c1b1f047459d31b
|
||||
2026-09-14-web-document-relative-app-routes.zh.md: c302ec88a4205239148132513d1ff2a4385033e8
|
||||
@@ -0,0 +1,47 @@
|
||||
# Agent Note: Web app-owned routes are document-relative
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-14-web-document-relative-app-routes.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Every browser reference to a shell-owned route was origin-root absolute: `/api/...` RPC, `/plugins/??...` plugin bundles, the Remote stream mux, the HMR event stream, and the launch entry's token-cleanup redirect to `/`. One listener served behind a prefix-stripping proxy (a mount such as `https://host/tools/dsh/` that forwards `/tools/dsh/...` as `/...`) therefore saw those requests at the origin root, where no route answers them: the shell, its plugin bundles, and its streams all missed. A single bundle has to serve both the origin root and any mount without a second build.
|
||||
|
||||
The mount cannot be recovered from the transport. A configured base URL makes it deployment input, forwarded-prefix inference makes a request header authoritative for routing, and response rewriting in the proxy reimplements the same fact in fragments. The one place that already knows the mount is the served document itself.
|
||||
|
||||
## Decision
|
||||
|
||||
The served HTTP shell owns its document base. Its index carries exactly one `<base href="./">`, spliced in after the opening head tag once the index taps have run, so it precedes the boot manifest, every plugin-resource row, and any markup a tap inserted. The base freezes the entry directory: `/mount/` and `/mount/index.html` are the supported entries, and a document URL that later moves (an in-page `pushState`) does not move the base. Nothing infers a prefix, so deep direct loads and reloads are not entry points; a bare mount still needs the proxy's own normalization to its slash form.
|
||||
|
||||
Browser references to those routes are relative to that document — `api/x`, `plugins/x` — while server route keys stay absolute pathnames: the RPC channel key `/api`, the `webServer` registration paths, and the response tables keyed by the combo route `/plugins/??...&rev=...`. A shared constant that names a key (`*_PATH`, `*_ENDPOINT`) is stripped at the boundary (`KEY.slice(1)`) before browser code uses it, and the browser form lives beside it as `*_ROUTE`.
|
||||
|
||||
The Host composes browser references in one place per producer. Module graph rows and batch descriptors carry the document-relative combo reference; the source-map trailer carries the combo query alone, because a script's source-map reference resolves against that script's own directory (`.../plugins/`) rather than the document. The response table stays keyed by the absolute route, which is also what a request reaches after the proxy strips its prefix.
|
||||
|
||||
The launch entry follows the same rule. `authenticatedUrl` adds the process token to the caller's URL and keeps its authority and mount, and the token exchange redirects to `./`, which removes the query token while preserving the request URL's directory. The Host/Origin fence and the cookie rules are unchanged.
|
||||
|
||||
Consumers that need an absolute URL resolve against `document.baseURI`: the Gateway when it constructs its WebSocket URL and selects `ws:` or `wss:`, and the Inspector when it locates its own bundle from the boot graph.
|
||||
|
||||
Desktop is the other document owner. Its window loads `dsh-app://app/`, an origin that serves the same dist at that root and forwards every app-owned path to the shell-owned Host, so its document directory already is the root and no base row is injected for it; it names that Host origin for the Gateway through the transport's `streamBaseUrl` instead. Its resources live at that root, which is also why the served HTTP shell's `./` does not apply to it.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**A configured base URL as the routing base (`publicUrl`).** Rejected: the document already knows its own URL, so this adds a second, drift-prone source for a fact the page can read, and it would have to be plumbed to every consumer instead of one shell decision. The advertised public URL remains an operator-facing print/open input, not a routing input.
|
||||
|
||||
**Inferring the mount from a forwarded-prefix header.** Rejected: it makes a request header authoritative for routing, so any client that sets it could move app-owned routes, and the proxy contract becomes server-visible configuration.
|
||||
|
||||
**A shared `Connection.resolveUrl` or routes base.** The HTTP carrier's document base and the worker tunnel's Host-root mapping differ. Resolving every target in Connection would prepend a static preview path to worker-local routes or lose a served mount. Each carrier resolves its own targets; UI resource consumers use the document base.
|
||||
|
||||
**An absolute `/` redirect with the proxy rewriting `Location`.** Rejected: the browser already knows the request's directory, and a configured redirect target cannot be right for both the loopback entry and a proxy mount.
|
||||
|
||||
## Consequences
|
||||
|
||||
One bundle serves the origin root and any mount, and the served document is the single source of the mount. The costs are explicit: the supported entries are only the dist root and the configured index path, a bare mount depends on the proxy's slash normalization, and a reload of a deep in-page path re-enters at that path rather than the frozen one.
|
||||
|
||||
Routes owned by feature packages (open-in-app, deliverables, session-log export, uploads, markdown media) follow the same rule through their own `*_ROUTE` constants; the static gate that keeps browser sources on this rule is recorded with those changes.
|
||||
|
||||
## Testing
|
||||
|
||||
The frontend-static real-composition test serves the index through the Loader and asserts one `<base href="./">` that precedes an injected plugin-resource row and tap markup. The client-modules node-half suite asserts that graph rows, batch descriptors, and the source-map trailer are document-relative while the response table still serves them by route, and the client test roster plus the assembled-boot fixture carry the same relative form. `browser-auth.host.spec.ts` covers the `./` redirect and the authority- and mount-preserving `authenticatedUrl`; `frontend-static.spec.ts` and `apps/cli/tests/web-auth.e2e.ts` run the exchange through a real Loader composition and the built CLI.
|
||||
|
||||
`apps/web/tests/public-mount.e2e.ts` is the browser proof: a real Chromium reaches one listener through the prefix-stripping proxy, the mounted WebSocket streams both ways, a `pushState` to a deeper path leaves `document.baseURI` at the mount, and a document-relative `fetch('api/session/create')` still succeeds beneath it.
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
# Agent Note: Web 应用自有路由相对文档寻址
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-14-web-document-relative-app-routes.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
浏览器对外壳自有路由的每一处引用都以源站根绝对形式书写:`/api/...` RPC、`/plugins/??...` 插件 bundle、Remote 流 mux、HMR 事件流,以及启动入口清理 token 时重定向到的 `/`。当一个监听端口位于剥离前缀的代理之后(例如 `https://host/tools/dsh/` 这样的挂载,把 `/tools/dsh/...` 转发为 `/...`),这些请求会落在源站根,那里没有任何路由应答:外壳、插件 bundle、流全部落空。同一份 bundle 必须同时服务源站根与任一挂载,而不能为此再构建一次。
|
||||
|
||||
挂载点无法从传输层恢复。配置 base URL 会把它变成部署输入,转发前缀推断会让请求头对路由具有权威性,代理内改写在碎片里重复同一事实。唯一已经知道挂载点的地方,就是所服务的文档自身。
|
||||
|
||||
## 决策
|
||||
|
||||
所服务的 HTTP 外壳拥有自己的文档 base。其 index 携带唯一的 `<base href="./">`,在原始 index 转换执行后拼接于起始 head 标签之后,因此它先于启动 manifest、每一行插件资源,以及任何转换插入的标记。该 base 冻结入口目录:`/mount/` 与 `/mount/index.html` 是受支持的入口,而文档 URL 之后移动(页面内 `pushState`)不会移动 base。系统不推断任何前缀,因此深层直接加载与重新加载不是入口;裸挂载仍需依赖代理自身的规范化补齐尾部斜杠。
|
||||
|
||||
浏览器对这些路由的引用相对该文档——`api/x`、`plugins/x`——而服务端路由键保持绝对路径名:RPC channel 键 `/api`、`webServer` 注册路径,以及以 combo 路由 `/plugins/??...&rev=...` 为键的响应表。命名路由键的共享常量(`*_PATH`、`*_ENDPOINT`)在边界处被剥去前导斜杠(`KEY.slice(1)`)后浏览器代码才使用,浏览器形式以 `*_ROUTE` 与之并列。
|
||||
|
||||
宿主在每个生产者里只在一处组合浏览器引用。模块启动图 row 与批次描述符携带文档相对的 combo 引用;source-map trailer 只携带 combo 查询,因为脚本的 source-map 引用相对该脚本自身目录(`.../plugins/`)解析,而非相对文档。响应表仍以绝对路由为键,而请求在代理剥去前缀后到达的也正是该路由。
|
||||
|
||||
启动入口遵循同一规则。`authenticatedUrl` 把进程 token 加到调用方的 URL 上并保留其 authority 与挂载;token 交换重定向到 `./`,移除 query token 的同时保留请求 URL 的目录。Host/Origin 栅栏与 cookie 规则不变。
|
||||
|
||||
需要绝对 URL 的消费方相对 `document.baseURI` 解析:Gateway 构造 WebSocket URL 并选择 `ws:` 或 `wss:` 时,以及 Inspector 从启动图定位自身 bundle 时。
|
||||
|
||||
Desktop 是另一个文档所有者。它的窗口加载 `dsh-app://app/`:该来源在根目录提供同一份 dist,并把其他所有应用路径转发给它拥有的 Host,因此它的文档目录本就是根目录,无需为它注入 base 行;它改为通过传输层的 `streamBaseUrl` 为 Gateway 指定该 Host origin。它的资源就位于该根目录,这也是所服务 HTTP 外壳的 `./` 不适用于它的原因。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**把配置 base URL 当作路由 base(`publicUrl`)。** 否决:文档已经知道自己的 URL,因此这是为页面本可读取的事实引入第二个易漂移来源,而且必须把它接入每个消费方,而不是只做一次外壳决策。对外公告的公开 URL 仍是面向运维的打印/打开输入,不是路由输入。
|
||||
|
||||
**从转发前缀请求头推断挂载点。** 否决:这会让请求头对路由具有权威性,任何设置该头的客户端都能挪动应用自有路由,代理约定也会变成服务端可见的配置。
|
||||
|
||||
**共享 `Connection.resolveUrl` 或 routes base。** HTTP 载体的文档基准与 worker tunnel 的 Host 根路径映射不同。在 Connection 中统一解析所有目标,会把静态 preview 路径加到 worker 本地路由之前,或丢失所服务的挂载。各载体解析自己的目标;UI 资源消费方使用文档基准。
|
||||
|
||||
**保留绝对 `/` 重定向并让代理改写 `Location`。** 否决:浏览器已经知道请求所在目录,而配置出来的重定向目标无法同时适用于 loopback 入口与代理挂载。
|
||||
|
||||
## 后果
|
||||
|
||||
一份 bundle 服务源站根与任一挂载,所服务的文档是挂载点的唯一来源。代价是显式的:受支持的入口只有 dist 根与配置的 index 路径;裸挂载依赖代理的斜杠规范化;页面内深层路径重新加载会在该路径重新进入,而非冻结目录。
|
||||
|
||||
功能包自有的路由(open-in-app、deliverables、会话日志导出、上传、markdown 媒体)通过各自的 `*_ROUTE` 常量遵循同一规则;让浏览器源码持续遵守该规则的静态门禁随那些改动一并记录。
|
||||
|
||||
## 测试
|
||||
|
||||
frontend-static 的真实组合测试通过 Loader 提供 index,并断言唯一的 `<base href="./">` 先于注入的插件资源行与转换标记。client-modules node 半侧测试断言启动图 row、批次描述符与 source-map trailer 均为文档相对,而响应表仍按路由提供它们,客户端测试 roster 与 assembled-boot fixture 携带同一相对形式。`browser-auth.host.spec.ts` 覆盖 `./` 重定向与保留 authority 和挂载的 `authenticatedUrl`;`frontend-static.spec.ts` 与 `apps/cli/tests/web-auth.e2e.ts` 分别经真实 Loader 组合与构建后的 CLI 运行该交换。
|
||||
|
||||
`apps/web/tests/public-mount.e2e.ts` 是浏览器证据:真实 Chromium 经剥离前缀的代理到达同一监听端口,挂载下的 WebSocket 双向成流,`pushState` 到更深路径后 `document.baseURI` 仍停在挂载点,而文档相对的 `fetch('api/session/create')` 在其下依然成功。
|
||||
+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: 2d727809b237ca919296dac82a3b201ba2dd8fbf
|
||||
2026-09-15-client-session-references.zh.md: 34fa6526a57f43fd7636c68c7ed72bb982a129f2
|
||||
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.
|
||||
|
||||
@@ -101,7 +101,7 @@ Source counts remain authoritative in `SessionListState.byId[id].retainedBy`; UI
|
||||
|
||||
Pending domains retain `SessionPendingInteractionMap`, request identities, precedence, publication disposers, and teardown delegation. The unified status includes the same effective request object; it does not copy requests or create a second pending registry. Workspace status indicators and Conversation composer selection read `useSessionStatus` instead of separate `useSessionPendingInteraction` and `useCompletedSessionIds` hooks.
|
||||
|
||||
Completion tracking subscribes to the existing `api-session/status` events so a running-to-idle transition is not lost in batched catalog snapshots. Catalog snapshots establish initial and reconnect baselines. A pending empty catalog is not evidence that Sessions disappeared. The update rules are:
|
||||
Completion tracking subscribes to the existing `api-session/status` events so a running-to-idle transition is not lost in batched catalog snapshots. Host-list rows (`ids`) establish initial and reconnect running baselines; synthetic catalog and retained rows preserve independently observed status. A pending empty catalog is not evidence that Sessions disappeared. The update rules are:
|
||||
|
||||
- An initial idle baseline does not create a completion reminder.
|
||||
- Observing running clears an earlier reminder and records the running baseline.
|
||||
@@ -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 内封闭的枚举。
|
||||
|
||||
@@ -101,7 +101,7 @@ Controller 将来源计数与每个活跃代的引用共同管理。每个来源
|
||||
|
||||
待处理领域保留 `SessionPendingInteractionMap`、请求身份、优先级、发布清理函数与卸载委托。统一状态包含同一个有效请求对象,不复制请求,也不创建第二套待处理注册表。Workspace 状态指示器与 Conversation composer 选择读取 `useSessionStatus`,不再分别读取 `useSessionPendingInteraction` 和 `useCompletedSessionIds` 钩子。
|
||||
|
||||
完成跟踪订阅已有 `api-session/status` 事件,避免 running 到 idle 的变化在合批目录快照中丢失。目录快照建立初始与重连基线。pending 状态下的空目录不能证明 Session 已消失。更新规则如下:
|
||||
完成跟踪订阅已有 `api-session/status` 事件,避免 running 到 idle 的变化在合批目录快照中丢失。Host 列表行(`ids`)建立初始与重连的 running 基线;目录合成行与 retained 行保留独立观察到的状态。pending 状态下的空目录不能证明 Session 已消失。更新规则如下:
|
||||
|
||||
- 初始 idle 基线不产生完成提醒。
|
||||
- 观察到 running 时清除旧提醒,并记录运行基线。
|
||||
@@ -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-15-first-class-tool-role-messages.md
|
||||
2026-09-15-first-class-tool-role-messages.md: 3749ad698890073634b1753d3370f9c9dc9a2e18
|
||||
2026-09-15-first-class-tool-role-messages.zh.md: c97d84a7b3ceaee17ebb6fe828bd739dba5fb0ea
|
||||
@@ -0,0 +1,28 @@
|
||||
# Agent Note: First-class tool-role messages
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-15-first-class-tool-role-messages.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The released Session format stores tool results inside a user-role `tool-result` content block, while the model conversation and provider protocols treat a tool result as a separate role. Keeping both descriptions active makes validation, replay, and adapter projections disagree.
|
||||
|
||||
## Decision
|
||||
|
||||
Session format V4 stores one tool result as a first-class `role: 'tool'` message. Its `toolCallId`, tool source call id, result content, and optional error flag are validated together. The V3-to-V4 edge lifts exactly one released wrapper and refuses nested wrappers before publication, retaining the source generation. Current V4 decoding is native-only; wrapper rows are accepted only by the adjacent migration edge.
|
||||
|
||||
Historical message and result extension fields keep distinct `plugin:message:` and `plugin:result:` names when their wrapper is removed. Complete original field names remain in each suffix, preventing collisions without adding core metadata fields. Opaque retention avoids inventing a shared field owner when the wrapper disappears. It retains duplicate values from distinct owners, and plugin consumers must use the V4 names. The wrapper alone supplies the interpreted call id and error flag; an old outer field with the same spelling remains opaque. The [conversion specification](../../../../packages/session/session-format-v3-to-v4/README.md#tool-results) owns exact preservation and refusal rules.
|
||||
|
||||
The conversation role map is closed. A model-visible role must have a persisted Session event and an adapter projection, so adapters do not silently turn an unlogged extension role into user text. Provider-specific source kinds remain merge-extensible and are owned by their producers.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Keep wrapper-shaped V4 rows** — this preserves the released payload but leaves the canonical role and provider wire formats inconsistent.
|
||||
- **Accept both wrapper and native V4 rows** — dual current representations make encoding, decoding, recovery, and replay depend on which path produced the artifact.
|
||||
- **Flatten historical nested results** — leaf text does not retain nested call IDs or error status, and providers did not interpret nested content uniformly. Unsupported migration preserves the original data until a lossless conversion is available.
|
||||
- **Keep role extensions open** — an extension could reach a model request without a Session event and could not be reconstructed on replay.
|
||||
|
||||
## Consequences
|
||||
|
||||
The adjacent migration changes the logical message role without changing message identity, tool-call identity, or result content. Native V4 semantic failures are hard admission failures; recoverable decoding still applies to released V3 canonical tails. Adapters map the first-class role to their provider-specific tool-result representation. The duplicated message `toolCallId` and source `callId` remain as an explicit equality-checked provider field and source-identity field pair.
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# Agent Note:一等 tool-role 消息
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-15-first-class-tool-role-messages.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
已发布的 Session 格式把工具结果存储在 user-role 的 `tool-result` 内容块中,而模型对话与提供方协议把工具结果视为独立角色。两种描述同时存在会使校验、回放和适配器投影互相不一致。
|
||||
|
||||
## 决策
|
||||
|
||||
Session format V4 将一次工具结果存储为一等 `role: 'tool'` 消息。它的 `toolCallId`、工具来源调用标识、结果内容和可选错误标记一起校验。V3 到 V4 的边缘迁移只提升一个已发布的包装,并在发布前拒绝嵌套包装,保留源代际。当前 V4 解码仅接受原生形式;包装行只由相邻迁移边缘接受。
|
||||
|
||||
移除历史 wrapper 时,消息与结果的扩展字段分别保留为 `plugin:message:` 和 `plugin:result:` 名称。后缀保留完整原字段名,避免冲突且不添加核心 metadata 字段。不透明保留避免在 wrapper 消失时虚构共同字段 owner。不同 owner 的重复值仍分别保留,插件消费者必须使用 V4 名称。只有 wrapper 提供具有解释语义的调用 id 和错误标志;同名的旧外层字段保持不透明。[转换规范](../../../../packages/session/session-format-v3-to-v4/README.zh.md#tool-results)拥有精确的保留与拒绝规则。
|
||||
|
||||
对话角色 map 是封闭的。模型可见角色必须有持久 Session 事件和适配器投影,因此适配器不会把未记录的扩展角色静默转换为 user 文本。提供方特有的来源 kind 仍然可合并扩展,并由各生产方拥有。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **保留包装形式的 V4 行**——保留已发布载荷,但规范角色与提供方 wire 格式仍不一致。
|
||||
- **同时接受包装和原生 V4 行**——两种当前表示会让编码、解码、恢复和回放依赖于产出 artifact 的路径。
|
||||
- **展平历史嵌套结果**——叶子文本无法保留嵌套调用 ID 或错误状态,而且提供方对嵌套内容的解释并不一致。拒绝不支持的迁移会保留原始数据,等待能够保留全部信息的转换。
|
||||
- **保持角色扩展开放**——扩展角色可能在没有 Session 事件的情况下到达模型请求,也无法在回放时重建。
|
||||
|
||||
## 后果
|
||||
|
||||
相邻迁移会改变逻辑消息角色,但不改变消息身份、工具调用身份或结果内容。原生 V4 语义失败属于硬准入失败;可恢复解码仍适用于已发布 V3 的规范尾部。适配器把一等角色映射为各提供方自己的工具结果表示。消息 `toolCallId` 与来源 `callId` 的重复仍作为一对显式且经过相等性校验的提供方字段与来源字段保留。
|
||||
+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-16-plugin-configuration-on-the-plugins-page.md
|
||||
2026-09-16-plugin-configuration-on-the-plugins-page.md: fb330bee44373d20c7aeea8cee521bf8def025d4
|
||||
2026-09-16-plugin-configuration-on-the-plugins-page.zh.md: 65c1f5a8bf4654b395ab4bfcf8015ae7c23a6acc
|
||||
2026-09-16-plugin-configuration-on-the-plugins-page.md: 475580af48257f8c9f1d080617de1e672b74b028
|
||||
2026-09-16-plugin-configuration-on-the-plugins-page.zh.md: 22d0aa4ff1e4d027ebfc3e019047c7d5e94497d7
|
||||
+9
-7
@@ -10,21 +10,23 @@ A plugin's settings lived in Settings, on the Plugins section's configuration ta
|
||||
|
||||
## Decision
|
||||
|
||||
**The Plugins page hosts configuration; Settings keeps the inventory.** The page declares three slots as children of its `main` entry. `plugins.item` (list) lists an official plugin in the Official group by its `label`. `plugins.bundle.config` (keyed by the bundle's package name) renders on the bundle's page between its description and its rows. `plugins.row.config` (keyed by `<package name>#<row id>`) gives that row a configure control that opens a page headed by the row id. Every entry is rendered in two views the page passes as owner props: `summary` for the one-liner under the title, `page` for the form. The page draws the title, icon, and crumb, projects the three ledgers into one observable (`configLedgerSource`) beside its store, and never names a configurable plugin.
|
||||
**The Plugins page hosts configuration; Settings keeps the inventory.** The page declares three slots as children of its `main` entry. `plugins.item` (list) lists an official plugin in the Official group by its `label`. `plugins.bundle.config` (keyed by the bundle's package name) renders on the bundle's page between its description and its rows. `plugins.row.config` (keyed by `<package name>#<row id>`) gives that row a configure control that opens its own page. The [plugin metadata decision](2026-09-18-localized-package-metadata.md) owns that page's title and description; its slot key and technical identities do not change with the displayed text.
|
||||
|
||||
The page passes `view: 'page'` for each form. Official plugin cards and details also use `view: 'summary'`; a row's detail page uses it only when the plugin has no display description. Bundle configuration renders only `page`. The page draws the title, icon, and crumb, projects the three ledgers into one observable (`configLedgerSource`) beside its store, and never names a configurable plugin. Sidebar navigation and installation-request ownership follow the [sidebar management decision](2026-09-09-plugin-management-in-the-web-sidebar.md).
|
||||
|
||||
**Attribution is declared by the registrant, in the slot name and key.** No manifest field, no registration metadata, and no owner information from the settings service: an official page registers without a bundle, a bundle's page with its package name, a row's page with the bundle and the row id its patch declares. A community bundle that ships no browser half has no configuration page; the page renders no generic form from a settings schema.
|
||||
|
||||
**Only a save writes.** The form's discard control and its unsaved marker are gone: leaving a page drops the staged edits, which the form does on unmount. A save still writes through the client settings scope with the revision fence.
|
||||
**Only a save writes.** The form has no discard control or unsaved marker; leaving a page drops its staged edits on unmount. A save writes through the client settings scope with the revision fence.
|
||||
|
||||
**The four host-plane pages register from `ui-settings-plugins` while the Host serves their namespaces.** The package keeps the Settings section as the **Built-in plugins** shell around the inventory tab and registers each page through `ctx.slots.inject` when the shared settings mirror shows the namespace, disposing it when the namespace goes, so a deployment that does not compose the owning plugin shows no trace of it. The `settings.plugin.item` slot is retired.
|
||||
|
||||
**The Official group.** The optional bundles the installation ships open the group, tagged **Beta** where the feature is one (Agent Teams), with no official tag; the configuration pages follow. Auto review leaves `OPTIONAL_BUNDLES` and the CLI's dependencies: it is a published experimental package the install guide names as its example.
|
||||
**The Official group.** The optional bundles the installation ships open the group, tagged **Beta** where the feature is one (Agent Teams), with no official tag; the configuration pages follow. Auto review is a published experimental package used as the install guide's example, not an `OPTIONAL_BUNDLES` entry or a CLI dependency.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A bundle's browser half registers a form with one slot registration and its own dictionary; the bundle's patch must declare the row under the id in the key, and the registration exists while the row that carries the bundle's browser half is on: `dsh-client-modules` attaches that half to the row whose specifier is the bare package name, so a row-level page keyed to a subpath row disappears with the root row, not with its own.
|
||||
- The four pages, their forms, and the settings write path are unchanged; `ui-settings-plugins` keeps its name for the section it still owns while its pages live on the Plugins page.
|
||||
- Settings lists the inventory only; the settings goldens that carried the Plugins nav entry and the configuration tab were re-recorded.
|
||||
- The four pages use the existing forms and settings write path; `ui-settings-plugins` owns the Settings section while its configuration pages live on the Plugins page.
|
||||
- Settings lists the inventory only; the Settings browser goldens cover that read-only section.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -32,8 +34,8 @@ A plugin's settings lived in Settings, on the Plugins section's configuration ta
|
||||
|
||||
**A generic form from the settings schema.** `describe()` already ships each namespace's schemastery schema, so a community namespace without a browser half could get a default form. Deferred: the official pages are curated, a generic form would expose internal namespaces without an allowlist, and attributing a namespace to a bundle needs an owner in the descriptor.
|
||||
|
||||
**Keeping the cards in Settings and linking to them from the page.** Cheapest, but the review asked for the form on the plugin's own page.
|
||||
**Keeping the cards in Settings and linking to them from the page.** This separates a plugin's description and configuration across different pages.
|
||||
|
||||
## Testing
|
||||
|
||||
`ui-plugin-manager` unit tests cover the ledger projection, the Official group's cards and pages, a bundle's form, and a row's page; `ui-settings-plugins` tests cover registration per served namespace and its withdrawal, and the form's save-only behavior. The `plugin-config` web lane edits the shell page through the real wire and opens a community fixture's row page after switching its bundle on; the `plugin-manager` lane records the new Official group.
|
||||
`ui-plugin-manager` unit tests cover the ledger projection, the Official group's cards and pages, a bundle's form, and a row's page, including description precedence over its summary; `ui-settings-plugins` tests cover registration per served namespace and its withdrawal, and the form's save-only behavior. The `plugin-config` web lane edits the shell page through the real wire and opens a community fixture's row page after switching its bundle on; the `plugin-manager` lane records the Official group.
|
||||
+11
-9
@@ -1,6 +1,6 @@
|
||||
# Agent Note:插件页上的插件配置
|
||||
# Agent Note: 插件页上的插件配置
|
||||
|
||||
状态:已实现
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-16-plugin-configuration-on-the-plugins-page.md) | 中文
|
||||
|
||||
@@ -10,21 +10,23 @@
|
||||
|
||||
## 决策
|
||||
|
||||
**插件页承载配置;设置只保留清单。** 页面在其 `main` 条目下声明三个子 slot。`plugins.item`(list)按 `label` 把官方插件列在官方分组里。`plugins.bundle.config`(以组合包的包名为键)渲染在组合包页面的描述与行之间。`plugins.row.config`(以 `<包名>#<行 id>` 为键)给这一行一个配置控件,打开以行 id 为标题的页面。每个条目都按页面以 owner props 传入的两种视图渲染:`summary` 是标题下的一句话简介,`page` 是表单。页面负责画标题、图标与面包屑,把三份账本投影成一个可观察对象(`configLedgerSource`)绑在 store 旁边,自身从不点名任何可配置插件。
|
||||
**插件页承载配置;设置只保留清单。** 页面在其 `main` 条目下声明三个子 slot。`plugins.item`(list)按 `label` 把官方插件列在官方分组里。`plugins.bundle.config`(以组合包的包名为键)渲染在组合包页面的描述与行之间。`plugins.row.config`(以 `<包名>#<行 id>` 为键)给这一行一个配置控件,打开它自己的页面。该页面的标题与描述遵循[插件元信息决策](2026-09-18-localized-package-metadata.zh.md);slot 键和技术身份不随展示文本改变。
|
||||
|
||||
页面通过 `view: 'page'` 渲染每个表单。官方插件卡片与详情还使用 `view: 'summary'`;行详情页只在插件没有展示描述时使用它。组合包配置仅渲染 `page`。页面负责画标题、图标与面包屑,把三份账本投影成一个可观察对象(`configLedgerSource`)绑在 store 旁边,自身从不点名任何可配置插件。侧栏导航与安装请求归属遵循[侧栏管理决策](2026-09-09-plugin-management-in-the-web-sidebar.zh.md)。
|
||||
|
||||
**归属由注册方在 slot 名与键里声明。** 不加 manifest 字段,不加注册元数据,也不从 settings 服务取 owner 信息:官方页面不带组合包注册,组合包的页面带自己的包名注册,行的页面带组合包与其 patch 声明的行 id 注册。没有浏览器半侧的社区组合包就没有配置页;页面不会从 settings schema 渲染通用表单。
|
||||
|
||||
**只有保存才写入。** 表单的放弃控件与未保存标记都去掉了:离开页面即丢弃暂存修改,表单在卸载时执行。保存仍经由客户端 settings scope,带 revision 栅栏。
|
||||
**只有保存才写入。** 表单没有放弃控件或未保存标记;离开页面时在卸载过程中丢弃暂存修改。保存经由客户端 settings scope,带 revision 栅栏。
|
||||
|
||||
**四个宿主平面页面由 `ui-settings-plugins` 在 Host 服务其命名空间期间注册。** 该包把设置分区保留为清单标签页外面的**内置插件**外壳,在共享的 settings 镜像显示某个命名空间时通过 `ctx.slots.inject` 注册对应页面,命名空间消失时销毁,因此没有组装该插件的部署不会留下它的痕迹。`settings.plugin.item` slot 退役。
|
||||
|
||||
**官方分组。** 安装随附的可选组合包开启这个分组,属于 beta 功能的(Agent Teams)带 **Beta** 标签,不再有官方标签;配置页排在其后。Auto review 离开 `OPTIONAL_BUNDLES` 与 CLI 的依赖:它是一个已发布的实验包,安装引导拿它作示例。
|
||||
**官方分组。** 安装随附的可选组合包开启这个分组,属于 beta 功能的(Agent Teams)带 **Beta** 标签,没有官方标签;配置页排在其后。Auto review 是安装引导用作示例的已发布实验包,不属于 `OPTIONAL_BUNDLES` 或 CLI 依赖。
|
||||
|
||||
## 后果
|
||||
|
||||
- 组合包的浏览器半侧用一次 slot 注册加自己的词典就能提供表单;组合包的 patch 必须以键里的 id 声明这一行,注册只在承载组合包浏览器半侧的那一行开启期间存在:`dsh-client-modules` 把该半侧挂在说明符恰为包名的那一行上,因此键指向子路径行的行级页面随根行消失,而不随它自己的行。
|
||||
- 四个页面、它们的表单与 settings 写入路径不变;`ui-settings-plugins` 保留其名字,因为它仍拥有那个分区,尽管它的页面住在插件页上。
|
||||
- 设置只列出清单;带有插件导航项与配置标签页的设置类 golden 已重录。
|
||||
- 四个页面使用现有表单和 settings 写入路径;`ui-settings-plugins` 拥有设置分区,其配置页则位于插件页。
|
||||
- 设置只列出清单;Settings 浏览器 golden 覆盖该只读分区。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -32,8 +34,8 @@
|
||||
|
||||
**从 settings schema 生成通用表单。** `describe()` 已经把每个命名空间的 schemastery schema 发给浏览器,没有浏览器半侧的社区命名空间本可获得一份默认表单。延期:官方页面是精选的,通用表单会在没有白名单的情况下暴露内部命名空间,而把命名空间归属到组合包还需要 descriptor 带 owner。
|
||||
|
||||
**把卡片留在设置里、从插件页链接过去。** 最省事,但 review 要求表单在插件自己的页面上。
|
||||
**把卡片留在设置里、从插件页链接过去。** 这会把一个插件的描述和配置分散到不同页面。
|
||||
|
||||
## 测试
|
||||
|
||||
`ui-plugin-manager` 单测覆盖账本投影、官方分组的卡片与页面、组合包的表单以及行的页面;`ui-settings-plugins` 单测覆盖按被服务的命名空间注册与撤下,以及表单只保存不放弃的行为。`plugin-config` web lane 通过真实链路编辑 shell 页面,并在开启社区夹具的组合包后打开其行的页面;`plugin-manager` lane 记录新的官方分组。
|
||||
`ui-plugin-manager` 单测覆盖账本投影、官方分组的卡片与页面、组合包的表单以及行的页面,包括描述优先于其摘要的规则;`ui-settings-plugins` 单测覆盖按被服务的命名空间注册与撤下,以及表单只保存不放弃的行为。`plugin-config` web lane 通过真实链路编辑 shell 页面,并在开启社区夹具的组合包后打开其行的页面;`plugin-manager` lane 记录官方分组。
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-15-desktop-profile-core-cleanup.md
|
||||
2026-09-15-desktop-profile-core-cleanup.md: da189664e2b55a9310b36ed02f0b275e2c1b1370
|
||||
2026-09-15-desktop-profile-core-cleanup.zh.md: dec12e4fe7f34beb5931961183041c3f642b44ae
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-developer-session-changes.md
|
||||
2026-09-17-developer-session-changes.md: 0545dfa4344f8ed87fe6343e13901647a5ccdac4
|
||||
2026-09-17-developer-session-changes.zh.md: 81c53115aaf4447dac358febcbd1310bb2a36cb2
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Developer messages for incremental session changes
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-17-developer-session-changes.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Dynamic tool changes need durable, ordered records before providers can preserve the cached conversation prefix while changing available tools. Encoding these changes as free-form reminders would lose their structured tool identity and couple session state to prompt wording.
|
||||
|
||||
## Decision
|
||||
|
||||
`DeveloperMessage` records admitted model-visible session changes in conversation order. Additions store only `toolName`; the containing event's `headerSeq` selects an earlier full `request/header` with exactly one schema for each added name. One event binds all its additions to one header revision. Additions replace the active same-name definition; removals identify the name without duplicating its schema. The header reference is absent when the message contains no additions. Same-name replacement, restart, fork, and surface replacement retain historical definition identity without consulting the current registry or latest request header. Intermediate registry changes that no admitted request exposes do not require developer records.
|
||||
|
||||
`developer/message` carries the message and its admitting turn/step coordinates. Native readers and Session admission reject missing, forward, unknown, non-header, and ambiguous schema references, incomplete referenced definitions, and reject retired inline definitions while preserving unrelated JSON fields. `sourceEventSeqs` remains independent derivation metadata. A replacement retains its selected header and cites the shadowed surface nodes; combining additions from different header revisions requires separate events. Forks retain the referenced prefix, and compaction does not delete header events. Future cardinality-changing migrations must remap `headerSeq` as a same-artifact event reference. Empty developer content retains its surface position without adding a model message or shadowing the protected system head. The tools package owns the `tool-registry` source kind under the [producer-owned source rules](2026-09-09-producer-owned-message-sources.md).
|
||||
|
||||
`ToolAdditionBlock.tool` is declared as optional `never` with `@persistenceReserved`. The schema history therefore records the rejected field: permitting any value later changes an existing value type and requires a format bump, while unrelated optional metadata remains additive.
|
||||
|
||||
`ToolSchema.deferLoading` requests deferred definition loading independently of tool-addition history. Tool construction, registry projection, and prompt assembly preserve the marker. Dynamic addition and definition loading are separate facts; a predeclared deferred definition need not acquire a fabricated addition record.
|
||||
|
||||
The V4 codec confines developer roles to the `developer/message` slot, including rejection from inbox and title-request arrays. Native validation checks required developer fields, historical header bindings, and developer-only tool-change blocks while preserving additional JSON properties. Optional metadata additions therefore remain readable under the persistence checker's optional-property rule; additional fields cannot weaken required-field, role, or open-step checks. An ignorable marker does not establish reader support: physical decoding preserves developer payloads until vocabulary-aware admission, while writers and native readers reject malformed known data. Developer admission leaves malformed ordinary message slots to decoder recovery. Installed Session adoption validates surface relationships without converting developer records into released-V3 user records. The [released migration rules](2026-08-31-released-session-format-migrations.md) continue to protect committed predecessor generations.
|
||||
|
||||
The producer requirement has one scoped exception: finalized Session V4 reserves the persisted representation needed by issue #4146. Waiting for provider and UI implementations would couple the format release to those independent integrations. The reservation permits storage and validation only; no shipped profile emits developer records, and unsupported consumers throw. Production emission requires provider, UI, and compaction support together.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Use text reminders as developer content.** The format represents structured session changes; model instructions belong in system messages. Future developer content is unspecified rather than constrained to reminders.
|
||||
|
||||
**Duplicate definitions in each addition.** Full request headers already preserve the exact schemas. A historical reference avoids a second persisted copy while retaining same-name replacement identity. Binding by the latest header or registry instead would reinterpret older additions.
|
||||
|
||||
**Treat deferred loading as proof of a tool-addition record.** That would prevent predeclared deferred tools. The schema flag describes loading, while the block records a dynamic addition.
|
||||
|
||||
## Consequences
|
||||
|
||||
Session V4 persists the tool-change representation for issue #4146. Automatic emission, provider serialization, deferred loading, and UI presentation are intentionally deferred so this type-and-persistence change does not block the V4 merge. Both DeepSeek protocols and pi-ai throw for developer history and deferred-loading requests; Chat and Trajectory throw for developer events. Shipped model-output and SDK snapshot scenarios contain no developer records.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note:使用 developer 消息记录增量 Session 变更
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-17-developer-session-changes.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
动态工具变更需要持久且有序的记录,提供方才能在改变可用工具时保留对话的缓存前缀。将这些变更编码为自由文本提醒会丢失结构化的工具身份,并将 Session 状态与提示词措辞耦合。
|
||||
|
||||
## Decision
|
||||
|
||||
`DeveloperMessage` 按对话顺序记录已接纳且模型可见的 Session 变更。添加块仅存储 `toolName`;所在事件的 `headerSeq` 选择更早的完整 `request/header`,其中每个添加名称必须恰好对应一个模式。同一事件的所有添加绑定到同一个请求头版本。添加替换当前同名定义;移除仅记录名称,不重复模式。没有添加块的消息不携带请求头引用。同名替换、重启、fork 和 surface 替换保留历史定义身份,不查询当前注册表或最新请求头。没有被任何已接纳请求暴露的中间注册表变更无需 developer 记录。
|
||||
|
||||
`developer/message` 携带消息及其接纳 turn/step 坐标。原生读取方和 Session 接纳逻辑拒绝缺失、前向、未知、非请求头及歧义模式引用、不完整的被引用定义,并拒绝已退役的内嵌定义,同时保留无关 JSON 字段。`sourceEventSeqs` 保持为独立的派生元数据。替换保留选定的请求头,并引用被覆盖的 surface 节点;不同请求头版本的添加必须分属不同事件。fork 保留所引用的前缀,压缩不删除请求头事件。未来改变事件数量的迁移必须将 `headerSeq` 作为同一文件内的事件引用重映射。空 developer 内容保留其 surface 位置,不添加模型消息,也不能覆盖受保护的 system 头节点。tools 包按照[生产者自有来源规则](2026-09-09-producer-owned-message-sources.zh.md)拥有 `tool-registry` 来源种类。
|
||||
|
||||
`ToolAdditionBlock.tool` 声明为可选的 `never`,并标注 `@persistenceReserved`。模式历史因此记录这个被拒绝的字段:后续允许任何值都会改变现有值类型并要求格式递增,无关的可选元数据仍属于可兼容添加。
|
||||
|
||||
`ToolSchema.deferLoading` 请求延迟加载工具定义,与工具添加历史相互独立。工具构造、注册表投影与提示词组装保留该标记。动态添加与定义加载是不同事实;预先声明的延迟加载定义无需虚构添加记录。
|
||||
|
||||
V4 编解码器将 developer 角色限定在 `developer/message` 字段内,并拒绝 inbox 与标题请求数组中的该角色。原生验证检查 developer 必需字段、历史请求头绑定及工具变更块仅限 developer 的约束,同时保留附加 JSON 属性。因此,可选元数据添加符合持久化检查器的可选属性规则,仍可读取;附加字段不能削弱必需字段、角色或打开 step 的检查。可忽略标记不代表读取方支持该事件:物理解码保留 developer 载荷,直到按词表执行接纳;写入方和原生读取方仍拒绝格式错误的已知数据。developer 接纳逻辑将格式错误的普通消息字段留给解码器恢复。已安装的 Session 接纳逻辑验证 surface 关系,不把 developer 记录转换为已发布 V3 的 user 记录。[已发布格式迁移规则](2026-08-31-released-session-format-migrations.zh.md)继续保护已提交的前代文件。
|
||||
|
||||
产生方要求有一个限定例外:已定稿 Session V4 为 issue #4146 保留所需的持久化表示。等待提供方与 UI 实现会将格式发布与这些独立集成耦合。该预留仅允许存储和验证;已提供的 profile 不发出 developer 记录,不支持的消费者会抛出错误。生产环境发出这些记录要求提供方、UI 与压缩支持一起落地。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**将文本提醒作为 developer 内容。** 此格式表示结构化 Session 变更;模型指令属于 system 消息。未来的 developer 内容尚未指定,不将其限制为提醒。
|
||||
|
||||
**在每次添加中重复定义。** 完整请求头已经保留精确模式。历史引用避免第二份持久化副本,同时保留同名替换的身份。若绑定到最新请求头或注册表,则会重新解释较早的添加。
|
||||
|
||||
**将延迟加载视为存在工具添加记录的证据。** 这会阻止预先声明的延迟工具。模式标记描述加载方式,内容块记录动态添加。
|
||||
|
||||
## Consequences
|
||||
|
||||
Session V4 为 issue #4146 持久化工具变更表示。自动发出事件、提供方序列化、延迟加载与 UI 展示有意留待后续实现,使此类型与持久化变更不阻塞 V4 合并。DeepSeek 两种协议与 pi-ai 对 developer 历史及延迟加载请求抛出错误;Chat 与 Trajectory 对 developer 事件抛出错误。已提供的模型输出与 SDK 快照场景不包含 developer 记录。
|
||||
+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-frame-shell-leading-window-chrome-seat.md
|
||||
2026-09-17-frame-shell-leading-window-chrome-seat.md: cc3ef576900e38dc26c46438fd5d490ac4361be0
|
||||
2026-09-17-frame-shell-leading-window-chrome-seat.zh.md: c652f313d4f4863decc14e556a1674487d1d3fea
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# Agent Note: Frame-owned shell.leading window-chrome seat
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-17-frame-shell-leading-window-chrome-seat.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The [macOS hidden-titlebar work](../feature/2026-09-13-macos-hidden-titlebar-vibrancy.md) hid the collapsed sidebar column entirely on darwin and moved the reopen and New Session controls into a conversation-header slot (`conversation.session.header.leading`), shown via CSS while `data-sidebar-collapsed` was published. Only the Conversation had that seat: with any other main panel selected (the plugin manager, or any future global panel), a collapsed window kept the floating traffic lights over the panel's content with no reopen control anywhere on screen. Each new panel would have had to grow its own leading seat and repeat the same clearance geometry.
|
||||
|
||||
## Decision
|
||||
|
||||
The window chrome belongs to the frame, not to one panel. ui-layout declares a fifth root-scoped child slot, `shell.leading` (single), and AppFrame renders its seat as a frame-level box over the columns — mounted only on the darwin desktop while `sidebarCollapsed`, the full-hide state that leaves window chrome without a home (Windows' zero-width collapse keeps its fixed caption controls, so the seat must not mount a second set there). The seat sits at the frame's top-left (left 88px clears the hiddenInset traffic lights, top 11px centers the 28px controls on the conversation title row, z-index 15 above column content and below frame overlays) and carries `-webkit-app-region: no-drag`, subtracting itself from any drag band beneath it.
|
||||
|
||||
Under the same collapsed condition the frame publishes `--dsh-frame-leading-clearance: 160px` — the inline band the traffic lights plus the seat's two controls occupy, measured from the frame's left edge. Window fullscreen hides the traffic lights (the desktop preload mirrors the state onto `html[data-fullscreen]`): the seat moves to `left: 12px` into their vacated band and the clearance drops to `84px`. The conversation title row is the one consumer: it pads `max(0px, clearance - 20px)` (its header already pads 20px). Entry pages that start below the window strip in both sidebar states (the plugin manager's page head) instead pad their top by `--dsh-frame-top-clearance` (48px, published on the darwin frame unconditionally), which clears the strip vertically rather than indenting inline.
|
||||
|
||||
ui-sidebar registers `HeaderLeadingControls` into `shell.leading` instead of the conversation seat, reusing the shell's inject face and locale; the component renders unconditionally because the frame owns the mount condition, and its platform check and CSS collapse-gating are deleted. This also drops ui-sidebar's dependency on ui-conversation. The original `conversation.session.header.leading` slot is gone per the pre-stable API rule (every consumer updated, no compatibility shim); the conversation header now exposes its own sessionless `conversation.header.leading` seat (root-scoped, rendered without a Session) as a public extension point, empty in the shipped composition because the window-chrome controls live in `shell.leading`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keeping the conversation seat and adding one per panel.** Every main panel would repeat the seat markup, the drag-region subtraction, and the traffic-light geometry, and a panel that forgot would strand a collapsed window without a reopen control. One frame-owned seat makes the guarantee structural.
|
||||
|
||||
**Mounting the occupant always and gating visibility in CSS (the prior mechanism).** This kept dead DOM and a no-drag subtraction in the header on every platform and every collapse state, and it could not help non-conversation panels at all. A frame-level TSX mount keeps the condition, the seat geometry, and the clearance publication in one file.
|
||||
|
||||
**Gating the seat on rightbar fullscreen as well.** Unnecessary: the fullscreen right panel (z-index 40) covers the seat (15) visually, and it already opts its whole box out of window dragging, so the seat's geometry cannot puncture it.
|
||||
|
||||
**Publishing clearance as a fixed padding each consumer hardcodes.** A raw `160px` in each panel would silently desynchronize from the seat's real band; one custom property published under exactly the mount condition keeps consumers correct when the geometry changes.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Every current and future main panel keeps the reopen and New Session controls while the sidebar is hidden, at zero per-panel cost; the conversation opts into `--dsh-frame-leading-clearance`, and entry pages take the unconditional `--dsh-frame-top-clearance` instead.
|
||||
- `conversation.session.header.leading` no longer exists in the client catalog; `shell.leading` carries the window chrome as a public root-scoped seat, while the conversation header's sessionless `conversation.header.leading` remains a separate public seat with no shipped occupant.
|
||||
- ui-sidebar no longer depends on ui-conversation (inject list, devDependency, and tsconfig reference removed).
|
||||
- The mount condition names darwin explicitly: Windows' `data-windows-titlebar` collapse also yields a zero-width column but keeps its own fixed caption controls, which a frame seat would duplicate. A platform that later hides its column without leaving its own chrome opts in by widening the condition.
|
||||
|
||||
## Testing
|
||||
|
||||
The ui-layout app-frame spec pins the seat's mount and unmount across the darwin collapse and its absence beside the 56px rail; the apply spec pins the five-slot declaration and teardown. The ui-sidebar apply spec pins the `shell.leading` registration (component, locale, shared inject face) and its removal on dispose; the sidebar-root spec pins the two controls' actions.
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# Agent Note: 框架持有的 shell.leading 窗口 chrome 座
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-17-frame-shell-leading-window-chrome-seat.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
[macOS 隐藏标题栏工作](../feature/2026-09-13-macos-hidden-titlebar-vibrancy.zh.md)在 darwin 上把收起的侧边栏整列隐藏,并把重新打开与 New Session 控件移入会话头部的 slot(`conversation.session.header.leading`),在 `data-sidebar-collapsed` 发布期间由 CSS 显示。但只有会话界面有这个座:选中任何其他主面板(插件管理器或未来的全局面板)时,收起的窗口只剩悬浮的红绿灯压在面板内容上,屏幕上没有任何重新打开控件。每个新面板都得自建 leading 座并重复同一套避让几何。
|
||||
|
||||
## Decision
|
||||
|
||||
窗口 chrome 属于框架,不属于某个面板。ui-layout 声明第五个 root 作用域子 slot `shell.leading`(single),AppFrame 把它的座渲染为覆盖各列的框架级盒子——仅在 darwin 桌面且 `sidebarCollapsed` 时挂载,即让窗口 chrome 无处安放的全隐藏状态(Windows 的零宽收起保留其固定 caption 控件,该座在那里挂载会出现第二组控件)。座位于框架左上角(left 88px 避开 hiddenInset 红绿灯,top 11px 让 28px 控件对齐会话标题行中线,z-index 15 高于列内容、低于框架浮层),并带 `-webkit-app-region: no-drag`,把自己从下方任何拖拽带中减去。
|
||||
|
||||
在同一收起条件下框架发布 `--dsh-frame-leading-clearance: 160px`——红绿灯加座内两个控件占据的行内带宽,自框架左边缘起量。窗口全屏隐藏红绿灯(桌面 preload 把状态镜像到 `html[data-fullscreen]`):座移入其空出的带宽(`left: 12px`),避让量降到 `84px`。会话标题行是唯一消费者:它 pad `max(0px, clearance - 20px)`(其头部已 pad 20px)。两种侧栏状态下都始于窗口顶带之下的入口页(插件管理器页头)改用 `--dsh-frame-top-clearance`(48px,darwin 框架上无条件发布)做顶部内边距,纵向避开顶带而非行内缩进。
|
||||
|
||||
ui-sidebar 把 `HeaderLeadingControls` 改注册到 `shell.leading` 而非会话座,复用 shell 的 inject face 与 locale;因挂载条件归框架所有,组件无条件渲染,其平台判断与 CSS 收起门控被删除,同时解除 ui-sidebar 对 ui-conversation 的依赖。原 `conversation.session.header.leading` slot 按 pre-stable API 规则不复存在(更新所有消费者,不留兼容垫层);会话头现在自带无会话可用的 `conversation.header.leading` 座(root 作用域、无 Session 也渲染)作为公开扩展点,因窗口 chrome 控件已住进 `shell.leading`,出厂组合中它保持空置。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**保留会话座并为每个面板各加一个。** 每个主面板都要重复座标记、拖拽区减除与红绿灯几何,漏掉的面板会让收起的窗口失去重新打开控件。框架持有的单一座使该保证成为结构性的。
|
||||
|
||||
**始终挂载占用方、由 CSS 控制可见性(先前机制)。** 它在所有平台与所有收起状态下都保留死 DOM 和头部的 no-drag 减除,且完全帮不到非会话面板。框架级 TSX 挂载把条件、座几何与避让量发布集中在一个文件。
|
||||
|
||||
**同时以右栏全屏为座的门控条件。** 不必要:全屏右面板(z-index 40)在视觉上盖住座(15),且它已让整个盒子退出窗口拖拽,座的几何无法穿透它。
|
||||
|
||||
**把避让量作为各消费者硬编码的固定内边距。** 各面板写死 `160px` 会与座的真实带宽悄然失同步;在恰为挂载条件下发布一个自定义属性,几何变化时消费者仍保持正确。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 侧边栏隐藏时,现有与未来的每个主面板都免费获得重新打开与 New Session 控件;会话接入 `--dsh-frame-leading-clearance`,入口页改用无条件发布的 `--dsh-frame-top-clearance`。
|
||||
- `conversation.session.header.leading` 从客户端目录中消失;`shell.leading` 作为公开 root 作用域座承载窗口 chrome,会话头无会话可用的 `conversation.header.leading` 则是另一个公开座,出厂组合中无占用者。
|
||||
- ui-sidebar 不再依赖 ui-conversation(inject 列表、devDependency 与 tsconfig 引用均已移除)。
|
||||
- 挂载条件显式点名 darwin:Windows 的 `data-windows-titlebar` 收起同样得到零宽列,但保留自己的固定 caption 控件,框架座会与之重复。未来某平台若隐藏整列且不留自己的 chrome,通过放宽该条件接入。
|
||||
|
||||
## Testing
|
||||
|
||||
ui-layout 的 app-frame spec 钉住 darwin 收起下座的挂载/卸载及 56px 控制栏旁座的缺席;apply spec 钉住五 slot 声明与拆除。ui-sidebar 的 apply spec 钉住 `shell.leading` 注册(组件、locale、共享 inject face)及 dispose 时的移除;sidebar-root spec 钉住两个控件的动作。
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-12-session-unarchive-settings-page.md
|
||||
2026-09-12-session-unarchive-settings-page.md: 3b1e364e3abc6cea0a8b29ad78abb0ff7c274dbe
|
||||
2026-09-12-session-unarchive-settings-page.zh.md: 61e108dd50e61b9e6e9f72098973c067e3257407
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-native-v4-read-validation.md
|
||||
2026-09-17-native-v4-read-validation.md: b111e9f1dd4aae6557b7893a9f334445ef0c85bc
|
||||
2026-09-17-native-v4-read-validation.zh.md: 20f1dc53b6eda42834e169c18f7b269e2933d27c
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: Mandatory native V4 relationship validation
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-17-native-v4-read-validation.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Tool-role results cannot be checked through the released V3 user-role representation without projecting away current fields. Replacing that validation view requires a native reader to retain the earlier lifecycle, retired-syntax, and system-message checks. The representation change and its mandatory admission therefore belong together.
|
||||
|
||||
An event can satisfy its TypeScript declaration and message rules while contradicting earlier durable events. A tool result without an advertised call and a checkpoint naming another compaction both contain locally valid fields. Accepting either as a current Session makes later readers depend on damaged relationships, and tightening admission after release can strand already-written histories.
|
||||
|
||||
## Decision
|
||||
|
||||
The V4 format restorer and current JSONL scanner share one mandatory cross-event relationship pass before exposing a restored artifact or storage handle. It checks turn and step order, advertised tool calls and settlements, PTC parentage and settlement identity, retry chains and request providers, human-message title citations, command completion references, compaction ownership and current spans, and the protected system head. The installed event set controls semantic interpretation, including compaction prepasses and title references. Unknown ignorable records retain their payloads and original positions; known records remain validated even when marked ignorable. The check reads native event fields without altering artifacts or projecting them into a historical format. JSON argument comparisons inspect own properties in both Node and the worker; inherited prototype members cannot satisfy recorded fields. The [tool-role decision](2026-09-15-first-class-tool-role-messages.md) owns result representation.
|
||||
|
||||
An unfinished tail may retain an open turn, step, tool call, or compaction. A closing event must settle the relationships it closes. An inherited unfinished compaction ends at its `session/end-seed` marker; it does not constrain the child's lifecycle. Repair message identities retain their historical numeric suffix after earlier migrations change sequence coordinates. Fork repairs retain their separately validated native identities.
|
||||
|
||||
Installed Session adoption continues to own event envelopes, message roles and metadata, canonical request headers, and surface replacement reference coverage. Optional runtime invariants provide diagnostics; their installation is not required for durable admission. Native system-message admission also validates coordinates, identity, content blocks, and known image/tool-call fields before physical recovery can discard rows. Additional JSON fields and unknown nonempty content tags remain intact; retired tool-result wrappers are refused. Retired `request/header.header.system` and required `tool/code-dispatch*` tags remain hard refusals before recovery; obsolete ignorable PTC records remain opaque. Physical framing alone does not establish semantic validity.
|
||||
|
||||
A retired tool-result wrapper outside its owning `tool/result` row would require changing the event, role, or source to represent it in V4. The adjacent migration lifts only the canonical owned wrapper; target restoration refuses wrappers remaining in interpreted content. Native row admission applies the same refusal before recoverable tail suppression. The check follows declared message, content, and Assistant-stream slots instead of recursively inspecting JSON, so tool arguments, replay state, schema parameters, nested extensions, and unknown ignorable payloads retain their recorded values.
|
||||
|
||||
`EpochHeader.system` explicitly reserves the retired key as `system?: never` with `@persistenceReserved`. The catalog retains its empty value set so permitting a value is a change to a forbidden field, rather than an ordinary optional-field addition. The native reader and Session adoption continue to reject that key; system prompts remain `system/message` events.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Project V4 back into a frozen V3 validation view.** This couples current acceptance to retired representations and invites new fields to disappear in the projection. Frozen generations remain independently readable, while the current restorer interprets its own fields.
|
||||
|
||||
**Rely on optional runtime invariants.** Detached readers and restoration paths may not install diagnostic companions. A required relationship must be checked before publication or consumption of the restored Session.
|
||||
|
||||
## Consequences
|
||||
|
||||
The native restorer maintains one bounded state machine per read and rejects contradictions before successor publication. Frozen V0–V3 validators and committed Session generations remain unchanged. Public catalog regressions cover invalid relationships alongside partial tails, inherited cuts, immutable unknown attribution, and complete tool exchanges; these checks enforce the declared V4 representation without changing its schemas. Breaking changes require a successor; compatible additions use new acknowledgements, and implementation fixes preserve the declared meaning.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: 原生 V4 关系的强制校验
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-17-native-v4-read-validation.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
tool 角色结果不能通过已发布的 V3 user 角色表示进行校验,否则投影会丢弃当前字段。替代该校验视图后,原生读取方必须保留原有的生命周期、退役语法和 system 消息检查。因此,表示变更与必需的接纳校验需要一同完成。
|
||||
|
||||
事件即使符合 TypeScript 声明和消息规则,也可能与较早的持久化事件矛盾。没有声明调用的工具结果、指向另一压缩事务的检查点,都可以拥有局部有效字段。将它们接纳为当前 Session 会让后续读取器依赖损坏的关系;发布后再收紧准入则可能让已写入的历史无法读取。
|
||||
|
||||
## Decision
|
||||
|
||||
V4 格式恢复器与当前 JSONL 扫描器共用同一强制跨事件关系校验,在公开恢复产物或存储句柄之前执行。它检查 turn 与 step 顺序、声明的工具调用及其结算、PTC 父子关系与结算身份、重试链与请求 provider、标题的人类消息引用、命令完成引用、压缩归属与当前区间,以及受保护的 system 首节点。已安装事件集合控制语义解释,包括压缩预扫描和标题引用。未知的可忽略记录保留其载荷与原始位置;已知记录即使标记为可忽略仍会接受校验。校验直接读取原生事件字段,不修改产物,也不将其投影为历史格式。Node 和 Worker 中的 JSON 参数比较都检查自有属性,继承的原型成员不能代替记录字段。[工具角色决策](2026-09-15-first-class-tool-role-messages.zh.md)负责结果表示。
|
||||
|
||||
未完成的尾部可以保留打开的 turn、step、工具调用或压缩事务。关闭事件必须结清它关闭的关系。继承的未完成压缩事务在其 `session/end-seed` 标记处结束,不约束子 Session 的生命周期。较早迁移改变序号坐标后,修复消息身份仍保留历史数字后缀。fork 修复保留经过独立校验的原生身份。
|
||||
|
||||
已安装的 Session 接纳器继续负责事件封装、消息角色与元数据、规范请求头,以及 surface 替换的引用覆盖。可选运行时不变式提供诊断;持久化准入不要求安装它们。原生 system 消息准入还会在物理恢复可能丢弃行之前,校验坐标、身份、内容块及已知 image/tool-call 字段。额外 JSON 字段和非空的未知内容标签保持不变,退役的 tool-result 包装会被拒绝。退役的 `request/header.header.system` 和必需的 `tool/code-dispatch*` 标签仍在恢复前强制拒绝;标记为可忽略的过时 PTC 记录保持不透明。仅通过物理分帧不能证明语义有效。
|
||||
|
||||
退役的 tool-result 包装若不属于对应的 `tool/result` 行,就需要改变事件、角色或来源才能在 V4 中表示。相邻迁移只提升规范的自有包装;目标恢复器拒绝解释内容中剩余的包装。原生行准入在可恢复尾部被忽略之前执行同样的拒绝。检查沿声明的消息、内容和 Assistant stream 字段进行,不递归扫描 JSON,因此工具参数、replay state、schema 参数、嵌套扩展和未知可忽略载荷保留其记录值。
|
||||
|
||||
`EpochHeader.system` 通过带有 `@persistenceReserved` 的 `system?: never` 显式保留退役键。目录保留其空值集合,使允许某个值被识别为对禁止字段的修改,而非普通可选字段添加。原生读取方和 Session 接纳继续拒绝该键;系统提示词仍由 `system/message` 事件承载。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**将 V4 投影回冻结的 V3 校验视图。** 这会将当前准入与退役表示绑定,并容易让新增字段在投影中消失。冻结代际仍可独立读取,当前恢复器解释自己的字段。
|
||||
|
||||
**依赖可选运行时不变式。** 分离读取器和恢复路径可能不安装诊断组件。必需关系必须在发布或消费恢复后的 Session 之前完成校验。
|
||||
|
||||
## Consequences
|
||||
|
||||
原生恢复器为每次读取维护独立状态机,在发布后继文件之前拒绝矛盾。冻结的 V0–V3 校验器及已提交的 Session 代际保持不变。公开目录回归覆盖无效关系、部分尾部、继承切分点、未知归属的不可变保存,以及完整工具交互;这些检查落实已声明的 V4 表示,不改变其 schema。破坏性变更要求后继版本;兼容新增使用新的确认记录,实现修复须保留声明的含义。
|
||||
+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-persistence-attribution-policy.md
|
||||
2026-09-17-persistence-attribution-policy.md: 19bebc8bc186f4b2b7e2e561fb6a53ceadcebabf
|
||||
2026-09-17-persistence-attribution-policy.zh.md: 3561c1d1afca5402cb94b1578603c4f1a2fc89a4
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Recorded user-source attribution compatibility
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-17-persistence-attribution-policy.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A producer-owned message source appears transitively in several persisted event types. A new attribution kind changes their union fingerprints even when existing readers preserve its fields without the producer. Request-only prompts need no durable identity, but constructing them as Session messages adds unnecessary source alternatives to that union.
|
||||
|
||||
## Decision
|
||||
|
||||
The core-owned user-message source property binds the [recorded compatibility policy](../process/2026-09-17-persistence-schema-review.md). A producer separately qualifies its literal wire kind as attribution-only. Both saved schemas must carry compatible policy state before a qualified addition receives a same-version classification. Existing alternatives remain structurally compared; missing policy, unqualified additions, removals, and changes to existing semantic groups retain strict classification. Fixed system, model, and tool source fields do not participate.
|
||||
|
||||
The tmux-context producer qualifies its location attribution. Its own projection uses the kind to avoid repeated injection, while readers without that producer retain the recorded content and every source JSON property. The qualification promises reader independence; it does not forbid producer-local duplicate suppression.
|
||||
|
||||
`RequestMessage` accepts a durable `Message` or a user-only `RequestUserInput` with content and no `id` or `source`. The LLM service and provider serializers accept both; Session writes, Agent delivery, and persisted title-request inputs require durable messages. Auto Review's outer prompt and the compaction summarizer's final instruction use request-only inputs and retain their content and freezing. The caller receives no implicit durable conversion. Assistant replay metadata and tool-result correlation retain their existing requirements.
|
||||
|
||||
The finalized V4 acknowledgement records the user-source policy and removes the two request-only source registrations. Its original predecessors and header transition remain intact. This does not grant a general exception for removing accepted variants: the [producer-source migration](2026-09-09-producer-owned-message-sources.md) preserves its producer mappings and extension namespaces, including historical request-only kinds, independently of active source declarations. Catalog generation and comparison remain owned by the [persistence reference](../../../../docs/persistence-changes/README.md#compatibility-rules).
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Treat every source addition as breaking.** This would require a Session-format increment for attribution that readers already retain safely, without distinguishing it from semantic changes.
|
||||
|
||||
**Relax every discriminated union or infer compatibility from current names.** A generic exception would admit semantic variants. Current declarations cannot establish what an older snapshot promised, and structural equality does not establish ownership of a source property.
|
||||
|
||||
**Use durable messages for every request.** Detached prompts would retain manufactured identities and unnecessary source declarations. The explicit user-only alternative permits their removal while typed Session interfaces retain durable identity requirements.
|
||||
|
||||
**Narrow historical context forms to current writer examples.** Writer examples do not establish every value admitted by released readers. Those read-facing variants remain represented.
|
||||
|
||||
## Consequences
|
||||
|
||||
A qualified addition still changes affected event fingerprints and requires an acknowledgement. Reviewers must verify the producer's attribution promise; type analysis cannot establish replay semantics or detect behavior hidden in opaque values.
|
||||
|
||||
Codec-to-Session tests retain unknown user attribution and extra JSON fields without the producer. Provider equality tests compare request-only and durable inputs, while type assertions reject request-only inputs at durable writes. Recorded Session generations and the V3-to-V4 conversion remain independent of catalog policy and rendering.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Agent Note:已记录的 user-source 归属兼容性
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-17-persistence-attribution-policy.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
生产者拥有的消息 source 会传递性地出现在多个持久化事件类型中。即使已有读取方无需该生产者也能保留字段,新增归属 kind 仍会改变这些联合类型的指纹。仅供请求使用的提示不需要持久身份,但将其构造成 Session 消息会为联合类型增加不必要的 source 分支。
|
||||
|
||||
## 决策
|
||||
|
||||
核心拥有的 user-message source 属性绑定[已记录的兼容性策略](../process/2026-09-17-persistence-schema-review.zh.md)。生产者另行将其 wire 字面量 kind 声明为仅用于归属。双方保存的 schema 必须具有兼容的策略状态,符合条件的添加才能获得同版本分类。已有分支继续接受结构比较;缺失策略、未标记的添加、删除以及已有语义分组的变更继续采用严格分类。固定的 system、model 和 tool source 字段不参与。
|
||||
|
||||
tmux-context 生产者将位置归属标记为符合条件。其自身投影使用 kind 避免重复注入,而未安装该生产者的读取方保留已记录内容及 source 的每个 JSON 属性。该标记承诺读取方不依赖生产者,并不禁止生产者内部的去重。
|
||||
|
||||
`RequestMessage` 接受持久化 `Message`,或仅含 content、没有 `id` 和 `source` 的 user 专用 `RequestUserInput`。LLM 服务和 provider 序列化器接受两者;Session 写入、Agent 交付及持久化标题请求输入要求持久化消息。Auto Review 的外层提示和压缩摘要器的最终指令使用仅供请求使用的输入,并保留内容与冻结行为。调用方不会获得隐式持久化转换。Assistant 回放元数据和工具结果关联保持原有要求。
|
||||
|
||||
已定稿的 V4 确认记录保存 user-source 策略,并移除两个仅供请求使用的 source 注册。原有前驱和 header 迁移保持不变。这不构成删除已接受分支的通用例外:[producer-source 迁移](2026-09-09-producer-owned-message-sources.zh.md)独立于活动 source 声明保留生产者映射与扩展命名空间,包括历史的仅供请求使用的 kind。目录生成和比较仍由[持久化参考](../../../../docs/persistence-changes/README.zh.md#compatibility-rules)负责。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**将所有 source 添加视为破坏性变更。** 这会要求为读取方已能安全保留的归属信息提升 Session 格式版本,无法区分它与语义变更。
|
||||
|
||||
**放宽所有可辨识联合或从当前名称推断兼容性。** 通用例外会接纳语义分支。当前声明不能确定旧快照作出的承诺,结构相等也不能确定 source 属性的归属。
|
||||
|
||||
**为每个请求使用持久化消息。** 独立提示会保留人为制造的身份和不必要的 source 声明。显式的 user 专用分支允许移除它们,同时让带类型的 Session 接口继续要求持久身份。
|
||||
|
||||
**将历史 context 形式收窄到当前写入方示例。** 写入方示例不能确定已发布读取方接纳的全部值。这些面向读取的分支继续保留。
|
||||
|
||||
## 影响
|
||||
|
||||
符合条件的添加仍会改变相关事件指纹,并要求确认记录。Review 者必须验证生产者的归属承诺;类型分析不能确定回放语义,也不能发现不透明值中隐藏的行为。
|
||||
|
||||
Codec 到 Session 的测试在未安装生产者时保留未知 user 归属和额外 JSON 字段。Provider 等价测试比较仅供请求使用的输入与持久化输入,类型断言则拒绝在持久化写入处使用仅供请求使用的输入。已记录的 Session 世代及 V3→V4 转换不依赖目录策略和渲染。
|
||||
+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-17-web-feature-routes-and-route-gate.md
|
||||
2026-09-17-web-feature-routes-and-route-gate.md: 3939bdcd0c53e766827e8fcf05f64fb4d284c59b
|
||||
2026-09-17-web-feature-routes-and-route-gate.zh.md: fb329c8fc1f93b9f979612b7163e838cc521428b
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Feature routes follow the document base, and a gate keeps them there
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-17-web-feature-routes-and-route-gate.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
[Web app-owned routes are document-relative](2026-09-14-web-document-relative-app-routes.md) makes the shell, its plugin bundles, and its streams reach one listener under any mount. Routes owned by feature packages still addressed the origin root: `/open-in-app/...`, the deliverables `present.*` and `changes.*` routes, the session-log export download, the upload worker's `/api/session/uploadFileBinary` post, and the `/api/file` route behind local markdown image paths. Under a prefix-stripping proxy each of those requests missed. Nothing stopped a new browser-face reference from binding the bundle to one mount again.
|
||||
|
||||
## Decision
|
||||
|
||||
Every feature producer keeps its absolute registration key and derives the browser form beside it: `OPEN_IN_APP_*_PATH`/`*_ROUTE`, `PRESENT_*_PATH`/`*_ROUTE`, `CHANGED_FILES_PATH` and `CHANGES_*_PATH`/`*_ROUTE`, `SESSION_LOG_EXPORT_PATH`/`_ROUTE`, `FILE_UPLOAD_PATH`/`_ROUTE`. Browser code addresses only the `*_ROUTE` form, and a Fetch-shaped carrier a page installs (`FileUploadFetch`, `RpcFetch`) receives that relative route and resolves it against its own base.
|
||||
|
||||
Two consumers need an absolute URL and resolve against `document.baseURI`: the upload worker, whose own base is a `blob:` URL, and the markdown image vocabulary, which emits only absolute `http(s)`, `blob`, or `data` destinations.
|
||||
|
||||
`verify-client-route-resolution` runs in `hygiene` and CI static checks over every browser source the client compiler face compiles. It governs request targets — the first argument of a request constructor, a `fetch`-shaped call, or a dynamic import; an assigned resource property; or a JSX `src`/`href` attribute — and rejects a root-absolute, protocol-relative, or absolute app route (`api`, `plugins`, `open-in-app` prefixes), and a relative app route resolved against a `location` read. A shared `*_PATH`/`*_ENDPOINT` key used as a request target must be stripped. The same pass checks the Host producers of browser references: a `url`, `src`, or `href` field stamped with a root-absolute app route is rejected, while route keys and response-table keys stay absolute and out of scope.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Banning every `location` read in browser code.** Rejected: identity comparisons, the preview's own query read, and the worker tunnel's host-relative mapping are all legitimate. The gate governs request targets, which is where a mount-bound route appears.
|
||||
|
||||
**Rewriting feature responses in the proxy.** Rejected: it duplicates the mount in deployment configuration for each route the proxy does not know about, and the document already resolves relative references.
|
||||
|
||||
## Consequences
|
||||
|
||||
The gate reads inline literal fragments only, so a composed reference (`comboReference(...)`, `artifact.url.slice(1)`) is not traced; `packages/client/modules/tests/node-half.client.spec.ts` owns the regression signal for composed graph rows, batch descriptors, and the source-map trailer. Preview pages do not expose their worker's file API at the document URL, so markdown local-file image URLs cannot reach that Host. [Session prose local media display](../feature/2026-09-07-session-prose-local-media-display.md) owns the image vocabulary.
|
||||
|
||||
## Testing
|
||||
|
||||
The gate's own spec pins a negative control per rule: interpolated targets (`${origin}/api/file`), protocol-relative and absolute forms, an unstripped shared key, a relative route resolved against a `location` read, and root-absolute producer fields. Each feature package's specs assert the relative route its browser half sends; `markdown-images.e2e.ts` keeps the local-image path covered end to end. The discovery spec pins which client projects contribute browser sources: a DOM project the Host aggregate also compiles contributes its plain `src/` unless it has a `src/client` half.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Agent Note: 功能路由遵循文档 base,并由门禁维持
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-17-web-feature-routes-and-route-gate.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[Web 应用自有路由相对文档寻址](2026-09-14-web-document-relative-app-routes.zh.md)让外壳、插件 bundle 与流在任一挂载下到达同一监听端口。功能包自有的路由仍寻址源站根:`/open-in-app/...`、deliverables 的 `present.*` 与 `changes.*` 路由、会话日志导出下载、上传 worker 的 `/api/session/uploadFileBinary` 提交,以及本地 markdown 图片路径背后的 `/api/file` 路由。在剥离前缀的代理之后,这些请求全部落空。也没有任何机制阻止新的浏览器侧引用再次把 bundle 绑定到单一挂载。
|
||||
|
||||
## 决策
|
||||
|
||||
每个功能生产者保留其绝对注册键,并在旁边派生浏览器形式:`OPEN_IN_APP_*_PATH`/`*_ROUTE`、`PRESENT_*_PATH`/`*_ROUTE`、`CHANGED_FILES_PATH` 与 `CHANGES_*_PATH`/`*_ROUTE`、`SESSION_LOG_EXPORT_PATH`/`_ROUTE`、`FILE_UPLOAD_PATH`/`_ROUTE`。浏览器代码只寻址 `*_ROUTE` 形式;页面安装的 Fetch 形载体(`FileUploadFetch`、`RpcFetch`)收到该相对路由,并相对自身基准解析。
|
||||
|
||||
两个消费方需要绝对 URL,并相对 `document.baseURI` 解析:上传 worker(其自身基准是 `blob:` URL)与 markdown 图片词汇表(只输出绝对的 `http(s)`、`blob` 或 `data` 目标)。
|
||||
|
||||
`verify-client-route-resolution` 在 `hygiene` 与 CI 静态检查中运行,覆盖 client 编译面编译的每个浏览器源文件。它治理请求目标——请求构造器、`fetch` 形调用或动态 import 的第一个参数;赋值的资源属性;JSX 的 `src`/`href` 属性——拒绝根绝对、协议相对或绝对的应用路由(`api`、`plugins`、`open-in-app` 前缀),以及相对 `location` 读取解析的相对应用路由。作为请求目标使用的共享 `*_PATH`/`*_ENDPOINT` 键必须先剥去前导斜杠。同一遍检查还覆盖宿主侧浏览器引用的生产者:以根绝对应用路由写入的 `url`、`src` 或 `href` 字段被拒绝,而路由键与响应表键保持绝对,不在范围内。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**在浏览器代码中禁止一切 `location` 读取。** 否决:身份比较、preview 自身查询读取、worker tunnel 的宿主相对映射都属正当。门禁治理请求目标——绑定挂载点的路由真正出现的位置。
|
||||
|
||||
**在代理中改写功能响应。** 否决:这会为代理并不知晓的每条路由在部署配置里重复挂载点,而文档本就能解析相对引用。
|
||||
|
||||
## 后果
|
||||
|
||||
门禁只读取内联字面片段,因此组合出来的引用(`comboReference(...)`、`artifact.url.slice(1)`)不被追踪;组合而成的启动图 row、批次描述符与 source-map trailer 的回归信号由 `packages/client/modules/tests/node-half.client.spec.ts` 持有。Preview 页面不会在文档 URL 下暴露其 worker 的文件 API,因此 markdown 本地文件图片 URL 无法到达该 Host。[会话正文本地媒体展示](../feature/2026-09-07-session-prose-local-media-display.zh.md)持有图片词汇表。
|
||||
|
||||
## 测试
|
||||
|
||||
门禁自身的 spec 为每条规则固定了负向对照:插值目标(`${origin}/api/file`)、协议相对与绝对形式、未剥前导斜杠的共享键、相对路由相对 `location` 读取解析,以及生产者中根绝对的引用字段。各功能包的 spec 断言其浏览器半侧发出的相对路由;`markdown-images.e2e.ts` 端到端维持本地图片路径的覆盖。发现逻辑的 spec 固定了哪些 client 项目贡献浏览器源文件:同时被 Host 聚合编译的 DOM 项目,若没有 `src/client` 半边,则其整个 `src/` 都计入。
|
||||
Loaded 100 of 1807 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user