diff --git a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml index 69cab56acd..8dfd8e352f 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml @@ -21,8 +21,8 @@ en: 57dbdd385f48bb3e zh: 5e15463fae4be582 /agent-note-bounded-recovery-for-transient-llm-request-failures/decision/bound-stalled-streams-where-they-can-be-stopped: - en: f7369086e87192b9 - zh: e2839ed8cc5c23b8 + en: 6a55107caa329eea + zh: 13a76e482739141e /agent-note-bounded-recovery-for-transient-llm-request-failures/decision/keep-attempts-separate-in-the-existing-log: en: 8f57db4bfd6e1c22 zh: 67ac62217bb68570 @@ -33,8 +33,8 @@ en: a6d0306740d470ee zh: 700bd4b0aa58f3e8 /agent-note-bounded-recovery-for-transient-llm-request-failures/verification: - en: db343f335a9d7cad - zh: 49b8300de55dcfeb + en: 7412edfd162c4a28 + zh: 64108a11e6194e35 /agent-note-bounded-recovery-for-transient-llm-request-failures/consequences: en: 08659e8606455ada zh: 862080f8df5f28ec diff --git a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md index db3a990025..8687f9d524 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md +++ b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md @@ -78,7 +78,7 @@ Each adapter exposes a validated `streamIdleTimeoutMs` configuration field with `@deepseek-ai/dsh-timeout` exposes a rearmable idle-watchdog primitive. One stable local `AbortController` is fused with the caller signal and passed to the transport for the whole adapter call; each outstanding `next()` arms the watchdog, resolution disarms it, and the next demand rearms it. Out-of-band transport activity calls `pulse()` to rearm an outstanding demand without yielding a value. Timeout aborts that stable controller with a capability-owned `TimeoutReason`, and `finally` clears the timer. The adapter classifies its watchdog as `TIMEOUT` and an earlier upstream abort as `ABORTED`. The existing one-shot `deadline()` is not presented as a sliding timer. -Boundary tests prove termination at both actual transports. The hand-written adapter aborts its fetch/reader, and the pi-ai adapter maps the stable signal through the SDK and proves the SDK closes the response. A timer that merely rejects a consumer promise while leaving the request running does not satisfy the contract. +Boundary tests prove termination at both actual transports. The hand-written adapter aborts its fetch/reader, and the pi-ai adapter maps the stable signal through the SDK and proves the SDK closes the response. A timer that merely rejects a consumer promise while leaving the request running does not satisfy the contract, so the watchdog does both: the signal aborts the request, and the deadline settles the outstanding demand even when the transport never observes that abort. ### Keep attempts separate in the existing log @@ -117,7 +117,7 @@ If recovery is exhausted, the final failure is stored once on `turn/end.reason` - Real agent-loop tests cover failure before chunks, partial chunks then failure, thrown and in-band failures, retry to success inside the same turn, exhaustion to structured `turn/end.reason`, and composition with `dsh-compaction-basic` context-overflow recovery. - The partial-chunk integration test proves failed chunks remain attributed to the failed step, no assistant message or tool side effect is committed for that step, and the successful retry records its own chunk seqs and provider/model route. - The plugin-owned `llm/retry` event is non-surface, survives a JSONL round trip, is ignored by message derivation, and drives TUI and Web retraction plus scheduled-retry rendering. Client tests cover complete wire validation, clock-independent countdown, cancellation versus completed retry labels, and trajectory attribution; keyless UI snapshots cover Web scheduling and success, real Web composition tests cover partial transport failure through recovery and exhausted recovery's terminal error row beside the settled retry chain, and ACP automation snapshots confirm that a discarded attempt stays off the wire while the recovered reply is emitted. -- Idle-watchdog tests prove the stable signal is rearmed only while `next()` is outstanding, disarmed during consumer think time and in `finally`, and classified separately from a total-call deadline and an earlier caller abort; adapter tests prove the signal stops the underlying request rather than merely detaching it. +- Idle-watchdog tests prove the stable signal is rearmed only while `next()` is outstanding, disarmed during consumer think time and in `finally`, and classified separately from a total-call deadline and an earlier caller abort; adapter tests prove the signal stops the underlying request rather than merely detaching it, and that the idle deadline settles a demand the transport abandoned. - Direct `ctx.llm.stream()` callers remain single-attempt and receive the same structured failure facts. ## Consequences diff --git a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md index e03fe37628..f39125afeb 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md @@ -78,7 +78,7 @@ agent loop(智能体循环)会将终止 finish 的 `LlmFailure` 传给 `agen `@deepseek-ai/dsh-timeout` 公开一个可重新布防的空闲看门狗原语。一个稳定的局部 `AbortController` 会与调用方信号融合,并在整个适配器调用期间传给传输层;每个尚未完成的 `next()` 都会布防看门狗,该调用完成时解除布防,下一次请求数据时再重新布防。带外传输活动会调用 `pulse()`,在不产生值的情况下为尚未完成的需求重新布防。超时会使用能力自身拥有的 `TimeoutReason` 中止这个稳定控制器,`finally` 则会清除定时器。适配器将自身看门狗归类为 `TIMEOUT`,将更早发生的上游中止归类为 `ABORTED`。现有的一次性 `deadline()` 不会被描述为滑动计时器。 -边界测试证明两个实际传输层都能终止。手写适配器会中止其 fetch/reader,pi-ai 适配器会把稳定信号映射到 SDK,并证明 SDK 会关闭响应。如果定时器只拒绝消费方 promise,却让请求继续运行,就不满足此约定。 +边界测试证明两个实际传输层都能终止。手写适配器会中止其 fetch/reader,pi-ai 适配器会把稳定信号映射到 SDK,并证明 SDK 会关闭响应。如果定时器只拒绝消费方 promise,却让请求继续运行,就不满足此约定;因此 watchdog 两者都做:信号中止请求,截止时间则在传输层始终不观察该中止时结算尚未完成的 demand。 ### 在现有日志中分隔尝试 @@ -117,7 +117,7 @@ agent loop(智能体循环)会将终止 finish 的 `LlmFailure` 传给 `agen - 真实 agent-loop 测试覆盖分片前失败、部分分片后失败、抛出及带内失败、在同一轮次内重试至成功、耗尽后写入结构化 `turn/end.reason`,以及与 `dsh-compaction-basic` 上下文溢出恢复的组合。 - 部分分片集成测试证明:失败分片仍归属于失败步骤,该步骤不会提交 assistant 消息或工具副作用,成功的重试会记录自己的分片 seq 和提供方/模型路由。 - 插件拥有的不进入表层的 `llm/retry` 事件可在 JSONL 往返后保留,被消息派生忽略,并驱动 TUI 和 Web 撤回及计划重试渲染。客户端测试覆盖完整的 wire 验证、独立于时钟的倒计时、已取消与已完成重试标签的区别以及轨迹归属;无密钥 UI 快照覆盖 Web 的调度与成功,真实 Web 组合测试覆盖部分传输失败直至恢复,以及耗尽后终态错误行与定格重试链并列的画面,ACP 自动化快照确认,被丢弃的尝试不会通过协议发出,而恢复后的回复会正常发出。 -- 空闲看门狗测试证明:只有 `next()` 尚未完成时才会重新布防稳定信号;在消费方思考期间及 `finally` 中会解除布防;它与总调用 deadline 以及更早发生的调用方中止分开分类。适配器测试证明该信号会终止底层请求,而不只是与其脱离。 +- 空闲看门狗测试证明:只有 `next()` 尚未完成时才会重新布防稳定信号;在消费方思考期间及 `finally` 中会解除布防;它与总调用 deadline 以及更早发生的调用方中止分开分类。适配器测试证明该信号会终止底层请求,而不只是与其脱离,并证明空闲截止时间会结算传输层放弃的 demand。 - `ctx.llm.stream()` 的直接调用方仍只尝试一次,并收到相同的结构化失败事实。 ## 后果 diff --git a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml index 670619cca9..a02253e5ac 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml @@ -9,14 +9,14 @@ en: 8ccef8fbd9260dfe zh: 106b2bafe969c676 /agent-note-prompt-variables-and-tool-guidance-ownership/decision: - en: c551d888282d9c73 - zh: 889b5ca3cdf25aff + en: 8d3c456a97365869 + zh: 983421e09cc90d5e /agent-note-prompt-variables-and-tool-guidance-ownership/decision/assemble-context: en: 46e67248692e333a zh: 51c5f2c6152c94d7 /agent-note-prompt-variables-and-tool-guidance-ownership/decision/prompt-variables: - en: 04b623ed2879fdf1 - zh: c111be62834fda93 + en: dc41d226f111d574 + zh: c6242bf30b2ed61c /agent-note-prompt-variables-and-tool-guidance-ownership/decision/persona-as-the-order-0-section: en: c2d32c0ac5d28759 zh: 59cd252ba6da6e59 @@ -30,11 +30,11 @@ en: 9a114359d2e8988b zh: ce39453408b2af7f /agent-note-prompt-variables-and-tool-guidance-ownership/out-of-scope: - en: bbfecc70d0b457fe - zh: 7f207a37a86484f4 + en: 06169acd6e002300 + zh: 605cf02f81df48bc /agent-note-prompt-variables-and-tool-guidance-ownership/shipped-invariants: en: 31ba63101fca5790 zh: af4ceefab2033142 /agent-note-prompt-variables-and-tool-guidance-ownership/consequences: - en: 993f17bf35f6a2e0 - zh: acb429176f0eebcc + en: 94c38eb61a2dcd4a + zh: 8a047857b6d3a4a3 diff --git a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md index 8f5542cb09..9bfcb0d86e 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md +++ b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md @@ -18,7 +18,7 @@ The assembled system prompt had four defects, all of one family: facts the harne ## Decision -**One principle: every fact in the prompt has exactly one owner.** The model name and workspace are config/session facts → the harness exposes them as variables and the persona references them. Per-tool semantics and when-to-use → the tool's `description`. Cross-call habits a description cannot carry → the tool package's prompt section. The product name and SDK identity line → the static `harness:identity` section. Deployment role and behavior → the deployment's persona. +**One principle: every prompt fact has exactly one owner.** The loop exposes route facts as variables referenced by the persona. The working-directory service supplies the current directory as user context. Per-tool semantics and when-to-use belong to the tool description; cross-call habits belong to its package prompt section. The product name and SDK identity line belong to the static `harness:identity` section. Deployment role and behavior belong to the deployment persona. ### Assemble context @@ -28,7 +28,7 @@ The assembled system prompt had four defects, all of one family: facts the harne Plugins register `{{name}}` values through `ctx.systemPrompt.variable(name, provider)`. Assembly resolves them into the waterfall-visible variable map. Rendering rejects unknown own-property references, registered providers that return `undefined`, malformed complete references, and unbalanced references that still contain a closing `}}`; a lone unmatched `{{` remains prose, and substituted values are not rescanned. Registration rejects invalid or duplicate variable names, and section names are unique. Sections may set `interpolate: false` to preserve generated documents literally; `tools:sdk` does so because tool descriptions and schemas may document their own `{{…}}` syntax. -`dsh-agent-loop` registers the two built-ins, both pure projections of the context agent: `model` (= `options.model`) and `cwd` (= `session.header.cwd`). The example personas write `powered by the {{model}} model` — the model name is stated once, in the `model:` config key. `{{cwd}}` is demonstrated in the ACP example only: every ACP session carries the client's cwd, while config-pre-created stdio agents have none (a persona claiming `{{cwd}}` there fails the turn — by design). The variables stay on the loop plugin (unlike the sections below): they are runtime facts of the agents THIS loop drives, and a replacement loop supplies its own. +`dsh-agent-loop` registers `provider` (= `options.provider`) and `model` (= `options.model`) as pure projections of the context agent. Example personas reference `{{model}}` so the route owns the model name. These variables remain on the loop plugin because they describe the agents it drives; a replacement loop supplies its own. The [Session working-directory decision](2026-09-13-session-working-directory.md) owns directory state and its literal user-context placement; the loop does not register `cwd`. ### Persona as the order-0 section @@ -54,7 +54,6 @@ Per-tool semantics and selection guidance live in tool descriptions. Prompt sect ## Out of scope - Further variables (`date`, platform, git state) — the registry makes each a one-line contribution by whichever plugin owns the fact; none is claimed here. -- A config `cwd` for pre-created stdio agents (would let the stdio persona use `{{cwd}}` and partition persistence by real path) — deferred until the session-cwd story is revisited. ## Shipped invariants @@ -68,5 +67,5 @@ Per-tool semantics and selection guidance live in tool descriptions. Prompt sect - Every fact in the assembled prompt now has exactly one owner, and the hand-maintained tool prose in leaf YAML is gone: loading or dropping a tool plugin no longer means editing any deployment's persona. - `{{model}}` reflects `AgentOptions.model` at assembly time. A plugin that switches models in the `agent/request` waterfall makes the prompt's claim stale for that step, and one that SUPPLIES the model there (options.model unset — the loop's documented fallback) leaves the variable valueless at render, failing a `{{model}}` persona before the waterfall runs. Both have the same remedy, and it is the ownership rule itself: the plugin that owns the late-bound model fact states it early on the `system-prompt/assemble` waterfall (`assembly.variables['model'] = …`) — one owner, both statements; a loop test pins the supply path end-to-end. Accepted. - While a bound provider is absent (not yet activated, unloaded, mid-HMR-reload), the subagent tool does not exist and a model request in that window simply lacks it. That is the honest state — the alternative was a registered tool whose description or execution could not be trusted. -- Strictness means a persona can fail a turn at render (e.g. `{{cwd}}` on a cwd-less session). The failure is contained — the turn ends `error`, the loop survives — and it is an authoring error we WANT loud. +- Strict interpolation rejects missing or valueless variables before a model request. The turn ends with `error` while the loop remains available for later turns; a persona referencing `{{model}}` requires its composing runtime to supply that value. - Inline escapes remain unsupported in interpolated text; literal sections need no escaping. PTC unit tests cover both modes and runtime languages, and the recorded `ptc-turn` scenario preserves tool-template examples in the model-visible prompt. diff --git a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md index 348ae36cf6..cf0f3f1c88 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md @@ -18,7 +18,7 @@ Status: implemented ## 决策 -**一条原则:提示词中的每个事实恰好有一个归属方。** 模型名称和工作区是配置/会话事实 → harness 将它们暴露为变量,persona 引用它们。每个工具的语义和何时使用 → 工具的 `description`。description 无法承载的跨调用习惯 → 包的提示词 section。产品名称和 SDK 身份说明 → 静态的 `harness:identity` section。部署角色与行为 → 部署的 persona。 +**一条原则:提示词中的每个事实恰好有一个归属方。** 循环将路由事实暴露为 persona 引用的变量。工作目录服务通过用户上下文提供当前目录。每个工具的语义和选择指导属于工具描述;跨调用习惯属于其包的提示词 section。产品名称和 SDK 身份说明属于静态的 `harness:identity` section。部署角色与行为属于部署的 persona。 ### 组装上下文 @@ -28,7 +28,7 @@ Status: implemented 插件通过 `ctx.systemPrompt.variable(name, provider)` 注册 `{{name}}` 值。组装过程将它们解析到 waterfall 可见的变量映射中。渲染阶段拒绝以下情况:引用未知的自有属性、已注册的提供方返回 `undefined`、格式错误的完整引用、以及仍包含闭合 `}}` 的不平衡引用;孤立的未匹配 `{{` 保留为行文,替换后的值不会被重新扫描。注册阶段拒绝无效或重复的变量名,section 名称也必须唯一。段可设置 `interpolate: false` 来原样保留生成的文档;`tools:sdk` 使用此设置,因为工具描述和 schema 可能会介绍自身的 `{{…}}` 语法。 -`dsh-agent-loop` 注册两个内置变量,均为上下文 agent 的纯投影:`model`(= `options.model`)和 `cwd`(= `session.header.cwd`)。示例 persona 写 `powered by the {{model}} model`——模型名称只在 `model:` 配置键中声明一次。`{{cwd}}` 仅在 ACP 示例中演示:每个 ACP 会话携带客户端的 cwd,而配置预创建的 stdio agent 没有 cwd(在那里声称 `{{cwd}}` 的 persona 会导致该轮次失败——这是有意为之)。变量留在 loop 插件上(不同于下面的 section):它们是本循环驱动的 agent 的运行时事实,替换循环自行提供自己的变量。 +`dsh-agent-loop` 注册 `provider`(= `options.provider`)和 `model`(= `options.model`),它们都是上下文 agent 的纯投影。示例 persona 引用 `{{model}}`,由路由拥有模型名称。这些变量保留在 loop 插件上,因为它们描述该循环驱动的 agent;替换循环自行提供自己的变量。[Session 工作目录决策](2026-09-13-session-working-directory.zh.md) 拥有目录状态及其字面用户上下文位置;循环不注册 `cwd`。 ### Persona 作为 order-0 section @@ -54,7 +54,6 @@ Status: implemented ## 不在范围内 - 更多变量(`date`、platform、git 状态):注册表使每个变量成为拥有该事实的插件的一行贡献;本 Agent Note 不认领任何一个。 -- 为预创建的 stdio agent 提供配置 `cwd`(可让 stdio persona 使用 `{{cwd}}` 并按真实路径分区持久化):推迟到会话 cwd 方案重新讨论时。 ## 交付的不变式 @@ -68,5 +67,5 @@ Status: implemented - 组装后的提示词中每个事实现在恰好有一个归属方,leaf YAML 中手工维护的工具行文已消除:加载或卸载一个工具插件不再需要编辑任何部署的 persona。 - `{{model}}` 在组装时反映 `AgentOptions.model`。如果一个插件在 `agent/request` waterfall 中切换模型,提示词对该步骤的声明就会过时;如果一个插件在那里提供模型(options.model 未设置——循环文档中记载的回退路径),变量在渲染时无值,包含 `{{model}}` 的 persona 会在 waterfall 运行前失败。两者的补救方式相同,就是归属规则本身:拥有延迟绑定模型事实的插件在 `system-prompt/assemble` waterfall 上提前声明它(`assembly.variables['model'] = …`)——一个归属方,两处声明;一个循环测试端到端固定了 supply 路径。已接受。 - 当一个已绑定的提供方不存在时(尚未激活、已卸载、HMR(热模块替换)重载中),subagent 工具不存在,该窗口内的模型请求中不会包含它。这是诚实的状态——替代方案是注册一个 description 或执行都不可信的工具。 -- 严格性意味着 persona 可能在渲染时导致轮次失败(例如在无 cwd 的会话上使用 `{{cwd}}`)。失败是受控的——该轮次以 `error` 结束,循环存活——且这是一个我们希望明确暴露的撰写错误。 +- 严格插值在模型请求之前拒绝缺失或无值的变量。该轮次以 `error` 结束,循环仍可服务后续轮次;引用 `{{model}}` 的 persona 要求组合它的运行时提供该值。 - 插值文本仍不支持行内转义;字面文本段无需转义。PTC 单元测试覆盖两种模式和运行时语言,录制的 `ptc-turn` 场景在模型可见的提示词中保留工具模板示例。 diff --git a/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml index 8a381fbd74..f92e73d1e4 100644 --- a/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml @@ -21,8 +21,8 @@ en: 333eab4967d753ec zh: dd7537bb82cc717f /agent-note-a-shared-timeout-deadline-primitive-with-hard-kill-left-to-each-capability/consequences: - en: 832f4dbd93a47f94 - zh: 65ada43f9f23a39b + en: 0f723f16456c343d + zh: 52b7aab9a6c4f7be /agent-note-a-shared-timeout-deadline-primitive-with-hard-kill-left-to-each-capability/alternatives-considered: - en: 7b3445622becbd36 - zh: bb358acf2ff28e26 + en: 0f3e3ecfa399c81a + zh: 951c2ee71047b11a diff --git a/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md index caae5fb40a..07ce41fa2e 100644 --- a/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md +++ b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md @@ -101,7 +101,7 @@ The signal only *notifies*; termination is always the listener's job, and the li - `SpawnSpec.timeoutMs` and `SpawnOutcome.timedOut`/`aborted` were removed rather than kept as always-zero/always-false vestiges: with `runBash` owning no timer and the executor owning classification, they were read nowhere. An always-0 field read by nothing is dead weight under the per-file coverage gate. - web_fetch shed its bespoke controller/timer/listener/reason-recovery; the classifier now keys off the deadline signal (`timeoutOf` + `aborted`) rather than the thrown error's shape, which is robust across both the request-phase reject-with-reason and the read-phase bare-`AbortError`. - `AbortSignal.any` and `using`/`Symbol.dispose` enter the repo for the first time here (Node ≥ 24 baseline, already met). -- Model streams now share one rearmable timer contract without turning a sliding idle interval into a total-call deadline or charging consumer think time. Adapters that can observe out-of-band transport activity may pulse an outstanding demand; suppressed activity remains invisible to the watchdog. The primitive still only notifies; adapter tests prove their transports observe its stable signal and terminate. +- Model streams now share one rearmable timer contract without turning a sliding idle interval into a total-call deadline or charging consumer think time. Adapters that can observe out-of-band transport activity may pulse an outstanding demand; suppressed activity remains invisible to the watchdog. The one-shot `deadline()` still only notifies; adapter tests prove their transports observe the rearmable watchdog's stable signal and terminate. The watchdog additionally settles the one demand it drives when its idle interval elapses, because a transport that drops the error of an aborted in-flight read would otherwise hold the caller past that interval. Out of scope, named to mark the boundary: `web_search` can gain an optional model-facing `timeout_ms` once its tool-schema/snapshot coverage is planned; the ripgrep-backed fs discovery tools ([packaged ripgrep search](../../archived/architecture/2026-08-01-packaged-ripgrep-search.md)) consume the same provider-owned deadline shape through `dsh-tool-call-timeout-policy` and `exec.signal`; a `tools/execute` waterfall middleware could arm a default deadline for every tool call by driving `exec.signal` — that would be a plugin that *consumes* this library and still only notifies, the hard kill remaining each capability's job. @@ -111,6 +111,6 @@ Out of scope, named to mark the boundary: `web_search` can gain an optional mode **Per-tool ad-hoc timeout, no shared code (the prior status quo, and Claude Code's choice).** Rejected because it was already producing divergence and duplicated correctness burden: web_fetch hand-rolled the exact controller/reason logic that future network/process-backed tools would each have to re-derive, and the fusion + `signal.reason` recovery are the error-prone parts. Claude Code tolerates full duplication; this repo has a single shared abort channel (`exec.signal` on every `execute`) that makes a small shared primitive strictly cleaner, so the cost/benefit differs. -**A `withTimeout(promise, ms)` wrapper instead of a signal factory.** Rejected because racing a promise against a timer resolves the *tool-call* promise on deadline without stopping the underlying work — the child process or fetch socket leaks on. Handing out a signal and requiring the capability to listen is what forces a real termination path to exist. This mirrors the "dispose must reach quiescence, not just request it" defensive rule. +**A `withTimeout(promise, ms)` wrapper instead of a signal factory.** Rejected because racing a promise against a timer resolves the *tool-call* promise on deadline without stopping the underlying work — the child process or fetch socket leaks on. Handing out a signal and requiring the capability to listen is what forces a real termination path to exist. This mirrors the "dispose must reach quiescence, not just request it" defensive rule. `idleWatchdog` is the deliberate exception, and only for the single iterator demand it drives: the signal it hands the transport still owns releasing that read (an abort destroys the socket), while the deadline ends the demand when the transport never settles it. **Keep separate bash timeout and cancellation triggers.** Rejected because one deadline signal removes the bespoke timer and standardizes classification. Racing causes report whichever abort arrived first, while the provider-owned termination path is independent of which cause won. diff --git a/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md index 1ecd7a0547..04569c51e5 100644 --- a/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md @@ -101,7 +101,7 @@ export function timeoutOf(x: AbortSignal | { reason?: unknown }, code?: string): - `SpawnSpec.timeoutMs` 和 `SpawnOutcome.timedOut`/`aborted` 被移除,而非作为始终为零/始终为 false 的残余保留:由于 `runBash` 不再拥有定时器且执行器负责分类,这些字段无处被读取。一个始终为 0 且无处读取的字段在逐文件覆盖率门禁下属于死代码。 - web_fetch 去除了其定制的 controller/timer/listener/reason-recovery;分类器现在基于 deadline 信号(`timeoutOf` + `aborted`)而非抛出错误的形状来判断,这在请求阶段的 reject-with-reason 和读取阶段的裸 `AbortError` 两种情况下都是健壮的。 - `AbortSignal.any` 和 `using`/`Symbol.dispose` 在此首次进入本仓库(Node ≥ 24 基线,已满足)。 -- 模型流现在共享一个可重启的定时器约定,不会把滑动的空闲间隔变成总调用截止时间,也不会计入消费方思考时间。能够观察到带外传输活动的适配器可以对尚未结算的 demand 调用 `pulse()`;被屏蔽的活动对 watchdog 仍不可见。该原语仍然只做通知;适配器测试证明其传输观察到稳定信号并终止。 +- 模型流现在共享一个可重启的定时器约定,不会把滑动的空闲间隔变成总调用截止时间,也不会计入消费方思考时间。能够观察到带外传输活动的适配器可以对尚未结算的 demand 调用 `pulse()`;被屏蔽的活动对 watchdog 仍不可见。一次性的 `deadline()` 仍然只做通知;适配器测试证明其传输观察到可重启 watchdog 的稳定信号并终止。此外,watchdog 会在空闲间隔走完时结算它所驱动的那个 demand:否则丢弃已中止读取错误的传输层会让调用方一直等到间隔之外。 以下内容不在本次范围内,列出以标明边界:`web_search` 可以在其工具 schema 和快照覆盖规划完成后获得可选的面向模型的 `timeout_ms`;基于 ripgrep 的文件系统发现工具([打包的 ripgrep 搜索](../../archived/architecture/2026-08-01-packaged-ripgrep-search.md))通过 `dsh-tool-call-timeout-policy` 和 `exec.signal` 消费同样的提供方自有 deadline 形状;`tools/execute` waterfall(瀑布式事件)中间件可以通过驱动 `exec.signal` 为每次工具调用设置默认 deadline——那将是一个*消费*本库的插件,仍然只做通知,硬终止仍是各能力自己的事。 @@ -111,6 +111,6 @@ export function timeoutOf(x: AbortSignal | { reason?: unknown }, code?: string): **每个工具各自实现超时,不共享代码(先前的现状,也是 Claude Code 的选择)。** 否决,因为它已经在产生分化和重复的正确性负担:web_fetch 手写了与未来网络/进程类工具各自需要重新推导的完全相同的 controller/reason 逻辑,而融合 + `signal.reason` 恢复正是容易出错的部分。Claude Code 容忍完全重复;本仓库有一个统一的共享 abort 通道(每次 `execute` 上的 `exec.signal`),使得采用一个小型共享原语明显更简洁,因此成本/收益不同。 -**用 `withTimeout(promise, ms)` 包装器代替信号工厂。** 否决,因为让 promise 与定时器竞争只是在截止时间到达时 resolve *工具调用*的 promise,而不会停止底层工作——子进程或 fetch socket 会泄漏。分发信号并要求能力监听,才能强制一条真实的终止路径存在。这与「dispose 必须达到完全停稳,而非仅仅请求它」的防御性规则一致。 +**用 `withTimeout(promise, ms)` 包装器代替信号工厂。** 否决,因为让 promise 与定时器竞争只是在截止时间到达时 resolve *工具调用*的 promise,而不会停止底层工作——子进程或 fetch socket 会泄漏。分发信号并要求能力监听,才能强制一条真实的终止路径存在。这与「dispose 必须达到完全停稳,而非仅仅请求它」的防御性规则一致。`idleWatchdog` 是有意保留的例外,且仅针对它所驱动的单个迭代器 demand:交给传输层的信号仍负责释放该读取(中止会销毁 socket),而当传输层永不结算时由截止时间结束该 demand。 **保留 bash 独立的超时和取消触发器。** 否决,因为一个 deadline 信号移除了定制定时器并标准化了分类。发生竞争时,报告先到达的那个 abort 作为原因,由提供方管理的终止路径不受哪个原因先胜出的影响。 diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml index 679008349c..5b400d15a2 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml @@ -21,8 +21,8 @@ en: c35ea4b858448b49 zh: 1f8ff0a9f82ca881 /agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/build-pipeline-and-artifacts: - en: 6356c325e36dc2a8 - zh: 63da32299cb356c9 + en: d3bbd24ead02af4f + zh: 64623d88bee4314b /agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/python-sdk-distribution-two-carriers-exe-for-production-node-for-development: en: 9bd60549d4a52ec0 zh: e2343b7210212ee0 diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md index b0853b3cf3..27d37ed541 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md @@ -48,7 +48,7 @@ The [Office kit](2026-09-14-independent-libreoffice-kit.md) and its installed de ### Build pipeline and artifacts -[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime--` under `dist-exe/` and copy it with its target-specific Office directory into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation. +[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime--` under `dist-exe/` and copy it with its target-specific Office directory into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted supplies package directories that staging makes symlink-free; staging removes nested workspace copies only when pnpm records the same dependency identity at the payload root, preserving singleton services; disabling workspace hoisting excludes unrelated workspace source packages; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation. CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) runs [installed-wheel validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) on Linux/Windows x64 for pull requests and Linux ARM64 plus both macOS architectures for master pushes. The [public publication workflow](../../archived/process/2026-08-11-python-publication-workflow.md) calls it for all five targets; `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / macos-x64 (`macos-15-intel`) / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, verifies their package and executable origin, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests and master pushes additionally run a real DeepSeek two-turn tool smoke on their selected targets; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS checks the runtime, ripgrep, and PTY helper architectures and verifies that all three deployment targets fit the wheel tag. A full five-target run retains six artifacts, each containing one release file: the platform-independent SDK wheel and five native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts `python-v` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and five native runtime wheels, then a single serialized job checks and publishes all six to the project PyPI registry. The [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows target and the explicit exclusion of Windows arm64. diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md index 154673cc76..4cc6e20459 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md @@ -48,7 +48,7 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真 ### 构建流水线与产物 -[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime--` 写入 `dist-exe/`,并将它与对应目标的 Office 目录一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。 +[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime--` 写入 `dist-exe/`,并将它与对应目标的 Office 目录一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 提供包目录,再由暂存步骤消除符号链接;只有 pnpm 在载荷根目录记录了相同依赖标识时,暂存步骤才删除对应的嵌套工作区副本,以保留服务单例;关闭工作区提升可排除无关的工作区源码包;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。 CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel 包验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)在拉取请求上运行 Linux/Windows x64,在 master 推送上运行 Linux ARM64 与两种 macOS 架构。[公开发布工作流](../../archived/process/2026-08-11-python-publication-workflow.md)调用它构建全部五个目标;`workflow_dispatch` 仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64、macos-x64(`macos-15-intel`)与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求与 master 推送还会在各自选定的目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则检查 runtime、ripgrep 与 PTY helper 的架构,并验证三个载荷的部署目标都符合 wheel 包标签。完整构建五个目标时保留 6 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 5 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v` 标签流水线,构建一个 SDK wheel 包和 5 个原生运行时 wheel 包,再由单个串行任务校验并将这 6 个文件发布到项目的 PyPI 注册表。[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 目标及对 Windows arm64 的明确排除。 diff --git a/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.i18n.yaml index 8622ce8f91..170036d0fb 100644 --- a/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.i18n.yaml @@ -9,8 +9,8 @@ en: fa4d3622fb18dd58 zh: 2b98640594c8344b /agent-note-one-harness-home-resolver/decision: - en: bbab99c7a1aae2fc - zh: 87453b98ca265699 + en: 816e5109718bc4ae + zh: af3f08f07c3cb761 /agent-note-one-harness-home-resolver/alternatives-considered: en: b1c9535540171d47 zh: 7df816d42945db94 diff --git a/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.md b/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.md index 1191c52bfa..a128d4fe40 100644 --- a/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.md +++ b/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.md @@ -21,7 +21,7 @@ One resolver owns the harness home, in `@deepseek-ai/dsh-home-paths`, single-roo explicit configured path > $DSH_HOME > ~/.dsh ``` -An empty or whitespace-only `$DSH_HOME` is treated as unset; otherwise `resolve('')` would silently place the home at the current working directory. The harness keeps all user data under one root; there is no XDG config/data/cache split. `dshHomePath(...segments)` joins deployment-owned children onto that root, and `dsh-app-boot` exposes it to Loader `!!js` config expressions before mounting entries, so shipped compositions derive `sessions` and `storages` without copying the resolver. `dshHomeDisplay()` names a resolved root symbolically for user-facing paths — `~/.dsh` for the default home, `$DSH_HOME` for any configured home — so the user-global `AGENTS.md` label never leaks an absolute machine path. It replaces agent-instructions's bespoke default-vs-`$DSH_HOME` check. +An empty or whitespace-only `$DSH_HOME` is treated as unset; otherwise `resolve('')` would silently place the home at the current working directory. The harness keeps all user data under one root; there is no XDG config/data/cache split. `dshHomePath(...segments)` joins deployment-owned children onto that root, and `dsh-app-boot` exposes it to Loader `!!js` config expressions before mounting entries, so shipped compositions derive `sessions` and `storages` without copying the resolver. `dshHomeDisplay()` names a resolved root symbolically for user-facing paths — `~/.dsh` for the default home, `$DSH_HOME` for any configured home — so the user-global `AGENTS.md` label never leaks an absolute machine path. It replaces agent-instructions's bespoke default-vs-`$DSH_HOME` check. Home is process policy rather than row configuration: `dsh-agent-instructions` accepts no home override, so a composition cannot point instruction loading at a root that sessions, skills, and storage would not read. `resolveAgentsHome()` and `agentsHomeDisplay()` apply the same precedence, tilde expansion, and symbolic labelling to the shared agent configuration root (`$DSH_AGENTS_HOME` or `~/.agents`), which stays outside the harness home because other agent tools share it ([shared agents root](2026-10-05-shared-agents-root-instructions.md)); `dsh-skill-filesystem` still accepts its own `dshHome` and `agentsHome` fields, which select its own skill roots and resolve through the shared resolver in the instruction-loading change. `dshCachePath(...segments)` derives paths below the resolved home's `cache` directory. An initial `{ dshHome }` option preserves a provider's explicit home override. It resolves paths without creating directories; callers own directory creation. `attachment-local` uses this helper for regenerable request-image variants while retaining durable attachment objects in their versioned storage tree, so clearing the cache cannot remove Session attachments. Existing request-image cache entries are left in place and are not read or copied; a cache miss regenerates the variant from its durable attachment. diff --git a/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.zh.md b/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.zh.md index cefa124b7c..3f02908f86 100644 --- a/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.zh.md @@ -21,7 +21,7 @@ Status: implemented explicit configured path > $DSH_HOME > ~/.dsh ``` -空或仅含空白的 `$DSH_HOME` 被当作未设置处理;否则,`resolve('')` 会悄悄把 home 落在当前工作目录。harness 把所有用户数据都放在同一个根目录下;不存在 XDG 的 config/data/cache 拆分。`dshHomePath(...segments)` 将部署负责的子路径拼接到该根目录下,`dsh-app-boot` 在挂载条目前向 Loader `!!js` 配置表达式暴露它,因此出厂组合无需复制解析器即可派生 `sessions` 和 `storages`。`dshHomeDisplay()` 为面向用户的路径以符号形式命名已解析的根目录——默认 home 显示为 `~/.dsh`,任何已配置的 home 显示为 `$DSH_HOME`——这样用户全局的 `AGENTS.md` 标签就绝不会泄露机器上的绝对路径。它取代了 agent-instructions 中自定义的「默认值 vs `$DSH_HOME`」判断。 +空或仅含空白的 `$DSH_HOME` 被当作未设置处理;否则,`resolve('')` 会悄悄把 home 落在当前工作目录。harness 把所有用户数据都放在同一个根目录下;不存在 XDG 的 config/data/cache 拆分。`dshHomePath(...segments)` 将部署负责的子路径拼接到该根目录下,`dsh-app-boot` 在挂载条目前向 Loader `!!js` 配置表达式暴露它,因此出厂组合无需复制解析器即可派生 `sessions` 和 `storages`。`dshHomeDisplay()` 为面向用户的路径以符号形式命名已解析的根目录——默认 home 显示为 `~/.dsh`,任何已配置的 home 显示为 `$DSH_HOME`——这样用户全局的 `AGENTS.md` 标签就绝不会泄露机器上的绝对路径。它取代了 agent-instructions 中自定义的「默认值 vs `$DSH_HOME`」判断。home 属于进程策略而非配置行:`dsh-agent-instructions` 不接受 home 覆盖,因此组合无法把指令加载指向会话、skill 与存储不会读取的根目录。`resolveAgentsHome()` 与 `agentsHomeDisplay()` 对共享 agent 配置根目录(`$DSH_AGENTS_HOME` 或 `~/.agents`)采用相同的优先级、波浪号展开与符号化标签;该根目录位于 harness home 之外,因为其他 agent 工具会共享它([共享 agents 根目录](2026-10-05-shared-agents-root-instructions.zh.md));`dsh-skill-filesystem` 仍接受自己的 `dshHome` 与 `agentsHome` 字段,用于选择它自己的 skill 根目录,并在指令加载改动中改用共享解析器。 `dshCachePath(...segments)` 在解析出的主目录下的 `cache` 目录中派生路径。首个 `{ dshHome }` 选项保留提供方显式配置的主目录覆盖值。它只解析路径,不创建目录;目录创建由调用方负责。`attachment-local` 将此函数用于可重新生成的请求图片版本,持久附件对象仍保留在其带版本的存储树中,因此清空缓存不会删除 Session 附件。已有请求图片缓存条目保留在原处,不再读取或复制;缓存未命中时从持久附件重新生成请求版本。 diff --git a/.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.i18n.yaml index 134b7e8571..b790e7c558 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.i18n.yaml @@ -12,8 +12,8 @@ en: f0d0d5ba5a9066ad zh: fbdab3731c33eb26 /agent-note-the-ptc-runtime-python-fd-3-frame-protocol/wire-contract: - en: 913f9465320dc191 - zh: f8e0338645d6c204 + en: 1225a32af91fcf84 + zh: 1dab4ecbd9ff7e34 /agent-note-the-ptc-runtime-python-fd-3-frame-protocol/mirror-alignment: en: 26128b65f2763920 zh: df24359faebcbfed diff --git a/.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.md b/.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.md index 61d644903f..b22ea4cddf 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.md +++ b/.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.md @@ -26,7 +26,7 @@ The package ships the runtime alongside the protocol; it remains independently b ## Wire contract -Frames are JSON-lines on fd 3, one object per line, leaving stdout/stderr free for the program's own output. Child → host: `boot-ack`, `call`, `log`, `done`. Host → child: `boot` (first frame), `run` (after `boot-ack`), and one `reply` per `call`. The `log` frame's `truncated` flag marks the frame that IS the child ledger's own truncation marker, so the host stops capturing at the same point the child did instead of inferring it from its own budget. The `log` frame's `open` flag marks an unterminated line committed by an explicit flush: the host holds it and appends the next frame to the same entry, so an explicit flush followed by more text reads back as one line rather than a fake newline. The one exception is truncation: when a later over-budget frame trips the ledger, the already-billed prefix is committed as its own entry and the truncation marker follows it (marker last, no re-charge). The merged entry's wire cost is billed exactly once, split incrementally across its fragments on both sides (O(k) for k fragments, never a re-walk of the whole hold): the FIRST fragment pays the full JSON-string cost plus the separator, each continuation and the closing frame pay only their content; the host's exact-cost caps are `logBudget - 1` for a first fragment (the ledger's reserved byte, matching `admit`) and `logBudget + 2` for a continuation or closing frame (billed without the two quotes), and `jsonStringCostUpTo` returns `undefined` below a 2-byte cap; the child keys its split billing off `_open_started` alone, so a closing frame bills as the merged tail. `done.error.kind` is one of `exception`, `invalid-output`, `output-limit`; wall/CPU budgets, aborts, and substrate death are observed host-side, not carried as frames. +Frames are JSON-lines on fd 3, one object per line, leaving stdout/stderr free for the program's own output. Child → host: `boot-ack`, `call`, `log`, `done`. Host → child: `boot` (first frame), `run` (after `boot-ack`), and one `reply` per `call`. The `log` frame's `truncated` flag marks the frame that IS the child ledger's own truncation marker, so the host stops capturing at the same point the child did instead of inferring it from its own budget. The `log` frame's `open` flag marks an unterminated line committed by an explicit flush: the host holds it and appends the next frame to the same entry, so an explicit flush followed by more text reads back as one line rather than a fake newline. The one exception is truncation: when a later over-budget frame trips the ledger, the already-billed prefix is committed as its own entry and the truncation marker follows it (marker last, no re-charge). The merged entry's wire cost is billed exactly once, split incrementally across its fragments on both sides (O(k) for k fragments, never a re-walk of the whole hold): the FIRST fragment pays the full JSON-string cost plus the separator, each continuation and the closing frame pay only their content; the host's exact-cost caps are `logBudget - 1` for a first fragment (the ledger's reserved byte, matching `admit`) and `logBudget + 2` for a continuation or closing frame (billed without the two quotes), and `jsonStringCostUpTo` returns `undefined` below a 2-byte cap; the child keys its split billing off `_open_started` alone, so a closing frame bills as the merged tail. `done.error.kind` is one of `exception`, `invalid-output`, `output-limit`, or `timeout` from the CPU check after model code returns; wall deadlines, aborts, and substrate death are observed host-side. The post-return CPU check attempts a diagnostic on fd 3 before mandatory signal termination because a sandbox launcher can convert that signal into an ambiguous exit code. Ordinary soft-limit expiry retains default `SIGXCPU` termination without a Python handler, so native calls cannot postpone enforcement until Python resumes. These frames remain untrusted outcome reports; kernel resource limits enforce the CPU ceiling independently. ## Mirror alignment diff --git a/.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.zh.md b/.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.zh.md index d01897161c..a8e7ea4ef4 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-31-ptc-runtime-python-fd3-protocol.zh.md @@ -26,7 +26,7 @@ CPython PTC 运行时位于 `packages/experimental/ptc-runtime-python`,以 `@d ## Wire contract -帧是 fd 3 上的 JSON-lines,每行一个对象,让 stdout/stderr 空出给程序自己的输出。Child → host:`boot-ack`、`call`、`log`、`done`。Host → child:`boot`(首帧)、`run`(在 `boot-ack` 之后)、以及每个 `call` 对应一个 `reply`。`log` 帧的 `truncated` 标志标记那个本身就是子进程 ledger 截断标记的帧,使 host 在与子进程相同的点停止捕获,而不是从自己的预算去推断。`log` 帧的 `open` 标志标记由显式 flush 提交的未结束行:宿主持有它并把下一个帧追加到同一条目,因此显式 flush 后接更多文本读回为一行而不是假换行。唯一例外是截断:当后续超预算帧触发账本时,已计费的前缀作为独立条目先提交,截断 marker 跟在后面(marker 保持末位,无重复计费)。合并条目的线上成本恰好计费一次,在两侧按片段增量分摊(k 个片段 O(k),绝不对整个持有重走):首片段付完整 JSON 字符串成本加分隔符,每个续接与闭合帧只付内容;宿主精确成本 cap 是首片段 `logBudget - 1`(账本预留字节,与 `admit` 一致)、续接或闭合帧 `logBudget + 2`(不含两个引号计费),且 `jsonStringCostUpTo` 在低于 2 字节 cap 时返回 `undefined`;子进程按 `_open_started` 单独键控拆分计费,因此闭合帧按合并尾部计费。`done.error.kind` 是 `exception`、`invalid-output`、`output-limit` 之一;wall/CPU 预算、abort、substrate 死亡都在 host 侧观测,不作为帧携带。 +帧是 fd 3 上的 JSON-lines,每行一个对象,让 stdout/stderr 空出给程序自己的输出。Child → host:`boot-ack`、`call`、`log`、`done`。Host → child:`boot`(首帧)、`run`(在 `boot-ack` 之后)、以及每个 `call` 对应一个 `reply`。`log` 帧的 `truncated` 标志标记那个本身就是子进程 ledger 截断标记的帧,使 host 在与子进程相同的点停止捕获,而不是从自己的预算去推断。`log` 帧的 `open` 标志标记由显式 flush 提交的未结束行:宿主持有它并把下一个帧追加到同一条目,因此显式 flush 后接更多文本读回为一行而不是假换行。唯一例外是截断:当后续超预算帧触发账本时,已计费的前缀作为独立条目先提交,截断 marker 跟在后面(marker 保持末位,无重复计费)。合并条目的线上成本恰好计费一次,在两侧按片段增量分摊(k 个片段 O(k),绝不对整个持有重走):首片段付完整 JSON 字符串成本加分隔符,每个续接与闭合帧只付内容;宿主精确成本 cap 是首片段 `logBudget - 1`(账本预留字节,与 `admit` 一致)、续接或闭合帧 `logBudget + 2`(不含两个引号计费),且 `jsonStringCostUpTo` 在低于 2 字节 cap 时返回 `undefined`;子进程按 `_open_started` 单独键控拆分计费,因此闭合帧按合并尾部计费。`done.error.kind` 是 `exception`、`invalid-output`、`output-limit` 或模型代码返回后 CPU 检查产生的 `timeout` 之一;wall 截止、abort 和 substrate 死亡在 host 侧观测。返回后的 CPU 检查会在强制信号终止前尝试通过 fd 3 报告诊断,因为沙箱 launcher 可能把该信号转换为有歧义的退出码。普通软限制到期保留默认的 `SIGXCPU` 终止行为,不安装 Python handler,因此原生调用不能把强制执行延后到 Python 恢复运行。这些帧仍是不可信的结果报告;内核资源限制独立强制执行 CPU 上限。 ## Mirror alignment diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml index 9ac900a05f..f32414668b 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml @@ -9,14 +9,14 @@ en: 1fe344c95a0de36d zh: 656c3e0f1117f64e /agent-note-forked-children-preserve-the-parent-request-prefix/decision: - en: 7461852090348b34 - zh: ad6c9e0ce65a3e85 + en: b70aafc9ce30dd00 + zh: 74e3ff915bd968a9 /agent-note-forked-children-preserve-the-parent-request-prefix/alternatives-considered: en: 80504ffd8331e6c5 zh: 193261bdde24b4bb /agent-note-forked-children-preserve-the-parent-request-prefix/consequences: - en: 54c4ce3ee0fee4d2 - zh: 8a63fb3486c76551 + en: 0738d36f17c889b5 + zh: b71ee47f75d6d4e0 /agent-note-forked-children-preserve-the-parent-request-prefix/consequences/accepted-risks: en: f634caa9973eaedb zh: 820291096c8e7352 diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md index 0edf6ca934..4b52122e86 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md @@ -12,9 +12,11 @@ The earlier shipped composition avoided this mismatch by keeping forked children ## Decision +The shared activation entry point, external execution ownership, and caller-versus-parent result delivery are governed by [Unified subagent activations](../simplification/2026-09-17-unified-subagent-activations.md). This record retains the independent rationale described below. + The model-facing `send_message` tool is registered globally for every Agent in a composition. A continuable forked child therefore receives the same tool name, description, schema, and ordering as its parent. Its initial task is appended after the inherited Session seed, and the task includes the direct parent id plus guidance to return results with `send_message({ agent_id, message })` when that tool is visible to the child. -The base and headless compositions retain one-shot fork as their conservative lifecycle policy. The `cordis`, `standard`, and `ptc` CLI presets may bind fork to the continuable lifecycle because that binding no longer inserts child-only request-head fields. `ForkInProcessProvider.prepareContinuable()` and `ctx.subagents.startContinuable()` remain the implementation seam for those presets. +The base bundle and the `cordis`, `standard`, and `ptc` CLI presets bind fork to the continuable lifecycle. Base-backed profiles, including headless, inherit that default. `ForkInProcessProvider.prepareContinuable()` and `ctx.subagents.startActivation()` preserve the same request-prefix behavior across these compositions. Byte-identical prefix reuse is qualified by explicit deployment choices. A fork delegation that applies a child persona or `toolFilter` may still change the request head. In particular, filtering out `send_message` removes both the schema and the return guidance from the child; the runtime does not bypass an explicit allow-list. @@ -32,8 +34,8 @@ Byte-identical prefix reuse is qualified by explicit deployment choices. A fork - Parent and continuable-fork child expose byte-identical ordered tool schemas when the delegation does not request a persona or tool filter. - The inherited Session seed precedes the child's initial task and return guidance. -- The base and headless profiles keep one-shot fork, while selected CLI presets exercise continuable fork without a child-only request-head addition. -- A child sends zero or more messages to its direct parent explicitly; its final answer is not implicitly copied. The manager-owned settlement notice remains unconditional and separate. +- The base bundle and full CLI presets use local continuable fork for both parent and caller delivery, without a child-only request-head addition. +- The child can send messages to its direct parent through `send_message`. Parent delivery adds a separate runtime settlement notice; caller delivery returns the result through the activation receipt. - Keyless snapshots and package tests pin schema equality, inherited-history ordering, parent-id guidance, and child-to-parent delivery through the same `send_message` operation used in the other direction. ### Accepted risks diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md index c2e3504bd2..2c426a1e7c 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md @@ -12,9 +12,11 @@ fork 与 spawn 的差异在于:fork 会用 parent 已完成轮次的前缀作 ## 决策 +共享 activation 入口、外部执行所有权,以及调用方与父级之间的结果投递选择由[统一 subagent activation](../simplification/2026-09-17-unified-subagent-activations.zh.md) 决策拥有。本记录保留下述独立理由。 + 面向模型的 `send_message` 工具在组合中的每个 Agent 上全局注册。因此,可继续 fork child 获得与 parent 相同的工具名称、描述、schema 和顺序。其初始任务追加在继承的 Session 种子之后;当 child 可以看到该工具时,任务还会包含直接 parent id,以及使用 `send_message({ agent_id, message })` 返回结果的指引。 -base 与 headless 组合保留 one-shot fork 作为其保守生命周期策略。`cordis`、`standard` 和 `ptc` CLI preset 可以把 fork 绑定为可继续生命周期,因为该绑定不再插入 child-only 请求头字段。`ForkInProcessProvider.prepareContinuable()` 与 `ctx.subagents.startContinuable()` 仍是这些 preset 使用的实现 seam。 +base 组合包与 `cordis`、`standard` 和 `ptc` CLI preset 将 fork 绑定为可继续生命周期。基于 base 的 profile(包括 headless)继承这一默认值。`ForkInProcessProvider.prepareContinuable()` 与 `ctx.subagents.startActivation()` 在这些组合中保持相同的请求前缀行为。 逐字节相同的前缀复用受显式部署选择约束。配置 child persona 或 `toolFilter` 的 fork 委派仍可能改变请求头。尤其是过滤掉 `send_message` 时,child 会同时失去该 schema 与返回指引;运行时不会绕过显式 allow-list。 @@ -32,8 +34,8 @@ base 与 headless 组合保留 one-shot fork 作为其保守生命周期策略 - 未请求 persona 或工具过滤时,parent 与可继续 fork child 暴露逐字节相同且顺序一致的工具 schema。 - 继承的 Session 种子位于 child 初始任务及其返回指引之前。 -- base 与 headless profile 保持 one-shot fork;选定的 CLI preset 会在没有 child-only 请求头增量的前提下使用可继续 fork。 -- child 显式向直接 parent 发送零条或多条消息;最终回答不会被隐式复制。管理器负责的结算通知仍然无条件执行,并且与 Agent 消息分离。 +- base 组合包与完整 CLI preset 对 parent 和 caller 投递均使用本地可继续 fork,不增加 child-only 请求头字段。 +- child 可以通过 `send_message` 向直接 parent 发消息。parent 投递会追加独立的运行时结算通知;caller 投递通过 activation 回执返回结果。 - keyless snapshot 与包测试固定 schema 相等性、继承历史顺序、parent-id 指引,以及通过同一个 `send_message` 操作完成的 child-to-parent 投递。 ### 已接受风险 diff --git a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml index 8e75ca571c..981cb258f7 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml @@ -24,8 +24,8 @@ en: e9f140cd22670dfa zh: bf85cfe9c12a0345 /agent-note-package-and-update-the-electron-desktop-application/security-and-release-policy: - en: 7fe7dae6bfff28ae - zh: 6e8deddf5e669a17 + en: 97ee608e96fc3602 + zh: 932b98e7d344cd87 /agent-note-package-and-update-the-electron-desktop-application/implementation: en: 041a6d35ed61b149 zh: 617c6b90a6f4fa4d diff --git a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md index db00413040..26a6b9ba99 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md +++ b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md @@ -81,7 +81,7 @@ Test releases require `DOWNLOAD_TEST_RELEASE_ID` from the platform dotenv file. Core dsh and the private Desktop Host come only from the signed application resource tree. Plugin installation forwards package specs to pnpm, including local and remote sources, but never accepts raw pnpm commands. pnpm owns dependency resolution and the profile’s `allowBuilds` policy; the Host loads activated bundles. -Electron release artifacts are signed; macOS artifacts are notarized. Package and upload commands read release settings from the Git-ignored target `.env.windows` or `.env.macos`; subprocesses receive the fields selected by orchestration. The target file exclusively owns release fields so stale shell or system credentials cannot override local selection, and loading does not mutate the parent environment. Packaging validates the mode-specific application ID, update origin, signing identity, and local files before builds, downloads, or release-record cleanup; macOS also requires one complete notarization strategy. The separate `check:package` command runs the same validation without accessing the token or Apple; actual signing and notarization still verify authentication. Configuration loading rejects missing or malformed identifiers and incomplete notarization credentials, while macOS packaging requires signing so certificate discovery cannot silently select another installed identity or emit an unsigned release. Runtime preparation verifies the exact Authority and Team ID plus the timestamp and hardened-runtime flags on every embedded Mach-O file. An after-sign hook performs Apple's deep strict application verification and requires the same leaf Authority and Team ID before artifact creation continues. The fixed-target installer command uses [isolated App copies for parallel notarization](../process/2026-09-09-parallel-macos-notarization.md): the ZIP contains a stapled App, while the signed DMG carries the ticket covering its unstapled inner App. The DMG artifact-completion hook requires the configured identity, a valid ticket, and Gatekeeper acceptance. Both artifact lanes must succeed before the command promotes their outputs and writes the release completion record; directory-only commands still notarize and staple the App. DMG blockmaps are disabled because macOS updates consume the signed ZIP, and stapling would otherwise invalidate an already-generated DMG blockmap. +Electron release artifacts are signed; macOS artifacts are notarized. Release package and upload commands read release settings from the Git-ignored target `.env.windows` or `.env.macos`; subprocesses receive the fields selected by orchestration. The target file exclusively owns release fields so stale shell or system credentials cannot override local selection, and loading does not mutate the parent environment. Packaging validates the mode-specific application ID, update origin, signing identity, and local files before builds, downloads, or release-record cleanup; macOS also requires one complete notarization strategy. The separate `check:package` command runs the same validation without accessing the token or Apple; actual signing and notarization still verify authentication. Configuration loading rejects missing or malformed identifiers and incomplete notarization credentials, while macOS release packaging requires signing so certificate discovery cannot silently select another installed identity or emit an unsigned release. Runtime preparation verifies the exact Authority and Team ID plus the timestamp and hardened-runtime flags on every embedded Mach-O file. An after-sign hook performs Apple's deep strict application verification and requires the same leaf Authority and Team ID before artifact creation continues. The fixed-target installer command uses [isolated App copies for parallel notarization](../process/2026-09-09-parallel-macos-notarization.md): the ZIP contains a stapled App, while the signed DMG carries the ticket covering its unstapled inner App. The DMG artifact-completion hook requires the configured identity, a valid ticket, and Gatekeeper acceptance. Both artifact lanes must succeed before the command promotes their outputs and writes the release completion record; directory-only commands still notarize and staple the App. DMG blockmaps are disabled because macOS updates consume the signed ZIP, and stapling would otherwise invalidate an already-generated DMG blockmap. The [pinned osx-sign patch](../../../../patches/@electron__osx-sign@1.3.3.patch) uses `lstat` in both published module builds, so Framework file and directory aliases do not trigger duplicate signing. The patch remains necessary until the selected upstream release skips those aliases. PAK files are resources sealed by the enclosing bundle; individual signatures add serial timestamp requests without additional resource integrity. Desktop preserves all locale files and skips only their standalone signatures. Executable code retains Developer ID signatures, secure timestamps, and hardened runtime. The [signer traversal regression](../../../../apps/desktop/tests/macos-signing-walk.spec.ts) exercises the installed dependency with real Framework aliases; release qualification still requires strict application verification, notarization, and startup. @@ -89,7 +89,7 @@ Windows release packaging supplies the public EV leaf certificate named by `DSH_ Windows package invocations force `ELECTRON_BUILDER_7Z_FILTER=BCJ`. The bundled 7-Zip 24.09 encoder automatically selects ARM64 filters for ARM64 PE files, but the NSIS decoder from `nsis-resources-3.4.1` omits those entries during extraction. A native extraction probe with the actual NSIS plugin loses both `node-pty` ARM64 binaries under automatic filtering and restores both byte-for-byte with BCJ. Keeping a compatible filter preserves dependency contents and runtime integrity instead of removing architecture-specific files or weakening verification. -Local Windows installation testing uses an explicit `--unsigned` package invocation with the same build and runtime preparation. It strips certificate inputs, isolates artifacts in `unsigned-artifacts`, and omits updater configuration and the release completion record. The regular package command explicitly selects signed mode even when its parent environment requests unsigned mode. This separation permits installation diagnosis without an EV token while preventing local test output from qualifying for release upload. +Local Windows and macOS installation testing uses an explicit `--unsigned` package invocation with the same build and runtime preparation. It strips certificate inputs, isolates artifacts in `unsigned-artifacts`, and omits updater configuration and the release completion record. macOS local builds ignore dotenv files and inherited release settings, use the standard application ID, and omit mandatory-update policy so local testing needs no deployment configuration. Their App receives a certificate-free ad-hoc signature; their DMG is unsigned and unnotarized. Windows local builds retain their platform configuration requirements. The regular package command explicitly selects signed mode even when its parent environment requests unsigned mode. This separation permits installation diagnosis without release credentials while preventing local test output from qualifying for release upload. Windows application replacement follows the [directory-installation reference](../../../../apps/desktop/README.md): extract beside the destination with a command-line tool that returns failure status, then rename complete directories on the same volume. The installer retains the old directory through staging and restores it if promotion fails. Registration, shortcuts, and the signed uninstaller remain owned by electron-builder. diff --git a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md index 276355c3c4..21ef91d0e0 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md @@ -81,7 +81,7 @@ Electron 更新只使用一个 `electron-updater` 发布流和签名 `electron-b 核心 dsh 和私有 Desktop Host 只来自签名应用的资源树。插件安装把包规格交给 pnpm,包括本地和远程来源,但不接受原始 pnpm 命令。pnpm 负责依赖解析和 profile 的 `allowBuilds` 策略;Host 加载已启用的 bundle。 -Electron 发布产物必须签名;macOS 产物必须公证。打包与上传命令从 Git 忽略的目标 `.env.windows` 或 `.env.macos` 读取发布配置,子进程通过编排器选择的环境字段接收配置。目标文件是发布字段的唯一来源,避免旧的 shell 或系统凭据覆盖本地选择;配置加载不修改父进程环境。打包在构建、下载或清理发布记录前校验该模式必需的应用 ID、更新地址、签名身份及本地文件,macOS 还要求一套完整公证凭据。单独的 `check:package` 执行同一校验而不访问 Token 或 Apple;凭据真实性仍由实际签名与公证验证。配置加载会拒绝缺失或格式错误的标识符和不完整的公证凭据,macOS 打包还会强制签名,避免证书发现过程静默选择其他已安装身份或生成未签名发布。运行时准备会验证每个内嵌 Mach-O 文件的精确 Authority 与 Team ID,以及时间戳和 hardened-runtime 标记。签名后钩子会执行 Apple 的深度严格应用验证,并要求同一叶证书 Authority 与 Team ID 完全匹配,验证通过后才继续生成产物。固定目标安装包命令使用[隔离的 App 副本并行公证](../process/2026-09-09-parallel-macos-notarization.zh.md):ZIP 包含已钉票的 App,签名 DMG 则携带覆盖其中未钉票 App 的票据。DMG 的 artifact-completion hook 要求其使用配置的身份、具备有效票据并通过 Gatekeeper。只有两条产物流都成功,命令才会移入其输出并写入发布完成记录;仅生成目录的命令仍会公证 App 并钉票。macOS 更新使用签名 ZIP,因此 DMG 不生成 blockmap;否则钉票会让已经生成的 DMG blockmap 失效。 +Electron 发布产物必须签名;macOS 产物必须公证。发布打包与上传命令从 Git 忽略的目标 `.env.windows` 或 `.env.macos` 读取发布配置,子进程通过编排器选择的环境字段接收配置。目标文件是发布字段的唯一来源,避免旧的 shell 或系统凭据覆盖本地选择;配置加载不修改父进程环境。打包在构建、下载或清理发布记录前校验该模式必需的应用 ID、更新地址、签名身份及本地文件,macOS 还要求一套完整公证凭据。单独的 `check:package` 执行同一校验而不访问 Token 或 Apple;凭据真实性仍由实际签名与公证验证。配置加载会拒绝缺失或格式错误的标识符和不完整的公证凭据,macOS 发布打包还会强制签名,避免证书发现过程静默选择其他已安装身份或生成未签名发布。运行时准备会验证每个内嵌 Mach-O 文件的精确 Authority 与 Team ID,以及时间戳和 hardened-runtime 标记。签名后钩子会执行 Apple 的深度严格应用验证,并要求同一叶证书 Authority 与 Team ID 完全匹配,验证通过后才继续生成产物。固定目标安装包命令使用[隔离的 App 副本并行公证](../process/2026-09-09-parallel-macos-notarization.zh.md):ZIP 包含已钉票的 App,签名 DMG 则携带覆盖其中未钉票 App 的票据。DMG 的 artifact-completion hook 要求其使用配置的身份、具备有效票据并通过 Gatekeeper。只有两条产物流都成功,命令才会移入其输出并写入发布完成记录;仅生成目录的命令仍会公证 App 并钉票。macOS 更新使用签名 ZIP,因此 DMG 不生成 blockmap;否则钉票会让已经生成的 DMG blockmap 失效。 [固定版本的 osx-sign 补丁](../../../../patches/@electron__osx-sign@1.3.3.patch)在两种已发布模块构建中使用 `lstat`,因此 Framework 的文件和目录别名不会触发重复签名。选定的上游版本能够跳过这些别名前,仍需保留该补丁。PAK 文件由外层 bundle 签名记录完整性;逐个签名会增加串行时间戳请求,但不会增加资源完整性保护。Desktop 保留全部语言文件,只跳过其单独签名。可执行代码仍使用 Developer ID 签名、安全时间戳和 hardened runtime。[签名器遍历回归测试](../../../../apps/desktop/tests/macos-signing-walk.spec.ts)使用真实 Framework 别名执行已安装依赖;发布验收仍要求严格应用验证、公证和启动。 @@ -89,7 +89,7 @@ Windows 发布打包通过 `/f` 向已配置且与 SafeNet 兼容的 SignTool Windows 打包调用强制设置 `ELECTRON_BUILDER_7Z_FILTER=BCJ`。内置的 7-Zip 24.09 编码器会为 ARM64 PE 文件自动选择 ARM64 过滤器,但 `nsis-resources-3.4.1` 中的 NSIS 解码器会在解压时遗漏这些条目。使用实际 NSIS 插件的原生解压验证表明,自动过滤会丢失两个 `node-pty` ARM64 二进制文件,而 BCJ 可以逐字节还原二者。使用兼容的过滤器能够保留依赖内容与运行时完整性,无需删除特定架构的文件或削弱校验。 -本地 Windows 安装测试使用显式的 `--unsigned` 打包调用,并执行相同的构建和运行时准备。它清除证书输入,将产物隔离到 `unsigned-artifacts`,并省略更新器配置和发布完成记录。即使父进程环境请求未签名模式,常规打包命令也会显式选择签名模式。这样既能在没有 EV Token 时诊断安装问题,也能防止本地测试产物通过发布上传校验。 +本地 Windows 和 macOS 安装测试使用显式的 `--unsigned` 打包调用,并执行相同的构建和运行时准备。它清除证书输入,将产物隔离到 `unsigned-artifacts`,并省略更新器配置和发布完成记录。macOS 本地构建忽略 dotenv 文件和继承的发布配置,使用标准应用 ID,并省略强制更新策略,使本地测试无需部署配置。其 App 使用无需证书的 ad-hoc 签名,DMG 不签名也不公证。Windows 本地构建保留平台配置要求。即使父进程环境请求未签名模式,常规打包命令也会显式选择签名模式。这样既能在没有发布凭据时诊断安装问题,也能防止本地测试产物通过发布上传校验。 Windows 应用替换遵循[目录安装参考](../../../../apps/desktop/README.zh.md):使用能返回失败状态的命令行工具解压到目标旁边,再在同卷内改名替换完整目录。安装器在暂存期间保留旧目录,正式替换失败时恢复旧目录。注册信息、快捷方式和签名卸载器仍由 electron-builder 持有。 diff --git a/.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.i18n.yaml index 63a2bfdc06..232b137972 100644 --- a/.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.i18n.yaml @@ -9,11 +9,11 @@ en: d2df1eb3602af45d zh: fd162b1c214f1494 /agent-note-retain-ignorable-session-events-for-external-plugins/decision: - en: a4162e65cf6768f3 - zh: bb700d9630642e7e + en: c78e92c0cc5a7eb2 + zh: 62b29ccc5cd142f2 /agent-note-retain-ignorable-session-events-for-external-plugins/alternatives-considered: - en: 30ec097e3ba4e951 - zh: 3e90fc035d04b09b + en: c91cdee1dda42176 + zh: 870dfbda591abb76 /agent-note-retain-ignorable-session-events-for-external-plugins/consequences: - en: 5f65bfb81f172d49 - zh: 069d41d6bda9f8af + en: b14df3e7f7489748 + zh: ac227743cf5c11f7 diff --git a/.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md b/.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md index e085e429a6..8c01a5aae2 100644 --- a/.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md +++ b/.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md @@ -16,6 +16,10 @@ The canonical `SessionEvent` envelope retains `ignorable?: true`, and every repr The field is removable only after a replacement supports the current third-party plugin across event production, persistence, reload, and transport, with an explicit cutover for sessions already containing the marker. The [session log versioning decision](2026-08-10-session-log-version-mechanism.md) continues to own the default-required safety rule and format-version policy. +`appendPluginRecord()` in `@deepseek-ai/dsh-session` is the one first-party writer of the marker. It appends ignorable records whose type is in the `plugin:` namespace, which the V3-to-V4 edge already assigns to unknown ignorable V3 events, and only production source under `packages/experimental/` may call it; the `verify-plugin-record-callers` gate enforces that, and the persistence catalog generator rejects a `SessionEventMap` member in the namespace. Experimental packages keep state there that their owners can lose: a format migration retains records on a best-effort basis, so a release package declares its events instead. + +Experimental packages declare record names and payload types in the separate `PluginRecordMap`. The map types writes and supplies the dedicated [experimental persistence catalog](../../../../docs/experimental-persistence-catalog.md), making plugin-owned persistence discoverable from current source without expanding its payload schemas or recording released type history. It does not add names to `KNOWN_SESSION_EVENT_TYPES` or change stored-event admission. The reader returns unknown payloads for owner validation, including records whose declarations have changed or disappeared. + Historical format migration is deliberately stricter in the alpha implementation. The v0-to-v1 edge refuses every unknown v0 type, including an ignorable one, because an opaque payload may contain references that a format edge cannot validate. The [alpha historical-event decision](2026-08-31-alpha-historical-unknown-event-refusal.md) owns that bounded exception; equal-version append and reload continue to follow this note. ## Alternatives considered @@ -26,8 +30,16 @@ Historical format migration is deliberately stricter in the alpha implementation **Treat every repository-external event as ignorable.** Rejected because a reader cannot infer that an unknown durable event is informational. An external event may change later reconstruction or plugin-owned state. +**Declare a `SessionEventMap` member for each experimental package's state.** Rejected because each member is required-on-read and enters the released-schema inventory and type history: a build without the experimental package refuses the Session, and removing the package leaves a released type behind. + +**Keep no catalog of plugin records.** Rejected because maintainers need to find in-repository persistence writers. The dedicated experimental catalog provides that inventory from current-source declarations while their payloads retain best-effort compatibility; a full schema history would impose the released-event obligations that these records deliberately avoid. + +**Let any package call the record writer.** Rejected because records survive a format migration only on a best-effort basis, and a release package must not keep state that an upgrade can drop. + +**Restrict the writer with a runtime capability.** Not adopted because the writer runs in its caller's process, where a runtime check cannot tell experimental code from release code; the repository gate checks every reference instead. + **Register mounted plugin event names as known.** Not adopted as the removal mechanism because event-name registration alone does not classify whether absence is safe, and acceptance would depend on the reader's current composition rather than the stored record. ## Consequences -Third-party informational events can remain reloadable when their stored records carry the explicit marker, while unknown required events still fail loudly. The field remains part of the public event envelope, JSONL representation, transport types, generated references, and their tests until a replacement satisfies the cutover condition. +Third-party informational events can remain reloadable when their stored records carry the explicit marker, while unknown required events still fail loudly. The field remains part of the public event envelope, JSONL representation, transport types, generated references, and their tests until a replacement satisfies the cutover condition, which now also covers plugin records written by experimental packages. diff --git a/.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.zh.md b/.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.zh.md index 1fe4f81441..63fefff2c3 100644 --- a/.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.zh.md @@ -16,6 +16,10 @@ Status: implemented 只有替代机制在事件生产、持久化、重新加载与传输中都支持当前第三方插件,并为已包含该标记的会话提供显式切换方案后,才能删除此字段。[Session log 版本决策](2026-08-10-session-log-version-mechanism.zh.md)继续定义默认读取必需的安全规则与格式版本策略。 +`@deepseek-ai/dsh-session` 中的 `appendPluginRecord()` 是该标记唯一的第一方写入方。它追加类型位于 `plugin:` 命名空间的 ignorable 记录(V3-to-V4 迁移边已把未知的 ignorable V3 事件放入该命名空间),且只有 `packages/experimental/` 下的生产源码可以调用它;`verify-plugin-record-callers` 门禁强制这一点,持久化目录生成器也拒绝该命名空间中的 `SessionEventMap` 成员。实验性包在这里保存其所有者可以丢失的状态:格式迁移以尽力而为的方式保留记录,因此正式发布的包改为声明自己的事件。 + +实验性包在独立的 `PluginRecordMap` 中声明记录名称与 payload 类型。该映射约束写入类型,并生成独立的[实验性持久化目录](../../../../docs/experimental-persistence-catalog.zh.md),让维护者能够从当前源码找到插件自有的持久化数据,而无须展开其 payload schema 或记录已发布类型历史。它不向 `KNOWN_SESSION_EVENT_TYPES` 添加名称,也不改变已存事件的准入。读取方返回未知类型的 payload,由所属包校验,其中也包括声明已变更或消失的记录。 + Alpha 实现中的历史格式迁移有意更严格。v0-to-v1 迁移边会拒绝每个未知 v0 类型,包括 ignorable 类型,因为不透明 payload 可能包含格式迁移边无法校验的引用。[Alpha 历史事件决策](2026-08-31-alpha-historical-unknown-event-refusal.zh.md)定义该有限例外;同版本 append 与 reload 继续遵循本记录。 ## 曾考虑的替代方案 @@ -26,8 +30,16 @@ Alpha 实现中的历史格式迁移有意更严格。v0-to-v1 迁移边会拒 **把所有仓库外事件都视为可忽略。** 不予采用,因为读取器无法推断未知持久事件是否属于信息性事件。外部事件可能改变后续重建或插件自有状态。 +**为每个实验性包的状态声明 `SessionEventMap` 成员。** 不予采用,因为每个成员都是读取必需的,并会进入已发布 schema 清单及类型历史:没有该实验性包的构建会拒绝该会话,移除该包后还会留下一个已发布的类型。 + +**不维护插件记录目录。** 不予采用,因为维护者需要找到仓库内的持久化写入方。独立的实验性目录根据当前源码声明提供这份清单,而 payload 仍然只有尽力而为的兼容性;完整的 schema 历史会施加这些记录刻意不承担的已发布事件义务。 + +**允许任何包调用记录写入方。** 不予采用,因为记录只能以尽力而为的方式经受格式迁移,而正式发布的包不得保存升级可能丢弃的状态。 + +**用运行时能力限制写入方。** 不予采用,因为写入方运行在调用方的进程中,运行时检查无法区分实验性代码与正式发布的代码;仓库门禁改为检查每一处引用。 + **把已挂载插件的事件名称注册为已知。** 不作为删除机制采用,因为只注册事件名称无法判定缺失该事件是否安全,而且接受结果会依赖读取器的当前组合,而不是已存记录。 ## 影响 -第三方信息性事件的已存记录带有显式标记时可以继续重新加载,未知必需事件则仍会明确失败。在替代机制满足切换条件前,该字段继续属于公开事件信封、JSONL 表示、传输类型、生成引用及其测试。 +第三方信息性事件的已存记录带有显式标记时可以继续重新加载,未知必需事件则仍会明确失败。在替代机制满足切换条件前,该字段继续属于公开事件信封、JSONL 表示、传输类型、生成引用及其测试;该切换条件现在也涵盖实验性包写入的插件记录。 diff --git a/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.i18n.yaml index 690b1c02b6..ddf55f9063 100644 --- a/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.i18n.yaml @@ -6,11 +6,11 @@ en: f5e76516df025fc5 zh: 462fe05b46e6c1ea /agent-note-explicit-agent-identity-at-runtime-boundaries/problem: - en: 0b737a098e4e19ce - zh: 97b4254c4533114f + en: 98b49b900b8d2f84 + zh: 557faac2976592be /agent-note-explicit-agent-identity-at-runtime-boundaries/decision: - en: 1cb0015482bba539 - zh: 7f4b2da7184f6f64 + en: 1c628cf9c743c639 + zh: d969b2475fe4a155 /agent-note-explicit-agent-identity-at-runtime-boundaries/verification: en: 85130faaa171036b zh: 06e5e9fab32dbfb4 diff --git a/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.md b/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.md index 5152f54bdc..16cd668f65 100644 --- a/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.md +++ b/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.md @@ -10,7 +10,7 @@ An Agent's Cordis Context owns registrations and their cleanup. Agent identity i The reverse association also required compensating mechanisms after type erasure. Host Remote forwarding inspected a routed subject for its Context, creation inferred runtime parentage from the caller Context, and adapters maintained reverse identity scans. These mechanisms duplicated identity already present in typed requests and obscured which caller owned an Agent at runtime. -Without an explicit owner, `SubagentContinuationManager` creates and resumes children through its private plugin Context, so Context-based inference classifies every continuable child as a runtime root even though the manager holds its exact parent. Root-only consumers could then attach scheduling tools, grant direct-human goal authority, or route user questions as if the child were top-level. +Without an explicit owner, `SubagentManager` creates and resumes children through its private plugin Context, so Context-based inference classifies every continuable child as a runtime root even though the manager holds its exact parent. Root-only consumers could then attach scheduling tools, grant direct-human goal authority, or route user questions as if the child were top-level. ## Decision @@ -18,7 +18,7 @@ Runtime interfaces carry Agent identity at the point that owns it. `AgentSetup` Scope-aware registries continue to use the opaque scope key only for registration membership. Tool-subagent does not classify that key or resolve an Agent from Context. A direct `AgentSetup` passes the unpublished Session explicitly and installs through the supplied Context before publication. For a settings-backed standing preset, the event payload supplies the Agent, its Session supplies the policy target, and its Context owns the registrations. -`SubagentContinuationManager` puts the exact parent in both fresh-creation and cold-resume options. A live continuable child is therefore excluded from `AgentRegistry.roots()` and satisfies `isOwnedBy(child.id, parent)`. Durable `parentSession` metadata does not substitute for this relation: a fork or resumed Session may be a runtime root when no live Agent owns it. +`SubagentManager` puts the exact parent in both fresh-creation and cold-resume options. A live continuable child is therefore excluded from `AgentRegistry.roots()` and satisfies `isOwnedBy(child.id, parent)`. Durable `parentSession` metadata does not substitute for this relation: a fork or resumed Session may be a runtime root when no live Agent owns it. The [scope reference](../../../../packages/core/scope/README.md) and [Agent loop reference](../../../../packages/core/agent-loop/README.md) document registration and lifecycle behavior. The [initiator-scope decision](2026-07-15-agent-initiator-scope.md) owns private-chain rationale; explicit runtime ownership does not change causal initiation or registration lifetime. diff --git a/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.zh.md b/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.zh.md index 7c411cd14b..3d4131af01 100644 --- a/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.zh.md @@ -10,7 +10,7 @@ Agent 的 Cordis Context 拥有注册及其清理。Agent 身份则为某项操 类型信息被擦除后,这项反向关联还需要补偿机制。Host Remote 转发会从已路由主体检查其 Context,创建流程会从调用方 Context 推断运行时父级,适配器则维护反向身份扫描。这些机制重复类型化请求中已有的身份,也掩盖了哪个调用方在运行时拥有 Agent。 -若没有显式所属方,`SubagentContinuationManager` 会通过私有插件 Context 创建和恢复子级,因此基于 Context 的推断会把每个可续跑子级归类为 runtime root,尽管管理器持有其确切父级。仅限根级的消费方随后可能附加调度工具、授予直接人类输入对应的 Goal 权限,或像处理顶层 Agent 一样路由用户问题。 +若没有显式所属方,`SubagentManager` 会通过私有插件 Context 创建和恢复子级,因此基于 Context 的推断会把每个可续跑子级归类为 runtime root,尽管管理器持有其确切父级。仅限根级的消费方随后可能附加调度工具、授予直接人类输入对应的 Goal 权限,或像处理顶层 Agent 一样路由用户问题。 ## 决策 @@ -18,7 +18,7 @@ Agent 的 Cordis Context 拥有注册及其清理。Agent 身份则为某项操 感知作用域的注册表继续仅使用不透明作用域键判断注册成员关系。tool-subagent 不会分类该键,也不会从 Context 解析 Agent。直接 `AgentSetup` 显式传入尚未发布的 Session,并在发布前通过所给 Context 完成安装。对于由设置控制的常驻 preset,事件 payload 提供 Agent,其 Session 提供策略目标,其 Context 拥有注册项。 -`SubagentContinuationManager` 会把确切父级放进全新创建与冷恢复的 options。因此,存活的可续跑子级不会出现在 `AgentRegistry.roots()` 中,并且满足 `isOwnedBy(child.id, parent)`。持久化 `parentSession` 元数据不能代替这项关系:没有存活 Agent 拥有 fork 或已恢复会话时,它仍可成为 runtime root。 +`SubagentManager` 会把确切父级放进全新创建与冷恢复的 options。因此,存活的可续跑子级不会出现在 `AgentRegistry.roots()` 中,并且满足 `isOwnedBy(child.id, parent)`。持久化 `parentSession` 元数据不能代替这项关系:没有存活 Agent 拥有 fork 或已恢复会话时,它仍可成为 runtime root。 [作用域参考](../../../../packages/core/scope/README.zh.md)与 [Agent loop 参考](../../../../packages/core/agent-loop/README.zh.md)说明注册和生命周期行为。[发起方作用域决策](2026-07-15-agent-initiator-scope.zh.md)负责私有调用链理由;显式运行时归属不改变因果发起关系或注册生命周期。 diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml index 290d987800..33ebd75e3d 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml @@ -9,8 +9,8 @@ en: 5a991fec31578cea zh: 13b75aa36eb22907 /agent-note-parent-owned-subagent-catalog-events/decision: - en: 198354f20b5d0a42 - zh: 7a6b4392117a60b5 + en: 4319c502038ab34d + zh: b11825ff9d744822 /agent-note-parent-owned-subagent-catalog-events/alternatives-considered: en: 7a3d4b9f69c243d8 zh: 82b03d16ccc94dd2 diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md index 97af55f290..2fac858789 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md @@ -12,15 +12,17 @@ The child descriptor remains necessary for recovery and composition, but it cann ## Decision -The parent Session's required `subagent/catalog` events are the persistent authority for direct-child discovery. Each event is one successful creation fact containing `childId`, `childCreatedAt`, mode, and the mode-discriminated label. Remote one-shot runs without a local Session remain outside this catalog. Invalid own facts, including unsupported payload versions, reject projection restoration because silently dropping a required fact would return an incomplete catalog. +The shared activation entry point, external execution ownership, and caller-versus-parent result delivery are governed by [Unified subagent activations](../simplification/2026-09-17-unified-subagent-activations.md). This record retains the independent rationale described below. -Creation publishes only successful facts. A one-shot run appends the catalog event after its provider returns a local child and before the run reaches its caller. A continuable run admits the initial prompt, appends the catalog event, then returns the child id. If admission or catalog append fails, creation fails and releases the activation; there is no compensating catalog event or rollback protocol. +The parent Session's required `subagent/catalog` events are the persistent authority for direct-child discovery. Every local child enters the catalog independently of its result recipient; external children enter only with parent delivery. Workflow membership records describe execution progress and do not replace local Session discovery. Each event contains `childId`, `childCreatedAt`, mode, and the mode-discriminated label. Local children use `continuable`; external executions use `external` and cannot be opened as local Sessions. Historical `one-shot` and `unknown` entries remain readable. Invalid own facts, including unsupported payload versions, reject projection restoration because silently dropping a required fact would return an incomplete catalog. + +Creation publishes only successful facts. A local activation admits the initial prompt before appending its catalog event. An external activation appends its event after provider startup and before returning the receipt. Failed admission or publication releases the unpublished execution; there is no compensating catalog event or rollback protocol. The child header and `subagent/descriptor` remain authoritative for recovery and composition. An Activation and the exact parent relationship remain authoritative for authorization and delivery. Mode and label are snapshotted once and the same detached values reach the parent catalog fact and child descriptor. The registered `subagentCatalog` projection materializes the parent facts. It delegates storage, append, iteration, and checkpoint validation to [`dsh-chunked-list`](../../../../packages/util/chunked-list/README.md), which stores facts in a persistent stack of 64-entry chunks, so an append copies at most the head chunk in bounded O(1) work. Materialization visits chunks from oldest to newest and preserves parent catalog event order in O(D) time for D facts. Concurrent creation is ordered by successful catalog append, independent of child timestamps and ids. A projection checkpoint clones the state once in O(D); projection-cache writes remain asynchronous and use the existing mandatory creation, turn-end, and disposal points. -The utility owns chunk layout and its shared capacity constant; the catalog owns event validation, fork filtering, and row conversion. Catalog projection state version 2 stores generic chunk values, so the projection registry rebuilds incompatible caches from Session events. Session event payloads and public catalog rows retain their formats. +The utility owns chunk layout and its shared capacity constant; the catalog owns event validation, fork filtering, and row conversion. Catalog projection state version 6 accepts external membership, so the projection registry rebuilds incompatible caches from Session events. Payload v0 and v1 remain unchanged; v2 records external executions. Fork isolation uses the exact `Session.inheritedEventCount` supplied to projection initialization. The fold ignores `subagent/catalog` events below that offset. The state stores the inherited offset but not each event seq because acceptance is decided during folding. diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md index 7825fe443e..10088c52a7 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md @@ -12,15 +12,17 @@ child descriptor 对恢复与 composition 仍然必要,但它不能作为 disc ## 决策 -parent Session 的 required `subagent/catalog` 事件是直接 child discovery 的持久化权威。每个事件都是一条成功创建事实,包含 `childId`、`childCreatedAt`、mode 与按 mode 区分的 label。没有本地 Session 的远程 one-shot run 不进入该目录。无效的自身 fact(包括不支持的 payload 版本)会使 projection 恢复失败,因为静默丢弃 required fact 会返回不完整的目录。 +共享 activation 入口、外部执行所有权,以及调用方与父级之间的结果投递选择由[统一 subagent activation](../simplification/2026-09-17-unified-subagent-activations.zh.md) 决策拥有。本记录保留下述独立理由。 -创建只发布成功事实。one-shot run 在 provider 返回本地 child 后、run 到达调用方前追加目录事件。continuable run 先准入初始 prompt,再追加目录事件,最后返回 child id。准入或目录追加失败时,创建失败并释放 activation;不存在补偿目录事件或 rollback 协议。 +父级 Session 的必读 `subagent/catalog` 事件是发现直接子级的持久化权威。每个本地子级都会进入目录,与结果接收方无关;外部子级仅在 parent 投递时进入目录。工作流成员记录描述执行进度,不替代本地 Session 发现。每个事件包含 `childId`、`childCreatedAt`、mode 与按 mode 区分的 label。本地子级使用 `continuable`;外部执行使用 `external`,不能作为本地 Session 打开。历史 `one-shot` 与 `unknown` 条目仍可读取。无效的自身事实(包括不支持的载荷版本)会使投影恢复失败,因为静默丢弃必读事实将返回不完整的 catalog。 + +创建只发布成功事实。本地 activation 先接纳初始提示词,再追加 catalog 事件。外部 activation 在提供方启动后、返回回执前追加事件。准入或发布失败会释放未发布的执行;不存在补偿 catalog 事件或回滚协议。 child header 与 `subagent/descriptor` 继续拥有恢复与 composition 权威。Activation 与精确 parent 关系继续拥有授权与投递权威。mode 与 label 只快照一次,同一份分离值写入 parent catalog fact 与 child descriptor。 注册的 `subagentCatalog` projection 物化 parent fact。它将存储、追加、迭代和检查点校验交给 [`dsh-chunked-list`](../../../../packages/util/chunked-list/README.zh.md),后者以每块 64 项的持久 stack 保存事实,因此 append 最多复制 head chunk,以有界 O(1) 工作完成。materialization 从旧到新访问 chunk,对 D 条事实以 O(D) 时间保留父目录事件顺序。并发创建按目录成功追加的顺序排列,与 child 时间戳和 id 无关。projection checkpoint 以 O(D) 克隆 state;projection-cache 继续异步写入,并使用既有创建、turn-end 与 disposal 强制点。 -工具库拥有分块布局及其共享容量常量;目录拥有事件校验、fork 过滤和目录行转换。目录 projection state 版本 2 保存通用块值,因此 projection registry 从 Session 事件重建不兼容的缓存。Session 事件载荷和公开目录行保持各自格式。 +工具库拥有 chunk 布局与共享容量常量;catalog 拥有事件校验、fork 过滤和行转换。catalog 投影状态版本 6 接受外部成员,因此投影注册表会从 Session 事件重建不兼容缓存。载荷 v0 与 v1 保持不变;v2 记录外部执行。 fork 隔离使用 projection 初始化时提供的精确 `Session.inheritedEventCount`。fold 忽略该 offset 之前的 `subagent/catalog` 事件。state 保存 inherited offset,但不保存每条 event seq,因为接受判定已在 fold 时完成。 diff --git a/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.i18n.yaml index c1cc88bcdd..5cadfbef6f 100644 --- a/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.i18n.yaml @@ -6,8 +6,8 @@ en: 6a4338564c01ad91 zh: 241248cff9bd4cfc /agent-note-add-immutable-profile-resolution-generations/problem: - en: 8e192c176fe48c4c - zh: adb965d650088eb0 + en: 907a40c722a6faff + zh: a471d7034d8c231f /agent-note-add-immutable-profile-resolution-generations/decision: en: a3022a87337058c5 zh: 1d53a08a25ed6eb3 @@ -21,8 +21,8 @@ en: 53a9e92698ce9a5d zh: 6124d446e8d4a532 /agent-note-add-immutable-profile-resolution-generations/decision/immutable-generations: - en: cd4e64bd80f448a2 - zh: 585e1fbd8721ae99 + en: e10b224e0e68b701 + zh: d43fbdf37e6bfc54 /agent-note-add-immutable-profile-resolution-generations/decision/shared-esm-and-commonjs-rule: en: 0e0575a04a081b52 zh: 94c74334939e6d13 @@ -33,8 +33,8 @@ en: 99308bd758859e02 zh: 35f1e7719fe80d2e /agent-note-add-immutable-profile-resolution-generations/decision/additive-package-changes: - en: 718dfa7826783f2a - zh: 21c3d5fdfae9cb99 + en: 0e5e2006caec3c91 + zh: fb3b61dd5692d2e3 /agent-note-add-immutable-profile-resolution-generations/decision/filesystem-and-runtime-carriers: en: b0607713102a093d zh: 2de9c33bfaa0e3e2 @@ -45,8 +45,8 @@ en: 2426173c58a17a48 zh: 0423b3700273accd /agent-note-add-immutable-profile-resolution-generations/verification: - en: fbb17aa8fe62b1e8 - zh: 2ed091fdfd995fc8 + en: 74cb43ea2ccdb334 + zh: b966bbed43a83c82 /agent-note-add-immutable-profile-resolution-generations/consequences: - en: 54cae20b2734a07e - zh: caa334f8f7a40452 + en: 457a7d1e82b45fe0 + zh: 800036e083a7674c diff --git a/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md b/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md index 649c6c358f..d933a4507e 100644 --- a/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md +++ b/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md @@ -8,7 +8,7 @@ English | [中文](2026-09-09-profile-resolution-generations.zh.md) A profile loads plugin rows from its own package project, while Harness packages and packages carried by selected bundles can live outside that project's ordinary dependency tree. Bridging the trees through shared symlinks, profile-owned links, or packaged-executable proxy packages persists package selections across processes and installations. Those files require reconciliation and locking, expose generated proxy manifests to metadata readers, and cannot represent a process-local change atomically. -The runtime design keeps installation-first, ordered-bundle, and local-before-fallback precedence. It covers imports performed by plugin modules as well as Loader row imports and works in the main thread and Harness-owned Workers. Generation replacement preserves existing package mappings and local package names, permits linked-root membership changes, and never mutates a live table entry by entry. +The runtime design keeps installation-first, ordered-bundle, and local-before-fallback precedence. It covers imports performed by plugin modules as well as Loader row imports and works in the main thread and Harness-owned Workers. Generation replacement preserves retained package identities, permits removing profile packages and changing linked-root membership, and never mutates a live table entry by entry. ## Decision @@ -75,7 +75,7 @@ Linked-root membership is immutable within a generation. A successor may add or The router retains the real target of each successfully published link name for its lifetime, solely to validate successors. Removing a root does not erase that record or keep its directory intercepted. Re-adding the same name and target is allowed; a different target is rejected because Node caches real paths. Failed publication changes neither the current generation nor the recorded targets. -The launcher constructs one startup generation. The service accepts a complete successor, but no package-manager transaction invokes replacement in this implementation. +The launcher constructs one startup generation. `ProfileRuntimeResolution` retains only the installation anchor, Harness home, and optional profile directory as recomputation inputs; successors reread disk selection and layers rather than inheriting a supplied profile's synthetic layers. A generation without a profile directory can recompute installation packages. The [package refresh decision](2026-09-30-profile-package-refresh-and-manifest-invalidation.md) assigns successor publication to Plugin Manager operations. ### Shared ESM and CommonJS rule @@ -105,9 +105,9 @@ New Workers inherit the latest published generation. Existing Workers keep the g ### Additive package changes -A caller adding a package completes its pnpm transaction before constructing a successor generation. Replacement rejects any generation that changes the directory or version of an existing package. The caller publishes an additive successor before mounting the new Loader row; this implementation does not provide that package transaction. A mount failure may leave the package installed but inactive. +A caller adding a package completes its pnpm transaction before constructing a successor generation. Replacement rejects any generation that changes the directory or version of a retained package. Plugin Manager publishes the successor before mounting the new Loader row. A mount failure may leave the package installed but inactive. -Changing or removing an existing runtime package mapping, or removing a recorded profile-local package name, requires process restart because Node's ESM Module Map, CommonJS cache, existing object references, and running Workers can retain the old module identity. These restrictions do not prohibit removing a linked root from the interception scope. Generation replacement does not claim to unload modules. +A retained package keeps its normalized directory, version, and scope; installation entries also keep their declaring anchor and cannot be removed. Violating these restrictions requires process restart because Node's ESM Module Map, CommonJS cache, existing object references, and running Workers can retain the old module identity. A profile entry may change its declaring anchor: deselecting the first bundle that reaches a shared dependency can leave another bundle selecting the same physical package. Profile-scoped mappings and profile-local package names may be removed; the caller stops their plugins before deleting package files. These restrictions do not prohibit removing a linked root from the interception scope. Generation replacement does not claim to unload modules. ### Filesystem and runtime carriers @@ -142,7 +142,7 @@ Behavior tests exercise root order, transitive and peer dependencies, local and ## Verification - One eager computation supplies the runtime resolution; startup neither writes nor retires module-resolution data. -- [Generation tests](../../../../packages/boot/app-boot/tests/profile-resolution.spec.ts) cover installation and selected-bundle graphs with ordinary directories and recursive symlinks, including different dependency versions beside logical and real anchors. They also cover linked-root removal, same-target restoration, overlapping roots, native misses, new requests from loaded modules, and relink rejection after removal. +- [Generation tests](../../../../packages/boot/app-boot/tests/profile-resolution.spec.ts) cover installation and selected-bundle graphs with ordinary directories and recursive symlinks, including different dependency versions beside logical and real anchors. They also cover same-package profile declarer changes, retained-identity rejection, captured-directory recomputation, linked-root removal, same-target restoration, overlapping roots, native misses, new requests from loaded modules, and relink rejection after removal. - [Source-launch tests](../../../../apps/cli/tests/source-launch.compat.spec.ts) and [built-bin tests](../../../../apps/cli/tests/built-bin.e2e.ts) run both profile layouts through the real CLI. They assert ESM/CJS versions, loaded paths, per-format dependency identity, and consistent Tools/AgentLoop module instances with an accessible scheduler key. - Pkg and Electron carriers select runtime resolution; Electron executes its Host in Node mode from the ASAR-backed dsh tree while native executable entries remain unpacked. - ESM and CommonJS adapters share one router and delegate final resolution to Node without `module.registerHooks` or `_findPath` replacement. @@ -152,4 +152,4 @@ Behavior tests exercise root order, transitive and peer dependencies, local and ## Consequences -Runtime startup avoids disk mutation and proxy manifests while retaining package-precedence rules. Real-directory anchors align dependency discovery with default Node loading and tsx workspace mapping, including cases where a logical symlink path would select another version. The implementation accepts the maintenance cost of Node Internal compatibility tests and an early, self-contained bootstrap in each owned Worker; it provides no disk-only backend or dual comparison mode. Package mappings and local package names remain additive; linked-root membership may change without unloading modules. +Runtime startup avoids disk mutation and proxy manifests while retaining package-precedence rules. Real-directory anchors align dependency discovery with default Node loading and tsx workspace mapping, including cases where a logical symlink path would select another version. The implementation accepts the maintenance cost of Node Internal compatibility tests and an early, self-contained bootstrap in each owned Worker; it provides no disk-only backend or dual comparison mode. Profile package records and linked-root membership may be removed without unloading modules; retained package identities stay unchanged. diff --git a/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md b/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md index 4e84cc1f1b..24da72defb 100644 --- a/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md @@ -8,7 +8,7 @@ Status: implemented profile 从自己的包项目加载插件配置项,而 Harness 包和所选 bundle 携带的包可能位于该项目普通依赖树之外。通过共享 symlink、profile 自有链接或打包可执行文件的代理包连接两棵依赖树,会让选包结果跨进程和安装版本持续存在。这些文件需要协调和锁来维护,并向元数据读取方暴露生成的代理 manifest(元数据清单),也无法原子表示进程内变更。 -运行时设计保留安装优先、有序 bundle 和本地包优先于 fallback 的顺序。它覆盖插件模块内部的 import 以及 Loader 配置项的 import,并在主线程和 Harness 自有 Worker 中工作。generation 替换保留既有包映射和本地包名,允许调整 linked root 集合,不会逐项修改正在使用的表。 +运行时设计保留安装优先、有序 bundle 和本地包优先于 fallback 的顺序。它覆盖插件模块内部的 import 以及 Loader 配置项的 import,并在主线程和 Harness 自有 Worker 中工作。generation 替换保持保留包的身份不变,允许移除 profile 包和调整 linked root 集合,不会逐项修改正在使用的表。 ## Decision @@ -75,7 +75,7 @@ linked root 集合在一个 generation 内不可变。后继 generation 可以 解析器在自身生命周期内保留每个已成功发布的 link 名称对应的真实目标,仅用于校验后继 generation。移除 root 不会抹掉该记录,也不会让其目录继续被拦截。同名、同目标可以重新加入;不同目标会被拒绝,因为 Node 缓存真实路径。发布失败时,当前 generation 和已记录目标均不变。 -launcher 只构造启动 generation。服务接受完整的后继 generation,但本实现没有包管理器事务调用替换操作。 +launcher 只构造启动 generation。`ProfileRuntimeResolution` 仅保留安装锚点、Harness home 和可选的 profile 目录作为重算输入;后继代重新读取磁盘上的选择与配置层,不继承传入 profile 的合成层。没有 profile 目录的 generation 可以重算安装包。[包操作刷新决策](2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md)规定由 Plugin Manager 的操作发布后继代。 ### ESM 与 CommonJS 共用规则 @@ -105,9 +105,9 @@ runtime resolution 列出它提供的包;Loader entries 组成活动插件列 ### 只增加包的变更 -添加包的调用方先完成 pnpm 事务,再构造下一代。替换操作会拒绝改变任何既有 package name 的目录或版本。调用方先发布只增加映射的后继 generation,再挂载新的 Loader 配置项;本实现不提供该包事务。挂载失败可以留下已安装但未启用的包。 +添加包的调用方先完成 pnpm 事务,再构造下一代。替换操作会拒绝改变任何保留包的目录或版本。Plugin Manager 先发布后继 generation,再挂载新的 Loader 配置项。挂载失败可以留下已安装但未启用的包。 -修改或删除既有运行时包映射,或删除已记录的 profile 本地包名,需要重启,因为 Node 的 ESM Module Map、CommonJS cache、现存对象引用和运行中的 Worker 都可能保留旧模块 identity。这些限制不禁止从拦截范围移除 linked root。generation 换代不声称卸载模块。 +保留包的规范化目录、版本和作用域必须不变;安装条目还必须保留声明锚点,且不能移除。违反这些限制需要重启,因为 Node 的 ESM Module Map、CommonJS cache、现存对象引用和运行中的 Worker 都可能保留旧模块 identity。profile 条目可以改变声明锚点:取消选入最先找到共享依赖的组合包后,另一个组合包仍可能选中同一物理包。profile 范围的映射和 profile 本地包名可以移除;调用方在删除包文件之前先停止相关插件。这些限制不禁止从拦截范围移除 linked root。generation 换代不声称卸载模块。 ### 文件系统与运行时载体 @@ -142,7 +142,7 @@ generation 构造发生在启动或显式更新阶段,不属于单次 resolve ## Verification - 一次 eager 计算供应 runtime resolution;启动既不写入也不退休模块解析数据。 -- [Generation 测试](../../../../packages/boot/app-boot/tests/profile-resolution.spec.ts)覆盖普通目录和递归软链接下的安装图与所选 bundle 图,包括逻辑锚点和真实锚点旁存在不同依赖版本的情况。测试还覆盖 linked root 移除、同目标恢复、重叠 root、原生缺包、已加载模块的新请求,以及移除后重新链接不同目标时的拒绝。 +- [Generation 测试](../../../../packages/boot/app-boot/tests/profile-resolution.spec.ts)覆盖普通目录和递归软链接下的安装图与所选 bundle 图,包括逻辑锚点和真实锚点旁存在不同依赖版本的情况。测试还覆盖同包 profile 声明者变更、保留包身份变更拒绝、捕获目录后的重算、linked root 移除、同目标恢复、重叠 root、原生缺包、已加载模块的新请求,以及移除后重新链接不同目标时的拒绝。 - [源码启动测试](../../../../apps/cli/tests/source-launch.compat.spec.ts)与[构建入口测试](../../../../apps/cli/tests/built-bin.e2e.ts)通过真实 CLI 运行两种 profile 布局,断言 ESM/CJS 版本、加载路径、各模块格式内的依赖身份,以及一致的 Tools/AgentLoop 模块实例和可访问的 scheduler 键。 - pkg 与 Electron 载体选择 runtime 解析;Electron 以 Node 模式从 ASAR 承载的 dsh 依赖树执行 Host,原生可执行条目保持 unpacked。 - ESM 与 CommonJS 适配器共享同一个路由器,并把最终解析委托给 Node,不使用 `module.registerHooks` 或替换 `_findPath`。 @@ -152,4 +152,4 @@ generation 构造发生在启动或显式更新阶段,不属于单次 resolve ## Consequences -runtime 启动避免磁盘修改和代理 manifest,同时保留包优先级规则。真实目录锚点让依赖发现对齐 Node 默认加载与 tsx workspace 映射,也覆盖软链接逻辑路径会选中另一版本的情况。实现需要持续维护 Node Internal 兼容测试,并在每个自有 Worker 中尽早执行自包含 bootstrap;它不提供纯磁盘后端或 dual 对比模式。包映射和本地包名仍只允许新增;linked root 集合可以变化,而不卸载模块。 +runtime 启动避免磁盘修改和代理 manifest,同时保留包优先级规则。真实目录锚点让依赖发现对齐 Node 默认加载与 tsx workspace 映射,也覆盖软链接逻辑路径会选中另一版本的情况。实现需要持续维护 Node Internal 兼容测试,并在每个自有 Worker 中尽早执行自包含 bootstrap;它不提供纯磁盘后端或 dual 对比模式。profile 包记录和 linked root 集合可以移除,而不卸载模块;保留包的身份保持不变。 diff --git a/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.i18n.yaml index 834d15576d..d83f853c54 100644 --- a/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.i18n.yaml @@ -12,8 +12,8 @@ en: cf072f53de09faca zh: 9edd5c4a2bfaae5a /agent-note-sandboxed-node-execution-for-ptc/decision/resolved-inputs-and-policy: - en: e93df995efab569a - zh: 850daf02d1207028 + en: 3aef37bb4f8a8e53 + zh: a7b477324d486ab3 /agent-note-sandboxed-node-execution-for-ptc/decision/control-and-lifetime: en: 2c7a694b04951772 zh: 133b95b0477b127f diff --git a/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.md b/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.md index f851775460..2104e58e8e 100644 --- a/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.md +++ b/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.md @@ -22,7 +22,7 @@ The host preserves `ELECTRON_RUN_AS_NODE` for child startup; the bootstrap remov File mode, observed denial and enforcement completeness travel in `PtcRunResult.sandbox` separately from the program outcome. Restricted execution fails if the required sandbox backend cannot launch. Full access is an explicit policy mode. Program success does not prove full enforcement, and neither the `process` descriptor nor the extra control pipe claims multi-tenant isolation. -The private Python provider keeps its existing execution implementation and configured wall deadline. Its resolver accepts cwd but rejects an explicit file policy or per-call timeout override; a capability descriptor never silently grants unsupported protection. +The [experimental Python provider](../../../../packages/experimental/ptc-runtime-python/README.md) shares the resolved file policy and sandbox diagnostics while retaining its local Unix execution and configured wall deadline. Per-call timeout overrides remain unsupported. ### Control and lifetime diff --git a/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.zh.md b/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.zh.md index 94f3a37eb7..429351cca2 100644 --- a/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.zh.md @@ -22,7 +22,7 @@ Host 在子进程启动时保留 `ELECTRON_RUN_AS_NODE`;bootstrap 在求值前 文件模式、观察到的拒绝与强制完整性通过 `PtcRunResult.sandbox` 独立于程序结果传递。所需沙箱后端无法启动时,受限执行失败。完整访问是显式策略模式。程序成功不证明完整强制能力,`process` 描述符与额外控制管道也不声明多租户隔离。 -私有 Python 提供方保留现有执行实现与配置的经过时间截止。其解析器接受 cwd,但拒绝显式文件策略或单次 timeout 覆盖;能力描述符不会静默授予不受支持的保护。 +[实验性 Python 提供方](../../../../packages/experimental/ptc-runtime-python/README.zh.md)共享已解析文件策略与沙箱诊断,同时保留本地 Unix 执行和配置的经过时间截止。仍不支持单次 timeout 覆盖。 ### 控制与生命周期 diff --git a/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.i18n.yaml new file mode 100644 index 0000000000..49a9f432ee --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-09-13-session-working-directory.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-13-session-working-directory.md +/agent-note-session-working-directory: + en: 4d5b74f6b55a2270 + zh: 8323ea3fbdf8fd8d +/agent-note-session-working-directory/problem: + en: 31fba384d90ce7d2 + zh: f8b814868f3280c3 +/agent-note-session-working-directory/decision: + en: 9ccde1c5e635b9c0 + zh: 4dc76229703d3ef7 +/agent-note-session-working-directory/alternatives-considered: + en: f0995feb9b6bcce8 + zh: 97417085fd17f70f +/agent-note-session-working-directory/consequences: + en: 2dd3a4de72ee6d74 + zh: be5d8a80c8eaac6e diff --git a/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.md b/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.md new file mode 100644 index 0000000000..8151c0e85a --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.md @@ -0,0 +1,47 @@ +# Agent Note: Session working directory + +Status: implemented + +English | [中文](2026-09-13-session-working-directory.zh.md) + +## Problem + +A fixed Session creation directory cannot represent a task that changes checkout during its conversation. Independent tools resolving paths from different sources can read and write different projects, while changing the process directory also affects unrelated Sessions. Putting mutable directory information in the system prompt changes the reusable prompt prefix. + +## Decision + +`dsh-working-directory` owns one logged effective directory per Session. Its projection initializes from the immutable original project and folds `working-directory/change`; consumers read that owner instead of deriving execution paths from the header or sandbox root. Directory changes validate through the filesystem provider, serialize within the Session, and publish only after the new directory is available. + +The `dsh-base` composition mounts `working_directory({ cd? })` globally. The tool reads or changes the Session directory, so every Agent preset can select or recover it, including presets without file-editing tools. The Web composition keeps action tools in preset scopes. + +Required user-context snapshots carry the current value; each committed change attempts to queue a notice through the ordinary Agent inbox. Cancellation or disposal may discard unadmitted notices while the directory event survives for subsequent requests. The original project is the recovery destination when the selected directory disappears. Missing recovery destinations produce an error. Write permissions remain independent. + +Subagents capture the parent's current directory or an explicit override and commit their selected directory after inherited history. Existing processes, terminals, and persistent shells keep their process-local directory. Project instructions, skills, and completion follow the effective directory without changing Session identity, grouping, or runtime composition. + +Live Session skill catalogs and file completion validate through the same directory owner. These reads can commit recovery and queue its notice before discovery, keeping discovered files and subsequent operations tied to the committed Session directory. Cold skill catalogs read the recorded projection without activating an Agent. + +Web sidebar terminal environment lookup reads the effective directory without filesystem validation or recovery, so a missing directory does not prevent reconnecting to a retained terminal. New terminal creation validates and recovers through the directory owner before spawning; the process keeps that directory independently of later Session changes. + +Playwright MCP and Chrome DevTools MCP validate the effective directory when acquiring a Session's MCP connection and use it to launch the server. Later directory changes do not restart the retained server or change its process directory. + +File links and command directory labels use result metadata captured when each operation runs, so historical rows remain correct after later directory changes. + +The [prompt-variables note](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) retains strict interpolation, route-variable ownership, and tool-guidance ownership. The [environment-suffix note](../../archived/bug-fix/2026-09-06-environment-prompt-suffix.md) retains ordered prefix/suffix placement for deployment, Harness source, and Web guidance. This decision owns directory state and placement in user context. + +## Alternatives considered + +**Rewrite the Session header.** The header is immutable identity metadata used by storage, grouping, and permission roots. Rewriting it would conflate current execution state with the original project. + +**Let worktree tools own directory state.** Ordinary directory changes, filesystem tools, and subagents need the same value. A Git-specific owner would duplicate selection and recovery. + +**Change the process directory or synchronize every shell.** Concurrent Sessions and existing subprocesses have independent lifetimes. A Session change affects new directory-based operations, while a shell's `cd` remains local. + +**Keep directory text in the system prompt.** Mutable operational context belongs after retained history, using the same user-context mechanism as permission state. + +## Consequences + +The runtime exposes one directory owner and preserves the original project. Consumers migrate together, including filesystem, shell, instructions, skills, subagents, and both SDKs. Directory existence can change outside the harness, so validation and logged recovery occur at use. Required runtime context remains visible even when optional context is suppressed. + +`{{cwd}}` has no loop-provided value; custom personas that depend on that built-in fail strict interpolation. Deployments remove those references using the [system-prompt migration guidance](../../../../packages/core/system-prompt/README.md). When both the current directory and its recovery destination are unavailable, prompt assembly rejects model turns; the [directory service recovery guidance](../../../../packages/session/working-directory/README.md#known-limitations-and-deferred-work) requires restoring the directory or selecting an existing absolute directory through the SDK or host service. + +Verification covers independent Sessions, relative changes, replay, cancellation, recovery, tool disposal, user-context rendering, provider inheritance, and real directory-based operations. diff --git a/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.zh.md b/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.zh.md new file mode 100644 index 0000000000..46246a9d67 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-13-session-working-directory.zh.md @@ -0,0 +1,47 @@ +# Agent Note: Session working directory + +Status: implemented + +[English](2026-09-13-session-working-directory.md) | 中文 + +## Problem + +固定的 Session 创建目录无法表示在对话中切换检出目录的任务。各工具从不同来源解析路径时,可能读写不同项目;改变进程目录还会影响其他 Session。把可变目录信息放入系统提示词会改变可复用的提示词前缀。 + +## Decision + +`dsh-working-directory` 拥有每个 Session 的一个日志化有效目录。投影从不可变的原始项目初始化,并折叠 `working-directory/change`;消费者读取该服务,而不是从 header 或沙箱根目录派生执行路径。目录变更通过文件系统 provider 验证,在 Session 内串行执行,并只在新目录可用后发布。 + +`dsh-base` 组合在全局挂载 `working_directory({ cd? })`。该工具读取或改变 Session 目录,使每个 Agent preset 都能选择或恢复它,包括没有文件编辑工具的 preset。Web 组合把操作工具放在 preset 作用域中。 + +必需的用户上下文快照携带当前值;每次已提交变更都会尝试通过普通 Agent 收件箱排入通知。取消或销毁可能丢弃尚未进入请求的通知,但目录事件仍然保留,供后续请求使用。所选目录消失时恢复到原始项目。恢复目录也消失时返回错误。写权限保持独立。 + +子代理捕获父代理当前目录或显式覆盖值,并在继承历史之后提交选定目录。已有进程、终端和持久 shell 保留进程局部目录。项目指令、技能和补全跟随有效目录,不改变 Session 身份、归属或运行时组合。 + +活跃 Session 的技能目录与文件补全通过同一个目录 owner 验证目录。这些读取可在发现操作之前提交恢复并排入通知,使发现的文件与后续操作关联到已提交的 Session 目录。冷态技能目录读取已记录的投影,不激活 Agent。 + +Web 侧栏终端的环境查询读取有效目录,不验证文件系统或执行恢复,因此目录缺失不会阻止重新连接保留的终端。新终端在创建进程前通过目录 owner 验证和恢复目录;后续 Session 目录变更不会改变该进程的目录。 + +Playwright MCP 和 Chrome DevTools MCP 在获取 Session 的 MCP 连接时验证有效目录,并在该目录启动服务器。后续目录变更不会重启保留的服务器,也不会改变其进程目录。 + +文件链接与命令目录标签使用各次操作执行时记录的结果元数据,因此后续目录变更不会使历史记录指向错误的位置。 + +[提示词变量记录](2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md)保留严格插值、路由变量归属与工具指导归属。[环境后缀记录](../../archived/bug-fix/2026-09-06-environment-prompt-suffix.md)保留部署、Harness 源码与 Web 指导的有序前缀/后缀位置。本决策拥有目录状态及其用户上下文位置。 + +## Alternatives considered + +**改写 Session header。** header 是存储、归属与权限根目录使用的不可变身份元数据。改写它会混淆当前执行状态与原始项目。 + +**让 worktree 工具拥有目录状态。** 普通目录变更、文件系统工具和子代理需要同一个值。Git 专用 owner 会重复实现选择与恢复。 + +**改变进程目录或同步所有 shell。** 并发 Session 与已有子进程具有独立生命周期。Session 变更影响新的目录相关操作,shell 内的 `cd` 则保持局部。 + +**把目录文本留在系统提示词中。** 可变操作上下文应位于保留历史之后,复用权限状态的用户上下文机制。 + +## Consequences + +运行时提供一个目录 owner,并保留原始项目。消费者一起迁移,包括文件系统、shell、指令、技能、子代理和两种 SDK。目录可能在 harness 之外消失,因此在使用时执行验证和日志化恢复。即使可选上下文被禁用,必需运行时上下文也仍然可见。 + +`{{cwd}}` 没有循环提供的值;依赖该内置变量的自定义 persona 会触发严格插值错误。部署方按照[系统提示词迁移说明](../../../../packages/core/system-prompt/README.zh.md)移除这些引用。当前目录与恢复目标均不可用时,提示词组装会拒绝模型轮次;[目录服务恢复说明](../../../../packages/session/working-directory/README.zh.md#known-limitations-and-deferred-work)要求恢复目录,或通过 SDK 或 Host 服务选择现有的绝对目录。 + +验证覆盖 Session 相互独立、相对路径变更、回放、取消、恢复、工具卸载、用户上下文渲染、provider 继承和真实目录操作。 diff --git a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml index 242b95ac9f..e7aef9adc0 100644 --- a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml @@ -9,11 +9,11 @@ en: 2ba6c43b20a3d347 zh: 5647179ddaa38ad9 /agent-note-ship-experimental-capabilities-as-optional-bundles/decision: - en: 0d6c40e376fb6e48 - zh: b0cf2ded4e395942 + en: e6ffc783859d7509 + zh: 8678f323985a24f4 /agent-note-ship-experimental-capabilities-as-optional-bundles/alternatives-considered: - en: a65d84004e48b3ef - zh: 17221f08e7a8a89b + en: 9ff0511566948d8b + zh: 4e1e37b79f63d390 /agent-note-ship-experimental-capabilities-as-optional-bundles/consequences: - en: d690cf4d49c6af79 - zh: 1c7a3077c35ae5b8 + en: 9d2b313f15fc9ce0 + zh: 522341c83565ac33 diff --git a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md index 70bc46df69..d529b27542 100644 --- a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md +++ b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md @@ -10,9 +10,13 @@ The Web plugin page offered two optional bundles, Agent Teams and voice input. A ## Decision -`OPTIONAL_BUNDLES` lists Agent Teams, voice input, Auto review, and the Inspector profile. Every optional bundle declares `icon` and exports `./locale/*.json` with `meta.title` and `meta.description`, so the Official group renders a localized title, description, and image; `verify-default-product-isolation` rejects an optional bundle without them. The Inspector profile owns the package-named bundle patch. The standalone Inspector's `cordis.patch.yml` mounts its local built entry as an explicit repository overlay; [Session Inspector](../feature/2026-09-24-session-inspector.md) owns the inspection behavior and activation choices. +`OPTIONAL_BUNDLES` owns the installation's default-off selections, including maintained optional capabilities and experimental capabilities. Every bundle declares `icon` and exports `./locale/*.json` with `meta.title` and `meta.description`; the Official group displays that metadata. A bundle's presentation does not determine its [experimental status](../../../../packages/experimental/README.md#status). The isolation check validates admission and still rejects experimental dependencies of nonexperimental bundles. -An optional bundle is a runtime dependency of the installation, so its dependency graph is downloaded by every `dsh` install. The list weighs the added installation cost: Auto review reuses the installation's closure, while the Inspector profile adds inspection packages, `serve-static`/`open`/`ws`, and 535 mirrored DevTools resources (about 11.95 MiB uncompressed), without another browser binary. The Inspector profile appears in the default plugin list but remains disabled until selected. The browser-use and computer-use providers stay explicit compositions: Playwright MCP, Chrome DevTools MCP, and the native Cua Driver runtime binaries add about 85 MB and 21 packages to every install whether or not the bundle is enabled, and a shipped Cua Driver MCP switch would offer a capability whose executable the installation does not carry. Those provider packages keep locale display metadata for their component rows. Two more packages stay out for other reasons: `ptc-runtime-python` replaces the PTC runtime, and `workflow-ptc` rejects a non-TypeScript runtime at load while the Web presets carry `workflow-ptc` rows a bundle patch cannot reach; `browser-use-stagehand-native` requires a native model name and API key at schema validation with no configuration form on the page. +An optional bundle's dependencies are downloaded with every `dsh` installation. [Reasoning translation](2026-10-05-anonymous-reasoning-translation.md) uses three experimental packages without adding third-party runtime dependencies. Admission therefore considers installation cost and whether a switch supplies a usable composition without an additional configuration form. Browser and computer providers remain explicit compositions because they add substantial runtimes or require an external executable. Stagehand requires model credentials; Python PTC replaces a core provider and conflicts with TypeScript workflow consumers. Demonstrative hooks, webhooks, and mods stay outside the list. [Session Inspector](../feature/2026-09-24-session-inspector.md) owns the Inspector's distinct activation behavior. + +Bundles add scoped rows with the [profile patch operation](../../../../packages/boot/app-boot/README.md): `preset` names a declaration row, and the ordinary patch operates on its child list. Full Web presets receive the optional model tools; minimal retains its current shell and working-directory tools. Search, Ralph, and terminals supply their own isolated services. Search opens one in-memory index on demand per retained preset revision; the Host search policy stays unchanged. Badge skills and title providers act on the Host. Existing Agents retain their preset generation, and later user patch layers retain precedence. Title replacement waits for disposal of the previous provider. + +Claude Code and Codex install on demand through the separate `ON_DEMAND_BUNDLES` catalog; the [on-demand native-bundle decision](2026-10-05-official-on-demand-bundles.md) owns the cost, version, and offline-discovery trade-offs. ## Alternatives considered @@ -20,6 +24,8 @@ An optional bundle is a runtime dependency of the installation, so its dependenc **Mount the registration-only `computer-use` and `browser-use` services in `dsh-base`.** The shared composition would carry rows that only an optional provider bundle needs; a provider bundle can insert the service row from its own patch and dependencies. +**Register preset extensions at runtime.** A separate registration path would need its own ordering, module resolution, generation updates, and inspection behavior. Profile patches keep one effective composition for boot, dumps, validation, and user overrides. + ## Consequences -The Official group carries four optional-bundle entries, each tagged experimental by its experimental name. Their runtime dependencies are installed even while the bundles are disabled. A browser-use or computer-use provider still needs its Service Definition and the provider in a profile patch or composition. +Official discovery does not imply experimental status or a different plugin lifecycle. Switching a bundle off keeps its installed files; failed activation retains the existing saved-selection behavior. Scoped rows appear read-only under their bundle, and removal respects modules retained by live preset generations. Individual and combined composition tests cover the supported presets; live switching and recorded Sessions cover activation and model-visible behavior. diff --git a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md index d5a9b6aea9..13abff45a0 100644 --- a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md @@ -10,9 +10,13 @@ Web 插件页只提供两个可选 bundle:Agent Teams 与语音输入。Auto r ## 决策 -`OPTIONAL_BUNDLES` 列出 Agent Teams、语音输入、Auto review 与 Inspector profile。每个可选 bundle 都声明 `icon` 并导出带 `meta.title` 与 `meta.description` 的 `./locale/*.json`,官方分组因此能渲染本地化标题、描述和图片;`verify-default-product-isolation` 会拒绝缺少这些声明的可选 bundle。Inspector profile 管理按包名挂载的 bundle patch。独立 Inspector 的 `cordis.patch.yml` 通过本地构建入口提供显式仓库 overlay;[Session Inspector](../feature/2026-09-24-session-inspector.zh.md) 管理检查行为与启用选择。 +`OPTIONAL_BUNDLES` 管理安装随附且默认关闭的选项,包括维护中的可选能力与实验性能力。每个 bundle 声明 `icon`,并导出带 `meta.title` 与 `meta.description` 的 `./locale/*.json`;官方分组显示这些元数据。Bundle 的展示方式不决定其[实验性状态](../../../../packages/experimental/README.zh.md#status)。隔离检查校验准入,并继续拒绝非实验 bundle 的实验性依赖。 -可选 bundle 是安装的运行时依赖,其依赖图会随每次 `dsh` 安装一起下载。名单权衡新增安装成本:Auto review 复用安装闭包,Inspector profile 增加检查包、`serve-static`/`open`/`ws` 和 535 个镜像 DevTools 资源(未压缩约 11.95 MiB),但不携带另一个浏览器二进制。Inspector profile 在默认插件列表显示,选中前保持关闭。浏览器操作与电脑操作提供方仍保持显式组合:Playwright MCP、Chrome DevTools MCP 与原生 Cua Driver 运行时二进制会让每次安装多出约 85 MB、21 个包,无论 bundle 是否启用;而随附的 Cua Driver MCP 开关会提供一个安装本身并不携带其可执行文件的能力。这些提供方包保留 locale 显示元数据,供组件行使用。另有两个包因其他原因不纳入:`ptc-runtime-python` 会替换 PTC 运行时,而 `workflow-ptc` 在加载时拒绝非 TypeScript 运行时,且 Web preset 内含 bundle patch 无法触及的 `workflow-ptc` 行;`browser-use-stagehand-native` 在 schema 校验时就要求原生模型名称与 API 密钥,而插件页没有对应的配置表单。 +可选 bundle 的依赖随每次 `dsh` 安装下载。[思考过程翻译](2026-10-05-anonymous-reasoning-translation.zh.md)使用三个实验包,不增加第三方运行时依赖。因此准入需要考虑安装成本,以及开关能否在不增加配置表单的情况下提供可用组合。浏览器与电脑提供方仍使用显式组合,因为它们会增加较大的运行时或要求外部可执行文件。Stagehand 需要模型凭据;Python PTC 替换核心提供方,并与 TypeScript workflow 消费方冲突。演示性钩子、webhook 与模组不进入名单。[Session Inspector](../feature/2026-09-24-session-inspector.zh.md) 管理 Inspector 特有的激活行为。 + +Bundle 通过 [profile patch 操作](../../../../packages/boot/app-boot/README.zh.md)添加作用域行:`preset` 指定声明行,普通 patch 对其子列表生效。完整 Web 预设获得可选模型工具;minimal 保持当前的 shell 与工作目录工具。搜索、Ralph 与终端提供自己的隔离服务。搜索在每个保留的预设修订中按需打开一个内存索引;Host 搜索策略保持不变。徽章技能与标题提供方作用于 Host。已有 Agent 保留其预设代际,后续用户 patch 层继续优先。标题替换等待前一个提供方完成清理。 + +Claude Code 与 Codex 通过独立的 `ON_DEMAND_BUNDLES` 目录按需安装,其成本、版本与离线发现取舍由[按需原生 bundle 决策](2026-10-05-official-on-demand-bundles.zh.md)管理。 ## 考虑过的替代方案 @@ -20,6 +24,8 @@ Web 插件页只提供两个可选 bundle:Agent Teams 与语音输入。Auto r **在 `dsh-base` 中挂载仅负责注册的 `computer-use` 与 `browser-use` 服务。** 共享组合将携带只有可选提供方 bundle 才需要的行;提供方 bundle 可以从自己的 patch 和依赖插入服务行。 +**在运行时注册预设扩展。** 独立注册路径需要自己的排序、模块解析、代际更新与检查行为。Profile patch 为启动、配置导出、校验和用户覆盖保留同一个有效组合。 + ## 后果 -官方分组包含四个可选 bundle 条目,每项都因实验包名而带实验性标签。即使组合包关闭,其运行时依赖也会安装。浏览器操作或电脑操作提供方仍需在 profile patch 或组合中同时挂载其 Service Definition 与提供方。 +官方发现不表示实验性状态,也不引入不同的插件生命周期。关闭 bundle 保留已安装文件;激活失败保留现有的已保存选择行为。作用域行在 bundle 内只读展示,删除操作尊重 live 预设代际仍持有的模块。独立与组合测试覆盖受支持的预设;实时切换与已记录 Session 覆盖激活和模型可见行为。 diff --git a/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.i18n.yaml new file mode 100644 index 0000000000..0187f43093 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.i18n.yaml @@ -0,0 +1,37 @@ +# Bilingual-pair consistency record for 2026-09-30-profile-package-refresh-and-manifest-invalidation.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.md +/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration: + en: ea66719b3dcdd21c + zh: 893364551594ddaf +/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/problem: + en: e59e3f5c61158701 + zh: 2ced78e3c84f604e +/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/decision: + en: a15c7c13f167e5ac + zh: a15c7c13f167e5ac +/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/decision/responsibilities: + en: 3ffd0c78fa204b04 + zh: f468bab00a7ff357 +/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/decision/runtime-resolution: + en: ca45b973e1968533 + zh: 002f23bb70e4fed7 +/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/decision/plugin-manager: + en: 9b04926c175f22e8 + zh: fbf05dd4065b0738 +/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/decision/hmr-package-configuration-expiry: + en: c7de8dafe47a5587 + zh: 6010a70d8451edd1 +/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/decision/future-work: + en: 31818f8f7f80470b + zh: ecb7ac85bb49cf6d +/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/alternatives-considered: + en: 7c1c6d21570cda0d + zh: e6f8334c85f1b404 +/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/verification: + en: 939cedceed200cb2 + zh: c7bb02d0da400c8d +/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/consequences: + en: 1b9e31b4e773fac5 + zh: c140ce07fc64bd55 diff --git a/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.md b/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.md new file mode 100644 index 0000000000..b1b86be199 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.md @@ -0,0 +1,119 @@ +# Agent Note: Refresh runtime resolution after package operations and expire HMR package configuration + +Status: implemented + +English | [中文](2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md) + +## Problem + +Two pieces of Host state did not follow changes while the process ran: + +- **Runtime resolution was built only at startup.** After a GUI install or enablement, nothing published the latest generation, so a bundle's private dependencies never reached the profile fallback and its bare plugins could not be found. After disablement or removal, a new generation could not remove profile-scoped entries or profile-local names, so stale records and package metadata remained. A newly installed package directory is new, and Node holds no stale cache for it; only the table was missing. +- **Package configuration and module evaluation have separate caches.** Clearing a plugin's module cache does not refresh the package.json fields that Node uses for exports, main, imports, and format detection. A package.json can also be imported as a JSON module, in which case its consumers need ordinary module reloads. + +## Decision + +### Responsibilities + +| Owner | Does | Does not | +|---|---|---| +| resolver (`app-boot/src/profile-resolution/resolver.ts`) | `replace()` allows removing `scope: profile` entries and profile-local names | Touch any Node cache | +| plugin-manager | Calls `pluginPackages.refresh()` at fixed points of package operations | Construct resolutions or manage module and package-configuration caches | +| HMR (`packages/boot/hmr`) | Expires package configuration and preserves ordinary reloads for manifests loaded as JSON modules | Reload plugins for configuration-only manifests; handle `node_modules` package replacement | + +### Runtime resolution + +The [generation Note](2026-09-09-profile-resolution-generations.md) owns package identity and Worker inheritance rules. + +Validation of a new generation in `replace()`: + +- `scope: profile` entries may be removed. +- Profile-local package names may be removed. +- Retained profile entries keep their normalized directory, version, and scope, but may change declaring anchor when another selected bundle supplies the same package. Installation mappings and declaring anchors remain unchanged. The profile scope cannot change; a new local name cannot override an existing entry; a published link name cannot select another real directory. + +### Plugin Manager + +| Operation | Publication point | +|---|---| +| Install a new package | After pnpm succeeds and the bundle selection is saved, before reconciliation, whether or not the bundle is enabled | +| Overwrite an installed package | No publication; the result is `restart-required` | +| Enable a bundle | After the selection is saved, before reconciliation | +| Disable a bundle | With HMR, after reconciliation stops its plugins; without HMR, no publication, and running plugins keep the current table | +| Remove | After pnpm remove succeeds, inside the HMR transaction | +| Failed or cancelled install, failed removal | No publication | + +`createRuntimeResolution()` returns a `ProfileRuntimeResolution`, which privately keeps the installation anchor, Harness home, and profile directory it was computed from. Its `computeLatestResolution()` rereads that profile with the same inputs and returns a new object for the latest generation, leaving the original unchanged; Workers receive only the table fields. `PluginPackages.refresh()` calls the current resolution's `computeLatestResolution()` and then `replace()`; a resolution constructed as plain data cannot be refreshed and makes the call throw. + +Without HMR, deselecting a startup bundle does not stop its plugins. While such a bundle remains deselected, later package operations keep the existing runtime table instead of publishing its removal. Other no-HMR installations and removals of non-running bundles still publish normally. + +A successful package operation is not rolled back when publication is rejected afterwards. The result reports the failed runtime application and retains the successful disk changes. Recomputing uses the captured profile directory on disk, not synthetic in-memory layers; a computed resolution without a profile recomputes installation packages only. + +### HMR package-configuration expiry + +In HMR's change dispatch, a changed file named `package.json` outside `node_modules` goes to `PackageManifests.invalidate()`. A manifest in the host dependency graph still requests a host reload; one loaded as a JSON module still reloads its consumers. A configuration-only manifest schedules no module reload. Configuration-owned paths stay with their dedicated watcher. Source changes in the same batch reload afterwards. + +`invalidate(manifest)` records the directory as expired. Afterwards: + +- package.json reads, scope lookups, type lookups, and nearest-manifest lookups below that directory read the current file. Ownership follows the real directory when a consumer reaches the package through a link. +- ESM `ResolveCache` is cleared. Its entries do not record every consulted manifest, and a package entry may resolve outside the package directory. +- CommonJS `_pathCache` is cleared for the same reason. Unrelated requests recompute their resolution without unloading their modules. +- CommonJS loads using the default resolver pass the freshly resolved filename to the native loader. Its private request alias cannot select an older entry; cached module instances remain intact. + +Configuration invalidation alone leaves loaded modules unchanged. Loader entries retain their raw import results before export normalization. HMR matches imported Node module objects to cached ModuleJob namespaces and reloads the loaded URLs. All matching entry records, including disabled entries, update only after a successful reload and remain unchanged on failure. Stopping and restarting a Loader entry resolves its package name again and selects the new entry. + +Entry names are scoped by configuration-tree base URL and retain all distinct imported namespaces. An uninitialized entry contributes the name without erasing loaded namespaces; name-based resolution is used only when none is recorded, or for `cordis:` builtins. Dependency analysis considers every recorded module. Entries sharing a plugin runtime use one replacement operation, while each Loader entry selects its replacement by its original namespace. Module imports and export normalization finish before the old runtime is removed. Failed imports or activation restore the previous modules and plugin implementations and clean up partially activated replacements. + +An instance without a Loader entry has no recorded module identity. If a shared runtime's replacement modules have different plugin callbacks, HMR reports an ambiguity error and rolls back instead of assigning that instance to an arbitrary module. + +Package invalidation lives in `packages/boot/hmr/src/package-manifest.ts`, which encapsulates its Node internal interfaces. `index.ts` dispatches manifest changes and locates loaded entries by module identity. HMR's `node_modules` exclusion is unchanged. + +| Interface | Action | +|---|---| +| modules binding `readPackageJSON`, `getPackageScopeConfig`, `getPackageType` | Replaced: paths in an expired directory read the current file; other calls reach the native method | +| `package_json_reader.getNearestParentPackageJSON` | Replaced: expired directories bypass its JS cache; a lookup without a manifest returns the native absent result | +| The ESM Loader's `ResolveCache` instance | The prototype `get` is replaced for one lookup to obtain the instance and restored at once; the resolution cache is then cleared | +| CommonJS `Module._pathCache` | The request-to-filename cache is cleared | +| CommonJS `Module._load` | Default-resolver requests load by resolved filename; builtins and registered resolve hooks keep the original path | + +The binding's `getNearestParentPackageJSON` is called only by `package_json_reader` and is not replaced. Each HMR instance owns a separate configuration cache. Its replacements are installed at the first expiry and restored when the service is disposed, alongside watcher and reload-queue cleanup. + +### Future Work + +- Online replacement inside `node_modules`, same-path reinstall, changed link targets, and cross-package reload propagation remain unsupported. Package updates need process restart; this does not guarantee that every management result already reports that requirement correctly. +- TSX versions using an asynchronous loader thread keep that thread's package configuration outside these hooks. Synchronizing it is deferred; this change does not provide general Worker cache synchronization. +- CommonJS private-request-cache refresh with registered synchronous resolve hooks is deferred. Those requests retain their original loader behavior. +- Disposing HMR restores the native readers, whose previous cached configuration can become visible again. Preserving invalidation state across HMR replacement is deferred. + +## Alternatives considered + +**Read current disk on every lookup (#4703).** Resolution follows the disk, but module caches stay and running plugins do not reload. Same-URL contents do not take effect, and different URLs load a second version beside the first in one process. It also maintains its own entry resolver, bypasses custom bare-name hooks, and does not refresh runtime resolution, so GUI-installed private dependencies still cannot be found. + +**Reread every observed manifest when runtime resolution publishes (#5496).** This places package-configuration expiry in table publication, decoupled from module caches: overwriting an installed package reports restart, yet the next unrelated publication makes new imports load the new version while the old plugin still runs. Package configuration is content and belongs with module caches, in HMR. + +**Let HMR reload packages inside `node_modules`.** It requires finding affected plugins over a module graph that includes `node_modules`, preventing duplicate evaluation of shared libraries and Cordis, and handling missing dynamic-import edges, module side effects, and Workers. It is not done here; those scenarios keep requiring restart. + +**Reload every plugin for a package.json change.** Configuration-only manifests do not require module evaluation. Manifests actually imported as JSON modules retain ordinary dependency-driven reloads. + +**Re-import reloaded plugins by package name.** It would let an entry rename follow a source reload, but changes `partialReload`'s rule that the loaded URL is the reload unit. Entry renames already take effect when the Loader entry restarts, so this is not changed. + +**Keep one namespace per entry name.** Entries with the same name and base URL can have loaded different modules before and after a manifest change. Keeping only the first or last namespace loses a live module; an uninitialized entry must not erase another entry's imported namespace. + +## Verification + +| Coverage | Location | +|---|---| +| A new generation removes profile entries and local names and still rejects retained-entry changes | `packages/boot/app-boot/tests/profile-resolution.spec.ts` | +| GUI install, deferred enablement, disablement, removal, overwrite, failure, and cancellation, with and without HMR | `packages/boot/plugin-manager/tests/package-reload.spec.ts` | +| Captured-directory recomputation, computed installation-only refresh, and plain-data rejection | `packages/boot/app-boot/tests/profile-resolution.spec.ts`, `packages/boot/app-boot/tests/profile-resolution-service.spec.ts` | +| Sequential no-HMR operations, shared dependency declarers, and disk-success/publication-failure outcomes | `packages/boot/plugin-manager/tests/package-reload.spec.ts` | +| Expired exports, main, imports, type, scope, and nearest manifests; native-reader parity; actual CommonJS loads and retained module instances; the `node_modules` boundary; restoration | `packages/boot/hmr/tests/package-manifest.spec.ts` | +| Configuration-only manifests, JSON-module and host reloads, source reloads, same-name and shared-runtime entries, import and activation rollback, entry restarts, and the `node_modules` exclusion | `packages/boot/hmr/tests/package-manifest-dispatch.spec.ts` | + +Tests need no API key and make no model calls. + +## Consequences + +- With HMR, supported package operations publish the updated private dependencies and remove unused mappings after plugins stop. Without HMR, a pending startup-bundle disablement defers later publications until restart, preserving running plugins' mappings. +- In the supported thread, package.json configuration reads use the changed fields; JSON modules retain their own reload behavior. The separate module, loader-thread, and Worker limitations above still apply. +- Package configuration below expired directories is parsed in JavaScript and must keep the native reader's field and error semantics; tests compare it with the native reader on Node 22, 24, and 26. +- The implementation depends on several Node internal interfaces, and these tests must run again for Node upgrades. diff --git a/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md b/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md new file mode 100644 index 0000000000..9ee201f27a --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md @@ -0,0 +1,119 @@ +# Agent Note: 包操作刷新 runtime resolution 与 HMR 包配置失效 + +Status: implemented + +[English](2026-09-30-profile-package-refresh-and-manifest-invalidation.md) | 中文 + +## Problem + +Host 运行期间有两处状态没有随变化更新: + +- **runtime resolution 只在启动时构造。** GUI 安装或启用 bundle 后,没有人发布最新代,bundle 的私有依赖进不了 profile fallback,它的 bare 插件找不到。停用、卸载后,新一代又不允许移除 profile 范围的条目和本地包名,旧记录和包元数据一直留着。新安装的包目录本身是新的,Node 对它没有旧缓存,缺的只是这张表。 +- **包配置与模块求值使用独立缓存。** 清理插件的模块缓存不会刷新 Node 用于 exports、main、imports 和格式判断的 package.json 字段。package.json 也可以被作为 JSON 模块导入,此时其消费方需要普通的模块重载。 + +## Decision + +### 职责 + +| 负责方 | 做什么 | 不做什么 | +|---|---|---| +| resolver(`app-boot/src/profile-resolution/resolver.ts`) | `replace()` 允许移除 `scope: profile` 条目和 profile 本地包名 | 不碰 Node 的任何缓存 | +| plugin-manager | 在包操作的确定时点调用 `pluginPackages.refresh()` | 不构造 resolution;不管模块缓存与包配置缓存 | +| HMR(`packages/boot/hmr`) | 使包配置失效,并保留作为 JSON 模块加载的 manifest 的普通重载 | 不因仅作为配置的 manifest 重载插件;不处理 `node_modules` 包替换 | + +### runtime resolution + +包身份与 Worker 继承规则由[代际说明](2026-09-09-profile-resolution-generations.zh.md)定义。 + +`replace()` 对新一代的校验: + +- `scope: profile` 的条目可以移除。 +- profile 本地包名可以移除。 +- 保留的 profile 条目保持规范化目录、版本与作用域不变;另一个已选 bundle 提供同一包时,可以更换声明锚点。安装范围的映射与声明锚点保持不变。profile 范围不能变;新本地包名不能覆盖已有条目;已发布的 link 名称不能换真实目录。 + +### plugin-manager + +| 操作 | 发布时点 | +|---|---| +| 安装新包 | pnpm 成功、保存 bundle 选择之后,reconciliation 之前;无论是否立即启用都发布 | +| 覆盖已安装的包 | 不发布,结果为 `restart-required` | +| 启用 bundle | 保存选择后发布,再做 reconciliation | +| 停用 bundle | 有 HMR 时,reconciliation 让插件退出后再发布;没有 HMR 时不发布,运行中的插件继续使用原表 | +| 卸载 | pnpm remove 成功后,在 HMR 事务内发布 | +| 安装失败、取消、卸载失败 | 不发布 | + +`createRuntimeResolution()` 返回 `ProfileRuntimeResolution`,它私有保存构造时的 installAnchor、home 和 profile 目录;`computeLatestResolution()` 用同一组输入重新读取 profile,返回表示最新代的新对象,原实例不变;Worker 只收到表数据。`PluginPackages.refresh()` 调用当前 resolution 的 `computeLatestResolution()` 再 `replace()`;以纯数据构造的 resolution 无法刷新,调用时抛错。 + +没有 HMR 时,取消启动 bundle 的选择不会停止它的插件。只要该 bundle 仍未选中,后续包操作就保留当前 runtime 表,不发布其移除。其他无 HMR 的安装和未运行 bundle 卸载仍正常发布。 + +包操作成功后若发布被拒绝,不回滚已完成的包操作。结果报告运行时应用失败,并保留成功的磁盘变更。重新计算使用捕获的磁盘 profile 目录,不继承内存合成 layers;没有 profile 的已计算 resolution 只重算安装范围的包。 + +### HMR 包配置失效 + +HMR 的文件变化分派中,文件名为 `package.json` 且不在 `node_modules` 内的变化,交给 `PackageManifests.invalidate()`。位于宿主依赖图中的 manifest 仍请求宿主重载;作为 JSON 模块加载的 manifest 仍重载消费方。仅作为配置的 manifest 不安排模块重载。配置专属路径仍由其独立 watcher 处理。同一批的源码变化随后重载。 + +`invalidate(manifest)` 把该目录登记为已失效,之后: + +- 位于该目录下的 package.json 读取、scope 查询、type 查询、最近 package.json 查询,都按当前磁盘解析。消费方经由链接路径访问时,按真实目录判断归属。 +- 清空 ESM `ResolveCache`。其条目不记录查询过的全部 manifest,包入口也可能解析到包目录之外。 +- 基于相同原因清空 CommonJS `_pathCache`。无关请求重新计算解析结果,不卸载其模块。 +- 使用默认 resolver 的 CommonJS 加载把重新解析的文件名交给原生 loader。其私有请求别名不再选中旧入口,已缓存的模块实例保持不变。 + +仅使配置失效不会改变已加载模块。Loader entry 保留导出规范化之前的原始导入结果。HMR 将已导入的 Node 模块对象与缓存中 ModuleJob 的模块命名空间按对象身份匹配,重载已加载 URL。所有匹配的 entry 记录,包括已停用的 entry,仅在重载成功后更新,失败时保持不变。停止并重新启动 Loader entry 时,会重新按包名解析并选择新入口。 + +entry 名称以配置树 base URL 为作用域,并保留所有不同的已导入命名空间。尚未初始化的 entry 只登记名称,不抹去已加载的命名空间;仅在没有记录命名空间或入口为 `cordis:` 内置模块时按名称解析。依赖分析考虑每个已记录模块。共享插件运行时的 entry 使用同一次替换操作,但每个 Loader entry 按自身原命名空间选择替换实现。模块导入与导出规范化在移除旧运行时之前完成。导入或激活失败时,恢复原来的模块和插件实现,并清理已部分激活的替换实例。 + +没有 Loader entry 的实例不具备已记录的模块身份。如果共享运行时的替换模块具有不同的插件回调,HMR 会报告歧义错误并回滚,而不是将该实例随意分配给某个模块。 + +包配置失效实现在 `packages/boot/hmr/src/package-manifest.ts`,封装其使用的 Node internal 接口。`index.ts` 分派 manifest 变更,并按模块身份定位已加载 entry。HMR 的 `node_modules` 排除规则不变。 + +| 接口 | 动作 | +|---|---| +| modules binding 的 `readPackageJSON`、`getPackageScopeConfig`、`getPackageType` | 替换:路径位于已失效目录时按当前磁盘解析,其余调用原生方法 | +| `package_json_reader.getNearestParentPackageJSON` | 替换:已失效目录绕过它的 JS 缓存;没有 manifest 时返回原生的缺失结果 | +| ESM Loader 的 `ResolveCache` 实例 | 临时替换原型的 `get`,取得实例后立即恢复,再清空解析缓存 | +| CJS `Module._pathCache` | 清空请求到文件名的缓存 | +| CJS `Module._load` | 默认 resolver 的请求按解析后的文件名加载;内置模块及注册的 resolve hooks 保留原路径 | + +binding 的 `getNearestParentPackageJSON` 只被 `package_json_reader` 自己调用,不替换。每个 HMR 实例拥有独立的配置缓存,在第一次失效时安装 hook,并在服务销毁时恢复,同时清理 watcher 和重载队列。 + +### 后续工作 + +- 不支持 `node_modules` 内在线替换、同路径重装、link 换目标及跨包重载传播。包更新需要重启进程;这不保证每个管理操作的结果都已正确报告这一要求。 +- 使用异步 loader 线程的 TSX 版本,其线程内包配置不受这些 hook 管理。线程同步留待后续;本次不提供通用 Worker 缓存同步。 +- 注册了同步 resolve hooks 时,CommonJS 私有请求缓存刷新留待后续;这些请求保留原有 loader 行为。 +- 销毁 HMR 会恢复原生 reader,其此前缓存的配置可能重新可见。跨 HMR 替换保留失效状态留待后续。 + +## Alternatives considered + +**每次查找都读当前磁盘(#4703)。** 解析能跟上磁盘,但不清模块缓存,也不重载运行中的插件。同一 URL 时新内容不生效,不同 URL 时新旧版本在同一进程并存。它还要维护一份入口解析实现,并且绕过自定义 bare-name hook。它也没有刷新 runtime resolution,GUI 安装的私有依赖依然找不到。 + +**在 runtime resolution 发布时全量重读已见过的 manifest(#5496)。** 它把包配置失效放进了绑定表发布,和模块缓存脱了钩:覆盖已安装的包时报需要重启,但下一次无关的发布会让新导入拿到新版本,旧插件仍在运行。包配置缓存属于内容,应该和模块缓存由同一方负责,也就是 HMR。 + +**让 HMR 支持 `node_modules` 内的包重载。** 这需要沿包含 `node_modules` 的模块图确定受影响插件,防止共享库和 Cordis 被重复求值,还要处理动态 import 漏边、模块副作用和 Worker。本次不做,这些场景继续要求重启。 + +**package.json 变化时重载所有插件。** 仅作为配置的 manifest 不要求模块求值。实际作为 JSON 模块导入的 manifest 保留普通的依赖驱动重载。 + +**按插件名重新 import 被重载的插件。** 这能让入口改名随源码重载生效,但会改变 `partialReload` 以已加载 URL 为重载单位的规则。入口改名在 Loader entry 重启时已经生效,所以本次不改。 + +**每个 entry 名称只保留一个命名空间。** 名称和 base URL 相同的 entry,可能在 manifest 变化前后加载了不同模块。只保留最早或最后一个命名空间会遗漏仍在使用的模块;尚未初始化的 entry 也不能抹去其他 entry 的已导入命名空间。 + +## Verification + +| 覆盖面 | 位置 | +|---|---| +| 新一代可移除 profile 条目与本地包名、仍拒绝修改保留条目 | `packages/boot/app-boot/tests/profile-resolution.spec.ts` | +| GUI 安装、延迟启用、停用、卸载、覆盖、失败、取消,有无 HMR | `packages/boot/plugin-manager/tests/package-reload.spec.ts` | +| 捕获目录后的重新计算、无 profile 的已计算 resolution 刷新、纯数据拒绝 | `packages/boot/app-boot/tests/profile-resolution.spec.ts`、`packages/boot/app-boot/tests/profile-resolution-service.spec.ts` | +| 无 HMR 连续操作、共享依赖声明来源、磁盘成功但发布失败 | `packages/boot/plugin-manager/tests/package-reload.spec.ts` | +| 失效后的 exports、main、imports、type、scope、最近 package.json,原生 reader 对照,实际 CommonJS 加载及旧模块实例保留,node_modules 边界,恢复 | `packages/boot/hmr/tests/package-manifest.spec.ts` | +| 配置专用 manifest、JSON 模块与宿主重载、源码重载、同名及共享运行时的 entry、导入与激活失败回滚、entry 重启和 node_modules 排除规则 | `packages/boot/hmr/tests/package-manifest-dispatch.spec.ts` | + +测试不需要 API key,不调用模型。 + +## Consequences + +- 有 HMR 时,支持的包操作发布更新后的私有依赖,并在插件停止后移除不再需要的映射。没有 HMR 时,待生效的启动 bundle 停用会将后续发布延迟到重启,保留运行中插件的映射。 +- 在支持的线程内,package.json 配置读取使用更新后的字段;JSON 模块保留自己的重载行为。上述模块、loader 线程及 Worker 的独立限制仍然适用。 +- 包配置读取改由 JS 解析 package.json,需要与原生读取保持字段和错误语义一致;测试在 Node 22、24、26 上与原生读取逐项比较。 +- 依赖多个 Node internal 接口,Node 升级时需要重跑这些测试。 diff --git a/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.i18n.yaml new file mode 100644 index 0000000000..ee7758db34 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-10-05-anonymous-reasoning-translation.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.md +/agent-note-anonymous-translation-remains-separate-from-model-history: + en: ab8fee99b1f6d828 + zh: 0e324a42eebc1403 +/agent-note-anonymous-translation-remains-separate-from-model-history/problem: + en: 5e10347fa87f98b5 + zh: cf3569631af8d66f +/agent-note-anonymous-translation-remains-separate-from-model-history/decision: + en: f13c134f53ed63c9 + zh: 868aacd453d26c82 +/agent-note-anonymous-translation-remains-separate-from-model-history/alternatives-considered: + en: 083d50f8564b0378 + zh: 50af332e64ad06f3 +/agent-note-anonymous-translation-remains-separate-from-model-history/consequences: + en: 57c774c6fb14a1b4 + zh: 5a05393e6f1d25b1 diff --git a/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.md b/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.md new file mode 100644 index 0000000000..0fa78afa4e --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.md @@ -0,0 +1,31 @@ +# Agent Note: Anonymous translation remains separate from model history + +Status: implemented + +English | [中文](2026-10-05-anonymous-reasoning-translation.zh.md) + +## Problem + +Readers need reasoning in their chosen language without another model credential or replacement of the original assistant output. Translation needs an owner for external requests, cancellation, and failures independent of Agent execution. + +## Decision + +The experimental translator owns explicit Google or Bing routing, provider-language normalization, and bounded network requests. Its Service Definition and both providers share one package because their current consumers use the same operation and lifetime. The optional GUI consumer owns settings, paragraph scheduling, per-disclosure caches, and translated display. Its authenticated Remote calls the translator; the agent loop and Session event types remain independent. + +Bing is the configured default. Enabling the bundle permits transmission of expanded reasoning to the chosen service, disclosed in plugin metadata and settings. Failure never selects another provider. Original remains available; translated text neither replaces persisted assistant output nor enters model input. The [optional-bundle admission policy](2026-09-21-experimental-capabilities-as-optional-bundles.md) keeps the feature disabled until selected. + +The reasoning-body chain carries original text and streaming status. Native Chat owns its original Markdown fallback; consumers do not replace the assistant renderer or alter Session projection. Only expanded bodies start translation. Collapsing, changing selection, or unloading aborts outstanding work and excludes late results. + +## Alternatives considered + +**Translate through the conversation's LLM provider.** This adds model requests and depends on model credentials, pricing, and routing. Anonymous translation has separate availability and requires none of those credentials. + +**Rewrite reasoning in the Session or provider stream.** This changes the original trajectory and can affect model reasoning passback. Display translation preserves the original model history. + +**Silently try another endpoint after failure.** This sends potentially private text to another operator without an explicit selection. Retry uses the same provider; switching providers is a user action. + +## Consequences + +Anonymous translation adds no third-party translation library or model-token usage. Anonymous endpoints provide no developer availability guarantee. Both providers send source text in POST request bodies. Translations may alter Markdown or code formatting and do not translate collapsed summaries. + +The translator retains Session-bound results under the [shared machine-translation persistence decision](2026-10-05-persistent-machine-translation.md); its stateless calls do not record results. diff --git a/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.zh.md b/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.zh.md new file mode 100644 index 0000000000..293f74a5ee --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-anonymous-reasoning-translation.zh.md @@ -0,0 +1,31 @@ +# Agent Note: 免登录翻译独立于模型历史 + +Status: implemented + +[English](2026-10-05-anonymous-reasoning-translation.md) | 中文 + +## 问题 + +读者需要用指定语言阅读思考内容,同时无需额外模型凭据,也不替换原始 assistant 输出。翻译需要独立于 Agent 执行的所有者,负责外部请求、取消与失败。 + +## 决策 + +实验性 translator 负责显式 Google 或 Bing 路由、Provider 语言标签映射及有界网络请求。Service Definition 与两种 Provider 共用一个包,因为当前消费者使用相同操作与生命周期。可选 GUI 消费者负责设置、段落调度、展开组件内缓存及译文展示。其经过身份验证的 Remote 调用 translator;agent loop 与 Session 事件类型保持独立。 + +配置的默认服务为 Bing。启用 Bundle 允许将展开的思考内容发送给所选服务,插件元数据与设置公开说明接收方。失败时不选择其他 Provider。原文始终可访问;译文不替换持久化 assistant 输出,也不进入模型输入。[可选 Bundle 准入规则](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md) 保证功能在显式选择前保持关闭。 + +reasoning-body chain 传递原始文本与流式状态。原生 Chat 负责原始 Markdown fallback;消费者不替换 assistant 渲染器,也不改变 Session projection。只有展开内容才开始翻译。折叠、切换设置或卸载都会中止未完成工作并忽略迟到结果。 + +## 考虑过的替代方案 + +**通过对话使用的 LLM Provider 翻译。** 这会增加模型请求,并依赖模型凭据、定价与路由。免登录翻译有独立的可用性,且无需这些凭据。 + +**重写 Session 或 Provider 流中的思考内容。** 这会改变原始轨迹,并可能影响思考内容回传。展示翻译保留原始模型历史。 + +**失败后静默尝试另一端点。** 这会在没有显式选择的情况下将潜在私有文本发送给另一个运营方。重试使用同一 Provider;切换 Provider 是用户操作。 + +## 后果 + +匿名翻译不增加第三方翻译库或模型 token 消耗。免登录端点不保证开发者可用性。两种服务均通过 POST 请求体发送原文。译文可能改变 Markdown 或代码格式,也不翻译折叠摘要。 + +translator 按[统一的机器翻译持久化决策](2026-10-05-persistent-machine-translation.zh.md)保留绑定 Session 的结果;无状态调用不记录结果。 diff --git a/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.i18n.yaml new file mode 100644 index 0000000000..4af09bcaae --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-10-05-official-on-demand-bundles.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.md +/agent-note-discover-official-bundles-without-installing-their-runtimes: + en: 1f2387f10628638b + zh: c835a8ee615e9da6 +/agent-note-discover-official-bundles-without-installing-their-runtimes/problem: + en: a590bcef6c9760c0 + zh: 19e6f61c2c7d8d3b +/agent-note-discover-official-bundles-without-installing-their-runtimes/decision: + en: 31e3c52841f31a11 + zh: ac2ad6e54ef9e150 +/agent-note-discover-official-bundles-without-installing-their-runtimes/alternatives-considered: + en: 42cb0f808a145871 + zh: 225012615d1ecdf5 +/agent-note-discover-official-bundles-without-installing-their-runtimes/consequences: + en: 899b07d90024e019 + zh: 01d22ac4b2444feb diff --git a/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.md b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.md new file mode 100644 index 0000000000..0b0ad0cf55 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.md @@ -0,0 +1,37 @@ +# Agent Note: Discover Official bundles without installing their runtimes + +Status: implemented + +English | [中文](2026-10-05-official-on-demand-bundles.zh.md) + +## Problem + +Native subagent integrations bring their own runtimes. Shipping those dependencies with every DSH installation charges that cost to people who never delegate to them, while requiring a package name hides the integrations from the Plugins page. + +## Decision + +`ON_DEMAND_BUNDLES` admits the Claude Code and Codex bundles to an installation-owned catalog. The catalog embeds their package-owned localized metadata and icons, so discovery works offline without resolving or importing either package. Official identifies project maintenance; optional delivery and experimental maturity remain independent properties. + +Selection uses the ordinary third-party bundle installer, profile dependency, scoped patches, compatibility evaluation, progress, cancellation, registry fallback, and build-script approval. Native authentication and configuration remain provider-owned. + +The running installation owns the target. Published applications offer the exact DSH release version and save an exact registry dependency pin. Source Web and development Desktop offer ordinary `link:` dependencies into the same checkout as the running CLI, using its prepared workspace dependencies and peers. The source target follows the actual installation, never the current working directory or a prerelease suffix; an incomplete checkout does not fall back to the registry. A development checkout can share a version string with different published code, so replacing an installed registry copy or another checkout’s link compares the source as well as the version. Replacement is explicit, preserves selection, and reports required restarts; startup does not download providers. [Plugin Manager](../../../../packages/boot/plugin-manager/README.md#use-this-package) owns the operational details. + +Every Official package remains in the DSH release family. Packed DSH dependencies, optional dependencies, and peers use the exact family version. Publication prerequisites place catalog packages before the DSH package advertising them without adding their runtimes to DSH production dependencies. Catalog verification rejects missing metadata, unsupported membership, and stale generated content. + +The [shipped optional-bundle policy](../process/2026-09-15-shipped-optional-bundles.md) still owns lightweight installation-provided bundles. The [optional-composition decision](2026-09-21-experimental-capabilities-as-optional-bundles.md) owns shared patch and preset-generation behavior. Native bundles require the full Web preset targets; custom profiles mount their providers and tools explicitly. The [identity compatibility decision](2026-10-06-web-desktop-plugin-identity-compatibility.md) owns retained names, selectors, and deferred naming cleanup. + +## Alternatives considered + +**Ship the native runtimes with every installation.** This makes first activation offline, but increases the default download for unused integrations. + +**Query the registry to build the discovery list.** This avoids embedded metadata, but makes discovery depend on network availability and does not bind the offered package version to the running application. + +**Give Official packages a separate installer or activation registry.** A second path would duplicate package-operation and profile-composition behavior. Installation-owned discovery data can reuse the existing third-party mechanisms. + +**Always install the registry package with the running version.** Version equality identifies a published release, but cannot identify unreleased source changes. It can deliver an older provider API and a provider-only patch to a development Host that expects current tool contributions. + +**Build a separate development registry or artifact cache.** Ordinary local links already keep provider code and its dependencies in the running checkout. A second delivery system would add lifecycle and invalidation work without improving that development promise. + +## Consequences + +Registry delivery requires a reachable registry; development delivery requires a prepared, built checkout and normal rebuild/restart discipline. Equal source version strings do not certify identical build outputs. Off only deselects; Remove remains separate and respects retained preset modules. Activation failure can retain the saved dependency and selection; there is no rollback. Offline metadata, release pins, source-target selection, same-version source mismatches, clean installed artifacts, and release ordering have dedicated checks. Native-provider authentication is not part of catalog discovery. diff --git a/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.zh.md b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.zh.md new file mode 100644 index 0000000000..6fbdaa6e8f --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.zh.md @@ -0,0 +1,37 @@ +# Agent Note: 发现 Official bundle 而不安装其运行时 + +Status: implemented + +[English](2026-10-05-official-on-demand-bundles.md) | 中文 + +## 问题 + +原生子智能体集成携带自己的运行时。把这些依赖放进每次 DSH 安装,会让从不向它们委派任务的用户也承担成本;要求用户知道包名,又会让插件页无法展示这些集成。 + +## 决策 + +`ON_DEMAND_BUNDLES` 将 Claude Code 和 Codex bundle 纳入安装方维护的目录。目录内嵌包自身拥有的本地化元信息和图标,因此发现过程离线可用,无需解析或导入这两个包。Official 表示项目维护;可选交付与实验成熟度仍是独立属性。 + +选择操作复用普通第三方 bundle 的安装器、profile 依赖、作用域 patch、兼容性判断、进度、取消、注册表回退和构建脚本审批。原生认证与配置仍由提供方拥有。 + +当前运行的安装决定目标。已发布应用提供精确的 DSH 发布版本,并保存精确的注册表依赖版本。源码 Web 和开发 Desktop 提供指向当前 CLI 所在 checkout 的普通 `link:` 依赖,使用其中已准备好的工作区依赖和 peer。源码目标跟随实际安装位置,不由当前工作目录或预发布版本后缀决定;不完整的 checkout 不回退到注册表。开发 checkout 可以与内容不同的已发布代码共用版本字符串,因此替换已安装的注册表副本或其他 checkout 的链接时,同时比较来源和版本。替换需显式触发,保留选择状态,并报告是否需要重启;启动时不下载提供方。[插件管理器](../../../../packages/boot/plugin-manager/README.zh.md#use-this-package)拥有操作细节。 + +所有 Official 包仍属于 DSH 发布家族。打包后的 DSH 依赖、可选依赖与 peer 均使用家族精确版本。发布前置关系要求先发布目录包,再发布宣传它们的 DSH 包,同时不把这些运行时加入 DSH 生产依赖。目录校验拒绝缺失的元信息、不受支持的成员和过期生成内容。 + +[随附可选 bundle 策略](../process/2026-09-15-shipped-optional-bundles.zh.md)仍管理安装随附的轻量 bundle。[可选组合决策](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)管理共享 patch 与预设代际行为。原生 bundle 要求完整 Web 预设目标存在;自定义 profile 显式挂载提供方和工具。[身份兼容决策](2026-10-06-web-desktop-plugin-identity-compatibility.zh.md)管理保留名称、选择器和暂缓的命名清理。 + +## 考虑过的替代方案 + +**每次安装都附带原生运行时。** 首次激活可离线完成,但会为未使用的集成增加默认下载量。 + +**查询注册表构建发现列表。** 可以避免内嵌元信息,但发现过程依赖网络可用性,且无法把提供的包版本绑定到运行中的应用。 + +**为 Official 包增加独立安装器或激活注册表。** 第二条路径会重复包操作和 profile 组合行为。安装方维护的发现数据可以复用已有第三方机制。 + +**始终安装与当前版本相同的注册表包。** 版本相同可以标识已发布版本,但不能标识尚未发布的源码修改。这可能向期待当前工具贡献的开发 Host 交付旧提供方 API 和仅含提供方的 patch。 + +**建立独立的开发注册表或产物缓存。** 普通本地链接已能让提供方代码及其依赖来自当前 checkout。第二套交付系统会增加生命周期和失效处理,而不改善这一开发保证。 + +## 后果 + +注册表交付要求注册表可达;开发交付要求准备并构建 checkout,遵循正常的重新构建和重启流程。源码版本字符串相同不证明构建输出相同。关闭仅取消选择;移除保持独立,并尊重预设仍保留的模块。激活失败可以保留已保存的依赖和选择,不加入回滚。离线元信息、发布版本锁定、源码目标选择、同版本来源差异、干净安装产物和发布顺序有专门检查。原生提供方认证不属于目录发现过程。 diff --git a/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.i18n.yaml new file mode 100644 index 0000000000..33da8f55cb --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-10-05-paid-native-flash-reasoning-translation.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.md +/agent-note-paid-native-flash-translation-uses-independent-queries: + en: 37a2416d68e5a73d + zh: ae439aa6ec6a2feb +/agent-note-paid-native-flash-translation-uses-independent-queries/problem: + en: 1029e393c5794564 + zh: 6044cd7da7fc0947 +/agent-note-paid-native-flash-translation-uses-independent-queries/decision: + en: be443b5b9ed2d0a4 + zh: 399c3c196a3a5858 +/agent-note-paid-native-flash-translation-uses-independent-queries/alternatives-considered: + en: e5ec698f302c8ae8 + zh: b3a7d4f037ca6bdb +/agent-note-paid-native-flash-translation-uses-independent-queries/consequences: + en: 74d2e9a7f22b6555 + zh: 249e56f0c7071fe3 diff --git a/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.md b/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.md new file mode 100644 index 0000000000..948ae90f1b --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.md @@ -0,0 +1,27 @@ +# Agent Note: Paid native Flash translation uses independent queries + +Status: implemented + +English | [中文](2026-10-05-paid-native-flash-reasoning-translation.zh.md) + +## Problem + +Readers may choose model-based translation in exchange for additional charges. Reusing their conversation request would inherit its history and generation controls, while routing through an arbitrary configured model would conceal the translation's billing and receiver choices. + +## Decision + +Paid translation is an explicit alternative to the [default anonymous path](2026-10-05-anonymous-reasoning-translation.md). Only active built-in `deepseek-account` and `deepseek-official` owners with configured credentials and the exact `deepseek-flash` catalog ID qualify. Display names and versions do not participate, and pi-ai routes are excluded. Each fragment receives one fresh text-only query, a simple translation instruction and disabled thinking; no previous fragment or main history is supplied. + +The original Session keeps `plugin:translator/request`, the [shared translation request record](2026-10-05-persistent-machine-translation.md), with exact model-visible input in optional `metadata.modelRequest` before dispatch. The existing `appendPluginRecord` writer snapshots its JSON payload and marks it ignorable without replacing main history. Read-only cache lookup precedes native admission and needs no live Session. A cache miss uses the active Session writer and flushes before dispatch; the GUI joins normal Session activation when needed, with its ordinary startup and resume effects. Format migration retains these informational records on a best-effort basis. Translation does not supply the main Session identity to the native request extensions, so a fragment cannot authorize uploading its full conversation log. The configured paid choice is enforced by the Host before billing. + +## Alternatives considered + +**Reuse the conversation's model call.** Its history, model and thinking settings are unrelated to a translation fragment, and session-log delivery could transmit more than the selected text. A fresh native Flash call makes these choices explicit. + +**Offer every model or version label.** This requires additional receiver and billing decisions. The stable `deepseek-flash` ID supports future versions while preserving the requested built-in route restriction. + +**Replace the original assistant output.** Translated text is retained separately as a shared result record; original reasoning remains the source of main model history. + +## Consequences + +Paid queries consume additional input and output tokens. The GUI discloses billing for uncached fragments. Reopening reuses persisted successful results, including when native credentials or model configuration are unavailable. Failures never choose another route. Local audit retention is independent of provider log delivery, and anonymous translation remains the default. diff --git a/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.zh.md b/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.zh.md new file mode 100644 index 0000000000..9feb7e8dff --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-paid-native-flash-reasoning-translation.zh.md @@ -0,0 +1,27 @@ +# Agent Note: 付费原生 Flash 翻译使用独立查询 + +Status: implemented + +[English](2026-10-05-paid-native-flash-reasoning-translation.md) | 中文 + +## 问题 + +读者可选择模型翻译并承担额外费用。复用对话请求会继承其历史及生成控制,而路由到任意已配置模型会隐藏翻译的计费及接收方选择。 + +## 决策 + +付费翻译是[默认匿名路径](2026-10-05-anonymous-reasoning-translation.zh.md)的显式替代选项。只有内置 `deepseek-account` 与 `deepseek-official` 所有者处于活动状态、凭据已配置,且目录包含 ID 恰好为 `deepseek-flash` 的模型时才符合条件。显示名称和版本不参与匹配,pi-ai 路由被排除。每个片段执行一次新的纯文本查询,使用简单翻译指令并关闭思考,不携带此前片段或主历史。 + +原始 Session 在派发前通过实验性记录 `plugin:translator/request` 在可选 `metadata.modelRequest` 中保留精确的模型可见请求,采用[统一的翻译请求记录](2026-10-05-persistent-machine-translation.zh.md)。现有 `appendPluginRecord` 写入器快照其 JSON 载荷并标为可忽略,不替换主历史。只读缓存查找先于原生准入,且不要求 Session 已激活。缓存未命中时使用已激活 Session 的写入器,在派发前刷新;GUI 按需等待正常 Session 激活,保留普通启动与恢复行为。格式迁移以尽力而为的方式保留这些信息性记录。翻译不向原生请求扩展提供主 Session 身份,因此单个片段不能授权上传完整对话日志。Host 在计费前强制要求已配置的付费选择。 + +## 考虑过的替代方案 + +**复用对话的模型调用。** 其历史、模型及思考设置与翻译片段无关,Session 日志派送还可能传输超出所选文本的内容。新的原生 Flash 调用使这些选择明确。 + +**提供所有模型或版本标签。** 这需要额外的接收方及计费决策。稳定的 `deepseek-flash` ID 支持未来版本,同时保留所要求的内置路由限制。 + +**替换原始 assistant 输出。** 译文通过统一的结果记录单独保留;原始思考仍是主模型历史的来源。 + +## 后果 + +付费查询消耗额外的输入和输出 token。GUI 说明未缓存片段的计费。重新展开复用已持久化的成功结果,即使原生凭据或模型配置不可用。失败时不选择其他路由。本地审计保留独立于提供方日志派送,匿名翻译仍为默认选项。 diff --git a/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.i18n.yaml new file mode 100644 index 0000000000..d54e1be33d --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-10-05-persistent-machine-translation.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.md +/agent-note-machine-translation-results-use-shared-session-records: + en: 0587db1f8b7b3c6d + zh: 63af01050024d4d1 +/agent-note-machine-translation-results-use-shared-session-records/problem: + en: 4194ba9786778876 + zh: 6b99613a4d0dd5d2 +/agent-note-machine-translation-results-use-shared-session-records/decision: + en: fb8800dede33177b + zh: b3f5098e54d5d982 +/agent-note-machine-translation-results-use-shared-session-records/alternatives-considered: + en: 3f251877332d74cc + zh: 61991d5fd4704a99 +/agent-note-machine-translation-results-use-shared-session-records/consequences: + en: c51bbb4ec6b7f20b + zh: 856dbfdf47509d90 diff --git a/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.md b/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.md new file mode 100644 index 0000000000..0826274392 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.md @@ -0,0 +1,35 @@ +# Agent Note: Machine translation results use shared Session records + +Status: implemented + +English | [中文](2026-10-05-persistent-machine-translation.zh.md) + +## Problem + +Readers need a saved translation when they reopen reasoning or restart the application. A disclosure-only cache repeats external requests, can change an already displayed result and can incur repeated model charges. Storing only model inputs leaves successful translations unavailable after restart. + +## Decision + +The translator owns `plugin:translator/request` and `plugin:translator/result` for every provider. A request records its selected provider, exact source text, source and target languages, and recipe identity. Optional metadata carries provider-owned request details. A result references the request's Session sequence and retains the completed translated text. Requests are durable before dispatch, and results are durable before they return to the GUI. + +The browser binds displayed translations to the disclosure's Session, and the Host validates the configured provider and explicit target language. The authenticated Client supplies the original text and Session identity; the Host does not verify that the text belongs to that Session. Successful records are reusable across disclosure lifetimes and restarts. Identity includes the provider and recipe, so changing the receiver, language or translation settings cannot substitute another result. Identical concurrent translations wait for one result; distinct fragments can run in parallel. + +Read-only lookup precedes provider work. New records use the active Session's existing writer through `appendPluginRecord` and targeted flush. An uncached inactive Session rejects before provider work; the GUI consumer joins normal Session controller activation and retries once. + +These records accompany the [original reasoning](2026-10-05-anonymous-reasoning-translation.md) and never replace its events or model input. They use the existing experimental record writer and best-effort format-migration retention. Provider availability gates new requests; a saved result remains readable without another inference. Stateless translator calls remain available without a Session. + +## Alternatives considered + +**Keep only disclosure-local results.** Reopening loses the displayed value and sends the same text again, with possible repeat charges. + +**Give anonymous and model translations different result formats.** The reader needs the same source-to-translation relationship from every provider. Provider-owned audit details belong in optional metadata. + +**Replace original assistant reasoning.** A translation is derived data. Keeping it alongside the original preserves model history and access to the source. + +**Open a separate writer for historical translations.** This competes with normal Session activation and requires coordination between two writer owners. The GUI already activates opened conversations; using its existing controller keeps that ownership in one place. + +## Consequences + +The Session retains translated text and request identity in addition to original reasoning. Only completed persisted results supply cached text; failed or incomplete attempts do not. A completion that has already entered the append-only log may survive a late caller cancellation. Records do not introduce released Session schema history, and future format migrations may drop experimental state. + +Saved results remain readable without activation. A new translation may wait for normal Session activation, including preset startup hooks, resume markers, interrupted-turn repair, and any required generation migration. These ordinary lifecycle effects can append context; translation does not bypass them. Cache reads leave older generations unpublished. The translator owns no second writer and requires no core lifecycle changes. diff --git a/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.zh.md b/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.zh.md new file mode 100644 index 0000000000..bf9b9b2c56 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-persistent-machine-translation.zh.md @@ -0,0 +1,35 @@ +# Agent Note: 机器翻译结果使用统一的 Session 记录 + +Status: implemented + +[English](2026-10-05-persistent-machine-translation.md) | 中文 + +## 问题 + +读者在重新展开思考内容或重启应用后仍需要已保存的译文。只在展开区域缓存会重复外部请求、可能改变已显示结果,也可能重复产生模型费用。仅存储模型输入无法在重启后恢复成功译文。 + +## 决策 + +translator 对所有 Provider 统一使用 `plugin:translator/request` 和 `plugin:translator/result`。请求记录所选 Provider、精确原文、源语言和目标语言,以及翻译规则标识。可选 metadata 保存 Provider 自有请求细节。结果引用请求的 Session 序号并保留完成后的译文。请求在派发前持久化,结果在返回 GUI 前持久化。 + +浏览器将展示翻译绑定到展开区域的 Session,Host 校验配置的提供方和显式目标语言。已认证的 Client 提供原文与 Session 身份;Host 不校验原文是否属于该 Session。成功记录可跨展开组件生命周期和重启复用。身份包含 Provider 和翻译规则,因此改变接收方、语言或翻译设置不会混用其他结果。相同的并发翻译等待同一结果,不同片段可并行执行。 + +只读查找先于 Provider 调用。新记录通过已激活 Session 的现有写入器调用 `appendPluginRecord` 并定向刷新。未激活 Session 缓存未命中时,在 Provider 调用前拒绝;GUI 消费者等待 Session controller 的正常激活,再重试一次。 + +这些记录与[原始思考内容](2026-10-05-anonymous-reasoning-translation.zh.md)一同保留,从不替换其事件或模型输入。它们使用现有的实验性记录写入器,格式迁移采用尽力保留语义。Provider 可用性限制新请求,已保存结果无需再次推理即可读取。不指定 Session 时,translator 仍支持无状态调用。 + +## 考虑过的替代方案 + +**只保留展开区域内的结果。** 重新展开会丢失已展示值并再次发送同样的文本,可能重复计费。 + +**匿名翻译与模型翻译使用不同的结果格式。** 读者对所有 Provider 都需要相同的原文到译文关系。Provider 自有审计细节属于可选 metadata。 + +**替换原始 assistant 思考内容。** 翻译属于派生数据。与原文并存可以保留模型历史和原文访问能力。 + +**为历史翻译另开写入器。** 这会与正常 Session 激活竞争,并需要协调两个写入所有者。GUI 已经会激活打开的对话;使用其现有 controller 可将所有权集中在一处。 + +## 后果 + +Session 在原始思考内容之外保留译文和请求身份。只有完成并持久化的结果提供缓存译文,失败或未完成尝试不提供。已进入 append-only 日志的完成记录可能在调用者迟到取消后保留。记录不引入已发布的 Session schema 历史,未来格式迁移可能丢弃实验性状态。 + +已保存结果无需激活即可读取。新翻译可能等待正常 Session 激活,包括 preset 启动钩子、恢复标记、中断轮次修复及所需的 generation 迁移。这些普通生命周期行为可能追加上下文;翻译不会绕过它们。缓存读取不会发布旧 generation。translator 不拥有第二个写入器,也不需要修改核心生命周期。 diff --git a/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.i18n.yaml new file mode 100644 index 0000000000..0d4737ef26 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-10-05-shared-agents-root-instructions.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.md +/agent-note-shared-agents-root-joins-the-user-global-instruction-chain: + en: d29282678b243512 + zh: f81e134f59df5491 +/agent-note-shared-agents-root-joins-the-user-global-instruction-chain/problem: + en: 8fd7583122a2b082 + zh: 032acf0c713780f7 +/agent-note-shared-agents-root-joins-the-user-global-instruction-chain/decision: + en: 69008d5fd1bb091b + zh: a7c4d0334834e6c7 +/agent-note-shared-agents-root-joins-the-user-global-instruction-chain/alternatives-considered: + en: 825c2470ba83573e + zh: d0ffd4fea4f8f0d0 +/agent-note-shared-agents-root-joins-the-user-global-instruction-chain/consequences: + en: cc30674b9b41d0f3 + zh: 36ac52fb078dbec4 diff --git a/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.md b/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.md new file mode 100644 index 0000000000..5687130961 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.md @@ -0,0 +1,37 @@ +# Agent Note: Shared agents root joins the user-global instruction chain + +Status: implemented + +English | [中文](2026-10-05-shared-agents-root-instructions.zh.md) + +## Problem + +DSH loads one user-global instruction file, `$DSH_HOME/AGENTS.md`. People who run several agent tools keep the instructions they share across those tools in the shared agent configuration root (`~/.agents/AGENTS.md`, or `$DSH_AGENTS_HOME`) that `dsh-skill-filesystem` already scans for skills. Those instructions reached DSH only when the person copied or symlinked them into the harness home, which duplicates one source of truth into DSH-owned data and leaves no way to tell which copy a session loaded. + +## Decision + +The user-global instruction scope has two ordered roots: `$DSH_HOME/AGENTS.md` first, then `/AGENTS.md`. The instruction loader resolves the harness home from `$DSH_HOME` or `~/.dsh`, and `agentsHome` from `$DSH_AGENTS_HOME` or `~/.agents`. `resolveAgentsHome` and `agentsHomeDisplay` in `dsh-home-paths` own that resolution for both `dsh-agent-instructions` and `dsh-skill-filesystem`, so the shared root has one resolver whose tilde expansion and blank-environment handling match the harness home. + +The two files are candidates of one candidate group. They deduplicate by trimmed content, so identical copies render once from the harness home, and an unobservable candidate preserves the group's last-good state. A duplicate of budget-retained content keeps being reconciled, so deleting or changing the retained candidate promotes it; a byte-budget omission leaves that candidate and its content duplicates unprobed. The harness-home file keeps the `user-global` scope key that earlier sessions recorded; the shared file carries `agents-global`, and reconciliation probes both roots on resume, so an external edit, a removal, or a newly created file appears as a change notice instead of a new baseline. + +A user-global scope directory is an internal reconciliation key, so it stays out of model-visible prose: a global delta states `These user-global instructions apply to all work.` instead of naming a scope directory, and a project path whose first component would collide with one renders with a leading `.` so it keeps its own scope. + +`USER_GLOBAL_DIRECTORIES` fixes the root order, and discovery, baseline reconciliation, and deduplication all iterate it. The harness home therefore decides content duplicates and keeps its position in the baseline, while distinct content from the shared root follows it. + +## Alternatives considered + +**Replace the harness-home file with the shared root.** The harness home owns DSH-specific configuration, and its `AGENTS.md` is the user-global file the package documents, tests, and renders today. Removing it would drop a DSH-only layer for anyone who shares instructions across tools, and sessions that recorded `user-global` state would lose their referent. + +**Load the shared root only when the harness-home file is absent.** The two files then alternate as the session-visible user-global file, which makes a DSH-specific file silently suppress the shared one instead of composing with it. + +**Load the shared file as a second candidate name in the harness home.** `instructionFileCandidates` names files within one directory; the two roots are different directories, so the option cannot express this chain without redefining candidate semantics. + +**Rely on path identity instead of content for the duplicate case.** The common layout is `$DSH_HOME/AGENTS.md` symlinked to `~/.agents/AGENTS.md`, which is two paths for one file. Path identity drops neither, so the same text would render twice under two display labels. + +## Consequences + +- Shared instructions load without a copy or symlink, and the symlinked layout still renders once, from the harness home. +- A session that started before this change receives one transition when the shared file exists with content that differs from the harness-home file. +- `dsh-agent-instructions` exposes no `dshHome` or `agentsHome` config field; its two roots are process policy. `dsh-skill-filesystem` retains its explicit home fields, which select that provider's own skill roots. +- The two user-global files share one fate on probe failure: the group preserves its last-good state together, because one digest decides which member renders. +- `packages/context/agent-instructions/tests/agent-instructions.spec.ts` pins the two-root order, content deduplication, duplicate promotion after the retained candidate changes or disappears, the global delta wording, the `replace` and `remove` transitions, `$DSH_AGENTS_HOME` resolution, the `~/.agents` display label, and the escaped project directory name. diff --git a/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.zh.md b/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.zh.md new file mode 100644 index 0000000000..bea805d075 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-05-shared-agents-root-instructions.zh.md @@ -0,0 +1,37 @@ +# Agent Note: 共享 agents 根目录加入用户全局指令链 + +Status: implemented + +[English](2026-10-05-shared-agents-root-instructions.md) | 中文 + +## 问题 + +DSH 只加载一个用户全局指令文件:`$DSH_HOME/AGENTS.md`。同时使用多个 agent 工具的人,会把跨工具共享的指令放在共享 agent 配置根目录(`~/.agents/AGENTS.md`,或 `$DSH_AGENTS_HOME`)——`dsh-skill-filesystem` 已经在该根目录下扫描 skill。这些指令只有被复制或 symlink 到 harness home 之后才能进入 DSH,这会把唯一真源复制进 DSH 自有数据,也让人无法判断会话加载的是哪一份。 + +## 决策 + +用户全局指令 scope 有两个有序根目录:先是 `$DSH_HOME/AGENTS.md`,然后是 `/AGENTS.md`。指令加载器从 `$DSH_HOME` 或 `~/.dsh` 解析 harness home,从 `$DSH_AGENTS_HOME` 或 `~/.agents` 解析 `agentsHome`。`dsh-home-paths` 中的 `resolveAgentsHome` 与 `agentsHomeDisplay` 为 `dsh-agent-instructions` 和 `dsh-skill-filesystem` 共同负责该解析,因此共享根目录只有一个解析器,其波浪号展开与空白环境变量处理都与 harness home 一致。 + +这两个文件是同一个候选组中的候选:按去除首尾空白后的内容去重,完全相同的副本只从 harness home 渲染一次,候选不可观测时保留该组最后一次成功状态。预算保留内容的重复候选仍参与对账,因此保留的候选被删除或改变时它会被提升为可见;字节预算省略会让该候选及其内容重复项不再被探测。harness home 文件保留更早会话记录过的 `user-global` scope key;共享文件使用 `agents-global`。对账在恢复会话时会探测两个根目录,因此外部编辑、移除或新建文件都会表现为变更通知,而不会重新生成基线。 + +用户全局 scope 目录是对账用的内部 key,因此不会进入模型可见文本:全局增量声明 `These user-global instructions apply to all work.`,而不指名 scope 目录;首段路径与 scope 目录同名的项目路径会带前导 `.` 渲染,从而保留自己的 scope。 + +根目录顺序由 `USER_GLOBAL_DIRECTORIES` 固定,发现、基线对账与去重都按它迭代。因此由 harness home 决定内容重复项,并在基线中保持自己的位置;共享根目录中内容不同的文件排在其后。 + +## 备选方案 + +**用共享根目录替换 harness home 文件。** harness home 存放 DSH 专有配置,其 `AGENTS.md` 是该包当前记录、测试并渲染的用户全局文件。移除它会剥夺跨工具共享指令者的 DSH 专有层,也会让记录了 `user-global` 状态的会话失去指涉对象。 + +**仅当 harness home 文件不存在时才加载共享根目录。** 这样两个文件会轮流充当会话可见的用户全局文件,使 DSH 专有文件静默压制共享文件,而不是与之组合。 + +**把共享文件作为 harness home 中的第二个候选名加载。** `instructionFileCandidates` 命名的是同一个目录内的文件;这两个根目录是不同目录,因此该选项无法表达这条指令链,除非重新定义候选语义。 + +**重复项改用路径标识而非内容判断。** 常见布局是把 `$DSH_HOME/AGENTS.md` symlink 到 `~/.agents/AGENTS.md`,这是同一个文件的两个路径。路径标识无法丢弃其中任何一个,同一段文本会以两个显示标签渲染两次。 + +## 影响 + +- 共享指令无需复制或 symlink 即可加载;symlink 布局仍然只从 harness home 渲染一次。 +- 在此变更之前启动的会话,若共享文件存在且内容与 harness home 文件不同,会收到一次变更通知。 +- `dsh-agent-instructions` 不暴露 `dshHome` 或 `agentsHome` 配置字段;它的两个根目录属于进程策略。`dsh-skill-filesystem` 保留显式 home 字段,用于选择该提供方自身的 skill 根目录。 +- 探测失败时两个用户全局文件共享同一命运:整个组一起保留最后一次成功状态,因为决定哪个成员渲染的是同一个 digest。 +- `packages/context/agent-instructions/tests/agent-instructions.spec.ts` 固定了两个根目录的顺序、内容去重、保留候选改变或消失后的重复项提升、全局增量措辞、`replace` 与 `remove` 变更、`$DSH_AGENTS_HOME` 解析、`~/.agents` 显示标签,以及转义后的项目目录名。 diff --git a/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.i18n.yaml new file mode 100644 index 0000000000..bab272d8e1 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.i18n.yaml @@ -0,0 +1,22 @@ +# Bilingual-pair consistency record for 2026-10-06-web-desktop-plugin-identity-compatibility.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.md +/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation: + en: 45d8ead4ca3db272 + zh: 4ef8e729d5d7e8c1 +/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/problem: + en: 8112c1c059d1cf10 + zh: 852d23c56a93a555 +/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/decision: + en: ac725c281d7dfb41 + zh: 5adcaa6ca6eab384 +/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/deferred-naming-cleanup: + en: 4c428f65ef611d07 + zh: 1fdc5467b0203d08 +/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/alternatives-considered: + en: 1bf03723fea6c9a4 + zh: 2d0d2fc997f7c022 +/agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/consequences: + en: 60f7c823b5d2ed6f + zh: 75f375487cab3c56 diff --git a/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.md b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.md new file mode 100644 index 0000000000..def042e26a --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.md @@ -0,0 +1,40 @@ +# Agent Note: Preserve Web and Desktop plugin identities across experimental consolidation + +Status: implemented + +English | [中文](2026-10-06-web-desktop-plugin-identity-compatibility.zh.md) + +## Problem + +Moving a capability into the experimental directory changes its ownership and support classification. Renaming its published package or an existing configuration row additionally changes how installed profiles refer to that capability. Those identity changes can make a working Web or Desktop customization require edits unrelated to the capability's behavior. + +## Decision + +The moved packages retain the published names declared by the [experimental naming policy](../../../../scripts/experimental-package-policy.ts). That typed directory-to-name map is the sole maintained inventory of naming exceptions. Package manifests, consumers, installation checks, and bundler checks agree with it; a missing, moved, or duplicated exception is rejected. [Experimental status](../../../../packages/experimental/README.md#status) remains independent of spelling, Official discovery, and explicit selection. + +Existing badge, Ralph, and native delegation row IDs remain valid selectors inside their applicable Host or preset scope. New isolation groups and newly introduced capabilities retain their new IDs. Later user layers still replace configuration or disable rows, and a complete preset replacement remains authoritative. Selecting an optional bundle is still explicit: a patch targeting an absent row does not select its bundle. + +Compatibility protection covers Web and Desktop profiles, including existing native provider package identities, selections, configuration, and authentication. Headless, SDK, and ACP compositions may require changes. Native bundles use the ordinary shared patch operation and require the full preset targets; separate provider-only compatibility wrappers are unnecessary for the protected profiles. Public APIs remain pre-stable, and new output metadata stays required unless an actual consumer needs an accommodation. + +## Deferred naming cleanup + +| Retained identity | Unconstrained cleanup | Why it is deferred | +|---|---|---| +| Published names in the naming-exception map | Give every experimental package the `@deepseek-ai/dsh-experimental-` prefix | Existing dependencies, imports, module declarations, and profile files use the established names | +| Existing badge, Ralph, and native-tool row IDs | Give optional contributions consistently distinct `optional-*` selectors | Existing user overrides use those IDs; the ownership move does not require a new selector | + +These naming changes are deferred, not prerequisites for experimental classification. A later cleanup requires an explicit release and migration decision, an inventory of affected Web/Desktop configurations, and installed-profile upgrade evidence. Directory moves or pre-stable API status alone do not retire these exceptions. The package list stays in the typed policy rather than being copied into this note or package READMEs. + +## Alternatives considered + +**Rename every package and row during consolidation.** Uniform spelling makes the new ownership visible, but adds dependency and configuration migration to a change that can preserve those references. + +**Publish aliases or keep duplicate compatibility rows.** Parallel identities need continuing resolution and removal rules and can register the same capability twice. Retaining the established identity avoids that additional mechanism. + +**Keep provider-only bundles through additional wrapper packages.** Web and Desktop already provide the required presets. Extra delivery packages would preserve compositions outside the selected compatibility scope and increase catalog and release maintenance. + +## Consequences + +The repository carries a bounded naming-exception map and tests that classify retained names through direct imports, aliases, transitive dependencies, and packed or bundled artifacts. An unprefixed name does not grant admission to the default product. Existing row identities have Web/Desktop composition coverage for user overrides, disabled state, and preset replacement. No general alias or automatic profile migration mechanism is introduced. + +The [classification decision](../process/2026-10-05-product-package-classification.md), [optional-composition decision](2026-09-21-experimental-capabilities-as-optional-bundles.md), and [on-demand discovery decision](2026-10-05-official-on-demand-bundles.md) retain their separate responsibilities. Released Session generations and persisted hook event names remain protected independently of this profile compatibility scope. diff --git a/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.zh.md b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.zh.md new file mode 100644 index 0000000000..1f513060ea --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.zh.md @@ -0,0 +1,40 @@ +# Agent Note: 在实验能力整合中保留 Web 和 Desktop 插件身份 + +Status: implemented + +[English](2026-10-06-web-desktop-plugin-identity-compatibility.md) | 中文 + +## 问题 + +把能力迁入 experimental 目录会改变其归属和支持分类。重命名已发布的包或既有配置行,还会改变已安装 profile 引用该能力的方式。这些身份变化可能要求正常工作的 Web 或 Desktop 自定义配置进行与能力行为无关的修改。 + +## 决策 + +迁移的包保留[实验包命名策略](../../../../scripts/experimental-package-policy.ts)声明的已发布名称。这个带类型的目录到包名映射是命名例外唯一手工维护的清单。包清单、消费者、安装检查和打包检查与其保持一致;缺失、迁移到其他位置或重复归属的例外会被拒绝。[实验状态](../../../../packages/experimental/README.zh.md#status)仍独立于名称拼写、Official 发现和显式选择。 + +既有徽章、Ralph 和原生委派行 ID 在对应的 Host 或预设作用域中保持有效。新增隔离组和新引入的能力保留各自的新 ID。后续用户层仍可替换配置或禁用行,完整预设替换仍具有决定权。可选 bundle 仍须显式选择:指向缺失行的 patch 不会自动选择其 bundle。 + +兼容性保护覆盖 Web 和 Desktop profile,包括既有原生提供方的包身份、选择状态、配置和认证。Headless、SDK 和 ACP 组合可以要求修改。原生 bundle 使用普通的共享 patch 操作并要求完整预设目标存在;受保护的 profile 不需要单独的仅提供方兼容包装包。公共 API 仍处于稳定前阶段,新增输出元信息保持必填,除非实际消费者需要适配。 + +## 暂缓的命名清理 + +| 保留的身份 | 不受兼容性约束时的清理 | 暂缓原因 | +|---|---|---| +| 命名例外映射中的已发布名称 | 为每个实验包使用 `@deepseek-ai/dsh-experimental-` 前缀 | 既有依赖、导入、模块声明和 profile 文件使用原有名称 | +| 既有徽章、Ralph 和原生工具行 ID | 为可选贡献统一使用独立的 `optional-*` 选择器 | 既有用户覆盖使用这些 ID;归属迁移不要求新的选择器 | + +这些命名变更被暂缓,不是实验分类的前提。后续清理需要明确的发布与迁移决策、受影响 Web/Desktop 配置清单,以及已安装 profile 的升级验证。仅目录迁移或公共 API 处于稳定前阶段,不足以删除这些例外。包清单保留在带类型的策略中,不复制到本记录或各包 README。 + +## 考虑过的替代方案 + +**在整合时重命名所有包和配置行。** 统一拼写可以体现新归属,但会为本可保留引用的变更加上依赖和配置迁移。 + +**发布别名或保留重复兼容行。** 并行身份需要持续维护解析和移除规则,还可能重复注册同一能力。保留既有身份可以避免增加这种机制。 + +**通过额外包装包保留仅提供方 bundle。** Web 和 Desktop 已提供所需预设。额外交付包会保留选定兼容范围之外的组合,并增加目录和发布维护成本。 + +## 后果 + +仓库维护范围明确的命名例外映射,并测试通过直接导入、别名、传递依赖及打包产物识别这些保留名称。没有实验前缀的名称不因此获准进入默认产品。既有行身份具有覆盖用户配置、禁用状态和预设替换的 Web/Desktop 组合测试。不引入通用别名或自动 profile 迁移机制。 + +[分类决策](../process/2026-10-05-product-package-classification.zh.md)、[可选组合决策](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)和[按需发现决策](2026-10-05-official-on-demand-bundles.zh.md)继续承担各自独立的职责。已发布 Session 代际和持久化 hook 事件名的保护独立于此 profile 兼容范围。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-06-packed-install-private-npm-cache.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-06-packed-install-private-npm-cache.i18n.yaml new file mode 100644 index 0000000000..0b35f92fb4 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-06-packed-install-private-npm-cache.i18n.yaml @@ -0,0 +1,22 @@ +# Bilingual-pair consistency record for 2026-09-06-packed-install-private-npm-cache.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-06-packed-install-private-npm-cache.md +/agent-note-packed-install-verification-owns-its-npm-cache: + en: 54d54ee850476cb9 + zh: 8d666e40eb2372f0 +/agent-note-packed-install-verification-owns-its-npm-cache/problem: + en: 5cd500387242a7cc + zh: 75de2c0049631634 +/agent-note-packed-install-verification-owns-its-npm-cache/decision: + en: 8a9ead906e1148d6 + zh: 6a5b911fc5849be2 +/agent-note-packed-install-verification-owns-its-npm-cache/alternatives-considered: + en: b2637475da22cec0 + zh: 5c0e3979caf44461 +/agent-note-packed-install-verification-owns-its-npm-cache/consequences: + en: a123744738fac02f + zh: 54bda8e161cb6e85 +/agent-note-packed-install-verification-owns-its-npm-cache/verification: + en: e37107020148c23e + zh: 94227a591fad9f17 diff --git a/.agents/notes/implemented/bug-fix/2026-09-06-packed-install-private-npm-cache.md b/.agents/notes/implemented/bug-fix/2026-09-06-packed-install-private-npm-cache.md new file mode 100644 index 0000000000..ce25230c8f --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-06-packed-install-private-npm-cache.md @@ -0,0 +1,29 @@ +# Agent Note: Packed-install verification owns its npm cache + +Status: implemented + +English | [中文](2026-09-06-packed-install-private-npm-cache.zh.md) + +## Problem + +The [packed-install verifier](../../../../scripts/release/verify-packed-install.ts) removes its temporary consumer but an npm install using the ambient cache leaves tarball content and logs outside that cleanup. Repeated release rehearsals can therefore grow the shared runner user cache even when every consumer is removed. A full root filesystem can fail unrelated CI jobs. + +## Decision + +The verifier passes `--cache /.npm-cache` only to its npm install invocation. The explicit argument overrides npm environment and user-configuration cache settings without changing the environment of the installed executable. The existing `finally` removes this cache together with the consumer after success, an install failure, or an executable failure. + +The [release sequence decision](../process/2026-08-10-npm-release-sequences.md) still owns artifact selection and publication. This decision only adds cache ownership to the verifier; it does not change other npm consumers. + +## Alternatives considered + +- **Delete the shared user cache:** unrelated jobs also own its contents. The verifier must not delete shared host state. +- **Change global `TMPDIR` or npm configuration:** that affects processes outside this invocation and is unnecessary when the consumer already has an owned temporary directory. +- **Keep a persistent verifier cache:** that retains cross-run downloads but requires a separate retention policy. Invocation ownership accepts repeated downloads instead. + +## Consequences + +This prevents this verifier from adding content to the ambient npm cache; it neither reclaims existing host cache contents nor guarantees that disk exhaustion from other producers stops. External dependencies can require fresh downloads on each invocation. A forced termination that prevents `finally` from running can leave the consumer and its cache behind; this change adds no crash-recovery cleanup. + +## Verification + +[The focused regression](../../../../scripts/release/verify-packed-install.spec.ts) runs the real verifier and npm offline against a tiny local tarball. A lifecycle script records actual private `_cacache` files before cleanup. The tests verify consumer removal after success, npm install failure, and installed-entry failure, while inherited environment and user-config cache canaries remain unchanged. Removing the cache argument makes the private-cache assertion fail. The executable test follows the POSIX release runner path; it does not exercise Windows command-shim resolution. diff --git a/.agents/notes/implemented/bug-fix/2026-09-06-packed-install-private-npm-cache.zh.md b/.agents/notes/implemented/bug-fix/2026-09-06-packed-install-private-npm-cache.zh.md new file mode 100644 index 0000000000..7e495439e4 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-06-packed-install-private-npm-cache.zh.md @@ -0,0 +1,29 @@ +# Agent Note: 打包安装验证拥有独立 npm 缓存 + +Status: implemented + +[English](2026-09-06-packed-install-private-npm-cache.md) | 中文 + +## 问题 + +[打包安装验证器](../../../../scripts/release/verify-packed-install.ts) 会删除临时消费目录,但使用环境配置缓存的 npm install 会把 tarball 内容和日志留在清理范围之外。因此,即使每个消费目录均被删除,重复的发布演练仍可能扩大共享 runner 用户缓存。根文件系统满载会使无关 CI 作业失败。 + +## 决策 + +验证器仅向自己的 npm install 调用传入 `--cache /.npm-cache`。显式参数覆盖 npm 环境变量和用户配置中的缓存设置,不改变已安装可执行文件的环境。现有 `finally` 在成功、安装失败或可执行文件失败后将此缓存与消费目录一起删除。 + +[发布序列决策](../process/2026-08-10-npm-release-sequences.zh.md) 仍负责产物选择与发布。本决策仅为验证器增加缓存所有权,不改变其他 npm 消费方。 + +## 曾考虑的替代方案 + +- **删除共享用户缓存:**其他作业也拥有其中的内容。验证器不得删除共享宿主状态。 +- **修改全局 `TMPDIR` 或 npm 配置:**这会影响本次调用之外的进程,而消费目录已有独立临时目录,因此没有必要。 +- **保留持久化验证缓存:**这能跨运行复用下载,但需要独立保留策略。调用级所有权选择接受重复下载。 + +## 后果 + +这会阻止该验证器向环境配置的 npm 缓存添加内容;既不会回收现有宿主缓存,也不保证其他来源导致的磁盘耗尽停止。外部依赖可能需要在每次调用时重新下载。若强制终止使 `finally` 无法运行,消费目录及其缓存仍可能残留;本改动不增加崩溃恢复清理。 + +## 验证 + +[定向回归测试](../../../../scripts/release/verify-packed-install.spec.ts) 使用微型本地 tarball 离线运行真实验证器和 npm。生命周期脚本在清理前记录私有 `_cacache` 中的实际文件。测试验证成功、npm 安装失败和已安装入口失败后消费目录均被删除,同时继承环境和用户配置指定缓存中的探针文件保持不变。移除缓存参数会使私有缓存断言失败。可执行测试沿用 POSIX 发布 runner 路径,不覆盖 Windows 命令垫片解析。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-27-pwsh-prompt-tail-grace.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-27-pwsh-prompt-tail-grace.i18n.yaml index d0742687e2..b4f5f12a7b 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-27-pwsh-prompt-tail-grace.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-09-27-pwsh-prompt-tail-grace.i18n.yaml @@ -6,8 +6,8 @@ en: 5e8c5673a0669d9c zh: c8442a6186288a2a /agent-note-a-seen-prompt-marker-keeps-its-own-tail-grace/problem: - en: 906f8707987ff36c - zh: 40430ec1e03ee340 + en: 2f5e9297c5fd4853 + zh: 8168c90bc6fa13b5 /agent-note-a-seen-prompt-marker-keeps-its-own-tail-grace/decision: en: a998f5c6abdd68eb zh: a7cdd13e1e4de6c6 diff --git a/.agents/notes/implemented/bug-fix/2026-09-27-pwsh-prompt-tail-grace.md b/.agents/notes/implemented/bug-fix/2026-09-27-pwsh-prompt-tail-grace.md index 375ba521d5..68d22a753a 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-27-pwsh-prompt-tail-grace.md +++ b/.agents/notes/implemented/bug-fix/2026-09-27-pwsh-prompt-tail-grace.md @@ -6,7 +6,7 @@ English | [中文](2026-09-27-pwsh-prompt-tail-grace.zh.md) ## Problem -Windows sessions on the self-hosted lane settled one or two of seven sends on the controlled prompt and charged the silence tier for the rest (2026-09-25/26, issue #2487). The exact readiness evidence is the controlled prompt's printable tail after the OSC `133;D` marker, and the bound on waiting for it was `idleSilenceMs + handoffGraceMs`. A native Windows probe shows the render arrives as two pty chunks: the marker alone, then the five-byte `dsh> ` 2-28 ms later (56 renders under Windows PowerShell 5.1, the same two-chunk render under pwsh 7.6.6), with nothing printable after any prompt and no terminal queries in the output. The marker is written by the shell's own prompt function and the tail by the same render, so a tail that arrives past the bound means the host stalled that delivery, not that the prompt is absent. The session settled `inferred_idle` anyway, and each such send cost the tool call roughly three seconds. The lane's own failures are not that state: the first master run carrying this field (run 36326153388, 2026-09-27 14:30Z) took the same 20.8 s as the run before it (36309006133), so its degraded sends settled at the plain bound with the tolerance inert; the [Windows-lane observation note](../testing/2026-09-27-observation-waits-on-observed-state.md) records the open investigation. +Windows sessions on the self-hosted lane settled one or two of seven sends on the controlled prompt and charged the silence tier for the rest (2026-09-25/26, issue #2487). The exact readiness evidence is the controlled prompt's printable tail after the OSC `133;D` marker, and the bound on waiting for it was `idleSilenceMs + handoffGraceMs`. A native Windows probe shows the render arrives as two pty chunks: the marker alone, then the five-byte `dsh> ` 2-28 ms later (56 renders under Windows PowerShell 5.1, the same two-chunk render under pwsh 7.6.6), with nothing printable after any prompt and no terminal queries in the output. The marker is written by the shell's own prompt function and the tail by the same render, so a tail that arrives past the bound means the host stalled that delivery, not that the prompt is absent. The session settled `inferred_idle` anyway, and each such send cost the tool call roughly three seconds. The lane's own failures are not that state: the first master run carrying this field (run 36326153388, 2026-09-27 14:30Z) took the same 20.8 s as the run before it (36309006133), so its degraded sends settled at the plain bound with the tolerance inert; the [Windows-lane observation note](../testing/2026-09-27-observation-waits-on-observed-state.md) records that investigation, and the [bundled console host note](2026-09-28-pty-bundled-console-host.md) measures that pool's tail arriving already malformed (`\ndsh>\x1b[1C`), which no bound on a well-formed tail can cover. This field still owns genuinely delayed tails. ## Decision diff --git a/.agents/notes/implemented/bug-fix/2026-09-27-pwsh-prompt-tail-grace.zh.md b/.agents/notes/implemented/bug-fix/2026-09-27-pwsh-prompt-tail-grace.zh.md index 511cedf728..5efebc7137 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-27-pwsh-prompt-tail-grace.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-09-27-pwsh-prompt-tail-grace.zh.md @@ -6,7 +6,7 @@ Status: implemented ## Problem -自托管 lane 上的 Windows 会话在七次 send 中只有一到两次按受控提示符结算,其余都要支付静默层(2026-09-25/26,issue #2487)。精确的就绪证据是 OSC `133;D` 标记之后受控提示符的可打印尾部,而等待它的上界是 `idleSilenceMs + handoffGraceMs`。原生 Windows 探测表明该渲染以两个 pty 分块到达:先是标记本身,2–28 毫秒后是 5 字节的 `dsh> `(Windows PowerShell 5.1 下 56 次渲染,pwsh 7.6.6 下同样是两分块渲染),任何提示符之后都没有可打印文本,输出中也没有终端查询。标记由 shell 自己的 prompt 函数写出、尾部由同一次渲染写出,因此尾部晚于上界到达意味着宿主机拖慢了这次投递,而不是提示符缺失。会话此前仍会按 `inferred_idle` 结算,每次这样的 send 让工具调用多付约三秒。该 lane 自身的失败不是这种状态:首个带着该字段的 master run(run 36326153388,2026-09-27 14:30Z)与其前一个 run(36309006133)同样耗时 20.8 秒,因此其退化的 send 是在宽限不起作用的情况下按普通上界结算的;[Windows lane 观测笔记](../testing/2026-09-27-observation-waits-on-observed-state.zh.md)记录了这项仍未关闭的调查。 +自托管 lane 上的 Windows 会话在七次 send 中只有一到两次按受控提示符结算,其余都要支付静默层(2026-09-25/26,issue #2487)。精确的就绪证据是 OSC `133;D` 标记之后受控提示符的可打印尾部,而等待它的上界是 `idleSilenceMs + handoffGraceMs`。原生 Windows 探测表明该渲染以两个 pty 分块到达:先是标记本身,2–28 毫秒后是 5 字节的 `dsh> `(Windows PowerShell 5.1 下 56 次渲染,pwsh 7.6.6 下同样是两分块渲染),任何提示符之后都没有可打印文本,输出中也没有终端查询。标记由 shell 自己的 prompt 函数写出、尾部由同一次渲染写出,因此尾部晚于上界到达意味着宿主机拖慢了这次投递,而不是提示符缺失。会话此前仍会按 `inferred_idle` 结算,每次这样的 send 让工具调用多付约三秒。该 lane 自身的失败不是这种状态:首个带着该字段的 master run(run 36326153388,2026-09-27 14:30Z)与其前一个 run(36309006133)同样耗时 20.8 秒,因此其退化的 send 是在宽限不起作用的情况下按普通上界结算的;[Windows lane 观测笔记](../testing/2026-09-27-observation-waits-on-observed-state.zh.md)记录了该调查,而[自带控制台宿主说明](2026-09-28-pty-bundled-console-host.zh.md)实测该池的尾部以已经畸形的形态到达(`\ndsh>\x1b[1C`),任何针对良构尾部的上界都覆盖不了。本字段仍然负责真正迟到的尾部。 ## Decision diff --git a/.agents/notes/implemented/bug-fix/2026-09-28-pty-bundled-console-host.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-28-pty-bundled-console-host.i18n.yaml new file mode 100644 index 0000000000..817b227486 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-28-pty-bundled-console-host.i18n.yaml @@ -0,0 +1,22 @@ +# Bilingual-pair consistency record for 2026-09-28-pty-bundled-console-host.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-28-pty-bundled-console-host.md +/agent-note-windows-persistent-ptys-use-the-console-host-node-pty-ships: + en: 5d0101a130154663 + zh: 9fe1948abd4b6576 +/agent-note-windows-persistent-ptys-use-the-console-host-node-pty-ships/problem: + en: d5e9c94b6a210a8e + zh: e2e15aac77dd34ee +/agent-note-windows-persistent-ptys-use-the-console-host-node-pty-ships/decision: + en: 1b4f56354db760c5 + zh: f467d13f84872e66 +/agent-note-windows-persistent-ptys-use-the-console-host-node-pty-ships/alternatives-considered: + en: c65cbf2fa36636a3 + zh: fdf846dd4c8a39cb +/agent-note-windows-persistent-ptys-use-the-console-host-node-pty-ships/consequences: + en: 5af486852c78125e + zh: 68215646a34b4412 +/agent-note-windows-persistent-ptys-use-the-console-host-node-pty-ships/testing: + en: eebdb2d7c7e61d9e + zh: da8dd1df06180dee diff --git a/.agents/notes/implemented/bug-fix/2026-09-28-pty-bundled-console-host.md b/.agents/notes/implemented/bug-fix/2026-09-28-pty-bundled-console-host.md new file mode 100644 index 0000000000..6caee6d9a3 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-28-pty-bundled-console-host.md @@ -0,0 +1,39 @@ +# Agent Note: Windows persistent PTYs use the console host node-pty ships + +Status: implemented + +English | [中文](2026-09-28-pty-bundled-console-host.zh.md) + +## Problem + +The master `serial / windows (self-hosted standby)` drill has been red since 2026-09-15: its `test:coverage` gate fails on `packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts`, either with `settleReasons` reporting one `stdin_read` among six `inferred_idle` settlements or with the case exhausting its 120 s budget. The same case passes in 5.3 s on the Blacksmith Windows image. + +Windows has no exact stdin-wait tier: `WindowsProcessInspector.isStdinWaiting()` always returns false and `foregroundPgid()` returns the shell pid, so `inspectForeground()` can only report `inputWaiting: false`. The persistent-pwsh readiness path therefore settles `stdin_read` only after it has seen both the OSC `133;D;` marker and the printable `dsh> ` tail written by the same prompt render (`terminal-bash/src/session.ts`). + +Probing the pool machine directly (node-pty 1.2.0-beta.15, `pwsh -NoLogo -NoProfile`, 160x40, `TERM=dumb`) shows why that tail never matches there: once a command's output scrolls the viewport, the console host Windows itself provides replays the prompt line as `\ndsh>\x1b[1C` — a leading newline, the prompt text without its trailing space, and a cursor-forward escape in place of that space. `TerminalSanitizer` accumulates the printable text after the marker, so the tail becomes `\ndsh>`, which neither equals `dsh> ` nor prefixes it, and every later send waits out the silence tier. Five of seven prompts took that form on the pool machine; the same command sequence on Windows 11 25H2 (conhost 10.0.26100.1) renders `dsh> ` verbatim. The [seen-marker tail grace](2026-09-27-pwsh-prompt-tail-grace.md) recorded the same red signal as a late tail and shipped `promptTailGraceMs`; this measurement replaces that attribution for these hosts, because a tail that has already arrived in a non-prefixing form never satisfies the condition the grace extends. + +## Decision + +`subprocess-local` allocates Windows terminals with node-pty's `useConptyDll: true`, so the PTY is backed by the OpenConsole the node-pty package ships instead of the console host the operating system provides. The option is set only when the runtime platform is `win32`; POSIX allocation is unchanged. + +The same command sequence on the pool machine through the bundled console host rendered `dsh> ` for all seven prompts. + +## Alternatives considered + +**Rely on `promptTailGraceMs`, the grace shipped for late tails.** Rejected: the extended bound applies only while the arrived tail still prefixes `dsh> ` (`tailPending` in `terminal-bash/src/session.ts`). A pool prompt renders `\ndsh>`, which prefixes nothing in that comparison, so the grace is never reached and the sends keep settling on the plain silence bound. + +**Teach `TerminalSanitizer` to tolerate the operating system console host's rendering.** Rejected: normalizing the leading newline and `CSI nC` into the tail keeps the readiness path working, but it writes one console host's quirk into the sanitizer and leaves any other repaint form to reintroduce the same divergence. The console host is a replaceable component, and the package already ships a current one. + +**Keep the operating system console host and accept the red drill.** Rejected: the drill is the only continuous evidence that the in-house Windows pool can take over the required lanes, and it has been red for two weeks. + +**Upgrade the pool machines' Windows.** Rejected as this repository's fix: it requires rebuilding production CI hosts (Server 2022, Windows 10 22H2), and the rendering difference returns on any host that is not current. + +## Consequences + +Windows PTY allocation now depends on node-pty's bundled OpenConsole binaries. The desktop runtime file policy already retains `node-pty/prebuilds/win32-x64/conpty/conpty.dll` and `OpenConsole.exe`, and the package resolves them relative to its own addon, so the packaged desktop runtime keeps working; that policy is now load-bearing for terminal allocation rather than only for keeping the source distribution intact. + +Console signals keep their documented behavior: SIGINT is still delivered as a `\x03` input write that the console host turns into a console-wide CTRL_C event, and the Windows inspector is unchanged. The allocated process id still names the shell: on the pool machine `pty.pid` resolves to `pwsh.exe` and equals the shell's own `$PID` under both console hosts. node-pty marks `useConptyDll` experimental, so a node-pty upgrade can change it; a unit test pins the option as present for `win32` and absent elsewhere. Both teardown paths still publish node-pty's exit event on the pool machine under the bundled host (about 1.1 s for a `taskkill` tree kill and for a bare `kill()`), and an unresolvable `conpty/conpty.dll` fails loud: node-pty throws instead of falling back to the operating system host. + +## Testing + +`packages/subprocess/subprocess-local/tests/local.spec.ts` pins the option per platform: present for `win32`, absent for a POSIX platform. The end-to-end signal for the pool is the next master push's `serial / windows (self-hosted standby)` run, whose sends settle on the prompt path instead of the silence bound. No e2e, snapshot, or sandbox case applies: this change selects a PTY backend and produces no model- or product-user-visible output of its own. The packaged runtime keeps the assets this option resolves, pinned by the retained-file assertions in `apps/desktop/tests/runtime-file-policy.spec.ts`, and the Electron payload smoke passes the option when it allocates a PTY from that tree on Windows, so the packaged path and the separately pinned retained-file list are both exercised. diff --git a/.agents/notes/implemented/bug-fix/2026-09-28-pty-bundled-console-host.zh.md b/.agents/notes/implemented/bug-fix/2026-09-28-pty-bundled-console-host.zh.md new file mode 100644 index 0000000000..c8d2907b6f --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-28-pty-bundled-console-host.zh.md @@ -0,0 +1,39 @@ +# Agent Note: Windows 持久 PTY 使用 node-pty 自带的控制台宿主 + +Status: implemented + +[English](2026-09-28-pty-bundled-console-host.md) | 中文 + +## 问题 + +master 的 `serial / windows (self-hosted standby)` 演练自 2026-09-15 起一直红:`test:coverage` gate 在 `packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts` 上失败,形态要么是 `settleReasons` 报出六次 `inferred_idle` 结算里只有一次 `stdin_read`,要么是用例耗尽 120 s 预算。同一个用例在 Blacksmith 的 Windows 镜像上 5.3 s 通过。 + +Windows 没有精确的 stdin-wait 档:`WindowsProcessInspector.isStdinWaiting()` 恒为 false、`foregroundPgid()` 返回 shell pid,因此 `inspectForeground()` 只能报 `inputWaiting: false`。持久 pwsh 的就绪路径只有在同时看到 OSC `133;D;` 标记与同一次 prompt 渲染写出的可打印 `dsh> ` 尾部之后,才结算 `stdin_read`(`terminal-bash/src/session.ts`)。 + +直接在池机器上探测(node-pty 1.2.0-beta.15、`pwsh -NoLogo -NoProfile`、160x40、`TERM=dumb`)说明了那个尾部为何在池上匹配不上:命令输出把视口滚动一次之后,Windows 自带的控制台宿主把提示符行重绘成 `\ndsh>\x1b[1C`——一个前导换行、去掉尾随空格的提示符文本,以及代替那个空格的 cursor-forward 转义。`TerminalSanitizer` 累积标记之后的非转义文本,于是尾部成了 `\ndsh>`,既不等于 `dsh> ` 也不构成它的前缀,此后每次 send 都要等满静默档。池机器上七个提示符里有五个是这个形态;同一串命令在 Windows 11 25H2(conhost 10.0.26100.1)上原样渲染出 `dsh> `。[已见标记的尾部宽限](2026-09-27-pwsh-prompt-tail-grace.zh.md)把同一个红信号记为尾部迟到,并为此交付了 `promptTailGraceMs`;本次测量取代了该归因在这类宿主上的适用范围——已经以非前缀形态到达的尾部永远不满足宽限所延长的那个条件。 + +## 决策 + +`subprocess-local` 分配 Windows 终端时传 node-pty 的 `useConptyDll: true`,PTY 因此由 node-pty 包自带的 OpenConsole 承载,而不是操作系统提供的控制台宿主。该选项仅在运行时平台为 `win32` 时设置;POSIX 分配不变。 + +同一串命令在池机器上经由自带控制台宿主时,七个提示符全部渲染出 `dsh> `。 + +## 备选方案 + +**依赖为迟到尾部交付的 `promptTailGraceMs`。** 否决:延长的上界只在已到达的尾部仍构成 `dsh> ` 前缀时生效(`terminal-bash/src/session.ts` 的 `tailPending`)。池上的提示符渲染成 `\ndsh>`,在这个比较里不构成任何前缀,因此宽限永远到不了,send 继续按普通静默上界结算。 + +**让 `TerminalSanitizer` 容忍操作系统控制台宿主的渲染形态。** 否决:把前导换行与 `CSI nC` 归一进尾部能让就绪路径继续工作,但它把一个控制台宿主的怪癖写进 sanitizer,之后任何别的重绘形态都会重新引入同一类偏离。控制台宿主是可替换组件,而包里已经带了当前版本。 + +**保留操作系统控制台宿主并接受演练长期红。** 否决:这条演练是「自有 Windows 池能接管必需 lane」的唯一持续证据,它已经红了两周。 + +**升级池机器的 Windows。** 作为本仓库的修法否决:它需要重建生产 CI 主机(Server 2022、Windows 10 22H2),而且任何非最新宿主上这个渲染差异都会回来。 + +## 后果 + +Windows PTY 分配现在依赖 node-pty 自带的 OpenConsole 二进制。桌面端运行时文件策略已经保留 `node-pty/prebuilds/win32-x64/conpty/conpty.dll` 与 `OpenConsole.exe`,且包按自身 addon 位置解析它们,因此打包后的桌面运行时仍然可用;该策略现在对终端分配是承重的,不只是为了保留源码分发内容。 + +控制台信号保持文档化的行为:SIGINT 仍以 `\x03` 输入写入投递、由控制台宿主转成控制台级 CTRL_C 事件,Windows inspector 未变。分配到的进程 id 仍然指向 shell:在池机器上 `pty.pid` 解析为 `pwsh.exe`,且与 shell 自己的 `$PID` 在两种控制台宿主下都相等。node-pty 把 `useConptyDll` 标为实验性,因此 node-pty 升级可能改变它;一条单测钉住该选项:`win32` 上存在、其它平台不存在。两种拆卸路径在池机器上、自带控制台宿主下都会发出 node-pty 的退出事件(`taskkill` 树杀与裸 `kill()` 各约 1.1 s);解析不到 `conpty/conpty.dll` 时是硬失败——node-pty 直接抛错,不会回退到操作系统控制台宿主。 + +## 测试 + +`packages/subprocess/subprocess-local/tests/local.spec.ts` 按平台钉住该选项:`win32` 上存在、POSIX 平台上不存在。池侧的端到端信号是下一次 master push 的 `serial / windows (self-hosted standby)` run——它的 send 将按提示符路径结算,而不是等满静默上界。e2e、snapshot、sandbox 三类不适用:本改动只选择 PTY 后端,本身不产生模型可见或产品用户可见的输出。打包运行时保留了该选项要解析的资产,由 `apps/desktop/tests/runtime-file-policy.spec.ts` 的保留清单断言钉住;Electron payload smoke 在 Windows 上从该树分配 PTY 时也传该选项,因此打包路径与单独钉住的保留清单都会被覆盖。 diff --git a/.agents/notes/implemented/feature/2026-06-15-ptc.i18n.yaml b/.agents/notes/implemented/feature/2026-06-15-ptc.i18n.yaml index 83afbded6e..38cc4a8bf5 100644 --- a/.agents/notes/implemented/feature/2026-06-15-ptc.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-06-15-ptc.i18n.yaml @@ -9,17 +9,17 @@ en: 4b312be894531bee zh: d655648ea5843ef5 /agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/decision: - en: 371abc68ce278a5c - zh: 3fa8d28213794932 + en: a1732e2b1b46cb39 + zh: ddfc2bdcffa099a5 /agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/decision/per-program-execution-controls: en: da694641e1d009de zh: 9d7492d39039af66 /agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/decision/the-registry-owns-the-mode: - en: 55287fedeaaee6d8 - zh: d2afe242e84acc43 + en: 091b7bc49b027353 + zh: 42dc77a810202694 /agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/decision/the-run-code-tool-and-the-dispatch-bridge: - en: 20536a38b74f80c6 - zh: fdf604958dc041f3 + en: 6a0e2698b082978e + zh: fbb03503520b30e9 /agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/decision/observability-tool-ptc-dispatch: en: b8e4cf9433c7ee18 zh: e5f4e8d7d03d3a7b @@ -33,8 +33,8 @@ en: d62b41a6a5c58e5f zh: ef72c5a9b32b370e /agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/decision/what-the-model-sees: - en: 43653fd91ab9165b - zh: 6ddfa853e69b2d80 + en: 81c12f19f7a967c1 + zh: 29734ca964acdec2 /agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/consequences: en: 761eb6c1aeb7af18 zh: 5434456a3d0a1b65 @@ -42,8 +42,8 @@ en: f5d287f610962a48 zh: 88368e1df517b087 /agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/alternatives-considered: - en: 90110f91f6f6130f - zh: 25d28433840f91a4 + en: e78530994a88d14f + zh: 49f64c7f1421f507 /agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/risks: - en: 2d00dc8c05a7a436 - zh: 09db2ffb60ef82c3 + en: d58925bdb912de23 + zh: d3a70237d656e5a4 diff --git a/.agents/notes/implemented/feature/2026-06-15-ptc.md b/.agents/notes/implemented/feature/2026-06-15-ptc.md index 36f9b16ce6..c49b21ecee 100644 --- a/.agents/notes/implemented/feature/2026-06-15-ptc.md +++ b/.agents/notes/implemented/feature/2026-06-15-ptc.md @@ -18,7 +18,7 @@ Tool presentation belongs to the registry that owns tool visibility: implementin Three decisions, each elaborated in its own section below: -1. **PTC mode is a first-class presentation mode of `ToolRuntime`** (`dsh-tools`), selected by a validated `mode` config: `'native'` (the default, contributing the visible capability schemas), `'ptc'` (the registry contributes only its reserved `run_code` transport plus a generated SDK `.d.ts` in the system prompt), or `'both'` (native schemas and the transport + SDK). The registry constructs its canonical contribution at the source; the cooperative prompt-assembly result remains authoritative, and the logged request header records exactly that returned presentation. +1. **PTC mode is a first-class presentation mode of `ToolRuntime`** (`dsh-tools`), selected by a validated `mode` config: `'native'` (the default, contributing the visible capability schemas) or `'ptc'` (the registry contributes only its reserved `run_code` transport plus a generated SDK `.d.ts` in the system prompt). The registry constructs its canonical contribution at the source; the cooperative prompt-assembly result remains authoritative, and the logged request header records exactly that returned presentation. 2. **Code execution is a capability seam** — `packages/ptc-runtime/` contains the Service Definition package `@deepseek-ai/dsh-ptc-runtime`, which owns `ctx.ptcRuntime` ([capability seams](../architecture/2026-06-13-capability-seams.md); Consumer = `dsh-tools`, with core-consumes-a-seam precedent in `agent-loop` → `dsh-llm`). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports `{ value, logs, error? }`. Language and substrate are backend properties, so a future Python or container backend is another Service Provider package, not a redesign. 3. **The shipped implementation is `@deepseek-ai/dsh-ptc-runtime-node`**: each program runs in a fresh managed Node process under the calling Session's resolved sandbox policy. Direct Node APIs remain available within that policy; named host bindings cross a separately validated control channel. @@ -32,13 +32,13 @@ The [Node execution decision](../architecture/2026-09-11-sandboxed-node-ptc-runt ### The registry owns the mode -`ToolRuntime` gains a schemastery-validated config (`static Config`), its first: `mode: 'native' | 'ptc' | 'both'`, default `'native'`. A deployment flips it from `cordis.yml` (`tools: { mode: ptc }`) — no code edit, per the no-hardcoded-tunables convention. +`ToolRuntime` gains a schemastery-validated config (`static Config`), its first: `mode: 'native' | 'ptc'`, default `'native'`. A deployment flips it from `cordis.yml` (`tools: { mode: ptc }`) — no code edit, per the no-hardcoded-tunables convention. -**Wire tool list.** The registry contributes visible capabilities in `'native'`, only `run_code` in `'ptc'`, and both in `'both'`. The final `PromptAssembly.tools` list is logged in the request header. `run_code` is a reserved presentation transport outside registration and restriction layers; direct prompt providers and the assembly waterfall remain responsible for their own contributions. +**Wire tool list.** The registry contributes visible capabilities in `'native'` and only `run_code` in `'ptc'`. The [presentation simplification](../simplification/2026-10-05-native-or-ptc-presentation.md) owns the removal of mixed presentation. The final `PromptAssembly.tools` list is logged in the request header. `run_code` is a reserved presentation transport outside registration and restriction layers; direct prompt providers and the assembly waterfall remain responsible for their own contributions. **Interaction with `toolOrder`:** a configured `systemPrompt.toolOrder` naming native capabilities rejects every assembly under `mode: 'ptc'`, because those names are outside that mode's wire-validation universe. This is correct behavior, not a bug: a deployment using PTC mode updates its order config or drops it. -**SDK prompt section.** In `'ptc'` and `'both'`, the lazy `tools:sdk` section in the tool-guidance order band renders the loaded runtime's language declarations plus fixed usage instructions for the scope's visible capabilities (TypeScript by default; the [language-dispatch note](../../archived/feature/2026-07-31-ptc-language-dispatch.md) added Python and the `ctx.ptcRuntime.language` renderer table). It shares lookup and execution visibility, excludes `run_code`, and sorts tools lexicographically for byte-stable output. +**SDK prompt section.** In `'ptc'`, the lazy `tools:sdk` section in the tool-guidance order band renders the loaded runtime's language declarations plus fixed usage instructions for the scope's visible capabilities (TypeScript by default; the [language-dispatch note](../../archived/feature/2026-07-31-ptc-language-dispatch.md) added Python and the `ctx.ptcRuntime.language` renderer table). It shares lookup and execution visibility, excludes `run_code`, and sorts tools lexicographically for byte-stable output. **Assembly ownership.** `run_code` and `tools:sdk` enter the trusted `system-prompt/assemble` waterfall as normal assembly inputs. A scoped `tools:sdk` section may shadow the global default before dispatch, and a listener may remove or replace either contribution. The waterfall's returned assembly is final, so whoever changes these inputs owns preserving a viable PTC mode protocol when the deployment expects PTC mode to remain usable; no restoration pass overrides deliberate composition. @@ -46,7 +46,7 @@ The [Node execution decision](../architecture/2026-09-11-sandboxed-node-ptc-runt ### The run_code tool and the dispatch bridge -Under `'ptc'` and `'both'` the registry owns `run_code` as a reserved presentation transport with two required parameters, `{ code: string; description: string }` (the description labels the call in UIs, the bash precedent). It is represented by a normal `ToolDefinition` for dispatch but stays outside the filterable capability layers, so restrictions cannot accidentally remove PTC mode's only entry point. Calls traverse the complete tool pipeline — `tools/pre-execute` → monotonic guards → `tools/execute` around dispatch → `tools/post-execute` → optional definition-owned `finalizeContent` → immutable `tools/result` notification — exactly like native calls; a permission plugin can inspect the program text before it runs, and final-result observers see the normalized outer outcome. Its `execute(args, exec)`: +Under `'ptc'` the registry owns `run_code` as a reserved presentation transport with two required parameters, `{ code: string; description: string }` (the description labels the call in UIs, the bash precedent). It is represented by a normal `ToolDefinition` for dispatch but stays outside the filterable capability layers, so restrictions cannot accidentally remove PTC mode's only entry point. Calls traverse the complete tool pipeline — `tools/pre-execute` → monotonic guards → `tools/execute` around dispatch → `tools/post-execute` → optional definition-owned `finalizeContent` → immutable `tools/result` notification — exactly like native calls; a permission plugin can inspect the program text before it runs, and final-result observers see the normalized outer outcome. Its `execute(args, exec)`: 1. **Build bindings.** One run-scoped signal follows outer cancellation and is aborted whenever the run settles. Each visible tool binding snapshots lossless-JSON arguments, enters the native-contract dispatch pool (the [live-parallel reference](../../../../packages/core/tools/README.md) owns the scheduling design), executes with a deterministic call id and the outer token as `parent`, defers returned contexts through the outer execution, and logs the `tool/ptc-dispatch-start`/`tool/ptc-dispatch` pair, the settle side carrying the full rendered result content. Success returns the tool's final canonical JSON value; failure becomes the program-visible `ToolCallError`. Every sub-call retains its own immutable execution identity and traverses the full tool pipeline. 2. **Runs resolved inputs:** `ctx.ptcRuntime.run(ctx.ptcRuntime.resolve(request))`. The request carries the program, bindings, run-scoped signal, calling Session cwd and applicable resolved file policy. The run-scoped signal connects outer settlement to execution cancellation. @@ -86,7 +86,7 @@ The selected OS backend constrains direct Node effects under the same policy use ### What the model sees -The SDK instructs the model to write an async body in the loaded runtime's language (an erasable-TypeScript body by default; a Python `async` body under a Python runtime — see the [language-dispatch note](../../archived/feature/2026-07-31-ptc-language-dispatch.md)), call tools through `await tools.name(args)`, catch rejected tool calls when needed, and return or log only the output that should re-enter context. Both flavors state the same contract in their own primitive: independent read-only calls MAY overlap under `Promise.all` (TypeScript) or `asyncio.gather` (Python), mutating calls run alone in submission order, and dependent work sequences with `await`. The declaration prefix can be as large as native schemas, especially in `'both'`, but remains stable for provider caching. +The SDK instructs the model to write an async body in the loaded runtime's language (an erasable-TypeScript body by default; a Python `async` body under a Python runtime — see the [language-dispatch note](../../archived/feature/2026-07-31-ptc-language-dispatch.md)), call tools through `await tools.name(args)`, catch rejected tool calls when needed, and return or log only the output that should re-enter context. Both flavors state the same contract in their own primitive: independent read-only calls MAY overlap under `Promise.all` (TypeScript) or `asyncio.gather` (Python), mutating calls run alone in submission order, and dependent work sequences with `await`. The declaration prefix can be as large as native schemas, but remains stable for provider caching. The transport's own `description` and both SDK instruction flavors open by naming `code` and `description` as the call's two required arguments. Prose that describes the call as passing a program leaves the second argument discoverable only through the parameter schema, and a model that emits `{code}` alone loses the whole written program to an `INVALID_ARGS` rejection. @@ -113,7 +113,7 @@ Deployments switching to `'ptc'` must update any native-only `toolOrder`. Assemb **Always-exclusive (Cloudflare-faithful, no mode).** Rejected for this SDK's primary consumer: a coding agent's bread-and-butter single calls (`bash`, `read`, `edit`) are already ideal as native calls, and forcing every edit through a program taxes the common case. The mode config keeps the faithful form (`'ptc'`) one line away without imposing it. -**Per-tool visibility tiers (this tool native, that tool ptc-only).** Deferred: it needs per-tool metadata and a presentation split that `'native' | 'ptc' | 'both'` does not, and its design depends on evidence about how models split usage under `'both'`. +**Per-tool visibility tiers (this tool native, that tool ptc-only).** Deferred: it needs per-tool metadata and separate admission rules for native and program-only operations. Whole-agent native or PTC presentation keeps one calling convention per agent. **Sanitized identifier aliases in the SDK** (`my-tool` → `my_tool`, Cloudflare's approach). Rejected: quoted keys on a `declare const` make every name reachable with zero alias-collision logic; models handle `tools["my-tool"](…)` fine. @@ -125,7 +125,7 @@ Deployments switching to `'ptc'` must update any native-only `toolOrder`. Assemb **`stripTypeScriptTypes` is marked experimental.** It is the same engine (amaro/swc) behind Node's own native `.ts` execution, exposed as an API across this repo's whole engines range. Mitigations: the runtime's unit suite checks position preservation and the required parts of the erasable-only rejection message, the call sits behind one private function, and `amaro`/`sucrase` are direct replacements if the API shifts. The erasable-only subset is a model-facing input restriction, and the error tells the model how to correct the program. -**Prompt cost of the SDK, especially under `'both'`.** The `.d.ts` can rival the native schemas it complements; `'both'` carries two representations. Prefix stability + provider caching amortize per-session cost; the mode is per-deployment; the Agent Note makes no unconditional-savings claim. Measured guidance (when to prefer which mode) is explicitly post-ship learning. +**Prompt cost of the SDK.** The `.d.ts` can rival the native schemas it replaces. Prefix stability + provider caching amortize per-session cost; the mode is per-deployment; the Agent Note makes no unconditional-savings claim. Measured guidance (when to prefer which mode) is explicitly post-ship learning. **Registry scope growth.** `dsh-tools` absorbs codegen, a tool, a bridge, and an event. Package modules separate these responsibilities (`ts-types.ts` and `ptc.ts` beside `schema.ts`, `json-schema.ts`, and `presentation.ts`), while `ctx.ptcRuntime` owns all ptc-runtime-specific implementation. diff --git a/.agents/notes/implemented/feature/2026-06-15-ptc.zh.md b/.agents/notes/implemented/feature/2026-06-15-ptc.zh.md index 241b05b4bf..669a63e991 100644 --- a/.agents/notes/implemented/feature/2026-06-15-ptc.zh.md +++ b/.agents/notes/implemented/feature/2026-06-15-ptc.zh.md @@ -18,7 +18,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一 三项决策,各自在下方独立小节中展开: -1. **PTC mode 是 `ToolRuntime`(`dsh-tools`)的一等呈现模式**,通过经校验的 `mode` 配置选择:`'native'`(默认,贡献可见能力 schema)、`'ptc'`(注册表仅贡献其保留的 `run_code` 传输通道加一份生成的 SDK `.d.ts` 到系统提示词中)或 `'both'`(原生 schema 加传输通道 + SDK)。注册表在源头构建其规范贡献;协作式提示词组装的结果仍具权威性,记录在日志中的请求头精确反映该返回的呈现。 +1. **PTC mode 是 `ToolRuntime`(`dsh-tools`)的一等呈现模式**,通过经校验的 `mode` 配置选择:`'native'`(默认,贡献可见能力 schema)或 `'ptc'`(注册表仅贡献其保留的 `run_code` 传输通道加一份生成的 SDK `.d.ts` 到系统提示词中)。注册表在源头构建其规范贡献;协作式提示词组装的结果仍具权威性,记录在日志中的请求头精确反映该返回的呈现。 2. **代码执行是一个能力 seam**——`packages/ptc-runtime/` 包含 Service Definition 包 `@deepseek-ai/dsh-ptc-runtime`,拥有 `ctx.ptcRuntime`([能力 seam](../architecture/2026-06-13-capability-seams.zh.md);消费方 = `dsh-tools`,core 消费 seam 的先例见 `agent-loop` → `dsh-llm`)。运行时对工具一无所知:它接收一段程序和命名的异步绑定,执行程序,报告 `{ value, logs, error? }`。语言和基底是后端属性,因此未来的 Python 或容器后端只是另一个 Service Provider 包,而非重新设计。 3. **交付的实现是 `@deepseek-ai/dsh-ptc-runtime-node`**:每个程序在全新的受管 Node 进程中,按调用 Session 的已解析沙箱策略运行。直接 Node API 在该策略内仍可使用;具名 Host 绑定通过单独验证的控制通道调用。 @@ -32,13 +32,13 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一 ### 注册表拥有模式 -`ToolRuntime` 获得一个经 schemastery 校验的配置(`static Config`),这是它的第一个配置:`mode: 'native' | 'ptc' | 'both'`,默认 `'native'`。部署通过 `cordis.yml` 翻转模式(`tools: { mode: ptc }`),无需改代码,遵循 no-hardcoded-tunables 约定。 +`ToolRuntime` 获得一个经 schemastery 校验的配置(`static Config`),这是它的第一个配置:`mode: 'native' | 'ptc'`,默认 `'native'`。部署通过 `cordis.yml` 翻转模式(`tools: { mode: ptc }`),无需改代码,遵循 no-hardcoded-tunables 约定。 -**协议工具列表。** 注册表在 `'native'` 下贡献可见能力,在 `'ptc'` 下仅贡献 `run_code`,在 `'both'` 下两者都贡献。最终的 `PromptAssembly.tools` 列表记录在请求头中。`run_code` 是一个保留的呈现传输通道,位于注册和限制层之外;直接提示词提供方和组装 waterfall 仍各自负责自己的贡献。 +**协议工具列表。** 注册表在 `'native'` 下贡献可见能力,在 `'ptc'` 下仅贡献 `run_code`。[呈现方式简化决策](../simplification/2026-10-05-native-or-ptc-presentation.zh.md) 记录混合呈现的移除。最终的 `PromptAssembly.tools` 列表记录在请求头中。`run_code` 是一个保留的呈现传输通道,位于注册和限制层之外;直接提示词提供方和组装 waterfall 仍各自负责自己的贡献。 **与 `toolOrder` 的交互:** 如果配置的 `systemPrompt.toolOrder` 引用了原生能力名称,在 `mode: 'ptc'` 下会拒绝所有组装,因为那些名称不在该模式的协议校验范围内。这是正确行为而非 bug:使用 PTC mode 的部署需要更新其 order 配置或移除它。 -**SDK 提示词段。** 在 `'ptc'` 和 `'both'` 下,tool-guidance order band 中的惰性 `tools:sdk` 段为当前 scope 的可见能力渲染所加载运行时语言的声明加固定的使用说明(默认 TypeScript;[语言分发 note](../../archived/feature/2026-07-31-ptc-language-dispatch.md) 加入了 Python 与按 `ctx.ptcRuntime.language` 选择的渲染器表)。它共享查找和执行可见性,排除 `run_code`,并按字典序排列工具以获得字节稳定的输出。 +**SDK 提示词段。** 在 `'ptc'` 下,tool-guidance order band 中的惰性 `tools:sdk` 段为当前 scope 的可见能力渲染所加载运行时语言的声明加固定的使用说明(默认 TypeScript;[语言分发 note](../../archived/feature/2026-07-31-ptc-language-dispatch.md) 加入了 Python 与按 `ctx.ptcRuntime.language` 选择的渲染器表)。它共享查找和执行可见性,排除 `run_code`,并按字典序排列工具以获得字节稳定的输出。 **组装所有权。** `run_code` 和 `tools:sdk` 作为正常的组装输入进入受信任的 `system-prompt/assemble` waterfall。一个 scoped 的 `tools:sdk` 段可以在分发前遮蔽全局默认值,监听器也可以移除或替换任一贡献。waterfall 返回的组装结果是最终的,因此修改这些输入的人有责任在部署期望 PTC mode 可用时保持协议面的完整性;没有恢复 pass 会覆盖有意的组合。 @@ -46,7 +46,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一 ### run_code 工具与分发桥 -在 `'ptc'` 和 `'both'` 下,注册表拥有 `run_code` 作为保留的呈现传输通道,带两个必需参数 `{ code: string; description: string }`(description 为 UI 标注该调用,沿用 bash 的先例)。它由一个正常的 `ToolDefinition` 表示以供分发,但位于可过滤的能力层之外,因此限制规则不会意外移除 PTC mode 的唯一入口。调用遍历完整的工具流水线——`tools/pre-execute` → 单调性守卫 → `tools/execute` 包裹分发 → `tools/post-execute` → 由定义拥有的可选 `finalizeContent` → 不可变的 `tools/result` 通知——与原生调用完全一致;权限插件可以在程序运行前检查程序文本,最终结果观察者看到的是规范化的外层结果。其 `execute(args, exec)`: +在 `'ptc'` 下,注册表拥有 `run_code` 作为保留的呈现传输通道,带两个必需参数 `{ code: string; description: string }`(description 为 UI 标注该调用,沿用 bash 的先例)。它由一个正常的 `ToolDefinition` 表示以供分发,但位于可过滤的能力层之外,因此限制规则不会意外移除 PTC mode 的唯一入口。调用遍历完整的工具流水线——`tools/pre-execute` → 单调性守卫 → `tools/execute` 包裹分发 → `tools/post-execute` → 由定义拥有的可选 `finalizeContent` → 不可变的 `tools/result` 通知——与原生调用完全一致;权限插件可以在程序运行前检查程序文本,最终结果观察者看到的是规范化的外层结果。其 `execute(args, exec)`: 1. **构建绑定。** 一个 run 级别的 signal 跟随外层取消,并在 run 结算时被 abort。每个可见工具绑定都会对无损 JSON 参数创建快照,进入原生约定的分发池(调度设计由[实时并行 参考](../../../../packages/core/tools/README.zh.md) 负责),以确定性的 call id 和外层 token 作为 `parent` 执行,通过外层 execution 延后返回的上下文,并记录 `tool/ptc-dispatch-start`/`tool/ptc-dispatch` 事件对,其中结算侧携带完整渲染后的结果内容。成功时返回工具最终的规范 JSON 值;失败则变为程序可见的 `ToolCallError`。每个子调用保留自己不可变的执行标识,并遍历完整的工具流水线。 2. **运行已解析输入:** `ctx.ptcRuntime.run(ctx.ptcRuntime.resolve(request))`。请求携带程序、绑定、运行级信号、调用 Session 的 cwd 与适用的已解析文件策略。运行级信号将外层结算与执行取消关联。 @@ -86,7 +86,7 @@ Node 提供方擦除可擦除 TypeScript,在配置的文件系统/子进程 ### 模型看到的内容 -SDK 指示模型编写一个所加载运行时语言的异步函数体(默认可擦除 TypeScript;Python 运行时下为 Python `async` 函数体——见[语言分发 note](../../archived/feature/2026-07-31-ptc-language-dispatch.md)),通过 `await tools.name(args)` 调用工具,在需要时捕获被拒绝的工具调用,并仅 return 或 log 应重新进入上下文的输出。两种 flavor 用各自的原语陈述同一约定:相互独立的只读调用可以(MAY)在 `Promise.all`(TypeScript)或 `asyncio.gather`(Python)下重叠,有副作用的调用按提交顺序单独运行,有依赖的工作用 `await` 排序。声明前缀可能与原生 schema 一样大,尤其在 `'both'` 下,但对提供方缓存保持稳定。 +SDK 指示模型编写一个所加载运行时语言的异步函数体(默认可擦除 TypeScript;Python 运行时下为 Python `async` 函数体——见[语言分发 note](../../archived/feature/2026-07-31-ptc-language-dispatch.md)),通过 `await tools.name(args)` 调用工具,在需要时捕获被拒绝的工具调用,并仅 return 或 log 应重新进入上下文的输出。两种 flavor 用各自的原语陈述同一约定:相互独立的只读调用可以(MAY)在 `Promise.all`(TypeScript)或 `asyncio.gather`(Python)下重叠,有副作用的调用按提交顺序单独运行,有依赖的工作用 `await` 排序。声明前缀可能与原生 schema 一样大,但对提供方缓存保持稳定。 传输自身的 `description` 与两种 flavor 的 SDK 说明都以点名 `code` 和 `description` 这两个必填参数开头。把该调用描述成「传入一个程序」的散文会让第二个参数只能从参数 schema 中发现,而只发出 `{code}` 的模型会因 `INVALID_ARGS` 被拒,连同已写好的整个程序一起丢失。 @@ -113,7 +113,7 @@ SDK 指示模型编写一个所加载运行时语言的异步函数体(默认 **始终排他(忠于 Cloudflare,无模式)。** 否决,因为本 SDK 的主要消费方是编码 agent:其日常的单次调用(`bash`、`read`、`edit`)作为原生调用已经是最优的,强制每次编辑都通过程序会给常见场景增加负担。mode 配置让忠实形式(`'ptc'`)只需一行配置即可启用,而不强加于人。 -**每工具可见性分层(此工具 native,彼工具 ptc-only)。** 推迟:它需要每工具元数据和 `'native' | 'ptc' | 'both'` 不提供的呈现拆分,且其设计取决于模型在 `'both'` 下如何分配使用的证据。 +**每工具可见性分层(此工具 native,彼工具 ptc-only)。** 推迟:它需要每工具元数据,以及针对原生操作和仅限程序操作的独立准入规则。整个 agent 采用原生或 PTC 呈现,可让每个 agent 保持一种调用约定。 **SDK 中的清洁化标识符别名**(`my-tool` → `my_tool`,Cloudflare 的做法)。否决:`declare const` 上的带引号键使每个名称可达,零别名碰撞逻辑;模型能正常处理 `tools["my-tool"](…)`。 @@ -125,7 +125,7 @@ SDK 指示模型编写一个所加载运行时语言的异步函数体(默认 **`stripTypeScriptTypes` 标记为 experimental。** 它与 Node 自身原生 `.ts` 执行背后的引擎(amaro/swc)相同,在本仓库的整个引擎范围内作为 API 暴露。缓解措施:运行时的单元测试套件会检查位置保持和可擦除限制拒绝消息中的必需部分;调用位于一个私有函数之后,且 `amaro`/`sucrase` 可在 API 变化时直接替换它。仅可擦除子集是面向模型的输入限制,错误消息会告诉模型如何修正程序。 -**SDK 的提示词成本,尤其在 `'both'` 下。** `.d.ts` 可能与它补充的原生 schema 体量相当;`'both'` 携带两种表示。前缀稳定性 + 提供方缓存摊销了每会话成本;mode 按部署配置;本 Agent Note 不做无条件节省的声明。何时优先使用哪种模式的量化指导明确属于上线后学习。 +**SDK 的提示词成本。** `.d.ts` 可能与它替代的原生 schema 体量相当。前缀稳定性 + 提供方缓存摊销了每会话成本;mode 按部署配置;本 Agent Note 不做无条件节省的声明。何时优先使用哪种模式的量化指导明确属于上线后学习。 **注册表 scope 增长。** `dsh-tools` 吸收了代码生成、一个工具、一个桥和一个事件。包内模块把这些职责分开(`ts-types.ts`、`ptc.ts` 与 `schema.ts`、`json-schema.ts`、`presentation.ts` 并列),所有 ptc-runtime 专用实现都由 `ctx.ptcRuntime` 提供。 diff --git a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.i18n.yaml b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.i18n.yaml index 9ec692a276..639381d125 100644 --- a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.i18n.yaml @@ -12,8 +12,8 @@ en: 532bb7268d9b93ce zh: ad47162233222ab0 /agent-note-parallel-tool-call-execution-by-per-call-safety/scheduling-and-ordering: - en: 3c01d3f0cbc97822 - zh: 1ae74f64773e2c55 + en: 4e85280cc5eb8697 + zh: 8e6b09b1527784fa /agent-note-parallel-tool-call-execution-by-per-call-safety/safety-contract: en: 16c4ee08b65ead23 zh: df80d6e49982615b diff --git a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md index d459961087..40adc9f9aa 100644 --- a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md +++ b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md @@ -48,7 +48,7 @@ Each started call appends `tool/call` immediately before its pre-execute gate. C An abort stops replenishment, waits for already-started calls, commits their results in order, drains accepted batch context after those results, and records synthetic aborted call/result pairs for undispatched calls. An unexpected scheduler failure stops new dispatches, waits for every already-started dispatch to settle, and rethrows the first failure. The driver records missing results before closing the failed step under the [failed-step recovery reference](../../../../packages/core/agent-loop/README.md). -PTC mode remains outside this scheduler because the model emits one native `run_code` call. `run_code` and its internal dispatch queue remain serial; native sibling calls in `mode: 'both'` use the normal scheduler. +The loop schedules native sibling calls and each outer `run_code` call. A `run_code` program has its own bounded dispatch pool, which uses the same parallel/exclusive classification; the [PTC execution reference](../../../../packages/core/tools/README.md#ptc-mode) owns that nested scheduling. ## Safety contract diff --git a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md index 4c55cde471..536541370b 100644 --- a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md +++ b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md @@ -48,7 +48,7 @@ Status: implemented 中止会停止补充池,等待已启动的调用,按顺序提交其结果,在这些结果之后排空已接受的批次上下文,并为未派发的调用记录合成的中止调用/结果对。调度器发生意外故障时,会停止新的派发,等待每项已启动的派发结算,并重新抛出第一个故障。驱动器按照[失败步骤恢复参考](../../../../packages/core/agent-loop/README.zh.md),在关闭失败步骤之前记录缺失结果。 -PTC mode 仍不使用此调度器,因为模型只会发出一个原生 `run_code` 调用。`run_code` 及其内部派发队列仍按串行方式执行;`mode: 'both'` 中的原生并列调用使用常规调度器。 +循环调度原生并列调用和每个外层 `run_code` 调用。`run_code` 程序拥有自己的有界派发池,并使用相同的并行/独占分类;[PTC 执行参考](../../../../packages/core/tools/README.zh.md#ptc-mode)负责说明该嵌套调度。 ## 安全约定 diff --git a/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.i18n.yaml b/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.i18n.yaml index f38847804c..5f8169b4c4 100644 --- a/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.i18n.yaml @@ -33,8 +33,8 @@ en: dedd87b9b35d8fab zh: 78db4c7e8c4e5f61 /agent-note-typed-tool-returns-in-ptc-mode/consequences: - en: 7c2ee65532ab76f8 - zh: 3fb074c69909d390 + en: f5726b5e7a2fc73f + zh: 0b9aa884db5d920f /agent-note-typed-tool-returns-in-ptc-mode/known-limitations-and-deferred-work: en: a42af629ad6872cc zh: 41e5c0bb3de0ae4d diff --git a/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.md b/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.md index fb8e2b68c6..032322407d 100644 --- a/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.md +++ b/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.md @@ -99,7 +99,7 @@ Keyless real-worker integration tests pin the two handle workflows that prose re ## Consequences -Code programs can compose tools through stable values instead of reverse-engineering Native prose. Native and Both Mode retain their existing text and UI presentation, while PTC mode receives output-schema types and exact runtime JSON. Tool authors must treat the canonical value as their programmatic API and put display-only formatting in the renderer. +Code programs can compose tools through stable values instead of reverse-engineering Native prose. Native mode retains its existing text and UI presentation, while PTC mode receives output-schema types and exact runtime JSON. Tool authors must treat the canonical value as their programmatic API and put display-only formatting in the renderer. The worker performs bounded-depth flat-wire transport and lossless validation but does not make intermediate values cheap or durable. Outer overflow is an explicit failed run, and error handling remains intentionally human-guided rather than a versioned code union. diff --git a/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.zh.md b/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.zh.md index 6f7896b386..6d8bf2d771 100644 --- a/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.zh.md +++ b/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.zh.md @@ -99,7 +99,7 @@ PTC mode 通过运行时请求中的 `{ name: "ToolCallError", memberNamePropert ## 后果 -PTC mode 程序可以通过稳定值组合工具,无需逆向解析 Native 自然语言。Native 和 Both Mode 保留现有文本与 UI 展示,PTC mode 则获得输出 schema 类型和精确的运行时 JSON。工具作者必须把规范值视为程序化 API,并将仅用于展示的格式化放入渲染器。 +PTC mode 程序可以通过稳定值组合工具,无需逆向解析 Native 自然语言。Native mode 保留现有文本与 UI 展示,PTC mode 则获得输出 schema 类型和精确的运行时 JSON。工具作者必须把规范值视为程序化 API,并将仅用于展示的格式化放入渲染器。 worker 会以嵌套深度有界的扁平协议格式传输数据并执行无损校验,但不会降低中间值的开销,也不会使其具备持久性。外层输出溢出会显式导致运行失败,错误处理则有意由人类引导,而不是依赖带版本的错误代码联合。 diff --git a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml index bc15b94fa0..6566e1a57e 100644 --- a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml @@ -3,29 +3,29 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md /agent-note-continuable-subagents: - en: 14c7d31032237174 - zh: ac2666670a0ca5ab + en: 110c0660aacfa3e4 + zh: cf405fad1c5834f7 /agent-note-continuable-subagents/problem: en: a72071eeedb77402 zh: 818d16ce429fd610 /agent-note-continuable-subagents/decision: - en: 445a8c9783aad214 - zh: 9b5c576bb76100a4 + en: 3420195435e02a8f + zh: 6156eedb3c23d360 /agent-note-continuable-subagents/decision/materialization-and-public-operations: - en: c7c55e5738a7c0d8 - zh: 54849d8938f7c77f + en: c3a83ff748a02143 + zh: 468bf62c8ddabf70 /agent-note-continuable-subagents/decision/durable-session-and-live-activation: - en: 92fd7302d4c8f2e5 - zh: e821764c07070ebc + en: 5c96c0343564c64a + zh: 85686e022d870cc7 /agent-note-continuable-subagents/decision/activation-lifecycle: - en: 68215c0ae5628608 - zh: fdf038decac8f07a + en: 5b84e6825247850c + zh: 18d43381aa3e8b05 /agent-note-continuable-subagents/decision/one-inbox-and-follow-up-delivery: - en: 065399ce5b1de59e - zh: b60af3c37851323e + en: 388934f4c4e50f65 + zh: 545a66157a4c46c4 /agent-note-continuable-subagents/decision/child-ownership: - en: 503fb4311cd94674 - zh: 376fa98ec4d22b20 + en: b6b62a4c083562b6 + zh: 86d02714e997942d /agent-note-continuable-subagents/decision/adjacent-agent-messaging: en: a6899f5a82d5dceb zh: 0671b89b2dca9c4e @@ -36,17 +36,17 @@ en: f5ba7e9c5339ceef zh: 31ae1b2f57e44463 /agent-note-continuable-subagents/decision/durability-disposal-and-recovery: - en: a15da9cc6f6cdb9d - zh: 48572ff7ca7fd623 + en: 0bbc18843aa04ad3 + zh: d56fab1d12a28a96 /agent-note-continuable-subagents/decision/scope: - en: ba4a08a89bb9ec30 - zh: db376a52db29774d + en: bb3e8b2f8054e075 + zh: 15c126d51031fcb3 /agent-note-continuable-subagents/alternatives-considered: - en: 485aef32231dac62 - zh: 5e2c103afb3e2d6f + en: d786fd413fabe0f5 + zh: c5d66f5b880d9758 /agent-note-continuable-subagents/consequences: - en: 807a7d8176249fce - zh: b2fca8a83551f700 + en: d7ba8e86c3f14eaa + zh: b31ba49dee6aa85d /agent-note-continuable-subagents/consequences/accepted-costs: - en: 17beb77521aa5dc7 - zh: f607ec51bbeae380 + en: 95199dfb2adab107 + zh: e7644bcb70169368 diff --git a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md index 142119cd0a..e76aae22fe 100644 --- a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md +++ b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md @@ -4,7 +4,7 @@ Status: implemented English | [中文](2026-07-28-continuable-subagent-conversations.zh.md) -This record replaces the Task-backed continuation manager from [Continuable background subagents](../../archived/feature/2026-07-21-continuable-background-subagents.md). It retains the single `ctx.subagents` service from [Merge subagent control into the subagent service](../../archived/simplification/2026-07-26-merge-subagent-control-service.md) and the intent-named `followup` operation from [Intent-named subagent continuation operations](../../archived/simplification/2026-07-27-intent-named-subagent-continuation-operations.md). +This record replaces the Task-backed continuation manager from [Continuable background subagents](../../archived/feature/2026-07-21-continuable-background-subagents.md). It retains the single `ctx.subagents` service from [Merge subagent control into the subagent service](../../archived/simplification/2026-07-26-merge-subagent-control-service.md). ## Problem @@ -18,6 +18,8 @@ Parent Agents need to send later work to the same live child without changing it ## Decision +The shared activation entry point, external execution ownership, and caller-versus-parent result delivery are governed by [Unified subagent activations](../simplification/2026-09-17-unified-subagent-activations.md). This record retains the independent rationale described below. + A continuable subagent has one durable Session and at most one process-local Activation: ```text @@ -28,25 +30,25 @@ persisted Session -> zero or more owned child Activations ``` -An Activation is one residency epoch for a reconstructed child Agent. It may execute multiple FIFO turns and remain resident while waiting for descendants. It is not a request, result, cancellation, or Task boundary. +An Activation is one residency epoch for a reconstructed child Agent. It may execute multiple FIFO turns and remain resident while waiting for descendants. Its receipt owns the result and cancellation of that exact residency; it does not assign a separate result or Task to each accepted message. The continuation manager owns activation admission, authority checks, the live ownership graph, cold resume, and child-first disposal. The Agent loop owns all turn ordering and execution. No continuable subagent has a Task, an Activation FIFO, or queued Activation state. ### Materialization and public operations -The named subagent provider participates only in preparing the initial creation spec, where `spawn` and `fork` differ. Its optional `prepareContinuable(request): Promise` method is the continuable-creation capability. The returned spec contains only detached provider-specific creation inputs such as the optional parent-history seed; it contains no Agent, `AgentHandle`, prompt delivery, result, disposal, or resume operation. The manager reserves the child identity, resolves the durable descriptor and common Agent setup, calls `ctx.agents.create()` through a private activation-owner scope, installs the returned `AgentHandle` into the Activation, establishes any continuable-parent ownership, and then calls `Agent.followup(initialPrompt)`. Inbox acceptance yields a `MessageId`; at that boundary `ctx.subagents.startContinuable()` returns `{ childId, messageId }` without waiting for the turn to start or for the message to enter the Session log. +The named subagent provider participates only in preparing the initial creation spec, where `spawn` and `fork` differ. Its optional `prepareContinuable(request): Promise` method is the continuable-creation capability. The returned spec contains only detached provider-specific creation inputs such as the optional parent-history seed; it contains no Agent, `AgentHandle`, prompt delivery, result, disposal, or resume operation. The manager reserves the child identity, resolves the durable descriptor and common Agent setup, calls `ctx.agents.create()` through a private activation-owner scope, installs the returned `AgentHandle` into the Activation, establishes any continuable-parent ownership, and then calls `Agent.followup(initialPrompt)`. Inbox acceptance yields a `MessageId`; at that boundary `ctx.subagents.startActivation()` returns a receipt containing `childId`, `messageId`, `result`, and `dispose()` without waiting for the turn to start or for the message to enter the Session log. Any failure before inbox acceptance rejects without returning either id. Agent creation provides rollback before handle transfer; after transfer, the manager keeps one closing transaction visible to concurrent delivery and drain, disposes the created handle, removes the Activation, and rolls back any parent `ownedChildren` membership before rejecting. Failure before the residency start edge publishes no terminal edge, while failure after a published start closes the lifecycle pair through normal disposal. -`backgroundMode: 'one-shot' | 'continuable'` remains deployment policy. Configured continuable mode requires `prepareContinuable`; method presence replaces `SubagentProvider.resume?()` as the capability check, while a capable provider may still run one-shot work. +Provider method presence selects execution: `prepareContinuable()` creates a local activation; otherwise `start()` starts external execution. `delivery` chooses the result recipient without changing the local lifecycle. -Cold resume does not dispatch through a subagent provider. The continuation manager folds the generic in-process descriptor, calls `ctx.agents.resume()` through the same activation-owner scope, installs the returned `AgentHandle`, and submits the waiting `next-turn`. `SubagentProvider.resume?()` and `SubagentProviderResumeRequest` are absent. The descriptor retains the initial provider name after that provider unregisters; the name does not grant a recovery capability or require the provider for later residency. Remote providers require a separate design. +Cold resume does not dispatch through a subagent provider. The continuation manager folds the generic in-process descriptor, calls `ctx.agents.resume()` through the same activation-owner scope, installs the returned `AgentHandle`, and submits the waiting `next-turn`. `SubagentProvider.resume?()` and `SubagentProviderResumeRequest` are absent. The descriptor retains the initial provider name after that provider unregisters; the name does not grant a recovery capability or require the provider for later residency. External executions do not support cold resume. -`SubagentProvider.start()` and `SubagentRun` remain exclusively on the unchanged one-shot path. A continuable Activation directly owns its `AgentHandle` and never creates, wraps, or retains a `SubagentRun`; `SubagentRun.steer?()` is therefore absent. +`SubagentProvider.start()` and `SubagentRun` belong to external execution. A local Activation directly owns its `AgentHandle`; both kinds share manager-owned capacity, settlement, and disposal. -`ctx.subagents.sendMessage(sender, targetId, content, { signal })` is the sole model-authored continuation-message operation. The exact live sender authorizes delivery to its direct parent or direct continuable child; cold resume checks direct-child authority before reconstruction and every path checks again in the final no-await inbox-admission span, so an Agent unregistered or replaced during materialization cannot authorize delivery. The service derives a durable `agent-message` source from that sender. The model-facing `send_message` tool keeps only `agent_id` and `message` and uses fixed Steer scheduling. Both start and send return the accepted `MessageId`, and neither reports how the manager materialized the Activation. +`ctx.subagents.sendMessage(sender, targetId, content, { signal })` is the sole model-authored continuation-message operation. The exact live sender authorizes delivery to its direct parent or direct continuable child; cold resume checks direct-child authority before reconstruction and every path checks again in the final no-await inbox-admission span, so an Agent unregistered or replaced during materialization cannot authorize delivery. The service derives a durable `agent-message` source from that sender. The model-facing `send_message` tool keeps only `agent_id` and `message` and uses fixed Steer scheduling. Local start returns an activation receipt containing the accepted `MessageId`; send returns the `MessageId` directly. Neither exposes whether materialization created or resumed the Agent. -For start and follow-up, the caller signal owns lookup, materialization, and admission only until inbox acceptance. After the operation returns its `MessageId`, the manager owns the Activation independently; later caller cancellation does not cancel the accepted turn or dispose the child. +For start and follow-up, the caller signal owns lookup, materialization, and admission only until inbox acceptance. After inbox acceptance, the manager owns the Activation independently; later caller cancellation does not cancel the accepted turn or dispose the child. ### Durable Session and live Activation @@ -54,7 +56,7 @@ The Session owns the stable child identity, transcript, direct-parent lineage, d An idle historical Session has no `AgentHandle`. The first authorized `next-turn` delivery resumes an Activation from the persisted Session and submits the message to its inbox. Cold resume uses the exact live parent Agent for authorization and, when that parent has an Activation, ownership; it never uses the parent for reconstruction. -The Activation directly owns the published `AgentHandle` until it settles, while the manager's private activation-owner scope is its structural Cordis owner. The continuable path creates no intermediate result-bearing execution wrapper, including `SubagentRun`; one-shot delegation remains unchanged and outside this lifecycle. Remote providers are out of scope here and require a separate Activation ownership contract when introduced. Historical Sessions consume no runtime memory after their Activation is disposed. +A local Activation directly owns the published `AgentHandle` until settlement; the private activation-owner scope is its structural Cordis owner. It creates no intermediate `SubagentRun`. The unified manager also owns external executions through their provider handles. Historical Sessions retain no Agent resources after disposal. ### Activation lifecycle @@ -81,21 +83,21 @@ no Activation The manager derives these states from Agent quiescence, the Inbox's pending state, and the owned-child set rather than maintaining a second execution state machine. A `next-turn` delivered while `running` joins the Agent inbox. A waking delivery while `waiting` wakes the same Agent and returns the Activation to `running`. Delivery after disposal cold-resumes a new Activation. -The manager linearizes manager-owned delivery, child release, and disposal for each durable child. A private `SubagentInbox` delegates Queue and Steer to the Agent inbox and owns the Activation's existing close transaction. If manager delivery races with final disposal, exactly one side wins this admission cutoff: delivery either enters the still-live Agent inbox, or observes closing and follows its operation-specific rejection or cold-resume path. Direct Agent work does not use this wrapper, so natural settlement uses short maintenance claims to validate the idle phase before the final flush and final disposal decision, then revalidates the Session sequence, Inbox pending state, wake generation, and owned-child set under the child lock. Accepted work that remains active or changes Session, Inbox, or ownership state invalidates that settlement attempt instead of being cancelled by it; maintenance that starts and finishes entirely during the flush has completed before the cutoff. +The manager linearizes manager-owned delivery, child release, and disposal for each durable child. Private manager delivery delegates Queue and Steer to the Agent inbox and checks the Activation closing promise. If manager delivery races with final disposal, exactly one side wins this admission cutoff: delivery either enters the still-live Agent inbox, or observes closing and follows its operation-specific rejection or cold-resume path. Direct Agent work does not use this wrapper, so natural settlement uses short maintenance claims to validate the idle phase before the final flush and final disposal decision, then revalidates the Session sequence, Inbox pending state, wake generation, and owned-child set under the child lock. Accepted work that remains active or changes Session, Inbox, or ownership state invalidates that settlement attempt instead of being cancelled by it; maintenance that starts and finishes entirely during the flush has completed before the cutoff. ### One inbox and follow-up delivery -The Agent inbox is the only queue. Every continuation message uses `Agent.followup()` and becomes one FIFO turn; neither the continuation manager nor the host maintains another message queue. Every pending Inbox occurrence keeps the current Activation live until it is claimed or discarded. This conservative rule also retains injected context: a quiet injection that remains after quiescence can keep the Activation and its live ancestors resident until a waking delivery claims it, a queue mutation removes it, or manager teardown disposes the tree. +The Agent inbox is the only queue. Agent messages use Steer; human prompts choose Queue or Steer. Every pending Inbox occurrence keeps the current Activation live until claimed or discarded, including quiet injected context. Neither the manager nor the host maintains a second message queue. Routing depends only on Activation residency: -| Activation state | `followup` | +| Activation state | Waking message delivery | |---|---| | `running` | enqueue in the same Activation | | `waiting` | wake the same Activation | | no Activation | cold-resume a new Activation | -The continuation layer defines no separate delivery-route result. Successful `ctx.subagents.followup()` and `send_message` delivery returns the accepted `MessageId`, while delivery failure throws. Existing `agent/inbox/enqueue`, `agent/inbox/dequeue`, and `agent/inbox/discard` events remain the message-lifecycle observations; adapters may render a generic acceptance but do not expose `started`, `queued`, `resumed`, or another subagent-specific route vocabulary. +Successful `sendMessage()` returns the accepted `MessageId`; failed delivery throws. Inbox events remain the message-lifecycle observations. The service exposes no separate started/queued/resumed routing result. ### Child ownership @@ -107,9 +109,9 @@ Child release occurs only after the child Agent is quiescent, its Inbox is empty Ownership is retained until the child Activation is disposed. A later refinement may release a request-scoped lease earlier, but it would require an exact turn-completion correlation that this Task-free design deliberately does not add. -Top-level teardown is host-owned rather than represented as another Activation. Manager unload invokes its internal manager-wide drain to close admission synchronously, await every admitted materialization through publication or rollback, stop the stable live forest, and release it child-first. A host that owns selected top-level Agents uses `drainContinuableDescendants(parents)`: exact Agent identities close admission only below those roots until each leaves the registry, while unrelated forests and manager-wide admission remain live; the manager stops their visible descendants before its first await, waits only materializations admitted below those roots, and releases only the selected branches. Every materialized start and live delivery rechecks caller cancellation, the applicable draining scope, Activation disposal, and exact parent authority in the same synchronous span as inbox submission, so teardown or parent replacement that wins before acceptance prevents delivery to the closing handle. Only after the applicable drain settles may the host dispose its top-level Agents; only manager-wide drain precedes manager-scope disposal. +Top-level teardown is host-owned rather than represented as another Activation. Manager unload invokes its internal manager-wide drain to close admission synchronously, await every admitted materialization through publication or rollback, stop the stable live forest, and release it child-first. A host that owns selected top-level Agents uses `drainDescendants(parents)`: exact Agent identities close admission only below those roots until each leaves the registry, while unrelated forests and manager-wide admission remain live; the manager stops their visible descendants before its first await, waits only materializations admitted below those roots, and releases only the selected branches. Every materialized start and live delivery rechecks caller cancellation, the applicable draining scope, Activation disposal, and exact parent authority in the same synchronous span as inbox submission, so teardown or parent replacement that wins before acceptance prevents delivery to the closing handle. Only after the applicable drain settles may the host dispose its top-level Agents; only manager-wide drain precedes manager-scope disposal. -The activation-owner scope exists because ordinary Cordis owner effects unwind in reverse registration order, which cannot express the dynamic child graph. Manager initialization registers the private scope's structural disposer first and its drain disposer afterward, so reverse unwind invokes the drain before releasing that scope; merely registering a cleanup effect on the same scope as later Agent handles would allow structural handle disposal to bypass child-first ordering. Each materialization registers its barrier participant and snapshots its exact live ancestry before starting the inner transaction, then remains tracked until it installs an Activation or fully rolls back. The Activation retains weak membership of that ancestry, so an intermediate Agent may leave the registry without hiding a still-live descendant from its host root. Its private `SubagentInbox` installs one memoized closing promise before cancellation or recursive callbacks, allowing scoped host shutdown, global manager unload, child release, and normal settlement to converge without double release. Cancellation propagates top-down before slow descendant cleanup; handle release remains child-first. Sibling branches drain independently; one disposal failure is recorded but does not prevent the manager from attempting the remaining selected handles, and the aggregate drain reports failure after all selected branches settle. Durable child Sessions survive this process-local teardown. +The activation-owner scope exists because ordinary Cordis owner effects unwind in reverse registration order, which cannot express the dynamic child graph. Manager initialization registers the private scope's structural disposer first and its drain disposer afterward, so reverse unwind invokes the drain before releasing that scope; merely registering a cleanup effect on the same scope as later Agent handles would allow structural handle disposal to bypass child-first ordering. Each materialization registers its barrier participant and snapshots its exact live ancestry before starting the inner transaction, then remains tracked until it installs an Activation or fully rolls back. The Activation retains weak membership of that ancestry, so an intermediate Agent may leave the registry without hiding a still-live descendant from its host root. The manager installs one memoized closing promise before cancellation or recursive callbacks, allowing scoped host shutdown, global manager unload, child release, and normal settlement to converge without double release. Cancellation propagates top-down before slow descendant cleanup; handle release remains child-first. Sibling branches drain independently; one disposal failure is recorded but does not prevent the manager from attempting the remaining selected handles, and the aggregate drain reports failure after all selected branches settle. Durable child Sessions survive this process-local teardown. ### Adjacent-Agent messaging @@ -129,7 +131,7 @@ Parent-originated delivery requires the parent to be live when admitted and keep ### Durability, disposal, and recovery -Without Jobs there is no `job_output`, `job_kill`, Task status, or per-message result promise. The caller signal can abort start or follow-up only before inbox acceptance. After acceptance, the parent cannot cancel the accepted message or dispose the Activation through `ctx.subagents`; the only public stop is the later [current-turn interrupt](../../../../packages/subagent/subagent/README.md), which cancels the live target's current turn with `keepInbox` and leaves residency, pending work, and descendants intact. +An activation receipt exposes `result` and `dispose()` without Jobs. The caller signal owns unpublished work; receipt disposal cancels the exact activation and awaits descendant startup rollback and child-first release. The [current-turn interrupt](../../../../packages/subagent/subagent/README.md) cancels only the live target turn with `keepInbox`, preserving residency, pending work, and descendants. Host and manager teardown remains the lifecycle stop path. Manager unload applies it globally; a host applies it only below the exact top-level Agents it owns. Each form closes the applicable admission scope, stops the selected visible Activations, awaits admitted materializations in that scope, releases child-first, and preserves the durable Sessions. @@ -141,9 +143,9 @@ Session and descriptor persistence survive restart. Activation state, Agent inbo ### Scope -This version covers continuable in-process children and leaves one-shot delegation unchanged. Remote providers require a separate Activation handle with equivalent authenticated control and child-first quiescence contracts before they can support the same behavior. +Local continuable control and external execution share activation ownership. Only local children support later messages and cold resume. -It adds no host-user continuation, subagent steering operation, durable mailbox, cross-process lease, automatic replay of interrupted inbox work, team authority, workflow authority, public residency query, or runtime cache; the later [current-turn interrupt](../../../../packages/subagent/subagent/README.md) added the one public stop operation on top of this lifecycle. Existing delegation-depth policy remains unchanged. Optional child-to-parent reporting is a later consumer of this lifecycle rather than part of the base continuable capability. +Human Queue/Steer delivery and [current-turn interruption](../../../../packages/subagent/subagent/README.md) use this lifecycle; receipt disposal stops its exact activation and owned descendants. Durable mailboxes, cross-process leases, automatic replay of interrupted inbox work, team authority, workflow authority, public residency queries, and runtime caches remain outside this capability. Delegation-depth policy and adjacent-Agent message authorization retain their own owners. ## Alternatives considered @@ -155,7 +157,7 @@ It adds no host-user continuation, subagent steering operation, durable mailbox, **Let the provider create, resume, or deliver through an Agent handle.** Initial providers own only `prepareContinuable()` and its detached creation-spec distinction: whether a child begins fresh or with a parent prefix. The manager must call `ctx.agents.create()` through its private activation-owner scope so that scope is a structural owner of every handle. A persisted in-process Session already contains the initial prefix and generic reconstruction descriptor, while delivery belongs to the Agent inbox. Giving providers any later handle, `SubagentRun`, or message ownership would retain provider ownership with no shipped behavior to justify it. -**Make report delivery part of the base lifecycle.** Repeatable child-to-parent reporting is compatible with this lifecycle, but quiet versus next-step delivery, acknowledgement, durability, and retry behavior are independent product choices. The later report package remains optional and consumes an explicit child-setup hook, so continuable residency does not silently grant a return channel. +**Make child-authored reporting part of residency.** Adjacent-Agent messaging is separately composed through `send_message`; its authority and scheduling are independent of residency. Parent-result delivery belongs to the manager and includes a runtime settlement notice even when the child sends no message. **Treat `SessionHeader.parentSession` as live ownership.** Durable lineage does not prove that the recorded parent currently owns the child. Membership in the live parent's `ownedChildren` records the process-local relationship without changing the durable parent id. @@ -176,28 +178,28 @@ It adds no host-user continuation, subagent steering operation, durable mailbox, The implementation pins these behaviors: - A continuable child has at most one live Activation and one Agent inbox; the continuation manager has no Activation FIFO or queued Activation state. -- `SubagentProvider.prepareContinuable?()` returns only a detached `ContinuableCreateSpec`; configured continuable mode requires that capability, while `backgroundMode` remains an independent policy choice. -- The manager calls `ctx.agents.create()` through its private activation-owner scope, installs the returned `AgentHandle` and parent ownership, calls `Agent.followup(initialPrompt)`, and returns `{ childId, messageId }` when inbox acceptance yields the `MessageId`, without waiting for turn start or a Session-log write. +- `SubagentProvider.prepareContinuable?()` returns detached local creation inputs; `start()` establishes external execution. +- The manager calls `ctx.agents.create()` through its private activation-owner scope, installs the returned `AgentHandle` and parent ownership, calls `Agent.followup(initialPrompt)`, and returns a receipt containing `childId`, `messageId`, `result`, and `dispose()` when inbox acceptance yields the `MessageId`, without waiting for turn start or a Session-log write. - Every failure before initial-prompt inbox acceptance rejects without ids and rolls back any created handle, Activation, and parent `ownedChildren` membership through a closing transaction visible to concurrent delivery and drain; lifecycle publication failure emits no unmatched terminal edge. - Cold resume calls `ctx.agents.resume()` from the continuation manager and never dispatches through or requires the initial subagent provider; the descriptor retains the initial provider name after provider removal, while `SubagentProvider.resume?()` and `SubagentProviderResumeRequest` are absent. -- A continuable Activation directly owns `AgentHandle` and never creates, wraps, or retains `SubagentRun`; `SubagentProvider.start()` and `SubagentRun` remain one-shot-only, without `SubagentRun.steer?()`. -- `followup()` accepts only the exact live direct parent and rechecks that identity at the final no-await inbox-admission boundary after any materialization; durable message source fields cannot authorize delivery. -- Continuation messages always use `Agent.followup()` and share its inbox FIFO, including when the child already has an open turn. -- `ctx.subagents.followup()` and its `send_message` adapter return only the accepted `MessageId`; the continuation layer accepts no delivery target and defines no subagent-specific route result. -- Caller signals stop start and follow-up only before inbox acceptance, while host-scoped and manager-global teardown retain child-first cleanup; the [current-turn interrupt](../../../../packages/subagent/subagent/README.md) is the one public stop and does not enter teardown. -- This version exposes no subagent steering operation or current-turn controller state. +- A local Activation directly owns `AgentHandle`; an external Activation owns `SubagentRun`. +- `sendMessage()` accepts only an exact live adjacent Agent and rechecks authority at final inbox admission after materialization; recorded source fields cannot authorize delivery. +- Agent messages use Steer; human Queue prompts retain FIFO turn ordering. +- `ctx.subagents.sendMessage()` returns the accepted `MessageId` without exposing materialization routing. +- Caller signals stop unpublished work; activation receipts and host drains own child-first teardown. The [current-turn interrupt](../../../../packages/subagent/subagent/README.md) preserves pending work and residency. +- Human prompt delivery and current-turn interruption retain their separate authorization checks. - An idle Agent with live owned children yields a `waiting` Activation whose `AgentHandle` remains retained. - A `next-turn` delivered to `waiting` wakes the same Activation; delivery after completed disposal cold-resumes a new Activation. - Every continuation-managed parent Activation disposes only after all directly owned child Activations complete `AgentHandle` disposal; top-level Agents do not join the waiting graph. - Final Activation settlement awaits `ctx.sessions.flush(child.session)` with admission open, logs rejection without interpreting listener participation as durability proof, revalidates the final state under the child lock, then closes admission, disposes the child handle, and releases parent ownership so a flush failure cannot leak a `waiting` Activation. - Manager teardown closes admission globally; a host owning selected top-level Agents instead closes admission only below their exact identities until those roots leave the registry. Both track admitted materializations by exact ancestry, install one memoized disposal cutoff per selected visible Activation, propagate cancellation top-down, release handles child-first, await every selected branch despite individual failures, and only then dispose the corresponding top-level Agents or manager scope. -- The base lifecycle has no implicit report behavior; the optional report package contributes an explicit child-scoped tool through the setup hook. +- Parent delivery emits runtime settlement notices independently of child-authored messages. - Session logs reconstruct only messages that were actually written, with the source that supplied each message; inbox-accepted but unlogged messages have no restart guarantee. - No continuable-subagent path creates or depends on a Task, `JobId`, Task completion notice, Task cancellation, or intermediate result-bearing execution wrapper. -- Unit coverage pins the `startContinuable()` inbox-acceptance return boundary, complete rollback for each pre-acceptance and lifecycle-publication failure, global and parent-scoped drain quiescence for materialization caught between Agent publication and Activation registration, sibling-forest isolation, exact ancestry after an intermediate Agent leaves the registry, provider-independent cold resume, final exact-parent reauthorization after cold-resume materialization, caller-signal and teardown ownership on both sides of acceptance, and the absence of automatic replay for accepted-but-unlogged messages. -- Unit coverage pins the residency-only routing table, single-inbox ordering, `MessageId` correlation through inbox events, follow-up during an open turn, waiting wakeup, cold resume, ownership registration and release, child-first disposal, send-versus-dispose races, direct Agent turns, Session-only work, and maintenance accepted during the final-flush await, best-effort final flush with absent and failing listeners, and the absence of public subagent cancellation and steering. -- Report-package unit coverage separately pins child-only visibility, setup revocation, authority, delivery modes, stable message identity, and lifecycle races. -- A keyless assembled-app snapshot covers parent delegation and follow-up queueing, the absence of subagent steering and implicit report delivery, retained waiting `AgentHandle`, and child-first disposal. A separate report snapshot covers the optional explicit return channel. +- Unit coverage pins the `startActivation()` inbox-acceptance return boundary, complete rollback for each pre-acceptance and lifecycle-publication failure, global and parent-scoped drain quiescence for materialization caught between Agent publication and Activation registration, sibling-forest isolation, exact ancestry after an intermediate Agent leaves the registry, provider-independent cold resume, final exact-parent reauthorization after cold-resume materialization, caller-signal and teardown ownership on both sides of acceptance, and the absence of automatic replay for accepted-but-unlogged messages. +- Unit coverage pins the residency-only routing table, single-inbox ordering, `MessageId` correlation through inbox events, follow-up during an open turn, waiting wakeup, cold resume, ownership registration and release, child-first disposal, send-versus-dispose races, direct Agent turns, Session-only work, and maintenance accepted during the final-flush await, best-effort final flush with absent and failing listeners, and separately authorized human prompt delivery and interruption. +- Adjacent-Agent tests cover direct-parent authority, tool visibility, cold resume, and delivery during settlement. +- Keyless assembled-app snapshots cover delegation, adjacent-Agent messaging, runtime settlement notices, and child-first disposal. ### Accepted costs @@ -207,8 +209,8 @@ Retaining an Activation while descendants run consumes Agent resources proportio The process-local inbox and ownership graph do not coordinate two harness processes. Deployments allowing concurrent access to one persistence store still require a durable lease and mailbox protocol. -Without the optional report package, completing a child turn neither sends its content to nor wakes the historical parent. With the package, only an explicit `report` call sends selected content; quiet delivery does not wake the parent, while next-step delivery wakes it and joins its nearest step boundary. In every case the detailed child output remains in its durable Session. +Child-authored `send_message` calls and manager-authored settlement notices remain distinct in the parent log. The detailed local child transcript remains in its own durable Session. -Queueing every continuation message means a parent cannot correct an in-progress child turn immediately; the correction runs as the next turn. A later UI steering action may reduce that latency without changing follow-up ordering. +Steer delivery reaches the next available step boundary; it does not preempt an executing tool. Queue delivery retains later-turn ordering. A failed best-effort final flush is logged while the runtime ownership graph continues draining; the persisted child state may be missing or stale. Retry and repair require a separate recovery design. diff --git a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md index 29ae6142a8..ce543b4efa 100644 --- a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md +++ b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md @@ -4,7 +4,7 @@ Status: implemented [English](2026-07-28-continuable-subagent-conversations.md) | 中文 -本记录取代[可继续的后台 subagent](../../archived/feature/2026-07-21-continuable-background-subagents.md)中由 Task 支撑的继续执行管理器。它保留[将 subagent 控制合并到 subagent 服务](../../archived/simplification/2026-07-26-merge-subagent-control-service.md)确立的单一 `ctx.subagents` 服务,以及[以意图命名的 subagent 继续执行操作](../../archived/simplification/2026-07-27-intent-named-subagent-continuation-operations.md)确立的 `followup` 操作。 +本记录取代[可继续的后台 subagent](../../archived/feature/2026-07-21-continuable-background-subagents.md)中由 Task 支撑的继续执行管理器。它保留[将 subagent 控制合并到 subagent 服务](../../archived/simplification/2026-07-26-merge-subagent-control-service.md)确立的单一 `ctx.subagents` 服务。 ## 问题 @@ -18,6 +18,8 @@ parent Agent 还需要在不改变当前轮次的前提下,向同一个在线 ## 决策 +共享 activation 入口、外部执行所有权,以及调用方与父级之间的结果投递选择由[统一 subagent activation](../simplification/2026-09-17-unified-subagent-activations.zh.md) 决策拥有。本记录保留下述独立理由。 + 一个可继续 subagent 拥有一个持久化会话,并且至多拥有一个进程内激活: ```text @@ -28,25 +30,25 @@ persisted Session -> zero or more owned child Activations ``` -激活是重建 child Agent 的一次驻留周期。它可以执行多个 FIFO 轮次,并在等待后代时保持驻留。它不是请求、结果、取消或 Task 边界。 +激活是重建 child Agent 的一次驻留周期。它可以执行多个 FIFO 轮次,并在等待后代时保持驻留。其回执持有这次确切驻留的结果与取消操作;它不会为每条已接受的消息分配独立结果或 Task。 继续执行管理器负责激活准入、权限检查、在线所有权图、冷恢复和 child-first dispose。Agent loop 负责全部轮次排序与执行。没有任何可继续 subagent 拥有 Task、激活 FIFO 或 queued 激活状态。 ### 物化与公开操作 -具名 subagent 提供方只参与准备初始创建规格,此时 `spawn` 与 `fork` 有所区别。其可选的 `prepareContinuable(request): Promise` 方法就是可继续创建能力。返回的规格只包含与 Agent 实例分离且由提供方决定的创建输入,例如可选的 parent 历史种子;它不包含 Agent、`AgentHandle`、提示词投递、结果、dispose 或恢复操作。管理器会预留 child 身份,解析持久化描述符和通用 Agent 配置,通过私有 activation-owner 作用域调用 `ctx.agents.create()`,将返回的 `AgentHandle` 安装到激活中,建立适用的可继续 parent 所有权,然后调用 `Agent.followup(initialPrompt)`。inbox 接受消息后会产生一个 `MessageId`;`ctx.subagents.startContinuable()` 在此边界返回 `{ childId, messageId }`,不等待轮次开始,也不等待消息写入会话日志。 +具名 subagent 提供方只参与准备初始创建规格,此时 `spawn` 与 `fork` 有所区别。其可选的 `prepareContinuable(request): Promise` 方法就是可继续创建能力。返回的规格只包含与 Agent 实例分离且由提供方决定的创建输入,例如可选的 parent 历史种子;它不包含 Agent、`AgentHandle`、提示词投递、结果、dispose 或恢复操作。管理器会预留 child 身份,解析持久化描述符和通用 Agent 配置,通过私有 activation-owner 作用域调用 `ctx.agents.create()`,将返回的 `AgentHandle` 安装到激活中,建立适用的可继续 parent 所有权,然后调用 `Agent.followup(initialPrompt)`。inbox 接受消息后会产生一个 `MessageId`;`ctx.subagents.startActivation()` 在此边界返回包含 `childId`、`messageId`、`result` 与 `dispose()` 的回执,不等待轮次开始,也不等待消息写入会话日志。 inbox 接受消息前发生任何失败,操作都会在不返回任何 id 的情况下被拒绝。Agent 创建流程负责 handle 移交前的回滚;移交后,管理器会保留一个对并发投递和 drain 可见的关闭事务,dispose 已创建的 handle、移除激活并回滚 parent `ownedChildren` 中的任何成员关系,再拒绝操作。在驻留 start 事件发布前失败不会发布终止事件,start 发布后失败则通过正常 dispose 闭合生命周期配对。 -`backgroundMode: 'one-shot' | 'continuable'` 仍是部署策略。配置为 continuable 时要求存在 `prepareContinuable`;该方法是否存在会取代 `SubagentProvider.resume?()` 成为能力检查,而具备该能力的提供方仍可运行 one-shot 工作。 +提供方方法是否存在决定执行方式:`prepareContinuable()` 创建本地 activation,否则由 `start()` 启动外部执行。`delivery` 选择结果接收方,不改变本地生命周期。 -冷恢复不会通过 subagent 提供方分发。继续执行管理器会归并通用的进程内描述符,通过同一个 activation-owner 作用域调用 `ctx.agents.resume()`,安装返回的 `AgentHandle`,并提交等待中的 `next-turn`。`SubagentProvider.resume?()` 和 `SubagentProviderResumeRequest` 均不存在。初始提供方注销后,描述符仍保留其名称;该名称不赋予恢复能力,也不要求后续驻留时该提供方存在。远程提供方需要单独设计。 +冷恢复不会通过 subagent 提供方分发。继续执行管理器会归并通用的进程内描述符,通过同一个 activation-owner 作用域调用 `ctx.agents.resume()`,安装返回的 `AgentHandle`,并提交等待中的 `next-turn`。`SubagentProvider.resume?()` 和 `SubagentProviderResumeRequest` 均不存在。初始提供方注销后,描述符仍保留其名称;该名称不赋予恢复能力,也不要求后续驻留时该提供方存在。外部执行不支持冷恢复。 -`SubagentProvider.start()` 和 `SubagentRun` 只保留在不变的 one-shot 路径上。可继续激活直接持有自身的 `AgentHandle`,绝不创建、包装或保留 `SubagentRun`;因此,`SubagentRun.steer?()` 不存在。 +`SubagentProvider.start()` 与 `SubagentRun` 属于外部执行。本地 Activation 直接持有其 `AgentHandle`;两类执行共享管理器持有的容量、结算与 dispose。 -`ctx.subagents.sendMessage(sender, targetId, content, { signal })` 是唯一由模型编写的继续执行消息操作。确切在线 sender 授权向其直接 parent 或直接可继续 child 投递;冷恢复会在重建前检查直接 child 权限,每条路径还会在最终无 await 的 inbox 准入区间再次检查,因此在物化期间被注销或替换的 Agent 无法授权投递。服务从该 sender 推导持久化 `agent-message` 来源信息。面向模型的 `send_message` 工具只保留 `agent_id` 和 `message`,并使用固定 Steer 调度。start 与 send 都返回已接受的 `MessageId`,两者都不报告管理器如何物化 Activation。 +`ctx.subagents.sendMessage(sender, targetId, content, { signal })` 是唯一由模型编写的继续执行消息操作。确切在线 sender 授权向其直接 parent 或直接可继续 child 投递;冷恢复会在重建前检查直接 child 权限,每条路径还会在最终无 await 的 inbox 准入区间再次检查,因此在物化期间被注销或替换的 Agent 无法授权投递。服务从该 sender 推导持久化 `agent-message` 来源信息。面向模型的 `send_message` 工具只保留 `agent_id` 和 `message`,并使用固定 Steer 调度。本地 start 返回包含已接受 `MessageId` 的 activation 回执;send 直接返回 `MessageId`。两者都不暴露物化过程是创建还是恢复 Agent。 -对于 start 和 follow-up,调用方 signal 只在 inbox 接受消息前持有查找、物化和准入。操作返回 `MessageId` 后,管理器会独立持有该激活;调用方之后的取消不会取消已接受的轮次,也不会 dispose child。 +对于 start 和 follow-up,调用方 signal 只在 inbox 接受消息前持有查找、物化和准入。inbox 接受消息后,管理器会独立持有该激活;调用方之后的取消不会取消已接受的轮次,也不会 dispose child。 ### 持久化会话与在线激活 @@ -54,7 +56,7 @@ inbox 接受消息前发生任何失败,操作都会在不返回任何 id 的 空闲的历史会话没有 `AgentHandle`。第一条通过鉴权的 `next-turn` 投递会根据持久化会话恢复激活,并将消息提交到其 inbox。冷恢复使用经过身份认证的确切在线 parent Agent 执行鉴权;当该 parent 有激活时,还使用它建立所有权,但绝不使用 parent 执行重建。 -激活会直接持有已发布的 `AgentHandle` 直至结算,而管理器的私有 activation-owner 作用域则是其 Cordis 结构化所有者。可继续 subagent 路径不创建任何中间的带结果执行包装层,包括 `SubagentRun`;一次性委派保持不变,且不属于该生命周期。远程提供方不在此处的范围内,引入时需要单独的激活所有权约定。激活 dispose 后,历史会话不消耗运行时内存。 +本地 Activation 直接持有已发布的 `AgentHandle` 直至结算;私有 activation-owner 作用域是其 Cordis 结构化所有者。它不创建中间 `SubagentRun`。统一管理器也通过提供方 handle 持有外部执行。dispose 后历史 Session 不保留 Agent 资源。 ### 激活生命周期 @@ -81,21 +83,21 @@ no Activation 管理器根据 Agent 是否完全停稳、Inbox 的待处理状态以及所持 child 集合派生这些状态,而不是维护第二套执行状态机。在 `running` 时投递的 `next-turn` 会进入 Agent inbox。在 `waiting` 时到达的唤醒投递会唤醒同一个 Agent,并使激活回到 `running`。在 dispose 完成后投递消息则会冷恢复新激活。 -管理器会针对每个持久化 child,将 manager 所有的投递、child 释放和 dispose 线性化。私有 `SubagentInbox` 会把 Queue 与 Steer 委托给 Agent inbox,并持有 Activation 既有的关闭事务。如果 manager 投递与最终 dispose 发生竞争,只有一方能越过这条准入截止点:投递要么进入仍在线的 Agent inbox,要么观察到正在关闭,并遵循该操作特有的拒绝或冷恢复路径。直接操作 Agent 的工作不经过这层包装,因此自然结算会通过短暂的 maintenance 占用,在最终 flush 与最终 dispose 决策之前验证 idle 阶段,并在 child lock 内重新验证 Session 序号、Inbox 待处理状态、wake generation 与 owned-child set。仍然活跃或改变 Session、Inbox 或所有权状态的已接受工作会让本次结算尝试失效,而不会被它取消;完全在 flush 期间开始并结束的 maintenance 已在截止点前完成。 +管理器会针对每个持久化 child,将 manager 所有的投递、child 释放和 dispose 线性化。管理器的私有投递将 Queue 与 Steer 委托给 Agent inbox,并检查 Activation 的 closing promise。如果 manager 投递与最终 dispose 发生竞争,只有一方能越过这条准入截止点:投递要么进入仍在线的 Agent inbox,要么观察到正在关闭,并遵循该操作特有的拒绝或冷恢复路径。直接操作 Agent 的工作不经过这层包装,因此自然结算会通过短暂的 maintenance 占用,在最终 flush 与最终 dispose 决策之前验证 idle 阶段,并在 child lock 内重新验证 Session 序号、Inbox 待处理状态、wake generation 与 owned-child set。仍然活跃或改变 Session、Inbox 或所有权状态的已接受工作会让本次结算尝试失效,而不会被它取消;完全在 flush 期间开始并结束的 maintenance 已在截止点前完成。 ### 一个 inbox 与 follow-up 投递 -Agent inbox 是唯一队列。每条继续执行消息都使用 `Agent.followup()`,并成为一个 FIFO 轮次;继续执行管理器和宿主都不维护另一条消息队列。每个待处理 Inbox occurrence 都会让当前激活保持在线,直到它被 claim 或 discard。这条保守规则也会保留注入 context:完全停稳后仍存在的静默注入可以让 Activation 及其在线祖先继续驻留,直到唤醒投递将其 claim、queue 变更将其移除,或 manager teardown dispose 整棵树。 +Agent inbox 是唯一队列。Agent 消息使用 Steer;人类提示词选择 Queue 或 Steer。每个待处理 Inbox 项(包括静默注入的上下文)都会让当前 Activation 保持在线,直到被认领或丢弃。管理器与宿主都不维护第二条消息队列。 路由只取决于激活的驻留状态: -| 激活状态 | `followup` | +| 激活状态 | 唤醒消息投递 | |---|---| | `running` | 在同一激活中排队 | | `waiting` | 唤醒同一激活 | | 无激活 | 冷恢复新激活 | -继续执行层不定义单独的投递路由结果。成功投递 `ctx.subagents.followup()` 或 `send_message` 时会返回已接受的 `MessageId`,投递失败则会抛出异常。现有的 `agent/inbox/enqueue`、`agent/inbox/dequeue` 和 `agent/inbox/discard` 事件仍用于观测消息生命周期;适配器可以呈现通用的接受确认,但不暴露 `started`、`queued`、`resumed` 或其他 subagent 专属路由词汇。 +成功的 `sendMessage()` 返回已接受的 `MessageId`;投递失败则抛错。Inbox 事件仍是消息生命周期的观察来源。服务不暴露独立的 started/queued/resumed 路由结果。 ### child 所有权 @@ -107,9 +109,9 @@ Agent inbox 是唯一队列。每条继续执行消息都使用 `Agent.followup( 系统会一直保留所有权,直至 child 激活完成 dispose。后续改进可以更早释放限定到请求的 lease,但这需要精确关联轮次完成,而本 Task-free 设计特意不增加该机制。 -顶层拆卸由宿主负责,而不表示为另一次激活。管理器卸载会调用其内部的管理器全局 drain,同步关闭准入,等待每个已获准的物化过程完成发布或回滚,停止稳定的在线森林,并按 child-first 顺序释放。拥有选定顶层 Agent 的宿主使用 `drainContinuableDescendants(parents)`:确切的 Agent 身份只关闭这些根之下的准入,直到每个身份离开注册表,而无关森林和管理器全局准入保持在线;管理器会在第一次 await 之前停止其可见后代,只等待这些根之下已获准的物化过程,并且只释放选定分支。每个已物化的 start 和在线投递都会在与 inbox 提交相同的同步区间内重新检查调用方取消、适用的 draining 作用域、Activation dispose 和确切的 parent 权限,因此只要拆卸或 parent 替换先于接受发生,就会阻止向正在关闭的 handle 投递。只有适用的 drain 结算后,宿主才能 dispose 自己的顶层 Agent;只有管理器全局 drain 会先于管理器作用域 dispose。 +顶层拆卸由宿主负责,而不表示为另一次激活。管理器卸载会调用其内部的管理器全局 drain,同步关闭准入,等待每个已获准的物化过程完成发布或回滚,停止稳定的在线森林,并按 child-first 顺序释放。拥有选定顶层 Agent 的宿主使用 `drainDescendants(parents)`:确切的 Agent 身份只关闭这些根之下的准入,直到每个身份离开注册表,而无关森林和管理器全局准入保持在线;管理器会在第一次 await 之前停止其可见后代,只等待这些根之下已获准的物化过程,并且只释放选定分支。每个已物化的 start 和在线投递都会在与 inbox 提交相同的同步区间内重新检查调用方取消、适用的 draining 作用域、Activation dispose 和确切的 parent 权限,因此只要拆卸或 parent 替换先于接受发生,就会阻止向正在关闭的 handle 投递。只有适用的 drain 结算后,宿主才能 dispose 自己的顶层 Agent;只有管理器全局 drain 会先于管理器作用域 dispose。 -activation-owner 作用域之所以存在,是因为普通 Cordis owner effect 按注册逆序撤销,无法表达动态 child 图。管理器初始化时先注册私有作用域的结构化 disposer,再注册自身的 drain disposer,使逆序撤销先执行 drain、再释放该作用域;如果只在与后续 Agent handle 相同的作用域上注册 cleanup effect,结构化 handle dispose 就可能绕过 child-first 顺序。每个物化过程都会在启动内部事务前注册其屏障参与项,并对其确切的在线祖先建立快照,然后保持跟踪,直到安装 Activation 或完全回滚。Activation 会保留其在这组祖先中的弱成员关系,因此中间 Agent 即使离开注册表,也不会让仍在线的后代脱离宿主根节点的可见范围。其私有 `SubagentInbox` 会在取消或递归回调前安装一个记忆化的 closing promise,使限定作用域的宿主关闭、全局管理器卸载、child 释放和正常结算能够汇合,而不会重复释放。取消会在等待缓慢的后代清理之前自顶向下传播;handle 释放仍是 child-first。同级分支独立 drain;系统会记录单次 dispose 失败,但仍会尝试其余选中 handle,聚合 drain 则在所有选中分支结算后报告失败。这次进程内拆卸不会销毁持久化 child 会话。 +activation-owner 作用域之所以存在,是因为普通 Cordis owner effect 按注册逆序撤销,无法表达动态 child 图。管理器初始化时先注册私有作用域的结构化 disposer,再注册自身的 drain disposer,使逆序撤销先执行 drain、再释放该作用域;如果只在与后续 Agent handle 相同的作用域上注册 cleanup effect,结构化 handle dispose 就可能绕过 child-first 顺序。每个物化过程都会在启动内部事务前注册其屏障参与项,并对其确切的在线祖先建立快照,然后保持跟踪,直到安装 Activation 或完全回滚。Activation 会保留其在这组祖先中的弱成员关系,因此中间 Agent 即使离开注册表,也不会让仍在线的后代脱离宿主根节点的可见范围。管理器会在取消或递归回调前安装一个记忆化的 closing promise,使限定作用域的宿主关闭、全局管理器卸载、child 释放和正常结算能够汇合,而不会重复释放。取消会在等待缓慢的后代清理之前自顶向下传播;handle 释放仍是 child-first。同级分支独立 drain;系统会记录单次 dispose 失败,但仍会尝试其余选中 handle,聚合 drain 则在所有选中分支结算后报告失败。这次进程内拆卸不会销毁持久化 child 会话。 ### 相邻 Agent 消息 @@ -129,7 +131,7 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect ### 持久性、dispose 与恢复 -没有 Task 后,系统不再提供 `job_output`、`job_kill`、Task 状态或逐消息结果 promise。调用方 signal 只能在 inbox 接受消息前中止 start 或 follow-up。消息被接受后,parent 不能通过 `ctx.subagents` 取消已接受的消息或 dispose 激活;唯一的公开停止操作是后来的[当前轮次中断](../../../../packages/subagent/subagent/README.zh.md),它以 `keepInbox` 取消在线目标的当前轮次,驻留、待处理工作与后代均保持不变。 +activation 回执无需 Jobs 即可暴露 `result` 与 `dispose()`。调用方 signal 持有未发布工作;回执 dispose 会取消确切的 activation,并等待后代启动回滚与 child-first 释放。[当前轮次中断](../../../../packages/subagent/subagent/README.zh.md)仅以 `keepInbox` 取消在线目标轮次,保留驻留、待处理工作与后代。 宿主和管理器拆卸仍是生命周期停止路径。管理器卸载会全局应用它;宿主只会在自己确切拥有的顶层 Agent 之下应用它。两种形式都会关闭适用的准入作用域,停止选中的可见 Activation,等待该作用域中已获准的物化过程,按 child-first 顺序释放,并保留持久化 Session。 @@ -141,9 +143,9 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect ### 范围 -本版本覆盖可继续的进程内 child,一次性委派保持不变。远程提供方必须具备单独的激活 handle,以及等价的认证控制与 child-first 完全停稳约定,才能支持同样的行为。 +本地可继续控制与外部执行共享 activation 所有权。只有本地 child 支持后续消息与冷恢复。 -它不新增 host-user 继续执行、subagent steering 操作、持久化邮箱、跨进程 lease、中断 inbox 工作的自动回放、团队权限、工作流权限、公开驻留查询、以及运行时缓存;后来的[当前轮次中断](../../../../packages/subagent/subagent/README.zh.md)在此生命周期之上补充了唯一的公开停止操作。现有委派深度策略保持不变。可选的 child 到 parent 报告是后续消费该生命周期的功能,不属于基础可继续能力。 +人工 Queue/Steer 投递和[当前轮次中断](../../../../packages/subagent/subagent/README.zh.md)使用此生命周期;回执释放会停止这次确切 activation 及其所属后代。持久化邮箱、跨进程 lease、中断 inbox 工作的自动回放、团队权限、工作流权限、公开驻留查询和运行时缓存仍不属于此能力。委派深度策略和相邻 Agent 消息授权各自由其原有组件管理。 ## 曾考虑的替代方案 @@ -155,7 +157,7 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect **让提供方通过 Agent handle 创建、恢复 child 或投递消息。** 初始提供方只持有 `prepareContinuable()` 及其分离式创建规格这一项差异:child 是全新启动,还是带有 parent 前缀。管理器必须通过私有 activation-owner 作用域自行调用 `ctx.agents.create()`,使该作用域成为每个 handle 的结构化所有者。持久化的进程内会话已经包含初始前缀及通用重建描述符,消息投递则属于 Agent inbox。让提供方持有任何后续 handle、`SubagentRun` 或消息所有权,会让提供方保留所有权,却没有已发布行为需要它。 -**将报告投递纳入基础生命周期。** 可重复的 child 到 parent 报告与该生命周期兼容,但静默投递还是 next-step 投递、确认、持久性和重试行为都是独立的产品决策。后续的 report 包保持可选,并消费一个显式的 child 设置钩子,因此可继续驻留不会默认授予返回通道。 +**把 child 编写的报告纳入驻留。** 相邻 Agent 消息通过 `send_message` 独立组合;其权限与调度独立于驻留。父级结果投递由管理器持有,即使 child 没有发消息,也会包含运行时结算通知。 **将 `SessionHeader.parentSession` 视为在线所有权。** 持久化谱系不能证明已记录的 parent 当前持有 child。在线 parent 的 `ownedChildren` 成员关系会记录进程内关系,而不改变持久化 parent id。 @@ -176,28 +178,28 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect 本实现固定了以下行为: - 可继续 child 至多拥有一个在线激活和一个 Agent inbox;继续执行管理器没有激活 FIFO 或 queued 激活状态。 -- `SubagentProvider.prepareContinuable?()` 只返回分离式 `ContinuableCreateSpec`;配置为 continuable 时要求具备该能力,而 `backgroundMode` 仍是独立的策略选择。 -- 管理器通过私有 activation-owner 作用域调用 `ctx.agents.create()`,安装返回的 `AgentHandle` 并建立 parent 所有权,调用 `Agent.followup(initialPrompt)`,然后在 inbox 接受消息并产生 `MessageId` 时返回 `{ childId, messageId }`,而不等待轮次开始或消息写入会话日志。 +- `SubagentProvider.prepareContinuable?()` 返回独立的本地创建输入;`start()` 建立外部执行。 +- 管理器通过私有 activation-owner 作用域调用 `ctx.agents.create()`,安装返回的 `AgentHandle` 并建立 parent 所有权,调用 `Agent.followup(initialPrompt)`,然后在 inbox 接受消息并产生 `MessageId` 时返回包含 `childId`、`messageId`、`result` 与 `dispose()` 的回执,而不等待轮次开始或消息写入会话日志。 - 初始提示词被 inbox 接受前的每条失败路径都会导致操作被拒绝且不返回 id,并通过一个对并发投递和 drain 可见的关闭事务回滚已创建的任何 handle、激活和 parent `ownedChildren` 成员关系;生命周期发布失败不会产生无配对的终止事件。 - 冷恢复由继续执行管理器调用 `ctx.agents.resume()`,绝不通过或依赖初始 subagent 提供方;提供方移除后,描述符仍保留初始提供方名称,且 `SubagentProvider.resume?()` 和 `SubagentProviderResumeRequest` 均不存在。 -- 可继续激活直接持有 `AgentHandle`,绝不创建、包装或保留 `SubagentRun`;`SubagentProvider.start()` 和 `SubagentRun` 只用于 one-shot,且没有 `SubagentRun.steer?()`。 -- `followup()` 只接受确切的在线直接 parent,并在任何物化之后的最终无 await 的 inbox 准入边界再次检查该身份;持久化消息来源信息不能授权投递。 -- 继续执行消息始终使用 `Agent.followup()` 并共享其 inbox FIFO,包括 child 已有开放轮次的情况。 -- `ctx.subagents.followup()` 及其 `send_message` 适配器只返回已接受的 `MessageId`;继续执行层不接受投递 target,也不定义 subagent 专属路由结果。 -- 调用方 signal 只能在 inbox 接受消息前停止 start 和 follow-up,限定到宿主的拆卸与管理器全局拆卸则保留 child-first 清理;[当前轮次中断](../../../../packages/subagent/subagent/README.zh.md)是唯一的公开停止操作,且不进入拆卸流程。 -- 本版本不暴露 subagent steering 操作或当前轮次控制方状态。 +- 本地 Activation 直接持有 `AgentHandle`;外部 Activation 持有 `SubagentRun`。 +- `sendMessage()` 只接受确切在线的相邻 Agent,并在物化后的最终 inbox 准入处重新检查权限;记录的来源字段不能授权投递。 +- Agent 消息使用 Steer;人类 Queue 提示词保留 FIFO 轮次顺序。 +- `ctx.subagents.sendMessage()` 返回已接受的 `MessageId`,不暴露物化路由。 +- 调用方 signal 停止未发布工作;activation 回执与宿主 drain 持有 child-first 拆卸。[当前轮次中断](../../../../packages/subagent/subagent/README.zh.md)保留待处理工作与驻留。 +- 人类提示词投递与当前轮次中断保留各自的权限检查。 - 带有在线所持 child 的空闲 Agent 会产生 `waiting` 激活,其 `AgentHandle` 继续保留。 - 向 `waiting` 投递 `next-turn` 会唤醒同一个激活;完成 dispose 后投递消息会冷恢复新激活。 - 每个由继续执行管理器管理的 parent 激活只会在直接持有的所有 child 激活完成 `AgentHandle` dispose 后进行 dispose;顶层 Agent 不加入等待图。 - Activation 最终结算会在准入开放时等待 `ctx.sessions.flush(child.session)`,将其作为 best-effort 屏障;它会记录 rejection,但不会把 listener 参与解释为持久性证明,随后在 child lock 内重新验证最终状态,再关闭准入、dispose child handle 并释放 parent 所有权,使 flush 失败不会泄漏 `waiting` Activation。 - 管理器拆卸会全局关闭准入;拥有选定顶层 Agent 的宿主则只关闭这些确切身份之下的准入,直到这些根离开注册表。两者都会按确切祖先关系跟踪已获准的物化过程,为每个选中的可见 Activation 安装一个记忆化 dispose 截止点,自顶向下传播取消,按 child-first 顺序释放 handle,即使个别分支失败也会等待所有选中分支,之后才 dispose 对应的顶层 Agent 或管理器作用域。 -- 基础生命周期不暴露隐式报告行为;可选的 report 包通过 setup 钩子贡献一个显式的 child 作用域工具。 +- parent 投递独立于 child 编写的消息发出运行时结算通知。 - 会话日志只会重建实际写入的消息,并保留每条消息的提供来源;已被 inbox 接受但未写入日志的消息没有重启保证。 - 可继续 subagent 路径不创建或依赖 Task、`JobId`、Task 完成通知、Task 取消或中间的带结果执行包装层。 -- 单元覆盖固定 `startContinuable()` 在 inbox 接受消息时的返回边界、每条接受前和生命周期发布失败路径的完整回滚、全局和限定到 parent 作用域的 drain 都会等待夹在 Agent 发布与 Activation 注册之间的物化过程完全停稳、同级森林隔离、中间 Agent 离开注册表后的确切祖先关系、不依赖提供方的冷恢复、冷恢复物化后的最终确切 parent 再授权、接受前后两个阶段的调用方 signal 与拆卸所有权,以及已接受但未写入日志的消息不会自动回放。 -- 单元覆盖固定仅由驻留状态决定的路由表、单 inbox 顺序、通过 inbox 事件关联 `MessageId`、在开放轮次期间 follow-up、等待唤醒、冷恢复、所有权注册与释放、child-first dispose、发送与 dispose 的竞争、在最终 flush await 期间接受的直接 Agent 轮次、仅修改 Session 的工作与 maintenance、没有 listener 和 listener 失败时的 best-effort 最终 flush,以及不存在公开 subagent 取消和 steering。 -- report 包的单元覆盖会分别固定仅 child 可见性、setup 撤销、权限、投递模式、稳定消息身份和生命周期竞争。 -- 一项无密钥整套应用快照覆盖 parent 委派和 follow-up 排队、不存在 subagent steering 和隐式 report 投递、保留 waiting 中的 `AgentHandle` 以及 child-first dispose。另一项 report 快照覆盖可选的显式返回通道。 +- 单元覆盖固定 `startActivation()` 在 inbox 接受消息时的返回边界、每条接受前和生命周期发布失败路径的完整回滚、全局和限定到 parent 作用域的 drain 都会等待夹在 Agent 发布与 Activation 注册之间的物化过程完全停稳、同级森林隔离、中间 Agent 离开注册表后的确切祖先关系、不依赖提供方的冷恢复、冷恢复物化后的最终确切 parent 再授权、接受前后两个阶段的调用方 signal 与拆卸所有权,以及已接受但未写入日志的消息不会自动回放。 +- 单元覆盖固定仅由驻留状态决定的路由表、单 inbox 顺序、通过 inbox 事件关联 `MessageId`、在开放轮次期间 follow-up、等待唤醒、冷恢复、所有权注册与释放、child-first dispose、发送与 dispose 的竞争、在最终 flush await 期间接受的直接 Agent 轮次、仅修改 Session 的工作与 maintenance、没有 listener 和 listener 失败时的 best-effort 最终 flush,以及单独鉴权的人类提示词投递与中断。 +- 相邻 Agent 测试覆盖直接 parent 权限、工具可见性、冷恢复与结算期间投递。 +- keyless 组装应用快照覆盖委派、相邻 Agent 消息、运行时结算通知与 child-first dispose。 ### 已接受的代价 @@ -207,8 +209,8 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect 进程内 inbox 和所有权图无法协调两个 harness 进程。允许多个进程并发访问同一持久化存储的部署,仍需要持久化 lease 和邮箱协议。 -未安装可选 report 包时,完成 child 轮次既不会把内容发送给历史 parent,也不会唤醒它。安装后,只有显式调用 `report` 才会发送选中内容;静默投递不唤醒 parent,next-step 投递则会唤醒它并加入最近的 step 边界。无论如何,child 的详细输出都会保留在其持久化会话中。 +child 编写的 `send_message` 调用与管理器编写的结算通知在父级日志中保持区分。本地 child 的完整文本记录保留在其自身的持久化 Session 中。 -将每条继续执行消息排队,意味着 parent 无法立即纠正正在进行的 child 轮次;纠正操作会在下一个轮次执行。后续 UI steering 操作可以缩短该延迟,而不改变 follow-up 排序。 +Steer 投递抵达下一个可用 step 边界,不会抢占正在执行的工具。Queue 投递保留后续轮次顺序。 best-effort 最终 flush 失败时会记录日志,同时运行时所有权图继续 drain;持久化 child 状态可能缺失或陈旧。重试与修复需要单独的恢复设计。 diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml b/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml index 1504305ffa..a38c0852ba 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml @@ -24,8 +24,8 @@ en: ab2ad55021eb34e0 zh: 2d1996df7a69201d /agent-note-durable-agent-teams-over-continuable-children/shared-checkout-boundary: - en: 3969de3b44e412c8 - zh: 12be5854816cb808 + en: 34920da68b5763e0 + zh: 7604392594e4b8a8 /agent-note-durable-agent-teams-over-continuable-children/web-projection: en: e6348054f024725b zh: 8740636e99940308 diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.md b/.agents/notes/implemented/feature/2026-08-05-agent-teams.md index 3f8a89ffd3..74556d041f 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.md +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.md @@ -50,9 +50,9 @@ Shared tasks are complete snapshots with Team-local ids and monotonic revisions. ## Shared checkout boundary -All members use the same cwd and observe writes immediately. The policy tells members to partition tasks, record advisory write scopes, order dependent work, and let the Lead inspect the final diff and run tests. A filesystem stale-version rejection requires rereading and rebasing the intended change. No equivalent guarantee is claimed for Bash, formatters, code generation, or direct external writes. +Members start in the Lead’s current directory and observe shared filesystem writes immediately. Later directory changes stay local to each Session. The policy tells members to partition tasks, record advisory write scopes, order dependent work, and let the Lead inspect the final diff and run tests. A filesystem stale-version rejection requires rereading and rebasing the intended change. No equivalent guarantee is claimed for Bash, formatters, code generation, or direct external writes. -Worktree isolation is not a harness runtime behavior. A deployment or prompt may arrange separate worktrees, but the Team domain does not infer branches, merge changes, or silently change cwd. This preserves the existing same-world subagent and sandbox contracts. +The Team domain does not infer branches, create worktrees, merge changes, or silently change cwd. A deployment or caller can use [explicit worktree creation](2026-09-13-explicit-worktree-creation.md) separately. This preserves the existing same-world subagent and sandbox contracts. ## Web projection diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md b/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md index f28a654601..d7c2bd7d7e 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md @@ -50,9 +50,9 @@ Peer 通讯使用 Lead 日志 mailbox。投递前先追加并 flush `team/messag ## Shared checkout boundary -所有 member 使用相同 cwd,并立即观察写入。策略要求 member 切分任务、记录提示性 write scope、为有序工作添加依赖,并由 Lead 检查最终 diff 和运行测试。文件系统 stale-version 拒绝后必须重新读取并 rebase 修改意图。Bash、formatter、codegen 与直接外部写入不具备等价保证。 +所有 member 创建时使用 Lead 的当前目录,并立即观察共享文件系统写入。后续目录变更仅影响各自 Session。策略要求 member 切分任务、记录提示性 write scope、为有序工作添加依赖,并由 Lead 检查最终 diff 和运行测试。文件系统 stale-version 拒绝后必须重新读取并 rebase 修改意图。Bash、formatter、codegen 与直接外部写入不具备等价保证。 -Worktree isolation 不是 harness runtime 行为。deployment 或 prompt 可以安排独立 worktree,但 Team 领域不会推断 branch、merge 变更或静默改变 cwd。这样保留既有 same-world subagent 与 sandbox 契约。 +Team 领域不会推断 branch、创建 worktree、merge 变更或静默改变 cwd。deployment 或调用方可以另外使用[显式工作树创建](2026-09-13-explicit-worktree-creation.zh.md)。这样保留既有 same-world subagent 与 sandbox 契约。 ## Web projection diff --git a/.agents/notes/implemented/feature/2026-09-13-explicit-worktree-creation.i18n.yaml b/.agents/notes/implemented/feature/2026-09-13-explicit-worktree-creation.i18n.yaml new file mode 100644 index 0000000000..6f3cb5f94d --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-13-explicit-worktree-creation.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-09-13-explicit-worktree-creation.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-13-explicit-worktree-creation.md +/agent-note-explicit-worktree-creation: + en: 68d9e5eab7671458 + zh: fc889567986f2bbe +/agent-note-explicit-worktree-creation/problem: + en: 12ab239ab0c58e31 + zh: c32874442ce66041 +/agent-note-explicit-worktree-creation/decision: + en: e3be190201a36710 + zh: 9ef0036b9b95377e +/agent-note-explicit-worktree-creation/alternatives-considered: + en: 72d42e892a37b94c + zh: 92a14a51c6a89c53 +/agent-note-explicit-worktree-creation/consequences: + en: 083851db27cc4846 + zh: b3db48d41356c9cf diff --git a/.agents/notes/implemented/feature/2026-09-13-explicit-worktree-creation.md b/.agents/notes/implemented/feature/2026-09-13-explicit-worktree-creation.md new file mode 100644 index 0000000000..9b84d0c9fd --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-13-explicit-worktree-creation.md @@ -0,0 +1,33 @@ +# Agent Note: Explicit worktree creation + +Status: implemented + +English | [中文](2026-09-13-explicit-worktree-creation.zh.md) + +## Problem + +A Session that needs an isolated checkout otherwise has to coordinate Git commands with its own working directory. Changing a shell process directory does not update the Session's later tools or recorded context, and a linked worktree's Git administration directory can lie outside the Session's existing write grant. + +## Decision + +The experimental [worktree runtime](../../../../packages/experimental/worktree/README.md) creates a new named branch and checkout from a pinned local commit, then enters its canonical directory through the shared working-directory service. The [tool](../../../../packages/experimental/tool-worktree/README.md) exposes `create_worktree({ name?, from? })`. These packages use ordinary explicit composition, with no additional runtime feature flag. + +The source repository comes from the calling Session's current directory. Creation does not fetch or copy uncommitted files. The default pool is `.agents/worktrees` under the current checkout’s top-level directory, including when the caller is in a linked checkout. The process that creates a pool writes its self-ignoring `.gitignore`; existing directories and ignore files are preserved. Names must be new branches and unused checkout paths. + +Git and directory creation use the mounted subprocess and sandbox providers without widening the calling Session's policy. Both destination and shared Git metadata writes must already be permitted. Successful creation changes only the current working directory; the Session identity and original sandbox root remain owned by their existing services. + +Leaving uses the shared working-directory operation and retains the checkout and branch. Failed setup can retain partial artifacts and reports their location. The service does not own a checkout registry, cleanup schedule, branch merge policy, or task association database. + +## Alternatives considered + +**A separate worktree-exit tool.** Leaving is the existing working-directory operation; another exit operation would needlessly couple directory changes to Git ownership or deletion. + +**Copy the dirty checkout into each new worktree.** A pinned local commit gives the caller an explicit baseline without mixing staged, unstaged, untracked, and ignored state or inventing conflict rules. + +**Automatic subagent isolation.** [Agent Teams](2026-08-05-agent-teams.md) keeps its existing explicit coordination model. Creating a checkout is available as an operation rather than an implicit consequence of delegation. + +**Expand write grants during creation.** A convenience operation cannot acquire authority over a shared Git administration directory merely because the destination is writable. + +## Consequences + +The caller can continue one Session in a new checkout with a reconstructable directory change. Retention avoids deleting work on exit or a failed final publication, but callers must manage unused checkouts and branches themselves. Git and Node must be available in the filesystem provider's execution world. Submodule setup and dependency installation remain separate operations. diff --git a/.agents/notes/implemented/feature/2026-09-13-explicit-worktree-creation.zh.md b/.agents/notes/implemented/feature/2026-09-13-explicit-worktree-creation.zh.md new file mode 100644 index 0000000000..f93a4a2cd7 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-13-explicit-worktree-creation.zh.md @@ -0,0 +1,33 @@ +# Agent Note: 显式创建工作树 + +Status: implemented + +[English](2026-09-13-explicit-worktree-creation.md) | 中文 + +## Problem + +需要隔离检出目录的 Session 原本必须自行协调 Git 命令与工作目录。改变 shell 进程目录不会更新 Session 后续工具或已记录的上下文,而且链接工作树的 Git 管理目录可能位于 Session 既有写权限范围之外。 + +## Decision + +实验性[工作树运行时](../../../../packages/experimental/worktree/README.zh.md)从固定的本地提交创建新的具名分支与检出目录,然后通过共享工作目录服务进入其规范路径。[工具](../../../../packages/experimental/tool-worktree/README.zh.md)公开 `create_worktree({ name?, from? })`。这些包使用普通的显式组合,不添加额外运行时功能开关。 + +源仓库由调用 Session 的当前目录决定。创建操作不会获取远端提交或复制未提交文件。默认父目录是当前检出目录顶层路径下的 `.agents/worktrees`,调用方位于链接检出目录中时也一样。创建该目录的进程写入忽略自身的 `.gitignore`;已有目录与忽略文件保持不变。名称必须对应新分支与尚未使用的检出路径。 + +Git 与目录创建使用挂载的子进程及沙箱提供方,不扩大调用 Session 的策略。目标目录与共享 Git 元数据必须均已允许写入。成功创建只改变当前工作目录;Session 身份与原始沙箱根目录仍由已有服务拥有。 + +离开使用共享工作目录操作,保留检出目录与分支。初始化失败可能留下部分产物,并报告其位置。该服务不拥有工作树注册表、清理计划、分支合并策略或任务关联数据库。 + +## Alternatives considered + +**单独提供工作树退出工具。** 离开就是已有工作目录操作;另一个退出操作会无谓地将目录切换与 Git 所有权或删除耦合。 + +**将未提交状态复制到每个新工作树。** 固定本地提交为调用方提供显式基准,无需混合暂存、未暂存、未跟踪和被忽略的状态,也无需制定冲突规则。 + +**自动隔离子代理。** [Agent Teams](2026-08-05-agent-teams.zh.md)保留已有的显式协调模型。创建检出目录是一项可调用操作,而不是委派的隐含结果。 + +**创建时扩大写权限。** 便捷操作不能仅因目标目录可写,就获得共享 Git 管理目录的权限。 + +## Consequences + +调用方可以在新的检出目录继续同一个 Session,且目录切换可重建。保留策略避免在离开或最后发布失败时删除工作,但调用方必须自行管理闲置检出目录与分支。Git 与 Node 必须存在于文件系统提供方的执行环境中。子模块配置与依赖安装仍是独立操作。 diff --git a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml index f40cecc02e..531b156371 100644 --- a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml @@ -17,12 +17,12 @@ /agent-note-private-npm-publication-as-three-independent-sequences/decision/versions-land-in-the-repository-from-a-local-command-ci-only-checks-and-uploads: en: 71d4cda384013bfb zh: 909d853c18496925 -/agent-note-private-npm-publication-as-three-independent-sequences/decision/vendor-publish-what-changed-and-let-tags-be-the-ledger: - en: 7f578bb18f7f8693 - zh: 49f25210f7c1a61f +/agent-note-private-npm-publication-as-three-independent-sequences/decision/vendor-advance-the-complete-family-with-per-package-tags: + en: 321f48c7e4afac24 + zh: 974a7fa1a75c810d /agent-note-private-npm-publication-as-three-independent-sequences/decision/publication-runs-only-on-github-and-the-registry-decides-what-goes-out: - en: 4f399390f58d7a34 - zh: 9df67248acfc61c8 + en: 8c8cd90e6eabe07d + zh: 7035b71a6eeeb379 /agent-note-private-npm-publication-as-three-independent-sequences/decision/workspace-internal-references-use-the-workspace-protocol: en: 82a7925d449fc283 zh: 653358a1ffbad80a @@ -33,8 +33,8 @@ en: d83f6cf38529c5b5 zh: 4df5659b2154d3b7 /agent-note-private-npm-publication-as-three-independent-sequences/decision/release-family-objects: - en: e8bb80d936e2cbbd - zh: bc4ae297e98a5446 + en: 5bc6a83d124b682d + zh: 17383ec5c3c7e0e5 /agent-note-private-npm-publication-as-three-independent-sequences/decision/workflow-shape-pack-on-pr-push-publish-from-a-manual-dispatch-workflow: en: ea0acc6409da4d5c zh: ac0bd2fe41f4fe67 @@ -42,8 +42,8 @@ en: c505237cad2c4128 zh: 55364ba462536680 /agent-note-private-npm-publication-as-three-independent-sequences/alternatives-considered: - en: 11877d09e5a84f6e - zh: ea372c779254be93 + en: ed22fa8a47b5c4b6 + zh: 7b367971a51a67ff /agent-note-private-npm-publication-as-three-independent-sequences/consequences: - en: f961ebb927e30b5c - zh: 019f1628fb1f2c56 + en: 46f82110175ea3f7 + zh: 1cdd1c99bfb88440 diff --git a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md index 5de43e3720..8552232363 100644 --- a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md +++ b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md @@ -36,9 +36,9 @@ Each sequence has one bump-and-commit command: it derives the target version, wr For equal release numbers, SemVer compares alphanumeric prerelease identifiers lexically: `alpha` is lower than `canary`, `canary` is lower than `rc`, and every prerelease is lower than the stable version. npm dist-tags are mutable aliases and do not participate in version precedence. -### vendor: publish what changed, and let tags be the ledger +### vendor: advance the complete family with per-package tags -The vendored packages are decoupled from upstream by their scope but keep their own version lines. The published version is the higher of the manifest version and the last published version, with the patch incremented — which also drops an upstream prerelease segment. The first published versions: +The vendored packages are decoupled from upstream by their scope but keep their own version lines. The next version uses the higher of the manifest version and the latest tagged version, incrementing the patch after a stable version. The first published versions: | Package | Upstream version | First published version | |---|---|---| @@ -52,13 +52,13 @@ The vendored packages are decoupled from upstream by their scope but keep their | `@deepseek-ai/cordis-plugin-group` | 1.0.0 | 1.0.1 | | `@deepseek-ai/cordis-plugin-logger-console` | 1.0.0 | 1.0.1 | -Taking the last published version as the baseline is what survives a re-sync: upstream restoring `4.0.0-rc.8` after this repository published `4.0.1` would otherwise compute `4.0.1` again and collide. `--prerelease rc.1` publishes a rehearsal instead, which takes `--tag next` and leaves the release numbers free: a prerelease has lower precedence than the release it precedes, so `4.0.1` still follows `4.0.1-rc.1`. That ordering is computed here rather than read from `git tag --sort=v:refname`, which places a prerelease above its release. +Taking the latest tagged version as the baseline prevents reuse after a re-sync: upstream restoring `4.0.0-rc.8` after this repository tagged `4.0.1` would otherwise compute `4.0.1` again. `release:vendor --prerelease rc.1` or `--prerelease alpha.1` selects a prerelease, whose default npm dist-tag is `next`. Later prereleases and the stable release reuse its release numbers: `4.0.1` follows `4.0.1-rc.1`. The script compares SemVer precedence rather than using `git tag --sort=v:refname`, which places a prerelease above its release. -Only changed packages publish, and the change judgement adds no state file: **each package has its own tag, and that tag records the commit it last published from**. For each package, bump reads the newest `vendor--v*` tag and diffs the package directory against it. A path counts when the manifest's `files` selects it, when npm publishes it regardless (`package.json`, `README*`, `LICENSE*`), or — for a package whose `files` selects `lib/` — when it is a build input (`src/**`, `tsconfig*.json`, a build config). That last rule exists because a built payload is not tracked by git: without it, a real source change reads as "nothing changed" and the next publication fails on a version whose bytes moved. +Every vendor release advances all nine packages, each with its own version and `vendor--v` tag. Repacking from another repository state can change a member's dependency ranges or built payload even when its directory has no source changes. Advancing the complete family avoids reusing an existing version for those different bytes. -A tag is a commit pointer, not proof of publication. Bump asks the registry whether the version its newest tag names exists and fails for a human to resolve when it does not, because a tag pushed for a publication that then failed would otherwise read as "already published" and skip the package indefinitely. Querying a private package needs credentials, so an unauthenticated machine reports the gap instead of failing. +A tag reserves a package version and identifies its commit; it does not establish successful publication. Bump uses the latest tag even if its publication failed. The publisher checks registry versions and integrity, and the release manager verifies completion for the complete family. -`vendor/cordis` publishes `src` as well. Its export map declares `"./src/*"`, so a tarball without those files points consumers at absent paths, and `files` selecting only build output left the change judgement with no tracked path to match. +`vendor/cordis` publishes `src` as well. Its export map declares `"./src/*"`, so a tarball without those files points consumers at absent paths. ### Publication runs only on GitHub, and the registry decides what goes out @@ -76,6 +76,10 @@ All three sequences decide this way, including the native one: it publishes thro Two registry behaviours shape how a publish is attempted. Writes are spaced by at least two seconds and retried with a backoff, because publishing several packages back to back outruns the registry's own processing and earns `E409 Failed to save packument`. And every retry re-reads the registry first: a reported failure can answer a write that landed anyway, so a version that now exists with this tarball's integrity counts as published rather than as a version to place again. +For dsh and vendor, `release:publish --dist-tag ` overrides the family's default publication channel. The vendor publish workflow accepts the optional `dist-tag` input; omitting it preserves the family default. An override must be a valid npm dist-tag. Before publishing any tarball, the publisher checks every packed member: the selected tag must already name the intended version, or both that tag and the version must be absent. A tag bound elsewhere, or an already published version without the selected tag, rejects the entire attempt before any upload. Existing integrity checks and retries still apply. Skipped versions are never retagged. + +The preflight does not reserve npm tags atomically. The release manager coordinates external publishers that could change a binding after the check. A dedicated dist-tag also does not exclude stable versions from existing dependency ranges: isolation requires prerelease versions and an audit of the actual packed dependency ranges. + ### Workspace-internal references use the `workspace:` protocol Every reference to a workspace member uses the `workspace:` protocol. Every dependency section and consumer uses exact `workspace:*` DSH references and `workspace:~` vendor/native references, including the native entry's optional platform packages. Vendor and native patch releases must preserve consumer-facing APIs and binary interfaces. Local workspace linking is unchanged. @@ -109,7 +113,7 @@ The entity in this domain is a **release family**: a set of packages sharing one | `publishOrder` | topological order over the sections npm installs plus peer declarations, ties broken by package name; a cycle among installed dependencies is reported rather than resolved arbitrarily, and a peer edge no order can honour is dropped and named | | `pack` | packs a whole family into one directory and records the upload order | | `verify` | the family's version baseline, the publish order it prints in full, and — when publishing — that the run comes from that family's tag and its members are publishable | -| `verify-packed-install` | installs the tarballs of one or more pack directories into a throwaway consumer and drives the installed executable | +| `verify-packed-install` | installs the tarballs of one or more pack directories into a throwaway consumer with an [owned npm cache](../bug-fix/2026-09-06-packed-install-private-npm-cache.md) and drives the installed executable | | `publish` | the three registry states above | | `process` / `tarball` | the one home for spawning commands and for reading a packed tarball, including the entry guard that keeps every script importable | @@ -146,9 +150,9 @@ The installed-consumer probe captures npm's HTTP diagnostics and includes them w **Event-level tags (`vendor-r1`, `vendor-r2`).** Prepared for one release event carrying several package versions. Once the registry decides what publishes, the workflow no longer infers the set from the tag, so per-package tags suffice — and each one names its own package's real version. -**Putting the nine vendored packages on one `4.0.x` line.** It removes change detection, but cosmokit would jump from `1.8.1` to `4.0.1` and lose its upstream lineage; the upstream ranges inside the nine (`^1.8.1` and friends) would stop matching immediately, forcing a rewrite of the vendored manifests. +**Putting the nine vendored packages on one `4.0.x` line.** Cosmokit would jump from `1.8.1` to `4.0.1` and lose its upstream lineage; the upstream ranges inside the nine (`^1.8.1` and friends) would stop matching immediately, forcing a rewrite of the vendored manifests. -**Incrementing every vendored package on every vendor release, with no change detection.** The least machinery, at the cost of new version numbers for packages whose content is byte-identical to the previous release. Tags reduce change detection to reading one tag and running one diff, which is not worth trading for inflated version numbers. +**Publishing only changed vendor directories.** A directory diff does not establish that repacking from another repository state produces identical bytes. Advancing all nine packages avoids integrity collisions for unchanged directories at the cost of additional versions. **Deciding "already published" from the version alone, without comparing content.** The reference flow queries no registry: publish uploads each tarball and npm rejects a duplicate version. Skipping on the version alone misses code that changed without a bump, which is the only failure that quietly leaves stale bytes on the registry. The cost is a registry query and a dependency on reproducible builds. @@ -166,14 +170,14 @@ The installed-consumer probe captures npm's HTTP diagnostics and includes them w ## Consequences -The release scripts are importable modules behind a guarded entry point, and their judgements carry unit tests: tag naming, publish order and cycle reporting, version-baseline arithmetic, the payload change judgement, and each family's payload policy. Two defects the first draft carried — a publish command that ran the pack command on import, and a change judgement blind to `vendor/cordis` source edits — are exactly what a test at that seam catches. +The release scripts are importable modules behind a guarded entry point, and their judgements carry unit tests: tag naming, publish order and cycle reporting, version-baseline arithmetic, and each family's payload policy. The entry guard prevents an import from executing a release command. A pull request runs the full pack for both sequences without credentials and installs the packed dsh tarballs into a throwaway consumer, where plain Node drives `dsh --version`. That probe is deliberately one command: it proves `files` selected a complete payload and that the published ranges resolve, and says nothing about interactive behavior. What this costs: -- **Tags can drift from the registry.** A tag pushed for a publication that then failed is caught by bump's registry check, but only where credentials exist; an unauthenticated machine reports the gap and continues. -- **The change judgement depends on visible tags.** A shallow clone, or a checkout without tags, degrades the vendored judgement to "publish everything for the first time". `fetch-depth: 0` is a precondition, not an optimization. +- **Tags can drift from the registry.** Tags reserve versions even when publication fails; the release manager verifies registry completion separately. +- **The version baseline depends on visible tags.** A shallow clone or a checkout without tags can reuse a reserved vendor version. `fetch-depth: 0` is a precondition, not an optimization. - **The protocol rewrite touched 1504 dependency declarations.** It does not change local resolution — pnpm already resolves from the workspace — but it changes the ranges that go out. - **Private packages need credentials to install.** Every consumer — CI, sandbox e2e, outside users — needs scope credentials, including for the Landlock packages, which have never been published and so cut off no existing anonymous path. - **`repository` names a different organization than the one running the workflows.** Token-based publication is unaffected; npm's OIDC attestation requires the two to agree, so adopting it means either repointing `repository` or publishing from the organization it names. diff --git a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md index e0dec2439a..9d130478e6 100644 --- a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md +++ b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md @@ -36,9 +36,9 @@ Status: implemented 基础版本号相同时,SemVer 按字典序比较字母数字型预发布标识:`alpha` 小于 `canary`,`canary` 小于 `rc`,所有预发布版本都小于稳定版本。npm dist-tag 是可变别名,不参与版本优先级比较。 -### vendor:谁改了谁发版,tag 就是账本 +### vendor:整族递增,每包保留自己的 tag -vendor 九包加了 scope 之后与上游脱钩,但保留各自的版本线。发布版本取「manifest 版本」与「上次发布版本」中较高的那个,再递增 patch——这一步同时去掉上游的预发布段。首发版本: +vendor 九包加了 scope 之后与上游脱钩,但保留各自的版本线。下一版本取 manifest 版本与最新 tag 版本中较高的那个,稳定版本之后递增 patch。首发版本: | 包 | 上游版本 | 首发版本 | |---|---|---| @@ -52,13 +52,13 @@ vendor 九包加了 scope 之后与上游脱钩,但保留各自的版本线。 | `@deepseek-ai/cordis-plugin-group` | 1.0.0 | 1.0.1 | | `@deepseek-ai/cordis-plugin-logger-console` | 1.0.0 | 1.0.1 | -以「上次发布版本」为基线才扛得住重同步:本仓发过 `4.0.1` 之后上游把版本恢复成 `4.0.0-rc.8`,只看 manifest 会再算出 `4.0.1` 并撞上已发版本。加 `--prerelease rc.1` 则发一次排练版:它进 `--tag next`,而且不占用那组数字——预发布的优先级低于它所先行的正式版,所以 `4.0.1` 仍然接在 `4.0.1-rc.1` 之后。这个次序由脚本自己算,不读 `git tag --sort=v:refname`——git 会把预发布排在正式版之前。 +以最新 tag 的版本为基线可避免重同步后复用版本:本仓标记 `4.0.1` 之后,上游若将版本恢复成 `4.0.0-rc.8`,只看 manifest 会再次算出 `4.0.1`。`release:vendor --prerelease rc.1` 或 `--prerelease alpha.1` 选择预发布版本,默认 npm dist-tag 为 `next`。后续预发布和稳定版本复用其基础版本号:`4.0.1` 接在 `4.0.1-rc.1` 之后。脚本按 SemVer 优先级比较,不使用会将预发布排在稳定版本之前的 `git tag --sort=v:refname`。 -只发改动过的包,而变更判据不引入新的状态文件:**每包一个 tag,tag 就是「上次发布到哪个 commit」的记录**。bump 对每个包取最新的 `vendor-<包名>-v*` tag,拿包目录与它做 diff。一条路径算命中的条件是:manifest 的 `files` 选中它,或 npm 无论如何都会发布它(`package.json`、`README*`、`LICENSE*`),或者——当该包的 `files` 选中 `lib/` 时——它是构建输入(`src/**`、`tsconfig*.json`、构建配置)。最后那条规则的存在理由是构建产物不在 git 里:没有它,真实的源码改动会读成「没变化」,而下一次发布会在一个字节已变的版本上失败。 +每次 vendor 发布都递增全部九个包,每包保留自己的版本和 `vendor--v` tag。即使包目录没有源码变更,从不同仓库状态重新打包也可能改变其依赖范围或构建产物。整族递增可避免为这些不同的字节复用已有版本。 -tag 只是 commit 指针,不是发布成功的证明。bump 会向 registry 核对「最新 tag 指向的版本是否真的存在」,不存在就明确失败交人处理——否则一个为失败发布而推的 tag 会被读成「已发布」,从此永远跳过该包。查询私有包需要凭据,因此未鉴权的机器只报告这道核对被跳过,不失败。 +tag 预留包版本并标识其 commit,不代表发布成功。即使对应发布失败,bump 仍使用最新 tag。publisher 核对 registry 中的版本和 integrity,发布负责人核实整族发布完成。 -`vendor/cordis` 现在也发布 `src`。它的 exports 声明了 `"./src/*"`,tarball 里没有这些文件就等于把消费方指向不存在的路径;而 `files` 只选构建产物,也让变更判据没有任何受 git 跟踪的路径可匹配。 +`vendor/cordis` 也发布 `src`。它的 exports 声明了 `"./src/*"`,tarball 中缺少这些文件就会将消费方指向不存在的路径。 ### 发布只在 GitHub 执行,由 registry 状态决定发什么 @@ -76,6 +76,10 @@ tag 只是 commit 指针,不是发布成功的证明。bump 会向 registry registry 的两个行为决定了「怎么尝试一次发布」。写入之间至少间隔两秒并带退避重试,因为连续背靠背发多个包会超出 registry 自身的处理速度,换来 `E409 Failed to save packument`。而每次重试都先重查 registry:报出来的失败可能对应一次其实已经落地的写入,所以「该版本现在存在且 integrity 与本 tarball 相同」算作已发布,而不是又一个待放置的版本。 +对 dsh 和 vendor,`release:publish --dist-tag ` 覆盖该族默认发布通道。vendor 发布 workflow 接受可选的 `dist-tag` 输入;省略时保留该族默认值。覆盖值必须是合法的 npm dist-tag。发布任何 tarball 前,publisher 检查每个打包成员:所选 tag 必须已指向预期版本,或者该 tag 和版本都不存在。tag 指向其他版本,或版本已经发布但缺少所选 tag,都会在上传前拒绝整次发布。原有 integrity 检查与重试继续生效。跳过的版本不会重新绑定 tag。 + +预检不会原子性地预留 npm tag。发布负责人协调可能在检查后更改绑定的外部发布者。专用 dist-tag 也不会让稳定版本被已有依赖范围排除:隔离需要预发布版本,并核查实际打包产物中的依赖范围。 + ### workspace 内部引用走 `workspace:` 协议 所有指向 workspace 成员的引用都用 `workspace:` 协议。每个消费者的所有依赖区段对 DSH 使用精确的 `workspace:*` 引用,对 vendor/native 使用 `workspace:~` 引用,包括原生入口的可选平台包。vendor 与 native 的补丁版本必须保持面向消费者的 API 和二进制接口兼容。本地 workspace 链接方式不变。 @@ -109,7 +113,7 @@ registry 的两个行为决定了「怎么尝试一次发布」。写入之间 | `publishOrder` | 按 npm 会安装的依赖段加 peer 声明做拓扑序,同层按包名排;安装依赖成环是报错而不是随意定序,任何排不进去的 peer 边被丢弃并点名 | | `pack` | 把整族打进一个目录并记录上传顺序 | | `verify` | 族的版本基线、完整打印出来的发布顺序;发布时还要求本次运行来自该族的 tag、且成员可发布 | -| `verify-packed-install` | 把一个或多个 pack 目录的 tarball 装进一次性 consumer,并驱动已安装的可执行入口 | +| `verify-packed-install` | 把一个或多个 pack 目录的 tarball 装进带[独立 npm 缓存](../bug-fix/2026-09-06-packed-install-private-npm-cache.zh.md)的一次性 consumer,并驱动已安装的可执行入口 | | `publish` | 上面那三态 | | `process` / `tarball` | 启动命令、读取打包 tarball 的唯一正家,其中的入口守卫让每个脚本都可被 import | @@ -146,9 +150,9 @@ dsh 的验证会一并安装 vendored 族的 pack 产物。harness 的包把 ven **事件级 tag(`vendor-r1`、`vendor-r2`)。** 为「一次发布事件携带多个包版本」准备。既然由 registry 决定发什么,workflow 就不再从 tag 推断集合,per-package tag 够用,而且每个 tag 携带的是它自己那个包的真实版本。 -**把九个 vendored 包统一到一条 `4.0.x` 版本线。** 省掉变更检测,但 cosmokit 会从 `1.8.1` 跳到 `4.0.1`、丢失上游血缘;九包内部的上游范围(`^1.8.1` 之类)会立刻失配,必须改写 vendored manifest。 +**把九个 vendored 包统一到一条 `4.0.x` 版本线。** Cosmokit 会从 `1.8.1` 跳到 `4.0.1`、丢失上游血缘;九包内部的上游范围(`^1.8.1` 之类)会立刻失配,必须改写 vendored manifest。 -**每次 vendor 发布把九包全部 patch+1,不做变更检测。** 机制最少,代价是内容与上一版逐字节相同的包也拿到新版本号。tag 把变更检测的成本压到「读一个 tag、跑一次 diff」,不值得为省这点让版本号虚涨。 +**只发布目录有变更的 vendor 包。** 目录 diff 不能证明从不同仓库状态重新打包会产出相同字节。递增全部九个包可避免未改目录的 integrity 冲突,代价是增加版本号。 **只按版本号判断「是否已发布」,不比对内容。** 参照流程根本不查 registry:publish 逐个上传,重复版本由 npm 拒绝。只按版本号跳过会漏掉「改了代码没 bump」,而这是唯一会安静地把旧字节留在 registry 上的错误。代价是引入一次 registry 查询和对构建可复现性的依赖。 @@ -166,14 +170,14 @@ dsh 的验证会一并安装 vendored 族的 pack 产物。harness 的包把 ven ## 后果 -发布脚本是带入口守卫的可 import 模块,其判断都有单测覆盖:tag 命名、发布顺序与环报告、版本基线运算、payload 变更判据,以及各族的 payload 策略。第一版带过的两个缺陷——publish 命令在 import 时执行了 pack 命令、变更判据对 `vendor/cordis` 的源码改动失明——正是这类测试在对应接缝上能抓住的。 +发布脚本是带入口守卫的可 import 模块,其判断都有单测覆盖:tag 命名、发布顺序与环报告、版本基线运算,以及各族的 payload 策略。入口守卫阻止 import 执行发布命令。 一个 pull request 会为两条序列跑完整的 pack(无凭据),并把打包好的 dsh tarball 装进一次性 consumer,用普通 Node 驱动 `dsh --version`。这个探针刻意只有一条命令:它证明 `files` 选出了完整 payload、发布出去的范围可解析,不涉及任何交互行为。 代价: -- **tag 可能与 registry 漂移。** 为失败发布而推的 tag 由 bump 的 registry 核对拦下,但只在有凭据的地方;未鉴权的机器只报告这道核对被跳过。 -- **变更判据依赖 tag 可见。** shallow clone 或未拉 tag 会把 vendored 族的判据退化成「全部首发」。`fetch-depth: 0` 是前提,不是优化。 +- **tag 可能与 registry 漂移。** 即使发布失败,tag 仍预留版本;发布负责人另行核实 registry 中的完成状态。 +- **版本基线依赖 tag 可见。** shallow clone 或未拉取 tag 的 checkout 可能复用已预留的 vendor 版本。`fetch-depth: 0` 是前提,不是优化。 - **协议改写触及 1504 处依赖声明。** 它不改变本机解析(pnpm 本来就从 workspace 解析),但改变了发布出去的范围写法。 - **私有包需要凭据才能安装。** 任何消费方——CI、沙箱 e2e、外部使用者——都要持有 scope 凭据,Landlock 三包也在其中;它们从未发布过,所以没有切断既有的匿名安装路径。 - **`repository` 指向的组织与运行 workflow 的组织不同。** 用 token 发布不受影响;npm 的 OIDC attestation 要求二者一致,届时要么把 `repository` 改指过去,要么从它指向的组织发布。 diff --git a/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.i18n.yaml b/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.i18n.yaml new file mode 100644 index 0000000000..b0b5d6295a --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-09-14-generated-cli-help-reference.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.md +/agent-note-generate-the-cli-reference-from-command-help: + en: 618bafe3fd2afdb9 + zh: 433df2a8d0530c41 +/agent-note-generate-the-cli-reference-from-command-help/problem: + en: 2ad571458d5d8191 + zh: 4db6bddbad65bf5c +/agent-note-generate-the-cli-reference-from-command-help/decision: + en: d99a93133c460385 + zh: 0f710ecf6e7137f2 +/agent-note-generate-the-cli-reference-from-command-help/alternatives-considered: + en: df2f7f07b3e84418 + zh: 2dd87333d9d460c3 +/agent-note-generate-the-cli-reference-from-command-help/consequences: + en: aada6686406c892e + zh: 9a30b6b1c5ab6d55 diff --git a/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.md b/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.md new file mode 100644 index 0000000000..2a33722fe0 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.md @@ -0,0 +1,25 @@ +# Agent Note: Generate the CLI reference from command help + +Status: implemented + +English | [中文](2026-09-14-generated-cli-help-reference.zh.md) + +## Problem + +Launcher flags, profile options, and nested commands have separate owners. A manually maintained command reference can omit subcommands or retain obsolete defaults. + +## Decision + +The CLI reference captures the supported `dsh` launcher's help, the plugin forwarder's help, and each shipped profile's help, then recursively discovers the subcommands listed in each page's `Commands:` section. Shipped profile names come from `PROFILE_TEMPLATES`. Every page retains its complete help text; identical alias help appears once with all invocation forms. + +Generation uses temporary Harness and Agents homes. Help exits before application work starts and requires no server or model. The plugin forwarder exposes its own help before pnpm arguments; flags after the pnpm command remain pnpm input. + +The generated bilingual pages retain CLI output in its original language. `verify-cli-help` compares the complete pages and pairing record and runs with the documentation checks. + +## Alternatives considered + +A second command inventory or help-specific runtime API would duplicate the parser's definitions. Reading actual help keeps the reference tied to the same descriptions, options, defaults, and examples users receive. + +## Consequences + +Command changes update the reference through the same generator. Reviewers can compare complete help without assembling separate command invocations. diff --git a/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.zh.md b/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.zh.md new file mode 100644 index 0000000000..beba0b1ce2 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.zh.md @@ -0,0 +1,25 @@ +# Agent Note: 从命令帮助生成 CLI 参考 + +Status: implemented + +[English](2026-09-14-generated-cli-help-reference.md) | 中文 + +## Problem + +启动器选项、profile 选项和嵌套命令由不同部分负责。手工维护的命令参考可能遗漏子命令或保留过时的默认值。 + +## Decision + +CLI 参考采集受支持的 `dsh` 启动器帮助、插件转发器帮助和各随附 profile 的帮助,并递归发现每页 `Commands:` 段落列出的子命令。随附 profile 的名称来自 `PROFILE_TEMPLATES`。每页都保留完整的帮助文本;相同的别名帮助只展示一次,并列出所有调用形式。 + +生成过程使用临时 Harness home 和 Agents home。帮助在应用开始工作前退出,不需要服务器或模型。插件转发器在 pnpm 参数之前提供自己的帮助;pnpm 命令之后的选项仍作为 pnpm 输入。 + +生成的双语页面保留 CLI 输出的原始语言。`verify-cli-help` 比较完整页面和配对记录,并随文档检查一起运行。 + +## Alternatives considered + +另一份命令清单或专供帮助使用的运行时 API 会重复解析器的定义。读取实际帮助,可以让参考文档始终对应用户收到的说明、选项、默认值和示例。 + +## Consequences + +命令变化通过同一个生成器更新参考文档。审阅者可以直接比较完整帮助,无需自行拼接各个命令的调用结果。 diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml index d0ca4b49ee..e3ffc60e70 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml @@ -9,11 +9,11 @@ en: dc43ba84345e1405 zh: dbc03572ab7710d2 /agent-note-ship-optional-bundles-with-the-installation/decision: - en: 92fd1e2e9345e407 - zh: 33a39f71413499ff + en: 5210b3b746cfee78 + zh: f1907f4807ae0098 /agent-note-ship-optional-bundles-with-the-installation/alternatives-considered: - en: 05c19f616009bee8 - zh: af318640481351f0 + en: 95b48b61757f1f13 + zh: e9fb2d6ea492a4bc /agent-note-ship-optional-bundles-with-the-installation/consequences: en: 4a9d3a049a605150 zh: 67b50bbc9f7f4bb0 diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md index bf5d2206ef..151cc8ccad 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md @@ -10,15 +10,15 @@ The Web plugin page manages only the bundles a person installed into the profile ## Decision -The launcher names in `OPTIONAL_BUNDLES` (`packages/boot/app-boot/src/profile.ts`, beside the profile templates) the bundles the installation ships for a person to switch on. Each must be a runtime dependency of `apps/cli` that declares `dsh.bundle.patch`, and no shipped profile template selects it. The plugin manager's `listBundles` reports such a bundle as `optional`: switched off until selected, never removable, resolved from the installation like any installation-supplied bundle. The Web plugin page opens its Official group with the optional bundles, tagged beta where the feature is one, ahead of the profile's own installed bundles. +The launcher names in `OPTIONAL_BUNDLES` (`packages/boot/app-boot/src/profile.ts`, beside the profile templates) the bundles the installation ships for a person to switch on. Each must be a runtime dependency of `apps/cli` that declares `dsh.bundle.patch`, and no shipped profile template selects it. The plugin manager's `listBundles` reports such a bundle as `optional`: switched off until selected, never removable, resolved from the installation like any installation-supplied bundle. The Web plugin page opens its Official group with the optional bundles, tagged experimental when the package is experimental, ahead of the profile's own installed bundles. Default-product isolation keeps its rules with one declared exception: an optional bundle's dependency graph is outside the default product. The static gate skips the `dependencies` edge from `@deepseek-ai/dsh` to a listed bundle and still rejects a runtime import, a shipped composition, a preset, or a default template that names it, an experimental dependency the list does not name, and a listed name that is not a runtime dependency or not a bundle. The workspace-constraints check accepts the same `dependencies` edges and no other runtime section, and the packed-install release check skips them from the installed entry package while requiring each listed bundle to be installed. -The [single-bundle reference](../../../../packages/experimental/agent-team-profile/README.md) supersedes the two Agent Teams selections with one `@deepseek-ai/dsh-experimental-agent-team-profile` selection. This note retains the installation-owned optional-bundle policy; the [optional-bundle decision for experimental capabilities](../architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md) names the current list and the display metadata each entry carries. +The [single-bundle reference](../../../../packages/experimental/agent-team-profile/README.md) supersedes the two Agent Teams selections with one `@deepseek-ai/dsh-experimental-agent-team-profile` selection. This note retains the installation-owned optional-bundle policy; the [optional-bundle decision for experimental capabilities](../architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md) owns composition and display metadata; `OPTIONAL_BUNDLES` owns the current shipped list. ## Alternatives considered -**A catalog of installable official bundles.** The page would offer names to install from the registry on demand. That keeps the installation unchanged but needs network access at the moment of switching on and a version pin per release. +**A catalog of installable official bundles.** The page would offer names to install from the registry on demand. That keeps the installation unchanged but needs network access at the moment of switching on and a version pin per release. Lightweight bundles retain the shipped policy; the [on-demand native-bundle decision](../architecture/2026-10-05-official-on-demand-bundles.md) owns the exception for costly native runtimes. **A flag on the bundle package.** A `dsh.bundle.optional` declaration would let any published bundle claim a place in the installation; the launcher's own list keeps the choice with the product. diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md index f6254e78d5..9ddd54da88 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md @@ -10,15 +10,15 @@ Web 插件页只管理用户装进 profile 的组合包。像 Agent Teams 这样 ## 决策 -启动器在 `OPTIONAL_BUNDLES`(`packages/boot/app-boot/src/profile.ts`,与 profile 模板并列)里点名安装随附、供用户开启的组合包。每一个都必须是 `apps/cli` 声明了 `dsh.bundle.patch` 的运行时依赖,且不被任何随附 profile 模板选中。插件管理器的 `listBundles` 把这类组合包报告为 `optional`:选中前保持关闭、永不可卸载、像其他安装提供的组合包一样从安装目录解析。Web 插件页以可选组合包开启「官方」分组,属于 beta 功能的带 beta 标签,排在 profile 自己已安装的组合包之前。 +启动器在 `OPTIONAL_BUNDLES`(`packages/boot/app-boot/src/profile.ts`,与 profile 模板并列)里点名安装随附、供用户开启的组合包。每一个都必须是 `apps/cli` 声明了 `dsh.bundle.patch` 的运行时依赖,且不被任何随附 profile 模板选中。插件管理器的 `listBundles` 把这类组合包报告为 `optional`:选中前保持关闭、永不可卸载、像其他安装提供的组合包一样从安装目录解析。Web 插件页以可选组合包开启「官方」分组,属于实验包的带实验性标签,排在 profile 自己已安装的组合包之前。 默认产品隔离的规则保持不变,只声明一个例外:可选组合包的依赖图在默认产品之外。静态门禁跳过从 `@deepseek-ai/dsh` 到列表中组合包的 `dependencies` 边,仍然拒绝运行时 import、随附组合、preset 或默认模板对它的引用,拒绝列表没有点名的实验依赖,也拒绝不是运行时依赖或不是组合包的列表项。workspace 约束检查接受同样的 `dependencies` 边而不接受其他运行时依赖段;发布时的 packed-install 检查对已安装入口包跳过这些边,并要求列表中的每个组合包都已安装。 -[单组合包参考](../../../../packages/experimental/agent-team-profile/README.zh.md)将两个 Agent Teams 选项替换为一个 `@deepseek-ai/dsh-experimental-agent-team-profile` 选项。本记录保留由安装方决定可选组合包的策略;[实验能力的可选 bundle 决策](../architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)列出当前名单以及每一项携带的显示元数据。 +[单组合包参考](../../../../packages/experimental/agent-team-profile/README.zh.md)将两个 Agent Teams 选项替换为一个 `@deepseek-ai/dsh-experimental-agent-team-profile` 选项。本记录保留由安装方决定可选组合包的策略;[实验能力的可选 bundle 决策](../architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)定义组合与显示元数据;当前随附名单由 `OPTIONAL_BUNDLES` 管理。 ## 考虑过的替代方案 -**可安装官方组合包目录。** 页面按需提供从注册表安装的包名。安装本身不变,但开启那一刻需要网络,并且每次发布都要钉一个版本。 +**可安装官方组合包目录。** 页面按需提供从注册表安装的包名。安装本身不变,但开启那一刻需要网络,并且每次发布都要钉一个版本。轻量 bundle 仍采用随附策略;[按需原生 bundle 决策](../architecture/2026-10-05-official-on-demand-bundles.zh.md)管理原生运行时成本较高的例外。 **组合包自身的标记。** `dsh.bundle.optional` 声明会让任何已发布的组合包都能自称随安装提供;由启动器自己的列表来决定,选择权留在产品手里。 diff --git a/.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.i18n.yaml b/.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.i18n.yaml new file mode 100644 index 0000000000..79f3339e71 --- /dev/null +++ b/.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-10-05-pr-artifact-typechecks.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.md +/agent-note-pr-artifact-builds-and-aggregate-typescript-checks: + en: a25e7cdcad03934f + zh: 48e31429730e0885 +/agent-note-pr-artifact-builds-and-aggregate-typescript-checks/problem: + en: e5c984fa32cc5e69 + zh: 25d5742b2106d830 +/agent-note-pr-artifact-builds-and-aggregate-typescript-checks/decision: + en: 6e84bceaba4573ca + zh: 25a37c147a675643 +/agent-note-pr-artifact-builds-and-aggregate-typescript-checks/alternatives-considered: + en: f61da066f9ddcd62 + zh: 816fd6e0063017cd +/agent-note-pr-artifact-builds-and-aggregate-typescript-checks/consequences: + en: 46c247ab91126d52 + zh: f1be7561ff402bcc diff --git a/.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.md b/.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.md new file mode 100644 index 0000000000..7577a7c06e --- /dev/null +++ b/.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.md @@ -0,0 +1,45 @@ +# Agent Note: PR artifact builds and aggregate TypeScript checks + +Status: implemented + +English | [中文](2026-10-05-pr-artifact-typechecks.zh.md) + +## Problem + +PR jobs need the same compiled packages for benchmarks, compatibility, packaging, and runtime checks. Building the repository-wide test and script programs in every artifact job repeats expensive diagnostics. In the allowed #3661 baseline, benchmark build preparation takes about 304 seconds before 432 seconds of serial measurements. A cold macOS arm64 Host compiler profile on Node 24.19.0 takes 84.01 seconds, including 55.92 seconds in the aggregate program; its 282 emitted projects take 24.33 seconds combined. Increasing concurrency consumes more shared-runner resources. + +## Decision + +Required Linux consumer and native Windows build jobs retain normal builds, including both complete aggregate programs. Public `build` and `typecheck` keep the same checks. [Node version ownership](2026-07-06-node-engine-floor.md) and [native Windows ownership](2026-08-08-native-windows-pull-request-ci.md) remain in force. + +Checked PR artifact builds compile every existing project reference from each compiler face with one serial `tsc -b` process, then run the ordinary Typert, bundling, Desktop, and Web stages. The helper reads the existing Host and Client aggregates rather than maintaining another package list. It omits only each root's additional test and script program; package diagnostics, emitted outputs, and Host-before-Client ordering remain required. Non-PR release and real-API workflow builds retain their normal checks. + +Only the CI benchmark helper selects `tsc -b --noCheck` for package emission. Benchmarks consume JavaScript and Typert runtime metadata; their declaration and build-information files are disposable and never published. Typert analyzes source types independently, while required complete builds own semantic diagnostics. Syntax and configuration failures still reject benchmark preparation. TypeScript records pending checks so a later ordinary incremental build diagnoses the same source without deleting caches or forcing a rebuild. Public benchmark scripts and every release, Python, preview, compatibility, and normal artifact builder keep their checked defaults. + +The Python executable builder selects artifact compilation with `--artifacts-only` after verifying its runtime closure. Invalid closure inputs fail before compilation, deployment, or packing; the default complete build and prebuilt `--skip-build` mode keep their existing stages. + +CI invokes the internal build helpers directly and keeps the root package manifest unchanged. Root manifest edits activate the separate native-addon platform matrix even when only build aliases change; avoiding that trigger preserves the existing workflow set and concurrent-job ceiling without weakening native validation. + +Job inventory, runner selectors, and benchmark sample counts stay fixed; gate and worker ceilings do not increase. The three compatibility cases run one job at a time; the two-target Python runtime matrix starts after the keyless Python SDK job succeeds. Four Linux lanes, the Python SDK, three Windows lanes, and one compatibility job permit at most nine worker jobs initially. Once the SDK job finishes, the remaining seven independent lanes, one compatibility job, and two runtime targets permit at most ten. The runtime plan and wheel stages precede those targets and each use one job. This permits runtime validation to overlap the Node 22 build within the original ten-job ceiling. Coverage partitions reuse compact file-cost metadata within their platform and runner pool. Parallel release packing uses pnpm's existing bounded scheduler; serial publication keeps family order. + +Each Node version runs the same seven source compatibility specs in one serial Vitest invocation. Both file parallelism and the CLI worker limit are fixed to one, and the gate sets `VITEST_MAX_WORKERS=1` because that environment variable overrides Vitest's CLI limit. Files retain fork isolation; the build-backed Node 22 lazy-search smoke keeps its separate build dependency. Three paired local samples preserve all 253 assertion outcomes and reduce startup-only median time from 11.52 to 8.69 seconds, without increasing owned descendant-process peak. + +## Alternatives considered + +**Raise job or worker concurrency.** Rejected because the shared runner must keep its current resource ceiling. + +**Remove aggregate checks from every build.** Rejected because package projects do not typecheck all repository tests and scripts; required Linux and Windows jobs must retain those diagnostics. + +**Cache compiled outputs across revisions.** Deferred because cache correctness and invalidation add obligations that removing duplicate aggregate work does not need. + +## Consequences + +The cold Host reference build takes 23.49 seconds with 1.91 GiB peak RSS, compared with 84.01 seconds and 3.87 GiB for the complete compiler pass. All 5,932 emitted files match by path and SHA-256. These are single local compiler samples, excluding bundling and tests; current-head CI owns the PR latency result. The benchmark scenarios and their budgets remain unchanged. + +Three alternating cold preparation pairs on Apple M4 Pro / Node 24.19.0 compare checked reference compilation with benchmark-only emission: 61.16, 82.53, and 57.80 seconds versus 55.53, 50.82, and 50.75 seconds. Median preparation falls from 61.16 to 50.82 seconds (16.9%); this includes native support, libraries, benchmark workers, and Web assets, excluding dependency installation and measurements. Every pair has the same 11,894 output paths, including 3,181 byte-identical JavaScript files and unchanged source maps. The 356 build-information files differ, and nine declarations only reorder union members; those files are not published from the benchmark lane. Library-build median user CPU falls from 76.45 to 64.23 seconds, while median maximum RSS stays near 2.75 GiB. Required CI determines the complete PR timing. + +Parallel packing relies on the pinned pnpm CLI to honor exact recursive filters, the pack destination, the workspace concurrency limit, and each package's lifecycle hooks. A pnpm upgrade must pass the real-CLI checks in `scripts/release/pack.spec.ts` before release packing adopts it. + +Coverage weights sum Vitest's distinct environment, preparation, setup, collection, and execution costs. CI's platform/environment/pool cache prefixes exclude the previous `coverage-times-` archives. Persistent local checkouts can retain wall-clock weights for files not measured during rollout; removing `.coverage-times.json` resets that history. These advisory weights affect partition assignment, while the complete inventory and merged coverage thresholds remain required. + +Checked artifact builds deliberately do not diagnose aggregate-only test or script errors. Required normal builds reject those errors; fixture checks also reject package errors on both paths and compare emitted JavaScript, declarations, maps, and build information. Removing or changing a required full build requires transferring both aggregate checks to another blocking owner first. Master serial references remain complete under their [existing policy](2026-07-21-serial-cross-platform-ci-reference.md). diff --git a/.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.zh.md b/.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.zh.md new file mode 100644 index 0000000000..c0b3c1bcf4 --- /dev/null +++ b/.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.zh.md @@ -0,0 +1,45 @@ +# Agent Note: PR 产物构建与 aggregate TypeScript 检查 + +Status: implemented + +[English](2026-10-05-pr-artifact-typechecks.md) | 中文 + +## Problem + +PR job 需要相同的编译包来运行 benchmark、兼容性、打包和运行时检查。每个产物 job 都构建仓库级测试和脚本 program,会重复执行昂贵的诊断。在允许使用的 #3661 baseline 中,benchmark 在 432 秒的串行测量前花费约 304 秒准备构建。在 macOS arm64、Node 24.19.0 上,一次冷启动 Host 编译器 profile 耗时 84.01 秒,其中 aggregate program 占 55.92 秒;282 个有 emit 的 project 合计耗时 24.33 秒。提高并发会消耗更多共享运行器资源。 + +## Decision + +必需的 Linux consumer 与原生 Windows build job 保留普通构建,包括两个完整 aggregate program。公开的 `build` 和 `typecheck` 保留相同检查。[Node 版本职责](2026-07-06-node-engine-floor.zh.md)与[原生 Windows 职责](2026-08-08-native-windows-pull-request-ci.zh.md)仍然有效。 + +带检查的 PR 产物构建通过一个串行 `tsc -b` 进程编译各 compiler face 的全部现有 project reference,然后运行普通 Typert、bundling、Desktop 和 Web 阶段。helper 读取现有 Host 与 Client aggregate,而不维护另一份包清单。它只省略各 root 附加的测试和脚本 program;包诊断、emit 输出及 Host 先于 Client 的顺序仍然必需。非 PR 的 release 和真实 API 工作流构建保留普通检查。 + +只有 CI benchmark helper 选择 `tsc -b --noCheck` 来 emit 包。benchmark 消费 JavaScript 和 Typert 运行时元数据;其声明与 build-information 文件是临时产物,不会发布。Typert 独立分析源码类型,语义诊断由必需的完整构建负责。语法与配置错误仍会拒绝 benchmark 准备。TypeScript 记录待检查状态,使后续普通增量构建无需删除缓存或强制重建,就能诊断相同源码。公开 benchmark script,以及所有 release、Python、preview、compatibility 和普通产物 builder,均保留带检查的默认行为。 + +Python executable builder 在验证 runtime closure 后,通过 `--artifacts-only` 选择产物编译。无效 closure 在编译、部署或打包前失败;默认完整构建和复用预构建产物的 `--skip-build` 模式保留原有阶段。 + +CI 直接调用内部 build helper,并保持 root package manifest 不变。即使只修改 build alias,root manifest 改动也会激活独立的 native-addon 平台矩阵;避免这一触发条件,可以在不削弱 native 验证的情况下保留现有 workflow 集合与并发 job 上限。 + +job 清单、runner 选择和 benchmark sample 数量保持固定;gate 与 worker 上限不提高。三个 compatibility case 每次运行一个 job;双目标 Python runtime 矩阵在 keyless Python SDK job 成功后启动。四个 Linux lane、Python SDK、三个 Windows lane 和一个 compatibility job,初始最多占用九个 worker job。SDK job 完成后,剩余七个独立 lane、一个 compatibility job 和两个 runtime target,最多占用十个 job。runtime plan 与 wheel 阶段先于这些 target,各占一个 job。这允许 runtime 验证与 Node 22 构建重叠,同时保持原有十个 job 的上限。覆盖率分区在各自的平台和 runner pool 内复用精简的文件成本元数据。并行 release 打包使用 pnpm 现有的有界调度器;串行发布保持 family 顺序。 + +每个 Node 版本在一次串行 Vitest 调用中运行相同的七个 source compatibility spec。文件并行和 CLI worker 上限均固定为一,gate 还设置 `VITEST_MAX_WORKERS=1`,因为该环境变量会覆盖 Vitest 的 CLI 上限。文件保持 fork 隔离;依赖构建的 Node 22 lazy-search smoke 保留独立的 build 依赖。三对本地样本保持全部 253 个 assertion outcome,将启动开销比较的 median 从 11.52 秒降至 8.69 秒,且不增加所拥有 descendant process 的峰值。 + +## Alternatives considered + +**提高 job 或 worker 并发。** 否决,因为共享运行器必须保持现有资源上限。 + +**从所有构建中移除 aggregate 检查。** 否决,因为包 project 不会检查所有仓库测试和脚本的类型;必需的 Linux 与 Windows job 必须保留这些诊断。 + +**跨 revision 缓存编译输出。** 暂缓,因为缓存正确性与失效规则增加了额外义务,而移除重复 aggregate 工作无需承担这些义务。 + +## Consequences + +冷启动 Host reference 构建耗时 23.49 秒、峰值 RSS 为 1.91 GiB;完整编译器阶段为 84.01 秒和 3.87 GiB。全部 5,932 个 emit 文件的路径与 SHA-256 相同。这些是单次本地编译器样本,不含 bundling 和测试;PR 延迟结果以当前 head 的 CI 为准。benchmark 场景与预算保持不变。 + +在 Apple M4 Pro / Node 24.19.0 上,三对交替冷启动准备构建比较带检查的 reference 编译与仅用于 benchmark 的 emit:前者为 61.16、82.53、57.80 秒,后者为 55.53、50.82、50.75 秒。准备阶段中位数从 61.16 降至 50.82 秒(16.9%);范围包括 native support、library、benchmark worker 和 Web 产物,不含依赖安装与测量。每对都包含相同的 11,894 个输出路径,其中 3,181 个 JavaScript 文件逐字节相同,source map 不变。356 个 build-information 文件不同,九个声明仅调整 union 成员顺序;benchmark lane 不会发布这些文件。library 构建的 user CPU 中位数从 76.45 降至 64.23 秒,maximum RSS 中位数保持在约 2.75 GiB。完整 PR 耗时以必需 CI 为准。 + +并行打包依赖锁定的 pnpm CLI 正确处理精确 recursive filter、pack destination、workspace 并发上限及各包的 lifecycle hook。升级 pnpm 时,必须先通过 `scripts/release/pack.spec.ts` 中的真实 CLI 检查,再让 release 打包采用该版本。 + +覆盖率权重累加 Vitest 互不重叠的 environment、preparation、setup、collection 和 execution 成本。CI 的平台/environment/pool cache prefix 不匹配原来的 `coverage-times-` archive。持久化本地 checkout 在切换期间可能为未测量的文件保留墙钟权重;删除 `.coverage-times.json` 可重置该历史。这些参考权重影响分区分配,完整清单与合并后的覆盖率阈值仍然必需。 + +带检查的产物构建有意不诊断仅属于 aggregate 的测试或脚本错误。必需的普通构建拒绝这些错误;fixture 检查还验证两个路径都会拒绝包错误,并比较 emit 的 JavaScript、声明、map 和 build 信息。删除或改变必需的完整构建前,必须先把两个 aggregate 检查转交另一处阻塞型 owner。master 串行 reference 按[现有政策](2026-07-21-serial-cross-platform-ci-reference.zh.md)保持完整。 diff --git a/.agents/notes/implemented/process/2026-10-05-product-package-classification.i18n.yaml b/.agents/notes/implemented/process/2026-10-05-product-package-classification.i18n.yaml new file mode 100644 index 0000000000..7673f38075 --- /dev/null +++ b/.agents/notes/implemented/process/2026-10-05-product-package-classification.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-10-05-product-package-classification.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-10-05-product-package-classification.md +/agent-note-require-product-use-or-an-explicit-package-classification: + en: 8caa2edff065f7db + zh: 163ee0f31c7adc6a +/agent-note-require-product-use-or-an-explicit-package-classification/problem: + en: 748eb2bf5f645911 + zh: b123aec22395193a +/agent-note-require-product-use-or-an-explicit-package-classification/decision: + en: 8678a01d48195e8f + zh: 9052cc7f64ed452f +/agent-note-require-product-use-or-an-explicit-package-classification/alternatives-considered: + en: 67177e349ecb70de + zh: e8263b9445f46ded +/agent-note-require-product-use-or-an-explicit-package-classification/consequences: + en: 449278a2bccea70e + zh: 5eb28c22783ac6cf diff --git a/.agents/notes/implemented/process/2026-10-05-product-package-classification.md b/.agents/notes/implemented/process/2026-10-05-product-package-classification.md new file mode 100644 index 0000000000..2f3a76ae85 --- /dev/null +++ b/.agents/notes/implemented/process/2026-10-05-product-package-classification.md @@ -0,0 +1,29 @@ +# Agent Note: Require product use or an explicit package classification + +Status: implemented + +English | [中文](2026-10-05-product-package-classification.zh.md) + +## Problem + +An installation dependency can remain dormant indefinitely. Its presence in the dependency graph does not establish that a shipped profile, runtime import, or optional capability uses it. Conversely, an embedding SDK or a deployment-configured provider can have a maintained product role without appearing in the default GUI. + +## Decision + +Nonexperimental packages have effective product use or an explicit entry in the [package policy](../../../../scripts/product-package-policy.ts). The policy distinguishes optional integrations, SDKs, build tools, test tools, Web distribution packages, and declaration-only infrastructure. Each exception records its maintained role. [Experimental status](../../../../packages/experimental/README.md#status) describes maturity and support, independently of installation, optional selection, and Official discovery. + +The [product-use check](../../../../scripts/verify-product-use.ts) follows effective shipped profiles, preset children, runtime source imports, and explicit dynamic mounts. It preserves disabled state through groups and Includes and applies patches before counting use. Manifest dependencies and type-only references do not create runtime use. Optional-bundle reachability remains separate from default reachability. Unknown packages and invalid or stale policy entries fail static CI and package hygiene. + +The [default-product isolation rule](2026-09-12-default-product-experimental-isolation.md) remains independent: its conservative installation and declaration traversal rejects unintended experimental dependencies even when they are disabled. That traversal cannot establish active product use. + +## Alternatives considered + +**Treat every installed package as used.** A redundant dependency would satisfy its own classification requirement and conceal dormant code. + +**Classify every nondefault capability as experimental.** Deployment prerequisites and installation cost do not establish immaturity; supported optional providers and SDKs need separate categories. + +**Count every declared patch row.** A disabled Include, missing target, or later replacement can prevent that row from mounting. Only the effective composition establishes use. + +## Consequences + +Adding a nonexperimental package requires a product consumer or a reviewed classification. Renames and reduced in-box availability require upgrade instructions, while recorded events and released data keep their existing obligations. Regression fixtures reject manifest-only dependencies, disabled subtrees, ineffective patches, and stale classifications. SDK, test, build, and declaration packages remain independently installable without acquiring a runtime mount requirement. diff --git a/.agents/notes/implemented/process/2026-10-05-product-package-classification.zh.md b/.agents/notes/implemented/process/2026-10-05-product-package-classification.zh.md new file mode 100644 index 0000000000..e53da65c0e --- /dev/null +++ b/.agents/notes/implemented/process/2026-10-05-product-package-classification.zh.md @@ -0,0 +1,29 @@ +# Agent Note: Require product use or an explicit package classification + +Status: implemented + +[English](2026-10-05-product-package-classification.md) | 中文 + +## 问题 + +安装依赖可能长期处于休眠状态。出现在依赖图中,并不表明随附 profile、运行时导入或可选能力使用了它。反过来,嵌入式 SDK 或需要部署配置的提供方即使不出现在默认 GUI 中,也可以承担维护中的产品职责。 + +## 决策 + +非实验包必须具有有效产品用途,或在[包策略](../../../../scripts/product-package-policy.ts)中显式列出。策略区分可选集成、SDK、构建工具、测试工具、Web 分发包和纯声明基础设施,每个例外记录其维护职责。[实验性状态](../../../../packages/experimental/README.zh.md#status)描述成熟度与支持承诺,独立于安装、可选启用与官方发现。 + +[产品用途检查](../../../../scripts/verify-product-use.ts)跟踪有效的随附 profile、预设子行、运行时源码导入与显式动态挂载。检查在组与 Include 中保留禁用状态,并先应用 patch 再统计用途。Manifest 依赖和纯类型引用不产生运行时用途。可选组合包的可达关系与默认可达关系分别计算。未知包及无效或过时的策略条目会使静态 CI 和包卫生检查失败。 + +[默认产品隔离规则](2026-09-12-default-product-experimental-isolation.zh.md)保持独立:其保守的安装与声明遍历即使对已禁用行,也会拒绝意外实验依赖。该遍历不能证明活动产品用途。 + +## 考虑过的替代方案 + +**将所有已安装包视为已使用。** 冗余依赖本身就能满足分类要求,从而掩盖休眠代码。 + +**将所有非默认能力归为实验性。** 部署前提与安装成本不能证明不成熟;受支持的可选提供方与 SDK 需要独立分类。 + +**统计所有声明的 patch 行。** 禁用的 Include、缺失目标或后续替换都可能阻止该行挂载。只有有效组合才能证明用途。 + +## 影响 + +新增非实验包需要产品消费者或经过审查的分类。改名和减少随附可用性需要升级说明,已记录事件和已发布数据继续遵守既有义务。回归 fixture 拒绝只有 manifest 依赖、禁用子树、无效 patch 和过时分类的情况。SDK、测试、构建与声明包仍可独立安装,无需新增运行时挂载。 diff --git a/.agents/notes/implemented/simplification/2026-09-17-unified-subagent-activations.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-17-unified-subagent-activations.i18n.yaml new file mode 100644 index 0000000000..cf9a9b80a3 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-09-17-unified-subagent-activations.i18n.yaml @@ -0,0 +1,25 @@ +# Bilingual-pair consistency record for 2026-09-17-unified-subagent-activations.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-17-unified-subagent-activations.md +/agent-note-unified-subagent-activations: + en: bf9959f51e79029c + zh: 9526781af4b72f6a +/agent-note-unified-subagent-activations/problem: + en: c97919745873e705 + zh: b531e60b626c4304 +/agent-note-unified-subagent-activations/decision: + en: 86c924e5eedb919d + zh: 36dd301bdba6325d +/agent-note-unified-subagent-activations/decision/retained-decisions: + en: 670043a18f9cd32c + zh: d925485386bab362 +/agent-note-unified-subagent-activations/alternatives-considered: + en: f1fe44375cd78882 + zh: 9abe43a7aa63c1b0 +/agent-note-unified-subagent-activations/consequences: + en: d9f618345ec98d3b + zh: 7aceba46b132414e +/agent-note-unified-subagent-activations/verification: + en: ee6acf6a9ae509db + zh: e239187d787f24fd diff --git a/.agents/notes/implemented/simplification/2026-09-17-unified-subagent-activations.md b/.agents/notes/implemented/simplification/2026-09-17-unified-subagent-activations.md new file mode 100644 index 0000000000..07268e4016 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-09-17-unified-subagent-activations.md @@ -0,0 +1,49 @@ +# Agent Note: Unified subagent activations + +Status: implemented + +English | [中文](2026-09-17-unified-subagent-activations.zh.md) + +## Problem + +Local one-shot execution and continuable residency separately owned child creation, cancellation, structured capture, results, and cleanup. Foreground and Job-backed tool paths added more ownership paths. Workflow code still needed to await a result, while external backends needed one process execution without acquiring a multi-turn protocol. + +## Decision + +`startActivation()` is the consumer entry point for every provider. A published handle exposes child identity, a result promise, and disposal of that exact activation. The caller signal cancels unpublished creation only. Local results wait for pending input and owned descendants before closing admission, so descendant replies can contribute to the final answer. Both local and external result promises settle after cleanup and notifications. Parent waits use that same completion promise; a captured execution result survives cleanup failure, which disposal reports separately. Workflow consumers already wait for cleanup before continuing, so activation ownership needs no independent result-readiness and release signals. + +Local spawn and fork providers contribute only `prepareContinuable()` data; the subagent service owns Agent creation and residency. The standalone in-process driver package is removed. Structured tools, validation, instructions, and terminal guards attach to one activation and close with it. After capture, that activation rejects further input; cold resume reconstructs an ordinary conversation without the previous schema. + +ACP, DSH SDK, Codex, and Claude Code retain their existing single-execution provider adapters. They join the same activation capacity and ownership graph without accepting follow-up messages or fabricating a local child Session. The parent-owned catalog registers every local child and parent-delivery external leaves. Local membership remains independent of result delivery so completed workflow Sessions and their descendants stay discoverable. Caller delivery leaves external membership tracking to its consumer. External entries carry `mode: 'external'` because they have no local Session to open. Creation records membership once; execution and cleanup do not update the catalog. Execution status has no required consumer and remains outside the membership record. Caller delivery returns the complete result; parent delivery queues the complete completion notice. + +The model-facing tool always returns a background child id and promises the manager's completion notice. It has no foreground switch or Job integration. Workflows select caller delivery, await the activation result, and dispose the handle before finishing; they add neither a completion notice nor initial return guidance to the parent/child exchange. Headless completion waits for its own child tree and subsequent parent turns. + +Parent completion notices include final text regardless of `send_message` availability or use. Tool availability cannot establish that the child sent its answer: it may send only progress or finish without calling the tool. An answer already sent through the tool can appear twice; accepting that duplication avoids making result delivery depend on model compliance or another delivery ledger. + +### Retained decisions + +The native Session reader rejects duplicate child membership for both local and external catalog entries. + +The [continuable residency](../feature/2026-07-28-continuable-subagent-conversations.md), [fork request prefix](../architecture/2026-08-10-fork-children-stay-one-shot.md), and [parent-owned catalog](../architecture/2026-09-01-parent-owned-subagent-catalog.md) records retain their independent rationale. The [subagent package reference](../../../../packages/subagent/subagent/README.md) owns current provider, result-delivery, and activation-capacity contracts; the corresponding [capability seam](../../archived/feature/2026-06-21-subagent-capability-seam.md), [settlement delivery](../../archived/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md), and [capacity](../../archived/feature/2026-09-15-continuable-activation-capacity.md) records remain historical snapshots. This decision owns the shared entry point, external participation, and caller-versus-parent result delivery. + +Released Session generations remain immutable. Historical one-shot descriptors stay readable; removing current execution paths does not justify rewriting or discarding durable history. + +## Alternatives considered + +**Keep a separate external execution projection.** It duplicates membership, restoration, and client update handling already supplied by the parent catalog. Repeated directory snapshots would also retransmit every retained result. The catalog retains only membership; results remain with their recipient. + +**Keep one-shot local execution for synchronous workflows.** Awaiting a result is a consumer requirement. A second Agent lifecycle duplicates cancellation and cleanup solely to supply a promise, which the activation can supply directly. + +**Add multi-turn support to every external backend.** This expands backend protocols, recovery, and authenticated routing without being necessary for shared ownership. The adapters retain their existing execution capability. + +**Use Jobs for all background children.** Jobs add a second work registry and cancellation authority beside activation residency and the Agent inbox. Child discovery and completion already belong to the subagent service. + +**Send every result to the parent.** A workflow already consumes and presents its child result. Another wake adds duplicate context and can disturb a parent that is awaiting the workflow tool. Delivery is an explicit consumer choice; the model-facing tool's notice remains unconditional. + +## Consequences + +One manager controls admission, cancellation, ownership, and resource release across providers. Workflow result collection remains synchronous at its API, while model delegation leaves the parent's tool step promptly. External children share accounting and durable discovery but gain no continuation or independent result archive. Restoring foreground or Job-backed local execution would require a concrete capability that result collection through an activation cannot provide. + +## Verification + +Focused tests cover local spawn/fork inheritance, structured capture and schema-free cold resume, explicit cancellation, external catalog membership, result-versus-disposal ordering, workflow collection without parent notices, and headless child-tree completion. Existing replay generations are preserved; updated recorded-session cases cover the current model-visible tool and notice behavior. diff --git a/.agents/notes/implemented/simplification/2026-09-17-unified-subagent-activations.zh.md b/.agents/notes/implemented/simplification/2026-09-17-unified-subagent-activations.zh.md new file mode 100644 index 0000000000..66c0ed81f6 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-09-17-unified-subagent-activations.zh.md @@ -0,0 +1,49 @@ +# Agent Note: 统一 subagent activation + +Status: implemented + +[English](2026-09-17-unified-subagent-activations.md) | 中文 + +## 问题 + +本地一次性执行与可继续驻留分别拥有子级创建、取消、结构化捕获、结果和清理。前台与 Job 支持的工具路径又增加了所有权路径。工作流仍需等待结果,而外部后端需要一次进程执行,无需获得多轮协议。 + +## 决策 + +`startActivation()` 是所有提供方的消费者入口。已发布句柄暴露子级身份、结果 promise 和对应 activation 的资源释放。调用方信号仅取消未发布的创建。本地结果等待待处理输入与所拥有的后代结束后才关闭准入,使后代回复能够进入最终答案。本地与外部结果 promise 都在清理和通知结束后完成。父级使用同一个完成 promise 等待;清理失败不覆盖已捕获的执行结果,由 disposal 单独报告。工作流消费者本就等待清理后再继续,因此 activation 所有权不需要独立的结果就绪与释放信号。 + +本地 spawn 和 fork 提供方只贡献 `prepareContinuable()` 数据,由 subagent 服务拥有 Agent 创建与驻留。独立进程内驱动器包被移除。结构化工具、校验、指令与终止保护附着于一次 activation,并随其关闭。捕获后该 activation 拒绝继续输入;冷恢复重建普通对话,不携带此前的 schema。 + +ACP、DSH SDK、Codex 和 Claude Code 保留单次执行适配器,共享 activation 容量与所有权,不接受后续消息,也不伪造本地子 Session。父级拥有的 catalog 登记所有本地子级,以及 parent 交付的外部叶子项。本地成员关系独立于结果交付,让已完成的工作流 Session 及其后代仍可被发现。caller 交付由消费方跟踪外部成员关系。外部条目携带 `mode: 'external'`,因为没有可打开的本地 Session。创建时记录一次成员关系;执行与清理不更新目录。执行状态没有必需的消费者,不属于成员记录。caller 交付返回完整结果,parent 交付将完整完成通知加入父级队列。 + +面向模型的工具始终返回后台子级 ID,并承诺管理器的完成通知。它没有前台开关或 Job 集成。工作流选择调用方投递,等待 activation 结果并在完成前释放句柄;它们不向父子交互添加完成通知或初始返回指导。Headless 完成流程等待其自身子树与后续父级轮次。 + +父级完成通知会附带最终文本,不受 `send_message` 是否可用或是否调用影响。工具可用不能证明子级已经发送答案:它可能只汇报进展,也可能未调用工具便结束。已经通过工具发送的答案可能出现两次;接受这类重复,可以避免让结果投递依赖模型遵循指令或额外的投递台账。 + +### 保留的决策 + +原生 Session 读取器对本地与外部目录条目都拒绝重复的子级成员记录。 + +[可继续驻留](../feature/2026-07-28-continuable-subagent-conversations.zh.md)、[fork 请求前缀](../architecture/2026-08-10-fork-children-stay-one-shot.zh.md)与[父级拥有的目录](../architecture/2026-09-01-parent-owned-subagent-catalog.zh.md)保留各自独立理由。具名提供方、结果投递与 activation 容量的当前约定见 [subagent 包文档](../../../../packages/subagent/subagent/README.zh.md);相应的[能力 seam](../../archived/feature/2026-06-21-subagent-capability-seam.md)、[结算投递](../../archived/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md)与[容量](../../archived/feature/2026-09-15-continuable-activation-capacity.md)记录作为历史快照保留。本决策拥有共享入口、外部执行参与以及调用方与父级之间的结果投递选择。 + +已发布 Session 世代保持不可变。历史一次性描述符仍可读取;移除当前执行路径不构成改写或丢弃持久历史的理由。 + +## 考虑过的替代方案 + +**保留独立的外部执行投影。** 它重复父级 catalog 已提供的成员登记、恢复与客户端更新,每次目录快照还会重传全部结果。catalog 仅保留成员关系,完整结果由接收方负责。 + +**为同步工作流保留本地一次性执行。** 等待结果是消费者需求。仅为提供 promise 而维护第二套 Agent 生命周期会重复取消与清理;activation 可以直接提供该 promise。 + +**为所有外部后端增加多轮支持。** 这会扩展后端协议、恢复和经过授权的路由,却不是共享所有权的必要条件。适配器保留现有执行能力。 + +**让所有后台子级使用 Jobs。** Jobs 在 activation 驻留和 Agent 收件箱之外增加第二套工作注册表与取消权限。子级发现和完成已属于 subagent 服务。 + +**把每个结果都发送给父级。** 工作流已经消费并展示子级结果;再次唤醒会重复上下文,也可能干扰正在等待工作流工具的父级。投递是显式消费者选择,面向模型工具的通知仍无条件提供。 + +## 后果 + +同一个管理器控制所有提供方的准入、取消、所有权和资源释放。工作流 API 仍同步收集结果,模型委派则及时结束父级工具 step。外部子级获得共享计数与持久发现,但不获得继续执行能力或独立结果档案。恢复前台或 Job 支持的本地执行,需要 activation 结果收集无法提供的具体能力。 + +## 验证 + +聚焦测试覆盖本地 spawn/fork 继承、结构化捕获与无 schema 冷恢复、显式取消、外部目录成员关系、结果与释放顺序、不发父级通知的工作流收集,以及 headless 子树完成。既有回放世代得到保留;更新的录制会话用例覆盖当前面向模型的工具与通知行为。 diff --git a/.agents/notes/implemented/simplification/2026-10-05-native-or-ptc-presentation.i18n.yaml b/.agents/notes/implemented/simplification/2026-10-05-native-or-ptc-presentation.i18n.yaml new file mode 100644 index 0000000000..ed1a46d891 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-10-05-native-or-ptc-presentation.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-10-05-native-or-ptc-presentation.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-10-05-native-or-ptc-presentation.md +/agent-note-one-tool-invocation-form-per-agent: + en: d45e87191f6e7667 + zh: 4350b7a9ff83b310 +/agent-note-one-tool-invocation-form-per-agent/problem: + en: 55d25e331b37b260 + zh: d1b091a38d1b106d +/agent-note-one-tool-invocation-form-per-agent/decision: + en: 42e5b09fbab63e84 + zh: 883ab2072a60a138 +/agent-note-one-tool-invocation-form-per-agent/alternatives-considered: + en: 62b0c9c4470b241d + zh: 4cf40e5dc6eebd63 +/agent-note-one-tool-invocation-form-per-agent/consequences: + en: a08a45bec640e3f6 + zh: 9f61a47d8133f3ee diff --git a/.agents/notes/implemented/simplification/2026-10-05-native-or-ptc-presentation.md b/.agents/notes/implemented/simplification/2026-10-05-native-or-ptc-presentation.md new file mode 100644 index 0000000000..97068af298 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-10-05-native-or-ptc-presentation.md @@ -0,0 +1,25 @@ +# Agent Note: One tool invocation form per agent + +Status: implemented + +English | [中文](2026-10-05-native-or-ptc-presentation.zh.md) + +## Problem + +Mixed tool presentation carries direct function schemas and a program SDK for the same capabilities. It adds schema-ordering, admission, configuration, and test combinations without a separate execution capability. The [PTC decision](../feature/2026-06-15-ptc.md) still owns the registry, runtime, and nested-call rules. + +## Decision + +Each agent selects `native` or `ptc`. Native presentation exposes visible capability schemas. PTC presentation exposes only `run_code` and the generated SDK. The registry rejects `both` in configuration. Presets can select different supported modes in one application. + +## Alternatives considered + +**Keep mixed presentation.** This lets one agent choose direct calls or programs for each step, but duplicates the advertised capability information and preserves a third admission case. Separate native and PTC agents provide the supported choices. + +**Silently map mixed presentation to another mode.** Either mapping changes how existing programs or direct calls execute. Explicit configuration rejection and the [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode/guide.md) make that choice visible. + +## Consequences + +An agent cannot alternate between direct end-tool calls and `run_code` through one mixed presentation. Reintroduction requires a concrete need for that behavior and evidence that it warrants the additional interface. + +Tests for shared policy, cancellation, nested context, and multimodal results use native or PTC setups. Older Session generations and explicitly retained recordings keep their original bytes and remain readable. The removal changes accepted configuration, not the plugin-author registration API or the guarded execution pipeline. diff --git a/.agents/notes/implemented/simplification/2026-10-05-native-or-ptc-presentation.zh.md b/.agents/notes/implemented/simplification/2026-10-05-native-or-ptc-presentation.zh.md new file mode 100644 index 0000000000..f6950cb50b --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-10-05-native-or-ptc-presentation.zh.md @@ -0,0 +1,25 @@ +# Agent Note: 每个 agent 使用一种工具调用形式 + +Status: implemented + +[English](2026-10-05-native-or-ptc-presentation.md) | 中文 + +## Problem + +混合工具呈现为同一组能力同时携带直接函数 schema 和程序 SDK。它增加了 schema 排序、准入、配置和测试组合,却没有提供独立的执行能力。[PTC 决策](../feature/2026-06-15-ptc.zh.md) 仍负责注册表、运行时和嵌套调用规则。 + +## Decision + +每个 agent 选择 `native` 或 `ptc`。原生呈现公开可见能力的 schema。PTC 呈现仅公开 `run_code` 和生成的 SDK。注册表拒绝配置中的 `both`。同一应用中的不同 preset 可以选择不同的受支持模式。 + +## Alternatives considered + +**保留混合呈现。** 这允许同一个 agent 在每一步选择直接调用或程序,但会重复通告能力信息,并保留第三种准入情况。独立的原生与 PTC agent 提供受支持的选择。 + +**将混合呈现静默映射到另一种模式。** 任一映射都会改变现有程序或直接调用的执行方式。明确拒绝配置,并提供[升级指南](../../../../docs/upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode/guide.zh.md),可以让这一选择可见。 + +## Consequences + +agent 不能通过一种混合呈现在直接调用具体工具与 `run_code` 之间切换。重新引入该模式,需要存在具体的行为需求,并有证据表明值得增加这种接口。 + +共享策略、取消、嵌套上下文和多模态结果测试使用原生或 PTC 配置。较早的 Session 代际及显式保留的记录保持原始字节,并且仍可读取。移除该模式改变的是受支持的配置,而非插件作者使用的注册 API 或受控执行流水线。 diff --git a/.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.i18n.yaml b/.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.i18n.yaml index 4bf28f6452..d03a5ff8c5 100644 --- a/.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.i18n.yaml @@ -9,8 +9,8 @@ en: 00ba1f66195351e2 zh: a738b81adceda351 /agent-note-required-ci-performance-gate-for-opening-large-sessions/decision: - en: 0a483af531d0dc46 - zh: 466a489dcb53dc60 + en: 4f94b61c48cceaf4 + zh: dbb51bfe1fb0f681 /agent-note-required-ci-performance-gate-for-opening-large-sessions/calibration-evidence: en: f1001a139470f6b7 zh: b7b46f96b84b1271 diff --git a/.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.md b/.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.md index dd1d0d876e..926203ea5b 100644 --- a/.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.md +++ b/.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.md @@ -12,7 +12,7 @@ Measuring only `SessionPersistence.open()` does not stably describe the result f ## Decision -Linux pull requests run a required `node 24 / benchmarks` job that executes `pnpm run check:ci:bench` → `pnpm run test:bench`. The private `@deepseek-ai/dsh-benchmarks` workspace owns benchmark-only dependencies. The command first builds workspace libraries and dedicated workers under `benchmarks/.dsh-build/`, then invokes `vitest.bench.config.ts`. The [standard hosted runner reference](../../../../.github/workflows/ci.yml) owns runner selection and the outer job timeout. The job runs the benchmark lane alone; Vitest runs one file at a time and only prepares input, starts measurement children, aggregates results, and enforces budgets. Every timed Node CPU path executes compiled JavaScript under plain Node with `NODE_OPTIONS` removed and no TypeScript loader; bare workspace imports therefore resolve from `benchmarks/node_modules` through package exports to built `lib/` entries. +Linux pull requests run a required `node 24 / benchmarks` job that executes `pnpm run check:ci:bench` → `scripts/run-ci-bench.ts`. Its disposable library build omits repeated TypeScript diagnostics under the [PR artifact build policy](../process/2026-10-05-pr-artifact-typechecks.md); required full builds retain them. The private `@deepseek-ai/dsh-benchmarks` workspace owns benchmark-only dependencies. The command first builds workspace libraries and dedicated workers under `benchmarks/.dsh-build/`, then invokes `vitest.bench.config.ts`. The [standard hosted runner reference](../../../../.github/workflows/ci.yml) owns runner selection and the outer job timeout. The job runs the benchmark lane alone; Vitest runs one file at a time and only prepares input, starts measurement children, aggregates results, and enforces budgets. Every timed Node CPU path executes compiled JavaScript under plain Node with `NODE_OPTIONS` removed and no TypeScript loader; bare workspace imports therefore resolve from `benchmarks/node_modules` through package exports to built `lib/` entries. Required performance gates live under top-level `benchmarks/`, grouped by measured user path rather than package ownership. Host files use `*.bench.ts`, Client-face files use `*.bench.client.ts`, and scenario-specific workers and fixtures stay beside their benchmark without a benchmark suffix. Package-local `.perf.ts` files remain non-gating diagnostics; `scripts/` owns orchestration rather than benchmark cases. diff --git a/.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.zh.md b/.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.zh.md index 226d3a709f..e8a64dca20 100644 --- a/.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.zh.md +++ b/.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.zh.md @@ -12,7 +12,7 @@ Session format v2 的推出改变了两条成本随模型输出增长的路径 ## 决定 -Linux pull request 运行必需的 `node 24 / benchmarks` job,执行 `pnpm run check:ci:bench` → `pnpm run test:bench`。私有 `@deepseek-ai/dsh-benchmarks` workspace 拥有 benchmark 专属依赖。该命令先构建 workspace library 和 `benchmarks/.dsh-build/` 下的专用 worker,再调用 `vitest.bench.config.ts`。[标准托管运行器说明](../../../../.github/workflows/ci.yml)拥有运行器选择及外层 job 超时。该 job 单独运行 benchmark lane;Vitest 逐文件运行,只负责准备输入、启动测量子进程、汇总结果和执行预算断言。每条被计时的 Node CPU 路径都以纯 Node 执行编译后的 JavaScript,并移除 `NODE_OPTIONS` 且不加载 TypeScript runtime;workspace 裸导入因此从 `benchmarks/node_modules` 通过 package exports 解析到构建后的 `lib/` 入口。 +Linux pull request 运行必需的 `node 24 / benchmarks` job,执行 `pnpm run check:ci:bench` → `scripts/run-ci-bench.ts`。其临时 library 构建按 [PR 产物构建政策](../process/2026-10-05-pr-artifact-typechecks.zh.md)省略重复的 TypeScript 诊断;必需的完整构建保留这些诊断。私有 `@deepseek-ai/dsh-benchmarks` workspace 拥有 benchmark 专属依赖。该命令先构建 workspace library 和 `benchmarks/.dsh-build/` 下的专用 worker,再调用 `vitest.bench.config.ts`。[标准托管运行器说明](../../../../.github/workflows/ci.yml)拥有运行器选择及外层 job 超时。该 job 单独运行 benchmark lane;Vitest 逐文件运行,只负责准备输入、启动测量子进程、汇总结果和执行预算断言。每条被计时的 Node CPU 路径都以纯 Node 执行编译后的 JavaScript,并移除 `NODE_OPTIONS` 且不加载 TypeScript runtime;workspace 裸导入因此从 `benchmarks/node_modules` 通过 package exports 解析到构建后的 `lib/` 入口。 必需性能 gate 位于顶层 `benchmarks/`,按被测用户路径而非 package 归属组织。Host 文件使用 `*.bench.ts`,Client 面文件使用 `*.bench.client.ts`,场景专属 worker 与 fixture 留在对应 benchmark 旁且不带 benchmark 后缀。包内 `.perf.ts` 文件仍是非门禁诊断;`scripts/` 负责编排而不承载 benchmark case。 diff --git a/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.i18n.yaml b/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.i18n.yaml index 4dc12c0baf..5669420993 100644 --- a/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.i18n.yaml @@ -9,20 +9,26 @@ en: 9bf1200b5f980008 zh: d2dc07922817c581 /agent-note-frontend-large-session-performance-budgets/decision: - en: 8603790da3e6f1b8 - zh: aa7bda7b42c60984 + en: db46b4d42d64aa1b + zh: a21caffa675e8b42 /agent-note-frontend-large-session-performance-budgets/calibration: - en: f3be2a0d346e80cc - zh: c49cc56e7714f68c + en: 2bbee3ec0bc9e618 + zh: b4f937d7f4f09ae5 /agent-note-frontend-large-session-performance-budgets/calibration/actual-ci-runs: en: d4d6aebafcdb8c12 zh: 112d3ca1a4ea8ae1 /agent-note-frontend-large-session-performance-budgets/calibration/standard-hosted-expectations-and-input-scheduling: en: 346cd1b3d5acf0ea zh: 42e08cd387f982c8 +/agent-note-frontend-large-session-performance-budgets/calibration/older-page-control-preparation: + en: de392f6c19df72ee + zh: 32cbabf9ebcacbcc +/agent-note-frontend-large-session-performance-budgets/calibration/trajectory-readiness-polling: + en: bdcfefd7358e335b + zh: f718f66ca5944203 /agent-note-frontend-large-session-performance-budgets/alternatives-considered: - en: 1472bd425ef0daec - zh: a89811758d70331e + en: 4d2a4a64ccd14507 + zh: 8a4aaf4d4abea13d /agent-note-frontend-large-session-performance-budgets/consequences: en: 3f4f510612875d54 zh: 3a4af464f159bfe8 diff --git a/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.md b/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.md index 36cd7458b3..c6c53c364e 100644 --- a/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.md +++ b/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.md @@ -14,7 +14,7 @@ The existing serial benchmark inventory includes two frontend owners: [active re `build:bench` keeps the Node-only library and worker build. `test:bench` additionally builds the Web shell before running all cases; the required benchmark CI job follows the [standard hosted runner reference](../../../../.github/workflows/ci.yml), unconditionally provisions Chromium and its Linux dependencies on that hosted runner, and enables the existing verbose gate output so successful raw samples remain available for calibration. Browser cases reuse the shipped-composition Web scaffold with private temporary roots and an atomically assigned loopback port. Only the nondeterministic model is replaced by synthetic replay. The scaffold Host runs under the existing Vitest source resolver; measured Client rendering runs built bundles in fresh Chromium processes. Browser wall times therefore include this test Host, transport, Playwright actionability, and rendering, and are not claims about a published Host process. -The browser input contains 240 closed turns, 40 tool results, and 20 code fences, plus mixed-language prose and reasoning. Historical Assistant records carry matching compact streams built through the production accumulator with 12-character reasoning/text deltas and 8-character tool-argument deltas; empty streams would omit stored and transferred payload costs. Nine older-page actions exhaust this input from its observed 25-turn initial window; the readiness probe follows mounted turn growth rather than duplicating the pagination algorithm. Each sample uses a fresh scaffold and browser. Setup, seeding, browser launch, initial shell load, and sidebar expansion are excluded from open timing. Open ends at transcript availability and an editable composer; page and navigation timings end at their target DOM state. Two animation frames include a rendering opportunity, not hardware presentation or a guarantee that every offscreen node painted. +The browser input contains 240 closed turns, 40 tool results, and 20 code fences, plus mixed-language prose and reasoning. Historical Assistant records carry matching compact streams built through the production accumulator with 12-character reasoning/text deltas and 8-character tool-argument deltas; empty streams would omit stored and transferred payload costs. Nine older-page actions exhaust this input from its observed 25-turn initial window; the readiness probe follows mounted turn growth rather than duplicating the pagination algorithm. Each sample uses a fresh scaffold and browser. Setup, seeding, browser launch, initial shell load, and sidebar expansion are excluded from open timing. Paging resolves exactly one current `Load earlier` control before timing, excluding its read-only accessibility lookup. The 875 ms paging ceiling includes actionability, the trusted click, mounted turn-tail growth and the final two animation frames; a replaced control fails. Open ends at transcript availability and an editable composer; page and navigation timings end at their target DOM state. Two animation frames include a rendering opportunity, not hardware presentation or a guarantee that every offscreen node painted. The continuation sends 120 text deltas at 16 ms replay pacing. The input witness is installed before Enter submission from the focused composer; typing retains focus without a mouse-refocus action or a separate pre-input animation-frame wait. First/final marker lookups sample visible text inside the latest Assistant step on animation frames, avoiding selector retry backoff. Hidden text and markers in older steps cannot satisfy the observer. The first observation captures reply markers and focus in the browser; diagnostics are retrieved after typing to avoid an additional pre-input round trip. Diagnostics also capture browser-clock timestamps and focus at the first actual input event. Replay never waits for input; starvation can still fail overlap. The synchronous input witness reads that same bounded reply. Whole-history text and accessibility queries add observer CPU and garbage collection to the measured interval, so reducing that observer work is benchmark repair, not product optimization. It records Enter-to-first-visible-reply, trusted draft typing whose first actual input event observes the first reply but no completion marker, complete reply wall time through settled persistence and the new rendered turn-tail, and Chromium main-thread task duration. The complete wall budget adds the fixed 1984 ms scripted pacing to a scaled overhead allowance; input and completion have their own enforced budgets. Post-GC browser heap and DOM counts remain diagnostics because one endpoint does not prove a leak. @@ -22,7 +22,7 @@ Reconnect uses three fresh compiled plain-Node children. Each creates a 100,000- ## Calibration -Three-sample medians on the arm64 reference machine, Node 24.19 and Chromium 149.0.7827.55, at the PR #3537 merge, establish the baseline below. An isolated repeat follows a complete workflow smoke. Each browser sample reports raw endpoint values and every page; the paging verdict uses the median of the sample maxima. Reconnect reports all child measurements. The following historical reference table uses 8 ms replay pacing and includes a two-frame wait in first-reply timing. Standard-hosted open, paging, Trajectory, and reconnect expectations are recorded separately below; other source reference constants retain these allowances. The bounded-observer 261.60 ms paging median exceeds its 260 ms reference allowance but remains below its 650 ms CI limit; the shared 2× time scale and 1.25× variance allowance produce CI limits. Memory uses only variance allowance. The shared scale originates in Node CI calibration. Both actual x64 browser runs below pass the fixed budgets on unchanged benchmark code; this supplies repeated-run evidence for these runners, not a universal browser speed ratio. +Three-sample medians on the arm64 reference machine, Node 24.19 and Chromium 149.0.7827.55, at the PR #3537 merge, establish the baseline below. An isolated repeat follows a complete workflow smoke. Each browser sample reports raw endpoint values and every page; the paging verdict uses the median of the sample maxima. Reconnect reports all child measurements. The following historical reference table uses 8 ms replay pacing and includes a two-frame wait in first-reply timing. Its paging samples include the accessibility lookup of the older-page control. Standard-hosted open, paging, Trajectory, and reconnect expectations are recorded separately below; other source reference constants retain these allowances. The bounded-observer 261.60 ms paging median exceeds its 260 ms reference allowance but remains below its 650 ms CI limit; the shared 2× time scale and 1.25× variance allowance produce CI limits. Memory uses only variance allowance. The shared scale originates in Node CI calibration. Both actual x64 browser runs below pass the fixed budgets on unchanged benchmark code; this supplies repeated-run evidence for these runners, not a universal browser speed ratio. | Endpoint | Measured median | Reference allowance | Historical CI limit | |---|---:|---:|---:| @@ -72,6 +72,20 @@ A controlled mouse-refocus delay waits for the real DONE marker without pausing The current Trajectory acceptance limit is 650 ms, represented by a 520 ms target with the shared 1.25× headroom. Run 35095609422 records medians of 666.651723 ms and 630.843184 ms on its original attempt and retry. This explicit 4% relaxation of the former 625 ms limit accepts the retry median but still rejects the original median; it is a budget decision, not a measured product speedup. Recorded retry samples exercise the accepted range, and the same verdict assertion rejects 651 ms. Other endpoint budgets and workload parameters remain unchanged. +### Older-page control preparation + +Older-page measurements wait for a uniquely named attached control before capturing it. Availability and read-only accessibility lookup are preparation; visibility, enabled/stable actionability, the trusted click, turn-tail growth and the final two frames remain timed. The historical 260 ms reference allowance, 261.60 ms median and 700 ms hosted expectation include the lookup that this endpoint excludes. + +Local samples with lookup excluded are 141.202083/126.114041/140.003334 ms, median 140.003334 ms. Hosted run 37272669722, benchmark job 111642813553 records 524.083466/355.648835/349.959862 ms, median 355.648835 ms, under the same 875 ms ceiling. These are observed samples, not a new calibration; the 700 ms expectation and 875 ms ceiling remain conservative retained limits pending repeated hosted measurements of this endpoint. The earlier same-checkout 887.624/441.989 ms CI medians include lookup and establish retry-cleared failure, not the composition of the first miss. + +### Trajectory readiness polling + +Trajectory activation samples its visible searchbox and a visible record inside `[data-trajectory-scroll]` on animation frames. Playwright's selector polling uses increasing delays up to 500 ms, so DOM readiness can precede the observer by hundreds of milliseconds. The trusted tab click, its actionability checks and the final two-frame wait remain timed; workload and the current 650 ms budget are unchanged. + +A controlled macOS arm64 diagnostic on Node 24.19.0, Chromium 149.0.7827.55 and Playwright 1.61.1 makes the searchbox and record ready after 300 or 350 ms. Selector waits take 793.082/791.053 ms and observe readiness 493.5/439.2 ms late; frame sampling takes 307.324/357.423 ms and observes it 8.1/7.9 ms late. This isolates conditional observer delay, not product latency or the cause of the first #4119 CI miss. + +Complete local 240-turn runs produce Trajectory samples of 157.795/146.737/148.571 ms before the observer change and 130.184/191.247/157.543 ms afterward, with medians 148.571 and 157.543 ms. Both runs pass every fixed endpoint budget; no local speedup or hosted calibration is claimed. The readiness regression rejects a hidden searchbox or record despite a visible unrelated table row; restoring the original selector waits makes that test fail. + ## Alternatives considered **Use the Node fold as paint evidence.** Rejected because it never performs DOM mutation, layout, or browser scheduling. The focused reconnect case likewise makes no GUI speed claim. @@ -82,6 +96,8 @@ The current Trajectory acceptance limit is 650 ms, represented by a 520 ms targe **Search the entire loaded history for every stream marker.** Rejected because Playwright injects text and accessibility scans into the same renderer whose CPU the benchmark measures. Scoping queries to the composer and latest Assistant preserves visible completion checks without making observer cost proportional to loaded history. +**Raise the Trajectory budget or retry failed samples.** This retains the measured selector backoff and can accept slower product behavior. Frame-based readiness removes that observer delay without changing the budget. + **Measure stream CPU alone.** Rejected because transport stalls and final-settlement delays can leave main-thread CPU low. The independent input, first-reply, and complete-wall budgets cover those waits. ## Consequences diff --git a/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md b/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md index 8a6bbaff8e..f2127f195b 100644 --- a/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md +++ b/.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md @@ -14,7 +14,7 @@ Node 对话折叠很快,并不能证明浏览器能绘制长对话或在流式 `build:bench` 保留仅 Node 的 library 与 worker 构建。`test:bench` 额外构建 Web shell 后再运行所有用例;必需的基准 CI job 遵循[标准托管运行器说明](../../../../.github/workflows/ci.yml),在该托管运行器上无条件安装 Chromium 及其 Linux 依赖,并启用现有门禁详细输出,使成功用例的原始样本可用于校准。浏览器用例复用产品组合的 Web scaffold,使用私有临时目录和原子分配的回环端口。只有不确定的模型被合成重放替代。scaffold Host 通过现有 Vitest 源码解析器运行;被测 Client 渲染在全新 Chromium 进程中执行构建后的 bundle。因此浏览器壁钟时间包含测试 Host、传输、Playwright 可交互性等待及渲染,不代表发布版 Host 进程。 -浏览器输入包含 240 个已关闭轮次、40 个工具结果和 20 个代码块,以及混合语言正文和推理。历史 Assistant 记录携带匹配的紧凑 stream,通过生产 accumulator 按 12 字符推理/文本 delta 和 8 字符工具参数 delta 构建;空 stream 会遗漏存储与传输负载成本。从观察到的初始 25 轮窗口开始,九次更早分页操作读完该输入;就绪探针跟踪已挂载轮次增长,不复制分页算法。每个样本使用全新 scaffold 和浏览器。环境准备、数据播种、浏览器启动、初始 shell 加载及侧栏展开不计入打开时间。打开测量在对话可用且输入框可编辑时结束;分页与导航测量在目标 DOM 状态出现时结束。两次动画帧包含一次渲染机会,不代表硬件显示或保证每个屏幕外节点都已绘制。 +浏览器输入包含 240 个已关闭轮次、40 个工具结果和 20 个代码块,以及混合语言正文和推理。历史 Assistant 记录携带匹配的紧凑 stream,通过生产 accumulator 按 12 字符推理/文本 delta 和 8 字符工具参数 delta 构建;空 stream 会遗漏存储与传输负载成本。从观察到的初始 25 轮窗口开始,九次更早分页操作读完该输入;就绪探针跟踪已挂载轮次增长,不复制分页算法。每个样本使用全新 scaffold 和浏览器。环境准备、数据播种、浏览器启动、初始 shell 加载及侧栏展开不计入打开时间。分页计时前解析恰好一个当前的 `Load earlier` 控件,将只读的无障碍查找归入准备。875 ms 分页上限仍包含可操作性等待、受信任的点击、已挂载 turn-tail 增长及最后两次动画帧;控件被替换时失败。打开测量在对话可用且输入框可编辑时结束;分页与导航测量在目标 DOM 状态出现时结束。两次动画帧包含一次渲染机会,不代表硬件显示或保证每个屏幕外节点都已绘制。 续接以 16 ms 重放间隔发送 120 个文本 delta。输入观察器在从已聚焦输入框按 Enter 提交前安装;键入保留焦点,不执行鼠标重新聚焦,也不单独等待输入前动画帧。首段/最终标记查找在动画帧上采样最新 Assistant step 内的可见文本,避免选择器重试退避。隐藏文本和较早步骤中的标记均不能满足观察条件。首次观察在浏览器内记录回复标记与焦点;诊断在键入后取回,避免增加输入前的通信往返。诊断还记录首个实际输入事件的浏览器时钟时间戳与焦点。重放从不等待输入;响应阻塞仍可能导致重叠失败。同步输入证据读取同一个受限回复。全历史文本与无障碍查询会向测量区间加入观察器 CPU 和垃圾回收成本,因此减少此类观察工作属于基准修正,而非产品优化。它记录 Enter 提交到首段可见回复的时间、首个实际输入事件观察到首段回复且完成标记尚未出现时的真实草稿键入、直到持久化结算并渲染新 turn-tail 的完整回复壁钟时间,以及 Chromium 主线程任务时间。完整壁钟预算在缩放后的额外开销额度上加固定的 1984 ms 脚本节奏;输入和完成均有独立执行的预算。强制 GC 后的浏览器 heap 和 DOM 数量仍仅供诊断,因为单个终点不能证明泄漏。 @@ -22,7 +22,7 @@ Node 对话折叠很快,并不能证明浏览器能绘制长对话或在流式 ## 校准 -在 arm64 参考机器、Node 24.19、Chromium 149.0.7827.55 和PR #3537 合并结果 上,三个样本的中位数建立下表基线。完整工作流 smoke 后执行一次隔离重复测量。每个浏览器样本报告原始终点数据和每一页;分页判定使用各样本最大值的中位数。重连报告全部子进程测量。下列历史参考表使用 8 ms 重放节奏,首段回复计时包含两帧等待。标准托管打开、分页、Trajectory 和重连预期在下文单独记录;其他源码参考常量保留这些额度。受限观察器的分页中位数 261.60 ms 超过 260 ms 参考额度,但仍低于 650 ms CI 限制;共享的 2× 时间倍率和 1.25× 方差余量产生 CI 限制。内存仅使用方差余量。共享倍率源自 Node CI 校准。下述两次实际 x64 浏览器运行在基准代码不变的情况下均通过固定预算;这提供这些 runner 的重复运行证据,而非普遍适用的浏览器速度比。 +在 arm64 参考机器、Node 24.19、Chromium 149.0.7827.55 和PR #3537 合并结果 上,三个样本的中位数建立下表基线。完整工作流 smoke 后执行一次隔离重复测量。每个浏览器样本报告原始终点数据和每一页;分页判定使用各样本最大值的中位数。重连报告全部子进程测量。下列历史参考表使用 8 ms 重放节奏,首段回复计时包含两帧等待。其中的分页样本包含较早分页控件的无障碍查找。标准托管打开、分页、Trajectory 和重连预期在下文单独记录;其他源码参考常量保留这些额度。受限观察器的分页中位数 261.60 ms 超过 260 ms 参考额度,但仍低于 650 ms CI 限制;共享的 2× 时间倍率和 1.25× 方差余量产生 CI 限制。内存仅使用方差余量。共享倍率源自 Node CI 校准。下述两次实际 x64 浏览器运行在基准代码不变的情况下均通过固定预算;这提供这些 runner 的重复运行证据,而非普遍适用的浏览器速度比。 | 终点 | 实测中位数 | 参考额度 | 历史 CI 限制 | |---|---:|---:|---:| @@ -72,6 +72,20 @@ Node 对话折叠很快,并不能证明浏览器能绘制长对话或在流式 当前 Trajectory 验收上限为 650 ms,由 520 ms 目标与共享的 1.25× 余量表示。运行 35095609422 的首次执行与重试分别记录中位数 666.651723 ms 和 630.843184 ms。相较于原先的 625 ms 上限,这次明确的 4% 放宽接受重试中位数,但仍拒绝首次中位数;这是预算决策,不代表测得产品提速。记录的重试样本覆盖新增的接受区间,同一个判定断言拒绝 651 ms。其他终点预算与工作负载参数保持不变。 +### 较早分页控件准备 + +较早分页测量先等待具有唯一名称的控件挂载,再捕获该控件。可用性与只读的无障碍查找归入准备;可见、启用及稳定等可操作性检查、受信任的点击、turn-tail 增长和最后两帧仍计时。历史 260 ms 参考额度、261.60 ms 中位数及 700 ms 托管预期包含当前终点已排除的查找。 + +排除查找后的本地样本为 141.202083/126.114041/140.003334 ms,中位数 140.003334 ms。托管运行 37272669722 的 benchmark job 111642813553 记录 524.083466/355.648835/349.959862 ms,中位数 355.648835 ms,仍通过相同的 875 ms 上限。这些是观察样本,不是新校准;700 ms 预期与 875 ms 上限作为保守的现有值保留,待此终点的重复托管测量核验。先前相同 checkout 的 887.624/441.989 ms CI 中位数包含查找,只建立重跑可清除的失败,不分解首次超限的组成。 + +### Trajectory 就绪轮询 + +Trajectory 激活在动画帧上采样可见搜索框与 `[data-trajectory-scroll]` 内的可见记录。Playwright 选择器轮询使用逐步增加、最高 500 ms 的延迟,因此 DOM 可能比观察器早数百毫秒就绪。真实 tab 点击、其可交互性检查与最后两帧等待仍计时;工作负载与当前 650 ms 预算不变。 + +在 macOS arm64、Node 24.19.0、Chromium 149.0.7827.55 和 Playwright 1.61.1 上,受控诊断使搜索框与记录在 300 或 350 ms 后就绪。选择器等待耗时 793.082/791.053 ms,晚 493.5/439.2 ms 观察到就绪;动画帧采样耗时 307.324/357.423 ms,晚 8.1/7.9 ms 观察到就绪。这隔离了有条件发生的观察器延迟,不代表产品延迟,也不能证明 #4119 首次 CI 超限的原因。 + +完整本地 240 轮运行在修改观察器前得到 Trajectory 样本 157.795/146.737/148.571 ms,修改后为 130.184/191.247/157.543 ms,中位数分别为 148.571 和 157.543 ms。两次运行均通过所有固定终点预算;不声称本地提速或托管环境校准。即使存在可见的无关表格行,就绪回归测试仍拒绝隐藏的搜索框或记录;恢复原选择器等待会使该测试失败。 + ## 考虑过的替代方案 **用 Node 折叠作为绘制证据。** 拒绝,因为它不执行 DOM 修改、布局或浏览器调度。聚焦重连用例同样不声称 GUI 提速。 @@ -82,6 +96,8 @@ Node 对话折叠很快,并不能证明浏览器能绘制长对话或在流式 **每次流式标记查找都搜索全部已加载历史。** 拒绝,因为 Playwright 把文本与无障碍扫描注入同一个被测 CPU 的渲染进程。将查询限定在输入框和最新 Assistant 中,可保留可见完成检查,同时避免观察器成本随已加载历史增长。 +**提高 Trajectory 预算或重试失败样本。** 这会保留已测得的选择器退避,并可能接受更慢的产品行为。基于动画帧的就绪观察移除该观察器延迟,不改变预算。 + **只测量流式 CPU。** 拒绝,因为传输停顿和最终结算延迟可能不增加主线程 CPU。独立的输入、首段回复和完整壁钟预算覆盖这些等待。 ## 影响 diff --git a/.agents/notes/implemented/testing/2026-10-07-memory-limited-benchmarks.i18n.yaml b/.agents/notes/implemented/testing/2026-10-07-memory-limited-benchmarks.i18n.yaml new file mode 100644 index 0000000000..d0b8ebff98 --- /dev/null +++ b/.agents/notes/implemented/testing/2026-10-07-memory-limited-benchmarks.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-10-07-memory-limited-benchmarks.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-10-07-memory-limited-benchmarks.md +/agent-note-memory-limited-benchmark-reruns: + en: 08979e022b49c675 + zh: b0c12b1f992ac681 +/agent-note-memory-limited-benchmark-reruns/problem: + en: b82519a7026a1a19 + zh: a72c4417426ec95f +/agent-note-memory-limited-benchmark-reruns/decision: + en: 3cf2db98939b3994 + zh: f80a1d3d6049381a +/agent-note-memory-limited-benchmark-reruns/decision/cases: + en: 9502c61025d27f23 + zh: f743a56404327f92 +/agent-note-memory-limited-benchmark-reruns/decision/limit-and-endpoint: + en: a314408967c5a450 + zh: 2b6ed3e1a96fe272 +/agent-note-memory-limited-benchmark-reruns/calibration-evidence: + en: ffe31b42aa199614 + zh: 29e4a3297ab55853 +/agent-note-memory-limited-benchmark-reruns/alternatives-considered: + en: 436516beabc2394f + zh: 7cabbecb5a05f77c +/agent-note-memory-limited-benchmark-reruns/consequences: + en: cb213ef0e7506c8f + zh: 3da65a3981c2518d diff --git a/.agents/notes/implemented/testing/2026-10-07-memory-limited-benchmarks.md b/.agents/notes/implemented/testing/2026-10-07-memory-limited-benchmarks.md new file mode 100644 index 0000000000..79cc9d918a --- /dev/null +++ b/.agents/notes/implemented/testing/2026-10-07-memory-limited-benchmarks.md @@ -0,0 +1,42 @@ +# Agent Note: Memory-limited benchmark reruns + +Status: implemented + +English | [中文](2026-10-07-memory-limited-benchmarks.zh.md) + +## Problem + +Benchmarks run on hosted runners with 16 GB of memory and on developer machines with more. Neither shows how DSH behaves on a machine with 4 GB or 8 GB available, where the Host, Chromium, and page cache share one memory budget. + +## Decision + +[run-memory-pressure-bench.ts](../../../../scripts/run-memory-pressure-bench.ts) builds the benchmark artifacts unconfined, then reruns a fixed list of existing benchmark files inside a cgroup v2 whose `memory.max` is 4 or 8 GiB and whose `memory.swap.max` is zero. The files keep their own workloads and budgets; the scenario fails when a case misses its normal budget or the cgroup kills a process. + +### Cases + +The list holds the files with the largest measured resident memory or the long-history UI path: [session-corpus](../../../../benchmarks/session-corpus/README.md) (list of 3,000 Sessions, content search over 1,000, fork), its projection-list file, [session-open](2026-09-04-session-open-performance-gate.md) (127,400-event Session), and [long-session-browser](../../../../benchmarks/long-session-browser/README.md) (Chromium and its Host). Terminal, fold, reconnect, and continuation files measure small in-process workloads and stay out. + +### Limit and endpoint + +The cgroup contains Vitest, every benchmark worker, the scaffold Host, and Chromium. Node 24 sizes its default heap from the cgroup limit, and page cache from Session logs is charged to the same limit, so eviction and rereads appear in the timings. On Linux hosts a `systemd-run --user --scope` provides a fresh cgroup. In a container that already enforces the limit, `--scoped` uses the container cgroup. Before running, the launcher requires `memory.max`, `memory.swap.max`, and `process.constrainedMemory()` to match the machine. After the run it reports the cgroup's `memory.peak` and `oom_kill`; both cover the cgroup's lifetime. + +## Calibration evidence + +On 2026-10-07, Docker on Apple silicon (Linux VM, `--cpus=4`, Node 24.21) ran the list once per limit and once without a limit. All 38 cases passed their budgets under both limits, with no OOM kill. + +| Measurement | No limit | 8 GiB | 4 GiB | +|---|---:|---:|---:| +| cgroup peak (MB) | 3,538 | 3,649 | 3,895 | +| List 3,000 first / repeat (ms) | 1,235 / 1,023 | 1,365 / 1,072 | 2,613 / 1,994 | +| Search 1,000 first (ms) | 54,015 | 50,766 | 61,662 | +| Browser slowest page / input (ms) | 152 / 95 | 144 / 91 | 187 / 123 | + +## Alternatives considered + +- `--max-old-space-size` per worker: the list and search workers use about 100 MB of JavaScript heap while their resident memory reaches 0.7–1.2 GB, mostly native memory, so a heap flag does not model the machine. Node already derives the same 2 GB old space for 4 GB and 8 GB cgroups. +- `RLIMIT_AS`: V8 reserves address space far beyond its resident memory and fails under a virtual-memory limit. +- A pull-request CI job: the PR workflow keeps at most ten concurrent worker jobs, and each memory-limited run takes as long as the benchmark lane, so the scenario is manual. + +## Consequences + +macOS has no per-process-tree memory cap; developers there run `--scoped` inside a Linux container started with `--memory` and `--memory-swap` equal to the limit. A file added to the list must keep its budgets valid under both limits. diff --git a/.agents/notes/implemented/testing/2026-10-07-memory-limited-benchmarks.zh.md b/.agents/notes/implemented/testing/2026-10-07-memory-limited-benchmarks.zh.md new file mode 100644 index 0000000000..f5a4db01a8 --- /dev/null +++ b/.agents/notes/implemented/testing/2026-10-07-memory-limited-benchmarks.zh.md @@ -0,0 +1,42 @@ +# Agent Note: 内存受限的基准重跑 + +Status: implemented + +[English](2026-10-07-memory-limited-benchmarks.md) | 中文 + +## 问题 + +基准运行在 16 GB 内存的托管 runner 和内存更大的开发机上。两者都无法显示 DSH 在可用内存为 4 GB 或 8 GB 的机器上的表现,而这类机器上 Host、Chromium 和页缓存共享同一份内存预算。 + +## 决定 + +[run-memory-pressure-bench.ts](../../../../scripts/run-memory-pressure-bench.ts) 先在无约束环境中构建基准产物,再在 `memory.max` 为 4 或 8 GiB、`memory.swap.max` 为零的 cgroup v2 中,重跑一份固定的现有基准文件列表。这些文件保留各自的负载和预算;某个用例未达到其常规预算或 cgroup 杀死进程时,该场景失败。 + +### 用例 + +列表包含常驻内存实测最大的文件和长历史 UI 路径:[session-corpus](../../../../benchmarks/session-corpus/README.zh.md)(3,000 个 Session 的列表、1,000 个 Session 的内容搜索、fork)、其 projection-list 文件、[session-open](2026-09-04-session-open-performance-gate.zh.md)(127,400 个事件的 Session)和 [long-session-browser](../../../../benchmarks/long-session-browser/README.zh.md)(Chromium 及其 Host)。终端、fold、重连和续写文件测量的是较小的进程内负载,不在列表中。 + +### 上限与端点 + +cgroup 包含 Vitest、每个基准 worker、脚手架 Host 和 Chromium。Node 24 按 cgroup 上限确定默认堆大小,Session 日志的页缓存也计入同一上限,因此淘汰和重读会体现在耗时中。在 Linux 主机上,`systemd-run --user --scope` 提供一个新的 cgroup。在已经执行该上限的容器中,`--scoped` 使用容器的 cgroup。运行前,启动脚本要求 `memory.max`、`memory.swap.max` 和 `process.constrainedMemory()` 与机器一致。运行后,它报告该 cgroup 的 `memory.peak` 和 `oom_kill`;两者覆盖 cgroup 的整个生命周期。 + +## 校准证据 + +2026-10-07,Apple silicon 上的 Docker(Linux VM,`--cpus=4`,Node 24.21)对每个上限各运行一次该列表,并在无上限时运行一次。两个上限下全部 38 个用例都通过了预算,没有 OOM kill。 + +| 测量项 | 无上限 | 8 GiB | 4 GiB | +|---|---:|---:|---:| +| cgroup 峰值(MB) | 3,538 | 3,649 | 3,895 | +| 列出 3,000 个:首次 / 重复(ms) | 1,235 / 1,023 | 1,365 / 1,072 | 2,613 / 1,994 | +| 搜索 1,000 个:首次(ms) | 54,015 | 50,766 | 61,662 | +| 浏览器最慢一页 / 输入(ms) | 152 / 95 | 144 / 91 | 187 / 123 | + +## 考虑过的替代方案 + +- 为每个 worker 设置 `--max-old-space-size`:列表和搜索 worker 使用约 100 MB JavaScript 堆,而常驻内存达到 0.7–1.2 GB,主要是原生内存,因此堆参数无法模拟机器。Node 已为 4 GB 和 8 GB cgroup 推导出相同的 2 GB old space。 +- `RLIMIT_AS`:V8 预留的地址空间远超其常驻内存,在虚拟内存上限下会失败。 +- PR CI 作业:PR 工作流最多保持十个并发 worker 作业,而每次内存受限运行与基准 lane 耗时相当,因此该场景为手动运行。 + +## 影响 + +macOS 没有按进程树的内存上限;那里的开发者在以等于上限的 `--memory` 和 `--memory-swap` 启动的 Linux 容器中运行 `--scoped`。加入列表的文件必须在两个上限下都保持预算有效。 diff --git a/.agents/notes/implemented/testing/2026-10-07-scaled-benchmark-report.i18n.yaml b/.agents/notes/implemented/testing/2026-10-07-scaled-benchmark-report.i18n.yaml new file mode 100644 index 0000000000..bd483649b5 --- /dev/null +++ b/.agents/notes/implemented/testing/2026-10-07-scaled-benchmark-report.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-10-07-scaled-benchmark-report.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-10-07-scaled-benchmark-report.md +/agent-note-scaled-machine-and-memory-pressure-benchmark-report: + en: 486b21866106fb62 + zh: eb0f2e90eeb426a3 +/agent-note-scaled-machine-and-memory-pressure-benchmark-report/problem: + en: d8e9dee3e22d691a + zh: 53d9fae666f032b7 +/agent-note-scaled-machine-and-memory-pressure-benchmark-report/decision: + en: 5587ef207abde949 + zh: a8c92e13b980c7c1 +/agent-note-scaled-machine-and-memory-pressure-benchmark-report/decision/machine-scaling: + en: f532650e0720e66a + zh: dfd6ffa6366c1f09 +/agent-note-scaled-machine-and-memory-pressure-benchmark-report/decision/memory-pressure: + en: bec0d7592ed58486 + zh: 2be2350c142f133d +/agent-note-scaled-machine-and-memory-pressure-benchmark-report/decision/exclusions: + en: dc685a11156d6cd3 + zh: 519bbdbd0b4c69d7 +/agent-note-scaled-machine-and-memory-pressure-benchmark-report/alternatives-considered: + en: 9011ce1b4702608d + zh: 214f38794afe13fd +/agent-note-scaled-machine-and-memory-pressure-benchmark-report/consequences: + en: af04c3e97e85f1e3 + zh: 533a73e06f708c9d diff --git a/.agents/notes/implemented/testing/2026-10-07-scaled-benchmark-report.md b/.agents/notes/implemented/testing/2026-10-07-scaled-benchmark-report.md new file mode 100644 index 0000000000..3b800774cc --- /dev/null +++ b/.agents/notes/implemented/testing/2026-10-07-scaled-benchmark-report.md @@ -0,0 +1,43 @@ +# Agent Note: Scaled machine and memory-pressure benchmark report + +Status: implemented + +English | [中文](2026-10-07-scaled-benchmark-report.zh.md) + +## Problem + +Benchmark budgets state whether one endpoint regressed on the hosted runner. They do not tell readers how slow the same endpoint is on an older laptop or a typical cloud instance, or whether its working set fits a machine with 4 GB or 8 GB of available memory, so a passing or failing number does not convey user impact. + +## Decision + +Every `test:bench` run writes `benchmarks/.dsh-report/report.html`, a self-contained page without network resources. Bench files pass each budgeted wall-clock median to `recordTimings` and whole-process peak RSS to `recordPeakMemory` from [scaling-report.ts](../../../../benchmarks/support/scaling-report.ts) before asserting budgets. Each call carries a case description stating what the case measures and which user scenario it affects; the page shows it when the reader hovers the case. A Vitest global setup clears the results before the run and renders the page after it, including after failed cases. Budgets and assertions do not change. + +### Machine scaling + +[machine-profiles.ts](../../../../benchmarks/support/machine-profiles.ts) lists laptops from 2015, 2019, 2023, and 2026 with SSD storage, mainstream cloud instances with their default system disks, the hosted runners, and the M4 Pro calibration reference. Each profile has a PassMark single-thread rating, retrieved on 2026-10-07, its logical CPU count, and a typical 4 KiB queue-depth-1 latency of its storage. A scaled endpoint is `ms × ((1 − io) × cpuScore(measured) / cpuScore(target) × coreFactor + io × ioLatency(target) / ioLatency(measured))`. + +Where the worker reports process CPU time over the endpoint interval, `io` is the measured share of wall time not spent on CPU, `max(0, 1 − cpuMs / ms)`, and the busy parallelism is `max(1, cpuMs / ms)`. The Session corpus, projection list, Session first history and Agent resume, and Agent continuation cases report it. Other endpoints declare a commented `io` beside the benchmark: in-memory Client and terminal work declares 0, and the browser, Session-open phase, and SDK profile endpoints declare estimates. `coreFactor` is `max(1, parallelism / cores(target)) / max(1, parallelism / cores(measured))`, so an endpoint that kept more CPUs busy than a target has slows down there. The page shows each default and its source, and readers can override and reset `io` per endpoint. The measuring machine is matched from the CPU model, uses its actual available CPU count, and can be changed in the page. + +On Apple M5 Pro the first content-index search measured 58.7 s; the model predicts 168 s on the EPYC 7763 runner, against a recorded hosted median of 144 s. + +### Memory pressure + +The Session corpus list, search, and fork cases report whole-process peak RSS for the largest synthetic working sets. The page shows each peak as a share of 4 GB and 8 GB of available memory. Peaks are not scaled. + +### Exclusions + +Long-Session `first`, `input`, and `streamWall` include paced replay waits and are not recorded. The browser page heap is read after a forced garbage collection, so it is not a peak and is not recorded. Memory size and thermal limits are not modelled. Measured non-CPU time also contains timers, event-loop yields, and child scheduling, and the measuring run reads a just-written corpus that the page cache mostly holds, so cold-cache storage waits on a user machine can be larger. Garbage-collection helper threads add CPU time, so measured parallelism is typically 1–2 and storage wait overlapping that work is not visible. Declared endpoints, including the browser case where Chromium and the Host share the machine, assume one busy CPU. + +## Alternatives considered + +**Run cases under an operating-system memory limit.** A cgroup limit needs root on Linux CI and is unavailable on macOS. The measured peaks stay below 1 GB, so a 4 GB limit would not change the timed path while adding privileged setup to a required gate. + +**Only declared storage shares.** Readers cannot judge a share they have to guess. Process CPU time is cheap to measure in the workers and gives a reproducible default, although the non-CPU time it leaves includes waits other than storage. + +**Benchmark-wide storage share.** Session open restores and projects after closing its file, so one share per benchmark overstates high-latency disks for memory-only endpoints. + +**Multi-core or Geekbench scores.** A multi-core score would overstate machines with many cores for endpoints dominated by one JavaScript thread; the measured busy parallelism applies core limits only where they matter. Geekbench pages were not retrievable for citation; PassMark single-thread ratings were. + +## Consequences + +Readers can see each endpoint on reviewed machines with measured defaults, and adjust the storage share, without changing any gate. The page depends on reviewed constants: cross-ISA ratings, shared vCPUs, and disk latency classes are coarse, so results are estimates rather than measurements. The memory view does not measure swap or reclaim costs. The `node 24 / benchmarks` job publishes the page as the `benchmark-report` artifact. diff --git a/.agents/notes/implemented/testing/2026-10-07-scaled-benchmark-report.zh.md b/.agents/notes/implemented/testing/2026-10-07-scaled-benchmark-report.zh.md new file mode 100644 index 0000000000..83182c650e --- /dev/null +++ b/.agents/notes/implemented/testing/2026-10-07-scaled-benchmark-report.zh.md @@ -0,0 +1,43 @@ +# Agent Note: 按机器缩放与内存压力的基准报告 + +Status: implemented + +[English](2026-10-07-scaled-benchmark-report.md) | 中文 + +## 问题 + +基准预算只说明某个端点是否在托管 runner 上回归。读者无法据此判断同一端点在旧笔记本或典型云服务器上有多慢,也无法判断其工作集能否放进 4 GB 或 8 GB 可用内存,因此通过或失败的数字无法体现对用户的影响。 + +## 决定 + +每次 `test:bench` 运行都会写出 `benchmarks/.dsh-report/report.html`,这是一个不依赖网络资源的独立页面。基准文件在断言预算之前,把每个有预算的墙钟中位数传给 [scaling-report.ts](../../../../benchmarks/support/scaling-report.ts) 的 `recordTimings`,把整进程峰值 RSS 传给 `recordPeakMemory`。每次调用都带有用例描述,说明该用例测量什么、影响哪个用户场景;读者悬停该用例时页面会显示它。Vitest global setup 在运行前清空结果,在运行后渲染页面,用例失败时也会渲染。预算和断言不变。 + +### 机器缩放 + +[machine-profiles.ts](../../../../benchmarks/support/machine-profiles.ts) 列出 2015、2019、2023、2026 年使用 SSD 的笔记本,带默认系统盘的主流云服务器,托管 runner,以及 M4 Pro 校准参考机。每个配置包含 2026-10-07 获取的 PassMark 单线程评分、逻辑 CPU 数,以及其存储的典型 4 KiB 队列深度 1 延迟。缩放后的端点为 `ms × ((1 − io) × cpuScore(measured) / cpuScore(target) × coreFactor + io × ioLatency(target) / ioLatency(measured))`。 + +当 worker 报告端点区间内的进程 CPU 时间时,`io` 是实测的非 CPU 墙钟时间占比 `max(0, 1 − cpuMs / ms)`,繁忙并行度为 `max(1, cpuMs / ms)`。Session 语料、投影列表、Session 首次历史与 Agent 恢复,以及 Agent 续聊用例会报告 CPU 时间。其他端点在基准旁声明并注释 `io`:内存中的 Client 与终端工作声明为 0,浏览器、Session-open 分阶段和 SDK profile 端点声明估计值。`coreFactor` 为 `max(1, parallelism / cores(target)) / max(1, parallelism / cores(measured))`,因此繁忙 CPU 数超过目标机器 CPU 数的端点会在该机器上变慢。页面显示每个默认值及其来源,读者可以按端点覆盖和重置 `io`。测量机器按 CPU 型号匹配,使用实际可用 CPU 数,也可以在页面中修改。 + +在 Apple M5 Pro 上,首次内容索引搜索实测 58.7 s;模型预测 EPYC 7763 runner 上为 168 s,托管 CI 记录的中位数为 144 s。 + +### 内存压力 + +Session 语料的列表、搜索和 fork 用例报告最大合成工作集的整进程峰值 RSS。页面把每个峰值显示为 4 GB 和 8 GB 可用内存的占比。峰值不做缩放。 + +### 排除项 + +长 Session 的 `first`、`input` 和 `streamWall` 包含按节奏回放的等待,不记录。浏览器页面堆在强制垃圾回收后读取,不是峰值,不记录。不建模内存容量和散热限制。实测的非 CPU 时间还包含定时器、事件循环让出和子进程调度;测量运行读取的是刚写入、主要位于页面缓存中的语料,因此用户机器上冷缓存的存储等待可能更长。垃圾回收辅助线程会增加 CPU 时间,因此实测并行度通常为 1–2,与这些工作重叠的存储等待不可见。声明的端点,包括 Chromium 与 Host 共用机器的浏览器用例,都假定只有一个繁忙 CPU。 + +## 备选方案 + +**在操作系统内存限制下运行用例。** Linux CI 上的 cgroup 限制需要 root,macOS 上不可用。实测峰值低于 1 GB,4 GB 限制不会改变计时路径,却会给必需门禁增加特权配置。 + +**只使用声明的存储占比。** 读者无法判断需要猜测的占比。在 worker 中测量进程 CPU 时间成本很低,并能给出可复现的默认值,尽管剩余的非 CPU 时间包含存储以外的等待。 + +**按基准统一存储占比。** Session open 在关闭文件后才执行恢复和投影,按基准使用一个占比会高估高延迟磁盘对纯内存端点的影响。 + +**多核或 Geekbench 分数。** 对于以单个 JavaScript 线程为主的端点,多核分数会高估多核机器;实测繁忙并行度只在相关时施加核心数限制。Geekbench 页面无法获取以供引用,PassMark 单线程评分可以获取。 + +## 影响 + +读者可以在经审阅的机器上查看每个端点及其实测默认值,并调整存储占比,而不改变任何门禁。页面依赖经审阅的常量:跨指令集评分、共享 vCPU 和磁盘延迟等级都较粗略,因此结果是估计值而不是测量值。内存视图不测量交换和回收成本。`node 24 / benchmarks` job 将该页面作为 `benchmark-report` 产物发布。 diff --git a/.agents/notes/proposed/feature/2026-08-04-task-surface.i18n.yaml b/.agents/notes/proposed/feature/2026-08-04-task-surface.i18n.yaml index 06ff6199f7..fbb5dea139 100644 --- a/.agents/notes/proposed/feature/2026-08-04-task-surface.i18n.yaml +++ b/.agents/notes/proposed/feature/2026-08-04-task-surface.i18n.yaml @@ -15,8 +15,8 @@ en: 9650ef8f65049b7d zh: c0925b701a6b681c /agent-note-task-surface-for-structured-session-interaction/tool-and-presentation-contract: - en: 96ce7e929e7e47ff - zh: d6813e83bf7bd7ee + en: 13f923b5958b5f6d + zh: c8aa92cac29769fd /agent-note-task-surface-for-structured-session-interaction/submission-contract: en: 480c434bc0825063 zh: bbcace61f8d82188 @@ -33,8 +33,8 @@ en: 121ceb5920763980 zh: 7d7c1576faf4f331 /agent-note-task-surface-for-structured-session-interaction/acceptance-criteria: - en: e5db6c885617dd5d - zh: 0a54a00d2a195bfb + en: 2b278bc21a87232a + zh: 49d0556ec77b542f /agent-note-task-surface-for-structured-session-interaction/risks: en: 84a297fcdef9aaad zh: a9ce38547d650e55 diff --git a/.agents/notes/proposed/feature/2026-08-04-task-surface.md b/.agents/notes/proposed/feature/2026-08-04-task-surface.md index 5799042443..bc8b0e9ae7 100644 --- a/.agents/notes/proposed/feature/2026-08-04-task-surface.md +++ b/.agents/notes/proposed/feature/2026-08-04-task-surface.md @@ -81,7 +81,7 @@ Limits are schema-backed configuration on the Task Surface service. The initial `show_task_surface` accepts `{ model: TaskSurfaceModelV1 }`. The Host parses and normalizes the complete model, rejects the call when that Session already has an open Task Surface, mints `surfaceId`, and returns canonical `{ surfaceId, model }` with the normalized model. `presentationMeta` persists `value.model`, so the projector and executor cannot disagree about normalization. The Native result names the Surface and explains that an ordinary message bypasses it when the client cannot render the panel. The tool then calls `exec.concludeTurn()` so the agent does not continue past the requested human checkpoint. -The tool definition omits `isConcurrencySafe`. Under the existing tool-registry contract, omission classifies every call as an exclusive ordering barrier; no new `ToolDefinition` field is introduced. The tool is composed only in Web profiles that mount both the Host service and Web renderer. Version 1 supports `native` and `both` tool modes; a `ptc`-only profile does not advertise it because PTC mode dispatch is nested and cannot carry its presentation metadata to the outer result. +The tool definition omits `isConcurrencySafe`. Under the existing tool-registry contract, omission classifies every call as an exclusive ordering barrier; no new `ToolDefinition` field is introduced. The tool is composed only in Web profiles that mount both the Host service and Web renderer. Version 1 supports `native` tool mode; a `ptc`-only profile does not advertise it because PTC mode dispatch is nested and cannot carry its presentation metadata to the outer result. The browser-safe domain package imports the type-only `Branded` primitive from `@deepseek-ai/dsh-brand` and owns all three Task Surface IDs. The canonical value is execution-local under the [canonical tool output contract](../../../../packages/core/tools/README.md). Replay therefore uses `output.presentationMeta(args, value)` to persist this tagged payload with `tool/result.meta`: @@ -248,7 +248,7 @@ The implementation depends on the existing message log, canonical tool output, t ## Acceptance criteria -- A real model in `native` or `both` mode can call one stable `show_task_surface` schema, the call ends its turn, and a capable Web client renders the same normalized model live and after replay; `ptc`-only mode does not advertise it. +- A real model in `native` mode can call one stable `show_task_surface` schema, the call ends its turn, and a capable Web client renders the same normalized model live and after replay; `ptc`-only mode does not advertise it. - The static `TaskSurfaceDock` is the only editor and remains actionable for an active result outside the loaded history window; the keyed toolview remains a read-only transcript summary and replay. A composer takeover hides the still-mounted Dock, preserves its draft, and reveals the same owner after release. - Submitting produces exactly one visible user message per `submissionId`, starts the next turn through normal queue admission, and retains exact branded occurrence correlation while keeping `source.kind: 'user'`; dismissing records one log event and starts no turn. - The queued client row retains the correlated message source. `getActive` exposes `queued` or `claiming` across same-process reconnect; commit closes the projection, while explicit discard clears pending state and leaves the Surface open. Queue-row disappearance alone changes no UI state. Edit and steer are rejected, and remove succeeds only before claim. diff --git a/.agents/notes/proposed/feature/2026-08-04-task-surface.zh.md b/.agents/notes/proposed/feature/2026-08-04-task-surface.zh.md index 6a1024ae8d..3bb982f032 100644 --- a/.agents/notes/proposed/feature/2026-08-04-task-surface.zh.md +++ b/.agents/notes/proposed/feature/2026-08-04-task-surface.zh.md @@ -81,7 +81,7 @@ Task Surface 服务通过受 schema 校验的配置定义限制。初始默认 `show_task_surface` 接收 `{ model: TaskSurfaceModelV1 }`。Host 解析并规范化完整模型;若该会话已有一个打开的 Task Surface,则拒绝调用;否则生成 `surfaceId`,并返回带规范化模型的规范值 `{ surfaceId, model }`。`presentationMeta` 持久化 `value.model`,使投影器和执行器不会对规范化结果产生分歧。Native 结果会指明该 Surface,并说明客户端无法渲染面板时,可以通过普通消息绕过它。随后工具调用 `exec.concludeTurn()`,防止 agent 越过所要求的人工检查点继续执行。 -工具定义省略 `isConcurrencySafe`。根据现有工具注册表约定,省略该字段会将每次调用归类为独占排序屏障,无需新增 `ToolDefinition` 字段。该工具只会组装到同时挂载 Host 服务和 Web 渲染器的 Web profile 中。版本 1 支持 `native` 和 `both` 工具模式;仅支持 `ptc` 的 profile 不会向模型公布该工具,因为 PTC mode 分发属于嵌套调用,无法把呈现元数据传到外层结果。 +工具定义省略 `isConcurrencySafe`。根据现有工具注册表约定,省略该字段会将每次调用归类为独占排序屏障,无需新增 `ToolDefinition` 字段。该工具只会组装到同时挂载 Host 服务和 Web 渲染器的 Web profile 中。版本 1 支持 `native` 工具模式;仅支持 `ptc` 的 profile 不会向模型公布该工具,因为 PTC mode 分发属于嵌套调用,无法把呈现元数据传到外层结果。 浏览器安全的领域包从 `@deepseek-ai/dsh-brand` 以仅类型方式导入 `Branded` 原语,并拥有全部三个 Task Surface ID。根据[规范工具输出约定](../../../../packages/core/tools/README.zh.md),规范值仅存在于本次执行中。因此,回放通过 `output.presentationMeta(args, value)` 将以下带标签的载荷随 `tool/result.meta` 一并持久化: @@ -248,7 +248,7 @@ Web 插件将未提交值保存在一个有界、按会话持久化的 slot stor ## 验收标准 -- 在 `native` 或 `both` 工具模式下,真实模型可以调用一个稳定的 `show_task_surface` schema;调用结束当前轮次;具备相应能力的 Web 客户端在实时运行和回放后都能渲染同一份规范化模型;仅支持 `ptc` 的模式不会向模型公布该工具。 +- 在 `native` 工具模式下,真实模型可以调用一个稳定的 `show_task_surface` schema;调用结束当前轮次;具备相应能力的 Web 客户端在实时运行和回放后都能渲染同一份规范化模型;仅支持 `ptc` 的模式不会向模型公布该工具。 - 静态 `TaskSurfaceDock` 是唯一的编辑器,即使活动结果位于已加载历史窗口之外也仍可操作;带 key 的 toolview 始终是 transcript 的只读摘要和回放。composer 接管会隐藏仍处于挂载状态的 Dock、保留其草稿,并在接管释放后重新显示同一个所有者。 - 每个 `submissionId` 的提交操作恰好生成一条可见用户消息,通过普通队列接纳开始下一轮,并在保留 `source.kind: 'user'` 的同时维持带品牌类型的确切调用实例关联;关闭操作记录一条日志事件,且不启动轮次。 - 客户端排队行保留已关联的消息来源。`getActive` 可在同一进程的重新连接前后公开 `queued` 或 `claiming`;持久化完成后会关闭投影,显式丢弃则会清除待处理状态并让 Surface 保持打开。队列行消失本身不会改变任何 UI 状态。系统会拒绝编辑和 steering,且移除操作只能在认领前成功。 diff --git a/.agents/notes/proposed/feature/2026-10-03-pi-extensions-bridge.i18n.yaml b/.agents/notes/proposed/feature/2026-10-03-pi-extensions-bridge.i18n.yaml new file mode 100644 index 0000000000..6a09cae1de --- /dev/null +++ b/.agents/notes/proposed/feature/2026-10-03-pi-extensions-bridge.i18n.yaml @@ -0,0 +1,43 @@ +# Bilingual-pair consistency record for 2026-10-03-pi-extensions-bridge.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-10-03-pi-extensions-bridge.md +/agent-note-pi-extensions-bridge: + en: cd9f68956ba34672 + zh: 1fc649c5eb0d3cf4 +/agent-note-pi-extensions-bridge/problem: + en: eb4d3f10c3feedb4 + zh: a1a1a75a724c9a8b +/agent-note-pi-extensions-bridge/proposal: + en: 56b36193ca6f829d + zh: a31e226794c1ca90 +/agent-note-pi-extensions-bridge/proposal/packages-and-composition: + en: 2ab10852b93ad41b + zh: 3c14d410235fcfac +/agent-note-pi-extensions-bridge/proposal/loading-and-execution: + en: d95ece799bc6bab7 + zh: 1e19d3b49473a57e +/agent-note-pi-extensions-bridge/proposal/serving-the-api: + en: b1365f16d76fe27a + zh: 105b88f80eb8964d +/agent-note-pi-extensions-bridge/proposal/built-in-tool-calls-as-pi-sees-them: + en: c74e4662ad0a8ee7 + zh: 653729b41e983b13 +/agent-note-pi-extensions-bridge/proposal/unserved-features: + en: 70c74cf93526cfad + zh: cab3efc2c109ad5a +/agent-note-pi-extensions-bridge/proposal/extension-state: + en: be58f498b8d28548 + zh: 4cc057f794c288bf +/agent-note-pi-extensions-bridge/proposal/core-mechanisms-narrower-than-pi-s: + en: 9fa8297436332064 + zh: 9a000e157940b0f8 +/agent-note-pi-extensions-bridge/alternatives-considered: + en: a3769b960278c17f + zh: e7d86ff1aff243b8 +/agent-note-pi-extensions-bridge/acceptance-criteria: + en: 60894db423a54ec3 + zh: 13b435271f6a1371 +/agent-note-pi-extensions-bridge/risks: + en: 2b9a626b13c604f6 + zh: f67cb80d2b699989 diff --git a/.agents/notes/proposed/feature/2026-10-03-pi-extensions-bridge.md b/.agents/notes/proposed/feature/2026-10-03-pi-extensions-bridge.md new file mode 100644 index 0000000000..28faf5911a --- /dev/null +++ b/.agents/notes/proposed/feature/2026-10-03-pi-extensions-bridge.md @@ -0,0 +1,142 @@ +# Agent Note: Pi extensions bridge + +Status: proposed + +English | [中文](2026-10-03-pi-extensions-bridge.zh.md) + +## Problem + +Pi 1.0.0 extensions are in-process TypeScript factories `(pi: ExtensionAPI) => void`. A factory subscribes to about forty lifecycle, context, tool, and provider events, registers model tools, slash commands, model providers, and MCP servers, and talks to the user through `ctx.ui` ([extensions guide](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/extensions.md), [API types](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/src/core/extensions/types.ts)). The API is published and widely used: about 11,000 npm packages carried the `pi-package` keyword in October 2026. + +DSH has bridges for two other ecosystems: shell hooks written for Claude Code and Codex ([hooks group](../../../../packages/experimental/README.md)) and Claude Code mods ([compatibility page](../../../../docs/subsystems/claude-code-mods.md)). Pi's extension API is broader than either. It lets one in-process module rewrite the messages of a single request, edit tool arguments, append its own durable records, and continue a finished run. Serving that API from DSH extension points answers two questions: how much of an extension written for another harness runs unchanged, and which DSH core mechanisms are narrower than Pi's. + +## Proposal + +Add an experimental bridge that loads unmodified Pi extension sources and serves Pi's `ExtensionAPI` from existing DSH extension points. The bridge is a capability demonstration: it runs extensions as written where an existing DSH mechanism carries the behavior, leaves the rest unserved, and lists every difference on a compatibility page. Apart from the restricted plugin records that extension state needs (row 9 of the gap table), each core gap is closed by a separate change. + +### Packages and composition + +- `@deepseek-ai/dsh-experimental-pi-extensions` is the Host package and provides `ctx.piExtensions`. `@deepseek-ai/dsh-experimental-client-ui-pi-extensions` is the Web package. Neither is an optional bundle; an opt-in overlay composes them, as for the Claude Code mods bridge. +- One cordis.yml row names one extension source: a file, a directory with an `index.ts` or `index.js`, or a Pi package directory whose `package.json` lists `pi.extensions`. The row also carries the extension's flag values. Row order is Pi's load order, which is also its handler dispatch order. +- The bridge does not scan `~/.pi` or `.pi`, install packages, or decide project trust. A Pi package's skills, prompt templates, and themes are not loaded. + +### Loading and execution + +- The bridge has its own loader and event dispatcher. `@earendil-works/pi-coding-agent` is not a dependency. +- `jiti` imports the source, so TypeScript loads in a built installation. The loader resolves the imports Pi supplies to extensions: `typebox` and `@earendil-works/pi-ai` to the bridge's own dependencies, `@earendil-works/pi-coding-agent` to a module of the bridge that implements the helpers that need no Pi runtime (`defineTool`, the tool-event type guards, the truncation helpers, `withFileMutationQueue`, `getAgentDir`), and `@earendil-works/pi-tui` to the optional peer dependency when the deployment installed it. An extension that imports a module the loader cannot resolve fails to load with an error naming the module. +- Extension code runs in the Host process with the process's full authority, as mods do. +- Every Session with an in-process Agent gets its own runtime: the module is evaluated once per row, and its factory runs once per Session, as Pi re-runs factories for each Session. State an extension keeps at module scope is shared by all Sessions of the Host process; Pi also keeps one module instance across the Sessions of a process, but runs one Session at a time. A subagent's Session gets a separate runtime in Pi's `print` mode with `hasUI: false`, so a tool call a subagent makes passes the same extension handlers as the root's. + +### Serving the API + +| Pi API | DSH mechanism | +|---|---| +| `session_start`, `session_shutdown` | `agent/created` (awaited before the first turn), `agent/disposed` | +| `input` (after inbox admission; see row 7), `before_agent_start` | `system-prompt/assemble` and `agent/pre-step`: claimed user messages are transformed or dropped, a returned `systemPrompt` replaces the assembled prompt for that turn, and returned messages join the step | +| `tool_call` | `tools/pre-execute`: `block` becomes a denial; a handler that throws denies the call | +| `tool_result` | `tools/post-execute`: replaced content is installed as the result's content | +| `agent_before_settle` | `agent/turn-stopping`: returned custom entries are recorded, and a returned message with `continue` is steered into the turn | +| `agent_start`, `agent_end`, `agent_settled`, `turn_start`, `turn_end`, `message_start`, `message_end`, `tool_execution_start`, `tool_execution_end`, `model_select`, `session_info_changed`, `session_compact` | Session events and `agent/status`, delivered in order as notifications | +| `registerTool`, `ctx.executeTool` | Agent-scoped `ctx.tools` registrations; nested calls run the full tool pipeline; built-in names follow the translation below | +| `registerCommand`, `getCommands` | Agent-scoped `ctx.commands` | +| `sendUserMessage`, `sendMessage` | `agent.followup()`, `agent.steer()`, `agent.inject()` | +| `appendEntry`, `setLabel`, tool `details`, `ctx.sessionManager` | Ignorable Session records and `tool/result` metadata, read back as Pi entries | +| `getActiveTools`, `setActiveTools`, `getAllTools` | `ctx.tools.schemas()`; `setActiveTools` applies a scoped `ctx.tools.restrict()` to global tools and adds or removes the bridge's own Agent-scoped registrations, because a restriction does not hide scoped tools | +| `setModel`, `getThinkingLevel`, `setThinkingLevel` | An `agent/request` override | +| `exec` | `ctx.subprocess`, argv without a shell | +| `ctx.ui.select`, `confirm`, `input`, `editor` | `ctx.userQuestions` | +| `ctx.ui.notify`, `setStatus`, `setWidget`, `setTitle`, `setEditorText` | A Remote stream the Web package renders | +| `registerFlag`, `getFlag`, `events` | Row configuration; an in-process event bus per Session runtime | + +All Pi handlers for one Session run through one ordered queue. A notification never overtakes an earlier one, and an awaited hook such as `tool_call` runs after every notification queued before it, which keeps the order Pi's awaited dispatch gives handlers. + +The Web and Desktop GUI appear to extensions as Pi's `rpc` mode with `hasUI: true`; headless, SDK, and ACP Sessions appear as `print` mode. Extensions already handle both: terminal-only calls such as `ctx.ui.custom()` return what Pi's `rpc` mode returns. + +### Built-in tool calls as Pi sees them + +DSH and Pi share the names `bash`, `read`, `edit`, `write`, and `grep`, but not their arguments, and Pi's `find` is DSH's `glob`. A Pi safety extension reads `event.input.path` or `event.input.command`. The bridge therefore presents each call of these six tools to handlers with Pi's name and Pi's argument names, translated from the DSH call. Other tools pass through unchanged. A test loads the registered DSH tool schemas and fails when a mapped parameter is renamed or removed, or when a tool gains a parameter the table has not classified. + +The opposite direction uses the same table. `getActiveTools()` and `getAllTools()` report Pi names for the six tools and DSH names for every other tool, and `setActiveTools()` accepts the same names. `ctx.executeTool()` given a Pi name translates the arguments into the DSH call; an `edit` call with more than one entry in `edits` is refused, because one DSH `edit` call makes one replacement. An extension tool registered under a Pi built-in name replaces the corresponding DSH tool for that Agent, as it replaces Pi's own tool (`find` replaces `glob`). + +### Unserved features + +- An event DSH cannot raise is accepted at registration, never raised, and named in a warning when the extension loads. +- A `pi` or `ctx` member DSH cannot serve throws an error naming the member. +- Where Pi fails closed, the bridge does too. A `tool_call` handler that edits `event.input` gets the call denied with a reason, because the logged call would otherwise differ from the executed one. + +### Extension state + +Pi extensions rebuild their state on `session_start` from what they wrote earlier. The bridge keeps that state in the Session log so that it follows resume and fork: + +- `appendEntry` and `setLabel` write ignorable records whose type starts with `plugin:`. A restricted core operation, callable only from experimental packages, appends them. They are not `SessionEventMap` members, so the persistence catalog and its type history are unchanged. A build without the bridge retains and skips them when it reads the current format, as the [ignorable events decision](../../implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md) requires. A later format edge carries them on a best-effort basis: it keeps them under their names where it can and otherwise states which it drops, while the alpha historical edges refuse every unknown event ([decision](../../implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.md)). +- A tool's `details` is stored as the `tool/result` metadata the tool's output declaration projects. +- `sendMessage` content is logged as an ordinary user-sourced message with the extension's content unchanged. A companion ignorable record carries `customType`, `display`, and `details`. + +### Core mechanisms narrower than Pi's + +Ranked by how much of Pi's API each blocks, with the GitHub issue that tracks each. Rows 1 and 3 follow from DSH rules: a request is derived from the Session log, and the log is linear. The other rows are missing extension points. + +| # | Mechanism | Pi API blocked | DSH today | Issue | +|---|---|---|---|---| +| 1 | Per-request rewrite of model input | `context`, `context_with_system`, `before_provider_request`, `prepareLoadout` hidden declarations | The loop derives each request from the log and freezes it; `llm/stream` listeners read it. A rewrite needs an awaited hook and a core event that every log reader applies to that request. | #5661 | +| 2 | Tool-argument rewrite | `tool_call` editing `event.input` | `PreToolDecision` excludes input rewriting ([proposal](2026-06-30-pre-tool-input-rewrite.md)). | #5662 | +| 3 | Session tree and assistant-message replacement | `navigateTree`, `session_before_tree`, `session_tree`, `message_end` replacement | The log is linear, a fork creates a new Session, and an `assistant/message` cannot replace another. | #5663 | +| 4 | Awaited step-end and pre-commit hooks | `turn_end` entries and `continue`, `message_end` | `step/end` and `assistant/message` are committed events; a Session-event listener cannot append. | #5664 | +| 5 | Tool progress and result replacement | `onUpdate`, `tool_execution_update`, `tool_result` turning an error into a success, tool `usage` | `ToolRunContext` has no progress channel, and `tools/post-execute` cannot replace the value of a failed result. | #5665 | +| 6 | Per-tool exposure | `codemode`, `deferred`, and `hidden` exposure | Presentation is chosen per Agent, and a tool the model cannot see refuses execution. | #5666 | +| 7 | Input admission | `input` before the prompt is stored | A prompt is durable in the inbox before `agent/pre-step`; a swallowed prompt leaves a turn without a step. | #5667 | +| 8 | Logged model call by a plugin | `ctx.modelRegistry.complete`, `stream`, `streamSimple` | No Session event records a plugin's own model request, which also blocks `$.model.complete` for mods. | #5668 | +| 9 | Plugin-owned durable records | `appendEntry` | `Session.append()` accepts declared event types only. The restricted `plugin:` records close this for experimental packages. | #5669 | +| 10 | Compaction customization | `session_before_compact` | One `CompactionEngine` owns summarization, and `/compact` takes no instructions. | #5670 | +| 11 | Lifecycle reasons | `session_start` reasons `new`, `fork`, and `reload`; `session_shutdown` reasons | `agent/created` reports `startup` or `resume`; `SessionStartSource` also declares `clear` and `compact`, which nothing emits yet; `agent/disposed` carries no reason. | #5671 | +| 12 | Host-plugin display in the GUI | `ctx.ui.notify`, `setStatus`, `setWidget` | No shared mechanism; each bridge ships its own Client package. | #5672 | +| 13 | Command capabilities | Argument completion; `newSession`, `fork`, and `switchSession` moving the user to another Session | `ctx.commands` takes unstructured text and has no Client-side effects. | #5673 | +| 14 | Provider request hooks | `before_provider_headers`, `after_provider_response`, `provider_stream_event` | Headers, responses, and raw stream events stay inside each adapter. | #5674 | +| 15 | Fault isolation | An extension bug stops one Pi process | An uncaught exception in the Host disposes the application for every Session. | #5675 | + +Terminal rendering (`ctx.ui.custom`, custom editors, renderers, shortcuts) is outside this list: DSH's GUI is not a terminal, and Pi's own `rpc` mode does not serve these either. + +## Alternatives considered + +**Depend on `@earendil-works/pi-coding-agent` and reuse its loader and runner.** It would reproduce Pi's handler ordering and error rules exactly and supply every helper extensions import. It was rejected because the exercise is to carry Pi's API on DSH mechanisms, not to embed Pi's harness: the runner is constructed over Pi's `SessionManager` and `ModelRegistry`, the package brings Pi's whole dependency closure, and its built-in tool factories would run Pi's own file and shell implementations outside DSH's filesystem and sandbox policy. + +**A wrapper module per extension, like `defineMod`.** A Pi extension is already a default-exported factory with no manifest to replace, so a wrapper adds a step for every extension and prevents loading a published Pi package directory. + +**Discover Pi's own locations automatically.** Loading `~/.pi/agent/extensions` and project `.pi/extensions` executes code a DSH user did not compose, which needs a project-trust decision DSH does not have. The mods bridge removed its directory loader for the same composition reason. Discovery can be a later opt-in layer. + +**Run extensions in a separate process per Session.** An uncaught extension error would then reach the Host as an event. Every awaited hook would become an inter-process round trip, and Pi's edit-in-place event objects would have to be reproduced by sending changes back. Deferred until real extensions show the isolation is needed. + +**Evaluate each extension module once per Session.** Concurrent Sessions would then not share module-scope state. Rejected because Pi keeps one module instance across the Sessions of a process, so an extension may rely on module state surviving a Session switch, and per-Session evaluation repeats the transpilation for every Session and subagent. + +**Run extensions for root Sessions only.** It needs an explicit filter, since `agent/created` fires for every Agent, and it lets a subagent's tool calls bypass a Pi safety extension. + +**Pass DSH tool names and arguments to handlers unchanged.** Pi's own `protected-paths` example reads `event.input.path`; with DSH's `file_path` it throws, and a handler written slightly differently would allow the write. + +**Declare `pi/*` Session events and a `pi-extension` message source.** Correct attribution, but each is a required-on-read persistence type: the catalog and type history change, and a build without the bridge refuses the Session. + +**Keep extension state in a storage domain outside the log.** State would not follow a fork, and `ctx.sessionManager.getBranch()` would not show what the extension wrote. + +**Raise `context` and `before_provider_request` without logging their effect.** The model would see input that the Session log cannot reconstruct. + +**Refuse to load an extension that subscribes to an unserved event.** Most extensions use several features; the mods bridge already warns and keeps the rest working. + +**Build a shared GUI notification mechanism first.** It would also serve the mods bridge, but it is a core addition; the Web package of this bridge renders its own stream until that exists. + +## Acceptance criteria + +- Pi's MIT-licensed example extensions are committed unchanged as fixtures, and only those a test exercises. Through a real agent loop with a mock model: a permission extension denies a tool call and the model sees the reason; an extension tool is called by the model and its `details` survive a resumed Session; an extension command runs; an extension replaces the system prompt for a turn; `appendEntry` state is rebuilt on `session_start` after resume. +- A subagent's tool call reaches the handlers of its own runtime. +- The tool translation test fails when a mapped DSH tool parameter changes. +- A Loader composition test boots the bridge from a cordis.yml, and a disposal test shows every registration removed when the bridge unloads. +- A recorded-Session snapshot covers an extension-denied tool call and an extension tool call. +- `docs/subsystems/pi-extensions.md` lists every event and member with its status and every behavioral difference. +- The new packages' `src/` have 100% coverage, and `doc-sync` passes. + +## Risks + +- Extension code has the Host's full authority, and one uncaught error stops every Session. Mount only extensions you would run as a plugin. +- Fidelity rests on the bridge's reading of Pi 1.0.0 dispatch rules, checked against Pi's examples but not executed on Pi's code. A later Pi release can change an event or helper the bridge serves. +- An extension that imports a helper the bridge does not implement fails to load, even if it would have used that helper on one code path only. +- A future Session format edge that cannot carry plugin records loses extension state for Sessions written before it. +- Extensions that keep per-Session state at module scope mix the state of concurrent Sessions, including subagents. +- A message an extension sends is attributed to the user everywhere outside the Web package that reads the companion record. diff --git a/.agents/notes/proposed/feature/2026-10-03-pi-extensions-bridge.zh.md b/.agents/notes/proposed/feature/2026-10-03-pi-extensions-bridge.zh.md new file mode 100644 index 0000000000..855f7b160c --- /dev/null +++ b/.agents/notes/proposed/feature/2026-10-03-pi-extensions-bridge.zh.md @@ -0,0 +1,142 @@ +# Agent Note: Pi 扩展桥接 + +Status: proposed + +[English](2026-10-03-pi-extensions-bridge.md) | 中文 + +## 问题 + +Pi 1.0.0 的扩展是进程内的 TypeScript 工厂函数 `(pi: ExtensionAPI) => void`。工厂函数订阅约四十个生命周期、上下文、工具与提供方事件,注册模型工具、斜杠命令、模型提供商与 MCP server,并通过 `ctx.ui` 与用户交互([扩展指南](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/extensions.md)、[API 类型](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/src/core/extensions/types.ts))。该 API 已发布并被广泛使用:截至 2026 年 10 月,npm 上约有 11,000 个包带有 `pi-package` 关键字。 + +DSH 已为另外两个生态提供桥接:为 Claude Code 与 Codex 编写的 shell 钩子([hooks 组](../../../../packages/experimental/README.zh.md)),以及 Claude Code mods([兼容性页面](../../../../docs/subsystems/claude-code-mods.zh.md))。Pi 的扩展 API 比两者都宽。它允许一个进程内模块改写单次请求的消息、编辑工具参数、追加自己的持久记录,并让已经结束的运行继续。用 DSH 扩展点提供这套 API 可以回答两个问题:为另一个 harness 编写的扩展有多少能原样运行,以及 DSH 的哪些核心机制比 Pi 窄。 + +## 提案 + +新增一个实验性桥接,加载未修改的 Pi 扩展源码,并用现有的 DSH 扩展点提供 Pi 的 `ExtensionAPI`。该桥接是一次能力演示:凡现有 DSH 机制能承载的行为,就让扩展原样运行;其余部分不提供,并在兼容性页面列出每一处差异。除扩展状态所需的受限插件记录(缺口表第 9 行)外,每项核心缺口都由单独的变更弥补。 + +### 包与组合 + +- `@deepseek-ai/dsh-experimental-pi-extensions` 是 Host 包,提供 `ctx.piExtensions`。`@deepseek-ai/dsh-experimental-client-ui-pi-extensions` 是 Web 包。两者都不是可选 bundle;与 Claude Code mods 桥接一样,由一个按需启用的 overlay 组合它们。 +- cordis.yml 的一行指定一个扩展来源:一个文件、一个含 `index.ts` 或 `index.js` 的目录,或一个在 `package.json` 中列出 `pi.extensions` 的 Pi 包目录。该行同时携带扩展的 flag 值。行的顺序就是 Pi 的加载顺序,也是其 handler 的分发顺序。 +- 桥接不扫描 `~/.pi` 或 `.pi`,不安装包,也不判定项目信任。Pi 包中的 skill、提示词模板与主题不会被加载。 + +### 加载与执行 + +- 桥接有自己的 loader 与事件分发器。`@earendil-works/pi-coding-agent` 不是依赖。 +- 源码由 `jiti` 导入,因此 TypeScript 在已构建的安装中也能加载。loader 解析 Pi 提供给扩展的那些导入:`typebox` 与 `@earendil-works/pi-ai` 解析到桥接自己的依赖;`@earendil-works/pi-coding-agent` 解析到桥接的一个模块,它实现了不需要 Pi 运行时的辅助函数(`defineTool`、工具事件类型守卫、截断辅助函数、`withFileMutationQueue`、`getAgentDir`);`@earendil-works/pi-tui` 在部署安装了该可选对等依赖(peer dependency)时解析到它。扩展导入了 loader 无法解析的模块时加载失败,错误信息会指出该模块。 +- 与 mods 一样,扩展代码在 Host 进程中以进程的全部权限运行。 +- 每个拥有进程内 Agent 的 Session 都有自己的运行时:每行的模块只求值一次,其工厂函数对每个 Session 运行一次,正如 Pi 为每个 Session 重新运行工厂函数。扩展保存在模块作用域的状态由 Host 进程中的所有 Session 共享;Pi 同样在一个进程的多个 Session 之间保留同一个模块实例,但同一时间只运行一个 Session。subagent 的 Session 有独立的运行时,处于 Pi 的 `print` 模式且 `hasUI: false`,因此 subagent 发起的工具调用与根 Agent 的调用经过同样的扩展 handler。 + +### 提供 API + +| Pi API | DSH 机制 | +|---|---| +| `session_start`、`session_shutdown` | `agent/created`(在首个轮次之前等待完成)、`agent/disposed` | +| `input`(在 inbox 准入之后,见第 7 行)、`before_agent_start` | `system-prompt/assemble` 与 `agent/pre-step`:被认领的用户消息可被改写或丢弃,返回的 `systemPrompt` 替换该轮次已组装的提示词,返回的消息加入该步骤 | +| `tool_call` | `tools/pre-execute`:`block` 成为拒绝;handler 抛出异常时拒绝该调用 | +| `tool_result` | `tools/post-execute`:被替换的内容安装为结果内容 | +| `agent_before_settle` | `agent/turn-stopping`:返回的自定义条目被记录,带 `continue` 返回的消息被 steer 进该轮次 | +| `agent_start`、`agent_end`、`agent_settled`、`turn_start`、`turn_end`、`message_start`、`message_end`、`tool_execution_start`、`tool_execution_end`、`model_select`、`session_info_changed`、`session_compact` | 会话事件与 `agent/status`,作为通知按顺序送达 | +| `registerTool`、`ctx.executeTool` | Agent 作用域的 `ctx.tools` 注册;嵌套调用走完整的工具管线;内置名称遵循下文的转换 | +| `registerCommand`、`getCommands` | Agent 作用域的 `ctx.commands` | +| `sendUserMessage`、`sendMessage` | `agent.followup()`、`agent.steer()`、`agent.inject()` | +| `appendEntry`、`setLabel`、工具 `details`、`ctx.sessionManager` | ignorable Session 记录与 `tool/result` 元数据,读回时还原为 Pi 条目 | +| `getActiveTools`、`setActiveTools`、`getAllTools` | `ctx.tools.schemas()`;`setActiveTools` 对全局工具使用作用域内的 `ctx.tools.restrict()`,并增删桥接自己在 Agent 作用域的注册,因为限制不会隐藏作用域内的工具 | +| `setModel`、`getThinkingLevel`、`setThinkingLevel` | `agent/request` 覆盖 | +| `exec` | `ctx.subprocess`,不经 shell 的 argv | +| `ctx.ui.select`、`confirm`、`input`、`editor` | `ctx.userQuestions` | +| `ctx.ui.notify`、`setStatus`、`setWidget`、`setTitle`、`setEditorText` | 由 Web 包渲染的 Remote 流 | +| `registerFlag`、`getFlag`、`events` | 行配置;每个 Session 运行时一个进程内事件总线 | + +同一个 Session 的所有 Pi handler 都经过一个有序队列。通知不会越过更早的通知;`tool_call` 这类需要等待的钩子,在排在它之前的所有通知之后运行。这样就保持了 Pi 的等待式分发给 handler 的顺序。 + +Web 与 Desktop GUI 对扩展呈现为 Pi 的 `rpc` 模式且 `hasUI: true`;headless、SDK 与 ACP Session 呈现为 `print` 模式。扩展本来就处理这两种模式:`ctx.ui.custom()` 这类仅限终端的调用返回 Pi 的 `rpc` 模式所返回的值。 + +### Pi 视角下的内置工具调用 + +DSH 与 Pi 共有 `bash`、`read`、`edit`、`write` 与 `grep` 这些名称,但参数不同,而 Pi 的 `find` 是 DSH 的 `glob`。Pi 的安全类扩展会读取 `event.input.path` 或 `event.input.command`。因此,桥接把这六个工具的每次调用以 Pi 的名称和 Pi 的参数名呈现给 handler,由 DSH 调用转换而来。其他工具原样传递。有一项测试加载已注册的 DSH 工具 schema,在被映射的参数改名或被移除时失败,在工具新增了映射表尚未归类的参数时也失败。 + +反方向使用同一张映射表。`getActiveTools()` 与 `getAllTools()` 对这六个工具报告 Pi 名称,对其他工具报告 DSH 名称;`setActiveTools()` 接受同样的名称。`ctx.executeTool()` 收到 Pi 名称时,把参数转换为 DSH 调用;`edits` 中多于一项的 `edit` 调用会被拒绝,因为一次 DSH `edit` 调用只做一处替换。以 Pi 内置名称注册的扩展工具会替换该 Agent 中对应的 DSH 工具,正如它在 Pi 中替换 Pi 自己的工具(`find` 替换 `glob`)。 + +### 不提供的功能 + +- DSH 无法触发的事件:注册时接受,永不触发,并在扩展加载时的警告中列出名称。 +- DSH 无法提供的 `pi` 或 `ctx` 成员:抛出指明该成员的错误。 +- Pi 在哪里失败关闭,桥接也在哪里失败关闭。编辑 `event.input` 的 `tool_call` handler 会使该调用被拒绝并附带原因,否则已记录的调用会与实际执行的调用不一致。 + +### 扩展状态 + +Pi 扩展在 `session_start` 时根据自己此前写入的内容重建状态。桥接把这些状态保存在 Session 日志中,使其跟随 resume 与 fork: + +- `appendEntry` 与 `setLabel` 写入类型以 `plugin:` 开头的 ignorable 记录。追加它们的是一项受限的核心操作,只能由实验性包调用。它们不是 `SessionEventMap` 成员,因此持久化目录及其类型历史不变。不带该桥接的构建在读取当前格式时保留并跳过它们,这是 [ignorable 事件决策](../../implemented/architecture/2026-08-30-retain-ignorable-external-session-events.zh.md)的要求。之后的格式迁移边以尽力而为的方式携带它们:能保留时以原名称保留,否则说明丢弃哪些;Alpha 历史格式迁移边则拒绝所有未知事件([决策](../../implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.zh.md))。 +- 工具的 `details` 保存为 `tool/result` 元数据,由工具的输出声明投影得出。 +- `sendMessage` 的内容记录为普通的用户来源消息,扩展的内容保持不变。一条伴随的 ignorable 记录携带 `customType`、`display` 与 `details`。 + +### 比 Pi 窄的核心机制 + +按各自阻塞的 Pi API 数量排序,并列出跟踪每一项的 GitHub issue。第 1、3 行源自 DSH 的规则:请求由 Session 日志派生,而日志是线性的。其余各行是缺失的扩展点。 + +| # | 机制 | 被阻塞的 Pi API | DSH 现状 | Issue | +|---|---|---|---|---| +| 1 | 按请求改写模型输入 | `context`、`context_with_system`、`before_provider_request`、`prepareLoadout` 隐藏声明 | 循环从日志派生每个请求并将其冻结;`llm/stream` 监听器只读取它。改写需要一个可等待的钩子,以及一个每个日志读取方都会应用到该请求上的核心事件。 | #5661 | +| 2 | 改写工具参数 | 编辑 `event.input` 的 `tool_call` | `PreToolDecision` 不包含输入改写([提案](2026-06-30-pre-tool-input-rewrite.zh.md))。 | #5662 | +| 3 | Session 树与 assistant 消息替换 | `navigateTree`、`session_before_tree`、`session_tree`、`message_end` 替换 | 日志是线性的,fork 会创建新的 Session,`assistant/message` 不能替换另一条。 | #5663 | +| 4 | 可等待的步骤结束钩子与提交前钩子 | `turn_end` 的条目与 `continue`、`message_end` | `step/end` 与 `assistant/message` 是已提交的事件;会话事件监听器不能追加事件。 | #5664 | +| 5 | 工具进度与结果替换 | `onUpdate`、`tool_execution_update`、把错误变为成功的 `tool_result`、工具 `usage` | `ToolRunContext` 没有进度通道,`tools/post-execute` 不能替换失败结果的值。 | #5665 | +| 6 | 按工具设置可见性 | `codemode`、`deferred` 与 `hidden` exposure | 呈现方式按 Agent 选择,模型看不到的工具拒绝执行。 | #5666 | +| 7 | 输入准入 | 在提示词被保存之前的 `input` | 提示词在 `agent/pre-step` 之前就已持久保存在 inbox 中;被吞掉的提示词会留下一个没有步骤的轮次。 | #5667 | +| 8 | 插件发起且被记录的模型调用 | `ctx.modelRegistry.complete`、`stream`、`streamSimple` | 没有会话事件记录插件自己的模型请求,这同样阻塞了 mods 的 `$.model.complete`。 | #5668 | +| 9 | 插件拥有的持久记录 | `appendEntry` | `Session.append()` 只接受已声明的事件类型。受限的 `plugin:` 记录为实验性包补上了这一点。 | #5669 | +| 10 | 定制压缩 | `session_before_compact` | 摘要由唯一的 `CompactionEngine` 负责,`/compact` 不接受指令。 | #5670 | +| 11 | 生命周期原因 | `session_start` 的 `new`、`fork` 与 `reload` 原因;`session_shutdown` 的原因 | `agent/created` 报告 `startup` 或 `resume`;`SessionStartSource` 还声明了尚无生产者的 `clear` 与 `compact`;`agent/disposed` 不带原因。 | #5671 | +| 12 | Host 插件在 GUI 中的显示 | `ctx.ui.notify`、`setStatus`、`setWidget` | 没有共享机制;每个桥接各自附带 Client 包。 | #5672 | +| 13 | 命令能力 | 参数补全;把用户带到另一个 Session 的 `newSession`、`fork` 与 `switchSession` | `ctx.commands` 接受非结构化文本,没有 Client 侧效果。 | #5673 | +| 14 | 提供方请求钩子 | `before_provider_headers`、`after_provider_response`、`provider_stream_event` | 请求头、响应与原始流事件都留在各适配器内部。 | #5674 | +| 15 | 故障隔离 | 扩展的缺陷只中止一个 Pi 进程 | Host 中未捕获的异常会为所有 Session 释放整个应用。 | #5675 | + +终端渲染(`ctx.ui.custom`、自定义编辑器、渲染器、快捷键)不在此列:DSH 的 GUI 不是终端,Pi 自己的 `rpc` 模式也不提供这些。 + +## 考虑过的替代方案 + +**依赖 `@earendil-works/pi-coding-agent` 并复用其 loader 与 runner。** 这样可以精确复现 Pi 的 handler 顺序与错误规则,并提供扩展导入的全部辅助函数。否决的原因是:这项工作要用 DSH 的机制承载 Pi 的 API,而不是嵌入 Pi 的 harness。该 runner 构造于 Pi 的 `SessionManager` 与 `ModelRegistry` 之上,这个包会带入 Pi 的全部依赖闭包,而且它的内置工具工厂会在 DSH 的文件系统与沙箱策略之外运行 Pi 自己的文件与 shell 实现。 + +**像 `defineMod` 那样为每个扩展写一个包装模块。** Pi 扩展本身就是默认导出的工厂函数,没有需要替代的 manifest,因此包装只会给每个扩展多加一步,并且无法直接加载已发布的 Pi 包目录。 + +**自动发现 Pi 自己的位置。** 加载 `~/.pi/agent/extensions` 与项目的 `.pi/extensions` 会执行 DSH 用户并未组合进来的代码,这需要 DSH 尚不具备的项目信任判定。mods 桥接出于同样的组合原因移除了它的目录 loader。发现机制可以作为之后按需启用的一层。 + +**为每个 Session 在独立进程中运行扩展。** 这样未捕获的扩展错误会以事件的形式到达 Host。但每个需要等待的钩子都会变成一次跨进程往返,Pi 的就地编辑事件对象也必须通过回传变更来复现。待真实扩展表明确实需要这种隔离时再做。 + +**为每个 Session 各求值一次扩展模块。** 这样并发的 Session 不会共享模块作用域的状态。否决的原因是:Pi 在一个进程的多个 Session 之间保留同一个模块实例,扩展可能依赖模块状态在切换 Session 后仍然存在;而且按 Session 求值会为每个 Session 与 subagent 重复转译。 + +**只为根 Session 运行扩展。** 这需要显式过滤,因为 `agent/created` 对每个 Agent 都会触发;它还会让 subagent 的工具调用绕过 Pi 的安全类扩展。 + +**把 DSH 的工具名称与参数原样传给 handler。** Pi 自己的 `protected-paths` 示例读取 `event.input.path`;遇到 DSH 的 `file_path` 时它会抛出异常,而写法稍有不同的 handler 会放行这次写入。 + +**声明 `pi/*` 会话事件与 `pi-extension` 消息来源。** 归属是正确的,但两者都是读取时必需的持久化类型:目录与类型历史会改变,没有该桥接的构建会拒绝这个 Session。 + +**把扩展状态保存在日志之外的 storage domain 中。** 状态不会跟随 fork,`ctx.sessionManager.getBranch()` 也看不到扩展写入的内容。 + +**触发 `context` 与 `before_provider_request` 但不记录其效果。** 模型会看到 Session 日志无法重建的输入。 + +**拒绝加载订阅了不提供的事件的扩展。** 多数扩展同时使用多项功能;mods 桥接已经采用“发出警告并让其余部分继续工作”的做法。 + +**先构建共享的 GUI 通知机制。** 它也能服务 mods 桥接,但属于核心新增;在它出现之前,本桥接的 Web 包渲染自己的流。 + +## 验收标准 + +- Pi 以 MIT 许可发布的示例扩展原样提交为 fixture,且只提交有测试覆盖的那些。在使用 mock 模型的真实 agent loop 中:权限扩展拒绝一次工具调用,模型看到原因;模型调用扩展工具,其 `details` 在恢复的 Session 中仍然存在;扩展命令可以运行;扩展替换某个轮次的系统提示词;resume 之后,`appendEntry` 的状态在 `session_start` 时被重建。 +- subagent 的工具调用到达它自己运行时的 handler。 +- 被映射的 DSH 工具参数变化时,工具转换测试失败。 +- Loader 组合测试从 cordis.yml 启动桥接,释放测试表明桥接卸载时每项注册都被移除。 +- 一个录制的 Session 快照覆盖一次被扩展拒绝的工具调用和一次扩展工具调用。 +- `docs/subsystems/pi-extensions.md` 列出每个事件与成员及其状态,以及每一处行为差异。 +- 新包的 `src/` 覆盖率为 100%,`doc-sync` 通过。 + +## 风险 + +- 扩展代码拥有 Host 的全部权限,一个未捕获的错误会中止所有 Session。只挂载你愿意作为插件运行的扩展。 +- 保真度取决于桥接对 Pi 1.0.0 分发规则的理解;它经过 Pi 示例的检验,但没有在 Pi 的代码上执行。之后的 Pi 版本可能改变桥接所提供的事件或辅助函数。 +- 扩展导入了桥接未实现的辅助函数时会加载失败,即使它只在某一条代码路径上用到该函数。 +- 无法携带插件记录的未来 Session 格式迁移边,会使迁移之前写入的 Session 丢失扩展状态。 +- 把按 Session 区分的状态放在模块作用域的扩展,会混用并发 Session(包括 subagent)的状态。 +- 扩展发送的消息,在读取伴随记录的 Web 包之外的所有地方,都归属于用户。 diff --git a/.agents/notes/proposed/feature/2026-10-03-pi-plugins-compatibility.i18n.yaml b/.agents/notes/proposed/feature/2026-10-03-pi-plugins-compatibility.i18n.yaml new file mode 100644 index 0000000000..18c0105d65 --- /dev/null +++ b/.agents/notes/proposed/feature/2026-10-03-pi-plugins-compatibility.i18n.yaml @@ -0,0 +1,46 @@ +# Bilingual-pair consistency record for 2026-10-03-pi-plugins-compatibility.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-10-03-pi-plugins-compatibility.md +/agent-note-pi-plugins-compatibility-levels-of-support: + en: 68e63fd54584ecbc + zh: e1cedbf64975bc53 +/agent-note-pi-plugins-compatibility-levels-of-support/problem: + en: f98ddabdc0ea831b + zh: e695439507286576 +/agent-note-pi-plugins-compatibility-levels-of-support/proposal: + en: 37a27883d679bb6a + zh: 62c60e0cd06bf8fb +/agent-note-pi-plugins-compatibility-levels-of-support/what-a-pi-plugin-is: + en: fd37c3aa5d6c092b + zh: e39a84ef9aabaa48 +/agent-note-pi-plugins-compatibility-levels-of-support/level-1-session-facets: + en: 5e08e808af0a741a + zh: eae79e9c8f1fc301 +/agent-note-pi-plugins-compatibility-levels-of-support/level-2-presentation-facets-on-the-host: + en: 6a4b96426808d653 + zh: c341dd2ba594ed45 +/agent-note-pi-plugins-compatibility-levels-of-support/level-3-transcript-and-stored-selection: + en: defc273e0759bb9f + zh: 59a4cf0dd0b46faa +/agent-note-pi-plugins-compatibility-levels-of-support/level-4-presentation-facets-in-the-browser: + en: 5b2fcfdb38eb6121 + zh: 1d583d9a6486d2dd +/agent-note-pi-plugins-compatibility-levels-of-support/level-5-parity: + en: 657aa240a06f663f + zh: 74ca5260a5a3d215 +/agent-note-pi-plugins-compatibility-levels-of-support/runtime-differences-the-plugin-model-exposes: + en: 114b54fae1ff94cd + zh: 0d0dde9f0563b27b +/agent-note-pi-plugins-compatibility-levels-of-support/chord-mechanisms-to-evaluate-for-dsh: + en: e44ed4dcd7fc9eb9 + zh: 825a41daa69931f9 +/agent-note-pi-plugins-compatibility-levels-of-support/alternatives-considered: + en: 390883362b220cfb + zh: 1c072dd19744a92e +/agent-note-pi-plugins-compatibility-levels-of-support/acceptance-criteria: + en: 386ae81c30ec7cfc + zh: 6f8bc4257fa8898c +/agent-note-pi-plugins-compatibility-levels-of-support/risks: + en: b6410c36a55acc47 + zh: e7de4e26be7a3c58 diff --git a/.agents/notes/proposed/feature/2026-10-03-pi-plugins-compatibility.md b/.agents/notes/proposed/feature/2026-10-03-pi-plugins-compatibility.md new file mode 100644 index 0000000000..352a9b3f40 --- /dev/null +++ b/.agents/notes/proposed/feature/2026-10-03-pi-plugins-compatibility.md @@ -0,0 +1,140 @@ +# Agent Note: Pi plugins compatibility — levels of support + +Status: proposed + +English | [中文](2026-10-03-pi-plugins-compatibility.zh.md) + +## Problem + +Pi 1.0.0 has two separate mechanisms for running customization code. Extensions are in-process TypeScript factories with a published API; the [Pi extensions bridge note](2026-10-03-pi-extensions-bridge.md) covers them. Plugins are a newer mechanism built on Pi's Chord composition runtime: a plugin package contributes facets, and each facet runs in the process its name selects. This note covers plugins. + +A compatibility layer for Pi plugins is not worth building today, for two reasons the upstream sources state themselves. + +- **The API is unpublished and unstable.** The plugin stack runs only with `PI_EXPERIMENTAL=1`. The `./experimental/plugin` export of `@earendil-works/pi-coding-agent` has only a `source` condition, and the npm tarball excludes its build output. Chord's plan says it "is not a stable public API contract yet" ([PLANNING.md](https://github.com/earendil-works/pi/blob/v1.0.0/packages/chord/PLANNING.md)), and pi-durable, which the Session worker runs on, says its "API changes without notice between releases" ([README](https://github.com/earendil-works/pi/blob/v1.0.0/packages/durable/README.md)). The [services README](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/src/experimental/services/README.md) lists `AgentController.navigate()`, `nextRun()`, and `resume()` as dropped. +- **A plugin can do less than an extension.** The Session worker builds its tool, hook, and prompt registry itself and does not expose it as a service, so a facet cannot register a model tool, intercept a tool call, or change the prompt. Pi ships one example plugin. + +Deferring the work without a record loses two results of the investigation: the mapping from Pi's plugin services onto DSH services, which decides what each increment of support costs, and the DSH runtime properties that differ from Chord's, which matter to DSH independently of Pi. + +## Proposal + +Treat Pi-plugin compatibility as six levels. Each level names what a plugin author gets, the upstream change that makes it worth building, and the DSH work it needs. DSH stays at level 0 until the trigger of a higher level occurs. A level includes the levels below it, except that level 4 replaces the Host-side presentation of level 2. + +| Level | Support | Upstream trigger | DSH work | +|---|---|---|---| +| 0 | None. This note is the only artifact. | — | Re-read the upstream status when Pi releases. | +| 1 | `session` facets run in one Chord host per DSH Session. `AgentController`, `Models`, and `SessionPlugins` are served over DSH services. | Pi ships the plugin API in its npm package, or removes the `PI_EXPERIMENTAL` requirement. | One experimental Host package. No core change. | +| 2 | `tui` facets run on the Host. Their slash commands become DSH commands, `select` becomes a user question, and `showStatus` a status line. Pi's example plugin runs unmodified. | Same as level 1. | Adapters, plus the Web status display of the Pi extensions bridge. | +| 3 | `Transcript` is served, and a Session's plugin selection is stored with the Session. | A plugin that consumes `Transcript` exists, or pi-durable declares `ConversationView` stable. | A projection from the Session log to `ConversationView`, and a per-Session selection record. | +| 4 | `tui` facets run in the browser and reach Session services through a generic Chord bridge. | Chord commits to browser support, and Pi defines a presentation facet that is not terminal-specific. | One generic JSON Remote endpoint, a browser facet loader, and argument text for Client commands. | +| 5 | Parity: one worker per Session, and plugins register model tools and hooks. | Pi exposes its tool and hook registry to facets. | Per-Session process isolation, and the tool and hook mappings of the Pi extensions bridge. | + +## What a Pi plugin is + +A plugin is a package directory. Its `package.json` may carry `chord.facets`, which maps a facet name to an entry file or disables it; without the field, Pi uses `src/session.ts` and `src/tui.ts` when they exist. An entry default-exports `{ id, setup(env) }`. `setup` is synchronous and only declares: `env.use` and `env.observe` acquire services, `env.provide` and `env.provideMany` publish them, `env.replicatedState` creates shared state, and `env.own`, `env.onActivate`, and `env.onDeactivate` attach lifecycle work. The Chord host validates the complete dependency graph, activates providers before consumers, and can replace a facet whose declared services are unchanged while consumers keep their service handles. + +The server builds each entry with Chord's esbuild bundler into a content-addressed CommonJS file listed in `chord-facets.json`. The Session worker loads the `session` entry with `node:vm` after checking its SHA-256 digest, and `require` resolves only the externals the entry declared. The `tui` entry reaches each client as a JSON artifact in a service response and loads the same way. The loader is not a sandbox: plugin code runs with the process's full authority. + +`pi server -e ` sets the server's default plugins, and `pi client -e ` selects plugins for one Session. Pi stores a Session's selection and refuses a different selection while that Session's worker is running. + +Pi serves these to facets: + +| Scope | Service | Members | +|---|---|---| +| server | `SessionDirectory`, `SessionManagement`, `PresentationPlugins` | Session list state; create, remove, attach, detach; build and reload of presentation artifacts | +| session | `AgentController` | `prompt`, `steer`, `followUp`, `cancelQueued`, `abort`, `compact`, `waitForPrompt` | +| session | `Models` | replicated catalog and selection state; `select`, `selectThinking`, `cycleThinking`, `getThinkingLevels`, `refresh` | +| session | `Transcript` | the root conversation's pi-durable `ConversationView` as replicated state | +| session | `SessionPlugins` | `reload` | +| presentation, process-local | `SlashCommands` | `register`, `replace`, `list`, `subscribe`; a command has a name, description, argument hint, optional argument completions, and `run(args, context)` | +| presentation, process-local | `PresentationUI` | `select(title, items, selected, context)`, `showStatus(message, context)` | + +A service that is not process-local may contain only replicated JSON state and methods whose arguments and result are JSON and whose last parameter is a Chord `Context`. + +## Level 1: session facets + +The package depends on `@earendil-works/chord` at one exact version. It builds each selected package with `bundleFacetPackage` into a cache under the Harness home and loads the `session` entry with `createFacetBundleLoader`. Chord neither installs dependencies nor runs lifecycle scripts, so a package arrives with its dependencies installed. + +The loader resolves two externals itself. `@earendil-works/chord` resolves to the package's own Chord instance, because Chord identifies replicated state through a registry held by the module instance. `@earendil-works/pi-coding-agent/experimental/plugin` resolves to a module that defines service tokens with Pi's IDs (`pi.agent-controller`, `pi.local.presentation-ui`, `pi.local.slash-commands`); Chord matches services by ID string, so a token defined outside Pi binds to a facet compiled against Pi's. + +The package creates one Chord host per Agent in `agent/created` and disposes it with the Agent's context. Built-in facets serve Pi's Session services: + +| Pi member | DSH mechanism | Difference | +|---|---|---| +| `AgentController.prompt` | `agent.followup()` | Refusal with `busy` is derived from `agent.status`. | +| `steer`, `followUp` | `agent.steer()`, `agent.followup()` | None. | +| `cancelQueued` | `agent.inbox.remove()` | Telling `already_consumed` from `not_found` reads the Session log. | +| `abort` | `agent.cancel()` | None. | +| `compact` | `ctx.compaction.compactNow()` | Non-null `customInstructions` is refused; DSH compaction takes none. | +| `waitForPrompt` | A fold over Session events from the queued message to its `turn/end` | DSH does not link a prompt to its answer; the fold does. | +| `Models` | The model catalog and model selection of `session-controller` | Reasoning-effort identifiers stand in for Pi's thinking levels. | +| `SessionPlugins.reload` | Rebuild, `FacetHost.reload()`, then dispose the retired generation | None. | + +`Transcript` is not provided at this level. Chord rejects a host whose facet requires a service nobody provides, so a plugin that uses `Transcript` fails to load with an error naming it. + +## Level 2: presentation facets on the Host + +Pi's `tui` facets use no terminal API. They consume `SlashCommands`, `PresentationUI`, and Session services, so they can run in a second Chord host per Agent on the DSH Host, bound in-process to the Session host. + +A registered slash command becomes an Agent-scoped `ctx.commands` registration whose handler passes the raw input as `args`. A command run on the Host is logged as `command/run` and `command/done`; Pi does not log presentation commands. `getArgumentCompletions` has no DSH counterpart and is not called. `PresentationUI.select` maps to `ctx.userQuestions.ask()` and needs a connected Web Client; `showStatus` maps to the status display the Pi extensions bridge adds to the Web Client. + +## Level 3: transcript and stored selection + +`Transcript` requires a projection from DSH Session events to pi-durable's `ConversationView`: entries in pi-durable's entry kinds plus its live, inbox, agent, and usage documents. The projection is lossy where DSH records events Pi has no entry for, and it binds DSH to a type pi-durable declares unstable. + +A Session's plugin selection is stored as a `plugin:`-prefixed ignorable Session record, the mechanism the Pi extensions bridge uses for extension state, and a live Agent's selection is fixed, as in Pi. + +## Level 4: presentation facets in the browser + +Running `tui` facets in the page matches Pi's placement of presentation code. It needs three additions. A browser loader evaluates the CommonJS artifact, because Chord's artifact loader uses the filesystem and `node:vm`. One generic Remote endpoint carries Chord's service calls, snapshots, and updates as JSON between the page and the Host, because DSH generates Remote methods at build time and the Client does not discover Host services at runtime. Client commands accept argument text, which `ui-commands` actions do not receive today. + +## Level 5: parity + +Pi isolates each Session in a worker process and plans to give plugins tools and hooks through its registry. Matching that needs a DSH Session to run in its own process or worker thread with Chord's service protocol across the process boundary, and, once Pi exposes the registry, the tool, hook, and prompt mappings that the Pi extensions bridge already defines. + +## Runtime differences the plugin model exposes + +| Property | Chord and Pi | DSH | Levels affected | +|---|---|---|---| +| Provider replacement | A consumer holds a handle that resolves the current provider on each access; a reload swaps the implementation behind it. | A fiber whose injected service changes is unloaded and re-run. | 1: the Chord host keeps Chord's behavior for services between facets; a DSH service mapped into Chord still restarts its adapter. | +| Fault isolation | One worker process per Session. | All Sessions share the Host process, and an uncaught exception or unhandled rejection disposes the application ([app-boot](../../../../packages/boot/app-boot/README.md)). | 1–4 run plugin code in the Host; 5 removes the difference. | +| Remote declaration | A host publishes a service's remote members from the object it was given at runtime. | Remote methods are generated at build time, and the Client does not discover Host services at runtime ([remotes](../../../../packages/api/remotes/README.md)). | 4 | +| Shared state | Replicated JSON state with operation batches, hydration, and gap recovery, read through one API locally and remotely. | Each domain defines its own snapshot and delta types, or publishes complete projection values. | 3, 4 | +| Runtime loading | One bundle per facet, loaded outside the module cache after an integrity check. | Packages are built ahead of time; a built installation does not transpile TypeScript. | 1 adds Chord's bundler as a dependency of the plugin package. | + +## Chord mechanisms to evaluate for DSH + +Four Chord mechanisms address limits DSH has independently of Pi. Each needs its own proposal; this note only records them. + +- **Stable service handles.** A consumer keeps its handle while a provider is replaced, so a plugin reload does not restart its dependents and lose their state. +- **Declaration before activation.** Setup declares every dependency synchronously, and the host rejects a missing provider, a duplicate provider, or a cycle before any facet activates. Cordis resolves dependencies as services appear, so a fiber with a missing provider stays pending until an audit after startup reports it. +- **Replicated JSON state.** One primitive gives local and remote consumers complete immutable values, with operation batches on the wire and a full reset when a subscriber falls behind. +- **Verified plugin artifacts.** A content-addressed bundle with a manifest digest lets a host load a plugin generation outside the module cache and release it when retired. + +## Alternatives considered + +**Build levels 1 and 2 now as a prototype.** The API is unpublished, has one example plugin, and its services README already lists three `AgentController` methods as dropped. A prototype would need maintenance against each Pi release without a user. The service mapping above keeps the design result without that cost. + +**Skip the record and decide when Pi stabilizes.** The service mapping and the runtime comparison took a full reading of Chord and Pi's experimental server, worker, and client. The runtime differences also apply to DSH work that has nothing to do with Pi. + +**Implement Pi's plugin services on Cordis without Chord.** A facet calls Chord's API directly: `env.use`, `env.provide`, and `replicatedState`. Serving that API without Chord means reimplementing graph validation, stable handles, keyed services, and replicated state. Chord is MIT-licensed, has one runtime dependency, and is the runtime plugins are compiled against. + +**Add plugins to the Pi extensions bridge package.** The two mechanisms share no API and have different stability. Separate packages let each be enabled, versioned, and removed without the other. + +**Run presentation facets only in the browser.** Level 2 reuses Pi's Node artifacts unchanged and needs no browser loader. Browser support is an open decision in Chord's plan, and Pi has no presentation facet that is not terminal-specific. + +## Acceptance criteria + +- **Level 0:** this note is merged, and the Pi extensions compatibility page links to it for the plugin mechanism. +- **Level 1:** the `session` facet of Pi's example plugin loads from its unmodified package directory. A test calls its greeting service and every served `AgentController` member against a real agent loop with a mock model. `SessionPlugins.reload` replaces the facet generation without recreating the Chord host, and disposing the Agent disposes the host. +- **Level 2:** `/hello ` from Pi's example plugin runs through `ctx.commands`, shows its status text in the Web Client, and submits its prompt. +- **Level 3:** a facet that subscribes to `Transcript` receives the root conversation after hydration and after each committed step, and a resumed Session loads the plugins it was created with. +- **Level 4:** the `tui` facet of Pi's example plugin runs in the browser, and its service calls reach the Host through the generic endpoint. +- **Every level:** a compatibility page lists each difference from Pi, and the package's README states that plugin code runs in the Host with its full authority. + +## Risks + +- The upstream API can change or be withdrawn before a level is built. The service mapping describes Pi 1.0.0 and needs a re-read against the release that triggers the work. +- Levels 1 to 4 run plugin code in the Host process, where one uncaught error stops every Session. +- Level 3 depends on a pi-durable type that upstream declares unstable. +- Level 4 adds a Remote endpoint whose payloads are validated by Chord's wire parsers instead of generated per-method codecs. +- The plugin package brings esbuild as a runtime dependency through Chord's bundler. diff --git a/.agents/notes/proposed/feature/2026-10-03-pi-plugins-compatibility.zh.md b/.agents/notes/proposed/feature/2026-10-03-pi-plugins-compatibility.zh.md new file mode 100644 index 0000000000..b7078a9eba --- /dev/null +++ b/.agents/notes/proposed/feature/2026-10-03-pi-plugins-compatibility.zh.md @@ -0,0 +1,140 @@ +# Agent Note: Pi 插件兼容——分级支持 + +Status: proposed + +[English](2026-10-03-pi-plugins-compatibility.md) | 中文 + +## 问题 + +Pi 1.0.0 有两套相互独立、用于运行定制代码的机制。扩展是进程内的 TypeScript 工厂函数,API 已发布,由 [Pi 扩展桥接 Agent Note](2026-10-03-pi-extensions-bridge.zh.md) 负责。插件是较新的机制,构建在 Pi 的 Chord 组合运行时之上:一个插件包贡献若干 facet,每个 facet 运行在其名称选定的进程里。本 Agent Note 只讨论插件。 + +现在还不值得为 Pi 插件构建兼容层,上游资料自己给出了两条理由。 + +- **API 未发布且不稳定。** 插件栈只在 `PI_EXPERIMENTAL=1` 下运行。`@earendil-works/pi-coding-agent` 的 `./experimental/plugin` 导出只有 `source` 条件,npm tarball 不包含它的构建产物。Chord 的规划文档称它“is not a stable public API contract yet”([PLANNING.md](https://github.com/earendil-works/pi/blob/v1.0.0/packages/chord/PLANNING.md));Session worker 所基于的 pi-durable 称其“API changes without notice between releases”([README](https://github.com/earendil-works/pi/blob/v1.0.0/packages/durable/README.md))。[services README](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/src/experimental/services/README.md) 把 `AgentController.navigate()`、`nextRun()` 与 `resume()` 列为已移除。 +- **插件能做的事比扩展少。** Session worker 自行构建工具、钩子与提示词注册表,不把它暴露为服务,因此 facet 无法注册模型工具、拦截工具调用或修改提示词。Pi 只附带一个示例插件。 + +如果不留记录就搁置这项工作,会丢失调研得到的两项结果:Pi 插件服务到 DSH 服务的映射,它决定每一级支持的成本;以及 DSH 运行时与 Chord 的差异,这些差异与 Pi 无关,对 DSH 自身同样重要。 + +## 提案 + +把 Pi 插件兼容划分为六个级别。每一级说明插件作者能得到什么、哪项上游变化使它值得构建,以及需要哪些 DSH 工作。在更高级别的触发条件出现之前,DSH 停留在 0 级。每一级包含其下各级,唯一例外是 4 级取代 2 级在 Host 侧的 presentation 实现。 + +| 级别 | 支持内容 | 上游触发条件 | DSH 工作 | +|---|---|---|---| +| 0 | 无。本 Agent Note 是唯一产物。 | — | Pi 发版时重新核对上游状态。 | +| 1 | `session` facet 运行在每个 DSH Session 各自的 Chord host 中。`AgentController`、`Models` 与 `SessionPlugins` 由 DSH 服务提供。 | Pi 在 npm 包中发布插件 API,或取消 `PI_EXPERIMENTAL` 要求。 | 一个实验性 Host 包。不改核心。 | +| 2 | `tui` facet 运行在 Host 上。它们的斜杠命令成为 DSH 命令,`select` 成为用户提问,`showStatus` 成为状态行。Pi 的示例插件原样运行。 | 同 1 级。 | 适配器,以及 Pi 扩展桥接的 Web 状态显示。 | +| 3 | 提供 `Transcript`,并把 Session 的插件选择随 Session 保存。 | 出现消费 `Transcript` 的插件,或 pi-durable 宣布 `ConversationView` 稳定。 | 从 Session 日志到 `ConversationView` 的投影,以及按 Session 保存的选择记录。 | +| 4 | `tui` facet 运行在浏览器中,经通用 Chord 桥访问 Session 服务。 | Chord 承诺支持浏览器,且 Pi 定义了不依赖终端的 presentation facet。 | 一个通用 JSON Remote 端点、一个浏览器 facet loader,以及 Client 命令的参数文本。 | +| 5 | 对等:每个 Session 一个 worker,插件可注册模型工具与钩子。 | Pi 把工具与钩子注册表暴露给 facet。 | 按 Session 的进程隔离,以及 Pi 扩展桥接已有的工具与钩子映射。 | + +## Pi 插件是什么 + +插件是一个包目录。它的 `package.json` 可以带 `chord.facets`,把 facet 名称映射到入口文件或将其禁用;没有该字段时,Pi 在 `src/session.ts` 与 `src/tui.ts` 存在的情况下使用它们。入口默认导出 `{ id, setup(env) }`。`setup` 是同步的,只做声明:`env.use` 与 `env.observe` 获取服务,`env.provide` 与 `env.provideMany` 发布服务,`env.replicatedState` 创建共享状态,`env.own`、`env.onActivate` 与 `env.onDeactivate` 挂接生命周期工作。Chord host 校验完整的依赖图,先激活提供方再激活消费方,并且能在消费方保留服务句柄的同时替换所声明服务不变的 facet。 + +服务器用 Chord 的 esbuild 打包器把每个入口构建为内容寻址的 CommonJS 文件,并登记在 `chord-facets.json` 中。Session worker 校验 SHA-256 摘要后用 `node:vm` 加载 `session` 入口,`require` 只解析该入口声明过的外部依赖。`tui` 入口作为 JSON 制品随服务响应到达各客户端,并以同样方式加载。该 loader 不是沙箱:插件代码以进程的全部权限运行。 + +`pi server -e ` 设置服务器的默认插件,`pi client -e ` 为单个 Session 选择插件。Pi 保存 Session 的选择,并在该 Session 的 worker 运行期间拒绝不同的选择。 + +Pi 向 facet 提供以下服务: + +| 作用域 | 服务 | 成员 | +|---|---|---| +| server | `SessionDirectory`、`SessionManagement`、`PresentationPlugins` | Session 列表状态;创建、删除、attach、detach;presentation 制品的构建与重载 | +| session | `AgentController` | `prompt`、`steer`、`followUp`、`cancelQueued`、`abort`、`compact`、`waitForPrompt` | +| session | `Models` | 可复制的目录与选择状态;`select`、`selectThinking`、`cycleThinking`、`getThinkingLevels`、`refresh` | +| session | `Transcript` | 根对话的 pi-durable `ConversationView`,以可复制状态提供 | +| session | `SessionPlugins` | `reload` | +| presentation,进程内 | `SlashCommands` | `register`、`replace`、`list`、`subscribe`;命令包含名称、描述、参数提示、可选的参数补全,以及 `run(args, context)` | +| presentation,进程内 | `PresentationUI` | `select(title, items, selected, context)`、`showStatus(message, context)` | + +非进程内的服务只能包含可复制的 JSON 状态,以及参数与结果均为 JSON、最后一个参数为 Chord `Context` 的方法。 + +## 1 级:session facet + +该包依赖某一精确版本的 `@earendil-works/chord`。它用 `bundleFacetPackage` 把每个选中的包构建到 Harness 主目录下的缓存,并用 `createFacetBundleLoader` 加载 `session` 入口。Chord 既不安装依赖也不运行生命周期脚本,因此包到达时必须已装好依赖。 + +loader 自行解析两个外部依赖。`@earendil-works/chord` 解析为该包自己的 Chord 实例,因为 Chord 通过模块实例持有的注册表识别可复制状态。`@earendil-works/pi-coding-agent/experimental/plugin` 解析为一个模块,它用 Pi 的 ID(`pi.agent-controller`、`pi.local.presentation-ui`、`pi.local.slash-commands`)定义服务 token;Chord 按 ID 字符串匹配服务,因此在 Pi 之外定义的 token 可以绑定到按 Pi 的 token 编译的 facet。 + +该包在 `agent/created` 中为每个 Agent 创建一个 Chord host,并随 Agent 的上下文一同释放。内置 facet 提供 Pi 的 Session 服务: + +| Pi 成员 | DSH 机制 | 差异 | +|---|---|---| +| `AgentController.prompt` | `agent.followup()` | 以 `busy` 拒绝的行为由 `agent.status` 推导。 | +| `steer`、`followUp` | `agent.steer()`、`agent.followup()` | 无。 | +| `cancelQueued` | `agent.inbox.remove()` | 区分 `already_consumed` 与 `not_found` 需要读取 Session 日志。 | +| `abort` | `agent.cancel()` | 无。 | +| `compact` | `ctx.compaction.compactNow()` | 拒绝非空的 `customInstructions`;DSH 压缩不接受指令。 | +| `waitForPrompt` | 对 Session 事件做 fold,从入队消息到它的 `turn/end` | DSH 不关联提示词与其回答;由该 fold 完成关联。 | +| `Models` | `session-controller` 的模型目录与模型选择 | 以推理强度标识符代替 Pi 的 thinking level。 | +| `SessionPlugins.reload` | 重新构建、调用 `FacetHost.reload()`,再释放退役的 generation | 无。 | + +本级不提供 `Transcript`。Chord 会拒绝含有“所需服务无人提供”的 facet 的 host,因此使用 `Transcript` 的插件加载失败,错误信息会指出该服务。 + +## 2 级:Host 上的 presentation facet + +Pi 的 `tui` facet 不使用任何终端 API。它们消费 `SlashCommands`、`PresentationUI` 与 Session 服务,因此可以运行在 DSH Host 上每个 Agent 的第二个 Chord host 中,并在进程内绑定到 Session host。 + +注册的斜杠命令成为 Agent 作用域的 `ctx.commands` 注册,其 handler 把原始输入作为 `args` 传入。在 Host 上运行的命令会记录为 `command/run` 与 `command/done`;Pi 不记录 presentation 命令。`getArgumentCompletions` 在 DSH 中没有对应物,不会被调用。`PresentationUI.select` 映射到 `ctx.userQuestions.ask()`,需要已连接的 Web Client;`showStatus` 映射到 Pi 扩展桥接为 Web Client 增加的状态显示。 + +## 3 级:transcript 与保存的选择 + +`Transcript` 需要从 DSH Session 事件到 pi-durable `ConversationView` 的投影:按 pi-durable 条目类型组织的条目,加上它的 live、inbox、agent 与 usage 文档。DSH 记录了 Pi 没有对应条目的事件,在这些地方投影是有损的;它还把 DSH 绑定到一个被 pi-durable 声明为不稳定的类型上。 + +Session 的插件选择保存为带 `plugin:` 前缀的 ignorable Session 记录,即 Pi 扩展桥接保存扩展状态所用的机制;与 Pi 一样,存活 Agent 的选择固定不变。 + +## 4 级:浏览器中的 presentation facet + +在页面中运行 `tui` facet 与 Pi 对 presentation 代码的放置一致。它需要三项新增。浏览器 loader 负责求值 CommonJS 制品,因为 Chord 的制品 loader 使用文件系统与 `node:vm`。一个通用 Remote 端点以 JSON 在页面与 Host 之间承载 Chord 的服务调用、快照与更新,因为 DSH 在构建时生成 Remote 方法,Client 不在运行时发现 Host 服务。Client 命令需要接受参数文本,目前 `ui-commands` 的 action 收不到它。 + +## 5 级:对等 + +Pi 把每个 Session 隔离在一个 worker 进程中,并计划通过其注册表向插件提供工具与钩子。要达到这一点,DSH Session 需要运行在自己的进程或 worker 线程中,并让 Chord 的服务协议跨越进程边界;在 Pi 暴露注册表之后,还需要 Pi 扩展桥接已经定义的工具、钩子与提示词映射。 + +## 插件模型暴露出的运行时差异 + +| 属性 | Chord 与 Pi | DSH | 受影响的级别 | +|---|---|---|---| +| 提供方替换 | 消费方持有句柄,每次访问时解析当前提供方;重载在句柄背后替换实现。 | 所注入服务发生变化的 fiber 会被卸载并重新运行。 | 1:Chord host 对 facet 之间的服务保持 Chord 的行为;映射进 Chord 的 DSH 服务仍会重启其适配器。 | +| 故障隔离 | 每个 Session 一个 worker 进程。 | 所有 Session 共享 Host 进程,未捕获的异常或未处理的 rejection 会释放整个应用([app-boot](../../../../packages/boot/app-boot/README.zh.md))。 | 1–4 在 Host 中运行插件代码;5 消除这一差异。 | +| Remote 声明 | host 根据运行时拿到的对象发布服务的远程成员。 | Remote 方法在构建时生成,Client 不在运行时发现 Host 服务([remotes](../../../../packages/api/remotes/README.zh.md))。 | 4 | +| 共享状态 | 可复制的 JSON 状态,带操作批次、hydration 与缺口恢复,本地与远程通过同一 API 读取。 | 每个领域定义自己的快照与增量类型,或发布完整的投影值。 | 3、4 | +| 运行时加载 | 每个 facet 一个 bundle,经完整性校验后在模块缓存之外加载。 | 包提前构建;已构建的安装不转译 TypeScript。 | 1 把 Chord 的打包器加为插件包的依赖。 | + +## 值得为 DSH 评估的 Chord 机制 + +有四项 Chord 机制针对的是 DSH 自身存在、与 Pi 无关的限制。每一项都需要单独的提案;本 Agent Note 只做记录。 + +- **稳定的服务句柄。** 提供方被替换时消费方保留句柄,因此插件重载不会重启其依赖方并丢失它们的状态。 +- **先声明后激活。** setup 同步声明全部依赖,host 在任何 facet 激活之前拒绝缺失的提供方、重复的提供方或循环依赖。Cordis 随服务出现逐步解析依赖,因此缺少提供方的 fiber 会一直挂起,直到启动后的审计报告它。 +- **可复制的 JSON 状态。** 一个原语为本地与远程消费方提供完整的不可变值,线上传输操作批次,订阅方落后时做一次完整重置。 +- **经过校验的插件制品。** 带 manifest 摘要的内容寻址 bundle 让 host 在模块缓存之外加载某一代插件,并在其退役后释放。 + +## 考虑过的替代方案 + +**现在就把 1、2 级做成原型。** 该 API 未发布,只有一个示例插件,其 services README 已把三个 `AgentController` 方法列为已移除。原型需要跟随 Pi 的每次发版维护,却没有用户。上文的服务映射保留了设计结论,而不必付出这项成本。 + +**不留记录,等 Pi 稳定后再决定。** 服务映射与运行时对比来自对 Chord 以及 Pi 实验性 server、worker 与 client 的完整阅读。这些运行时差异同样适用于与 Pi 无关的 DSH 工作。 + +**不用 Chord,直接在 Cordis 上实现 Pi 的插件服务。** facet 直接调用 Chord 的 API:`env.use`、`env.provide` 与 `replicatedState`。不用 Chord 而提供这套 API,意味着重新实现依赖图校验、稳定句柄、keyed 服务与可复制状态。Chord 采用 MIT 许可,只有一个运行时依赖,而且插件正是针对它编译的。 + +**把插件并入 Pi 扩展桥接包。** 两套机制没有共同的 API,稳定性也不同。分成独立的包,才能各自启用、定版与移除。 + +**只在浏览器中运行 presentation facet。** 2 级原样复用 Pi 的 Node 制品,不需要浏览器 loader。浏览器支持在 Chord 的规划中仍是未决事项,Pi 也没有不依赖终端的 presentation facet。 + +## 验收标准 + +- **0 级:** 本 Agent Note 已合并,Pi 扩展兼容性页面在插件机制处链接到它。 +- **1 级:** Pi 示例插件的 `session` facet 从未修改的包目录加载。测试在使用 mock 模型的真实 agent loop 上调用它的问候服务以及每个已提供的 `AgentController` 成员。`SessionPlugins.reload` 替换 facet generation 而不重建 Chord host,释放 Agent 时 host 随之释放。 +- **2 级:** Pi 示例插件的 `/hello ` 经 `ctx.commands` 运行,在 Web Client 中显示其状态文本,并提交其提示词。 +- **3 级:** 订阅 `Transcript` 的 facet 在 hydration 之后以及每个已提交的步骤之后收到根对话,恢复的 Session 加载其创建时所用的插件。 +- **4 级:** Pi 示例插件的 `tui` facet 在浏览器中运行,其服务调用经通用端点到达 Host。 +- **每一级:** 兼容性页面列出与 Pi 的每一处差异,包的 README 说明插件代码以 Host 的全部权限运行。 + +## 风险 + +- 在任何一级构建之前,上游 API 都可能变化或被撤回。服务映射描述的是 Pi 1.0.0,需要对照触发这项工作的发行版重新核对。 +- 1 至 4 级在 Host 进程中运行插件代码,一个未捕获的错误会中止所有 Session。 +- 3 级依赖一个被上游声明为不稳定的 pi-durable 类型。 +- 4 级增加的 Remote 端点由 Chord 的 wire 解析器校验载荷,而不是由按方法生成的编解码器校验。 +- 插件包经由 Chord 的打包器引入 esbuild 作为运行时依赖。 diff --git a/.agents/skills/record-browser-gif/SKILL.md b/.agents/skills/record-browser-gif/SKILL.md index 73fbc7fb67..a16f8294e9 100644 --- a/.agents/skills/record-browser-gif/SKILL.md +++ b/.agents/skills/record-browser-gif/SKILL.md @@ -27,7 +27,7 @@ The recording itself is part of the evidence: use a real server booted from that A GIF for a specific pull request demonstrates that pull request's tree, so stage per pull request: 1. Require a clean worktree, record its exact commit with `git rev-parse HEAD`, then build that recorded tree — here, `pnpm run build && pnpm run build:web`. A GIF recorded against another commit's build misattributes the evidence. -2. Boot one server per port from that tree with fresh scratch `DSH_HOME`, `DSH_AGENTS_HOME`, workspace, and session state. Give the browser a fresh isolated context or profile as well; if the browser workflow cannot create one, clear that origin's cookies and site storage before navigation so persisted client state cannot affect the evidence. Source the root `.env` for the API key through the application's normal path; never echo the key. +2. Boot one server per port from that tree with fresh scratch `DSH_HOME`, `DSH_AGENTS_HOME`, workspace, and session state. Give the browser a fresh isolated context or profile as well; if the browser workflow cannot create one, clear that origin's cookies and site storage before navigation so persisted client state cannot affect the evidence. Real API credentials can be available in `~/.dsh/.credentials.yaml` even when `DEEPSEEK_API_KEY` and the root `.env` are absent. With a scratch `DSH_HOME`, use a recording overlay to set the `dsh-credentials-local` provider's `path` to that existing file's absolute path. Let the application's normal credential provider resolve the key; never echo it. 3. Treat one storyboard as one evidence run: every published frame comes from that server and those state roots, workspace, session, and model-backed scenario run. If capture automation fails, discard its frames and rerun from fresh roots; never splice frames from separate runs. 4. When switching between pull requests, stop the old server by PID or an exact match on its command line. A broad `pkill -f` pattern can match and kill the shell that launched it — including your own. diff --git a/.github/review-ownership/requirements.txt b/.github/review-ownership/requirements.txt index a0c04bed92..3429a636c6 100644 --- a/.github/review-ownership/requirements.txt +++ b/.github/review-ownership/requirements.txt @@ -1 +1 @@ -Pygments==2.19.2 +Pygments==2.21.0 diff --git a/.github/workflows/build-exe-for-python-sdk.yml b/.github/workflows/build-exe-for-python-sdk.yml index e53d524ead..9ea51311b6 100644 --- a/.github/workflows/build-exe-for-python-sdk.yml +++ b/.github/workflows/build-exe-for-python-sdk.yml @@ -72,7 +72,7 @@ jobs: version: ${{ steps.version.outputs.version }} repository-version: ${{ steps.version.outputs.repository-version }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 - name: Resolve repository version id: version @@ -130,7 +130,7 @@ jobs: && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 5 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 - uses: actions/setup-python@v6.3.0 with: @@ -145,7 +145,7 @@ jobs: --package sdk --output-dir dist-python - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@v7.0.1 with: name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl path: dist-python/deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl @@ -168,11 +168,11 @@ jobs: matrix: include: ${{ fromJSON(needs.plan.outputs.matrix) }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} @@ -185,7 +185,7 @@ jobs: # setup-node's built-in pnpm store cache keys on platform AND arch, so # the Linux architectures sharing runner.os stay on separate caches. - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: 24 cache: pnpm @@ -199,7 +199,7 @@ jobs: # Cache pkg's target Node binary; lockfile changes roll the # exact key while the restore prefix can seed its replacement. - - uses: actions/cache@v4 + - uses: actions/cache@v6.1.0 with: path: ~/.pkg-cache key: pkg-fetch-${{ matrix.target }}-${{ hashFiles('pnpm-lock.yaml') }} @@ -232,7 +232,8 @@ jobs: esac addon_dir="$(realpath packages/subprocess/subprocess-local/node_modules/node-pty)" pnpm_setup_root="$(realpath "$(dirname "$(dirname "$PNPM_HOME")")")" - (cd "$addon_dir" && npm_config_build_from_source=true pnpm run install) + # Verification installs would install node-gyp inside the package and package it into the exe. + (cd "$addon_dir" && npm_config_build_from_source=true pnpm --config.verifyDepsBeforeRun=false run install) addon="$addon_dir/build/Release/pty.node" [ -f "$addon_dir/build/Makefile" ] || { echo "::error::node-pty install did not generate $addon_dir/build/Makefile" @@ -259,7 +260,9 @@ jobs: - name: Build single-exe env: DSH_BUILD_CLIENT_PROFILE: official - run: pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=${{ matrix.target }} + run: >- + pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=${{ matrix.target }} + ${{ inputs.ci && github.event_name == 'pull_request' && '--artifacts-only' || '' }} - name: Resolve platform outputs (POSIX) id: runtime-posix @@ -308,7 +311,7 @@ jobs: --runtime-exe "${{ steps.runtime-posix.outputs.exe || steps.runtime-windows.outputs.exe }}" --output-dir dist-python - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@v8.0.1 with: name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl path: dist-python @@ -497,7 +500,7 @@ jobs: env -u PYTHONPATH -u DSH_RUNTIME_MODE /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-mcp --installed-wheel ' - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@v7.0.1 with: name: ${{ steps.runtime-posix.outputs.wheel || steps.runtime-windows.outputs.wheel }} path: dist-python/${{ steps.runtime-posix.outputs.wheel || steps.runtime-windows.outputs.wheel }} diff --git a/.github/workflows/build-preview-cloudflare.yml b/.github/workflows/build-preview-cloudflare.yml index 80caebdb76..e8ad9bd2ec 100644 --- a/.github/workflows/build-preview-cloudflare.yml +++ b/.github/workflows/build-preview-cloudflare.yml @@ -35,15 +35,15 @@ jobs: runs-on: ubuntu-24.04 name: cloudflare pages preview steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} @@ -57,7 +57,7 @@ jobs: # Read-only: the preview lane consumes the default-branch cache without # putting cache upload on its own path. - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@v6.1.0 with: path: ${{ steps.pnpm-store.outputs.path }} key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} @@ -69,9 +69,9 @@ jobs: # apps/web consumes workspace packages as built lib products, and # build:preview packs the image through the packer's installed bin - # (lib/bin.js), so neither half exists before the full build runs. + # (lib/bin.js), so neither half exists before the artifact build runs. - name: Build workspace - run: pnpm run build + run: pnpm run build --artifacts-only - name: Build the preview page and pack the VFS image env: diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml index 2fdaecb505..5feb9fb33c 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -48,15 +48,15 @@ jobs: name: windows node 24 / wine timeout-minutes: 15 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} @@ -68,7 +68,7 @@ jobs: store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent) echo "path=$store_path" >> "$GITHUB_OUTPUT" - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@v6.1.0 with: path: ${{ steps.pnpm-store.outputs.path }} key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} @@ -80,7 +80,7 @@ jobs: id: wine-cache-key run: echo "key=wine-debs-${ImageOS:-linux}-${ImageVersion:-v0}" >> "$GITHUB_OUTPUT" - - uses: actions/cache@v4 + - uses: actions/cache@v6.1.0 with: path: ~/wine-debs key: ${{ steps.wine-cache-key.outputs.key }} @@ -139,7 +139,7 @@ jobs: # github.event.before, so full history is required: depth 2 would miss it # on multi-commit or force pushes; full fetch is cheap here because # checkout resolves against the VM's local mirror. - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: fetch-depth: 0 @@ -149,11 +149,11 @@ jobs: - name: Redirect Node compile cache to runner temp run: echo "NODE_COMPILE_CACHE=${{ runner.temp }}/node-compile-cache" >> "$GITHUB_ENV" - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} @@ -191,13 +191,13 @@ jobs: name: serial / macos runs-on: macos-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} @@ -230,7 +230,7 @@ jobs: runs-on: [self-hosted, dsh-win-ci, windows] timeout-minutes: 120 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 - name: Enable Developer Mode (symlink support) shell: pwsh @@ -238,11 +238,11 @@ jobs: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} @@ -356,15 +356,15 @@ jobs: blacksmith: '' workload: production-site steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} # The benchmark's Windows lanes deliberately skip the store cache like # the independent native Windows job; an empty input disables caching. - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} cache: ${{ matrix.platform == 'linux' && 'pnpm' || '' }} @@ -467,15 +467,15 @@ jobs: blacksmith: '' workers: '2' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} # Unlike the larger-runner suite, both platforms cache the store here: # the consolidated topology measures cache mechanics as workload. - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} cache: pnpm diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdcec2540d..93c3d5293c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: echo "TMPDIR=${{ runner.temp }}" >> "$GITHUB_ENV" # Fetch complete history so the archive gate can read the trusted PR base from a reused shallow checkout. - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: fetch-depth: 0 persist-credentials: false @@ -71,11 +71,11 @@ jobs: - name: Redirect Node compile cache to runner temp run: echo "NODE_COMPILE_CACHE=${{ runner.temp }}/node-compile-cache" >> "$GITHUB_ENV" - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} @@ -90,7 +90,7 @@ jobs: # Pull requests consume the default-branch cache but do not put cache # compression and upload on the paid latency-critical path. Skipped # under failover — see the coverage lane's identical rationale. - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@v6.1.0 if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]' with: path: ${{ steps.pnpm-store.outputs.path }} @@ -137,20 +137,28 @@ jobs: run: | echo "TMPDIR=${{ runner.temp }}" >> "$GITHUB_ENV" - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false + - name: Restore coverage duration history + uses: actions/cache/restore@v6.1.0 + with: + path: .coverage-times.json + key: coverage-times-Linux-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_LINUX || 'enterprise' }}-${{ github.run_id }}-${{ github.run_attempt }} + restore-keys: | + coverage-times-Linux-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_LINUX || 'enterprise' }}- + # Redirect the Node compile cache (enabled by pnpm and TypeScript) off # the root partition's /tmp before the first pnpm call in this lane. - name: Redirect Node compile cache to runner temp run: echo "NODE_COMPILE_CACHE=${{ runner.temp }}/node-compile-cache" >> "$GITHUB_ENV" - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} @@ -164,7 +172,7 @@ jobs: # Self-hosted runners reuse the shared persistent pnpm store. # Hosted runners restore the remote cache into their ephemeral store. - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@v6.1.0 if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]' with: path: ${{ steps.pnpm-store.outputs.path }} @@ -188,6 +196,13 @@ jobs: - name: Run exhaustive coverage run: pnpm run check:ci:coverage + - name: Save coverage duration history + if: ${{ !cancelled() }} + uses: actions/cache/save@v6.1.0 + with: + path: .coverage-times.json + key: coverage-times-Linux-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_LINUX || 'enterprise' }}-${{ github.run_id }}-${{ github.run_attempt }} + node-24-bench: if: github.event_name == 'pull_request' runs-on: ubuntu-24.04 @@ -196,15 +211,15 @@ jobs: # benchmark lane alone instead of joining a concurrent gate aggregate. timeout-minutes: 20 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} @@ -216,7 +231,7 @@ jobs: store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent) echo "path=$store_path" >> "$GITHUB_OUTPUT" - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@v6.1.0 with: path: ${{ steps.pnpm-store.outputs.path }} key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} @@ -234,6 +249,20 @@ jobs: DSH_GATE_VERBOSE: '1' run: pnpm run check:ci:bench + # The report global teardown renders benchmarks/.dsh-report after the run, + # including after a failing budget assertion. A run that fails before + # recording any timing has no report, so missing files warn instead of + # failing the benchmark job a second time. + - name: Upload scaled benchmark report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7.0.1 + with: + name: benchmark-report + path: benchmarks/.dsh-report/ + include-hidden-files: true + if-no-files-found: warn + retention-days: 7 + node-24-consumers: if: github.event_name == 'pull_request' runs-on: >- @@ -262,7 +291,7 @@ jobs: echo "TMPDIR=${{ runner.temp }}" >> "$GITHUB_ENV" echo "PLAYWRIGHT_BROWSERS_PATH=${RUNNER_TEMP%/*}/ms-playwright" >> "$GITHUB_ENV" - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false @@ -271,11 +300,11 @@ jobs: - name: Redirect Node compile cache to runner temp run: echo "NODE_COMPILE_CACHE=${{ runner.temp }}/node-compile-cache" >> "$GITHUB_ENV" - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} @@ -292,7 +321,7 @@ jobs: # hosted cache keys, so each restore-keys fallback hits the matching archived # entry until it evicts, after which the store is cold. Self-hosted runners # reuse the shared persistent store instead of restoring the remote archive. - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@v6.1.0 if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]' with: path: ${{ steps.pnpm-store.outputs.path }} @@ -301,7 +330,7 @@ jobs: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm- # Self-hosted runners retain their own browser cache between jobs. - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@v6.1.0 if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]' with: path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} @@ -383,6 +412,7 @@ jobs: DSH_GATE_FAIL_FAST: '1' strategy: fail-fast: false + max-parallel: 1 matrix: include: - node: '22.19' @@ -401,7 +431,7 @@ jobs: runner: ubuntu-latest gate_concurrency: '1' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false @@ -413,11 +443,11 @@ jobs: echo "npm_config_devdir=$RUNNER_TEMP/node-gyp" >> "$GITHUB_ENV" echo "PNPM_CONFIG_STORE_DIR=$HOME/.local/share/pnpm/store" >> "$GITHUB_ENV" - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 env: # The runner overwrites RUNNER_* step env before starting JavaScript actions. NODE_OPTIONS: ${{ runner.environment == 'self-hosted' && '--import=./scripts/ci-compatible-toolcache.mjs' || '' }} @@ -458,7 +488,7 @@ jobs: runs-on: ubuntu-latest name: python 3.10 / keyless SDK steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 - uses: actions/setup-python@v6.3.0 with: @@ -468,7 +498,7 @@ jobs: - name: Install uv run: python -m pip install uv==0.11.23 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: 24 @@ -483,6 +513,7 @@ jobs: # Linux ARM64 and both macOS architectures run in ci-master.yml. python-runtime: if: github.event_name == 'pull_request' + needs: python-sdk name: python runtime / release-shaped matrix uses: ./.github/workflows/build-exe-for-python-sdk.yml with: @@ -511,7 +542,7 @@ jobs: # A failing build or site aborts the sibling gate on the same runner. DSH_GATE_FAIL_FAST: '1' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false - name: Enable Developer Mode (symlink support) @@ -519,10 +550,10 @@ jobs: run: >- reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} - name: Install (immutable) @@ -579,7 +610,7 @@ jobs: # multi-minute instrumented run. DSH_GATE_FAIL_FAST: '1' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false # Checkout removes the gitignored duration history, so restore it from @@ -589,21 +620,21 @@ jobs: # entries are immutable, so the save key is unique per run and the # restore matches the newest entry through the stable prefix. - name: Restore coverage duration history - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v6.1.0 with: path: .coverage-times.json - key: coverage-times-${{ github.run_id }} + key: coverage-times-Windows-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_WINDOWS || 'enterprise' }}-${{ github.run_id }}-${{ github.run_attempt }} restore-keys: | - coverage-times- + coverage-times-Windows-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_WINDOWS || 'enterprise' }}- - name: Enable Developer Mode (symlink support) shell: pwsh run: >- reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} - name: Install (immutable) @@ -632,10 +663,10 @@ jobs: # measured durations remain useful even when a partition failed. The # per-run key keeps every save a fresh immutable cache entry. if: ${{ !cancelled() }} - uses: actions/cache/save@v4 + uses: actions/cache/save@v6.1.0 with: path: .coverage-times.json - key: coverage-times-${{ github.run_id }} + key: coverage-times-Windows-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_WINDOWS || 'enterprise' }}-${{ github.run_id }}-${{ github.run_attempt }} windows-native-tests: if: github.event_name == 'pull_request' @@ -648,7 +679,7 @@ jobs: name: windows node 24 / native tests timeout-minutes: 60 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false - name: Enable Developer Mode (symlink support) @@ -656,10 +687,10 @@ jobs: run: >- reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} - name: Install (immutable) @@ -683,7 +714,7 @@ jobs: --testTimeout 90000 packages/shell/tool-pwsh/tests/loader.spec.ts packages/workflow/workflow-ptc/tests/workflow-ptc.spec.ts - packages/workflow/tool-ralph/tests/integration.spec.ts + packages/experimental/tool-ralph/tests/integration.spec.ts packages/subprocess/subprocess-local/tests/process-exit.spec.ts # Single stable required check for branch protection: require "all checks diff --git a/.github/workflows/docs-pages.yml b/.github/workflows/docs-pages.yml index 6cd25b059d..6c6cc155d4 100644 --- a/.github/workflows/docs-pages.yml +++ b/.github/workflows/docs-pages.yml @@ -41,14 +41,14 @@ jobs: pages: read steps: # Complete history: the release scripts read tags. - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} cache: pnpm diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 8b3bfc1400..397ac07b45 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -67,11 +67,11 @@ jobs: # and retry 2; 45m still bounds retry storms against a slow API. timeout-minutes: 45 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: 24 cache: pnpm @@ -107,7 +107,7 @@ jobs: # the built artifact under plain Node, resolving plugins through real package # exports — the shape a real consumer runs. That requires a prior build. - name: Build (lib for the e2e example bins) - run: pnpm run build:official + run: ${{ github.event_name == 'pull_request' && 'pnpm run build --artifacts-only --profile official' || 'pnpm run build:official' }} - name: Prepare Office runtime run: pnpm run prepare:primary-runtime --target linux-x64 --output .cache/e2e-office diff --git a/.github/workflows/expected-filenames.yml b/.github/workflows/expected-filenames.yml index 99c9886e35..12ff5d7bc7 100644 --- a/.github/workflows/expected-filenames.yml +++ b/.github/workflows/expected-filenames.yml @@ -22,7 +22,7 @@ jobs: ${{ vars.DSH_CI_FAILOVER_LINUX == 'blacksmith' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 - name: Check tracked filenames run: scripts/check-expected-filenames.sh diff --git a/.github/workflows/node-addon-system-release.yml b/.github/workflows/node-addon-system-release.yml index 6c5596232b..fef93c885b 100644 --- a/.github/workflows/node-addon-system-release.yml +++ b/.github/workflows/node-addon-system-release.yml @@ -32,7 +32,7 @@ jobs: outputs: prebuilds: ${{ steps.matrix.outputs.prebuilds }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - id: matrix run: echo "prebuilds=$(node ./scripts/github-matrix.mjs release-prebuild)" >> "$GITHUB_OUTPUT" @@ -45,13 +45,13 @@ jobs: fail-fast: false matrix: ${{ fromJson(needs.matrix.outputs.prebuilds) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: package_json_file: package.json - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7.0.0 with: node-version: 24 cache: pnpm @@ -80,7 +80,7 @@ jobs: pnpm test:packaging - name: Upload prebuild artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7.0.1 with: name: ${{ matrix.artifact }} path: native/system/${{ matrix.dir }}/bin/** @@ -92,13 +92,13 @@ jobs: needs: build-prebuilds runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: package_json_file: package.json - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7.0.0 with: node-version: 24 cache: pnpm @@ -116,7 +116,7 @@ jobs: RELEASE_PUBLISH: ${{ inputs.publish }} - name: Download prebuild artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8.0.1 with: pattern: prebuild-* path: native/system/.release/prebuild-artifacts @@ -138,7 +138,7 @@ jobs: NALR_REQUIRE_LANDLOCK: 1 - name: Upload npm tarballs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7.0.1 with: name: npm-tarballs path: native/system/dist/npm/* @@ -157,17 +157,17 @@ jobs: steps: # The publish script is the only repository file this job needs, and it # imports nothing outside Node's builtins, so there is no install step. - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7.0.0 with: node-version: 24 registry-url: https://registry.npmjs.org - name: Download npm tarballs - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8.0.1 with: name: npm-tarballs path: native/system/dist/npm diff --git a/.github/workflows/node-addon-system.yml b/.github/workflows/node-addon-system.yml index a763a2a8d2..c50411aeba 100644 --- a/.github/workflows/node-addon-system.yml +++ b/.github/workflows/node-addon-system.yml @@ -48,7 +48,7 @@ jobs: outputs: ci: ${{ steps.matrix.outputs.ci }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - id: matrix run: echo "ci=$(node ./scripts/github-matrix.mjs ci)" >> "$GITHUB_OUTPUT" @@ -61,13 +61,15 @@ jobs: fail-fast: false matrix: ${{ fromJson(needs.matrix.outputs.ci) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - - uses: pnpm/action-setup@v4 + # pnpm v11 ships no darwin-x64 standalone binary, and the pnpm/action-setup + # v6 self-update path needs one; this job stays on the v5 npm-install line. + - uses: pnpm/action-setup@v5.0.0 with: package_json_file: package.json - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7.0.0 with: node-version: 24 cache: pnpm @@ -125,7 +127,7 @@ jobs: node:24-alpine node --test ./test/flock.test.js ./test/package-matrix.test.js - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7.0.0 with: node-version: 20 @@ -139,7 +141,7 @@ jobs: node:20-alpine node --test ./test/flock.test.js ./test/package-matrix.test.js - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7.0.0 with: node-version: 22 @@ -153,7 +155,7 @@ jobs: node:22-alpine node --test ./test/flock.test.js ./test/package-matrix.test.js - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7.0.0 with: node-version: 26 diff --git a/.github/workflows/pi-ai-provider-e2e.yml b/.github/workflows/pi-ai-provider-e2e.yml index 255c7654e7..5a4774e705 100644 --- a/.github/workflows/pi-ai-provider-e2e.yml +++ b/.github/workflows/pi-ai-provider-e2e.yml @@ -30,11 +30,11 @@ jobs: name: Azure OpenAI Responses + Anthropic Messages timeout-minutes: 20 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: 24 cache: pnpm diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index b756f5bf38..0474ccfe92 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -40,7 +40,7 @@ jobs: matrix: python: ['3.10', '3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false @@ -48,7 +48,7 @@ jobs: with: python-version: ${{ matrix.python }} - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@v8.0.1 with: pattern: deepseek_harness_* path: dist @@ -83,7 +83,7 @@ jobs: outputs: version: ${{ steps.version.outputs.version }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false @@ -134,7 +134,7 @@ jobs: } fi - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@v8.0.1 with: pattern: deepseek_harness_* path: dist @@ -175,7 +175,7 @@ jobs: sha256sum *.whl | sort -k2 > SHA256SUMS cat SHA256SUMS - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@v7.0.1 with: name: python-release-${{ steps.version.outputs.version }} path: dist/* @@ -193,7 +193,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@v8.0.1 with: name: python-release-${{ needs.validate.outputs.version }} path: dist @@ -228,7 +228,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@v8.0.1 with: name: python-release-${{ needs.validate.outputs.version }} path: dist diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index cdee5ce196..ec03a117de 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -21,16 +21,16 @@ jobs: runs-on: ubuntu-24.04 steps: # Complete history: the release scripts read tags. - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} @@ -42,7 +42,7 @@ jobs: store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent) echo "path=$store_path" >> "$GITHUB_OUTPUT" - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@v6.1.0 with: path: ${{ steps.pnpm-store.outputs.path }} key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} @@ -82,7 +82,7 @@ jobs: - name: Verify packed install run: pnpm run release:verify-packed-install --family dsh --from dist/npm --from dist/npm-vendor --from dist/npm-landlock - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7.0.1 with: name: dsh-npm-tarballs path: dist/npm/* @@ -104,15 +104,15 @@ jobs: steps: # Checkout and install carry the release scripts only. There is no build # step: publication uploads the bytes the pack job produced. - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} registry-url: https://registry.npmjs.org @@ -120,7 +120,7 @@ jobs: - name: Install (immutable, no package scripts) run: pnpm install --frozen-lockfile --ignore-scripts - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8.0.1 with: name: dsh-npm-tarballs path: dist/npm diff --git a/.github/workflows/release-vendor-publish.yml b/.github/workflows/release-vendor-publish.yml index a50a408025..25ec400b27 100644 --- a/.github/workflows/release-vendor-publish.yml +++ b/.github/workflows/release-vendor-publish.yml @@ -7,6 +7,12 @@ name: Release publish (vendor) on: workflow_dispatch: + inputs: + dist-tag: + description: Optional unused npm channel. Leave empty for latest (stable) or next (prerelease). + required: false + type: string + default: '' permissions: contents: read @@ -21,16 +27,16 @@ jobs: runs-on: ubuntu-24.04 steps: # Complete history: the release scripts read tags. - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} @@ -42,7 +48,7 @@ jobs: store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent) echo "path=$store_path" >> "$GITHUB_OUTPUT" - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@v6.1.0 with: path: ${{ steps.pnpm-store.outputs.path }} key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} @@ -68,7 +74,7 @@ jobs: - name: Verify packed install run: pnpm run release:verify-packed-install --family vendor --from dist/npm-vendor - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7.0.1 with: name: vendor-npm-tarballs path: dist/npm-vendor/* @@ -87,15 +93,15 @@ jobs: contents: read steps: # Checkout and install carry the release scripts only; no build step. - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} registry-url: https://registry.npmjs.org @@ -103,7 +109,7 @@ jobs: - name: Install (immutable, no package scripts) run: pnpm install --frozen-lockfile --ignore-scripts - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8.0.1 with: name: vendor-npm-tarballs path: dist/npm-vendor @@ -111,4 +117,10 @@ jobs: - name: Publish tarballs env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: pnpm run release:publish --family vendor --from dist/npm-vendor + RELEASE_DIST_TAG: ${{ inputs.dist-tag }} + run: | + args=() + if [[ -n "$RELEASE_DIST_TAG" ]]; then + args+=(--dist-tag "$RELEASE_DIST_TAG") + fi + pnpm run release:publish --family vendor --from dist/npm-vendor "${args[@]}" diff --git a/.github/workflows/release-vendor.yml b/.github/workflows/release-vendor.yml index 75fb865767..f8bf170c1c 100644 --- a/.github/workflows/release-vendor.yml +++ b/.github/workflows/release-vendor.yml @@ -43,7 +43,7 @@ jobs: || 'ubuntu-24.04' }} steps: # Complete history: the release scripts read tags. - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: fetch-depth: 0 persist-credentials: false @@ -55,11 +55,11 @@ jobs: echo "npm_config_devdir=${{ runner.temp }}/node-gyp" >> "$GITHUB_ENV" echo "TMPDIR=${{ runner.temp }}" >> "$GITHUB_ENV" - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} package-manager-cache: false @@ -72,7 +72,7 @@ jobs: store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent) echo "path=$store_path" >> "$GITHUB_OUTPUT" - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@v6.1.0 if: runner.environment == 'github-hosted' with: path: ${{ steps.pnpm-store.outputs.path }} @@ -89,7 +89,7 @@ jobs: # The vendored packages publish their own sources and build outputs; the # host build produces what their manifests select. - name: Build - run: pnpm run build:lib:host + run: ${{ github.event_name == 'pull_request' && 'pnpm exec tsx scripts/compile-referenced-projects.ts host-libraries' || 'pnpm run build:lib:host' }} # Concurrency here is rehearsal-only: the credentialed publish workflows # invoke release:pack without the flag and keep the strictly serial path. @@ -99,7 +99,7 @@ jobs: - name: Verify packed install run: pnpm run release:verify-packed-install --family vendor --from dist/npm-vendor - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7.0.1 with: name: vendor-npm-tarballs path: dist/npm-vendor/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 633186c8f1..7fd8c9ab8a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]') || 'ubuntu-24.04' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: persist-credentials: false clean: true @@ -52,11 +52,11 @@ jobs: echo "npm_config_devdir=${{ runner.temp }}/node-gyp" >> "$GITHUB_ENV" echo "TMPDIR=${{ runner.temp }}" >> "$GITHUB_ENV" - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} package-manager-cache: false @@ -69,7 +69,7 @@ jobs: store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent) echo "path=$store_path" >> "$GITHUB_OUTPUT" - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@v6.1.0 if: runner.environment == 'github-hosted' with: path: ${{ steps.pnpm-store.outputs.path }} @@ -102,7 +102,7 @@ jobs: || 'ubuntu-24.04' }} steps: # Complete history: the release scripts read tags. - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 with: fetch-depth: 0 persist-credentials: false @@ -114,11 +114,11 @@ jobs: echo "npm_config_devdir=${{ runner.temp }}/node-gyp" >> "$GITHUB_ENV" echo "TMPDIR=${{ runner.temp }}" >> "$GITHUB_ENV" - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} package-manager-cache: false @@ -131,7 +131,7 @@ jobs: store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent) echo "path=$store_path" >> "$GITHUB_OUTPUT" - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@v6.1.0 if: runner.environment == 'github-hosted' with: path: ${{ steps.pnpm-store.outputs.path }} @@ -146,7 +146,7 @@ jobs: run: pnpm run release:verify --family dsh - name: Build - run: pnpm run build:official + run: ${{ github.event_name == 'pull_request' && 'pnpm run build --artifacts-only --profile official' || 'pnpm run build:official' }} # Concurrency here is rehearsal-only: the credentialed publish workflows # invoke release:pack without the flag and keep the strictly serial path. @@ -172,7 +172,7 @@ jobs: - name: Verify packed install run: pnpm run release:verify-packed-install --family dsh --from dist/npm --from dist/npm-vendor --from dist/npm-landlock - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7.0.1 with: name: dsh-npm-tarballs path: dist/npm/* diff --git a/.github/workflows/sandbox.yml b/.github/workflows/sandbox.yml index 46c6ac71f2..1494b8d1db 100644 --- a/.github/workflows/sandbox.yml +++ b/.github/workflows/sandbox.yml @@ -66,11 +66,11 @@ jobs: || matrix.os }} timeout-minutes: 20 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: 24 @@ -146,11 +146,11 @@ jobs: runs-on: macos-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5.0.0 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.0.0 with: node-version: 24 diff --git a/.gitignore b/.gitignore index 86e330fc0c..3cb86ce87d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ packages/**/__pycache__/ apps/web/dist/ .artifacts/ .dsh-build/ +.dsh-report/ .playwright-mcp/ .orig .worktrees/ diff --git a/.oxlintrc.json b/.oxlintrc.json index 348b057bc3..f52fdb1e84 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -23,6 +23,7 @@ "packages/typert/generator/tests/fixtures/type-model/**", // tsgolint rejects this fixture's preserved project shapes before rules run. "website/.generated/**", "vendor/**", // Vendored source keeps upstream style and idioms. + "packages/experimental/claude-code-mods/examples/*/{hooks,tests,types}/**", // Claude Code's published example mods and their tests keep the upstream style; see the headers. "native/**", // The imported landlock-run subtree has its own gates; see native/README.md. "**/*.js", "**/*.mjs", diff --git a/AGENTS.md b/AGENTS.md index 4940e9b484..38e5ae46cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,6 @@ packages/ @deepseek-ai/dsh- workspaces at packages/// jobs/ background jobs bundle/ profile bundles workflow/ workflow execution - webhook/ webhook ingress todo/ todo_write tool plan/ logged planning goal/ session goals @@ -48,7 +47,6 @@ packages/ @deepseek-ai/dsh- workspaces at packages/// preset/ agent composition guard/ loop/tool guards extensions/ runtime self-modification - hooks/ Claude Code/Codex bridges session/ durable sessions session-query/ browsing/search/export attachment/ binary attachments @@ -66,7 +64,7 @@ packages/ @deepseek-ai/dsh- workspaces at packages/// host/ GUI host client/ GUI client mcp/ external tools - experimental/ pre-stable prototypes; public by default with explicit private exceptions + experimental/ evaluated capabilities without a product-support commitment test-support/ test infrastructure util/ zero-dependency utilities python/ Python SDK/runtime (python/README.md) @@ -78,7 +76,7 @@ scripts/ gates and generators website/ VitePress documentation projection ``` -Package groups: [packages/README.md](packages/README.md). +Package groups: [packages/README.md](packages/README.md). [Experimental status](packages/experimental/README.md#status) is independent of optional delivery; nonexperimental packages need product use or an explicit [classification](scripts/product-package-policy.ts). ## Commands diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 3602ce87cb..741c98db08 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -141,7 +141,7 @@ External packages installed for runtime use or distributed inside the prebuilt b pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification: -- `@earendil-works/pi-ai@0.87.1` — [`patches/@earendil-works__pi-ai@0.87.1.patch`](patches/@earendil-works__pi-ai@0.87.1.patch) +- `@earendil-works/pi-ai@1.0.2` — [`patches/@earendil-works__pi-ai@1.0.2.patch`](patches/@earendil-works__pi-ai@1.0.2.patch) - `@electron/osx-sign@1.3.3` — [`patches/@electron__osx-sign@1.3.3.patch`](patches/@electron__osx-sign@1.3.3.patch) - `@fortune-sheet/core@1.0.4` — [`patches/@fortune-sheet__core@1.0.4.patch`](patches/@fortune-sheet__core@1.0.4.patch) - `@fortune-sheet/react@1.0.4` — [`patches/@fortune-sheet__react@1.0.4.patch`](patches/@fortune-sheet__react@1.0.4.patch) diff --git a/apps/cli/composition.md b/apps/cli/composition.md index e627d9e47d..9888602296 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -64,6 +64,10 @@ flowchart LR cfg --> plugin_dsh_base_attachment_local plugin_dsh_base_session_query_sqlite["session-query-sqlite
@deepseek-ai/dsh-session-query-sqlite"] cfg --> plugin_dsh_base_session_query_sqlite + plugin_dsh_base_working_directory["working-directory
@deepseek-ai/dsh-working-directory"] + cfg --> plugin_dsh_base_working_directory + plugin_dsh_base_tool_working_directory["tool-working-directory
@deepseek-ai/dsh-tool-working-directory"] + cfg --> plugin_dsh_base_tool_working_directory plugin_dsh_base_session_projection["session-projection
@deepseek-ai/dsh-session-projection"] cfg --> plugin_dsh_base_session_projection plugin_dsh_base_storage["storage
@deepseek-ai/dsh-storage"] @@ -112,8 +116,6 @@ flowchart LR cfg --> plugin_dsh_base_skill plugin_dsh_base_skill_filesystem["skill-filesystem
@deepseek-ai/dsh-skill-filesystem"] cfg --> plugin_dsh_base_skill_filesystem - plugin_dsh_base_skill_badge["skill-badge
@deepseek-ai/dsh-skill-badge"] - cfg --> plugin_dsh_base_skill_badge plugin_dsh_base_tool_skill["tool-skill
@deepseek-ai/dsh-tool-skill"] cfg --> plugin_dsh_base_tool_skill plugin_dsh_base_commands["commands
@deepseek-ai/dsh-commands"] @@ -170,8 +172,6 @@ flowchart LR cfg --> plugin_dsh_base_tool_todo plugin_dsh_base_tool_goal["tool-goal
@deepseek-ai/dsh-tool-goal"] cfg --> plugin_dsh_base_tool_goal - plugin_dsh_base_tool_ralph["tool-ralph
@deepseek-ai/dsh-tool-ralph"] - cfg --> plugin_dsh_base_tool_ralph plugin_dsh_base_repeat_tool_reminder["repeat-tool-reminder
@deepseek-ai/dsh-repeat-tool-reminder"] cfg --> plugin_dsh_base_repeat_tool_reminder plugin_dsh_base_web["web
@deepseek-ai/dsh-web"] @@ -228,6 +228,8 @@ flowchart LR | `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` | | `attachment-local` | `@deepseek-ai/dsh-attachment-local` | | `session-query-sqlite` | `@deepseek-ai/dsh-session-query-sqlite` | +| `working-directory` | `@deepseek-ai/dsh-working-directory` | +| `tool-working-directory` | `@deepseek-ai/dsh-tool-working-directory` | | `session-projection` | `@deepseek-ai/dsh-session-projection` | | `storage` | `@deepseek-ai/dsh-storage` | | `storage-json` | `@deepseek-ai/dsh-storage-json` | @@ -252,7 +254,6 @@ flowchart LR | `agent-instructions` | `@deepseek-ai/dsh-agent-instructions` | | `skill` | `@deepseek-ai/dsh-skill` | | `skill-filesystem` | `@deepseek-ai/dsh-skill-filesystem` | -| `skill-badge` | `@deepseek-ai/dsh-skill-badge` | | `tool-skill` | `@deepseek-ai/dsh-tool-skill` | | `commands` | `@deepseek-ai/dsh-commands` | | `command-feedback` | `@deepseek-ai/dsh-command-feedback` | @@ -281,7 +282,6 @@ flowchart LR | `image-offload` | `@deepseek-ai/dsh-compaction-image-offload` | | `tool-todo` | `@deepseek-ai/dsh-tool-todo` | | `tool-goal` | `@deepseek-ai/dsh-tool-goal` | -| `tool-ralph` | `@deepseek-ai/dsh-tool-ralph` | | `repeat-tool-reminder` | `@deepseek-ai/dsh-repeat-tool-reminder` | | `web` | `@deepseek-ai/dsh-web` | | `web-search-deepseek` | `@deepseek-ai/dsh-web-search-deepseek` | diff --git a/apps/cli/package.json b/apps/cli/package.json index e98a7c41ce..3da15fcad8 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh", "description": "dsh CLI: profile launch, plugin management, and configuration inspection", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -45,8 +45,6 @@ "@deepseek-ai/dsh-goal-round-driver": "workspace:*", "@deepseek-ai/dsh-headless": "workspace:*", "@deepseek-ai/dsh-home-paths": "workspace:*", - "@deepseek-ai/dsh-hooks-claude-code": "workspace:*", - "@deepseek-ai/dsh-hooks-codex": "workspace:*", "@deepseek-ai/dsh-jobs-local": "workspace:*", "@deepseek-ai/dsh-launch-environment": "workspace:*", "@deepseek-ai/dsh-mcp-client": "workspace:*", @@ -79,7 +77,6 @@ "@deepseek-ai/dsh-tool-jobs": "workspace:*", "@deepseek-ai/dsh-tool-pwsh": "workspace:*", "@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:*", - "@deepseek-ai/dsh-tool-ralph": "workspace:*", "@deepseek-ai/dsh-tool-schedule": "workspace:*", "@deepseek-ai/dsh-tool-skill": "workspace:*", "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:*", @@ -89,12 +86,10 @@ "@deepseek-ai/dsh-tool-web": "workspace:*", "@deepseek-ai/dsh-tool-workflow": "workspace:*", "@deepseek-ai/dsh-web-app": "workspace:*", - "@deepseek-ai/dsh-webhook": "workspace:*", - "@deepseek-ai/dsh-webhook-github": "workspace:*", "@deepseek-ai/dsh-workflow-ptc": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", "commander": "^15.0.0", - "js-yaml": "^4.2.0", + "js-yaml": "^4.3.2", "node-addon-require-builtin": "^0.1.6", "@deepseek-ai/dsh-http-proxy": "workspace:*", "@deepseek-ai/dsh-mcp-resources": "workspace:*", @@ -102,9 +97,22 @@ "@deepseek-ai/dsh-atomic-write": "workspace:*", "@deepseek-ai/dsh-agent-preset": "workspace:*", "@deepseek-ai/dsh-experimental-voice-input-bundle": "workspace:*", - "@deepseek-ai/dsh-experimental-auto-review": "workspace:*" + "@deepseek-ai/dsh-experimental-auto-review": "workspace:*", + "@deepseek-ai/dsh-experimental-cot-translation-bundle": "workspace:*", + "@deepseek-ai/dsh-experimental-badge-skill-bundle": "workspace:*", + "@deepseek-ai/dsh-experimental-ralph-bundle": "workspace:*", + "@deepseek-ai/dsh-experimental-session-titles-bundle": "workspace:*", + "@deepseek-ai/dsh-experimental-terminal-bundle": "workspace:*", + "@deepseek-ai/dsh-experimental-session-search": "workspace:*", + "@deepseek-ai/dsh-experimental-tool-worktree": "workspace:*" }, "devDependencies": { + "@deepseek-ai/dsh-skill-badge": "workspace:*", + "@deepseek-ai/dsh-webhook-github": "workspace:*", + "@deepseek-ai/dsh-webhook": "workspace:*", + "@deepseek-ai/dsh-tool-ralph": "workspace:*", + "@deepseek-ai/dsh-hooks-codex": "workspace:*", + "@deepseek-ai/dsh-hooks-claude-code": "workspace:*", "@agentclientprotocol/sdk": "1.4.0", "@deepseek-ai/dsh-acp": "workspace:*", "@deepseek-ai/dsh-agent": "workspace:*", @@ -150,8 +158,8 @@ "@types/js-yaml": "^4.0.9", "@types/ws": "8.18.1", "ajv": "^8.20.0", - "execa": "^10.0.0", - "ws": "8.21.0" + "execa": "^10.0.1", + "ws": "8.21.3" }, "exports": { "./profile-boot": { diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index b5a62ccbed..da6e0863ff 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -3,8 +3,8 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/reference/README.md /dsh-cli-behavior-reference: - en: d25712a65d21ab45 - zh: b1c57b706bc64085 + en: 5a4f43756dae900e + zh: d58c479481cfa5fe /dsh-cli-behavior-reference/profile-boot: en: e3a159750f5f9ed3 zh: e02d9004fcc239f9 @@ -18,11 +18,11 @@ en: d6368bf97a02bd57 zh: 2ab7888a108961ce /dsh-cli-behavior-reference/plugin-management: - en: 9980ff1600fa9460 - zh: cc66ba45e27b5540 + en: 8d2cfff9bab7327f + zh: 21515ab6e6798504 /dsh-cli-behavior-reference/web-profile: - en: 42f0b9349cf1574b - zh: a3cc6e352e71badb + en: 536c6883a0d54b14 + zh: 6dd2469b71189c87 /dsh-cli-behavior-reference/shared-deployment-behavior: en: 6a7abc9339e8f37f zh: 133adb834c8e195d diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md index edc3dd4272..e8105bab78 100644 --- a/apps/cli/reference/README.md +++ b/apps/cli/reference/README.md @@ -4,6 +4,8 @@ English | [中文](README.zh.md) This reference defines the profile, plugin-management, and config-dump command modes. Argv is parsed once through [`src/args.ts`](../src/args.ts), and [`src/bin.ts`](../src/bin.ts) dynamically imports only the selected runner. +The [generated command help](../../../docs/cli-help.md) contains complete launcher, plugin-management, and shipped-profile help. Run `pnpm run gen-cli-help` to refresh it; `pnpm run verify-cli-help` checks it against the current CLI. + ## Profile boot `dsh ` abbreviates `dsh --profile ` and boots the profile at `$DSH_HOME/profiles/`. The shorthand name must immediately follow `dsh`; `plugin` remains the plugin-management command, so boot a profile with that name using `dsh --profile plugin`. The effective tree is composed over an empty root by applying, in order: each bundle patch named in the profile manifest's `dsh.profile.bundles` list, the profile's own `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml` (machine-local preferences shared by every profile, so it outranks the per-profile layer), and each `--patch ` overlay in argv order. Later layers win per row; a patch replaces the targeted row's complete `config` value rather than deep-merging keys, and may insert new rows. The final YAML composition controls whether `dsh-hmr` watches configuration; without HMR, changes require restart. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit. @@ -80,6 +82,8 @@ New directories and files request modes `0700` and `0600` on POSIX. The CLI prin `dsh plugin --profile ` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `` to `pnpm` with the profile directory as working directory — `add`, `remove`, `why`, `update`, and every other pnpm verb work unchanged; pnpm must be on PATH. Relative path specs (`.`, `../plugin`, and their `file:`/`link:` forms) are anchored to the invoking directory first, so `add .` from a plugin checkout installs that checkout, not the profile. After every successful run, `dsh.profile.bundles` is reconciled against the installed state: each dependency resolving to a package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` joins the layer stack (so an `update` that gains the declaration activates it), a bundle-less dependency stays plain with a one-time warning, and a removed dependency leaves the stack. +`dsh plugin` reads `--profile` only before the forwarded arguments, which start at the first token it does not recognize, so `--profile ` comes first. When the forwarded arguments begin with the pnpm command, everything after that command reaches pnpm verbatim, including `-h` and `--help`. When they begin with a pnpm option such as `--filter ` or `-r`, a later `-h` or `--help` still prints the forwarder's help instead of pnpm's, so put the pnpm command before them to reach pnpm's help. `-h` or `--help` before any forwarded argument prints the forwarder's own help and exits 0. + The Codex and Claude Code subagent providers are separate optional Bundles. Add either package, both in one command, or remove either package independently: ```sh @@ -122,7 +126,7 @@ The base-backed modes treat the invoking directory as the default workspace root New sessions in base-backed profiles default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads and network access are not confined, while process visibility depends on the selected sandbox backend — bwrap runs commands in a private PID namespace that hides host processes, and Landlock and Seatbelt leave host process visibility unchanged. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one. The standalone `sdk-minimal` tree instead pins `danger-full-access` and mounts no approval or permission-settings service. -`DSH_TOOLS_MODE` selects `native`, `ptc`, or `both` for the process; another value fails at boot. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and composes only the platform-selected persistent shell. Select 极简模式 when creating a Web session; every other prompt section and model-facing plugin remains absent from that agent while the shared browser, workspace, persistence, sandbox, and permission host stays in place. +`DSH_TOOLS_MODE` selects `native` or `ptc` for the process; another value fails at boot. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and composes only the platform-selected persistent shell. Select 极简模式 when creating a Web session; every other prompt section and model-facing plugin remains absent from that agent while the shared browser, workspace, persistence, sandbox, and permission host stays in place. ## Shared deployment behavior diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index e4c4be781c..2471de196e 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -4,6 +4,8 @@ 本参考定义 profile 启动、插件管理和配置 dump 等命令模式。argv 由 [`src/args.ts`](../src/args.ts) 统一解析一次,[`src/bin.ts`](../src/bin.ts) 只会动态导入选中的运行器。 +[生成的命令帮助](../../../docs/cli-help.zh.md)包含启动器、插件管理和随附 profile 的完整帮助。运行 `pnpm run gen-cli-help` 刷新,运行 `pnpm run verify-cli-help` 检查它是否与当前 CLI 一致。 + ## Profile 启动 @@ -82,6 +84,8 @@ dsh --profile web --patch ./extra.yml --dump-config `dsh plugin --profile ` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `` 转发给 `pnpm`:`add`、`remove`、`why`、`update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.`、`../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,系统都会根据当前安装状态更新 `dsh.profile.bundles`:如果某项依赖解析到的包在 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,该依赖就会加入配置层栈;如果某项依赖在 `update` 后获得该声明,也会随即激活。没有组合包声明的依赖仍作为普通依赖保留,并显示一次性警告;已移除的依赖则从配置层栈中删除。 +`dsh plugin` 只在被转发的参数之前读取 `--profile`;被转发的参数从它无法识别的第一个 token 开始,因此 `--profile ` 必须写在最前面。被转发的参数以 pnpm 命令开头时,该命令之后的所有内容都会原样交给 pnpm,包括 `-h` 和 `--help`。被转发的参数以 `--filter ` 或 `-r` 等 pnpm 选项开头时,后面的 `-h` 或 `--help` 仍会打印转发器的帮助而不是 pnpm 的帮助,因此要查看 pnpm 的帮助,需把 pnpm 命令写在它们之前。在任何被转发的参数之前出现的 `-h` 或 `--help` 会打印转发器自身的帮助并以 0 退出。 + Codex 与 Claude Code subagent 提供方是两个彼此独立的可选组合包。可以只添加一个包、在同一命令中添加两个包,或独立移除任一包: ```sh @@ -124,7 +128,7 @@ dsh web --help 基于 base 的 profile 中,新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取和网络访问不受限制,进程可见性则取决于所选沙箱后端——bwrap 在私有 PID 命名空间中运行命令并隐藏宿主进程,Landlock 与 Seatbelt 保持宿主进程可见性不变。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。独立的 `sdk-minimal` 配置树则固定为 `danger-full-access`,且不挂载 approval 或权限 settings 服务。 -`DSH_TOOLS_MODE` 为进程选择 `native`、`ptc` 或 `both`;其他值会导致启动失败。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并且仅组合按平台选择的持久 shell。创建 Web 会话时请选择极简模式;该 agent 不包含任何其他提示词段落或面向模型的插件,而共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。 +`DSH_TOOLS_MODE` 为进程选择 `native` 或 `ptc`;其他值会导致启动失败。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并且仅组合按平台选择的持久 shell。创建 Web 会话时请选择极简模式;该 agent 不包含任何其他提示词段落或面向模型的插件,而共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。 ## 共享部署行为 diff --git a/apps/cli/src/args.ts b/apps/cli/src/args.ts index 7adf1913a3..24d6789656 100644 --- a/apps/cli/src/args.ts +++ b/apps/cli/src/args.ts @@ -187,6 +187,10 @@ export function parseDshArgs(argv: readonly string[], version: string, manageDes if (first === 'plugin') { const plugin = program.command('plugin').description('manage a profile\'s plugins by forwarding the remaining arguments to pnpm in the profile directory') plugin + // Commander copies the launcher's disabled help option into subcommands. The + // forwarder restores its own help and stops reading options at forwarded pnpm input. + .helpOption('-h, --help', 'show this help') + .passThroughOptions() .requiredOption('--profile ', 'the profile whose plugins to manage (initialized on first use)', selectProfile) .allowUnknownOption() .argument('[args...]', 'pnpm arguments, forwarded verbatim (add , remove , why , ...)') diff --git a/apps/cli/tests/agent-team-headless.e2e.ts b/apps/cli/tests/agent-team-headless.e2e.ts index 481cfc7332..45e954e40f 100644 --- a/apps/cli/tests/agent-team-headless.e2e.ts +++ b/apps/cli/tests/agent-team-headless.e2e.ts @@ -88,11 +88,11 @@ describe('dsh run with Agent Teams enabled', () => { expect(files).toHaveLength(4) const logs = await Promise.all(files.map(file => readFile(join(sessions, file), 'utf8'))) const parsed = logs.map(records) - const workflowChild = parsed.find(log => log.some(record => record.type === 'subagent/descriptor' - && (record.data as { mode: string }).mode === 'one-shot')) + const workflowChild = parsed.find(log => log.some(record => record.type === 'user/message' + && JSON.stringify((record.data as { content: unknown }).content) === JSON.stringify([{ type: 'text', text: 'TEAM_WORKFLOW_CHILD' }]))) expect(workflowChild).toBeDefined() expect(workflowChild!.find(record => record.type === 'subagent/descriptor')?.data) - .toMatchObject({ mode: 'one-shot', provider: 'spawn' }) + .toMatchObject({ mode: 'continuable', provider: 'spawn' }) expect(workflowChild!.filter(record => record.type === 'user/message' && (record.data as { source: { kind: string } }).source.kind === 'user').map(record => record.data)) .toEqual([expect.objectContaining({ content: [{ type: 'text', text: 'TEAM_WORKFLOW_CHILD' }] })]) diff --git a/apps/cli/tests/args.spec.ts b/apps/cli/tests/args.spec.ts index 88138f2dec..e255789466 100644 --- a/apps/cli/tests/args.spec.ts +++ b/apps/cli/tests/args.spec.ts @@ -120,6 +120,25 @@ describe('parseDshArgs', () => { // Unknown pnpm flags forward verbatim. expect(parse(['plugin', '--profile', 'tui', 'add', '--save-dev', 'x'])) .toEqual({ mode: 'plugin', profile: 'tui', args: ['add', '--save-dev', 'x'] }) + // Options after the pnpm command belong to pnpm, including the forwarder's own names. + expect(parse(['plugin', '--profile', 'tui', 'add', '--help'])) + .toEqual({ mode: 'plugin', profile: 'tui', args: ['add', '--help'] }) + expect(parse(['plugin', '--profile', 'tui', 'add', '-h', '--profile', 'x'])) + .toEqual({ mode: 'plugin', profile: 'tui', args: ['add', '-h', '--profile', 'x'] }) + // A leading pnpm option starts the forwarded arguments before the pnpm command. + expect(parse(['plugin', '--profile', 'tui', '--filter', 'x', '--profile', 'y', 'add', 'z'])) + .toEqual({ mode: 'plugin', profile: 'tui', args: ['--filter', 'x', '--profile', 'y', 'add', 'z'] }) + expect(exitCode(['plugin', 'add', 'x', '--profile', 'tui'])).toBe(1) + expect(exitCode(['plugin', '--filter', 'x', 'add', '--profile', 'tui'])).toBe(1) + }) + + it.each([ + ['plugin', '--help'], ['plugin', '-h'], ['plugin', '--profile', 'tui', '--help'], + ['plugin', '--profile', 'tui', '--filter', 'x', 'add', '--help'], + ])('prints the plugin forwarder help for %j', (...argv: string[]) => { + const stdout = vi.spyOn(process.stdout, 'write').mockReturnValue(true) + expect(exitCode(argv)).toBe(0) + expect(stdout.mock.calls.map(([chunk]) => String(chunk)).join('')).toContain('Usage: dsh plugin [options] [args...]') }) it.each(['desktop', 'Desktop', 'DESKTOP'])('permits installed Desktop plugin commands for %s', (profile) => { diff --git a/apps/cli/tests/built-bin.e2e.ts b/apps/cli/tests/built-bin.e2e.ts index cb84236414..09a0f39b1f 100644 --- a/apps/cli/tests/built-bin.e2e.ts +++ b/apps/cli/tests/built-bin.e2e.ts @@ -1249,6 +1249,9 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', ['plugin-package-inventory-deepseek', '@deepseek-ai/dsh-plugin-package-inventory-deepseek'], ['llm-deepseek', '@deepseek-ai/dsh-llm-deepseek-api-key'], ['sandbox', '@deepseek-ai/dsh-sandbox-local'], + ['filesystem', '@deepseek-ai/dsh-fs-local'], + ['working-directory', '@deepseek-ai/dsh-working-directory'], + ['tool-working-directory', '@deepseek-ai/dsh-tool-working-directory'], ['session-projection', '@deepseek-ai/dsh-session-projection'], ['sandbox-policy', '@deepseek-ai/dsh-sandbox-policy'], ['subprocess', '@deepseek-ai/dsh-subprocess-local'], diff --git a/apps/cli/tests/dsh-badge.expected.e2e.ts b/apps/cli/tests/dsh-badge.expected.e2e.ts index ca70622be2..a1f456c5b5 100644 --- a/apps/cli/tests/dsh-badge.expected.e2e.ts +++ b/apps/cli/tests/dsh-badge.expected.e2e.ts @@ -5,7 +5,7 @@ const binScript = fileURLToPath(new URL('./fixtures/dsh-badge/snapshot.ts', impo const configPath = fileURLToPath(new URL('./fixtures/dsh-badge/cordis.yml', import.meta.url)) const defaultConfigPath = fileURLToPath(new URL('./fixtures/dsh-badge/default.cordis.yml', import.meta.url)) const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) -const badgeAssetsPath = fileURLToPath(new URL('../../../packages/skill/skill-badge/assets/', import.meta.url)) +const badgeAssetsPath = fileURLToPath(new URL('../../../packages/experimental/skill-badge/assets/', import.meta.url)) describe('dsh badge assembled snapshot', () => { it('advertises and loads the opt-in bundled skill through the shipped app', async () => { diff --git a/apps/cli/tests/expected/dump-config-schema.json b/apps/cli/tests/expected/dump-config-schema.json index ce57b82073..29f03673a0 100644 --- a/apps/cli/tests/expected/dump-config-schema.json +++ b/apps/cli/tests/expected/dump-config-schema.json @@ -445,6 +445,13 @@ ] } }, + { + "not": { + "required": [ + "preset" + ] + } + }, { "anyOf": [ { @@ -494,6 +501,13 @@ ] } }, + { + "not": { + "required": [ + "preset" + ] + } + }, { "anyOf": [ { @@ -543,6 +557,13 @@ ] } }, + { + "not": { + "required": [ + "preset" + ] + } + }, { "anyOf": [ { @@ -578,6 +599,11 @@ "properties": { "insert": { "$ref": "#/$defs/entryList" + }, + "preset": { + "type": "string", + "pattern": "\\S", + "description": "Outer preset row id. The remaining patch fields apply to its current config.plugins list; a missing or unaddressable preset fails composition." } }, "description": "An insert appends entries, optionally inside the group identified by id. Other patches replace supplied fields; config is replaced wholesale, not deep-merged. A truthy name asserts the existing plugin name rather than renaming it. Unknown targets and non-insert patches without a nonempty id are warned and skipped." diff --git a/apps/cli/tests/fixtures/dsh-badge/cordis.yml b/apps/cli/tests/fixtures/dsh-badge/cordis.yml index dd419b684f..e67020c176 100644 --- a/apps/cli/tests/fixtures/dsh-badge/cordis.yml +++ b/apps/cli/tests/fixtures/dsh-badge/cordis.yml @@ -1,5 +1,6 @@ -- id: skill-badge - disabled: false +- insert: + - id: skill-badge + name: '@deepseek-ai/dsh-skill-badge' - id: skill-filesystem config: diff --git a/apps/cli/tests/fixtures/dsh-badge/snapshot.ts b/apps/cli/tests/fixtures/dsh-badge/snapshot.ts index 4c2331ed0b..c84912a9f1 100644 --- a/apps/cli/tests/fixtures/dsh-badge/snapshot.ts +++ b/apps/cli/tests/fixtures/dsh-badge/snapshot.ts @@ -11,9 +11,16 @@ const overlayPath = process.argv[2] if (overlayPath === undefined) throw new Error('dsh-badge snapshot requires an overlay path') const rootConfigPath = fileURLToPath(new URL('../../../../../packages/bundle/base/tests/fixtures/root.cordis.yml', import.meta.url)) const basePatchPath = fileURLToPath(new URL('../../../../../packages/bundle/base/cordis.patch.yml', import.meta.url)) +// The fixture owns the optional provider; the base bundle has no dependency on it. +const overlays = loadOverlayPatches('dsh-badge-snapshot', overlayPath).map(patch => patch.insert === undefined ? patch : { + ...patch, + insert: patch.insert.map(row => row.name === '@deepseek-ai/dsh-skill-badge' + ? { ...row, name: import.meta.resolve('@deepseek-ai/dsh-skill-badge') } + : row), +}) const ctx = await boot('dsh-badge-snapshot', rootConfigPath, [ ...loadOverlayPatches('dsh-badge-snapshot', basePatchPath), - ...loadOverlayPatches('dsh-badge-snapshot', overlayPath), + ...overlays, ]) try { diff --git a/apps/cli/tests/github-webhook-real.e2e.ts b/apps/cli/tests/github-webhook-real.e2e.ts index 78ad62c968..4c23211d81 100644 --- a/apps/cli/tests/github-webhook-real.e2e.ts +++ b/apps/cli/tests/github-webhook-real.e2e.ts @@ -4,15 +4,16 @@ import type { ChildProcess } from 'node:child_process' import { spawn } from 'node:child_process' import { createHmac, randomUUID } from 'node:crypto' import { existsSync } from 'node:fs' -import { mkdir, mkdtemp, realpath, rm } from 'node:fs/promises' +import { mkdir, mkdtemp, readFile, realpath, rm, symlink, writeFile } from 'node:fs/promises' import { createServer } from 'node:net' import type { AddressInfo } from 'node:net' import { tmpdir } from 'node:os' -import { join } from 'node:path' +import { dirname, join } from 'node:path' import { setTimeout as delay } from 'node:timers/promises' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import WebSocket from 'ws' +import { initProfile, PROFILE_TEMPLATES } from '@deepseek-ai/dsh-app-boot' const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url)) const BUILT_BIN = join(REPO_ROOT, 'apps/cli/lib/bin.js') @@ -357,6 +358,18 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('GitHub webhook through the real const workspacePath = join(root, 'workspace') await mkdir(workspacePath) const canonicalWorkspacePath = await realpath(workspacePath) + const profile = join(root, '.dsh/profiles/web') + initProfile(profile, PROFILE_TEMPLATES.web!.bundles) + const manifestPath = join(profile, 'package.json') + const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as { dependencies: Record } + for (const name of ['@deepseek-ai/dsh-webhook', '@deepseek-ai/dsh-webhook-github']) { + const directory = dirname(fileURLToPath(import.meta.resolve(`${name}/package.json`))) + const link = join(profile, 'node_modules', name) + await mkdir(dirname(link), { recursive: true }) + await symlink(directory, link, 'junction') + manifest.dependencies[name] = `file:${directory}` + } + await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`) const webhookPort = await freePort() const child = spawn(process.execPath, [ BUILT_BIN, diff --git a/apps/cli/tests/optional-bundle-transitions.e2e.ts b/apps/cli/tests/optional-bundle-transitions.e2e.ts new file mode 100644 index 0000000000..78d0e45813 --- /dev/null +++ b/apps/cli/tests/optional-bundle-transitions.e2e.ts @@ -0,0 +1,349 @@ +/** Optional bundles through the shipped Web Loader, live package manager, and real Agent loop. */ +import { randomUUID } from 'node:crypto' +import { existsSync } from 'node:fs' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import type { Context } from '@deepseek-ai/cordis' +import type { Agent, AgentHandle } from '@deepseek-ai/dsh-agent' +import type { ProfileContext } from '@deepseek-ai/dsh-app-boot' +import { LlmAdapter, ToolCallId, createUserMessage } from '@deepseek-ai/dsh-llm' +import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm' +import { SessionId } from '@deepseek-ai/dsh-session' +import type {} from '@deepseek-ai/dsh-agent-preset-registry' +import type {} from '@deepseek-ai/dsh-hmr' +import type {} from '@deepseek-ai/dsh-plugin-manager' +import type {} from '@deepseek-ai/dsh-permission-presets' +import type {} from '@deepseek-ai/dsh-session-title-llm' +import type {} from '@deepseek-ai/dsh-skill' +import type {} from '@deepseek-ai/dsh-tools' +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest' +import { textResponse, toolCallResponse } from '../../../packages/core/agent-loop/tests/mock-adapter.ts' + +const root = fileURLToPath(new URL('../../..', import.meta.url)) +const installAnchor = join(root, 'apps/cli/package.json') +const bundles = { + search: '@deepseek-ai/dsh-experimental-session-search', + ralph: '@deepseek-ai/dsh-experimental-ralph-bundle', + terminal: '@deepseek-ai/dsh-experimental-terminal-bundle', + badge: '@deepseek-ai/dsh-experimental-badge-skill-bundle', + titles: '@deepseek-ai/dsh-experimental-session-titles-bundle', +} as const +type Response = StreamChunk[] | ((request: GenerateOptions) => StreamChunk[]) + +/** Only the external model is scripted; tools and child Agents use the shipped implementations. */ +class BundleModel extends LlmAdapter { + readonly requests: GenerateOptions[] = [] + readonly scripts = new Map() + private titleNumber = 0 + + override resolveModel(provider: string, model: string): Promise { + return Promise.resolve({ provider, id: model, name: model }) + } + + async * stream(request: GenerateOptions): AsyncIterable { + this.requests.push(request) + if (request.purpose === 'session-title') { + yield* textResponse(`Bundle title ${String(++this.titleNumber)}`) + return + } + if (request.tools?.some(tool => tool.name === 'structured_output')) { + yield* toolCallResponse(`report-${randomUUID()}`, 'structured_output', { + status: 'complete', summary: 'OPTIONAL_RALPH_DONE', evidence: ['The scripted worker completed.'], nextSteps: [], blocker: '', + }) + return + } + const response = this.scripts.get(request.sessionId ?? '')?.shift() + if (response === undefined) throw new Error(`No scripted model response for ${String(request.sessionId)}`) + yield* typeof response === 'function' ? response(request) : response + } +} + +describe.skipIf(!existsSync(join(root, 'apps/cli/lib/bin.js')))('Official optional bundle transitions', { retry: 0 }, () => { + let ctx: Context + let temporary: string + let workspace: string + const model = new BundleModel() + const handles = new Set() + const selected = new Set() + let originalEnvironment: Record = {} + + beforeAll(async () => { + temporary = await mkdtemp(join(tmpdir(), 'dsh-optional-bundles-')) + workspace = join(temporary, 'workspace') + await mkdir(workspace) + const home = join(temporary, 'home') + const values = { + DSH_HOME: home, DSH_AGENTS_HOME: join(temporary, 'agents'), DSH_BUNDLED_SKILL_DIR: join(temporary, 'skills'), + DSH_TELEMETRY_DISABLED: '1', TMUX: '', TMUX_PANE: '', + } + originalEnvironment = Object.fromEntries(Object.keys(values).map(key => [key, process.env[key]])) + Object.assign(process.env, values) + const require = createRequire(import.meta.url) + const app = require(join(root, 'packages/boot/app-boot/lib/index.js')) as typeof import('@deepseek-ai/dsh-app-boot') + const cmdline = require(join(root, 'packages/boot/cmdline/lib/index.js')) as typeof import('@deepseek-ai/dsh-cmdline') + const directory = join(home, 'profiles', 'web') + app.initProfile(directory, ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app']) + await writeFile(join(directory, 'cordis.yml'), '[]\n') + await writeFile(join(directory, 'cordis.patch.yml'), JSON.stringify([ + { id: 'agent-default-model', config: { provider: 'bundle-test', model: 'scripted' } }, + ])) + const profile = app.loadProfileDirectory('bundle-test', directory, installAnchor) + const profileContext: ProfileContext = { + name: 'web', dir: directory, patchPath: profile.patchPath, installAnchor, cwd: workspace, home, + startedBundles: profile.layers.map(layer => layer.packageName), overlays: [], telemetryDisabledEnv: '1', + } + const resolution = await app.createRuntimeResolution({ installAnchor, home, profile }) + const listeners = new Set<() => void>() + let ready = false + ctx = await app.boot('bundle-test', join(directory, 'cordis.yml'), app.readProfilePatches('bundle-test', profileContext), async (owner) => { + owner.provide('profileContext', profileContext) + owner.provide('appReady', { onReady: (listener) => { + if (ready) { listener(); return () => {} } + listeners.add(listener) + return () => { listeners.delete(listener) } + } }) + await owner.plugin(app.PluginPackages, { resolution }) + cmdline.provideCmdline(owner, { args: ['--no-open', '--port', '0'], exit: (code) => { throw new Error(`Web command exited ${String(code)}`) } }) + }) + ready = true + for (const listener of listeners) listener() + listeners.clear() + ctx.llm.registerAdapter(['bundle-test'], model) + expect(ctx.get('hmr')).toBeDefined() + }, 120_000) + + afterEach(async () => { + for (const handle of handles) await handle.dispose() + handles.clear() + for (const name of [...selected]) { + expect(await ctx.pluginManager.setBundleEnabled(name, false)).toMatchObject({ application: 'applied' }) + selected.delete(name) + } + }, 120_000) + + afterAll(async () => { + try { + for (const handle of handles) await handle.dispose() + await ctx?.fiber.dispose() + } finally { + for (const [key, value] of Object.entries(originalEnvironment)) { + if (value === undefined) Reflect.deleteProperty(process.env, key) + else process.env[key] = value + } + if (temporary !== undefined) await rm(temporary, { recursive: true, force: true }) + } + }, 120_000) + + async function agent(preset = 'standard', cwd = workspace): Promise { + const handle = await ctx.agents.create({ + sessionId: SessionId(`optional-${preset}-${randomUUID()}`), meta: { cwd, agentPreset: preset }, + agentOptions: { provider: 'bundle-test', model: 'scripted' }, + setup: scoped => ctx.agentPresets.mount(scoped, preset).then(() => undefined), + }) + handles.add(handle) + ctx.permissionPresets.set(handle.agent.session, 'danger-full-access') + return handle + } + + async function turn(owner: Agent, prompt: string, responses: Response[]): Promise { + model.scripts.set(owner.session.id, [...responses]) + owner.followup(createUserMessage({ content: [{ type: 'text', text: prompt }], source: { kind: 'user' } })) + await owner.whenIdle() + expect(model.scripts.get(owner.session.id)).toEqual([]) + } + + async function select(name: string, enabled: boolean): Promise { + if (enabled) selected.add(name) + const result = await ctx.pluginManager.setBundleEnabled(name, enabled) + if (!enabled && result.application === 'applied') selected.delete(name) + expect(result, JSON.stringify(result)).toMatchObject({ changed: true, application: 'applied' }) + } + + const names = (owner?: Agent) => ctx.tools.schemas(owner).map(tool => tool.name).sort() + const toolResult = (owner: Agent, callId: string) => owner.session.deriveMessages() + .flatMap(message => message.role === 'tool' && message.toolCallId === callId ? message.content : []) + .flatMap(block => block.type === 'text' ? [block.text] : []).join('\n') + + it('keeps the default host and minimal tool catalogs while every optional bundle is off', async () => { + const minimal = await agent('minimal') + const standard = await agent() + expect(names()).toEqual(['working_directory']) + expect(names(minimal.agent)).toEqual([process.platform === 'win32' ? 'pwsh' : 'bash', 'working_directory']) + for (const name of ['str_replace_editor', 'session_search', 'terminal_open', 'ralph']) expect(names(standard.agent)).not.toContain(name) + expect((await ctx.skills.list({ cwd: workspace, scope: standard.agent })).some(skill => skill.name === 'dsh-badge')).toBe(false) + }) + + it('generates and executes the PTC SDK for the selected search bundle', async () => { + await select(bundles.search, true) + const coded = await agent('ptc') + const native = await agent('cordis') + const minimal = await agent('minimal') + const assembly = await ctx.systemPrompt.assemble({ scope: coded.agent }) + expect(assembly.tools.map(tool => tool.name)).toEqual(['run_code']) + expect(assembly.sections.find(section => section.name === 'tools:sdk')?.text).toContain('session_trace') + expect(names(native.agent)).toContain('session_search') + expect(names(minimal.agent)).toEqual([process.platform === 'win32' ? 'pwsh' : 'bash', 'working_directory']) + await turn(coded.agent, 'Read this session lineage through run_code.', [ + toolCallResponse('ptc-search', 'run_code', { + code: 'console.log(await tools.session_trace({}));', + description: 'Read the current session lineage', + }), textResponse('PTC_SEARCH_DONE'), + ]) + expect(toolResult(coded.agent, 'ptc-search')).toContain(coded.agent.session.id) + expect(coded.agent.session.snapshotEvents().some(event => event.type === 'tool/ptc-dispatch' && event.data.name === 'session_trace')).toBe(true) + await select(bundles.search, false) + }) + + it('searches persisted conversation content only through the selected search bundle', async () => { + const seed = await agent() + await turn(seed.agent, 'Remember OPTIONAL_SEARCH_MARKER for a later session.', [textResponse('Recorded the marker.')]) + const seedId = seed.agent.session.id + await seed.dispose() + handles.delete(seed) + const live = await agent() + await turn(live.agent, 'Keep OPTIONAL_SEARCH_MARKER in this live session.', [textResponse('Live marker recorded.')]) + const otherWorkspace = join(temporary, 'other-workspace') + await mkdir(otherWorkspace) + const foreign = await agent('standard', otherWorkspace) + await turn(foreign.agent, 'Keep OPTIONAL_SEARCH_MARKER in the other workspace.', [textResponse('Other marker recorded.')]) + await select(bundles.search, true) + expect(names(live.agent)).not.toContain('session_search') + const searcher = await agent() + await turn(searcher.agent, 'Find the previous marker in session history.', [ + toolCallResponse('search-history', 'session_search', { query: 'OPTIONAL_SEARCH_MARKER' }), textResponse('SEARCH_DONE'), + ]) + expect(toolResult(searcher.agent, 'search-history')).toContain(seedId) + expect(toolResult(searcher.agent, 'search-history')).toContain(live.agent.session.id) + expect(toolResult(searcher.agent, 'search-history')).not.toContain(foreign.agent.session.id) + expect(toolResult(searcher.agent, 'search-history')).toContain('OPTIONAL_SEARCH_MARKER') + await select(bundles.search, false) + expect(names((await agent()).agent)).not.toContain('session_search') + await turn(searcher.agent, 'Search the marker again using the retained capability.', [ + toolCallResponse('search-retained', 'session_search', { query: 'OPTIONAL_SEARCH_MARKER' }), textResponse('RETAINED_SEARCH_DONE'), + ]) + expect(toolResult(searcher.agent, 'search-retained')).toContain(seedId) + const sessionId = searcher.agent.session.id + await searcher.dispose() + handles.delete(searcher) + const reopened = await ctx.agents.resume({ resumeSessionId: sessionId, setup: scoped => ctx.agentPresets.mount(scoped, 'standard').then(() => undefined) }) + handles.add(reopened) + expect(names(reopened.agent)).not.toContain('session_search') + }) + + it('loads the actual badge asset only while its bundle is selected', async () => { + const owner = await agent() + const load = (callId: string) => ctx.tools.execute({ + callId: ToolCallId(callId), name: 'skill', arguments: { name: 'dsh-badge' }, + agent: owner.agent, signal: new AbortController().signal, + }) + expect((await load('badge-before')).isError).toBe(true) + await select(bundles.badge, true) + await turn(owner.agent, 'Load the dsh badge skill.', [ + toolCallResponse('badge-on', 'skill', { name: 'dsh-badge' }), textResponse('BADGE_DONE'), + ]) + expect(toolResult(owner.agent, 'badge-on')).toContain('powered by dsh') + expect(toolResult(owner.agent, 'badge-on')).toContain('dsh-badge.png') + const minimal = await agent('minimal') + expect((await ctx.skills.list({ cwd: workspace, scope: minimal.agent })).map(skill => skill.name)).toContain('dsh-badge') + expect(names(minimal.agent)).toEqual([process.platform === 'win32' ? 'pwsh' : 'bash', 'working_directory']) + await select(bundles.badge, false) + expect((await load('badge-after')).isError).toBe(true) + }) + + it('runs and closes a real persistent terminal delivered by its bundle', async () => { + await select(bundles.terminal, true) + const owner = await agent() + const target = join(workspace, 'terminal-output.txt') + let terminalId: string | undefined + const id = () => { + if (terminalId === undefined) throw new Error('terminal_open returned no terminal id') + return terminalId + } + await turn(owner.agent, 'Use a persistent terminal to retain a variable, write its value, and close the terminal.', [ + toolCallResponse('terminal-open', 'terminal_open', { type: 'shell', name: 'optional', cwd: workspace }), + (request) => { + terminalId = /started terminal session (pty-\d+)/.exec(JSON.stringify(request.messages))?.[1] + return toolCallResponse('terminal-set', 'terminal_send', { + sessionId: id(), text: process.platform === 'win32' ? "$env:DSH_OPTIONAL = 'TERMINAL_BUNDLE_OK'" : 'export DSH_OPTIONAL=TERMINAL_BUNDLE_OK', + }) + }, + () => toolCallResponse('terminal-write', 'terminal_send', { + sessionId: id(), text: process.platform === 'win32' + ? `Set-Content -NoNewline -Path '${target.replaceAll("'", "''")}' -Value $env:DSH_OPTIONAL` + : `printf '%s' "$DSH_OPTIONAL" > ${JSON.stringify(target)}`, + }), + () => toolCallResponse('terminal-close', 'terminal_close', { sessionId: id() }), + textResponse('TERMINAL_DONE'), + ]) + expect(await readFile(target, 'utf8')).toBe('TERMINAL_BUNDLE_OK') + expect(toolResult(owner.agent, 'terminal-close')).toContain('closed terminal session') + await select(bundles.terminal, false) + expect(names((await agent()).agent)).not.toContain('terminal_open') + }) + + it('runs a real fresh-Agent Ralph workflow through the selected bundle', async () => { + await select(bundles.ralph, true) + const owner = await agent() + await turn(owner.agent, 'The human explicitly requests one Ralph loop.', [ + toolCallResponse('ralph-run', 'ralph', { objective: 'Report completion with the structured output tool.', maxRounds: 1 }), + textResponse('RALPH_DONE'), + ]) + expect(toolResult(owner.agent, 'ralph-run')).toContain('Ralph worker reported completion after 1 round') + expect(toolResult(owner.agent, 'ralph-run')).toContain('OPTIONAL_RALPH_DONE') + expect(model.requests.some(request => request.sessionId !== owner.agent.session.id && request.tools?.some(tool => tool.name === 'structured_output'))).toBe(true) + await select(bundles.ralph, false) + expect(names((await agent()).agent)).not.toContain('ralph') + }) + + it('switches title cadence off/on/off through the live manager', async () => { + const titleRequests = (owner: Agent) => owner.session.snapshotEvents().filter(event => event.type === 'session/title-llm-request') + const first = await agent() + await turn(first.agent, 'First title prompt.', [textResponse('ONE')]) + await expect.poll(() => titleRequests(first.agent).length).toBe(1) + await turn(first.agent, 'Second title prompt.', [textResponse('TWO')]) + expect(titleRequests(first.agent)).toHaveLength(1) + await select(bundles.titles, true) + const following = await agent() + await turn(following.agent, 'Start a growing conversation.', [textResponse('ONE')]) + await expect.poll(() => titleRequests(following.agent).length).toBe(1) + expect(titleRequests(following.agent)[0]?.data.titleProvider).toBe('session-title-all-prompts-llm') + await expect.poll(() => ctx.sessionTitle.get(following.agent.session)?.source.kind).toBe('provider') + await turn(following.agent, 'Continue the growing conversation.', [textResponse('TWO')]) + await expect.poll(() => titleRequests(following.agent).length).toBe(2) + expect(titleRequests(following.agent).every(event => event.data.titleProvider === 'session-title-all-prompts-llm')).toBe(true) + await select(bundles.titles, false) + const restored = await agent() + await turn(restored.agent, 'Restore first-prompt titles.', [textResponse('ONE')]) + await expect.poll(() => titleRequests(restored.agent).length).toBe(1) + await turn(restored.agent, 'The title stays on the first prompt.', [textResponse('TWO')]) + expect(titleRequests(restored.agent)).toHaveLength(1) + expect(titleRequests(restored.agent)[0]?.data.titleProvider).toBe('session-title-first-prompt-llm') + }) + + it('composes all five bundles while leaving minimal and host tools unchanged', async () => { + for (const name of Object.values(bundles)) await select(name, true) + let coded: Agent | undefined + for (const preset of ['standard', 'cordis', 'ptc']) { + const owner = await agent(preset) + expect(names(owner.agent)).toEqual(expect.arrayContaining(['session_search', 'terminal_open', 'ralph'])) + if (preset === 'ptc') coded = owner.agent + } + if (coded === undefined) throw new Error('the PTC Agent was not created') + await turn(coded, 'Read this session lineage and load the badge through one program.', [ + toolCallResponse('combined-program', 'run_code', { + code: 'console.log(await tools.session_trace({})); console.log(await tools.skill({ name: "dsh-badge" }));', + description: 'Use optional session search and badge together', + }), textResponse('COMBINED_DONE'), + ]) + expect(toolResult(coded, 'combined-program')).toContain(coded.session.id) + expect(toolResult(coded, 'combined-program')).toContain('dsh-badge') + const minimal = await agent('minimal') + expect(names(minimal.agent)).toEqual([process.platform === 'win32' ? 'pwsh' : 'bash', 'working_directory']) + expect(names()).toEqual(['working_directory']) + for (const name of [...Object.values(bundles)].reverse()) await select(name, false) + expect(names((await agent()).agent)).not.toContain('session_search') + }) +}) diff --git a/apps/cli/tests/profiles/acp/tests/fixtures/image-offload.cordis.yml b/apps/cli/tests/profiles/acp/tests/fixtures/image-offload.cordis.yml index 21a9ead728..7059fa886e 100644 --- a/apps/cli/tests/profiles/acp/tests/fixtures/image-offload.cordis.yml +++ b/apps/cli/tests/profiles/acp/tests/fixtures/image-offload.cordis.yml @@ -36,7 +36,7 @@ name: '@deepseek-ai/dsh-system-prompt' config: personaPrefix: | - You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. + You are a coding assistant powered by the {{model}} model. Keep answers brief and factual. diff --git a/apps/cli/tests/profiles/acp/tests/goal-expected/goal-round-driver/session.expected.jsonl b/apps/cli/tests/profiles/acp/tests/goal-expected/goal-round-driver/session.expected.jsonl index 6cd9ea429c..19495490be 100644 --- a/apps/cli/tests/profiles/acp/tests/goal-expected/goal-round-driver/session.expected.jsonl +++ b/apps/cli/tests/profiles/acp/tests/goal-expected/goal-round-driver/session.expected.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":3,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0} +{"type":"session","version":4,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0} {"type":"permission/preset","data":{"preset":"danger-full-access"}} {"type":"sandbox/mode","data":{"mode":"danger-full-access"}} {"type":"approval/policy","data":{"policy":"never"}} @@ -6,21 +6,21 @@ {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} -{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} +{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Create a durable two-round goal for the ACP snapshot, inspect it, then report readiness."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Create a durable two-round goal","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_goal_create","name":"create_goal","arguments":"{\"objective\":\"Finish the ACP goal-round-driver snapshot proof\",\"max_goal_rounds\":2}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":20,"outputTokens":8},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_goal_create","name":"create_goal","args":["{\"objective\":\"Finish the ACP goal-round-driver snapshot proof\",\"max_goal_rounds\":2}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_goal_create","name":"create_goal","arguments":"{\"objective\":\"Finish the ACP goal-round-driver snapshot proof\",\"max_goal_rounds\":2}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":20,"outputTokens":8}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_goal_create","name":"create_goal","arguments":"{\"objective\":\"Finish the ACP goal-round-driver snapshot proof\",\"max_goal_rounds\":2}"}} {"type":"goal/change","data":{"kind":"goal/change","version":1,"operation":"create","goal":{"id":"goal-{{sessionId}}","revision":1,"objective":"Finish the ACP goal-round-driver snapshot proof","phase":"active","maxGoalRounds":2},"roundsStarted":0,"createdAt":0,"updatedAt":0}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_goal_create"},"content":[{"type":"tool-result","toolCallId":"call_goal_create","content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":1,\"objective\":\"Finish the ACP goal-round-driver snapshot proof\",\"phase\":\"active\",\"roundsStarted\":0,\"maxGoalRounds\":2},\"activation\":\"armed\"}"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_goal_create"},"toolCallId":"call_goal_create","content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":1,\"objective\":\"Finish the ACP goal-round-driver snapshot proof\",\"phase\":\"active\",\"roundsStarted\":0,\"maxGoalRounds\":2},\"activation\":\"armed\"}"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_goal_get","name":"get_goal","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":30,"outputTokens":4},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_goal_get","name":"get_goal","args":["{}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_goal_get","name":"get_goal","arguments":"{}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":30,"outputTokens":4}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_goal_get","name":"get_goal","arguments":"{}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_goal_get"},"content":[{"type":"tool-result","toolCallId":"call_goal_get","content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":1,\"objective\":\"Finish the ACP goal-round-driver snapshot proof\",\"phase\":\"active\",\"roundsStarted\":0,\"maxGoalRounds\":2},\"activation\":\"armed\"}"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[20],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_goal_get"},"toolCallId":"call_goal_get","content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":1,\"objective\":\"Finish the ACP goal-round-driver snapshot proof\",\"phase\":\"active\",\"roundsStarted\":0,\"maxGoalRounds\":2},\"activation\":\"armed\"}"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[20],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"GOAL READY"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":35,"outputTokens":2},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["GOAL READY"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"GOAL READY"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":35,"outputTokens":2}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} diff --git a/apps/cli/tests/profiles/acp/tests/goal-expected/goal-wrapup/session.expected.jsonl b/apps/cli/tests/profiles/acp/tests/goal-expected/goal-wrapup/session.expected.jsonl index 5aa8425050..7e3a02fce9 100644 --- a/apps/cli/tests/profiles/acp/tests/goal-expected/goal-wrapup/session.expected.jsonl +++ b/apps/cli/tests/profiles/acp/tests/goal-expected/goal-wrapup/session.expected.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":3,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0} +{"type":"session","version":4,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0} {"type":"permission/preset","data":{"preset":"danger-full-access"}} {"type":"sandbox/mode","data":{"mode":"danger-full-access"}} {"type":"approval/policy","data":{"policy":"never"}} @@ -6,16 +6,16 @@ {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} -{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} +{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Create a durable goal for the wrap-up snapshot, then report readiness."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Create a durable goal for","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_goal_create","name":"create_goal","arguments":"{\"objective\":\"Finish the ACP goal wrap-up snapshot proof\",\"max_goal_rounds\":2}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":20,"outputTokens":8},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_goal_create","name":"create_goal","args":["{\"objective\":\"Finish the ACP goal wrap-up snapshot proof\",\"max_goal_rounds\":2}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_goal_create","name":"create_goal","arguments":"{\"objective\":\"Finish the ACP goal wrap-up snapshot proof\",\"max_goal_rounds\":2}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":20,"outputTokens":8}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_goal_create","name":"create_goal","arguments":"{\"objective\":\"Finish the ACP goal wrap-up snapshot proof\",\"max_goal_rounds\":2}"}} {"type":"goal/change","data":{"kind":"goal/change","version":1,"operation":"create","goal":{"id":"goal-{{sessionId}}","revision":1,"objective":"Finish the ACP goal wrap-up snapshot proof","phase":"active","maxGoalRounds":2},"roundsStarted":0,"createdAt":0,"updatedAt":0}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_goal_create"},"content":[{"type":"tool-result","toolCallId":"call_goal_create","content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":1,\"objective\":\"Finish the ACP goal wrap-up snapshot proof\",\"phase\":\"active\",\"roundsStarted\":0,\"maxGoalRounds\":2},\"activation\":\"armed\"}"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_goal_create"},"toolCallId":"call_goal_create","content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":1,\"objective\":\"Finish the ACP goal wrap-up snapshot proof\",\"phase\":\"active\",\"roundsStarted\":0,\"maxGoalRounds\":2},\"activation\":\"armed\"}"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"GOAL READY"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":28,"outputTokens":2},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["GOAL READY"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"GOAL READY"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":28,"outputTokens":2}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} @@ -30,12 +30,12 @@ {"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_goal_complete","name":"update_goal","arguments":"{\"goal_id\":\"goal-{{sessionId}}\",\"revision\":1,\"action\":\"complete\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":40,"outputTokens":9},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_goal_complete","name":"update_goal","args":["{\"goal_id\":\"goal-{{sessionId}}\",\"revision\":1,\"action\":\"complete\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_goal_complete","name":"update_goal","arguments":"{\"goal_id\":\"goal-{{sessionId}}\",\"revision\":1,\"action\":\"complete\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":40,"outputTokens":9}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"call_goal_complete","name":"update_goal","arguments":"{\"goal_id\":\"goal-{{sessionId}}\",\"revision\":1,\"action\":\"complete\"}"}} {"type":"goal/change","data":{"kind":"goal/change","version":1,"operation":"complete","goal":{"id":"goal-{{sessionId}}","revision":2,"objective":"Finish the ACP goal wrap-up snapshot proof","phase":"complete","maxGoalRounds":2},"roundsStarted":1,"createdAt":0,"updatedAt":0}} -{"type":"tool/result","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"call_goal_complete"},"content":[{"type":"tool-result","toolCallId":"call_goal_complete","content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":2,\"objective\":\"Finish the ACP goal wrap-up snapshot proof\",\"phase\":\"complete\",\"roundsStarted\":1,\"maxGoalRounds\":2},\"activation\":\"disarmed\"}"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[29],"surfaceOp":"append"} -{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nObjective: \"Finish the ACP goal wrap-up snapshot proof\"\nThe goal is marked complete and this autonomous run is ending. Write the closing message to the user now: state the outcome, summarize what was done and how it was verified, and point to the concrete results (files, commits, or other artifacts). Report only what earlier rounds and tool results in this session actually establish; when a detail is not in the session, say so instead of inventing it. Note anything the user should review or do next. Address the user directly. Do not call any more tools in this run; further work waits for the user's next instruction.\n"}],"source":{"kind":"plugin","plugin":"tool-goal","form":"notice","summary":"complete: Finish the ACP goal wrap-up snapshot proof"},"role":"user","id":"{{sessionId}}"}]}} +{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_goal_complete"},"toolCallId":"call_goal_complete","content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":2,\"objective\":\"Finish the ACP goal wrap-up snapshot proof\",\"phase\":\"complete\",\"roundsStarted\":1,\"maxGoalRounds\":2},\"activation\":\"disarmed\"}"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[29],"surfaceOp":"append"} +{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nObjective: \"Finish the ACP goal wrap-up snapshot proof\"\nThe goal is marked complete and this autonomous run is ending. Write the closing message to the user now: state the outcome, summarize what was done and how it was verified, and point to the concrete results (files, commits, or other artifacts). Report only what earlier rounds and tool results in this session actually establish; when a detail is not in the session, say so instead of inventing it. Note anything the user should review or do next. Address the user directly. Do not call any more tools in this run; further work waits for the user's next instruction.\n"}],"source":{"kind":"tool-goal","form":"notice","summary":"complete: Finish the ACP goal wrap-up snapshot proof"},"role":"user","id":"{{sessionId}}"}]}} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":2}} -{"type":"user/message","data":{"content":[{"type":"text","text":"\nObjective: \"Finish the ACP goal wrap-up snapshot proof\"\nThe goal is marked complete and this autonomous run is ending. Write the closing message to the user now: state the outcome, summarize what was done and how it was verified, and point to the concrete results (files, commits, or other artifacts). Report only what earlier rounds and tool results in this session actually establish; when a detail is not in the session, say so instead of inventing it. Note anything the user should review or do next. Address the user directly. Do not call any more tools in this run; further work waits for the user's next instruction.\n"}],"source":{"kind":"plugin","plugin":"tool-goal","form":"notice","summary":"complete: Finish the ACP goal wrap-up snapshot proof"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"\nObjective: \"Finish the ACP goal wrap-up snapshot proof\"\nThe goal is marked complete and this autonomous run is ending. Write the closing message to the user now: state the outcome, summarize what was done and how it was verified, and point to the concrete results (files, commits, or other artifacts). Report only what earlier rounds and tool results in this session actually establish; when a detail is not in the session, say so instead of inventing it. Note anything the user should review or do next. Address the user directly. Do not call any more tools in this run; further work waits for the user's next instruction.\n"}],"source":{"kind":"tool-goal","form":"notice","summary":"complete: Finish the ACP goal wrap-up snapshot proof"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"GOAL WRAP-UP: the snapshot objective is achieved and this closing message reaches the user."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":52,"outputTokens":14},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["GOAL WRAP-UP: the snapshot objective is achieved and this closing message reaches the user."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"GOAL WRAP-UP: the snapshot objective is achieved and this closing message reaches the user."}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":52,"outputTokens":14}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":2}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/apps/cli/tests/profiles/acp/tests/image-offload.expected.e2e.ts b/apps/cli/tests/profiles/acp/tests/image-offload.expected.e2e.ts index 59195c7669..259ba7341e 100644 --- a/apps/cli/tests/profiles/acp/tests/image-offload.expected.e2e.ts +++ b/apps/cli/tests/profiles/acp/tests/image-offload.expected.e2e.ts @@ -139,7 +139,8 @@ it('pins Messages Files offload and inline fallback in assembled requests', asyn const fileImage = { type: 'image', source: { type: 'file', file_id: 'file-api-snapshot-1' } } const offloadedImage = `[image omitted to fit request image limits; ${attachmentId}.${normalizedAccess}]` const imageHandle = `Image ${attachmentId}; request preview 1x1px.${normalizedAccess}` - const runtimeContext = 'Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\n' + const runtimeContext = (cwd: string): string => 'Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\n' + + `Current working directory: ${JSON.stringify(cwd)}.\n\n` + 'Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\n' + 'Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).' const messages = requests[0]?.messages as { content?: unknown }[] | undefined @@ -151,7 +152,7 @@ it('pins Messages Files offload and inline fallback in assembled requests', asyn { type: 'text', text: imageHandle }, fileImage, { type: 'text', text: ', then use read_image on red.png and reply with DONE.' }, - { type: 'text', text: runtimeContext }, + { type: 'text', text: runtimeContext(result.cwd) }, ]) const followup = requests[1]?.messages as Array<{ role: string; content: Array> }> @@ -202,7 +203,7 @@ it('pins Messages Files offload and inline fallback in assembled requests', asyn { type: 'text', text: `Image ${attachmentId}; request preview 1x1px.${fallbackAccess}` }, { type: 'image', source: { type: 'base64', media_type: 'image/png', data: image } }, { type: 'text', text: ', then use read_image on red.png and reply with DONE.' }, - { type: 'text', text: runtimeContext }, + { type: 'text', text: runtimeContext(fallback.cwd) }, ]) } finally { await new Promise(resolve => server.close(() => { resolve() })) diff --git a/apps/cli/tests/profiles/fixtures/official-installer-observer.mjs b/apps/cli/tests/profiles/fixtures/official-installer-observer.mjs new file mode 100644 index 0000000000..5ca8db1ac6 --- /dev/null +++ b/apps/cli/tests/profiles/fixtures/official-installer-observer.mjs @@ -0,0 +1,117 @@ +/** Observe ordinary manager operations in an installed Web profile without starting native agents. */ +import { existsSync } from 'node:fs' +import { readFile, readdir, stat } from 'node:fs/promises' +import { basename, join } from 'node:path' + +export const name = 'official-installer-observer' +export const inject = ['pluginManager', 'agentPresets', 'subagents', 'sessions', 'appReady'] + +let lastStage = 'none' + +/** Report one stage on the child's stdout, which the test forwards when it fails. */ +function note(stage) { + lastStage = stage + process.stdout.write(`PACKED_OFFICIAL_STEP=${stage} ${String(Date.now())}\n`) +} + +// A run that never finishes must name its stage in the parent's log even when the child is killed at its deadline. +setInterval(() => { note(`stalled-at ${lastStage}`) }, 30_000).unref() + +export function apply(ctx, config) { + let active = true + note('applied') + const stop = ctx.appReady.onReady(() => { + note('ready') + void exercise(ctx, config).then( + value => { if (active) process.stdout.write(`PACKED_OFFICIAL_RESULT=${JSON.stringify({ value })}\n`) }, + error => { if (active) process.stdout.write(`PACKED_OFFICIAL_RESULT=${JSON.stringify({ error: String(error) })}\n`) }, + ) + }) + ctx.effect(() => () => { active = false; stop() }, 'packed-official-observer') +} + +function applied(stage, result) { + if (result.application !== 'applied') throw new Error(`${stage}: ${JSON.stringify(result)}`) +} + +/** + * Run one operation under a bound, so a stalled manager operation reports what it waits for instead of + * holding the whole child until its timeout. pnpm 11.7.0 can print its result and never exit + * (pnpm#12297, fixed in 11.23.0); the run record names the process that stayed alive. + * @param label - the operation being run. + * @param operation - the manager call to await. + * @param boundMs - the time the operation may take. + * @param profile - the isolated profile directory the operation writes. + * @returns The operation's own result. + */ +async function bounded(label, operation, boundMs, profile) { + let timer + const expiry = new Promise((_resolve, reject) => { + timer = setTimeout(() => { reject(new Error(`${label} did not settle within ${String(boundMs)}ms`)) }, boundMs) + }) + try { + return await Promise.race([operation(), expiry]) + } catch (error) { + if (!String(error).includes('did not settle')) throw error + const locks = await readdir(profile).then(files => files.filter(file => file.endsWith('.lock')).join(',') || 'none', () => 'none') + const record = await readFile(join(profile, '.plugin-manager', 'run.json'), 'utf8') + .then(text => JSON.parse(text), () => undefined) + const pid = record?.pid + const alive = pid === undefined ? false : (() => { try { process.kill(pid, 0); return true } catch { return false } })() + const state = alive && existsSync(`/proc/${String(pid)}/status`) + ? await readFile(`/proc/${String(pid)}/status`, 'utf8').then(text => text.split('\n').filter(line => /^(State|Threads):/u.test(line)).join(' '), () => '') + : '' + const cmdline = alive && existsSync(`/proc/${String(pid)}/cmdline`) + ? await readFile(`/proc/${String(pid)}/cmdline`, 'utf8').then(text => text.replaceAll('\0', ' ').trim().slice(0, 120), () => '') + : '' + throw new Error(`${String(error)}\nprofile-locks=${locks}\npnpm-run pid=${String(pid)} alive=${String(alive)} ${state} ${cmdline}`) + } finally { clearTimeout(timer) } +} + +async function exercise(ctx, config) { + note('list-initial') + const initial = await ctx.pluginManager.listBundles() + note('listed-initial') + const steps = [] + for (const name of config.packages) { + const spec = initial.find(bundle => bundle.name === name)?.installTarget?.spec + if (spec === undefined) throw new Error(`No catalog installation target for ${name}`) + const options = { enabled: true, saveExact: true, registry: config.registry } + note(`install ${name}`) + let install = await ctx.pluginManager.installBundle(spec, options) + const approval = install.pendingBuilds + if (approval?.length) install = await ctx.pluginManager.installBundle(spec, { ...options, approvedBuilds: approval }) + note(`installed ${name}`) + applied(`install ${name}`, install) + const manifest = JSON.parse(await readFile(join(config.profile, 'package.json'), 'utf8')) + const installed = (await ctx.pluginManager.listBundles()).find(bundle => bundle.name === name) + // A transitive repository package the registry served, absent from npm at this version. + const localDependency = JSON.parse(await readFile(join(config.profile, 'node_modules', config.localDependency, 'package.json'), 'utf8')).version + const provider = name.endsWith('-codex') ? 'codex' : 'claude-code' + const providerEnabled = ctx.subagents.list().includes(provider) + const nativeDir = join(config.profile, 'node_modules', config.nativePackages[name]) + const binaryName = `${provider === 'codex' ? 'codex' : 'claude'}${process.platform === 'win32' ? '.exe' : ''}` + const nativeBinary = (await readdir(nativeDir, { recursive: true })).find(path => basename(path) === binaryName) + const nativeArtifact = nativeBinary !== undefined && (await stat(join(nativeDir, nativeBinary))).size > 0 + const presets = await ctx.agentPresets.list() + const beforeUnavailable = await readFile(join(config.profile, 'package.json'), 'utf8') + note(`unavailable ${name}`) + const unavailable = await ctx.pluginManager.installBundle(`${name}@9999.0.0`, options) + note(`unavailable-done ${name}`) + const unchangedAfterUnavailable = beforeUnavailable === await readFile(join(config.profile, 'package.json'), 'utf8') + const off = await ctx.pluginManager.setBundleEnabled(name, false) + applied(`disable ${name}`, off) + const disabled = (await ctx.pluginManager.listBundles()).find(bundle => bundle.name === name) + const on = await ctx.pluginManager.setBundleEnabled(name, true) + applied(`enable ${name}`, on) + const enabled = (await ctx.pluginManager.listBundles()).find(bundle => bundle.name === name) + note(`remove ${name}`) + const removed = await bounded(`remove ${name}`, () => ctx.pluginManager.removeBundle(name), 90_000, config.profile) + note(`removed ${name}`) + applied(`remove ${name}`, removed) + const absent = (await ctx.pluginManager.listBundles()).find(bundle => bundle.name === name) + steps.push({ name, install, savedVersion: manifest.dependencies?.[name], installed, localDependency, providerEnabled, nativeArtifact, + presets, unavailable, unchangedAfterUnavailable, off, disabled, on, enabled, removed, absent }) + } + return { initial, steps, sessionCount: ctx.sessions.list().length } +} diff --git a/apps/cli/tests/profiles/headless/goal-snapshot.patch.yml b/apps/cli/tests/profiles/headless/goal-snapshot.patch.yml index 7db9691a6c..3778da03de 100644 --- a/apps/cli/tests/profiles/headless/goal-snapshot.patch.yml +++ b/apps/cli/tests/profiles/headless/goal-snapshot.patch.yml @@ -24,6 +24,9 @@ - insert: - id: llm-replay name: '@deepseek-ai/dsh-llm-replay' + config: + providers: + - id: deepseek-official - id: session-log-deepseek config: diff --git a/apps/cli/tests/profiles/headless/subagent-inheritance-snapshot.patch.yml b/apps/cli/tests/profiles/headless/subagent-inheritance-snapshot.patch.yml index e41c1f97f2..d347a9cfe4 100644 --- a/apps/cli/tests/profiles/headless/subagent-inheritance-snapshot.patch.yml +++ b/apps/cli/tests/profiles/headless/subagent-inheritance-snapshot.patch.yml @@ -29,6 +29,9 @@ - id: replay name: '@deepseek-ai/dsh-llm-replay' + - id: await-subagents + name: '../../../../../packages/test-support/session-snapshot/tests/fixtures/subagent-settlement-barrier.ts' + # Publish the persisted agent before the driver selects the single root. - id: resumed-agent name: './tests/fixtures/subagent-inheritance-agent.ts' diff --git a/apps/cli/tests/profiles/headless/subagent-settlement-snapshot.patch.yml b/apps/cli/tests/profiles/headless/subagent-settlement-snapshot.patch.yml index 498c600d7b..2b4b636ecb 100644 --- a/apps/cli/tests/profiles/headless/subagent-settlement-snapshot.patch.yml +++ b/apps/cli/tests/profiles/headless/subagent-settlement-snapshot.patch.yml @@ -24,6 +24,9 @@ - insert: - id: llm-replay name: '@deepseek-ai/dsh-llm-replay' + config: + providers: + - id: deepseek-official # Hold the parent's second step until the manager notice enters its inbox. - id: settlement-fence diff --git a/apps/cli/tests/profiles/headless/tests/coding-task.e2e.ts b/apps/cli/tests/profiles/headless/tests/coding-task.e2e.ts index bb0b4297b8..531510393e 100644 --- a/apps/cli/tests/profiles/headless/tests/coding-task.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/coding-task.e2e.ts @@ -55,7 +55,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('coding task: fix a failing test expect(before.status).not.toBe(0) ctx = await codingHarness(workdir, { personaPrefix: SYSTEM_PROMPT }) - const agent = await ctx.agentLoop.create(SessionId('e2e-task'), { provider: 'deepseek-official', model: 'deepseek-v4-flash' }) + const agent = await ctx.agentLoop.create(SessionId('e2e-task'), { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, { cwd: workdir }) agent.followup(createUserMessage({ content: [{ diff --git a/apps/cli/tests/profiles/headless/tests/compaction.e2e.ts b/apps/cli/tests/profiles/headless/tests/compaction.e2e.ts index bf2d550bcc..b3e5cdb79e 100644 --- a/apps/cli/tests/profiles/headless/tests/compaction.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/compaction.e2e.ts @@ -49,7 +49,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('compaction: a long session compa }, persistenceRoot: join(workdir, '.sessions'), }) - const agent = await ctx.agentLoop.create(SessionId('e2e-compaction'), { provider: 'deepseek-official', model: 'deepseek-v4-flash' }) + const agent = await ctx.agentLoop.create(SessionId('e2e-compaction'), { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, { cwd: workdir }) // Completed read turns cannot collapse into one retained parallel tool group. for (let i = 1; i <= 4; i++) { diff --git a/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl index 36fe8fd2d4..ab06b02706 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl @@ -4,11 +4,11 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Probe strict-schema fillers against missing-goal revision 1, then create a durable goal to finish the snapshot proof and inspect it."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Probe strict-schema fillers against miss","messageSeqs":[8],"source":{"kind":"fallback"}}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Probe strict-schema fillers against missing-goal revision 1, then create a durable goal to finish the snapshot proof and inspect it.\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Probe strict-schema fillers against missing-goal revision 1, then create a durable goal to finish the snapshot proof and inspect it.\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":4096}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_goal_probe","name":"update_goal","arguments":"{\"goal_id\":\"missing-goal\",\"revision\":1,\"action\":\"pause\",\"objective\":\"\",\"max_goal_rounds\":0,\"blocked_reason\":\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":15,"outputTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_goal_probe","name":"update_goal","args":["{\"goal_id\":\"missing-goal\",\"revision\":1,\"action\":\"pause\",\"objective\":\"\",\"max_goal_rounds\":0,\"blocked_reason\":\"\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_goal_probe","name":"update_goal","arguments":"{\"goal_id\":\"missing-goal\",\"revision\":1,\"action\":\"pause\",\"objective\":\"\",\"max_goal_rounds\":0,\"blocked_reason\":\"\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":15,"outputTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"call_goal_probe","name":"update_goal","arguments":"{\"goal_id\":\"missing-goal\",\"revision\":1,\"action\":\"pause\",\"objective\":\"\",\"max_goal_rounds\":0,\"blocked_reason\":\"\"}"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":16,"time":0,"data":{"title":"Goal tool snapshot","messageSeqs":[8],"source":{"kind":"provider","provider":"session-title-first-prompt-llm","model":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl index 1f85f2ff2a..904ee65726 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":3,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0} +{"type":"session","version":4,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0} {"type":"permission/preset","data":{"preset":"danger-full-access"}} {"type":"sandbox/mode","data":{"mode":"danger-full-access"}} {"type":"approval/policy","data":{"policy":"never"}} @@ -6,16 +6,16 @@ {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} -{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} +{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Prove the product headless profile path with one real tool round trip."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"cli-mock","model":"cli-mock","reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"cli-mock","model":"cli-mock"}} {"type":"session/title","data":{"title":"Prove the product headless profile","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"session/title-llm-request","data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"cli-mock","model":"cli-mock"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Prove the product headless profile path with one real tool round trip.\"}]"}],"source":{"kind":"plugin","plugin":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}} +{"type":"session/title-llm-request","data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"cli-mock","model":"cli-mock"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Prove the product headless profile path with one real tool round trip.\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":4096,"reasoningEffort":"off"}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Inspecting the task before the tool call."},{"type":"tool-call","id":"cli-smoke-call","name":"bash","arguments":"{\"command\":\"printf CLI_TOOL_ROUND_TRIP\",\"description\":\"Prove the CLI tool round trip.\"}"}],"source":{"kind":"model","provider":"cli-mock","model":"cli-mock"},"id":"{{sessionId}}"},"usage":{"inputTokens":11,"outputTokens":3,"cacheReadTokens":2},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":0,"index":0,"dt":[],"texts":["Inspecting the task before the tool call."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Inspecting the task before the tool call."}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":1,"dt":[],"id":"cli-smoke-call","name":"bash","args":["{\"command\":\"printf CLI_TOOL_ROUND_TRIP\",\"description\":\"Prove the CLI tool round trip.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"cli-smoke-call","name":"bash","arguments":"{\"command\":\"printf CLI_TOOL_ROUND_TRIP\",\"description\":\"Prove the CLI tool round trip.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":11,"outputTokens":3,"cacheReadTokens":2}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"cli-smoke-call","name":"bash","arguments":"{\"command\":\"printf CLI_TOOL_ROUND_TRIP\",\"description\":\"Prove the CLI tool round trip.\"}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"cli-smoke-call"},"content":[{"type":"tool-result","toolCallId":"cli-smoke-call","content":[{"type":"text","text":"CLI_TOOL_ROUND_TRIP"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"cli-smoke-call"},"toolCallId":"cli-smoke-call","content":[{"type":"text","text":"CLI_TOOL_ROUND_TRIP"}],"isError":false,"id":"{{sessionId}}"},"meta":{"cwd":"{{cwd}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"request/header","data":{"header":{"config":{"provider":"cli-mock","model":"cli-mock","reasoningEffort":"off"},"tools":"{{tools}}"},"reason":"change"}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl index 0f7431a36e..6ea0d43a17 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl @@ -4,11 +4,11 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"say pong\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"say pong\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":4096,"reasoningEffort":"off"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/attempt","seq":14,"time":0,"data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"llm-deepseek: the API key resolved from DEEPSEEK_API_KEY contains characters no HTTP header can carry; set DEEPSEEK_API_KEY to the raw key alone (the web Models page writes it)","code":"INVALID_CREDENTIAL"}}}}]}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":15,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":16,"time":0,"data":{"turn":1,"reason":{"kind":"error","error":{"message":"llm-deepseek: the API key resolved from DEEPSEEK_API_KEY contains characters no HTTP header can carry; set DEEPSEEK_API_KEY to the raw key alone (the web Models page writes it)","code":"INVALID_CREDENTIAL"}}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl index e8f421ae70..f24d221b96 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl @@ -4,11 +4,11 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"say pong\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"say pong\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":4096,"reasoningEffort":"off"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/attempt","seq":14,"time":0,"data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"llm-deepseek: no API key for provider route \"deepseek-official\"; store DEEPSEEK_API_KEY through the credentials service (the web Models page writes it), or export DEEPSEEK_API_KEY in the launching environment","code":"MISSING_CREDENTIAL"}}}}]}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":15,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":16,"time":0,"data":{"turn":1,"reason":{"kind":"error","error":{"message":"llm-deepseek: no API key for provider route \"deepseek-official\"; store DEEPSEEK_API_KEY through the credentials service (the web Models page writes it), or export DEEPSEEK_API_KEY in the launching environment","code":"MISSING_CREDENTIAL"}}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl index bce6fb173d..a667224c5f 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl @@ -4,11 +4,11 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"retry the transient provider failure"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"retry the transient provider failure","messageSeqs":[8],"source":{"kind":"fallback"}}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"retry the transient provider failure\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"retry the transient provider failure\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":4096}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/attempt","seq":14,"time":0,"data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"snapshot transient failure","code":"RATE_LIMIT","status":429}}}}]}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"llm/retry","seq":15,"time":0,"data":{"retryId":"{{sessionId}}","turn":1,"step":1,"provider":"deepseek-official","mode":"normal","policyKey":"[\"normal\",1,[\"RATE_LIMIT\"],1,1,0]","retry":1,"maxRetries":1,"delayMs":1,"failure":{"message":"snapshot transient failure","code":"RATE_LIMIT","status":429}}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"llm/retry-started","seq":16,"time":0,"data":{"retryId":"{{sessionId}}","turn":1,"step":1,"retry":1}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl index 3db7f52dd3..2afe21d157 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl @@ -18,7 +18,7 @@ {"type":"step/start","data":{"turn":2,"step":1}} {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Continue safely from the interrupted operation."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Perform one side-effecting remote mutati","messageSeqs":[3],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl index b210549cfa..5ea5179b2e 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl @@ -16,7 +16,7 @@ {"type":"step/start","data":{"turn":2,"step":1}} {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Call list_agents once and report what it shows."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Start a background job.","messageSeqs":[3],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl index 70c1834115..00868634d4 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl @@ -1,20 +1,20 @@ -{"type":"session","version":3,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{sessionId}}","isSeeded":false,"origin":"subagent","delegationDepth":1} +{"type":"session","version":4,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{sessionId}}","isSeeded":false,"origin":"subagent","delegationDepth":1} +{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Delegated write probe","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash","agentReasoningEffort":"low"}} {"type":"sandbox/mode","data":{"mode":"read-only","source":"delegation"}} {"type":"approval/policy","data":{"policy":"never","source":"delegation"}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"{{sessionId}}\", message: \"\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Delegated write probe"}} {"type":"step/start","data":{"turn":1,"step":1}} -{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"{{sessionId}}\", message: \"\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Use the write tool exactly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"child-write","name":"write","arguments":"{\"file_path\": \"inherited.txt\", \"content\": \"escaped\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"child-write","name":"write","args":["{\"file_path\": \"inherited.txt\", \"content\": \"escaped\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"child-write","name":"write","arguments":"{\"file_path\": \"inherited.txt\", \"content\": \"escaped\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"child-write","name":"write","arguments":"{\"file_path\": \"inherited.txt\", \"content\": \"escaped\"}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"child-write"},"content":[{"type":"tool-result","toolCallId":"child-write","content":[{"type":"text","text":"Error: [sandbox: file access denied under read-only mode]\n[sandbox: escalation available — retry this exact operation once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]"}],"isError":true}],"role":"user","id":"{{sessionId}}"},"error":{"name":"FsError","code":"FS_SANDBOX_DENIED"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"child-write"},"toolCallId":"child-write","content":[{"type":"text","text":"Error: [sandbox: file access denied under read-only mode]\n[sandbox: escalation available — retry this exact operation once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]"}],"isError":true,"id":"{{sessionId}}"},"error":{"name":"FsError","code":"FS_SANDBOX_DENIED"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["CHILD_DENIED [sandbox: file access denied under read-only mode]"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl index f4077c4510..1cee326a29 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl @@ -16,17 +16,20 @@ {"type":"step/start","data":{"turn":2,"step":1}} {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Delegate the write probe to a subagent."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","write"]},"reason":"resume","startsSeries":true}} -{"type":"developer/message","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"exit_plan_mode"},{"type":"tool-addition","toolName":"get_goal"},{"type":"tool-addition","toolName":"glob"},{"type":"tool-addition","toolName":"grep"},{"type":"tool-addition","toolName":"interrupt_agent"},{"type":"tool-addition","toolName":"job_kill"},{"type":"tool-addition","toolName":"job_list"},{"type":"tool-addition","toolName":"job_output"},{"type":"tool-addition","toolName":"list_agents"},{"type":"tool-addition","toolName":"read"},{"type":"tool-addition","toolName":"read_image"},{"type":"tool-addition","toolName":"send_message"},{"type":"tool-addition","toolName":"skill"},{"type":"tool-addition","toolName":"subagent"},{"type":"tool-addition","toolName":"subagent_fork"},{"type":"tool-addition","toolName":"todo_write"},{"type":"tool-addition","toolName":"update_goal"},{"type":"tool-addition","toolName":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"resume","startsSeries":true}} +{"type":"developer/message","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"exit_plan_mode"},{"type":"tool-addition","toolName":"get_goal"},{"type":"tool-addition","toolName":"glob"},{"type":"tool-addition","toolName":"grep"},{"type":"tool-addition","toolName":"interrupt_agent"},{"type":"tool-addition","toolName":"job_kill"},{"type":"tool-addition","toolName":"job_list"},{"type":"tool-addition","toolName":"job_output"},{"type":"tool-addition","toolName":"list_agents"},{"type":"tool-addition","toolName":"read"},{"type":"tool-addition","toolName":"read_image"},{"type":"tool-addition","toolName":"send_message"},{"type":"tool-addition","toolName":"skill"},{"type":"tool-addition","toolName":"subagent"},{"type":"tool-addition","toolName":"subagent_fork"},{"type":"tool-addition","toolName":"todo_write"},{"type":"tool-addition","toolName":"update_goal"},{"type":"tool-addition","toolName":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Tighten this session to read-only.","messageSeqs":[3],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"delegate-write","name":"subagent","args":["{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"one-shot","label":"Delegated write probe"}} -{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"delegate-write"},"toolCallId":"delegate-write","content":[{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[23],"surfaceOp":"append"} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Delegated write probe"}} +{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}} +{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"delegate-write"},"toolCallId":"delegate-write","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[23],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} +{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":2}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"The delegated child was denied by the sandbox. PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["The delegated child was denied by the sandbox. PARENT_DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The delegated child was denied by the sandbox. PARENT_DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":2}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl index 520e96bbc0..1b82a99cf0 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":3,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{sessionId}}","isSeeded":false,"origin":"subagent","delegationDepth":1} +{"type":"session","version":4,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{sessionId}}","isSeeded":false,"origin":"subagent","delegationDepth":1} {"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Return child result","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}} {"type":"sandbox/mode","data":{"mode":"workspace-write","source":"delegation"}} {"type":"approval/policy","data":{"policy":"never","source":"delegation"}} @@ -6,9 +6,9 @@ {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} -{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} +{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"{{sessionId}}\", message: \"\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Reply with exactly CHILD_RESULT and","messageSeqs":[8],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/parent.override.json b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/parent.override.json index 336bcafb3b..91428bb387 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/parent.override.json +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/parent.override.json @@ -33,8 +33,8 @@ "kind": "chunks", "chunks": [ { "type": "block-start", "index": 0, "blockType": "text" }, - { "type": "text-delta", "index": 0, "text": "PARENT_RECEIVED_CHILD_RESULT" }, - { "type": "block-end", "index": 0, "block": { "type": "text", "text": "PARENT_RECEIVED_CHILD_RESULT" } }, + { "type": "text-delta", "index": 0, "text": "PARENT_RECEIVED_SETTLEMENT_NOTICE" }, + { "type": "block-end", "index": 0, "block": { "type": "text", "text": "PARENT_RECEIVED_SETTLEMENT_NOTICE" } }, { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 5 } }, { "type": "finish", "reason": { "kind": "stop" } } ] diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl index 3c8e6615eb..f06a75700f 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl @@ -1,19 +1,19 @@ -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":3,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Start one continuable background subagent and answer from its completion notice. Do not call list_agents, send_message, job_output, or job_list."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":3,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Start one continuable subagent and acknowledge its completion notice. Do not call list_agents or send_message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":4,"time":0,"data":{"turn":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":5,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Start one continuable background subagent and answer from its completion notice. Do not call list_agents, send_message, job_output, or job_list."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Start one continuable subagent and acknowledge its completion notice. Do not call list_agents or send_message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Start one continuable background subagen","messageSeqs":[8],"source":{"kind":"fallback"}}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Start one continuable background subagent and answer from its completion notice. Do not call list_agents, send_message, job_output, or job_list.\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Start one continuable subagent and","messageSeqs":[8],"source":{"kind":"fallback"}}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Start one continuable subagent and acknowledge its completion notice. Do not call list_agents or send_message.\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":4096}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"start-child","name":"subagent","args":["{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":16,"time":0,"data":{"title":"Subagent settlement","messageSeqs":[8],"source":{"kind":"provider","provider":"session-title-first-prompt-llm","model":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"subagent/catalog","seq":17,"time":0,"data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Return child result"}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"start-child"},"content":[{"type":"text","text":"started subagent {{sessionId}}"}],"role":"tool","toolCallId":"start-child","isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"start-child"},"toolCallId":"start-child","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":19,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":20,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":21,"time":0,"data":{"turn":1,"step":2}}} @@ -22,7 +22,7 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":24,"time":0,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":25,"time":0,"data":{"turn":1,"step":3}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":26,"time":0,"data":{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":27,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_RECEIVED_CHILD_RESULT"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["PARENT_RECEIVED_CHILD_RESULT"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_RECEIVED_CHILD_RESULT"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":27,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_RECEIVED_SETTLEMENT_NOTICE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["PARENT_RECEIVED_SETTLEMENT_NOTICE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_RECEIVED_SETTLEMENT_NOTICE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":28,"time":0,"data":{"turn":1,"step":3}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":29,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}} -{"type":"result","sessionId":"{{sessionId}}","output":"PARENT_RECEIVED_CHILD_RESULT","usage":{"inputTokens":30,"outputTokens":15}} +{"type":"result","sessionId":"{{sessionId}}","output":"PARENT_RECEIVED_SETTLEMENT_NOTICE","usage":{"inputTokens":30,"outputTokens":15}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl index 7dc7407bff..5ed8464fb7 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl @@ -17,7 +17,7 @@ {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"\nUpdated instructions from: AGENTS.md\n\nThis file changed after it was loaded. Use the following content instead of the previously loaded instructions from this file.\n\nNew workspace instruction after offline edit.\n\n"}],"source":{"kind":"agent-instructions","form":"instructions","changes":[{"action":"replace","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"d8375b516f158718bd3463bc8eb7ed42c011b29f"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Remember the workspace instruction.","messageSeqs":[3],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl index 76af22df23..f52d5f06e9 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl @@ -17,7 +17,7 @@ {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"\nThis complete workspace instruction baseline replaces all earlier workspace instruction baselines. The following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nCurrent AGENTS rule.\n\n\nInstructions from: CLAUDE.md\n\nCurrent CLAUDE rule.\n\n"}],"source":{"kind":"agent-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"7f53d2327837129750aef117f9754a001c46cf68"},{"action":"set","scope":".\u0000CLAUDE.md","path":"CLAUDE.md","digest":"5b1e9e3fd759eee6b43ceff899e47fb10c64701a"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Remember the workspace instruction.","messageSeqs":[3],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/headless/tests/full-loop.e2e.ts b/apps/cli/tests/profiles/headless/tests/full-loop.e2e.ts index bbacf9dc15..c8051b05ea 100644 --- a/apps/cli/tests/profiles/headless/tests/full-loop.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/full-loop.e2e.ts @@ -29,7 +29,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('full loop: real model + real bas it('runs a bash command on request and reports its output', async () => { workdir = await mkdtemp(join(tmpdir(), 'dsh-full-loop-e2e-')) ctx = await codingHarness(workdir, { personaPrefix: SYSTEM_PROMPT }) - const agent = await ctx.agentLoop.create(SessionId('e2e-loop'), { provider: 'deepseek-official', model: 'deepseek-v4-flash' }) + const agent = await ctx.agentLoop.create(SessionId('e2e-loop'), { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, { cwd: workdir }) agent.followup(createUserMessage({ content: [{ type: 'text', text: 'Run `echo e2e-ok` with the bash tool and tell me its exact output.' }], source: { kind: 'user' } })) await waitForIdle(ctx, agent) diff --git a/apps/cli/tests/profiles/headless/tests/harness.ts b/apps/cli/tests/profiles/headless/tests/harness.ts index c07b4b73fd..cf51dc23a6 100644 --- a/apps/cli/tests/profiles/headless/tests/harness.ts +++ b/apps/cli/tests/profiles/headless/tests/harness.ts @@ -66,6 +66,7 @@ export interface CodingHarnessOptions { export async function codingHarness(workdir: string, options: CodingHarnessOptions = {}): Promise { const ctx = new Context() await mountAgentLoopTestDependencies(ctx, { + workingDirectory: true, systemPrompt: { personaPrefix: options.personaPrefix ?? '' }, }) await ctx.plugin(AgentLoop, { agents: [] }) diff --git a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts index d464f2863f..7a46b1dc89 100644 --- a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts @@ -47,11 +47,21 @@ const headlessSessionExpected = join(goldensDir, 'headless-profile', 'session.ex const headlessReasoningExpected = join(goldensDir, 'headless-profile', 'reasoning.stderr.expected.txt') const headlessFailureExpected = join(goldensDir, 'headless-profile', 'stderr.expected.txt') const refreshing = process.env.DSH_SNAPSHOT === 'refresh' +/** The shipped base bundle's title output cap, which marks a title request's `max_tokens`. */ +const TITLE_MAX_TOKENS = 4096 interface JsonObject { [key: string]: unknown } +/** Framed user text that distinguishes the auxiliary title request from a main request. */ +const TITLE_PROMPT_MARKER = 'Generate the session title from this JSON array of human messages' + +/** Whether one recorded request body is the auxiliary title request. */ +function isTitleRequest(request: JsonObject): boolean { + return JSON.stringify(request['messages'] ?? null).includes(TITLE_PROMPT_MARKER) +} + interface PersistedLog { readonly content: string readonly header: JsonObject @@ -100,7 +110,7 @@ async function deepseekDefaultsServer( const write = (): void => { // One-shot teardown may cancel background title work after the main response. if (keepAlives-- > 0 - || (options.waitForTitleRequest === true && !requests.some(request => request.max_tokens === 64))) { + || (options.waitForTitleRequest === true && !requests.some(isTitleRequest))) { response.write(': keep-alive\n\n') timer = setTimeout(write, 60) return @@ -259,6 +269,8 @@ describe('headless stream-json snapshots', () => { if (actual === undefined) throw new Error('the headless profile did not persist its session') const context = contextFromLogs([actual.content]) const session = normalizeSessionSnapshot(actual.content, context) + const titleRequest = parseJsonl(session).find(event => event.type === 'session/title-llm-request') + expect(titleRequest?.data).toMatchObject({ reasoningEffort: 'off', maxTokens: TITLE_MAX_TOKENS }) if (refreshing) await writeFile(headlessSessionExpected, session) await expectSessionSnapshot(session, context, headlessSessionExpected) expect(session).toContain(task) @@ -610,7 +622,8 @@ describe('headless stream-json snapshots', () => { expect(server.requests).toHaveLength(2) expect(server.paths).toEqual(['/v1/messages', '/v1/messages']) const agentRequest = server.requests.find(request => request.max_tokens === 256_000) - const titleRequest = server.requests.find(request => request.max_tokens === 64) + const titleRequest = server.requests.find(isTitleRequest) + expect(titleRequest?.max_tokens).toBe(TITLE_MAX_TOKENS) expect(agentRequest?.output_config).toEqual({ effort: 'low' }) expect(titleRequest).toBeDefined() const header = (parseJsonl(result.stdout) @@ -659,7 +672,10 @@ describe('headless stream-json snapshots', () => { } const title = await fetch(server.url, { method: 'POST', - body: JSON.stringify({ max_tokens: 64 }), + body: JSON.stringify({ + max_tokens: TITLE_MAX_TOKENS, + messages: [{ role: 'user', content: [{ type: 'text', text: TITLE_PROMPT_MARKER }] }], + }), }) for (;;) { const chunk = await reader.read() @@ -699,10 +715,10 @@ describe('headless stream-json snapshots', () => { expect(result.stderr).toBe('') expect(server.requests).toHaveLength(2) - const agentRequest = server.requests.find(request => request.max_tokens === 1024) - const titleRequest = server.requests.find(request => request.max_tokens === 64) + const agentRequest = server.requests.find(request => !isTitleRequest(request)) + const titleRequest = server.requests.find(isTitleRequest) expect(agentRequest).not.toHaveProperty('max_completion_tokens') - expect(titleRequest).toBeDefined() + expect(titleRequest?.max_tokens).toBe(TITLE_MAX_TOKENS) const header = (parseJsonl(result.stdout) .map(record => record.event) .find((event): event is JsonObject => ( @@ -752,12 +768,12 @@ describe('headless stream-json snapshots', () => { const parent = logs.find(log => typeof log.header.parentSession !== 'string') if (parent === undefined) throw new Error('Agent Teams snapshot did not persist its Lead') const rows = parseJsonl(parent.content) - const workflowChild = logs.find(log => parseJsonl(log.content).some(row => row.type === 'subagent/descriptor' - && (row.data as JsonObject).mode === 'one-shot')) + const workflowChild = logs.find(log => parseJsonl(log.content).some(row => row.type === 'user/message' + && JSON.stringify((row.data as JsonObject).content) === JSON.stringify([{ type: 'text', text: 'TEAM_WORKFLOW_CHILD' }]))) if (workflowChild === undefined) throw new Error('Team profile did not persist its workflow child') const workflowRows = parseJsonl(workflowChild.content) expect(workflowRows.find(row => row.type === 'subagent/descriptor')?.data) - .toMatchObject({ mode: 'one-shot', provider: 'spawn' }) + .toMatchObject({ mode: 'continuable', provider: 'spawn' }) expect(workflowRows.filter(row => row.type === 'user/message' && ((row.data as JsonObject).source as JsonObject).kind === 'user').map(row => row.data)) .toEqual([expect.objectContaining({ content: [{ type: 'text', text: 'TEAM_WORKFLOW_CHILD' }] })]) @@ -958,13 +974,13 @@ describe('headless stream-json snapshots', () => { await expectHeadlessStream(normalized, streamExpected) }, LOADER_SMOKE_TEST_TIMEOUT_MS) - it('delivers a continuable child result without parent polling', async () => { + it('notifies the parent when a continuable child settles without polling', async () => { const parentReplay = join(settlementScenarioDir, 'parent.replay.jsonl') const parentOverride = join(settlementScenarioDir, 'parent.override.json') const childReplay = join(settlementScenarioDir, 'child.replay.jsonl') const childExpected = join(settlementScenarioDir, 'child.expected.jsonl') const streamExpected = join(settlementScenarioDir, 'stream-json.expected.jsonl') - const task = 'Start one continuable background subagent and answer from its completion notice. Do not call list_agents, send_message, job_output, or job_list.' + const task = 'Start one continuable subagent and acknowledge its completion notice. Do not call list_agents or send_message.' let runCwd = '' const result = await runLoaderSmoke({ label: 'continuable settlement headless stream-json snapshot', @@ -1007,6 +1023,10 @@ describe('headless stream-json snapshots', () => { }) }) expect(notices).toHaveLength(1) + expect(notices[0]).toMatchObject({ + source: { kind: 'subagent-settled', form: 'notice', senderSessionId: child.header.id }, + }) + expect(JSON.stringify(notices[0])).toContain('finished and will do no further work unless you send it more.') expect(JSON.stringify(notices[0])).toContain('CHILD_RESULT') const context = contextFromLogs([parent.content, child.content]) @@ -1022,7 +1042,7 @@ describe('headless stream-json snapshots', () => { const records = parseJsonl(result.stdout) expect(records.at(-1)).toMatchObject({ type: 'result', - output: 'PARENT_RECEIVED_CHILD_RESULT', + output: 'PARENT_RECEIVED_SETTLEMENT_NOTICE', }) const normalized = normalizeHeadlessStream(result.stdout, runCwd) if (refreshing) await writeFile(streamExpected, normalized) diff --git a/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts b/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts index 48e8f9fc9f..806f081a77 100644 --- a/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts @@ -3,15 +3,15 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, onTestFinished, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import LlmRuntime, { createUserMessage, ToolCallId, HarnessError } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId, HarnessError } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import type { SessionEvent } from '@deepseek-ai/dsh-session' -import SystemPrompt from '@deepseek-ai/dsh-system-prompt' -import ToolRuntime, { RUN_CODE_NAME, defineTool } from '@deepseek-ai/dsh-tools' +import { RUN_CODE_NAME, defineTool } from '@deepseek-ai/dsh-tools' import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools' -import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' +import type { Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' +import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' import * as BashEnvPlugin from '@deepseek-ai/dsh-shell-env' @@ -55,12 +55,11 @@ afterEach(async () => { async function ptcModeHarness(cwd: string): Promise { const harness = new Context() - await harness.plugin(LlmRuntime) - await harness.plugin(SessionStore) - await harness.plugin(SessionProjectionRegistry) - await harness.plugin(SystemPrompt, { personaPrefix: PERSONA }) - await harness.plugin(ToolRuntime, { mode: 'ptc' }) - await harness.plugin(AgentRegistry) + await mountAgentLoopTestDependencies(harness, { + workingDirectory: true, + systemPrompt: { personaPrefix: PERSONA }, + tools: { mode: 'ptc' }, + }) await harness.plugin(AgentLoop, { agents: [] }) await harness.plugin(LlmDeepSeek) if (harness.get('subprocess') === undefined) await harness.plugin(LocalSubprocessRuntime) @@ -73,13 +72,12 @@ async function ptcModeHarness(cwd: string): Promise { async function workspacePtcModeHarness(): Promise { const harness = new Context() - await harness.plugin(LlmRuntime) - await harness.plugin(SessionStore) - await harness.plugin(SessionProjectionRegistry) - await harness.plugin(SystemPrompt, { personaPrefix: PERSONA }) - await harness.plugin(ToolRuntime, { mode: 'ptc' }) - await harness.plugin(AgentRegistry) await harness.plugin(LocalFileSystem, { cwd: '/' }) + await mountAgentLoopTestDependencies(harness, { + workingDirectory: true, + systemPrompt: { personaPrefix: PERSONA }, + tools: { mode: 'ptc' }, + }) await harness.plugin(ToolFs) await harness.plugin(AgentInstructions, { maxBytes: 65536 }) await harness.plugin(AgentLoop, { agents: [] }) @@ -131,8 +129,7 @@ async function mountRuntime(harness: Context): Promise { /** Keyless real-process harness for direct typed-binding acceptance tests. */ async function typedPtcModeHarness(): Promise { const harness = new Context() - await harness.plugin(SystemPrompt) - await harness.plugin(ToolRuntime, { mode: 'ptc' }) + await mountAgentLoopTestDependencies(harness, { workingDirectory: true, tools: { mode: 'ptc' } }) await mountRuntime(harness) return harness } @@ -298,7 +295,8 @@ describe('PTC mode typed values: keyless real-process contracts', () => { const agent = { id: SessionId('ptc-cordis'), session: ctx.sessions.create(SessionId('ptc-cordis'), { meta: { cwd: process.cwd() } }), - } as unknown as Agent + ctx, + } as Agent const value = completion(await runCode(ctx, ` const listed = await tools.cordis_inspect_list({}); @@ -328,7 +326,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('PTC mode: real model writes a pr it('collapses the wire tool list to [run_code], bridges sub-calls, and returns curated output', async () => { workdir = await mkdtemp(join(tmpdir(), 'dsh-ptc-e2e-')) ctx = await ptcModeHarness(workdir) - const agent = await ctx.agentLoop.create(SessionId('e2e-ptc'), { provider: 'deepseek-official', model: 'deepseek-v4-flash' }) + const agent = await ctx.agentLoop.create(SessionId('e2e-ptc'), { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, { cwd: workdir }) agent.followup(createUserMessage({ content: [{ diff --git a/apps/cli/tests/profiles/headless/tests/subagent-inheritance.expected.e2e.ts b/apps/cli/tests/profiles/headless/tests/subagent-inheritance.expected.e2e.ts index 4b5970cd9f..52df6db0d9 100644 --- a/apps/cli/tests/profiles/headless/tests/subagent-inheritance.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/subagent-inheritance.expected.e2e.ts @@ -3,7 +3,7 @@ * its child log and confines a real write under a wider deployment default. */ -import { readFile, readdir, writeFile } from 'node:fs/promises' +import { readFile, readdir, realpath, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { Context } from '@deepseek-ai/cordis' @@ -111,8 +111,8 @@ describe('parent-only override inheritance snapshot', () => { DSH_SNAPSHOT_CHILD_FILES: childReplay, }, prepare: async (runCwd) => { - cwd = runCwd - await seedReadOnlyParent(join(runCwd, '.sessions'), runCwd) + cwd = await realpath(runCwd) + await seedReadOnlyParent(join(cwd, '.sessions'), cwd) }, inspect: async (runCwd) => { // THE physical fact: the child's write never reached the disk. Under @@ -132,11 +132,12 @@ describe('parent-only override inheritance snapshot', () => { const childRecords = child.trimEnd().split('\n').map( line => JSON.parse(line) as Record, ) - expect(childRecords[1]).toMatchObject({ + expect(childRecords.find(record => record.type === 'sandbox/mode')).toMatchObject({ type: 'sandbox/mode', - seq: SessionSeq(0), data: { mode: 'read-only', source: 'delegation' }, }) + expect(childRecords.findIndex(record => record.type === 'sandbox/mode')) + .toBeLessThan(childRecords.findIndex(record => record.type === 'turn/start')) const runtimeContexts = (content: string): string[] => content.trimEnd().split('\n').flatMap((line) => { const record = JSON.parse(line) as { diff --git a/apps/cli/tests/profiles/headless/tests/todo-write.e2e.ts b/apps/cli/tests/profiles/headless/tests/todo-write.e2e.ts index 4ff5fef99c..ce688b60bd 100644 --- a/apps/cli/tests/profiles/headless/tests/todo-write.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/todo-write.e2e.ts @@ -27,7 +27,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('todo_write: real model records a it('appends a todo/write event with the model-produced task list', async () => { workdir = await mkdtemp(join(tmpdir(), 'dsh-todo-write-e2e-')) ctx = await codingHarness(workdir, { personaPrefix: TODO_SYSTEM_PROMPT }) - const agent = await ctx.agentLoop.create(SessionId('e2e-todo'), { provider: 'deepseek-official', model: 'deepseek-v4-flash' }) + const agent = await ctx.agentLoop.create(SessionId('e2e-todo'), { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, { cwd: workdir }) agent.followup(createUserMessage({ content: [{ type: 'text', text: diff --git a/apps/cli/tests/profiles/headless/tests/workspace-context-resume.expected.e2e.ts b/apps/cli/tests/profiles/headless/tests/workspace-context-resume.expected.e2e.ts index b873ad7d77..aa6da3ad23 100644 --- a/apps/cli/tests/profiles/headless/tests/workspace-context-resume.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/workspace-context-resume.expected.e2e.ts @@ -79,7 +79,6 @@ async function seedVisibleBaseline( content: file.content, })), { maxBytes: 65536 }) const config = resolveConfig({ - dshHome: join(cwd, '.dsh'), maxBytes: 65536, ...options.instructionFileCandidates === undefined ? {} diff --git a/apps/cli/tests/profiles/headless/workspace-context-resume-snapshot.patch.yml b/apps/cli/tests/profiles/headless/workspace-context-resume-snapshot.patch.yml index 6507b366d6..42a63474af 100644 --- a/apps/cli/tests/profiles/headless/workspace-context-resume-snapshot.patch.yml +++ b/apps/cli/tests/profiles/headless/workspace-context-resume-snapshot.patch.yml @@ -11,7 +11,6 @@ - id: agent-instructions config: maxBytes: 65536 - dshHome: !!js process.cwd() + '/.dsh' - id: session-persistence-jsonl config: diff --git a/apps/cli/tests/profiles/official-installer-packed.e2e.ts b/apps/cli/tests/profiles/official-installer-packed.e2e.ts new file mode 100644 index 0000000000..3062b5c5b8 --- /dev/null +++ b/apps/cli/tests/profiles/official-installer-packed.e2e.ts @@ -0,0 +1,199 @@ +/** Install actual packed Official providers through the ordinary manager in a source-independent Web process. */ +import { existsSync, globSync, readdirSync, readFileSync, statSync } from 'node:fs' +import { readFile, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { createInterface } from 'node:readline' +import { fileURLToPath } from 'node:url' +import { execa } from 'execa' +import yaml from 'js-yaml' +import { expect, it, onTestFinished } from 'vitest' +import type { BundleInfo, ChangeResult } from '@deepseek-ai/dsh-plugin-manager' +import { getDshRuntimeVersion } from '../../../../packages/boot/app-boot/src/plugin-compatibility.ts' +import { generateOfficialBundleCatalog } from '../../../../scripts/gen-official-bundle-catalog.ts' +import { + REGISTRY_MANIFEST_FILE, + createRegistryManifest, + hostPlatform, + packBundleArtifacts, + readReleaseMembers, + readWorkspacePackages, + selectBundleDependencies, + sourceEvidence, + startBundleRegistry, + verifyBuiltBundles, + writeRegistryManifest, +} from '../../../../scripts/local-bundle-registry.ts' +import { assertInstalledTree, createPackedInstallation } from './packed-installation.ts' + +const root = fileURLToPath(new URL('../../../..', import.meta.url)) +const packages = ['@deepseek-ai/dsh-subagent-claude-code', '@deepseek-ai/dsh-subagent-codex'] +const built = existsSync(join(root, 'apps/cli/lib/bin.js')) + && packages.every(name => existsSync(join(root, 'packages/subagent', name.slice('@deepseek-ai/dsh-'.length), 'lib/index.js'))) + +interface Observation { + initial: BundleInfo[] + sessionCount: number + steps: Array<{ + name: string + install: ChangeResult + savedVersion: string + installed: BundleInfo + providerEnabled: boolean + nativeArtifact: boolean + localDependency: string + presets: Array<{ id: string; broken?: string }> + unavailable: ChangeResult + unchangedAfterUnavailable: boolean + off: ChangeResult + disabled: BundleInfo + on: ChangeResult + enabled: BundleInfo + removed: ChangeResult + absent: BundleInfo + }> +} + +/** + * Read the tail of the newest plugin-manager pnpm log, for a failure the child cannot report itself. + * @param profile - isolated profile directory. + * @returns The last log lines, or a marker when no run logged anything. + */ +function pnpmLogTail(profile: string): string { + const logs = join(profile, '.plugin-manager', 'logs') + const newest = existsSync(logs) + ? readdirSync(logs, { withFileTypes: true }).filter(entry => entry.isDirectory()) + .map(entry => join(logs, entry.name, 'pnpm.log')) + .filter(path => existsSync(path)) + .sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs)[0] + : undefined + if (newest === undefined) return '--- plugin-manager log: none ---' + return `--- ${newest} ---\n${readFileSync(newest, 'utf8').split('\n').slice(-40).join('\n')}` +} + +it.skipIf(!built)('installs the exported registry\'s local closure and switches Official bundles through the generic manager', { + // Real packing of two dependency closures, an isolated product installation, and an isolated plugin installation + // own this budget; no native model request runs. + timeout: 900_000, + retry: 0, +}, async () => { + // The product under test is a real source-independent installation that ships neither provider, so both stay + // available to the Official catalog and only the local registry can supply them. + const fixture = await createPackedInstallation('dsh-official-packed-', []) + const { temporary, installation, home, profile, environment } = fixture + + // The registry packs and serves the catalog's own local dependency closure; no checkout link or file: override + // stands in for a package the registry should answer. + const entries = generateOfficialBundleCatalog(root).map(entry => entry.packageName) + const workspace = readWorkspacePackages(root) + verifyBuiltBundles({ entries, packages: workspace }) + const selection = selectBundleDependencies({ entries, packages: workspace, platform: hostPlatform() }) + const runDirectory = join(temporary, 'registry') + const artifacts = await packBundleArtifacts({ + root, destination: runDirectory, members: selection.members, release: readReleaseMembers(root), + }) + const version = artifacts.find(artifact => artifact.name === packages[0])?.version + if (version === undefined) throw new Error('the local registry packed no claude-code provider') + for (const name of packages) expect(artifacts.find(artifact => artifact.name === name), name).toMatchObject({ version }) + await writeRegistryManifest(join(runDirectory, REGISTRY_MANIFEST_FILE), createRegistryManifest({ + buildId: 'packed-e2e', + source: sourceEvidence(root, getDshRuntimeVersion()), + entries, + workspaceNames: selection.workspaceNames, + artifacts, + })) + const registry = await startBundleRegistry({ + directory: runDirectory, artifacts, workspaceNames: selection.workspaceNames, + }) + onTestFinished(() => registry.close()) + + await writeFile(join(profile, 'package.json'), JSON.stringify({ name: 'packed-official-profile', private: true, dependencies: {}, + dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'] } }, + })) + // Only the settings a real profile gets. No override, ignore list, or scoped .npmrc may route a package around the + // registry, so every dependency of the packed bundles is answered by this run or by its fixed upstream redirect. + await writeFile(join(profile, 'pnpm-workspace.yaml'), 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n') + const userConfig = join(temporary, 'empty.npmrc') + await writeFile(userConfig, '') + environment.npm_config_userconfig = userConfig + const observer = join(temporary, 'official-installer-observer.mjs') + await writeFile(observer, await readFile(new URL('./fixtures/official-installer-observer.mjs', import.meta.url), 'utf8')) + const patch = join(temporary, 'official-installer.patch.yml') + await writeFile(patch, yaml.dump([{ insert: [{ id: 'official-installer-observer', name: observer, + config: { + registry: registry.url, + profile, + packages, + localDependency: '@deepseek-ai/dsh-brand', + nativePackages: { + // The Agent SDK publishes a musl variant of its Linux artifact, which is what pnpm installs there. + '@deepseek-ai/dsh-subagent-claude-code': `@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}${hostPlatform().libc?.[0] === 'musl' ? '-musl' : ''}`, + '@deepseek-ai/dsh-subagent-codex': `@openai/codex-${process.platform}-${process.arch}`, + }, + }, + }] }])) + const child = execa(process.execPath, [join(installation, 'node_modules/@deepseek-ai/dsh/lib/bin.js'), + 'web', '--patch', patch, '--no-open', '--port', '0'], { + cwd: temporary, env: environment, extendEnv: false, reject: false, timeout: 240_000, + }) + if (child.stdout === null) throw new Error('installed Web process has no stdout') + const lines = createInterface({ input: child.stdout }) + // A failure in the body must stay the reported one: teardown asserts only when the body + // passed, so a Web process that does not exit cannot replace the real error. + let failure: unknown + try { + const observed = await new Promise((resolveObservation, reject) => { + lines.on('line', (line) => { + if (!line.startsWith('PACKED_OFFICIAL_RESULT=')) return + const report = JSON.parse(line.slice('PACKED_OFFICIAL_RESULT='.length)) as { value?: Observation; error?: string } + if (report.error !== undefined) reject(new Error(report.error)) + else if (report.value !== undefined) resolveObservation(report.value) + }) + void child.then((result) => { reject(new Error(`Web process exited before observation:\n${result.stdout}\n${result.stderr}`)) }) + }) + for (const name of packages) expect(observed.initial.find(entry => entry.name === name)).toMatchObject({ + official: true, availability: 'missing', installed: false, enabled: false, installTarget: { spec: `${name}@${version}`, version }, + }) + expect(observed.steps).toHaveLength(packages.length) + for (const step of observed.steps) { + expect(step.install, JSON.stringify(step.install)).toMatchObject({ application: 'applied', enabled: true, version }) + expect(step.savedVersion).toBe(version) + expect(step.installed).toMatchObject({ official: true, availability: 'profile', enabled: true, installed: true, version }) + expect(step.installed.rows.filter(row => row.preset !== undefined).map(row => row.preset).sort()) + .toEqual(['preset-cordis', 'preset-ptc', 'preset-standard']) + expect(step.providerEnabled).toBe(true) + expect(step.nativeArtifact).toBe(true) + // A transitive repository package the closure needs can only have come from this run's artifacts. + expect(step.localDependency).toBe(version) + expect(step.presets.every(preset => preset.broken === undefined)).toBe(true) + expect(step.unavailable).toMatchObject({ application: 'failed', stage: 'install' }) + expect(step.unchangedAfterUnavailable).toBe(true) + expect(step.off).toMatchObject({ application: 'applied' }) + expect(step.disabled).toMatchObject({ official: true, installed: true, enabled: false }) + expect(step.on).toMatchObject({ application: 'applied' }) + expect(step.enabled).toMatchObject({ official: true, installed: true, enabled: true }) + expect(step.removed, JSON.stringify(step.removed)).toMatchObject({ application: 'applied' }) + expect(step.absent).toMatchObject({ official: true, availability: 'missing', enabled: false, installed: false }) + } + expect(observed.sessionCount).toBe(0) + expect(globSync('sessions/**/*.jsonl*', { cwd: home })).toEqual([]) + await assertInstalledTree(profile) + // The completed run records every package it served, so a failure can be matched to the exact checkout. + const record = JSON.parse(await readFile(join(runDirectory, REGISTRY_MANIFEST_FILE), 'utf8')) as { buildId: string; packages: unknown[] } + expect(record.buildId).toBe('packed-e2e') + expect(record.packages).toHaveLength(selection.members.length) + expect(record.packages).toEqual(expect.arrayContaining([expect.objectContaining({ name: packages[0], version })])) + } catch (error) { + // The manager's own pnpm output names a stalled or refused operation, which the child's stdout cannot show. + failure = error instanceof Error ? new Error(`${error.message}\n\n${pnpmLogTail(profile)}`) : error + } finally { + lines.close() + child.kill('SIGTERM') + const result = await child + if (failure === undefined) { + // The installed Web process stops on SIGTERM within its own budget; a timed-out child is a teardown defect. + expect(result.timedOut, `${result.stdout}\n${result.stderr}`).toBe(false) + expect(result.exitCode === 0 || result.signal === 'SIGTERM', result.stderr).toBe(true) + } + } + if (failure !== undefined) throw failure +}) diff --git a/apps/cli/tests/profiles/packed-installation.ts b/apps/cli/tests/profiles/packed-installation.ts new file mode 100644 index 0000000000..6cfcd2ebb0 --- /dev/null +++ b/apps/cli/tests/profiles/packed-installation.ts @@ -0,0 +1,190 @@ +/** Pack and install a source-independent product for idle Web-profile integration tests. */ +import { createRequire } from 'node:module' +import { existsSync, globSync } from 'node:fs' +import { lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, relative, resolve, sep } from 'node:path' +import { fileURLToPath } from 'node:url' +import { execa } from 'execa' +import yaml from 'js-yaml' +import { expect, onTestFinished } from 'vitest' +import { pnpmCommand } from '../../../../scripts/release/process.ts' +import { packedWorkspaceClosure, type WorkspacePackage } from '../../../../packages/sandbox/sandbox-local/tests/packed-workspace-closure.ts' + +const root = fileURLToPath(new URL('../../../..', import.meta.url)) + +/** Capture workspace dependency links without following their targets. */ +async function workspaceLinks(directories: string[]): Promise> { + const links: Record = {} + for (const directory of directories) { + const modules = join(directory, 'node_modules') + links[modules] = existsSync(modules) ? 'directory' : 'absent' + if (!existsSync(modules)) continue + for (const entry of await readdir(modules, { withFileTypes: true })) { + const path = join(modules, entry.name) + const paths = entry.name.startsWith('@') && entry.isDirectory() + ? (await readdir(path)).map(name => join(path, name)) : [path] + for (const dependency of paths) { + const stat = await lstat(dependency) + links[dependency] = stat.isSymbolicLink() ? await readlink(dependency) : stat.isDirectory() ? 'directory' : 'file' + } + } + } + return links +} + +/** Reject workspace links throughout an installed dependency tree. + * @param directory - Installation or profile whose links must remain inside its own tree. + */ +export async function assertInstalledTree(directory: string): Promise { + const installedRoot = await realpath(directory) + const visited = new Set() + const visit = async (path: string): Promise => { + const resolved = await realpath(path) + expect(resolved === installedRoot || resolved.startsWith(installedRoot + sep), `${path} escapes its installation`).toBe(true) + if (visited.has(resolved)) return + visited.add(resolved) + for (const entry of await readdir(path, { withFileTypes: true })) { + const child = join(path, entry.name) + if (entry.isSymbolicLink()) { + // Removed packages can leave dangling .bin shims; those links must also stay inside the profile. + const target = existsSync(child) ? await realpath(child) : resolve(dirname(child), await readlink(child)) + expect(target.startsWith(installedRoot + sep), `${child} escapes its installation`).toBe(true) + } else if (entry.isDirectory()) await visit(child) + } + } + await visit(directory) +} + +/** Actual tarballs and an isolated installed CLI, with an initially empty Web profile. */ +export interface PackedInstallation { + temporary: string + installation: string + home: string + profile: string + archives: string + environment: NodeJS.ProcessEnv + tarballs: Map + members: Map + externalVersions: Record + packageCommand: (args: string[], cwd: string) => Promise<{ stdout: string; stderr: string }> +} + +/** Build the packed product closure and additional packages without changing workspace dependency links. + * Native confinement and Session-writing operations are outside this idle-profile fixture. + * @param temporaryPrefix - Unique temporary-directory prefix identifying the owning test. + * @param extraPackages - Additional workspace packages to pack without installing in the product. + * @returns Isolated installation paths, tarballs, environment, and checked pnpm invocation. + */ +export async function createPackedInstallation(temporaryPrefix: string, extraPackages: readonly string[]): Promise { + const temporary = await realpath(await mkdtemp(join(tmpdir(), temporaryPrefix))) + onTestFinished(() => rm(temporary, { recursive: true, force: true })) + const installation = join(temporary, 'installation') + const home = join(temporary, 'home') + const profile = join(home, 'profiles', 'web') + const archives = join(temporary, 'archives') + await mkdir(profile, { recursive: true }) + await mkdir(archives) + const environment: NodeJS.ProcessEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => ( + !/KEY|SECRET|TOKEN|PASSWORD/.test(key) && key !== 'NODE_OPTIONS' && key !== 'NODE_PATH' + ))) + Object.assign(environment, { + DSH_HOME: home, + DSH_AGENTS_HOME: join(temporary, 'agents'), + DSH_TELEMETRY_DISABLED: '1', + CI: 'true', + }) + const [pnpm, ...prefix] = pnpmCommand() + const packageCommand = async (args: string[], cwd: string) => { + const result = await execa(pnpm, [...prefix, ...args], { + cwd, env: environment, extendEnv: false, timeout: 180_000, reject: false, + }) + expect(result.timedOut, `${result.stdout}\n${result.stderr}`).toBe(false) + expect(result.signal, result.stderr).toBeUndefined() + expect(result.exitCode, `${result.stdout}\n${result.stderr}`).toBe(0) + return result + } + const packages = new Map() + for (const path of globSync([ + 'apps/*/package.json', 'packages/*/*/package.json', 'vendor/*/package.json', 'native/system/packages/*/package.json', + ], { cwd: root })) { + const manifest = JSON.parse(await readFile(join(root, path), 'utf8')) as Record + if (typeof manifest.name !== 'string') throw new Error(`${path} has no package name`) + packages.set(manifest.name, { name: manifest.name, manifest, directory: dirname(join(root, path)) }) + } + const sourceDirectories = [root, ...[...packages.values()].map(member => member.directory)] + const initialLinks = await workspaceLinks(sourceDirectories) + onTestFinished(async () => { expect(await workspaceLinks(sourceDirectories)).toEqual(initialLinks) }) + const closure = packedWorkspaceClosure('@deepseek-ai/dsh', packages) + // These idle-profile tests create no Session or confined process. The native + // platform packages load lazily for those operations and have separate smokes. + const supportsIdleProfile = (member: WorkspacePackage) => ( + !member.name.startsWith('@deepseek-ai/node-addon-system-') + && (!Array.isArray(member.manifest.os) || member.manifest.os.includes(process.platform)) + && (!Array.isArray(member.manifest.cpu) || member.manifest.cpu.includes(process.arch)) + ) + const runtimeMembers = closure.filter(supportsIdleProfile) + const members = new Map(runtimeMembers.map(member => [member.name, member])) + for (const name of extraPackages) { + for (const member of packedWorkspaceClosure(name, packages).filter(supportsIdleProfile)) members.set(member.name, member) + } + // Packing reads package-owned files; installation never runs in the workspace. + await packageCommand([ + 'pack', '--recursive', '--workspace-concurrency=4', + ...[...members.keys()].map(name => `--filter=${name}`), '--pack-destination', archives, + ], root) + const tarballs = new Map([...members.values()].map((member) => { + if (typeof member.manifest.version !== 'string') throw new Error(`${member.name} has no version`) + const name = member.name.replaceAll('/', '-').replace('@', '') + const archive = join(archives, `${name}-${member.manifest.version}.tgz`) + expect(existsSync(archive), `missing packed payload for ${member.name}`).toBe(true) + return [member.name, archive] + })) + const workspaceSettings = yaml.load(await readFile(join(root, 'pnpm-workspace.yaml'), 'utf8')) as { + allowBuilds: Record + patchedDependencies: Record + minimumReleaseAgeExclude: string[] + overrides: Record + } + const externalVersions: Record = {} + for (const member of members.values()) { + const require = createRequire(join(member.directory, 'package.json')) + for (const section of ['dependencies', 'optionalDependencies', 'peerDependencies']) { + const dependencies = member.manifest[section] as Record | undefined + for (const [name, range] of Object.entries(dependencies ?? {})) { + if (range.startsWith('workspace:')) continue + const manifest = require.resolve.paths(name)?.map(path => join(path, name, 'package.json')).find(path => existsSync(path)) + if (manifest === undefined) continue + const installed = JSON.parse(await readFile(manifest, 'utf8')) as { version: string } + externalVersions[`${name}@${range}`] = installed.version + } + } + } + await mkdir(installation) + for (const path of Object.values(workspaceSettings.patchedDependencies)) { + await mkdir(dirname(join(installation, path)), { recursive: true }) + await writeFile(join(installation, path), await readFile(join(root, path))) + } + await writeFile(join(installation, 'package.json'), JSON.stringify({ + name: 'packed-product-installation', private: true, + dependencies: Object.fromEntries(runtimeMembers.map(member => [member.name, `file:${tarballs.get(member.name)!}`])), + })) + await writeFile(join(installation, 'pnpm-workspace.yaml'), yaml.dump({ + packages: ['.'], nodeLinker: 'hoisted', autoInstallPeers: false, allowUnusedPatches: true, + ignoredOptionalDependencies: closure.filter(member => !members.has(member.name)).map(member => member.name), + allowBuilds: { + ...workspaceSettings.allowBuilds, + [`@deepseek-ai/dsh-subprocess-local@file:${relative(installation, tarballs.get('@deepseek-ai/dsh-subprocess-local')!).split(sep).join('/')}`]: true, + }, + patchedDependencies: workspaceSettings.patchedDependencies, + minimumReleaseAgeExclude: workspaceSettings.minimumReleaseAgeExclude, + overrides: { + ...workspaceSettings.overrides, + ...externalVersions, + ...Object.fromEntries(runtimeMembers.map(member => [member.name, `file:${tarballs.get(member.name)!}`])), + }, + })) + await packageCommand(['install', '--prefer-offline', '--no-frozen-lockfile'], installation) + await assertInstalledTree(installation) + return { temporary, installation, home, profile, archives, environment, tarballs, members, externalVersions, packageCommand } +} diff --git a/apps/cli/tests/profiles/sdk/dynamic-tool-cache.e2e.ts b/apps/cli/tests/profiles/sdk/dynamic-tool-cache.e2e.ts index e90f5b4b54..3bdc2e1cc5 100644 --- a/apps/cli/tests/profiles/sdk/dynamic-tool-cache.e2e.ts +++ b/apps/cli/tests/profiles/sdk/dynamic-tool-cache.e2e.ts @@ -40,6 +40,35 @@ function totalInput(usage: TokenUsage): number { return usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0) } +/** Tool calls the fixture recorded, in execution order. */ +async function recordedCalls(path: string): Promise { + return (await readFile(path, 'utf8')).trim().split('\n') +} + +/** + * Assert the model called `earlier` and then `later`. The model owns its plan beyond the required + * calls, so extra add/remove cycles it chooses to make are legitimate and are not counted here. + */ +function expectCalledBefore(calls: string[], earlier: string, later: string): void { + const earlierIndex = calls.indexOf(earlier) + expect(earlierIndex, `${earlier} was never called`).toBeGreaterThanOrEqual(0) + const laterIndex = calls.indexOf(later, earlierIndex + 1) + expect(laterIndex, `${later} was never called after ${earlier}`).toBeGreaterThan(earlierIndex) +} + +/** + * Net tool-registry change between two consecutive requests, matching how the agent loop derives + * a `developer/message` from the difference between the current and previous request headers. + */ +function registryChanges(previous: ObservedRequest, request: ObservedRequest): { type: string; toolName: string }[] { + const before = new Set(previous.body.tools?.map(tool => tool.name)) + const after = new Set(request.body.tools?.map(tool => tool.name)) + return [ + ...[...after].filter(name => !before.has(name)).map(toolName => ({ type: 'tool-addition', toolName })), + ...[...before].filter(name => !after.has(name)).map(toolName => ({ type: 'tool-removal', toolName })), + ] +} + describe.skipIf(!process.env.DEEPSEEK_API_KEY)('SDK native tool updates with real DeepSeek', () => { it.each([false, true])('keeps the preceding conversation cached after a tool addition (prompt update: %s)', { retry: 0 }, async (updatePrompt) => { const root = await mkdtemp(join(tmpdir(), 'dsh-sdk-tool-cache-')) @@ -80,9 +109,10 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('SDK native tool updates with rea await run(`Keep these reference records. Do not call tools. Reply READY.\n${records}`) await run('Keep the reference records. Do not call tools. Reply READY.') expect(await run(`Use cache_tool_control with action add. After it returns, call ${sampleTool} exactly once and report the generated sample label. This is synthetic example data for this run. Call these separately in order.`)).toContain(sampleLabel) - expect(await readFile(callsPath, 'utf8')).toBe('add\nsample\n') - expect(await run('Use cache_tool_control with action remove. After it returns, reply REMOVED and do not call the sample tool.')).toContain('REMOVED') - expect(await readFile(callsPath, 'utf8')).toBe('add\nsample\nremove\n') + expectCalledBefore(await recordedCalls(callsPath), 'add', 'sample') + expect(await run('Use cache_tool_control with action remove. Whether it removes the sample tool or reports that it is not registered, reply REMOVED and do not call the sample tool.')).toContain('REMOVED') + const calls = await recordedCalls(callsPath) + expectCalledBefore(calls, 'sample', 'remove') const requests = (await readFile(evidencePath, 'utf8')).trim().split('\n').map(line => JSON.parse(line) as ObservedRequest) expect(events.filter(event => event.type === 'assistant/attempt')).toHaveLength(0) @@ -113,9 +143,15 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('SDK native tool updates with rea expect(added.body.messages.flatMap(message => message.content)).toContainEqual({ type: 'tool_addition', tool: { type: 'tool_reference', name: sampleTool } }) expect(removed.body.tools?.map(tool => tool.name)).toEqual(['cache_tool_control']) expect(removed.body.messages.flatMap(message => message.content).some(block => block.type === 'tool_addition')).toBe(false) - expect(events.filter(event => event.type === 'developer/message').map(event => event.data.message.content)).toEqual([ - [{ type: 'tool-addition', toolName: sampleTool }], - [{ type: 'tool-removal', toolName: sampleTool }], - ]) + // Transcript registry history is derived from the tool lists the provider saw, not from call + // counts: a model-chosen extra add/remove cycle changes both sides or neither. + const expectedRegistryHistory = requests.slice(1).flatMap((request, index) => { + const previous = requests[index] + if (previous === undefined) throw new Error('Missing preceding request') + const content = registryChanges(previous, request) + return content.length > 0 ? [content] : [] + }) + expect(events.filter(event => event.type === 'developer/message').map(event => event.data.message.content)) + .toEqual(expectedRegistryHistory) }) }) diff --git a/apps/cli/tests/profiles/sdk/fixtures/subagent-dsh-sdk-release-on-job-output.ts b/apps/cli/tests/profiles/sdk/fixtures/subagent-dsh-sdk-release-on-job-output.ts deleted file mode 100644 index 554f62dcb1..0000000000 --- a/apps/cli/tests/profiles/sdk/fixtures/subagent-dsh-sdk-release-on-job-output.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** Release the gated background SDK child only after job_output starts waiting. */ - -import { writeFileSync } from 'node:fs' -import type { Context } from '@deepseek-ai/cordis' - -export const name = 'subagent-dsh-sdk-release-on-job-output' -export const inject = ['tools'] - -/** - * Register the test-only execution-order barrier. - * @param ctx - parent runtime context carrying the tool execution waterfall. - */ -export function apply(ctx: Context): void { - ctx.on('tools/execute', async (exec, next) => { - const delegated = next() - if (exec.name === 'job_output') { - writeFileSync('.dsh-sdk-background-release', 'release\n') - } - return delegated - }, { prepend: true }) -} diff --git a/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts b/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts index 56dec2af4b..64c1c20ab3 100644 --- a/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts +++ b/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts @@ -303,6 +303,7 @@ describe('Python SDK dsh profile keyless smoke', () => { expect(tools.map(tool => tool.name)).toEqual([ process.platform === 'win32' ? 'pwsh' : 'bash', ...(editorEnabled ? ['str_replace_editor'] : []), + 'working_directory', ]) expect(modelRequests).toHaveLength(editorEnabled ? 3 : 1) if (editorEnabled) { diff --git a/apps/cli/tests/profiles/web/tests/runtime-roster-isolation.spec.ts b/apps/cli/tests/profiles/web/tests/runtime-roster-isolation.spec.ts new file mode 100644 index 0000000000..06f0c67f86 --- /dev/null +++ b/apps/cli/tests/profiles/web/tests/runtime-roster-isolation.spec.ts @@ -0,0 +1,38 @@ +/** Runtime evidence keeps retained experimental names visible through installed aliases. */ + +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { expect, it, onTestFinished } from 'vitest' +import { EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS } from '../../../../../../scripts/experimental-package-policy.ts' +import { experimentalRuntimeReferences } from './runtime-roster-isolation.ts' +import type { RuntimeRoster } from './runtime-roster.ts' + +it.each(Object.values(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS))('finds retained experimental package %s in real roster fields', (name) => { + const root = mkdtempSync(join(tmpdir(), 'dsh-runtime-roster-policy-')) + onTestFinished(() => { rmSync(root, { recursive: true, force: true }) }) + const directory = join(root, 'node_modules/ordinary-alias') + mkdirSync(directory, { recursive: true }) + writeFileSync(join(directory, 'package.json'), JSON.stringify({ name })) + const module = pathToFileURL(join(directory, 'index.js')).href + const subpath = `${name}/client` + const external = `${name}/external` + const injected = `${name}/injected` + const batched = `${name}/batch` + const roster: RuntimeRoster = { + entries: [{ id: 'entry', name, state: 1 }], + plugins: [{ name: 'plugin', owner: subpath, state: 1, modules: [module] }], + modules: [module], + client: { + rev: 'fixture', + entries: [{ id: 'ordinary-client', url: '/client.js', rev: 'fixture', inject: [injected], external: [external] }], + batches: [{ phase: 'application', url: '/batch.js', rev: 'fixture', entries: [batched] }], + }, + } + expect(experimentalRuntimeReferences(roster)).toEqual([name, subpath, module, external, injected, batched].sort()) + writeFileSync(join(directory, 'package.json'), JSON.stringify({ name: `${name}-unrelated` })) + expect(experimentalRuntimeReferences({ + entries: [], plugins: [], modules: [module], client: { rev: 'fixture', entries: [], batches: [] }, + })).toEqual([]) +}) diff --git a/apps/cli/tests/profiles/web/tests/runtime-roster-isolation.ts b/apps/cli/tests/profiles/web/tests/runtime-roster-isolation.ts new file mode 100644 index 0000000000..a79e358c2b --- /dev/null +++ b/apps/cli/tests/profiles/web/tests/runtime-roster-isolation.ts @@ -0,0 +1,24 @@ +/** Experimental identity checks over independently observed built Web runtime records. */ + +import { fileURLToPath } from 'node:url' +import { hasExperimentalPackageReference, isExperimentalPackageName } from '../../../../../../scripts/experimental-package-policy.ts' +import { modulePackage } from './runtime-roster.ts' +import type { RuntimeRoster } from './runtime-roster.ts' + +/** + * Locate experimental package identities and experimental source/artifact paths in runtime evidence. + * @param roster - independently collected Web runtime records. + * @returns diagnostic evidence for every forbidden package reference. + */ +export function experimentalRuntimeReferences(roster: RuntimeRoster): string[] { + const references = new Set([ + ...roster.entries.map(entry => entry.name), + ...roster.plugins.flatMap(plugin => [plugin.owner ?? '', ...plugin.modules]), + ...roster.modules, + ...roster.client.entries.flatMap(entry => [entry.id, ...entry.inject ?? [], ...entry.external ?? []]), + ...roster.client.batches.flatMap(batch => batch.entries), + ]) + return [...references].filter(reference => hasExperimentalPackageReference(reference) + || (reference.startsWith('file:') && (fileURLToPath(reference).replaceAll('\\', '/').includes('/packages/experimental/') + || isExperimentalPackageName(modulePackage(reference) ?? '')))).sort() +} diff --git a/apps/cli/tests/profiles/web/tests/runtime-roster.ts b/apps/cli/tests/profiles/web/tests/runtime-roster.ts index f6156fd1da..4e48203d03 100644 --- a/apps/cli/tests/profiles/web/tests/runtime-roster.ts +++ b/apps/cli/tests/profiles/web/tests/runtime-roster.ts @@ -75,21 +75,3 @@ export function runtimeRoster(ctx: Context): RuntimeRoster { client: ctx.clientModules.graph(), } } - -/** - * Locate experimental package identities and experimental source/artifact paths in runtime evidence. - * @param roster - independently collected Web runtime records. - * @returns diagnostic evidence for every forbidden package reference. - */ -export function experimentalRuntimeReferences(roster: RuntimeRoster): string[] { - const references = new Set([ - ...roster.entries.map(entry => entry.name), - ...roster.plugins.flatMap(plugin => [plugin.owner ?? '', ...plugin.modules]), - ...roster.modules, - ...roster.client.entries.flatMap(entry => [entry.id, ...entry.inject ?? [], ...entry.external ?? []]), - ...roster.client.batches.flatMap(batch => batch.entries), - ]) - return [...references].filter(reference => reference.includes('@deepseek-ai/dsh-experimental-') - || (reference.startsWith('file:') && (fileURLToPath(reference).replaceAll('\\', '/').includes('/packages/experimental/') - || modulePackage(reference)?.startsWith('@deepseek-ai/dsh-experimental-')))).sort() -} diff --git a/apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts b/apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts index ef0f10cc1d..7563a323aa 100644 --- a/apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts +++ b/apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts @@ -3,7 +3,8 @@ import { FiberState } from '@deepseek-ai/cordis' import type { WebBootGraph } from '@deepseek-ai/dsh-client-modules/client' import { expect, it } from 'vitest' -import { experimentalRuntimeReferences, modulePackage } from './runtime-roster.ts' +import { modulePackage } from './runtime-roster.ts' +import { experimentalRuntimeReferences } from './runtime-roster-isolation.ts' import { withDefaultWeb, webGet } from './default-web-process.ts' const experimentalName = '@deepseek-ai/dsh-experimental-client-ui-agent-team' diff --git a/apps/cli/tests/profiles/webhook-packed.e2e.ts b/apps/cli/tests/profiles/webhook-packed.e2e.ts new file mode 100644 index 0000000000..3844ceb22b --- /dev/null +++ b/apps/cli/tests/profiles/webhook-packed.e2e.ts @@ -0,0 +1,104 @@ +/** Exercise the published webhook example from an isolated installed Web profile. */ +import { createHmac } from 'node:crypto' +import { existsSync, globSync } from 'node:fs' +import { readFile, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { execa } from 'execa' +import yaml from 'js-yaml' +import { expect, it } from 'vitest' +import { assertInstalledTree, createPackedInstallation } from './packed-installation.ts' + +const root = fileURLToPath(new URL('../../../..', import.meta.url)) +const adapter = '@deepseek-ai/dsh-webhook-github' +const runtime = '@deepseek-ai/dsh-webhook' +const built = existsSync(join(root, 'apps/cli/lib/bin.js')) + && existsSync(join(root, 'packages/experimental/webhook-github/lib/index.js')) +const secret = 'packed-webhook-test-secret' + +it.skipIf(!built)('loads the packed GitHub example with its explicitly installed runtime peer', { + // Packing and installing the complete product closure owns this budget; no model request runs. + timeout: 360_000, + retry: 0, +}, async () => { + const { temporary, installation, home, profile, environment, tarballs, members, externalVersions, packageCommand } + = await createPackedInstallation('dsh-webhook-packed-', [runtime, adapter]) + Object.assign(environment, { DSH_GITHUB_WEBHOOK_SECRET: secret, DSH_GITHUB_WEBHOOK_PORT: '0' }) + expect(existsSync(join(installation, 'node_modules', runtime))).toBe(false) + expect(existsSync(join(installation, 'node_modules', adapter))).toBe(false) + await writeFile(join(profile, 'package.json'), JSON.stringify({ + name: 'packed-webhook-profile', private: true, dependencies: {}, + dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'] } }, + })) + const profileDependencies = new Set([runtime, adapter]) + for (const name of profileDependencies) { + const member = members.get(name) + if (member === undefined) throw new Error(`missing packed dependency ${name}`) + for (const [dependency, range] of Object.entries(member.manifest.dependencies as Record | undefined ?? {})) { + if (range.startsWith('workspace:')) profileDependencies.add(dependency) + } + } + await writeFile(join(profile, 'pnpm-workspace.yaml'), yaml.dump({ + packages: ['.'], nodeLinker: 'hoisted', autoInstallPeers: false, + overrides: { + ...externalVersions, + ...Object.fromEntries([...profileDependencies].map(name => [name, `file:${tarballs.get(name)!}`])), + }, + })) + const adapterArchive = tarballs.get(adapter) + const runtimeArchive = tarballs.get(runtime) + if (adapterArchive === undefined || runtimeArchive === undefined) throw new Error('webhook tarballs were not packed') + await packageCommand(['add', runtimeArchive, adapterArchive, '--prefer-offline', '--ignore-scripts'], profile) + const manifest = JSON.parse(await readFile(join(profile, 'package.json'), 'utf8')) as { dependencies: Record } + expect(Object.keys(manifest.dependencies).sort()).toEqual([runtime, adapter].sort()) + await assertInstalledTree(profile) + + const example = join(profile, 'node_modules', adapter, 'examples/github-review/cordis.yml') + expect(existsSync(example)).toBe(true) + expect(existsSync(join(dirname(example), 'github-ready-review-rule.mjs'))).toBe(true) + const observer = join(temporary, 'observe-ingress.mjs') + await writeFile(observer, `export const name = 'observe-packed-ingress' +export const inject = ['webServer', 'webhookRuntime'] +export function apply(ctx) { + process.stdout.write('PACKED_WEBHOOK_READY=http://127.0.0.1:' + ctx.webServer.port + '\\n') +} +`) + const patch = join(temporary, 'test.patch.yml') + await writeFile(patch, yaml.dump([ + { id: 'session-telemetry-otel', disabled: true }, + { id: 'github-webhook-ingress', insert: [{ id: 'observe-packed-ingress', name: observer }] }, + ])) + const child = execa(process.execPath, [ + join(installation, 'node_modules/@deepseek-ai/dsh/lib/bin.js'), 'web', '--patch', example, '--patch', patch, '--no-open', '--port', '0', + ], { cwd: temporary, env: environment, extendEnv: false, reject: false, timeout: 90_000 }) + try { + const ready = new Promise((resolveReady, rejectReady) => { + let output = '' + child.stdout?.on('data', (chunk: Buffer) => { + output += chunk.toString() + const origin = /PACKED_WEBHOOK_READY=(http:\/\/127\.0\.0\.1:\d+)/.exec(output)?.[1] + if (origin !== undefined && output.includes('dsh web: ')) resolveReady(origin) + }) + void child.then((result) => { + rejectReady(new Error(`Web profile exited before readiness:\n${result.stdout}\n${result.stderr}`)) + }) + }) + const origin = await ready + expect((await fetch(`${origin}/api`, { signal: AbortSignal.timeout(10_000) })).status).toBe(404) + const body = JSON.stringify({ zen: 'A signed ping does not start an Agent.' }) + const response = await fetch(`${origin}/github`, { + method: 'POST', body, signal: AbortSignal.timeout(10_000), + headers: { + 'content-type': 'application/json', 'x-github-event': 'ping', 'x-github-delivery': 'packed-ping', + 'x-hub-signature-256': `sha256=${createHmac('sha256', secret).update(body).digest('hex')}`, + }, + }) + expect(response.status, await response.text()).toBe(202) + } finally { + child.kill('SIGTERM') + const result = await child + expect(result.timedOut, `${result.stdout}\n${result.stderr}`).toBe(false) + expect(result.exitCode === 0 || result.signal === 'SIGTERM', result.stderr).toBe(true) + } + expect(globSync('sessions/**/*.jsonl*', { cwd: home })).toEqual([]) +}) diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index 079aa6dbce..6fa2861ce0 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -13,15 +13,17 @@ import { loadProfile, PluginPackages, type Profile, + type ProfilePatch, } from '@deepseek-ai/dsh-app-boot' import { provideCmdline } from '@deepseek-ai/dsh-cmdline' import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session' import type { Agent } from '@deepseek-ai/dsh-agent' import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include' -import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' +import { afterAll, beforeAll, describe, expect, it, onTestFinished, vi } from 'vitest' import { dump, load } from 'js-yaml' import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' import { bundlePatchPaths, composeEntries } from '@deepseek-ai/dsh-app-boot' +import { createPluginProfile } from '../../desktop/src/project-manager.ts' /** Profile entry ids whose volatile fields these scenarios edit through Settings. */ const SETTINGS_NAMESPACE = 'agent-preset-registry' const SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE = 'subagent-model-selection-settings' @@ -61,9 +63,10 @@ const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell */ async function bootWeb( profileHome: string, - extra: PatchOptions[] = [], + extra: ProfilePatch[] = [], profilePackages: readonly string[] = [], profileBundles?: readonly string[], + surface: 'web' | 'desktop' | 'spec' = 'spec', ): Promise { const storageRoot = join(profileHome, 'storages') const overrides: PatchOptions[] = [ @@ -90,10 +93,12 @@ async function bootWeb( // agent's capabilities, which is all this file asserts. { id: 'web-runtime', disabled: true }, { id: 'session-telemetry-otel', disabled: true }, + { id: 'desktop-product-telemetry', disabled: true }, + { id: 'product-analytics', disabled: true }, // A deployment-level skill on the host registry's GLOBAL layer — the same // registration shape a repository plugin's skill root uses. The layered // skills test below proves it reaches preset-composed agents. - { id: 'skill-badge', disabled: false }, + { insert: [{ id: 'skill-badge', name: import.meta.resolve('@deepseek-ai/dsh-skill-badge') }] }, { id: 'modules', disabled: true }, // The physical Connection row owns the disabled HTTP server. bootWeb // supplies only its in-process registries so Host services still prove @@ -120,9 +125,10 @@ async function bootWeb( ...extra, ] const home = profileHome - const profileDir = join(home, 'profiles', 'spec') + const profileDir = join(home, 'profiles', surface) await mkdir(profileDir, { recursive: true }) - if (profileBundles === undefined) initProfile(profileDir, ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app']) + if (surface === 'desktop') createPluginProfile(profileDir) + else if (profileBundles === undefined) initProfile(profileDir, ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app']) // Product Bundles are installed into the Profile, not the dsh app. Model // pnpm's package link for only the selected products; their own production // dependencies resolve from the linked workspace packages, while shared @@ -134,7 +140,7 @@ async function bootWeb( await symlink(packageDir, link, 'junction') } let profile: Profile = { skippedBundles: [], - name: 'spec', + name: surface, dir: profileDir, layers: [], patchPath: join(profileDir, 'cordis.patch.yml'), @@ -150,7 +156,7 @@ async function bootWeb( dependencies: Object.fromEntries(profileBundles.map(name => [name, 'workspace:*'])), dsh: { profile: { bundles: profileBundles } }, }, null, 2) + '\n') - profile = loadProfile('dsh-test', 'spec', INSTALL_ANCHOR, home, { userLayer: false }) + profile = loadProfile('dsh-test', surface, INSTALL_ANCHOR, home, { userLayer: false }) bundlePatches = profile.layers.flatMap(layer => layer.patches) } // Deployment defaults live in a bundle beneath the profile patch, so Settings writes are not shadowed by overlays. @@ -166,7 +172,7 @@ async function bootWeb( const rootConfig = join(profileDir, 'cordis.yml') await writeFile(rootConfig, '[]\n') return await boot('dsh-test', rootConfig, [...bundlePatches, ...overrides], async (bootCtx) => { - bootCtx.provide('profileContext', { name: 'spec', dir: profileDir, patchPath: profile.patchPath, + bootCtx.provide('profileContext', { name: surface, dir: profileDir, patchPath: profile.patchPath, installAnchor: INSTALL_ANCHOR, home, cwd: home, startedBundles: profileBundles ?? ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'], overlays: [], telemetryDisabledEnv: '1' }) @@ -192,27 +198,15 @@ function toolParameterNames(ctx: Context, agent: Agent, toolName: string): strin return Object.keys(properties).sort() } -function enablePresetTool(composition: string, id: string): string { - const rows = load(composition, { schema: entryListSchema }) as import('@deepseek-ai/cordis-plugin-loader').EntryOptions[] - const visit = (entries: typeof rows): boolean => entries.some((row) => { - if (row.id === id) { row.disabled = false; return true } - return row.group === true && visit(row.config as typeof rows) - }) - if (!visit(rows)) throw new Error(`missing preset row ${id}`) - return dump(rows, { schema: entryListSchema }) -} - let ctx: Context beforeAll(async () => { ctx = await bootWeb(await mkdtemp(join(tmpdir(), 'dsh-web-presets-'))) }, 120_000) describe('the shipped Web composition', () => { - it('leaves the global tool layer empty', () => { - // Every model-facing tool belongs to a preset, `ask_user_question` - // included: a tool in the global layer reaches EVERY agent regardless of - // which preset composed it, expanding that preset's tool list. - expect(toolNames(ctx)).toEqual([]) + it('exposes only the directory tool in the global layer', () => { + // Directory selection is global; action tools belong to their Agent presets. + expect(toolNames(ctx)).toEqual(['working_directory']) }) it('keeps the token meter and its context-meter projections on the host plane', async () => { @@ -267,7 +261,7 @@ describe('the shipped Web composition', () => { 'schedule_create', 'schedule_delete', 'schedule_list', 'schedule_update', 'send_message', 'skill', 'subagent', 'subagent_fork', 'todo_write', 'update_goal', 'web_fetch', 'web_search', - 'workflow', 'write', + 'workflow', 'working_directory', 'write', ]) expect(ctx.commands.find(handle.agent, 'goal')).toBeDefined() } finally { @@ -319,7 +313,7 @@ describe('the shipped Web composition', () => { expect(assembly.sections).toEqual([ { name: 'deployment:persona-prefix', text: MINIMAL_PROMPT }, ]) - expect(assembly.tools.map(tool => tool.name)).toEqual(['bash']) + expect(assembly.tools.map(tool => tool.name)).toEqual(['bash', 'working_directory']) expect(assembly.tools.find(tool => tool.name === 'bash')?.description).toBe(MINIMAL_BASH_DESCRIPTION) expect(ctx.commands.find(handle.agent, 'goal')).toBeUndefined() // serviceFor reports preset-owned providers; unisolated consumers inherit the host fs. @@ -343,14 +337,14 @@ describe('the shipped Web composition', () => { setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined), }) try { - expect(toolNames(ctx, minimal.agent)).toEqual(['bash']) + expect(toolNames(ctx, minimal.agent)).toEqual(['bash', 'working_directory']) expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10) await minimal.dispose() // Tearing the minimal session down leaves the full one whole. expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10) - expect(toolNames(ctx)).toEqual([]) + expect(toolNames(ctx)).toEqual(['working_directory']) } finally { await full.dispose() } @@ -509,7 +503,7 @@ describe('the shipped Web composition', () => { // stays the preset's choice — minimal mounts no `tool-skill`, so its // tool table has no loader even though the global layer is readable. expect((await ctx.skills.list({ scope: handle.agent })).map(skill => skill.name)).toContain('dsh-badge') - expect(toolNames(ctx, handle.agent)).toEqual(['bash']) + expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'working_directory']) } finally { await handle.dispose() } @@ -549,14 +543,19 @@ describe('product Bundle and user-preset intersection', () => { const standardConfig = composeEntries([webPatches('test')]).find(row => row.id === 'preset-standard')!.config as import('@deepseek-ai/dsh-agent-preset-registry').PresetDefinition const standard = dump(standardConfig.plugins, { schema: entryListSchema }) for (const id of presetIds) { - let composition = standard + const plugins = load(standard, { schema: entryListSchema }) as import('@deepseek-ai/cordis-plugin-loader').EntryOptions[] + // User presets declare their own delegation rows; provider bundles target only the three full shipped presets. if (id === 'products-codex' || id === 'products-both') { - composition = enablePresetTool(composition, 'tool-subagent-codex') + plugins.push({ id: 'custom-tool-subagent-codex', name: '@deepseek-ai/dsh-tool-subagent', config: { + provider: 'codex', toolName: 'subagent_codex', maxDepth: 'provider-managed', + } }) } if (id === 'products-claude' || id === 'products-both') { - composition = enablePresetTool(composition, 'tool-subagent-claude-code') + plugins.push({ id: 'custom-tool-subagent-claude-code', name: '@deepseek-ai/dsh-tool-subagent', config: { + provider: 'claude-code', toolName: 'subagent_claude_code', maxDepth: 'provider-managed', + } }) } - definitions.push({ id: `preset-${id}`, name: '@deepseek-ai/dsh-agent-preset', config: { id, plugins: load(composition, { schema: entryListSchema }) } }) + definitions.push({ id: `preset-${id}`, name: '@deepseek-ai/dsh-agent-preset', config: { id, plugins } }) } const packageDir = (product: Product): string => ( product === 'codex' ? CODEX_PACKAGE_DIR : CLAUDE_CODE_PACKAGE_DIR @@ -611,7 +610,7 @@ describe('product Bundle and user-preset intersection', () => { expect(tools).toEqual(expect.arrayContaining(['job_kill', 'job_list', 'job_output'])) for (const productTool of productTools) { expect(toolParameterNames(productCtx, handle.agent, productTool)).toEqual([ - 'description', 'prompt', 'run_in_background', + 'cwd', 'description', 'prompt', ]) } } finally { @@ -627,6 +626,75 @@ describe('product Bundle and user-preset intersection', () => { }, 120_000) }) +describe.each(['web', 'desktop'] as const)('%s native bundle adoption', (surface) => { + const products = ['codex', 'claude-code'] as const + const bundleNames = products.map(product => `@deepseek-ai/dsh-subagent-${product}`) + + async function bootSelected(extra: ProfilePatch[]): Promise { + const root = await mkdtemp(join(tmpdir(), `dsh-${surface}-native-adoption-`)) + onTestFinished(async () => { await rm(root, { recursive: true, force: true }) }) + return await bootWeb(root, extra, [CODEX_PACKAGE_DIR, CLAUDE_CODE_PACKAGE_DIR], [ + '@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', ...bundleNames, + ], surface) + } + + it('keeps selected provider identities and later tool overrides without duplicating custom preset tools', async () => { + const definition = composeEntries([webPatches('adoption')]).find(row => row.id === 'preset-cordis')! + .config as import('@deepseek-ai/dsh-agent-preset-registry').PresetDefinition + const config = { ...definition, plugins: [...definition.plugins, ...products.map(product => ({ + id: `tool-subagent-${product}`, name: '@deepseek-ai/dsh-tool-subagent', disabled: product === 'codex', + config: { provider: product, toolName: `custom_${product.replaceAll('-', '_')}`, maxDepth: 'provider-managed' }, + }))] } + const providerConfig = { model: 'adoption-model', env: { DSH_NATIVE_ADOPTION: 'preserved' } } + const productCtx = await bootSelected([ + ...products.map(product => ({ id: `subagent-${product}`, config: providerConfig })), + { preset: 'preset-standard', id: 'tool-subagent-codex', disabled: true }, + { preset: 'preset-standard', id: 'tool-subagent-claude-code', config: { + provider: 'claude-code', toolName: 'custom_claude_code', maxDepth: 'provider-managed', + } }, + { id: 'preset-cordis', config }, + ]) + const spawn = vi.spyOn(productCtx.subprocess, 'spawn') + try { + expect(productCtx.subagents.list()).toEqual(expect.arrayContaining([...products])) + for (const product of products) { + expect([...productCtx.loader.entries()].find(entry => entry.options.id === `subagent-${product}`)?.options.config) + .toMatchObject(providerConfig) + } + for (const [preset, expected] of [ + ['standard', ['custom_claude_code']], ['cordis', ['custom_claude_code']], + ['ptc', ['subagent_claude_code', 'subagent_codex']], ['minimal', []], + ] as const) { + const handle = await productCtx.agents.create({ + sessionId: SessionId(`adoption-${surface}-${preset}-${randomUUID()}`), + setup: owner => productCtx.agentPresets.mount(owner, preset).then(() => undefined), + }) + try { + expect(toolNames(productCtx, handle.agent).filter(name => /(?:subagent|custom)_(?:codex|claude_code)/.test(name))) + .toEqual(expected) + } finally { await handle.dispose() } + } + expect(spawn).not.toHaveBeenCalled() + } finally { + spawn.mockRestore() + await productCtx.fiber.dispose() + } + }, 120_000) + + it('reports a redundant manual tool instead of silently replacing the bundle contribution', async () => { + const productCtx = await bootSelected([{ preset: 'preset-standard', insert: [{ + id: 'manual-codex', name: '@deepseek-ai/dsh-tool-subagent', config: { + provider: 'codex', toolName: 'subagent_codex', maxDepth: 'provider-managed', + }, + }] }]) + try { + expect((await productCtx.agentPresets.list()).find(preset => preset.id === 'standard')?.broken) + .toContain('already registered') + expect(productCtx.profileContext.startedBundles).toEqual(expect.arrayContaining(bundleNames)) + } finally { await productCtx.fiber.dispose() } + }, 120_000) +}) + describe('a user preset declared from the shipped cordis rows', () => { /** The JSON text a `cordis_inspect_*` result renders. */ function resultText(result: { content: ReadonlyArray<{ type: string; text?: string }> }): string { @@ -862,9 +930,8 @@ describe('the default preset as a user setting', () => { setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined), }) try { - // `mount()` with no id resolves the effective default. One tool, not - // `standard`'s catalog: the setting decided the composition. - expect(toolNames(ctx, handle.agent)).toEqual(['bash']) + // An omitted preset id resolves the stored default instead of the composed default. + expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'working_directory']) } finally { await handle.dispose() } @@ -910,7 +977,7 @@ describe('a profile patch stored before Developer tools owned preset selection', setup: agentCtx => legacy.agentPresets.mount(agentCtx).then(() => undefined), }) try { - expect(toolNames(legacy, handle.agent)).toEqual(['bash']) + expect(toolNames(legacy, handle.agent)).toEqual(['bash', 'working_directory']) } finally { await handle.dispose() } @@ -927,7 +994,7 @@ describe('a session keeps the preset it was created with', () => { try { // The api-proxy guard reads exactly this: the header records what the // session runs, so naming anything else is a caller error rather than a - // switch. Its history was produced under `minimal`'s single tool. + // switch. expect(handle.agent.session.header.agentPreset).toBe('minimal') } finally { await handle.dispose() diff --git a/apps/desktop-host/package.json b/apps/desktop-host/package.json index 71c49aad18..f2a75bfa51 100644 --- a/apps/desktop-host/package.json +++ b/apps/desktop-host/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-desktop-host", "description": "Private Node-mode host process for the Electron desktop application", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "private": true, "license": "MIT", "type": "module", diff --git a/apps/desktop-host/src/index.ts b/apps/desktop-host/src/index.ts index 4a47a92ce8..dd8f57c54c 100644 --- a/apps/desktop-host/src/index.ts +++ b/apps/desktop-host/src/index.ts @@ -27,7 +27,7 @@ async function main(): Promise { profile: 'desktop', resolvedProfile: { profile, installAnchor }, patchFiles: [], - args: ['--no-open', '--port', '19387'], + args: ['--no-open', '--port', '0'], ...(process.argv[5] === undefined ? {} : { packageManager: { command: process.execPath, diff --git a/apps/desktop-host/src/office-engine.ts b/apps/desktop-host/src/office-engine.ts index d8f0bd1d5f..f58019d116 100644 --- a/apps/desktop-host/src/office-engine.ts +++ b/apps/desktop-host/src/office-engine.ts @@ -29,7 +29,11 @@ export function installOfficeEngineResolution(runtimeDir: string): ModuleHooks | return registerHooks({ resolve(specifier, context, nextResolve) { const resolved = nextResolve(specifier, context) - if (!/^@deepseek-ai\/libreoffice-kit-(?:darwin|win32|linux)-/u.test(specifier)) return resolved + if (!resolved.url.startsWith('file:')) return resolved + const engineRequest = /^@deepseek-ai\/libreoffice-kit-(?:darwin|win32|linux)-/u.test(specifier) + const engineTarget = /\/node_modules\/@deepseek-ai\/libreoffice-kit-(?:darwin|win32|linux)-[^/]+\//u + .test(new URL(resolved.url).pathname) + if (!engineRequest && !engineTarget) return resolved const canonical = pathToFileURL(realpathSync(fileURLToPath(resolved.url))).href if (!canonical.startsWith(source)) { if (canonical.startsWith(pathToFileURL(archive + '/').href)) { diff --git a/apps/desktop-host/tests/office-engine.spec.ts b/apps/desktop-host/tests/office-engine.spec.ts index 716aef756d..83c544dc01 100644 --- a/apps/desktop-host/tests/office-engine.spec.ts +++ b/apps/desktop-host/tests/office-engine.spec.ts @@ -1,19 +1,26 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' import { createRequire, type ModuleHooks } from 'node:module' import { tmpdir } from 'node:os' -import { dirname, join } from 'node:path' +import { dirname, join, sep } from 'node:path' import { afterEach, expect, it } from 'vitest' import { installOfficeEngineResolution } from '../src/office-engine.ts' +import { PackageManifests } from '../../../packages/boot/hmr/src/package-manifest.ts' const roots: string[] = [] const hooks: ModuleHooks[] = [] +const disposers: Array<() => void> = [] afterEach(() => { + for (const dispose of disposers.splice(0).reverse()) dispose() for (const hook of hooks.splice(0)) hook.deregister() + const cache = createRequire(import.meta.url).cache + for (const path of Object.keys(cache)) { + if (roots.some(root => path.startsWith(root + sep))) Reflect.deleteProperty(cache, path) + } for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) }) function fixture(runtimeName = 'dsh') { - const root = mkdtempSync(join(tmpdir(), 'desktop-office-resolution-')) + const root = realpathSync(mkdtempSync(join(tmpdir(), 'desktop-office-resolution-'))) roots.push(root) const runtime = join(root, 'app.asar', runtimeName) const manifest = 'node_modules/@deepseek-ai/libreoffice-kit-darwin-arm64/package.json' @@ -40,6 +47,45 @@ it('resolves engine manifests to physical directories and leaves unrelated modul expect(f.require('@deepseek-ai/libreoffice-kit/package.json')).toEqual({ name: '@deepseek-ai/libreoffice-kit' }) }) +it('resolves an absolute engine manifest to its unpacked copy', () => { + const f = fixture() + expect(f.require(join(f.runtime, f.manifest))) + .toMatchObject({ path: realpathSync(dirname(join(f.root, 'app.asar.unpacked', 'dsh', f.manifest))) }) +}) + +it.each(['exports', 'main'] as const)('refreshes a cached plugin %s while Office resolution stays active', (field) => { + const f = fixture() + const packageRoot = join(f.root, 'plugin') + mkdirSync(packageRoot) + const manifest = join(packageRoot, 'package.json') + const a = join(packageRoot, 'a.cjs') + const b = join(packageRoot, 'b.cjs') + writeFileSync(manifest, JSON.stringify({ name: 'plugin', [field]: './a.cjs' })) + writeFileSync(a, 'module.exports = { marker: "a" }') + writeFileSync(b, 'module.exports = { marker: "b" }') + symlinkSync(packageRoot, join(f.runtime, 'node_modules', 'plugin'), process.platform === 'win32' ? 'junction' : 'dir') + const require = createRequire(join(f.runtime, 'package.json')) + const original: unknown = require('plugin') + const next: unknown = require(b) + const originalModule = require.cache[a] + const nextModule = require.cache[b] + const engine = f.require('@deepseek-ai/libreoffice-kit-darwin-arm64/package.json') + expect(original).toEqual({ marker: 'a' }) + expect(next).toEqual({ marker: 'b' }) + expect(require('plugin')).toBe(original) + const manifests = new PackageManifests() + disposers.push(() => { manifests.dispose() }) + writeFileSync(manifest, JSON.stringify({ name: 'plugin', [field]: './b.cjs' })) + manifests.invalidate(manifest) + expect(require('plugin')).toBe(next) + expect(require.resolve('plugin')).toBe(b) + expect(require.cache[a]).toBe(originalModule) + expect(require.cache[b]).toBe(nextModule) + expect(require(a)).toBe(original) + expect(f.require('@deepseek-ai/libreoffice-kit-darwin-arm64/package.json')).toBe(engine) + expect(f.require(join(f.runtime, f.manifest))).toBe(engine) +}) + it('rejects an engine missing from the unpacked tree instead of using its archived copy', () => { const f = fixture() rmSync(join(f.root, 'app.asar.unpacked'), { recursive: true }) diff --git a/apps/desktop/.env.macos.example b/apps/desktop/.env.macos.example index d21463ef9d..da5502ce5d 100644 --- a/apps/desktop/.env.macos.example +++ b/apps/desktop/.env.macos.example @@ -1,3 +1,4 @@ +# Release configuration only; local --unsigned builds ignore this file. # Copy to .env.macos (Git ignored). Use UTF-8 and quote secrets containing # or spaces. # Paths may be absolute or relative to apps/desktop. Values are not shell-expanded. DSH_DESKTOP_APP_ID=com.deepseek.harness @@ -20,7 +21,7 @@ DSH_DESKTOP_MACOS_DOWNLOAD_PROXY= # Requires permission to change system proxies; affects other applications during notarization. DSH_DESKTOP_MACOS_NOTARIZATION_PROXY= -# The selected mandatory-update origin is required, including unsigned and preparation builds. +# The selected mandatory-update origin is required for release and preparation builds. DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN= DSH_DESKTOP_MANDATORY_UPDATE_PROD_ORIGIN= # Set approved login origins for test builds; omit allowedAuthOrigins for production. diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index 8e528ddf37..9c95cd3111 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -3,8 +3,8 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/desktop/README.md /deepseek-harness-desktop: - en: 882c46770e3796c8 - zh: 7d22f2f4f9190c64 + en: 0f0f6ea914eff51e + zh: 4934245b04e9f1c0 /deepseek-harness-desktop/terminal-command: en: bef92953c237363f zh: d21d6effd21b8c00 @@ -42,29 +42,35 @@ en: 4cd8a8da6977928d zh: 9f624855a2edac37 /deepseek-harness-desktop/package/release-versions: - en: 0324169124c4414c - zh: f591bee485467e93 + en: 99607dc36badb7f1 + zh: 5df47873fe8cdea8 +/deepseek-harness-desktop/package/local-macos-dmg-without-release-signing: + en: 57869a5e3885f843 + zh: 7cfc01b2dcbebd78 +/deepseek-harness-desktop/package/local-official-bundles-from-a-loopback-registry: + en: 5f05072ec82cc62d + zh: a3d63b58d1ef2db7 /deepseek-harness-desktop/package/runtime-file-selection: - en: f0e345fd89868803 - zh: fd2c775ad23fa2f0 + en: e7c1eb8e78fe46e3 + zh: b7ea497dd75be524 /deepseek-harness-desktop/package/upload-updates: - en: 72073b2e72590067 - zh: 63b1f9790395c37d + en: f48baa680e841b0d + zh: 25ded1aeaac6dad1 /deepseek-harness-desktop/package/unsigned-windows-test-installer: en: cfd5780dd26a8e46 zh: f7a16b1ede793b7b /deepseek-harness-desktop/package/windows-installer-interface: - en: 94ddb11f514d8dea - zh: 15021ae8e3bc8a9c + en: 1fc1db8f54475c8f + zh: dd834ed206070396 /deepseek-harness-desktop/package/windows-ev-signing: - en: 8efe3498dbe8686d - zh: a88c16f20a9430f3 + en: be1379e4b4703e14 + zh: 244ccd2874bb155a /deepseek-harness-desktop/updates: - en: d0df867b69cbffa4 - zh: eeb47b79b86fe5ee + en: 10d58aa5bbffbd67 + zh: d9c2476e744a6537 /deepseek-harness-desktop/updates/mandatory-update-policy: - en: c00a0e7d32056ba9 - zh: a76dae44cb513d6f + en: 1527dceec540649e + zh: 543782c36f91f893 /deepseek-harness-desktop/updates/local-updater-qualification: en: bc50fd8a4d96c227 zh: fb0a6ce80c31cda6 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 7d6317e6f1..57eb2cb333 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -4,7 +4,7 @@ English | [中文](README.zh.md) Desktop analytics follows the [product collection policy](../../packages/client/product-analytics/README.md), including its live application setting. Web usage is excluded. Update installation waits for its local analytics intake before locking API admission and stopping the Host. Intake has a one-second deadline; failure does not prevent installation, and collector delivery is not awaited. -The desktop application is an Electron shell around the complete dsh Web application. An Electron RunAsNode child starts the shared profile runner, and Electron immediately loads the packaged Web entry at `dsh-app://app/`. Its shared loading page waits for Host boot injections, then starts the client without navigating to another document. Electron forwards application HTTP requests to the authenticated Web Host, dropping connection-level response headers (`transfer-encoding`, `connection`, `keep-alive`) that describe the Node fetch rather than the resource, and marking plugin bundle responses `no-store` because their per-launch revisions would only accumulate in Chromium's disk cache; WebSocket streams connect to that Host with credentials attached only for the owned application window. Node IPC carries boot injections, readiness, and shutdown. Desktop defaults to port `19387`, separate from Web’s `3080`; a `webserver.config.port` patch can override it. +The desktop application is an Electron shell around the complete dsh Web application. An Electron RunAsNode child starts the shared profile runner, and Electron immediately loads the packaged Web entry at `dsh-app://app/`. Its shared loading page waits for Host boot injections, then starts the client without navigating to another document. Electron forwards application HTTP requests to the authenticated Web Host, dropping connection-level response headers (`transfer-encoding`, `connection`, `keep-alive`) that describe the Node fetch rather than the resource, and marking plugin bundle responses `no-store` because their per-launch revisions would only accumulate in Chromium's disk cache; WebSocket streams connect to that Host with credentials attached only for the owned application window. Node IPC carries boot injections, readiness, and shutdown. Desktop listens on an OS-assigned port by default, so it cannot collide with Web’s `3080` or a reserved port; a `webserver.config.port` patch can override it. The first application-menu command, **About DeepSeek Harness**, opens Electron's native About panel with the application icon, product name, and installed release version. The menu follows the Desktop shell locale. On macOS, Hide, Hide Others, Show All, and Quit use localized labels; Hide and Quit include the DeepSeek Harness product name. These commands retain their native actions and shortcuts. macOS reads the icon from its application bundle, so an unpackaged development launch displays Electron's icon; Windows receives the packaged PNG. @@ -211,9 +211,9 @@ A production release publishes the product version and takes no `--build-version Version derivation does not change the fixed update channel or `nightly.yml` / `nightly-mac.yml` filenames. SemVer orders `0.1.6-alpha.1 < 0.1.6-alpha.1.20260916.1 < 0.1.6-alpha.2`, and a stable base's test version precedes that stable release. Clients only accept a greater version: replacing a feed cannot move an installed higher version to a lower corrected version. Such clients need manual installation; keep automatic downgrade disabled. The [archived version decision](../../.agents/notes/archived/process/2026-09-16-desktop-release-version-derivation.md) explains why the channel does not supply the prerelease identifier. -Packaging, upload, and manual macOS signature verification read `apps/desktop/.env.windows` or `.env.macos`, selected by target platform. Copy the [Windows template](.env.windows.example) or [macOS template](.env.macos.example) and fill in the local settings; Git ignores both local files, and packaged artifacts exclude them. Release fields come only from the target file, without fallback to system or shell variables; `PATH`, proxies, and build-tool settings remain inherited. The published version is an argument rather than a release field, and upload reads it from the completion record the packaging run wrote. Files use UTF-8 with optional BOM; relative certificate, SignTool, Apple API key, and keychain paths resolve from `apps/desktop`, values are not shell-expanded, and passwords containing `#` or spaces need quotes. CI also creates the target file before invoking packaging. +Except for local unsigned macOS builds, packaging, upload, and manual macOS signature verification read `apps/desktop/.env.windows` or `.env.macos`, selected by target platform. Copy the [Windows template](.env.windows.example) or [macOS template](.env.macos.example) and fill in the local settings; Git ignores both local files, and packaged artifacts exclude them. Release fields come only from the target file, without fallback to system or shell variables; `PATH`, proxies, and build-tool settings remain inherited. The published version is an argument rather than a release field, and upload reads it from the completion record the packaging run wrote. Files use UTF-8 with optional BOM; relative certificate, SignTool, Apple API key, and keychain paths resolve from `apps/desktop`, values are not shell-expanded, and passwords containing `#` or spaces need quotes. CI also creates the target file before invoking packaging. -Every package command checks the application ID, update origin, and mode-specific signing configuration before building or downloading, then probes the external tools the run will use: the archive reader, and on a Windows target the installer compiler. macOS checks the identity, Team ID, one complete notarization strategy, readable local `CSC_LINK` p12 file, explicit `CSC_KEY_PASSWORD`, and referenced API key and keychain files; Windows checks the public code-signing certificate, SignTool file, container name, and PIN format. Windows preparation-only and explicit unsigned builds do not require signing credentials. Configuration checks do not authenticate the PIN, log in to the token, unlock a keychain, or contact Apple; actual signing and notarization perform those checks. `--build-version auto` does contact the destination bucket, including under `--check`. Run the same checks separately: +Every package command checks the application ID and its mode-specific update and signing configuration before building or downloading, then probes the external tools the run will use: the archive reader, and on a Windows target the installer compiler. macOS checks the identity, Team ID, one complete notarization strategy, readable local `CSC_LINK` p12 file, explicit `CSC_KEY_PASSWORD`, and referenced API key and keychain files; Windows checks the public code-signing certificate, SignTool file, container name, and PIN format. Windows preparation-only builds and explicit unsigned builds on either platform do not require signing credentials. Configuration checks do not authenticate the PIN, log in to the token, unlock a keychain, or contact Apple; actual signing and notarization perform those checks. `--build-version auto` does contact the destination bucket, including under `--check`. Run the same checks separately: ```sh pnpm --dir apps/desktop run check:package @@ -237,13 +237,42 @@ The macOS arm64 command requires Apple Silicon. The macOS x64 command runs on In Each target owns its packed package inputs, prepared runtime, package set, dsh tree, pnpm preparation state, unpacked application, update metadata, and final artifacts under `apps/desktop/.desktop-build/targets//`. The Electron archive cache remains shared under `.desktop-build/downloads` because every archive name includes its version, platform, and architecture and is verified before extraction. A target build never consumes another target's mutable preparation state. +### Local macOS DMG without release signing + +After `pnpm install`, run one command from the repository root. No `.env.macos`, Apple account, certificate, or update service configuration is required: + +```sh +pnpm run package:desktop:mac:arm64:unsigned +pnpm run package:desktop:mac:x64:unsigned +``` + +Add `--check` to validate local configuration and tools without building, or `--dir` to stop at the unpacked application. The DMG is `apps/desktop/.desktop-build/targets//unsigned-artifacts/deepseek-harness--mac--unsigned.dmg`. After all checks pass, the command prints `DMG:` followed by its absolute path. Open it and copy the application to a local test directory. + +These commands ignore `.env.macos` and inherited release settings, use `com.deepseek.harness` as the app ID, and omit automatic-update and mandatory-update configuration. Build tools, standard network proxies, and `DSH_DESKTOP_NPM_REGISTRY` remain available from the shell. They skip Developer ID signing, Apple notarization, temporary signing keychains, and notarization proxy changes. Electron receives a local ad-hoc signature without certificates so the modified executable can run; the DMG is unsigned. Both preparation and the assembled application still run runtime integrity and smoke checks. Local builds omit update feeds, ZIPs, and release completion records, and the upload commands cannot publish them. macOS may require approval in System Settings → Privacy & Security to open an unnotarized application. + +### Local Official bundles from a loopback registry + +The test DMG excludes the provider runtimes the Official on-demand catalog advertises, and a release publishes matching bundle versions to npm. To install locally built bundles before that release, start a loopback registry that packs and serves the catalog's complete local dependency closure: + +```sh +pnpm run dev:bundle-registry +``` + +The command validates the catalog and the workspace inventory, runs `pnpm run build:official`, packs the closure into a new `dist/test-bundles//` directory, writes `bundle-registry.json` last as the completion marker, and only then prints the artifact path, the registry URL, and the exact `name@version` specs. It serves `127.0.0.1` on an allocated port under a fresh namespace until Ctrl+C or SIGTERM, and stopping it leaves both the artifacts and the installed packages on disk. It needs no `.env.macos`, Apple credentials, or npm credentials, and it does not change `pnpm run package:desktop:unsigned`. + +In the packaged application, open Plugins → Add plugin, paste one printed spec, choose **Custom address**, paste the printed registry URL, and install. A custom registry is asked alone, so this process must answer every dependency: repository-owned packages come from the run's own archives, and third-party packages keep npm's real metadata and bytes through a fixed redirect to `registry.npmjs.org`, which still requires network access. The installed entry stays Official because its package name is in the catalog. The registry choice is remembered in this browser; choose an offered registry again to switch back. + +A run's artifacts are immutable, so a rebuild serves a new namespace and a new URL, and the Official **Update** control does not appear while the installed and running versions match. The profile records the previous run's tarball URL in `pnpm-lock.yaml`, and pnpm verifies recorded entries, so installing or reinstalling that bundle against a new URL is refused with `ERR_PNPM_TARBALL_URL_MISMATCH` and can leave the earlier files in place. To test changed code at the same version, clear the stale resolution first — `pnpm clean --lockfile` in `$DSH_HOME/profiles/`, or install into a fresh profile — then install again from the new URL. `bundle-registry.json` records each archive's version, filename, size, and SHA-512 integrity together with the packing checkout's commit and dirty flag. + +The served `time` is the real packing time, so pnpm 11's default 24-hour `minimumReleaseAge` records the exemption it granted in the profile's `pnpm-workspace.yaml` (`minimumReleaseAgeExclude`); an explicitly stricter policy there fails the installation with pnpm's own message instead of being overridden. A `@scope:registry` route in `~/.npmrc` or the profile's `.npmrc` overrides `--registry` for that scope, so it must not route the served scope. + ### Runtime file selection Desktop packs workspace packages locally and installs external dependencies through the target's bundled Node and pnpm. [Desktop's file policy](scripts/runtime-file-policy.ts) then filters the immutable `resources/app.asar/dsh/node_modules` copy before signing and integrity sealing. It omits TypeScript declarations, recognized JavaScript/CSS/TypeScript source maps, TypeScript build caches, Domino's test directory, selected native compiler outputs, and node-pty prebuilds for other platforms. It preserves runtime JavaScript, native modules and their DLL/EXE helpers, WASM, unknown assets, licenses, and notices. Dependency manifests pass through electron-builder's metadata cleanup before integrity sealing, so archiving preserves their recorded bytes. The policy does not alter npm tarballs, the bundled package manager, or user-installed plugin files. The [Office conversion provider](../../packages/document/office-to-pdf/README.md) carries the target’s declared native engine, or WASM when the kit declares no matching native target. Preparation rejects a missing target engine before packaging. The complete Office dependencies, including CLI, JavaScript libraries, and the selected engine’s executables, data, licenses, and notices, are unpacked under `resources/app.asar.unpacked/dsh/node_modules/`. The Desktop Host resolves engine manifests to these physical directories and supplies loaded skills with absolute standalone Node and unpacked CLI paths. Node lives in `resources/runtime/primary-runtime/dependencies/node/bin/`; the CLI is the unpacked `@deepseek-ai/libreoffice-kit/lib/cli.js`. On macOS, the native helper receives the JIT entitlement required by [LibreOffice’s UNO bridge](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in). -The packaged application runs compiled JavaScript and pre-generated Typert metadata; it does not compile TypeScript plugins. Source-level debugger navigation and editor declarations remain available in development packages. [Copy-policy tests](tests/runtime-file-policy.spec.ts) cover exclusions and retained assets; The [payload smoke](tests/fixtures/runtime-payload-smoke.mjs) runs under Electron RunAsNode before the Host smoke and final inventory verification. The payload smoke resolves the search tool’s ripgrep executable and verifies text search and file enumeration. Signed Windows builds run these checks after dependency signing; other builds run them during `prepare:dsh`. The [Host smoke](scripts/smoke-runtime.ts) creates DOCX, XLSX, and PPTX inputs with bundled Python, converts each through the real Office provider, and checks PDF output. Every assembled application, including directory and unsigned Windows builds, repeats the payload and Host checks against ASAR. Archive integrity checks compare the complete archived descriptor with preparation, file bytes and membership in both archive and unpacked trees, recorded executable flags for archived files, and physical permissions for unpacked files. The Host smoke also obtains CLI paths from the loaded skill and runs capabilities and DOCX conversion with an empty PATH. Conversion failures stop packaging before a release record is written; macOS DMG/ZIP builds run these checks before notarization. +The packaged application runs compiled JavaScript and pre-generated Typert metadata; it does not compile TypeScript plugins. Source-level debugger navigation and editor declarations remain available in development packages. [Copy-policy tests](tests/runtime-file-policy.spec.ts) cover exclusions and retained assets; The [payload smoke](tests/fixtures/runtime-payload-smoke.mjs) runs under Electron RunAsNode before the Host smoke and final inventory verification. The payload smoke resolves the search tool’s ripgrep executable and verifies text search and file enumeration. Signed Windows builds run these checks after dependency signing; other builds run them during `prepare:dsh`. The [Host smoke](scripts/smoke-runtime.ts) creates DOCX, XLSX, and PPTX inputs with bundled Python, converts each through the real Office provider, and checks PDF output. Every assembled application, including directory and unsigned builds, repeats the payload and Host checks against ASAR. Archive integrity checks compare the complete archived descriptor with preparation, file bytes and membership in both archive and unpacked trees, recorded executable flags for archived files, and physical permissions for unpacked files. The Host smoke also obtains CLI paths from the loaded skill and runs capabilities and DOCX conversion with an empty PATH. Conversion failures stop packaging before a release record is written; macOS DMG/ZIP builds run these checks before notarization. Windows release qualification also runs [directory and replacement checks](scripts/smoke-windows.ps1) manually after the Desktop build. Set `$Makensis`, `$SevenZip`, and `$PluginDir` to the pinned builder’s NSIS compiler, 7-Zip executable, and x86-unicode NSIS plugin directory; pass the prepared `window-frame.dll` as `-FrameLibrary` to exercise the native extraction path and its failure report as well. From the repository root, run the command below. It verifies directory replacement and rollback, and both locked-file replacement modes; it is not part of the unit-test lane. @@ -293,7 +322,7 @@ The target dotenv file for internal-test packaging explicitly sets `DSH_DESKTOP_ Early internal-test packages use the `test` deployment. Select `production` explicitly only for a production release; changing upload credentials does not retarget an existing package. Packaging requires no COS credentials, disables electron-builder publishing, strips COS credentials from child processes, and records completion only after signing and notarization succeed. Upload validates this record, deployment, target, shared version, filenames, sizes, and SHA-512 before reading credentials. Packages and blockmaps upload before YAML; historical objects are retained. Every release publishes `nightly.yml` or `nightly-mac.yml`; stable releases also publish `latest.yml` or `latest-mac.yml` pointing at the same artifacts. Published YAML uses absolute binary URLs. The uploader leaves Cache-Control unset, including the empty header the COS SDK would otherwise add: deployment infrastructure owns cache policy, with uncached feeds and separately configurable binary caching. Serialize publication per target and verify public artifacts and feed contents before release qualification. -The macOS configuration uses the required release environment instead of accepting whichever certificate appears first in a keychain. It rejects empty values, a malformed Team ID, a signing identity that includes electron-builder's unsupported `Developer ID Application:` prefix, and incomplete notarization credentials. macOS packaging requires the configured identity and its private key. Runtime preparation applies that identity, a secure timestamp, and hardened runtime to every embedded Mach-O file; after signing the application, a deep strict check rejects any other leaf authority or Team ID before artifact creation. The fixed-target macOS installer commands create separate copies of the signed application and run two artifact lanes concurrently. One lane notarizes and staples the App before generating the ZIP and its update metadata. The other encloses its signed App copy in a signed DMG, then notarizes, staples, and verifies the DMG; its inner App has no individually stapled ticket. Both lanes must finish successfully before their artifacts reach the final directory and the release completion record is written. Directory-only commands also require notarization credentials and wait for Apple notarization and App stapling. The [parallel notarization decision](../../.agents/notes/implemented/process/2026-09-09-parallel-macos-notarization.md) owns copy isolation and container ticket semantics. `CSC_LINK` must name a local p12 containing the Developer ID Application certificate and private key; URLs and Base64 inputs are not supported. `CSC_KEY_PASSWORD` is its export password, not an Apple account or login password; an explicitly empty value supports an unencrypted p12. Before building, packaging creates and unlocks a private temporary keychain, imports the p12, authorizes signing, and signs a small probe. Runtime and App signing use this keychain explicitly; existing login keychains require no setup or manual unlocking. Child processes receive its path without the p12 password. The keychain is deleted after success or ordinary failure; CI must clean temporary credentials after forced termination. CI writes the certificate and `.env.macos` from its secret store, restricts file access, and deletes both after the job. Ambient `CSC_NAME` and certificate discovery order do not select the release owner. Notary credentials may instead use electron-builder's complete Apple ID or keychain-profile strategy. The two macOS identity variables are also required when repeating the application check manually with `pnpm --dir apps/desktop run verify:mac-signature -- `. +The signed macOS configuration uses the required release environment instead of accepting whichever certificate appears first in a keychain. It rejects empty values, a malformed Team ID, a signing identity that includes electron-builder's unsupported `Developer ID Application:` prefix, and incomplete notarization credentials. Signed macOS packaging requires the configured identity and its private key. Runtime preparation applies that identity, a secure timestamp, and hardened runtime to every embedded Mach-O file; after signing the application, a deep strict check rejects any other leaf authority or Team ID before artifact creation. The signed fixed-target macOS installer commands create separate copies of the signed application and run two artifact lanes concurrently. One lane notarizes and staples the App before generating the ZIP and its update metadata. The other encloses its signed App copy in a signed DMG, then notarizes, staples, and verifies the DMG; its inner App has no individually stapled ticket. Both lanes must finish successfully before their artifacts reach the final directory and the release completion record is written. Signed directory-only commands also require notarization credentials and wait for Apple notarization and App stapling. The [parallel notarization decision](../../.agents/notes/implemented/process/2026-09-09-parallel-macos-notarization.md) owns copy isolation and container ticket semantics. `CSC_LINK` must name a local p12 containing the Developer ID Application certificate and private key; URLs and Base64 inputs are not supported. `CSC_KEY_PASSWORD` is its export password, not an Apple account or login password; an explicitly empty value supports an unencrypted p12. Before building, packaging creates and unlocks a private temporary keychain, imports the p12, authorizes signing, and signs a small probe. Runtime and App signing use this keychain explicitly; existing login keychains require no setup or manual unlocking. Child processes receive its path without the p12 password. The keychain is deleted after success or ordinary failure; CI must clean temporary credentials after forced termination. CI writes the certificate and `.env.macos` from its secret store, restricts file access, and deletes both after the job. Ambient `CSC_NAME` and certificate discovery order do not select the release owner. Notary credentials may instead use electron-builder's complete Apple ID or keychain-profile strategy. The two macOS identity variables are also required when repeating the application check manually with `pnpm --dir apps/desktop run verify:mac-signature -- `. macOS signing visits real files without following Framework symlink aliases. PAK resources retain all shipped languages and are sealed by the enclosing Framework or application signature instead of receiving individual signatures. The [release policy](../../.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md) owns the dependency patch and verification requirements. @@ -327,7 +356,7 @@ The command requires `DSH_DESKTOP_APP_ID` and the normal build dependencies, inc ### Windows installer interface -The Windows installer uses native NSIS pages with light and dark palettes, system shadows, an editable installation directory, and a finish page whose launch checkbox is selected by default. Installation is restricted to the current user. Clicking Install or pressing Enter validates the current path; new destinations must be empty, and nonempty destinations must be registered installations. Running executables at the affected installation path produce a native prompt and remain running; same-named applications in other directories do not block installation. Silent updates wait up to ten seconds for the affected application to exit, then stop with exit code 2 if it is still running. +The Windows installer uses native NSIS pages with light and dark palettes, system shadows, an editable installation directory, and a finish page whose launch checkbox is selected by default. Installation is restricted to the current user. Clicking Install or pressing Enter validates the current path; drive roots remain invalid after normalization, including repeated trailing separators. New destinations must be empty, and nonempty destinations must be registered installations. Running executables at the affected installation path produce a native prompt and remain running; same-named applications in other directories do not block installation. Silent updates wait up to ten seconds for the affected application to exit, then stop with exit code 2 if it is still running. The theme follows Windows at startup; `/THEME=light`, `/THEME=dark`, and `/THEME=auto` select a palette explicitly. The window appears after its branded controls are ready. When the welcome page first appears, the installer moves above ordinary windows once and flashes its taskbar button if another window has focus; it does not remain always on top. Progress reads the pinned 7-Zip extractor’s percentage; directory promotion, registration, and cleanup retain bounded estimates. The weighted percentage does not predict remaining time. After NSIS reports success, the bar fills over 600 ms and displays 100% briefly before the finish page appears; the transition targets 750 ms. The finish page preserves the window position. Finish dismisses the installer before launching the installed executable; a launch failure restores the page for retry. Directory replacement and failure recovery follow the installation flow described above. First-launch profile preparation remains a separate Desktop operation. @@ -394,9 +423,9 @@ pnpm run prepare:desktop This diagnostic command is an alternative stopping point, not the first half of a two-command build. A later `package:desktop*` command repeats the official build and preparation so it cannot consume stale dsh packages, runtime files, or dsh content. -Every package command builds the repository, packs the first-party production closures rooted at dsh and the private Desktop Host, and prepares the target Electron distribution and pnpm CLI. `prepare:dsh` installs the production graph once at build time, prepares materialized packages for electron-builder to archive under `app.asar/dsh`, removes package-manager metadata, and writes `desktop-runtime.json` with shared package versions and final file hashes. On macOS it signs and verifies native files before inventory generation; electron-builder excludes this already-signed tree from nested re-signing. Resource mappings explicitly include `dsh/node_modules`, which the default root-directory filter omits; the prepared runtime inventory is checked after native signing. Native executables and libraries are unpacked beside ASAR; Python, standalone Node and pnpm remain in external runtime resources. Windows packaging checks every prepared PE against its ASAR unpacked entry and byte-identical disk copy, including unsigned builds. Builder glob rules match braces in PE filenames as single-character wildcards, so a matching neighboring file may also be unpacked. Prepared runtime smoke uses the verified target descriptor rather than the build host architecture. Signed installer, notarization, installed upgrade, and target-specific native-module qualification require the release environment. +Every package command builds the repository, packs the first-party production closures rooted at dsh and the private Desktop Host, and prepares the target Electron distribution and pnpm CLI. `prepare:dsh` installs the production graph once at build time, prepares materialized packages for electron-builder to archive under `app.asar/dsh`, removes package-manager metadata, and writes `desktop-runtime.json` with shared package versions and final file hashes. Signed macOS builds sign and verify native files before inventory generation; electron-builder excludes this already-signed tree from nested re-signing. Resource mappings explicitly include `dsh/node_modules`, which the default root-directory filter omits; the prepared runtime inventory is checked after native signing. Native executables and libraries are unpacked beside ASAR; Python, standalone Node and pnpm remain in external runtime resources. Windows packaging checks every prepared PE against its ASAR unpacked entry and byte-identical disk copy, including unsigned builds. Builder glob rules match braces in PE filenames as single-character wildcards, so a matching neighboring file may also be unpacked. Prepared runtime smoke uses the verified target descriptor rather than the build host architecture. Signed installer, notarization, installed upgrade, and target-specific native-module qualification require the release environment. -macOS packaging writes `Contents/Resources/app-update.yml` while assembling the App and before code signing, including the directory build that feeds the parallel ZIP and DMG lanes. The signing hook verifies the exact feed and updater cache directory. Both lane copies and the promoted App are checked again before the release completion record is written; a missing or mismatched configuration prevents artifact promotion and therefore prevents upload. +Signed macOS packaging writes `Contents/Resources/app-update.yml` while assembling the App and before code signing, including the directory build that feeds the parallel ZIP and DMG lanes. The signing hook verifies the exact feed and updater cache directory. Both lane copies and the promoted App are checked again before the release completion record is written; a missing or mismatched configuration prevents artifact promotion and therefore prevents upload. An unpacked artifact contains Electron, the materialized dsh production tree, pnpm, and the shell application. Installer size and filesystem size differ; release qualification measures both, plus the profile’s plugin storage and first-launch latency. The runtime trades more application files for eliminating core package installation on the user’s machine. @@ -416,9 +445,11 @@ If task teardown fails after confirmed Host exit, installation is refused and th Confirmed Host exit without successful task teardown displays localized recovery guidance in both ordinary and mandatory update dialogs. A typed preparation cause selects that guidance in each locale; changing translated wording cannot reclassify the failure. “View technical details” is collapsed by default and exposes only exit status, signal, shutdown acknowledgement, and deadline facts, not plugin stderr. Expanding it neither retries nor authorizes installation. + + ### Mandatory update policy -Packaging reads `.env.windows` or `.env.macos`: `DSH_DESKTOP_AUTO_UPDATE_ENV=test` (the default) selects `DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN`; `production` selects `DSH_DESKTOP_MANDATORY_UPDATE_PROD_ORIGIN`. The templates leave both origins blank; fill in the selected deployment origin in the ignored local dotenv file. The selected origin is required before preparation or signing, including unsigned and preparation-only builds; the unselected origin is optional. These settings never fall back to the parent environment or the other deployment. Packaging embeds the selected policy with the application ID; packaged applications ignore runtime overrides. +Packaging reads `.env.windows` or `.env.macos`: `DSH_DESKTOP_AUTO_UPDATE_ENV=test` (the default) selects `DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN`; `production` selects `DSH_DESKTOP_MANDATORY_UPDATE_PROD_ORIGIN`. The templates leave both origins blank; fill in the selected deployment origin in the ignored local dotenv file. Local unsigned macOS builds omit this policy and do not read either file. Other modes require the selected origin before preparation or signing, including unsigned Windows and preparation-only builds; the unselected origin is optional. These settings never fall back to the parent environment or the other deployment. Packaging embeds the selected policy with the application ID; packaged applications ignore runtime overrides. `DSH_DESKTOP_MANDATORY_UPDATE_CONFIG` JSON supplies test login origins and optional polling and download-page options; packaging rejects `origin` and `authentication` inside it. The page allowlist defaults to the selected service origin; explicitly allow other approved download-page origins when needed. Test builds select `feishu-test` and require `allowedAuthOrigins` in `DSH_DESKTOP_MANDATORY_UPDATE_CONFIG`; production selects `anonymous` and rejects that field. Each login origin must be an HTTPS origin without credentials, path, query, or fragment. The login window permits document navigation only to the selected policy origin and these configured origins. Policy requests reject redirects; only test authentication carries gateway cookies. Unpackaged development instead reads a complete policy JSON from this variable and requires `DSH_DESKTOP_APP_ID`; absent JSON disables development policy queries, and only anonymous development permits HTTP `127.0.0.1`. A user-initiated ordinary check triggers policy work concurrently but never waits for or reports a policy failure. Only a confirmed blocking decision cancels ordinary dialogs. Test authentication waits until the active ordinary dialog finishes, and cancellation or failure does not discard the updater result. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 4a428705e9..d8256885dc 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -4,7 +4,7 @@ 桌面埋点遵循[产品采集策略](../../packages/client/product-analytics/README.zh.md)及其动态应用配置,不包含 Web 使用情况。安装更新会等待该操作的本地埋点接收请求结束,再锁定 API 准入并停止 Host。接收请求的时限为一秒,失败不会阻止安装,也不等待收集端完成发送。 -桌面应用是完整 dsh Web 应用外的一层 Electron 壳。Electron RunAsNode 子进程启动共享 profile runner,Electron 立即从 `dsh-app://app/` 加载打包内的 Web 入口。共享加载页等待 Host 启动注入,然后在同一文档中启动客户端。Electron 将应用 HTTP 请求转发给已认证的 Web Host,转发时丢弃描述 Node fetch 连接而非资源本身的响应头(`transfer-encoding`、`connection`、`keep-alive`),并把插件 bundle 响应标记为 `no-store`,因为其每次启动都变化的 revision 只会在 Chromium 磁盘缓存中累积;WebSocket 流连接到该 Host,仅为归属的应用窗口附加凭据。Node IPC 承载启动注入、就绪与关闭。Desktop 默认使用端口 `19387`,与 Web 的 `3080` 分开;可通过 `webserver.config.port` patch 覆盖。 +桌面应用是完整 dsh Web 应用外的一层 Electron 壳。Electron RunAsNode 子进程启动共享 profile runner,Electron 立即从 `dsh-app://app/` 加载打包内的 Web 入口。共享加载页等待 Host 启动注入,然后在同一文档中启动客户端。Electron 将应用 HTTP 请求转发给已认证的 Web Host,转发时丢弃描述 Node fetch 连接而非资源本身的响应头(`transfer-encoding`、`connection`、`keep-alive`),并把插件 bundle 响应标记为 `no-store`,因为其每次启动都变化的 revision 只会在 Chromium 磁盘缓存中累积;WebSocket 流连接到该 Host,仅为归属的应用窗口附加凭据。Node IPC 承载启动注入、就绪与关闭。Desktop 默认监听系统分配的端口,因此不会与 Web 的 `3080` 或系统保留端口冲突;可通过 `webserver.config.port` patch 覆盖。 应用菜单第一项“**关于 DeepSeek Harness**”打开 Electron 原生关于面板,展示应用图标、产品名称和当前安装的发布版本。菜单文案跟随桌面壳的语言。macOS 的隐藏、隐藏其他、显示全部和退出条目使用本地化文案,隐藏和退出条目包含 DeepSeek Harness 产品名称。这些条目保留原生动作和快捷键。macOS 从应用包读取图标,因此未打包的开发启动会显示 Electron 图标;Windows 使用随包分发的 PNG。 @@ -213,9 +213,9 @@ production 发布使用产品版本本身,不传 `--build-version`。其上传 版本派生不改变固定更新通道,也不改变 `nightly.yml` / `nightly-mac.yml` 文件名。SemVer 排序为 `0.1.6-alpha.1 < 0.1.6-alpha.1.20260916.1 < 0.1.6-alpha.2`,稳定基础版本的测试版低于该稳定版。客户端只接受更高版本:替换 feed 无法让已安装的较高版本更新到较低的纠正版。这类客户端需要手动安装;保持自动降级关闭。[已归档的版本决策](../../.agents/notes/archived/process/2026-09-16-desktop-release-version-derivation.md)解释为什么不能用通道名替换预发布标识。 -打包、上传以及手动 macOS 签名检查使用 `apps/desktop/.env.windows` 或 `.env.macos`,由目标平台选择。复制对应的 [Windows 模板](.env.windows.example) 或 [macOS 模板](.env.macos.example),填写本机配置;Git 忽略这两个本地文件,安装产物也不包含它们。发布字段只从目标文件读取,不回退到系统或 shell 中的同名变量;`PATH`、代理和构建工具环境仍保留。发布版本是命令参数而非发布字段,上传从打包写下的完成记录中读取它。文件使用 UTF-8,支持 BOM;相对证书、SignTool、Apple API Key 和钥匙串路径以 `apps/desktop` 为基准,变量值不做 shell 展开,包含 `#` 或空格的密码需要引号。CI 同样在运行前生成目标文件。 +除本地 macOS 未签名构建外,打包、上传以及手动 macOS 签名检查使用 `apps/desktop/.env.windows` 或 `.env.macos`,由目标平台选择。复制对应的 [Windows 模板](.env.windows.example) 或 [macOS 模板](.env.macos.example),填写本机配置;Git 忽略这两个本地文件,安装产物也不包含它们。发布字段只从目标文件读取,不回退到系统或 shell 中的同名变量;`PATH`、代理和构建工具环境仍保留。发布版本是命令参数而非发布字段,上传从打包写下的完成记录中读取它。文件使用 UTF-8,支持 BOM;相对证书、SignTool、Apple API Key 和钥匙串路径以 `apps/desktop` 为基准,变量值不做 shell 展开,包含 `#` 或空格的密码需要引号。CI 同样在运行前生成目标文件。 -每条打包命令在构建与下载前检查应用 ID、更新地址和该模式需要的签名配置,随后探测本次运行要用的外部工具:归档读取工具,以及 Windows 目标的安装器编译器。macOS 检查身份、Team ID、一套完整公证凭据、`CSC_LINK` 指定的可读本地 p12 文件、显式配置的 `CSC_KEY_PASSWORD`,以及引用的 API Key 和钥匙串文件;Windows 检查公开代码签名证书、SignTool 文件、容器名称和 PIN 格式。仅准备 Windows 资源或显式未签名打包不要求签名凭据。配置检查不验证 PIN 是否正确、Token 是否登录、钥匙串是否解锁或 Apple 是否接受凭据;实际签名与公证负责这些检查。`--build-version auto` 会访问目标 bucket,`--check` 下同样如此。单独运行相同检查: +每条打包命令在构建与下载前检查应用 ID 以及该模式需要的更新与签名配置,随后探测本次运行要用的外部工具:归档读取工具,以及 Windows 目标的安装器编译器。macOS 检查身份、Team ID、一套完整公证凭据、`CSC_LINK` 指定的可读本地 p12 文件、显式配置的 `CSC_KEY_PASSWORD`,以及引用的 API Key 和钥匙串文件;Windows 检查公开代码签名证书、SignTool 文件、容器名称和 PIN 格式。仅准备 Windows 资源以及两个平台上的显式未签名打包均不要求签名凭据。配置检查不验证 PIN 是否正确、Token 是否登录、钥匙串是否解锁或 Apple 是否接受凭据;实际签名与公证负责这些检查。`--build-version auto` 会访问目标 bucket,`--check` 下同样如此。单独运行相同检查: ```sh pnpm --dir apps/desktop run check:package @@ -239,13 +239,42 @@ macOS arm64 命令要求 Apple Silicon。macOS x64 命令可以在 Intel macOS 每个目标都在 `apps/desktop/.desktop-build/targets//` 下持有自己的打包输入、已准备运行时、包集合、dsh 依赖树、pnpm 准备状态、未打包应用、更新元数据和最终产物。Electron 归档缓存继续由 `.desktop-build/downloads` 共享,因为每个归档文件名都包含版本、平台和架构,并且在解包前经过验证。目标构建绝不读取其他目标的可变准备状态。 +### 无发布签名的本地 macOS DMG + +执行 `pnpm install` 后,在仓库根目录运行一条命令即可,无需 `.env.macos`、Apple 账号、证书或更新服务配置: + +```sh +pnpm run package:desktop:mac:arm64:unsigned +pnpm run package:desktop:mac:x64:unsigned +``` + +添加 `--check` 仅验证本地配置和工具而不构建,或添加 `--dir` 只生成解包应用。DMG 路径为 `apps/desktop/.desktop-build/targets//unsigned-artifacts/deepseek-harness--mac--unsigned.dmg`。所有检查通过后,命令打印 `DMG:` 及其绝对路径。打开后将应用复制到本地测试目录。 + +这些命令忽略 `.env.macos` 和继承的发布配置,使用 `com.deepseek.harness` 作为应用 ID,并省略自动更新和强制更新配置。构建工具、标准网络代理与 `DSH_DESKTOP_NPM_REGISTRY` 仍可从 shell 提供。它们跳过 Developer ID 签名、Apple 公证、临时签名钥匙串和公证代理修改。Electron 使用无需证书的本地 ad-hoc 签名,以便修改后的可执行文件能够运行;DMG 不签名。准备阶段和组装后的应用仍执行运行时完整性与冒烟检查。本地构建不生成更新源、ZIP 或发布完成记录,上传命令无法发布这些产物。macOS 可能要求在“系统设置 → 隐私与安全性”中批准打开未经公证的应用。 + +### 用回环 registry 安装本地 Official 包 + +测试 DMG 不包含 Official 按需目录所宣传的 provider 运行时,而正式发布才会把对应版本的 bundle 发到 npm。要在发布前安装本地构建的 bundle,请启动一个回环 registry,它会打包并提供该目录的完整本地依赖闭包: + +```sh +pnpm run dev:bundle-registry +``` + +该命令先校验目录与 workspace 清单,运行 `pnpm run build:official`,把闭包打包进新的 `dist/test-bundles//` 目录,最后写入 `bundle-registry.json` 作为完成标记,然后才打印产物路径、registry URL 和精确的 `name@version` 规格。它在分配到的端口上只监听 `127.0.0.1`,使用一次性的命名空间提供服务,直到 Ctrl+C 或 SIGTERM;停止后产物和已安装的包都保留在磁盘上。它不需要 `.env.macos`、Apple 凭据或 npm 凭据,也不改变 `pnpm run package:desktop:unsigned`。 + +在打包应用中打开“插件 → 添加插件”,粘贴一条打印出的规格,选择**自定义地址**,粘贴打印出的 registry URL 后安装。自定义 registry 会被单独询问,因此必须由本进程回答每一个依赖:仓库自有的包取自本次运行的产物,第三方包则通过固定重定向到 `registry.npmjs.org` 保留 npm 的真实元数据与字节,因此仍需要网络访问。安装后的条目仍归类为 Official,因为它的包名在目录中。registry 选择会在此浏览器中记住;再次选择某个提供的 registry 即可切回。 + +一次运行的产物是不可变的,所以重新构建会提供新的命名空间和新 URL;当已安装版本与运行版本一致时,Official 的**更新**控件不会出现。profile 会在 `pnpm-lock.yaml` 中记录上一次运行的 tarball URL,而 pnpm 会校验已记录的条目,因此用新 URL 再次安装或重装该 bundle 会被 `ERR_PNPM_TARBALL_URL_MISMATCH` 拒绝,并且可能继续保留旧文件。要在同一版本上验证改动后的代码,请先清除过期的解析结果——在 `$DSH_HOME/profiles/` 中运行 `pnpm clean --lockfile`,或改用全新的 profile——然后再从新 URL 安装。`bundle-registry.json` 记录每个归档的版本、文件名、大小和 SHA-512 完整性,以及打包所用 checkout 的 commit 和 dirty 标记。 + +所提供的 `time` 是真实的打包时间,因此 pnpm 11 默认的 24 小时 `minimumReleaseAge` 会在 profile 的 `pnpm-workspace.yaml` 中记录它授予的豁免(`minimumReleaseAgeExclude`);如果那里显式配置了更严格的策略,安装会以 pnpm 自己的报错失败,而不会被覆盖。`~/.npmrc` 或 profile 的 `.npmrc` 中的 `@scope:registry` 路由会覆盖该 scope 的 `--registry`,因此不能让它们指向正在提供服务的 scope。 + ### 运行时文件筛选 Desktop 在本地打包工作区包,并通过目标捆绑的 Node 和 pnpm 安装外部依赖。[Desktop 文件策略](scripts/runtime-file-policy.ts)随后在签名和完整性封装前过滤不可变的 `resources/app.asar/dsh/node_modules` 副本。它排除 TypeScript 声明、已识别的 JavaScript/CSS/TypeScript source map、TypeScript 构建缓存、Domino 测试目录、选定的原生编译器输出和其他平台的 node-pty 预构建文件。它保留运行时 JavaScript、原生模块及其 DLL/EXE 辅助文件、WASM、未知资源、许可证和 notices。依赖清单在完整性封装前经过 electron-builder 的元数据清理,确保归档保持已记录的字节。该策略不修改 npm tarball、捆绑的包管理器或用户安装的插件文件。 [Office 转换提供方](../../packages/document/office-to-pdf/README.zh.md)携带目标已声明的原生引擎;kit 未声明匹配原生目标时携带 WASM 引擎。准备阶段在打包前拒绝缺少目标引擎的情况。完整 Office 依赖(CLI、JavaScript 库和选定引擎的可执行文件、数据、许可证及 notices)解包到 `resources/app.asar.unpacked/dsh/node_modules/` 下。Desktop Host 将引擎清单解析到这些物理目录,并向加载的技能提供独立 Node 和解包后 CLI 的绝对路径。Node 位于 `resources/runtime/primary-runtime/dependencies/node/bin/`;CLI 位于解包后的 `@deepseek-ai/libreoffice-kit/lib/cli.js`。macOS 上的原生辅助程序获得 [LibreOffice UNO 桥](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in)所需的 JIT entitlement。 -打包应用运行编译后的 JavaScript 和预生成的 Typert 元数据,不编译 TypeScript 插件。源码级调试导航和编辑器声明仍可从开发包中获取。[复制规则测试](tests/runtime-file-policy.spec.ts)覆盖排除项和保留资源;[产物 smoke](tests/fixtures/runtime-payload-smoke.mjs) 在 Host smoke 和最终清单验证之前,使用 Electron RunAsNode 执行。产物 smoke 解析搜索工具使用的 ripgrep 可执行文件,并验证文本搜索和文件枚举。Windows 签名构建在依赖签名后运行这些检查;其他构建在 `prepare:dsh` 中运行。[Host smoke](scripts/smoke-runtime.ts) 使用捆绑的 Python 创建 DOCX、XLSX 和 PPTX 输入,通过真实 Office 提供方逐一转换并检查 PDF 输出。每个组装后的应用(包括目录包和 Windows 未签名构建)都会针对 ASAR 重复产物和 Host 检查。归档完整性检查将归档内完整描述符与准备结果比对,并核对归档和解包目录中的文件内容与清单、归档内文件记录的执行标志,以及解包文件的物理权限。转换失败会在写入发布记录前终止打包;macOS DMG/ZIP 构建在公证前执行这些检查。 +打包应用运行编译后的 JavaScript 和预生成的 Typert 元数据,不编译 TypeScript 插件。源码级调试导航和编辑器声明仍可从开发包中获取。[复制规则测试](tests/runtime-file-policy.spec.ts)覆盖排除项和保留资源;[产物 smoke](tests/fixtures/runtime-payload-smoke.mjs) 在 Host smoke 和最终清单验证之前,使用 Electron RunAsNode 执行。产物 smoke 解析搜索工具使用的 ripgrep 可执行文件,并验证文本搜索和文件枚举。Windows 签名构建在依赖签名后运行这些检查;其他构建在 `prepare:dsh` 中运行。[Host smoke](scripts/smoke-runtime.ts) 使用捆绑的 Python 创建 DOCX、XLSX 和 PPTX 输入,通过真实 Office 提供方逐一转换并检查 PDF 输出。每个组装后的应用(包括目录包和未签名构建)都会针对 ASAR 重复产物和 Host 检查。归档完整性检查将归档内完整描述符与准备结果比对,并核对归档和解包目录中的文件内容与清单、归档内文件记录的执行标志,以及解包文件的物理权限。转换失败会在写入发布记录前终止打包;macOS DMG/ZIP 构建在公证前执行这些检查。 Windows 发布验收还需在 Desktop 构建后手动运行[目录和替换检查](scripts/smoke-windows.ps1)。将 `$Makensis`、`$SevenZip` 和 `$PluginDir` 分别设为锁定版本构建器的 NSIS 编译器、7-Zip 可执行文件和 x86-unicode NSIS 插件目录;通过 `-FrameLibrary` 传入已准备好的 `window-frame.dll`,即可同时覆盖原生解压路径及其失败报告。从仓库根目录运行以下命令。它验证 目录替换与回滚和两种文件占用替换方式;不属于单元测试通道。 @@ -295,7 +324,7 @@ pnpm run upload:mac:arm64 前期内测包使用 `test` 部署。只有正式发布才显式选择 `production`;更换上传凭据不会改变已有安装包的更新目标。打包不需要 COS 凭据,会禁用 electron-builder 发布、移除子进程的 COS 凭据,并且仅在签名与公证成功后记录完成状态。上传在读取凭据前验证该记录、部署、目标、共同版本号、文件名、大小与 SHA-512。安装包和 blockmap 先于 YAML 上传;历史对象继续保留。每个版本发布 `nightly.yml` 或 `nightly-mac.yml`;稳定版本还发布指向相同产物的 `latest.yml` 或 `latest-mac.yml`。发布的 YAML 使用安装包绝对 URL。上传器不设置 Cache-Control,包括 COS SDK 否则会添加的空头部:缓存策略由部署基础设施负责,清单不缓存,安装包缓存单独配置。同一目标应串行发布,并在发布验收前验证公网产物与清单内容。 -macOS 配置使用必填发布环境,不会接受钥匙串中最先发现的证书。空值、格式错误的 Team ID、包含 electron-builder 不支持的 `Developer ID Application:` 前缀的签名身份,以及不完整的公证凭据都会被拒绝。macOS 打包要求已配置的身份及其私钥可用。运行时准备会把该身份、安全时间戳与 hardened runtime 应用到每个内嵌 Mach-O 文件;应用签名完成后,深度严格检查会拒绝其他叶证书 Authority 或 Team ID,验证通过才生成发布产物。macOS 固定目标安装包命令为已签名应用创建独立副本,并发执行两条产物流。一路先公证 App 并钉票,再生成 ZIP 及其更新元数据。另一路把已签名 App 副本封装进签名 DMG,再公证 DMG、钉票并验证;其中的 App 不单独附加票据。只有两路均成功结束,产物才会移入最终目录并写入发布完成记录。仅生成目录的命令同样需要公证凭据,并等待 Apple 公证和 App 钉票完成。[并行公证决策](../../.agents/notes/implemented/process/2026-09-09-parallel-macos-notarization.zh.md)负责副本隔离与容器票据语义。`CSC_LINK` 必须指向包含 Developer ID Application 证书及私钥的本地 p12,不支持 URL 或 Base64 输入。`CSC_KEY_PASSWORD` 是其导出密码,不是 Apple 账号或登录密码;未加密的 p12 可显式填写空值。构建前,打包流程自动创建并解锁私有临时钥匙串、导入 p12、授权签名并签署小型探针。运行时与 App 签名显式使用该钥匙串,无需预先配置或手动解锁登录钥匙串。子进程只接收钥匙串路径,不接收 p12 密码。成功或普通失败后删除临时钥匙串;强制终止后由 CI 清理临时凭据。CI 从密钥存储生成证书文件和 `.env.macos`,限制文件访问权限,并在作业结束后删除二者。环境中的 `CSC_NAME` 与证书发现顺序都不能选择发布所有者。公证凭据也可以使用 electron-builder 支持的完整 Apple ID 或钥匙串 profile 方式。手动执行 `pnpm --dir apps/desktop run verify:mac-signature -- ` 重复应用检查时,也必须提供两个 macOS 身份变量。 +macOS 签名配置使用必填发布环境,不会接受钥匙串中最先发现的证书。空值、格式错误的 Team ID、包含 electron-builder 不支持的 `Developer ID Application:` 前缀的签名身份,以及不完整的公证凭据都会被拒绝。macOS 签名打包要求已配置的身份及其私钥可用。运行时准备会把该身份、安全时间戳与 hardened runtime 应用到每个内嵌 Mach-O 文件;应用签名完成后,深度严格检查会拒绝其他叶证书 Authority 或 Team ID,验证通过才生成发布产物。macOS 签名固定目标安装包命令为已签名应用创建独立副本,并发执行两条产物流。一路先公证 App 并钉票,再生成 ZIP 及其更新元数据。另一路把已签名 App 副本封装进签名 DMG,再公证 DMG、钉票并验证;其中的 App 不单独附加票据。只有两路均成功结束,产物才会移入最终目录并写入发布完成记录。签名的仅目录命令同样需要公证凭据,并等待 Apple 公证和 App 钉票完成。[并行公证决策](../../.agents/notes/implemented/process/2026-09-09-parallel-macos-notarization.zh.md)负责副本隔离与容器票据语义。`CSC_LINK` 必须指向包含 Developer ID Application 证书及私钥的本地 p12,不支持 URL 或 Base64 输入。`CSC_KEY_PASSWORD` 是其导出密码,不是 Apple 账号或登录密码;未加密的 p12 可显式填写空值。构建前,打包流程自动创建并解锁私有临时钥匙串、导入 p12、授权签名并签署小型探针。运行时与 App 签名显式使用该钥匙串,无需预先配置或手动解锁登录钥匙串。子进程只接收钥匙串路径,不接收 p12 密码。成功或普通失败后删除临时钥匙串;强制终止后由 CI 清理临时凭据。CI 从密钥存储生成证书文件和 `.env.macos`,限制文件访问权限,并在作业结束后删除二者。环境中的 `CSC_NAME` 与证书发现顺序都不能选择发布所有者。公证凭据也可以使用 electron-builder 支持的完整 Apple ID 或钥匙串 profile 方式。手动执行 `pnpm --dir apps/desktop run verify:mac-signature -- ` 重复应用检查时,也必须提供两个 macOS 身份变量。 macOS 签名遍历真实文件,不跟随 Framework 的软链接别名。PAK 资源保留全部随附语言,由外层 Framework 或应用签名记录完整性,不逐个签名。[发布策略](../../.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md)负责依赖补丁和验证要求。 @@ -329,7 +358,7 @@ pnpm run package:desktop:win:x64:unsigned ### Windows 安装界面 -Windows 安装程序使用原生 NSIS 页面,提供亮暗配色、系统阴影、可编辑的安装目录,以及默认勾选立即启动的完成页。安装仅面向当前用户。点击安装或按 Enter 均校验当前路径;新安装位置必须为空,非空位置必须是已登记的安装目录。受影响安装路径中的程序运行时显示系统提示,并保持应用运行;其他目录中的同名应用不阻止安装。静默更新最多等待受影响应用退出十秒,若仍在运行则以退出码 2 结束。 +Windows 安装程序使用原生 NSIS 页面,提供亮暗配色、系统阴影、可编辑的安装目录,以及默认勾选立即启动的完成页。安装仅面向当前用户。点击安装或按 Enter 均校验当前路径;路径规范化后仍拒绝磁盘根目录,包括带重复末尾分隔符的写法。新安装位置必须为空,非空位置必须是已登记的安装目录。受影响安装路径中的程序运行时显示系统提示,并保持应用运行;其他目录中的同名应用不阻止安装。静默更新最多等待受影响应用退出十秒,若仍在运行则以退出码 2 结束。 主题在启动时跟随 Windows;可用 `/THEME=light`、`/THEME=dark` 和 `/THEME=auto` 显式选择配色。窗口在品牌控件准备完成后显示。欢迎页首次出现时,安装窗口会一次性移到普通窗口前方;若焦点在其他窗口,任务栏按钮会闪烁提示,但安装窗口不会始终置顶。进度读取锁定版本的 7-Zip 解压器百分比;目录替换、注册和清理仍使用有界估算。加权百分比不代表剩余时间。NSIS 报告成功后,进度条用 600 毫秒补满并短暂显示 100%,再显示完成页;切换目标时长为 750 毫秒。完成页保留窗口位置。点击完成后,安装程序先隐藏窗口,再启动已安装的可执行文件;启动失败会恢复页面以供重试。目录替换和失败恢复遵循上文描述的安装流程。首次启动的配置档案准备仍属于独立的 Desktop 操作。 @@ -396,9 +425,9 @@ pnpm run prepare:desktop 这条诊断命令是另一种停止位置,并非两条命令构建流程的前半段。之后执行 `package:desktop*` 时仍会重新完成正式构建与准备,避免使用陈旧的 dsh 包、运行时文件或 dsh 内容。 -每条打包命令都会构建仓库,打包以 dsh 和私有 Desktop Host 为根的第一方生产依赖闭包,并准备目标专用的 Electron 分发包与 pnpm CLI。`prepare:dsh` 在构建时安装一次生产依赖图,准备物化包供 electron-builder 归档到 `app.asar/dsh`,移除包管理器元数据,并生成包含共享包版本和最终文件哈希的 `desktop-runtime.json`。在 macOS 上,它先签名并验证原生文件,再生成清单;electron-builder 不对已签名的此目录重复进行嵌套签名。资源映射明确包含默认根目录过滤器会忽略的 `dsh/node_modules`;准备完成的运行时清单在原生签名后检查。原生可执行文件及库解包到 ASAR 旁;Python、独立 Node 和 pnpm 保留在外部 runtime 资源中。Windows 打包逐项检查准备好的 PE,确认其 ASAR 条目已标记为解包,且磁盘副本字节一致;未签名构建也执行此检查。Builder glob 规则用单字符通配符匹配 PE 文件名中的花括号,因此同目录中名称匹配的文件也可能被解包。准备好的运行时 smoke 沿用已验证的目标描述符,不使用构建宿主的架构。签名安装包、公证、已安装应用升级和各目标原生模块的验收需要发布环境。 +每条打包命令都会构建仓库,打包以 dsh 和私有 Desktop Host 为根的第一方生产依赖闭包,并准备目标专用的 Electron 分发包与 pnpm CLI。`prepare:dsh` 在构建时安装一次生产依赖图,准备物化包供 electron-builder 归档到 `app.asar/dsh`,移除包管理器元数据,并生成包含共享包版本和最终文件哈希的 `desktop-runtime.json`。macOS 签名构建先签名并验证原生文件,再生成清单;electron-builder 不对已签名的此目录重复进行嵌套签名。资源映射明确包含默认根目录过滤器会忽略的 `dsh/node_modules`;准备完成的运行时清单在原生签名后检查。原生可执行文件及库解包到 ASAR 旁;Python、独立 Node 和 pnpm 保留在外部 runtime 资源中。Windows 打包逐项检查准备好的 PE,确认其 ASAR 条目已标记为解包,且磁盘副本字节一致;未签名构建也执行此检查。Builder glob 规则用单字符通配符匹配 PE 文件名中的花括号,因此同目录中名称匹配的文件也可能被解包。准备好的运行时 smoke 沿用已验证的目标描述符,不使用构建宿主的架构。签名安装包、公证、已安装应用升级和各目标原生模块的验收需要发布环境。 -macOS 打包在组装 App 时、代码签名前写入 `Contents/Resources/app-update.yml`,供并行 ZIP 与 DMG 路线使用的目录构建也执行此操作。签名钩子验证准确的更新源和 updater 缓存目录。写入发布完成记录前,流程会再次检查两条路线的副本和最终移入的 App;配置缺失或不匹配会阻止移入产物,因而也会阻止上传。 +macOS 签名打包在组装 App 时、代码签名前写入 `Contents/Resources/app-update.yml`,供并行 ZIP 与 DMG 路线使用的目录构建也执行此操作。签名钩子验证准确的更新源和 updater 缓存目录。写入发布完成记录前,流程会再次检查两条路线的副本和最终移入的 App;配置缺失或不匹配会阻止移入产物,因而也会阻止上传。 未压缩产物包含 Electron、物化后的 dsh 生产依赖树、pnpm,以及壳应用。安装包大小与文件系统占用不同;发布验收需要测量两者,以及 profile 插件存储和首次启动耗时。此布局用更多应用内文件换取消除用户机器上的核心包安装过程。 @@ -418,9 +447,11 @@ Windows 下载完成后的更新确认说明应用会在安装期间关闭、完 已确认 Host 退出但任务未成功收尾时,常规与强更弹窗均展示本地化恢复提示。两种语言都根据类型化的准备失败原因选择提示,翻译文案变化不会改变失败分类。“查看技术详情”默认折叠,仅展示退出状态、信号、关闭确认和截止时间事实,不展示插件 stderr。展开详情既不重试,也不授权安装。 + + ### 强制更新策略 -打包读取 `.env.windows` 或 `.env.macos`:`DSH_DESKTOP_AUTO_UPDATE_ENV=test`(默认值)选择 `DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN`;`production` 选择 `DSH_DESKTOP_MANDATORY_UPDATE_PROD_ORIGIN`。模板将两个源站留空;在 Git 忽略的目标 dotenv 文件中填写所选部署的源站。在准备产物或签名前,所选源站必须配置,包括未签名和仅准备构建;未选环境的源站可不填。这些配置不会回退到父进程环境或另一部署环境。打包将选定策略与应用 ID 写入元数据;打包应用忽略运行时覆盖。 +打包读取 `.env.windows` 或 `.env.macos`:`DSH_DESKTOP_AUTO_UPDATE_ENV=test`(默认值)选择 `DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN`;`production` 选择 `DSH_DESKTOP_MANDATORY_UPDATE_PROD_ORIGIN`。模板将两个源站留空;在 Git 忽略的目标 dotenv 文件中填写所选部署的源站。本地 macOS 未签名构建省略此策略,不读取这两个文件。其他模式在准备产物或签名前必须配置所选源站,包括 Windows 未签名和仅准备构建;未选环境的源站可不填。这些配置不会回退到父进程环境或另一部署环境。打包将选定策略与应用 ID 写入元数据;打包应用忽略运行时覆盖。 `DSH_DESKTOP_MANDATORY_UPDATE_CONFIG` JSON 提供测试登录源站,以及可选的轮询和下载页面选项;打包拒绝其中的 `origin` 和 `authentication`。页面白名单默认只包含所选服务源站;需要其他已批准下载页面源站时应显式配置。测试包选择 `feishu-test`,且必须在 `DSH_DESKTOP_MANDATORY_UPDATE_CONFIG` 中配置 `allowedAuthOrigins`;正式包选择 `anonymous`,并拒绝该字段。每个登录源站必须是没有凭据、路径、查询或片段的 HTTPS origin。登录窗口仅允许文档导航到所选策略源站和这些已配置源站。策略请求拒绝重定向;仅测试鉴权携带网关 Cookie。未打包开发模式则从此变量读取完整策略 JSON,并要求 `DSH_DESKTOP_APP_ID`;缺少 JSON 会禁用开发模式策略查询,仅匿名开发允许 HTTP `127.0.0.1`。用户发起常规检查时会并发触发策略检查,但不会等待或展示策略失败。只有已确认的强更决定可以关闭常规弹窗。测试环境鉴权会等待当前常规弹窗结束,取消或失败不会丢弃 updater 结果。 diff --git a/apps/desktop/installer/path.nsh b/apps/desktop/installer/path.nsh index 6d5723c196..01aafe135f 100644 --- a/apps/desktop/installer/path.nsh +++ b/apps/desktop/installer/path.nsh @@ -106,7 +106,8 @@ Function ${PREFIX}InstallerValidatePath ${GetParent} $2 $2 ${Loop} System::Call 'kernel32::GetFullPathNameW(w "$InstallerPath", i ${NSIS_MAX_STRLEN}, w .r4, p 0) i.r0' - ${If} $0 == 0 + ; Repeated trailing separators can normalize to a drive root. + ${If} $0 < 4 ${OrIf} $0 >= ${NSIS_MAX_STRLEN} Return ${EndIf} diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 3eaf57f56a..0a84d09e6f 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-desktop", "description": "Electron desktop shell for a bundled dsh runtime and external plugins", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "private": true, "license": "MIT", "type": "module", @@ -9,7 +9,7 @@ "scripts": { "test:installer": "node scripts/test-windows-installer.mjs", "build": "tsc -b && pnpm run bundle", - "bundle": "tsdown", + "bundle": "tsdown --config-loader native", "dev": "tsx scripts/dev.ts", "start": "tsx scripts/dev.ts --skip-build", "test:updates:local": "pnpm run build && node scripts/test-local-updater.mjs", @@ -27,8 +27,10 @@ "restore:mac-proxy": "tsx scripts/macos-notarization-proxy.ts", "package:dir": "tsx scripts/package-target.ts --dir", "package:mac:arm64": "tsx scripts/package-target.ts mac-arm64", + "package:mac:arm64:unsigned": "tsx scripts/package-target.ts mac-arm64 --unsigned", "package:mac:arm64:dir": "tsx scripts/package-target.ts mac-arm64 --dir", "package:mac:x64": "tsx scripts/package-target.ts mac-x64", + "package:mac:x64:unsigned": "tsx scripts/package-target.ts mac-x64 --unsigned", "package:mac:x64:dir": "tsx scripts/package-target.ts mac-x64 --dir", "package:win:x64": "tsx scripts/package-target.ts win-x64", "package:win:x64:unsigned": "tsx scripts/package-target.ts win-x64 --unsigned", @@ -43,7 +45,7 @@ "dependencies": { "electron-updater": "^6.8.9", "semver": "^7.8.5", - "ws": "^8.21.0", + "ws": "^8.21.3", "@deepseek-ai/dsh-api-gateway": "workspace:*", "@deepseek-ai/cordis": "workspace:~" }, @@ -56,17 +58,17 @@ "@electron/get": "^5.1.0", "@electron/notarize": "2.5.0", "@types/js-yaml": "^4.0.9", - "@types/node": "^22.20.0", + "@types/node": "^22.20.1", "@types/semver": "^7.8.0", "app-builder-lib": "26.15.3", "cos-nodejs-sdk-v5": "3.0.0", "electron": "^44.0.0", "electron-builder": "^26.15.3", "extract-zip": "^2.0.1", - "js-yaml": "^4.2.0", + "js-yaml": "^4.3.2", "koffi": "3.1.1", - "pnpm": "11.7.0", - "sharp": "^0.35.3", + "pnpm": "11.28.5", + "sharp": "^0.35.5", "typescript": "^6.0.3", "@deepseek-ai/dsh-deepseek-account": "workspace:*", "@types/ws": "^8.18.1", @@ -75,7 +77,7 @@ "react-dom": "^18.2.0", "@types/react": "~18.3.1", "@types/react-dom": "~18.3.0", - "vite": "^6.0.0", + "vite": "^6.4.3", "@deepseek-ai/dsh-client-ui-theme": "workspace:*", "@deepseek-ai/dsh-client-shortcuts": "workspace:*", "@deepseek-ai/dsh-atomic-write": "workspace:*", diff --git a/apps/desktop/scripts/desktop-build-version-discovery.ts b/apps/desktop/scripts/desktop-build-version-discovery.ts index 6bb7a1b045..fd5a2c9e10 100644 --- a/apps/desktop/scripts/desktop-build-version-discovery.ts +++ b/apps/desktop/scripts/desktop-build-version-discovery.ts @@ -30,7 +30,7 @@ const LISTING_DEADLINE_MS = 8_000 /** Objects one listing page may return. */ const LISTING_PAGE_SIZE = 1000 -/** Artifact name electron-builder writes for one build, on either platform; unsigned Windows builds add a suffix. */ +/** Artifact name electron-builder writes for one build, on either platform; unsigned builds add a suffix. */ const ARTIFACT = /(?:^|\/)deepseek-harness-(?.+)-(?:mac|win)-(?:arm64|x64)(?:-unsigned)?\.(?:exe|dmg|zip)$/u /** Inputs that decide which versions are already taken. */ diff --git a/apps/desktop/scripts/desktop-package-environment.d.mts b/apps/desktop/scripts/desktop-package-environment.d.mts index b0d7bc2dbe..a49ca46eb9 100644 --- a/apps/desktop/scripts/desktop-package-environment.d.mts +++ b/apps/desktop/scripts/desktop-package-environment.d.mts @@ -1,4 +1,11 @@ -/** Load platform-local release settings without changing the caller's process environment. */ +/** Select local test defaults or platform release settings without changing the caller's environment. */ + +/** + * Select local macOS test defaults without reading dotenv files or inheriting release settings. + * @param environment Parent environment, retained for build tools and networking. + * @returns Unsigned build environment with the standard application identifier and no update policy. + */ +export function localMacOSPackageEnvironment(environment?: NodeJS.ProcessEnv): NodeJS.ProcessEnv /** * Read the target's required UTF-8 dotenv file; release settings never fall back to ambient values. @@ -14,8 +21,8 @@ export function loadDesktopPackageEnvironment( ): NodeJS.ProcessEnv /** - * Validate release configuration before preparation without invoking a token or Apple's services. - * @param environment File-owned release settings. + * Validate the packaging mode before preparation without invoking a token or Apple's services. + * @param environment Selected local defaults or file-owned release settings. * @param target Selected release target. * @param options Explicit packaging mode. * @returns Nothing. diff --git a/apps/desktop/scripts/desktop-package-environment.mjs b/apps/desktop/scripts/desktop-package-environment.mjs index 3addf3ff65..51b878100a 100644 --- a/apps/desktop/scripts/desktop-package-environment.mjs +++ b/apps/desktop/scripts/desktop-package-environment.mjs @@ -1,4 +1,4 @@ -/** Load platform-local release settings without changing the caller's process environment. */ +/** Select local test defaults or platform release settings without changing the caller's environment. */ import { accessSync, constants, readFileSync, statSync } from 'node:fs' import { dirname, join, resolve } from 'node:path' @@ -19,6 +19,19 @@ const MACOS_SETTING = /^(?:DSH_DESKTOP_MACOS_(?:SIGNING_IDENTITY|TEAM_ID|PACK_CO const AMBIENT_RELEASE_SETTING = /^(?:DSH_DESKTOP_(?:APP_ID|AUTO_UPDATE_ENV|MANDATORY_UPDATE_.*|WINDOWS_.*|MACOS_.*)|APPLE_.*|(?:WIN_)?CSC_.*|DOWNLOAD_(?:TEST|PROD)_.*)$/iu const FILE_SETTINGS = ['DSH_DESKTOP_WINDOWS_CER_FILE', 'DSH_DESKTOP_WINDOWS_SIGNTOOL', 'APPLE_API_KEY', 'APPLE_KEYCHAIN', 'CSC_LINK'] +/** + * Select local macOS test defaults without reading dotenv files or inheriting release settings. + * @param {NodeJS.ProcessEnv} environment Parent environment, retained for build tools and networking. + * @returns {NodeJS.ProcessEnv} Unsigned build environment with the standard application identifier and no update policy. + */ +export function localMacOSPackageEnvironment(environment = process.env) { + return { + ...Object.fromEntries(Object.entries(environment).filter(([name]) => !AMBIENT_RELEASE_SETTING.test(name))), + DSH_DESKTOP_APP_ID: 'com.deepseek.harness', + DSH_DESKTOP_UNSIGNED: '1', + } +} + /** * Read the target's required UTF-8 dotenv file; release settings never fall back to ambient values. * @param {'win32' | 'darwin'} platform Target platform. @@ -70,8 +83,8 @@ function requireReadableFile(environment, name) { } /** - * Validate release configuration before preparation without invoking a token or Apple's services. - * @param {NodeJS.ProcessEnv} environment File-owned release settings. + * Validate the packaging mode before preparation without invoking a token or Apple's services. + * @param {NodeJS.ProcessEnv} environment Selected local defaults or file-owned release settings. * @param {{ platform: 'win32' | 'darwin', arch: string }} target Selected release target. * @param {{ unsigned?: boolean, prepareOnly?: boolean }} options Explicit packaging mode. * @returns {void} @@ -79,7 +92,7 @@ function requireReadableFile(environment, name) { export function validateDesktopPackageEnvironment(environment, target, options = {}) { resolveDesktopAppId(environment) resolveNpmRegistry(environment) - resolveDesktopPolicyEnvironment(environment) + if (target.platform !== 'darwin' || !options.unsigned) resolveDesktopPolicyEnvironment(environment) if (target.platform === 'darwin') resolveMacOSPackageSettings(environment) else resolveWindowsPackageSettings(environment) if (options.unsigned) return diff --git a/apps/desktop/scripts/electron-builder-config.mjs b/apps/desktop/scripts/electron-builder-config.mjs index 1fb787803c..f08b783865 100644 --- a/apps/desktop/scripts/electron-builder-config.mjs +++ b/apps/desktop/scripts/electron-builder-config.mjs @@ -51,7 +51,6 @@ export function createElectronBuilderConfig( preparedRuntimeVersion = undefined, ) { const appId = resolveDesktopAppId(env) - const policy = resolveDesktopPolicyEnvironment(env) const targetPlatform = env.DSH_DESKTOP_TARGET_PLATFORM const resolvedPlatform = targetPlatform ?? hostPlatform const resolvedArch = env.DSH_DESKTOP_TARGET_ARCH ?? hostArch @@ -59,12 +58,12 @@ export function createElectronBuilderConfig( throw new Error('desktop package: DSH_DESKTOP_UNSIGNED must be 0 or 1') } const unsigned = env.DSH_DESKTOP_UNSIGNED === '1' - if (unsigned && resolvedPlatform !== 'win32') throw new Error('desktop package: unsigned builds require Windows') const packagesMacOS = targetPlatform === 'darwin' || (targetPlatform === undefined && hostPlatform === 'darwin') const packagesWindows = resolvedPlatform === 'win32' + const policy = packagesMacOS && unsigned ? undefined : resolveDesktopPolicyEnvironment(env) if (resolvedPlatform === 'win32') installWindowsDirectoryInstaller() - const macOSSigning = packagesMacOS ? resolveMacOSSigningEnvironment(env) : undefined - if (packagesMacOS) resolveMacOSNotarizationEnvironment(env) + const macOSSigning = packagesMacOS && !unsigned ? resolveMacOSSigningEnvironment(env) : undefined + if (packagesMacOS && !unsigned) resolveMacOSNotarizationEnvironment(env) const buildPaths = desktopTargetBuildPaths(resolveDesktopBuildTarget(env, hostPlatform, hostArch)) let primaryRuntimeDestination let dshDestination @@ -102,7 +101,7 @@ export function createElectronBuilderConfig( protocols: [{ name: 'DeepSeek Harness', schemes: ['dsh'] }], extraMetadata: { dshDesktopAppId: appId, - dshMandatoryUpdatePolicy: policy, + ...(policy === undefined ? {} : { dshMandatoryUpdatePolicy: policy }), ...buildVersion === productVersion ? {} : { version: buildVersion }, ...packaged === undefined ? {} : { dshBuildCommit: packaged.commit, dshBuildDirty: packaged.dirty }, }, @@ -152,19 +151,20 @@ export function createElectronBuilderConfig( category: 'public.app-category.developer-tools', // macOS matches the application locale against this bundle, not Electron Framework resources. extendInfo: { CFBundleLocalizations: ['en', 'zh_CN'] }, - identity: macOSSigning?.signingIdentity, - forceCodeSigning: true, + // Ad-hoc signing keeps the modified Electron executable runnable without a Developer ID. + identity: unsigned ? '-' : macOSSigning?.signingIdentity, + forceCodeSigning: !unsigned, hardenedRuntime: true, extendInfo: { NSMicrophoneUsageDescription: 'DeepSeek Harness uses your microphone to transcribe speech into message drafts.' }, entitlements: fileURLToPath(new URL('./macos-entitlements.plist', import.meta.url)), entitlementsInherit: fileURLToPath(new URL('./macos-entitlements.plist', import.meta.url)), - // ASAR-unpacked native runtime files are pre-signed; PAK resources are sealed by their enclosing bundle. + // Prepared runtime files retain their signatures; PAK resources are sealed by their enclosing bundle. signIgnore: ['/Contents/Resources/app\\.asar\\.unpacked/dsh(?:/|$)', '/Contents/Resources/runtime/primary-runtime(?:/|$)', '\\.pak$'], - notarize: true, - target: ['dmg', 'zip'], + notarize: !unsigned, + target: unsigned ? ['dmg'] : ['dmg', 'zip'], }, dmg: { - sign: true, + sign: !unsigned, writeUpdateInfo: false, }, beforePack: async context => { @@ -204,7 +204,7 @@ export function createElectronBuilderConfig( }) await verifyWindowsAsarUnpack(buildPaths.dsh, context.packager.getResourcesDir(context.appOutDir), windowsCode) } - if (context.electronPlatformName !== 'darwin') return + if (context.electronPlatformName !== 'darwin' || unsigned) return const appPath = join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`) if (update !== undefined) { await verifyMacOSAppUpdateConfig(appPath, resolveMacOSAppUpdateFeed(context.packager.config.publish), @@ -213,7 +213,7 @@ export function createElectronBuilderConfig( verifyMacOSSignatureAfterSign(context, macOSSigning ?? resolveMacOSSigningEnvironment(env)) }, artifactBuildCompleted: artifact => { - if (!artifact.file.endsWith('.dmg')) return + if (unsigned || !artifact.file.endsWith('.dmg')) return return notarizeMacOSDiskImageArtifact( artifact, env, diff --git a/apps/desktop/scripts/package-target.ts b/apps/desktop/scripts/package-target.ts index 62f4fcbbb2..a133ccdcf8 100644 --- a/apps/desktop/scripts/package-target.ts +++ b/apps/desktop/scripts/package-target.ts @@ -10,7 +10,7 @@ import { } from './desktop-auto-update-environment.mjs' import { desktopTargetBuildPaths } from './desktop-build-paths.mjs' import { packageMacOSArtifacts, type DesktopPrepackagedArtifact } from './package-macos.ts' -import { loadDesktopPackageEnvironment, validateDesktopPackageEnvironment } from './desktop-package-environment.mjs' +import { loadDesktopPackageEnvironment, localMacOSPackageEnvironment, validateDesktopPackageEnvironment } from './desktop-package-environment.mjs' import { createPackagingRun, recordPackagingEvent } from './packaging-run.mjs' import { withWindowsSigningStage } from './windows-signing-stage.mjs' import { prepareWindowsSignatureCacheDirectory, resolveWindowsSignatureCacheDirectory } from './windows-signature-cache-directory.mjs' @@ -93,9 +93,9 @@ export function withoutWindowsSigningEnvironment(environment: NodeJS.ProcessEnv) } /** - * Select signing and NSIS-compatible archive filters for electron-builder. + * Select signing mode and strip unsigned-build credentials for packaging subprocesses. * @param environment - Target packaging environment. - * @param unsigned - Whether to create a local unsigned Windows artifact. + * @param unsigned - Whether to create a local artifact without release signing or notarization. * @returns Packaging environment without certificate inputs for unsigned builds. */ export function desktopElectronBuilderEnvironment(environment: NodeJS.ProcessEnv, unsigned: boolean): NodeJS.ProcessEnv { @@ -105,7 +105,7 @@ export function desktopElectronBuilderEnvironment(environment: NodeJS.ProcessEnv if (!unsigned) return selected return { ...Object.fromEntries(Object.entries(withoutWindowsSigningEnvironment(selected)) - .filter(([name]) => !/^(?:WIN_)?CSC_/iu.test(name))), + .filter(([name]) => !/^(?:(?:WIN_)?CSC_|APPLE_|DSH_DESKTOP_MACOS_(?:SIGNING_|TEAM_ID$))/iu.test(name))), CSC_IDENTITY_AUTO_DISCOVERY: 'false', DSH_DESKTOP_UNSIGNED: '1', } @@ -234,7 +234,6 @@ export function parseDesktopPackageInvocation( }) if (positionals.length > 1) throw new Error('desktop package: expected at most one target') const name = positionals[0] ?? hostTargetName(hostPlatform, hostArch) - if (values.unsigned && name !== 'win-x64') throw new Error('desktop package: --unsigned requires win-x64') if (values.unsigned && values['prepare-only']) throw new Error('desktop package: --unsigned cannot use --prepare-only') const requestedBuildVersion = values['build-version']?.trim() if (values['build-version'] !== undefined && (requestedBuildVersion === undefined || requestedBuildVersion === '')) { @@ -332,7 +331,9 @@ async function resolveRequestedBuildVersion( async function main(): Promise { const invocation = parseDesktopPackageInvocation(process.argv.slice(2)) const { target } = invocation - const environment = loadDesktopPackageEnvironment(target.platform) + const environment = target.platform === 'darwin' && invocation.unsigned + ? localMacOSPackageEnvironment() + : loadDesktopPackageEnvironment(target.platform) const productVersion = packageVersion(join(APP_ROOT, 'package.json'), 'desktop package') // Release settings come from the target dotenv file alone, so the version this run publishes is an // argument; the environment variable below only carries it to the child processes that build. @@ -366,8 +367,9 @@ async function main(): Promise { recordPackagingEvent(run.directory, { type: 'macos-settings', packConcurrency: settings.packConcurrency, downloadProxyConfigured: settings.downloadProxy !== undefined, notarizationProxyConfigured: settings.notarizationProxy !== undefined }) - await packagingStep(run.directory, 'macos-package', () => withMacOSSigningKeychain(environment, - signingEnvironment => packageTarget(invocation, signingEnvironment, run)), secrets) + await packagingStep(run.directory, 'macos-package', () => invocation.unsigned + ? packageTarget(invocation, environment, run) + : withMacOSSigningKeychain(environment, signingEnvironment => packageTarget(invocation, signingEnvironment, run)), secrets) } else { await packagingStep(run.directory, 'windows-package', () => packageTarget(invocation, environment, run), secrets) } @@ -381,6 +383,11 @@ async function main(): Promise { else process.env.DSH_DESKTOP_PACKAGING_RUN_DIR = previousDirectory run.finish(success) } + if (success && target.platform === 'darwin' && !invocation.directory && !invocation.prepareOnly) { + const paths = desktopTargetBuildPaths(target.name) + const filename = `deepseek-harness-${buildVersion}-mac-${target.arch}${invocation.unsigned ? '-unsigned' : ''}.dmg` + console.log(`DMG: ${join(invocation.unsigned ? paths.unsignedArtifacts : paths.artifacts, filename)}`) + } } /** @@ -407,7 +414,8 @@ export async function packageTarget( rmSync(releaseRecordPath, { force: true }) rmSync(`${releaseRecordPath}.tmp`, { force: true }) } - const buildEnv = withoutWindowsSigningEnvironment(withoutDesktopUploadCredentials(environment)) + const buildEnv = withoutWindowsSigningEnvironment(withoutDesktopUploadCredentials( + desktopElectronBuilderEnvironment(environment, invocation.unsigned))) const targetEnv: NodeJS.ProcessEnv = { ...buildEnv, DSH_DESKTOP_TARGET_PLATFORM: target.platform, @@ -473,7 +481,7 @@ export async function packageTarget( await execute(['run', 'prepare:dsh', ...(signPrimaryRuntime ? ['--defer-runtime-smoke'] : [])], downloadEnv) if (signPrimaryRuntime) await execute(['run', 'sign:primary-runtime', '--dsh'], electronBuilderEnv) if (invocation.prepareOnly) return - if (target.platform === 'darwin' && !invocation.directory) { + if (target.platform === 'darwin' && !invocation.unsigned && !invocation.directory) { await execute([ ...desktopElectronBuilderArguments(target, true), '--config.mac.notarize=false', @@ -486,7 +494,7 @@ export async function packageTarget( artifactsRoot: buildPaths.artifacts, environment: electronBuilderEnv, }, artifact => execute(desktopElectronBuilderArguments(target, false, artifact), electronBuilderEnv)), undefined, undefined, proxyEvent) - } else if (target.platform === 'darwin') { + } else if (target.platform === 'darwin' && !invocation.unsigned) { await execute([...desktopElectronBuilderArguments(target, true), '--config.mac.notarize=false'], electronBuilderEnv) await execute(['exec', 'tsx', 'scripts/smoke-packaged-runtime.ts'], targetEnv) const appPath = join(buildPaths.artifacts, target.arch === 'arm64' ? 'mac-arm64' : 'mac', 'DeepSeek Harness.app') @@ -497,7 +505,7 @@ export async function packageTarget( await execute(['exec', 'tsx', 'scripts/smoke-packaged-runtime.ts', ...(invocation.unsigned ? ['--unsigned'] : [])], targetEnv) } if (!invocation.directory && !invocation.unsigned) writeReleaseRecord(target, electronBuilderEnv, buildPaths.artifacts) - if (journal) recordPackagingEvent(journal, { type: 'artifacts', directory: buildPaths.artifacts }) + if (journal) recordPackagingEvent(journal, { type: 'artifacts', directory: invocation.unsigned ? buildPaths.unsignedArtifacts : buildPaths.artifacts }) } if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) await main() diff --git a/apps/desktop/scripts/prepare-dsh.ts b/apps/desktop/scripts/prepare-dsh.ts index 442e01757e..4e03f763cb 100644 --- a/apps/desktop/scripts/prepare-dsh.ts +++ b/apps/desktop/scripts/prepare-dsh.ts @@ -153,7 +153,7 @@ async function main(): Promise { if (!existsSync(join(DSH_OUTPUT_ROOT, 'node_modules', '@deepseek-ai', `libreoffice-kit-${officeEngine}`, 'prebuilds.json'))) { throw new Error(`desktop runtime: missing required LibreOffice engine ${officeEngine}`) } - if (process.platform === 'darwin') { + if (process.platform === 'darwin' && process.env.DSH_DESKTOP_UNSIGNED !== '1') { await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'sign:dsh-native', () => signMacOSRuntime(DSH_OUTPUT_ROOT, resolveDesktopAppId(process.env), resolveMacOSSigningEnvironment(process.env), target.arch, join(BUILD_PATHS.root, 'signature-cache'))) await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'sign:primary-native', () => signMacOSRuntime(join(RUNTIME_ROOT, 'primary-runtime'), resolveDesktopAppId(process.env), resolveMacOSSigningEnvironment(process.env), target.arch, join(BUILD_PATHS.root, 'signature-cache'))) } diff --git a/apps/desktop/scripts/smoke-packaged-runtime.ts b/apps/desktop/scripts/smoke-packaged-runtime.ts index 106830b5ad..b91030008b 100644 --- a/apps/desktop/scripts/smoke-packaged-runtime.ts +++ b/apps/desktop/scripts/smoke-packaged-runtime.ts @@ -11,7 +11,6 @@ const paths = resolveDesktopTargetBuildPaths() const { values } = parseArgs({ options: { unsigned: { type: 'boolean', default: false } }, allowPositionals: false }) const target = resolveDesktopBuildTarget() const windows = target === 'win-x64' -if (values.unsigned && !windows) throw new Error('desktop smoke: unsigned artifacts require Windows') const artifacts = values.unsigned ? paths.unsignedArtifacts : paths.artifacts const application = windows ? join(artifacts, 'win-unpacked') : join(artifacts, target === 'mac-arm64' ? 'mac-arm64' : 'mac', 'DeepSeek Harness.app', 'Contents') diff --git a/apps/desktop/tests/desktop-package-environment.spec.ts b/apps/desktop/tests/desktop-package-environment.spec.ts index 76129b56f8..a892a8548a 100644 --- a/apps/desktop/tests/desktop-package-environment.spec.ts +++ b/apps/desktop/tests/desktop-package-environment.spec.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, expect, it } from 'vitest' -import { loadDesktopPackageEnvironment, validateDesktopPackageEnvironment } from '../scripts/desktop-package-environment.mjs' +import { loadDesktopPackageEnvironment, localMacOSPackageEnvironment, validateDesktopPackageEnvironment } from '../scripts/desktop-package-environment.mjs' import { resolveWindowsPackageSettings } from '../scripts/windows-package-settings.mjs' const WINDOWS = { platform: 'win32', arch: 'x64' } as const @@ -23,6 +23,25 @@ async function withDirectory(action: (directory: string) => Promise): Prom } describe('Desktop local packaging configuration', () => { + it('selects macOS local defaults while excluding inherited release settings and retaining build networking', () => { + const parent = { + PATH: 'build-tools', HTTPS_PROXY: 'http://proxy.example:8080', DSH_DESKTOP_NPM_REGISTRY: 'https://registry.npmmirror.com', + DSH_DESKTOP_APP_ID: 'com.example.release', DSH_DESKTOP_UNSIGNED: '0', + DSH_DESKTOP_AUTO_UPDATE_ENV: 'production', DSH_DESKTOP_MANDATORY_UPDATE_CONFIG: 'invalid-json', + DSH_DESKTOP_MANDATORY_UPDATE_PROD_ORIGIN: 'https://policy.example.com', + DSH_DESKTOP_MACOS_SIGNING_IDENTITY: 'Release identity', CSC_LINK: 'missing.p12', + APPLE_API_KEY: 'missing.p8', DOWNLOAD_PROD_COS_SECRET_KEY: 'unused-secret', + } + const local = localMacOSPackageEnvironment(parent) + expect(local).toEqual({ + PATH: parent.PATH, HTTPS_PROXY: parent.HTTPS_PROXY, DSH_DESKTOP_NPM_REGISTRY: parent.DSH_DESKTOP_NPM_REGISTRY, + DSH_DESKTOP_APP_ID: 'com.deepseek.harness', DSH_DESKTOP_UNSIGNED: '1', + }) + expect(() => { validateDesktopPackageEnvironment(local, MACOS, { unsigned: true }) }).not.toThrow() + expect(parent.DSH_DESKTOP_UNSIGNED).toBe('0') + expect(parent.DSH_DESKTOP_APP_ID).toBe('com.example.release') + }) + it('takes cache concurrency from the Windows file and defaults to four without ambient overrides', async () => { await withDirectory(async (directory) => { const parent = { DSH_DESKTOP_WINDOWS_SIGNATURE_CACHE_CONCURRENCY: '8' } @@ -139,6 +158,14 @@ describe('Desktop local packaging configuration', () => { }).not.toThrow() }) + it('accepts unsigned macOS configuration without Apple or update policy settings while keeping release validation', () => { + const local = localMacOSPackageEnvironment({}) + expect(() => { validateDesktopPackageEnvironment(local, MACOS, { unsigned: true }) }).not.toThrow() + expect(() => { validateDesktopPackageEnvironment(local, MACOS) }) + .toThrow('DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN') + expect(() => { validateDesktopPackageEnvironment(RELEASE, MACOS) }).toThrow('DSH_DESKTOP_MACOS_SIGNING_IDENTITY') + }) + it('accepts one local npm registry mirror and rejects other registry forms', () => { const release = { ...POLICY, DSH_DESKTOP_APP_ID: RELEASE.DSH_DESKTOP_APP_ID } expect(() => { diff --git a/apps/desktop/tests/desktop-policy-environment.spec.ts b/apps/desktop/tests/desktop-policy-environment.spec.ts index e4d893eca0..be5b4ede25 100644 --- a/apps/desktop/tests/desktop-policy-environment.spec.ts +++ b/apps/desktop/tests/desktop-policy-environment.spec.ts @@ -47,8 +47,13 @@ it('rejects login origins in production', () => { .toThrow('must not configure allowedAuthOrigins') }) -it.each([{ unsigned: true }, { prepareOnly: true }, {}])('fails before signing/preparation when policy is absent in %j', (options) => { +it.each([{ unsigned: true }, { prepareOnly: true }, {}])('requires policy outside local macOS builds in %j', (options) => { for (const platform of ['win32', 'darwin'] as const) { + if (platform === 'darwin' && options.unsigned) { + expect(() => { validateDesktopPackageEnvironment({ DSH_DESKTOP_APP_ID: 'com.example.test' }, { platform, arch: 'x64' }, options) }) + .not.toThrow() + continue + } expect(() => { validateDesktopPackageEnvironment({ DSH_DESKTOP_APP_ID: 'com.example.test' }, { platform, arch: 'x64' }, options) }) .toThrow('DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN') } diff --git a/apps/desktop/tests/expected/windows-installer.json b/apps/desktop/tests/expected/windows-installer.json index 031352a046..a9d08b5a3c 100644 --- a/apps/desktop/tests/expected/windows-installer.json +++ b/apps/desktop/tests/expected/windows-installer.json @@ -2,6 +2,7 @@ "launchCheckboxState": 1, "nativeProgressVisible": false, "cases": [ + "canonical-drive-roots-rejected-before-ownership-check", "enter-validates-current-path-and-unchecked-launch", "completion-preserves-window-position", "welcome-ready-before-first-show", diff --git a/apps/desktop/tests/fixtures/runtime-payload-smoke.mjs b/apps/desktop/tests/fixtures/runtime-payload-smoke.mjs index d0d7bb94f6..503c4bae3c 100644 --- a/apps/desktop/tests/fixtures/runtime-payload-smoke.mjs +++ b/apps/desktop/tests/fixtures/runtime-payload-smoke.mjs @@ -61,7 +61,11 @@ async function checkPty() { // A Windows GUI executable needs a console-owning shell when launched inside ConPTY. const executable = process.platform === 'win32' ? process.env.ComSpec : process.execPath const args = process.platform === 'win32' ? ['/d', '/c', 'node', script] : [script] - const terminal = pty.spawn(executable, args, { cwd: scratch, env, cols: 80, rows: 24 }) + // Mirror the product's Windows allocation: the console host node-pty ships. + const terminal = pty.spawn(executable, args, { + cwd: scratch, env, cols: 80, rows: 24, + ...(process.platform === 'win32' ? { useConptyDll: true } : {}), + }) let output = '' let exited = false let timedOut = false diff --git a/apps/desktop/tests/macos-signature.spec.ts b/apps/desktop/tests/macos-signature.spec.ts index c8d9f859af..d39ee99cb9 100644 --- a/apps/desktop/tests/macos-signature.spec.ts +++ b/apps/desktop/tests/macos-signature.spec.ts @@ -129,10 +129,31 @@ describe('desktop macOS release signature', () => { }) }) - it('rejects unsigned macOS builds and malformed signing modes', async () => { + it.each(['arm64', 'x64'])('builds local macOS %s DMGs without release credentials or update metadata', async (arch) => { + const { createElectronBuilderConfig } = await import('../scripts/electron-builder-config.mjs') + const config = createElectronBuilderConfig({ + DSH_DESKTOP_APP_ID: RELEASE_ENVIRONMENT.DSH_DESKTOP_APP_ID, + DSH_DESKTOP_UNSIGNED: '1', + }, 'darwin', arch) + expect(portablePath(config.directories.output)).toContain(`/targets/mac-${arch}/unsigned-artifacts`) + expect(config.artifactName).toBe('deepseek-harness-${version}-${os}-${arch}-unsigned.${ext}') + expect(config.mac).toMatchObject({ identity: '-', forceCodeSigning: false, notarize: false, target: ['dmg'] }) + expect(config.dmg).toMatchObject({ sign: false, writeUpdateInfo: false }) + expect(config.publish).toBeNull() + expect(config.extraMetadata).not.toHaveProperty('dshMandatoryUpdatePolicy') + await expect(config.afterSign({ electronPlatformName: 'darwin' } as Parameters[0])).resolves.toBeUndefined() + expect(config.artifactBuildCompleted({ file: '/tmp/local-unsigned.dmg' })).toBeUndefined() + }) + + it('omits mandatory-update policy from local macOS builds even when release settings are supplied', async () => { + const { createElectronBuilderConfig } = await import('../scripts/electron-builder-config.mjs') + const config = createElectronBuilderConfig({ ...RELEASE_ENVIRONMENT, DSH_DESKTOP_UNSIGNED: '1' }, 'darwin', 'arm64') + expect(config.extraMetadata).not.toHaveProperty('dshMandatoryUpdatePolicy') + expect(config.publish).toBeNull() + }) + + it('rejects malformed signing modes', async () => { const { createElectronBuilderConfig } = await import('../electron-builder.config.mjs') - expect(() => createElectronBuilderConfig({ ...RELEASE_ENVIRONMENT, DSH_DESKTOP_UNSIGNED: '1' })) - .toThrow(/unsigned builds require Windows/u) expect(() => createElectronBuilderConfig({ ...RELEASE_ENVIRONMENT, DSH_DESKTOP_UNSIGNED: 'yes' })) .toThrow(/must be 0 or 1/u) }) diff --git a/apps/desktop/tests/package-target-stages.spec.ts b/apps/desktop/tests/package-target-stages.spec.ts index 41e1ee8d4e..cde1b0079d 100644 --- a/apps/desktop/tests/package-target-stages.spec.ts +++ b/apps/desktop/tests/package-target-stages.spec.ts @@ -113,6 +113,35 @@ it('checks the assembled macOS runtime before notarizing and recording the relea expect(writeFileSync).toHaveBeenCalledOnce() }) +it.each([ + ['mac-arm64', false], ['mac-arm64', true], ['mac-x64', false], ['mac-x64', true], +] as const)('builds unsigned %s with directory=%s without Apple credentials, notarization, or a release record', async (target, directory) => { + const { run, stages } = supervisor() + await packageTarget(parseDesktopPackageInvocation([target, '--unsigned', ...(directory ? ['--dir'] : [])], 'darwin', 'arm64'), { + ...environment, CSC_LINK: 'unused.p12', CSC_KEY_PASSWORD: 'unused-secret', APPLE_API_KEY: 'unused.p8', + DSH_DESKTOP_MACOS_SIGNING_IDENTITY: 'Unused identity', DSH_DESKTOP_MACOS_TEAM_ID: 'TEAMID1234', + }, run) + expect(stages.at(-2)).toBe(`exec electron-builder --config electron-builder.config.mjs --mac --${target === 'mac-arm64' ? 'arm64' : 'x64'} --publish never${directory ? ' --dir' : ''}`) + expect(stages.at(-1)).toBe('exec tsx scripts/smoke-packaged-runtime.ts --unsigned') + for (const call of run.run.mock.calls) { + expect(call[3].env).toMatchObject({ DSH_DESKTOP_UNSIGNED: '1', CSC_IDENTITY_AUTO_DISCOVERY: 'false' }) + for (const key of ['CSC_LINK', 'CSC_KEY_PASSWORD', 'APPLE_API_KEY', 'DSH_DESKTOP_MACOS_SIGNING_IDENTITY', 'DSH_DESKTOP_MACOS_TEAM_ID']) { + expect(call[3].env).not.toHaveProperty(key) + } + } + expect(withMacOSNotarizationProxy).not.toHaveBeenCalled() + expect(packageMacOSArtifacts).not.toHaveBeenCalled() + expect(writeFileSync).not.toHaveBeenCalled() +}) + +it('fails unsigned macOS packaging when the assembled runtime cannot run', async () => { + const { run, stages } = supervisor('exec tsx scripts/smoke-packaged-runtime.ts --unsigned') + await expect(packageTarget(parseDesktopPackageInvocation(['mac-arm64', '--unsigned'], 'darwin', 'arm64'), environment, run)) + .rejects.toThrow('stage refused') + expect(stages.at(-1)).toBe('exec tsx scripts/smoke-packaged-runtime.ts --unsigned') + expect(writeFileSync).not.toHaveBeenCalled() +}) + it.each([false, true])('refuses macOS notarization and release records after an assembled-runtime failure (directory=%s)', async (directory) => { const { run } = supervisor('exec tsx scripts/smoke-packaged-runtime.ts') await expect(packageTarget(parseDesktopPackageInvocation(['mac-arm64', ...(directory ? ['--dir'] : [])], 'darwin', 'arm64'), environment, run)) diff --git a/apps/desktop/tests/package-target.spec.ts b/apps/desktop/tests/package-target.spec.ts index c802f668d2..5019841e84 100644 --- a/apps/desktop/tests/package-target.spec.ts +++ b/apps/desktop/tests/package-target.spec.ts @@ -73,14 +73,16 @@ describe('desktop package target', () => { expect(desktopElectronBuilderArguments(target, true)).toContain('--dir') }) - it('accepts unsigned Windows artifacts and rejects other targets or preparation-only use', () => { + it('accepts unsigned supported targets and rejects preparation-only use', () => { expect(parseDesktopPackageInvocation(['win-x64', '--unsigned'], 'win32', 'x64').unsigned).toBe(true) expect(parseDesktopPackageInvocation(['win-x64'], 'win32', 'x64').unsigned).toBe(false) expect(parseDesktopPackageInvocation(['--unsigned', '--dir'], 'win32', 'x64')).toMatchObject({ unsigned: true, directory: true, }) - expect(() => parseDesktopPackageInvocation(['mac-arm64', '--unsigned'], 'darwin', 'arm64')) - .toThrow(/requires win-x64/u) + for (const name of ['mac-arm64', 'mac-x64']) { + expect(parseDesktopPackageInvocation([name, '--unsigned'], 'darwin', 'arm64').unsigned).toBe(true) + expect(parseDesktopPackageInvocation([name, '--unsigned', '--dir'], 'darwin', 'arm64').directory).toBe(true) + } expect(() => parseDesktopPackageInvocation(['--unsigned', '--prepare-only'], 'win32', 'x64')) .toThrow(/cannot use --prepare-only/u) }) @@ -93,10 +95,16 @@ describe('desktop package target', () => { CSC_KEY_PASSWORD: 'secret', WIN_CSC_LINK: 'windows.pfx', CSC_IDENTITY_AUTO_DISCOVERY: 'true', + APPLE_API_KEY: 'private.p8', + APPLE_APP_SPECIFIC_PASSWORD: 'apple-secret', + DSH_DESKTOP_MACOS_SIGNING_IDENTITY: 'Example Company (TEAMID1234)', + DSH_DESKTOP_MACOS_TEAM_ID: 'TEAMID1234', + DSH_DESKTOP_MACOS_PACK_CONCURRENCY: '2', DSH_DESKTOP_UNSIGNED: '1', } expect(desktopElectronBuilderEnvironment(environment, true)).toEqual({ DSH_DESKTOP_APP_ID: 'com.example.desktop', + DSH_DESKTOP_MACOS_PACK_CONCURRENCY: '2', CSC_IDENTITY_AUTO_DISCOVERY: 'false', DSH_DESKTOP_UNSIGNED: '1', }) diff --git a/apps/desktop/tests/windows-installer-smoke.ps1 b/apps/desktop/tests/windows-installer-smoke.ps1 index 67a8aa38df..53404b494d 100644 --- a/apps/desktop/tests/windows-installer-smoke.ps1 +++ b/apps/desktop/tests/windows-installer-smoke.ps1 @@ -142,11 +142,13 @@ try { Click-Control $process $copy.INSTALLER_BROWSE Dismiss $process $copy.INSTALLER_CHOOSE_PATH [void][InstallerCapture]::SendMessage($window, 0x28, $edit, [IntPtr]1) - foreach ($invalidPath in @('C:\Windows\Harness Installer Test', [IO.Path]::GetPathRoot($installPath), ([IO.Path]::GetPathRoot($installPath) + '\'))) { + $driveRoot = [IO.Path]::GetPathRoot($installPath) + foreach ($invalidPath in @('C:\Windows\Harness Installer Test', $driveRoot, ($driveRoot + '\'), ($driveRoot + '\\'))) { [void][InstallerCapture]::SendMessage($edit, 0xC, [IntPtr]::Zero, $invalidPath) [void][InstallerCapture]::PostMessage($edit, 0x100, [IntPtr]13, [IntPtr]::Zero) Dismiss $process $copy.INSTALLER_PATH_INVALID } + $results.Add('canonical-drive-roots-rejected-before-ownership-check') [void][InstallerCapture]::SendMessage($edit, 0xC, [IntPtr]::Zero, $installPath) [InstallerCapture]::MoveBy($window, 73, -41) $bounds = [InstallerCapture]::Bounds($window) diff --git a/apps/web/package.json b/apps/web/package.json index 7bc21e7178..6dfc75207f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-frontend", "description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -47,26 +47,28 @@ "@deepseek-ai/dsh-experimental-webworker-runtime": "workspace:*", "@deepseek-ai/dsh-remote-mock": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", - "@types/node": "^22.0.0", + "@types/node": "^22.20.1", "@types/react": "~18.3.1", "@types/react-dom": "~18.3.0", "@types/ws": "8.18.1", - "@vitejs/plugin-react": "^4.0.0", + "@vitejs/plugin-react": "^4.7.0", "http-server": "^14.1.1", "fflate": "^0.8.2", "playwright": "^1.49.0", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^6.0.3", - "vite": "^6.0.0", - "vitest": "^4.1.8", - "ws": "8.21.0", + "vite": "^6.4.3", + "vitest": "^4.1.11", + "ws": "8.21.3", "@deepseek-ai/dsh-client-ui-theme": "workspace:*", "@deepseek-ai/dsh-launch-environment": "workspace:*", "@deepseek-ai/dsh-workspace-changes": "workspace:*", "@deepseek-ai/dsh-subprocess-local": "workspace:*", "@deepseek-ai/dsh-experimental-auto-review": "workspace:*", "@deepseek-ai/dsh-experimental-speech-to-text": "workspace:*", - "@deepseek-ai/dsh-experimental-voice-input-bundle": "workspace:*" + "@deepseek-ai/dsh-experimental-voice-input-bundle": "workspace:*", + "@deepseek-ai/dsh-experimental-cot-translation-bundle": "workspace:*", + "@deepseek-ai/dsh-experimental-worktree": "workspace:*" } } diff --git a/apps/web/tests/README.i18n.yaml b/apps/web/tests/README.i18n.yaml index 0b302db6f3..524edaeb31 100644 --- a/apps/web/tests/README.i18n.yaml +++ b/apps/web/tests/README.i18n.yaml @@ -6,8 +6,8 @@ en: 3e8ecd3509fa944f zh: 3fd3ff9fcd1e1e90 /apps-web-browser-e2e/completion-observations: - en: 263a34ba5c63e316 - zh: ac1b79f39dde721f + en: 4a06ba5463b53bb3 + zh: 70863c70e15c6746 /apps-web-browser-e2e/these-are-host-face-tests: en: 8c406d423c8e2137 zh: a66a144fd146f56c diff --git a/apps/web/tests/README.md b/apps/web/tests/README.md index 0fa47d231e..0076a3b809 100644 --- a/apps/web/tests/README.md +++ b/apps/web/tests/README.md @@ -18,6 +18,10 @@ Ordinary scenarios begin with no registered Workspace or Session and a durable m State-sensitive cases use Workspace, admission, attachment, and model-stream barriers to separate visible intermediate states from completed operations. Model-picker persistence assertions wait for the saved default, independently of menu closure. Details close waits for frame transitions; archive verification assigns an explicit title to the seeded Session and follows that identity across reload. The [CI test reliability workflow](../../../.agents/skills/dsh-ci-test-reliability/SKILL.md#synchronize-on-state) owns these synchronization rules. +[Idle-submission handoff](idle-submission-handoff.e2e.ts) waits up to the scaffold's 30-second turn-completion budget for Host frames and browser placement. Vitest owns scaffold teardown so a replay-consumption error does not replace the failed scenario assertion. + +Plugin-manager switches wait up to `PLUGIN_TOGGLE_SETTLE_MS` (10 seconds) for the Host's profile write and the store's follow-up inventory read, the two steps that republish `aria-checked`; that constant's JSDoc records the measured chain. + Explicit scrolling uses `scrollIntoView` from `support.ts`: it resolves the locator again when its old element detaches and checks connection in the same browser task as native scrolling. Scenarios retain their visibility and geometry assertions after scrolling. ## These are Host-face tests diff --git a/apps/web/tests/README.zh.md b/apps/web/tests/README.zh.md index 782ee743ac..d1968d5080 100644 --- a/apps/web/tests/README.zh.md +++ b/apps/web/tests/README.zh.md @@ -18,6 +18,10 @@ pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps 依赖状态的用例使用 Workspace、接纳、附件和模型流屏障,区分可见中间状态与已完成操作。模型选择器的持久化断言等待默认设置保存完成,不以菜单关闭作为完成信号。详情关闭等待框架过渡结束;归档验证为 seed Session 设置显式标题,并跨重载跟踪该身份。[CI 测试可靠性工作流](../../../.agents/skills/dsh-ci-test-reliability/SKILL.md#synchronize-on-state)负责这些同步规则。 +[空闲提交交接](idle-submission-handoff.e2e.ts)对 Host 帧和浏览器中的消息归位的等待使用 scaffold 的 30 秒 Turn 完成预算。Vitest 负责 scaffold 清理,使 replay 消耗检查错误不会覆盖场景中失败的断言。 + +插件管理器的开关为 Host 的 profile 写入与 store 随后的 inventory 读取预留 `PLUGIN_TOGGLE_SETTLE_MS`(10 秒),这两步完成后才会重新发布 `aria-checked`;链路的实测值记录在该常量的 JSDoc 中。 + 显式滚动使用 `support.ts` 的 `scrollIntoView`:旧元素脱离 DOM 时重新解析 locator,并在同一个浏览器任务中检查连接状态、执行原生滚动。各场景保留滚动后的可见性与几何断言。 ## 这些是 Host 面的测试 diff --git a/apps/web/tests/agent-preset-selection.e2e.ts b/apps/web/tests/agent-preset-selection.e2e.ts index 714407c69e..6fc12aba02 100644 --- a/apps/web/tests/agent-preset-selection.e2e.ts +++ b/apps/web/tests/agent-preset-selection.e2e.ts @@ -18,7 +18,7 @@ import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import { SESSION_FORMAT_VERSION, SessionId as sessionId, type SessionEvent, type SessionHeader, type SessionId, } from '@deepseek-ai/dsh-session' -import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent' +import { SUBAGENT_DESCRIPTOR_VERSION } from '@deepseek-ai/dsh-subagent' import { createSystemMessage, createUserMessage } from '@deepseek-ai/dsh-llm' import { captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole, @@ -163,9 +163,10 @@ async function seedSubagent(scaffold: WebScaffold, parentId: SessionId): Promise type: 'subagent/descriptor', seq: 2, time: SEEDED_CHILD_CREATED_AT + 2, - data: snapshotSubagentDescriptor({ + data: { + version: SUBAGENT_DESCRIPTOR_VERSION, mode: 'one-shot', provider: 'spawn', label: 'header order probe', - }), + }, }, { type: 'turn/end', @@ -183,9 +184,10 @@ async function seedSubagent(scaffold: WebScaffold, parentId: SessionId): Promise * seeded session records `minimal` too, so a substring match over the whole * list answers before the switch has landed. * @param scaffold - authenticated Web Host scaffold. + * @param targetId - exact Session to inspect after several Sessions have been created. * @returns the live session's preset, or undefined before it is listed. */ -async function livePreset(scaffold: WebScaffold): Promise { +async function livePreset(scaffold: WebScaffold, targetId?: SessionId): Promise { const response = await scaffold.hostFetch('/api/session/list', { method: 'POST', headers: { 'content-type': 'application/json' }, @@ -204,7 +206,7 @@ async function livePreset(scaffold: WebScaffold): Promise { } } } - const preset = body.result.value?.items.find(item => item.sessionId !== SEED_ID) + const preset = body.result.value?.items.find(item => targetId === undefined ? item.sessionId !== SEED_ID : item.sessionId === targetId) ?.projections?.values.agentPreset return typeof preset === 'string' ? preset : undefined } @@ -355,7 +357,7 @@ describe('web e2e: agent-preset selection', () => { await writeComposerDraft(page, composer, '') }, 90_000) - it('resets the hidden default to Standard while preserving the blank session until the user switches it', async () => { + it('preserves the current mode while New Session uses the reset Standard default', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-disabled')) await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('standard') @@ -379,16 +381,18 @@ describe('web e2e: agent-preset selection', () => { await page.keyboard.press('Escape') await expect.poll(async () => (await scaffold.ctx.agentPresets.remoteExportList()).presets.find(preset => preset.isDefault)?.id).toBe('standard') - const reuse = page.waitForResponse('**/api/session/create') + const creation = page.waitForResponse('**/api/session/create') await page.getByRole('button', { name: 'New session', exact: true }).last().click() - const reused = await reuse - expect(reused.request().postDataJSON()).toHaveProperty('payload.args.request.sessionId') - expect(await reused.json()).toMatchObject({ result: { ok: true, value: { agentPreset: 'minimal' } } }) + const created = await creation + expect(created.request().postDataJSON()).not.toHaveProperty('payload.args.request.sessionId') + const createdBody = await created.json() as { result: { value: { sessionId: SessionId; agentPreset: string } } } + expect(createdBody).toMatchObject({ result: { ok: true, value: { agentPreset: 'standard' } } }) + const freshSessionId = createdBody.result.value.sessionId + const freshConversation = page.locator(`[data-conversation-session="${freshSessionId}"]`) + await freshConversation.waitFor() await page.reload() - await page.getByRole('button', { name: 'Minimal mode', exact: true }).click() - await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('minimal') - await page.getByRole('menuitem', { name: /^Standard mode/ }).click() - await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('standard') + await freshConversation.waitFor() + await page.getByRole('button', { name: 'Standard mode', exact: true }).waitFor() await openSettings(page, 'en') const reopened = page.getByRole('dialog', { name: 'Settings' }) @@ -401,7 +405,7 @@ describe('web e2e: agent-preset selection', () => { await reopened.getByRole('button', { name: 'New task default: Standard mode' }).waitFor({ timeout: 10_000 }) await reopened.getByRole('button', { name: 'Set as new task default: Minimal mode' }).waitFor({ timeout: 10_000 }) await reopened.getByRole('button', { name: 'Close' }).last().click() - await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('standard') + await expect.poll(() => livePreset(scaffold, freshSessionId), { timeout: 15_000 }).toBe('standard') await page.getByRole('button', { name: 'Standard mode' }).waitFor({ timeout: 10_000 }) }) diff --git a/apps/web/tests/assembled-boot.ts b/apps/web/tests/assembled-boot.ts index b7ab90a8e2..e58ee67be0 100644 --- a/apps/web/tests/assembled-boot.ts +++ b/apps/web/tests/assembled-boot.ts @@ -11,7 +11,7 @@ import { globSync, readFileSync } from 'node:fs' import { createRequire } from 'node:module' import { dirname, join, resolve } from 'node:path' import { pathToFileURL } from 'node:url' -import { act, cleanup } from '@testing-library/react' +import { act, cleanup, fireEvent, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, vi } from 'vitest' import { bootInjections, orderByModuleGraph } from '@deepseek-ai/dsh-client-modules' import type { ClientModuleLoaderTarget, WebBootEntry, WebBootGraph } from '@deepseek-ai/dsh-client-modules/client' @@ -310,6 +310,29 @@ export function mountAssembledApp(options: AssembledBootOptions = {}): Assembled return remote } +/** + * Create a fixture Session through the sidebar and wait for its own composer to mount. + * @param remote - fixture transport returned by mountAssembledApp. + * @returns composer inside the Session named by this creation's reply. + */ +export async function createAssembledSessionComposer(remote: AssembledRemote): Promise { + const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 }) + const start = tree.querySelector('button[aria-label="New session in fixture"]') + if (start === null) throw new Error('fixture Workspace new-session action missing') + const previousCalls = remote.mock.log.calls('session/create').length + fireEvent.click(start) + return await waitFor(() => { + const creation = remote.mock.log.calls('session/create')[previousCalls] + if (creation?.state !== 'answered') throw new Error('fixture Session creation has not completed') + const { value: { sessionId } } = creation.result as { value: { sessionId: string } } + const conversation = [...document.querySelectorAll('[data-conversation-session]')] + .find(element => element.getAttribute('data-conversation-session') === sessionId) + const composer = conversation?.querySelector('[data-composer-input][contenteditable="true"]') + if (composer === undefined || composer === null) throw new Error(`composer for Session ${sessionId} has not mounted`) + return composer + }, { timeout: 10_000 }) +} + /** * Match a CSS-module class by its logical name. * Module class names carry a per-build hash in one of two schemes — diff --git a/apps/web/tests/auto-review-child.overlay.yml b/apps/web/tests/auto-review-child.overlay.yml deleted file mode 100644 index a44addc554..0000000000 --- a/apps/web/tests/auto-review-child.overlay.yml +++ /dev/null @@ -1,9 +0,0 @@ -# Add one one-shot binding beside the shipped continuable delegation tool so -# this integration lane covers both in-process lifecycle modes. -- insert: - - id: auto-review-one-shot-subagent - name: '@deepseek-ai/dsh-tool-subagent' - config: - provider: spawn - toolName: subagent_one_shot - backgroundMode: one-shot diff --git a/apps/web/tests/background-job-list.e2e.ts b/apps/web/tests/background-job-list.e2e.ts index fa53fe4e0a..673270ae86 100644 --- a/apps/web/tests/background-job-list.e2e.ts +++ b/apps/web/tests/background-job-list.e2e.ts @@ -135,6 +135,55 @@ describe.skipIf(MODE === 'record')('web e2e: background job list', () => { expect(tripwire.warnings).toEqual([]) }, 90_000) + it('keeps the open list above the expanded right sidebar and in the header Tab order', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-background-job-right-sidebar')) + const trigger = page.getByRole('button', { name: '1 background job', exact: true }) + if (await trigger.getAttribute('aria-expanded') === 'true') await trigger.click() + await page.getByRole('button', { name: 'Open right sidebar' }).click() + const sidebar = page.locator('[data-rightbar-col]') + // A toggle holds data-animating on the frame until its tracks settle. + await expect.poll(() => sidebar.evaluate(column => column.parentElement?.hasAttribute('data-animating'))).toBe(false) + await trigger.click() + const list = page.getByRole('list', { name: 'Background jobs' }) + await list.waitFor({ timeout: 10_000 }) + // The list crosses into the sidebar column, so the hit test below covers it. + const [menuBox, sidebarBox] = await Promise.all([list.boundingBox(), sidebar.boundingBox()]) + if (menuBox === null || sidebarBox === null) throw new Error('job list geometry is unavailable') + expect(menuBox.x + menuBox.width).toBeGreaterThan(sidebarBox.x) + // Every corner of the list hit-tests to the list itself; the inset clears its rounded corners. + const covered = await list.evaluate((menu) => { + const rect = menu.getBoundingClientRect() + const inset = 20 + const points: Array<[number, number]> = [ + [rect.left + inset, rect.top + inset], [rect.right - inset, rect.top + inset], + [rect.left + inset, rect.bottom - inset], [rect.right - inset, rect.bottom - inset], + ] + return points.filter(([x, y]) => !menu.contains(document.elementFromPoint(x, y))) + }) + expect(covered).toEqual([]) + + // Keyboard order matches the in-place list: the trigger leads into the + // list, and Tab off its last control reaches the control after the trigger. + const after = await trigger.evaluate((node) => { + const all = [...document.querySelectorAll('button, a[href], [tabindex]:not([tabindex="-1"])')] + const outside = all.filter(el => el.closest('[aria-label="Background jobs"]') === null && el.tabIndex >= 0) + return outside[outside.indexOf(node as HTMLElement) + 1]?.outerHTML.slice(0, 120) + }) + expect(after).toMatch(/^
') + await page.locator('button').evaluate(button => { + button.addEventListener('click', event => { + button.setAttribute('data-click-trusted', String(event.isTrusted)) + const tail = document.createElement('div') + tail.setAttribute('data-chat-flow-key', '9:turn-tail:next') + document.body.append(tail) + }) + }) + const button = page.getByRole('button', { name: 'Load earlier', exact: true }) + const click = button.click.bind(button) + const elementHandles = button.elementHandles.bind(button) + const prepare = async (): Promise => { entered.resolve(undefined); await release.promise } + vi.spyOn(page, 'getByRole').mockReturnValue(button) + // The Locator spy gates preparation in the original selector-inside-timer control. + vi.spyOn(button, 'click').mockImplementation(async options => { await prepare(); await click(options) }) + vi.spyOn(button, 'elementHandles').mockImplementation(async () => { await prepare(); return elementHandles() }) + let now = 0 + operation = measureOlderPage(page, 1, () => now).then(value => ({ value }), (error: unknown) => ({ error })) + await Promise.race([ + entered.promise, + operation.then(outcome => { + if ('error' in outcome) throw outcome.error + throw new Error('Paging completed before the selector preparation barrier') + }), + ]) + expect(await page.locator('button').getAttribute('data-click-trusted')).toBeNull() + expect(await page.locator(TAIL).count()).toBe(1) + now = PAGE_BUDGET_MS + 1 + release.resolve(undefined) + const outcome = await operation + if ('error' in outcome) throw outcome.error + expectEndpointWithinBudget(outcome.value, PAGE_BUDGET_MS) + expect(outcome.value).toBe(0) + expect(await page.locator('button').getAttribute('data-click-trusted')).toBe('true') + expect(await page.locator(TAIL).count()).toBe(2) + } finally { + release.resolve(undefined) + try { await browser.close() } finally { + try { await operation } finally { vi.restoreAllMocks() } + } + } +}) + +it('waits for a named older-page control before starting its clock', async () => { + const browser = await chromium.launch({ headless: true }) + const entered = Promise.withResolvers() + let operation: Promise<{ value: number } | { error: unknown }> | undefined + try { + const page = await browser.newPage() + await page.setContent('
') + await page.locator('button').evaluate(button => { + button.addEventListener('click', event => { + button.setAttribute('data-click-trusted', String(event.isTrusted)) + const tail = document.createElement('div') + tail.setAttribute('data-chat-flow-key', '9:turn-tail:next') + document.body.append(tail) + }) + }) + const locator = page.getByRole('button', { name: 'Load earlier', exact: true }) + const waitFor = locator.waitFor.bind(locator) + vi.spyOn(page, 'getByRole').mockReturnValue(locator) + vi.spyOn(locator, 'waitFor').mockImplementation(options => { entered.resolve(undefined); return waitFor(options) }) + let now = 0 + operation = measureOlderPage(page, 1, () => now).then(value => ({ value }), (error: unknown) => ({ error })) + await Promise.race([ + entered.promise, + operation.then(outcome => { + if ('error' in outcome) throw outcome.error + throw new Error('Paging completed before control readiness') + }), + ]) + expect(await page.locator('button').getAttribute('data-click-trusted')).toBeNull() + now = PAGE_BUDGET_MS + 1 + await page.locator('button').evaluate(button => { + button.textContent = 'Load earlier' + button.removeAttribute('disabled') + }) + const outcome = await operation + if ('error' in outcome) throw outcome.error + expect(outcome.value).toBe(0) + expect(await page.locator('button').getAttribute('data-click-trusted')).toBe('true') + expect(await page.locator(TAIL).count()).toBe(2) + } finally { + try { await browser.close() } finally { + try { await operation } finally { vi.restoreAllMocks() } + } + } +}) + +it('keeps browser deadlines active with a fixed measurement clock', async () => { + const browser = await chromium.launch({ headless: true }) + try { + const page = await browser.newPage() + await expect(measure(page, async () => { + const pending = await page.waitForFunction(() => false, undefined, { timeout: 100 }) + await pending.dispose() + }, () => 0)).rejects.toThrow('Timeout 100ms exceeded') + } finally { + await browser.close() + } +}) + +it('rejects older-page rendering beyond the unchanged paging ceiling', async () => { + const browser = await chromium.launch({ headless: true }) + let operation: Promise<{ value: number } | { error: unknown }> | undefined + try { + const page = await browser.newPage() + await page.setContent('
') + await page.locator('button').evaluate(button => { + button.addEventListener('click', event => { button.setAttribute('data-click-trusted', String(event.isTrusted)) }) + }) + let now = 0 + operation = measureOlderPage(page, 1, () => now).then(value => ({ value }), (error: unknown) => ({ error })) + await page.waitForFunction(() => document.querySelector('button')?.getAttribute('data-click-trusted') === 'true') + now = PAGE_BUDGET_MS + 1 + await page.evaluate(() => { + const tail = document.createElement('div') + tail.setAttribute('data-chat-flow-key', '9:turn-tail:next') + document.body.append(tail) + }) + const outcome = await operation + if ('error' in outcome) throw outcome.error + expect(outcome.value).toBe(PAGE_BUDGET_MS + 1) + expect(() => expectEndpointWithinBudget(outcome.value, PAGE_BUDGET_MS)).toThrow() + } finally { + try { await browser.close() } finally { + try { await operation } finally { vi.restoreAllMocks() } + } + } +}) + +it('rejects ambiguous or replaced older-page buttons before paging', async () => { + const browser = await chromium.launch({ headless: true }) + try { + const page = await browser.newPage() + await page.setContent('') + await expect(measureOlderPage(page, 0)).rejects.toThrow('strict mode violation') + await page.setContent('') + const button = page.getByRole('button', { name: 'Load earlier', exact: true }) + const elementHandles = button.elementHandles.bind(button) + vi.spyOn(page, 'getByRole').mockReturnValue(button) + vi.spyOn(button, 'elementHandles').mockImplementation(async () => { + const handles = await elementHandles() + await button.evaluate(element => { element.replaceWith(element.cloneNode(true)) }) + return handles + }) + await expect(measureOlderPage(page, 0)).rejects.toThrow('Element is not attached') + } finally { + try { await browser.close() } finally { vi.restoreAllMocks() } + } +}) + it('opens, pages, navigates and streams into a 240-turn browser history', async () => { if (webSnapshotMode() !== 'replay') throw new Error('browser benchmarks require keyless replay mode') const samples: { open: number; page: number; trajectory: number; first: number; streamTask: number; streamWall: number; input: number; inputOverlapped: boolean; heapMb: number; nodes: number }[] = [] @@ -165,16 +378,12 @@ it('opens, pages, navigates and streams into a 240-turn browser history', async expect(initialTurns).toBeLessThan(HISTORY_TURNS) let count = initialTurns while (count < HISTORY_TURNS) { - pages.push(await measure(page, async () => { - await page.getByRole('button', { name: 'Load earlier', exact: true }).click() - await page.waitForFunction(({ selector, previous }) => document.querySelectorAll(selector).length > previous, { selector: TAIL, previous: count }) - })) + pages.push(await measureOlderPage(page, count)) count = await page.locator(TAIL).count() } const trajectory = await measure(page, async () => { await page.getByRole('tab', { name: 'Trajectory', exact: true }).click() - await page.getByRole('searchbox', { name: 'Search trajectory', exact: true }).waitFor() - await page.getByRole('row').last().waitFor() + await waitForTrajectory(page) }) await page.getByRole('tab', { name: 'Chat', exact: true }).click() await page.waitForFunction(({ selector, expected }) => document.querySelectorAll(selector).length === expected, { selector: TAIL, expected: HISTORY_TURNS }) @@ -240,5 +449,6 @@ it('opens, pages, navigates and streams into a 240-turn browser history', async open: OPEN_BUDGET_MS, page: PAGE_BUDGET_MS, trajectory: TRAJECTORY_BUDGET_MS, } console.log(JSON.stringify({ benchmark: 'long-session-browser/median', turns: HISTORY_TURNS, deltas: DELTAS, paceMs: PACE_MS, samples, aggregate, referenceMs: REFERENCE, expectedOpenCiMs: EXPECTED_OPEN_CI_MS, expectedPageCiMs: EXPECTED_PAGE_CI_MS, expectedTrajectoryCiMs: EXPECTED_TRAJECTORY_CI_MS, budgets })) + recordTimings(CASE, Object.fromEntries(SCALED_ENDPOINTS.map(key => [key, { ms: aggregate[key]!, ioShare: IO_SHARE }])), budgets) for (const [key, value] of Object.entries(aggregate)) expectEndpointWithinBudget(value, budgets[key]!) }) diff --git a/benchmarks/session-corpus/projection-list.bench.ts b/benchmarks/session-corpus/projection-list.bench.ts index 26af1e52b3..d176b68dd2 100644 --- a/benchmarks/session-corpus/projection-list.bench.ts +++ b/benchmarks/session-corpus/projection-list.bench.ts @@ -6,6 +6,7 @@ import { join } from 'node:path' import { expect, it } from 'vitest' import { runBuiltBenchmarkWorker } from '../support/built-worker.ts' import { ciTimeBudget, PERFORMANCE_BUDGET_HEADROOM } from '../support/calibration.ts' +import { recordTimings } from '../support/scaling-report.ts' import type { ProjectionListReport } from './projection-list.worker.ts' const WORKER = join(import.meta.dirname, '..', '.dsh-build', 'session-corpus', 'projection-list.worker.js') @@ -17,6 +18,10 @@ const LIST_REFERENCE_MS = { modest: 50, tail: 500, cheap: 150 } as const const CALLBACK_REFERENCE_MS = 30 /** Reference retained-heap allowance; memory receives variance headroom but no CPU scaling. */ const RETAINED_HEAP_REFERENCE_BYTES = 240 * 1024 * 1024 +/** Queue delay is time other callbacks wait behind list work on the Host thread, which is CPU work. */ +const CALLBACK_IO_SHARE = 0 +/** Sessions per workload. */ +const SESSIONS = { modest: 50, tail: 300, cheap: 3_000 } as const function median(values: readonly number[]): number { return [...values].sort((left, right) => left - right)[Math.floor(values.length / 2)] as number @@ -68,9 +73,18 @@ it.each(['modest', 'tail', 'cheap'] as const)('serves the %s projection list wit console.log(JSON.stringify({ benchmark: `session-corpus/projection-list-${workload}`, reports, medians, budgets, environment: { node: process.version, platform: process.platform, arch: process.arch, cpu: cpus()[0]?.model } })) + recordTimings({ + id: `session-corpus/projection-list-${workload}`, + measures: `Host Session-list projection and JSON for ${SESSIONS[workload].toLocaleString('en-US')} Sessions, and the longest Host queue delay meanwhile.`, + affects: 'Showing the Session sidebar; a long queue delay stalls other Web GUI requests.', + }, { + firstListAndJsonMs: { ms: medians.firstListAndJsonMs, cpuMs: medians.firstListAndJsonCpuMs }, + repeatListAndJsonMs: { ms: medians.repeatListAndJsonMs, cpuMs: medians.repeatListAndJsonCpuMs }, + worstCallbackDelayMs: { ms: medians.worstCallbackDelayMs, ioShare: CALLBACK_IO_SHARE }, + }, { firstListAndJsonMs: budgets.listAndJsonMs, repeatListAndJsonMs: budgets.listAndJsonMs, worstCallbackDelayMs: budgets.callbackDelayMs }) for (const report of reports) { expect(report.workload).toBe(workload) - expect(report.fixture.sessions).toBe({ modest: 50, tail: 300, cheap: 3_000 }[workload]) + expect(report.fixture.sessions).toBe(SESSIONS[workload]) expect(report.fixture.baseRows).toBe({ modest: 100, tail: 1_000, cheap: 0 }[workload]) expect(report.samples).toHaveLength(4) for (const sample of [...report.samples, report.responsiveness]) { diff --git a/benchmarks/session-corpus/session-corpus.bench.ts b/benchmarks/session-corpus/session-corpus.bench.ts index 1cc7e2b10f..00bc2d2979 100644 --- a/benchmarks/session-corpus/session-corpus.bench.ts +++ b/benchmarks/session-corpus/session-corpus.bench.ts @@ -7,12 +7,14 @@ import { performance } from 'node:perf_hooks' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { runBuiltBenchmarkWorker } from '../support/built-worker.ts' import { PERFORMANCE_BUDGET_HEADROOM } from '../support/calibration.ts' +import { recordPeakMemory, recordTimings, type BenchmarkCase, type Timing } from '../support/scaling-report.ts' import { anchorShape, ANCHOR_COUNT, sessionShape } from './corpus-shape.ts' import { subagentRank } from './synthetic-corpus.ts' import type { AnchorsReport, ForkReport, ListReport, + PhaseCpu, SearchReport, SeedReport, SessionCorpusReport, @@ -69,8 +71,40 @@ const EXPECTED_CI_PEAK_RSS_MB = { fork: 750, } as const satisfies Record +const CASES: Readonly> = { + list: { + id: `session-corpus/list-${String(CORPUS.list)}`, + measures: `Cold Web Host boot, then first and repeated session.list over ${CORPUS.list.toLocaleString('en-US')} Sessions of realistic length.`, + affects: 'Starting DSH and showing the Session list for a heavy user.', + }, + search: { + id: 'session-corpus/search', + measures: `First session.search, which builds the content index over ${CORPUS.search.toLocaleString('en-US')} Sessions, then a second query.`, + affects: 'Searching Session content in deployments that enable search; shipped profiles disable it.', + }, + fork: { + id: 'session-corpus/fork', + measures: `session.fork for ten length strata, the p99 Session, and the longest of ${CORPUS.fork.toLocaleString('en-US')} Sessions.`, + affects: 'Forking a conversation to try another direction.', + }, +} + type CorpusName = keyof typeof CORPUS +/** + * Pair the median wall time of each list phase with the median process CPU time of the same phase. + * @param samples - Wall-clock samples per phase. + * @param cpu - CPU time per sample. + * @returns Endpoints for the scaled report. + */ +function phaseTimings( + samples: Readonly>, + cpu: readonly PhaseCpu[], +): Record { + const phase = (key: keyof PhaseCpu): Timing => ({ ms: median(samples[key]), cpuMs: median(cpu.map(entry => entry[key])) }) + return { bootMs: phase('bootMs'), firstMs: phase('firstMs'), repeatMs: phase('repeatMs') } +} + function topLevelRank(rank: number): number { return subagentRank(rank) ? rank - 1 : rank } @@ -221,6 +255,9 @@ describe('Session corpus operations', () => { heapUsedMb: reports.map(report => report.memory.heapUsedMb), } console.log(JSON.stringify({ benchmark: `session-corpus/list-${String(CORPUS.list)}`, samples, budgets, environment: environment() })) + recordTimings(CASES.list, phaseTimings(samples, reports.map(report => report.cpu)), + { bootMs: budgets.bootMs, firstMs: budgets.firstMs, repeatMs: budgets.repeatMs }) + recordPeakMemory(CASES.list, { peakRssMb: Math.max(...samples.peakRssMb) }) for (const report of reports) expect(report.itemsWithProjections).toBe(CORPUS.list) expectWithinBudget(median(samples.bootMs), budgets.bootMs) expectWithinBudget(median(samples.firstMs), budgets.firstMs) @@ -237,6 +274,11 @@ describe('Session corpus operations', () => { // One sample: the cold index build dominates this file's time. const report = await run([root('search'), 'search']) console.log(JSON.stringify({ benchmark: 'session-corpus/search', report, budgets, environment: environment() })) + recordTimings(CASES.search, { + firstMs: { ms: report.firstMs, cpuMs: report.cpu.firstMs }, + repeatMs: { ms: report.repeatMs, cpuMs: report.cpu.repeatMs }, + }, { firstMs: budgets.firstMs, repeatMs: budgets.repeatMs }) + recordPeakMemory(CASES.search, { peakRssMb: report.memory.peakRssMb }) expectWithinBudget(report.firstMs, budgets.firstMs) expectWithinBudget(report.repeatMs, budgets.repeatMs) expectWithinBudget(report.memory.peakRssMb, budgets.peakRssMb) @@ -254,11 +296,13 @@ describe('Session corpus operations', () => { reports.push(await run([root('fork'), 'fork', ...[...FORK_STRATA, FORK_P99_RANK].map(String)])) } const longest = await run([root('fork'), 'fork', String(FORK_LONGEST_RANK)]) - const forkMs = (report: ForkReport, rank: number): number => { + const forkEntry = (report: ForkReport, rank: number): ForkReport['forks'][number] => { const fork = report.forks.find(entry => entry.rank === rank) if (fork === undefined) throw new Error(`fork report omits rank ${String(rank)}`) - return fork.forkMs + return fork } + const forkMs = (report: ForkReport, rank: number): number => forkEntry(report, rank).forkMs + const forkCpuMs = (report: ForkReport, rank: number): number => forkEntry(report, rank).forkCpuMs const samples = { stratumMedianMs: rounded(reports.map(report => median(FORK_STRATA.map(rank => forkMs(report, rank))))), p99Ms: rounded(reports.map(report => forkMs(report, FORK_P99_RANK))), @@ -267,6 +311,16 @@ describe('Session corpus operations', () => { peakRssMb: [...reports, longest].map(report => report.memory.peakRssMb), } console.log(JSON.stringify({ benchmark: 'session-corpus/fork', samples, budgets, environment: environment() })) + recordTimings(CASES.fork, { + stratumMedianMs: { + ms: median(samples.stratumMedianMs), + cpuMs: median(reports.map(report => median(FORK_STRATA.map(rank => forkCpuMs(report, rank))))), + }, + p99Ms: { ms: median(samples.p99Ms), cpuMs: median(reports.map(report => forkCpuMs(report, FORK_P99_RANK))) }, + longestMs: { ms: samples.longestMs, cpuMs: forkCpuMs(longest, FORK_LONGEST_RANK) }, + }, + { stratumMedianMs: budgets.stratumMedianMs, p99Ms: budgets.p99Ms, longestMs: budgets.longestMs }) + recordPeakMemory(CASES.fork, { peakRssMb: Math.max(...samples.peakRssMb) }) expectWithinBudget(median(samples.stratumMedianMs), budgets.stratumMedianMs) expectWithinBudget(median(samples.p99Ms), budgets.p99Ms) expectWithinBudget(samples.longestMs, budgets.longestMs) diff --git a/benchmarks/session-corpus/session-corpus.worker.ts b/benchmarks/session-corpus/session-corpus.worker.ts index aed7297aa3..8dabac10ca 100644 --- a/benchmarks/session-corpus/session-corpus.worker.ts +++ b/benchmarks/session-corpus/session-corpus.worker.ts @@ -60,12 +60,20 @@ export interface SeedReport { readonly seedMs: number } +/** Process CPU milliseconds, user plus system, over the same intervals as the wall-clock endpoints. */ +export interface PhaseCpu { + readonly bootMs: number + readonly firstMs: number + readonly repeatMs: number +} + /** List report: Host boot, then a cold and a repeated Session list. */ export interface ListReport { readonly mode: 'list' readonly bootMs: number readonly firstMs: number readonly repeatMs: number + readonly cpu: PhaseCpu readonly items: number readonly itemsWithProjections: number readonly memory: CorpusMemory @@ -77,6 +85,7 @@ export interface SearchReport { readonly bootMs: number readonly firstMs: number readonly repeatMs: number + readonly cpu: PhaseCpu readonly firstItems: number readonly repeatItems: number readonly memory: CorpusMemory @@ -90,6 +99,7 @@ export interface ForkReport { readonly rank: number readonly sourceEvents: number readonly forkMs: number + readonly forkCpuMs: number }[] readonly memory: CorpusMemory } @@ -251,16 +261,25 @@ async function seed(root: string, name: string, count: number): Promise { const started = performance.now() + const cpuStarted = cpuMs() const { ctx, controller } = await mountHost(root) try { const booted = performance.now() + const cpuBooted = cpuMs() const signal = new AbortController().signal const first = await controller.list({}, signal) const firstDone = performance.now() + const cpuFirstDone = cpuMs() const repeated = await controller.list({}, signal) const done = performance.now() + const cpuDone = cpuMs() if (first.items.length !== expected || repeated.items.length !== expected) { throw new Error(`Session list returned ${String(first.items.length)} of ${String(expected)} Sessions`) } @@ -269,6 +288,7 @@ async function measureList(root: string, expected: number): Promise bootMs: booted - started, firstMs: firstDone - booted, repeatMs: done - firstDone, + cpu: { bootMs: cpuBooted - cpuStarted, firstMs: cpuFirstDone - cpuBooted, repeatMs: cpuDone - cpuFirstDone }, items: first.items.length, itemsWithProjections: first.items.filter(item => item.projections !== undefined).length, memory: await memory(), @@ -280,14 +300,18 @@ async function measureList(root: string, expected: number): Promise async function measureSearch(root: string): Promise { const started = performance.now() + const cpuStarted = cpuMs() const { ctx, controller } = await mountHost(root) try { const booted = performance.now() + const cpuBooted = cpuMs() const signal = new AbortController().signal const first = await controller.search({ query: SEARCH_QUERIES[0] }, signal) const firstDone = performance.now() + const cpuFirstDone = cpuMs() const repeated = await controller.search({ query: SEARCH_QUERIES[1] }, signal) const done = performance.now() + const cpuDone = cpuMs() if (first.items.length === 0 || repeated.items.length === 0) { throw new Error('Session search matched no synthetic Session') } @@ -296,6 +320,7 @@ async function measureSearch(root: string): Promise { bootMs: booted - started, firstMs: firstDone - booted, repeatMs: done - firstDone, + cpu: { bootMs: cpuBooted - cpuStarted, firstMs: cpuFirstDone - cpuBooted, repeatMs: cpuDone - cpuFirstDone }, firstItems: first.items.length, repeatItems: repeated.items.length, memory: await memory(), @@ -313,11 +338,13 @@ async function measureFork(root: string, ranks: readonly number[]): Promise> = { + 'first-open': 'Opening a large Session for the first time after upgrading DSH.', + 'post-upgrade-reopen': 'Opening a large Session again in a new DSH process.', +} + +/** + * Describe one Session-open case. + * @param access - Access kind and its label. + * @param endpoint - Case suffix and what it measures. + * @returns Case metadata for the scaled report. + */ +function openCase(access: AccessBenchmarkSpec, endpoint: { readonly suffix: string; readonly measures: string }): BenchmarkCase { + return { + id: `session-open/${access.accessKind}/${endpoint.suffix}`, + measures: `${endpoint.measures} One 127,400-event Session, ${access.label}.`, + affects: ACCESS_AFFECTS[access.accessKind], + } +} type WorkerRun = BuiltBenchmarkWorkerRun @@ -393,6 +419,12 @@ describe('opening a large Session for first open and post-upgrade reopen', () => projection: PROJECTION_BUDGET_MS, }, })) + recordTimings(openCase(access, { suffix: 'phases', measures: 'File open, read, Session restore, and projection phases.' }), { + openMs: { ms: result.openMs.median, ioShare: PHASE_IO_SHARE.file }, + readMs: { ms: result.readMs.median, ioShare: PHASE_IO_SHARE.file }, + sessionRestoreMs: { ms: result.sessionRestoreMs.median, ioShare: PHASE_IO_SHARE.memory }, + projectionMs: { ms: result.projectionMs.median, ioShare: PHASE_IO_SHARE.memory }, + }, { openMs: access.openBudgetMs, readMs: READ_BUDGET_MS, sessionRestoreMs: SESSION_RESTORE_BUDGET_MS, projectionMs: PROJECTION_BUDGET_MS }) expectOpenWithinBudget(result.openMs.median, access.openBudgetMs) expect(result.readMs.median).toBeLessThanOrEqual(READ_BUDGET_MS) expect(result.sessionRestoreMs.median).toBeLessThanOrEqual(SESSION_RESTORE_BUDGET_MS) @@ -417,6 +449,9 @@ describe('opening a large Session for first open and post-upgrade reopen', () => result, budgetMs: access.firstHistoryBudgetMs, })) + recordTimings(openCase(access, { suffix: 'first-history', measures: 'Host delivery of the first history snapshot.' }), + { totalMs: { ms: result.totalMs.median, cpuMs: result.cpuUserMs.median + result.cpuSystemMs.median } }, + { totalMs: access.firstHistoryBudgetMs }) expect(result.totalMs.median).toBeLessThanOrEqual(access.firstHistoryBudgetMs) }) @@ -439,6 +474,9 @@ describe('opening a large Session for first open and post-upgrade reopen', () => budgetMs: access.agentResumeBudgetMs, retainedHeapBudgetMb: AGENT_RETAINED_HEAP_BUDGET_MB, })) + recordTimings(openCase(access, { suffix: 'agent-resume', measures: 'Cold Agent resume until the resumed Agent handle returns.' }), + { totalMs: { ms: result.totalMs.median, cpuMs: result.cpuUserMs.median + result.cpuSystemMs.median } }, + { totalMs: access.agentResumeBudgetMs }) expect(result.totalMs.median).toBeLessThanOrEqual(access.agentResumeBudgetMs) expect(result.retainedHeapMb.median).toBeLessThanOrEqual(AGENT_RETAINED_HEAP_BUDGET_MB) }) diff --git a/benchmarks/support/machine-profiles.ts b/benchmarks/support/machine-profiles.ts new file mode 100644 index 0000000000..0ceb3417ba --- /dev/null +++ b/benchmarks/support/machine-profiles.ts @@ -0,0 +1,58 @@ +/** + * Reviewed machine profiles for the scaled-scenario benchmark report. + * + * `cpuScore` is the PassMark single-thread rating retrieved on 2026-10-07 from cpubenchmark.net. Measured + * endpoints run on one JavaScript thread, so the report scales their CPU-bound share by the single-thread + * ratio. `cores` limits endpoints whose measured CPU time shows more than one busy CPU; memory size is not + * modelled. PassMark ratings across ISAs, for burstable or shared vCPUs, and for hybrid efficiency cores, are + * coarse. + * + * `ioLatencyUs` is a typical synchronous 4 KiB random-access latency at queue depth 1 for the profile's + * default storage. Laptop values come from published SSD reviews for the interface generation; cloud values + * come from the provider's documented latency class for its default system disk. They are estimates. + */ + +/** One modelled machine with its CPU and default-storage performance. */ +export interface MachineProfile { + readonly id: string + readonly label: string + readonly cpu: string + readonly cpuScore: number + /** Logical CPUs: hardware threads on laptops, vCPUs on cloud instances. */ + readonly cores: number + readonly storage: string + readonly ioLatencyUs: number + /** Lower-case substrings of `os.cpus()[0].model` that identify the measuring machine as this profile. */ + readonly cpuModelMatches?: readonly string[] +} + +/** Profiles in report order. */ +export const MACHINE_PROFILES: readonly MachineProfile[] = [ + { id: 'laptop-2015-win', label: '2015 Windows laptop', cpu: 'Intel Core i5-5200U', cpuScore: 1486, cores: 4, storage: 'SATA SSD', ioLatencyUs: 90 }, + { id: 'laptop-2015-mac', label: '2015 MacBook Pro 13"', cpu: 'Intel Core i5-5257U', cpuScore: 1737, cores: 4, storage: 'PCIe AHCI SSD', ioLatencyUs: 90 }, + { id: 'laptop-2019-win', label: '2019 Windows laptop', cpu: 'Intel Core i5-8265U', cpuScore: 2003, cores: 8, storage: 'NVMe PCIe 3.0 SSD', ioLatencyUs: 70 }, + { id: 'laptop-2023-win', label: '2023 Windows laptop', cpu: 'Intel Core i5-1335U', cpuScore: 3241, cores: 12, storage: 'NVMe PCIe 4.0 SSD', ioLatencyUs: 55 }, + { id: 'laptop-2023-mac', label: '2023 MacBook Air', cpu: 'Apple M2', cpuScore: 4208, cores: 8, storage: 'Apple SSD', ioLatencyUs: 55, cpuModelMatches: ['apple m2'] }, + { id: 'laptop-2026-win', label: '2026 Windows laptop', cpu: 'Intel Core Ultra 5 225H', cpuScore: 4247, cores: 14, storage: 'NVMe PCIe 4.0 SSD', ioLatencyUs: 50 }, + { id: 'laptop-2026-mac', label: '2026 MacBook', cpu: 'Apple M5', cpuScore: 6341, cores: 10, storage: 'Apple SSD', ioLatencyUs: 50, cpuModelMatches: ['apple m5'] }, + { id: 'aws-t3', label: 'AWS t3.medium', cpu: 'Intel Xeon Platinum 8259CL', cpuScore: 1948, cores: 2, storage: 'EBS gp3', ioLatencyUs: 500 }, + { id: 'aws-m6i', label: 'AWS m6i.large', cpu: 'Intel Xeon Platinum 8375C', cpuScore: 2474, cores: 2, storage: 'EBS gp3', ioLatencyUs: 500 }, + { id: 'aws-m7i', label: 'AWS m7i.large', cpu: 'Intel Xeon Platinum 8488C', cpuScore: 3113, cores: 2, storage: 'EBS gp3', ioLatencyUs: 500 }, + { id: 'aws-m7g', label: 'AWS m7g.large', cpu: 'AWS Graviton3 (Neoverse V1)', cpuScore: 1554, cores: 2, storage: 'EBS gp3', ioLatencyUs: 500 }, + { id: 'gcp-n2', label: 'GCP n2-standard-4', cpu: 'Intel Xeon Platinum 8273CL', cpuScore: 2200, cores: 4, storage: 'pd-balanced', ioLatencyUs: 800 }, + { id: 'azure-d4s-v5', label: 'Azure D4s v5', cpu: 'Intel Xeon Ice Lake (8375C rating)', cpuScore: 2474, cores: 4, storage: 'Premium SSD P10', ioLatencyUs: 1000 }, + { id: 'aliyun-g7', label: 'Alibaba Cloud ecs.g7.xlarge', cpu: 'Intel Xeon Ice Lake (8375C rating)', cpuScore: 2474, cores: 4, storage: 'ESSD PL0', ioLatencyUs: 400 }, + { id: 'github-ubuntu-24.04', label: 'GitHub ubuntu-24.04 runner', cpu: 'AMD EPYC 7763', cpuScore: 2517, cores: 2, storage: 'Azure managed disk', ioLatencyUs: 300, cpuModelMatches: ['epyc 7763'] }, + { id: 'github-ubuntu-24.04-genoa', label: 'GitHub ubuntu-24.04 runner (Genoa)', cpu: 'AMD EPYC 9V74', cpuScore: 2888, cores: 2, storage: 'Azure managed disk', ioLatencyUs: 300, cpuModelMatches: ['epyc 9v74'] }, + { id: 'reference-m4-pro', label: 'Calibration reference', cpu: 'Apple M4 Pro', cpuScore: 4547, cores: 14, storage: 'Apple SSD', ioLatencyUs: 55, cpuModelMatches: ['apple m4'] }, +] + +/** + * Identify the measuring machine among the reviewed profiles. + * @param cpuModel - `os.cpus()[0].model` of the machine that ran the benchmarks. + * @returns the matching profile id, or `undefined` when no profile describes the machine. + */ +export function matchMachineProfile(cpuModel: string): string | undefined { + const model = cpuModel.toLowerCase() + return MACHINE_PROFILES.find(profile => profile.cpuModelMatches?.some(match => model.includes(match)) === true)?.id +} diff --git a/benchmarks/support/scaling-report.ts b/benchmarks/support/scaling-report.ts new file mode 100644 index 0000000000..876e197222 --- /dev/null +++ b/benchmarks/support/scaling-report.ts @@ -0,0 +1,294 @@ +/** + * Collects budgeted benchmark wall-clock endpoints and renders them as a self-contained interactive HTML + * report that rescales each endpoint to the reviewed machine profiles. + * + * Bench files call {@link recordTimings} in the Vitest worker; the Vitest global setup of + * `vitest.bench.config.ts` clears {@link REPORT_DIRECTORY} before the run and renders `report.html` after + * it, including after failed cases. + * + * A scaled endpoint is `ms × ((1 − io) × cpuScore(measured) / cpuScore(target) × coreFactor + io × + * ioLatency(target) / ioLatency(measured))`. When the endpoint reports process CPU time, `io` is the measured + * share of wall time not spent on CPU, `max(0, 1 − cpuMs / ms)`, and its busy parallelism is + * `max(1, cpuMs / ms)`; otherwise `io` is a declared estimate and parallelism is 1. `coreFactor` is + * `max(1, parallelism / cores(target)) / max(1, parallelism / cores(measured))`. Readers can override `io` + * per endpoint. Memory and dimensionless ratios are not scaled. + * + * Bench files also pass peak memory to {@link recordPeakMemory}. The report compares those unscaled peaks + * with 4 GB and 8 GB of available memory; it does not run cases under an operating-system memory limit. + */ + +import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { availableParallelism, cpus } from 'node:os' +import { join } from 'node:path' +import { MACHINE_PROFILES, matchMachineProfile } from './machine-profiles.ts' + +/** Directory owning `results.jsonl` and `report.html`; ignored by Git. */ +export const REPORT_DIRECTORY = join(import.meta.dirname, '..', '.dsh-report') +const RESULTS_PATH = join(REPORT_DIRECTORY, 'results.jsonl') +const REPORT_PATH = join(REPORT_DIRECTORY, 'report.html') + +/** Identity and reader-facing description of one benchmark case, shown when hovering its rows in the report. */ +export interface BenchmarkCase { + /** Case id, such as `session-open/first-open/phases`. */ + readonly id: string + /** What the timed or sampled work is, including workload size. */ + readonly measures: string + /** Which user-visible scenario the measurement represents. */ + readonly affects: string +} + +/** Fields that every record copies from its {@link BenchmarkCase}. */ +interface CaseFields { + readonly benchmark: string + readonly measures: string + readonly affects: string +} + +function caseFields(benchmarkCase: BenchmarkCase): CaseFields { + return { benchmark: benchmarkCase.id, measures: benchmarkCase.measures, affects: benchmarkCase.affects } +} + +/** + * One endpoint: its wall-clock milliseconds and either the process CPU milliseconds measured over the same + * interval or, where the worker cannot measure CPU time, a declared share from 0 to 1 of wall time spent + * waiting on storage. + */ +export type Timing = + | { readonly ms: number; readonly cpuMs: number } + | { readonly ms: number; readonly ioShare: number } + +/** One recorded wall-clock endpoint. */ +export type TimingRecord = CaseFields & Timing & { + readonly kind: 'time' + readonly metric: string + readonly budgetMs?: number +} + +/** + * Append measured endpoints of one benchmark case to the report results. + * @param benchmarkCase - Case id and description. + * @param timings - Endpoints keyed by name; record before asserting budgets. + * @param budgetsMs - CI budgets keyed by the same endpoint names, where the case enforces one. + */ +export function recordTimings( + benchmarkCase: BenchmarkCase, + timings: Readonly>, + budgetsMs: Readonly> = {}, +): void { + appendRecords(Object.entries(timings).map(([metric, timing]): TimingRecord => { + const budgetMs = budgetsMs[metric] + return { kind: 'time', ...caseFields(benchmarkCase), metric, ...timing, ...budgetMs === undefined ? {} : { budgetMs } } + })) +} + +/** One recorded peak memory measurement. */ +export interface MemoryRecord extends CaseFields { + readonly kind: 'memory' + readonly metric: string + readonly mb: number +} + +/** + * Append peak memory of one benchmark case to the report results. + * @param benchmarkCase - Case id and description. + * @param peaksMb - Peak megabytes keyed by measurement name, such as `peakRssMb`. + */ +export function recordPeakMemory(benchmarkCase: BenchmarkCase, peaksMb: Readonly>): void { + appendRecords(Object.entries(peaksMb).map(([metric, mb]): MemoryRecord => ({ kind: 'memory', ...caseFields(benchmarkCase), metric, mb }))) +} + +function appendRecords(records: readonly (TimingRecord | MemoryRecord)[]): void { + mkdirSync(REPORT_DIRECTORY, { recursive: true }) + appendFileSync(RESULTS_PATH, records.map(record => JSON.stringify(record) + '\n').join('')) +} + +/** Vitest global setup: discard results of an earlier run. */ +export function setup(): void { + rmSync(REPORT_DIRECTORY, { recursive: true, force: true }) +} + +/** Vitest global teardown: render the report when any case recorded a timing. */ +export function teardown(): void { + if (!existsSync(RESULTS_PATH)) return + const records = readFileSync(RESULTS_PATH, 'utf8').split('\n').filter(line => line !== '').map(line => JSON.parse(line) as TimingRecord | MemoryRecord) + const cpuModel = cpus()[0]?.model.trim() ?? 'unknown CPU' + writeFileSync(REPORT_PATH, renderReport({ + records, + cpuModel, + platform: `${process.platform}-${process.arch}`, + node: process.version, + revision: process.env['GITHUB_SHA'], + generatedAt: new Date().toISOString(), + cores: availableParallelism(), + measuredProfile: matchMachineProfile(cpuModel), + })) + console.log(`benchmark report: ${REPORT_PATH}`) +} + +/** Inputs embedded in one rendered report. */ +export interface ReportData { + readonly records: readonly (TimingRecord | MemoryRecord)[] + readonly cpuModel: string + readonly platform: string + readonly node: string + readonly revision: string | undefined + readonly generatedAt: string + /** Logical CPUs available to the measuring process; replaces the matched profile's core count. */ + readonly cores: number + /** Profile id describing the measuring machine; the reader chooses one when absent. */ + readonly measuredProfile: string | undefined +} + +/** + * Render the self-contained report page. + * @param data - Recorded endpoints and the measuring environment. + * @returns HTML without external resources. + */ +export function renderReport(data: ReportData): string { + const payload = JSON.stringify({ ...data, profiles: MACHINE_PROFILES }).replaceAll('<', '\\u003c') + return ` + + + + +DeepSeek Harness benchmark scenarios + + + +

DeepSeek Harness benchmark scenarios

+

+

Machines

+

Each measured endpoint is rescaled to other machines. Its CPU share scales by the PassMark single-thread rating ratio and slows further when the endpoint kept more CPUs busy than the target has. Its storage share scales by the typical 4 KiB queue-depth-1 latency ratio of the default disk. Endpoints that report process CPU time use their measured share of wall time not spent on CPU as the storage share; the others use a declared estimate. Profiles are estimates; memory size is not modelled.

+
+ + + + +
+

+ +

< 100 ms< 1 s< 10 s≥ 10 s Hover an endpoint for what it measures; click a row to chart it.

+
+

Memory pressure

+

Measured peak memory of each case against 4 GB and 8 GB of memory available to DeepSeek Harness. Peaks are not scaled. Cases were not run under an operating-system memory limit, so swapping and reclaim costs are not measured.

+ +

< 50%< 75%< 100%≥ 100% of available memory

+
+ + + +` +} diff --git a/benchmarks/terminal-io/terminal-io.bench.ts b/benchmarks/terminal-io/terminal-io.bench.ts index 631a00a24a..bb00ff832c 100644 --- a/benchmarks/terminal-io/terminal-io.bench.ts +++ b/benchmarks/terminal-io/terminal-io.bench.ts @@ -3,6 +3,7 @@ import { join } from 'node:path' import { expect, it } from 'vitest' import { runBuiltBenchmarkWorker } from '../support/built-worker.ts' import { ciTimeBudget } from '../support/calibration.ts' +import { recordTimings, type BenchmarkCase, type Timing } from '../support/scaling-report.ts' import type { TerminalIoReport } from './terminal-io.worker.ts' const MIB = 1024 * 1024 @@ -11,6 +12,18 @@ const WORKER = join(import.meta.dirname, '..', '.dsh-build', 'terminal-io', 'ter /** M5 Pro / Node 26.5 reference expectations, before shared CI scaling and headroom. */ const EXPECTED_MS = { steadyIngest: 20, steadyComplete: 50, fullComplete: 120 } const MAX_CAPACITY_RATIO = 4 +/** Terminal output is buffered in memory; no storage access is timed. */ +const IO_SHARE = 0 +const STEADY_CASE: BenchmarkCase = { + id: 'terminal-io/steady', + measures: 'Terminal ingest and completion of 1 MiB of output into 128 KiB and 4 MiB retained windows.', + affects: 'Commands that keep printing output in the terminal tool.', +} +const FIVE_MIB_CASE: BenchmarkCase = { + id: 'terminal-io/five-mib', + measures: 'Complete terminal send of 5 MiB of output with a 4 MiB retained window.', + affects: 'Commands that print large logs.', +} const MAX_RETAINED_HEAP_BYTES = 16 * MIB function median(values: readonly number[]): number { @@ -38,6 +51,11 @@ it('bounds steady overflow cost as retained terminal capacity grows 32 times', a const ingestBudgetMs = ciTimeBudget(EXPECTED_MS.steadyIngest) const completeBudgetMs = ciTimeBudget(EXPECTED_MS.steadyComplete) console.log(JSON.stringify({ scenario: 'terminal-steady', small, large, capacityRatio, ingestBudgetMs, completeBudgetMs })) + const declared = (ms: number): Timing => ({ ms, ioShare: IO_SHARE }) + recordTimings(STEADY_CASE, { + smallIngestMs: declared(median(small.map(row => row.ingestMs))), largeIngestMs: declared(median(large.map(row => row.ingestMs))), + smallCompleteMs: declared(median(small.map(row => row.completeMs))), largeCompleteMs: declared(median(large.map(row => row.completeMs))), + }, { smallIngestMs: ingestBudgetMs, largeIngestMs: ingestBudgetMs, smallCompleteMs: completeBudgetMs, largeCompleteMs: completeBudgetMs }) expect(capacityRatio).toBeLessThanOrEqual(MAX_CAPACITY_RATIO) for (const rows of [small, large]) { expect(median(rows.map(row => row.ingestMs))).toBeLessThanOrEqual(ingestBudgetMs) @@ -64,6 +82,7 @@ it('completes a five MiB terminal send with bounded retained output', async () = const completeMedianMs = median(samples.map(row => row.completeMs)) const completeBudgetMs = ciTimeBudget(EXPECTED_MS.fullComplete) console.log(JSON.stringify({ scenario: 'terminal-five-mib', samples, completeMedianMs, completeBudgetMs })) + recordTimings(FIVE_MIB_CASE, { completeMs: { ms: completeMedianMs, ioShare: IO_SHARE } }, { completeMs: completeBudgetMs }) expect(completeMedianMs).toBeLessThanOrEqual(completeBudgetMs) expect(Math.max(...samples.map(row => row.retainedHeapBytes))).toBeLessThanOrEqual(MAX_RETAINED_HEAP_BYTES) }) diff --git a/docs/AGENTS.md b/docs/AGENTS.md index b524a84b3e..d4bbea471f 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -29,7 +29,7 @@ Each fact has one home: the tier whose job it is; elsewhere, link there. | [user/](user/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history | | Package README | The per-package contract: config, semantics, limitations, extension points, and [Model Experience](cookbook/adding-a-package.md#4-write-the-package-readme) | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns | | [development.md](development.md) | Contributor setup, daily workflow, and a summary of CI; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ Agent Notes), check-by-check lists that drift from `package.json` scripts | -| Generated reference: the per-page `cordis-surface` regions in [subsystems/](subsystems/README.md), the [Cordis core API + inherited tier](cordis-api/context.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive English sources regenerated from source and freshness-gated; reviewed Chinese counterparts follow the [pairing workflow](i18n/README.md#scope-and-exclusions) | Hand edits to generated English sources or regions; Chinese counterparts update through pairing only | +| Generated reference: the per-page `cordis-surface` regions in [subsystems/](subsystems/README.md), the [Cordis core API + inherited tier](cordis-api/context.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md), [cli-help](cli-help.md) | Exhaustive English sources regenerated from source and freshness-gated; reviewed Chinese counterparts follow the [pairing workflow](i18n/README.md#scope-and-exclusions) | Hand edits to generated English sources or regions; Chinese counterparts update through pairing only | | Skills (`.agents/skills/`) | Reusable workflows and specialized decision standards | Product and runtime contracts (→ docs or source) | Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookbooks; type definitions → subsystems; package contracts → READMEs; standing orders → root `AGENTS.md` with a rationale link. diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 6e8253ffe6..0b096186d7 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -3,32 +3,32 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/architecture.md /deepseek-harness-architecture: - en: 2125f3ec236bc7b2 - zh: 23d8815579592ec4 + en: 20678ed3f962afba + zh: 87a2b81675d93346 /deepseek-harness-architecture/cordis: en: da5060e577578ea7 zh: 2e77d570e32d8938 /deepseek-harness-architecture/profiles-and-bundles: - en: 8ef4912295aa32f3 - zh: cf8848a83743f3cc + en: 21423f481b18b0be + zh: 5343bd7ea0fae5bc /deepseek-harness-architecture/application-launch: en: 334b4f390b6e376b zh: ce094f24c6162be3 /deepseek-harness-architecture/desktop-application: - en: 9f08587366c03567 - zh: bf6abf664869dd88 + en: b9eeb18645121a8b + zh: 70d87a12c43e042a /deepseek-harness-architecture/core-packages: - en: b4a221ac2910600a - zh: 341046d3c9a9c9cd + en: 20ef882e44b06a10 + zh: c10c3934ed5570e9 /deepseek-harness-architecture/events: en: 214c3c81c21df6f9 zh: 75d2cdc3c9fc9930 /deepseek-harness-architecture/turn-flow: - en: badc0ffea030cdce - zh: 3241f0b37c3ec067 + en: e6402b28c0ed15fa + zh: 6c07be9047b42056 /deepseek-harness-architecture/session-log: - en: 82e67cf5b66b8add - zh: 5d0ea37270bdd0d5 + en: a2e35c43438fec6d + zh: e27bdac8e9feff32 /deepseek-harness-architecture/capability-seams: en: 93d1dc2eeda4aec2 zh: ee7a9da89151ea17 diff --git a/docs/architecture.md b/docs/architecture.md index d23d0759f0..51dcfa7e0f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,8 +4,6 @@ English | [中文](architecture.zh.md) Read this before changing anything under `packages/`. It assumes you know Cordis; if you do not, start with the [primer](cordis-primer.md) or the [tutorial](cordis-tutorial/index.md). -We recommend using an agent to explore the codebase and understand its architecture. - ## Cordis [Cordis](cordis-primer.md) is the framework under dsh: plugins contribute services, typed events, and reversible effects to a shared context. Every part of the product is a plugin, including the model adapter, the tool registry, the session log, and the agent loop itself, so each is replaceable from configuration. @@ -24,7 +22,7 @@ Each declares itself in its own `package.json` under a `dsh` field: `dsh.profile [`dsh-base`](../packages/bundle/base/README.md) is the shared first layer of the `web`, `headless`, `sdk`, and `acp` profiles: model adapters, tools, persistence, sandbox and approval policy, settings, credentials, telemetry. [`dsh-web-app`](../packages/bundle/web-app/README.md) adds the browser application, [`dsh-headless`](../packages/bundle/headless/README.md) adds a one-shot runner with no server, [`dsh-sdk-app`](../packages/bundle/sdk-app/README.md) adds the SDK JSON-RPC server, and [`dsh-acp-app`](../packages/bundle/acp-app/README.md) adds the automation-only ACP server. [`dsh-sdk-minimal`](../packages/bundle/sdk-minimal/README.md) is the deliberate exception: one bundle owns its complete explicit SDK tree and does not apply `dsh-base`. -Layers apply to an empty entry list in this order: each bundle in the profile's listed order, then the profile's `cordis.patch.yml`, then the home-level one, then any `--patch` overlay. A patch targets a row by id and replaces its whole config, or inserts new rows. +Layers apply to an empty entry list: the profile's ordered bundles, its `cordis.patch.yml`, the home patch, then `--patch` overlays. A patch targets a row by id and replaces its whole config, or inserts new rows. A `preset` patch applies these operations inside that declaration’s `config.plugins`, with the same layer precedence. YAML controls HMR: base enables config-only `dsh-hmr`; headless, SDK and ACP disable it; `sdk-minimal` omits it. Profile patches override these defaults. HMR coordinates watching and reloads; the launcher provides profile data and readiness. @@ -52,12 +50,10 @@ The Python SDK follows the same application architecture. Its runtime wheel pack The [Electron desktop application](../apps/desktop/README.md) ships its exact dsh production runtime in signed resources and owns the reserved `$DSH_HOME/profiles/desktop`. Shared helpers initialize profile files, reconcile bundles, and resolve installation and bundle dependencies without replacing pnpm-owned packages. Desktop and npm CLI share product data but keep packages, activation, and lockfiles separate. Desktop’s bundled CLI manages its initialized plugins. -Electron starts the private Desktop Host in Electron Node mode. The Host invokes the shared CLI profile runner and complete Web application. The window immediately loads packaged Web assets and waits for boot injections before activating client plugins in the same document. Web owns RPC and streams; the desktop carrier connects the local page to the authenticated Host. Node IPC carries boot injections, readiness, fatal errors, and shutdown. Desktop defaults to port `19387`; profile configuration can override it. Shell-owned UI runs plugin transactions through bundled pnpm with normal user and profile configuration. +Electron starts the private Desktop Host in Electron Node mode. The Host invokes the shared CLI profile runner and complete Web application. The window immediately loads packaged Web assets and waits for boot injections before activating client plugins in the same document. Web owns RPC and streams; the desktop carrier connects the local page to the authenticated Host. Node IPC carries boot injections, readiness, fatal errors, and shutdown. Desktop listens on an OS-assigned port by default; profile configuration can override it. Shell-owned UI runs plugin transactions through bundled pnpm with normal user and profile configuration. ## Core packages -Here are some core packages that contribute to the Cordis tree. - | Package | Owns | `ctx` key | |---|---|---| | [`core/session`](subsystems/session.md) | The append-only `SessionEvent` log and in-memory store | `ctx.sessions` | @@ -67,7 +63,6 @@ Here are some core packages that contribute to the Cordis tree. | [`core/agent-loop`](subsystems/core.md) | The default driver implementing that interface | `ctx.agentLoop` | | [`core/scope`](subsystems/scope.md) | The per-agent scoped-registration primitive | library, no key | | [`llm/llm`](subsystems/llm-streaming.md) | Message and stream vocabulary plus the adapter seam | `ctx.llm` | -| [`webhook/webhook`](subsystems/webhook.md) | Authenticated-delivery dispatch and Workspace Session creation | `ctx.webhookRuntime` | ## Events @@ -88,13 +83,15 @@ A **step** is one model request plus the tools it calls. A **turn** is zero or m ```text turn/start claim next-step input plus one queued message - assemble prompt sections + tool schemas; project runtime context - -> agent/pre-step reject | enter(messages, startsRequestSeries?) + assemble prompt sections + tool schemas + -> agent/pre-step; project runtime context at fallback + reject | enter(messages, startsRequestSeries?) reject, or a first enter rewritten empty -> close the turn with no step step/start agent/request -> prepareCall (cancellation commits neither system nor users) reconcile system/message using the prepared call capability - append entered messages as user/message; log request/header and request/context as needed + append entered messages as user/message; reconcile retained runtime context + log request/header and request/context as needed derive and freeze model history from the log stream the bound prepared call -> llm/stream -> agent/assistant-stream start agent/assistant-stream chunk* @@ -110,7 +107,7 @@ turn/end One inbox feeds the driver; injected context waits for a waking message. AgentLoop’s durable `inbox` projection exposes pending input without live Agents. -`agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, `agent/request` and `prepareCall()` resolve the actual route before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Every attempt synchronously reconciles the same rendered assembly, appends users only on the first attempt, logs header/context as needed, and derives and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The first admitted step reserves the system head before user messages even for an empty prompt (no wire message). The prompt travels only as `system/message` history: an empty rendering clears all active system nodes, leaving no old prompt model-visible; capable routes append non-empty updates after cached history, including supported tool updates; incapable routes and new request series consolidate non-empty prompt text at the first system node, with logged empty replacements for non-empty later system nodes ([decision](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md); [decision rule](../packages/core/agent-loop/README.md#understand-the-implementation)). +`agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, `agent/request` and `prepareCall()` resolve the actual route before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Each attempt refreshes registered runtime facts, reconciles the bound prompt, admits users once, and restores removed runtime context, logs header/context as needed, and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The first admitted step reserves the system head before user messages even for an empty prompt (no wire message). The prompt travels only as `system/message` history: an empty rendering clears all active system nodes, leaving no old prompt model-visible; capable routes append non-empty updates after cached history, including supported tool updates; incapable routes and new request series consolidate non-empty prompt text at the first system node, with logged empty replacements for non-empty later system nodes ([decision](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md); [decision rule](../packages/core/agent-loop/README.md#understand-the-implementation)). The loop sends immutable requests with live cancellation and reuses freeze evidence for frozen identities; [agent-loop](../packages/core/agent-loop/README.md) owns construction and cancellation causes. @@ -128,6 +125,8 @@ Session consumers know only the current logical format. Header-only `stat` and ` **Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events incrementally, host consumers read one typed state with `stateOf()`, and carriers batch cropped client views with `snapshot()`. A host reader either requires this service during activation or fails explicitly when the registry or required key is absent. Contributors may retain `ctx.inject(['sessionProjections'], ...)` registration without silently defaulting a missing host value. The agent loop registers shared `turnBoundary` state for its readers ([decision](../.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md)). +[Working directories](subsystems/working-directory.md) supply user context and execution paths without changing original project identity, sandbox write roots, or existing process directories. + ## Capability seams A **seam** is a swappable capability with three roles: a **Service Definition** declaring the interface, a **Service Provider** implementing it, and a **Consumer** using it, commonly a model-facing tool. A package may combine roles, but one role alone is not a seam; adding a capability means designing all three ([capability graph](capability-seams.md)). diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index bbd8eac74f..8836217bd1 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -4,8 +4,6 @@ 改动 `packages/` 下的任何内容之前,请先阅读本文。本文假定你已了解 Cordis;如果尚未了解,请先阅读[入门](cordis-primer.zh.md)或[教程](cordis-tutorial/index.zh.md)。 -建议使用 agent(智能体)探索代码库并理解其架构。 - ## Cordis [Cordis](cordis-primer.zh.md) 是 dsh 底层的框架:插件向共享上下文贡献服务、类型化事件和可逆的副作用。产品的每一部分都是插件,包括模型适配器、工具注册表、会话日志,以及 agent loop(智能体循环)本身,因此每个都可以从配置替换。 @@ -24,7 +22,7 @@ [`dsh-base`](../packages/bundle/base/README.zh.md) 是 `web`、`headless`、`sdk` 与 `acp` profile 的共享第一层:模型适配器、工具、持久化、沙箱与审批策略、设置、凭据、遥测。[`dsh-web-app`](../packages/bundle/web-app/README.zh.md) 增加浏览器应用,[`dsh-headless`](../packages/bundle/headless/README.zh.md) 增加不带服务器的一次性运行器,[`dsh-sdk-app`](../packages/bundle/sdk-app/README.zh.md) 增加 SDK JSON-RPC 服务器,[`dsh-acp-app`](../packages/bundle/acp-app/README.zh.md) 增加仅用于自动化的 ACP 服务器。[`dsh-sdk-minimal`](../packages/bundle/sdk-minimal/README.zh.md) 是刻意保留的例外:一个组合包拥有完整的显式 SDK 配置树,不应用 `dsh-base`。 -各层按此顺序应用在空条目列表之上:先按 profile 列出的顺序应用每个组合包,然后是 profile 的 `cordis.patch.yml`,然后是 home 级的那份,最后是任意 `--patch` overlay。一条 patch 按 id 定位某个条目并替换其整个 config,或插入新条目。 +各层依次应用在空条目列表之上:profile 中有序的组合包、其 `cordis.patch.yml`、home patch,最后是 `--patch` overlay。一条 patch 按 id 定位某个条目并替换其整个 config,或插入新条目。`preset` patch 在目标声明的 `config.plugins` 内应用这些操作,沿用相同的层优先级。 YAML 控制 HMR:base 启用仅监视配置的 `dsh-hmr`;Headless、SDK 和 ACP 禁用它;`sdk-minimal` 不包含它。Profile patch 覆盖这些默认值。HMR 协调监听和重载;启动器提供 profile 数据和就绪信号。 @@ -52,12 +50,10 @@ Python SDK 遵循相同的应用架构。其运行时 wheel 把普通 `dsh` CLI [Electron 桌面应用](../apps/desktop/README.zh.md)在签名资源中携带精确匹配的 dsh 生产运行时,并拥有保留的 `$DSH_HOME/profiles/desktop`。共享 helper 初始化 profile 文件、协调 bundle,并解析安装与 bundle 的依赖而不替换 pnpm 拥有的包。Desktop 与 npm CLI 共享产品数据,但包、启用选择与锁文件保持独立。Desktop 内置 CLI 管理其已初始化的插件。 -Electron 使用 Electron Node 模式启动私有 Desktop Host。Host 调用共享 CLI profile runner 与完整 Web 应用。窗口立即加载打包 Web 资源,等待启动注入后在同一文档中激活客户端插件。Web 负责 RPC 与流;桌面载体将本地页面连接到已认证的 Host。Node IPC 承载启动注入、就绪、致命错误与关闭。Desktop 默认端口为 `19387`,profile 配置可覆盖。壳拥有的 UI 通过内置 pnpm 执行插件事务,并遵循正常用户与 profile 配置。 +Electron 使用 Electron Node 模式启动私有 Desktop Host。Host 调用共享 CLI profile runner 与完整 Web 应用。窗口立即加载打包 Web 资源,等待启动注入后在同一文档中激活客户端插件。Web 负责 RPC 与流;桌面载体将本地页面连接到已认证的 Host。Node IPC 承载启动注入、就绪、致命错误与关闭。Desktop 默认监听系统分配的端口,profile 配置可覆盖。壳拥有的 UI 通过内置 pnpm 执行插件事务,并遵循正常用户与 profile 配置。 ## 核心包 -以下是向 Cordis 树贡献内容的部分核心包。 - | 包 | 职责 | `ctx` 键 | |---|---|---| | [`core/session`](subsystems/session.zh.md) | 仅追加的 `SessionEvent` 日志和内存存储 | `ctx.sessions` | @@ -67,7 +63,6 @@ Electron 使用 Electron Node 模式启动私有 Desktop Host。Host 调用共 | [`core/agent-loop`](subsystems/core.zh.md) | 实现该接口的默认驱动器 | `ctx.agentLoop` | | [`core/scope`](subsystems/scope.zh.md) | 按 agent 划分作用域的注册原语 | 库,无 ctx 键 | | [`llm/llm`](subsystems/llm-streaming.zh.md) | 消息与流式词汇表,以及适配器 seam | `ctx.llm` | -| [`webhook/webhook`](subsystems/webhook.zh.md) | 已认证 delivery 的分派和 Workspace Session 创建 | `ctx.webhookRuntime` | @@ -92,13 +87,15 @@ AgentLoop 在启动已排队工作前等待串行 `agent/created` 初始化。 ```text turn/start claim next-step input plus one queued message - assemble prompt sections + tool schemas; project runtime context - -> agent/pre-step reject | enter(messages, startsRequestSeries?) + assemble prompt sections + tool schemas + -> agent/pre-step; project runtime context at fallback + reject | enter(messages, startsRequestSeries?) reject, or a first enter rewritten empty -> close the turn with no step step/start agent/request -> prepareCall (cancellation commits neither system nor users) reconcile system/message using the prepared call capability - append entered messages as user/message; log request/header and request/context as needed + append entered messages as user/message; reconcile retained runtime context + log request/header and request/context as needed derive and freeze model history from the log stream the bound prepared call -> llm/stream -> agent/assistant-stream start agent/assistant-stream chunk* @@ -114,7 +111,7 @@ turn/end 输入通过同一个 inbox 到达驱动器;注入的上下文等待一条唤醒消息。AgentLoop 的持久 `inbox` 投影使待处理输入在没有活跃 Agent 时仍可读取。 -`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析实际路由,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试同步协调同一份已渲染组装结果、仅在首次尝试追加用户消息、按需记录 header/context、派生并冻结请求,再通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。首次接纳的步骤在用户消息之前预留系统头节点,即使提示词为空(不产生协议消息)。提示词仅通过 `system/message` 历史传递:空渲染文本清除所有生效的系统节点,模型不再看到旧提示词;具备能力的路由可在缓存前缀之后追加非空更新,包括同时发生的受支持工具更新;不具备能力的路由与新请求序列将非空提示词文本归并到首个系统节点,并为非空的后续系统节点记录空内容替换([决策](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md);[决策规则](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。 +`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析实际路由,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。首次接纳的步骤在用户消息之前预留系统头节点,即使提示词为空(不产生协议消息)。提示词仅通过 `system/message` 历史传递:空渲染文本清除所有生效的系统节点,模型不再看到旧提示词;具备能力的路由可在缓存前缀之后追加非空更新,包括同时发生的受支持工具更新;不具备能力的路由与新请求序列将非空提示词文本归并到首个系统节点,并为非空的后续系统节点记录空内容替换([决策](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md);[决策规则](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。 循环发送不可变请求,同时保持取消有效,仅对完全冻结的对象复用冻结证据;[agent-loop](../packages/core/agent-loop/README.zh.md) 负责请求构建与取消原因。 @@ -132,6 +129,8 @@ Session 消费方只了解当前逻辑格式。仅 header 的 `stat` 与 `list` **投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元增量折叠已提交事件,host 消费方通过 `stateOf()` 读取单个类型化状态,载体通过 `snapshot()` 批量取得裁剪后的客户端视图。host 读取方要么在激活时要求该服务,要么在注册表或必需 key 缺席时明确失败。贡献方可以保留 `ctx.inject(['sessionProjections'], ...)` 注册,但不能为缺失的 host 值静默提供默认值。agent loop 为读取方注册共享的 `turnBoundary` 状态([决策](../.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md))。 +[工作目录](subsystems/working-directory.zh.md) 提供用户上下文和执行路径,不改变原始项目标识、沙箱写入根目录或已有进程目录。 + ## 能力 seam 一个 **seam** 是一项可替换能力,包含三种角色:声明接口的 **Service Definition**、实现它的 **Service Provider**,以及使用它的 **Consumer**(通常是面向模型的工具)。一个包可以合并承担多个角色,但单一角色本身不是 seam;添加一项能力意味着把三者一并设计([能力图](capability-seams.zh.md))。 diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml index 764dbaf986..1c39cb4a31 100644 --- a/docs/capability-seams.i18n.yaml +++ b/docs/capability-seams.i18n.yaml @@ -6,5 +6,5 @@ en: 41d772192075e133 zh: 5ef3da69b4de1d8a /capability-seams-and-core-services: - en: 65f0b4104daac871 - zh: 48bbc8eb309f27d4 + en: 5bdd69045ebd8e3c + zh: f791f38cf578f9c9 diff --git a/docs/capability-seams.md b/docs/capability-seams.md index fd8ac9a534..f240b94937 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -7,6 +7,10 @@ A service can be a core spine service, a swappable capability seam, a bundle/com ```mermaid flowchart LR + pkg_experimental_client_ui_cot_translation["experimental-client-ui-cot-translation"] + svc_cotTranslation["ctx.cotTranslation
Reasoning translation Remote"] + pkg_experimental_translator["experimental-translator"] + svc_translator["ctx.translator
Anonymous text translation"] pkg_hmr["hmr"] svc_hmr["ctx.hmr
Serialized module and configuration reloads"] pkg_app_boot["app-boot"] @@ -20,6 +24,17 @@ flowchart LR svc_connection["ctx.connection
Authenticated browser transport"] pkg_api_gateway["api-gateway"] pkg_host_frontend_static["host-frontend-static"] + pkg_experimental_worktree["experimental-worktree"] + svc_worktrees["ctx.worktrees
Git worktree creation"] + pkg_experimental_tool_worktree["experimental-tool-worktree"] + pkg_working_directory["working-directory"] + svc_workingDirectory["ctx.workingDirectory
Session working directory"] + pkg_tool_working_directory["tool-working-directory"] + pkg_tool_fs["tool-fs"] + pkg_tool_bash["tool-bash"] + pkg_tool_pwsh["tool-pwsh"] + pkg_subagent["subagent"] + pkg_sdk_jsonrpc_server["sdk-jsonrpc-server"] pkg_mcp_resources["mcp-resources"] svc_mcpResources["ctx.mcpResources
Scoped MCP resource access"] pkg_mcp_client["mcp-client"] @@ -39,7 +54,6 @@ flowchart LR svc_attachments["ctx.attachments
Durable binary attachment storage"] pkg_attachment_local["attachment-local"] pkg_api_session_controller["api-session-controller"] - pkg_tool_fs["tool-fs"] pkg_llm_pi_ai["llm-pi-ai"] pkg_llm_deepseek["llm-deepseek"] pkg_client_file_upload["client-file-upload"] @@ -63,7 +77,6 @@ flowchart LR pkg_session_persistence["session-persistence"] pkg_session_query["session-query"] pkg_session_query_sqlite["session-query-sqlite"] - pkg_subagent_in_process_driver["subagent-in-process-driver"] pkg_message_feedback["message-feedback"] pkg_experimental_api_speech_to_text["experimental-api-speech-to-text"] svc_speechController["ctx.speechController
Experimental transcription Remote"] @@ -90,7 +103,6 @@ flowchart LR svc_typertGateway["ctx.typertGateway
Typert Host invocation gateway"] svc_sessionPersistence["ctx.sessionPersistence
Durable session persistence seam"] pkg_session_persistence_jsonl["session-persistence-jsonl"] - pkg_tool_bash["tool-bash"] pkg_hooks_claude_code["hooks-claude-code"] pkg_hooks_codex["hooks-codex"] pkg_config_editor["config-editor"] @@ -197,7 +209,6 @@ flowchart LR pkg_shell["shell"] svc_shell["ctx.shell
Bash executor seam"] pkg_pwsh_local["pwsh-local"] - pkg_tool_pwsh["tool-pwsh"] pkg_shell_env["shell-env"] svc_shellEnv["ctx.shellEnv
Managed bash environment registry"] pkg_terminal["terminal"] @@ -223,7 +234,6 @@ flowchart LR pkg_fs_observation_policy["fs-observation-policy"] pkg_compaction["compaction"] svc_compaction["ctx.compaction
Compaction seam"] - pkg_subagent["subagent"] svc_subagents["ctx.subagents
Subagent provider and continuation service"] pkg_subagent_spawn_in_process["subagent-spawn-in-process"] pkg_subagent_fork_in_process["subagent-fork-in-process"] @@ -236,6 +246,8 @@ flowchart LR pkg_experimental_agent_team["experimental-agent-team"] svc_agentTeams["ctx.agentTeams
Agent Teams coordination domain"] pkg_experimental_tool_agent_team["experimental-tool-agent-team"] + pkg_experimental_claude_code_mods["experimental-claude-code-mods"] + svc_claudeCodeMods["ctx.claudeCodeMods
Claude Code mods bridge"] pkg_inspector["inspector"] svc_inspector["ctx.inspector
Cross-realm runtime inspection"] pkg_jobs["jobs"] @@ -319,11 +331,15 @@ flowchart LR pkg_experimental_browser_use_chrome_devtools_mcp --> svc_browserUse pkg_experimental_browser_use_playwright_mcp --> svc_browserUse pkg_experimental_browser_use_stagehand_native --> svc_browserUse + pkg_experimental_claude_code_mods --> svc_claudeCodeMods + pkg_experimental_client_ui_cot_translation --> svc_cotTranslation pkg_experimental_computer_use_cua_driver_mcp --> svc_computerUse pkg_experimental_computer_use_cua_driver_native --> svc_computerUse pkg_experimental_ptc_runtime_python --> svc_ptcRuntime pkg_experimental_speech_to_text --> svc_speechToText pkg_experimental_speech_to_text_sensevoice --> svc_speechToText + pkg_experimental_translator --> svc_translator + pkg_experimental_worktree --> svc_worktrees pkg_file_reference --> svc_fileReferences pkg_file_reference_local --> svc_fileReferences pkg_fs --> svc_fs @@ -419,6 +435,7 @@ flowchart LR pkg_webhook --> svc_webhookRuntime pkg_workflow --> svc_workflowEngine pkg_workflow_ptc --> svc_workflowEngine + pkg_working_directory --> svc_workingDirectory pkg_workspace --> svc_workspaceRegistry pkg_workspace_changes --> svc_workspaceChanges svc_agentDefaultModel --> pkg_api_session_controller @@ -428,7 +445,7 @@ flowchart LR svc_agentTeams --> pkg_experimental_tool_agent_team svc_agents --> pkg_acp svc_agents --> pkg_agent_loop - svc_agents --> pkg_subagent_in_process_driver + svc_agents --> pkg_subagent svc_approval --> pkg_acp svc_approval --> pkg_tool_bash svc_approval --> pkg_tools @@ -465,7 +482,6 @@ flowchart LR svc_jobs --> pkg_tool_bash svc_jobs --> pkg_tool_jobs svc_jobs --> pkg_tool_pwsh - svc_jobs --> pkg_tool_subagent svc_jobs --> pkg_tool_terminal svc_llm --> pkg_agent_loop svc_llm --> pkg_compaction_basic @@ -506,7 +522,7 @@ flowchart LR svc_sessions --> pkg_session_persistence svc_sessions --> pkg_session_query svc_sessions --> pkg_session_query_sqlite - svc_sessions --> pkg_subagent_in_process_driver + svc_sessions --> pkg_subagent svc_settings --> pkg_api_settings_controller svc_shell --> pkg_hooks_claude_code svc_shell --> pkg_hooks_codex @@ -551,6 +567,7 @@ flowchart LR svc_tools --> pkg_tool_terminal svc_tools --> pkg_tool_todo svc_tools --> pkg_tool_web + svc_translator --> pkg_experimental_client_ui_cot_translation svc_typert --> pkg_api_gateway svc_typert --> pkg_typert_loader svc_userQuestions --> pkg_tool_ask_user @@ -561,18 +578,29 @@ flowchart LR svc_webhookRuntime --> pkg_webhook_github svc_workflowEngine --> pkg_tool_ralph svc_workflowEngine --> pkg_tool_workflow + svc_workingDirectory --> pkg_sdk_jsonrpc_server + svc_workingDirectory --> pkg_subagent + svc_workingDirectory --> pkg_tool_bash + svc_workingDirectory --> pkg_tool_fs + svc_workingDirectory --> pkg_tool_pwsh + svc_workingDirectory --> pkg_tool_working_directory svc_workspaceRegistry --> pkg_api_session_controller svc_workspaceRegistry --> pkg_api_workspace_controller + svc_worktrees --> pkg_experimental_tool_worktree svc_fs -. event gate .-> pkg_fs_observation_policy ``` | ctx key | Role | Owner | Implementations | Direct consumers | Companion plugins | Note | | --- | --- | --- | --- | --- | --- | --- | +| `ctx.cotTranslation` | `service` | [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | - | - | - | Serves reasoning translations, reuses saved results, and joins ordinary Session activation for uncached inactive Sessions. | +| `ctx.translator` | `service` | [`experimental-translator`](../packages/experimental/translator) | - | [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | - | Resolves Google or Bing requests and retains shared experimental records through the active Session writer. | | `ctx.hmr` | `core` | [`hmr`](../packages/boot/hmr) | - | [`app-boot`](../packages/boot/app-boot) | - | Owns module and exact configuration watchers; application mutations share its queue and automatic reloads await the application file lock. | | `ctx.pluginRegistryProbe` | `core` | [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager) | - | [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager) | - | Races public registry responses on the Host; the Client owns the initial registry recommendation. | | `ctx.pluginManager` | `core` | [`plugin-manager`](../packages/boot/plugin-manager) | - | [`plugin-manager`](../packages/boot/plugin-manager), `ui-settings-plugin-inventory` | - | Shares profile package operations with the CLI and reports persisted and running state to Web and agent callers. | | `ctx.profileContext` | `core` | [`app-boot`](../packages/boot/app-boot) | - | [`plugin-manager`](../packages/boot/plugin-manager) | - | The dsh launcher supplies data-only profile locations and composition inputs; reload scheduling belongs to dsh-hmr. | | `ctx.connection` | `core` | [`client-connection`](../packages/client/connection) | - | [`api-gateway`](../packages/api/gateway), [`host-frontend-static`](../packages/host/frontend-static) | - | Owns browser authentication and shared HTTP request dispatch; API adapters register endpoints and streams. | +| `ctx.worktrees` | `seam` | [`experimental-worktree`](../packages/experimental/worktree) | [`experimental-worktree`](../packages/experimental/worktree) | [`experimental-tool-worktree`](../packages/experimental/tool-worktree) | - | Explicit experimental creation from a pinned local commit. Existing write permissions govern checkout and shared Git metadata; the working-directory service owns the resulting Session directory. | +| `ctx.workingDirectory` | `seam` | [`working-directory`](../packages/session/working-directory) | [`working-directory`](../packages/session/working-directory) | [`tool-working-directory`](../packages/session/tool-working-directory), [`tool-fs`](../packages/fs/tool-fs), [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`subagent`](../packages/subagent/subagent), [`sdk-jsonrpc-server`](../packages/sdk/server) | - | One Session projection owns the effective execution directory. The filesystem validates changes; user context reports them while original metadata and write grants stay fixed. | | `ctx.mcpResources` | `seam` | [`mcp-resources`](../packages/mcp/mcp-resources) | [`mcp-client`](../packages/mcp/mcp-client) | [`mcp-resources`](../packages/mcp/mcp-resources) | - | Connection-owned providers serve shared resource tools in the calling agent scope. | | `ctx.browserUse` | `seam` | [`browser-use`](../packages/browser-use/browser-use) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | - | One provider-owned name per service instance. Providers own their tools and browser resources per live Session; the shared service has no browser operation API. | | `ctx.computerUse` | `seam` | [`computer-use`](../packages/computer-use/computer-use) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | - | One provider-owned name per service instance. Each provider also owns its model tools; the service has no common action API, runtime selection, or Session workflow lock. | @@ -583,7 +611,7 @@ flowchart LR | `ctx.deepseekLlmApiExtensions` | `seam` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | [`session-log-deepseek`](../packages/session/session-log-deepseek), [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | [`llm-deepseek`](../packages/llm/llm-deepseek) | - | Plugins prepare independent top-level fields; the official adapter merges them and commits their delivery state after HTTP acceptance. | | `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. | | `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. | -| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`message-feedback`](../packages/feedback/message-feedback) | - | Owns append-only Session instances and emits the durable session event feed. | +| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent`](../packages/subagent/subagent), [`message-feedback`](../packages/feedback/message-feedback) | - | Owns append-only Session instances and emits the durable session event feed. | | `ctx.speechController` | `core` | [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | - | - | - | Validates bounded browser audio before provider dispatch. | | `ctx.sessionController` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | Owns Session commands, cold reads, durable-event following, live control state, model catalogs, workspace opening, and Agent activation policy. | | `ctx.sessionFileReferences` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | Delegates file-reference discovery through the Session Controller's established Agent lookup policy. | @@ -598,7 +626,7 @@ flowchart LR | `ctx.directoryPickerController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | Carries the picking seam onto the wire: capability gating, cancellation, and the seam-coded failures a browser directory flow discriminates on. | | `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges. | | `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier. | -| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/shell/tool-bash), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`message-feedback`](../packages/feedback/message-feedback) | - | The JSONL backend persists the SessionEvent vocabulary as one artifact per Session. | +| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/shell/tool-bash), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`message-feedback`](../packages/feedback/message-feedback) | - | The JSONL backend persists the SessionEvent vocabulary as one artifact per Session. | | `ctx.configEditor` | `core` | [`config-editor`](../packages/boot/config-editor) | - | [`settings`](../packages/settings/settings), [`agent-default-model`](../packages/core/agent-default-model) | - | Persists profile config patches under the application file lock and HMR queue, then reconciles Loader entries. | | `ctx.settings` | `core` | [`settings`](../packages/settings/settings) | - | [`api-settings-controller`](../packages/api/settings-controller) | - | Forms project volatile Config fields from active profile entries and delegate validated edits to config-editor. Plugins consume their own Config references. | | `ctx.subagentModelSelection` | `core` | [`tool-subagent`](../packages/subagent/tool-subagent) | - | [`tool-subagent`](../packages/subagent/tool-subagent) | - | Owns the default-off settings namespace that Agent-scoped delegation tools sample when composing a new top-level Session. | @@ -614,29 +642,29 @@ flowchart LR | `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | Owns per-assistant-message feedback in the canonical Session log, target validation, per-item compare-and-set, and the Host unary Remote contract. Feedback stays outside model history; log export follows the consumer policy. | | `ctx.sessionFeedback` | `core` | [`command-feedback`](../packages/feedback/command-feedback) | - | - | - | Records one Session-level remark with its category as a log-only feedback/record event on a live Session through the Host unary Remote contract; the /feedback command shares the same producer. | | `ctx.workspaceRegistry` | `core` | [`workspace`](../packages/workspace/workspace) | - | [`api-workspace-controller`](../packages/api/workspace-controller), [`api-session-controller`](../packages/api/session-controller) | - | Owns WorkspaceId-branded records over the domain facility; stable sessionIds accounts drive Host RPC and GUI projections. | -| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/session-query/tool-session-query) | - | The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations, while the model consumer owns workspace authority and cursor-free rendering. | +| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/experimental/tool-session-query) | - | The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations, while the model consumer owns workspace authority and cursor-free rendering. | | `ctx.fileReferences` | `seam` | [`file-reference`](../packages/context/file-reference) | [`file-reference-local`](../packages/context/file-reference-local) | [`api-session-controller`](../packages/api/session-controller) | - | The interface returns path-only completion candidates within an Agent cwd; providers own namespace access and ranking without reading file contents. | | `ctx.sessionReferenceResolver` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax. | -| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm), [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | - | - | Owns the deterministic fallback, latest-title fold, and sole optional asynchronous provider registration. | -| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-web`](../packages/web/tool-web) | - | Collects prompt sections and model-facing tool schemas for each step. | -| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/interaction/tool-ask-user), [`tool-bash`](../packages/shell/tool-bash), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web) | - | Registers capabilities, owns PTC mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation. | +| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm), [`session-title-all-prompts-llm`](../packages/experimental/session-title-all-prompts-llm) | - | - | Owns the deterministic fallback, latest-title fold, and sole optional asynchronous provider registration. | +| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-web`](../packages/web/tool-web) | - | Collects prompt sections and model-facing tool schemas for each step. | +| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/interaction/tool-ask-user), [`tool-bash`](../packages/shell/tool-bash), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web) | - | Registers capabilities, owns PTC mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation. | | `ctx.userQuestions` | `seam` | [`user-questions`](../packages/interaction/user-questions) | - | [`tool-ask-user`](../packages/interaction/tool-ask-user) | - | UI front ends provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. | | `ctx.planMode` | `core` | [`plan-mode`](../packages/plan/plan-mode) | - | - | - | Folds logged plan/mode state, flushes user selections at turn boundaries, renders deployment-owned guidance, registers /plan, and keeps the plan-exit schema stable across transitions. | | `ctx.agentPresets` | `core` | [`agent-preset-registry`](../packages/preset/agent-preset-registry) | - | - | - | Eagerly mounts YAML-declared preset revisions, binds Agents and cold readers to scoped contributions, and retains retired revisions until their last user releases them. | | `ctx.commands` | `core` | [`commands`](../packages/interaction/commands) | - | - | - | Plugins register direct human commands without sending invocations to the model. | | `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session/session-projection) | - | [`api-session-controller`](../packages/api/session-controller), [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session/session-title) | - | Domains register state-driven fold units; the eager drive keeps per-session watermark states and the Session controller serves baselines and pushes changed values. | | `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`api-session-controller`](../packages/api/session-controller), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points), serves cached projection views, and accelerates prepared-Session projection hydration. | -| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl), [`skill-badge`](../packages/skill/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. | -| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. | +| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl), [`skill-badge`](../packages/experimental/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. | +| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent`](../packages/subagent/subagent) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. | | `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Reads the default ModelSelection from volatile Config and saves selections through the profile editor. | | `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`base`](../packages/bundle/base), [`sdk-minimal`](../packages/bundle/sdk-minimal) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. | | `ctx.schedule` | `core` | [`schedule`](../packages/schedule/schedule) | - | - | - | Stores tasks independently of Session activation and queues due messages in the original Session. | | `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. | | `ctx.ssh` | `core` | [`ssh`](../packages/ssh/ssh) | - | [`fs-ssh`](../packages/ssh/fs-ssh), [`subprocess-ssh`](../packages/ssh/subprocess-ssh), [`sandbox-ssh`](../packages/ssh/sandbox-ssh) | - | Owns one authenticated OpenSSH connection, installed helper identity, independent program streams and disconnect cleanup for the paired remote providers. | | `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local), [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | [`bash-local`](../packages/shell/bash-local), [`bash-sandbox`](../packages/shell/bash-sandbox), [`terminal-bash`](../packages/terminal/terminal-bash), [`lsp-stdio`](../packages/lsp/lsp-stdio), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | - | The bash executors, the PTY shell backend, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn through ctx.subprocess; the service owns process coordinates, tree/session lifetime, stdio dispositions, terminal mechanics, and kill escalation. | -| `ctx.shell` | `seam` | [`shell`](../packages/shell/shell) | [`bash-local`](../packages/shell/bash-local), [`bash-sandbox`](../packages/shell/bash-sandbox), [`pwsh-local`](../packages/shell/pwsh-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing shell tools and hook bridges consume this seam; sandboxed, remote, or PowerShell executors replace bash-local without touching them. | +| `ctx.shell` | `seam` | [`shell`](../packages/shell/shell) | [`bash-local`](../packages/shell/bash-local), [`bash-sandbox`](../packages/shell/bash-sandbox), [`pwsh-local`](../packages/shell/pwsh-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex) | - | The model-facing shell tools and hook bridges consume this seam; sandboxed, remote, or PowerShell executors replace bash-local without touching them. | | `ctx.shellEnv` | `core` | [`shell-env`](../packages/shell/shell-env) | - | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh) | - | Plugins declare effect-scoped DSH_* facts; each shell tool collects one trusted snapshot per execution and its executor rebuilds the namespace. | -| `ctx.terminals` | `seam` | [`terminal`](../packages/terminal/terminal) | [`terminal-bash`](../packages/terminal/terminal-bash) | [`tool-terminal`](../packages/terminal/tool-terminal) | - | The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-terminal exposes the owner-scoped model tools. | +| `ctx.terminals` | `seam` | [`terminal`](../packages/terminal/terminal) | [`terminal-bash`](../packages/terminal/terminal-bash) | [`tool-terminal`](../packages/experimental/tool-terminal) | - | The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-terminal exposes the owner-scoped model tools. | | `ctx.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local), [`sandbox-ssh`](../packages/ssh/sandbox-ssh) | [`bash-sandbox`](../packages/shell/bash-sandbox), [`terminal-bash`](../packages/terminal/terminal-bash) | - | Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement. | | `ctx.sandboxPolicy` | `core` | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | - | [`bash-sandbox`](../packages/shell/bash-sandbox), [`fs-sandbox`](../packages/fs/fs-sandbox), [`terminal-bash`](../packages/terminal/terminal-bash) | - | The one home for the deployment default mode + workspace root; only the sandboxed executor and provider read the service (the tool layers use the pure `sandbox/mode` fold it also exports). Both enforcing families read it so bash and fs cannot confine to different roots. | | `ctx.approval` | `seam` | [`user-approval`](../packages/interaction/user-approval) | - | [`tools`](../packages/core/tools), [`tool-bash`](../packages/shell/tool-bash), [`acp`](../packages/acp/acp) | - | One-shot permission decisions dispatched over the `approval/request` waterfall; answerers are listeners (the ACP bridge for its own agents), absence fails closed to `unavailable`. | @@ -644,18 +672,19 @@ flowchart LR | `ctx.ptcRuntime` | `seam` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | [`tools`](../packages/core/tools), [`workflow-ptc`](../packages/workflow/workflow-ptc) | - | Runs programs against host-provided async bindings; tools owns PTC presentation and workflow-ptc owns workflow orchestration. | | `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-ssh`](../packages/ssh/fs-ssh) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-observation-policy contributes observed-state checks through the fs/* event gate. | | `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; there is no model-facing compact tool. | -| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route. | +| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/experimental/tool-ralph) | - | Providers prepare local children or execute external tasks; one activation manager owns both lifecycles. The delegation tool selects a backend, control tools deliver local follow-ups, and workflows await activation results. | | `ctx.speechToText` | `seam` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text) | [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | - | Routes explicit recognizers; the browser uses the authenticated Remote and keeps transcripts in the draft until submission. | | `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | - | Owns the implicit-root roster, durable peer mailbox, shared task DAG, and continuable-child lifecycle; tool-agent-team contributes model controls. | +| `ctx.claudeCodeMods` | `core` | [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods) | - | - | - | Loads mods that defineMod plugins add, raises their hook chains from harness extension points, and draws the band above the prompt over its Remote. | | `ctx.inspector` | `core` | `inspector` | - | - | - | Owns the Worker-hosted CDP target and the transport-independent Host and Client observation and Cordis-tree query API. | -| `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`api-job-controller`](../packages/api/job-controller) | - | Producers (background bash/pwsh, PTY sends, and subagent delegations) register running work; record-declaring jobs additionally stream raw output for non-consuming observers; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry. | +| `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-jobs`](../packages/jobs/tool-jobs), [`api-job-controller`](../packages/api/job-controller) | - | Producers (background bash/pwsh and PTY sends) register running work; record-declaring jobs additionally stream raw output for non-consuming observers; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry. | | `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. | | `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. | | `ctx.directoryPicker` | `seam` | [`host-directory-picker`](../packages/host/directory-picker) | [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | [`api-workspace-controller`](../packages/api/workspace-controller) | - | Discriminated interaction capability: the native backend opens one OS chooser on the host display, the browse backend serves listing/creation primitives for the in-app browser; dual-face backends fill ui-workspace directory-flow slots from their browser halves (no wire advertisement). | | `ctx.webServer` | `core` | [`host-webserver`](../packages/host/webserver) | - | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-hmr`](../packages/client/hmr) | - | Plain node:http carrier: named-route registry, index transform taps, and the static dist fallback; web-transport plugins register their own routes. | | `ctx.clientModules` | `core` | [`client-modules`](../packages/client/modules) | - | [`client-hmr`](../packages/client/hmr) | - | Composes the __DSH_BOOT__ entry graph from an incremental dsh.client scan, serves plugin bundles, and notifies rebuilt/graph-changed subscribers. | -| `ctx.workflowEngine` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-ptc`](../packages/workflow/workflow-ptc) | [`tool-workflow`](../packages/workflow/tool-workflow), [`tool-ralph`](../packages/workflow/tool-ralph) | - | One engine per context, as in bash, with no named-provider registry; the general workflow and fixed Ralph consumers start runs whose agent() calls fan out through ctx.subagents. | -| `ctx.webhookRuntime` | `core` | [`webhook`](../packages/webhook/webhook) | - | [`webhook-github`](../packages/webhook/webhook-github) | - | Provider adapters dispatch authenticated deliveries; trusted plugins register independent process-local rules, and the runtime turns non-null results into ordinary Workspace-backed Sessions without delivery or completion state. | +| `ctx.workflowEngine` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-ptc`](../packages/workflow/workflow-ptc) | [`tool-workflow`](../packages/workflow/tool-workflow), [`tool-ralph`](../packages/experimental/tool-ralph) | - | One engine per context, as in bash, with no named-provider registry; the general workflow and fixed Ralph consumers start runs whose agent() calls fan out through ctx.subagents. | +| `ctx.webhookRuntime` | `core` | [`webhook`](../packages/experimental/webhook) | - | [`webhook-github`](../packages/experimental/webhook-github) | - | Provider adapters dispatch authenticated deliveries; trusted plugins register independent process-local rules, and the runtime turns non-null results into ordinary Workspace-backed Sessions without delivery or completion state. | | `ctx.lsp` | `seam` | [`lsp`](../packages/lsp/lsp) | [`lsp-stdio`](../packages/lsp/lsp-stdio) | [`tool-lsp`](../packages/lsp/tool-lsp) | - | Provider registration and selection plus normalized query execution over exactly four operations; the seam offers no protocol escape hatch, so a backend translates into the normalized request and result. | | `ctx.dynamicCordisRunner` | `core` | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | - | [`tool-cordis`](../packages/extensions/tool-cordis) | - | Owns the in-memory definition registry, the vm sandbox for host halves, and the request-run round trip; browser pages reach the same service over the wire through its remote namespace. | | `ctx.cordisInspect` | `core` | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | - | [`tool-cordis`](../packages/extensions/tool-cordis) | - | Registers host inspect providers, mirrors the client provider manifest, and routes client queries through the dynamic Cordis transport. | diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md index 9d96d5dc60..17b488deeb 100644 --- a/docs/capability-seams.zh.md +++ b/docs/capability-seams.zh.md @@ -9,6 +9,10 @@ ```mermaid flowchart LR + pkg_experimental_client_ui_cot_translation["experimental-client-ui-cot-translation"] + svc_cotTranslation["ctx.cotTranslation
Reasoning translation Remote"] + pkg_experimental_translator["experimental-translator"] + svc_translator["ctx.translator
Anonymous text translation"] pkg_hmr["hmr"] svc_hmr["ctx.hmr
Serialized module and configuration reloads"] pkg_app_boot["app-boot"] @@ -22,6 +26,17 @@ flowchart LR svc_connection["ctx.connection
Authenticated browser transport"] pkg_api_gateway["api-gateway"] pkg_host_frontend_static["host-frontend-static"] + pkg_experimental_worktree["experimental-worktree"] + svc_worktrees["ctx.worktrees
Git worktree creation"] + pkg_experimental_tool_worktree["experimental-tool-worktree"] + pkg_working_directory["working-directory"] + svc_workingDirectory["ctx.workingDirectory
Session working directory"] + pkg_tool_working_directory["tool-working-directory"] + pkg_tool_fs["tool-fs"] + pkg_tool_bash["tool-bash"] + pkg_tool_pwsh["tool-pwsh"] + pkg_subagent["subagent"] + pkg_sdk_jsonrpc_server["sdk-jsonrpc-server"] pkg_mcp_resources["mcp-resources"] svc_mcpResources["ctx.mcpResources
Scoped MCP resource access"] pkg_mcp_client["mcp-client"] @@ -41,7 +56,6 @@ flowchart LR svc_attachments["ctx.attachments
Durable binary attachment storage"] pkg_attachment_local["attachment-local"] pkg_api_session_controller["api-session-controller"] - pkg_tool_fs["tool-fs"] pkg_llm_pi_ai["llm-pi-ai"] pkg_llm_deepseek["llm-deepseek"] pkg_client_file_upload["client-file-upload"] @@ -65,7 +79,6 @@ flowchart LR pkg_session_persistence["session-persistence"] pkg_session_query["session-query"] pkg_session_query_sqlite["session-query-sqlite"] - pkg_subagent_in_process_driver["subagent-in-process-driver"] pkg_message_feedback["message-feedback"] pkg_experimental_api_speech_to_text["experimental-api-speech-to-text"] svc_speechController["ctx.speechController
Experimental transcription Remote"] @@ -92,7 +105,6 @@ flowchart LR svc_typertGateway["ctx.typertGateway
Typert Host invocation gateway"] svc_sessionPersistence["ctx.sessionPersistence
Durable session persistence seam"] pkg_session_persistence_jsonl["session-persistence-jsonl"] - pkg_tool_bash["tool-bash"] pkg_hooks_claude_code["hooks-claude-code"] pkg_hooks_codex["hooks-codex"] pkg_config_editor["config-editor"] @@ -199,7 +211,6 @@ flowchart LR pkg_shell["shell"] svc_shell["ctx.shell
Bash executor seam"] pkg_pwsh_local["pwsh-local"] - pkg_tool_pwsh["tool-pwsh"] pkg_shell_env["shell-env"] svc_shellEnv["ctx.shellEnv
Managed bash environment registry"] pkg_terminal["terminal"] @@ -225,7 +236,6 @@ flowchart LR pkg_fs_observation_policy["fs-observation-policy"] pkg_compaction["compaction"] svc_compaction["ctx.compaction
Compaction seam"] - pkg_subagent["subagent"] svc_subagents["ctx.subagents
Subagent provider and continuation service"] pkg_subagent_spawn_in_process["subagent-spawn-in-process"] pkg_subagent_fork_in_process["subagent-fork-in-process"] @@ -238,6 +248,8 @@ flowchart LR pkg_experimental_agent_team["experimental-agent-team"] svc_agentTeams["ctx.agentTeams
Agent Teams coordination domain"] pkg_experimental_tool_agent_team["experimental-tool-agent-team"] + pkg_experimental_claude_code_mods["experimental-claude-code-mods"] + svc_claudeCodeMods["ctx.claudeCodeMods
Claude Code mods bridge"] pkg_inspector["inspector"] svc_inspector["ctx.inspector
Cross-realm runtime inspection"] pkg_jobs["jobs"] @@ -321,11 +333,15 @@ flowchart LR pkg_experimental_browser_use_chrome_devtools_mcp --> svc_browserUse pkg_experimental_browser_use_playwright_mcp --> svc_browserUse pkg_experimental_browser_use_stagehand_native --> svc_browserUse + pkg_experimental_claude_code_mods --> svc_claudeCodeMods + pkg_experimental_client_ui_cot_translation --> svc_cotTranslation pkg_experimental_computer_use_cua_driver_mcp --> svc_computerUse pkg_experimental_computer_use_cua_driver_native --> svc_computerUse pkg_experimental_ptc_runtime_python --> svc_ptcRuntime pkg_experimental_speech_to_text --> svc_speechToText pkg_experimental_speech_to_text_sensevoice --> svc_speechToText + pkg_experimental_translator --> svc_translator + pkg_experimental_worktree --> svc_worktrees pkg_file_reference --> svc_fileReferences pkg_file_reference_local --> svc_fileReferences pkg_fs --> svc_fs @@ -421,6 +437,7 @@ flowchart LR pkg_webhook --> svc_webhookRuntime pkg_workflow --> svc_workflowEngine pkg_workflow_ptc --> svc_workflowEngine + pkg_working_directory --> svc_workingDirectory pkg_workspace --> svc_workspaceRegistry pkg_workspace_changes --> svc_workspaceChanges svc_agentDefaultModel --> pkg_api_session_controller @@ -430,7 +447,7 @@ flowchart LR svc_agentTeams --> pkg_experimental_tool_agent_team svc_agents --> pkg_acp svc_agents --> pkg_agent_loop - svc_agents --> pkg_subagent_in_process_driver + svc_agents --> pkg_subagent svc_approval --> pkg_acp svc_approval --> pkg_tool_bash svc_approval --> pkg_tools @@ -467,7 +484,6 @@ flowchart LR svc_jobs --> pkg_tool_bash svc_jobs --> pkg_tool_jobs svc_jobs --> pkg_tool_pwsh - svc_jobs --> pkg_tool_subagent svc_jobs --> pkg_tool_terminal svc_llm --> pkg_agent_loop svc_llm --> pkg_compaction_basic @@ -508,7 +524,7 @@ flowchart LR svc_sessions --> pkg_session_persistence svc_sessions --> pkg_session_query svc_sessions --> pkg_session_query_sqlite - svc_sessions --> pkg_subagent_in_process_driver + svc_sessions --> pkg_subagent svc_settings --> pkg_api_settings_controller svc_shell --> pkg_hooks_claude_code svc_shell --> pkg_hooks_codex @@ -553,6 +569,7 @@ flowchart LR svc_tools --> pkg_tool_terminal svc_tools --> pkg_tool_todo svc_tools --> pkg_tool_web + svc_translator --> pkg_experimental_client_ui_cot_translation svc_typert --> pkg_api_gateway svc_typert --> pkg_typert_loader svc_userQuestions --> pkg_tool_ask_user @@ -563,18 +580,29 @@ flowchart LR svc_webhookRuntime --> pkg_webhook_github svc_workflowEngine --> pkg_tool_ralph svc_workflowEngine --> pkg_tool_workflow + svc_workingDirectory --> pkg_sdk_jsonrpc_server + svc_workingDirectory --> pkg_subagent + svc_workingDirectory --> pkg_tool_bash + svc_workingDirectory --> pkg_tool_fs + svc_workingDirectory --> pkg_tool_pwsh + svc_workingDirectory --> pkg_tool_working_directory svc_workspaceRegistry --> pkg_api_session_controller svc_workspaceRegistry --> pkg_api_workspace_controller + svc_worktrees --> pkg_experimental_tool_worktree svc_fs -. event gate .-> pkg_fs_observation_policy ``` | ctx 键 | 角色 | 所属包 | 实现 | 直接消费方 | 配套插件 | 说明 | | --- | --- | --- | --- | --- | --- | --- | +| `ctx.cotTranslation` | `service` | [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | - | - | - | 为展开的思考内容提供有界的 Session 翻译请求,不改变模型可见历史。 | +| `ctx.translator` | `service` | [`experimental-translator`](../packages/experimental/translator) | - | [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | - | 解析 Google 或 Bing 请求,并可选保留统一的实验性 Session 记录。 | | `ctx.hmr` | `core` | [`hmr`](../packages/boot/hmr) | - | [`app-boot`](../packages/boot/app-boot) | - | 负责模块和精确配置监听;应用修改共用其队列,自动重载等待应用文件锁。 | | `ctx.pluginRegistryProbe` | `core` | [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager) | - | [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager) | - | 在 Host 上并发比较公共安装源响应;初始安装源推荐由 Client 负责。 | | `ctx.pluginManager` | `core` | [`plugin-manager`](../packages/boot/plugin-manager) | - | [`plugin-manager`](../packages/boot/plugin-manager), `ui-settings-plugin-inventory` | - | 与 CLI 共享 profile 包操作,并向 Web 和 Agent 调用方分别报告持久状态与运行状态。 | | `ctx.profileContext` | `core` | [`app-boot`](../packages/boot/app-boot) | - | [`plugin-manager`](../packages/boot/plugin-manager) | - | dsh launcher 提供纯数据形式的 profile 位置与组合输入;重载调度由 dsh-hmr 负责。 | | `ctx.connection` | `core` | [`client-connection`](../packages/client/connection) | - | [`api-gateway`](../packages/api/gateway), [`host-frontend-static`](../packages/host/frontend-static) | - | 负责浏览器认证与共享 HTTP 请求分发;API 适配器注册端点和流。 | +| `ctx.worktrees` | `seam` | [`experimental-worktree`](../packages/experimental/worktree) | [`experimental-worktree`](../packages/experimental/worktree) | [`experimental-tool-worktree`](../packages/experimental/tool-worktree) | - | 显式实验组合从固定的本地提交创建工作树。检出目录与共享 Git 元数据受现有写权限约束;工作目录服务拥有创建后的 Session 目录。 | +| `ctx.workingDirectory` | `seam` | [`working-directory`](../packages/session/working-directory) | [`working-directory`](../packages/session/working-directory) | [`tool-working-directory`](../packages/session/tool-working-directory), [`tool-fs`](../packages/fs/tool-fs), [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`subagent`](../packages/subagent/subagent), [`sdk-jsonrpc-server`](../packages/sdk/server) | - | 一个 Session 投影拥有有效执行目录。文件系统验证变更,用户上下文报告目录信息,原始元数据与写入授权保持不变。 | | `ctx.mcpResources` | `seam` | [`mcp-resources`](../packages/mcp/mcp-resources) | [`mcp-client`](../packages/mcp/mcp-client) | [`mcp-resources`](../packages/mcp/mcp-resources) | - | 连接所有者提供的操作在调用 agent 的作用域内服务于共享资源工具。 | | `ctx.browserUse` | `seam` | [`browser-use`](../packages/browser-use/browser-use) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | - | 每个服务实例注册一个提供方拥有的名称。提供方按实时 Session 拥有自己的工具与浏览器资源;共享服务不提供浏览器操作 API。 | | `ctx.computerUse` | `seam` | [`computer-use`](../packages/computer-use/computer-use) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | - | 每个服务实例只注册一个提供方自定的名称。各提供方也拥有自己的模型工具;服务不提供通用操作 API、运行时选择或 Session 流程锁。 | @@ -585,7 +613,7 @@ flowchart LR | `ctx.deepseekLlmApiExtensions` | `seam` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | [`session-log-deepseek`](../packages/session/session-log-deepseek), [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | [`llm-deepseek`](../packages/llm/llm-deepseek) | - | 插件准备彼此独立的顶层字段;官方适配器会合并这些字段,并在 HTTP 接受后提交其交付状态。 | | `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 拥有按会话隔离的回放折叠区;压力消费方共享不可变且带修订版本的测量结果。 | | `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 | -| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`message-feedback`](../packages/feedback/message-feedback) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 | +| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent`](../packages/subagent/subagent), [`message-feedback`](../packages/feedback/message-feedback) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 | | `ctx.speechController` | `core` | [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | - | - | - | 在 Provider 调用前校验受限的浏览器音频。 | | `ctx.sessionController` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | 负责 Session 命令、冷读取、持久事件跟随、实时控制状态、模型目录、workspace 打开与 Agent 激活策略。 | | `ctx.sessionFileReferences` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | 通过 Session Controller 的既有 Agent lookup 策略委托文件引用发现。 | @@ -600,7 +628,7 @@ flowchart LR | `ctx.directoryPickerController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | 把选目录 seam 送上线:能力门禁、取消传播,以及浏览器目录流程用于分支判断的 seam 错误码。 | | `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | 插件直接或通过 dsh-typert-loader 注册实时 zod 贡献;API 网关消费调用描述符和提供方,其他运行时消费方则在各自边界查询 schema 与反射元数据。 | | `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | 将生成的 Remote 描述符与实时 Cordis 服务关联,解析已注册的身份,并通过共享的 Connection RPC 载体提供一元调用。 | -| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/shell/tool-bash), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`message-feedback`](../packages/feedback/message-feedback) | - | JSONL backend 把 SessionEvent 词汇持久化为每个 Session 一份产物。 | +| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/shell/tool-bash), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`message-feedback`](../packages/feedback/message-feedback) | - | JSONL backend 把 SessionEvent 词汇持久化为每个 Session 一份产物。 | | `ctx.configEditor` | `core` | [`config-editor`](../packages/boot/config-editor) | - | [`settings`](../packages/settings/settings), [`agent-default-model`](../packages/core/agent-default-model) | - | Persists profile config patches under the application file lock and HMR queue, then reconciles Loader entries. | | `ctx.settings` | `core` | [`settings`](../packages/settings/settings) | - | [`api-settings-controller`](../packages/api/settings-controller) | - | Forms project volatile Config fields from active profile entries and delegate validated edits to config-editor. Plugins consume their own Config references. | | `ctx.subagentModelSelection` | `core` | [`tool-subagent`](../packages/subagent/tool-subagent) | - | [`tool-subagent`](../packages/subagent/tool-subagent) | - | 拥有默认关闭的设置命名空间;Agent 作用域的委派工具会在组合新顶层 Session 时读取它。 | @@ -616,29 +644,29 @@ flowchart LR | `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | 拥有权威 Session 日志中的逐 assistant 消息反馈、目标校验、逐条目 compare-and-set 及 Host 一元 Remote 契约。反馈不进入模型历史;日志导出遵循消费方策略。 | | `ctx.sessionFeedback` | `core` | [`command-feedback`](../packages/feedback/command-feedback) | - | - | - | 通过 Host 一元 Remote 契约在 live Session 上把一条带分类的 Session 级评价记录为仅写日志的 feedback/record 事件;/feedback 命令共用同一个生产方。 | | `ctx.workspaceRegistry` | `core` | [`workspace`](../packages/workspace/workspace) | - | [`api-workspace-controller`](../packages/api/workspace-controller), [`api-session-controller`](../packages/api/session-controller) | - | 通过领域设施拥有带 WorkspaceId 品牌类型的记录;稳定的 sessionIds 账户驱动 Host RPC 与 GUI 投影。 | -| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/session-query/tool-session-query) | - | 该接口提供精确读取、过滤和追踪;具体后端还提供全文协调、排序、摘要片段和游标世代,而模型消费方负责工作区权限与不含游标的渲染。 | +| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/experimental/tool-session-query) | - | 该接口提供精确读取、过滤和追踪;具体后端还提供全文协调、排序、摘要片段和游标世代,而模型消费方负责工作区权限与不含游标的渲染。 | | `ctx.fileReferences` | `seam` | [`file-reference`](../packages/context/file-reference) | [`file-reference-local`](../packages/context/file-reference-local) | [`api-session-controller`](../packages/api/session-controller) | - | 该接口返回 Agent cwd 内仅含路径的补全候选;提供方负责命名空间访问与排序,但不读取文件内容。 | | `ctx.sessionReferenceResolver` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | 将当前表层中有界的对话快照投影为持久但不可信的消息上下文;Host 适配器负责提及语法。 | -| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm), [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | - | - | 负责确定性回退、最新标题折叠区,以及唯一的可选异步提供方注册。 | -| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-web`](../packages/web/tool-web) | - | 为每个步骤收集提示词各部分和面向模型的工具 schema。 | -| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/interaction/tool-ask-user), [`tool-bash`](../packages/shell/tool-bash), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web) | - | 注册能力,负责 PTC mode 传输,并让调用依次经过策略前处理、单调守卫、环绕分派、策略后处理和最终结果观测。 | +| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm), [`session-title-all-prompts-llm`](../packages/experimental/session-title-all-prompts-llm) | - | - | 负责确定性回退、最新标题折叠区,以及唯一的可选异步提供方注册。 | +| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-web`](../packages/web/tool-web) | - | 为每个步骤收集提示词各部分和面向模型的工具 schema。 | +| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/interaction/tool-ask-user), [`tool-bash`](../packages/shell/tool-bash), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web) | - | 注册能力,负责 PTC mode 传输,并让调用依次经过策略前处理、单调守卫、环绕分派、策略后处理和最终结果观测。 | | `ctx.userQuestions` | `seam` | [`user-questions`](../packages/interaction/user-questions) | - | [`tool-ask-user`](../packages/interaction/tool-ask-user) | - | UI 前端提供当前生效的人工回答提供方;tool-ask-user 在提供方无关的 ask() promise 上暂停工具调用。 | | `ctx.planMode` | `core` | [`plan-mode`](../packages/plan/plan-mode) | - | - | - | 折叠已记录的计划/模式状态,在轮次边界刷新用户选择,渲染由部署方拥有的指导信息,注册 /plan,并在状态转换期间保持计划退出 schema 稳定。 | | `ctx.agentPresets` | `core` | [`agent-preset-registry`](../packages/preset/agent-preset-registry) | - | - | - | 立即挂载 YAML 声明的 preset 版本,把 Agent 和冷读取绑定到作用域内的贡献,并保留已替换的版本,直到最后一个使用者释放它。 | | `ctx.commands` | `core` | [`commands`](../packages/interaction/commands) | - | - | - | 插件注册直接面向人的命令,而不会把调用发送给模型。 | | `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session/session-projection) | - | [`api-session-controller`](../packages/api/session-controller), [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session/session-title) | - | 各领域注册由状态驱动的折叠单元;主动驱动过程维护每个会话的水位状态,Session controller 提供 baseline 并推送发生变化的值。 | | `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`api-session-controller`](../packages/api/session-controller), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference) | - | 按会话持久保存投影单元状态的检查点(节流检查点,以及轮次/结束/分离时的必选检查点),提供缓存投影视图,并加速 prepared Session 的投影恢复。 | -| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl), [`skill-badge`](../packages/skill/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | 合并提供方的 skill(技能)目录;tool-skill 渲染会话前缀目录,并加载完整的 skill 正文。 | -| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | - | 拥有实时 Agent 句柄、创建/恢复工厂 seam,以及进程本地的发起方传播。 | +| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl), [`skill-badge`](../packages/experimental/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | 合并提供方的 skill(技能)目录;tool-skill 渲染会话前缀目录,并加载完整的 skill 正文。 | +| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent`](../packages/subagent/subagent) | - | 拥有实时 Agent 句柄、创建/恢复工厂 seam,以及进程本地的发起方传播。 | | `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Reads the default ModelSelection from volatile Config and saves selections through the profile editor. | | `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`base`](../packages/bundle/base), [`sdk-minimal`](../packages/bundle/sdk-minimal) | - | 唯一的具体循环插件;扩展包依赖 dsh-agent 的事件和服务,而不依赖此包。 | | `ctx.schedule` | `core` | [`schedule`](../packages/schedule/schedule) | - | - | - | 独立于 Session 的加载状态存储任务,并将到期消息排入原 Session。 | | `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | 从会话日志折叠带修订版本的目标状态,并将实时延续激活保留在进程本地。 | | `ctx.ssh` | `core` | [`ssh`](../packages/ssh/ssh) | - | [`fs-ssh`](../packages/ssh/fs-ssh), [`subprocess-ssh`](../packages/ssh/subprocess-ssh), [`sandbox-ssh`](../packages/ssh/sandbox-ssh) | - | 负责一条经过认证的 OpenSSH 连接、已安装辅助程序身份、独立程序流,以及配套远端提供方的断连清理。 | | `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local), [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | [`bash-local`](../packages/shell/bash-local), [`bash-sandbox`](../packages/shell/bash-sandbox), [`terminal-bash`](../packages/terminal/terminal-bash), [`lsp-stdio`](../packages/lsp/lsp-stdio), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | - | Bash 执行器、PTY shell 后端、LSP Host,以及进程外 ACP、Codex 和 Claude Code subagent 后端都通过 ctx.subprocess 执行 spawn;该服务负责进程坐标、进程树/会话生命周期、stdio 处置、终端机制和 kill 升级。 | -| `ctx.shell` | `seam` | [`shell`](../packages/shell/shell) | [`bash-local`](../packages/shell/bash-local), [`bash-sandbox`](../packages/shell/bash-sandbox), [`pwsh-local`](../packages/shell/pwsh-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | - | 面向模型的 shell 工具和钩子桥接消费此 seam;沙箱、远程或 PowerShell 执行器可以替换 bash-local,而无需改动这些消费方。 | +| `ctx.shell` | `seam` | [`shell`](../packages/shell/shell) | [`bash-local`](../packages/shell/bash-local), [`bash-sandbox`](../packages/shell/bash-sandbox), [`pwsh-local`](../packages/shell/pwsh-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex) | - | 面向模型的 shell 工具和钩子桥接消费此 seam;沙箱、远程或 PowerShell 执行器可以替换 bash-local,而无需改动这些消费方。 | | `ctx.shellEnv` | `core` | [`shell-env`](../packages/shell/shell-env) | - | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh) | - | 插件声明限定于 effect 作用域的 DSH_* 事实;每个 shell 工具在每次执行时收集一份可信快照,其执行器据此重建命名空间。 | -| `ctx.terminals` | `seam` | [`terminal`](../packages/terminal/terminal) | [`terminal-bash`](../packages/terminal/terminal-bash) | [`tool-terminal`](../packages/terminal/tool-terminal) | - | 注册表负责精确到 Agent 的会话身份和清理;后端负责终端机制,tool-terminal 则提供限定于所有者作用域的模型接口。 | +| `ctx.terminals` | `seam` | [`terminal`](../packages/terminal/terminal) | [`terminal-bash`](../packages/terminal/terminal-bash) | [`tool-terminal`](../packages/experimental/tool-terminal) | - | 注册表负责精确到 Agent 的会话身份和清理;后端负责终端机制,tool-terminal 则提供限定于所有者作用域的模型接口。 | | `ctx.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local), [`sandbox-ssh`](../packages/ssh/sandbox-ssh) | [`bash-sandbox`](../packages/shell/bash-sandbox), [`terminal-bash`](../packages/terminal/terminal-bash) | - | 消费方交出即将执行 spawn 的确切 argv;与配套子进程提供方共享执行环境的后端按每次调用的策略包装该 argv,并报告强制执行情况。 | | `ctx.sandboxPolicy` | `core` | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | - | [`bash-sandbox`](../packages/shell/bash-sandbox), [`fs-sandbox`](../packages/fs/fs-sandbox), [`terminal-bash`](../packages/terminal/terminal-bash) | - | 统一保存部署默认模式和工作区根目录;只有沙箱执行器和提供方读取该服务(工具层使用它同时导出的纯 `sandbox/mode` 折叠区)。两类强制执行组件都读取该服务,因此 bash 与 fs 不会限制到不同的根目录。 | | `ctx.approval` | `seam` | [`user-approval`](../packages/interaction/user-approval) | - | [`tools`](../packages/core/tools), [`tool-bash`](../packages/shell/tool-bash), [`acp`](../packages/acp/acp) | - | 一次性权限决策通过 `approval/request` waterfall(瀑布式事件)分派;回答方是监听器(即 ACP 为自身 agent 提供的桥接),没有回答方时以 `unavailable` 关闭失败。 | @@ -646,18 +674,19 @@ flowchart LR | `ctx.ptcRuntime` | `seam` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | [`tools`](../packages/core/tools), [`workflow-ptc`](../packages/workflow/workflow-ptc) | - | 使用 Host 提供的异步绑定运行程序;tools 负责 PTC 呈现,workflow-ptc 负责工作流编排。 | | `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-ssh`](../packages/ssh/fs-ssh) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | tool-fs 通过 ctx.fs 执行读取/写入/编辑;fs-sandbox 按共享沙箱模式限制变更;fs-observation-policy 通过 fs/* 事件门禁贡献基于观测状态的检查。 | | `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 基础后端消费步骤后的压力事件和请求错误恢复事件;不存在面向模型的压缩工具。 | -| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | 提供方实现传输;该服务还负责可选的、基于 Activation 的延续编排,tool-subagent 选择一次性或可延续委派,tool-subagent-control 传递后续消息,而 tool-ralph 要求一条全新的结构化输出路由。 | +| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/experimental/tool-ralph) | - | 提供方准备本地子级或执行外部任务;一个 activation 管理器持有两类生命周期。委派工具选择后端,控制工具投递本地后续消息,工作流等待 activation 结果。 | | `ctx.speechToText` | `seam` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text) | [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | - | 路由显式选择的识别器;浏览器使用带认证的 Remote,并在提交前将转写保留在草稿中。 | | `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | - | 负责隐式 Root roster、持久 peer mailbox、共享任务 DAG 与 continuable child 生命周期;tool-agent-team 提供模型控制工具。 | +| `ctx.claudeCodeMods` | `core` | [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods) | - | - | - | 加载 defineMod 插件添加的模组,从 harness 扩展点触发其钩子链,并通过其 Remote 绘制提示框上方的横幅。 | | `ctx.inspector` | `core` | `inspector` | - | - | - | 负责 Worker 托管的 CDP target,以及独立于传输的 Host 和 Client observation 与 Cordis tree query API。 | -| `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`api-job-controller`](../packages/api/job-controller) | - | 生产方(后台 bash/pwsh、PTY 发送和 subagent 委派)登记正在运行的工作;声明 record 的 job 还为非消费观察者流式提供原始输出;tool-jobs 是面向模型的控制器,用于读取、列出和终止这些工作;jobs-local 是进程本地注册表。 | +| `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-jobs`](../packages/jobs/tool-jobs), [`api-job-controller`](../packages/api/job-controller) | - | 生产方(后台 bash/pwsh 与 PTY 发送)登记正在运行的工作;声明 record 的 job 还为非消费观察者流式提供原始输出;tool-jobs 是面向模型的控制器,用于读取、列出和终止这些工作;jobs-local 是进程本地注册表。 | | `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | 搜索和抓取提供方注册到同一个 ctx.web seam;tool-web 负责稳定的面向模型名称。 | | `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | 后端保存过大的工具文本,并返回面向模型的定位信息和取回提示;spill-policy 是 tools/post-execute 消费方,负责决定何时 spill。 | | `ctx.directoryPicker` | `seam` | [`host-directory-picker`](../packages/host/directory-picker) | [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | [`api-workspace-controller`](../packages/api/workspace-controller) | - | 带判别标记的交互能力:原生后端在 Host 显示设备上打开一个操作系统选择器,浏览后端为应用内浏览器提供列表与创建原语;双端后端通过其浏览器侧填充 ui-workspace 目录流程的 slot(不通过协议发布)。 | | `ctx.webServer` | `core` | [`host-webserver`](../packages/host/webserver) | - | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-hmr`](../packages/client/hmr) | - | 普通的 node:http 载体:具名路由注册表、索引转换 tap,以及静态 dist 回退;Web 传输插件注册自己的路由。 | | `ctx.clientModules` | `core` | [`client-modules`](../packages/client/modules) | - | [`client-hmr`](../packages/client/hmr) | - | 通过增量 `dsh.client` 扫描组合 __DSH_BOOT__ 入口图,提供插件组合包,并通知重建/图变更订阅方。 | -| `ctx.workflowEngine` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-ptc`](../packages/workflow/workflow-ptc) | [`tool-workflow`](../packages/workflow/tool-workflow), [`tool-ralph`](../packages/workflow/tool-ralph) | - | 每个上下文使用一个引擎,与 bash 相同,且没有具名提供方注册表;通用工作流与固定 Ralph 消费方启动运行,其中的 agent() 调用通过 ctx.subagents 扇出。 | -| `ctx.webhookRuntime` | `core` | [`webhook`](../packages/webhook/webhook) | - | [`webhook-github`](../packages/webhook/webhook-github) | - | 提供方适配器分派已认证交付;可信插件注册独立的进程本地规则,runtime 把非 null 结果转换为普通的 Workspace-backed Session,不保留交付或完成状态。 | +| `ctx.workflowEngine` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-ptc`](../packages/workflow/workflow-ptc) | [`tool-workflow`](../packages/workflow/tool-workflow), [`tool-ralph`](../packages/experimental/tool-ralph) | - | 每个上下文使用一个引擎,与 bash 相同,且没有具名提供方注册表;通用工作流与固定 Ralph 消费方启动运行,其中的 agent() 调用通过 ctx.subagents 扇出。 | +| `ctx.webhookRuntime` | `core` | [`webhook`](../packages/experimental/webhook) | - | [`webhook-github`](../packages/experimental/webhook-github) | - | 提供方适配器分派已认证交付;可信插件注册独立的进程本地规则,runtime 把非 null 结果转换为普通的 Workspace-backed Session,不保留交付或完成状态。 | | `ctx.lsp` | `seam` | [`lsp`](../packages/lsp/lsp) | [`lsp-stdio`](../packages/lsp/lsp-stdio) | [`tool-lsp`](../packages/lsp/tool-lsp) | - | 提供方注册与选择,加上恰好四种操作的标准化查询执行;该 seam 不提供协议逃生口,后端必须转换为标准化请求和结果。 | | `ctx.dynamicCordisRunner` | `core` | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | - | [`tool-cordis`](../packages/extensions/tool-cordis) | - | 拥有内存定义注册表、Host 半的 vm 沙箱和 request-run 往返流程;浏览器页面通过其 Remote 命名空间在线访问同一服务。 | | `ctx.cordisInspect` | `core` | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | - | [`tool-cordis`](../packages/extensions/tool-cordis) | - | 注册 Host inspect 提供方、镜像 Client 提供方 manifest,并通过动态 Cordis 传输路由 Client 查询。 | diff --git a/docs/cli-help.i18n.yaml b/docs/cli-help.i18n.yaml new file mode 100644 index 0000000000..6445218133 --- /dev/null +++ b/docs/cli-help.i18n.yaml @@ -0,0 +1,34 @@ +# Bilingual-pair consistency record for cli-help.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/cli-help.md +/: + en: d11ca88bdc409249 + zh: d11ca88bdc409249 +/dsh-cli-help: + en: b7174eab3476579d + zh: 5ddab967320e3cff +/dsh-cli-help/contents: + en: 00308bcc6dc90afd + zh: 16f7442b76686a02 +/dsh-cli-help/dsh: + en: e0695abcf7d8b521 + zh: 712c23b3c8a115ca +/dsh-cli-help/dsh-plugin: + en: adad99c125d0ac51 + zh: f11024971184f737 +/dsh-cli-help/dsh-acp: + en: bcb0f84577ce5f03 + zh: 4382e617006849db +/dsh-cli-help/dsh-headless: + en: 878a1dd78017964d + zh: bb43d91b4beb9bda +/dsh-cli-help/dsh-sdk: + en: 701f239c5157dedb + zh: ee11be2932b05ce4 +/dsh-cli-help/dsh-sdk-minimal: + en: 8f0d7548d299ffd3 + zh: fa2262e70d6819a0 +/dsh-cli-help/dsh-web: + en: 5d073104e8954cc8 + zh: 91cd28f7c6ac96ac diff --git a/docs/cli-help.md b/docs/cli-help.md new file mode 100644 index 0000000000..4539636c4a --- /dev/null +++ b/docs/cli-help.md @@ -0,0 +1,194 @@ + + +# dsh CLI help + +This reference contains complete help for the launcher, subcommands, and shipped profiles. Identical help appears once with every invocation listed. Help output retains its original language. + +Run `pnpm run gen-cli-help` to regenerate; run `pnpm run verify-cli-help` to check freshness. + +## Contents + +- [`dsh`](#dsh) +- [`dsh plugin`](#dsh-plugin) +- [`dsh acp`](#dsh-acp) +- [`dsh headless`](#dsh-headless) +- [`dsh sdk`](#dsh-sdk) +- [`dsh sdk-minimal`](#dsh-sdk-minimal) +- [`dsh web`](#dsh-web) + + +## `dsh` + +Invocations: `dsh --help` + +```text +Usage: dsh [--profile] [options] [app-args...] + dsh plugin --profile + +dsh: boot a DeepSeek Harness profile — an ordered stack of plugin-bundle patch +layers under your own overrides. + +Arguments: + args arguments for the booted profile's app (see: + dsh --profile --help) + +Options: + -V, --version output the version number + --profile the profile under $DSH_HOME/profiles to boot + --from-default-profile initialize a new custom profile from a shipped + profile template + --patch extra patch-list overlay applied after the + profile layer (repeatable) + --dump-config print the composed profile tree and exit + --dump-config-schema print JSON Schema for profile entries and + patches without mounting + --dump-default-config print the profile tree without its user layer + or --patch overlays and exit + +Examples: + dsh web boot the web profile (same as: dsh --profile web) + dsh rescue --from-default-profile web + create rescue from the shipped web template, then boot it + dsh headless "run the tests" answer one task, print the result, and exit + dsh tui --patch ./extra.yml boot a custom profile with one extra overlay + dsh tui --resume arguments after the launcher flags reach the app + dsh web --help the web app's own flags and help + dsh plugin --profile tui add install a plugin into the tui profile + +``` + + +## `dsh plugin` + +Invocations: `dsh plugin --help` + +```text +Usage: dsh plugin [options] [args...] + +manage a profile's plugins by forwarding the remaining arguments to pnpm in the +profile directory + +Arguments: + args pnpm arguments, forwarded verbatim (add , remove , + why , ...) + +Options: + --profile the profile whose plugins to manage (initialized on first + use) + -h, --help show this help +``` + + +## `dsh acp` + +Invocations: `dsh acp --help`, `dsh --profile acp --help` + +```text +Usage: dsh --profile acp [options] + +Serve automation clients over Agent Client Protocol stdio. + +Options: + -h, --help show this help + +Example: + dsh --profile acp serve ACP until the client disconnects + +``` + + +## `dsh headless` + +Invocations: `dsh headless --help`, `dsh --profile headless --help` + +```text +Usage: dsh --profile headless [options] [task...] + +Answer one task and exit; the answer goes to stdout and diagnostics to stderr. + +Arguments: + task the task text; multiple words are joined by spaces, and `-` + reads stdin + +Options: + --json write newline-delimited run events to stdout instead of the + final message + --session-id adopt the persisted Session with this id; an unknown id is + an error + -h, --help show this help + +Examples: + dsh --profile headless "run the tests" answer one task and exit + echo "run the tests" | dsh --profile headless read the task from stdin + dsh --profile headless --json "run the tests" emit machine-readable run events + dsh --profile headless --session-id session-… "continue" resume an existing Session + +``` + + +## `dsh sdk` + +Invocations: `dsh sdk --help`, `dsh --profile sdk --help` + +```text +Usage: dsh --profile sdk [options] + +Serve DeepSeek Harness SDK clients over stdio JSON-RPC. + +Options: + -h, --help show this help + +Example: + dsh --profile sdk serve one SDK runtime until its client disconnects + +``` + + +## `dsh sdk-minimal` + +Invocations: `dsh sdk-minimal --help`, `dsh --profile sdk-minimal --help` + +```text +Usage: dsh --profile sdk-minimal [options] + +Serve DeepSeek Harness SDK clients over stdio JSON-RPC. + +Options: + -h, --help show this help + +Example: + dsh --profile sdk-minimal serve one SDK runtime until its client disconnects + +``` + + +## `dsh web` + +Invocations: `dsh web --help`, `dsh --profile web --help` + +```text +Usage: dsh --profile web [options] + +Serve the DeepSeek Harness browser UI. + +Options: + --host bind host + --no-open do not open the Web UI in the default browser + --port listen port; pass 0 to let the OS pick a free + one + --public-url advertise this HTTP(S) root in the printed, + opened, web-surface, and DSH_WEB_URL forms; + grants no trust + --trusted-host extra authority the /api browser-trust fence + accepts (host or host:port; repeatable) + -h, --help show this help + +Examples: + dsh --profile web serve on the composed host and port + dsh --profile web --no-open serve without opening a browser + dsh --profile web --port 8080 serve on another port + dsh --profile web --public-url https://app.example/ui/ --trusted-host app.example + advertise a prefix-stripping HTTPS proxy entry and admit its authority + +``` diff --git a/docs/cli-help.zh.md b/docs/cli-help.zh.md new file mode 100644 index 0000000000..f875c988e1 --- /dev/null +++ b/docs/cli-help.zh.md @@ -0,0 +1,196 @@ + + +# dsh 命令行帮助 + +[English](cli-help.md) | 中文 + +此参考收录启动器、子命令和随附 profile 的完整帮助。同一份帮助只展示一次,并列出所有调用方式。帮助输出保留原文。 + +运行 `pnpm run gen-cli-help` 重新生成;运行 `pnpm run verify-cli-help` 检查是否与当前命令一致。 + +## 目录 + +- [`dsh`](#dsh) +- [`dsh plugin`](#dsh-plugin) +- [`dsh acp`](#dsh-acp) +- [`dsh headless`](#dsh-headless) +- [`dsh sdk`](#dsh-sdk) +- [`dsh sdk-minimal`](#dsh-sdk-minimal) +- [`dsh web`](#dsh-web) + + +## `dsh` + +调用方式: `dsh --help` + +```text +Usage: dsh [--profile] [options] [app-args...] + dsh plugin --profile + +dsh: boot a DeepSeek Harness profile — an ordered stack of plugin-bundle patch +layers under your own overrides. + +Arguments: + args arguments for the booted profile's app (see: + dsh --profile --help) + +Options: + -V, --version output the version number + --profile the profile under $DSH_HOME/profiles to boot + --from-default-profile initialize a new custom profile from a shipped + profile template + --patch extra patch-list overlay applied after the + profile layer (repeatable) + --dump-config print the composed profile tree and exit + --dump-config-schema print JSON Schema for profile entries and + patches without mounting + --dump-default-config print the profile tree without its user layer + or --patch overlays and exit + +Examples: + dsh web boot the web profile (same as: dsh --profile web) + dsh rescue --from-default-profile web + create rescue from the shipped web template, then boot it + dsh headless "run the tests" answer one task, print the result, and exit + dsh tui --patch ./extra.yml boot a custom profile with one extra overlay + dsh tui --resume arguments after the launcher flags reach the app + dsh web --help the web app's own flags and help + dsh plugin --profile tui add install a plugin into the tui profile + +``` + + +## `dsh plugin` + +调用方式: `dsh plugin --help` + +```text +Usage: dsh plugin [options] [args...] + +manage a profile's plugins by forwarding the remaining arguments to pnpm in the +profile directory + +Arguments: + args pnpm arguments, forwarded verbatim (add , remove , + why , ...) + +Options: + --profile the profile whose plugins to manage (initialized on first + use) + -h, --help show this help +``` + + +## `dsh acp` + +调用方式: `dsh acp --help`, `dsh --profile acp --help` + +```text +Usage: dsh --profile acp [options] + +Serve automation clients over Agent Client Protocol stdio. + +Options: + -h, --help show this help + +Example: + dsh --profile acp serve ACP until the client disconnects + +``` + + +## `dsh headless` + +调用方式: `dsh headless --help`, `dsh --profile headless --help` + +```text +Usage: dsh --profile headless [options] [task...] + +Answer one task and exit; the answer goes to stdout and diagnostics to stderr. + +Arguments: + task the task text; multiple words are joined by spaces, and `-` + reads stdin + +Options: + --json write newline-delimited run events to stdout instead of the + final message + --session-id adopt the persisted Session with this id; an unknown id is + an error + -h, --help show this help + +Examples: + dsh --profile headless "run the tests" answer one task and exit + echo "run the tests" | dsh --profile headless read the task from stdin + dsh --profile headless --json "run the tests" emit machine-readable run events + dsh --profile headless --session-id session-… "continue" resume an existing Session + +``` + + +## `dsh sdk` + +调用方式: `dsh sdk --help`, `dsh --profile sdk --help` + +```text +Usage: dsh --profile sdk [options] + +Serve DeepSeek Harness SDK clients over stdio JSON-RPC. + +Options: + -h, --help show this help + +Example: + dsh --profile sdk serve one SDK runtime until its client disconnects + +``` + + +## `dsh sdk-minimal` + +调用方式: `dsh sdk-minimal --help`, `dsh --profile sdk-minimal --help` + +```text +Usage: dsh --profile sdk-minimal [options] + +Serve DeepSeek Harness SDK clients over stdio JSON-RPC. + +Options: + -h, --help show this help + +Example: + dsh --profile sdk-minimal serve one SDK runtime until its client disconnects + +``` + + +## `dsh web` + +调用方式: `dsh web --help`, `dsh --profile web --help` + +```text +Usage: dsh --profile web [options] + +Serve the DeepSeek Harness browser UI. + +Options: + --host bind host + --no-open do not open the Web UI in the default browser + --port listen port; pass 0 to let the OS pick a free + one + --public-url advertise this HTTP(S) root in the printed, + opened, web-surface, and DSH_WEB_URL forms; + grants no trust + --trusted-host extra authority the /api browser-trust fence + accepts (host or host:port; repeatable) + -h, --help show this help + +Examples: + dsh --profile web serve on the composed host and port + dsh --profile web --no-open serve without opening a browser + dsh --profile web --port 8080 serve on another port + dsh --profile web --public-url https://app.example/ui/ --trusted-host app.example + advertise a prefix-stripping HTTPS proxy entry and admit its authority + +``` diff --git a/docs/config-catalog.md b/docs/config-catalog.md index bcee8614ed..3de445a55b 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -59,15 +59,13 @@ export interface Config { ## `@deepseek-ai/dsh-agent-instructions` -- `inject`: `sessionProjections` +- `inject`: `sessionProjections` · `workingDirectory` - `source`: [`packages/context/agent-instructions/src/config.ts:18`](../packages/context/agent-instructions/src/config.ts) ```ts config-catalog /** User-facing workspace instruction loader configuration. */ export interface Config { - /** Harness home containing the fixed user-global `AGENTS.md`; defaults to `$DSH_HOME` or `~/.dsh`. */ - dshHome?: string - /** Directory entries that identify the project root while walking upward from the session cwd. */ + /** Directory entries that identify the project root while walking upward from the current working directory. */ projectRootMarkers?: string[] /** UTF-8 byte cap for one rendered baseline or dynamic batch; non-positive or non-finite disables loading. */ maxBytes: number @@ -141,7 +139,7 @@ export type Config = PresetDefinition - `inject`: `loader` · `sessionProjections` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/preset/agent-preset-registry/src/preset.ts:13`](../packages/preset/agent-preset-registry/src/preset.ts) +- `source`: [`packages/preset/agent-preset-registry/src/preset.ts:15`](../packages/preset/agent-preset-registry/src/preset.ts) ```ts config-catalog /** Registry selection policy. */ @@ -168,7 +166,7 @@ export interface Config { export interface Config { /** * The form this agent's model sees. `native` sends every visible schema, - * `ptc` sends only `run_code` plus a generated SDK, `both` sends both. + * `ptc` sends only `run_code` plus a generated SDK. * Required rather than defaulted: the deployment default is what a preset * without this row already gets, so an omitted value would mean the row was * composed for nothing. @@ -256,8 +254,8 @@ export interface SettingsControllerInternals { ## `@deepseek-ai/dsh-api-terminal-controller` -- `inject`: `subprocess` · `sandboxPolicy` · `typert` -- `source`: [`packages/api/terminal-controller/src/index.ts:26`](../packages/api/terminal-controller/src/index.ts) +- `inject`: `subprocess` · `sandboxPolicy` · `workingDirectory` · `typert` +- `source`: [`packages/api/terminal-controller/src/index.ts:27`](../packages/api/terminal-controller/src/index.ts) ```ts config-catalog /** Deployment limits and an optional shell profile. */ @@ -657,7 +655,7 @@ export interface Config { ## `@deepseek-ai/dsh-client-ui-theme` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/client/ui-theme/src/index.ts:22`](../packages/client/ui-theme/src/index.ts) +- `source`: [`packages/client/ui-theme/src/index.ts:20`](../packages/client/ui-theme/src/index.ts) ```ts config-catalog /** Runtime preferences projected to the browser. */ @@ -666,6 +664,16 @@ export interface Config { preference: Volatile /** Browser font size in pixels. */ fontSize: Volatile + /** Code-block font size in pixels. */ + codeFontSize: Volatile + /** Sidebar terminal font size in pixels. */ + terminalFontSize: Volatile + /** Interface and body text font list; `''` keeps the built-in stack. */ + textFontFamily: Volatile + /** Code font list; `''` keeps the built-in stack. */ + codeFontFamily: Volatile + /** Sidebar terminal font list; `''` keeps the built-in stack. */ + terminalFontFamily: Volatile } /** Theme preference persisted by the product Appearance row. */ @@ -874,9 +882,9 @@ export interface Config { ## `@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp` -- `inject`: `browserUse` · `agents` · `tools` · `systemPrompt` +- `inject`: `browserUse` · `agents` · `tools` · `systemPrompt` · `workingDirectory` - `refs`: `BrowserMcpConfig` (`@deepseek-ai/dsh-experimental-browser-use-runtime/mcp`) -- `source`: [`packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts:14`](../packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts) +- `source`: [`packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts:15`](../packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts) ```ts config-catalog /** Fixed Chromium launch or existing-browser attachment settings. */ @@ -889,9 +897,9 @@ export type Config = BrowserMcpConfig ## `@deepseek-ai/dsh-experimental-browser-use-playwright-mcp` -- `inject`: `browserUse` · `agents` · `tools` · `systemPrompt` +- `inject`: `browserUse` · `agents` · `tools` · `systemPrompt` · `workingDirectory` - `refs`: `BrowserMcpConfig` (`@deepseek-ai/dsh-experimental-browser-use-runtime/mcp`) -- `source`: [`packages/experimental/browser-use-playwright-mcp/src/index.ts:15`](../packages/experimental/browser-use-playwright-mcp/src/index.ts) +- `source`: [`packages/experimental/browser-use-playwright-mcp/src/index.ts:16`](../packages/experimental/browser-use-playwright-mcp/src/index.ts) ```ts config-catalog /** Fixed Chromium launch or existing-browser attachment settings. */ @@ -941,6 +949,61 @@ export interface StagehandModelConfig { ``` + + + +## `@deepseek-ai/dsh-experimental-claude-code-mods` + +- `inject`: `workingDirectory` +- `source`: [`packages/experimental/claude-code-mods/src/index.ts:53`](../packages/experimental/claude-code-mods/src/index.ts) + +```ts config-catalog +/** Plugin config: the limits mod hooks run under. */ +export interface Config { + /** A hook's own running-time limit in milliseconds (Claude Code: 10 seconds). */ + hookTimeoutMs?: number + /** A `.catch` handler's running-time limit in milliseconds (Claude Code: 1 second). */ + catchTimeoutMs?: number + /** Default `$.process.run` and `$.http.fetch` timeout in milliseconds (Claude Code: 30 seconds). */ + processTimeoutMs?: number + /** Claude Code tool name → harness tool name entries added to the built-in alias table. */ + toolAliases?: Record + /** Columns the band above the prompt reports to `ui.render` as `bodyColumns` and `viewport.columns`. */ + bandColumns?: number + /** Rows the band reports as `maxRows`. */ + bandRows?: number +} +``` + + + + + +## `@deepseek-ai/dsh-experimental-client-ui-cot-translation` + +- `inject`: `translator` · `typert` · `sessionController` +- `refs`: [`TranslationProvider`](subsystems/translation.md) · `Volatile` (`@deepseek-ai/cordis`) +- `source`: [`packages/experimental/client-ui-cot-translation/src/index.ts:14`](../packages/experimental/client-ui-cot-translation/src/index.ts) + +```ts config-catalog +/** Live preferences presented on the bundle's Plugins page. */ +export interface Config { + /** Explicit translation provider, defaulting to Bing. */ + provider: Volatile + /** Target language code; auto follows the browser UI locale. */ + targetLanguage: Volatile +} + +/** Translation choices; auto targets the active browser UI language. */ +export interface CotTranslationPreferences { + /** Explicit provider selection; requests never fall back to another provider. */ + provider: TranslationProvider + /** BCP 47 language code, or auto to follow the UI language. */ + targetLanguage: string +} +``` + + @@ -1039,7 +1102,8 @@ export interface InspectorOptions { ## `@deepseek-ai/dsh-experimental-ptc-runtime-python` -- `source`: [`packages/experimental/ptc-runtime-python/src/index.ts:42`](../packages/experimental/ptc-runtime-python/src/index.ts) +- `inject`: `sandbox` · `sandboxPolicy` +- `source`: [`packages/experimental/ptc-runtime-python/src/index.ts:45`](../packages/experimental/ptc-runtime-python/src/index.ts) ```ts config-catalog /** Plugin config: every cap, changeable from `cordis.yml` (no hardcoded tunables). */ @@ -1199,13 +1263,81 @@ export interface Config { ``` + + + +## `@deepseek-ai/dsh-experimental-translator` + +- `source`: [`packages/experimental/translator/src/index.ts:25`](../packages/experimental/translator/src/index.ts) + +```ts config-catalog +/** Routing and bounds for anonymous requests and independent native model translation. */ +export interface Config { + /** Provider selected when a consumer omits it. */ + provider: TranslationProvider + /** Google-compatible anonymous translation endpoint. */ + googleEndpoint: string + /** Bing-compatible Microsoft Edge browser translation endpoint. */ + bingEndpoint: string + /** Deadline covering the request and complete response body, in milliseconds. */ + timeoutMs: number + /** Maximum UTF-16 code units per request; at least two to admit one supplementary-plane character. */ + maxTextChars: number + /** Maximum anonymous JSON-body bytes or paid assembled UTF-8 translation bytes. */ + maxResponseBytes: number + /** Deadline for native admission and complete translation output, in milliseconds. */ + deepseekTimeoutMs: number + /** Requested output-token cap for independent native Flash translation. */ + deepseekMaxOutputTokens: number +} + +/** Explicit anonymous endpoint or native paid model route. */ +export type TranslationProvider = AnonymousTranslationProvider | PaidTranslationProvider + +/** Anonymous browser endpoint selected for one translation. */ +export type AnonymousTranslationProvider = 'google' | 'bing' + +/** Explicit native DeepSeek credential route selected for paid translation. */ +export type PaidTranslationProvider = 'deepseek-account' | 'deepseek-official' +``` + + + + + +## `@deepseek-ai/dsh-experimental-worktree` + +- `inject`: `workingDirectory` · `fs` · `subprocess` · `sandbox` · `sandboxPolicy` +- `source`: [`packages/experimental/worktree/src/index.ts:19`](../packages/experimental/worktree/src/index.ts) + +```ts config-catalog +/** Worktree location, generated names, executables, and subprocess resource limits. */ +export interface Config { + /** Relative checkout pool inside the source repository. */ + directory?: string + /** Prefix for automatically generated branch and checkout names. */ + namePrefix?: string + /** Git 2.45 or newer executable name or absolute execution-world path. */ + gitCommand?: string + /** Node executable used for sandboxed directory allocation. */ + nodeCommand?: string + /** Deadline in milliseconds for one command. */ + timeoutMs?: number + /** Subprocess termination grace in milliseconds. */ + graceMs?: number + /** Maximum captured bytes per subprocess output stream. */ + maxOutputBytes?: number +} +``` + + ## `@deepseek-ai/dsh-file-reference-local` -- `inject`: `agents` -- `source`: [`packages/context/file-reference-local/src/index.ts:34`](../packages/context/file-reference-local/src/index.ts) +- `inject`: `agents` · `workingDirectory` +- `source`: [`packages/context/file-reference-local/src/index.ts:35`](../packages/context/file-reference-local/src/index.ts) ```ts config-catalog /** Local file-reference discovery configuration. */ @@ -1284,7 +1416,7 @@ export interface Config { ## `@deepseek-ai/dsh-headless` - `inject`: `agentDefaultModel` · `agents` · `sessions` -- `source`: [`packages/bundle/headless/src/index.ts:42`](../packages/bundle/headless/src/index.ts) +- `source`: [`packages/bundle/headless/src/index.ts:43`](../packages/bundle/headless/src/index.ts) ```ts config-catalog /** Plugin config: the task and run options resolved from this app's injected provider service. */ @@ -1305,7 +1437,7 @@ export interface Config { ## `@deepseek-ai/dsh-hmr` - `refs`: `ChokidarOptions` (`chokidar`) -- `source`: [`packages/boot/hmr/src/index.ts:51`](../packages/boot/hmr/src/index.ts) +- `source`: [`packages/boot/hmr/src/index.ts:53`](../packages/boot/hmr/src/index.ts) ```ts config-catalog /** Module roots and watcher timing, with Chokidar deployment options. */ @@ -1327,8 +1459,8 @@ export interface HmrConfig extends ChokidarOptions { ## `@deepseek-ai/dsh-hooks-claude-code` -- `inject`: `shell` · `sessionProjections` -- `source`: [`packages/hooks/hooks-claude-code/src/index.ts:51`](../packages/hooks/hooks-claude-code/src/index.ts) +- `inject`: `shell` · `sessionProjections` · `workingDirectory` +- `source`: [`packages/experimental/hooks-claude-code/src/index.ts:51`](../packages/experimental/hooks-claude-code/src/index.ts) ```ts config-catalog /** Plugin config: where the CC hook config lives + substitution roots. */ @@ -1348,7 +1480,7 @@ export interface Config { /** * Replaces `${CLAUDE_PROJECT_DIR}` in command strings AND is exported as the * `CLAUDE_PROJECT_DIR` env var for hook processes. When omitted, the env var - * defaults per-run to the agent's session workspace (`session.header.cwd`, the + * defaults per-run to the agent's current directory (the * same dir the hook runs in) — Claude Code always exports this var, and common * unmodified hooks reference `$CLAUDE_PROJECT_DIR` for project-relative paths. */ @@ -1366,8 +1498,8 @@ export interface Config { ## `@deepseek-ai/dsh-hooks-codex` -- `inject`: `shell` · `sessionProjections` -- `source`: [`packages/hooks/hooks-codex/src/index.ts:50`](../packages/hooks/hooks-codex/src/index.ts) +- `inject`: `shell` · `sessionProjections` · `workingDirectory` +- `source`: [`packages/experimental/hooks-codex/src/index.ts:50`](../packages/experimental/hooks-codex/src/index.ts) ```ts config-catalog /** Plugin config: where the Codex hooks.json lives + the model name for payloads. */ @@ -1918,7 +2050,7 @@ export interface ReplayModelConfig { * no image pricing. */ imageRequestTokens?: number - /** Optional reasoning-effort ids the replay route accepts, in display order. */ + /** Optional reasoning-effort ids from least to greatest selectable effort. */ reasoningEfforts?: string[] /** * Optional effort materialized when callers omit one; must appear in @@ -2230,7 +2362,7 @@ export interface PlanModeConfig { ## `@deepseek-ai/dsh-plugin-manager` - `inject`: `loader` · `profileContext` -- `source`: [`packages/boot/plugin-manager/src/index.ts:40`](../packages/boot/plugin-manager/src/index.ts) +- `source`: [`packages/boot/plugin-manager/src/index.ts:44`](../packages/boot/plugin-manager/src/index.ts) ```ts config-catalog /** The pnpm executable, registries, and limits for diagnostics, lookups and connection checks. */ @@ -2516,9 +2648,9 @@ export interface Config { ## `@deepseek-ai/dsh-sdk-jsonrpc-server` -- `inject`: `agents` +- `inject`: `agents` · `workingDirectory` - `refs`: `Readable` (`node:stream`) · `Writable` (`node:stream`) -- `source`: [`packages/sdk/server/src/index.ts:25`](../packages/sdk/server/src/index.ts) +- `source`: [`packages/sdk/server/src/index.ts:24`](../packages/sdk/server/src/index.ts) ```ts config-catalog /** JSON-RPC deployment config plus runtime-only test hooks. */ @@ -2708,8 +2840,8 @@ export interface Config { ## `@deepseek-ai/dsh-session-telemetry-otel` - `inject`: `sessions` · `otel` -- `refs`: `BatchLogRecordProcessorOptions` (`@opentelemetry/sdk-logs`) · `OTLPExporterNodeConfigBase` (`@opentelemetry/otlp-exporter-base`) -- `source`: [`packages/session/session-telemetry-otel/src/index.ts:87`](../packages/session/session-telemetry-otel/src/index.ts) +- `refs`: [`LogExporterOptions`](../packages/telemetry/otel/src/index.ts) · [`SessionLogProcessorOptions`](../packages/telemetry/otel/src/index.ts) +- `source`: [`packages/session/session-telemetry-otel/src/index.ts:85`](../packages/session/session-telemetry-otel/src/index.ts) ```ts config-catalog /** @@ -2723,16 +2855,18 @@ export interface Config { /** * Explicit SDK HTTP transport settings, including optional routing headers. * Ambient credentials are not inherited. URL is required while uploading. + * Exporter self-observability metering is not supported. */ - exporter?: OTLPExporterNodeConfigBase & { + exporter?: Omit & { /** Full logs endpoint (e.g. `https://collector.example.com/v1/logs`). Required outside `DISABLED`; validated at load. */ url?: string } /** * Count, queue, cadence, and per-request watchdog settings for the byte-bounded * processor. A watchdog warning never releases an unsettled transport slot. + * SDK processor self-observability metering is not supported. */ - processor?: Omit + processor?: SessionLogProcessorOptions /** Maximum time spent awaiting the SDK provider's complete shutdown path. */ shutdownTimeoutMillis?: number /** Uncompressed OTLP request byte limit, at most 4,000,000. */ @@ -2775,7 +2909,7 @@ export interface Config { - `inject`: `sessionTitle` · `llm` · `sessions` - `refs`: [`SessionTitleLlmConfig`](../packages/session/session-title-llm/src/index.ts) -- `source`: [`packages/session/session-title-all-prompts-llm/src/index.ts:15`](../packages/session/session-title-all-prompts-llm/src/index.ts) +- `source`: [`packages/experimental/session-title-all-prompts-llm/src/index.ts:15`](../packages/experimental/session-title-all-prompts-llm/src/index.ts) ```ts config-catalog /** Required LLM policy; this plugin adds no defaults. */ @@ -2836,7 +2970,7 @@ export interface Config { ## `@deepseek-ai/dsh-skill-filesystem` - `inject`: `skills` -- `source`: [`packages/skill/skill-filesystem/src/index.ts:49`](../packages/skill/skill-filesystem/src/index.ts) +- `source`: [`packages/skill/skill-filesystem/src/index.ts:48`](../packages/skill/skill-filesystem/src/index.ts) ```ts config-catalog /** Local filesystem skill provider configuration. */ @@ -3066,8 +3200,9 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' ## `@deepseek-ai/dsh-subagent` +- `inject`: `workingDirectory` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/subagent/subagent/src/index.ts:190`](../packages/subagent/subagent/src/index.ts) +- `source`: [`packages/subagent/subagent/src/index.ts:184`](../packages/subagent/subagent/src/index.ts) ```ts config-catalog /** Host configuration for continuable subagent capacity. */ @@ -3086,7 +3221,7 @@ export interface Config { ## `@deepseek-ai/dsh-subagent-acp` - `inject`: `subagents` · `subprocess` -- `source`: [`packages/subagent/subagent-acp/src/index.ts:27`](../packages/subagent/subagent-acp/src/index.ts) +- `source`: [`packages/subagent/subagent-acp/src/index.ts:25`](../packages/subagent/subagent-acp/src/index.ts) ```ts config-catalog /** Config: how to spawn and drive the child ACP agent process. */ @@ -3097,14 +3232,6 @@ export interface Config { command: string /** Arguments passed to {@link command}. */ args: string[] - /** - * Working directory override for the child process and its ACP session. - * Must be non-empty; a relative path resolves against the harness launch - * directory at load, and the result must be an existing directory. When - * omitted, each child inherits its delegating parent session's cwd — and - * starting one from a parent session that has no cwd fails. - */ - cwd?: string /** * How to auto-answer the child's `session/request_permission` prompts: * `reject` (default — decline every prompt) or `allow` (approve via the first @@ -3225,15 +3352,6 @@ export interface Config { patches: string[] /** Absolute isolated Harness home for every nested child process. */ dshHome: string - /** - * Working directory override for the child process and its SDK session - * workspace. Must be non-empty; a relative path resolves against the - * harness launch directory at load, and the result must be an existing - * directory. When omitted, each child inherits its delegating parent - * session's cwd — and starting one from a parent session that has no cwd - * fails. - */ - cwd?: string /** Provider route the child runtime initializes with (default `deepseek-official`). */ provider: string /** Model the child runtime initializes with (default `deepseek-v4-flash`). */ @@ -3267,7 +3385,7 @@ export interface Config { ## `@deepseek-ai/dsh-subagent-fork-in-process` - `inject`: `subagents` -- `source`: [`packages/subagent/subagent-fork-in-process/src/index.ts:31`](../packages/subagent/subagent-fork-in-process/src/index.ts) +- `source`: [`packages/subagent/subagent-fork-in-process/src/index.ts:29`](../packages/subagent/subagent-fork-in-process/src/index.ts) ```ts config-catalog /** Config: the registry name to register the provider under. */ @@ -3284,7 +3402,7 @@ export interface Config { ## `@deepseek-ai/dsh-subagent-spawn-in-process` - `inject`: `subagents` -- `source`: [`packages/subagent/subagent-spawn-in-process/src/index.ts:25`](../packages/subagent/subagent-spawn-in-process/src/index.ts) +- `source`: [`packages/subagent/subagent-spawn-in-process/src/index.ts:23`](../packages/subagent/subagent-spawn-in-process/src/index.ts) ```ts config-catalog /** Config: the registry name to register the provider under. */ @@ -3300,14 +3418,14 @@ export interface Config { ## `@deepseek-ai/dsh-system-prompt` -- `source`: [`packages/core/system-prompt/src/index.ts:247`](../packages/core/system-prompt/src/index.ts) +- `source`: [`packages/core/system-prompt/src/index.ts:254`](../packages/core/system-prompt/src/index.ts) ```ts config-catalog /** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */ export interface Config { /** Include the fixed DeepSeek Harness identity before the deployment persona (default true). */ includeHarnessIdentity?: boolean - /** Include dynamic runtime-context snapshots in model history (default true). */ + /** Include optional runtime-context snapshots in model history (default true); required context remains. */ includeRuntimeContext?: boolean /** * Deployment-wide persona prefix template before first-party guidance. A scoped section named @@ -3334,7 +3452,7 @@ export interface Config { ## `@deepseek-ai/dsh-terminal-bash` -- `inject`: `terminals` · `sandboxPolicy` · `sessionProjections` · `subprocess` +- `inject`: `terminals` · `sandboxPolicy` · `sessionProjections` · `subprocess` · `workingDirectory` - `source`: [`packages/terminal/terminal-bash/src/config.ts:10`](../packages/terminal/terminal-bash/src/config.ts) ```ts config-catalog @@ -3463,7 +3581,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-bash` -- `inject`: `tools` · `shell` · `systemPrompt` · `shellEnv` +- `inject`: `tools` · `shell` · `systemPrompt` · `shellEnv` · `workingDirectory` - `source`: [`packages/shell/tool-bash/src/index.ts:37`](../packages/shell/tool-bash/src/index.ts) ```ts config-catalog @@ -3493,8 +3611,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-bash-persistent` -- `inject`: `tools` · `terminals` -- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:444`](../packages/shell/tool-bash-persistent/src/index.ts) +- `inject`: `tools` · `terminals` · `workingDirectory` +- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:445`](../packages/shell/tool-bash-persistent/src/index.ts) ```ts config-catalog /** Configuration for the persistent Bash tool. */ @@ -3516,8 +3634,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-fs` -- `inject`: `tools` · `fs` · `systemPrompt` -- `source`: [`packages/fs/tool-fs/src/index.ts:25`](../packages/fs/tool-fs/src/index.ts) +- `inject`: `tools` · `fs` · `systemPrompt` · `workingDirectory` +- `source`: [`packages/fs/tool-fs/src/index.ts:26`](../packages/fs/tool-fs/src/index.ts) ```ts config-catalog /** Plugin config (all optional — `Config` supplies the defaults). */ @@ -3539,8 +3657,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-fs-search` -- `inject`: `tools` · `systemPrompt` · `subprocess` -- `source`: [`packages/fs/tool-fs-search/src/index.ts:73`](../packages/fs/tool-fs-search/src/index.ts) +- `inject`: `tools` · `systemPrompt` · `subprocess` · `workingDirectory` +- `source`: [`packages/fs/tool-fs-search/src/index.ts:74`](../packages/fs/tool-fs-search/src/index.ts) ```ts config-catalog /** Plugin config; over-cap glob sampling is an explicit deployment choice and the remaining fields have defaults. */ @@ -3629,8 +3747,8 @@ export type CompletionDelivery = 'quiet' | 'wakeup' ## `@deepseek-ai/dsh-tool-lsp` -- `inject`: `tools` · `lsp` · `systemPrompt` -- `source`: [`packages/lsp/tool-lsp/src/index.ts:57`](../packages/lsp/tool-lsp/src/index.ts) +- `inject`: `tools` · `lsp` · `systemPrompt` · `workingDirectory` +- `source`: [`packages/lsp/tool-lsp/src/index.ts:56`](../packages/lsp/tool-lsp/src/index.ts) ```ts config-catalog /** Plugin configuration: result caps and the timeout budget. */ @@ -3650,8 +3768,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-present` -- `inject`: `tools` · `fs` · `sessionProjections` -- `source`: [`packages/deliverables/tool-present/src/index.ts:15`](../packages/deliverables/tool-present/src/index.ts) +- `inject`: `tools` · `fs` · `sessionProjections` · `workingDirectory` +- `source`: [`packages/deliverables/tool-present/src/index.ts:16`](../packages/deliverables/tool-present/src/index.ts) ```ts config-catalog /** Per-call delivery limit. */ @@ -3667,7 +3785,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-pwsh` -- `inject`: `tools` · `shell` · `systemPrompt` · `shellEnv` +- `inject`: `tools` · `shell` · `systemPrompt` · `shellEnv` · `workingDirectory` - `source`: [`packages/shell/tool-pwsh/src/index.ts:54`](../packages/shell/tool-pwsh/src/index.ts) ```ts config-catalog @@ -3697,8 +3815,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-pwsh-persistent` -- `inject`: `tools` · `terminals` -- `source`: [`packages/shell/tool-pwsh-persistent/src/index.ts:457`](../packages/shell/tool-pwsh-persistent/src/index.ts) +- `inject`: `tools` · `terminals` · `workingDirectory` +- `source`: [`packages/shell/tool-pwsh-persistent/src/index.ts:458`](../packages/shell/tool-pwsh-persistent/src/index.ts) ```ts config-catalog /** Configuration for the persistent pwsh tool. */ @@ -3721,7 +3839,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-ralph` - `inject`: `tools` · `workflowEngine` · `subagents` · `systemPrompt` -- `source`: [`packages/workflow/tool-ralph/src/index.ts:21`](../packages/workflow/tool-ralph/src/index.ts) +- `source`: [`packages/experimental/tool-ralph/src/index.ts:21`](../packages/experimental/tool-ralph/src/index.ts) ```ts config-catalog /** Deployment policy for the fixed Ralph workflow. */ @@ -3744,7 +3862,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-session-query` - `inject`: `tools` · `systemPrompt` · `sessionQuery` · `sessionProjections` -- `source`: [`packages/session-query/tool-session-query/src/index.ts:28`](../packages/session-query/tool-session-query/src/index.ts) +- `source`: [`packages/experimental/tool-session-query/src/index.ts:28`](../packages/experimental/tool-session-query/src/index.ts) ```ts config-catalog /** Deployment-owned search count and timeout bounds. */ @@ -3762,8 +3880,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-skill` -- `inject`: `agents` · `tools` · `skills` -- `source`: [`packages/skill/tool-skill/src/index.ts:61`](../packages/skill/tool-skill/src/index.ts) +- `inject`: `agents` · `tools` · `skills` · `workingDirectory` +- `source`: [`packages/skill/tool-skill/src/index.ts:62`](../packages/skill/tool-skill/src/index.ts) ```ts config-catalog /** Model-facing skill catalog configuration. */ @@ -3817,18 +3935,6 @@ export interface Config { * Session and inherit that decision in its child Sessions. */ modelSelectionSettings?: boolean - /** - * Expose `run_in_background` (default true). Disabled instances omit the - * parameter and reject forced background calls. - */ - enableRunInBackground?: boolean - /** - * Background execution policy (default `one-shot`). `one-shot` defaults calls - * to foreground; `continuable` defaults them to background, requires a provider - * with the `prepareContinuable` capability, and returns the durable child id. - * Follow-up adapters remain independently optional. - */ - backgroundMode?: 'one-shot' | 'continuable' /** * Agent options applied to every child; omitted fields use child-loop defaults. */ @@ -3869,8 +3975,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-terminal` -- `inject`: `terminals` · `tools` · `systemPrompt` -- `source`: [`packages/terminal/tool-terminal/src/index.ts:36`](../packages/terminal/tool-terminal/src/index.ts) +- `inject`: `terminals` · `tools` · `systemPrompt` · `workingDirectory` +- `source`: [`packages/experimental/tool-terminal/src/index.ts:38`](../packages/experimental/tool-terminal/src/index.ts) ```ts config-catalog /** Model-facing terminal tool configuration. */ @@ -3991,7 +4097,7 @@ export interface Config { ## `@deepseek-ai/dsh-tools` - `inject`: `systemPrompt` -- `source`: [`packages/core/tools/src/index.ts:674`](../packages/core/tools/src/index.ts) +- `source`: [`packages/core/tools/src/index.ts:675`](../packages/core/tools/src/index.ts) ```ts config-catalog /** Plugin config: how the registered tools are presented to the model. */ @@ -4000,9 +4106,9 @@ export interface Config { * Model presentation. `native` (default) sends every visible schema; `ptc` * sends only `run_code` plus a generated SDK prompt and collapses the * executor to the same surface (a model-direct call may only name - * `run_code`; `run_code` SDK sub-dispatches keep every visible tool); `both` - * sends both forms. PTC mode requires a `ctx.ptcRuntime` whose `language` - * has a registered SDK renderer (TypeScript or Python) and fail prompt + * `run_code`; `run_code` SDK sub-dispatches keep every visible tool). + * PTC mode requires a `ctx.ptcRuntime` whose `language` + * has a registered SDK renderer (TypeScript or Python) and fails prompt * assembly when it is absent or has no renderer. Under `ptc`, native names * in `toolOrder` are invalid. */ @@ -4018,7 +4124,7 @@ export interface Config { } /** How the registry presents its tools to the model (see {@link Config.mode}). */ -export type ToolPresentationMode = 'native' | 'ptc' | 'both' +export type ToolPresentationMode = 'native' | 'ptc' ``` @@ -4242,7 +4348,7 @@ export interface Config { ## `@deepseek-ai/dsh-webhook-github` - `inject`: `webServer` · `webhookRuntime` · `credentials` -- `source`: [`packages/webhook/webhook-github/src/index.ts:17`](../packages/webhook/webhook-github/src/index.ts) +- `source`: [`packages/experimental/webhook-github/src/index.ts:17`](../packages/experimental/webhook-github/src/index.ts) ```ts config-catalog /** Required GitHub ingress configuration. */ @@ -4264,8 +4370,8 @@ export interface Config { ## `@deepseek-ai/dsh-workflow-ptc` -- `inject`: `subagents` · `ptcRuntime` · `sandboxPolicy` -- `source`: [`packages/workflow/workflow-ptc/src/index.ts:32`](../packages/workflow/workflow-ptc/src/index.ts) +- `inject`: `subagents` · `ptcRuntime` · `sandboxPolicy` · `workingDirectory` +- `source`: [`packages/workflow/workflow-ptc/src/index.ts:33`](../packages/workflow/workflow-ptc/src/index.ts) ```ts config-catalog /** Plugin config (all optional — `static Config` supplies the defaults). */ @@ -4284,13 +4390,30 @@ export interface Config { ``` + + + +## `@deepseek-ai/dsh-working-directory` + +- `inject`: `fs` · `sessionProjections` · `systemPrompt` +- `source`: [`packages/session/working-directory/src/index.ts:56`](../packages/session/working-directory/src/index.ts) + +```ts config-catalog +/** Deployment default for Sessions without an original project directory. */ +export interface Config { + /** Absolute execution-world fallback directory; omitted values use the launch directory. */ + defaultDirectory?: string +} +``` + + ## `@deepseek-ai/dsh-workspace-changes` -- `inject`: `subprocess` -- `source`: [`packages/deliverables/workspace-changes/src/index.ts:33`](../packages/deliverables/workspace-changes/src/index.ts) +- `inject`: `subprocess` · `fs` +- `source`: [`packages/deliverables/workspace-changes/src/index.ts:34`](../packages/deliverables/workspace-changes/src/index.ts) ```ts config-catalog /** Snapshot, capture, and comparison bounds. Invalid values fail plugin load. */ @@ -4384,11 +4507,13 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-config-editor` | `loader` · `profileContext` | [`packages/boot/config-editor/src/index.ts`](../packages/boot/config-editor/src/index.ts) | | `@deepseek-ai/dsh-cordis-client-runner` | — | [`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts) | | `@deepseek-ai/dsh-deepseek-llm-api-extensions` | — | [`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts) | -| `@deepseek-ai/dsh-experimental-auto-review` | `approval` · `llm` · `permissionPresets` · `sessions` · `tools` | [`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts) | +| `@deepseek-ai/dsh-experimental-auto-review` | `approval` · `llm` · `permissionPresets` · `sessions` · `tools` · `workingDirectory` | [`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts) | | `@deepseek-ai/dsh-experimental-client-ui-agent-team` | — | [`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts) | +| `@deepseek-ai/dsh-experimental-client-ui-claude-code-mods` | — | [`packages/experimental/client-ui-claude-code-mods/src/index.ts`](../packages/experimental/client-ui-claude-code-mods/src/index.ts) | | `@deepseek-ai/dsh-experimental-client-ui-voice-input` | — | [`packages/experimental/client-ui-voice-input/src/index.ts`](../packages/experimental/client-ui-voice-input/src/index.ts) | | `@deepseek-ai/dsh-experimental-computer-use-cua-driver-native` | `computerUse` · `tools` · `systemPrompt` | [`packages/experimental/computer-use-cua-driver-native/src/index.ts`](../packages/experimental/computer-use-cua-driver-native/src/index.ts) | | `@deepseek-ai/dsh-experimental-session-inspector` | — | [`packages/experimental/session-inspector/src/index.ts`](../packages/experimental/session-inspector/src/index.ts) | +| `@deepseek-ai/dsh-experimental-tool-worktree` | `tools` · `worktrees` | [`packages/experimental/tool-worktree/src/index.ts`](../packages/experimental/tool-worktree/src/index.ts) | | `@deepseek-ai/dsh-fs-observation-policy` | — | [`packages/fs/fs-observation-policy/src/index.ts`](../packages/fs/fs-observation-policy/src/index.ts) | | `@deepseek-ai/dsh-fs-ssh` | `ssh` · `sandboxPolicy` | [`packages/ssh/fs-ssh/src/index.ts`](../packages/ssh/fs-ssh/src/index.ts) | | `@deepseek-ai/dsh-goal-round-driver` | `agents` · `goals` · `sessions` | [`packages/goal/goal-round-driver/src/index.ts`](../packages/goal/goal-round-driver/src/index.ts) | @@ -4406,7 +4531,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-session-stats` | `sessionProjections` | [`packages/session/session-stats/src/index.ts`](../packages/session/session-stats/src/index.ts) | | `@deepseek-ai/dsh-session-turn-outline` | `sessionProjections` | [`packages/session/session-turn-outline/src/index.ts`](../packages/session/session-turn-outline/src/index.ts) | | `@deepseek-ai/dsh-settings` | `configEditor` · `profileContext` | [`packages/settings/settings/src/index.ts`](../packages/settings/settings/src/index.ts) | -| `@deepseek-ai/dsh-skill-badge` | `skills` | [`packages/skill/skill-badge/src/index.ts`](../packages/skill/skill-badge/src/index.ts) | +| `@deepseek-ai/dsh-skill-badge` | `skills` | [`packages/experimental/skill-badge/src/index.ts`](../packages/experimental/skill-badge/src/index.ts) | | `@deepseek-ai/dsh-storage` | — | [`packages/storage/storage/src/index.ts`](../packages/storage/storage/src/index.ts) | | `@deepseek-ai/dsh-subprocess-local` | — | [`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts) | | `@deepseek-ai/dsh-subprocess-ssh` | `ssh` | [`packages/ssh/subprocess-ssh/src/index.ts`](../packages/ssh/subprocess-ssh/src/index.ts) | @@ -4415,8 +4540,9 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-tool-cordis` | `tools` · `cordisInspect` | [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts) | | `@deepseek-ai/dsh-tool-schedule` | `tools` | [`packages/schedule/tool-schedule/src/index.ts`](../packages/schedule/tool-schedule/src/index.ts) | | `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) | +| `@deepseek-ai/dsh-tool-working-directory` | `tools` · `workingDirectory` | [`packages/session/tool-working-directory/src/index.ts`](../packages/session/tool-working-directory/src/index.ts) | | `@deepseek-ai/dsh-user-questions` | — | [`packages/interaction/user-questions/src/index.ts`](../packages/interaction/user-questions/src/index.ts) | -| `@deepseek-ai/dsh-webhook` | `agents` · `agentDefaultModel` · `agentPresets` · `permissionPresets` · `sessionTitle` · `workspaceRegistry` | [`packages/webhook/webhook/src/index.ts`](../packages/webhook/webhook/src/index.ts) | +| `@deepseek-ai/dsh-webhook` | `agents` · `agentDefaultModel` · `agentPresets` · `permissionPresets` · `sessionTitle` · `workspaceRegistry` | [`packages/experimental/webhook/src/index.ts`](../packages/experimental/webhook/src/index.ts) | | `@deepseek-ai/dsh-workspace` | `storageDomain` · `sessionPersistence` | [`packages/workspace/workspace/src/index.ts`](../packages/workspace/workspace/src/index.ts) | @@ -4468,13 +4594,19 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. | `@deepseek-ai/dsh-cmdline` | — | [`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts) | | `@deepseek-ai/dsh-deque` | — | [`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts) | | `@deepseek-ai/dsh-experimental-agent-team-profile` | — | [`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts) | +| `@deepseek-ai/dsh-experimental-badge-skill-bundle` | — | [`packages/experimental/badge-skill-bundle/src/index.ts`](../packages/experimental/badge-skill-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-browser-use-runtime` | — | [`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts) | +| `@deepseek-ai/dsh-experimental-cot-translation-bundle` | — | [`packages/experimental/cot-translation-bundle/src/index.ts`](../packages/experimental/cot-translation-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-inspector-profile` | — | [`packages/experimental/inspector-profile/src/index.ts`](../packages/experimental/inspector-profile/src/index.ts) | +| `@deepseek-ai/dsh-experimental-ralph-bundle` | — | [`packages/experimental/ralph-bundle/src/index.ts`](../packages/experimental/ralph-bundle/src/index.ts) | +| `@deepseek-ai/dsh-experimental-session-search` | — | [`packages/experimental/session-search/src/index.ts`](../packages/experimental/session-search/src/index.ts) | +| `@deepseek-ai/dsh-experimental-session-titles-bundle` | — | [`packages/experimental/session-titles-bundle/src/index.ts`](../packages/experimental/session-titles-bundle/src/index.ts) | +| `@deepseek-ai/dsh-experimental-terminal-bundle` | — | [`packages/experimental/terminal-bundle/src/index.ts`](../packages/experimental/terminal-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-voice-input-bundle` | — | [`packages/experimental/voice-input-bundle/src/index.ts`](../packages/experimental/voice-input-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-webworker-packer` | — | [`packages/experimental/webworker-packer/src/index.ts`](../packages/experimental/webworker-packer/src/index.ts) | | `@deepseek-ai/dsh-experimental-webworker-runtime` | — | [`packages/experimental/webworker-runtime/src/index.ts`](../packages/experimental/webworker-runtime/src/index.ts) | | `@deepseek-ai/dsh-home-paths` | — | [`packages/util/home-paths/src/index.ts`](../packages/util/home-paths/src/index.ts) | -| `@deepseek-ai/dsh-hook-protocol` | — | [`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts) | +| `@deepseek-ai/dsh-hook-protocol` | — | [`packages/experimental/hook-protocol/src/index.ts`](../packages/experimental/hook-protocol/src/index.ts) | | `@deepseek-ai/dsh-http-proxy` | — | [`packages/util/http-proxy/src/index.ts`](../packages/util/http-proxy/src/index.ts) | | `@deepseek-ai/dsh-launch-environment` | — | [`packages/util/launch-environment/src/index.ts`](../packages/util/launch-environment/src/index.ts) | | `@deepseek-ai/dsh-lazy-require` | — | [`packages/util/lazy-require/src/index.ts`](../packages/util/lazy-require/src/index.ts) | @@ -4499,7 +4631,6 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. | `@deepseek-ai/dsh-session-snapshot` | — | [`packages/test-support/session-snapshot/src/index.ts`](../packages/test-support/session-snapshot/src/index.ts) | | `@deepseek-ai/dsh-session-telemetry` | — | [`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts) | | `@deepseek-ai/dsh-session-title-llm` | — | [`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts) | -| `@deepseek-ai/dsh-subagent-in-process-driver` | — | [`packages/subagent/subagent-in-process-driver/src/index.ts`](../packages/subagent/subagent-in-process-driver/src/index.ts) | | `@deepseek-ai/dsh-timeout` | — | [`packages/util/timeout/src/index.ts`](../packages/util/timeout/src/index.ts) | | `@deepseek-ai/dsh-typert-generator` | — | [`packages/typert/generator/src/index.ts`](../packages/typert/generator/src/index.ts) | | `@deepseek-ai/dsh-typert-protocol` | — | [`packages/typert/protocol/src/index.ts`](../packages/typert/protocol/src/index.ts) | diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 1f734eac99..6682104549 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -61,15 +61,13 @@ export interface Config { ## `@deepseek-ai/dsh-agent-instructions` -- `inject`: `sessionProjections` +- `inject`: `sessionProjections` · `workingDirectory` - `source`: [`packages/context/agent-instructions/src/config.ts:18`](../packages/context/agent-instructions/src/config.ts) ```ts config-catalog /** User-facing workspace instruction loader configuration. */ export interface Config { - /** Harness home containing the fixed user-global `AGENTS.md`; defaults to `$DSH_HOME` or `~/.dsh`. */ - dshHome?: string - /** Directory entries that identify the project root while walking upward from the session cwd. */ + /** Directory entries that identify the project root while walking upward from the current working directory. */ projectRootMarkers?: string[] /** UTF-8 byte cap for one rendered baseline or dynamic batch; non-positive or non-finite disables loading. */ maxBytes: number @@ -143,7 +141,7 @@ export type Config = PresetDefinition - `inject`: `loader` · `sessionProjections` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/preset/agent-preset-registry/src/preset.ts:13`](../packages/preset/agent-preset-registry/src/preset.ts) +- `source`: [`packages/preset/agent-preset-registry/src/preset.ts:15`](../packages/preset/agent-preset-registry/src/preset.ts) ```ts config-catalog /** Registry selection policy. */ @@ -170,7 +168,7 @@ export interface Config { export interface Config { /** * The form this agent's model sees. `native` sends every visible schema, - * `ptc` sends only `run_code` plus a generated SDK, `both` sends both. + * `ptc` sends only `run_code` plus a generated SDK. * Required rather than defaulted: the deployment default is what a preset * without this row already gets, so an omitted value would mean the row was * composed for nothing. @@ -258,8 +256,8 @@ export interface SettingsControllerInternals { ## `@deepseek-ai/dsh-api-terminal-controller` -- `inject`: `subprocess` · `sandboxPolicy` · `typert` -- `source`: [`packages/api/terminal-controller/src/index.ts:26`](../packages/api/terminal-controller/src/index.ts) +- `inject`: `subprocess` · `sandboxPolicy` · `workingDirectory` · `typert` +- `source`: [`packages/api/terminal-controller/src/index.ts:27`](../packages/api/terminal-controller/src/index.ts) ```ts config-catalog /** Deployment limits and an optional shell profile. */ @@ -659,7 +657,7 @@ export interface Config { ## `@deepseek-ai/dsh-client-ui-theme` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/client/ui-theme/src/index.ts:22`](../packages/client/ui-theme/src/index.ts) +- `source`: [`packages/client/ui-theme/src/index.ts:20`](../packages/client/ui-theme/src/index.ts) ```ts config-catalog /** Runtime preferences projected to the browser. */ @@ -668,6 +666,16 @@ export interface Config { preference: Volatile /** Browser font size in pixels. */ fontSize: Volatile + /** Code-block font size in pixels. */ + codeFontSize: Volatile + /** Sidebar terminal font size in pixels. */ + terminalFontSize: Volatile + /** Interface and body text font list; `''` keeps the built-in stack. */ + textFontFamily: Volatile + /** Code font list; `''` keeps the built-in stack. */ + codeFontFamily: Volatile + /** Sidebar terminal font list; `''` keeps the built-in stack. */ + terminalFontFamily: Volatile } /** Theme preference persisted by the product Appearance row. */ @@ -876,9 +884,9 @@ export interface Config { ## `@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp` -- `inject`: `browserUse` · `agents` · `tools` · `systemPrompt` +- `inject`: `browserUse` · `agents` · `tools` · `systemPrompt` · `workingDirectory` - `refs`: `BrowserMcpConfig` (`@deepseek-ai/dsh-experimental-browser-use-runtime/mcp`) -- `source`: [`packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts:14`](../packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts) +- `source`: [`packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts:15`](../packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts) ```ts config-catalog /** Fixed Chromium launch or existing-browser attachment settings. */ @@ -891,9 +899,9 @@ export type Config = BrowserMcpConfig ## `@deepseek-ai/dsh-experimental-browser-use-playwright-mcp` -- `inject`: `browserUse` · `agents` · `tools` · `systemPrompt` +- `inject`: `browserUse` · `agents` · `tools` · `systemPrompt` · `workingDirectory` - `refs`: `BrowserMcpConfig` (`@deepseek-ai/dsh-experimental-browser-use-runtime/mcp`) -- `source`: [`packages/experimental/browser-use-playwright-mcp/src/index.ts:15`](../packages/experimental/browser-use-playwright-mcp/src/index.ts) +- `source`: [`packages/experimental/browser-use-playwright-mcp/src/index.ts:16`](../packages/experimental/browser-use-playwright-mcp/src/index.ts) ```ts config-catalog /** Fixed Chromium launch or existing-browser attachment settings. */ @@ -943,6 +951,61 @@ export interface StagehandModelConfig { ``` + + + +## `@deepseek-ai/dsh-experimental-claude-code-mods` + +- `inject`: `workingDirectory` +- `source`: [`packages/experimental/claude-code-mods/src/index.ts:53`](../packages/experimental/claude-code-mods/src/index.ts) + +```ts config-catalog +/** Plugin config: the limits mod hooks run under. */ +export interface Config { + /** A hook's own running-time limit in milliseconds (Claude Code: 10 seconds). */ + hookTimeoutMs?: number + /** A `.catch` handler's running-time limit in milliseconds (Claude Code: 1 second). */ + catchTimeoutMs?: number + /** Default `$.process.run` and `$.http.fetch` timeout in milliseconds (Claude Code: 30 seconds). */ + processTimeoutMs?: number + /** Claude Code tool name → harness tool name entries added to the built-in alias table. */ + toolAliases?: Record + /** Columns the band above the prompt reports to `ui.render` as `bodyColumns` and `viewport.columns`. */ + bandColumns?: number + /** Rows the band reports as `maxRows`. */ + bandRows?: number +} +``` + + + + + +## `@deepseek-ai/dsh-experimental-client-ui-cot-translation` + +- `inject`: `translator` · `typert` · `sessionController` +- `refs`: [`TranslationProvider`](subsystems/translation.zh.md) · `Volatile` (`@deepseek-ai/cordis`) +- `source`: [`packages/experimental/client-ui-cot-translation/src/index.ts:14`](../packages/experimental/client-ui-cot-translation/src/index.ts) + +```ts config-catalog +/** Live preferences presented on the bundle's Plugins page. */ +export interface Config { + /** Explicit translation provider, defaulting to Bing. */ + provider: Volatile + /** Target language code; auto follows the browser UI locale. */ + targetLanguage: Volatile +} + +/** Translation choices; auto targets the active browser UI language. */ +export interface CotTranslationPreferences { + /** Explicit provider selection; requests never fall back to another provider. */ + provider: TranslationProvider + /** BCP 47 language code, or auto to follow the UI language. */ + targetLanguage: string +} +``` + + @@ -1041,7 +1104,8 @@ export interface InspectorOptions { ## `@deepseek-ai/dsh-experimental-ptc-runtime-python` -- `source`: [`packages/experimental/ptc-runtime-python/src/index.ts:42`](../packages/experimental/ptc-runtime-python/src/index.ts) +- `inject`: `sandbox` · `sandboxPolicy` +- `source`: [`packages/experimental/ptc-runtime-python/src/index.ts:45`](../packages/experimental/ptc-runtime-python/src/index.ts) ```ts config-catalog /** Plugin config: every cap, changeable from `cordis.yml` (no hardcoded tunables). */ @@ -1201,13 +1265,81 @@ export interface Config { ``` + + + +## `@deepseek-ai/dsh-experimental-translator` + +- `source`: [`packages/experimental/translator/src/index.ts:25`](../packages/experimental/translator/src/index.ts) + +```ts config-catalog +/** Routing and bounds for anonymous requests and independent native model translation. */ +export interface Config { + /** Provider selected when a consumer omits it. */ + provider: TranslationProvider + /** Google-compatible anonymous translation endpoint. */ + googleEndpoint: string + /** Bing-compatible Microsoft Edge browser translation endpoint. */ + bingEndpoint: string + /** Deadline covering the request and complete response body, in milliseconds. */ + timeoutMs: number + /** Maximum UTF-16 code units per request; at least two to admit one supplementary-plane character. */ + maxTextChars: number + /** Maximum anonymous JSON-body bytes or paid assembled UTF-8 translation bytes. */ + maxResponseBytes: number + /** Deadline for native admission and complete translation output, in milliseconds. */ + deepseekTimeoutMs: number + /** Requested output-token cap for independent native Flash translation. */ + deepseekMaxOutputTokens: number +} + +/** Explicit anonymous endpoint or native paid model route. */ +export type TranslationProvider = AnonymousTranslationProvider | PaidTranslationProvider + +/** Anonymous browser endpoint selected for one translation. */ +export type AnonymousTranslationProvider = 'google' | 'bing' + +/** Explicit native DeepSeek credential route selected for paid translation. */ +export type PaidTranslationProvider = 'deepseek-account' | 'deepseek-official' +``` + + + + + +## `@deepseek-ai/dsh-experimental-worktree` + +- `inject`: `workingDirectory` · `fs` · `subprocess` · `sandbox` · `sandboxPolicy` +- `source`: [`packages/experimental/worktree/src/index.ts:19`](../packages/experimental/worktree/src/index.ts) + +```ts config-catalog +/** Worktree location, generated names, executables, and subprocess resource limits. */ +export interface Config { + /** Relative checkout pool inside the source repository. */ + directory?: string + /** Prefix for automatically generated branch and checkout names. */ + namePrefix?: string + /** Git 2.45 or newer executable name or absolute execution-world path. */ + gitCommand?: string + /** Node executable used for sandboxed directory allocation. */ + nodeCommand?: string + /** Deadline in milliseconds for one command. */ + timeoutMs?: number + /** Subprocess termination grace in milliseconds. */ + graceMs?: number + /** Maximum captured bytes per subprocess output stream. */ + maxOutputBytes?: number +} +``` + + ## `@deepseek-ai/dsh-file-reference-local` -- `inject`: `agents` -- `source`: [`packages/context/file-reference-local/src/index.ts:34`](../packages/context/file-reference-local/src/index.ts) +- `inject`: `agents` · `workingDirectory` +- `source`: [`packages/context/file-reference-local/src/index.ts:35`](../packages/context/file-reference-local/src/index.ts) ```ts config-catalog /** Local file-reference discovery configuration. */ @@ -1286,7 +1418,7 @@ export interface Config { ## `@deepseek-ai/dsh-headless` - `inject`: `agentDefaultModel` · `agents` · `sessions` -- `source`: [`packages/bundle/headless/src/index.ts:42`](../packages/bundle/headless/src/index.ts) +- `source`: [`packages/bundle/headless/src/index.ts:43`](../packages/bundle/headless/src/index.ts) ```ts config-catalog /** Plugin config: the task and run options resolved from this app's injected provider service. */ @@ -1307,7 +1439,7 @@ export interface Config { ## `@deepseek-ai/dsh-hmr` - `refs`: `ChokidarOptions` (`chokidar`) -- `source`: [`packages/boot/hmr/src/index.ts:51`](../packages/boot/hmr/src/index.ts) +- `source`: [`packages/boot/hmr/src/index.ts:53`](../packages/boot/hmr/src/index.ts) ```ts config-catalog /** Module roots and watcher timing, with Chokidar deployment options. */ @@ -1329,8 +1461,8 @@ export interface HmrConfig extends ChokidarOptions { ## `@deepseek-ai/dsh-hooks-claude-code` -- `inject`: `shell` · `sessionProjections` -- `source`: [`packages/hooks/hooks-claude-code/src/index.ts:51`](../packages/hooks/hooks-claude-code/src/index.ts) +- `inject`: `shell` · `sessionProjections` · `workingDirectory` +- `source`: [`packages/experimental/hooks-claude-code/src/index.ts:51`](../packages/experimental/hooks-claude-code/src/index.ts) ```ts config-catalog /** Plugin config: where the CC hook config lives + substitution roots. */ @@ -1350,7 +1482,7 @@ export interface Config { /** * Replaces `${CLAUDE_PROJECT_DIR}` in command strings AND is exported as the * `CLAUDE_PROJECT_DIR` env var for hook processes. When omitted, the env var - * defaults per-run to the agent's session workspace (`session.header.cwd`, the + * defaults per-run to the agent's current directory (the * same dir the hook runs in) — Claude Code always exports this var, and common * unmodified hooks reference `$CLAUDE_PROJECT_DIR` for project-relative paths. */ @@ -1368,8 +1500,8 @@ export interface Config { ## `@deepseek-ai/dsh-hooks-codex` -- `inject`: `shell` · `sessionProjections` -- `source`: [`packages/hooks/hooks-codex/src/index.ts:50`](../packages/hooks/hooks-codex/src/index.ts) +- `inject`: `shell` · `sessionProjections` · `workingDirectory` +- `source`: [`packages/experimental/hooks-codex/src/index.ts:50`](../packages/experimental/hooks-codex/src/index.ts) ```ts config-catalog /** Plugin config: where the Codex hooks.json lives + the model name for payloads. */ @@ -1920,7 +2052,7 @@ export interface ReplayModelConfig { * no image pricing. */ imageRequestTokens?: number - /** Optional reasoning-effort ids the replay route accepts, in display order. */ + /** Optional reasoning-effort ids from least to greatest selectable effort. */ reasoningEfforts?: string[] /** * Optional effort materialized when callers omit one; must appear in @@ -2232,7 +2364,7 @@ export interface PlanModeConfig { ## `@deepseek-ai/dsh-plugin-manager` - `inject`: `loader` · `profileContext` -- `source`: [`packages/boot/plugin-manager/src/index.ts:40`](../packages/boot/plugin-manager/src/index.ts) +- `source`: [`packages/boot/plugin-manager/src/index.ts:44`](../packages/boot/plugin-manager/src/index.ts) ```ts config-catalog /** The pnpm executable, registries, and limits for diagnostics, lookups and connection checks. */ @@ -2518,9 +2650,9 @@ export interface Config { ## `@deepseek-ai/dsh-sdk-jsonrpc-server` -- `inject`: `agents` +- `inject`: `agents` · `workingDirectory` - `refs`: `Readable` (`node:stream`) · `Writable` (`node:stream`) -- `source`: [`packages/sdk/server/src/index.ts:25`](../packages/sdk/server/src/index.ts) +- `source`: [`packages/sdk/server/src/index.ts:24`](../packages/sdk/server/src/index.ts) ```ts config-catalog /** JSON-RPC deployment config plus runtime-only test hooks. */ @@ -2710,8 +2842,8 @@ export interface Config { ## `@deepseek-ai/dsh-session-telemetry-otel` - `inject`: `sessions` · `otel` -- `refs`: `BatchLogRecordProcessorOptions` (`@opentelemetry/sdk-logs`) · `OTLPExporterNodeConfigBase` (`@opentelemetry/otlp-exporter-base`) -- `source`: [`packages/session/session-telemetry-otel/src/index.ts:87`](../packages/session/session-telemetry-otel/src/index.ts) +- `refs`: [`LogExporterOptions`](../packages/telemetry/otel/src/index.ts) · [`SessionLogProcessorOptions`](../packages/telemetry/otel/src/index.ts) +- `source`: [`packages/session/session-telemetry-otel/src/index.ts:85`](../packages/session/session-telemetry-otel/src/index.ts) ```ts config-catalog /** @@ -2725,16 +2857,18 @@ export interface Config { /** * Explicit SDK HTTP transport settings, including optional routing headers. * Ambient credentials are not inherited. URL is required while uploading. + * Exporter self-observability metering is not supported. */ - exporter?: OTLPExporterNodeConfigBase & { + exporter?: Omit & { /** Full logs endpoint (e.g. `https://collector.example.com/v1/logs`). Required outside `DISABLED`; validated at load. */ url?: string } /** * Count, queue, cadence, and per-request watchdog settings for the byte-bounded * processor. A watchdog warning never releases an unsettled transport slot. + * SDK processor self-observability metering is not supported. */ - processor?: Omit + processor?: SessionLogProcessorOptions /** Maximum time spent awaiting the SDK provider's complete shutdown path. */ shutdownTimeoutMillis?: number /** Uncompressed OTLP request byte limit, at most 4,000,000. */ @@ -2777,7 +2911,7 @@ export interface Config { - `inject`: `sessionTitle` · `llm` · `sessions` - `refs`: [`SessionTitleLlmConfig`](../packages/session/session-title-llm/src/index.ts) -- `source`: [`packages/session/session-title-all-prompts-llm/src/index.ts:15`](../packages/session/session-title-all-prompts-llm/src/index.ts) +- `source`: [`packages/experimental/session-title-all-prompts-llm/src/index.ts:15`](../packages/experimental/session-title-all-prompts-llm/src/index.ts) ```ts config-catalog /** Required LLM policy; this plugin adds no defaults. */ @@ -2838,7 +2972,7 @@ export interface Config { ## `@deepseek-ai/dsh-skill-filesystem` - `inject`: `skills` -- `source`: [`packages/skill/skill-filesystem/src/index.ts:49`](../packages/skill/skill-filesystem/src/index.ts) +- `source`: [`packages/skill/skill-filesystem/src/index.ts:48`](../packages/skill/skill-filesystem/src/index.ts) ```ts config-catalog /** Local filesystem skill provider configuration. */ @@ -3068,8 +3202,9 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' ## `@deepseek-ai/dsh-subagent` +- `inject`: `workingDirectory` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/subagent/subagent/src/index.ts:190`](../packages/subagent/subagent/src/index.ts) +- `source`: [`packages/subagent/subagent/src/index.ts:184`](../packages/subagent/subagent/src/index.ts) ```ts config-catalog /** Host configuration for continuable subagent capacity. */ @@ -3088,7 +3223,7 @@ export interface Config { ## `@deepseek-ai/dsh-subagent-acp` - `inject`: `subagents` · `subprocess` -- `source`: [`packages/subagent/subagent-acp/src/index.ts:27`](../packages/subagent/subagent-acp/src/index.ts) +- `source`: [`packages/subagent/subagent-acp/src/index.ts:25`](../packages/subagent/subagent-acp/src/index.ts) ```ts config-catalog /** Config: how to spawn and drive the child ACP agent process. */ @@ -3099,14 +3234,6 @@ export interface Config { command: string /** Arguments passed to {@link command}. */ args: string[] - /** - * Working directory override for the child process and its ACP session. - * Must be non-empty; a relative path resolves against the harness launch - * directory at load, and the result must be an existing directory. When - * omitted, each child inherits its delegating parent session's cwd — and - * starting one from a parent session that has no cwd fails. - */ - cwd?: string /** * How to auto-answer the child's `session/request_permission` prompts: * `reject` (default — decline every prompt) or `allow` (approve via the first @@ -3227,15 +3354,6 @@ export interface Config { patches: string[] /** Absolute isolated Harness home for every nested child process. */ dshHome: string - /** - * Working directory override for the child process and its SDK session - * workspace. Must be non-empty; a relative path resolves against the - * harness launch directory at load, and the result must be an existing - * directory. When omitted, each child inherits its delegating parent - * session's cwd — and starting one from a parent session that has no cwd - * fails. - */ - cwd?: string /** Provider route the child runtime initializes with (default `deepseek-official`). */ provider: string /** Model the child runtime initializes with (default `deepseek-v4-flash`). */ @@ -3269,7 +3387,7 @@ export interface Config { ## `@deepseek-ai/dsh-subagent-fork-in-process` - `inject`: `subagents` -- `source`: [`packages/subagent/subagent-fork-in-process/src/index.ts:31`](../packages/subagent/subagent-fork-in-process/src/index.ts) +- `source`: [`packages/subagent/subagent-fork-in-process/src/index.ts:29`](../packages/subagent/subagent-fork-in-process/src/index.ts) ```ts config-catalog /** Config: the registry name to register the provider under. */ @@ -3286,7 +3404,7 @@ export interface Config { ## `@deepseek-ai/dsh-subagent-spawn-in-process` - `inject`: `subagents` -- `source`: [`packages/subagent/subagent-spawn-in-process/src/index.ts:25`](../packages/subagent/subagent-spawn-in-process/src/index.ts) +- `source`: [`packages/subagent/subagent-spawn-in-process/src/index.ts:23`](../packages/subagent/subagent-spawn-in-process/src/index.ts) ```ts config-catalog /** Config: the registry name to register the provider under. */ @@ -3302,14 +3420,14 @@ export interface Config { ## `@deepseek-ai/dsh-system-prompt` -- `source`: [`packages/core/system-prompt/src/index.ts:247`](../packages/core/system-prompt/src/index.ts) +- `source`: [`packages/core/system-prompt/src/index.ts:254`](../packages/core/system-prompt/src/index.ts) ```ts config-catalog /** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */ export interface Config { /** Include the fixed DeepSeek Harness identity before the deployment persona (default true). */ includeHarnessIdentity?: boolean - /** Include dynamic runtime-context snapshots in model history (default true). */ + /** Include optional runtime-context snapshots in model history (default true); required context remains. */ includeRuntimeContext?: boolean /** * Deployment-wide persona prefix template before first-party guidance. A scoped section named @@ -3336,7 +3454,7 @@ export interface Config { ## `@deepseek-ai/dsh-terminal-bash` -- `inject`: `terminals` · `sandboxPolicy` · `sessionProjections` · `subprocess` +- `inject`: `terminals` · `sandboxPolicy` · `sessionProjections` · `subprocess` · `workingDirectory` - `source`: [`packages/terminal/terminal-bash/src/config.ts:10`](../packages/terminal/terminal-bash/src/config.ts) ```ts config-catalog @@ -3465,7 +3583,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-bash` -- `inject`: `tools` · `shell` · `systemPrompt` · `shellEnv` +- `inject`: `tools` · `shell` · `systemPrompt` · `shellEnv` · `workingDirectory` - `source`: [`packages/shell/tool-bash/src/index.ts:37`](../packages/shell/tool-bash/src/index.ts) ```ts config-catalog @@ -3495,8 +3613,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-bash-persistent` -- `inject`: `tools` · `terminals` -- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:444`](../packages/shell/tool-bash-persistent/src/index.ts) +- `inject`: `tools` · `terminals` · `workingDirectory` +- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:445`](../packages/shell/tool-bash-persistent/src/index.ts) ```ts config-catalog /** Configuration for the persistent Bash tool. */ @@ -3518,8 +3636,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-fs` -- `inject`: `tools` · `fs` · `systemPrompt` -- `source`: [`packages/fs/tool-fs/src/index.ts:25`](../packages/fs/tool-fs/src/index.ts) +- `inject`: `tools` · `fs` · `systemPrompt` · `workingDirectory` +- `source`: [`packages/fs/tool-fs/src/index.ts:26`](../packages/fs/tool-fs/src/index.ts) ```ts config-catalog /** Plugin config (all optional — `Config` supplies the defaults). */ @@ -3541,8 +3659,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-fs-search` -- `inject`: `tools` · `systemPrompt` · `subprocess` -- `source`: [`packages/fs/tool-fs-search/src/index.ts:73`](../packages/fs/tool-fs-search/src/index.ts) +- `inject`: `tools` · `systemPrompt` · `subprocess` · `workingDirectory` +- `source`: [`packages/fs/tool-fs-search/src/index.ts:74`](../packages/fs/tool-fs-search/src/index.ts) ```ts config-catalog /** Plugin config; over-cap glob sampling is an explicit deployment choice and the remaining fields have defaults. */ @@ -3631,8 +3749,8 @@ export type CompletionDelivery = 'quiet' | 'wakeup' ## `@deepseek-ai/dsh-tool-lsp` -- `inject`: `tools` · `lsp` · `systemPrompt` -- `source`: [`packages/lsp/tool-lsp/src/index.ts:57`](../packages/lsp/tool-lsp/src/index.ts) +- `inject`: `tools` · `lsp` · `systemPrompt` · `workingDirectory` +- `source`: [`packages/lsp/tool-lsp/src/index.ts:56`](../packages/lsp/tool-lsp/src/index.ts) ```ts config-catalog /** Plugin configuration: result caps and the timeout budget. */ @@ -3652,8 +3770,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-present` -- `inject`: `tools` · `fs` · `sessionProjections` -- `source`: [`packages/deliverables/tool-present/src/index.ts:15`](../packages/deliverables/tool-present/src/index.ts) +- `inject`: `tools` · `fs` · `sessionProjections` · `workingDirectory` +- `source`: [`packages/deliverables/tool-present/src/index.ts:16`](../packages/deliverables/tool-present/src/index.ts) ```ts config-catalog /** Per-call delivery limit. */ @@ -3669,7 +3787,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-pwsh` -- `inject`: `tools` · `shell` · `systemPrompt` · `shellEnv` +- `inject`: `tools` · `shell` · `systemPrompt` · `shellEnv` · `workingDirectory` - `source`: [`packages/shell/tool-pwsh/src/index.ts:54`](../packages/shell/tool-pwsh/src/index.ts) ```ts config-catalog @@ -3699,8 +3817,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-pwsh-persistent` -- `inject`: `tools` · `terminals` -- `source`: [`packages/shell/tool-pwsh-persistent/src/index.ts:457`](../packages/shell/tool-pwsh-persistent/src/index.ts) +- `inject`: `tools` · `terminals` · `workingDirectory` +- `source`: [`packages/shell/tool-pwsh-persistent/src/index.ts:458`](../packages/shell/tool-pwsh-persistent/src/index.ts) ```ts config-catalog /** Configuration for the persistent pwsh tool. */ @@ -3723,7 +3841,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-ralph` - `inject`: `tools` · `workflowEngine` · `subagents` · `systemPrompt` -- `source`: [`packages/workflow/tool-ralph/src/index.ts:21`](../packages/workflow/tool-ralph/src/index.ts) +- `source`: [`packages/experimental/tool-ralph/src/index.ts:21`](../packages/experimental/tool-ralph/src/index.ts) ```ts config-catalog /** Deployment policy for the fixed Ralph workflow. */ @@ -3746,7 +3864,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-session-query` - `inject`: `tools` · `systemPrompt` · `sessionQuery` · `sessionProjections` -- `source`: [`packages/session-query/tool-session-query/src/index.ts:28`](../packages/session-query/tool-session-query/src/index.ts) +- `source`: [`packages/experimental/tool-session-query/src/index.ts:28`](../packages/experimental/tool-session-query/src/index.ts) ```ts config-catalog /** Deployment-owned search count and timeout bounds. */ @@ -3764,8 +3882,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-skill` -- `inject`: `agents` · `tools` · `skills` -- `source`: [`packages/skill/tool-skill/src/index.ts:61`](../packages/skill/tool-skill/src/index.ts) +- `inject`: `agents` · `tools` · `skills` · `workingDirectory` +- `source`: [`packages/skill/tool-skill/src/index.ts:62`](../packages/skill/tool-skill/src/index.ts) ```ts config-catalog /** Model-facing skill catalog configuration. */ @@ -3819,18 +3937,6 @@ export interface Config { * Session and inherit that decision in its child Sessions. */ modelSelectionSettings?: boolean - /** - * Expose `run_in_background` (default true). Disabled instances omit the - * parameter and reject forced background calls. - */ - enableRunInBackground?: boolean - /** - * Background execution policy (default `one-shot`). `one-shot` defaults calls - * to foreground; `continuable` defaults them to background, requires a provider - * with the `prepareContinuable` capability, and returns the durable child id. - * Follow-up adapters remain independently optional. - */ - backgroundMode?: 'one-shot' | 'continuable' /** * Agent options applied to every child; omitted fields use child-loop defaults. */ @@ -3871,8 +3977,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-terminal` -- `inject`: `terminals` · `tools` · `systemPrompt` -- `source`: [`packages/terminal/tool-terminal/src/index.ts:36`](../packages/terminal/tool-terminal/src/index.ts) +- `inject`: `terminals` · `tools` · `systemPrompt` · `workingDirectory` +- `source`: [`packages/experimental/tool-terminal/src/index.ts:38`](../packages/experimental/tool-terminal/src/index.ts) ```ts config-catalog /** Model-facing terminal tool configuration. */ @@ -3993,7 +4099,7 @@ export interface Config { ## `@deepseek-ai/dsh-tools` - `inject`: `systemPrompt` -- `source`: [`packages/core/tools/src/index.ts:674`](../packages/core/tools/src/index.ts) +- `source`: [`packages/core/tools/src/index.ts:675`](../packages/core/tools/src/index.ts) ```ts config-catalog /** Plugin config: how the registered tools are presented to the model. */ @@ -4002,9 +4108,9 @@ export interface Config { * Model presentation. `native` (default) sends every visible schema; `ptc` * sends only `run_code` plus a generated SDK prompt and collapses the * executor to the same surface (a model-direct call may only name - * `run_code`; `run_code` SDK sub-dispatches keep every visible tool); `both` - * sends both forms. PTC mode requires a `ctx.ptcRuntime` whose `language` - * has a registered SDK renderer (TypeScript or Python) and fail prompt + * `run_code`; `run_code` SDK sub-dispatches keep every visible tool). + * PTC mode requires a `ctx.ptcRuntime` whose `language` + * has a registered SDK renderer (TypeScript or Python) and fails prompt * assembly when it is absent or has no renderer. Under `ptc`, native names * in `toolOrder` are invalid. */ @@ -4020,7 +4126,7 @@ export interface Config { } /** How the registry presents its tools to the model (see {@link Config.mode}). */ -export type ToolPresentationMode = 'native' | 'ptc' | 'both' +export type ToolPresentationMode = 'native' | 'ptc' ``` @@ -4244,7 +4350,7 @@ export interface Config { ## `@deepseek-ai/dsh-webhook-github` - `inject`: `webServer` · `webhookRuntime` · `credentials` -- `source`: [`packages/webhook/webhook-github/src/index.ts:17`](../packages/webhook/webhook-github/src/index.ts) +- `source`: [`packages/experimental/webhook-github/src/index.ts:17`](../packages/experimental/webhook-github/src/index.ts) ```ts config-catalog /** Required GitHub ingress configuration. */ @@ -4266,8 +4372,8 @@ export interface Config { ## `@deepseek-ai/dsh-workflow-ptc` -- `inject`: `subagents` · `ptcRuntime` · `sandboxPolicy` -- `source`: [`packages/workflow/workflow-ptc/src/index.ts:32`](../packages/workflow/workflow-ptc/src/index.ts) +- `inject`: `subagents` · `ptcRuntime` · `sandboxPolicy` · `workingDirectory` +- `source`: [`packages/workflow/workflow-ptc/src/index.ts:33`](../packages/workflow/workflow-ptc/src/index.ts) ```ts config-catalog /** Plugin config (all optional — `static Config` supplies the defaults). */ @@ -4286,13 +4392,30 @@ export interface Config { ``` + + + +## `@deepseek-ai/dsh-working-directory` + +- `inject`: `fs` · `sessionProjections` · `systemPrompt` +- `source`: [`packages/session/working-directory/src/index.ts:56`](../packages/session/working-directory/src/index.ts) + +```ts config-catalog +/** Deployment default for Sessions without an original project directory. */ +export interface Config { + /** Absolute execution-world fallback directory; omitted values use the launch directory. */ + defaultDirectory?: string +} +``` + + ## `@deepseek-ai/dsh-workspace-changes` -- `inject`: `subprocess` -- `source`: [`packages/deliverables/workspace-changes/src/index.ts:33`](../packages/deliverables/workspace-changes/src/index.ts) +- `inject`: `subprocess` · `fs` +- `source`: [`packages/deliverables/workspace-changes/src/index.ts:34`](../packages/deliverables/workspace-changes/src/index.ts) ```ts config-catalog /** Snapshot, capture, and comparison bounds. Invalid values fail plugin load. */ @@ -4386,11 +4509,13 @@ export interface Config { | `@deepseek-ai/dsh-config-editor` | `loader` · `profileContext` | [`packages/boot/config-editor/src/index.ts`](../packages/boot/config-editor/src/index.ts) | | `@deepseek-ai/dsh-cordis-client-runner` | — | [`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts) | | `@deepseek-ai/dsh-deepseek-llm-api-extensions` | — | [`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts) | -| `@deepseek-ai/dsh-experimental-auto-review` | `approval` · `llm` · `permissionPresets` · `sessions` · `tools` | [`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts) | +| `@deepseek-ai/dsh-experimental-auto-review` | `approval` · `llm` · `permissionPresets` · `sessions` · `tools` · `workingDirectory` | [`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts) | | `@deepseek-ai/dsh-experimental-client-ui-agent-team` | — | [`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts) | +| `@deepseek-ai/dsh-experimental-client-ui-claude-code-mods` | — | [`packages/experimental/client-ui-claude-code-mods/src/index.ts`](../packages/experimental/client-ui-claude-code-mods/src/index.ts) | | `@deepseek-ai/dsh-experimental-client-ui-voice-input` | — | [`packages/experimental/client-ui-voice-input/src/index.ts`](../packages/experimental/client-ui-voice-input/src/index.ts) | | `@deepseek-ai/dsh-experimental-computer-use-cua-driver-native` | `computerUse` · `tools` · `systemPrompt` | [`packages/experimental/computer-use-cua-driver-native/src/index.ts`](../packages/experimental/computer-use-cua-driver-native/src/index.ts) | | `@deepseek-ai/dsh-experimental-session-inspector` | — | [`packages/experimental/session-inspector/src/index.ts`](../packages/experimental/session-inspector/src/index.ts) | +| `@deepseek-ai/dsh-experimental-tool-worktree` | `tools` · `worktrees` | [`packages/experimental/tool-worktree/src/index.ts`](../packages/experimental/tool-worktree/src/index.ts) | | `@deepseek-ai/dsh-fs-observation-policy` | — | [`packages/fs/fs-observation-policy/src/index.ts`](../packages/fs/fs-observation-policy/src/index.ts) | | `@deepseek-ai/dsh-fs-ssh` | `ssh` · `sandboxPolicy` | [`packages/ssh/fs-ssh/src/index.ts`](../packages/ssh/fs-ssh/src/index.ts) | | `@deepseek-ai/dsh-goal-round-driver` | `agents` · `goals` · `sessions` | [`packages/goal/goal-round-driver/src/index.ts`](../packages/goal/goal-round-driver/src/index.ts) | @@ -4408,7 +4533,7 @@ export interface Config { | `@deepseek-ai/dsh-session-stats` | `sessionProjections` | [`packages/session/session-stats/src/index.ts`](../packages/session/session-stats/src/index.ts) | | `@deepseek-ai/dsh-session-turn-outline` | `sessionProjections` | [`packages/session/session-turn-outline/src/index.ts`](../packages/session/session-turn-outline/src/index.ts) | | `@deepseek-ai/dsh-settings` | `configEditor` · `profileContext` | [`packages/settings/settings/src/index.ts`](../packages/settings/settings/src/index.ts) | -| `@deepseek-ai/dsh-skill-badge` | `skills` | [`packages/skill/skill-badge/src/index.ts`](../packages/skill/skill-badge/src/index.ts) | +| `@deepseek-ai/dsh-skill-badge` | `skills` | [`packages/experimental/skill-badge/src/index.ts`](../packages/experimental/skill-badge/src/index.ts) | | `@deepseek-ai/dsh-storage` | — | [`packages/storage/storage/src/index.ts`](../packages/storage/storage/src/index.ts) | | `@deepseek-ai/dsh-subprocess-local` | — | [`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts) | | `@deepseek-ai/dsh-subprocess-ssh` | `ssh` | [`packages/ssh/subprocess-ssh/src/index.ts`](../packages/ssh/subprocess-ssh/src/index.ts) | @@ -4417,8 +4542,9 @@ export interface Config { | `@deepseek-ai/dsh-tool-cordis` | `tools` · `cordisInspect` | [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts) | | `@deepseek-ai/dsh-tool-schedule` | `tools` | [`packages/schedule/tool-schedule/src/index.ts`](../packages/schedule/tool-schedule/src/index.ts) | | `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) | +| `@deepseek-ai/dsh-tool-working-directory` | `tools` · `workingDirectory` | [`packages/session/tool-working-directory/src/index.ts`](../packages/session/tool-working-directory/src/index.ts) | | `@deepseek-ai/dsh-user-questions` | — | [`packages/interaction/user-questions/src/index.ts`](../packages/interaction/user-questions/src/index.ts) | -| `@deepseek-ai/dsh-webhook` | `agents` · `agentDefaultModel` · `agentPresets` · `permissionPresets` · `sessionTitle` · `workspaceRegistry` | [`packages/webhook/webhook/src/index.ts`](../packages/webhook/webhook/src/index.ts) | +| `@deepseek-ai/dsh-webhook` | `agents` · `agentDefaultModel` · `agentPresets` · `permissionPresets` · `sessionTitle` · `workspaceRegistry` | [`packages/experimental/webhook/src/index.ts`](../packages/experimental/webhook/src/index.ts) | | `@deepseek-ai/dsh-workspace` | `storageDomain` · `sessionPersistence` | [`packages/workspace/workspace/src/index.ts`](../packages/workspace/workspace/src/index.ts) | @@ -4470,13 +4596,19 @@ export interface Config { | `@deepseek-ai/dsh-cmdline` | — | [`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts) | | `@deepseek-ai/dsh-deque` | — | [`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts) | | `@deepseek-ai/dsh-experimental-agent-team-profile` | — | [`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts) | +| `@deepseek-ai/dsh-experimental-badge-skill-bundle` | — | [`packages/experimental/badge-skill-bundle/src/index.ts`](../packages/experimental/badge-skill-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-browser-use-runtime` | — | [`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts) | +| `@deepseek-ai/dsh-experimental-cot-translation-bundle` | — | [`packages/experimental/cot-translation-bundle/src/index.ts`](../packages/experimental/cot-translation-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-inspector-profile` | — | [`packages/experimental/inspector-profile/src/index.ts`](../packages/experimental/inspector-profile/src/index.ts) | +| `@deepseek-ai/dsh-experimental-ralph-bundle` | — | [`packages/experimental/ralph-bundle/src/index.ts`](../packages/experimental/ralph-bundle/src/index.ts) | +| `@deepseek-ai/dsh-experimental-session-search` | — | [`packages/experimental/session-search/src/index.ts`](../packages/experimental/session-search/src/index.ts) | +| `@deepseek-ai/dsh-experimental-session-titles-bundle` | — | [`packages/experimental/session-titles-bundle/src/index.ts`](../packages/experimental/session-titles-bundle/src/index.ts) | +| `@deepseek-ai/dsh-experimental-terminal-bundle` | — | [`packages/experimental/terminal-bundle/src/index.ts`](../packages/experimental/terminal-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-voice-input-bundle` | — | [`packages/experimental/voice-input-bundle/src/index.ts`](../packages/experimental/voice-input-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-webworker-packer` | — | [`packages/experimental/webworker-packer/src/index.ts`](../packages/experimental/webworker-packer/src/index.ts) | | `@deepseek-ai/dsh-experimental-webworker-runtime` | — | [`packages/experimental/webworker-runtime/src/index.ts`](../packages/experimental/webworker-runtime/src/index.ts) | | `@deepseek-ai/dsh-home-paths` | — | [`packages/util/home-paths/src/index.ts`](../packages/util/home-paths/src/index.ts) | -| `@deepseek-ai/dsh-hook-protocol` | — | [`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts) | +| `@deepseek-ai/dsh-hook-protocol` | — | [`packages/experimental/hook-protocol/src/index.ts`](../packages/experimental/hook-protocol/src/index.ts) | | `@deepseek-ai/dsh-http-proxy` | — | [`packages/util/http-proxy/src/index.ts`](../packages/util/http-proxy/src/index.ts) | | `@deepseek-ai/dsh-launch-environment` | — | [`packages/util/launch-environment/src/index.ts`](../packages/util/launch-environment/src/index.ts) | | `@deepseek-ai/dsh-lazy-require` | — | [`packages/util/lazy-require/src/index.ts`](../packages/util/lazy-require/src/index.ts) | @@ -4501,7 +4633,6 @@ export interface Config { | `@deepseek-ai/dsh-session-snapshot` | — | [`packages/test-support/session-snapshot/src/index.ts`](../packages/test-support/session-snapshot/src/index.ts) | | `@deepseek-ai/dsh-session-telemetry` | — | [`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts) | | `@deepseek-ai/dsh-session-title-llm` | — | [`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts) | -| `@deepseek-ai/dsh-subagent-in-process-driver` | — | [`packages/subagent/subagent-in-process-driver/src/index.ts`](../packages/subagent/subagent-in-process-driver/src/index.ts) | | `@deepseek-ai/dsh-timeout` | — | [`packages/util/timeout/src/index.ts`](../packages/util/timeout/src/index.ts) | | `@deepseek-ai/dsh-typert-generator` | — | [`packages/typert/generator/src/index.ts`](../packages/typert/generator/src/index.ts) | | `@deepseek-ai/dsh-typert-protocol` | — | [`packages/typert/protocol/src/index.ts`](../packages/typert/protocol/src/index.ts) | diff --git a/docs/cookbook/adding-a-session-format-version.i18n.yaml b/docs/cookbook/adding-a-session-format-version.i18n.yaml index f0c70b2798..e37df25963 100644 --- a/docs/cookbook/adding-a-session-format-version.i18n.yaml +++ b/docs/cookbook/adding-a-session-format-version.i18n.yaml @@ -18,8 +18,8 @@ en: 77c9c26d94fb33ca zh: 313c54c45cfc7dbc /cookbook-adding-a-session-log-format-version/3-implement-per-artifact-stages-and-validation: - en: 2bea8b93ac9b359a - zh: 2c24e71680f66ef7 + en: c8d879d5c9fb14ed + zh: ea19e54c808fea0b /cookbook-adding-a-session-log-format-version/4-update-current-version-consumers: en: a285126945d02c12 zh: e039c77cf7866ad2 diff --git a/docs/cookbook/adding-a-session-format-version.md b/docs/cookbook/adding-a-session-format-version.md index b962ffb1c1..7c6d08b993 100644 --- a/docs/cookbook/adding-a-session-format-version.md +++ b/docs/cookbook/adding-a-session-format-version.md @@ -58,6 +58,8 @@ Treat the inherited cut as a logical event count, not a physical row count. Expo Define the new edge's event admission and transformation rules explicitly. The [V2-to-V3 source audit](../../packages/session/session-format-v2-to-v3/README.md#source-audit) and [alpha V0→V1 rule](../../.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.md) own the policies of those released edges, not the new edge. Do not generalize either to every edge. A change to structure or event positions requires classifying source events, payload members, and references, and explicitly deciding whether opaque data can remain valid. [Equal-version retention](../../.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md) alone does not prove a structural transformation safe. Validate target semantics and give each newly accepted case a rejecting counterexample; never widen older edges to hide an unsupported transformation. +Plugin records that `appendPluginRecord()` writes are ignorable events in the `plugin:` namespace, which experimental packages read back by type. Carry them forward under their names when the target format can represent them, and state in the edge README which records it drops otherwise; the [ignorable events decision](../../.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md) keeps them on a best-effort basis. Apply this policy even when a stored record has no declaration in the [experimental persistence catalog](../experimental-persistence-catalog.md). + Use native writer output as the conversion oracle: replay the same recorded LLM messages, tool outputs, and other inputs through the old and target writers. Compare migrated old output with raw target output before any format-normalizing helper; mask only documented volatile fields. For V4, ordinary tool results become flat tool-role messages with ordinary content blocks. A theoretically permitted old structure alone does not justify a new core content type. Alpha converters may explicitly refuse historical cases without an implemented, evidenced mapping, including otherwise valid logs. Keep the source unchanged and publish no partial successor. Prefer real corpora and reproducible writers when extending support; inspect user corpora read-only and create sanitized regression fixtures. diff --git a/docs/cookbook/adding-a-session-format-version.zh.md b/docs/cookbook/adding-a-session-format-version.zh.md index 398d77addd..bfadeab878 100644 --- a/docs/cookbook/adding-a-session-format-version.zh.md +++ b/docs/cookbook/adding-a-session-format-version.zh.md @@ -58,6 +58,8 @@ pnpm run gen-session-format-catalog 显式定义新迁移边的事件准入与变换规则。[V2 到 V3 源审计](../../packages/session/session-format-v2-to-v3/README.zh.md#source-audit)和 [Alpha V0→V1 规则](../../.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.zh.md)分别负责对应已发布迁移边的策略,而非新迁移边的策略。不要将任一策略推广到所有迁移边。结构或事件位置变化时,必须分类源事件、载荷成员与引用,并显式判断不透明数据能否保持有效。[同版本保留](../../.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.zh.md)本身不能证明结构变换安全。校验目标语义,并为每个新增可接受案例提供一个被拒绝的反例;绝不放宽旧迁移边来掩盖不受支持的转换。 +`appendPluginRecord()` 写入的插件记录是 `plugin:` 命名空间中的 ignorable 事件,实验性包按类型把它们读回。目标格式能够表示它们时,以原名称继续携带;否则在迁移边 README 中说明它丢弃哪些记录。[ignorable 事件决策](../../.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.zh.md)以尽力而为的方式保留这些记录。即使已存记录在[实验性持久化目录](../experimental-persistence-catalog.zh.md)中没有声明,也适用这一策略。 + 以原生 writer 输出作为转换对照:用相同录制的 LLM 消息、工具输出和其他输入回放旧 writer 与目标 writer。任何格式归一化 helper 运行前,比较迁移后的旧输出与目标原始输出,只屏蔽已说明的易变字段。V4 的普通工具结果变为包含普通内容块的平铺 tool-role 消息。旧类型理论上允许某种结构,本身不足以支持新增核心内容类型。 Alpha 转换器可以明确拒绝尚无已实现、有证据映射的历史场景,包括原本有效的日志。源文件保持不变,且不发布部分 successor。扩展支持时优先使用真实语料和可复现 writer;只读检查用户语料,并创建脱敏回归 fixture。 diff --git a/docs/cookbook/reviewing-persistence-type-changes.md b/docs/cookbook/reviewing-persistence-type-changes.md index bf3810453c..15b6c9fab9 100644 --- a/docs/cookbook/reviewing-persistence-type-changes.md +++ b/docs/cookbook/reviewing-persistence-type-changes.md @@ -57,12 +57,12 @@ Save the input as `.artifacts/persistence-change.prose.json`, creating the ignor "en": { "summary": "Makes the persisted hook audit decision optional.", "compatibility": "Existing records remain valid. Hook execution consumes HookOutput instead of replaying this audit field. Producers still write decisions, and absence does not imply pass.", - "verification": "pnpm exec vitest run packages/hooks/hook-protocol/tests/events.spec.ts: 10 tests passed." + "verification": "pnpm exec vitest run packages/experimental/hook-protocol/tests/events.spec.ts: 10 tests passed." }, "zh": { "summary": "将持久化的钩子审计决策改为可选。", "compatibility": "已有记录仍然有效。钩子执行消费 HookOutput,不回放此审计字段。写入方仍然记录决策,缺失不代表 pass。", - "verification": "pnpm exec vitest run packages/hooks/hook-protocol/tests/events.spec.ts:10 个测试通过。" + "verification": "pnpm exec vitest run packages/experimental/hook-protocol/tests/events.spec.ts:10 个测试通过。" } } ``` diff --git a/docs/cookbook/reviewing-persistence-type-changes.zh.md b/docs/cookbook/reviewing-persistence-type-changes.zh.md index 4e31658b6b..ed82decae9 100644 --- a/docs/cookbook/reviewing-persistence-type-changes.zh.md +++ b/docs/cookbook/reviewing-persistence-type-changes.zh.md @@ -57,12 +57,12 @@ pnpm --silent run persistence-review --before .artifacts/base.schema.json --afte "en": { "summary": "Makes the persisted hook audit decision optional.", "compatibility": "Existing records remain valid. Hook execution consumes HookOutput instead of replaying this audit field. Producers still write decisions, and absence does not imply pass.", - "verification": "pnpm exec vitest run packages/hooks/hook-protocol/tests/events.spec.ts: 10 tests passed." + "verification": "pnpm exec vitest run packages/experimental/hook-protocol/tests/events.spec.ts: 10 tests passed." }, "zh": { "summary": "将持久化的钩子审计决策改为可选。", "compatibility": "已有记录仍然有效。钩子执行消费 HookOutput,不回放此审计字段。写入方仍然记录决策,缺失不代表 pass。", - "verification": "pnpm exec vitest run packages/hooks/hook-protocol/tests/events.spec.ts:10 个测试通过。" + "verification": "pnpm exec vitest run packages/experimental/hook-protocol/tests/events.spec.ts:10 个测试通过。" } } ``` diff --git a/docs/deepseek-llm-api-wire-extensions.i18n.yaml b/docs/deepseek-llm-api-wire-extensions.i18n.yaml index 64bde5b974..ca1ec47310 100644 --- a/docs/deepseek-llm-api-wire-extensions.i18n.yaml +++ b/docs/deepseek-llm-api-wire-extensions.i18n.yaml @@ -15,8 +15,8 @@ en: f0b1443c364a431c zh: 16d16ddf5c451e8e /official-deepseek-llm-api-wire-extensions/dsh-plugin-packages: - en: 49d6f011e9d985a7 - zh: bbf61b4e4480c555 + en: f89059050613f3e8 + zh: da5cf2a4eeb81b75 /official-deepseek-llm-api-wire-extensions/dsh-session-log: en: de1a3f2c377d74e3 zh: 7c803345f71117c5 diff --git a/docs/deepseek-llm-api-wire-extensions.md b/docs/deepseek-llm-api-wire-extensions.md index 790b51fd88..216725d71d 100644 --- a/docs/deepseek-llm-api-wire-extensions.md +++ b/docs/deepseek-llm-api-wire-extensions.md @@ -61,11 +61,11 @@ After the configured endpoint returns HTTP 2xx, the adapter runs the prepared `a | `version` | `1` | Schema version for `dsh_plugin_packages` | | `packages` | array | Complete active set for this request | | `packages[].name` | string | Exact non-empty npm package name from the owning manifest | -| `packages[].version` | string | Exact non-empty package version from the same manifest | +| `packages[].version` | optional string | Exact non-blank package version from the same manifest; absent when unavailable | -Every request re-reads active non-group Loader entries from the host tree and, when available for the request Session, its standing agent-preset tree. Relative and absolute modules use their nearest owning manifest; bare package entries follow the Loader resolution base that activated them. A named manifest without a non-empty version fails request preparation. +Every request re-reads active non-group Loader entries from the host tree and, when available for the request Session, its standing agent-preset tree. Relative and absolute modules use their nearest owning manifest; bare package entries follow the Loader resolution base that activated them. A named manifest without a non-blank string version contributes `{ name }`, regardless of `private`. Missing names and unreadable manifests omit only the affected entries; package metadata does not block requests. -The sender deduplicates exact `(name, version)` pairs and sorts first by `name`, then by `version`, with a locale-independent text comparison. Simultaneously active versions of one package remain separate entries. Receivers must not collapse the array by package name or infer package activation from array order. +The sender deduplicates exact `(name, optional version)` pairs and sorts first by `name`, then by `version`, with a locale-independent text comparison and absent versions first. Simultaneously active versions of one package remain separate entries, including a name-only entry. Receivers must accept absent versions, must not collapse the array by package name, and must not infer package activation from array order. Disabled, pending, failed, unloading, disposed, and structural Loader entries are absent. Ordinary dependencies, loose modules without a named owning package, programmatically mounted child fibers, and in-memory dynamic plugins are also absent because they have no authoritative Loader-backed package identity. diff --git a/docs/deepseek-llm-api-wire-extensions.zh.md b/docs/deepseek-llm-api-wire-extensions.zh.md index 08be493fcc..33c8908ae5 100644 --- a/docs/deepseek-llm-api-wire-extensions.zh.md +++ b/docs/deepseek-llm-api-wire-extensions.zh.md @@ -61,11 +61,11 @@ | `version` | `1` | `dsh_plugin_packages` 的 schema 版本 | | `packages` | 数组 | 本次请求的完整存活集合 | | `packages[].name` | 字符串 | 来自所属 manifest(元数据清单)的确切非空 npm 包名 | -| `packages[].version` | 字符串 | 来自同一 manifest 的确切非空包版本 | +| `packages[].version` | 可选字符串 | 来自同一 manifest 的确切非空白包版本;不可用时省略 | -每个请求都会重新读取宿主树中的存活非分组 Loader 配置项;请求会话存在 standing agent-preset 树时,也会读取该树。相对与绝对模块使用距离自身最近的所属 manifest;裸包配置项使用激活自身的 Loader 解析基准。具名 manifest 未提供非空版本时,请求准备会失败。 +每个请求都会重新读取宿主树中的存活非分组 Loader 配置项;请求会话存在 standing agent-preset 树时,也会读取该树。相对与绝对模块使用距离自身最近的所属 manifest;裸包配置项使用激活自身的 Loader 解析基准。具名 manifest 的版本不是非空白字符串时,会贡献 `{ name }`,不受 `private` 影响。名称缺失或 manifest 不可读时,只省略对应配置项;包元数据不会阻塞请求。 -发送方会对确切 `(name, version)` 组合去重,并使用与 locale 无关的文本比较,先按 `name`、再按 `version` 排序。同一包的多个同时存活版本会保留为独立配置项。接收方不得按包名折叠该数组,也不得根据数组顺序推断包的激活关系。 +发送方会对确切 `(name, 可选 version)` 组合去重,并使用与 locale 无关的文本比较,先按 `name`、再按 `version` 排序,缺失版本排在前面。同一包的多个同时存活版本会保留为独立配置项,包括只有名称的配置项。接收方必须接受版本缺失,不得按包名折叠该数组,也不得根据数组顺序推断包的激活关系。 该清单不包含已禁用、pending、failed、unloading、disposed 和结构性 Loader 配置项。普通依赖、没有具名所属包的松散模块、以编程方式挂载的子 fiber,以及内存动态插件也不在其中,因为它们没有权威的 Loader 包来源信息。 diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml index 0a4130454d..2a08605a85 100644 --- a/docs/development.i18n.yaml +++ b/docs/development.i18n.yaml @@ -9,8 +9,8 @@ en: 5207d17725c07a27 zh: 258884c4300ac63a /development-guide/setup-tutorial/prerequisites: - en: 6078798f0fda1347 - zh: ea73704c4ec4f2a9 + en: ae47d95cd48bbb1d + zh: 2378e6ed3305e28d /development-guide/setup-tutorial/windows-and-wsl-2: en: e86be579be6f1813 zh: 14f6a220337d0861 @@ -21,8 +21,8 @@ en: 3221d1076554001f zh: 29f166be6a5c04d9 /development-guide/contributor-reference/typescript-project-layout: - en: 42c03e89df4379e3 - zh: 9df43176aa64d047 + en: e3327c5492a593cb + zh: d43a5edcb11523ae /development-guide/contributor-reference/environment-variables: en: dba2c72ee6ffa930 zh: b65c27fbb9129ec8 @@ -30,8 +30,8 @@ en: dc98eda4f4021feb zh: b08766708803cf10 /development-guide/contributor-reference/ci-gates: - en: 8fa1aca14c548969 - zh: a2bd1199fa7f7a8b + en: b64a914e41b1d285 + zh: 6c031b507c3c0e84 /development-guide/contributor-reference/daily-commands: en: 08aa6ec1cdabfeb7 zh: dcce5ae9d3c829d0 diff --git a/docs/development.md b/docs/development.md index 847f4235a9..668beec326 100644 --- a/docs/development.md +++ b/docs/development.md @@ -9,7 +9,8 @@ The setup tutorial takes a new contributor from prerequisites to a checked check ### Prerequisites - Node.js supports 22.19+ and 24+. CI covers 22.19, 24, and 26; see the [Node engine floor Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.md). -- Corepack-enabled pnpm. The repo pins `pnpm@11.7.0` in `package.json`; run `corepack enable` if `pnpm --version` does not resolve through Corepack. +- Node.js TypeScript type stripping enabled. The repository build scripts load `tsdown.config.ts` with tsdown's native config loader, so they fail when `--no-experimental-strip-types` is in `NODE_OPTIONS` or the Node.js build lacks TypeScript support; `pnpm run build` checks this first and names the cause. +- Corepack-enabled pnpm. The repo pins `pnpm@11.28.5` in `package.json`; run `corepack enable` if `pnpm --version` does not resolve through Corepack. - Git 2.26 or newer; hook setup enables Git's worktree-specific configuration extension. - Optional: a DeepSeek API key for the Web, headless, and ACP automation demos and real-API e2e tests. @@ -84,6 +85,8 @@ tsdown --env.DSH_BUILD_FACE client pnpm run build:web ``` +`pnpm run build --artifacts-only` emits the same project and application artifacts while checking and compiling only the existing package references of each compiler face. Required Linux and Windows normal builds and public `typecheck` also check the repository-wide test and script programs. CI benchmark preparation additionally uses `--noCheck` for its disposable library outputs, relying on those required builds for diagnostics; other artifact builders keep package checking ([rationale](../.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.md)). + Both tsdown passes match `vendor/*`, `packages/*/*`, and `apps/cli`; the Host pass also matches `apps/desktop-host`. They neither scan build artifacts to discover Client packages nor maintain a Host/Client package filter list. Package-local tsdown configs select entries for the current phase through `DSH_BUILD_FACE`: an ordinary Client plugin produces both its Node loader and browser bundle during the Client phase; `api-remotes` uses `hostPhase: true` to produce its Host entry early and only its browser bundle during the Client phase. Tsdown consumes only the JavaScript emitted to `lib/types` by the preceding tsc phase. Tsdown builds the matched workspace members concurrently, so `apps/desktop`, whose main bundle inlines workspace devDependencies from their `lib/` output, bundles in its own step after the Host pass ([Desktop README](../apps/desktop/README.md#bundled-workspace-dependencies)). Typert runs only during Host tsdown, seeded by `tsconfig.host.json`. It analyzes Host types and generates both Host reflection artifacts and the Host-for-Client Remote projection; Client tsdown does not start Typert. Consequently, `pnpm run typecheck` runs the complete Host lib phase before Client tsc, while `pnpm run build` continues through Client tsdown and the Web build. @@ -131,7 +134,7 @@ Contributors can opt into the comprehensive local gate set with `pnpm run check: ### CI gates -The keyless [CI workflow](../.github/workflows/ci.yml) groups independent gates into broad lanes and runs a smaller compatibility signal across supported Node versions. Artifact consumers wait for one build within their lane. Required benchmarks run separately on standard GitHub-hosted Linux; the [benchmark runner reference](../benchmarks/AGENTS.md) owns routing and the job timeout. The separate real-API workflow runs `pnpm run test:e2e` with its configured worker bound. See [scripts/run-gates.ts](../scripts/run-gates.ts) and the workflow files for the current gate and job inventory. +The keyless [CI workflow](../.github/workflows/ci.yml) groups independent gates into broad lanes and runs a smaller compatibility signal across supported Node versions. Artifact consumers wait for one build within their lane. Coverage partitions reuse file timings scoped to their platform and runner pool to balance work within the configured worker limit. Required benchmarks run separately on standard GitHub-hosted Linux; the [benchmark runner reference](../benchmarks/AGENTS.md) owns routing and the job timeout. The separate real-API workflow runs `pnpm run test:e2e` with its configured worker bound. See [scripts/run-gates.ts](../scripts/run-gates.ts) and the workflow files for the current gate and job inventory. The credential-free dsh dependency-layout and dsh/vendor pack rehearsals use the existing Linux self-hosted pool only when `DSH_CI_FAILOVER_LINUX=selfhosted` and the event is a trusted master push or same-repository, non-fork, non-Dependabot pull request. All other cases, including manual dispatch, use `ubuntu-24.04`; manual publication stays hosted. See the [release rehearsal runner reference](../.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md) for persistent-store isolation and fallback limits. diff --git a/docs/development.zh.md b/docs/development.zh.md index eb56870e25..3154c4d988 100644 --- a/docs/development.zh.md +++ b/docs/development.zh.md @@ -11,7 +11,8 @@ ### 前置条件 - Node.js 支持 22.19+ 与 24+。CI 覆盖 22.19、24 和 26;见 [Node 引擎下限 Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.zh.md)。 -- 启用了 Corepack 的 pnpm。仓库在 `package.json` 中固定使用 `pnpm@11.7.0`;如果 `pnpm --version` 无法通过 Corepack 解析,请先运行 `corepack enable`。 +- 启用 Node.js TypeScript 类型剥离。仓库构建脚本用 tsdown 的 native 配置加载器加载 `tsdown.config.ts`,因此当 `NODE_OPTIONS` 含 `--no-experimental-strip-types` 或 Node.js 构建缺少 TypeScript 支持时会失败;`pnpm run build` 会先检查这一条件并指出原因。 +- 启用了 Corepack 的 pnpm。仓库在 `package.json` 中固定使用 `pnpm@11.28.5`;如果 `pnpm --version` 无法通过 Corepack 解析,请先运行 `corepack enable`。 - Git 2.26 或更高版本;钩子设置会启用 Git 的 worktree 专属配置扩展。 - 可选:一个 DeepSeek API key,用于 Web、headless 和 ACP(Agent Client Protocol)自动化 agent(智能体)演示以及真实 API 的 e2e 测试。 @@ -88,6 +89,8 @@ tsdown --env.DSH_BUILD_FACE client pnpm run build:web ``` +`pnpm run build --artifacts-only` 产出相同的 project 和应用产物,但只检查并编译各 compiler face 的现有包 reference。必需的 Linux 与 Windows 普通构建以及公开 `typecheck` 还检查仓库级测试和脚本 program。CI benchmark 准备阶段还对其临时 library 输出使用 `--noCheck`,由这些必需构建负责诊断;其他产物 builder 保留包检查([理由](../.agents/notes/implemented/process/2026-10-05-pr-artifact-typechecks.zh.md))。 + 两次 tsdown 都匹配 `vendor/*`、`packages/*/*` 与 `apps/cli`,Host 阶段另外匹配 `apps/desktop-host`;两者都不扫描构建产物来发现 Client 包,也不维护 Host/Client 包过滤表。包内 tsdown 配置根据 `DSH_BUILD_FACE` 决定当前阶段的入口:普通 Client 插件在 Client 阶段同时生成 Node loader 与 browser bundle;`api-remotes` 通过 `hostPhase: true` 提前生成 Host 入口,再在 Client 阶段只生成 browser bundle。tsdown 只消费 `lib/types` 中由前置 tsc 发射的 JavaScript。tsdown 并发构建匹配到的 workspace 成员;`apps/desktop` 的主进程 bundle 需要从工作区 devDependencies 的 `lib/` 产物内联这些包,因此在 Host 阶段之后单独一步打 bundle([Desktop README](../apps/desktop/README.zh.md#bundled-workspace-dependencies))。 Typert 只在 Host tsdown 中以 `tsconfig.host.json` 为种子运行。它分析 Host 类型并生成 Host 反射产物及 Host-for-Client Remote 投影;Client tsdown 不启动 Typert。`pnpm run typecheck` 因此先执行完整 Host lib 阶段,再运行 Client tsc;`pnpm run build` 继续执行 Client tsdown 和 Web 构建。 @@ -135,7 +138,7 @@ vendor manifest 守卫检查 `vendor/*/src` 下的改动是否连同对应的 `v ### CI 门禁 -keyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若干宽粒度 lane,并在受支持的 Node 版本上运行一组较小的兼容性检查。产物消费方在各自 lane 内等待一次 build。必需 benchmark 在标准 GitHub 托管 Linux 上独立运行;[benchmark 运行器说明](../benchmarks/AGENTS.md)拥有路由及 job 超时。单独的真实 API 工作流按其配置的 worker 上限运行 `pnpm run test:e2e`。当前门禁和 job 清单以 [scripts/run-gates.ts](../scripts/run-gates.ts) 和工作流文件为准。 +keyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若干宽粒度 lane,并在受支持的 Node 版本上运行一组较小的兼容性检查。产物消费方在各自 lane 内等待一次 build。覆盖率分区复用按平台和运行器池隔离的文件耗时,在配置的 worker 上限内均衡工作量。必需 benchmark 在标准 GitHub 托管 Linux 上独立运行;[benchmark 运行器说明](../benchmarks/AGENTS.md)拥有路由及 job 超时。单独的真实 API 工作流按其配置的 worker 上限运行 `pnpm run test:e2e`。当前门禁和 job 清单以 [scripts/run-gates.ts](../scripts/run-gates.ts) 和工作流文件为准。 不带凭据的 dsh 依赖布局检查与 dsh/vendor 打包演练仅在 `DSH_CI_FAILOVER_LINUX=selfhosted`,且事件为受信任的 master 推送或同仓库、非 fork、非 Dependabot 拉取请求时使用现有 Linux 自托管池。其余情况(包括手动触发)均使用 `ubuntu-24.04`;手动发布仍使用托管运行器。持久化存储隔离与回退限制见[发布演练运行器说明](../.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md)。 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 3ed51c618a..5adc51ba90 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -11,23 +11,23 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:238`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | -| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) | +| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | -| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | -| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) | +| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | +| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) | | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | -| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | -| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | +| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:611`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:591`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:618`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:597`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:604`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:52`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) | +| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:56`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - | | `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` | @@ -46,39 +46,39 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `deepseek-account/signed-out` | `emit` | [`packages/credentials/deepseek-account/src/index.ts:12`](../packages/credentials/deepseek-account/src/index.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | [`deepseek-account`](../packages/credentials/deepseek-account) | | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `workspace-controller` | | `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | -| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | +| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:77`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem), `workspace-files` | -| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:59`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | +| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:59`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `goal/activation-changed` | `emit` | [`packages/goal/goal/src/types.ts:150`](../packages/goal/goal/src/types.ts) | [`goal`](../packages/goal/goal) (`emit`) | `remotes` | | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | -| `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:30`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - | -| `hmr/reload` | `emit` | [`packages/boot/hmr/src/index.ts:35`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - | +| `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:32`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - | +| `hmr/reload` | `emit` | [`packages/boot/hmr/src/index.ts:37`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - | | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `remotes` | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | | `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:48`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` | -| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:250`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | -| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:256`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | -| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:263`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | +| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:264`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | +| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:270`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | +| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:277`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`permission-presets`](../packages/interaction/permission-presets), [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:82`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:172`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server` | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:154`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:164`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | -| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | +| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`working-directory`](../packages/session/working-directory) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | -| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:208`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:164`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` | -| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:176`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | -| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:153`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:190`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | -| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:198`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`tool-present`](../packages/deliverables/tool-present) | +| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:165`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` | +| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:177`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | +| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | +| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) | | `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` | | `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` | | `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) | @@ -100,7 +100,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `internal/dispatch` | - | [`terminal-bash`](../packages/terminal/terminal-bash) | | `internal/plugin` | - | `computer-use-cua-driver-native`, `inspector`, `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), [`mcp-client`](../packages/mcp/mcp-client), `modules` | | `internal/service` | - | `gateway` | -| `internal/status` | - | [`agent`](../packages/core/agent), `inspector`, [`web`](../packages/web/web) | +| `internal/status` | - | [`agent`](../packages/core/agent), `inspector`, `translator`, [`web`](../packages/web/web) | | `internal/update` | - | [`app-boot`](../packages/boot/app-boot) | | `loader/volatile-update` | - | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `product-analytics`, `speech-to-text` | | `slots/changed` | `ui-renderer` (`emit`) | - | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 911434f569..0133716910 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -13,23 +13,23 @@ | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:238`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | -| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) | +| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | -| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | -| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) | +| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | +| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) | | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | -| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | -| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | +| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:611`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:591`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:618`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:597`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:604`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:52`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) | +| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:56`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - | | `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` | @@ -48,39 +48,39 @@ | `deepseek-account/signed-out` | `emit` | [`packages/credentials/deepseek-account/src/index.ts:12`](../packages/credentials/deepseek-account/src/index.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | [`deepseek-account`](../packages/credentials/deepseek-account) | | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `workspace-controller` | | `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | -| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | +| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:77`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem), `workspace-files` | -| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:59`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | +| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:59`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `goal/activation-changed` | `emit` | [`packages/goal/goal/src/types.ts:150`](../packages/goal/goal/src/types.ts) | [`goal`](../packages/goal/goal) (`emit`) | `remotes` | | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | -| `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:30`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - | -| `hmr/reload` | `emit` | [`packages/boot/hmr/src/index.ts:35`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - | +| `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:32`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - | +| `hmr/reload` | `emit` | [`packages/boot/hmr/src/index.ts:37`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - | | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `remotes` | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | | `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:48`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` | -| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:250`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | -| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:256`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | -| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:263`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | +| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:264`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | +| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:270`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | +| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:277`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`permission-presets`](../packages/interaction/permission-presets), [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:82`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:172`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server` | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:154`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:164`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | -| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | +| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`working-directory`](../packages/session/working-directory) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | -| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:208`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:164`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` | -| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:176`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | -| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:153`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:190`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | -| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:198`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`tool-present`](../packages/deliverables/tool-present) | +| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:165`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` | +| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:177`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | +| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | +| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) | | `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` | | `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` | | `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) | @@ -102,7 +102,7 @@ | `internal/dispatch` | - | [`terminal-bash`](../packages/terminal/terminal-bash) | | `internal/plugin` | - | `computer-use-cua-driver-native`, `inspector`, `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), [`mcp-client`](../packages/mcp/mcp-client), `modules` | | `internal/service` | - | `gateway` | -| `internal/status` | - | [`agent`](../packages/core/agent), `inspector`, [`web`](../packages/web/web) | +| `internal/status` | - | [`agent`](../packages/core/agent), `inspector`, `translator`, [`web`](../packages/web/web) | | `internal/update` | - | [`app-boot`](../packages/boot/app-boot) | | `loader/volatile-update` | - | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `product-analytics`, `speech-to-text` | | `slots/changed` | `ui-renderer` (`emit`) | - | diff --git a/docs/experimental-persistence-catalog.i18n.yaml b/docs/experimental-persistence-catalog.i18n.yaml new file mode 100644 index 0000000000..a65825ab71 --- /dev/null +++ b/docs/experimental-persistence-catalog.i18n.yaml @@ -0,0 +1,10 @@ +# Bilingual-pair consistency record for experimental-persistence-catalog.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/experimental-persistence-catalog.md +/: + en: a11f106978c9dce7 + zh: a11f106978c9dce7 +/experimental-plugin-record-catalog: + en: 84dbc9c06ab92877 + zh: b82ef2a46292ab0e diff --git a/docs/experimental-persistence-catalog.md b/docs/experimental-persistence-catalog.md new file mode 100644 index 0000000000..0868849c51 --- /dev/null +++ b/docs/experimental-persistence-catalog.md @@ -0,0 +1,17 @@ + + +# Experimental Plugin Record Catalog + +English | [中文](experimental-persistence-catalog.zh.md) + +These current-source PluginRecordMap declarations describe ignorable, log-only records owned by experimental packages. Payload annotations link to their owning source without expanding referenced types. Records stay outside the known-event set, persistence schema fingerprints, and compatibility history; format migrations retain them on a best-effort basis. + +Run `pnpm run gen-persistence-catalog` to regenerate both persistence catalogs, their language pairs, and associated artifacts. `pnpm run verify-persistence-catalog` checks all generated files. + +Shared Session event declarations and resolved schemas appear in the [Session persistence event catalog](persistence-catalog.md). + +| Record | Owner | Purpose | Payload and source | +|---|---|---|---| +| `plugin:translator/request` | `@deepseek-ai/dsh-experimental-translator` | Provider-independent translation input, retained before dispatch. | TranslationRequestRecord · [`packages/experimental/translator/src/types.ts`](../packages/experimental/translator/src/types.ts) | +| `plugin:translator/result` | `@deepseek-ai/dsh-experimental-translator` | Successful translation, retained before it is returned to the consumer. | TranslationResultRecord · [`packages/experimental/translator/src/types.ts`](../packages/experimental/translator/src/types.ts) | diff --git a/docs/experimental-persistence-catalog.zh.md b/docs/experimental-persistence-catalog.zh.md new file mode 100644 index 0000000000..32cb1d0533 --- /dev/null +++ b/docs/experimental-persistence-catalog.zh.md @@ -0,0 +1,17 @@ + + +# 实验性插件记录目录 + +[English](experimental-persistence-catalog.md) | 中文 + +这些当前源码中的 PluginRecordMap 声明描述实验性包拥有的 ignorable、仅日志记录。载荷类型注解链接到所属源码,不展开引用类型。记录不进入已知事件集合、持久化 schema 指纹或兼容性历史;格式迁移以尽力而为的方式保留它们。 + +运行 `pnpm run gen-persistence-catalog` 可重新生成两份持久化目录及其中英配对和关联制品。`pnpm run verify-persistence-catalog` 检查所有生成文件。 + +共享 Session 事件声明与解析后的 schema 见[会话持久化事件目录](persistence-catalog.zh.md)。 + +| 记录 | 所属包 | 用途 | 载荷与源码 | +|---|---|---|---| +| `plugin:translator/request` | `@deepseek-ai/dsh-experimental-translator` | Provider-independent translation input, retained before dispatch. | TranslationRequestRecord · [`packages/experimental/translator/src/types.ts`](../packages/experimental/translator/src/types.ts) | +| `plugin:translator/result` | `@deepseek-ai/dsh-experimental-translator` | Successful translation, retained before it is returned to the consumer. | TranslationResultRecord · [`packages/experimental/translator/src/types.ts`](../packages/experimental/translator/src/types.ts) | diff --git a/docs/module-graph.md b/docs/module-graph.md index 7aa818fae4..63a00c948d 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -64,7 +64,6 @@ flowchart TD end subgraph group_skill["packages/skill"] pkg_skill["skill"] - pkg_skill_badge["skill-badge"] pkg_skill_filesystem["skill-filesystem"] pkg_skill_office["skill-office"] pkg_tool_skill["tool-skill"] @@ -77,7 +76,6 @@ flowchart TD pkg_subagent_codex["subagent-codex"] pkg_subagent_dsh_sdk["subagent-dsh-sdk"] pkg_subagent_fork_in_process["subagent-fork-in-process"] - pkg_subagent_in_process_driver["subagent-in-process-driver"] pkg_subagent_spawn_in_process["subagent-spawn-in-process"] pkg_tool_subagent["tool-subagent"] pkg_tool_subagent_control["tool-subagent-control"] @@ -101,16 +99,10 @@ flowchart TD subgraph group_plan["packages/plan"] pkg_plan_mode["plan-mode"] end - subgraph group_hooks["packages/hooks"] - pkg_hook_protocol["hook-protocol"] - pkg_hooks_claude_code["hooks-claude-code"] - pkg_hooks_codex["hooks-codex"] - end subgraph group_session_query["packages/session-query"] pkg_session_log_export["session-log-export"] pkg_session_query["session-query"] pkg_session_query_sqlite["session-query-sqlite"] - pkg_tool_session_query["tool-session-query"] end subgraph group_acp["packages/acp"] pkg_acp["acp"] @@ -250,24 +242,46 @@ flowchart TD pkg_experimental_agent_team_profile["experimental-agent-team-profile"] pkg_experimental_api_speech_to_text["experimental-api-speech-to-text"] pkg_experimental_auto_review["experimental-auto-review"] + pkg_experimental_badge_skill_bundle["experimental-badge-skill-bundle"] pkg_experimental_browser_use_chrome_devtools_mcp["experimental-browser-use-chrome-devtools-mcp"] pkg_experimental_browser_use_playwright_mcp["experimental-browser-use-playwright-mcp"] pkg_experimental_browser_use_runtime["experimental-browser-use-runtime"] pkg_experimental_browser_use_stagehand_native["experimental-browser-use-stagehand-native"] + pkg_experimental_claude_code_mods["experimental-claude-code-mods"] pkg_experimental_client_ui_agent_team["experimental-client-ui-agent-team"] + pkg_experimental_client_ui_claude_code_mods["experimental-client-ui-claude-code-mods"] + pkg_experimental_client_ui_cot_translation["experimental-client-ui-cot-translation"] pkg_experimental_client_ui_voice_input["experimental-client-ui-voice-input"] pkg_experimental_computer_use_cua_driver_mcp["experimental-computer-use-cua-driver-mcp"] pkg_experimental_computer_use_cua_driver_native["experimental-computer-use-cua-driver-native"] + pkg_experimental_cot_translation_bundle["experimental-cot-translation-bundle"] pkg_experimental_inspector["experimental-inspector"] pkg_experimental_inspector_profile["experimental-inspector-profile"] pkg_experimental_ptc_runtime_python["experimental-ptc-runtime-python"] + pkg_experimental_ralph_bundle["experimental-ralph-bundle"] pkg_experimental_session_inspector["experimental-session-inspector"] + pkg_experimental_session_search["experimental-session-search"] + pkg_experimental_session_titles_bundle["experimental-session-titles-bundle"] pkg_experimental_speech_to_text["experimental-speech-to-text"] pkg_experimental_speech_to_text_sensevoice["experimental-speech-to-text-sensevoice"] + pkg_experimental_terminal_bundle["experimental-terminal-bundle"] pkg_experimental_tool_agent_team["experimental-tool-agent-team"] + pkg_experimental_tool_worktree["experimental-tool-worktree"] + pkg_experimental_translator["experimental-translator"] pkg_experimental_voice_input_bundle["experimental-voice-input-bundle"] pkg_experimental_webworker_packer["experimental-webworker-packer"] pkg_experimental_webworker_runtime["experimental-webworker-runtime"] + pkg_experimental_worktree["experimental-worktree"] + pkg_hook_protocol["hook-protocol"] + pkg_hooks_claude_code["hooks-claude-code"] + pkg_hooks_codex["hooks-codex"] + pkg_session_title_all_prompts_llm["session-title-all-prompts-llm"] + pkg_skill_badge["skill-badge"] + pkg_tool_ralph["tool-ralph"] + pkg_tool_session_query["tool-session-query"] + pkg_tool_terminal["tool-terminal"] + pkg_webhook["webhook"] + pkg_webhook_github["webhook-github"] end subgraph group_extensions["packages/extensions"] pkg_client_ui_cordis["client-ui-cordis"] @@ -359,10 +373,11 @@ flowchart TD pkg_session_telemetry["session-telemetry"] pkg_session_telemetry_otel["session-telemetry-otel"] pkg_session_title["session-title"] - pkg_session_title_all_prompts_llm["session-title-all-prompts-llm"] pkg_session_title_first_prompt_llm["session-title-first-prompt-llm"] pkg_session_title_llm["session-title-llm"] pkg_session_turn_outline["session-turn-outline"] + pkg_tool_working_directory["tool-working-directory"] + pkg_working_directory["working-directory"] end subgraph group_settings["packages/settings"] pkg_settings["settings"] @@ -402,7 +417,6 @@ flowchart TD subgraph group_terminal["packages/terminal"] pkg_terminal["terminal"] pkg_terminal_bash["terminal-bash"] - pkg_tool_terminal["tool-terminal"] end subgraph group_test_support["packages/test-support"] pkg_agent_loop_testkit["agent-loop-testkit"] @@ -419,12 +433,7 @@ flowchart TD pkg_typert_protocol["typert-protocol"] pkg_typert_registry["typert-registry"] end - subgraph group_webhook["packages/webhook"] - pkg_webhook["webhook"] - pkg_webhook_github["webhook-github"] - end subgraph group_workflow["packages/workflow"] - pkg_tool_ralph["tool-ralph"] pkg_tool_workflow["tool-workflow"] pkg_workflow["workflow"] pkg_workflow_ptc["workflow-ptc"] @@ -464,7 +473,6 @@ flowchart TD pkg_subprocess --> pkg_http_proxy pkg_remote_mock --> pkg_typert_protocol pkg_typert_loader --> pkg_typert_registry - pkg_skill_badge --> pkg_skill pkg_skill_office --> pkg_skill pkg_web_fetch_http --> pkg_http_proxy pkg_web_fetch_http --> pkg_timeout @@ -490,6 +498,7 @@ flowchart TD pkg_experimental_webworker_runtime --> pkg_client_connection pkg_experimental_webworker_runtime --> pkg_client_modules pkg_experimental_webworker_runtime --> pkg_host_webserver + pkg_skill_badge --> pkg_skill pkg_host_frontend_static --> pkg_client_connection pkg_host_frontend_static --> pkg_host_webserver pkg_persona --> pkg_system_prompt @@ -520,6 +529,13 @@ flowchart TD pkg_hmr --> pkg_app_boot pkg_hmr --> pkg_cmdline pkg_experimental_speech_to_text --> pkg_settings + pkg_experimental_translator --> pkg_credentials + pkg_experimental_translator --> pkg_launch_environment + pkg_experimental_translator --> pkg_llm + pkg_experimental_translator --> pkg_session + pkg_experimental_translator --> pkg_session_persistence + pkg_experimental_translator --> pkg_timeout + pkg_experimental_translator --> pkg_util_values pkg_host_product_telemetry_otel --> pkg_otel pkg_ptc_runtime --> pkg_sandbox pkg_sandbox_local --> pkg_llm @@ -572,8 +588,6 @@ flowchart TD pkg_skill_filesystem --> pkg_fs pkg_skill_filesystem --> pkg_home_paths pkg_skill_filesystem --> pkg_skill - pkg_hook_protocol --> pkg_session - pkg_hook_protocol --> pkg_shell pkg_api_workspace_controller --> pkg_api_gateway pkg_api_workspace_controller --> pkg_client_connection pkg_api_workspace_controller --> pkg_host_directory_picker @@ -585,11 +599,10 @@ flowchart TD pkg_config_editor --> pkg_hmr pkg_experimental_api_speech_to_text --> pkg_experimental_speech_to_text pkg_experimental_api_speech_to_text --> pkg_typert_protocol - pkg_experimental_ptc_runtime_python --> pkg_ptc_runtime - pkg_experimental_ptc_runtime_python --> pkg_timeout - pkg_experimental_ptc_runtime_python --> pkg_util_values pkg_experimental_speech_to_text_sensevoice --> pkg_experimental_speech_to_text pkg_experimental_speech_to_text_sensevoice --> pkg_subprocess + pkg_hook_protocol --> pkg_session + pkg_hook_protocol --> pkg_shell pkg_lsp_stdio --> pkg_brand pkg_lsp_stdio --> pkg_fs pkg_lsp_stdio --> pkg_llm @@ -668,6 +681,12 @@ flowchart TD pkg_session_title --> pkg_llm pkg_session_title --> pkg_session pkg_session_title --> pkg_session_projection + pkg_working_directory --> pkg_agent + pkg_working_directory --> pkg_fs + pkg_working_directory --> pkg_llm + pkg_working_directory --> pkg_session + pkg_working_directory --> pkg_session_projection + pkg_working_directory --> pkg_system_prompt pkg_terminal --> pkg_agent pkg_terminal --> pkg_brand pkg_loader_smoke --> pkg_agent @@ -691,6 +710,7 @@ flowchart TD pkg_tools --> pkg_session pkg_tools --> pkg_system_prompt pkg_tools --> pkg_user_approval + pkg_tools --> pkg_working_directory pkg_command_goal --> pkg_commands pkg_command_goal --> pkg_goal pkg_command_goal --> pkg_llm @@ -719,6 +739,17 @@ flowchart TD pkg_deepseek_account_platform --> pkg_credentials pkg_deepseek_account_platform --> pkg_deepseek_account pkg_deepseek_account_platform --> pkg_host_webserver + pkg_experimental_ptc_runtime_python --> pkg_ptc_runtime + pkg_experimental_ptc_runtime_python --> pkg_sandbox + pkg_experimental_ptc_runtime_python --> pkg_sandbox_policy + pkg_experimental_ptc_runtime_python --> pkg_timeout + pkg_experimental_ptc_runtime_python --> pkg_util_values + pkg_experimental_worktree --> pkg_agent + pkg_experimental_worktree --> pkg_fs + pkg_experimental_worktree --> pkg_sandbox + pkg_experimental_worktree --> pkg_sandbox_policy + pkg_experimental_worktree --> pkg_subprocess + pkg_experimental_worktree --> pkg_working_directory pkg_command_feedback --> pkg_anonymous_user_id pkg_command_feedback --> pkg_commands pkg_command_feedback --> pkg_session @@ -761,6 +792,7 @@ flowchart TD pkg_terminal_bash --> pkg_session_projection pkg_terminal_bash --> pkg_subprocess pkg_terminal_bash --> pkg_terminal + pkg_terminal_bash --> pkg_working_directory pkg_agent_loop --> pkg_agent pkg_agent_loop --> pkg_llm pkg_agent_loop --> pkg_scope @@ -786,6 +818,7 @@ flowchart TD pkg_tool_fs --> pkg_system_prompt pkg_tool_fs --> pkg_tools pkg_tool_fs --> pkg_user_approval + pkg_tool_fs --> pkg_working_directory pkg_tool_fs_search --> pkg_llm pkg_tool_fs_search --> pkg_output_retention pkg_tool_fs_search --> pkg_session @@ -794,6 +827,7 @@ flowchart TD pkg_tool_fs_search --> pkg_system_prompt pkg_tool_fs_search --> pkg_timeout pkg_tool_fs_search --> pkg_tools + pkg_tool_fs_search --> pkg_working_directory pkg_tool_str_replace_editor --> pkg_fs pkg_tool_str_replace_editor --> pkg_output_retention pkg_tool_str_replace_editor --> pkg_sandbox @@ -803,6 +837,7 @@ flowchart TD pkg_tool_skill --> pkg_llm pkg_tool_skill --> pkg_skill pkg_tool_skill --> pkg_tools + pkg_tool_skill --> pkg_working_directory pkg_tool_workspace_dependencies --> pkg_tools pkg_tool_web --> pkg_llm pkg_tool_web --> pkg_system_prompt @@ -820,12 +855,6 @@ flowchart TD pkg_plan_mode --> pkg_system_prompt pkg_plan_mode --> pkg_tools pkg_plan_mode --> pkg_user_questions - pkg_hooks_codex --> pkg_agent - pkg_hooks_codex --> pkg_hook_protocol - pkg_hooks_codex --> pkg_llm - pkg_hooks_codex --> pkg_session - pkg_hooks_codex --> pkg_session_projection - pkg_hooks_codex --> pkg_tools pkg_command_compact --> pkg_commands pkg_command_compact --> pkg_compaction pkg_compaction_image_offload --> pkg_agent @@ -839,17 +868,21 @@ flowchart TD pkg_agent_instructions --> pkg_session pkg_agent_instructions --> pkg_session_projection pkg_agent_instructions --> pkg_tools + pkg_agent_instructions --> pkg_working_directory pkg_file_reference_local --> pkg_agent pkg_file_reference_local --> pkg_file_reference pkg_file_reference_local --> pkg_system_prompt pkg_file_reference_local --> pkg_tools + pkg_file_reference_local --> pkg_working_directory pkg_tool_present --> pkg_agent pkg_tool_present --> pkg_fs pkg_tool_present --> pkg_llm pkg_tool_present --> pkg_session pkg_tool_present --> pkg_session_projection pkg_tool_present --> pkg_tools + pkg_tool_present --> pkg_working_directory pkg_workspace_changes --> pkg_agent + pkg_workspace_changes --> pkg_fs pkg_workspace_changes --> pkg_session pkg_workspace_changes --> pkg_subprocess pkg_workspace_changes --> pkg_tools @@ -857,10 +890,12 @@ flowchart TD pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_browser_use pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_system_prompt pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_tools + pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_working_directory pkg_experimental_browser_use_playwright_mcp --> pkg_agent pkg_experimental_browser_use_playwright_mcp --> pkg_browser_use pkg_experimental_browser_use_playwright_mcp --> pkg_system_prompt pkg_experimental_browser_use_playwright_mcp --> pkg_tools + pkg_experimental_browser_use_playwright_mcp --> pkg_working_directory pkg_experimental_browser_use_stagehand_native --> pkg_agent pkg_experimental_browser_use_stagehand_native --> pkg_browser_use pkg_experimental_browser_use_stagehand_native --> pkg_system_prompt @@ -868,6 +903,27 @@ flowchart TD pkg_experimental_computer_use_cua_driver_native --> pkg_computer_use pkg_experimental_computer_use_cua_driver_native --> pkg_system_prompt pkg_experimental_computer_use_cua_driver_native --> pkg_tools + pkg_experimental_tool_worktree --> pkg_experimental_worktree + pkg_experimental_tool_worktree --> pkg_tools + pkg_hooks_codex --> pkg_agent + pkg_hooks_codex --> pkg_hook_protocol + pkg_hooks_codex --> pkg_llm + pkg_hooks_codex --> pkg_session + pkg_hooks_codex --> pkg_session_projection + pkg_hooks_codex --> pkg_tools + pkg_hooks_codex --> pkg_working_directory + pkg_session_title_all_prompts_llm --> pkg_llm + pkg_session_title_all_prompts_llm --> pkg_session + pkg_session_title_all_prompts_llm --> pkg_session_title + pkg_session_title_all_prompts_llm --> pkg_session_title_llm + pkg_tool_terminal --> pkg_agent + pkg_tool_terminal --> pkg_jobs + pkg_tool_terminal --> pkg_llm + pkg_tool_terminal --> pkg_output_retention + pkg_tool_terminal --> pkg_system_prompt + pkg_tool_terminal --> pkg_terminal + pkg_tool_terminal --> pkg_tools + pkg_tool_terminal --> pkg_working_directory pkg_cordis_host_runner --> pkg_agent pkg_cordis_host_runner --> pkg_brand pkg_cordis_host_runner --> pkg_llm @@ -900,6 +956,7 @@ flowchart TD pkg_tool_lsp --> pkg_system_prompt pkg_tool_lsp --> pkg_timeout pkg_tool_lsp --> pkg_tools + pkg_tool_lsp --> pkg_working_directory pkg_mcp_resources --> pkg_llm pkg_mcp_resources --> pkg_scope pkg_mcp_resources --> pkg_system_prompt @@ -917,14 +974,12 @@ flowchart TD pkg_session_checkpoint_policy --> pkg_session pkg_session_checkpoint_policy --> pkg_session_persistence pkg_session_checkpoint_policy --> pkg_tools - pkg_session_title_all_prompts_llm --> pkg_llm - pkg_session_title_all_prompts_llm --> pkg_session - pkg_session_title_all_prompts_llm --> pkg_session_title - pkg_session_title_all_prompts_llm --> pkg_session_title_llm pkg_session_title_first_prompt_llm --> pkg_llm pkg_session_title_first_prompt_llm --> pkg_session pkg_session_title_first_prompt_llm --> pkg_session_title pkg_session_title_first_prompt_llm --> pkg_session_title_llm + pkg_tool_working_directory --> pkg_tools + pkg_tool_working_directory --> pkg_working_directory pkg_shell_env --> pkg_app_boot pkg_shell_env --> pkg_home_paths pkg_shell_env --> pkg_shell @@ -934,11 +989,13 @@ flowchart TD pkg_tool_bash_persistent --> pkg_terminal pkg_tool_bash_persistent --> pkg_timeout pkg_tool_bash_persistent --> pkg_tools + pkg_tool_bash_persistent --> pkg_working_directory pkg_tool_pwsh_persistent --> pkg_agent pkg_tool_pwsh_persistent --> pkg_output_retention pkg_tool_pwsh_persistent --> pkg_terminal pkg_tool_pwsh_persistent --> pkg_timeout pkg_tool_pwsh_persistent --> pkg_tools + pkg_tool_pwsh_persistent --> pkg_working_directory pkg_ssh --> pkg_brand pkg_ssh --> pkg_fs pkg_ssh --> pkg_fs_local @@ -949,13 +1006,6 @@ flowchart TD pkg_ssh --> pkg_session_projection pkg_ssh --> pkg_subprocess pkg_ssh --> pkg_subprocess_local - pkg_tool_terminal --> pkg_agent - pkg_tool_terminal --> pkg_jobs - pkg_tool_terminal --> pkg_llm - pkg_tool_terminal --> pkg_output_retention - pkg_tool_terminal --> pkg_system_prompt - pkg_tool_terminal --> pkg_terminal - pkg_tool_terminal --> pkg_tools pkg_llm_replay --> pkg_compaction pkg_llm_replay --> pkg_deepseek_llm_api_extensions pkg_llm_replay --> pkg_llm @@ -999,6 +1049,15 @@ flowchart TD pkg_experimental_auto_review --> pkg_permission_presets pkg_experimental_auto_review --> pkg_session pkg_experimental_auto_review --> pkg_tools + pkg_experimental_auto_review --> pkg_working_directory + pkg_webhook --> pkg_agent + pkg_webhook --> pkg_agent_default_model + pkg_webhook --> pkg_agent_preset_registry + pkg_webhook --> pkg_llm + pkg_webhook --> pkg_permission_presets + pkg_webhook --> pkg_session + pkg_webhook --> pkg_session_title + pkg_webhook --> pkg_workspace pkg_tool_cordis --> pkg_agent pkg_tool_cordis --> pkg_app_boot pkg_tool_cordis --> pkg_cordis_host_runner @@ -1032,6 +1091,7 @@ flowchart TD pkg_tool_bash --> pkg_system_prompt pkg_tool_bash --> pkg_tools pkg_tool_bash --> pkg_user_approval + pkg_tool_bash --> pkg_working_directory pkg_tool_pwsh --> pkg_agent pkg_tool_pwsh --> pkg_jobs pkg_tool_pwsh --> pkg_llm @@ -1042,6 +1102,7 @@ flowchart TD pkg_tool_pwsh --> pkg_system_prompt pkg_tool_pwsh --> pkg_tools pkg_tool_pwsh --> pkg_user_approval + pkg_tool_pwsh --> pkg_working_directory pkg_fs_ssh --> pkg_fs pkg_fs_ssh --> pkg_sandbox pkg_fs_ssh --> pkg_sandbox_policy @@ -1053,23 +1114,16 @@ flowchart TD pkg_subprocess_ssh --> pkg_subprocess_local pkg_agent_loop_testkit --> pkg_agent pkg_agent_loop_testkit --> pkg_agent_loop + pkg_agent_loop_testkit --> pkg_fs_local pkg_agent_loop_testkit --> pkg_llm pkg_agent_loop_testkit --> pkg_session pkg_agent_loop_testkit --> pkg_session_projection pkg_agent_loop_testkit --> pkg_system_prompt pkg_agent_loop_testkit --> pkg_tools - pkg_webhook --> pkg_agent - pkg_webhook --> pkg_agent_default_model - pkg_webhook --> pkg_agent_preset_registry - pkg_webhook --> pkg_llm - pkg_webhook --> pkg_permission_presets - pkg_webhook --> pkg_session - pkg_webhook --> pkg_session_title - pkg_webhook --> pkg_workspace + pkg_agent_loop_testkit --> pkg_working_directory pkg_subagent --> pkg_agent pkg_subagent --> pkg_agent_preset_registry pkg_subagent --> pkg_attachment - pkg_subagent --> pkg_jobs pkg_subagent --> pkg_llm pkg_subagent --> pkg_permission_presets pkg_subagent --> pkg_sandbox @@ -1084,6 +1138,7 @@ flowchart TD pkg_subagent --> pkg_typert_protocol pkg_subagent --> pkg_user_approval pkg_subagent --> pkg_util_time + pkg_subagent --> pkg_working_directory pkg_subagent --> pkg_workspace pkg_spill_policy --> pkg_attachment pkg_spill_policy --> pkg_fs @@ -1096,14 +1151,6 @@ flowchart TD pkg_session_query_sqlite --> pkg_session pkg_session_query_sqlite --> pkg_session_persistence pkg_session_query_sqlite --> pkg_session_query - pkg_tool_session_query --> pkg_agent - pkg_tool_session_query --> pkg_llm - pkg_tool_session_query --> pkg_session - pkg_tool_session_query --> pkg_session_projection - pkg_tool_session_query --> pkg_session_query - pkg_tool_session_query --> pkg_system_prompt - pkg_tool_session_query --> pkg_timeout - pkg_tool_session_query --> pkg_tools pkg_acp --> pkg_agent pkg_acp --> pkg_attachment pkg_acp --> pkg_llm @@ -1112,6 +1159,7 @@ flowchart TD pkg_acp --> pkg_session_persistence pkg_acp --> pkg_token_meter pkg_acp --> pkg_user_approval + pkg_plugin_manager --> pkg_agent_preset_registry pkg_plugin_manager --> pkg_brand pkg_plugin_manager --> pkg_hmr pkg_plugin_manager --> pkg_host_plugin_inventory @@ -1121,12 +1169,6 @@ flowchart TD pkg_plugin_manager --> pkg_tools pkg_plugin_manager --> pkg_typert_protocol pkg_plugin_manager --> pkg_user_approval - pkg_headless --> pkg_agent - pkg_headless --> pkg_agent_default_model - pkg_headless --> pkg_fs - pkg_headless --> pkg_llm - pkg_headless --> pkg_session - pkg_headless --> pkg_session_query pkg_compaction_tool_result_pruner --> pkg_compaction pkg_compaction_tool_result_pruner --> pkg_llm pkg_compaction_tool_result_pruner --> pkg_session @@ -1137,6 +1179,29 @@ flowchart TD pkg_experimental_browser_use_runtime --> pkg_scope pkg_experimental_browser_use_runtime --> pkg_system_prompt pkg_experimental_browser_use_runtime --> pkg_tools + pkg_experimental_browser_use_runtime --> pkg_working_directory + pkg_experimental_claude_code_mods --> pkg_agent + pkg_experimental_claude_code_mods --> pkg_commands + pkg_experimental_claude_code_mods --> pkg_fs + pkg_experimental_claude_code_mods --> pkg_llm + pkg_experimental_claude_code_mods --> pkg_scope + pkg_experimental_claude_code_mods --> pkg_session + pkg_experimental_claude_code_mods --> pkg_session_projection + pkg_experimental_claude_code_mods --> pkg_storage_domain + pkg_experimental_claude_code_mods --> pkg_subprocess + pkg_experimental_claude_code_mods --> pkg_token_meter + pkg_experimental_claude_code_mods --> pkg_tools + pkg_experimental_claude_code_mods --> pkg_typert_protocol + pkg_experimental_claude_code_mods --> pkg_user_questions + pkg_experimental_claude_code_mods --> pkg_working_directory + pkg_tool_session_query --> pkg_agent + pkg_tool_session_query --> pkg_llm + pkg_tool_session_query --> pkg_session + pkg_tool_session_query --> pkg_session_projection + pkg_tool_session_query --> pkg_session_query + pkg_tool_session_query --> pkg_system_prompt + pkg_tool_session_query --> pkg_timeout + pkg_tool_session_query --> pkg_tools pkg_webhook_github --> pkg_credentials pkg_webhook_github --> pkg_host_webserver pkg_webhook_github --> pkg_session @@ -1157,14 +1222,11 @@ flowchart TD pkg_subagent_codex --> pkg_subagent pkg_subagent_codex --> pkg_subprocess pkg_subagent_codex --> pkg_timeout - pkg_subagent_in_process_driver --> pkg_agent - pkg_subagent_in_process_driver --> pkg_llm - pkg_subagent_in_process_driver --> pkg_session - pkg_subagent_in_process_driver --> pkg_subagent - pkg_subagent_in_process_driver --> pkg_system_prompt - pkg_subagent_in_process_driver --> pkg_tools + pkg_subagent_fork_in_process --> pkg_agent + pkg_subagent_fork_in_process --> pkg_session + pkg_subagent_fork_in_process --> pkg_subagent + pkg_subagent_spawn_in_process --> pkg_subagent pkg_tool_subagent --> pkg_agent - pkg_tool_subagent --> pkg_jobs pkg_tool_subagent --> pkg_llm pkg_tool_subagent --> pkg_scope pkg_tool_subagent --> pkg_session @@ -1176,13 +1238,6 @@ flowchart TD pkg_tool_subagent_control --> pkg_session pkg_tool_subagent_control --> pkg_subagent pkg_tool_subagent_control --> pkg_tools - pkg_hooks_claude_code --> pkg_agent - pkg_hooks_claude_code --> pkg_hook_protocol - pkg_hooks_claude_code --> pkg_llm - pkg_hooks_claude_code --> pkg_session - pkg_hooks_claude_code --> pkg_session_projection - pkg_hooks_claude_code --> pkg_subagent - pkg_hooks_claude_code --> pkg_tools pkg_api_session_controller --> pkg_agent pkg_api_session_controller --> pkg_agent_default_model pkg_api_session_controller --> pkg_agent_preset_registry @@ -1211,7 +1266,15 @@ flowchart TD pkg_api_session_controller --> pkg_util_time pkg_api_session_controller --> pkg_util_values pkg_api_session_controller --> pkg_util_workspace_path + pkg_api_session_controller --> pkg_working_directory pkg_api_session_controller --> pkg_workspace + pkg_headless --> pkg_agent + pkg_headless --> pkg_agent_default_model + pkg_headless --> pkg_fs + pkg_headless --> pkg_llm + pkg_headless --> pkg_session + pkg_headless --> pkg_session_query + pkg_headless --> pkg_subagent pkg_compaction_basic --> pkg_agent pkg_compaction_basic --> pkg_commands pkg_compaction_basic --> pkg_compaction @@ -1238,15 +1301,23 @@ flowchart TD pkg_experimental_agent_team --> pkg_session_persistence pkg_experimental_agent_team --> pkg_session_projection pkg_experimental_agent_team --> pkg_subagent - pkg_sdk_protocol --> pkg_llm - pkg_sdk_protocol --> pkg_session - pkg_sdk_protocol --> pkg_subagent + pkg_hooks_claude_code --> pkg_agent + pkg_hooks_claude_code --> pkg_hook_protocol + pkg_hooks_claude_code --> pkg_llm + pkg_hooks_claude_code --> pkg_session + pkg_hooks_claude_code --> pkg_session_projection + pkg_hooks_claude_code --> pkg_subagent + pkg_hooks_claude_code --> pkg_tools + pkg_hooks_claude_code --> pkg_working_directory pkg_tool_ralph --> pkg_agent pkg_tool_ralph --> pkg_llm pkg_tool_ralph --> pkg_subagent pkg_tool_ralph --> pkg_system_prompt pkg_tool_ralph --> pkg_tools pkg_tool_ralph --> pkg_workflow + pkg_sdk_protocol --> pkg_llm + pkg_sdk_protocol --> pkg_session + pkg_sdk_protocol --> pkg_subagent pkg_workflow_ptc --> pkg_agent pkg_workflow_ptc --> pkg_llm pkg_workflow_ptc --> pkg_ptc_runtime @@ -1256,12 +1327,7 @@ flowchart TD pkg_workflow_ptc --> pkg_subagent pkg_workflow_ptc --> pkg_tools pkg_workflow_ptc --> pkg_workflow - pkg_subagent_fork_in_process --> pkg_agent - pkg_subagent_fork_in_process --> pkg_session - pkg_subagent_fork_in_process --> pkg_subagent - pkg_subagent_fork_in_process --> pkg_subagent_in_process_driver - pkg_subagent_spawn_in_process --> pkg_subagent - pkg_subagent_spawn_in_process --> pkg_subagent_in_process_driver + pkg_workflow_ptc --> pkg_working_directory pkg_experimental_client_ui_agent_team --> pkg_api_session_controller pkg_experimental_client_ui_agent_team --> pkg_client_locale pkg_experimental_client_ui_agent_team --> pkg_client_ui_conversation @@ -1272,6 +1338,22 @@ flowchart TD pkg_experimental_client_ui_agent_team --> pkg_client_ui_workspace pkg_experimental_client_ui_agent_team --> pkg_experimental_agent_team pkg_experimental_client_ui_agent_team --> pkg_session + pkg_experimental_client_ui_claude_code_mods --> pkg_api_remotes + pkg_experimental_client_ui_claude_code_mods --> pkg_api_session_controller + pkg_experimental_client_ui_claude_code_mods --> pkg_client_locale + pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_conversation + pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_primitives + pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_renderer + pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_session + pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_slots + pkg_experimental_client_ui_claude_code_mods --> pkg_experimental_claude_code_mods + pkg_experimental_client_ui_claude_code_mods --> pkg_session + pkg_experimental_client_ui_claude_code_mods --> pkg_typert_protocol + pkg_experimental_client_ui_cot_translation --> pkg_api_session_controller + pkg_experimental_client_ui_cot_translation --> pkg_experimental_translator + pkg_experimental_client_ui_cot_translation --> pkg_session + pkg_experimental_client_ui_cot_translation --> pkg_settings + pkg_experimental_client_ui_cot_translation --> pkg_typert_protocol pkg_experimental_client_ui_voice_input --> pkg_api_gateway pkg_experimental_client_ui_voice_input --> pkg_api_remotes pkg_experimental_client_ui_voice_input --> pkg_api_session_controller @@ -1315,6 +1397,7 @@ flowchart TD pkg_sdk_jsonrpc_server --> pkg_sdk_protocol pkg_sdk_jsonrpc_server --> pkg_session pkg_sdk_jsonrpc_server --> pkg_subagent + pkg_sdk_jsonrpc_server --> pkg_working_directory pkg_client_test_runtime --> pkg_api_gateway pkg_client_test_runtime --> pkg_api_session_controller pkg_client_test_runtime --> pkg_api_workspace_controller @@ -1441,8 +1524,14 @@ flowchart TD | [`credentials`](../packages/credentials/credentials) | `credentials` | — | | [`office-to-pdf`](../packages/document/office-to-pdf) | `document` | — | | [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — | +| [`experimental-badge-skill-bundle`](../packages/experimental/badge-skill-bundle) | `experimental` | — | +| [`experimental-cot-translation-bundle`](../packages/experimental/cot-translation-bundle) | `experimental` | — | | [`experimental-inspector-profile`](../packages/experimental/inspector-profile) | `experimental` | — | +| [`experimental-ralph-bundle`](../packages/experimental/ralph-bundle) | `experimental` | — | | [`experimental-session-inspector`](../packages/experimental/session-inspector) | `experimental` | — | +| [`experimental-session-search`](../packages/experimental/session-search) | `experimental` | — | +| [`experimental-session-titles-bundle`](../packages/experimental/session-titles-bundle) | `experimental` | — | +| [`experimental-terminal-bundle`](../packages/experimental/terminal-bundle) | `experimental` | — | | [`experimental-voice-input-bundle`](../packages/experimental/voice-input-bundle) | `experimental` | — | | [`experimental-webworker-packer`](../packages/experimental/webworker-packer) | `experimental` | — | | [`client-ui-cordis`](../packages/extensions/ui-cordis) | `extensions` | — | @@ -1483,7 +1572,6 @@ flowchart TD | [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`http-proxy`](../packages/util/http-proxy) | | [`remote-mock`](../packages/test-support/remote-mock) | `test-support` | [`typert-protocol`](../packages/typert/protocol) | | [`typert-loader`](../packages/typert/loader) | `typert` | [`typert-registry`](../packages/typert/registry) | -| [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) | | [`skill-office`](../packages/skill/skill-office) | `skill` | [`skill`](../packages/skill/skill) | | [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`http-proxy`](../packages/util/http-proxy), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) | | [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) | @@ -1496,6 +1584,7 @@ flowchart TD | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) | | [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) | | [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) | +| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) | | [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver) | | [`persona`](../packages/preset/persona) | `preset` | [`system-prompt`](../packages/core/system-prompt) | | [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | @@ -1513,6 +1602,7 @@ flowchart TD | [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | | [`hmr`](../packages/boot/hmr) | `boot` | [`app-boot`](../packages/boot/app-boot), [`cmdline`](../packages/boot/cmdline) | | [`experimental-speech-to-text`](../packages/experimental/speech-to-text) | `experimental` | [`settings`](../packages/settings/settings) | +| [`experimental-translator`](../packages/experimental/translator) | `experimental` | [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) | | [`host-product-telemetry-otel`](../packages/host/product-telemetry-otel) | `host` | [`otel`](../packages/telemetry/otel) | | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | `ptc-runtime` | [`sandbox`](../packages/sandbox/sandbox) | | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) | @@ -1528,12 +1618,11 @@ flowchart TD | [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) | | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | `fs` | [`fs`](../packages/fs/fs) | | [`skill-filesystem`](../packages/skill/skill-filesystem) | `skill` | [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`skill`](../packages/skill/skill) | -| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`session`](../packages/core/session), [`shell`](../packages/shell/shell) | | [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`host-directory-picker`](../packages/host/directory-picker), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) | | [`config-editor`](../packages/boot/config-editor) | `boot` | [`app-boot`](../packages/boot/app-boot), [`hmr`](../packages/boot/hmr) | | [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | `experimental` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`typert-protocol`](../packages/typert/protocol) | -| [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | `experimental` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) | | [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | `experimental` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`subprocess`](../packages/subprocess/subprocess) | +| [`hook-protocol`](../packages/experimental/hook-protocol) | `experimental` | [`session`](../packages/core/session), [`shell`](../packages/shell/shell) | | [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`bash-local`](../packages/shell/bash-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | @@ -1552,11 +1641,12 @@ flowchart TD | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) | | [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`session`](../packages/core/session) | | [`session-title`](../packages/session/session-title) | `session` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | +| [`working-directory`](../packages/session/working-directory) | `session` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) | | [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) | | [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) | -| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) | +| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) | | [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) | | [`goal-round-driver`](../packages/goal/goal-round-driver) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) | @@ -1564,6 +1654,8 @@ flowchart TD | [`api-account-controller`](../packages/api/account-controller) | `api` | [`agent`](../packages/core/agent), [`deepseek-account`](../packages/credentials/deepseek-account), [`typert-protocol`](../packages/typert/protocol) | | [`compaction`](../packages/compaction/compaction) | `compaction` | [`brand`](../packages/util/brand), [`commands`](../packages/interaction/commands), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) | `credentials` | [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`host-webserver`](../packages/host/webserver) | +| [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | `experimental` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) | +| [`experimental-worktree`](../packages/experimental/worktree) | `experimental` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`subprocess`](../packages/subprocess/subprocess), [`working-directory`](../packages/session/working-directory) | | [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`commands`](../packages/interaction/commands), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | | [`permission-presets`](../packages/interaction/permission-presets) | `interaction` | [`commands`](../packages/interaction/commands), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) | | [`jobs-local`](../packages/jobs/jobs-local) | `jobs` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`scope`](../packages/core/scope), [`timeout`](../packages/util/timeout) | @@ -1571,46 +1663,48 @@ flowchart TD | [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) | | [`bash-sandbox`](../packages/shell/bash-sandbox) | `shell` | [`bash-local`](../packages/shell/bash-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) | | [`pwsh-sandbox`](../packages/shell/pwsh-sandbox) | `shell` | [`pwsh-local`](../packages/shell/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) | -| [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal) | +| [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal), [`working-directory`](../packages/session/working-directory) | | [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`agent-tool-presentation`](../packages/core/agent-tool-presentation) | `core` | [`tools`](../packages/core/tools) | | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | -| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) | +| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`output-retention`](../packages/util/output-retention), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) | -| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | +| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`tool-workspace-dependencies`](../packages/skill/tool-workspace-dependencies) | `skill` | [`tools`](../packages/core/tools) | | [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) | | [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | | [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) | -| [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | | [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction) | | [`compaction-image-offload`](../packages/compaction/compaction-image-offload) | `compaction` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | -| [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-present`](../packages/deliverables/tool-present) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | -| [`workspace-changes`](../packages/deliverables/workspace-changes) | `deliverables` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) | -| [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`tool-present`](../packages/deliverables/tool-present) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`workspace-changes`](../packages/deliverables/workspace-changes) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) | +| [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | `experimental` | [`computer-use`](../packages/computer-use/computer-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`experimental-tool-worktree`](../packages/experimental/tool-worktree) | `experimental` | [`experimental-worktree`](../packages/experimental/worktree), [`tools`](../packages/core/tools) | +| [`hooks-codex`](../packages/experimental/hooks-codex) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`session-title-all-prompts-llm`](../packages/experimental/session-title-all-prompts-llm) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | +| [`tool-terminal`](../packages/experimental/tool-terminal) | `experimental` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`terminal`](../packages/terminal/terminal), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | `extensions` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) | | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-protocol`](../packages/typert/protocol) | | [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder) | `guard` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools) | | [`tool-call-timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`tool-ask-user`](../packages/interaction/tool-ask-user) | `interaction` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) | | [`tool-jobs`](../packages/jobs/tool-jobs) | `jobs` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`mcp-resources`](../packages/mcp/mcp-resources) | `mcp` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`agent-preset-registry`](../packages/preset/agent-preset-registry) | `preset` | [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) | | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) | -| [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | | [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | +| [`tool-working-directory`](../packages/session/tool-working-directory) | `session` | [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`shell-env`](../packages/shell/shell-env) | `shell` | [`app-boot`](../packages/boot/app-boot), [`home-paths`](../packages/util/home-paths), [`shell`](../packages/shell/shell), [`tools`](../packages/core/tools) | -| [`tool-bash-persistent`](../packages/shell/tool-bash-persistent) | `shell` | [`agent`](../packages/core/agent), [`output-retention`](../packages/util/output-retention), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | -| [`tool-pwsh-persistent`](../packages/shell/tool-pwsh-persistent) | `shell` | [`agent`](../packages/core/agent), [`output-retention`](../packages/util/output-retention), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`tool-bash-persistent`](../packages/shell/tool-bash-persistent) | `shell` | [`agent`](../packages/core/agent), [`output-retention`](../packages/util/output-retention), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`tool-pwsh-persistent`](../packages/shell/tool-pwsh-persistent) | `shell` | [`agent`](../packages/core/agent), [`output-retention`](../packages/util/output-retention), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`ssh`](../packages/ssh/ssh) | `ssh` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-local`](../packages/sandbox/sandbox-local), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) | -| [`tool-terminal`](../packages/terminal/tool-terminal) | `terminal` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`terminal`](../packages/terminal/terminal), [`tools`](../packages/core/tools) | | [`llm-replay`](../packages/test-support/llm-replay) | `test-support` | [`compaction`](../packages/compaction/compaction), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`session`](../packages/core/session) | @@ -1618,51 +1712,53 @@ flowchart TD | [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) | | [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) | | [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) | -| [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | +| [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`webhook`](../packages/experimental/webhook) | `experimental` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) | | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`app-boot`](../packages/boot/app-boot), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`tools`](../packages/core/tools) | | [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`brand`](../packages/util/brand), [`typert-protocol`](../packages/typert/protocol) | | [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-resources`](../packages/mcp/mcp-resources), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`agent-preset`](../packages/preset/agent-preset) | `preset` | [`agent-preset-registry`](../packages/preset/agent-preset-registry) | | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`otel`](../packages/telemetry/otel), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) | -| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | -| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | +| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) | +| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) | | [`fs-ssh`](../packages/ssh/fs-ssh) | `ssh` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`ssh`](../packages/ssh/ssh) | | [`sandbox-ssh`](../packages/ssh/sandbox-ssh) | `ssh` | [`sandbox`](../packages/sandbox/sandbox), [`ssh`](../packages/ssh/ssh) | | [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | `ssh` | [`ssh`](../packages/ssh/ssh), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) | -| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) | -| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) | +| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`fs-local`](../packages/fs/fs-local), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time), [`working-directory`](../packages/session/working-directory), [`workspace`](../packages/workspace/workspace) | | [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) | | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) | -| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) | -| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) | -| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) | +| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) | | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | -| [`experimental-browser-use-runtime`](../packages/experimental/browser-use-runtime) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`mcp-client`](../packages/mcp/mcp-client), [`scope`](../packages/core/scope), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`webhook-github`](../packages/webhook/webhook-github) | `webhook` | [`credentials`](../packages/credentials/credentials), [`host-webserver`](../packages/host/webserver), [`session`](../packages/core/session), [`webhook`](../packages/webhook/webhook) | +| [`experimental-browser-use-runtime`](../packages/experimental/browser-use-runtime) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`mcp-client`](../packages/mcp/mcp-client), [`scope`](../packages/core/scope), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods) | `experimental` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subprocess`](../packages/subprocess/subprocess), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions), [`working-directory`](../packages/session/working-directory) | +| [`tool-session-query`](../packages/experimental/tool-session-query) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`webhook-github`](../packages/experimental/webhook-github) | `experimental` | [`credentials`](../packages/credentials/credentials), [`host-webserver`](../packages/host/webserver), [`session`](../packages/core/session), [`webhook`](../packages/experimental/webhook) | | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | -| [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | +| [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`subagent`](../packages/subagent/subagent) | +| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | -| [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | -| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) | +| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`working-directory`](../packages/session/working-directory), [`workspace`](../packages/workspace/workspace) | +| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`subagent`](../packages/subagent/subagent) | | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) | | [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent) | +| [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | -| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | -| [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | -| [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | -| [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | +| [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow), [`working-directory`](../packages/session/working-directory) | | [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session) | +| [`experimental-client-ui-claude-code-mods`](../packages/experimental/client-ui-claude-code-mods) | `experimental` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | +| [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | `experimental` | [`api-session-controller`](../packages/api/session-controller), [`experimental-translator`](../packages/experimental/translator), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) | | [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | [`api-gateway`](../packages/api/gateway), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-store`](../packages/client/store), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text), [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) | | [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) | -| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | +| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`working-directory`](../packages/session/working-directory) | | [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) | | [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | | [`tool-schedule`](../packages/schedule/tool-schedule) | `schedule` | [`llm`](../packages/llm/llm), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 48da82511d..9b3a1e2036 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -66,7 +66,6 @@ flowchart TD end subgraph group_skill["packages/skill"] pkg_skill["skill"] - pkg_skill_badge["skill-badge"] pkg_skill_filesystem["skill-filesystem"] pkg_skill_office["skill-office"] pkg_tool_skill["tool-skill"] @@ -79,7 +78,6 @@ flowchart TD pkg_subagent_codex["subagent-codex"] pkg_subagent_dsh_sdk["subagent-dsh-sdk"] pkg_subagent_fork_in_process["subagent-fork-in-process"] - pkg_subagent_in_process_driver["subagent-in-process-driver"] pkg_subagent_spawn_in_process["subagent-spawn-in-process"] pkg_tool_subagent["tool-subagent"] pkg_tool_subagent_control["tool-subagent-control"] @@ -103,16 +101,10 @@ flowchart TD subgraph group_plan["packages/plan"] pkg_plan_mode["plan-mode"] end - subgraph group_hooks["packages/hooks"] - pkg_hook_protocol["hook-protocol"] - pkg_hooks_claude_code["hooks-claude-code"] - pkg_hooks_codex["hooks-codex"] - end subgraph group_session_query["packages/session-query"] pkg_session_log_export["session-log-export"] pkg_session_query["session-query"] pkg_session_query_sqlite["session-query-sqlite"] - pkg_tool_session_query["tool-session-query"] end subgraph group_acp["packages/acp"] pkg_acp["acp"] @@ -252,24 +244,46 @@ flowchart TD pkg_experimental_agent_team_profile["experimental-agent-team-profile"] pkg_experimental_api_speech_to_text["experimental-api-speech-to-text"] pkg_experimental_auto_review["experimental-auto-review"] + pkg_experimental_badge_skill_bundle["experimental-badge-skill-bundle"] pkg_experimental_browser_use_chrome_devtools_mcp["experimental-browser-use-chrome-devtools-mcp"] pkg_experimental_browser_use_playwright_mcp["experimental-browser-use-playwright-mcp"] pkg_experimental_browser_use_runtime["experimental-browser-use-runtime"] pkg_experimental_browser_use_stagehand_native["experimental-browser-use-stagehand-native"] + pkg_experimental_claude_code_mods["experimental-claude-code-mods"] pkg_experimental_client_ui_agent_team["experimental-client-ui-agent-team"] + pkg_experimental_client_ui_claude_code_mods["experimental-client-ui-claude-code-mods"] + pkg_experimental_client_ui_cot_translation["experimental-client-ui-cot-translation"] pkg_experimental_client_ui_voice_input["experimental-client-ui-voice-input"] pkg_experimental_computer_use_cua_driver_mcp["experimental-computer-use-cua-driver-mcp"] pkg_experimental_computer_use_cua_driver_native["experimental-computer-use-cua-driver-native"] + pkg_experimental_cot_translation_bundle["experimental-cot-translation-bundle"] pkg_experimental_inspector["experimental-inspector"] pkg_experimental_inspector_profile["experimental-inspector-profile"] pkg_experimental_ptc_runtime_python["experimental-ptc-runtime-python"] + pkg_experimental_ralph_bundle["experimental-ralph-bundle"] pkg_experimental_session_inspector["experimental-session-inspector"] + pkg_experimental_session_search["experimental-session-search"] + pkg_experimental_session_titles_bundle["experimental-session-titles-bundle"] pkg_experimental_speech_to_text["experimental-speech-to-text"] pkg_experimental_speech_to_text_sensevoice["experimental-speech-to-text-sensevoice"] + pkg_experimental_terminal_bundle["experimental-terminal-bundle"] pkg_experimental_tool_agent_team["experimental-tool-agent-team"] + pkg_experimental_tool_worktree["experimental-tool-worktree"] + pkg_experimental_translator["experimental-translator"] pkg_experimental_voice_input_bundle["experimental-voice-input-bundle"] pkg_experimental_webworker_packer["experimental-webworker-packer"] pkg_experimental_webworker_runtime["experimental-webworker-runtime"] + pkg_experimental_worktree["experimental-worktree"] + pkg_hook_protocol["hook-protocol"] + pkg_hooks_claude_code["hooks-claude-code"] + pkg_hooks_codex["hooks-codex"] + pkg_session_title_all_prompts_llm["session-title-all-prompts-llm"] + pkg_skill_badge["skill-badge"] + pkg_tool_ralph["tool-ralph"] + pkg_tool_session_query["tool-session-query"] + pkg_tool_terminal["tool-terminal"] + pkg_webhook["webhook"] + pkg_webhook_github["webhook-github"] end subgraph group_extensions["packages/extensions"] pkg_client_ui_cordis["client-ui-cordis"] @@ -361,10 +375,11 @@ flowchart TD pkg_session_telemetry["session-telemetry"] pkg_session_telemetry_otel["session-telemetry-otel"] pkg_session_title["session-title"] - pkg_session_title_all_prompts_llm["session-title-all-prompts-llm"] pkg_session_title_first_prompt_llm["session-title-first-prompt-llm"] pkg_session_title_llm["session-title-llm"] pkg_session_turn_outline["session-turn-outline"] + pkg_tool_working_directory["tool-working-directory"] + pkg_working_directory["working-directory"] end subgraph group_settings["packages/settings"] pkg_settings["settings"] @@ -404,7 +419,6 @@ flowchart TD subgraph group_terminal["packages/terminal"] pkg_terminal["terminal"] pkg_terminal_bash["terminal-bash"] - pkg_tool_terminal["tool-terminal"] end subgraph group_test_support["packages/test-support"] pkg_agent_loop_testkit["agent-loop-testkit"] @@ -421,12 +435,7 @@ flowchart TD pkg_typert_protocol["typert-protocol"] pkg_typert_registry["typert-registry"] end - subgraph group_webhook["packages/webhook"] - pkg_webhook["webhook"] - pkg_webhook_github["webhook-github"] - end subgraph group_workflow["packages/workflow"] - pkg_tool_ralph["tool-ralph"] pkg_tool_workflow["tool-workflow"] pkg_workflow["workflow"] pkg_workflow_ptc["workflow-ptc"] @@ -466,7 +475,6 @@ flowchart TD pkg_subprocess --> pkg_http_proxy pkg_remote_mock --> pkg_typert_protocol pkg_typert_loader --> pkg_typert_registry - pkg_skill_badge --> pkg_skill pkg_skill_office --> pkg_skill pkg_web_fetch_http --> pkg_http_proxy pkg_web_fetch_http --> pkg_timeout @@ -492,6 +500,7 @@ flowchart TD pkg_experimental_webworker_runtime --> pkg_client_connection pkg_experimental_webworker_runtime --> pkg_client_modules pkg_experimental_webworker_runtime --> pkg_host_webserver + pkg_skill_badge --> pkg_skill pkg_host_frontend_static --> pkg_client_connection pkg_host_frontend_static --> pkg_host_webserver pkg_persona --> pkg_system_prompt @@ -522,6 +531,13 @@ flowchart TD pkg_hmr --> pkg_app_boot pkg_hmr --> pkg_cmdline pkg_experimental_speech_to_text --> pkg_settings + pkg_experimental_translator --> pkg_credentials + pkg_experimental_translator --> pkg_launch_environment + pkg_experimental_translator --> pkg_llm + pkg_experimental_translator --> pkg_session + pkg_experimental_translator --> pkg_session_persistence + pkg_experimental_translator --> pkg_timeout + pkg_experimental_translator --> pkg_util_values pkg_host_product_telemetry_otel --> pkg_otel pkg_ptc_runtime --> pkg_sandbox pkg_sandbox_local --> pkg_llm @@ -574,8 +590,6 @@ flowchart TD pkg_skill_filesystem --> pkg_fs pkg_skill_filesystem --> pkg_home_paths pkg_skill_filesystem --> pkg_skill - pkg_hook_protocol --> pkg_session - pkg_hook_protocol --> pkg_shell pkg_api_workspace_controller --> pkg_api_gateway pkg_api_workspace_controller --> pkg_client_connection pkg_api_workspace_controller --> pkg_host_directory_picker @@ -587,11 +601,10 @@ flowchart TD pkg_config_editor --> pkg_hmr pkg_experimental_api_speech_to_text --> pkg_experimental_speech_to_text pkg_experimental_api_speech_to_text --> pkg_typert_protocol - pkg_experimental_ptc_runtime_python --> pkg_ptc_runtime - pkg_experimental_ptc_runtime_python --> pkg_timeout - pkg_experimental_ptc_runtime_python --> pkg_util_values pkg_experimental_speech_to_text_sensevoice --> pkg_experimental_speech_to_text pkg_experimental_speech_to_text_sensevoice --> pkg_subprocess + pkg_hook_protocol --> pkg_session + pkg_hook_protocol --> pkg_shell pkg_lsp_stdio --> pkg_brand pkg_lsp_stdio --> pkg_fs pkg_lsp_stdio --> pkg_llm @@ -670,6 +683,12 @@ flowchart TD pkg_session_title --> pkg_llm pkg_session_title --> pkg_session pkg_session_title --> pkg_session_projection + pkg_working_directory --> pkg_agent + pkg_working_directory --> pkg_fs + pkg_working_directory --> pkg_llm + pkg_working_directory --> pkg_session + pkg_working_directory --> pkg_session_projection + pkg_working_directory --> pkg_system_prompt pkg_terminal --> pkg_agent pkg_terminal --> pkg_brand pkg_loader_smoke --> pkg_agent @@ -693,6 +712,7 @@ flowchart TD pkg_tools --> pkg_session pkg_tools --> pkg_system_prompt pkg_tools --> pkg_user_approval + pkg_tools --> pkg_working_directory pkg_command_goal --> pkg_commands pkg_command_goal --> pkg_goal pkg_command_goal --> pkg_llm @@ -721,6 +741,17 @@ flowchart TD pkg_deepseek_account_platform --> pkg_credentials pkg_deepseek_account_platform --> pkg_deepseek_account pkg_deepseek_account_platform --> pkg_host_webserver + pkg_experimental_ptc_runtime_python --> pkg_ptc_runtime + pkg_experimental_ptc_runtime_python --> pkg_sandbox + pkg_experimental_ptc_runtime_python --> pkg_sandbox_policy + pkg_experimental_ptc_runtime_python --> pkg_timeout + pkg_experimental_ptc_runtime_python --> pkg_util_values + pkg_experimental_worktree --> pkg_agent + pkg_experimental_worktree --> pkg_fs + pkg_experimental_worktree --> pkg_sandbox + pkg_experimental_worktree --> pkg_sandbox_policy + pkg_experimental_worktree --> pkg_subprocess + pkg_experimental_worktree --> pkg_working_directory pkg_command_feedback --> pkg_anonymous_user_id pkg_command_feedback --> pkg_commands pkg_command_feedback --> pkg_session @@ -763,6 +794,7 @@ flowchart TD pkg_terminal_bash --> pkg_session_projection pkg_terminal_bash --> pkg_subprocess pkg_terminal_bash --> pkg_terminal + pkg_terminal_bash --> pkg_working_directory pkg_agent_loop --> pkg_agent pkg_agent_loop --> pkg_llm pkg_agent_loop --> pkg_scope @@ -788,6 +820,7 @@ flowchart TD pkg_tool_fs --> pkg_system_prompt pkg_tool_fs --> pkg_tools pkg_tool_fs --> pkg_user_approval + pkg_tool_fs --> pkg_working_directory pkg_tool_fs_search --> pkg_llm pkg_tool_fs_search --> pkg_output_retention pkg_tool_fs_search --> pkg_session @@ -796,6 +829,7 @@ flowchart TD pkg_tool_fs_search --> pkg_system_prompt pkg_tool_fs_search --> pkg_timeout pkg_tool_fs_search --> pkg_tools + pkg_tool_fs_search --> pkg_working_directory pkg_tool_str_replace_editor --> pkg_fs pkg_tool_str_replace_editor --> pkg_output_retention pkg_tool_str_replace_editor --> pkg_sandbox @@ -805,6 +839,7 @@ flowchart TD pkg_tool_skill --> pkg_llm pkg_tool_skill --> pkg_skill pkg_tool_skill --> pkg_tools + pkg_tool_skill --> pkg_working_directory pkg_tool_workspace_dependencies --> pkg_tools pkg_tool_web --> pkg_llm pkg_tool_web --> pkg_system_prompt @@ -822,12 +857,6 @@ flowchart TD pkg_plan_mode --> pkg_system_prompt pkg_plan_mode --> pkg_tools pkg_plan_mode --> pkg_user_questions - pkg_hooks_codex --> pkg_agent - pkg_hooks_codex --> pkg_hook_protocol - pkg_hooks_codex --> pkg_llm - pkg_hooks_codex --> pkg_session - pkg_hooks_codex --> pkg_session_projection - pkg_hooks_codex --> pkg_tools pkg_command_compact --> pkg_commands pkg_command_compact --> pkg_compaction pkg_compaction_image_offload --> pkg_agent @@ -841,17 +870,21 @@ flowchart TD pkg_agent_instructions --> pkg_session pkg_agent_instructions --> pkg_session_projection pkg_agent_instructions --> pkg_tools + pkg_agent_instructions --> pkg_working_directory pkg_file_reference_local --> pkg_agent pkg_file_reference_local --> pkg_file_reference pkg_file_reference_local --> pkg_system_prompt pkg_file_reference_local --> pkg_tools + pkg_file_reference_local --> pkg_working_directory pkg_tool_present --> pkg_agent pkg_tool_present --> pkg_fs pkg_tool_present --> pkg_llm pkg_tool_present --> pkg_session pkg_tool_present --> pkg_session_projection pkg_tool_present --> pkg_tools + pkg_tool_present --> pkg_working_directory pkg_workspace_changes --> pkg_agent + pkg_workspace_changes --> pkg_fs pkg_workspace_changes --> pkg_session pkg_workspace_changes --> pkg_subprocess pkg_workspace_changes --> pkg_tools @@ -859,10 +892,12 @@ flowchart TD pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_browser_use pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_system_prompt pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_tools + pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_working_directory pkg_experimental_browser_use_playwright_mcp --> pkg_agent pkg_experimental_browser_use_playwright_mcp --> pkg_browser_use pkg_experimental_browser_use_playwright_mcp --> pkg_system_prompt pkg_experimental_browser_use_playwright_mcp --> pkg_tools + pkg_experimental_browser_use_playwright_mcp --> pkg_working_directory pkg_experimental_browser_use_stagehand_native --> pkg_agent pkg_experimental_browser_use_stagehand_native --> pkg_browser_use pkg_experimental_browser_use_stagehand_native --> pkg_system_prompt @@ -870,6 +905,27 @@ flowchart TD pkg_experimental_computer_use_cua_driver_native --> pkg_computer_use pkg_experimental_computer_use_cua_driver_native --> pkg_system_prompt pkg_experimental_computer_use_cua_driver_native --> pkg_tools + pkg_experimental_tool_worktree --> pkg_experimental_worktree + pkg_experimental_tool_worktree --> pkg_tools + pkg_hooks_codex --> pkg_agent + pkg_hooks_codex --> pkg_hook_protocol + pkg_hooks_codex --> pkg_llm + pkg_hooks_codex --> pkg_session + pkg_hooks_codex --> pkg_session_projection + pkg_hooks_codex --> pkg_tools + pkg_hooks_codex --> pkg_working_directory + pkg_session_title_all_prompts_llm --> pkg_llm + pkg_session_title_all_prompts_llm --> pkg_session + pkg_session_title_all_prompts_llm --> pkg_session_title + pkg_session_title_all_prompts_llm --> pkg_session_title_llm + pkg_tool_terminal --> pkg_agent + pkg_tool_terminal --> pkg_jobs + pkg_tool_terminal --> pkg_llm + pkg_tool_terminal --> pkg_output_retention + pkg_tool_terminal --> pkg_system_prompt + pkg_tool_terminal --> pkg_terminal + pkg_tool_terminal --> pkg_tools + pkg_tool_terminal --> pkg_working_directory pkg_cordis_host_runner --> pkg_agent pkg_cordis_host_runner --> pkg_brand pkg_cordis_host_runner --> pkg_llm @@ -902,6 +958,7 @@ flowchart TD pkg_tool_lsp --> pkg_system_prompt pkg_tool_lsp --> pkg_timeout pkg_tool_lsp --> pkg_tools + pkg_tool_lsp --> pkg_working_directory pkg_mcp_resources --> pkg_llm pkg_mcp_resources --> pkg_scope pkg_mcp_resources --> pkg_system_prompt @@ -919,14 +976,12 @@ flowchart TD pkg_session_checkpoint_policy --> pkg_session pkg_session_checkpoint_policy --> pkg_session_persistence pkg_session_checkpoint_policy --> pkg_tools - pkg_session_title_all_prompts_llm --> pkg_llm - pkg_session_title_all_prompts_llm --> pkg_session - pkg_session_title_all_prompts_llm --> pkg_session_title - pkg_session_title_all_prompts_llm --> pkg_session_title_llm pkg_session_title_first_prompt_llm --> pkg_llm pkg_session_title_first_prompt_llm --> pkg_session pkg_session_title_first_prompt_llm --> pkg_session_title pkg_session_title_first_prompt_llm --> pkg_session_title_llm + pkg_tool_working_directory --> pkg_tools + pkg_tool_working_directory --> pkg_working_directory pkg_shell_env --> pkg_app_boot pkg_shell_env --> pkg_home_paths pkg_shell_env --> pkg_shell @@ -936,11 +991,13 @@ flowchart TD pkg_tool_bash_persistent --> pkg_terminal pkg_tool_bash_persistent --> pkg_timeout pkg_tool_bash_persistent --> pkg_tools + pkg_tool_bash_persistent --> pkg_working_directory pkg_tool_pwsh_persistent --> pkg_agent pkg_tool_pwsh_persistent --> pkg_output_retention pkg_tool_pwsh_persistent --> pkg_terminal pkg_tool_pwsh_persistent --> pkg_timeout pkg_tool_pwsh_persistent --> pkg_tools + pkg_tool_pwsh_persistent --> pkg_working_directory pkg_ssh --> pkg_brand pkg_ssh --> pkg_fs pkg_ssh --> pkg_fs_local @@ -951,13 +1008,6 @@ flowchart TD pkg_ssh --> pkg_session_projection pkg_ssh --> pkg_subprocess pkg_ssh --> pkg_subprocess_local - pkg_tool_terminal --> pkg_agent - pkg_tool_terminal --> pkg_jobs - pkg_tool_terminal --> pkg_llm - pkg_tool_terminal --> pkg_output_retention - pkg_tool_terminal --> pkg_system_prompt - pkg_tool_terminal --> pkg_terminal - pkg_tool_terminal --> pkg_tools pkg_llm_replay --> pkg_compaction pkg_llm_replay --> pkg_deepseek_llm_api_extensions pkg_llm_replay --> pkg_llm @@ -1001,6 +1051,15 @@ flowchart TD pkg_experimental_auto_review --> pkg_permission_presets pkg_experimental_auto_review --> pkg_session pkg_experimental_auto_review --> pkg_tools + pkg_experimental_auto_review --> pkg_working_directory + pkg_webhook --> pkg_agent + pkg_webhook --> pkg_agent_default_model + pkg_webhook --> pkg_agent_preset_registry + pkg_webhook --> pkg_llm + pkg_webhook --> pkg_permission_presets + pkg_webhook --> pkg_session + pkg_webhook --> pkg_session_title + pkg_webhook --> pkg_workspace pkg_tool_cordis --> pkg_agent pkg_tool_cordis --> pkg_app_boot pkg_tool_cordis --> pkg_cordis_host_runner @@ -1034,6 +1093,7 @@ flowchart TD pkg_tool_bash --> pkg_system_prompt pkg_tool_bash --> pkg_tools pkg_tool_bash --> pkg_user_approval + pkg_tool_bash --> pkg_working_directory pkg_tool_pwsh --> pkg_agent pkg_tool_pwsh --> pkg_jobs pkg_tool_pwsh --> pkg_llm @@ -1044,6 +1104,7 @@ flowchart TD pkg_tool_pwsh --> pkg_system_prompt pkg_tool_pwsh --> pkg_tools pkg_tool_pwsh --> pkg_user_approval + pkg_tool_pwsh --> pkg_working_directory pkg_fs_ssh --> pkg_fs pkg_fs_ssh --> pkg_sandbox pkg_fs_ssh --> pkg_sandbox_policy @@ -1055,23 +1116,16 @@ flowchart TD pkg_subprocess_ssh --> pkg_subprocess_local pkg_agent_loop_testkit --> pkg_agent pkg_agent_loop_testkit --> pkg_agent_loop + pkg_agent_loop_testkit --> pkg_fs_local pkg_agent_loop_testkit --> pkg_llm pkg_agent_loop_testkit --> pkg_session pkg_agent_loop_testkit --> pkg_session_projection pkg_agent_loop_testkit --> pkg_system_prompt pkg_agent_loop_testkit --> pkg_tools - pkg_webhook --> pkg_agent - pkg_webhook --> pkg_agent_default_model - pkg_webhook --> pkg_agent_preset_registry - pkg_webhook --> pkg_llm - pkg_webhook --> pkg_permission_presets - pkg_webhook --> pkg_session - pkg_webhook --> pkg_session_title - pkg_webhook --> pkg_workspace + pkg_agent_loop_testkit --> pkg_working_directory pkg_subagent --> pkg_agent pkg_subagent --> pkg_agent_preset_registry pkg_subagent --> pkg_attachment - pkg_subagent --> pkg_jobs pkg_subagent --> pkg_llm pkg_subagent --> pkg_permission_presets pkg_subagent --> pkg_sandbox @@ -1086,6 +1140,7 @@ flowchart TD pkg_subagent --> pkg_typert_protocol pkg_subagent --> pkg_user_approval pkg_subagent --> pkg_util_time + pkg_subagent --> pkg_working_directory pkg_subagent --> pkg_workspace pkg_spill_policy --> pkg_attachment pkg_spill_policy --> pkg_fs @@ -1098,14 +1153,6 @@ flowchart TD pkg_session_query_sqlite --> pkg_session pkg_session_query_sqlite --> pkg_session_persistence pkg_session_query_sqlite --> pkg_session_query - pkg_tool_session_query --> pkg_agent - pkg_tool_session_query --> pkg_llm - pkg_tool_session_query --> pkg_session - pkg_tool_session_query --> pkg_session_projection - pkg_tool_session_query --> pkg_session_query - pkg_tool_session_query --> pkg_system_prompt - pkg_tool_session_query --> pkg_timeout - pkg_tool_session_query --> pkg_tools pkg_acp --> pkg_agent pkg_acp --> pkg_attachment pkg_acp --> pkg_llm @@ -1114,6 +1161,7 @@ flowchart TD pkg_acp --> pkg_session_persistence pkg_acp --> pkg_token_meter pkg_acp --> pkg_user_approval + pkg_plugin_manager --> pkg_agent_preset_registry pkg_plugin_manager --> pkg_brand pkg_plugin_manager --> pkg_hmr pkg_plugin_manager --> pkg_host_plugin_inventory @@ -1123,12 +1171,6 @@ flowchart TD pkg_plugin_manager --> pkg_tools pkg_plugin_manager --> pkg_typert_protocol pkg_plugin_manager --> pkg_user_approval - pkg_headless --> pkg_agent - pkg_headless --> pkg_agent_default_model - pkg_headless --> pkg_fs - pkg_headless --> pkg_llm - pkg_headless --> pkg_session - pkg_headless --> pkg_session_query pkg_compaction_tool_result_pruner --> pkg_compaction pkg_compaction_tool_result_pruner --> pkg_llm pkg_compaction_tool_result_pruner --> pkg_session @@ -1139,6 +1181,29 @@ flowchart TD pkg_experimental_browser_use_runtime --> pkg_scope pkg_experimental_browser_use_runtime --> pkg_system_prompt pkg_experimental_browser_use_runtime --> pkg_tools + pkg_experimental_browser_use_runtime --> pkg_working_directory + pkg_experimental_claude_code_mods --> pkg_agent + pkg_experimental_claude_code_mods --> pkg_commands + pkg_experimental_claude_code_mods --> pkg_fs + pkg_experimental_claude_code_mods --> pkg_llm + pkg_experimental_claude_code_mods --> pkg_scope + pkg_experimental_claude_code_mods --> pkg_session + pkg_experimental_claude_code_mods --> pkg_session_projection + pkg_experimental_claude_code_mods --> pkg_storage_domain + pkg_experimental_claude_code_mods --> pkg_subprocess + pkg_experimental_claude_code_mods --> pkg_token_meter + pkg_experimental_claude_code_mods --> pkg_tools + pkg_experimental_claude_code_mods --> pkg_typert_protocol + pkg_experimental_claude_code_mods --> pkg_user_questions + pkg_experimental_claude_code_mods --> pkg_working_directory + pkg_tool_session_query --> pkg_agent + pkg_tool_session_query --> pkg_llm + pkg_tool_session_query --> pkg_session + pkg_tool_session_query --> pkg_session_projection + pkg_tool_session_query --> pkg_session_query + pkg_tool_session_query --> pkg_system_prompt + pkg_tool_session_query --> pkg_timeout + pkg_tool_session_query --> pkg_tools pkg_webhook_github --> pkg_credentials pkg_webhook_github --> pkg_host_webserver pkg_webhook_github --> pkg_session @@ -1159,14 +1224,11 @@ flowchart TD pkg_subagent_codex --> pkg_subagent pkg_subagent_codex --> pkg_subprocess pkg_subagent_codex --> pkg_timeout - pkg_subagent_in_process_driver --> pkg_agent - pkg_subagent_in_process_driver --> pkg_llm - pkg_subagent_in_process_driver --> pkg_session - pkg_subagent_in_process_driver --> pkg_subagent - pkg_subagent_in_process_driver --> pkg_system_prompt - pkg_subagent_in_process_driver --> pkg_tools + pkg_subagent_fork_in_process --> pkg_agent + pkg_subagent_fork_in_process --> pkg_session + pkg_subagent_fork_in_process --> pkg_subagent + pkg_subagent_spawn_in_process --> pkg_subagent pkg_tool_subagent --> pkg_agent - pkg_tool_subagent --> pkg_jobs pkg_tool_subagent --> pkg_llm pkg_tool_subagent --> pkg_scope pkg_tool_subagent --> pkg_session @@ -1178,13 +1240,6 @@ flowchart TD pkg_tool_subagent_control --> pkg_session pkg_tool_subagent_control --> pkg_subagent pkg_tool_subagent_control --> pkg_tools - pkg_hooks_claude_code --> pkg_agent - pkg_hooks_claude_code --> pkg_hook_protocol - pkg_hooks_claude_code --> pkg_llm - pkg_hooks_claude_code --> pkg_session - pkg_hooks_claude_code --> pkg_session_projection - pkg_hooks_claude_code --> pkg_subagent - pkg_hooks_claude_code --> pkg_tools pkg_api_session_controller --> pkg_agent pkg_api_session_controller --> pkg_agent_default_model pkg_api_session_controller --> pkg_agent_preset_registry @@ -1213,7 +1268,15 @@ flowchart TD pkg_api_session_controller --> pkg_util_time pkg_api_session_controller --> pkg_util_values pkg_api_session_controller --> pkg_util_workspace_path + pkg_api_session_controller --> pkg_working_directory pkg_api_session_controller --> pkg_workspace + pkg_headless --> pkg_agent + pkg_headless --> pkg_agent_default_model + pkg_headless --> pkg_fs + pkg_headless --> pkg_llm + pkg_headless --> pkg_session + pkg_headless --> pkg_session_query + pkg_headless --> pkg_subagent pkg_compaction_basic --> pkg_agent pkg_compaction_basic --> pkg_commands pkg_compaction_basic --> pkg_compaction @@ -1240,15 +1303,23 @@ flowchart TD pkg_experimental_agent_team --> pkg_session_persistence pkg_experimental_agent_team --> pkg_session_projection pkg_experimental_agent_team --> pkg_subagent - pkg_sdk_protocol --> pkg_llm - pkg_sdk_protocol --> pkg_session - pkg_sdk_protocol --> pkg_subagent + pkg_hooks_claude_code --> pkg_agent + pkg_hooks_claude_code --> pkg_hook_protocol + pkg_hooks_claude_code --> pkg_llm + pkg_hooks_claude_code --> pkg_session + pkg_hooks_claude_code --> pkg_session_projection + pkg_hooks_claude_code --> pkg_subagent + pkg_hooks_claude_code --> pkg_tools + pkg_hooks_claude_code --> pkg_working_directory pkg_tool_ralph --> pkg_agent pkg_tool_ralph --> pkg_llm pkg_tool_ralph --> pkg_subagent pkg_tool_ralph --> pkg_system_prompt pkg_tool_ralph --> pkg_tools pkg_tool_ralph --> pkg_workflow + pkg_sdk_protocol --> pkg_llm + pkg_sdk_protocol --> pkg_session + pkg_sdk_protocol --> pkg_subagent pkg_workflow_ptc --> pkg_agent pkg_workflow_ptc --> pkg_llm pkg_workflow_ptc --> pkg_ptc_runtime @@ -1258,12 +1329,7 @@ flowchart TD pkg_workflow_ptc --> pkg_subagent pkg_workflow_ptc --> pkg_tools pkg_workflow_ptc --> pkg_workflow - pkg_subagent_fork_in_process --> pkg_agent - pkg_subagent_fork_in_process --> pkg_session - pkg_subagent_fork_in_process --> pkg_subagent - pkg_subagent_fork_in_process --> pkg_subagent_in_process_driver - pkg_subagent_spawn_in_process --> pkg_subagent - pkg_subagent_spawn_in_process --> pkg_subagent_in_process_driver + pkg_workflow_ptc --> pkg_working_directory pkg_experimental_client_ui_agent_team --> pkg_api_session_controller pkg_experimental_client_ui_agent_team --> pkg_client_locale pkg_experimental_client_ui_agent_team --> pkg_client_ui_conversation @@ -1274,6 +1340,22 @@ flowchart TD pkg_experimental_client_ui_agent_team --> pkg_client_ui_workspace pkg_experimental_client_ui_agent_team --> pkg_experimental_agent_team pkg_experimental_client_ui_agent_team --> pkg_session + pkg_experimental_client_ui_claude_code_mods --> pkg_api_remotes + pkg_experimental_client_ui_claude_code_mods --> pkg_api_session_controller + pkg_experimental_client_ui_claude_code_mods --> pkg_client_locale + pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_conversation + pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_primitives + pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_renderer + pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_session + pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_slots + pkg_experimental_client_ui_claude_code_mods --> pkg_experimental_claude_code_mods + pkg_experimental_client_ui_claude_code_mods --> pkg_session + pkg_experimental_client_ui_claude_code_mods --> pkg_typert_protocol + pkg_experimental_client_ui_cot_translation --> pkg_api_session_controller + pkg_experimental_client_ui_cot_translation --> pkg_experimental_translator + pkg_experimental_client_ui_cot_translation --> pkg_session + pkg_experimental_client_ui_cot_translation --> pkg_settings + pkg_experimental_client_ui_cot_translation --> pkg_typert_protocol pkg_experimental_client_ui_voice_input --> pkg_api_gateway pkg_experimental_client_ui_voice_input --> pkg_api_remotes pkg_experimental_client_ui_voice_input --> pkg_api_session_controller @@ -1317,6 +1399,7 @@ flowchart TD pkg_sdk_jsonrpc_server --> pkg_sdk_protocol pkg_sdk_jsonrpc_server --> pkg_session pkg_sdk_jsonrpc_server --> pkg_subagent + pkg_sdk_jsonrpc_server --> pkg_working_directory pkg_client_test_runtime --> pkg_api_gateway pkg_client_test_runtime --> pkg_api_session_controller pkg_client_test_runtime --> pkg_api_workspace_controller @@ -1443,8 +1526,14 @@ flowchart TD | [`credentials`](../packages/credentials/credentials) | `credentials` | — | | [`office-to-pdf`](../packages/document/office-to-pdf) | `document` | — | | [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — | +| [`experimental-badge-skill-bundle`](../packages/experimental/badge-skill-bundle) | `experimental` | — | +| [`experimental-cot-translation-bundle`](../packages/experimental/cot-translation-bundle) | `experimental` | — | | [`experimental-inspector-profile`](../packages/experimental/inspector-profile) | `experimental` | — | +| [`experimental-ralph-bundle`](../packages/experimental/ralph-bundle) | `experimental` | — | | [`experimental-session-inspector`](../packages/experimental/session-inspector) | `experimental` | — | +| [`experimental-session-search`](../packages/experimental/session-search) | `experimental` | — | +| [`experimental-session-titles-bundle`](../packages/experimental/session-titles-bundle) | `experimental` | — | +| [`experimental-terminal-bundle`](../packages/experimental/terminal-bundle) | `experimental` | — | | [`experimental-voice-input-bundle`](../packages/experimental/voice-input-bundle) | `experimental` | — | | [`experimental-webworker-packer`](../packages/experimental/webworker-packer) | `experimental` | — | | [`client-ui-cordis`](../packages/extensions/ui-cordis) | `extensions` | — | @@ -1485,7 +1574,6 @@ flowchart TD | [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`http-proxy`](../packages/util/http-proxy) | | [`remote-mock`](../packages/test-support/remote-mock) | `test-support` | [`typert-protocol`](../packages/typert/protocol) | | [`typert-loader`](../packages/typert/loader) | `typert` | [`typert-registry`](../packages/typert/registry) | -| [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) | | [`skill-office`](../packages/skill/skill-office) | `skill` | [`skill`](../packages/skill/skill) | | [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`http-proxy`](../packages/util/http-proxy), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) | | [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) | @@ -1498,6 +1586,7 @@ flowchart TD | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) | | [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) | | [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) | +| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) | | [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver) | | [`persona`](../packages/preset/persona) | `preset` | [`system-prompt`](../packages/core/system-prompt) | | [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | @@ -1515,6 +1604,7 @@ flowchart TD | [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | | [`hmr`](../packages/boot/hmr) | `boot` | [`app-boot`](../packages/boot/app-boot), [`cmdline`](../packages/boot/cmdline) | | [`experimental-speech-to-text`](../packages/experimental/speech-to-text) | `experimental` | [`settings`](../packages/settings/settings) | +| [`experimental-translator`](../packages/experimental/translator) | `experimental` | [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) | | [`host-product-telemetry-otel`](../packages/host/product-telemetry-otel) | `host` | [`otel`](../packages/telemetry/otel) | | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | `ptc-runtime` | [`sandbox`](../packages/sandbox/sandbox) | | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) | @@ -1530,12 +1620,11 @@ flowchart TD | [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) | | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | `fs` | [`fs`](../packages/fs/fs) | | [`skill-filesystem`](../packages/skill/skill-filesystem) | `skill` | [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`skill`](../packages/skill/skill) | -| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`session`](../packages/core/session), [`shell`](../packages/shell/shell) | | [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`host-directory-picker`](../packages/host/directory-picker), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) | | [`config-editor`](../packages/boot/config-editor) | `boot` | [`app-boot`](../packages/boot/app-boot), [`hmr`](../packages/boot/hmr) | | [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | `experimental` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`typert-protocol`](../packages/typert/protocol) | -| [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | `experimental` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) | | [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | `experimental` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`subprocess`](../packages/subprocess/subprocess) | +| [`hook-protocol`](../packages/experimental/hook-protocol) | `experimental` | [`session`](../packages/core/session), [`shell`](../packages/shell/shell) | | [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`bash-local`](../packages/shell/bash-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | @@ -1554,11 +1643,12 @@ flowchart TD | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) | | [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`session`](../packages/core/session) | | [`session-title`](../packages/session/session-title) | `session` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | +| [`working-directory`](../packages/session/working-directory) | `session` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) | | [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) | | [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) | -| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) | +| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) | | [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) | | [`goal-round-driver`](../packages/goal/goal-round-driver) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) | @@ -1566,6 +1656,8 @@ flowchart TD | [`api-account-controller`](../packages/api/account-controller) | `api` | [`agent`](../packages/core/agent), [`deepseek-account`](../packages/credentials/deepseek-account), [`typert-protocol`](../packages/typert/protocol) | | [`compaction`](../packages/compaction/compaction) | `compaction` | [`brand`](../packages/util/brand), [`commands`](../packages/interaction/commands), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) | `credentials` | [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`host-webserver`](../packages/host/webserver) | +| [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | `experimental` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) | +| [`experimental-worktree`](../packages/experimental/worktree) | `experimental` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`subprocess`](../packages/subprocess/subprocess), [`working-directory`](../packages/session/working-directory) | | [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`commands`](../packages/interaction/commands), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | | [`permission-presets`](../packages/interaction/permission-presets) | `interaction` | [`commands`](../packages/interaction/commands), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) | | [`jobs-local`](../packages/jobs/jobs-local) | `jobs` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`scope`](../packages/core/scope), [`timeout`](../packages/util/timeout) | @@ -1573,46 +1665,48 @@ flowchart TD | [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) | | [`bash-sandbox`](../packages/shell/bash-sandbox) | `shell` | [`bash-local`](../packages/shell/bash-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) | | [`pwsh-sandbox`](../packages/shell/pwsh-sandbox) | `shell` | [`pwsh-local`](../packages/shell/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) | -| [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal) | +| [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal), [`working-directory`](../packages/session/working-directory) | | [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`agent-tool-presentation`](../packages/core/agent-tool-presentation) | `core` | [`tools`](../packages/core/tools) | | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | -| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) | +| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`output-retention`](../packages/util/output-retention), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) | -| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | +| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`tool-workspace-dependencies`](../packages/skill/tool-workspace-dependencies) | `skill` | [`tools`](../packages/core/tools) | | [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) | | [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | | [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) | -| [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | | [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction) | | [`compaction-image-offload`](../packages/compaction/compaction-image-offload) | `compaction` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | -| [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-present`](../packages/deliverables/tool-present) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) | -| [`workspace-changes`](../packages/deliverables/workspace-changes) | `deliverables` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) | -| [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`tool-present`](../packages/deliverables/tool-present) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`workspace-changes`](../packages/deliverables/workspace-changes) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) | +| [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | `experimental` | [`computer-use`](../packages/computer-use/computer-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`experimental-tool-worktree`](../packages/experimental/tool-worktree) | `experimental` | [`experimental-worktree`](../packages/experimental/worktree), [`tools`](../packages/core/tools) | +| [`hooks-codex`](../packages/experimental/hooks-codex) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`session-title-all-prompts-llm`](../packages/experimental/session-title-all-prompts-llm) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | +| [`tool-terminal`](../packages/experimental/tool-terminal) | `experimental` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`terminal`](../packages/terminal/terminal), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | `extensions` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) | | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-protocol`](../packages/typert/protocol) | | [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder) | `guard` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools) | | [`tool-call-timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`tool-ask-user`](../packages/interaction/tool-ask-user) | `interaction` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) | | [`tool-jobs`](../packages/jobs/tool-jobs) | `jobs` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`mcp-resources`](../packages/mcp/mcp-resources) | `mcp` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`agent-preset-registry`](../packages/preset/agent-preset-registry) | `preset` | [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) | | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) | -| [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | | [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | +| [`tool-working-directory`](../packages/session/tool-working-directory) | `session` | [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`shell-env`](../packages/shell/shell-env) | `shell` | [`app-boot`](../packages/boot/app-boot), [`home-paths`](../packages/util/home-paths), [`shell`](../packages/shell/shell), [`tools`](../packages/core/tools) | -| [`tool-bash-persistent`](../packages/shell/tool-bash-persistent) | `shell` | [`agent`](../packages/core/agent), [`output-retention`](../packages/util/output-retention), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | -| [`tool-pwsh-persistent`](../packages/shell/tool-pwsh-persistent) | `shell` | [`agent`](../packages/core/agent), [`output-retention`](../packages/util/output-retention), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`tool-bash-persistent`](../packages/shell/tool-bash-persistent) | `shell` | [`agent`](../packages/core/agent), [`output-retention`](../packages/util/output-retention), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`tool-pwsh-persistent`](../packages/shell/tool-pwsh-persistent) | `shell` | [`agent`](../packages/core/agent), [`output-retention`](../packages/util/output-retention), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`ssh`](../packages/ssh/ssh) | `ssh` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-local`](../packages/sandbox/sandbox-local), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) | -| [`tool-terminal`](../packages/terminal/tool-terminal) | `terminal` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`terminal`](../packages/terminal/terminal), [`tools`](../packages/core/tools) | | [`llm-replay`](../packages/test-support/llm-replay) | `test-support` | [`compaction`](../packages/compaction/compaction), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`session`](../packages/core/session) | @@ -1620,51 +1714,53 @@ flowchart TD | [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) | | [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) | | [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) | -| [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | +| [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`webhook`](../packages/experimental/webhook) | `experimental` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) | | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`app-boot`](../packages/boot/app-boot), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`tools`](../packages/core/tools) | | [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`brand`](../packages/util/brand), [`typert-protocol`](../packages/typert/protocol) | | [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-resources`](../packages/mcp/mcp-resources), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`agent-preset`](../packages/preset/agent-preset) | `preset` | [`agent-preset-registry`](../packages/preset/agent-preset-registry) | | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`otel`](../packages/telemetry/otel), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) | -| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | -| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | +| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) | +| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) | | [`fs-ssh`](../packages/ssh/fs-ssh) | `ssh` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`ssh`](../packages/ssh/ssh) | | [`sandbox-ssh`](../packages/ssh/sandbox-ssh) | `ssh` | [`sandbox`](../packages/sandbox/sandbox), [`ssh`](../packages/ssh/ssh) | | [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | `ssh` | [`ssh`](../packages/ssh/ssh), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) | -| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) | -| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) | +| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`fs-local`](../packages/fs/fs-local), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time), [`working-directory`](../packages/session/working-directory), [`workspace`](../packages/workspace/workspace) | | [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) | | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) | -| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) | -| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) | -| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) | +| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) | | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | -| [`experimental-browser-use-runtime`](../packages/experimental/browser-use-runtime) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`mcp-client`](../packages/mcp/mcp-client), [`scope`](../packages/core/scope), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`webhook-github`](../packages/webhook/webhook-github) | `webhook` | [`credentials`](../packages/credentials/credentials), [`host-webserver`](../packages/host/webserver), [`session`](../packages/core/session), [`webhook`](../packages/webhook/webhook) | +| [`experimental-browser-use-runtime`](../packages/experimental/browser-use-runtime) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`mcp-client`](../packages/mcp/mcp-client), [`scope`](../packages/core/scope), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods) | `experimental` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subprocess`](../packages/subprocess/subprocess), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions), [`working-directory`](../packages/session/working-directory) | +| [`tool-session-query`](../packages/experimental/tool-session-query) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`webhook-github`](../packages/experimental/webhook-github) | `experimental` | [`credentials`](../packages/credentials/credentials), [`host-webserver`](../packages/host/webserver), [`session`](../packages/core/session), [`webhook`](../packages/experimental/webhook) | | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | -| [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | +| [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`subagent`](../packages/subagent/subagent) | +| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | -| [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | -| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) | +| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`working-directory`](../packages/session/working-directory), [`workspace`](../packages/workspace/workspace) | +| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`subagent`](../packages/subagent/subagent) | | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) | | [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent) | +| [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | -| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | -| [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | -| [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | -| [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | +| [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow), [`working-directory`](../packages/session/working-directory) | | [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session) | +| [`experimental-client-ui-claude-code-mods`](../packages/experimental/client-ui-claude-code-mods) | `experimental` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | +| [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | `experimental` | [`api-session-controller`](../packages/api/session-controller), [`experimental-translator`](../packages/experimental/translator), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) | | [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | [`api-gateway`](../packages/api/gateway), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-store`](../packages/client/store), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text), [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) | | [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) | -| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | +| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`working-directory`](../packages/session/working-directory) | | [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) | | [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | | [`tool-schedule`](../packages/schedule/tool-schedule) | `schedule` | [`llm`](../packages/llm/llm), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 0797c29333..4e1c160841 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -6,11 +6,11 @@ en: a11f106978c9dce7 zh: a11f106978c9dce7 /session-persistence-event-catalog: - en: e7c80d1de4d328b4 - zh: d9f368c27fd9f998 + en: 1387c4505dde2faf + zh: 48570fa1c13d6910 /session-persistence-event-catalog/persistence-type-fingerprints: - en: 33dd28b1c768cd8b - zh: 7256442baf2faed1 + en: d8542cb8987022e0 + zh: 38e135021cb21f20 /session-persistence-event-catalog/event-envelope: en: 7ebb1a2e0dd1f916 zh: 8946f415dda5c1b8 @@ -108,11 +108,11 @@ en: 8d6fa78d450a0e56 zh: 8d6fa78d450a0e56 /session-persistence-event-catalog/events/hook/hook-invoked-log-only: - en: 7e87780c1e9328b7 - zh: 79b535ed8a26bc4f + en: 705ffb04951178a1 + zh: 6031b9e7b48f5549 /session-persistence-event-catalog/events/hook/hook-result-log-only: - en: 5479d86486adbc40 - zh: b9df61258535c7da + en: b84c2cbfca534935 + zh: ee1d71fad6dfb896 /session-persistence-event-catalog/events/image: en: 4378f94cd30a2ba8 zh: 4378f94cd30a2ba8 @@ -177,8 +177,8 @@ en: 6c6a27578dff5828 zh: 9a5add2bc0df2d0a /session-persistence-event-catalog/events/session/session-title-llm-request-log-only: - en: 201b220320c466ab - zh: 2823df91f681793d + en: 5eb108772a8498c9 + zh: 1fbc45e377084bae /session-persistence-event-catalog/events/session-log-deepseek: en: 7b827672c80aecdd zh: 7b827672c80aecdd @@ -198,8 +198,8 @@ en: 9630752e97ddbfbc zh: 9630752e97ddbfbc /session-persistence-event-catalog/events/subagent/subagent-catalog-log-only: - en: b0d8c66ca02aea4d - zh: 74adeca7962a61ed + en: e6ce4b5a98ff2f52 + zh: 9a8e112754679aef /session-persistence-event-catalog/events/subagent/subagent-descriptor-log-only: en: f5d53639dc5afdfc zh: 8c553e7397d08832 @@ -240,11 +240,11 @@ en: 96cc0c05d3a2d01b zh: f6f86d5bd29db6a5 /session-persistence-event-catalog/events/tool/tool-ptc-dispatch-log-only: - en: 1c5fbea27fd81894 - zh: 0188fe2faf5b263c + en: 3d5d72b99e702a8c + zh: e8c3b82cd5361c4a /session-persistence-event-catalog/events/tool/tool-ptc-dispatch-start-log-only: - en: 14548299cbb98be3 - zh: 3b081f53e8591fc0 + en: de38cf70d81ad7c3 + zh: dd0a2a8be099271d /session-persistence-event-catalog/events/tool/tool-result-surface: en: 2857bd718f6e7264 zh: 61aac43cff6451b6 @@ -284,6 +284,12 @@ /session-persistence-event-catalog/events/web/web-deepseek-search-llm-request-log-only: en: f12c598242992aa3 zh: 08609553e15a91f3 +/session-persistence-event-catalog/events/working-directory: + en: e8dd0cebbb930eb7 + zh: e8dd0cebbb930eb7 +/session-persistence-event-catalog/events/working-directory/working-directory-change-log-only: + en: 075369b880bd304b + zh: 0eca94dbaf5d9090 /session-persistence-event-catalog/events/workspace: en: 63ee1a85e4b59ae3 zh: 63ee1a85e4b59ae3 @@ -459,8 +465,11 @@ en: 0a0098cf30464dcf zh: 0a0098cf30464dcf /session-persistence-event-catalog/resolved-persistence-types/every: - en: eeb0f1af5d02bc22 - zh: eeb0f1af5d02bc22 + en: ea3afecddf32f372 + zh: ea3afecddf32f372 +/session-persistence-event-catalog/resolved-persistence-types/external: + en: 2b8208db8e7bfa49 + zh: 2b8208db8e7bfa49 /session-persistence-event-catalog/resolved-persistence-types/failed: en: 43330e69d3defaab zh: 43330e69d3defaab @@ -885,8 +894,14 @@ en: a773058b4f0685f6 zh: a773058b4f0685f6 /session-persistence-event-catalog/resolved-persistence-types/webhook: - en: 390b9ebc0521ee93 - zh: 390b9ebc0521ee93 + en: e7b2736a2adb5db0 + zh: e7b2736a2adb5db0 +/session-persistence-event-catalog/resolved-persistence-types/working-directory: + en: 6aabd13040c9e590 + zh: 6aabd13040c9e590 +/session-persistence-event-catalog/resolved-persistence-types/working-directory-change: + en: 8fe8f07c8548191d + zh: 8fe8f07c8548191d /session-persistence-event-catalog/resolved-persistence-types/workspace-write: en: a529f422b9889f7d zh: a529f422b9889f7d @@ -906,17 +921,17 @@ en: 9ec6b8bd755f3e33 zh: 9ec6b8bd755f3e33 /session-persistence-event-catalog/resolved-persistence-types/agentinstructionchange: - en: c79fd9b4b7a0a5ba - zh: d196895911636220 + en: 98710438ecb65dd6 + zh: 82c06bda2056f000 /session-persistence-event-catalog/resolved-persistence-types/agentinstructionchange~2: en: dba6b1babdde0cfe zh: 99b209824b99b693 /session-persistence-event-catalog/resolved-persistence-types/agentinstructionsource: - en: 4b46cc6c412c939d - zh: 77fc7f61972ab194 + en: ef87a859325357fc + zh: f132439b0161ece6 /session-persistence-event-catalog/resolved-persistence-types/agentmessagesource: - en: 69385a93c3abf7f5 - zh: 0adbb7fd29dccb2e + en: a1f024101085f6f1 + zh: eedf4dc28c048838 /session-persistence-event-catalog/resolved-persistence-types/approvaloutcome: en: bb7b3652609ac651 zh: bdde4e652413c5eb @@ -957,11 +972,11 @@ en: 586743dfeab78c55 zh: 26ccef4a02dd7a58 /session-persistence-event-catalog/resolved-persistence-types/deepseeksearchllmrequest: - en: ea25cd944b4b4020 - zh: 87f8944f766d3a85 + en: 8bb149f81f7c2544 + zh: 7e9c410f530b7c82 /session-persistence-event-catalog/resolved-persistence-types/developermessage: - en: af7ce240db9e3463 - zh: cd15cd0cd369c2c0 + en: da88473da1299be0 + zh: fe756703e34fcc42 /session-persistence-event-catalog/resolved-persistence-types/epochheader: en: d31020fa1fda6fa4 zh: 77984c4bf09443fb @@ -1005,11 +1020,11 @@ en: d13de5c951afd4e5 zh: 44069b6d0f3b8303 /session-persistence-event-catalog/resolved-persistence-types/headerline: - en: 0228dc7dcff3ff5a - zh: bfab168767453f00 + en: d0e4b453af58af7f + zh: 8209d2206840b137 /session-persistence-event-catalog/resolved-persistence-types/hookdialect: - en: c8420c48f6167ad9 - zh: a6dc3dc1ee50ea81 + en: c5418d1e906e9114 + zh: 0bc528427386d8a4 /session-persistence-event-catalog/resolved-persistence-types/imageattachmentref: en: 76ee1e266cd651da zh: 55c7c95a223ccbed @@ -1047,11 +1062,11 @@ en: d2a3b6594fc71ea0 zh: a5a7df42594fdb57 /session-persistence-event-catalog/resolved-persistence-types/llmcallconfig: - en: aa3a91341bf04b9e - zh: d767b45d45846333 + en: 620752606b81ad3b + zh: 336903c12d1d3bdc /session-persistence-event-catalog/resolved-persistence-types/llmcallconfigadapterdefaults: - en: 5cdc7289f276f93e - zh: 11b399ac1a145e9c + en: 8f669d7819805099 + zh: 391d51d7835afe02 /session-persistence-event-catalog/resolved-persistence-types/llmfailure: en: a18d2091f7e64726 zh: 550fc25036bd096e @@ -1059,11 +1074,11 @@ en: 6dbe099c62f7c226 zh: 6fd788f63c038c91 /session-persistence-event-catalog/resolved-persistence-types/llmretrystartedeventdata: - en: f4202ef53de39006 - zh: f8f1d0bfc0451c31 + en: cc63fb8b8fad7d63 + zh: 2026e3da98cad54e /session-persistence-event-catalog/resolved-persistence-types/message: - en: a3d2a5be822c5a86 - zh: 871cc3cafabe99be + en: e478c640a9c17a4f + zh: b4c6a4930a3789a1 /session-persistence-event-catalog/resolved-persistence-types/messagefeedbackdelete: en: c582953270d4ca44 zh: 966ccfe2e7660a2a @@ -1074,14 +1089,14 @@ en: f58575a1fc96df42 zh: 9f0c6ed9033e5c9d /session-persistence-event-catalog/resolved-persistence-types/messagefeedbackrating: - en: ef7b940d5654ce5a - zh: 72d4c5b3f6e2ff17 + en: 46f4cbb41e7c053a + zh: 511e10d1be567bc5 /session-persistence-event-catalog/resolved-persistence-types/messagesource: - en: f1789e91fd4338e3 - zh: 8250eb33cda749b5 + en: c4ed18665fbc1e15 + zh: 6db2b051fc329408 /session-persistence-event-catalog/resolved-persistence-types/message~2: - en: 412cc1296786d489 - zh: f457e8629515a526 + en: 2c79aaf7cec7d9d6 + zh: 1403dd3cf9ed4a82 /session-persistence-event-catalog/resolved-persistence-types/modelmessagesource: en: 3b677f021d7dd4d3 zh: 2a875742b22879a2 @@ -1101,14 +1116,14 @@ en: 3e36e2f29ff1b506 zh: 3dcc1a1dced59d9f /session-persistence-event-catalog/resolved-persistence-types/presentedfile~2: - en: 45eec434ea27986c - zh: c8d52b34de4df04f + en: 807fde1cf42ddef3 + zh: 2ea131667b990d1e /session-persistence-event-catalog/resolved-persistence-types/ptcdispatcheventdata: - en: 78e72b973fe1c580 - zh: 9947fbdb2ad284d8 + en: 81e66c5a07e17dd6 + zh: 0ecf3cfceba14d2b /session-persistence-event-catalog/resolved-persistence-types/ptcdispatchstarteventdata: - en: 3e120cbeff3f99ac - zh: 19cf00f4b5ef2121 + en: fbbf650635960ec8 + zh: 9457953f4cbe07c6 /session-persistence-event-catalog/resolved-persistence-types/reasoningblock: en: 07e04c3e336f880f zh: 6f8986ede9994fa1 @@ -1140,11 +1155,11 @@ en: 9a54c4478ad3b620 zh: 02c4c1fd7e252770 /session-persistence-event-catalog/resolved-persistence-types/sessiontitleeventdata: - en: 2c684a3db9219c45 - zh: 202000f582ca1ec8 + en: 4ff17eae0b30056d + zh: 6642afbaa87da3ab /session-persistence-event-catalog/resolved-persistence-types/sessiontitlellmrequesteventdata: - en: 7b180deead48738e - zh: 35d43b7ef489f97b + en: 390c1daad2342ce8 + zh: 07ce68854dce8ea4 /session-persistence-event-catalog/resolved-persistence-types/sessiontitlemodelidentity: en: 9531468264499498 zh: 9da23c0ad2776563 @@ -1155,23 +1170,23 @@ en: 2642015fb4518c8c zh: 152b294f41fbdcca /session-persistence-event-catalog/resolved-persistence-types/skillcatalogsource: - en: 3e76d9f3d5bf82a0 - zh: fd037f5c12ee6330 + en: de04d42a8b0004df + zh: db64863dd7da1e26 /session-persistence-event-catalog/resolved-persistence-types/skillinvocationsource: en: ecc15ebadb77afdf zh: 97ac27d6a4175f6f /session-persistence-event-catalog/resolved-persistence-types/streamchunk: - en: e865a9a3b7749c1a - zh: 16800bf55135c89e + en: 0d571899222eb04f + zh: de28c6d67d1763c1 /session-persistence-event-catalog/resolved-persistence-types/subagentcatalogevent: - en: e9c8f82e0497e5a5 - zh: 55ab13b1a5067759 + en: 725539dcd5c335ed + zh: 5ca08d3667f84ff9 /session-persistence-event-catalog/resolved-persistence-types/subagentdescriptordata: en: c56821e57987ae9e zh: 0364f59a472d51da /session-persistence-event-catalog/resolved-persistence-types/subagentsettledmessagesource: - en: a5e19707264c7449 - zh: 4c619e931997d3f0 + en: d34acab659e74957 + zh: 25cabc6c9fb44a7e /session-persistence-event-catalog/resolved-persistence-types/surfaceop: en: 4f3349f21317ba18 zh: 508c26670085953a @@ -1224,8 +1239,8 @@ en: f43a635a14568780 zh: 8d22226b045122eb /session-persistence-event-catalog/resolved-persistence-types/toolrestriction: - en: b6f6001a25bcbf82 - zh: bd2b3c9fe0ad2ba8 + en: d7d3c0238d9b373e + zh: f49d93ee54e7a780 /session-persistence-event-catalog/resolved-persistence-types/toolresultmessage: en: 2258683c3cfa4ce3 zh: 5663aa76c911761b @@ -1251,14 +1266,14 @@ en: 544292eddeff5faa zh: 4e36e575a5030fb2 /session-persistence-event-catalog/resolved-persistence-types/turnendreason: - en: 7fd4741c641f9252 - zh: be115df1a8ee59ce + en: cdf0bbf6399a3677 + zh: f4422bf67b216bae /session-persistence-event-catalog/resolved-persistence-types/usermessage: - en: c9090b73ac4ce05b - zh: beb1f58404b0a2b8 + en: b1c19f6d8f6f31fc + zh: 3843640f47897934 /session-persistence-event-catalog/resolved-persistence-types/usermessage~2: - en: a3974d05a5d357d3 - zh: 99e73822f538c99d + en: 2ee253b3743ee6a2 + zh: 5ee8b08850d93d8e /session-persistence-event-catalog/resolved-persistence-types/workflowagentoutcome: en: d0a3f9aab18c3de5 zh: 76ac69a835ca4e29 @@ -1281,11 +1296,11 @@ en: 5e7d82a493a1d4d8 zh: d2d785891dd766fc /session-persistence-event-catalog/resolved-persistence-types/number~2: - en: 38211d71372021e4 - zh: c0305af81a67e285 + en: 386989934d1bb62e + zh: 69b8bc182a5acbd3 /session-persistence-event-catalog/resolved-persistence-types/string: - en: 052f9ce1988e44f2 - zh: 5adaad6c9bca43e2 + en: 21a0bfa3d2e4d775 + zh: e1d212b7cbc37c35 /session-persistence-event-catalog/resolved-persistence-types/string~2: en: 0089620bf4638263 zh: 1922bef1713094bb @@ -1371,11 +1386,14 @@ en: dc8738014c719f4e zh: c773fd60c0090231 /session-persistence-event-catalog/resolved-persistence-types/childcreatedat-childid-label-mode~5: - en: 296001b5c7359a11 - zh: b5fdd40b7068497e + en: 43cf5cd666b591a0 + zh: 7c7a3011f8681f12 +/session-persistence-event-catalog/resolved-persistence-types/childcreatedat-childid-label-mode~6: + en: a1f188c9058cf621 + zh: eb43c0f045fb9afb /session-persistence-event-catalog/resolved-persistence-types/code-name-reason: - en: 9615c513ac256ad8 - zh: a492b49c787d4549 + en: 5b7790906008fcba + zh: 3d1a6bdb82d82fde /session-persistence-event-catalog/resolved-persistence-types/commandid-kind-sourceeventseq-text: en: 87b03e1c73d2a326 zh: d7b9f06863cad891 @@ -1389,11 +1407,14 @@ en: 45f1d815198508e1 zh: 5c2818b0e38e1356 /session-persistence-event-catalog/resolved-persistence-types/compactionid-sourcecommandid-turn: - en: 4ba70efccc268763 - zh: 06ba9b3e824c80a7 + en: bef51e0442ebb903 + zh: 240ebeb160533d8c +/session-persistence-event-catalog/resolved-persistence-types/cwd: + en: 1bc55be85a68f0a3 + zh: 9e84a55ed11bba5d /session-persistence-event-catalog/resolved-persistence-types/decision-durationms-exitcode-handlerid: - en: ed8ca43bb7622393 - zh: 697e1ccbe9439aa9 + en: a99ac57bbec0370f + zh: 9073329e324a66e0 /session-persistence-event-catalog/resolved-persistence-types/delayms-failure-maxretries-mode: en: 27987dc154def5d8 zh: 7dd493f04daa654f @@ -1401,14 +1422,14 @@ en: 10a2d594b106f6fe zh: 8408379e744ad987 /session-persistence-event-catalog/resolved-persistence-types/description-name: - en: 024bacc58243ccc7 - zh: 0672c4360841916f + en: e501ab0e92f12d77 + zh: 087fb91a04f85398 /session-persistence-event-catalog/resolved-persistence-types/description-name~2: en: a07969828eed2e6f zh: e06893743af544b5 /session-persistence-event-catalog/resolved-persistence-types/dialect-handlerid-matcher-point: - en: afb33a900f77ddf6 - zh: 90edc32643550430 + en: eeba88520aacd126 + zh: 8e2ca4e2bd2c1e3a /session-persistence-event-catalog/resolved-persistence-types/end-start: en: 4b7a8793663d3fef zh: 249459c825d330c3 @@ -1428,11 +1449,11 @@ en: 51af619ed8532a43 zh: a5ad878c63d0e046 /session-persistence-event-catalog/resolved-persistence-types/header-reason-startsseries: - en: 2b549aa36fe3ab7b - zh: 069ef4498112c4e7 + en: 562fcce1a6c17ade + zh: 15828c9bf4dcbe6e /session-persistence-event-catalog/resolved-persistence-types/headerseq-message-step-turn: - en: 97dc4446fd0b02ac - zh: 57ec4b27d0221957 + en: 884dda8acdf28a83 + zh: 53ef06336629743c /session-persistence-event-catalog/resolved-persistence-types/height-width: en: a5df50710083ac25 zh: aad3ccf6f6de70e4 @@ -1449,11 +1470,11 @@ en: 41f73c58a77dfa28 zh: af77f4aee3cb4a40 /session-persistence-event-catalog/resolved-persistence-types/inherited: - en: 7dcf0c1ade316952 - zh: 3c9cd7564659ec3d + en: f46de4c8d413b567 + zh: af0283688492ac43 /session-persistence-event-catalog/resolved-persistence-types/inserted-outcome-removedcount-start: - en: f2167c4cf3aa9b4d - zh: b4ed04a9a526f49e + en: 62e1cb96abf41fd5 + zh: c24c03766f6790d8 /session-persistence-event-catalog/resolved-persistence-types/interrupted-message-step-stream: en: f460572284c92755 zh: bae7561ce887b040 @@ -1758,8 +1779,8 @@ en: 08be4349548406a9 zh: 8e7cf9966198ef78 /session-persistence-event-catalog/resolved-persistence-types/kind-tool-registry: - en: 77a2e15c8413b18c - zh: 91d39eed9dfd96d1 + en: 79db83dd2c430faf + zh: 173f568241f5ce86 /session-persistence-event-catalog/resolved-persistence-types/kind-user: en: aacb77f1cead58ba zh: e6ce9dac4bdd07b7 @@ -1788,8 +1809,11 @@ en: 77a8fc6a2f3a9319 zh: 4dc1de1358b51a8d /session-persistence-event-catalog/resolved-persistence-types/kind-webhook-form-notice: - en: 4cd5dc7523fefebb - zh: f467561fbd0ae394 + en: 0b74295f7c5c8f11 + zh: 5f1213cec87f6be0 +/session-persistence-event-catalog/resolved-persistence-types/kind-working-directory: + en: 9848d783b7b5af16 + zh: f161eaf1a841cd55 /session-persistence-event-catalog/resolved-persistence-types/max-tokens-messages-model-tools: en: afedf4a31a67eb14 zh: 98d3bc882f2b61c3 @@ -1845,11 +1869,11 @@ en: a79b2ab16b9ed215 zh: 764cd1400ee8db63 /session-persistence-event-catalog/resolved-persistence-types/type-agent-preset-selected: - en: c8b6e46c8e161688 - zh: 3f45f3e70ce9f600 + en: 52112a8633ba49e4 + zh: 95318f8c8b2d19ee /session-persistence-event-catalog/resolved-persistence-types/type-agent-inbox-spliced: - en: d73cb120a4aa138e - zh: 1f5ce9f1bb430aec + en: 35219c983f8168c9 + zh: 5bdf2bc555cc5e1a /session-persistence-event-catalog/resolved-persistence-types/type-approval-asked: en: 9c77f18b0dad22f0 zh: 8894cf2878fe90db @@ -1893,11 +1917,11 @@ en: ba73fb10b7e2ffc9 zh: 8d5c97e63e5f16da /session-persistence-event-catalog/resolved-persistence-types/type-deliverables-presented: - en: ec7e9f13864db847 - zh: 694e948cd52387ea + en: d69ad6b38df667d3 + zh: 72fb317a507a0b3b /session-persistence-event-catalog/resolved-persistence-types/type-developer-message: - en: 73514ab7122858e8 - zh: 335ebf8ba5454d7e + en: 1d16681802c679ce + zh: 01c03831212dd260 /session-persistence-event-catalog/resolved-persistence-types/type-feedback-message-delete: en: 204561e2311db97e zh: 77264396bd5e25c4 @@ -1962,20 +1986,20 @@ en: acf7e21e4d21ebff zh: fb83a853634d204a /session-persistence-event-catalog/resolved-persistence-types/type-session-title: - en: 8dd08982ac65646b - zh: 3f22850e3160ed36 + en: 203f5661ca1850b1 + zh: b3d9ec1019188df4 /session-persistence-event-catalog/resolved-persistence-types/type-session-title-llm-request: - en: 48c9d78076942c91 - zh: 6cafb7732d3a1258 + en: 246920f06c3a2175 + zh: 56deee3b86499174 /session-persistence-event-catalog/resolved-persistence-types/type-step-end: en: 7fc233c4a569dae2 zh: bc2b3fed4a397c00 /session-persistence-event-catalog/resolved-persistence-types/type-step-start: - en: 55ec9a3f52280221 - zh: bf2afb135514eb45 + en: bb27281e17bb5e4f + zh: b53e3fc8d916118b /session-persistence-event-catalog/resolved-persistence-types/type-subagent-catalog: - en: 85475a5c4198d213 - zh: 16a71daeb92288db + en: 0f697ddcde1625dd + zh: 0dd87d2e108c5312 /session-persistence-event-catalog/resolved-persistence-types/type-subagent-descriptor: en: 2adab4361a5b558d zh: 7f2a8da669f9e117 @@ -2025,11 +2049,11 @@ en: 939c100f8a590003 zh: b372827fffc17b1c /session-persistence-event-catalog/resolved-persistence-types/type-tool-call: - en: 2dc168f9f2bd25d6 - zh: d75bd24319468403 + en: fa99a04f82d0f7d0 + zh: 0f900151f73276e7 /session-persistence-event-catalog/resolved-persistence-types/type-tool-ptc-dispatch: - en: 4c950143ed9318b0 - zh: 2a0cfcaf190a01a9 + en: e598ac0ecca97709 + zh: ab07e2bf9235d135 /session-persistence-event-catalog/resolved-persistence-types/type-tool-ptc-dispatch-start: en: 11e3a2031948dd55 zh: a3666d1a2f08bb8c @@ -2043,17 +2067,20 @@ en: 1b045de96172be4b zh: 8a15c6f35b576b37 /session-persistence-event-catalog/resolved-persistence-types/type-usage: - en: 5e70d314feb1b86b - zh: b98f69d4d8442c14 + en: fa542ccb91698e56 + zh: 7b725c813d69588d /session-persistence-event-catalog/resolved-persistence-types/type-user-message: - en: 4f793420e83a748e - zh: 70a4cb82e27965dd + en: fb49c375aec5952c + zh: 19b80920c1068b86 /session-persistence-event-catalog/resolved-persistence-types/type-web-deepseek-search-llm-request: en: f109997e295af37d zh: b2dcfd599f73b61c /session-persistence-event-catalog/resolved-persistence-types/type-web-search-20250305: - en: a5ffa4a988ead0df - zh: fb8aa9a4fb0a264b + en: e0071555851c7cee + zh: 1f368baf78a7d689 +/session-persistence-event-catalog/resolved-persistence-types/type-working-directory-change: + en: 2fcb09b83898ee18 + zh: 44c7f987f0bf3588 /session-persistence-event-catalog/resolved-persistence-types/type-workspace-changes: en: 411ecd58c4405c8a zh: e53709d457a1f118 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 989987d5df..e20df04e5d 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -3,7 +3,9 @@ # Session Persistence Event Catalog -Every repository-declared durable Session event appears here with its source declaration and resolved types. The catalog covers the logical and physical headers, event envelopes, and every plugin declaration merge. See [Session](subsystems/session.md) for replay and [persistence](subsystems/persistence.md) for storage. +Every repository-declared durable Session event appears here with its source declaration and resolved types. The catalog covers the logical and physical headers, event envelopes, and every SessionEventMap declaration merge. See [Session](subsystems/session.md) for replay and [persistence](subsystems/persistence.md) for storage. + +Experimental records appear in the separate [experimental plugin record catalog](experimental-persistence-catalog.md). Run `pnpm run gen-persistence-catalog` to regenerate both catalog languages, their pairing record, the known-event module, and the machine schema inventory. `pnpm run verify-persistence-catalog` checks all generated files. Declaration fences preserve source JSDoc and type references; resolved definitions expose their transitive structure. @@ -21,7 +23,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`HeaderLine`](#persistence-type-sha256-18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f) | | `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`union (3 variants)`](#persistence-type-sha256-5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc) | | `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`{ type: "agent-preset/selected" }`](#persistence-type-sha256-a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b) | -| `event:agent/inbox/spliced` | event | `6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178) | +| `event:agent/inbox/spliced` | event | `9a36727716d0e73f681f3b725e7f28f89417c68174c8af0ff2b1ef52b08326a0` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-9a36727716d0e73f681f3b725e7f28f89417c68174c8af0ff2b1ef52b08326a0) | | `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`{ type: "approval/asked" }`](#persistence-type-sha256-3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1) | | `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`{ type: "approval/decided" }`](#persistence-type-sha256-bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3) | | `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`{ type: "approval/policy" }`](#persistence-type-sha256-26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78) | @@ -34,7 +36,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`{ type: "compaction/start" }`](#persistence-type-sha256-db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965) | | `event:compaction/summary` | event | `e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602` | [`{ type: "compaction/summary" }`](#persistence-type-sha256-e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602) | | `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`{ type: "deliverables/presented" }`](#persistence-type-sha256-13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176) | -| `event:developer/message` | event | `186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467` | [`{ type: "developer/message" }`](#persistence-type-sha256-186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467) | +| `event:developer/message` | event | `ba5b10ea77d68f4a29439a9bd2a14f6a560acf55731749d57237757cc72fbc06` | [`{ type: "developer/message" }`](#persistence-type-sha256-ba5b10ea77d68f4a29439a9bd2a14f6a560acf55731749d57237757cc72fbc06) | | `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`{ type: "feedback/message-delete" }`](#persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796) | | `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`{ type: "feedback/message-put" }`](#persistence-type-sha256-b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf) | | `event:feedback/record` | event | `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` | [`{ type: "feedback/record" }`](#persistence-type-sha256-b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8) | @@ -54,10 +56,10 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) | | `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) | | `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) | -| `event:session/title-llm-request` | event | `ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46) | +| `event:session/title-llm-request` | event | `b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) | -| `event:subagent/catalog` | event | `3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604) | +| `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) | | `event:subagent/descriptor` | event | `b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5` | [`{ type: "subagent/descriptor" }`](#persistence-type-sha256-b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5) | | `event:subagent/model-selection-policy` | event | `a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc` | [`{ type: "subagent/model-selection-policy" }`](#persistence-type-sha256-a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc) | | `event:system/message` | event | `69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1` | [`{ type: "system/message" }`](#persistence-type-sha256-69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1) | @@ -71,13 +73,14 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:tool-workflow/run-end` | event | `42e0916e0dda5f6d1e7bb05d8514717147c036a79c9f36085683469516c1fd3f` | [`{ type: "tool-workflow/run-end" }`](#persistence-type-sha256-42e0916e0dda5f6d1e7bb05d8514717147c036a79c9f36085683469516c1fd3f) | | `event:tool-workflow/run-start` | event | `c1f9e0405de6d18cabb9ee70782a027f9bbdc57e5abec9dcccdd56119e2e9058` | [`{ type: "tool-workflow/run-start" }`](#persistence-type-sha256-c1f9e0405de6d18cabb9ee70782a027f9bbdc57e5abec9dcccdd56119e2e9058) | | `event:tool/call` | event | `3b1be838223869fe0a08210db85bf773796ed3f2373ac16555dff227cade0c48` | [`{ type: "tool/call" }`](#persistence-type-sha256-3b1be838223869fe0a08210db85bf773796ed3f2373ac16555dff227cade0c48) | -| `event:tool/ptc-dispatch` | event | `100f6dca1468538239522cde3533e5bd721d0f1a7b50bea8b0eb533ea6c96163` | [`{ type: "tool/ptc-dispatch" }`](#persistence-type-sha256-100f6dca1468538239522cde3533e5bd721d0f1a7b50bea8b0eb533ea6c96163) | +| `event:tool/ptc-dispatch` | event | `d66e11dddf5dfa3c71547e2cbd7b0608824cd8d6243fc16f532017ff28e8fba5` | [`{ type: "tool/ptc-dispatch" }`](#persistence-type-sha256-d66e11dddf5dfa3c71547e2cbd7b0608824cd8d6243fc16f532017ff28e8fba5) | | `event:tool/ptc-dispatch-start` | event | `ec38b5949af8eacaf00df002f4acbe344f934f8a061e9cdc65a52a48e5f6dd93` | [`{ type: "tool/ptc-dispatch-start" }`](#persistence-type-sha256-ec38b5949af8eacaf00df002f4acbe344f934f8a061e9cdc65a52a48e5f6dd93) | | `event:tool/result` | event | `7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397` | [`{ type: "tool/result" }`](#persistence-type-sha256-7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397) | | `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) | | `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) | -| `event:user/message` | event | `b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411` | [`{ type: "user/message" }`](#persistence-type-sha256-b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411) | +| `event:user/message` | event | `2ace3b8059b82b6d9837fed518ca97a99eb37c4482df7aad0e71c85489428ade` | [`{ type: "user/message" }`](#persistence-type-sha256-2ace3b8059b82b6d9837fed518ca97a99eb37c4482df7aad0e71c85489428ade) | | `event:web/deepseek-search-llm-request` | event | `cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331) | +| `event:working-directory/change` | event | `124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3` | [`{ type: "working-directory/change" }`](#persistence-type-sha256-124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3) | | `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) | ## Event envelope @@ -580,7 +583,7 @@ Source: [`packages/goal/goal/src/domain.ts:66`](../packages/goal/goal/src/domain } ``` -Source: [`packages/hooks/hook-protocol/src/types.ts:19`](../packages/hooks/hook-protocol/src/types.ts) +Source: [`packages/experimental/hook-protocol/src/types.ts:19`](../packages/experimental/hook-protocol/src/types.ts) @@ -603,7 +606,7 @@ Source: [`packages/hooks/hook-protocol/src/types.ts:19`](../packages/hooks/hook- } ``` -Source: [`packages/hooks/hook-protocol/src/types.ts:31`](../packages/hooks/hook-protocol/src/types.ts) +Source: [`packages/experimental/hook-protocol/src/types.ts:31`](../packages/experimental/hook-protocol/src/types.ts) ### `image/*` @@ -840,7 +843,7 @@ Source: [`packages/session/session-title/src/index.ts:77`](../packages/session/s Types: [SessionTitleLlmRequestEventData](subsystems/session-title.md) -Source: [`packages/session/session-title-llm/src/index.ts:52`](../packages/session/session-title-llm/src/index.ts) +Source: [`packages/session/session-title-llm/src/index.ts:54`](../packages/session/session-title-llm/src/index.ts) ### `session-log-deepseek/*` @@ -900,7 +903,7 @@ Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/ 'subagent/catalog': SubagentCatalogEvent ``` -Source: [`packages/subagent/subagent/src/catalog.ts:43`](../packages/subagent/subagent/src/catalog.ts) +Source: [`packages/subagent/subagent/src/catalog.ts:44`](../packages/subagent/subagent/src/catalog.ts) @@ -909,8 +912,8 @@ Source: [`packages/subagent/subagent/src/catalog.ts:43`](../packages/subagent/su ```ts persistence-catalog /** * Durable identity and lifecycle mode of a session-backed subagent child, - * appended once by the establishing provider inside the child's initial - * turn, before its first request. Continuable records also carry their + * appended once inside the child's initial turn, before its first + * request. Continuable records also carry their * resumable composition. Log-only: it carries no `surfaceOp`, never enters * model history, and survives compaction. */ @@ -1065,8 +1068,8 @@ Source: [`packages/core/session/src/types.ts:361`](../packages/core/session/src/ * `tool/ptc-dispatch-start` with the same `subCallId`), the tool `name` * with the same JSON-normalized `arguments`, and the sub-call's complete * durable outcome in `tool/result`'s own vocabulary (`content` + `isError` - * + optional structured `error`), so UIs and SDKs render a sub-call through - * the exact path used for a native call. Every started sub-call settles + * + optional presentation `meta` and structured `error`), so UIs and SDKs + * render a sub-call through the exact path used for a native call. Every started sub-call settles * with exactly one of these (abort included: the aborted pipeline result * is an `isError` outcome). * Log-only: `deriveMessages()` ignores it, so sub-calls never re-enter @@ -1078,7 +1081,7 @@ Source: [`packages/core/session/src/types.ts:361`](../packages/core/session/src/ 'tool/ptc-dispatch': PtcDispatchEventData ``` -Source: [`packages/core/tools/src/types.ts:58`](../packages/core/tools/src/types.ts) +Source: [`packages/core/tools/src/types.ts:61`](../packages/core/tools/src/types.ts) @@ -1101,7 +1104,7 @@ Source: [`packages/core/tools/src/types.ts:58`](../packages/core/tools/src/types 'tool/ptc-dispatch-start': PtcDispatchStartEventData ``` -Source: [`packages/core/tools/src/types.ts:42`](../packages/core/tools/src/types.ts) +Source: [`packages/core/tools/src/types.ts:45`](../packages/core/tools/src/types.ts) @@ -1264,6 +1267,22 @@ Source: [`packages/core/session/src/types.ts:309`](../packages/core/session/src/ Source: [`packages/web/web-search-deepseek/src/provider.ts:82`](../packages/web/web-search-deepseek/src/provider.ts) +### `working-directory/*` + + + +#### `working-directory/change` — log-only + +```ts persistence-catalog +/** Effective working directory; the immutable Session header retains the original project. */ +'working-directory/change': { + /** Absolute directory used by subsequent directory-based operations. */ + cwd: string +} +``` + +Source: [`packages/session/working-directory/src/index.ts:36`](../packages/session/working-directory/src/index.ts) + ### `workspace/*` @@ -1733,6 +1752,14 @@ SHA-256: `e42b0f40b5d0b628c6e1c44a8fb9eda0780037a81959e1b93d2dfb47b46b0e87` `"every"` + + +### `"external"` + +SHA-256: `fc351843b8ecbd05d739c4ca4fcd4bc7cfad6c0beacbbd48389959bb1bf06cf3` + +`"external"` + ### `"failed"` @@ -2875,6 +2902,22 @@ SHA-256: `db6c5246911d47a7969f6b4cb47c7d0dbbde75c402710b22ace3672fa97bebe0` `"webhook"` + + +### `"working-directory"` + +SHA-256: `7b1ce60193fa8b69292e5843858d9cee7dc565dcb40d33c136e5d7e8b8d4296a` + +`"working-directory"` + + + +### `"working-directory/change"` + +SHA-256: `d4c799e3d51cae1040362ba4968efc484739e14416c9c1df20c2f108b4ef2eb5` + +`"working-directory/change"` + ### `"workspace-write"` @@ -2933,7 +2976,7 @@ SHA-256: `af6e173f7be819bcd0a5c8d77918649c10ce761d8a184ff472f2e01ce64eb2e0` SHA-256: `83da72c6857601c75da11212ef847f57d457b9c99f0a82246dcdff3e26700239` -Sources: [`packages/context/agent-instructions/src/render.ts:47`](../packages/context/agent-instructions/src/render.ts) +Sources: [`packages/context/agent-instructions/src/render.ts:53`](../packages/context/agent-instructions/src/render.ts) | Property | Presence | Type | |---|---|---| @@ -2960,7 +3003,7 @@ Array of [`AgentInstructionChange`](#persistence-type-sha256-83da72c6857601c75da SHA-256: `6f22a9d02b9a1f1157f3b0e7bdccd7e7b12b085c4efdb709dfc74a465cd4e590` -Sources: [`packages/context/agent-instructions/src/state.ts:37`](../packages/context/agent-instructions/src/state.ts) +Sources: [`packages/context/agent-instructions/src/state.ts:40`](../packages/context/agent-instructions/src/state.ts) | Property | Presence | Type | |---|---|---| @@ -2980,7 +3023,7 @@ Sources: [`packages/context/agent-instructions/src/state.ts:37`](../packages/con SHA-256: `4a9bef9f27a942520cc2279ca7dd3082556e4bff543f66b5c0333e1bbed1cd86` -Sources: [`packages/subagent/subagent/src/continuation-messages.ts:15`](../packages/subagent/subagent/src/continuation-messages.ts) +Sources: [`packages/subagent/subagent/src/continuation-messages.ts:14`](../packages/subagent/subagent/src/continuation-messages.ts) | Property | Presence | Type | |---|---|---| @@ -3223,7 +3266,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web | `body` | required | [`{ max_tokens, messages, model, tools }`](#persistence-type-sha256-930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731) | | `endpoint` | required | `string` | - + @@ -3231,7 +3274,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web ### `DeveloperMessage` -SHA-256: `1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200` +SHA-256: `af99c95d46bed802870f1ebefe0ea11f92a03cb37b8e0cbe3f94b7aaaf509b08` Sources: [`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message.ts) @@ -3240,11 +3283,11 @@ Sources: [`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message | `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | required | `string` | | `role` | required | `"developer"` | -| `source` | required | [`MessageSource`](#persistence-type-sha256-26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116) | +| `source` | required | [`MessageSource`](#persistence-type-sha256-1fa156c85450ca31063d0e0d2853028e6dac9687f2d3191fa811648210118154) | Source compatibility: `source` — `session-source-attribution` v1; `session.developer-message.source`; `kind`; `preserve`. -Attribution-only additions: `tmux-context`, `user-question-reply`. +Attribution-only additions: `tmux-context`, `user-question-reply`, `working-directory`. @@ -3539,13 +3582,13 @@ Sources: [`packages/session/session-persistence-jsonl/src/format.ts:83`](../pack - + ### `HookDialect` SHA-256: `a6b41f5bede8094d7bf635be5d69b1bd7a25e006ca9cf9415bceb9322f1a7feb` -Sources: [`packages/hooks/hook-protocol/src/types.ts:48`](../packages/hooks/hook-protocol/src/types.ts) +Sources: [`packages/experimental/hook-protocol/src/types.ts:48`](../packages/experimental/hook-protocol/src/types.ts) One of: @@ -3772,7 +3815,7 @@ Sources: [`packages/schedule/schedule/src/types.ts:185`](../packages/schedule/sc SHA-256: `67d5f73ceae82f5265258142ad6cf172de2c1fef93ba551b7f0454e0a93bffe0` -Sources: [`packages/llm/llm/src/call-config.ts:23`](../packages/llm/llm/src/call-config.ts) +Sources: [`packages/llm/llm/src/call-config.ts:31`](../packages/llm/llm/src/call-config.ts) | Property | Presence | Type | |---|---|---| @@ -3793,7 +3836,7 @@ Sources: [`packages/llm/llm/src/call-config.ts:23`](../packages/llm/llm/src/call SHA-256: `326a9bfda27dda5882fb3cbe6e715df41dae2298fdae3d66437457268a665693` -Sources: [`packages/llm/llm/src/call-config.ts:36`](../packages/llm/llm/src/call-config.ts) +Sources: [`packages/llm/llm/src/call-config.ts:54`](../packages/llm/llm/src/call-config.ts) | Property | Presence | Type | |---|---|---| @@ -3857,7 +3900,7 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr | `step` | required | `number` | | `turn` | required | `number` | - + @@ -3865,15 +3908,15 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr ### `Message` -SHA-256: `cb7b863ddb7b3eab5f18f216602feba25f47b456e73e4e72b6bcc259875c6dab` +SHA-256: `d901c4f5e856591c3217af983caa15bba568d580700f0958bab56b88004eced9` Sources: [`packages/llm/llm/src/message.ts:196`](../packages/llm/llm/src/message.ts) One of: - [`ToolResultMessage`](#persistence-type-sha256-4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731) -- [`DeveloperMessage`](#persistence-type-sha256-1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200) -- [`UserMessage`](#persistence-type-sha256-cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd) +- [`DeveloperMessage`](#persistence-type-sha256-af99c95d46bed802870f1ebefe0ea11f92a03cb37b8e0cbe3f94b7aaaf509b08) +- [`UserMessage`](#persistence-type-sha256-89b2edff547be1ecfcd78ef18aeeb3cf507fcbf7d6093ec04fb111ef4c8e8a97) - [`SystemMessage`](#persistence-type-sha256-09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363) - [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be) @@ -3950,7 +3993,7 @@ One of: - `"negative"` - `"positive"` - + @@ -3958,7 +4001,7 @@ One of: ### `MessageSource` -SHA-256: `26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116` +SHA-256: `1fa156c85450ca31063d0e0d2853028e6dac9687f2d3191fa811648210118154` Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message.ts) @@ -4074,14 +4117,15 @@ Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message | `user-approval` | not declared | none | [`{ kind: "user-approval" }`](#persistence-type-sha256-6292bcbedaa97feaec5b48a03bb8144b581b93309d9d6c905f26da7a6a7a0a4f) | | `user-question-reply` | not declared | `callId`: `string`; `outcome`: `"answered"` | [`{ kind: "user-question-reply" }`](#persistence-type-sha256-c55827fba7018198772c50e37cb7d3f60a3d8bb6e94b07de72dee0d9f8968005) | | `webhook` | `"notice"` | `deliveryId`: `string`; `provider`: `string`; `ruleId`: `string`; `source`: `string`; `summary`: `string` | [`{ kind: "webhook", form: "notice" }`](#persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94) | +| `working-directory` | not declared | none | [`{ kind: "working-directory" }`](#persistence-type-sha256-2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5) | - + ### `Message[]` -SHA-256: `3f42f45cbb8a06586d247e5f3763dc02c8d31a145a9a3b82fabcd101e7e224cc` +SHA-256: `4be8fd28fefa5534675ea9aabd0a24efc2838b989bbc74da20a1efbd0daede8c` -Array of [`Message`](#persistence-type-sha256-cb7b863ddb7b3eab5f18f216602feba25f47b456e73e4e72b6bcc259875c6dab). +Array of [`Message`](#persistence-type-sha256-d901c4f5e856591c3217af983caa15bba568d580700f0958bab56b88004eced9). @@ -4199,7 +4243,7 @@ SHA-256: `1d9cb3caa96100b18b1911fa3ff8c751ef6992971a03c5f4d9aefc1a0004a345` Array of [`PresentedFile`](#persistence-type-sha256-b8fc636a2121df9d8423c242e03c9d23b7ab7c6fdce00e45746932c60b730f97). - + @@ -4207,9 +4251,9 @@ Array of [`PresentedFile`](#persistence-type-sha256-b8fc636a2121df9d8423c242e03c ### `PtcDispatchEventData` -SHA-256: `0ee52de154f8955c7abc82cffebc57452dcc8c44dccbdb6ad16c71a11305ead8` +SHA-256: `cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6` -Sources: [`packages/core/tools/src/types.ts:20`](../packages/core/tools/src/types.ts) +Sources: [`packages/core/tools/src/types.ts:21`](../packages/core/tools/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -4217,6 +4261,7 @@ Sources: [`packages/core/tools/src/types.ts:20`](../packages/core/tools/src/type | `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `error` | optional | [`{ code, name, reason? }`](#persistence-type-sha256-a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735) | | `isError` | required | `boolean` | +| `meta` | optional | [`JsonValue`](#persistence-type-sha256-c592ce75aab73fcab19c1d7845684c72cf402b78d2e1f2833a58ecf9f3598ed6) | | `name` | required | `string` | | `parentCallId` | required | `string` | | `rootCallId` | required | `string` | @@ -4232,7 +4277,7 @@ Sources: [`packages/core/tools/src/types.ts:20`](../packages/core/tools/src/type SHA-256: `2fe5026daf14bd4ff2b7bfb7cf9791c302d9cbba06802178b6dfec00b892698f` -Sources: [`packages/core/tools/src/types.ts:11`](../packages/core/tools/src/types.ts) +Sources: [`packages/core/tools/src/types.ts:12`](../packages/core/tools/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -4448,7 +4493,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/ | `source` | required | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) | | `title` | required | `string` | - + @@ -4456,7 +4501,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/ ### `SessionTitleLlmRequestEventData` -SHA-256: `fe3cdc6f9050584cea4a6d6998c31334bff7899f5ff1095082a44fdf3b47e996` +SHA-256: `045926475a46f625f84ae2be292b384817059f16927c69a9adab93717923ac19` Sources: [`packages/session/session-title-llm/src/index.ts:34`](../packages/session/session-title-llm/src/index.ts) @@ -4464,7 +4509,8 @@ Sources: [`packages/session/session-title-llm/src/index.ts:34`](../packages/sess |---|---|---| | `maxTokens` | required | `number` | | `messageSeqs` | required | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | -| `messages` | required | [`Message[]`](#persistence-type-sha256-3f42f45cbb8a06586d247e5f3763dc02c8d31a145a9a3b82fabcd101e7e224cc) | +| `messages` | required | [`Message[]`](#persistence-type-sha256-4be8fd28fefa5534675ea9aabd0a24efc2838b989bbc74da20a1efbd0daede8c) | +| `reasoningEffort` | optional | `string` | | `route` | required | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) | | `system` | required | `string` | | `titleProvider` | required | `string` | @@ -4526,7 +4572,7 @@ Sources: [`packages/session/session-title/src/types.ts:28`](../packages/session/ SHA-256: `fc2ae6962f9f8801c2b1e414668e1e17cbb7544c4f39381198977a38bd10c093` -Sources: [`packages/skill/tool-skill/src/index.ts:34`](../packages/skill/tool-skill/src/index.ts) +Sources: [`packages/skill/tool-skill/src/index.ts:35`](../packages/skill/tool-skill/src/index.ts) | Property | Presence | Type | |---|---|---| @@ -4575,7 +4621,7 @@ One of: - [`{ type: "finish" }`](#persistence-type-sha256-a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1) - [`{ type: "usage" }`](#persistence-type-sha256-f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515) - + @@ -4583,17 +4629,18 @@ One of: ### `SubagentCatalogEvent` -SHA-256: `1de64ea9a0849ce631b7e4e1255ae773fabd94a4a7c8d27e716f8012302e12c0` +SHA-256: `f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87` Sources: [`packages/subagent/subagent/src/catalog.ts:28`](../packages/subagent/subagent/src/catalog.ts) One of: -- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd) - [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-e2ad8f5bcd813f268ffef1b10a12d7ddd2b8ced8b9b6d0275b3f6b7ed094a187) +- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09) - [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-5d542ac7bdf13310c46e1a884dec9fb18ad8c6e153cff207e639cd7bf9d7e694) - [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-b6896938660a1dfe4ddd100fb3164d9f8fa57feeabfe37441a80106c9942697a) - [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-a0f25b5aeeebf2bf1542ce67726b095e3b139a39c71861d57e827f12dea1f499) +- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd) @@ -4622,7 +4669,7 @@ One of: SHA-256: `4e97c3d85c0fc817ee58873c38c25a2474abaaa0af4839311b50b68db3b8cf1a` -Sources: [`packages/subagent/subagent/src/continuation-messages.ts:30`](../packages/subagent/subagent/src/continuation-messages.ts) +Sources: [`packages/subagent/subagent/src/continuation-messages.ts:29`](../packages/subagent/subagent/src/continuation-messages.ts) | Property | Presence | Type | |---|---|---| @@ -4949,7 +4996,7 @@ Sources: [`packages/llm/llm/src/types.ts:127`](../packages/llm/llm/src/types.ts) SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f` -Sources: [`packages/core/tools/src/index.ts:700`](../packages/core/tools/src/index.ts) +Sources: [`packages/core/tools/src/index.ts:701`](../packages/core/tools/src/index.ts) | Property | Presence | Type | |---|---|---| @@ -5118,7 +5165,7 @@ Sources: [`packages/core/session/src/types.ts:232`](../packages/core/session/src | `interrupted` | not declared | none | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) | | `max-tokens` | not declared | none | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) | - + @@ -5126,7 +5173,7 @@ Sources: [`packages/core/session/src/types.ts:232`](../packages/core/session/src ### `UserMessage` -SHA-256: `cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd` +SHA-256: `89b2edff547be1ecfcd78ef18aeeb3cf507fcbf7d6093ec04fb111ef4c8e8a97` Sources: [`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message.ts) @@ -5135,19 +5182,19 @@ Sources: [`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message | `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | required | `string` | | `role` | required | `"user"` | -| `source` | required | [`MessageSource`](#persistence-type-sha256-26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116) | +| `source` | required | [`MessageSource`](#persistence-type-sha256-1fa156c85450ca31063d0e0d2853028e6dac9687f2d3191fa811648210118154) | Source compatibility: `source` — `session-source-attribution` v1; `session.user-message.source`; `kind`; `preserve`. -Attribution-only additions: `tmux-context`, `user-question-reply`. +Attribution-only additions: `tmux-context`, `user-question-reply`, `working-directory`. - + ### `UserMessage[]` -SHA-256: `5a92f12db2059c282cdea45b6845c65e6f4a95dd3d649aeddbde6f9ae03c561f` +SHA-256: `2fef90def36aa758ca096c5f6ad0e00dbb2101943e19ca54b05618801974894a` -Array of [`UserMessage`](#persistence-type-sha256-cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd). +Array of [`UserMessage`](#persistence-type-sha256-89b2edff547be1ecfcd78ef18aeeb3cf507fcbf7d6093ec04fb111ef4c8e8a97). @@ -5269,6 +5316,12 @@ Array of `number`. + + + + + + @@ -5293,12 +5346,6 @@ Array of `number`. - - - - - - @@ -5335,7 +5382,7 @@ Array of `number`. SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b` -Sources: [`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) +Sources: [`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) `string` @@ -5699,13 +5746,27 @@ SHA-256: `b6896938660a1dfe4ddd100fb3164d9f8fa57feeabfe37441a80106c9942697a` | `mode` | required | `"one-shot"` | | `version` | required | `1` | + + +### `{ childCreatedAt, childId, label?, mode, … }` + +SHA-256: `cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09` + +| Property | Presence | Type | +|---|---|---| +| `childCreatedAt` | required | `number` | +| `childId` | required | `string` | +| `label` | optional | `string` | +| `mode` | required | `"external"` | +| `version` | required | `2` | + ### `{ code, name, reason? }` SHA-256: `a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735` -Sources: [`packages/core/session/src/types.ts:383`](../packages/core/session/src/types.ts) · [`packages/core/tools/src/types.ts:24`](../packages/core/tools/src/types.ts) +Sources: [`packages/core/session/src/types.ts:383`](../packages/core/session/src/types.ts) · [`packages/core/tools/src/types.ts:27`](../packages/core/tools/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5798,13 +5859,25 @@ Sources: [`packages/compaction/compaction/src/types.ts:24`](../packages/compacti | `sourceCommandId` | optional | `string` | | `turn` | required | [`OptionalSessionSeq`](#persistence-type-sha256-3bd652ebfa8726b3ce3937a4f1bd2759e02f86b3a08a20f9e41a8513656fbc5f) | + + +### `{ cwd }` + +SHA-256: `da60ed58a8fa23d7aa4920ab5c1f4e3d28109cb0797ef9e5c8b125721b847582` + +Sources: [`packages/session/working-directory/src/index.ts:36`](../packages/session/working-directory/src/index.ts) + +| Property | Presence | Type | +|---|---|---| +| `cwd` | required | `string` | + ### `{ decision, durationMs, exitCode?, handlerId, … }` SHA-256: `8ad45ad053b4d89f8865c17cc9ff3b8748e86045f3f0fb816269a34af2619141` -Sources: [`packages/hooks/hook-protocol/src/types.ts:31`](../packages/hooks/hook-protocol/src/types.ts) +Sources: [`packages/experimental/hook-protocol/src/types.ts:31`](../packages/experimental/hook-protocol/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5863,7 +5936,7 @@ Sources: [`packages/llm/llm-retry/src/types.ts:30`](../packages/llm/llm-retry/sr SHA-256: `83079c8a3f733ac3fa603eefa0fbb4737ca4d8b69e7e2e7298b60f9ac4098693` -Sources: [`packages/skill/tool-skill/src/index.ts:40`](../packages/skill/tool-skill/src/index.ts) +Sources: [`packages/skill/tool-skill/src/index.ts:41`](../packages/skill/tool-skill/src/index.ts) | Property | Presence | Type | |---|---|---| @@ -5884,7 +5957,7 @@ Array of [`{ description, name }`](#persistence-type-sha256-83079c8a3f733ac3fa60 SHA-256: `119abe2b9c0b2f46ecb31d82b9acba4d4b30e049f655e2475e8b4ae12d5a9e25` -Sources: [`packages/hooks/hook-protocol/src/types.ts:19`](../packages/hooks/hook-protocol/src/types.ts) +Sources: [`packages/experimental/hook-protocol/src/types.ts:19`](../packages/experimental/hook-protocol/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5990,18 +6063,18 @@ Sources: [`packages/core/session/src/types.ts:390`](../packages/core/session/src | `reason` | required | [`RequestHeaderReason`](#persistence-type-sha256-530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6) | | `startsSeries` | optional | `true` | - + ### `{ headerSeq?, message, step, turn }` -SHA-256: `a5d80b5a2f3a6bbaf8f0334126632b78f94d8346122c68e8122ea89906d63d26` +SHA-256: `5b55817a403569fbb2d379496df2e3f3f71107f6c577a218028ce187c60b61e6` Sources: [`packages/core/session/src/types.ts:311`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| | `headerSeq` | optional | `number` | -| `message` | required | [`DeveloperMessage`](#persistence-type-sha256-1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200) | +| `message` | required | [`DeveloperMessage`](#persistence-type-sha256-af99c95d46bed802870f1ebefe0ea11f92a03cb37b8e0cbe3f94b7aaaf509b08) | | `step` | required | `number` | | `turn` | required | `number` | @@ -6085,17 +6158,17 @@ Sources: [`packages/core/session/src/types.ts:427`](../packages/core/session/src |---|---|---| | `inherited` | optional | `true` | - + ### `{ inserted, outcome?, removedCount?, start, … }` -SHA-256: `149c5a77529cb0dc06f5ceb6273069b7606174d1e7893b5f9b1071c15306b846` +SHA-256: `83a665476a9ee9af05cb1fdded12dd647d1b97b491200d7f12ef1af7cdac4428` Sources: [`packages/core/agent/src/types.ts:96`](../packages/core/agent/src/types.ts) | Property | Presence | Type | |---|---|---| -| `inserted` | required | [`UserMessage[]`](#persistence-type-sha256-5a92f12db2059c282cdea45b6845c65e6f4a95dd3d649aeddbde6f9ae03c561f) | +| `inserted` | required | [`UserMessage[]`](#persistence-type-sha256-2fef90def36aa758ca096c5f6ad0e00dbb2101943e19ca54b05618801974894a) | | `outcome` | optional | `"canceled"` | | `removedCount` | optional | `number` | | `start` | required | `number` | @@ -7256,7 +7329,7 @@ SHA-256: `c5228b7a55498d84f529591744a0ab26e0b93ade18a70a26db1377a1ecfa6b49` SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543` -Sources: [`packages/core/tools/src/index.ts:34`](../packages/core/tools/src/index.ts) +Sources: [`packages/core/tools/src/index.ts:35`](../packages/core/tools/src/index.ts) | Property | Presence | Type | |---|---|---| @@ -7374,7 +7447,7 @@ Sources: [`packages/interaction/user-questions/src/types.ts:117`](../packages/in SHA-256: `9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94` -Sources: [`packages/webhook/webhook/src/types.ts:74`](../packages/webhook/webhook/src/types.ts) +Sources: [`packages/experimental/webhook/src/types.ts:74`](../packages/experimental/webhook/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7386,6 +7459,18 @@ Sources: [`packages/webhook/webhook/src/types.ts:74`](../packages/webhook/webhoo | `source` | required | `string` | | `summary` | required | `string` | + + +### `{ kind: "working-directory" }` + +SHA-256: `2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5` + +Sources: [`packages/session/working-directory/src/index.ts:29`](../packages/session/working-directory/src/index.ts) + +| Property | Presence | Type | +|---|---|---| +| `kind` | required | `"working-directory"` | + ### `{ max_tokens, messages, model, tools }` @@ -7643,17 +7728,17 @@ SHA-256: `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | `time` | required | `number` | | `type` | required | `"agent-preset/selected"` | - + ### `{ type: "agent/inbox/spliced" }` -SHA-256: `6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178` +SHA-256: `9a36727716d0e73f681f3b725e7f28f89417c68174c8af0ff2b1ef52b08326a0` | Property | Presence | Type | |---|---|---| -| `data` | required | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-149c5a77529cb0dc06f5ceb6273069b7606174d1e7893b5f9b1071c15306b846) | +| `data` | required | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-83a665476a9ee9af05cb1fdded12dd647d1b97b491200d7f12ef1af7cdac4428) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | @@ -7894,17 +7979,17 @@ SHA-256: `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | `time` | required | `number` | | `type` | required | `"deliverables/presented"` | - + ### `{ type: "developer/message" }` -SHA-256: `186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467` +SHA-256: `ba5b10ea77d68f4a29439a9bd2a14f6a560acf55731749d57237757cc72fbc06` | Property | Presence | Type | |---|---|---| -| `data` | required | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-a5d80b5a2f3a6bbaf8f0334126632b78f94d8346122c68e8122ea89906d63d26) | +| `data` | required | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-5b55817a403569fbb2d379496df2e3f3f71107f6c577a218028ce187c60b61e6) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `sourceEventSeqs` | optional | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | @@ -8260,17 +8345,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | `time` | required | `number` | | `type` | required | `"session/title"` | - + ### `{ type: "session/title-llm-request" }` -SHA-256: `ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46` +SHA-256: `b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6` | Property | Presence | Type | |---|---|---| -| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-fe3cdc6f9050584cea4a6d6998c31334bff7899f5ff1095082a44fdf3b47e996) | +| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-045926475a46f625f84ae2be292b384817059f16927c69a9adab93717923ac19) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | @@ -8308,17 +8393,17 @@ SHA-256: `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | `time` | required | `number` | | `type` | required | `"step/start"` | - + ### `{ type: "subagent/catalog" }` -SHA-256: `3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604` +SHA-256: `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | Property | Presence | Type | |---|---|---| -| `data` | required | [`SubagentCatalogEvent`](#persistence-type-sha256-1de64ea9a0849ce631b7e4e1255ae773fabd94a4a7c8d27e716f8012302e12c0) | +| `data` | required | [`SubagentCatalogEvent`](#persistence-type-sha256-f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | @@ -8598,17 +8683,17 @@ SHA-256: `3b1be838223869fe0a08210db85bf773796ed3f2373ac16555dff227cade0c48` | `time` | required | `number` | | `type` | required | `"tool/call"` | - + ### `{ type: "tool/ptc-dispatch" }` -SHA-256: `100f6dca1468538239522cde3533e5bd721d0f1a7b50bea8b0eb533ea6c96163` +SHA-256: `d66e11dddf5dfa3c71547e2cbd7b0608824cd8d6243fc16f532017ff28e8fba5` | Property | Presence | Type | |---|---|---| -| `data` | required | [`PtcDispatchEventData`](#persistence-type-sha256-0ee52de154f8955c7abc82cffebc57452dcc8c44dccbdb6ad16c71a11305ead8) | +| `data` | required | [`PtcDispatchEventData`](#persistence-type-sha256-cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | @@ -8693,17 +8778,17 @@ Sources: [`packages/llm/llm/src/types.ts:458`](../packages/llm/llm/src/types.ts) | `type` | required | `"usage"` | | `usage` | required | [`TokenUsage`](#persistence-type-sha256-fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394) | - + ### `{ type: "user/message" }` -SHA-256: `b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411` +SHA-256: `2ace3b8059b82b6d9837fed518ca97a99eb37c4482df7aad0e71c85489428ade` | Property | Presence | Type | |---|---|---| -| `data` | required | [`UserMessage`](#persistence-type-sha256-cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd) | +| `data` | required | [`UserMessage`](#persistence-type-sha256-89b2edff547be1ecfcd78ef18aeeb3cf507fcbf7d6093ec04fb111ef4c8e8a97) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `sourceEventSeqs` | optional | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | @@ -8741,6 +8826,22 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:71`](../packages/web | `name` | required | `"web_search"` | | `type` | required | `"web_search_20250305"` | + + + + +### `{ type: "working-directory/change" }` + +SHA-256: `124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3` + +| Property | Presence | Type | +|---|---|---| +| `data` | required | [`{ cwd }`](#persistence-type-sha256-da60ed58a8fa23d7aa4920ab5c1f4e3d28109cb0797ef9e5c8b125721b847582) | +| `ignorable` | optional | `true` | +| `seq` | required | `number` | +| `time` | required | `number` | +| `type` | required | `"working-directory/change"` | + diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 295160c5d0..4e8c425b73 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -5,7 +5,9 @@ [English](persistence-catalog.md) | 中文 -本目录列出仓库声明的每个持久化 Session 事件及其源码声明和解析类型,覆盖逻辑与物理 header、事件信封以及各插件的声明合并。回放规则参见 [Session](subsystems/session.zh.md),存储规则参见[持久化](subsystems/persistence.zh.md)。 +本目录列出仓库声明的每个持久化 Session 事件及其源码声明和解析类型,覆盖逻辑与物理 header、事件信封及所有 SessionEventMap 声明合并。回放规则参见 [Session](subsystems/session.zh.md),存储规则参见[持久化](subsystems/persistence.zh.md)。 + +实验性记录单独列在[实验性插件记录目录](experimental-persistence-catalog.zh.md)中。 运行 `pnpm run gen-persistence-catalog` 可重新生成目录的两种语言、配对记录、已知事件模块和机器 schema 目录。`pnpm run verify-persistence-catalog` 检查所有生成文件。声明围栏保留源码 JSDoc 和类型引用;解析后的定义展开其传递引用结构。 @@ -23,7 +25,7 @@ | `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`HeaderLine`](#persistence-type-sha256-18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f) | | `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`union (3 variants)`](#persistence-type-sha256-5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc) | | `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`{ type: "agent-preset/selected" }`](#persistence-type-sha256-a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b) | -| `event:agent/inbox/spliced` | event | `6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178) | +| `event:agent/inbox/spliced` | event | `9a36727716d0e73f681f3b725e7f28f89417c68174c8af0ff2b1ef52b08326a0` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-9a36727716d0e73f681f3b725e7f28f89417c68174c8af0ff2b1ef52b08326a0) | | `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`{ type: "approval/asked" }`](#persistence-type-sha256-3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1) | | `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`{ type: "approval/decided" }`](#persistence-type-sha256-bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3) | | `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`{ type: "approval/policy" }`](#persistence-type-sha256-26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78) | @@ -36,7 +38,7 @@ | `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`{ type: "compaction/start" }`](#persistence-type-sha256-db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965) | | `event:compaction/summary` | event | `e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602` | [`{ type: "compaction/summary" }`](#persistence-type-sha256-e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602) | | `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`{ type: "deliverables/presented" }`](#persistence-type-sha256-13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176) | -| `event:developer/message` | event | `186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467` | [`{ type: "developer/message" }`](#persistence-type-sha256-186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467) | +| `event:developer/message` | event | `ba5b10ea77d68f4a29439a9bd2a14f6a560acf55731749d57237757cc72fbc06` | [`{ type: "developer/message" }`](#persistence-type-sha256-ba5b10ea77d68f4a29439a9bd2a14f6a560acf55731749d57237757cc72fbc06) | | `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`{ type: "feedback/message-delete" }`](#persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796) | | `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`{ type: "feedback/message-put" }`](#persistence-type-sha256-b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf) | | `event:feedback/record` | event | `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` | [`{ type: "feedback/record" }`](#persistence-type-sha256-b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8) | @@ -56,10 +58,10 @@ | `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) | | `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) | | `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) | -| `event:session/title-llm-request` | event | `ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46) | +| `event:session/title-llm-request` | event | `b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) | -| `event:subagent/catalog` | event | `3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604) | +| `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) | | `event:subagent/descriptor` | event | `b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5` | [`{ type: "subagent/descriptor" }`](#persistence-type-sha256-b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5) | | `event:subagent/model-selection-policy` | event | `a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc` | [`{ type: "subagent/model-selection-policy" }`](#persistence-type-sha256-a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc) | | `event:system/message` | event | `69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1` | [`{ type: "system/message" }`](#persistence-type-sha256-69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1) | @@ -73,13 +75,14 @@ | `event:tool-workflow/run-end` | event | `42e0916e0dda5f6d1e7bb05d8514717147c036a79c9f36085683469516c1fd3f` | [`{ type: "tool-workflow/run-end" }`](#persistence-type-sha256-42e0916e0dda5f6d1e7bb05d8514717147c036a79c9f36085683469516c1fd3f) | | `event:tool-workflow/run-start` | event | `c1f9e0405de6d18cabb9ee70782a027f9bbdc57e5abec9dcccdd56119e2e9058` | [`{ type: "tool-workflow/run-start" }`](#persistence-type-sha256-c1f9e0405de6d18cabb9ee70782a027f9bbdc57e5abec9dcccdd56119e2e9058) | | `event:tool/call` | event | `3b1be838223869fe0a08210db85bf773796ed3f2373ac16555dff227cade0c48` | [`{ type: "tool/call" }`](#persistence-type-sha256-3b1be838223869fe0a08210db85bf773796ed3f2373ac16555dff227cade0c48) | -| `event:tool/ptc-dispatch` | event | `100f6dca1468538239522cde3533e5bd721d0f1a7b50bea8b0eb533ea6c96163` | [`{ type: "tool/ptc-dispatch" }`](#persistence-type-sha256-100f6dca1468538239522cde3533e5bd721d0f1a7b50bea8b0eb533ea6c96163) | +| `event:tool/ptc-dispatch` | event | `d66e11dddf5dfa3c71547e2cbd7b0608824cd8d6243fc16f532017ff28e8fba5` | [`{ type: "tool/ptc-dispatch" }`](#persistence-type-sha256-d66e11dddf5dfa3c71547e2cbd7b0608824cd8d6243fc16f532017ff28e8fba5) | | `event:tool/ptc-dispatch-start` | event | `ec38b5949af8eacaf00df002f4acbe344f934f8a061e9cdc65a52a48e5f6dd93` | [`{ type: "tool/ptc-dispatch-start" }`](#persistence-type-sha256-ec38b5949af8eacaf00df002f4acbe344f934f8a061e9cdc65a52a48e5f6dd93) | | `event:tool/result` | event | `7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397` | [`{ type: "tool/result" }`](#persistence-type-sha256-7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397) | | `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) | | `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) | -| `event:user/message` | event | `b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411` | [`{ type: "user/message" }`](#persistence-type-sha256-b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411) | +| `event:user/message` | event | `2ace3b8059b82b6d9837fed518ca97a99eb37c4482df7aad0e71c85489428ade` | [`{ type: "user/message" }`](#persistence-type-sha256-2ace3b8059b82b6d9837fed518ca97a99eb37c4482df7aad0e71c85489428ade) | | `event:web/deepseek-search-llm-request` | event | `cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331) | +| `event:working-directory/change` | event | `124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3` | [`{ type: "working-directory/change" }`](#persistence-type-sha256-124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3) | | `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) | ## 事件信封 @@ -582,7 +585,7 @@ export type SessionEvent = { } ``` -来源:[`packages/hooks/hook-protocol/src/types.ts:19`](../packages/hooks/hook-protocol/src/types.ts) +来源:[`packages/experimental/hook-protocol/src/types.ts:19`](../packages/experimental/hook-protocol/src/types.ts) @@ -605,7 +608,7 @@ export type SessionEvent = { } ``` -来源:[`packages/hooks/hook-protocol/src/types.ts:31`](../packages/hooks/hook-protocol/src/types.ts) +来源:[`packages/experimental/hook-protocol/src/types.ts:31`](../packages/experimental/hook-protocol/src/types.ts) ### `image/*` @@ -842,7 +845,7 @@ export type SessionEvent = { 类型:[SessionTitleLlmRequestEventData](subsystems/session-title.zh.md) -来源:[`packages/session/session-title-llm/src/index.ts:52`](../packages/session/session-title-llm/src/index.ts) +来源:[`packages/session/session-title-llm/src/index.ts:54`](../packages/session/session-title-llm/src/index.ts) ### `session-log-deepseek/*` @@ -902,7 +905,7 @@ export type SessionEvent = { 'subagent/catalog': SubagentCatalogEvent ``` -来源:[`packages/subagent/subagent/src/catalog.ts:43`](../packages/subagent/subagent/src/catalog.ts) +来源:[`packages/subagent/subagent/src/catalog.ts:44`](../packages/subagent/subagent/src/catalog.ts) @@ -911,8 +914,8 @@ export type SessionEvent = { ```ts persistence-catalog /** * Durable identity and lifecycle mode of a session-backed subagent child, - * appended once by the establishing provider inside the child's initial - * turn, before its first request. Continuable records also carry their + * appended once inside the child's initial turn, before its first + * request. Continuable records also carry their * resumable composition. Log-only: it carries no `surfaceOp`, never enters * model history, and survives compaction. */ @@ -1067,8 +1070,8 @@ export type SessionEvent = { * `tool/ptc-dispatch-start` with the same `subCallId`), the tool `name` * with the same JSON-normalized `arguments`, and the sub-call's complete * durable outcome in `tool/result`'s own vocabulary (`content` + `isError` - * + optional structured `error`), so UIs and SDKs render a sub-call through - * the exact path used for a native call. Every started sub-call settles + * + optional presentation `meta` and structured `error`), so UIs and SDKs + * render a sub-call through the exact path used for a native call. Every started sub-call settles * with exactly one of these (abort included: the aborted pipeline result * is an `isError` outcome). * Log-only: `deriveMessages()` ignores it, so sub-calls never re-enter @@ -1080,7 +1083,7 @@ export type SessionEvent = { 'tool/ptc-dispatch': PtcDispatchEventData ``` -来源:[`packages/core/tools/src/types.ts:58`](../packages/core/tools/src/types.ts) +来源:[`packages/core/tools/src/types.ts:61`](../packages/core/tools/src/types.ts) @@ -1103,7 +1106,7 @@ export type SessionEvent = { 'tool/ptc-dispatch-start': PtcDispatchStartEventData ``` -来源:[`packages/core/tools/src/types.ts:42`](../packages/core/tools/src/types.ts) +来源:[`packages/core/tools/src/types.ts:45`](../packages/core/tools/src/types.ts) @@ -1266,6 +1269,22 @@ export type SessionEvent = { 来源:[`packages/web/web-search-deepseek/src/provider.ts:82`](../packages/web/web-search-deepseek/src/provider.ts) +### `working-directory/*` + + + +#### `working-directory/change` — log-only + +```ts persistence-catalog +/** Effective working directory; the immutable Session header retains the original project. */ +'working-directory/change': { + /** Absolute directory used by subsequent directory-based operations. */ + cwd: string +} +``` + +来源:[`packages/session/working-directory/src/index.ts:36`](../packages/session/working-directory/src/index.ts) + ### `workspace/*` @@ -1735,6 +1754,14 @@ SHA-256: `e42b0f40b5d0b628c6e1c44a8fb9eda0780037a81959e1b93d2dfb47b46b0e87` `"every"` + + +### `"external"` + +SHA-256: `fc351843b8ecbd05d739c4ca4fcd4bc7cfad6c0beacbbd48389959bb1bf06cf3` + +`"external"` + ### `"failed"` @@ -2877,6 +2904,22 @@ SHA-256: `db6c5246911d47a7969f6b4cb47c7d0dbbde75c402710b22ace3672fa97bebe0` `"webhook"` + + +### `"working-directory"` + +SHA-256: `7b1ce60193fa8b69292e5843858d9cee7dc565dcb40d33c136e5d7e8b8d4296a` + +`"working-directory"` + + + +### `"working-directory/change"` + +SHA-256: `d4c799e3d51cae1040362ba4968efc484739e14416c9c1df20c2f108b4ef2eb5` + +`"working-directory/change"` + ### `"workspace-write"` @@ -2935,7 +2978,7 @@ SHA-256: `af6e173f7be819bcd0a5c8d77918649c10ce761d8a184ff472f2e01ce64eb2e0` SHA-256: `83da72c6857601c75da11212ef847f57d457b9c99f0a82246dcdff3e26700239` -来源:[`packages/context/agent-instructions/src/render.ts:47`](../packages/context/agent-instructions/src/render.ts) +来源:[`packages/context/agent-instructions/src/render.ts:53`](../packages/context/agent-instructions/src/render.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -2962,7 +3005,7 @@ SHA-256: `2ab12a0e345658f9342816d773fe610f049982bcad82631f43b9205abe43e5ee` SHA-256: `6f22a9d02b9a1f1157f3b0e7bdccd7e7b12b085c4efdb709dfc74a465cd4e590` -来源:[`packages/context/agent-instructions/src/state.ts:37`](../packages/context/agent-instructions/src/state.ts) +来源:[`packages/context/agent-instructions/src/state.ts:40`](../packages/context/agent-instructions/src/state.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -2982,7 +3025,7 @@ SHA-256: `6f22a9d02b9a1f1157f3b0e7bdccd7e7b12b085c4efdb709dfc74a465cd4e590` SHA-256: `4a9bef9f27a942520cc2279ca7dd3082556e4bff543f66b5c0333e1bbed1cd86` -来源:[`packages/subagent/subagent/src/continuation-messages.ts:15`](../packages/subagent/subagent/src/continuation-messages.ts) +来源:[`packages/subagent/subagent/src/continuation-messages.ts:14`](../packages/subagent/subagent/src/continuation-messages.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -3225,7 +3268,7 @@ SHA-256: `2517ba143a271508d3ca35126d5aca8f7f1facf5aaddce4adf011832042fa2b9` | `body` | 必需 | [`{ max_tokens, messages, model, tools }`](#persistence-type-sha256-930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731) | | `endpoint` | 必需 | `string` | - + @@ -3233,7 +3276,7 @@ SHA-256: `2517ba143a271508d3ca35126d5aca8f7f1facf5aaddce4adf011832042fa2b9` ### `DeveloperMessage` -SHA-256: `1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200` +SHA-256: `af99c95d46bed802870f1ebefe0ea11f92a03cb37b8e0cbe3f94b7aaaf509b08` 来源:[`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message.ts) @@ -3242,11 +3285,11 @@ SHA-256: `1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200` | `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | 必需 | `string` | | `role` | 必需 | `"developer"` | -| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116) | +| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-1fa156c85450ca31063d0e0d2853028e6dac9687f2d3191fa811648210118154) | 消息源兼容策略:`source` — `session-source-attribution` v1; `session.developer-message.source`; `kind`; `preserve`. -仅表示归属的新增 kind:`tmux-context`, `user-question-reply`. +仅表示归属的新增 kind:`tmux-context`, `user-question-reply`, `working-directory`. @@ -3541,13 +3584,13 @@ SHA-256: `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` - + ### `HookDialect` SHA-256: `a6b41f5bede8094d7bf635be5d69b1bd7a25e006ca9cf9415bceb9322f1a7feb` -来源:[`packages/hooks/hook-protocol/src/types.ts:48`](../packages/hooks/hook-protocol/src/types.ts) +来源:[`packages/experimental/hook-protocol/src/types.ts:48`](../packages/experimental/hook-protocol/src/types.ts) 以下类型之一: @@ -3774,7 +3817,7 @@ SHA-256: `4b1e632e05776982a2d21c25a50c7c04c199d311c5675218fa73ff9fe30d22cd` SHA-256: `67d5f73ceae82f5265258142ad6cf172de2c1fef93ba551b7f0454e0a93bffe0` -来源:[`packages/llm/llm/src/call-config.ts:23`](../packages/llm/llm/src/call-config.ts) +来源:[`packages/llm/llm/src/call-config.ts:31`](../packages/llm/llm/src/call-config.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -3795,7 +3838,7 @@ SHA-256: `67d5f73ceae82f5265258142ad6cf172de2c1fef93ba551b7f0454e0a93bffe0` SHA-256: `326a9bfda27dda5882fb3cbe6e715df41dae2298fdae3d66437457268a665693` -来源:[`packages/llm/llm/src/call-config.ts:36`](../packages/llm/llm/src/call-config.ts) +来源:[`packages/llm/llm/src/call-config.ts:54`](../packages/llm/llm/src/call-config.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -3859,7 +3902,7 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20` | `step` | 必需 | `number` | | `turn` | 必需 | `number` | - + @@ -3867,15 +3910,15 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20` ### `Message` -SHA-256: `cb7b863ddb7b3eab5f18f216602feba25f47b456e73e4e72b6bcc259875c6dab` +SHA-256: `d901c4f5e856591c3217af983caa15bba568d580700f0958bab56b88004eced9` 来源:[`packages/llm/llm/src/message.ts:196`](../packages/llm/llm/src/message.ts) 以下类型之一: - [`ToolResultMessage`](#persistence-type-sha256-4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731) -- [`DeveloperMessage`](#persistence-type-sha256-1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200) -- [`UserMessage`](#persistence-type-sha256-cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd) +- [`DeveloperMessage`](#persistence-type-sha256-af99c95d46bed802870f1ebefe0ea11f92a03cb37b8e0cbe3f94b7aaaf509b08) +- [`UserMessage`](#persistence-type-sha256-89b2edff547be1ecfcd78ef18aeeb3cf507fcbf7d6093ec04fb111ef4c8e8a97) - [`SystemMessage`](#persistence-type-sha256-09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363) - [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be) @@ -3952,7 +3995,7 @@ SHA-256: `390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2` - `"negative"` - `"positive"` - + @@ -3960,7 +4003,7 @@ SHA-256: `390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2` ### `MessageSource` -SHA-256: `26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116` +SHA-256: `1fa156c85450ca31063d0e0d2853028e6dac9687f2d3191fa811648210118154` 来源:[`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message.ts) @@ -4076,14 +4119,15 @@ SHA-256: `26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116` | `user-approval` | 未声明 | 无 | [`{ kind: "user-approval" }`](#persistence-type-sha256-6292bcbedaa97feaec5b48a03bb8144b581b93309d9d6c905f26da7a6a7a0a4f) | | `user-question-reply` | 未声明 | `callId`: `string`; `outcome`: `"answered"` | [`{ kind: "user-question-reply" }`](#persistence-type-sha256-c55827fba7018198772c50e37cb7d3f60a3d8bb6e94b07de72dee0d9f8968005) | | `webhook` | `"notice"` | `deliveryId`: `string`; `provider`: `string`; `ruleId`: `string`; `source`: `string`; `summary`: `string` | [`{ kind: "webhook", form: "notice" }`](#persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94) | +| `working-directory` | 未声明 | 无 | [`{ kind: "working-directory" }`](#persistence-type-sha256-2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5) | - + ### `Message[]` -SHA-256: `3f42f45cbb8a06586d247e5f3763dc02c8d31a145a9a3b82fabcd101e7e224cc` +SHA-256: `4be8fd28fefa5534675ea9aabd0a24efc2838b989bbc74da20a1efbd0daede8c` -[`Message`](#persistence-type-sha256-cb7b863ddb7b3eab5f18f216602feba25f47b456e73e4e72b6bcc259875c6dab) 的数组。 +[`Message`](#persistence-type-sha256-d901c4f5e856591c3217af983caa15bba568d580700f0958bab56b88004eced9) 的数组。 @@ -4201,7 +4245,7 @@ SHA-256: `1d9cb3caa96100b18b1911fa3ff8c751ef6992971a03c5f4d9aefc1a0004a345` [`PresentedFile`](#persistence-type-sha256-b8fc636a2121df9d8423c242e03c9d23b7ab7c6fdce00e45746932c60b730f97) 的数组。 - + @@ -4209,9 +4253,9 @@ SHA-256: `1d9cb3caa96100b18b1911fa3ff8c751ef6992971a03c5f4d9aefc1a0004a345` ### `PtcDispatchEventData` -SHA-256: `0ee52de154f8955c7abc82cffebc57452dcc8c44dccbdb6ad16c71a11305ead8` +SHA-256: `cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6` -来源:[`packages/core/tools/src/types.ts:20`](../packages/core/tools/src/types.ts) +来源:[`packages/core/tools/src/types.ts:21`](../packages/core/tools/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4219,6 +4263,7 @@ SHA-256: `0ee52de154f8955c7abc82cffebc57452dcc8c44dccbdb6ad16c71a11305ead8` | `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `error` | 可选 | [`{ code, name, reason? }`](#persistence-type-sha256-a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735) | | `isError` | 必需 | `boolean` | +| `meta` | 可选 | [`JsonValue`](#persistence-type-sha256-c592ce75aab73fcab19c1d7845684c72cf402b78d2e1f2833a58ecf9f3598ed6) | | `name` | 必需 | `string` | | `parentCallId` | 必需 | `string` | | `rootCallId` | 必需 | `string` | @@ -4234,7 +4279,7 @@ SHA-256: `0ee52de154f8955c7abc82cffebc57452dcc8c44dccbdb6ad16c71a11305ead8` SHA-256: `2fe5026daf14bd4ff2b7bfb7cf9791c302d9cbba06802178b6dfec00b892698f` -来源:[`packages/core/tools/src/types.ts:11`](../packages/core/tools/src/types.ts) +来源:[`packages/core/tools/src/types.ts:12`](../packages/core/tools/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4450,7 +4495,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4` | `source` | 必需 | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) | | `title` | 必需 | `string` | - + @@ -4458,7 +4503,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4` ### `SessionTitleLlmRequestEventData` -SHA-256: `fe3cdc6f9050584cea4a6d6998c31334bff7899f5ff1095082a44fdf3b47e996` +SHA-256: `045926475a46f625f84ae2be292b384817059f16927c69a9adab93717923ac19` 来源:[`packages/session/session-title-llm/src/index.ts:34`](../packages/session/session-title-llm/src/index.ts) @@ -4466,7 +4511,8 @@ SHA-256: `fe3cdc6f9050584cea4a6d6998c31334bff7899f5ff1095082a44fdf3b47e996` |---|---|---| | `maxTokens` | 必需 | `number` | | `messageSeqs` | 必需 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | -| `messages` | 必需 | [`Message[]`](#persistence-type-sha256-3f42f45cbb8a06586d247e5f3763dc02c8d31a145a9a3b82fabcd101e7e224cc) | +| `messages` | 必需 | [`Message[]`](#persistence-type-sha256-4be8fd28fefa5534675ea9aabd0a24efc2838b989bbc74da20a1efbd0daede8c) | +| `reasoningEffort` | 可选 | `string` | | `route` | 必需 | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) | | `system` | 必需 | `string` | | `titleProvider` | 必需 | `string` | @@ -4528,7 +4574,7 @@ SHA-256: `30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f` SHA-256: `fc2ae6962f9f8801c2b1e414668e1e17cbb7544c4f39381198977a38bd10c093` -来源:[`packages/skill/tool-skill/src/index.ts:34`](../packages/skill/tool-skill/src/index.ts) +来源:[`packages/skill/tool-skill/src/index.ts:35`](../packages/skill/tool-skill/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4577,7 +4623,7 @@ SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180` - [`{ type: "finish" }`](#persistence-type-sha256-a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1) - [`{ type: "usage" }`](#persistence-type-sha256-f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515) - + @@ -4585,17 +4631,18 @@ SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180` ### `SubagentCatalogEvent` -SHA-256: `1de64ea9a0849ce631b7e4e1255ae773fabd94a4a7c8d27e716f8012302e12c0` +SHA-256: `f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87` 来源:[`packages/subagent/subagent/src/catalog.ts:28`](../packages/subagent/subagent/src/catalog.ts) 以下类型之一: -- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd) - [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-e2ad8f5bcd813f268ffef1b10a12d7ddd2b8ced8b9b6d0275b3f6b7ed094a187) +- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09) - [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-5d542ac7bdf13310c46e1a884dec9fb18ad8c6e153cff207e639cd7bf9d7e694) - [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-b6896938660a1dfe4ddd100fb3164d9f8fa57feeabfe37441a80106c9942697a) - [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-a0f25b5aeeebf2bf1542ce67726b095e3b139a39c71861d57e827f12dea1f499) +- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd) @@ -4624,7 +4671,7 @@ SHA-256: `86d1d0e9cfebb240421187e3dc37fb71cc38a1501885cf32f8f9adef7b0a2bde` SHA-256: `4e97c3d85c0fc817ee58873c38c25a2474abaaa0af4839311b50b68db3b8cf1a` -来源:[`packages/subagent/subagent/src/continuation-messages.ts:30`](../packages/subagent/subagent/src/continuation-messages.ts) +来源:[`packages/subagent/subagent/src/continuation-messages.ts:29`](../packages/subagent/subagent/src/continuation-messages.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4951,7 +4998,7 @@ SHA-256: `43e311235aa537df1bacdd92d144df8a952337a76e9ac892cdf670f1a797bad8` SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f` -来源:[`packages/core/tools/src/index.ts:700`](../packages/core/tools/src/index.ts) +来源:[`packages/core/tools/src/index.ts:701`](../packages/core/tools/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5120,7 +5167,7 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402` | `interrupted` | 未声明 | 无 | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) | | `max-tokens` | 未声明 | 无 | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) | - + @@ -5128,7 +5175,7 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402` ### `UserMessage` -SHA-256: `cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd` +SHA-256: `89b2edff547be1ecfcd78ef18aeeb3cf507fcbf7d6093ec04fb111ef4c8e8a97` 来源:[`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message.ts) @@ -5137,19 +5184,19 @@ SHA-256: `cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd` | `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | 必需 | `string` | | `role` | 必需 | `"user"` | -| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116) | +| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-1fa156c85450ca31063d0e0d2853028e6dac9687f2d3191fa811648210118154) | 消息源兼容策略:`source` — `session-source-attribution` v1; `session.user-message.source`; `kind`; `preserve`. -仅表示归属的新增 kind:`tmux-context`, `user-question-reply`. +仅表示归属的新增 kind:`tmux-context`, `user-question-reply`, `working-directory`. - + ### `UserMessage[]` -SHA-256: `5a92f12db2059c282cdea45b6845c65e6f4a95dd3d649aeddbde6f9ae03c561f` +SHA-256: `2fef90def36aa758ca096c5f6ad0e00dbb2101943e19ca54b05618801974894a` -[`UserMessage`](#persistence-type-sha256-cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd) 的数组。 +[`UserMessage`](#persistence-type-sha256-89b2edff547be1ecfcd78ef18aeeb3cf507fcbf7d6093ec04fb111ef4c8e8a97) 的数组。 @@ -5271,6 +5318,12 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78` + + + + + + @@ -5295,12 +5348,6 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78` - - - - - - @@ -5337,7 +5384,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78` SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b` -来源:[`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) +来源:[`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) `string` @@ -5701,13 +5748,27 @@ SHA-256: `b6896938660a1dfe4ddd100fb3164d9f8fa57feeabfe37441a80106c9942697a` | `mode` | 必需 | `"one-shot"` | | `version` | 必需 | `1` | + + +### `{ childCreatedAt, childId, label?, mode, … }` + +SHA-256: `cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `childCreatedAt` | 必需 | `number` | +| `childId` | 必需 | `string` | +| `label` | 可选 | `string` | +| `mode` | 必需 | `"external"` | +| `version` | 必需 | `2` | + ### `{ code, name, reason? }` SHA-256: `a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735` -来源:[`packages/core/session/src/types.ts:383`](../packages/core/session/src/types.ts) · [`packages/core/tools/src/types.ts:24`](../packages/core/tools/src/types.ts) +来源:[`packages/core/session/src/types.ts:383`](../packages/core/session/src/types.ts) · [`packages/core/tools/src/types.ts:27`](../packages/core/tools/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5800,13 +5861,25 @@ SHA-256: `3e4e7683b0192c9f0e6a75c59076108f017892cdb302a98032c2632fd79c356f` | `sourceCommandId` | 可选 | `string` | | `turn` | 必需 | [`OptionalSessionSeq`](#persistence-type-sha256-3bd652ebfa8726b3ce3937a4f1bd2759e02f86b3a08a20f9e41a8513656fbc5f) | + + +### `{ cwd }` + +SHA-256: `da60ed58a8fa23d7aa4920ab5c1f4e3d28109cb0797ef9e5c8b125721b847582` + +来源:[`packages/session/working-directory/src/index.ts:36`](../packages/session/working-directory/src/index.ts) + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `cwd` | 必需 | `string` | + ### `{ decision, durationMs, exitCode?, handlerId, … }` SHA-256: `8ad45ad053b4d89f8865c17cc9ff3b8748e86045f3f0fb816269a34af2619141` -来源:[`packages/hooks/hook-protocol/src/types.ts:31`](../packages/hooks/hook-protocol/src/types.ts) +来源:[`packages/experimental/hook-protocol/src/types.ts:31`](../packages/experimental/hook-protocol/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5865,7 +5938,7 @@ SHA-256: `374fdd68aee5ff844e378c0cbdd961d247a0f22e4ade0bb03d800cd2a46a2b8b` SHA-256: `83079c8a3f733ac3fa603eefa0fbb4737ca4d8b69e7e2e7298b60f9ac4098693` -来源:[`packages/skill/tool-skill/src/index.ts:40`](../packages/skill/tool-skill/src/index.ts) +来源:[`packages/skill/tool-skill/src/index.ts:41`](../packages/skill/tool-skill/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5886,7 +5959,7 @@ SHA-256: `aba175fb20ffc71112c55efe70458ff5d7dbfb83501892b4ba8d077f0ea5d7ba` SHA-256: `119abe2b9c0b2f46ecb31d82b9acba4d4b30e049f655e2475e8b4ae12d5a9e25` -来源:[`packages/hooks/hook-protocol/src/types.ts:19`](../packages/hooks/hook-protocol/src/types.ts) +来源:[`packages/experimental/hook-protocol/src/types.ts:19`](../packages/experimental/hook-protocol/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5992,18 +6065,18 @@ SHA-256: `ad0c38a0adb82a94c925456df41586d9b5f9202fca18593eae2213d4445b4585` | `reason` | 必需 | [`RequestHeaderReason`](#persistence-type-sha256-530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6) | | `startsSeries` | 可选 | `true` | - + ### `{ headerSeq?, message, step, turn }` -SHA-256: `a5d80b5a2f3a6bbaf8f0334126632b78f94d8346122c68e8122ea89906d63d26` +SHA-256: `5b55817a403569fbb2d379496df2e3f3f71107f6c577a218028ce187c60b61e6` 来源:[`packages/core/session/src/types.ts:311`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| | `headerSeq` | 可选 | `number` | -| `message` | 必需 | [`DeveloperMessage`](#persistence-type-sha256-1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200) | +| `message` | 必需 | [`DeveloperMessage`](#persistence-type-sha256-af99c95d46bed802870f1ebefe0ea11f92a03cb37b8e0cbe3f94b7aaaf509b08) | | `step` | 必需 | `number` | | `turn` | 必需 | `number` | @@ -6087,17 +6160,17 @@ SHA-256: `17d1afb770d9941936130996da00dc86782cfef731d8d6526162c301256a4ac3` |---|---|---| | `inherited` | 可选 | `true` | - + ### `{ inserted, outcome?, removedCount?, start, … }` -SHA-256: `149c5a77529cb0dc06f5ceb6273069b7606174d1e7893b5f9b1071c15306b846` +SHA-256: `83a665476a9ee9af05cb1fdded12dd647d1b97b491200d7f12ef1af7cdac4428` 来源:[`packages/core/agent/src/types.ts:96`](../packages/core/agent/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| -| `inserted` | 必需 | [`UserMessage[]`](#persistence-type-sha256-5a92f12db2059c282cdea45b6845c65e6f4a95dd3d649aeddbde6f9ae03c561f) | +| `inserted` | 必需 | [`UserMessage[]`](#persistence-type-sha256-2fef90def36aa758ca096c5f6ad0e00dbb2101943e19ca54b05618801974894a) | | `outcome` | 可选 | `"canceled"` | | `removedCount` | 可选 | `number` | | `start` | 必需 | `number` | @@ -7258,7 +7331,7 @@ SHA-256: `c5228b7a55498d84f529591744a0ab26e0b93ade18a70a26db1377a1ecfa6b49` SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543` -来源:[`packages/core/tools/src/index.ts:34`](../packages/core/tools/src/index.ts) +来源:[`packages/core/tools/src/index.ts:35`](../packages/core/tools/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7376,7 +7449,7 @@ SHA-256: `c55827fba7018198772c50e37cb7d3f60a3d8bb6e94b07de72dee0d9f8968005` SHA-256: `9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94` -来源:[`packages/webhook/webhook/src/types.ts:74`](../packages/webhook/webhook/src/types.ts) +来源:[`packages/experimental/webhook/src/types.ts:74`](../packages/experimental/webhook/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7388,6 +7461,18 @@ SHA-256: `9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94` | `source` | 必需 | `string` | | `summary` | 必需 | `string` | + + +### `{ kind: "working-directory" }` + +SHA-256: `2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5` + +来源:[`packages/session/working-directory/src/index.ts:29`](../packages/session/working-directory/src/index.ts) + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `kind` | 必需 | `"working-directory"` | + ### `{ max_tokens, messages, model, tools }` @@ -7645,17 +7730,17 @@ SHA-256: `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | `time` | 必需 | `number` | | `type` | 必需 | `"agent-preset/selected"` | - + ### `{ type: "agent/inbox/spliced" }` -SHA-256: `6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178` +SHA-256: `9a36727716d0e73f681f3b725e7f28f89417c68174c8af0ff2b1ef52b08326a0` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-149c5a77529cb0dc06f5ceb6273069b7606174d1e7893b5f9b1071c15306b846) | +| `data` | 必需 | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-83a665476a9ee9af05cb1fdded12dd647d1b97b491200d7f12ef1af7cdac4428) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | @@ -7896,17 +7981,17 @@ SHA-256: `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | `time` | 必需 | `number` | | `type` | 必需 | `"deliverables/presented"` | - + ### `{ type: "developer/message" }` -SHA-256: `186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467` +SHA-256: `ba5b10ea77d68f4a29439a9bd2a14f6a560acf55731749d57237757cc72fbc06` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-a5d80b5a2f3a6bbaf8f0334126632b78f94d8346122c68e8122ea89906d63d26) | +| `data` | 必需 | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-5b55817a403569fbb2d379496df2e3f3f71107f6c577a218028ce187c60b61e6) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `sourceEventSeqs` | 可选 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | @@ -8262,17 +8347,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | `time` | 必需 | `number` | | `type` | 必需 | `"session/title"` | - + ### `{ type: "session/title-llm-request" }` -SHA-256: `ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46` +SHA-256: `b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-fe3cdc6f9050584cea4a6d6998c31334bff7899f5ff1095082a44fdf3b47e996) | +| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-045926475a46f625f84ae2be292b384817059f16927c69a9adab93717923ac19) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | @@ -8310,17 +8395,17 @@ SHA-256: `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | `time` | 必需 | `number` | | `type` | 必需 | `"step/start"` | - + ### `{ type: "subagent/catalog" }` -SHA-256: `3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604` +SHA-256: `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`SubagentCatalogEvent`](#persistence-type-sha256-1de64ea9a0849ce631b7e4e1255ae773fabd94a4a7c8d27e716f8012302e12c0) | +| `data` | 必需 | [`SubagentCatalogEvent`](#persistence-type-sha256-f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | @@ -8600,17 +8685,17 @@ SHA-256: `3b1be838223869fe0a08210db85bf773796ed3f2373ac16555dff227cade0c48` | `time` | 必需 | `number` | | `type` | 必需 | `"tool/call"` | - + ### `{ type: "tool/ptc-dispatch" }` -SHA-256: `100f6dca1468538239522cde3533e5bd721d0f1a7b50bea8b0eb533ea6c96163` +SHA-256: `d66e11dddf5dfa3c71547e2cbd7b0608824cd8d6243fc16f532017ff28e8fba5` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`PtcDispatchEventData`](#persistence-type-sha256-0ee52de154f8955c7abc82cffebc57452dcc8c44dccbdb6ad16c71a11305ead8) | +| `data` | 必需 | [`PtcDispatchEventData`](#persistence-type-sha256-cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | @@ -8695,17 +8780,17 @@ SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515` | `type` | 必需 | `"usage"` | | `usage` | 必需 | [`TokenUsage`](#persistence-type-sha256-fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394) | - + ### `{ type: "user/message" }` -SHA-256: `b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411` +SHA-256: `2ace3b8059b82b6d9837fed518ca97a99eb37c4482df7aad0e71c85489428ade` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`UserMessage`](#persistence-type-sha256-cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd) | +| `data` | 必需 | [`UserMessage`](#persistence-type-sha256-89b2edff547be1ecfcd78ef18aeeb3cf507fcbf7d6093ec04fb111ef4c8e8a97) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `sourceEventSeqs` | 可选 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | @@ -8743,6 +8828,22 @@ SHA-256: `2d11ca7b0d4493e244b74eba093227866b33249841e59a1e9bf56afed38604c3` | `name` | 必需 | `"web_search"` | | `type` | 必需 | `"web_search_20250305"` | + + + + +### `{ type: "working-directory/change" }` + +SHA-256: `124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `data` | 必需 | [`{ cwd }`](#persistence-type-sha256-da60ed58a8fa23d7aa4920ab5c1f4e3d28109cb0797ef9e5c8b125721b847582) | +| `ignorable` | 可选 | `true` | +| `seq` | 必需 | `number` | +| `time` | 必需 | `number` | +| `type` | 必需 | `"working-directory/change"` | + diff --git a/docs/persistence-changes/2026-09-13-working-directory.i18n.yaml b/docs/persistence-changes/2026-09-13-working-directory.i18n.yaml new file mode 100644 index 0000000000..9f630bfbcb --- /dev/null +++ b/docs/persistence-changes/2026-09-13-working-directory.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-09-13-working-directory.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-09-13-working-directory.md +/: + en: 3d5a424ae7d7e46b + zh: c22a8e4a62d8cc1a +/2026-09-13-working-directory: + en: 2ae6bda09b2fb22b + zh: 45204fff7e931e07 +/2026-09-13-working-directory/summary: + en: ac7c5d84de3040fa + zh: 85af1f5ea31e8956 +/2026-09-13-working-directory/table-of-contents: + en: 6cf314a069ceb451 + zh: 9adff678e9ecab3b +/2026-09-13-working-directory/declaration: + en: 6907136f28e48c90 + zh: 02f8298485f3d8e7 +/2026-09-13-working-directory/compatibility: + en: 3dae5b1bf48b0435 + zh: aea832a92da37dc4 +/2026-09-13-working-directory/verification: + en: ff3444e53a105e07 + zh: 7a77d20c75e59a59 +/2026-09-13-working-directory/dev-note: + en: b75c8458928b72b9 + zh: 003ba4e17b4b5269 diff --git a/docs/persistence-changes/2026-09-13-working-directory.md b/docs/persistence-changes/2026-09-13-working-directory.md new file mode 100644 index 0000000000..0c3de3fd7e --- /dev/null +++ b/docs/persistence-changes/2026-09-13-working-directory.md @@ -0,0 +1,48 @@ +--- +description: "Records a persistence type transition and its compatibility acknowledgement." +kind: persistence-change +--- + +# 2026-09-13-working-directory + +English | [中文](2026-09-13-working-directory.zh.md) + +## Summary + +Adds durable per-Session working-directory changes. + +## Table of Contents + +- [Declaration](#declaration) +- [Compatibility](#compatibility) +- [Verification](#verification) +- [Dev Note](#dev-note) + + +## Declaration + +```yaml persistence-change +schemaVersion: 1 +id: 2026-09-13-working-directory +baseline: false +changes: + - root: "event:working-directory/change" + previous: null + after: "124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3" + decision: same-version +``` + + +## Compatibility + +Existing Session headers and events remain valid. The new event is required on read: older builds that do not know working-directory/change refuse logs containing it. The event vocabulary grows without changing the structural Session format. + + +## Verification + +Focused working-directory and system-prompt suites pass 113 tests with per-file 100% coverage. Keyless headless replay verifies changes, recovery, user-context notices, and a stable system prompt. + + +## Dev Note + +None. diff --git a/docs/persistence-changes/2026-09-13-working-directory.schema.json b/docs/persistence-changes/2026-09-13-working-directory.schema.json new file mode 100644 index 0000000000..cf728b6d31 --- /dev/null +++ b/docs/persistence-changes/2026-09-13-working-directory.schema.json @@ -0,0 +1,76 @@ +{ + "formatVersion": 1, + "roots": [ + { + "key": "event:working-directory/change", + "kind": "event", + "event": "working-directory/change", + "surface": false, + "digest": "124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 3, + "optional": true + }, + { + "name": "seq", + "type": 4, + "optional": false + }, + { + "name": "time", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "cwd", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "working-directory/change" + } + ] + } + } + ], + "types": [] +} diff --git a/docs/persistence-changes/2026-09-13-working-directory.zh.md b/docs/persistence-changes/2026-09-13-working-directory.zh.md new file mode 100644 index 0000000000..81985385a3 --- /dev/null +++ b/docs/persistence-changes/2026-09-13-working-directory.zh.md @@ -0,0 +1,48 @@ +--- +description: "记录持久化类型更改及其兼容性确认。" +kind: persistence-change +--- + +# 2026-09-13-working-directory + +[English](2026-09-13-working-directory.md) | 中文 + +## 概述 + +增加每个 Session 的持久化工作目录变更。 + +## 目录 + +- [声明](#declaration) +- [兼容性](#compatibility) +- [验证](#verification) +- [开发备注](#dev-note) + + +## 声明 + +```yaml persistence-change +schemaVersion: 1 +id: 2026-09-13-working-directory +baseline: false +changes: + - root: "event:working-directory/change" + previous: null + after: "124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3" + decision: same-version +``` + + +## 兼容性 + +已有 Session header 和事件继续有效。新事件在读取时为必需:不认识 working-directory/change 的旧版本拒绝包含它的日志。事件词汇扩展不改变 Session 的结构格式。 + + +## 验证 + +工作目录和系统提示词定向套件通过 113 个测试,各源码文件覆盖率达到 100%。无密钥 headless 回放验证变更、恢复、用户上下文通知和稳定的系统提示词。 + + +## 开发备注 + +无。 diff --git a/docs/persistence-changes/2026-09-19-external-subagent-catalog.i18n.yaml b/docs/persistence-changes/2026-09-19-external-subagent-catalog.i18n.yaml new file mode 100644 index 0000000000..d2a7867bef --- /dev/null +++ b/docs/persistence-changes/2026-09-19-external-subagent-catalog.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-09-19-external-subagent-catalog.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-09-19-external-subagent-catalog.md +/: + en: 3d5a424ae7d7e46b + zh: c22a8e4a62d8cc1a +/2026-09-19-external-subagent-catalog: + en: 81860767c8b7e373 + zh: 4fcb6866cd8d8696 +/2026-09-19-external-subagent-catalog/summary: + en: 02848a04a27423e5 + zh: 8fa393f6130f3212 +/2026-09-19-external-subagent-catalog/table-of-contents: + en: 6cf314a069ceb451 + zh: 9adff678e9ecab3b +/2026-09-19-external-subagent-catalog/declaration: + en: 6907136f28e48c90 + zh: 02f8298485f3d8e7 +/2026-09-19-external-subagent-catalog/compatibility: + en: ae460855a021bf2b + zh: 3789da36fc4202fd +/2026-09-19-external-subagent-catalog/verification: + en: f629646a19627773 + zh: 7538959cb86407a1 +/2026-09-19-external-subagent-catalog/dev-note: + en: b75c8458928b72b9 + zh: 003ba4e17b4b5269 diff --git a/docs/persistence-changes/2026-09-19-external-subagent-catalog.md b/docs/persistence-changes/2026-09-19-external-subagent-catalog.md new file mode 100644 index 0000000000..1522d0ffa5 --- /dev/null +++ b/docs/persistence-changes/2026-09-19-external-subagent-catalog.md @@ -0,0 +1,48 @@ +--- +description: "Records a persistence type transition and its compatibility acknowledgement." +kind: persistence-change +--- + +# 2026-09-19-external-subagent-catalog + +English | [中文](2026-09-19-external-subagent-catalog.zh.md) + +## Summary + +Add external mode to parent catalog entries for children without a local Session. + +## Table of Contents + +- [Declaration](#declaration) +- [Compatibility](#compatibility) +- [Verification](#verification) +- [Dev Note](#dev-note) + + +## Declaration + +```yaml persistence-change +schemaVersion: 1 +id: 2026-09-19-external-subagent-catalog +baseline: false +changes: + - root: "event:subagent/catalog" + previous: "2026-09-20-unknown-child-catalog" + after: "53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac" + decision: same-version +``` + + +## Compatibility + +Existing version-0 and version-1 one-shot and continuable facts remain valid; version 1 also retains unknown mode. New external executions write payload version 2 with mode external, without a separate marker. Creation writes one membership fact; execution and cleanup do not update it. Duplicate child ids remain invalid. This does not change released generations or the Session format version. + + +## Verification + +Focused catalog, external activation, native V4 restoration, client navigation, and control-tool tests passed. They cover external membership, historical local modes, invalid marker rejection, non-navigable external leaves, and duplicate-child rejection. + + +## Dev Note + +None. diff --git a/docs/persistence-changes/2026-09-19-external-subagent-catalog.schema.json b/docs/persistence-changes/2026-09-19-external-subagent-catalog.schema.json new file mode 100644 index 0000000000..337379c605 --- /dev/null +++ b/docs/persistence-changes/2026-09-19-external-subagent-catalog.schema.json @@ -0,0 +1,290 @@ +{ + "formatVersion": 2, + "roots": [ + { + "key": "event:subagent/catalog", + "kind": "event", + "event": "subagent/catalog", + "surface": false, + "digest": "53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 17, + "optional": true + }, + { + "name": "seq", + "type": 3, + "optional": false + }, + { + "name": "time", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 7, + 9, + 12, + 14, + 15 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "childCreatedAt", + "type": 3, + "optional": false + }, + { + "name": "childId", + "type": 4, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": false + }, + { + "name": "mode", + "type": 5, + "optional": false + }, + { + "name": "version", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "continuable" + }, + { + "kind": "literal", + "value": 0 + }, + { + "kind": "object", + "properties": [ + { + "name": "childCreatedAt", + "type": 3, + "optional": false + }, + { + "name": "childId", + "type": 4, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": false + }, + { + "name": "mode", + "type": 5, + "optional": false + }, + { + "name": "version", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "childCreatedAt", + "type": 3, + "optional": false + }, + { + "name": "childId", + "type": 4, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": true + }, + { + "name": "mode", + "type": 10, + "optional": false + }, + { + "name": "version", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "external" + }, + { + "kind": "literal", + "value": 2 + }, + { + "kind": "object", + "properties": [ + { + "name": "childCreatedAt", + "type": 3, + "optional": false + }, + { + "name": "childId", + "type": 4, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": true + }, + { + "name": "mode", + "type": 13, + "optional": false + }, + { + "name": "version", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "one-shot" + }, + { + "kind": "object", + "properties": [ + { + "name": "childCreatedAt", + "type": 3, + "optional": false + }, + { + "name": "childId", + "type": 4, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": true + }, + { + "name": "mode", + "type": 13, + "optional": false + }, + { + "name": "version", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "childCreatedAt", + "type": 3, + "optional": false + }, + { + "name": "childId", + "type": 4, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": true + }, + { + "name": "mode", + "type": 16, + "optional": false + }, + { + "name": "version", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "unknown" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "subagent/catalog" + } + ] + } + } + ], + "types": [] +} diff --git a/docs/persistence-changes/2026-09-19-external-subagent-catalog.zh.md b/docs/persistence-changes/2026-09-19-external-subagent-catalog.zh.md new file mode 100644 index 0000000000..e1d563ed48 --- /dev/null +++ b/docs/persistence-changes/2026-09-19-external-subagent-catalog.zh.md @@ -0,0 +1,48 @@ +--- +description: "记录持久化类型更改及其兼容性确认。" +kind: persistence-change +--- + +# 2026-09-19-external-subagent-catalog + +[English](2026-09-19-external-subagent-catalog.md) | 中文 + +## 概述 + +为父级目录中没有本地 Session 的子级添加 external 模式。 + +## 目录 + +- [声明](#declaration) +- [兼容性](#compatibility) +- [验证](#verification) +- [开发备注](#dev-note) + + +## 声明 + +```yaml persistence-change +schemaVersion: 1 +id: 2026-09-19-external-subagent-catalog +baseline: false +changes: + - root: "event:subagent/catalog" + previous: "2026-09-20-unknown-child-catalog" + after: "53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac" + decision: same-version +``` + + +## 兼容性 + +已有 version-0 和 version-1 的 one-shot 与 continuable 事实仍然有效;version 1 还保留 unknown 模式。新外部执行写入 payload version 2 和 external 模式,不使用单独的标记。创建时写入一次成员事实,执行和清理不更新它。重复的子 id 仍然无效。这不改变已发布的代际或 Session 格式版本。 + + +## 验证 + +定向 catalog、外部 activation、原生 V4 恢复、客户端导航和控制工具测试通过,覆盖外部成员关系、历史本地模式、非法标记拒绝、不可导航的外部叶节点,以及重复子级拒绝。 + + +## 开发备注 + +无。 diff --git a/docs/persistence-changes/2026-10-05-ptc-operation-metadata.i18n.yaml b/docs/persistence-changes/2026-10-05-ptc-operation-metadata.i18n.yaml new file mode 100644 index 0000000000..1d225bc26f --- /dev/null +++ b/docs/persistence-changes/2026-10-05-ptc-operation-metadata.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-10-05-ptc-operation-metadata.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-10-05-ptc-operation-metadata.md +/: + en: 3d5a424ae7d7e46b + zh: c22a8e4a62d8cc1a +/2026-10-05-ptc-operation-metadata: + en: 1dead83b6553cd1a + zh: 9b6177c2b132640f +/2026-10-05-ptc-operation-metadata/summary: + en: 515119ce4f93c34d + zh: 36f42e64c69945cf +/2026-10-05-ptc-operation-metadata/table-of-contents: + en: 6cf314a069ceb451 + zh: 9adff678e9ecab3b +/2026-10-05-ptc-operation-metadata/declaration: + en: 6907136f28e48c90 + zh: 02f8298485f3d8e7 +/2026-10-05-ptc-operation-metadata/compatibility: + en: e9737c6c28ee0ab0 + zh: 468a046ccd51d95b +/2026-10-05-ptc-operation-metadata/verification: + en: a6d2fb4553a4f895 + zh: bb2367ad270ee0bc +/2026-10-05-ptc-operation-metadata/dev-note: + en: b75c8458928b72b9 + zh: 003ba4e17b4b5269 diff --git a/docs/persistence-changes/2026-10-05-ptc-operation-metadata.md b/docs/persistence-changes/2026-10-05-ptc-operation-metadata.md new file mode 100644 index 0000000000..aef4870208 --- /dev/null +++ b/docs/persistence-changes/2026-10-05-ptc-operation-metadata.md @@ -0,0 +1,48 @@ +--- +description: "Records a persistence type transition and its compatibility acknowledgement." +kind: persistence-change +--- + +# 2026-10-05-ptc-operation-metadata + +English | [中文](2026-10-05-ptc-operation-metadata.zh.md) + +## Summary + +Adds optional pure presentation metadata to persisted nested PTC tool results so history retains operation-time directories and file targets. + +## Table of Contents + +- [Declaration](#declaration) +- [Compatibility](#compatibility) +- [Verification](#verification) +- [Dev Note](#dev-note) + + +## Declaration + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-05-ptc-operation-metadata +baseline: false +changes: + - root: "event:tool/ptc-dispatch" + previous: "2026-09-16-session-format-v4" + after: "d66e11dddf5dfa3c71547e2cbd7b0608824cd8d6243fc16f532017ff28e8fba5" + decision: same-version +``` + + +## Compatibility + +Existing tool/ptc-dispatch records without meta remain valid. The optional JsonValue field does not change canonical program values, model-visible content, tool identities or message ordering. Historical calls without a recorded relative target keep safe unavailable navigation. The writer and accepted baseline stay at V4. + + +## Verification + +963 focused tests passed with per-file 100% coverage of the six affected runtime/UI sources; actual nested shell and relative filesystem regressions, frozen metadata controls, TypeScript SDK 45 tests, Python SDK 29 tests and three owning keyless refresh/replays passed. + + +## Dev Note + +None. diff --git a/docs/persistence-changes/2026-10-05-ptc-operation-metadata.schema.json b/docs/persistence-changes/2026-10-05-ptc-operation-metadata.schema.json new file mode 100644 index 0000000000..6c5e59e6da --- /dev/null +++ b/docs/persistence-changes/2026-10-05-ptc-operation-metadata.schema.json @@ -0,0 +1,457 @@ +{ + "formatVersion": 2, + "roots": [ + { + "key": "event:tool/ptc-dispatch", + "kind": "event", + "event": "tool/ptc-dispatch", + "surface": false, + "digest": "d66e11dddf5dfa3c71547e2cbd7b0608824cd8d6243fc16f532017ff28e8fba5", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 29, + "optional": true + }, + { + "name": "seq", + "type": 17, + "optional": false + }, + { + "name": "time", + "type": 17, + "optional": false + }, + { + "name": "type", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 2, + "optional": false + }, + { + "name": "content", + "type": 3, + "optional": false + }, + { + "name": "error", + "type": 31, + "optional": true + }, + { + "name": "isError", + "type": 32, + "optional": false + }, + { + "name": "meta", + "type": 33, + "optional": true + }, + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "parentCallId", + "type": 6, + "optional": false + }, + { + "name": "rootCallId", + "type": 6, + "optional": false + }, + { + "name": "subCallId", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "array", + "element": 4 + }, + { + "kind": "union", + "types": [ + 5, + 8, + 10, + 13, + 15, + 19, + 21 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 9, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "tool", + "type": 11, + "optional": true + }, + { + "name": "toolName", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 12, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "never" + }, + { + "kind": "literal", + "value": "tool-addition" + }, + { + "kind": "object", + "properties": [ + { + "name": "toolName", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 14, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-removal" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 16, + "optional": false + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 17, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 6, + "optional": false + }, + { + "name": "id", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "type", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 22, + "optional": false + }, + { + "name": "offloaded", + "type": 29, + "optional": true + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 17, + "optional": false + }, + { + "name": "height", + "type": 17, + "optional": false + }, + { + "name": "mediaType", + "type": 23, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": true + }, + { + "name": "originalDimensions", + "type": 28, + "optional": true + }, + { + "name": "width", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 24, + 25, + 26, + 27 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 17, + "optional": false + }, + { + "name": "width", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "code", + "type": 6, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + }, + { + "name": "reason", + "type": 6, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "union", + "types": [ + 6, + 17, + 32, + 34, + 35, + 36 + ] + }, + { + "kind": "array", + "element": 33 + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "object", + "properties": [], + "indices": [ + { + "key": 6, + "value": 33 + } + ] + }, + { + "kind": "literal", + "value": "tool/ptc-dispatch" + } + ] + } + } + ], + "types": [] +} diff --git a/docs/persistence-changes/2026-10-05-ptc-operation-metadata.zh.md b/docs/persistence-changes/2026-10-05-ptc-operation-metadata.zh.md new file mode 100644 index 0000000000..3e531b72f9 --- /dev/null +++ b/docs/persistence-changes/2026-10-05-ptc-operation-metadata.zh.md @@ -0,0 +1,48 @@ +--- +description: "记录持久化类型更改及其兼容性确认。" +kind: persistence-change +--- + +# 2026-10-05-ptc-operation-metadata + +[English](2026-10-05-ptc-operation-metadata.md) | 中文 + +## 概述 + +为持久化的嵌套 PTC 工具结果增加可选的纯展示元数据,使历史保留操作时的目录和文件目标。 + +## 目录 + +- [声明](#declaration) +- [兼容性](#compatibility) +- [验证](#verification) +- [开发备注](#dev-note) + + +## 声明 + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-05-ptc-operation-metadata +baseline: false +changes: + - root: "event:tool/ptc-dispatch" + previous: "2026-09-16-session-format-v4" + after: "d66e11dddf5dfa3c71547e2cbd7b0608824cd8d6243fc16f532017ff28e8fba5" + decision: same-version +``` + + +## 兼容性 + +没有 meta 的已有 tool/ptc-dispatch 记录仍然有效。可选 JsonValue 字段不改变程序读取的规范值、模型可见内容、工具标识或消息顺序。没有记录相对文件目标的历史调用仍安全地禁用导航。写入版本和已接受基线保持 V4。 + + +## 验证 + +963 个定向测试通过,六个受影响 runtime/UI 源文件逐文件覆盖率 100%;实际嵌套 shell 与相对文件操作回归、冻结元数据控制、TypeScript SDK 45 个测试、Python SDK 29 个测试及三个 owning keyless refresh/replay 通过。 + + +## 开发备注 + +无。 diff --git a/docs/persistence-changes/2026-10-05-working-directory-attribution.i18n.yaml b/docs/persistence-changes/2026-10-05-working-directory-attribution.i18n.yaml new file mode 100644 index 0000000000..1ae5d09e4f --- /dev/null +++ b/docs/persistence-changes/2026-10-05-working-directory-attribution.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-10-05-working-directory-attribution.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-10-05-working-directory-attribution.md +/: + en: 3d5a424ae7d7e46b + zh: c22a8e4a62d8cc1a +/2026-10-05-working-directory-attribution: + en: 0dfc1c93be2577cb + zh: 3448816e53ae4bda +/2026-10-05-working-directory-attribution/summary: + en: 5e013c7f8b9e268c + zh: 134bc44b2081addc +/2026-10-05-working-directory-attribution/table-of-contents: + en: 6cf314a069ceb451 + zh: 9adff678e9ecab3b +/2026-10-05-working-directory-attribution/declaration: + en: 6907136f28e48c90 + zh: 02f8298485f3d8e7 +/2026-10-05-working-directory-attribution/compatibility: + en: 39494aa3af994e6f + zh: 07c2880503668176 +/2026-10-05-working-directory-attribution/verification: + en: 5963942c0d87e518 + zh: 5dbb6ac0fca1f279 +/2026-10-05-working-directory-attribution/dev-note: + en: b75c8458928b72b9 + zh: 003ba4e17b4b5269 diff --git a/docs/persistence-changes/2026-10-05-working-directory-attribution.md b/docs/persistence-changes/2026-10-05-working-directory-attribution.md new file mode 100644 index 0000000000..80996eee5c --- /dev/null +++ b/docs/persistence-changes/2026-10-05-working-directory-attribution.md @@ -0,0 +1,60 @@ +--- +description: "Records a persistence type transition and its compatibility acknowledgement." +kind: persistence-change +--- + +# 2026-10-05-working-directory-attribution + +English | [中文](2026-10-05-working-directory-attribution.zh.md) + +## Summary + +Adds producer-owned working-directory notice attribution. + +## Table of Contents + +- [Declaration](#declaration) +- [Compatibility](#compatibility) +- [Verification](#verification) +- [Dev Note](#dev-note) + + +## Declaration + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-05-working-directory-attribution +baseline: false +changes: + - root: "event:agent/inbox/spliced" + previous: "2026-09-21-user-question-reply" + after: "9a36727716d0e73f681f3b725e7f28f89417c68174c8af0ff2b1ef52b08326a0" + decision: same-version + - root: "event:developer/message" + previous: "2026-09-21-user-question-reply" + after: "ba5b10ea77d68f4a29439a9bd2a14f6a560acf55731749d57237757cc72fbc06" + decision: same-version + - root: "event:session/title-llm-request" + previous: "2026-09-21-user-question-reply" + after: "19fab83e64cf84ee8a6caf62c592e85c9b195bfd19e84a2667c6693a395681fd" + decision: same-version + - root: "event:user/message" + previous: "2026-09-21-user-question-reply" + after: "2ace3b8059b82b6d9837fed518ca97a99eb37c4482df7aad0e71c85489428ade" + decision: same-version +``` + + +## Compatibility + +Existing records remain valid. The additive working-directory source kind is attribution only; the declared source policy preserves unknown attribution without interpreting it. The Session format remains unchanged. + + +## Verification + +pnpm exec vitest run packages/session/working-directory/tests packages/shell/tool-bash/tests/tools.spec.ts packages/shell/tool-pwsh/tests/tools.spec.ts packages/subagent/subagent-in-process-driver/tests/schedule-tools.spec.ts: 4 files and 203 tests passed. + + +## Dev Note + +None. diff --git a/docs/persistence-changes/2026-10-05-working-directory-attribution.schema.json b/docs/persistence-changes/2026-10-05-working-directory-attribution.schema.json new file mode 100644 index 0000000000..8b93756bfd --- /dev/null +++ b/docs/persistence-changes/2026-10-05-working-directory-attribution.schema.json @@ -0,0 +1,11474 @@ +{ + "formatVersion": 2, + "roots": [ + { + "key": "event:agent/inbox/spliced", + "kind": "event", + "event": "agent/inbox/spliced", + "surface": false, + "digest": "9a36727716d0e73f681f3b725e7f28f89417c68174c8af0ff2b1ef52b08326a0", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 27, + "optional": true + }, + { + "name": "seq", + "type": 20, + "optional": false + }, + { + "name": "time", + "type": 20, + "optional": false + }, + { + "name": "type", + "type": 204, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "inserted", + "type": 2, + "optional": false + }, + { + "name": "outcome", + "type": 200, + "optional": true + }, + { + "name": "removedCount", + "type": 20, + "optional": true + }, + { + "name": "start", + "type": 20, + "optional": false + }, + { + "name": "target", + "type": 201, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "role", + "type": 32, + "optional": false + }, + { + "name": "source", + "type": 33, + "optional": false, + "compatibility": { + "version": 1, + "policy": "session-source-attribution", + "binding": "session.user-message.source", + "discriminator": "kind", + "unknownKinds": "preserve", + "attributionKinds": [ + "tmux-context", + "user-question-reply", + "working-directory" + ] + } + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "union", + "types": [ + 6, + 9, + 11, + 14, + 16, + 18, + 29 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "tool", + "type": 12, + "optional": true + }, + { + "name": "toolName", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "never" + }, + { + "kind": "literal", + "value": "tool-addition" + }, + { + "kind": "object", + "properties": [ + { + "name": "toolName", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 15, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-removal" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 19, + "optional": false + }, + { + "name": "offloaded", + "type": 27, + "optional": true + }, + { + "name": "type", + "type": 28, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 20, + "optional": false + }, + { + "name": "height", + "type": 20, + "optional": false + }, + { + "name": "mediaType", + "type": 21, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "originalDimensions", + "type": 26, + "optional": true + }, + { + "name": "width", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 22, + 23, + 24, + 25 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 20, + "optional": false + }, + { + "name": "width", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 30, + "optional": false + }, + { + "name": "type", + "type": 31, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 20, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 34, + 39, + 42, + 45, + 47, + 49, + 51, + 53, + 55, + 57, + 59, + 61, + 63, + 65, + 67, + 69, + 70, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 121, + 122, + 123, + 124, + 125, + 127, + 129, + 132, + 133, + 134, + 135, + 136, + 138, + 139, + 140, + 141, + 143, + 144, + 145, + 146, + 147, + 149, + 150, + 151, + 153, + 155, + 156, + 164, + 167, + 169, + 170, + 172, + 174, + 178, + 180, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "dsh-session-title-llm" + }, + { + "kind": "array", + "element": 38 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 41, + "optional": false + }, + { + "name": "senderSessionId", + "type": 7, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + }, + { + "name": "senderSessionId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-claude-code" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-codex" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model-selection" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "plan-mode" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "repeat-tool-reminder" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "runtime-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "schedule" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "time-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 62, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tmux-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 66, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-jobs" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 68, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user-approval" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 68, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 68, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 68, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 68, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 7, + "optional": false + }, + { + "name": "kind", + "type": 126, + "optional": false + }, + { + "name": "revision", + "type": 20, + "optional": false + }, + { + "name": "round", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 128, + "optional": false + }, + { + "name": "messageId", + "type": 7, + "optional": false + }, + { + "name": "senderId", + "type": 7, + "optional": false + }, + { + "name": "senderName", + "type": 7, + "optional": false + }, + { + "name": "teamId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 130, + "optional": false + }, + { + "name": "model", + "type": 7, + "optional": false + }, + { + "name": "provider", + "type": 7, + "optional": false + }, + { + "name": "replayState", + "type": 131, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 137, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "ptc-mode" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 56, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 58, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 142, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "system-prompt" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 148, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-registry" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 32, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 68, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 152, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "working-directory" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 154, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "cordis-host-runner" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 27, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 7, + "optional": true + }, + { + "name": "changes", + "type": 157, + "optional": false + }, + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 163, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 158 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 159, + "optional": false + }, + { + "name": "digest", + "type": 7, + "optional": true + }, + { + "name": "path", + "type": 7, + "optional": false + }, + { + "name": "scope", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 160, + 161, + 162 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 7, + "optional": false + }, + { + "name": "kind", + "type": 165, + "optional": false + }, + { + "name": "outcome", + "type": 166, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user-question-reply" + }, + { + "kind": "literal", + "value": "answered" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 7, + "optional": false + }, + { + "name": "kind", + "type": 168, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 7, + "optional": true + }, + { + "name": "kind", + "type": 32, + "optional": false + }, + { + "name": "rpcId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "compactionId", + "type": 7, + "optional": false + }, + { + "name": "kind", + "type": 171, + "optional": false + }, + { + "name": "sourceCommandId", + "type": 7, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "compact-checkpoint" + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 7, + "optional": false + }, + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 173, + "optional": false + }, + { + "name": "provider", + "type": 7, + "optional": false + }, + { + "name": "ruleId", + "type": 7, + "optional": false + }, + { + "name": "source", + "type": 7, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 175, + "optional": false + }, + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 177, + "optional": false + }, + { + "name": "update", + "type": 27, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 176 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 179, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 181, + "optional": false + }, + { + "name": "references", + "type": 182, + "optional": false + }, + { + "name": "version", + "type": 187, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 183 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 20, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 184, + "optional": false + }, + { + "name": "compacted", + "type": 186, + "optional": false + }, + { + "name": "inputIndex", + "type": 20, + "optional": false + }, + { + "name": "label", + "type": 7, + "optional": false + }, + { + "name": "omittedBytes", + "type": 20, + "optional": false + }, + { + "name": "omittedMessages", + "type": 20, + "optional": false + }, + { + "name": "originalMessages", + "type": 20, + "optional": false + }, + { + "name": "retainedMessages", + "type": 20, + "optional": false + }, + { + "name": "sessionId", + "type": 7, + "optional": false + }, + { + "name": "truncated", + "type": 186, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 20, + 185 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 62, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 66, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 68, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "canceled" + }, + { + "kind": "union", + "types": [ + 202, + 203 + ] + }, + { + "kind": "literal", + "value": "next-step" + }, + { + "kind": "literal", + "value": "next-turn" + }, + { + "kind": "literal", + "value": "agent/inbox/spliced" + } + ] + } + }, + { + "key": "event:developer/message", + "kind": "event", + "event": "developer/message", + "surface": true, + "digest": "ba5b10ea77d68f4a29439a9bd2a14f6a560acf55731749d57237757cc72fbc06", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 26, + "optional": true + }, + { + "name": "seq", + "type": 2, + "optional": false + }, + { + "name": "sourceEventSeqs", + "type": 200, + "optional": true + }, + { + "name": "surfaceOp", + "type": 201, + "optional": false + }, + { + "name": "time", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 204, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "headerSeq", + "type": 2, + "optional": true + }, + { + "name": "message", + "type": 3, + "optional": false + }, + { + "name": "step", + "type": 2, + "optional": false + }, + { + "name": "turn", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 4, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "role", + "type": 31, + "optional": false + }, + { + "name": "source", + "type": 32, + "optional": false, + "compatibility": { + "version": 1, + "policy": "session-source-attribution", + "binding": "session.developer-message.source", + "discriminator": "kind", + "unknownKinds": "preserve", + "attributionKinds": [ + "tmux-context", + "user-question-reply", + "working-directory" + ] + } + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "union", + "types": [ + 6, + 9, + 11, + 14, + 16, + 18, + 28 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "tool", + "type": 12, + "optional": true + }, + { + "name": "toolName", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "never" + }, + { + "kind": "literal", + "value": "tool-addition" + }, + { + "kind": "object", + "properties": [ + { + "name": "toolName", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 15, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-removal" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 17, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 19, + "optional": false + }, + { + "name": "offloaded", + "type": 26, + "optional": true + }, + { + "name": "type", + "type": 27, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "mediaType", + "type": 20, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": true + }, + { + "name": "originalDimensions", + "type": 25, + "optional": true + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 21, + 22, + 23, + 24 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 29, + "optional": false + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 7, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "literal", + "value": "developer" + }, + { + "kind": "union", + "types": [ + 33, + 38, + 41, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + 62, + 64, + 66, + 68, + 69, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 121, + 122, + 123, + 124, + 126, + 128, + 131, + 132, + 133, + 134, + 135, + 136, + 138, + 139, + 140, + 141, + 143, + 144, + 145, + 146, + 147, + 149, + 151, + 152, + 154, + 156, + 164, + 167, + 169, + 170, + 172, + 174, + 178, + 180, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 35, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "user-approval" + }, + { + "kind": "array", + "element": 37 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 40, + "optional": false + }, + { + "name": "senderSessionId", + "type": 7, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 43, + "optional": false + }, + { + "name": "senderSessionId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 45, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "dsh-session-title-llm" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 47, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-claude-code" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 49, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-codex" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model-selection" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "plan-mode" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "repeat-tool-reminder" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "runtime-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "schedule" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 61, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "time-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 63, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tmux-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 65, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 67, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-jobs" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 35, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 45, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 47, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 49, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 63, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 65, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 45, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 47, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 49, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 63, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 65, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 45, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 47, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 49, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 63, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 65, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 45, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 47, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 49, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 63, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 65, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 7, + "optional": false + }, + { + "name": "kind", + "type": 125, + "optional": false + }, + { + "name": "revision", + "type": 2, + "optional": false + }, + { + "name": "round", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 127, + "optional": false + }, + { + "name": "messageId", + "type": 7, + "optional": false + }, + { + "name": "senderId", + "type": 7, + "optional": false + }, + { + "name": "senderName", + "type": 7, + "optional": false + }, + { + "name": "teamId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 129, + "optional": false + }, + { + "name": "model", + "type": 7, + "optional": false + }, + { + "name": "provider", + "type": 7, + "optional": false + }, + { + "name": "replayState", + "type": 130, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 45, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 47, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 49, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 51, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 53, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 137, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "ptc-mode" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 142, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "system-prompt" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 63, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 65, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 148, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-registry" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 150, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 153, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "working-directory" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 155, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "cordis-host-runner" + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 26, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 7, + "optional": true + }, + { + "name": "changes", + "type": 157, + "optional": false + }, + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 163, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 158 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 159, + "optional": false + }, + { + "name": "digest", + "type": 7, + "optional": true + }, + { + "name": "path", + "type": 7, + "optional": false + }, + { + "name": "scope", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 160, + 161, + 162 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 7, + "optional": false + }, + { + "name": "kind", + "type": 165, + "optional": false + }, + { + "name": "outcome", + "type": 166, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user-question-reply" + }, + { + "kind": "literal", + "value": "answered" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 7, + "optional": false + }, + { + "name": "kind", + "type": 168, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 7, + "optional": true + }, + { + "name": "kind", + "type": 150, + "optional": false + }, + { + "name": "rpcId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "compactionId", + "type": 7, + "optional": false + }, + { + "name": "kind", + "type": 171, + "optional": false + }, + { + "name": "sourceCommandId", + "type": 7, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "compact-checkpoint" + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 7, + "optional": false + }, + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 173, + "optional": false + }, + { + "name": "provider", + "type": 7, + "optional": false + }, + { + "name": "ruleId", + "type": 7, + "optional": false + }, + { + "name": "source", + "type": 7, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 175, + "optional": false + }, + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 177, + "optional": false + }, + { + "name": "update", + "type": 26, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 176 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 7, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 179, + "optional": false + }, + { + "name": "name", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 181, + "optional": false + }, + { + "name": "references", + "type": 182, + "optional": false + }, + { + "name": "version", + "type": 187, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 183 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 2, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 184, + "optional": false + }, + { + "name": "compacted", + "type": 186, + "optional": false + }, + { + "name": "inputIndex", + "type": 2, + "optional": false + }, + { + "name": "label", + "type": 7, + "optional": false + }, + { + "name": "omittedBytes", + "type": 2, + "optional": false + }, + { + "name": "omittedMessages", + "type": 2, + "optional": false + }, + { + "name": "originalMessages", + "type": 2, + "optional": false + }, + { + "name": "retainedMessages", + "type": 2, + "optional": false + }, + { + "name": "sessionId", + "type": 7, + "optional": false + }, + { + "name": "truncated", + "type": 186, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 185 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 45, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 47, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 49, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 53, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 61, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 63, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 65, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 67, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "union", + "types": [ + 202, + 203 + ] + }, + { + "kind": "literal", + "value": "append" + }, + { + "kind": "object", + "properties": [ + { + "name": "endSeq", + "type": 2, + "optional": false + }, + { + "name": "op", + "type": 161, + "optional": false + }, + { + "name": "startSeq", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "developer/message" + } + ] + } + }, + { + "key": "event:session/title-llm-request", + "kind": "event", + "event": "session/title-llm-request", + "surface": false, + "digest": "19fab83e64cf84ee8a6caf62c592e85c9b195bfd19e84a2667c6693a395681fd", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 29, + "optional": true + }, + { + "name": "seq", + "type": 2, + "optional": false + }, + { + "name": "time", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 210, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "maxTokens", + "type": 2, + "optional": false + }, + { + "name": "messageSeqs", + "type": 3, + "optional": false + }, + { + "name": "messages", + "type": 4, + "optional": false + }, + { + "name": "route", + "type": 209, + "optional": false + }, + { + "name": "system", + "type": 10, + "optional": false + }, + { + "name": "titleProvider", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "union", + "types": [ + 6, + 37, + 204, + 205, + 207 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "isError", + "type": 34, + "optional": true + }, + { + "name": "role", + "type": 35, + "optional": false + }, + { + "name": "source", + "type": 36, + "optional": false + }, + { + "name": "toolCallId", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 8 + }, + { + "kind": "union", + "types": [ + 9, + 12, + 14, + 17, + 19, + 21, + 31 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "tool", + "type": 15, + "optional": true + }, + { + "name": "toolName", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 16, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "never" + }, + { + "kind": "literal", + "value": "tool-addition" + }, + { + "kind": "object", + "properties": [ + { + "name": "toolName", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-removal" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 10, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 22, + "optional": false + }, + { + "name": "offloaded", + "type": 29, + "optional": true + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 10, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "mediaType", + "type": 23, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": true + }, + { + "name": "originalDimensions", + "type": 28, + "optional": true + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 24, + 25, + 26, + 27 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 32, + "optional": false + }, + { + "name": "type", + "type": 33, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 10, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 10, + "optional": false + }, + { + "name": "kind", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "role", + "type": 38, + "optional": false + }, + { + "name": "source", + "type": 39, + "optional": false, + "compatibility": { + "version": 1, + "policy": "session-source-attribution", + "binding": "session.developer-message.source", + "discriminator": "kind", + "unknownKinds": "preserve", + "attributionKinds": [ + "tmux-context", + "user-question-reply", + "working-directory" + ] + } + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "developer" + }, + { + "kind": "union", + "types": [ + 36, + 40, + 45, + 47, + 49, + 51, + 53, + 55, + 56, + 58, + 60, + 62, + 63, + 65, + 67, + 69, + 71, + 72, + 73, + 76, + 79, + 81, + 83, + 92, + 95, + 97, + 98, + 100, + 102, + 104, + 109, + 111, + 119, + 120, + 121, + 123, + 124, + 125, + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 157, + 158, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 174, + 175, + 176, + 177, + 178, + 179, + 180, + 181, + 182, + 183, + 184, + 185, + 186, + 187, + 188, + 189, + 191, + 192, + 194, + 197, + 198, + 199, + 200, + 201, + 203 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "tool-goal" + }, + { + "kind": "array", + "element": 44 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 10, + "optional": false + }, + { + "name": "text", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "runtime-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "schedule" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "system-prompt" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "time-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tmux-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-jobs" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-registry" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user-approval" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "working-directory" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 68, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "cordis-host-runner" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "dsh-session-title-llm" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 75, + "optional": false + }, + { + "name": "senderSessionId", + "type": 10, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 78, + "optional": false + }, + { + "name": "senderSessionId", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-claude-code" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-codex" + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 29, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 10, + "optional": true + }, + { + "name": "changes", + "type": 84, + "optional": false + }, + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 91, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 85 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 86, + "optional": false + }, + { + "name": "digest", + "type": 10, + "optional": true + }, + { + "name": "path", + "type": 10, + "optional": false + }, + { + "name": "scope", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 87, + 88, + 89 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 10, + "optional": false + }, + { + "name": "kind", + "type": 93, + "optional": false + }, + { + "name": "outcome", + "type": 94, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user-question-reply" + }, + { + "kind": "literal", + "value": "answered" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model-selection" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 10, + "optional": true + }, + { + "name": "kind", + "type": 61, + "optional": false + }, + { + "name": "rpcId", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "compactionId", + "type": 10, + "optional": false + }, + { + "name": "kind", + "type": 99, + "optional": false + }, + { + "name": "sourceCommandId", + "type": 10, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "compact-checkpoint" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "plan-mode" + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 10, + "optional": false + }, + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 103, + "optional": false + }, + { + "name": "provider", + "type": 10, + "optional": false + }, + { + "name": "ruleId", + "type": 10, + "optional": false + }, + { + "name": "source", + "type": 10, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 105, + "optional": false + }, + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 108, + "optional": false + }, + { + "name": "update", + "type": 29, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 106 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 110, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 113, + "optional": false + }, + { + "name": "references", + "type": 114, + "optional": false + }, + { + "name": "version", + "type": 118, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 115 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 2, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 116, + "optional": false + }, + { + "name": "compacted", + "type": 34, + "optional": false + }, + { + "name": "inputIndex", + "type": 2, + "optional": false + }, + { + "name": "label", + "type": 10, + "optional": false + }, + { + "name": "omittedBytes", + "type": 2, + "optional": false + }, + { + "name": "omittedMessages", + "type": 2, + "optional": false + }, + { + "name": "originalMessages", + "type": 2, + "optional": false + }, + { + "name": "retainedMessages", + "type": 2, + "optional": false + }, + { + "name": "sessionId", + "type": 10, + "optional": false + }, + { + "name": "truncated", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 117 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "repeat-tool-reminder" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 10, + "optional": false + }, + { + "name": "kind", + "type": 190, + "optional": false + }, + { + "name": "revision", + "type": 2, + "optional": false + }, + { + "name": "round", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 193, + "optional": false + }, + { + "name": "messageId", + "type": 10, + "optional": false + }, + { + "name": "senderId", + "type": 10, + "optional": false + }, + { + "name": "senderName", + "type": 10, + "optional": false + }, + { + "name": "teamId", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 195, + "optional": false + }, + { + "name": "model", + "type": 10, + "optional": false + }, + { + "name": "provider", + "type": 10, + "optional": false + }, + { + "name": "replayState", + "type": 196, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 202, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "ptc-mode" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "role", + "type": 61, + "optional": false + }, + { + "name": "source", + "type": 39, + "optional": false, + "compatibility": { + "version": 1, + "policy": "session-source-attribution", + "binding": "session.user-message.source", + "discriminator": "kind", + "unknownKinds": "preserve", + "attributionKinds": [ + "tmux-context", + "user-question-reply", + "working-directory" + ] + } + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "role", + "type": 206, + "optional": false + }, + { + "name": "source", + "type": 194, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "assistant" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "role", + "type": 208, + "optional": false + }, + { + "name": "source", + "type": 49, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "system" + }, + { + "kind": "object", + "properties": [ + { + "name": "model", + "type": 10, + "optional": false + }, + { + "name": "provider", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session/title-llm-request" + } + ] + } + }, + { + "key": "event:user/message", + "kind": "event", + "event": "user/message", + "surface": true, + "digest": "2ace3b8059b82b6d9837fed518ca97a99eb37c4482df7aad0e71c85489428ade", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 25, + "optional": true + }, + { + "name": "seq", + "type": 18, + "optional": false + }, + { + "name": "sourceEventSeqs", + "type": 198, + "optional": true + }, + { + "name": "surfaceOp", + "type": 199, + "optional": false + }, + { + "name": "time", + "type": 18, + "optional": false + }, + { + "name": "type", + "type": 202, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 2, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "role", + "type": 30, + "optional": false + }, + { + "name": "source", + "type": 31, + "optional": false, + "compatibility": { + "version": 1, + "policy": "session-source-attribution", + "binding": "session.user-message.source", + "discriminator": "kind", + "unknownKinds": "preserve", + "attributionKinds": [ + "tmux-context", + "user-question-reply", + "working-directory" + ] + } + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "union", + "types": [ + 4, + 7, + 9, + 12, + 14, + 16, + 27 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "tool", + "type": 10, + "optional": true + }, + { + "name": "toolName", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "never" + }, + { + "kind": "literal", + "value": "tool-addition" + }, + { + "kind": "object", + "properties": [ + { + "name": "toolName", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-removal" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 5, + "optional": false + }, + { + "name": "id", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "type", + "type": 15, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 17, + "optional": false + }, + { + "name": "offloaded", + "type": 25, + "optional": true + }, + { + "name": "type", + "type": 26, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 18, + "optional": false + }, + { + "name": "height", + "type": 18, + "optional": false + }, + { + "name": "mediaType", + "type": 19, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": true + }, + { + "name": "originalDimensions", + "type": 24, + "optional": true + }, + { + "name": "width", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "union", + "types": [ + 20, + 21, + 22, + 23 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 18, + "optional": false + }, + { + "name": "width", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 28, + "optional": false + }, + { + "name": "type", + "type": 29, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 5, + "optional": false + }, + { + "name": "bytes", + "type": 18, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "union", + "types": [ + 32, + 35, + 37, + 39, + 41, + 43, + 45, + 47, + 49, + 51, + 53, + 56, + 58, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 114, + 116, + 119, + 120, + 121, + 122, + 123, + 125, + 126, + 127, + 128, + 130, + 131, + 132, + 133, + 134, + 136, + 137, + 138, + 140, + 142, + 143, + 151, + 154, + 156, + 157, + 159, + 161, + 165, + 167, + 175, + 179, + 180, + 181, + 182, + 183, + 184, + 185, + 186, + 187, + 188, + 189, + 190, + 191, + 193, + 195, + 196, + 197 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 34, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "plan-mode" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "repeat-tool-reminder" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 38, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "runtime-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 40, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "schedule" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "time-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tmux-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-jobs" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user-approval" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "dsh-session-title-llm" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "hooks-claude-code" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-codex" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model-selection" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 38, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 40, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 38, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 40, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 38, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 40, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 38, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 40, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 5, + "optional": false + }, + { + "name": "kind", + "type": 113, + "optional": false + }, + { + "name": "revision", + "type": 18, + "optional": false + }, + { + "name": "round", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 115, + "optional": false + }, + { + "name": "messageId", + "type": 5, + "optional": false + }, + { + "name": "senderId", + "type": 5, + "optional": false + }, + { + "name": "senderName", + "type": 5, + "optional": false + }, + { + "name": "teamId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 117, + "optional": false + }, + { + "name": "model", + "type": 5, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "replayState", + "type": 118, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 124, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "ptc-mode" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 38, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 40, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 129, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "system-prompt" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 44, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 135, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-registry" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 139, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "working-directory" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 141, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "cordis-host-runner" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 25, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 5, + "optional": true + }, + { + "name": "changes", + "type": 144, + "optional": false + }, + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 150, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 145 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 146, + "optional": false + }, + { + "name": "digest", + "type": 5, + "optional": true + }, + { + "name": "path", + "type": 5, + "optional": false + }, + { + "name": "scope", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 147, + 148, + 149 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 5, + "optional": false + }, + { + "name": "kind", + "type": 152, + "optional": false + }, + { + "name": "outcome", + "type": 153, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user-question-reply" + }, + { + "kind": "literal", + "value": "answered" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 5, + "optional": false + }, + { + "name": "kind", + "type": 155, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 5, + "optional": true + }, + { + "name": "kind", + "type": 30, + "optional": false + }, + { + "name": "rpcId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "compactionId", + "type": 5, + "optional": false + }, + { + "name": "kind", + "type": 158, + "optional": false + }, + { + "name": "sourceCommandId", + "type": 5, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "compact-checkpoint" + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 5, + "optional": false + }, + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 160, + "optional": false + }, + { + "name": "provider", + "type": 5, + "optional": false + }, + { + "name": "ruleId", + "type": 5, + "optional": false + }, + { + "name": "source", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 162, + "optional": false + }, + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 164, + "optional": false + }, + { + "name": "update", + "type": 25, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 163 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 54, + "optional": false + }, + { + "name": "kind", + "type": 166, + "optional": false + }, + { + "name": "name", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 168, + "optional": false + }, + { + "name": "references", + "type": 169, + "optional": false + }, + { + "name": "version", + "type": 174, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 170 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 18, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 171, + "optional": false + }, + { + "name": "compacted", + "type": 173, + "optional": false + }, + { + "name": "inputIndex", + "type": 18, + "optional": false + }, + { + "name": "label", + "type": 5, + "optional": false + }, + { + "name": "omittedBytes", + "type": 18, + "optional": false + }, + { + "name": "omittedMessages", + "type": 18, + "optional": false + }, + { + "name": "originalMessages", + "type": 18, + "optional": false + }, + { + "name": "retainedMessages", + "type": 18, + "optional": false + }, + { + "name": "sessionId", + "type": 5, + "optional": false + }, + { + "name": "truncated", + "type": 173, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 18, + 172 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 176, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "sections", + "type": 177, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "array", + "element": 178 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 5, + "optional": false + }, + { + "name": "text", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 176, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "sections", + "type": 177, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 176, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + }, + { + "name": "sections", + "type": 177, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 176, + "optional": false + }, + { + "name": "kind", + "type": 34, + "optional": false + }, + { + "name": "sections", + "type": 177, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 176, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + }, + { + "name": "sections", + "type": 177, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 176, + "optional": false + }, + { + "name": "kind", + "type": 38, + "optional": false + }, + { + "name": "sections", + "type": 177, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 176, + "optional": false + }, + { + "name": "kind", + "type": 40, + "optional": false + }, + { + "name": "sections", + "type": 177, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 176, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + }, + { + "name": "sections", + "type": 177, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 176, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + }, + { + "name": "sections", + "type": 177, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 176, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "sections", + "type": 177, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 176, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + }, + { + "name": "sections", + "type": 177, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 176, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + }, + { + "name": "sections", + "type": 177, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 176, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + }, + { + "name": "sections", + "type": 177, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 192, + "optional": false + }, + { + "name": "senderSessionId", + "type": 5, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 194, + "optional": false + }, + { + "name": "senderSessionId", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 18 + }, + { + "kind": "union", + "types": [ + 200, + 201 + ] + }, + { + "kind": "literal", + "value": "append" + }, + { + "kind": "object", + "properties": [ + { + "name": "endSeq", + "type": 18, + "optional": false + }, + { + "name": "op", + "type": 148, + "optional": false + }, + { + "name": "startSeq", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user/message" + } + ] + } + } + ], + "types": [] +} diff --git a/docs/persistence-changes/2026-10-05-working-directory-attribution.zh.md b/docs/persistence-changes/2026-10-05-working-directory-attribution.zh.md new file mode 100644 index 0000000000..e7ed00e08f --- /dev/null +++ b/docs/persistence-changes/2026-10-05-working-directory-attribution.zh.md @@ -0,0 +1,60 @@ +--- +description: "记录持久化类型更改及其兼容性确认。" +kind: persistence-change +--- + +# 2026-10-05-working-directory-attribution + +[English](2026-10-05-working-directory-attribution.md) | 中文 + +## 概述 + +新增由工作目录组件拥有的通知来源标记。 + +## 目录 + +- [声明](#declaration) +- [兼容性](#compatibility) +- [验证](#verification) +- [开发备注](#dev-note) + + +## 声明 + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-05-working-directory-attribution +baseline: false +changes: + - root: "event:agent/inbox/spliced" + previous: "2026-09-21-user-question-reply" + after: "9a36727716d0e73f681f3b725e7f28f89417c68174c8af0ff2b1ef52b08326a0" + decision: same-version + - root: "event:developer/message" + previous: "2026-09-21-user-question-reply" + after: "ba5b10ea77d68f4a29439a9bd2a14f6a560acf55731749d57237757cc72fbc06" + decision: same-version + - root: "event:session/title-llm-request" + previous: "2026-09-21-user-question-reply" + after: "19fab83e64cf84ee8a6caf62c592e85c9b195bfd19e84a2667c6693a395681fd" + decision: same-version + - root: "event:user/message" + previous: "2026-09-21-user-question-reply" + after: "2ace3b8059b82b6d9837fed518ca97a99eb37c4482df7aad0e71c85489428ade" + decision: same-version +``` + + +## 兼容性 + +已有记录仍然有效。新增的 working-directory 来源种类只提供来源标记;已声明的来源策略保留未知标记而不解释其含义。Session 格式保持不变。 + + +## 验证 + +pnpm exec vitest run packages/session/working-directory/tests packages/shell/tool-bash/tests/tools.spec.ts packages/shell/tool-pwsh/tests/tools.spec.ts packages/subagent/subagent-in-process-driver/tests/schedule-tools.spec.ts:4 个文件、203 个测试通过。 + + +## 开发备注 + +无。 diff --git a/docs/persistence-changes/2026-10-07-title-reasoning-effort.i18n.yaml b/docs/persistence-changes/2026-10-07-title-reasoning-effort.i18n.yaml new file mode 100644 index 0000000000..3ac60a790d --- /dev/null +++ b/docs/persistence-changes/2026-10-07-title-reasoning-effort.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-10-07-title-reasoning-effort.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-10-07-title-reasoning-effort.md +/: + en: 3d5a424ae7d7e46b + zh: c22a8e4a62d8cc1a +/2026-10-07-title-reasoning-effort: + en: 62238f237d762372 + zh: 67c868935122faba +/2026-10-07-title-reasoning-effort/summary: + en: a23a870e04888548 + zh: a794ea9022adf492 +/2026-10-07-title-reasoning-effort/table-of-contents: + en: 6cf314a069ceb451 + zh: 9adff678e9ecab3b +/2026-10-07-title-reasoning-effort/declaration: + en: 6907136f28e48c90 + zh: 02f8298485f3d8e7 +/2026-10-07-title-reasoning-effort/compatibility: + en: 2aef689aa3af8d1c + zh: dc468f9d2c98d70c +/2026-10-07-title-reasoning-effort/verification: + en: 48ef9b977eaf074b + zh: b1183bda3a6a38be +/2026-10-07-title-reasoning-effort/dev-note: + en: b75c8458928b72b9 + zh: 003ba4e17b4b5269 diff --git a/docs/persistence-changes/2026-10-07-title-reasoning-effort.md b/docs/persistence-changes/2026-10-07-title-reasoning-effort.md new file mode 100644 index 0000000000..658dac171d --- /dev/null +++ b/docs/persistence-changes/2026-10-07-title-reasoning-effort.md @@ -0,0 +1,48 @@ +--- +description: "Records a persistence type transition and its compatibility acknowledgement." +kind: persistence-change +--- + +# 2026-10-07-title-reasoning-effort + +English | [中文](2026-10-07-title-reasoning-effort.zh.md) + +## Summary + +Records the concrete reasoning effort selected for auxiliary session-title requests. + +## Table of Contents + +- [Declaration](#declaration) +- [Compatibility](#compatibility) +- [Verification](#verification) +- [Dev Note](#dev-note) + + +## Declaration + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-07-title-reasoning-effort +baseline: false +changes: + - root: "event:session/title-llm-request" + previous: "2026-10-05-working-directory-attribution" + after: "b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6" + decision: same-version +``` + + +## Compatibility + +The new reasoningEffort field is optional. Existing V4 records remain valid and retain their original absence of a recorded effort. Prepared title requests write the resolved effort when the route exposes selectable reasoning; no Session format bump is required. + + +## Verification + +Focused LLM, adapter, title-provider, and replay tests passed: 50 files and 1960 tests. The session-title-after-turn SDK snapshot passed with a recorded low effort selected from a route whose default is high. + + +## Dev Note + +None. diff --git a/docs/persistence-changes/2026-10-07-title-reasoning-effort.schema.json b/docs/persistence-changes/2026-10-07-title-reasoning-effort.schema.json new file mode 100644 index 0000000000..06cad34dff --- /dev/null +++ b/docs/persistence-changes/2026-10-07-title-reasoning-effort.schema.json @@ -0,0 +1,2990 @@ +{ + "formatVersion": 2, + "roots": [ + { + "key": "event:session/title-llm-request", + "kind": "event", + "event": "session/title-llm-request", + "surface": false, + "digest": "b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 29, + "optional": true + }, + { + "name": "seq", + "type": 2, + "optional": false + }, + { + "name": "time", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 210, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "maxTokens", + "type": 2, + "optional": false + }, + { + "name": "messageSeqs", + "type": 3, + "optional": false + }, + { + "name": "messages", + "type": 4, + "optional": false + }, + { + "name": "reasoningEffort", + "type": 10, + "optional": true + }, + { + "name": "route", + "type": 209, + "optional": false + }, + { + "name": "system", + "type": 10, + "optional": false + }, + { + "name": "titleProvider", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "union", + "types": [ + 6, + 37, + 204, + 205, + 207 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "isError", + "type": 34, + "optional": true + }, + { + "name": "role", + "type": 35, + "optional": false + }, + { + "name": "source", + "type": 36, + "optional": false + }, + { + "name": "toolCallId", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 8 + }, + { + "kind": "union", + "types": [ + 9, + 12, + 14, + 17, + 19, + 21, + 31 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "tool", + "type": 15, + "optional": true + }, + { + "name": "toolName", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 16, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "never" + }, + { + "kind": "literal", + "value": "tool-addition" + }, + { + "kind": "object", + "properties": [ + { + "name": "toolName", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-removal" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 10, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 22, + "optional": false + }, + { + "name": "offloaded", + "type": 29, + "optional": true + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 10, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "mediaType", + "type": 23, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": true + }, + { + "name": "originalDimensions", + "type": 28, + "optional": true + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 24, + 25, + 26, + 27 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 32, + "optional": false + }, + { + "name": "type", + "type": 33, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 10, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 10, + "optional": false + }, + { + "name": "kind", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "role", + "type": 38, + "optional": false + }, + { + "name": "source", + "type": 39, + "optional": false, + "compatibility": { + "version": 1, + "policy": "session-source-attribution", + "binding": "session.developer-message.source", + "discriminator": "kind", + "unknownKinds": "preserve", + "attributionKinds": [ + "tmux-context", + "user-question-reply", + "working-directory" + ] + } + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "developer" + }, + { + "kind": "union", + "types": [ + 36, + 40, + 45, + 47, + 49, + 51, + 53, + 55, + 56, + 58, + 60, + 62, + 63, + 65, + 67, + 69, + 71, + 72, + 73, + 76, + 79, + 81, + 83, + 92, + 95, + 97, + 98, + 100, + 102, + 104, + 109, + 111, + 119, + 120, + 121, + 123, + 124, + 125, + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 157, + 158, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 174, + 175, + 176, + 177, + 178, + 179, + 180, + 181, + 182, + 183, + 184, + 185, + 186, + 187, + 188, + 189, + 191, + 192, + 194, + 197, + 198, + 199, + 200, + 201, + 203 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "tool-goal" + }, + { + "kind": "array", + "element": 44 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 10, + "optional": false + }, + { + "name": "text", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "runtime-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "schedule" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "system-prompt" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "time-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tmux-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-jobs" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-registry" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user-approval" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "working-directory" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 68, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "cordis-host-runner" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "dsh-session-title-llm" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 75, + "optional": false + }, + { + "name": "senderSessionId", + "type": 10, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 78, + "optional": false + }, + { + "name": "senderSessionId", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-claude-code" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-codex" + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 29, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 10, + "optional": true + }, + { + "name": "changes", + "type": 84, + "optional": false + }, + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 91, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 85 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 86, + "optional": false + }, + { + "name": "digest", + "type": 10, + "optional": true + }, + { + "name": "path", + "type": 10, + "optional": false + }, + { + "name": "scope", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 87, + 88, + 89 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 10, + "optional": false + }, + { + "name": "kind", + "type": 93, + "optional": false + }, + { + "name": "outcome", + "type": 94, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user-question-reply" + }, + { + "kind": "literal", + "value": "answered" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model-selection" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 10, + "optional": true + }, + { + "name": "kind", + "type": 61, + "optional": false + }, + { + "name": "rpcId", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "compactionId", + "type": 10, + "optional": false + }, + { + "name": "kind", + "type": 99, + "optional": false + }, + { + "name": "sourceCommandId", + "type": 10, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "compact-checkpoint" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "plan-mode" + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 10, + "optional": false + }, + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 103, + "optional": false + }, + { + "name": "provider", + "type": 10, + "optional": false + }, + { + "name": "ruleId", + "type": 10, + "optional": false + }, + { + "name": "source", + "type": 10, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 105, + "optional": false + }, + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 108, + "optional": false + }, + { + "name": "update", + "type": 29, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 106 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 110, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 113, + "optional": false + }, + { + "name": "references", + "type": 114, + "optional": false + }, + { + "name": "version", + "type": 118, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 115 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 2, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 116, + "optional": false + }, + { + "name": "compacted", + "type": 34, + "optional": false + }, + { + "name": "inputIndex", + "type": 2, + "optional": false + }, + { + "name": "label", + "type": 10, + "optional": false + }, + { + "name": "omittedBytes", + "type": 2, + "optional": false + }, + { + "name": "omittedMessages", + "type": 2, + "optional": false + }, + { + "name": "originalMessages", + "type": 2, + "optional": false + }, + { + "name": "retainedMessages", + "type": 2, + "optional": false + }, + { + "name": "sessionId", + "type": 10, + "optional": false + }, + { + "name": "truncated", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 117 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "repeat-tool-reminder" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 10, + "optional": false + }, + { + "name": "kind", + "type": 190, + "optional": false + }, + { + "name": "revision", + "type": 2, + "optional": false + }, + { + "name": "round", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 193, + "optional": false + }, + { + "name": "messageId", + "type": 10, + "optional": false + }, + { + "name": "senderId", + "type": 10, + "optional": false + }, + { + "name": "senderName", + "type": 10, + "optional": false + }, + { + "name": "teamId", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 195, + "optional": false + }, + { + "name": "model", + "type": 10, + "optional": false + }, + { + "name": "provider", + "type": 10, + "optional": false + }, + { + "name": "replayState", + "type": 196, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 202, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "ptc-mode" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "role", + "type": 61, + "optional": false + }, + { + "name": "source", + "type": 39, + "optional": false, + "compatibility": { + "version": 1, + "policy": "session-source-attribution", + "binding": "session.user-message.source", + "discriminator": "kind", + "unknownKinds": "preserve", + "attributionKinds": [ + "tmux-context", + "user-question-reply", + "working-directory" + ] + } + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "role", + "type": 206, + "optional": false + }, + { + "name": "source", + "type": 194, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "assistant" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "role", + "type": 208, + "optional": false + }, + { + "name": "source", + "type": 49, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "system" + }, + { + "kind": "object", + "properties": [ + { + "name": "model", + "type": 10, + "optional": false + }, + { + "name": "provider", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session/title-llm-request" + } + ] + } + } + ], + "types": [] +} diff --git a/docs/persistence-changes/2026-10-07-title-reasoning-effort.zh.md b/docs/persistence-changes/2026-10-07-title-reasoning-effort.zh.md new file mode 100644 index 0000000000..39c05b5b52 --- /dev/null +++ b/docs/persistence-changes/2026-10-07-title-reasoning-effort.zh.md @@ -0,0 +1,48 @@ +--- +description: "记录持久化类型更改及其兼容性确认。" +kind: persistence-change +--- + +# 2026-10-07-title-reasoning-effort + +[English](2026-10-07-title-reasoning-effort.md) | 中文 + +## 概述 + +记录辅助会话标题请求选择的具体推理强度。 + +## 目录 + +- [声明](#declaration) +- [兼容性](#compatibility) +- [验证](#verification) +- [开发备注](#dev-note) + + +## 声明 + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-07-title-reasoning-effort +baseline: false +changes: + - root: "event:session/title-llm-request" + previous: "2026-10-05-working-directory-attribution" + after: "b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6" + decision: same-version +``` + + +## 兼容性 + +新增 reasoningEffort 字段为可选。现有 V4 记录仍有效,继续保留其未记录强度的事实。路由提供可选推理强度时,已准备的标题请求写入解析后的强度;无需提升 Session 格式版本。 + + +## 验证 + +LLM、适配器、标题提供方与回放的定向测试通过:50 个文件、1960 个测试。session-title-after-turn SDK 快照通过,记录从默认 high 的路由中选出的 low 强度。 + + +## 开发备注 + +无。 diff --git a/docs/persistence-changes/README.i18n.yaml b/docs/persistence-changes/README.i18n.yaml index 92e983bcc1..30537db444 100644 --- a/docs/persistence-changes/README.i18n.yaml +++ b/docs/persistence-changes/README.i18n.yaml @@ -18,8 +18,8 @@ en: 6cd0f79a9455bdf2 zh: a5978b7834c0802b /persistence-type-change-records/compatibility-rules: - en: 7d5ce8c14e387922 - zh: 9106c78283ad932c + en: 62ce78d5618572b6 + zh: 023021d431f8a8e6 /persistence-type-change-records/history-and-limitations: en: 10a426d36c41548b zh: ef03b693d9d9be26 diff --git a/docs/persistence-changes/README.md b/docs/persistence-changes/README.md index 2f3d2dd50a..4f6cf773a9 100644 --- a/docs/persistence-changes/README.md +++ b/docs/persistence-changes/README.md @@ -53,14 +53,14 @@ Every detected structural change requires an acknowledgement. Record creation an | Add an optional event-body property, including its complete subtree | `same-version` | | Make a required event-body property optional | `same-version` | | Add an ordinary event type | `same-version` | -| Add a higher numeric `data.version` to an ordinary event while retaining every old payload alternative unchanged | `same-version` | +| Add a higher numeric `data.version` to an ordinary event while retaining every old payload alternative under the existing compatibility rules | `same-version` | | Add an explicitly qualified attribution kind to a user/developer source slot whose before and after schemas carry the same supported policy | `same-version` | | Make an optional property required, add a required property, change an existing type, or remove/rename a property or event | `version-bump` | | Change the Session header or event envelope | `version-bump` | A finalized checkpoint protects the accepted baseline without replacing these compatibility rules. At V4, optional additions, ordinary events, and qualified attribution additions can receive new same-version records. Breaking differences require a higher writer version and an acknowledgement containing its own header increase. The accepted V4 record cannot be updated to reuse its original 3→4 transition. -An ordinary event may add payload alternatives with a required, nonnegative integer `data.version` greater than every existing payload version, provided all existing alternatives remain structurally unchanged. The reader must retain support for the old payloads; adding variants at an existing version, dropping old versions, and changing the Session header or envelope remain breaking. Older readers may reject the new payload version. The [catalog acknowledgement](2026-09-20-unknown-child-catalog.md) records one such transition. +An ordinary event may add payload alternatives with a required, nonnegative integer `data.version` greater than every existing payload version, provided every existing alternative remains readable under the compatibility rules above. Optional property additions and required-to-optional changes may accompany the new version; each retained alternative is compared separately. The reader must retain support for the old payloads; adding variants at an existing version, dropping old versions, and changing the Session header or envelope remain breaking. Older readers may reject the new payload version. The [catalog acknowledgement](2026-09-20-unknown-child-catalog.md) records one such transition. The extractor accepts an explicit `@persistenceSource` binding for a core-owned source property on a literal user or developer role; it does not infer a binding from an unannotated type. A producer qualifies its `MessageSourceMap` entry with `@persistenceAttribution`. Qualification promises that an unknown kind and its JSON metadata survive reading without the producer, and that the kind imposes no validation, replay, or authority requirement. A producer may inspect its own kind to resume duplicate suppression; other readers must preserve and derive the recorded messages without that projection. The recorded schema retains the binding, policy version, literal `kind` discriminator, preservation promise, and qualified kind set. Inventory format 2 stores those promises; extraction without a bound policy retains format 1. Session format versions are independent. Both compared snapshots must carry compatible policy state. Existing kind groups still receive ordinary structural comparison; removals, unmarked additions, policy changes, and unrelated breaking changes remain strict. Multiple context-form alternatives with the same wire kind form one group. diff --git a/docs/persistence-changes/README.zh.md b/docs/persistence-changes/README.zh.md index d2cec933b2..5754ff07c1 100644 --- a/docs/persistence-changes/README.zh.md +++ b/docs/persistence-changes/README.zh.md @@ -53,14 +53,14 @@ description: "审阅和维护已记录的会话持久化类型变更、对应 sc | 添加可选事件体属性,包括其完整子树 | `same-version` | | 将必选事件体属性改为可选 | `same-version` | | 添加普通事件类型 | `same-version` | -| 为普通事件增加更高的数字 `data.version`,并原样保留所有旧载荷分支 | `same-version` | +| 为普通事件增加更高的数字 `data.version`,并按现有兼容性规则保留所有旧载荷分支 | `same-version` | | 在前后 schema 均带有相同受支持策略的 user/developer 消息源字段中,添加显式声明为归属信息的 kind | `same-version` | | 将可选属性改为必选、添加必选属性、更改已有类型,或删除/重命名属性或事件 | `version-bump` | | 更改会话头或事件封装 | `version-bump` | 定稿检查点保护已接受基线,不替换这些兼容性规则。在 V4 中,可选新增、普通事件及符合条件的归属 kind 新增可以使用新的同版本记录。破坏性差异要求更高的写入器版本,以及包含自身头版本递增的确认记录。不能更新已接受 V4 记录以复用其原有 3→4 转换。 -普通事件可增加具有必选、非负整数 `data.version` 的载荷分支,新版本必须高于所有已有载荷版本,且所有已有分支结构保持不变。读取器必须保留旧载荷支持;在已有版本中增加分支、移除旧版本,以及修改 Session header 或事件封装仍属于破坏性变更。旧读取器可能拒绝新载荷版本。[Catalog 确认记录](2026-09-20-unknown-child-catalog.zh.md) 记录了此类演进。 +普通事件可增加具有必选、非负整数 `data.version` 的载荷分支,新版本必须高于所有已有载荷版本,且所有已有分支按上述兼容性规则保持可读。可选属性新增和必选改可选可以与新版本一同引入;每个保留分支均独立比较。读取器必须保留旧载荷支持;在已有版本中增加分支、移除旧版本,以及修改 Session header 或事件封装仍属于破坏性变更。旧读取器可能拒绝新载荷版本。[Catalog 确认记录](2026-09-20-unknown-child-catalog.zh.md) 记录了此类演进。 提取器接受核心拥有的 source 属性上针对 user 或 developer 字面量角色的显式 `@persistenceSource` 绑定,不会从未标记类型中推断绑定。生产者用 `@persistenceAttribution` 标记其 `MessageSourceMap` 成员。该标记承诺:读取器无需生产者即可保留未知 kind 及其 JSON 元数据,且该 kind 不引入校验、回放或权限要求。生产者可以检查自身 kind 来恢复去重状态;其他读取器必须无需该投影也能保留并派生已记录的消息。记录的 schema 保存绑定、策略版本、字面量 `kind` 判别字段、保留承诺及符合条件的 kind 集合。Inventory format 2 保存这些承诺;没有绑定策略的提取仍使用 format 1。Session 格式版本独立于此。比较双方的快照必须带有兼容的策略状态。已有 kind 分组仍进行常规结构比较;删除、未标记的添加、策略更改及无关破坏性变更继续采用严格规则。同一 wire kind 的多个上下文形式分支归为一组。 diff --git a/docs/persistence-changes/historical-formats/README.i18n.yaml b/docs/persistence-changes/historical-formats/README.i18n.yaml index ab050bed1e..a85d9e2a90 100644 --- a/docs/persistence-changes/historical-formats/README.i18n.yaml +++ b/docs/persistence-changes/historical-formats/README.i18n.yaml @@ -15,8 +15,8 @@ en: 14c57f30e676f6e3 zh: b1a0ccf26fab3727 /session-persistence-formats/format-references: - en: b19e6cfc84bfc46d - zh: 5b1d86bcdab672ca + en: 553fd4963cf19abe + zh: 7ca3cd5d0a88d35a /session-persistence-formats/scope-and-evidence: en: b220062139ddac1e zh: 7c4c0efe05dafa9c diff --git a/docs/persistence-changes/historical-formats/README.md b/docs/persistence-changes/historical-formats/README.md index c2da060729..8f06d0e1ee 100644 --- a/docs/persistence-changes/historical-formats/README.md +++ b/docs/persistence-changes/historical-formats/README.md @@ -32,7 +32,7 @@ The index is generated from validated snapshots and the current writer constant. | 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 | | 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 | | 3 | PR #4320 | [V3](v3.md) | [JSON](v3.schema.json) | 60 / 467 | -| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 62 / 590 | +| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 63 / 597 | diff --git a/docs/persistence-changes/historical-formats/README.zh.md b/docs/persistence-changes/historical-formats/README.zh.md index 253c0943ba..f377a801aa 100644 --- a/docs/persistence-changes/historical-formats/README.zh.md +++ b/docs/persistence-changes/historical-formats/README.zh.md @@ -32,7 +32,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式 | 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 | | 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 | | 3 | PR #4320 | [V3](v3.zh.md) | [JSON](v3.schema.json) | 60 / 467 | -| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 62 / 590 | +| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 63 / 597 | diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index f29dde8245..e6b73c0fcd 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -406,7 +406,7 @@ "kind": "event", "event": "agent/inbox/spliced", "surface": false, - "digest": "6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178", + "digest": "9a36727716d0e73f681f3b725e7f28f89417c68174c8af0ff2b1ef52b08326a0", "schema": { "root": 0, "nodes": [ @@ -435,7 +435,7 @@ }, { "name": "type", - "type": 202, + "type": 204, "optional": false } ], @@ -451,7 +451,7 @@ }, { "name": "outcome", - "type": 198, + "type": 200, "optional": true }, { @@ -466,7 +466,7 @@ }, { "name": "target", - "type": 199, + "type": 201, "optional": false } ], @@ -506,7 +506,8 @@ "unknownKinds": "preserve", "attributionKinds": [ "tmux-context", - "user-question-reply" + "user-question-reply", + "working-directory" ] } } @@ -816,29 +817,24 @@ "kind": "union", "types": [ 34, - 37, 39, - 41, - 43, + 42, 45, 47, 49, 51, 53, 55, - 58, - 60, - 62, + 57, + 59, + 61, 63, - 64, 65, - 66, 67, - 68, 69, 70, - 71, 72, + 73, 74, 75, 76, @@ -850,8 +846,8 @@ 82, 83, 84, - 85, 86, + 87, 88, 89, 90, @@ -865,6 +861,7 @@ 98, 99, 100, + 101, 102, 103, 104, @@ -875,20 +872,21 @@ 109, 110, 111, - 112, 113, 114, + 115, 116, + 117, 118, + 119, + 120, 121, 122, 123, 124, 125, 127, - 128, 129, - 130, 132, 133, 134, @@ -897,34 +895,38 @@ 138, 139, 140, - 142, + 141, 143, + 144, + 145, + 146, + 147, + 149, + 150, 151, - 154, + 153, + 155, 156, - 157, - 159, - 161, - 165, + 164, 167, - 175, - 179, + 169, + 170, + 172, + 174, + 178, 180, - 181, - 182, - 183, - 184, - 185, - 186, - 187, 188, 189, 190, 191, + 192, 193, + 194, 195, 196, - 197 + 197, + 198, + 199 ] }, { @@ -940,6 +942,60 @@ "type": 36, "optional": false }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "dsh-session-title-llm" + }, + { + "kind": "array", + "element": 38 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 41, + "optional": false + }, + { + "name": "senderSessionId", + "type": 7, + "optional": false + }, { "name": "summary", "type": 7, @@ -952,6 +1008,135 @@ "kind": "literal", "value": "notice" }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 44, + "optional": false + }, + { + "name": "senderSessionId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-claude-code" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-codex" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 50, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model-selection" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 40, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, { "kind": "literal", "value": "plan-mode" @@ -961,12 +1146,12 @@ "properties": [ { "name": "form", - "type": 35, + "type": 40, "optional": false }, { "name": "kind", - "type": 38, + "type": 54, "optional": false }, { @@ -986,12 +1171,12 @@ "properties": [ { "name": "form", - "type": 35, + "type": 40, "optional": false }, { "name": "kind", - "type": 40, + "type": 56, "optional": false }, { @@ -1011,12 +1196,12 @@ "properties": [ { "name": "form", - "type": 35, + "type": 40, "optional": false }, { "name": "kind", - "type": 42, + "type": 58, "optional": false }, { @@ -1036,12 +1221,12 @@ "properties": [ { "name": "form", - "type": 35, + "type": 40, "optional": false }, { "name": "kind", - "type": 44, + "type": 60, "optional": false }, { @@ -1061,12 +1246,12 @@ "properties": [ { "name": "form", - "type": 35, + "type": 40, "optional": false }, { "name": "kind", - "type": 46, + "type": 62, "optional": false }, { @@ -1086,12 +1271,12 @@ "properties": [ { "name": "form", - "type": 35, + "type": 40, "optional": false }, { "name": "kind", - "type": 48, + "type": 64, "optional": false }, { @@ -1111,12 +1296,12 @@ "properties": [ { "name": "form", - "type": 35, + "type": 40, "optional": false }, { "name": "kind", - "type": 50, + "type": 66, "optional": false }, { @@ -1136,12 +1321,12 @@ "properties": [ { "name": "form", - "type": 35, + "type": 40, "optional": false }, { "name": "kind", - "type": 52, + "type": 68, "optional": false }, { @@ -1161,12 +1346,12 @@ "properties": [ { "name": "form", - "type": 35, + "type": 40, "optional": false }, { "name": "kind", - "type": 54, + "type": 36, "optional": false }, { @@ -1177,160 +1362,12 @@ ], "indices": [] }, - { - "kind": "literal", - "value": "dsh-session-title-llm" - }, { "kind": "object", "properties": [ { "name": "form", - "type": 56, - "optional": false - }, - { - "name": "kind", - "type": 57, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "literal", - "value": "hooks-claude-code" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 56, - "optional": false - }, - { - "name": "kind", - "type": 59, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "hooks-codex" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 56, - "optional": false - }, - { - "name": "kind", - "type": 61, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model-selection" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 56, - "optional": false - }, - { - "name": "kind", - "type": 36, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 56, - "optional": false - }, - { - "name": "kind", - "type": 38, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 56, - "optional": false - }, - { - "name": "kind", - "type": 40, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 56, - "optional": false - }, - { - "name": "kind", - "type": 42, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 56, - "optional": false - }, - { - "name": "kind", - "type": 44, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 56, + "type": 71, "optional": false }, { @@ -1341,12 +1378,16 @@ ], "indices": [] }, + { + "kind": "literal", + "value": "instructions" + }, { "kind": "object", "properties": [ { "name": "form", - "type": 56, + "type": 71, "optional": false }, { @@ -1362,7 +1403,7 @@ "properties": [ { "name": "form", - "type": 56, + "type": 71, "optional": false }, { @@ -1378,7 +1419,7 @@ "properties": [ { "name": "form", - "type": 56, + "type": 71, "optional": false }, { @@ -1394,7 +1435,7 @@ "properties": [ { "name": "form", - "type": 56, + "type": 71, "optional": false }, { @@ -1410,12 +1451,140 @@ "properties": [ { "name": "form", - "type": 73, + "type": 71, "optional": false }, { "name": "kind", - "type": 57, + "type": 56, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 68, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 46, "optional": false } ], @@ -1430,135 +1599,7 @@ "properties": [ { "name": "form", - "type": 73, - "optional": false - }, - { - "name": "kind", - "type": 59, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 73, - "optional": false - }, - { - "name": "kind", - "type": 61, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 73, - "optional": false - }, - { - "name": "kind", - "type": 36, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 73, - "optional": false - }, - { - "name": "kind", - "type": 38, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 73, - "optional": false - }, - { - "name": "kind", - "type": 40, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 73, - "optional": false - }, - { - "name": "kind", - "type": 42, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 73, - "optional": false - }, - { - "name": "kind", - "type": 44, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 73, - "optional": false - }, - { - "name": "kind", - "type": 46, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 73, + "type": 85, "optional": false }, { @@ -1574,7 +1615,7 @@ "properties": [ { "name": "form", - "type": 73, + "type": 85, "optional": false }, { @@ -1590,7 +1631,7 @@ "properties": [ { "name": "form", - "type": 73, + "type": 85, "optional": false }, { @@ -1606,7 +1647,7 @@ "properties": [ { "name": "form", - "type": 73, + "type": 85, "optional": false }, { @@ -1622,32 +1663,12 @@ "properties": [ { "name": "form", - "type": 87, + "type": 85, "optional": false }, { "name": "kind", - "type": 57, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 87, - "optional": false - }, - { - "name": "kind", - "type": 59, + "type": 56, "optional": false } ], @@ -1658,12 +1679,12 @@ "properties": [ { "name": "form", - "type": 87, + "type": 85, "optional": false }, { "name": "kind", - "type": 61, + "type": 58, "optional": false } ], @@ -1674,7 +1695,87 @@ "properties": [ { "name": "form", - "type": 87, + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 68, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, "optional": false }, { @@ -1690,71 +1791,7 @@ "properties": [ { "name": "form", - "type": 87, - "optional": false - }, - { - "name": "kind", - "type": 38, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 87, - "optional": false - }, - { - "name": "kind", - "type": 40, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 87, - "optional": false - }, - { - "name": "kind", - "type": 42, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 87, - "optional": false - }, - { - "name": "kind", - "type": 44, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 87, + "type": 43, "optional": false }, { @@ -1770,7 +1807,7 @@ "properties": [ { "name": "form", - "type": 87, + "type": 43, "optional": false }, { @@ -1786,7 +1823,7 @@ "properties": [ { "name": "form", - "type": 87, + "type": 43, "optional": false }, { @@ -1802,7 +1839,7 @@ "properties": [ { "name": "form", - "type": 87, + "type": 43, "optional": false }, { @@ -1818,7 +1855,7 @@ "properties": [ { "name": "form", - "type": 87, + "type": 43, "optional": false }, { @@ -1834,12 +1871,140 @@ "properties": [ { "name": "form", - "type": 101, + "type": 43, "optional": false }, { "name": "kind", - "type": 57, + "type": 56, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 68, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 43, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 46, "optional": false } ], @@ -1854,135 +2019,7 @@ "properties": [ { "name": "form", - "type": 101, - "optional": false - }, - { - "name": "kind", - "type": 59, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 101, - "optional": false - }, - { - "name": "kind", - "type": 61, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 101, - "optional": false - }, - { - "name": "kind", - "type": 36, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 101, - "optional": false - }, - { - "name": "kind", - "type": 38, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 101, - "optional": false - }, - { - "name": "kind", - "type": 40, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 101, - "optional": false - }, - { - "name": "kind", - "type": 42, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 101, - "optional": false - }, - { - "name": "kind", - "type": 44, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 101, - "optional": false - }, - { - "name": "kind", - "type": 46, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 101, + "type": 112, "optional": false }, { @@ -1998,7 +2035,7 @@ "properties": [ { "name": "form", - "type": 101, + "type": 112, "optional": false }, { @@ -2014,7 +2051,7 @@ "properties": [ { "name": "form", - "type": 101, + "type": 112, "optional": false }, { @@ -2030,7 +2067,7 @@ "properties": [ { "name": "form", - "type": 101, + "type": 112, "optional": false }, { @@ -2041,6 +2078,134 @@ ], "indices": [] }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 56, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 58, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 68, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 36, + "optional": false + } + ], + "indices": [] + }, { "kind": "object", "properties": [ @@ -2051,7 +2216,7 @@ }, { "name": "kind", - "type": 115, + "type": 126, "optional": false }, { @@ -2076,7 +2241,7 @@ "properties": [ { "name": "kind", - "type": 117, + "type": 128, "optional": false }, { @@ -2111,7 +2276,7 @@ "properties": [ { "name": "kind", - "type": 119, + "type": 130, "optional": false }, { @@ -2126,7 +2291,7 @@ }, { "name": "replayState", - "type": 120, + "type": 131, "optional": true } ], @@ -2140,124 +2305,6 @@ "kind": "opaque", "reason": "unknown" }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 57, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 59, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 61, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 36, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 126, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "ptc-mode" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 38, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 40, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 42, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 131, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "system-prompt" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 44, - "optional": false - } - ], - "indices": [] - }, { "kind": "object", "properties": [ @@ -2291,6 +2338,17 @@ ], "indices": [] }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, { "kind": "object", "properties": [ @@ -2302,6 +2360,113 @@ ], "indices": [] }, + { + "kind": "literal", + "value": "ptc-mode" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 56, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 58, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 142, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "system-prompt" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 60, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 62, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 148, + "optional": false + } + ], + "indices": [] + }, { "kind": "literal", "value": "tool-registry" @@ -2322,7 +2487,7 @@ "properties": [ { "name": "kind", - "type": 52, + "type": 68, "optional": false } ], @@ -2333,7 +2498,22 @@ "properties": [ { "name": "kind", - "type": 141, + "type": 152, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "working-directory" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 154, "optional": false } ], @@ -2348,7 +2528,7 @@ "properties": [ { "name": "kind", - "type": 54, + "type": 36, "optional": false } ], @@ -2369,17 +2549,17 @@ }, { "name": "changes", - "type": 144, + "type": 157, "optional": false }, { "name": "form", - "type": 56, + "type": 71, "optional": false }, { "name": "kind", - "type": 150, + "type": 163, "optional": false } ], @@ -2387,14 +2567,14 @@ }, { "kind": "array", - "element": 145 + "element": 158 }, { "kind": "object", "properties": [ { "name": "action", - "type": 146, + "type": 159, "optional": false }, { @@ -2418,9 +2598,9 @@ { "kind": "union", "types": [ - 147, - 148, - 149 + 160, + 161, + 162 ] }, { @@ -2449,12 +2629,12 @@ }, { "name": "kind", - "type": 152, + "type": 165, "optional": false }, { "name": "outcome", - "type": 153, + "type": 166, "optional": false } ], @@ -2478,7 +2658,7 @@ }, { "name": "kind", - "type": 155, + "type": 168, "optional": false } ], @@ -2519,7 +2699,7 @@ }, { "name": "kind", - "type": 158, + "type": 171, "optional": false }, { @@ -2544,12 +2724,12 @@ }, { "name": "form", - "type": 35, + "type": 40, "optional": false }, { "name": "kind", - "type": 160, + "type": 173, "optional": false }, { @@ -2584,17 +2764,17 @@ "properties": [ { "name": "entries", - "type": 162, + "type": 175, "optional": false }, { "name": "form", - "type": 101, + "type": 112, "optional": false }, { "name": "kind", - "type": 164, + "type": 177, "optional": false }, { @@ -2607,7 +2787,7 @@ }, { "kind": "array", - "element": 163 + "element": 176 }, { "kind": "object", @@ -2634,12 +2814,12 @@ "properties": [ { "name": "form", - "type": 56, + "type": 71, "optional": false }, { "name": "kind", - "type": 166, + "type": 179, "optional": false }, { @@ -2659,22 +2839,22 @@ "properties": [ { "name": "form", - "type": 73, + "type": 85, "optional": false }, { "name": "kind", - "type": 168, + "type": 181, "optional": false }, { "name": "references", - "type": 169, + "type": 182, "optional": false }, { "name": "version", - "type": 174, + "type": 187, "optional": false } ], @@ -2686,7 +2866,7 @@ }, { "kind": "array", - "element": 170 + "element": 183 }, { "kind": "object", @@ -2698,12 +2878,12 @@ }, { "name": "capturedThroughSeq", - "type": 171, + "type": 184, "optional": false }, { "name": "compacted", - "type": 173, + "type": 186, "optional": false }, { @@ -2743,7 +2923,7 @@ }, { "name": "truncated", - "type": 173, + "type": 186, "optional": false } ], @@ -2753,7 +2933,7 @@ "kind": "union", "types": [ 20, - 172 + 185 ] }, { @@ -2773,199 +2953,7 @@ "properties": [ { "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 57, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "array", - "element": 178 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 7, - "optional": false - }, - { - "name": "text", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 59, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 61, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 36, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 38, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 40, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 42, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 44, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, + "type": 35, "optional": false }, { @@ -2975,7 +2963,7 @@ }, { "name": "sections", - "type": 177, + "type": 37, "optional": false } ], @@ -2986,7 +2974,7 @@ "properties": [ { "name": "form", - "type": 176, + "type": 35, "optional": false }, { @@ -2996,7 +2984,7 @@ }, { "name": "sections", - "type": 177, + "type": 37, "optional": false } ], @@ -3007,7 +2995,7 @@ "properties": [ { "name": "form", - "type": 176, + "type": 35, "optional": false }, { @@ -3017,7 +3005,7 @@ }, { "name": "sections", - "type": 177, + "type": 37, "optional": false } ], @@ -3028,7 +3016,7 @@ "properties": [ { "name": "form", - "type": 176, + "type": 35, "optional": false }, { @@ -3038,7 +3026,7 @@ }, { "name": "sections", - "type": 177, + "type": 37, "optional": false } ], @@ -3049,7 +3037,7 @@ "properties": [ { "name": "form", - "type": 176, + "type": 35, "optional": false }, { @@ -3059,7 +3047,7 @@ }, { "name": "sections", - "type": 177, + "type": 37, "optional": false } ], @@ -3075,67 +3063,12 @@ }, { "name": "kind", - "type": 192, + "type": 56, "optional": false }, { - "name": "senderSessionId", - "type": 7, - "optional": false - }, - { - "name": "summary", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "subagent-settled" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 87, - "optional": false - }, - { - "name": "kind", - "type": 194, - "optional": false - }, - { - "name": "senderSessionId", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "agent-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 35, - "optional": false - }, - { - "name": "kind", - "type": 57, - "optional": false - }, - { - "name": "summary", - "type": 7, + "name": "sections", + "type": 37, "optional": false } ], @@ -3151,12 +3084,12 @@ }, { "name": "kind", - "type": 59, + "type": 58, "optional": false }, { - "name": "summary", - "type": 7, + "name": "sections", + "type": 37, "optional": false } ], @@ -3172,12 +3105,96 @@ }, { "name": "kind", - "type": 61, + "type": 60, "optional": false }, { - "name": "summary", - "type": 7, + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 62, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 66, + "optional": false + }, + { + "name": "sections", + "type": 37, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 35, + "optional": false + }, + { + "name": "kind", + "type": 68, + "optional": false + }, + { + "name": "sections", + "type": 37, "optional": false } ], @@ -3190,8 +3207,8 @@ { "kind": "union", "types": [ - 200, - 201 + 202, + 203 ] }, { @@ -6554,7 +6571,7 @@ "kind": "event", "event": "developer/message", "surface": true, - "digest": "186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467", + "digest": "ba5b10ea77d68f4a29439a9bd2a14f6a560acf55731749d57237757cc72fbc06", "schema": { "root": 0, "nodes": [ @@ -6578,12 +6595,12 @@ }, { "name": "sourceEventSeqs", - "type": 198, + "type": 200, "optional": true }, { "name": "surfaceOp", - "type": 199, + "type": 201, "optional": false }, { @@ -6593,7 +6610,7 @@ }, { "name": "type", - "type": 202, + "type": 204, "optional": false } ], @@ -6659,7 +6676,8 @@ "unknownKinds": "preserve", "attributionKinds": [ "tmux-context", - "user-question-reply" + "user-question-reply", + "working-directory" ] } } @@ -6965,29 +6983,24 @@ "kind": "union", "types": [ 33, - 36, 38, - 40, - 42, + 41, 44, 46, 48, 50, 52, 54, - 57, - 59, - 61, + 56, + 58, + 60, 62, - 63, 64, - 65, 66, - 67, 68, 69, - 70, 71, + 72, 73, 74, 75, @@ -6999,8 +7012,8 @@ 81, 82, 83, - 84, 85, + 86, 87, 88, 89, @@ -7014,6 +7027,7 @@ 97, 98, 99, + 100, 101, 102, 103, @@ -7024,56 +7038,61 @@ 108, 109, 110, - 111, 112, 113, + 114, 115, + 116, 117, + 118, + 119, 120, 121, 122, 123, 124, - 125, - 127, + 126, 128, - 129, - 130, + 131, 132, 133, 134, 135, 136, 138, + 139, 140, 141, 143, + 144, + 145, + 146, + 147, + 149, 151, + 152, 154, 156, - 157, - 159, - 161, - 165, + 164, 167, - 175, - 179, + 169, + 170, + 172, + 174, + 178, 180, - 181, - 182, - 183, - 184, - 185, - 186, - 187, 188, 189, 190, 191, + 192, 193, + 194, 195, 196, - 197 + 197, + 198, + 199 ] }, { @@ -7089,6 +7108,60 @@ "type": 35, "optional": false }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "user-approval" + }, + { + "kind": "array", + "element": 37 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 7, + "optional": false + }, + { + "name": "text", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 40, + "optional": false + }, + { + "name": "senderSessionId", + "type": 7, + "optional": false + }, { "name": "summary", "type": 7, @@ -7101,6 +7174,135 @@ "kind": "literal", "value": "notice" }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 43, + "optional": false + }, + { + "name": "senderSessionId", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 45, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "dsh-session-title-llm" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 47, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-claude-code" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 49, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-codex" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 39, + "optional": false + }, + { + "name": "kind", + "type": 51, + "optional": false + }, + { + "name": "summary", + "type": 7, + "optional": false + } + ], + "indices": [] + }, { "kind": "literal", "value": "model-selection" @@ -7110,12 +7312,12 @@ "properties": [ { "name": "form", - "type": 34, + "type": 39, "optional": false }, { "name": "kind", - "type": 37, + "type": 53, "optional": false }, { @@ -7135,12 +7337,12 @@ "properties": [ { "name": "form", - "type": 34, + "type": 39, "optional": false }, { "name": "kind", - "type": 39, + "type": 55, "optional": false }, { @@ -7160,12 +7362,12 @@ "properties": [ { "name": "form", - "type": 34, + "type": 39, "optional": false }, { "name": "kind", - "type": 41, + "type": 57, "optional": false }, { @@ -7185,12 +7387,12 @@ "properties": [ { "name": "form", - "type": 34, + "type": 39, "optional": false }, { "name": "kind", - "type": 43, + "type": 59, "optional": false }, { @@ -7210,12 +7412,12 @@ "properties": [ { "name": "form", - "type": 34, + "type": 39, "optional": false }, { "name": "kind", - "type": 45, + "type": 61, "optional": false }, { @@ -7235,12 +7437,12 @@ "properties": [ { "name": "form", - "type": 34, + "type": 39, "optional": false }, { "name": "kind", - "type": 47, + "type": 63, "optional": false }, { @@ -7260,12 +7462,12 @@ "properties": [ { "name": "form", - "type": 34, + "type": 39, "optional": false }, { "name": "kind", - "type": 49, + "type": 65, "optional": false }, { @@ -7285,12 +7487,12 @@ "properties": [ { "name": "form", - "type": 34, + "type": 39, "optional": false }, { "name": "kind", - "type": 51, + "type": 67, "optional": false }, { @@ -7310,12 +7512,12 @@ "properties": [ { "name": "form", - "type": 34, + "type": 39, "optional": false }, { "name": "kind", - "type": 53, + "type": 35, "optional": false }, { @@ -7326,160 +7528,12 @@ ], "indices": [] }, - { - "kind": "literal", - "value": "user-approval" - }, { "kind": "object", "properties": [ { "name": "form", - "type": 55, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "instructions" - }, - { - "kind": "literal", - "value": "dsh-session-title-llm" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 55, - "optional": false - }, - { - "name": "kind", - "type": 58, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "hooks-claude-code" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 55, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "hooks-codex" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 55, - "optional": false - }, - { - "name": "kind", - "type": 35, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 55, - "optional": false - }, - { - "name": "kind", - "type": 37, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 55, - "optional": false - }, - { - "name": "kind", - "type": 39, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 55, - "optional": false - }, - { - "name": "kind", - "type": 41, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 55, - "optional": false - }, - { - "name": "kind", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 55, + "type": 70, "optional": false }, { @@ -7490,12 +7544,16 @@ ], "indices": [] }, + { + "kind": "literal", + "value": "instructions" + }, { "kind": "object", "properties": [ { "name": "form", - "type": 55, + "type": 70, "optional": false }, { @@ -7511,7 +7569,7 @@ "properties": [ { "name": "form", - "type": 55, + "type": 70, "optional": false }, { @@ -7527,7 +7585,7 @@ "properties": [ { "name": "form", - "type": 55, + "type": 70, "optional": false }, { @@ -7543,7 +7601,7 @@ "properties": [ { "name": "form", - "type": 55, + "type": 70, "optional": false }, { @@ -7559,12 +7617,140 @@ "properties": [ { "name": "form", - "type": 72, + "type": 70, "optional": false }, { "name": "kind", - "type": 56, + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 63, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 65, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 70, + "optional": false + }, + { + "name": "kind", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 45, "optional": false } ], @@ -7579,135 +7765,7 @@ "properties": [ { "name": "form", - "type": 72, - "optional": false - }, - { - "name": "kind", - "type": 58, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 72, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 72, - "optional": false - }, - { - "name": "kind", - "type": 35, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 72, - "optional": false - }, - { - "name": "kind", - "type": 37, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 72, - "optional": false - }, - { - "name": "kind", - "type": 39, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 72, - "optional": false - }, - { - "name": "kind", - "type": 41, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 72, - "optional": false - }, - { - "name": "kind", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 72, - "optional": false - }, - { - "name": "kind", - "type": 45, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 72, + "type": 84, "optional": false }, { @@ -7723,7 +7781,7 @@ "properties": [ { "name": "form", - "type": 72, + "type": 84, "optional": false }, { @@ -7739,7 +7797,7 @@ "properties": [ { "name": "form", - "type": 72, + "type": 84, "optional": false }, { @@ -7755,7 +7813,7 @@ "properties": [ { "name": "form", - "type": 72, + "type": 84, "optional": false }, { @@ -7771,32 +7829,12 @@ "properties": [ { "name": "form", - "type": 86, + "type": 84, "optional": false }, { "name": "kind", - "type": 56, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "relay" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 58, + "type": 55, "optional": false } ], @@ -7807,12 +7845,12 @@ "properties": [ { "name": "form", - "type": 86, + "type": 84, "optional": false }, { "name": "kind", - "type": 60, + "type": 57, "optional": false } ], @@ -7823,7 +7861,87 @@ "properties": [ { "name": "form", - "type": 86, + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 63, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 65, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, + "optional": false + }, + { + "name": "kind", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 84, "optional": false }, { @@ -7839,71 +7957,7 @@ "properties": [ { "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 37, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 39, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 41, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, + "type": 42, "optional": false }, { @@ -7919,7 +7973,7 @@ "properties": [ { "name": "form", - "type": 86, + "type": 42, "optional": false }, { @@ -7935,7 +7989,7 @@ "properties": [ { "name": "form", - "type": 86, + "type": 42, "optional": false }, { @@ -7951,7 +8005,7 @@ "properties": [ { "name": "form", - "type": 86, + "type": 42, "optional": false }, { @@ -7967,7 +8021,7 @@ "properties": [ { "name": "form", - "type": 86, + "type": 42, "optional": false }, { @@ -7983,12 +8037,140 @@ "properties": [ { "name": "form", - "type": 100, + "type": 42, "optional": false }, { "name": "kind", - "type": 56, + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 63, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 65, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 42, + "optional": false + }, + { + "name": "kind", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 45, "optional": false } ], @@ -8003,135 +8185,7 @@ "properties": [ { "name": "form", - "type": 100, - "optional": false - }, - { - "name": "kind", - "type": 58, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 100, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 100, - "optional": false - }, - { - "name": "kind", - "type": 35, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 100, - "optional": false - }, - { - "name": "kind", - "type": 37, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 100, - "optional": false - }, - { - "name": "kind", - "type": 39, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 100, - "optional": false - }, - { - "name": "kind", - "type": 41, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 100, - "optional": false - }, - { - "name": "kind", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 100, - "optional": false - }, - { - "name": "kind", - "type": 45, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 100, + "type": 111, "optional": false }, { @@ -8147,7 +8201,7 @@ "properties": [ { "name": "form", - "type": 100, + "type": 111, "optional": false }, { @@ -8163,7 +8217,7 @@ "properties": [ { "name": "form", - "type": 100, + "type": 111, "optional": false }, { @@ -8179,7 +8233,7 @@ "properties": [ { "name": "form", - "type": 100, + "type": 111, "optional": false }, { @@ -8190,6 +8244,134 @@ ], "indices": [] }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 63, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 65, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 111, + "optional": false + }, + { + "name": "kind", + "type": 35, + "optional": false + } + ], + "indices": [] + }, { "kind": "object", "properties": [ @@ -8200,7 +8382,7 @@ }, { "name": "kind", - "type": 114, + "type": 125, "optional": false }, { @@ -8225,7 +8407,7 @@ "properties": [ { "name": "kind", - "type": 116, + "type": 127, "optional": false }, { @@ -8260,7 +8442,7 @@ "properties": [ { "name": "kind", - "type": 118, + "type": 129, "optional": false }, { @@ -8275,7 +8457,7 @@ }, { "name": "replayState", - "type": 119, + "type": 130, "optional": true } ], @@ -8289,124 +8471,6 @@ "kind": "opaque", "reason": "unknown" }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 56, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 58, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 60, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 35, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 37, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 126, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "ptc-mode" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 39, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 41, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 131, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "system-prompt" - }, { "kind": "object", "properties": [ @@ -8451,6 +8515,17 @@ ], "indices": [] }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 53, + "optional": false + } + ], + "indices": [] + }, { "kind": "object", "properties": [ @@ -8462,6 +8537,113 @@ ], "indices": [] }, + { + "kind": "literal", + "value": "ptc-mode" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 55, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 142, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "system-prompt" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 63, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 65, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 67, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 148, + "optional": false + } + ], + "indices": [] + }, { "kind": "literal", "value": "tool-registry" @@ -8471,7 +8653,7 @@ "properties": [ { "name": "kind", - "type": 139, + "type": 150, "optional": false } ], @@ -8486,7 +8668,7 @@ "properties": [ { "name": "kind", - "type": 53, + "type": 35, "optional": false } ], @@ -8497,7 +8679,22 @@ "properties": [ { "name": "kind", - "type": 142, + "type": 153, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "working-directory" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 155, "optional": false } ], @@ -8522,17 +8719,17 @@ }, { "name": "changes", - "type": 144, + "type": 157, "optional": false }, { "name": "form", - "type": 55, + "type": 70, "optional": false }, { "name": "kind", - "type": 150, + "type": 163, "optional": false } ], @@ -8540,14 +8737,14 @@ }, { "kind": "array", - "element": 145 + "element": 158 }, { "kind": "object", "properties": [ { "name": "action", - "type": 146, + "type": 159, "optional": false }, { @@ -8571,9 +8768,9 @@ { "kind": "union", "types": [ - 147, - 148, - 149 + 160, + 161, + 162 ] }, { @@ -8602,12 +8799,12 @@ }, { "name": "kind", - "type": 152, + "type": 165, "optional": false }, { "name": "outcome", - "type": 153, + "type": 166, "optional": false } ], @@ -8631,7 +8828,7 @@ }, { "name": "kind", - "type": 155, + "type": 168, "optional": false } ], @@ -8651,7 +8848,7 @@ }, { "name": "kind", - "type": 139, + "type": 150, "optional": false }, { @@ -8672,7 +8869,7 @@ }, { "name": "kind", - "type": 158, + "type": 171, "optional": false }, { @@ -8697,12 +8894,12 @@ }, { "name": "form", - "type": 34, + "type": 39, "optional": false }, { "name": "kind", - "type": 160, + "type": 173, "optional": false }, { @@ -8737,17 +8934,17 @@ "properties": [ { "name": "entries", - "type": 162, + "type": 175, "optional": false }, { "name": "form", - "type": 100, + "type": 111, "optional": false }, { "name": "kind", - "type": 164, + "type": 177, "optional": false }, { @@ -8760,7 +8957,7 @@ }, { "kind": "array", - "element": 163 + "element": 176 }, { "kind": "object", @@ -8787,12 +8984,12 @@ "properties": [ { "name": "form", - "type": 55, + "type": 70, "optional": false }, { "name": "kind", - "type": 166, + "type": 179, "optional": false }, { @@ -8812,22 +9009,22 @@ "properties": [ { "name": "form", - "type": 72, + "type": 84, "optional": false }, { "name": "kind", - "type": 168, + "type": 181, "optional": false }, { "name": "references", - "type": 169, + "type": 182, "optional": false }, { "name": "version", - "type": 174, + "type": 187, "optional": false } ], @@ -8839,7 +9036,7 @@ }, { "kind": "array", - "element": 170 + "element": 183 }, { "kind": "object", @@ -8851,12 +9048,12 @@ }, { "name": "capturedThroughSeq", - "type": 171, + "type": 184, "optional": false }, { "name": "compacted", - "type": 173, + "type": 186, "optional": false }, { @@ -8896,7 +9093,7 @@ }, { "name": "truncated", - "type": 173, + "type": 186, "optional": false } ], @@ -8906,7 +9103,7 @@ "kind": "union", "types": [ 2, - 172 + 185 ] }, { @@ -8926,199 +9123,7 @@ "properties": [ { "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "snapshot" - }, - { - "kind": "array", - "element": 178 - }, - { - "kind": "object", - "properties": [ - { - "name": "name", - "type": 7, - "optional": false - }, - { - "name": "text", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 58, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 60, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 35, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 37, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 39, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 41, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, - "optional": false - }, - { - "name": "kind", - "type": 43, - "optional": false - }, - { - "name": "sections", - "type": 177, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 176, + "type": 34, "optional": false }, { @@ -9128,7 +9133,7 @@ }, { "name": "sections", - "type": 177, + "type": 36, "optional": false } ], @@ -9139,7 +9144,7 @@ "properties": [ { "name": "form", - "type": 176, + "type": 34, "optional": false }, { @@ -9149,7 +9154,7 @@ }, { "name": "sections", - "type": 177, + "type": 36, "optional": false } ], @@ -9160,7 +9165,7 @@ "properties": [ { "name": "form", - "type": 176, + "type": 34, "optional": false }, { @@ -9170,7 +9175,7 @@ }, { "name": "sections", - "type": 177, + "type": 36, "optional": false } ], @@ -9181,7 +9186,7 @@ "properties": [ { "name": "form", - "type": 176, + "type": 34, "optional": false }, { @@ -9191,7 +9196,7 @@ }, { "name": "sections", - "type": 177, + "type": 36, "optional": false } ], @@ -9202,7 +9207,7 @@ "properties": [ { "name": "form", - "type": 176, + "type": 34, "optional": false }, { @@ -9212,7 +9217,7 @@ }, { "name": "sections", - "type": 177, + "type": 36, "optional": false } ], @@ -9228,67 +9233,12 @@ }, { "name": "kind", - "type": 192, + "type": 55, "optional": false }, { - "name": "senderSessionId", - "type": 7, - "optional": false - }, - { - "name": "summary", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "subagent-settled" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 194, - "optional": false - }, - { - "name": "senderSessionId", - "type": 7, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "agent-message" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 34, - "optional": false - }, - { - "name": "kind", - "type": 56, - "optional": false - }, - { - "name": "summary", - "type": 7, + "name": "sections", + "type": 36, "optional": false } ], @@ -9304,12 +9254,12 @@ }, { "name": "kind", - "type": 58, + "type": 57, "optional": false }, { - "name": "summary", - "type": 7, + "name": "sections", + "type": 36, "optional": false } ], @@ -9325,12 +9275,96 @@ }, { "name": "kind", - "type": 60, + "type": 59, "optional": false }, { - "name": "summary", - "type": 7, + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 61, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 63, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 65, + "optional": false + }, + { + "name": "sections", + "type": 36, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 34, + "optional": false + }, + { + "name": "kind", + "type": 67, + "optional": false + }, + { + "name": "sections", + "type": 36, "optional": false } ], @@ -9343,8 +9377,8 @@ { "kind": "union", "types": [ - 200, - 201 + 202, + 203 ] }, { @@ -9361,7 +9395,7 @@ }, { "name": "op", - "type": 148, + "type": 161, "optional": false }, { @@ -11856,7 +11890,7 @@ "kind": "event", "event": "session/title-llm-request", "surface": false, - "digest": "ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46", + "digest": "b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6", "schema": { "root": 0, "nodes": [ @@ -11885,7 +11919,7 @@ }, { "name": "type", - "type": 208, + "type": 210, "optional": false } ], @@ -11909,9 +11943,14 @@ "type": 4, "optional": false }, + { + "name": "reasoningEffort", + "type": 10, + "optional": true + }, { "name": "route", - "type": 207, + "type": 209, "optional": false }, { @@ -11944,9 +11983,9 @@ "types": [ 6, 37, - 202, - 203, - 205 + 204, + 205, + 207 ] }, { @@ -12330,7 +12369,8 @@ "unknownKinds": "preserve", "attributionKinds": [ "tmux-context", - "user-question-reply" + "user-question-reply", + "working-directory" ] } } @@ -12352,34 +12392,33 @@ 51, 53, 55, - 57, - 59, + 56, + 58, 60, 62, - 64, + 63, 65, - 68, + 67, + 69, 71, + 72, 73, - 75, - 77, + 76, 79, - 88, - 91, + 81, + 83, 92, - 94, - 96, + 95, + 97, 98, 100, - 105, - 107, - 115, - 116, - 117, - 118, + 102, + 104, + 109, + 111, 119, 120, - 122, + 121, 123, 124, 125, @@ -12443,17 +12482,19 @@ 183, 184, 185, + 186, 187, + 188, 189, + 191, 192, - 193, 194, - 195, - 196, 197, + 198, 199, 200, - 201 + 201, + 203 ] }, { @@ -12483,7 +12524,7 @@ }, { "kind": "literal", - "value": "user-approval" + "value": "tool-goal" }, { "kind": "array", @@ -12518,7 +12559,7 @@ }, { "kind": "literal", - "value": "system-prompt" + "value": "runtime-context" }, { "kind": "object", @@ -12533,7 +12574,7 @@ }, { "kind": "literal", - "value": "time-context" + "value": "schedule" }, { "kind": "object", @@ -12548,7 +12589,7 @@ }, { "kind": "literal", - "value": "tmux-context" + "value": "system-prompt" }, { "kind": "object", @@ -12563,7 +12604,7 @@ }, { "kind": "literal", - "value": "tool-goal" + "value": "time-context" }, { "kind": "object", @@ -12578,37 +12619,7 @@ }, { "kind": "literal", - "value": "tool-jobs" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 56, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "tool-registry" - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 58, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "user" + "value": "tmux-context" }, { "kind": "object", @@ -12621,6 +12632,36 @@ ], "indices": [] }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-jobs" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-registry" + }, { "kind": "object", "properties": [ @@ -12632,6 +12673,72 @@ ], "indices": [] }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user-approval" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "working-directory" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 68, + "optional": false + } + ], + "indices": [] + }, { "kind": "literal", "value": "cordis-host-runner" @@ -12639,19 +12746,9 @@ { "kind": "object", "properties": [ - { - "name": "form", - "type": 41, - "optional": false - }, { "name": "kind", - "type": 63, - "optional": false - }, - { - "name": "sections", - "type": 43, + "type": 70, "optional": false } ], @@ -12664,9 +12761,19 @@ { "kind": "object", "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, { "name": "kind", - "type": 63, + "type": 64, + "optional": false + }, + { + "name": "sections", + "type": 43, "optional": false } ], @@ -12677,12 +12784,33 @@ "properties": [ { "name": "form", - "type": 66, + "type": 41, "optional": false }, { "name": "kind", - "type": 67, + "type": 70, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 75, "optional": false }, { @@ -12711,12 +12839,12 @@ "properties": [ { "name": "form", - "type": 69, + "type": 77, "optional": false }, { "name": "kind", - "type": 70, + "type": 78, "optional": false }, { @@ -12740,12 +12868,12 @@ "properties": [ { "name": "form", - "type": 66, + "type": 74, "optional": false }, { "name": "kind", - "type": 72, + "type": 80, "optional": false }, { @@ -12765,12 +12893,12 @@ "properties": [ { "name": "form", - "type": 66, + "type": 74, "optional": false }, { "name": "kind", - "type": 74, + "type": 82, "optional": false }, { @@ -12785,56 +12913,6 @@ "kind": "literal", "value": "hooks-codex" }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 76, - "optional": false - }, - { - "name": "summary", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "model-selection" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 78, - "optional": false - }, - { - "name": "summary", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "plan-mode" - }, { "kind": "object", "properties": [ @@ -12850,17 +12928,17 @@ }, { "name": "changes", - "type": 80, + "type": 84, "optional": false }, { "name": "form", - "type": 86, + "type": 90, "optional": false }, { "name": "kind", - "type": 87, + "type": 91, "optional": false } ], @@ -12868,14 +12946,14 @@ }, { "kind": "array", - "element": 81 + "element": 85 }, { "kind": "object", "properties": [ { "name": "action", - "type": 82, + "type": 86, "optional": false }, { @@ -12899,9 +12977,9 @@ { "kind": "union", "types": [ - 83, - 84, - 85 + 87, + 88, + 89 ] }, { @@ -12934,12 +13012,12 @@ }, { "name": "kind", - "type": 89, + "type": 93, "optional": false }, { "name": "outcome", - "type": 90, + "type": 94, "optional": false } ], @@ -12953,6 +13031,31 @@ "kind": "literal", "value": "answered" }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model-selection" + }, { "kind": "object", "properties": [ @@ -12963,7 +13066,7 @@ }, { "name": "kind", - "type": 58, + "type": 61, "optional": false }, { @@ -12974,31 +13077,6 @@ ], "indices": [] }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 93, - "optional": false - }, - { - "name": "summary", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "repeat-tool-reminder" - }, { "kind": "object", "properties": [ @@ -13009,7 +13087,7 @@ }, { "name": "kind", - "type": 95, + "type": 99, "optional": false }, { @@ -13024,6 +13102,31 @@ "kind": "literal", "value": "compact-checkpoint" }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "plan-mode" + }, { "kind": "object", "properties": [ @@ -13034,12 +13137,12 @@ }, { "name": "form", - "type": 66, + "type": 74, "optional": false }, { "name": "kind", - "type": 97, + "type": 103, "optional": false }, { @@ -13069,47 +13172,22 @@ "kind": "literal", "value": "webhook" }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 99, - "optional": false - }, - { - "name": "summary", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "runtime-context" - }, { "kind": "object", "properties": [ { "name": "entries", - "type": 101, + "type": 105, "optional": false }, { "name": "form", - "type": 103, + "type": 107, "optional": false }, { "name": "kind", - "type": 104, + "type": 108, "optional": false }, { @@ -13122,7 +13200,7 @@ }, { "kind": "array", - "element": 102 + "element": 106 }, { "kind": "object", @@ -13153,12 +13231,12 @@ "properties": [ { "name": "form", - "type": 86, + "type": 90, "optional": false }, { "name": "kind", - "type": 106, + "type": 110, "optional": false }, { @@ -13178,22 +13256,22 @@ "properties": [ { "name": "form", - "type": 108, + "type": 112, "optional": false }, { "name": "kind", - "type": 109, + "type": 113, "optional": false }, { "name": "references", - "type": 110, + "type": 114, "optional": false }, { "name": "version", - "type": 114, + "type": 118, "optional": false } ], @@ -13209,7 +13287,7 @@ }, { "kind": "array", - "element": 111 + "element": 115 }, { "kind": "object", @@ -13221,7 +13299,7 @@ }, { "name": "capturedThroughSeq", - "type": 112, + "type": 116, "optional": false }, { @@ -13276,7 +13354,7 @@ "kind": "union", "types": [ 2, - 113 + 117 ] }, { @@ -13297,7 +13375,7 @@ }, { "name": "kind", - "type": 72, + "type": 80, "optional": false }, { @@ -13318,91 +13396,28 @@ }, { "name": "kind", + "type": 82, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", "type": 74, "optional": false }, - { - "name": "sections", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 41, - "optional": false - }, { "name": "kind", - "type": 76, - "optional": false - }, - { - "name": "sections", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 41, - "optional": false - }, - { - "name": "kind", - "type": 78, - "optional": false - }, - { - "name": "sections", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 41, - "optional": false - }, - { - "name": "kind", - "type": 93, - "optional": false - }, - { - "name": "sections", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 121, + "type": 122, "optional": false }, { @@ -13415,24 +13430,24 @@ }, { "kind": "literal", - "value": "schedule" + "value": "repeat-tool-reminder" }, { "kind": "object", "properties": [ { "name": "form", - "type": 66, + "type": 41, "optional": false }, { "name": "kind", - "type": 48, + "type": 96, "optional": false }, { - "name": "summary", - "type": 10, + "name": "sections", + "type": 43, "optional": false } ], @@ -13443,149 +13458,17 @@ "properties": [ { "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 50, - "optional": false - }, - { - "name": "summary", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 52, - "optional": false - }, - { - "name": "summary", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "summary", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 42, - "optional": false - }, - { - "name": "summary", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 66, - "optional": false - }, - { - "name": "kind", - "type": 63, - "optional": false - }, - { - "name": "summary", - "type": 10, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 72, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", "type": 74, "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false }, { "name": "kind", - "type": 76, + "type": 46, + "optional": false + }, + { + "name": "summary", + "type": 10, "optional": false } ], @@ -13596,342 +13479,17 @@ "properties": [ { "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 78, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 93, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 99, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 121, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 48, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 50, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 52, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 41, - "optional": false - }, - { - "name": "kind", - "type": 99, - "optional": false - }, - { - "name": "sections", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 42, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 86, - "optional": false - }, - { - "name": "kind", - "type": 63, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 108, - "optional": false - }, - { - "name": "kind", - "type": 72, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 108, - "optional": false - }, - { - "name": "kind", "type": 74, "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 108, - "optional": false - }, - { - "name": "kind", - "type": 76, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 108, - "optional": false - }, - { - "name": "kind", - "type": 78, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 108, - "optional": false - }, - { - "name": "kind", - "type": 93, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 108, - "optional": false - }, - { - "name": "kind", - "type": 99, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 41, - "optional": false - }, - { - "name": "kind", - "type": 121, - "optional": false - }, - { - "name": "sections", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 108, - "optional": false - }, - { - "name": "kind", - "type": 121, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 108, - "optional": false }, { "name": "kind", "type": 48, "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 108, - "optional": false }, { - "name": "kind", - "type": 50, + "name": "summary", + "type": 10, "optional": false } ], @@ -13942,257 +13500,17 @@ "properties": [ { "name": "form", - "type": 108, - "optional": false - }, - { - "name": "kind", - "type": 52, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 108, - "optional": false - }, - { - "name": "kind", - "type": 54, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 108, - "optional": false - }, - { - "name": "kind", - "type": 42, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 108, - "optional": false - }, - { - "name": "kind", - "type": 63, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 69, - "optional": false - }, - { - "name": "kind", - "type": 72, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 69, - "optional": false - }, - { - "name": "kind", "type": 74, "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 41, - "optional": false - }, - { - "name": "kind", - "type": 48, - "optional": false - }, - { - "name": "sections", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 69, - "optional": false - }, - { - "name": "kind", - "type": 76, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 69, - "optional": false - }, - { - "name": "kind", - "type": 78, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 69, - "optional": false - }, - { - "name": "kind", - "type": 93, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 69, - "optional": false - }, - { - "name": "kind", - "type": 99, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 69, - "optional": false - }, - { - "name": "kind", - "type": 121, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 69, - "optional": false - }, - { - "name": "kind", - "type": 48, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 69, - "optional": false - }, - { - "name": "kind", - "type": 50, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 69, - "optional": false }, { "name": "kind", "type": 52, "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 69, - "optional": false }, { - "name": "kind", - "type": 54, + "name": "summary", + "type": 10, "optional": false } ], @@ -14203,82 +13521,18 @@ "properties": [ { "name": "form", - "type": 41, - "optional": false - }, - { - "name": "kind", - "type": 50, - "optional": false - }, - { - "name": "sections", - "type": 43, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 69, - "optional": false - }, - { - "name": "kind", - "type": 42, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 69, - "optional": false - }, - { - "name": "kind", - "type": 63, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 103, - "optional": false - }, - { - "name": "kind", - "type": 72, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 103, - "optional": false - }, - { - "name": "kind", "type": 74, "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false } ], "indices": [] @@ -14288,12 +13542,17 @@ "properties": [ { "name": "form", - "type": 103, + "type": 74, "optional": false }, { "name": "kind", - "type": 76, + "type": 42, + "optional": false + }, + { + "name": "summary", + "type": 10, "optional": false } ], @@ -14304,12 +13563,17 @@ "properties": [ { "name": "form", - "type": 103, + "type": 74, "optional": false }, { "name": "kind", - "type": 78, + "type": 57, + "optional": false + }, + { + "name": "summary", + "type": 10, "optional": false } ], @@ -14320,12 +13584,17 @@ "properties": [ { "name": "form", - "type": 103, + "type": 74, "optional": false }, { "name": "kind", - "type": 93, + "type": 64, + "optional": false + }, + { + "name": "summary", + "type": 10, "optional": false } ], @@ -14336,12 +13605,17 @@ "properties": [ { "name": "form", - "type": 103, + "type": 74, "optional": false }, { "name": "kind", - "type": 99, + "type": 70, + "optional": false + }, + { + "name": "summary", + "type": 10, "optional": false } ], @@ -14352,12 +13626,12 @@ "properties": [ { "name": "form", - "type": 103, + "type": 90, "optional": false }, { "name": "kind", - "type": 121, + "type": 80, "optional": false } ], @@ -14368,7 +13642,87 @@ "properties": [ { "name": "form", - "type": 103, + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, "optional": false }, { @@ -14379,6 +13733,698 @@ ], "indices": [] }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, { "kind": "object", "properties": [ @@ -14405,12 +14451,12 @@ "properties": [ { "name": "form", - "type": 103, + "type": 107, "optional": false }, { "name": "kind", - "type": 50, + "type": 48, "optional": false } ], @@ -14421,7 +14467,7 @@ "properties": [ { "name": "form", - "type": 103, + "type": 107, "optional": false }, { @@ -14437,7 +14483,7 @@ "properties": [ { "name": "form", - "type": 103, + "type": 107, "optional": false }, { @@ -14453,7 +14499,7 @@ "properties": [ { "name": "form", - "type": 103, + "type": 107, "optional": false }, { @@ -14469,12 +14515,44 @@ "properties": [ { "name": "form", - "type": 103, + "type": 107, "optional": false }, { "name": "kind", - "type": 63, + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 70, "optional": false } ], @@ -14490,7 +14568,7 @@ }, { "name": "kind", - "type": 186, + "type": 190, "optional": false }, { @@ -14513,9 +14591,30 @@ { "kind": "object", "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, { "name": "kind", - "type": 188, + "type": 54, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 193, "optional": false }, { @@ -14550,7 +14649,7 @@ "properties": [ { "name": "kind", - "type": 190, + "type": 195, "optional": false }, { @@ -14565,7 +14664,7 @@ }, { "name": "replayState", - "type": 191, + "type": 196, "optional": true } ], @@ -14582,19 +14681,9 @@ { "kind": "object", "properties": [ - { - "name": "form", - "type": 41, - "optional": false - }, { "name": "kind", - "type": 54, - "optional": false - }, - { - "name": "sections", - "type": 43, + "type": 80, "optional": false } ], @@ -14605,7 +14694,7 @@ "properties": [ { "name": "kind", - "type": 72, + "type": 82, "optional": false } ], @@ -14616,7 +14705,7 @@ "properties": [ { "name": "kind", - "type": 74, + "type": 96, "optional": false } ], @@ -14627,7 +14716,7 @@ "properties": [ { "name": "kind", - "type": 76, + "type": 101, "optional": false } ], @@ -14638,18 +14727,7 @@ "properties": [ { "name": "kind", - "type": 78, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 198, + "type": 202, "optional": false } ], @@ -14664,29 +14742,7 @@ "properties": [ { "name": "kind", - "type": 93, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 99, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 121, + "type": 122, "optional": false } ], @@ -14707,7 +14763,7 @@ }, { "name": "role", - "type": 58, + "type": 61, "optional": false }, { @@ -14722,7 +14778,8 @@ "unknownKinds": "preserve", "attributionKinds": [ "tmux-context", - "user-question-reply" + "user-question-reply", + "working-directory" ] } } @@ -14744,12 +14801,12 @@ }, { "name": "role", - "type": 204, + "type": 206, "optional": false }, { "name": "source", - "type": 189, + "type": 194, "optional": false } ], @@ -14774,12 +14831,12 @@ }, { "name": "role", - "type": 206, + "type": 208, "optional": false }, { "name": "source", - "type": 45, + "type": 49, "optional": false } ], @@ -14959,7 +15016,7 @@ "kind": "event", "event": "subagent/catalog", "surface": false, - "digest": "3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604", + "digest": "53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac", "schema": { "root": 0, "nodes": [ @@ -14973,7 +15030,7 @@ }, { "name": "ignorable", - "type": 14, + "type": 17, "optional": true }, { @@ -14988,7 +15045,7 @@ }, { "name": "type", - "type": 15, + "type": 18, "optional": false } ], @@ -15000,8 +15057,9 @@ 2, 7, 9, - 11, - 12 + 12, + 14, + 15 ] }, { @@ -15109,6 +15167,45 @@ "type": 10, "optional": false }, + { + "name": "version", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "external" + }, + { + "kind": "literal", + "value": 2 + }, + { + "kind": "object", + "properties": [ + { + "name": "childCreatedAt", + "type": 3, + "optional": false + }, + { + "name": "childId", + "type": 4, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": true + }, + { + "name": "mode", + "type": 13, + "optional": false + }, { "name": "version", "type": 6, @@ -15141,7 +15238,7 @@ }, { "name": "mode", - "type": 10, + "type": 13, "optional": false }, { @@ -15172,7 +15269,7 @@ }, { "name": "mode", - "type": 13, + "type": 16, "optional": false }, { @@ -17288,7 +17385,7 @@ "kind": "event", "event": "tool/ptc-dispatch", "surface": false, - "digest": "100f6dca1468538239522cde3533e5bd721d0f1a7b50bea8b0eb533ea6c96163", + "digest": "d66e11dddf5dfa3c71547e2cbd7b0608824cd8d6243fc16f532017ff28e8fba5", "schema": { "root": 0, "nodes": [ @@ -17302,22 +17399,22 @@ }, { "name": "ignorable", - "type": 26, + "type": 29, "optional": true }, { "name": "seq", - "type": 19, + "type": 17, "optional": false }, { "name": "time", - "type": 19, + "type": 17, "optional": false }, { "name": "type", - "type": 33, + "type": 37, "optional": false } ], @@ -17346,6 +17443,11 @@ "type": 32, "optional": false }, + { + "name": "meta", + "type": 33, + "optional": true + }, { "name": "name", "type": 6, @@ -17385,8 +17487,8 @@ 10, 13, 15, - 17, - 28 + 19, + 21 ] }, { @@ -17482,6 +17584,51 @@ "kind": "literal", "value": "tool-removal" }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 16, + "optional": false + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 6, + "optional": false + }, + { + "name": "bytes", + "type": 17, + "optional": false + }, + { + "name": "name", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "file" + }, { "kind": "object", "properties": [ @@ -17502,7 +17649,7 @@ }, { "name": "type", - "type": 16, + "type": 20, "optional": false } ], @@ -17517,17 +17664,17 @@ "properties": [ { "name": "attachment", - "type": 18, + "type": 22, "optional": false }, { "name": "offloaded", - "type": 26, + "type": 29, "optional": true }, { "name": "type", - "type": 27, + "type": 30, "optional": false } ], @@ -17543,17 +17690,17 @@ }, { "name": "bytes", - "type": 19, + "type": 17, "optional": false }, { "name": "height", - "type": 19, + "type": 17, "optional": false }, { "name": "mediaType", - "type": 20, + "type": 23, "optional": false }, { @@ -17563,28 +17710,24 @@ }, { "name": "originalDimensions", - "type": 25, + "type": 28, "optional": true }, { "name": "width", - "type": 19, + "type": 17, "optional": false } ], "indices": [] }, - { - "kind": "primitive", - "type": "number" - }, { "kind": "union", "types": [ - 21, - 22, - 23, - 24 + 24, + 25, + 26, + 27 ] }, { @@ -17608,12 +17751,12 @@ "properties": [ { "name": "height", - "type": 19, + "type": 17, "optional": false }, { "name": "width", - "type": 19, + "type": 17, "optional": false } ], @@ -17627,47 +17770,6 @@ "kind": "literal", "value": "image" }, - { - "kind": "object", - "properties": [ - { - "name": "attachment", - "type": 29, - "optional": false - }, - { - "name": "type", - "type": 30, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "attachmentId", - "type": 6, - "optional": false - }, - { - "name": "bytes", - "type": 19, - "optional": false - }, - { - "name": "name", - "type": 6, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "file" - }, { "kind": "object", "properties": [ @@ -17693,6 +17795,35 @@ "kind": "primitive", "type": "boolean" }, + { + "kind": "union", + "types": [ + 6, + 17, + 32, + 34, + 35, + 36 + ] + }, + { + "kind": "array", + "element": 33 + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "object", + "properties": [], + "indices": [ + { + "key": 6, + "value": 33 + } + ] + }, { "kind": "literal", "value": "tool/ptc-dispatch" @@ -18726,7 +18857,7 @@ "kind": "event", "event": "user/message", "surface": true, - "digest": "b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411", + "digest": "2ace3b8059b82b6d9837fed518ca97a99eb37c4482df7aad0e71c85489428ade", "schema": { "root": 0, "nodes": [ @@ -18750,12 +18881,12 @@ }, { "name": "sourceEventSeqs", - "type": 196, + "type": 198, "optional": true }, { "name": "surfaceOp", - "type": 197, + "type": 199, "optional": false }, { @@ -18765,7 +18896,7 @@ }, { "name": "type", - "type": 200, + "type": 202, "optional": false } ], @@ -18801,7 +18932,8 @@ "unknownKinds": "preserve", "attributionKinds": [ "tmux-context", - "user-question-reply" + "user-question-reply", + "working-directory" ] } } @@ -19120,7 +19252,7 @@ 47, 49, 51, - 54, + 53, 56, 58, 60, @@ -19133,7 +19265,7 @@ 67, 68, 69, - 71, + 70, 72, 73, 74, @@ -19146,7 +19278,7 @@ 81, 82, 83, - 85, + 84, 86, 87, 88, @@ -19159,7 +19291,7 @@ 95, 96, 97, - 99, + 98, 100, 101, 102, @@ -19172,39 +19304,38 @@ 109, 110, 111, - 113, - 115, - 118, + 112, + 114, + 116, 119, 120, 121, 122, - 124, + 123, 125, 126, 127, - 129, + 128, 130, 131, 132, 133, - 135, + 134, 136, 137, - 139, + 138, 140, - 148, + 142, + 143, 151, - 153, 154, 156, - 158, - 162, - 164, - 172, - 176, - 177, - 178, + 157, + 159, + 161, + 165, + 167, + 175, 179, 180, 181, @@ -19215,11 +19346,13 @@ 186, 187, 188, + 189, 190, - 192, + 191, 193, - 194, - 195 + 195, + 196, + 197 ] }, { @@ -19249,7 +19382,7 @@ }, { "kind": "literal", - "value": "repeat-tool-reminder" + "value": "plan-mode" }, { "kind": "object", @@ -19272,6 +19405,31 @@ ], "indices": [] }, + { + "kind": "literal", + "value": "repeat-tool-reminder" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 33, + "optional": false + }, + { + "name": "kind", + "type": 38, + "optional": false + }, + { + "name": "summary", + "type": 5, + "optional": false + } + ], + "indices": [] + }, { "kind": "literal", "value": "runtime-context" @@ -19286,7 +19444,7 @@ }, { "name": "kind", - "type": 38, + "type": 40, "optional": false }, { @@ -19311,7 +19469,7 @@ }, { "name": "kind", - "type": 40, + "type": 42, "optional": false }, { @@ -19336,7 +19494,7 @@ }, { "name": "kind", - "type": 42, + "type": 44, "optional": false }, { @@ -19361,7 +19519,7 @@ }, { "name": "kind", - "type": 44, + "type": 46, "optional": false }, { @@ -19386,7 +19544,7 @@ }, { "name": "kind", - "type": 46, + "type": 48, "optional": false }, { @@ -19411,7 +19569,7 @@ }, { "name": "kind", - "type": 48, + "type": 50, "optional": false }, { @@ -19436,7 +19594,7 @@ }, { "name": "kind", - "type": 50, + "type": 52, "optional": false }, { @@ -19456,12 +19614,12 @@ "properties": [ { "name": "form", - "type": 52, + "type": 54, "optional": false }, { "name": "kind", - "type": 53, + "type": 55, "optional": false } ], @@ -19480,12 +19638,12 @@ "properties": [ { "name": "form", - "type": 52, + "type": 54, "optional": false }, { "name": "kind", - "type": 55, + "type": 57, "optional": false } ], @@ -19500,12 +19658,12 @@ "properties": [ { "name": "form", - "type": 52, + "type": 54, "optional": false }, { "name": "kind", - "type": 57, + "type": 59, "optional": false } ], @@ -19520,27 +19678,7 @@ "properties": [ { "name": "form", - "type": 52, - "optional": false - }, - { - "name": "kind", - "type": 59, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "literal", - "value": "plan-mode" - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 52, + "type": 54, "optional": false }, { @@ -19556,7 +19694,7 @@ "properties": [ { "name": "form", - "type": 52, + "type": 54, "optional": false }, { @@ -19572,7 +19710,7 @@ "properties": [ { "name": "form", - "type": 52, + "type": 54, "optional": false }, { @@ -19588,7 +19726,7 @@ "properties": [ { "name": "form", - "type": 52, + "type": 54, "optional": false }, { @@ -19604,7 +19742,7 @@ "properties": [ { "name": "form", - "type": 52, + "type": 54, "optional": false }, { @@ -19620,7 +19758,7 @@ "properties": [ { "name": "form", - "type": 52, + "type": 54, "optional": false }, { @@ -19636,7 +19774,7 @@ "properties": [ { "name": "form", - "type": 52, + "type": 54, "optional": false }, { @@ -19652,7 +19790,7 @@ "properties": [ { "name": "form", - "type": 52, + "type": 54, "optional": false }, { @@ -19668,7 +19806,7 @@ "properties": [ { "name": "form", - "type": 52, + "type": 54, "optional": false }, { @@ -19684,12 +19822,28 @@ "properties": [ { "name": "form", - "type": 70, + "type": 54, "optional": false }, { "name": "kind", - "type": 53, + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 71, + "optional": false + }, + { + "name": "kind", + "type": 55, "optional": false } ], @@ -19704,23 +19858,7 @@ "properties": [ { "name": "form", - "type": 70, - "optional": false - }, - { - "name": "kind", - "type": 55, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 70, + "type": 71, "optional": false }, { @@ -19736,7 +19874,7 @@ "properties": [ { "name": "form", - "type": 70, + "type": 71, "optional": false }, { @@ -19752,7 +19890,7 @@ "properties": [ { "name": "form", - "type": 70, + "type": 71, "optional": false }, { @@ -19768,7 +19906,7 @@ "properties": [ { "name": "form", - "type": 70, + "type": 71, "optional": false }, { @@ -19784,7 +19922,7 @@ "properties": [ { "name": "form", - "type": 70, + "type": 71, "optional": false }, { @@ -19800,7 +19938,7 @@ "properties": [ { "name": "form", - "type": 70, + "type": 71, "optional": false }, { @@ -19816,7 +19954,7 @@ "properties": [ { "name": "form", - "type": 70, + "type": 71, "optional": false }, { @@ -19832,7 +19970,7 @@ "properties": [ { "name": "form", - "type": 70, + "type": 71, "optional": false }, { @@ -19848,7 +19986,7 @@ "properties": [ { "name": "form", - "type": 70, + "type": 71, "optional": false }, { @@ -19864,7 +20002,7 @@ "properties": [ { "name": "form", - "type": 70, + "type": 71, "optional": false }, { @@ -19880,7 +20018,7 @@ "properties": [ { "name": "form", - "type": 70, + "type": 71, "optional": false }, { @@ -19896,12 +20034,28 @@ "properties": [ { "name": "form", - "type": 84, + "type": 71, "optional": false }, { "name": "kind", - "type": 53, + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 85, + "optional": false + }, + { + "name": "kind", + "type": 55, "optional": false } ], @@ -19916,23 +20070,7 @@ "properties": [ { "name": "form", - "type": 84, - "optional": false - }, - { - "name": "kind", - "type": 55, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 84, + "type": 85, "optional": false }, { @@ -19948,7 +20086,7 @@ "properties": [ { "name": "form", - "type": 84, + "type": 85, "optional": false }, { @@ -19964,7 +20102,7 @@ "properties": [ { "name": "form", - "type": 84, + "type": 85, "optional": false }, { @@ -19980,7 +20118,7 @@ "properties": [ { "name": "form", - "type": 84, + "type": 85, "optional": false }, { @@ -19996,7 +20134,7 @@ "properties": [ { "name": "form", - "type": 84, + "type": 85, "optional": false }, { @@ -20012,7 +20150,7 @@ "properties": [ { "name": "form", - "type": 84, + "type": 85, "optional": false }, { @@ -20028,7 +20166,7 @@ "properties": [ { "name": "form", - "type": 84, + "type": 85, "optional": false }, { @@ -20044,7 +20182,7 @@ "properties": [ { "name": "form", - "type": 84, + "type": 85, "optional": false }, { @@ -20060,7 +20198,7 @@ "properties": [ { "name": "form", - "type": 84, + "type": 85, "optional": false }, { @@ -20076,7 +20214,7 @@ "properties": [ { "name": "form", - "type": 84, + "type": 85, "optional": false }, { @@ -20092,7 +20230,7 @@ "properties": [ { "name": "form", - "type": 84, + "type": 85, "optional": false }, { @@ -20108,12 +20246,28 @@ "properties": [ { "name": "form", - "type": 98, + "type": 85, "optional": false }, { "name": "kind", - "type": 53, + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 55, "optional": false } ], @@ -20128,23 +20282,7 @@ "properties": [ { "name": "form", - "type": 98, - "optional": false - }, - { - "name": "kind", - "type": 55, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 98, + "type": 99, "optional": false }, { @@ -20160,7 +20298,7 @@ "properties": [ { "name": "form", - "type": 98, + "type": 99, "optional": false }, { @@ -20176,7 +20314,7 @@ "properties": [ { "name": "form", - "type": 98, + "type": 99, "optional": false }, { @@ -20192,7 +20330,7 @@ "properties": [ { "name": "form", - "type": 98, + "type": 99, "optional": false }, { @@ -20208,7 +20346,7 @@ "properties": [ { "name": "form", - "type": 98, + "type": 99, "optional": false }, { @@ -20224,7 +20362,7 @@ "properties": [ { "name": "form", - "type": 98, + "type": 99, "optional": false }, { @@ -20240,7 +20378,7 @@ "properties": [ { "name": "form", - "type": 98, + "type": 99, "optional": false }, { @@ -20256,7 +20394,7 @@ "properties": [ { "name": "form", - "type": 98, + "type": 99, "optional": false }, { @@ -20272,7 +20410,7 @@ "properties": [ { "name": "form", - "type": 98, + "type": 99, "optional": false }, { @@ -20288,7 +20426,7 @@ "properties": [ { "name": "form", - "type": 98, + "type": 99, "optional": false }, { @@ -20304,7 +20442,7 @@ "properties": [ { "name": "form", - "type": 98, + "type": 99, "optional": false }, { @@ -20315,6 +20453,22 @@ ], "indices": [] }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 99, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, { "kind": "object", "properties": [ @@ -20325,7 +20479,7 @@ }, { "name": "kind", - "type": 112, + "type": 113, "optional": false }, { @@ -20350,7 +20504,7 @@ "properties": [ { "name": "kind", - "type": 114, + "type": 115, "optional": false }, { @@ -20385,7 +20539,7 @@ "properties": [ { "name": "kind", - "type": 116, + "type": 117, "optional": false }, { @@ -20400,7 +20554,7 @@ }, { "name": "replayState", - "type": 117, + "type": 118, "optional": true } ], @@ -20414,17 +20568,6 @@ "kind": "opaque", "reason": "unknown" }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 53, - "optional": false - } - ], - "indices": [] - }, { "kind": "object", "properties": [ @@ -20463,7 +20606,18 @@ "properties": [ { "name": "kind", - "type": 123, + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 124, "optional": false } ], @@ -20473,17 +20627,6 @@ "kind": "literal", "value": "ptc-mode" }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 34, - "optional": false - } - ], - "indices": [] - }, { "kind": "object", "properties": [ @@ -20511,7 +20654,18 @@ "properties": [ { "name": "kind", - "type": 128, + "type": 40, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 129, "optional": false } ], @@ -20521,17 +20675,6 @@ "kind": "literal", "value": "system-prompt" }, - { - "kind": "object", - "properties": [ - { - "name": "kind", - "type": 40, - "optional": false - } - ], - "indices": [] - }, { "kind": "object", "properties": [ @@ -20570,7 +20713,18 @@ "properties": [ { "name": "kind", - "type": 134, + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 135, "optional": false } ], @@ -20596,7 +20750,7 @@ "properties": [ { "name": "kind", - "type": 48, + "type": 50, "optional": false } ], @@ -20607,7 +20761,22 @@ "properties": [ { "name": "kind", - "type": 138, + "type": 139, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "working-directory" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 141, "optional": false } ], @@ -20622,7 +20791,7 @@ "properties": [ { "name": "kind", - "type": 50, + "type": 52, "optional": false } ], @@ -20643,17 +20812,17 @@ }, { "name": "changes", - "type": 141, + "type": 144, "optional": false }, { "name": "form", - "type": 52, + "type": 54, "optional": false }, { "name": "kind", - "type": 147, + "type": 150, "optional": false } ], @@ -20661,14 +20830,14 @@ }, { "kind": "array", - "element": 142 + "element": 145 }, { "kind": "object", "properties": [ { "name": "action", - "type": 143, + "type": 146, "optional": false }, { @@ -20692,9 +20861,9 @@ { "kind": "union", "types": [ - 144, - 145, - 146 + 147, + 148, + 149 ] }, { @@ -20723,12 +20892,12 @@ }, { "name": "kind", - "type": 149, + "type": 152, "optional": false }, { "name": "outcome", - "type": 150, + "type": 153, "optional": false } ], @@ -20752,7 +20921,7 @@ }, { "name": "kind", - "type": 152, + "type": 155, "optional": false } ], @@ -20793,7 +20962,7 @@ }, { "name": "kind", - "type": 155, + "type": 158, "optional": false }, { @@ -20823,7 +20992,7 @@ }, { "name": "kind", - "type": 157, + "type": 160, "optional": false }, { @@ -20858,17 +21027,17 @@ "properties": [ { "name": "entries", - "type": 159, + "type": 162, "optional": false }, { "name": "form", - "type": 98, + "type": 99, "optional": false }, { "name": "kind", - "type": 161, + "type": 164, "optional": false }, { @@ -20881,7 +21050,7 @@ }, { "kind": "array", - "element": 160 + "element": 163 }, { "kind": "object", @@ -20908,12 +21077,12 @@ "properties": [ { "name": "form", - "type": 52, + "type": 54, "optional": false }, { "name": "kind", - "type": 163, + "type": 166, "optional": false }, { @@ -20933,22 +21102,22 @@ "properties": [ { "name": "form", - "type": 70, + "type": 71, "optional": false }, { "name": "kind", - "type": 165, + "type": 168, "optional": false }, { "name": "references", - "type": 166, + "type": 169, "optional": false }, { "name": "version", - "type": 171, + "type": 174, "optional": false } ], @@ -20960,7 +21129,7 @@ }, { "kind": "array", - "element": 167 + "element": 170 }, { "kind": "object", @@ -20972,12 +21141,12 @@ }, { "name": "capturedThroughSeq", - "type": 168, + "type": 171, "optional": false }, { "name": "compacted", - "type": 170, + "type": 173, "optional": false }, { @@ -21017,7 +21186,7 @@ }, { "name": "truncated", - "type": 170, + "type": 173, "optional": false } ], @@ -21027,7 +21196,7 @@ "kind": "union", "types": [ 18, - 169 + 172 ] }, { @@ -21047,17 +21216,17 @@ "properties": [ { "name": "form", - "type": 173, + "type": 176, "optional": false }, { "name": "kind", - "type": 53, + "type": 55, "optional": false }, { "name": "sections", - "type": 174, + "type": 177, "optional": false } ], @@ -21069,7 +21238,7 @@ }, { "kind": "array", - "element": 175 + "element": 178 }, { "kind": "object", @@ -21092,28 +21261,7 @@ "properties": [ { "name": "form", - "type": 173, - "optional": false - }, - { - "name": "kind", - "type": 55, - "optional": false - }, - { - "name": "sections", - "type": 174, - "optional": false - } - ], - "indices": [] - }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 173, + "type": 176, "optional": false }, { @@ -21123,7 +21271,7 @@ }, { "name": "sections", - "type": 174, + "type": 177, "optional": false } ], @@ -21134,7 +21282,7 @@ "properties": [ { "name": "form", - "type": 173, + "type": 176, "optional": false }, { @@ -21144,7 +21292,7 @@ }, { "name": "sections", - "type": 174, + "type": 177, "optional": false } ], @@ -21155,7 +21303,7 @@ "properties": [ { "name": "form", - "type": 173, + "type": 176, "optional": false }, { @@ -21165,7 +21313,7 @@ }, { "name": "sections", - "type": 174, + "type": 177, "optional": false } ], @@ -21176,7 +21324,7 @@ "properties": [ { "name": "form", - "type": 173, + "type": 176, "optional": false }, { @@ -21186,7 +21334,7 @@ }, { "name": "sections", - "type": 174, + "type": 177, "optional": false } ], @@ -21197,7 +21345,7 @@ "properties": [ { "name": "form", - "type": 173, + "type": 176, "optional": false }, { @@ -21207,7 +21355,7 @@ }, { "name": "sections", - "type": 174, + "type": 177, "optional": false } ], @@ -21218,7 +21366,7 @@ "properties": [ { "name": "form", - "type": 173, + "type": 176, "optional": false }, { @@ -21228,7 +21376,7 @@ }, { "name": "sections", - "type": 174, + "type": 177, "optional": false } ], @@ -21239,7 +21387,7 @@ "properties": [ { "name": "form", - "type": 173, + "type": 176, "optional": false }, { @@ -21249,7 +21397,7 @@ }, { "name": "sections", - "type": 174, + "type": 177, "optional": false } ], @@ -21260,7 +21408,7 @@ "properties": [ { "name": "form", - "type": 173, + "type": 176, "optional": false }, { @@ -21270,7 +21418,7 @@ }, { "name": "sections", - "type": 174, + "type": 177, "optional": false } ], @@ -21281,7 +21429,7 @@ "properties": [ { "name": "form", - "type": 173, + "type": 176, "optional": false }, { @@ -21291,7 +21439,7 @@ }, { "name": "sections", - "type": 174, + "type": 177, "optional": false } ], @@ -21302,7 +21450,7 @@ "properties": [ { "name": "form", - "type": 173, + "type": 176, "optional": false }, { @@ -21312,7 +21460,7 @@ }, { "name": "sections", - "type": 174, + "type": 177, "optional": false } ], @@ -21323,7 +21471,7 @@ "properties": [ { "name": "form", - "type": 173, + "type": 176, "optional": false }, { @@ -21333,7 +21481,28 @@ }, { "name": "sections", - "type": 174, + "type": 177, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 176, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + }, + { + "name": "sections", + "type": 177, "optional": false } ], @@ -21349,7 +21518,7 @@ }, { "name": "kind", - "type": 189, + "type": 192, "optional": false }, { @@ -21374,12 +21543,12 @@ "properties": [ { "name": "form", - "type": 84, + "type": 85, "optional": false }, { "name": "kind", - "type": 191, + "type": 194, "optional": false }, { @@ -21394,27 +21563,6 @@ "kind": "literal", "value": "agent-message" }, - { - "kind": "object", - "properties": [ - { - "name": "form", - "type": 33, - "optional": false - }, - { - "name": "kind", - "type": 53, - "optional": false - }, - { - "name": "summary", - "type": 5, - "optional": false - } - ], - "indices": [] - }, { "kind": "object", "properties": [ @@ -21485,8 +21633,8 @@ { "kind": "union", "types": [ - 198, - 199 + 200, + 201 ] }, { @@ -21503,7 +21651,7 @@ }, { "name": "op", - "type": 145, + "type": 148, "optional": false }, { @@ -21726,6 +21874,76 @@ ] } }, + { + "key": "event:working-directory/change", + "kind": "event", + "event": "working-directory/change", + "surface": false, + "digest": "124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 3, + "optional": true + }, + { + "name": "seq", + "type": 4, + "optional": false + }, + { + "name": "time", + "type": 4, + "optional": false + }, + { + "name": "type", + "type": 5, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "cwd", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "literal", + "value": "working-directory/change" + } + ] + } + }, { "key": "event:workspace/changes", "kind": "event", @@ -21828,6 +22046,15 @@ "names": [], "sources": [] }, + { + "digest": "045926475a46f625f84ae2be292b384817059f16927c69a9adab93717923ac19", + "names": [ + "packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData" + ], + "sources": [ + "packages/session/session-title-llm/src/index.ts:34" + ] + }, { "digest": "05a2796621b1ee6c03ef7abe560b25116bb4da4d592312117242a78415a32b04", "names": [], @@ -21969,15 +22196,6 @@ "names": [], "sources": [] }, - { - "digest": "0ee52de154f8955c7abc82cffebc57452dcc8c44dccbdb6ad16c71a11305ead8", - "names": [ - "packages/core/tools/src/types.ts#PtcDispatchEventData" - ], - "sources": [ - "packages/core/tools/src/types.ts:20" - ] - }, { "digest": "0eeb17e1df89b1ebc2902116fe7145e0d321ef0db26e6bdebc802cb8513a01c1", "names": [], @@ -22000,20 +22218,6 @@ "names": [], "sources": [] }, - { - "digest": "100f6dca1468538239522cde3533e5bd721d0f1a7b50bea8b0eb533ea6c96163", - "names": [], - "sources": [] - }, - { - "digest": "1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200", - "names": [ - "packages/llm/llm/src/message.ts#DeveloperMessage" - ], - "sources": [ - "packages/llm/llm/src/message.ts:157" - ] - }, { "digest": "1033093edd0db80ff410e00830b523405e00bb0c7684948e531ff65095799625", "names": [], @@ -22033,7 +22237,7 @@ "digest": "119abe2b9c0b2f46ecb31d82b9acba4d4b30e049f655e2475e8b4ae12d5a9e25", "names": [], "sources": [ - "packages/hooks/hook-protocol/src/types.ts:19" + "packages/experimental/hook-protocol/src/types.ts:19" ] }, { @@ -22043,6 +22247,11 @@ "packages/interaction/user-approval/src/index.ts:40" ] }, + { + "digest": "124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3", + "names": [], + "sources": [] + }, { "digest": "12a0cfa619ac4a5fc6c3e8e359c8dae15769edc61497a575935c78d5842f8e42", "names": [], @@ -22077,13 +22286,6 @@ "names": [], "sources": [] }, - { - "digest": "149c5a77529cb0dc06f5ceb6273069b7606174d1e7893b5f9b1071c15306b846", - "names": [], - "sources": [ - "packages/core/agent/src/types.ts:96" - ] - }, { "digest": "15196447222782e773eb943c92b18316ce96b9af0f0cfddb6e57ba8274ecc5ff", "names": [], @@ -22128,11 +22330,6 @@ "packages/core/session/src/types.ts:427" ] }, - { - "digest": "186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467", - "names": [], - "sources": [] - }, { "digest": "18dc21f12bbac8ade8377e6fbb54179e20c3d955d73fba3244c57fb6bf001df0", "names": [ @@ -22255,15 +22452,6 @@ "names": [], "sources": [] }, - { - "digest": "1de64ea9a0849ce631b7e4e1255ae773fabd94a4a7c8d27e716f8012302e12c0", - "names": [ - "packages/subagent/subagent/src/catalog.ts#SubagentCatalogEvent" - ], - "sources": [ - "packages/subagent/subagent/src/catalog.ts:28" - ] - }, { "digest": "1df34a6c086866ffdc2dd9e8e5c581c3751a010cdf6b8538a7327f7a3237f0b4", "names": [ @@ -22312,6 +22500,15 @@ "packages/core/session/src/types.ts:196" ] }, + { + "digest": "1fa156c85450ca31063d0e0d2853028e6dac9687f2d3191fa811648210118154", + "names": [ + "packages/llm/llm/src/message.ts#MessageSource" + ], + "sources": [ + "packages/llm/llm/src/message.ts:136" + ] + }, { "digest": "1fdad624334d354820dfb3a26e28d0bc9fc1b62a33f87bab63f715b3f4391bfd", "names": [], @@ -22343,6 +22540,13 @@ "names": [], "sources": [] }, + { + "digest": "2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5", + "names": [], + "sources": [ + "packages/session/working-directory/src/index.ts:29" + ] + }, { "digest": "224aa11d4d5f88c450822d01b99e6ca5e3a9c7f26a5ede10bae33384a09ef4dc", "names": [], @@ -22402,15 +22606,6 @@ "names": [], "sources": [] }, - { - "digest": "26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116", - "names": [ - "packages/llm/llm/src/message.ts#MessageSource" - ], - "sources": [ - "packages/llm/llm/src/message.ts:136" - ] - }, { "digest": "2765e9119edf5816ac1ab4c95c5ee32c90403f3742962eff11544f7752d59d2e", "names": [], @@ -22460,6 +22655,11 @@ "packages/llm/llm/src/types.ts:455" ] }, + { + "digest": "2ace3b8059b82b6d9837fed518ca97a99eb37c4482df7aad0e71c85489428ade", + "names": [], + "sources": [] + }, { "digest": "2aed07493b1c65b6d00de5586da1e743cf501684b9ad43a1765fb4007d4ea456", "names": [], @@ -22530,9 +22730,14 @@ "packages/core/tools/src/types.ts#PtcDispatchStartEventData" ], "sources": [ - "packages/core/tools/src/types.ts:11" + "packages/core/tools/src/types.ts:12" ] }, + { + "digest": "2fef90def36aa758ca096c5f6ad0e00dbb2101943e19ca54b05618801974894a", + "names": [], + "sources": [] + }, { "digest": "30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f", "names": [ @@ -22562,7 +22767,7 @@ "packages/llm/llm/src/call-config.ts#LlmCallConfigAdapterDefaults" ], "sources": [ - "packages/llm/llm/src/call-config.ts:36" + "packages/llm/llm/src/call-config.ts:54" ] }, { @@ -22646,11 +22851,6 @@ "packages/feedback/message-feedback/src/types.ts:17" ] }, - { - "digest": "3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604", - "names": [], - "sources": [] - }, { "digest": "3b04790e08b35241ffa0d38f6d7e4f8788aa921341f6c0d7574a329397c59408", "names": [], @@ -22708,7 +22908,7 @@ "packages/core/tools/src/index.ts#ToolRestriction" ], "sources": [ - "packages/core/tools/src/index.ts:700" + "packages/core/tools/src/index.ts:701" ] }, { @@ -22742,11 +22942,6 @@ "names": [], "sources": [] }, - { - "digest": "3f42f45cbb8a06586d247e5f3763dc02c8d31a145a9a3b82fabcd101e7e224cc", - "names": [], - "sources": [] - }, { "digest": "3f74ba74acd10644c1ed6947a411d8088bd2cfac74f61bb8540f96d68e31f004", "names": [], @@ -22866,7 +23061,7 @@ "packages/subagent/subagent/src/continuation-messages.ts#AgentMessageSource" ], "sources": [ - "packages/subagent/subagent/src/continuation-messages.ts:15" + "packages/subagent/subagent/src/continuation-messages.ts:14" ] }, { @@ -22909,6 +23104,11 @@ "packages/core/session/src/types.ts:193" ] }, + { + "digest": "4be8fd28fefa5534675ea9aabd0a24efc2838b989bbc74da20a1efbd0daede8c", + "names": [], + "sources": [] + }, { "digest": "4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b", "names": [ @@ -22919,6 +23119,9 @@ "packages/experimental/agent-team/src/types.ts#TeamId", "packages/experimental/agent-team/src/types.ts#TeamMessageId", "packages/experimental/agent-team/src/types.ts#TeamTaskId", + "packages/experimental/webhook/src/brand.ts#WebhookDeliveryId", + "packages/experimental/webhook/src/brand.ts#WebhookRuleId", + "packages/experimental/webhook/src/brand.ts#WebhookSourceId", "packages/feedback/message-feedback/src/types.ts#MessageFeedbackVersion", "packages/goal/goal/src/types.ts#GoalId", "packages/interaction/commands/src/brand.ts#CommandId", @@ -22931,9 +23134,6 @@ "packages/schedule/schedule/src/types.ts#ScheduleId", "packages/session/session-title/src/index.ts#SessionTitleProviderId", "packages/session/session-title/src/types.ts#SessionTitleProviderId", - "packages/webhook/webhook/src/brand.ts#WebhookDeliveryId", - "packages/webhook/webhook/src/brand.ts#WebhookRuleId", - "packages/webhook/webhook/src/brand.ts#WebhookSourceId", "packages/workflow/workflow/src/types.ts#WorkflowRunId" ], "sources": [ @@ -22944,6 +23144,9 @@ "packages/experimental/agent-team/src/types.ts:20", "packages/experimental/agent-team/src/types.ts:32", "packages/experimental/agent-team/src/types.ts:8", + "packages/experimental/webhook/src/brand.ts:12", + "packages/experimental/webhook/src/brand.ts:6", + "packages/experimental/webhook/src/brand.ts:9", "packages/feedback/message-feedback/src/types.ts:14", "packages/goal/goal/src/types.ts:17", "packages/interaction/commands/src/brand.ts:31", @@ -22956,9 +23159,6 @@ "packages/schedule/schedule/src/types.ts:14", "packages/session/session-title/src/index.ts:42", "packages/session/session-title/src/types.ts:17", - "packages/webhook/webhook/src/brand.ts:12", - "packages/webhook/webhook/src/brand.ts:6", - "packages/webhook/webhook/src/brand.ts:9", "packages/workflow/workflow/src/types.ts:13" ] }, @@ -22988,7 +23188,7 @@ "packages/subagent/subagent/src/continuation-messages.ts#SubagentSettledMessageSource" ], "sources": [ - "packages/subagent/subagent/src/continuation-messages.ts:30" + "packages/subagent/subagent/src/continuation-messages.ts:29" ] }, { @@ -23070,6 +23270,11 @@ "packages/core/session/src/types.ts:273" ] }, + { + "digest": "53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac", + "names": [], + "sources": [] + }, { "digest": "53cf1704ae0709c7678bcf070c83c619fc48ca1b5c173b74cfa8702f22a8e492", "names": [], @@ -23148,11 +23353,6 @@ "names": [], "sources": [] }, - { - "digest": "5a92f12db2059c282cdea45b6845c65e6f4a95dd3d649aeddbde6f9ae03c561f", - "names": [], - "sources": [] - }, { "digest": "5abab3a27fd2736660ecddd94b28d6bbf114986dd894880153bd1be3ccacb563", "names": [], @@ -23163,6 +23363,13 @@ "names": [], "sources": [] }, + { + "digest": "5b55817a403569fbb2d379496df2e3f3f71107f6c577a218028ce187c60b61e6", + "names": [], + "sources": [ + "packages/core/session/src/types.ts:311" + ] + }, { "digest": "5b6fb1f226ff56402db08a6aae82af76100b14056271875f69ec526a1ae05d51", "names": [], @@ -23270,11 +23477,6 @@ "names": [], "sources": [] }, - { - "digest": "6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178", - "names": [], - "sources": [] - }, { "digest": "6292bcbedaa97feaec5b48a03bb8144b581b93309d9d6c905f26da7a6a7a0a4f", "names": [], @@ -23341,7 +23543,7 @@ "packages/llm/llm/src/call-config.ts#LlmCallConfig" ], "sources": [ - "packages/llm/llm/src/call-config.ts:23" + "packages/llm/llm/src/call-config.ts:31" ] }, { @@ -23466,7 +23668,7 @@ "packages/context/agent-instructions/src/state.ts#AgentInstructionSource" ], "sources": [ - "packages/context/agent-instructions/src/state.ts:37" + "packages/context/agent-instructions/src/state.ts:40" ] }, { @@ -23583,6 +23785,11 @@ "names": [], "sources": [] }, + { + "digest": "7b1ce60193fa8b69292e5843858d9cee7dc565dcb40d33c136e5d7e8b8d4296a", + "names": [], + "sources": [] + }, { "digest": "7c6129b6961cf4b2a030f36c78d2b0a5be38617651a9b81322e054ee512bee06", "names": [ @@ -23714,7 +23921,7 @@ "digest": "83079c8a3f733ac3fa603eefa0fbb4737ca4d8b69e7e2e7298b60f9ac4098693", "names": [], "sources": [ - "packages/skill/tool-skill/src/index.ts:40" + "packages/skill/tool-skill/src/index.ts:41" ] }, { @@ -23722,6 +23929,13 @@ "names": [], "sources": [] }, + { + "digest": "83a665476a9ee9af05cb1fdded12dd647d1b97b491200d7f12ef1af7cdac4428", + "names": [], + "sources": [ + "packages/core/agent/src/types.ts:96" + ] + }, { "digest": "83d6916021eede8f2d83a1872716caa1efdec39a3681890996c55147187fdd12", "names": [], @@ -23733,7 +23947,7 @@ "packages/context/agent-instructions/src/render.ts#AgentInstructionChange" ], "sources": [ - "packages/context/agent-instructions/src/render.ts:47" + "packages/context/agent-instructions/src/render.ts:53" ] }, { @@ -23824,6 +24038,15 @@ "packages/llm/llm/src/types.ts:456" ] }, + { + "digest": "89b2edff547be1ecfcd78ef18aeeb3cf507fcbf7d6093ec04fb111ef4c8e8a97", + "names": [ + "packages/llm/llm/src/message.ts#UserMessage" + ], + "sources": [ + "packages/llm/llm/src/message.ts:162" + ] + }, { "digest": "8a046b52331959aa3394680d6acbe09b8a5447c6b61375f263445bfcef19b7f1", "names": [], @@ -23845,7 +24068,7 @@ "digest": "8ad45ad053b4d89f8865c17cc9ff3b8748e86045f3f0fb816269a34af2619141", "names": [], "sources": [ - "packages/hooks/hook-protocol/src/types.ts:31" + "packages/experimental/hook-protocol/src/types.ts:31" ] }, { @@ -23919,7 +24142,7 @@ "digest": "8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543", "names": [], "sources": [ - "packages/core/tools/src/index.ts:34" + "packages/core/tools/src/index.ts:35" ] }, { @@ -24045,6 +24268,11 @@ "packages/web/web-search-deepseek/src/provider.ts:64" ] }, + { + "digest": "9a36727716d0e73f681f3b725e7f28f89417c68174c8af0ff2b1ef52b08326a0", + "names": [], + "sources": [] + }, { "digest": "9a410ddf56792b2b7cd4a224a2e54dc4dee30a8e8e6b1c2b9a1d19992b217adc", "names": [ @@ -24105,7 +24333,7 @@ "digest": "9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94", "names": [], "sources": [ - "packages/webhook/webhook/src/types.ts:74" + "packages/experimental/webhook/src/types.ts:74" ] }, { @@ -24192,13 +24420,6 @@ "packages/experimental/agent-team/src/types.ts:234" ] }, - { - "digest": "a5d80b5a2f3a6bbaf8f0334126632b78f94d8346122c68e8122ea89906d63d26", - "names": [], - "sources": [ - "packages/core/session/src/types.ts:311" - ] - }, { "digest": "a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc", "names": [], @@ -24207,10 +24428,10 @@ { "digest": "a6b41f5bede8094d7bf635be5d69b1bd7a25e006ca9cf9415bceb9322f1a7feb", "names": [ - "packages/hooks/hook-protocol/src/types.ts#HookDialect" + "packages/experimental/hook-protocol/src/types.ts#HookDialect" ], "sources": [ - "packages/hooks/hook-protocol/src/types.ts:48" + "packages/experimental/hook-protocol/src/types.ts:48" ] }, { @@ -24237,7 +24458,7 @@ "names": [], "sources": [ "packages/core/session/src/types.ts:383", - "packages/core/tools/src/types.ts:24" + "packages/core/tools/src/types.ts:27" ] }, { @@ -24372,11 +24593,6 @@ "names": [], "sources": [] }, - { - "digest": "ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46", - "names": [], - "sources": [] - }, { "digest": "aec150797b655c5f15ebb7a501454b0c208fbaccec9ace49d7de2171fc02f47d", "names": [], @@ -24392,6 +24608,15 @@ "names": [], "sources": [] }, + { + "digest": "af99c95d46bed802870f1ebefe0ea11f92a03cb37b8e0cbe3f94b7aaaf509b08", + "names": [ + "packages/llm/llm/src/message.ts#DeveloperMessage" + ], + "sources": [ + "packages/llm/llm/src/message.ts:157" + ] + }, { "digest": "aff60ef947c18b012644852bdd0d5bfe72065e061dd563787c76c646b4a10f6b", "names": [], @@ -24466,6 +24691,11 @@ "packages/experimental/agent-team/src/types.ts:71" ] }, + { + "digest": "b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6", + "names": [], + "sources": [] + }, { "digest": "b3681c026a0b98e98b103f057dfd217fbe904129c090efd668253ec9aea7fece", "names": [], @@ -24582,11 +24812,6 @@ "packages/goal/goal/src/types.ts:45" ] }, - { - "digest": "b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411", - "names": [], - "sources": [] - }, { "digest": "b8fc636a2121df9d8423c242e03c9d23b7ab7c6fdce00e45746932c60b730f97", "names": [ @@ -24616,6 +24841,11 @@ "names": [], "sources": [] }, + { + "digest": "ba5b10ea77d68f4a29439a9bd2a14f6a560acf55731749d57237757cc72fbc06", + "names": [], + "sources": [] + }, { "digest": "ba77b58995e36f14bb13886b0493d0a1a63d5db10b1a6cd9f6ac65a72cf832ec", "names": [], @@ -24778,13 +25008,9 @@ "sources": [] }, { - "digest": "cb7b863ddb7b3eab5f18f216602feba25f47b456e73e4e72b6bcc259875c6dab", - "names": [ - "packages/llm/llm/src/message.ts#Message" - ], - "sources": [ - "packages/llm/llm/src/message.ts:196" - ] + "digest": "cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09", + "names": [], + "sources": [] }, { "digest": "cbca49bde16b656d5b8fbe4a8a507769f7b594c6287782b809c03814941bc5e3", @@ -24813,20 +25039,20 @@ "packages/llm/llm-retry/src/types.ts:17" ] }, + { + "digest": "cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6", + "names": [ + "packages/core/tools/src/types.ts#PtcDispatchEventData" + ], + "sources": [ + "packages/core/tools/src/types.ts:21" + ] + }, { "digest": "cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331", "names": [], "sources": [] }, - { - "digest": "cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd", - "names": [ - "packages/llm/llm/src/message.ts#UserMessage" - ], - "sources": [ - "packages/llm/llm/src/message.ts:162" - ] - }, { "digest": "cffcdc19881ab61ec0a9ce18743996f7ac44ac8889f34dfac36ae745574aa22f", "names": [ @@ -24853,6 +25079,11 @@ "packages/llm/llm/src/types.ts:457" ] }, + { + "digest": "d4c799e3d51cae1040362ba4968efc484739e14416c9c1df20c2f108b4ef2eb5", + "names": [], + "sources": [] + }, { "digest": "d4e4d575abeb4b72d616dc65c17ac0eaa943cc135609395044c38d5afcb10e31", "names": [], @@ -24868,6 +25099,11 @@ "names": [], "sources": [] }, + { + "digest": "d66e11dddf5dfa3c71547e2cbd7b0608824cd8d6243fc16f532017ff28e8fba5", + "names": [], + "sources": [] + }, { "digest": "d6e654b88b584fcdf290a56802b1730bc7b9847ae86baa4265ab7769eb48990c", "names": [], @@ -24878,6 +25114,15 @@ "names": [], "sources": [] }, + { + "digest": "d901c4f5e856591c3217af983caa15bba568d580700f0958bab56b88004eced9", + "names": [ + "packages/llm/llm/src/message.ts#Message" + ], + "sources": [ + "packages/llm/llm/src/message.ts:196" + ] + }, { "digest": "d9196086e41e4dd5c2ce87f5c25529c8112a1731d258cf0728747d41a9ee3241", "names": [], @@ -24897,6 +25142,13 @@ "names": [], "sources": [] }, + { + "digest": "da60ed58a8fa23d7aa4920ab5c1f4e3d28109cb0797ef9e5c8b125721b847582", + "names": [], + "sources": [ + "packages/session/working-directory/src/index.ts:36" + ] + }, { "digest": "dacf3d5f3e4ff619c1320e64a48dec5e7ecd7870bece65fbd449a0e63adfa521", "names": [], @@ -25292,6 +25544,15 @@ "names": [], "sources": [] }, + { + "digest": "f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87", + "names": [ + "packages/subagent/subagent/src/catalog.ts#SubagentCatalogEvent" + ], + "sources": [ + "packages/subagent/subagent/src/catalog.ts:28" + ] + }, { "digest": "f789af5ab844827eb5a4f357c0afae1eeed108c6c22d8ca1a56e586b349a7c85", "names": [], @@ -25364,9 +25625,14 @@ "packages/skill/tool-skill/src/index.ts#SkillCatalogSource" ], "sources": [ - "packages/skill/tool-skill/src/index.ts:34" + "packages/skill/tool-skill/src/index.ts:35" ] }, + { + "digest": "fc351843b8ecbd05d739c4ca4fcd4bc7cfad6c0beacbbd48389959bb1bf06cf3", + "names": [], + "sources": [] + }, { "digest": "fcf427290c6213c2a7feb57492d832ce324f5b9737a759f424dab69e6bd101a0", "names": [], @@ -25387,15 +25653,6 @@ "names": [], "sources": [] }, - { - "digest": "fe3cdc6f9050584cea4a6d6998c31334bff7899f5ff1095082a44fdf3b47e996", - "names": [ - "packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData" - ], - "sources": [ - "packages/session/session-title-llm/src/index.ts:34" - ] - }, { "digest": "fe792a6cd430494503837e21e79f91677704ccdd9e870c679aabd7d43ed47ddd", "names": [], diff --git a/docs/session-format-status.i18n.yaml b/docs/session-format-status.i18n.yaml index 833cd194f6..ef3b64d39c 100644 --- a/docs/session-format-status.i18n.yaml +++ b/docs/session-format-status.i18n.yaml @@ -15,11 +15,11 @@ en: 02d18ba8a66cfb10 zh: 54b753bbcb8f8a9e /session-format-version-and-release-status/finalization-record: - en: 9a1b2f4c61551ab7 - zh: 666a2339c47343e1 + en: 15b6134dcb3a09d5 + zh: 4260c82e79baa021 /session-format-version-and-release-status/release-record: - en: ee6e54ebc2b6dd37 - zh: 928343edfd3fb943 + en: 83ce79a241b630d7 + zh: 53f3c461ef731f67 /session-format-version-and-release-status/updating-the-record: en: 7eaa730a2d163bd6 zh: 53ed7618537db2f5 diff --git a/docs/session-format-status.md b/docs/session-format-status.md index 9df313da2f..78b6701095 100644 --- a/docs/session-format-status.md +++ b/docs/session-format-status.md @@ -36,17 +36,17 @@ V4 has an accepted compatibility baseline in the [checkpoint](persistence-change Finalization does not freeze every future V4 addition and does not assert publication. The release record below retains the independently verified published version. Ordinary comments, aliases, source locations, and implementation fixes preserving the accepted meaning do not change this baseline. -Before the first V4 publication, every integration of a newer V3-writing master must pass the [explicit V3 vocabulary check](cookbook/adding-a-session-format-version.md#final-v3-vocabulary) against the recorded local source commit. Verify the source pin’s freshness and review new event payload conversions before updating the migration-owned set. After publication, the final V3 vocabulary remains historical and independent of current V4 additions. +The [final V3 vocabulary](cookbook/adding-a-session-format-version.md#final-v3-vocabulary) remains historical and independent of current V4 additions. ## Release record ```yaml session-format-release -latestReleasedVersion: 3 -evidenceTag: dsh-v0.1.5-alpha.1 +latestReleasedVersion: 4 +evidenceTag: dsh-v0.2.0-rc.2 ``` -Evidence: published product tag `dsh-v0.1.5-alpha.1`; tagged writer: `packages/core/session/src/types.ts`. +Evidence: published product tag `dsh-v0.2.0-rc.2`; tagged writer: `packages/core/session/src/types.ts`. ## Updating the record diff --git a/docs/session-format-status.zh.md b/docs/session-format-status.zh.md index 31e1710632..141917a63c 100644 --- a/docs/session-format-status.zh.md +++ b/docs/session-format-status.zh.md @@ -36,17 +36,17 @@ V4 的已接受兼容性基线保存在[检查点](persistence-changes/finalized 定稿不冻结 V4 之后的每项新增,也不表示已发布。下方发布记录保留独立验证的已发布版本。普通注释、别名、源码位置,以及保留已接受含义的实现修复,不改变该基线。 -首次发布 V4 前,每次集成仍写入 V3 的较新 master 都必须对已记录的本地源提交执行[显式 V3 词汇校验](cookbook/adding-a-session-format-version.zh.md#final-v3-vocabulary)。更新迁移所有的集合前,要核验源 pin 的新鲜度并审查新事件 payload 的转换。发布后,最终 V3 词汇保持为历史定义,与当前 V4 新增事件无关。 +最终 [V3 词汇](cookbook/adding-a-session-format-version.zh.md#final-v3-vocabulary)保持为历史定义,与当前 V4 新增事件无关。 ## 发布记录 ```yaml session-format-release -latestReleasedVersion: 3 -evidenceTag: dsh-v0.1.5-alpha.1 +latestReleasedVersion: 4 +evidenceTag: dsh-v0.2.0-rc.2 ``` -证据:已发布产品标签 `dsh-v0.1.5-alpha.1`;该标签的写入器路径:`packages/core/session/src/types.ts`。 +证据:已发布产品标签 `dsh-v0.2.0-rc.2`;该标签的写入器路径:`packages/core/session/src/types.ts`。 ## 更新记录 diff --git a/docs/subsystems/README.i18n.yaml b/docs/subsystems/README.i18n.yaml index e23bf8b735..76288201ad 100644 --- a/docs/subsystems/README.i18n.yaml +++ b/docs/subsystems/README.i18n.yaml @@ -3,5 +3,5 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/README.md /subsystems: - en: 3716b19db3b2e1b2 - zh: a9611e4745f0ac89 + en: 34b6bc8c15b8009b + zh: 7a07e5c32b3feca1 diff --git a/docs/subsystems/README.md b/docs/subsystems/README.md index f7382c49ff..0960b21527 100644 --- a/docs/subsystems/README.md +++ b/docs/subsystems/README.md @@ -18,6 +18,8 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures | [deliverables.md](deliverables.md) | what a turn hands to the user: `PresentedFile` deliveries from `present` and the Host-served `WorkspaceChangesSummary` of changed files from git snapshots | | [commands.md](commands.md) | the human-command registry service: definitions, adapter discovery, direct invocation, results, and parsing views | | [session.md](session.md) | the full `SessionEventMap` variant catalog, `TurnEndReason`, `deriveMessages()`, execution enclosure, and standalone events | +| [working-directory.md](working-directory.md) | Session execution directories, durable changes, recovery, and user context | +| [worktrees.md](worktrees.md) | Experimental Git branch and checkout creation under existing permissions | | [persistence.md](persistence.md) | the durability seam: `SessionPersistence`, the JSONL provider, `session/flush`, crash recovery, `SessionHeader` | | [settings.md](settings.md) | the user-settings seam: `SettingsNamespace` registration, layered resolution (defaults → composition `base` → user document), owner scopes, hot commits | | [credentials.md](credentials.md) | the credential seam: `CredentialRef` references (never values) in configuration, per-operation resolution, UI-safe `CredentialInfo`, provider source layers | @@ -47,7 +49,9 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures | [compaction.md](compaction.md) | the compaction seam: the `compaction/*` session events, `CompactionResult`, the `CompactionEngine` interface | | [subagent.md](subagent.md) | the subagent seam: the named-provider registry, `SubagentStartRequest`/`Result`/`Run`, the start-time-vs-runtime capability split | | [voice-input.md](voice-input.md) | experimental named recognizers, transient audio and revision-guarded draft insertion | +| [translation.md](translation.md) | experimental anonymous text translation and explicit provider requests | | [agent-team.md](agent-team.md) | Agent Teams: implicit Lead identity, named continuable teammates, durable peer mailbox, and shared task DAG | +| [claude-code-mods.md](claude-code-mods.md) | Claude Code mods through the experimental bridge: every event, `$` member, chain rule, drawing feature, and example mod that differs from Claude Code | | [web.md](web.md) | the web access seam: `WebSearchRequest`/`Result`, `WebFetchRequest`/`Result`, `WebFetchBody`, provider availability, `WebError` | | [spill.md](spill.md) | the spill storage seam: `SaveTextSpill`, `SpillOwner`/`SpillSource`, `SpillRef`, the branded `SpillLocator` | | [workflow.md](workflow.md) | the workflow seam: `WorkflowStartRequest`, `WorkflowMeta`, `WorkflowRun`/`Result`, the `workflow/*` event payloads, `WorkflowError` fatality | diff --git a/docs/subsystems/README.zh.md b/docs/subsystems/README.zh.md index 5228af1a60..e5f33a3060 100644 --- a/docs/subsystems/README.zh.md +++ b/docs/subsystems/README.zh.md @@ -18,6 +18,8 @@ | [deliverables.md](deliverables.zh.md) | 一轮交给用户的东西:`present` 声明的 `PresentedFile` 交付,以及由 git 快照得出、由 Host 提供的 `WorkspaceChangesSummary` 改动摘要 | | [commands.md](commands.zh.md) | 人类命令注册表服务:定义、适配器发现、直接调用、结果与解析视图 | | [session.md](session.zh.md) | 完整的 `SessionEventMap` 变体目录、`TurnEndReason`、`deriveMessages()`、执行封闭与独立事件 | +| [working-directory.md](working-directory.zh.md) | Session 执行目录、持久化变更、恢复与用户上下文 | +| [worktrees.md](worktrees.zh.md) | 现有权限下的实验性 Git 分支与检出目录创建 | | [persistence.md](persistence.zh.md) | 持久性 seam:`SessionPersistence`、JSONL 提供方、`session/flush`、崩溃恢复、`SessionHeader` | | [settings.md](settings.zh.md) | 用户设置 seam:`SettingsNamespace` 注册、分层解析(默认值 → 组合 `base` → 用户文档)、owner scope、热提交 | | [credentials.md](credentials.zh.md) | 凭据 seam:配置中的 `CredentialRef` 引用(绝不含值)、按操作解析、对 UI 安全的 `CredentialInfo`、提供方来源层 | @@ -47,7 +49,9 @@ | [compaction.md](compaction.zh.md) | 压缩(compaction)seam:`compaction/*` 会话事件、`CompactionResult`、`CompactionEngine` 接口 | | [subagent.md](subagent.zh.md) | subagent seam:命名提供方注册表、`SubagentStartRequest`/`Result`/`Run`、启动时与运行时能力拆分 | | [voice-input.md](voice-input.zh.md) | 实验性具名语音识别、临时音频和受版本保护的草稿插入 | +| [translation.md](translation.zh.md) | 实验性免登录文本翻译与显式 Provider 请求 | | [agent-team.md](agent-team.zh.md) | Agent Teams:隐式 Lead 身份、具名 continuable teammate、持久 peer mailbox 与共享任务 DAG | +| [claude-code-mods.md](claude-code-mods.zh.md) | 经实验性桥接运行的 Claude Code 模组:与 Claude Code 不同的每个事件、`$` 成员、链规则、绘制特性与示例模组 | | [web.md](web.zh.md) | Web 访问 seam:`WebSearchRequest`/`Result`、`WebFetchRequest`/`Result`、`WebFetchBody`、提供方可用性、`WebError` | | [spill.md](spill.zh.md) | spill 存储 seam:`SaveTextSpill`、`SpillOwner`/`SpillSource`、`SpillRef`、品牌类型 `SpillLocator` | | [workflow.md](workflow.zh.md) | 工作流 seam:`WorkflowStartRequest`、`WorkflowMeta`、`WorkflowRun`/`Result`、`workflow/*` 事件载荷、`WorkflowError` 致命性 | diff --git a/docs/subsystems/boot.i18n.yaml b/docs/subsystems/boot.i18n.yaml index 39fbc85407..52d584bd1f 100644 --- a/docs/subsystems/boot.i18n.yaml +++ b/docs/subsystems/boot.i18n.yaml @@ -6,5 +6,5 @@ en: 962b4398c9052959 zh: 14471c340a9f6e60 /profile-management/management-records: - en: 2d5acb4065719db7 - zh: 3d21e44376092d77 + en: a10d3f358e9b7c8e + zh: 28b0f44589a5020d diff --git a/docs/subsystems/boot.md b/docs/subsystems/boot.md index 6a63dbae56..be79a823a3 100644 --- a/docs/subsystems/boot.md +++ b/docs/subsystems/boot.md @@ -6,13 +6,15 @@ The [boot package group](../../packages/boot/README.md) owns launcher-provided p ## Management records +`ProfilePatch` adds an optional literal `preset` row id to native Include patch fields; `id` then targets a child in that preset. [App-boot](../../packages/boot/app-boot/README.md#profiles) owns compilation, ordering, validation, and path resolution. + `PluginEntryId` identifies one Loader entry; callers obtain it from `listPlugins` rather than constructing a patch id. `PluginInfo` carries module identity, effective enablement, fiber phase and optional display `meta`, plus a unique `patchId` or a `readOnlyReason`. -`BundleInfo` carries the package name, optional installed version, selected enablement, removal availability, optional resolution error, and, for a bundle the profile's own dependency supplies and the installation does not, `source`: that dependency as a spec `pnpm add` accepts. Its optional `meta` and each `BundleRowInfo.meta` contain display text or a metadata diagnostic; Clients select a language at render time. +`BundleInfo.official` identifies project-maintained shipped optional bundles and on-demand catalog entries independently of installation. `availability` identifies a readable installation-provided or profile package, or `missing`; on-demand catalog entries exclude undeclared transitive copies. `installed` records the profile dependency declaration. An on-demand `installTarget` carries the host-derived exact spec and version. `BundleInfo` also carries the package name, optional installed version, selected enablement, removal availability, optional resolution error, and, for a bundle the profile's own dependency supplies and the installation does not, `source`: that dependency as a spec `pnpm add` accepts. Its optional `meta` and each `BundleRowInfo.meta` contain display text or a metadata diagnostic; Clients select a language at render time. -`InstallBundleOptions.enabled` defaults to true. False installs without selecting the bundle layer. `approvedBuilds` grants persistent script permission to the supplied pending package names before installation. `registry` names the registry asked first; absent, the configured one. +`InstallBundleOptions.saveExact` passes `--save-exact` to the shared package installer. `InstallBundleOptions.enabled` defaults to true. False installs without selecting the bundle layer. `approvedBuilds` grants persistent script permission to the supplied pending package names before installation. `registry` names the registry asked first; absent, the configured one. `PluginRegistries` carries the configured first registry, `null` for the one pnpm's own configuration names, the fallbacks asked after it, and `resolved`, the URL pnpm's own configuration names or `null` while unread. `InspectOptions.registry` names the registry a lookup asks first. @@ -30,7 +32,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp ### `ctx.configEditor` — `ConfigEditor` -Persist complete raw configs and apply them through the normal Loader path. +Persist Host entry configs without changing preset-scoped operations, then reconcile through Loader. ```ts cordis-catalog /** Addressable profile rows; nested Includes have independent configuration ownership. @@ -109,12 +111,12 @@ Manage profile files and apply their declared reload lifecycle. */ @Remote async listPlugins(): Promise -/** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles. +/** Read installed, installation-provided, and offline Official catalog bundles, plus selected non-bundle names. * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected. * @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional * display metadata, activation selections, whether the installation offers the bundle, and removal availability. */ -@Remote listBundles(): Promise +@Remote async listBundles(): Promise /** Read the registries this manager asks: the configured first one, its fallbacks in order, and what pnpm's own configuration names. * @returns The registries in pnpm's comparison form; null is the one pnpm's own configuration names, `resolved` as pnpm reads it now. @@ -151,7 +153,7 @@ Manage profile files and apply their declared reload lifecycle. * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay. * @param spec One package spec, including local paths relative to the invocation directory. * @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names, - * the pending build scripts to allow for this profile before pnpm runs, and the registry asked first. + * the pending build scripts to allow, whether to save an exact dependency, and the registry asked first. * @returns Package-manager diagnostics, the registries asked, and the observed activation outcome. */ @Remote installBundle(spec: string, options?: InstallBundleOptions): Promise diff --git a/docs/subsystems/boot.zh.md b/docs/subsystems/boot.zh.md index b042f9517b..93024b965a 100644 --- a/docs/subsystems/boot.zh.md +++ b/docs/subsystems/boot.zh.md @@ -6,13 +6,15 @@ ## 管理记录 +`ProfilePatch` 在原生 Include patch 字段上增加可选的字面量 `preset` 条目 id;此时 `id` 指向该 preset 内的子条目。[App-boot](../../packages/boot/app-boot/README.zh.md#profiles) 定义编译、顺序、校验与路径解析规则。 + `PluginEntryId` 标识一个 Loader 条目;调用方从 `listPlugins` 获取,不自行拼接 patch id。 `PluginInfo` 包含模块标识、实际启停状态、fiber 阶段和可选的展示 `meta`,以及唯一的 `patchId` 或 `readOnlyReason`。 -`BundleInfo` 包含包名、可选的安装版本、组合层选择状态、删除可用性、可选的解析错误;由 profile 自身依赖提供、且安装不提供的组合包还带有 `source`,即该依赖在 `pnpm add` 中可用的 spec。它的可选 `meta` 与各行的 `BundleRowInfo.meta` 包含展示文本或元信息诊断;Client 在渲染时选择语言。 +`BundleInfo.official` 标识项目维护的随附可选组合包和按需目录项,独立于安装状态。`availability` 表示可读取的安装随附包、profile 包或 `missing`;按需目录项不计入未声明的传递依赖副本。`installed` 记录 profile 的依赖声明。按需条目的 `installTarget` 包含 Host 生成的精确 spec 和版本。`BundleInfo` 还包含包名、可选的安装版本、组合层选择状态、删除可用性、可选的解析错误;由 profile 自身依赖提供、且安装不提供的组合包还带有 `source`,即该依赖在 `pnpm add` 中可用的 spec。它的可选 `meta` 与各行的 `BundleRowInfo.meta` 包含展示文本或元信息诊断;Client 在渲染时选择语言。 -`InstallBundleOptions.enabled` 默认为 true,false 表示安装但不选择组合包层。`approvedBuilds` 在安装前向指定的待审批包名授予持久脚本权限。`registry` 指定首先询问的注册表;缺省为配置的那个。 +`InstallBundleOptions.saveExact` 向共享包安装器传入 `--save-exact`。`InstallBundleOptions.enabled` 默认为 true,false 表示安装但不选择组合包层。`approvedBuilds` 在安装前向指定的待审批包名授予持久脚本权限。`registry` 指定首先询问的注册表;缺省为配置的那个。 `PluginRegistries` 携带配置的第一个注册表(`null` 即 pnpm 自身配置指定的那个)、随后依次询问的备选注册表,以及 `resolved`——pnpm 自身配置指向的 URL,未读到时为 `null`。`InspectOptions.registry` 指定一次查询首先询问的注册表。 @@ -30,7 +32,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp ### `ctx.configEditor` — `ConfigEditor` -Persist complete raw configs and apply them through the normal Loader path. +Persist Host entry configs without changing preset-scoped operations, then reconcile through Loader. ```ts cordis-catalog /** Addressable profile rows; nested Includes have independent configuration ownership. @@ -109,12 +111,12 @@ Manage profile files and apply their declared reload lifecycle. */ @Remote async listPlugins(): Promise -/** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles. +/** Read installed, installation-provided, and offline Official catalog bundles, plus selected non-bundle names. * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected. * @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional * display metadata, activation selections, whether the installation offers the bundle, and removal availability. */ -@Remote listBundles(): Promise +@Remote async listBundles(): Promise /** Read the registries this manager asks: the configured first one, its fallbacks in order, and what pnpm's own configuration names. * @returns The registries in pnpm's comparison form; null is the one pnpm's own configuration names, `resolved` as pnpm reads it now. @@ -151,7 +153,7 @@ Manage profile files and apply their declared reload lifecycle. * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay. * @param spec One package spec, including local paths relative to the invocation directory. * @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names, - * the pending build scripts to allow for this profile before pnpm runs, and the registry asked first. + * the pending build scripts to allow, whether to save an exact dependency, and the registry asked first. * @returns Package-manager diagnostics, the registries asked, and the observed activation outcome. */ @Remote installBundle(spec: string, options?: InstallBundleOptions): Promise diff --git a/docs/subsystems/claude-code-mods.i18n.yaml b/docs/subsystems/claude-code-mods.i18n.yaml new file mode 100644 index 0000000000..67dce12eae --- /dev/null +++ b/docs/subsystems/claude-code-mods.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for claude-code-mods.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/subsystems/claude-code-mods.md +/claude-code-mods-compatibility: + en: e520df3bf44e1160 + zh: 557956eaf6faf4a7 +/claude-code-mods-compatibility/packaging-and-loading: + en: 75cf021ce8babaab + zh: caa20540f8bce11a +/claude-code-mods-compatibility/events: + en: cea776ec0063c311 + zh: 6b8c8b32f2f7ede9 +/claude-code-mods-compatibility/the-chain: + en: 111c186536345342 + zh: 8ddc81c78c05a840 +/claude-code-mods-compatibility/members: + en: 11c3d07e9deea30a + zh: 3a47a1114869d845 +/claude-code-mods-compatibility/drawing: + en: 1bd1a60970fcf426 + zh: 8f6a8e069f742fb7 +/claude-code-mods-compatibility/testing: + en: 8b3530b79e09a067 + zh: c041e63f7fd8de5a +/claude-code-mods-compatibility/the-example-mods: + en: b3ef5ea831c5c48f + zh: fa64186430e7c772 diff --git a/docs/subsystems/claude-code-mods.md b/docs/subsystems/claude-code-mods.md new file mode 100644 index 0000000000..1d125affae --- /dev/null +++ b/docs/subsystems/claude-code-mods.md @@ -0,0 +1,146 @@ +# Claude Code mods: compatibility + +English | [中文](claude-code-mods.zh.md) + +Every way a mod behaves differently through this bridge than under Claude Code, as of Claude Code 2.1.287 ([mods reference](https://code.claude.com/docs/en/plugins/mods/reference), its `claude-code.d.ts`) and the [Getting started with Claude Code mods](https://claude.dev/blog/getting-started-with-claude-code-mods/) post of 2026-10-01. A row names the Claude Code behavior, what this bridge does, and the reason or the follow-up. Anything not listed here behaves as the reference describes; a hook on an event this host never raises is named in a warning when the mod loads, and a `$` call outside the served table rejects with `no implementation for .`. + +## Packaging and loading + +| Claude Code | This bridge | Reason or follow-up | +|---|---|---| +| A plugin directory with `.claude-plugin/plugin.json` and `hooks/hooks.json` naming one hooks module; `claude --plugin-dir`, marketplaces, `/plugin install` | A mod is a plugin: `defineMod({ name, version, root, userConfig, register })` wraps the hooks module and mounts through cordis.yml after the bridge; `plugin.json` and `hooks.json` are not read | Mods join the one composition mechanism every DSH plugin uses; the example directories keep their manifests so they also run under `claude --plugin-dir` | +| `userConfig` schema in `plugin.json` with typed defaults; options validated at load | `userConfig` is the defaults object of the `defineMod` spec; the plugin's cordis.yml `config` overlays it and is validated as a record of strings, numbers, booleans, and string lists | No manifest is read | +| Tiers (`user`, `project`, `managed`) and `next.to(e, tier)` | Every mod loads as `user`; `tier()` in a test is accepted and ignored; `next.to` rejects | Managed settings are not a DSH concept | +| Hot reload of the hooks module on edit; `claude plugin validate`, `claude plugin test`, types generation from `types/index.d.ts` | None of these commands; a remount re-runs `register` on the same evaluated module, so module-level variables keep their values | Follow-up only on demand; the test kit's `createModTestKit` replaces `claude plugin test` inside Vitest | +| A hooks module may be TypeScript | A `.ts` hooks module loads only where the launcher transpiles (the source launch does; a built install does not) | DSH ships plain Node | +| Mods run in-process with `$` as their only access to the host | The hooks module runs in-process with Node's globals, no access rule, and the process's full authority: `$.env` reads and writes the harness environment, `$.http.fetch` reaches any URL, `$.fs` and `$.tool.call` act as the session | No sandbox is applied to mods; mount only mods you would run as a plugin | +| `import type { … } from 'claude-code'`, `declare module 'claude-code' { interface PluginState }` | The `claude-code` module name resolves only inside this repository's test setup, to the bridge's types; a mod outside it imports `@deepseek-ai/dsh-experimental-claude-code-mods` | The type names are the bridge's own | +| `settings hooks` in `hooks.json` run beside mod hooks | Not run; mount `@deepseek-ai/dsh-hooks-claude-code` for them | Different bridge | + +## Events + +| Event | Claude Code | This bridge | +|---|---|---| +| `session.start` | Raised once per session, before the first prompt | Raised for a root agent from `agent/created`, awaited before its first turn; not raised for subagents | +| `session.end` | Raised once per session | Raised for a root agent from `agent/disposed`; `$.state` is readable until the hooks settle | +| `prompt.submit` | `e.text` is the typed prompt; `context` entries become one block each after the prompt as typed; a prompt a plugin submitted carries `origin: { kind: 'plugin', name }` | Same; `e.text` joins the text blocks of the human's own (`user`-sourced) messages in the claimed batch, a batch of injected context alone raises nothing, and a prompt `$.prompt.submit` queued carries the submitting mod as its origin | +| `turn.start`, `turn.complete` | Per turn; subagent turns carry `agentId` | Same; `turn.complete`'s `{ text }` reaches the host log, not a line under the answer; `durationMs` counts from `turn/start` | +| `tool.call` | Runs before Claude Code's permission check; `next({ ...e, command })` rewrites the arguments the tool runs with, and `next({ ...e, tool })` reroutes the call | Runs around `tools/execute`, after the harness permission decision; a hook (or `.catch` handler) that rewrites arguments or names another tool is skipped with a report — the argument case names the [pre-tool input rewrite proposal](../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md) — because the logged call runs as logged. `{ deny }`, `{ result }`, observing, and rewriting the result or `isError` after `next` work as described | +| `command.run` | Runs for a command the mod registered | Same | +| `ui.render` | Raised for every render site (`AbovePrompt`, `Pane`, `ToolUse`, `AssistantMessage`, `Spinner`, …) | Raised for `AbovePrompt` only, once per redraw of a session's band; `Pane` hooks run only through the test kit's `$.ui.mount({ component: 'Pane' })` | +| `tool.check`, `tool.describe`, `tool.list`, `tool.register` | Shape the tool list and permissions | Not raised; `$.tool.register` and `$.tool.list` are served as `$` calls | +| `turn.step` | Per model step | Not raised | +| Every other reference event: the other `prompt.*` events, `skill.prompt`, `attribution.text`, `command.describe`, `config.*`, the other `session.*` events, `agent.*`, `plugin.register`, `engine.create`, `telemetry.*`, `ui.resolve`, `ui.press`, `ui.input`, `ui.select`, `ui.focus`, `ui.scroll`, `ui.close`, `ui.message` | Raised by the engine | Registered, never raised, named in a warning at load | +| `classic.*` | Classic settings hooks as events | Refused at `register`, like any unknown event name | +| `.` op events (`fs.read`, `store.set`, …) | A later-loaded mod's `$` call reaches the mods loaded before it | Same; `tool.call` arrives through the tool pipeline instead | + +## The chain + +| Claude Code | This bridge | Reason | +|---|---|---| +| A hook may call `next` more than once; each call runs the chain beneath again | `next` runs once: a second call returns the first run's result | One tool execution per logged call | +| A hook that answers while its `next` is still running | The run beneath is awaited before the hook's answer settles; a failure beneath after the answer is reported | The event settles only when nothing it started is running | +| 10 s own-time budget, 1 s for `.catch`, time inside `next` and `$` calls excluded except `$.clock.sleep` | Same, configurable (`hookTimeoutMs`, `catchTimeoutMs`) | — | +| Not returning a result object is a failure | Same; `null` is accepted as an answer (a surface drawn empty) | `ui.render` with nothing to draw | +| A result's unknown fields are dropped and wrong types fail the hook | A `prompt.submit` result with wrong types keeps the original text and only the string context lines | Lenient on the one event where partial results are common | + +## `$` members + +| Namespace | Claude Code | This bridge | +|---|---|---| +| `$.plugin` | `name`, `root`, `tier` | `name`, `root` from the `defineMod` spec | +| `$.ui` | `resolve`, `invalidate`, `open`, `close`, `panes`, `focus`, `log`, `toast`, `status`, `notice`, `ask`, `blit`, `copy`, `press`, `input`, `select` | `resolve`, `invalidate`, `open`, `close`, `panes`, `log`, `toast`, `status`, `ask`; `open` answers `{ isPlaced: false }` with a reason and redraws the band; `log`, `toast`, `status` reach the host log; the rest reject | +| `$.command` | `register`, `run`, `list` | Same; `run` of a command nobody answers rejects with the registering mod named | +| `$.tool` | `register`, `call`, `list` | Same; a registered tool is `mcp____`; `call` runs the harness pipeline and injects a tool's deferred contexts into the session | +| `$.prompt` | `submit`, `compose` | `submit`; the message is `user`-sourced, framed `Message from the "" mod:` unless `asUser`, and the `prompt.submit` it raises names the mod as origin | +| `$.session` | `id`, `cwd`, `root`, `repo`, `model`, `turns`, `messages`, `usage`, `version`, `send`, `append`, `authorize`, `compact`, `surfaces` | `id`, `cwd`, `root`, `model`, `turns`, `messages`, `usage`, `version`; `cwd` reports the committed current directory; `root` retains the original project; `usage.context.window` is `0` and `percent` absent until the token meter knows the route's window; `rateLimits` is empty; `version` names this bridge | +| `$.state` | Per session, survives hot reload | Per session, in memory; a read during `ui.render` subscribes the band to the slot | +| `$.store` | Per plugin, durable | Same, in the `claude_code_mods` storage domain, 4 MiB per plugin | +| `$.clock` | `now`, `sleep`, `after`, `every`; timers die with the plugin | Same; a timer also dies with the session whose event scheduled it, and `sleep` rejects when the event is cancelled | +| `$.fs` | `read`, `write`, `list`, `exists`, `stat`, `ancestors` | All but `ancestors`; 4 MiB per file; `stat.mtimeMs` is `0` | +| `$.process` | `run`, `spawn` | `run` only, argv without a shell, `processTimeoutMs` default | +| `$.http` | `fetch` | Same, bodies up to 4 MiB | +| `$.env` | `get`, `set` | Same; the process environment is shared by every session and plugin | +| `$.model`, `$.agent`, `$.config`, `$.settings`, `$.mcp`, `$.audio`, `$.telemetry`, `$.turn` | Served | Reject with `no implementation`; `$.model.complete` waits on a logged side-request event | + +## Drawing + +| Claude Code | This bridge | +|---|---| +| `Box`, `Text`, `Button`, `Input`, `Spacer`, and the host's own element kinds | `Box`, `Text`, `Button` from `$.ui.resolve(e)`; the tree is validated, and a `Button`'s `onPress` stays in the host behind a per-drawing action id | +| `Box` props: `flexDirection`, `padding*`, `gap`, `border*`, `width`, `height`, `overflow`, `scroll` | `flexDirection`, `padding`, `paddingX`, `paddingY`, `gap`, `border`, `borderColor`; the others serialize and the Web band ignores them | +| `Text` props: `color`, `bold`, `dimColor`, `italic`, `underline`, `wrap`, `truncate` | `color` (the terminal palette names), `bold`, `dimColor`, `italic`, `underline`; the Web band wraps | +| `Button` props: `label`, `hotkey`, `disabled`, `onPress`; a bare digit arms the hotkey | `label`, `hotkey` (shown as a hint), `disabled`, `onPress` (a click) | +| The band is one instance; `hasSurvey`, `isWorking`, `maxRows`, `bodyColumns` reflect the terminal | One instance per session, consulted in mod load order; `bodyColumns` and `viewport.columns` are the bridge's `bandColumns` (default `120`), `maxRows` its `bandRows` (default `10`), `hasSurvey` and `isWorking` are `false`; the Web band wraps instead of scrolling | +| The band redraws on the host's frame and when a `$.state` value a render read changes | Redraws after `session.start`, when a `$.state` value the last drawing read is written, after `$.ui.open`, `$.ui.close`, `$.ui.invalidate`, after each `tool.call` chain and `turn.complete` settle, and after a press; a mod that changes module-level state at other times draws it at the next trigger. A press runs `onPress` without a time limit | +| `Pane` with `focus`, `scroll`, docked beside the transcript | No pane: `$.ui.open` answers `{ isPlaced: false }` so a mod falls back to the band; the test kit mounts `Pane` hooks directly | +| Hotkeys, `TextInput`, focus, scrolling, `ui.press`, `ui.input`, `ui.select` | Not served; follow-up | + +## Testing + +| `claude-code/testing` | This bridge | +|---|---| +| `describe`, `test(name, ($, on) => …)`, `expect`, `mock`, `tier` | Inside this repository the same module name provides them over Vitest; `test` infers the mod from the test file's location (`/tests/*.test.ts` → `/index.ts`) and `defineModTests` overrides it. Outside, `createModTestKit` and `mock` come from `@deepseek-ai/dsh-experimental-claude-code-mods/testing` | +| `on` stubs the engine's answers; the kit answers `ui.*` itself | Same; `ui.open` answers `{ isPlaced: false }`, `ui.invalidate` and `ui.close` succeed; `session.cwd`, `process.run`, `fs.*`, `store.*`, `env.*` need a stub (`mock.store`, `mock.env`, `mock.clock` answer whole namespaces) | +| `$.ui.mount({ plugin, surface, component, props })` → `find`, `findAll`, `press`, `unmount` | Same, plus `tree()` and `text()`; every read renders afresh through every loaded mod's hooks; `plugin` and `surface` are accepted for source compatibility | +| 5 s hook budget in tests | Same (`budgetMs`) | + +## The example mods + +| Mod | Source | Through this bridge | +|---|---|---| +| Token Weather | The post's module, type contract, and test, unchanged | Runs as published; the test's three `as any` casts are dropped because the kit is typed. Draws in the band from `$.session.usage` through the token meter | +| Blast Radius | The post's `tool.call` hook unchanged; `classify`, `measure`, and the trees completed to the post's description | Holds the command with real `sleep` polls, draws the report in the band (`isPlaced: false`), and Proceed or Cancel decides; load it before Token Weather | +| Replay Theater | The post's five hooks unchanged; `stepsFor`, the diff, `openReplay`, the pane and the hint completed | Records Edit and Write calls, hints in the band after the turn, and steps through the diffs in the band because no pane is placed | +| `diff` ([anthropics/claude-code](https://github.com/anthropics/claude-code/tree/main/mods/diff), read at `52c7644`) | TypeScript, ~570 files, hooks `command.run`, `prompt.submit`, `session.start`, `tool.call`, `ui.close`, `ui.focus`, `ui.render`, `ui.scroll` | Not runnable: needs `$.settings.read`, `$.telemetry.*`, `$.session.messages` over a pane, `ui.focus` and `ui.scroll` events, and a placed `Pane`. Assessed by reading; the repository's terms do not allow a fixture copy | +| `agents-md` (same repository) | Hooks `agent.spawn`, `prompt.context`, `session.start`, `tool.call` | Not runnable: `prompt.context` and `agent.spawn` are not raised and `$.fs.ancestors`, `$.telemetry.*` are not served | +| `sec-default` (same repository) | Hooks `tool.check`, `prompt.compose`, `settings.read`, `classic.*`, `plugin.register`, … | Not runnable: managed-tier policy over events this host does not raise | +| `telemetry` (same repository) | Hooks `engine.create`, `telemetry.*`, `session.start`, `session.end`; calls `$.session.repo`, `$.settings.read` | Not runnable: `telemetry.*` and `engine.create` are not raised and the two `$` members are not served | + + + + + +## Cordis API + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.claudeCodeMods` — `ClaudeCodeMods` + +The bridge service: loaded mods, their hooks, and the harness listeners that raise their events. Mods join through ClaudeCodeMods.add, which defineMod calls when a mod plugin mounts. The Remote methods let a Client draw each session's band above the prompt and press its buttons. + +```ts cordis-catalog +/** + * Watch the band above the prompt of one session: the current drawing, then + * every redraw, until the Client stops watching. + * @param agent - the session's agent, resolved by the Gateway. + * @param signal - carrier cancellation. + * @returns the band's snapshots. + */ +@Remote({ mode: 'stream' }) watchBand(agent: Agent, signal: AbortSignal): AsyncIterable + +/** + * Press a button of the band's current drawing: runs the mod's `onPress` and redraws. + * @param agent - the session's agent, resolved by the Gateway. + * @param generation - the drawing the Client saw. + * @param actionId - the button's action id in that drawing. + * @returns the snapshot after the press. + */ +@Remote pressBand(agent: Agent, generation: number, actionId: string): Promise + +/** + * Load one mod beneath every mod loaded before it: run its `register`, + * keep its hooks, and report which of its events this host never raises. + * @param definition - the mod as its plugin defined it. + * @returns the disposer that removes the mod's hooks, closes its timers, and releases its registrations. + * @throws Error when the name is invalid or taken, or when `register` throws (Claude Code's `hooks module did not load` wording). + */ +async add(definition: ModDefinition): Promise<() => Promise> +``` + +Types: [Agent](core.md) + +Source: [`packages/experimental/claude-code-mods/src/index.ts`](../../packages/experimental/claude-code-mods/src/index.ts) + diff --git a/docs/subsystems/claude-code-mods.zh.md b/docs/subsystems/claude-code-mods.zh.md new file mode 100644 index 0000000000..696b6af1e7 --- /dev/null +++ b/docs/subsystems/claude-code-mods.zh.md @@ -0,0 +1,146 @@ +# Claude Code 模组:兼容性 + +[English](claude-code-mods.md) | 中文 + +模组经本桥接运行时与在 Claude Code 下行为不同的每一处,基准为 Claude Code 2.1.287([模组参考](https://code.claude.com/docs/en/plugins/mods/reference)及其 `claude-code.d.ts`)与 2026-10-01 的 [Getting started with Claude Code mods](https://claude.dev/blog/getting-started-with-claude-code-mods/) 一文。每行给出 Claude Code 的行为、本桥接的做法,以及原因或后续工作。未列出的一切均按参考文档描述的方式运行;对本宿主永不触发的事件注册钩子会在模组加载时以警告点名,服务表之外的 `$` 调用以 `no implementation for .` 拒绝。 + +## 打包与加载 + +| Claude Code | 本桥接 | 原因或后续 | +|---|---|---| +| 一个插件目录,含 `.claude-plugin/plugin.json` 与指定一个钩子模块的 `hooks/hooks.json`;`claude --plugin-dir`、市场、`/plugin install` | 模组就是插件:`defineMod({ name, version, root, userConfig, register })` 包装钩子模块,并在桥接之后经 cordis.yml 挂载;不读取 `plugin.json` 与 `hooks.json` | 模组加入每个 DSH 插件都使用的同一组合机制;示例目录保留其清单,因此也能在 `claude --plugin-dir` 下运行 | +| `plugin.json` 中带类型默认值的 `userConfig` schema;选项在加载时校验 | `userConfig` 是 `defineMod` 规格的默认值对象;插件的 cordis.yml `config` 覆盖其上,并按字符串、数字、布尔与字符串列表的记录校验 | 不读取清单 | +| 层级(`user`、`project`、`managed`)与 `next.to(e, tier)` | 每个模组都以 `user` 加载;测试中的 `tier()` 被接受并忽略;`next.to` 拒绝 | 托管设置不是 DSH 的概念 | +| 编辑时热重载钩子模块;`claude plugin validate`、`claude plugin test`、由 `types/index.d.ts` 生成类型 | 这些命令都没有;重新挂载会在同一已求值模块上重新运行 `register`,模块级变量保留其值 | 仅按需跟进;测试工具包的 `createModTestKit` 在 Vitest 中替代 `claude plugin test` | +| 钩子模块可以是 TypeScript | `.ts` 钩子模块只在启动器转译时可加载(源码启动可以;构建后的安装不行) | DSH 交付的是纯 Node | +| 模组在进程内运行,`$` 是其访问宿主的唯一途径 | 钩子模块以 Node 全局对象在进程内运行,没有访问规则,并拥有进程的全部权限:`$.env` 读写 harness 环境变量,`$.http.fetch` 可访问任意 URL,`$.fs` 与 `$.tool.call` 以会话的身份行动 | 不对模组施加沙箱;只挂载你愿意作为插件运行的模组 | +| `import type { … } from 'claude-code'`、`declare module 'claude-code' { interface PluginState }` | `claude-code` 模块名只在本仓库的测试设置中解析到桥接的类型;仓库外的模组导入 `@deepseek-ai/dsh-experimental-claude-code-mods` | 类型名是桥接自己的 | +| `hooks.json` 中的设置钩子与模组钩子并行运行 | 不运行;为它们挂载 `@deepseek-ai/dsh-hooks-claude-code` | 不同的桥接 | + +## 事件 + +| 事件 | Claude Code | 本桥接 | +|---|---|---| +| `session.start` | 每会话触发一次,在首个提示词之前 | 由根智能体的 `agent/created` 触发,在其首轮之前等待完成;不为子智能体触发 | +| `session.end` | 每会话触发一次 | 由根智能体的 `agent/disposed` 触发;钩子结束前 `$.state` 可读 | +| `prompt.submit` | `e.text` 是输入的提示词;`context` 条目各成一块跟在输入的提示词之后;插件提交的提示词带 `origin: { kind: 'plugin', name }` | 相同;`e.text` 拼接已认领批次中人类自己(`user` 来源)消息的文本块,仅含注入上下文的批次不触发,`$.prompt.submit` 排入的提示词以提交的模组为来源 | +| `turn.start`、`turn.complete` | 每轮触发;子智能体的轮带 `agentId` | 相同;`turn.complete` 的 `{ text }` 到达宿主日志,而非答案下方的一行;`durationMs` 从 `turn/start` 起算 | +| `tool.call` | 在 Claude Code 的权限检查之前运行;`next({ ...e, command })` 改写工具实际运行的参数,`next({ ...e, tool })` 把调用改派给另一工具 | 包裹 `tools/execute` 运行,在 harness 权限决定之后;改写参数或改名工具的钩子(或 `.catch` 处理器)会被跳过并报告——参数情形指向[工具前输入改写提案](../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.zh.md)——因为已写入日志的调用按原样运行。`{ deny }`、`{ result }`、观察以及在 `next` 之后改写结果或 `isError` 均按描述工作 | +| `command.run` | 为模组注册的命令运行 | 相同 | +| `ui.render` | 为每个渲染位点触发(`AbovePrompt`、`Pane`、`ToolUse`、`AssistantMessage`、`Spinner`……) | 只为 `AbovePrompt` 触发,每次会话横幅重绘一次;`Pane` 钩子只经测试工具包的 `$.ui.mount({ component: 'Pane' })` 运行 | +| `tool.check`、`tool.describe`、`tool.list`、`tool.register` | 塑造工具列表与权限 | 不触发;`$.tool.register` 与 `$.tool.list` 作为 `$` 调用服务 | +| `turn.step` | 每个模型步骤 | 不触发 | +| 参考中的其他所有事件:其他 `prompt.*` 事件、`skill.prompt`、`attribution.text`、`command.describe`、`config.*`、其他 `session.*` 事件、`agent.*`、`plugin.register`、`engine.create`、`telemetry.*`、`ui.resolve`、`ui.press`、`ui.input`、`ui.select`、`ui.focus`、`ui.scroll`、`ui.close`、`ui.message` | 由引擎触发 | 可注册,永不触发,在加载时以警告点名 | +| `classic.*` | 经典设置钩子作为事件 | 在 `register` 时拒绝,与任何未知事件名相同 | +| `.` 操作事件(`fs.read`、`store.set`……) | 更晚加载的模组的 `$` 调用到达在它之前加载的模组 | 相同;`tool.call` 则经由工具流水线到达 | + +## 链 + +| Claude Code | 本桥接 | 原因 | +|---|---|---| +| 钩子可多次调用 `next`;每次调用都再次运行下层链 | `next` 只运行一次:第二次调用返回第一次运行的结果 | 每条写入日志的调用只执行一次工具 | +| 钩子在其 `next` 仍在运行时作答 | 下层运行会在钩子答案落定前被等待;答案之后的下层失败会被报告 | 事件只在其启动的一切都结束后落定 | +| 自身时间预算 10 秒,`.catch` 为 1 秒,`next` 与 `$` 调用内的时间不计,`$.clock.sleep` 除外 | 相同,可配置(`hookTimeoutMs`、`catchTimeoutMs`) | — | +| 不返回结果对象视为失败 | 相同;`null` 被接受为答案(绘制为空的表面) | 无可绘制内容的 `ui.render` | +| 结果中的未知字段被丢弃,类型错误使钩子失败 | 类型错误的 `prompt.submit` 结果保留原文本,并只取字符串形式的 context 行 | 对这一常见部分结果的事件从宽处理 | + +## `$` 成员 + +| 命名空间 | Claude Code | 本桥接 | +|---|---|---| +| `$.plugin` | `name`、`root`、`tier` | 来自 `defineMod` 规格的 `name`、`root` | +| `$.ui` | `resolve`、`invalidate`、`open`、`close`、`panes`、`focus`、`log`、`toast`、`status`、`notice`、`ask`、`blit`、`copy`、`press`、`input`、`select` | `resolve`、`invalidate`、`open`、`close`、`panes`、`log`、`toast`、`status`、`ask`;`open` 带原因回答 `{ isPlaced: false }` 并重绘横幅;`log`、`toast`、`status` 到达宿主日志;其余拒绝 | +| `$.command` | `register`、`run`、`list` | 相同;`run` 一个无人回答的命令时拒绝,并点名注册它的模组 | +| `$.tool` | `register`、`call`、`list` | 相同;注册的工具为 `mcp____`;`call` 运行 harness 流水线,并把工具推迟的上下文注入会话 | +| `$.prompt` | `submit`、`compose` | `submit`;消息为 `user` 来源,除非 `asUser` 否则套上 `Message from the "" mod:` 框架,它触发的 `prompt.submit` 以该模组为来源 | +| `$.session` | `id`、`cwd`、`root`、`repo`、`model`、`turns`、`messages`、`usage`、`version`、`send`、`append`、`authorize`、`compact`、`surfaces` | `id`、`cwd`、`root`、`model`、`turns`、`messages`、`usage`、`version`;`cwd` 报告已提交的当前目录;`root` 保留原始项目;在 token 计量器得知路由窗口前 `usage.context.window` 为 `0` 且无 `percent`;`rateLimits` 为空;`version` 指明本桥接 | +| `$.state` | 按会话,热重载后保留 | 按会话,内存中;`ui.render` 期间的读取让横幅订阅该槽位 | +| `$.store` | 按插件,持久 | 相同,在 `claude_code_mods` 存储域中,每插件 4 MiB | +| `$.clock` | `now`、`sleep`、`after`、`every`;定时器随插件消亡 | 相同;定时器还随安排它的事件所属会话消亡,事件取消时 `sleep` 拒绝 | +| `$.fs` | `read`、`write`、`list`、`exists`、`stat`、`ancestors` | 除 `ancestors` 外全部;每文件 4 MiB;`stat.mtimeMs` 为 `0` | +| `$.process` | `run`、`spawn` | 仅 `run`,不经 shell 的 argv,默认 `processTimeoutMs` | +| `$.http` | `fetch` | 相同,正文最多 4 MiB | +| `$.env` | `get`、`set` | 相同;进程环境由所有会话与插件共享 | +| `$.model`、`$.agent`、`$.config`、`$.settings`、`$.mcp`、`$.audio`、`$.telemetry`、`$.turn` | 已服务 | 以 `no implementation` 拒绝;`$.model.complete` 等待一个写入日志的副请求事件 | + +## 绘制 + +| Claude Code | 本桥接 | +|---|---| +| `Box`、`Text`、`Button`、`Input`、`Spacer` 以及宿主自己的元素种类 | 来自 `$.ui.resolve(e)` 的 `Box`、`Text`、`Button`;树经校验,`Button` 的 `onPress` 留在宿主中,位于按绘制分配的动作 id 之后 | +| `Box` 属性:`flexDirection`、`padding*`、`gap`、`border*`、`width`、`height`、`overflow`、`scroll` | `flexDirection`、`padding`、`paddingX`、`paddingY`、`gap`、`border`、`borderColor`;其余会序列化,Web 横幅忽略 | +| `Text` 属性:`color`、`bold`、`dimColor`、`italic`、`underline`、`wrap`、`truncate` | `color`(终端调色板名)、`bold`、`dimColor`、`italic`、`underline`;Web 横幅会换行 | +| `Button` 属性:`label`、`hotkey`、`disabled`、`onPress`;单个数字键触发热键 | `label`、`hotkey`(显示为提示)、`disabled`、`onPress`(点击) | +| 横幅是一个实例;`hasSurvey`、`isWorking`、`maxRows`、`bodyColumns` 反映终端 | 每会话一个实例,按模组加载顺序咨询;`bodyColumns` 与 `viewport.columns` 取桥接的 `bandColumns`(默认 `120`),`maxRows` 取 `bandRows`(默认 `10`),`hasSurvey` 与 `isWorking` 为 `false`;Web 横幅换行而非滚动 | +| 横幅随宿主帧重绘,并在渲染读取过的 `$.state` 值变化时重绘 | 在 `session.start` 之后、上次绘制读取过的 `$.state` 值被写入时、在 `$.ui.open`、`$.ui.close`、`$.ui.invalidate` 之后、在每条 `tool.call` 链与 `turn.complete` 结束后以及按下之后重绘;在其他时刻改变模块级状态的模组在下一次触发时绘出。按下运行 `onPress` 时没有时间限制 | +| 带 `focus`、`scroll`、停靠在对话旁的 `Pane` | 没有面板:`$.ui.open` 回答 `{ isPlaced: false }`,模组回退到横幅;测试工具包直接挂载 `Pane` 钩子 | +| 热键、`TextInput`、焦点、滚动、`ui.press`、`ui.input`、`ui.select` | 不服务;后续工作 | + +## 测试 + +| `claude-code/testing` | 本桥接 | +|---|---| +| `describe`、`test(name, ($, on) => …)`、`expect`、`mock`、`tier` | 在本仓库内同一模块名基于 Vitest 提供它们;`test` 从测试文件位置推断模组(`/tests/*.test.ts` → `/index.ts`),`defineModTests` 可覆盖。仓库外则从 `@deepseek-ai/dsh-experimental-claude-code-mods/testing` 获取 `createModTestKit` 与 `mock` | +| `on` 为引擎的答案打桩;工具包自行回答 `ui.*` | 相同;`ui.open` 回答 `{ isPlaced: false }`,`ui.invalidate` 与 `ui.close` 成功;`session.cwd`、`process.run`、`fs.*`、`store.*`、`env.*` 需要桩(`mock.store`、`mock.env`、`mock.clock` 回答整个命名空间) | +| `$.ui.mount({ plugin, surface, component, props })` → `find`、`findAll`、`press`、`unmount` | 相同,另有 `tree()` 与 `text()`;每次读取都经所有已加载模组的钩子重新渲染;`plugin` 与 `surface` 为源码兼容而接受 | +| 测试中钩子预算 5 秒 | 相同(`budgetMs`) | + +## 示例模组 + +| 模组 | 来源 | 经本桥接 | +|---|---|---| +| Token Weather | 文中的模块、类型契约与测试,原样 | 按发布原样运行;测试中的三处 `as any` 转换因工具包带类型而去掉。通过 token 计量器从 `$.session.usage` 在横幅中绘制 | +| Blast Radius | 文中的 `tool.call` 钩子原样;`classify`、`measure` 与树按文中描述补全 | 以真实的 `sleep` 轮询持有命令,在横幅中绘制报告(`isPlaced: false`),由 Proceed 或 Cancel 决定;请加载在 Token Weather 之前 | +| Replay Theater | 文中的五个钩子原样;`stepsFor`、差异、`openReplay`、面板与提示补全 | 记录 Edit 与 Write 调用,轮结束后在横幅中提示,并因无面板放置而在横幅中逐步浏览差异 | +| `diff`([anthropics/claude-code](https://github.com/anthropics/claude-code/tree/main/mods/diff),于 `52c7644` 阅读) | TypeScript,约 570 个文件,钩住 `command.run`、`prompt.submit`、`session.start`、`tool.call`、`ui.close`、`ui.focus`、`ui.render`、`ui.scroll` | 不可运行:需要 `$.settings.read`、`$.telemetry.*`、面板上的 `$.session.messages`、`ui.focus` 与 `ui.scroll` 事件以及已放置的 `Pane`。以阅读方式评估;该仓库的条款不允许复制夹具 | +| `agents-md`(同一仓库) | 钩住 `agent.spawn`、`prompt.context`、`session.start`、`tool.call` | 不可运行:`prompt.context` 与 `agent.spawn` 不触发,`$.fs.ancestors`、`$.telemetry.*` 不服务 | +| `sec-default`(同一仓库) | 钩住 `tool.check`、`prompt.compose`、`settings.read`、`classic.*`、`plugin.register`…… | 不可运行:基于本宿主不触发的事件的托管层策略 | +| `telemetry`(同一仓库) | 钩住 `engine.create`、`telemetry.*`、`session.start`、`session.end`;调用 `$.session.repo`、`$.settings.read` | 不可运行:`telemetry.*` 与 `engine.create` 不触发,两个 `$` 成员不服务 | + + + + + +## Cordis API + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.claudeCodeMods` — `ClaudeCodeMods` + +The bridge service: loaded mods, their hooks, and the harness listeners that raise their events. Mods join through ClaudeCodeMods.add, which defineMod calls when a mod plugin mounts. The Remote methods let a Client draw each session's band above the prompt and press its buttons. + +```ts cordis-catalog +/** + * Watch the band above the prompt of one session: the current drawing, then + * every redraw, until the Client stops watching. + * @param agent - the session's agent, resolved by the Gateway. + * @param signal - carrier cancellation. + * @returns the band's snapshots. + */ +@Remote({ mode: 'stream' }) watchBand(agent: Agent, signal: AbortSignal): AsyncIterable + +/** + * Press a button of the band's current drawing: runs the mod's `onPress` and redraws. + * @param agent - the session's agent, resolved by the Gateway. + * @param generation - the drawing the Client saw. + * @param actionId - the button's action id in that drawing. + * @returns the snapshot after the press. + */ +@Remote pressBand(agent: Agent, generation: number, actionId: string): Promise + +/** + * Load one mod beneath every mod loaded before it: run its `register`, + * keep its hooks, and report which of its events this host never raises. + * @param definition - the mod as its plugin defined it. + * @returns the disposer that removes the mod's hooks, closes its timers, and releases its registrations. + * @throws Error when the name is invalid or taken, or when `register` throws (Claude Code's `hooks module did not load` wording). + */ +async add(definition: ModDefinition): Promise<() => Promise> +``` + +Types: [Agent](core.zh.md) + +Source: [`packages/experimental/claude-code-mods/src/index.ts`](../../packages/experimental/claude-code-mods/src/index.ts) + diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index c6c02c6c42..665df11563 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -497,12 +497,12 @@ async register(definition: PresetDefinition): Promise<() => Promise> /** Inspect retained revisions, or the exact revision an Agent joined. * @param ctx - optional Agent context; omission includes all retained revisions. - * @returns detached module references and isolation diagnostics; no match returns an empty list. + * @returns detached definition and module row identities, resolution bases, and isolation diagnostics; no match returns an empty list. */ inspectCompositions(ctx?: Context): AgentPresetInspection[] /** Read every declared preset, including activation failures. - * @returns Display metadata and loading diagnostics. + * @returns Display metadata, declaring Loader row identities, and loading diagnostics. */ async list(): Promise diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index 582a817f1f..44ee6d3605 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -507,12 +507,12 @@ async register(definition: PresetDefinition): Promise<() => Promise> /** Inspect retained revisions, or the exact revision an Agent joined. * @param ctx - optional Agent context; omission includes all retained revisions. - * @returns detached module references and isolation diagnostics; no match returns an empty list. + * @returns detached definition and module row identities, resolution bases, and isolation diagnostics; no match returns an empty list. */ inspectCompositions(ctx?: Context): AgentPresetInspection[] /** Read every declared preset, including activation failures. - * @returns Display metadata and loading diagnostics. + * @returns Display metadata, declaring Loader row identities, and loading diagnostics. */ async list(): Promise diff --git a/docs/subsystems/jobs.i18n.yaml b/docs/subsystems/jobs.i18n.yaml index e7264086e8..17ea2f9f9e 100644 --- a/docs/subsystems/jobs.i18n.yaml +++ b/docs/subsystems/jobs.i18n.yaml @@ -9,8 +9,8 @@ en: 529f401da4757a98 zh: 041ff5e86b5dd8db /background-job-runtime/producer-contract: - en: 2b9c087048b73091 - zh: 9d1ff560ce79c9b1 + en: af0be338a0ba5e60 + zh: fc0eefb8c31f3a98 /background-job-runtime/the-output-ring: en: eee762c5ae896a2d zh: 93984e2007381ae8 diff --git a/docs/subsystems/jobs.md b/docs/subsystems/jobs.md index ab60d0bf1a..6cc4e3e5a5 100644 --- a/docs/subsystems/jobs.md +++ b/docs/subsystems/jobs.md @@ -15,7 +15,6 @@ Types shared by long-running producers, `ctx.jobs`, and job controls. The [seam */ interface JobKindMap { bash: 'bash' - subagent: 'subagent' } ``` @@ -33,7 +32,7 @@ A `JobSpec` declares identity, the owning session, optional pull `output` source * the output ring. */ interface JobSpec { - /** Producer kind — also the id prefix (`bash`, `subagent`, …). */ + /** Producer kind — also the id prefix (`bash`, …). */ kind: JobKind /** One-line model-facing label (the command; the delegation description). */ label: string @@ -95,7 +94,7 @@ interface JobHandle { } ``` -`JobHooks.done` resolves after the producer releases its resources, not merely when work finishes. A job whose result is a value rather than a stream — a subagent's report, a workflow's rendered result — returns it as `JobOutcome.result`; the model's first read after settlement carries it once. +`JobHooks.done` resolves after the producer releases its resources, not merely when work finishes. A job whose result is a value rather than a stream — a workflow's rendered result — returns it as `JobOutcome.result`; the model's first read after settlement carries it once. ```ts type-equiv /** Hooks through which the runtime controls and observes producer work. */ diff --git a/docs/subsystems/jobs.zh.md b/docs/subsystems/jobs.zh.md index 72055ac5c0..b80f64fade 100644 --- a/docs/subsystems/jobs.zh.md +++ b/docs/subsystems/jobs.zh.md @@ -15,7 +15,6 @@ */ interface JobKindMap { bash: 'bash' - subagent: 'subagent' } ``` @@ -33,7 +32,7 @@ interface JobKindMap { * the output ring. */ interface JobSpec { - /** Producer kind — also the id prefix (`bash`, `subagent`, …). */ + /** Producer kind — also the id prefix (`bash`, …). */ kind: JobKind /** One-line model-facing label (the command; the delegation description). */ label: string @@ -95,7 +94,7 @@ interface JobHandle { } ``` -`JobHooks.done` 会在生产方释放其资源后 resolve,而不是仅在工作完成时 resolve。结果是一个值而非流的 job——subagent 的报告、workflow 渲染出的结果——把它作为 `JobOutcome.result` 返回;模型在结算后的第一次读取携带它一次。 +`JobHooks.done` 会在生产方释放其资源后 resolve,而不是仅在工作完成时 resolve。结果是一个值而非流的 job——workflow 渲染出的结果——把它作为 `JobOutcome.result` 返回;模型在结算后的第一次读取携带它一次。 ```ts type-equiv /** Hooks through which the runtime controls and observes producer work. */ diff --git a/docs/subsystems/llm-streaming.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml index 8f8328bcc1..8491f61b6d 100644 --- a/docs/subsystems/llm-streaming.i18n.yaml +++ b/docs/subsystems/llm-streaming.i18n.yaml @@ -36,8 +36,8 @@ en: 58dec2259b0b1c6f zh: fc99fe965acaabf3 /llm-streaming/the-model-request: - en: ddca84b082554567 - zh: 804ffb1cb0c94bfa + en: 824d18f76f990711 + zh: 8a2895513652e528 /llm-streaming/the-model-request/the-request-envelope-llmcallconfig-and-the-logged-header: en: f2e6932e211f179b zh: 75112586a6f3f4aa diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md index 7b2825f4c7..f87c1e9d45 100644 --- a/docs/subsystems/llm-streaming.md +++ b/docs/subsystems/llm-streaming.md @@ -529,7 +529,7 @@ interface LlmModelContext { } ``` -Reasoning effort is another exact-route capability. The core brands identifiers but does not enumerate their values; each adapter owns the ordered set, display names, and optional deployment default. +Reasoning effort is another exact-route capability. The core brands identifiers but does not enumerate their values; each adapter owns the choices in increasing effort order, display names, and optional deployment default. Ordering compares selectable controls, not predicted token use, cost, or latency. ```ts type-equiv /** Adapter-owned identifier for one model's selectable reasoning effort. */ @@ -551,7 +551,7 @@ interface LlmReasoningEffortInfo { ```ts type-equiv /** Selectable reasoning efforts for one exact provider/model route. */ interface LlmModelReasoningInfo { - /** Supported efforts in adapter-preferred display order. */ + /** Supported efforts from least to greatest selectable reasoning effort, not predicted token use or latency. */ efforts: readonly LlmReasoningEffortInfo[] /** * Adapter-configured default materialized into requests when callers omit @@ -634,8 +634,8 @@ interface GenerateOptions { sessionId?: Branded<'SessionId'> /** * Provider-neutral classification for an auxiliary model call. Adapters may - * map the purpose to model-hidden transport metadata or purpose-specific - * generation policy. Ordinary conversation requests leave it unset. + * map the purpose to model-hidden transport metadata. Ordinary conversation + * requests leave it unset. */ purpose?: 'compaction' | 'session-title' } @@ -744,6 +744,16 @@ On the wire, a loop-built request is the derived history alone: the rendered pro FIXME(call-config-shape): revisit which remaining fields are genuinely epoch-level for cache purposes (`model` and the model-owned reasoning effort are explicit; the sampling scalars sit here out of caution). +```ts type-equiv +/** Concrete generation settings; omitted controls use the selected route's defaults. */ +interface LlmCallControls { + reasoningEffort?: ReasoningEffortId + temperature?: number + maxTokens?: number + stop?: string[] +} +``` + ```ts type-equiv /** * Provider, model, reasoning effort, and sampling scalars of one conversation's @@ -751,16 +761,28 @@ FIXME(call-config-shape): revisit which remaining fields are genuinely epoch-lev * the loop builds requests from the logged header rather than accepting these * per call. */ -interface LlmCallConfig { +interface LlmCallConfig extends LlmCallControls { provider: string model: string - reasoningEffort?: ReasoningEffortId - temperature?: number - maxTokens?: number - stop?: string[] } ``` +```ts type-equiv +/** + * Synchronous, pure configuration of one call before defaults and validation. + * Compose functions in the desired order; later writes replace earlier ones. + * The returned controls cannot change the captured route. Errors reject + * preparation before dispatch; only the resolved configuration is recordable. + * @param controls - detached, deeply frozen proposed controls, without defaults. + * @param model - detached, deeply frozen metadata from the captured adapter generation. + * @returns concrete controls; omitted fields receive the route's defaults. + */ +type ConfigureCall = ( + controls: Readonly, + model: Readonly, +) => LlmCallControls +``` + ```ts type-equiv /** * Effective config fields supplied by exact-model adapter resolution rather @@ -1054,11 +1076,15 @@ async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise +async prepareCall( config: LlmCallConfig, signal?: AbortSignal, configure?: ConfigureCall, ): Promise /** * Stream one model call as raw chunks (token-level deltas). Replay state is diff --git a/docs/subsystems/llm-streaming.zh.md b/docs/subsystems/llm-streaming.zh.md index e8688c1764..2f3c90e096 100644 --- a/docs/subsystems/llm-streaming.zh.md +++ b/docs/subsystems/llm-streaming.zh.md @@ -535,7 +535,7 @@ interface LlmModelContext { } ``` -推理强度是另一项针对确切路由的能力。核心为标识符添加品牌类型,但不枚举其值;有序集合、展示名称和可选的部署默认值均由各适配器持有。 +推理强度是另一项确切路由能力。核心为标识符添加品牌类型,但不枚举其值;每个适配器负责按强度递增排列的选项、显示名称与可选部署默认值。该顺序比较可选控制,不预测 token 用量、成本或延迟。 ```ts type-equiv /** Adapter-owned identifier for one model's selectable reasoning effort. */ @@ -557,7 +557,7 @@ interface LlmReasoningEffortInfo { ```ts type-equiv /** Selectable reasoning efforts for one exact provider/model route. */ interface LlmModelReasoningInfo { - /** Supported efforts in adapter-preferred display order. */ + /** Supported efforts from least to greatest selectable reasoning effort, not predicted token use or latency. */ efforts: readonly LlmReasoningEffortInfo[] /** * Adapter-configured default materialized into requests when callers omit @@ -640,8 +640,8 @@ interface GenerateOptions { sessionId?: Branded<'SessionId'> /** * Provider-neutral classification for an auxiliary model call. Adapters may - * map the purpose to model-hidden transport metadata or purpose-specific - * generation policy. Ordinary conversation requests leave it unset. + * map the purpose to model-hidden transport metadata. Ordinary conversation + * requests leave it unset. */ purpose?: 'compaction' | 'session-title' } @@ -750,6 +750,16 @@ interface LlmDiscoveredModel { FIXME(call-config-shape):重新审视其余哪些字段出于缓存目的确实属于 epoch 层级(`model` 和模型持有的推理强度已明确属于;采样标量目前出于谨慎保留在此)。 +```ts type-equiv +/** Concrete generation settings; omitted controls use the selected route's defaults. */ +interface LlmCallControls { + reasoningEffort?: ReasoningEffortId + temperature?: number + maxTokens?: number + stop?: string[] +} +``` + ```ts type-equiv /** * Provider, model, reasoning effort, and sampling scalars of one conversation's @@ -757,16 +767,28 @@ FIXME(call-config-shape):重新审视其余哪些字段出于缓存目的确 * the loop builds requests from the logged header rather than accepting these * per call. */ -interface LlmCallConfig { +interface LlmCallConfig extends LlmCallControls { provider: string model: string - reasoningEffort?: ReasoningEffortId - temperature?: number - maxTokens?: number - stop?: string[] } ``` +```ts type-equiv +/** + * Synchronous, pure configuration of one call before defaults and validation. + * Compose functions in the desired order; later writes replace earlier ones. + * The returned controls cannot change the captured route. Errors reject + * preparation before dispatch; only the resolved configuration is recordable. + * @param controls - detached, deeply frozen proposed controls, without defaults. + * @param model - detached, deeply frozen metadata from the captured adapter generation. + * @returns concrete controls; omitted fields receive the route's defaults. + */ +type ConfigureCall = ( + controls: Readonly, + model: Readonly, +) => LlmCallControls +``` + ```ts type-equiv /** * Effective config fields supplied by exact-model adapter resolution rather @@ -1060,11 +1082,15 @@ async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise +async prepareCall( config: LlmCallConfig, signal?: AbortSignal, configure?: ConfigureCall, ): Promise /** * Stream one model call as raw chunks (token-level deltas). Replay state is diff --git a/docs/subsystems/session-title.i18n.yaml b/docs/subsystems/session-title.i18n.yaml index 1ca280db5c..a701f0ad9d 100644 --- a/docs/subsystems/session-title.i18n.yaml +++ b/docs/subsystems/session-title.i18n.yaml @@ -9,8 +9,8 @@ en: c6be2d86647daa6f zh: fb259d9fb4738f39 /session-titles/auxiliary-request-record: - en: 4942bec7a7d1d9de - zh: 9edfc1c908577c37 + en: c36916905dad76f9 + zh: c82ff6c3d62e53e5 /session-titles/provider-input-and-output: en: 886e036e442ae06a zh: 49f95782b8aa9bf3 diff --git a/docs/subsystems/session-title.md b/docs/subsystems/session-title.md index 75e17f06d7..699b0148a6 100644 --- a/docs/subsystems/session-title.md +++ b/docs/subsystems/session-title.md @@ -64,7 +64,7 @@ interface SessionTitleSnapshot extends SessionTitleEventData { ## Auxiliary request record -The shared LLM helper records each validated, dispatchable title request before calling the model. The payload reproduces the model-visible system and message input, routing, output limit, provider ownership, and source-message attribution even when generation later fails. +The shared LLM helper records each validated, dispatchable title request before calling the model. The payload reproduces the model-visible system and message input, routing, output limit, resolved reasoning effort, provider ownership, and source-message attribution even when generation later fails. ```ts type-equiv /** Exact model-visible request recorded before one auxiliary title dispatch. */ @@ -81,6 +81,8 @@ interface SessionTitleLlmRequestEventData { readonly messages: Message[] /** Exact auxiliary output-token cap. */ readonly maxTokens: number + /** Resolved reasoning effort when recorded; older requests can omit it. */ + readonly reasoningEffort?: ReasoningEffortId } ``` diff --git a/docs/subsystems/session-title.zh.md b/docs/subsystems/session-title.zh.md index 187dad8f3a..1b360411a8 100644 --- a/docs/subsystems/session-title.zh.md +++ b/docs/subsystems/session-title.zh.md @@ -64,7 +64,7 @@ interface SessionTitleSnapshot extends SessionTitleEventData { ## 辅助请求记录 -共享 LLM 辅助组件会在调用模型前,记录每一项已经过验证且可分发的标题请求。即使后续生成失败,载荷仍会复现模型可见的系统输入与消息输入、路由、输出上限、提供方归属和源消息归因。 +共享 LLM 辅助组件会在调用模型前,记录每一项已经过验证且可分发的标题请求。即使后续生成失败,载荷仍会复现模型可见的系统输入与消息输入、路由、输出上限、解析后的推理强度、提供方归属和源消息归因。 ```ts type-equiv /** Exact model-visible request recorded before one auxiliary title dispatch. */ @@ -81,6 +81,8 @@ interface SessionTitleLlmRequestEventData { readonly messages: Message[] /** Exact auxiliary output-token cap. */ readonly maxTokens: number + /** Resolved reasoning effort when recorded; older requests can omit it. */ + readonly reasoningEffort?: ReasoningEffortId } ``` diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml index 38dff01a1b..1a778923dc 100644 --- a/docs/subsystems/session.i18n.yaml +++ b/docs/subsystems/session.i18n.yaml @@ -59,6 +59,9 @@ /sessions/plugin-contributed-log-only-events: en: 0a014a755b75449f zh: 9d59d74490a1bcb2 +/sessions/plugin-contributed-log-only-events/plugin-records: + en: 15102fd0b937fbf3 + zh: 37ebf1d715db097c /sessions/durability-contract: en: 62e3e4fbcba292a3 zh: cb6dcaf531621e85 diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md index 65bcc3edb4..1dd08630bb 100644 --- a/docs/subsystems/session.md +++ b/docs/subsystems/session.md @@ -752,6 +752,49 @@ When several events in one plugin-owned family assemble into one Web Client Conv The hook bridges' `hook/invoked` / `hook/result` pairs (from `@deepseek-ai/dsh-hook-protocol`) correlate by `handlerId`. `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` fire inside the loop's open turn, so their `hook/*` records are turn-enclosed by construction. `SessionStart` gets no `hook/*` record because it runs before turn 1; its context remains pending in the inbox until a waking delivery opens a turn. +### Plugin records + +Experimental packages declare plugin-owned state in `PluginRecordMap`: `appendPluginRecord()` uses each declaration to type its name and payload, and appends an event marked `ignorable: true` in the `plugin:` namespace. `pluginRecordOf()` reads stored records with unknown payloads regardless of current declarations. The [experimental persistence catalog](../experimental-persistence-catalog.md) lists current declarations. The [package README](../../packages/core/session/README.md#write-experimental-plugin-records) owns the caller restriction, retention, and read path. + +```ts type-equiv +/** + * Event type of an experimental plugin record: `plugin:` followed by + * slash-separated segments of lowercase letters, digits, `.`, `_`, and `-`, + * each starting with a letter or digit. A record type is never a + * {@link SessionEventMap} member. + */ +type PluginRecordType = `plugin:${string}` +``` + +```ts type-equiv +/** + * Payloads written by experimental packages, keyed by their `plugin:` record + * names. Packages augment this map to type writes and enter the current plugin + * record catalog; these declarations do not enter {@link SessionEventMap} or + * released persistence schemas. Stored records still require owner validation. + */ +interface PluginRecordMap {} +``` + +```ts type-equiv +/** + * One committed experimental plugin record, read from the log by + * `pluginRecordOf`. The record's owner validates `data` before use, because a + * restored record carries whatever JSON an earlier build of its owner wrote, + * or a V3 event that a format migration renamed into the `plugin:` namespace held. + */ +interface PluginRecord { + /** The record type, chosen by the owning plugin. */ + readonly type: PluginRecordType + /** The record's position in the Session log. */ + readonly seq: SessionSeq + /** Unix epoch milliseconds at append. */ + readonly time: number + /** The JSON payload as committed. */ + readonly data: unknown +} +``` + ## Durability contract What a persistence backend relies on: the durable log persists every event losslessly, and every Assistant attempt is one `assistant/message` or `assistant/attempt` whose embedded compact stream preserves the original timed chunks. `seq` stays contiguous across these settlements and all interleaved events. A backend may choose its own storage framing for an event batch as long as a handle's `read()` returns the exact appended events; current JSONL writes one row per event (see [persistence.md](persistence.md)). All `event.data` must be JSON-serializable; `Session.append` enforces this at the source (throwing on non-serializable data), so a bad event never enters the log and `session.snapshotEvents()` always equals what a backend can persist. Adding an event type that carries non-serializable data, corrupts core execution nesting, or violates its owner's declared relation is a breaking change to the on-disk format. diff --git a/docs/subsystems/session.zh.md b/docs/subsystems/session.zh.md index 6ed98c07f2..8085cb8067 100644 --- a/docs/subsystems/session.zh.md +++ b/docs/subsystems/session.zh.md @@ -756,6 +756,49 @@ interface TurnEndReasonMap { 钩子桥接层的 `hook/invoked` / `hook/result` 对(来自 `@deepseek-ai/dsh-hook-protocol`)通过 `handlerId` 关联。`UserPromptSubmit`、`PreToolUse`、`PostToolUse` 与 `Stop` 在 loop 已打开的轮次内触发,因此其 `hook/*` 记录天然位于轮次之内。`SessionStart` 不生成 `hook/*` 记录,因为它在轮次 1 之前运行;其上下文会在 inbox 中保持待处理,直到唤醒交付打开一个轮次。 +### 插件记录 + +实验性包在 `PluginRecordMap` 中声明插件自有的状态:`appendPluginRecord()` 根据每项声明约束名称与 payload 类型,并在 `plugin:` 命名空间中追加一条带 `ignorable: true` 标记的事件。`pluginRecordOf()` 读取已存记录,其 payload 为未知类型,不受当前声明约束。[实验性持久化目录](../experimental-persistence-catalog.zh.md)列出当前声明。调用方限制、保留方式与读取路径见[包 README](../../packages/core/session/README.zh.md#write-experimental-plugin-records)。 + +```ts type-equiv +/** + * Event type of an experimental plugin record: `plugin:` followed by + * slash-separated segments of lowercase letters, digits, `.`, `_`, and `-`, + * each starting with a letter or digit. A record type is never a + * {@link SessionEventMap} member. + */ +type PluginRecordType = `plugin:${string}` +``` + +```ts type-equiv +/** + * Payloads written by experimental packages, keyed by their `plugin:` record + * names. Packages augment this map to type writes and enter the current plugin + * record catalog; these declarations do not enter {@link SessionEventMap} or + * released persistence schemas. Stored records still require owner validation. + */ +interface PluginRecordMap {} +``` + +```ts type-equiv +/** + * One committed experimental plugin record, read from the log by + * `pluginRecordOf`. The record's owner validates `data` before use, because a + * restored record carries whatever JSON an earlier build of its owner wrote, + * or a V3 event that a format migration renamed into the `plugin:` namespace held. + */ +interface PluginRecord { + /** The record type, chosen by the owning plugin. */ + readonly type: PluginRecordType + /** The record's position in the Session log. */ + readonly seq: SessionSeq + /** Unix epoch milliseconds at append. */ + readonly time: number + /** The JSON payload as committed. */ + readonly data: unknown +} +``` + ## 持久性约定 持久化后端依赖的约定如下:持久日志无损保存每个事件,每个 Assistant attempt 都是一个 `assistant/message` 或 `assistant/attempt`,其嵌入式紧凑 stream 会保留原始带时间 chunk。`seq` 在这些 settlement 与所有交错事件之间保持连续。后端可以为事件批次选择自己的存储 framing,只要句柄的 `read()` 返回与追加时完全一致的事件即可;当前 JSONL 每个事件写一行(见 [persistence.md](persistence.zh.md))。所有 `event.data` 都必须可序列化为 JSON;`Session.append` 会从源头强制这一要求(遇到不可序列化数据时抛出),因此错误事件绝不会进入日志,`session.snapshotEvents()` 始终与后端可持久化的内容一致。新增会携带不可序列化数据、破坏核心执行嵌套或违反事件所有方声明关系的事件类型,都会构成磁盘格式的破坏性变更。 diff --git a/docs/subsystems/skills.i18n.yaml b/docs/subsystems/skills.i18n.yaml index d95e69dd63..c4fbc992b6 100644 --- a/docs/subsystems/skills.i18n.yaml +++ b/docs/subsystems/skills.i18n.yaml @@ -3,8 +3,8 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/skills.md /skills: - en: 047c73a023dcdcd1 - zh: 052f42fbfd202f32 + en: ad77015352a17ed6 + zh: 5c84933161845393 /skills/provider-registry: en: abe338b6c6d1ebd1 zh: c604db2de10a23c4 diff --git a/docs/subsystems/skills.md b/docs/subsystems/skills.md index e330edd2c5..3bfdbff402 100644 --- a/docs/subsystems/skills.md +++ b/docs/subsystems/skills.md @@ -2,9 +2,9 @@ English | [中文](skills.zh.md) -The [skill capability family](../../packages/skill) includes the Service Definition ([dsh-skill](../../packages/skill/skill), `ctx.skills`), the local Service Provider ([dsh-skill-filesystem](../../packages/skill/skill-filesystem)), optional packaged providers ([dsh-skill-badge](../../packages/skill/skill-badge), [dsh-skill-office](../../packages/skill/skill-office), and the Windows ACL diagnosis provider in [dsh-sandbox-windows-acl](../../packages/sandbox/sandbox-windows-acl)), and the Consumer ([dsh-tool-skill](../../packages/skill/tool-skill)). The registry merges provider catalogs across its host and per-scope layers; providers contribute local or packaged skills; the Consumer owns the initial and replacement catalogs plus the model-facing `skill` tool. Skills are optional instructions, not session events, so their vocabulary lives here rather than in [core.md](core.md). +The [skill capability family](../../packages/skill) includes the Service Definition ([dsh-skill](../../packages/skill/skill), `ctx.skills`), the local Service Provider ([dsh-skill-filesystem](../../packages/skill/skill-filesystem)), optional packaged providers ([dsh-skill-badge](../../packages/experimental/skill-badge), [dsh-skill-office](../../packages/skill/skill-office), and the Windows ACL diagnosis provider in [dsh-sandbox-windows-acl](../../packages/sandbox/sandbox-windows-acl)), and the Consumer ([dsh-tool-skill](../../packages/skill/tool-skill)). The registry merges provider catalogs across its host and per-scope layers; providers contribute local or packaged skills; the Consumer owns the initial and replacement catalogs plus the model-facing `skill` tool. Skills are optional instructions, not session events, so their vocabulary lives here rather than in [core.md](core.md). -Source: [`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts), [`packages/skill/skill-filesystem/src/index.ts`](../../packages/skill/skill-filesystem/src/index.ts), [`packages/skill/skill-badge/src/index.ts`](../../packages/skill/skill-badge/src/index.ts), [`packages/skill/skill-office/src/index.ts`](../../packages/skill/skill-office/src/index.ts), [`packages/sandbox/sandbox-windows-acl/src/acl-skill.ts`](../../packages/sandbox/sandbox-windows-acl/src/acl-skill.ts), and [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts). +Source: [`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts), [`packages/skill/skill-filesystem/src/index.ts`](../../packages/skill/skill-filesystem/src/index.ts), [`packages/experimental/skill-badge/src/index.ts`](../../packages/experimental/skill-badge/src/index.ts), [`packages/skill/skill-office/src/index.ts`](../../packages/skill/skill-office/src/index.ts), [`packages/sandbox/sandbox-windows-acl/src/acl-skill.ts`](../../packages/sandbox/sandbox-windows-acl/src/acl-skill.ts), and [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts). ## Provider registry diff --git a/docs/subsystems/skills.zh.md b/docs/subsystems/skills.zh.md index 03a6a06f4e..d93f8416fb 100644 --- a/docs/subsystems/skills.zh.md +++ b/docs/subsystems/skills.zh.md @@ -2,9 +2,9 @@ [English](skills.md) | 中文 -[skill(技能)能力族](../../packages/skill) 包含 Service Definition([dsh-skill](../../packages/skill/skill),`ctx.skills`)、本地 Service Provider([dsh-skill-filesystem](../../packages/skill/skill-filesystem))、可选的随包提供方([dsh-skill-badge](../../packages/skill/skill-badge)、[dsh-skill-office](../../packages/skill/skill-office) 及 [dsh-sandbox-windows-acl](../../packages/sandbox/sandbox-windows-acl) 中的 Windows ACL 诊断提供方)和 Consumer([dsh-tool-skill](../../packages/skill/tool-skill))。注册表在其宿主层与各 scope 层之间合并各提供方的目录;提供方贡献本地或随包 skill;Consumer 拥有初始目录和替换目录,以及面向模型的 `skill` 工具。skill 是可选的指令而非会话事件,因此其词汇定义在此处而非 [core.md](core.zh.md)。 +[skill(技能)能力族](../../packages/skill) 包含 Service Definition([dsh-skill](../../packages/skill/skill),`ctx.skills`)、本地 Service Provider([dsh-skill-filesystem](../../packages/skill/skill-filesystem))、可选的随包提供方([dsh-skill-badge](../../packages/experimental/skill-badge)、[dsh-skill-office](../../packages/skill/skill-office) 及 [dsh-sandbox-windows-acl](../../packages/sandbox/sandbox-windows-acl) 中的 Windows ACL 诊断提供方)和 Consumer([dsh-tool-skill](../../packages/skill/tool-skill))。注册表在其宿主层与各 scope 层之间合并各提供方的目录;提供方贡献本地或随包 skill;Consumer 拥有初始目录和替换目录,以及面向模型的 `skill` 工具。skill 是可选的指令而非会话事件,因此其词汇定义在此处而非 [core.md](core.zh.md)。 -源码:[`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts)、[`packages/skill/skill-filesystem/src/index.ts`](../../packages/skill/skill-filesystem/src/index.ts)、[`packages/skill/skill-badge/src/index.ts`](../../packages/skill/skill-badge/src/index.ts)、[`packages/skill/skill-office/src/index.ts`](../../packages/skill/skill-office/src/index.ts)、[`packages/sandbox/sandbox-windows-acl/src/acl-skill.ts`](../../packages/sandbox/sandbox-windows-acl/src/acl-skill.ts) 与 [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts)。 +源码:[`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts)、[`packages/skill/skill-filesystem/src/index.ts`](../../packages/skill/skill-filesystem/src/index.ts)、[`packages/experimental/skill-badge/src/index.ts`](../../packages/experimental/skill-badge/src/index.ts)、[`packages/skill/skill-office/src/index.ts`](../../packages/skill/skill-office/src/index.ts)、[`packages/sandbox/sandbox-windows-acl/src/acl-skill.ts`](../../packages/sandbox/sandbox-windows-acl/src/acl-skill.ts) 与 [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts)。 ## 提供方注册表 diff --git a/docs/subsystems/slots.i18n.yaml b/docs/subsystems/slots.i18n.yaml index aec206af8c..67850a5a7d 100644 --- a/docs/subsystems/slots.i18n.yaml +++ b/docs/subsystems/slots.i18n.yaml @@ -18,8 +18,8 @@ en: 2fcfc55fbac80f72 zh: ea3016f394c56b07 /web-client-slots/developer-provided-injection: - en: 4fb5222bb2b064e7 - zh: b961f792052b4ce5 + en: 7081335b356bf46b + zh: 631e60d29daf7196 /web-client-slots/current-hierarchy: en: 8a4cc4b8608ce0f6 zh: 21281dcf047503b2 diff --git a/docs/subsystems/slots.md b/docs/subsystems/slots.md index b6f9a2f12e..8a9448a4f8 100644 --- a/docs/subsystems/slots.md +++ b/docs/subsystems/slots.md @@ -106,6 +106,8 @@ A reserved `hooks` object in that return value accepts bare `getSnapshot`/`subsc The owner of a slot may put an `inject` face in the child declaration when every occupant needs the same capability. Plain members reach all occupants unchanged. Function-valued members inside its `hooks` object are hook factories; they receive the slot's standard props and optional per-render `hookContext`, then return the constrained hook exposed to the occupant. `conversation.chat.node` uses this mechanism to provide `useTurnData(key)` for the node currently being rendered. +The session-scoped `conversation.chat.reasoning-body` chain receives unchanged `text` and `running` values for an expanded reasoning body. Its owner renders original compact Markdown when all entries decline; display contributions retain access to the original reasoning. + Use owner props for values already known at one render occurrence, registration `inject` for one entry's callbacks and private observables, slot-level `inject` for a capability controlled by the slot owner, and a declared store for mutable view state shared across entries or preserved across remounts. React nodes compose through child slots, not through injected values. ## Current hierarchy @@ -147,6 +149,7 @@ root │ │ └─ conversation.view │ │ ├─ conversation.chat.node │ │ │ ├─ conversation.chat.assistant-actions +│ │ │ ├─ conversation.chat.reasoning-body │ │ │ ├─ conversation.chat.commandview │ │ │ ├─ conversation.chat.turnTail │ │ │ └─ tool.call.toolview diff --git a/docs/subsystems/slots.zh.md b/docs/subsystems/slots.zh.md index 63acb06cd6..bf8bc0cac7 100644 --- a/docs/subsystems/slots.zh.md +++ b/docs/subsystems/slots.zh.md @@ -106,6 +106,8 @@ Renderer 还会根据声明的 store 创建 `useStore`,并根据声明的 loca 当每个 occupant 都需要同一种能力时,slot owner 可以在 child 声明里放置 `inject` face。普通成员会原样交给所有 occupant;其 `hooks` 对象中的函数成员是 hook factory,它会收到 slot 的标准 props 与可选的逐次渲染 `hookContext`,再返回提供给 occupant 的受限 hook。`conversation.chat.node` 正是通过这种机制,为当前渲染的 node 提供 `useTurnData(key)`。 +Session 作用域的 `conversation.chat.reasoning-body` 链为展开的思考正文提供未修改的 `text` 和 `running`。所有条目都拒绝认领时,owner 渲染原始紧凑 Markdown;显示贡献保留查看原始思考内容的能力。 + 一次渲染时 owner 已知的值走 owner props;单个 entry 的 callback 与私有 observable 走注册项 `inject`;由 slot owner 控制、所有 occupant 共享的能力走 slot 级 `inject`;需要跨 entry 共享或跨重新挂载保留的可变视图状态走声明的 store。React node 通过 child slot 组合,不通过注入值传递。 ## 当前层级 @@ -147,6 +149,7 @@ root │ │ └─ conversation.view │ │ ├─ conversation.chat.node │ │ │ ├─ conversation.chat.assistant-actions +│ │ │ ├─ conversation.chat.reasoning-body │ │ │ ├─ conversation.chat.commandview │ │ │ ├─ conversation.chat.turnTail │ │ │ └─ tool.call.toolview diff --git a/docs/subsystems/subagent.i18n.yaml b/docs/subsystems/subagent.i18n.yaml index 59585aee9a..104548c425 100644 --- a/docs/subsystems/subagent.i18n.yaml +++ b/docs/subsystems/subagent.i18n.yaml @@ -2,30 +2,27 @@ # section, a hash of its English and Chinese blocks outside code blocks and generated regions. # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/subagent.md -/subagent: - en: becdb30aeb411272 - zh: 72724ec854ec4e84 -/subagent/two-kinds-of-capability-discovered-two-ways: - en: 7be7cbc224948ed1 - zh: 6da4f61394ed900e -/subagent/the-one-shot-start-request: - en: 37b7876d6f186642 - zh: 196a9df4fca5a4fd -/subagent/continuable-children-and-activations: - en: e4b88406f0c541dd - zh: 58fb89fa90721416 -/subagent/durable-enumeration-listchildren-listdescendants-and-their-entries: - en: 5ff018408210d739 - zh: 909d4a1788d65629 -/subagent/the-terminal-result-subagentresult: - en: 6070effb5f84f701 - zh: 2b8a6980ceb0b7b2 -/subagent/a-one-shot-run-subagentrun: - en: 39df94dd1343ffde - zh: 047605d7f5ba91ae -/subagent/the-provider-contract-subagentprovider: - en: cb05cffdb837b7bc - zh: a53133845d673f0a -/subagent/in-process-backends-permission-depth-and-seed: - en: 4f009d383e782b2d - zh: 0c11e25423968f24 +/subagents: + en: 9d48a27a49585a3d + zh: 89da32c8a16c290a +/subagents/capabilities-and-activation-requests: + en: 8c92b131f67997f6 + zh: c2dc606a0859420c +/subagents/local-children-and-activations: + en: d926d4777ec0f11e + zh: 0ae6076e24b66401 +/subagents/messages-and-interruption: + en: 623b06b3c12540bc + zh: f56c6341e1c62768 +/subagents/durable-enumeration: + en: 30c3607a969696ce + zh: e6359cdf22b1e235 +/subagents/results-and-backend-handles: + en: 69ea5bed7d9fb64a + zh: b187a822b813e80a +/subagents/the-provider-contract-subagentprovider: + en: 226ba8094a8cf96c + zh: 719678662905d1eb +/subagents/durable-enumeration-listchildren-listdescendants-and-their-entries: + en: d8ce7969b65ad86e + zh: d39dac6592681bf2 diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md index 028b8e17de..8ad39194a1 100644 --- a/docs/subsystems/subagent.md +++ b/docs/subsystems/subagent.md @@ -1,30 +1,21 @@ -# Subagent +# Subagents English | [中文](subagent.zh.md) -The subagent seam lets an agent delegate work to a child agent. Like [bash](shell.md), it is **one optional capability**, not part of the agent loop, so its types live here rather than in [core.md](core.md). It differs from the other capability seams because **multiple provider implementations coexist** in one context, registered by name (`ctx.subagents`), while bash allows only one executor. Its registry follows the [LLM adapter registry](llm-streaming.md), not the single-service bash executor. +The unified activation API manages local conversations and external executions. -Service Definition: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Service Providers are sibling packages (`dsh-subagent-spawn-in-process`, `dsh-subagent-fork-in-process`, `dsh-subagent-acp`, `dsh-subagent-codex`, `dsh-subagent-claude-code`, `dsh-subagent-dsh-sdk`); the model-facing Consumers are [dsh-tool-subagent](../../packages/subagent/tool-subagent) (per-provider delegation) and [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) (the optional global `send_message`, `interrupt_agent`, and `list_agents` controls). The same `ctx.subagents` service owns continuable-child orchestration through an internal activation manager, direct-child discovery through the parent catalog, and recursive descendant discovery through parent catalogs. Product-provider rationale lives in [the historical Codex and Claude Code Agent Note](../../.agents/notes/archived/feature/2026-08-04-claude-code-and-codex-subagent-backends.md); common-seam rationale lives in [the historical subagent Agent Note](../../.agents/notes/archived/feature/2026-06-21-subagent-capability-seam.md), [the continuable subagents Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), and [the adjacent-Agent messaging Agent Note](../../.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md); [the archived list-identity-projection record](../../.agents/notes/archived/architecture/2026-08-06-subagent-list-identity-projection.md) documents the original list-identity decision. +## Capabilities and activation requests -Sources: [`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts), [`packages/subagent/subagent/src/index.ts`](../../packages/subagent/subagent/src/index.ts), and [`packages/subagent/subagent/src/continuation.ts`](../../packages/subagent/subagent/src/continuation.ts) +Every backend uses startActivation. Capability flags validate request options; prepareContinuable selects local Agent creation, while start selects one external execution. The model-facing tool uses parent delivery. Workflow and code mode consumers can await caller results without injecting another parent message. Historical children with unavailable descriptors remain discoverable as `mode: 'unknown'` without granting continuation capabilities; the [package README](../../packages/subagent/subagent/README.md) defines catalog persistence. -The `subagentCatalog` projection exposes `SubagentCatalogEntry[]` in parent event order through Session observations and client snapshots. Each entry contains the child id, creation time, mode, and mode-dependent label; fork-inherited catalog facts are excluded. [The subagent package](../../packages/subagent/subagent/README.md) owns catalog creation and persistence semantics. Historical children with unavailable descriptors have `mode: 'unknown'`; their header identity remains discoverable without granting continuation capabilities. - -## Two kinds of capability, discovered two ways - -A provider advertises its **start-time** features on a static descriptor the service checks BEFORE a one-shot run exists; a request that needs one the provider lacks is rejected loud (`SubagentError('UNSUPPORTED_CAPABILITY')`), never accepted-then-ignored. Those flags describe only the one-shot [`start()`](#the-provider-contract-subagentprovider) path, where the provider composes the child. **Continuable** children are composed by the continuation manager itself, so they are gated by one optional method whose presence IS the capability, with TS narrowing as the discovery mechanism: [`SubagentProvider.prepareContinuable`](#the-provider-contract-subagentprovider). +Optional `request.cwd` selects the child's initial directory; relative paths resolve against the parent's current effective directory. Omitting it captures that directory at admission. Local child Sessions retain the parent's original project, and cold resume retains their logged directory state. ```ts type-equiv /** - * Which START-TIME features a provider supports. Checked by the service before delegating to - * {@link SubagentProvider.start}: a request that needs a capability the chosen provider lacks - * is rejected with a typed error rather than accepted-then-ignored (the "fail loud, no silent - * degradation" rule). These flags describe the ONE-SHOT - * {@link SubagentProvider.start} path, where the provider composes the child; - * continuable children are composed by the continuation manager itself and are - * gated by {@link SubagentProvider.prepareContinuable} instead. Each flag - * corresponds one-to-one to a {@link SubagentStartRequest} option: `depthLimit` - * to `maxDepth`; the other names match. + * Start-time options supported by a registered backend. The manager checks + * every requested option before preparing a local child or starting an external + * execution. depthLimit controls maxDepth; the remaining flag names match + * SubagentStartRequest fields. */ interface SubagentCapabilities { readonly agentOptions: boolean @@ -35,27 +26,57 @@ interface SubagentCapabilities { } ``` -## The one-shot start request +```ts type-equiv +/** One managed child execution and its result delivery policy. */ +interface SubagentActivationSpec { + /** Registered backend to use. */ + readonly provider: string + /** Short task label retained in cataloged child membership. */ + readonly label: string + /** Optional reserved identity for a local child; external backends allocate their own ids. */ + readonly childId?: SessionId + /** Task, parent, and backend-supported execution options. */ + readonly request: Omit + /** Cancellation before publication; callers own later cancellation through dispose. */ + readonly signal: AbortSignal + /** Parent delivery notifies the model; caller delivery only returns the result. Local children always enter the parent catalog. */ + readonly delivery: 'parent' | 'caller' +} +``` -The tool layer builds this request from the model input and its own config; the service validates it against the named provider before `start`. Required `parent` supplies the session cwd, lineage, and delegation depth. Optional Agent provider, model, reasoning-effort, and token overrides, output schema, depth, tool filter, and persona require matching capability flags. In-process backends merge `agentOptions` over the parent Agent's options, scope filters and personas to child creation, and implement the supported object-rooted schema with a forced capture tool. The DSH SDK backend merges the four Agent route fields over its instance defaults and validates them in the child runtime's initialization; ACP, Codex, and Claude Code reject `agentOptions` before starting their transports. +```ts type-equiv +/** A managed execution; disposal addresses this exact activation, never a later resume. */ +interface SubagentActivation { + /** Stable identity of the child. */ + readonly childId: SessionId + /** Accepted inbox message, when the backend has a local inbox. */ + readonly messageId?: MessageId + /** + * Execution result after teardown settles and notifications are sent; capture failures reject. + * Teardown failures are reported by dispose() without replacing a captured result. + */ + readonly result: Promise + /** Stop and release this activation and its owned descendants; rejects on teardown failure. */ + dispose(): Promise +} +``` ```ts type-equiv /** - * What a caller asks for when starting a ONE-SHOT subagent. The tool layer - * builds this from the model's `{ description, prompt }` plus its own config; - * the service validates {@link SubagentCapabilities} against the named provider - * and resolves the durable descriptor before dispatching to - * {@link SubagentProvider.start}. + * Task and optional capabilities supplied to a backend. startActivation carries + * these fields in request while owning its label, cancellation, and delivery + * policy separately. */ interface SubagentStartRequest { + /** Initial child directory; relative paths resolve against the parent's current directory. Omitted inherits that directory at start. */ + readonly cwd?: string /** Optional short display label persisted with a session-backed child. */ readonly label?: string /** Content delivered as the child's user message. */ readonly prompt: ContentBlock[] /** - * The spawning agent. In-process providers derive workspace, lineage, and - * delegation depth from its durable session state. ACP reads only its cwd, - * and only when no deployment `cwd` override is configured. + * The spawning agent. Its effective directory supplies the default cwd; + * in-process children retain its origin, lineage, and delegation depth. */ readonly parent: Agent /** @@ -106,121 +127,17 @@ interface SubagentStartRequest { } ``` -`signal` is the single cancellation channel before and after readiness. The [subagent composition-controls Agent Note](../../.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) owns the persona, live global-tool filter, absolute-depth, and visibility-not-authority rationale. - -The caller-facing request does not carry catalog format details or continuation state. `SubagentRuntime.start()` resolves the detached one-shot descriptor after capability checks, then passes this provider-facing request to the selected transport; a continuable child never reaches `SubagentProvider.start()`: - ```ts type-equiv -/** - * Provider-facing one-shot request after {@link SubagentRuntime.start} resolves - * the durable child descriptor. - */ +/** Provider-facing request with an absolute directory selected before startup. */ interface ResolvedSubagentStartRequest extends SubagentStartRequest { - /** Detached descriptor a session-backed provider persists in the child log. */ - readonly descriptor: SubagentDescriptorData + /** Absolute child directory captured from the parent or explicit request. */ + readonly cwd: string } ``` -## Continuable children and activations +## Local children and activations -A **continuable background subagent** is one durable child Session with at most one process-local **Activation**, the period when a reconstructed child Agent is resident. An Activation is not a request, result, cancellation, or Task: it may execute many FIFO turns and stays resident while descendants it created are still running. The continuation manager owns activation admission, direct-parent authorization, the live ownership graph, cold resume, and child-first disposal; the Agent loop owns all turn ordering and execution. No continuable path creates a Task or an intermediate result-bearing wrapper. - -```text -persisted Session - -> optional live Activation - -> one retained AgentHandle - -> Agent inbox as the only turn FIFO - -> zero or more owned child Activations -``` - -`SubagentRuntime.startContinuable()` reserves the stable child id, snapshots the versioned `subagent/descriptor` payload, asks the named provider for its detached `ContinuableCreateSpec`, creates the child Agent through a private activation-owner scope, establishes any continuable-parent ownership, and submits the initial prompt. It resolves with `{ childId, messageId }` when inbox acceptance yields the message id — without waiting for the turn to start or for the message to enter the Session log. Every failure before that acceptance rejects with neither id, disposing any created handle and rolling back the Activation and parent ownership. - -`SubagentRuntime.sendMessage()` is the sole model-authored message operation. It accepts the exact live sender plus a target id, permits only a direct parent or direct continuable child, derives sender attribution itself, and routes a direct-child target by Activation residency: - -| Target Activation state | `sendMessage` | -|---|---| -| `running` | steer the nearest step in the same Activation | -| `waiting` | wake and steer the same Activation | -| no Activation | cold-resume a new Activation, then steer it | - -`running` means the Agent has an active driver or maintenance task; `waiting` means no Agent activity is active but its Inbox is nonempty or it owns at least one child Activation that has not completed disposal; `settled` means no Agent activity is active, the Inbox is empty, and every owned child is disposed, at which point the manager disposes the [`AgentHandle`](core.md#creation-and-ownership) and removes the Activation. The manager derives these internal conditions from `Agent.whenIdle()`, `Agent.inbox.hasPending`, the owned-child set, and an Activation generation that invalidates stale observations, rather than maintaining a second execution state machine. After the final Session flush, the child-lock decision uses the synchronous task entry of `Agent.runMaintenance()` to claim the idle phase and close admission in the same JavaScript turn. This conservative rule does not distinguish delivery modes: context parked by `Agent.inject()` can keep an idle Activation and its live ancestors resident until a waking delivery claims it, a queue mutation removes it, or manager teardown discards it. - -The Agent inbox is the only queue. Every Agent message uses `Agent.steer()`: an idle target starts a turn, while a running target claims it at the nearest step boundary. The browser `subagent.prompt` Remote separately carries `delivery: 'queue' | 'steer'` through the same internal admission path; Queue opens a later FIFO turn, while Steer retains the Agent loop's best-effort nearest-step behavior and the message's human source. Successful delivery returns the accepted `MessageId`; the existing `agent/inbox/inserted`, `agent/inbox/claimed`, and `agent/inbox/discarded` events remain the message-lifecycle observations, and the continuation layer defines no second queue. - -Authority comes from the exact live sender. Parent-to-child delivery requires the target's `SessionHeader.parentSession` to name the sender; child-to-parent delivery requires the sender's resident Activation to name the target. Siblings, ancestors beyond one edge, self-targets, stale Agent objects, and one-shot children are rejected. Each accepted message is framed as `Agent sent a message:` and records `AgentMessageSource`; the source records the sender but grants no authority. - -For `startContinuable()`, `sendMessage()`, and browser prompt delivery, the caller signal owns lookup, materialization, and admission only until inbox acceptance. Afterwards the manager owns the Activation independently: later caller cancellation neither cancels the accepted turn nor disposes the child. The public subagent service exposes no caller-selected Agent-message scheduling; browser human Queue and Steer remain internal adapter choices. - -Live queue occurrence mutation remains in the Session domain. `session.updateQueue` admits ordinary Edit, Remove, and QueueDock Steer for a live subagent-owned Agent only when its current projected identity is continuable and its descriptor sequence is in that child's own non-seed suffix. The identity projection folds descriptors last-wins so a child descriptor supersedes descriptors retained from fork lineage; the own-suffix sequence check prevents a seed-only ancestor identity from authorizing mutation. One-shot, missing, unknown, corrupt, or cold children remain rejected, and queue mutation never cold-resumes a child. The target Session id is the human authority for these mutations, including pending `nextStep` steering or injected context. Steer requires a queued `MessageId` and an Agent that reports running when the command begins; cancellation after admission uses the Agent's accepted waking `nextTurn` fallback. Edit rewrites content under the same `MessageId`, and both Edit and Steer complete their Inbox work synchronously, so settlement observes only the final state. `agent/inbox/claimed` and `agent/inbox/discarded` wake the watcher to re-read whether any pending occurrence remains; this lets direct Agent delivery resume parked work and lets removing the last parked occurrence settle an idle child. The [human inbox-control reference](../../packages/api/session-controller/README.md) owns these semantics. - -`SubagentRuntime.interrupt(targetSessionId, authority)` is the one public stop: it authorizes synchronously, issues `Agent.cancel(cause, { keepInbox: true })` on the live target, and returns without awaiting quiescence. The Activation, its unclaimed pending inbox work, and published descendants are untouched; work already claimed into the interrupted turn is not requeued. Once the interrupted driver is idle, a waking send resumes the parked FIFO queue. An absent target — unknown, one-shot, or already settled — and a manager-less composition are accepted no-ops. For a live target, a mismatched parent address or caller outside its live ancestry rejects with `UNAUTHORIZED`; stale ancestor objects and self-targeting ancestor requests reject before target lookup. - -```ts type-equiv -/** - * Authority under which one interrupt request is admitted. `user` carries the - * durable direct-parent address a human client presented; `ancestor` carries - * the exact live Agent object whose recorded lineage must contain the caller. - */ -type SubagentInterruptAuthority = - | { readonly kind: 'user'; readonly parentSessionId: SessionId } - | { readonly kind: 'ancestor'; readonly agent: Agent } -``` - -Every Activation owns its `AgentHandle` and an `ownedChildren: Set`; because one Session has at most one live Activation, the child Session id identifies the live child without another runtime-incarnation reference. Starting a child or submitting parent-originated work registers the child in a continuation-managed parent's set before the child can run, and that parent cannot settle while the set is non-empty. A top-level or other non-continuation Agent has no Activation and stays outside the waiting graph. Child release happens only after the child has no active Agent work, its Inbox is empty, every child of that child is disposed, the best-effort final session flush settles, and the child's `AgentHandle` completes disposal. - -Final settlement awaits `ctx.sessions.flush(session)` but ignores its participation boolean because an arbitrary listener cannot prove that a persistence backend stored the state. Rejection is logged without failing the Activation, and the manager still disposes the handle and releases ownership; the persisted child state may then be missing or stale on a later resume. Manager unload invokes an internal manager-wide drain that closes admission and disposes every live forest; `drainContinuableDescendants(parents)` closes admission only below exact live host-owned Agents and disposes their continuable descendants while unrelated forests remain live. Both await already-admitted materializations in their scope, propagate cancellation top-down, release handles child-first, and await every selected branch despite individual failures. Durable child Sessions survive that process-local teardown. - -```ts type-equiv -/** Durable attribution for one model-authored message between adjacent Agents. */ -interface AgentMessageSource { - readonly kind: 'agent-message' - /** A message another agent addressed to this one (`relay` context form). */ - readonly form: 'relay' - /** Session id of the Agent whose tool call produced the message. */ - readonly senderSessionId: SessionId -} -``` - -```ts type-equiv -/** Options for one model-authored message between adjacent Agents. */ -interface SubagentSendMessageOptions { - /** Caller cancellation, owning the operation only until inbox acceptance. */ - readonly signal: AbortSignal -} -``` - -```ts type-equiv -/** Identities returned once a continuable child accepted its initial prompt. */ -interface ContinuableStart { - /** The durable child session id, stable across activations. */ - readonly childId: SessionId - /** The accepted initial prompt's inbox message id. */ - readonly messageId: MessageId -} -``` - -When a resident Activation settles, the manager delivers one notice to the child's durable direct parent describing how that epoch ended and carrying the nonempty text blocks from its final assistant output, or `It left no closing message.` when none remain. That delivery is unconditional for every child whose id a caller received, happens before the ownership release that would let the parent be judged settled, and reaches a resident parent through the same waking Agent delivery as an Agent message. A parent whose own lineage is already tearing down receives it without a wake, because waking an idle Agent starts a turn rather than queueing work. Its source has a distinct kind so a transcript never presents a runtime account as something the child wrote. - -```ts type-equiv -/** - * Durable attribution for the runtime's own account of a continuable child - * settling. Deliberately a different kind from - * {@link AgentMessageSource}: an Agent message is content the sender chose, - * while this message is the manager stating what became of the child, and a - * transcript that merged them would credit the child with words it never wrote. - */ -interface SubagentSettledMessageSource { - readonly kind: 'subagent-settled' - /** A runtime account shown without expanding the row (`notice` context form). */ - readonly form: 'notice' - /** One-line account of how the child ended. */ - readonly summary: string - /** Session id of the child that settled. */ - readonly senderSessionId: SessionId -} -``` - -The provider participates only in preparing the initial creation spec, where `spawn` and `fork` differ. Its returned spec carries only detached provider-specific creation inputs — the optional parent-history seed — and no Agent, `AgentHandle`, prompt delivery, result, disposal, or resume operation. Cold resume does not dispatch through a provider at all: the manager folds the generic descriptor, calls `ctx.agents.resume()` through the same activation-owner scope, and submits the waiting turn. +A local child has a durable Session and at most one live activation. The manager reserves identity and capacity, prepares the child through its backend, and owns input admission, cold resume, and child-first disposal. The activation result settles after resource release and completion notification. Disposal reports cleanup failure separately without replacing a captured result. Structured capture closes input for that activation; a later cold resume does not inherit the output schema. ```ts type-equiv /** @@ -231,6 +148,8 @@ The provider participates only in preparing the initial creation spec, where `sp * history. */ interface ContinuableCreateRequest { + /** Absolute initial directory captured before provider preparation. */ + readonly cwd: string /** The reserved durable child session id, for provider diagnostics. */ readonly sessionId: SessionId /** The delegating parent agent whose history a seeding provider reads. */ @@ -260,32 +179,66 @@ interface ContinuableCreateSpec { } ``` -The descriptor (`SubagentDescriptorData` in [descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts)) is a mode-discriminated durable identity for every session-backed subagent. Both modes carry the provider name. A `one-shot` descriptor optionally carries a caller-owned display `label`; a `continuable` descriptor requires the delegation `description` as its durable creation label and additionally snapshots resolved child `agentOptions.provider`/`model`/`reasoningEffort` and optional `persona`/`toolFilter` for cold resume. It never snapshots the merge-extensible `AgentOptions` object, so an unrelated extension value cannot break continuation and a later composition input is a deliberate version change. It omits `subagentDepth` (cold resume trusts the persisted header's `delegationDepth` as the monotone floor) and `outputSchema` (one run or Activation's result contract, not durable identity). +## Messages and interruption -A local one-shot provider appends the descriptor inside the child's initial turn before its first request. The continuation manager appends the descriptor after any provider-supplied lineage and before the initial prompt is admitted; `Session.inheritedEventCount` remains the fork-lineage boundary: resume-time descriptor authority reads the child's own suffix, while the identity projection folds `subagent/descriptor` last-wins so the child's own descriptor overrides a fork-seeded ancestor's. The event is log-only: no `surfaceOp`, never in model history, and retained across compaction by the append-only log. Malformed current-version descriptors are corrupt; unsupported versions cannot be classified by this runtime. - -## Durable enumeration: `listChildren()`, `listDescendants()`, and their entries - -The model-facing `list_agents` adapter reports current activity as `running` or `inactive`. These values do not describe task completion or guarantee that `send_message` will succeed. - -`SubagentRuntime.listChildren(parentSessionId, signal?)` reads the parent's `subagentCatalog` view through a live-preferred Session observation and releases that observation on success or failure. It returns direct-child entries in parent event order without reading child logs or enumerating the Session corpus. Query failures propagate; a missing catalog projection fails explicitly. Browser rows derive membership from the shared projection store and add activity from Session status; the control stream pushes complete catalog updates. `listDescendants()` recursively reads those catalogs and derives `hasChildren` from each child catalog. [The parent-catalog Agent Note](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns creation, fork isolation, ordering, and persistence costs. - -`SubagentRuntime.listDescendants(rootSessionId)` recursively calls the same catalog reader in stable pre-order, preserving each parent's event order. One-shot and unknown-mode entries remain traversal nodes; unknown modes produce `unsupported` diagnostics. An unreadable child catalog produces `corrupt` or `unavailable` and stops only that branch. Root read failures, missing services or projections, and cancellation reject the listing. Each reachable catalog is observed once and released before the next read; repeated ids and cycles are skipped. Sessions absent from reachable catalogs are not discovered, including ordinary Session forks and any subagents below those forks. Each row carries its catalog parent and root-relative depth: +sendMessage authorizes the exact live sender and permits only adjacent local agents. Running children receive steering input; absent continuable children resume from persistence. External executions reject follow-ups. Both local and external executions automatically report their final text, structured result, diagnostic, and status in the parent settlement notice. Local children can also send messages through send_message. caller delivery suppresses settlement notices. drainDescendants and drainChildren await owned work teardown for local and external activations. ```ts type-equiv -/** One catalog descendant with its direct parent and edge distance from the requested root. */ -type SubagentDescendantListEntry = SubagentListEntry & { - /** Parent whose catalog contains this child. */ - readonly parentId: SessionId - /** Edge distance from the requested root; direct children are `1`. */ - readonly depth: number +/** Durable attribution for one model-authored message between adjacent Agents. */ +interface AgentMessageSource { + readonly kind: 'agent-message' + /** A message another agent addressed to this one (`relay` context form). */ + readonly form: 'relay' + /** Session id of the Agent whose tool call produced the message. */ + readonly senderSessionId: SessionId } ``` +```ts type-equiv +/** + * Durable attribution for the runtime's own account of a continuable child + * settling. Deliberately a different kind from + * {@link AgentMessageSource}: an Agent message is content the sender chose, + * while this message is the manager stating what became of the child, and a + * transcript that merged them would credit the child with words it never wrote. + */ +interface SubagentSettledMessageSource { + readonly kind: 'subagent-settled' + /** A runtime account shown without expanding the row (`notice` context form). */ + readonly form: 'notice' + /** One-line account of how the child ended. */ + readonly summary: string + /** Session id of the child that settled. */ + readonly senderSessionId: SessionId +} +``` -## The terminal result: `SubagentResult` +```ts type-equiv +/** Options for one model-authored message between adjacent Agents. */ +interface SubagentSendMessageOptions { + /** Caller cancellation, owning the operation only until inbox acceptance. */ + readonly signal: AbortSignal +} +``` -The outcome of a one-shot run, resolved by `SubagentRun.result`. `structured` is present only after a requested `outputSchema` was successfully satisfied; requesting a schema does not guarantee it, and a provider may return `stopReason: 'error'` when the child fails or finishes without a valid capture. A provider may attach a safe, non-assistant `diagnostic` to a non-`completed` result; the provider removes tool inputs, file contents, environment values, credentials, and raw protocol payloads and limits the complete value to 4096 UTF-8 bytes before consumers present it separately from `output`. A non-`completed` `stopReason` means `output` may be partial — the consumer maps it to an `isError` tool result rather than reporting partial output as success. +```ts type-equiv +/** + * Authority under which one interrupt request is admitted. `user` carries the + * durable direct-parent address a human client presented; `ancestor` carries + * the exact live Agent object whose recorded lineage must contain the caller. + */ +type SubagentInterruptAuthority = + | { readonly kind: 'user'; readonly parentSessionId: SessionId } + | { readonly kind: 'ancestor'; readonly agent: Agent } +``` + +## Durable enumeration + +`listChildren` reads the parent-owned catalog of all local children and parent-delivery external children. Caller-delivery external executions leave membership and result collection to their caller; local workflow children remain discoverable after completion. External entries carry `mode: 'external'` and cannot open a child Session or accept follow-ups. Catalog entries record membership at creation without execution status. `list_agents` exposes direct continuable children with `running` or `inactive` activity; see the [control tool](../../packages/subagent/tool-subagent-control/README.md#list_agents). + +## Results and backend handles + +SubagentResult is available to program consumers. The activation registry retains the external backend handle until disposal completes. A cleanup failure does not replace a captured execution result: the activation result and parent notice retain it, while dispose rejects and the live subagent/end event reports error. Backend-owned diagnostics must satisfy the safe-detail requirements below. ```ts type-equiv /** @@ -320,8 +273,6 @@ interface SubagentResult { } ``` -`SubagentStopReason` is a [merge-extensible derived union](core.md#the-map--derived-union-pattern) — a backend may add variants, so consumers branch on the known cases and treat an unknown terminal reason as a failure: - ```ts type-equiv /** * Why a subagent run ended. Merge-extensible (a backend may add variants); @@ -343,33 +294,17 @@ interface SubagentStopReasonMap { } ``` -## A one-shot run: `SubagentRun` - -`SubagentRun` is the consumer-owned handle for a published one-shot child — one disposable foreground delegation with one result, never a durable child handle. Prompt submission, turn work, and infrastructure faults after publication belong to `result`. Consumers await that result and always dispose the run to reach quiescence. Child failures resolve with a non-completed stop reason; only unrepresentable infrastructure faults reject. A run has no steering and no resume: continuable conversations have no run at all, because the continuation manager holds their `AgentHandle` directly and orders every turn through the child's own inbox. - ```ts type-equiv /** - * ONE-SHOT child handle returned after publication. Prompt submission, turn - * work, and infrastructure faults after that boundary belong to {@link result}. - * Consumers await that result and must always {@link dispose} to cancel - * remaining work and reach quiescence. A run is one disposable foreground - * delegation with one result; continuable conversations have no run — the - * continuation manager holds their `AgentHandle` directly and orders every - * turn through the child's own inbox. + * Backend execution handle owned directly by the activation registry. + * A result may become available before resource release; the manager always + * disposes the handle and awaits cleanup. Backend startup failures clean up + * partial resources before rejecting, while accepted execution failures settle + * through result. */ interface SubagentRun { - /** - * Parent-scoped run id. For a local run, this MUST equal the published child - * session id, whose `parentSession` records `request.parent.session.id`; a - * remote provider mints an id unique in the parent namespace. - */ + /** Provider-minted id, unique across all parents, providers, and local Sessions in this runtime. */ readonly id: SessionId - /** - * The exact published in-process child, or `undefined` for a remote run. - * When present, its id is {@link id}; the provider retains no ownership - * implication beyond the run's ordinary {@link dispose} contract. - */ - readonly localAgent: Agent | undefined /** * Resolves with the child's terminal {@link SubagentResult} when the run * settles. Does NOT reject on a child-level failure — a model/transport @@ -386,11 +321,9 @@ interface SubagentRun { } ``` -A local one-shot run MUST publish an ordinary child agent/session before `start()` fulfills, return that child session id as `SubagentRun.id`, expose the exact child as `localAgent`, record `request.parent.session.id` in the child's `parentSession` header, and append the resolved descriptor inside the child's initial turn before its first request. Runtime ownership may place the child under the parent, provider, or root scope. A remote provider instead returns a parent-scoped lifecycle id and `localAgent: undefined`; without a local child Session, it is absent from durable enumeration. +## The provider contract: SubagentProvider -## The provider contract: `SubagentProvider` - -Each provider is a named child-agent transport, and multiple providers may coexist. The service validates requested start-time capabilities before `start()`, and rejects a continuable start on a provider without `prepareContinuable`. `inheritsParentContext` describes only conversation seeding (`fork`: true; `spawn` and `acp`: false), allowing consumers to generate accurate model-facing wording without implying inherited tools, services, or authority. A provider whose one-shot route has static provider-owned defaults publishes optional immutable `agentRouteDefaults`, allowing a Consumer to merge model/tool overrides against the correct baseline before preflight. +`SubagentManager` owns startup, message admission, and execution lifetimes. Spawn and Fork contribute detached creation inputs for local Agents. Codex, Claude Code, ACP, and DSH SDK contribute one execution handle; they do not gain multiple turns. Each activation is an execution record that directly retains its AgentHandle or SubagentRun. External completion follows the handle’s result promise; inbox and idle admission belong only to local execution. Both paths share admission, capacity, parent ownership, cancellation, and disposal. The [package reference](../../packages/subagent/subagent/README.md) describes composition and deployment requirements. ```ts type-equiv /** @@ -413,31 +346,28 @@ interface SubagentProvider { */ readonly inheritsParentContext: boolean /** - * Optional static provider-owned provider/model route for one-shot Agent + * Optional static provider-owned provider/model route for child Agent * options. Consumers merge tool/model overrides over these values before * preflight; providers whose route derives from the parent omit it. The value * is detached immutable data and requires `agentOptions` support. */ readonly agentRouteDefaults?: Readonly<{ provider: string; model: string }> /** - * Establish a ONE-SHOT child and return its handle after publication. + * Establish one external execution and return its owned handle. * The service has already validated that every requested start-time - * capability is supported and resolved `request.descriptor`, so a - * session-backed implementation appends that descriptor inside the child's - * initial turn. Before fulfillment, the provider owns setup and cleans any - * unpublished partial resources before rejecting. Ownership transfers on + * capability is supported. Before fulfillment, the provider owns setup and + * cleans any unpublished partial resources before rejecting. Ownership transfers on * fulfillment; subsequent turn or infrastructure failure settles through * the returned run. Distinct starts may overlap; cancellation, failure, * result settlement, and disposal remain independent for each run. */ - start(request: ResolvedSubagentStartRequest): Promise + start?(request: ResolvedSubagentStartRequest): Promise /** * OPTIONAL (continuable-creation capability): contribute the detached * creation inputs that distinguish this provider's continuable children — * only whether the child session is seeded with parent history. Method - * presence IS the capability: the service rejects continuable starts on - * providers without it, while a provider that has it may still serve - * ordinary one-shot delegations. + * presence selects local activation execution. Providers without it execute + * through start and do not accept subsequent messages. * * This is the provider's ONLY participation in a continuable child. The * continuation manager owns identity reservation, composition, Agent @@ -450,18 +380,25 @@ interface SubagentProvider { } ``` -Provider `start()` fulfills with a published run. The service mints a unique `runId`, snapshots `local` from the provider's exact `localAgent`, observes the result, emits `subagent/start`, and returns the same run; a `start()` rejection implies cleanup of unpublished resources and emits no lifecycle pair, while a post-publication result rejection closes the emitted pair. Each continuable Activation emits the same observe-only pair for its residency epoch, so a cold resume is a new epoch with its own `runId`. The paired `subagent/end` carries the same identity and the final output or infrastructure failure. Both events are observe-only and contain listener exceptions. Their `provider` field names the provider that started the run or Activation epoch; it does not claim that the provider remains registered when the edge is emitted. +## Durable enumeration: `listChildren()`, `listDescendants()`, and their entries -## In-process backends: permission, depth, and seed +The model-facing `list_agents` adapter reports current activity as `running` or `inactive`. These values do not describe task completion or guarantee that `send_message` will succeed. -The spawn and fork backends create an ordinary one-shot agent through `parent.ctx`, pass cancellation into core creation, and dispose through `AgentHandle`; a continuable child is instead created by the continuation manager through its own activation-owner scope. Provider removal blocks new starts without revoking accepted runs. Each child gets a new flat scope rather than inheriting parent registrations. Permission, depth, and fork seeding reuse existing Session vocabulary: +`SubagentRuntime.listChildren(parentSessionId, signal?)` reads the parent's `subagentCatalog` view through a live-preferred Session observation and releases that observation on success or failure. It returns direct-child entries in parent event order without reading child logs or enumerating the Session corpus. Query failures propagate; a missing catalog projection fails explicitly. Browser rows derive membership from the shared projection store and add activity from Session status; the control stream pushes complete catalog updates. `listDescendants()` recursively reads those catalogs and derives `hasChildren` from each child catalog. [The parent-catalog Agent Note](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns creation, fork isolation, ordering, and persistence costs. -- **Delegated permission** is captured before the first await. Auto and Full access parents append their captured `permission/preset` identity to the fresh child after fork seeding and sandbox/approval overrides. One-shot and continuable children share this path; cold resume reads only the child log. Read Only and Workspace Write retain the inherited sandbox override plus `approval: never`, so unmatched bundles remain `custom`. Each Auto child call is reviewed independently using existing `parentSession`, creation prompt, and authenticated human/direct-parent messages. The [Auto review decision](../../.agents/notes/implemented/feature/2026-08-28-auto-review.md) defines low/medium/high semantics; no delegation records, receipt, Header field, descriptor field, or Session format is added. +`SubagentRuntime.listDescendants(rootSessionId)` recursively calls the same catalog reader in stable pre-order, preserving each parent's event order. External entries are leaves without local Sessions. One-shot and unknown-mode entries remain traversal nodes; unknown modes produce `unsupported` diagnostics. An unreadable child catalog produces `corrupt` or `unavailable` and stops only that branch. Root read failures, missing services or projections, and cancellation reject the listing. Each reachable catalog is observed once and released before the next read; repeated ids and cycles are skipped. Sessions absent from reachable catalogs are not discovered, including ordinary Session forks and any subagents below those forks. Each row carries its catalog parent and root-relative depth: + +```ts type-equiv +/** One catalog descendant with its direct parent and edge distance from the requested root. */ +type SubagentDescendantListEntry = SubagentListEntry & { + /** Parent whose catalog contains this child. */ + readonly parentId: SessionId + /** Edge distance from the requested root; direct children are `1`. */ + readonly depth: number +} +``` -- **Delegation depth** is durable `SessionHeader.delegationDepth` plus the merge-extensible runtime field `AgentOptions.subagentDepth`; absence means top-level depth zero, and the greater present value is authoritative. The seam owns both fields — the loop neither sets nor reads them — so an in-process child persists parent depth + 1, cold resume cannot lower it, and every start rejects a derived depth outside the safe-integer domain or above a defined absolute `request.maxDepth` cap. -- **Fork seeding** uses [`CreateAgentOptions.seed`](core.md#creation-and-ownership) (a `SessionEvent[]` prefix threaded through `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })`, the same primitive `ctx.agents.resume()` uses). The fork backend passes a *balanced completed-turn prefix* of the parent's log — the parent's events up to and including its last `turn/end` — so the seed is contiguous-from-0 and contains only balanced turns (the in-flight, unbalanced turn is excluded). -`SubagentCatalogEntry` describes a direct child with complete or unknown-mode discovery information; `SubagentCatalogState` is the host-only projection state. `listChildren()` owns a live-preferred parent observation without opening child logs. Browser consumers read `subagentCatalog` through the shared Session projection store and combine membership with Session-list activity. `SubagentCatalogRow` belongs to recursive catalog listing. [The parent-catalog decision](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns the persistent facts and read semantics. @@ -491,7 +428,7 @@ Source: [`packages/subagent/tool-subagent/src/model-selection-settings.ts`](../. ### `ctx.subagents` — `SubagentRuntime` -Named provider registry with one-shot runs, durable discovery, and continuable-child operations. +Named provider registry with managed activations, durable discovery, and local child messaging. ```ts cordis-catalog /** @@ -502,15 +439,26 @@ Named provider registry with one-shot runs, durable discovery, and continuable-c resolveMaxDepth(configured?: number | 'provider-managed'): number | undefined /** - * Establish one durable continuable child and deliver its initial prompt. - * Resolves when the child's inbox accepts that prompt, without waiting for the - * turn to start or for the message to reach the Session log; any earlier - * failure rejects with no ids and rolls back the child entirely. - * @param spec - provider, delegation request, and caller cancellation. - * @returns the durable child id and the accepted prompt's message id. - * @throws when continuation services are unavailable or materialization fails. + * Start a local child under its reserved identity. + * @param spec - local task, reserved child id, and result recipient. + * @returns activation with its accepted initial message id. */ -async startContinuable(spec: ContinuableStartSpec): Promise +startActivation(spec: SubagentActivationSpec & { readonly childId: SessionId }): Promise + +/** + * Start a local or external child. + * @param spec - task, backend, and result recipient. + * @returns activation with a message id only for local children. + */ +startActivation(spec: SubagentActivationSpec): Promise + +/** + * Join progressing descendants without cancelling them. Idle descendants whose + * inboxes require a later wake stay resident and do not delay host completion. + * @param parent - the exact parent whose descendant work is observed. + * @returns whether work was joined; hosts recheck parent idle after true. + */ +async waitForChildren(parent: Agent): Promise /** * Steer one model-authored message to the sender's direct parent or direct @@ -529,13 +477,13 @@ async startContinuable(spec: ContinuableStartSpec): Promise async sendMessage( sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions, ): Promise /** - * Interrupt one live continuable child's current turn under a human parent + * Interrupt one live child's current execution under a human parent * address or an exact live ancestor Agent. Fire-and-return: the cancel * signal is issued before this returns, but the target may keep running * until it observes the signal. Unclaimed pending inbox work, the Activation, * and published descendants are preserved; claimed work is not requeued. - * Once the interrupted driver is idle, a waking send resumes the parked FIFO - * queue. An absent target — including a one-shot or unknown id — + * Once the interrupted Agent is idle, a waking send resumes the parked FIFO + * queue. External backends stop their single execution. An absent target * is an accepted no-op, as is a manager-less composition, which cannot own a * live Activation. * @param targetSessionId - the durable child session id to interrupt. @@ -546,28 +494,28 @@ async sendMessage( sender: Agent, targetId: SessionId, content: ContentBlock[], interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void /** - * Close continuable admission below exact live parent Agents, stop only their + * Close subagent admission below exact live parent Agents, stop only their * visible descendant Activations synchronously, then await admitted scoped * materializations and release those forests child-first. The scoped cutoff * lasts until each exact parent leaves the registry; unrelated parent trees * remain live. * @param parents - exact host-owned parent Agents entering teardown. - * @returns once every retained descendant Activation released its `AgentHandle`. + * @returns once every retained descendant activation released its execution handle. * @throws an aggregate error after all branches settle when any failed. */ -async drainContinuableDescendants(parents: readonly Agent[]): Promise +async drainDescendants(parents: readonly Agent[]): Promise /** - * Release selected resident continuable direct children of one exact live + * Release selected resident direct children of one exact live * parent. Other children of the same parent remain admitted and resident. * Absent targets and a manager-less composition are accepted no-ops. * @param parent - exact live direct parent authorizing the selected release. * @param childIds - durable direct-child ids to release when resident. - * @returns once every selected Activation released its `AgentHandle`. + * @returns once every selected activation released its execution handle. * @throws {SubagentError} `UNAUTHORIZED` when a resident target belongs to a * different parent or the supplied parent identity is stale. */ -async drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): Promise +async drainChildren(parent: Agent, childIds: readonly SessionId[]): Promise /** * Read the parent's durable direct-child catalog without loading or resuming an Agent. @@ -583,7 +531,8 @@ listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise ``` Types: [Agent](core.md) · [ContentBlock](llm-streaming.md) · [MessageId](llm-streaming.md) · [SessionId](core.md) diff --git a/docs/subsystems/subagent.zh.md b/docs/subsystems/subagent.zh.md index 780fa2e85a..220ffe5d42 100644 --- a/docs/subsystems/subagent.zh.md +++ b/docs/subsystems/subagent.zh.md @@ -1,30 +1,21 @@ -# Subagent +# 子代理 [English](subagent.md) | 中文 -subagent seam 让一个 agent(智能体)将工作委派给子 agent。与 [bash](shell.zh.md) 一样,它是**一项可选能力**,不属于 agent loop(智能体循环),因此其类型定义在此而非 [core.md](core.zh.md) 中。它不同于其他能力 seam,因为**同一上下文中可共存多个提供方实现**,并按名称注册(`ctx.subagents`),而 bash 只允许一个执行器。该注册表遵循 [LLM(大语言模型)适配器注册表](llm-streaming.zh.md),而非单服务的 bash 执行器。 +统一的 activation API 管理本地会话和外部执行。 -Service Definition:[dsh-subagent](../../packages/subagent/subagent)(`ctx.subagents` + 下文词汇)。Service Provider 是六个兄弟包:`dsh-subagent-spawn-in-process`、`dsh-subagent-fork-in-process`、`dsh-subagent-acp`、`dsh-subagent-codex`、`dsh-subagent-claude-code`、`dsh-subagent-dsh-sdk`;面向模型的 Consumer 包括 [dsh-tool-subagent](../../packages/subagent/tool-subagent)(按提供方委派)和 [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)(可选的全局 `send_message`、`interrupt_agent` 与 `list_agents` 控制工具)。同一个 `ctx.subagents` 服务通过内部激活管理器负责可继续子 agent 编排,通过 parent 目录发现直接 child,并通过父目录递归发现后代。产品提供方设计理由见 [历史Codex 与 Claude Code Agent Note](../../.agents/notes/archived/feature/2026-08-04-claude-code-and-codex-subagent-backends.md);通用 seam 的设计理由见 [历史subagent Agent Note](../../.agents/notes/archived/feature/2026-06-21-subagent-capability-seam.md)、[可继续 subagent Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md)和[相邻 Agent 消息 Agent Note](../../.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md);[已归档的列表身份投影记录](../../.agents/notes/archived/architecture/2026-08-06-subagent-list-identity-projection.md)记录了最初的列表身份决策。 +## 能力与 activation 请求 -源码:[`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts)、[`packages/subagent/subagent/src/index.ts`](../../packages/subagent/subagent/src/index.ts)和 [`packages/subagent/subagent/src/continuation.ts`](../../packages/subagent/subagent/src/continuation.ts) +所有后端都使用 startActivation。能力标记校验请求选项;prepareContinuable 选择本地 Agent 创建,start 选择一次外部执行。面向模型的工具采用 parent 回传。Workflow 与 code mode 消费者可以等待 caller 结果,不额外向父代理注入消息。历史子会话的 descriptor 不可用时,以 `mode: 'unknown'` 保持可发现性,但不授予继续执行能力;[包 README](../../packages/subagent/subagent/README.zh.md) 定义目录持久化语义。 -`subagentCatalog` projection 通过 Session 观察和客户端快照暴露按父会话事件排序的 `SubagentCatalogEntry[]`。每个条目包含子级 id、创建时间、模式和依模式确定的标签;fork 继承的目录事实不在其中。[subagent 包](../../packages/subagent/subagent/README.zh.md) 定义目录创建和持久化语义。历史子会话的 descriptor 不可用时使用 `mode: 'unknown'`,保留 header 身份供发现,但不授予继续执行能力。 - -## 两类能力,两种发现方式 - -提供方通过一个静态描述符公布其**启动时**功能,服务会在单次 run 存在之前即行检查;如果请求依赖提供方不具备的功能,会被明确拒绝(`SubagentError('UNSUPPORTED_CAPABILITY')`),绝不会被接受后静默忽略。这些 flag 仅描述单次 [`start()`](#the-provider-contract-subagentprovider) 路径,即由提供方组合子 agent 的路径。**可继续**子 agent 由继续执行管理器自行组合,因此它们由唯一一个可选方法把关,方法存在即为能力,并以 TypeScript 的类型收窄作为发现机制:[`SubagentProvider.prepareContinuable`](#the-provider-contract-subagentprovider)。 +可选的 `request.cwd` 指定子级的初始目录;相对路径以父级当前有效目录为基准解析。省略时,在准入阶段捕获该目录。本地子 Session 保留父级的原始项目,冷恢复保留其日志中的目录状态。 ```ts type-equiv /** - * Which START-TIME features a provider supports. Checked by the service before delegating to - * {@link SubagentProvider.start}: a request that needs a capability the chosen provider lacks - * is rejected with a typed error rather than accepted-then-ignored (the "fail loud, no silent - * degradation" rule). These flags describe the ONE-SHOT - * {@link SubagentProvider.start} path, where the provider composes the child; - * continuable children are composed by the continuation manager itself and are - * gated by {@link SubagentProvider.prepareContinuable} instead. Each flag - * corresponds one-to-one to a {@link SubagentStartRequest} option: `depthLimit` - * to `maxDepth`; the other names match. + * Start-time options supported by a registered backend. The manager checks + * every requested option before preparing a local child or starting an external + * execution. depthLimit controls maxDepth; the remaining flag names match + * SubagentStartRequest fields. */ interface SubagentCapabilities { readonly agentOptions: boolean @@ -35,27 +26,57 @@ interface SubagentCapabilities { } ``` -## 单次启动请求 +```ts type-equiv +/** One managed child execution and its result delivery policy. */ +interface SubagentActivationSpec { + /** Registered backend to use. */ + readonly provider: string + /** Short task label retained in cataloged child membership. */ + readonly label: string + /** Optional reserved identity for a local child; external backends allocate their own ids. */ + readonly childId?: SessionId + /** Task, parent, and backend-supported execution options. */ + readonly request: Omit + /** Cancellation before publication; callers own later cancellation through dispose. */ + readonly signal: AbortSignal + /** Parent delivery notifies the model; caller delivery only returns the result. Local children always enter the parent catalog. */ + readonly delivery: 'parent' | 'caller' +} +``` -工具层根据模型输入和自身配置构建此请求;服务在 `start` 之前针对指定提供方进行校验。必填的 `parent` 提供会话 cwd、谱系与委派深度。可选的 Agent 提供方、模型、推理强度与 token 覆盖、output schema、depth、工具过滤器和 persona 需要对应的能力 flag 匹配。进程内后端会把 `agentOptions` 合并到父 Agent 选项之上,将 filter 和 persona 的作用域限定在子 agent 创建阶段,并通过强制 capture 工具实现所支持的 object-rooted schema。DSH SDK 后端会把四个 Agent 路由字段合并到实例默认值之上,并在子运行时初始化期间校验;ACP、Codex 与 Claude Code 会在启动传输前拒绝 `agentOptions`。 +```ts type-equiv +/** A managed execution; disposal addresses this exact activation, never a later resume. */ +interface SubagentActivation { + /** Stable identity of the child. */ + readonly childId: SessionId + /** Accepted inbox message, when the backend has a local inbox. */ + readonly messageId?: MessageId + /** + * Execution result after teardown settles and notifications are sent; capture failures reject. + * Teardown failures are reported by dispose() without replacing a captured result. + */ + readonly result: Promise + /** Stop and release this activation and its owned descendants; rejects on teardown failure. */ + dispose(): Promise +} +``` ```ts type-equiv /** - * What a caller asks for when starting a ONE-SHOT subagent. The tool layer - * builds this from the model's `{ description, prompt }` plus its own config; - * the service validates {@link SubagentCapabilities} against the named provider - * and resolves the durable descriptor before dispatching to - * {@link SubagentProvider.start}. + * Task and optional capabilities supplied to a backend. startActivation carries + * these fields in request while owning its label, cancellation, and delivery + * policy separately. */ interface SubagentStartRequest { + /** Initial child directory; relative paths resolve against the parent's current directory. Omitted inherits that directory at start. */ + readonly cwd?: string /** Optional short display label persisted with a session-backed child. */ readonly label?: string /** Content delivered as the child's user message. */ readonly prompt: ContentBlock[] /** - * The spawning agent. In-process providers derive workspace, lineage, and - * delegation depth from its durable session state. ACP reads only its cwd, - * and only when no deployment `cwd` override is configured. + * The spawning agent. Its effective directory supplies the default cwd; + * in-process children retain its origin, lineage, and delegation depth. */ readonly parent: Agent /** @@ -106,121 +127,17 @@ interface SubagentStartRequest { } ``` -`signal` 是就绪前后唯一的取消通道。[subagent 组合控制 Agent Note](../../.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md)规定 persona、live 全局工具过滤、绝对深度以及「可见性而非权限」的设计理由。 - -面向调用方的请求不携带目录格式细节或继续执行状态。`SubagentRuntime.start()` 会在能力检查后解析分离的一次性描述符,再将以下面向提供方的请求传给所选传输;可继续子 agent 绝不会到达 `SubagentProvider.start()`: - ```ts type-equiv -/** - * Provider-facing one-shot request after {@link SubagentRuntime.start} resolves - * the durable child descriptor. - */ +/** Provider-facing request with an absolute directory selected before startup. */ interface ResolvedSubagentStartRequest extends SubagentStartRequest { - /** Detached descriptor a session-backed provider persists in the child log. */ - readonly descriptor: SubagentDescriptorData + /** Absolute child directory captured from the parent or explicit request. */ + readonly cwd: string } ``` -## 可继续子 agent 与激活 +## 本地子代理与 activation -**可继续后台 subagent** 是一份持久化子 agent 会话(Session),至多关联一个进程内的 **Activation(激活)**,即被重建的子 Agent 处于驻留状态的时段。Activation 不是请求、结果、取消或 Task:它可以执行多个 FIFO 轮次,并在其创建的后代仍在运行期间保持驻留。继续执行管理器负责 activation 准入、直接父级鉴权、实时所有权图、冷恢复(cold resume)与子级优先释放;agent loop 负责一切轮次排序与执行。任何可继续路径都不会创建 Task,也不会创建承载中间结果的包装层。 - -```text -persisted Session - -> optional live Activation - -> one retained AgentHandle - -> Agent inbox as the only turn FIFO - -> zero or more owned child Activations -``` - -`SubagentRuntime.startContinuable()` 会预留稳定的子 agent id,对版本化的 `subagent/descriptor` payload 建立快照,向指定提供方索取其分离的 `ContinuableCreateSpec`,通过私有的 activation-owner 作用域创建子 Agent,建立任何可继续父级的所有权,并提交初始提示词。当收件箱(inbox)准入产出消息 id 时,它以 `{ childId, messageId }` resolve——无需等待轮次开始,也无需等待消息进入会话日志。在该准入之前的任何失败都会以两个 id 都不返回的方式 reject,并 dispose(资源释放)任何已创建的 handle,回滚 Activation 与父级所有权。 - -`SubagentRuntime.sendMessage()` 是唯一由模型编写消息的操作。它接收确切在线 sender 与目标 id,只允许直接 parent 或直接可继续 child,自行推导 sender 来源信息,并根据目标 child 的 Activation 驻留状态路由: - -| 目标 Activation 状态 | `sendMessage` | -|---|---| -| `running` | 在同一 Activation 中 steer 最近的 step | -| `waiting` | 唤醒并 steer 同一 Activation | -| 无 Activation | 冷恢复新的 Activation,然后 steer | - -`running` 表示 Agent 拥有活跃的 driver 或 maintenance 任务;`waiting` 表示没有活跃的 Agent 工作,但其 Inbox 非空或仍拥有至少一个尚未完成 dispose 的子 Activation;`settled` 表示没有活跃的 Agent 工作、Inbox 为空且其拥有的每个子级都已 dispose,此时管理器会 dispose [`AgentHandle`](core.zh.md#creation-and-ownership) 并移除该 Activation。管理器根据 `Agent.whenIdle()`、`Agent.inbox.hasPending`、其拥有的子级集合,以及让过期观察失效的 Activation generation 推导这些内部条件,而非维护第二套执行状态机。最终 Session flush 之后,child-lock 决策会通过 `Agent.runMaintenance()` 的同步 task 入口占用 idle 阶段,并在同一个 JavaScript turn 内关闭准入。这条保守规则不区分投递模式:`Agent.inject()` 停放的 context 可以让空闲 Activation 及其在线祖先继续驻留,直到唤醒投递将其 claim、queue 变更将其移除,或 manager teardown 将其丢弃。 - -Agent 收件箱是唯一队列。每条 Agent 消息都使用 `Agent.steer()`:空闲目标会启动一个轮次,运行中目标则在最近的 step 边界领取消息。浏览器 `subagent.prompt` Remote 会另行通过同一条内部准入路径携带 `delivery: 'queue' | 'steer'`;Queue 开启后续 FIFO 轮次,Steer 保留 Agent loop 的 best-effort 最近 step 行为以及消息的人类来源。投递成功会返回被接受的 `MessageId`;既有的 `agent/inbox/inserted`、`agent/inbox/claimed` 与 `agent/inbox/discarded` 事件仍是消息生命周期的观测点,继续执行层不定义第二条队列。 - -权限来自确切在线 sender。parent 到 child 的投递要求目标的 `SessionHeader.parentSession` 指向 sender;child 到 parent 的投递要求 sender 的驻留 Activation 指向目标。sibling、相隔多于一条边的 ancestor、self-target、陈旧 Agent 对象与一次性 child 都会被拒绝。每条已接受消息都以 `Agent sent a message:` 作为前缀,并记录 `AgentMessageSource`;来源信息记录 sender,但不授予权限。 - -对于 `startContinuable()`、`sendMessage()` 与浏览器 prompt 投递,调用方 signal 仅在收件箱接受之前掌管查找、物化与准入。此后管理器独立掌管该 Activation:之后的调用方取消既不会取消已接受的轮次,也不会 dispose 子 agent。公开 subagent 服务不暴露由调用方选择的 Agent 消息调度;浏览器人类 Queue 与 Steer 仍是内部适配器选择。 - -在线 queue occurrence 变更属于 Session 域。只有在线 subagent-owned Agent 的当前 projection identity 为 continuable,且其 descriptor 序号位于该 child 自身的非 seed suffix 时,`session.updateQueue` 才会接纳普通 Edit、Remove 与 QueueDock Steer。Identity projection 以 last-wins 方式折叠 descriptor,因此 child descriptor 会覆盖 fork lineage 保留的 descriptor;own-suffix 序号检查会阻止仅来自 seed 的祖先 identity 授权变更。One-shot、缺失、未知、损坏或冷 child 会被拒绝,queue 变更绝不会冷恢复 child。这些变更以目标 Session id 作为人类权限,包括待处理 `nextStep` steering 或注入 context。Steer 要求 queued `MessageId`,且 command 开始时 Agent 必须报告 running;准入后发生取消时,会使用 Agent 已接受的唤醒 `nextTurn` fallback。Edit 会在同一个 `MessageId` 下改写内容,且 Edit 与 Steer 都会同步完成 Inbox 变更,因此 settlement 只会观察最终状态。`agent/inbox/claimed` 与 `agent/inbox/discarded` 都会唤醒 watcher 重新读取是否仍有待处理 occurrence;这样,直接 Agent 投递可以恢复停放工作,而移除最后一个停放 occurrence 可使 idle child 结算。[人类 inbox 控制 参考](../../packages/api/session-controller/README.zh.md)拥有这些语义。 - -`SubagentRuntime.interrupt(targetSessionId, authority)` 是唯一的公开停止操作:它同步完成鉴权,对在线目标发出 `Agent.cancel(cause, { keepInbox: true })`,然后不等待完全停稳即返回。Activation、其尚未领取的待处理 inbox 工作与已发布的后代均不受影响;已被领取进入中断轮次的工作不会重新入队。被中断的 driver 进入 idle 后,一次唤醒发送会恢复被暂停的 FIFO 队列。不存在的目标——未知、一次性或已结算——以及未绑定管理器的组合是被接受的 no-op。对在线目标,错误的 parent 地址或不在其在线祖先链中的调用方会以 `UNAUTHORIZED` 拒绝;陈旧的 ancestor 对象和指向自身的 ancestor 请求会在查找目标前拒绝。 - -```ts type-equiv -/** - * Authority under which one interrupt request is admitted. `user` carries the - * durable direct-parent address a human client presented; `ancestor` carries - * the exact live Agent object whose recorded lineage must contain the caller. - */ -type SubagentInterruptAuthority = - | { readonly kind: 'user'; readonly parentSessionId: SessionId } - | { readonly kind: 'ancestor'; readonly agent: Agent } -``` - -每个 Activation 都拥有自己的 `AgentHandle` 和一个 `ownedChildren: Set`;由于一份会话至多有一个存活 Activation,子会话 id 无需另一个运行时化身引用即可标识存活的子 agent。启动子 agent 或提交源自 parent 的工作,会在子 agent 能够运行之前将其注册到受继续执行管理的父级集合中;只要该集合非空,该父级就无法 settle。顶层或其他非继续执行的 Agent 没有 Activation,处于 waiting 图之外。只有当子 Agent 没有活跃工作、其 Inbox 为空、该子 agent 的每个子级都已 dispose、best-effort 的最终会话 flush 结算完毕,且子 agent 的 `AgentHandle` 完成 dispose 之后,才会释放子 agent。 - -最终结算会等待 `ctx.sessions.flush(session)`,但会忽略其参与布尔值,因为任意 listener 都无法证明某个持久化后端已存储该状态。rejection 会被记录,但不会使 Activation 失败;管理器仍会 dispose 该 handle 并释放所有权,此后持久化的子 agent 状态在后续恢复时可能缺失或陈旧。管理器卸载会调用内部的管理器全局 drain,关闭准入并 dispose 每片在线森林;`drainContinuableDescendants(parents)` 只关闭由 host 确切拥有的在线 Agent 之下的准入,并 dispose 其可继续后代,而无关森林保持在线。两者都会等待各自作用域内已获准的物化过程,自顶向下传播取消,按 child-first 顺序释放 handle,并且即使个别分支失败也会等待所有选中分支。持久化子会话不受该进程内拆卸的影响。 - -```ts type-equiv -/** Durable attribution for one model-authored message between adjacent Agents. */ -interface AgentMessageSource { - readonly kind: 'agent-message' - /** A message another agent addressed to this one (`relay` context form). */ - readonly form: 'relay' - /** Session id of the Agent whose tool call produced the message. */ - readonly senderSessionId: SessionId -} -``` - -```ts type-equiv -/** Options for one model-authored message between adjacent Agents. */ -interface SubagentSendMessageOptions { - /** Caller cancellation, owning the operation only until inbox acceptance. */ - readonly signal: AbortSignal -} -``` - -```ts type-equiv -/** Identities returned once a continuable child accepted its initial prompt. */ -interface ContinuableStart { - /** The durable child session id, stable across activations. */ - readonly childId: SessionId - /** The accepted initial prompt's inbox message id. */ - readonly messageId: MessageId -} -``` - -当驻留 Activation 结算时,管理器会向该 child 持久化的直接 parent 投递一条通知,说明该 epoch 如何结束,并携带其最终 assistant 输出中的非空文本块;若没有剩余的非空文本,则携带 `It left no closing message.`。对每个调用方拿到过 id 的 child,这条投递都是无条件的;它发生在会让 parent 被判定为已结算的所有权释放之前,并通过与 Agent 消息相同的唤醒 Agent 投递到达驻留 parent。若 parent 自身所在的谱系已在拆卸中,这条通知会以不唤醒的方式送达,因为唤醒一个 idle Agent 是开启一个轮次,而不是排队等待工作。其来源信息使用一个独立的 kind,因此 transcript(文本记录)绝不会把运行时的记账呈现为 child 自己写下的内容。 - -```ts type-equiv -/** - * Durable attribution for the runtime's own account of a continuable child - * settling. Deliberately a different kind from - * {@link AgentMessageSource}: an Agent message is content the sender chose, - * while this message is the manager stating what became of the child, and a - * transcript that merged them would credit the child with words it never wrote. - */ -interface SubagentSettledMessageSource { - readonly kind: 'subagent-settled' - /** A runtime account shown without expanding the row (`notice` context form). */ - readonly form: 'notice' - /** One-line account of how the child ended. */ - readonly summary: string - /** Session id of the child that settled. */ - readonly senderSessionId: SessionId -} -``` - -提供方只参与准备初始创建 spec,`spawn` 与 `fork` 在此有所不同。其返回的 spec 只携带分离的、提供方专属的创建输入——即可选的父级历史种子——不含 Agent、`AgentHandle`、提示词投递、结果、dispose 或恢复操作。冷恢复根本不经由提供方分发:管理器折叠通用描述符,通过同一个 activation-owner 作用域调用 `ctx.agents.resume()`,并提交等待中的轮次。 +本地子代理拥有持久 Session,最多有一个在线 activation。Manager 预留身份和容量,通过后端准备子代理,并管理输入准入、冷恢复以及先子后父的释放顺序。activation 结果在资源释放和完成通知之后结算。释放操作单独报告清理失败,不会覆盖已捕获的结果。结构化结果提交会关闭该 activation 的输入;之后的冷恢复不会继承输出 schema。 ```ts type-equiv /** @@ -231,6 +148,8 @@ interface SubagentSettledMessageSource { * history. */ interface ContinuableCreateRequest { + /** Absolute initial directory captured before provider preparation. */ + readonly cwd: string /** The reserved durable child session id, for provider diagnostics. */ readonly sessionId: SessionId /** The delegating parent agent whose history a seeding provider reads. */ @@ -260,34 +179,66 @@ interface ContinuableCreateSpec { } ``` -描述符([descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts) 中的 `SubagentDescriptorData`)是每个由会话支撑的 subagent 所使用、按模式判别的持久化身份。两种模式都携带提供方名称。`one-shot` 描述符可以携带调用方拥有的可选显示 `label`;`continuable` 描述符要求以委派 `description` 作为持久化创建标签,并另外对已解析的子 agent `agentOptions.provider`/`model`/`reasoningEffort` 与可选的 `persona`/`toolFilter` 建立快照,用于冷恢复。它绝不会对可合并扩展的 `AgentOptions` 对象建立快照,因此无关的扩展值不会破坏继续执行,后续新增组合配置输入则是一次有意的版本更改。描述符省略 `subagentDepth`(冷恢复以持久化 header 中的 `delegationDepth` 作为单调下界)和 `outputSchema`(单次运行或 Activation 的结果约定,而非持久化身份)。 +## 消息与中断 -本地一次性提供方会在子 agent 的初始轮次内、首次请求前追加描述符。继续执行管理器会在任何提供方提供的谱系之后、初始提示词获准之前追加描述符;`Session.inheritedEventCount` 仍是 fork 谱系边界:恢复时的描述符权威读取子 agent 自身的后缀,而身份投影以 last-wins 折叠 `subagent/descriptor`,子 agent 自己的描述符会覆盖 fork seed 中祖先的描述符。该事件只进入日志:不含 `surfaceOp`,绝不进入模型历史,并由仅追加日志跨压缩保留。格式错误的当前版本描述符属于损坏;本运行时无法对不受支持的版本进行分类。 - -## 持久化枚举:`listChildren()`、`listDescendants()` 与其条目 - -模型侧的 `list_agents` 适配器将当前活动表示为 `running` 或 `inactive`。这些值不描述任务完成情况,也不保证 `send_message` 会成功。 - -`SubagentRuntime.listChildren(parentSessionId, signal?)` 通过优先使用在线 Session 的观察读取父会话的 `subagentCatalog` 视图,并在成功或失败时释放观察。它按父会话事件顺序返回直接子级条目,不读取子级日志,也不枚举 Session 语料库。查询失败直接传播;缺少目录投影时显式失败。浏览器条目从共享 projection store 派生成员关系,并从 Session 状态补充活动状态;control stream 推送完整目录更新。`listDescendants()` 递归读取这些目录,并从每个子级目录推导 `hasChildren`。[父目录 Agent Note](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明创建、fork 隔离、排序和持久化成本。 - -`SubagentRuntime.listDescendants(rootSessionId)` 以稳定前序递归调用同一目录读取函数,保留每个父级的事件顺序。一次性和未知模式条目仍是遍历节点;未知模式产生 `unsupported` 诊断。无法读取的子级目录产生 `corrupt` 或 `unavailable`,只停止该分支。根读取失败、服务或投影缺失、取消会使整个查询失败。每个可达目录只观察一次,并在下一次读取前释放;重复 id 和循环引用会被跳过。可达目录中不存在的 Session 不会被发现,包括普通 Session fork 及其下的子 agent。每行携带目录中的父级和相对根的深度: +sendMessage 根据确切的在线发送者实例授权,仅允许相邻本地代理通信。运行中的子代理接收引导输入;不在线的可继续子代理从持久化恢复。外部执行拒绝后续消息。本地与外部执行都会通过父级结束通知自动回传最终文本、结构化结果、诊断和状态。本地子代理也可以通过 send_message 发送消息。caller 回传会抑制结束通知。drainDescendants 与 drainChildren 对本地和外部 activation 都等待所属任务清理完成。 ```ts type-equiv -/** One catalog descendant with its direct parent and edge distance from the requested root. */ -type SubagentDescendantListEntry = SubagentListEntry & { - /** Parent whose catalog contains this child. */ - readonly parentId: SessionId - /** Edge distance from the requested root; direct children are `1`. */ - readonly depth: number +/** Durable attribution for one model-authored message between adjacent Agents. */ +interface AgentMessageSource { + readonly kind: 'agent-message' + /** A message another agent addressed to this one (`relay` context form). */ + readonly form: 'relay' + /** Session id of the Agent whose tool call produced the message. */ + readonly senderSessionId: SessionId } ``` +```ts type-equiv +/** + * Durable attribution for the runtime's own account of a continuable child + * settling. Deliberately a different kind from + * {@link AgentMessageSource}: an Agent message is content the sender chose, + * while this message is the manager stating what became of the child, and a + * transcript that merged them would credit the child with words it never wrote. + */ +interface SubagentSettledMessageSource { + readonly kind: 'subagent-settled' + /** A runtime account shown without expanding the row (`notice` context form). */ + readonly form: 'notice' + /** One-line account of how the child ended. */ + readonly summary: string + /** Session id of the child that settled. */ + readonly senderSessionId: SessionId +} +``` - +```ts type-equiv +/** Options for one model-authored message between adjacent Agents. */ +interface SubagentSendMessageOptions { + /** Caller cancellation, owning the operation only until inbox acceptance. */ + readonly signal: AbortSignal +} +``` -## 终态结果:`SubagentResult` +```ts type-equiv +/** + * Authority under which one interrupt request is admitted. `user` carries the + * durable direct-parent address a human client presented; `ancestor` carries + * the exact live Agent object whose recorded lineage must contain the caller. + */ +type SubagentInterruptAuthority = + | { readonly kind: 'user'; readonly parentSessionId: SessionId } + | { readonly kind: 'ancestor'; readonly agent: Agent } +``` -单次 run 的最终产出,由 `SubagentRun.result` resolve。`structured` 仅在请求了 `outputSchema` 且成功满足时才存在;请求 schema 不保证一定能得到它,当子 agent 失败或结束时未产出有效 capture 时,提供方可能返回 `stopReason: 'error'`。提供方可以为非 `completed` 结果附带安全且不属于 assistant 内容的 `diagnostic`;在消费方将它与 `output` 分开呈现前,提供方会排除工具输入、文件内容、环境值、凭证与原始协议载荷,并把完整值限制在 4096 个 UTF-8 字节以内。非 `completed` 的 `stopReason` 意味着 `output` 可能不完整——消费方将其映射为 `isError` 的工具结果,而非将部分输出报告为成功。 +## 持久枚举 + +`listChildren` 读取父级拥有的目录,其中包含所有本地子级及 parent 投递的外部子级。caller 投递的外部执行由调用方负责成员关系与结果收集;本地工作流子级在完成后仍可被发现。外部条目携带 `mode: 'external'`,不能打开子 Session 或接受后续消息。目录在创建时记录成员关系,不携带执行状态。`list_agents` 展示可继续的直接子级,其活跃状态为 `running` 或 `inactive`;参见[控制工具](../../packages/subagent/tool-subagent-control/README.zh.md#list_agents)。 + +## 结果与后端句柄 + +SubagentResult 供程序消费方读取。activation 注册表保留外部后端句柄,直到释放完成。清理失败不会覆盖已捕获的执行结果:activation 结果与父级通知保留该结果,dispose 拒绝,而在线 subagent/end 事件报告 error。后端提供的诊断必须满足下述安全信息要求。 ```ts type-equiv /** @@ -322,8 +273,6 @@ interface SubagentResult { } ``` -`SubagentStopReason` 是一个[可合并扩展的派生联合类型](core.zh.md#the-map--derived-union-pattern)——后端可以添加变体,因此消费方应对已知 case 分支处理,将未知的终态原因视为失败: - ```ts type-equiv /** * Why a subagent run ended. Merge-extensible (a backend may add variants); @@ -345,33 +294,17 @@ interface SubagentStopReasonMap { } ``` -## 单次 run:`SubagentRun` - -`SubagentRun` 是消费方持有的、指向一个已发布单次子 agent 的句柄——一次可 dispose 的前台委派,只有一个结果,绝不是持久化子 agent handle。发布后的提示词提交、轮次工作与基础设施故障归 `result` 所有。消费方 await 该结果并始终 dispose 该 run,直至完全停稳。子 agent 失败时以非 completed 的 stop reason resolve;只有无法表示的基础设施故障才会 reject。run 没有 steering,也没有恢复:可继续对话根本没有 run,因为继续执行管理器直接持有它们的 `AgentHandle`,并通过子 agent 自己的收件箱为每个轮次排序。 - ```ts type-equiv /** - * ONE-SHOT child handle returned after publication. Prompt submission, turn - * work, and infrastructure faults after that boundary belong to {@link result}. - * Consumers await that result and must always {@link dispose} to cancel - * remaining work and reach quiescence. A run is one disposable foreground - * delegation with one result; continuable conversations have no run — the - * continuation manager holds their `AgentHandle` directly and orders every - * turn through the child's own inbox. + * Backend execution handle owned directly by the activation registry. + * A result may become available before resource release; the manager always + * disposes the handle and awaits cleanup. Backend startup failures clean up + * partial resources before rejecting, while accepted execution failures settle + * through result. */ interface SubagentRun { - /** - * Parent-scoped run id. For a local run, this MUST equal the published child - * session id, whose `parentSession` records `request.parent.session.id`; a - * remote provider mints an id unique in the parent namespace. - */ + /** Provider-minted id, unique across all parents, providers, and local Sessions in this runtime. */ readonly id: SessionId - /** - * The exact published in-process child, or `undefined` for a remote run. - * When present, its id is {@link id}; the provider retains no ownership - * implication beyond the run's ordinary {@link dispose} contract. - */ - readonly localAgent: Agent | undefined /** * Resolves with the child's terminal {@link SubagentResult} when the run * settles. Does NOT reject on a child-level failure — a model/transport @@ -388,13 +321,9 @@ interface SubagentRun { } ``` -本地单次 run 必须在 `start()` fulfill 之前发布一个普通子 agent/会话,将该子会话 id 作为 `SubagentRun.id` 返回,以 `localAgent` 暴露确切的子 agent,在子 agent 的 `parentSession` header 中记录 `request.parent.session.id`,并在子 agent 的初始轮次内、首次请求前追加已解析的描述符。运行时所有权可以把子 agent 放在 parent、提供方或 root 作用域下。远程提供方则返回 parent 作用域的生命周期 id 与 `localAgent: undefined`;由于没有本地 child Session,它不会出现在持久化枚举结果中。 +## 提供方约定:SubagentProvider - - -## 提供方约定:`SubagentProvider` - -每个提供方都是一个具名的子 agent 传输层,多个提供方可以共存。服务在 `start()` 之前校验请求的启动时能力,并拒绝在没有 `prepareContinuable` 的提供方上发起可继续 start。`inheritsParentContext` 仅描述对话种子注入(`fork`:true;`spawn` 和 `acp`:false),使消费方能生成准确的面向模型措辞,而不暗示继承了工具、服务或权限。如果某个提供方的一次性路由拥有静态的提供方自有默认值,它会公开可选且不可变的 `agentRouteDefaults`,使 Consumer 能够在预检前以正确基线合并模型与工具覆盖。 +`SubagentManager` 统一管理启动、消息准入与执行生命周期。Spawn 与 Fork 提供用于本地 Agent 的独立创建输入。Codex、Claude Code、ACP 和 DSH SDK 提供一个执行句柄,不增加多轮能力。每条 activation 都是直接持有 AgentHandle 或 SubagentRun 的执行期记录。外部完成由句柄的 result promise 确定;inbox 与空闲准入仅属于本地执行。两条路径共用准入、容量、父子所有权、取消和释放机制。[包参考](../../packages/subagent/subagent/README.zh.md)说明组合与部署要求。 ```ts type-equiv /** @@ -417,31 +346,28 @@ interface SubagentProvider { */ readonly inheritsParentContext: boolean /** - * Optional static provider-owned provider/model route for one-shot Agent + * Optional static provider-owned provider/model route for child Agent * options. Consumers merge tool/model overrides over these values before * preflight; providers whose route derives from the parent omit it. The value * is detached immutable data and requires `agentOptions` support. */ readonly agentRouteDefaults?: Readonly<{ provider: string; model: string }> /** - * Establish a ONE-SHOT child and return its handle after publication. + * Establish one external execution and return its owned handle. * The service has already validated that every requested start-time - * capability is supported and resolved `request.descriptor`, so a - * session-backed implementation appends that descriptor inside the child's - * initial turn. Before fulfillment, the provider owns setup and cleans any - * unpublished partial resources before rejecting. Ownership transfers on + * capability is supported. Before fulfillment, the provider owns setup and + * cleans any unpublished partial resources before rejecting. Ownership transfers on * fulfillment; subsequent turn or infrastructure failure settles through * the returned run. Distinct starts may overlap; cancellation, failure, * result settlement, and disposal remain independent for each run. */ - start(request: ResolvedSubagentStartRequest): Promise + start?(request: ResolvedSubagentStartRequest): Promise /** * OPTIONAL (continuable-creation capability): contribute the detached * creation inputs that distinguish this provider's continuable children — * only whether the child session is seeded with parent history. Method - * presence IS the capability: the service rejects continuable starts on - * providers without it, while a provider that has it may still serve - * ordinary one-shot delegations. + * presence selects local activation execution. Providers without it execute + * through start and do not accept subsequent messages. * * This is the provider's ONLY participation in a continuable child. The * continuation manager owns identity reservation, composition, Agent @@ -454,18 +380,27 @@ interface SubagentProvider { } ``` -提供方的 `start()` 会以已发布的 run fulfill。服务铸造唯一的 `runId`,从提供方确切的 `localAgent` 快照 `local`,观察结果,emit `subagent/start`,并返回同一个 run;`start()` rejection 意味着未发布资源已清理,且不会 emit 生命周期事件对,而发布后的结果 rejection 会结束已经 emit 的事件对。每个可继续 Activation 都会为其驻留纪元 emit 相同的仅观察事件对,因此一次冷恢复就是一段拥有自己 `runId` 的新纪元。配对的 `subagent/end` 携带相同标识与最终输出或基础设施失败。两个事件都仅用于观察,且会隔离各自的 listener 异常。其中的 `provider` 字段标明了启动 run 或 Activation 时段的提供方,并不声明该 edge 发出时提供方仍处于注册状态。 +## 持久化枚举:`listChildren()`、`listDescendants()` 与其条目 -## 进程内后端:权限、深度与种子 +模型侧的 `list_agents` 适配器将当前活动表示为 `running` 或 `inactive`。这些值不描述任务完成情况,也不保证 `send_message` 会成功。 -spawn 和 fork 后端通过 `parent.ctx` 创建一个普通的单次 agent,将取消信号传入核心创建流程,并通过 `AgentHandle` 进行 dispose;而可继续子 agent 则由继续执行管理器通过其自己的 activation-owner 作用域创建。移除提供方会阻止新的 start,但不会撤销已接受的 run。每个子 agent 获得一个新的扁平作用域,而非继承父级注册。权限、深度与 fork 种子注入复用既有的 Session 词汇: +`SubagentRuntime.listChildren(parentSessionId, signal?)` 通过优先使用在线 Session 的观察读取父会话的 `subagentCatalog` 视图,并在成功或失败时释放观察。它按父会话事件顺序返回直接子级条目,不读取子级日志,也不枚举 Session 语料库。查询失败直接传播;缺少目录投影时显式失败。浏览器条目从共享 projection store 派生成员关系,并从 Session 状态补充活动状态;control stream 推送完整目录更新。`listDescendants()` 递归读取这些目录,并从每个子级目录推导 `hasChildren`。[父目录 Agent Note](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明创建、fork 隔离、排序和持久化成本。 -- **委派权限**在首次 await 前捕获。Auto 与 Full access 父级在 fresh child 完成 fork seed 和 sandbox/approval override 后,追加捕获的 `permission/preset` 身份。单次与可继续 child 共用此路径;cold resume 只读取 child 日志。Read Only 与 Workspace Write 保留继承的 sandbox override 加 `approval: never`,不匹配预设的组合仍为 `custom`。每个 Auto child 调用都使用既有 `parentSession`、创建 prompt 和经过核验的 human/直接父级消息独立审查。[Auto review 决策](../../.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md)定义 low/medium/high 语义;不增加 委派记录、receipt、Header 字段、descriptor 字段或 Session format。 +`SubagentRuntime.listDescendants(rootSessionId)` 以稳定前序递归调用同一目录读取函数,保留每个父级的事件顺序。外部条目是没有本地 Session 的叶节点。一次性和未知模式条目仍是遍历节点;未知模式产生 `unsupported` 诊断。无法读取的子级目录产生 `corrupt` 或 `unavailable`,只停止该分支。根读取失败、服务或投影缺失、取消会使整个查询失败。每个可达目录只观察一次,并在下一次读取前释放;重复 id 和循环引用会被跳过。可达目录中不存在的 Session 不会被发现,包括普通 Session fork 及其下的子 agent。每行携带目录中的父级和相对根的深度: -- **委派深度**由持久 `SessionHeader.delegationDepth` 与可合并扩展的运行时字段 `AgentOptions.subagentDepth` 共同表示;缺失表示顶层深度为零,存在的较大值具有权威性。两个字段都归该 seam 所有——循环既不设置也不读取它们——因此进程内子 agent 会持久保存 parent 深度 + 1,冷恢复无法降低深度,而且每次 start 都会拒绝超出安全整数域、或高于已定义绝对 `request.maxDepth` 上限的派生深度。 -- **Fork 种子注入**使用 [`CreateAgentOptions.seed`](core.zh.md#creation-and-ownership)(一个 `SessionEvent[]` 前缀,经由 `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })` 传递,与 `ctx.agents.resume()` 使用的原语相同)。fork 后端传入父级日志的一段*平衡的已完成轮次前缀*——父级事件直到并包括其最后一个 `turn/end`——因此种子从 0 连续,且只包含平衡的轮次(进行中的、未平衡的轮次被排除在外)。 +```ts type-equiv +/** One catalog descendant with its direct parent and edge distance from the requested root. */ +type SubagentDescendantListEntry = SubagentListEntry & { + /** Parent whose catalog contains this child. */ + readonly parentId: SessionId + /** Edge distance from the requested root; direct children are `1`. */ + readonly depth: number +} +``` + + + -`SubagentCatalogEntry` 描述一条完整或未知模式的直接子级目录记录;`SubagentCatalogState` 是仅 host 使用的 projection state。`listChildren()` 拥有一次 live-preferred 父 Session observation,不打开子级日志。浏览器消费者通过共享 Session projection store 读取 `subagentCatalog`,并将成员关系与 Session 列表活动状态组合。`SubagentCatalogRow` 属于递归目录列表。[父目录决策](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 规定持久事实与读取语义。 @@ -495,7 +430,7 @@ Source: [`packages/subagent/tool-subagent/src/model-selection-settings.ts`](../. ### `ctx.subagents` — `SubagentRuntime` -Named provider registry with one-shot runs, durable discovery, and continuable-child operations. +Named provider registry with managed activations, durable discovery, and local child messaging. ```ts cordis-catalog /** @@ -506,15 +441,26 @@ Named provider registry with one-shot runs, durable discovery, and continuable-c resolveMaxDepth(configured?: number | 'provider-managed'): number | undefined /** - * Establish one durable continuable child and deliver its initial prompt. - * Resolves when the child's inbox accepts that prompt, without waiting for the - * turn to start or for the message to reach the Session log; any earlier - * failure rejects with no ids and rolls back the child entirely. - * @param spec - provider, delegation request, and caller cancellation. - * @returns the durable child id and the accepted prompt's message id. - * @throws when continuation services are unavailable or materialization fails. + * Start a local child under its reserved identity. + * @param spec - local task, reserved child id, and result recipient. + * @returns activation with its accepted initial message id. */ -async startContinuable(spec: ContinuableStartSpec): Promise +startActivation(spec: SubagentActivationSpec & { readonly childId: SessionId }): Promise + +/** + * Start a local or external child. + * @param spec - task, backend, and result recipient. + * @returns activation with a message id only for local children. + */ +startActivation(spec: SubagentActivationSpec): Promise + +/** + * Join progressing descendants without cancelling them. Idle descendants whose + * inboxes require a later wake stay resident and do not delay host completion. + * @param parent - the exact parent whose descendant work is observed. + * @returns whether work was joined; hosts recheck parent idle after true. + */ +async waitForChildren(parent: Agent): Promise /** * Steer one model-authored message to the sender's direct parent or direct @@ -533,13 +479,13 @@ async startContinuable(spec: ContinuableStartSpec): Promise async sendMessage( sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions, ): Promise /** - * Interrupt one live continuable child's current turn under a human parent + * Interrupt one live child's current execution under a human parent * address or an exact live ancestor Agent. Fire-and-return: the cancel * signal is issued before this returns, but the target may keep running * until it observes the signal. Unclaimed pending inbox work, the Activation, * and published descendants are preserved; claimed work is not requeued. - * Once the interrupted driver is idle, a waking send resumes the parked FIFO - * queue. An absent target — including a one-shot or unknown id — + * Once the interrupted Agent is idle, a waking send resumes the parked FIFO + * queue. External backends stop their single execution. An absent target * is an accepted no-op, as is a manager-less composition, which cannot own a * live Activation. * @param targetSessionId - the durable child session id to interrupt. @@ -550,28 +496,28 @@ async sendMessage( sender: Agent, targetId: SessionId, content: ContentBlock[], interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void /** - * Close continuable admission below exact live parent Agents, stop only their + * Close subagent admission below exact live parent Agents, stop only their * visible descendant Activations synchronously, then await admitted scoped * materializations and release those forests child-first. The scoped cutoff * lasts until each exact parent leaves the registry; unrelated parent trees * remain live. * @param parents - exact host-owned parent Agents entering teardown. - * @returns once every retained descendant Activation released its `AgentHandle`. + * @returns once every retained descendant activation released its execution handle. * @throws an aggregate error after all branches settle when any failed. */ -async drainContinuableDescendants(parents: readonly Agent[]): Promise +async drainDescendants(parents: readonly Agent[]): Promise /** - * Release selected resident continuable direct children of one exact live + * Release selected resident direct children of one exact live * parent. Other children of the same parent remain admitted and resident. * Absent targets and a manager-less composition are accepted no-ops. * @param parent - exact live direct parent authorizing the selected release. * @param childIds - durable direct-child ids to release when resident. - * @returns once every selected Activation released its `AgentHandle`. + * @returns once every selected activation released its execution handle. * @throws {SubagentError} `UNAUTHORIZED` when a resident target belongs to a * different parent or the supplied parent identity is stale. */ -async drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): Promise +async drainChildren(parent: Agent, childIds: readonly SessionId[]): Promise /** * Read the parent's durable direct-child catalog without loading or resuming an Agent. @@ -587,7 +533,8 @@ listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise ``` Types: [Agent](core.zh.md) · [ContentBlock](llm-streaming.zh.md) · [MessageId](llm-streaming.zh.md) · [SessionId](core.zh.md) diff --git a/docs/subsystems/system-prompt.i18n.yaml b/docs/subsystems/system-prompt.i18n.yaml index e11fef3eb7..9c893ee1a7 100644 --- a/docs/subsystems/system-prompt.i18n.yaml +++ b/docs/subsystems/system-prompt.i18n.yaml @@ -15,5 +15,5 @@ en: e31de7452a4f1186 zh: db697d4db7f88642 /system-prompt-assembly/dynamic-prompt-context: - en: 043740c2cf02b0aa - zh: 5f0bca1306975c72 + en: 2abb13dec8e6d041 + zh: 8cf434f360d8a519 diff --git a/docs/subsystems/system-prompt.md b/docs/subsystems/system-prompt.md index 898520e8fc..c529c150bb 100644 --- a/docs/subsystems/system-prompt.md +++ b/docs/subsystems/system-prompt.md @@ -82,8 +82,26 @@ interface PromptContext { readonly name: string /** Contexts are joined in ascending order. */ readonly order: number - /** Static text or a provider evaluated for each assembly. Empty text contributes nothing. */ + /** Static text or a provider evaluated at assembly and admission refresh. Empty text contributes nothing. */ readonly text: string | ((context: AssembleContext) => string) + /** Whether to interpolate prompt variables. Defaults to true; false preserves literal text. */ + readonly interpolate?: boolean + /** Keep this operational context when optional runtime context is disabled. */ + readonly required?: boolean +} +``` + +`required: true` keeps an operational contribution when optional runtime context is suppressed. `interpolate: false` preserves literal text, including variable-like directory names. `AssembledContext` carries the resolved text and that interpolation choice into rendering. + +```ts type-equiv +/** One resolved dynamic context contribution. */ +interface AssembledContext { + /** The contributing context's unique name. */ + name: string + /** The resolved text before variable interpolation. */ + text: string + /** Whether to interpolate prompt variables. Defaults to true; false preserves literal text. */ + interpolate?: boolean } ``` @@ -135,7 +153,7 @@ getContextOrder(name: PromptContextOrderName): number context(context: PromptContext): () => void /** - * Suppress every dynamic runtime-context contribution in the calling + * Suppress optional dynamic runtime-context contributions in the calling * context's scope without changing the services that own or enforce those * facts. Multiple suppressors remain independently disposable. * @returns the exact Cordis effect disposer. @@ -161,6 +179,18 @@ tools(provider: (context: AssembleContext) => ToolProviderResult): () => void */ variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void +/** + * Refresh accepted registered runtime facts for request admission. Contexts + * added only by the assembly waterfall retain their accepted values. Current + * suppression removes optional contexts, and missing required registrations + * are restored in registry order. Sections, tools, and interpolation variables + * retain the accepted assembly; their providers and waterfall do not rerun. + * @param assembly - accepted assembly for this scope and step. + * @param context - the same scope and current plugin-defined assembly fields. + * @returns the accepted assembly with current runtime-context provider text. + */ +refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly + /** * Assemble global and scoped providers, detach tool parameters, apply * canonical ordering, then run the assembly waterfall. Scoped sections and diff --git a/docs/subsystems/system-prompt.zh.md b/docs/subsystems/system-prompt.zh.md index 82e557a309..a805c1d771 100644 --- a/docs/subsystems/system-prompt.zh.md +++ b/docs/subsystems/system-prompt.zh.md @@ -82,8 +82,26 @@ interface PromptContext { readonly name: string /** Contexts are joined in ascending order. */ readonly order: number - /** Static text or a provider evaluated for each assembly. Empty text contributes nothing. */ + /** Static text or a provider evaluated at assembly and admission refresh. Empty text contributes nothing. */ readonly text: string | ((context: AssembleContext) => string) + /** Whether to interpolate prompt variables. Defaults to true; false preserves literal text. */ + readonly interpolate?: boolean + /** Keep this operational context when optional runtime context is disabled. */ + readonly required?: boolean +} +``` + +`required: true` 在可选运行时上下文被抑制时保留操作所需的贡献。`interpolate: false` 原样保留文本,包括看起来像变量的目录名。`AssembledContext` 将已求值文本及其插值选择传给渲染过程。 + +```ts type-equiv +/** One resolved dynamic context contribution. */ +interface AssembledContext { + /** The contributing context's unique name. */ + name: string + /** The resolved text before variable interpolation. */ + text: string + /** Whether to interpolate prompt variables. Defaults to true; false preserves literal text. */ + interpolate?: boolean } ``` @@ -135,7 +153,7 @@ getContextOrder(name: PromptContextOrderName): number context(context: PromptContext): () => void /** - * Suppress every dynamic runtime-context contribution in the calling + * Suppress optional dynamic runtime-context contributions in the calling * context's scope without changing the services that own or enforce those * facts. Multiple suppressors remain independently disposable. * @returns the exact Cordis effect disposer. @@ -161,6 +179,18 @@ tools(provider: (context: AssembleContext) => ToolProviderResult): () => void */ variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void +/** + * Refresh accepted registered runtime facts for request admission. Contexts + * added only by the assembly waterfall retain their accepted values. Current + * suppression removes optional contexts, and missing required registrations + * are restored in registry order. Sections, tools, and interpolation variables + * retain the accepted assembly; their providers and waterfall do not rerun. + * @param assembly - accepted assembly for this scope and step. + * @param context - the same scope and current plugin-defined assembly fields. + * @returns the accepted assembly with current runtime-context provider text. + */ +refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly + /** * Assemble global and scoped providers, detach tool parameters, apply * canonical ordering, then run the assembly waterfall. Scoped sections and diff --git a/docs/subsystems/tools.i18n.yaml b/docs/subsystems/tools.i18n.yaml index 83f05cc0f8..32a5981c35 100644 --- a/docs/subsystems/tools.i18n.yaml +++ b/docs/subsystems/tools.i18n.yaml @@ -15,8 +15,8 @@ en: 8c3fc4ca26f7e314 zh: 2e27263cb25243b9 /tools/execution-extensible-waterfalls-plus-monotonic-policy: - en: 1ff922c9803d5456 - zh: 983d71cbbff7b702 + en: dcac8fa88ef3e719 + zh: aae2f4ce8852d8d8 /tools/the-enforced-raw-json-schema-subset: en: 28e354bfa2f9cebb zh: be1f75abccf8afdf diff --git a/docs/subsystems/tools.md b/docs/subsystems/tools.md index 4d03629399..585ce1582a 100644 --- a/docs/subsystems/tools.md +++ b/docs/subsystems/tools.md @@ -17,7 +17,7 @@ interface ToolOutputDefinition { readonly schema: JsonSchemaNode /** Pure projection from validated arguments and value to Native/model content. */ render(args: unknown, value: JsonValue): ContentBlock[] - /** Pure replayable presentation projection, computed only for top-level calls. */ + /** Pure replayable presentation projection for native and nested calls. */ presentationMeta?(args: unknown, value: JsonValue): JsonValue } ``` @@ -389,9 +389,9 @@ interface ToolErrorInfo { } ``` -The result carries only the outcome. Call identity remains on the immutable `ToolExecution` that accompanies it through every hook and on the durable `tool/call` / `tool/result` session events, so wrappers cannot create a second, disagreeing identity. The canonical `value` is execution-local: the loop persists only `content`, `error`, and `meta`, while `tool/ptc-dispatch` stores the sub-call's rendered `content`, `isError`, and optional structured `error`. Replay reproduces presentation but cannot reconstruct canonical intermediate values. Optional `ToolErrorInfo.reason` retains raw user-facing detail without adding it to model-facing content. +The result carries only the outcome. Call identity remains on the immutable `ToolExecution` that accompanies it through every hook and on the durable `tool/call` / `tool/result` session events, so wrappers cannot create a second, disagreeing identity. The canonical `value` is execution-local: the loop persists only `content`, `error`, and `meta`, while `tool/ptc-dispatch` stores the sub-call's rendered `content`, `isError`, optional structured `error`, and presentation `meta`. Replay reproduces presentation but cannot reconstruct canonical intermediate values. Optional `ToolErrorInfo.reason` retains raw user-facing detail without adding it to model-facing content. -On success the registry snapshots and validates the body value, freezes it, and invokes the pure renderer plus the optional top-level-call metadata projector. It separately materializes the durable presentation fields immediately before `tools/result`; an invalid value, renderer/projector failure, or non-JSON presentation becomes a JSON-safe `isError`. The final live observer therefore sees the exact execution-local value beside fields safe for the later durable append. +On success the registry snapshots and validates the body value, freezes it, and invokes the pure renderer plus the optional presentation metadata projector for native and nested calls. It separately materializes the durable presentation fields immediately before `tools/result`; an invalid value, renderer/projector failure, or non-JSON presentation becomes a JSON-safe `isError`. The final live observer therefore sees the exact execution-local value beside fields safe for the later durable append. Before final content, the registry materializes the candidate result; a failure in content, structured error, additional context, or presentation metadata becomes a JSON-safe `isError` result that still reaches `finalizeContent`. The registry invokes that callback exactly once, then materializes and freezes the accepted result immediately before `tools/result`, so the observed live outcome is safe for the later durable `tool/result` append. diff --git a/docs/subsystems/tools.zh.md b/docs/subsystems/tools.zh.md index 9aa6bf9656..4ee27fcb4d 100644 --- a/docs/subsystems/tools.zh.md +++ b/docs/subsystems/tools.zh.md @@ -17,7 +17,7 @@ interface ToolOutputDefinition { readonly schema: JsonSchemaNode /** Pure projection from validated arguments and value to Native/model content. */ render(args: unknown, value: JsonValue): ContentBlock[] - /** Pure replayable presentation projection, computed only for top-level calls. */ + /** Pure replayable presentation projection for native and nested calls. */ presentationMeta?(args: unknown, value: JsonValue): JsonValue } ``` @@ -389,9 +389,9 @@ interface ToolErrorInfo { } ``` -结果仅承载产出。调用身份保留在不可变的 `ToolExecution` 上,后者伴随结果经过每个钩子,并出现在持久化的 `tool/call` / `tool/result` 会话事件上,因此包装层无法创建第二个相互矛盾的身份。规范的 `value` 仅存在于执行期间:循环只持久化 `content`、`error` 和 `meta`,`tool/ptc-dispatch` 则存储子调用渲染后的 `content`、`isError` 与可选结构化 `error`。回放可以重现展示,却无法重建规范的中间值。可选的 `ToolErrorInfo.reason` 保留面向用户的原始详情,不将其加入模型可见内容。 +结果仅承载产出。调用身份保留在不可变的 `ToolExecution` 上,后者伴随结果经过每个钩子,并出现在持久化的 `tool/call` / `tool/result` 会话事件上,因此包装层无法创建第二个相互矛盾的身份。规范的 `value` 仅存在于执行期间:循环只持久化 `content`、`error` 和 `meta`,`tool/ptc-dispatch` 则存储子调用渲染后的 `content`、`isError`、可选结构化 `error` 和展示 `meta`。回放可以重现展示,却无法重建规范的中间值。可选的 `ToolErrorInfo.reason` 保留面向用户的原始详情,不将其加入模型可见内容。 -成功时,注册表会快照并校验函数体返回值,将其冻结,然后调用纯渲染器;对于直接的外层调用,还会调用可选的元数据投影器。注册表会在 `tools/result` 之前另行物化持久展示字段;无效值、渲染器/投影器失败或非 JSON 展示都会转为 JSON 安全的 `isError`。因此,最终实时观察者能看到精确的执行期值,以及可安全用于后续持久追加的字段。 +成功时,注册表会快照并校验函数体返回值,将其冻结,然后调用纯渲染器,并为原生与嵌套调用执行可选的展示元数据投影器。注册表会在 `tools/result` 之前另行物化持久展示字段;无效值、渲染器/投影器失败或非 JSON 展示都会转为 JSON 安全的 `isError`。因此,最终实时观察者能看到精确的执行期值,以及可安全用于后续持久追加的字段。 在得到最终内容之前,注册表会物化候选结果;若内容、结构化错误、附加上下文或展示元数据无法物化,则会转为仍可到达 `finalizeContent` 的 JSON 安全 `isError` 结果。注册表恰好调用该回调一次,随后在 `tools/result` 之前立即物化并冻结已接受的结果,因此实时观察到的产出可安全用于后续持久化的 `tool/result` 追加。 diff --git a/docs/subsystems/translation.i18n.yaml b/docs/subsystems/translation.i18n.yaml new file mode 100644 index 0000000000..a6f25d640c --- /dev/null +++ b/docs/subsystems/translation.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for translation.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/subsystems/translation.md +/text-translation: + en: fe10b0616f7bc164 + zh: 84a392a62efdaab9 +/text-translation/requests-and-results: + en: 485abf6107c73e68 + zh: 78a274d488274035 +/text-translation/failures-and-data-handling: + en: cf92a1920650888c + zh: 3e8d3aa244cc8d46 +/text-translation/persistent-translations: + en: 40d175fbe66fd4cd + zh: bff18511a8d386e9 +/text-translation/reasoning-display: + en: 209dbbbf3000e087 + zh: f8e661359d0f3cb5 diff --git a/docs/subsystems/translation.md b/docs/subsystems/translation.md new file mode 100644 index 0000000000..c5de82676f --- /dev/null +++ b/docs/subsystems/translation.md @@ -0,0 +1,103 @@ +# Text translation + +English | [中文](translation.zh.md) + +The experimental [translator](../../packages/experimental/translator/README.md) translates text through anonymous browser endpoints or explicitly selected paid native Flash routes. Consumers own display, scheduling, and Session activation; Session-bound requests retain reusable results through an active Session writer. + +## Requests and results + +`TranslationProvider` selects `bing`, `google`, `deepseek-account` or `deepseek-official`. `TranslationRequest` supplies `text`, `targetLanguage`, and optional `sourceLanguage`, `provider` and branded `sessionId`. `resolve()` captures explicit routing in `TranslationSpec`: the configured provider defaults to Bing and omitted source language uses automatic detection. Common Chinese locale tags map to the selected provider's language codes. `translate()` returns plain text from the resolved provider. + +The service exposes its configured `maxTextChars`, measured in UTF-16 code units. Consumers split longer inputs before requesting translation. Each call bounds the response bytes and includes the response-body read in its deadline. Caller cancellation and service disposal abort accepted requests; disposal waits for them to settle. + +## Failures and data handling + +`TranslationError` carries a `TranslationErrorCode` distinguishing text limits, HTTP failures, invalid responses, response limits, owned deadlines, and request failures. `TRANSLATION_SESSION_REQUIRED` rejects a missing persistence backend or Session; `TRANSLATION_SESSION_INACTIVE` rejects an uncached inactive Session before provider dispatch; `TRANSLATION_STORAGE_ERROR` rejects invalid saved records or failed durability. Provider diagnostics omit submitted text and response bodies. Failed calls do not select another provider. Responses are validated as external JSON and redirects are rejected. + +`TRANSLATION_UNAVAILABLE` rejects a missing active built-in owner or registered route, unconfigured credentials, absent exact `deepseek-flash` catalog membership, an owner/model-list/official credential-reference change before dispatch, provider withdrawal, or a prepared call whose reasoning effort is not `off`. A native `finish` with reason `error` maps to `TRANSLATION_REQUEST_FAILED`; a missing or other non-`stop` finish maps to `TRANSLATION_INVALID_RESPONSE`. + +Google receives source text in a form POST body; Bing receives it in a JSON POST body. Both are remote services. Their anonymous endpoints provide no supported developer availability or quota guarantee. Regional connectivity must be verified on the user's network. + +## Persistent translations + +Session-bound calls use `plugin:translator/request` and `plugin:translator/result` for every provider. Requests retain exact source, language tags, selected provider and recipe identity; a successful result references its request sequence. Optional request metadata holds provider-specific details without changing these record types. The translator checkpoints requests before external dispatch and completed results before returning them. Read-only cache lookup precedes provider work and needs no live Session; failures and incomplete attempts do not supply reusable text. New records use the active Session's existing writer. The translator opens no write handle and does not activate a Session. Translation records preserve the original events and model input. Anonymous calls without a Session stay stateless; paid routes require a Session. Experimental record retention across future format migrations is best-effort. + +Paid cache misses require an active built-in Account or Official owner, configured credentials and exact `deepseek-flash` catalog membership; names and version labels are irrelevant, and pi-ai is excluded. Each miss prepares a separate text-only query with thinking off. Its exact config, system and unique user fragment occupy optional `metadata.modelRequest` in the same request record. Saved results are read before this admission, so missing credentials do not invalidate existing translations. Native request extensions receive no main Session identity and cannot upload its full log. + +## Reasoning display + +The [optional bundle](../../packages/experimental/cot-translation-bundle/README.md) composes the translator with the [GUI consumer](../../packages/experimental/client-ui-cot-translation/README.md). Its `cotTranslation` Remote serves authenticated browser calls. When a Session is inactive and has no saved result, the Host joins normal Session controller activation and retries once. Activation retains normal preset hooks, resume markers, and interrupted-turn repair; cancellation stops the caller's wait and prevents a later translation call without cancelling shared activation. Bundle settings select a translation service and target language; `auto` follows the active interface language. + +`CotTranslationPreferences` contains the accepted provider and target language. `CotTranslationSnapshot` pairs these preferences with the translator's current `maxTextChars` and eligible provider IDs. The browser reads this Host snapshot before registering translation controls, including when local settings storage is unavailable. Accepted settings updates and reconnects refresh both values; a changed request limit cancels and splits the expanded text again. + +The session-scoped `conversation.chat.reasoning-body` chain receives the original `text` and `running` flag. Chat renders original Markdown when no contribution claims it. While enabled, the GUI translates completed paragraphs and request-sized prefixes of long streaming paragraphs only after expansion, and restores completed results from the current Session. Its local cache only optimizes that disclosure. The unfinished tail remains original. Original remains accessible. Closing the disclosure, switching provider or language, and unloading cancel outstanding calls and exclude late results. Provider failure preserves the original and requires an explicit retry; translated text never replaces original Session events or model input. + + + + + +## Cordis API + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.cotTranslation` — `CotTranslationController` + +Optional reasoning translation delegates Session-bound storage to the translator. + +```ts cordis-catalog +/** + * Read accepted translation preferences and the current request limit without sending text. + * @param signal - browser query cancellation or Remote contribution withdrawal. + * @returns authoritative preferences, eligible routes, and maximum UTF-16 text length per request. + */ +@Remote async limits(signal: AbortSignal): Promise + +/** + * Translate one displayed fragment through the reader's selected provider. + * @param request - original text and Session identity supplied by the Client; provider and explicit + * target language match accepted preferences, while auto uses the browser locale. + * @param signal - browser cancellation or Remote contribution withdrawal. + * @returns translated text; an uncached inactive Session joins ordinary GUI activation before retry. + * Failures omit the source and provider response. Cancellation stops this caller's wait, not shared activation. + */ +@Remote async translate(request: TranslationRequest, signal: AbortSignal): Promise +``` + +Source: [`packages/experimental/client-ui-cot-translation/src/index.ts`](../../packages/experimental/client-ui-cot-translation/src/index.ts) + + + +### `ctx.translator` — `Translator` + +One Host service with explicit routing, cancellation and quiescent unload. + +```ts cordis-catalog +/** + * Inspect eligible native routes without inference; saved results remain readable for unavailable routes. + * @param signal - optional caller cancellation, combined with service disposal. + * @returns anonymous choices followed by eligible explicitly selected paid routes. + */ +async availableProviders(signal?: AbortSignal): Promise + +/** + * Resolve provider and source-language defaults without sending text. + * @param request - consumer text, destination and optional routing choices. + * @returns a complete specification; exceeding `maxTextChars` throws `TRANSLATION_TEXT_LIMIT`. + */ +resolve(request: TranslationRequest): TranslationSpec + +/** + * Translate one resolved specification; the selected provider receives its text. + * @param spec - complete routing and language choices from `resolve()`. + * @param signal - optional caller cancellation, combined with service disposal. + * @returns translated plain text, durably retained before return when a Session is supplied. + * An uncached supplied Session must be active; otherwise rejects with `TRANSLATION_SESSION_INACTIVE` before dispatch. + * Rejects provider/storage/limit failures and preserves cancellation reasons. + */ +async translate(spec: TranslationSpec, signal?: AbortSignal): Promise +``` + +Source: [`packages/experimental/translator/src/index.ts`](../../packages/experimental/translator/src/index.ts) + diff --git a/docs/subsystems/translation.zh.md b/docs/subsystems/translation.zh.md new file mode 100644 index 0000000000..e3700edcbd --- /dev/null +++ b/docs/subsystems/translation.zh.md @@ -0,0 +1,103 @@ +# 文本翻译 + +[English](translation.md) | 中文 + +实验性 [translator](../../packages/experimental/translator/README.zh.md) 通过免登录浏览器端点或显式选择的付费原生 Flash 路由翻译文本。消费者负责展示、调度及 Session 激活;绑定 Session 的请求通过已激活 Session 的写入器保留可复用结果。 + +## 请求与结果 + +`TranslationProvider` 选择 `bing`、`google`、`deepseek-account` 或 `deepseek-official`。`TranslationRequest` 提供 `text`、`targetLanguage` 及可选的 `sourceLanguage`、`provider` 与带品牌类型的 `sessionId`。`resolve()` 将显式路由写入 `TranslationSpec`:配置的默认 Provider 为 Bing,省略源语言时自动检测。常见中文 locale 标签映射为所选 Provider 的语言代码。`translate()` 返回所选 Provider 翻译后的纯文本。 + +服务公开配置的 `maxTextChars`,单位为 UTF-16 code unit。消费者先拆分超长输入,再请求翻译。每次调用限制响应字节数,并将响应体读取计入超时。调用者取消和服务卸载都会中止已接收的请求;卸载等待这些请求完成。 + +## 失败与数据处理 + +`TranslationError` 的 `TranslationErrorCode` 区分文本超限、HTTP 失败、无效响应、响应超限、服务自身超时与请求失败。`TRANSLATION_SESSION_REQUIRED` 拒绝缺失的持久化后端或 Session,`TRANSLATION_SESSION_INACTIVE` 在调用 Provider 前拒绝未激活且缓存未命中的 Session,`TRANSLATION_STORAGE_ERROR` 拒绝无效保存记录或持久化失败。Provider 错误信息省略提交的文本与响应体。失败时不选择其他 Provider。响应作为外部 JSON 校验,重定向被拒绝。 + +`TRANSLATION_UNAVAILABLE` 拒绝缺失的活跃内置所有者或已注册路由、未配置的凭据、目录中不存在精确的 `deepseek-flash`、派发前所有者/模型列表/官方凭据引用变化、提供方卸载,以及准备后的调用推理强度不是 `off` 的情况。原生 `finish` 的原因为 `error` 时映射为 `TRANSLATION_REQUEST_FAILED`;缺失 finish 或其他非 `stop` 原因映射为 `TRANSLATION_INVALID_RESPONSE`。 + +Google 在 form POST 请求体中接收原文,Bing 在 JSON POST 请求体中接收原文。两者都是远端服务。免登录端点不保证开发者 API 的可用性或免费额度。区域连通性须在用户网络上验证。 + +## 持久化翻译 + +绑定 Session 的调用对所有 Provider 使用 `plugin:translator/request` 与 `plugin:translator/result`。请求保留精确原文、语言标签、所选 Provider 和翻译规则标识,成功结果引用请求序号。可选请求 metadata 保存 Provider 专有细节,不改变记录类型。translator 在外部派发前完成请求检查点,在返回前完成结果检查点。只读缓存查找先于 Provider 调用,且不要求 Session 已激活;失败或未完成尝试不提供可复用译文。新记录使用已激活 Session 的现有写入器。translator 不打开写句柄,也不激活 Session。翻译记录保留原始事件与模型输入。不指定 Session 的匿名调用保持无状态;付费路由要求 Session。未来格式迁移对实验性记录的保留为尽力而为。 + +付费缓存未命中时要求内置账号或官方 API 所有者处于活动状态、凭据已配置,且目录包含 ID 恰好为 `deepseek-flash` 的模型;名称和版本标签不参与匹配,pi-ai 路由被排除。每次未命中准备一次关闭思考的独立纯文本查询。精确配置、系统指令和唯一原文片段保存在同一请求记录的可选 `metadata.modelRequest` 中。读取保存结果先于此项准入,因此凭据缺失不会使既有译文失效。原生请求扩展不接收主 Session 身份,不能上传其完整日志。 + +## 思考内容展示 + +[可选 Bundle](../../packages/experimental/cot-translation-bundle/README.zh.md) 将 translator 与 [GUI 消费者](../../packages/experimental/client-ui-cot-translation/README.zh.md) 组合。其 `cotTranslation` Remote 为经过身份验证的浏览器调用提供服务。Session 未激活且没有已保存结果时,Host 等待 Session controller 的正常激活,再重试一次。激活保留正常的 preset 钩子、恢复标记和中断轮次修复;取消会结束调用者的等待并阻止随后调用翻译,不取消共享激活。Bundle 设置选择翻译服务及目标语言;`auto` 跟随当前界面语言。 + +`CotTranslationPreferences` 包含已接受的翻译服务和目标语言。`CotTranslationSnapshot` 将这些偏好与 translator 当前的 `maxTextChars` 及可用服务 ID 一起返回。浏览器在注册翻译控件前读取此 Host 快照,本地设置存储不可用时也如此。接受的设置更新及重新连接会刷新这两项值;请求长度限制变化时,展开内容的请求被取消并重新分片。 + +Session 作用域的 `conversation.chat.reasoning-body` chain 接收原始 `text` 和 `running` 标志。没有 contribution 接管时,Chat 渲染原始 Markdown。启用后,GUI 只在展开后翻译完整段落和流式长段落中达到请求长度的前缀,并从当前 Session 恢复已完成结果;本地缓存只优化当前展开区域。未完成的末尾片段保留原文。原文始终可访问。关闭展开内容、切换 Provider 或语言、卸载插件都会取消未完成调用并忽略迟到的结果。Provider 失败时保留原文,必须显式重试;译文从不替换原始 Session 事件或模型输入。 + + + + + +## Cordis API + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.cotTranslation` — `CotTranslationController` + +Optional reasoning translation delegates Session-bound storage to the translator. + +```ts cordis-catalog +/** + * Read accepted translation preferences and the current request limit without sending text. + * @param signal - browser query cancellation or Remote contribution withdrawal. + * @returns authoritative preferences, eligible routes, and maximum UTF-16 text length per request. + */ +@Remote async limits(signal: AbortSignal): Promise + +/** + * Translate one displayed fragment through the reader's selected provider. + * @param request - original text and Session identity supplied by the Client; provider and explicit + * target language match accepted preferences, while auto uses the browser locale. + * @param signal - browser cancellation or Remote contribution withdrawal. + * @returns translated text; an uncached inactive Session joins ordinary GUI activation before retry. + * Failures omit the source and provider response. Cancellation stops this caller's wait, not shared activation. + */ +@Remote async translate(request: TranslationRequest, signal: AbortSignal): Promise +``` + +Source: [`packages/experimental/client-ui-cot-translation/src/index.ts`](../../packages/experimental/client-ui-cot-translation/src/index.ts) + + + +### `ctx.translator` — `Translator` + +One Host service with explicit routing, cancellation and quiescent unload. + +```ts cordis-catalog +/** + * Inspect eligible native routes without inference; saved results remain readable for unavailable routes. + * @param signal - optional caller cancellation, combined with service disposal. + * @returns anonymous choices followed by eligible explicitly selected paid routes. + */ +async availableProviders(signal?: AbortSignal): Promise + +/** + * Resolve provider and source-language defaults without sending text. + * @param request - consumer text, destination and optional routing choices. + * @returns a complete specification; exceeding `maxTextChars` throws `TRANSLATION_TEXT_LIMIT`. + */ +resolve(request: TranslationRequest): TranslationSpec + +/** + * Translate one resolved specification; the selected provider receives its text. + * @param spec - complete routing and language choices from `resolve()`. + * @param signal - optional caller cancellation, combined with service disposal. + * @returns translated plain text, durably retained before return when a Session is supplied. + * An uncached supplied Session must be active; otherwise rejects with `TRANSLATION_SESSION_INACTIVE` before dispatch. + * Rejects provider/storage/limit failures and preserves cancellation reasons. + */ +async translate(spec: TranslationSpec, signal?: AbortSignal): Promise +``` + +Source: [`packages/experimental/translator/src/index.ts`](../../packages/experimental/translator/src/index.ts) + diff --git a/docs/subsystems/webhook.md b/docs/subsystems/webhook.md index d84853fc82..8bc71ab121 100644 --- a/docs/subsystems/webhook.md +++ b/docs/subsystems/webhook.md @@ -66,5 +66,5 @@ register(rule: WebhookRule): () => Promise dispatch(delivery: VerifiedWebhookDelivery): void ``` -Source: [`packages/webhook/webhook/src/index.ts`](../../packages/webhook/webhook/src/index.ts) +Source: [`packages/experimental/webhook/src/index.ts`](../../packages/experimental/webhook/src/index.ts) diff --git a/docs/subsystems/webhook.zh.md b/docs/subsystems/webhook.zh.md index 9a76c81cb4..4d0e108f72 100644 --- a/docs/subsystems/webhook.zh.md +++ b/docs/subsystems/webhook.zh.md @@ -66,5 +66,5 @@ register(rule: WebhookRule): () => Promise dispatch(delivery: VerifiedWebhookDelivery): void ``` -Source: [`packages/webhook/webhook/src/index.ts`](../../packages/webhook/webhook/src/index.ts) +Source: [`packages/experimental/webhook/src/index.ts`](../../packages/experimental/webhook/src/index.ts) diff --git a/docs/subsystems/workflow.i18n.yaml b/docs/subsystems/workflow.i18n.yaml index 6a7a76733e..f3b906de0d 100644 --- a/docs/subsystems/workflow.i18n.yaml +++ b/docs/subsystems/workflow.i18n.yaml @@ -3,8 +3,8 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/workflow.md /workflow: - en: eaf02eb7b85ec696 - zh: 648807dce7860e2d + en: 4b264eb88e5cedfb + zh: 78585763a79cd1c9 /workflow/the-start-request: en: 6c8755833293a36c zh: a0f061387fe0d3a6 diff --git a/docs/subsystems/workflow.md b/docs/subsystems/workflow.md index 588c6430f3..5b51dc1ff1 100644 --- a/docs/subsystems/workflow.md +++ b/docs/subsystems/workflow.md @@ -4,7 +4,7 @@ English | [中文](workflow.zh.md) The workflow seam lets an agent run a model-written orchestration SCRIPT that starts subagents. Like [subagent](subagent.md) it is **one optional capability**, not part of the agent loop, so its types and operations live here rather than in [core.md](core.md). Like bash, it permits ONE engine implementation per context to provide `ctx.workflowEngine`; there is no named-provider registry (a second engine replaces the first through plugin configuration rather than running beside it). -Service Definition: [dsh-workflow](../../packages/workflow/workflow) (`ctx.workflowEngine` and the vocabulary below). [dsh-workflow-ptc](../../packages/workflow/workflow-ptc) executes the VM and helpers through the shared Node PTC process runtime under the calling Session's file policy. The consumers are [dsh-tool-workflow](../../packages/workflow/tool-workflow) and the opt-in [dsh-tool-ralph](../../packages/workflow/tool-ralph). [Workflow sandbox reuse](../../.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.md) owns execution choices; the [dynamic-workflows reference](../../packages/workflow/workflow/README.md) owns script semantics. +Service Definition: [dsh-workflow](../../packages/workflow/workflow) (`ctx.workflowEngine` and the vocabulary below). [dsh-workflow-ptc](../../packages/workflow/workflow-ptc) executes the VM and helpers through the shared Node PTC process runtime under the calling Session's file policy. The consumers are [dsh-tool-workflow](../../packages/workflow/tool-workflow) and the opt-in [dsh-tool-ralph](../../packages/experimental/tool-ralph). [Workflow sandbox reuse](../../.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.md) owns execution choices; the [dynamic-workflows reference](../../packages/workflow/workflow/README.md) owns script semantics. Sources: browser-safe vocabulary in [`packages/workflow/workflow/src/types.ts`](../../packages/workflow/workflow/src/types.ts), Host request and live-run handles in [`runtime-types.ts`](../../packages/workflow/workflow/src/runtime-types.ts). diff --git a/docs/subsystems/workflow.zh.md b/docs/subsystems/workflow.zh.md index 1e04710326..1514fa31d6 100644 --- a/docs/subsystems/workflow.zh.md +++ b/docs/subsystems/workflow.zh.md @@ -4,7 +4,7 @@ 工作流 seam 允许 agent(智能体)运行由模型编写、会启动 subagent 的编排脚本。与 [subagent](subagent.zh.md) 一样,它是**一项可选能力**,不属于 agent loop,因此其类型和操作记录在此处,而非 [core.md](core.zh.md)。与 bash 一样,每个上下文只允许一个引擎实现提供 `ctx.workflowEngine`;没有命名提供方注册表(第二个引擎通过插件配置替换第一个,而不与它同时运行)。 -Service Definition:[dsh-workflow](../../packages/workflow/workflow)(`ctx.workflowEngine` 和下文词汇)。[dsh-workflow-ptc](../../packages/workflow/workflow-ptc)通过共享 Node PTC 进程运行时按调用 Session 的文件策略执行 VM 与辅助函数。消费方为 [dsh-tool-workflow](../../packages/workflow/tool-workflow) 和需显式启用的 [dsh-tool-ralph](../../packages/workflow/tool-ralph)。[工作流沙箱复用](../../.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.zh.md)负责执行选择;[动态工作流参考](../../packages/workflow/workflow/README.zh.md)负责脚本语义。 +Service Definition:[dsh-workflow](../../packages/workflow/workflow)(`ctx.workflowEngine` 和下文词汇)。[dsh-workflow-ptc](../../packages/workflow/workflow-ptc)通过共享 Node PTC 进程运行时按调用 Session 的文件策略执行 VM 与辅助函数。消费方为 [dsh-tool-workflow](../../packages/workflow/tool-workflow) 和需显式启用的 [dsh-tool-ralph](../../packages/experimental/tool-ralph)。[工作流沙箱复用](../../.agents/notes/implemented/architecture/2026-09-13-workflow-ptc-sandbox-reuse.zh.md)负责执行选择;[动态工作流参考](../../packages/workflow/workflow/README.zh.md)负责脚本语义。 源码:浏览器安全词汇位于 [`packages/workflow/workflow/src/types.ts`](../../packages/workflow/workflow/src/types.ts),Host 请求与活跃运行句柄位于 [`runtime-types.ts`](../../packages/workflow/workflow/src/runtime-types.ts)。 diff --git a/docs/subsystems/working-directory.i18n.yaml b/docs/subsystems/working-directory.i18n.yaml new file mode 100644 index 0000000000..de4ffb228e --- /dev/null +++ b/docs/subsystems/working-directory.i18n.yaml @@ -0,0 +1,13 @@ +# Bilingual-pair consistency record for working-directory.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/subsystems/working-directory.md +/working-directory: + en: 0882d8b4c09c2890 + zh: a75af7ce19f661fa +/working-directory/directory-selection: + en: b67e268cc9422d31 + zh: 4c7243c87aadcd7a +/working-directory/model-context: + en: 9789081854cbdd35 + zh: 1f6712c4e5b9c47e diff --git a/docs/subsystems/working-directory.md b/docs/subsystems/working-directory.md new file mode 100644 index 0000000000..4e9ae74c57 --- /dev/null +++ b/docs/subsystems/working-directory.md @@ -0,0 +1,64 @@ +# Working Directory + +English | [中文](working-directory.zh.md) + +A Session's active directory is owned by [`dsh-working-directory`](../../packages/session/working-directory/README.md). The immutable header identifies the original project; `working-directory/change` records the effective directory used by later directory-based operations. The generated [persistence catalog](../persistence-catalog.md) owns the event declaration. + +## Directory selection + +`ctx.workingDirectory.get(session)` reads the logged value. `ensure(agent)` validates it and restores the original project when the directory disappeared. `set(agent, path)` validates and records a requested change. Sessions without a header directory use the configured runtime fallback; an unavailable fallback produces an error. + +Changes are local to one Session. Relative filesystem operations, newly started processes, instruction discovery, skills, and file completion use the active directory. Existing terminals and persistent shells retain their own directories. Sandbox writable roots and project membership remain anchored to the original project. + +## Model context + +The initial value and subsequent current values reach the model as required user-context snapshots, even when optional runtime context is disabled. Each committed change attempts to queue a notice through the ordinary Agent inbox; cancellation or disposal can discard notices before request admission. The directory event survives, and the next admitted request receives the current value. Directory context never enters the system prompt. [`working_directory`](../../packages/session/tool-working-directory/README.md) exposes one optional `cd` argument. + + + + + +## Cordis API + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.workingDirectory` — `WorkingDirectoryService` + +One owner for each Session's effective directory and its model-visible changes. + +```ts cordis-catalog +/** + * Read the committed directory without filesystem I/O. + * @param session - Session whose directory is requested. + * @returns its effective absolute directory. + */ +get(session: Session): string + +/** + * Validate the current directory and restore the original project when it disappeared. + * @param agent - live or unpublished Agent owning the Session. + * @param signal - cancellation for filesystem inspection. + * @returns the existing directory; recovery is committed before fulfillment. + * A notice failure is warned without reverting the committed state. + * @throws when the original project is also unavailable. + */ +ensure(agent: Agent, signal?: AbortSignal): Promise + +/** + * Change one Session's directory without changing existing processes or permissions. + * @param agent - live or unpublished Agent owning the Session. + * @param path - absolute path or a path relative to its current directory. + * @param signal - cancellation before the durable change. + * @returns the canonical absolute directory, committed before fulfillment. + * A notice failure is warned; the next request still receives the committed directory. + * @throws when the requested path is not an existing directory. + */ +set(agent: Agent, path: string, signal?: AbortSignal): Promise +``` + +Types: [Agent](core.md) · [Session](session.md) + +Source: [`packages/session/working-directory/src/index.ts`](../../packages/session/working-directory/src/index.ts) + diff --git a/docs/subsystems/working-directory.zh.md b/docs/subsystems/working-directory.zh.md new file mode 100644 index 0000000000..bb239ddc43 --- /dev/null +++ b/docs/subsystems/working-directory.zh.md @@ -0,0 +1,64 @@ +# 工作目录 + +[English](working-directory.md) | 中文 + +Session 的当前目录由 [`dsh-working-directory`](../../packages/session/working-directory/README.zh.md) 拥有。不可变 header 标识原始项目;`working-directory/change` 记录后续目录相关操作使用的有效目录。生成的[持久化目录](../persistence-catalog.zh.md) 拥有事件声明。 + +## 目录选择 + +`ctx.workingDirectory.get(session)` 读取日志中的值。`ensure(agent)` 验证它,并在目录消失时恢复原始项目。`set(agent, path)` 验证并记录请求的变更。header 没有目录的 Session 使用配置的运行时回退目录;回退目录不可用时返回错误。 + +变更仅影响一个 Session。相对路径文件系统操作、新启动的进程、指令发现、技能和文件补全使用当前目录。已有终端和持久 shell 保留自己的目录。沙箱可写根目录与项目归属继续绑定原始项目。 + +## 模型上下文 + +初始值与后续当前值通过必需的用户上下文快照到达模型,即使可选运行时上下文被禁用也不例外。每次已提交变更都会尝试通过普通 Agent 收件箱排入通知;取消或销毁可能丢弃尚未进入请求的通知。目录事件仍然保留,下一次获准进入的请求会收到当前值。目录上下文不进入系统提示词。[`working_directory`](../../packages/session/tool-working-directory/README.zh.md) 提供一个可选的 `cd` 参数。 + + + + + +## Cordis API + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.workingDirectory` — `WorkingDirectoryService` + +One owner for each Session's effective directory and its model-visible changes. + +```ts cordis-catalog +/** + * Read the committed directory without filesystem I/O. + * @param session - Session whose directory is requested. + * @returns its effective absolute directory. + */ +get(session: Session): string + +/** + * Validate the current directory and restore the original project when it disappeared. + * @param agent - live or unpublished Agent owning the Session. + * @param signal - cancellation for filesystem inspection. + * @returns the existing directory; recovery is committed before fulfillment. + * A notice failure is warned without reverting the committed state. + * @throws when the original project is also unavailable. + */ +ensure(agent: Agent, signal?: AbortSignal): Promise + +/** + * Change one Session's directory without changing existing processes or permissions. + * @param agent - live or unpublished Agent owning the Session. + * @param path - absolute path or a path relative to its current directory. + * @param signal - cancellation before the durable change. + * @returns the canonical absolute directory, committed before fulfillment. + * A notice failure is warned; the next request still receives the committed directory. + * @throws when the requested path is not an existing directory. + */ +set(agent: Agent, path: string, signal?: AbortSignal): Promise +``` + +Types: [Agent](core.zh.md) · [Session](session.zh.md) + +Source: [`packages/session/working-directory/src/index.ts`](../../packages/session/working-directory/src/index.ts) + diff --git a/docs/subsystems/workspace.md b/docs/subsystems/workspace.md index 1e1e6c0fe6..e501d7b7bb 100644 --- a/docs/subsystems/workspace.md +++ b/docs/subsystems/workspace.md @@ -232,10 +232,10 @@ Typed Remote control of transient Session-owned terminal processes. ```ts cordis-catalog /** - * Read the Session working directory and terminal limits without resolving a shell. + * Read the Session's current directory and terminal limits without filesystem validation or shell lookup. * @param agent - Session owner supplied by the Gateway. * @param signal - request cancellation. - * @returns the Session workspace directory and terminal limits. + * @returns the logged current directory and terminal limits; retained terminals keep their own process directories. */ @Remote environment(agent: Agent, signal: AbortSignal): TerminalEnvironment diff --git a/docs/subsystems/workspace.zh.md b/docs/subsystems/workspace.zh.md index 6603e77b2f..868712b4c9 100644 --- a/docs/subsystems/workspace.zh.md +++ b/docs/subsystems/workspace.zh.md @@ -232,10 +232,10 @@ Typed Remote control of transient Session-owned terminal processes. ```ts cordis-catalog /** - * Read the Session working directory and terminal limits without resolving a shell. + * Read the Session's current directory and terminal limits without filesystem validation or shell lookup. * @param agent - Session owner supplied by the Gateway. * @param signal - request cancellation. - * @returns the Session workspace directory and terminal limits. + * @returns the logged current directory and terminal limits; retained terminals keep their own process directories. */ @Remote environment(agent: Agent, signal: AbortSignal): TerminalEnvironment diff --git a/docs/subsystems/worktrees.i18n.yaml b/docs/subsystems/worktrees.i18n.yaml new file mode 100644 index 0000000000..03544fbc81 --- /dev/null +++ b/docs/subsystems/worktrees.i18n.yaml @@ -0,0 +1,13 @@ +# Bilingual-pair consistency record for worktrees.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/subsystems/worktrees.md +/git-worktrees: + en: 21ec4a3b825d7e81 + zh: c841c5ca4b4e8959 +/git-worktrees/creation-and-retention: + en: 53f643c5d30e00b2 + zh: ba9e52efc62c7656 +/git-worktrees/request-and-result: + en: 8391b091607600eb + zh: 6c735a337d60d566 diff --git a/docs/subsystems/worktrees.md b/docs/subsystems/worktrees.md new file mode 100644 index 0000000000..14db6711bc --- /dev/null +++ b/docs/subsystems/worktrees.md @@ -0,0 +1,76 @@ +# Git Worktrees + +English | [中文](worktrees.zh.md) + +The experimental [`dsh-experimental-worktree`](../../packages/experimental/worktree/README.md) service provides `ctx.worktrees`. Its [`create_worktree`](../../packages/experimental/tool-worktree/README.md) consumer creates a new Git branch and checkout, then selects it through the [working-directory service](working-directory.md). The [Git Worktrees optional bundle](../../packages/experimental/tool-worktree/README.md#use-this-package) loads both packages through the GUI plugin manager. It ships switched off; explicit compositions can also mount the two packages. + +## Creation and retention + +The calling Session's current directory selects the source repository. Creation pins the requested local commit, branch, or tag before allocating the checkout; omission selects `HEAD`. Each name must identify a new branch and an unused checkout path. The operation neither fetches missing objects nor copies staged, unstaged, untracked, or ignored files from the source checkout. + +The default destination is `/.agents/worktrees/`. `repositoryRoot` is the top-level directory of the caller’s current checkout; a linked checkout therefore contains its own pool. The service writes a `.gitignore` containing `*` and a trailing newline only when its allocation creates the configured pool directory. Existing files and ignore rules are preserved. The calling Session’s existing sandbox policy must permit writes to the destination and shared Git administration directory. Successful Git setup precedes the working-directory change. + +Leaving through `working_directory({ cd: path })` retains the checkout and branch. Cancellation or failure can also retain partial artifacts; the error identifies their location. The service aborts and drains pending operations on disposal. The [package README](../../packages/experimental/worktree/README.md) owns configuration and Git requirements. + +## Request and result + +The request selects only the new name and local revision. The result identifies the created checkout and exact commit; it does not change the Session's original project or write permissions. + +```ts type-equiv +/** A new checkout from a locally available Git commit, branch, or tag. */ +interface CreateWorktreeRequest { + /** New branch name, also used as the relative checkout directory; omission generates a name. */ + name?: string + /** Local revision to resolve before creation; omission selects HEAD. */ + from?: string +} +``` + +```ts type-equiv +/** Committed Git baseline and the calling Session's resulting working directory. */ +interface CreatedWorktree { + /** Canonical absolute checkout directory in the filesystem provider's execution world. */ + path: string + /** Newly created branch name. */ + branch: string + /** Resolved commit object name; repository-local replacement refs can change its checked-out content. */ + baseCommit: string + /** Canonical root of the source checkout. */ + repositoryRoot: string +} +``` + + + + + +## Cordis API + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.worktrees` — `WorktreeService` + +Creates retained Git worktrees under the mounted filesystem and sandbox providers. + +```ts cordis-catalog +/** + * Create a fresh branch and checkout at a pinned local revision, then enter it. + * Existing branches or checkout paths fail. Uncommitted files stay in the source checkout. + * Checkout disables configured clean, smudge, and process filters without changing Git config. + * Repository-local replacement refs still apply; baseCommit reports the resolved object name. + * The new checkout becomes current only after Git setup succeeds. Failures may retain newly + * allocated Git/filesystem artifacts; no branch or checkout is removed automatically. + * @param agent - caller whose current directory selects the source repository and file policy. + * @param request - optional new name and local revision; defaults are generated name and HEAD. + * @param signal - cancellation of lookup, creation, and working-directory publication. + * @returns canonical checkout path, branch name, pinned commit, and source repository root. + */ +async create(agent: Agent, request: CreateWorktreeRequest = {}, signal?: AbortSignal): Promise +``` + +Types: [Agent](core.md) + +Source: [`packages/experimental/worktree/src/index.ts`](../../packages/experimental/worktree/src/index.ts) + diff --git a/docs/subsystems/worktrees.zh.md b/docs/subsystems/worktrees.zh.md new file mode 100644 index 0000000000..719d99266b --- /dev/null +++ b/docs/subsystems/worktrees.zh.md @@ -0,0 +1,76 @@ +# Git 工作树 + +[English](worktrees.md) | 中文 + +实验性的 [`dsh-experimental-worktree`](../../packages/experimental/worktree/README.zh.md) 服务提供 `ctx.worktrees`。其消费方 [`create_worktree`](../../packages/experimental/tool-worktree/README.zh.md) 创建新的 Git 分支与检出目录,再通过[工作目录服务](working-directory.zh.md)选中它。[Git 工作树可选 bundle](../../packages/experimental/tool-worktree/README.zh.md#use-this-package)通过 GUI 插件管理器一起加载这两个包。它随安装提供,但默认关闭;显式组合也可以挂载这两个包。 + +## 创建与保留 + +调用方 Session 的当前目录决定源仓库。创建操作在分配检出目录前,将请求的本地提交、分支或标签固定为具体提交;省略时选择 `HEAD`。每个名称必须对应新分支与未使用的检出路径。操作既不获取缺失对象,也不复制源检出目录中的暂存、未暂存、未跟踪或被忽略文件。 + +默认目标为 `/.agents/worktrees/`。`repositoryRoot` 是调用方当前检出目录的顶层目录;因此,链接检出目录会在自身内部保存工作树父目录。仅当服务的分配操作创建了配置指定的父目录时,服务才会写入内容为 `*` 加换行符的 `.gitignore`。已有文件与忽略规则保持不变。调用方 Session 的现有沙箱策略必须允许写入目标目录与共享 Git 管理目录。Git 初始化成功后才改变工作目录。 + +通过 `working_directory({ cd: path })` 离开时,会保留检出目录与分支。取消或失败也可能留下部分产物;错误会指出它们的位置。服务销毁时会中止待完成操作并等待它们结束。[包 README](../../packages/experimental/worktree/README.zh.md)拥有配置与 Git 要求。 + +## 请求与结果 + +请求只选择新名称与本地版本。结果标识创建的检出目录与确切提交;它不会改变 Session 的原始项目或写权限。 + +```ts type-equiv +/** A new checkout from a locally available Git commit, branch, or tag. */ +interface CreateWorktreeRequest { + /** New branch name, also used as the relative checkout directory; omission generates a name. */ + name?: string + /** Local revision to resolve before creation; omission selects HEAD. */ + from?: string +} +``` + +```ts type-equiv +/** Committed Git baseline and the calling Session's resulting working directory. */ +interface CreatedWorktree { + /** Canonical absolute checkout directory in the filesystem provider's execution world. */ + path: string + /** Newly created branch name. */ + branch: string + /** Resolved commit object name; repository-local replacement refs can change its checked-out content. */ + baseCommit: string + /** Canonical root of the source checkout. */ + repositoryRoot: string +} +``` + + + + + +## Cordis API + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.worktrees` — `WorktreeService` + +Creates retained Git worktrees under the mounted filesystem and sandbox providers. + +```ts cordis-catalog +/** + * Create a fresh branch and checkout at a pinned local revision, then enter it. + * Existing branches or checkout paths fail. Uncommitted files stay in the source checkout. + * Checkout disables configured clean, smudge, and process filters without changing Git config. + * Repository-local replacement refs still apply; baseCommit reports the resolved object name. + * The new checkout becomes current only after Git setup succeeds. Failures may retain newly + * allocated Git/filesystem artifacts; no branch or checkout is removed automatically. + * @param agent - caller whose current directory selects the source repository and file policy. + * @param request - optional new name and local revision; defaults are generated name and HEAD. + * @param signal - cancellation of lookup, creation, and working-directory publication. + * @returns canonical checkout path, branch name, pinned commit, and source repository root. + */ +async create(agent: Agent, request: CreateWorktreeRequest = {}, signal?: AbortSignal): Promise +``` + +Types: [Agent](core.zh.md) + +Source: [`packages/experimental/worktree/src/index.ts`](../../packages/experimental/worktree/src/index.ts) + diff --git a/docs/testing.i18n.yaml b/docs/testing.i18n.yaml index b870988726..d7936c0fef 100644 --- a/docs/testing.i18n.yaml +++ b/docs/testing.i18n.yaml @@ -9,8 +9,8 @@ en: c024da7e8d9cb056 zh: c4cc1a66d2903656 /testing-policy/how-specs-execute: - en: 298c13b105e86eae - zh: 4f1b0b7742fb7b49 + en: 5d8eb65958da096d + zh: 4a40f1c5fe5b2d14 /testing-policy/the-with-key-policy-inference-is-cheap-here: en: d555af53068bf523 zh: ddc4265e5de5b3be diff --git a/docs/testing.md b/docs/testing.md index 21b2916a92..9ea6db1501 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -18,7 +18,7 @@ Session fixtures retain headers and payloads but omit body sequence/time envelop ## How specs execute -Forked workers run several spec files at once, the coverage gate splits into concurrent partitions beside the other gates in its job, and the self-hosted runners share one host and one volume. Only the process is isolated: ports, predictable paths, external namespaces, and inherited children are not. Own each acquired resource through its teardown, and read a spec that passes only when it runs alone as a defect in the spec rather than an unstable runner. [dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) owns the allocation, restoration, synchronization, timeout-budget, platform, and teardown rules; its [flake diagnosis workflow](../.agents/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md) classifies an existing probabilistic failure. +Forked workers run several spec files at once, the coverage gate splits into concurrent partitions beside the other gates in its job, and the self-hosted runners share one host and one volume. Only the process is isolated: ports, predictable paths, external namespaces, and inherited children are not. Own each acquired resource through teardown. A spec passing only alone has a test defect, not an unstable runner. [dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) owns the allocation, restoration, synchronization, timeout-budget, platform, and teardown rules; its [flake diagnosis workflow](../.agents/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md) classifies an existing probabilistic failure. [Unexpected fork exits](../scripts/coverage-fork-diagnostics.ts) report PID, file, code, and signal. ## The with-key policy: inference is cheap here diff --git a/docs/testing.zh.md b/docs/testing.zh.md index 714d5a8509..81c16c60ab 100644 --- a/docs/testing.zh.md +++ b/docs/testing.zh.md @@ -18,7 +18,7 @@ Session fixture 保留 header 与 payload,但省略正文 seq/time envelope; ## spec 如何被执行 -fork 出的 worker 会同时运行多个 spec 文件,coverage gate 会拆成并发的 partition,与同一个 job 中的其它 gate 并排运行,而自托管 runner 共用同一台宿主机和同一个卷。被隔离的只有进程:端口、可预测路径、外部命名空间和继承而来的子进程都不隔离。为每个占用的资源负责到它的 teardown,并把「只有单独运行时才通过」的 spec 读作该 spec 的缺陷,而不是 runner 不稳定。[dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) 负责资源分配、状态恢复、同步、超时预算、平台差异与 teardown 规则;它的 [flake 诊断流程](../.agents/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md)用于归类已经存在的概率性失败。 +fork 出的 worker 会同时运行多个 spec 文件,coverage gate 会拆成并发的 partition,与同一个 job 中的其它 gate 并排运行,而自托管 runner 共用同一台宿主机和同一个卷。被隔离的只有进程:端口、可预测路径、外部命名空间和继承而来的子进程都不隔离。为每个占用的资源负责到 teardown。只在单独运行时通过的 spec 存在测试缺陷,而非 runner 不稳定。[dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) 负责资源分配、状态恢复、同步、超时预算、平台差异与 teardown 规则;它的 [flake 诊断流程](../.agents/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md)用于归类已经存在的概率性失败。 [fork 意外退出](../scripts/coverage-fork-diagnostics.ts)时会报告 PID、文件、退出码和信号。 ## 带密钥策略:推理(inference)在这里很便宜 diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 48ff1a5b62..e14eeb549f 100644 --- a/docs/tool-catalog.i18n.yaml +++ b/docs/tool-catalog.i18n.yaml @@ -9,8 +9,8 @@ en: 6cc2cd9ced70bb09 zh: 1c0e6cb4ba2ed637 /tool-schema-catalog/tool-package-map: - en: 75120ef94b3c2830 - zh: c5db28dd6853e91a + en: 7caa258c67464109 + zh: 53bfb5581c48dc45 /tool-schema-catalog/deepseek-ai-dsh-plugin-manager: en: e0143916bb634762 zh: e0143916bb634762 @@ -60,8 +60,8 @@ en: 76fbbf0fe38aef10 zh: 76fbbf0fe38aef10 /tool-schema-catalog/deepseek-ai-dsh-tools/run-code: - en: daae801c6b93b624 - zh: 7cd6fd029b683d69 + en: 2cfa8042a920b191 + zh: bbd0aae394fb3806 /tool-schema-catalog/deepseek-ai-dsh-plan-mode: en: b500b530845e26a9 zh: b500b530845e26a9 @@ -136,28 +136,28 @@ zh: c6ce31ff2c195520 /tool-schema-catalog/deepseek-ai-dsh-tool-fs-search/grep: en: 6e1f7da62cc9a35b - zh: ee7f3081887f8067 + zh: 594817b0befeea86 /tool-schema-catalog/deepseek-ai-dsh-tool-terminal: en: 77501c1b592b6a88 zh: 77501c1b592b6a88 /tool-schema-catalog/deepseek-ai-dsh-tool-terminal/terminal-close: - en: 6d6e428c866ac6c1 - zh: 8033539fcdcb6e7b + en: 4da73ab1c95aadc6 + zh: 998471bc4f5af00a /tool-schema-catalog/deepseek-ai-dsh-tool-terminal/terminal-list: - en: ae7d5bce32142aa5 - zh: e956a650a5bf294b + en: 4cde2b1d38f93e04 + zh: 2c93f43d54ec9da2 /tool-schema-catalog/deepseek-ai-dsh-tool-terminal/terminal-open: - en: c542bd7f7e872441 - zh: 14731de81780b518 + en: 7051c20587ae426d + zh: 39a6aad70d643fbc /tool-schema-catalog/deepseek-ai-dsh-tool-terminal/terminal-read: - en: 6a7491d2436e4404 - zh: 772ff408ec0a67e3 + en: 833b47cca9b00b9d + zh: 24b3db5b48f0446a /tool-schema-catalog/deepseek-ai-dsh-tool-terminal/terminal-send: - en: e24ca8735dd59adb - zh: 24f7140a0aed0968 + en: 60a8805611533884 + zh: 50e4ac6e7f7b9b0a /tool-schema-catalog/deepseek-ai-dsh-tool-terminal/terminal-signal: - en: 36a7f502acf00049 - zh: 00b4567b534eb7b2 + en: 0191964f5be70426 + zh: 501c2769633a2a0b /tool-schema-catalog/deepseek-ai-dsh-tool-goal: en: 74fff3741e7c13de zh: 74fff3741e7c13de @@ -190,13 +190,13 @@ zh: 32982123b1f0e5bc /tool-schema-catalog/deepseek-ai-dsh-tool-lsp/lsp: en: 877f1dc0f69e2038 - zh: 2fa570020f87919c + zh: 5dda8001fb614315 /tool-schema-catalog/deepseek-ai-dsh-tool-ralph: en: 45bd083870e2c92d zh: 45bd083870e2c92d /tool-schema-catalog/deepseek-ai-dsh-tool-ralph/ralph: - en: a14c562ee26ddfd8 - zh: caa6d1acf95a8199 + en: e97552156225cf10 + zh: 9ea32b8485e950ec /tool-schema-catalog/deepseek-ai-dsh-tool-skill: en: 927a5ac0974b7642 zh: 927a5ac0974b7642 @@ -207,20 +207,20 @@ en: eac1dff4b6d9e195 zh: eac1dff4b6d9e195 /tool-schema-catalog/deepseek-ai-dsh-tool-session-query/session-event-read: - en: 6687105065ac3a5f - zh: 48b2f4d38536e4c7 + en: 285bab0bd6b50949 + zh: 9c839e05237a7226 /tool-schema-catalog/deepseek-ai-dsh-tool-session-query/session-event-search: - en: 41251b4cbe9af70d - zh: 8855a5f32b4720e5 + en: 5cd9f05b83f5797e + zh: dc6880566b0c5138 /tool-schema-catalog/deepseek-ai-dsh-tool-session-query/session-event-trace: - en: 14b5ec05fa8c0932 - zh: 3c85d44c13cc42c5 + en: de64e837459d7313 + zh: 60bfcb3691ab862c /tool-schema-catalog/deepseek-ai-dsh-tool-session-query/session-search: - en: 8263202c97568fc3 - zh: 92067337294c7a00 + en: c7bc2ee2892bf0e9 + zh: 1cd6a5d6f7494ca1 /tool-schema-catalog/deepseek-ai-dsh-tool-session-query/session-trace: - en: 22866623e207d94e - zh: 59f4f8eadcdba85c + en: 394175dfcf2bf489 + zh: f62bda32d3a775c7 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent: en: 841abe0dd5913ea7 zh: 841abe0dd5913ea7 @@ -228,20 +228,20 @@ en: c89084b31ffccbce zh: d7b3b7000f2d2be9 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent/subagent: - en: 55cbd29d70bcf3d7 - zh: 886a1b57446bd5e1 + en: 9270966e03a2a62b + zh: 0544ac614aed3144 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control: en: ab82e130c3413f04 zh: ab82e130c3413f04 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/interrupt-agent: - en: 0cec3f2520050839 - zh: 06735f086cea84f1 + en: 9190a32df5e10ddd + zh: 48e1b05956d63e26 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/list-agents: en: ae22e8543d123c39 - zh: 4943a5c5980fc2d3 + zh: 0991a73ef49bcdea /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/send-message: - en: 2ebdc450faef6446 - zh: 7e3cff35fedc6a32 + en: 69c1c0a80c24e3a4 + zh: 04f4ac5ec365aa63 /tool-schema-catalog/deepseek-ai-dsh-tool-jobs: en: 9be673b1604d0114 zh: 9be673b1604d0114 @@ -252,8 +252,8 @@ en: 0c37fb1fed979d72 zh: 5ea9d07f5a4a8a6c /tool-schema-catalog/deepseek-ai-dsh-tool-jobs/job-output: - en: f927cca09b283e21 - zh: 2acd9b36737f17c2 + en: 1e084359a8cd42b3 + zh: 3f6c245dbbb2b920 /tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team: en: 6b1106f417b50db6 zh: 6b1106f417b50db6 @@ -288,8 +288,20 @@ en: dea62d26fb18ff68 zh: dea62d26fb18ff68 /tool-schema-catalog/deepseek-ai-dsh-tool-todo/todo-write: - en: 8c64fe5f2e58a890 - zh: e3c780fc842aeb43 + en: f252137b940b38d1 + zh: 25bd9c6b1ce783d8 +/tool-schema-catalog/deepseek-ai-dsh-tool-working-directory: + en: 1bdc4627ca4fff29 + zh: 1bdc4627ca4fff29 +/tool-schema-catalog/deepseek-ai-dsh-tool-working-directory/working-directory: + en: ac1944f6035b8b50 + zh: f5fc005c13185211 +/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-worktree: + en: d5f5679abe86e316 + zh: d5f5679abe86e316 +/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-worktree/create-worktree: + en: 3cf89e31f011ae9b + zh: 28e78b4fae2d075b /tool-schema-catalog/deepseek-ai-dsh-tool-workflow: en: bb38a9d29f46563c zh: bb38a9d29f46563c diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 867eec6d33..143d1b29df 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -19,29 +19,31 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - | | `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`, `stagehand_extract`, `stagehand_navigate`, `stagehand_observe`, `stagehand_screenshot`, `stagehand_tabs` | `ctx.browserUse`, `ctx.agents`, `ctx.tools`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | - | | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userQuestions` | `tool/call`, `tool/result after an answer or timeout`, `late user/message` | - | ask_user_question keeps the original blocking behavior by default; set `mode: timed` to opt into a foreground timeout and pending result while the question remains answerable. In timed mode, `timeout: -1` keeps that call blocking indefinitely. | -| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.ptcRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` / `mode: both` (see the PTC mode Agent Note). Under `ptc` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. | +| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.workingDirectory (Agent executions)`, `ctx.tools`, `ctx.ptcRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` (see the PTC mode Agent Note). Under `ptc` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. | | `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`, `ctx.systemPrompt`, `ctx.userQuestions (execution time, opportunistic)` | `tool/call`, `plan/mode inactive on an approved review`, `tool/result` | - | exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary. | -| `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.tools`, `ctx.shell`, `ctx.systemPrompt`, `ctx.shellEnv`, `ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`, `tool/result` | - | The bash tool is the model-facing consumer of the bash executor seam. With a job registry composed every call registers with the generic `ctx.jobs` runtime as it starts, collected/stopped through the `job_*` tools from `@deepseek-ai/dsh-tool-jobs`; without one, or with `enableRunInBackground: false`, the tool registers a foreground-only schema without the `run_in_background` parameter. | -| `@deepseek-ai/dsh-tool-present` | `present` | `ctx.tools`, `ctx.fs`, `ctx.sessionProjections` | `tool/call`, `deliverables/presented after a successful final result`, `tool/result` | - | Deliveries belong to the calling Session; Web ui-deliverables supplies source-file opening and cards. | -| `@deepseek-ai/dsh-tool-pwsh` | `pwsh` | `ctx.tools`, `ctx.shell`, `ctx.systemPrompt`, `ctx.shellEnv`, `ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`, `tool/result` | - | The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.shell`); it mirrors the bash tool call-for-call minus sandbox controls — `run_in_background` runs register with the generic `ctx.jobs` runtime and are collected/stopped through the `job_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-shell-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\...` paths and `$env:NAME` variables. | +| `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.workingDirectory`, `ctx.tools`, `ctx.shell`, `ctx.systemPrompt`, `ctx.shellEnv`, `ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`, `tool/result` | - | The bash tool is the model-facing consumer of the bash executor seam. With a job registry composed every call registers with the generic `ctx.jobs` runtime as it starts, collected/stopped through the `job_*` tools from `@deepseek-ai/dsh-tool-jobs`; without one, or with `enableRunInBackground: false`, the tool registers a foreground-only schema without the `run_in_background` parameter. | +| `@deepseek-ai/dsh-tool-present` | `present` | `ctx.workingDirectory`, `ctx.tools`, `ctx.fs`, `ctx.sessionProjections` | `tool/call`, `deliverables/presented after a successful final result`, `tool/result` | - | Deliveries belong to the calling Session; Web ui-deliverables supplies source-file opening and cards. | +| `@deepseek-ai/dsh-tool-pwsh` | `pwsh` | `ctx.workingDirectory`, `ctx.tools`, `ctx.shell`, `ctx.systemPrompt`, `ctx.shellEnv`, `ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`, `tool/result` | - | The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.shell`); it mirrors the bash tool call-for-call minus sandbox controls — `run_in_background` runs register with the generic `ctx.jobs` runtime and are collected/stopped through the `job_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-shell-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\...` paths and `$env:NAME` variables. | | `@deepseek-ai/dsh-tool-cordis` | `cordis_inspect_list`, `cordis_inspect_query` | `ctx.tools`, `ctx.cordisInspect` | `tool/call`, `tool/result` | - | Creator mode provides two read-only runtime inspection tools. The Cordis host runner supplies the inspection registry; Client queries require a connected page. Author persistent changes as bundles and install them with plugin_manager. | -| `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent bash tool; deployment composition supplies the PTY backend and may override the model-facing environment description. | -| `@deepseek-ai/dsh-tool-pwsh-persistent` | `pwsh` | `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent pwsh tool, the Windows counterpart of the persistent bash tool; deployment composition supplies a pwsh-dialect PTY backend and may override the model-facing environment description. | +| `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.workingDirectory`, `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent bash tool; deployment composition supplies the PTY backend and may override the model-facing environment description. | +| `@deepseek-ai/dsh-tool-pwsh-persistent` | `pwsh` | `ctx.workingDirectory`, `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent pwsh tool, the Windows counterpart of the persistent bash tool; deployment composition supplies a pwsh-dialect PTY backend and may override the model-facing environment description. | | `@deepseek-ai/dsh-tool-str-replace-editor` | `str_replace_editor` | `ctx.tools`, `ctx.fs` | `tool/call`, `fs/observed after view presence/absence, edit absence, or successful mutation`, `tool/result` | - | Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API. | -| `@deepseek-ai/dsh-tool-fs` | `edit`, `read`, `read_image`, `write` | `ctx.tools`, `ctx.fs`, `ctx.systemPrompt`, `ctx.attachments (image-tool registration)`, `ctx.llm + an image-capable route (image-tool execution)` | `tool/call`, `fs/write-intent or fs/edit-intent for mutations`, `fs/observed after read presence/absence or successful file operation`, `durable attachment (read_image)`, `tool/result` | - | The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-observation-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The image tool is not registered without `ctx.attachments`; its schema is route-independent, and execution refuses unless the exact routed model declares image input. | -| `@deepseek-ai/dsh-tool-fs-search` | `glob`, `grep` | `ctx.tools`, `ctx.subprocess`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | glob and grep are unconditional discovery tools that spawn the packaged ripgrep binary (`@vscode/ripgrep`) through ctx.subprocess as ordinary foreground calls (never background jobs) — no host `rg` install and no shell layer. The catalog uses `sampleOverCapGlobResults: true`; deployments must choose that behavior explicitly. Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. | -| `@deepseek-ai/dsh-tool-terminal` | `terminal_close`, `terminal_list`, `terminal_open`, `terminal_read`, `terminal_send`, `terminal_signal` | `ctx.tools`, `ctx.terminals`, `ctx.systemPrompt`, `ctx.jobs at call time for run_in_background` | `tool/call`, `tool/result` | - | The six terminal tools are opt-in and complement one-shot shell/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.jobs`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema. | +| `@deepseek-ai/dsh-tool-fs` | `edit`, `read`, `read_image`, `write` | `ctx.workingDirectory`, `ctx.tools`, `ctx.fs`, `ctx.systemPrompt`, `ctx.attachments (image-tool registration)`, `ctx.llm + an image-capable route (image-tool execution)` | `tool/call`, `fs/write-intent or fs/edit-intent for mutations`, `fs/observed after read presence/absence or successful file operation`, `durable attachment (read_image)`, `tool/result` | - | The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-observation-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The image tool is not registered without `ctx.attachments`; its schema is route-independent, and execution refuses unless the exact routed model declares image input. | +| `@deepseek-ai/dsh-tool-fs-search` | `glob`, `grep` | `ctx.workingDirectory`, `ctx.tools`, `ctx.subprocess`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | glob and grep are unconditional discovery tools that spawn the packaged ripgrep binary (`@vscode/ripgrep`) through ctx.subprocess as ordinary foreground calls (never background jobs) — no host `rg` install and no shell layer. The catalog uses `sampleOverCapGlobResults: true`; deployments must choose that behavior explicitly. Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. | +| `@deepseek-ai/dsh-tool-terminal` | `terminal_close`, `terminal_list`, `terminal_open`, `terminal_read`, `terminal_send`, `terminal_signal` | `ctx.workingDirectory`, `ctx.tools`, `ctx.terminals`, `ctx.systemPrompt`, `ctx.jobs at call time for run_in_background` | `tool/call`, `tool/result` | - | The six terminal tools are opt-in and complement one-shot shell/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.jobs`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema. | | `@deepseek-ai/dsh-tool-goal` | `create_goal`, `get_goal`, `update_goal` | `ctx.tools`, `ctx.agents`, `ctx.goals`, `ctx.systemPrompt`, `a calling Agent in an authorized open turn` | `tool/call`, `goal/change for mutations`, `tool/result` | - | create, edit, pause, and resume require direct-human root authority; complete and blocked also accept the exact current goal round. The default blocked lower bound is three admitted rounds. | | `@deepseek-ai/dsh-tool-schedule` | `schedule_create`, `schedule_delete`, `schedule_list`, `schedule_update` | `ctx.tools`, `ctx.schedule` | `tool/call`, `Schedule storage domain create, update, or delete`, `tool/result` | - | A preset or Agent scope mounts this package; the preset decides which agents receive the four management tools. Each call acts on the calling Agent's Session. Accepts after_seconds, explicit absolute at, bounded fixed-rate every_seconds, daily and weekly local times in an explicit IANA zone, and cron as a five-field expression. Management uses the Host storage domain; due messages resume the original Session. | -| `@deepseek-ai/dsh-tool-lsp` | `lsp` | `ctx.tools`, `ctx.lsp`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | The lsp tool keeps provider selection and language-server subprocesses behind ctx.lsp, so its model-visible schema stays stable across providers. Requires a registered provider (e.g. `@deepseek-ai/dsh-lsp-stdio`) at runtime; without one, a query returns the structured `LSP_UNAVAILABLE` error rather than changing the schema. | +| `@deepseek-ai/dsh-tool-lsp` | `lsp` | `ctx.workingDirectory`, `ctx.tools`, `ctx.lsp`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | The lsp tool keeps provider selection and language-server subprocesses behind ctx.lsp, so its model-visible schema stays stable across providers. Requires a registered provider (e.g. `@deepseek-ai/dsh-lsp-stdio`) at runtime; without one, a query returns the structured `LSP_UNAVAILABLE` error rather than changing the schema. | | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`, `ctx.workflowEngine`, `ctx.subagents`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents every fresh round)` | `tool/call`, `tool/result`, `workflow and child session events during execution` | - | A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. | -| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.tools`, `ctx.agents`, `ctx.skills` | `tool/call`, `tool/result`, `user/message replacement catalogs via agent.inject()` | - | - | +| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.workingDirectory`, `ctx.tools`, `ctx.agents`, `ctx.skills` | `tool/call`, `tool/result`, `user/message replacement catalogs via agent.inject()` | - | - | | `@deepseek-ai/dsh-tool-session-query` | `session_event_read`, `session_event_search`, `session_event_trace`, `session_search`, `session_trace` | `ctx.tools`, `ctx.systemPrompt`, `ctx.sessionQuery`, `a calling Agent for workspace authority` | `tool/call`, `tool/result` | - | The five read-only tools hide provider cursors and authorize every result from the immutable calling agent session. The package is opt-in; compositions that need enforced deadlines or bounded inline output also mount the generic timeout or spill policies. | -| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance independently controls whether it reads model-selection settings and its background behavior through `modelSelectionSettings`, `backgroundMode`, and `enableRunInBackground`. | -| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named control tools over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). | -| `@deepseek-ai/dsh-tool-jobs` | `job_kill`, `job_list`, `job_output` | `ctx.tools`, `ctx.jobs`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `user/message via agent.inject() for background completion notices` | - | The kind-agnostic background-job controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`. | +| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. | +| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). | +| `@deepseek-ai/dsh-tool-jobs` | `job_kill`, `job_list`, `job_output` | `ctx.tools`, `ctx.jobs`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `user/message via agent.inject() for background completion notices` | - | The kind-agnostic background-job controller: background bash commands and PTY sends are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`. | | `@deepseek-ai/dsh-experimental-tool-agent-team` | `interrupt_agent`, `list_agents`, `send_message`, `spawn_teammate`, `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/message/queued`, `team/message/delivered`, `team/task`, `tool/result` | - | All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. | | `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task. | +| `@deepseek-ai/dsh-tool-working-directory` | `working_directory` | `ctx.tools`, `ctx.workingDirectory` | `tool/call`, `working-directory/change`, `user context for directory changes`, `tool/result` | - | Reads or changes the calling Session directory. Existing shells and processes retain their own directories; origin metadata and permission roots stay fixed. | +| `@deepseek-ai/dsh-experimental-tool-worktree` | `create_worktree` | `ctx.tools`, `ctx.worktrees`, `a calling Agent` | `tool/call`, `Git branch and checkout`, `working-directory/change`, `tool/result` | - | Explicit experimental composition only. Creates a new branch and checkout from a pinned local commit under existing write permissions, then changes the Session directory. Leaving retains the branch and checkout. | | `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`, `ctx.workflowEngine`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents the script children)` | `tool/call`, `tool/result` | - | - | | `@deepseek-ai/dsh-tool-workspace-dependencies` | `load_workspace_dependencies` | `ctx.tools` | `tool/call`, `tool/result` | - | - | | `@deepseek-ai/dsh-tool-web` | `web_fetch`, `web_search` | `ctx.tools`, `ctx.web`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | web_search and web_fetch keep provider selection behind ctx.web so model-visible schemas stay stable across backend swaps. | @@ -74,7 +76,7 @@ List plugins or bundles in the current profile, enable or disable them, install }, "target": { "type": "string", - "description": "Plugin entry id, bundle package name, or installation spec, according to action." + "description": "Plugin entry id, bundle package name, or installation spec, according to action. For an Official catalog entry, pass list_bundles installTarget.spec to install_bundle." }, "enabled": { "type": "boolean", @@ -563,7 +565,7 @@ Execute a TypeScript program against the available tools. Takes two required arg Source: [`packages/core/tools/src/ptc.ts`](../packages/core/tools/src/ptc.ts) -Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` / `mode: both` (see the PTC mode Agent Note). Under `ptc` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. +Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` (see the PTC mode Agent Note). Under `ptc` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. @@ -1141,7 +1143,7 @@ Close one persistent terminal and wait until its captured owned process tree is } ``` -Source: [`packages/terminal/tool-terminal/src/index.ts`](../packages/terminal/tool-terminal/src/index.ts) +Source: [`packages/experimental/tool-terminal/src/index.ts`](../packages/experimental/tool-terminal/src/index.ts) ### `terminal_list` @@ -1154,7 +1156,7 @@ List persistent terminal sessions owned by the current agent. } ``` -Source: [`packages/terminal/tool-terminal/src/index.ts`](../packages/terminal/tool-terminal/src/index.ts) +Source: [`packages/experimental/tool-terminal/src/index.ts`](../packages/experimental/tool-terminal/src/index.ts) ### `terminal_open` @@ -1174,7 +1176,7 @@ Create a persistent, owner-isolated terminal session from a registered backend t }, "cwd": { "type": "string", - "description": "Initial working directory. Defaults to the deployment workspace root." + "description": "Initial working directory, relative to the Session current directory when not absolute." } }, "required": [ @@ -1183,7 +1185,7 @@ Create a persistent, owner-isolated terminal session from a registered backend t } ``` -Source: [`packages/terminal/tool-terminal/src/index.ts`](../packages/terminal/tool-terminal/src/index.ts) +Source: [`packages/experimental/tool-terminal/src/index.ts`](../packages/experimental/tool-terminal/src/index.ts) ### `terminal_read` @@ -1212,7 +1214,7 @@ Read a bounded page of retained output from a persistent terminal without sendin } ``` -Source: [`packages/terminal/tool-terminal/src/index.ts`](../packages/terminal/tool-terminal/src/index.ts) +Source: [`packages/experimental/tool-terminal/src/index.ts`](../packages/experimental/tool-terminal/src/index.ts) ### `terminal_send` @@ -1246,7 +1248,7 @@ Send text to a persistent terminal. By default Enter is submitted and the call w } ``` -Source: [`packages/terminal/tool-terminal/src/index.ts`](../packages/terminal/tool-terminal/src/index.ts) +Source: [`packages/experimental/tool-terminal/src/index.ts`](../packages/experimental/tool-terminal/src/index.ts) ### `terminal_signal` @@ -1279,7 +1281,7 @@ Send an allowed signal to the current foreground process group of a persistent t } ``` -Source: [`packages/terminal/tool-terminal/src/index.ts`](../packages/terminal/tool-terminal/src/index.ts) +Source: [`packages/experimental/tool-terminal/src/index.ts`](../packages/experimental/tool-terminal/src/index.ts) The six terminal tools are opt-in and complement one-shot shell/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.jobs`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema. @@ -1745,7 +1747,7 @@ Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only } ``` -Source: [`packages/workflow/tool-ralph/src/index.ts`](../packages/workflow/tool-ralph/src/index.ts) +Source: [`packages/experimental/tool-ralph/src/index.ts`](../packages/experimental/tool-ralph/src/index.ts) A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. @@ -1809,7 +1811,7 @@ Read one full unabridged event and optional neighboring raw-event summaries from } ``` -Source: [`packages/session-query/tool-session-query/src/index.ts`](../packages/session-query/tool-session-query/src/index.ts) +Source: [`packages/experimental/tool-session-query/src/index.ts`](../packages/experimental/tool-session-query/src/index.ts) ### `session_event_search` @@ -1869,7 +1871,7 @@ Search prior events in one authorized session; the current session excludes the } ``` -Source: [`packages/session-query/tool-session-query/src/index.ts`](../packages/session-query/tool-session-query/src/index.ts) +Source: [`packages/experimental/tool-session-query/src/index.ts`](../packages/experimental/tool-session-query/src/index.ts) ### `session_event_trace` @@ -1894,7 +1896,7 @@ Read every direct replacement and relationship to a cited source event for one e } ``` -Source: [`packages/session-query/tool-session-query/src/index.ts`](../packages/session-query/tool-session-query/src/index.ts) +Source: [`packages/experimental/tool-session-query/src/index.ts`](../packages/experimental/tool-session-query/src/index.ts) ### `session_search` @@ -1987,7 +1989,7 @@ Search prior sessions in the caller workspace and return the strongest matching } ``` -Source: [`packages/session-query/tool-session-query/src/index.ts`](../packages/session-query/tool-session-query/src/index.ts) +Source: [`packages/experimental/tool-session-query/src/index.ts`](../packages/experimental/tool-session-query/src/index.ts) ### `session_trace` @@ -2005,7 +2007,7 @@ Read the authorized session lineage around one session, including complete visib } ``` -Source: [`packages/session-query/tool-session-query/src/index.ts`](../packages/session-query/tool-session-query/src/index.ts) +Source: [`packages/experimental/tool-session-query/src/index.ts`](../packages/experimental/tool-session-query/src/index.ts) The five read-only tools hide provider cursors and authorize every result from the immutable calling agent session. The package is opt-in; compositions that need enforced deadlines or bounded inline output also mount the generic timeout or spill policies. @@ -2037,12 +2039,16 @@ Source: [`packages/subagent/tool-subagent/src/list-models.ts`](../packages/subag ### `subagent` -Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This call waits for the result by default. +Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. ```json { "type": "object", "properties": { + "cwd": { + "type": "string", + "description": "Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent." + }, "description": { "type": "string", "description": "A short (3-5 word) description of the delegated task, for display." @@ -2050,10 +2056,6 @@ Delegate a self-contained task to a subagent (a separate agent that works in its "prompt": { "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." - }, - "run_in_background": { - "type": "boolean", - "description": "Run as a background job and return its id (collect with job_output, stop with job_kill). Defaults to false." } }, "required": [ @@ -2065,7 +2067,7 @@ Delegate a self-contained task to a subagent (a separate agent that works in its Source: [`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts) -The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance independently controls whether it reads model-selection settings and its background behavior through `modelSelectionSettings`, `backgroundMode`, and `enableRunInBackground`. +The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. @@ -2073,7 +2075,7 @@ The registered delegation name is the load-time `toolName` config (default `suba ### `interrupt_agent` -Ask a subagent to stop its current work. This call returns without waiting for it to stop. You can continue a direct child's conversation later with send_message. Subagents it started will keep running. +Ask a subagent to stop its current work. This call returns without waiting for it to stop. You can continue a local direct child's conversation later with send_message. External executions stop permanently and cannot receive follow-ups. Subagents it started will keep running. ```json { @@ -2140,7 +2142,7 @@ Send a message to an agent. A working agent receives it at its next step; an idl Source: [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) -The globally named control tools over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). +The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). @@ -2213,7 +2215,7 @@ Read a background job: output since the previous read for stream jobs, or the re Source: [`packages/jobs/tool-jobs/src/index.ts`](../packages/jobs/tool-jobs/src/index.ts) -The kind-agnostic background-job controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`. +The kind-agnostic background-job controller: background bash commands and PTY sends are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`. @@ -2555,6 +2557,58 @@ Source: [`packages/todo/tool-todo/src/index.ts`](../packages/todo/tool-todo/src/ todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task. + + +## `@deepseek-ai/dsh-tool-working-directory` + +### `working_directory` + +Read the current working directory, or change it with cd. Relative paths use the current directory. Existing shells and running processes keep their own directories. + +```json +{ + "type": "object", + "properties": { + "cd": { + "type": "string", + "description": "Existing directory to enter. Omit to read the current directory." + } + } +} +``` + +Source: [`packages/session/tool-working-directory/src/index.ts`](../packages/session/tool-working-directory/src/index.ts) + +Reads or changes the calling Session directory. Existing shells and processes retain their own directories; origin metadata and permission roots stay fixed. + + + +## `@deepseek-ai/dsh-experimental-tool-worktree` + +### `create_worktree` + +Create a new Git branch and worktree from a local commit, branch, or tag, then change this session's working directory to it. Defaults to HEAD and a generated name. Uncommitted files stay in the source checkout. Existing names fail. Use working_directory with cd to leave; the checkout and branch remain. + +```json +{ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "New branch name, also used as the checkout directory. Omit to generate a unique name." + }, + "from": { + "type": "string", + "description": "Local commit, branch, or tag to start from. Defaults to HEAD; no fetch is performed." + } + } +} +``` + +Source: [`packages/experimental/tool-worktree/src/index.ts`](../packages/experimental/tool-worktree/src/index.ts) + +Explicit experimental composition only. Creates a new branch and checkout from a pinned local commit under existing write permissions, then changes the Session directory. Leaving retains the branch and checkout. + ## `@deepseek-ai/dsh-tool-workflow` diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index 17fd5d6caa..23ef011dc5 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -23,29 +23,31 @@ | `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - | | `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`、`stagehand_extract`、`stagehand_navigate`、`stagehand_observe`、`stagehand_screenshot`、`stagehand_tabs` | `ctx.browserUse`、`ctx.agents`、`ctx.tools`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | - | | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`、`ctx.userQuestions` | `tool/call`、`tool/result after an answer or timeout`、`late user/message` | - | ask_user_question 默认保持原有阻塞行为;设置 `mode: timed` 后才启用前台超时与 pending 结果,同时问题仍可回答;timed 模式内 `timeout: -1` 让本次调用无限期阻塞。 | -| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`、`ctx.ptcRuntime (execution time)`、`ctx.systemPrompt` | `tool/call`、`one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`、`tool/result` | - | 在 `mode: ptc`/`mode: both` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 PTC mode Agent Note)。在 `ptc` 下,它是注册表对协议格式(wire format)的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。 | +| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.workingDirectory (Agent executions)`、`ctx.tools`、`ctx.ptcRuntime (execution time)`、`ctx.systemPrompt` | `tool/call`、`one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`、`tool/result` | - | 在 `mode: ptc` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 PTC mode Agent Note)。在 `ptc` 下,它是注册表对协议格式(wire format)的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。 | | `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`、`ctx.systemPrompt`、`ctx.userQuestions (execution time, opportunistic)` | `tool/call`、`plan/mode inactive on an approved review`、`tool/result` | - | 规划未激活时,exit_plan_mode 仍保留在面向模型的 schema 中,这样状态转换不会在规划策略变更之外额外造成工具目录变动。其执行路径会拒绝规划模式之外的调用;在规划模式下,它通过用户交互 seam 提交计划(批准/根据反馈继续规划),批准后会在步骤边界记录规划模式已停用。 | -| `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.tools`、`ctx.shell`、`ctx.systemPrompt`、`ctx.shellEnv`、`ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`、`tool/result` | - | bash 工具是 bash 执行器 seam 面向模型的消费方。组合中有 job 注册表时,每次调用一启动就注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具(来自 `@deepseek-ai/dsh-tool-jobs`)收集/停止;没有注册表或 `enableRunInBackground: false` 时,工具注册不带 `run_in_background` 参数的纯前台 schema。 | -| `@deepseek-ai/dsh-tool-present` | `present` | `ctx.tools`, `ctx.fs`, `ctx.sessionProjections` | `tool/call`, `deliverables/presented 在成功的最终结果之后`, `tool/result` | - | 交付归调用方 Session 所有;Web ui-deliverables 提供源文件打开与卡片。 | -| `@deepseek-ai/dsh-tool-pwsh` | `pwsh` | `ctx.tools`、`ctx.shell`、`ctx.systemPrompt`、`ctx.shellEnv`、`ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`、`tool/result` | - | pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费方(由 `@deepseek-ai/dsh-pwsh-local` 等 PowerShell 执行器为 `ctx.shell` 提供后端);除沙箱接口外,它逐项对应 bash 工具调用。使用 `run_in_background` 的运行会注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具收集/停止;托管的 `DSH_*` 环境来自 `@deepseek-ai/dsh-shell-env`。每次调用都在新进程中运行,不使用持久 PTY 会话。路径采用原生 `C:\...` 形式,变量采用 `$env:NAME`。 | +| `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.workingDirectory`、`ctx.tools`、`ctx.shell`、`ctx.systemPrompt`、`ctx.shellEnv`、`ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`、`tool/result` | - | bash 工具是 bash 执行器 seam 面向模型的消费方。组合中有 job 注册表时,每次调用一启动就注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具(来自 `@deepseek-ai/dsh-tool-jobs`)收集/停止;没有注册表或 `enableRunInBackground: false` 时,工具注册不带 `run_in_background` 参数的纯前台 schema。 | +| `@deepseek-ai/dsh-tool-present` | `present` | `ctx.workingDirectory`、`ctx.tools`, `ctx.fs`, `ctx.sessionProjections` | `tool/call`, `deliverables/presented 在成功的最终结果之后`, `tool/result` | - | 交付归调用方 Session 所有;Web ui-deliverables 提供源文件打开与卡片。 | +| `@deepseek-ai/dsh-tool-pwsh` | `pwsh` | `ctx.workingDirectory`、`ctx.tools`、`ctx.shell`、`ctx.systemPrompt`、`ctx.shellEnv`、`ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`、`tool/result` | - | pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费方(由 `@deepseek-ai/dsh-pwsh-local` 等 PowerShell 执行器为 `ctx.shell` 提供后端);除沙箱接口外,它逐项对应 bash 工具调用。使用 `run_in_background` 的运行会注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具收集/停止;托管的 `DSH_*` 环境来自 `@deepseek-ai/dsh-shell-env`。每次调用都在新进程中运行,不使用持久 PTY 会话。路径采用原生 `C:\...` 形式,变量采用 `$env:NAME`。 | | `@deepseek-ai/dsh-tool-cordis` | `cordis_inspect_list`, `cordis_inspect_query` | `ctx.tools`, `ctx.cordisInspect` | `tool/call`, `tool/result` | - | 创造模式提供两个只读运行时检查工具。Cordis host runner 提供检查注册表;Client 查询需要已连接页面。持久化变更编写为组合包,再通过 plugin_manager 安装。 | -| `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 bash 工具;部署组合提供 PTY 后端,并可覆盖面向模型的环境描述。 | -| `@deepseek-ai/dsh-tool-pwsh-persistent` | `pwsh` | `ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 pwsh 工具,持久 bash 工具的 Windows 对应物;部署组合提供 pwsh 方言的 PTY 后端,并可覆盖面向模型的环境描述。 | +| `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.workingDirectory`、`ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 bash 工具;部署组合提供 PTY 后端,并可覆盖面向模型的环境描述。 | +| `@deepseek-ai/dsh-tool-pwsh-persistent` | `pwsh` | `ctx.workingDirectory`、`ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 pwsh 工具,持久 bash 工具的 Windows 对应物;部署组合提供 pwsh 方言的 PTY 后端,并可覆盖面向模型的环境描述。 | | `@deepseek-ai/dsh-tool-str-replace-editor` | `str_replace_editor` | `ctx.tools`、`ctx.fs` | `tool/call`、`fs/observed after view presence/absence, edit absence, or successful mutation`、`tool/result` | - | 基于文件系统 seam 的独立查看/创建/唯一字面量替换/按行插入工具;可与任何 shell 或终端接口组合。 | -| `@deepseek-ai/dsh-tool-fs` | `edit`、`read`、`read_image`、`write` | `ctx.tools`、`ctx.fs`、`ctx.systemPrompt`、`ctx.attachments (image-tool registration)`、`ctx.llm + an image-capable route (image-tool execution)` | `tool/call`、`fs/write-intent or fs/edit-intent for mutations`、`fs/observed after read presence/absence or successful file operation`、`durable attachment (read_image)`、`tool/result` | - | 先读后写/编辑策略由 `@deepseek-ai/dsh-fs-observation-policy` 添加;它是一个 `fs/*` 事件门禁插件,不会改变 schema。加载这些工具的部署按预期也应加载该插件。没有 `ctx.attachments` 时图片工具不会注册;其 schema 与路由无关,执行时除非确切路由的模型声明图片输入,否则拒绝。 | -| `@deepseek-ai/dsh-tool-fs-search` | `glob`、`grep` | `ctx.tools`、`ctx.subprocess`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn 随包提供的 ripgrep 二进制文件(`@vscode/ripgrep`),并作为普通前台调用运行,绝不作为后台任务;无需在宿主机安装 `rg`,也不经过 shell 层。本目录使用 `sampleOverCapGlobResults: true`;部署必须显式选择该行为。结果超过上限时,会通过可选的 ctx.spillStore 后端保存完整的格式化列表;在共置部署中,如果后端公开本地路径,返回的定位信息可供后续读取/搜索。 | -| `@deepseek-ai/dsh-tool-terminal` | `terminal_close`、`terminal_list`、`terminal_open`、`terminal_read`、`terminal_send`、`terminal_signal` | `ctx.tools`、`ctx.terminals`、`ctx.systemPrompt`、`ctx.jobs at call time for run_in_background` | `tool/call`、`tool/result` | - | 这 6 个终端工具需要选择启用,用于补充一次性 bash/文件系统工具。`terminal_send(run_in_background: true)` 会注册到 `ctx.jobs`;schema 不包含 TUI、具名按键序列、BEL、调整尺寸、自动启动和跨 agent 共享。 | +| `@deepseek-ai/dsh-tool-fs` | `edit`、`read`、`read_image`、`write` | `ctx.workingDirectory`、`ctx.tools`、`ctx.fs`、`ctx.systemPrompt`、`ctx.attachments (image-tool registration)`、`ctx.llm + an image-capable route (image-tool execution)` | `tool/call`、`fs/write-intent or fs/edit-intent for mutations`、`fs/observed after read presence/absence or successful file operation`、`durable attachment (read_image)`、`tool/result` | - | 先读后写/编辑策略由 `@deepseek-ai/dsh-fs-observation-policy` 添加;它是一个 `fs/*` 事件门禁插件,不会改变 schema。加载这些工具的部署按预期也应加载该插件。没有 `ctx.attachments` 时图片工具不会注册;其 schema 与路由无关,执行时除非确切路由的模型声明图片输入,否则拒绝。 | +| `@deepseek-ai/dsh-tool-fs-search` | `glob`、`grep` | `ctx.workingDirectory`、`ctx.tools`、`ctx.subprocess`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn 随包提供的 ripgrep 二进制文件(`@vscode/ripgrep`),并作为普通前台调用运行,绝不作为后台任务;无需在宿主机安装 `rg`,也不经过 shell 层。本目录使用 `sampleOverCapGlobResults: true`;部署必须显式选择该行为。结果超过上限时,会通过可选的 ctx.spillStore 后端保存完整的格式化列表;在共置部署中,如果后端公开本地路径,返回的定位信息可供后续读取/搜索。 | +| `@deepseek-ai/dsh-tool-terminal` | `terminal_close`、`terminal_list`、`terminal_open`、`terminal_read`、`terminal_send`、`terminal_signal` | `ctx.workingDirectory`、`ctx.tools`、`ctx.terminals`、`ctx.systemPrompt`、`ctx.jobs at call time for run_in_background` | `tool/call`、`tool/result` | - | 这 6 个终端工具需要选择启用,用于补充一次性 bash/文件系统工具。`terminal_send(run_in_background: true)` 会注册到 `ctx.jobs`;schema 不包含 TUI、具名按键序列、BEL、调整尺寸、自动启动和跨 agent 共享。 | | `@deepseek-ai/dsh-tool-goal` | `create_goal`、`get_goal`、`update_goal` | `ctx.tools`、`ctx.agents`、`ctx.goals`、`ctx.systemPrompt`、`a calling Agent in an authorized open turn` | `tool/call`、`goal/change for mutations`、`tool/result` | - | create、edit、pause 和 resume 要求直接来自人类的根权限;complete 和 blocked 也接受确切的当前 Goal Round。blocked 的默认下限是 3 个获准的 Round。 | | `@deepseek-ai/dsh-tool-schedule` | `schedule_create`、`schedule_delete`、`schedule_list`、`schedule_update` | `ctx.tools`、`ctx.schedule` | `tool/call`、Schedule storage domain 创建、更新或删除、`tool/result` | - | preset 或 Agent scope 挂载本包;由 preset 决定哪些 agent 获得这四个管理工具。每次调用都作用于调用方 Agent 的 Session。接受 after_seconds、显式绝对 at、有界固定速率 every_seconds、带显式 IANA 时区的每日与每周本地时间,以及作为五字段表达式的 cron。管理使用宿主 storage domain;到期消息会恢复原 Session。 | -| `@deepseek-ai/dsh-tool-lsp` | `lsp` | `ctx.tools`、`ctx.lsp`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,因此其模型可见 schema 在更换提供方时保持稳定。运行时要求已注册提供方,例如 `@deepseek-ai/dsh-lsp-stdio`;如果没有提供方,查询会返回结构化 `LSP_UNAVAILABLE` 错误,而不会改变 schema。 | +| `@deepseek-ai/dsh-tool-lsp` | `lsp` | `ctx.workingDirectory`、`ctx.tools`、`ctx.lsp`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,因此其模型可见 schema 在更换提供方时保持稳定。运行时要求已注册提供方,例如 `@deepseek-ai/dsh-lsp-stdio`;如果没有提供方,查询会返回结构化 `LSP_UNAVAILABLE` 错误,而不会改变 schema。 | | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`、`ctx.workflowEngine`、`ctx.subagents`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents every fresh round)` | `tool/call`、`tool/result`、`workflow and child session events during execution` | - | 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 | -| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.tools`、`ctx.agents`、`ctx.skills` | `tool/call`、`tool/result`、`user/message replacement catalogs via agent.inject()` | - | - | +| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.workingDirectory`、`ctx.tools`、`ctx.agents`、`ctx.skills` | `tool/call`、`tool/result`、`user/message replacement catalogs via agent.inject()` | - | - | | `@deepseek-ai/dsh-tool-session-query` | `session_event_read`、`session_event_search`、`session_event_trace`、`session_search`、`session_trace` | `ctx.tools`、`ctx.systemPrompt`、`ctx.sessionQuery`、`a calling Agent for workspace authority` | `tool/call`、`tool/result` | - | 这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。 | -| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 则展示为已启用 Session 中可用的固定配套工具。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。每个实例通过 `modelSelectionSettings`、`backgroundMode` 与 `enableRunInBackground` 独立控制是否读取模型选择设置及其后台行为。 | -| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是控制可继续后台 subagent 的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 | -| `@deepseek-ai/dsh-tool-jobs` | `job_kill`、`job_list`、`job_output` | `ctx.tools`、`ctx.jobs`、`ctx.systemPrompt` | `tool/call`、`tool/result`、`user/message via agent.inject() for background completion notices` | - | 与任务种类无关的后台任务控制器:后台 bash 命令、PTY 发送和 subagent 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 | +| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 则展示为已启用 Session 中可用的固定配套工具。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | +| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 | +| `@deepseek-ai/dsh-tool-jobs` | `job_kill`、`job_list`、`job_output` | `ctx.tools`、`ctx.jobs`、`ctx.systemPrompt` | `tool/call`、`tool/result`、`user/message via agent.inject() for background completion notices` | - | 与任务种类无关的后台任务控制器:后台 bash 命令与 PTY 发送 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 | | `@deepseek-ai/dsh-experimental-tool-agent-team` | `interrupt_agent`、`list_agents`、`send_message`、`spawn_teammate`、`team_task_create`、`team_task_get`、`team_task_list`、`team_task_update`、`wait_agent` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agentTeams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/message/queued`、`team/message/delivered`、`team/task`、`tool/result` | - | 这 9 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 | | `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`、`owning Agent session` | `tool/call`、`todo/write`、`tool/result` | - | todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为检查清单。`allowParallelInProgress` 是没有默认值的必填项,因此本目录明确选择 `true`,对应描述允许同时存在多个 `in_progress` 项。选择 `false` 的部署会获得同一工具,但描述会要求只能有 1 个活动任务。 | +| `@deepseek-ai/dsh-tool-working-directory` | `working_directory` | `ctx.tools`, `ctx.workingDirectory` | `tool/call`, `working-directory/change`, `user context for directory changes`, `tool/result` | - | 读取或改变调用方 Session 的目录。已有 shell 和进程保留自己的目录;原始元数据和权限根目录保持不变。 | +| `@deepseek-ai/dsh-experimental-tool-worktree` | `create_worktree` | `ctx.tools`, `ctx.worktrees`, `a calling Agent` | `tool/call`, `Git branch and checkout`, `working-directory/change`, `tool/result` | - | 仅通过显式实验组合加载。在现有写权限下从固定的本地提交创建新分支与检出目录,再改变 Session 工作目录。离开时保留分支与检出目录。 | | `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`、`ctx.workflowEngine`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents the script children)` | `tool/call`、`tool/result` | - | - | | `@deepseek-ai/dsh-tool-workspace-dependencies` | `load_workspace_dependencies` | `ctx.tools` | `tool/call`, `tool/result` | - | - | | `@deepseek-ai/dsh-tool-web` | `web_fetch`、`web_search` | `ctx.tools`、`ctx.web`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | web_search 和 web_fetch 将提供方选择置于 ctx.web 之后,使模型可见 schema 在更换后端时保持稳定。 | @@ -78,7 +80,7 @@ }, "target": { "type": "string", - "description": "Plugin entry id, bundle package name, or installation spec, according to action." + "description": "Plugin entry id, bundle package name, or installation spec, according to action. For an Official catalog entry, pass list_bundles installTarget.spec to install_bundle." }, "enabled": { "type": "boolean", @@ -567,7 +569,7 @@ ask_user_question 默认保持原有阻塞行为;设置 `mode: timed` 后才 来源:[`packages/core/tools/src/ptc.ts`](../packages/core/tools/src/ptc.ts) -在 `mode: ptc`/`mode: both` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 PTC mode Agent Note)。在 `ptc` 下,它是注册表对协议格式的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。 +在 `mode: ptc` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 PTC mode Agent Note)。在 `ptc` 下,它是注册表对协议格式的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。 @@ -1124,7 +1126,7 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费 glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn 随包提供的 ripgrep 二进制文件(`@vscode/ripgrep`),并作为普通前台调用运行,绝不作为后台任务;无需在宿主机安装 `rg`,也不经过 shell 层。本目录使用 `sampleOverCapGlobResults: true`;部署必须显式选择该行为。结果超过上限时,会通过可选的 ctx.spillStore 后端保存完整的格式化列表;在共置部署中,如果后端公开本地路径,返回的定位信息可供后续读取/搜索。 - + ## `@deepseek-ai/dsh-tool-terminal` @@ -1147,7 +1149,7 @@ glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn } ``` -来源:[`packages/terminal/tool-terminal/src/index.ts`](../packages/terminal/tool-terminal/src/index.ts) +来源:[`packages/experimental/tool-terminal/src/index.ts`](../packages/experimental/tool-terminal/src/index.ts) ### `terminal_list` @@ -1160,7 +1162,7 @@ glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn } ``` -来源:[`packages/terminal/tool-terminal/src/index.ts`](../packages/terminal/tool-terminal/src/index.ts) +来源:[`packages/experimental/tool-terminal/src/index.ts`](../packages/experimental/tool-terminal/src/index.ts) ### `terminal_open` @@ -1180,7 +1182,7 @@ glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn }, "cwd": { "type": "string", - "description": "Initial working directory. Defaults to the deployment workspace root." + "description": "Initial working directory, relative to the Session current directory when not absolute." } }, "required": [ @@ -1189,7 +1191,7 @@ glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn } ``` -来源:[`packages/terminal/tool-terminal/src/index.ts`](../packages/terminal/tool-terminal/src/index.ts) +来源:[`packages/experimental/tool-terminal/src/index.ts`](../packages/experimental/tool-terminal/src/index.ts) ### `terminal_read` @@ -1218,7 +1220,7 @@ glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn } ``` -来源:[`packages/terminal/tool-terminal/src/index.ts`](../packages/terminal/tool-terminal/src/index.ts) +来源:[`packages/experimental/tool-terminal/src/index.ts`](../packages/experimental/tool-terminal/src/index.ts) ### `terminal_send` @@ -1252,7 +1254,7 @@ glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn } ``` -来源:[`packages/terminal/tool-terminal/src/index.ts`](../packages/terminal/tool-terminal/src/index.ts) +来源:[`packages/experimental/tool-terminal/src/index.ts`](../packages/experimental/tool-terminal/src/index.ts) ### `terminal_signal` @@ -1285,7 +1287,7 @@ glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn } ``` -来源:[`packages/terminal/tool-terminal/src/index.ts`](../packages/terminal/tool-terminal/src/index.ts) +来源:[`packages/experimental/tool-terminal/src/index.ts`](../packages/experimental/tool-terminal/src/index.ts) 这 6 个终端工具需要选择启用,用于补充一次性 bash/文件系统工具。`terminal_send(run_in_background: true)` 会注册到 `ctx.jobs`;schema 不包含 TUI、具名按键序列、BEL、调整尺寸、自动启动和跨 agent 共享。 @@ -1724,7 +1726,7 @@ preset 或 Agent scope 挂载本包;由 preset 决定哪些 agent 获得这四 lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,因此其模型可见 schema 在更换提供方时保持稳定。运行时要求已注册提供方,例如 `@deepseek-ai/dsh-lsp-stdio`;如果没有提供方,查询会返回结构化 `LSP_UNAVAILABLE` 错误,而不会改变 schema。 - + ## `@deepseek-ai/dsh-tool-ralph` @@ -1751,7 +1753,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, } ``` -来源:[`packages/workflow/tool-ralph/src/index.ts`](../packages/workflow/tool-ralph/src/index.ts) +来源:[`packages/experimental/tool-ralph/src/index.ts`](../packages/experimental/tool-ralph/src/index.ts) 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 @@ -1815,7 +1817,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, } ``` -来源:[`packages/session-query/tool-session-query/src/index.ts`](../packages/session-query/tool-session-query/src/index.ts) +来源:[`packages/experimental/tool-session-query/src/index.ts`](../packages/experimental/tool-session-query/src/index.ts) ### `session_event_search` @@ -1875,7 +1877,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, } ``` -来源:[`packages/session-query/tool-session-query/src/index.ts`](../packages/session-query/tool-session-query/src/index.ts) +来源:[`packages/experimental/tool-session-query/src/index.ts`](../packages/experimental/tool-session-query/src/index.ts) ### `session_event_trace` @@ -1900,7 +1902,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, } ``` -来源:[`packages/session-query/tool-session-query/src/index.ts`](../packages/session-query/tool-session-query/src/index.ts) +来源:[`packages/experimental/tool-session-query/src/index.ts`](../packages/experimental/tool-session-query/src/index.ts) ### `session_search` @@ -1993,7 +1995,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, } ``` -来源:[`packages/session-query/tool-session-query/src/index.ts`](../packages/session-query/tool-session-query/src/index.ts) +来源:[`packages/experimental/tool-session-query/src/index.ts`](../packages/experimental/tool-session-query/src/index.ts) ### `session_trace` @@ -2011,7 +2013,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, } ``` -来源:[`packages/session-query/tool-session-query/src/index.ts`](../packages/session-query/tool-session-query/src/index.ts) +来源:[`packages/experimental/tool-session-query/src/index.ts`](../packages/experimental/tool-session-query/src/index.ts) 这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。 @@ -2043,12 +2045,16 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `subagent` -将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。此调用默认等待结果。 +将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。请提供完整、独立的提示词,因为它看不到当前对话。此工具启动独立管理的 subagent 并立即返回其 id。运行时会在任务结束时通知你。子代理通过 `send_message` 汇报结果;你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。 ```json { "type": "object", "properties": { + "cwd": { + "type": "string", + "description": "Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent." + }, "description": { "type": "string", "description": "A short (3-5 word) description of the delegated task, for display." @@ -2056,10 +2062,6 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, "prompt": { "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." - }, - "run_in_background": { - "type": "boolean", - "description": "Run as a background job and return its id (collect with job_output, stop with job_kill). Defaults to false." } }, "required": [ @@ -2071,7 +2073,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 来源:[`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts) -注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 则展示为已启用 Session 中可用的固定配套工具。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。每个实例通过 `modelSelectionSettings`、`backgroundMode` 与 `enableRunInBackground` 独立控制是否读取模型选择设置及其后台行为。 +注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 则展示为已启用 Session 中可用的固定配套工具。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 @@ -2079,7 +2081,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `interrupt_agent` -请 subagent 停止当前工作。此调用不等待其停止即返回。之后可以用 send_message 继续与直接子级的对话。它启动的 subagent 会继续运行。 +根据 agent id 请求取消后台 agent 的当前轮次。目标可以是你的直接子级,也可以是在你下方创建的更深层 agent。对于本地 agent,只有当前轮次会停止:已经排队发给该 agent 的消息会一直搁置到后续的 send_message;它启动的 agent 会继续运行;该 agent 本身仍可接受后续操作。外部执行会永久停止,无法接收后续消息。停止请求被接受后,此调用立即返回,因此目标可能还会短暂运行;中断一个已经完成的 agent 是可接受的空操作。 ```json { @@ -2100,7 +2102,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `list_agents` -列出你启动的 subagent 及其 id、标签和状态。running 表示正在工作;inactive 表示当前未在工作。subagent 完成时你会收到通知,无需反复查看状态。使用 send_message 继续对话。 +按持久 ID 和标签列出调用方目录中可继续的直接子代理。使用此工具回忆已启动的子代理,不要轮询完成状态;完成时会收到通知。状态来自在线注册表:running 表示当前正在工作;inactive 表示当前没有执行轮次,无论子代理已加载还是需要恢复,并不表示任务完成、成功、失败或正在等待其他代理。`send_message` 可在最近的步骤边界引导正在运行的子代理,或启动、恢复非活跃子代理的轮次;任何状态下的直接子级都可作为 `send_message` 的候选目标。列表快照不保证投递成功,`send_message` 仍进行权威检查,且可能失败。 ```json { @@ -2122,7 +2124,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `send_message` -向某个 agent 发送消息。工作中的 agent 会在下一个 step 收到消息;空闲的 agent 会以该消息开始新一轮。返回投递确认,而不是该 agent 的答案。 +根据 agent id 向直接可继续 child 发送消息。如果你是驻留的可继续 child,也可以把自己的直接 parent 作为目标。如果目标仍在工作,消息会 steer 其最近的 step;如果目标处于 idle,消息会启动一个轮次。此调用不会返回该 agent 的答案,只会确认消息已投递。调用失败表示消息**未**投递。 ```json { @@ -2146,7 +2148,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 来源:[`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) -这些是控制可继续后台 subagent 的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 +这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 @@ -2219,7 +2221,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 来源:[`packages/jobs/tool-jobs/src/index.ts`](../packages/jobs/tool-jobs/src/index.ts) -与任务种类无关的后台任务控制器:后台 bash 命令、PTY 发送和 subagent 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 +与任务种类无关的后台任务控制器:后台 bash 命令与 PTY 发送 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 @@ -2562,6 +2564,58 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为检查清单。`allowParallelInProgress` 是没有默认值的必填项,因此本目录明确选择 `true`,对应描述允许同时存在多个 `in_progress` 项。选择 `false` 的部署会获得同一工具,但描述会要求只能有 1 个活动任务。 + + +## `@deepseek-ai/dsh-tool-working-directory` + +### `working_directory` + +读取当前工作目录,或通过 cd 改变它。相对路径使用当前目录。已有 shell 和运行中的进程保留自己的目录。 + +```json +{ + "type": "object", + "properties": { + "cd": { + "type": "string", + "description": "Existing directory to enter. Omit to read the current directory." + } + } +} +``` + +来源:[`packages/session/tool-working-directory/src/index.ts`](../packages/session/tool-working-directory/src/index.ts) + +读取或改变调用方 Session 的目录。已有 shell 和进程保留自己的目录;原始元数据和权限根目录保持不变。 + + + +## `@deepseek-ai/dsh-experimental-tool-worktree` + +### `create_worktree` + +从本地提交、分支或标签创建新的 Git 分支与工作树,再将本 Session 的工作目录切换到该位置。默认使用 HEAD 和自动生成的名称。未提交文件保留在源检出目录中。名称已存在时失败。使用 working_directory 的 cd 参数离开;检出目录与分支仍然保留。 + +```json +{ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "New branch name, also used as the checkout directory. Omit to generate a unique name." + }, + "from": { + "type": "string", + "description": "Local commit, branch, or tag to start from. Defaults to HEAD; no fetch is performed." + } + } +} +``` + +来源:[`packages/experimental/tool-worktree/src/index.ts`](../packages/experimental/tool-worktree/src/index.ts) + +仅通过显式实验组合加载。在现有写权限下从固定的本地提交创建新分支与检出目录,再改变 Session 工作目录。离开时保留分支与检出目录。 + ## `@deepseek-ai/dsh-tool-workflow` diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/experimental-package-composition/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/experimental-package-composition/guide.i18n.yaml new file mode 100644 index 0000000000..92dd85b5a7 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/experimental-package-composition/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/experimental-package-composition/guide.md +/: + en: ba51914a407e33ae + zh: f0440eb120093d36 +/explicit-installation-and-composition-for-optional-capabilities: + en: 5a3fcd469d908d58 + zh: df94cc0beddc20ef +/explicit-installation-and-composition-for-optional-capabilities/change: + en: 3143b032a22268ec + zh: e55603836a24022a +/explicit-installation-and-composition-for-optional-capabilities/migration: + en: e197a95e2e59af22 + zh: dfdda504922c1dc5 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/experimental-package-composition/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/experimental-package-composition/guide.md new file mode 100644 index 0000000000..678146bfc1 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/experimental-package-composition/guide.md @@ -0,0 +1,28 @@ +--- +kind: upgrade-guide +description: "Explicit installation, peer dependencies, and configuration for optional hooks, webhooks, tools, and skills." +--- + +# Explicit installation and composition for optional capabilities + +English | [中文](guide.zh.md) + +## Change + +The CLI and Python runtime omit the hook bridges from their installation dependencies, and the CLI omits the webhook pair. Base and the full Web presets omit dormant Ralph rows, and base omits its dormant badge row. A patch that only enables a removed row does not insert its plugin. Default active behavior, plugin registration names, tool names, and recorded Session event names remain unchanged. + +Package ownership and naming follow the [experimental package policy](../../../../scripts/experimental-package-policy.ts). Installation and composition are separate: installing a plugin package does not mount it, and a bundle must be selected before its contributed rows can be configured. + +## Migration + +1. Install any packages missing from the target profile together with their required peers. Profiles disable automatic peer installation. For example, run `dsh plugin --profile web add @deepseek-ai/dsh-hooks-claude-code @deepseek-ai/dsh-hook-protocol`. The Codex bridge needs the same protocol peer; the GitHub adapter needs `@deepseek-ai/dsh-webhook` alongside `@deepseek-ai/dsh-webhook-github`. +2. Select an available bundle before overriding its rows. For a capability composed manually, replace an override of a removed row with a complete insertion. Add preset-local tools inside that preset's `config.plugins`, including the services they require. A Host badge insertion is: + + ```yaml + - insert: + - id: skill-badge + name: '@deepseek-ai/dsh-skill-badge' + ``` + +3. For GitHub review, use the example distributed with the adapter instead of the removed CLI example. Follow the [installed-profile instructions](../../../user/guide/github-review.md); keep customized rule modules inside the profile so their imports resolve its installed packages. +4. Start the profile and confirm that its configured plugins activate without missing-package, missing-peer, or missing-row errors. Confirm the expected tool or skill is available in the intended Agent preset. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/experimental-package-composition/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/experimental-package-composition/guide.zh.md new file mode 100644 index 0000000000..4ca4f3cf51 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/experimental-package-composition/guide.zh.md @@ -0,0 +1,28 @@ +--- +kind: upgrade-guide +description: "可选 hooks、webhooks、工具与技能所需的显式安装、peer 依赖和配置。" +--- + +# 可选能力的显式安装与组合 + +[English](guide.md) | 中文 + +## 变更 + +CLI 与 Python 运行时的安装依赖不再包含 hook 桥接,CLI 也不再包含 webhook 包对。Base 与完整 Web 预设省略休眠的 Ralph 行,base 也省略休眠的徽章行。仅启用已移除行的 patch 不会插入其插件。默认活动行为、插件注册名称、工具名称与已记录的 Session 事件名称保持不变。 + +包归属和命名遵循[实验包策略](../../../../scripts/experimental-package-policy.ts)。安装与组合相互独立:安装插件包不会挂载它,必须先选择 bundle 才能配置其贡献的行。 + +## 迁移 + +1. 将目标 profile 缺少的包与所需 peer 一起安装。Profile 禁用自动 peer 安装。例如,运行 `dsh plugin --profile web add @deepseek-ai/dsh-hooks-claude-code @deepseek-ai/dsh-hook-protocol`。Codex 桥接需要相同的协议 peer;GitHub 适配器需要同时安装 `@deepseek-ai/dsh-webhook` 与 `@deepseek-ai/dsh-webhook-github`。 +2. 覆盖 bundle 的行之前先选择可用的 bundle。对于手动组合的能力,将覆盖已移除行的 patch 替换为完整插入。在预设的 `config.plugins` 中添加预设内工具及其所需服务。Host 徽章插入示例: + + ```yaml + - insert: + - id: skill-badge + name: '@deepseek-ai/dsh-skill-badge' + ``` + +3. GitHub review 使用适配器随附的示例,替代已删除的 CLI 示例。遵循[安装版 profile 说明](../../../user/guide/github-review.zh.md);将定制规则模块保留在 profile 内,使其导入能解析该 profile 安装的包。 +4. 启动 profile,确认配置的插件激活时没有缺包、缺 peer 或缺行错误。确认预期工具或技能在目标 Agent 预设中可用。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/instruction-home-override/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/instruction-home-override/guide.i18n.yaml new file mode 100644 index 0000000000..fe7644b98d --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/instruction-home-override/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/instruction-home-override/guide.md +/: + en: 7081d888aedd32d2 + zh: 167263e5e4e514cc +/instruction-loading-resolves-one-harness-home: + en: b308584aa55910bd + zh: 910cc6b2b7f9b3ea +/instruction-loading-resolves-one-harness-home/change: + en: afd003587be927b4 + zh: 15dcd2ec55a5c968 +/instruction-loading-resolves-one-harness-home/migration: + en: 483d23f92ad84a8c + zh: 3c2648ebf2156b34 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/instruction-home-override/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/instruction-home-override/guide.md new file mode 100644 index 0000000000..4c8f156407 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/instruction-home-override/guide.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "The `agent-instructions` row no longer accepts `dshHome`; instruction loading always resolves the harness home from `$DSH_HOME` or `~/.dsh`." +--- + +# Instruction loading resolves one harness home + +English | [中文](guide.zh.md) + +## Change + +`@deepseek-ai/dsh-agent-instructions` removed its `dshHome` config field. The plugin resolves the harness home through `resolveDshHome()` from `@deepseek-ai/dsh-home-paths`, like every other harness consumer: the home is `$DSH_HOME` when it is set and non-blank, otherwise `~/.dsh`, with tilde expansion. + +A `cordis.yml`, profile patch, or `--patch` overlay that still sets `dshHome` on the `agent-instructions` row has no effect: the row keeps the stray key and the plugin ignores it, so instruction loading follows the process home. Other providers, such as `dsh-skill-filesystem` and `dsh-shell-env`, keep their own home fields; this removal covers the instruction loader only. + +## Migration + +1. Delete `dshHome` from the `agent-instructions` row in `cordis.yml`, `$DSH_HOME/cordis.patch.yml`, `$DSH_HOME/profiles//cordis.patch.yml`, and any `--patch` overlay. +2. Where the override pointed at a non-default home, export `DSH_HOME` for the process instead, so every harness consumer reads that home. +3. Confirm with `dsh --profile --dump-config` that the `agent-instructions` row lists only its remaining fields, and that a new session's first request shows `Instructions from: $DSH_HOME/AGENTS.md` (or `~/.dsh/AGENTS.md`) when that file exists. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/instruction-home-override/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/instruction-home-override/guide.zh.md new file mode 100644 index 0000000000..9af456de67 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/instruction-home-override/guide.zh.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "`agent-instructions` 行不再接受 `dshHome`;指令加载始终从 `$DSH_HOME` 或 `~/.dsh` 解析 harness home。" +--- + +# 指令加载只解析一个 harness home + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-agent-instructions` 移除了 `dshHome` 配置字段。该插件通过 `@deepseek-ai/dsh-home-paths` 的 `resolveDshHome()` 解析 harness home,与 harness 其他消费方一致:`$DSH_HOME` 已设置且非空白时以它为准,否则使用 `~/.dsh`,并展开波浪号。 + +`cordis.yml`、profile patch 或 `--patch` overlay 中 `agent-instructions` 行上残留的 `dshHome` 不再有任何效果:该行会保留这个多余键,插件忽略它,指令加载跟随进程 home。其他 provider(如 `dsh-skill-filesystem`、`dsh-shell-env`)保留各自的 home 字段;本次删除只覆盖指令加载器。 + +## 迁移 + +1. 从 `cordis.yml`、`$DSH_HOME/cordis.patch.yml`、`$DSH_HOME/profiles//cordis.patch.yml` 以及任何 `--patch` overlay 的 `agent-instructions` 行中删除 `dshHome`。 +2. 若该覆盖原本指向非默认 home,改为在进程中导出 `DSH_HOME`,让所有 harness 消费方读取同一个 home。 +3. 用 `dsh --profile --dump-config` 确认 `agent-instructions` 行只剩其余字段,并确认新会话的首次请求在该文件存在时显示 `Instructions from: $DSH_HOME/AGENTS.md`(或 `~/.dsh/AGENTS.md`)。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/mock-messages-events/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/mock-messages-events/guide.i18n.yaml new file mode 100644 index 0000000000..abb5d6e7be --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/mock-messages-events/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/mock-messages-events/guide.md +/: + en: 50f48b2d699ff15d + zh: 3767619b47fd76fb +/read-named-events-from-the-messages-mock-server: + en: d97603a00507734c + zh: 6cfec46c21e97b70 +/read-named-events-from-the-messages-mock-server/change: + en: f03d6e1d637f6397 + zh: cabeac0144d33ac4 +/read-named-events-from-the-messages-mock-server/migration: + en: 7528082a3376359e + zh: 0e8eaad875394baa diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/mock-messages-events/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/mock-messages-events/guide.md new file mode 100644 index 0000000000..eabae9f724 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/mock-messages-events/guide.md @@ -0,0 +1,17 @@ +--- +kind: upgrade-guide +description: "Messages mock streams publish named SSE events for native SDK consumers." +--- + +# Read named events from the Messages mock server + +English | [中文](guide.zh.md) + +## Change + +The next release makes `@deepseek-ai/dsh-llm-mock-server` emit the Messages event name beside each structured SSE payload. Native SDKs require these names to process message and content-block events. Consumers that accept only unnamed SSE messages must update their event handling. Scripted payloads, ordering, request capture, and chunk counts are unchanged; the deliberately malformed JSON behavior still sends its invalid data directly. + +## Migration + +1. Consume the named Messages events and parse their `data` fields. Do not filter the stream to unnamed events. +2. Verify a complete text response, tool-call response, and the fault behaviors your test uses. The [mock server reference](../../../../packages/test-support/llm-mock-server/README.md) describes the available scripts. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/mock-messages-events/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/mock-messages-events/guide.zh.md new file mode 100644 index 0000000000..126c489e03 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/mock-messages-events/guide.zh.md @@ -0,0 +1,17 @@ +--- +kind: upgrade-guide +description: "Messages 模拟流为原生 SDK 消费方发布具名 SSE 事件。" +--- + +# 读取 Messages 模拟服务器的具名事件 + +[English](guide.md) | 中文 + +## 变更 + +下一版本的 `@deepseek-ai/dsh-llm-mock-server` 在每个结构化 SSE 载荷旁发送 Messages 事件名。原生 SDK 需要这些名称才能处理消息与内容块事件。只接受无名 SSE 消息的消费方必须更新事件处理。脚本载荷、顺序、请求捕获与分片计数保持不变;刻意生成畸形 JSON 的行为仍直接发送无效数据。 + +## 迁移 + +1. 消费具名 Messages 事件并解析其 `data` 字段。不要将流过滤为仅含无名事件。 +2. 验证完整文本响应、工具调用响应和测试使用的故障行为。[模拟服务器参考](../../../../packages/test-support/llm-mock-server/README.zh.md)说明了可用脚本。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/native-subagent-bundle-tools/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/native-subagent-bundle-tools/guide.i18n.yaml new file mode 100644 index 0000000000..870c91d93d --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/native-subagent-bundle-tools/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/native-subagent-bundle-tools/guide.md +/: + en: a77928d6130fe601 + zh: da46d612268c3ced +/native-subagent-bundles-include-preset-tools: + en: b5c79b41fc34574a + zh: d784adcf343b0817 +/native-subagent-bundles-include-preset-tools/change: + en: 7d4af3e4ab91ef72 + zh: 837377c96d5eee12 +/native-subagent-bundles-include-preset-tools/migration: + en: 3257a5d18d07cada + zh: b663a7ae020bbfee diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/native-subagent-bundle-tools/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/native-subagent-bundle-tools/guide.md new file mode 100644 index 0000000000..5a08637440 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/native-subagent-bundle-tools/guide.md @@ -0,0 +1,32 @@ +--- +kind: upgrade-guide +description: "Claude Code and Codex bundles add delegation tools to full Web presets while retaining their package and row identities." +--- + +# Native subagent bundles include preset tools + +English | [中文](guide.zh.md) + +## Change + +`@deepseek-ai/dsh-subagent-claude-code` and `@deepseek-ai/dsh-subagent-codex` keep their package names, bundle selections, provider row IDs, configuration, and native authentication. In Web and Desktop, a selected bundle now adds enabled delegation tools to `preset-standard`, `preset-cordis`, and `preset-ptc`; previously the bundle registered only its provider. Existing Agents retain their composition; new Agents and subsequently reopened Sessions receive the selected tools. + +The contributed rows retain `tool-subagent-claude-code` and `tool-subagent-codex`. Later user layers can override their configuration or disabled state, and a complete preset configuration replacement keeps its own child list. An override does not select an unselected bundle. Duplicate declarations of the same model-facing tool still fail. + +Delegation follows the shared managed-activation API: the tool returns a child ID, and completion arrives as a logged notice to the parent Agent. Remove obsolete `backgroundMode` settings; `maxDepth: provider-managed` remains supported for these native providers. + +## Migration + +1. Preserve selected bundle names, provider configuration, and authentication. Existing provider-only packages need **Update** in Plugins → Official to receive the tool-contributing bundle; restart when requested. To adopt a new provider, enable its card. [Installation targets](../../../../packages/boot/plugin-manager/README.md#use-this-package) follow the published DSH version or source checkout; equal version strings do not make an older registry package equivalent to a development link. Off only deselects; Remove remains separate. +2. Configure the familiar tool row ID with a scoped patch. Configuration overrides still replace the complete configuration. For example, this keeps Codex disabled in standard while other full presets use their selected composition: + + ```yaml + - preset: preset-standard + id: tool-subagent-codex + disabled: true + ``` + +3. If an additional manual row registers the same tool in a preset, remove that redundant row or disable the bundle's contribution explicitly. A copied complete preset can retain its existing tool declarations and disabled state. Verify the effective tool catalog in a new Session; minimal keeps its current shell and working-directory tools. +4. Plugin clients receive required `BundleInfo.official` and `BundleInfo.availability` fields; detail-slot `PluginPackageRef` receives them too. `official` identifies project maintenance, `availability` reports `installation`, `profile`, or `missing`, and `installed` still means a profile dependency declaration. Detail-slot readers that only display existing fields need no changes; constructors and exact validators must include the new fields. Optional `installTarget` identifies the offered version and registry or local-link spec. + +**Other profiles.** Headless, SDK, ACP, and custom profiles without the three full preset targets are outside this compatibility scope. Selecting these bundle layers fails for missing preset targets and can retain the saved selection; there is no automatic rollback. Such profiles can keep the installed provider package without selecting its bundle and explicitly compose provider/tool rows using the [Codex](../../../../packages/subagent/subagent-codex/README.md#exposing-the-tool) or [Claude Code](../../../../packages/subagent/subagent-claude-code/README.md#exposing-the-tool) example. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/native-subagent-bundle-tools/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/native-subagent-bundle-tools/guide.zh.md new file mode 100644 index 0000000000..e98a34f503 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/native-subagent-bundle-tools/guide.zh.md @@ -0,0 +1,32 @@ +--- +kind: upgrade-guide +description: "Claude Code 与 Codex bundle 为完整 Web 预设添加委派工具,同时保留包名和行标识。" +--- + +# 原生子代理 bundle 包含预设工具 + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-subagent-claude-code` 和 `@deepseek-ai/dsh-subagent-codex` 保留包名、bundle 选择、提供方行 ID、配置及原生认证。在 Web 和 Desktop 中,已选中的 bundle 现在会为 `preset-standard`、`preset-cordis` 和 `preset-ptc` 添加启用的委派工具;此前 bundle 只注册提供方。已有 Agent 保留其组合;新 Agent 和之后重新打开的 Session 使用选中的工具。 + +贡献行保留 `tool-subagent-claude-code` 和 `tool-subagent-codex`。后续用户层可以覆盖其配置或禁用状态,完整的预设配置替换保留自己的子列表。覆盖不会选中未选中的 bundle。重复声明同一个面向模型的工具仍会失败。 + +委派遵循共享的托管 activation API:工具返回子级 ID,完成结果通过已记录的通知发送给父 Agent。移除已废弃的 `backgroundMode` 设置;这些原生提供方仍支持 `maxDepth: provider-managed`。 + +## 迁移 + +1. 保留已选中的 bundle 名称、提供方配置和认证。已有仅提供方的包需要在插件 → 官方中**更新**才能获得贡献工具的 bundle;按提示重启。要使用新的提供方,启用其卡片。[安装目标](../../../../packages/boot/plugin-manager/README.zh.md#use-this-package)对应已发布的 DSH 版本或源码 checkout;版本字符串相同不代表旧注册表包等同于开发链接。关闭只取消选择;删除仍为独立操作。 +2. 使用作用域补丁配置熟悉的工具行 ID。配置覆盖仍会替换整份配置。例如,以下补丁在 standard 中保持 Codex 禁用,其他完整预设则使用其选中的组合: + + ```yaml + - preset: preset-standard + id: tool-subagent-codex + disabled: true + ``` + +3. 如果额外的手动行在预设中注册了同一个工具,移除该重复行,或显式禁用 bundle 的贡献行。复制的完整预设可以保留其现有工具声明和禁用状态。在新 Session 中验证有效工具目录;minimal 保留当前的 shell 和工作目录工具。 +4. 插件客户端收到必填的 `BundleInfo.official` 和 `BundleInfo.availability` 字段;详情 slot 的 `PluginPackageRef` 也会收到它们。`official` 标识项目维护,`availability` 表示 `installation`、`profile` 或 `missing`,`installed` 仍表示 profile 的依赖声明。只展示已有字段的详情 slot 读取方无需修改;构造方和精确校验器必须包含新增字段。可选的 `installTarget` 标识所提供的版本及注册表或本地链接 spec。 + +**其他 profile。** Headless、SDK、ACP 和缺少三个完整预设目标的自定义 profile 不在此次兼容范围内。选择这些 bundle 层会因缺少预设目标而失败,并可能保留已保存的选择;没有自动回滚。这些 profile 可以保留已安装的提供方包而不选择其 bundle,并参考 [Codex](../../../../packages/subagent/subagent-codex/README.zh.md#exposing-the-tool) 或 [Claude Code](../../../../packages/subagent/subagent-claude-code/README.zh.md#exposing-the-tool) 示例显式组合提供方和工具行。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/new-session-generations/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/new-session-generations/guide.i18n.yaml new file mode 100644 index 0000000000..01ecc3a8b0 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/new-session-generations/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/new-session-generations/guide.md +/: + en: ad37e45180bc096f + zh: e44c85b10b1d9d4b +/start-a-fresh-agent-with-new-session: + en: 5e4b26181326018f + zh: 99bc75dcd12bf647 +/start-a-fresh-agent-with-new-session/change: + en: ab12aadb31d0d3e8 + zh: 637b446aef86456e +/start-a-fresh-agent-with-new-session/migration: + en: 9565e66b6bcdf269 + zh: 226e470de85aa8b6 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/new-session-generations/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/new-session-generations/guide.md new file mode 100644 index 0000000000..bc735fe299 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/new-session-generations/guide.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "New Session creates a fresh Agent instead of reusing an existing empty Session." +--- + +# Start a fresh Agent with New Session + +English | [中文](guide.zh.md) + +## Change + +In Web and Desktop, New Session creates a fresh Session and Agent using the current preset definition. It previously reused an existing empty Session in the workspace, which could retain tools from before an optional bundle changed. + +Client calls to `uiWorkspace.startSession(workspaceId)` follow the same behavior. Workspace reconnection through `openWorkspace` or `connectWorkspace` still reuses an eligible blank. Calls with draft-preparation options preserve their existing reuse and content rules. Existing Agents retain their composition. + +## Migration + +1. After changing optional bundles, use New Session to obtain the current tool composition. Continue in an existing Session when its retained composition is intentional. +2. Client integrations that reconnect a workspace should use `openWorkspace` or `connectWorkspace`. Keep explicit draft-preparation options when preparing an existing draft. +3. Confirm that the new Agent has the selected tools; no persisted Session files or profile patches need rewriting. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/new-session-generations/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/new-session-generations/guide.zh.md new file mode 100644 index 0000000000..b3371908a1 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/new-session-generations/guide.zh.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "新会话会创建新的 Agent,不再复用已有空 Session。" +--- + +# 通过新会话创建新的 Agent + +[English](guide.md) | 中文 + +## 变更 + +在 Web 和 Desktop 中,新会话按当前预设定义创建新的 Session 和 Agent。此前该操作会复用工作区内已有的空 Session,因而可能保留可选组合包变更前的工具。 + +客户端调用 `uiWorkspace.startSession(workspaceId)` 采用相同行为。通过 `openWorkspace` 或 `connectWorkspace` 重连工作区时仍复用符合条件的空 Session。带草稿准备选项的调用保留已有复用与内容规则。已有 Agent 保留其组合。 + +## 迁移 + +1. 更改可选组合包后,通过新会话取得当前工具组合。需要保留原组合时,继续使用已有 Session。 +2. 重连工作区的客户端集成应使用 `openWorkspace` 或 `connectWorkspace`。准备已有草稿时保留显式草稿准备选项。 +3. 确认新 Agent 拥有已选择的工具;无需重写持久化 Session 文件或 profile patch。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.i18n.yaml new file mode 100644 index 0000000000..e632de9feb --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.md +/: + en: 3f9b48b2230ec6e4 + zh: c1aad0e9121d46a4 +/dsh-plugin-option-placement: + en: 3d717a2b7e9087d3 + zh: e6004d015e6a4829 +/dsh-plugin-option-placement/change: + en: 15fe8cf74db0ba53 + zh: c6ea1056aa7a6f77 +/dsh-plugin-option-placement/migration: + en: b7239597ebc4d970 + zh: fa45f851cf1f3af1 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.md new file mode 100644 index 0000000000..8bb89d56a9 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "dsh plugin reads --profile only before the forwarded pnpm arguments and prints its own help." +--- + +# `dsh plugin` option placement + +English | [中文](guide.zh.md) + +## Change + +`dsh plugin` reads `--profile` only before the forwarded pnpm arguments, which start at the first token it does not recognize, and it has its own help. Previously it accepted `--profile ` anywhere on the command line, had no help of its own, and forwarded `-h` and `--help` to pnpm from any position. + +When the forwarded arguments begin with the pnpm command, everything after that command now reaches pnpm verbatim, including `-h`, `--help`, and `--profile`. In any other position, including after a leading pnpm option such as `--filter `, `-h` or `--help` prints the `dsh plugin` help and exits 0. Scripts that place `--profile` after the pnpm command or after a leading pnpm option fail with `required option '--profile ' not specified`, and `dsh plugin --profile --help` no longer prints pnpm's help. + +## Migration + +1. Move `--profile ` before every pnpm argument: replace `dsh plugin add --profile ` with `dsh plugin --profile add `. +2. Request pnpm's help for one command after that command, with any pnpm options after it too, as in `dsh plugin --profile add --help`. For pnpm's general help, forward `help`: `dsh plugin --profile help`. +3. Confirm that `dsh plugin --help` prints `Usage: dsh plugin [options] [args...]` and that the commands from steps 1 and 2 reach pnpm. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.zh.md new file mode 100644 index 0000000000..50c71978cf --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.zh.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "dsh plugin 只在被转发的 pnpm 参数之前读取 --profile,并提供自己的帮助。" +--- + +# `dsh plugin` 选项位置 + +[English](guide.md) | 中文 + +## 变更 + +`dsh plugin` 只在被转发的 pnpm 参数之前读取 `--profile`,被转发的参数从它无法识别的第一个 token 开始;它还提供自己的帮助。此前它接受出现在命令行任意位置的 `--profile `,没有自己的帮助,并且无论 `-h` 和 `--help` 位于何处都会转发给 pnpm。 + +被转发的参数以 pnpm 命令开头时,该命令之后的所有内容现在都会原样交给 pnpm,包括 `-h`、`--help` 和 `--profile`。在其他任何位置,包括位于 `--filter ` 等开头的 pnpm 选项之后,`-h` 或 `--help` 都会打印 `dsh plugin` 的帮助并以 0 退出。把 `--profile` 写在 pnpm 命令或开头的 pnpm 选项之后的脚本会因 `required option '--profile ' not specified` 而失败,`dsh plugin --profile --help` 也不再打印 pnpm 的帮助。 + +## 迁移 + +1. 把 `--profile ` 移到所有 pnpm 参数之前:将 `dsh plugin add --profile ` 改为 `dsh plugin --profile add `。 +2. 查看某个 pnpm 命令的帮助时,把帮助选项和其他 pnpm 选项都写在该命令之后,例如 `dsh plugin --profile add --help`。查看 pnpm 的总体帮助时转发 `help`:`dsh plugin --profile help`。 +3. 确认 `dsh plugin --help` 打印 `Usage: dsh plugin [options] [args...]`,并且第 1、2 步中的命令能够到达 pnpm。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/preset-bundle-rows/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/preset-bundle-rows/guide.i18n.yaml new file mode 100644 index 0000000000..bf382c449c --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/preset-bundle-rows/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/preset-bundle-rows/guide.md +/: + en: 25de822f0f884eb9 + zh: c910706695f91c76 +/identify-bundle-rows-by-their-preset: + en: 95cdaba0de8fe664 + zh: f7382ff6b9f65a49 +/identify-bundle-rows-by-their-preset/change: + en: e43402c18f6b6de8 + zh: 32f3e46546e9d1f8 +/identify-bundle-rows-by-their-preset/migration: + en: 89cead6b9cd199bd + zh: 8a13a234d9feacbb diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/preset-bundle-rows/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/preset-bundle-rows/guide.md new file mode 100644 index 0000000000..225cf96268 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/preset-bundle-rows/guide.md @@ -0,0 +1,29 @@ +--- +kind: upgrade-guide +description: "Plugin-manager bundle inventories include preset-scoped rows whose row IDs are unique only within their preset." +--- + +# Identify bundle rows by their preset + +English | [中文](guide.zh.md) + +## Change + +The next release adds preset-scoped declarations to `PluginManager.listBundles().rows`. A bundle can declare the same `rowId` in several presets. `PluginRowRef` values passed to detail slots carry the same optional `preset` identity. These rows carry `preset`, optional `composition` state, and `readOnlyReason: 'preset-managed'`; they do not carry an editable root `entryId`. + +Clients that key rows only by bundle name and `rowId`, or treat every row as a root plugin switch, must distinguish preset declarations. Existing root rows retain their current identity and controls. + +## Migration + +1. Include `preset` in row identity, alongside the bundle name and `rowId`. Keep the absent-preset case distinct from every named preset. +2. Render `preset-managed` rows read-only in direct row controls. Use the bundle switch or an explicit scoped profile patch to configure them. Only pass a returned root `entryId` to `setPluginEnabled`. +3. Read preset row state from `composition`, instead of looking up a root Loader entry with the same `rowId`. +4. For YAML edits, use `preset` to address the outer declaration and `id` to address its child: + + ```yaml + - preset: preset-standard + id: optional-tool-str-replace-editor + disabled: true + ``` + +5. Verify that a bundle contributing to standard, cordis, and ptc shows three distinct rows and that root-row switches continue to work. The [profile patch reference](../../../../packages/boot/app-boot/README.md) describes ordering and validation. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/preset-bundle-rows/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/preset-bundle-rows/guide.zh.md new file mode 100644 index 0000000000..d2800b9591 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/preset-bundle-rows/guide.zh.md @@ -0,0 +1,29 @@ +--- +kind: upgrade-guide +description: "插件管理器的组合包清单包含预设作用域行,其行 ID 仅在所属预设内唯一。" +--- + +# 按所属预设区分组合包行 + +[English](guide.md) | 中文 + +## 变更 + +下一个版本在 `PluginManager.listBundles().rows` 中加入预设作用域声明。一个组合包可以在多个预设中声明相同的 `rowId`。详情插槽中的 `PluginRowRef` 也携带可选的 `preset` 标识。这些行包含 `preset`、可选的 `composition` 状态和 `readOnlyReason: 'preset-managed'`,不包含可编辑的根级 `entryId`。 + +仅用组合包名和 `rowId` 作为行标识,或将所有行都视为根插件开关的客户端,必须区分预设声明。已有根行保留当前标识和控制方式。 + +## 迁移 + +1. 将 `preset` 与组合包名、`rowId` 一起纳入行标识。区分没有预设的情况与每个具名预设。 +2. 在直接行控制中只读展示 `preset-managed` 行。通过组合包开关或显式的预设作用域 profile patch 配置这些行。只将实际返回的根级 `entryId` 传给 `setPluginEnabled`。 +3. 从 `composition` 读取预设行状态,不要按同一个 `rowId` 查找根 Loader 条目。 +4. 编辑 YAML 时,用 `preset` 定位外层声明,用 `id` 定位其子行: + + ```yaml + - preset: preset-standard + id: optional-tool-str-replace-editor + disabled: true + ``` + +5. 确认同时向 standard、cordis 和 ptc 贡献行的组合包显示三个不同的行,且根行开关仍可使用。[Profile patch 参考](../../../../packages/boot/app-boot/README.zh.md)说明顺序与校验规则。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/python-ptc-sandbox/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/python-ptc-sandbox/guide.i18n.yaml new file mode 100644 index 0000000000..57b9ee6c34 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/python-ptc-sandbox/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/python-ptc-sandbox/guide.md +/: + en: 37441fc2c29e3ee9 + zh: de434de0f9da7d40 +/python-ptc-applies-the-shared-file-sandbox: + en: 655754b417fa8699 + zh: 5571cb25b6621c07 +/python-ptc-applies-the-shared-file-sandbox/change: + en: a4575a117b53ce7c + zh: 2ea9f832b2179537 +/python-ptc-applies-the-shared-file-sandbox/migration: + en: faaefac69870d31f + zh: 08398580b7e3ac62 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/python-ptc-sandbox/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/python-ptc-sandbox/guide.md new file mode 100644 index 0000000000..a2545db4f8 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/python-ptc-sandbox/guide.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: "Python PTC compositions require sandbox services, and direct Python file operations follow the resolved file policy." +--- + +# Python PTC applies the shared file sandbox + +English | [中文](guide.zh.md) + +## Change + +`@deepseek-ai/dsh-experimental-ptc-runtime-python` requires `sandbox` and `sandboxPolicy`. Previously, direct Python file operations ran without confinement and the provider rejected explicit `sandboxPolicy` inputs. It now applies the shared policy to each program and reports confinement in `PtcRunResult.sandbox`. A restricted run fails with `sandbox-unavailable` when confinement cannot be established. + +This affects custom compositions that select the Python runtime. Shipped profiles continue to use the Node runtime. Python remains local to macOS or Linux; its configured resource budgets and unsupported per-call timeout overrides remain unchanged. + +## Migration + +1. In the `cordis.yml` or profile patch that selects the Python runtime, retain the existing shared sandbox services. If they are absent, mount `@deepseek-ai/dsh-sandbox-local` and `@deepseek-ai/dsh-sandbox-policy` with its required `@deepseek-ai/dsh-session-projection` service. See the [Python runtime requirements](../../../../packages/experimental/ptc-runtime-python/README.md#use-this-package). +2. Set the shared `sandbox-policy` row's `mode` and `workspaceRoot` for agentless calls. Its default mode is `read-only`; `workspace-write` permits workspace writes. PTC tool calls use the Session's resolved policy and directory. Do not add a separate Python-only sandbox setting. +3. Direct service callers use `runtime.run(runtime.resolve(request))`; pass a trusted `sandboxPolicy` only when an explicit override is required. Model-facing `run_code` uses the existing approval flow for a one-call wider mode, without changing nested tools' permissions or replaying the program automatically. +4. Verify a program can write inside the intended workspace under `workspace-write`, fails to write outside it, and returns `result.sandbox.mode` and `result.sandbox.enforcement`. Under `read-only`, direct file writes fail. Inspect `sandbox-unavailable` separately from a denied file operation and repair the local sandbox installation before retrying. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/python-ptc-sandbox/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/python-ptc-sandbox/guide.zh.md new file mode 100644 index 0000000000..9ed3b989b6 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/python-ptc-sandbox/guide.zh.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: "Python PTC 组合需要沙箱服务,且直接 Python 文件操作遵循已解析文件策略。" +--- + +# Python PTC 应用共享文件沙箱 + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-experimental-ptc-runtime-python` 需要 `sandbox` 和 `sandboxPolicy`。此前,直接 Python 文件操作不受约束,提供方拒绝显式 `sandboxPolicy` 输入。现在,它为每个程序应用共享策略,并通过 `PtcRunResult.sandbox` 报告约束情况。无法建立约束时,受限运行以 `sandbox-unavailable` 失败。 + +这影响选择 Python 运行时的自定义组合。已发布 profile 继续使用 Node 运行时。Python 仍限于本地 macOS 或 Linux;其配置的资源预算保持不变,仍不支持单次 timeout 覆盖。 + +## 迁移 + +1. 在选择 Python 运行时的 `cordis.yml` 或 profile patch 中,保留已有共享沙箱服务。若尚未提供,挂载 `@deepseek-ai/dsh-sandbox-local` 和 `@deepseek-ai/dsh-sandbox-policy`,以及后者所需的 `@deepseek-ai/dsh-session-projection` 服务。参见 [Python 运行时要求](../../../../packages/experimental/ptc-runtime-python/README.zh.md#use-this-package)。 +2. 为无 Agent 调用设置共享 `sandbox-policy` 行的 `mode` 和 `workspaceRoot`。默认模式是 `read-only`;`workspace-write` 允许工作区写入。PTC 工具调用使用 Session 的已解析策略和目录。不要新增 Python 专用沙箱设置。 +3. 直接服务调用方使用 `runtime.run(runtime.resolve(request))`;仅在需要显式覆盖时传入可信 `sandboxPolicy`。面向模型的 `run_code` 通过现有审批流程为一次调用扩大权限,不改变嵌套工具的权限,也不自动重放程序。 +4. 验证程序在 `workspace-write` 下可以写入预期工作区、写入工作区外时失败,并返回 `result.sandbox.mode` 和 `result.sandbox.enforcement`。在 `read-only` 下,直接文件写入失败。将 `sandbox-unavailable` 与文件操作被拒绝区分处理,修复本地沙箱安装后再重试。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.i18n.yaml new file mode 100644 index 0000000000..7167802a12 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.md +/: + en: 4f9ba8c8ed040a83 + zh: aabc458063878004 +/prepare-custom-title-routes: + en: cab86bb2cc427297 + zh: 7a14e268bf53fc73 +/prepare-custom-title-routes/change: + en: 278f593203887bbb + zh: 72949b90220f4a6f +/prepare-custom-title-routes/migration: + en: b7c439680f8a1ec8 + zh: 82b4243cb1b2ef0a diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.md new file mode 100644 index 0000000000..626851cf28 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.md @@ -0,0 +1,18 @@ +--- +kind: upgrade-guide +description: "Custom title routes require registered adapters and reasoning efforts in increasing order." +--- + +# Prepare custom title routes + +English | [中文](guide.zh.md) + +## Change + +Model-backed session titles prepare their requests through `ctx.llm.prepareCall()`. Compositions that serve title routes only through `llm/stream` middleware must also register an adapter. Custom adapters must list selectable reasoning efforts from least to greatest, rather than in arbitrary display order. Title policy selects the first entry through a configuration function. Existing concrete `prepareCall()` inputs remain valid. + +## Migration + +1. Register the title route with `ctx.llm.registerAdapter()`. Replay compositions can configure the existing `llm-replay` plugin's `providers` list. +2. In the adapter's model metadata, order `reasoning.efforts` from least to greatest selectable effort. Apply the same order to replay models' `reasoningEfforts`. Keep the configured default independent of list order. Omit reasoning metadata when the route has no selectable controls. +3. Generate a title and check that `session/title-llm-request` records the selected effort and a provider title is saved. Existing Session records without that optional field remain readable. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.zh.md new file mode 100644 index 0000000000..bbb0b370e6 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.zh.md @@ -0,0 +1,18 @@ +--- +kind: upgrade-guide +description: "自定义标题路由要求注册适配器,并按递增顺序排列推理强度。" +--- + +# 准备自定义标题路由 + +[English](guide.md) | 中文 + +## 变更 + +模型生成的会话标题通过 `ctx.llm.prepareCall()` 准备请求。仅通过 `llm/stream` 中间件提供标题路由的组合还必须注册适配器。自定义适配器必须将可选推理强度从低到高排列,而不是使用任意显示顺序。标题策略通过配置函数选择首个条目。现有具体 `prepareCall()` 输入仍然有效。 + +## 迁移 + +1. 通过 `ctx.llm.registerAdapter()` 注册标题路由。回放组合可配置现有 `llm-replay` 插件的 `providers` 列表。 +2. 在适配器的模型元数据中,将 `reasoning.efforts` 按可选强度从低到高排列。回放模型的 `reasoningEfforts` 使用同一顺序。配置默认值保持独立于列表顺序。路由没有可选控制时省略推理元数据。 +3. 生成标题,确认 `session/title-llm-request` 记录了所选强度,且提供方标题已保存。不含该可选字段的现有 Session 记录仍可读取。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml new file mode 100644 index 0000000000..c1938c0f53 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md +/: + en: 9ae9e5682ed3cd7e + zh: 7423fcb7aed1938a +/migrate-delegation-to-managed-subagent-activations: + en: fcf5f06084f989d9 + zh: 5e3e6a05786390dd +/migrate-delegation-to-managed-subagent-activations/change: + en: 17add29d9f80674e + zh: 4d4050e99f019cca +/migrate-delegation-to-managed-subagent-activations/migration: + en: d4269b2c58484a3d + zh: 0ae39476ecd330e6 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md new file mode 100644 index 0000000000..5a88ff2ee3 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md @@ -0,0 +1,59 @@ +--- +kind: upgrade-guide +description: "Managed subagent activations replace foreground and Job-backed delegation, its configuration keys, and the published in-process driver." +--- + +# Migrate delegation to managed subagent activations + +English | [中文](guide.zh.md) + +## Change + +`@deepseek-ai/dsh-tool-subagent` removes `backgroundMode`, `enableRunInBackground`, and the model argument `run_in_background`. Every delegation now returns `{ kind: 'activation', subagentId }` after accepting the child, and the runtime delivers its final answer in a parent completion notice. Foreground results and `{ kind: 'background', jobId }` results are removed; subagents are no longer collected or stopped through `job_output` or `job_kill`. + +The published `@deepseek-ai/dsh-subagent-in-process-driver` package is removed. Local providers contribute creation inputs through `prepareContinuable()`; the subagent service owns execution. Spawn and Fork children can receive later messages, including after settlement when persistence and Session query are available. External providers execute once and do not accept follow-up messages. + +## Migration + +1. Remove `backgroundMode` and `enableRunInBackground` from `tool-subagent` rows in `cordis.yml`, profile patches, and overlays. Keep the provider and remaining configuration. For example: + + ```yaml + - name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: spawn + toolName: subagent + ``` + + Local tool delegation requires Session persistence. Compose it with the [subagent service and provider](../../../../packages/subagent/subagent/README.md#use-this-package). + +2. Remove `run_in_background` from tool calls and update result consumers to read `subagentId`. Collect the completion notice instead of foreground output or a Job result. Use `interrupt_agent` to stop the current execution; use `send_message` only for local children when those control tools are composed. + +3. For host code that must await a result, replace `ctx.subagents.start()` or `startContinuable()` with `startActivation()` and caller delivery. Given a composed service and live parent: + + ```ts + import type { Agent } from '@deepseek-ai/dsh-agent' + import type SubagentRuntime from '@deepseek-ai/dsh-subagent' + + async function collect(subagents: SubagentRuntime, parent: Agent, signal: AbortSignal) { + const activation = await subagents.startActivation({ + provider: 'spawn', + label: 'Review changes', + request: { parent, prompt: [{ type: 'text', text: 'Review these changes.' }] }, + signal, + delivery: 'caller', + }) + const result = await activation.result + await activation.dispose() + return result + } + ``` + + Caller delivery emits no parent completion notice. `result` settles after cleanup and includes `output`, `stopReason`, and optional `structured` and `diagnostic` values; inspect `stopReason` before treating it as success. The startup signal stops owning cancellation after publication: retain the receipt and call `dispose()` to cancel that exact activation. Cleanup failures are reported by `dispose()` separately from the captured result. + +4. Remove the driver dependency and imports, including `startInProcessRun`. Mount the existing Spawn or Fork provider and call the service as above. Custom local providers implement `prepareContinuable()` using the [provider API](../../../../docs/subsystems/subagent.md); there is no replacement standalone driver. + + External providers keep `start()`, but each returned `SubagentRun.id` must be unique across parents, providers, and local Sessions in the runtime. Generate a fresh id for each execution instead of using a parent-local counter. + + For `dsh-subagent-dsh-sdk`, update any separately configured `dshBin` runtime to support `session/wait`. For directory configuration, follow the [working-directory migration](../working-directory/guide.md). + +5. Confirm a delegation returns the activation result variant, its completion notice contains the final answer, and caller delivery returns the result without adding that notice. Confirm local children remain listed after completion and external children cannot be continued. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md new file mode 100644 index 0000000000..aa83610496 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md @@ -0,0 +1,59 @@ +--- +kind: upgrade-guide +description: "受管理的子 agent activation 替代前台与 Job 后台委派、相关配置键和已发布的进程内驱动包。" +--- + +# 将委派迁移到受管理的子 agent activation + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-tool-subagent` 移除了 `backgroundMode`、`enableRunInBackground` 和模型参数 `run_in_background`。每次委派现在都会在接纳子 agent(智能体)后返回 `{ kind: 'activation', subagentId }`,运行时通过父级完成通知交付最终答案。前台结果和 `{ kind: 'background', jobId }` 结果已移除;子 agent 不再通过 `job_output` 或 `job_kill` 收集结果或停止。 + +已发布的 `@deepseek-ai/dsh-subagent-in-process-driver` 包被移除。本地提供方通过 `prepareContinuable()` 提供创建输入,由子 agent 服务拥有执行过程。Spawn 和 Fork 子级可以接收后续消息;组合了持久化和 Session 查询服务时,结算后也能接收。外部提供方只执行一次,不接受后续消息。 + +## 迁移 + +1. 从 `cordis.yml`、配置文件补丁和覆盖文件中的 `tool-subagent` 配置项删除 `backgroundMode` 和 `enableRunInBackground`。保留提供方和其余配置。例如: + + ```yaml + - name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: spawn + toolName: subagent + ``` + + 本地工具委派需要 Session 持久化。将其与[子 agent 服务及提供方](../../../../packages/subagent/subagent/README.zh.md#use-this-package)一起组合。 + +2. 从工具调用中删除 `run_in_background`,并将结果消费方改为读取 `subagentId`。收集完成通知,替代前台输出或 Job 结果。组合了相应控制工具时,使用 `interrupt_agent` 停止当前执行;`send_message` 仅用于本地子级。 + +3. 对于必须等待结果的宿主代码,将 `ctx.subagents.start()` 或 `startContinuable()` 替换为 `startActivation()`,并选择调用方投递。以下示例假设服务已组合且父 agent 在线: + + ```ts + import type { Agent } from '@deepseek-ai/dsh-agent' + import type SubagentRuntime from '@deepseek-ai/dsh-subagent' + + async function collect(subagents: SubagentRuntime, parent: Agent, signal: AbortSignal) { + const activation = await subagents.startActivation({ + provider: 'spawn', + label: 'Review changes', + request: { parent, prompt: [{ type: 'text', text: 'Review these changes.' }] }, + signal, + delivery: 'caller', + }) + const result = await activation.result + await activation.dispose() + return result + } + ``` + + 调用方投递不会发送父级完成通知。`result` 在清理结束后结算,包含 `output`、`stopReason` 以及可选的 `structured` 和 `diagnostic` 值;将结果视为成功前先检查 `stopReason`。发布后,启动信号不再控制取消:保留回执,通过 `dispose()` 取消该次 activation。`dispose()` 单独报告清理失败,不替换已捕获的执行结果。 + +4. 删除驱动包依赖及其导入,包括 `startInProcessRun`。挂载现有的 Spawn 或 Fork 提供方,并按上例调用服务。自定义本地提供方按[提供方 API](../../../../docs/subsystems/subagent.zh.md)实现 `prepareContinuable()`;没有替代的独立驱动包。 + + 外部提供方保留 `start()`,但每次返回的 `SubagentRun.id` 必须在运行时内跨父级、提供方和本地 Session 保持唯一。为每次执行生成新的 id,替代父级独立计数器。 + + 使用 `dsh-subagent-dsh-sdk` 时,将单独配置的 `dshBin` 运行时更新到支持 `session/wait` 的版本。 目录配置按照[工作目录迁移指南](../working-directory/guide.zh.md)调整。 + +5. 确认委派返回 activation 结果变体,完成通知包含最终答案,且调用方投递返回结果而不添加该通知。确认本地子级在完成后仍可列出,外部子级不支持继续执行。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/terminal-command-labels/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/terminal-command-labels/guide.i18n.yaml new file mode 100644 index 0000000000..fe08933d1f --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/terminal-command-labels/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/terminal-command-labels/guide.md +/: + en: 0722f826c1c12360 + zh: 9b48ce197b6091f2 +/terminal-command-labels: + en: a75e371670f4ebc3 + zh: cbad7dd538376e70 +/terminal-command-labels/change: + en: 0e386cd5b5d349dc + zh: c456641a3aa938b3 +/terminal-command-labels/migration: + en: 2044b801804a40aa + zh: 0f98e7da80e2f066 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/terminal-command-labels/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/terminal-command-labels/guide.md new file mode 100644 index 0000000000..fdebb0aba5 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/terminal-command-labels/guide.md @@ -0,0 +1,18 @@ +--- +kind: upgrade-guide +description: "Custom TerminalBlock consumers must provide localized accessible names for overflowing command lines." +--- + +# Terminal command labels + +English | [中文](guide.zh.md) + +## Change + +`TerminalBlockLabels` from `@deepseek-ai/dsh-client-ui-primitives` requires a `commandLine(line: number): string` formatter. Previously, callers supplied no accessible name for command lines. Each horizontally overflowing line now uses this name for its keyboard-focusable group; lines that fit, including wrapped lines, add no Tab stop. This affects custom UI plugins that render `TerminalBlock`. + +## Migration + +1. Add `commandLine` to each labels object passed to `TerminalBlock`. The argument is the authored line number, starting at one. +2. Return a localized name, such as “Command line 1”, through the plugin's locale dictionary. Keep the original command text separate from this name. +3. Rebuild the plugin. Verify that a long command can receive keyboard focus and scroll with the arrow keys, and that a short or wrapped command adds no Tab stop. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/terminal-command-labels/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/terminal-command-labels/guide.zh.md new file mode 100644 index 0000000000..39e63f5c69 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/terminal-command-labels/guide.zh.md @@ -0,0 +1,18 @@ +--- +kind: upgrade-guide +description: "自定义 TerminalBlock 消费者必须为横向溢出的命令行提供本地化的无障碍名称。" +--- + +# 终端命令标签 + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-client-ui-primitives` 的 `TerminalBlockLabels` 要求提供 `commandLine(line: number): string` 格式化函数。此前,调用者不提供命令行的无障碍名称。现在,每条横向溢出的命令行使用这个名称标识可通过键盘聚焦的分组;能够完整显示的行(包括折行后的行)不会增加 Tab 停靠点。这会影响渲染 `TerminalBlock` 的自定义 UI 插件。 + +## 迁移 + +1. 为传给 `TerminalBlock` 的每个 labels 对象添加 `commandLine`。参数是原始命令文本的行号,从一开始。 +2. 通过插件的语言字典返回本地化名称,例如“命令第 1 行”。名称与原始命令文本分开保留。 +3. 重新构建插件。确认长命令可通过键盘聚焦并使用方向键滚动,短命令或折行后的命令则不会增加 Tab 停靠点。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode/guide.i18n.yaml new file mode 100644 index 0000000000..d50e824fe7 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode/guide.md +/: + en: 123b457f6ec93005 + zh: aa0366d7aae638ca +/select-native-or-ptc-tool-presentation: + en: 493e892beb7aecb2 + zh: 149d7678dde936bb +/select-native-or-ptc-tool-presentation/change: + en: a72ed2baec1e7e5b + zh: a54c9b0c1e88829f +/select-native-or-ptc-tool-presentation/migration: + en: 64379f963e886e64 + zh: 18b46f7287598a9e diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode/guide.md new file mode 100644 index 0000000000..c6a2ce9328 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode/guide.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: "Tool presentation accepts native or ptc; the mixed both mode is rejected." +--- + +# Select native or PTC tool presentation + +English | [中文](guide.zh.md) + +## Change + +The `mode` fields of `@deepseek-ai/dsh-tools` and `@deepseek-ai/dsh-agent-tool-presentation` accept only `native` or `ptc`. The `both` value is rejected. This also affects `DSH_TOOLS_MODE=both`, which supplies the tools configuration through the shipped profile patches. + +`native` exposes visible tools as direct function calls. `ptc` exposes `run_code`; programs call the visible tools through the generated SDK. Different agent presets can continue using different modes in the same application. + +## Migration + +1. In `cordis.yml`, profile `cordis.patch.yml`, or a custom overlay, replace `mode: both` with `mode: native` for direct calls or `mode: ptc` for program-based calls. +2. Replace `DSH_TOOLS_MODE=both` with `DSH_TOOLS_MODE=native` or `DSH_TOOLS_MODE=ptc`. For preset-specific selection, update the preset's `@deepseek-ai/dsh-agent-tool-presentation` row instead. +3. Update code that calls `ctx.tools.presentAs('both')` to select `native` or `ptc`. In PTC programs, keep calls inside `run_code`; do not emit the SDK function names as direct model tool calls. +4. Start the affected profile and confirm that configuration loads. Native requests expose the permitted direct tools; PTC requests expose `run_code` and its SDK instructions. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode/guide.zh.md new file mode 100644 index 0000000000..cd19e9a47f --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/tool-presentation-mode/guide.zh.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: "工具呈现方式仅接受 native 或 ptc;混合的 both 模式会被拒绝。" +--- + +# 选择原生或 PTC 工具呈现方式 + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-tools` 和 `@deepseek-ai/dsh-agent-tool-presentation` 的 `mode` 字段仅接受 `native` 或 `ptc`。`both` 值会被拒绝。这也影响 `DSH_TOOLS_MODE=both`,该环境变量通过随附的 profile 补丁提供工具配置。 + +`native` 将可见工具公开为直接函数调用。`ptc` 公开 `run_code`,程序通过生成的 SDK 调用可见工具。同一应用中的不同 agent preset 仍可使用不同模式。 + +## 迁移 + +1. 在 `cordis.yml`、profile 的 `cordis.patch.yml` 或自定义覆盖文件中,将 `mode: both` 替换为用于直接调用的 `mode: native`,或用于程序调用的 `mode: ptc`。 +2. 将 `DSH_TOOLS_MODE=both` 替换为 `DSH_TOOLS_MODE=native` 或 `DSH_TOOLS_MODE=ptc`。若需按 preset 选择,请改为更新该 preset 的 `@deepseek-ai/dsh-agent-tool-presentation` 行。 +3. 将调用 `ctx.tools.presentAs('both')` 的代码改为选择 `native` 或 `ptc`。在 PTC 程序中,将调用保留在 `run_code` 内;不要将 SDK 函数名称作为模型的直接工具调用发出。 +4. 启动受影响的 profile,确认配置可加载。原生请求公开获准的直接工具;PTC 请求公开 `run_code` 及其 SDK 说明。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/working-directory/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/working-directory/guide.i18n.yaml new file mode 100644 index 0000000000..9345a236bb --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/working-directory/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/working-directory/guide.md +/: + en: bd716a636d3631a4 + zh: 7a3a4de1d3669d57 +/migrate-working-directory-configuration-and-tool-presentation: + en: 0c783661ea6973cb + zh: 087e487812b4933b +/migrate-working-directory-configuration-and-tool-presentation/change: + en: d2738fe9f819599d + zh: 4eaacaeadcd80adf +/migrate-working-directory-configuration-and-tool-presentation/migration: + en: 01ae6e89e3312de8 + zh: 503981500691b714 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/working-directory/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/working-directory/guide.md new file mode 100644 index 0000000000..d51f48713c --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/working-directory/guide.md @@ -0,0 +1,25 @@ +--- +kind: upgrade-guide +description: "Session working directories replace built-in cwd configuration, and tool presentation metadata also runs for nested PTC calls." +--- + +# Migrate working-directory configuration and tool presentation + +English | [中文](guide.zh.md) + +## Change + +The next release removes the loop's built-in `{{cwd}}` prompt variable and the `cwd` configuration key from `@deepseek-ai/dsh-subagent-acp` and `@deepseek-ai/dsh-subagent-dsh-sdk`. Custom prompt templates that retain an unresolved `{{cwd}}` fail before a model request. A Session's current execution directory is supplied as required user context and is persisted separately from its original `Session.header.cwd`. + +New subagents use their start request's `cwd` when provided, otherwise they capture the parent's current directory. Existing processes keep their own directories. Changing the execution directory does not change the original project or grant additional write permissions. + +Tool `output.presentationMeta` also runs for successful nested PTC calls. Authors whose projection assumes only native top-level calls are affected. A projection error fails that nested call with `ToolOutputError`; canonical program values and model content remain unchanged. + +## Migration + +1. In home, profile, or invocation `personaSuffix` configuration and preset `suffix` fields, remove the directory clause that references `{{cwd}}`, retaining unrelated text. Omit or clear a suffix that becomes empty. The [working-directory service](../../../../packages/session/working-directory/README.md) supplies the directory context. +2. Remove `config.cwd` from ACP and DSH-SDK subagent provider rows in your profile patches. To select a directory for one child, pass `cwd` in its [start request](../../../subsystems/subagent.md). To change the parent's execution directory, use `working_directory({ cd: "/absolute/project" })` before starting the child. +3. Run `working_directory({})` in the affected Session and confirm the reported directory. Start a child without an override and check its execution directory; use an explicit start `cwd` when the child must use another checkout. Read `Session.header.cwd` only for original project identity or permission roots, rather than current-directory operations. +4. Keep custom `output.presentationMeta` projections pure and return JSON-safe metadata from their arguments and canonical result. Exercise both a native call and a nested PTC call, and confirm the recorded metadata identifies the operation's actual target without changing its result. + +Dynamic context providers are evaluated again at request admission, after pre-step listeners and route preparation. Return current state without relying on one evaluation per step. Transform registered context inside its provider; admission refresh replaces its cached text while preserving assembly-only context contributions and the accepted system prompt, tool schemas, and variables. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/working-directory/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/working-directory/guide.zh.md new file mode 100644 index 0000000000..d8a985a9bd --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/working-directory/guide.zh.md @@ -0,0 +1,25 @@ +--- +kind: upgrade-guide +description: "Session 工作目录替代内置 cwd 配置,工具展示元数据也会在嵌套 PTC 调用中生成。" +--- + +# 迁移工作目录配置与工具展示 + +[English](guide.md) | 中文 + +## 变更 + +下一版本移除 loop 的内置 `{{cwd}}` 提示词变量,以及 `@deepseek-ai/dsh-subagent-acp` 和 `@deepseek-ai/dsh-subagent-dsh-sdk` 的 `cwd` 配置键。自定义提示词模板保留未解析的 `{{cwd}}` 时,会在模型请求发出前失败。Session 当前执行目录作为必需的 user context 提供,并与原始 `Session.header.cwd` 分开持久化。 + +新子代理使用启动请求中显式提供的 `cwd`,未提供时捕获父代理的当前目录。已有进程保留各自的目录。切换执行目录不会改变原始项目,也不会授予额外写权限。 + +工具 `output.presentationMeta` 也会在成功的嵌套 PTC 调用中运行。投影仅假设原生顶层调用的作者需要调整实现。投影异常会使该次嵌套调用以 `ToolOutputError` 失败;程序读取的规范值与模型内容不变。 + +## 迁移 + +1. 在 home、profile 或本次调用的 `personaSuffix` 配置,以及 preset 的 `suffix` 字段中,移除引用 `{{cwd}}` 的目录语句,保留其他文本。suffix 变为空时省略或清空该字段。[工作目录服务](../../../../packages/session/working-directory/README.zh.md)负责提供目录上下文。 +2. 从 profile patch 中的 ACP 与 DSH-SDK 子代理 provider 行移除 `config.cwd`。为单个子代理选择目录时,在其[启动请求](../../../subsystems/subagent.zh.md)中传入 `cwd`。改变父代理执行目录时,先调用 `working_directory({ cd: "/absolute/project" })`,再启动子代理。 +3. 在相关 Session 中调用 `working_directory({})` 并确认报告的目录。启动一个不带目录覆盖的子代理并检查其执行目录;子代理需要另一个 checkout 时,显式提供启动 `cwd`。仅用 `Session.header.cwd` 读取原始项目标识或权限根目录,当前目录操作使用工作目录服务。 +4. 保持自定义 `output.presentationMeta` 投影纯净,并从调用参数和规范结果返回可用于 JSON 的元数据。分别执行原生调用和嵌套 PTC 调用,确认记录的元数据指向操作的实际目标,同时不改变调用结果。 + +动态上下文 provider 会在请求准入时再次求值,晚于 pre-step 监听器与路由准备。返回当前状态,不依赖每个步骤只求值一次。对已注册上下文的转换应在其 provider 内完成;准入刷新会替换缓存文本,同时保留仅由组装添加的上下文,以及已接纳的系统提示词、工具 schema 与变量。 diff --git a/docs/user/guide/github-review.i18n.yaml b/docs/user/guide/github-review.i18n.yaml index c2c413d88c..921406a949 100644 --- a/docs/user/guide/github-review.i18n.yaml +++ b/docs/user/guide/github-review.i18n.yaml @@ -9,14 +9,14 @@ en: 6784d85f90231608 zh: 3e62e9cadfdc2c18 /create-review-sessions-from-github-webhooks/start-dsh: - en: 5751931f5c89b1c0 - zh: 867950515a152d47 + en: 6c2c8394bfaf5d11 + zh: a863fa2ad6bc48bb /create-review-sessions-from-github-webhooks/expose-the-dedicated-endpoint: en: f18a2969b3082da9 zh: 87d3d0e58d25a62f /create-review-sessions-from-github-webhooks/rule-behavior: - en: e90b3061a15fdedc - zh: a8e9549e732314f6 + en: 3306087b78170a99 + zh: 7c7b0baefa1dc303 /create-review-sessions-from-github-webhooks/programmatic-extensions: en: d448fc3e440a0ec6 zh: 18568898933ef054 diff --git a/docs/user/guide/github-review.md b/docs/user/guide/github-review.md index 69c61df95c..981458d36b 100644 --- a/docs/user/guide/github-review.md +++ b/docs/user/guide/github-review.md @@ -22,20 +22,17 @@ export DSH_GITHUB_WEBHOOK_SECRET="$(openssl rand -hex 32)" printf '%s\n' "$DSH_GITHUB_WEBHOOK_SECRET" ``` -From a development checkout: +Install both experimental packages into the Web profile first. Profiles do not install peers automatically; the GitHub adapter needs the webhook runtime explicitly: ```sh +dsh plugin --profile web add @deepseek-ai/dsh-webhook @deepseek-ai/dsh-webhook-github export DSH_GITHUB_REVIEW_WORKSPACE=/path/to/deepseek-harness -pnpm dsh web --patch apps/cli/config/examples/github-review/cordis.yml +dsh web --patch "${DSH_HOME:-$HOME/.dsh}/profiles/web/node_modules/@deepseek-ai/dsh-webhook-github/examples/github-review/cordis.yml" ``` -An installed DSH uses the same overlay through an absolute path: +The adapter distributes the example, which loads from the profile's installation directory so its rule module can resolve profile-installed peers. Do not run the rule from an arbitrary directory outside the profile. To customize it, copy both example files to `$DSH_HOME/profiles/web/github-review/`, edit the configuration, and point `--patch` at that copy of `cordis.yml`; use `~/.dsh` when `DSH_HOME` is unset. -```sh -dsh web --patch /absolute/path/to/github-review/cordis.yml -``` - -For a permanent profile, place `github-ready-review-rule.mjs` beside `$DSH_HOME/profiles/web/cordis.patch.yml`, append the rows from `cordis.yml` to that patch, and start with `dsh web`. The shipped CLI already contains both webhook packages; the overlay alone activates them. +The repository example lives under `packages/experimental/webhook-github/examples/github-review/`. The CLI does not install webhook packages or list them as Official plugins; installing the packages and supplying the overlay are separate steps. ## Expose the dedicated endpoint @@ -65,7 +62,7 @@ Active: yes ## Rule behavior -The rule accepts only source `primary-github`, repository `deepseek-harness/deepseek-harness`, event `pull_request`, and action `ready_for_review`. It passes the exact head SHA plus selected PR fields to the review prompt, labeling the JSON as untrusted metadata and forbidding file, branch, PR, or GitHub mutation. +The rule accepts only source `primary-github`, repository `deepseek-ai/deepseek-harness`, event `pull_request`, and action `ready_for_review`. It passes the exact head SHA plus selected PR fields to the review prompt, labeling the JSON as untrusted metadata and forbidding file, branch, PR, or GitHub mutation. The Session request selects the `standard` agent preset and `read-only` permission preset. `workspacePath` is canonicalized through `WorkspaceRegistry.create()`, so the first matching delivery creates the Web Workspace when absent and later deliveries reuse it. @@ -89,7 +86,7 @@ It can also map repositories to different local paths: ```js const workspacePath = { - 'deepseek-harness/deepseek-harness': '/path/to/deepseek-harness', + 'deepseek-ai/deepseek-harness': '/path/to/deepseek-harness', 'deepseek-harness/dsh-sdk': '/path/to/dsh-sdk', }[payload.repository.full_name] if (workspacePath === undefined) return null diff --git a/docs/user/guide/github-review.zh.md b/docs/user/guide/github-review.zh.md index 114aa6546d..4ad240f91a 100644 --- a/docs/user/guide/github-review.zh.md +++ b/docs/user/guide/github-review.zh.md @@ -22,20 +22,17 @@ export DSH_GITHUB_WEBHOOK_SECRET="$(openssl rand -hex 32)" printf '%s\n' "$DSH_GITHUB_WEBHOOK_SECRET" ``` -在开发 checkout 中运行: +先把两个实验包安装到 Web profile。Profile 不会自动安装 peer;GitHub 适配器需要显式安装 webhook 运行时: ```sh +dsh plugin --profile web add @deepseek-ai/dsh-webhook @deepseek-ai/dsh-webhook-github export DSH_GITHUB_REVIEW_WORKSPACE=/path/to/deepseek-harness -pnpm dsh web --patch apps/cli/config/examples/github-review/cordis.yml +dsh web --patch "${DSH_HOME:-$HOME/.dsh}/profiles/web/node_modules/@deepseek-ai/dsh-webhook-github/examples/github-review/cordis.yml" ``` -安装版 DSH 通过绝对路径使用同一 overlay: +示例随 GitHub 适配器发布,并从 profile 的安装目录加载,因此规则模块能解析该 profile 安装的 peer。不要把规则单独放到 profile 之外的任意目录运行。需要定制时,将示例的两个文件复制到 `$DSH_HOME/profiles/web/github-review/`,修改其配置并把 `--patch` 指向复制后的 `cordis.yml`;未设置 `DSH_HOME` 时使用 `~/.dsh`。 -```sh -dsh web --patch /absolute/path/to/github-review/cordis.yml -``` - -对于永久 profile,把 `github-ready-review-rule.mjs` 放在 `$DSH_HOME/profiles/web/cordis.patch.yml` 旁边,把 `cordis.yml` 中的行追加到该 patch,然后运行 `dsh web`。随附 CLI 已经包含两个 webhook 包;只需 overlay 即可激活它们。 +仓库中的示例位于 `packages/experimental/webhook-github/examples/github-review/`。Webhook 包不随 CLI 安装,也不出现在官方插件列表中;安装包与提供 overlay 是两个独立步骤。 ## 暴露专用端点 @@ -65,7 +62,7 @@ Active: yes ## 规则行为 -规则只接受来源 `primary-github`、仓库 `deepseek-harness/deepseek-harness`、事件 `pull_request` 与动作 `ready_for_review`。它会把精确 head SHA 和选定 PR 字段传给评审提示词,把 JSON 标为不受信任的元数据,并禁止修改文件、分支、PR 或 GitHub 状态。 +规则只接受来源 `primary-github`、仓库 `deepseek-ai/deepseek-harness`、事件 `pull_request` 与动作 `ready_for_review`。它会把精确 head SHA 和选定 PR 字段传给评审提示词,把 JSON 标为不受信任的元数据,并禁止修改文件、分支、PR 或 GitHub 状态。 Session 请求选择 `standard` agent preset 与 `read-only` permission preset。`workspacePath` 通过 `WorkspaceRegistry.create()` 规范化,因此第一次匹配交付会在 Workspace 不存在时创建它,后续交付会复用它。 @@ -89,7 +86,7 @@ if (!response.ok || (await response.json()).automaticReview !== true) return nul ```js const workspacePath = { - 'deepseek-harness/deepseek-harness': '/path/to/deepseek-harness', + 'deepseek-ai/deepseek-harness': '/path/to/deepseek-harness', 'deepseek-harness/dsh-sdk': '/path/to/dsh-sdk', }[payload.repository.full_name] if (workspacePath === undefined) return null diff --git a/native/system/package.json b/native/system/package.json index a4fd9bcf0c..3fd10861dd 100644 --- a/native/system/package.json +++ b/native/system/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "license": "BSD-3-Clause", - "packageManager": "pnpm@11.7.0", + "packageManager": "pnpm@11.28.5", "scripts": { "build": "pnpm build:ts", "build:ts": "tsc -b", @@ -27,8 +27,8 @@ }, "devDependencies": { "@deepseek-ai/node-addon-system": "workspace:~", - "@types/node": "^26.0.1", - "tsx": "^4.20.6", + "@types/node": "^26.4.1", + "tsx": "^4.23.13", "typescript": "^6.0.3" } } diff --git a/native/system/scripts/pack-release.mjs b/native/system/scripts/pack-release.mjs index 17290d669a..4a6e5b116f 100644 --- a/native/system/scripts/pack-release.mjs +++ b/native/system/scripts/pack-release.mjs @@ -95,12 +95,8 @@ try { const publishOrder = []; for (const dir of dirs) { const manifest = readJson(path.join(root, dir, 'package.json')); - // Platform packages are packed with npm: pnpm pack (observed on 11.7.0) - // normalizes file modes and STRIPS the executable bit, which ships a - // launcher no consumer can spawn; npm pack preserves it. Platform packages - // have no dependencies by construction, so they need none of pnpm's - // workspace-protocol conversion — the entry packages do, and carry no - // executables, so they keep pnpm pack. + // Platform packages have no workspace dependencies; npm preserves their + // executable modes. Entry packages need pnpm's workspace version conversion. if (platformSet.has(dir)) { run('npm', ['pack', `./${dir}`, '--pack-destination', destination], packRoot); } else { diff --git a/native/system/test/release-packing.test.js b/native/system/test/release-packing.test.js index 86ccf03078..4ffd3f79e8 100644 --- a/native/system/test/release-packing.test.js +++ b/native/system/test/release-packing.test.js @@ -15,7 +15,7 @@ function fixture(t) { t.after(() => fs.rmSync(workspace, { recursive: true, force: true })); const dir = path.join(workspace, 'native/system'); fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(path.join(workspace, 'package.json'), `${JSON.stringify({ private: true, packageManager: 'pnpm@11.7.0' })}\n`); + fs.writeFileSync(path.join(workspace, 'package.json'), `${JSON.stringify({ private: true, packageManager: 'pnpm@11.28.5' })}\n`); fs.writeFileSync(path.join(workspace, 'pnpm-workspace.yaml'), 'packages:\n - native/system\n - native/system/packages/*\n'); fs.writeFileSync(path.join(workspace, 'LICENSE'), workspaceLicense); const scratch = path.join(workspace, 'scratch'); @@ -26,7 +26,7 @@ function fixture(t) { fs.writeFileSync(path.join(dir, file), `${JSON.stringify(value, null, 2)}\n`); }; const repository = (name) => ({ type: 'git', url: publicRepository, directory: `native/system/packages/${name}` }); - writeJson('package.json', { name: 'native-packing-fixture', private: true, version: '1.2.3', packageManager: 'pnpm@11.7.0' }); + writeJson('package.json', { name: 'native-packing-fixture', private: true, version: '1.2.3', packageManager: 'pnpm@11.28.5' }); writeJson('packages/linux-x64/package.json', { name: '@fixture/native-linux-x64', version: '1.2.3', repository: repository('linux-x64'), os: ['linux'], cpu: ['x64'], files: ['bin/', 'prebuilds.json'], diff --git a/package.json b/package.json index 75d586f00f..837bf1d905 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "@deepseek-ai/dsh-root", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "license": "MIT", "private": true, "type": "module", - "packageManager": "pnpm@11.7.0", + "packageManager": "pnpm@11.28.5", "engines": { "node": "^22.19.0 || >=24.0.0" }, @@ -19,11 +19,11 @@ "scripts": { "prepare:primary-runtime": "tsx scripts/primary-runtime/prepare.ts", "build": "tsx scripts/build.ts", - "build:bench": "npm run build:native-system && npm run build:lib && tsdown --config benchmarks/tsdown.config.ts", + "build:bench": "npm run build:native-system && npm run build:lib && tsdown --config-loader native --config benchmarks/tsdown.config.ts", "build:official": "tsx scripts/build.ts --profile official", "build:lib": "pnpm run build:lib:host && pnpm run build:lib:client", - "build:lib:host": "node --max-old-space-size=4096 ./node_modules/typescript/bin/tsc -b tsconfig.host.json && tsdown --env.DSH_BUILD_FACE host && pnpm --filter @deepseek-ai/dsh-desktop run bundle", - "build:lib:client": "tsc -b tsconfig.client.json && tsdown --env.DSH_BUILD_FACE client", + "build:lib:host": "node --max-old-space-size=4096 ./node_modules/typescript/bin/tsc -b tsconfig.host.json && tsdown --config-loader native --env.DSH_BUILD_FACE host && pnpm --filter @deepseek-ai/dsh-desktop run bundle", + "build:lib:client": "tsc -b tsconfig.client.json && tsdown --config-loader native --env.DSH_BUILD_FACE client", "build:web": "pnpm --filter @deepseek-ai/dsh-web-frontend run build", "build:desktop": "pnpm --filter @deepseek-ai/dsh-desktop run build", "dev:desktop": "pnpm --filter @deepseek-ai/dsh-desktop run dev", @@ -32,8 +32,10 @@ "package:desktop": "pnpm --filter @deepseek-ai/dsh-desktop run package", "package:desktop:dir": "pnpm --filter @deepseek-ai/dsh-desktop run package:dir", "package:desktop:mac:arm64": "pnpm --filter @deepseek-ai/dsh-desktop run package:mac:arm64", + "package:desktop:mac:arm64:unsigned": "pnpm --filter @deepseek-ai/dsh-desktop run package:mac:arm64:unsigned", "package:desktop:mac:arm64:dir": "pnpm --filter @deepseek-ai/dsh-desktop run package:mac:arm64:dir", "package:desktop:mac:x64": "pnpm --filter @deepseek-ai/dsh-desktop run package:mac:x64", + "package:desktop:mac:x64:unsigned": "pnpm --filter @deepseek-ai/dsh-desktop run package:mac:x64:unsigned", "package:desktop:mac:x64:dir": "pnpm --filter @deepseek-ai/dsh-desktop run package:mac:x64:dir", "package:desktop:win:x64": "pnpm --filter @deepseek-ai/dsh-desktop run package:win:x64", "package:desktop:win:x64:unsigned": "pnpm --filter @deepseek-ai/dsh-desktop run package:win:x64:unsigned", @@ -60,6 +62,7 @@ "test:e2e": "vitest run --config vitest.e2e.config.ts", "test:bench": "npm run build:bench && npm run build:web && npm run test:bench:built", "test:bench:built": "vitest run --config vitest.bench.config.ts", + "test:bench:memory": "tsx scripts/run-memory-pressure-bench.ts", "test:expected": "vitest run --config vitest.expected.config.ts", "test:expected:refresh": "DSH_SNAPSHOT=refresh vitest run --config vitest.expected.config.ts", "test:approval-policy": "node --test .github/review-ownership/check-approval.test.mjs .github/review-ownership/blame-ownership.test.mjs .github/review-ownership/author-weight.test.mjs", @@ -144,12 +147,15 @@ "verify-client-ui-i18n": "tsx scripts/verify-client-ui-i18n.ts", "verify-client-route-resolution": "tsx scripts/verify-client-route-resolution.ts", "verify-no-bare-dispatcher": "tsx scripts/verify-no-bare-dispatcher.ts", + "verify-plugin-record-callers": "tsx scripts/verify-plugin-record-callers.ts", "verify-cordis-config": "tsx scripts/verify-cordis-config.ts", "rescope-vendor": "tsx scripts/rescope-vendor.ts", "rescope-vendor:check": "tsx scripts/rescope-vendor.ts --check", "verify-client-domain-graph": "tsx scripts/verify-client-domain-graph.ts", "gen-tsconfig-paths": "tsx scripts/gen-tsconfig-paths.ts", "verify-tsconfig-paths": "tsx scripts/gen-tsconfig-paths.ts --check", + "gen-cli-help": "tsx scripts/gen-cli-help.ts", + "verify-cli-help": "tsx scripts/gen-cli-help.ts --check", "gen-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts", "verify-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts --check", "gen-cordis-api": "tsx scripts/gen-cordis-api.ts", @@ -201,9 +207,17 @@ "mock:llm": "node --import tsx packages/test-support/llm-mock-server/src/bin.ts", "start:web": "node --import tsx/esm apps/cli/src/bin.ts web", "dev:web": "tsx scripts/dev-web.ts --poll", - "postinstall": "node scripts/install-lefthook.mjs" + "dev:bundle-registry": "tsx scripts/dev-bundle-registry.ts", + "postinstall": "node scripts/install-lefthook.mjs", + "gen-official-bundle-catalog": "tsx scripts/gen-official-bundle-catalog.ts", + "verify-official-bundle-catalog": "tsx scripts/gen-official-bundle-catalog.ts --check", + "verify-product-use": "tsx scripts/verify-product-use.ts" }, "devDependencies": { + "@deepseek-ai/dsh-hooks-claude-code": "workspace:*", + "@deepseek-ai/dsh-hooks-codex": "workspace:*", + "@deepseek-ai/dsh-tool-ralph": "workspace:*", + "@deepseek-ai/dsh-tool-terminal": "workspace:*", "@deepseek-ai/dsh-agent": "workspace:*", "@deepseek-ai/dsh-browser-use": "workspace:*", "@deepseek-ai/dsh-experimental-browser-use-runtime": "workspace:*", @@ -213,50 +227,57 @@ "@deepseek-ai/dsh-computer-use": "workspace:*", "@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp": "workspace:*", "@deepseek-ai/dsh-experimental-computer-use-cua-driver-native": "workspace:*", + "@deepseek-ai/dsh-experimental-claude-code-mods": "workspace:*", "@deepseek-ai/dsh-llm-replay": "workspace:*", "@deepseek-ai/dsh-mcp-client": "workspace:*", "@deepseek-ai/dsh-tool-session-query": "workspace:*", "@deepseek-ai/dsh-web-fetch-http": "workspace:*", "@stylistic/eslint-plugin": "^5.10.0", "@testing-library/dom": "^10.4.1", - "@testing-library/react": "^16.3.2", + "@testing-library/react": "^16.3.3", "@types/js-yaml": "^4.0.9", "@types/jsdom": "^28.0.3", "@types/mdast": "^4.0.4", - "@types/node": "^22.20.0", + "@types/node": "^22.20.1", + "@types/semver": "^7.8.0", "@types/spdx-expression-parse": "^4.0.0", - "@vitest/coverage-v8": "^4.1.8", + "@vitest/coverage-v8": "^4.1.11", "@yao-pkg/pkg": "6.21.0", - "@yarnpkg/cli-dist": "4.17.1", - "eslint-plugin-sonarjs": "^4.1.0", - "execa": "^10.0.0", + "@yarnpkg/cli-dist": "4.18.0", + "eslint-plugin-sonarjs": "^4.2.0", + "execa": "^10.0.1", "extract-zip": "^2.0.1", - "fast-check": "^4.8.0", + "fast-check": "^4.9.0", "fflate": "^0.8.2", "istanbul-lib-report": "^3.0.1", - "js-yaml": "^4.2.0", - "jscpd": "^5.0.12", + "js-yaml": "^4.3.2", + "jscpd": "^5.1.2", "jsdom": "29.1.1", - "lefthook": "^2.1.9", - "lightningcss": "^1.32.0", + "lefthook": "^2.1.12", + "lightningcss": "^1.33.0", "mdast-util-from-markdown": "^2.0.3", "mdast-util-gfm": "^3.1.0", - "mermaid": "11.16.0", + "mermaid": "11.17.2", "micromark-extension-gfm": "^3.0.0", - "oxlint": "1.76.0", + "oxlint": "1.81.0", "oxlint-tsgolint": "7.0.2001", - "pnpm": "11.7.0", - "publint": "^0.3.21", - "smol-toml": "^1.7.1", + "pnpm": "11.28.5", + "publint": "^0.3.24", + "semver": "^7.8.5", + "smol-toml": "^1.9.0", "spdx-expression-parse": "^5.0.0", - "tar": "^7.5.0", + "tar": "^7.5.22", "tsdown": "^0.22.2", - "tsx": "^4.22.4", + "tsx": "^4.23.13", "typescript": "^6.0.3", - "vite": "8.0.16", + "vite": "8.2.2", "vite-tsconfig-paths": "^6.1.1", - "vitest": "^4.1.8", + "vitest": "^4.1.11", "@deepseek-ai/dsh-skill-office": "workspace:*", - "@deepseek-ai/dsh-tool-workspace-dependencies": "workspace:*" + "@deepseek-ai/dsh-tool-workspace-dependencies": "workspace:*", + "@deepseek-ai/dsh-tool-working-directory": "workspace:*", + "@deepseek-ai/dsh-working-directory": "workspace:*", + "@deepseek-ai/dsh-experimental-worktree": "workspace:*", + "@deepseek-ai/dsh-experimental-tool-worktree": "workspace:*" } } diff --git a/packages/README.i18n.yaml b/packages/README.i18n.yaml index 331665509e..4412f95938 100644 --- a/packages/README.i18n.yaml +++ b/packages/README.i18n.yaml @@ -15,11 +15,11 @@ en: ffc55792d6e922e1 zh: 83bd759638857e2d /packages/package-groups: - en: 989c5a7131853dc6 - zh: 259d15e2bceb0bb5 + en: ff0b0b8388457f47 + zh: b9ace22bb3cbad48 /packages/release-expectations: - en: 7a152a53e2b1f2e5 - zh: 3c154c20db49713d + en: 236081c1659b25ab + zh: 809110adf003802e /packages/dependencies: en: 9576e7c486bf123f zh: fedd18da3e27ad00 diff --git a/packages/README.md b/packages/README.md index 3322d3cb7d..9485cc1001 100644 --- a/packages/README.md +++ b/packages/README.md @@ -40,7 +40,7 @@ Every package lives in exactly one group; new packages join existing groups, and | [`subprocess/`](subprocess/README.md) | Subprocess capability family: Service Definition + local process-tree provider | | [`ssh/`](ssh/README.md) | POSIX remote connection with paired filesystem, subprocess and sandbox providers | | [`shell/`](shell/README.md) | Bash capability family: executor seam, local impl, model-facing tools | -| [`terminal/`](terminal/README.md) | Persistent PTY capability family: owner-scoped sessions, local implementation, model-facing tools | +| [`terminal/`](terminal/README.md) | Persistent PTY service with owner-scoped sessions and a local shell backend | | [`ptc-runtime/`](ptc-runtime/README.md) | PTC execution capability family: Service Definition + sandboxed Node provider + PTC mode Consumer | | [`computer-use/`](computer-use/README.md) | Exclusive named desktop-provider registration | | [`browser-use/`](browser-use/README.md) | Exclusive named browser-provider registration | @@ -53,9 +53,8 @@ Every package lives in exactly one group; new packages join existing groups, and | [`context/`](context/README.md) | Model-visible request context: workspace instructions, time context, references | | [`subagent/`](subagent/README.md) | Subagent capability family: provider-registry contract and model-facing delegation tools | | [`jobs/`](jobs/README.md) | Generic background-job runtime and model-facing job control tools | -| [`experimental/`](experimental/README.md) | Pre-stable prototypes with explicit private exceptions | -| [`workflow/`](workflow/README.md) | Workflow seam, PTC process engine, and model-facing `workflow`/`ralph` tools | -| [`webhook/`](webhook/README.md) | Verified external events, trusted rules, and fire-and-forget Workspace Sessions | +| [`experimental/`](experimental/README.md) | Capabilities under evaluation; [status and support policy](experimental/README.md#status) | +| [`workflow/`](workflow/README.md) | Workflow seam, PTC process engine, and the model-facing `workflow` tool | | [`web/`](web/README.md) | Web capability family: seam, search/fetch providers, model-facing web tools | | [`document/`](document/README.md) | Shared Host Office-to-PDF conversion | | [`attachment/`](attachment/README.md) | Durable attachment identity, validation, local content-addressed storage | @@ -67,7 +66,6 @@ Every package lives in exactly one group; new packages join existing groups, and | [`bundle/`](bundle/README.md) | Installable `dsh --profile` patch layers | | [`extensions/`](extensions/README.md) | Agent runtime self-modification: live plugin/service inspection and model-written mount/unmount | | [`mcp/`](mcp/README.md) | External Model Context Protocol servers exposed as native tools | -| [`hooks/`](hooks/README.md) | Hook bridges + the shared Claude Code / Codex wire-protocol library | | [`session/`](session/README.md) | Durable session data plane: persistence seam + backends, projection seam, log-backed titles, session reporting | | [`session-query/`](session-query/README.md) | Session retrieval family: logical corpus, bounded reads, lineage, semantic filtering, SQLite full-text search | | [`settings/`](settings/README.md) | User-settings seam + file-backed provider | @@ -88,7 +86,7 @@ Every package lives in exactly one group; new packages join existing groups, and ## Release expectations -Most groups are product — stable API. The exceptions: `experimental/` publishes without stability or support promises, and `test-support/` and `util/` are support with lower compatibility expectations. +Product groups have maintained product roles, while their public APIs remain pre-stable. [Experimental status](experimental/README.md#status) is independent of publication, installation, and optional delivery. Nonexperimental runtime packages must have product use or an explicit [package classification](../scripts/product-package-policy.ts); SDK, build, test, and declaration infrastructure have separate classifications. ----- diff --git a/packages/README.zh.md b/packages/README.zh.md index c187e74f52..ec2a9b81f8 100644 --- a/packages/README.zh.md +++ b/packages/README.zh.md @@ -40,7 +40,7 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会 | [`subprocess/`](subprocess/README.zh.md) | 子进程能力系列:Service Definition + 本地进程树提供方 | | [`ssh/`](ssh/README.zh.md) | POSIX 远端连接及配套文件系统、子进程与沙箱提供方 | | [`shell/`](shell/README.zh.md) | Bash 能力系列:执行器 seam、本地实现、面向模型的工具 | -| [`terminal/`](terminal/README.zh.md) | 持久 PTY 能力系列:限定所有者范围的会话、本地实现、面向模型的工具 | +| [`terminal/`](terminal/README.zh.md) | 持久 PTY 服务:限定所有者范围的会话与本地 shell 后端 | | [`ptc-runtime/`](ptc-runtime/README.zh.md) | PTC 执行能力族:Service Definition + 沙箱 Node 提供方 + PTC mode Consumer | | [`computer-use/`](computer-use/README.zh.md) | 按名称独占注册桌面提供方 | | [`browser-use/`](browser-use/README.zh.md) | 按名称独占注册浏览器提供方 | @@ -53,9 +53,8 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会 | [`context/`](context/README.zh.md) | 模型可见请求上下文:workspace 指令、时间上下文、引用 | | [`subagent/`](subagent/README.zh.md) | subagent 能力系列:提供方注册表约定和面向模型的委托工具 | | [`jobs/`](jobs/README.zh.md) | 通用后台任务运行时和面向模型的作业控制工具 | -| [`experimental/`](experimental/README.zh.md) | 预稳定原型,包含显式私有例外 | -| [`workflow/`](workflow/README.zh.md) | 工作流 seam、PTC 进程引擎、面向模型的 `workflow`/`ralph` 工具 | -| [`webhook/`](webhook/README.zh.md) | 已验证外部事件、受信规则与即发即弃 Workspace 会话 | +| [`experimental/`](experimental/README.zh.md) | 仍在评估中的能力;[状态与支持策略](experimental/README.zh.md#status) | +| [`workflow/`](workflow/README.zh.md) | 工作流 seam、PTC 进程引擎、面向模型的 `workflow` 工具 | | [`web/`](web/README.zh.md) | Web 能力系列:seam、搜索/获取提供方、面向模型的 Web 工具 | | [`document/`](document/README.zh.md) | 共享宿主 Office 到 PDF 转换 | | [`attachment/`](attachment/README.zh.md) | 持久附件标识、校验、本地内容寻址存储 | @@ -67,7 +66,6 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会 | [`bundle/`](bundle/README.zh.md) | 可安装的 `dsh --profile` 补丁层 | | [`extensions/`](extensions/README.zh.md) | agent 运行时自修改:实时插件/服务检查与模型所写挂载/卸载 | | [`mcp/`](mcp/README.zh.md) | 将外部 Model Context Protocol 服务器的工具接入为原生工具 | -| [`hooks/`](hooks/README.zh.md) | 钩子桥接 + 共享的 Claude Code/Codex 线协议库 | | [`session/`](session/README.zh.md) | 持久会话数据平面:持久化 seam + 后端、投影 seam、基于日志的标题、会话上报 | | [`session-query/`](session-query/README.zh.md) | 会话检索系列:逻辑语料库、有界读取、血缘、语义过滤、SQLite 全文搜索 | | [`settings/`](settings/README.zh.md) | 用户设置 seam + 基于文件的提供方 | @@ -88,7 +86,7 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会 ## 发布预期 -大多数组属于产品组,提供稳定 API。例外:`experimental/` 发布时不提供稳定性或支持承诺,`test-support/` 与 `util/` 是兼容性预期较低的支持组。 +产品组承担维护职责,但其公开 API 仍处于预稳定阶段。[实验性状态](experimental/README.zh.md#status)独立于发布、安装和可选交付。非实验运行时包必须被产品使用或在[包分类策略](../scripts/product-package-policy.ts)中明确列出;SDK、构建、测试与声明基础设施分别分类。 ----- diff --git a/packages/acp/acp/README.i18n.yaml b/packages/acp/acp/README.i18n.yaml index 540d77f43a..68c18de182 100644 --- a/packages/acp/acp/README.i18n.yaml +++ b/packages/acp/acp/README.i18n.yaml @@ -27,8 +27,8 @@ en: 4ed8e37834267edb zh: a32d5dedb4e8e919 /deepseek-ai-dsh-acp/use-this-package/protocol-contract: - en: 804b87d8ebed9f8d - zh: 1b0c1c7269cdf040 + en: 29099e63f9a173c6 + zh: 80d98e87f442d335 /deepseek-ai-dsh-acp/understand-the-implementation: en: ea8afe89dbdd1934 zh: 6567979f92f34afd diff --git a/packages/acp/acp/README.md b/packages/acp/acp/README.md index 3fe8c04ff2..fa713b872f 100644 --- a/packages/acp/acp/README.md +++ b/packages/acp/acp/README.md @@ -68,11 +68,15 @@ One connection can run several sessions at once, each independent. The calls a c | `session/resume` | A persisted inactive session whose canonical workspace is verified before composition; its log is restored without replaying old updates. | | `session/close` | Quiescent cancellation, update draining, descendant disposal, persistence flush, and disposal of only the addressed Agent scope. | | `session/set_config_option` | A serialized update to the advertised `model` or `reasoning_effort`, returning the complete resulting state. | -| `session/prompt` | Ordered text, resource links, and supported images, one prompt at a time per session; settlement follows Agent idle and ordered update delivery. | +| `session/prompt` | Ordered text, resource links, and supported images, one prompt at a time per session; settlement follows root and descendant completion plus ordered update delivery. | | `session/cancel` / `$/cancel_request` | The prompt-owned cancellation path; without an ACP prompt in flight it cancels autonomous work, while unknown session ids are no-ops. | | `session/update` | Committed assistant messages and thoughts, generic tool lifecycle, configuration changes, and context usage, serialized per session. | | `session/request_permission` | A permission prompt with one-shot allow/reject choices; your client can answer automatically. | +A prompt remains in flight while managed descendants run, while their results trigger later root turns, and while committed output drains. Root work accepted during that output drain joins the same activity interval before completion. Its completion includes the latest root stop reason and any assistant output delivery failure; only the original prompt turn pins the model selection captured at admission. Cancellation stops the root activity present at that call and waits for that activity and its queued output. Later child messages may start autonomous work and do not delay the cancelled response; the session remains available for later prompts. Idle descendants with parked inbox messages remain resident but do not delay prompt completion. Closing the session also drains its descendants. + +A failure to append `turn/start` or `turn/end` rejects the prompt even when a preceding turn completed. + Session configuration offers opaque provider/model choices from the live LLM service catalog and a `reasoning_effort` selector when the exact model declares one. A prompt snapshots that selection before asynchronous image admission and pins it across every model step in that turn; a concurrent option change applies to the next turn. ACP clients are trusted controllers: stdio MCP entries authorize their absolute commands and environment, HTTP entries authorize their absolute HTTP(S) URLs and headers, and any initial connection or discovery failure rolls back the unpublished Agent. Unsupported surfaces are omitted or rejected: `session/load`, deletion, fork, additional directories, SSE or ACP-transport MCP, modes, commands, plans, terminals, client filesystem operations, and elicitation. ----- diff --git a/packages/acp/acp/README.zh.md b/packages/acp/acp/README.zh.md index 2b7404b56b..e62e154191 100644 --- a/packages/acp/acp/README.zh.md +++ b/packages/acp/acp/README.zh.md @@ -68,11 +68,15 @@ kind: "package-reference" | `session/resume` | 恢复一个已持久化且非活跃的会话;组合前校验其规范工作区,并恢复日志但不回放旧更新。 | | `session/close` | 停稳式取消、更新 drain、后代释放、持久化 flush,并且只释放指定 Agent 作用域。 | | `session/set_config_option` | 串行更新公布的 `model` 或 `reasoning_effort`,并返回完整结果状态。 | -| `session/prompt` | 有序文本、资源链接与受支持图片,每个会话一次一个提示词;Agent 空闲且有序更新交付后才结算。 | +| `session/prompt` | 有序文本、资源链接与受支持图片,每个会话一次一个提示词;根 Agent 与后代完成且有序更新交付后才结算。 | | `session/cancel` / `$/cancel_request` | 提示词所拥有的取消路径;没有进行中的 ACP 提示词时取消自主工作,未知会话 id 则为空操作。 | | `session/update` | 已提交 assistant 消息与 thought、通用工具生命周期、配置变化与上下文用量,按会话串行交付。 | | `session/request_permission` | 带一次性允许/拒绝选项的权限提示;你的客户端可以自动回答。 | +受管理后代运行期间、它们的结果触发根 Agent 后续轮次期间,以及已提交输出仍在交付期间,提示词保持进行中。输出交付期间接受的根 Agent 工作会加入同一活动区间,并在该区间完成后结算。最终结果包含最新根轮次的停止原因与所有助手输出交付错误;只有原始提示词轮次固定使用接受时捕获的模型选择。取消会停止调用时已有的根 Agent 活动,并等待该活动及其已排队输出完成。之后到达的子 Agent 消息可以启动自主工作,但不会延迟取消响应;会话仍可接受后续提示词。收件箱消息处于停放状态的空闲后代继续驻留,但不延迟提示词完成。关闭会话还会释放其后代。 + +即使前一轮已经完成,追加 `turn/start` 或 `turn/end` 失败仍会使提示词请求失败。 + 会话配置从实时 LLM(大语言模型)服务目录提供不透明的提供方/模型选项,并在确切模型声明推理选项时提供 `reasoning_effort`。提示词会在异步图片准入前快照该选择,并在该轮次的每个模型步骤中固定它;并发选项变更从下一轮次开始生效。ACP 客户端是受信控制器:stdio MCP 条目授权其绝对命令与环境,HTTP 条目授权其绝对 HTTP(S) URL 与 header;初始连接或发现失败会回滚尚未发布的 Agent。不支持的界面会被省略或拒绝:`session/load`、删除、fork、附加目录、SSE(Server-Sent Events)或 ACP 传输 MCP、mode、命令、计划、终端、客户端文件系统操作与 elicitation。 ----- diff --git a/packages/acp/acp/package.json b/packages/acp/acp/package.json index 69174fc2bb..5a9e332ce9 100644 --- a/packages/acp/acp/package.json +++ b/packages/acp/acp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-acp", "description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -59,6 +59,7 @@ "@deepseek-ai/dsh-session-persistence": "workspace:*", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*", "@deepseek-ai/dsh-session-projection": "workspace:*", + "@deepseek-ai/dsh-subagent": "workspace:*", "@deepseek-ai/dsh-token-meter": "workspace:*", "@deepseek-ai/dsh-tools": "workspace:*", "@deepseek-ai/dsh-user-approval": "workspace:*" diff --git a/packages/acp/acp/src/index.ts b/packages/acp/acp/src/index.ts index 44ef4741f1..0754df3562 100644 --- a/packages/acp/acp/src/index.ts +++ b/packages/acp/acp/src/index.ts @@ -141,8 +141,12 @@ export function apply(ctx: Context, config: AcpConfig): void { ownedRecord(agent)?.onInboxClaimed(message, turn) }) - ctx.on('agent/error', ({ agent, turn, error }) => { - ownedRecord(agent)?.onAgentError(turn, error) + ctx.on('agent/error', ({ agent, error }) => { + ownedRecord(agent)?.onAgentError(error) + }) + + ctx.on('agent/status', ({ agent, status }) => { + ownedRecord(agent)?.onAgentStatus(status) }) ctx.on('llm/adapters-updated', () => { diff --git a/packages/acp/acp/src/session.ts b/packages/acp/acp/src/session.ts index 3fff075359..9b6ee8fd10 100644 --- a/packages/acp/acp/src/session.ts +++ b/packages/acp/acp/src/session.ts @@ -10,7 +10,7 @@ import { type SessionNotification, type StopReason, } from '@agentclientprotocol/sdk' -import type { Agent, AgentHandle, AgentOptions, ModelSelection } from '@deepseek-ai/dsh-agent' +import type { Agent, AgentHandle, AgentOptions, AgentStatus, ModelSelection } from '@deepseek-ai/dsh-agent' import { createUserMessage, errorChain, type UserMessage } from '@deepseek-ai/dsh-llm' import { type Session, type SessionEvent, type SessionId, type TurnEndReason } from '@deepseek-ai/dsh-session' import { AcpContentError, admitAcpPrompt } from './content.ts' @@ -19,10 +19,12 @@ import { mountAcpMcpServers } from './mcp.ts' import { AcpModelControl } from './model-control.ts' import { assistantUpdates, toolCallUpdate, toolResultUpdate } from './updates.ts' -/** The continuable-subagent teardown used without depending on the subagent package. */ -interface ContinuableDrain { - /** Dispose continuable descendants below exact host-owned parents child-first. */ - drainContinuableDescendants(parents: readonly Agent[]): Promise +/** Subagent completion and teardown without depending on the subagent package. */ +interface SubagentLifecycle { + /** Dispose subagent descendants below exact host-owned parents child-first. */ + drainDescendants(parents: readonly Agent[]): Promise + /** Observe currently managed descendants without closing future admission. */ + waitForChildren(parent: Agent): Promise } /** Inputs shared by fresh and resumed ACP session construction. */ @@ -55,7 +57,8 @@ interface InflightPrompt { admissionDone: Promise finishAdmission: () => void admissionController: AbortController - cancelRequested: boolean + cancellationDone: PromiseWithResolvers + cancelStartedIdle: boolean settlementStarted: boolean outputError: Error | undefined agentError: Error | undefined @@ -234,7 +237,7 @@ export class AcpSession { } /** - * Admit, enqueue, and settle one prompt at whole-Agent quiescence. + * Admit one prompt and settle after the root, descendants, and updates finish. * @param params - standard ACP prompt request for this session. * @param imageEnabled - connection capability advertised at initialization. * @param requestSignal - JSON-RPC request cancellation signal. @@ -260,7 +263,8 @@ export class AcpSession { admissionDone: admission.promise, finishAdmission: admission.resolve, admissionController, - cancelRequested: false, + cancellationDone: Promise.withResolvers(), + cancelStartedIdle: false, settlementStarted: false, outputError: undefined, agentError: undefined, @@ -308,7 +312,7 @@ export class AcpSession { inflight.finishAdmission() } - if (inflight.cancelRequested) { + if (inflight.admissionController.signal.aborted) { this.settleAfterQuiescence(inflight) return { stopReason: await completion.promise } } @@ -343,6 +347,10 @@ export class AcpSession { * @param event - committed durable event. */ onSessionEvent(session: Session, event: SessionEvent): void { + if (event.type === 'turn/start' && this.inflight?.turn !== undefined) { + this.inflight.turn = event.data.turn + this.inflight.endReason = undefined + } try { if (event.type === 'assistant/message') { const inflight = this.inflight?.turn === event.data.turn ? this.inflight : undefined @@ -383,6 +391,7 @@ export class AcpSession { const inflight = this.inflight if (inflight !== undefined && event.type === 'turn/end' && inflight.turn === event.data.turn) { inflight.endReason = event.data.reason + inflight.agentError = undefined } if (event.type === 'turn/end') this.modelControl.releaseTurn(event.data.turn) } @@ -402,19 +411,29 @@ export class AcpSession { /** * Correlate an Agent interval failure with the active ACP prompt. - * @param turn - failed turn number. * @param error - original same-process failure. */ - onAgentError(turn: number, error: unknown): void { + onAgentError(error: unknown): void { const inflight = this.inflight if (inflight === undefined || !inflight.messageQueued) return - // AgentLoop balances an in-turn failure with durable turn/end; settlement - // reads that exact error reason. This slot records interval failures outside it. - if (inflight.turn === turn) return + // A later committed turn/end supersedes this live failure. Failed turn + // start/end appends have no terminal record and keep the failure observable. inflight.agentError = new Error(errorChain(error)) this.settleAfterQuiescence(inflight) } + /** + * Complete cancelled activity without following a later root driver. + * @param status - the emitted transition, which may precede a reentrant wake. + */ + onAgentStatus(status: AgentStatus): void { + const inflight = this.inflight + if (inflight === undefined || !inflight.messageQueued || !inflight.admissionController.signal.aborted) return + // Maintenance remains publicly idle; its next running transition belongs + // to work released after the maintenance task finished. + if (status === 'idle' || inflight.cancelStartedIdle) inflight.cancellationDone.resolve(this.outputTail) + } + /** Await every update queued before this call. */ drainUpdates(): Promise { return this.outputTail @@ -439,12 +458,12 @@ export class AcpSession { } catch (error: unknown) { failures.push(new Error('ACP session activity drain failed', { cause: error })) } - const subagents = this.ctx.get('subagents') as ContinuableDrain | undefined + const subagents = this.ctx.get('subagents') as SubagentLifecycle | undefined try { - await subagents?.drainContinuableDescendants([this.agent]) + await subagents?.drainDescendants([this.agent]) } catch (error: unknown) { - this.ctx.logger.warn(`acp: continuable subagent teardown failed: ${errorChain(error)}`) - failures.push(new Error('continuable subagent teardown failed', { cause: error })) + this.ctx.logger.warn(`acp: subagent teardown failed: ${errorChain(error)}`) + failures.push(new Error('subagent teardown failed', { cause: error })) } try { await this.ctx.sessions.flush(this.agent.session) @@ -474,10 +493,20 @@ export class AcpSession { private cancelPrompt(detail: string): void { const inflight = this.inflight if (inflight === undefined) return - inflight.cancelRequested = true + const firstCancellation = !inflight.admissionController.signal.aborted + if (firstCancellation) inflight.cancelStartedIdle = this.agent.status === 'idle' inflight.admissionController.abort(new Error(detail)) this.settleAfterQuiescence(inflight) - if (inflight.messageQueued) this.agent.cancel({ kind: 'user' }) + if (!inflight.messageQueued) { + inflight.cancellationDone.resolve() + return + } + this.agent.cancel({ kind: 'user' }) + if (firstCancellation) { + const done = inflight.cancellationDone + // Plugin disposal can detach status observers before this activity retires. + void this.agent.whenIdle().then(() => { done.resolve(this.outputTail) }, done.reject) + } } private settleAfterQuiescence(inflight: InflightPrompt): void { @@ -486,13 +515,26 @@ export class AcpSession { void (async () => { await inflight.admissionDone if (inflight.messageQueued) { - await this.agent.whenIdle() - await this.outputTail + const subagents = this.ctx.get('subagents') as SubagentLifecycle | undefined + const cancelled = inflight.cancellationDone.promise + const isCancelled = (): boolean => inflight.admissionController.signal.aborted + const idleAt = (seq: Session['seq']): boolean => this.agent.status === 'idle' && this.agent.session.seq === seq + while (true) { + await Promise.race([this.agent.whenIdle(), cancelled]) + if (isCancelled()) break + const idleSeq = this.agent.session.seq + const children = await Promise.race([subagents?.waitForChildren(this.agent), cancelled]) + if (isCancelled()) break + if (children || !idleAt(idleSeq)) continue + await this.outputTail + if (isCancelled() || idleAt(idleSeq)) break + } + if (isCancelled()) await cancelled } /* v8 ignore next -- this prompt owns the slot until this exact settlement clears it. */ if (this.inflight !== inflight) return this.inflight = undefined - if (inflight.cancelRequested) { + if (inflight.admissionController.signal.aborted) { inflight.resolve('cancelled') return } diff --git a/packages/acp/acp/tests/bridge.spec.ts b/packages/acp/acp/tests/bridge.spec.ts index cd9ede1641..c0f5d73f89 100644 --- a/packages/acp/acp/tests/bridge.spec.ts +++ b/packages/acp/acp/tests/bridge.spec.ts @@ -938,13 +938,13 @@ describe('automation-only ACP bridge', () => { }) it('renders the deployment persona for an ACP-created agent', async () => { - harness = await makeBridgeHarness({ persona: 'Automation persona for {{model}} in {{cwd}}.', script: [textResponse('ok')] }) + harness = await makeBridgeHarness({ persona: 'Automation persona for {{model}}.', script: [textResponse('ok')] }) await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] }) await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] }) const head = harness.adapter.requests[0]?.messages[0] expect(head?.role).toBe('system') - expect(head?.content).toContainEqual({ type: 'text', text: expect.stringContaining(`Automation persona for mock in ${process.cwd()}.`) as unknown }) + expect(head?.content.some(part => part.type === 'text' && part.text.includes('Automation persona for mock.'))).toBe(true) }) it('requires one absolute primary workspace', async () => { diff --git a/packages/acp/acp/tests/dispose.spec.ts b/packages/acp/acp/tests/dispose.spec.ts index 325255fb81..f607eabd18 100644 --- a/packages/acp/acp/tests/dispose.spec.ts +++ b/packages/acp/acp/tests/dispose.spec.ts @@ -69,7 +69,7 @@ describe('ACP connection ownership', () => { // A continuable Activation outlives the turn that started it, so the bridge // must release that forest before the agents whose runtime it depends on. harness.ctx.provide('subagents', { - drainContinuableDescendants: (parents: readonly Agent[]) => { + drainDescendants: (parents: readonly Agent[]) => { drainedParents = parents order.push('drained') return Promise.resolve() @@ -92,7 +92,7 @@ describe('ACP connection ownership', () => { const order: string[] = [] const release = Promise.withResolvers() harness.ctx.provide('subagents', { - drainContinuableDescendants: async () => { + drainDescendants: async () => { order.push('drain started') await release.promise order.push('drain finished') @@ -128,7 +128,7 @@ describe('ACP connection ownership', () => { const warnings: string[] = [] harness.ctx.logger.warn = (message: string) => { warnings.push(message) } harness.ctx.provide('subagents', { - drainContinuableDescendants: () => Promise.reject(new Error('activation teardown failed')), + drainDescendants: () => Promise.reject(new Error('activation teardown failed')), } as never) await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] }) @@ -136,7 +136,7 @@ describe('ACP connection ownership', () => { await harness.acpFiber.dispose() // A stuck descendant must not strand the bridge's own teardown. - expect(warnings.some(warning => warning.includes('continuable subagent teardown failed'))).toBe(true) + expect(warnings.some(warning => warning.includes('subagent teardown failed'))).toBe(true) expect(harness.ctx.agents.get(SessionId(sessionId))).toBeUndefined() }) diff --git a/packages/acp/acp/tests/turns.spec.ts b/packages/acp/acp/tests/turns.spec.ts index 0dffdc5836..1ef2bc94ce 100644 --- a/packages/acp/acp/tests/turns.spec.ts +++ b/packages/acp/acp/tests/turns.spec.ts @@ -1,8 +1,13 @@ +import { mountWorkingDirectoryFixture } from '../../../subagent/subagent/tests/working-directory-fixture.ts' import { createUserMessage, type StreamChunk } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' import { afterEach, describe, expect, it, vi } from 'vitest' import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk' import { SessionId } from '@deepseek-ai/dsh-session' +import { setImmediate } from 'node:timers/promises' +import SubagentRuntime, { NO_START_CAPABILITIES } from '@deepseek-ai/dsh-subagent' +import { defineContentToolFixture } from '@deepseek-ai/dsh-tools' +import { toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' import { errorResponse, makeBridgeHarness, @@ -30,6 +35,30 @@ function messageText(harness: BridgeHarness): string { )).join('') } +async function installDelegationTool(ctx: BridgeHarness['ctx']): Promise<() => boolean> { + await mountWorkingDirectoryFixture(ctx) + await ctx.plugin(SubagentRuntime) + ctx.subagents.registerProvider({ + name: 'local-test', capabilities: NO_START_CAPABILITIES, inheritsParentContext: false, + prepareContinuable: () => Promise.resolve({}), + }) + let delegated = false + ctx.tools.register(defineContentToolFixture({ + name: 'delegate_again', description: 'Run a later child.', parameters: {}, + execute: async (_args, exec) => { + if (exec.agent === undefined) throw new Error('delegation fixture requires an Agent') + const child = await ctx.subagents.startActivation({ + provider: 'local-test', label: 'later child', delivery: 'caller', signal: exec.signal, + request: { parent: exec.agent, prompt: [{ type: 'text', text: 'later child task' }] }, + }) + const result = await child.result + delegated = result.stopReason === 'completed' + return [...result.output] + }, + })) + return () => delegated +} + describe('ACP prompt lifecycle', () => { let harness: BridgeHarness | undefined @@ -177,6 +206,33 @@ describe('ACP prompt lifecycle', () => { vi.restoreAllMocks() }) + it.each(['turn/start', 'turn/end'] as const)('rejects a later %s append failure after an earlier completed turn', async (failedEvent) => { + harness = await makeBridgeHarness({ script: [textResponse('waiting'), textResponse('summary')] }) + const sessionId = await newSession(harness) + const parent = harness.ctx.agents.get(SessionId(sessionId))! + const append = parent.session.append.bind(parent.session) + const appendSpy = vi.spyOn(parent.session, 'append') + const failures: number[] = [] + harness.ctx.on('agent/error', ({ turn }) => { failures.push(turn) }) + const waitForChildren = vi.fn().mockImplementationOnce(() => { + appendSpy.mockImplementation(((type: string, ...rest: never[]) => { + if (type === failedEvent) throw new Error(`summary ${failedEvent} unavailable`) + return (append as (...args: never[]) => unknown)(type as never, ...rest) + }) as never) + parent.followup(createUserMessage({ content: [{ type: 'text', text: 'child result' }], source: { kind: 'test' } })) + return Promise.resolve(true) + }).mockResolvedValue(false) + harness.ctx.provide('subagents', { waitForChildren, drainDescendants: vi.fn() } as never) + try { + await expect(harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'delegate' }] })) + .rejects.toThrow(`summary ${failedEvent} unavailable`) + expect(failures).toEqual([failedEvent === 'turn/start' ? 1 : 2]) + expect(harness.adapter.requests).toHaveLength(failedEvent === 'turn/start' ? 1 : 2) + } finally { + appendSpy.mockRestore() + } + }) + it('settles even when an earlier turn observer throws', async () => { harness = await makeBridgeHarness({ script: [textResponse('answer')] }) harness.ctx.on('session/event', (_session, event) => { @@ -525,6 +581,370 @@ describe('ACP prompt lifecycle', () => { await vi.waitFor(() => { expect(messageText(harness!)).toBe('recovered') }) }) + it.each(['completed', 'error'] as const)('waits for descendants and reports the later %s summary turn', async (outcome) => { + harness = await makeBridgeHarness({ script: [ + textResponse('waiting'), + outcome === 'completed' ? textResponse('summary') : errorResponse('summary failed'), + ] }) + const sessionId = await newSession(harness) + const parent = harness.ctx.agents.get(SessionId(sessionId))! + const waiting = Promise.withResolvers() + const children = Promise.withResolvers() + const waitForChildren = vi.fn() + .mockImplementationOnce(() => { waiting.resolve(undefined); return children.promise }) + .mockResolvedValue(false) + harness.ctx.provide('subagents', { waitForChildren, drainDescendants: vi.fn() } as never) + let settled = false + const prompt = harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'delegate' }] }) + const observed = prompt.then(() => { settled = true }, () => { settled = true }) + await waiting.promise + expect(settled).toBe(false) + parent.followup(createUserMessage({ content: [{ type: 'text', text: 'child result' }], source: { kind: 'test' } })) + children.resolve(true) + if (outcome === 'completed') { + await expect(prompt).resolves.toEqual({ stopReason: 'end_turn' }) + expect(messageText(harness)).toBe('waitingsummary') + } else { + await expect(prompt).rejects.toThrow('summary failed') + } + await observed + }) + + it('cancels the child wait without closing the session to its next prompt', async () => { + harness = await makeBridgeHarness({ script: [textResponse('waiting'), textResponse('next')] }) + const sessionId = await newSession(harness) + const waiting = Promise.withResolvers() + const children = Promise.withResolvers() + const waitForChildren = vi.fn() + .mockImplementationOnce(() => { waiting.resolve(undefined); return children.promise }) + .mockResolvedValue(false) + const drainDescendants = vi.fn() + harness.ctx.provide('subagents', { waitForChildren, drainDescendants } as never) + const first = harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'delegate' }] }) + await waiting.promise + await harness.client.cancel({ sessionId }) + await expect(first).resolves.toEqual({ stopReason: 'cancelled' }) + expect(drainDescendants).not.toHaveBeenCalled() + children.resolve(true) + await expect(harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'again' }] })) + .resolves.toEqual({ stopReason: 'end_turn' }) + expect(messageText(harness)).toBe('waitingnext') + }) + + it('releases a cancelled prompt before a late child message finishes and permits another delegation', async () => { + harness = await makeBridgeHarness({ script: [ + textResponse('waiting'), textResponse('late autonomous answer'), + toolCallResponse('delegate-again', 'delegate_again', {}), + textResponse('later child answer'), textResponse('later parent answer'), + ] }) + const ctx = harness.ctx + const delegated = await installDelegationTool(ctx) + const sessionId = await newSession(harness) + const parent = ctx.agents.get(SessionId(sessionId))! + const waiting = Promise.withResolvers() + const children = Promise.withResolvers() + vi.spyOn(ctx.subagents, 'waitForChildren').mockImplementationOnce(() => { + waiting.resolve(undefined) + return children.promise + }) + const lateStarted = Promise.withResolvers() + const releaseLate = Promise.withResolvers() + const stream = harness.adapter.stream.bind(harness.adapter) + vi.spyOn(harness.adapter, 'stream').mockImplementation(async function* (options) { + if (options.messages.at(-1)?.content.some(block => block.type === 'text' && block.text === 'late child result')) { + lateStarted.resolve(undefined) + await releaseLate.promise + } + yield* stream(options) + }) + const cancel = parent.cancel.bind(parent) + let delivered = false + vi.spyOn(parent, 'cancel').mockImplementation((...args) => { + cancel(...args) + if (delivered) return + delivered = true + queueMicrotask(() => { + parent.followup(createUserMessage({ content: [{ type: 'text', text: 'late child result' }], source: { kind: 'test' } })) + children.resolve(true) + }) + }) + let result: { stopReason: string } | undefined + const first = harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'delegate' }] }) + .then((value) => { result = value; return value }) + try { + await waiting.promise + await harness.client.cancel({ sessionId }) + await lateStarted.promise + await setImmediate() + expect(result).toEqual({ stopReason: 'cancelled' }) + expect(parent.status).toBe('running') + releaseLate.resolve(undefined) + await parent.whenIdle() + await expect(harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'delegate again' }] })) + .resolves.toEqual({ stopReason: 'end_turn' }) + expect(delegated()).toBe(true) + expect(messageText(harness)).toContain('later parent answer') + } finally { + releaseLate.resolve(undefined) + await first + } + }) + + it('settles an initially cancelled prompt after its output without waiting for a later summary', async () => { + const script: StreamChunk[][] = [] + harness = await makeBridgeHarness({ script }) + const ctx = harness.ctx + const delegated = await installDelegationTool(ctx) + const ref = await harness.attachments!.saveImage({ data: Uint8Array.of(6), mediaType: 'image/png' }) + ctx.tools.register(defineContentToolFixture({ + name: 'continue_stream', description: 'Continue the interrupted response.', parameters: {}, + execute: () => Promise.resolve([{ type: 'text', text: 'continue until cancelled' }]), + })) + // Interrupted streams retain only text/reasoning, so a completed step supplies the queued image. + script.push([ + { type: 'block-start', index: 1, blockType: 'image' }, + { type: 'block-end', index: 1, block: { type: 'image', attachment: ref } }, + ...toolCallResponse('continue-stream', 'continue_stream', {}), + ], textResponse('late autonomous answer'), toolCallResponse('delegate-again', 'delegate_again', {}), + textResponse('later child answer'), textResponse('later parent answer')) + const streaming = Promise.withResolvers() + const cleanupStarted = Promise.withResolvers() + const releaseCleanup = Promise.withResolvers() + const reading = Promise.withResolvers() + const releaseImage = Promise.withResolvers() + const lateStarted = Promise.withResolvers() + const releaseLate = Promise.withResolvers() + harness.attachments!.beforeRead = () => { + reading.resolve(undefined) + return releaseImage.promise + } + const stream = harness.adapter.stream.bind(harness.adapter) + vi.spyOn(harness.adapter, 'stream').mockImplementation(async function* (options) { + const prompt = options.messages.at(-1)?.content + if (prompt?.some(block => block.type === 'text' && block.text === 'continue until cancelled')) { + const signal = options.signal + if (signal === undefined) throw new Error('interrupted stream requires a signal') + try { + yield { type: 'block-start', index: 0, blockType: 'text' } + yield { type: 'text-delta', index: 0, text: 'partial' } + streaming.resolve(undefined) + await new Promise((_resolve, reject) => { + if (signal.aborted) { + reject(new Error('aborted')) + return + } + signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true }) + }) + } finally { + cleanupStarted.resolve(undefined) + await releaseCleanup.promise + } + return + } + if (prompt?.some(block => block.type === 'text' && block.text === 'late child result')) { + lateStarted.resolve(undefined) + await releaseLate.promise + } + yield* stream(options) + }) + const sessionId = await newSession(harness) + const parent = ctx.agents.get(SessionId(sessionId))! + let result: { stopReason: string } | undefined + const first = harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'interrupted image' }] }) + .then((value) => { result = value; return value }) + try { + await streaming.promise + await harness.client.cancel({ sessionId }) + await cleanupStarted.promise + parent.followup(createUserMessage({ content: [{ type: 'text', text: 'late child result' }], source: { kind: 'test' } })) + expect(result).toBeUndefined() + releaseCleanup.resolve(undefined) + await Promise.all([reading.promise, lateStarted.promise]) + await setImmediate() + expect(result).toBeUndefined() + expect(messageText(harness)).toBe('') + releaseImage.resolve(undefined) + await vi.waitFor(() => { expect(result).toEqual({ stopReason: 'cancelled' }) }) + expect(parent.status).toBe('running') + expect(harness.updates.filter(update => update.sessionUpdate === 'agent_message_chunk')).toEqual([ + expect.objectContaining({ content: { type: 'image', data: 'Bg==', mimeType: 'image/png' } }), + expect.objectContaining({ content: { type: 'text', text: 'partial' } }), + ]) + releaseLate.resolve(undefined) + await parent.whenIdle() + await expect(harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'delegate again' }] })) + .resolves.toEqual({ stopReason: 'end_turn' }) + expect(delegated()).toBe(true) + expect(messageText(harness)).toContain('later parent answer') + } finally { + releaseCleanup.resolve(undefined) + releaseImage.resolve(undefined) + releaseLate.resolve(undefined) + await first + await parent.whenIdle() + } + }) + + it('waits for cancelled maintenance cleanup without following its later summary', async () => { + harness = await makeBridgeHarness({ script: [ + textResponse('late autonomous answer'), + toolCallResponse('delegate-again', 'delegate_again', {}), + textResponse('later child answer'), textResponse('later parent answer'), + ] }) + const ctx = harness.ctx + const delegated = await installDelegationTool(ctx) + const sessionId = await newSession(harness) + const parent = ctx.agents.get(SessionId(sessionId))! + const queued = Promise.withResolvers() + ctx.on('agent/inbox/inserted', ({ agent, message }) => { + if (agent === parent && message.content.some(block => block.type === 'text' && block.text === 'queued prompt')) { + queued.resolve(undefined) + } + }) + const cleanupStarted = Promise.withResolvers() + const releaseCleanup = Promise.withResolvers() + const lateStarted = Promise.withResolvers() + const releaseLate = Promise.withResolvers() + const stream = harness.adapter.stream.bind(harness.adapter) + vi.spyOn(harness.adapter, 'stream').mockImplementation(async function* (options) { + if (options.messages.at(-1)?.content.some(block => block.type === 'text' && block.text === 'late child result')) { + lateStarted.resolve(undefined) + await releaseLate.promise + } + yield* stream(options) + }) + const maintenance = parent.runMaintenance(async (signal) => { + await new Promise((resolve) => { + signal.addEventListener('abort', () => { resolve() }, { once: true }) + }) + cleanupStarted.resolve(undefined) + await releaseCleanup.promise + }) + let result: { stopReason: string } | undefined + const first = harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'queued prompt' }] }) + .then((value) => { result = value; return value }) + try { + await queued.promise + await harness.client.cancel({ sessionId }) + await cleanupStarted.promise + parent.followup(createUserMessage({ content: [{ type: 'text', text: 'late child result' }], source: { kind: 'test' } })) + await setImmediate() + expect(parent.status).toBe('idle') + expect(result).toBeUndefined() + releaseCleanup.resolve(undefined) + await lateStarted.promise + await maintenance + await vi.waitFor(() => { expect(result).toEqual({ stopReason: 'cancelled' }) }) + expect(parent.status).toBe('running') + releaseLate.resolve(undefined) + await parent.whenIdle() + await expect(harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'delegate again' }] })) + .resolves.toEqual({ stopReason: 'end_turn' }) + expect(delegated()).toBe(true) + expect(messageText(harness)).toContain('later parent answer') + } finally { + releaseCleanup.resolve(undefined) + releaseLate.resolve(undefined) + await maintenance + await first + await parent.whenIdle() + } + }) + + it.each([false, true])('waits for the later summary output and reports its delivery failure (%s)', async (fail) => { + const script: StreamChunk[][] = [textResponse('waiting')] + harness = await makeBridgeHarness({ script }) + const ref = await harness.attachments!.saveImage({ data: Uint8Array.of(4), mediaType: 'image/png' }) + script.push([ + { type: 'block-start', index: 0, blockType: 'image' }, + { type: 'block-end', index: 0, block: { type: 'image', attachment: ref } }, + { type: 'finish', reason: { kind: 'stop' } }, + ]) + const sessionId = await newSession(harness) + const parent = harness.ctx.agents.get(SessionId(sessionId))! + const reading = Promise.withResolvers() + const delivery = Promise.withResolvers() + harness.attachments!.beforeRead = async () => { + reading.resolve(undefined) + await delivery.promise + if (fail) throw new Error('summary image unavailable') + } + const waitForChildren = vi.fn().mockImplementationOnce(() => { + parent.followup(createUserMessage({ content: [{ type: 'text', text: 'child result' }], source: { kind: 'test' } })) + return Promise.resolve(true) + }).mockResolvedValue(false) + harness.ctx.provide('subagents', { waitForChildren, drainDescendants: vi.fn() } as never) + let settled = false + const prompt = harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'delegate' }] }) + const observed = prompt.then(() => { settled = true }, () => { settled = true }) + await reading.promise + expect(settled).toBe(false) + delivery.resolve(undefined) + if (fail) await expect(prompt).rejects.toThrow('assistant output delivery failed') + else await expect(prompt).resolves.toEqual({ stopReason: 'end_turn' }) + await observed + }) + + it('includes root work started during output draining and permits the next prompt to delegate', async () => { + const script: StreamChunk[][] = [] + harness = await makeBridgeHarness({ script }) + const ctx = harness.ctx + const delegated = await installDelegationTool(ctx) + const ref = await harness.attachments!.saveImage({ data: Uint8Array.of(5), mediaType: 'image/png' }) + script.push([ + { type: 'block-start', index: 0, blockType: 'image' }, + { type: 'block-end', index: 0, block: { type: 'image', attachment: ref } }, + { type: 'finish', reason: { kind: 'stop' } }, + ], textResponse('late autonomous answer'), toolCallResponse('delegate-again', 'delegate_again', {}), + textResponse('later child answer'), textResponse('later parent answer')) + const reading = Promise.withResolvers() + const releaseImage = Promise.withResolvers() + harness.attachments!.beforeRead = () => { + reading.resolve(undefined) + return releaseImage.promise + } + const sessionId = await newSession(harness) + const parent = ctx.agents.get(SessionId(sessionId))! + const noChildren = Promise.withResolvers() + vi.spyOn(ctx.subagents, 'waitForChildren').mockImplementationOnce(() => { + noChildren.resolve(undefined) + return Promise.resolve(false) + }) + const lateStarted = Promise.withResolvers() + const releaseLate = Promise.withResolvers() + const stream = harness.adapter.stream.bind(harness.adapter) + vi.spyOn(harness.adapter, 'stream').mockImplementation(async function* (options) { + if (options.messages.at(-1)?.content.some(block => block.type === 'text' && block.text === 'late autonomous turn')) { + lateStarted.resolve(undefined) + await releaseLate.promise + } + yield* stream(options) + }) + let result: { stopReason: string } | undefined + const first = harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'render image' }] }) + .then((value) => { result = value; return value }) + try { + await Promise.all([reading.promise, noChildren.promise]) + await setImmediate() + parent.followup(createUserMessage({ content: [{ type: 'text', text: 'late autonomous turn' }], source: { kind: 'test' } })) + await lateStarted.promise + releaseImage.resolve(undefined) + await setImmediate() + expect(result).toBeUndefined() + expect(parent.status).toBe('running') + releaseLate.resolve(undefined) + await expect(first).resolves.toEqual({ stopReason: 'end_turn' }) + expect(messageText(harness)).toContain('late autonomous answer') + await expect(harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'delegate again' }] })) + .resolves.toEqual({ stopReason: 'end_turn' }) + expect(delegated()).toBe(true) + } finally { + releaseImage.resolve(undefined) + releaseLate.resolve(undefined) + await first + } + }) + it('a failed turn with no retry still rejects', async () => { harness = await makeBridgeHarness({ script: [errorResponse('terminal boom')] }) let offered = 0 diff --git a/packages/api/account-controller/package.json b/packages/api/account-controller/package.json index 0bda49aa57..ae3bfe24a1 100644 --- a/packages/api/account-controller/package.json +++ b/packages/api/account-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-account-controller", "description": "Expose safe account operations over authenticated Remote", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/api/gateway/package.json b/packages/api/gateway/package.json index d2bd53e59f..0ee56662b0 100644 --- a/packages/api/gateway/package.json +++ b/packages/api/gateway/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-gateway", "description": "Typert Remote Host dispatcher and Client API endpoint", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -58,7 +58,7 @@ "@deepseek-ai/dsh-deque": "workspace:*", "@deepseek-ai/dsh-timeout": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "ws": "^8.21.0", + "ws": "^8.21.3", "@deepseek-ai/dsh-typert-protocol": "workspace:*" }, "peerDependencies": { diff --git a/packages/api/job-controller/package.json b/packages/api/job-controller/package.json index 99c3b80395..f9fd59429b 100644 --- a/packages/api/job-controller/package.json +++ b/packages/api/job-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-job-controller", "description": "Job Remote observation stream and the reference-counted client job-output service", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/api/remotes/package.json b/packages/api/remotes/package.json index 0a46885712..f09b9a96ae 100644 --- a/packages/api/remotes/package.json +++ b/packages/api/remotes/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-remotes", "description": "Remote BFF assembly for application-selected Host capabilities", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/api/session-controller/README.i18n.yaml b/packages/api/session-controller/README.i18n.yaml index 0e8234975a..18627b3c99 100644 --- a/packages/api/session-controller/README.i18n.yaml +++ b/packages/api/session-controller/README.i18n.yaml @@ -15,8 +15,8 @@ en: 1fdf707322fb6996 zh: 0204f7e9d4aa3cce /session-controller/use-this-package: - en: 77552723167e5d8e - zh: 1d77a657c707190a + en: ae2715276ec70675 + zh: 0937d1335c21eff2 /session-controller/client-references: en: 41bdda1ac32df558 zh: aa5d0e325924066b diff --git a/packages/api/session-controller/README.md b/packages/api/session-controller/README.md index d5739619c1..d5a7f2a7a7 100644 --- a/packages/api/session-controller/README.md +++ b/packages/api/session-controller/README.md @@ -31,7 +31,7 @@ History pages and follow opening snapshots carry one `{ type: 'event', event: Se The Client journal validates current Session event envelopes before publishing follow snapshots, live entries, or history pages. It reuses the browser-safe Session validators for required surface markers, exact replacement endpoints, earlier unique source seqs, embedded Assistant provider metadata, request-header omissions, and tool-error consistency. Invalid records fail without field stripping or normalization; range membership and source existence remain durable-log checks on the Host. -Each endpoint states its activation policy. List reads only stored headers and projection-cache rows: it never calls per-session stat or opens a cold Session body. A current-format cache identity may supply every list hint; a lifecycle-matching predecessor cache may supply only its version-compatible title as a stale display fact, never as an authoritative fold seed. Search, attachment, history pages, log following, skill discovery, and workspace-path opening can inspect persistence without activating an Agent; `canOpenWorkspacePath()` reports native-opening availability without addressing a Session. Cancellation requires live state; queue mutation, model, rename, prompt, and file-reference operations may resolve or resume an ordinary Session. Prompt rejects content with neither non-whitespace text nor an attachment before resolving the Agent or appending Session events; queue edits accept only non-empty text content. Prompt admission consumes opaque receipts from the injected [`fileUploads`](../../client/file-upload/README.md) Host service and resolves every same-Agent receipt before sending the complete ordered content list through `ctx.attachments`. Prompt retries whose `requestId` is already queued or logged return the original acceptance without inserting another message. Create and fork are the only operations that create a new Agent directly. The service applies one preset-aware resume policy and subagent ownership fence to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces. Queue mutation has one narrow exception: a live child whose current projected identity is continuable and comes from its own non-seed suffix accepts the ordinary Edit, Remove, and QueueDock Steer actions across both inbox destinations. One-shot, missing, unknown, corrupt, seed-only, or cold children remain rejected without resume. The skill catalog uses a live Agent when present or the recorded preset's standing scope when cold, so listing never starts an Agent. The authenticated delivery routes use `workspaceDesktop()` for the serving Host name and file-manager behavior. `openWorkspacePath({ path, action: "reveal" })` delegates file-manager navigation to the native adapter; omitting `action` opens the file-type association, including HTML and SVG. Both operations require the composed filesystem to map the requested Host path to the same canonical process path; unmapped remote paths are refused before a native command runs. `session.projections` reads one complete baseline through a live-preferred Session observation without activating an Agent. It returns null for a missing Session and serves any registered projection keys. The Client exposes shared values and explicit-read state through `projectionsBySession`; domains select their own keys. Session-list summaries carry `agentAvailable`, updated through existing summary and status events independently of durable projections. Initial reads and live projection frames use the same sequence ordering. +Each endpoint states its activation policy. List reads only stored headers and projection-cache rows: it never calls per-session stat or opens a cold Session body. A current-format cache identity may supply every list hint; a lifecycle-matching predecessor cache may supply only its version-compatible title as a stale display fact, never as an authoritative fold seed. Search, attachment, history pages, log following, skill discovery, and workspace-path opening can inspect persistence without activating an Agent; `canOpenWorkspacePath()` reports native-opening availability without addressing a Session. Cancellation requires live state; queue mutation, model, rename, prompt, and file-reference operations may resolve or resume an ordinary Session. Prompt rejects content with neither non-whitespace text nor an attachment before resolving the Agent or appending Session events; queue edits accept only non-empty text content. Prompt admission consumes opaque receipts from the injected [`fileUploads`](../../client/file-upload/README.md) Host service and resolves every same-Agent receipt before sending the complete ordered content list through `ctx.attachments`. Prompt retries whose `requestId` is already queued or logged return the original acceptance without inserting another message. Create and fork are the only operations that create a new Agent directly. The service applies one preset-aware resume policy and subagent ownership fence to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces. Queue mutation has one narrow exception: a live child whose current projected identity is continuable and comes from its own non-seed suffix accepts the ordinary Edit, Remove, and QueueDock Steer actions across both inbox destinations. One-shot, missing, unknown, corrupt, seed-only, or cold children remain rejected without resume. The skill catalog uses a live Agent's effective working directory and scoped registry when present. Cold listing uses the projected current directory, falling back to the recorded origin and then the configured directory default. It retains the preset standing scope without activating an Agent. The authenticated delivery routes use `workspaceDesktop()` for the serving Host name and file-manager behavior. `openWorkspacePath({ path, action: "reveal" })` delegates file-manager navigation to the native adapter; omitting `action` opens the file-type association, including HTML and SVG. Both operations require the composed filesystem to map the requested Host path to the same canonical process path; unmapped remote paths are refused before a native command runs. `session.projections` reads one complete baseline through a live-preferred Session observation without activating an Agent. It returns null for a missing Session and serves any registered projection keys. The Client exposes shared values and explicit-read state through `projectionsBySession`; domains select their own keys. Session-list summaries carry `agentAvailable`, updated through existing summary and status events independently of durable projections. Initial reads and live projection frames use the same sequence ordering. Host list generation yields between complete rows only when its configured work slice is exhausted; cheap remaining work returns without a forced yield. Request cancellation is checked per row and after each yield and rejects without partial results. Each row is synchronous; the list does not promise one cross-Session snapshot. A Session already queued as cold remains a cached row if it attaches during a yield: Client in-flight mutation replay corrects its availability, and sequenced projections supersede cached hints. diff --git a/packages/api/session-controller/README.zh.md b/packages/api/session-controller/README.zh.md index eb33197fa9..c6c7f51633 100644 --- a/packages/api/session-controller/README.zh.md +++ b/packages/api/session-controller/README.zh.md @@ -31,7 +31,7 @@ kind: "package-reference" Client journal 在发布 follow 快照、live entry 或历史页之前验证当前 Session 事件 envelope。它复用浏览器安全的 Session validator,检查必需的 surface marker、精确的 replacement endpoint、更早且唯一的 source seq、内嵌 Assistant 提供方元数据、request header 可选字段的省略规则以及工具错误一致性。无效 record 直接失败,不删除字段或归一化;范围成员与来源存在性仍由 Host 的持久日志检查。 -每个 endpoint 都声明自己的激活策略。列表只读取持久化 header 与 projection cache row,绝不调用逐 Session stat 或打开冷 Session body。当前格式 cache identity 可以提供全部列表 hint;生命周期匹配的 predecessor cache 只能提供版本兼容的 title,作为可能过时的展示事实,绝不能作为权威 fold seed。搜索、附件、历史页、日志跟随、skill 发现和工作区路径打开可以在不激活 Agent 的情况下检查 persistence;`canOpenWorkspacePath()` 无需指定 Session 即可报告原生打开能力。取消要求 live 状态;queue 变更、模型、重命名、prompt 和文件引用操作可以解析或恢复普通 Session。提示词会在解析 Agent 或追加 Session 事件前,拒绝既没有非空白文本也没有附件的 content;queue edit 只接受非空文本 content。prompt 准入从注入的 [`fileUploads`](../../client/file-upload/README.zh.md) Host 服务取得不透明凭证,在把完整有序内容列表交给 `ctx.attachments` 前解析每个属于同一 Agent 的凭证。`requestId` 已进入 queue 或日志时,prompt 重试直接返回原来的接受结果,不会重复插入消息。只有 create 与 fork 会直接创建新 Agent。该服务把同一套感知 preset 的恢复策略和 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。Queue 变更只有一个狭窄例外:当前 projection identity 为 continuable 且来自自身非 seed suffix 的在线 child,可以在两个 inbox 目标上使用普通 Edit、Remove 与 QueueDock Steer action。One-shot、缺失、未知、损坏、仅含 seed identity 或冷 child 继续被拒绝,且不会恢复。skill 目录优先使用已有 live Agent,否则使用所记录 preset 的常驻 scope,因此列表查询绝不会启动 Agent。经过鉴权的文件交付路由通过 `workspaceDesktop()` 获取提供服务的 Host 名称和文件管理器行为。`openWorkspacePath({ path, action: "reveal" })` 将文件管理器导航委托给原生适配器;省略 `action` 时按文件类型关联打开,包括 HTML 和 SVG。两种操作都要求当前文件系统将请求的 Host 路径映射到同一个规范进程路径;无法映射的远端路径会在执行原生命令之前被拒绝。 `session.projections` 通过一次 live-preferred Session observation 读取完整基线,不激活 Agent。Session 不存在时返回 null,并可提供任意已注册的 projection key。Client 通过 `projectionsBySession` 暴露共享值和显式读取状态,由领域选择自身的 key。Session 列表摘要携带 `agentAvailable`,通过已有摘要与状态事件更新,与持久化 projection 相互独立。初始读取与实时 projection 帧使用相同的序号排序规则。 +每个 endpoint 都声明自己的激活策略。列表只读取持久化 header 与 projection cache row,绝不调用逐 Session stat 或打开冷 Session body。当前格式 cache identity 可以提供全部列表 hint;生命周期匹配的 predecessor cache 只能提供版本兼容的 title,作为可能过时的展示事实,绝不能作为权威 fold seed。搜索、附件、历史页、日志跟随、skill 发现和工作区路径打开可以在不激活 Agent 的情况下检查 persistence;`canOpenWorkspacePath()` 无需指定 Session 即可报告原生打开能力。取消要求 live 状态;queue 变更、模型、重命名、prompt 和文件引用操作可以解析或恢复普通 Session。提示词会在解析 Agent 或追加 Session 事件前,拒绝既没有非空白文本也没有附件的 content;queue edit 只接受非空文本 content。prompt 准入从注入的 [`fileUploads`](../../client/file-upload/README.zh.md) Host 服务取得不透明凭证,在把完整有序内容列表交给 `ctx.attachments` 前解析每个属于同一 Agent 的凭证。`requestId` 已进入 queue 或日志时,prompt 重试直接返回原来的接受结果,不会重复插入消息。只有 create 与 fork 会直接创建新 Agent。该服务把同一套感知 preset 的恢复策略和 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。Queue 变更只有一个狭窄例外:当前 projection identity 为 continuable 且来自自身非 seed suffix 的在线 child,可以在两个 inbox 目标上使用普通 Edit、Remove 与 QueueDock Steer action。One-shot、缺失、未知、损坏、仅含 seed identity 或冷 child 继续被拒绝,且不会恢复。skill 目录优先使用已有 live Agent 的有效工作目录与限定范围内的注册表。冷态列表使用投影中的当前目录,依次回退到记录的起始目录与配置的默认目录。它保留 preset 常驻 scope,不会激活 Agent。经过鉴权的文件交付路由通过 `workspaceDesktop()` 获取提供服务的 Host 名称和文件管理器行为。`openWorkspacePath({ path, action: "reveal" })` 将文件管理器导航委托给原生适配器;省略 `action` 时按文件类型关联打开,包括 HTML 和 SVG。两种操作都要求当前文件系统将请求的 Host 路径映射到同一个规范进程路径;无法映射的远端路径会在执行原生命令之前被拒绝。 `session.projections` 通过一次 live-preferred Session observation 读取完整基线,不激活 Agent。Session 不存在时返回 null,并可提供任意已注册的 projection key。Client 通过 `projectionsBySession` 暴露共享值和显式读取状态,由领域选择自身的 key。Session 列表摘要携带 `agentAvailable`,通过已有摘要与状态事件更新,与持久化 projection 相互独立。初始读取与实时 projection 帧使用相同的序号排序规则。 Host 仅在配置的工作时间片耗尽时,于完整条目之间让出事件循环;剩余工作较少时直接返回,不强制让出。每行及每次让出之后检查请求取消,取消时拒绝返回部分结果。每行同步计算;列表不承诺跨 Session 的统一快照。已经进入冷态队列的 Session 若在让出期间接入,仍返回缓存行:Client 对进行中请求的变更重放会纠正其可用性,带序号的投影会覆盖缓存提示。 diff --git a/packages/api/session-controller/package.json b/packages/api/session-controller/package.json index dc50269e26..3f421fe675 100644 --- a/packages/api/session-controller/package.json +++ b/packages/api/session-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-session-controller", "description": "Session Remote commands, cold reads, and live control transport", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -105,7 +105,8 @@ "@deepseek-ai/dsh-util-time": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/dsh-util-workspace-path": "workspace:*", - "@deepseek-ai/dsh-workspace": "workspace:*" + "@deepseek-ai/dsh-workspace": "workspace:*", + "@deepseek-ai/dsh-working-directory": "workspace:*" }, "peerDependenciesMeta": { "@deepseek-ai/dsh-session-persistence": { @@ -160,6 +161,7 @@ "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/dsh-util-workspace-path": "workspace:*", "@deepseek-ai/dsh-workspace": "workspace:*", - "@types/mime-types": "^3.0.1" + "@types/mime-types": "^3.0.1", + "@deepseek-ai/dsh-working-directory": "workspace:*" } } diff --git a/packages/api/session-controller/src/client/sessions/manager.ts b/packages/api/session-controller/src/client/sessions/manager.ts index 50af09df13..db7236c1b2 100644 --- a/packages/api/session-controller/src/client/sessions/manager.ts +++ b/packages/api/session-controller/src/client/sessions/manager.ts @@ -162,7 +162,7 @@ export class SessionManager { for (const parentSessionId of this.projectionStores.keys()) { const child = this.projectionStores.get(parentSessionId)?.values().subagentCatalog ?.find(entry => entry.id === sessionId) - if (child !== undefined) { + if (child !== undefined && child.mode !== 'external') { return { parentSessionId, childSessionId: sessionId, @@ -248,7 +248,7 @@ export class SessionManager { const address = this.addresses.get(sessionId) const child = address === undefined ? undefined : this.projectionStores.get(address.parentSessionId)?.values().subagentCatalog ?.find(entry => entry.id === sessionId) - if (child !== undefined) { + if (child !== undefined && child.mode !== 'external') { // A catalogued child exists only after its delegated session has // durable history, even though child rows do not carry `blank`. session.handleBlank(false) diff --git a/packages/api/session-controller/src/client/sessions/service.ts b/packages/api/session-controller/src/client/sessions/service.ts index 8166b6f32d..964105a486 100644 --- a/packages/api/session-controller/src/client/sessions/service.ts +++ b/packages/api/session-controller/src/client/sessions/service.ts @@ -40,7 +40,7 @@ export interface SessionSummary { /** * New Session presentation and reuse eligibility, derived from the Host * summary, `sessionListMetadata`, and client acceptance/running observations. - * New Session reuses a blank one targeting the same workspace. Filtering + * Workspace reconnection may reuse a blank in the same workspace. Filtering * stays with the consumer: the store carries every row, while the Workspace * browser shows only the selected blank entry. */ @@ -633,6 +633,7 @@ export class ClientSessions implements ISessions { } for (const [parentId, projection] of Object.entries(projectionsBySession)) { for (const child of projection.values.subagentCatalog ?? []) { + if (child.mode === 'external') continue const childId = child.id const summary = byId[childId] const projectionValues = summary?.projectionValues ?? this.manager.projectionValues(childId) diff --git a/packages/api/session-controller/src/skill-catalog.ts b/packages/api/session-controller/src/skill-catalog.ts index d384052b3b..c9865b0e6b 100644 --- a/packages/api/session-controller/src/skill-catalog.ts +++ b/packages/api/session-controller/src/skill-catalog.ts @@ -4,6 +4,7 @@ import type { Context } from '@deepseek-ai/cordis' import type {} from '@deepseek-ai/dsh-agent-preset-registry/types' import { SessionQueryError } from '@deepseek-ai/dsh-session-query' import { isUserInvocable } from '@deepseek-ai/dsh-skill' +import type {} from '@deepseek-ai/dsh-working-directory' import type { ScopeKey } from '@deepseek-ai/dsh-scope' import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' import type { SkillListRequest, SkillListValue } from './types.ts' @@ -17,7 +18,7 @@ declare module '@deepseek-ai/cordis' { /** Host service backing `ctx.remote.skills` without activating a cold Agent. */ export class SessionSkillCatalog extends TypertRemoteService { - static inject = ['agents', 'sessionQuery', 'typert'] + static inject = ['agents', 'sessionQuery', 'typert', 'workingDirectory'] /** @param ctx - Host context carrying Session reads and optional skill/preset services. */ constructor(ctx: Context) { @@ -33,17 +34,20 @@ export class SessionSkillCatalog extends TypertRemoteService { */ @Remote async list(request: SkillListRequest, signal: AbortSignal): Promise { - void signal const { sessionId } = request - let cwd: string | undefined + let cwd: string let agentPreset: string | undefined try { using observation = await this.ctx.sessionQuery.observeSession(sessionId) if (observation.projections === undefined) { throw new Error('skill catalog requires a projected Session observation') } - cwd = observation.header.cwd + cwd = observation.projections.values.workingDirectory + ?? observation.header.cwd + ?? this.ctx.workingDirectory.defaultDirectory agentPreset = observation.projections.values.agentPreset ?? undefined + const live = this.ctx.agents.get(sessionId) + if (live !== undefined) cwd = await this.ctx.workingDirectory.ensure(live, signal) } catch (error: unknown) { if (error instanceof SessionQueryError && error.code === 'SESSION_QUERY_SESSION_NOT_FOUND') { @@ -55,9 +59,6 @@ export class SessionSkillCatalog extends TypertRemoteService { {}, ) } - if (cwd === undefined) { - throw new RemoteError('gateway/internal', `session "${sessionId}" has no project cwd`, {}) - } const live = this.ctx.agents.get(sessionId) const presets = this.ctx.get('agentPresets') diff --git a/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts b/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts index a22e35cee4..5a4d946bf0 100644 --- a/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts +++ b/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts @@ -67,9 +67,9 @@ async function commandHarness( mode: 'continuable', provider: 'test', label: 'child', })) } else if (childMode === 'one-shot') { - session.append('subagent/descriptor', snapshotSubagentDescriptor({ - mode: 'one-shot', provider: 'test', label: 'child', - })) + session.append('subagent/descriptor', { + version: SUBAGENT_DESCRIPTOR_VERSION, mode: 'one-shot', provider: 'test', label: 'child', + }) } else if (childMode === 'corrupt') { session.append('subagent/descriptor', { version: SUBAGENT_DESCRIPTOR_VERSION, diff --git a/packages/api/session-controller/tests/manager.client.spec.ts b/packages/api/session-controller/tests/manager.client.spec.ts index 4cab61367e..4da138a716 100644 --- a/packages/api/session-controller/tests/manager.client.spec.ts +++ b/packages/api/session-controller/tests/manager.client.spec.ts @@ -730,6 +730,17 @@ describe('Host Remote event routing', () => { }) describe('subagent catalogs', () => { + it('retains external membership without creating a local browsing address', ({ mock, remote }) => { + const manager = makeManager(mock, remote) + onTestFinished(() => manager.dispose()) + const entries = [{ id: S2, createdAt: 1, mode: 'external' as const, label: 'external task' }] + manager.handleControlFrame({ type: 'projection', sessionId: S1, key: 'subagentCatalog', seq: 0, value: entries }) + + expect(manager.getListSnapshot().projectionsBySession[S1]?.values.subagentCatalog).toEqual(entries) + expect(manager.subagentAddress(S2)).toBeUndefined() + expect(manager.getListSnapshot().items).toEqual([]) + }) + it('keeps a catalog-discovered child address across identity resolution and status frames', async ({ mock, remote, start }) => { remote.session.list.mockImplementation(() => Promise.resolve(ok({ items: [ summary(S1), diff --git a/packages/api/session-controller/tests/session-skills.host.spec.ts b/packages/api/session-controller/tests/session-skills.host.spec.ts index 795a6c0b29..939ef7a269 100644 --- a/packages/api/session-controller/tests/session-skills.host.spec.ts +++ b/packages/api/session-controller/tests/session-skills.host.spec.ts @@ -1,5 +1,6 @@ import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import { provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit' import type { Agent } from '@deepseek-ai/dsh-agent' import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session' import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query' @@ -9,7 +10,7 @@ import { SessionSkillCatalog } from '../src/skill-catalog.ts' function observation( sessionId: SessionId, - options: { readonly cwd?: string; readonly agentPreset?: string } = {}, + options: { readonly cwd?: string; readonly workingDirectory?: string | null; readonly agentPreset?: string } = {}, ): SessionObservation { const events = Object.freeze([]) const lease = (): SessionObservation => ({ @@ -28,6 +29,7 @@ function observation( asOfSeq: -1, values: { ...options.agentPreset === undefined ? {} : { agentPreset: options.agentPreset }, + ...options.workingDirectory === undefined ? {} : { workingDirectory: options.workingDirectory }, }, }, retain: lease, @@ -40,18 +42,20 @@ async function context(): Promise { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(AgentRegistry) + provideWorkingDirectoryFixture(ctx, '/catalog/default') return ctx } describe('SessionSkillCatalog', () => { - it('reads a cold Session catalog without resuming an Agent', async () => { + it('reads a moved cold Session catalog from its recorded current directory without resuming an Agent', async () => { const ctx = await context() const sessionId = SessionId('cold-skills') - const observed = observation(sessionId, { cwd: '/cold/project' }) + const observed = observation(sessionId, { cwd: '/cold/original', workingDirectory: '/cold/project' }) const dispose = vi.spyOn(observed, Symbol.dispose) const observeSession = vi.fn(() => Promise.resolve(observed)) ctx.provide('sessionQuery', { observeSession } as never) const resume = vi.spyOn(ctx.agents, 'resume') + const ensureDirectory = vi.spyOn(ctx.workingDirectory, 'ensure') const list = vi.fn(() => Promise.resolve([ { name: 'review', @@ -81,16 +85,47 @@ describe('SessionSkillCatalog', () => { expect(observeSession).toHaveBeenCalledWith(sessionId) expect(dispose).toHaveBeenCalledOnce() expect(resume).not.toHaveBeenCalled() + expect(ensureDirectory).not.toHaveBeenCalled() expect(ctx.agents.list()).toEqual([]) expect(list).toHaveBeenCalledWith({ cwd: '/cold/project', scope: undefined }) }) + it.each([ + { cwd: undefined, workingDirectory: '/cold/current', expected: '/cold/current' }, + { cwd: '/cold/original', workingDirectory: null, expected: '/cold/original' }, + { cwd: undefined, workingDirectory: null, expected: '/catalog/default' }, + { cwd: undefined, workingDirectory: undefined, expected: '/catalog/default' }, + ])('resolves cold directory metadata $cwd / $workingDirectory to $expected without filesystem checks', async ({ cwd, workingDirectory, expected }) => { + const ctx = await context() + try { + const sessionId = SessionId('cold-directory-fallback') + ctx.provide('sessionQuery', { + observeSession: () => Promise.resolve(observation(sessionId, { + ...cwd === undefined ? {} : { cwd }, + ...workingDirectory === undefined ? {} : { workingDirectory }, + })), + } as never) + const list = vi.fn(() => Promise.resolve([])) + ctx.provide('skills', { list } as never) + const ensure = vi.spyOn(ctx.workingDirectory, 'ensure') + const resume = vi.spyOn(ctx.agents, 'resume') + await expect(new SessionSkillCatalog(ctx).list({ sessionId }, new AbortController().signal)) + .resolves.toEqual({ skills: [] }) + expect(list).toHaveBeenCalledWith({ cwd: expected, scope: undefined }) + expect(ensure).not.toHaveBeenCalled() + expect(resume).not.toHaveBeenCalled() + } finally { + await ctx.fiber.dispose() + } + }) + it('uses a live Agent to address a preset-owned registry', async () => { const ctx = await context() const sessionId = SessionId('live-skills') const session = ctx.sessions.create(sessionId, { meta: { cwd: '/live/project' } }) const agent = { id: sessionId, session, status: 'idle', ctx } as Agent await ctx.agents.register(agent) + const ensureDirectory = vi.spyOn(ctx.workingDirectory, 'ensure').mockResolvedValue('/live/current') ctx.provide('sessionQuery', { observeSession: () => Promise.resolve(observation(sessionId, { cwd: '/live/project' })), } as never) @@ -113,7 +148,9 @@ describe('SessionSkillCatalog', () => { modelInvocable: false, }], }) - expect(scopedList).toHaveBeenCalledWith({ cwd: '/live/project', scope: agent }) + expect(scopedList).toHaveBeenCalledWith({ cwd: '/live/current', scope: agent }) + expect(ensureDirectory).toHaveBeenCalledWith(agent, expect.any(AbortSignal)) + expect(session.header.cwd).toBe('/live/project') expect(acquireScope).not.toHaveBeenCalled() }) @@ -192,22 +229,18 @@ describe('SessionSkillCatalog', () => { await expect(failed).rejects.toThrow('skill registry is absent') }) - it('rejects observations without projections or a project cwd', async () => { + it('rejects observations without projections', async () => { const ctx = await context() const sessionId = SessionId('incomplete-skills') const withoutProjections = { ...observation(sessionId, { cwd: '/project' }), projections: undefined } const observeSession = vi.fn() .mockResolvedValueOnce(withoutProjections) - .mockResolvedValueOnce(observation(sessionId)) ctx.provide('sessionQuery', { observeSession } as never) const catalog = new SessionSkillCatalog(ctx) const unprojected = catalog.list({ sessionId }, new AbortController().signal) await expect(unprojected).rejects.toMatchObject({ code: 'gateway/internal' }) await expect(unprojected).rejects.toThrow('projected Session observation') - const cwdless = catalog.list({ sessionId }, new AbortController().signal) - await expect(cwdless).rejects.toMatchObject({ code: 'gateway/internal' }) - await expect(cwdless).rejects.toThrow('has no project cwd') }) it('classifies a provider listing failure', async () => { diff --git a/packages/api/session-controller/tests/subagent-catalog.host.spec.ts b/packages/api/session-controller/tests/subagent-catalog.host.spec.ts index 3c8cf0ca00..4d9c4360fc 100644 --- a/packages/api/session-controller/tests/subagent-catalog.host.spec.ts +++ b/packages/api/session-controller/tests/subagent-catalog.host.spec.ts @@ -1,4 +1,5 @@ import { Context } from '@deepseek-ai/cordis' +import { provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit' import AgentRegistry from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' import SessionStore, { SESSION_FORMAT_VERSION, SessionLogOffset, SessionSeq, SessionId } from '@deepseek-ai/dsh-session' @@ -51,6 +52,7 @@ async function bench() { ), }) as never) installSessionReadTestServices(ctx) + provideWorkingDirectoryFixture(ctx) await ctx.plugin(SubagentRuntime) const controller = createSessionTestController(ctx, defaults) return { ctx, controller } diff --git a/packages/api/session-controller/tsconfig.host.json b/packages/api/session-controller/tsconfig.host.json index 9b80bc33f2..6376b437ca 100644 --- a/packages/api/session-controller/tsconfig.host.json +++ b/packages/api/session-controller/tsconfig.host.json @@ -23,34 +23,95 @@ "src/skill-catalog.ts" ], "references": [ - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" }, - { "path": "../../core/agent" }, - { "path": "../../core/agent-default-model" }, - { "path": "../../core/scope" }, - { "path": "../../core/session" }, - { "path": "../../context/file-reference" }, - { "path": "../../attachment/attachment" }, - { "path": "../../client/file-upload/tsconfig.host.json" }, - { "path": "../../client/connection/tsconfig.host.json" }, - { "path": "../../interaction/permission-presets" }, - { "path": "../../jobs/jobs" }, - { "path": "../../fs/fs" }, - { "path": "../../llm/llm" }, - { "path": "../../util/deque" }, - { "path": "../../util/native-command" }, - { "path": "../../preset/agent-preset-registry" }, - { "path": "../../session/session-persistence" }, - { "path": "../../session/session-projection" }, - { "path": "../../session/session-projection-cache" }, - { "path": "../../session/session-title" }, - { "path": "../../session-query/session-query" }, - { "path": "../../skill/skill" }, - { "path": "../../subagent/subagent" }, - { "path": "../../util/time" }, - { "path": "../../util/values" }, - { "path": "../../typert/protocol" }, - { "path": "../../typert/registry" }, - { "path": "../../workspace/workspace" } + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../core/agent" + }, + { + "path": "../../core/agent-default-model" + }, + { + "path": "../../core/scope" + }, + { + "path": "../../core/session" + }, + { + "path": "../../context/file-reference" + }, + { + "path": "../../attachment/attachment" + }, + { + "path": "../../client/file-upload/tsconfig.host.json" + }, + { + "path": "../../client/connection/tsconfig.host.json" + }, + { + "path": "../../interaction/permission-presets" + }, + { + "path": "../../jobs/jobs" + }, + { + "path": "../../fs/fs" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../util/deque" + }, + { + "path": "../../util/native-command" + }, + { + "path": "../../preset/agent-preset-registry" + }, + { + "path": "../../session/session-persistence" + }, + { + "path": "../../session/session-projection" + }, + { + "path": "../../session/session-projection-cache" + }, + { + "path": "../../session/session-title" + }, + { + "path": "../../session-query/session-query" + }, + { + "path": "../../skill/skill" + }, + { + "path": "../../subagent/subagent" + }, + { + "path": "../../util/time" + }, + { + "path": "../../util/values" + }, + { + "path": "../../typert/protocol" + }, + { + "path": "../../typert/registry" + }, + { + "path": "../../workspace/workspace" + }, + { + "path": "../../session/working-directory" + } ] } diff --git a/packages/api/settings-controller/package.json b/packages/api/settings-controller/package.json index 8601923a52..ded8c8bf22 100644 --- a/packages/api/settings-controller/package.json +++ b/packages/api/settings-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-settings-controller", "description": "Remote owner for the configuration surfaces over the settings-domain seams", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/api/terminal-controller/README.i18n.yaml b/packages/api/terminal-controller/README.i18n.yaml index d0ed5a8927..167264cbde 100644 --- a/packages/api/terminal-controller/README.i18n.yaml +++ b/packages/api/terminal-controller/README.i18n.yaml @@ -9,14 +9,14 @@ en: d5eb7912c65b4e26 zh: 7deb2a8167a82c90 /deepseek-ai-dsh-api-terminal-controller/summary: - en: 8e645f037a71a0f6 - zh: 421d962126692dd7 + en: afcf6f0d2f899ecc + zh: e38e2c0830e42ac9 /deepseek-ai-dsh-api-terminal-controller/table-of-contents: en: d152484eb41ac6b4 zh: 1fa9a72d1ee045cc /deepseek-ai-dsh-api-terminal-controller/use-this-package: - en: 0148cabb3a856226 - zh: 059a60413474a173 + en: abda92ff99530baa + zh: 67aa726392ae8e05 /deepseek-ai-dsh-api-terminal-controller/understand-the-implementation: en: 94dcd1835c55acc2 zh: cbfeb2da8cb60395 @@ -24,11 +24,11 @@ en: b1b5d2a2bfb8cc6a zh: aa0ea568aa63fda5 /deepseek-ai-dsh-api-terminal-controller/model-experience: - en: 2c52eec781441d2a - zh: 35a7c88f180f74a1 + en: 283529571e8cf73f + zh: 23336ccfbe65d315 /deepseek-ai-dsh-api-terminal-controller/model-experience/kv-cache-effect: - en: 507d3b3b80596fdc - zh: 4659007c4671afdc + en: be6f8e99d307c7ac + zh: 7dd4dcc0429e766c /deepseek-ai-dsh-api-terminal-controller/known-limitations-and-deferred-work: en: 6af6663b852441db zh: 244ed7729cae8d81 diff --git a/packages/api/terminal-controller/README.md b/packages/api/terminal-controller/README.md index 6fcdef9c4c..2709674163 100644 --- a/packages/api/terminal-controller/README.md +++ b/packages/api/terminal-controller/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Open the execution environment's default shell in a Session workspace from the Web sidebar. Reconnect to existing processes and close their complete provider-owned process ranges. Terminal output stays outside the Agent transcript. Keeping a terminal open retains its process and a bounded screen buffer. +Open the execution environment's default shell in the Session's current working directory from the Web sidebar. Reconnect to existing processes and close their complete provider-owned process ranges. Terminal output stays outside the Agent transcript. Keeping a terminal open retains its process and a bounded screen buffer. ## Table of Contents @@ -25,9 +25,9 @@ Open the execution environment's default shell in a Session workspace from the W ## Use this package -The Web bundle mounts this package with the subprocess provider, sandbox policy and Typert Gateway. Sandbox policy supplies only the fallback working directory for Sessions without a cwd. `remote.terminal` exposes `environment`, `shells`, `list`, `create`, `retain`, `follow`, `write`, `resize`, `rename` and `close`; each operation is scoped by Session identity. Listing reads retained Host terminals directly, so viewing an offline Session neither activates an Agent nor produces a recovery error. +The Web bundle mounts this package with the subprocess provider, working-directory service, sandbox policy and Typert Gateway. `remote.terminal` exposes `environment`, `shells`, `list`, `create`, `retain`, `follow`, `write`, `resize`, `rename` and `close`; each operation is scoped by Session identity. Listing reads retained Host terminals directly, so viewing an offline Session neither activates an Agent nor produces a recovery error. -Shell discovery lists the execution environment's declared default shell first. Only when the provider omits that default does resolution use `/bin/sh` on POSIX or `cmd.exe` on Windows. An optional `shell` profile overrides that choice with executable `path`, display `name` and `args` (default `[]`). The selector also probes `shellCandidates` through the execution provider and omits confirmed lookup misses. It lists each executable name once, ignoring letter case and `.exe`, and keeps the earliest entry, because PATH lookup can reach the default through another directory, such as `/usr/bin/bash` for `/bin/bash` on merged-`/usr` systems. Creation accepts a discovered `shellPath` and verifies it again; resolution or transport failure is reported without launching a different shell. Environment lookup returns the working directory and limits without resolving a shell, so an unavailable default does not prevent reattaching to an existing process. Automatic POSIX profiles start interactively, and PowerShell uses `-NoLogo`, so completion and startup configuration remain shell-owned. The Session workspace supplies the initial directory. User terminals run with the execution environment’s system-user permissions, independently of the Agent’s sandbox mode and approval policy. Operating-system and container restrictions still apply; DSH does not elevate the user. The subprocess provider retains its credential-environment scrubbing. +Shell discovery lists the execution environment's declared default shell first. Only when the provider omits that default does resolution use `/bin/sh` on POSIX or `cmd.exe` on Windows. An optional `shell` profile overrides that choice with executable `path`, display `name` and `args` (default `[]`). The selector also probes `shellCandidates` through the execution provider and omits confirmed lookup misses. It lists each executable name once, ignoring letter case and `.exe`, and keeps the earliest entry, because PATH lookup can reach the default through another directory, such as `/usr/bin/bash` for `/bin/bash` on merged-`/usr` systems. Creation accepts a discovered `shellPath` and verifies it again; resolution or transport failure is reported without launching a different shell. Environment lookup reads the current directory and limits without filesystem validation or shell resolution, so unavailable directories or default shells do not prevent reattaching to an existing process. Automatic POSIX profiles start interactively, and PowerShell uses `-NoLogo`, so completion and startup configuration remain shell-owned. New terminals validate the current directory through the [working-directory service](../../session/working-directory/README.md) before spawning, including its recovery behavior. Existing terminals keep their process directories. User terminals run with the execution environment’s system-user permissions, independently of the Agent’s sandbox mode and approval policy. Operating-system and container restrictions still apply; DSH does not elevate the user. The subprocess provider retains its credential-environment scrubbing. | Configuration | Default | Meaning | |---|---|---| @@ -76,11 +76,11 @@ Closing saves an unfinished cleanup request before releasing the tab, then await ## Model Experience -None, as this package handles user terminal interaction without adding model input. +Indirectly, through [working-directory recovery](../../session/working-directory/README.md): creating a terminal may update directory user context, while terminal output stays outside model history. #### KV Cache effect -None; terminal output travels only between the browser and Host. +Directory recovery appends user context without replacing the system-prompt prefix; terminal output stays outside model requests. ## Known Limitations and Deferred Work diff --git a/packages/api/terminal-controller/README.zh.md b/packages/api/terminal-controller/README.zh.md index e8d1b9b172..49fc7ab343 100644 --- a/packages/api/terminal-controller/README.zh.md +++ b/packages/api/terminal-controller/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -从 Web 侧栏在会话工作区打开执行环境的默认 shell。重新连接已有进程,并关闭 provider 管理的完整进程范围。终端输出不进入 Agent 对话记录。保留终端会占用进程和有界屏幕缓存。 +从 Web 侧栏在 Session 当前工作目录打开执行环境的默认 shell。重新连接已有进程,并关闭 provider 管理的完整进程范围。终端输出不进入 Agent 对话记录。保留终端会占用进程和有界屏幕缓存。 ## 目录 @@ -25,9 +25,9 @@ kind: "package-reference" ## 使用此包 -Web bundle 将此包与 subprocess provider、sandbox policy 和 Typert Gateway 一起挂载。Sandbox policy 仅为没有 cwd 的 Session 提供默认工作目录。`remote.terminal` 提供 `environment`、`shells`、`list`、`create`、`retain`、`follow`、`write`、`resize`、`rename` 和 `close`;每个操作均按 Session 标识限定范围。列表直接读取 Host 保留的终端,因此查看离线 Session 不会激活 Agent,也不会产生恢复错误。 +Web bundle 将此包与 subprocess provider、working-directory 服务、sandbox policy 和 Typert Gateway 一起挂载。`remote.terminal` 提供 `environment`、`shells`、`list`、`create`、`retain`、`follow`、`write`、`resize`、`rename` 和 `close`;每个操作均按 Session 标识限定范围。列表直接读取 Host 保留的终端,因此查看离线 Session 不会激活 Agent,也不会产生恢复错误。 -Shell 探测结果首先列出执行环境声明的默认 shell。仅当 provider 未声明默认值时,才在 POSIX 使用 `/bin/sh`,在 Windows 使用 `cmd.exe`。可选的 `shell` profile 通过可执行路径 `path`、显示名称 `name` 和参数 `args`(默认 `[]`)覆盖这一选择。选择器还会通过执行 provider 探测 `shellCandidates`,并省略确定未找到的候选。同一可执行文件名(不区分大小写,忽略 `.exe`)只列出最早的一项,因为 PATH 查找可能经由其他目录找到默认 shell,例如 merged-`/usr` 系统上 `/bin/bash` 对应的 `/usr/bin/bash`。创建请求接受探测返回的 `shellPath` 并再次验证;解析或传输失败会直接报告,不启动其他 shell。环境查询只返回工作目录和限制,不解析 shell,因此默认 shell 不可用时仍可重新连接已有进程。POSIX 自动 profile 以交互模式启动,PowerShell 使用 `-NoLogo`,补全和启动配置仍由 shell 提供。初始目录来自 Session 工作区。用户终端使用执行环境中系统用户的权限,独立于 Agent 的沙箱模式和审批策略。操作系统和容器的限制仍然生效;DSH 不提升用户权限。Subprocess provider 继续清除环境中的凭据变量。 +Shell 探测结果首先列出执行环境声明的默认 shell。仅当 provider 未声明默认值时,才在 POSIX 使用 `/bin/sh`,在 Windows 使用 `cmd.exe`。可选的 `shell` profile 通过可执行路径 `path`、显示名称 `name` 和参数 `args`(默认 `[]`)覆盖这一选择。选择器还会通过执行 provider 探测 `shellCandidates`,并省略确定未找到的候选。同一可执行文件名(不区分大小写,忽略 `.exe`)只列出最早的一项,因为 PATH 查找可能经由其他目录找到默认 shell,例如 merged-`/usr` 系统上 `/bin/bash` 对应的 `/usr/bin/bash`。创建请求接受探测返回的 `shellPath` 并再次验证;解析或传输失败会直接报告,不启动其他 shell。环境查询读取当前目录和限制,不校验文件系统或解析 shell,因此目录或默认 shell 不可用时仍可重新连接已有进程。POSIX 自动 profile 以交互模式启动,PowerShell 使用 `-NoLogo`,补全和启动配置仍由 shell 提供。新终端启动前通过[工作目录服务](../../session/working-directory/README.zh.md)校验当前目录,并采用该服务的恢复行为。已有终端保留其进程目录。用户终端使用执行环境中系统用户的权限,独立于 Agent 的沙箱模式和审批策略。操作系统和容器的限制仍然生效;DSH 不提升用户权限。Subprocess provider 继续清除环境中的凭据变量。 | 配置 | 默认值 | 含义 | |---|---|---| @@ -76,11 +76,11 @@ Client 在分配前将每条 Session/内容与终端身份的关联保存到独 ## 模型体验 -无;此包只处理用户交互式终端,不向模型请求添加内容。 +间接影响由[工作目录恢复](../../session/working-directory/README.zh.md)拥有:创建终端可能更新目录用户上下文,终端输出仍不进入模型历史。 #### KV 缓存影响 -无;终端输出只在浏览器与 Host 之间传输。 +目录恢复追加用户上下文,不替换 system-prompt 前缀;终端输出仍不进入模型请求。 ## 已知限制与延期工作 diff --git a/packages/api/terminal-controller/package.json b/packages/api/terminal-controller/package.json index aea86312b3..9b67bd26cc 100644 --- a/packages/api/terminal-controller/package.json +++ b/packages/api/terminal-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-terminal-controller", "description": "Session-owned interactive terminals with shell discovery, screen recovery and typed Remote control", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -80,7 +80,8 @@ "@deepseek-ai/dsh-session-projection": "workspace:*", "@deepseek-ai/dsh-subprocess": "workspace:*", "@deepseek-ai/dsh-subprocess-local": "workspace:*", - "@deepseek-ai/dsh-util-crypto": "workspace:*" + "@deepseek-ai/dsh-util-crypto": "workspace:*", + "@deepseek-ai/dsh-working-directory": "workspace:*" }, "files": [ "lib/index.js", diff --git a/packages/api/terminal-controller/src/index.ts b/packages/api/terminal-controller/src/index.ts index 24bf1a1f01..953c34a9fc 100644 --- a/packages/api/terminal-controller/src/index.ts +++ b/packages/api/terminal-controller/src/index.ts @@ -4,6 +4,7 @@ import z from '@deepseek-ai/schemastery' import type { Agent } from '@deepseek-ai/dsh-agent' import type { SessionId } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-sandbox-policy' +import type {} from '@deepseek-ai/dsh-working-directory' import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' import { discoverShells, resolveShell } from './shells.ts' import { BrowserTerminal } from './terminal.ts' @@ -73,7 +74,7 @@ interface OwnedSession { /** Typed Remote control of transient Session-owned terminal processes. */ export class TerminalController extends TypertRemoteService { - static inject = ['subprocess', 'sandboxPolicy', 'typert'] + static inject = ['subprocess', 'sandboxPolicy', 'workingDirectory', 'typert'] static Config: z = z.object({ shell: z.union([z.object({ path: z.string().required(), name: z.string().required(), args: z.array(z.string()).default([]), @@ -109,16 +110,16 @@ export class TerminalController extends TypertRemoteService { } /** - * Read the Session working directory and terminal limits without resolving a shell. + * Read the Session's current directory and terminal limits without filesystem validation or shell lookup. * @param agent - Session owner supplied by the Gateway. * @param signal - request cancellation. - * @returns the Session workspace directory and terminal limits. + * @returns the logged current directory and terminal limits; retained terminals keep their own process directories. */ @Remote environment(agent: Agent, signal: AbortSignal): TerminalEnvironment { signal.throwIfAborted() - const { sandboxPolicy } = this.execution(agent) - return { cwd: agent.session.header.cwd ?? sandboxPolicy.workspaceRoot, + const { workingDirectory } = this.execution(agent) + return { cwd: workingDirectory.get(agent.session), maxInputBytes: this.config.maxInputBytes, maxCols: this.config.maxCols, maxRows: this.config.maxRows, scrollback: this.config.scrollback } } @@ -328,29 +329,31 @@ export class TerminalController extends TypertRemoteService { || !Number.isSafeInteger(rows) || rows < 1 || rows > this.config.maxRows) throw new Error('Terminal dimensions exceed the configured limits') } - private execution(agent: Agent): { subprocess: Context['subprocess']; sandboxPolicy: Context['sandboxPolicy'] } { + private execution(agent: Agent): { subprocess: Context['subprocess']; sandboxPolicy: Context['sandboxPolicy']; workingDirectory: Context['workingDirectory'] } { // The Agent context selects execution providers but does not inject consumer services. const subprocess = agent.ctx.get('subprocess') const sandboxPolicy = agent.ctx.get('sandboxPolicy') if (subprocess === undefined || sandboxPolicy === undefined) throw new Error('The Session execution environment requires subprocess and sandbox policy providers') - return { subprocess, sandboxPolicy } + const workingDirectory = agent.ctx.get('workingDirectory') + if (workingDirectory === undefined) throw new Error('The Session execution environment requires a working-directory provider') + return { subprocess, sandboxPolicy, workingDirectory } } private async spawn(agent: Agent, owner: OwnedSession, request: TerminalCreateRequest, signal: AbortSignal): Promise { - const environment = this.environment(agent, signal) - const { subprocess } = this.execution(agent) + const { subprocess, workingDirectory } = this.execution(agent) + const cwd = await workingDirectory.ensure(agent, signal) const shell = request.shellPath === undefined ? await resolveShell(subprocess, this.config.shell, signal) : (await this.shells(agent, signal)).find(candidate => candidate.path === request.shellPath) if (shell === undefined) throw new Error('Selected shell is not available in this execution environment') const handle = await subprocess.spawnTerminal({ - argv: [shell.path, ...shell.args], cwd: environment.cwd, cols: request.cols, rows: request.rows, + argv: [shell.path, ...shell.args], cwd, cols: request.cols, rows: request.rows, terminalType: 'xterm-256color', env: { DSH_SESSION_ID: agent.id }, shellActivity: true, graceMs: this.config.disposeGraceMs, signal, }) const info: WebTerminalInfo = { - id: request.id, shell, title: shell.name, cwd: environment.cwd, + id: request.id, shell, title: shell.name, cwd, cols: request.cols, rows: request.rows, state: 'running', exitCode: null, } try { diff --git a/packages/api/terminal-controller/src/types.ts b/packages/api/terminal-controller/src/types.ts index 5e1df56868..8490ea1a7f 100644 --- a/packages/api/terminal-controller/src/types.ts +++ b/packages/api/terminal-controller/src/types.ts @@ -30,8 +30,9 @@ export interface TerminalShell { readonly name: string } -/** Working directory and limits shared by new and restored terminals. */ +/** Current Session directory and terminal limits; retained terminals keep their own process directories. */ export interface TerminalEnvironment { + /** Logged current directory, read without filesystem validation. */ readonly cwd: string readonly maxInputBytes: number readonly maxCols: number diff --git a/packages/api/terminal-controller/tests/controller.spec.ts b/packages/api/terminal-controller/tests/controller.spec.ts index b229ceb169..d9f7ee273f 100644 --- a/packages/api/terminal-controller/tests/controller.spec.ts +++ b/packages/api/terminal-controller/tests/controller.spec.ts @@ -30,7 +30,12 @@ function fixture(overrides: Partial = {}) { roots.push(ctx) const effects = vi.spyOn(ctx.fiber, 'effect') const sandboxPolicy = { defaultMode: 'danger-full-access', workspaceRoot: '/workspace', resolve: vi.fn((): SandboxExecutionPolicy => ({ mode: 'danger-full-access', workspaceRoot: '/workspace' })) } + const workingDirectory = { + get: vi.fn<(session: Agent['session']) => string>(session => session.header.cwd ?? '/workspace'), + ensure: vi.fn<(agent: Agent, signal?: AbortSignal) => Promise>(async agent => agent.session.header.cwd ?? '/workspace'), + } ctx.provide('sandboxPolicy', sandboxPolicy as never) + ctx.provide('workingDirectory', workingDirectory as never) const output = new PassThrough() const done = Promise.withResolvers<{ exitCode: number; signal: null }>() const handle = { @@ -48,7 +53,7 @@ function fixture(overrides: Partial = {}) { if (result?.type !== 'return' || typeof result.value !== 'function') throw new Error(`Missing effect: ${label}`) return result.value() } - return { ctx, agent: owner(ctx), controller, subprocess, handle, sandboxPolicy, disposeEffect } + return { ctx, agent: owner(ctx), controller, subprocess, handle, sandboxPolicy, workingDirectory, disposeEffect } } describe('TerminalController', () => { @@ -100,6 +105,75 @@ describe('TerminalController', () => { expect(sandboxPolicy.resolve).not.toHaveBeenCalled() }) + it('uses the current directory for new user terminals while retaining earlier directories and permissions', async () => { + const { ctx, controller, agent, subprocess, sandboxPolicy, workingDirectory } = fixture() + const policy: SandboxExecutionPolicy = { mode: 'workspace-write', workspaceRoot: '/workspace', sessionId: agent.id } + sandboxPolicy.resolve.mockReturnValue(policy) + const confine = vi.fn(async (argv: readonly string[], _policy: SandboxExecutionPolicy) => ({ argv: [...argv] })) + ctx.provide('sandbox', { confine } as never) + const first = await controller.create(agent, request, signal()) + workingDirectory.get.mockReturnValue('/workspace/selected') + workingDirectory.ensure.mockResolvedValue('/workspace/selected') + + expect(controller.environment(agent, signal()).cwd).toBe('/workspace/selected') + expect(workingDirectory.ensure).toHaveBeenCalledOnce() + expect(await controller.create(agent, request, signal())).toBe(first) + expect(first.cwd).toBe('/workspace') + const second = await controller.create(agent, { ...request, id: 'second' as WebTerminalId }, signal()) + + expect(second.cwd).toBe('/workspace/selected') + expect(subprocess.spawnTerminal).toHaveBeenNthCalledWith(1, expect.objectContaining({ cwd: '/workspace' })) + expect(subprocess.spawnTerminal).toHaveBeenNthCalledWith(2, expect.objectContaining({ cwd: '/workspace/selected' })) + expect(confine).not.toHaveBeenCalled() + expect(sandboxPolicy.resolve).not.toHaveBeenCalled() + expect(controller.list(agent.id).map(terminal => terminal.cwd)).toEqual(['/workspace', '/workspace/selected']) + }) + + it('captures the validated directory before a delayed shell lookup', async () => { + const { controller, agent, subprocess, workingDirectory } = fixture() + workingDirectory.ensure.mockResolvedValue('/workspace/starting') + const entered = Promise.withResolvers() + const release = Promise.withResolvers() + subprocess.resolveExecutable.mockImplementationOnce(async (path) => { + entered.resolve(undefined) + await release.promise + return path + }) + const creating = controller.create(agent, request, signal()) + try { + await entered.promise + workingDirectory.get.mockReturnValue('/workspace/later') + workingDirectory.ensure.mockResolvedValue('/workspace/later') + release.resolve(undefined) + expect((await creating).cwd).toBe('/workspace/starting') + expect(subprocess.spawnTerminal).toHaveBeenCalledWith(expect.objectContaining({ cwd: '/workspace/starting' })) + expect(workingDirectory.ensure).toHaveBeenCalledOnce() + } finally { + release.resolve(undefined) + await creating + } + }) + + it('uses recovered directory values for new creation and leaves environment reads unvalidated', async () => { + const { controller, agent, subprocess, workingDirectory } = fixture() + workingDirectory.get.mockReturnValue('/missing') + workingDirectory.ensure.mockResolvedValue('/workspace/recovered') + expect(controller.environment(agent, signal()).cwd).toBe('/missing') + expect(workingDirectory.ensure).not.toHaveBeenCalled() + + const created = await controller.create(agent, request, signal()) + + expect(created.cwd).toBe('/workspace/recovered') + expect(workingDirectory.ensure).toHaveBeenCalledWith(agent, expect.any(AbortSignal)) + expect(subprocess.spawnTerminal).toHaveBeenCalledWith(expect.objectContaining({ cwd: '/workspace/recovered' })) + workingDirectory.ensure.mockRejectedValue(new Error('original directory is unavailable')) + expect(controller.environment(agent, signal()).cwd).toBe('/missing') + expect(await controller.create(agent, request, signal())).toBe(created) + await expect(controller.create(agent, { ...request, id: 'unavailable' as WebTerminalId }, signal())) + .rejects.toThrow('original directory is unavailable') + expect(subprocess.spawnTerminal).toHaveBeenCalledOnce() + }) + it('deduplicates concurrent creates, scopes access by Session, and preserves terminals when a stream aborts', async () => { const { controller, agent, ctx, subprocess, handle } = fixture() const [first, second] = await Promise.all([controller.create(agent, request, signal()), controller.create(agent, request, signal())]) @@ -299,13 +373,15 @@ describe('TerminalController', () => { expect(subprocess.spawnTerminal).toHaveBeenCalledWith(expect.objectContaining({ cwd: '/another-workspace' })) }) - it.each(['subprocess', 'sandboxPolicy'] as const)('fails clearly when the Session lacks %s', (missing) => { - const { controller, subprocess, sandboxPolicy } = fixture() + it.each(['subprocess', 'sandboxPolicy', 'workingDirectory'] as const)('fails clearly when the Session lacks %s', (missing) => { + const { controller, subprocess, sandboxPolicy, workingDirectory } = fixture() const isolated = new Context() roots.push(isolated) if (missing !== 'subprocess') isolated.provide('subprocess', subprocess as never) if (missing !== 'sandboxPolicy') isolated.provide('sandboxPolicy', sandboxPolicy as never) - expect(() => controller.environment(owner(isolated), signal())).toThrow('requires subprocess and sandbox policy providers') + if (missing !== 'workingDirectory') isolated.provide('workingDirectory', workingDirectory as never) + expect(() => controller.environment(owner(isolated), signal())).toThrow(missing === 'workingDirectory' + ? 'requires a working-directory provider' : 'requires subprocess and sandbox policy providers') }) it('allows Agent sandbox-mode changes while retaining the same user terminal', async () => { diff --git a/packages/api/terminal-controller/tsconfig.host.json b/packages/api/terminal-controller/tsconfig.host.json index ec31d46046..68bc1a559e 100644 --- a/packages/api/terminal-controller/tsconfig.host.json +++ b/packages/api/terminal-controller/tsconfig.host.json @@ -38,6 +38,9 @@ { "path": "../../session/session-projection" }, + { + "path": "../../session/working-directory" + }, { "path": "../../subprocess/subprocess" }, diff --git a/packages/api/workspace-controller/package.json b/packages/api/workspace-controller/package.json index 8cd898beed..118a4772a9 100644 --- a/packages/api/workspace-controller/package.json +++ b/packages/api/workspace-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-workspace-controller", "description": "Workspace Remote commands and reconnect-safe state transport", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/api/workspace-files/package.json b/packages/api/workspace-files/package.json index d1537d23c5..01a35b7da2 100644 --- a/packages/api/workspace-files/package.json +++ b/packages/api/workspace-files/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-workspace-files", "description": "Workspace file service and Client resource provider: bounded reads, directory listing, and live metadata over the workspaceFiles Remote namespace", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/attachment/attachment-local/package.json b/packages/attachment/attachment-local/package.json index 5f25e49265..810513ab0e 100644 --- a/packages/attachment/attachment-local/package.json +++ b/packages/attachment/attachment-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-attachment-local", "description": "Private content-addressed DSH_HOME attachment storage", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -28,7 +28,7 @@ "dependencies": { "@deepseek-ai/dsh-lazy-require": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "sharp": "^0.35.3" + "sharp": "^0.35.5" }, "devDependencies": { "@deepseek-ai/dsh-attachment": "workspace:*", diff --git a/packages/attachment/attachment/package.json b/packages/attachment/attachment/package.json index 9876949586..e119eb4700 100644 --- a/packages/attachment/attachment/package.json +++ b/packages/attachment/attachment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-attachment", "description": "Durable immutable attachment storage seam for the DeepSeek Harness", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/boot/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml index f9a2474897..df0e4f782e 100644 --- a/packages/boot/app-boot/README.i18n.yaml +++ b/packages/boot/app-boot/README.i18n.yaml @@ -24,8 +24,8 @@ en: 1099528bde0e2aee zh: b68772bdb9bb7856 /deepseek-ai-dsh-app-boot/use-this-package/profiles: - en: d50b0fdaf62353dd - zh: 0fccb6968602e5de + en: 1db121100677e45f + zh: c6a8c6316020eb1b /deepseek-ai-dsh-app-boot/use-this-package/previewing-the-effective-configuration: en: 646a0ccdd7881ad1 zh: e3696acb1573dd7b @@ -33,11 +33,11 @@ en: c4a301652b851ce9 zh: 784871e19cd9db0a /deepseek-ai-dsh-app-boot/use-this-package/reading-plugin-display-metadata: - en: 98ef2935171952c0 - zh: 8c160bdeffd68b0f + en: ebf6cb3960a5b378 + zh: 41ba129a0f86c168 /deepseek-ai-dsh-app-boot/use-this-package/startup-and-reload-failures: - en: 09890e218ab49c84 - zh: e3d86597e7fb1adb + en: b2e5cfdf8847d214 + zh: 8d65771a3bd8a7c9 /deepseek-ai-dsh-app-boot/use-this-package/telling-the-agent-where-the-harness-lives: en: 07c459f26f4f8517 zh: 210f1e4894731b1a @@ -45,8 +45,8 @@ en: f4a2695cb4c8a570 zh: 89837235d2f376cf /deepseek-ai-dsh-app-boot/understand-the-implementation/design-notes: - en: 4ac0c33806b5a5b2 - zh: cdb56f3adfcb4364 + en: 25a0d1b46fd66d68 + zh: 5cd8b4ac382dccdc /deepseek-ai-dsh-app-boot/understand-the-implementation/helper-behavior: en: f7bc3a8dd4b00bdd zh: 30098428aa769ad0 diff --git a/packages/boot/app-boot/README.md b/packages/boot/app-boot/README.md index eb525411e5..4821673423 100644 --- a/packages/boot/app-boot/README.md +++ b/packages/boot/app-boot/README.md @@ -62,7 +62,21 @@ Your machine-local preferences also live in the Harness home: The enabled `dsh-hmr` plugin watches the profile manifest and both user patch files, re-reads the ordered bundle layers, and applies the [reload failure policy](#startup-and-reload-failures). [DSH HMR](../hmr/README.md) serializes these reloads with [Plugin Manager](../plugin-manager/README.md) configuration writes; package operations run outside its queue. The launcher does not install HMR or watchers; disabled or absent HMR means changes require restart. -Inserted plugin names may be absolute filesystem paths, file URLs, or package specifiers. Patch loading converts absolute paths and patch-relative `./` or `../` paths to file URLs within `insert` rows and their nested groups; existing-entry name assertions and replacement `config` values remain literal. +Inserted plugin names may be absolute filesystem paths, file URLs, or package specifiers. Patch loading converts absolute paths and patch-relative `./` or `../` paths to file URLs within `insert` rows, their nested groups, and inserted preset definitions; existing-entry name assertions and replacement `config` values remain literal. + +A profile patch may set `preset` to an outer row id and apply the remaining ordinary patch fields inside that row's literal `config.plugins` list. Any bundle or user layer can contribute these operations. Each operation sees the current child list; later user operations take precedence, and a later whole-preset `config` replacement replaces every earlier child contribution. Ordinary outer patches retain Include's single-index lookup: children introduced by replacing a group's `config` are not addressable outer targets. Missing or malformed outer preset targets and duplicate child ids introduced by an insertion fail composition. An unmatched inner `id` warns and skips, so disabling a bundle does not invalidate an old user override of its child. + +```yaml +- preset: preset-standard + insert: + - id: optional-string-editor + name: '@deepseek-ai/dsh-tool-str-replace-editor' +- preset: preset-standard + id: optional-string-editor + disabled: true +``` + +`ProfilePatch` extends the native patch fields with `preset`. `applyProfilePatches` composes these operations; `compileProfilePatches` returns ordinary Include patches. Startup, profile reload, effective-config dumps, schema inspection, and compatibility preflight share this compiler. Scoped `insert` module paths resolve beside their declaring patch, including nested groups; `!!js` remains unevaluated until each child activates. Native included-file patch lists keep their ordinary Include syntax. Loaded scoped insertions without IDs receive stable internal IDs derived from the canonical declaring file and entry position, including nested groups; ordinary anonymous root rows keep Loader’s existing behavior. Generated IDs identify contributions across reloads and retained revisions, and are implementation details rather than names to use in authored patches. Compiler failures name the declaring patch file and operation, including after profile layers are combined. Before mounting profile rows, the `dsh` launcher computes one immutable runtime resolution from the installation and ordered bundle dependency graphs. Every profile launcher uses runtime resolution, including plain Node, packaged executables, and the Electron Host. It installs the runtime resolution through Node's ESM and CommonJS resolvers without creating fallback links. @@ -82,12 +96,18 @@ The projector preserves native omission behavior by checking literal defaults ag ### Reading plugin display metadata +`resolvePluginResource(specifier, parentURL)` resolves a plugin module or exported resource to its local filesystem path through the active Node ESM resolver without evaluating it. It throws when the resolver is unavailable or the resource cannot resolve to a local file. + +`realModuleFile(path)` canonicalizes an existing module resource in Node and packaged executables. It follows physical file symlinks and preserves archive-backed resources through their canonical containing directory. Missing or inaccessible resources throw. + Use `readPluginMeta(specifier, parentURL)` or `ctx.pluginPackages.metaOf(specifier, parentURL)` to read installed package display text without importing or activating the plugin. Lookup uses the complete package specifier and the caller's resolution base, respecting Node exports. File paths and file URLs return no metadata without resolving resources. For a package-root specifier, missing locale fields fall back to the accessible `package.json`; a subpath specifier never reads a `package.json`. Malformed metadata returns an `error` diagnostic. Results retain translations for Client-side language selection. The reader also loads an image data URL, even when locale text is complete: a package root uses its manifest `icon`, or `/icon` when that field is omitted; a subpath uses `/icon`. An icon error preserves valid text alongside the diagnostic. See [Plugin display metadata](../../../docs/cookbook/adding-a-package.md#plugin-display-metadata) for the author format. +`ON_DEMAND_BUNDLES` names the public Official packages installed only on request. `OFFICIAL_ON_DEMAND_CATALOG` embeds their package-owned localized metadata and icons for offline discovery; `pnpm gen-official-bundle-catalog` explicitly updates generated content, and `verify-official-bundle-catalog` checks resource completeness and freshness; builds consume the committed catalog without importing provider code. The catalog contains no install target: [Plugin Manager](../plugin-manager/README.md#use-this-package) derives it from the running version and installation and uses the ordinary bundle installer. The [product-use check](../../../scripts/verify-product-use.ts) composes shipped and on-demand selections against Web without executing providers. + ### Startup and reload failures -Profile reconciliation returns diagnostics for unchanged inactive entries without failing an unrelated mutation. A new inactive entry, a changed configuration or fiber, or a changed diagnostic fails reconciliation; removed fibers must still finish disposal. Explicit enablement targets must activate even when their failure predates the operation. Successful reconciliation emits `app-boot/config-reload` after lifecycle settlement and diagnostic checks, including programmatic updates without HMR. The event carries no diff or parsed config. Successful reconciliation returns after lifecycle settlement and diagnostic checks; volatile-only entry changes are committed by Loader during the update. +Profile reconciliation returns diagnostics for unchanged inactive entries without failing an unrelated mutation. A new inactive entry, a changed configuration or fiber, or a changed diagnostic fails reconciliation; removed fibers must still finish disposal. Active root rows that disappear or become literally disabled release their resources before replacement rows activate, so a singleton provider can drain in-flight work before its successor registers. Compilation and disposal checks use the same parsed root entries for each reconciliation. Explicit enablement targets must activate even when their failure predates the operation. Successful reconciliation emits `app-boot/config-reload` after lifecycle settlement and diagnostic checks, including programmatic updates without HMR. The event carries no diff or parsed config. Successful reconciliation returns after lifecycle settlement and diagnostic checks; volatile-only entry changes are committed by Loader during the update. After the Loader settles, app-boot warns when only optional entries are inactive. If an enabled required entry cannot activate, `boot()` rejects with `StartupError` after disposal. An independently owned logger exporter retains warning and error records through asynchronous disposal and is released before `boot()` settles. Its message groups all failed plugins and pending services, marks required entries, and retains original stacks, nested causes, and aggregate members. The CLI prints that message once and saves [full startup diagnostics](../../../apps/cli/reference/README.md#startup-diagnostics) before exiting with code 1; unrelated exceptions retain their normal stack output. In the table, stopping startup means disposing any mounted plugins and exiting nonzero without reporting readiness; continuing keeps successful plugins running. Later configuration HMR does not repeat the required-startup audit and does not roll back the whole update. @@ -135,7 +155,7 @@ This section explains how the outcomes above are realized and points at the code - **Package metadata.** `ctx.pluginPackages.packageOf` locates the owning package without loading code or requiring an exported `package.json`; a subpath selects its package without validating that file. The installed runtime resolution owns selection even for a miss. Low-level embedders that install the service without one retain native lookup. Display metadata uses the separate entry-aware reader described above. - **Two Loader builtins.** `mountRootInclude` registers `cordis:include` and `cordis:group` as Loader builtins: a group row gives one `isolate` realm to a provider and its consumers together, and an agent preset outside this workspace cannot resolve `@deepseek-ai/cordis-plugin-group` by name. Both load through the ambient module pipeline rather than the included tree's own specifier resolution. - **Consumer-owned strictness.** Ordinary Loader groups keep successful siblings. App-boot applies the global required-entry policy after initial settlement; agent presets and dynamic multi-entry compositions own and dispose their separate Loader subtree when they require all-or-nothing setup. App-boot reads failed fibers to report their recorded errors and coalesces duplicate Loader rejection notifications through one process checkpoint. -- **One runtime resolution.** The installation-first and ordered-bundle breadth-first traversal produces the runtime table. Runtime resolution creates no links; runtime resolution entries occupy their package names at `$DSH_HOME/profiles/node_modules`, and every other name sees that directory as an ordinary ancestor. Profile load removes the `.dsh-module-fallback` projections that link-backend releases wrote into a profile; pnpm-installed packages stay. External bare targets selected by package `imports` use the same package order, while Node retains mapping, conditions, and exact target resolution. A complete successor may add package names and update linked-root membership atomically within the existing package-mapping and local-name constraints. +- **One runtime resolution.** The installation-first and ordered-bundle breadth-first traversal produces the runtime table. Runtime resolution creates no links; runtime resolution entries occupy their package names at `$DSH_HOME/profiles/node_modules`, and every other name sees that directory as an ordinary ancestor. Profile load removes the `.dsh-module-fallback` projections that link-backend releases wrote into a profile; pnpm-installed packages stay. External bare targets selected by package `imports` use the same package order, while Node retains mapping, conditions, and exact target resolution. A complete successor may add package names, remove profile-scoped mappings and profile-local names, and update linked-root membership atomically; retained mappings keep their normalized directory, version, and scope. Installation anchors remain fixed; a profile mapping may change its declarer. [Plugin Manager](../plugin-manager/README.md) publishes successors after package operations ([decision](../../../.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.md)). - **Removing linked interception.** A successor may remove a linked root without restart. Directories outside all remaining roots use native lookup for subsequent requests, which may find a development copy or report a missing package. Existing module references and Node caches stay unchanged. Re-adding the same link name and target is allowed; pointing a previously published name at a different target is rejected even after removal ([generation rules](../../../.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md#immutable-generations)). - **Application-owned profiles.** Application-owned profiles use the same runtime resolution. Links within the active profile directory, including pnpm store links, are not external roots even when the profile is outside the shared profiles tree. Resolution does not modify their `node_modules`; pnpm owns installed packages. - **Owned Workers.** Worker build banners import `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap` before bundled business code. Each Worker installs the structured-cloned runtime resolution in its own isolate. The bootstrap bundle has no static package imports. Source Worker entries retain their self-contained dependency closure, and third-party Workers receive no injection. diff --git a/packages/boot/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md index 13f76fb81c..236c59d522 100644 --- a/packages/boot/app-boot/README.zh.md +++ b/packages/boot/app-boot/README.zh.md @@ -62,7 +62,21 @@ profile 导入插件前,DSH 会检查其 `peerDependencies` 中对 `@deepseek- 启用的 `dsh-hmr` 插件会监视 profile manifest 与两份用户 patch 文件,重新读取按顺序排列的组合包层,并应用[重载失败策略](#startup-and-reload-failures)。[DSH HMR](../hmr/README.zh.md) 将这些重载与[插件管理器](../plugin-manager/README.zh.md)的配置写入串行化;包操作在其队列之外执行。启动器不安装 HMR 或监视器;HMR 被禁用或不存在时,更改需要重启。 -插入条目的插件名可以是绝对文件系统路径、文件 URL 或包标识符。patch 加载会把 `insert` 条目及其嵌套分组中的绝对路径以及相对于 patch 文件的 `./` 或 `../` 路径转换为文件 URL;对已有条目名称的断言及替换用的 `config` 值保持原样。 +插入条目的插件名可以是绝对文件系统路径、文件 URL 或包标识符。patch 加载会把 `insert` 条目、其嵌套分组和插入的 preset 定义中的绝对路径,以及相对于 patch 文件的 `./` 或 `../` 路径转换为文件 URL;对已有条目名称的断言及替换用的 `config` 值保持原样。 + +profile patch 可以将 `preset` 设为外层条目 id,把其余普通 patch 字段应用到该条目的字面量 `config.plugins` 列表中。任何 bundle 或用户层都可以提供这些操作。每个操作作用于当前子条目列表;较晚的用户操作优先,而较晚的整个 preset `config` 替换会覆盖此前所有子条目贡献。普通外层 patch 保留 Include 的单次索引查找语义:替换 group 的 `config` 所引入的子条目无法作为外层目标。外层 preset 目标缺失或格式错误、插入造成子条目 id 重复都会使组合失败。内层 `id` 不匹配时只警告并跳过,因此禁用 bundle 不会使用户保留的子条目覆盖失效。 + +```yaml +- preset: preset-standard + insert: + - id: optional-string-editor + name: '@deepseek-ai/dsh-tool-str-replace-editor' +- preset: preset-standard + id: optional-string-editor + disabled: true +``` + +`ProfilePatch` 在原生 patch 字段上增加 `preset`。`applyProfilePatches` 组合这些操作;`compileProfilePatches` 返回普通 Include patch。启动、profile 重载、有效配置 dump、schema 检查及兼容性预检共用此编译器。限定到 preset 的 `insert` 模块路径以声明它的 patch 所在目录为基准,包括嵌套分组;`!!js` 保持不求值,直至对应子条目激活。原生被 Include 引入文件的 patch 列表仍使用普通 Include 语法。 读取的作用域插入行若未声明 ID,会依据声明文件的规范路径及条目位置获得稳定的内部 ID,嵌套组内的行也如此;普通匿名根行保留 Loader 的既有行为。生成的 ID 用于在重载和保留代际之间识别贡献项,是实现细节,不是作者应在 patch 中引用的名称。编译失败会标明声明 patch 文件和操作序号,profile 层合并后也保留这些信息。 挂载 profile 条目前,`dsh` launcher 会从安装依赖图与有序 bundle 依赖图计算一份不可变的 runtime resolution。普通 Node、打包可执行文件与 Electron Host 等所有 profile 启动器都使用 runtime 解析,将 runtime resolution 安装到 Node 的 ESM 与 CommonJS 解析器中,不创建 fallback 链接。 @@ -82,12 +96,18 @@ profile 导入插件前,DSH 会检查其 `peerDependencies` 中对 `@deepseek- ### 读取插件展示元信息 +`resolvePluginResource(specifier, parentURL)` 通过活动的 Node ESM 解析器将插件模块或导出资源解析为本地文件路径,不执行其代码。解析器不可用或资源无法解析为本地文件时会抛出错误。 + +`realModuleFile(path)` 在 Node 和打包可执行程序中规范化已有模块资源的路径。它跟随物理文件符号链接,并通过规范化后的所在目录保留归档资源。资源不存在或无法访问时抛出错误。 + 使用 `readPluginMeta(specifier, parentURL)` 或 `ctx.pluginPackages.metaOf(specifier, parentURL)` 读取已安装包的展示文本,无需导入或激活插件。查询使用完整包标识与调用方的解析基准,并遵循 Node exports。文件路径与文件 URL 不解析资源,直接返回无元信息。包根标识缺失的 locale 字段回退到可访问的 `package.json`;子路径标识从不读取 `package.json`。格式错误的元信息返回 `error` 诊断。结果保留翻译,由 Client 选择语言。即使 locale 文本完整,读取器也会加载图片 data URL:包根使用清单 `icon`,省略该字段时使用 `<包名>/icon`;子路径使用 `<标识>/icon`。图标出错时,保留有效文本并附上诊断。作者格式见[插件展示元信息](../../../docs/cookbook/adding-a-package.zh.md#plugin-display-metadata)。 +`ON_DEMAND_BUNDLES` 列出仅按需安装的官方公开包。`OFFICIAL_ON_DEMAND_CATALOG` 内嵌这些包拥有的本地化元信息和图标,支持离线发现;`pnpm gen-official-bundle-catalog` 显式更新生成内容,`verify-official-bundle-catalog` 检查资源完整性与新鲜度;构建只读取已提交的目录,不导入提供者代码。目录不保存安装目标;[插件管理器](../plugin-manager/README.zh.md#use-this-package)根据当前版本和安装位置确定目标,并使用普通组合包安装器。[产品用途检查](../../../scripts/verify-product-use.ts)将随附和按需选择与 Web 组合,但不执行提供者。 + ### 启动与重载失败 -profile 重载返回未变化的已有故障诊断,不让无关修改因此失败。新增未激活条目、配置或 fiber 变化、诊断变化都会使重载失败;被移除的 fiber 仍须完成释放。显式启用的目标必须成功激活,即使它的故障早于本次操作。成功重载在生命周期结束及诊断检查通过后发出 `app-boot/config-reload`,包括未启用 HMR 时的程序化更新。事件不携带 diff 或解析后的配置。 成功重载在生命周期结束及诊断检查通过后返回;仅 volatile 的条目变化由 Loader 在更新过程中提交。 +profile 重载返回未变化的已有故障诊断,不让无关修改因此失败。新增未激活条目、配置或 fiber 变化、诊断变化都会使重载失败;被移除的 fiber 仍须完成释放。消失或被字面量禁用的活跃根条目先释放资源,再激活替代条目,因此单实例提供方可以先排空进行中的工作,再让后继者注册。每次重组时,编译与释放检查使用同一份已解析的根条目。显式启用的目标必须成功激活,即使它的故障早于本次操作。成功重载在生命周期结束及诊断检查通过后发出 `app-boot/config-reload`,包括未启用 HMR 时的程序化更新。事件不携带 diff 或解析后的配置。 成功重载在生命周期结束及诊断检查通过后返回;仅 volatile 的条目变化由 Loader 在更新过程中提交。 Loader 结算后,app-boot 在仅 optional 条目未激活时输出警告。如果已启用的 required 条目无法激活,`boot()` 会在释放资源后以 `StartupError` 拒绝。独立管理生命周期的 logger exporter 会保留异步资源释放期间的警告和错误记录,并在 `boot()` 结算前释放。其消息分组列出所有失败插件和等待的服务,标记 required 条目,并保留原始堆栈、嵌套原因和聚合错误成员。CLI 仅输出该消息一次,并在保存[完整启动诊断](../../../apps/cli/reference/README.zh.md#startup-diagnostics)后以退出码 1 结束;其他异常保留正常堆栈输出。表中的“终止启动”指释放已挂载插件并以非零码退出,不报告就绪;“继续”指保留成功运行的插件。后续配置 HMR 不会再次执行 required 启动审计,也不会回滚整个更新。 @@ -135,7 +155,7 @@ Loader 结算后,app-boot 在仅 optional 条目未激活时输出警告。如 - **包元数据。** `ctx.pluginPackages.packageOf` 定位所属包,不加载代码,也不要求导出 `package.json`;子路径选择其所属包,不校验该文件。安装 runtime resolution 后,即使查询未命中也以其选包规则为准。仅安装服务而不提供 runtime resolution 的底层嵌入方保留原生查询。展示元数据使用上文另述的入口感知读取器。 - **两个 Loader builtin。** `mountRootInclude` 把 `cordis:include` 与 `cordis:group` 注册为 Loader builtin:group 行能把一个提供方与它的消费方放进同一个 `isolate` realm,而位于本工作区之外的 agent preset 无法按名称解析 `@deepseek-ai/cordis-plugin-group`。两者都通过宿主的模块管线加载,而非被包含树自身的说明符解析。 - **由 consumer 持有严格语义。** 普通 Loader group 保留成功 sibling。App-boot 在首次结算后应用全局 required-entry policy;agent preset 与动态多 entry 组合在需要 all-or-nothing setup 时,持有并拆卸各自的独立 Loader 子树。App-boot 读取 failed fiber 来报告已记录的错误,并在一个进程检查点内合并 Loader 重复的 rejection 通知。 -- **唯一 runtime resolution。** 安装优先、有序 bundle 逐根 breadth-first 遍历生成运行时表。runtime 解析不创建链接;runtime resolution 条目占据 `$DSH_HOME/profiles/node_modules` 上各自的包名位置,其余包名把该目录当作普通祖先。profile 加载时删除 Link 后端发布版写进 profile 的 `.dsh-module-fallback` 投影;pnpm 安装的包保留。package `imports` 选中的外部 bare target 使用相同的选包顺序,映射、conditions 和精确 target 解析仍由 Node 负责。完整后继 runtime resolution 可以在既有包映射和本地包名约束内原子增加 package name、更新 linked root 集合。 +- **唯一 runtime resolution。** 安装优先、有序 bundle 逐根 breadth-first 遍历生成运行时表。runtime 解析不创建链接;runtime resolution 条目占据 `$DSH_HOME/profiles/node_modules` 上各自的包名位置,其余包名把该目录当作普通祖先。profile 加载时删除 Link 后端发布版写进 profile 的 `.dsh-module-fallback` 投影;pnpm 安装的包保留。package `imports` 选中的外部 bare target 使用相同的选包顺序,映射、conditions 和精确 target 解析仍由 Node 负责。完整后继 runtime resolution 可以原子地增加 package name、移除 profile 范围的映射和 profile 本地包名、更新 linked root 集合;保留映射的规范化目录、版本和作用域不变。安装范围的锚点不变;profile 映射可以更换声明来源。[插件管理器](../plugin-manager/README.zh.md)在包操作之后发布后继代([决策](../../../.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md))。 - **移除链接拦截。** 后继 generation 可以移除 linked root,无需重启。目录不再被任何剩余 root 覆盖时,后续请求使用原生查询,可能找到开发副本,也可能报告缺包。已有模块引用和 Node 缓存保持不变。同名、同目标可以重新加入;曾发布的名称改指向不同目标时,即使中间移除过也会被拒绝([generation 规则](../../../.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md#immutable-generations))。 - **应用自有 profile。** 应用自有 profile 使用相同的 runtime resolution。目标位于当前 profile 目录内的链接(包括 pnpm store 链接)不算外部 root,即使 profile 位于共享 profiles 树外。解析过程不修改其 `node_modules`;已安装包由 pnpm 管理。 - **自有 Worker。** Worker 构建 banner 会在业务 bundle 前导入 `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap`。每个 Worker 在自己的 isolate 中安装结构化克隆的 runtime resolution。bootstrap bundle 不静态导入任何包。源码 Worker 入口保留自包含依赖,第三方 Worker 不接受注入。 diff --git a/packages/boot/app-boot/package.json b/packages/boot/app-boot/package.json index 08f105f6fa..af2930239b 100644 --- a/packages/boot/app-boot/package.json +++ b/packages/boot/app-boot/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-app-boot", "description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -36,7 +36,7 @@ "@deepseek-ai/dsh-package-manifest": "workspace:*", "@eslint-community/regexpp": "^4.12.2", "ajv": "^8.20.0", - "js-yaml": "^4.2.0", + "js-yaml": "^4.3.2", "node-addon-require-builtin": "^0.1.6", "resolve.exports": "^2.0.3", "semver": "^7.8.5" diff --git a/packages/boot/app-boot/src/compatibility-preflight.ts b/packages/boot/app-boot/src/compatibility-preflight.ts index 5b4589b117..1e33abe184 100644 --- a/packages/boot/app-boot/src/compatibility-preflight.ts +++ b/packages/boot/app-boot/src/compatibility-preflight.ts @@ -7,6 +7,7 @@ import type { Context } from '@deepseek-ai/cordis' import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader' import { applyEntryPatches, entryListSchema, type PatchOptions } from '@deepseek-ai/cordis-plugin-include' import { load } from 'js-yaml' +import { applyProfilePatches, type ProfilePatch } from './profile-patches.ts' import { resolvePluginResource } from './package-meta.ts' import { barePackageName } from './profile-resolution/resolver.ts' import type {} from './profile-resolution/service.ts' @@ -178,10 +179,10 @@ function preflight( * @returns One prepared insertion patch for profiles, or the original patches for non-profile callers. */ export function prepareProfilePatches( - ctx: Context, patches: PatchOptions[], parentURL: string, binName = 'dsh', + ctx: Context, patches: ProfilePatch[], parentURL: string, binName = 'dsh', ): PatchOptions[] { if (ctx.get('profileContext') === undefined) return patches - const entries = applyEntryPatches([], patches, patchWarning(ctx)) + const entries = applyProfilePatches([], patches, patchWarning(ctx)) const rows = prepareProfileEntries(ctx, entries, parentURL, binName) return rows.length === 0 ? [] : [{ insert: rows }] } diff --git a/packages/boot/app-boot/src/config-schema/document.ts b/packages/boot/app-boot/src/config-schema/document.ts index 70428c6671..768fb3c152 100644 --- a/packages/boot/app-boot/src/config-schema/document.ts +++ b/packages/boot/app-boot/src/config-schema/document.ts @@ -143,13 +143,23 @@ export async function buildConfigSchemaDocument( required: ['id'], properties: { id: { const: id } }, allOf: [ { not: { required: ['insert'] } }, + { not: { required: ['preset'] } }, { anyOf: [{ not: { required: ['name'] } }, { properties: { name: { enum: ['', target.name] } } }] }, ], }, then: { properties: { config: { $ref: configRef } } }, }) } - definitions.patch = { ...patchStructure(), allOf: [ref('entryMetadata'), ...patchRules] } + definitions.patch = { + ...patchStructure(), allOf: [ref('entryMetadata'), ...patchRules], + properties: { + insert: ref('entryList'), + preset: { + type: 'string', pattern: '\\S', + description: 'Outer preset row id. The remaining patch fields apply to its current config.plugins list; a missing or unaddressable preset fails composition.', + }, + }, + } const positions = new Map(entries.map((entry, index) => [entry.path, index])) diagnostics.sort((left, right) => (positions.get(left.path ?? '') ?? -1) - (positions.get(right.path ?? '') ?? -1)) return { diff --git a/packages/boot/app-boot/src/config-schema/index.ts b/packages/boot/app-boot/src/config-schema/index.ts index 44bb553249..e515c961f0 100644 --- a/packages/boot/app-boot/src/config-schema/index.ts +++ b/packages/boot/app-boot/src/config-schema/index.ts @@ -1,6 +1,6 @@ /** Profile schema generation: composition diagnostics, runtime resolution, and boot-free discovery. */ -import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include' +import type { ProfilePatch } from '../profile-patches.ts' import { composeEntries, createRuntimeResolution, type Profile } from '../profile.ts' import { collectConfigSchemas } from './collect.ts' import type { ConfigSchemaDiagnostic, ConfigSchemaDump } from './types.ts' @@ -20,7 +20,7 @@ export type { ConfigSchemaDump, NativeConfigSchema } from './types.ts' */ export async function generateConfigSchema( profile: Profile, - layers: readonly PatchOptions[][], + layers: readonly ProfilePatch[][], installAnchor: string, ): Promise { const diagnostics: ConfigSchemaDiagnostic[] = [] diff --git a/packages/boot/app-boot/src/index.ts b/packages/boot/app-boot/src/index.ts index 188cb33460..38abd1f256 100644 --- a/packages/boot/app-boot/src/index.ts +++ b/packages/boot/app-boot/src/index.ts @@ -13,7 +13,7 @@ import { basename, dirname, isAbsolute, resolve } from 'node:path' import * as yaml from 'js-yaml' import { Context, type FiberState } from '@deepseek-ai/cordis' import Loader, { type Entry, type EntryOptions } from '@deepseek-ai/cordis-plugin-loader' -import Include, { applyEntryPatches, entryListSchema, type PatchOptions } from '@deepseek-ai/cordis-plugin-include' +import Include, { entryListSchema } from '@deepseek-ai/cordis-plugin-include' import Group from '@deepseek-ai/cordis-plugin-group' import { dshHomePath, resolveDshHome } from '@deepseek-ai/dsh-home-paths' import { createLaunchEnvironmentSnapshot, type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment' @@ -25,8 +25,12 @@ export { setProfileVersionExemption, type ProfileCompatibility, } from './profile-compatibility.ts' import { prepareProfilePatches } from './compatibility-preflight.ts' +import { applyProfilePatches, compileProfilePatches, profilePatchPreset, prepareLoadedProfilePatches, type ProfilePatch } from './profile-patches.ts' +export { applyProfilePatches, compileProfilePatches, profilePatchPreset, type ProfilePatch, type ProfilePatchWarning } from './profile-patches.ts' export { prepareProfileEntries, prepareProfilePatches } from './compatibility-preflight.ts' -export { readPluginMeta } from './package-meta.ts' +export { readPluginMeta, resolvePluginResource } from './package-meta.ts' +export { ON_DEMAND_BUNDLES, OFFICIAL_ON_DEMAND_CATALOG, type OfficialBundleCatalogEntry } from './official-bundles.ts' +export { realModuleFile } from './profile-resolution/legacy-links.ts' export { generateConfigSchema, type ConfigSchemaDump, type NativeConfigSchema } from './config-schema/index.ts' export { createConfigProjector, LOADER_EXPRESSION_SCHEMA, type ConfigProjection } from './config-schema/projector.ts' export { isNativeConfigSchema } from './config-schema/native.ts' @@ -56,6 +60,7 @@ declare module '@deepseek-ai/cordis' { export { composeEntries, createRuntimeResolution, + ProfileRuntimeResolution, DEFAULT_PROFILE_BUNDLES, OPTIONAL_BUNDLES, bundlePatchFiles, @@ -271,7 +276,7 @@ const userPatchesSchema = entryListSchema * @returns Diagnostics for unchanged pre-existing inactive entries; new or changed failures reject. */ export async function reconcileProfilePatches( - ctx: Context, patches: PatchOptions[], binName: string, requiredIds: readonly string[] = [], + ctx: Context, patches: ProfilePatch[], binName: string, requiredIds: readonly string[] = [], ): Promise { const entry = bootstrapIncludes.get(ctx) if (entry === undefined) throw new Error(`${binName}: profile reload requires the root Include entry`) @@ -285,7 +290,9 @@ export async function reconcileProfilePatches( const { patches: _previous, ...includeConfig } = entry.options.config as Include.Config // The recomposition judges the rows the launch judged, resolved from the file this Include read. const parentURL = new URL('.', new URL(includeConfig.path, entry.parent.tree.ctx.baseUrl)).href - const prepared = prepareProfilePatches(ctx, patches, parentURL, binName) + const initial = readRootEntries(includeConfig.path, parentURL, binName) + const prepared = prepareRootPatches(ctx, patches, includeConfig.path, parentURL, binName, initial) + await drainRetiringEntries(ctx, entry, initial, prepared) await entry.update({ config: { ...includeConfig, patches: prepared } }) const results = await Promise.allSettled(previousFibers.map(({ fiber }) => fiber.await())) await ctx.loader.await() @@ -301,10 +308,35 @@ export async function reconcileProfilePatches( return failures.map(inactiveDiagnostic) } +/** Disappearing and literal-disabled rows release singleton services before their replacements activate. */ +async function drainRetiringEntries( + ctx: Context, root: Entry, initial: EntryOptions[], patches: readonly ProfilePatch[], +): Promise { + const rows = applyProfilePatches(initial, patches) + const next = new Map() + const visit = (rows: EntryOptions[], outerDisabled = false): void => { + for (const row of rows) { + const disabled = outerDisabled || row.disabled === true + next.set(row.id, !row.group && disabled) + if (row.group && Array.isArray(row.config)) visit(row.config as EntryOptions[], disabled) + } + } + visit(rows) + const retiring = [...ctx.loader.entries()].flatMap((entry) => { + const fiber = entry.fiber + return entry.parent.tree === root.subtree && fiber?.state === FIBER_ACTIVE + && (!next.has(entry.options.id) || next.get(entry.options.id)) ? [fiber] : [] + }) + await Promise.all(retiring.map(async (fiber) => { + await fiber.dispose() + await fiber.await() + })) +} + /** - * Load an optional patch-list file: a top-level YAML array of loader patch - * entries (`@deepseek-ai/cordis-plugin-include`'s `PatchOptions`): id-targeted config - * overrides and `insert` lists, with `!!js` expressions allowed. A missing + * Load an optional patch-list file: a top-level YAML array of profile patch + * entries: id-targeted overrides, `insert` lists, and preset-scoped operations, + * with `!!js` expressions allowed. A missing * file means "no layer"; an unreadable, unparsable, or non-array file throws — * a present patch file that cannot apply is a misconfiguration and must fail * loud at boot, never be silently skipped. @@ -312,7 +344,7 @@ export async function reconcileProfilePatches( * @param file - absolute path of the patch file. * @returns the parsed patches, or `undefined` when the file does not exist. */ -export function loadOptionalPatches(binName: string, file: string): PatchOptions[] | undefined { +export function loadOptionalPatches(binName: string, file: string): ProfilePatch[] | undefined { let content: string try { content = readFileSync(file, 'utf8') @@ -332,7 +364,7 @@ export function loadOptionalPatches(binName: string, file: string): PatchOptions * @param file - absolute path of the overlay file. * @returns the parsed patch list. */ -export function loadOverlayPatches(binName: string, file: string): PatchOptions[] { +export function loadOverlayPatches(binName: string, file: string): ProfilePatch[] { let content: string try { content = readFileSync(file, 'utf8') @@ -343,24 +375,30 @@ export function loadOverlayPatches(binName: string, file: string): PatchOptions[ } /** Convert inserted filesystem paths to file URLs, anchoring relative paths beside the patch; keep assertion names literal. */ -function anchorInsertedPluginNames(patches: PatchOptions[], file: string): PatchOptions[] { +function anchorInsertedPluginNames(patches: ProfilePatch[], file: string): ProfilePatch[] { const base = dirname(resolve(file)) const visit = (entry: EntryOptions): void => { if (typeof entry.name === 'string' && (isAbsolute(entry.name) || entry.name.startsWith('./') || entry.name.startsWith('../'))) { entry.name = pathToFileURL(resolve(base, entry.name)).href } if (entry.group && Array.isArray(entry.config)) entry.config.forEach(visit) + const config = entry.config as { plugins?: unknown } | undefined + if (entry.name === '@deepseek-ai/dsh-agent-preset' && Array.isArray(config?.plugins)) { + (config.plugins as EntryOptions[]).forEach(visit) + } + } + for (const patch of patches) { + patch.insert?.forEach(visit) } - for (const patch of patches) patch.insert?.forEach(visit) return patches } /** * Parse one loader patch list: a top-level YAML array of - * `@deepseek-ai/cordis-plugin-include` `PatchOptions` (id-targeted config overrides and - * `insert` lists, `!!js` expressions allowed). Every invalid field or value throws, - * because a patch file that cannot be applied at all is a misconfiguration; a - * single patch whose target row is absent stays a per-entry Loader warning, so - * one overlay shared across surfaces does not have to match every tree. + * profile patches (id-targeted overrides, `insert` lists, and preset-scoped + * operations, with `!!js` expressions allowed). Invalid lists and malformed + * preset targets throw because they cannot be applied. An ordinary or scoped + * child patch whose target is absent stays a per-entry Loader warning; a + * missing outer preset target fails composition. * @param binName - the diagnostic prefix on the thrown error. * @param file - the source path, quoted in errors. * @param content - the file's text. @@ -369,7 +407,7 @@ function anchorInsertedPluginNames(patches: PatchOptions[], file: string): Patch */ function parsePatchList( binName: string, file: string, content: string, label: string, -): PatchOptions[] { +): ProfilePatch[] { let parsed: unknown try { parsed = yaml.load(content, { schema: userPatchesSchema }) @@ -384,7 +422,7 @@ function parsePatchList( throw new Error(`${binName}: ${label} entry ${index + 1} in ${file} must be a mapping (a loader patch entry)`) } }) - return anchorInsertedPluginNames(parsed as PatchOptions[], file) + return anchorInsertedPluginNames(prepareLoadedProfilePatches(parsed as ProfilePatch[], file, binName), file) } /** One overlay patch list with the source label printed in dump comments. */ @@ -392,13 +430,13 @@ export interface ConfigDumpLayer { /** Source name shown in dump comments (a file basename or path). */ label: string /** The layer's patches, from {@link loadOverlayPatches} / {@link loadOptionalPatches}. */ - patches: PatchOptions[] + patches: ProfilePatch[] } /** * Compose the configured entry list: parse the base config file with the - * include's entry-list dialect, apply every layer's patches as ONE flattened - * list through the include's own patch algorithm (`applyEntryPatches`) — the + * include's entry-list dialect, compile preset operations, then apply the + * flattened native patch list through Include's `applyEntryPatches` — the * same call `boot()` makes, so even patch-visibility corner cases (a later * layer targeting a group child a plain config replacement introduced, which * the single-pass id index never sees) compose identically — then render the @@ -418,8 +456,8 @@ export interface ConfigDumpLayer { * A patch that matches no row is reported through `warn` with its layer * label, mirroring the Loader's boot-time warning. Earlier layers' patches * see an identical preceding state in every snapshot that includes them, so - * each snapshot's warning list extends the previous one and the new tail - * belongs to the added layer. + * additional warnings belong to the added layer. Warning multiplicity is + * preserved when scoped and ordinary diagnostics have different ordering. * @param binName - the diagnostic prefix on read/parse errors. * @param absoluteConfigPath - the base config file `boot()` would include. * @param layers - overlay layers in application order (later wins). @@ -452,16 +490,14 @@ export function renderConfigDump( // YAML parsing yields untyped rows; the include validates each entry // at mount, and the dump prints whatever the file holds, so `EntryOptions` // here is structural trust in the same file `boot()` would include. - const base = parsed as Parameters[0] + const base = parsed as EntryOptions[] // snapshot_k = ONE application of layers 1..k flattened, using the exact // arguments boot passes for that prefix. snapshot_N is the mounted composition. - // The patches are cloned per call: applyEntryPatches detaches the entry - // list but pushes `insert` rows by reference from the patch list, so - // sharing patch objects across snapshot calls would leak a later - // snapshot's mutations into an earlier one's result. - const snapshot = (count: number, warnings: string[]): ReturnType => { - const flattened = structuredClone(layers.slice(0, count).flatMap(layer => layer.patches)) - return applyEntryPatches(base, flattened, (message: string, ...args: unknown[]) => { + // The compiler clones patch objects for each snapshot: native insertions retain + // references, so sharing them would let later snapshots mutate earlier results. + const snapshot = (count: number, warnings: string[]): EntryOptions[] => { + const flattened = layers.slice(0, count).flatMap(layer => layer.patches) + return applyProfilePatches(base, flattened, (message: string, ...args: unknown[]) => { // The include logs through cordis's printf-style logger (`%C` = code); a // dump has no logger, so substitute inline for a plain line. let index = 0 @@ -478,8 +514,11 @@ export function renderConfigDump( if (layer === undefined) continue const warnings: string[] = [] composed = snapshot(count, warnings) - for (const line of warnings.slice(previousWarnings.length)) { - warn(`${binName}: [${layer.label}] ${line}`) + const remaining = [...previousWarnings] + for (const line of warnings) { + const previousIndex = remaining.indexOf(line) + if (previousIndex >= 0) remaining.splice(previousIndex, 1) + else warn(`${binName}: [${layer.label}] ${line}`) } const before = previous.map(entry => JSON.stringify(entry)) for (let index = 0; index < composed.length; index += 1) { @@ -523,6 +562,22 @@ function groupedDump( return lines.join('\n') + '\n' } +/** Compile profile operations before passing an ordinary patch list to the root Include. */ +function readRootEntries(configPath: string, parentURL: string, binName: string): EntryOptions[] { + const parsed: unknown = yaml.load(readFileSync(new URL(configPath, parentURL), 'utf8'), { schema: entryListSchema }) + if (!Array.isArray(parsed)) throw new Error(`${binName}: config ${configPath} must be a top-level array of entries`) + return parsed as EntryOptions[] +} + +function prepareRootPatches( + ctx: Context, patches: ProfilePatch[], configPath: string, parentURL: string, binName: string, initial?: EntryOptions[], +): ProfilePatch[] { + if (ctx.get('profileContext') !== undefined) return prepareProfilePatches(ctx, patches, parentURL, binName) + if (!patches.some(patch => profilePatchPreset(patch) !== undefined)) return patches + return compileProfilePatches(initial ?? readRootEntries(configPath, parentURL, binName), patches, + (message, ...args) => { ctx.logger.warn(message, ...args) }) +} + /** * Mount and remember the exact root Include entry used by app boot and user patch-layer HMR. * @param ctx - context carrying an initialized Loader service. @@ -538,7 +593,7 @@ function groupedDump( export async function mountRootInclude( ctx: Context, absoluteConfigPath: string, - patches: readonly PatchOptions[] = [], + patches: readonly ProfilePatch[] = [], bareModuleBaseUrl?: string, binName = 'dsh', ): Promise { @@ -566,7 +621,7 @@ export async function mountRootInclude( // diagnostics unstable across runs (and snapshot fixtures). // The launcher's own copy is prepared here: compatibility decisions must be made before the root // Include imports anything, and they change no profile patch layer, manifest, or bundle list. - const prepared = prepareProfilePatches(ctx, [...patches], pathToFileURL(dirname(absoluteConfigPath)).href + '/', binName) + const prepared = prepareRootPatches(ctx, [...patches], pathToFileURL(absoluteConfigPath).href, pathToFileURL(dirname(absoluteConfigPath)).href + '/', binName) const includeConfig: Include.Config = { path: pathToFileURL(absoluteConfigPath).href, ...prepared.length > 0 ? { patches: prepared } : {}, @@ -972,7 +1027,7 @@ export async function auditStartupEntries( export async function boot( binName: string, absoluteConfigPath: string, - patches?: PatchOptions[], + patches?: ProfilePatch[], prepare?: (ctx: Context) => Promise | void, bareModuleBaseUrl?: string, ): Promise { diff --git a/packages/boot/app-boot/src/official-bundle-packages.ts b/packages/boot/app-boot/src/official-bundle-packages.ts new file mode 100644 index 0000000000..b77f9fcdd9 --- /dev/null +++ b/packages/boot/app-boot/src/official-bundle-packages.ts @@ -0,0 +1,7 @@ +/** Package admission shared by offline catalog generation and release ordering. */ + +/** Public DSH release members available through the ordinary on-demand bundle installer. */ +export const ON_DEMAND_BUNDLES: readonly string[] = [ + '@deepseek-ai/dsh-subagent-claude-code', + '@deepseek-ai/dsh-subagent-codex', +] diff --git a/packages/boot/app-boot/src/official-bundles.generated.ts b/packages/boot/app-boot/src/official-bundles.generated.ts new file mode 100644 index 0000000000..1fffe43c5e --- /dev/null +++ b/packages/boot/app-boot/src/official-bundles.generated.ts @@ -0,0 +1,23 @@ +/** Generated offline Official bundle metadata. Run pnpm gen-official-bundle-catalog; do not edit. */ + +import type { OfficialBundleCatalogEntry } from './official-bundles.ts' + +/** Official on-demand entries, independent of installed packages and registry availability. */ +export const OFFICIAL_ON_DEMAND_CATALOG: readonly OfficialBundleCatalogEntry[] = [ + { + packageName: '@deepseek-ai/dsh-subagent-claude-code', + meta: { + title: { 'en': 'Claude Code subagent', 'zh': 'Claude Code \u5B50\u667A\u80FD\u4F53' }, + description: { 'en': 'Delegate one task to Claude Code using its native authentication and settings.', 'zh': '\u4F7F\u7528 Claude Code \u539F\u751F\u8BA4\u8BC1\u548C\u8BBE\u7F6E\u59D4\u6D3E\u5355\u4E2A\u4EFB\u52A1\u3002' }, + icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIxNC45ODkzIiB5PSIxNS4xODc3IiB3aWR0aD0iMTIuMjM2NSIgaGVpZ2h0PSIxMi4yMzY1IiByeD0iMiIgZmlsbD0idXJsKCNkc2hfcGx1Z2luX2FydF9SMGEpIiBmaWxsLW9wYWNpdHk9IjAuOCI+PC9yZWN0PjxyZWN0IHg9IjguODcxMDkiIHk9IjkuMDY5NyIgd2lkdGg9IjEyLjIzNjUiIGhlaWdodD0iMTIuMjM2NSIgcng9IjIiIGZpbGw9InVybCgjZHNoX3BsdWdpbl9hcnRfUjBiKSIgZmlsbC1vcGFjaXR5PSIwLjgiPjwvcmVjdD48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYSIgeDE9IjIxLjEwNzUiIHkxPSIxNS4xODc3IiB4Mj0iMjEuMTA3NSIgeTI9IjI3LjQyNDMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjNDVFN0E0Ij48L3N0b3A+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDU5MDlEIj48L3N0b3A+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYiIgeDE9IjE0Ljk4OTQiIHkxPSI5LjA2OTciIHgyPSIxNC45ODk0IiB5Mj0iMjEuMzA2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiM2OUI5RkYiPjwvc3RvcD48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMjRERTIiPjwvc3RvcD48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4K', + }, + }, + { + packageName: '@deepseek-ai/dsh-subagent-codex', + meta: { + title: { 'en': 'Codex subagent', 'zh': 'Codex \u5B50\u667A\u80FD\u4F53' }, + description: { 'en': 'Delegate one task to Codex using its native authentication and settings.', 'zh': '\u4F7F\u7528 Codex \u539F\u751F\u8BA4\u8BC1\u548C\u8BBE\u7F6E\u59D4\u6D3E\u5355\u4E2A\u4EFB\u52A1\u3002' }, + icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIxNC45ODkzIiB5PSIxNS4xODc3IiB3aWR0aD0iMTIuMjM2NSIgaGVpZ2h0PSIxMi4yMzY1IiByeD0iMiIgZmlsbD0idXJsKCNkc2hfcGx1Z2luX2FydF9SMGEpIiBmaWxsLW9wYWNpdHk9IjAuOCI+PC9yZWN0PjxyZWN0IHg9IjguODcxMDkiIHk9IjkuMDY5NyIgd2lkdGg9IjEyLjIzNjUiIGhlaWdodD0iMTIuMjM2NSIgcng9IjIiIGZpbGw9InVybCgjZHNoX3BsdWdpbl9hcnRfUjBiKSIgZmlsbC1vcGFjaXR5PSIwLjgiPjwvcmVjdD48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYSIgeDE9IjIxLjEwNzUiIHkxPSIxNS4xODc3IiB4Mj0iMjEuMTA3NSIgeTI9IjI3LjQyNDMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjNDVFN0E0Ij48L3N0b3A+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDU5MDlEIj48L3N0b3A+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYiIgeDE9IjE0Ljk4OTQiIHkxPSI5LjA2OTciIHgyPSIxNC45ODk0IiB5Mj0iMjEuMzA2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiM2OUI5RkYiPjwvc3RvcD48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMjRERTIiPjwvc3RvcD48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4K', + }, + }, +] diff --git a/packages/boot/app-boot/src/official-bundles.ts b/packages/boot/app-boot/src/official-bundles.ts new file mode 100644 index 0000000000..fdcb8fda52 --- /dev/null +++ b/packages/boot/app-boot/src/official-bundles.ts @@ -0,0 +1,15 @@ +/** Official bundles offered without installing their provider runtimes in the DSH distribution. */ + +import type { PluginLocalizedMeta } from '@deepseek-ai/dsh-package-manifest' + +export { ON_DEMAND_BUNDLES } from './official-bundle-packages.ts' + +/** Offline discovery metadata; installation state and the requested version belong to the running host. */ +export interface OfficialBundleCatalogEntry { + /** Exact npm package name accepted by the existing bundle installer. */ + readonly packageName: string + /** Package-owned localized text and a self-contained icon, embedded without provider code. */ + readonly meta: Required> +} + +export { OFFICIAL_ON_DEMAND_CATALOG } from './official-bundles.generated.ts' diff --git a/packages/boot/app-boot/src/package-meta.ts b/packages/boot/app-boot/src/package-meta.ts index ede6023924..b54a930724 100644 --- a/packages/boot/app-boot/src/package-meta.ts +++ b/packages/boot/app-boot/src/package-meta.ts @@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url' import { ModuleLoader } from '@deepseek-ai/cordis-plugin-loader' import type { LocalizedText, PluginLocalizedMeta } from '@deepseek-ai/dsh-package-manifest' import { barePackageName } from './profile-resolution/resolver.ts' +import { realModuleFile } from './profile-resolution/legacy-links.ts' const LANGUAGE_ID = /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/u @@ -32,7 +33,7 @@ function iconOf( label = manifestPath const root = realpathSync(dirname(manifestPath)) target = resolve(root, icon) - const local = relative(root, realpathSync(target)) + const local = relative(root, realModuleFile(target)) if (local === '..' || local.startsWith(`..${sep}`) || isAbsolute(local)) { throw new Error(`${label}: icon must remain inside its manifest directory`) } @@ -40,11 +41,11 @@ function iconOf( const exported = optionalResourcePath(`${specifier}/icon`, parentURL) if (exported === undefined) return undefined target = label = exported - assertPackageOwned(realpathSync(target), packageName) + assertPackageOwned(realModuleFile(target), packageName) } const mediaType = ICON_MEDIA_TYPES.get(extname(target).toLowerCase()) if (mediaType === undefined) throw new Error(`${label}: icon must be SVG, PNG, JPEG, or WebP`) - const file = realpathSync(target) + const file = realModuleFile(target) const stat = statSync(file) if (!stat.isFile()) throw new Error(`${label}: icon must be a regular file`) if (stat.size > MAX_ICON_BYTES) throw new Error(`${label}: icon exceeds 256 KiB`) @@ -64,7 +65,7 @@ function assertPackageOwned(file: string, name: string): void { /** * Resolve a plugin resource through the active Node ESM resolver without evaluating it. - * @param specifier - complete resource module specifier, including its locale filename. + * @param specifier - plugin module or exported resource specifier. * @param parentURL - owning module-resolution base. * @returns the local filesystem path selected by Node and the active profile. * @throws when the resolver is unavailable or the resource cannot resolve to a local file. diff --git a/packages/boot/app-boot/src/profile-context.ts b/packages/boot/app-boot/src/profile-context.ts index 328b543e00..4f299ae699 100644 --- a/packages/boot/app-boot/src/profile-context.ts +++ b/packages/boot/app-boot/src/profile-context.ts @@ -2,7 +2,7 @@ import { join } from 'node:path' import { composeEntries, loadProfileDirectory, PROFILE_PATCH_FILENAME, type Profile } from './profile.ts' import { loadOptionalPatches } from './index.ts' -import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include' +import { cloneProfilePatches, type ProfilePatch } from './profile-patches.ts' /** Application-owned package manager executable; environment applies only to package operations. */ export interface ProfilePnpmInvocation { @@ -24,7 +24,7 @@ export interface ProfileContext { /** Bundle packages used to start this process, before any persisted edits. */ readonly startedBundles: readonly string[] /** Parsed command-line overlays, applied above profile and home patches. */ - readonly overlays: readonly PatchOptions[] + readonly overlays: readonly ProfilePatch[] /** Launch-time DSH_TELEMETRY_DISABLED value; any non-empty value opts out. */ readonly telemetryDisabledEnv: string | undefined } @@ -49,7 +49,7 @@ const TELEMETRY_ROW_ID = 'session-telemetry-otel' * @param hasRow - whether the composition carries the telemetry row. * @returns the disable patch, or `undefined` when no hard-disable patch is required. */ -export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: boolean): PatchOptions | undefined { +export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: boolean): ProfilePatch | undefined { if ((disabledEnv ?? '') === '' || !hasRow) return undefined return { id: TELEMETRY_ROW_ID, disabled: true } } @@ -60,9 +60,9 @@ export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: b * @param initialProfile Already loaded startup profile; omitted reads the current files. * @returns Detached ordered patches; this function does not update the Loader. */ -export function readProfilePatches(binName: string, context: ProfileContext, initialProfile?: Profile): PatchOptions[] { +export function readProfilePatches(binName: string, context: ProfileContext, initialProfile?: Profile): ProfilePatch[] { const profile = initialProfile ?? loadProfileDirectory(binName, context.dir, context.installAnchor, { userLayer: false }) - const patches = structuredClone([ + const patches = cloneProfilePatches([ ...profile.layers.flatMap(layer => layer.patches), ...(initialProfile?.patches ?? loadOptionalPatches(binName, context.patchPath) ?? []), ...(loadOptionalPatches(binName, join(context.home, PROFILE_PATCH_FILENAME)) ?? []), diff --git a/packages/boot/app-boot/src/profile-patches.ts b/packages/boot/app-boot/src/profile-patches.ts new file mode 100644 index 0000000000..d7f34ab539 --- /dev/null +++ b/packages/boot/app-boot/src/profile-patches.ts @@ -0,0 +1,162 @@ +/** Compile preset-scoped profile operations to the native Include patch language. */ + +import { createHash } from 'node:crypto' +import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include' +import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader' +import { realModuleFile } from './profile-resolution/legacy-links.ts' + +/** A native entry patch, optionally applied inside one preset row's `config.plugins`. */ +export interface ProfilePatch extends PatchOptions { + /** Literal id of an addressable outer preset row; `id` then addresses one of its child rows. */ + preset?: string +} + +interface PatchSource { readonly binName: string; readonly file: string; readonly operation: number } +const patchSources = new WeakMap() + +function sourcedError(patch: ProfilePatch, error: unknown): unknown { + const source = patchSources.get(patch) + return source === undefined ? error : new Error( + `${source.binName}: ${error instanceof Error ? error.message : String(error)} in ${source.file} (patch ${source.operation + 1})`, + { cause: error }, + ) +} + +/** Clone patch data while preserving the file attribution held outside its serialized fields. + * @param patches Complete ordered patches, possibly loaded from files. + * @returns Detached patches with the same diagnostic sources and shared-reference relationships. + */ +export function cloneProfilePatches(patches: readonly ProfilePatch[]): ProfilePatch[] { + const copied = structuredClone([...patches]) + const sources = patches.map(patch => patchSources.get(patch)) + copied.forEach((patch, index) => { + const source = sources[index] + if (source !== undefined) patchSources.set(patch, source) + }) + return copied +} + +/** Attach source diagnostics and stable internal identities to parsed scoped insertions. + * @param patches Parsed file patches, modified before they reach any composition consumer. + * @param file Declaring patch file; its canonical path distinguishes anonymous contributions. + * @param binName Diagnostic prefix. + * @returns The same patches, with anonymous scoped rows identified by file and position. + * @throws When a preset target is malformed, naming its declaring file and operation. + */ +export function prepareLoadedProfilePatches(patches: ProfilePatch[], file: string, binName: string): ProfilePatch[] { + const canonical = realModuleFile(file) + patches.forEach((patch, operation) => { + patchSources.set(patch, { binName, file, operation }) + try { + if (profilePatchPreset(patch) === undefined || patch.insert === undefined) return + const identify = (rows: EntryOptions[], parent: readonly number[]): void => { + rows.forEach((row, index) => { + const position = [...parent, index] + if (!row.id) row.id = `__dsh_profile_${createHash('sha256').update(JSON.stringify([canonical, operation, position])).digest('hex').slice(0, 32)}` + if (row.group && Array.isArray(row.config)) identify(row.config as EntryOptions[], position) + }) + } + identify(patch.insert, []) + } catch (error) { + throw sourcedError(patch, error) + } + }) + return patches +} + +/** Native Include diagnostic arguments. + * @param message Diagnostic format; `%C` marks a value rendered as code. + * @param args Values substituted into the diagnostic format. + */ +export type ProfilePatchWarning = (message: string, ...args: unknown[]) => void + +function visitRows(rows: readonly EntryOptions[], visit: (row: EntryOptions) => void): void { + for (const row of rows) { + visit(row) + if (row.group && Array.isArray(row.config)) visitRows(row.config as EntryOptions[], visit) + } +} + +/** Validate the profile-only target while keeping native patch fields under Include's existing rules. + * @param patch Parsed or programmatically supplied profile patch. + * @returns The literal outer row id, or undefined for an ordinary patch. + * @throws When a declared preset target is not a nonempty literal string. + */ +export function profilePatchPreset(patch: ProfilePatch): string | undefined { + if (!Object.hasOwn(patch, 'preset')) return undefined + if (typeof patch.preset !== 'string' || patch.preset.trim() === '') { + throw new Error('profile patch preset must be a nonempty literal row id') + } + return patch.preset +} + +function targetOf(initial: EntryOptions[], patches: PatchOptions[], preset: string): EntryOptions { + // The native index retains replaced-away children and never indexes replacement-created children. + // A marker override locates its actual reachable target without rebuilding that index from the result. + const marker = {} + const rows = applyEntryPatches(initial, [...structuredClone(patches), { id: preset, inject: marker }], () => {}) + let target: EntryOptions | undefined + visitRows(rows, (row) => { if (row.inject === marker) target = row }) + if (target === undefined) throw new Error(`profile patch preset ${JSON.stringify(preset)} is missing or not addressable`) + const config: unknown = target.config + if (config === null || typeof config !== 'object' || Array.isArray(config) || !Array.isArray(Reflect.get(config, 'plugins'))) { + throw new Error(`profile patch preset ${JSON.stringify(preset)} must have a literal config.plugins entry list`) + } + return target +} + +function assertInsertedIds(preset: string, rows: readonly EntryOptions[]): void { + const ids = new Set() + visitRows(rows, (row) => { + if (!row.id) return + if (ids.has(row.id)) throw new Error(`profile patch preset ${JSON.stringify(preset)} has duplicate entry id ${JSON.stringify(row.id)}`) + ids.add(row.id) + }) +} + +/** Lower preset operations to native whole-config replacements without changing native root patch indexing. + * Each scoped operation sees the then-current preset children. Inputs remain unchanged. + * @param initial Root entries before profile patches. + * @param patches Complete ordered profile patch list. + * @param warn Diagnostic sink for scoped child patches; ordinary patches report when Include applies the result. + * @returns Detached native patches, suitable for the root Include. + * @throws For malformed or unaddressable preset targets and duplicate inserted child ids. + */ +export function compileProfilePatches( + initial: EntryOptions[], patches: readonly ProfilePatch[], warn: ProfilePatchWarning = () => {}, +): PatchOptions[] { + const lowered: PatchOptions[] = [] + for (const patch of cloneProfilePatches(patches)) { + try { + const preset = profilePatchPreset(patch) + if (preset === undefined) { + lowered.push(patch) + continue + } + const target = targetOf(initial, lowered, preset) + const config = target.config as Record & { plugins: EntryOptions[] } + const { preset: _preset, ...operation } = patch + const plugins = applyEntryPatches(config.plugins, [operation], (message: string, ...args: unknown[]) => { + warn(`preset ${JSON.stringify(preset)}: ${message}`, ...args) + }) + if (operation.insert !== undefined) assertInsertedIds(preset, plugins) + lowered.push({ id: preset, config: { ...config, plugins } }) + } catch (error) { + throw sourcedError(patch, error) + } + } + return lowered +} + +/** Compose root entries and profile patches with the same compiler used for startup and reload. + * @param initial Root entries before profile patches. + * @param patches Complete ordered profile patch list. + * @param warn Diagnostic sink for unmatched ordinary and scoped child patches. + * @returns The effective entry list, with expressions unevaluated. + * @throws For malformed or unaddressable preset targets and duplicate inserted child ids. + */ +export function applyProfilePatches( + initial: EntryOptions[], patches: readonly ProfilePatch[], warn: ProfilePatchWarning = () => {}, +): EntryOptions[] { + return applyEntryPatches(initial, compileProfilePatches(initial, patches, warn), warn) +} diff --git a/packages/boot/app-boot/src/profile-resolution/legacy-links.ts b/packages/boot/app-boot/src/profile-resolution/legacy-links.ts index 6c67762b77..7d9c75f37a 100644 --- a/packages/boot/app-boot/src/profile-resolution/legacy-links.ts +++ b/packages/boot/app-boot/src/profile-resolution/legacy-links.ts @@ -1,6 +1,7 @@ -/** Package directory canonicalization through the active runtime carrier's filesystem. */ +/** Package directory and resource canonicalization through the active runtime carrier's filesystem. */ -import { realpathSync } from 'node:fs' +import { lstatSync, realpathSync } from 'node:fs' +import { basename, dirname, join } from 'node:path' /** * Return whether the process reads application modules from pkg's virtual filesystem. @@ -18,3 +19,15 @@ function isPackagedExecutable(): boolean { export function realModuleDirectory(path: string): string { return isPackagedExecutable() ? realpathSync(path) : realpathSync.native(path) } + +/** Canonicalize a module resource, preserving physical file aliases. + * SEA archive files are symlink-free and support stat and reads, while their containing directories support realpath. + * @param path Existing module resource path. + * @returns Canonical resource path under the active carrier's filesystem. + * @throws When the resource or its directory cannot be inspected or resolved. + */ +export function realModuleFile(path: string): string { + return !isPackagedExecutable() || lstatSync(path).isSymbolicLink() + ? realpathSync(path) + : join(realModuleDirectory(dirname(path)), basename(path)) +} diff --git a/packages/boot/app-boot/src/profile-resolution/resolver.ts b/packages/boot/app-boot/src/profile-resolution/resolver.ts index 27473ba14f..85c530665d 100644 --- a/packages/boot/app-boot/src/profile-resolution/resolver.ts +++ b/packages/boot/app-boot/src/profile-resolution/resolver.ts @@ -124,11 +124,14 @@ export interface RuntimeInterception { */ packageDir(specifier: string, parentURL: string): string | undefined /** - * Atomically publish a complete successor and fresh caches. Linked roots may be added or removed. - * Removed roots stop intercepting uncovered directories; existing modules and Node caches remain intact. - * @param successor - fully constructed generation retaining existing package mappings and local names. - * @throws when the profile scope or existing mappings change, local names are removed or override - * existing mappings, or a previously published link name selects a different real directory. + * Atomically publish a complete successor and fresh caches. Profile-scoped mappings, local names, and linked + * roots may be removed; removed roots stop intercepting uncovered directories. Existing modules and Node caches + * remain intact, so callers remove a package only after its plugins stop. + * A retained profile mapping may change its declarer if its normalized directory, version, and scope stay unchanged. + * @param successor - fully constructed generation retaining the installation mappings. + * @throws when the profile scope, a retained directory, version, scope, or installation declarer changes; + * an installation mapping is removed; a new local name overrides an existing mapping; or a previously published + * link name selects a different real directory. */ replace(successor: RuntimeResolution): void /** Restore the native resolver methods. Interceptions dispose in reverse order. */ @@ -378,20 +381,16 @@ class ResolutionRouter { } for (const [name, current] of this.current.entries) { const next = entries.get(name) + if (next === undefined && current.scope === 'profile') continue if (next === undefined || !sameResolution(current.packageDir, next.packageDir) - || !sameResolution(current.declarer, next.declarer) + || (current.scope === 'installation' && !sameResolution(current.declarer, next.declarer)) || current.version !== next.version || current.scope !== next.scope) { throw new Error(`profile resolution: replacing ${JSON.stringify(name)} requires a process restart`) } } const localPackageNames = new Set(successor.localPackageNames) - for (const name of this.current.localPackageNames) { - if (!localPackageNames.has(name)) { - throw new Error(`profile resolution: removing local package ${JSON.stringify(name)} requires a process restart`) - } - } for (const name of localPackageNames) { if (!this.current.localPackageNames.has(name) && this.current.entries.has(name)) { throw new Error(`profile resolution: overriding ${JSON.stringify(name)} locally requires a process restart`) diff --git a/packages/boot/app-boot/src/profile-resolution/service.ts b/packages/boot/app-boot/src/profile-resolution/service.ts index 5523b51eaf..7d493698a7 100644 --- a/packages/boot/app-boot/src/profile-resolution/service.ts +++ b/packages/boot/app-boot/src/profile-resolution/service.ts @@ -11,7 +11,7 @@ import { registerWorkerResolution, type RuntimeInterception, } from './resolver.ts' -import type { RuntimeResolution } from '../profile.ts' +import { ProfileRuntimeResolution, type RuntimeResolution } from '../profile.ts' import { readPluginMeta } from '../package-meta.ts' declare module '@deepseek-ai/cordis' { @@ -61,10 +61,12 @@ export class PluginPackages extends Service { private packages = new Map() private readonly interception: RuntimeInterception | undefined private disposeWorkerResolution: (() => void) | undefined + private current: RuntimeResolution | undefined constructor(ctx: Context, config: PluginPackagesConfig = {}) { super(ctx, 'pluginPackages') if (config.resolution === undefined) return + this.current = config.resolution const interception = installRuntimeInterception(config.resolution) this.disposeWorkerResolution = registerWorkerResolution(config.resolution) this.interception = interception @@ -76,17 +78,33 @@ export class PluginPackages extends Service { /** * Publish a complete successor generation for this process and subsequently created Workers. - * Linked roots may be removed without unloading modules or clearing Node caches. + * Profile mappings and local package names may be removed after their plugins stop; linked roots may also be removed. + * Retained profile mappings may change their declarer, but not their normalized directory, version, or scope; + * installation mappings must remain unchanged. Publication does not unload modules or clear Node caches. * @param successor - fully constructed generation accepted by {@link RuntimeInterception.replace}. */ replace(successor: RuntimeResolution): void { if (this.interception === undefined) throw new Error('plugin-packages: runtime resolution is not installed') this.interception.replace(successor) + this.current = successor this.packages = new Map() this.disposeWorkerResolution?.() this.disposeWorkerResolution = registerWorkerResolution(successor) } + /** + * Publish the latest generation computed by the installed resolution through {@link replace}. Package contents + * and loaded modules are not reloaded. + * @throws when no resolution is installed, it is plain data rather than a {@link ProfileRuntimeResolution}, + * reading the latest files fails, or the successor is rejected. A computed resolution without a profile can refresh. + */ + async refresh(): Promise { + if (!(this.current instanceof ProfileRuntimeResolution)) { + throw new Error('plugin-packages: the installed runtime resolution cannot be recomputed') + } + this.replace(await this.current.computeLatestResolution()) + } + /** * Locate the package named by a specifier without requiring a package export. * @param specifier - module specifier whose package owns the requested module. diff --git a/packages/boot/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts index 9a5e8578c3..e8aee3ce21 100644 --- a/packages/boot/app-boot/src/profile.ts +++ b/packages/boot/app-boot/src/profile.ts @@ -25,12 +25,12 @@ import { createRequire } from 'node:module' import { existsSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs' import { basename, dirname, join, relative, resolve, sep } from 'node:path' import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader' -import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include' import { resolveDshHome } from '@deepseek-ai/dsh-home-paths' import type { DshBundleManifest, DshPackageManifest } from '@deepseek-ai/dsh-package-manifest' import { evaluatePluginCompatibility, pluginCompatibilityWarning } from './plugin-compatibility.ts' import { readProfileVersionExemptions } from './profile-compatibility.ts' import { loadOverlayPatches } from './index.ts' +import { applyProfilePatches, type ProfilePatch } from './profile-patches.ts' import { realModuleDirectory } from './profile-resolution/legacy-links.ts' /** Directory under the Harness home holding every profile. */ @@ -83,7 +83,7 @@ export interface ProfileLayer { /** Absolute paths of the bundle's patch files, in application order. */ patchPaths: readonly string[] /** The parsed patch lists of every file, concatenated in application order. */ - patches: PatchOptions[] + patches: ProfilePatch[] } /** A loaded profile: resolved bundle layers plus the user's own patch layer. */ @@ -97,7 +97,7 @@ export interface Profile { /** Absolute path of the profile's own patch file. */ patchPath: string /** The profile's own patches; empty when the file is absent. */ - patches: PatchOptions[] + patches: ProfilePatch[] /** Selected bundles that contributed no layer, in `dsh.profile.bundles` order, with why. */ skippedBundles: SkippedBundle[] } @@ -221,10 +221,17 @@ export const DEFAULT_PROFILE_BUNDLES: readonly string[] = ['@deepseek-ai/dsh-bas * [admission](../../../../.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md)). */ export const OPTIONAL_BUNDLES: readonly string[] = [ + '@deepseek-ai/dsh-experimental-session-search', + '@deepseek-ai/dsh-experimental-ralph-bundle', + '@deepseek-ai/dsh-experimental-terminal-bundle', + '@deepseek-ai/dsh-experimental-badge-skill-bundle', + '@deepseek-ai/dsh-experimental-session-titles-bundle', '@deepseek-ai/dsh-experimental-agent-team-profile', '@deepseek-ai/dsh-experimental-voice-input-bundle', + '@deepseek-ai/dsh-experimental-cot-translation-bundle', '@deepseek-ai/dsh-experimental-auto-review', '@deepseek-ai/dsh-experimental-inspector-profile', + '@deepseek-ai/dsh-experimental-tool-worktree', ] const PROFILE_PATCH_TEMPLATE = `# Your patch layer for this dsh profile, applied after every bundle layer: @@ -440,7 +447,7 @@ export interface RuntimeResolutionOptions { */ export async function createRuntimeResolution( options: RuntimeResolutionOptions, -): Promise { +): Promise { const { installAnchor, profile, home = resolveDshHome() } = options const profilesDir = join(home, PROFILES_DIR) const manifest = readOptionalProfileManifest(profile) @@ -455,7 +462,7 @@ export async function createRuntimeResolution( : collectProfileScopePackages(profile, packageNames, profileDeclarers, profileVersions) const linkedRoots = profile === undefined ? [] : linkedProfileRoots(profile, profilesDir) // The Promise return type is the pre-stable API; construction has no asynchronous step. - return await Promise.resolve(Object.freeze({ + return await Promise.resolve(new ProfileRuntimeResolution({ installAnchor, profileDir: profile?.dir, home }, { profilesDir, profileDir: profile?.dir, localPackageNames: Object.freeze(localPackageNames), @@ -473,6 +480,54 @@ export async function createRuntimeResolution( })) } +/** Inputs a {@link ProfileRuntimeResolution} reuses to compute its successor. */ +interface ResolutionSource { + installAnchor: string + home: string + profileDir: string | undefined +} + +/** + * A runtime resolution that remembers the inputs it was computed from. Worker environment data carries only its + * fields; the inputs stay private to the thread that computed it. + */ +export class ProfileRuntimeResolution implements RuntimeResolution { + readonly profilesDir: string + readonly profileDir: string | undefined + readonly localPackageNames: readonly string[] + readonly entries: readonly RuntimeResolutionEntry[] + readonly linkedRoots: readonly LinkedRoot[] + readonly #source: ResolutionSource + + /** + * @param source - inputs of {@link createRuntimeResolution}, reused by {@link computeLatestResolution}. + * @param table - the computed package table. + */ + constructor(source: ResolutionSource, table: RuntimeResolution) { + this.#source = source + this.profilesDir = table.profilesDir + this.profileDir = table.profileDir + this.localPackageNames = table.localPackageNames + this.entries = table.entries + this.linkedRoots = table.linkedRoots + Object.freeze(this) + } + + /** + * Compute the latest generation from the same installation, profile directory, and Harness home, rereading the + * profile's manifest, bundle selection, and installed packages from disk, without retaining synthetic layers. + * With no profile directory, only installation packages are recomputed. This instance is unchanged. + * @returns a new resolution for the latest generation. + */ + computeLatestResolution(): Promise { + const { installAnchor, home, profileDir } = this.#source + return createRuntimeResolution({ + installAnchor, home, + ...profileDir === undefined ? {} : { profile: loadProfileDirectory('dsh', profileDir, installAnchor) }, + }) + } +} + /** Synthetic profiles used by direct callers may have no on-disk manifest. */ function readOptionalProfileManifest(profile: Profile | undefined): ProfileManifest | undefined { if (profile === undefined) return undefined @@ -751,16 +806,16 @@ export function loadProfile( /** * Compose patch layers into the effective entry list over an empty root — - * the same single `applyEntryPatches` call the boot include makes, so flag - * derivation and config dumps see exactly what mounts. + * compile preset operations and apply the same native patch list as boot, + * so flag derivation and config dumps see exactly what mounts. * @param layers - patch lists in application order. * @param warn - sink for skipped-patch diagnostics; defaults to silent (boot repeats them). * @returns the composed entry list. */ export function composeEntries( - layers: readonly PatchOptions[][], warn: (message: string) => void = () => {}, + layers: readonly ProfilePatch[][], warn: (message: string) => void = () => {}, ): EntryOptions[] { - return applyEntryPatches([], structuredClone(layers.flat()), (message: string, ...args: unknown[]) => { + return applyProfilePatches([], layers.flat(), (message: string, ...args: unknown[]) => { let index = 0 warn(message.replace(/%C/g, () => JSON.stringify(args[index++]))) }) diff --git a/packages/boot/app-boot/tests/config-schema.spec.ts b/packages/boot/app-boot/tests/config-schema.spec.ts index ed7ce470dc..aaacfcd68e 100644 --- a/packages/boot/app-boot/tests/config-schema.spec.ts +++ b/packages/boot/app-boot/tests/config-schema.spec.ts @@ -79,7 +79,8 @@ describe('generateConfigSchema', () => { ] const original = structuredClone(layers) const installAnchor = join(dir, 'installation.json') - const resolve = vi.spyOn(profileOperations, 'createRuntimeResolution').mockResolvedValue(resolution) + const resolve = vi.spyOn(profileOperations, 'createRuntimeResolution') + .mockResolvedValue(new profileOperations.ProfileRuntimeResolution({ installAnchor: '', home: '', profileDir: undefined }, resolution)) const result = await generateConfigSchema(profile, layers, installAnchor) expect(resolve).toHaveBeenCalledExactlyOnceWith({ installAnchor, profile }) expect(result['x-cordis'].entries.map(entry => [entry.path, entry.name])).toEqual([ @@ -499,6 +500,10 @@ describe('collectConfigSchemas', () => { expect(validates(result, [{ name: 'server', config: { port: 'wrong' } }])).toBe(false) expect(validates(result, [{ name: 'new-plugin', config: { arbitrary: true } }])).toBe(true) expect(validates(result, [{ id: 'server', disabled: true }], 'patchList')).toBe(true) + expect(validates(result, [{ preset: 'preset-standard', id: 'server', config: 'child-owned' }], 'patchList')).toBe(true) + expect(validates(result, [{ preset: '', insert: [] }], 'patchList')).toBe(false) + expect(validates(result, [{ preset: ' ', insert: [] }], 'patchList')).toBe(false) + expect(validates(result, [{ preset: { __jsExpr: 'ctx.preset' }, insert: [] }], 'patchList')).toBe(false) expect(validates(result, [{ id: 'server', config: { port: 'wrong' } }], 'patchList')).toBe(false) expect(validates(result, [{ id: 'server', config: {} }], 'patchList')).toBe(false) expect(validates(result, [{ id: 'server', name: 'not-server', config: 'ignored' }], 'patchList')).toBe(true) diff --git a/packages/boot/app-boot/tests/official-bundles.spec.ts b/packages/boot/app-boot/tests/official-bundles.spec.ts new file mode 100644 index 0000000000..37ab1f4a70 --- /dev/null +++ b/packages/boot/app-boot/tests/official-bundles.spec.ts @@ -0,0 +1,20 @@ +/** The offline catalog does not depend on installed provider packages or registry requests. */ + +import { describe, expect, it } from 'vitest' +import { OFFICIAL_ON_DEMAND_CATALOG, ON_DEMAND_BUNDLES } from '../src/official-bundles.ts' + +describe('Official on-demand catalog', () => { + it('offers exactly the two native subagent integrations with offline localized metadata', () => { + expect(ON_DEMAND_BUNDLES).toEqual(['@deepseek-ai/dsh-subagent-claude-code', '@deepseek-ai/dsh-subagent-codex']) + expect(OFFICIAL_ON_DEMAND_CATALOG.map(entry => entry.packageName)).toEqual(ON_DEMAND_BUNDLES) + for (const entry of OFFICIAL_ON_DEMAND_CATALOG) { + expect(entry.meta.icon).toMatch(/^data:image\/svg\+xml;base64,/) + for (const text of [entry.meta.title, entry.meta.description]) { + if (typeof text === 'string') throw new Error('Official metadata requires both shipped languages') + expect(text.en).toMatch(/\S/) + expect(text.zh).toMatch(/\S/) + } + expect(entry).not.toHaveProperty('version') + } + }) +}) diff --git a/packages/boot/app-boot/tests/package-meta.spec.ts b/packages/boot/app-boot/tests/package-meta.spec.ts index 98f5c9f658..21ea6b9fd5 100644 --- a/packages/boot/app-boot/tests/package-meta.spec.ts +++ b/packages/boot/app-boot/tests/package-meta.spec.ts @@ -244,6 +244,46 @@ describe('plugin locale display metadata', () => { }) }) + it.each(['manifest', 'export'])('reads a package-owned %s icon when the archive cannot realpath regular files', (source) => { + manifest({ './package.json': './package.json', './icon': './icon.svg' }, source === 'manifest' ? { icon: './icon.svg' } : {}) + const icon = join(dir, 'icon.svg') + file(icon, '') + // The executable resolver supplies module URLs independently of archive file realpath support. + resolvePluginResource('localized/icon', parentURL) + const original = fs.realpathSync + const previousPkg = Object.getOwnPropertyDescriptor(process, 'pkg') + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + fs.realpathSync = new Proxy(original, { + apply(target, receiver: unknown, args: unknown[]): unknown { + if (String(args[0]).endsWith(join('localized', 'icon.svg'))) { + throw Object.assign(new Error('archive-backed icon has no realpath entry'), { code: 'ENOENT' }) + } + return Reflect.apply(target, receiver, args) + }, + }) + syncBuiltinESMExports() + try { + expect(() => fs.realpathSync(icon)).toThrow('archive-backed') + expect(readPluginMeta('localized', parentURL)).toEqual({ title: 'localized', icon: 'data:image/svg+xml;base64,PHN2Zy8+' }) + } finally { + fs.realpathSync = original + if (previousPkg === undefined) Reflect.deleteProperty(process, 'pkg') + else Object.defineProperty(process, 'pkg', previousPkg) + syncBuiltinESMExports() + } + }) + + it.each(['manifest', 'export'])('refuses a %s icon file symlink owned by another package', (source) => { + manifest({ './package.json': './package.json', './icon': './icon.svg' }, source === 'manifest' ? { icon: './icon.svg' } : {}) + const foreign = join(root, 'foreign-package', 'icon.svg') + file(foreign, '') + file(join(root, 'foreign-package', 'package.json'), '{"name":"foreign-package"}') + symlinkSync(foreign, join(dir, 'icon.svg'), 'file') + const meta = readPluginMeta('localized', parentURL) + expect(meta?.icon).toBeUndefined() + expect(meta?.error).toContain(source === 'manifest' ? 'icon must remain inside its manifest directory' : 'icon must remain inside its package directory') + }) + it.each([ ['svg', 'image/svg+xml'], ['png', 'image/png'], ['jpg', 'image/jpeg'], ['jpeg', 'image/jpeg'], ['webp', 'image/webp'], ['SVG', 'image/svg+xml'], diff --git a/packages/boot/app-boot/tests/profile-patches-carrier.spec.ts b/packages/boot/app-boot/tests/profile-patches-carrier.spec.ts new file mode 100644 index 0000000000..d9d913d11e --- /dev/null +++ b/packages/boot/app-boot/tests/profile-patches-carrier.spec.ts @@ -0,0 +1,78 @@ +/** Patch identities work with archive-backed files and preserve physical filesystem aliases. */ +import { lstatSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, expect, it, onTestFinished, vi } from 'vitest' +import { loadOverlayPatches, realModuleFile } from '../src/index.ts' + +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, realpathSync: Object.assign(vi.fn(actual.realpathSync), { native: actual.realpathSync.native }) } +}) + +afterEach(() => { vi.mocked(realpathSync).mockReset() }) + +function packagedRuntime(): void { + const previous = Object.getOwnPropertyDescriptor(process, 'pkg') + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + onTestFinished(() => { + if (previous === undefined) Reflect.deleteProperty(process, 'pkg') + else Object.defineProperty(process, 'pkg', previous) + }) +} + +function fixture(): string { + const root = mkdtempSync(join(tmpdir(), 'profile-patch-carrier-')) + onTestFinished(() => { rmSync(root, { recursive: true, force: true }) }) + return root +} + +it('loads ordinary and anonymous scoped patches when packaged files support reads and directory realpath', () => { + packagedRuntime() + const root = fixture() + const file = join(root, 'cordis.patch.yml') + writeFileSync(file, JSON.stringify([ + { insert: [{ id: 'base', name: 'base' }] }, + { preset: 'preset-standard', insert: [{ name: 'anonymous' }] }, + ])) + vi.mocked(realpathSync).mockImplementation((path, options) => { + if (lstatSync(path).isFile()) throw Object.assign(new Error('archive file has no physical realpath'), { code: 'ENOENT' }) + return realpathSync.native(path, options) + }) + expect(() => realpathSync(file)).toThrow('archive file has no physical realpath') + const first = loadOverlayPatches('test', file) + expect(first[0]).toEqual({ insert: [{ id: 'base', name: 'base' }] }) + expect(first[1]?.insert?.[0]).toEqual({ id: expect.any(String) as string, name: 'anonymous' }) + expect(loadOverlayPatches('test', file)).toEqual(first) + expect(realModuleFile(file)).toBe(join(realpathSync(root), 'cordis.patch.yml')) +}) + +it.each([false, true])('keeps directory and file symlinks on the same patch identity (packaged: %s)', (packaged) => { + if (packaged) packagedRuntime() + const root = fixture() + const directory = join(root, 'bundle') + mkdirSync(directory) + const file = join(directory, 'Patch.yml') + writeFileSync(file, JSON.stringify([{ preset: 'preset-standard', insert: [{ name: 'anonymous' }] }])) + const directoryAlias = join(root, 'bundle-alias') + const fileAlias = join(root, 'patch-alias.yml') + symlinkSync(directory, directoryAlias, process.platform === 'win32' ? 'junction' : 'dir') + symlinkSync(file, fileAlias, 'file') + const first = loadOverlayPatches('test', file) + expect(loadOverlayPatches('test', join(directoryAlias, 'Patch.yml'))).toEqual(first) + expect(loadOverlayPatches('test', fileAlias)).toEqual(first) + expect(realModuleFile(fileAlias)).toBe(realpathSync(file)) +}) + +it('keeps ordinary physical resource normalization identical to Node realpath', () => { + const root = fixture() + const file = join(root, 'MixedCase.yml') + writeFileSync(file, '[]\n') + const requested = process.platform === 'win32' ? file.toLowerCase() : file + expect(realModuleFile(requested)).toBe(realpathSync(requested)) +}) + +it('reports missing packaged resources instead of assigning them an identity', () => { + packagedRuntime() + expect(() => realModuleFile(join(fixture(), 'absent.yml'))).toThrow('ENOENT') +}) diff --git a/packages/boot/app-boot/tests/profile-patches.spec.ts b/packages/boot/app-boot/tests/profile-patches.spec.ts new file mode 100644 index 0000000000..7afd826d32 --- /dev/null +++ b/packages/boot/app-boot/tests/profile-patches.spec.ts @@ -0,0 +1,305 @@ +/** Preset operations preserve native root patch lookup and share startup/dump/reload compilation. */ +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { describe, expect, it, onTestFinished } from 'vitest' +import { load } from 'js-yaml' +import { Context } from '@deepseek-ai/cordis' +import { EntryGroup, type EntryOptions } from '@deepseek-ai/cordis-plugin-loader' +import Group from '@deepseek-ai/cordis-plugin-group' +import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' +import { + applyProfilePatches, boot, compileProfilePatches, composeEntries, loadOverlayPatches, + prepareProfilePatches, profilePatchPreset, reconcileProfilePatches, renderConfigDump, type ProfilePatch, +} from '../src/index.ts' + +import { cloneProfilePatches } from '../src/profile-patches.ts' + +const preset = (plugins: EntryOptions[] = []): Omit & { + config: { id: string; description: string; plugins: EntryOptions[] } +} => ({ + id: 'preset-standard', name: 'cordis:test-preset', config: { id: 'standard', description: 'Original', plugins }, +}) +const row = (id: string): EntryOptions => ({ id, name: 'test-plugin', config: { value: 'initial' } }) +const children = (rows: EntryOptions[]): EntryOptions[] => (rows[0]!.config as { plugins: EntryOptions[] }).plugins +const temp = (): string => { + const dir = mkdtempSync(join(tmpdir(), 'dsh-preset-patches-')) + onTestFinished(() => { rmSync(dir, { recursive: true, force: true }) }) + return dir +} + +describe('preset patch composition', () => { + it('composes bundle insertions and later scoped user overrides without mutating inputs', () => { + const initial = [preset([row('base')])] + const patches: ProfilePatch[] = [ + { preset: 'preset-standard', insert: [row('optional')] }, + { preset: 'preset-standard', id: 'optional', config: { value: { __jsExpr: 'ctx.value' } } }, + { preset: 'preset-standard', id: 'base', disabled: true }, + ] + const original = structuredClone({ initial, patches }) + const result = applyProfilePatches(initial, patches) + expect(children(result)).toEqual([ + { ...row('base'), disabled: true }, + { ...row('optional'), config: { value: { __jsExpr: 'ctx.value' } } }, + ]) + expect(result[0]?.config).toMatchObject({ description: 'Original' }) + expect({ initial, patches }).toEqual(original) + expect(composeEntries([[{ insert: initial }], patches])).toEqual(result) + expect(compileProfilePatches(initial, patches).every(patch => !Object.hasOwn(patch, 'preset'))).toBe(true) + }) + + it('allows a later whole-preset replacement to replace all scoped edits', () => { + const replacement = { id: 'standard', plugins: [row('user')] } + expect(applyProfilePatches([preset()], [ + { preset: 'preset-standard', insert: [row('optional')] }, + { id: 'preset-standard', config: replacement }, + ])[0]?.config).toEqual(replacement) + }) + + it('applies each scoped operation to the current child list, including replacement children', () => { + const group = { id: 'group', name: 'cordis:group', group: true, config: [] } + const result = applyProfilePatches([preset([group])], [ + { preset: 'preset-standard', id: 'group', config: [row('replaced-child')] }, + { preset: 'preset-standard', id: 'replaced-child', disabled: true }, + { preset: 'preset-standard', id: 'group', insert: [row('inserted-child')] }, + ]) + expect(children(result)[0]?.config).toEqual([ + { ...row('replaced-child'), disabled: true }, row('inserted-child'), + ]) + }) + + it('retains ordinary root single-index behavior before and after a scoped operation', () => { + const warn: string[] = [] + const result = applyProfilePatches([ + { id: 'group', name: 'cordis:group', group: true, config: [] }, preset(), + ], [ + { id: 'group', config: [row('replacement')] }, + { preset: 'preset-standard', insert: [row('optional')] }, + { id: 'replacement', disabled: true }, + ], message => warn.push(message)) + expect(result[0]?.config).toEqual([row('replacement')]) + expect(warn).toEqual(['patch: entry %C not found']) + }) + + it.each([ + [{ id: 'group', name: 'cordis:group', group: true, config: [] }, [preset()]], + [{ id: 'group', name: 'cordis:group', group: true, config: [preset()] }, []], + ])('rejects an outer target added or removed by a whole-group replacement', (group, replacement) => { + expect(() => applyProfilePatches([group as EntryOptions], [ + { id: 'group', config: replacement }, { preset: 'preset-standard', insert: [row('optional')] }, + ])).toThrow('is missing or not addressable') + }) + + it('accepts an outer target inserted in a patchable group', () => { + const result = applyProfilePatches([{ id: 'group', name: 'cordis:group', group: true, config: [] }], [ + { id: 'group', insert: [preset()] }, + { preset: 'preset-standard', insert: [row('optional')] }, + ]) + expect(children(result[0]?.config as EntryOptions[])).toEqual([row('optional')]) + }) + + it('warns and skips a stale user override after its optional bundle is deselected', () => { + const messages: unknown[][] = [] + expect(applyProfilePatches([preset()], [{ preset: 'preset-standard', id: 'optional', disabled: true }], + (...args) => messages.push(args))).toEqual([preset()]) + expect(messages).toEqual([['preset "preset-standard": patch: entry %C not found', 'optional']]) + }) + + it.each([null, '', ' ', 1, { __jsExpr: 'ctx.preset' }])('rejects a nonliteral or empty outer target %j', (target) => { + const dir = temp() + const path = join(dir, 'patch.yml') + writeFileSync(path, JSON.stringify([{ preset: target, insert: [] }])) + expect(() => loadOverlayPatches('test', path)).toThrow('nonempty literal row id') + expect(() => loadOverlayPatches('test', path)).toThrow(path) + }) + + it('allows callers to omit a warning sink for skipped ordinary and scoped rows', () => { + expect(applyProfilePatches([], [{ id: 'missing', disabled: true }])).toEqual([]) + expect(compileProfilePatches([preset()], [{ preset: 'preset-standard', id: 'missing', disabled: true }])) + .toEqual([{ id: 'preset-standard', config: preset().config }]) + }) + + it('rejects a missing outer row', () => { + expect(() => applyProfilePatches([], [{ preset: 'missing', insert: [] }])).toThrow('preset "missing" is missing') + expect(profilePatchPreset({ id: 'ordinary' })).toBeUndefined() + }) + + it.each([undefined, null, [], { plugins: { __jsExpr: '[]' } }])('rejects a malformed preset config %j', (config) => { + expect(() => applyProfilePatches([{ ...preset(), config }], [{ preset: 'preset-standard', insert: [] }])) + .toThrow('literal config.plugins entry list') + }) + + it.each([ + { inserted: [row('same')] }, + { inserted: [{ id: 'nested', name: 'cordis:group', group: true, config: [row('same')] }] }, + ])('rejects inserted ids colliding with existing nested ids', ({ inserted }) => { + expect(() => applyProfilePatches([preset([ + { id: 'old-group', name: 'cordis:group', group: true, config: [row('same')] }, + ])], [{ preset: 'preset-standard', insert: inserted }])).toThrow('duplicate entry id "same"') + }) + + it('rejects duplicate ids inside an insertion and allows anonymous rows', () => { + expect(() => applyProfilePatches([preset()], [{ preset: 'preset-standard', insert: [row('same'), row('same')] }])) + .toThrow('duplicate entry id "same"') + const path = join(temp(), 'anonymous.patch.yml') + writeFileSync(path, JSON.stringify([{ preset: 'preset-standard', insert: [{ name: 'anonymous' }] }])) + expect(children(applyProfilePatches([preset()], loadOverlayPatches('test', path)))) + .toEqual([{ id: expect.any(String) as string, name: 'anonymous' }]) + }) + + it('keeps anonymous scoped identities stable by file and position without naming ordinary anonymous rows', () => { + const dir = temp() + const file = join(dir, 'contribution.patch.yml') + const content = JSON.stringify([ + { insert: [{ name: 'ordinary-anonymous' }] }, + { preset: 'preset-standard', insert: [ + { name: 'anonymous' }, + { id: 'explicit', name: 'cordis:group', group: true, config: [{ name: 'nested-anonymous' }] }, + ] }, + { preset: 'preset-standard', insert: [{ name: 'anonymous' }] }, + ]) + writeFileSync(file, content) + const first = loadOverlayPatches('test', file) + const second = loadOverlayPatches('test', file) + expect(second).toEqual(first) + expect(first[0]?.insert?.[0]?.id).toBeUndefined() + expect(first[1]?.insert?.[1]?.id).toBe('explicit') + const nested = first[1]?.insert?.[1]?.config as EntryOptions[] + const ids = [first[1]?.insert?.[0]?.id, nested[0]?.id, first[2]?.insert?.[0]?.id] + expect(ids.every(id => typeof id === 'string' && id.length > 0)).toBe(true) + expect(new Set(ids).size).toBe(3) + const other = join(dir, 'other.patch.yml') + writeFileSync(other, content) + expect(loadOverlayPatches('test', other)[1]?.insert?.[0]?.id).not.toBe(ids[0]) + expect(children(applyProfilePatches([preset()], first))).toEqual(children(applyProfilePatches([preset()], second))) + expect(Object.keys(first[1]!)).toEqual(['preset', 'insert']) + }) + + it.each([ + { initial: [], insert: [] }, + { initial: [preset()], insert: [{ id: 'group', name: 'cordis:group', group: true, config: [row('duplicate'), row('duplicate')] }] }, + { initial: [{ ...preset(), config: null }], insert: [] }, + ])('attributes scoped errors after cloning and flattening through composition, dumps, and boot', async ({ initial, insert }) => { + const dir = temp() + const path = join(dir, 'cordis.yml') + writeFileSync(path, JSON.stringify(initial)) + const patchFile = join(dir, 'third-party.patch.yml') + writeFileSync(patchFile, JSON.stringify([{ preset: 'preset-standard', insert }])) + const patches = cloneProfilePatches(loadOverlayPatches('test', patchFile)) + expect(() => composeEntries([[{ insert: initial }], patches])).toThrow(patchFile) + expect(() => renderConfigDump('test', path, [{ label: 'third-party', patches }])).toThrow(patchFile) + await expect(boot('test', path, patches)).rejects.toThrow(patchFile) + }) + + it('keeps anonymous rows from the initial preset beside newly inserted rows', () => { + const initial = load(JSON.stringify([{ ...preset(), config: { id: 'standard', plugins: [{ name: 'anonymous-base' }] } }])) as EntryOptions[] + expect(children(applyProfilePatches(initial, [{ preset: 'preset-standard', insert: [row('contributed')] }]))) + .toEqual([{ name: 'anonymous-base' }, row('contributed')]) + }) + + it('attributes errors from a caller warning sink to the loaded scoped operation', () => { + const path = join(temp(), 'warning.patch.yml') + writeFileSync(path, JSON.stringify([{ preset: 'preset-standard', id: 'absent', disabled: true }])) + expect(() => applyProfilePatches([preset()], loadOverlayPatches('test', path), () => { throw 'warning sink unavailable' })) + .toThrow(`warning sink unavailable in ${path}`) + }) + + it('attributes ordinary and scoped warnings to their own dump layers', () => { + const dir = temp() + const path = join(dir, 'cordis.yml') + writeFileSync(path, JSON.stringify([preset()])) + const warnings: string[] = [] + renderConfigDump('test', path, [ + { label: 'ordinary', patches: [{ id: 'missing', disabled: true }] }, + { label: 'scoped', patches: [{ preset: 'preset-standard', id: 'missing', disabled: true }] }, + ], message => warnings.push(message)) + expect(warnings).toEqual([ + 'test: [ordinary] patch: entry "missing" not found', + 'test: [scoped] preset "preset-standard": patch: entry "missing" not found', + ]) + }) +}) + +const capturePreset = { + [EntryGroup.key]: true, + async apply(ctx: Context, config: { plugins: EntryOptions[] }) { await ctx.plugin(Group, config.plugins) }, +} + +it('boots, dumps, and reloads a third-party relative module inside a contributed group', async () => { + const dir = temp() + const bundleDir = join(dir, 'third-party') + mkdirSync(bundleDir) + writeFileSync(join(bundleDir, 'relative.mjs'), [ + 'export function apply(ctx, config) {', + ' ctx.effect(() => ctx.reflect.provide("presetPatchResult", config.value))', + '}', + '', + ].join('\n')) + const path = join(dir, 'cordis.yml') + writeFileSync(path, JSON.stringify([preset()])) + const patchPath = join(bundleDir, 'cordis.patch.yml') + writeFileSync(patchPath, [ + '- preset: preset-standard', + ' insert:', + ' - id: nested', + ' name: cordis:group', + ' group: true', + ' config:', + ' - id: contributed', + ' name: ./relative.mjs', + ' config:', + ' value: !!js ctx.get("fixtureValue")', + '', + ].join('\n')) + const patches = loadOverlayPatches('test', patchPath) + const ctx = await boot('test', path, patches, (context) => { + context.loader.builtins['test-preset'] = capturePreset + context.provide('fixtureValue', 'from-child-context') + }) + onTestFinished(() => ctx.fiber.dispose()) + expect(ctx.get('presetPatchResult')).toBe('from-child-context') + const declared = [...ctx.loader.entries()].find(entry => entry.options.id === 'preset-standard')! + const dump = renderConfigDump('test', path, [{ label: 'third-party', patches }]) + expect(load(dump, { schema: entryListSchema })).toEqual([declared.options]) + expect(dump).toContain(pathToFileURL(join(bundleDir, 'relative.mjs')).href) + expect(dump).toContain('!!js ctx.get("fixtureValue")') + await reconcileProfilePatches(ctx, [...patches, { + preset: 'preset-standard', id: 'contributed', config: { value: 'user' }, + }], 'test') + expect(ctx.get('presetPatchResult')).toBe('user') + await reconcileProfilePatches(ctx, [], 'test') + expect(ctx.get('presetPatchResult')).toBeUndefined() + await reconcileProfilePatches(ctx, [{ preset: 'preset-standard', id: 'contributed', disabled: true }], 'test') + expect(ctx.get('presetPatchResult')).toBeUndefined() + await reconcileProfilePatches(ctx, patches, 'test') + expect(ctx.get('presetPatchResult')).toBe('from-child-context') +}) + +it('rejects a malformed root configuration before compiling its scoped patch', async () => { + const dir = temp() + const path = join(dir, 'cordis.yml') + writeFileSync(path, '{}\n') + await expect(boot('test', path, [{ preset: 'preset-standard', insert: [] }])) + .rejects.toThrow('must be a top-level array of entries') +}) + +it('leaves lower-level nonprofile compatibility callers in control of their native patches', async () => { + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + const patches = [{ id: 'custom', disabled: true }] + expect(prepareProfilePatches(ctx, patches, pathToFileURL(temp()).href)).toBe(patches) +}) + +it('anchors relative modules in an inserted preset definition and its nested groups', () => { + const dir = temp() + const path = join(dir, 'cordis.patch.yml') + writeFileSync(path, JSON.stringify([{ insert: [{ + ...preset([{ id: 'group', name: 'cordis:group', group: true, config: [{ id: 'child', name: './child.mjs' }] }]), + name: '@deepseek-ai/dsh-agent-preset', + }] }])) + const patches = loadOverlayPatches('test', path) + expect(patches[0]?.insert?.[0]?.config).toMatchObject({ plugins: [{ config: [{ + name: pathToFileURL(join(dir, 'child.mjs')).href, + }] }] }) +}) diff --git a/packages/boot/app-boot/tests/profile-resolution-service.spec.ts b/packages/boot/app-boot/tests/profile-resolution-service.spec.ts index d066329d36..be57535fd4 100644 --- a/packages/boot/app-boot/tests/profile-resolution-service.spec.ts +++ b/packages/boot/app-boot/tests/profile-resolution-service.spec.ts @@ -1,6 +1,6 @@ /** Package metadata queries share the active runtime resolution. */ -import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs' +import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs' import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' @@ -9,7 +9,7 @@ import { getEnvironmentData } from 'node:worker_threads' import { Context } from '@deepseek-ai/cordis' import { afterEach, describe, expect, it } from 'vitest' import { PluginPackages } from '../src/profile-resolution/service.ts' -import type { RuntimeResolution } from '../src/profile.ts' +import { createRuntimeResolution, initProfile, loadProfileDirectory, type RuntimeResolution } from '../src/profile.ts' const roots: string[] = [] const contexts: Context[] = [] @@ -53,6 +53,71 @@ function resolution( } describe('profile package metadata service', () => { + it('refreshes the installed resolution from its own profile and installation', async () => { + const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-package-service-refresh-'))) + roots.push(root) + const installAnchor = join(root, 'install', 'package.json') + file(installAnchor, JSON.stringify({ name: '@deepseek-ai/dsh', version: '0.0.0' })) + const profileDir = join(root, 'profiles', 'test') + initProfile(profileDir, ['extra']) + const bundleDir = join(profileDir, 'node_modules', 'extra') + const privateDir = join(bundleDir, 'node_modules', 'metadata-lib') + file(join(bundleDir, 'package.json'), JSON.stringify({ + name: 'extra', version: '1.0.0', dependencies: { 'metadata-lib': '1.0.0' }, dsh: { bundle: { patch: './cordis.patch.yml' } }, + })) + file(join(bundleDir, 'cordis.patch.yml'), '[]\n') + const parentURL = pathToFileURL(join(profileDir, 'caller.mjs')).href + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(PluginPackages, { + resolution: await createRuntimeResolution({ installAnchor, home: root, profile: loadProfileDirectory('dsh', profileDir, installAnchor) }), + }) + expect(ctx.pluginPackages.packageOf('metadata-lib', parentURL)).toBeUndefined() + + pkg(privateDir, '1.0.0') + const manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')) as Record + writeFileSync(join(profileDir, 'package.json'), JSON.stringify({ ...manifest, dependencies: { extra: '1.0.0' } })) + await ctx.pluginPackages.refresh() + + expect(ctx.pluginPackages.packageOf('metadata-lib', parentURL)).toMatchObject({ dir: privateDir, version: '1.0.0' }) + expect(createRequire(join(profileDir, 'caller.cjs')).resolve('metadata-lib')).toBe(join(privateDir, 'index.cjs')) + }) + + it('refreshes a computed resolution without a profile', async () => { + const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-package-service-installation-'))) + roots.push(root) + const installAnchor = join(root, 'install', 'package.json') + file(installAnchor, JSON.stringify({ name: 'installation', version: '1.0.0', dependencies: { 'metadata-lib': '*' } })) + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(PluginPackages, { resolution: await createRuntimeResolution({ installAnchor, home: root }) }) + const parentURL = pathToFileURL(join(root, 'profiles', 'test', 'caller.cjs')).href + expect(ctx.pluginPackages.packageOf('metadata-lib', parentURL)).toBeUndefined() + const packageDir = join(root, 'install', 'node_modules', 'metadata-lib') + pkg(packageDir, '1.0.0') + + await ctx.pluginPackages.refresh() + + expect(ctx.pluginPackages.packageOf('metadata-lib', parentURL)).toMatchObject({ dir: packageDir, version: '1.0.0' }) + expect(createRequire(parentURL).resolve('metadata-lib')).toBe(join(packageDir, 'index.cjs')) + }) + + it('rejects refreshing a plain-data or absent resolution', async () => { + const root = mkdtempSync(join(tmpdir(), 'dsh-package-service-plain-')) + roots.push(root) + const packageDir = join(root, 'lib') + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(PluginPackages, { + resolution: resolution(join(root, 'profiles'), join(root, 'profiles', 'test'), packageDir, pkg(packageDir, '1.0.0'), '1.0.0'), + }) + await expect(ctx.pluginPackages.refresh()).rejects.toThrow('plugin-packages: the installed runtime resolution cannot be recomputed') + const bare = new Context() + contexts.push(bare) + await bare.plugin(PluginPackages) + await expect(bare.pluginPackages.refresh()).rejects.toThrow('plugin-packages: the installed runtime resolution cannot be recomputed') + }) + it('reads translated metadata from the selected local package without importing its entry', async () => { const root = mkdtempSync(join(tmpdir(), 'dsh-localized-package-service-')) roots.push(root) diff --git a/packages/boot/app-boot/tests/profile-resolution.spec.ts b/packages/boot/app-boot/tests/profile-resolution.spec.ts index a36347661d..82cf24b0ea 100644 --- a/packages/boot/app-boot/tests/profile-resolution.spec.ts +++ b/packages/boot/app-boot/tests/profile-resolution.spec.ts @@ -1618,6 +1618,14 @@ describe('runtime resolution', { concurrent: false }, () => { : entry), }) }).toThrow(/requires a process restart/u) + expect(() => { + registration.replace({ + ...first, + entries: first.entries.map(entry => entry.name === '@deepseek-ai/dsh-core' + ? { ...entry, declarer: join(f.root, 'another-bundle', 'package.json') } + : entry), + }) + }).toThrow(/requires a process restart/u) expect(() => { registration.replace({ ...first, localPackageNames: ['@deepseek-ai/dsh-core'] }) }).toThrow(/requires a process restart/u) @@ -1629,7 +1637,6 @@ describe('runtime resolution', { concurrent: false }, () => { }).toThrow(/cannot change its profile scope/u) registration.replace({ ...first, localPackageNames: ['new-local'] }) registration.replace({ ...first, localPackageNames: ['new-local'] }) - expect(() => { registration.replace(first) }).toThrow(/removing local package/u) const linked = { name: 'linked-plugin', realPath: join(f.root, 'work', 'a') } const withLocal = { ...first, localPackageNames: ['new-local'] } registration.replace({ ...withLocal, linkedRoots: [linked] }) @@ -1643,6 +1650,104 @@ describe('runtime resolution', { concurrent: false }, () => { )).toBe(f.installed) }) + it('removes profile mappings and local names while retaining changed profile mappings as restarts', async () => { + const f = fixture() + const firstDir = join(f.root, 'bundle-a', 'node_modules', 'private-lib') + const secondDir = join(f.root, 'bundle-b', 'node_modules', 'private-lib') + pkg(firstDir, 'private-lib', 1) + pkg(secondDir, 'private-lib', 2) + const base = await resolutionOf(f) + const entry = { + name: 'private-lib', packageDir: firstDir, version: '1.0.0', + declarer: join(firstDir, 'package.json'), scope: 'profile' as const, + } + const first = { ...base, entries: [...base.entries, entry], localPackageNames: ['local-lib'] } + const registration = installRuntimeInterception(first) + registrations.push(registration) + const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href + expect(registration.packageDir('private-lib', parent)).toBe(firstDir) + expect(() => { + registration.replace({ ...first, entries: [...base.entries, { + ...entry, packageDir: secondDir, version: '2.0.0', declarer: join(secondDir, 'package.json'), + }] }) + }).toThrow(/replacing "private-lib" requires a process restart/u) + expect(registration.packageDir('private-lib', parent)).toBe(firstDir) + registration.replace(base) + expect(registration.packageDir('private-lib', parent)).toBeUndefined() + expect(() => resolveFrom('private-lib', parent)).toThrow(expect.objectContaining({ code: 'ERR_MODULE_NOT_FOUND' })) + registration.replace(first) + expect(registration.packageDir('private-lib', parent)).toBe(firstDir) + }) + + it('accepts another profile declarer for the same normalized package directory and version', async () => { + const f = fixture() + const shared = join(f.root, 'shared', 'node_modules', 'private-lib') + pkg(shared, 'private-lib', 1) + const firstAnchor = pkg(join(f.root, 'shared', 'bundle-a'), 'bundle-a', 1, { 'private-lib': '*' }) + const nextAnchor = pkg(join(f.root, 'shared', 'bundle-b'), 'bundle-b', 1, { 'private-lib': '*' }) + const alias = join(f.root, 'private-lib-alias') + symlinkSync(shared, alias, process.platform === 'win32' ? 'junction' : 'dir') + try { + const base = await resolutionOf(f) + const entry = { name: 'private-lib', packageDir: shared, declarer: firstAnchor, version: '1.0.0', scope: 'profile' as const } + const registration = installRuntimeInterception({ ...base, entries: [...base.entries, entry] }) + registrations.push(registration) + const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href + const require = createRequire(parent) + expect(registration.packageDir('private-lib', parent)).toBe(shared) + expect(require.resolve('private-lib')).toBe(join(shared, 'index.cjs')) + expect(resolveFrom('private-lib', parent)).toBe(pathToFileURL(join(shared, 'index.js')).href) + + registration.replace({ ...base, entries: [...base.entries, { ...entry, packageDir: alias, declarer: nextAnchor }] }) + + expect(registration.packageDir('private-lib', parent)).toBe(alias) + expect(require.resolve('private-lib')).toBe(join(shared, 'index.cjs')) + expect(resolveFrom('private-lib', parent)).toBe(pathToFileURL(join(shared, 'index.js')).href) + } finally { + unlinkSync(alias) + } + }) + + it.each(['directory', 'version', 'scope'] as const)('rejects a retained profile mapping with a different %s', async (change) => { + const f = fixture() + const packageDir = join(f.root, 'bundle', 'node_modules', 'private-lib') + pkg(packageDir, 'private-lib', 1) + const base = await resolutionOf(f) + const entry = { name: 'private-lib', packageDir, declarer: join(packageDir, 'package.json'), version: '1.0.0', scope: 'profile' as const } + const registration = installRuntimeInterception({ ...base, entries: [...base.entries, entry] }) + registrations.push(registration) + const successor = { + ...entry, + packageDir: change === 'directory' ? join(f.root, 'different') : entry.packageDir, + version: change === 'version' ? '2.0.0' : entry.version, + scope: change === 'scope' ? 'installation' as const : entry.scope, + } + + expect(() => { registration.replace({ ...base, entries: [...base.entries, successor] }) }) + .toThrow(/replacing "private-lib" requires a process restart/u) + expect(registration.packageDir('private-lib', pathToFileURL(join(f.profile.dir, 'entry.mjs')).href)).toBe(packageDir) + }) + + it('recomputes from the captured directory without retaining synthetic profile layers', async () => { + const f = fixture() + const profileDir = f.profile.dir + const bundleDir = join(f.root, 'synthetic-bundle') + pkg(bundleDir, 'synthetic-bundle', 1, { 'private-lib': '*' }) + pkg(join(bundleDir, 'node_modules', 'private-lib'), 'private-lib', 1) + f.profile.layers.push({ packageName: 'synthetic-bundle', packageDir: bundleDir, patchPaths: [], patches: [] }) + const first = await createRuntimeResolution({ installAnchor: f.installAnchor, home: f.root, profile: f.profile }) + expect(first.entries.some(entry => entry.name === 'private-lib')).toBe(true) + f.profile.dir = join(f.root, 'another-profile') + file(join(f.profile.dir, 'package.json'), JSON.stringify({ name: 'another-profile' })) + + const latest = await first.computeLatestResolution() + + expect(latest.profileDir).toBe(profileDir) + expect(latest.entries.some(entry => entry.name === 'private-lib')).toBe(false) + expect(first.profileDir).toBe(profileDir) + expect(first.entries.some(entry => entry.name === 'private-lib')).toBe(true) + }) + it('leaves non-package and out-of-scope metadata lookups to native resolution', async () => { const f = fixture() const outside = join(f.root, 'outside') diff --git a/packages/boot/app-boot/tests/profile.spec.ts b/packages/boot/app-boot/tests/profile.spec.ts index 297b198401..fbd47c20bf 100644 --- a/packages/boot/app-boot/tests/profile.spec.ts +++ b/packages/boot/app-boot/tests/profile.spec.ts @@ -514,6 +514,7 @@ describe('createRuntimeResolution', () => { expect(resolution.entries.find(entry => entry.name === 'dep-of-a')?.packageDir).toBe(join(modules, 'dep-of-a')) expect(existsSync(join(home, 'profiles', 'node_modules'))).toBe(false) await expect(createRuntimeResolution({ installAnchor: anchor, home })).resolves.toEqual(resolution) + await expect(resolution.computeLatestResolution()).resolves.toEqual(resolution) }) it('keeps selected bundle closures profile-local without overriding installation packages', async () => { diff --git a/packages/boot/app-boot/tests/user-patches.spec.ts b/packages/boot/app-boot/tests/user-patches.spec.ts index 465f9d82c6..6db0154321 100644 --- a/packages/boot/app-boot/tests/user-patches.spec.ts +++ b/packages/boot/app-boot/tests/user-patches.spec.ts @@ -414,11 +414,13 @@ describe('profile reconciliation settlement', () => { onTestFinished(async () => { release.resolve(undefined); await ctx.fiber.dispose() }) let settled = false const operation = reconcileProfilePatches(ctx, [], NAME).then(() => { settled = true }) + onTestFinished(async () => { release.resolve(undefined); await operation }) await started.promise - expect([...ctx.loader.entries()].some(entry => entry.options.id === 'held')).toBe(false) + expect([...ctx.loader.entries()].find(entry => entry.options.id === 'held')?.fiber?.uid).toBeNull() expect(settled).toBe(false) release.resolve(undefined) await operation + expect([...ctx.loader.entries()].some(entry => entry.options.id === 'held')).toBe(false) expect(settled).toBe(true) }) }) diff --git a/packages/boot/cmdline/package.json b/packages/boot/cmdline/package.json index 4859752fb5..c6ea6cb37a 100644 --- a/packages/boot/cmdline/package.json +++ b/packages/boot/cmdline/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cmdline", "description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/boot/config-editor/README.i18n.yaml b/packages/boot/config-editor/README.i18n.yaml index 5727bdff08..265864b3ec 100644 --- a/packages/boot/config-editor/README.i18n.yaml +++ b/packages/boot/config-editor/README.i18n.yaml @@ -18,8 +18,8 @@ en: 1eedaa5d72b48177 zh: 39abc1d927a692d2 /deepseek-ai-dsh-config-editor/understand-the-implementation: - en: baa887b5ecfa7001 - zh: d3e03c2b1d10c67e + en: 6c0265c949db473c + zh: fe1849f32f2d111c /deepseek-ai-dsh-config-editor/further-exploration: en: 0c9e1d1401a865cc zh: 6130d52636e84676 diff --git a/packages/boot/config-editor/README.md b/packages/boot/config-editor/README.md index 17da6551f9..e67bdad23a 100644 --- a/packages/boot/config-editor/README.md +++ b/packages/boot/config-editor/README.md @@ -42,6 +42,8 @@ The [editor](src/index.ts) reconciles external changes before deriving a candida Configuration reads share one composition for entries without profile config overrides. Overridden entries are composed separately with only their own override removed; profile inserts and other entries' overrides remain effective. Returned configs are detached, and compositions are not cached between reads. +Host configuration reads, edits, and resets ignore preset-scoped operations, including those whose child IDs match Host entry IDs. + diff --git a/packages/boot/config-editor/README.zh.md b/packages/boot/config-editor/README.zh.md index d1ff18344f..62abcab4bc 100644 --- a/packages/boot/config-editor/README.zh.md +++ b/packages/boot/config-editor/README.zh.md @@ -42,6 +42,8 @@ kind: "package-reference" 读取配置时,没有 profile config 覆盖项的条目共用一次组合结果。有覆盖项的条目分别组合,仅移除自身的覆盖项;profile 插入的条目和其他条目的覆盖项仍然生效。返回的配置为独立副本,组合结果不跨读取缓存。 +Host 配置读取、编辑和重置均忽略预设作用域操作,包括子项 ID 与 Host 条目 ID 相同的操作。 + diff --git a/packages/boot/config-editor/package.json b/packages/boot/config-editor/package.json index 831becbac5..346d9d4442 100644 --- a/packages/boot/config-editor/package.json +++ b/packages/boot/config-editor/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-config-editor", "description": "Persist plugin configuration through profile patches and Loader reconciliation", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -36,7 +36,7 @@ "dependencies": { "@deepseek-ai/dsh-atomic-write": "workspace:*", "yaml": "^2.8.1", - "js-yaml": "^4.1.0" + "js-yaml": "^4.3.2" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/boot/config-editor/src/index.ts b/packages/boot/config-editor/src/index.ts index ffec879f40..4a3e5be473 100644 --- a/packages/boot/config-editor/src/index.ts +++ b/packages/boot/config-editor/src/index.ts @@ -3,11 +3,11 @@ import { readFile } from 'node:fs/promises' import { join } from 'node:path' import { isDeepStrictEqual } from 'node:util' import { Context, FiberState, Service, resolveConfig } from '@deepseek-ai/cordis' -import { entryListSchema, type PatchOptions } from '@deepseek-ai/cordis-plugin-include' +import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' import yaml from 'js-yaml' import type { Entry, EntryOptions } from '@deepseek-ai/cordis-plugin-loader' import type {} from '@deepseek-ai/dsh-hmr' -import { composeEntries, loadProfileDirectory, readProfilePatches, reconcileProfilePatches } from '@deepseek-ai/dsh-app-boot' +import { composeEntries, loadProfileDirectory, profilePatchPreset, readProfilePatches, reconcileProfilePatches, type ProfilePatch } from '@deepseek-ai/dsh-app-boot' import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write' import { isMap, isSeq, parseDocument, Scalar, visit } from 'yaml' @@ -22,7 +22,7 @@ function flatten(rows: EntryOptions[]): EntryOptions[] { return rows.flatMap(row => [row, ...row.group && Array.isArray(row.config) ? flatten(row.config as EntryOptions[]) : []]) } -/** Persist complete raw configs and apply them through the normal Loader path. */ +/** Persist Host entry configs without changing preset-scoped operations, then reconcile through Loader. */ export class ConfigEditor extends Service { static inject = ['loader', 'profileContext'] @@ -50,8 +50,9 @@ export class ConfigEditor extends Service { const profile = this.ownerContext.profileContext const loaded = loadProfileDirectory('dsh', profile.dir, profile.installAnchor) const entries = this.entries() + const hostPatches = loaded.patches.filter(patch => profilePatchPreset(patch) === undefined) // An own config key can replace inherited config even when its value is undefined. - const overridden = new Set(loaded.patches.filter(patch => patch.insert === undefined && Object.hasOwn(patch, 'config')).map(patch => patch.id)) + const overridden = new Set(hostPatches.filter(patch => patch.insert === undefined && Object.hasOwn(patch, 'config')).map(patch => patch.id)) const composed = new Map() if (entries.some(entry => !overridden.has(entry.options.id))) { for (const row of flatten(composeEntries([...loaded.layers.map(layer => layer.patches), loaded.patches]))) { @@ -63,7 +64,7 @@ export class ConfigEditor extends Service { inherited: overridden.has(entry.options.id) ? this.inherited(entry, loaded) : structuredClone((composed.get(entry.options.id)?.config ?? {}) as Record), - override: structuredClone((loaded.patches.findLast( + override: structuredClone((hostPatches.findLast( row => row.id === entry.options.id && row.config !== undefined, )?.config ?? {}) as Record), })) @@ -71,7 +72,7 @@ export class ConfigEditor extends Service { private inherited(entry: Entry, loaded: ReturnType): Record { const patches = loaded.patches.map((patch) => { - if (patch.id !== entry.options.id || patch.insert !== undefined) return patch + if (patch.id !== entry.options.id || patch.insert !== undefined || profilePatchPreset(patch) !== undefined) return patch const rest = { ...patch }; Reflect.deleteProperty(rest, 'config') return rest }) @@ -115,12 +116,12 @@ export class ConfigEditor extends Service { if (!isSeq(document.contents)) throw new Error('Profile patch must be a YAML sequence') document.contents.flow = false const index = document.contents.items.findLastIndex((item, index) => isMap(item) - && document.getIn([index, 'id']) === entry.options.id && !item.has('insert') + && document.getIn([index, 'id']) === entry.options.id && !item.has('insert') && !item.has('preset') && (!item.has('name') || document.getIn([index, 'name']) === entry.options.name)) if (isDeepStrictEqual(next, inherited)) { for (let index = document.contents.items.length - 1; index >= 0; index--) { const row = document.contents.items[index] - if (!isMap(row) || document.getIn([index, 'id']) !== entry.options.id || row.has('insert')) continue + if (!isMap(row) || document.getIn([index, 'id']) !== entry.options.id || row.has('insert') || row.has('preset')) continue row.delete('config') if (row.items.length === Number(row.has('id')) + Number(row.has('name'))) document.delete(index) } @@ -134,7 +135,7 @@ export class ConfigEditor extends Service { } }) const profile = this.ownerContext.profileContext const loaded = loadProfileDirectory('dsh', profile.dir, profile.installAnchor) - const patches = readProfilePatches('dsh', profile, { ...loaded, patches: yaml.load(String(document), { schema: entryListSchema }) as PatchOptions[] }) + const patches = readProfilePatches('dsh', profile, { ...loaded, patches: yaml.load(String(document), { schema: entryListSchema }) as ProfilePatch[] }) const effective = flatten(composeEntries([patches])).find(row => row.id === entry.options.id) if (!isDeepStrictEqual(effective?.config ?? {}, next)) { throw new Error(`Configuration for "${entry.options.id}" is overridden by a home patch or command-line overlay`) diff --git a/packages/boot/hmr/README.i18n.yaml b/packages/boot/hmr/README.i18n.yaml index e66a76d589..4f1eef45bf 100644 --- a/packages/boot/hmr/README.i18n.yaml +++ b/packages/boot/hmr/README.i18n.yaml @@ -21,8 +21,8 @@ en: 45ee194ad781d603 zh: b3d7fcd8ed73717c /deepseek-ai-dsh-hmr/understand-the-implementation: - en: 247499831fdee81f - zh: 690f1859c1f73bbe + en: 227c3c99c3c4b597 + zh: 853b656340cb8bce /deepseek-ai-dsh-hmr/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 @@ -39,8 +39,8 @@ en: 89e0624fafd701fb zh: 19d36a33c1d9af9d /deepseek-ai-dsh-hmr/known-limitations-and-deferred-work: - en: 379bc2c72fd9e53e - zh: 1fff1e0e676bb161 + en: 0f46d4df1dab7f3b + zh: d172835159a20cce /deepseek-ai-dsh-hmr/known-limitations-and-deferred-work/dev-note: en: 7972e4b2005ec9a4 zh: 754c49ec538c164c diff --git a/packages/boot/hmr/README.md b/packages/boot/hmr/README.md index 6c7d9bb87f..513abc07b2 100644 --- a/packages/boot/hmr/README.md +++ b/packages/boot/hmr/README.md @@ -56,7 +56,15 @@ Chokidar options, including polling, retain their existing meaning. Exact config `watchConfig()` registers an awaited configuration handler. `runExclusive()` serializes configuration changes and Loader updates with automatic reloads and rejects nested transactions. Package installation and removal run outside this queue. HMR does not acquire the package writer lock; manifest notifications reload only when the ordered `dsh.profile.bundles` list changes. Profile and home patch changes also trigger recomposition. File events received during a configuration transaction are processed afterward. Include refreshes and profile reconciliation reach plugins through ordinary Loader entry updates; Loader commits volatile-only changes in place. -App-boot owns profile parsing and patch precedence. HMR reads the launcher’s data-only `profileContext`, registers the profile manifest and both user patch watches during initialization, and waits for application readiness before processing changes. Its disposal closes the watchers and cancels reloads waiting for startup. HMR also owns module-cache replacement and reload scheduling. Configuration watchers start outside the active transaction context so later notifications can enter the queue. +App-boot owns profile parsing and patch precedence. HMR reads the launcher’s data-only `profileContext`, registers the profile manifest and both user patch watches during initialization, and waits for application readiness before processing changes. Its disposal closes the watchers and cancels reloads waiting for startup. Configuration watchers start outside the active transaction context so later notifications can enter the queue. + +A changed `package.json` outside `node_modules` expires that package's cached configuration. A configuration-only manifest triggers no module reload; a manifest imported as a JSON module retains dependency-driven reloads, and one in the host dependency graph retains host reloads. Dedicated configuration watches keep their existing ownership. A new package entry takes effect when its Loader entry restarts. + +Loader entries retain their raw import results. HMR retains every distinct imported namespace for each entry name and configuration-tree base URL; an uninitialized entry does not overwrite another entry's namespace. HMR matches those objects to cached ModuleJob namespaces instead of resolving entry names again. Names without an imported namespace and `cordis:` builtins keep name-based resolution. Source reloads therefore use the loaded modules after `exports` or `main` changes. + +Entries sharing a runtime are replaced together, with each entry receiving the implementation for its own module. Duplicate entries for one module retain their separate plugin instances. On replacement failure, HMR removes partially activated replacements and restores the previous modules and plugin implementations. A successful reload updates every matching entry record, including disabled entries; a failed replacement leaves those records unchanged. Entries that have never loaded remain uninitialized. + +[package-manifest.ts](src/package-manifest.ts) owns the Node internal interfaces for package reads, scope, type, and nearest-manifest lookups. Each HMR instance owns its configuration cache and restores its installed hooks on disposal. Manifest invalidation clears the ESM `ResolveCache` and CommonJS `_pathCache`: neither records every consulted manifest, and entries may point outside their package directory. CommonJS loads resolve requests with Node's default resolver before entering the native loader, bypassing old request aliases while preserving evaluated modules. Synchronous resolve hooks receive the resulting filenames. Ordinary module replacement remains HMR's separate operation. Watched module paths use Node ESM resolution's `realpathSync()` spelling, including Windows short directory names, so file events match the module cache. @@ -86,7 +94,11 @@ Reloading a contributing plugin can change later request prefixes; HMR does not - Module replacement requires Node loader internals. Framework dependency changes call the host-provided `loader.exit()` hook; HMR itself does not restart the process. -- Replacing installed package versions still requires a restart through Plugin Manager. The browser Client module graph retains its separate browser-side loading mechanism. +- Replacing installed package versions still requires a restart through Plugin Manager; manifests below `node_modules` keep Node's cached configuration. The browser Client module graph retains its separate browser-side loading mechanism. +- TSX loaders and ordinary application-created Workers are outside the supported package-cache refresh scope. +- CommonJS requests must be resolvable by Node's default resolver. Synchronous hooks may postprocess the resulting filenames, as Desktop's Office resolver does; hooks requiring the original request name or introducing virtual requests are unsupported. Builtins retain Node's original loading behavior. +- Future work: retain package invalidation across HMR disposal and replacement. Restoring the native readers can expose their older cached configuration again. +- If a shared runtime's replacement modules have different plugin callbacks, an instance without a Loader entry cannot be assigned to one of them. HMR reports an ambiguity error and rolls back the reload. - `watchConfig()` resolves when Chokidar reports readiness. On darwin, libuv starts the FSEvents stream afterwards on its own thread, so a write that lands within milliseconds of registration is not reported until the next event in that directory; edits made after startup are unaffected. ### Dev Note diff --git a/packages/boot/hmr/README.zh.md b/packages/boot/hmr/README.zh.md index b6976cf255..ba99733ccf 100644 --- a/packages/boot/hmr/README.zh.md +++ b/packages/boot/hmr/README.zh.md @@ -56,7 +56,15 @@ Chokidar 选项(包括轮询)保持原有含义。精确配置监听同时 `watchConfig()` 注册会被等待的配置处理器。`runExclusive()` 将配置变更、Loader 更新与自动重载串行化,并拒绝嵌套事务。包安装和删除在该队列之外执行。HMR 不获取包操作写锁;manifest 通知仅在有序的 `dsh.profile.bundles` 列表变化时触发重载。profile 与 home patch 变化也会触发重新组合。配置事务期间收到的文件事件在事务结束后处理。 Include 刷新和 profile 重载都通过普通的 Loader 条目更新到达插件;仅 volatile 变化由 Loader 就地提交。 -App-boot 负责 profile 解析和 patch 优先级规则。HMR 读取启动器提供的纯数据 `profileContext`,在初始化时注册 profile manifest 和两份用户 patch 的监听,并等待应用就绪后处理更改。销毁 HMR 时会关闭监听器并取消等待启动的重载。HMR 也负责模块缓存替换和重载调度。配置监听器在当前事务上下文之外启动,使后续通知可以进入队列。 +App-boot 负责 profile 解析和 patch 优先级规则。HMR 读取启动器提供的纯数据 `profileContext`,在初始化时注册 profile manifest 和两份用户 patch 的监听,并等待应用就绪后处理更改。销毁 HMR 时会关闭监听器并取消等待启动的重载。配置监听器在当前事务上下文之外启动,使后续通知可以进入队列。 + +`node_modules` 之外的 `package.json` 变化使该包的配置缓存失效。仅作为配置的 manifest 不触发模块重载;作为 JSON 模块导入的 manifest 保留依赖驱动重载,位于宿主依赖图中的 manifest 保留宿主重载。专属配置监听保留原有归属。新包入口在 Loader entry 重启时生效。 + +Loader entry 保留原始导入结果。HMR 为每个 entry 名称与配置树 base URL 保留所有不同的已导入命名空间;尚未初始化的 entry 不会覆盖其他 entry 的命名空间。HMR 将这些对象与缓存中 ModuleJob 的模块命名空间按对象身份匹配,不再重新解析 entry 名称。没有已导入命名空间的名称和 `cordis:` 内置入口仍按名称解析。因此 `exports` 或 `main` 改变后,源码重载仍使用已加载模块。 + +共享同一运行时的 entry 一起替换,每个 entry 使用其自身模块对应的实现。同一模块的重复 entry 仍保有各自的插件实例。替换失败时,HMR 清理已部分激活的替换实例,并恢复原来的模块和插件实现。重载成功后更新所有匹配的 entry 记录,包括已停用的 entry;替换失败时这些记录保持不变。从未加载过的 entry 仍保持未初始化状态。 + +[package-manifest.ts](src/package-manifest.ts) 封装 package 读取、scope、type 和最近 manifest 查询所需的 Node internal 接口。每个 HMR 实例拥有自己的配置缓存,销毁时恢复它安装的 hook。manifest 失效会清空 ESM `ResolveCache` 和 CommonJS `_pathCache`:它们都不记录查询过的全部 manifest,入口也可能指向包目录之外。CommonJS 加载先用 Node 默认 resolver 解析请求,再进入原生 loader,绕过旧请求别名并保留已求值模块。同步 resolve hook 收到的是解析后的文件名。普通模块替换仍由 HMR 独立处理。 被监听模块的路径沿用 Node ESM 解析所用的 `realpathSync()` 表示,包括 Windows 短目录名,使文件事件与模块缓存匹配。 @@ -86,7 +94,11 @@ App-boot 负责 profile 解析和 patch 优先级规则。HMR 读取启动器提 - 模块替换需要 Node loader 内部接口。框架依赖变化调用宿主提供的 `loader.exit()` 钩子;HMR 本身不重启进程。 -- 通过插件管理器替换已安装包版本仍需要重启。浏览器 Client 模块图保留独立的浏览器侧加载机制。 +- 通过插件管理器替换已安装包版本仍需要重启;`node_modules` 之下的 manifest 保留 Node 缓存的配置。浏览器 Client 模块图保留独立的浏览器侧加载机制。 +- TSX loader 和普通应用自行创建的 Worker 不在包缓存刷新支持范围内。 +- CommonJS 请求必须能由 Node 默认 resolver 解析。同步 hook 可以像 Desktop 的 Office resolver 一样处理解析后的文件名;依赖原始请求名或引入虚拟请求的 hook 不受支持。内置模块保留 Node 原有的加载行为。 +- 后续工作:跨 HMR 销毁和替换保留包配置失效状态。恢复原生 reader 后,其旧配置缓存可能重新可见。 +- 如果共享运行时的替换模块具有不同的插件回调,没有 Loader entry 的实例就无法确定应使用哪一个。HMR 会报告歧义错误并回滚此次重载。 - `watchConfig()` 在 Chokidar 报告就绪时 resolve。darwin 上 libuv 随后才在自己的线程启动 FSEvents 流,因此注册后数毫秒内落地的写入要等到该目录的下一个事件才会被报告;启动之后的编辑不受影响。 ### 开发备注 diff --git a/packages/boot/hmr/package.json b/packages/boot/hmr/package.json index 5e61624165..d957e864ba 100644 --- a/packages/boot/hmr/package.json +++ b/packages/boot/hmr/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hmr", "description": "Coordinated module and profile configuration hot reload", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -38,13 +38,14 @@ "dependencies": { "@babel/code-frame": "^7.29.0", "chokidar": "^4.0.3", - "picomatch": "^4.0.3", + "node-addon-require-builtin": "^0.1.6", + "picomatch": "^4.0.7", "@deepseek-ai/schemastery": "workspace:~" }, "devDependencies": { "@types/babel__code-frame": "^7.27.0", - "@types/picomatch": "^3.0.2", - "esbuild": "^0.28.1", + "@types/picomatch": "^4.0.3", + "esbuild": "^0.28.2", "@deepseek-ai/cordis-plugin-timer": "workspace:~", "@deepseek-ai/cordis": "workspace:~", "@deepseek-ai/cordis-plugin-loader": "workspace:~", diff --git a/packages/boot/hmr/src/index.ts b/packages/boot/hmr/src/index.ts index 045890f022..cad957dbda 100644 --- a/packages/boot/hmr/src/index.ts +++ b/packages/boot/hmr/src/index.ts @@ -1,15 +1,17 @@ /** Serialized module and profile-configuration reloads. */ +import assert from 'node:assert/strict' import { AsyncLocalStorage } from 'node:async_hooks' import { watchConfig as watchExactConfig } from './watch-config.ts' -import { Context, Inject, Service, type Plugin } from '@deepseek-ai/cordis' -import { ModuleLoader, type ModuleJob, type ResolveResult } from '@deepseek-ai/cordis-plugin-loader' +import { Context, Inject, Service, type Fiber, type Plugin } from '@deepseek-ai/cordis' +import type { ModuleLoader, ModuleJob, ResolveResult } from '@deepseek-ai/cordis-plugin-loader' import type { Include } from '@deepseek-ai/cordis-plugin-include' import { FSWatcher, watch, type ChokidarOptions } from 'chokidar' -import { basename, dirname, join, relative, resolve } from 'node:path' +import { basename, dirname, join, relative, resolve, sep } from 'node:path' import { readFileSync, realpathSync } from 'node:fs' import { readProfileManifest, readProfilePatches, reconcileProfilePatches, PROFILE_PATCH_FILENAME } from '@deepseek-ai/dsh-app-boot' import type {} from '@deepseek-ai/dsh-cmdline' import { handleError } from './error.ts' +import { PackageManifests } from './package-manifest.ts' import type {} from '@deepseek-ai/cordis-plugin-timer' import { fileURLToPath, pathToFileURL } from 'node:url' import { createRequire } from 'node:module' @@ -79,9 +81,129 @@ async function loadDependencies(job: ModuleJob, ignored = new Set()) { /** Module location and runtime retained during a replacement. */ export interface Reload { filename: string + /** Original namespaces and URLs of all entry modules participating in this runtime replacement. */ + modules: ReloadModules runtime?: Plugin.Runtime | undefined } +/** Entry names with distinct namespaces; iteration falls back to undefined only for names without a loaded namespace. */ +class EntryNamespaces { + private readonly names = new Map>() + + constructor(private internal: ModuleLoader, private moduleUrls: ReadonlyMap) {} + + static getModuleUrls(internal: ModuleLoader): Map { + const moduleUrls = new Map() + for (const url of internal.loadCache.keys()) { + const job = internal.loadCache.get(url) + if (!job?.module) continue + try { + moduleUrls.set(job.module.getNamespace(), url) + } catch (_error) { + // Node also caches unfinished jobs whose namespace is not available yet. + } + } + return moduleUrls + } + + add(name: string, moduleNamespace: unknown): this { + const namespaces = this.names.get(name) ?? new Set() + namespaces.add(moduleNamespace) + this.names.set(name, namespaces) + return this + } + + *[Symbol.iterator](): Generator<[name: string, moduleNamespace: unknown]> { + for (const [name, namespaces] of this.names) { + for (const moduleNamespace of namespaces) { + if (moduleNamespace === undefined && namespaces.size > 1) continue + yield [name, moduleNamespace] + } + } + } + + async resolve(name: string, baseUrl: string, moduleNamespace: unknown): Promise> { + if (moduleNamespace === undefined || name.startsWith('cordis:')) { + switch (this.internal.version) { + case 'v1': return await this.internal.resolve(name, baseUrl, {}) + case 'v2': return this.internal.resolveSync(baseUrl, { specifier: name, attributes: {} }) + } + } + const url = this.moduleUrls.get(moduleNamespace) + if (url === undefined) throw new Error(`HMR cannot locate the loaded module for ${name} from ${baseUrl}`) + return { url } + } +} + +type ReloadFiber = { + fiber: Fiber + entry: Fiber['entry'] + config: unknown + moduleNamespace: unknown +} + +type ReloadModule = { filename: string; moduleNamespace: unknown; plugin: Plugin } + +/** Entry modules belonging to one runtime, keyed by their original namespaces across replacement. */ +class ReloadModules { + private readonly modules = new Map() + + static include(reloads: ReadonlyMap, plugin: Plugin, job: ModuleJob, runtime: Plugin.Runtime | undefined): boolean { + let info = reloads.get(plugin) + if (info === undefined && runtime) info = [...reloads.values()].find(info => info.runtime === runtime) + if (info === undefined) return false + info.modules.add(job, plugin) + return true + } + + add(job: ModuleJob, plugin: Plugin): this { + assert(job.module, `HMR pending module is missing: ${job.url}`) + const moduleNamespace: unknown = job.module.getNamespace() + this.modules.set(moduleNamespace, { filename: job.url, moduleNamespace, plugin }) + return this + } + + async importRemaining(loader: Context['loader'], getOuterStack: () => string[], primary: ReloadModule): Promise { + const replacements = new ReloadModules() + for (const [originalNamespace, { filename }] of this.modules) { + if (filename === primary.filename) { + replacements.modules.set(originalNamespace, primary) + continue + } + const moduleNamespace: unknown = await loader.import(filename, getOuterStack) + const plugin = loader.unwrapExports(moduleNamespace) as Plugin + replacements.modules.set(originalNamespace, { filename, moduleNamespace, plugin }) + } + return replacements + } + + *getActiveImplementations(ctx: Context, plugin: Plugin, fibers: readonly ReloadFiber[]): Generator<[ReloadFiber, Plugin]> { + const callbacks = new Set([...this.modules.values()].map(value => ctx.registry.resolve(value.plugin))) + const implementations = fibers.map((previousFiber) => { + if (previousFiber.entry === undefined) return [previousFiber, plugin] as const + const replacement = this.modules.get(previousFiber.moduleNamespace) + const implementation = replacement ? replacement.plugin : ctx.loader.unwrapExports(previousFiber.moduleNamespace) as Plugin + callbacks.add(ctx.registry.resolve(implementation)) + return [previousFiber, implementation] as const + }) + for (const [previousFiber, implementation] of implementations) { + if (previousFiber.fiber.parent.fiber.uid === null) continue + if (previousFiber.entry === undefined && callbacks.size > 1) { + throw new Error('HMR replacement is ambiguous for a plugin instance without a Loader entry') + } + yield [previousFiber, implementation] + } + } + + updateEntries(loader: Context['loader']): void { + for (const [originalNamespace, { moduleNamespace }] of this.modules) { + for (const entry of loader.entries()) { + if (entry.moduleNamespace === originalNamespace) entry.moduleNamespace = moduleNamespace + } + } + } +} + /** Hot reload service with Cordis-compatible module configuration and events. */ @Inject('loader') @Inject('timer') @@ -131,6 +253,7 @@ class Hmr extends Service { private applicationReady: Promise = Promise.resolve(true) private closing = false private readonly configPaths = new Set() + private readonly manifests = new PackageManifests() /** Serialize a caller-owned mutation with all automatic reload paths. * @param operation Work that must not overlap module or configuration replacement. @@ -185,22 +308,13 @@ class Hmr extends Service { this.baseDir = fileURLToPath(new URL(config.base || '.', ctx.baseUrl)) } - /** - * Resolve a module specifier to a URL, compatible with Node 22-24. - */ - private async _resolve(specifier: string, parentURL: string, attrs: ImportAttributes): Promise { - switch (this.internal.version) { - case 'v1': return await this.internal.resolve(specifier, parentURL, attrs) - case 'v2': return this.internal.resolveSync(parentURL, { specifier, attributes: attrs }) - } - } - async* [Service.init](): AsyncGenerator<() => Promise, void, unknown> { yield async () => { this.closing = true await this.watcher?.close() // A configuration reload may remove its own HMR entry. if (!this.executing.getStore()) await this.operations + this.manifests.dispose() } const profile = this.ownerContext.get('profileContext') @@ -276,15 +390,20 @@ class Hmr extends Service { const filename = canonicalPath(resolve(watchBaseDir, path)) const configuredFilename = resolve(this.baseDir, path) if (this.configPaths.has(filename) || this.configPaths.has(configuredFilename)) continue + const isManifest = basename(filename) === 'package.json' && !filename.includes(`${sep}node_modules${sep}`) + if (isManifest) { + this.manifests.invalidate(filename) + } const url = pathToFileURL(filename).href if (this.externals.has(url)) { fullReload = true continue } - if (this.internal.loadCache.has(url)) { + if (this.internal.loadCache.has(url) || this.internal.loadCache.has(url, 'json')) { this.stashed.add(url) continue } + if (isManifest) continue const include = [...loader.entries()].map(entry => entry.subtree as Include | undefined) .find(tree => tree?.filename === filename || tree?.filename === configuredFilename) if (include !== undefined) includes.add(include) @@ -408,23 +527,24 @@ class Hmr extends Service { const pending = new Map() const reloads = new Map() + const moduleUrls = EntryNamespaces.getModuleUrls(this.internal) // Build a map of plugin names per config tree URL. // Plugin entry files are treated as atomic reload units. - const nameMap = new Map>() + const nameMap = new Map() for (const entry of this.ctx.loader.entries()) { const baseUrl = entry.parent.tree.ctx.baseUrl if (baseUrl === undefined) throw new Error('HMR entry tree has no base URL') - const names = nameMap.get(baseUrl) ?? new Set() - names.add(entry.options.name) + const names = nameMap.get(baseUrl) ?? new EntryNamespaces(this.internal, moduleUrls) + names.add(entry.options.name, entry.moduleNamespace) nameMap.set(baseUrl, names) } - // Resolve each plugin name to its file URL and check if it needs reload + // Find each plugin's loaded URL and check if it needs reload. for (const [baseUrl, names] of nameMap) { - for (const name of names) { + for (const [name, moduleNamespace] of names) { try { - const { url } = await this._resolve(name, baseUrl, {}) + const { url } = await names.resolve(name, baseUrl, moduleNamespace) if (this.declined.has(url)) continue const job = this.internal.loadCache.get(url) const plugin = this.ctx.loader.unwrapExports(job?.module?.getNamespace()) as Plugin | undefined @@ -443,15 +563,27 @@ class Hmr extends Service { const dependencies = [...await loadDependencies(job, this.declined)] this.declined.add(job.url) + if (dependencies.length === 0) { + pending.delete(job) + continue + } if (!dependencies.some(dep => this.accepted.has(dep))) continue dependencies.forEach(dep => this.accepted.add(dep)) + const runtime = this.ctx.registry.get(plugin) + if (ReloadModules.include(reloads, plugin, job, runtime)) continue reloads.set(plugin, { filename: job.url, - runtime: this.ctx.registry.get(plugin), + runtime, + modules: new ReloadModules().add(job, plugin), }) } + // Re-export roots of a replaced runtime must not retain bindings to its old modules. + for (const [job, plugin] of pending) { + if (ReloadModules.include(reloads, plugin, job, this.ctx.registry.get(plugin))) this.accepted.add(job.url) + } + /** * Clear module caches for all accepted files before re-importing. * @@ -502,14 +634,23 @@ class Hmr extends Service { fibers: [...info.runtime?.fibers ?? []].map((fiber) => { const entry = fiber.entry?.fiber?.uid === fiber.uid ? fiber.entry : undefined const config: unknown = entry === undefined ? fiber._config : entry.options.config - return { fiber, entry, config } + return { fiber, entry, config, moduleNamespace: entry?.moduleNamespace } }), })) - const attempts: Array<(typeof generations)[number] & { replacement: Plugin }> = [] + type ReloadAttempt = (typeof generations)[number] & { + replacement: Plugin + replacements: ReloadModules + activated: Fiber[] + } + const attempts: ReloadAttempt[] = [] try { for (const generation of generations) { - const replacement = this.ctx.loader.unwrapExports(await this.ctx.loader.import(generation.filename, this.getOuterStack)) as Plugin - attempts.push({ ...generation, replacement }) + const moduleNamespace: unknown = await this.ctx.loader.import(generation.filename, this.getOuterStack) + const replacement = this.ctx.loader.unwrapExports(moduleNamespace) as Plugin + const replacements = await generation.modules.importRemaining(this.ctx.loader, this.getOuterStack, { + filename: generation.filename, moduleNamespace, plugin: replacement, + }) + attempts.push({ ...generation, replacement, replacements, activated: [] }) } } catch (e) { handleError(this.ctx, e) @@ -517,11 +658,14 @@ class Hmr extends Service { throw e } - const reload = async (plugin: Plugin, fibers: (typeof generations)[number]['fibers']) => { - const activated = [] - for (const previousFiber of fibers) { - if (previousFiber.fiber.parent.fiber.uid === null) continue - const fiber = previousFiber.fiber.parent.registry.plugin(plugin, previousFiber.config, this.getOuterStack).ctx.fiber + const reload = async ( + plugin: Plugin, + fibers: (typeof generations)[number]['fibers'], + replacements = new ReloadModules(), + activated: Fiber[] = [], + ) => { + for (const [previousFiber, implementation] of replacements.getActiveImplementations(this.ctx, plugin, fibers)) { + const fiber = previousFiber.fiber.parent.registry.plugin(implementation, previousFiber.config, this.getOuterStack).ctx.fiber if (previousFiber.entry !== undefined) { fiber.entry = previousFiber.entry previousFiber.entry.fiber = fiber @@ -533,7 +677,7 @@ class Hmr extends Service { const removed = new Set() try { - for (const { previous: plugin, replacement, filename, runtime, fibers } of attempts) { + for (const { previous: plugin, replacement, filename, runtime, fibers, replacements, activated } of attempts) { if (!runtime) continue const path = relative(this.baseDir, fileURLToPath(filename)) @@ -547,7 +691,7 @@ class Hmr extends Service { } try { - await reload(replacement, fibers) + await reload(replacement, fibers, replacements, activated) this.ctx.logger.info('reload plugin at %C', path) } catch (err) { this.ctx.logger.warn('failed to reload plugin at %C', path) @@ -558,14 +702,17 @@ class Hmr extends Service { } catch (error) { // Restore caches and re-register old plugins after a replacement failure. rollback() - for (const { previous: plugin, replacement, fibers } of attempts) { + for (const { previous: plugin, fibers, activated } of attempts) { if (!removed.has(plugin)) continue try { - const replacementRuntime = this.ctx.registry.get(replacement) - const replacementFibers = [...replacementRuntime?.fibers ?? []] - this.ctx.registry.delete(replacement) - // Failed startup errors remain on fibers after their teardown finishes. - await Promise.allSettled(replacementFibers.map(fiber => fiber.await())) + for (const { runtime } of activated) { + if (runtime && this.ctx.registry.get(runtime.callback) === runtime) { + const replacementFibers = [...runtime.fibers] + this.ctx.registry.delete(runtime.callback) + // Failed startup errors remain on fibers after their teardown finishes. + await Promise.allSettled(replacementFibers.map(fiber => fiber.await())) + } + } await reload(plugin, fibers) } catch (err) { this.ctx.logger.warn(err) @@ -575,6 +722,9 @@ class Hmr extends Service { } await this.ctx.loader.await() + for (const { replacements } of attempts) { + replacements.updateEntries(this.ctx.loader) + } this.ctx.emit('hmr/reload', reloads) this.stashed = new Set() } diff --git a/packages/boot/hmr/src/package-manifest.ts b/packages/boot/hmr/src/package-manifest.ts new file mode 100644 index 0000000000..0c41fbb42c --- /dev/null +++ b/packages/boot/hmr/src/package-manifest.ts @@ -0,0 +1,330 @@ +/** Expire Node's cached package configuration for package directories whose manifest changed. */ +import { isUtf8 } from 'node:buffer' +import { readFileSync, realpathSync } from 'node:fs' +import { createRequire, isBuiltin } from 'node:module' +import { basename, dirname, join, sep, toNamespacedPath } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +/** Package fields Node's resolver consumes; exports and imports hold JSON values. */ +interface PackageConfig { + name?: string + main?: string + type: string + exports?: unknown + imports?: unknown +} + +/** Serialized fields returned by the native modules binding: name, main, type, imports, exports, manifest path. */ +type PackageTuple = [string | undefined, string | undefined, string, string | undefined, string | undefined, string] + +/** + * Methods of `internalBinding('modules')` that read package configuration through Node's native cache. Node's + * package reader, CommonJS loader, and ESM resolver and format detection look them up at each call. + */ +interface PackageBinding { + readPackageJSON(this: void, path: string, isEsm?: boolean, base?: string, specifier?: string): PackageTuple | undefined + getPackageScopeConfig(this: void, url: string): PackageTuple | string + getPackageType(this: void, url: string): string | undefined +} + +/** + * `internal/modules/package_json_reader` export that keeps its own nearest-manifest cache; the CommonJS loader calls it + * through the module object, so it is the only caller of the binding's nearest-manifest lookup. + */ +interface PackageReader { + getNearestParentPackageJSON(this: void, path: string): { data: PackageConfig; exists: boolean; path: string } | undefined +} + +/** Node's ESM resolution cache: a Map from parent URL to the serialized request keys resolved from it. */ +interface ResolveCacheClass { + prototype: { get: (this: Map, key: string, parent: string) => unknown } +} + +interface CommonJsModule { + _pathCache: Record + _resolveFilename(request: string, parent: NodeJS.Module | null | undefined, isMain?: boolean): string + _load(request: string, parent: NodeJS.Module | null | undefined, isMain?: boolean, ...options: unknown[]): unknown +} + +interface NativeAccess { + binding: PackageBinding + reader: PackageReader + ResolveCache: ResolveCacheClass + cjs: CommonJsModule + esmLoader: { resolveSync(...args: unknown[]): unknown } +} + +/** Snapshot own methods; each disposer restores its method only while its replacement is still installed. */ +function createReplaceHelper unknown }>(target: T) { + const rawImpl = { ...target } + return { + rawImpl, + replaceMethod(key: K, replacement: T[K]): () => void { + target[key] = replacement + return () => { if (target[key] === replacement) target[key] = rawImpl[key] } + }, + } +} + +/** Same lookup as the vendored Loader's `requireInternal`: `--expose-internals` first, then the builtin addon. */ +function requireInternal(id: string): unknown { + const require = createRequire(import.meta.url) + if (process.execArgv.includes('--expose-internals')) { + try { + return require(id) + } catch (_error) { + // Some internals, such as internal/bootstrap/realm, stay private under --expose-internals. + } + } + try { + return (require('node-addon-require-builtin') as { requireBuiltin(id: string): unknown }).requireBuiltin(id) + } catch (_error) { + // Without the addon, the caller reports the missing interface. + } +} + +function loadNodeInternals(): NativeAccess { + const realm = requireInternal('internal/bootstrap/realm') as { internalBinding(id: 'modules'): PackageBinding } + const esm = requireInternal('internal/modules/esm/loader') as { + getOrInitializeCascadedLoader(): NativeAccess['esmLoader'] + } + return { + binding: realm.internalBinding('modules'), + reader: requireInternal('internal/modules/package_json_reader') as PackageReader, + ResolveCache: (requireInternal('internal/modules/esm/module_map') as { ResolveCache: ResolveCacheClass }).ResolveCache, + cjs: (requireInternal('internal/modules/cjs/loader') as { Module: CommonJsModule }).Module, + esmLoader: esm.getOrInitializeCascadedLoader(), + } +} + +/** + * Read the ESM loader's resolution cache by observing one lookup through its prototype. + * The prototype method is restored before this returns. + */ +function captureEsmResolveCache(native: NativeAccess, parentURL: string): Map | undefined { + const prototype = native.ResolveCache.prototype + const original = prototype.get + const seen = new Set>() + prototype.get = function (this: Map, key: string, parent: string): unknown { + seen.add(this) + return Reflect.apply(original, this, [key, parent]) + } + try { + const loader = native.esmLoader + /* v8 ignore else -- CI coverage runs the Node 24 loader; the Node 22 matrix exercises the v1 signature */ + if ('getOrCreateModuleJob' in loader) loader.resolveSync(parentURL, { specifier: 'node:path', attributes: {} }) + else loader.resolveSync('node:path', parentURL, {}) + } finally { + prototype.get = original + } + return [...seen][0] +} + +interface Configuration { + data: PackageConfig + serialized: PackageTuple +} + +interface ImportContext { + base: string + specifier: string | undefined +} + +/** Package directories whose manifests are read from disk instead of Node's native cache. */ +export class PackageManifests { + private readonly directories = new Set() + private readonly configurations = new Map() + /** Real directory of each directory a lookup reached; linked consumers name a package by its link path. */ + private readonly realDirectories = new Map() + private native: NativeAccess | undefined + private rawBinding!: PackageBinding + private rawReader!: PackageReader + private rawCommonJs!: Pick + private readonly restorers: Array<() => void> = [] + + /** + * Expire cached configuration for one package directory. Loaded modules stay evaluated; later resolutions and + * format checks read the manifest on disk. + * @param manifest - absolute path of the changed `package.json`. + */ + invalidate(manifest: string): void { + const native = this.installPackageHooks() + const directory = dirname(manifest) + this.directories.add(directory) + this.configurations.clear() + this.realDirectories.clear() + // A package's entry may resolve outside its directory; cache keys do not record the manifests they consulted. + for (const key of Object.keys(native.cjs._pathCache)) Reflect.deleteProperty(native.cjs._pathCache, key) + const directoryURL = pathToFileURL(directory).href + '/' + const cache = captureEsmResolveCache(native, directoryURL) + /* v8 ignore else -- asynchronous loader hooks own a separate cache; loader-thread invalidation is deferred */ + if (cache !== undefined) Map.prototype.clear.call(cache) + } + + /** Restore every replaced Node method this instance installed. */ + dispose(): void { + for (const restore of this.restorers.splice(0).reverse()) restore() + this.native = undefined + this.realDirectories.clear() + } + + private isPathWithinDirectory(path: string, directory: string): boolean { + return path === directory || path.startsWith(join(directory, sep)) + } + + private isPathInInvalidatedDirectory(path: string): boolean { + const parent = dirname(path) + let real = this.realDirectories.get(parent) + if (real === undefined) { + try { real = realpathSync(parent) } catch (_error) { + // A missing directory keeps its configured spelling; it has no manifest to read through a link. + real = parent + } + this.realDirectories.set(parent, real) + } + const target = join(real, basename(path)) + for (const directory of this.directories) if (this.isPathWithinDirectory(target, directory)) return true + return false + } + + private isUrlInInvalidatedDirectory(url: string): boolean { + try { return this.isPathInInvalidatedDirectory(fileURLToPath(url)) } catch (_error) { + // Only file URLs can name an invalidated package directory. + return false + } + } + + private installPackageHooks(): NativeAccess { + if (this.native !== undefined) return this.native + const native = loadNodeInternals() + this.native = native + const binding = createReplaceHelper(native.binding) + const reader = createReplaceHelper(native.reader) + const commonJs = createReplaceHelper>(native.cjs) + this.rawBinding = binding.rawImpl + this.rawReader = reader.rawImpl + this.rawCommonJs = commonJs.rawImpl + this.restorers.push( + binding.replaceMethod('readPackageJSON', this.hookBindingReadPackageJSON.bind(this)), + binding.replaceMethod('getPackageScopeConfig', this.hookBindingGetPackageScopeConfig.bind(this)), + binding.replaceMethod('getPackageType', this.hookBindingGetPackageType.bind(this)), + reader.replaceMethod('getNearestParentPackageJSON', this.hookReaderGetNearestParentPackageJSON.bind(this)), + commonJs.replaceMethod('_load', this.hookCommonJsLoad.bind(this)), + ) + return native + } + + private hookCommonJsLoad(...[request, parent, isMain, ...options]: Parameters) { + const native = this.installPackageHooks() + // Absolute filenames bypass Node's private request cache without evicting evaluated modules. + const filename = request.startsWith('node:') || isBuiltin(request) + ? request : native.cjs._resolveFilename(request, parent, isMain) + return this.rawCommonJs._load.call(native.cjs, filename, parent, isMain, ...options) + } + + private hookBindingReadPackageJSON(path: string, isEsm?: boolean, base?: string, specifier?: string) { + return this.isPathInInvalidatedDirectory(path) + ? this.readCachedPackageConfig( + path, + isEsm && base !== undefined ? { base, specifier } : undefined, + )?.serialized + : this.rawBinding.readPackageJSON(path, isEsm, base, specifier) + } + + private hookBindingGetPackageScopeConfig(url: string) { + if (!this.isUrlInInvalidatedDirectory(url)) return this.rawBinding.getPackageScopeConfig(url) + const found = this.findNearestPackageConfig(fileURLToPath(url), true) + if (typeof found === 'string') return this.rawBinding.getPackageScopeConfig(pathToFileURL(found).href) + return found.configuration?.serialized ?? found.path + } + + private hookBindingGetPackageType(url: string) { + if (!this.isUrlInInvalidatedDirectory(url)) return this.rawBinding.getPackageType(url) + const found = this.findNearestPackageConfig(fileURLToPath(url), true) + return typeof found === 'string' + ? this.rawBinding.getPackageType(pathToFileURL(found).href) + : found.configuration?.data.type + } + + private hookReaderGetNearestParentPackageJSON(path: string) { + if (!this.isPathInInvalidatedDirectory(path)) return this.rawReader.getNearestParentPackageJSON(path) + const found = this.findNearestPackageConfig(path, false) + if (typeof found === 'string') return this.rawReader.getNearestParentPackageJSON(found) + // A lookup that stops without a manifest keeps the running Node version's own absent result. + if (found.configuration === undefined) return this.rawReader.getNearestParentPackageJSON(path) + return { data: found.configuration.data, exists: true, path: found.path } + } + + private readCachedPackageConfig(path: string, context?: ImportContext): Configuration | undefined { + if (!this.configurations.has(path)) this.configurations.set(path, this.readPackageConfigFromDisk(path, context)) + return this.configurations.get(path) + } + + /** A string names the first manifest outside the invalidated directories; Node answers for it. */ + private findNearestPackageConfig(path: string, includeRoot: boolean): { path: string; configuration?: Configuration } | string { + let directory = dirname(path) + while (true) { + const manifest = join(directory, 'package.json') + const parent = dirname(directory) + if (basename(directory) === 'node_modules' || (!includeRoot && parent === directory)) return { path: manifest } + if (!this.isPathInInvalidatedDirectory(manifest)) return manifest + const configuration = this.readCachedPackageConfig(manifest) + if (configuration !== undefined) return { path: toNamespacedPath(manifest), configuration } + /* v8 ignore next -- root-scope queries run in an isolated child to contain non-termination */ + if (parent === directory) return { path: manifest } + directory = parent + } + } + + /** @param context - optional ESM import details used only when reporting invalid package configuration. */ + private readPackageConfigFromDisk(path: string, context?: ImportContext): Configuration | undefined { + let bytes: Buffer + try { bytes = readFileSync(path) } catch (_error) { + // Node reports a missing manifest as absent configuration. + return undefined + } + const throwInvalidPackageConfig = (): never => { + // The native reader throws a plain Error carrying only this code. + const base = context?.base + const importing = base === undefined ? '' + : ` while importing "${context?.specifier}" from ${base.startsWith('file:') ? fileURLToPath(base) : base}` + throw Object.assign(new Error(`Invalid package config ${toNamespacedPath(path)}${importing}.`), { code: 'ERR_INVALID_PACKAGE_CONFIG' }) + } + if (!isUtf8(bytes)) return throwInvalidPackageConfig() + const source = bytes.toString('utf8') + let parsed: unknown + try { parsed = JSON.parse(source.charCodeAt(0) === 0xfeff ? source.slice(1) : source) } catch (_error) { + return throwInvalidPackageConfig() + } + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return throwInvalidPackageConfig() + const fields = parsed as Record + if (fields.name !== undefined && typeof fields.name !== 'string') return throwInvalidPackageConfig() + if (fields.type !== undefined && typeof fields.type !== 'string') return throwInvalidPackageConfig() + for (const key of ['name', 'type', 'exports', 'imports']) { + const value = fields[key] + if (typeof value === 'string' && !value.isWellFormed()) return throwInvalidPackageConfig() + } + const normalizePackageJsonField = (value: unknown): unknown => + value !== null && (typeof value === 'string' || typeof value === 'object') ? value : undefined + const data: PackageConfig = { + ...typeof fields.name === 'string' ? { name: fields.name } : {}, + ...typeof fields.main === 'string' && fields.main.isWellFormed() ? { main: fields.main } : {}, + type: fields.type === 'module' || fields.type === 'commonjs' ? fields.type : 'none', + exports: normalizePackageJsonField(fields.exports), + imports: normalizePackageJsonField(fields.imports), + } + const serializePackageJsonField = (value: unknown): string | undefined => + value === undefined ? undefined : typeof value === 'string' ? value : JSON.stringify(value) + return { + data, + serialized: [ + data.name, + data.main, + data.type, + serializePackageJsonField(data.imports), + serializePackageJsonField(data.exports), + toNamespacedPath(path), + ], + } + } +} diff --git a/packages/boot/hmr/src/watch-config.ts b/packages/boot/hmr/src/watch-config.ts index 6a03be2d76..6c62610dec 100644 --- a/packages/boot/hmr/src/watch-config.ts +++ b/packages/boot/hmr/src/watch-config.ts @@ -73,12 +73,24 @@ export async function watchConfig( watcher.on('unlink', onChange) const ready = Promise.withResolvers() let pending = true + let disposed = false watcher.once('ready', () => { pending = false; ready.resolve() }) - watcher.on('error', (error) => { + const onError = (error: unknown): void => { + // A watcher error after disposal is not actionable: this registration's + // path is gone and no refresh will run. + if (disposed) return if (pending) { pending = false; ready.reject(error) } else { ctx.logger.warn(error) } - }) + } + watcher.on('error', onError) const dispose = async () => { - await watcher.close() + disposed = true + // `close()` drops every listener but leaves a scheduled write-settle poll; + // its straggler stats a file this teardown is deleting, which Windows + // reports as EPERM, and an 'error' emission with no listener is rethrown + // as an uncaught exception. Re-attach across the close. + const closing = watcher.close() + watcher.on('error', onError) + await closing paths.delete(target.filename) if (!inTransaction()) await running } diff --git a/packages/boot/hmr/tests/fixtures/package-root-scope.mjs b/packages/boot/hmr/tests/fixtures/package-root-scope.mjs new file mode 100644 index 0000000000..76e637424c --- /dev/null +++ b/packages/boot/hmr/tests/fixtures/package-root-scope.mjs @@ -0,0 +1,22 @@ +/** Isolate root-scope traversal; the filesystem root is read but never modified. */ +import assert from 'node:assert/strict' +import { createRequire } from 'node:module' +import { join, parse } from 'node:path' +import { pathToFileURL } from 'node:url' +import { PackageManifests } from '../../src/package-manifest.ts' + +const [root] = process.argv.slice(2) +const reader = createRequire(import.meta.url)('internal/modules/package_json_reader') +const source = join(root, 'missing', 'entry.js') +const url = pathToFileURL(source).href +const expectedScope = reader.getPackageScopeConfig(url) +const expectedNearest = reader.getNearestParentPackageJSON(source) +const manifests = new PackageManifests() +try { + await manifests.invalidate(join(parse(root).root, 'package.json')) + assert.deepEqual(reader.getPackageScopeConfig(url), expectedScope) + assert.deepEqual(reader.getNearestParentPackageJSON(source), expectedNearest) + process.stdout.write('scope traversal completed\n') +} finally { + manifests.dispose() +} diff --git a/packages/boot/hmr/tests/modules.spec.ts b/packages/boot/hmr/tests/modules.spec.ts index 93b96a007c..5cf710ea07 100644 --- a/packages/boot/hmr/tests/modules.spec.ts +++ b/packages/boot/hmr/tests/modules.spec.ts @@ -58,6 +58,42 @@ async function fixture(version: 'v1' | 'v2' = 'v2') { return { ctx, cache, imports, imported, reload, module, url, resolve } } +it('does not reactivate a runtime removed during dependency traversal', async () => { + const { ctx, module, imports, imported, reload } = await fixture() + const dependency = module('dependency.mjs') + const original = { apply() {} } + const replacement = { apply: vi.fn() } + const job = module('plugin.mjs', original, [dependency]) + const entry = ctx.loader.resolve(await ctx.loader.create({ name: job.url })) + await ctx.loader.await() + const fiber = entry.fiber! + const reached = Promise.withResolvers() + const linked = Promise.withResolvers() + Object.defineProperty(job, 'linked', { configurable: true, get() { + reached.resolve(undefined) + return linked.promise + } }) + onTestFinished(() => { linked.resolve([dependency]) }) + imports.set(job.url, replacement) + imported.mockClear() + reload.stashed.add(dependency.url) + const operation = reload.partialReload() + try { + await reached.promise + ctx.registry.delete(original) + await fiber.await() + expect(ctx.registry.get(original)).toBeUndefined() + linked.resolve([dependency]) + await operation + expect(imported).toHaveBeenCalledWith(job.url, expect.any(Function)) + expect(replacement.apply).not.toHaveBeenCalled() + expect(ctx.registry.get(replacement)).toBeUndefined() + } finally { + linked.resolve([dependency]) + await operation + } +}) + it.each(['v1', 'v2'] as const)('replaces a %s module and retains the latest Loader entry configuration', async (version) => { const { ctx, module, imports, reload } = await fixture(version) const mounted: string[] = [] @@ -166,6 +202,34 @@ it('does not replace a plugin whose dependencies have not changed', async () => expect(imported).not.toHaveBeenCalled() }) +it('keeps a node_modules entry cached when it shares a runtime with a replaced source entry', async () => { + const { ctx, module, imports, imported, cache, reload } = await fixture() + const original = { apply: vi.fn() } + const replacement = { apply: vi.fn() } + const source = module('source.mjs', original) + const installedNamespace = { apply: original.apply } + const installed = module('node_modules/addon/index.mjs', installedNamespace) + await ctx.loader.root.update([ + { id: 'source', name: source.url }, + { id: 'installed', name: installed.url }, + ]) + await ctx.loader.await() + expect(ctx.loader.resolve('source').fiber?.runtime).toBe(ctx.loader.resolve('installed').fiber?.runtime) + imports.set(source.url, replacement) + imported.mockClear() + reload.stashed.add(source.url) + + await reload.partialReload() + + expect(imported).toHaveBeenCalledExactlyOnceWith(source.url, expect.any(Function)) + expect(replacement.apply).toHaveBeenCalledOnce() + expect(cache.get(installed.url)).toBe(installed) + expect(ctx.loader.resolve('installed').moduleNamespace).toBe(installedNamespace) + expect(ctx.loader.resolve('installed').fiber?.runtime?.callback).toBe(original.apply) + expect(ctx.loader.resolve('source').fiber?.runtime?.callback).toBe(replacement.apply) + expect(reload.accepted).not.toContain(installed.url) +}) + it('replaces a plugin when a linked dependency changes, excluding framework modules', async () => { const { ctx, module, imports, reload } = await fixture() const dependency = module('dependency.mjs') @@ -237,14 +301,15 @@ it('leaves disposed child instances to their replacing parent', async () => { expect(entry.fiber?._config).toEqual({ value: 'entry' }) }) -it('invalidates a disabled module without activating it', async () => { - const { ctx, module, imports, reload } = await fixture() +it.each(['v1', 'v2'] as const)('invalidates a disabled %s module without activating it', async (version) => { + const { ctx, module, imports, resolve, reload } = await fixture(version) const job = module('disabled.mjs', { apply() {} }) const id = await ctx.loader.create({ name: job.url, disabled: true }) const apply = vi.fn() imports.set(job.url, { apply }) reload.stashed.add(job.url) await reload.partialReload() + expect(resolve.mock.calls).toEqual(version === 'v1' ? [[job.url, ctx.baseUrl, {}]] : [[job.url]]) expect(ctx.loader.resolve(id).fiber).toBeUndefined() expect(apply).not.toHaveBeenCalled() }) @@ -263,6 +328,45 @@ it('does not disturb an unchanged runtime after an earlier replacement fails', a expect(untouched).toHaveBeenCalledOnce() }) +it('cleans up a shared replacement runtime once when a later plugin activation fails', async () => { + const { ctx, module, imports, cache, reload } = await fixture() + const mounted: string[] = [] + const disposed: string[] = [] + const original = { apply(_ctx: Context, config: { label: string }) { mounted.push(config.label) } } + const replacement = { apply(ctx: Context, config: { label: string }) { + ctx.effect(() => () => { disposed.push(config.label) }) + } } + const first = module('shared.mjs', original) + const second = module('failing.mjs', { apply() {} }) + await ctx.loader.root.update([ + { id: 'one', name: first.url, config: { label: 'one' } }, + { id: 'two', name: first.url, config: { label: 'two' } }, + { id: 'failing', name: second.url }, + ]) + await ctx.loader.await() + imports.set(first.url, replacement) + imports.set(second.url, { apply() { throw new Error('later activation failed') } }) + const remove = vi.spyOn(ctx.registry, 'delete') + const event = vi.fn() + ctx.on('hmr/reload', event) + reload.stashed.add(first.url) + reload.stashed.add(second.url) + + await expect(reload.partialReload()).rejects.toThrow('later activation failed') + + expect(disposed.sort()).toEqual(['one', 'two']) + expect(mounted).toEqual(['one', 'two', 'one', 'two']) + expect(remove.mock.calls.filter(([plugin]) => plugin === replacement.apply)).toHaveLength(1) + expect(ctx.registry.get(replacement)).toBeUndefined() + const restored = ctx.registry.get(original) + expect([...restored!.fibers]).toHaveLength(2) + expect(ctx.loader.resolve('one').fiber?.runtime).toBe(restored) + expect(ctx.loader.resolve('two').fiber?.runtime).toBe(restored) + expect(cache.get(first.url)).toBe(first) + expect(cache.get(second.url)).toBe(second) + expect(event).not.toHaveBeenCalled() +}) + it('restores the prior plugin when the replacement does not export a plugin', async () => { const { ctx, module, imported, reload } = await fixture() const original = { apply: vi.fn() } @@ -292,7 +396,7 @@ it('ignores framework entries and reports unresolved entry modules', async () => const framework = module('framework.mjs', { apply() {} }) const uncached = module('uncached.mjs', { apply() {} }) await ctx.loader.create({ name: framework.url }) - await ctx.loader.create({ name: uncached.url }) + await ctx.loader.create({ name: uncached.url, disabled: true }) await ctx.loader.await() reload.externals.add(framework.url) resolve.mockImplementation((name) => { diff --git a/packages/boot/hmr/tests/package-manifest-dispatch.spec.ts b/packages/boot/hmr/tests/package-manifest-dispatch.spec.ts new file mode 100644 index 0000000000..3a4f518bf8 --- /dev/null +++ b/packages/boot/hmr/tests/package-manifest-dispatch.spec.ts @@ -0,0 +1,669 @@ +/** HMR dispatch of package manifest changes through a real Loader and a controlled watcher. */ +import { randomUUID } from 'node:crypto' +import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { Context, type Plugin } from '@deepseek-ai/cordis' +import Loader from '@deepseek-ai/cordis-plugin-loader' +import Timer from '@deepseek-ai/cordis-plugin-timer' +import { FSWatcher, type ChokidarOptions } from 'chokidar' +import { expect, it, onTestFinished, vi } from 'vitest' +import Hmr from '../src/index.ts' + +const watchers = vi.hoisted(() => [] as FSWatcher[]) +// File notifications are delivered only when a test emits them. +vi.mock('chokidar', async (original) => { + const actual = await original() + return { ...actual, watch: (_paths: string | string[], options: ChokidarOptions = {}) => { + const watcher = new actual.FSWatcher(options) + watchers.push(watcher) + queueMicrotask(() => watcher.emit('ready')) + return watcher + } } +}) + +function file(path: string, source: string): void { + mkdirSync(dirname(path), { recursive: true }) + writeFileSync(path, source) +} + +function plugin(marker: string): string { + return `import { value } from '#dep'; +export function apply(ctx) { + ctx.get('manifestTrace').push('start:${marker}:' + value); + ctx.effect(() => () => { ctx.get('manifestTrace').push('stop:${marker}'); }); +} +` +} + +function manifest(entry: string, dep: string): string { + return JSON.stringify({ name: 'addon', type: 'module', exports: `./${entry}.mjs`, imports: { '#dep': `./${dep}.mjs` } }) +} + +async function fixture(source = plugin('a'), options: { lateHmr?: boolean; packageJson?: string } = {}) { + const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-hmr-manifest-dispatch-'))) + const ctx = new Context() + onTestFinished(async () => { + try { await ctx.fiber.dispose() } finally { rmSync(root, { recursive: true, force: true }) } + }) + const pkg = join(root, 'plugins', 'addon') + file(join(pkg, 'package.json'), options.packageJson ?? manifest('a', 'dep-1')) + file(join(pkg, 'a.mjs'), source) + file(join(pkg, 'b.mjs'), plugin('b')) + file(join(pkg, 'dep-1.mjs'), 'export const value = 1') + file(join(pkg, 'dep-2.mjs'), 'export const value = 2') + file(join(root, 'app', 'package.json'), '{"type":"module"}') + mkdirSync(join(root, 'app', 'node_modules'), { recursive: true }) + symlinkSync(pkg, join(root, 'app', 'node_modules', 'addon'), process.platform === 'win32' ? 'junction' : 'dir') + const trace: string[] = [] + ctx.baseUrl = pathToFileURL(join(root, 'app')).href + '/' + ctx.provide('manifestTrace', trace) + await ctx.plugin(Loader) + await ctx.plugin(Timer) + const startHmr = async () => { + const start = watchers.length + const fiber = (await ctx.plugin(Hmr, { root: ['../plugins'], ignored: [], debounce: 0 })).ctx.fiber + return { fiber, watcher: watchers.slice(start).at(-1)! } + } + const initialHmr = options.lateHmr ? undefined : await startHmr() + const entry = ctx.loader.resolve(await ctx.loader.create({ name: 'addon' })) + await ctx.loader.await() + let hmr = initialHmr ?? await startHmr() + const warn = vi.spyOn(ctx.logger, 'warn') + onTestFinished(() => { warn.mockRestore() }) + const restartHmr = async () => { + await hmr.fiber.dispose() + hmr = await startHmr() + } + const emit = async (...paths: string[]) => { + const dispatch = vi.spyOn(ctx.hmr, 'runExclusive') + try { + for (const path of paths) hmr.watcher.emit('change', path) + await vi.waitFor(() => { expect(dispatch).toHaveBeenCalledOnce() }) + const result = dispatch.mock.results[0]! + expect(result.type).toBe('return') + await result.value + } finally { + dispatch.mockRestore() + } + } + return { ctx, pkg, trace, emit, entry, warn, restartHmr } +} + +it('does not reload for a manifest alone and resolves the new entry when the Loader entry restarts', async () => { + const f = await fixture() + expect(f.trace).toEqual(['start:a:1']) + file(join(f.pkg, 'package.json'), manifest('b', 'dep-2')) + await f.emit(join(f.pkg, 'package.json')) + expect(f.trace).toEqual(['start:a:1']) + + await f.entry.update({ disabled: true }) + await f.entry.update({ disabled: false }) + await f.ctx.loader.await() + expect(f.trace).toEqual(['start:a:1', 'stop:a', 'start:b:2']) +}) + +const jsonPlugin = `import manifest from './package.json' with { type: 'json' }; +export function apply(ctx) { + ctx.get('manifestTrace').push('start:json:' + manifest.imports['#dep']); + ctx.effect(() => () => { ctx.get('manifestTrace').push('stop:json'); }); +} +` + +it('reloads a plugin that imports its changed manifest as a JSON module', async () => { + const f = await fixture(jsonPlugin) + expect(f.trace).toEqual(['start:json:./dep-1.mjs']) + file(join(f.pkg, 'package.json'), manifest('a', 'dep-2')) + await f.emit(join(f.pkg, 'package.json')) + expect(f.trace).toEqual(['start:json:./dep-1.mjs', 'stop:json', 'start:json:./dep-2.mjs']) +}) + +it('requests a host reload for a changed manifest in the host module graph', async () => { + const f = await fixture(jsonPlugin) + const filename = join(f.pkg, 'package.json') + const externals = Reflect.get(f.ctx.hmr, 'externals') as Set + externals.add(pathToFileURL(filename).href) + const exit = vi.spyOn(f.ctx.loader, 'exit').mockImplementation(() => {}) + onTestFinished(() => { exit.mockRestore() }) + file(filename, manifest('a', 'dep-2')) + await f.emit(filename) + expect(exit).toHaveBeenCalledOnce() + expect(f.trace).toEqual(['start:json:./dep-1.mjs']) +}) + +it('leaves a configuration-owned manifest to its dedicated watcher', async () => { + const f = await fixture(jsonPlugin) + const filename = join(f.pkg, 'package.json') + const dispose = await f.ctx.hmr.watchConfig(filename, async () => {}) + try { + file(filename, manifest('a', 'dep-2')) + await f.emit(filename) + expect(f.trace).toEqual(['start:json:./dep-1.mjs']) + } finally { + await dispose() + } +}) + +it('resolves package imports from the changed manifest when a source change reloads the plugin', async () => { + const f = await fixture() + file(join(f.pkg, 'package.json'), manifest('a', 'dep-2')) + file(join(f.pkg, 'a.mjs'), `${plugin('a')}// edited\n`) + await f.emit(join(f.pkg, 'a.mjs'), join(f.pkg, 'package.json')) + await f.ctx.loader.await() + expect(f.trace).toEqual(['start:a:1', 'stop:a', 'start:a:2']) +}) + +it('keeps cached package imports when only source changes', async () => { + const f = await fixture() + file(join(f.pkg, 'package.json'), manifest('a', 'dep-2')) + file(join(f.pkg, 'a.mjs'), `${plugin('a')}// edited\n`) + await f.emit(join(f.pkg, 'a.mjs')) + await f.ctx.loader.await() + expect(f.trace).toEqual(['start:a:1', 'stop:a', 'start:a:1']) +}) + +it('leaves manifests below node_modules to their package lifecycle', async () => { + const f = await fixture() + const installed = join(dirname(f.pkg), 'node_modules', 'dep', 'package.json') + file(installed, '{"name":"dep"}') + const invalidate = vi.spyOn(Reflect.get(f.ctx.hmr, 'manifests') as { invalidate(path: string): void }, 'invalidate') + onTestFinished(() => { invalidate.mockRestore() }) + await f.emit(installed) + expect(invalidate).not.toHaveBeenCalled() +}) + +it.each(['exports', 'main'] as const)('keeps reloading the loaded entry after %s moves and selects the new entry on restart', async (field) => { + const packageJson = (entry: string, dep: string) => JSON.stringify({ + name: 'addon', type: 'module', [field]: `./${entry}.mjs`, imports: { '#dep': `./${dep}.mjs` }, + }) + const f = await fixture(plugin('a'), { packageJson: packageJson('a', 'dep-1') }) + const originalNamespace = f.entry.moduleNamespace + file(join(f.pkg, 'package.json'), packageJson('b', 'dep-2')) + await f.emit(join(f.pkg, 'package.json')) + expect(f.trace).toEqual(['start:a:1']) + expect(f.entry.moduleNamespace).toBe(originalNamespace) + file(join(f.pkg, 'a.mjs'), plugin('a-edited')) + await f.emit(join(f.pkg, 'a.mjs')) + expect(f.trace).toEqual(['start:a:1', 'stop:a', 'start:a-edited:2']) + expect(f.entry.moduleNamespace).not.toBe(originalNamespace) + expect(f.entry.moduleNamespace).toBe(await f.ctx.loader.import(pathToFileURL(join(f.pkg, 'a.mjs')).href)) + await f.entry.update({ disabled: true }) + await f.entry.update({ disabled: false }) + await f.ctx.loader.await() + expect(f.trace).toEqual(['start:a:1', 'stop:a', 'start:a-edited:2', 'stop:a-edited', 'start:b:2']) + expect(f.warn).not.toHaveBeenCalled() +}) + +it('retains the loaded entry through repeated reloads when the new entry is already cached', async () => { + const f = await fixture() + file(join(f.pkg, 'package.json'), manifest('b', 'dep-2')) + await f.emit(join(f.pkg, 'package.json')) + await f.ctx.loader.import('addon') + const originalNamespace = f.entry.moduleNamespace + file(join(f.pkg, 'a.mjs'), plugin('a-first')) + await f.emit(join(f.pkg, 'a.mjs')) + const firstNamespace = f.entry.moduleNamespace + expect(firstNamespace).not.toBe(originalNamespace) + file(join(f.pkg, 'a.mjs'), plugin('a-second')) + await f.emit(join(f.pkg, 'a.mjs')) + expect(f.entry.moduleNamespace).not.toBe(firstNamespace) + expect(f.trace).toEqual(['start:a:1', 'stop:a', 'start:a-first:2', 'stop:a-first', 'start:a-second:2']) + expect(f.warn).not.toHaveBeenCalled() +}) + +it.each(['apply', 'apply as default'])('retains a re-export entry using its namespace when it exports %s', async (exported) => { + const f = await fixture(`export { ${exported} } from './b.mjs';\n`) + const implementation: unknown = await f.ctx.loader.import(pathToFileURL(join(f.pkg, 'b.mjs')).href) + expect(implementation).toMatchObject({ apply: f.entry.fiber?.runtime?.callback }) + expect(f.entry.moduleNamespace).not.toBe(implementation) + file(join(f.pkg, 'c.mjs'), plugin('c')) + file(join(f.pkg, 'package.json'), manifest('c', 'dep-1')) + await f.emit(join(f.pkg, 'package.json')) + file(join(f.pkg, 'dep-1.mjs'), 'export const value = 2') + await f.emit(join(f.pkg, 'dep-1.mjs')) + expect(f.trace).toEqual(['start:b:1', 'stop:b', 'start:b:2']) + expect(f.entry.moduleNamespace).toBe(await f.ctx.loader.import(pathToFileURL(join(f.pkg, 'a.mjs')).href)) + await f.entry.update({ disabled: true }) + await f.entry.update({ disabled: false }) + await f.ctx.loader.await() + expect(f.trace).toEqual(['start:b:1', 'stop:b', 'start:b:2', 'stop:b', 'start:c:2']) + expect(f.warn).not.toHaveBeenCalled() +}) + +it('tracks an entry loaded before HMR starts', async () => { + const f = await fixture(plugin('a'), { lateHmr: true }) + file(join(f.pkg, 'package.json'), manifest('b', 'dep-1')) + await f.emit(join(f.pkg, 'package.json')) + file(join(f.pkg, 'a.mjs'), plugin('a-late')) + await f.emit(join(f.pkg, 'a.mjs')) + expect(f.trace).toEqual(['start:a:1', 'stop:a', 'start:a-late:1']) + expect(f.warn).not.toHaveBeenCalled() +}) + +it('keeps entry identity when HMR is recreated between source reloads', async () => { + const f = await fixture() + file(join(f.pkg, 'a.mjs'), plugin('a-first')) + await f.emit(join(f.pkg, 'a.mjs')) + const firstNamespace = f.entry.moduleNamespace + await f.restartHmr() + expect(f.entry.moduleNamespace).toBe(firstNamespace) + file(join(f.pkg, 'a.mjs'), plugin('a-second')) + await f.emit(join(f.pkg, 'a.mjs')) + expect(f.entry.moduleNamespace).not.toBe(firstNamespace) + expect(f.trace).toEqual(['start:a:1', 'stop:a', 'start:a-first:1', 'stop:a-first', 'start:a-second:1']) + expect(f.warn).not.toHaveBeenCalled() +}) + +it('leaves an unimported disabled entry without a Node cache entry alone', async () => { + const f = await fixture() + const disabled = f.ctx.loader.resolve(await f.ctx.loader.create({ + name: pathToFileURL(join(f.pkg, 'b.mjs')).href, disabled: true, + })) + expect(disabled.moduleNamespace).toBeUndefined() + file(join(f.pkg, 'a.mjs'), plugin('a-edited')) + await f.emit(join(f.pkg, 'a.mjs')) + expect(disabled.moduleNamespace).toBeUndefined() + expect(disabled.fiber).toBeUndefined() + expect(f.trace).toEqual(['start:a:1', 'stop:a', 'start:a-edited:1']) + expect(f.warn).not.toHaveBeenCalled() +}) + +it('leaves a cordis builtin outside the Node namespace index without reporting an error', async () => { + const f = await fixture() + const builtin = { apply: vi.fn() } + f.ctx.loader.builtins.fixture = builtin + const entry = f.ctx.loader.resolve(await f.ctx.loader.create({ name: 'cordis:fixture' })) + await f.ctx.loader.await() + expect(entry.moduleNamespace).toBe(builtin) + file(join(f.pkg, 'a.mjs'), plugin('a-edited')) + await f.emit(join(f.pkg, 'a.mjs')) + expect(builtin.apply).toHaveBeenCalledOnce() + expect(f.trace).toEqual(['start:a:1', 'stop:a', 'start:a-edited:1']) + expect(f.warn).not.toHaveBeenCalled() +}) + +it('updates disabled entry namespaces through repeated reloads without activating them', async () => { + const f = await fixture() + const originalNamespace = f.entry.moduleNamespace + await f.entry.update({ disabled: true }) + await f.ctx.loader.await() + file(join(f.pkg, 'a.mjs'), plugin('disabled-first')) + await f.emit(join(f.pkg, 'a.mjs')) + const firstNamespace = f.entry.moduleNamespace + expect(firstNamespace).not.toBe(originalNamespace) + expect(firstNamespace).toBe(await f.ctx.loader.import('addon')) + expect(f.trace).toEqual(['start:a:1', 'stop:a']) + file(join(f.pkg, 'a.mjs'), plugin('disabled-second')) + await f.emit(join(f.pkg, 'a.mjs')) + expect(f.entry.moduleNamespace).not.toBe(firstNamespace) + expect(f.trace).toEqual(['start:a:1', 'stop:a']) + expect(f.warn).not.toHaveBeenCalled() + await f.entry.update({ disabled: false }) + await f.ctx.loader.await() + expect(f.trace).toEqual(['start:a:1', 'stop:a', 'start:disabled-second:1']) +}) + +it('reports a resolution error for an unimported entry through the existing warning path', async () => { + const f = await fixture() + await f.ctx.loader.create({ name: './missing-plugin.mjs', disabled: true }) + file(join(f.pkg, 'a.mjs'), plugin('a-edited')) + await f.emit(join(f.pkg, 'a.mjs')) + expect(f.warn).toHaveBeenCalledWith(expect.objectContaining({ code: 'ERR_MODULE_NOT_FOUND' })) + expect(f.trace).toEqual(['start:a:1', 'stop:a', 'start:a-edited:1']) +}) + +it('reports a recorded namespace missing from the cache while reloading another entry', async () => { + const f = await fixture() + const originalFiber = f.entry.fiber + const originalNamespace = f.entry.moduleNamespace + await f.ctx.loader.create({ name: pathToFileURL(join(f.pkg, 'b.mjs')).href }) + await f.ctx.loader.await() + expect(Map.prototype.delete.call(f.ctx.loader.internal!.loadCache, pathToFileURL(join(f.pkg, 'a.mjs')).href)).toBe(true) + file(join(f.pkg, 'b.mjs'), plugin('b-edited')) + await f.emit(join(f.pkg, 'b.mjs')) + expect(f.warn).toHaveBeenCalledWith(expect.objectContaining({ + message: `HMR cannot locate the loaded module for addon from ${f.ctx.baseUrl}`, + })) + expect(f.entry.fiber).toBe(originalFiber) + expect(f.entry.moduleNamespace).toBe(originalNamespace) + expect(f.trace).toEqual(['start:a:1', 'start:b:1', 'stop:b', 'start:b-edited:1']) +}) + +it.each(['import', 'activation'] as const)('retains the prior namespace after replacement %s fails and can reload again', async (phase) => { + const f = await fixture() + const originalNamespace = f.entry.moduleNamespace + const originalCallback = f.entry.fiber?.runtime?.callback + const message = `replacement ${phase} failed` + file(join(f.pkg, 'a.mjs'), phase === 'import' + ? `throw new Error(${JSON.stringify(message)});\n${plugin('failed')}` + : `export function apply() { throw new Error(${JSON.stringify(message)}); }`) + await expect(f.emit(join(f.pkg, 'a.mjs'))).rejects.toThrow(message) + expect(f.entry.moduleNamespace).toBe(originalNamespace) + expect(await f.ctx.loader.import('addon')).toBe(originalNamespace) + expect(f.entry.fiber?.runtime?.callback).toBe(originalCallback) + expect(f.trace).toEqual(phase === 'import' ? ['start:a:1'] : ['start:a:1', 'stop:a', 'start:a:1']) + file(join(f.pkg, 'a.mjs'), plugin('recovered')) + await f.emit(join(f.pkg, 'a.mjs')) + expect(f.entry.moduleNamespace).not.toBe(originalNamespace) + expect(f.trace.at(-1)).toBe('start:recovered:1') +}) + +it('reloads a native CommonJS entry and retains its new import namespace', async () => { + const f = await fixture() + const filename = join(f.pkg, 'common.cjs') + const source = (version: number) => `exports.apply = ctx => { + ctx.get('manifestTrace').push('start:cjs:${version}'); + ctx.effect(() => () => { ctx.get('manifestTrace').push('stop:cjs:${version}'); }); + };` + file(filename, source(1)) + const entry = f.ctx.loader.resolve(await f.ctx.loader.create({ name: pathToFileURL(filename).href })) + await f.ctx.loader.await() + const originalNamespace = entry.moduleNamespace + file(filename, source(2)) + await f.emit(filename) + expect(entry.moduleNamespace).not.toBe(originalNamespace) + expect(entry.moduleNamespace).toBe(await f.ctx.loader.import(pathToFileURL(filename).href)) + expect(f.trace).toEqual(['start:a:1', 'start:cjs:1', 'stop:cjs:1', 'start:cjs:2']) + expect(f.warn).not.toHaveBeenCalled() +}) + +it('handles an entry still evaluating while another entry reloads', async () => { + const f = await fixture() + const entered = Promise.withResolvers() + const release = Promise.withResolvers() + const key = 'dsh-hmr-pending-entry-' + randomUUID() + Reflect.set(globalThis, key, { entered: () => { entered.resolve(undefined) }, wait: release.promise }) + f.ctx.provide('releasePendingEntry', () => { release.resolve(undefined) }) + const filename = join(f.pkg, 'pending.mjs') + file(filename, `const gate = globalThis[${JSON.stringify(key)}]; gate.entered(); await gate.wait;\n${plugin('pending')}`) + const loading = f.ctx.loader.create({ name: pathToFileURL(filename).href }) + onTestFinished(async () => { + release.resolve(undefined) + try { await loading } finally { Reflect.deleteProperty(globalThis, key) } + }) + try { + await entered.promise + file(join(f.pkg, 'a.mjs'), plugin('a-edited').replace('export function apply(ctx) {', + "export function apply(ctx) { ctx.get('releasePendingEntry')();")) + await f.emit(join(f.pkg, 'a.mjs')) + const entry = f.ctx.loader.resolve(await loading) + await f.ctx.loader.await() + expect(entry.moduleNamespace).toBe(await f.ctx.loader.import(pathToFileURL(filename).href)) + expect(f.trace).toEqual(['start:a:1', 'stop:a', 'start:a-edited:1', 'start:pending:1']) + expect(f.warn).not.toHaveBeenCalled() + } finally { + release.resolve(undefined) + await loading + } +}) + +it('keeps an active entry namespace when a later same-name entry has never been enabled', async () => { + const f = await fixture() + await f.ctx.loader.root.update([ + ...f.ctx.loader.root.data, + { id: 'disabled-shadow', name: 'addon', disabled: true }, + ]) + await f.ctx.loader.await() + const disabled = f.ctx.loader.resolve('disabled-shadow') + expect(disabled.moduleNamespace).toBeUndefined() + expect(disabled.fiber).toBeUndefined() + const before = [...f.trace] + file(join(f.pkg, 'package.json'), manifest('b', 'dep-2')) + await f.emit(join(f.pkg, 'package.json')) + expect(f.trace).toEqual(before) + file(join(f.pkg, 'a.mjs'), plugin('a-edited')) + await f.emit(join(f.pkg, 'a.mjs')) + expect(f.trace).toEqual([...before, 'stop:a', 'start:a-edited:2']) + expect(disabled.fiber).toBeUndefined() + expect(f.warn).not.toHaveBeenCalled() +}) + +type DispatchFixture = Awaited> + +async function addConfiguredEntry(f: DispatchFixture, id: string, name: string, disabled = false) { + const options = { id, name, disabled } + const entry = f.ctx.loader.resolve(await f.ctx.loader.create(options)) + await f.ctx.loader.await() + return entry +} + +function cachedNamespace(f: DispatchFixture, filename: string): unknown { + const url = pathToFileURL(join(f.pkg, filename)).href + const job = f.ctx.loader.internal!.loadCache.get(url) + expect(job?.module).toBeDefined() + return job!.module!.getNamespace() +} + +function liveFibers(runtime: Plugin.Runtime | null | undefined) { + expect(runtime).toBeDefined() + expect(runtime).not.toBeNull() + return [...runtime!.fibers].filter(fiber => fiber.uid !== null) +} + +async function sharedEntryFixture(kind: 'default' | 'named') { + const source = kind === 'default' + ? "export { apply as default } from './b.mjs';\n" + : "export { apply } from './b.mjs';\n" + const f = await fixture(source) + const secondFile = kind === 'default' ? 'c.mjs' : 'b.mjs' + if (kind === 'default') file(join(f.pkg, secondFile), source) + const second = await addConfiguredEntry(f, 'shared-second', pathToFileURL(join(f.pkg, secondFile)).href) + const firstPlugin = f.ctx.loader.unwrapExports(f.entry.moduleNamespace) as Plugin + const secondPlugin = f.ctx.loader.unwrapExports(second.moduleNamespace) as Plugin + expect(f.entry.moduleNamespace).not.toBe(second.moduleNamespace) + expect(f.entry.moduleNamespace).toBe(cachedNamespace(f, 'a.mjs')) + expect(second.moduleNamespace).toBe(cachedNamespace(f, secondFile)) + if (kind === 'default') expect(firstPlugin).toBe(secondPlugin) + else expect(firstPlugin).not.toBe(secondPlugin) + expect(f.entry.fiber?.runtime).toBe(second.fiber?.runtime) + expect(liveFibers(f.entry.fiber?.runtime)).toHaveLength(2) + expect(f.trace).toEqual(['start:b:1', 'start:b:1']) + return { ...f, second, secondFile, firstPlugin } +} + +it('reloads both active namespaces for one package name independently after exports move', async () => { + const f = await fixture() + const firstNamespace = f.entry.moduleNamespace + const firstRuntime = f.entry.fiber?.runtime + file(join(f.pkg, 'package.json'), manifest('b', 'dep-2')) + await f.emit(join(f.pkg, 'package.json')) + const second = await addConfiguredEntry(f, 'active-after-move', 'addon') + const secondNamespace = second.moduleNamespace + const secondRuntime = second.fiber?.runtime + expect(firstNamespace).not.toBe(secondNamespace) + expect(firstRuntime).not.toBe(secondRuntime) + expect(liveFibers(firstRuntime)).toHaveLength(1) + expect(liveFibers(secondRuntime)).toHaveLength(1) + expect(f.trace).toEqual(['start:a:1', 'start:b:2']) + + file(join(f.pkg, 'a.mjs'), plugin('a-independent')) + await f.emit(join(f.pkg, 'a.mjs')) + const reloadedFirst = f.entry.moduleNamespace + expect(reloadedFirst).not.toBe(firstNamespace) + expect(reloadedFirst).toBe(cachedNamespace(f, 'a.mjs')) + expect(second.moduleNamespace).toBe(secondNamespace) + expect(second.fiber?.runtime).toBe(secondRuntime) + expect(f.trace).toEqual(['start:a:1', 'start:b:2', 'stop:a', 'start:a-independent:2']) + + file(join(f.pkg, 'b.mjs'), plugin('b-independent')) + await f.emit(join(f.pkg, 'b.mjs')) + expect(f.entry.moduleNamespace).toBe(reloadedFirst) + expect(second.moduleNamespace).not.toBe(secondNamespace) + expect(second.moduleNamespace).toBe(cachedNamespace(f, 'b.mjs')) + expect(liveFibers(f.entry.fiber?.runtime)).toHaveLength(1) + expect(liveFibers(second.fiber?.runtime)).toHaveLength(1) + expect(f.trace).toEqual([ + 'start:a:1', 'start:b:2', 'stop:a', 'start:a-independent:2', 'stop:b', 'start:b-independent:2', + ]) + expect(f.warn).not.toHaveBeenCalled() +}) + +it('refreshes both default re-export namespaces sharing one plugin without multiplying instances', async () => { + const f = await sharedEntryFixture('default') + const firstNamespace = f.entry.moduleNamespace + const secondNamespace = f.second.moduleNamespace + const oldFibers = liveFibers(f.entry.fiber?.runtime) + const before = f.trace.length + file(join(f.pkg, 'dep-1.mjs'), 'export const value = 2') + await f.emit(join(f.pkg, 'dep-1.mjs')) + + expect(f.entry.moduleNamespace).not.toBe(firstNamespace) + expect(f.second.moduleNamespace).not.toBe(secondNamespace) + expect(f.entry.moduleNamespace).not.toBe(f.second.moduleNamespace) + expect(f.entry.moduleNamespace).toBe(cachedNamespace(f, 'a.mjs')) + expect(f.second.moduleNamespace).toBe(cachedNamespace(f, 'c.mjs')) + expect(f.ctx.loader.unwrapExports(f.entry.moduleNamespace)).toBe(f.ctx.loader.unwrapExports(f.second.moduleNamespace)) + expect(f.entry.fiber?.runtime).toBe(f.second.fiber?.runtime) + expect(liveFibers(f.entry.fiber?.runtime)).toHaveLength(2) + expect(oldFibers.every(fiber => fiber.uid === null)).toBe(true) + expect(f.trace.slice(before).sort()).toEqual(['start:b:2', 'start:b:2', 'stop:b', 'stop:b']) + expect(f.warn).not.toHaveBeenCalled() +}) + +it('refreshes named re-export and implementation namespaces sharing a callback without multiplying instances', async () => { + const f = await sharedEntryFixture('named') + const firstNamespace = f.entry.moduleNamespace + const secondNamespace = f.second.moduleNamespace + const oldFibers = liveFibers(f.entry.fiber?.runtime) + const before = f.trace.length + file(join(f.pkg, 'b.mjs'), plugin('implementation-updated')) + await f.emit(join(f.pkg, 'b.mjs')) + + expect(f.entry.moduleNamespace).not.toBe(firstNamespace) + expect(f.second.moduleNamespace).not.toBe(secondNamespace) + expect(f.entry.moduleNamespace).toBe(cachedNamespace(f, 'a.mjs')) + expect(f.second.moduleNamespace).toBe(cachedNamespace(f, 'b.mjs')) + expect(f.ctx.loader.unwrapExports(f.entry.moduleNamespace)).not.toBe(f.ctx.loader.unwrapExports(f.second.moduleNamespace)) + expect(f.entry.fiber?.runtime).toBe(f.second.fiber?.runtime) + expect(liveFibers(f.entry.fiber?.runtime)).toHaveLength(2) + expect(oldFibers.every(fiber => fiber.uid === null)).toBe(true) + expect(f.trace.slice(before).sort()).toEqual([ + 'start:implementation-updated:1', 'start:implementation-updated:1', 'stop:b', 'stop:b', + ]) + expect(f.warn).not.toHaveBeenCalled() +}) + +function activeTraceCount(trace: string[], marker: string): number { + return trace.filter(event => event.startsWith(`start:${marker}:`)).length + - trace.filter(event => event === `stop:${marker}`).length +} + +it('assigns each entry its own replacement when a shared runtime splits into distinct callbacks', async () => { + const f = await sharedEntryFixture('default') + const firstNamespace = f.entry.moduleNamespace + const secondNamespace = f.second.moduleNamespace + const oldFibers = liveFibers(f.entry.fiber?.runtime) + const before = f.trace.length + file(join(f.pkg, 'a.mjs'), plugin('a-split')) + file(join(f.pkg, 'c.mjs'), plugin('c-split')) + await f.emit(join(f.pkg, 'a.mjs'), join(f.pkg, 'c.mjs')) + + expect(f.entry.moduleNamespace).not.toBe(firstNamespace) + expect(f.second.moduleNamespace).not.toBe(secondNamespace) + expect(f.entry.moduleNamespace).toBe(cachedNamespace(f, 'a.mjs')) + expect(f.second.moduleNamespace).toBe(cachedNamespace(f, 'c.mjs')) + expect(f.entry.moduleNamespace).toMatchObject({ apply: f.entry.fiber?.runtime?.callback }) + expect(f.second.moduleNamespace).toMatchObject({ apply: f.second.fiber?.runtime?.callback }) + expect(f.entry.fiber?.runtime).not.toBe(f.second.fiber?.runtime) + expect(liveFibers(f.entry.fiber?.runtime)).toHaveLength(1) + expect(liveFibers(f.second.fiber?.runtime)).toHaveLength(1) + expect(oldFibers.every(fiber => fiber.uid === null)).toBe(true) + expect(f.trace.slice(before).sort()).toEqual(['start:a-split:1', 'start:c-split:1', 'stop:b', 'stop:b']) + expect(f.warn).not.toHaveBeenCalled() +}) + +it('restores shared entries and a programmatic instance when split replacements are ambiguous', async () => { + const f = await sharedEntryFixture('default') + const firstNamespace = f.entry.moduleNamespace + const secondNamespace = f.second.moduleNamespace + const programmatic = (await f.ctx.plugin(f.firstPlugin)).ctx.fiber + expect(programmatic.entry).toBeUndefined() + expect(liveFibers(f.ctx.registry.get(f.firstPlugin))).toHaveLength(3) + file(join(f.pkg, 'a.mjs'), plugin('a-split')) + file(join(f.pkg, 'c.mjs'), plugin('c-split')) + await expect(f.emit(join(f.pkg, 'a.mjs'), join(f.pkg, 'c.mjs'))).rejects.toThrow( + 'HMR replacement is ambiguous for a plugin instance without a Loader entry', + ) + + expect(f.entry.moduleNamespace).toBe(firstNamespace) + expect(f.second.moduleNamespace).toBe(secondNamespace) + expect(cachedNamespace(f, 'a.mjs')).toBe(firstNamespace) + expect(cachedNamespace(f, 'c.mjs')).toBe(secondNamespace) + const restored = f.ctx.registry.get(f.firstPlugin) + const fibers = liveFibers(restored) + expect(fibers).toHaveLength(3) + expect(fibers.filter(fiber => fiber.entry === undefined)).toHaveLength(1) + expect(f.entry.fiber?.runtime).toBe(restored) + expect(f.second.fiber?.runtime).toBe(restored) + expect(activeTraceCount(f.trace, 'b')).toBe(3) + expect(activeTraceCount(f.trace, 'a-split')).toBe(0) + expect(activeTraceCount(f.trace, 'c-split')).toBe(0) +}) + +it.each(['import', 'activation'] as const)('restores every shared entry when one replacement %s fails and can reload again', async (phase) => { + const f = await sharedEntryFixture('default') + const firstNamespace = f.entry.moduleNamespace + const secondNamespace = f.second.moduleNamespace + const message = `shared replacement ${phase} failed` + file(join(f.pkg, 'a.mjs'), plugin('a-replacement')) + file(join(f.pkg, 'c.mjs'), phase === 'import' + ? `throw new Error(${JSON.stringify(message)});\n${plugin('c-failed')}` + : `export function apply() { throw new Error(${JSON.stringify(message)}); }`) + await expect(f.emit(join(f.pkg, 'a.mjs'), join(f.pkg, 'c.mjs'))).rejects.toThrow(message) + + expect(f.entry.moduleNamespace).toBe(firstNamespace) + expect(f.second.moduleNamespace).toBe(secondNamespace) + expect(cachedNamespace(f, 'a.mjs')).toBe(firstNamespace) + expect(cachedNamespace(f, 'c.mjs')).toBe(secondNamespace) + const restored = f.ctx.registry.get(f.firstPlugin) + expect(f.entry.fiber?.runtime).toBe(restored) + expect(f.second.fiber?.runtime).toBe(restored) + expect(liveFibers(restored)).toHaveLength(2) + expect(activeTraceCount(f.trace, 'b')).toBe(2) + expect(activeTraceCount(f.trace, 'a-replacement')).toBe(0) + expect(activeTraceCount(f.trace, 'c-failed')).toBe(0) + + file(join(f.pkg, 'c.mjs'), plugin('c-recovered')) + await f.emit(join(f.pkg, 'a.mjs'), join(f.pkg, 'c.mjs')) + expect(f.entry.moduleNamespace).not.toBe(firstNamespace) + expect(f.second.moduleNamespace).not.toBe(secondNamespace) + expect(f.entry.moduleNamespace).toBe(cachedNamespace(f, 'a.mjs')) + expect(f.second.moduleNamespace).toBe(cachedNamespace(f, 'c.mjs')) + expect(f.entry.moduleNamespace).toMatchObject({ apply: f.entry.fiber?.runtime?.callback }) + expect(f.second.moduleNamespace).toMatchObject({ apply: f.second.fiber?.runtime?.callback }) + expect(f.entry.fiber?.runtime).not.toBe(f.second.fiber?.runtime) + expect(liveFibers(f.entry.fiber?.runtime)).toHaveLength(1) + expect(liveFibers(f.second.fiber?.runtime)).toHaveLength(1) + expect(activeTraceCount(f.trace, 'b')).toBe(0) + expect(activeTraceCount(f.trace, 'a-replacement')).toBe(1) + expect(activeTraceCount(f.trace, 'c-recovered')).toBe(1) +}) + +it('deduplicates a repeated namespace without activating a same-name uninitialized entry', async () => { + const f = await fixture() + const second = await addConfiguredEntry(f, 'duplicate-active', 'addon') + const disabled = await addConfiguredEntry(f, 'duplicate-disabled', 'addon', true) + const originalNamespace = f.entry.moduleNamespace + expect(second.moduleNamespace).toBe(originalNamespace) + expect(disabled.moduleNamespace).toBeUndefined() + expect(disabled.fiber).toBeUndefined() + expect(liveFibers(f.entry.fiber?.runtime)).toHaveLength(2) + file(join(f.pkg, 'package.json'), manifest('b', 'dep-2')) + await f.emit(join(f.pkg, 'package.json')) + const before = f.trace.length + file(join(f.pkg, 'a.mjs'), plugin('deduplicated')) + await f.emit(join(f.pkg, 'a.mjs')) + + expect(f.entry.moduleNamespace).not.toBe(originalNamespace) + expect(f.entry.moduleNamespace).toBe(cachedNamespace(f, 'a.mjs')) + expect(second.moduleNamespace).toBe(f.entry.moduleNamespace) + expect(f.entry.fiber?.runtime).toBe(second.fiber?.runtime) + expect(liveFibers(f.entry.fiber?.runtime)).toHaveLength(2) + expect(disabled.moduleNamespace).toBeUndefined() + expect(disabled.fiber).toBeUndefined() + expect(f.trace.slice(before).sort()).toEqual([ + 'start:deduplicated:2', 'start:deduplicated:2', 'stop:a', 'stop:a', + ]) + expect(f.warn).not.toHaveBeenCalled() +}) diff --git a/packages/boot/hmr/tests/package-manifest.spec.ts b/packages/boot/hmr/tests/package-manifest.spec.ts new file mode 100644 index 0000000000..52847f1cdc --- /dev/null +++ b/packages/boot/hmr/tests/package-manifest.spec.ts @@ -0,0 +1,457 @@ +/** Package configuration read by Node's resolver follows an invalidated manifest on disk. */ +import { spawn } from 'node:child_process' +import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs' +import NodeModule, { createRequire, registerHooks } from 'node:module' +import { tmpdir } from 'node:os' +import { dirname, join, toNamespacedPath } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { ModuleLoader } from '@deepseek-ai/cordis-plugin-loader' +import { afterEach, describe, expect, it } from 'vitest' +import { PackageManifests } from '../src/package-manifest.ts' + +const require = createRequire(import.meta.url) +const addon = require('node-addon-require-builtin') as { requireBuiltin(id: string): unknown } +const reader = addon.requireBuiltin('internal/modules/package_json_reader') as { + read: (path: string, options?: { isESM: boolean; base: string; specifier: string }) => { exists: boolean; type: string } + getPackageScopeConfig: (url: string) => { exists: boolean; type: string; pjsonPath: string } + getNearestParentPackageJSON: (path: string) => { data: { type: string }; path: string } | undefined +} +const formats = addon.requireBuiltin('internal/modules/esm/get_format') as { + defaultGetFormatWithoutErrors(url: URL, context?: object): string | null +} +const binding = (addon.requireBuiltin('internal/bootstrap/realm') as { + internalBinding(id: 'modules'): Record +}).internalBinding('modules') + +const cleanup: Array<() => void | Promise> = [] +afterEach(async () => { for (const dispose of cleanup.splice(0).reverse()) await dispose() }) + +function file(path: string, source: string): void { + mkdirSync(dirname(path), { recursive: true }) + writeFileSync(path, source) +} + +function fixture() { + const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-hmr-manifest-'))) + cleanup.push(() => { rmSync(root, { recursive: true, force: true }) }) + const dir = join(root, 'packages', 'pkg') + const manifest = join(dir, 'package.json') + const importer = join(root, 'importer', 'entry.mjs') + file(join(root, 'importer', 'package.json'), '{"type":"module","dependencies":{"pkg":"*"}}') + file(importer, 'export {}') + // A workspace-style link: the importer resolves `pkg` from its own node_modules to the source directory. + mkdirSync(join(root, 'importer', 'node_modules'), { recursive: true }) + const link = join(root, 'importer', 'node_modules', 'pkg') + symlinkSync(dir, link, process.platform === 'win32' ? 'junction' : 'dir') + for (const name of ['a', 'b']) { + file(join(dir, `${name}.mjs`), `export const marker = '${name}'`) + file(join(dir, `${name}.cjs`), `module.exports = { marker: '${name}' }`) + } + const manifests = new PackageManifests() + cleanup.push(() => { manifests.dispose() }) + const invalidate = (path: string) => Promise.resolve().then(manifests.invalidate.bind(manifests, path)) + return { root, dir, manifest, importer, importerURL: pathToFileURL(importer).href, manifests, invalidate } +} + +function resolveEsm(specifier: string, parentURL: string): string { + const loader = ModuleLoader.fromInternal()! + return (loader.version === 'v2' + ? loader.resolveSync(parentURL, { specifier, attributes: {} }) + : loader.resolveSync(specifier, parentURL, {})).url +} + +describe('package manifest invalidation', { concurrent: false }, () => { + it('preserves filename-based resolve hooks and node-prefixed builtins after invalidation', async () => { + const f = fixture() + file(f.manifest, '{}') + const importerRequire = createRequire(f.importer) + const nativeFs = importerRequire('node:fs') as typeof import('node:fs') + const missingBuiltin = 'node:dsh-hmr-missing' + const missingFailure = outcome(() => importerRequire(missingBuiltin)) + expect(missingFailure).toHaveProperty('code', 'ERR_UNKNOWN_BUILTIN_MODULE') + const target = join(f.dir, 'a.cjs') + const specifier = join(f.dir, 'alias.cjs') + file(specifier, 'module.exports = { marker: "alias" }') + cleanup.push(() => { Reflect.deleteProperty(importerRequire.cache, target) }) + let hooks: ReturnType | undefined = registerHooks({ + resolve(request, context, nextResolve) { + const resolved = nextResolve(request, context) + return resolved.url === pathToFileURL(specifier).href ? { ...resolved, url: pathToFileURL(target).href } : resolved + }, + }) + cleanup.push(() => { hooks?.deregister() }) + try { + const original = importerRequire(specifier) as { marker: string } + expect(original).toEqual({ marker: 'a' }) + await f.invalidate(f.manifest) + expect(importerRequire(specifier)).toBe(original) + } finally { + hooks.deregister() + hooks = undefined + } + + const descriptor = Object.getOwnPropertyDescriptor(importerRequire.cache, 'fs') + const restore = () => { + if (descriptor) Object.defineProperty(importerRequire.cache, 'fs', descriptor) + else Reflect.deleteProperty(importerRequire.cache, 'fs') + } + cleanup.push(restore) + const fake = new NodeModule('fs') + const fakeExports = { marker: 'fake fs' } + fake.exports = fakeExports + try { + importerRequire.cache.fs = fake + expect(importerRequire('fs')).toBe(fakeExports) + expect(importerRequire('node:fs')).toBe(nativeFs) + expect(outcome(() => importerRequire(missingBuiltin))).toEqual(missingFailure) + } finally { + restore() + } + }) + + it('loads changed CommonJS exports without evicting previously loaded modules', async () => { + const f = fixture() + const a = join(f.dir, 'a.cjs') + const b = join(f.dir, 'b.cjs') + const unrelated = join(f.root, 'unrelated.cjs') + file(unrelated, 'module.exports = { marker: "unrelated" }') + file(f.manifest, JSON.stringify({ name: 'pkg', exports: './a.cjs' })) + const importerRequire = createRequire(f.importer) + cleanup.push(() => { + for (const path of [a, b, unrelated]) Reflect.deleteProperty(importerRequire.cache, path) + }) + const original = importerRequire('pkg') as { marker: string } + const other = importerRequire(unrelated) as { marker: string } + const originalModule = importerRequire.cache[a] + const otherModule = importerRequire.cache[unrelated] + expect(original).toEqual({ marker: 'a' }) + expect(originalModule).toBeDefined() + expect(otherModule).toBeDefined() + + file(f.manifest, JSON.stringify({ name: 'pkg', exports: './b.cjs' })) + await f.invalidate(f.manifest) + expect(importerRequire.resolve('pkg')).toBe(b) + const current = importerRequire('pkg') as { marker: string } + expect(importerRequire.cache[a]).toBe(originalModule) + expect(importerRequire.cache[unrelated]).toBe(otherModule) + expect(original).toEqual({ marker: 'a' }) + expect(importerRequire(a)).toBe(original) + expect(importerRequire(unrelated)).toBe(other) + expect(current).toEqual({ marker: 'b' }) + }) + + it.each(['symlink', 'main'] as const)('refreshes a cached external %s entry after its manifest changes', async (entry) => { + const f = fixture() + const external = join(f.root, 'packages', 'outside', 'entry.mjs') + file(external, 'export {}') + if (entry === 'symlink') { + symlinkSync(dirname(external), join(f.dir, 'linked'), process.platform === 'win32' ? 'junction' : 'dir') + file(f.manifest, JSON.stringify({ name: 'pkg', exports: './linked/entry.mjs' })) + } else { + file(f.manifest, JSON.stringify({ name: 'pkg', main: external })) + } + const importerRequire = createRequire(f.importer) + expect(importerRequire.resolve('pkg')).toBe(external) + expect(resolveEsm('pkg', f.importerURL)).toBe(pathToFileURL(external).href) + + file(f.manifest, JSON.stringify({ name: 'pkg', main: './b.mjs', exports: './b.mjs' })) + await f.invalidate(f.manifest) + expect.soft(importerRequire.resolve('pkg')).toBe(join(f.dir, 'b.mjs')) + expect(resolveEsm('pkg', f.importerURL)).toBe(pathToFileURL(join(f.dir, 'b.mjs')).href) + }) + + it('resolves changed exports and main only after the manifest is invalidated', () => { + const f = fixture() + file(f.manifest, JSON.stringify({ name: 'pkg', exports: { import: './a.mjs', require: './a.cjs' } })) + const importerRequire = createRequire(f.importer) + expect(resolveEsm('pkg', f.importerURL)).toBe(pathToFileURL(join(f.dir, 'a.mjs')).href) + expect(importerRequire.resolve('pkg')).toBe(join(f.dir, 'a.cjs')) + + file(f.manifest, JSON.stringify({ name: 'pkg', exports: { import: './b.mjs', require: './b.cjs' } })) + expect(resolveEsm('pkg', f.importerURL)).toBe(pathToFileURL(join(f.dir, 'a.mjs')).href) + f.manifests.invalidate(f.manifest) + expect(resolveEsm('pkg', f.importerURL)).toBe(pathToFileURL(join(f.dir, 'b.mjs')).href) + expect(importerRequire.resolve('pkg')).toBe(join(f.dir, 'b.cjs')) + + file(f.manifest, JSON.stringify({ name: 'pkg', main: './a.cjs' })) + f.manifests.invalidate(f.manifest) + expect(importerRequire.resolve('pkg')).toBe(join(f.dir, 'a.cjs')) + }) + + it('updates package type, scope, and nearest-package reads inside the directory', () => { + const f = fixture() + file(f.manifest, JSON.stringify({ name: 'pkg', type: 'commonjs' })) + const source = join(f.dir, 'lib', 'entry.js') + file(source, 'export {}') + expect(formats.defaultGetFormatWithoutErrors(pathToFileURL(source))).toBe('commonjs') + expect(reader.getNearestParentPackageJSON(source)?.data.type).toBe('commonjs') + + file(f.manifest, JSON.stringify({ name: 'pkg', type: 'module' })) + f.manifests.invalidate(f.manifest) + expect(formats.defaultGetFormatWithoutErrors(pathToFileURL(source))).toBe('module') + expect(reader.getNearestParentPackageJSON(source)?.data.type).toBe('module') + expect(reader.getPackageScopeConfig(pathToFileURL(source).href)).toMatchObject({ + exists: true, type: 'module', pjsonPath: toNamespacedPath(f.manifest), + }) + }) + + it('resolves changed package imports from inside the directory', () => { + const f = fixture() + file(f.manifest, JSON.stringify({ name: 'pkg', type: 'module', imports: { '#target': './a.mjs' } })) + const parent = pathToFileURL(join(f.dir, 'inside.mjs')).href + file(join(f.dir, 'inside.mjs'), 'export {}') + expect(resolveEsm('#target', parent)).toBe(pathToFileURL(join(f.dir, 'a.mjs')).href) + file(f.manifest, JSON.stringify({ name: 'pkg', type: 'module', imports: { '#target': './b.mjs' } })) + f.manifests.invalidate(f.manifest) + expect(resolveEsm('#target', parent)).toBe(pathToFileURL(join(f.dir, 'b.mjs')).href) + }) + + it('reports an invalid manifest with the native error and recovers after it is fixed', () => { + const f = fixture() + file(f.manifest, JSON.stringify({ name: 'pkg', exports: './a.mjs' })) + expect(resolveEsm('pkg', f.importerURL)).toBe(pathToFileURL(join(f.dir, 'a.mjs')).href) + file(f.manifest, '{') + f.manifests.invalidate(f.manifest) + expect(() => reader.read(f.manifest)).toThrow(expect.objectContaining({ code: 'ERR_INVALID_PACKAGE_CONFIG' })) + file(f.manifest, JSON.stringify({ name: 'pkg', exports: './b.mjs' })) + f.manifests.invalidate(f.manifest) + expect(resolveEsm('pkg', f.importerURL)).toBe(pathToFileURL(join(f.dir, 'b.mjs')).href) + }) + + it('leaves other package directories to Node and restores native readers on disposal', () => { + const f = fixture() + const other = join(f.root, 'packages', 'other', 'package.json') + file(other, JSON.stringify({ name: 'other', type: 'commonjs' })) + file(f.manifest, JSON.stringify({ name: 'pkg', type: 'commonjs' })) + expect(reader.read(other).type).toBe('commonjs') + const original = binding.readPackageJSON + f.manifests.invalidate(f.manifest) + file(other, JSON.stringify({ name: 'other', type: 'module' })) + expect(reader.read(other).type).toBe('commonjs') + expect(binding.readPackageJSON).not.toBe(original) + f.manifests.dispose() + expect(binding.readPackageJSON).toBe(original) + }) + + it('reports invalid JSON with its non-file ESM base after invalidation', async () => { + const f = fixture() + file(f.manifest, '{') + await f.invalidate(f.manifest) + const base = 'data:text/javascript,export{}' + const failure = outcome(() => reader.read(f.manifest, { isESM: true, base, specifier: 'pkg' })) + expect(failure).toHaveProperty('code', 'ERR_INVALID_PACKAGE_CONFIG') + expect(failure).toHaveProperty('message', expect.stringContaining(base)) + }) +}) + +function outcome(run: () => unknown): unknown { + try { return run() } catch (error) { + const failure = error as Error & { code?: string } + return { code: failure.code, message: failure.message } + } +} + +describe('package manifest reader parity', { concurrent: false }, () => { + it('terminates scope lookup after invalidating the filesystem root without modifying it', async () => { + const f = fixture() + const child = spawn(process.execPath, [ + '--expose-internals', + fileURLToPath(new URL('./fixtures/package-root-scope.mjs', import.meta.url)), + f.root, + ], { stdio: ['ignore', 'pipe', 'pipe'] }) + const completed = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => { + child.once('error', reject) + child.once('close', (code, signal) => { resolve({ code, signal }) }) + }) + cleanup.push(async () => { + if (child.exitCode === null && child.signalCode === null) child.kill() + await completed + }) + let stdout = '', stderr = '' + child.stdout.setEncoding('utf8').on('data', (chunk: string) => { stdout += chunk }) + child.stderr.setEncoding('utf8').on('data', (chunk: string) => { stderr += chunk }) + const result = await completed + expect(result.signal, stderr).toBeNull() + expect(result.code, stderr).toBe(0) + expect(stdout).toBe('scope traversal completed\n') + }) + + it('reads valid JSON with a non-file ESM base like Node', async () => { + const f = fixture() + file(f.manifest, '{}') + const options = { isESM: true, base: 'data:text/javascript,export{}', specifier: 'pkg' } + const expected = outcome(() => reader.read(f.manifest, options)) + await f.invalidate(f.manifest) + expect(outcome(() => reader.read(f.manifest, options))).toEqual(expected) + }) + + it('refreshes a cached real directory after a package link moves', async () => { + const f = fixture() + const other = join(f.root, 'packages', 'other') + const link = join(f.root, 'importer', 'node_modules', 'pkg') + const linkedManifest = join(link, 'package.json') + file(f.manifest, '{"type":"commonjs"}') + file(join(other, 'package.json'), '{"type":"commonjs"}') + unlinkSync(link) + symlinkSync(other, link, process.platform === 'win32' ? 'junction' : 'dir') + await f.invalidate(f.manifest) + expect(reader.read(linkedManifest).type).toBe('commonjs') + + unlinkSync(link) + symlinkSync(f.dir, link, process.platform === 'win32' ? 'junction' : 'dir') + file(f.manifest, '{"type":"module"}') + await f.invalidate(f.manifest) + expect(reader.read(linkedManifest).type).toBe('module') + }) + + it.each([ + '{}', + '{"name":"pkg","main":"./a.cjs","type":"commonjs"}', + '{"name":"pkg","exports":{"import":"./a.mjs","require":"./a.cjs"}}', + '{"name":"pkg","exports":false,"main":"./a.cjs"}', + '{"name":"pkg","imports":{"#item":"./a.mjs"},"type":"module"}', + '{"name":"pkg","exports":"./a.mjs"}', + '{', 'null', '[]', '1', '{"name":1}', '{"type":1}', '{"type":"other"}', + '{"name":"\\ud800"}', '{"main":"\\ud800"}', '{"type":"\\ud800"}', '{"exports":"\\ud800"}', + ])('reads %s like the native reader', (source) => { + const f = fixture() + file(f.manifest, source) + const expected = outcome(() => reader.read(f.manifest)) + f.manifests.invalidate(f.manifest) + expect(outcome(() => reader.read(f.manifest))).toEqual(expected) + }) + + it('reports an invalid manifest found while importing like the native resolver', () => { + const f = fixture() + const errorOf = (run: () => unknown): Error => { + try { run() } catch (error) { return error as Error } + throw new Error('expected the lookup to fail') + } + file(f.manifest, '{') + const expected = errorOf(() => resolveEsm('pkg', f.importerURL)) + f.manifests.invalidate(f.manifest) + const actual = errorOf(() => resolveEsm('pkg', f.importerURL)) + expect([String(actual), Object.keys(actual)]).toEqual([String(expected), Object.keys(expected)]) + expect(String(errorOf(() => reader.read(f.manifest)))).toBe(String(expected).replace(/ while importing .*(?=\.$)/u, '').replace(join(f.root, 'importer', 'node_modules', 'pkg'), f.dir)) + }) + + it('reads invalid UTF-8 like the native reader', () => { + const f = fixture() + mkdirSync(f.dir, { recursive: true }) + writeFileSync(f.manifest, Buffer.concat([Buffer.from('{"name":"'), Buffer.from([0xff]), Buffer.from('"}')])) + const expected = outcome(() => reader.read(f.manifest)) + f.manifests.invalidate(f.manifest) + expect(outcome(() => reader.read(f.manifest))).toEqual(expected) + }) + + it('refreshes a changed CommonJS main after a cached lookup', () => { + const f = fixture() + file(f.manifest, JSON.stringify({ name: 'pkg', main: './a.cjs' })) + const importerRequire = createRequire(f.importer) + expect(importerRequire.resolve('pkg')).toBe(join(f.dir, 'a.cjs')) + file(f.manifest, JSON.stringify({ name: 'pkg', main: './b.cjs' })) + expect(importerRequire.resolve('pkg')).toBe(join(f.dir, 'a.cjs')) + f.manifests.invalidate(f.manifest) + expect(importerRequire.resolve('pkg')).toBe(join(f.dir, 'b.cjs')) + }) + + it('updates the package type of extensionless files', () => { + const f = fixture() + file(f.manifest, JSON.stringify({ name: 'pkg', type: 'commonjs' })) + const extensionless = pathToFileURL(join(f.dir, 'bin', 'tool')) + file(join(f.dir, 'bin', 'tool'), '') + const before = formats.defaultGetFormatWithoutErrors(extensionless) + file(f.manifest, JSON.stringify({ name: 'pkg', type: 'module' })) + f.manifests.invalidate(f.manifest) + expect(formats.defaultGetFormatWithoutErrors(extensionless)).not.toBe(before) + expect(formats.defaultGetFormatWithoutErrors(extensionless)).toBe('module') + }) + + it('continues to the enclosing scope after the manifest is deleted', () => { + const f = fixture() + const enclosing = join(f.root, 'packages', 'package.json') + file(enclosing, JSON.stringify({ name: 'packages', type: 'commonjs' })) + file(f.manifest, JSON.stringify({ name: 'pkg', type: 'module' })) + const source = join(f.dir, 'lib', 'entry.js') + file(source, '') + expect(reader.getPackageScopeConfig(pathToFileURL(source).href).type).toBe('module') + rmSync(f.manifest) + f.manifests.invalidate(f.manifest) + expect(reader.getPackageScopeConfig(pathToFileURL(source).href)).toMatchObject({ + exists: true, type: 'commonjs', pjsonPath: toNamespacedPath(enclosing), + }) + expect(formats.defaultGetFormatWithoutErrors(pathToFileURL(join(f.dir, 'bin', 'tool')))).toBe( + formats.defaultGetFormatWithoutErrors(pathToFileURL(join(f.root, 'packages', 'tool'))), + ) + expect(reader.getNearestParentPackageJSON(source)?.path).toBe(toNamespacedPath(enclosing)) + }) + + it('stops a scope lookup at a node_modules boundary like Node', () => { + const f = fixture() + file(f.manifest, JSON.stringify({ name: 'pkg', type: 'module' })) + const nested = join(f.dir, 'node_modules', 'dep', 'entry.js') + file(nested, '') + const expected = [ + outcome(() => reader.getPackageScopeConfig(pathToFileURL(nested).href)), + outcome(() => reader.getNearestParentPackageJSON(nested)), + ] + f.manifests.invalidate(f.manifest) + expect([ + outcome(() => reader.getPackageScopeConfig(pathToFileURL(nested).href)), + outcome(() => reader.getNearestParentPackageJSON(nested)), + ]).toEqual(expected) + }) + + it('answers missing directories and non-file URLs natively', () => { + const f = fixture() + file(f.manifest, '{}') + const missing = join(f.root, 'missing', 'package.json') + const nativeScope = binding.getPackageScopeConfig as (url: string) => unknown + const expected = [outcome(() => reader.read(missing)), outcome(() => nativeScope('node:fs'))] + f.manifests.invalidate(f.manifest) + expect([outcome(() => reader.read(missing)), outcome(() => (binding.getPackageScopeConfig as (url: string) => unknown)('node:fs'))]) + .toEqual(expected) + }) + + it('falls back to the builtin addon for internals that --expose-internals does not expose', () => { + const f = fixture() + file(f.manifest, JSON.stringify({ name: 'pkg', main: './a.cjs' })) + const importerRequire = createRequire(f.importer) + expect(importerRequire.resolve('pkg')).toBe(join(f.dir, 'a.cjs')) + const argv = [...process.execArgv] + process.execArgv.push('--expose-internals') + try { + file(f.manifest, JSON.stringify({ name: 'pkg', main: './b.cjs' })) + f.manifests.invalidate(f.manifest) + } finally { + process.execArgv.splice(0, process.execArgv.length, ...argv) + } + expect(importerRequire.resolve('pkg')).toBe(join(f.dir, 'b.cjs')) + }) + + it('keeps readers replaced after installation when disposed', async () => { + const f = fixture() + file(f.manifest, '{}') + const previousRead = binding.readPackageJSON + const previousScope = binding.getPackageScopeConfig + const previousType = binding.getPackageType + const previousNearest = reader.getNearestParentPackageJSON + try { + await f.invalidate(f.manifest) + const later = () => undefined + binding.readPackageJSON = later + reader.getNearestParentPackageJSON = later + f.manifests.dispose() + expect(binding.readPackageJSON).toBe(later) + expect(reader.getNearestParentPackageJSON).toBe(later) + expect(binding.getPackageScopeConfig).toBe(previousScope) + expect(binding.getPackageType).toBe(previousType) + } finally { + f.manifests.dispose() + binding.readPackageJSON = previousRead + binding.getPackageScopeConfig = previousScope + binding.getPackageType = previousType + reader.getNearestParentPackageJSON = previousNearest + } + }) +}) diff --git a/packages/boot/hmr/tests/watch-config.spec.ts b/packages/boot/hmr/tests/watch-config.spec.ts index db31c0b654..7d0caf035b 100644 --- a/packages/boot/hmr/tests/watch-config.spec.ts +++ b/packages/boot/hmr/tests/watch-config.spec.ts @@ -321,6 +321,27 @@ describe('HMR exact config paths', () => { expect(warn).toHaveBeenCalledWith(failure) }) + it('absorbs a watcher error emitted after close removed its listeners', async () => { + const dir = mkdtempSync(join(tmpdir(), 'dsh-patch-close-error-')) + hmrRoots.push(dir) + const filename = join(dir, 'plugins.yml') + const ctx = await bootHmr(dir) + onTestFinished(() => ctx.fiber.dispose()) + const watcher = new FSWatcher() + const previousFactory = configWatch.create + onTestFinished(() => { configWatch.create = previousFactory }) + configWatch.create = () => { queueMicrotask(() => { watcher.emit('ready') }); return watcher } + const dispose = await watchConfig(ctx, filename, {}, () => {}) + await dispose() + expect(watcher.listenerCount('error')).toBe(1) + + // Mirror the real close(): it drops every listener before a pending write-settle poll fires. + expect(() => watcher.emit('error', Object.assign( + new Error("EPERM: operation not permitted, stat 'C:\\Temp\\dsh\\.credentials.yaml'"), + { code: 'EPERM', syscall: 'stat' }, + ))).not.toThrow() + }) + it('closes a ready watcher when its context has already been disposed', async () => { const dir = mkdtempSync(join(tmpdir(), 'dsh-patch-disposed-')) hmrRoots.push(dir) diff --git a/packages/boot/plugin-manager/README.i18n.yaml b/packages/boot/plugin-manager/README.i18n.yaml index fe36236ac3..7a2bca2062 100644 --- a/packages/boot/plugin-manager/README.i18n.yaml +++ b/packages/boot/plugin-manager/README.i18n.yaml @@ -15,8 +15,8 @@ en: 87679c51aa3693f5 zh: 11684a48a5806afb /deepseek-ai-dsh-plugin-manager/use-this-package: - en: 2cbe6e4a68994e2f - zh: ba0328f5538d8746 + en: af5bf0a205b5a2a1 + zh: 0114039816454a1a /deepseek-ai-dsh-plugin-manager/use-this-package/version-compatibility-and-exemptions: en: 920c0acb3b9feaf3 zh: ba6e832a256880a7 @@ -24,8 +24,8 @@ en: d5643a06f300c30a zh: 9348b6a7676a2849 /deepseek-ai-dsh-plugin-manager/understand-the-implementation: - en: 0f3d0c6c622577c1 - zh: 5a85f66e33977da3 + en: 7b54d30e231bb325 + zh: 08df9441fed9927f /deepseek-ai-dsh-plugin-manager/further-exploration: en: c6538243d8702a0d zh: 04a82d923353c75f @@ -45,11 +45,11 @@ en: c29f60be7f3edc1a zh: d9b9388b96ef95cc /deepseek-ai-dsh-plugin-manager/known-limitations-and-deferred-work: - en: fde2d9491fe5dd4f - zh: ef6e6db15c4f19d2 + en: 7d7ea29063e358ca + zh: e3399db77a97f62a /deepseek-ai-dsh-plugin-manager/known-limitations-and-deferred-work/failure-behavior: - en: 4b31f3638e4a6628 - zh: 23f6599a963bcf89 + en: 4447e4df8d62b274 + zh: c76443e2edb97b1d /deepseek-ai-dsh-plugin-manager/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/boot/plugin-manager/README.md b/packages/boot/plugin-manager/README.md index ddd74bcfa5..6763a7a9f8 100644 --- a/packages/boot/plugin-manager/README.md +++ b/packages/boot/plugin-manager/README.md @@ -41,10 +41,18 @@ A plugin toggle updates only `disabled` in the last matching override in the pro A selected bundle that cannot load remains in `listBundles` with an `error`; `enabled` records the saved selection, not successful loading. The plugin page shows the error and allows deselection. A broken bundle cannot be enabled. Management bundles remain protected if their files become unreadable. +Scoped bundle rows carry both their declared `rowId` and outer `preset` target. The manager reads declarations without composing the bundle alone, so rows remain visible while Off. Their optional `composition` state comes from the current preset inventory; they have no editable root `entryId`, and their `preset-managed` read-only reason directs configuration to the preset or bundle. Enabling audits every affected preset using its registered declaring row, including failures the registry retained instead of throwing; unrelated unchanged preset failures become warnings. Bundle toggles validate the saved composition even without HMR; composition failures keep the saved selection and return a diagnostic immediately. Remove refuses while a current or retained preset generation uses a contributed row, matched by defining preset row, child row ID, and resolved module, or any module belonging to the bundle package itself. This protection lasts after Off until those generations release the modules; unrelated rows using a shared dependency do not prevent removal. Anonymous scoped insertions receive the same protection. Missing module files do not make a different contribution match; identical retained declarations remain protected when their files are unavailable. + `listBundles` includes optional display `meta` for each bundle and its declared plugin rows, including disabled bundles. The Client selects a language from these values. The separate `description` field is the bundle's raw `package.json.description`; a metadata diagnostic does not block management. `source` gives a profile dependency as a spec `pnpm add` accepts: a git address or URL as recorded without the user information of an http(s) URL (a query string stays as recorded), a `file:` or `link:` path made absolute against the profile or the home directory for `~`, and a registry range, tag, or alias after the package name, as is any spec of a package installed under another name; a bundle the installation supplies has none, even when the profile also depends on it, because the installation's copy is the one that loads. The `plugin_manager` tool omits UI display metadata from list results. +`listBundles` marks shipped optional bundles and on-demand catalog entries as `official`, independently of package installation. `availability` identifies a readable installation-provided or profile package, or `missing`; on-demand catalog entries exclude undeclared transitive copies. `installed` only records whether the profile declares the dependency. An absent on-demand entry carries embedded localized metadata and a host-derived `installTarget` containing the running DSH version and install spec, without registry access. Profile-declared packages with unreadable files retain their declaration and expose the normal error. + `inspect(spec, options)` reads what a spec names before anything installs: a registry name is asked of the registry through `pnpm view`, run in the profile directory so the same proxy and authentication settings apply as to the install; an absolute path has its `package.json` read; a git address or tarball answers only its form and the `host` it is fetched from. The answer carries the name, version, description, whether the package declares a bundle, and the `registry` that answered, or a `problem`: `invalid-spec`, `already-installed`, `not-found`, `not-a-package`, `not-a-bundle`, `network`, or `unknown`, with the `registries` asked. A caller's `signal` or `inspectTimeoutMs` ends the lookup. +Published installations offer `package@`. Source Web and development Desktop offer `link:` from the running CLI installation, including its development symlink. Package names resolve through workspace manifests; current working directory and prerelease suffixes do not select a checkout. Missing packages, bundle patches, or built entry points produce a diagnostic without registry fallback. Prepare the checkout with `pnpm install` and `pnpm run build`. Linked packages use that checkout’s dependencies and peers; rebuilding and restarting remain necessary after code changes. A registry copy or another checkout’s link can need replacement even when its version matches. + +Official on-demand installs and explicit updates call the same `installBundle` operation as other bundles. Registry targets use `saveExact: true`; development targets retain the ordinary `link:` dependency. Updates pass the current `enabled` selection; replacing an existing dependency reports `restart-required`. Turning Off only deselects the layer, and Remove uses the existing removal checks. Installation failure restores the profile manifest and lockfile; activation failure after installation can retain the saved dependency and selection. The existing peer-compatibility check still applies, without an additional Official-only version admission rule. + `installBundle` checks GitHub repositories with `git ls-remote` before starting pnpm, using the profile directory and the installer's Git and proxy configuration. `githubConnectionTimeoutMs` defaults to 5000 ms and limits only this check, not package download or builds. The check disables credential helpers and prompts; only network failures and timeouts stop installation, reporting `failedAt: 'spec-host'` with the existing failure kind and diagnostic log. Authentication, repository lookup and other failures are left to pnpm, including its HTTPS-to-SSH fallback. Cancellation and manager disposal stop the check and its descendants. Registry packages, paths, tarballs and other Git hosts skip this check. A reachable repository can still fail during download or bundle validation. Registries are asked in turn. The plan starts at `options.registry`, else the configured `registry` (`null` is the one pnpm's own configuration names), and continues through `fallbackRegistries` while a registry is unreachable, times out, or answers that it has no such package or version, which a mirror not yet synced does. A registry outside the configured set is asked alone, so a private registry never falls through to a public one; pnpm's own registry counts as part of the set only while what it names, read through `pnpm config get registry` before each plan, is npm's own registry or one of the fallbacks, and is otherwise asked alone as a private one. A registry pnpm's own configuration already names is asked once. The lookup runs `pnpm view` with `--registry` and without pnpm's own retries, so a dead registry is reported within `inspectTimeoutMs` and the next one is asked; pnpm prints its refusal as JSON on stdout, which is read like stderr. The install keeps pnpm's retry settings. `registries()` answers the configured set and what pnpm names, for a picker. The [registry Agent Note](../../../.agents/notes/implemented/architecture/2026-09-18-plugin-install-registries.md) owns the rationale. @@ -55,7 +63,7 @@ The browser-safe `@deepseek-ai/dsh-plugin-manager/registry` entry exports `OFFIC `waitForInstall(requestId)` lets a client recover a lost response by waiting for the active installation, including its non-cancellable application phase. It returns the same outcome as the original call, or `null` if the request is not active. Completed results are not retained; `null` establishes neither success nor cancellation. -When pnpm 11 blocks dependency scripts, the failed installation reports every pending package name in the profile under `pendingBuilds`, including names left by earlier attempts; a failed run restores `package.json` and `pnpm-lock.yaml` but deliberately not `pnpm-workspace.yaml`, where pnpm records them. The Web plugin page offers **Allow these scripts and retry**; the tool can grant permission on the user's behalf through `approvedBuilds` on `install_bundle`, after the user approves those scripts in the conversation. The service validates pending names; it does not verify conversation approval. Approval persists by package name in this profile, permits commands with the host user's permissions, and survives another installation failure. Only currently undecided names can be approved; existing denials and wildcard rules cannot be overridden through this action. Approval rejects YAML anchors or aliases inside `allowBuilds`. Retry preserves the original activation choice. +When pnpm 11 blocks dependency scripts, the failed installation reports every pending package name in the profile under `pendingBuilds`, including names left by earlier attempts and names the run reported as ignored. pnpm writes those entries into `pnpm-workspace.yaml` only when it can prompt, so the manager records the reported names itself; a failed run restores `package.json` and `pnpm-lock.yaml` but deliberately not `pnpm-workspace.yaml`. The Web plugin page offers **Allow these scripts and retry**; the tool can grant permission on the user's behalf through `approvedBuilds` on `install_bundle`, after the user approves those scripts in the conversation. The service validates pending names; it does not verify conversation approval. Approval persists by package name in this profile, permits commands with the host user's permissions, and survives another installation failure. Only currently undecided names can be approved; existing denials and wildcard rules cannot be overridden through this action. Approval rejects YAML anchors or aliases inside `allowBuilds`. Retry preserves the original activation choice. ### Version compatibility and exemptions @@ -89,9 +97,13 @@ The CLI exposes `dsh plugin --profile version-exemptions`, `allow-vers
Implementation internals — click to expand -The service and `dsh plugin` share the package operations in [operations.ts](src/operations.ts). The launcher supplies the current profile; [DSH HMR](../hmr/README.md) serializes module reloads, file watching and management writes. Each refresh re-reads bundle selection and patch layers, updates the original root Include, and awaits removed plugin resources as well as the remaining Loader tree. CLI and service operations share the profile manifest writer lock to prevent concurrent package and manifest writes. HMR does not acquire that lock. Pnpm runs outside the HMR queue; installation selects the bundle after pnpm succeeds, while removal deselects and unloads the bundle before pnpm runs. A service run whose captured output stays silent for `idleTimeoutMs` is terminated, reports `timedOut` alongside its exit status, is classified `timeout` whatever status the signal left behind, and is not retried on the next registry, which bounds how long one operation can hold the profile lock; the CLI inherits the terminal, captures no output, and stays unbounded for its operator to interrupt. A run completes when its process exits, and the pipes then drain under a bounded grace period, so a descendant that inherited them cannot hold the operation open. A terminated run stops its whole process tree and waits for it, because a lifecycle script outlives the pnpm process that started it (issue #4981). Each operation records the pnpm run it starts in `.plugin-manager/run.json` and removes the record when the run ends. A lock whose holder process exited is taken over by the next writer, but that process's pnpm tree can still be running, so an operation that finds a record waits up to five seconds for the recorded run to stop and otherwise fails with a diagnostic that names the process and the record, without running pnpm. Dependency-only changes do not trigger configuration reloads. +The service and `dsh plugin` share the package operations in [operations.ts](src/operations.ts). The launcher supplies the current profile; [DSH HMR](../hmr/README.md) serializes module reloads, file watching and management writes. Each configuration refresh re-reads bundle selection and patch layers, updates the original root Include, and awaits removed plugin resources as well as the remaining Loader tree. CLI and service operations share the profile manifest writer lock to prevent concurrent package and manifest writes. HMR does not acquire that lock. Pnpm runs outside the HMR queue; installation selects the bundle after pnpm succeeds, while removal deselects and unloads the bundle before pnpm runs. -Results contain the last attempted stage, target, saved-state change, application status and error codes. Web dictionaries render management text; pnpm and Loader diagnostics remain unmodified. Unrelated pre-existing inactive entries return warnings; new or changed failures and inactive explicit enablement targets fail the operation. A failed or cancelled installation restores the manifest and lockfile it snapshotted before pnpm ran ([rationale](../../../.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.md)); a failed removal retains its partial changes and diagnostics. Installations are tracked by request id until their call settles, so a cancellation names one run and joins its settlement without taking the profile lock. The CLI inherits authentication variables and terminal descriptors; service operations use a scrubbed environment and captured output. +Installing a new package, enabling a bundle, disabling it with HMR after its plugins stop, and a successful removal publish the profile's runtime resolution. Shared dependencies may pass to another bundle's declaration when their normalized directory, version, and scope stay unchanged. Without HMR, a deselected startup bundle still runs: while any such bundle is absent from the saved selection, subsequent package operations retain the entire current table and report `restart-required`. Other new installations and removals of non-running bundles can still publish without HMR; activation waits for restart. Overwriting an installed package publishes nothing and reports `restart-required` ([decision](../../../.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.md)). + +A service run whose captured output stays silent for `idleTimeoutMs` is terminated, reports `timedOut` alongside its exit status, is classified `timeout` whatever status the signal left behind, and is not retried on the next registry, which bounds how long one operation can hold the profile lock; the CLI inherits the terminal, captures no output, and stays unbounded for its operator to interrupt. A run completes when its process exits, and the pipes then drain under a bounded grace period, so a descendant that inherited them cannot hold the operation open. A terminated run stops its whole process tree and waits for it, because a lifecycle script outlives the pnpm process that started it (issue #4981). Each operation records the pnpm run it starts in `.plugin-manager/run.json` and removes the record when the run ends. A lock whose holder process exited is taken over by the next writer, but that process's pnpm tree can still be running, so an operation that finds a record waits up to five seconds for the recorded run to stop and otherwise fails with a diagnostic that names the process and the record, without running pnpm. Dependency-only changes do not trigger configuration reloads. + +Results contain the last attempted stage, target, saved-state change, application status and error codes. Web dictionaries render management text; pnpm and Loader diagnostics remain unmodified. Unrelated pre-existing inactive entries return warnings; new or changed failures and inactive explicit enablement targets fail the operation. A failed or cancelled package run, or failed bundle validation, restores the manifest and lockfile snapshotted before pnpm ran ([rationale](../../../.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.md)); later failures retain saved changes as described in [Failure behavior](#failure-behavior). Installations are tracked by request id until their call settles, so a cancellation names one run and joins its settlement without taking the profile lock. The CLI inherits authentication variables and terminal descriptors; service operations use a scrubbed environment and captured output.
@@ -129,7 +141,7 @@ Tool results append to the transcript. Enabling or disabling other tools can cha - Web approves the entire displayed pending group; it has no per-package selection. - Package replacements require restarting the process to load a fresh JavaScript module generation. - Startup-only profiles cannot remove packages used to start the current process; stop it and use `dsh plugin`. -- The manager cannot disable its own management components, change another profile, or edit an agent preset's composition. +- The manager cannot disable its own management components, change another profile, or directly edit individual rows inside an agent preset. - A failed removal may leave dependencies partially changed, and a failed or cancelled installation can leave downloaded files under `node_modules` or the pnpm store. Inactive dependencies with missing files remain removable. Diagnostic logs remain under the profile's `.plugin-manager/logs` directory. - Management results describe Host activation. Browser synchronization failures appear separately in the Settings plugin list. - Desktop package operations remain owned by the Desktop shell. @@ -143,9 +155,10 @@ Failures preserve completed steps and report the actual remaining state. Profile |---|---| | Install: pnpm or bundle validation fails | Restore `package.json` and `pnpm-lock.yaml` as snapshotted before pnpm ran; files pnpm downloaded may remain. Report installation failure. | | Enable: saving selection or loading fails | Keep the installed dependency and any saved selection. Report enablement failure; allow repair, disablement or removal. | +| Resolution publication after a successful package operation fails | Keep completed disk changes and the previous runtime table. Report `application: 'failed'` with the publication diagnostic; pnpm's zero exit code does not mean publication succeeded. | | Remove: any step fails | Stop at the failed step. Preserve completed changes, retain remaining dependencies for retry, and report removal failure. Do not re-enable the bundle. | -Installation finishes after pnpm and bundle validation succeed; subsequent enablement failure does not undo installation. Removal proceeds in order: remove the bundle from `dsh.profile.bundles`, unload its runtime contributions, then run `pnpm remove`. A failed step prevents subsequent steps. A selected name that neither the profile's dependencies nor the installation holds, such as a retired bundle, is also removable; its removal stops after deselection and runs no pnpm. +Installation finishes after pnpm and bundle validation succeed; subsequent publication or enablement failure does not undo installation. Removal proceeds in order: remove the bundle from `dsh.profile.bundles`, unload its runtime contributions, run `pnpm remove`, then publish runtime resolution unless a deselected startup bundle still runs without HMR. A failed step prevents subsequent steps; publication failure after removal does not restore deleted packages. A selected name that neither the profile's dependencies nor the installation holds, such as a retired bundle, is also removable; its removal stops after deselection and runs no pnpm. Restoration rewrites only the two snapshotted files; user-authored patch configuration, application data, diagnostic logs and files pnpm downloaded remain untouched, and the next package operation prunes packages no manifest references. diff --git a/packages/boot/plugin-manager/README.zh.md b/packages/boot/plugin-manager/README.zh.md index 9e9c119fb0..ba365135f2 100644 --- a/packages/boot/plugin-manager/README.zh.md +++ b/packages/boot/plugin-manager/README.zh.md @@ -41,10 +41,18 @@ kind: "package-reference" 已选择但无法加载的组合包仍会出现在 `listBundles` 中,并携带 `error`;`enabled` 表示保存的选择,不代表加载成功。插件页面显示错误并允许取消选择。损坏的组合包无法启用。管理组合包的文件变得不可读后仍受保护。 +限定到 preset 的组合包行同时携带声明的 `rowId` 和外层 `preset` 目标。管理器读取声明,不单独组合该 bundle,因此 Off 状态下仍能列出这些行。可选的 `composition` 状态来自当前 preset 清单;这些行没有可编辑的根条目 `entryId`,其 `preset-managed` 只读原因指引用户通过预设或组合包进行配置。启用根据注册的声明行检查所有受影响的 preset,包括注册表保留而未抛出的故障;无关且未变化的 preset 故障作为警告返回。即使没有 HMR,组合包开关也会校验保存后的组合;组合失败会保留已保存的选择并立即返回诊断。当前或被保留的 preset 代仍使用贡献行(按定义 preset 的行、子行 ID 和解析后的模块匹配),或属于组合包自身的任意模块时,Remove 都会拒绝。这项保护在 Off 后仍持续,直到这些代释放模块;无关行使用共享依赖不会阻止移除。匿名作用域插入行受到相同保护。模块文件缺失不会导致另一贡献项被误认为匹配;文件不可用时,相同的保留声明仍受保护。 + `listBundles` 为各组合包及其声明的插件行提供可选的展示 `meta`,包括已禁用的组合包。Client 从这些值中选择语言。单独的 `description` 字段是该组合包原始的 `package.json.description`;元信息诊断不会阻止管理操作。`source` 以 `pnpm add` 接受的 spec 给出 profile 依赖:git 地址或 URL 按记录给出,但去掉 http(s) URL 的 user information(查询字符串保持原样);`file:` 或 `link:` 路径按 profile 目录解析为绝对路径,`~` 展开为主目录;注册表版本范围、标签或别名接在包名的 `@` 之后,以别名安装的包的 spec 也是如此。安装提供的组合包没有该字段,即使 profile 也依赖它,因为实际加载的是安装中的副本。`plugin_manager` 工具的列表结果不包含 UI 展示元信息。 +`listBundles` 将随附可选组合包和按需目录项标为 `official`,与包的安装状态独立。`availability` 表示可读取的安装随附包、profile 包,或 `missing`;按需目录项不计入未声明的传递依赖副本。`installed` 仅表示 profile 是否声明该依赖。尚未安装的按需条目携带内嵌的本地化元信息,以及 Host 生成、包含当前 DSH 版本与安装 spec 的 `installTarget`,无需访问注册表。profile 已声明但文件不可读的包保留依赖声明,并展示原有错误。 + `inspect(spec, options)` 在任何东西安装之前读出 spec 指向什么:注册表包名通过 `pnpm view` 询问注册表,在 profile 目录中运行,因而与安装使用同样的代理与认证设置;绝对路径读取其 `package.json`;git 地址或 tarball 只答复自己的形式和它被拉取的 `host`。答复携带名称、版本、描述、该包是否声明组合包,以及作答的 `registry`,否则给出 `problem`:`invalid-spec`、`already-installed`、`not-found`、`not-a-package`、`not-a-bundle`、`network` 或 `unknown`,并附上问过的 `registries`。调用方的 `signal` 或 `inspectTimeoutMs` 会结束查询。 +已发布的安装提供 `package@<当前 DSH 版本>`。源码 Web 与开发 Desktop 提供 `link:<工作区包的绝对目录>`,目录来自当前 CLI 的安装位置,包括其开发符号链接。包名通过工作区清单解析;当前工作目录和预发布版本后缀不用于选择 checkout。包、bundle patch 或已构建入口缺失时报告诊断,不回退到注册表。使用 `pnpm install` 和 `pnpm run build` 准备 checkout。链接包使用该 checkout 的依赖与 peer;代码改变后仍需重新构建并重启。即使版本相同,注册表副本或其他 checkout 的链接也可能需要替换。 + +官方按需安装与显式更新调用和其他组合包相同的 `installBundle` 操作。注册表目标使用 `saveExact: true`;开发目标保留普通 `link:` 依赖。更新传入当前 `enabled` 选择状态;替换已有依赖会报告 `restart-required`。关闭仅取消选择配置层,移除使用原有移除检查。安装失败恢复 profile 清单和锁文件;安装成功后激活失败可能保留已保存的依赖和选择。原有 peer 兼容性检查继续生效,不增加仅针对官方条目的版本准入规则。 + `installBundle` 在启动 pnpm 前通过 `git ls-remote` 检查 GitHub 仓库,使用 profile 目录及安装器的 Git 与代理配置。`githubConnectionTimeoutMs` 默认为 5000 毫秒,只限制这次检查,不限制包下载或构建。检查禁用凭据助手和认证提示;只有网络失败与超时会停止安装,通过现有失败类型与诊断日志返回,并标记 `failedAt: 'spec-host'`。认证、仓库查找及其他失败继续交给 pnpm,包括其 HTTPS 到 SSH 的回退。取消安装或销毁管理器会停止检查及其子进程。注册表包、本地路径、压缩包和其他 Git 主机跳过此检查。仓库可达后,下载或组合包验证仍可能失败。 注册表按顺序询问。计划从 `options.registry` 开始,否则从配置的 `registry`(`null` 即 pnpm 自身配置指定的那个)开始,并在一个注册表不可达、超时或答复没有这个包或版本(尚未同步的镜像会如此)时继续问 `fallbackRegistries`。配置集合之外的注册表只问它自己,因而私有源永远不会落到公共源;pnpm 自身的注册表只在它指向的地址(每次做计划前经 `pnpm config get registry` 读出)是 npm 官方源或某个备选源时才算集合成员,否则视为私有源只问它自己。pnpm 自身配置已经指向的注册表只问一次。查询以 `--registry` 运行 `pnpm view` 且不带 pnpm 自身的重试,所以死掉的注册表会在 `inspectTimeoutMs` 内报告并转问下一个;pnpm 把拒绝以 JSON 打印在 stdout,读法与 stderr 相同。安装保留 pnpm 的重试设置。`registries()` 把配置集合和 pnpm 指向的地址答复给选择器。[注册表 Agent Note](../../../.agents/notes/implemented/architecture/2026-09-18-plugin-install-registries.zh.md) 拥有理由。 @@ -55,7 +63,7 @@ kind: "package-reference" `waitForInstall(requestId)` 让客户端在响应丢失后等待活动安装的结果,包括不可取消的应用阶段。它返回与原调用相同的结果,请求不在活动中时返回 `null`。已完成的结果不保留;`null` 不表示成功或已取消。 -pnpm 11 拦下依赖脚本时,失败的安装在 `pendingBuilds` 里报告 profile 中所有待决定的包名,包括先前尝试留下的;失败的运行会恢复 `package.json` 与 `pnpm-lock.yaml`,但有意不恢复 pnpm 记录这些名字的 `pnpm-workspace.yaml`。Web 插件页提供**允许这些脚本并重试**;工具可以在用户于对话中批准这些脚本后,通过 `install_bundle` 的 `approvedBuilds` 代为授权。服务只校验待决定的名字,不核实对话中的批准。授权按包名保存在当前 profile,允许以宿主用户的权限执行命令,并在再次安装失败后保留。只能批准当前未决定的名字;已有的拒绝与通配规则不能通过此操作覆盖。`allowBuilds` 里出现 YAML 锚点或别名时拒绝授权。重试保留原来的启用选择。 +pnpm 11 拦下依赖脚本时,失败的安装在 `pendingBuilds` 里报告 profile 中所有待决定的包名,包括先前尝试留下的,以及本次运行报告为已忽略的。pnpm 只在能够交互提示时才把这些条目写入 `pnpm-workspace.yaml`,因此由管理器自行记录本次运行报告的包名;失败的运行会恢复 `package.json` 与 `pnpm-lock.yaml`,但有意不恢复 `pnpm-workspace.yaml`。Web 插件页提供**允许这些脚本并重试**;工具可以在用户于对话中批准这些脚本后,通过 `install_bundle` 的 `approvedBuilds` 代为授权。服务只校验待决定的名字,不核实对话中的批准。授权按包名保存在当前 profile,允许以宿主用户的权限执行命令,并在再次安装失败后保留。只能批准当前未决定的名字;已有的拒绝与通配规则不能通过此操作覆盖。`allowBuilds` 里出现 YAML 锚点或别名时拒绝授权。重试保留原来的启用选择。 ### 版本兼容性与豁免 @@ -89,9 +97,13 @@ CLI 提供 `dsh plugin --profile version-exemptions`、`allow-version
实现细节——点击展开 -服务与 `dsh plugin` 共用 [operations.ts](src/operations.ts) 中的包管理操作。启动器提供当前 profile;[DSH HMR](../hmr/README.zh.md) 串行执行模块重载、文件监听和管理写入。每次刷新重新读取组合包选择与 patch 层,更新原有根 Include,并等待已移除插件释放资源及剩余 Loader 树稳定。CLI 与 service 操作共用 profile manifest 写锁,防止并发包操作和 manifest 写入。HMR 不获取该锁。pnpm 在 HMR 队列之外执行;安装在 pnpm 成功后选入组合包,删除则在执行 pnpm 前取消选入并完成卸载。service 运行若在 `idleTimeoutMs` 内没有任何捕获输出即被终止,与退出状态一并报告 `timedOut`,不论信号留下什么退出状态都归类为 `timeout`,且不再转问下一个注册表,因此单次操作占用 profile 锁的时长有上界;CLI 继承终端、不捕获输出,因此不受此上界约束,由操作者中断。运行以进程退出为完成点,随后只在一个有界的宽限窗口内排空管道,因此继承管道的孙进程无法让操作挂起。被终止的运行会停止整棵进程树并等待其消失,因为生命周期脚本的存活时间超过启动它的 pnpm 进程(issue #4981)。每个操作把它启动的 pnpm 运行记录在 `.plugin-manager/run.json` 中,并在运行结束时删除该记录。持有者进程已退出的锁会被下一个写入方接管,但该进程的 pnpm 进程树可能仍在运行,因此发现记录的操作最多等待五秒让记录中的运行停止,否则不运行 pnpm,并以指明该进程与记录文件的诊断失败。仅依赖字段变化不会触发配置重载。 +服务与 `dsh plugin` 共用 [operations.ts](src/operations.ts) 中的包管理操作。启动器提供当前 profile;[DSH HMR](../hmr/README.zh.md) 串行执行模块重载、文件监听和管理写入。每次配置刷新重新读取组合包选择与 patch 层,更新原有根 Include,并等待已移除插件释放资源及剩余 Loader 树稳定。CLI 与 service 操作共用 profile manifest 写锁,防止并发包操作和 manifest 写入。HMR 不获取该锁。pnpm 在 HMR 队列之外执行;安装在 pnpm 成功后选入组合包,删除则在执行 pnpm 前取消选入并完成卸载。 -结果包含最后尝试的阶段、目标、磁盘变化、应用状态和错误码。Web 词典呈现管理文案;pnpm 与 Loader 的诊断保持原样。无关的已有故障作为警告返回;新出现、配置变化后的故障,以及显式启用目标未激活,都会使操作失败。失败或被取消的安装会恢复 pnpm 运行前快照的 manifest 与 lockfile([理由](../../../.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.zh.md));失败的删除保留部分改动和诊断。安装按 request id 跟踪到调用结束,因此取消只针对一次运行,并且不取 profile 锁就能等待它结束。CLI 继承认证环境和终端描述符;service 使用清理后的环境并捕获输出。 +安装新包、启用组合包、有 HMR 时停用组合包并等插件退出后、以及删除成功后,都会发布 profile 的 runtime resolution。共享依赖的规范化目录、版本和作用域不变时,可以改由另一个组合包声明。没有 HMR 时,已取消选入的启动组合包仍在运行:只要保存的选择列表中缺少任何这样的组合包,后续包操作就保留整张当前表,并报告 `restart-required`。其他新安装和未运行组合包的删除仍可在没有 HMR 时发布;激活等待重启。覆盖已安装的包不发布,并报告 `restart-required`([决策](../../../.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md))。 + +service 运行若在 `idleTimeoutMs` 内没有任何捕获输出即被终止,与退出状态一并报告 `timedOut`,不论信号留下什么退出状态都归类为 `timeout`,且不再转问下一个注册表,因此单次操作占用 profile 锁的时长有上界;CLI 继承终端、不捕获输出,因此不受此上界约束,由操作者中断。运行以进程退出为完成点,随后只在一个有界的宽限窗口内排空管道,因此继承管道的孙进程无法让操作挂起。被终止的运行会停止整棵进程树并等待其消失,因为生命周期脚本的存活时间超过启动它的 pnpm 进程(issue #4981)。每个操作把它启动的 pnpm 运行记录在 `.plugin-manager/run.json` 中,并在运行结束时删除该记录。持有者进程已退出的锁会被下一个写入方接管,但该进程的 pnpm 进程树可能仍在运行,因此发现记录的操作最多等待五秒让记录中的运行停止,否则不运行 pnpm,并以指明该进程与记录文件的诊断失败。仅依赖字段变化不会触发配置重载。 + +结果包含最后尝试的阶段、目标、磁盘变化、应用状态和错误码。Web 词典呈现管理文案;pnpm 与 Loader 的诊断保持原样。无关的已有故障作为警告返回;新出现、配置变化后的故障,以及显式启用目标未激活,都会使操作失败。包管理运行失败或被取消,或组合包校验失败时,会恢复 pnpm 运行前快照的 manifest 与 lockfile([理由](../../../.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.zh.md));后续失败按[失败行为](#failure-behavior)所述保留已保存的改动。安装按 request id 跟踪到调用结束,因此取消只针对一次运行,并且不取 profile 锁就能等待它结束。CLI 继承认证环境和终端描述符;service 使用清理后的环境并捕获输出。管理器直接读取文件和 Loader 状态,不维护第二份目标状态注册表,因此不发布单独的运行时不变式伴生入口。
@@ -129,7 +141,7 @@ CLI 提供 `dsh plugin --profile version-exemptions`、`allow-version - Web 一次批准显示出来的整组待决定包,没有逐包选择。 - 替换已有包后需要重启进程,以加载新的 JavaScript 模块版本。 - 仅启动时加载的 profile 不能删除当前进程启动时使用的包;停止进程后使用 `dsh plugin`。 -- 管理器不能关闭自身所需的管理组件、修改其他 profile 或编辑 agent 预设组合。 +- 管理器不能关闭自身所需的管理组件、修改其他 profile 或直接编辑 agent 预设内部的单个条目。 - 失败的删除可能留下部分依赖改动,失败或被取消的安装可能在 `node_modules` 或 pnpm 缓存中留下已下载文件。文件缺失的未启用依赖仍可删除。诊断日志保留在 profile 的 `.plugin-manager/logs` 目录中。 - 管理结果描述 Host 激活状态。浏览器同步失败会在设置的插件列表中单独显示。 - Desktop 包管理操作仍由 Desktop shell 负责。 @@ -143,9 +155,10 @@ CLI 提供 `dsh plugin --profile version-exemptions`、`allow-version |---|---| | 安装:pnpm 执行或组合包校验失败 | 恢复 pnpm 运行前快照的 `package.json` 与 `pnpm-lock.yaml`;pnpm 已下载的文件可能保留。报告安装失败。 | | 启用:保存选择项或加载失败 | 保留已安装的依赖和已保存的选择项。报告启用失败,允许修正、停用或卸载。 | +| 包操作成功后解析表发布失败 | 保留已完成的磁盘改动和上一张运行时表。报告 `application: 'failed'` 及发布诊断;pnpm 的零退出码不代表发布成功。 | | 卸载:任一步失败 | 停在失败步骤,保留已完成的改动和待重试删除的依赖,报告卸载失败。不重新启用组合包。 | -pnpm 执行和组合包校验成功即完成安装,后续启用失败不撤销安装。卸载依次执行:从 `dsh.profile.bundles` 移除组合包、卸载运行时贡献、执行 `pnpm remove`。任一步失败都不继续执行后续步骤。profile 依赖和安装都不持有的已选名称(例如已下线的组合包)同样可卸载;其卸载在取消选择后结束,不运行 pnpm。 +pnpm 执行和组合包校验成功即完成安装,后续发布或启用失败不撤销安装。卸载依次执行:从 `dsh.profile.bundles` 移除组合包、卸载运行时贡献、执行 `pnpm remove`,然后发布 runtime resolution,除非没有 HMR 且已取消选入的启动组合包仍在运行。任一步失败都不继续执行后续步骤;删除后的发布失败不会恢复已删除的包。 profile 依赖和安装都不持有的已选名称(例如已下线的组合包)同样可卸载;其卸载在取消选择后结束,不运行 pnpm。 恢复只重写这两份快照文件;用户编写的 patch 配置、应用数据、诊断日志以及 pnpm 已下载的文件保持原样,没有 manifest 引用的包由下一次包操作清理。 diff --git a/packages/boot/plugin-manager/package.json b/packages/boot/plugin-manager/package.json index 6e6104d60a..4a83a1781a 100644 --- a/packages/boot/plugin-manager/package.json +++ b/packages/boot/plugin-manager/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plugin-manager", "description": "Current-profile plugin and bundle management shared by dsh CLI, Web and agent tools", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -59,7 +59,7 @@ "@deepseek-ai/dsh-app-boot": "workspace:*", "@deepseek-ai/dsh-atomic-write": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "execa": "^10.0.0", + "execa": "^10.0.1", "yaml": "^2.9.0", "zod": "^4.4.3", "@deepseek-ai/dsh-util-values": "workspace:*" @@ -75,7 +75,8 @@ "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/dsh-sandbox-policy": "workspace:*", "@deepseek-ai/dsh-sandbox": "workspace:*", - "@deepseek-ai/dsh-user-approval": "workspace:*" + "@deepseek-ai/dsh-user-approval": "workspace:*", + "@deepseek-ai/dsh-agent-preset-registry": "workspace:*" }, "peerDependenciesMeta": { "@deepseek-ai/dsh-hmr": { @@ -83,9 +84,13 @@ }, "@deepseek-ai/dsh-user-approval": { "optional": true + }, + "@deepseek-ai/dsh-agent-preset-registry": { + "optional": true } }, "devDependencies": { + "chokidar": "^4.0.3", "@deepseek-ai/dsh-package-manifest": "workspace:*", "@deepseek-ai/cordis": "workspace:~", "@deepseek-ai/cordis-plugin-loader": "workspace:~", @@ -102,6 +107,10 @@ "@deepseek-ai/dsh-session": "workspace:*", "@deepseek-ai/dsh-session-projection": "workspace:*", "@deepseek-ai/dsh-sandbox": "workspace:*", - "@deepseek-ai/dsh-user-approval": "workspace:*" + "@deepseek-ai/dsh-user-approval": "workspace:*", + "@deepseek-ai/dsh-agent-preset-registry": "workspace:*", + "@deepseek-ai/dsh-agent-preset": "workspace:*", + "@deepseek-ai/dsh-scope": "workspace:*", + "@deepseek-ai/dsh-session-title": "workspace:*" } } diff --git a/packages/boot/plugin-manager/src/build-approval.ts b/packages/boot/plugin-manager/src/build-approval.ts index 5ea1569292..75600f8c22 100644 --- a/packages/boot/plugin-manager/src/build-approval.ts +++ b/packages/boot/plugin-manager/src/build-approval.ts @@ -5,6 +5,12 @@ import { isAlias, isMap, isNode, isScalar, parseDocument, visit } from 'yaml' import { ManagementFailure } from './failure.ts' import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write' +/** Value pnpm leaves for a build script that still needs a decision. */ +const UNDECIDED_BUILD = 'set this to true or false' + +/** Where a non-interactive run names the build scripts it skipped. */ +const IGNORED_BUILDS = /\bIgnored build scripts: (.+)/ + async function readPolicy(dir: string) { let text: string try { text = await readFile(join(dir, 'pnpm-workspace.yaml'), 'utf8') } @@ -22,18 +28,43 @@ async function readPolicy(dir: string) { throw new Error('allowBuilds must not contain YAML anchors or aliases') } }) + const keys = isMap(builds) ? builds.items.flatMap(({ key }) => + isScalar(key) && typeof key.value === 'string' ? [key.value] : []) : [] const pending = isMap(builds) ? builds.items.flatMap(({ key, value }) => isScalar(key) && typeof key.value === 'string' && !/[*?]/.test(key.value) - && isScalar(value) && value.value === 'set this to true or false' ? [key.value] : []) : [] - return { document, pending } + && isScalar(value) && value.value === UNDECIDED_BUILD ? [key.value] : []) : [] + return { document, pending, keys } } -/** Read package names left undecided by pnpm 11, including after installation cleanup. +/** + * Record the build scripts a failed run reported as ignored, then list every name awaiting a decision. + * + * pnpm writes those policy entries itself only when it can prompt, and the + * manager always runs it non-interactively, so the run's own report is the + * record that survives. * @param dir Current profile directory. + * @param output Captured output of the failed run. * @returns Exact package names awaiting a build decision; wildcard rules are excluded. */ -export async function readPendingBuilds(dir: string): Promise { - return (await readPolicy(dir)).pending +export async function recordPendingBuilds(dir: string, output: string): Promise { + const { document, pending, keys } = await readPolicy(dir) + const known = new Set(keys) + const added = ignoredBuildNames(output).filter(name => !known.has(name)) + if (added.length === 0) return pending + for (const name of added) document.setIn(['allowBuilds', name], UNDECIDED_BUILD) + await writeFileAtomic(join(dir, 'pnpm-workspace.yaml'), String(document), { mode: 0o600 }) + return [...pending, ...added] +} + +/** + * Read the package names a pnpm run reported as ignored build scripts. + * @param output Captured output of the run. + * @returns Exact names in report order; wildcard patterns are excluded. + */ +export function ignoredBuildNames(output: string): string[] { + const reported = IGNORED_BUILDS.exec(output)?.[1] + if (reported === undefined) return [] + return reported.split(',').map(name => name.trim()).filter(name => name !== '' && !/[*?]/.test(name)) } /** Persist approval without running scripts; the caller holds the profile manifest lock. diff --git a/packages/boot/plugin-manager/src/bundle-rows.ts b/packages/boot/plugin-manager/src/bundle-rows.ts new file mode 100644 index 0000000000..9a150f3bd7 --- /dev/null +++ b/packages/boot/plugin-manager/src/bundle-rows.ts @@ -0,0 +1,54 @@ +/** Raw bundle declarations remain inspectable while their prerequisite layers are disabled. */ +import { existsSync } from 'node:fs' +import { dirname, isAbsolute, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader' +import { realModuleFile, profilePatchPreset, type ProfilePatch, type PluginPackages } from '@deepseek-ai/dsh-app-boot' + +/** A declared child row and the outer preset operation that contributes it. */ +export interface BundleDeclaration { + readonly row: EntryOptions + readonly preset?: string +} + +/** Read inserted rows without applying a bundle against an incomplete profile. + * @param patches Parsed bundle patches, whose module paths are already anchored. + * @returns Declarations in patch order, including nested groups. + */ +export function bundleDeclarations(patches: readonly ProfilePatch[]): BundleDeclaration[] { + const result: BundleDeclaration[] = [] + const visit = (rows: EntryOptions[], preset?: string): void => { + for (const row of rows) { + result.push({ row, ...preset === undefined ? {} : { preset } }) + if (row.group && Array.isArray(row.config)) visit(row.config as EntryOptions[], preset) + } + } + for (const patch of patches) { + if (patch.insert !== undefined) visit(patch.insert, profilePatchPreset(patch)) + } + return result +} + +/** Locate the package owning a bare import or filesystem module without importing its code. + * @param specifier Configured module name or anchored file URL. + * @param base Declaring Loader tree's resolution URL. + * @param packages Optional profile package lookup for bare specifiers. + * @returns Canonical package manifest path, or undefined for builtins, missing modules, and unowned files. + */ +export function bundleModuleOwner( + specifier: string, base: string, packages?: Pick, +): string | undefined { + const known = packages?.packageOf(specifier, base) + if (known !== undefined) return realModuleFile(known.manifestPath) + if (!specifier.startsWith('file:') && !specifier.startsWith('.') && !isAbsolute(specifier)) return undefined + const file = isAbsolute(specifier) ? specifier : fileURLToPath(new URL(specifier, base)) + if (!existsSync(file)) return undefined + let dir = dirname(realModuleFile(file)) + while (true) { + const manifest = join(dir, 'package.json') + if (existsSync(manifest)) return realModuleFile(manifest) + const parent = dirname(dir) + if (parent === dir) return undefined + dir = parent + } +} diff --git a/packages/boot/plugin-manager/src/index.ts b/packages/boot/plugin-manager/src/index.ts index 37b875632a..5c77c0a97c 100644 --- a/packages/boot/plugin-manager/src/index.ts +++ b/packages/boot/plugin-manager/src/index.ts @@ -7,25 +7,29 @@ import { pathToFileURL } from 'node:url' import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write' import { Context } from '@deepseek-ai/cordis' import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader' -import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include' +import type { PluginInventorySnapshot } from '@deepseek-ai/dsh-host-plugin-inventory/types' +import type { AgentPreset } from '@deepseek-ai/dsh-agent-preset-registry' import z from '@deepseek-ai/schemastery' import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol' import { pluginEntryId, readPluginInventory } from '@deepseek-ai/dsh-host-plugin-inventory' import { - readPluginMeta, readProfileManifest, resolveBundleDir, loadOverlayPatches, composeEntries, + readPluginMeta, resolvePluginResource, realModuleFile, readProfileManifest, resolveBundleDir, loadOverlayPatches, composeEntries, reconcileProfilePatches, readProfilePatches, OPTIONAL_BUNDLES, bundlePatchPaths, evaluatePluginCompatibility, readProfileCompatibility, readProfileVersionExemptions, - setProfileVersionExemption, PROFILE_COMPATIBILITY_FILENAME, + setProfileVersionExemption, PROFILE_COMPATIBILITY_FILENAME, profilePatchPreset, type ProfilePatch, + OFFICIAL_ON_DEMAND_CATALOG, getDshRuntimeVersion, } from '@deepseek-ai/dsh-app-boot' import type {} from '@deepseek-ai/dsh-hmr' import type { ProfileContext, ProfileManifest } from '@deepseek-ai/dsh-app-boot' import { bundleManifest, readProfileRegistry, registryArguments, runProfilePnpm, saveManifest, viewProfilePackage } from './operations.ts' import { classifyInstallFailure } from './install-failure.ts' +import { officialBundleInstallTarget } from './official-install-target.ts' import { dependencySpec, InvalidInstallSpecError, parseInstallSpec, type ParsedInstallSpec } from './install-spec.ts' import { attributeFailure, normalizeRegistry, NPMMIRROR_REGISTRY, registryPlan } from './registry.ts' import { writePluginEnabled } from './patch.ts' import { incompatiblePlugin, ManagementFailure } from './failure.ts' -import { approveBuilds, readPendingBuilds } from './build-approval.ts' +import { approveBuilds, recordPendingBuilds } from './build-approval.ts' +import { bundleDeclarations, bundleModuleOwner, type BundleDeclaration } from './bundle-rows.ts' import { checkGithubConnection } from './github-connection.ts' import type { BundleInfo, BundleRowInfo, ChangeResult, InspectOptions, InstallBundleOptions, ManagementError, PackageResult, PluginChange, @@ -271,60 +275,84 @@ export class PluginManager extends TypertRemoteService { }) } - /** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles. + /** Read installed, installation-provided, and offline Official catalog bundles, plus selected non-bundle names. * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected. * @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional * display metadata, activation selections, whether the installation offers the bundle, and removal availability. */ @Remote - listBundles(): Promise { + async listBundles(): Promise { + const inventory = await readPluginInventory(this.ctx) + const definitions = await this.ctx.get('agentPresets')?.list() ?? [] const manifest = readProfileManifest('dsh', this.profile.dir) const exemptions = readProfileVersionExemptions(this.profile.dir) const selected = manifest.dsh?.profile?.bundles ?? [] const recorded = manifest.dependencies ?? {} const dependencies = Object.keys(recorded) const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8')) as InstallationManifest - const names = [...new Set([...selected, ...dependencies, ...Object.keys(installation.dependencies ?? {})])] + const catalog = new Map(OFFICIAL_ON_DEMAND_CATALOG.map(entry => [entry.packageName, entry])) + const names = [...new Set([...selected, ...dependencies, ...Object.keys(installation.dependencies ?? {}), ...catalog.keys()])] const bundles: BundleInfo[] = [] + const runtimeVersion = getDshRuntimeVersion() for (const name of names) { const installed = dependencies.includes(name) const optional = OPTIONAL_BUNDLES.includes(name) const enabled = selected.includes(name) const shipped = Object.hasOwn(installation.dependencies ?? {}, name) - // Bundle resolution reads the installation first, so a profile dependency the installation manifest also names, - // like one it forbids removing, is not the loaded copy. + const offered = catalog.get(name) + const official = optional || offered !== undefined + let target: Pick = {} + let targetError: ManagementError | undefined + if (offered !== undefined) { + try { target = { installTarget: officialBundleInstallTarget(name, this.profile.installAnchor, runtimeVersion) } } + catch (error) { targetError = managementError(error) } + } + const catalogMeta = offered === undefined ? {} : { meta: offered.meta } + if (offered !== undefined && !installed && !shipped) { + bundles.push({ name, official, availability: 'missing', ...catalogMeta, ...target, enabled, installed, optional, + removable: enabled, rows: [], overrides: [], + ...targetError !== undefined ? { error: targetError } : enabled ? { error: { code: 'not-bundle' } } : {} }) + continue + } + // Installation-owned copies remain protected even if a profile also declares their names. const owned = installed && !shipped - // A selection neither the profile nor the installation holds, such as a retired bundle, is removed by deselecting it. const removable = owned || (enabled && !installed && !shipped) const sourceOf = (packageName?: string): { source?: string } => owned ? { source: dependencySpec(name, recorded[name] as string, this.profile.dir, packageName) } : {} const readOnlyReason = this.protectsManager(name) ? 'management-required' as const : undefined + let availability: BundleInfo['availability'] = 'missing' + let version: string | undefined try { const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor) + availability = shipped ? 'installation' : 'profile' if (info === undefined) { - if (enabled) bundles.push({ name, ...sourceOf(), enabled, installed, optional, - removable: removable && readOnlyReason === undefined, - ...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: { code: 'not-bundle' }, rows: [], overrides: [] }) + if (enabled || offered !== undefined) bundles.push({ name, official, availability, ...catalogMeta, ...target, + ...sourceOf(), enabled, installed, optional, removable: removable && readOnlyReason === undefined, + ...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: targetError ?? { code: 'not-bundle' }, rows: [], overrides: [] }) continue } + version = info.version const compatibility = evaluatePluginCompatibility(info, exemptions) if (compatibility !== undefined && !compatibility.exempted) throw new ManagementFailure('incompatible-version', [incompatiblePlugin(compatibility)]) const dir = resolveBundleDir('dsh', name, this.profile.installAnchor, this.profile.dir) - const meta = readPluginMeta(info.name ?? name, pathToFileURL(join(dir, 'package.json')).href) - bundles.push({ name, ...(info.version === undefined ? {} : { version: info.version }), + const meta = offered?.meta ?? readPluginMeta(info.name ?? name, pathToFileURL(join(dir, 'package.json')).href) + bundles.push({ name, official, availability, ...target, ...(version === undefined ? {} : { version }), ...(info.description === undefined || info.description === '' ? {} : { description: info.description }), ...meta === undefined ? {} : { meta }, ...sourceOf(info.name), enabled, installed, optional, removable: removable && readOnlyReason === undefined, ...(readOnlyReason === undefined ? {} : { readOnlyReason }), - ...this.declaredRows(name, info) }) + ...this.declaredRows(name, info, inventory, definitions), + ...targetError === undefined ? {} : { error: targetError } }) } catch (error) { - if (enabled || installed) { - bundles.push({ name, ...sourceOf(), enabled, installed, optional, removable: removable && readOnlyReason === undefined, - ...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: managementError(error), rows: [], overrides: [] }) + if (enabled || installed || optional || offered !== undefined) { + bundles.push({ name, official, availability, ...catalogMeta, ...target, ...(version === undefined ? {} : { version }), + ...sourceOf(), enabled, installed, optional, removable: removable && readOnlyReason === undefined, + ...(readOnlyReason === undefined ? {} : { readOnlyReason }), + error: targetError ?? managementError(error), rows: [], overrides: [] }) } } } - return Promise.resolve(bundles) + return bundles } /** Read the registries this manager asks: the configured first one, its fallbacks in order, and what pnpm's own configuration names. @@ -452,7 +480,11 @@ export class PluginManager extends TypertRemoteService { setBundleEnabled(name: string, enabled: boolean): Promise { return this.change(result => this.configure(async () => { await this.selectBundle(name, enabled) - result.warnings = await this.reload(enabled ? this.bundleRows(name).map(row => row.id) : []) + if (enabled) await this.refreshPackages() + const rows = enabled ? this.bundleRows(name) : [] + result.warnings = await this.reload(rows.filter(row => row.preset === undefined).map(({ row }) => row.id), + enabled ? this.bundlePresets(name) : []) + if (!enabled && this.ownerContext.get('hmr') !== undefined) await this.refreshPackages() }), { stage: 'enable', target: name, enabled }, 'bundle') } @@ -464,7 +496,7 @@ export class PluginManager extends TypertRemoteService { * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay. * @param spec One package spec, including local paths relative to the invocation directory. * @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names, - * the pending build scripts to allow for this profile before pnpm runs, and the registry asked first. + * the pending build scripts to allow, whether to save an exact dependency, and the registry asked first. * @returns Package-manager diagnostics, the registries asked, and the observed activation outcome. */ @Remote @@ -513,7 +545,7 @@ export class PluginManager extends TypertRemoteService { if (stopped()) throw new InstallCancelledError() result.registries.push(registry) announce('installing', { registry, index: index + 1, total: plan.length }) - run = await this.runPnpm(['add', spec, ...registryArguments(registry)], control.abort.signal, requestId) + run = await this.runPnpm(['add', spec, ...options?.saveExact === true ? ['--save-exact'] : [], ...registryArguments(registry)], control.abort.signal, requestId) result.packageResult = run if (stopped()) throw new InstallCancelledError() // A compatibility refusal is the package's own answer, so no other registry is asked. @@ -536,8 +568,8 @@ export class PluginManager extends TypertRemoteService { const succeeded = run.exitCode === 0 && run.timedOut !== true if (succeeded) delete result.failedAt if (!succeeded) { - // pnpm-workspace.yaml is not restored, so the names pnpm left undecided there can be offered for approval. - try { result.pendingBuilds = await readPendingBuilds(this.profile.dir) } + // pnpm-workspace.yaml is not restored, so the names it left undecided, or reported as ignored, are offered for approval. + try { result.pendingBuilds = await recordPendingBuilds(this.profile.dir, run.output) } catch (error) { this.ownerContext.logger.warn('Could not read pending build approvals after pnpm failed', error) } @@ -546,7 +578,11 @@ export class PluginManager extends TypertRemoteService { const after = readProfileManifest('dsh', this.profile.dir).dependencies ?? {} const installed = Object.keys(after).filter(name => before[name] !== after[name]) // Registry retries can retain the saved range after a partial installation. - if (installed.length === 0) installed.push(...Object.keys(after).filter(name => spec === name || spec.startsWith(`${name}@`))) + if (installed.length === 0) { + const parsed = parsedForRegistry(spec) + const localName = parsed.kind === 'path' ? readProfileManifest('dsh', parsed.path).name : undefined + installed.push(...Object.keys(after).filter(name => name === localName || spec === name || spec.startsWith(`${name}@`))) + } const target = installed[0] if (installed.length !== 1 || target === undefined) throw new ManagementFailure('ambiguous-install') name = target @@ -571,7 +607,8 @@ export class PluginManager extends TypertRemoteService { return this.configure(async () => { if (options?.enabled !== false) await this.selectBundle(name, true) if (Object.hasOwn(before, name)) return 'restart-required' - if (options?.enabled !== false) result.warnings = await this.reload() + await this.refreshPackages() + if (options?.enabled !== false) result.warnings = await this.reload([], this.bundlePresets(name)) }) }, { stage: 'install', target: spec, enabled: options?.enabled !== false }, 'install') control.result = result @@ -616,18 +653,21 @@ export class PluginManager extends TypertRemoteService { const installed = await this.configure(async () => { const bundle = (await this.listBundles()).find(item => item.name === name) if (bundle === undefined || !bundle.removable) throw new ManagementFailure('not-removable') + const contributions = bundle.error === undefined ? this.bundleRows(name) : [] if (this.ownerContext.get('hmr') === undefined && (this.profile.startedBundles.includes(name) - || (bundle.error === undefined && this.bundleRows(name).some(row => [...this.ctx.loader.entries()] - .some(entry => entry.options.id === row.id && entry.fiber !== undefined))))) { + || contributions.some(({ row, preset }) => preset === undefined && [...this.ctx.loader.entries()] + .some(entry => entry.options.id === row.id && entry.fiber !== undefined)) + || this.retainedBundleModules(name, contributions))) { throw new ManagementFailure('stop-profile') } - const contributions = bundle.error === undefined ? this.bundleRows(name) : [] if (bundle.enabled) { await this.selectBundle(name, false) result.warnings = await this.reload() } if ([...this.ctx.loader.entries()].some(entry => entry.fiber?.uid != null - && contributions.some(row => row.id === entry.options.id && row.name === entry.options.name))) { + && contributions.some(({ row, preset }) => preset === undefined + && row.id === entry.options.id && row.name === entry.options.name)) + || this.retainedBundleModules(name, contributions)) { throw new ManagementFailure('bundle-in-use') } return bundle.installed @@ -637,41 +677,69 @@ export class PluginManager extends TypertRemoteService { if (result.packageResult.exitCode !== 0 || result.packageResult.timedOut === true) { throw new Error(result.packageResult.output) } + await this.configure(() => this.refreshPackages()) }, { stage: 'remove', target: name }, 'remove') } - /** The rows a bundle's patch inserts and the existing rows it changes; an unreadable patch throws. */ - private declaredRows(name: string, info: ProfileManifest): Pick { - const bundle = info.dsh?.bundle - /* v8 ignore next -- bundleManifest answers only manifests that declare a patch */ - if (bundle === undefined) return { rows: [], overrides: [] } + /** The rows a bundle declares, joined by outer preset and child id where applicable. */ + private declaredRows(name: string, info: ProfileManifest, inventory: PluginInventorySnapshot, definitions: readonly AgentPreset[]): Pick { + const patches = this.bundlePatches(name, info) const dir = resolveBundleDir('dsh', name, this.profile.installAnchor, this.profile.dir) - const patches: PatchOptions[] = bundlePatchPaths(dir, bundle).flatMap(file => loadOverlayPatches('dsh', file)) - // One entry per row id: the Loader keeps a single entry for an id, whichever layer declared it last. - const live = new Map() - for (const entry of this.ctx.loader.entries()) { - /* v8 ignore next -- the Loader gives every entry an id before it is listed */ - if (typeof entry.options.id === 'string') live.set(entry.options.id, { - entryId: pluginEntryId(entry.id), baseUrl: entry.parent.tree.ctx.baseUrl, - }) - } + const entries = [...this.ctx.loader.entries()] const rows: BundleRowInfo[] = [] const packages = this.ctx.get('pluginPackages') - for (const row of flatten(composeEntries([patches.filter(item => item.insert !== undefined)]))) { + for (const { row, preset } of bundleDeclarations(patches)) { if (typeof row.id !== 'string' || typeof row.name !== 'string') continue - const active = live.get(row.id) - const entryId = active?.entryId - const base = active?.baseUrl ?? pathToFileURL(join(dir, 'package.json')).href + const matches = preset === undefined ? entries.filter(entry => entry.options.id === row.id && entry.options.name === row.name) : [] + const active = matches.length === 1 ? matches[0] : undefined + const base = active?.parent.tree.ctx.baseUrl ?? pathToFileURL(join(dir, 'package.json')).href const meta = packages?.metaOf(row.name, base) + const presetId = preset === undefined ? undefined : definitions.find(item => item.definitionEntryId === preset)?.id + const composition = inventory.agentPresets?.find(item => item.id === presetId)?.rows + .find(item => item.entryId === row.id && item.moduleName === row.name) rows.push({ rowId: row.id, moduleName: row.name, - ...entryId === undefined ? {} : { entryId }, ...meta === undefined ? {} : { meta } }) + ...preset === undefined ? {} : { preset, readOnlyReason: 'preset-managed' as const }, + ...composition === undefined ? {} : { composition: { enabled: composition.enabled, fiberPhase: composition.fiberPhase } }, + ...active === undefined ? {} : { entryId: pluginEntryId(active.id) }, ...meta === undefined ? {} : { meta } }) } - const declared = new Set(rows.map(row => row.rowId)) - const overrides = [...new Set(patches.flatMap(item => - item.insert === undefined && typeof item.id === 'string' && !declared.has(item.id) ? [item.id] : []))] + const key = (preset: string | undefined, id: string): string => JSON.stringify([preset, id]) + const declared = new Set(rows.map(row => key(row.preset, row.rowId))) + const overrides = [...new Set(patches.flatMap((item) => { + const preset = profilePatchPreset(item) + return item.insert === undefined && typeof item.id === 'string' && !declared.has(key(preset, item.id)) + ? [preset === undefined ? item.id : `${preset}/${item.id}`] : [] + }))] return { rows, overrides } } + /** Retained revisions still hold module code even after a bundle has been deselected. */ + private retainedBundleModules(name: string, declarations: readonly BundleDeclaration[]): boolean { + const presets = this.ctx.get('agentPresets') + if (presets === undefined || declarations.length === 0) return false + const manifest = join(resolveBundleDir('dsh', name, this.profile.installAnchor, this.profile.dir), 'package.json') + const base = pathToFileURL(manifest).href + const owner = realModuleFile(manifest) + const moduleKey = (specifier: string, parentURL: string): string | undefined => { + if (specifier.startsWith('cordis:')) return specifier + try { return realModuleFile(resolvePluginResource(specifier, parentURL)) } + catch (error) { + // An externally removed module cannot match another resolved file; identical declarations remain protected below. + if (['ENOENT', 'ENOTDIR', 'ERR_MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(String((error as NodeJS.ErrnoException).code))) return undefined + throw error + } + } + return presets.inspectCompositions().some(composition => composition.modules.some((module) => { + const moduleBase = module.baseUrl ?? base + if (bundleModuleOwner(module.moduleName, moduleBase, this.ctx.get('pluginPackages')) === owner) return true + return module.useHostBase && declarations.some(({ row, preset }) => { + if (preset === undefined || preset !== composition.definitionEntryId || row.id !== module.entryId) return false + const declared = moduleKey(row.name, base) + const retained = moduleKey(module.moduleName, moduleBase) + return declared === undefined || retained === undefined ? row.name === module.moduleName : declared === retained + }) + })) + } + /** Run one pnpm command in the profile, streaming its output as install-log chunks. */ private async runPnpm( args: readonly string[], signal?: AbortSignal, requestId?: PluginInstallRequestId, @@ -749,21 +817,31 @@ export class PluginManager extends TypertRemoteService { if (enabled) this.protectsManager(name) } - private bundleRows(name: string): EntryOptions[] { - const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor) + private bundlePatches(name: string, known?: ProfileManifest): ProfilePatch[] { + const info = known ?? bundleManifest(name, this.profile.dir, this.profile.installAnchor) if (info?.dsh?.bundle === undefined) return [] const dir = resolveBundleDir('dsh', name, this.profile.installAnchor, this.profile.dir) - return flatten(composeEntries([bundlePatchPaths(dir, info.dsh.bundle).flatMap(file => loadOverlayPatches('dsh', file))])) + return bundlePatchPaths(dir, info.dsh.bundle).flatMap(file => loadOverlayPatches('dsh', file)) + } + + private bundleRows(name: string): BundleDeclaration[] { return bundleDeclarations(this.bundlePatches(name)) } + + private bundlePresets(name: string): string[] { + return [...new Set(this.bundlePatches(name).flatMap((patch) => { + const preset = profilePatchPreset(patch) + return preset === undefined ? [] : [preset] + }))] } private protectsManager(name: string): boolean { if (this.managementBundles.has(name)) return true - let rows: EntryOptions[] + let rows: BundleDeclaration[] try { rows = this.bundleRows(name) } catch (_error) { // Unreadable bundles contribute no new rows; listBundles reports their diagnostics. return false } - const protectedBundle = rows.some(row => protectedModules.has(row.name) || `include:${row.id}` === this.ownerEntryId) + const protectedBundle = rows.some(({ row, preset }) => preset === undefined + && (protectedModules.has(row.name) || `include:${row.id}` === this.ownerEntryId)) if (protectedBundle) this.managementBundles.add(name) return protectedBundle } @@ -774,9 +852,42 @@ export class PluginManager extends TypertRemoteService { return hmr === undefined ? apply() : hmr.runExclusive(apply) } - private async reload(requiredIds: readonly string[] = []): Promise { - if (this.ownerContext.get('hmr') === undefined) return [] - return reconcileProfilePatches(this.ownerContext.root, readProfilePatches('dsh', this.profile), 'dsh', requiredIds) + private async refreshPackages(): Promise { + if (this.ownerContext.get('hmr') === undefined) { + const selected = readProfileManifest('dsh', this.profile.dir).dsh?.profile?.bundles ?? [] + // Deselected startup bundles still run without HMR and need the existing package table. + if (this.profile.startedBundles.some(name => !selected.includes(name))) return + } + await this.ownerContext.get('pluginPackages')?.refresh() + } + + /** Configuration identity distinguishes a changed failed preset from an unrelated pre-existing warning. */ + private presetConfiguration(preset: AgentPreset): string { + return JSON.stringify([...this.ctx.loader.entries()].flatMap((entry) => { + const config = entry.options.config as { id?: unknown; plugins?: unknown } | undefined + return entry.options.id === preset.definitionEntryId && config?.id === preset.id && Array.isArray(config.plugins) ? [config] : [] + })) + } + + private async reload(requiredIds: readonly string[] = [], requiredPresets: readonly string[] = []): Promise { + if (this.ownerContext.get('hmr') === undefined) { + composeEntries([readProfilePatches('dsh', this.profile)]) + return [] + } + const presets = this.ctx.get('agentPresets') + const before = new Map((await presets?.list() ?? []).map(row => + [row.id, { broken: row.broken, config: this.presetConfiguration(row) }])) + const warnings = await reconcileProfilePatches( + this.ownerContext.root, readProfilePatches('dsh', this.profile), 'dsh', [...requiredIds, ...requiredPresets], + ) + const after = await this.ctx.get('agentPresets')?.list() ?? [] + const required = new Set(after.filter(row => row.definitionEntryId !== undefined && requiredPresets.includes(row.definitionEntryId)) + .map(row => row.id)) + const broken = after.filter(row => row.broken !== undefined) + const failed = broken.filter(row => required.has(row.id) || before.get(row.id)?.broken !== row.broken + || before.get(row.id)?.config !== this.presetConfiguration(row)) + if (failed.length > 0) throw new Error(failed.map(row => `agent preset ${row.id}: ${row.broken}`).join('\n')) + return [...warnings, ...broken.map(row => `agent preset ${row.id}: ${row.broken}`)] } private async change( diff --git a/packages/boot/plugin-manager/src/official-install-target.ts b/packages/boot/plugin-manager/src/official-install-target.ts new file mode 100644 index 0000000000..8b5aa5f1e8 --- /dev/null +++ b/packages/boot/plugin-manager/src/official-install-target.ts @@ -0,0 +1,58 @@ +/** Select Official bundle packages from the running installation without importing provider code. */ +import { existsSync, globSync, readFileSync, realpathSync, statSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { bundlePatchPaths, type ProfileManifest } from '@deepseek-ai/dsh-app-boot' + +interface WorkspaceManifest extends ProfileManifest { + main?: string +} + +function manifestAt(file: string): WorkspaceManifest { + return JSON.parse(readFileSync(file, 'utf8')) as WorkspaceManifest +} + +/** + * Offer a source checkout link or an exact published package version. Source installations are + * identified by the real CLI manifest and its unpacked workspace dependencies, including Desktop's + * development symlink. An incomplete checkout is an error, never a registry fallback. + * @param name - Official catalog package name. + * @param installAnchor - Running application's DSH package.json, possibly reached through a symlink. + * @param version - Running DSH version used by its release family. + * @returns An ordinary package-manager spec and its package version. + * @throws when a source installation lacks its workspace package, bundle patch, or built runtime. + */ +export function officialBundleInstallTarget(name: string, installAnchor: string, version: string): { spec: string; version: string } { + const anchor = realpathSync(installAnchor) + const installation = manifestAt(anchor) + const source = installation.name === '@deepseek-ai/dsh' + && Object.values(installation.dependencies ?? {}).some(value => value.startsWith('workspace:')) + if (!source) return { spec: `${name}@${version}`, version } + const root = resolve(dirname(anchor), '../..') + if (anchor !== join(root, 'apps', 'cli', 'package.json') + || !existsSync(join(root, 'package.json')) + || manifestAt(join(root, 'package.json')).name !== '@deepseek-ai/dsh-root') { + throw new Error(`Official bundle ${name}: source DSH installation ${anchor} requires its complete workspace checkout`) + } + const matches = globSync('packages/*/*/package.json', { cwd: root }) + .map(file => ({ file: join(root, file), manifest: manifestAt(join(root, file)) })) + .filter(entry => entry.manifest.name === name) + const selected = matches[0] + if (matches.length !== 1 || selected === undefined) { + throw new Error(`Official bundle ${name}: expected one workspace package in ${root}, found ${matches.length}; restore the checkout and run pnpm install`) + } + const { file, manifest } = selected + const directory = realpathSync(dirname(file)) + if (manifest.version !== version || manifest.private === true || manifest.dsh?.bundle === undefined) { + throw new Error(`Official bundle ${name}: ${file} must declare a public bundle at DSH version ${version}`) + } + const main = typeof manifest.main === 'string' ? resolve(directory, manifest.main) : undefined + if (main === undefined || !existsSync(main) || !statSync(main).isFile()) { + throw new Error(`Official bundle ${name}: built runtime is missing in ${directory}; run pnpm install and pnpm run build in ${root}`) + } + for (const patch of bundlePatchPaths(directory, manifest.dsh.bundle)) { + if (!existsSync(patch) || !statSync(patch).isFile()) { + throw new Error(`Official bundle ${name}: bundle patch ${patch} is missing; restore the checkout`) + } + } + return { spec: `link:${directory}`, version } +} diff --git a/packages/boot/plugin-manager/src/operations.ts b/packages/boot/plugin-manager/src/operations.ts index 2df35b7efc..8d58402e76 100644 --- a/packages/boot/plugin-manager/src/operations.ts +++ b/packages/boot/plugin-manager/src/operations.ts @@ -4,17 +4,17 @@ import { existsSync, readFileSync } from 'node:fs' import { mkdir, mkdtemp, open, rm } from 'node:fs/promises' import { join, resolve } from 'node:path' import { execa } from 'execa' -import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader' import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write' import { DEFAULT_PROFILE_BUNDLES, bundlePatchPaths, initProfile, PROFILE_TEMPLATES, readProfileManifest, - resolveBundleDir, resolveProfileDir, loadOverlayPatches, composeEntries, readProfileVersionExemptions, + resolveBundleDir, resolveProfileDir, loadOverlayPatches, readProfileVersionExemptions, evaluatePluginCompatibility, pluginCompatibilityWarning, type ProfileManifest, } from '@deepseek-ai/dsh-app-boot' import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess' import { parseInstallSpec } from './install-spec.ts' import { awaitTreeGone, leadsOwnGroup, treeAlive, type RunTree } from './run-tree.ts' import { incompatiblePlugin } from './failure.ts' +import { bundleDeclarations } from './bundle-rows.ts' import type { IncompatiblePlugin, PackageResult, Registry } from './types.ts' export { setProfileVersionExemption, readProfileVersionExemptions } from '@deepseek-ai/dsh-app-boot' @@ -251,15 +251,11 @@ function bundleComponentManifests(manifest: ProfileManifest, dir: string, anchor if (bundle === undefined) return [] const patches = bundlePatchPaths(dir, bundle).flatMap(file => loadOverlayPatches('dsh', file)) const names = new Set() - const visit = (rows: EntryOptions[]) => { - for (const row of rows) { - if (row.group && Array.isArray(row.config)) visit(row.config as EntryOptions[]) - if (typeof row.name !== 'string' || row.name.startsWith('.') || row.name.startsWith('/') || row.name.includes(':')) continue - const parts = row.name.split('/') - names.add(parts.slice(0, row.name.startsWith('@') ? 2 : 1).join('/')) - } + for (const { row } of bundleDeclarations(patches)) { + if (typeof row.name !== 'string' || row.name.startsWith('.') || row.name.startsWith('/') || row.name.includes(':')) continue + const parts = row.name.split('/') + names.add(parts.slice(0, row.name.startsWith('@') ? 2 : 1).join('/')) } - visit(composeEntries([patches.filter(patch => patch.insert !== undefined)])) return [...names].flatMap((name) => { let packageDir: string try { packageDir = resolveBundleDir('dsh', name, anchor, dir) } catch (error) { diff --git a/packages/boot/plugin-manager/src/patch.ts b/packages/boot/plugin-manager/src/patch.ts index 5776b586fa..3a4aa69f09 100644 --- a/packages/boot/plugin-manager/src/patch.ts +++ b/packages/boot/plugin-manager/src/patch.ts @@ -4,9 +4,9 @@ import { isMap, isSeq, parseDocument } from 'yaml' import { loadOptionalPatches } from '@deepseek-ai/dsh-app-boot' import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write' -/** Replace the last matching override or append one after existing insertions. +/** Replace the last matching Host override or append one, preserving preset-scoped operations. * @param filename Current profile patch file. - * @param id Unique composition entry id. + * @param id Unique Host composition entry id. * @param name Module name used to match name-qualified overrides. * @param enabled Desired entry enablement. * @returns Whether the file changed. @@ -28,7 +28,7 @@ export async function writePluginEnabled(filename: string, id: string, name: str loadOptionalPatches('dsh', filename) const items = document.contents.items const target = items.findLast((item, index) => { - if (!isMap(item) || document.getIn([index, 'id']) !== id || item.has('insert')) return false + if (!isMap(item) || document.getIn([index, 'id']) !== id || item.has('insert') || item.has('preset')) return false const expectedName = document.getIn([index, 'name']) return !expectedName || expectedName === name }) diff --git a/packages/boot/plugin-manager/src/tools.ts b/packages/boot/plugin-manager/src/tools.ts index df9b0a200e..29c58e898d 100644 --- a/packages/boot/plugin-manager/src/tools.ts +++ b/packages/boot/plugin-manager/src/tools.ts @@ -21,7 +21,7 @@ export function apply(ctx: Context): void { description: 'List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Every action requires danger-full-access permission or approval for this call. Approval does not change the session permission mode. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart. Incompatible DSH peer dependencies block installation and activation. Version exemptions risk crashes and data loss: warn the user and obtain explicit permission for the exact plugin and runtime versions before granting one.', parameters: { action: { type: 'string', required: true, enum: ['list_plugins', 'list_bundles', 'set_plugin', 'set_bundle', 'install_bundle', 'remove_bundle', 'list_version_exemptions', 'set_version_exemption'], description: 'Management operation.' }, - target: { type: 'string', description: 'Plugin entry id, bundle package name, or installation spec, according to action.' }, + target: { type: 'string', description: 'Plugin entry id, bundle package name, or installation spec, according to action. For an Official catalog entry, pass list_bundles installTarget.spec to install_bundle.' }, enabled: { type: 'boolean', description: 'Required for set operations; defaults to true for installation. For set_version_exemption, true grants and false revokes.' }, runtimeVersion: { type: 'string', description: 'For set_version_exemption: exact DSH version from list_version_exemptions. Target must be the manifest package-name@version, not an alias or version range.' }, acceptRisk: { type: 'boolean', description: 'For granting an exemption: true only after warning the user about possible crashes and data loss and receiving explicit permission for this exact plugin/runtime pair. General installation permission is not enough.' }, diff --git a/packages/boot/plugin-manager/src/types.ts b/packages/boot/plugin-manager/src/types.ts index 90194e9427..725d146c3f 100644 --- a/packages/boot/plugin-manager/src/types.ts +++ b/packages/boot/plugin-manager/src/types.ts @@ -1,12 +1,12 @@ /** Public plugin management records shared with clients. */ import type { Branded } from '@deepseek-ai/dsh-brand' import type { PluginLocalizedMeta } from '@deepseek-ai/dsh-package-manifest' -import type { PluginInventoryEntry } from '@deepseek-ai/dsh-host-plugin-inventory/types' +import type { PluginInventoryEntry, AgentPresetPluginRow } from '@deepseek-ai/dsh-host-plugin-inventory/types' export type { PluginEntryId } from '@deepseek-ai/dsh-host-plugin-inventory/types' import type { PluginEntryId } from '@deepseek-ai/dsh-host-plugin-inventory/types' /** Reasons a profile control cannot modify its target. */ -export type ReadOnlyReason = 'management-required' | 'unaddressable' +export type ReadOnlyReason = 'management-required' | 'unaddressable' | 'preset-managed' /** A package whose declared DSH peers reject the running DSH version, without an exemption for the exact pair. */ export interface IncompatiblePlugin { @@ -33,8 +33,14 @@ export type PluginInfo = PluginInventoryEntry & ( /** One row a bundle's patch declares, with its live entry while the bundle contributes it. */ export interface BundleRowInfo { - /** The row id as the patch declares it. */ + /** Declared row id, or an internal identity for an anonymous scoped insertion. */ rowId: string + /** Outer preset row id; together with rowId identifies a scoped declaration. */ + preset?: string + /** Why direct row controls are unavailable; scoped rows are configured through their preset or bundle. */ + readOnlyReason?: ReadOnlyReason + /** Current preset composition state; scoped rows have no editable root Loader entry. */ + composition?: Pick /** The module the row names. */ moduleName: string /** Local package display metadata, including rows whose bundle is disabled. */ @@ -43,9 +49,15 @@ export interface BundleRowInfo { entryId?: PluginEntryId } -/** One installed or installation-provided bundle. */ +/** One shipped, profile-installed, or discoverable on-demand bundle. */ export interface BundleInfo { name: string + /** Project-maintained shipped optional bundle or on-demand catalog entry, independent of installation and selection. */ + official: boolean + /** Readable package location; on-demand catalog entries exclude undeclared transitive copies, and declared files may be missing. */ + availability: 'installation' | 'profile' | 'missing' + /** Exact released Host version or same-checkout development link offered for an on-demand catalog entry. */ + installTarget?: { spec: string; version: string } version?: string /** Local display text with available translations or literal fallbacks, or a metadata diagnostic. */ meta?: PluginLocalizedMeta @@ -53,7 +65,7 @@ export interface BundleInfo { description?: string /** Selected in the profile manifest; a load error means its layer was skipped. */ enabled: boolean - /** Whether the profile's own dependencies hold the package; false for a bundle the dsh installation supplies. */ + /** Whether the profile declares this dependency; availability separately reports whether its files can be read. */ installed: boolean /** * Present for a profile dependency the installation does not also supply: the spec `pnpm add` accepts, with local @@ -148,6 +160,8 @@ export type PluginInstallRequestId = Branded<'PluginInstallRequestId'> /** Bundle installation defaults to activation; callers that offer cancellation supply their request id. */ export interface InstallBundleOptions { enabled?: boolean + /** Save the resolved dependency version without a range; applies to any bundle installation. */ + saveExact?: boolean requestId?: PluginInstallRequestId /** Explicitly allow these pending packages' scripts for this profile, then install; a name no longer pending refuses the call. */ approvedBuilds?: string[] diff --git a/packages/boot/plugin-manager/tests/build-approval.spec.ts b/packages/boot/plugin-manager/tests/build-approval.spec.ts index 2c46e4da97..f7620aee82 100644 --- a/packages/boot/plugin-manager/tests/build-approval.spec.ts +++ b/packages/boot/plugin-manager/tests/build-approval.spec.ts @@ -4,7 +4,7 @@ import { join } from 'node:path' import { tmpdir } from 'node:os' import { expect, it, onTestFinished } from 'vitest' import { parse } from 'yaml' -import { approveBuilds, readPendingBuilds } from '../src/build-approval.ts' +import { approveBuilds, recordPendingBuilds } from '../src/build-approval.ts' function fixture(text?: string) { const dir = mkdtempSync(join(tmpdir(), 'build-approval-')) @@ -16,12 +16,12 @@ function fixture(text?: string) { it('approves only named pending packages and preserves comments, decisions and settings', async () => { const { dir, filename } = fixture('# profile settings\nother: &unrelated value\ncopy: *unrelated\nnodeLinker: hoisted\nallowBuilds:\n native: set this to true or false\n "@scope/other": set this to true or false\n trusted: true\n denied: false\n "@scope/*": set this to true or false\n') - expect(await readPendingBuilds(dir)).toEqual(['native', '@scope/other']) + expect(await recordPendingBuilds(dir, '')).toEqual(['native', '@scope/other']) await approveBuilds(dir, ['native']) const text = readFileSync(filename, 'utf8') expect(text).toContain('# profile settings') expect(parse(text)).toMatchObject({ nodeLinker: 'hoisted', allowBuilds: { native: true, trusted: true, denied: false } }) - expect(await readPendingBuilds(dir)).toEqual(['@scope/other']) + expect(await recordPendingBuilds(dir, '')).toEqual(['@scope/other']) }) it.each(['missing', 'denied', '*', '--all'])('rejects an unlisted approval atomically: %s', async (name) => { @@ -34,20 +34,20 @@ it.each(['missing', 'denied', '*', '--all'])('rejects an unlisted approval atomi it('preserves pnpm file dependency selectors verbatim', async () => { const name = '@scope/addon@file:../local addon' const { dir, filename } = fixture(`allowBuilds:\n '${name}': set this to true or false\n`) - expect(await readPendingBuilds(dir)).toEqual([name]) + expect(await recordPendingBuilds(dir, '')).toEqual([name]) await approveBuilds(dir, [name]) expect(parse(readFileSync(filename, 'utf8'))).toEqual({ allowBuilds: { [name]: true } }) }) it.each([undefined, '{}\n', 'nodeLinker: hoisted\n', 'allowBuilds: {}\n'])('has no pending approval without pnpm placeholders: %s', async (text) => { const { dir } = fixture(text) - expect(await readPendingBuilds(dir)).toEqual([]) + expect(await recordPendingBuilds(dir, '')).toEqual([]) await approveBuilds(dir, []) }) it.each(['[', '[]\n', 'allowBuilds: false\n'])('rejects malformed workspace settings without rewriting them: %s', async (text) => { const { dir, filename } = fixture(text) - await expect(readPendingBuilds(dir)).rejects.toThrow() + await expect(recordPendingBuilds(dir, '')).rejects.toThrow() await expect(approveBuilds(dir, ['native'])).rejects.toThrow() expect(readFileSync(filename, 'utf8')).toBe(text) }) @@ -55,7 +55,7 @@ it.each(['[', '[]\n', 'allowBuilds: false\n'])('rejects malformed workspace sett it('reports unreadable workspace settings', async () => { const { dir, filename } = fixture() mkdirSync(filename) - await expect(readPendingBuilds(dir)).rejects.toThrow() + await expect(recordPendingBuilds(dir, '')).rejects.toThrow() }) it.each([ @@ -67,3 +67,44 @@ it.each([ await expect(approveBuilds(dir, ['native'])).rejects.toThrow() expect(readFileSync(filename, 'utf8')).toBe(original) }) + +it('records the ignored build scripts a non-interactive run reported', async () => { + const { dir, filename } = fixture('# profile settings\nnodeLinker: hoisted\n') + const output = [ + 'Progress: resolved 2, reused 2, downloaded 0, added 2, done', + '[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: native@1.0.0, @scope/other@file:../other, @scope/*', + '', + 'Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.', + ].join('\n') + + expect(await recordPendingBuilds(dir, output)).toEqual(['native@1.0.0', '@scope/other@file:../other']) + const text = readFileSync(filename, 'utf8') + expect(text).toContain('# profile settings') + expect(parse(text)).toMatchObject({ + nodeLinker: 'hoisted', + allowBuilds: { 'native@1.0.0': 'set this to true or false', '@scope/other@file:../other': 'set this to true or false' }, + }) + expect(await recordPendingBuilds(dir, output)).toEqual(['native@1.0.0', '@scope/other@file:../other']) + await approveBuilds(dir, ['native@1.0.0']) + expect(parse(readFileSync(filename, 'utf8'))).toMatchObject({ allowBuilds: { 'native@1.0.0': true } }) +}) + +it.each([ + ['no report', 'Progress: resolved 1, reused 1, downloaded 0, added 1, done\n'], + ['an empty report', '[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: \n'], + ['only wildcards', '[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: @scope/*, native@?\n'], +])('leaves the profile settings untouched for %s', async (_label, output) => { + const original = 'allowBuilds:\n decided: true\n' + const { dir, filename } = fixture(original) + expect(await recordPendingBuilds(dir, output)).toEqual([]) + expect(readFileSync(filename, 'utf8')).toBe(original) +}) + +it('preserves approval keys that are not package names', async () => { + const { dir, filename } = fixture('allowBuilds:\n 1: true\n') + expect(await recordPendingBuilds(dir, '[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: native@1.0.0\n')) + .toEqual(['native@1.0.0']) + expect(parse(readFileSync(filename, 'utf8'))).toMatchObject({ + allowBuilds: { 1: true, 'native@1.0.0': 'set this to true or false' }, + }) +}) diff --git a/packages/boot/plugin-manager/tests/bundle-rows.spec.ts b/packages/boot/plugin-manager/tests/bundle-rows.spec.ts new file mode 100644 index 0000000000..e65959f8d5 --- /dev/null +++ b/packages/boot/plugin-manager/tests/bundle-rows.spec.ts @@ -0,0 +1,60 @@ +/** Declaration identity and module ownership before activating a bundle. */ +import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { expect, it, onTestFinished } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import { PluginPackages } from '@deepseek-ai/dsh-app-boot' +import { bundleDeclarations, bundleModuleOwner } from '../src/bundle-rows.ts' + +it('reads nested declarations without requiring their target profile or preset', () => { + const row = { id: 'child', name: './child.mjs' } + expect(bundleDeclarations([ + { id: 'absent-group', insert: [row] }, + { preset: 'absent-preset', insert: [{ id: 'group', name: 'cordis:group', group: true, config: [row] }] }, + { preset: 'absent-preset', id: 'other', disabled: true }, + ])).toEqual([ + { row }, + { row: { id: 'group', name: 'cordis:group', group: true, config: [row] }, preset: 'absent-preset' }, + { row, preset: 'absent-preset' }, + ]) +}) + +it('finds canonical owners for bare, relative, absolute, and file URL modules without evaluating them', async () => { + const dir = mkdtempSync(join(tmpdir(), 'bundle-owner-')) + const ctx = new Context() + onTestFinished(async () => { await ctx.fiber.dispose(); rmSync(dir, { recursive: true, force: true }) }) + const packageDir = join(dir, 'node_modules', 'fixture') + mkdirSync(join(packageDir, 'lib'), { recursive: true }) + const manifest = join(packageDir, 'package.json') + writeFileSync(manifest, '{"name":"fixture","version":"1.0.0","exports":"./lib/index.mjs"}\n') + const module = join(packageDir, 'lib', 'index.mjs') + writeFileSync(module, 'throw new Error("must not import ownership probes")\n') + const base = pathToFileURL(join(dir, 'profile.json')).href + await ctx.plugin(PluginPackages) + const expected = realpathSync(manifest) + expect(bundleModuleOwner('fixture', base, ctx.pluginPackages)).toBe(expected) + expect(bundleModuleOwner('fixture/lib/index.mjs', base, ctx.pluginPackages)).toBe(expected) + expect(bundleModuleOwner('./node_modules/fixture/lib/index.mjs', base)).toBe(expected) + expect(bundleModuleOwner(module, base)).toBe(expected) + expect(bundleModuleOwner(pathToFileURL(module).href, base)).toBe(expected) + expect(bundleModuleOwner('cordis:group', base)).toBeUndefined() + expect(bundleModuleOwner('uninstalled', base)).toBeUndefined() + expect(bundleModuleOwner('./absent.mjs', base)).toBeUndefined() + const loose = join(dir, 'loose.mjs') + writeFileSync(loose, '') + expect(bundleModuleOwner(loose, base)).toBeUndefined() + const linked = join(dir, 'linked') + symlinkSync(packageDir, linked, process.platform === 'win32' ? 'junction' : 'dir') + expect(bundleModuleOwner(pathToFileURL(join(linked, 'lib/index.mjs')).href, base)).toBe(expected) + const otherPackage = join(dir, 'other-package') + mkdirSync(otherPackage) + writeFileSync(join(otherPackage, 'package.json'), '{"name":"other-package"}\n') + const otherModule = join(otherPackage, 'foreign.mjs') + writeFileSync(otherModule, 'export function apply() {}\n') + const fileLink = join(packageDir, 'borrowed.mjs') + symlinkSync(otherModule, fileLink, 'file') + expect(bundleModuleOwner(fileLink, base)).toBe(realpathSync(join(otherPackage, 'package.json'))) + +}) diff --git a/packages/boot/plugin-manager/tests/manager.spec.ts b/packages/boot/plugin-manager/tests/manager.spec.ts index 6e17528400..52cd122dce 100644 --- a/packages/boot/plugin-manager/tests/manager.spec.ts +++ b/packages/boot/plugin-manager/tests/manager.spec.ts @@ -12,7 +12,7 @@ import type { Context } from '@deepseek-ai/cordis' import { afterAll, beforeAll, expect, it, onTestFinished, vi } from 'vitest' import { boot, composeEntries, initProfile, loadProfileDirectory, readProfilePatches, readProfileManifest, - reconcileProfilePatches, OPTIONAL_BUNDLES, PluginPackages, readPluginMeta, getDshRuntimeVersion, + reconcileProfilePatches, OPTIONAL_BUNDLES, OFFICIAL_ON_DEMAND_CATALOG, PluginPackages, readPluginMeta, getDshRuntimeVersion, type ProfileContext, type RuntimeResolution, } from '@deepseek-ai/dsh-app-boot' import PluginManager, { type Config, type PluginChange, type PluginInstallLogChunk, type PluginInstallProgress, type PluginInstallRequestId } from '../src/index.ts' @@ -30,18 +30,24 @@ let restoreGitCommandLineConfig: () => void beforeAll(() => { restoreGitCommandLineConfig = isolateGitCommandLineConfig() }) afterAll(() => { restoreGitCommandLineConfig() }) -async function fixture(reload: 'live' | 'startup' = 'live', overlay = false, prepare?: (ctx: Context) => void, config: Config = {}, packageManager?: ProfileContext['packageManager'], prepareFiles?: (dir: string) => void) { +async function fixture(reload: 'live' | 'startup' = 'live', overlay = false, prepare?: (ctx: Context) => void, config: Config = {}, packageManager?: ProfileContext['packageManager'], prepareFiles?: (dir: string) => void, sourceInstallation = false) { const temporaryHome = mkdtempSync(join(tmpdir(), 'plugin-manager-')) let owner: Context | undefined onTestFinished(async () => { await owner?.fiber.dispose(); rmSync(temporaryHome, { recursive: true, force: true }) }) // pnpm resolves workspace roots through native realpath, including Windows 8.3 aliases. const home = await realpath(temporaryHome) const dir = join(home, 'profiles', 'test') - const anchor = join(home, 'package.json') - writeFileSync(anchor, '{"name":"installation","dependencies":{}}\n') + const anchor = sourceInstallation ? join(home, 'apps', 'cli', 'package.json') : join(home, 'package.json') + if (sourceInstallation) { + mkdirSync(join(home, 'apps', 'cli'), { recursive: true }) + writeFileSync(join(home, 'package.json'), '{"name":"@deepseek-ai/dsh-root"}') + } + writeFileSync(anchor, JSON.stringify(sourceInstallation + ? { name: '@deepseek-ai/dsh', dependencies: { '@deepseek-ai/dsh-app-boot': 'workspace:*' } } + : { name: 'installation', dependencies: {} })) initProfile(dir, ['core', 'extra']) - const bundle = (name: string, rows: unknown[]) => { - const path = join(dir, 'node_modules', name) + const bundle = (name: string, rows: unknown[], packageRoot = dir) => { + const path = join(packageRoot, 'node_modules', name) mkdirSync(path, { recursive: true }) writeFileSync(join(path, 'package.json'), JSON.stringify({ name, version: '1.0.0', dsh: { bundle: { patch: './cordis.patch.yml' } } })) writeFileSync(join(path, 'cordis.patch.yml'), JSON.stringify([{ insert: rows }])) @@ -294,14 +300,14 @@ it('lists bundle versions and current-profile plugin targets', async () => { const plugins = await manager.listPlugins() expect(plugins.find(row => row.entryId === 'include:managed')).toMatchObject({ patchId: 'managed', enabled: true }) expect(plugins.find(row => row.entryId === 'include:manager')?.readOnlyReason).toBe('management-required') - expect(await manager.listBundles()).toEqual([ + expect((await manager.listBundles()).filter(bundle => bundle.installTarget === undefined)).toEqual([ { - name: 'core', version: '1.0.0', enabled: true, installed: false, optional: false, removable: false, readOnlyReason: 'management-required', + name: 'core', version: '1.0.0', enabled: true, installed: false, official: false, availability: 'profile', optional: false, removable: false, readOnlyReason: 'management-required', meta: { title: 'core' }, rows: [{ rowId: 'manager', moduleName: 'cordis:manager', entryId: 'include:manager' }], overrides: [], }, { - name: 'extra', version: '1.0.0', source: 'extra@1.0.0', enabled: true, installed: true, optional: false, removable: true, + name: 'extra', version: '1.0.0', source: 'extra@1.0.0', enabled: true, installed: true, official: false, availability: 'profile', optional: false, removable: true, meta: { title: 'extra' }, rows: [{ rowId: 'managed', moduleName: pathToFileURL(join(dir, 'node_modules', 'extra', 'plugin.mjs')).href, entryId: 'include:managed' }], overrides: [], }, @@ -323,7 +329,7 @@ it('describes a bundle by its manifest and patch: one-liner, rows without a live writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) const moduleName = pathToFileURL(join(dir, 'node_modules', 'described', 'plugin.mjs')).href expect((await manager.listBundles()).find(row => row.name === 'described')).toEqual({ - name: 'described', version: '2.0.0', description: 'Describes itself.', source: 'described@2.0.0', enabled: false, installed: true, optional: false, removable: true, + name: 'described', version: '2.0.0', description: 'Describes itself.', source: 'described@2.0.0', enabled: false, installed: true, official: false, availability: 'profile', optional: false, removable: true, meta: { title: 'described', description: 'Describes itself.' }, rows: [{ rowId: 'described-row', moduleName }], overrides: ['managed'], }) @@ -355,7 +361,8 @@ it('names where each installed bundle comes from as a spec pnpm installs', async writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) // The installation's own copy of a bundle is the one that loads, so the profile's dependency on it names nothing. writeFileSync(profile.installAnchor, JSON.stringify({ name: 'installation', dependencies: { shadowed: '1.0.0' } })) - expect(Object.fromEntries((await manager.listBundles()).map(row => [row.name, row.source]))).toEqual({ + expect(Object.fromEntries((await manager.listBundles()) + .filter(row => row.installTarget === undefined).map(row => [row.name, row.source]))).toEqual({ core: undefined, extra: 'extra@^1.0.0', tagged: 'tagged@latest', aliased: 'aliased@npm:@acme/aliased@2', jsr: 'jsr@jsr:@acme/jsr@^1', github: 'github:someone/dsh-plugin#v1', ssh: 'git@github.com:someone/dsh-plugin.git', deploy: 'deploy@git.corp:team/dsh-plugin.git', sshUrl: 'git+ssh://git@github.com/someone/dsh-plugin.git', email: 'git+http://git.example.com/repo.git', @@ -446,7 +453,7 @@ it.each([ writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) expect((await manager.listBundles()).find(row => row.name === 'unnamed')).toEqual({ - name: 'unnamed', version: '1.0.0', source: 'unnamed@1.0.0', enabled: false, installed: true, optional: false, removable: true, + name: 'unnamed', version: '1.0.0', source: 'unnamed@1.0.0', enabled: false, installed: true, official: false, availability: 'profile', optional: false, removable: true, rows: [], overrides: [], ...expected, }) }) @@ -708,10 +715,10 @@ it('reports a selected plain dependency as a problem, omits an unselected one, a writeFileSync(join(dir, 'node_modules', 'core', 'package.json'), '{"name":"core","dsh":{"bundle":{"patch":"./cordis.patch.yml"}}}') expect((await manager.listBundles())[0]?.version).toBeUndefined() writeFileSync(join(dir, 'package.json'), '{}') - expect(await manager.listBundles()).toEqual([]) + expect((await manager.listBundles()).filter(bundle => bundle.installTarget === undefined)).toEqual([]) expect(await manager.setBundleEnabled('unknown', false)).toMatchObject({ application: 'failed' }) writeFileSync(profile.installAnchor, '{"dependencies":{"missing-builtin":"1"}}') - expect(await manager.listBundles()).toEqual([]) + expect((await manager.listBundles()).filter(bundle => bundle.installTarget === undefined)).toEqual([]) }) it('refuses management bundle disablement and permits repeated bundle selections', async () => { @@ -1084,7 +1091,7 @@ it.each(OPTIONAL_BUNDLES)('offers %s switched off and never removable', async (o expect((await manager.listBundles()).find(row => row.name === offered)).toEqual({ name: offered, version: '3.0.0', description: 'Package one-liner.', meta: { title: offered, description: 'Package one-liner.' }, - enabled: false, installed: false, optional: true, removable: false, + enabled: false, installed: false, official: true, availability: 'installation', optional: true, removable: false, rows: [{ rowId: 'offered-row', moduleName: pathToFileURL(join(supplied, 'plugin.mjs')).href }], overrides: [], }) expect(await manager.setBundleEnabled(offered, true)).toMatchObject({ application: 'applied' }) @@ -1099,7 +1106,7 @@ it('removes a selected bundle no dependency holds by deselecting it without pnpm ...manifest, dsh: { profile: { bundles: [...manifest.dsh?.profile?.bundles ?? [], 'retired'] } }, })) expect((await manager.listBundles()).find(row => row.name === 'retired')).toMatchObject({ - enabled: true, installed: false, optional: false, removable: true, error: { code: 'operation-error' }, + enabled: true, installed: false, official: false, availability: 'missing', optional: false, removable: true, error: { code: 'operation-error' }, }) const pnpm = vi.spyOn(operations, 'runProfilePnpm') onTestFinished(() => { pnpm.mockRestore() }) @@ -1451,3 +1458,206 @@ it.each([false, true])('rechecks installed bundle peers before accepting a disab } expect(readProfileManifest('test', dir).dsh?.profile?.bundles).toEqual(['core', 'extra']) }) + + +it('lists absent Official entries offline without resolving their packages or consulting registries', async () => { + const { manager } = await fixture() + const resolve = vi.spyOn(operations, 'bundleManifest') + const registry = vi.spyOn(operations, 'viewProfilePackage') + onTestFinished(() => { resolve.mockRestore(); registry.mockRestore() }) + const bundles = await manager.listBundles() + for (const entry of OFFICIAL_ON_DEMAND_CATALOG) { + expect(bundles.filter(bundle => bundle.name === entry.packageName)).toEqual([{ + name: entry.packageName, official: true, availability: 'missing', enabled: false, installed: false, optional: false, + removable: false, rows: [], overrides: [], meta: entry.meta, + installTarget: { spec: `${entry.packageName}@${getDshRuntimeVersion()}`, version: getDshRuntimeVersion() }, + }]) + expect(resolve.mock.calls.some(([name]) => name === entry.packageName)).toBe(false) + } + expect(registry).not.toHaveBeenCalled() +}) + +it('distinguishes an unreadable profile dependency from an absent catalog selection', async () => { + const { manager, dir, profile } = await fixture() + const name = OFFICIAL_ON_DEMAND_CATALOG[0]!.packageName + const manifest = readProfileManifest('test', dir) + manifest.dependencies = { ...manifest.dependencies, [name]: '1.0.0' } + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + const broken = join(profile.home, 'node_modules', name) + mkdirSync(broken, { recursive: true }) + writeFileSync(join(broken, 'package.json'), '{') + expect((await manager.listBundles()).find(bundle => bundle.name === name)).toMatchObject({ + official: true, installed: true, availability: 'missing', enabled: false, removable: true, + source: `${name}@1.0.0`, error: { code: 'operation-error' }, + }) +}) + +it('uses the generic exact installer and keeps an Official entry through on, off, and removal', async () => { + const { manager, dir, bundle, profile } = await fixture() + const name = OFFICIAL_ON_DEMAND_CATALOG[0]!.packageName + const version = getDshRuntimeVersion() + const pnpm = vi.spyOn(operations, 'runProfilePnpm').mockImplementation(async (_dir, args) => { + const manifest = readProfileManifest('test', dir) + if (args[0] === 'add') { + bundle(name, [{ id: 'official-probe', name: './plugin.mjs', config: { service: 'officialProbe' } }], profile.home) + const packagePath = join(profile.home, 'node_modules', name, 'package.json') + const packaged = JSON.parse(readFileSync(packagePath, 'utf8')) as Record + writeFileSync(packagePath, JSON.stringify({ ...packaged, version })) + manifest.dependencies = { ...manifest.dependencies, [name]: version } + } else { + delete manifest.dependencies?.[name] + rmSync(join(profile.home, 'node_modules', name), { recursive: true, force: true }) + } + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + return { exitCode: 0, output: '', truncated: false, logPath: '/install.log' } + }) + onTestFinished(() => { pnpm.mockRestore() }) + expect(await manager.installBundle(`${name}@${version}`, { saveExact: true })).toMatchObject({ application: 'applied', enabled: true }) + expect(pnpm.mock.calls[0]?.[1]).toContain('--save-exact') + expect((await manager.listBundles()).find(item => item.name === name)).toMatchObject({ official: true, availability: 'profile', enabled: true, version }) + expect(await manager.setBundleEnabled(name, false)).toMatchObject({ application: 'applied' }) + expect(readProfileManifest('test', dir).dependencies?.[name]).toBe(version) + expect(await manager.removeBundle(name)).toMatchObject({ application: 'applied' }) + expect((await manager.listBundles()).filter(item => item.name === name)).toHaveLength(1) + expect((await manager.listBundles()).find(item => item.name === name)).toMatchObject({ official: true, availability: 'missing', enabled: false, installed: false }) +}) + +it.each([true, false])('updates an Official dependency while preserving selection=%s and requiring restart', async (enabled) => { + const { manager, dir, bundle, profile } = await fixture() + const name = OFFICIAL_ON_DEMAND_CATALOG[0]!.packageName + bundle(name, [], profile.home) + const manifest = readProfileManifest('test', dir) + manifest.dependencies = { ...manifest.dependencies, [name]: '1.0.0' } + if (enabled) manifest.dsh!.profile!.bundles!.push(name) + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + const listed = (await manager.listBundles()).find(item => item.name === name) + expect(listed).toMatchObject({ version: '1.0.0', official: true, availability: 'profile', + installTarget: { spec: `${name}@${getDshRuntimeVersion()}`, version: getDshRuntimeVersion() } }) + expect(listed?.error).toBeUndefined() + const pnpm = vi.spyOn(operations, 'runProfilePnpm').mockImplementation(async () => { + const current = readProfileManifest('test', dir) + current.dependencies = { ...current.dependencies, [name]: getDshRuntimeVersion() } + writeFileSync(join(dir, 'package.json'), JSON.stringify(current)) + return { exitCode: 0, output: '', truncated: false, logPath: '/install.log' } + }) + onTestFinished(() => { pnpm.mockRestore() }) + expect(await manager.installBundle(`${name}@${getDshRuntimeVersion()}`, { enabled, saveExact: true })) + .toMatchObject({ application: 'restart-required', enabled }) + expect(readProfileManifest('test', dir).dsh?.profile?.bundles?.includes(name)).toBe(enabled) +}) + +it('restores dependency and selection after an exact version is unavailable', async () => { + const { manager, dir, bundle, profile } = await fixture() + const name = OFFICIAL_ON_DEMAND_CATALOG[0]!.packageName + bundle(name, [], profile.home) + const manifest = readProfileManifest('test', dir) + manifest.dependencies = { ...manifest.dependencies, [name]: '1.0.0' } + manifest.dsh!.profile!.bundles!.push(name) + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + const before = readFileSync(join(dir, 'package.json'), 'utf8') + const pnpm = vi.spyOn(operations, 'runProfilePnpm').mockImplementation(async () => { + writeFileSync(join(dir, 'package.json'), '{}') + return { exitCode: 1, output: 'No matching version', truncated: false, logPath: '/install.log', kind: 'no-matching-version' } + }) + onTestFinished(() => { pnpm.mockRestore() }) + expect(await manager.installBundle(`${name}@${getDshRuntimeVersion()}`, { saveExact: true })).toMatchObject({ application: 'failed', stage: 'install' }) + expect(readFileSync(join(dir, 'package.json'), 'utf8')).toBe(before) +}) + +it('retains a successful installation and saved selection when Official activation fails', async () => { + const { manager, dir, bundle, profile } = await fixture() + const name = OFFICIAL_ON_DEMAND_CATALOG[0]!.packageName + const pnpm = vi.spyOn(operations, 'runProfilePnpm').mockImplementation(async () => { + bundle(name, [{ id: 'official-probe', name: './plugin.mjs', config: { fail: true } }], profile.home) + const manifest = readProfileManifest('test', dir) + manifest.dependencies = { ...manifest.dependencies, [name]: getDshRuntimeVersion() } + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + return { exitCode: 0, output: '', truncated: false, logPath: '/install.log' } + }) + onTestFinished(() => { pnpm.mockRestore() }) + expect(await manager.installBundle(`${name}@${getDshRuntimeVersion()}`, { saveExact: true })).toMatchObject({ application: 'failed', stage: 'enable' }) + expect(readProfileManifest('test', dir).dsh?.profile?.bundles).toContain(name) + expect(readProfileManifest('test', dir).dependencies?.[name]).toBe(getDshRuntimeVersion()) +}) + +it('reports and removes a saved Official selection whose dependency is absent', async () => { + const { manager, dir } = await fixture() + const name = OFFICIAL_ON_DEMAND_CATALOG[0]!.packageName + const manifest = readProfileManifest('test', dir) + manifest.dsh!.profile!.bundles!.push(name) + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + expect((await manager.listBundles()).find(bundle => bundle.name === name)).toMatchObject({ + official: true, installed: false, availability: 'missing', enabled: true, removable: true, error: { code: 'not-bundle' }, + }) + const pnpm = vi.spyOn(operations, 'runProfilePnpm') + onTestFinished(() => { pnpm.mockRestore() }) + expect(await manager.removeBundle(name)).toMatchObject({ application: 'applied' }) + expect(pnpm).not.toHaveBeenCalled() + expect((await manager.listBundles()).find(bundle => bundle.name === name)).toMatchObject({ availability: 'missing', enabled: false }) +}) + + +it('keeps a broken installation-carried catalog entry visible with offline metadata', async () => { + const { manager, profile } = await fixture() + const entry = OFFICIAL_ON_DEMAND_CATALOG[0]! + writeFileSync(profile.installAnchor, JSON.stringify({ name: 'installation', dependencies: { [entry.packageName]: '1.0.0' } })) + const broken = join(profile.home, 'node_modules', entry.packageName) + mkdirSync(broken, { recursive: true }) + writeFileSync(join(broken, 'package.json'), '{') + expect((await manager.listBundles()).find(bundle => bundle.name === entry.packageName)).toMatchObject({ + official: true, availability: 'missing', enabled: false, installed: false, optional: false, removable: false, + meta: entry.meta, installTarget: { version: getDshRuntimeVersion() }, error: { code: 'operation-error' }, + }) +}) + + +it('offers checkout links beside stale registry and other-checkout dependencies at the same version', async () => { + const { manager, dir, bundle, profile } = await fixture('live', false, undefined, {}, undefined, undefined, true) + const name = OFFICIAL_ON_DEMAND_CATALOG[0]!.packageName + const version = getDshRuntimeVersion() + const source = join(profile.home, 'packages', 'native', 'provider') + mkdirSync(join(source, 'lib'), { recursive: true }) + writeFileSync(join(source, 'package.json'), JSON.stringify({ name, version, main: './lib/index.js', dsh: { bundle: { patch: './cordis.patch.yml' } } })) + writeFileSync(join(source, 'lib', 'index.js'), 'export function apply() {}\n') + writeFileSync(join(source, 'cordis.patch.yml'), '[]\n') + bundle(name, [], profile.home) + const installedManifest = join(profile.home, 'node_modules', name, 'package.json') + writeFileSync(installedManifest, JSON.stringify({ ...JSON.parse(readFileSync(installedManifest, 'utf8')), version })) + const target = { spec: `link:${source}`, version } + expect((await manager.listBundles()).find(item => item.name === name)).toMatchObject({ availability: 'missing', installTarget: target }) + for (const spec of [version, 'link:../other-checkout/native', `link:${source}`]) { + const manifest = readProfileManifest('test', dir) + manifest.dependencies = { ...manifest.dependencies, [name]: spec } + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + const listed = (await manager.listBundles()).find(item => item.name === name) + expect(listed).toMatchObject({ version, official: true, availability: 'profile', installTarget: target }) + expect(listed?.source === listed?.installTarget?.spec).toBe(spec === `link:${source}`) + } +}) + +it.each(['absent', 'bundle', 'not-bundle', 'unreadable'])('keeps a missing source bundle visible when its installed copy is %s', async (state) => { + const { manager, dir, bundle, profile } = await fixture('live', false, undefined, {}, undefined, undefined, true) + const name = OFFICIAL_ON_DEMAND_CATALOG[0]!.packageName + if (state !== 'absent') { + bundle(name, [], profile.home) + const manifest = readProfileManifest('test', dir) + manifest.dependencies = { ...manifest.dependencies, [name]: '1.0.0' } + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + const file = join(profile.home, 'node_modules', name, 'package.json') + if (state === 'not-bundle') writeFileSync(file, JSON.stringify({ name, version: '1.0.0' })) + if (state === 'unreadable') writeFileSync(file, '{') + } + expect((await manager.listBundles()).find(item => item.name === name)).toMatchObject({ + official: true, error: { code: 'operation-error' }, + }) + expect((await manager.listBundles()).find(item => item.name === name)?.error?.diagnostic).toContain('restore the checkout') + expect((await manager.listBundles()).find(item => item.name === name)?.installTarget).toBeUndefined() +}) + +it('recognizes a repeated local link install without a dependency-spec change', async () => { + const { manager, dir } = await fixture() + const install = vi.spyOn(operations, 'runProfilePnpm').mockResolvedValue({ exitCode: 0, output: '', truncated: false, logPath: join(dir, 'pnpm.log') }) + onTestFinished(() => { install.mockRestore() }) + expect(await manager.installBundle(`link:${join(dir, 'node_modules', 'extra')}`, { enabled: false })) + .toMatchObject({ changed: false, application: 'restart-required', bundle: 'extra', enabled: false }) +}) diff --git a/packages/boot/plugin-manager/tests/official-install-target.spec.ts b/packages/boot/plugin-manager/tests/official-install-target.spec.ts new file mode 100644 index 0000000000..96101fa59f --- /dev/null +++ b/packages/boot/plugin-manager/tests/official-install-target.spec.ts @@ -0,0 +1,100 @@ +/** Official installation targets follow the actual CLI installation and its workspace packages. */ +import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { expect, it, onTestFinished } from 'vitest' +import { officialBundleInstallTarget } from '../src/official-install-target.ts' + +const name = '@deepseek-ai/dsh-subagent-codex' +const version = '0.2.1-alpha.1' + +function fixture() { + const temporary = mkdtempSync(join(tmpdir(), 'official-target-')) + onTestFinished(() => { rmSync(temporary, { recursive: true, force: true }) }) + const root = realpathSync(temporary) + const anchor = join(root, 'apps', 'cli', 'package.json') + mkdirSync(dirname(anchor), { recursive: true }) + writeFileSync(join(root, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-root' })) + writeFileSync(anchor, JSON.stringify({ name: '@deepseek-ai/dsh', version, dependencies: { '@deepseek-ai/dsh-app-boot': 'workspace:*' } })) + const directory = join(root, 'packages', 'relocated', 'native-provider') + mkdirSync(join(directory, 'lib'), { recursive: true }) + writeFileSync(join(directory, 'package.json'), JSON.stringify({ name, version, main: 'lib/index.js', dsh: { bundle: { patch: './cordis.patch.yml' } } })) + writeFileSync(join(directory, 'lib', 'index.js'), 'export function apply() {}\n') + writeFileSync(join(directory, 'cordis.patch.yml'), '[]\n') + return { root, anchor, directory } +} + +it('offers an exact prerelease registry target for a packed installation inside a checkout', () => { + const { anchor } = fixture() + writeFileSync(anchor, JSON.stringify({ name: '@deepseek-ai/dsh', version, dependencies: { '@deepseek-ai/dsh-app-boot': version } })) + expect(officialBundleInstallTarget(name, anchor, version)).toEqual({ spec: `${name}@${version}`, version }) +}) + +it('keeps an installation without workspace dependencies on the registry path', () => { + const { anchor } = fixture() + writeFileSync(anchor, JSON.stringify({ name: '@deepseek-ai/dsh', version })) + expect(officialBundleInstallTarget(name, anchor, version)).toEqual({ spec: `${name}@${version}`, version }) +}) + +it('finds the package by manifest name rather than assuming its workspace directory', () => { + const { anchor, directory } = fixture() + expect(officialBundleInstallTarget(name, anchor, version)).toEqual({ spec: `link:${directory}`, version }) +}) + +it('follows the CLI directory link used by an unpackaged Desktop application', () => { + const { root, anchor, directory } = fixture() + const link = join(root, 'desktop-runtime', 'node_modules', '@deepseek-ai', 'dsh') + mkdirSync(dirname(link), { recursive: true }) + symlinkSync(dirname(anchor), link, process.platform === 'win32' ? 'junction' : 'dir') + expect(officialBundleInstallTarget(name, join(link, 'package.json'), version)).toEqual({ spec: `link:${directory}`, version }) +}) + +it.each(['missing', 'wrong-name', 'wrong-location'])('refuses a source CLI with a %s workspace root', (kind) => { + const { root, anchor } = fixture() + if (kind === 'missing') rmSync(join(root, 'package.json')) + if (kind === 'wrong-name') writeFileSync(join(root, 'package.json'), '{"name":"unrelated"}') + const selected = kind === 'wrong-location' ? join(root, 'package.json') : anchor + if (kind === 'wrong-location') writeFileSync(selected, readFileSync(anchor)) + expect(() => officialBundleInstallTarget(name, selected, version)).toThrow('requires its complete workspace checkout') +}) + +it.each(['missing', 'duplicate'])('refuses a %s workspace package without offering a registry fallback', (kind) => { + const { root, anchor, directory } = fixture() + if (kind === 'missing') rmSync(directory, { recursive: true }) + else { + const duplicate = join(root, 'packages', 'elsewhere', 'duplicate') + mkdirSync(duplicate, { recursive: true }) + writeFileSync(join(duplicate, 'package.json'), readFileSync(join(directory, 'package.json'))) + } + expect(() => officialBundleInstallTarget(name, anchor, version)).toThrow('expected one workspace package') +}) + +it.each([ + { version: '0.1.0' }, { private: true }, { dsh: {} }, +])('refuses a workspace package with invalid release or bundle metadata: %j', (changes) => { + const { anchor, directory } = fixture() + const file = join(directory, 'package.json') + writeFileSync(file, JSON.stringify({ ...JSON.parse(readFileSync(file, 'utf8')), ...changes })) + expect(() => officialBundleInstallTarget(name, anchor, version)).toThrow('must declare a public bundle at DSH version') +}) + +it.each(['missing-main', 'missing-output', 'directory-output'])('refuses an unbuilt workspace package: %s', (kind) => { + const { anchor, directory } = fixture() + if (kind === 'missing-main') { + const file = join(directory, 'package.json') + writeFileSync(file, JSON.stringify({ ...JSON.parse(readFileSync(file, 'utf8')), main: undefined })) + } else { + const output = join(directory, 'lib', 'index.js') + rmSync(output) + if (kind === 'directory-output') mkdirSync(output) + } + expect(() => officialBundleInstallTarget(name, anchor, version)).toThrow('run pnpm install and pnpm run build') +}) + +it.each(['missing', 'directory'])('refuses an unavailable bundle patch: %s', (kind) => { + const { anchor, directory } = fixture() + const patch = join(directory, 'cordis.patch.yml') + rmSync(patch) + if (kind === 'directory') mkdirSync(patch) + expect(() => officialBundleInstallTarget(name, anchor, version)).toThrow('bundle patch') +}) diff --git a/packages/boot/plugin-manager/tests/operations.spec.ts b/packages/boot/plugin-manager/tests/operations.spec.ts index 9a062e8af9..f9aa34314c 100644 --- a/packages/boot/plugin-manager/tests/operations.spec.ts +++ b/packages/boot/plugin-manager/tests/operations.spec.ts @@ -471,7 +471,7 @@ it.each(['devDependencies', 'optionalDependencies'])('checks installed direct pl expect(await runProfilePnpm(context, ['install'], { execution: 'service', outputBytes: 8192 })).toMatchObject({ exitCode: 1 }) }) -it('rejects an incompatible component declared by a newly installed bundle', async () => { +it.each([undefined, 'preset-standard'])('rejects an incompatible component contributed to %s by a newly installed bundle', async (preset) => { const { dir, context, pnpm } = fixture() pnpm.mutate = (target) => { install(target, 'bundle') @@ -481,7 +481,16 @@ it('rejects an incompatible component declared by a newly installed bundle', asy writeFileSync(join(component, 'package.json'), JSON.stringify({ name: 'component', version: '1.0.0', peerDependencies: { '@deepseek-ai/dsh': '>=999.0.0' }, })) - writeFileSync(join(packageDir, 'cordis.patch.yml'), '- insert:\n - id: component\n name: component/subpath\n') + const unrelated = join(target, 'node_modules', 'component') + mkdirSync(unrelated, { recursive: true }) + writeFileSync(join(unrelated, 'package.json'), JSON.stringify({ name: 'component', version: '2.0.0' })) + mkdirSync(join(packageDir, 'nested')) + writeFileSync(join(packageDir, 'nested', 'group.mjs'), 'export function apply() {}\n') + writeFileSync(join(packageDir, 'cordis.patch.yml'), JSON.stringify([{ + ...preset === undefined ? {} : { preset }, insert: [{ id: 'relative-group', name: './nested/group.mjs', group: true, + config: [{ id: 'component', name: 'component/subpath' }], + }], + }])) } const outcome = await runProfilePnpm(context, ['add', 'bundle'], { execution: 'service', outputBytes: 8192, activateNewBundles: false }) expect(outcome.exitCode).toBe(1) @@ -503,7 +512,7 @@ it('restores the manifest on malformed installed peer metadata and bounds the wa expect(readFileSync(join(dir, 'package.json'), 'utf8')).toBe(before) }) -it('checks scoped components in nested groups while leaving unresolved and local rows to startup', async () => { +it('validates nested preset contributions without requiring their target preset to be selected', async () => { const { context, pnpm } = fixture() pnpm.mutate = (target) => { install(target, 'bundle') @@ -511,7 +520,7 @@ it('checks scoped components in nested groups while leaving unresolved and local const component = join(packageDir, 'node_modules', '@example', 'component') mkdirSync(component, { recursive: true }) writeFileSync(join(component, 'package.json'), JSON.stringify({ name: '@example/component', version: '1.0.0' })) - writeFileSync(join(packageDir, 'cordis.patch.yml'), JSON.stringify([{ insert: [ + writeFileSync(join(packageDir, 'cordis.patch.yml'), JSON.stringify([{ preset: 'preset-standard', insert: [ { id: 'group', group: true, config: [{ id: 'component', name: '@example/component/subpath' }] }, { id: 'empty-group', group: true, config: {} }, { id: 'relative', name: './plugin.mjs' }, diff --git a/packages/boot/plugin-manager/tests/package-reload.spec.ts b/packages/boot/plugin-manager/tests/package-reload.spec.ts new file mode 100644 index 0000000000..d83aab3be3 --- /dev/null +++ b/packages/boot/plugin-manager/tests/package-reload.spec.ts @@ -0,0 +1,457 @@ +/** GUI package operations publish profile package resolution around activation and disposal through the real Loader. */ +import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { FiberState, type Context } from '@deepseek-ai/cordis' +import Timer from '@deepseek-ai/cordis-plugin-timer' +import { + boot, createRuntimeResolution, initProfile, loadProfileDirectory, PluginPackages, + readProfileManifest, readProfilePatches, type ProfileContext, +} from '@deepseek-ai/dsh-app-boot' +import Hmr from '@deepseek-ai/dsh-hmr' +import type { ChokidarOptions } from 'chokidar' +import { expect, it, onTestFinished, vi } from 'vitest' +import PluginManager, { type PluginInstallRequestId } from '../src/index.ts' +import * as operations from '../src/operations.ts' + +// Changes in these cases come only from GUI operations, never from file notifications. +vi.mock('chokidar', async (original) => { + const actual = await original() + return { ...actual, watch: (_paths: string | string[], options: ChokidarOptions = {}) => { + const watcher = new actual.FSWatcher(options) + queueMicrotask(() => watcher.emit('ready')) + return watcher + } } +}) + +/** Write the `addon` bundle, whose plugin is its private bare dependency `addon-plugin`. */ +function writeBundle(dir: string, version: number): void { + const bundleDir = join(dir, 'node_modules', 'addon') + const pluginDir = join(bundleDir, 'node_modules', 'addon-plugin') + rmSync(bundleDir, { recursive: true, force: true }) + mkdirSync(pluginDir, { recursive: true }) + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify({ + name: 'addon', version: `${version}.0.0`, type: 'module', + dependencies: { 'addon-plugin': `${version}.0.0` }, dsh: { bundle: { patch: './cordis.patch.yml' } }, + })) + writeFileSync(join(bundleDir, 'cordis.patch.yml'), JSON.stringify([{ insert: [{ id: 'addon', name: 'addon-plugin' }] }])) + writeFileSync(join(pluginDir, 'package.json'), JSON.stringify({ + name: 'addon-plugin', version: `${version}.0.0`, type: 'module', exports: './plugin.mjs', + })) + writeFileSync(join(pluginDir, 'plugin.mjs'), ` +import { appendFileSync } from 'node:fs'; +appendFileSync(${JSON.stringify(join(dir, 'addon-evaluations.log'))}, ${JSON.stringify(`addon:${version}\n`)}); +export function apply(ctx) { + ctx.get('packageReloadTrace').push('start:addon:${version}'); + ctx.provide('addonVersion', ${version}); + ctx.effect(() => () => { ctx.get('packageReloadTrace').push('stop:addon:${version}'); }); +} +`) +} + +/** Write `addon` and save it as a profile dependency, as `pnpm add` does. */ +function installFiles(dir: string, version: number): void { + writeBundle(dir, version) + const manifest = readProfileManifest('test', dir) + manifest.dependencies = { ...manifest.dependencies, addon: `${version}.0.0` } + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) +} + +/** Delete `addon` and drop it from the profile dependencies, as `pnpm remove` does. */ +function removeFiles(dir: string): void { + const manifest = readProfileManifest('test', dir) + delete manifest.dependencies?.addon + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + rmSync(join(dir, 'node_modules', 'addon'), { recursive: true }) +} + +function installOtherBundle(dir: string, shared = false): void { + const bundleDir = join(dir, 'node_modules', 'other') + mkdirSync(bundleDir, { recursive: true }) + writeFileSync(join(bundleDir, 'package.json'), JSON.stringify({ + name: 'other', version: '1.0.0', dependencies: shared ? { 'addon-plugin': '1.0.0' } : {}, + dsh: { bundle: { patch: './cordis.patch.yml' } }, + })) + writeFileSync(join(bundleDir, 'cordis.patch.yml'), '[]\n') + const manifest = readProfileManifest('test', dir) + manifest.dependencies = { ...manifest.dependencies, other: '1.0.0' } + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) +} + +async function fixture(options: { live?: boolean; installed?: boolean; enabled?: boolean; shared?: boolean } = {}) { + const home = realpathSync.native(mkdtempSync(join(tmpdir(), 'manager-package-reload-'))) + let owner: Context | undefined + onTestFinished(async () => { + await owner?.fiber.dispose() + rmSync(home, { recursive: true, force: true }) + }) + const dir = join(home, 'profiles', 'test') + const installAnchor = join(home, 'package.json') + writeFileSync(installAnchor, '{"name":"installation","dependencies":{}}\n') + initProfile(dir, options.installed === true && options.enabled !== false ? ['core', 'addon'] : ['core']) + writeFileSync(join(dir, 'cordis.yml'), '[]\n') + const core = join(dir, 'node_modules', 'core') + mkdirSync(core, { recursive: true }) + writeFileSync(join(core, 'package.json'), JSON.stringify({ + name: 'core', version: '1.0.0', dsh: { bundle: { patch: './cordis.patch.yml' } }, + })) + writeFileSync(join(core, 'cordis.patch.yml'), JSON.stringify([{ insert: [ + { id: 'manager', name: 'cordis:manager' }, + { id: 'sibling', name: './sibling.mjs' }, + ] }])) + writeFileSync(join(core, 'sibling.mjs'), ` +export function apply(ctx) { + ctx.get('packageReloadTrace').push('start:sibling'); + ctx.provide('packageReloadSibling', {}); + ctx.effect(() => () => { ctx.get('packageReloadTrace').push('stop:sibling'); }); +} +`) + const manifest = readProfileManifest('test', dir) + manifest.dependencies = {} + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + if (options.installed === true) installFiles(dir, 1) + if (options.shared === true) { + renameSync(join(dir, 'node_modules', 'addon', 'node_modules', 'addon-plugin'), join(dir, 'node_modules', 'addon-plugin')) + installOtherBundle(dir, true) + const shared = readProfileManifest('test', dir) + shared.dsh = { profile: { bundles: ['core', 'addon', 'other'] } } + writeFileSync(join(dir, 'package.json'), JSON.stringify(shared)) + } + const loaded = loadProfileDirectory('test', dir, installAnchor) + const profile: ProfileContext = { + name: 'test', dir, patchPath: join(dir, 'cordis.patch.yml'), installAnchor, cwd: home, home, + startedBundles: loaded.layers.map(layer => layer.packageName), overlays: [], telemetryDisabledEnv: undefined, + } + const resolution = await createRuntimeResolution({ installAnchor, profile: loaded, home }) + const trace: string[] = [] + const ctx = await boot('test', join(dir, 'cordis.yml'), readProfilePatches('test', profile), async (host) => { + owner = host + host.provide('profileContext', profile) + host.provide('appReady', { onReady(listener) { listener(); return () => {} } }) + host.provide('packageReloadTrace', trace) + host.loader.builtins.manager = PluginManager + await host.plugin(PluginPackages, { resolution }) + }) + if (options.live !== false) { + await ctx.plugin(Timer) + await ctx.plugin(Hmr, { root: [], ignored: [], debounce: 0 }) + await ctx.hmr.runExclusive(async () => {}) + } + const registry = vi.spyOn(operations, 'readProfileRegistry').mockResolvedValue('https://registry.npmjs.org/') + onTestFinished(() => { registry.mockRestore() }) + // Publications share the plugins' trace, so their order relative to plugin starts and stops is observable. + const replace = ctx.pluginPackages.replace.bind(ctx.pluginPackages) + const published = vi.spyOn(ctx.pluginPackages, 'replace').mockImplementation((successor) => { + trace.push('publish') + replace(successor) + }) + onTestFinished(() => { published.mockRestore() }) + const bundleDir = join(dir, 'node_modules', 'addon') + return { + ctx, dir, trace, published, manager: ctx.pluginManager, + parentURL: pathToFileURL(join(dir, 'caller.cjs')).href, + bundleURL: pathToFileURL(join(bundleDir, 'package.json')).href, + bundleDir, + evaluationPath: join(dir, 'addon-evaluations.log'), + entry: () => [...ctx.loader.entries()].find(row => row.id === 'include:addon'), + } +} + +/** Answer each pnpm run by recording it in the trace and applying `run` to the profile files. */ +function mockPnpm(dir: string, trace: string[], run: (args: readonly string[]) => void, exitCode = 0) { + const pnpm = vi.spyOn(operations, 'runProfilePnpm').mockImplementation(async (_context, args) => { + trace.push(`pnpm:${String(args[0])}`) + run(args) + return { + exitCode, output: exitCode === 0 ? 'completed' : 'fixture install failed', truncated: false, logPath: join(dir, 'pnpm.log'), + } + }) + onTestFinished(() => { pnpm.mockRestore() }) + return pnpm +} + +it('publishes a newly installed enabled bundle before its private bare plugin starts', async () => { + const { ctx, dir, trace, published, manager, parentURL, evaluationPath, entry } = await fixture() + expect(ctx.pluginPackages.packageOf('addon', parentURL)).toBeUndefined() + mockPnpm(dir, trace, (args) => { + expect(args.slice(0, 2)).toEqual(['add', 'addon']) + installFiles(dir, 1) + }) + + expect(await manager.installBundle('addon', { enabled: true })).toMatchObject({ application: 'applied', changed: true }) + expect(published).toHaveBeenCalledOnce() + expect(trace).toEqual(['start:sibling', 'pnpm:add', 'publish', 'start:addon:1']) + expect(entry()?.fiber?.state).toBe(FiberState.ACTIVE) + expect(ctx.get('addonVersion')).toBe(1) + expect(ctx.pluginPackages.packageOf('addon-plugin', parentURL)?.version).toBe('1.0.0') + expect(readFileSync(evaluationPath, 'utf8')).toBe('addon:1\n') +}) + +it('publishes a newly installed disabled bundle without evaluating its plugin', async () => { + const { ctx, dir, trace, published, manager, parentURL, bundleURL, evaluationPath, entry } = await fixture() + mockPnpm(dir, trace, () => { installFiles(dir, 1) }) + + expect(await manager.installBundle('addon', { enabled: false })).toMatchObject({ application: 'applied', changed: true }) + expect(published).toHaveBeenCalledOnce() + expect(trace).toEqual(['start:sibling', 'pnpm:add', 'publish']) + expect(entry()).toBeUndefined() + expect(existsSync(evaluationPath)).toBe(false) + expect(ctx.pluginPackages.packageOf('addon', parentURL)?.version).toBe('1.0.0') + expect(ctx.pluginPackages.packageOf('addon-plugin', bundleURL)?.version).toBe('1.0.0') +}) + +it('publishes an enabled bundle before its plugin starts', async () => { + const { ctx, dir, trace, published, manager, parentURL, evaluationPath, entry } = await fixture() + mockPnpm(dir, trace, () => { installFiles(dir, 1) }) + expect(await manager.installBundle('addon', { enabled: false })).toMatchObject({ application: 'applied' }) + published.mockClear() + trace.length = 0 + + expect(await manager.setBundleEnabled('addon', true)).toMatchObject({ application: 'applied', changed: true }) + expect(published).toHaveBeenCalledOnce() + expect(trace).toEqual(['publish', 'start:addon:1']) + expect(entry()?.fiber?.state).toBe(FiberState.ACTIVE) + expect(ctx.get('addonVersion')).toBe(1) + expect(ctx.pluginPackages.packageOf('addon-plugin', parentURL)?.version).toBe('1.0.0') + expect(readFileSync(evaluationPath, 'utf8')).toBe('addon:1\n') +}) + +it('publishes a disabled bundle only after its plugin stops', async () => { + const { ctx, trace, published, manager, parentURL, entry } = await fixture({ installed: true }) + expect(entry()?.fiber?.state).toBe(FiberState.ACTIVE) + expect(ctx.pluginPackages.packageOf('addon-plugin', parentURL)?.version).toBe('1.0.0') + const sibling: unknown = ctx.get('packageReloadSibling') + + expect(await manager.setBundleEnabled('addon', false)).toMatchObject({ application: 'applied', changed: true }) + expect(published).toHaveBeenCalledOnce() + expect(trace).toEqual(['start:sibling', 'start:addon:1', 'stop:addon:1', 'publish']) + expect(entry()).toBeUndefined() + expect(ctx.get('addonVersion')).toBeUndefined() + expect(ctx.pluginPackages.packageOf('addon-plugin', parentURL)).toBeUndefined() + expect(ctx.pluginPackages.packageOf('addon', parentURL)?.version).toBe('1.0.0') + expect(ctx.get('packageReloadSibling')).toBe(sibling) +}) + +it('keeps the running plugin and its private package lookup when a bundle is disabled without HMR', async () => { + const { ctx, trace, published, manager, parentURL, entry } = await fixture({ live: false, installed: true }) + const previous = entry()?.fiber + expect(previous?.state).toBe(FiberState.ACTIVE) + + expect(await manager.setBundleEnabled('addon', false)).toMatchObject({ application: 'restart-required', changed: true }) + expect(published).not.toHaveBeenCalled() + expect(entry()?.fiber).toBe(previous) + expect(previous?.state).toBe(FiberState.ACTIVE) + expect(ctx.get('addonVersion')).toBe(1) + expect(ctx.pluginPackages.packageOf('addon-plugin', parentURL)?.version).toBe('1.0.0') + expect(trace).toEqual(['start:sibling', 'start:addon:1']) +}) + +it('keeps the running bundle resolution through later package operations without HMR', async () => { + const { ctx, dir, trace, published, manager, parentURL, entry } = await fixture({ live: false, installed: true }) + const previous = entry()?.fiber + const mapping = ctx.pluginPackages.packageOf('addon-plugin', parentURL) + expect(previous?.state).toBe(FiberState.ACTIVE) + expect(mapping?.version).toBe('1.0.0') + mockPnpm(dir, trace, (args) => { + if (args[0] === 'add') { + installOtherBundle(dir) + } else { + expect(args).toEqual(['remove', 'other']) + const manifest = readProfileManifest('test', dir) + delete manifest.dependencies?.other + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + rmSync(join(dir, 'node_modules', 'other'), { recursive: true }) + } + }) + const expectRunningBundle = () => { + expect(ctx.pluginPackages.packageOf('addon-plugin', parentURL)).toBe(mapping) + expect(entry()?.fiber).toBe(previous) + expect(previous?.state).toBe(FiberState.ACTIVE) + expect(ctx.get('addonVersion')).toBe(1) + expect(published).not.toHaveBeenCalled() + } + + expect(await manager.setBundleEnabled('addon', false)).toMatchObject({ application: 'restart-required', changed: true }) + expectRunningBundle() + expect(await manager.installBundle('other', { enabled: false })).toMatchObject({ application: 'restart-required', changed: true }) + expectRunningBundle() + expect(await manager.setBundleEnabled('other', true)).toMatchObject({ application: 'restart-required', changed: true }) + expectRunningBundle() + expect(await manager.removeBundle('other')).toMatchObject({ application: 'restart-required', changed: true }) + expectRunningBundle() + expect(readProfileManifest('test', dir).dsh?.profile?.bundles).toEqual(['core']) + expect(readProfileManifest('test', dir).dependencies).toEqual({ addon: '1.0.0' }) + expect(existsSync(join(dir, 'node_modules', 'other'))).toBe(false) + expect(trace).toEqual(['start:sibling', 'start:addon:1', 'pnpm:add', 'pnpm:remove']) +}) + +it.each(['disable', 'remove'] as const)('retains a shared dependency after %s of its first declaring bundle', async (operation) => { + const { ctx, dir, trace, published, manager, parentURL, entry } = await fixture({ installed: true, shared: true }) + const sharedDir = join(dir, 'node_modules', 'addon-plugin') + expect(ctx.pluginPackages.packageOf('addon-plugin', parentURL)?.dir).toBe(sharedDir) + mockPnpm(dir, trace, () => { removeFiles(dir) }) + + const result = operation === 'disable' ? await manager.setBundleEnabled('addon', false) : await manager.removeBundle('addon') + + expect(result).toMatchObject({ application: 'applied', changed: true }) + expect(published).toHaveBeenCalledOnce() + expect(published.mock.calls[0]?.[0].entries.find(item => item.name === 'addon-plugin')).toMatchObject({ + packageDir: sharedDir, version: '1.0.0', scope: 'profile', declarer: join(dir, 'node_modules', 'other', 'package.json'), + }) + expect(entry()).toBeUndefined() + expect(readProfileManifest('test', dir).dsh?.profile?.bundles).toEqual(['core', 'other']) + expect(ctx.pluginPackages.packageOf('addon-plugin', parentURL)?.dir).toBe(sharedDir) + expect(createRequire(join(dir, 'node_modules', 'other', 'package.json')).resolve('addon-plugin')).toBe(join(sharedDir, 'plugin.mjs')) +}) + +it('removes a running bundle after its plugin stops and then publishes', async () => { + const { ctx, dir, trace, published, manager, parentURL, bundleURL, bundleDir, entry } = await fixture({ installed: true }) + const previous = entry()?.fiber + expect(previous?.state).toBe(FiberState.ACTIVE) + const sibling: unknown = ctx.get('packageReloadSibling') + const pnpm = mockPnpm(dir, trace, (args) => { + expect(args).toEqual(['remove', 'addon']) + expect(entry()).toBeUndefined() + removeFiles(dir) + }) + + expect(await manager.removeBundle('addon')).toMatchObject({ + application: 'applied', changed: true, packageResult: { exitCode: 0 }, + }) + expect(pnpm).toHaveBeenCalledOnce() + expect(published).toHaveBeenCalledOnce() + expect(trace).toEqual(['start:sibling', 'start:addon:1', 'stop:addon:1', 'pnpm:remove', 'publish']) + expect(previous?.uid).toBeNull() + expect(existsSync(bundleDir)).toBe(false) + expect(ctx.pluginPackages.packageOf('addon', parentURL)).toBeUndefined() + expect(ctx.pluginPackages.packageOf('addon-plugin', parentURL)).toBeUndefined() + expect(ctx.pluginPackages.packageOf('addon-plugin', bundleURL)).toBeUndefined() + expect(ctx.get('packageReloadSibling')).toBe(sibling) + expect((await manager.listBundles()).some(bundle => bundle.name === 'addon')).toBe(false) +}) + +it('removes and publishes a bundle that is not running without HMR', async () => { + const { ctx, dir, trace, published, manager, parentURL, bundleDir, evaluationPath } = await fixture({ + live: false, installed: true, enabled: false, + }) + expect(ctx.pluginPackages.packageOf('addon', parentURL)?.version).toBe('1.0.0') + const sibling: unknown = ctx.get('packageReloadSibling') + const pnpm = mockPnpm(dir, trace, (args) => { + expect(args).toEqual(['remove', 'addon']) + removeFiles(dir) + }) + + expect(await manager.removeBundle('addon')).toMatchObject({ + application: 'restart-required', changed: true, packageResult: { exitCode: 0 }, + }) + expect(pnpm).toHaveBeenCalledOnce() + expect(published).toHaveBeenCalledOnce() + expect(trace).toEqual(['start:sibling', 'pnpm:remove', 'publish']) + expect(existsSync(bundleDir)).toBe(false) + expect(readProfileManifest('test', dir).dependencies).not.toHaveProperty('addon') + expect(ctx.pluginPackages.packageOf('addon', parentURL)).toBeUndefined() + expect(existsSync(evaluationPath)).toBe(false) + expect(ctx.get('packageReloadSibling')).toBe(sibling) +}) + +it.each([{ live: true }, { live: false }])('keeps the running version and publishes nothing when an installed bundle is overwritten with HMR=$live', async ({ live }) => { + const { ctx, dir, trace, published, manager, evaluationPath, entry } = await fixture({ live, installed: true }) + const previous = entry()?.fiber + expect(previous?.state).toBe(FiberState.ACTIVE) + expect(ctx.get('addonVersion')).toBe(1) + mockPnpm(dir, trace, (args) => { + expect(args.slice(0, 2)).toEqual(['add', 'addon@2']) + installFiles(dir, 2) + }) + + expect(await manager.installBundle('addon@2', { enabled: true })).toMatchObject({ application: 'restart-required', changed: true }) + expect(published).not.toHaveBeenCalled() + expect(trace).toEqual(['start:sibling', 'start:addon:1', 'pnpm:add']) + expect(entry()?.fiber).toBe(previous) + expect(previous?.state).toBe(FiberState.ACTIVE) + expect(ctx.get('addonVersion')).toBe(1) + expect(readFileSync(evaluationPath, 'utf8')).toBe('addon:1\n') +}) + +it('publishes nothing after installation fails', async () => { + const { ctx, dir, trace, published, manager, parentURL, evaluationPath, entry } = await fixture() + mockPnpm(dir, trace, () => { installFiles(dir, 1) }, 1) + + expect(await manager.installBundle('addon', { enabled: true })).toMatchObject({ + application: 'failed', changed: false, error: { diagnostic: 'fixture install failed' }, packageResult: { exitCode: 1 }, + }) + expect(published).not.toHaveBeenCalled() + expect(trace).toEqual(['start:sibling', 'pnpm:add']) + expect(readProfileManifest('test', dir).dependencies).toEqual({}) + expect(ctx.pluginPackages.packageOf('addon-plugin', parentURL)).toBeUndefined() + expect(entry()).toBeUndefined() + expect(existsSync(evaluationPath)).toBe(false) +}) + +it.each(['install', 'remove'] as const)('keeps successful %s disk changes when resolution publication fails', async (operation) => { + const { ctx, dir, trace, published, manager, parentURL, entry } = await fixture({ installed: operation === 'remove' }) + const previous = ctx.pluginPackages.packageOf('addon-plugin', parentURL) + mockPnpm(dir, trace, () => { if (operation === 'install') installFiles(dir, 1); else removeFiles(dir) }) + published.mockImplementation(() => { throw new Error('publication rejected') }) + + const result = operation === 'install' ? await manager.installBundle('addon') : await manager.removeBundle('addon') + + expect(result).toMatchObject({ + application: 'failed', changed: true, error: { diagnostic: 'publication rejected' }, packageResult: { exitCode: 0 }, + }) + expect(published).toHaveBeenCalledOnce() + const manifest = readProfileManifest('test', dir) + expect(manifest.dependencies).toEqual(operation === 'install' ? { addon: '1.0.0' } : {}) + expect(manifest.dsh?.profile?.bundles).toEqual(operation === 'install' ? ['core', 'addon'] : ['core']) + expect(existsSync(join(dir, 'node_modules', 'addon'))).toBe(operation === 'install') + expect(ctx.pluginPackages.packageOf('addon-plugin', parentURL)).toBe(previous) + expect(entry()).toBeUndefined() +}) + +it('publishes nothing after installation is cancelled', async () => { + const { ctx, dir, trace, published, manager, parentURL, evaluationPath, entry } = await fixture() + const started = Promise.withResolvers() + const stopped = Promise.withResolvers() + const pnpm = vi.spyOn(operations, 'runProfilePnpm').mockImplementation(async (_context, _args, options) => { + installFiles(dir, 1) + const signal = options.signal + if (signal === undefined) throw new Error('Installation did not supply its cancellation signal') + const abort = () => { stopped.resolve(undefined) } + signal.addEventListener('abort', abort, { once: true }) + started.resolve(undefined) + try { await stopped.promise } finally { signal.removeEventListener('abort', abort) } + return { exitCode: 1, output: 'cancelled', truncated: false, logPath: join(dir, 'pnpm.log') } + }) + const requestId = '9feec888-53de-4c23-8486-965e5a0367b3' as PluginInstallRequestId + const installing = manager.installBundle('addon', { enabled: true, requestId }) + onTestFinished(async () => { + stopped.resolve(undefined) + await installing + pnpm.mockRestore() + }) + await started.promise + + expect(await manager.cancelInstall(requestId)).toEqual({ status: 'cancelled' }) + expect(await installing).toMatchObject({ application: 'cancelled', changed: false }) + expect(published).not.toHaveBeenCalled() + expect(trace).toEqual(['start:sibling']) + expect(readProfileManifest('test', dir).dependencies).toEqual({}) + expect(ctx.pluginPackages.packageOf('addon-plugin', parentURL)).toBeUndefined() + expect(entry()).toBeUndefined() + expect(existsSync(evaluationPath)).toBe(false) +}) + +it('publishes a newly installed bundle without HMR and leaves its activation to the restart', async () => { + const { ctx, dir, trace, published, manager, parentURL, evaluationPath, entry } = await fixture({ live: false }) + mockPnpm(dir, trace, () => { installFiles(dir, 1) }) + + expect(await manager.installBundle('addon', { enabled: true })).toMatchObject({ application: 'restart-required', changed: true }) + expect(published).toHaveBeenCalledOnce() + expect(trace).toEqual(['start:sibling', 'pnpm:add', 'publish']) + expect(ctx.pluginPackages.packageOf('addon', parentURL)?.version).toBe('1.0.0') + expect(entry()).toBeUndefined() + expect(ctx.get('addonVersion')).toBeUndefined() + expect(existsSync(evaluationPath)).toBe(false) +}) diff --git a/packages/boot/plugin-manager/tests/patch.spec.ts b/packages/boot/plugin-manager/tests/patch.spec.ts index 30bb947c84..a51c544d2f 100644 --- a/packages/boot/plugin-manager/tests/patch.spec.ts +++ b/packages/boot/plugin-manager/tests/patch.spec.ts @@ -41,6 +41,19 @@ it('creates a missing patch file and appends after insertions', async () => { ]) }) +it.each([false, true])('preserves same-ID preset overrides when a Host override exists: %s', async (hostOverride) => { + const preset = { preset: 'preset-standard', id: 'tool-bash', name: 'package', disabled: true } + const file = await fixture(JSON.stringify([ + ...hostOverride ? [{ id: 'tool-bash', disabled: false }] : [], preset, + ])) + expect(await writePluginEnabled(file, 'tool-bash', 'package', false)).toBe(true) + const disabled = { id: 'tool-bash', disabled: true } + expect(loadOptionalPatches('test', file)).toEqual(hostOverride ? [disabled, preset] : [preset, disabled]) + expect(await writePluginEnabled(file, 'tool-bash', 'package', true)).toBe(true) + const enabled = { id: 'tool-bash', disabled: false } + expect(loadOptionalPatches('test', file)).toEqual(hostOverride ? [enabled, preset] : [preset, enabled]) +}) + it.each(['- id: [broken', 'mapping: true\n'])('refuses malformed documents without overwriting %s', async (text) => { const file = await fixture(text) await expect(writePluginEnabled(file, 'tool', 'package', true)).rejects.toThrow() diff --git a/packages/boot/plugin-manager/tests/preset-bundles.spec.ts b/packages/boot/plugin-manager/tests/preset-bundles.spec.ts new file mode 100644 index 0000000000..08c063b180 --- /dev/null +++ b/packages/boot/plugin-manager/tests/preset-bundles.spec.ts @@ -0,0 +1,424 @@ +/** Scoped bundles use real preset generations, profile reloads, and package-removal admission. */ +import fs, { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { syncBuiltinESMExports } from 'node:module' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { expect, it, onTestFinished, vi } from 'vitest' +import type { Context } from '@deepseek-ai/cordis' +import Timer from '@deepseek-ai/cordis-plugin-timer' +import { boot, initProfile, PluginPackages, readProfileManifest, readProfilePatches, createRuntimeResolution, loadProfileDirectory, type ProfileContext, type ProfilePatch } from '@deepseek-ai/dsh-app-boot' +import Hmr from '@deepseek-ai/dsh-hmr' +import AgentPreset from '@deepseek-ai/dsh-agent-preset' +import AgentPresets from '@deepseek-ai/dsh-agent-preset-registry' +import { createScope } from '@deepseek-ai/dsh-scope' +import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session' +import SessionProjections from '@deepseek-ai/dsh-session-projection' +import SessionTitle, { SessionTitleProviderId, type SessionTitleProviderRequest, type SessionTitleProviderResult } from '@deepseek-ai/dsh-session-title' +import { createUserMessage } from '@deepseek-ai/dsh-llm' +import PluginManager from '../src/index.ts' +import * as operations from '../src/operations.ts' +import * as appBoot from '@deepseek-ai/dsh-app-boot' + +async function fixture(options: { title?: boolean; hmr?: boolean; presets?: boolean } = {}) { + const home = mkdtempSync(join(tmpdir(), 'preset-bundle-')) + let owner: Context | undefined + onTestFinished(async () => { await owner?.fiber.dispose(); rmSync(home, { recursive: true, force: true }) }) + const dir = join(home, 'profiles', 'test') + const anchor = join(home, 'package.json') + writeFileSync(anchor, '{"name":"test-installation","dependencies":{}}\n') + initProfile(dir, ['core']) + writeFileSync(join(dir, 'cordis.yml'), '[]\n') + const bundle = (name: string, patches: ProfilePatch[]): string => { + const path = join(dir, 'node_modules', name) + mkdirSync(path, { recursive: true }) + writeFileSync(join(path, 'package.json'), JSON.stringify({ name, version: '1.0.0', dsh: { bundle: { patch: './cordis.patch.yml' } } })) + writeFileSync(join(path, 'cordis.patch.yml'), JSON.stringify(patches)) + writeFileSync(join(path, 'plugin.mjs'), 'export function apply(ctx, config) { if (config?.fail) throw new Error("scoped plugin failed"); ctx.get("scopedBundleTrace").push("start"); ctx.effect(() => () => { ctx.get("scopedBundleTrace").push("stop") }) }\n') + return path + } + bundle('core', [{ insert: [ + { id: 'manager', name: 'cordis:manager' }, + { id: 'sessions', name: 'cordis:sessions' }, + { id: 'session-projections', name: 'cordis:session-projections' }, + { id: 'presets', name: 'cordis:presets', config: { default: 'standard' } }, + ...(options.presets === false ? [] : ['standard', 'cordis']).map(id => ({ id: `preset-${id}`, name: 'cordis:preset', config: { id, plugins: [] } })), + ...options.title === true ? [ + { id: 'title', name: 'cordis:title', config: { fallbackMaxWords: 5, fallbackMaxBytes: 100, maxTitleBytes: 100 } }, + { id: 'default-title', name: 'cordis:title-provider', config: { id: 'default' } }, + ] : [], + ] }]) + const addon = bundle('addon', ['standard', 'cordis'].map(preset => ({ preset: `preset-${preset}`, insert: [{ + id: 'shared-id', name: './plugin.mjs', + }] }))) + const manifest = readProfileManifest('test', dir) + manifest.dependencies = { addon: '1.0.0' } + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + const profile: ProfileContext = { + name: 'test', dir, patchPath: join(dir, 'cordis.patch.yml'), installAnchor: anchor, + cwd: home, home, startedBundles: ['core'], overlays: [], telemetryDisabledEnv: undefined, + } + const resolution = await createRuntimeResolution({ installAnchor: anchor, profile: loadProfileDirectory('test', dir, anchor), home }) + const calls: { + id: string + request: SessionTitleProviderRequest + pending: ReturnType> + }[] = [] + const entered: { current: ReturnType> } = { current: Promise.withResolvers() } + const trace: string[] = [] + const ctx = await boot('test', join(dir, 'cordis.yml'), readProfilePatches('test', profile), async (host) => { + owner = host + host.provide('profileContext', profile) + host.provide('scopedBundleTrace', trace) + host.provide('appReady', { onReady(listener) { listener(); return () => {} } }) + Object.assign(host.loader.builtins, { + manager: PluginManager, sessions: SessionStore, 'session-projections': SessionProjections, + presets: AgentPresets, preset: AgentPreset, title: SessionTitle, + 'title-provider': { + inject: ['sessionTitle'], + apply(ctx: Context, config: { id: string }) { + ctx.sessionTitle.register({ + id: SessionTitleProviderId(config.id), automatic: 'all-prompts', + generate(request) { + const pending = Promise.withResolvers() + calls.push({ id: config.id, request, pending }) + entered.current.resolve(undefined) + return pending.promise + }, + }) + }, + }, + }) + await host.plugin(PluginPackages, { resolution }) + }) + await ctx.plugin(Timer) + if (options.hmr !== false) { + await ctx.plugin(Hmr, { root: [], ignored: [], debounce: 0 }) + await ctx.hmr.runExclusive(async () => {}) + } + onTestFinished(() => { + for (const call of calls) call.pending.resolve({ title: 'teardown', messageSeqs: call.request.messages.map(message => message.seq) }) + }) + return { ctx, manager: ctx.pluginManager, profile, dir, addon, bundle, trace, calls, entered } +} + +it('lists scoped declarations while Off and keeps equal inner ids distinct across presets', async () => { + const { manager, addon } = await fixture() + const patchFile = join(addon, 'cordis.patch.yml') + const patches = JSON.parse(readFileSync(patchFile, 'utf8')) as ProfilePatch[] + writeFileSync(patchFile, JSON.stringify([...patches, + { id: 'shared-id', disabled: true }, + { preset: 'preset-standard', id: 'absent', disabled: true }, + ])) + const moduleName = pathToFileURL(join(addon, 'plugin.mjs')).href + expect((await manager.listBundles()).find(row => row.name === 'addon')?.rows).toEqual([ + { rowId: 'shared-id', preset: 'preset-standard', readOnlyReason: 'preset-managed', moduleName }, + { rowId: 'shared-id', preset: 'preset-cordis', readOnlyReason: 'preset-managed', moduleName }, + ]) + expect(await manager.setBundleEnabled('addon', true)).toMatchObject({ application: 'applied', changed: true }) + const enabled = (await manager.listBundles()).find(row => row.name === 'addon')! + expect(enabled.rows).toEqual([ + { rowId: 'shared-id', preset: 'preset-standard', readOnlyReason: 'preset-managed', moduleName, composition: { enabled: true, fiberPhase: 'active' } }, + { rowId: 'shared-id', preset: 'preset-cordis', readOnlyReason: 'preset-managed', moduleName, composition: { enabled: true, fiberPhase: 'active' } }, + ]) + expect(enabled.rows.every(row => row.entryId === undefined)).toBe(true) + expect(enabled.overrides).toEqual(['shared-id', 'preset-standard/absent']) + expect(await manager.setBundleEnabled('addon', false)).toMatchObject({ application: 'applied' }) + expect((await manager.listBundles()).find(row => row.name === 'addon')?.rows.every(row => row.composition === undefined)).toBe(true) +}) + +it('loads a third-party relative child inside a real preset group and retains its package until release', async () => { + const { manager, ctx, dir, addon, bundle, trace } = await fixture() + bundle('addon', [{ preset: 'preset-standard', insert: [{ + id: 'relative-group', name: 'cordis:group', group: true, config: [{ + id: 'relative-child', name: './nested/relative.mjs', config: { value: 'nested-preset' }, + }], + }] }]) + mkdirSync(join(addon, 'nested')) + writeFileSync(join(addon, 'nested/relative.mjs'), [ + 'export function apply(ctx, config) {', + ' ctx.get("scopedBundleTrace").push(`relative:start:${config.value}`)', + ' ctx.effect(() => () => { ctx.get("scopedBundleTrace").push("relative:stop") })', + '}', + '', + ].join('\n')) + const moduleName = pathToFileURL(join(addon, 'nested/relative.mjs')).href + const declared = (await manager.listBundles()).find(row => row.name === 'addon')?.rows + .find(row => row.rowId === 'relative-child') + expect(declared).toEqual({ rowId: 'relative-child', preset: 'preset-standard', readOnlyReason: 'preset-managed', moduleName }) + expect(await manager.setBundleEnabled('addon', true)).toMatchObject({ application: 'applied' }) + expect(trace).toEqual(['relative:start:nested-preset']) + expect((await ctx.agentPresets.compositionInventory()).find(preset => preset.id === 'standard')?.rows) + .toMatchObject([{ entryId: 'relative-child', moduleName, enabled: true }]) + + const owner = createScope(ctx, {}) + onTestFinished(() => owner.dispose()) + await ctx.agentPresets.mount(owner.ctx, 'standard') + expect(await manager.setBundleEnabled('addon', false)).toMatchObject({ application: 'applied' }) + expect(ctx.agentPresets.inspectCompositions(owner.ctx)[0]?.modules).toMatchObject([{ moduleName }]) + expect(trace).toEqual(['relative:start:nested-preset']) + const remove = vi.spyOn(operations, 'runProfilePnpm').mockImplementation(async () => { + const manifest = readProfileManifest('test', dir) + delete manifest.dependencies?.addon + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + return { exitCode: 0, output: '', truncated: false, logPath: join(dir, 'remove.log') } + }) + onTestFinished(() => { remove.mockRestore() }) + expect(await manager.removeBundle('addon')).toMatchObject({ application: 'failed', error: { code: 'bundle-in-use' } }) + expect(remove).not.toHaveBeenCalled() + await owner.dispose() + expect(trace).toEqual(['relative:start:nested-preset', 'relative:stop']) + expect(await manager.removeBundle('addon')).toMatchObject({ application: 'applied', changed: true }) + expect(remove).toHaveBeenCalledOnce() +}) + +it('retains a failed selection, reports affected preset failures again, and keeps unrelated failures as warnings', async () => { + const { manager, dir, bundle, ctx } = await fixture() + bundle('broken', [{ preset: 'preset-standard', insert: [{ id: 'broken', name: './plugin.mjs', config: { fail: true } }] }]) + const result = await manager.setBundleEnabled('broken', true) + expect(result).toMatchObject({ application: 'failed', changed: true, error: { diagnostic: expect.stringContaining('agent preset standard') as string } }) + expect(readProfileManifest('test', dir).dsh?.profile?.bundles).toContain('broken') + expect(await manager.setBundleEnabled('broken', true)).toMatchObject({ application: 'failed', changed: false }) + expect((await ctx.agentPresets.list()).find(row => row.id === 'standard')?.broken).toContain('scoped plugin failed') + bundle('unrelated', [{ insert: [{ id: 'unrelated', name: './plugin.mjs' }] }]) + expect(await manager.setBundleEnabled('unrelated', true)).toMatchObject({ application: 'applied', warnings: [expect.stringContaining('agent preset standard')] }) + expect(await manager.setBundleEnabled('broken', false)).toMatchObject({ application: 'applied' }) + expect((await ctx.agentPresets.list()).every(row => row.broken === undefined)).toBe(true) +}) + +it('refuses a no-op scoped enable when its Host preset row already failed before registering', async () => { + const { manager, ctx, bundle, dir } = await fixture() + bundle('invalid-definition', [{ id: 'preset-standard', config: { id: '', plugins: [] } }]) + expect(await manager.setBundleEnabled('invalid-definition', true)).toMatchObject({ application: 'failed' }) + expect((await ctx.agentPresets.list()).map(row => row.id)).not.toContain('standard') + bundle('scoped-noop', [{ preset: 'preset-standard', insert: [] }]) + expect(await manager.setBundleEnabled('scoped-noop', true)).toMatchObject({ + application: 'failed', changed: true, error: { diagnostic: expect.stringContaining('Preset id must not be empty') as string }, + }) + expect(readProfileManifest('test', dir).dsh?.profile?.bundles).toContain('scoped-noop') +}) + +it.each([{ id: 'standard' }, { id: 'standard', plugins: [] }])( + 'keeps existing preset failures as warnings when an unrelated Host config is %j', async (config) => { + const { manager, bundle, ctx } = await fixture() + expect(await ctx.agentPresets.list()).toMatchObject([{ id: 'cordis', definitionEntryId: 'preset-cordis' }, { id: 'standard', definitionEntryId: 'preset-standard' }]) + expect((await ctx.agentPresets.remoteExportList()).presets.every(row => !Object.hasOwn(row, 'definitionEntryId'))).toBe(true) + bundle('broken', [{ preset: 'preset-standard', insert: [{ id: 'broken', name: './plugin.mjs', config: { fail: true } }] }]) + expect(await manager.setBundleEnabled('broken', true)).toMatchObject({ application: 'failed' }) + bundle('ordinary', [{ insert: [{ id: 'ordinary', name: './plugin.mjs', config }] }]) + expect(await manager.setBundleEnabled('ordinary', true)).toMatchObject({ + application: 'applied', warnings: [expect.stringContaining('agent preset standard')], + }) + }, +) + +it('reports invalid composition immediately without HMR while retaining the saved selection', async () => { + const { manager, dir } = await fixture({ hmr: false, presets: false }) + expect(await manager.setBundleEnabled('addon', true)).toMatchObject({ + application: 'failed', changed: true, error: { diagnostic: expect.stringContaining(join('addon', 'cordis.patch.yml')) as string }, + }) + expect(readProfileManifest('test', dir).dsh?.profile?.bundles).toContain('addon') + expect(await manager.setBundleEnabled('addon', false)).toMatchObject({ application: 'restart-required', changed: true }) + expect(readProfileManifest('test', dir).dsh?.profile?.bundles).not.toContain('addon') +}) + +it('refuses Remove after Off while an old Agent scope retains the bundle, then removes after release', async () => { + const { manager, ctx, dir, trace } = await fixture() + expect(await manager.setBundleEnabled('addon', true)).toMatchObject({ application: 'applied' }) + const agent = createScope(ctx, {}) + onTestFinished(() => agent.dispose()) + await ctx.agentPresets.mount(agent.ctx, 'standard') + expect(await manager.setBundleEnabled('addon', false)).toMatchObject({ application: 'applied' }) + expect(ctx.agentPresets.inspectCompositions(agent.ctx)[0]?.modules).toHaveLength(1) + const remove = vi.spyOn(operations, 'runProfilePnpm').mockImplementation(async () => { + const manifest = readProfileManifest('test', dir) + delete manifest.dependencies?.addon + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + return { exitCode: 0, output: '', truncated: false, logPath: join(dir, 'remove.log') } + }) + onTestFinished(() => { remove.mockRestore() }) + expect(await manager.removeBundle('addon')).toMatchObject({ application: 'failed', error: { code: 'bundle-in-use' }, changed: false }) + expect(remove).not.toHaveBeenCalled() + await agent.dispose() + expect(trace.filter(item => item === 'stop')).toHaveLength(2) + expect(await manager.removeBundle('addon')).toMatchObject({ application: 'applied', changed: true }) + expect(remove).toHaveBeenCalledOnce() +}) + +it.each([false, true].flatMap(anonymous => [false, true].map(archive => ({ anonymous, archive })))) +('removes a released contribution while base presets keep using the same shared module (anonymous=$anonymous, archive=$archive)', async ({ anonymous, archive }) => { + // Resolver identities must be created under the same carrier that later refreshes them. + if (archive) { + const previousPkg = Object.getOwnPropertyDescriptor(process, 'pkg') + onTestFinished(() => { + if (previousPkg === undefined) Reflect.deleteProperty(process, 'pkg') + else Object.defineProperty(process, 'pkg', previousPkg) + }) + Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) + } + const { manager, ctx, dir, addon } = await fixture() + const shared = join(dir, 'node_modules', 'shared-fixture') + mkdirSync(shared) + writeFileSync(join(shared, 'package.json'), JSON.stringify({ name: 'shared-fixture', version: '1.0.0', type: 'module', exports: './index.mjs' })) + writeFileSync(join(shared, 'index.mjs'), 'export function apply() {}\n') + const core = join(dir, 'node_modules', 'core') + const corePatches = JSON.parse(readFileSync(join(core, 'cordis.patch.yml'), 'utf8')) as ProfilePatch[] + for (const row of corePatches[0]!.insert!) { + if (row.id === 'preset-standard') row.config = { id: 'standard', plugins: [{ id: 'base-shared', name: 'shared-fixture' }] } + if (row.id === 'preset-cordis') row.config = { id: 'cordis', plugins: [{ id: 'addon-shared', name: 'shared-fixture' }] } + } + writeFileSync(join(core, 'cordis.patch.yml'), JSON.stringify(corePatches)) + writeFileSync(join(addon, 'cordis.patch.yml'), JSON.stringify([{ + preset: 'preset-standard', insert: [{ ...anonymous ? {} : { id: 'addon-shared' }, name: 'shared-fixture' }], + }])) + for (const packageDir of [core, addon]) { + const manifest = readProfileManifest('test', packageDir) + manifest.dependencies = { 'shared-fixture': '1.0.0' } + writeFileSync(join(packageDir, 'package.json'), JSON.stringify(manifest)) + } + expect(await manager.setBundleEnabled('addon', true)).toMatchObject({ application: 'applied' }) + const owner = createScope(ctx, {}) + onTestFinished(() => owner.dispose()) + await ctx.agentPresets.mount(owner.ctx, 'standard') + expect(await manager.setBundleEnabled('addon', false)).toMatchObject({ application: 'applied' }) + expect(ctx.agentPresets.inspectCompositions(owner.ctx)).toMatchObject([{ + id: 'standard', definitionEntryId: 'preset-standard', modules: [ + { entryId: 'base-shared', moduleName: 'shared-fixture' }, + { entryId: anonymous ? expect.any(String) as string : 'addon-shared', moduleName: 'shared-fixture' }, + ], + }]) + if (archive) { + const original = fs.realpathSync + onTestFinished(() => { fs.realpathSync = original; syncBuiltinESMExports() }) + fs.realpathSync = new Proxy(original, { + apply(target, receiver: unknown, args: unknown[]): unknown { + if (['package.json', 'index.mjs'].some(file => String(args[0]).endsWith(join('shared-fixture', file)))) { + throw Object.assign(new Error('archive-backed files have no realpath entry'), { code: 'ENOENT' }) + } + return Reflect.apply(target, receiver, args) + }, + }) + syncBuiltinESMExports() + expect(() => fs.realpathSync(join(shared, 'package.json'))).toThrow('archive-backed') + expect(readFileSync(join(shared, 'index.mjs'), 'utf8')).toContain('export function apply') + } + const remove = vi.spyOn(operations, 'runProfilePnpm').mockImplementation(async () => { + const manifest = readProfileManifest('test', dir) + delete manifest.dependencies?.addon + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + return { exitCode: 0, output: '', truncated: false, logPath: join(dir, 'remove.log') } + }) + onTestFinished(() => { remove.mockRestore() }) + expect(await manager.removeBundle('addon')).toMatchObject({ application: 'failed', error: { code: 'bundle-in-use' } }) + expect(remove).not.toHaveBeenCalled() + await owner.dispose() + expect(ctx.agentPresets.inspectCompositions().flatMap(preset => preset.modules).map(module => module.moduleName)) + .toEqual(['shared-fixture', 'shared-fixture']) + const removed = await manager.removeBundle('addon') + expect(removed, JSON.stringify(removed.error)).toMatchObject({ application: 'applied', changed: true }) + expect(remove).toHaveBeenCalledOnce() +}) + +it('removes an unrelated bundle when another retained row with the same id has lost its module file', async () => { + const { manager, ctx, dir, addon, bundle } = await fixture() + expect(await manager.setBundleEnabled('addon', true)).toMatchObject({ application: 'applied' }) + const owner = createScope(ctx, {}) + onTestFinished(() => owner.dispose()) + await ctx.agentPresets.mount(owner.ctx, 'standard') + expect(await manager.setBundleEnabled('addon', false)).toMatchObject({ application: 'applied' }) + bundle('unrelated', [{ preset: 'preset-standard', insert: [{ id: 'shared-id', name: './plugin.mjs' }] }]) + const manifest = readProfileManifest('test', dir) + manifest.dependencies = { ...manifest.dependencies, unrelated: '1.0.0' } + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + rmSync(join(addon, 'plugin.mjs')) + const remove = vi.spyOn(operations, 'runProfilePnpm').mockResolvedValue({ + exitCode: 0, output: '', truncated: false, logPath: join(dir, 'remove.log'), + }) + onTestFinished(() => { remove.mockRestore() }) + expect(await manager.removeBundle('addon')).toMatchObject({ application: 'failed', error: { code: 'bundle-in-use' } }) + expect(remove).not.toHaveBeenCalled() + const resolution = vi.spyOn(appBoot, 'resolvePluginResource').mockImplementationOnce(() => { throw new Error('resolver unavailable') }) + onTestFinished(() => { resolution.mockRestore() }) + expect(await manager.removeBundle('unrelated')).toMatchObject({ application: 'failed', error: { diagnostic: 'resolver unavailable' } }) + expect(remove).not.toHaveBeenCalled() + resolution.mockRestore() + expect(await manager.removeBundle('unrelated')).toMatchObject({ application: 'applied' }) + expect(remove).toHaveBeenCalledOnce() + expect(ctx.agentPresets.inspectCompositions(owner.ctx)[0]?.modules).toHaveLength(1) +}) + +it('retains contributed builtin rows even when no package owns their module names', async () => { + const { manager, ctx, bundle } = await fixture() + ctx.loader.builtins['retained-fixture'] = { apply() {} } + bundle('addon', [{ preset: 'preset-standard', insert: [{ id: 'builtin', name: 'cordis:retained-fixture' }] }]) + expect(await manager.setBundleEnabled('addon', true)).toMatchObject({ application: 'applied' }) + const agent = createScope(ctx, {}) + onTestFinished(() => agent.dispose()) + await ctx.agentPresets.mount(agent.ctx, 'standard') + expect(await manager.setBundleEnabled('addon', false)).toMatchObject({ application: 'applied' }) + const inspect = ctx.agentPresets.inspectCompositions.bind(ctx.agentPresets) + const projection = vi.spyOn(ctx.agentPresets, 'inspectCompositions').mockImplementation(() => inspect().map(composition => ({ + ...composition, modules: composition.modules.map(({ baseUrl: _baseUrl, ...module }) => module), + }))) + onTestFinished(() => { projection.mockRestore() }) + expect(await manager.removeBundle('addon')).toMatchObject({ application: 'failed', error: { code: 'bundle-in-use' } }) +}) + +it('refuses removal when a different module from the same package remains in a preset', async () => { + const { manager, ctx, dir, addon } = await fixture() + const source = readFileSync(join(addon, 'plugin.mjs'), 'utf8') + writeFileSync(join(addon, 'other.mjs'), source) + expect(await manager.setBundleEnabled('addon', true)).toMatchObject({ application: 'applied' }) + writeFileSync(join(dir, 'cordis.patch.yml'), JSON.stringify([{ + preset: 'preset-standard', insert: [{ id: 'independent', name: pathToFileURL(join(addon, 'other.mjs')).href }], + }])) + expect(await manager.setBundleEnabled('addon', false)).toMatchObject({ application: 'applied' }) + expect(ctx.agentPresets.inspectCompositions().some(row => row.modules.some(module => module.moduleName.endsWith('/other.mjs')))).toBe(true) + const remove = vi.spyOn(operations, 'runProfilePnpm') + onTestFinished(() => { remove.mockRestore() }) + expect(await manager.removeBundle('addon')).toMatchObject({ application: 'failed', error: { code: 'bundle-in-use' } }) + expect(remove).not.toHaveBeenCalled() +}) + +async function requestTitle(session: Session): Promise { + session.append('turn/start', { turn: 1 }) + session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Title for provider switch' }], source: { kind: 'user' } }), { surfaceOp: 'append' }) + await new Promise((resolve) => { queueMicrotask(resolve) }) + session.append('request/header', { header: { config: { provider: 'test', model: 'test' } }, reason: 'initial' }) +} + +it('drains and rejects stale title completions before activating each replacement in both directions', async () => { + const { manager, ctx, bundle, calls, entered } = await fixture({ title: true }) + bundle('titles', [ + { id: 'default-title', disabled: true }, + { insert: [{ id: 'optional-title', name: 'cordis:title-provider', config: { id: 'optional' } }] }, + ]) + for (const [enabled, expected] of [[true, 'default'], [false, 'optional']] as const) { + entered.current = Promise.withResolvers() + const session = ctx.sessions.create(SessionId(`title-switch-${String(enabled)}`)) + await requestTitle(session) + await entered.current.promise + const call = calls.at(-1)! + expect(call.id).toBe(expected) + const aborted = Promise.withResolvers() + call.request.signal.addEventListener('abort', () => { aborted.resolve(undefined) }, { once: true }) + let complete = false + const switching = manager.setBundleEnabled('titles', enabled).then((result) => { complete = true; return result }) + onTestFinished(async () => { + call.pending.resolve({ title: 'teardown', messageSeqs: call.request.messages.map(message => message.seq) }) + await switching + }) + await aborted.promise + expect(complete).toBe(false) + expect(calls).toHaveLength(enabled ? 1 : 2) + call.pending.resolve({ title: 'stale result', messageSeqs: call.request.messages.map(message => message.seq) }) + expect(await switching).toMatchObject({ application: 'applied' }) + expect(ctx.sessionTitle.get(session)?.source.kind).toBe('fallback') + } + entered.current = Promise.withResolvers() + const final = ctx.sessions.create(SessionId('title-switch-final')) + await requestTitle(final) + await entered.current.promise + expect(calls.at(-1)?.id).toBe('default') + calls.at(-1)?.pending.resolve({ title: 'final title', messageSeqs: calls.at(-1)!.request.messages.map(message => message.seq) }) +}) diff --git a/packages/boot/plugin-manager/tests/tools.spec.ts b/packages/boot/plugin-manager/tests/tools.spec.ts index e18b5acce8..bc7da82dfe 100644 --- a/packages/boot/plugin-manager/tests/tools.spec.ts +++ b/packages/boot/plugin-manager/tests/tools.spec.ts @@ -10,6 +10,7 @@ import SessionProjections from '@deepseek-ai/dsh-session-projection' import ApprovalService, { type ApprovalOutcome } from '@deepseek-ai/dsh-user-approval' import { expect, it, onTestFinished, vi } from 'vitest' import type PluginManager from '../src/index.ts' +import type { BundleInfo } from '../src/types.ts' import * as tool from '../src/tools.ts' function resultText(result: Awaited>): string { @@ -80,6 +81,40 @@ it('preserves selected bundle load errors in the agent list result', async () => `) }) +it('exposes an absent Official bundle installation target without display metadata', async () => { + const { manager, call } = await fixture() + const offered: BundleInfo = { + name: '@deepseek-ai/dsh-subagent-codex', official: true, availability: 'missing', + installed: false, enabled: false, optional: false, removable: false, rows: [], overrides: [], + installTarget: { spec: '@deepseek-ai/dsh-subagent-codex@1.2.3', version: '1.2.3' }, + meta: { title: 'Codex subagent' }, + } + manager.listBundles.mockResolvedValue([offered]) + expect(JSON.parse(resultText(await call({ action: 'list_bundles' })))).toMatchInlineSnapshot(` + { + "entries": [ + { + "availability": "missing", + "enabled": false, + "installTarget": { + "spec": "@deepseek-ai/dsh-subagent-codex@1.2.3", + "version": "1.2.3", + }, + "installed": false, + "name": "@deepseek-ai/dsh-subagent-codex", + "official": true, + "optional": false, + "overrides": [], + "removable": false, + "rows": [], + }, + ], + "nextOffset": null, + "total": 1, + } + `) +}) + it('checks the calling session on each execution, including after permission is revoked', async () => { const { call, manager } = await fixture() const id = SessionId('manager-permissions') diff --git a/packages/boot/plugin-manager/tsconfig.json b/packages/boot/plugin-manager/tsconfig.json index ba181795a1..643f870407 100644 --- a/packages/boot/plugin-manager/tsconfig.json +++ b/packages/boot/plugin-manager/tsconfig.json @@ -64,6 +64,9 @@ }, { "path": "../../interaction/user-approval" + }, + { + "path": "../../preset/agent-preset-registry" } ] } diff --git a/packages/browser-use/browser-use/package.json b/packages/browser-use/browser-use/package.json index d5f5e322ec..8178f5d01d 100644 --- a/packages/browser-use/browser-use/package.json +++ b/packages/browser-use/browser-use/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-browser-use", "description": "Exclusive named browser-use provider registration", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/acp-app/README.i18n.yaml b/packages/bundle/acp-app/README.i18n.yaml index d2e5b8d55b..fe732545af 100644 --- a/packages/bundle/acp-app/README.i18n.yaml +++ b/packages/bundle/acp-app/README.i18n.yaml @@ -27,8 +27,8 @@ en: a7451ee84e5e156a zh: a7451ee84e5e156a /deepseek-ai-dsh-acp-app/model-experience/acp-coding-agent-persona/what-the-model-sees: - en: bc0db4c628a1e547 - zh: 740d07803bca3ce4 + en: eab16c37bdefccf5 + zh: 93b318b3959e2c5f /deepseek-ai-dsh-acp-app/model-experience/acp-coding-agent-persona/token-effect: en: 371d28831239e0a8 zh: 0d2678654607b657 diff --git a/packages/bundle/acp-app/README.md b/packages/bundle/acp-app/README.md index 240c2d0ce7..d67f9421c1 100644 --- a/packages/bundle/acp-app/README.md +++ b/packages/bundle/acp-app/README.md @@ -44,7 +44,7 @@ The complete supported method matrix, MCP trust model, update mapping, and stop #### What the model sees -The profile supplies `You are a coding agent powered by the {{model}} model.` before first-party guidance and `Your working directory is {{cwd}}.` in a separate persona suffix. The ACP row's route and each `session/new` cwd resolve the placeholders. +The profile supplies `You are a coding agent powered by the {{model}} model.` before first-party guidance; the ACP route resolves `{{model}}`. Each Session receives its current directory in required user-role context from [`dsh-working-directory`](../../session/working-directory/README.md). #### Token effect diff --git a/packages/bundle/acp-app/README.zh.md b/packages/bundle/acp-app/README.zh.md index bd9e90d5ae..53ebac8a3a 100644 --- a/packages/bundle/acp-app/README.zh.md +++ b/packages/bundle/acp-app/README.zh.md @@ -44,7 +44,7 @@ ACP v1 SDK 客户端先初始化 `dsh --profile acp`,再用绝对 `cwd` 与可 #### 模型看到什么 -profile 在第一方指导之前提供 `You are a coding agent powered by the {{model}} model.`,并在独立的 persona 后缀中提供 `Your working directory is {{cwd}}.`。ACP 配置项的路由与每个 `session/new` 的 cwd 会解析其中的占位符。 +profile 在第一方指导之前提供 `You are a coding agent powered by the {{model}} model.`;ACP 路由解析 `{{model}}`。每个 Session 通过 [`dsh-working-directory`](../../session/working-directory/README.zh.md) 提供的必需 user-role 上下文接收当前目录。 #### Token 影响 diff --git a/packages/bundle/acp-app/cordis.patch.yml b/packages/bundle/acp-app/cordis.patch.yml index 313dab80ae..ca261faae8 100644 --- a/packages/bundle/acp-app/cordis.patch.yml +++ b/packages/bundle/acp-app/cordis.patch.yml @@ -2,7 +2,6 @@ - id: system-prompt config: - personaSuffix: Your working directory is {{cwd}}. personaPrefix: >- You are a coding agent powered by the {{model}} model. diff --git a/packages/bundle/acp-app/package.json b/packages/bundle/acp-app/package.json index fd1590a36c..d1d12860c2 100644 --- a/packages/bundle/acp-app/package.json +++ b/packages/bundle/acp-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-acp-app", "description": "The dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-base", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/base/README.i18n.yaml b/packages/bundle/base/README.i18n.yaml index 397428513a..5b191312a7 100644 --- a/packages/bundle/base/README.i18n.yaml +++ b/packages/bundle/base/README.i18n.yaml @@ -21,8 +21,8 @@ en: 0f971f7ac7c09139 zh: ceb866da681c709c /deepseek-ai-dsh-base/use-this-package/what-you-get: - en: 7ee3a3def76e0ce6 - zh: b53b1fdc78cf0339 + en: 5bfb7755e987862c + zh: 1f0e9ea7a826a509 /deepseek-ai-dsh-base/use-this-package/shell-tools-per-platform: en: 2dce12f82d6ee955 zh: d4599eb899a3cd88 diff --git a/packages/bundle/base/README.md b/packages/bundle/base/README.md index c3d0e6fd39..9474a8ce63 100644 --- a/packages/bundle/base/README.md +++ b/packages/bundle/base/README.md @@ -49,7 +49,9 @@ Run `dsh --profile my-profile "your task"` and you get a working agent with mode Out of the box, every profile built on this core provides: a DeepSeek model connection (the provider and model are configurable, and you can enable extra providers from your settings), the full tool set — file editing, shell commands, web search, public HTTP(S) fetch, subagents, task and goal tracking — durable sessions that survive restarts, and the default permission policy that confines file writes to your workspace and asks before risky actions. Web fetch runs without per-call approval; its provider rejects non-public destinations. Feedback stays in the Session log. [OTel session upload](../../session/session-telemetry-otel/README.md) defaults to `FEEDBACK_ONLY` for all users, including `deepseek-official`: new text feedback, message ratings, edits, and withdrawals release the complete canonical prefix through that event, including context. Later records wait for the next explicit feedback; sending an authorized batch needs no further interaction or model call. `DISABLED` prevents OTel capture. The default-on [DeepSeek session-log contributor](../../session/session-log-deepseek/README.md) remains a separate request path. -Default file editing uses `read`, `write`, and `edit`. The `str_replace_editor` tool remains available as an explicit opt-in. To add it to a base-backed profile, put this entry in the profile, home, or invocation patch: +Both `subagent` and `subagent_fork` create managed children and return a child id immediately. Use `send_message` to steer active children or continue their conversations after completion. [Delegation-tool behavior](../../subagent/tool-subagent/README.md) describes result delivery and backend capabilities. + +The built-in `working_directory` tool changes the directory for subsequent operations while existing processes and write permissions remain independent. Current directory information appears in user context. Default file editing uses `read`, `write`, and `edit`. The `str_replace_editor` tool remains available as an explicit opt-in. To add it to a base-backed profile, put this entry in the profile, home, or invocation patch: ```yaml - insert: diff --git a/packages/bundle/base/README.zh.md b/packages/bundle/base/README.zh.md index cf2bf9bd34..b4e45bcaf6 100644 --- a/packages/bundle/base/README.zh.md +++ b/packages/bundle/base/README.zh.md @@ -49,7 +49,9 @@ kind: "package-bundle" 开箱即用,基于本核心构建的每个 profile 都提供:DeepSeek 模型连接(提供方与模型可配置,你还可以在设置中启用额外提供方)、完整工具集——文件编辑、shell 命令、web 搜索、公开 HTTP(S) 抓取、subagent、任务与目标跟踪——可跨重启存活的持久会话,以及默认权限策略:把文件写入限制在工作区内,危险操作前征询许可。Web 抓取无需逐次审批,其提供方会拒绝非公开目的地址。反馈保存在会话日志中。[OTel 会话上传](../../session/session-telemetry-otel/README.zh.md)对所有用户默认使用 `FEEDBACK_ONLY`,包括 `deepseek-official`:新的文本反馈、消息评分、编辑与撤回会释放截至该事件的完整规范会话日志前缀,包含上下文。后续记录等待下一次显式反馈;发送已授权批次无需进一步交互或模型调用。`DISABLED` 阻止 OTel 捕获。默认开启的 [DeepSeek 会话日志贡献器](../../session/session-log-deepseek/README.zh.md)仍是独立的请求路径。 -默认文件编辑使用 `read`、`write` 和 `edit`。`str_replace_editor` 工具仍可显式启用。要将它加入基于 base 的 profile,请在 profile、home 或逐次调用 patch 中添加以下条目: +`subagent` 与 `subagent_fork` 都创建受管理的子任务,并立即返回 child id。使用 `send_message` 可引导活跃子任务,或在完成后继续其对话。[委派工具行为](../../subagent/tool-subagent/README.zh.md)说明了结果交付和后端能力。 + +内置 `working_directory` 工具改变后续操作使用的目录,已有进程与写权限保持独立。当前目录信息出现在用户上下文中。默认文件编辑使用 `read`、`write` 和 `edit`。`str_replace_editor` 工具仍可显式启用。要将它加入基于 base 的 profile,请在 profile、home 或逐次调用 patch 中添加以下条目: ```yaml - insert: diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index 9c35ebf5c2..5219c2444f 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -65,7 +65,10 @@ targetWords: 5 targetCjkCharacters: 10 maxInputBytes: 4096 - maxOutputTokens: 64 + # A ceiling against runaway output, not a length target: the prompt + # keeps titles short, while reasoning a model cannot switch off also + # counts as output. Stays within common model output limits. + maxOutputTokens: 4096 timeoutMs: 60000 - id: user-questions @@ -155,6 +158,12 @@ # Shared projection registry: subagent catalog identity (mode/label) folds # through its registered units, so the `list_agents` surface below fails # loud without it; web layers reuse this same mount for list rows. + - id: working-directory + name: '@deepseek-ai/dsh-working-directory' + + - id: tool-working-directory + name: '@deepseek-ai/dsh-tool-working-directory' + - id: session-projection name: '@deepseek-ai/dsh-session-projection' @@ -297,10 +306,6 @@ - id: skill-filesystem name: '@deepseek-ai/dsh-skill-filesystem' - - id: skill-badge - name: '@deepseek-ai/dsh-skill-badge' - disabled: true - - id: tool-skill name: '@deepseek-ai/dsh-tool-skill' @@ -372,12 +377,10 @@ config: provider: spawn toolName: subagent - backgroundMode: continuable # Fork omits model selection so provider/model stay equal to the parent and - # the inherited history remains eligible for KV Cache reuse. This base row - # stays one-shot; preset layers may select continuable mode without adding a - # child-only system-prompt section or tool schema ahead of that history. + # the inherited history remains eligible for KV Cache reuse. Parent and child + # share the messaging tools; return guidance follows the inherited history. # See .agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md # and .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. - id: tool-subagent-fork @@ -385,7 +388,6 @@ config: provider: fork toolName: subagent_fork - backgroundMode: one-shot - id: ptc-runtime name: '@deepseek-ai/dsh-ptc-runtime-node' @@ -437,20 +439,6 @@ - id: tool-goal name: '@deepseek-ai/dsh-tool-goal' - # Fresh-agent Ralph iteration over a build-time-fixed script. Off by - # default: the tool description restricts `ralph` to runs the human - # explicitly asked for, and completion is a worker self-report, not an - # independent evaluation. An overlay row restores it for a base-backed - # profile (`$DSH_HOME/cordis.patch.yml`, or a `--patch `): - # - id: tool-ralph - # disabled: false - - id: tool-ralph - name: '@deepseek-ai/dsh-tool-ralph' - disabled: true - config: - subagentProvider: spawn - maxRounds: 64 - # Consecutive-repeat reminders on the tool chain. - id: repeat-tool-reminder name: '@deepseek-ai/dsh-repeat-tool-reminder' diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index be14c75ad5..be152900d3 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-base", "description": "The shared dsh core as a profile bundle: the first patch layer of base-backed profiles, inserting core rows over the empty profile root", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -81,7 +81,6 @@ "@deepseek-ai/dsh-session-title": "workspace:*", "@deepseek-ai/dsh-session-title-first-prompt-llm": "workspace:*", "@deepseek-ai/dsh-skill": "workspace:*", - "@deepseek-ai/dsh-skill-badge": "workspace:*", "@deepseek-ai/dsh-skill-filesystem": "workspace:*", "@deepseek-ai/dsh-spill-local": "workspace:*", "@deepseek-ai/dsh-spill-policy": "workspace:*", @@ -102,7 +101,6 @@ "@deepseek-ai/dsh-tool-fs-search": "workspace:*", "@deepseek-ai/dsh-tool-goal": "workspace:*", "@deepseek-ai/dsh-tool-pwsh": "workspace:*", - "@deepseek-ai/dsh-tool-ralph": "workspace:*", "@deepseek-ai/dsh-tool-skill": "workspace:*", "@deepseek-ai/dsh-tool-subagent": "workspace:*", "@deepseek-ai/dsh-tool-subagent-control": "workspace:*", @@ -126,7 +124,9 @@ "@deepseek-ai/dsh-deepseek-account-platform": "workspace:*", "@deepseek-ai/dsh-config-editor": "workspace:*", "@deepseek-ai/dsh-settings": "workspace:*", - "@deepseek-ai/dsh-otel": "workspace:*" + "@deepseek-ai/dsh-otel": "workspace:*", + "@deepseek-ai/dsh-working-directory": "workspace:*", + "@deepseek-ai/dsh-tool-working-directory": "workspace:*" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" diff --git a/packages/bundle/headless/README.i18n.yaml b/packages/bundle/headless/README.i18n.yaml index c697c53bd5..c3715b490f 100644 --- a/packages/bundle/headless/README.i18n.yaml +++ b/packages/bundle/headless/README.i18n.yaml @@ -36,14 +36,14 @@ en: be1b73a6d0ee3902 zh: bdba099685d68b27 /deepseek-ai-dsh-headless/understand-the-implementation/run-flow: - en: 2f2adf0c346c6c1b - zh: f1e988c7e597a852 + en: 3729fbdc598637da + zh: 254c6961b65b9e2b /deepseek-ai-dsh-headless/understand-the-implementation/patch-surface-over-base: en: a22752c052878579 zh: 1e42c946d8673eeb /deepseek-ai-dsh-headless/understand-the-implementation/exit-mapping: - en: cc6c485a42d40d28 - zh: c57775d9319d8dfd + en: c2466d3d8a26177a + zh: b75743bfa2e7723a /deepseek-ai-dsh-headless/understand-the-implementation/source-map: en: d82278600e0efdbd zh: 0f546bdba164ef25 diff --git a/packages/bundle/headless/README.md b/packages/bundle/headless/README.md index 98ecb77f5d..e7776f0591 100644 --- a/packages/bundle/headless/README.md +++ b/packages/bundle/headless/README.md @@ -77,7 +77,7 @@ The runner is a direct driver over the core API carrier: it resolves the Agent i ### Run flow -The runner awaits the complete application (`ctx.get('loader')?.await()`) so the composed tools and adapters are not half-mounted, reads the shared [`agentDefaultModel`](../../core/agent-default-model/README.md) selection, resolves the task from config or stdin, then resolves the Agent identity: a fresh `session-` by default, or the persisted Session `--session-id` names, which it adopts through [`sessionQuery`](../../session-query/session-query/README.md) and refuses when no log exists. It submits the task as an ordinary user message. Without `--json` it streams that Agent's non-empty reasoning deltas to stderr; with `--json` it projects the run instead. It waits for quiescence, then flushes the Session and folds the owned interval (`firstSeq` onward) into the last non-empty `assistant/message` text and final `turn/end` reason. It writes the final text to stdout (or the `final` event) and requests exit. +The runner awaits the complete application (`ctx.get('loader')?.await()`) so the composed tools and adapters are not half-mounted, reads the shared [`agentDefaultModel`](../../core/agent-default-model/README.md) selection, resolves the task from config or stdin, then resolves the Agent identity: a fresh `session-` by default, or the persisted Session `--session-id` names, which it adopts through [`sessionQuery`](../../session-query/session-query/README.md) and refuses when no log exists. It submits the task as an ordinary user message. Without `--json` it streams that Agent's non-empty reasoning deltas to stderr; with `--json` it projects the run instead. It waits for child work and rechecks that the parent remains idle with an unchanged Session sequence before finishing, so settlement notices can drive a final parent answer. Idle children with parked inbox messages do not delay completion; process shutdown discards their unclaimed messages. It then flushes the Session and folds the owned interval (`firstSeq` onward) into the last non-empty `assistant/message` text and final `turn/end` reason. It writes the final text to stdout (or the `final` event) and requests exit. ### Patch surface over base @@ -85,7 +85,7 @@ The patch rides over `dsh-base`: it inherits the projection cache and shared PTC ### Exit mapping -A completed final `turn/end` exits 0; any other outcome — aborted, error, or no turn in the owned interval — exits 1. An `error` reason also writes `dsh: : ` to stderr. A direct driver failure (for example, Agent creation or an unusable `--session-id`) writes `dsh: ` to stderr and exits 1, and in `--json` mode also emits an `error` event. +A completed final `turn/end` exits 0; any other outcome — aborted, error, or no turn in the owned interval — exits 1. An `error` reason also writes `dsh: : ` to stderr. A live Agent failure without a later committed `turn/end` is a direct driver failure, including a summary whose start or end cannot be persisted; an earlier completed turn cannot make that run succeed. A direct driver failure (for example, Agent creation or an unusable `--session-id`) writes `dsh: ` to stderr and exits 1, and in `--json` mode also emits an `error` event without a `final` event. ### Source map diff --git a/packages/bundle/headless/README.zh.md b/packages/bundle/headless/README.zh.md index e039880df7..a7d35fdbe9 100644 --- a/packages/bundle/headless/README.zh.md +++ b/packages/bundle/headless/README.zh.md @@ -77,7 +77,7 @@ runner 是核心 API 载体之上的直接驱动器:它确定 Agent 标识— ### 运行流程 -runner 等待整个应用结算(`ctx.get('loader')?.await()`),确保已组合的工具与适配器不会半挂载,读取共享的 [`agentDefaultModel`](../../core/agent-default-model/README.zh.md) 选择,从配置或 stdin 解析任务,然后确定 Agent 标识:默认是全新的 `session-`,或是 `--session-id` 指名的持久化 Session——通过 [`sessionQuery`](../../session-query/session-query/README.zh.md) 沿用,日志不存在时拒绝。它把任务作为普通用户消息提交。不带 `--json` 时,它把该 Agent 的非空推理增量流式写入 stderr;带 `--json` 时改为投影本次运行。它等待完全停稳,然后对会话执行 flush,并把所属区间(从 `firstSeq` 起)折叠为最后一条非空 `assistant/message` 文本与最终 `turn/end` 原因。最后,它把最终文本写入 stdout(或 `final` 事件)并请求退出。 +runner 等待整个应用结算(`ctx.get('loader')?.await()`),确保已组合的工具与适配器不会半挂载,读取共享的 [`agentDefaultModel`](../../core/agent-default-model/README.zh.md) 选择,从配置或 stdin 解析任务,然后确定 Agent 标识:默认是全新的 `session-`,或是 `--session-id` 指名的持久化 Session——通过 [`sessionQuery`](../../session-query/session-query/README.zh.md) 沿用,日志不存在时拒绝。它把任务作为普通用户消息提交。不带 `--json` 时,它把该 Agent 的非空推理增量流式写入 stderr;带 `--json` 时改为投影本次运行。它等待子级工作,并在结束前复核父级仍空闲且 Session 序号未变,让结算通知可以驱动父级生成最终答案。收件箱消息处于停放状态的空闲子级不会延迟完成;进程关闭时丢弃这些未领取消息。随后它对会话执行 flush,并把所属区间(从 `firstSeq` 起)折叠为最后一条非空 `assistant/message` 文本与最终 `turn/end` 原因。最后,它把最终文本写入 stdout(或 `final` 事件)并请求退出。 ### 基于 base 的 patch 内容 @@ -85,7 +85,7 @@ patch 叠加在 `dsh-base` 之上:继承投影缓存与共享 PTC 运行时, ### 退出映射 -最终 `turn/end` 完成时退出码为 0;任何其他结果——aborted、error,或所属区间内没有轮次——退出码为 1。结束原因为 `error` 时还会向 stderr 写入 `dsh: : `。直接驱动器失败(例如 Agent 创建失败或不可用的 `--session-id`)向 stderr 写入 `dsh: ` 并退出 1,且在 `--json` 模式下额外发出一个 `error` 事件。 +最终 `turn/end` 完成时退出码为 0;任何其他结果——aborted、error,或所属区间内没有轮次——退出码为 1。结束原因为 `error` 时还会向 stderr 写入 `dsh: : `。若实时 Agent 失败后没有提交更晚的 `turn/end`,则视为直接驱动器失败,包括汇总轮次的开始或结束无法持久化;更早的已完成轮次不能使该次运行成功。直接驱动器失败(例如 Agent 创建失败或不可用的 `--session-id`)向 stderr 写入 `dsh: ` 并退出 1,且在 `--json` 模式下额外发出一个 `error` 事件,不发出 `final` 事件。 ### 源码地图 diff --git a/packages/bundle/headless/cordis.patch.yml b/packages/bundle/headless/cordis.patch.yml index bb5eb2aa09..20f13b54e6 100644 --- a/packages/bundle/headless/cordis.patch.yml +++ b/packages/bundle/headless/cordis.patch.yml @@ -8,7 +8,6 @@ - id: system-prompt config: - personaSuffix: Your working directory is {{cwd}}. personaPrefix: >- You are a coding agent powered by the {{model}} model. diff --git a/packages/bundle/headless/package.json b/packages/bundle/headless/package.json index e5261aa45f..cf2dc9f38f 100644 --- a/packages/bundle/headless/package.json +++ b/packages/bundle/headless/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-headless", "description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -54,7 +54,8 @@ "@deepseek-ai/dsh-llm": "workspace:*", "@deepseek-ai/dsh-session": "workspace:*", "@deepseek-ai/dsh-session-query": "workspace:*", - "@deepseek-ai/dsh-fs": "workspace:*" + "@deepseek-ai/dsh-fs": "workspace:*", + "@deepseek-ai/dsh-subagent": "workspace:*" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", @@ -66,6 +67,12 @@ "@deepseek-ai/dsh-llm": "workspace:*", "@deepseek-ai/dsh-session": "workspace:*", "@deepseek-ai/dsh-session-query": "workspace:*", - "@deepseek-ai/dsh-fs": "workspace:*" + "@deepseek-ai/dsh-fs": "workspace:*", + "@deepseek-ai/dsh-subagent": "workspace:*" + }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-subagent": { + "optional": true + } } } diff --git a/packages/bundle/headless/src/index.ts b/packages/bundle/headless/src/index.ts index 5e62b222eb..93006eb810 100644 --- a/packages/bundle/headless/src/index.ts +++ b/packages/bundle/headless/src/index.ts @@ -29,6 +29,7 @@ import { SessionQueryError } from '@deepseek-ai/dsh-session-query' import type {} from '@deepseek-ai/cordis-plugin-loader' import type {} from '@deepseek-ai/dsh-cmdline' import type {} from '@deepseek-ai/dsh-session-query' +import type {} from '@deepseek-ai/dsh-subagent' import { internals } from './runner-internals.ts' import { projectJsonRun, boundJsonLine } from './json-stream.ts' @@ -302,6 +303,7 @@ function fail(io: HeadlessIo, error: unknown, json: boolean): void { /** * Run one task through one Agent and request process exit. + * Live Agent failures require a later durable terminal before a recovered run can succeed. * @param ctx - plugin context carrying the Agent, default model, Session, and launcher IO services. * @param config - task, optional exact Session identity, and output mode. * @param io - process-facing effects. @@ -360,17 +362,33 @@ async function run(ctx: Context, config: Config, io: HeadlessIo): Promise const firstSeq = agent.session.seq const projection = config.json === true ? projectJsonRun(ctx, agent, io.stdout, { cwd }) : undefined const stopReasoning = projection === undefined ? streamReasoning(ctx, agent, io.stderr) : undefined + let failure: { error: unknown; atOffset: SessionLogOffset } | undefined + const stopErrors = ctx.on('agent/error', ({ agent: subject, error }) => { + if (subject === agent) failure = { error, atOffset: agent.session.seq } + }) + const stopTerminals = ctx.on('session/event', (session, event) => { + if (session === agent.session && event.type === 'turn/end' + && failure !== undefined && event.seq >= failure.atOffset) failure = undefined + }) try { try { agent.followup(createUserMessage({ content: [{ type: 'text', text: task }], source: { kind: 'user' }, })) - await agent.whenIdle() + const subagents = ctx.get('subagents') + while (true) { + await agent.whenIdle() + const idleSeq = agent.session.seq + // Child settlement can wake the parent before the child wait observes it. + if (!await subagents?.waitForChildren(agent) + && agent.status === 'idle' && agent.session.seq === idleSeq) break + } } finally { stopReasoning?.() } await sessions.flush(agent.session) + if (failure !== undefined) throw failure.error const outcome = summarize(agent.session, firstSeq) if (projection === undefined) io.stdout.write(outcome.text + '\n') else projection.finish(outcome.text) @@ -379,6 +397,8 @@ async function run(ctx: Context, config: Config, io: HeadlessIo): Promise } io.exit(outcome.reason?.kind === 'completed' ? 0 : 1) } finally { + stopErrors() + stopTerminals() projection?.dispose() } } diff --git a/packages/bundle/headless/tests/headless.spec.ts b/packages/bundle/headless/tests/headless.spec.ts index 5c8bd6b0f9..cc73507d5f 100644 --- a/packages/bundle/headless/tests/headless.spec.ts +++ b/packages/bundle/headless/tests/headless.spec.ts @@ -13,13 +13,14 @@ import type { ResumeAgentOptions, } from '@deepseek-ai/dsh-agent' import AgentDefaultModelConfig from '@deepseek-ai/dsh-agent-default-model' -import { LlmAttemptId, ToolCallId, createAssistantMessage, createToolResultMessage, type MessageId, type StreamChunk } from '@deepseek-ai/dsh-llm' +import { LlmAttemptId, ToolCallId, createAssistantMessage, createToolResultMessage, createUserMessage, type MessageId, type StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Session, SessionId, UserMessage } from '@deepseek-ai/dsh-session' import { SessionQueryError } from '@deepseek-ai/dsh-session-query' -import { createInboxStub } from '@deepseek-ai/dsh-agent-loop-testkit' +import { createInboxStub, provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit' import { apply, Config } from '../src/index.ts' +import SubagentRuntime from '@deepseek-ai/dsh-subagent' import { internals } from '../src/runner-internals.ts' const originalInternals = { ...internals } @@ -57,6 +58,8 @@ interface BenchOptions { preliveMeta?: { cwd?: string; origin?: 'subagent'; agentPreset?: string } /** Run when the runner awaits idle, e.g. to append to the attached log. */ onWhenIdle?: (agent: Agent) => void + /** Release child cleanup after the observed parent activity ends. */ + afterIdle?: () => void } const frameStates = new WeakMap; revision: number; index: number }>() @@ -125,6 +128,7 @@ async function bench(script: Script, options: BenchOptions = {}): Promise<{ run(): Promise<{ code: number; out: string; err: string; order: string[] }> }> { const ctx = new Context() + provideWorkingDirectoryFixture(ctx) if (options.filesystemCwd !== undefined) { const cwd = options.filesystemCwd ctx.provide('fs', { @@ -143,25 +147,29 @@ async function bench(script: Script, options: BenchOptions = {}): Promise<{ ): Promise => { const inbox = createInboxStub() let idle = Promise.resolve() + let running = false const agent: Agent = { id: session.id, options: createOptions.agentOptions ?? {}, session, inbox, - status: 'idle', + get status() { return running ? 'running' : 'idle' }, ctx: ownerCtx, cancel: () => {}, runMaintenance: () => Promise.reject(new Error('not used')), send: () => {}, followup: (message: UserMessage) => { agent.inbox.append('next-turn', message) - idle = Promise.resolve().then(() => script.afterPrompt(session, message, agent)) + running = true + idle = Promise.resolve().then(() => script.afterPrompt(session, message, agent)).finally(() => { running = false }) }, steer: () => {}, inject: () => {}, - whenIdle: () => { + whenIdle: async () => { options.onWhenIdle?.(agent) - return idle + let activity: Promise + do { await (activity = idle) } while (activity !== idle) + options.afterIdle?.() }, } await createOptions.setup?.(ownerCtx, agent) @@ -224,6 +232,120 @@ async function bench(script: Script, options: BenchOptions = {}): Promise<{ } describe('headless runner', () => { + it('waits for parent awakened between idle and child observation', async () => { + const cleanup = Promise.withResolvers() + const cleaning = Promise.withResolvers() + const finalTurn = Promise.withResolvers() + const parentWait = Promise.withResolvers() + let turns = 0 + const test = await bench({ + async afterPrompt(session, message, parent) { + turns += 1 + if (turns === 1) { + await test.ctx.subagents.startActivation({ + provider: 'external', label: 'work', delivery: 'parent', signal: new AbortController().signal, + request: { parent, prompt: [] }, + }) + await cleaning.promise + appendTurn(session, 1, message, 'waiting for child', true) + } else { + await finalTurn.promise + appendTurn(session, 2, message, 'child result incorporated', true) + } + }, + }, { + afterIdle: () => { if (turns === 1) cleanup.resolve(undefined) }, + onWhenIdle: () => { if (turns === 2) parentWait.resolve(undefined) }, + }) + await test.ctx.plugin(SubagentRuntime) + test.ctx.subagents.registerProvider({ + name: 'external', inheritsParentContext: false, + capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + start: async () => ({ + id: brandString('settlement-child'), + result: Promise.resolve({ output: [{ type: 'text' as const, text: 'child answer' }], stopReason: 'completed' as const }), + dispose: () => { cleaning.resolve(undefined); return cleanup.promise }, + }), + }) + try { + const run = test.run() + expect(await Promise.race([run.then(() => 'exited'), parentWait.promise.then(() => 'waiting')])).toBe('waiting') + expect(test.output().out).toBe('') + finalTurn.resolve(undefined) + await expect(run).resolves.toMatchObject({ code: 0, out: 'child result incorporated\n' }) + } finally { + cleanup.resolve(undefined) + finalTurn.resolve(undefined) + await test.ctx.fiber.dispose() + } + }) + + it('waits for later child batches started after settlement wakes the parent', async () => { + const waiting = [Promise.withResolvers(), Promise.withResolvers()] + const released = [Promise.withResolvers(), Promise.withResolvers()] + let turns = 0 + let checks = 0 + const test = await bench({ + afterPrompt(session, message) { + turns += 1 + appendTurn(session, turns, message, turns === 3 ? 'all children complete' : 'waiting for children', true) + }, + }) + test.ctx.provide('subagents', { + async waitForChildren(parent: Agent) { + expect(test.ctx.agents.get(parent.id)).toBe(parent) + const batch = checks++ + if (batch === 2) return false + waiting[batch]!.resolve(undefined) + await released[batch]!.promise + parent.followup(createUserMessage({ content: [{ type: 'text', text: `child batch ${batch + 1} complete` }], source: { kind: 'user' } })) + return true + }, + } as never) + const run = test.run() + try { + await waiting[0]!.promise + expect(test.output().out).toBe('') + released[0]!.resolve(undefined) + await waiting[1]!.promise + expect(turns).toBe(2) + expect(test.output().order).not.toContain('exit') + released[1]!.resolve(undefined) + await expect(run).resolves.toMatchObject({ code: 0, out: 'all children complete\n' }) + expect(checks).toBe(3) + } finally { + for (const release of released) release.resolve(undefined) + await run + await test.ctx.fiber.dispose() + } + }) + + it('rechecks children after a parent turn completes during the child wait', async () => { + let turns = 0 + let checks = 0 + const test = await bench({ + afterPrompt(session, message) { + turns += 1 + appendTurn(session, turns, message, turns === 3 ? 'all batches complete' : 'waiting for batch', true) + }, + }) + test.ctx.provide('subagents', { + async waitForChildren(parent: Agent) { + const check = checks++ + if (check === 2) return false + parent.followup(createUserMessage({ content: [{ type: 'text', text: 'batch complete' }], source: { kind: 'user' } })) + await parent.whenIdle() + // The first observation missed a released child; its parent started another batch. + return check !== 0 + }, + } as never) + try { + await expect(test.run()).resolves.toMatchObject({ code: 0, out: 'all batches complete\n' }) + } finally { + await test.ctx.fiber.dispose() + } + }) + it('records a fresh Session in the filesystem provider working directory', async () => { const cwd = '/remote/workspace' const test = await bench({ diff --git a/packages/bundle/headless/tests/terminal-failures.spec.ts b/packages/bundle/headless/tests/terminal-failures.spec.ts new file mode 100644 index 0000000000..5c20881ea9 --- /dev/null +++ b/packages/bundle/headless/tests/terminal-failures.spec.ts @@ -0,0 +1,154 @@ +/** Headless exit status after parent summaries fail outside a durable terminal. */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import type { Agent } from '@deepseek-ai/dsh-agent' +import AgentDefaultModelConfig from '@deepseek-ai/dsh-agent-default-model' +import AgentLoop from '@deepseek-ai/dsh-agent-loop' +import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' +import { createUserMessage } from '@deepseek-ai/dsh-llm' +import { SessionId } from '@deepseek-ai/dsh-session' +import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' +import { apply } from '../src/index.ts' +import { internals } from '../src/runner-internals.ts' + +const originalInternals = { ...internals } +let ctx: Context | undefined + +afterEach(async () => { + await ctx?.fiber.dispose() + ctx = undefined + Object.assign(internals, originalInternals) + vi.restoreAllMocks() +}) + +async function harness() { + const context = new Context() + ctx = context + await mountAgentLoopTestDependencies(context) + await context.plugin(AgentLoop, { agents: [] }) + await context.plugin(AgentDefaultModelConfig, { provider: 'mock', model: 'mock' }) + context.llm.registerAdapter(['mock'], new MockAdapter([ + textResponse('waiting'), textResponse('summary'), textResponse('recovered'), + ])) + let out = '' + let err = '' + internals.stdout = { write: (chunk: string) => { out += chunk; return true } } + internals.stderr = { write: (chunk: string) => { err += chunk; return true } } + const exited = Promise.withResolvers() + context.provide('appExit', exited.resolve) + return { + ctx: context, + async run(json = false) { + apply(context, { task: 'delegate', json }) + return { code: await exited.promise, out, err } + }, + } +} + +function followup(parent: Agent): void { + parent.followup(createUserMessage({ content: [{ type: 'text', text: 'child result' }], source: { kind: 'user' } })) +} + +describe('headless terminal failures', () => { + it.each([ + { failedEvent: 'turn/start', json: false }, + { failedEvent: 'turn/end', json: false }, + { failedEvent: 'turn/start', json: true }, + { failedEvent: 'turn/end', json: true }, + ] as const)('fails after a later $failedEvent cannot commit (json=$json)', async ({ failedEvent, json }) => { + const test = await harness() + const failures: unknown[] = [] + test.ctx.on('agent/error', ({ error }) => { failures.push(error) }) + const waitForChildren = vi.fn().mockImplementationOnce((parent: Agent) => { + const append = parent.session.append.bind(parent.session) + const failAppend: typeof parent.session.append = (type, data, ...intent) => { + if (type === failedEvent) throw new Error(`summary ${failedEvent} unavailable`) + return append(type, data, ...intent) + } + vi.spyOn(parent.session, 'append').mockImplementation(failAppend) + followup(parent) + return Promise.resolve(true) + }).mockResolvedValue(false) + test.ctx.provide('subagents', { waitForChildren } as never) + + const result = await test.run(json) + expect(failures).toHaveLength(1) + expect(result.code).toBe(1) + expect(result.err).toBe(`dsh: summary ${failedEvent} unavailable\n`) + if (json) { + const events = result.out.trim().split('\n').map(line => JSON.parse(line) as { type: string; message?: string }) + expect(events.at(-1)).toEqual({ type: 'error', message: `summary ${failedEvent} unavailable` }) + expect(events.some(event => event.type === 'final')).toBe(false) + } else { + expect(result.out).toBe('') + } + }) + + it.each([false, true])('uses the durable outcome after a summary hook fails (recover=%s)', async (recover) => { + const test = await harness() + const waitForChildren = vi.fn().mockImplementationOnce((parent: Agent) => { + const stop = test.ctx.on('agent/pre-step', () => { + stop() + throw new Error('summary hook failed') + }) + followup(parent) + return Promise.resolve(true) + }).mockImplementationOnce((parent: Agent) => { + if (recover) followup(parent) + return Promise.resolve(recover) + }).mockResolvedValue(false) + test.ctx.provide('subagents', { waitForChildren } as never) + + await expect(test.run()).resolves.toEqual(recover + ? { code: 0, out: 'summary\n', err: '' } + : { code: 1, out: 'waiting\n', err: 'dsh: UNKNOWN: summary hook failed\n' }) + }) + + it.each(['turn/start', 'turn/end'] as const)('accepts a recovered summary after a missing %s', async (failedEvent) => { + const test = await harness() + const waitForChildren = vi.fn().mockImplementationOnce((parent: Agent) => { + const append = parent.session.append.bind(parent.session) + let failed = false + const failAppend: typeof parent.session.append = (type, data, ...intent) => { + if (!failed && type === failedEvent) { + failed = true + throw new Error(`summary ${failedEvent} unavailable`) + } + return append(type, data, ...intent) + } + vi.spyOn(parent.session, 'append').mockImplementation(failAppend) + followup(parent) + return Promise.resolve(true) + }).mockImplementationOnce((parent: Agent) => { + followup(parent) + return Promise.resolve(true) + }).mockResolvedValue(false) + test.ctx.provide('subagents', { waitForChildren } as never) + + await expect(test.run()).resolves.toEqual({ + code: 0, + out: 'recovered\n', + err: '', + }) + }) + + it('ignores a different Agent failure during root completion', async () => { + const test = await harness() + const { agent: other } = await test.ctx.agents.create({ + sessionId: SessionId('other'), agentOptions: { provider: 'mock', model: 'mock' }, + }) + test.ctx.on('agent/pre-step', ({ agent }, next) => { + if (agent === other) throw new Error('another Agent failed') + return next() + }) + const waitForChildren = vi.fn().mockImplementationOnce(async () => { + followup(other) + await other.whenIdle() + return false + }).mockResolvedValue(false) + test.ctx.provide('subagents', { waitForChildren } as never) + + await expect(test.run()).resolves.toEqual({ code: 0, out: 'waiting\n', err: '' }) + }) +}) diff --git a/packages/bundle/headless/tsconfig.json b/packages/bundle/headless/tsconfig.json index e1d1128fea..640ee3b2ad 100644 --- a/packages/bundle/headless/tsconfig.json +++ b/packages/bundle/headless/tsconfig.json @@ -37,6 +37,9 @@ }, { "path": "../../fs/fs" + }, + { + "path": "../../subagent/subagent" } ] } diff --git a/packages/bundle/sdk-app/README.i18n.yaml b/packages/bundle/sdk-app/README.i18n.yaml index 0211211c9e..b8b29a0453 100644 --- a/packages/bundle/sdk-app/README.i18n.yaml +++ b/packages/bundle/sdk-app/README.i18n.yaml @@ -24,8 +24,8 @@ en: af149b1e7ed0a5fc zh: fe483822d03deddb /deepseek-ai-dsh-sdk-app/model-experience/sdk-coding-agent-persona/what-the-model-sees: - en: 895f2c7fc9357d23 - zh: d2e6455f4adcf7d3 + en: afe0af085d77833e + zh: f41ef2a2219f1248 /deepseek-ai-dsh-sdk-app/model-experience/sdk-coding-agent-persona/token-effect: en: 371d28831239e0a8 zh: 57595359ff30a259 diff --git a/packages/bundle/sdk-app/README.md b/packages/bundle/sdk-app/README.md index 03163050c5..df3d0aa353 100644 --- a/packages/bundle/sdk-app/README.md +++ b/packages/bundle/sdk-app/README.md @@ -44,7 +44,7 @@ The packaged Python runtime enables the [Office runtime query and skills](../../ #### What the model sees -The profile supplies `You are a coding agent powered by the {{model}} model.` before first-party guidance and `Your working directory is {{cwd}}.` in a separate persona suffix. The exact SDK initialization route and session cwd resolve the placeholders. Default file tool schemas include `read`, `write`, and `edit`; they omit `str_replace_editor`. +The profile supplies `You are a coding agent powered by the {{model}} model.` before first-party guidance; the SDK initialization route resolves `{{model}}`. Each Session receives its current directory in required user-role context from [`dsh-working-directory`](../../session/working-directory/README.md). Default file tool schemas include `read`, `write`, and `edit`; they omit `str_replace_editor`. #### Token effect diff --git a/packages/bundle/sdk-app/README.zh.md b/packages/bundle/sdk-app/README.zh.md index d61f586a0f..026d442b1a 100644 --- a/packages/bundle/sdk-app/README.zh.md +++ b/packages/bundle/sdk-app/README.zh.md @@ -44,7 +44,7 @@ SDK 使用 base 默认提供的 `read`、`write` 和 `edit`。要添加 `str_rep #### 模型看到什么 -profile 在第一方指导之前提供 `You are a coding agent powered by the {{model}} model.`,并在独立的 persona 后缀中提供 `Your working directory is {{cwd}}.`。确切的 SDK 初始化路由与会话 cwd 会解析其中的占位符。默认文件工具 schema 包含 `read`、`write` 和 `edit`,不包含 `str_replace_editor`。 +profile 在第一方指导之前提供 `You are a coding agent powered by the {{model}} model.`;SDK 初始化路由解析 `{{model}}`。每个 Session 通过 [`dsh-working-directory`](../../session/working-directory/README.zh.md) 提供的必需 user-role 上下文接收当前目录。默认文件工具 schema 包含 `read`、`write` 和 `edit`,不包含 `str_replace_editor`。 #### Token 影响 diff --git a/packages/bundle/sdk-app/cordis.patch.yml b/packages/bundle/sdk-app/cordis.patch.yml index 85aeaca996..d852a0a41d 100644 --- a/packages/bundle/sdk-app/cordis.patch.yml +++ b/packages/bundle/sdk-app/cordis.patch.yml @@ -2,7 +2,6 @@ - id: system-prompt config: - personaSuffix: Your working directory is {{cwd}}. personaPrefix: >- You are a coding agent powered by the {{model}} model. diff --git a/packages/bundle/sdk-app/package.json b/packages/bundle/sdk-app/package.json index 95308deec7..fdfc1d2ad3 100644 --- a/packages/bundle/sdk-app/package.json +++ b/packages/bundle/sdk-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-app", "description": "The dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-base", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/sdk-minimal/README.i18n.yaml b/packages/bundle/sdk-minimal/README.i18n.yaml index cc7d5e843e..1a22a57b32 100644 --- a/packages/bundle/sdk-minimal/README.i18n.yaml +++ b/packages/bundle/sdk-minimal/README.i18n.yaml @@ -3,23 +3,23 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/bundle/sdk-minimal/README.md /: - en: ae5bdac5add26503 - zh: 93cd37d41c05fc9c + en: 7cbc0e332890851f + zh: 07c71026d8e1bb15 /deepseek-ai-dsh-sdk-minimal: en: 807c43eedba7d53e zh: 9f1efe5d6ac393f7 /deepseek-ai-dsh-sdk-minimal/summary: - en: 1885da605fd804b8 - zh: 742cbfdd2a4a224b + en: c0a98b4bcbb3e719 + zh: 92895dd5ce381cd1 /deepseek-ai-dsh-sdk-minimal/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-sdk-minimal/use-this-package: - en: 107f4613e077f9ad - zh: e7d89433a98b296b + en: f920307c0cf12118 + zh: edf9051846fecd5c /deepseek-ai-dsh-sdk-minimal/understand-the-implementation: - en: 281b8d6f1aa0e526 - zh: 070c2a66b1b2feee + en: 91f4a78ab7a688d3 + zh: 7bb8bf33c0d8b429 /deepseek-ai-dsh-sdk-minimal/understand-the-implementation/source-map: en: 3784c554e96ab004 zh: 0a05d4fe1e26b3b1 @@ -33,11 +33,11 @@ en: bd87e16b0c6e0506 zh: c8ee5e3b4578754e /deepseek-ai-dsh-sdk-minimal/model-experience/minimal-coding-agent-composition/what-the-model-sees: - en: fa9af6239831a097 - zh: 6951e08b165cb282 + en: 724e1f4075df9293 + zh: 35955569ded10b56 /deepseek-ai-dsh-sdk-minimal/model-experience/minimal-coding-agent-composition/token-effect: - en: 65613fb41fcc3151 - zh: fc20a732aca8282c + en: 5df298b2c9f95d27 + zh: def93cd59b5ca843 /deepseek-ai-dsh-sdk-minimal/model-experience/minimal-coding-agent-composition/kv-cache-effect: en: 904ae09b5fdb87c1 zh: de6fcaaa12bd9733 diff --git a/packages/bundle/sdk-minimal/README.md b/packages/bundle/sdk-minimal/README.md index 9ca6d68810..7a4a0ff127 100644 --- a/packages/bundle/sdk-minimal/README.md +++ b/packages/bundle/sdk-minimal/README.md @@ -1,5 +1,5 @@ --- -description: "Standalone SDK profile with one default shell tool for users who need a minimal cross-platform coding agent without the shared base bundle." +description: "Standalone SDK profile with a persistent shell and Session working-directory tool for a minimal cross-platform coding agent without the shared base bundle." kind: "package-bundle" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Use `dsh --profile sdk-minimal` when an SDK client needs a small, explicit coding-agent runtime. By default, the profile advertises only a platform-selected persistent shell, persists sessions as uncompressed JSONL, and selects the model from the SDK initialization request. It supplies a complete Cordis tree and deliberately excludes `dsh-base`, Web, settings, managed credentials, telemetry, compaction, filesystem tools, workspace instructions, skills, jobs, and subagents. Its danger-full-access policy lets the shell modify any path available to the process, so use it only with an isolated workspace. +Use `dsh --profile sdk-minimal` when an SDK client needs a small, explicit coding-agent runtime. By default, the profile advertises a platform-selected persistent shell and `working_directory`, persists sessions as uncompressed JSONL, and selects the model from the SDK initialization request. It supplies a complete Cordis tree and excludes `dsh-base`, Web, settings, managed credentials, telemetry, compaction, filesystem tools, workspace instructions, skills, jobs, and subagents. Its danger-full-access policy lets the shell modify any path available to the process, so use it only with an isolated workspace. ## Table of Contents @@ -38,7 +38,7 @@ Use `dsh plugin --profile sdk-minimal` to manage persistent external dependencie The profile mounts exactly one persistent shell stack: Bash on Linux and macOS, or PowerShell on Windows. Both stacks use a 300-second timeout and one owner-scoped terminal; the other platform's rows remain disabled. -Like every shipped profile, it mounts [MCP resources](../../mcp/mcp-resources/README.md) once. Configure only [MCP client entries](../../mcp/mcp-client/README.md) to add servers. Clients mounted by another provider also count as configured. With no server configured in the caller's scope, MCP contributes no prompt text or tools and the default remains one shell tool. +Like every shipped profile, it mounts [MCP resources](../../mcp/mcp-resources/README.md) once. Configure only [MCP client entries](../../mcp/mcp-client/README.md) to add servers. Clients mounted by another provider also count as configured. With no server configured in the caller's scope, MCP contributes no prompt text or tools and the default remains the shell and `working_directory` tools. ----- @@ -48,7 +48,7 @@ Like every shipped profile, it mounts [MCP resources](../../mcp/mcp-resources/RE
Implementation internals — click to expand -The bundle's single insert is the complete application tree: SDK stdio startup and JSON-RPC serving, one environment-configured DeepSeek adapter, the explicit agent core, conditional MCP resource tools, local subprocess execution, a platform-selected persistent shell PTY, and uncompressed JSONL persistence under `$DSH_HOME/sessions`. It does not inherit another bundle, so every extra row is an explicit profile change. +The bundle's single insert is the complete application tree: SDK stdio startup and JSON-RPC serving, one environment-configured DeepSeek adapter, the explicit agent core, conditional MCP resource tools, local filesystem and subprocess services, Session working-directory state and its tool, a platform-selected persistent shell PTY, and uncompressed JSONL persistence under `$DSH_HOME/sessions`. It does not inherit another bundle, so every extra row is an explicit profile change. ### Source map @@ -78,11 +78,11 @@ The bundle's single insert is the complete application tree: SDK stdio startup a #### What the model sees -The system prompt is `DSH_SYSTEM_PROMPT` or `You are a helpful software engineer assistant.`. With no MCP server configured, the only advertised tool is owner-scoped persistent `bash` on Linux/macOS or `pwsh` on Windows; runtime context, filesystem tools, workspace instructions, skills, jobs controls, compaction, and Harness identity are absent. +The system prompt is `DSH_SYSTEM_PROMPT` or `You are a helpful software engineer assistant.`. With no MCP server configured, the advertised tools are `working_directory` and owner-scoped persistent `bash` on Linux/macOS or `pwsh` on Windows. Required user context supplies the current directory. Optional runtime context, filesystem tools, workspace instructions, skills, jobs controls, compaction, and Harness identity are absent. #### Token effect -By default, one stable persona plus one tool schema. Configured MCP servers add their tools, shared resource tools, and available server instructions. Tool results and ordinary conversation history grow with the session. +By default, one stable persona, two tool schemas, and user context for the initial directory and subsequent changes. Configured MCP servers add their tools, shared resource tools, and available server instructions. Tool results and ordinary conversation history grow with the session. #### KV Cache effect diff --git a/packages/bundle/sdk-minimal/README.zh.md b/packages/bundle/sdk-minimal/README.zh.md index 24b023c284..96de535e19 100644 --- a/packages/bundle/sdk-minimal/README.zh.md +++ b/packages/bundle/sdk-minimal/README.zh.md @@ -1,5 +1,5 @@ --- -description: "供需要不含共享 base bundle 的极简跨平台 coding agent(编程智能体)的用户使用的独立 SDK profile,默认提供一个 shell 工具。" +description: "独立 SDK profile,提供持久 shell 和 Session 工作目录工具,用于不含共享 base bundle 的极简跨平台 coding agent(编程智能体)。" kind: "package-bundle" --- @@ -9,7 +9,7 @@ kind: "package-bundle" ## 概述 -当 SDK 客户端需要小型、显式的 coding agent 运行时时,请使用 `dsh --profile sdk-minimal`。该 profile 默认只公布按平台选择的持久 shell,把会话持久化为未压缩 JSONL,并从 SDK 初始化请求选择模型。它提供完整 Cordis 配置树,并刻意排除 `dsh-base`、Web、settings、托管凭据、遥测、压缩(compaction)、文件系统工具、workspace 指令、skill(技能)、jobs 与 subagent。其 danger-full-access 策略允许 shell 修改进程可访问的任何路径,因此只能配合隔离 workspace 使用。 +当 SDK 客户端需要小型、显式的 coding agent 运行时时,请使用 `dsh --profile sdk-minimal`。该 profile 默认公布按平台选择的持久 shell 和 `working_directory`,把会话持久化为未压缩 JSONL,并从 SDK 初始化请求选择模型。它提供完整 Cordis 配置树,并排除 `dsh-base`、Web、settings、托管凭据、遥测、压缩(compaction)、文件系统工具、workspace 指令、skill(技能)、jobs 与 subagent。其 danger-full-access 策略允许 shell 修改进程可访问的任何路径,因此只能配合隔离 workspace 使用。 ## 目录 @@ -38,7 +38,7 @@ dsh --profile sdk-minimal 该 profile 只挂载一套持久 shell:Linux 和 macOS 使用 Bash,Windows 使用 PowerShell。两套配置都使用 300 秒超时与一个 agent 自有终端;另一平台的配置项保持禁用。 -与其他随附 profile 一样,它统一挂载 [MCP 资源](../../mcp/mcp-resources/README.zh.md)一次。只需配置 [MCP 客户端条目](../../mcp/mcp-client/README.zh.md)即可添加服务器。其他提供方挂载的客户端也属于已配置状态。调用方作用域中未配置服务器时,MCP 不贡献提示词文本或工具,默认仍只有一个 shell 工具。 +与其他随附 profile 一样,它统一挂载 [MCP 资源](../../mcp/mcp-resources/README.zh.md)一次。只需配置 [MCP 客户端条目](../../mcp/mcp-client/README.zh.md)即可添加服务器。其他提供方挂载的客户端也属于已配置状态。调用方作用域中未配置服务器时,MCP 不贡献提示词文本或工具,默认仍为 shell 与 `working_directory` 两个工具。 ----- @@ -48,7 +48,7 @@ dsh --profile sdk-minimal
实现细节——点击展开 -该 bundle 的单个 insert 就是完整应用配置树:SDK stdio 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、显式 agent 核心、按配置启用的 MCP 资源工具、本地子进程执行、按平台选择的持久 shell PTY,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 持久化。它不继承其他 bundle,因此每个额外配置项都是显式 profile 变更。 +该 bundle 的单个 insert 就是完整应用配置树:SDK stdio 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、显式 agent 核心、按配置启用的 MCP 资源工具、本地文件系统与子进程服务、Session 工作目录状态及其工具、按平台选择的持久 shell PTY,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 持久化。它不继承其他 bundle,因此每个额外配置项都是显式 profile 变更。 ### 源码地图 @@ -78,11 +78,11 @@ dsh --profile sdk-minimal #### 模型看到的内容 -系统提示词取 `DSH_SYSTEM_PROMPT`,未设置时使用 `You are a helpful software engineer assistant.`。未配置 MCP 服务器时,对外公布的唯一工具是 Linux/macOS 上 agent 所有的持久 `bash` 或 Windows 上的 `pwsh`;运行时上下文、文件系统工具、workspace 指令、skill、jobs 控制、压缩与 Harness 身份均不存在。 +系统提示词取 `DSH_SYSTEM_PROMPT`,未设置时使用 `You are a helpful software engineer assistant.`。未配置 MCP 服务器时,对外公布的工具为 `working_directory`,以及 Linux/macOS 上 agent 所有的持久 `bash` 或 Windows 上的 `pwsh`。必需的用户上下文提供当前目录。可选运行时上下文、文件系统工具、workspace 指令、skill、jobs 控制、压缩与 Harness 身份均不存在。 #### Token 影响 -默认是一个稳定 persona 加一个工具 schema。已配置 MCP 服务器会添加自己的工具、共享资源工具及可用的服务器指令。工具结果与普通对话历史随会话增长。 +默认是一个稳定 persona、两个工具 schema,以及初始目录与后续变更的用户上下文。已配置 MCP 服务器会添加自己的工具、共享资源工具及可用的服务器指令。工具结果与普通对话历史随会话增长。 #### KV Cache 影响 diff --git a/packages/bundle/sdk-minimal/cordis.patch.yml b/packages/bundle/sdk-minimal/cordis.patch.yml index 0ff0ae9a6c..6c8dbce78a 100644 --- a/packages/bundle/sdk-minimal/cordis.patch.yml +++ b/packages/bundle/sdk-minimal/cordis.patch.yml @@ -35,6 +35,15 @@ # Shared projection registry: sandbox-policy and terminal-bash fold # sandbox-mode state through its units and require it as a hard injection. + - id: filesystem + name: '@deepseek-ai/dsh-fs-local' + + - id: working-directory + name: '@deepseek-ai/dsh-working-directory' + + - id: tool-working-directory + name: '@deepseek-ai/dsh-tool-working-directory' + - id: session-projection name: '@deepseek-ai/dsh-session-projection' diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json index 43f5ea6f68..df59eaae4d 100644 --- a/packages/bundle/sdk-minimal/package.json +++ b/packages/bundle/sdk-minimal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-minimal", "description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessions", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -59,7 +59,10 @@ "@deepseek-ai/dsh-terminal-bash": "workspace:*", "@deepseek-ai/dsh-tool-bash-persistent": "workspace:*", "@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:*", - "@deepseek-ai/dsh-tools": "workspace:*" + "@deepseek-ai/dsh-tools": "workspace:*", + "@deepseek-ai/dsh-working-directory": "workspace:*", + "@deepseek-ai/dsh-tool-working-directory": "workspace:*", + "@deepseek-ai/dsh-fs-local": "workspace:*" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" diff --git a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts index dda3b50ed6..4194bdf4b0 100644 --- a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts +++ b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts @@ -33,6 +33,9 @@ describe('dsh-sdk-minimal bundle', () => { ['plugin-package-inventory-deepseek', '@deepseek-ai/dsh-plugin-package-inventory-deepseek'], ['llm-deepseek', '@deepseek-ai/dsh-llm-deepseek-api-key'], ['sandbox', '@deepseek-ai/dsh-sandbox-local'], + ['filesystem', '@deepseek-ai/dsh-fs-local'], + ['working-directory', '@deepseek-ai/dsh-working-directory'], + ['tool-working-directory', '@deepseek-ai/dsh-tool-working-directory'], ['session-projection', '@deepseek-ai/dsh-session-projection'], ['sandbox-policy', '@deepseek-ai/dsh-sandbox-policy'], ['subprocess', '@deepseek-ai/dsh-subprocess-local'], diff --git a/packages/bundle/web-app/README.i18n.yaml b/packages/bundle/web-app/README.i18n.yaml index a50173349e..0c5a6c4a2e 100644 --- a/packages/bundle/web-app/README.i18n.yaml +++ b/packages/bundle/web-app/README.i18n.yaml @@ -36,8 +36,8 @@ en: fb819d10930089a8 zh: f2dee0ff170a4d41 /deepseek-ai-dsh-web-app/understand-the-implementation/patch-semantics: - en: 954a424ea7182c8f - zh: 9339bc21dba44d40 + en: 23ddb4d33e79c0a1 + zh: dc9a458b9492b49b /deepseek-ai-dsh-web-app/understand-the-implementation/advertised-application-url: en: ca977d2b2bcf72c5 zh: 2b81b377c133c1a8 diff --git a/packages/bundle/web-app/README.md b/packages/bundle/web-app/README.md index 32076cf231..fc4584fbad 100644 --- a/packages/bundle/web-app/README.md +++ b/packages/bundle/web-app/README.md @@ -87,7 +87,7 @@ The bundle is one patch layer of five files plus one runtime glue plugin: `cordi ### Patch semantics -A patch replaces the targeted row's whole `config`, so each web row restates every key it owns: the persona prefix and suffix templates, the `DSH_TOOLS_MODE` PTC mode opt-in, and the `session-query-sqlite` values on the base rows, then `insert` adds the web host rows, transport, and browser roster. The `webserver` and `web-runtime` rows inject the `webStartup` provider and read their invocation values directly; the `connection` row instead reads the bind-dependent `webRuntime` values the web-runtime row publishes, which are that provider's authorities plus the LAN literals of an all-interfaces bind. The per-agent tool rows the base mounts process-wide are disabled here and the preset roster takes over; the reasoning for each host-plane versus preset-plane decision is inline in the patch. +A patch replaces the targeted row's whole `config`, so each web row restates every key it owns: the persona prefix template, the `DSH_TOOLS_MODE` PTC mode opt-in, and the `session-query-sqlite` values on the base rows, then `insert` adds the web host rows, transport, and browser roster. The `webserver` and `web-runtime` rows inject the `webStartup` provider and read their invocation values directly; the `connection` row instead reads the bind-dependent `webRuntime` values the web-runtime row publishes, which are that provider's authorities plus the LAN literals of an all-interfaces bind. The per-agent tool rows the base mounts process-wide are disabled here and the preset roster takes over; the reasoning for each host-plane versus preset-plane decision is inline in the patch. ### Advertised application URL diff --git a/packages/bundle/web-app/README.zh.md b/packages/bundle/web-app/README.zh.md index 750407dd0f..3b7774bfd2 100644 --- a/packages/bundle/web-app/README.zh.md +++ b/packages/bundle/web-app/README.zh.md @@ -87,7 +87,7 @@ dsh --profile web --no-open --port 8080 ### patch 语义 -patch 会替换目标行的整个 `config`,因此每个 Web 行都重述自己拥有的每个键:基础行上的 persona 前缀与后缀模板、`DSH_TOOLS_MODE` PTC mode 开关与 `session-query-sqlite` 值,随后 `insert` 添加 Web 宿主行、传输层与浏览器名录。`webserver` 与 `web-runtime` 行注入 `webStartup` 提供方并直接读取本次调用的取值;`connection` 行则改为读取 web-runtime 行发布的、与绑定相关的 `webRuntime` 值,即该提供方的 authority 加上全接口绑定的 LAN 字面量。base 以进程级挂载的按 agent 工具行在这里被禁用,由 preset 名录接管;每项宿主层与 preset 层归属决策的理由以行内注释写在 patch 里。 +patch 会替换目标行的整个 `config`,因此每个 Web 行都重述自己拥有的每个键:基础行上的 persona 前缀模板、`DSH_TOOLS_MODE` PTC mode 开关与 `session-query-sqlite` 值,随后 `insert` 添加 Web 宿主行、传输层与浏览器名录。`webserver` 与 `web-runtime` 行注入 `webStartup` 提供方并直接读取本次调用的取值;`connection` 行则改为读取 web-runtime 行发布的、与绑定相关的 `webRuntime` 值,即该提供方的 authority 加上全接口绑定的 LAN 字面量。base 以进程级挂载的按 agent 工具行在这里被禁用,由 preset 名录接管;每项宿主层与 preset 层归属决策的理由以行内注释写在 patch 里。 ### 公告应用 URL diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index a452579e9d..ee5201b9c7 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -15,7 +15,6 @@ - id: system-prompt config: - personaSuffix: Your working directory is {{cwd}}. personaPrefix: >- You are a coding agent powered by the {{model}} model. @@ -31,7 +30,7 @@ - id: tools config: - # TEMPORARY workaround: DSH_TOOLS_MODE (native|ptc|both) opts a whole dsh + # TEMPORARY workaround: DSH_TOOLS_MODE (native|ptc) opts a whole dsh # process into PTC mode while per-session tool-presentation selection is being # designed; unset keeps the schema default (native). Remove the env seam # once the web UI owns the choice per session. @@ -472,8 +471,8 @@ disabled: true # The background-job REGISTRY stays on the host plane; only the model-facing -# `job_*` controls move. Its producers — `tool-bash` here, `tool-terminal` and a -# non-continuable `tool-subagent` elsewhere — are preset rows that resolve it +# `job_*` controls move. Its producers — `tool-bash` here and `tool-terminal` +# elsewhere — are preset rows that resolve it # with `ctx.get`, and an entry-local realm around the registry is invisible to # every sibling row outside that realm, so `run_in_background` answered # "background jobs unavailable" while the controls sat in the catalog. That is @@ -558,12 +557,6 @@ - id: tool-workflow disabled: true -# `base` ships this row disabled; the preset-plane gate reads declared row ids -# without regard to `disabled`, so this row must stay to keep `tool-ralph` off -# the host plane and out of collision with every preset's row. -- id: tool-ralph - disabled: true - - id: agent-instructions disabled: true diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index 5b9f722bfc..81c3782121 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-app", "description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -160,7 +160,6 @@ "@deepseek-ai/dsh-tool-present": "workspace:*", "@deepseek-ai/dsh-tool-pwsh": "workspace:*", "@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:*", - "@deepseek-ai/dsh-tool-ralph": "workspace:*", "@deepseek-ai/dsh-tool-schedule": "workspace:*", "@deepseek-ai/dsh-tool-skill": "workspace:*", "@deepseek-ai/dsh-tool-subagent": "workspace:*", @@ -174,7 +173,7 @@ "@deepseek-ai/dsh-workspace-changes": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", "commander": "^15.0.0", - "open": "^11.0.0", + "open": "^11.0.2", "@deepseek-ai/dsh-api-account-controller": "workspace:*", "@deepseek-ai/dsh-client-ui-settings-account": "workspace:*", "@deepseek-ai/dsh-client-shortcuts": "workspace:*", diff --git a/packages/bundle/web-app/presets/cordis.patch.yml b/packages/bundle/web-app/presets/cordis.patch.yml index f6527f77ba..277176f1a0 100644 --- a/packages/bundle/web-app/presets/cordis.patch.yml +++ b/packages/bundle/web-app/presets/cordis.patch.yml @@ -13,7 +13,6 @@ - id: persona name: '@deepseek-ai/dsh-persona' config: - suffix: Your working directory is {{cwd}}. prefix: >- You are a coding agent powered by the {{model}} model. - id: agent-instructions @@ -96,7 +95,6 @@ provider: spawn toolName: subagent modelSelectionSettings: true - backgroundMode: continuable toolFilter: deny: - schedule_create @@ -108,41 +106,18 @@ config: provider: fork toolName: subagent_fork - backgroundMode: continuable toolFilter: deny: - schedule_create - schedule_delete - schedule_list - schedule_update - - id: tool-subagent-codex - name: '@deepseek-ai/dsh-tool-subagent' - disabled: true - config: - provider: codex - toolName: subagent_codex - backgroundMode: one-shot - maxDepth: provider-managed - - id: tool-subagent-claude-code - name: '@deepseek-ai/dsh-tool-subagent' - disabled: true - config: - provider: claude-code - toolName: subagent_claude_code - backgroundMode: one-shot - maxDepth: provider-managed - id: workflow-ptc name: '@deepseek-ai/dsh-workflow-ptc' config: provider: spawn - id: tool-workflow name: '@deepseek-ai/dsh-tool-workflow' - - id: tool-ralph - name: '@deepseek-ai/dsh-tool-ralph' - disabled: true - config: - subagentProvider: spawn - maxRounds: 64 - id: tool-ask-user name: '@deepseek-ai/dsh-tool-ask-user' - id: tool-todo diff --git a/packages/bundle/web-app/presets/ptc.patch.yml b/packages/bundle/web-app/presets/ptc.patch.yml index de206c33be..1aed4cb9e9 100644 --- a/packages/bundle/web-app/presets/ptc.patch.yml +++ b/packages/bundle/web-app/presets/ptc.patch.yml @@ -11,7 +11,6 @@ - id: persona name: '@deepseek-ai/dsh-persona' config: - suffix: Your working directory is {{cwd}}. prefix: You are a coding agent powered by the {{model}} model. - id: agent-instructions name: '@deepseek-ai/dsh-agent-instructions' @@ -97,7 +96,6 @@ provider: spawn toolName: subagent modelSelectionSettings: true - backgroundMode: continuable toolFilter: deny: - schedule_create @@ -109,29 +107,12 @@ config: provider: fork toolName: subagent_fork - backgroundMode: continuable toolFilter: deny: - schedule_create - schedule_delete - schedule_list - schedule_update - - id: tool-subagent-codex - name: '@deepseek-ai/dsh-tool-subagent' - disabled: true - config: - provider: codex - toolName: subagent_codex - backgroundMode: one-shot - maxDepth: provider-managed - - id: tool-subagent-claude-code - name: '@deepseek-ai/dsh-tool-subagent' - disabled: true - config: - provider: claude-code - toolName: subagent_claude_code - backgroundMode: one-shot - maxDepth: provider-managed - id: workflow-ptc name: '@deepseek-ai/dsh-workflow-ptc' disabled: true @@ -140,12 +121,6 @@ - id: tool-workflow name: '@deepseek-ai/dsh-tool-workflow' disabled: true - - id: tool-ralph - name: '@deepseek-ai/dsh-tool-ralph' - disabled: true - config: - subagentProvider: spawn - maxRounds: 64 - id: tool-ask-user name: '@deepseek-ai/dsh-tool-ask-user' - id: tool-todo diff --git a/packages/bundle/web-app/presets/standard.patch.yml b/packages/bundle/web-app/presets/standard.patch.yml index a0b474b0e3..f2a9e080fe 100644 --- a/packages/bundle/web-app/presets/standard.patch.yml +++ b/packages/bundle/web-app/presets/standard.patch.yml @@ -11,7 +11,6 @@ - id: persona name: '@deepseek-ai/dsh-persona' config: - suffix: Your working directory is {{cwd}}. prefix: You are a coding agent powered by the {{model}} model. - id: agent-instructions name: '@deepseek-ai/dsh-agent-instructions' @@ -97,7 +96,6 @@ provider: spawn toolName: subagent modelSelectionSettings: true - backgroundMode: continuable toolFilter: deny: - schedule_create @@ -109,41 +107,18 @@ config: provider: fork toolName: subagent_fork - backgroundMode: continuable toolFilter: deny: - schedule_create - schedule_delete - schedule_list - schedule_update - - id: tool-subagent-codex - name: '@deepseek-ai/dsh-tool-subagent' - disabled: true - config: - provider: codex - toolName: subagent_codex - backgroundMode: one-shot - maxDepth: provider-managed - - id: tool-subagent-claude-code - name: '@deepseek-ai/dsh-tool-subagent' - disabled: true - config: - provider: claude-code - toolName: subagent_claude_code - backgroundMode: one-shot - maxDepth: provider-managed - id: workflow-ptc name: '@deepseek-ai/dsh-workflow-ptc' config: provider: spawn - id: tool-workflow name: '@deepseek-ai/dsh-tool-workflow' - - id: tool-ralph - name: '@deepseek-ai/dsh-tool-ralph' - disabled: true - config: - subagentProvider: spawn - maxRounds: 64 - id: tool-ask-user name: '@deepseek-ai/dsh-tool-ask-user' - id: tool-todo diff --git a/packages/client/connection/package.json b/packages/client/connection/package.json index ac61c45e70..b233722f10 100644 --- a/packages/client/connection/package.json +++ b/packages/client/connection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-connection", "description": "Authenticated RPC transport and generation lifecycle", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/file-upload/package.json b/packages/client/file-upload/package.json index 858ff12775..eecaa28d28 100644 --- a/packages/client/file-upload/package.json +++ b/packages/client/file-upload/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-file-upload", "description": "Agent-scoped browser file upload, streaming intake, and staged receipt service", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/hmr/package.json b/packages/client/hmr/package.json index 4273a95ef0..373c176123 100644 --- a/packages/client/hmr/package.json +++ b/packages/client/hmr/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-hmr", "description": "Web client graph synchronization and rebuilt-bundle reload transport", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/locale/package.json b/packages/client/locale/package.json index da2661cc55..a1577330a2 100644 --- a/packages/client/locale/package.json +++ b/packages/client/locale/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-locale", "description": "Locale plugin: Host-backed preference, extensible language catalog, browser fallback, and typed built-in dictionaries", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/modules/package.json b/packages/client/modules/package.json index 316a37c0c2..497adf5366 100644 --- a/packages/client/modules/package.json +++ b/packages/client/modules/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-modules", "description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/product-analytics/package.json b/packages/client/product-analytics/package.json index 48cc366c90..a7a8b4cddc 100644 --- a/packages/client/product-analytics/package.json +++ b/packages/client/product-analytics/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-product-analytics", "description": "Desktop product event collection and authenticated Host reporting", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/resources/package.json b/packages/client/resources/package.json index 9927e3838e..531d64def9 100644 --- a/packages/client/resources/package.json +++ b/packages/client/resources/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-resources", "description": "Unified client resource model: protocol-registered providers turn URL addresses into live values, consumed through the useResource global standard hook", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/shortcuts/package.json b/packages/client/shortcuts/package.json index 5d23246d5b..76f34297ec 100644 --- a/packages/client/shortcuts/package.json +++ b/packages/client/shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-shortcuts", "description": "Application keyboard command registry and physical-key routing", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/store/package.json b/packages/client/store/package.json index b7d52e19e7..e6284b5e8c 100644 --- a/packages/client/store/package.json +++ b/packages/client/store/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-store", "description": "React-free observable and snapshot-store contracts with the shared Zustand/Immer engine", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -27,7 +27,7 @@ }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", - "zustand": "~4.4.7", + "zustand": "~4.5.7", "immer": "^10.1.1" }, "files": [ diff --git a/packages/client/tsdown.client.ts b/packages/client/tsdown.client.ts index 386beef230..2e8c06b18e 100644 --- a/packages/client/tsdown.client.ts +++ b/packages/client/tsdown.client.ts @@ -20,6 +20,7 @@ import { transform } from 'lightningcss' import { optionalStringArray } from './modules/src/client/manifest.ts' import { PLATFORM_MODULES, PRELOADED_CLIENT_EXTERNALS } from './web/src/platform.ts' import { clientBuildEnvironmentDefines } from '../../scripts/client-build-environment.ts' +import { isExperimentalPackageName } from '../../scripts/experimental-package-policy.ts' import { BundleInputIsolation, physicalBundleInput } from '../../scripts/bundle-input-isolation.ts' /** @@ -629,7 +630,7 @@ function clientInputIsolation(id: string): { plugin: TsdownPlugin sourcePath: (source: string, mapPath: string) => string } { - const experimental = id.startsWith('@deepseek-ai/dsh-experimental-') + const experimental = isExperimentalPackageName(id) const inputs = new BundleInputIsolation(REPOSITORY_ROOT, `client bundle isolation (${id})`) return { plugin: { diff --git a/packages/client/ui-agent-preset/README.i18n.yaml b/packages/client/ui-agent-preset/README.i18n.yaml index e1ab2a5ab1..7181942309 100644 --- a/packages/client/ui-agent-preset/README.i18n.yaml +++ b/packages/client/ui-agent-preset/README.i18n.yaml @@ -15,11 +15,11 @@ en: d152484eb41ac6b4 zh: b3ad7867f23dad1c /deepseek-ai-dsh-client-ui-agent-preset/use-this-package: - en: abb24ddb24f5588d - zh: 2c58c8cd0e2a335e + en: d616984e840f036e + zh: 3e7c97c7d10669eb /deepseek-ai-dsh-client-ui-agent-preset/understand-the-implementation: - en: 4c88c83529317389 - zh: 5125122bdacfa3da + en: b3221ef8bc6d8096 + zh: 68cb05566249da10 /deepseek-ai-dsh-client-ui-agent-preset/further-exploration: en: bbae8d79844bc481 zh: 4302107fd1f460e6 diff --git a/packages/client/ui-agent-preset/README.md b/packages/client/ui-agent-preset/README.md index a896c050d5..09df3e46f3 100644 --- a/packages/client/ui-agent-preset/README.md +++ b/packages/client/ui-agent-preset/README.md @@ -29,7 +29,7 @@ Settings shows the built-in and custom card groups with default highlighting and With Coding Tools off in General Settings, the new-session picker and Settings roster offer Standard, Creator, and custom presets; turning them on also offers PTC and Minimal. Default selection and the Creator entry remain available in both states. Choosing a healthy default also synchronizes the blank session on the current new-task surface. Creator starts a new task using the `cordis` preset. -Turning Coding Tools off clears all unapplied choices through the existing Developer tools subscription. Once the off value is accepted, a host-backed connection resets a saved built-in PTC or Minimal default to Standard. Saved Standard, Creator, and custom defaults (including named PTC or Minimal overrides) stay unchanged. The user can select an available preset again after turning Coding Tools off. Existing sessions, including blank sessions reused by New Session, keep their selected mode; its label remains visible even when that mode is absent from the menu. Turning Coding Tools back on does not restore the old default or cleared choice. A remote connection using in-memory settings does not rewrite the Host's default. If Standard is missing or broken, the default remains unchanged and an error is shown. +Turning Coding Tools off clears all unapplied choices through the existing Developer tools subscription. Once the off value is accepted, a host-backed connection resets a saved built-in PTC or Minimal default to Standard. Saved Standard, Creator, and custom defaults (including named PTC or Minimal overrides) stay unchanged. The user can select an available preset again after turning Coding Tools off. Existing sessions keep their selected mode; New Session uses the current default. An existing mode's label remains visible even when that mode is absent from the menu. Turning Coding Tools back on does not restore the old default or cleared choice. A remote connection using in-memory settings does not rewrite the Host's default. If Standard is missing or broken, the default remains unchanged and an error is shown. Known shipped presets offer mode details and usage examples in a read-only dialog. Its tabs preserve each page's scroll position; closing returns focus to the opening action. Help does not change the new-task default. The default badge replaces the card's group badge, and the preset id appears beside the title. Guide copy and examples belong to this package. @@ -45,7 +45,7 @@ Entering Creator selects `cordis` for the receiving blank Session without changi `agentPresets/list` supplies the roster and marks the current default, and `agentPresets/read` one declaration's YAML for the viewer; default changes write the `agent-preset-registry` settings namespace. The picker, blank-session synchronization and read-only session label use recorded preset identities. Connection resets and settings updates refresh the roster. -`CreatePluginMenuItem` contributes to `plugins.add.actions` using the shared roster store. Its selection calls the owner's `onDismiss()` before the same `startCreatorDraft` callback exposed to Settings as `creatorDraft`. That callback stages `cordis` through `AgentPresetSeatController` and opens the task through `uiWorkspace.startSession`; it can reuse a blank Session and applies the staged choice when a Session binding becomes available. The choice is consumed once applied. Both Coding Tools states use the same picker and selection store, and `AgentPresetSeat` renders refusals through the shared `Toast` component. +`CreatePluginMenuItem` contributes to `plugins.add.actions` using the shared roster store. Its selection calls the owner's `onDismiss()` before the same `startCreatorDraft` callback exposed to Settings as `creatorDraft`. That callback stages `cordis` through `AgentPresetSeatController` and opens the task through `uiWorkspace.startSession` with explicit draft preservation; it can reuse a blank Session and applies the staged choice when a Session binding becomes available. The choice is consumed once applied. Both Coding Tools states use the same picker and selection store, and `AgentPresetSeat` renders refusals through the shared `Toast` component.
diff --git a/packages/client/ui-agent-preset/README.zh.md b/packages/client/ui-agent-preset/README.zh.md index 1002bdd374..7514fc9953 100644 --- a/packages/client/ui-agent-preset/README.zh.md +++ b/packages/client/ui-agent-preset/README.zh.md @@ -29,7 +29,7 @@ kind: "package-reference" 关闭通用设置中的代码工作工具后,新会话选择器和设置页列表提供标准、创造和自定义预设;开启后还提供 PTC 和极简模式。两种状态下均保留默认值选择和创造入口。选择健康定义作为默认值也会同步当前新任务页面的空白会话。Creator 入口开启一个使用 `cordis` preset 的新任务。 -关闭代码工作工具时,沿用现有 Developer tools 订阅清空所有尚未应用的选择。关闭状态被接受后,使用 Host 持久化设置的连接会将已保存的内置 PTC 或极简默认值回退为标准模式。已保存的标准、创造和自定义默认值(包括有名称的 PTC 或极简覆盖项)保持不变。关闭后用户仍可重新选择可用预设。已有会话(包括点击新建会话时复用的空白会话)保留所选模式;即使该模式不在菜单中,其标签仍会显示。重新开启代码工作工具不会恢复原默认值或已清除的选择。使用内存设置的远程连接不会改写 Host 的默认值。标准模式缺失或损坏时,默认值保持不变,并显示错误。 +关闭代码工作工具时,沿用现有 Developer tools 订阅清空所有尚未应用的选择。关闭状态被接受后,使用 Host 持久化设置的连接会将已保存的内置 PTC 或极简默认值回退为标准模式。已保存的标准、创造和自定义默认值(包括有名称的 PTC 或极简覆盖项)保持不变。关闭后用户仍可重新选择可用预设。已有会话保留所选模式;新会话使用当前默认值。即使已有会话的模式不在菜单中,其标签仍会显示。重新开启代码工作工具不会恢复原默认值或已清除的选择。使用内存设置的远程连接不会改写 Host 的默认值。标准模式缺失或损坏时,默认值保持不变,并显示错误。 已知的内置预设提供只读的模式说明与使用示例对话框。各页签保留各自的滚动位置;关闭后焦点回到打开它的操作。帮助不会改变新任务默认值。默认徽标取代卡片的分组徽标,预设 id 显示在标题旁。指南文案与示例归本包所有。 @@ -45,7 +45,7 @@ kind: "package-reference" `agentPresets/list` 提供列表并标记当前默认值,`agentPresets/read` 为查看器提供一条声明的 YAML;默认值的修改写入 `agent-preset-registry` settings 命名空间。选择器、空白会话同步和只读会话标签使用记录的 preset 标识。连接重置和设置更新会刷新列表。 -`CreatePluginMenuItem` 使用共享的预设列表 store,向 `plugins.add.actions` 贡献菜单项。选择时先调用页面传入的 `onDismiss()`,再调用与设置页 `creatorDraft` 相同的 `startCreatorDraft` 回调。该回调通过 `AgentPresetSeatController` 暂存 `cordis`,再通过 `uiWorkspace.startSession` 打开任务;它可以复用空白 Session,并在 Session 绑定可用时应用暂存选择。暂存选择应用后即清除。代码工作工具开启或关闭时均使用同一个选择器和选择 store,`AgentPresetSeat` 通过共享的 `Toast` 组件显示拒绝原因。 +`CreatePluginMenuItem` 使用共享的预设列表 store,向 `plugins.add.actions` 贡献菜单项。选择时先调用页面传入的 `onDismiss()`,再调用与设置页 `creatorDraft` 相同的 `startCreatorDraft` 回调。该回调通过 `AgentPresetSeatController` 暂存 `cordis`,再通过显式保留草稿的 `uiWorkspace.startSession` 调用打开任务;它可以复用空白 Session,并在 Session 绑定可用时应用暂存选择。暂存选择应用后即清除。代码工作工具开启或关闭时均使用同一个选择器和选择 store,`AgentPresetSeat` 通过共享的 `Toast` 组件显示拒绝原因。
diff --git a/packages/client/ui-agent-preset/package.json b/packages/client/ui-agent-preset/package.json index 2f5bc155e6..d65a284667 100644 --- a/packages/client/ui-agent-preset/package.json +++ b/packages/client/ui-agent-preset/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-agent-preset", "description": "Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-agent-preset/src/client/index.ts b/packages/client/ui-agent-preset/src/client/index.ts index 7869f9cf4d..184b7fd791 100644 --- a/packages/client/ui-agent-preset/src/client/index.ts +++ b/packages/client/ui-agent-preset/src/client/index.ts @@ -201,7 +201,7 @@ export function apply(ctx: ClientContext): void { const startCreatorDraft = () => { const seat = mainBlankSeat() ?? unboundSeat seat.stage('cordis', true) - scope.uiWorkspace.startSession() + scope.uiWorkspace.startSession(undefined, { clearPreviousDraft: false }) void seat.apply() } diff --git a/packages/client/ui-agent-preset/tests/apply.client.spec.ts b/packages/client/ui-agent-preset/tests/apply.client.spec.ts index 459c4db863..f23bde9a6e 100644 --- a/packages/client/ui-agent-preset/tests/apply.client.spec.ts +++ b/packages/client/ui-agent-preset/tests/apply.client.spec.ts @@ -217,7 +217,7 @@ function uiWorkspaceDouble() { const starts: unknown[] = [] return { starts, - startSession: (workspaceId?: unknown) => { starts.push(workspaceId ?? null) }, + startSession: (workspaceId?: unknown, options?: unknown) => { starts.push({ workspaceId, options }) }, } } @@ -1136,7 +1136,7 @@ describe('ui-agent-preset apply', () => { await vi.waitFor(() => { expect(calls).toContain('select:cordis') }) expect(seat.hooks.agentPresetSeat.getSnapshot().current).toBe('cordis') - expect(uiWorkspace.starts).toHaveLength(1) + expect(uiWorkspace.starts).toEqual([{ workspaceId: undefined, options: { clearPreviousDraft: false } }]) conversation() }) diff --git a/packages/client/ui-approval/package.json b/packages/client/ui-approval/package.json index ae49c044ed..4d8ef679e1 100644 --- a/packages/client/ui-approval/package.json +++ b/packages/client/ui-approval/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-approval", "description": "Approval composer takeover over the scoped Remote Event waterfall", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-attachment/package.json b/packages/client/ui-attachment/package.json index c730f2eb18..e37afaf50d 100644 --- a/packages/client/ui-attachment/package.json +++ b/packages/client/ui-attachment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-attachment", "description": "Dynamic attachment presentation plugin for conversation input, message-image, and trajectory image slots", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-brand-official/package.json b/packages/client/ui-brand-official/package.json index f415ff31c5..b4587a84ac 100644 --- a/packages/client/ui-brand-official/package.json +++ b/packages/client/ui-brand-official/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-brand-official", "description": "Official DeepSeek Harness brand occupants for the Web client's sidebar slots", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -47,7 +47,7 @@ "@deepseek-ai/dsh-client-ui-renderer": "workspace:*", "@deepseek-ai/dsh-client-ui-sidebar": "workspace:*", "@deepseek-ai/cordis": "workspace:~", - "@testing-library/react": "^16.1.0", + "@testing-library/react": "^16.3.3", "@types/react": "~18.3.1", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index b2435fa8ab..91c230be23 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -33,8 +33,8 @@ en: 0ca3e8c4eab1ca78 zh: 1a1c71bab151eb7d /deepseek-ai-dsh-client-ui-chat/grouped-rendering: - en: bb4661cd2b435e2b - zh: bde68d03839f7da8 + en: c769b9eaddf02549 + zh: e36efc654be557e2 /deepseek-ai-dsh-client-ui-chat/scroll-ownership: en: 69b021c5a5d8b5a7 zh: 091ae7986c7a8a20 diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index b8e1441864..02bcbe3ddb 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -93,7 +93,7 @@ Chat registers its process Group Definition through `uiConversation.groups`. Rea `groupPart` selects reasoning or response in the Assistant renderer without copying Node payloads. A Tool node owns its preparing, dispatched, and result stages under one callId. Each part has a distinct DOM anchor for reading-position restoration; Turn navigation addresses the original Node key and lands on its first visible part. Group sources, member parents, and keys survive display-mode changes and newly loaded prefixes that extend an intact group. The source Node Store remains the only Node-data owner, and a replaced Builder rebinds keyed subscriptions without remounting seats. Mode changes retain size observers and reuse the Turn-state selector. -Live tool deltas share reasoning's frame-batched publication; durable calls and results publish immediately. Repeated deltas retain the Tool node when observed argument answers, the anchor, location, and visibility are unchanged. The Definition supplies the same lazy argument reader to tool rows and group detail, without tool-specific registration. +Live tool deltas share reasoning's frame-batched publication; durable calls and results publish immediately. Native and nested results retain event presentation metadata, so historical file and command labels use the operation’s location. Repeated deltas retain the Tool node when observed argument answers, the anchor, location, and visibility are unchanged. The Definition supplies the same lazy argument reader to tool rows and group detail, without tool-specific registration. The process group uses a stable `div` layout box, a scroll body, and an uncapped content box that reports growth inside the body. Business styles must adapt spacing within and across groups, including hidden or empty members and the answer-spacing exception. CSS variables do not belong in the Group Definition. @@ -105,6 +105,8 @@ Scroll-edge fades initialize when `ResizeObserver` reports the open group's layo Each group owns local `useDisclosure` state that survives mode changes while its component stays mounted. +The `conversation.chat.reasoning-body` chain offers the original reasoning text and streaming state only when its body is expanded. An unclaimed body keeps compact Markdown; a claiming display extension must preserve access to the original text. Extensions do not replace the reasoning preview or its disclosure policy. + The Chat-node slot injects a reset-bound `useDisclosure` Hook for reasoning and tools. Intermediate renderers forward it without subscribing; each invocation owns independent open state. Source callbacks retain their receiver and stable identity. When an enclosing Turn actually hides a process member, its seat resets those disclosures without replacing component keys or changing the Hook reference. Display-mode changes preserve their open state. Process rows share one text and icon color in every Work details mode: tertiary at rest and secondary on hover, while error and warning text keeps its semantic color. Leading icons, disclosure chevrons, and their boxes scale with the content font-size delta. diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index 607b3075a4..560e37e1fa 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -93,7 +93,7 @@ Chat 通过 `uiConversation.groups` 注册过程 Group Definition。React 通过 `groupPart` 在 Assistant 渲染器中选择推理或回复,不复制 Node 载荷。同一个 callId 的准备、派发与结果阶段由 Tool 节点自己拥有。每个部分有独立的 DOM 锚点用于恢复阅读位置;轮次导航使用原 Node key,落到它的第一个可见部分。展示模式切换以及为完整旧组补入更早成员时,保留组来源、成员父级及 key。原 Node Store 仍是唯一节点数据所有者,替换 Builder 时重新绑定按键订阅,不重挂载容器。模式变化保留尺寸观察器,并复用整轮状态选择器。 -实时工具 delta 与推理共用按帧合并的发布节奏;持久调用和结果立即发布。当已读取参数的答案、锚点、位置及可见性均未变化时,重复 delta 保持 Tool 节点不变。Definition 向工具行和分组详情提供同一个懒计算参数读器,无需按工具注册。 +实时工具 delta 与推理共用按帧合并的发布节奏;持久调用和结果立即发布。原生与嵌套结果保留事件中的展示元数据,使历史文件与命令标签使用执行时的位置。当已读取参数的答案、锚点、位置及可见性均未变化时,重复 delta 保持 Tool 节点不变。Definition 向工具行和分组详情提供同一个懒计算参数读器,无需按工具注册。 过程组使用稳定的 `div` 布局盒子、滚动正文及不限高的内容盒子,后者报告正文内部的内容增长。业务样式必须适配组内及组边界间距,处理隐藏或空成员以及回答前的间距特例。CSS 变量不属于 Group Definition。 @@ -105,6 +105,8 @@ Chat 通过 `uiConversation.groups` 注册过程 Group Definition。React 通过 每个组拥有本地 `useDisclosure` 状态,组件保持挂载时,模式切换保留该状态。 +`conversation.chat.reasoning-body` 链仅在思考正文展开时提供原始思考文本和流式状态。未被认领的正文使用紧凑 Markdown;认领显示的扩展必须保留查看原文的能力。扩展不替换思考预览或展开策略。 + Chat 节点 slot 为推理与工具注入绑定重置来源的 `useDisclosure` 钩子。中间 renderer 只透传、不订阅,每次调用拥有独立展开状态。来源回调保留接收对象及稳定引用。外层轮次实际隐藏过程成员时,所在节点重置这些开合状态,不替换组件 key,也不改变钩子引用。展示模式切换保留展开状态。 过程行的文字与图标在所有工作过程展示模式下共用一种颜色:默认 tertiary、悬停 secondary,错误与警告文字保留语义色。前置图标、折叠箭头及其容器随正文字号增量缩放。 diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json index 2d665716bd..432346bf9d 100644 --- a/packages/client/ui-chat/package.json +++ b/packages/client/ui-chat/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-chat", "description": "Chat Conversation target, node definitions, renderers, and details surface", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -87,7 +87,7 @@ "@deepseek-ai/dsh-tools": "workspace:*", "@deepseek-ai/dsh-util-workspace-path": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", - "@tanstack/react-virtual": "^3.14.9", + "@tanstack/react-virtual": "^3.14.10", "@types/react": "~18.3.1", "@types/react-dom": "~18.3.0", "react": "^18.2.0", diff --git a/packages/client/ui-chat/src/client/chat/AssistantMarkdown.tsx b/packages/client/ui-chat/src/client/chat/AssistantMarkdown.tsx index 5d97a698f7..476565ecbb 100644 --- a/packages/client/ui-chat/src/client/chat/AssistantMarkdown.tsx +++ b/packages/client/ui-chat/src/client/chat/AssistantMarkdown.tsx @@ -1,5 +1,6 @@ import { Fragment, memo, useMemo } from 'react' import { fileMediaUrl } from '@deepseek-ai/dsh-util-workspace-path' +import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' import type { ReactNode } from 'react' import { JsonBlock, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives' import type { MarkdownFileMentions, MarkdownPathImages } from '@deepseek-ai/dsh-client-ui-primitives' @@ -38,6 +39,8 @@ export interface AssistantMarkdownProps { interrupted?: boolean | undefined /** Render consecutive image blocks through the attachment slot. */ renderMessageImages: ChatNodeOwnerProps['renderMessageImages'] + /** Optional expanded reasoning extension; absent for standalone renderers. */ + renderReasoningBody?: PropsRenderSlots<'conversation.chat.reasoning-body'>['renderSlotChain'] | undefined /** Hide reasoning that belongs to the Turn-level process disclosure. */ reasoningHidden?: boolean | undefined /** Live display policy for reasoning summaries. */ @@ -53,7 +56,7 @@ export interface AssistantMarkdownProps { /** Reasoning block as the Think variant summary row (figma 39:28304). */ export const AssistantMarkdown = memo(function AssistantMarkdown({ blocks, streaming, interrupted, renderMessageImages, groupPart, useDisclosure, - reasoningHidden = false, usePresentation, revealProcess, mentions, t, + reasoningHidden = false, usePresentation, revealProcess, mentions, renderReasoningBody, t, }: AssistantMarkdownProps) { // Stable per locale revision (t identity changes on switch): a fresh object // per render would rebuild MarkdownText's component table every chunk. @@ -97,7 +100,7 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({ reveal={revealProcess} > + useDisclosure={useDisclosure} renderReasoningBody={renderReasoningBody} t={t} /> , ) break diff --git a/packages/client/ui-chat/src/client/chat/AssistantNodeView.tsx b/packages/client/ui-chat/src/client/chat/AssistantNodeView.tsx index 76ce74ef02..e934c4e708 100644 --- a/packages/client/ui-chat/src/client/chat/AssistantNodeView.tsx +++ b/packages/client/ui-chat/src/client/chat/AssistantNodeView.tsx @@ -1,13 +1,15 @@ import { memo, useCallback, useMemo } from 'react' -import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots' +import type { InjectFace, PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' import type { ChatNodeViewProps, PresentationInjected, TurnTailOwnerProps } from '../contract/slots.ts' import { AssistantMarkdown } from './AssistantMarkdown.tsx' type AssistantNodeViewProps = ChatNodeViewProps<'assistant-step'> & InjectFace + & PropsRenderSlots<'conversation.chat.reasoning-body'> /** Streaming, settled, and interrupted Assistant states share one keyed renderer instance. */ export const AssistantNodeView = memo(function AssistantNodeView({ - node, groupPart, useDisclosure, useTurnData, turnProcess, openFile, renderMessageImages, fileMentions, usePresentation, t, + node, groupPart, useDisclosure, useTurnData, turnProcess, openFile, renderMessageImages, fileMentions, + usePresentation, renderSlotChain, t, }: AssistantNodeViewProps) { const data = node.data const turn = node.location.kind === 'turn' || node.location.kind === 'step' @@ -39,6 +41,7 @@ export const AssistantNodeView = memo(function AssistantNodeView({ renderMessageImages={renderMessageImages} reasoningHidden={reasoningHidden} usePresentation={usePresentation} + renderReasoningBody={renderSlotChain} revealProcess={revealProcess} mentions={mentions} t={t} diff --git a/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx b/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx index 301a338df4..621d37f987 100644 --- a/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx +++ b/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx @@ -1,6 +1,7 @@ /** Assistant reasoning disclosure, independent of Tool-call presentation. */ import { memo, useMemo } from 'react' import { DisclosureRow, IconThinkOutlineRegular, MarkdownText, TextShimmer } from '@deepseek-ai/dsh-client-ui-primitives' +import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' import type { ChatViewSlotProps, UseDisclosure, UsePresentation } from '../contract/slots.ts' import { markdownLabels } from '../markdown-labels.ts' import a11yCss from './accessibility.module.css' @@ -41,14 +42,16 @@ function latestCompletedParagraphFirstLine(text: string): string { * @param props.running - whether this block is the streaming tail. * @param props.usePresentation - live display-policy selector for this reasoning row. * @param props.useDisclosure - independent open state with enclosing-Turn resets. + * @param props.renderReasoningBody - optional expanded-body extension with original Markdown fallback. * @param props.t - conversation locale seat for status and Markdown actions. * @returns the reasoning disclosure. */ -export const ReasoningRow = memo(function ReasoningRow({ text, running, usePresentation, useDisclosure, t }: { +export const ReasoningRow = memo(function ReasoningRow({ text, running, usePresentation, useDisclosure, renderReasoningBody, t }: { text: string running: boolean useDisclosure: UseDisclosure usePresentation: UsePresentation + renderReasoningBody?: PropsRenderSlots<'conversation.chat.reasoning-body'>['renderSlotChain'] | undefined t: ChatViewSlotProps['t'] }) { const { expanded, toggle } = useDisclosure() @@ -69,9 +72,13 @@ export const ReasoningRow = memo(function ReasoningRow({ text, running, usePrese ), [running, summary]) const content = useMemo(() => expanded ? (
- + {renderReasoningBody === undefined + ? + : renderReasoningBody('conversation.chat.reasoning-body', { text, running }, { + fallback: , + })}
- ) : undefined, [expanded, labels, running, text]) + ) : undefined, [expanded, labels, renderReasoningBody, running, text]) return (
({ hooks: { presentation } }), }, AssistantNodeView)) ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({ diff --git a/packages/client/ui-chat/src/client/contract/slots.ts b/packages/client/ui-chat/src/client/contract/slots.ts index 5c7a8e37cd..33dd85d4fc 100644 --- a/packages/client/ui-chat/src/client/contract/slots.ts +++ b/packages/client/ui-chat/src/client/contract/slots.ts @@ -68,6 +68,14 @@ export interface QuotaNoticeState { seq: number } +/** Original reasoning offered to optional display-only renderers. */ +export interface ReasoningBodyOwnerProps { + /** Complete or streaming model-provided reasoning, unchanged by display extensions. */ + text: string + /** Whether the reasoning tail is still streaming. */ + running: boolean +} + /** Owner currency of one quota notice offered to the frame-wide chain. */ export interface QuotaNoticeOwnerProps { /** Stable failure code retained in the Session log. */ @@ -307,6 +315,12 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { * replaces that command renderer; an unoccupied key uses the generic card. */ 'conversation.chat.commandview': { kind: 'keyed'; scope: 'session'; owner: CommandRowOwnerProps } + /** + * Display-only rendering of an expanded reasoning body. The first matching + * contribution renders; an unclaimed body uses the original compact Markdown. + * Contributions must preserve access to the original model-provided text. + */ + 'conversation.chat.reasoning-body': { kind: 'chain'; scope: 'session'; owner: ReasoningBodyOwnerProps } /** * Ordered feature contributions before a completed Turn's action row. Each * entry receives the Turn, closing sequence, and file opener. A fresh `id` diff --git a/packages/client/ui-chat/src/client/conversation-nodes/tool.ts b/packages/client/ui-chat/src/client/conversation-nodes/tool.ts index 6d4e5c28ea..0252539059 100644 --- a/packages/client/ui-chat/src/client/conversation-nodes/tool.ts +++ b/packages/client/ui-chat/src/client/conversation-nodes/tool.ts @@ -125,6 +125,7 @@ interface DispatchData { readonly isError?: boolean readonly error?: { name: string; code: string; reason?: string } readonly content?: ToolResultNode['content'] + readonly meta?: ToolResultNode['meta'] } function childCall(match: ConversationMatch, data: DispatchData): StartedToolCall { @@ -154,6 +155,7 @@ function childResult(match: ConversationMatch, data: DispatchData, previous?: To call: { name: data.name, argsRaw: jsonArguments(data.arguments) }, callTime: previous?.time ?? null, content: data.content ?? [], + meta: data.meta, isError: data.isError === true, ...data.error === undefined ? {} : { error: data.error }, subCalls: [], diff --git a/packages/client/ui-chat/src/client/index.ts b/packages/client/ui-chat/src/client/index.ts index 7dc1146431..8bb8dfb8c3 100644 --- a/packages/client/ui-chat/src/client/index.ts +++ b/packages/client/ui-chat/src/client/index.ts @@ -35,7 +35,7 @@ export type { AssistantActionOwnerProps, ChatFileMentions, ChatNodeHookContext, ChatNodeInjected, ChatNodeOwnerProps, ChatNodeViewProps, ChatScrollPosition, ChatStore, ChatViewInjected, ChatViewSlotProps, CommandRowOwnerProps, CommandRowProps, MessageImagesProps, OpenFileOptions, PresentationInjected, - QuotaNoticeCode, QuotaNoticeHostProps, QuotaNoticeInjected, QuotaNoticeOwnerProps, QuotaNoticeState, + QuotaNoticeCode, QuotaNoticeHostProps, QuotaNoticeInjected, QuotaNoticeOwnerProps, QuotaNoticeState, ReasoningBodyOwnerProps, TurnProcessOwnerProps, TurnTailOwnerProps, UseChat, UseChatNodeTurnData, UseDisclosure, UsePresentation, } from './contract/slots.ts' export type { diff --git a/packages/client/ui-chat/tests/chat-view.client.spec.tsx b/packages/client/ui-chat/tests/chat-view.client.spec.tsx index 7205405e49..47931e1f23 100644 --- a/packages/client/ui-chat/tests/chat-view.client.spec.tsx +++ b/packages/client/ui-chat/tests/chat-view.client.spec.tsx @@ -342,7 +342,8 @@ function makeHarness( case 'context': return case 'assistant-step': - return + return options?.fallback} /> case 'command': return ( { isError: true, error: { name: 'AutoReviewDeniedError', code: 'AUTO_REVIEW_DENIED', reason: 'blocked' }, content: [{ type: 'text', text: 'contents' }], + meta: { cwd: '/selected', path: '/selected/README.md' }, }), at(16, 'tool/result', { turn: 2, @@ -1432,6 +1433,7 @@ describe('built-in conversation node Definitions', () => { { kind: 'tool-result', callId: 'child', parentCallId: 'history-root', call: { name: 'read' }, error: { name: 'AutoReviewDeniedError', code: 'AUTO_REVIEW_DENIED', reason: 'blocked' }, + meta: { cwd: '/selected', path: '/selected/README.md' }, }, ]) @@ -1454,6 +1456,7 @@ describe('built-in conversation node Definitions', () => { { kind: 'tool-result', callId: 'child', parentCallId: 'history-root', call: { name: 'read' }, error: { name: 'AutoReviewDeniedError', code: 'AUTO_REVIEW_DENIED', reason: 'blocked' }, + meta: { cwd: '/selected', path: '/selected/README.md' }, }, ]) diff --git a/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx b/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx index 14388245a6..78e27beb4b 100644 --- a/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx +++ b/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx @@ -1,5 +1,6 @@ // @vitest-environment jsdom -import { afterEach, describe, expect, it } from 'vitest' +import type { ChainRenderOpts } from '@deepseek-ai/dsh-client-ui-slots' +import { afterEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, fireEvent, render } from '@testing-library/react' import { bindSnapshotSelector, makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' @@ -20,6 +21,18 @@ const t = makeTranslate(zh, commonZh) const renderMessageImages: AssistantMarkdownProps['renderMessageImages'] = () => null describe('ReasoningRow', () => { + it('offers only expanded reasoning to its display extension and preserves original Markdown fallback', () => { + const extension = vi.fn((_slot: string, _owner: object, options?: ChainRenderOpts) => options?.fallback) + const view = render() + expect(extension).not.toHaveBeenCalled() + fireEvent.click(view.getByRole('button')) + expect(extension.mock.calls[0]?.[0]).toBe('conversation.chat.reasoning-body') + expect(extension.mock.calls[0]?.[1]).toEqual({ text: 'Original thought', running: false }) + expect(extension.mock.calls[0]?.[2]?.fallback).toBeDefined() + expect(view.getAllByText('Original thought')).toHaveLength(1) + }) + it.each([ ['', ''], ['An unfinished line', ''], diff --git a/packages/client/ui-commands/package.json b/packages/client/ui-commands/package.json index 25e1a3cc19..ecedb1d9f8 100644 --- a/packages/client/ui-commands/package.json +++ b/packages/client/ui-commands/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-commands", "description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 444a85d6bd..a22be7582c 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -18,8 +18,8 @@ en: 1d2a3a7f98c2cc9b zh: f895dd5e9f91e0a9 /deepseek-ai-dsh-client-ui-conversation/shell-and-standard-props: - en: 8fddd7e838da57d8 - zh: 98748142be2e61d4 + en: 50b5e00a5fb7c2e5 + zh: 91333db3947f395d /deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries: en: bb1244310f5c0382 zh: 824b84077432b559 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index b557262cf4..ec4221995f 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -88,11 +88,11 @@ Plain Enter uses the configured delivery mode, exactly Ctrl+Enter or Cmd+Enter u Each composer attempt carries its original occurrence time, gesture, delivery intent, and Session model/run-state snapshot through asynchronous command arbitration. The ordinary-message notification fires once before reference serialization, including attachment-only submissions; handled or claimed commands do not notify. Snapshot and notification failures cannot block sending. -Default sends commit optimistically: Enter clears the draft, occurrence table, and undo history in the same transaction, keeps the composer in `plain`, and runs the send as a detached attempt, so typing and further sends continue during the flight. `sendSession` registers a Session submission echo (`session.beginSubmission`) with the delivery mode before serializing, preserving selected image and file order in `pendingSubmissions`; Session derives the placement from that mode and its current running state, so idle sends use the transcript, busy Queue sends use QueueDock, and busy Steer sends use the pending-steering surface. It then yields one paint, encodes images through the browser's native `FileReader` data-URL path, and cites staged file receipts. Command submissions use the same receipts for generic files, so sending `/goal` or `/plan` never reads those browser files again. The prompt reuses the submission `requestId`; Session retires the echo once at its display handoff, correlated by that `rpcId`. Concurrent failures are restored together in submission order until the user edits the restored content; command submissions keep the frozen `submitting` phase. Detached attempts retain their attachment ids through admission and Session scope disposal. An observed retirement immediately exposes each image preview through the durable cache, replaces it with the canonical URL after fetching the admitted attachment, revokes each URL after its use ends, and releases file cards. Selected generic files enter one FIFO background-upload queue; `maxConcurrentFileUploads` defaults to two active Worker transports, the Conversation service retains queued and active operations plus byte progress across Session navigation, and removing a draft skips its queued transfer or aborts its active transport. Where the browser shell exposes `__DSH_HOST_PATHS__` (the Desktop application), dropped or pasted folders and dropped, picked, or pasted non-image files with a real path become `@path` chips; images keep uploading. Drag-and-drop and paste identify directories through the browser entry API; when that API is absent or returns no entry, pasted items retain ordinary file handling. The file picker cannot select directories. References require the `ui-reference` plugin and the original paths to remain readable by the model's file tools. Paths inside the workspace are relative; others remain absolute. The whole batch is validated before insertion, retains source order and selected text, and uses whitespace-separated, closed-quote mentions. A browser without the bridge refuses dropped or pasted folders; a Desktop folder without a reported path is rejected separately. Continuable subagents disable attachment intake and skip local echoes because their transport does not preserve the browser request id. +Default sends commit optimistically: Enter clears the draft, occurrence table, and undo history in the same transaction, keeps the composer in `plain`, and runs the send as a detached attempt, so typing and further sends continue during the flight. `sendSession` registers a Session submission echo (`session.beginSubmission`) with the delivery mode before serializing, preserving selected image and file order in `pendingSubmissions`; Session derives the placement from that mode and its current running state, so idle sends use the transcript, busy Queue sends use QueueDock, and busy Steer sends use the pending-steering surface. It then yields one paint, encodes images through the browser's native `FileReader` data-URL path, and cites staged file receipts. Command submissions use the same receipts for generic files, so sending `/goal` or `/plan` never reads those browser files again. The prompt reuses the submission `requestId`; Session retires the echo once at its display handoff, correlated by that `rpcId`. Concurrent failures are restored together in submission order until the user edits the restored content; command submissions keep the frozen `submitting` phase. Detached attempts retain their attachment ids through admission and Session scope disposal. An observed retirement immediately exposes each image preview through the durable cache, replaces it with the canonical URL after fetching the admitted attachment, revokes each URL after its use ends, and releases file cards. Selected generic files enter one FIFO background-upload queue; `maxConcurrentFileUploads` defaults to two active Worker transports, the Conversation service retains queued and active operations plus byte progress across Session navigation, and removing a draft skips its queued transfer or aborts its active transport. Where the browser shell exposes `__DSH_HOST_PATHS__` (the Desktop application), dropped or pasted folders and dropped, picked, or pasted non-image files with a real path become `@path` chips; images keep uploading. Drag-and-drop and paste identify directories through the browser entry API; when that API is absent or returns no entry, pasted items retain ordinary file handling. The file picker cannot select directories. References require the `ui-reference` plugin and the original paths to remain readable by the model's file tools. Paths inside the workspace are relative; others remain absolute. The whole batch is validated before insertion, retains source order and selected text, and uses whitespace-separated, closed-quote mentions. A browser without the bridge refuses dropped or pasted folders; a Desktop folder without a reported path is rejected separately. Continuable subagents accept pasted and dropped PNG, JPEG, WebP, and GIF images with the same projected limits; a batch containing a generic file, unsupported image format, or directory is refused before intake. They skip local echoes because their transport does not preserve the browser request id. Queued submission echoes show “Sending…” beside disabled edit, remove, and steer buttons; a collapsed dock keeps the sending status in its header. A matching Host queue row replaces the echo and enables each action according to its normal text-content and running-state requirements. Prompt acknowledgement alone does not enable queue actions. A failed submission removes its echo and displays an error; the composer restores the failed draft when it is empty or still contains the previous automatic restoration, preserving subsequently typed text. -Disabled Send and Stop buttons suppress their tooltips, including a Stop button that becomes a disabled Send button when the turn ends. While a normal composer is running, its primary pointer action remains Stop when the draft is empty or input is unavailable. Actionable text or attachments switch the same seat to Send; clearing or successfully submitting the draft restores Stop. The busy-Enter setting selects the Queue or Steer delivery for ordinary Sessions and continuable children, and the running Send button delivers through the same mode plain Enter resolves to; while it is enabled (no upload pending) over a plain message draft its label names that mode (Queue message or Steer message), so the setting governs Enter and the button together while Cmd/Ctrl+Enter still uses the other mode, and idle sessions, empty drafts, and `/` command lines keep the plain Send label. Their QueueDock rows share Edit, Remove, and Steer, and an empty draft shares the steer-all chord. One-shot children remain read-only. Plan mode and active goals do not change attachment intake. Continuable children keep separate Send and Stop actions but expose no File row, paste, or drop intake; if their parent is offline, Send and the composer gestures lock while QueueDock controls for the live inbox remain available ([decisions](../../../.agents/notes/archived/bug-fix/2026-08-20-running-draft-primary-send.md), [inbox controls](../../subagent/subagent/README.md)). +Disabled Send and Stop buttons suppress their tooltips, including a Stop button that becomes a disabled Send button when the turn ends. While a normal composer is running, its primary pointer action remains Stop when the draft is empty or input is unavailable. Actionable text or attachments switch the same seat to Send; clearing or successfully submitting the draft restores Stop. The busy-Enter setting selects the Queue or Steer delivery for ordinary Sessions and continuable children, and the running Send button delivers through the same mode plain Enter resolves to; while it is enabled (no upload pending) over a plain message draft its label names that mode (Queue message or Steer message), so the setting governs Enter and the button together while Cmd/Ctrl+Enter still uses the other mode, and idle sessions, empty drafts, and `/` command lines keep the plain Send label. Their QueueDock rows share Edit, Remove, and Steer, and an empty draft shares the steer-all chord. One-shot children remain read-only. Plan mode and active goals do not change attachment intake. Continuable children keep separate Send and Stop actions and accept pasted or dropped images, but expose no File row; if their parent is offline, Send and the composer gestures lock while QueueDock controls for the live inbox remain available ([decisions](../../../.agents/notes/archived/bug-fix/2026-08-20-running-draft-primary-send.md), [inbox controls](../../subagent/subagent/README.md)). File chips and editable skill references share a whole-reference hover background and follow the composer's line height and text baseline. The first click delegates preview opening to the registered reference source immediately, including the first click of a double-click sequence. Subsequent clicks retain native text selection; an existing noncollapsed selection suppresses pointer preview activation. Previewing does not change the draft, its clipboard projection, or submission. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index a441b15f7d..bb0c423a36 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -88,11 +88,11 @@ Session 首次绑定或缓存的 Session 成为 current 时,shell 会在渲染 每次输入框提交都携带原始发生时间、操作来源、投递意图以及会话模型和运行状态快照,贯穿异步命令裁决。普通消息通知在引用序列化前发出一次,纯附件提交也遵循这一规则;已处理或认领的命令不发出通知。快照读取与通知消费异常不能阻断发送。 -默认发送采用乐观提交:Enter 在同一事务里清空草稿、occurrence 表和撤销历史,composer 保持 `plain`,发送作为 detached attempt 运行,发送期间可以继续输入和提交。`sendSession` 在序列化之前用投递模式注册 Session 提交回显(`session.beginSubmission`),并在 `pendingSubmissions` 中保留图片与文件的选择顺序;Session 根据该模式与当前运行状态推导位置,因此空闲发送进入 transcript(文本记录),繁忙时 Queue 进入 QueueDock,繁忙时 Steer 进入 pending-steering 区域。随后让出一帧,图片经浏览器原生 `FileReader` data-URL 路径编码,文件则引用已暂存凭证。命令提交也用同一凭证表示通用文件,因此发送 `/goal` 或 `/plan` 时不会再次读取这些浏览器文件。提示词复用提交 `requestId`;Session 按同一 `rpcId` 关联展示接管,并仅退休回显一次。多个并发发送失败时,在用户编辑还原内容之前按提交顺序合并还原;命令提交保持冻结的 `submitting` 阶段。Detached attempt 持有附件 id,直到 admission 完成或 Session scope 销毁。回显以 observed 退休时,durable 图片缓存立即公开每个预览 URL,读取 admitted 附件后用规范化 URL 替换预览,并在各 URL 停止使用后撤销,同时释放文件卡。选中的通用文件进入同一个先进先出的后台上传队列;`maxConcurrentFileUploads` 默认允许两个 Worker transport 同时运行,Conversation 服务在切换 Session 时继续持有排队和运行中的传输操作及字节进度,移除草稿会跳过排队中的传输或中止正在运行的传输。浏览器 shell 暴露 `__DSH_HOST_PATHS__` 时(桌面应用),拖入或粘贴的文件夹以及拖入、选择或粘贴的带真实路径的非图片文件会成为 `@路径` chip;图片仍然上传。拖放和粘贴通过浏览器 entry API 识别目录;该 API 不可用或没有返回 entry 时,粘贴项沿用普通文件处理。文件选择器不能选择目录。引用需要启用 `ui-reference` 插件,原路径也必须仍可由模型的文件工具读取。工作区内的路径使用相对形式,其他路径保留绝对形式。整批文件先校验再插入,保留来源顺序和已选中的文字,引用之间有空白分隔,含空格的路径使用闭合引号。没有该桥的浏览器会拒绝拖入或粘贴的文件夹,桌面端无法获取文件夹路径时单独报错。continuable 子代理禁用附件入口,也不创建本地回显,因为其 transport 不保留浏览器 request id。 +默认发送采用乐观提交:Enter 在同一事务里清空草稿、occurrence 表和撤销历史,composer 保持 `plain`,发送作为 detached attempt 运行,发送期间可以继续输入和提交。`sendSession` 在序列化之前用投递模式注册 Session 提交回显(`session.beginSubmission`),并在 `pendingSubmissions` 中保留图片与文件的选择顺序;Session 根据该模式与当前运行状态推导位置,因此空闲发送进入 transcript(文本记录),繁忙时 Queue 进入 QueueDock,繁忙时 Steer 进入 pending-steering 区域。随后让出一帧,图片经浏览器原生 `FileReader` data-URL 路径编码,文件则引用已暂存凭证。命令提交也用同一凭证表示通用文件,因此发送 `/goal` 或 `/plan` 时不会再次读取这些浏览器文件。提示词复用提交 `requestId`;Session 按同一 `rpcId` 关联展示接管,并仅退休回显一次。多个并发发送失败时,在用户编辑还原内容之前按提交顺序合并还原;命令提交保持冻结的 `submitting` 阶段。Detached attempt 持有附件 id,直到 admission 完成或 Session scope 销毁。回显以 observed 退休时,durable 图片缓存立即公开每个预览 URL,读取 admitted 附件后用规范化 URL 替换预览,并在各 URL 停止使用后撤销,同时释放文件卡。选中的通用文件进入同一个先进先出的后台上传队列;`maxConcurrentFileUploads` 默认允许两个 Worker transport 同时运行,Conversation 服务在切换 Session 时继续持有排队和运行中的传输操作及字节进度,移除草稿会跳过排队中的传输或中止正在运行的传输。浏览器 shell 暴露 `__DSH_HOST_PATHS__` 时(桌面应用),拖入或粘贴的文件夹以及拖入、选择或粘贴的带真实路径的非图片文件会成为 `@路径` chip;图片仍然上传。拖放和粘贴通过浏览器 entry API 识别目录;该 API 不可用或没有返回 entry 时,粘贴项沿用普通文件处理。文件选择器不能选择目录。引用需要启用 `ui-reference` 插件,原路径也必须仍可由模型的文件工具读取。工作区内的路径使用相对形式,其他路径保留绝对形式。整批文件先校验再插入,保留来源顺序和已选中的文字,引用之间有空白分隔,含空格的路径使用闭合引号。没有该桥的浏览器会拒绝拖入或粘贴的文件夹,桌面端无法获取文件夹路径时单独报错。continuable 子代理允许粘贴和拖入 PNG、JPEG、WebP、GIF 图片,并沿用相同的投影限制;包含通用文件、不支持的图片格式或目录的批次会在进入草稿前整批拒绝。它们不创建本地回显,因为其 transport 不保留浏览器 request id。 排队提交的本地回显在禁用的编辑、删除、插话按钮旁显示“发送中…”;折叠后的队列在标题栏保留发送状态。匹配的 Host 队列行替换回显后,各操作按原有的纯文本内容和运行状态要求启用。仅收到提示词确认不会启用队列操作。提交失败会移除回显并显示错误;输入框为空或仍保留上一次自动恢复的内容时,composer 恢复失败草稿,保留用户随后输入的文字。 -Send 和 Stop 按钮禁用时不显示提示气泡,轮次结束后由 Stop 切换成禁用 Send 的按钮也遵循此规则。普通 composer 运行时,如果草稿为空或输入不可用,主指针操作保持为 Stop。可提交的文字或附件会把同一位置切换为 Send;清空或成功提交草稿后恢复 Stop。繁忙态 Enter 设置为普通 Session 与可继续 child 选择 Queue 或 Steer 投递,运行中的 Send 按钮按 plain Enter 解析出的同一模式投递;当它在普通消息草稿上可用(没有待上传文件)时,其标签以该模式命名(排队发送或插话发送),因此该设置同时约束 Enter 与按钮,而 Cmd/Ctrl+Enter 仍使用另一模式;空闲会话、空草稿与 `/` 命令行保留普通的 Send 标签。它们的 QueueDock 行共享 Edit、Remove 与 Steer,空草稿也共享 steer-all 组合键。One-shot child 继续只读。Plan Mode 与 active goal 不改变附件入口。可继续 child 保留独立的 Send 与 Stop 操作,但不提供「文件」菜单项、粘贴或拖放入口;parent 离线时,Send 与 composer 手势锁定,但在线 inbox 的 QueueDock 控制仍可使用([决策](../../../.agents/notes/archived/bug-fix/2026-08-20-running-draft-primary-send.md)、[inbox 控制](../../subagent/subagent/README.zh.md))。 +Send 和 Stop 按钮禁用时不显示提示气泡,轮次结束后由 Stop 切换成禁用 Send 的按钮也遵循此规则。普通 composer 运行时,如果草稿为空或输入不可用,主指针操作保持为 Stop。可提交的文字或附件会把同一位置切换为 Send;清空或成功提交草稿后恢复 Stop。繁忙态 Enter 设置为普通 Session 与可继续 child 选择 Queue 或 Steer 投递,运行中的 Send 按钮按 plain Enter 解析出的同一模式投递;当它在普通消息草稿上可用(没有待上传文件)时,其标签以该模式命名(排队发送或插话发送),因此该设置同时约束 Enter 与按钮,而 Cmd/Ctrl+Enter 仍使用另一模式;空闲会话、空草稿与 `/` 命令行保留普通的 Send 标签。它们的 QueueDock 行共享 Edit、Remove 与 Steer,空草稿也共享 steer-all 组合键。One-shot child 继续只读。Plan Mode 与 active goal 不改变附件入口。可继续 child 保留独立的 Send 与 Stop 操作,允许粘贴或拖入图片,但不提供「文件」菜单项;parent 离线时,Send 与 composer 手势锁定,但在线 inbox 的 QueueDock 控制仍可使用([决策](../../../.agents/notes/archived/bug-fix/2026-08-20-running-draft-primary-send.md)、[inbox 控制](../../subagent/subagent/README.zh.md))。 文件标签和可编辑的 skill 引用共用覆盖整个引用的悬停背景,并跟随输入框的行高与文字基线。首次点击立即由已注册的引用来源负责打开预览,包括双击序列的第一次点击。后续点击保留原生文本选择行为;已有非折叠选区时,指针点击不打开预览。预览不改变草稿、剪贴板文本或提交内容。 diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index 73b7c388bb..af91420381 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-conversation", "description": "Target-neutral Conversation assembly, shell, composer, queue, and view navigation", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-conversation/src/client/input/editor/view-binding.ts b/packages/client/ui-conversation/src/client/input/editor/view-binding.ts index f42b3a42be..dacb1796b8 100644 --- a/packages/client/ui-conversation/src/client/input/editor/view-binding.ts +++ b/packages/client/ui-conversation/src/client/input/editor/view-binding.ts @@ -91,7 +91,7 @@ export function installDraftFilePicker( fileInputRef: RefObject, ): () => void { return keyboard.bindFilePicker({ - available: () => gate.current.canAcceptDrop && fileInputRef.current !== null, + available: () => gate.current.canAcceptDrop && fileInputRef.current !== null && !fileInputRef.current.disabled, open: () => { fileInputRef.current?.click() }, }) } diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index 5f978ed0c8..4703ae3440 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -308,6 +308,7 @@ export const zh = { 'row.preparing': '正在准备调用', 'row.failed': '失败', 'row.stopped': '已停止', + 'row.commandArguments': '命令参数', 'row.input': '输入', 'row.output': '输出', 'row.inspect': '查看', @@ -369,6 +370,7 @@ export const zh = { 'terminal.signal': '信号 {signal}', 'terminal.exitCode': '退出码 {code}', 'terminal.noExitCode': '未正常退出', + 'terminal.commandLine': '命令第 {n} 行', 'terminal.running': '运行中', 'terminal.failed': '失败', 'terminal.done': '已完成', @@ -684,6 +686,7 @@ export const en = { 'row.preparing': 'Preparing tool call', 'row.failed': 'Failed', 'row.stopped': 'Stopped', + 'row.commandArguments': 'Command arguments', 'row.input': 'IN', 'row.output': 'OUT', 'row.inspect': 'Inspect', @@ -745,6 +748,7 @@ export const en = { 'terminal.signal': 'signal {signal}', 'terminal.exitCode': 'exit code {code}', 'terminal.noExitCode': 'no exit code', + 'terminal.commandLine': 'Command line {n}', 'terminal.running': 'Running', 'terminal.failed': 'Failed', 'terminal.done': 'Done', diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx index 031413132b..bbf780f940 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx @@ -36,6 +36,7 @@ import { } from '../input/editor/view-binding.ts' import { resolveSubmitMode } from '../input/submission-policy.ts' import { attachmentErrorText, imageSizeText } from '../image-labels.ts' +import { isImageMediaType } from '../service.ts' import { ContextMeter } from './ContextMeter.tsx' import { observeControlRow } from './control-row-layout.ts' import css from './InputBar.module.css' @@ -209,8 +210,13 @@ export const InputBar = memo(function InputBar({ // The host enforces the same image limits at submit for callers that bypass // this composer. const intakeFiles = useCallback((files: readonly File[], directories?: ReadonlySet): void => { - if (subagent !== null || addFiles === undefined || files.length === 0) return + if (locked || machineBusy || (subagent !== null && !continuable) || addFiles === undefined || files.length === 0) return const rejected = ((): string | null => { + // Child prompts support images, but not generic file receipts or the + // Desktop path-reference fallback. Refuse mixed batches before intake. + if (continuable && files.some(file => directories?.has(file) || !isImageMediaType(file.type))) { + return t('image.unsupportedType') + } if (imageLimits !== undefined) { const mediaTypes = imageLimits.mediaTypes as readonly string[] const images = files.filter(file => mediaTypes.includes(file.type)) @@ -230,9 +236,9 @@ export const InputBar = memo(function InputBar({ return addFiles(files, directories) })() if (rejected !== null) showToast(rejected) - }, [subagent, addFiles, attachments, imageLimits, showToast, t]) + }, [locked, machineBusy, subagent, continuable, addFiles, attachments, imageLimits, showToast, t]) - const canAcceptDrop = subagent === null && !locked && !machineBusy && addFiles !== undefined + const canAcceptDrop = (subagent === null || continuable) && !locked && !machineBusy && addFiles !== undefined const fileInputRef = useRef(null) const onPickFiles = (e: ChangeEvent): void => { diff --git a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx index 3b025155eb..7ba82035d5 100644 --- a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx +++ b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx @@ -49,6 +49,10 @@ Range.prototype.getBoundingClientRect = ZERO_RECT const SCTX = {} as Context const SID = 's1' as SessionId +const CONTINUABLE_CHILD = { + address: { parentSessionId: 'parent' as SessionId, childSessionId: SID, mode: 'continuable' as const }, + parentAvailable: true, +} function snapshotOf(overrides: Partial = {}): SessionSnapshot { return { ...sessionFixture(SID), ...overrides } @@ -339,6 +343,54 @@ describe('composer placeholder visibility', () => { }) describe('image draft rail', () => { + it.each([false, true])('accepts pasted and dropped images for a continuable child (running=%s)', async (running) => { + const addFiles = vi.fn(() => null) + const { textarea, shell, slotCalls } = bench({ subagent: CONTINUABLE_CHILD, running, addFiles }) + const image = new File([Uint8Array.of(1, 2, 3)], 'pixel.png', { type: 'image/png' }) + fireEvent.paste(textarea, { + clipboardData: { + items: [{ kind: 'file', type: image.type, getAsFile: () => image }], + getData: () => '一起发送的文字', + }, + }) + expect(addFiles).toHaveBeenCalledWith([image], undefined) + await vi.waitFor(() => { expect(shell.snapshot.draft).toBe('一起发送的文字') }) + expect(attachmentOwner(slotCalls).canAcceptDrop).toBe(true) + act(() => { attachmentOwner(slotCalls).onAddFiles([image]) }) + expect(addFiles).toHaveBeenCalledTimes(2) + // This fix restores paste/drop independently of the menu's picker policy. + expect(shell.canPickFiles()).toBe(false) + }) + + it.each([ + ['a generic file', new File(['text'], 'notes.txt', { type: 'text/plain' }), false], + ['an unsupported image', new File([''], 'drawing.svg', { type: 'image/svg+xml' }), false], + ['a directory', new File([], 'folder.png', { type: 'image/png' }), true], + ])('refuses the entire child attachment batch containing %s', (_name, unsupported, directory) => { + const addFiles = vi.fn(() => null) + const { slotCalls, view, shell } = bench({ subagent: CONTINUABLE_CHILD, addFiles, draft: '保留草稿' }) + const image = new File([Uint8Array.of(1)], 'pixel.png', { type: 'image/png' }) + act(() => { + attachmentOwner(slotCalls).onAddFiles([image, unsupported], directory ? new Set([unsupported]) : undefined) + }) + expect(addFiles).not.toHaveBeenCalled() + expect(view.getByRole('alert').textContent).toContain('仅支持 PNG、JPG、WebP、GIF 格式的图片') + expect(shell.snapshot.draft).toBe('保留草稿') + }) + + it.each([ + ['an offline parent', { ...CONTINUABLE_CHILD, parentAvailable: false }], + ['a one-shot child', { ...CONTINUABLE_CHILD, address: { ...CONTINUABLE_CHILD.address, mode: 'one-shot' as const } }], + ])('keeps attachment intake closed for %s', (_name, subagent) => { + const addFiles = vi.fn(() => null) + const { slotCalls } = bench({ subagent, addFiles }) + expect(attachmentOwner(slotCalls).canAcceptDrop).toBe(false) + act(() => { + attachmentOwner(slotCalls).onAddFiles([new File([Uint8Array.of(1)], 'pixel.png', { type: 'image/png' })]) + }) + expect(addFiles).not.toHaveBeenCalled() + }) + it('collects clipboard files while preserving text from a mixed paste', async () => { const addFiles = vi.fn(() => null) const { textarea, shell } = bench({ addFiles }) @@ -380,7 +432,10 @@ describe('image draft rail', () => { expect(addFiles).toHaveBeenCalledWith([folder, emptyFile, withoutApi, withoutEntry], new Set([folder])) }) - it('pre-checks projected limits at intake: whole-batch refusal with product copy, none added', () => { + it.each([ + ['ordinary session', {}], + ['continuable child', { subagent: CONTINUABLE_CHILD }], + ])('pre-checks projected image limits for %s: whole-batch refusal with product copy', (_name, session) => { const limits = { maxImageBytes: 1024 * 1024, maxImagesPerMessage: 2, @@ -394,13 +449,13 @@ describe('image draft rail', () => { act(() => { attachmentOwner(result.slotCalls).onAddFiles(files) }) } // Count: three at once over a two-image limit → the whole batch refused. - const overCount = bench({ addFiles: vi.fn(() => null), imageLimits: limits }) + const overCount = bench({ ...session, addFiles: vi.fn(() => null), imageLimits: limits }) intake(overCount, [png(8, 'a.png'), png(8, 'b.png'), png(8, 'c.png')]) expect(overCount.view.getByRole('alert').textContent).toContain('一条消息最多添加 2 张图片') expect(overCount.props.addFiles).not.toHaveBeenCalled() cleanup() // Per-file bytes. - const overFile = bench({ addFiles: vi.fn(() => null), imageLimits: limits }) + const overFile = bench({ ...session, addFiles: vi.fn(() => null), imageLimits: limits }) intake(overFile, [png(1024 * 1024 + 1, 'big.png')]) expect(overFile.view.getByRole('alert').textContent).toContain('单张图片不能超过 1MB') expect(overFile.props.addFiles).not.toHaveBeenCalled() @@ -408,13 +463,13 @@ describe('image draft rail', () => { // Aggregate bytes across the existing rail plus the new batch. const held = new File([new ArrayBuffer(1024 * 1024 * 1.5)], 'held.png', { type: 'image/png' }) const attachment = { kind: 'image' as const, id: 'draft-1' as DraftAttachmentId, file: held, previewUrl: 'blob:held' } - const overTotal = bench({ addFiles: vi.fn(() => null), imageLimits: limits, attachments: [attachment] }) + const overTotal = bench({ ...session, addFiles: vi.fn(() => null), imageLimits: limits, attachments: [attachment] }) intake(overTotal, [png(1024 * 1024, 'more.png')]) expect(overTotal.view.getByRole('alert').textContent).toContain('图片总大小超过 2MB') expect(overTotal.props.addFiles).not.toHaveBeenCalled() cleanup() // Within every limit: the batch passes through to addFiles. - const within = bench({ addFiles: vi.fn(() => null), imageLimits: limits }) + const within = bench({ ...session, addFiles: vi.fn(() => null), imageLimits: limits }) const fits = png(16, 'fits.png') intake(within, [fits]) expect(within.props.addFiles).toHaveBeenCalledWith([fits], undefined) @@ -1033,7 +1088,7 @@ describe('running and lock semantics', () => { expect(interruptButton).not.toBeNull() expect(textarea.getAttribute('aria-disabled')).not.toBe('true') expect(view.container.querySelector('input[type="file"]')?.disabled).toBe(true) - expect(attachmentOwner(slotCalls).canAcceptDrop).toBe(false) + expect(attachmentOwner(slotCalls).canAcceptDrop).toBe(true) const click = vi.spyOn(HTMLInputElement.prototype, 'click') onTestFinished(() => { click.mockRestore() }) expect(shell.canPickFiles()).toBe(false) diff --git a/packages/client/ui-deliverables/README.i18n.yaml b/packages/client/ui-deliverables/README.i18n.yaml index 61e5ffccb7..1c80fb0114 100644 --- a/packages/client/ui-deliverables/README.i18n.yaml +++ b/packages/client/ui-deliverables/README.i18n.yaml @@ -27,11 +27,11 @@ en: 231547659321f3a8 zh: bc5935cdd46ada56 /deepseek-ai-dsh-client-ui-deliverables/use-this-package/inline-code-links: - en: e36ccef8bf908ea4 - zh: 30e11327a55e5dfa + en: ef13aac8ac36a5c3 + zh: 1e2ae4b3eaf98818 /deepseek-ai-dsh-client-ui-deliverables/understand-the-implementation: - en: 9c48cf102ec5109e - zh: 73cabaafdb3ded3f + en: 13e06395a2c447bf + zh: c2c29bbfbfabe756 /deepseek-ai-dsh-client-ui-deliverables/further-exploration: en: 476b59e8671db982 zh: b3d0495a03e8940a @@ -51,8 +51,8 @@ en: 52626a05acc8b320 zh: 6a8e62b0d47d5297 /deepseek-ai-dsh-client-ui-deliverables/known-limitations-and-deferred-work: - en: 3b59e3b776c52ba4 - zh: ceee2728a0e7779b + en: dcebaf6fd7885452 + zh: 017ef058d4dcdc25 /deepseek-ai-dsh-client-ui-deliverables/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-deliverables/README.md b/packages/client/ui-deliverables/README.md index 401c565955..21d0146b84 100644 --- a/packages/client/ui-deliverables/README.md +++ b/packages/client/ui-deliverables/README.md @@ -54,7 +54,7 @@ Without wrapping, the two columns synchronize vertical scrolling and horizontal ### Inline-code links -The closing prose links produced or delivered paths: an inline-code token resolves by exact path, or by being exactly the basename of exactly one such path — a basename two paths share stays inert rather than guessing, so a mention can never open the wrong file. A resolved mention keeps its code chip and takes the markdown sheet's link language, with the full path as its title. +The closing prose links produced or delivered paths: an inline-code token resolves by exact target, by an exact argument recorded for that target, or by a unique basename. Every matching fact must identify the same file; ambiguous arguments and basenames stay inert. No suffix or current-directory guess is used. A resolved mention keeps its code chip and takes the markdown sheet's link language, with the full path as its title. ----- @@ -64,7 +64,7 @@ The closing prose links produced or delivered paths: an inline-code token resolv
Implementation internals — click to expand -The Node half registers the static `ui:deliverable-file-references` system-prompt section described under [Model Experience](#model-experience). Explicit Markdown links use the shared [Markdown renderer](../ui-primitives/README.md); inline-code matching remains limited to produced or delivered files. The browser half registers a wrapper around the changed-files card and explicit deliveries into the chat view's `conversation.chat.turnTail` list alongside other feature artifacts. `deliverablesDefinition` folds the sequence of each Turn's latest validated `workspace/changes` announcement into `DeliverablesTurnData.changes`, whose summary the card reads from the Host and caches until the connection is replaced, its `deliverables/presented` events into deliveries, and the successful first-party mutation calls of `write`, `edit`, and mutating `str_replace_editor` commands into produced paths from their validated raw arguments; the produced paths feed only the prose mention resolver. Reads, deletes, unsupported tools, malformed calls, malformed events, and failed results contribute nothing. Each row opens `dsh-resource://changes-review/session///` through `ctx.sidebarRight.openResource` with the file's index as the `changes-review` navigation parameter; the package registers the `changes-review` tab type at the `builtin` band for that pattern and its body under the keyed `sidebar.right.pane.tab` seat with an exclusive store of per-tab choices, and the body reads the summary and the comparisons through the authenticated routes into stores cleared on connection replacement. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes every surface and leaves the view's empty list at zero cost. +The Node half registers the static `ui:deliverable-file-references` system-prompt section described under [Model Experience](#model-experience). Explicit Markdown links use the shared [Markdown renderer](../ui-primitives/README.md); inline-code matching remains limited to produced or delivered files. The browser half registers a wrapper around the changed-files card and explicit deliveries into the chat view's `conversation.chat.turnTail` list alongside other feature artifacts. `deliverablesDefinition` folds the sequence of each Turn's latest validated `workspace/changes` announcement into `DeliverablesTurnData.changes`, whose summary the card reads from the Host and caches until the connection is replaced, its `deliverables/presented` events into deliveries, and the successful first-party mutation calls of `write`, `edit`, and mutating `str_replace_editor` commands into recorded file targets and exact argument aliases after validating their raw arguments; the produced paths feed only the prose mention resolver. Successful absolute `meta.path` values identify those targets. Root results without that field retain the legacy argument fallback; malformed relative targets contribute no guessed path. Reads, deletes, unsupported tools, malformed calls, malformed events, and failed results contribute nothing. Each row opens `dsh-resource://changes-review/session///` through `ctx.sidebarRight.openResource` with the file's index as the `changes-review` navigation parameter; the package registers the `changes-review` tab type at the `builtin` band for that pattern and its body under the keyed `sidebar.right.pane.tab` seat with an exclusive store of per-tab choices, and the body reads the summary and the comparisons through the authenticated routes into stores cleared on connection replacement. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes every surface and leaves the view's empty list at zero cost. Native opening uses an authenticated POST addressed by the viewed Session, event sequence, and original file index; the review tab's native open of a changed file uses the same coordinates. For a declaration the Host reads the viewed Session header with the event and passes its cwd, or the deployment workspace root when absent, to `workspaceFiles.stat`; for a changed file it passes the working directory the served summary carries. This uses the same composed filesystem as Sidebar previews and does not activate an Agent, including for child Sessions. Native actions require the canonical process path to map from a Host path back to that same process path. Providers without this mapping return 422, after which the review tab hides its native open; a same-named Host file is insufficient. The same configured desktop availability governs metadata and execution. Edits affect subsequent opens; deletion returns an error. No file-content copy or attachment is created. Plugin disposal cancels and awaits pending native-open requests. @@ -111,7 +111,7 @@ The section is static at first-party order 9000 for the lifetime of the package These limits define the current deliverables vocabulary. They are current package constraints, not a general file-linking comparison or a task backlog. - **Inline local images require an HTTP(S) page**: Desktop’s `dsh-app:` pages do not support this file route. POSIX absolute paths and Windows drive-letter paths are supported; relative Chat image paths require the viewed Session’s workspace root. Separate image file links are optional. -- **Mention matching is exact path or unique basename only** — a suffix mention stays inert; widening the matcher is deferred until a real closing-message shape needs it. +- **Mention matching uses exact paths, unique argument aliases, or unique basenames only** — a suffix mention stays inert; widening the matcher is deferred until a real closing-message shape needs it. - **Terminal-created files require explicit delivery** — the card lists them once git records the change, but delivery cards and inline-code references require `present`; explicit Markdown links can reference existing files directly. - **Declarations do not preserve file contents** — reopening or transferring a Session requires source files accessible through the viewed Session’s filesystem. Missing files, directories, and final symbolic links return 404. - **Native opening needs a Host desktop** — without one the review tab offers no native open; the comparison itself needs only the Host that recorded the turn. diff --git a/packages/client/ui-deliverables/README.zh.md b/packages/client/ui-deliverables/README.zh.md index dae6c3c92e..26e18037e1 100644 --- a/packages/client/ui-deliverables/README.zh.md +++ b/packages/client/ui-deliverables/README.zh.md @@ -54,7 +54,7 @@ Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present`,用于声明 ### 行内代码链接 -收尾正文链接产出或已交付的路径:行内代码 token 按精确路径解析,或当它恰好等于其中某条路径的 basename 且该路径唯一时解析——两条路径共享同一 basename 时保持不可点击而不作猜测,因此提及绝不打开错误的文件。解析成功的提及保留代码标签,并采用 Markdown 样式表的链接样式,完整路径作为其 `title`。 +收尾正文链接产出或已交付的路径:行内代码 token 可按精确目标路径、为该目标记录的精确参数,或唯一 basename 解析。所有匹配事实必须指向同一文件;有歧义的参数与 basename 保持不可点击。不使用后缀或当前目录猜测。解析成功的提及保留代码标签,并采用 Markdown 样式表的链接样式,完整路径作为其 `title`。 ----- @@ -64,7 +64,7 @@ Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present`,用于声明
实现细节——点击展开 -Node 半部注册[模型体验](#model-experience)所述的静态 `ui:deliverable-file-references` 系统提示词段。显式 Markdown 链接使用共享的 [Markdown 渲染器](../ui-primitives/README.zh.md);行内代码匹配仍仅限产出或交付文件。浏览器半部把组合改动文件卡片与显式交付的包装组件注册进 chat 视图的 `conversation.chat.turnTail` 列表,与其他功能产物并列。`deliverablesDefinition` 把每个轮次最新且通过校验的 `workspace/changes` 宣告的序号折叠进 `DeliverablesTurnData.changes`,卡片按它向 Host 读取摘要并缓存到连接被替换为止,把 `deliverables/presented` 事件折叠为交付,并根据 `write`、`edit` 和有修改作用的 `str_replace_editor` 命令中经过校验的原始参数把成功的第一方修改调用折叠为产出路径;产出路径只供正文提及解析器使用。读取、删除、不受支持的工具、格式错误的调用、格式错误的事件和失败结果不贡献任何条目。每一行通过 `ctx.sidebarRight.openResource` 打开 `dsh-resource://changes-review/session///`,并以文件下标作为 `changes-review` 的导航参数;本包在 `builtin` 档为该模式注册 `changes-review` tab 类型,把其 body 连同一个按 tab 保存选择的独占 store 注册到按键的 `sidebar.right.pane.tab` 座位下,body 通过经过认证的路由把摘要和对比读进连接更换时清空的 store。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会移除全部表面,视图的空列表以零成本留下。 +Node 半部注册[模型体验](#model-experience)所述的静态 `ui:deliverable-file-references` 系统提示词段。显式 Markdown 链接使用共享的 [Markdown 渲染器](../ui-primitives/README.zh.md);行内代码匹配仍仅限产出或交付文件。浏览器半部把组合改动文件卡片与显式交付的包装组件注册进 chat 视图的 `conversation.chat.turnTail` 列表,与其他功能产物并列。`deliverablesDefinition` 把每个轮次最新且通过校验的 `workspace/changes` 宣告的序号折叠进 `DeliverablesTurnData.changes`,卡片按它向 Host 读取摘要并缓存到连接被替换为止,把 `deliverables/presented` 事件折叠为交付,并根据 `write`、`edit` 和有修改作用的 `str_replace_editor` 命令中经过校验的原始参数与结果记录的绝对 `meta.path` 把成功的第一方修改调用折叠为产出路径及精确参数别名;产出路径只供正文提及解析器使用。不带该字段的成功根调用保留历史参数回退;格式错误的相对目标不产生猜测路径。读取、删除、不受支持的工具、格式错误的调用、格式错误的事件和失败结果不贡献任何条目。每一行通过 `ctx.sidebarRight.openResource` 打开 `dsh-resource://changes-review/session///`,并以文件下标作为 `changes-review` 的导航参数;本包在 `builtin` 档为该模式注册 `changes-review` tab 类型,把其 body 连同一个按 tab 保存选择的独占 store 注册到按键的 `sidebar.right.pane.tab` 座位下,body 通过经过认证的路由把摘要和对比读进连接更换时清空的 store。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会移除全部表面,视图的空列表以零成本留下。 原生打开使用经过认证的 POST,通过当前查看的会话、事件序号和原始文件索引定位声明;review tab 对改动文件的原生打开使用同一组坐标。对声明,Host 读取事件及当前查看的会话 header,将其中的 cwd 传给 `workspaceFiles.stat`,未记录 cwd 时使用部署的工作目录;对改动文件,传的是所提供摘要携带的工作目录。它与侧栏预览使用同一组合文件系统,无需启动 Agent,子会话也适用。原生操作要求规范化的进程路径能从 Host 路径映射回同一进程路径。提供方没有这种映射时返回 422,之后 review tab 隐藏原生打开;Host 上存在同名文件并不足够。同一份桌面可用性配置同时约束信息查询和实际执行。编辑会影响后续打开的内容;删除后返回错误。不创建文件内容副本或附件。插件释放时取消并等待进行中的原生打开请求。 @@ -111,7 +111,7 @@ Node 半部注册[模型体验](#model-experience)所述的静态 `ui:deliverabl 这些限制界定了当前产出物词表。它们是当前包约束,不是通用文件链接对比或任务积压。 - **本地行内图片要求 HTTP(S) 页面**:Desktop 的 `dsh-app:` 页面不支持此文件路由。支持 POSIX 绝对路径与 Windows 盘符路径;Chat 中的相对图片路径需要当前查看的 Session 工作区根目录。图片的独立文件链接是可选的。 -- **提及匹配只认精确路径或唯一 basename**——后缀式提及保持惰性;等真实的收尾消息形态产生需求后再放宽匹配规则。 +- **提及匹配只认精确路径、唯一参数别名或唯一 basename**——后缀式提及保持惰性;等真实的收尾消息形态产生需求后再放宽匹配规则。 - **终端创建的文件需要显式交付**——git 记录到改动后卡片会列出它们,但交付卡片和行内代码引用需要调用 `present`;显式 Markdown 链接可以直接引用现有文件。 - **声明不保存文件内容**:重新打开或转移 Session 后,源文件仍需能被当前查看的 Session 文件系统访问。文件缺失、为目录或最终路径为符号链接时返回 404。 - **原生打开需要 Host 桌面**——没有桌面时 review tab 不提供原生打开;对比本身只需要记录了该轮的 Host。 diff --git a/packages/client/ui-deliverables/package.json b/packages/client/ui-deliverables/package.json index 4e732fb9c2..174fcf3f72 100644 --- a/packages/client/ui-deliverables/package.json +++ b/packages/client/ui-deliverables/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-deliverables", "description": "Changed-files card with per-file comparison tabs, delivery cards, and clickable final-response file references for Web", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-deliverables/src/client/index.ts b/packages/client/ui-deliverables/src/client/index.ts index 5e24855305..bbc165e99c 100644 --- a/packages/client/ui-deliverables/src/client/index.ts +++ b/packages/client/ui-deliverables/src/client/index.ts @@ -106,7 +106,8 @@ export function apply(ctx: ClientContext): void { const presented = presentedForClosing(owner) if (paths === null && presented.length === 0) return undefined return producedFileMentions([...new Set([...paths ?? [], ...presented.map(file => file.path)])], owner.openFile, - path => t('presented.previewButton', { name: path })) + path => t('presented.previewButton', { name: path }), + owner.turn.data.get('deliverables')?.produced.filter(operation => operation.seq <= owner.seq)) }, } ctx.provide('chatFileMentions', mentions) diff --git a/packages/client/ui-deliverables/src/client/turn-deliverables.ts b/packages/client/ui-deliverables/src/client/turn-deliverables.ts index 7eb765ec2b..fb2f962b84 100644 --- a/packages/client/ui-deliverables/src/client/turn-deliverables.ts +++ b/packages/client/ui-deliverables/src/client/turn-deliverables.ts @@ -1,9 +1,11 @@ /** * Turn-scoped produced-file Definition and readers. Client-only and - * model-free: produced paths come from successful first-party mutation calls, - * changed files from the Host's recorded git summary, and deliveries from - * `present`; never from presentation data or the closing prose. + * model-free: produced paths come from successful first-party mutation calls and + * their recorded file targets, changed files from the Host's recorded git + * summary, and deliveries from `present`; never from presentation data or + * the closing prose. */ +import { isAbsoluteWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path' import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-session/surface' import type { TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client' import type { ConversationNodeDefinition } from '@deepseek-ai/dsh-client-ui-conversation/client' @@ -21,6 +23,7 @@ export interface PresentedPath extends PresentedFile { interface ProducedPath { readonly seq: number readonly path: string + readonly argumentPath?: string } /** The latest `workspace/changes` announcement of one Turn; the Host serves its summary by this sequence. */ @@ -114,6 +117,13 @@ function pathValue(value: unknown): string | null { return typeof value === 'string' && value.trim().length > 0 ? value : null } +/** Resolve a settled mutation from recorded metadata, retaining legacy argument paths. */ +function mutationResultPath(argumentPath: string, meta: unknown): string | null { + if (!isRecord(meta) || !Object.hasOwn(meta, 'path')) return argumentPath + if (typeof meta.path === 'string' && isAbsoluteWorkspacePath(meta.path)) return meta.path + return isAbsoluteWorkspacePath(argumentPath) ? argumentPath : null +} + /** Narrow parsed JSON to an argument object. */ function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) @@ -122,8 +132,8 @@ function isRecord(value: unknown): value is Record { /** * Files produced by one Turn data value. * - * The source is the arguments of successful `write`, `edit`, and mutating - * `str_replace_editor` calls, not the closing prose: a produced file must be + * The source is successful `write`, `edit`, and mutating `str_replace_editor` + * calls and their recorded absolute targets; legacy results use arguments. A produced file must be * listed whether or not the model remembered to name it. Reads, unsupported * tools, malformed calls, and failed results contribute nothing. Paths keep * first-seen order and appear once, so a file written and then edited in the @@ -202,10 +212,17 @@ export const deliverablesDefinition: ConversationNodeDefinition { if (scope !== 'turn' || context.state === undefined) return null @@ -254,11 +271,12 @@ export { basename } from '../presented.ts' /** * Resolves inline-code references against one turn's produced or delivered - * paths. Exact paths resolve directly; a basename resolves only when exactly - * one supplied path has that basename. Ambiguous and unknown tokens stay inert. + * paths and explicit mutation arguments. A reference resolves only when every + * exact or basename match identifies one target. Ambiguous and unknown tokens stay inert. * @param paths - The turn's produced or delivered paths, already deduplicated. * @param openFile - The chat view's file opener. * @param label - Localizes the accessible open-label for a resolved path. + * @param operations - successful mutations whose literal arguments may name the same canonical targets. * @returns The resolver MarkdownText consumes; the full path rides `title`, * the same disambiguator the row's chips carry. */ @@ -266,18 +284,17 @@ export function producedFileMentions( paths: readonly string[], openFile: (path: string) => void, label: (path: string) => string, + operations: readonly Pick[] = [], ): MarkdownFileMentions { return { resolve(value) { - const path = paths.includes(value) ? value : onlyPathWithBasename(paths, value) + const matches = new Set(paths.filter(path => path === value || basename(path) === value)) + for (const operation of operations) { + if (operation.argumentPath === value) matches.add(operation.path) + } + const path = matches.size === 1 ? matches.values().next().value : undefined if (path === undefined) return undefined return { open: () => { openFile(path) }, label: label(path), title: path } }, } } - -/** The single supplied path whose basename is exactly `value`, else undefined. */ -function onlyPathWithBasename(paths: readonly string[], value: string): string | undefined { - const matches = paths.filter(path => basename(path) === value) - return matches.length === 1 ? matches[0] : undefined -} diff --git a/packages/client/ui-deliverables/tests/deliverables.client.spec.tsx b/packages/client/ui-deliverables/tests/deliverables.client.spec.tsx index 09dfa6a74c..be0c8fbeeb 100644 --- a/packages/client/ui-deliverables/tests/deliverables.client.spec.tsx +++ b/packages/client/ui-deliverables/tests/deliverables.client.spec.tsx @@ -187,8 +187,9 @@ function rawCall( ) } -function result(seq: number, callId: string, isError = false, turn = 1): SessionLiveEventEntry { +function result(seq: number, callId: string, isError = false, turn = 1, meta?: unknown): SessionLiveEventEntry { return at(seq, 'tool/result', { + ...meta === undefined ? {} : { meta }, turn, step: 1, message: { @@ -228,6 +229,65 @@ describe('produced-file Turn data', () => { expect(selectProducedFiles(tailOwner(undefined, 9, () => {}, 2))).toBeNull() }) + it('opens a moved write at its recorded target when an absolute delivery names the same file', () => { + const path = '/project/child/report.txt' + const value = assembler([ + at(1, 'turn/start', { turn: 1 }), + call(2, 'write', 'write', { file_path: 'report.txt', content: 'CHILD FILE' }), + result(3, 'write', false, 1, { path, diffs: [] }), + at(4, 'deliverables/presented', { turn: 1, callId: 'present', files: [{ path }] }), + ]) + const data = deliverablesOf(value) + const paths = producedForClosing(data) + expect(paths).toEqual([path]) + const delivered = presentedForClosing(tailOwner(data, 5)) + const opened = vi.fn() + const mentions = producedFileMentions([...new Set([...paths, ...delivered.map(file => file.path)])], opened, target => target) + mentions.resolve('report.txt')?.open() + expect(opened).toHaveBeenCalledWith(path) + expect(mentions.resolve('report.txt')?.title).toBe(path) + }) + + it('deduplicates canonical mutation targets and keeps same-basename files distinct', () => { + const value = assembler([ + at(1, 'turn/start', { turn: 1 }), + call(2, 'first', 'write', { file_path: 'report.txt', content: 'first' }), + result(3, 'first', false, 1, { path: '/project/a/report.txt' }), + call(4, 'alias', 'edit', { file_path: './report.txt', old_string: 'first', new_string: 'updated' }), + result(5, 'alias', false, 1, { path: '/project/a/report.txt' }), + call(6, 'second', 'write', { file_path: 'report.txt', content: 'second' }), + result(7, 'second', false, 1, { path: '/project/b/report.txt' }), + ]) + const paths = producedForClosing(deliverablesOf(value)) + expect(paths).toEqual(['/project/a/report.txt', '/project/b/report.txt']) + const mentions = producedFileMentions(paths, () => {}, target => target) + expect(mentions.resolve('report.txt')).toBeUndefined() + expect(mentions.resolve('/project/b/report.txt')?.title).toBe('/project/b/report.txt') + }) + + it.each([{ path: 'unknown/report.txt' }, { path: null }, { path: 42 }])('keeps a relative target inert when its recorded path is invalid: %j', (meta) => { + const value = assembler([ + at(1, 'turn/start', { turn: 1 }), + call(2, 'unknown', 'write', { file_path: 'report.txt', content: 'new' }), + result(3, 'unknown', false, 1, meta), + ]) + expect(producedForClosing(deliverablesOf(value))).toEqual([]) + }) + + it('opens an explicit absolute mutation target when its recorded path is unusable', () => { + const path = '/project/child/report.txt' + const value = assembler([ + at(1, 'turn/start', { turn: 1 }), + call(2, 'absolute', 'write', { file_path: path, content: 'new' }), + result(3, 'absolute', false, 1, { path: null }), + ]) + const paths = producedForClosing(deliverablesOf(value)) + expect(paths).toEqual([path]) + const opened = vi.fn() + producedFileMentions(paths, opened, target => target).resolve('report.txt')?.open() + expect(opened).toHaveBeenCalledWith(path) + }) + it('folds successful first-party mutation paths from their raw arguments', () => { const value = assembler([ at(1, 'turn/start', { turn: 1 }), @@ -714,6 +774,32 @@ describe('ChangedFiles card', () => { describe('producedFileMentions resolver', () => { const label = (path: string) => `打开 ${path}` + it('does not let a legacy bare path override an ambiguous canonical basename', () => { + const resolver = producedFileMentions(['report.txt', '/project/child/report.txt'], () => {}, label) + expect(resolver.resolve('report.txt')).toBeUndefined() + expect(resolver.resolve('/project/child/report.txt')?.title).toBe('/project/child/report.txt') + }) + + it('maps only uniquely recorded argument aliases to their canonical targets', () => { + const first = '/project/one/out/report.txt' + const second = '/project/two/out/report.txt' + const opened = vi.fn() + const unique = producedFileMentions([first], opened, label, [ + { path: first, argumentPath: 'out/report.txt' }, + { path: first, argumentPath: './out/report.txt' }, + ]) + unique.resolve('out/report.txt')?.open() + expect(opened).toHaveBeenCalledWith(first) + expect(unique.resolve('./out/report.txt')?.title).toBe(first) + expect(unique.resolve('one/out/report.txt')).toBeUndefined() + const ambiguous = producedFileMentions([first, second], opened, label, [ + { path: first, argumentPath: 'out/report.txt' }, + { path: second, argumentPath: 'out/report.txt' }, + ]) + expect(ambiguous.resolve('out/report.txt')).toBeUndefined() + expect(ambiguous.resolve('report.txt')).toBeUndefined() + }) + it('resolves exact paths and unique basenames; ambiguity and unknowns stay unresolved', () => { const opened: string[] = [] const resolver = producedFileMentions( @@ -794,6 +880,13 @@ describe('plugin registration', () => { expect(mentions?.resolve('report.html')?.label).toBe('Open site/report.html in sidebar') mentions?.resolve('report.html')?.open() expect(opened).toEqual(['site/report.html']) + const canonical = '/project/child/out/report.txt' + const moved = tailOwner({ produced: [{ seq: 2, path: canonical, argumentPath: 'out/report.txt' }] }, 3, previewPath => opened.push(previewPath)) + const movedMentions = service?.forClosing(moved, SessionId('moved-session')) + movedMentions?.resolve('out/report.txt')?.open() + expect(opened.at(-1)).toBe(canonical) + expect(movedMentions?.resolve('out/report.txt')?.title).toBe(canonical) + const fetcher = vi.fn().mockResolvedValue(new Response(null, { status: 204 })) vi.stubGlobal('fetch', fetcher) const preview = vi.fn<(path: string) => void>() diff --git a/packages/client/ui-directory-picker-browse/package.json b/packages/client/ui-directory-picker-browse/package.json index 035f1ac0fc..557ca70801 100644 --- a/packages/client/ui-directory-picker-browse/package.json +++ b/packages/client/ui-directory-picker-browse/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-directory-picker-browse", "description": "In-app directory browsing surface: the workspace directory-flow owner rendering the host's listing and creation primitives", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -52,7 +52,7 @@ "@deepseek-ai/dsh-client-ui-renderer": "workspace:*", "@deepseek-ai/dsh-client-ui-slots": "workspace:*", "@deepseek-ai/dsh-client-ui-workspace": "workspace:*", - "@testing-library/react": "^16.1.0", + "@testing-library/react": "^16.3.3", "@types/react": "~18.3.1", "@deepseek-ai/cordis": "workspace:~", "react": "^18.2.0", diff --git a/packages/client/ui-directory-picker-native/package.json b/packages/client/ui-directory-picker-native/package.json index 6962aa049c..727c9f22b2 100644 --- a/packages/client/ui-directory-picker-native/package.json +++ b/packages/client/ui-directory-picker-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-directory-picker-native", "description": "Native directory-picker surface: the renderless workspace directory-flow occupant driving the local Desktop or Host OS chooser", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -46,7 +46,7 @@ "@deepseek-ai/dsh-client-test-runtime": "workspace:*", "@deepseek-ai/dsh-client-ui-renderer": "workspace:*", "@deepseek-ai/dsh-client-ui-workspace": "workspace:*", - "@testing-library/react": "^16.1.0", + "@testing-library/react": "^16.3.3", "@types/react": "~18.3.1", "@deepseek-ai/cordis": "workspace:~", "react": "^18.2.0", diff --git a/packages/client/ui-dockkit/package.json b/packages/client/ui-dockkit/package.json index f5f78482c2..e42a8d828f 100644 --- a/packages/client/ui-dockkit/package.json +++ b/packages/client/ui-dockkit/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-dockkit", "description": "Docking layout kit: split-tree engine with invertible operations, and the React components that render and drive it (zero cordis)", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -26,7 +26,7 @@ "@deepseek-ai/cordis": "workspace:~", "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/dsh-client-ui-primitives": "workspace:*", - "@testing-library/react": "^16.1.0", + "@testing-library/react": "^16.3.3", "@types/react": "~18.3.1", "@types/react-dom": "~18.3.0", "react-dom": "^18.2.0", diff --git a/packages/client/ui-goal/package.json b/packages/client/ui-goal/package.json index 47ea4f37bb..b1cac92875 100644 --- a/packages/client/ui-goal/package.json +++ b/packages/client/ui-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-goal", "description": "Session goal surface: GoalBar docked above the composer, read from the goal session projection", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -60,7 +60,7 @@ "@deepseek-ai/dsh-client-ui-session": "workspace:*", "@deepseek-ai/dsh-commands": "workspace:*", "@deepseek-ai/dsh-goal": "workspace:*", - "@testing-library/react": "^16.1.0", + "@testing-library/react": "^16.3.3", "@types/react": "~18.3.1", "@deepseek-ai/cordis": "workspace:~", "react": "^18.2.0", diff --git a/packages/client/ui-input-trigger/package.json b/packages/client/ui-input-trigger/package.json index b8c73e7eb4..280dbdaaad 100644 --- a/packages/client/ui-input-trigger/package.json +++ b/packages/client/ui-input-trigger/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-input-trigger", "description": "Input trigger pipeline: '/' and '@' detection, candidate menu, pick routing to registered sources", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-jobs/README.i18n.yaml b/packages/client/ui-jobs/README.i18n.yaml index 74868aab1a..0b6fe4ebae 100644 --- a/packages/client/ui-jobs/README.i18n.yaml +++ b/packages/client/ui-jobs/README.i18n.yaml @@ -24,8 +24,8 @@ en: c34dad62fee63306 zh: 167d9da7928a9488 /deepseek-ai-dsh-client-ui-jobs/understand-the-implementation: - en: f6f5d497ab340b9e - zh: 94832761177f12ca + en: 98daaa0c15166ea5 + zh: a37b5db4be2e40fe /deepseek-ai-dsh-client-ui-jobs/further-exploration: en: 01eaaecb5850f391 zh: 2eabd14cde191ab8 diff --git a/packages/client/ui-jobs/README.md b/packages/client/ui-jobs/README.md index 52b920b439..713488561d 100644 --- a/packages/client/ui-jobs/README.md +++ b/packages/client/ui-jobs/README.md @@ -45,7 +45,7 @@ Expanding an observable row opens that job's output observation stream from `ctx
Implementation internals — click to expand -One slot entry in the header actions band (after the preset label) renders the trigger and popover; the popover fits itself to the viewport by measuring its anchor. All data arrives through `ctx.jobs` — the component holds no transport state. The roster follows the mount: one `useEffect` keeps the session's `job.list` stream open while the control lives. Observation follows visibility: another `useEffect` opens the stream for the expanded row's job and closes it on collapse, unmount, or popover close. +One slot entry in the header actions band (after the preset label) renders the trigger and popover; the popover portals to the document body, so the clipping header column and the right sidebar cannot cut it off, and is placed from its anchor and clamped inside the viewport. All data arrives through `ctx.jobs` — the component holds no transport state. The roster follows the mount: one `useEffect` keeps the session's `job.list` stream open while the control lives. Observation follows visibility: another `useEffect` opens the stream for the expanded row's job and closes it on collapse, unmount, or popover close. | File | Role | |---|---| diff --git a/packages/client/ui-jobs/README.zh.md b/packages/client/ui-jobs/README.zh.md index 1881d9432a..b593a3773e 100644 --- a/packages/client/ui-jobs/README.zh.md +++ b/packages/client/ui-jobs/README.zh.md @@ -45,7 +45,7 @@ kind: "package-reference"
实现内幕——点击展开 -头部操作带里的一个 slot 条目(preset 标签之后)渲染触发器与弹出层;弹出层通过测量锚点把自己收进视口。所有数据经 `ctx.jobs` 到达——组件不持有任何传输状态。名册跟随挂载:一个 `useEffect` 在控件存活期间保持会话的 `job.list` 流打开。观测跟随可见性:另一个 `useEffect` 为展开行的 job 打开流,并在收起、卸载或弹出层关闭时关闭它。 +头部操作带里的一个 slot 条目(preset 标签之后)渲染触发器与弹出层;弹出层挂载到 document body,因此不会被裁剪的头部列或右侧边栏截断,并按锚点定位、限制在视口内。所有数据经 `ctx.jobs` 到达——组件不持有任何传输状态。名册跟随挂载:一个 `useEffect` 在控件存活期间保持会话的 `job.list` 流打开。观测跟随可见性:另一个 `useEffect` 为展开行的 job 打开流,并在收起、卸载或弹出层关闭时关闭它。 | 文件 | 角色 | |---|---| diff --git a/packages/client/ui-jobs/package.json b/packages/client/ui-jobs/package.json index aef45c055b..7cd9351e30 100644 --- a/packages/client/ui-jobs/package.json +++ b/packages/client/ui-jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-jobs", "description": "Session-header background-job list with on-demand streaming record panels", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "type": "module", "main": "lib/index.js", "types": "lib/types/index.d.ts", @@ -46,7 +46,9 @@ "@deepseek-ai/dsh-jobs": "workspace:*", "@deepseek-ai/dsh-session": "workspace:*", "@types/react": "~18.3.1", - "react": "^18.2.0" + "@types/react-dom": "~18.3.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-jobs/src/client/JobListAction.module.css b/packages/client/ui-jobs/src/client/JobListAction.module.css index d51e9f7156..549d34f63c 100644 --- a/packages/client/ui-jobs/src/client/JobListAction.module.css +++ b/packages/client/ui-jobs/src/client/JobListAction.module.css @@ -1,7 +1,3 @@ -.root { - position: relative; -} - .trigger { display: inline-flex; align-items: center; @@ -38,10 +34,11 @@ margin: 0 5px; } +/* Portaled and fixed in the viewport with coordinates supplied inline from the + trigger rect; the z-index layers above the right sidebar (at most 40) and + below modal overlays (1000). */ .menu { - position: absolute; - top: calc(100% + 5px); - left: 0; + position: fixed; z-index: 100; box-sizing: border-box; display: flex; diff --git a/packages/client/ui-jobs/src/client/JobListAction.tsx b/packages/client/ui-jobs/src/client/JobListAction.tsx index 2bca64cb3b..ed60e3ca5b 100644 --- a/packages/client/ui-jobs/src/client/JobListAction.tsx +++ b/packages/client/ui-jobs/src/client/JobListAction.tsx @@ -1,8 +1,9 @@ -import { useEffect, useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react' +import { useEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent } from 'react' +import { createPortal } from 'react-dom' import type { JobsSnapshot, JobView, ObservedJob } from '@deepseek-ai/dsh-api-job-controller/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import { - IconChevronDownOutlineRegular, IconStopFillRegular, StateDot, TerminalBlock, useDismissOnOutsidePointer, + IconChevronDownOutlineRegular, IconStopFillRegular, StateDot, TerminalBlock, useAnchoredPosition, useDismissOnOutsidePointer, type StateDotState, type TerminalBlockLabels, } from '@deepseek-ai/dsh-client-ui-primitives' import type { InjectFace, PropsLocale, PropsRuntime, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots' @@ -49,6 +50,15 @@ const NO_JOBS: readonly JobView[] = [] /** Minimum gap kept between the popover and the viewport edges (the Menu primitive's portal margin). */ const VIEWPORT_MARGIN = 12 +/** Distance between the trigger's bottom edge and the popover. */ +const MENU_GAP = 5 + +/** Unplaced list: hidden but laid out so the first placement measures real dimensions. */ +const MEASURE_STYLE: CSSProperties = { visibility: 'hidden', left: 0, top: 0 } + +/** Keyboard-reachable controls inside the list, in document order. */ +const FOCUSABLE = 'button:not(:disabled), a[href], [tabindex]:not([tabindex="-1"])' + /** How long an armed kill waits for its confirming press before disarming. */ const KILL_ARM_MS = 3_000 @@ -126,6 +136,7 @@ function formatDuration(elapsedMs: number, t: TranslateNS): string { /** Localized display copy for the embedded terminal panel. */ function terminalLabels(t: TranslateNS): TerminalBlockLabels { return { + commandLine: line => t('terminal.commandLine', { n: line }), // The labels contract requires exit-fact formatters, but this panel never // passes exit facts, so TerminalBlock never invokes them. /* v8 ignore next */ @@ -339,9 +350,6 @@ export function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, const rootRef = useRef(null) const triggerRef = useRef(null) const menuRef = useRef(null) - // Horizontal shift applied to the trigger-anchored popover so it stays - // inside the viewport (the stylesheet alone cannot see the anchor offset). - const [menuShift, setMenuShift] = useState(0) const rows = useMemo(() => ordered(jobs), [jobs]) const liveRows = useMemo(() => rows.filter(isLive), [rows]) @@ -352,7 +360,13 @@ export function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, const settledExpanded = settledOpen ?? liveRows.length === 0 const visibleCount = liveRows.length + settledRows.length - useDismissOnOutsidePointer(rootRef, open, setOpen) + // The popover portals to the document body: the session header sits in a + // clipping column that the right sidebar borders or overlaps, so an in-place + // popover is cut off or covered there. + const menuPosition = useAnchoredPosition({ + open, anchorRef: rootRef, panelRef: menuRef, gap: MENU_GAP, margin: VIEWPORT_MARGIN, + }) + useDismissOnOutsidePointer(rootRef, open, setOpen, menuRef) // The roster follows the mounted session: one stream while this control // lives, released with it. @@ -366,32 +380,6 @@ export function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, return () => { clearInterval(timer) } }, [open, liveRows.length]) - // Fit the open popover to the viewport: shift left when the anchored width - // would cross the right edge, never past the left margin. - useLayoutEffect(() => { - if (!open) { - setMenuShift(0) - return - } - const fit = (): void => { - const root = rootRef.current - const menu = menuRef.current - /* v8 ignore next -- both refs are attached while the open popover renders. */ - if (root === null || menu === null) return - const width = menu.offsetWidth - // Unlaid-out nodes (and jsdom) measure 0: keep the pure CSS anchor. - if (width === 0) return - const anchorLeft = root.getBoundingClientRect().left - setMenuShift(Math.max( - VIEWPORT_MARGIN - anchorLeft, - Math.min(0, window.innerWidth - VIEWPORT_MARGIN - width - anchorLeft), - )) - } - fit() - window.addEventListener('resize', fit) - return () => { window.removeEventListener('resize', fit) } - }, [open]) - // Observation follows visibility: the stream opens when an observable panel // expands and closes when it collapses, unmounts, or the popover closes. const expandedRow = open && expandedKey !== undefined @@ -475,11 +463,33 @@ export function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, } } + // The portaled list sits at the end of the document, so Tab is bridged to + // keep the in-place order: the trigger leads into the list, and leaving + // either end of the list returns to the trigger's position in the page. const onKeyDown = (event: KeyboardEvent): void => { - if (event.key !== 'Escape' || !open) return - event.preventDefault() - setOpen(false) - triggerRef.current?.focus() + if (!open) return + const trigger = triggerRef.current + if (event.key === 'Escape') { + event.preventDefault() + setOpen(false) + trigger?.focus() + return + } + const menu = menuRef.current + /* v8 ignore next -- both refs are attached while the open list renders. */ + if (event.key !== 'Tab' || trigger === null || menu === null) return + const focusable = [...menu.querySelectorAll(FOCUSABLE)] + if (event.target === trigger) { + // Every list carries a control: a live row's toggle or the settled section's buttons. + if (event.shiftKey) return + event.preventDefault() + focusable[0]?.focus() + } else if (event.shiftKey ? event.target === focusable[0] : event.target === focusable.at(-1)) { + // Shift+Tab lands on the trigger itself; Tab continues from it to the + // control that follows the trigger. + if (event.shiftKey) event.preventDefault() + trigger.focus() + } } const item = (job: JobView) => ( @@ -505,7 +515,7 @@ export function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, ) return ( -
+
{open - ? ( -
    + ? createPortal( +
      {liveRows.length > 0 ? : null} @@ -551,7 +566,8 @@ export function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, ) : null} {settledExpanded ? settledRows.map(item) : null} -
    +
, + document.body, ) : null}
diff --git a/packages/client/ui-jobs/src/client/locales.ts b/packages/client/ui-jobs/src/client/locales.ts index 466288d01e..5db43e0514 100644 --- a/packages/client/ui-jobs/src/client/locales.ts +++ b/packages/client/ui-jobs/src/client/locales.ts @@ -34,6 +34,7 @@ export const zh = { 'terminal.signal': '信号 {signal}', 'terminal.exitCode': '退出码 {code}', 'terminal.noExitCode': '未正常退出', + 'terminal.commandLine': '命令第 {n} 行', 'terminal.running': '运行中', 'terminal.failed': '已失败', 'terminal.done': '已完成', @@ -77,6 +78,7 @@ export const en: Record = { 'terminal.signal': 'signal {signal}', 'terminal.exitCode': 'exit {code}', 'terminal.noExitCode': 'no exit code', + 'terminal.commandLine': 'Command line {n}', 'terminal.running': 'running', 'terminal.failed': 'failed', 'terminal.done': 'done', diff --git a/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx b/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx index 26b6bd43a5..ab673c2274 100644 --- a/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx +++ b/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx @@ -282,41 +282,19 @@ describe('JobListAction observation', () => { expect(writeText).toHaveBeenCalledWith('pnpm run build') }) - it('shifts an overflowing popover back inside the viewport and follows resizes', () => { - vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(440) - vi.spyOn(HTMLDivElement.prototype, 'getBoundingClientRect').mockReturnValue({ left: 344 } as DOMRect) - const originalWidth = window.innerWidth - Object.defineProperty(window, 'innerWidth', { value: 700, configurable: true, writable: true }) - try { - render() - openList() - const menu = screen.getByRole('list', { name: zh['list.aria'] }) - // 700 - 12 - 440 - 344 = -96: the popover moves left to keep the margin. - expect(menu.style.left).toBe('-96px') - - window.innerWidth = 900 - fireEvent(window, new Event('resize')) - // 900 - 12 - 440 - 344 = 104 > 0: the anchored position fits again. - expect(menu.style.left).toBe('0px') - } finally { - Object.defineProperty(window, 'innerWidth', { value: originalWidth, configurable: true, writable: true }) - } - }) - - it('never crosses the left viewport margin for an oversized popover', () => { - vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(800) - vi.spyOn(HTMLDivElement.prototype, 'getBoundingClientRect').mockReturnValue({ left: 4 } as DOMRect) - const originalWidth = window.innerWidth - Object.defineProperty(window, 'innerWidth', { value: 700, configurable: true, writable: true }) - try { - render() - openList() - const menu = screen.getByRole('list', { name: zh['list.aria'] }) - // max(12 - 4, min(0, 700 - 12 - 800 - 4)) = 8: clamped at the left margin. - expect(menu.style.left).toBe('8px') - } finally { - Object.defineProperty(window, 'innerWidth', { value: originalWidth, configurable: true, writable: true }) - } + it('portals the open list below its trigger and keeps it open on pointer presses inside', () => { + vi.spyOn(HTMLDivElement.prototype, 'getBoundingClientRect').mockReturnValue({ left: 344, bottom: 40 } as DOMRect) + const { container } = render() + openList() + const menu = screen.getByRole('list', { name: zh['list.aria'] }) + // Outside the header's clipping column, so the right sidebar cannot cover it. + expect(container.contains(menu)).toBe(false) + expect(menu.style.left).toBe('344px') + expect(menu.style.top).toBe('45px') + fireEvent.pointerDown(menu) + expect(screen.getByRole('list', { name: zh['list.aria'] })).toBeDefined() + fireEvent.pointerDown(document.body) + expect(screen.queryByRole('list', { name: zh['list.aria'] })).toBeNull() }) it('ignores other keys while open and closes once the roster empties', () => { @@ -361,10 +339,10 @@ describe('JobListAction observation', () => { gapBefore: false, streaming: true, } - const { container } = render() + render() openList() fireEvent.click(screen.getByRole('button', { name: zh['row.expandAria'].replace('{label}', 'pnpm run build') })) - expect(container.querySelector('[data-running]')).not.toBeNull() + expect(document.body.querySelector('[data-running]')).not.toBeNull() expect(screen.queryByText(zh['terminal.noOutput'])).toBeNull() }) @@ -383,6 +361,34 @@ describe('JobListAction observation', () => { expect(screen.getByText('实时输出流中断:connection lost')).toBeDefined() }) + it('bridges Tab between the trigger and the portaled list and closes on Escape', () => { + render() + const trigger = screen.getAllByRole('button')[0]! + fireEvent.keyDown(trigger, { key: 'Tab' }) + openList() + const [first, last] = within(screen.getByRole('list', { name: zh['list.aria'] })).getAllByRole('button') + trigger.focus() + // Shift+Tab from the trigger leaves toward the controls before it. + expect(fireEvent.keyDown(trigger, { key: 'Tab', shiftKey: true })).toBe(true) + expect(document.activeElement).toBe(trigger) + // Tab from the trigger enters the list. + expect(fireEvent.keyDown(trigger, { key: 'Tab' })).toBe(false) + expect(document.activeElement).toBe(first) + // Inside the list Tab stays the browser's until an end is reached. + expect(fireEvent.keyDown(first!, { key: 'Tab' })).toBe(true) + // Tab off the last control resumes from the trigger's place in the page. + expect(fireEvent.keyDown(last!, { key: 'Tab' })).toBe(true) + expect(document.activeElement).toBe(trigger) + // Shift+Tab off the first control returns to the trigger itself. + first!.focus() + expect(fireEvent.keyDown(first!, { key: 'Tab', shiftKey: true })).toBe(false) + expect(document.activeElement).toBe(trigger) + first!.focus() + fireEvent.keyDown(first!, { key: 'Escape' }) + expect(screen.queryByRole('list', { name: zh['list.aria'] })).toBeNull() + expect(document.activeElement).toBe(trigger) + }) + it('stops observation when the popover closes via Escape', () => { const stop = vi.fn() render( stop)} />) diff --git a/packages/client/ui-layout/README.i18n.yaml b/packages/client/ui-layout/README.i18n.yaml index 6e9ff49084..4ad7481ef7 100644 --- a/packages/client/ui-layout/README.i18n.yaml +++ b/packages/client/ui-layout/README.i18n.yaml @@ -9,8 +9,8 @@ en: a2f7bed6f7fd12d9 zh: 9d7981efe76c734d /deepseek-ai-dsh-client-ui-layout/summary: - en: 0315d385c20948ac - zh: b4d31027504adc25 + en: 88db8e221f3eedc2 + zh: 4a72a6887895f0fc /deepseek-ai-dsh-client-ui-layout/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb @@ -21,11 +21,11 @@ en: 7eac53b38d0777fe zh: 7f9422dff671ba50 /deepseek-ai-dsh-client-ui-layout/use-this-package/theme-presentation: - en: 1e1d3361dac7d8b0 - zh: b6b3d210d65738f0 + en: 5ac34303ad90b484 + zh: abcc4a1dcf28391e /deepseek-ai-dsh-client-ui-layout/understand-the-implementation: - en: 244fc753908f70b6 - zh: 8b932150b01a0516 + en: 5ab6b311a912fb06 + zh: d7e88894686b1682 /deepseek-ai-dsh-client-ui-layout/further-exploration: en: 74de780b4592e28e zh: a413f06f8ec406d0 diff --git a/packages/client/ui-layout/README.md b/packages/client/ui-layout/README.md index acc75b5302..43c61b89d1 100644 --- a/packages/client/ui-layout/README.md +++ b/packages/client/ui-layout/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -This package provides the Web GUI's three-column AppFrame, edge-column widths, and `ctx.layout` presentation control. The right column concedes space before the center; its occupant renders fullscreen while the frame retains the wide-screen track underneath. The theme presenter owns color scheme, alias tokens, content font size, and document metadata. Layout state resets on reload. +This package provides the Web GUI's three-column AppFrame, edge-column widths, and `ctx.layout` presentation control. The right column concedes space before the center; its occupant renders fullscreen while the frame retains the wide-screen track underneath. The theme presenter owns color scheme, alias tokens, font sizes, font lists, and document metadata. Layout state resets on reload. ## Table of Contents @@ -44,7 +44,7 @@ The root publishes `--dsh-frame-top-clearance` for the reserved strip (48px on m ### Theme presentation -The presenter consumes resolved theme snapshots and projects them onto the document: `html { color-scheme }` for native UA chrome, `body[data-ds-dark-theme]` from the active color scheme, the theme's alias tokens and `--dsh-content-font-size` as inline variables on body, and one owned `` whose content follows the computed body background. Disposing the presenter removes its metadata node with its other global writes. +The presenter consumes resolved theme snapshots and projects them onto the document: `html { color-scheme }` for native UA chrome, `body[data-ds-dark-theme]` from the active color scheme, the theme's alias tokens, `--dsh-content-font-size`, `--dsh-code-font-size`, `--dsh-terminal-font-size`, and each non-empty `--dsh-font-family-` list as inline variables on body, and one owned `` whose content follows the computed body background. Disposing the presenter removes its metadata node with its other global writes. ----- @@ -56,7 +56,7 @@ The presenter consumes resolved theme snapshots and projects them onto the docum `selectPanel(id)` checks the live `main` registry before changing selection; an absent key throws and leaves the current panel intact. `beginNavigation()` returns an abort signal for an asynchronous UI navigation. A later call, a valid panel selection (including repeated selection), or layout disposal aborts that signal without cancelling underlying Session creation. Consumers check the signal before committing navigation or moving drafts. -One registration declares the frame slots and binds `ctx.layout` methods `selectPanel`, `toggleSidebar`, `openRightbar(track, fullscreen)`, and `closeRightbar`. One root store separates `panelInfo` selection from `layoutInfo` measurements, width preferences, and presentation reports. `ctx.layout.panelInfo` and the standard `usePanelInfo` hook share the same selection source; AppFrame subscribes to the stable layout object. The `rightbar` owner supplies actual `width`, `viewportWidth`, and normal-presentation eligibility `canShow`; insufficient room causes a deterministic close, never automatic reopening on widening. Fullscreen hides the width handle without releasing a track the occupant retains. AppFrame keeps the column containers mounted. The right column's root controller renders `rightbar.session` through `SessionProvider` only while the Conversation is selected; its unmount report releases the track. The independent title component uses the selected Session title only while the Conversation is visible, with the build-configured product title or localized `common.brand.localBuild` as its fallback; locale revisions update that fallback. The theme presenter is a second effect: pure DOM writes from resolved snapshots — initial state through the getter once, then event-driven only, with no React path. It applies palette, font-size, and token variables before measuring the rendered background as the single color authority. Fullscreen presentation suppresses grid and handle transitions; its occupant reports the new columns only after covering the frame. Fullscreen exit keeps transitions suppressed while the frame installs its destination geometry: close removes the right track, and restore retains it. Subsequent normal geometry actions restore ordinary transitions. +One registration declares the frame slots and binds `ctx.layout` methods `selectPanel`, `toggleSidebar`, `openRightbar(track, fullscreen)`, and `closeRightbar`. One root store separates `panelInfo` selection from `layoutInfo` measurements, width preferences, and presentation reports. `ctx.layout.panelInfo` and the standard `usePanelInfo` hook share the same selection source; AppFrame subscribes to the stable layout object. The `rightbar` owner supplies actual `width`, `viewportWidth`, and normal-presentation eligibility `canShow`; insufficient room causes a deterministic close, never automatic reopening on widening. Fullscreen hides the width handle without releasing a track the occupant retains. AppFrame keeps the column containers mounted. The right column's root controller renders `rightbar.session` through `SessionProvider` only while the Conversation is selected; its unmount report releases the track. The independent title component uses the selected Session title only while the Conversation is visible, with the build-configured product title or localized `common.brand.localBuild` as its fallback; locale revisions update that fallback. The theme presenter is a second effect: pure DOM writes from resolved snapshots — initial state through the getter once, then event-driven only, with no React path. It applies palette, font-size, font-list, and token variables before measuring the rendered background as the single color authority. Fullscreen presentation suppresses grid and handle transitions; its occupant reports the new columns only after covering the frame. Fullscreen exit keeps transitions suppressed while the frame installs its destination geometry: close removes the right track, and restore retains it. Subsequent normal geometry actions restore ordinary transitions.
diff --git a/packages/client/ui-layout/README.zh.md b/packages/client/ui-layout/README.zh.md index 729155fd2b..9b426dcdef 100644 --- a/packages/client/ui-layout/README.zh.md +++ b/packages/client/ui-layout/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -本包提供 Web GUI 的三栏 AppFrame、左右栏宽度与 `ctx.layout` 呈现控制。右栏先让步以保护中栏空间,全屏由占用方呈现,框架保留宽屏底层轨道。主题呈现器负责配色、别名 token、正文字号与 document 元数据;布局状态在刷新后重置。 +本包提供 Web GUI 的三栏 AppFrame、左右栏宽度与 `ctx.layout` 呈现控制。右栏先让步以保护中栏空间,全屏由占用方呈现,框架保留宽屏底层轨道。主题呈现器负责配色、别名 token、字号、字体列表与 document 元数据;布局状态在刷新后重置。 ## 目录 @@ -44,7 +44,7 @@ Windows Electron 的 `data-windows-titlebar` 标记在所有列上方预留顶 ### 主题呈现 -呈现器消费解析后的主题快照,并投影到 document:`html { color-scheme }` 驱动原生 UA 控件,依据当前配色方案设置 `body[data-ds-dark-theme]`,把主题的别名 token 与 `--dsh-content-font-size` 设为 body 上的内联变量,并持有一个 ``,其内容随计算后的 body 背景色更新。对呈现器执行 dispose(资源释放)时,它会连同其他全局写入一起移除自己的元数据节点。 +呈现器消费解析后的主题快照,并投影到 document:`html { color-scheme }` 驱动原生 UA 控件,依据当前配色方案设置 `body[data-ds-dark-theme]`,把主题的别名 token、`--dsh-content-font-size`、`--dsh-code-font-size`、`--dsh-terminal-font-size` 以及每个非空的 `--dsh-font-family-` 列表设为 body 上的内联变量,并持有一个 ``,其内容随计算后的 body 背景色更新。对呈现器执行 dispose(资源释放)时,它会连同其他全局写入一起移除自己的元数据节点。 ----- @@ -56,7 +56,7 @@ Windows Electron 的 `data-windows-titlebar` 标记在所有列上方预留顶 `selectPanel(id)` 在改变选中态前检查实时 `main` 注册表;缺失的 key 会抛错并保留当前面板。`beginNavigation()` 为异步 UI 导航返回 abort signal。后续调用、有效面板选择(包括重复选择)或布局释放会中止该 signal,但不取消底层会话创建。消费方在提交导航或搬移草稿前检查 signal。 -一次注册声明框架的子 slot,并绑定 `ctx.layout` 的 `selectPanel`、`toggleSidebar`、`openRightbar(track, fullscreen)` 与 `closeRightbar`。同一个 root 存储把 `panelInfo` 选中态与 `layoutInfo` 测量、宽度偏好、呈现报告分开。`ctx.layout.panelInfo` 与标准 `usePanelInfo` hook 共用同一个选中态来源;AppFrame 订阅引用稳定的布局对象。`rightbar` owner 提供实际 `width`、`viewportWidth`,以及表示能否以普通模式呈现的 `canShow`;占用方在空间不足时执行确定性的收起,变宽不自行重新展开。全屏隐藏宽度手柄,但不自行释放占用方要求保留的轨道。AppFrame 保持各列容器挂载。右栏的 root 控制器仅在选中会话界面时,经 `SessionProvider` 渲染 `rightbar.session`;内容卸载时的报告释放轨道。独立的标题组件仅在会话界面可见时使用所选会话标题,以构建配置的产品标题或本地化 `common.brand.localBuild` 为回退值;语言变化会更新该回退值。主题呈现器是第二个 effect:从解析后的快照做纯 DOM 写入——初始状态经 getter 读取一次,此后仅事件驱动,不经过 React。它先应用调色板、字号与 token 变量,再把渲染出的背景测量为唯一的颜色依据。全屏呈现禁用网格和手柄过渡;占用方完全覆盖框架后才报告新的列布局。退出全屏时,框架先保持无过渡并安装目标布局:关闭移除右轨道,恢复保留右轨道。后续普通几何操作恢复正常过渡。 +一次注册声明框架的子 slot,并绑定 `ctx.layout` 的 `selectPanel`、`toggleSidebar`、`openRightbar(track, fullscreen)` 与 `closeRightbar`。同一个 root 存储把 `panelInfo` 选中态与 `layoutInfo` 测量、宽度偏好、呈现报告分开。`ctx.layout.panelInfo` 与标准 `usePanelInfo` hook 共用同一个选中态来源;AppFrame 订阅引用稳定的布局对象。`rightbar` owner 提供实际 `width`、`viewportWidth`,以及表示能否以普通模式呈现的 `canShow`;占用方在空间不足时执行确定性的收起,变宽不自行重新展开。全屏隐藏宽度手柄,但不自行释放占用方要求保留的轨道。AppFrame 保持各列容器挂载。右栏的 root 控制器仅在选中会话界面时,经 `SessionProvider` 渲染 `rightbar.session`;内容卸载时的报告释放轨道。独立的标题组件仅在会话界面可见时使用所选会话标题,以构建配置的产品标题或本地化 `common.brand.localBuild` 为回退值;语言变化会更新该回退值。主题呈现器是第二个 effect:从解析后的快照做纯 DOM 写入——初始状态经 getter 读取一次,此后仅事件驱动,不经过 React。它先应用调色板、字号、字体列表与 token 变量,再把渲染出的背景测量为唯一的颜色依据。全屏呈现禁用网格和手柄过渡;占用方完全覆盖框架后才报告新的列布局。退出全屏时,框架先保持无过渡并安装目标布局:关闭移除右轨道,恢复保留右轨道。后续普通几何操作恢复正常过渡。
diff --git a/packages/client/ui-layout/package.json b/packages/client/ui-layout/package.json index 639b6f58c2..9c04a714ca 100644 --- a/packages/client/ui-layout/package.json +++ b/packages/client/ui-layout/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-layout", "description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-layout/src/client/theme-presenter.ts b/packages/client/ui-layout/src/client/theme-presenter.ts index be305142bc..92c6d60008 100644 --- a/packages/client/ui-layout/src/client/theme-presenter.ts +++ b/packages/client/ui-layout/src/client/theme-presenter.ts @@ -3,13 +3,15 @@ * document — `html { color-scheme }` for native UA chrome (scrollbars, form * controls), `body[data-ds-dark-theme]` for the token palette, the active * theme's alias-token overrides as inline CSS variables on body, the content - * font-size axis (`--dsh-content-font-size`), `html[data-ds-theme-source]` + * font-size axis (`--dsh-content-font-size`), the code and terminal sizes + * (`--dsh-code-font-size`, `--dsh-terminal-font-size`), the user font lists + * (`--dsh-font-family-`), `html[data-ds-theme-source]` * for native-chrome mirroring, and one presenter-owned * `meta[name="theme-color"]` for surrounding browser UI. Pure DOM writes, no * React involvement; the presenter only ever retracts what it wrote itself, * so foreign attributes, metadata, and inline styles survive. */ -import type { ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client' +import type { FontRole, ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client' /** Body attribute selecting the dark base palette in the token stylesheets. */ export const DARK_ATTRIBUTE = 'data-ds-dark-theme' @@ -28,6 +30,20 @@ export const THEME_SOURCE_ATTRIBUTE = 'data-ds-theme-source' /** Body variable carrying the user's content font size in px. */ export const CONTENT_FONT_SIZE_VARIABLE = '--dsh-content-font-size' +/** Body variables carrying the user's font sizes in px by role. */ +const FONT_SIZE_VARIABLES = { + text: CONTENT_FONT_SIZE_VARIABLE, + code: '--dsh-code-font-size', + terminal: '--dsh-terminal-font-size', +} as const satisfies Record + +/** Body variables carrying the user's normalized font lists; absent selects the built-in stack. */ +const FONT_FAMILY_VARIABLES = { + text: '--dsh-font-family-text', + code: '--dsh-font-family-code', + terminal: '--dsh-font-family-terminal', +} as const satisfies Record + /** Applies theme snapshots to the document; one instance per plugin fiber. */ export class ThemePresenter { /** Token names this presenter wrote in the last apply (its retraction set). */ @@ -44,7 +60,7 @@ export class ThemePresenter { /** * Project a snapshot onto the document: set root `color-scheme` and the body * palette attribute from `active.colorScheme` (never the id — `system` is - * resolved upstream), publish the content font-size axis, then replace the + * resolved upstream), publish the font sizes and font lists, then replace the * previously applied token variables with `active.tokens`. Browser * theme-color metadata follows the computed body background after those * writes, so the rendered palette remains the color authority. @@ -58,7 +74,14 @@ export class ThemePresenter { const body = document.body if (scheme === 'dark') body.setAttribute(DARK_ATTRIBUTE, '') else body.removeAttribute(DARK_ATTRIBUTE) - body.style.setProperty(CONTENT_FONT_SIZE_VARIABLE, `${snapshot.fontSize}px`) + for (const [role, name] of Object.entries(FONT_SIZE_VARIABLES)) { + body.style.setProperty(name, `${snapshot.fontSizes[role as FontRole]}px`) + } + for (const [kind, name] of Object.entries(FONT_FAMILY_VARIABLES)) { + const list = snapshot.fontFamilies[kind as FontRole] + if (list === '') body.style.removeProperty(name) + else body.style.setProperty(name, list) + } for (const name of this.appliedTokens) body.style.removeProperty(name) this.appliedTokens = [] for (const [name, value] of Object.entries(snapshot.active.tokens)) { @@ -71,14 +94,15 @@ export class ThemePresenter { /** * Retract root color-scheme, the theme-source attribute, the palette - * attribute, token variables, the font-size axis, and the owned metadata node. + * attribute, token variables, the font sizes, font lists, and the owned metadata node. */ dispose(): void { document.documentElement.style.removeProperty('color-scheme') document.documentElement.removeAttribute(THEME_SOURCE_ATTRIBUTE) const body = document.body body.removeAttribute(DARK_ATTRIBUTE) - body.style.removeProperty(CONTENT_FONT_SIZE_VARIABLE) + for (const name of Object.values(FONT_SIZE_VARIABLES)) body.style.removeProperty(name) + for (const name of Object.values(FONT_FAMILY_VARIABLES)) body.style.removeProperty(name) for (const name of this.appliedTokens) body.style.removeProperty(name) this.appliedTokens = [] this.themeColorMeta.remove() diff --git a/packages/client/ui-layout/tests/theme-presenter.client.spec.ts b/packages/client/ui-layout/tests/theme-presenter.client.spec.ts index 471e7c023f..7ab781965c 100644 --- a/packages/client/ui-layout/tests/theme-presenter.client.spec.ts +++ b/packages/client/ui-layout/tests/theme-presenter.client.spec.ts @@ -1,7 +1,8 @@ // @vitest-environment jsdom import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import type { ThemePreference, ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client' +import type { FontFamilies, ThemePreference, ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client' +import { fontFamilyVariable } from '@deepseek-ai/dsh-client-ui-theme/src/theme-settings.ts' import { DARK_ATTRIBUTE, THEME_SOURCE_ATTRIBUTE, ThemePresenter } from '@deepseek-ai/dsh-client-ui-layout/src/client/theme-presenter.ts' const LIGHT_THEME_COLOR = 'rgb(255, 255, 255)' @@ -12,10 +13,11 @@ function snapshot( tokens: Record = {}, fontSize = 14, preference: ThemePreference = colorScheme, + fontFamilies: FontFamilies = { text: '', code: '', terminal: '' }, ): ThemeSnapshot { // The presenter must key off colorScheme, not the id — keep them distinct. const active = { id: `${colorScheme}-test`, colorScheme, tokens } - return { preference, fontSize, active, themes: [active], revision: 1 } + return { preference, fontSizes: { text: fontSize, code: 11, terminal: 13 }, fontFamilies, active, themes: [active], revision: 1 } } function clearThemePresentation(): void { @@ -82,10 +84,31 @@ describe('ThemePresenter', () => { const presenter = new ThemePresenter() presenter.apply(snapshot('light')) expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('14px') + expect(document.body.style.getPropertyValue('--dsh-code-font-size')).toBe('11px') + expect(document.body.style.getPropertyValue('--dsh-terminal-font-size')).toBe('13px') presenter.apply(snapshot('light', {}, 17)) expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('17px') }) + it('publishes non-empty font lists and removes a list once it returns to the built-in stack', () => { + const presenter = new ThemePresenter() + presenter.apply(snapshot('light', {}, 14, 'light', { text: '"Inter"', code: '"Iosevka"', terminal: '"Hack"' })) + // The presenter writes the same variables the ui-theme boot script and token sheet use. + for (const [kind, list] of [['text', '"Inter"'], ['code', '"Iosevka"'], ['terminal', '"Hack"']] as const) { + expect(document.body.style.getPropertyValue(fontFamilyVariable(kind))).toBe(list) + } + presenter.apply(snapshot('light', {}, 14, 'light', { text: '"Inter"', code: '"Iosevka"', terminal: '' })) + expect(document.body.style.getPropertyValue('--dsh-font-family-text')).toBe('"Inter"') + expect(document.body.style.getPropertyValue('--dsh-font-family-code')).toBe('"Iosevka"') + expect(document.body.style.getPropertyValue('--dsh-font-family-terminal')).toBe('') + presenter.apply(snapshot('light', {}, 14, 'light', { text: '', code: '"Iosevka"', terminal: '"MesloLGS NF"' })) + expect(document.body.style.getPropertyValue('--dsh-font-family-text')).toBe('') + expect(document.body.style.getPropertyValue('--dsh-font-family-terminal')).toBe('"MesloLGS NF"') + presenter.dispose() + expect(document.body.style.getPropertyValue('--dsh-font-family-code')).toBe('') + expect(document.body.style.getPropertyValue('--dsh-font-family-terminal')).toBe('') + }) + it('publishes the theme source: system stays system, fixed preferences publish the resolved scheme', () => { const presenter = new ThemePresenter() presenter.apply(snapshot('dark', {}, 14, 'system')) @@ -106,6 +129,7 @@ describe('ThemePresenter', () => { expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false) expect(document.body.style.getPropertyValue('--dsw-alias-bg')).toBe('') expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('') + expect(document.body.style.getPropertyValue('--dsh-code-font-size')).toBe('') expect(document.body.style.getPropertyValue('--foreign')).toBe('kept') expect(meta?.isConnected).toBe(false) }) diff --git a/packages/client/ui-message-feedback/package.json b/packages/client/ui-message-feedback/package.json index d15c2e1501..df6cb6895e 100644 --- a/packages/client/ui-message-feedback/package.json +++ b/packages/client/ui-message-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-message-feedback", "description": "The Web feedback surface: per-message Like/Dislike in the assistant-message action strip and the feedback dialog behind both ratings and /feedback, backed by the messageFeedback and sessionFeedback Host Remotes", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -59,7 +59,7 @@ "@deepseek-ai/dsh-session": "workspace:*", "@deepseek-ai/dsh-typert-protocol": "workspace:*", "@deepseek-ai/cordis": "workspace:~", - "@testing-library/react": "^16.1.0", + "@testing-library/react": "^16.3.3", "@types/react": "~18.3.1", "@types/react-dom": "~18.3.0", "react": "^18.2.0", diff --git a/packages/client/ui-model-selection/package.json b/packages/client/ui-model-selection/package.json index 9e8fcd4a27..90a4f1dbcb 100644 --- a/packages/client/ui-model-selection/package.json +++ b/packages/client/ui-model-selection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-model-selection", "description": "Model selection over the shared model catalog, Session projection, and session.selectModel", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-open-in-app/package.json b/packages/client/ui-open-in-app/package.json index 5f1656313f..e1b15cdf72 100644 --- a/packages/client/ui-open-in-app/package.json +++ b/packages/client/ui-open-in-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-open-in-app", "description": "Web \"Open In...\" controls: the Session-header split button opening the workspace directory in an installed application, and the document preview's default-application controls for one file", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-permission-presets/package.json b/packages/client/ui-permission-presets/package.json index 65fa7463c4..48e196a2eb 100644 --- a/packages/client/ui-permission-presets/package.json +++ b/packages/client/ui-permission-presets/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-permission-presets", "description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-plan/package.json b/packages/client/ui-plan/package.json index d977f0643e..efaa5d5869 100644 --- a/packages/client/ui-plan/package.json +++ b/packages/client/ui-plan/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-plan", "description": "Plan mode controls, persistent transcript plan cards, and sidebar Markdown previews", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-plugin-manager/README.i18n.yaml b/packages/client/ui-plugin-manager/README.i18n.yaml index fc2756e9e0..d9c4fc68fc 100644 --- a/packages/client/ui-plugin-manager/README.i18n.yaml +++ b/packages/client/ui-plugin-manager/README.i18n.yaml @@ -15,17 +15,17 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package: - en: c41c530198c0d610 - zh: 584a6d658ff9f022 + en: 921a2c708727d077 + zh: 26a80d58a875bb3b /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/installing-a-bundle: - en: 70b3d1dfc27406ca - zh: 9a65898773856d71 + en: a5d83df504b38101 + zh: ddbcd4e940779843 /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/switching-a-bundle: en: 2d29970dcd0b2836 zh: 73f6b519546d2b6b /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/switching-one-row-of-a-bundle: - en: 620c6a3ee20848bd - zh: a9429f047a12bb4c + en: b371def25a3598a2 + zh: 05fd50c186551936 /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/configuration-pages: en: c32019330fd9586d zh: dd22110abd10159c @@ -54,8 +54,8 @@ en: ca75c51c89c2c9b0 zh: 2064958b0754647e /deepseek-ai-dsh-client-ui-plugin-manager/known-limitations-and-deferred-work: - en: e9e56a6f084e7ebb - zh: bebb7086ed0237bf + en: 149c0d23a56a4944 + zh: e2feaadc871581ae /deepseek-ai-dsh-client-ui-plugin-manager/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-plugin-manager/README.md b/packages/client/ui-plugin-manager/README.md index 64f3413aa2..de79ee0b1b 100644 --- a/packages/client/ui-plugin-manager/README.md +++ b/packages/client/ui-plugin-manager/README.md @@ -27,10 +27,12 @@ Use the **Plugins** entry in the Web sidebar to manage the profile's installed b ## Use this package -Select **Plugins** in the sidebar. The information button uses an 11px caption-colored icon in a 20px square hover area, with a 4px gap after the subtitle. It explains that this page configures official plugins and installs or manages other plugins, while **Settings → Built-in plugins** shows the built-in inventory and runtime status. Hover or focus shows the shared tooltip; clicking keeps it open until another click, Escape, Tab, or an outside click. The page reads the inventory and the bundles through `api-remotes` when first opened; a Host without a managed profile shows the page as unavailable. **Official** comes first and lists the bundles the installation ships for switching on — off until switched on, without an uninstall, and tagged **Experimental** for experimental features — followed by the official plugins that registered a configuration page; **Installed** lists the bundles the profile holds. Cards are listed by name, so switching a bundle on or off does not move its card. A dependency without a bundle patch is not a plugin and is not listed unless the profile selects it, in which case it carries a problem tag. Global configuration remains in the Settings **Built-in plugins** section. +Select **Plugins** in the sidebar. The information button uses an 11px caption-colored icon in a 20px square hover area, with a 4px gap after the subtitle. It explains that this page configures official plugins and installs or manages other plugins, while **Settings → Built-in plugins** shows the built-in inventory and runtime status. Hover or focus shows the shared tooltip; clicking keeps it open until another click, Escape, Tab, or an outside click. The page reads the inventory and the bundles through `api-remotes` when first opened; a Host without a managed profile shows the page as unavailable. **Official** comes first and lists installation-carried optional bundles and on-demand catalog entries, followed by official plugins that registered a configuration page. Catalog entries remain in Official after installation or removal; **Installed** lists other profile bundles. Installation-carried optional bundles have no uninstall action. Experimental packages carry the **Experimental** marker. Cards are listed by name, so switching a bundle on or off does not move its card. A dependency without a bundle patch is not a plugin and is not listed unless the profile selects it, in which case it carries a problem tag. Global configuration remains in the Settings **Built-in plugins** section. Installed bundles and their plugin rows display their own title and description in the current UI language on cards and detail pages. Each field falls back from exported locale `meta` to the accessible `package.json` of a package-root plugin; subpath plugins never read one; the final title is the full package or module name, with no package description when both sources omit it. See [Plugin display metadata](../../../docs/cookbook/adding-a-package.md#plugin-display-metadata) for the author format. Bundle cards, details, and component rows display the package root's manifest `icon` or exported `./icon`, or a subpath plugin's exported `/icon`; absent or undecodable images retain the default artwork. Installation previews still use registry or manifest information. +On-demand Official cards are visible offline before installation. Turning one On opens the existing install progress dialog, installs the [target offered by the running Host](../../boot/plugin-manager/README.md#use-this-package), and selects its layer. The same registry fallback, cancellation, and script-approval controls apply. Turning Off keeps the dependency installed; Remove is separate. A different version or development link source offers **Update**; details show current and offered sources and versions. Updates preserve selection and report whether a restart is required. Missing or unreadable packages keep their recorded dependency source alongside the diagnostic. An enabled bundle can still be switched Off when its checkout target is unavailable. + The first list read shows a short Official-heading placeholder and four skeleton cards, using the real cards' row, icon, and text-line spacing, with the action area left blank. They use the same opacity pulse as Session search and remain static with reduced motion. The accessible loading label and read timing are unchanged; refreshes retain the existing list, and installation and component-loading indicators keep their own states. The **Refresh** button shows its tooltip after 500 ms of hovering or keyboard focus. During a manual refresh, the button shows a spinner and stays disabled until all pending reads settle and at least 400 ms have elapsed, preventing a brief spinner flash; repeated refresh requests are ignored and the existing list remains visible. Success shows no toast. With a loaded list, a failed manual refresh keeps the cached content and shows a failure toast, including after leaving Plugins; select **Refresh** again to retry. Before any successful list read, failures show an in-place message with **Retry**. Background reads do not show the refresh spinner. @@ -50,7 +52,7 @@ On first use, when pnpm uses the unconfigured official npm registry and npmmirro The Host sends GET requests to `https://registry.npmjs.org/-/ping` and `https://registry.npmmirror.com/-/ping` through its normal fetch proxy. A 2xx response wins; redirects and failures do not. The remaining request is cancelled, both response bodies are discarded, and cleanup completes before the result returns. The default deadline is 1500 ms and results, including unavailable results, are cached for five minutes. Configure `registryProbeTimeoutMs` and `registryProbeCacheTtlMs` on `ui-plugin-manager`; `registryProbeEnabled: false` disables probing. If both requests fail or time out, the existing default remains selected. No IP-location service or Session content is involved. -**Add plugin** takes a package name with an optional version, a Git address, a tarball, or an absolute local path; the dialog says a package name is what follows `dsh plugin add` in a README. Enter in the package or custom-registry field submits the install request, but Enter used to confirm an input-method candidate does not. **Install guide and examples** under the field opens a guide that shows the package-name example; **Use example** drops it into the field. Typing or pasting the Git or local-path template verbatim still raises the reminder asking for the actual value. A trust box above **Install** stays visible including when the guide is collapsed, and adds that installed plugins do not update automatically: upgrading means uninstalling and installing the new version, which later releases will keep improving. **Registry** beside it names the registry the install asks first and unfolds the choice: pnpm's own registry, named by the registry it actually names — the official npm registry, the mainland China mirror, or its host — which keeps the neutral default name until the Host reads it, is listed once, and carries its host unless that host is already its name; each mirror the Host configured (`pluginManager.registries`), npmmirror as the mainland China mirror; and a typed http(s) address. The options use an opaque selector panel, fill rows only on hover, and show hosts in tertiary text without parentheses. The custom-address area highlights on hover; clicking anywhere in it selects the custom registry and focuses its input, whose focus style matches the package field. Keyboard focus alone preserves the selected registry; activating the custom radio or editing the address selects the custom registry. Tab moves from the selected radio to the address, and Shift+Tab moves back; leaving either end closes the selector and returns focus to **Registry**. Switching to an offered registry retains the custom URL draft for switching back. Submitting an invalid custom address blocks installation, opens the registry selector, and focuses the address. Invalid package and custom-address fields keep a red error border and add the shared blue inset focus ring on focus. The options float over the dialog from the control, so unfolding them never lengthens the card, and the card's content scrolls when the guide makes it taller than the viewport. The initial choice follows the response comparison above; the choice is then remembered in this browser (`localStorage`) and starts the next dialog, and a remembered address the Host no longer offers stays as a typed one and remains the requested address when installation starts before the registry list returns. **Install** first asks the Host to read what the spec names (`pluginManager.inspect`) at the chosen registry: a name the list already shows, a name no registry has, a path without a package, a package without a bundle patch, or a spec pnpm would refuse comes back under the field as one sentence, with the spec kept for editing; when no registry could be reached, the sentence names every one asked. The install then starts at the registry that answered. An accepted spec opens the installing screen, which shows the package's name, one-liner, and version as the Host read them and folds pnpm's command and output behind **Show install details**. A finished install shows the version pnpm installed; when a run that asked one registry installed another version than the one read, it names both versions, attributes the difference to pnpm's supply-chain security policy (`minimumReleaseAge`), states the risk of a newly published version, and gives the exact `name@version` to install after uninstalling. A finished install offers **Enable now**, which switches the new bundle on, closes the dialog, and scrolls the list to it; closing instead leaves it installed and off. While the Host moves on to another registry, the installing screen says which registry could not serve the package and which is asked now, and each pnpm run behind the details carries a badge naming its registry. A failed install says what went wrong in one line, as the Host attributed it — no registry could be reached, naming every one asked; the host a GitHub address or a tarball link is fetched from could not be reached, which no registry helps; the package was not found, the disk is full, the profile is not writable, pnpm blocked a build script — with pnpm's output behind the details and **Retry** at hand, and **Change registry** beside it when the Host laid the failure at the registry it asked, which returns to the spec with the registry options open; the Host has already put the profile files back. When pnpm blocked a dependency's install scripts, the failed screen lists the packages whose scripts wait for permission and offers **Allow these scripts and retry** in place of **Retry**; the Host saves the permission in the profile's `pnpm-workspace.yaml`, which a failed run leaves as pnpm wrote it, then runs pnpm again, and the installed screen names what was allowed. A successful installation does not certify that a module can activate. +**Add plugin** takes a package name with an optional version, a Git address, a tarball, or an absolute local path; the dialog says a package name is what follows `dsh plugin add` in a README. Enter in the package or custom-registry field submits the install request, but Enter used to confirm an input-method candidate does not. **Install guide and examples** under the field opens a guide that shows the package-name example; **Use example** drops it into the field. Typing or pasting the Git or local-path template verbatim still raises the reminder asking for the actual value. A trust box above **Install** stays visible including when the guide is collapsed, and explains that on-demand Official entries offer explicit Update, while other plugins are upgraded by uninstalling and reinstalling. **Registry** beside it names the registry the install asks first and unfolds the choice: pnpm's own registry, named by the registry it actually names — the official npm registry, the mainland China mirror, or its host — which keeps the neutral default name until the Host reads it, is listed once, and carries its host unless that host is already its name; each mirror the Host configured (`pluginManager.registries`), npmmirror as the mainland China mirror; and a typed http(s) address. The options use an opaque selector panel, fill rows only on hover, and show hosts in tertiary text without parentheses. The custom-address area highlights on hover; clicking anywhere in it selects the custom registry and focuses its input, whose focus style matches the package field. Keyboard focus alone preserves the selected registry; activating the custom radio or editing the address selects the custom registry. Tab moves from the selected radio to the address, and Shift+Tab moves back; leaving either end closes the selector and returns focus to **Registry**. Switching to an offered registry retains the custom URL draft for switching back. Submitting an invalid custom address blocks installation, opens the registry selector, and focuses the address. Invalid package and custom-address fields keep a red error border and add the shared blue inset focus ring on focus. The options float over the dialog from the control, so unfolding them never lengthens the card, and the card's content scrolls when the guide makes it taller than the viewport. The initial choice follows the response comparison above; the choice is then remembered in this browser (`localStorage`) and starts the next dialog, and a remembered address the Host no longer offers stays as a typed one and remains the requested address when installation starts before the registry list returns. **Install** first asks the Host to read what the spec names (`pluginManager.inspect`) at the chosen registry: a name the list already shows, a name no registry has, a path without a package, a package without a bundle patch, or a spec pnpm would refuse comes back under the field as one sentence, with the spec kept for editing; when no registry could be reached, the sentence names every one asked. The install then starts at the registry that answered. An accepted spec opens the installing screen, which shows the package's name, one-liner, and version as the Host read them and folds pnpm's command and output behind **Show install details**. Each command line stays on one line and scrolls horizontally; selecting its text includes offscreen arguments. A finished install shows the version pnpm installed; when a run that asked one registry installed another version than the one read, it names both versions, attributes the difference to pnpm's supply-chain security policy (`minimumReleaseAge`), states the risk of a newly published version, and gives the exact `name@version` to install after uninstalling. A finished install offers **Enable now**, which switches the new bundle on, closes the dialog, and scrolls the list to it; closing instead leaves it installed and off. While the Host moves on to another registry, the installing screen says which registry could not serve the package and which is asked now, and each pnpm run behind the details carries a badge naming its registry. A failed install says what went wrong in one line, as the Host attributed it — no registry could be reached, naming every one asked; the host a GitHub address or a tarball link is fetched from could not be reached, which no registry helps; the package was not found, the disk is full, the profile is not writable, pnpm blocked a build script — with pnpm's output behind the details and **Retry** at hand, and **Change registry** beside it when the Host laid the failure at the registry it asked, which returns to the spec with the registry options open; the Host has already put the profile files back. When pnpm blocked a dependency's install scripts, the failed screen lists the packages whose scripts wait for permission and offers **Allow these scripts and retry** in place of **Retry**; the Host saves the permission in the profile's `pnpm-workspace.yaml`, which a failed run leaves as pnpm wrote it, then runs pnpm again, and the installed screen names what was allowed. A successful installation does not certify that a module can activate. During preparation and download, **Cancel install** asks the Host to stop the run and waits for confirmation. Loading the bundle cannot be cancelled. Closing with ×, Escape, or the backdrop hides the dialog immediately and requests cancellation when possible. **View installation** reopens the same task with its output; another installation cannot start while its outcome is pending or unconfirmed. Confirmed cancellation returns to the spec and shows a toast; the manifest and lockfile are restored, while downloaded files can remain. A lost installation response triggers a request to recover the result; **Check installation status** and reconnect retry that request. If the Host has no active request, **Installation result unavailable** allows returning to editing after checking the plugin list. A cancellation sent before acceptance waits and retries automatically; a failed cancellation can be retried manually. Hidden tasks report their outcome through toasts without reopening the dialog. @@ -62,7 +64,7 @@ A bundle's page shows its full package name under the title. Its **Source** sect ### Switching one row of a bundle -A row's switch on the bundle's page calls `pluginManager.setPluginEnabled`, which writes the row's `disabled` override into the profile's `cordis.patch.yml`. The tree recomposes at once on a profile with HMR, so the row's host half unmounts or mounts while the rest of the bundle keeps running, and the page follows the client module graph without reloading. Rows use the shared status marker for their Host fiber phase: pending and disabled are idle, loading and unloading are ongoing, active is done, and failed is error. The switch appears only on a bundle that is on; a row without a live entry, or one the Host will not address through the profile patch, is locked with the Host's reason. A list longer than ten rows gets a filter over localized titles, descriptions, row ids, and module names. +A row's switch on the bundle's page calls `pluginManager.setPluginEnabled`, which writes the row's `disabled` override into the profile's `cordis.patch.yml`. The tree recomposes at once on a profile with HMR, so the row's host half unmounts or mounts while the rest of the bundle keeps running, and the page follows the client module graph without reloading. Rows use the shared status marker for their Host fiber phase: pending and disabled are idle, loading and unloading are ongoing, active is done, and failed is error. The switch appears only on a bundle that is on; a row without a live entry, or one the Host will not address through the profile patch, is locked with the Host's reason. Scoped preset rows show their preset target beside the declared id and their current composition state; their controls and configuration pages remain read-only, with guidance to configure them through the preset or bundle. Equal child ids in different presets retain distinct list and navigation identities; detail contributions receive the same preset identity in each row reference. A preset row with an unevaluated enablement expression displays Conditional, without an Off count or switch. A list longer than ten rows gets a filter over localized titles, descriptions, row ids, and module names. ### Configuration pages @@ -160,7 +162,7 @@ These limits define the reach of the management view; they are current package c - **Only bundles are managed** — a dependency without a bundle patch is refused before it installs; one the profile already holds is left off the page unless the profile selects it, and loading plain plugin modules stays a file operation. - **Rows show a phase, not a reason** — a failed row reads as failed without the Host's error text; the Host log has it. - **One install at a time** — the dialog runs one pnpm command; a second spec waits for the first to finish. -- **No version picker** — the spec is typed as pnpm accepts it; the page neither lists registry versions nor offers upgrades. Its refusals and the incompatibility reason on an installed plugin tell the person to upgrade a profile-installed plugin by uninstalling and installing it again, and that a plugin shipped with DSH upgrades with DSH. +- **No version picker** — the spec is typed as pnpm accepts it; the page does not list registry versions. Official on-demand entries offer the [running installation’s target](../../boot/plugin-manager/README.md#use-this-package). Other profile-installed plugins use uninstall and reinstall to change versions; installation-carried bundles upgrade with DSH. - **The registry choice is this browser's** — it lives in `localStorage`, so another browser applies its own initial recommendation; the `dsh plugin` command and the agent tool use the Host's configured registry. - **Every registry read runs pnpm** — opening the dialog, the check, and the install each ask pnpm what its own configuration names; a machine without pnpm reads it as unknown and is offered no fallback. diff --git a/packages/client/ui-plugin-manager/README.zh.md b/packages/client/ui-plugin-manager/README.zh.md index 5a8d5d130c..936cf2ef46 100644 --- a/packages/client/ui-plugin-manager/README.zh.md +++ b/packages/client/ui-plugin-manager/README.zh.md @@ -27,10 +27,12 @@ kind: "package-reference" ## 使用本包 -在侧栏选择**插件**。信息按钮使用 11px 的 caption 色图标和 20px 方形悬停区域,与副标题间隔 4px。它说明当前页用于配置官方插件、安装和管理其他插件,**设置 → 内置插件**则展示内置插件列表及运行状态。悬停或聚焦显示共享 tooltip;点击后保持展开,直至再次点击、Escape、Tab 或外部点击。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。**官方**排在前面,列出安装随附、供开启的组合包——开启前保持关闭、没有卸载、属于实验性功能的带**实验性**标签——其后是注册了配置页的官方插件;**已安装**列出 profile 持有的组合包。卡片按名称排序,启停组合包不会挪动它的卡片。没有组合包 patch 的依赖不是插件,除非 profile 选中了它才会带异常标签列出。全局配置仍在设置的**内置插件**分区中编辑。 +在侧栏选择**插件**。信息按钮使用 11px 的 caption 色图标和 20px 方形悬停区域,与副标题间隔 4px。它说明当前页用于配置官方插件、安装和管理其他插件,**设置 → 内置插件**则展示内置插件列表及运行状态。悬停或聚焦显示共享 tooltip;点击后保持展开,直至再次点击、Escape、Tab 或外部点击。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。**官方**排在前面,列出安装随附的可选组合包与按需目录条目,其后是注册了配置页的官方插件。目录条目在安装或移除后仍留在官方分组;**已安装**列出其他 profile 组合包。安装随附的可选组合包没有卸载操作。实验性包带有**实验性**标签。卡片按名称排序,启停组合包不会挪动它的卡片。没有组合包 patch 的依赖不是插件,除非 profile 选中了它才会带异常标签列出。全局配置仍在设置的**内置插件**分区中编辑。 已安装的组合包及其插件行在卡片和详情页中,按当前界面语言显示各自的标题与描述。每个字段先读取导出的 locale `meta`,缺失时回退到包根插件可访问的 `package.json`,子路径插件从不读取;标题最终使用完整包名或模块名,两处都没有描述时不提供包描述。作者格式见[插件展示元信息](../../../docs/cookbook/adding-a-package.zh.md#plugin-display-metadata)。组合包卡片、详情和组件行显示包根的清单 `icon` 或导出的 `./icon`,或子路径插件导出的 `<子路径>/icon`;未声明或无法解码时保留默认插画。安装预览仍使用注册表或 manifest 信息。 +官方按需卡片在安装前即可离线显示。开启会打开已有的安装进度对话框,安装[当前 Host 提供的目标](../../boot/plugin-manager/README.zh.md#use-this-package)并选择配置层。安装源回退、取消和脚本审批继续使用同一组控件。关闭保留依赖;移除是独立操作。版本或开发链接来源不同会提供**更新**;详情显示当前及所提供的来源和版本。更新保留选择状态,并报告是否需要重启。文件缺失或不可读的包在诊断旁保留记录的依赖来源。已启用的 bundle 在 checkout 目标不可用时仍可关闭。 + 首次读取列表时,页面显示一条较短的「官方」标题占位和四行骨架卡片,沿用真实卡片的行、图标和文字间距,操作区域留白。骨架使用与 Session 搜索相同的透明度呼吸动效,在减少动态效果模式下保持静止。无障碍加载文案和读取时机不变;刷新时保留已有列表,安装及组件加载仍使用各自的状态提示。 **刷新**按钮在悬停或键盘聚焦 500 毫秒后显示提示。手动刷新期间,按钮显示旋转加载指示并保持禁用,直到所有待处理读取结束且至少经过 400 毫秒,避免加载指示一闪而过;重复刷新请求会被忽略,已有列表保持可见。成功不显示 toast。已有列表时,手动刷新失败保留缓存内容并显示失败 toast,即使已离开插件页也能看到;再次选择**刷新**即可重试。在首次成功读取列表之前,失败仍原位显示提示与**重试**。后台读取不显示刷新按钮的加载指示。 @@ -50,7 +52,7 @@ kind: "package-reference" Host 通过普通 fetch 代理向 `https://registry.npmjs.org/-/ping` 和 `https://registry.npmmirror.com/-/ping` 发送 GET 请求,以最先返回 2xx 的源为结果;重定向和失败不参与选择。随后取消另一条请求、丢弃两个响应体,并在清理完成后返回。默认时限为 1500 毫秒,包括不可用结果在内的探测结果均缓存五分钟。在 `ui-plugin-manager` 上配置 `registryProbeTimeoutMs` 和 `registryProbeCacheTtlMs`;`registryProbeEnabled: false` 关闭探测。两条请求都失败或超时时保留现有默认源。不使用 IP 地区服务,也不发送 Session 内容。 -**添加插件**接受包名(可带版本)、Git 地址、压缩包或本地绝对路径;对话框说明包名就是 README 里 `dsh plugin add` 后面的那一段。在包名或自定义安装源输入框中按 Enter 会提交安装请求,但用于确认输入法候选词的 Enter 不会触发安装。输入框下方的**插件安装引导和示例**展开一段引导,给出包名示例;**填入示例**把它填进输入框。输入框中若原样保留 Git 地址或本地路径的模板文本,仍会显示要求替换为实际值的提醒。**安装**按钮上方的提示框在引导收起后也可见,并说明插件安装后暂不支持自动更新:升级需先卸载再安装新版,后续版本会持续改善升级体验。旁边的**安装源**写着安装首先询问的注册表,展开后可选:pnpm 自身的注册表,按它实际指向的源命名——npm 官方源、中国大陆镜像源,其余以主机名显示——Host 读不到时保留中性的默认名称,只列一次,选项标题带上主机名(主机名即名称时不重复显示);Host 配置的每个镜像(`pluginManager.registries`),npmmirror 显示为中国大陆镜像源;以及手动输入的 http(s) 地址。选项使用不透明选择面板,仅悬停行使用底色,主机名不加括号并使用三级文字色。自定义地址区域在悬停时高亮;点击其中任意位置会选中自定义源并聚焦输入框,焦点样式与包名输入框一致。仅通过键盘聚焦不会改变已选安装源;激活自定义单选项或编辑地址才会选中自定义源。Tab 从已选单选项移到地址框,Shift+Tab 反向返回;从任一端移出时关闭选择器,并将焦点交还**安装源**按钮。切到预设源时保留自定义地址草稿,切回后继续使用。提交无效的自定义地址时会阻止安装,展开安装源选择器并聚焦地址框。包名和自定义地址输入框无效时保留红色错误边框,聚焦时加上共享的蓝色内嵌焦点环。选项从控件上浮在对话框之上,展开不会拉长卡片;引导使卡片高过视口时,卡片内容可以滚动。初始选择遵循上文的响应比较规则;之后选择记在本浏览器(`localStorage`)里,下次打开对话框从它开始,Host 不再提供的已记住地址保留为手动输入的地址;在安装源列表返回前开始安装,也仍请求该地址。**安装**先让 Host 在所选注册表读出 spec 指向什么(`pluginManager.inspect`):列表中已有的名字、所有注册表都没有的名字、没有包的路径、没有组合包 patch 的包,或 pnpm 会拒绝的 spec,都以一句话回到输入框下方,spec 保留可继续编辑;所有注册表都连不上时,这句话列出问过的每一个。随后安装从作答的那个注册表开始。通过检查的 spec 打开安装中界面,展示 Host 读到的包名、一句话简介和版本,pnpm 的命令与输出折叠在**查看安装详情**之后。安装完成的画面显示 pnpm 实际安装的版本;只问过一个注册表的运行装入的版本与检查读到的不同时,会写明两个版本,说明差异来自 pnpm 供应链安全策略(`minimumReleaseAge`),提示新发布版本的风险,并给出卸载后应安装的精确 `name@version`。安装完成后提供**立即启用**:启用新组合包、关闭对话框并把列表滚动到它;直接关闭则让它保持已安装但关闭。Host 改问另一个注册表时,安装中界面会说明哪个源没能提供这个包、现在改问哪个,详情里每次 pnpm 运行都带一个写明所用源的标记。安装失败时按 Host 的归因用一行话说明原因——所有安装源都连不上,并列出问过的每一个;GitHub 地址或压缩包链接自身的主机连不上,换源无济于事;包不存在、磁盘已满、profile 不可写、pnpm 拦下了构建脚本——pnpm 输出在详情里,**重试**就在手边,Host 把失败归于所问注册表时还在旁边提供**更换安装源**,回到 spec 输入界面并展开安装源选项;Host 已经把 profile 文件放回原样。pnpm 拦下依赖的安装脚本时,失败界面列出等待允许的包,并以**允许这些脚本并重试**取代**重试**;Host 把授权写进 profile 的 `pnpm-workspace.yaml`(失败的运行保留 pnpm 写入的这个文件)再运行 pnpm,安装完成界面会说明允许了哪些脚本。安装成功不代表模块一定能够激活。 +**添加插件**接受包名(可带版本)、Git 地址、压缩包或本地绝对路径;对话框说明包名就是 README 里 `dsh plugin add` 后面的那一段。在包名或自定义安装源输入框中按 Enter 会提交安装请求,但用于确认输入法候选词的 Enter 不会触发安装。输入框下方的**插件安装引导和示例**展开一段引导,给出包名示例;**填入示例**把它填进输入框。输入框中若原样保留 Git 地址或本地路径的模板文本,仍会显示要求替换为实际值的提醒。**安装**按钮上方的提示框在引导收起后也可见,并说明官方按需插件提供显式更新;其他插件通过卸载后重新安装升级。旁边的**安装源**写着安装首先询问的注册表,展开后可选:pnpm 自身的注册表,按它实际指向的源命名——npm 官方源、中国大陆镜像源,其余以主机名显示——Host 读不到时保留中性的默认名称,只列一次,选项标题带上主机名(主机名即名称时不重复显示);Host 配置的每个镜像(`pluginManager.registries`),npmmirror 显示为中国大陆镜像源;以及手动输入的 http(s) 地址。选项使用不透明选择面板,仅悬停行使用底色,主机名不加括号并使用三级文字色。自定义地址区域在悬停时高亮;点击其中任意位置会选中自定义源并聚焦输入框,焦点样式与包名输入框一致。仅通过键盘聚焦不会改变已选安装源;激活自定义单选项或编辑地址才会选中自定义源。Tab 从已选单选项移到地址框,Shift+Tab 反向返回;从任一端移出时关闭选择器,并将焦点交还**安装源**按钮。切到预设源时保留自定义地址草稿,切回后继续使用。提交无效的自定义地址时会阻止安装,展开安装源选择器并聚焦地址框。包名和自定义地址输入框无效时保留红色错误边框,聚焦时加上共享的蓝色内嵌焦点环。选项从控件上浮在对话框之上,展开不会拉长卡片;引导使卡片高过视口时,卡片内容可以滚动。初始选择遵循上文的响应比较规则;之后选择记在本浏览器(`localStorage`)里,下次打开对话框从它开始,Host 不再提供的已记住地址保留为手动输入的地址;在安装源列表返回前开始安装,也仍请求该地址。**安装**先让 Host 在所选注册表读出 spec 指向什么(`pluginManager.inspect`):列表中已有的名字、所有注册表都没有的名字、没有包的路径、没有组合包 patch 的包,或 pnpm 会拒绝的 spec,都以一句话回到输入框下方,spec 保留可继续编辑;所有注册表都连不上时,这句话列出问过的每一个。随后安装从作答的那个注册表开始。通过检查的 spec 打开安装中界面,展示 Host 读到的包名、一句话简介和版本,pnpm 的命令与输出折叠在**查看安装详情**之后。每条命令行保持单行并可横向滚动;选择文本时可包含屏幕外的参数。安装完成的画面显示 pnpm 实际安装的版本;只问过一个注册表的运行装入的版本与检查读到的不同时,会写明两个版本,说明差异来自 pnpm 供应链安全策略(`minimumReleaseAge`),提示新发布版本的风险,并给出卸载后应安装的精确 `name@version`。安装完成后提供**立即启用**:启用新组合包、关闭对话框并把列表滚动到它;直接关闭则让它保持已安装但关闭。Host 改问另一个注册表时,安装中界面会说明哪个源没能提供这个包、现在改问哪个,详情里每次 pnpm 运行都带一个写明所用源的标记。安装失败时按 Host 的归因用一行话说明原因——所有安装源都连不上,并列出问过的每一个;GitHub 地址或压缩包链接自身的主机连不上,换源无济于事;包不存在、磁盘已满、profile 不可写、pnpm 拦下了构建脚本——pnpm 输出在详情里,**重试**就在手边,Host 把失败归于所问注册表时还在旁边提供**更换安装源**,回到 spec 输入界面并展开安装源选项;Host 已经把 profile 文件放回原样。pnpm 拦下依赖的安装脚本时,失败界面列出等待允许的包,并以**允许这些脚本并重试**取代**重试**;Host 把授权写进 profile 的 `pnpm-workspace.yaml`(失败的运行保留 pnpm 写入的这个文件)再运行 pnpm,安装完成界面会说明允许了哪些脚本。安装成功不代表模块一定能够激活。 准备和下载期间,**取消安装**会请求 Host 停止运行并等待确认。加载组合包的阶段不可取消。点击 ×、按 Escape 或点击遮罩会立即隐藏对话框,并在可以取消时请求取消。**查看安装任务**会重新打开同一任务并保留输出;结果待定或尚未确认时不能发起另一项安装。确认取消后回到 spec 输入界面并显示 toast;manifest 与 lockfile 已恢复,已下载文件可能保留。安装响应丢失后会请求恢复结果;**核对安装状态**和重连会重试该请求。Host 已无活动请求时,**未能获取安装结果**允许检查插件列表后返回编辑。早于接收确认的取消请求会等待并自动重试;取消失败可手动重试。隐藏的任务通过 toast 通知结果,不会重新弹出对话框。 @@ -62,7 +64,7 @@ Host 将网络失败或超时归因于 GitHub 地址,且提供 npmmirror 时 ### 切换组合包里的一行 -组合包页面上行的开关调用 `pluginManager.setPluginEnabled`,往 profile 的 `cordis.patch.yml` 写入该行的 `disabled` 覆盖。启用了 HMR 的 profile 的树随即重组,该行的宿主半区卸下或挂上,组合包其余部分照常运行,页面无需重载即跟随客户端模块图。行使用共享状态标记表示 Host fiber 阶段:pending 与 disabled 为 idle,loading 与 unloading 为 ongoing,active 为 done,failed 为 error。开关只出现在已打开的组合包上;没有存活条目的行,以及 Host 不通过 profile patch 寻址的行,带着 Host 的原因锁定。超过十行的列表带一个按本地化标题、描述、行 id 和模块名筛选的输入框。 +组合包页面上行的开关调用 `pluginManager.setPluginEnabled`,往 profile 的 `cordis.patch.yml` 写入该行的 `disabled` 覆盖。启用了 HMR 的 profile 的树随即重组,该行的宿主半区卸下或挂上,组合包其余部分照常运行,页面无需重载即跟随客户端模块图。行使用共享状态标记表示 Host fiber 阶段:pending 与 disabled 为 idle,loading 与 unloading 为 ongoing,active 为 done,failed 为 error。开关只出现在已打开的组合包上;没有存活条目的行,以及 Host 不通过 profile patch 寻址的行,带着 Host 的原因锁定。限定到 preset 的行在声明 id 旁显示 preset 目标及当前组合状态;其控件和配置页面保持只读,并指引用户通过所属预设或组合包进行配置。不同 preset 中相同的子条目 id 在列表和导航中保留不同标识;详情扩展收到的行引用也携带同样的 preset 标识。启用表达式尚未求值的 preset 行显示「取决于条件」,不计入关闭数量,也不显示开关。超过十行的列表带一个按本地化标题、描述、行 id 和模块名筛选的输入框。 ### 配置页 @@ -160,7 +162,7 @@ Host 入口通过生成的 Remote 接口暴露 `pluginRegistryProbe.fastest()` - **只管理组合包**——没有组合包 patch 的依赖在安装前就被拒绝;profile 里已有的这类依赖不上页面,除非 profile 选中了它;加载普通插件模块仍是文件操作。 - **行只显示阶段,不显示原因**——失败的行只显示为失败,没有 Host 的错误文本;Host 日志里有。 - **一次只能安装一个**——对话框一次运行一个 pnpm 命令;第二个 spec 要等前一个完成。 -- **没有版本选择器**——spec 按 pnpm 接受的写法输入;页面不列出注册表版本,也不提供升级。拒绝提示和已安装插件的不兼容原因会告诉用户:profile 安装的插件通过卸载后重新安装来升级,随 DSH 提供的插件随 DSH 升级。 +- **没有版本选择器**——spec 按 pnpm 接受的写法输入;页面不列出注册表版本。官方按需条目提供[当前安装对应的目标](../../boot/plugin-manager/README.zh.md#use-this-package)。其他 profile 插件通过卸载后重新安装更改版本;安装随附的组合包随 DSH 升级。 - **安装源选择只属于本浏览器**——它存在 `localStorage` 里,所以另一个浏览器会独立计算初始推荐;`dsh plugin` 命令和 agent 工具使用 Host 配置的注册表。 - **每次读注册表都要运行 pnpm**——打开对话框、检查、安装各问一次 pnpm 自身配置指向哪里;没有 pnpm 的机器读作未知,不提供备选。 diff --git a/packages/client/ui-plugin-manager/package.json b/packages/client/ui-plugin-manager/package.json index 28398e793f..696e1a2f6c 100644 --- a/packages/client/ui-plugin-manager/package.json +++ b/packages/client/ui-plugin-manager/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-plugin-manager", "description": "Plugin management for the dsh web client: the sidebar Plugins panel installs, enables, disables, retries, and composes installed plugin packages", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -64,7 +64,7 @@ "@deepseek-ai/dsh-client-ui-sidebar": "workspace:*", "@deepseek-ai/dsh-client-ui-slots": "workspace:*", "@deepseek-ai/dsh-package-manifest": "workspace:*", - "@testing-library/react": "^16.1.0", + "@testing-library/react": "^16.3.3", "@types/react": "~18.3.1", "@types/react-dom": "~18.3.0", "clsx": "^2.1.1", diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx index 1c1f742537..afd254073f 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx @@ -26,7 +26,7 @@ import type { createNavigationStore } from './navigation-store.ts' import { rowConfigKey, type OfficialItem } from './config-ledger.ts' import { INSTALL_GIT_EXAMPLE, INSTALL_PATH_EXAMPLE, type PluginManagerLocaleKey } from './locales.ts' import { - asksMirror, githubRecoveryRegistry, isInstallPending, offeredRegistries, rowKey, + asksMirror, githubRecoveryRegistry, isInstallPending, offeredRegistries, rowKey, packageRowKey, type InstallInputError, type InstallState, type InstallSubject, type PackageRow, type PackageView, type PluginManagerFace, type RegistryChoice, } from './manager-store.ts' @@ -129,7 +129,7 @@ const PHASE_STATES = { /** The count line over a pack's components: the total, then only the states that occur. */ function partsSummary(rows: readonly PackageRow[], t: Translate): string { const failed = rows.filter(row => row.phase === 'failed').length - const off = rows.filter(row => !row.enabled).length + const off = rows.filter(row => !row.enabled && !row.conditional).length const running = rows.filter(row => row.enabled && row.phase === 'active').length return [ t('partsCountTotal', { count: String(rows.length) }), @@ -195,6 +195,7 @@ function RowSwitch({ row, title, t, busy, onChange }: { readonly busy: boolean readonly onChange: (enabled: boolean) => void }): ReactNode { + if (row.conditional) return null const locked = row.readOnlyReason !== undefined || row.entryId === undefined return ( ({ row, ...rowText(row, resolveText) })) const shown = query === '' ? localized : localized.filter(({ row, title, description }) => - [title, description, row.rowId, row.moduleName].some(value => value?.toLowerCase().includes(query))) + [title, description, row.rowId, row.preset, row.moduleName].some(value => value?.toLowerCase().includes(query))) return (
@@ -268,10 +270,10 @@ function RowsSection({ rows, t, resolveText, toggle, configure }: {
    {shown.map(({ row, title, description }) => (
  • @@ -285,7 +287,7 @@ function RowsSection({ rows, t, resolveText, toggle, configure }: { ) : {title}} {description === undefined ? null : {description}} - {title === row.rowId ? null : {row.rowId}} + {title === row.rowId && row.preset === undefined ? null : {row.preset === undefined ? row.rowId : `${row.preset}/${row.rowId}`}} {title === row.moduleName ? null : {row.moduleName}}
    @@ -308,21 +310,17 @@ function RowsSection({ rows, t, resolveText, toggle, configure }: { ) } -/** - * Where a bundle comes from: the spec that installs it elsewhere, or built in - * for one whose loaded copy the installation supplies, and its version. A - * selected bundle that neither the profile nor the installation holds has no - * section. - */ +/** Declared install source and versions; unavailable files retain their recorded dependency spec. */ function SourceSection({ pkg, t }: { readonly pkg: PackageView; readonly t: Translate }): ReactNode { - if (pkg.source === undefined && !pkg.installed && !pkg.optional) return null + if (pkg.source === undefined && !pkg.installed && pkg.availability === 'installation' && !pkg.optional) return null return (

    {t('sourceTitle')}

    {t('sourceSpec')}
    -
    {pkg.source === undefined ? t('sourceBuiltIn') : {pkg.source}}
    +
    {pkg.source !== undefined ? {pkg.source} : pkg.availability === 'missing' + ? t(pkg.installed ? 'statusMissingFiles' : 'sourceNotInstalled') : t('sourceBuiltIn')}
    {pkg.version === undefined ? null @@ -332,6 +330,18 @@ function SourceSection({ pkg, t }: { readonly pkg: PackageView; readonly t: Tran
    {pkg.version}
)} + {pkg.installTarget === undefined ? null : ( + <> +
+
{t('sourceTargetSpec')}
+
{pkg.installTarget.spec}
+
+
+
{t('sourceTargetVersion')}
+
{pkg.installTarget.version}
+
+ + )}
) @@ -352,7 +362,7 @@ function EnableSwitch({ pkg, title, t, busy, onSetEnabled }: { @@ -446,6 +456,13 @@ function DetailTop({ crumbLabel, crumbText, onBack, icon, actions }: { ) } +function updateAvailable(pkg: PackageView): boolean { + const target = pkg.installTarget + if (!pkg.installed || pkg.availability === 'installation' || target === undefined) return false + return pkg.version !== target.version + || ((target.spec.startsWith('link:') || pkg.source?.startsWith('link:') === true) && pkg.source !== target.spec) +} + /** One package as a card that opens its page: its name, its one-liner, its tags, and its bundle switch. */ function PackageCard({ pkg, t, resolveText, busy, highlighted, onOpen, onSetEnabled }: { readonly pkg: PackageView @@ -474,6 +491,8 @@ function PackageCard({ pkg, t, resolveText, busy, highlighted, onOpen, onSetEnab <> {beta ? {t('statusBeta')} : null} {status === 'problem' ? {t('statusProblem')} : null} + {pkg.availability === 'missing' ? {t(pkg.installed ? 'statusMissingFiles' : 'statusNotInstalled')} : null} + {updateAvailable(pkg) ? {t('statusUpdateAvailable')} : null} )} description={description} @@ -503,7 +522,8 @@ function ItemCard({ item, t, onOpen, renderSlot }: { /** One row as the detail slots see it. */ function rowRef(row: PackageRow): PluginRowRef { - return { rowId: row.rowId, moduleName: row.moduleName, enabled: row.enabled } + return { rowId: row.rowId, ...row.preset === undefined ? {} : { preset: row.preset }, + moduleName: row.moduleName, enabled: row.enabled } } /** One bundle as the detail slots see it. */ @@ -512,6 +532,8 @@ function packageRef(pkg: PackageView): PluginPackageRef { name: pkg.name, ...pkg.version === undefined ? {} : { version: pkg.version }, installed: pkg.installed, + availability: pkg.availability, + official: pkg.official, enabled: pkg.enabled, rows: pkg.rows.map(rowRef), } @@ -610,7 +632,7 @@ function RowDetail({ pkg, row, t, resolveText, onBack, renderSlot, form }: { */ function PackageDetail({ pkg, t, resolveText, busy, rowBusy, configured, configure, renderSlot, - onBack, onSetEnabled, onUninstall, onSetRowEnabled, + onBack, onSetEnabled, onUninstall, onUpdate, onSetRowEnabled, }: { readonly pkg: PackageView readonly t: Translate @@ -625,6 +647,7 @@ function PackageDetail({ readonly onBack: () => void readonly onSetEnabled: (enabled: boolean) => void readonly onUninstall: () => void + readonly onUpdate: () => void readonly onSetRowEnabled: (row: PackageRow, enabled: boolean) => void }): ReactNode { const { title, description, beta } = packageText(pkg, resolveText) @@ -640,6 +663,12 @@ function PackageDetail({ actions={(
{renderSlot('plugins.detail.actions', { subject })} + {updateAvailable(pkg) ? ( + + ) : null} {pkg.installed || pkg.removable ? (

{pkg.name}

@@ -681,13 +712,13 @@ function PackageDetail({ ) : null} - busy || rowBusy(row), onSetEnabled: onSetRowEnabled } : undefined} configure={configure} - /> + />)} {renderSlot('plugins.detail.section', { subject })}
@@ -701,6 +732,7 @@ const INSTALL_TERMINAL_LINES = 12 /** The install terminal's display copy, from the tab's dictionary. */ function terminalLabels(t: Translate): TerminalBlockLabels { return { + commandLine: line => t('terminalCommandLine', { n: String(line) }), /* v8 ignore next -- the Host reports a killed pnpm as a null exit code, never a signal name; the label interface needs one */ signal: signal => t('terminalSignal', { signal }), exitCode: code => t('terminalExitCode', { code: String(code) }), @@ -1298,7 +1330,7 @@ function InstallDialog({ : null} {phase !== 'done' ? null - : install.installed !== null + : install.installed !== null && install.subject?.selection === undefined ? : } @@ -1363,14 +1395,14 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode { // selected name the Host cannot read; the installation's other bundles are inspected in the Settings // Plugins section's Plugin list tab. const listed = state.packages.filter(pkg => !BUILTIN_PROFILE_BUNDLES.has(pkg.name) - && (pkg.installed || pkg.optional || pkg.error !== undefined)) - const mine = listed.filter(pkg => pkg.installed || !pkg.optional) - const official = listed.filter(pkg => pkg.optional && !pkg.installed) + && (pkg.installed || pkg.official || pkg.error !== undefined)) + const mine = listed.filter(pkg => !pkg.official) + const official = listed.filter(pkg => pkg.official) const loaded = state.status === 'ready' || state.status === 'error' const refreshing = state.refreshStatus === 'refreshing' const openPkg = view.kind === 'package' || view.kind === 'row' ? listed.find(pkg => pkg.name === view.name) : undefined const openItem = view.kind === 'item' ? ledger.items.find(item => item.id === view.id) : undefined - const openRow = view.kind === 'row' && openPkg !== undefined ? openPkg.rows.find(row => row.rowId === view.rowId) : undefined + const openRow = view.kind === 'row' && openPkg !== undefined ? openPkg.rows.find(row => packageRowKey(row) === view.rowId) : undefined const showsCards = openPkg === undefined && openItem === undefined const activated = listed.find(pkg => pkg.name === activation && pkg.enabled && !state.busy.includes(pkg.name)) const setRowEnabled = (row: PackageRow, enabled: boolean): void => { @@ -1378,16 +1410,18 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode { if (row.entryId !== undefined) props.setRowEnabled(row.entryId, enabled) } const configure = (pkg: PackageView): RowConfigure => ({ - has: row => ledger.rows.has(rowConfigKey(pkg.name, row.rowId)), - open: (row) => { setView({ kind: 'row', name: pkg.name, rowId: row.rowId }) }, + has: row => row.preset === undefined && ledger.rows.has(rowConfigKey(pkg.name, row.rowId)), + open: (row) => { setView({ kind: 'row', name: pkg.name, rowId: packageRowKey(row) }) }, }) + const packageBusy = (pkg: PackageView): boolean => state.busy.includes(pkg.name) + || (state.install.subject?.name === pkg.name && (isInstallPending(state.install.phase) || state.install.phase === 'checking')) const packageCard = (pkg: PackageView): ReactNode => ( { setActivation(null); setView({ kind: 'package', name: pkg.name }) }} onSetEnabled={(enabled) => { setActivation(enabled ? pkg.name : null); props.setEnabled(pkg.name, enabled) }} @@ -1492,7 +1526,7 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode { pkg={openPkg} t={t} resolveText={resolveText} - busy={state.busy.includes(openPkg.name)} + busy={packageBusy(openPkg)} rowBusy={row => row.entryId !== undefined && state.busy.includes(rowKey(row.entryId))} configured={ledger.bundles.has(openPkg.name)} configure={configure(openPkg)} @@ -1500,6 +1534,7 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode { onBack={() => { setView({ kind: 'list' }) }} onSetEnabled={(enabled) => { props.setEnabled(openPkg.name, enabled) }} onUninstall={() => { props.uninstall(openPkg.name) }} + onUpdate={() => { props.update(openPkg.name) }} onSetRowEnabled={setRowEnabled} /> ) diff --git a/packages/client/ui-plugin-manager/src/client/locales.ts b/packages/client/ui-plugin-manager/src/client/locales.ts index 5c6962ab29..79e889931f 100644 --- a/packages/client/ui-plugin-manager/src/client/locales.ts +++ b/packages/client/ui-plugin-manager/src/client/locales.ts @@ -58,6 +58,7 @@ export const zh = { backToPackage: '返回 {name}', configureRow: '配置 {name}', rowStateIdle: '未运行', + rowStateConditional: '取决于条件', uninstall: '卸载', uninstallLabel: '卸载 {name}', installTitle: '添加插件', @@ -75,7 +76,7 @@ export const zh = { installGuideFill: '填入示例', installGuideFillAria: '填入示例 {example}', installGuideSafety: '请确认插件来源可信。插件在本机以你的权限运行,来源不明的插件可能损坏 DeepSeek Harness,或读取和泄露你的数据。', - installUpgradeNotice: '插件安装后,暂不支持自动更新。若需升级,请先卸载再安装新版,后续版本会持续改善升级体验。', + installUpgradeNotice: '按需安装的官方插件会在版本与 DSH 不同时提供“更新”。其他插件请先卸载再安装新版。', registryToggle: '安装源', registryLegend: '从哪个 npm 源下载插件', registryDefault: '默认安装源', @@ -100,6 +101,14 @@ export const zh = { installProblemUnknown: '无法获取插件信息:{reason}', installingTitle: '插件安装中…', installedTitle: '已安装', + statusNotInstalled: '未安装', + statusMissingFiles: '包不可用', + statusUpdateAvailable: '可更新', + sourceNotInstalled: '未安装', + sourceTargetSpec: '当前 DSH 对应来源', + sourceTargetVersion: '当前 DSH 对应版本', + update: '更新', + updateLabel: '更新 {name}', installFailedTitle: '插件安装失败', installGithubFailedTitle: '无法访问 GitHub', installGithubTimeoutTitle: '连接 GitHub 超时', @@ -156,6 +165,7 @@ export const zh = { installFailureTimeout: '安装超时', installFailurePnpmMissing: '没有找到 pnpm,无法安装', installFailureGeneric: '安装过程中出错,原因见安装详情', + terminalCommandLine: '命令第 {n} 行', terminalRunning: '运行中', terminalFailed: '失败', terminalDone: '已完成', @@ -191,6 +201,7 @@ export const zh = { failedRowDisable: '组件停用失败:{reason}', reasonManagementRequired: '插件管理所需,不能停用或卸载', reasonUnaddressable: '当前 profile 的 patch 无法唯一定位这一项', + reasonPresetManaged: '通过所属预设或组合包配置此项', reasonUnknownPlugin: '找不到该插件', reasonInvalidSpec: '请输入有效的包名或地址', reasonAmbiguousInstall: '无法从依赖变更中确定安装了哪一个包', @@ -262,6 +273,7 @@ export const en = { backToPackage: 'Back to {name}', configureRow: 'Configure {name}', rowStateIdle: 'Not running', + rowStateConditional: 'Conditional', uninstall: 'Uninstall', uninstallLabel: 'Uninstall {name}', installTitle: 'Add plugin', @@ -279,7 +291,7 @@ export const en = { installGuideFill: 'Use example', installGuideFillAria: 'Use the example {example}', installGuideSafety: 'Install only plugins you trust: they run with your permissions and can damage DeepSeek Harness or leak your data.', - installUpgradeNotice: 'Installed plugins do not update automatically yet. To upgrade a plugin, uninstall it and install the new version. Later releases will keep improving the upgrade experience.', + installUpgradeNotice: 'On-demand Official plugins offer Update when their version differs from DSH. To update other plugins, uninstall and reinstall them.', registryToggle: 'Registry', registryLegend: 'The npm registry the plugin is downloaded from', registryDefault: 'Default registry', @@ -304,6 +316,14 @@ export const en = { installProblemUnknown: 'The plugin could not be looked up: {reason}', installingTitle: 'Installing the plugin…', installedTitle: 'Installed', + statusNotInstalled: 'Not installed', + statusMissingFiles: 'Package unavailable', + statusUpdateAvailable: 'Update available', + sourceNotInstalled: 'Not installed', + sourceTargetSpec: 'Source for this DSH', + sourceTargetVersion: 'Version for this DSH', + update: 'Update', + updateLabel: 'Update {name}', installFailedTitle: 'The plugin could not be installed', installGithubFailedTitle: 'Cannot access GitHub', installGithubTimeoutTitle: 'GitHub connection timed out', @@ -360,6 +380,7 @@ export const en = { installFailureTimeout: 'The install timed out', installFailurePnpmMissing: 'pnpm was not found, so nothing can be installed', installFailureGeneric: 'Something went wrong during the install; the details say what', + terminalCommandLine: 'Command line {n}', terminalRunning: 'Running', terminalFailed: 'Failed', terminalDone: 'Done', @@ -395,6 +416,7 @@ export const en = { failedRowDisable: 'Could not disable the component: {reason}', reasonManagementRequired: 'Plugin management needs it; it cannot be switched off or uninstalled.', reasonUnaddressable: 'The profile patch cannot address this one uniquely.', + reasonPresetManaged: 'Configure this item through its preset or bundle.', reasonUnknownPlugin: 'No such plugin.', reasonInvalidSpec: 'Enter a valid package name or address.', reasonAmbiguousInstall: 'Which package was installed cannot be told from the dependency change.', diff --git a/packages/client/ui-plugin-manager/src/client/manager-store.ts b/packages/client/ui-plugin-manager/src/client/manager-store.ts index abef905427..69073f37d3 100644 --- a/packages/client/ui-plugin-manager/src/client/manager-store.ts +++ b/packages/client/ui-plugin-manager/src/client/manager-store.ts @@ -62,14 +62,18 @@ export type ManagerNotice = export interface PackageRow { /** The Loader entry carrying the row while the bundle is on; absent for a row of a bundle that is off. */ readonly entryId?: PluginEntryId - /** The row id as the bundle declares it. */ + /** Declared row id, or an internal identity for an anonymous scoped insertion. */ readonly rowId: string + /** Outer preset row id for a scoped, read-only contribution. */ + readonly preset?: string /** The module the row names. */ readonly moduleName: string /** Local package display text and metadata diagnostics supplied by the Host. */ readonly meta?: PluginLocalizedMeta /** Whether the entry runs; false for a row without a live entry. */ readonly enabled: boolean + /** Whether a preset expression has not yet resolved this row's enablement. */ + readonly conditional?: boolean /** The entry's fiber phase, null without a live fiber. */ readonly phase: PluginInfo['fiberPhase'] /** Why the Host refuses to switch the row, when it does. */ @@ -79,11 +83,17 @@ export interface PackageRow { /** One bundle as the page shows it: the Host's bundle joined with the entries its rows run as. */ export interface PackageView { readonly name: string + /** Official catalog identity is independent of installation and selection. */ + readonly official: boolean + /** Readable package location, separately from the profile dependency declaration. */ + readonly availability: BundleInfo['availability'] + /** Host-derived exact version offered for installation or an explicit update. */ + readonly installTarget?: NonNullable readonly version?: string readonly description?: string /** Local package display text and metadata diagnostics supplied by the Host. */ readonly meta?: PluginLocalizedMeta - /** Whether the profile's own dependencies hold the package; false for a bundle the installation supplies. */ + /** Whether the profile declares this dependency; availability separately describes its readable files. */ readonly installed: boolean /** Present for a profile dependency the installation does not also supply: the spec `pnpm add` accepts. */ readonly source?: string @@ -101,7 +111,13 @@ export interface PackageView { } /** The typed spec as the Host read it, on the installing, installed, and failed screens. */ -export type InstallSubject = Extract & { readonly spec: string } +export type InstallSubject = Extract & { + readonly spec: string + /** Present for a catalog install or update: desired selection after installation. */ + readonly selection?: boolean + /** Save an exact dependency version through the generic installer. */ + readonly saveExact?: boolean +} /** The registry the person picked for an install: one the Host offers, or a typed URL. */ export type RegistryChoice = @@ -206,7 +222,7 @@ export interface InstallState { readonly runs: readonly InstallRun[] /** Whether the run's command and output are unfolded, including after reopening. */ readonly detailsOpen: boolean - /** The bundle the finished run added, left off until enabled from the installed screen. */ + /** The bundle the finished run added; ordinary installs await enablement, catalog operations carry their selection. */ readonly installed: string | null /** The version the finished run installed, when its manifest declares one; null otherwise. */ readonly installedVersion: string | null @@ -342,6 +358,8 @@ export interface PluginManagerFace { clearHighlight: () => void /** Put a bundle into, or take it out of, the profile's layer list. */ setEnabled: (packageName: string, enabled: boolean) => void + /** Install the host's catalog version while retaining the current selection. */ + update: (packageName: string) => void /** Ask before removing a package from the profile. */ uninstall: (packageName: string) => void confirm: () => void @@ -405,6 +423,14 @@ export function rowKey(entryId: string): string { return `row:${entryId}` } +/** Identify a bundle row independently of equal child ids in other presets. + * @param row Bundle declaration with its optional preset target. + * @returns Stable identity for list and detail navigation. + */ +export function packageRowKey(row: Pick): string { + return row.preset === undefined ? row.rowId : JSON.stringify([row.preset, row.rowId]) +} + /** * One bundle as the page shows it: its rows joined with the Host's entries. * @param bundle - the Host's bundle. @@ -414,18 +440,24 @@ export function rowKey(entryId: string): string { export function packageView(bundle: BundleInfo, plugins: readonly PluginInfo[]): PackageView { const rows = bundle.rows.map((row): PackageRow => { const live = row.entryId === undefined ? undefined : plugins.find(plugin => plugin.entryId === row.entryId) + const readOnlyReason = row.readOnlyReason ?? live?.readOnlyReason return { rowId: row.rowId, + ...row.preset === undefined ? {} : { preset: row.preset }, moduleName: row.moduleName, - enabled: live?.enabled ?? false, - phase: live?.fiberPhase ?? null, + enabled: row.composition === undefined ? live?.enabled ?? false : row.composition.enabled === true, + phase: row.composition?.fiberPhase ?? live?.fiberPhase ?? null, + ...row.composition?.enabled === 'conditional' ? { conditional: true } : {}, ...row.meta === undefined ? {} : { meta: row.meta }, ...row.entryId === undefined ? {} : { entryId: row.entryId }, - ...live?.readOnlyReason === undefined ? {} : { readOnlyReason: live.readOnlyReason }, + ...readOnlyReason === undefined ? {} : { readOnlyReason }, } }) return { name: bundle.name, + official: bundle.official, + availability: bundle.availability, + ...bundle.installTarget === undefined ? {} : { installTarget: bundle.installTarget }, installed: bundle.installed, optional: bundle.optional, removable: bundle.removable, @@ -459,7 +491,9 @@ const IDLE_INSTALL: InstallState = { /** The dialog back at its spec: the run and its outcome forgotten, the spec and the registries kept. */ function specAgain(install: InstallState): InstallState { const { open, spec, registries, registry, mirrorRecovery } = install - return { ...IDLE_INSTALL, open, spec, registries, registry, ...mirrorRecovery === undefined ? {} : { mirrorRecovery } } + return { ...IDLE_INSTALL, open, spec, registries, registry, + ...install.subject?.selection === undefined ? {} : { subject: install.subject }, + ...mirrorRecovery === undefined ? {} : { mirrorRecovery } } } /** @@ -596,7 +630,8 @@ export class PluginManagerController { const install = this.getSnapshot().install // Typing while the Host checks or installs is not possible; a new spec after an outcome starts over. if (install.phase === 'checking' || isInstallPending(install.phase)) return - this.patchInstall(install.phase === 'idle' ? { spec: text, inputError: null } : { ...specAgain(install), spec: text }) + this.patchInstall(install.phase === 'idle' ? { spec: text, inputError: null, subject: null } + : { ...specAgain(install), spec: text, subject: null }) }, runInstall: () => { void this.runInstall() }, toggleRegistryOptions: () => { this.patchInstall({ registryOpen: !this.getSnapshot().install.registryOpen }) }, @@ -628,12 +663,21 @@ export class PluginManagerController { enableInstalled: () => { void this.enableInstalled() }, clearHighlight: () => { if (this.getSnapshot().highlight !== null) this.patch({ highlight: null }) }, setEnabled: (packageName, enabled) => { + const pkg = this.getSnapshot().packages.find(pkg => pkg.name === packageName) + if (enabled && pkg?.availability === 'missing' && pkg.installTarget !== undefined) { + this.openCatalogInstall(pkg, pkg.installTarget, true) + return + } void this.run(packageName, { packageName, action: enabled ? 'enable' : 'disable' }, async () => { const result = await this.ctx.remote.pluginManager.setBundleEnabled(packageName, enabled) this.applied(result, packageName) if (result.ok && (result.value.application === 'applied' || result.value.application === 'restart-required')) this.trackToggle(packageName, enabled) }) }, + update: (packageName) => { + const pkg = this.getSnapshot().packages.find(pkg => pkg.name === packageName) + if (pkg?.installTarget !== undefined && pkg.availability !== 'installation') this.openCatalogInstall(pkg, pkg.installTarget, pkg.enabled) + }, uninstall: (packageName) => { this.pendingConfirm = () => this.run(packageName, { packageName, action: 'uninstall' }, async () => { this.ctx.get('productAnalytics')?.track('confirm_uninstall_plugin', { plugin_name: packageName }) @@ -832,6 +876,29 @@ export class PluginManagerController { return this.disposed || signal.aborted } + /** Reuse the install dialog with an exact catalog target and an explicit selection outcome. */ + private openCatalogInstall(pkg: PackageView, target: NonNullable, enabled: boolean): void { + if (this.disposed) return + const { phase } = this.getSnapshot().install + if (phase === 'checking' || isInstallPending(phase)) { + this.patchInstall({ open: true }) + return + } + this.abortInspect() + const registry = this.registryMemory.getSnapshot() ?? OFFICIAL_REGISTRY + const local = target.spec.startsWith('link:') + const subject: InstallSubject = { + status: 'accepted', kind: local ? 'path' : 'registry', name: pkg.name, version: target.version, bundle: true, + spec: target.spec, registry: registry.kind === 'custom' ? registry.url : registry.registry, + selection: enabled, ...local ? {} : { saveExact: true }, + } + this.patch({ install: { ...IDLE_INSTALL, open: true, spec: subject.spec, registry, subject } }) + const read: RegistryRead = { choice: registry } + this.registryRead = read + read.done = this.readRegistries(read).finally(() => { delete read.done }) + void this.runInstall() + } + /** * Check the typed spec, then install it. The Host reads what the spec * names first; a refused spec returns to the field with the reason, an @@ -848,9 +915,10 @@ export class PluginManagerController { } // A name the list already shows is refused at once, before the Host is asked. const listed = state.packages.find(pkg => pkg.name === spec) - if (listed !== undefined) { + const catalogSubject = install.subject?.selection === undefined || install.subject.spec !== spec ? undefined : install.subject + if (listed !== undefined && listed.availability !== 'missing' && catalogSubject === undefined) { this.finishAnalytics('failed', 'already-installed') - this.patchInstall({ phase: 'idle', inputError: { problem: listed.installed ? 'already-installed' : 'shipped', reason: spec } }) + this.patchInstall({ phase: 'idle', inputError: { problem: listed.availability === 'installation' ? 'shipped' : 'already-installed', reason: spec } }) return } const choice = install.registry @@ -864,7 +932,7 @@ export class PluginManagerController { const controller = new AbortController() this.inspectAbort = controller this.patchInstall({ - phase: 'checking', inputError: null, subject: null, runs: [], detailsOpen: false, attempts: null, registryOpen: false, + phase: 'checking', inputError: null, subject: catalogSubject ?? null, runs: [], detailsOpen: false, attempts: null, registryOpen: false, installed: null, installedVersion: null, restartRequired: false, failure: null, approvedBuilds: [], }) const read = this.registryRead @@ -876,6 +944,11 @@ export class PluginManagerController { const selected = currentChoice.kind === 'custom' ? { ...currentChoice, url: currentChoice.url.trim() } : currentChoice const registry: Registry = selected.kind === 'custom' ? selected.url : selected.registry this.registryMemory.set(selected) + if (catalogSubject !== undefined) { + this.inspectAbort = undefined + await this.startInstall({ ...catalogSubject, registry }) + return + } const inspected = await this.ctx.remote.pluginManager.inspect(spec, { registry }, controller.signal) if (this.gone(controller.signal)) return this.inspectAbort = undefined @@ -908,7 +981,9 @@ export class PluginManagerController { // the wire, and every such event reads again; those reads must not cancel // the run's settlement. const result = await this.ctx.remote.pluginManager.installBundle(spec, { - enabled: false, requestId, registry, ...approvedBuilds === undefined ? {} : { approvedBuilds: [...approvedBuilds] }, + enabled: subject.selection ?? false, requestId, registry, + ...subject.saveExact === undefined ? {} : { saveExact: subject.saveExact }, + ...approvedBuilds === undefined ? {} : { approvedBuilds: [...approvedBuilds] }, }) if (this.disposed || this.request !== request) return if (!result.ok) { @@ -970,6 +1045,12 @@ export class PluginManagerController { ...asked, }) } + const completed = this.getSnapshot().install + if (completed.phase === 'done' && completed.subject?.selection !== undefined && result.bundle !== undefined) { + this.applied({ ok: true, value: result }, result.bundle) + this.patch({ highlight: result.bundle }) + if (completed.subject.selection) this.trackToggle(result.bundle, true) + } const phase = this.getSnapshot().install.phase if (phase === 'done' || phase === 'failed') this.notifyHiddenInstall(phase) void this.load() @@ -1146,7 +1227,7 @@ export class PluginManagerController { const pluginName = row ? bundle?.rows.find(item => item.entryId === name)?.moduleName : name if (bundle === undefined || pluginName === undefined) return this.ctx.get('productAnalytics')?.track('plugin_toggle', { - plugin_name: pluginName, plugin_type: row ? 'plugin' : 'bundle', is_enabled: enabled, is_builtin: !bundle.installed, + plugin_name: pluginName, plugin_type: row ? 'plugin' : 'bundle', is_enabled: enabled, is_builtin: bundle.availability === 'installation', }) } diff --git a/packages/client/ui-plugin-manager/src/client/presentation.ts b/packages/client/ui-plugin-manager/src/client/presentation.ts index 33998a49ae..6c01da2e7c 100644 --- a/packages/client/ui-plugin-manager/src/client/presentation.ts +++ b/packages/client/ui-plugin-manager/src/client/presentation.ts @@ -48,6 +48,7 @@ function registryHost(registry: string): string { const CODE_KEYS = { 'management-required': 'reasonManagementRequired', 'unaddressable': 'reasonUnaddressable', + 'preset-managed': 'reasonPresetManaged', 'unknown-plugin': 'reasonUnknownPlugin', 'invalid-spec': 'reasonInvalidSpec', 'ambiguous-install': 'reasonAmbiguousInstall', diff --git a/packages/client/ui-plugin-manager/src/client/slot-contract.ts b/packages/client/ui-plugin-manager/src/client/slot-contract.ts index e2dd229e78..f2dce8cc57 100644 --- a/packages/client/ui-plugin-manager/src/client/slot-contract.ts +++ b/packages/client/ui-plugin-manager/src/client/slot-contract.ts @@ -17,6 +17,7 @@ */ import type {} from '@deepseek-ai/dsh-client-ui-slots' +import type { BundleInfo } from '@deepseek-ai/dsh-api-remotes/client' import type { ConfigForm, ConfigFormSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client' /** The view the page asks a configuration entry for. */ @@ -29,8 +30,10 @@ export interface PluginConfigViewProps { /** One row of a bundle as a detail contribution sees it. */ export interface PluginRowRef { - /** The row id as the bundle's patch declares it. */ + /** Declared row id, or an internal identity for an anonymous scoped insertion. */ readonly rowId: string + /** Outer preset row id; absent for a Host row. */ + readonly preset?: string /** The module the row names. */ readonly moduleName: string /** Whether the row's entry runs. */ @@ -43,8 +46,12 @@ export interface PluginPackageRef { readonly name: string /** The installed version, when the Host reports one. */ readonly version?: string - /** Whether the profile's own dependencies hold the package; false for a bundle the installation supplies. */ + /** Whether the profile declares the package dependency. */ readonly installed: boolean + /** Readable package location; missing entries can retain a dependency declaration. */ + readonly availability: BundleInfo['availability'] + /** Whether the installation offers this package in Official. */ + readonly official: boolean /** Whether the bundle is switched on. */ readonly enabled: boolean /** The rows the bundle declares. */ diff --git a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx index 35f5faea8f..6c872c6437 100644 --- a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx +++ b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx @@ -32,6 +32,8 @@ const t = translate(en) function pkg(overrides: Partial = {}): PackageView { return { name: 'dsh-better-sidebar', + official: overrides.optional === true, + availability: overrides.installed === false ? 'installation' : 'profile', version: '0.16.0', installed: true, optional: false, @@ -110,6 +112,7 @@ function renderTab( closeInstall: vi.fn(), editInstallSpec: vi.fn(), runInstall: vi.fn(), + update: vi.fn(), cancelInstall: vi.fn(), reconcileInstall: vi.fn(), toggleInstallDetails: vi.fn(), @@ -289,9 +292,9 @@ describe('PluginManagerPage', () => { it('says where a bundle comes from: the spec that installs it, or built in, with its version', () => { const b = renderTab({ packages: [ pkg({ source: 'github:someone/dsh-better-sidebar' }), - { name: 'dsh-official', installed: false, optional: true, removable: false, enabled: false, rows: [] }, - { name: 'dsh-shadowed', installed: true, optional: false, removable: false, enabled: true, rows: [] }, - { name: 'dsh-missing', installed: false, optional: false, removable: true, enabled: true, error: { code: 'unknown-plugin' }, rows: [] }, + { name: 'dsh-official', installed: false, official: true, availability: 'installation', optional: true, removable: false, enabled: false, rows: [] }, + { name: 'dsh-shadowed', installed: true, official: false, availability: 'profile', optional: false, removable: false, enabled: true, rows: [] }, + { name: 'dsh-missing', installed: false, official: false, availability: 'missing', optional: false, removable: true, enabled: true, error: { code: 'unknown-plugin' }, rows: [] }, ] }) const facts = (): string[] => [...document.querySelectorAll('[data-plugin-source] dt, [data-plugin-source] dd')].map(node => node.textContent) act(() => { b.navigation.actions.setView({ kind: 'package', name: 'dsh-better-sidebar' }) }) @@ -303,7 +306,7 @@ describe('PluginManagerPage', () => { act(() => { b.navigation.actions.setView({ kind: 'package', name: 'dsh-shadowed' }) }) expect(facts()).toEqual([en.sourceSpec, en.sourceBuiltIn]) act(() => { b.navigation.actions.setView({ kind: 'package', name: 'dsh-missing' }) }) - expect(document.querySelector('[data-plugin-source]')).toBeNull() + expect(facts()).toEqual([en.sourceSpec, en.sourceNotInstalled]) }) it('preserves the requested bundle through StrictMode effect replay and page remounts', () => { @@ -729,6 +732,55 @@ describe('PluginManagerPage', () => { } }) + it('shows distinct preset targets and keeps scoped rows read-only despite a matching root configuration page', () => { + const rows: PackageRow[] = ['preset-standard', 'preset-cordis'].map(preset => ({ + rowId: 'shared', preset, moduleName: 'shared-tool', enabled: true, phase: 'active', + readOnlyReason: 'preset-managed', meta: { title: 'shared' }, + })) + const subjects: PluginsSubject[] = [] + const { actions, setLanguage } = renderTab({ packages: [pkg({ rows })] }, { rows: new Set(['dsh-better-sidebar#shared']) }, { + 'plugins.detail.actions:': (_view, owner) => { + const subject = subjectOf(owner) + if (subject !== undefined) subjects.push(subject) + return null + }, + }) + fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') })) + const listed = [...document.querySelectorAll('[data-plugin-row]')] + expect(new Set(listed.map(item => item.getAttribute('data-plugin-row'))).size).toBe(2) + expect(subjects.at(-1)).toMatchObject({ kind: 'bundle', pkg: { rows: [ + { rowId: 'shared', preset: 'preset-standard' }, { rowId: 'shared', preset: 'preset-cordis' }, + ] } }) + expect(screen.getByText('preset-standard/shared')).toBeTruthy() + expect(screen.getByText('preset-cordis/shared')).toBeTruthy() + for (const item of listed) { + const toggle = within(item as HTMLElement).getByRole('switch') + expect(toggle).toHaveProperty('disabled', true) + expect(toggle.getAttribute('title')).toBe(en.reasonPresetManaged) + } + expect(screen.queryByRole('button', { name: en.configureRow.replace('{name}', 'shared') })).toBeNull() + expect(actions.setRowEnabled).not.toHaveBeenCalled() + setLanguage(zh) + for (const item of listed) { + expect(within(item as HTMLElement).getByRole('switch').getAttribute('title')).toBe(zh.reasonPresetManaged) + } + }) + + it('shows an unevaluated preset condition without an Off label or switch', () => { + const rows: PackageRow[] = [{ rowId: 'conditional', preset: 'preset-standard', moduleName: 'tool', + enabled: false, conditional: true, phase: null, readOnlyReason: 'preset-managed' }] + const { setLanguage } = renderTab({ packages: [pkg({ rows })] }) + fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') })) + const item = document.querySelector('[data-plugin-row]')! + expect(within(item).getByText(en.rowStateConditional)).toBeTruthy() + expect(within(item).queryByText(en.partOff)).toBeNull() + expect(within(item).queryByRole('switch')).toBeNull() + expect(item.getAttribute('data-state')).toBeNull() + expect(screen.queryByText(en.partsCountOff.replace('{count}', '1'), { exact: false })).toBeNull() + setLanguage(zh) + expect(within(item).getByText(zh.rowStateConditional)).toBeTruthy() + }) + it('resolves row fields independently from bundle metadata and preserves subpath specifiers', () => { const rows = [ row({ moduleName: '@acme/dsh-sidebar/navigation', meta: { description: { en: 'Navigation description.' } } }), @@ -958,7 +1010,7 @@ describe('PluginManagerPage', () => { // A contribution sees the bundle's facts, not the page's own state. expect(subjects.at(-1)).toEqual({ kind: 'bundle', - pkg: { name: 'dsh-better-sidebar', version: '0.16.0', installed: true, enabled: true, rows: [{ rowId: 'sidebar', moduleName: 'dsh-better-sidebar', enabled: true }] }, + pkg: { name: 'dsh-better-sidebar', version: '0.16.0', installed: true, official: false, availability: 'profile', enabled: true, rows: [{ rowId: 'sidebar', moduleName: 'dsh-better-sidebar', enabled: true }] }, }) fireEvent.click(screen.getByRole('button', { name: en.configureRow.replace('{name}', 'dsh-better-sidebar') })) @@ -979,10 +1031,10 @@ describe('PluginManagerPage', () => { }) it('leaves the version out of a bundle the Host reports none for', () => { - const unversioned: PackageView = { name: 'dsh-better-sidebar', installed: true, optional: false, removable: true, enabled: true, rows: [] } + const unversioned: PackageView = { name: 'dsh-better-sidebar', installed: true, official: false, availability: 'profile', optional: false, removable: true, enabled: true, rows: [] } renderTab({ packages: [unversioned] }, {}, bodies) fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') })) - expect(subjects.at(-1)).toEqual({ kind: 'bundle', pkg: { name: 'dsh-better-sidebar', installed: true, enabled: true, rows: [] } }) + expect(subjects.at(-1)).toEqual({ kind: 'bundle', pkg: { name: 'dsh-better-sidebar', installed: true, official: false, availability: 'profile', enabled: true, rows: [] } }) }) }) @@ -1988,3 +2040,103 @@ it('supplies the accepted entry values and atomic mutation action to a custom pl fireEvent.click(screen.getByText('Save custom')) expect(mutate).toHaveBeenCalledWith([{ op: 'set', path: ['count'], value: 3 }], 7) }) + + +describe('on-demand Official bundles', () => { + const name = '@deepseek-ai/dsh-subagent-codex' + const title = 'Codex subagent' + const catalog = (): PackageView => ({ name, meta: { title }, official: true, availability: 'missing', installed: false, + optional: false, removable: false, enabled: false, rows: [], + installTarget: { spec: `${name}@2.0.0`, version: '2.0.0' }, + }) + + it('keeps one card in Official before installation, after installation, and after removal', () => { + const entry = catalog() + const { actions, set } = renderTab({ packages: [entry] }) + const assertOfficial = () => { + expect(document.querySelectorAll(`[data-plugin-package="${name}"]`)).toHaveLength(1) + expect(document.querySelector('[data-plugin-group="official"]')?.contains(document.querySelector(`[data-plugin-package="${name}"]`))).toBe(true) + expect(document.querySelector('[data-plugin-group="bundles"]')).toBeNull() + } + assertOfficial() + expect(screen.getByText(en.statusNotInstalled)).toBeTruthy() + fireEvent.click(screen.getByRole('switch', { name: en.enableToggle.replace('{name}', title) })) + expect(actions.setEnabled).toHaveBeenCalledWith(name, true) + set({ packages: [{ ...entry, installed: true, availability: 'profile', enabled: true, removable: true, version: '2.0.0' }] }) + assertOfficial() + set({ packages: [entry] }) + assertOfficial() + }) + + it.each(['checking', 'starting'] as const)('keeps the catalog switch busy while its installation is %s', (phase) => { + const entry = catalog() + renderTab({ packages: [entry], install: { ...IDLE_INSTALL, phase, subject: { + status: 'accepted', kind: 'registry', name, version: '2.0.0', spec: `${name}@2.0.0`, registry: null, + bundle: true, selection: true, saveExact: true, + } } }) + expect(screen.getByRole('switch', { name: en.enableToggle.replace('{name}', title) }).hasAttribute('disabled')).toBe(true) + }) + + it('offers an explicit update and displays installed and target versions', () => { + const entry = { ...catalog(), installed: true, availability: 'profile' as const, removable: true, version: '1.0.0' } + const { actions } = renderTab({ packages: [entry] }) + expect(screen.getByText(en.statusUpdateAvailable)).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', title) })) + const detail = document.querySelector('[data-plugin-detail]') as HTMLElement + expect(within(detail).getByText('1.0.0')).toBeTruthy() + expect(within(detail).getByText('2.0.0')).toBeTruthy() + fireEvent.click(within(detail).getByRole('button', { name: en.updateLabel.replace('{name}', title) })) + expect(actions.update).toHaveBeenCalledWith(name) + }) + + it.each([undefined, `${name}@1.0.0`])('retains dependency source=%s beside unavailable status and the update action', (source) => { + renderTab({ packages: [{ ...catalog(), ...source === undefined ? {} : { source }, installed: true, removable: true, error: { code: 'operation-error', diagnostic: 'Unreadable package files' } }] }) + expect(screen.getByText(en.statusMissingFiles)).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', title) })) + const detail = document.querySelector('[data-plugin-detail]') as HTMLElement + expect(within(detail).queryByText(en.statusNotInstalled)).toBeNull() + if (source !== undefined) expect(within(detail).getByText(source).tagName).toBe('CODE') + expect(within(detail).queryAllByText(en.statusMissingFiles)).not.toEqual([]) + expect(within(detail).getByRole('button', { name: en.updateLabel.replace('{name}', title) })).toBeTruthy() + }) + + it.each([ + ['link:/current-checkout/codex', `${name}@2.0.0`, true], + ['link:/current-checkout/codex', 'link:/other-checkout/codex', true], + ['link:/current-checkout/codex', 'link:/current-checkout/codex', false], + [`${name}@2.0.0`, 'link:/current-checkout/codex', true], + [`${name}@2.0.0`, `${name}@^2.0.0`, false], + [`${name}@2.0.0`, undefined, false], + ] as const)('offers source replacement from %s with recorded source %s: %s', (spec, source, expected) => { + const entry: PackageView = { ...catalog(), installed: true, availability: 'profile', removable: true, version: '2.0.0', + installTarget: { spec, version: '2.0.0' }, ...source === undefined ? {} : { source } } + renderTab({ packages: [entry] }) + expect(screen.queryByText(en.statusUpdateAvailable) !== null).toBe(expected) + fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', title) })) + const detail = document.querySelector('[data-plugin-detail]') as HTMLElement + expect(within(detail).getByText(en.sourceTargetSpec)).toBeTruthy() + expect(within(detail).getAllByText(spec).every(element => element.tagName === 'CODE')).toBe(true) + expect(within(detail).queryByRole('button', { name: en.updateLabel.replace('{name}', title) }) !== null).toBe(expected) + }) + + it('allows Off while an enabled source bundle has no usable installation target', () => { + const { installTarget: _target, ...entry } = catalog() + const { actions, set } = renderTab({ packages: [{ ...entry, enabled: true, + error: { code: 'operation-error', diagnostic: 'Source package is not built' } }] }) + const toggle = screen.getByRole('switch', { name: en.enableToggle.replace('{name}', title) }) + expect(toggle.hasAttribute('disabled')).toBe(false) + fireEvent.click(toggle) + expect(actions.setEnabled).toHaveBeenCalledWith(name, false) + set({ packages: [{ ...entry, error: { code: 'operation-error', diagnostic: 'Source package is not built' } }] }) + expect(screen.getByRole('switch', { name: en.enableToggle.replace('{name}', title) }).hasAttribute('disabled')).toBe(true) + }) + + it('does not present an absent catalog package as built in or as an empty loaded component list', () => { + renderTab({ packages: [catalog()] }) + fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', title) })) + const detail = document.querySelector('[data-plugin-detail]') as HTMLElement + expect(within(detail).queryByText(en.sourceBuiltIn)).toBeNull() + expect(within(detail).queryByRole('button', { name: en.uninstallLabel.replace('{name}', title) })).toBeNull() + expect(detail.querySelector('[data-plugin-rows]')).toBeNull() + }) +}) diff --git a/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts b/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts index 1a9fc03f8e..90d8e7a10a 100644 --- a/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts +++ b/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts @@ -8,13 +8,15 @@ import type { BundleInfo, ChangeResult, ManagementError, PluginEntryId, PluginIn import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime' import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots' import type { ConfigLedger } from '../src/client/config-ledger.ts' -import { offeredRegistries, packageView, PluginManagerController, rowKey, sortPackages } from '../src/client/manager-store.ts' +import { offeredRegistries, packageView, packageRowKey, PluginManagerController, rowKey, sortPackages } from '../src/client/manager-store.ts' const INCOMPATIBLE = { name: 'dsh-late', version: '2.0.0', runtimeVersion: '0.1.0', peers: { '@deepseek-ai/dsh': '^0.2.0' } } const ROW_ENTRY = 'include:sidebar' as PluginEntryId const BUNDLE: BundleInfo = { name: 'dsh-better-sidebar', + official: false, + availability: 'profile', version: '0.16.0', description: 'A sidebar.', enabled: false, @@ -114,10 +116,23 @@ it('hands a custom page the shared configuration form of its entry', () => { }) describe('packageView', () => { + it('keeps scoped rows read-only with distinct identities and their observed preset state', () => { + const view = packageView({ ...BUNDLE, rows: [ + { rowId: 'shared', preset: 'preset-standard', moduleName: 'tool', readOnlyReason: 'preset-managed', composition: { enabled: true, fiberPhase: 'active' } }, + { rowId: 'shared', preset: 'preset-cordis', moduleName: 'tool', readOnlyReason: 'preset-managed', composition: { enabled: 'conditional', fiberPhase: null } }, + ] }, []) + expect(view.rows).toEqual([ + { rowId: 'shared', preset: 'preset-standard', moduleName: 'tool', enabled: true, phase: 'active', readOnlyReason: 'preset-managed' }, + { rowId: 'shared', preset: 'preset-cordis', moduleName: 'tool', enabled: false, phase: null, conditional: true, readOnlyReason: 'preset-managed' }, + ]) + expect(new Set(view.rows.map(packageRowKey)).size).toBe(2) + expect(packageRowKey({ rowId: 'ordinary' })).toBe('ordinary') + }) + it('joins a bundle with the entries its rows run as', () => { expect(packageView(BUNDLE, PLUGINS)).toEqual({ name: 'dsh-better-sidebar', version: '0.16.0', description: 'A sidebar.', source: 'dsh-better-sidebar@^0.16.0', - installed: true, optional: false, removable: true, enabled: false, + installed: true, official: false, availability: 'profile', optional: false, removable: true, enabled: false, rows: [ { rowId: 'sidebar', moduleName: 'dsh-better-sidebar', entryId: ROW_ENTRY, enabled: true, phase: 'active' }, { rowId: 'theme', moduleName: 'dsh-better-sidebar/theme', enabled: false, phase: null }, @@ -125,13 +140,13 @@ describe('packageView', () => { }) // A row the inventory no longer lists, a protected row, and a bundle the Host cannot read. const protectedBundle: BundleInfo = { - name: '@deepseek-ai/dsh-base', enabled: true, installed: false, optional: false, removable: false, readOnlyReason: 'management-required', + name: '@deepseek-ai/dsh-base', enabled: true, installed: false, official: false, availability: 'installation', optional: false, removable: false, readOnlyReason: 'management-required', error: { code: 'operation-error', diagnostic: 'broken' }, rows: [{ rowId: 'core', moduleName: '@deepseek-ai/dsh-base', entryId: 'include:core' as PluginEntryId }, { rowId: 'gone', moduleName: 'x', entryId: 'include:gone' as PluginEntryId }], overrides: [], } expect(packageView(protectedBundle, PLUGINS)).toEqual({ - name: '@deepseek-ai/dsh-base', installed: false, optional: false, removable: false, enabled: true, readOnlyReason: 'management-required', + name: '@deepseek-ai/dsh-base', installed: false, official: false, availability: 'installation', optional: false, removable: false, enabled: true, readOnlyReason: 'management-required', error: { code: 'operation-error', diagnostic: 'broken' }, rows: [ { rowId: 'core', moduleName: '@deepseek-ai/dsh-base', entryId: 'include:core', enabled: true, phase: 'active', readOnlyReason: 'management-required' }, @@ -143,7 +158,7 @@ describe('packageView', () => { describe('sortPackages', () => { it('orders packages by the short name a person reads, not by the Host order or enablement', async () => { - const plain = { enabled: true, installed: true, optional: false, removable: true, rows: [], overrides: [] } + const plain = { enabled: true, installed: true, official: false, availability: 'profile' as const, optional: false, removable: true, rows: [], overrides: [] } const zeta: BundleInfo = { ...plain, name: 'dsh-zeta' } const alpha: BundleInfo = { ...plain, name: '@acme/dsh-alpha', enabled: false } const views = [zeta, BUNDLE, alpha].map(bundle => packageView(bundle, PLUGINS)) @@ -638,7 +653,7 @@ describe('PluginManagerController', () => { }) it('refuses a spec the list already shows without asking the Host, and words what the Host refused', async () => { - const shipped: BundleInfo = { ...BUNDLE, name: '@deepseek-ai/dsh-official', installed: false, optional: true, removable: false } + const shipped: BundleInfo = { ...BUNDLE, name: '@deepseek-ai/dsh-official', installed: false, official: true, availability: 'installation', optional: true, removable: false } const { plugins, face, state, controller } = bench({ listBundles: vi.fn(() => Promise.resolve(ok([BUNDLE, shipped]))), inspect: vi.fn() @@ -1799,3 +1814,190 @@ it('reports a successful install with only documented result fields', async () = input_value: 'dsh-new', is_success: true, duration: expect.any(Number) as number, plugin_name: 'dsh-new', }]]) }) + + +const CATALOG: BundleInfo = { + name: '@deepseek-ai/dsh-subagent-codex', official: true, availability: 'missing', installed: false, optional: false, + enabled: false, removable: false, rows: [], overrides: [], + installTarget: { spec: '@deepseek-ai/dsh-subagent-codex@2.0.0', version: '2.0.0' }, +} + +it('installs an absent Official entry directly at the host target and enables it through the normal installer', async () => { + const { controller, plugins, face, state } = bench({ listBundles: vi.fn().mockResolvedValue(ok([CATALOG])) }) + await controller.load() + face.setEnabled(CATALOG.name, true) + await vi.waitFor(() => { expect(plugins.installBundle).toHaveBeenCalledOnce() }) + expect(plugins.inspect).not.toHaveBeenCalled() + expect(plugins.installBundle).toHaveBeenCalledWith(CATALOG.installTarget!.spec, { + enabled: true, saveExact: true, requestId: state().install.requestId, registry: null, + }) + expect(state().install.subject).toMatchObject({ selection: true, saveExact: true, version: '2.0.0' }) +}) + +it.each(['before', 'after'] as const)('reports one Official toggle when inventory refresh finishes %s installation', async (order) => { + const installing = deferred>>() + const refreshing = deferred>>() + const installed: BundleInfo = { ...CATALOG, availability: 'profile', installed: true, enabled: true, removable: true, version: '2.0.0' } + const result = ok({ ...APPLIED, bundle: CATALOG.name }) + const listBundles = vi.fn().mockResolvedValue(ok([installed])) + .mockResolvedValueOnce(ok([CATALOG])).mockReturnValueOnce(refreshing.promise) + const { controller, face, state, started, track } = bench({ listBundles, installBundle: vi.fn(() => installing.promise) }) + const reads: Promise[] = [] + onTestFinished(async () => { installing.resolve(result); refreshing.resolve(ok([installed])); await Promise.all(reads) }) + await controller.load() + face.setEnabled(CATALOG.name, true) + await started() + const refresh = controller.load() + reads.push(refresh) + await vi.waitFor(() => { expect(listBundles).toHaveBeenCalledTimes(2) }) + expect(state().packages.find(pkg => pkg.name === CATALOG.name)?.availability).toBe('missing') + if (order === 'before') { + refreshing.resolve(ok([installed])) + await refresh + } + installing.resolve(result) + await vi.waitFor(() => { expect(state().install.phase).toBe('done') }) + expect(track.mock.calls.filter(([event]) => event === 'plugin_toggle')).toEqual([['plugin_toggle', { + plugin_name: CATALOG.name, plugin_type: 'bundle', is_enabled: true, is_builtin: false, + }]]) + refreshing.resolve(ok([installed])) + await refresh + await controller.load() + expect(track.mock.calls.filter(([event]) => event === 'plugin_toggle')).toHaveLength(1) +}) + +it.each([true, false])('updates an Official entry with enabled=%s through the same exact installer', async (enabled) => { + const installed: BundleInfo = { ...CATALOG, availability: 'profile', installed: true, version: '1.0.0', enabled, removable: true } + const { controller, plugins, face, state } = bench({ listBundles: vi.fn().mockResolvedValue(ok([installed])), + installBundle: vi.fn().mockResolvedValue(ok({ ...APPLIED, bundle: CATALOG.name, application: 'restart-required' })), + }) + await controller.load() + face.update(CATALOG.name) + await vi.waitFor(() => { expect(state().install.phase).toBe('done') }) + expect(plugins.inspect).not.toHaveBeenCalled() + expect(plugins.installBundle).toHaveBeenCalledWith(CATALOG.installTarget!.spec, { + enabled, saveExact: true, requestId: state().install.requestId, registry: null, + }) + expect(state().install.restartRequired).toBe(true) +}) + +it('preserves the Official exact target and selection when script approval retries an installation', async () => { + const install = vi.fn().mockResolvedValueOnce(ok({ ...APPLIED, application: 'failed', stage: 'install', + error: { code: 'operation-error' }, pendingBuilds: ['native-provider'], + })).mockResolvedValueOnce(ok({ ...APPLIED, bundle: CATALOG.name })) + const { controller, plugins, face, state } = bench({ listBundles: vi.fn().mockResolvedValue(ok([CATALOG])), installBundle: install }) + await controller.load() + face.setEnabled(CATALOG.name, true) + await vi.waitFor(() => { expect(state().install.phase).toBe('failed') }) + face.approveBuildsAndRetry() + await vi.waitFor(() => { expect(install).toHaveBeenCalledTimes(2) }) + expect(plugins.inspect).not.toHaveBeenCalled() + expect(install).toHaveBeenLastCalledWith(CATALOG.installTarget!.spec, { + enabled: true, saveExact: true, requestId: state().install.requestId, registry: null, approvedBuilds: ['native-provider'], + }) +}) + +it('retains a failed Official install target while changing its registry', async () => { + const { controller, plugins, face, state } = bench({ listBundles: vi.fn().mockResolvedValue(ok([CATALOG])), + installBundle: vi.fn().mockResolvedValueOnce(ok({ ...APPLIED, application: 'failed', stage: 'install', error: { code: 'operation-error' } })) + .mockResolvedValue(ok({ ...APPLIED, bundle: CATALOG.name })), + }) + await controller.load() + face.setEnabled(CATALOG.name, true) + await vi.waitFor(() => { expect(state().install.phase).toBe('failed') }) + face.changeRegistry() + face.chooseRegistry({ kind: 'custom', url: CORP }) + face.runInstall() + await vi.waitFor(() => { expect(plugins.installBundle).toHaveBeenCalledTimes(2) }) + expect(plugins.installBundle).toHaveBeenLastCalledWith(CATALOG.installTarget!.spec, { + enabled: true, saveExact: true, requestId: state().install.requestId, registry: CORP, + }) + expect(plugins.inspect).not.toHaveBeenCalled() + await vi.waitFor(() => { expect(state().install.phase).toBe('done') }) + face.closeInstall() + face.setEnabled(CATALOG.name, true) + await vi.waitFor(() => { expect(plugins.installBundle).toHaveBeenCalledTimes(3) }) + expect(plugins.installBundle).toHaveBeenLastCalledWith(CATALOG.installTarget!.spec, { + enabled: true, saveExact: true, requestId: state().install.requestId, registry: CORP, + }) +}) + +it('keeps Official disablement on the existing selection operation', async () => { + const { controller, plugins, face } = bench({ listBundles: vi.fn().mockResolvedValue(ok([{ ...CATALOG, enabled: true }])) }) + await controller.load() + face.setEnabled(CATALOG.name, false) + await vi.waitFor(() => { expect(plugins.setBundleEnabled).toHaveBeenCalledWith(CATALOG.name, false) }) + expect(plugins.installBundle).not.toHaveBeenCalled() +}) + +it('reopens one Official operation through registry preparation and installation instead of starting duplicates', async () => { + const registries = deferred>>() + const installing = deferred>>() + const { controller, plugins, face, state } = bench({ listBundles: vi.fn().mockResolvedValue(ok([CATALOG])), + registries: vi.fn().mockReturnValue(registries.promise), installBundle: vi.fn().mockReturnValue(installing.promise), + }) + await controller.load() + face.setEnabled(CATALOG.name, true) + expect(state().install.phase).toBe('checking') + face.setEnabled(CATALOG.name, true) + expect(plugins.registries).toHaveBeenCalledOnce() + registries.resolve(ok(REGISTRIES)) + await vi.waitFor(() => { expect(state().install.phase).toBe('starting') }) + face.update(CATALOG.name) + expect(plugins.installBundle).toHaveBeenCalledOnce() + installing.resolve(ok({ ...APPLIED, bundle: CATALOG.name })) + await vi.waitFor(() => { expect(state().install.phase).toBe('done') }) +}) + +it('ignores a stale update target and prevents a disposed tab from beginning an Official install', async () => { + const { controller, plugins, face } = bench({ listBundles: vi.fn().mockResolvedValue(ok([CATALOG, BUNDLE])) }) + await controller.load() + face.update('removed') + face.update(BUNDLE.name) + expect(plugins.installBundle).not.toHaveBeenCalled() + controller.dispose() + face.setEnabled(CATALOG.name, true) + expect(plugins.installBundle).not.toHaveBeenCalled() +}) + + +it('installs a source Official entry as a local path through the ordinary installer', async () => { + const spec = 'link:/development/dsh/packages/subagent/subagent-codex' + const entry = { ...CATALOG, installTarget: { spec, version: '2.0.0' } } + const { controller, plugins, face, state } = bench({ listBundles: vi.fn().mockResolvedValue(ok([entry])) }) + await controller.load() + face.setEnabled(entry.name, true) + await vi.waitFor(() => { expect(state().install.phase).toBe('done') }) + expect(plugins.inspect).not.toHaveBeenCalled() + expect(plugins.installBundle).toHaveBeenCalledWith(spec, { + enabled: true, requestId: state().install.requestId, registry: null, + }) + expect(state().install.subject).toMatchObject({ kind: 'path', spec, selection: true }) + expect(state().install.subject?.saveExact).toBeUndefined() +}) + +it.each([true, false])('replaces a same-version registry package with its source link and preserves enabled=%s', async (enabled) => { + const spec = 'link:/development/dsh/packages/subagent/subagent-codex' + const entry: BundleInfo = { ...CATALOG, installTarget: { spec, version: '2.0.0' }, availability: 'profile', + installed: true, version: '2.0.0', source: `${CATALOG.name}@2.0.0`, enabled, removable: true } + const { controller, plugins, face, state } = bench({ listBundles: vi.fn().mockResolvedValue(ok([entry])), + installBundle: vi.fn().mockResolvedValue(ok({ ...APPLIED, bundle: CATALOG.name, application: 'restart-required' })), + }) + await controller.load() + face.update(entry.name) + await vi.waitFor(() => { expect(state().install.phase).toBe('done') }) + expect(plugins.installBundle).toHaveBeenCalledWith(spec, { enabled, requestId: state().install.requestId, registry: null }) + expect(state().install.subject).toMatchObject({ kind: 'path', spec, selection: enabled }) + expect(state().install.restartRequired).toBe(true) +}) + +it('deselects a source bundle whose target is unavailable without starting installation', async () => { + const { installTarget: _target, ...withoutTarget } = CATALOG + const entry: BundleInfo = { ...withoutTarget, enabled: true, + error: { code: 'operation-error', diagnostic: 'Source package is not built' } } + const { controller, plugins, face } = bench({ listBundles: vi.fn().mockResolvedValue(ok([entry])) }) + await controller.load() + face.setEnabled(entry.name, false) + await vi.waitFor(() => { expect(plugins.setBundleEnabled).toHaveBeenCalledWith(entry.name, false) }) + expect(plugins.installBundle).not.toHaveBeenCalled() +}) diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 7e76a25bf6..06cd120754 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -18,14 +18,14 @@ en: 7bae72c97d0449f5 zh: 4589ecc2b216edc5 /deepseek-ai-dsh-client-ui-primitives/use-this-package/component-catalog: - en: f7df84aad26a788d - zh: 707e05d1491285e1 + en: 66d83132d97e56c6 + zh: fb47139ce1f742fe /deepseek-ai-dsh-client-ui-primitives/use-this-package/controls-and-icons: en: f0e54915b896af5a zh: 2047520fd321be90 /deepseek-ai-dsh-client-ui-primitives/use-this-package/rendering-agent-output: - en: d79e20306593c4bf - zh: 5c0f1100e2cd6fcc + en: 4a1a61bb2d64fe4c + zh: 1b0097fff440c2ef /deepseek-ai-dsh-client-ui-primitives/use-this-package/localizing-copy: en: 4aef65dea11de083 zh: abc22cd7c65cdf38 @@ -42,8 +42,8 @@ en: 8bbfa14c27f3bfd7 zh: 88f7548ac3a114e1 /deepseek-ai-dsh-client-ui-primitives/understand-the-implementation/geometry-and-overflow: - en: 49f6341d81ded00e - zh: cab87dc54a42ba69 + en: e283bf2366b43879 + zh: d089076d7834e853 /deepseek-ai-dsh-client-ui-primitives/further-exploration: en: e82b713cdb2f954b zh: a877f34b148f433f @@ -54,8 +54,8 @@ en: ca75c51c89c2c9b0 zh: de54f3467ef3b9be /deepseek-ai-dsh-client-ui-primitives/known-limitations-and-deferred-work: - en: 2f14f1e9d3239316 - zh: c3fdaafc4c591dff + en: 22fc14aa3a224552 + zh: aa6309e84ad8c2a3 /deepseek-ai-dsh-client-ui-primitives/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 4f7a1ef892..509fdd62fe 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -52,7 +52,7 @@ Check this table before writing a control in a feature package. A plugin cannot | `SegmentedTabs` | Controlled equal-width tabs with a sliding indicator and Left/Right, Home, and End navigation. The caller supplies labels, tab/panel ids, and panel content. | | `Tag` | Read-only capsule badge; `tone` selects one of eight palettes. | | `PathLabel` | Single-line file path with subdued directories, a primary filename, and the full path on hover. Fitting paths align left; clipped paths preserve their suffix with a left-edge fade that updates on path and size changes. | -| `StateDot` | Solid green `done`, amber `warning`, red `error`, and neutral-grey `idle` marks in a 10px slot, plus a tertiary-grey 14px rotating `ongoing` loader whose animations pin to document time zero so every visible loader rotates in phase. `aria-hidden`, so the render site owns the name. `appearance="step"` shows a filled check for completion and a hollow pending circle. | +| `StateDot` | Solid green `done`, amber `warning`, red `error`, and neutral-grey `idle` marks in a 10px slot, plus a tertiary-grey 14px rotating `ongoing` loader whose animations pin to document time zero on mount and on every animation restart, such as a list reorder, so every visible loader rotates in phase. `aria-hidden`, so the render site owns the name. `appearance="step"` shows a filled check for completion and a hollow pending circle. | | `ConnectionIndicator` | Inline connection-recovery control across outage, retry, and recovered states. | | `DisclosureRow` | 24px compact disclosure that lays title and content side by side. The header supplies tertiary at rest and secondary on hover; text and icons inherit it regardless of expandability or open state unless the caller supplies a feature-specific override. Semantic status colors remain explicit. Collapsed rows preview a down chevron on hover, and expanded rows keep an up chevron visible. Memoized with shallow prop comparison; keep callbacks and React-node props stable when their content is unchanged. | | `TextShimmer` | One left-to-right highlight across a row’s title, separators, summary, and suffix, with a 300ms initial delay, a one-second sweep and a 500ms rest. The mask spans the content width, capped by the visible row and tilts 15° from vertical, with a flat peak and soft edges. The moving decoration stays clipped to the row without enlarging its scrollable area. Base text inherits the caller’s color, including hover changes; the theme supplies the translucent highlight. Place icons and chevrons outside `TextShimmer` so only text and separators receive the highlight. A string child uses the same retained text interface; nested `TextShimmer` instances share the outer animation. Composite children render twice while active and must have no effects or element ids. Mark separators with `data-shimmer-decoration` so their fill receives the highlight. `DisclosureRow` forwards `contentClassName` for the text area and `contentLayoutClassName` for its inner layout. The inert decorative copy is excluded from interaction, selection, and accessibility; text updates retain the animation, and reduced-motion mode keeps the static base. | @@ -66,6 +66,7 @@ Check this table before writing a control in a feature package. A plugin cannot | `SettingsFormModel`, `settingsNumberField`, `settingsTextField` | The staged-edit model behind such a page over a settings scope: drafts are staged and written on save, a field is overridden by its presence in the user layer, and a save that did not land keeps its drafts. | | `JsonTree`, `JsonBlock` | Read-only JSON inspection. | | `MarkdownText`, `MarkdownDelegateProvider`, `CodeBlock` | Untrusted GFM with TeX math, owner-delegated HTTP(S) navigation, and highlighted code. `CodeBlock` accepts opt-in `lineNumbers`; copied source excludes the gutter, and `contentRef` exposes its stable source wrapper to an owner that uses it as a scrollport. Set `showHeader={false}` when the owner supplies its own language and copy toolbar. | +| `CommandText` | Selectable command or argument text; only horizontal overflow adds a named keyboard focus target. | | `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, `WebBlock` | The agent-output card matching each tool-result intent. | | `icons/*`, `FishLogo`, `BrandWordmark`, `ReferenceIconRegular`/`ReferenceIconMedium`, `LinkIconRegular`/`LinkIconMedium` | Glyphs and brand marks. The thinking glyph keeps an inset orbit within its 16px view box. Use `LinkIconMedium` for 14px clickable-link categories and known-site marks. | | `PermissionIconReadOnlyRegular`/`Medium`, `PermissionIconWorkspaceWriteRegular`/`Medium`, `PermissionIconFullAccessRegular`/`Medium` | Permission-mode glyphs for read-only, workspace-write, and full-access choices. | @@ -105,7 +106,7 @@ The nearest `MarkdownDelegateProvider` supplies optional `openExternalLink` and `JsonTree` clamps collapsed strings to `collapsedStringLines` (three by default). Expanded strings show raw text, retain sibling commas, and fit within the window and outer scrolling containers. Resize and ancestor-scroll events update that limit. Row copy feedback updates independently of JSON value rendering; pending clipboard writes cannot update a different row or an unmounted tree. -`ImageLightbox` is the shared original-image modal with focus restoration and Escape dismissal. The internal thumbnail renderer receives loading/failure labels from its owner. `HoverCard.inline` keeps a file link in the text flow and uses the shared menu material, keyboard-visible focus, and placement above or below the anchor without covering it. Escape closes an open thumbnail even when focus is elsewhere; subsequent Escape presses reach the owner. `MarkdownDelegateProvider.fileImages` supplies a decoded-path resolver and complete image labels: settled image links gain hover previews, and standalone images gain lightbox activation. Image-only anchors retain their single navigation target. The parser recovers only complete, unescaped, standalone local image references with bare spaces and an unambiguous supported image suffix; code and ambiguous destinations remain literal. +`ImageLightbox` is the shared original-image modal with focus restoration and Escape dismissal. Like `Modal`, its mask leaves the Windows caption unpainted, and its close control sits 20px below the caption. The internal thumbnail renderer receives loading/failure labels from its owner. `HoverCard.inline` keeps a file link in the text flow and uses the shared menu material, keyboard-visible focus, and placement above or below the anchor without covering it. Escape closes an open thumbnail even when focus is elsewhere; subsequent Escape presses reach the owner. `MarkdownDelegateProvider.fileImages` supplies a decoded-path resolver and complete image labels: settled image links gain hover previews, and standalone images gain lightbox activation. Image-only anchors retain their single navigation target. The parser recovers only complete, unescaped, standalone local image references with bare spaces and an unambiguous supported image suffix; code and ambiguous destinations remain literal. ### Localizing copy @@ -160,7 +161,7 @@ While a reply streams, `MarkdownText` parses incrementally: all but the trailing ### Geometry and overflow -The output cards share one geometry model: `white-space: pre` with horizontal scrolling so column-aligned content keeps its alignment, and a head-plus-tail slice behind an expand button past `maxLines` (default 16) so a long body never stretches the card. `TerminalBlock` parses ANSI into React spans with a per-line column buffer for cursor movement, honoring erase-in-line, tab stops, and character width. Hosts opt out of the shared geometry per surface: rebinding `--dsl-terminal-command-whitespace` / `--dsl-terminal-line-whitespace` to `pre-wrap` wraps commands and output in full with no sideways scroll, `maxLines: Infinity` disables the fold for hosts capping height through `--dsl-terminal-output-max-height` instead, `copyText` overrides the copy payload (and keeps the control rendered before any output), and `runStateDot: false` omits the run-state dot when the surrounding row already carries the state, reclaiming its gutter via `--dsl-terminal-gutter`. The banner divider follows the rendered body, so a running card that streams live output separates its command from the text like a settled card. +The output cards share one geometry model: `white-space: pre` with horizontal scrolling so column-aligned content keeps its alignment, and a head-plus-tail slice behind an expand button past `maxLines` (default 16) so a long body never stretches the card. `TerminalBlock` uses `CommandText` to keep each authored command line selectable and horizontally scrollable; the prompt and status stay fixed. Only horizontally overflowing text enters the Tab order as a named group, using the owner’s localized `commandLine(line)` label. Text that fits, including wrapped Jobs commands, adds no Tab stop. Size, text, and font changes update focusability without changing the scroll position. `TerminalBlock` parses ANSI into React spans with a per-line column buffer for cursor movement, honoring erase-in-line, tab stops, and character width. Hosts opt out of the shared geometry per surface: rebinding `--dsl-terminal-command-whitespace` / `--dsl-terminal-line-whitespace` to `pre-wrap` wraps commands and output in full with no sideways scroll, `maxLines: Infinity` disables the fold for hosts capping height through `--dsl-terminal-output-max-height` instead, `copyText` overrides the copy payload (and keeps the control rendered before any output), and `runStateDot: false` omits the run-state dot when the surrounding row already carries the state, reclaiming its gutter via `--dsl-terminal-gutter`. The banner divider follows the rendered body, so a running card that streams live output separates its command from the text like a settled card. @@ -199,6 +200,7 @@ These limits define how the atoms behave at the edges; they are current package - **Known-site marks are a fixed list** — only the named hosts resolve to their own mark, and every other external host keeps the globe; recognizing an arbitrary site would require fetching its icon over the network. - **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it. - **A long highlighted fence retains its complete token DOM** — streaming avoids re-parsing, re-tokenizing, and reconciling the completed prefix, but it does not discard old colors or virtualize token spans. Final DOM cardinality therefore still follows the fence's token count; nested/container fences and a pathological single long line remain on the general tail path. +- **Overlong lines stay uncolored** — a source line of 1,000 or more UTF-16 units renders as one plain run in every highlighted surface, because TextMate scanning cost grows superlinearly with line length and a single generated line can otherwise block the page for seconds. The grammar state skips that line, so a comment or template literal that opens or closes on it mis-colors the following lines. Highlighting still runs synchronously during render, so a file with thousands of ordinary lines costs proportionally more. - **The fish logo is a redrawn approximation** — its font glyph has no exportable vector geometry in the local design data, so a hand-authored recreation stands in until an exact export path exists. - **`Pill` and `Input` have no design source** — both atoms are self-defined; the sidebar search field and view-tab strip that resemble them are consumer-owned compositions, not these atoms. - **No `Active` `StateDot` variant** — the supported states are done, warning, ongoing, error, and idle. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index e3e49703c0..1f458060ed 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -52,7 +52,7 @@ kind: "package-library" | `SegmentedTabs` | 受控的等宽分段标签,支持滑动指示条及左/右方向键、Home、End 导航。调用方提供文案、标签与面板 id,以及面板内容。 | | `Tag` | 只读胶囊徽章;`tone` 选择八种配色之一。 | | `PathLabel` | 单行文件路径:目录使用弱化颜色,文件名使用主色,悬停可查看完整路径。空间足够时靠左显示;溢出时保留尾部并在左侧渐隐,路径或尺寸变化时更新。 | -| `StateDot` | 10px 槽内的绿色 `done`、琥珀色 `warning`、红色 `error`、中性灰色 `idle` 圆点,以及 tertiary 灰色 14px 旋转 `ongoing` loading,其动画固定到文档时间零点,所以所有可见 loading 同相旋转。它是 `aria-hidden` 的,名称由渲染点提供。 `appearance="step"` 以实心勾表示完成、空心圆表示等待。 | +| `StateDot` | 10px 槽内的绿色 `done`、琥珀色 `warning`、红色 `error`、中性灰色 `idle` 圆点,以及 tertiary 灰色 14px 旋转 `ongoing` loading,其动画在挂载时以及每次动画重启(如列表重排)时固定到文档时间零点,所以所有可见 loading 同相旋转。它是 `aria-hidden` 的,名称由渲染点提供。 `appearance="step"` 以实心勾表示完成、空心圆表示等待。 | | `ConnectionIndicator` | 行内连接恢复控件,覆盖断线、重试与已恢复三种状态。 | | `DisclosureRow` | 24px 紧凑折叠行,标题与内容左右排列。标题行默认使用 tertiary,悬停时使用 secondary;文字与图标继承该颜色,不受能否展开及开合状态影响,调用方可提供功能专属覆盖。语义状态色保持显式指定。收起时悬停预览向下箭头,展开后持续显示向上箭头。使用浅层 prop 比较进行 memo;内容未变时,保持回调与 React 节点 prop 的引用稳定。 | | `TextShimmer` | 同一道高光从左到右扫过一行的标题、分隔符、摘要与后缀:首次等待 300ms,扫动一秒,再静止 500ms。遮罩覆盖内容宽度,并限制在可见行宽内,相对垂直方向倾斜 15°,峰值区域平坦,两侧柔和渐隐。移动的装饰层始终裁剪在行内,不会扩大可滚动区域。底色继承调用方的颜色,包括悬停变化;主题提供半透明高光。图标和箭头放在 `TextShimmer` 外,只有文字和分隔符参与扫光。字符串子节点沿用稳定的文字接口;嵌套的 `TextShimmer` 共用外层动画。活动期间组合子节点渲染两次,因此不得带副作用或元素 id。分隔符标记 `data-shimmer-decoration`,让填色参与扫光。`DisclosureRow` 将 `contentClassName` 传给文字区域,将 `contentLayoutClassName` 传给内部布局。惰性装饰副本不参与交互、选区和无障碍访问;文字更新保留动画,减少动态效果模式保留静态底色。 | @@ -66,6 +66,7 @@ kind: "package-library" | `SettingsFormModel`、`settingsNumberField`、`settingsTextField` | 这类页面背后基于设置 scope 的暂存编辑模型:草稿先暂存、保存时写入,字段是否被覆盖看用户层是否含有它,未落地的保存保留草稿。 | | `JsonTree`、`JsonBlock` | 只读 JSON 查看。 | | `MarkdownText`、`MarkdownDelegateProvider`、`CodeBlock` | 不可信 GFM 与 TeX 数学、owner 委托的 HTTP(S) 导航,以及高亮代码。`CodeBlock` 可通过 `lineNumbers` 开启行号;复制的源码不含行号栏,`contentRef` 则向需要把稳定源码包装节点用作滚动区的 owner 提供该节点。调用方提供自己的语言与复制工具栏时,设置 `showHeader={false}`。 | +| `CommandText` | 可选中的命令或参数文本;仅横向溢出时添加具名的键盘聚焦目标。 | | `TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock`、`WebBlock` | 与各类工具结果意图对应的 agent 输出卡片。 | | `icons/*`、`FishLogo`、`BrandWordmark`、`ReferenceIconRegular`/`ReferenceIconMedium`、`LinkIconRegular`/`LinkIconMedium` | 字形与品牌标识。思考图标的轨道在 16px 视口内保留内边距。`LinkIconMedium` 用于 14px 的可点击链接分类及已知站点标记。 | | `PermissionIconReadOnlyRegular`/`Medium`、`PermissionIconWorkspaceWriteRegular`/`Medium`、`PermissionIconFullAccessRegular`/`Medium` | 只读、工作区写入与完全访问选项使用的权限模式图形。 | @@ -105,7 +106,7 @@ kind: "package-library" `JsonTree` 把折叠字符串限制为 `collapsedStringLines` 行(默认三行)。展开后显示原始文本、保留同级逗号,并限制在窗口与外层滚动容器内;尺寸变化和祖先滚动事件会更新此限制。行复制反馈独立于 JSON 值渲染更新;尚未完成的剪贴板写入不会更新另一行或已卸载的树。 -`ImageLightbox` 是共享原图浮层,支持焦点恢复与 Esc 关闭。包内缩略图渲染器由调用方提供加载及失败文案。`HoverCard.inline` 使文件链接保持在文本流内,并使用共享菜单材质、键盘可见焦点,并在锚点上方或下方定位而不遮挡锚点。即使焦点位于其他位置,Esc 也会关闭已打开的缩略图;后续 Esc 按键继续传给 owner。`MarkdownDelegateProvider.fileImages` 提供已解码路径解析器与完整图片文案:消息落定后的图片链接支持悬停预览,独立图片支持点击放大。仅包含图片的链接保留单一导航目标。解析器仅恢复完整、未转义、独立成段且带明确图片扩展名的含裸空格本地图片引用;代码与有歧义的目标保持原文。 +`ImageLightbox` 是共享原图浮层,支持焦点恢复与 Esc 关闭。与 `Modal` 相同,其遮罩不覆盖 Windows 顶栏,关闭按钮位于顶栏下方 20px。包内缩略图渲染器由调用方提供加载及失败文案。`HoverCard.inline` 使文件链接保持在文本流内,并使用共享菜单材质、键盘可见焦点,并在锚点上方或下方定位而不遮挡锚点。即使焦点位于其他位置,Esc 也会关闭已打开的缩略图;后续 Esc 按键继续传给 owner。`MarkdownDelegateProvider.fileImages` 提供已解码路径解析器与完整图片文案:消息落定后的图片链接支持悬停预览,独立图片支持点击放大。仅包含图片的链接保留单一导航目标。解析器仅恢复完整、未转义、独立成段且带明确图片扩展名的含裸空格本地图片引用;代码与有歧义的目标保持原文。 ### 本地化文案 @@ -160,7 +161,7 @@ kind: "package-library" ### 几何与溢出 -输出卡片共享同一套几何模型:`white-space: pre` 并横向滚动,让按列对齐的内容保持对齐;超过 `maxLines`(默认 16)时折叠为头部切片加尾部切片,由展开按钮控制,长正文不会撑高卡片。`TerminalBlock` 把 ANSI 解析为 React span,并带逐行列缓冲处理光标移动,遵循行内擦除、制表位与字符宽度。宿主可按表层选择退出共享几何:把 `--dsl-terminal-command-whitespace` / `--dsl-terminal-line-whitespace` 重绑为 `pre-wrap` 让命令与输出完整换行且不横向滚动;`maxLines: Infinity` 为改用 `--dsl-terminal-output-max-height` 限高滚动的宿主禁用折叠;`copyText` 覆盖复制载荷(并让控件在任何输出出现之前就保持渲染);`runStateDot: false` 在外围行已携带同一状态时省去状态点,并经 `--dsl-terminal-gutter` 收回其落区。横幅分割线跟随渲染出的正文:正在流式输出的 running 卡片像已结束卡片一样把命令与文本分隔开。 +输出卡片共享同一套几何模型:`white-space: pre` 并横向滚动,让按列对齐的内容保持对齐;超过 `maxLines`(默认 16)时折叠为头部切片加尾部切片,由展开按钮控制,长正文不会撑高卡片。`TerminalBlock` 通过 `CommandText` 让每条原始命令行可选中并横向滚动;提示符和状态保持固定。只有横向溢出的文本以具名 group 进入 Tab 顺序,名称使用 owner 的本地化 `commandLine(line)` 标签。能完整显示的文本(包括 Jobs 中已换行的命令)不会添加 Tab 停靠点。尺寸、文本与字体变化会更新可聚焦状态,不改变滚动位置。`TerminalBlock` 把 ANSI 解析为 React span,并带逐行列缓冲处理光标移动,遵循行内擦除、制表位与字符宽度。宿主可按表层选择退出共享几何:把 `--dsl-terminal-command-whitespace` / `--dsl-terminal-line-whitespace` 重绑为 `pre-wrap` 让命令与输出完整换行且不横向滚动;`maxLines: Infinity` 为改用 `--dsl-terminal-output-max-height` 限高滚动的宿主禁用折叠;`copyText` 覆盖复制载荷(并让控件在任何输出出现之前就保持渲染);`runStateDot: false` 在外围行已携带同一状态时省去状态点,并经 `--dsl-terminal-gutter` 收回其落区。横幅分割线跟随渲染出的正文:正在流式输出的 running 卡片像已结束卡片一样把命令与文本分隔开。 @@ -199,6 +200,7 @@ kind: "package-library" - **已知站点标记是固定列表**:只有列名的主机解析为自己的标记,其余外部主机仍使用地球;要识别任意站点需要通过网络抓取它的图标。 - **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。 - **长高亮 fence 会保留完整 token DOM**:流式路径避免重新解析、重新 tokenize 和 reconcile 已完成前缀,但不会丢弃旧颜色或虚拟化 token span。因此最终 DOM 数量仍随 fence 的 token 数增长;嵌套/容器内 fence 与病态的单个超长行仍走通用尾部路径。 +- **超长行不着色**:所有高亮界面中,长度达到 1,000 个 UTF-16 单元的源码行渲染为一段纯文本,因为 TextMate 扫描成本随行长超线性增长,单个生成行否则会阻塞页面数秒。语法状态会跳过该行,因此在该行开始或结束的注释、模板字符串会使后续行着色错误。高亮仍在渲染期间同步执行,因此数千个普通行的文件仍按比例增加开销。 - **鱼形标志是重新绘制的近似版本**:它来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。 - **`Pill` 与 `Input` 没有设计来源**:两个原子组件均自行定义;与其相似的侧边栏搜索字段和视图标签条由消费方组合,不是这些原子组件。 - **`StateDot` 没有 `Active` 变体**:支持的状态为 done、warning、ongoing、error 和 idle。 diff --git a/packages/client/ui-primitives/package.json b/packages/client/ui-primitives/package.json index e08a59f04e..e8a5b84d1a 100644 --- a/packages/client/ui-primitives/package.json +++ b/packages/client/ui-primitives/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-primitives", "description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)", - "version": "0.2.0-rc.2", + "version": "0.2.1-alpha.1", "publishConfig": { "access": "public" }, @@ -33,15 +33,15 @@ "diff": "^9.0.0", "react-dom": "^18.2.0", "anser": "^2.3.5", - "katex": "^0.16.47", + "katex": "^0.18.7", "micromark-core-commonmark": "^2.0.3", "micromark-util-character": "^2.1.1", "micromark-util-classify-character": "^2.0.1", "micromark-util-symbol": "^2.0.1", "micromark-util-types": "^2.0.2", - "shiki": "^4.3.1", + "shiki": "^4.4.3", "@deepseek-ai/dsh-util-workspace-path": "workspace:*", - "@shikijs/langs": "^4.3.1", + "@shikijs/langs": "^4.4.3", "@types/mdast": "^4.0.4", "micromark-factory-space": "^2.0.1", "micromark-extension-math": "^3.1.0", diff --git a/packages/client/ui-primitives/src/CommandText.module.css b/packages/client/ui-primitives/src/CommandText.module.css new file mode 100644 index 0000000000..b6b0a3d816 --- /dev/null +++ b/packages/client/ui-primitives/src/CommandText.module.css @@ -0,0 +1,12 @@ +.text { + display: block; + min-width: 0; + overflow-x: auto; + white-space: var(--dsl-command-whitespace, pre); + overflow-wrap: anywhere; +} + +.text:focus-visible { + outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); + outline-offset: -2px; +} diff --git a/packages/client/ui-primitives/src/CommandText.tsx b/packages/client/ui-primitives/src/CommandText.tsx new file mode 100644 index 0000000000..4670857799 --- /dev/null +++ b/packages/client/ui-primitives/src/CommandText.tsx @@ -0,0 +1,46 @@ +/** Selectable command text with keyboard access while horizontally overflowing. */ +import { useLayoutEffect, useRef, useState } from 'react' +import clsx from 'clsx' +import css from './CommandText.module.css' + +/** Display text and owner-localized identification for a command scrollport. */ +export interface CommandTextProps { + /** Original command or formatted command arguments, without truncation. */ + text: string + /** Accessible name while the text can scroll horizontally. */ + label: string + /** Owner typography and positioning. */ + className?: string | undefined +} + +/** + * Render command text without adding Tab stops for content that fits. + * @param props - Text, localized scrollport name, and owner styling. + * @returns A horizontal text scrollport, named and keyboard-focusable only when overflowing. + */ +export function CommandText({ text, label, className }: CommandTextProps) { + const ref = useRef(null) + const [overflow, setOverflow] = useState(false) + useLayoutEffect(() => { + const element = ref.current + /* v8 ignore next -- the span is rendered unconditionally. */ + if (element === null) return undefined + const measure = () => { setOverflow(element.scrollWidth > element.clientWidth) } + measure() + const observer = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(measure) + observer?.observe(element) + const fonts = 'fonts' in element.ownerDocument ? element.ownerDocument.fonts : undefined + fonts?.addEventListener('loadingdone', measure) + return () => { + observer?.disconnect() + fonts?.removeEventListener('loadingdone', measure) + } + }, [text]) + return ( + {text} + ) +} diff --git a/packages/client/ui-primitives/src/ImageLightbox.module.css b/packages/client/ui-primitives/src/ImageLightbox.module.css index 1907fe930f..4cffa16c36 100644 --- a/packages/client/ui-primitives/src/ImageLightbox.module.css +++ b/packages/client/ui-primitives/src/ImageLightbox.module.css @@ -1,9 +1,17 @@ +/* Full-viewport layer that blocks background clicks, including on the Windows caption. */ .backdrop { position: fixed; inset: 0; z-index: 1000; - display: grid; - place-items: center; +} + +/* Painted area below the Windows caption; like Modal, the caption stays unpainted. */ +.stage { + position: absolute; + inset: var(--dsh-frame-chrome-top, 0px) 0 0; + display: flex; + align-items: center; + justify-content: center; padding: 40px; } @@ -18,7 +26,7 @@ .image { position: relative; max-width: min(100%, 1600px); - max-height: calc(100vh - 80px); + max-height: 100%; object-fit: contain; border-radius: var(--dsw-radius-lg); background: var(--dsw-specific-input-major); @@ -27,7 +35,7 @@ } .close { - position: fixed; + position: absolute; top: 20px; right: 20px; z-index: 1; diff --git a/packages/client/ui-primitives/src/ImageLightbox.tsx b/packages/client/ui-primitives/src/ImageLightbox.tsx index 893ed2fe95..1cb3fe4a8c 100644 --- a/packages/client/ui-primitives/src/ImageLightbox.tsx +++ b/packages/client/ui-primitives/src/ImageLightbox.tsx @@ -54,11 +54,13 @@ export function ImageLightbox({ src, alt, labels, onClose }: { aria-modal="true" aria-label={labels.dialog} > - ))} diff --git a/packages/client/ui-primitives/src/index.ts b/packages/client/ui-primitives/src/index.ts index a76b58d346..7fe8783d6a 100644 --- a/packages/client/ui-primitives/src/index.ts +++ b/packages/client/ui-primitives/src/index.ts @@ -83,6 +83,8 @@ export type { RelativeTime, RelativeTimeUnit } from './relative-time.ts' export { JsonTree } from './JsonTree.tsx' export type { JsonTreeProps, JsonTreeLabels } from './JsonTree.tsx' export { TerminalBlock, DEFAULT_TERMINAL_MAX_LINES } from './TerminalBlock.tsx' +export { CommandText } from './CommandText.tsx' +export type { CommandTextProps } from './CommandText.tsx' export type { TerminalBlockProps, TerminalBlockLabels } from './TerminalBlock.tsx' export { ReadBlock, DEFAULT_READ_MAX_LINES } from './ReadBlock.tsx' export type { ReadBlockProps, ReadBlockLine, ReadBlockLabels } from './ReadBlock.tsx' diff --git a/packages/client/ui-primitives/src/markdown/highlight.ts b/packages/client/ui-primitives/src/markdown/highlight.ts index 0ae164d138..7a54506760 100644 --- a/packages/client/ui-primitives/src/markdown/highlight.ts +++ b/packages/client/ui-primitives/src/markdown/highlight.ts @@ -275,6 +275,16 @@ const regexEngine = createJavaScriptRegexEngine({ }), }) +/** + * Lines at least this many UTF-16 units long render as one uncolored token. + * With the JavaScript regex engine, scanning one JavaScript string-literal + * line costs about 70 ms at 1,000 units, 300 ms at 2,000, 600 ms at 5,000 and + * 2.5 s at 10,000, so this cap keeps one line within a few frames. A skipped + * line leaves the grammar state unchanged: a multi-line construct that opens + * or closes on it colors the following lines as if the line were absent. + */ +const TOKENIZE_MAX_LINE_LENGTH = 1000 + let singleton: HighlighterCore | undefined /** Representative paths through every boot grammar, compiled before user content is timed. */ @@ -386,7 +396,9 @@ export function highlightToHtml(code: string, lang: string | undefined): string const resolved = grammarForHint(lang) if (resolved === undefined) return undefined if (!ensureGrammar(resolved)) return undefined - return highlighter().codeToHtml(code, { lang: resolved, theme: 'css-variables' }) + return highlighter().codeToHtml(code, { + lang: resolved, theme: 'css-variables', tokenizeMaxLineLength: TOKENIZE_MAX_LINE_LENGTH, + }) } /** @@ -492,6 +504,7 @@ export class StreamingHighlightSession { return highlighter().codeToTokensBase(text, { lang: resolved, theme: 'css-variables', + tokenizeMaxLineLength: TOKENIZE_MAX_LINE_LENGTH, ...(this.state === undefined ? {} : { grammarState: this.state }), }) } @@ -586,7 +599,9 @@ export function highlightLines(code: string, lang: string | undefined): Highligh const resolved = grammarForHint(lang) if (resolved === undefined) return undefined if (!ensureGrammar(resolved)) return undefined - const { tokens } = highlighter().codeToTokens(code, { lang: resolved, theme: 'css-variables' }) + const { tokens } = highlighter().codeToTokens(code, { + lang: resolved, theme: 'css-variables', tokenizeMaxLineLength: TOKENIZE_MAX_LINE_LENGTH, + }) // shiki tokenizes `a\nb` into two lines; a trailing newline (`a\n`) adds a // third, empty line the caller's own line array does not carry. Drop that // one terminator line so the two structures stay in step. The explicit diff --git a/packages/client/ui-primitives/tests/code-highlighting.client.spec.tsx b/packages/client/ui-primitives/tests/code-highlighting.client.spec.tsx index 1c2b829a5d..a2595b550c 100644 --- a/packages/client/ui-primitives/tests/code-highlighting.client.spec.tsx +++ b/packages/client/ui-primitives/tests/code-highlighting.client.spec.tsx @@ -4,7 +4,9 @@ import { afterEach, describe, expect, it } from 'vitest' import { cleanup, renderHook, waitFor } from '@testing-library/react' import { languageForPath as sharedLanguageForPath, readLangHintForPath } from '@deepseek-ai/dsh-util-code-language' import { CODE_HIGHLIGHT_EXTENSIONS, languageForPath, useCodeHighlighter } from '../src/code-highlighting.ts' -import { grammarForHint, supportsHighlighting } from '../src/markdown/highlight.ts' +import { + StreamingHighlightSession, grammarForHint, highlightLines, highlightToHtml, supportsHighlighting, +} from '../src/markdown/highlight.ts' afterEach(cleanup) @@ -102,4 +104,24 @@ describe('code highlighting', () => { } expect(hook.result.current('sample')).not.toBeUndefined() }) + + it('leaves a line of 1,000 or more units as one uncolored run while its neighbors still highlight', () => { + const literal = (length: number): string => `const t = "${'a'.repeat(length - 12)}"` + const plain = literal(1000) + const code = ['const before = 1', literal(999), plain, 'const after = 2'].join('\n') + const lines = highlightLines(code, 'javascript') + expect(lines?.map(line => line.length > 1)).toEqual([true, true, false, true]) + expect(lines?.[2]).toEqual([{ text: plain, style: { color: '' } }]) + expect(new StreamingHighlightSession().update(code, 'javascript')?.[2]).toEqual([{ text: plain, style: { color: '' } }]) + expect(highlightToHtml(code, 'javascript')).toContain(`${plain}`) + }) + + it('colors lines after a skipped line identically when streamed and settled', () => { + const code = ['/*', `${'a'.repeat(1000)} */`, 'const after = 2'].join('\n') + const session = new StreamingHighlightSession() + session.update(code.slice(0, code.indexOf('const')), 'javascript') + const streamed = session.update(code, 'javascript') + expect(streamed?.map(line => line.map(span => span.style.color))) + .toEqual(highlightLines(code, 'javascript')?.map(line => line.map(span => span.style.color))) + }) }) diff --git a/packages/client/ui-primitives/tests/command-text.client.spec.tsx b/packages/client/ui-primitives/tests/command-text.client.spec.tsx new file mode 100644 index 0000000000..da67a5ed13 --- /dev/null +++ b/packages/client/ui-primitives/tests/command-text.client.spec.tsx @@ -0,0 +1,80 @@ +// @vitest-environment jsdom +import { act, cleanup, render } from '@testing-library/react' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { CommandText } from '../src/CommandText.tsx' +import { TerminalBlock } from '../src/TerminalBlock.tsx' +import { terminalBlockLabels } from './labels.client.ts' + +const originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts') + +beforeEach(() => { + vi.stubGlobal('ResizeObserver', undefined) + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(100) + vi.spyOn(HTMLElement.prototype, 'scrollWidth', 'get').mockImplementation(function (this: HTMLElement) { + return (this.textContent?.length ?? 0) * 10 + }) +}) + +afterEach(() => { + cleanup() + vi.unstubAllGlobals() + vi.restoreAllMocks() + if (originalFonts === undefined) Reflect.deleteProperty(document, 'fonts') + else Object.defineProperty(document, 'fonts', originalFonts) +}) + +it('names and focuses overflowing text as it changes, without changing its scroll position', () => { + const view = render() + const command = view.getByText('echo ok') + expect(command.hasAttribute('tabindex')).toBe(false) + expect(command.hasAttribute('role')).toBe(false) + expect(command.hasAttribute('aria-label')).toBe(false) + view.rerender() + expect(view.getByRole('group', { name: 'Command line 1' })).toBe(command) + expect(command.tabIndex).toBe(0) + command.scrollLeft = 40 + view.rerender() + expect(command.scrollLeft).toBe(40) + view.rerender() + expect(command.hasAttribute('tabindex')).toBe(false) + expect(command.hasAttribute('aria-label')).toBe(false) +}) + +it('updates focusability after resizing and font loading, and releases both observers', () => { + let resize: (() => void) | undefined + const observe = vi.fn() + const disconnect = vi.fn() + vi.stubGlobal('ResizeObserver', class { + constructor(callback: () => void) { resize = callback } + observe = observe + disconnect = disconnect + }) + const fonts = new EventTarget() + Object.defineProperty(document, 'fonts', { configurable: true, value: fonts }) + const removeListener = vi.spyOn(fonts, 'removeEventListener') + const width = vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(300) + const view = render() + const command = view.getByText('echo wider command') + expect(observe).toHaveBeenCalledWith(command) + expect(command.hasAttribute('tabindex')).toBe(false) + if (resize === undefined) throw new Error('Missing resize observer') + width.mockReturnValue(100) + act(resize) + expect(command.tabIndex).toBe(0) + width.mockReturnValue(300) + act(() => { fonts.dispatchEvent(new Event('loadingdone')) }) + expect(command.hasAttribute('tabindex')).toBe(false) + view.unmount() + expect(disconnect).toHaveBeenCalledOnce() + expect(removeListener).toHaveBeenCalledWith('loadingdone', expect.any(Function)) + width.mockClear() + fonts.dispatchEvent(new Event('loadingdone')) + expect(width).not.toHaveBeenCalled() +}) + +it('adds one named Tab stop for the overflowing line of a multiline command', () => { + const view = render() + const commands = [...view.container.querySelectorAll('[data-command-text]')] + expect(commands.map(command => command.hasAttribute('tabindex'))).toEqual([false, true, false]) + expect(view.getByRole('group', { name: '命令第 2 行' })).toBe(commands[1]) +}) diff --git a/packages/client/ui-primitives/tests/fixtures/markdown-dom/math-edge-cases.settled.txt b/packages/client/ui-primitives/tests/fixtures/markdown-dom/math-edge-cases.settled.txt index 83b008e2d5..5fe261b0f7 100644 --- a/packages/client/ui-primitives/tests/fixtures/markdown-dom/math-edge-cases.settled.txt +++ b/packages/client/ui-primitives/tests/fixtures/markdown-dom/math-edge-cases.settled.txt @@ -12,8 +12,8 @@ #text "\\href{javascript:alert(1)}{unsafe}"