Merge commit '84d329db60462a97ff7de82d8c7bd101676a3f0e' into codex/subprocess-win32-process-primitives

This commit is contained in:
pku-xht
2026-08-19 16:19:10 +08:00
212 changed files with 5352 additions and 642 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md
2026-08-04-configuration-source-ownership.md: c90979ff3ade89c11af7fb9a73d536aa2c1daa12
2026-08-04-configuration-source-ownership.zh.md: 31a813eddf4aefaf4bce1fd9999660abc8032c29
2026-08-04-configuration-source-ownership.md: 2cd09ae2daca2b15657caa18ff210fa178c2999b
2026-08-04-configuration-source-ownership.zh.md: 76625cade4b1034c2327344945f80e3a4fec5bdc
@@ -42,7 +42,7 @@ The launching environment wins because `DEEPSEEK_API_KEY=… dsh`, a CI secret,
**The project the harness is launched in is trusted, by default and without a prompt.** A checkout may carry its own endpoint, its own ordinary variables, and its own key; the key ranks below the managed store, so a key stored through the Models page is never displaced by one a checkout happens to contain. `LaunchEnvironmentSnapshot.getFrom(name, sources)` still searches only the layers a caller names, and omitting one is a refusal rather than a demotion — the mechanism exists for the decisions where a layer must be unreachable, not because the project is one of them today.
**Trust does not extend to changing the harness itself.** `loadLayeredEnv` rejects, at load and before anything is materialized, any `.env` that sets a variable governing how a process launches (`PATH`, `SHELL`, `NODE_OPTIONS`, `LD_PRELOAD`), what code a runtime executes before the program it was asked to run (`BASH_ENV`, `PERL5OPT`, `PYTHONSTARTUP`, `RUBYOPT`, `JAVA_TOOL_OPTIONS`, the Git hook commands), where model-visible instructions load from (the whole `DSH_*` namespace, `HOME`, `XDG_*`), or how the network is reached and trusted (proxy and CA variables). Matching is case-insensitive, so `https_proxy` is not a bypass.
**Trust does not extend to changing the harness itself.** `loadLayeredEnv` rejects, at load and before anything is materialized, any `.env` that sets a variable governing how a process launches (`PATH`, `SHELL`, `NODE_OPTIONS`, `LD_PRELOAD`), which ambient program handles an operation (`EDITOR`, `PAGER`, `BROWSER`), what code a runtime executes before the program it was asked to run (`BASH_ENV`, `PERL5OPT`, `PYTHONSTARTUP`, `RUBYOPT`, `JAVA_TOOL_OPTIONS`, the Git hook commands), where model-visible instructions load from (the whole `DSH_*` namespace, `HOME`, `XDG_*`), or how the network is reached and trusted (proxy and CA variables). Matching is case-insensitive, so `https_proxy` is not a bypass.
The line is that these take effect with no user action, before any turn, outside the permission policy and the sandbox. `DSH_PERMISSION_MODE` would switch off the approvals that make trusting a project meaningful at all, and `BASH_ENV` runs a file of the project's choosing on every single `bash -c` the bash tool issues — the project's code running under the agent's policy is the deal; the project rewriting that policy is not. Enumerating these is a losing game one variable at a time, which is why the whole `DSH_*` namespace is denied rather than an audited subset, and why the list is organised by what a variable *does* rather than by which runtime owns it. There is no opt-out: an escape hatch would have to be readable from somewhere, and anything a discovered file could set is the hole itself.
@@ -53,7 +53,7 @@ The line is that these take effect with no user action, before any turn, outside
## Consequences
- The web credential form now takes effect against an older key in the user's `.env`; only a key exported in the launching shell still makes it read-only, and the diagnostic says so.
- A `.env` holding `DSH_*`, `PATH`, or a proxy variable fails the launch instead of being applied. Developers keeping switches in a repository `.env` move them to their shell — a deliberate, loud break.
- A `.env` holding `DSH_*`, `PATH`, `BROWSER`, or a proxy variable fails the launch instead of being applied. Developers keeping switches in a repository `.env` move them to their shell — a deliberate, loud break.
- Composition is no longer overridable by a stale shell endpoint. It is still overridable by a user's stored `settings.yaml`, which is the settings seam's layering and not something this note changes; the product CLI offers no flag above it, so a deployment that must win against stored settings owns its own bin or loader tree.
- Not solved: the layers are still materialized into `process.env`, so ordinary project variables continue to reach child processes under the subprocess scrub. Bootstrap variables cannot come from a file at all; the environment package records the remaining subprocess reach as a limitation.
- Exa and Perplexity still capture their key at load time rather than through the credential seam. They no longer read raw `process.env` — they resolve through the trusted layers — but converting them to per-request credential resolution is separate work.
@@ -43,7 +43,7 @@ inherited process environment (read-only, wins)
**harness 被启动于其中的项目默认可信,且不做询问。** 一个 checkout 可以携带自己的 endpoint、自己的普通变量和自己的密钥;密钥排在受管存储之下,因此通过 Models 页存下的密钥绝不会被 checkout 中恰好带有的那一个顶掉。`LaunchEnvironmentSnapshot.getFrom(name, sources)` 仍然只搜索调用方点名的层,省略某层仍是拒绝而不是降级——该机制是为「某一层必须不可达」的那些决策准备的,而项目层今天不在其列。
**信任不延伸到改变 harness 本身。** `loadLayeredEnv` 会在加载时、且在物化任何内容之前,拒绝任何设置了下列变量的 `.env`:决定进程如何启动的(`PATH``SHELL``NODE_OPTIONS``LD_PRELOAD`)、决定运行时在执行被要求运行的程序之前先执行哪些代码的(`BASH_ENV``PERL5OPT``PYTHONSTARTUP``RUBYOPT``JAVA_TOOL_OPTIONS`、Git 的钩子命令)、决定模型可见指令从哪里加载的(整个 `DSH_*` 命名空间、`HOME``XDG_*`),以及决定网络如何访问以及如何建立信任的(proxy 与 CA 变量)。匹配不区分大小写,因此 `https_proxy` 不是绕过手段。
**信任不延伸到改变 harness 本身。** `loadLayeredEnv` 会在加载时、且在物化任何内容之前,拒绝任何设置了下列变量的 `.env`:决定进程如何启动的(`PATH``SHELL``NODE_OPTIONS``LD_PRELOAD`)、决定由哪个环境程序处理一项操作的(`EDITOR``PAGER``BROWSER`)、决定运行时在执行被要求运行的程序之前先执行哪些代码的(`BASH_ENV``PERL5OPT``PYTHONSTARTUP``RUBYOPT``JAVA_TOOL_OPTIONS`、Git 的钩子命令)、决定模型可见指令从哪里加载的(整个 `DSH_*` 命名空间、`HOME``XDG_*`),以及决定网络如何访问以及如何建立信任的(proxy 与 CA 变量)。匹配不区分大小写,因此 `https_proxy` 不是绕过手段。
这条界线在于:它们无需任何用户动作、在任何轮次开始之前、且在权限策略与沙箱之外就生效。`DSH_PERMISSION_MODE` 会关掉让「信任项目」根本成立的那道审批,而 `BASH_ENV` 会在 bash 工具每次发出 `bash -c` 时执行项目指定的文件——项目的代码在 agent(智能体)的策略下运行是约定,项目改写那份策略不是。一个变量一个变量地枚举是必输的游戏,所以整个 `DSH_*` 命名空间被拒绝而不是只拒绝一份经审查的子集,也所以这份清单是按变量*做什么*而不是按哪个运行时拥有它来组织的。不设逃生门:逃生门本身总得从某处读取,而任何被发现的文件能设置的东西,就是那个漏洞本身。
@@ -54,7 +54,7 @@ inherited process environment (read-only, wins)
## Consequences
- Web 凭据表单现在能压过用户 `.env` 里更旧的密钥;只有在启动 shell 里 export 的密钥才会让它变成只读,诊断信息也会这么说。
-`DSH_*``PATH` 或 proxy 变量的 `.env` 会导致启动失败而不是被应用。把开关放在仓库 `.env` 里的开发者需要改放到 shell——这是一次刻意且响亮的破坏。
-`DSH_*``PATH``BROWSER` 或 proxy 变量的 `.env` 会导致启动失败而不是被应用。把开关放在仓库 `.env` 里的开发者需要改放到 shell——这是一次刻意且响亮的破坏。
- composition 不再会被陈旧的 shell endpoint 覆盖。但它仍然会被用户已存的 `settings.yaml` 覆盖,这是 settings seam 的分层方式,本 Note 不改变它;产品 CLI 没有高于它的标志,因此需要压过已存 settings 的部署方要自带 bin 或 loader 配置树。
- 未解决的:各层仍然会被物化进 `process.env`,因此普通项目变量继续按子进程清洗规则抵达子进程。bootstrap 变量完全不能来自文件;环境包将其余变量仍可抵达子进程这一点记录为一项限制。
- Exa 与 Perplexity 仍在加载时捕获密钥,而不是经凭据 seam。它们不再读裸 `process.env`——改为经受信层解析——但把它们改造成按请求解析凭据是另一件事。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-cancelled-stream-prefix-finalize.md
2026-08-10-cancelled-stream-prefix-finalize.md: 0cae25b786922fba8204d68ca9c0a669e43d76a0
2026-08-10-cancelled-stream-prefix-finalize.zh.md: e961ea6a51f74dcc244e4ad8970eae4cbe4c9a6c
@@ -0,0 +1,39 @@
# Agent Note: Cancelled streams finalize their delivered prefix
Status: implemented
English | [中文](2026-08-10-cancelled-stream-prefix-finalize.zh.md)
## Problem
A cancelled stream can leave `assistant/chunk` events that clients continue rendering while `deriveMessages()` excludes them because no `assistant/message` records the delivered prefix. A follow-up such as "expand on your second point" then lacks text the user read, and a fork at the cancelled turn inherits the same gap.
The model history must contain assistant content that remains visible to the user after cancellation.
## Decision
`ReactLoopAgent.step()` catches cancellation while consuming a model stream, when its `BlockAssembler`, logged chunk seqs, and provider route identify the delivered prefix. It appends that prefix as the step's `assistant/message` with `interrupted: true`, `surfaceOp: 'append'`, and `sourceEventSeqs` containing exactly the logged chunks. The append precedes `step/end` and the aborted `turn/end`.
`BlockAssembler.interruptedBlocks()` returns closed and open `text` and `reasoning` blocks with non-whitespace content in stream order. It omits tool calls because interruption precedes dispatch and no real result exists; it also omits empty blocks and open unknown block types. An empty result appends no assistant message. Provider `error` and `aborted` finishes leave the stream-consumption scope before `agent/request-error`, so provider failures and cancellation during recovery commit no content from the failed request.
Chat and Trajectory Conversation Definitions read `interrupted` from the durable message. Chat renders the Stopped marker, while Trajectory keeps the provider request in the error lifecycle after `step/end` and retains the durable result seq and provenance. Cancellation during tool execution follows the tool scheduler contract because the assistant message has already committed: started calls produce real results, and undispatched calls receive `ABORTED_BEFORE_DISPATCH` results.
## Alternatives considered
**Always discard the prefix.** This avoids a new durable marker but makes every cancel-then-follow-up and fork omit assistant content that remains visible to the user.
**Assemble the prefix from chunks during projection.** `deriveMessages()` and client Conversation Definitions would each need interruption assembly rules, and the log would have no authoritative assistant message for the prefix. This also expands model history beyond the three `SurfaceEventType` events.
**Retain complete tool calls with synthetic aborted results.** These calls never dispatched, so synthetic results would claim an execution outcome that did not occur and add content the user did not receive as a tool result.
**Append a model-visible interruption message such as `[interrupted by user]`.** This can tell the model that the prefix is incomplete, but it requires a separate source type, projection rule, UI treatment, and localized wording. The durable aborted `turn/end` preserves the fact needed for that later decision.
## Consequences
Post-cancel follow-ups and forks include the delivered prefix. The ACP bridge drains ordered assistant output before settling the prompt, so the final `agent_message_chunk` update precedes the cancelled stop reason.
Terminal provider errors still discard their streamed prefix. That asymmetry remains because an error turn ends without the user's cancellation decision and requires its own retention policy.
## Testing
`packages/core/agent-loop/tests/cancel.spec.ts` covers content, cited seqs, event order, next-request parity, reasoning-only output, tool-call omission, recovery cancellation, and the empty-prefix case. `packages/llm/llm/tests/assembler.spec.ts` covers `interruptedBlocks()`. `packages/client/ui-conversation/tests/conversation-node-definitions.client.spec.ts` and `packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts` cover both client projections. The keyless `cancel` ACP snapshot and `goal-round-driver` goal snapshot cover assembled applications.
@@ -0,0 +1,39 @@
# Agent Note: 被取消的流定稿其已送达前缀
Status: implemented
[English](2026-08-10-cancelled-stream-prefix-finalize.md) | 中文
## Problem
被取消的流可能留下客户端继续渲染的 `assistant/chunk` 事件,但如果没有 `assistant/message` 记录已送达前缀,`deriveMessages()` 就会排除这部分内容。后续的「第二点展开讲讲」之类追问会缺少用户已读到的文本,在该轮次上创建的分支也会继承这个缺口。
模型历史必须包含取消后仍对用户可见的 assistant 内容。
## Decision
`ReactLoopAgent.step()` 在消费模型流期间捕捉取消,此时 `BlockAssembler`、已记录的分片 seq 和提供方路由可以确定已送达前缀。循环把该前缀追加为 step 的 `assistant/message`,并设置 `interrupted: true``surfaceOp: 'append'` 以及恰好包含已记录分片的 `sourceEventSeqs`。该追加先于 `step/end` 和记录 aborted 的 `turn/end`
`BlockAssembler.interruptedBlocks()` 按流顺序返回内容非空白的已闭合和未闭合 `text``reasoning` 块。打断先于分派,没有真实工具结果,因此它会省略工具调用,也会省略空块和未闭合的未知块类型。返回结果为空时不追加 assistant 消息。提供方的 `error``aborted` finish 会在 `agent/request-error` 前离开流消费范围,因此提供方故障和恢复期间的取消都不会提交失败请求的内容。
Chat 和 Trajectory Conversation Definition 从持久消息读取 `interrupted`。Chat 渲染 Stopped 标记,Trajectory 则在 `step/end` 后把提供方请求保持在 error 生命周期,并保留持久结果 seq 和提供方信息。工具执行期间的取消遵循工具调度器约定,因为 assistant 消息已提交:已启动的调用生成真实结果,未分派的调用获得 `ABORTED_BEFORE_DISPATCH` 结果。
## Alternatives considered
**始终丢弃前缀。** 这能避免新增持久标记,但每次取消后的追问和分支都会缺少仍对用户可见的 assistant 内容。
**在投影时从分片组装前缀。** `deriveMessages()` 和客户端 Conversation Definition 都需要实现打断组装规则,日志中也没有该前缀的权威 assistant 消息。这还会让模型历史超出三类 `SurfaceEventType` 事件。
**保留完整工具调用并合成 aborted 结果。** 这些调用从未分派,合成结果会声称一个并未发生的执行结果,还会增加用户未收到的工具结果内容。
**追加 `[interrupted by user]` 之类模型可见的打断消息。** 这可以告诉模型前缀并不完整,但需要独立的来源类型、投影规则、UI 处理和本地化文案。持久的 aborted `turn/end` 保留了该后续决策所需的事实。
## Consequences
取消后的追问和分支会包含已送达前缀。ACP 桥会在结算 prompt 前排空按序传送的 assistant 输出,因此最后一条 `agent_message_chunk` 更新先于 cancelled stop reason。
终局提供方错误仍会丢弃已流出前缀。该不对称保留,因为 error 轮次的结束不来自用户的取消决定,需要独立的保留策略。
## Testing
`packages/core/agent-loop/tests/cancel.spec.ts` 覆盖内容、引用的 seq、事件顺序、下一请求的一致性、仅 reasoning 的输出、工具调用省略、恢复期间的取消和空前缀情形。`packages/llm/llm/tests/assembler.spec.ts` 覆盖 `interruptedBlocks()``packages/client/ui-conversation/tests/conversation-node-definitions.client.spec.ts``packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts` 覆盖两种客户端投影。keyless 的 `cancel` ACP 快照和 `goal-round-driver` goal 快照覆盖完整应用。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-21-cross-session-references.md
2026-07-21-cross-session-references.md: f49c75a9d4e3304b44d12a3df85430ffb494b8a9
2026-07-21-cross-session-references.zh.md: 81a1970497bf175948a70bb1982f336f19d4d19e
2026-07-21-cross-session-references.md: 774a948345e3d45adbba47ef6a7edd3e6f0740b2
2026-07-21-cross-session-references.zh.md: fd864203d17954167646edf3b1a62946f8fa3f23
@@ -6,33 +6,35 @@ English | [中文](2026-07-21-cross-session-references.zh.md)
## Problem
TUI users need to bring relevant work from another conversation into one new message without resuming, forking, or granting the source transcript authority over the current session. The harness already exposes exact session enumeration and raw event inspection, but every host independently parsing logs would duplicate compaction folding, filtering by cited source-event seqs, size limits, error behavior, and persistence. Encoding host markup directly into the agent message contract would also bind the core loop to one UI syntax.
Web users need to bring relevant work from another conversation into one new message without resuming, forking, or granting the source transcript authority over the current session. The harness already exposes exact session enumeration and raw event inspection, but every host independently parsing logs would duplicate compaction folding, provenance filtering, size limits, error behavior, and persistence. Encoding host markup directly into the agent message contract would also bind the core loop to one UI syntax.
## Decision
`@deepseek-ai/dsh-session-reference` is one context consumer service at `ctx.sessionReferenceResolver`. Hosts normalize their protocol into `SessionReferenceInput[]` and call `prepare()` before delivery. The service returns detached readable content plus an optional identified, frozen `UserMessage` snapshot; core agent packages do not parse session URIs or read another log.
`@deepseek-ai/dsh-session-reference` is one context consumer service at `ctx.sessionReferenceResolver`. Its outer `agent/pre-step` listener parses canonical mentions in accepted direct user messages and calls `prepare()` without adding reference behavior to a host gateway. The service returns detached readable content plus an optional identified, frozen `UserMessage` snapshot; core agent packages do not parse session URIs or read another log.
`dsh-session:<base64url(JSON.stringify(sessionId))>` is the canonical host-independent identifier. JSON string encoding precedes base64url so quotes, slashes, backslashes, Unicode, newlines, and every other JavaScript string value round-trip without delimiter ambiguity. TUI renders that URI inside `@[label](uri)`; text-only clients may use the same inline mention. Explicit Markdown mentions reject malformed URIs. Bare text becomes a reference only for a non-empty base64url-shaped payload, whose decode must still be canonical; empty or punctuation-only uses remain ordinary discussion text.
`dsh-session:<base64url(JSON.stringify(sessionId))>` is the canonical host-independent identifier. JSON string encoding precedes base64url so quotes, slashes, backslashes, Unicode, newlines, and every other JavaScript string value round-trip without delimiter ambiguity. Web receives that URI inside the Host-produced `@[label](uri)` mention and keeps it behind an atomic session chip; text-only clients may use the same inline mention. Explicit Markdown mentions reject malformed URIs. Bare text becomes a reference only for a non-empty base64url-shaped payload, whose decode must still be canonical; empty or punctuation-only uses remain ordinary discussion text.
The service uses `ctx.sessionQuery.readSurface(sessionId)`, which loads one live-preferred corpus observation, folds it with the session package's canonical surface algorithm, and returns a detached header, capture seq, and current nodes. FTS is not a dependency: discovery matches id, cwd, or the latest folded title, while message bodies remain outside the candidate layer. Non-empty queries batch title observations across the visible corpus with bounded persisted-log concurrency and cancellation; a dedicated title index can replace that discovery path without changing reference identity or preparation.
## Snapshot and projection
Preparation deduplicates in first-appearance order, rejects the target id, enforces a configurable limit with a hard maximum of three references, and performs all reads in parallel. It returns no partially prepared context: any read, cancellation, validation, or budget error rejects the operation before `followup()` or `steer()`. Cancellation races in-flight discovery and exact reads, so a host settles promptly even when a persistence backend cannot interrupt its pending operation; any late backend settlement is observed but cannot enqueue the message. A source is read before enqueue, so later source messages, compaction, deletion, or persistence replacement cannot change the target session.
Preparation deduplicates in first-appearance order, rejects the target id, enforces a configurable limit with a hard maximum of three references, and performs all reads in parallel. It returns no partial context: any read, cancellation, validation, or budget error ends the turn before the accepted messages enter model-visible history. Cancellation races in-flight discovery and exact reads, so the listener settles promptly even when a persistence backend cannot interrupt its pending operation. A queued message captures each source when it reaches `agent/pre-step`; later source messages, compaction, deletion, or persistence replacement cannot change the context recorded in the target session.
Projection retains direct-user messages and steering, completed assistant text, and checkpoint user messages carrying the canonical source exported by `dsh-compaction`. That marker is part of the compaction capability contract rather than a backend package name. When a source prompt already contains baked prefix context, projection reads only its model-hidden display content, so referencing that target later does not recursively propagate an earlier snapshot. Projection excludes shadowed pre-compaction nodes, tools and results, reasoning, injected context, other plugin user messages, log-only records, and incomplete assistant chunks. Repeated compaction therefore exposes only the latest folded checkpoint lineage still on the current surface plus its retained tail; there is no raw/current switch and no shadow recovery.
Projection retains direct-user messages and steering, completed assistant text, and checkpoint user messages carrying the canonical source exported by `dsh-compaction`. That marker is part of the compaction capability contract rather than a backend package name. Reference snapshots remain separate sourced `user/message` events, so projection excludes them as injected context and never recursively propagates an earlier snapshot. Projection also excludes shadowed pre-compaction nodes, tools and results, reasoning, other plugin user messages, log-only records, and incomplete assistant chunks. Repeated compaction therefore exposes only the latest folded checkpoint lineage still on the current surface plus its retained tail; there is no raw/current switch and no shadow recovery.
One aggregated context is serialized as JSON beneath a fixed untrusted-background warning. The warning tells the model not to follow instructions, permission claims, or tool requests from referenced sessions unless the current user repeats them. Tag-safe serialization emits every data `<` as the lossless JSON escape `\u003c`; source strings therefore cannot spell the surrounding XML-like tags or escape the data region. The same serializer drives each source's independent byte accounting. AgentLoop persists the snapshot as a sourced `user/message` immediately before the direct `user/message`; target replay therefore satisfies the model-visible/log-reconstructable invariant without a new event type, placement mode, or prompt envelope.
## Message ownership
TUI owns the snapshot/direct-message transaction without extending the generic inbox record. While the agent is idle, it installs a one-shot outer `agent/pre-step` listener before `followup()`; an enter decision receives the snapshot as another message, while rejection or an earlier ordinary discard releases the listener and writes neither message. While the agent is running, TUI calls `inject(snapshot)` then `steer(prompt)`, placing both in the next-step inbox for the same later claim. A rejecting or failed pre-step leaves that claimed pair removed; messages inserted after the claim remain pending. The [separate-context decision](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) owns this generic delivery boundary.
The service's outer `agent/pre-step` listener calls downstream listeners first and processes only an `enter` decision. It parses each accepted direct user message, preserves that message's id while replacing canonical mentions with readable labels, and inserts the frozen snapshot immediately before that message. Queue edits and queue-to-steer relocation need no reference-specific state because the final claimed messages are the input to preparation. The [separate-context decision](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) owns this context ordering.
Reference preparation is not a new steering protocol and does not create a turn by itself. Idle delivery uses `followup()` and pre-step entry; running delivery uses the shared next-step inbox while retaining snapshot order.
Reference preparation is not a new delivery protocol and does not create a turn by itself. A preparation failure terminates the already accepted turn through the agent loop's existing plugin-failure path.
## Host adapters
TUI combines session candidates with the existing `@` file provider. Candidate lookup matches case-insensitive substrings of the session id, cwd, or latest folded title, displays that title, and falls back to the session id when a title observation is absent or fails. Lookup follows the editor's cancellation signal, and session id, cwd, and mention labels escape external terminal controls while the canonical URI retains the original id. TUI prepares only submissions containing structured mentions, disables duplicate submit while awaiting snapshots, restores failed input, renders the readable direct content as the user message, and renders session-reference source metadata as a compact source list instead of exposing the complete JSON in the terminal.
The unified Web `@` source combines session candidates with Host-backed file discovery. Session candidate lookup matches case-insensitive substrings of the session id, cwd, or latest folded title, displays that title, and falls back to the session id when a title observation is absent or fails. Lookup follows the request's cancellation signal, and session id, cwd, and mention labels escape external control characters while the canonical URI retains the original id.
Web exposes file and session discovery through generated Remote methods on their owning services, as detailed in [Web file and session references](2026-07-27-web-file-and-session-references.md). Session picks are atomic chips backed by the Host-produced canonical mention. Ordinary `session.prompt` delivery carries that mention without a reference-specific API Proxy route. Replay associates the separate session-reference context with its neighboring direct message and renders a compact source summary instead of exposing the snapshot JSON.
The [automation-only ACP transport](../simplification/2026-07-23-acp-automation-only-protocol.md) deliberately does not mount session-query or session-reference services.
@@ -45,16 +47,16 @@ Each of at most three references is independently capped at 65,536 UTF-8 bytes b
- **Wait for SQLite FTS5** — rejected because snapshot correctness requires exact id reads and canonical surface folding, not content search. FTS improves discovery only.
- **Put mention syntax in agent delivery methods** — rejected because it would make the core protocol parse one host's presentation syntax and prevent typed non-text hosts from sharing the semantic layer.
- **Implement references separately in each host** — rejected because projection, security warning, retention, and persistence would drift across hosts.
- **Attach context to `SendOptions` and the direct prompt's inbox record** — rejected because generic delivery would own a domain transaction through admission, steering, cancellation, and observation. A domain-specific admission wrapper and the existing next-step inbox preserve the required pairing without enlarging every direct prompt.
- **Bake the prefix host-side before `followup()`** — rejected because `agent/pre-step` must inspect and rewrite only the direct prompt. Keeping the snapshot as a separate sourced message preserves that boundary and lets TUI hide background bytes from the direct user bubble.
- **Attach context to `SendOptions` and the direct prompt's inbox record** — rejected because generic delivery would own a domain transaction through admission, steering, cancellation, and observation. The domain listener can prepare the final claimed message without enlarging every direct prompt.
- **Bake the prefix host-side before `followup()`** — rejected because `agent/pre-step` must inspect and rewrite only the direct prompt. Keeping the snapshot as a separate sourced message preserves that boundary and lets Web hide background bytes from the direct user bubble.
- **Replay the raw source log or restore shadowed events** — rejected because compact defines the current model surface and may intentionally retire sensitive or expensive history.
- **Resume or fork the source** — rejected because the feature supplies read-only background for one target message, not identity or lifecycle continuity.
- **Inject at request time by rereading the source** — rejected because the reference would become nondeterministic, cancellation races could alter its bytes, and target replay would depend on external mutable state.
- **Reread the source after the model step enters** — rejected because target replay would depend on external mutable state instead of the logged snapshot.
## Verification
Unit and integration coverage pins URI round-trips and text-boundary punctuation, explicit malformed references, id/cwd/title candidate matching and ranking, failed title-observation fallback, candidate cancellation, terminal-control escaping, projection exclusions, non-recursive snapshot projection, backend-independent compact checkpoints, tag-safe framing, deduplication, self-reference, count limits, all-or-nothing reads, prompt cancellation against a non-settling storage read, independent per-source byte retention, prompt blocking, admission-time staging, send/steer placement, title isolation, missing capability, and compact TUI replay. One keyless terminal snapshot types a title-only substring against an opaque session id and pins the rendered candidate. Another keyless TUI snapshot runs the real agent loop: the source surface replaces old user/assistant history with a compact checkpoint, the target submits a mention, and the captured model request contains a sourced snapshot message followed by the readable current prompt, without either shadowed string.
Unit and integration coverage pins URI round-trips and text-boundary punctuation, explicit malformed references, id/cwd/title candidate matching and ranking, failed title-observation fallback, candidate cancellation, control-character escaping, projection exclusions, non-recursive snapshot projection, backend-independent compact checkpoints, tag-safe framing, deduplication, self-reference, count limits, all-or-nothing reads, cancellation against a non-settling storage read, independent per-source byte retention, frozen message ownership, pre-step parsing and insertion, downstream rejection, node-owned replay association, title isolation, and the generated Remote discovery faces. A keyless Web snapshot pins the assembled reference selection path.
## Consequences
The new plugin is the stable semantic boundary and adds no persistence schema, event type, FTS dependency, source subscription, or compact shadow access. The standard TUI demo bundle mounts it explicitly and exposes its count and per-source byte limits in its config; custom hosts remain unchanged until they mount the service and adapt their input. Reference contexts increase target history size within configured bounds and can later be summarized by ordinary target compaction, after which the source session is irrelevant.
The new plugin is the stable semantic boundary and adds no persistence schema, event type, FTS dependency, source subscription, or compact shadow access. The standard CLI composition mounts it explicitly for Web and exposes its count and per-source byte limits in config; custom hosts remain unchanged until they mount the service and adapt their input. Reference contexts increase target history size within configured bounds and can later be summarized by ordinary target compaction, after which the source session is irrelevant.
@@ -6,33 +6,35 @@ Status: implemented
## 问题
TUI 用户需要把另一场对话中的相关工作带入一条新消息,但不恢复、不 fork,也不让源 transcript(文本记录)对当前会话拥有权威性。harness 已经提供准确的会话枚举与原始事件检查,但若每个宿主都独立解析日志,就会重复实现压缩(compaction)折叠、按被引用来源事件 seq 过滤、大小限制、错误行为和持久化。把宿主标记直接编码进 agent(智能体)消息约定,还会让核心循环绑定某一种 UI 语法。
Web 用户需要把另一场对话中的相关工作带入一条新消息,但不恢复、不 fork,也不让源 transcript(文本记录)对当前会话拥有权威性。harness 已经提供准确的会话枚举与原始事件检查,但若每个宿主都独立解析日志,就会重复实现压缩(compaction)折叠、来源过滤、大小限制、错误行为和持久化。把宿主标记直接编码进 agent(智能体)消息约定,还会让核心循环绑定某一种 UI 语法。
## 决策
`@deepseek-ai/dsh-session-reference` 是注册在 `ctx.sessionReferenceResolver` 上的单一上下文消费服务。宿主先把各自的协议规范化为 `SessionReferenceInput[]`,并在交付前调用 `prepare()`。该服务返回分离的可读内容和一份可选的、带标识且冻结的 `UserMessage` 快照;核心 agent 包既不解析会话 URI,也不读取其他日志。
`@deepseek-ai/dsh-session-reference` 是注册在 `ctx.sessionReferenceResolver` 上的单一上下文消费服务。它的外层 `agent/pre-step` 监听器会解析已接受直接用户消息中的规范 mention,并调用 `prepare()`,宿主网关无需添加引用行为。该服务返回分离的可读内容和一份可选的、带标识且冻结的 `UserMessage` 快照;核心 agent 包既不解析会话 URI,也不读取其他日志。
`dsh-session:<base64url(JSON.stringify(sessionId))>` 是与宿主无关的规范标识符。系统先执行 JSON 字符串编码,再执行 base64url 编码,因此引号、正斜杠、反斜杠、Unicode、换行符以及其他任意 JavaScript 字符串值都能无损往返,不会因分隔符产生歧义。TUI 把该 URI 渲染到 `@[label](uri)` ;纯文本客户端可以使用同一种行内提及标记。显式 Markdown 提及标记会拒绝格式错误的 URI。裸文本只有在负载非空且形状符合 base64url 时才会成为引用,而且解码过程仍须通过规范性校验;空负载或只含标点符号的用法仍按普通讨论文本处理。
`dsh-session:<base64url(JSON.stringify(sessionId))>` 是与宿主无关的规范标识符。系统先执行 JSON 字符串编码,再执行 base64url 编码,因此引号、正斜杠、反斜杠、Unicode、换行符以及其他任意 JavaScript 字符串值都能无损往返,不会因分隔符产生歧义。Web 接收由 Host 生成、包含该 URI `@[label](uri)` 提及标记,并把它封装为原子 session chip;纯文本客户端可以使用同一种行内提及标记。显式 Markdown 提及标记会拒绝格式错误的 URI。裸文本只有在负载非空且形状符合 base64url 时才会成为引用,而且解码过程仍须通过规范性校验;空负载或只含标点符号的用法仍按普通讨论文本处理。
该服务使用 `ctx.sessionQuery.readSurface(sessionId)`:它优先从实时会话加载一次语料观察结果,使用会话包的规范表层算法执行折叠,并返回与源数据分离的会话头、捕获序号和当前节点。FTS 不是依赖项:候选发现会匹配 id、cwd 或最新折叠后的标题,而消息主体不进入候选层。非空查询会对可见语料中的标题观察结果执行批处理,以有界并发读取持久化日志,并支持取消;专用标题索引可以替换这条发现路径,而无需改变引用标识或准备过程。
## 快照与投影
准备过程按首次出现的顺序去重、拒绝目标会话自身的 id,并且执行可配置的数量限制,但引用硬上限为三个,所有读取均并行执行。该过程不会返回部分完成的上下文:任何读取、取消、校验或预算错误都会在调用 `followup()``steer()` 前拒绝本次操作。取消会与进行中的候选发现和精确读取竞速,因此即使持久化后端无法中断待处理操作,宿主也能及时结束等待;后端迟到的完成结果仍会被观察,但不能让消息入队。源会话在入队前完成读取,因此源会话后续新增消息、执行压缩、被删除或替换持久化内容,都无法改变目标会话中的快照
准备过程按首次出现的顺序去重、拒绝目标会话自身的 id,并且执行可配置的数量限制,但引用硬上限为三个,所有读取均并行执行。该过程不会返回部分上下文:任何读取、取消、校验或预算错误都会在已接受消息进入面向模型的历史之前结束该轮次。取消会与进行中的候选发现和精确读取竞速,因此即使持久化后端无法中断待处理操作,监听器也能及时结束等待。queued 消息到达 `agent/pre-step` 时会捕获每个源;此后源会话新增消息、执行压缩、被删除或替换持久化内容,都无法改变目标会话中记录的上下文
投影会保留直接用户消息与 steering(中途引导)、已完成的 assistant 文本,以及携带由 `dsh-compaction` 导出的规范来源标记的检查点用户消息。该标记属于压缩能力约定的一部分,而非某个后端包名称。当源提示词已包含合并写入的前缀上下文时,投影只读取其模型不可见的显示内容,因此后续引用该目标不会递归传播先的快照。投影会排除压缩前已被遮蔽的节点、工具及其结果、推理(reasoning)、注入的上下文、其他插件用户消息、仅用于日志的记录,以及尚未完成的 assistant 分片。因此,重复压缩只会暴露当前表层仍保留的最新折叠检查点谱系及其尾部消息;系统不提供 raw/current 开关,也不恢复被遮蔽的内容。
投影会保留直接用户消息与 steering(中途引导)、已完成的 assistant 文本,以及携带由 `dsh-compaction` 导出的规范来源标记的检查点用户消息。该标记属于压缩能力约定的一部分,而非某个后端包名称。引用快照始终是独立且带来源的 `user/message` 事件,因此投影会把它们作为注入上下文排除,绝不递归传播先的快照。投影会排除压缩前已被遮蔽的节点、工具及其结果、推理(reasoning)、其他插件用户消息、仅用于日志的记录,以及尚未完成的 assistant 分片。因此,重复压缩只会暴露当前表层仍保留的最新折叠检查点谱系及其尾部消息;系统不提供 raw/current 开关,也不恢复被遮蔽的内容。
系统把一个聚合上下文序列化为 JSON,并置于固定的不可信背景警告之后。该警告要求模型不要遵循被引用会话中的指令、权限声明或工具请求,除非当前用户再次提出这些内容。标签安全序列化会把数据中的每个 `<` 无损转义为 JSON `\u003c`;因此源字符串无法拼出外围类似 XML 的标签,也无法逃逸数据区域。同一个序列化器会独立核算每个源的字节数。AgentLoop 会把快照持久化为一条带来源信息的 `user/message`,紧接在直接 `user/message` 之前。因此,目标回放无需新增事件类型、放置模式或提示词封套,也能满足「模型可见/日志可重建」不变量。
## 消息所有权
TUI 负责快照/直接消息事务,不扩展通用收件箱记录。agent 空闲时,它会在调用 `followup()` 前安装一次性的外层 `agent/pre-step` 监听器enter 决策会把快照作为另一条消息接收,而 reject 或更早的普通丢弃会释放监听器,并且不写入任何消息。agent 运行时,TUI 会依次调用 `inject(snapshot)``steer(prompt)`,把两者放入 next-step inbox,等待后续同一次领取。pre-step reject 或失败会使这对已领取消息维持已移除状态;领取后插入的消息继续等待。这一通用交付边界由[上下文分离决策](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md)规定。
该服务的外层 `agent/pre-step` 监听器会先调用下游监听器,并且只处理 `enter` 决策。它会解析每条已接受的直接用户消息,在把规范 mention 替换为可读标签时保留消息 id,并把冻结快照插入到该消息紧前。最终领取的消息是准备过程的输入,因此队列编辑和从 queue 移动到 steer 不需要引用专用状态。[上下文分离决策](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md)规定了这一上下文顺序
引用准备过程不是新的 steering 协议,本身也不会创建轮次。空闲交付使用 `followup()` 和 pre-step 进入决策;运行期间的交付使用共享 next-step inbox,并保持快照顺序
引用准备过程不是新的投递协议,本身也不会创建轮次。准备失败会通过 agent loop 的现有插件失败路径终止已经接受的轮次
## 宿主适配器
TUI 把会话候选与现有 `@` 文件提供方组合在一起。候选查询会对 session id、cwd 或最新折叠后的标题执行不区分大小写的子串匹配,显示该标题,并在没有标题观察结果或标题观察失败时回退到 session id。候选查询遵循编辑器的取消信号;session id、cwd 和提及标签中的外部终端控制字符会被转义,但规范 URI 仍保留原始 id。TUI 只准备包含结构化提及标记的提交;等待快照时禁用重复提交;失败时恢复输入;它把可读的直接内容渲染为用户消息,并把会话引用来源元数据渲染为精简的来源列表,不在终端中暴露完整 JSON。
统一的 Web `@` source 把会话候选与 Host 支持的文件发现组合在一起。会话候选查询会对 session id、cwd 或最新折叠后的标题执行不区分大小写的子串匹配,显示该标题,并在没有标题观察结果或标题观察失败时回退到 session id。查询遵循请求的取消信号;session id、cwd 和提及标签中的外部控制字符会被转义,但规范 URI 仍保留原始 id。
Web 通过所属服务上的生成 Remote 方法提供文件与会话发现,详见 [Web 文件与会话引用](2026-07-27-web-file-and-session-references.md)。session 选择项是由 Host 生成的规范 mention 支撑的原子 chip。普通 `session.prompt` 投递会携带该 mention,无需引用专用 API Proxy 路由。回放会把独立的 session-reference 上下文与相邻直接消息关联起来,并渲染精简来源摘要,而不暴露快照 JSON。
[仅面向自动化的 ACPAgent Client Protocol)传输层](../simplification/2026-07-23-acp-automation-only-protocol.md)有意不挂载会话查询或会话引用服务。
@@ -45,16 +47,16 @@ TUI 把会话候选与现有 `@` 文件提供方组合在一起。候选查询
- **等待 SQLite FTS5**:不予采纳,因为快照正确性依赖按准确 id 读取和规范表层折叠,而不是内容搜索。FTS 只改进候选发现。
- **把提及标记语法放入 agent 投递方法**:不予采纳,因为这会迫使核心协议解析某个宿主的展示语法,并阻止带类型的非文本宿主复用同一语义层。
- **在每个宿主中分别实现引用**:不予采纳,因为投影、安全警告、保留策略和持久化会在不同宿主之间逐渐偏离。
- **把上下文附加到 `SendOptions` 和直接提示词的收件箱记录**:不予采纳,因为通用投递将不得不负责贯穿准入、steering、取消和观察的领域事务。领域专用的准入包装层和现有 next-step inbox 可以保持所需配对,而无需扩大每条直接提示词。
- **在调用 `followup()` 前由宿主合并前缀**:不予采纳,因为 `agent/pre-step` 必须只检查和改写直接提示词。将快照保留为独立的带来源消息,可以维持该边界,并让 TUI 从直接用户气泡中隐藏背景字节。
- **把上下文附加到 `SendOptions` 和直接提示词的收件箱记录**:不予采纳,因为通用投递将不得不负责贯穿准入、steering、取消和观察的领域事务。领域监听器可以准备最终领取的消息,无需扩大每条直接提示词。
- **在调用 `followup()` 前由宿主合并前缀**:不予采纳,因为 `agent/pre-step` 必须只检查和改写直接提示词。将快照保留为独立的带来源消息,可以维持该边界,并让 Web 从直接用户气泡中隐藏背景字节。
- **回放原始源日志或恢复被遮蔽的事件**:不予采纳,因为压缩定义了当前模型表层,并且可能有意淘汰敏感或开销高昂的历史内容。
- **恢复或 fork 源会话**:不予采纳,因为本功能只为一条目标消息提供只读背景,不提供身份或生命周期连续性。
- **在请求时重新读取源会话并注入**:不予采纳,因为这会让引用变得不确定,取消竞态可能改变其字节内容,目标回放会依赖可变的外部状态。
- **模型步骤进入后重新读取源会话**:不予采纳,因为目标回放会依赖可变的外部状态,而不是已记录的快照
## 验证
单元与集成测试覆盖 URI 无损往返与文本边界标点、显式格式错误的引用、按 id/cwd/标题进行候选匹配与排序、标题观察失败时的回退、候选查询取消、终端控制字符转义、投影排除规则、快照的非递归投影、与后端无关的压缩检查点、标签安全封套、去重、自引用、数量限制、读取的全有或全无、存储读取不结束时取消提示词、逐源独立字节保留、提示词阻止、准入期间的暂存、send/steer 放置方式、标题隔离、能力缺失和压缩场景下的 TUI 回放。一个无密钥终端快照会在会话 id 不透明的情况下输入一个只与标题匹配的子串,并固定渲染出的候选项。另一个无密钥 TUI 快照会运行真实的 agent loop(智能体循环):源表层用一个压缩检查点替换旧的用户/assistant 历史,目标会话提交一个提及标记,捕获到的模型请求包含一条带来源的快照消息,后面跟随可读的当前提示词,并且不包含任一被遮蔽的字符串
单元与集成测试覆盖 URI 无损往返与文本边界标点、显式格式错误的引用、按 id/cwd/标题进行候选匹配与排序、标题观察失败时的回退、候选查询取消、控制字符转义、投影排除规则、快照的非递归投影、与后端无关的压缩检查点、标签安全封套、去重、自引用、数量限制、读取的全有或全无、存储读取不结束时取消、逐源独立字节保留、冻结消息所有权、pre-step 解析和插入、下游拒绝、节点负责的回放关联、标题隔离,以及生成的 Remote 发现接口。一个无密钥 Web 快照会固定组装后的引用选择路径
## 后果
新插件构成稳定的语义边界,不会新增持久化 schema、事件类型、FTS 依赖、源会话订阅或对压缩所遮蔽内容的访问。标准 TUI 演示组合包会显式挂载它,并在自身配置中暴露引用数量和逐源字节上限;自定义宿主在挂载该服务并适配输入前保持不变。引用上下文会在配置的界限内增大目标历史,随后可由目标会话的普通压缩进行摘要;完成压缩后,源会话便不再相关。
新插件构成稳定的语义边界,不会新增持久化 schema、事件类型、FTS 依赖、源会话订阅或对压缩所遮蔽内容的访问。标准 CLI 组合会为 Web 显式挂载它,并在配置中暴露引用数量和逐源字节上限;自定义宿主在挂载该服务并适配输入前保持不变。引用上下文会在配置的界限内增大目标历史,随后可由目标会话的普通压缩进行摘要;完成压缩后,源会话便不再相关。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-web-file-and-session-references.md
2026-07-27-web-file-and-session-references.md: ad8e5c53832a567bd38d1d1e560122cb8b630daa
2026-07-27-web-file-and-session-references.zh.md: acb016866efc42ef3ea9f661cb10ee1459cf1a6b
@@ -0,0 +1,49 @@
# Agent Note: Web file and session references
Status: implemented
English | [中文](2026-07-27-web-file-and-session-references.zh.md)
## Problem
The Web composer had a reusable slash/reference trigger pipeline, but its `@` source was inert subagent-label text. Web needed Host-backed workspace-path discovery and structured cross-session snapshots without scanning the Host filesystem in the browser or binding session identity to a display label.
## Decision
Web exposes one combined `@file` and `@session` menu through `@deepseek-ai/dsh-client-ui-reference`. For each unquoted query it starts both Remote discovery calls concurrently and deterministically orders files before sessions with locale-registered labels; non-selectable file and session section headings distinguish the two contiguous candidate sections without entering the keyboard-selection index. An open quoted token searches files only. Either candidate domain may fail independently without hiding successful rows from the other.
The file capability follows the three-package seam: `@deepseek-ai/dsh-file-reference` owns `ctx.fileReferences`, the shared `@path` token grammar, candidate shape, and stable model guidance; `@deepseek-ai/dsh-file-reference-local` owns bounded per-agent Host-filesystem indexes, invalidation, and scoped prompt installation; `dsh-client-ui-reference` consumes the generated Remote namespaces and shared grammar. A file pick remains path-only prompt text and a directory pick retriggers completion below its trailing slash.
A session pick is an atomic composer reference. Its visible label is presentation, while its hidden value and clipboard form are the canonical `@[label](dsh-session:…)` mention produced by the Host. Ordinary `session.prompt` delivery carries that mention unchanged. The session-reference service parses accepted direct user messages at `agent/pre-step`, captures every source, replaces the canonical mention with readable text while preserving the direct message id, and inserts the frozen snapshot immediately before that message. The API Proxy contains no reference-specific route, dependency, or error code.
The input machine keeps ordinary draft text and atomic references until the default sink reports Host acceptance. Serialization or prompt transport failure returns the same draft to editing. After acceptance, reference preparation belongs to the agent turn; a malformed mention, failed source read, cancellation, or budget failure terminates that turn. The logged prompt remains the replay authority. The concrete user and steering chat-node definition associates labels from an immediately preceding session-reference context, so the renderer receives the association from its own node data and shows a compact source summary instead of snapshot JSON.
## Reference transaction
```text
type @ → parallel file/session Remote calls → pick path text or canonical session chip
→ serialize draft → ordinary session.prompt enqueue
→ agent/pre-step parses mentions → capture sources → context + readable prompt
```
File lookup is advisory and cancellable; selection itself performs no read. Session preparation is all-or-nothing for one accepted model step. A queued message captures each source when the message is claimed, so queue edits and queue-to-steer relocation use the same path without gateway coordination.
## Alternatives considered
**Implement file discovery and grammar inside the Web client.** Rejected because browser-side code cannot safely access the Host workspace, while duplicating grammar, ranking, bounds, and invalidation would drift from the Host provider.
**Scan files through ordinary filesystem-tool RPCs.** Rejected because recursive fuzzy discovery is editor latency work, not a model-facing exact filesystem operation, and would couple the menu to tool policy and provider round trips.
**Eagerly attach selected file contents.** Rejected because selection would spend context before relevance is known and bypass the logged, auditable `read` call/result sequence.
**Represent sessions as plain `@label` text.** Rejected because labels are neither stable nor unique and cannot identify the source snapshot. Canonical Host-produced mentions preserve opaque session identity while keeping a readable display.
**Clear the composer before prompt admission settles.** Rejected because a transport or admission failure would lose the only editable copy of the request and visually claim acceptance that never occurred.
## Verification
Package tests pin shared file grammar and ranking, cache invalidation and lifecycle cleanup, parallel Web lookup, quoted paths, independent candidate failure, cancellation, grouped headings that do not alter option indexes, file/directory continuation, canonical session chips, adjacent-reference and adjacent-text reference projection, codec round-trip, generated Remote type inference, pre-step preparation, downstream rejection, and chat-node-owned label association. The keyless assembled Web snapshot renders the available reference sections, selects a file, then selects a session reference through the real client composition.
## Consequences
Web now uses the shared `@file` discovery seam and structured session-reference identity, while Host services remain the authority for filesystem and session access. File and session discovery are unary Remote contracts on the owning services, so generated client types replace handwritten RPC interfaces and browser bundles remain free of Node APIs. Candidate lookup failures remain quiet menu degradation. Reference preparation failures occur after prompt acceptance and end the agent turn. File references cost only path text plus stable conditional guidance, whereas session references retain the bounded snapshot cost and trust framing owned by `dsh-session-reference`.
@@ -0,0 +1,49 @@
# Agent Note: Web 文件与会话引用
Status: implemented
[English](2026-07-27-web-file-and-session-references.md) | 中文
## 问题
Web 输入框已有可复用的斜杠命令/引用触发流水线,但它的 `@` source 只是不会产生实际作用的 subagent 标签文本。Web 需要由宿主提供工作区路径发现和结构化跨会话快照,同时避免在浏览器中扫描宿主文件系统或把会话身份绑定到显示标签。
## 决策
Web 通过 `@deepseek-ai/dsh-client-ui-reference` 暴露一个合并的 `@file``@session` 菜单。每次处理未加引号的查询时,它会并发启动两项 Remote 发现调用,以确定性顺序把文件排在会话之前,并使用注册在 locale 字典中的标签;不可选择的文件与会话分组标题会区分两个连续的候选分组,且不会进入键盘选择索引。尚未闭合的带引号 token 只搜索文件。任一候选领域都可以独立失败,不会隐藏另一领域成功返回的行。
文件功能遵循由三个包构成的 seam`@deepseek-ai/dsh-file-reference` 拥有 `ctx.fileReferences`、共享 `@path` token 语法、候选形状和稳定的模型指引;`@deepseek-ai/dsh-file-reference-local` 拥有每个 agent(智能体)有界的宿主文件系统索引、失效处理和作用域内的提示词安装;`dsh-client-ui-reference` 消费生成的 Remote 命名空间与共享语法。选择文件后仍只会把路径文本写入提示词,选择目录则会在其尾部斜杠后重新触发补全。
选择会话会创建一个原子的输入框引用。可见标签只用于呈现,隐藏值和剪贴板形式则是宿主生成的规范 `@[label](dsh-session:…)` mention。普通 `session.prompt` 投递会原样携带该 mention。session-reference 服务会在 `agent/pre-step` 解析已接受的直接用户消息,捕获每个源,在保留直接消息 id 的同时把规范 mention 替换为可读文本,并把冻结快照插入到该消息紧前。API Proxy 不包含引用专用路由、依赖或错误码。
输入状态机在默认 sink 报告宿主已接受前,会保留普通草稿文本和原子引用。序列化或提示词传输失败后,同一草稿会回到可编辑状态。接受后,引用准备属于 agent 轮次;格式错误的 mention、源读取失败、取消或预算失败会终止该轮次。已记录的提示词仍是回放权威。具体的 user 和 steering chat-node 定义会关联紧邻前一条 session-reference 上下文中的标签,因此渲染器会从自身节点数据接收关联信息,并显示精简的来源摘要,而不是快照 JSON。
## 引用事务
```text
type @ → parallel file/session Remote calls → pick path text or canonical session chip
→ serialize draft → ordinary session.prompt enqueue
→ agent/pre-step parses mentions → capture sources → context + readable prompt
```
文件查询仅供参考且可取消;选择操作本身不会读取文件。会话准备针对一个已接受的模型步骤保持全有或全无。queued 消息被领取时会捕获每个源,因此队列编辑和从 queue 移动到 steer 使用同一路径,无需网关协调。
## 备选方案
**在 Web 客户端内部实现文件发现与语法。** 不予采纳,因为浏览器侧代码无法安全访问宿主工作区,而且重复的语法、排序、边界和失效处理会与宿主提供方产生偏差。
**通过普通文件系统工具 RPC 扫描文件。** 不予采纳,因为递归模糊发现属于编辑器低延迟工作,而不是面向模型的精确文件系统操作;该方案还会把菜单与工具策略及提供方往返绑定。
**选择文件时立即附加其内容。** 不予采纳,因为该方案会在尚未确定相关性时消耗上下文,并绕过可从日志重建、可审计的 `read` 调用/结果序列。
**用普通 `@label` 文本表示会话。** 不予采纳,因为标签既不稳定也不唯一,无法标识源快照。宿主生成的规范提及标记既能保留不透明会话身份,也能保持显示内容易读。
**提示词准入结算前清空输入框。** 不予采纳,因为传输或准入失败会丢失请求唯一可编辑的副本,并在视觉上错误表示一个从未成功的接受操作。
## 验证
包(package)测试固定共享文件语法和排序、缓存失效及生命周期清理、Web 并行查询、带引号的路径、候选项独立失败、取消、不改变候选项索引的分组标题、文件/目录继续补全、规范会话 chip、相邻引用及相邻文本条件下的引用投影、codec 无损往返、生成的 Remote 类型推断、pre-step 准备、下游拒绝,以及 chat node 自有的标签关联。无密钥的装配 Web 快照会渲染可用的引用分组,并通过真实客户端组合依次选择文件和会话引用。
## 后果
Web 现在使用共享的 `@file` 发现 seam 和结构化会话引用身份,宿主服务仍然是文件系统与会话访问的权威来源。文件和会话发现都是所属服务上的一元 Remote 契约,因此生成的客户端类型会替代手写 RPC 接口,浏览器 bundle 中也不包含 Node API。候选查询失败仍会让菜单静默降级。引用准备失败发生在提示词已接受之后,并会结束 agent 轮次。文件引用只产生路径文本和稳定的条件式指引成本,而会话引用仍保留 `dsh-session-reference` 所拥有的有界快照开销与信任限定文本。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md
2026-07-27-web-subagent-conversations.md: cdf2b12e69b359b8ab00d4b107aa78a21691ac4e
2026-07-27-web-subagent-conversations.zh.md: 697bfeef86e2ef456aed6ee6ad8eae1173c96a6f
2026-07-27-web-subagent-conversations.md: d20e55429d04d4308ae26d001575e664058c7dea
2026-07-27-web-subagent-conversations.zh.md: 67c9b5224abea6c0710303ae56e315ba34c7ac09
@@ -85,7 +85,7 @@ The shipped Web composition mounts SQLite session query beside JSONL persistence
**Put the adapter in the webserver.** Rejected because catalog and continuation are channel-independent client capabilities; the webserver only carries validated messages.
**Create a new UI package.** Rejected because `ui-subagent` already owns Web subagent references and is the coherent owner for catalog and addressed-child presentation.
**Put Host-backed file and session references in this package.** Rejected because catalog and addressed-child presentation depend on subagent lineage, while combined reference discovery is a separate Host capability consumed by [`ui-reference`](../../../../packages/client/ui-reference/README.md).
**Auto-resume an absent parent.** Rejected because continuation requires the exact live direct parent. Child navigation must not mutate the parent lifecycle.
@@ -85,7 +85,7 @@ one-shot 行始终会用文案替代输入框,说明执行记录为只读。
**将适配器放入 webserver。** 不予采纳,因为目录与继续执行是通道无关的客户端能力;webserver 只承载已校验的消息。
**新建 UI 包。** 不予采纳,因为 `ui-subagent` 已经负责 Web subagent 引用,也是目录与已寻址 child 呈现的统一 owner
**把由 Host 支撑的文件与会话引用放进本包。** 不予采纳,因为目录与已寻址 child 呈现依赖 subagent 谱系,而组合引用发现是独立的 Host 功能,由 [`ui-reference`](../../../../packages/client/ui-reference/README.md) 消费
**自动恢复缺失的 parent。** 不予采纳,因为继续执行要求确切的存活直接 parent。child 导航不得改变 parent 生命周期。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-12-open-ready-web-ui.md
2026-08-12-open-ready-web-ui.md: 075082aff6c0b811de3c6750637cb823fe83be61
2026-08-12-open-ready-web-ui.zh.md: de73ab0c8574ff924229b54c2aedf41899fedee8
@@ -0,0 +1,35 @@
# Agent Note: `dsh web` opens its ready page
Status: implemented
English | [中文](2026-08-12-open-ready-web-ui.zh.md)
## Problem
`dsh web` bound the HTTP server and printed its canonical local URL, but left the user to copy that URL into a browser even though the root README described the command as opening the Web UI. A browser handoff also cannot run at the server's bind callback alone: the API routes, browser plugin roster, and static fallback may still be mounting, so the first page request could observe an incomplete application that the process is about to reject.
## Decision
The Web app's command provider resolves `openBrowser: true` for an ordinary invocation and `false` for `--no-open`. The bundle passes that value into its `web-runtime` row; deployments may still replace the complete row config explicitly. The runtime samples inherited `SSH_CONNECTION` and `SSH_TTY` once during activation and suppresses browser handoff when either is non-empty, because the process then serves remote host loopback while the SSH client or editor owns the user's local forwarded address.
The Web runtime treats URL printing and browser opening as separate actions at one readiness point. It waits for the complete Loader tree to settle and confirms that `webServer` is still live, then prints the configured URL line and, outside SSH, prints `dsh web: opening the default browser; pass --no-open to disable` immediately before handing the canonical loopback URL to the operating system's default browser. An SSH launch keeps the host URL line so the operator can identify the remote port, but cannot derive or open the forwarding owner's local address. A deployment that explicitly binds all interfaces still opens loopback locally while the printed LAN URL remains informational; the CLI rejects `--host 0.0.0.0`. `openBrowser` and `printUrl` can be disabled independently.
The handoff uses the maintained `open` package for macOS, Windows, Linux, containers, and WSL. A short-lived Node helper invokes that package with the canonical scrubbed child environment, so Harness credentials and `DSH_*` state do not reach the operating-system launcher or a newly started browser. `BROWSER` is a launch-only command selector: app boot rejects it in a discovered `.env`, while only an inherited value can reach a compatible opener path that honors the variable. On Windows the helper waits for the short-lived PowerShell launcher to exit because `open` resolves when that process spawns, before it has handed the URL to the shell; other platforms stop after the opener accepts spawn. The runtime never waits for the browser to exit. The parent reads helper stderr so a failure writes one English diagnostic with the specific reason and manual URL to stderr without disposing the already-ready server; a later browser exit is outside the handoff result.
Unit coverage pins command defaults, `--no-open`, SSH suppression, readiness ordering, teardown and failure suppression, helper outcomes, stderr reason propagation, the Windows launcher lifetime, the scrubbed helper environment, the inherited-only `BROWSER` rule, the pre-handoff opt-out status, and the reason-bearing non-fatal diagnostic. A real Loader composition binds an OS-assigned port, serves the actual static fallback, replaces only the operating-system handoff, and requests the handed-off URL immediately to prove it is already reachable. Assembled keyless snapshots run the built `dsh web` command locally, with a failing opener, with VS Code plus SSH markers, and from a project that declares `BROWSER`: the local case verifies that the handed-off page is the printed, reachable page containing the boot manifest while credential and Harness-state variables are absent from the opener; the failure case verifies the stderr reason and manual URL after readiness; the remote case verifies that the host URL remains visible without a browser launch; the file-layer command case fails before readiness or handoff. Repository browser and packaging tests pass `--no-open` because they own their browser or run unattended.
## Alternatives considered
**Open from the CLI launcher** — rejected because the launcher deliberately knows only profile selection and cannot derive the OS-assigned port or the app-owned Loader settlement point without reversing the app-owned command-line decision.
**Open from `dsh-host-webserver` when its socket binds** — rejected because that package is a generic route carrier with no shell or frontend knowledge, and socket readiness precedes application readiness.
**Infer whether to open from TTY, CI, editor, display, container, or WSL variables** — rejected because those signals do not establish a host/browser split and misclassify detached terminals and desktop launches. Non-empty `SSH_CONNECTION` or `SSH_TTY` is narrower evidence: it identifies a remote host whose loopback URL is not the forwarding owner's local URL. The default plus explicit `--no-open` remains stable for non-SSH launches.
**Require Enter before opening the browser** — rejected for the local default because it turns ordinary server startup into a second stdin-owned interaction and excludes desktop or supervised launches with no usable terminal. `--no-open` remains the explicit opt-out for a caller that owns the browser or wants a server only.
**Hand-roll platform commands** — rejected because URL opening has distinct macOS, Windows, Linux, container, and WSL behavior. The maintained dependency owns those platform branches while this package retains only readiness and failure semantics.
## Consequences
An ordinary local `dsh web` invocation announces the automatic handoff and its `--no-open` opt-out, then opens one ready page without making the generic HTTP carrier desktop-aware or exposing its ambient credentials to the desktop launcher. An SSH invocation prints the remote host URL but leaves opening the forwarded local address to the SSH client or editor. A discovered `.env` that sets `BROWSER` fails launch instead of selecting an executable; a platform opener that honors the variable can read it only when the operator exports it in the launching shell. Other unattended consumers must pass `--no-open`; a handoff failure writes its reason and manual URL to stderr while preserving the usable server. The Web app gains the locked `open` dependency, the shared subprocess environment scrubber, and the opener's transitive platform helpers; it does not own, wait for, or terminate the browser after the operating-system handoff succeeds.
@@ -0,0 +1,35 @@
# Agent Note: `dsh web` 打开已就绪页面
Status: implemented
[English](2026-08-12-open-ready-web-ui.md) | 中文
## Problem
`dsh web` 会绑定 HTTP 服务器并打印规范本地 URL,但仍要求用户把 URL 复制到浏览器,尽管根 README 已把该命令描述为会打开 Web UI。浏览器交接也不能只以服务器绑定回调为时机:API 路由、浏览器插件名录和静态回退可能仍在挂载,第一次页面请求可能看到一个尚未完整且即将被进程判定为启动失败的应用。
## Decision
Web 应用的命令提供方为普通调用解析出 `openBrowser: true`,为 `--no-open` 解析出 `false`。组合包把该值传给自己的 `web-runtime` 行;部署仍可显式替换该行的完整配置。运行时在激活期间对继承的 `SSH_CONNECTION``SSH_TTY` 采样一次,只要其中一项非空就会跳过浏览器交接,因为此时进程提供的是远端宿主机 loopback,而用户的本地转发地址由 SSH 客户端或编辑器持有。
Web 运行时把 URL 打印与浏览器打开作为同一就绪点上的两个独立动作。它等待完整 Loader 配置树结算,并确认 `webServer` 仍在线,然后打印已配置的 URL 行;非 SSH 环境下还会在把规范 loopback URL 交给操作系统默认浏览器之前立即打印英文提示 `dsh web: opening the default browser; pass --no-open to disable`。SSH 启动会保留宿主机 URL 行,以便操作者识别远端端口,但进程无法推导或打开转发持有方的本地地址。部署显式绑定所有网络接口时,本机仍打开 loopback,打印出的 LAN URL 只用于告知;CLI 会拒绝 `--host 0.0.0.0``openBrowser``printUrl` 可以分别关闭。
交接使用维护中的 `open` 包处理 macOS、Windows、Linux、容器和 WSL。一个短生命周期 Node helper 使用规范的脱敏子进程环境调用该包,因此 Harness 凭据和 `DSH_*` 状态不会进入操作系统启动器或新启动的浏览器。`BROWSER` 是只能来自启动环境的命令选择器:应用启动过程会拒绝被发现的 `.env` 中的该变量,只有继承值才能抵达会读取该变量的兼容 opener 路径。在 Windows 上,helper 会等待短生命周期 PowerShell launcher 退出,因为 `open` 会在该进程 spawn 时、尚未把 URL 交给 shell 之前返回;其他平台则在 opener 接受 spawn 后结束。运行时绝不等待浏览器退出。父进程会读取 helper stderr,因此失败时只向 stderr 写入一条包含具体原因和手动访问 URL 的英文诊断,不会 dispose 已就绪的服务器;浏览器之后退出不属于本次交接结果。
单元覆盖钉住命令默认值、`--no-open`、SSH 抑制、就绪顺序、资源释放与失败抑制、helper 结果、stderr 原因传播、Windows launcher 生命周期、helper 的脱敏环境、`BROWSER` 仅可继承的规则、交接前 opt-out 提示以及包含原因的非致命诊断。真实 Loader 组合会绑定由操作系统分配的端口、提供实际静态回退,只替换操作系统交接,并立即请求被交接的 URL,以证明页面此时已可访问。无密钥的整体快照会分别在本机环境、opener 失败环境、带 VS Code 与 SSH 标记的环境,以及声明了 `BROWSER` 的项目中运行构建后的 `dsh web` 命令:本机用例验证被交接的页面就是打印出的、已可访问且包含启动清单的页面,同时 opener 中不存在凭据与 Harness 状态变量;失败用例验证就绪后的 stderr 原因和手动 URL;远端用例验证宿主机 URL 仍可见,但不会启动浏览器;文件层命令用例则在就绪或交接前失败。仓库内浏览器与打包测试会传入 `--no-open`,因为它们自行持有浏览器或在无人值守环境运行。
## Alternatives considered
**从 CLI 启动器打开** — 否决,因为启动器刻意只了解 profile 选择,无法取得操作系统分配的端口或应用自有的 Loader 结算点;让它了解这些事实会推翻应用自有命令行决策。
**在 `dsh-host-webserver` 绑定 socket 时打开** — 否决,因为该包是不了解 shell 与前端的通用路由载体,而且 socket 就绪早于应用就绪。
**根据 TTY、CI、编辑器、显示、容器或 WSL 环境变量推断是否打开** — 否决,因为这些信号不能证明宿主机与浏览器分离,并会误判分离终端和桌面启动。非空的 `SSH_CONNECTION``SSH_TTY` 是更窄的证据:它表明远端宿主机 loopback URL 并不是转发持有方的本地 URL。非 SSH 启动仍保持默认打开并提供显式 `--no-open`
**打开浏览器前要求按下 Enter** — 不作为本机默认行为,因为它会把普通服务器启动变成由 stdin 持有的第二次交互,并排除没有可用终端的桌面启动或受监督启动。调用方自行持有浏览器或只需要服务器时,仍通过 `--no-open` 显式退出。
**手写各平台命令** — 否决,因为 URL 打开在 macOS、Windows、Linux、容器和 WSL 上各有不同。维护中的依赖持有这些平台分支,本包只保留就绪与失败语义。
## Consequences
普通的本机 `dsh web` 调用会先公告自动交接及其 `--no-open` 退出方式,再打开一个已就绪的页面,同时不会让通用 HTTP 载体感知桌面环境,也不会向桌面启动器暴露环境凭据。SSH 调用会打印远端宿主机 URL,但由 SSH 客户端或编辑器负责打开转发后的本地地址。被发现的 `.env` 如果设置 `BROWSER`,启动就会失败,而不是选择一个可执行文件;会读取该变量的平台 opener 只有在操作者从启动 shell 中 export 时才能取得它。其他无人值守消费方必须传入 `--no-open`;交接失败时会向 stderr 写入原因与手动访问 URL,同时保留可用服务器。Web 应用新增锁定的 `open` 依赖、共享子进程环境脱敏器及 opener 的传递平台辅助包;操作系统交接成功后,本应用不持有、不等待也不终止浏览器。
+5 -9
View File
@@ -76,15 +76,11 @@ jobs:
- name: Install (immutable)
run: pnpm install --frozen-lockfile
# The with-key escalation e2e self-skips without a usable runner. Install
# bwrap so trusted CI exercises it; the userns knob is best-effort and the
# test's functional probe decides.
- name: Install bubblewrap (unrestrict userns)
run: |
sudo apt-get update -q
sudo apt-get install -yq bubblewrap
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 \
|| echo "apparmor userns knob absent — the functional probe decides"
# The with-key escalation e2e self-skips without a usable runner. Prepare
# bwrap so trusted CI exercises it; the functional probe remains the
# authority on whether the runner can use the sandbox.
- name: Prepare bubblewrap (unrestrict userns)
run: bash scripts/prepare-ci-bubblewrap.sh
# Guard against a false green: the e2e suites self-skip when the key is
# absent, so a missing/misconfigured secret would otherwise pass as
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write README.md
README.md: 8a4bd01332a23ce4144c661784bc549e0ba72d21
README.zh.md: c507bf884bd426feead6a96adbdb5c136456e3b5
README.md: 9ccd27b8934449bd0d2311317dc38aee5a5c0cdc
README.zh.md: 7eb9ef1a62afbcf95b235e5b90fa7529869af59d
+3 -1
View File
@@ -20,7 +20,7 @@ Install `Node.js`, then run:
npx @deepseek-ai/dsh web
```
The command starts the Web UI, served at `http://127.0.0.1:3080` by default. See [Web UI guide](docs/user/guide/index.md).
The command starts the Web UI at `http://127.0.0.1:3080` by default and opens it in the default browser for a local launch. An SSH launch only prints the host URL because the SSH client or editor owns the local forwarded address. Pass `--no-open` to run the server without opening a browser. See [Web UI guide](docs/user/guide/index.md).
### Run from source
@@ -34,6 +34,8 @@ pnpm run build
pnpm dsh web
```
`pnpm run build` prepares the repository artifacts. `pnpm dsh web` uses those built artifacts without rebuilding.
## Community and support
- Feel free to submit feedback or bug reports through [GitHub Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions).
+3 -1
View File
@@ -20,7 +20,7 @@ DeepSeek Harness 目前处于 _开发者预览_ 阶段,正在快速迭代。**
npx @deepseek-ai/dsh web
```
该命令会启动 Web UI,默认地址为 `http://127.0.0.1:3080`。详见 [Web UI 指南](docs/user/guide/index.md)。
该命令默认会在 `http://127.0.0.1:3080` 启动 Web UI,本机启动时还会用默认浏览器打开页面。通过 SSH 启动时只打印宿主机 URL,因为本地转发地址由 SSH 客户端或编辑器持有。传入 `--no-open` 可仅运行服务器而不打开浏览器。详见 [Web UI 指南](docs/user/guide/index.md)。
### 从源码运行
@@ -34,6 +34,8 @@ pnpm run build
pnpm dsh web
```
`pnpm run build` 会准备仓库产物。`pnpm dsh web` 会直接使用这些已构建产物,不会重新构建。
## 社区与支持
- 欢迎通过 [GitHub Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions) 提交反馈或 bug 报告。
+1
View File
@@ -77,6 +77,7 @@ External packages that a workspace package resolves at runtime. The tier covers
| [`micromark-util-types`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-types) | MIT |
| [`node-addon-require-builtin`](https://www.npmjs.com/package/node-addon-require-builtin) | MIT |
| [`node-pty`](https://github.com/microsoft/node-pty) | MIT |
| [`open`](https://github.com/sindresorhus/open) | MIT |
| [`picomatch`](https://github.com/micromatch/picomatch) | MIT |
| [`react`](https://github.com/facebook/react) | MIT |
| [`react-dom`](https://github.com/facebook/react) | MIT |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/cli/reference/README.md
README.md: 7828f55a2e4adfd85a0018baada6945ea75aacb0
README.zh.md: e14e13731c314efd4d39913b91f2e90ba624e55c
README.md: e60f9d9e00dc77c8b2f22edcba67f9e8f3ba2f07
README.zh.md: a99531a67d447039803a7a8248f2cc30a19d9be9
+4 -3
View File
@@ -24,7 +24,7 @@ The shipped apps own these command lines:
| Profile | Arguments |
|---|---|
| `web` | `--host`, `--port`, repeatable `--trusted-host` |
| `web` | `--host`, `--port`, repeatable `--trusted-host`, `--no-open` |
| `headless` | the task text, as the positional argument |
A one-shot task (`dsh --profile headless "run the tests"`) creates one fresh persisted Agent through the core registry, submits the task, waits for quiescence, and flushes the Session before deriving the last non-empty assistant text and final `turn/end` reason from its durable interval. It prints the text on stdout and exits 0 for `completed`, else 1. An invocation with no task is a usage error from that app. The shipped headless profile mounts no ApiProxy, Host, HTTP server, Web runtime, or browser client; a successful run writes nothing to stderr and opens no listening port.
@@ -64,16 +64,17 @@ Git-hosted plugins that ship sources build during install through their `prepare
## Web alias
`dsh web` is a hardcoded alias for `--profile web`; the flags after it belong to the web app, whose ordinary bundle provider parses them. `--host` and `--port` override the composed values of the rows that carry them, and repeatable `--trusted-host` contributes invocation authorities through `ctx.webRuntime.trustedHosts` (a deployment expression concatenates its own authorities). The client-plugin HMR receiver is always mounted and stays idle until a separate `pnpm run dev:web` watcher rebuilds client bundles.
`dsh web` is a hardcoded alias for `--profile web`; the flags after it belong to the web app, whose ordinary bundle provider parses them. `--host` and `--port` override the composed values of the rows that carry them, repeatable `--trusted-host` contributes invocation authorities through `ctx.webRuntime.trustedHosts` (a deployment expression concatenates its own authorities), and `--no-open` disables the default-browser handoff for this invocation. The client-plugin HMR receiver is always mounted and stays idle until a separate `pnpm run dev:web` watcher rebuilds client bundles.
```sh
dsh web
dsh web --no-open
dsh web --patch ./extra.cordis.yml
dsh web --dump-config
dsh web --help
```
The production Web runner needs built package and frontend artifacts (`pnpm run build`). It serves `http://127.0.0.1:3080` by default. The CLI intentionally does not support `--host 0.0.0.0` yet and exits with a usage error; `--trusted-host` adds named authorities accepted by the `/api` browser-trust fence.
The production Web runner needs built package and frontend artifacts (`pnpm run build`). It serves `http://127.0.0.1:3080` by default and, for a local launch, opens that canonical host URL only after the complete Loader tree settles. A non-empty inherited `SSH_CONNECTION` or `SSH_TTY` suppresses the browser handoff because the SSH client or editor owns the local forwarded address; the host URL is still printed. The CLI intentionally does not support `--host 0.0.0.0` yet and exits with a usage error. Immediately before a local handoff it prints `dsh web: opening the default browser; pass --no-open to disable`; if the operating-system handoff fails, a diagnostic on stderr states the reason, leaves the server running, and names the URL for manual use. `--trusted-host` adds named authorities accepted by the `/api` browser-trust fence.
Process shutdown gives the plugin tree up to five seconds to dispose. The first `SIGINT`/`SIGTERM` starts that graceful drain — `SIGTERM` is a supervisor's ordinary stop request and exits 0 on every surface, `SIGINT` reports 130; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first `Ctrl+C` is the escalation and exits immediately instead of being swallowed.
+4 -3
View File
@@ -24,7 +24,7 @@
| Profile | 参数 |
|---|---|
| `web` | `--host``--port`、可重复的 `--trusted-host` |
| `web` | `--host``--port`、可重复的 `--trusted-host``--no-open` |
| `headless` | 任务文本,作为位置参数 |
一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent(智能体),提交任务、等待完全停稳并对会话执行 flush,再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 stdout 打印文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载 ApiProxy、Host、HTTP 服务器、Web 运行时或浏览器客户端;成功运行不会向 stderr 写入任何内容,也不会打开监听端口。
@@ -64,16 +64,17 @@ dsh --profile tui
## Web 别名
`dsh web``--profile web` 的硬编码别名;写在它之后的 flag 属于 web 应用,由组合包中的普通提供方解析。`--host``--port` 覆盖承载它们的那些行的组合取值,可重复的 `--trusted-host` 通过 `ctx.webRuntime.trustedHosts` 提供本次调用的 authority(部署表达式会拼接自己的 authority),客户端插件 HMR(热模块替换)接收器始终挂载,在单独运行的 `pnpm run dev:web` watcher 重建客户端 bundle 之前保持空闲。
`dsh web``--profile web` 的硬编码别名;写在它之后的 flag 属于 web 应用,由组合包中的普通提供方解析。`--host``--port` 覆盖承载它们的那些行的组合取值,可重复的 `--trusted-host` 通过 `ctx.webRuntime.trustedHosts` 提供本次调用的 authority(部署表达式会拼接自己的 authority),`--no-open` 则只对本次调用关闭默认浏览器交接。客户端插件 HMR(热模块替换)接收器始终挂载,在单独运行的 `pnpm run dev:web` watcher 重建客户端 bundle 之前保持空闲。
```sh
dsh web
dsh web --no-open
dsh web --patch ./extra.cordis.yml
dsh web --dump-config
dsh web --help
```
生产 Web 运行器需要已构建的包和前端产物(`pnpm run build`)。默认服务地址是 `http://127.0.0.1:3080`。CLI 目前有意不支持 `--host 0.0.0.0`,并会以用法错误退出;`--trusted-host` 可添加 `/api` 浏览器信任围栏接受的具名 authority。
生产 Web 运行器需要已构建的包和前端产物(`pnpm run build`)。默认服务地址是 `http://127.0.0.1:3080`;本机启动时,只在完整 Loader 配置树结算后才用默认浏览器打开该规范宿主机 URL。继承的 `SSH_CONNECTION``SSH_TTY` 非空时会跳过浏览器交接,因为本地转发地址由 SSH 客户端或编辑器持有;宿主机 URL 仍会打印。CLI 目前有意不支持 `--host 0.0.0.0`,并会以用法错误退出。本机交接前会打印英文提示 `dsh web: opening the default browser; pass --no-open to disable`;若操作系统交接失败,stderr 诊断会说明原因、给出 URL 供手动访问,服务器仍继续运行。`--trusted-host` 可添加 `/api` 浏览器信任围栏接受的具名 authority。
进程关闭时,插件树最多有 5 秒完成 dispose。首次收到 `SIGINT``SIGTERM` 时会开始优雅排空:`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出;`SIGINT` 则报告 130。第二次收到信号时会立即强制退出。如果一次性运行在正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略。
+2 -2
View File
@@ -36,8 +36,8 @@ describe('parseDshArgs', () => {
.toEqual({ mode: 'profile', profile: 'tui', patches: [], args: ['--resume', 'abc'] })
expect(parse(['--profile', 'web', '-h']))
.toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['-h'] })
expect(parse(['web', '--host', '127.0.0.1', '--port', '8080', '--dev']))
.toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['--host', '127.0.0.1', '--port', '8080', '--dev'] })
expect(parse(['web', '--host', '127.0.0.1', '--port', '8080', '--no-open', '--future-web-flag']))
.toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['--host', '127.0.0.1', '--port', '8080', '--no-open', '--future-web-flag'] })
expect(parse(['--profile', 'headless', 'run', 'the', 'tests']))
.toEqual({ mode: 'profile', profile: 'headless', patches: [], args: ['run', 'the', 'tests'] })
// Launcher flags placed after that boundary belong to the app too.
+42
View File
@@ -0,0 +1,42 @@
import { spawn } from 'node:child_process'
import { join } from 'node:path'
const handoffProbe = `
const { writeFileSync } = require('node:fs')
const marker = process.argv[1]
const helperPid = Number(process.argv[2])
setTimeout(() => {
let helperAlive = true
if (process.platform === 'win32') {
try {
process.kill(helperPid, 0)
} catch {
helperAlive = false
}
}
if (helperAlive) writeFileSync(marker, '')
}, 50)
`
export default async function open(url) {
if (process.env.BROWSER_OPEN_TEST_FAILURE !== undefined) {
throw new Error(process.env.BROWSER_OPEN_TEST_FAILURE)
}
const response = await fetch(url)
const html = await response.text()
console.log(`dsh browser-open: ${JSON.stringify({
url,
status: response.status,
bootManifest: html.includes('__DSH_BOOT__'),
apiKeyPresent: process.env.DEEPSEEK_API_KEY !== undefined,
dshHomePresent: process.env.DSH_HOME !== undefined,
})}`)
// The Windows launcher writes the server-exit marker only while its helper
// remains alive, so the assembled test detects an early helper exit.
const launcher = spawn(process.execPath, [
'--eval', handoffProbe,
'--', join(process.cwd(), `.dsh-browser-open-${process.ppid}`), String(process.pid),
], { stdio: 'ignore' })
launcher.unref()
return launcher
}
+41
View File
@@ -0,0 +1,41 @@
import { existsSync, rmSync } from 'node:fs'
import { registerHooks } from 'node:module'
import { join } from 'node:path'
const openerUrl = new URL('./open.mjs', import.meta.url).href
const exitMarker = join(process.cwd(), `.dsh-browser-open-${process.pid}`)
const markerPoll = setInterval(() => {
if (!existsSync(exitMarker)) return
rmSync(exitMarker, { force: true })
process.exit(0)
}, 25)
markerPoll.unref()
registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === 'open') return { shortCircuit: true, url: openerUrl }
return nextResolve(specifier, context)
},
})
// The SSH case has no opener helper to stop the long-lived Web process.
if (process.env.DSH_BROWSER_OPEN_TEST_EXIT_ON_READY === '1') {
const originalLog = console.log
console.log = (...args) => {
originalLog(...args)
if (typeof args[0] === 'string' && args[0].startsWith('dsh web: ')) {
setTimeout(() => process.exit(0), 250)
}
}
}
if (process.env.DSH_BROWSER_OPEN_TEST_EXIT_ON_FAILURE === '1') {
const originalError = console.error
console.error = (...args) => {
originalError(...args)
if (typeof args[0] === 'string' && args[0].startsWith('web-app: could not open the default browser because ')) {
setTimeout(() => process.exit(0), 0)
}
}
}
@@ -57,6 +57,7 @@ function runBuiltWeb(cwd: string): Promise<{ stdout: string; stderr: string; cod
const child = spawn(process.execPath, [
builtBin,
'web',
'--no-open',
'--host',
'127.0.0.1',
'--port',
+239
View File
@@ -0,0 +1,239 @@
/** Assembled keyless snapshot for the default `dsh web` browser handoff. */
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { execa } from 'execa'
import { afterEach, describe, expect, it } from 'vitest'
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
const builtBin = join(repoRoot, 'apps/cli/lib/bin.js')
const frontendIndex = join(repoRoot, 'apps/web/dist/index.html')
const openerHook = new URL('./fixtures/web-browser-open/register.mjs', import.meta.url).href
const openingMessage = 'dsh web: opening the default browser; pass --no-open to disable'
const tempRoots: string[] = []
const builtArtifactsExist = existsSync(builtBin) && existsSync(frontendIndex)
if (process.env.DSH_EXAMPLE_MODE === 'lib' && !builtArtifactsExist) {
throw new Error('dsh web browser-open snapshot requires built CLI and Web artifacts in lib mode')
}
afterEach(() => {
for (const root of tempRoots.splice(0)) rmSync(root, { recursive: true, force: true })
})
interface BrowserOpenRecord {
url: string
status: number
bootManifest: boolean
apiKeyPresent: boolean
dshHomePresent: boolean
}
function normalizeLocalUrl(url: string): string {
return url.replace(/:\d+$/, ':{{port}}')
}
describe.skipIf(!builtArtifactsExist)('dsh web browser-open assembled snapshot', () => {
it('hands the reachable page to the default browser after the shipped tree settles', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-web-browser-open-snapshot-'))
tempRoots.push(root)
const result = await execa(process.execPath, [
'--import', openerHook,
builtBin,
'web',
'--port', '0',
], {
cwd: root,
env: {
...process.env,
DEEPSEEK_API_KEY: 'keyless-browser-open-no-call',
DSH_AGENTS_HOME: join(root, '.agents'),
DSH_HOME: join(root, '.dsh'),
DSH_TELEMETRY_DISABLED: '1',
NODE_NO_WARNINGS: '1',
SSH_CONNECTION: '',
SSH_TTY: '',
},
input: '',
timeout: 30_000,
killSignal: 'SIGKILL',
reject: false,
})
const readyUrl = /dsh web: (http:\/\/[^\s]+)/u.exec(result.stdout)?.[1]
const openLine = result.stdout.split('\n').find(line => line.startsWith('dsh browser-open: '))
const opening = result.stdout.includes(openingMessage)
if (readyUrl === undefined || openLine === undefined || !opening) {
throw new Error(`dsh web browser-open evidence missing\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
}
const opened = JSON.parse(openLine.slice('dsh browser-open: '.length)) as BrowserOpenRecord
expect({
exitCode: result.exitCode,
opening,
readyUrl: normalizeLocalUrl(readyUrl),
openedUrl: normalizeLocalUrl(opened.url),
status: opened.status,
bootManifest: opened.bootManifest,
apiKeyPresent: opened.apiKeyPresent,
dshHomePresent: opened.dshHomePresent,
stderr: result.stderr,
}).toMatchInlineSnapshot(`
{
"apiKeyPresent": false,
"bootManifest": true,
"dshHomePresent": false,
"exitCode": 0,
"openedUrl": "http://127.0.0.1:{{port}}",
"opening": true,
"readyUrl": "http://127.0.0.1:{{port}}",
"status": 200,
"stderr": "",
}
`)
})
it('prints the launcher reason and manual URL after the Web app is ready', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-web-browser-open-failure-snapshot-'))
tempRoots.push(root)
const result = await execa(process.execPath, [
'--import', openerHook,
builtBin,
'web',
'--port', '0',
], {
cwd: root,
env: {
...process.env,
BROWSER_OPEN_TEST_FAILURE: 'fixture desktop unavailable',
DEEPSEEK_API_KEY: 'keyless-browser-open-no-call',
DSH_AGENTS_HOME: join(root, '.agents'),
DSH_BROWSER_OPEN_TEST_EXIT_ON_FAILURE: '1',
DSH_HOME: join(root, '.dsh'),
DSH_TELEMETRY_DISABLED: '1',
NODE_NO_WARNINGS: '1',
SSH_CONNECTION: '',
SSH_TTY: '',
},
input: '',
timeout: 30_000,
killSignal: 'SIGKILL',
reject: false,
})
const readyUrl = /dsh web: (http:\/\/[^\s]+)/u.exec(result.stdout)?.[1]
const diagnostic = result.stderr.split(/\r?\n/u)
.find(line => line.startsWith('web-app: could not open the default browser because '))
?.replace(/http:\/\/127\.0\.0\.1:\d+/u, 'http://127.0.0.1:{{port}}')
expect({
diagnostic,
exitCode: result.exitCode,
opened: result.stdout.includes('dsh browser-open: '),
opening: result.stdout.includes(openingMessage),
readyUrl: readyUrl === undefined ? undefined : normalizeLocalUrl(readyUrl),
}).toMatchInlineSnapshot(`
{
"diagnostic": "web-app: could not open the default browser because fixture desktop unavailable; visit http://127.0.0.1:{{port}} manually",
"exitCode": 0,
"opened": false,
"opening": true,
"readyUrl": "http://127.0.0.1:{{port}}",
}
`)
})
it('prints the host URL without launching a browser in a VS Code Remote SSH session', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-web-browser-open-ssh-snapshot-'))
tempRoots.push(root)
const result = await execa(process.execPath, [
'--import', openerHook,
builtBin,
'web',
'--port', '0',
], {
cwd: root,
env: {
...process.env,
DEEPSEEK_API_KEY: 'keyless-browser-open-no-call',
DSH_AGENTS_HOME: join(root, '.agents'),
DSH_BROWSER_OPEN_TEST_EXIT_ON_READY: '1',
DSH_HOME: join(root, '.dsh'),
DSH_TELEMETRY_DISABLED: '1',
NODE_NO_WARNINGS: '1',
SSH_CONNECTION: '10.0.0.2 55000 10.0.0.9 22',
SSH_TTY: '',
VSCODE_IPC_HOOK_CLI: '/tmp/vscode-ipc',
},
input: '',
timeout: 30_000,
killSignal: 'SIGKILL',
reject: false,
})
const readyUrl = /dsh web: (http:\/\/[^\s]+)/u.exec(result.stdout)?.[1]
expect({
exitCode: result.exitCode,
opening: result.stdout.includes(openingMessage),
readyUrl: readyUrl === undefined ? undefined : normalizeLocalUrl(readyUrl),
opened: result.stdout.includes('dsh browser-open: '),
stderr: result.stderr,
}).toMatchInlineSnapshot(`
{
"exitCode": 0,
"opened": false,
"opening": false,
"readyUrl": "http://127.0.0.1:{{port}}",
"stderr": "",
}
`)
})
it('rejects a project browser command before starting the Web app', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-web-browser-open-env-snapshot-'))
tempRoots.push(root)
writeFileSync(join(root, '.env'), 'BROWSER=./project-browser\n')
const result = await execa(process.execPath, [
'--import', openerHook,
builtBin,
'web',
'--port', '0',
], {
cwd: root,
env: {
...process.env,
DEEPSEEK_API_KEY: 'keyless-browser-open-no-call',
DSH_AGENTS_HOME: join(root, '.agents'),
DSH_HOME: join(root, '.dsh'),
DSH_TELEMETRY_DISABLED: '1',
NODE_NO_WARNINGS: '1',
SSH_CONNECTION: '',
SSH_TTY: '',
},
input: '',
timeout: 30_000,
killSignal: 'SIGKILL',
reject: false,
})
const diagnostic = result.stderr.split(/\r?\n/u)
.find(line => line.startsWith('Error: dsh: '))
?.replace(/^Error: dsh: .*[/\\]\.env/u, 'dsh: {{root}}/.env')
expect({
diagnostic,
exitCode: result.exitCode,
opening: result.stdout.includes(openingMessage),
opened: result.stdout.includes('dsh browser-open: '),
ready: result.stdout.includes('dsh web: '),
}).toMatchInlineSnapshot(`
{
"diagnostic": "dsh: {{root}}/.env sets "BROWSER", which only the launching environment may set (it decides how this process starts, where its code and instructions load from, or how it reaches the network); export BROWSER instead of putting it in a .env file",
"exitCode": 1,
"opened": false,
"opening": false,
"ready": false,
}
`)
})
})
+1 -1
View File
@@ -92,7 +92,7 @@ it('hot-reloads a real client-plugin source edit without refreshing the page', a
watcher = subprocessCtx.subprocess.spawn(spawnSpec(['pnpm', 'run', 'dev:web'], REPO_ROOT))
await waitForOutput(watcher, /dev-web: watching/, 'pnpm run dev:web')
host = subprocessCtx.subprocess.spawn(spawnSpec(
[process.execPath, binPath, 'web', '--port', '0'],
[process.execPath, binPath, 'web', '--no-open', '--port', '0'],
world,
{
DEEPSEEK_API_KEY: 'keyless-hmr-no-call',
+113
View File
@@ -0,0 +1,113 @@
// Web e2e scenario: the shipped composition discovers local files and cold
// sessions through the real Host, groups both domains in the shared @ menu,
// and projects each pick back into the composer without issuing a model call.
import { writeFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import {
SESSION_FORMAT_VERSION,
Session,
SessionId,
} from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-session-title'
import {
assertFixtureInventory,
captureStableAria,
compareOrRefreshGolden,
launchWebScaffold,
seedSession,
watchConsole,
webSnapshotMode,
type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/reference-composer', import.meta.url))
const MENU_EXPECTED = join(SNAPSHOT_DIR, 'menu.expected.md')
const MODE = webSnapshotMode()
const SOURCE_SESSION_ID = 'reference-source-session'
/** Build one closed source session with a stable title for reference discovery. */
function sourceSessionFixture(): string {
const session = Session.create(SessionId(SOURCE_SESSION_ID))
session.append('turn/start', {
turn: 1,
})
const user = session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'Research context for the reference menu.' }],
source: { kind: 'user' },
}), { surfaceOp: 'append' })
session.append('session/title', {
title: 'Research notes',
messageSeqs: [user.seq],
source: { kind: 'fallback' },
})
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
return [
JSON.stringify({
type: 'session',
version: SESSION_FORMAT_VERSION,
id: '{{sessionId}}',
createdAt: 0,
cwd: '{{cwd}}',
}),
...session.events.map(event => JSON.stringify(event)),
'',
].join('\n')
}
describe.skipIf(MODE === 'record')('web e2e: file and session references through the real host', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
let tripwire: ReturnType<typeof watchConsole>
beforeAll(async () => {
scaffold = await launchWebScaffold({})
await seedSession(scaffold, sourceSessionFixture(), SOURCE_SESSION_ID)
browser = await chromium.launch()
page = await newEnglishPage(browser)
tripwire = watchConsole(page)
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await connectFreshWorkspace(page, scaffold.workspaceCwd)
await writeFile(join(scaffold.workspaceCwd, 'workspace', 'reference.txt'), 'reference fixture\n')
}, 120_000)
afterAll(async () => {
await browser?.close()
await scaffold?.close()
})
it('groups both sources and projects file text plus an atomic session chip', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-reference-composer'))
const input = page.locator('textarea').first()
const menu = page.getByRole('listbox', { name: 'Trigger suggestions' })
await input.fill('@')
await expect.poll(() => menu.getByRole('option').count(), { timeout: 15_000 }).toBeGreaterThanOrEqual(2)
const snapshot = await captureStableAria(page, '[role="listbox"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(MENU_EXPECTED, snapshot, MODE)
expect(snapshot).toContain('Files & folders')
expect(snapshot).toContain('Session conversations')
expect(snapshot).toContain('File \u00b7 reference.txt')
expect(snapshot).toContain('Session \u00b7 Research notes')
expect(snapshot).not.toContain('text: Subagents')
await input.fill('@reference')
await menu.getByRole('option', { name: /File \u00b7 reference\.txt/ }).click()
await expect.poll(() => input.inputValue()).toBe('@reference.txt ')
await input.fill('@Research')
await menu.getByRole('option', { name: /Session \u00b7 Research notes/ }).click()
await expect.poll(() => page.locator('[data-decoration="chip"]').textContent()).toBe('@Research notes')
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
await assertFixtureInventory(SNAPSHOT_DIR, ['menu.expected.md'])
})
})
+6 -4
View File
@@ -458,10 +458,11 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
config: { host: '127.0.0.1', port: 0 },
},
// The bundle's web-runtime row resolves the same built dist under test
// (apps/web IS @deepseek-ai/dsh-web-frontend); only the URL line is silenced.
// (apps/web IS @deepseek-ai/dsh-web-frontend); native browser opening and the
// URL line are disabled because this scaffold owns its Playwright browser.
// Preserve the composed surface-context choice because a patch replaces
// the row's complete config.
{ id: 'web-runtime', config: { printUrl: false, surfaceContext } },
{ id: 'web-runtime', config: { openBrowser: false, printUrl: false, surfaceContext } },
...options.remoteAuthority === undefined
? []
: [{ id: 'connection', config: { trustedHosts: [options.remoteAuthority] } }],
@@ -856,8 +857,9 @@ function normalizeAria(snapshot: string, workspaceCwd: string): string {
// Seeded compaction prices realized file paths, whose length differs
// between local worktrees and CI scratch directories.
.replace(/(Compacted \d+ history items \(~)\d+( tokens\))/g, '$1{{tokens}}$2')
// Message IconActions clocks widen by calendar day/year; collapse every
// format so goldens stay stable across midnight and year changes.
// Session summaries and Message IconActions clocks cross calendar
// boundaries; collapse every shape so goldens stay stable across them.
.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, '{{timestamp}}')
.replace(/\d{4}年\d{1,2}月\d{1,2}日 \d{2}:\d{2}/g, '{{clock}}')
.replace(/\d{1,2}月\d{1,2}日 \d{2}:\d{2}/g, '{{clock}}')
.replace(/(?<!\d)\d{1,2}:\d{2}:\d{2}(?:\.\d+)?(?:\s*[AP]M)?(?!\d)/gi, '{{clock}}')
+5 -4
View File
@@ -160,7 +160,7 @@ describe('dsh web keyless CLI smoke', () => {
const tsxLoader = pathToFileURL(createRequire(join(REPO_ROOT, 'package.json')).resolve('tsx')).href
const child = spawn(
process.execPath,
['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', '0'],
['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--no-open', '--port', '0'],
{
cwd: sessionsDir,
env: {
@@ -226,7 +226,7 @@ describe('dsh web keyless CLI smoke', () => {
const tsxLoader = pathToFileURL(createRequire(join(REPO_ROOT, 'package.json')).resolve('tsx')).href
const child = spawn(
process.execPath,
['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', '0'],
['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--no-open', '--port', '0'],
{
cwd: workspace,
env: {
@@ -339,7 +339,7 @@ describe('dsh web keyless CLI smoke', () => {
const tsxLoader = pathToFileURL(createRequire(join(REPO_ROOT, 'package.json')).resolve('tsx')).href
const child = spawn(
process.execPath,
['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', '0'],
['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--no-open', '--port', '0'],
{
cwd: workspace,
env: {
@@ -421,7 +421,7 @@ describe('dsh web keyless CLI smoke', () => {
const tsxLoader = pathToFileURL(createRequire(join(REPO_ROOT, 'package.json')).resolve('tsx')).href
const child = spawn(
process.execPath,
['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', '0'],
['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--no-open', '--port', '0'],
{
cwd: workspace,
env: {
@@ -491,6 +491,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
// Pin the in-browser picker: the shipped `-auto` row would resolve to
// the native OS chooser on this bind, and no page can drive that.
'--patch', fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)),
'--no-open',
'--port', String(port),
],
{
@@ -20,9 +20,13 @@
- text: Stopped
- button "Copy":
- img
- button "Good response":
- img
- button "Bad response":
- img
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}}
- text: {{clock}} Ran for {{duration}} TTFT {{duration}}
- textbox "Message the agent"
- button "Commands":
- img
@@ -31,4 +35,4 @@
- text: DeepSeek-V4-Flash
- img
- button "Send message" [disabled]
- text: 1 turns · 1 steps
- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}}
@@ -20,9 +20,13 @@
- text: Stopped
- button "Copy":
- img
- button "Good response":
- img
- button "Bad response":
- img
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}}
- text: {{clock}} Ran for {{duration}} TTFT {{duration}}
- button "2 queued messages" [expanded]
- list:
- listitem:
@@ -50,4 +54,4 @@
- text: DeepSeek-V4-Flash
- img
- button "Send message" [disabled]
- text: 1 turns · 1 steps
- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}}
@@ -0,0 +1,5 @@
- listbox "Trigger suggestions":
- text: reference Files & folders
- option "File · reference.txt reference.txt" [selected]
- text: Session conversations
- option "Session · Research notes reference-source-session · {{cwd}} · {{timestamp}}"
@@ -30,6 +30,10 @@
- button "Copy":
- img
- tooltip "Copy"
- button "Good response":
- img
- button "Bad response":
- img
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
+1 -2
View File
@@ -106,9 +106,8 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
// this exact occurrence into the current turn's steering outbox.
await input.fill(STEER)
await input.press('Enter')
const queued = page.getByText(STEER, { exact: true })
await queued.waitFor({ timeout: 10_000 })
const queuedRow = page.getByRole('listitem').filter({ hasText: STEER })
await queuedRow.waitFor({ timeout: 10_000 })
const steerButton = queuedRow.getByRole('button', { name: 'Steer queued message' })
await expect.poll(() => steerButton.isEnabled(), { timeout: 10_000 }).toBe(true)
await steerButton.click({ timeout: 10_000 })
+1
View File
@@ -63,6 +63,7 @@
"tests/message-feedback.e2e.ts",
"tests/message-feedback-layout.e2e.ts",
"tests/markdown-images.e2e.ts",
"tests/reference-composer.e2e.ts",
"tests/math-rendering.e2e.ts",
"tests/markdown-cjk-strong.e2e.ts",
"tests/markdown-inline-code-links.e2e.ts",
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/capability-seams.md
capability-seams.md: 710c399510b6b123123e0a9586d86bfc3a96dff9
capability-seams.zh.md: e0559d464ba1ecd2160eaba40c24bfd829e4a0d6
capability-seams.md: 23fc498cc280518489aadbe752a1702d43f17972
capability-seams.zh.md: 0a8822bc8d1f0296ac6698b59fda60666faaf54a
+6
View File
@@ -66,6 +66,9 @@ flowchart LR
svc_sessionQuery["ctx.sessionQuery<br/>Session reads, traces, filters, and search"]
pkg_session_reference["session-reference"]
pkg_tool_session_query["tool-session-query"]
pkg_file_reference["file-reference"]
svc_fileReferences["ctx.fileReferences<br/>File reference discovery"]
pkg_file_reference_local["file-reference-local"]
svc_sessionReferenceResolver["ctx.sessionReferenceResolver<br/>Cross-session snapshot preparation"]
pkg_session_title["session-title"]
svc_sessionTitle["ctx.sessionTitle<br/>Log-backed session titles"]
@@ -222,6 +225,8 @@ flowchart LR
pkg_directory_picker_browse --> svc_directoryPicker
pkg_directory_picker_native --> svc_directoryPicker
pkg_e2b --> svc_e2b
pkg_file_reference --> svc_fileReferences
pkg_file_reference_local --> svc_fileReferences
pkg_fs --> svc_fs
pkg_fs_e2b --> svc_fs
pkg_fs_local --> svc_fs
@@ -433,6 +438,7 @@ flowchart LR
| `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | Owns local per-assistant-message feedback, lifecycle and target validation, per-item compare-and-set, and the Host unary Remote contract without entering Session history or telemetry. |
| `ctx.workspaceRegistry` | `core` | [`workspace`](../packages/workspace/workspace) | - | `apiproxy` | - | 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.fileReferences` | `seam` | [`file-reference`](../packages/context/file-reference) | [`file-reference-local`](../packages/context/file-reference-local) | - | - | The interface returns path-only completion candidates within the addressed Agent cwd through its unary Remote contract; 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. |
+6
View File
@@ -68,6 +68,9 @@ flowchart LR
svc_sessionQuery["ctx.sessionQuery<br/>Session reads, traces, filters, and search"]
pkg_session_reference["session-reference"]
pkg_tool_session_query["tool-session-query"]
pkg_file_reference["file-reference"]
svc_fileReferences["ctx.fileReferences<br/>File reference discovery"]
pkg_file_reference_local["file-reference-local"]
svc_sessionReferenceResolver["ctx.sessionReferenceResolver<br/>Cross-session snapshot preparation"]
pkg_session_title["session-title"]
svc_sessionTitle["ctx.sessionTitle<br/>Log-backed session titles"]
@@ -224,6 +227,8 @@ flowchart LR
pkg_directory_picker_browse --> svc_directoryPicker
pkg_directory_picker_native --> svc_directoryPicker
pkg_e2b --> svc_e2b
pkg_file_reference --> svc_fileReferences
pkg_file_reference_local --> svc_fileReferences
pkg_fs --> svc_fs
pkg_fs_e2b --> svc_fs
pkg_fs_local --> svc_fs
@@ -435,6 +440,7 @@ flowchart LR
| `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | 拥有本地逐 assistant 消息反馈、生命周期与目标校验、逐条目 compare-and-set 及 Host 一元 Remote 契约,且不进入 Session 历史或遥测。 |
| `ctx.workspaceRegistry` | `core` | [`workspace`](../packages/workspace/workspace) | - | `apiproxy` | - | 通过领域设施拥有带 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.fileReferences` | `seam` | [`file-reference`](../packages/context/file-reference) | [`file-reference-local`](../packages/context/file-reference-local) | - | - | 该接口通过其一元 Remote 契约返回指定 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。 |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
config-catalog.md: 86aa96bd60ec8a5cbe95ab730d4209c2c890217c
config-catalog.zh.md: 9d9cefd13b9f4d57811ef87e49642f5491616707
config-catalog.md: 912ee5687d4e75768e6f51636c7ca2bfcb7ec490
config-catalog.zh.md: d22e9dcd1aa427838081afc5671c62a9722e80aa
+25 -1
View File
@@ -587,6 +587,26 @@ export interface Config {
Source: [`packages/e2b/e2b/src/index.ts:43`](../packages/e2b/e2b/src/index.ts)
<a id="deepseek-aidsh-file-reference-local"></a>
## `@deepseek-ai/dsh-file-reference-local`
Requires: `agents`
```ts config-catalog
/** Local file-reference discovery configuration. */
export interface Config {
/** Maximum ranked candidates returned for one query. */
maxResults?: number
/** Maximum indexed files and directories per agent workspace. */
maxEntries?: number
/** Directory basenames never traversed or offered. */
excludedDirectories?: string[]
}
```
Source: [`packages/context/file-reference-local/src/index.ts:35`](../packages/context/file-reference-local/src/index.ts)
<a id="deepseek-aidsh-fs-local"></a>
## `@deepseek-ai/dsh-fs-local`
@@ -3005,6 +3025,8 @@ Requires: `webServer`
```ts config-catalog
/** Plugin config: composed deployment settings plus per-invocation command-line values. */
export interface Config {
/** Permit default-browser handoff after the Loader tree settles; an SSH launch suppresses it. */
openBrowser: boolean
/** Print the URL line on activation; a non-interactive layer can turn it off. */
printUrl: boolean
/**
@@ -3019,7 +3041,7 @@ export interface Config {
}
```
Source: [`packages/bundle/web-app/src/index.ts:38`](../packages/bundle/web-app/src/index.ts)
Source: [`packages/bundle/web-app/src/index.ts:42`](../packages/bundle/web-app/src/index.ts)
<a id="deepseek-aidsh-web-fetch-http"></a>
@@ -3179,6 +3201,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-client-ui-model-selection` ([`packages/client/ui-model-selection/src/index.ts`](../packages/client/ui-model-selection/src/index.ts))
- `@deepseek-ai/dsh-client-ui-permission-presets` ([`packages/client/ui-permission-presets/src/index.ts`](../packages/client/ui-permission-presets/src/index.ts))
- `@deepseek-ai/dsh-client-ui-plan` ([`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts))
- `@deepseek-ai/dsh-client-ui-reference` ([`packages/client/ui-reference/src/index.ts`](../packages/client/ui-reference/src/index.ts))
- `@deepseek-ai/dsh-client-ui-renderer` ([`packages/client/ui-renderer/src/index.ts`](../packages/client/ui-renderer/src/index.ts))
- `@deepseek-ai/dsh-client-ui-settings` ([`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts))
- `@deepseek-ai/dsh-client-ui-settings-general` ([`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts))
@@ -3233,6 +3256,7 @@ Abstract service classes — a deployment loads a concrete implementation packag
- `@deepseek-ai/dsh-code-runtime` — abstract `CodeRuntime` ([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts))
- `@deepseek-ai/dsh-compaction` — abstract `CompactionEngine` ([`packages/compaction/compaction/src/index.ts`](../packages/compaction/compaction/src/index.ts))
- `@deepseek-ai/dsh-credentials` — abstract `CredentialProvider` ([`packages/credentials/credentials/src/index.ts`](../packages/credentials/credentials/src/index.ts))
- `@deepseek-ai/dsh-file-reference` — abstract `FileReferenceService` ([`packages/context/file-reference/src/index.ts`](../packages/context/file-reference/src/index.ts))
- `@deepseek-ai/dsh-fs` — abstract `FileSystem` ([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts))
- `@deepseek-ai/dsh-host-directory-picker` — abstract `DirectoryPicker` ([`packages/host/directory-picker/src/index.ts`](../packages/host/directory-picker/src/index.ts))
- `@deepseek-ai/dsh-jobs` — abstract `JobRegistry` ([`packages/jobs/jobs/src/index.ts`](../packages/jobs/jobs/src/index.ts))
+25 -1
View File
@@ -589,6 +589,26 @@ export interface Config {
来源:[`packages/e2b/e2b/src/index.ts:43`](../packages/e2b/e2b/src/index.ts)
<a id="deepseek-aidsh-file-reference-local"></a>
## `@deepseek-ai/dsh-file-reference-local`
需要:`agents`
```ts config-catalog
/** Local file-reference discovery configuration. */
export interface Config {
/** Maximum ranked candidates returned for one query. */
maxResults?: number
/** Maximum indexed files and directories per agent workspace. */
maxEntries?: number
/** Directory basenames never traversed or offered. */
excludedDirectories?: string[]
}
```
来源:[`packages/context/file-reference-local/src/index.ts:35`](../packages/context/file-reference-local/src/index.ts)
<a id="deepseek-aidsh-fs-local"></a>
## `@deepseek-ai/dsh-fs-local`
@@ -3009,6 +3029,8 @@ export interface WebRuntimeConfig {
```ts config-catalog
/** Plugin config: composed deployment settings plus per-invocation command-line values. */
export interface Config {
/** Permit default-browser handoff after the Loader tree settles; an SSH launch suppresses it. */
openBrowser: boolean
/** Print the URL line on activation; a non-interactive layer can turn it off. */
printUrl: boolean
/**
@@ -3023,7 +3045,7 @@ export interface Config {
}
```
来源:[`packages/bundle/web-app/src/index.ts:38`](../packages/bundle/web-app/src/index.ts)
来源:[`packages/bundle/web-app/src/index.ts:42`](../packages/bundle/web-app/src/index.ts)
<a id="deepseek-aidsh-web-fetch-http"></a>
@@ -3183,6 +3205,7 @@ export interface Config {
- `@deepseek-ai/dsh-client-ui-model-selection`[`packages/client/ui-model-selection/src/index.ts`](../packages/client/ui-model-selection/src/index.ts)
- `@deepseek-ai/dsh-client-ui-permission-presets`[`packages/client/ui-permission-presets/src/index.ts`](../packages/client/ui-permission-presets/src/index.ts)
- `@deepseek-ai/dsh-client-ui-plan`[`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts)
- `@deepseek-ai/dsh-client-ui-reference`[`packages/client/ui-reference/src/index.ts`](../packages/client/ui-reference/src/index.ts)
- `@deepseek-ai/dsh-client-ui-renderer`[`packages/client/ui-renderer/src/index.ts`](../packages/client/ui-renderer/src/index.ts)
- `@deepseek-ai/dsh-client-ui-settings`[`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts)
- `@deepseek-ai/dsh-client-ui-settings-general`[`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts)
@@ -3237,6 +3260,7 @@ export interface Config {
- `@deepseek-ai/dsh-code-runtime` — 抽象 `CodeRuntime`[`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts)
- `@deepseek-ai/dsh-compaction` — 抽象 `CompactionEngine`[`packages/compaction/compaction/src/index.ts`](../packages/compaction/compaction/src/index.ts)
- `@deepseek-ai/dsh-credentials` — 抽象 `Credentials`[`packages/credentials/credentials/src/index.ts`](../packages/credentials/credentials/src/index.ts)
- `@deepseek-ai/dsh-file-reference` — 抽象 `FileReferenceService`[`packages/context/file-reference/src/index.ts`](../packages/context/file-reference/src/index.ts)
- `@deepseek-ai/dsh-fs` — 抽象 `FileSystem`[`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts)
- `@deepseek-ai/dsh-host-directory-picker` — 抽象 `DirectoryPicker`[`packages/host/directory-picker/src/index.ts`](../packages/host/directory-picker/src/index.ts)
- `@deepseek-ai/dsh-jobs` — 抽象 `JobRegistry`[`packages/jobs/jobs/src/index.ts`](../packages/jobs/jobs/src/index.ts)
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
event-producer-consumer.md: c906ee6329fac66e7391c266213dd150dd5b8e09
event-producer-consumer.zh.md: 77bf401b7215bd263c0d84f04e0eabe6b28b7915
event-producer-consumer.md: 2e4bec93f5b9f68ab885d50bab27e9a9a6036028
event-producer-consumer.zh.md: e3f5602287d7b688a2191937871e1639dc271e65
+4 -4
View File
@@ -9,13 +9,13 @@ 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:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `apiproxy` |
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../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), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../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) |
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../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/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../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:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`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), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`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-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-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`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), [`team`](../packages/experimental/team) |
@@ -40,7 +40,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../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`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../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-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`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-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../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-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`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-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
+4 -4
View File
@@ -11,13 +11,13 @@
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `apiproxy` |
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../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), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../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) |
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../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/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../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:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`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), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`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-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-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`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), [`team`](../packages/experimental/team) |
@@ -42,7 +42,7 @@
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../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`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../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-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`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-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../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-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`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-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/module-graph.md
module-graph.md: 95304807038c2e0a3e57eab09697f4cfe61bc0b8
module-graph.zh.md: 68a04eea607eb1261332aa674501001c8e93d328
module-graph.md: 21fe828b1a7ec302787ef48f47576d9af0e47da9
module-graph.zh.md: d52319400481ed974075ed5e04cadc8dad2d25c6
+29 -3
View File
@@ -139,6 +139,7 @@ flowchart TD
pkg_client_ui_permission_presets["client-ui-permission-presets"]
pkg_client_ui_plan["client-ui-plan"]
pkg_client_ui_primitives["client-ui-primitives"]
pkg_client_ui_reference["client-ui-reference"]
pkg_client_ui_renderer["client-ui-renderer"]
pkg_client_ui_settings["client-ui-settings"]
pkg_client_ui_settings_general["client-ui-settings-general"]
@@ -170,6 +171,8 @@ flowchart TD
end
subgraph group_context["packages/context"]
pkg_agent_instructions["agent-instructions"]
pkg_file_reference["file-reference"]
pkg_file_reference_local["file-reference-local"]
pkg_session_reference["session-reference"]
pkg_time_context["time-context"]
pkg_tmux_context["tmux-context"]
@@ -512,6 +515,9 @@ flowchart TD
pkg_web_search_deepseek --> pkg_web
pkg_spill_local --> pkg_invariants
pkg_spill_local --> pkg_spill
pkg_file_reference --> pkg_agent
pkg_file_reference --> pkg_invariants
pkg_file_reference --> pkg_typert_protocol
pkg_time_context --> pkg_agent
pkg_time_context --> pkg_invariants
pkg_time_context --> pkg_session
@@ -836,6 +842,11 @@ flowchart TD
pkg_agent_instructions --> pkg_llm
pkg_agent_instructions --> pkg_session
pkg_agent_instructions --> pkg_tools
pkg_file_reference_local --> pkg_agent
pkg_file_reference_local --> pkg_file_reference
pkg_file_reference_local --> pkg_invariants
pkg_file_reference_local --> pkg_system_prompt
pkg_file_reference_local --> pkg_tools
pkg_session_reference --> pkg_agent
pkg_session_reference --> pkg_compaction
pkg_session_reference --> pkg_invariants
@@ -843,6 +854,7 @@ flowchart TD
pkg_session_reference --> pkg_output_retention
pkg_session_reference --> pkg_session
pkg_session_reference --> pkg_session_query
pkg_session_reference --> pkg_typert_protocol
pkg_cordis_host_runner --> pkg_agent
pkg_cordis_host_runner --> pkg_brand
pkg_cordis_host_runner --> pkg_invariants
@@ -1163,6 +1175,7 @@ flowchart TD
pkg_api_remotes --> pkg_commands
pkg_api_remotes --> pkg_cordis_host_runner
pkg_api_remotes --> pkg_credentials
pkg_api_remotes --> pkg_file_reference
pkg_api_remotes --> pkg_goal
pkg_api_remotes --> pkg_host_plugin_inventory
pkg_api_remotes --> pkg_invariants
@@ -1170,6 +1183,7 @@ flowchart TD
pkg_api_remotes --> pkg_message_feedback
pkg_api_remotes --> pkg_session
pkg_api_remotes --> pkg_session_persistence
pkg_api_remotes --> pkg_session_reference
pkg_api_remotes --> pkg_settings
pkg_api_remotes --> pkg_typert_registry
pkg_client_runtime --> pkg_agent
@@ -1207,6 +1221,7 @@ flowchart TD
pkg_client_test_runtime --> pkg_invariants
pkg_client_ui_input_trigger --> pkg_client_locale
pkg_client_ui_input_trigger --> pkg_client_runtime
pkg_client_ui_input_trigger --> pkg_file_reference
pkg_client_ui_input_trigger --> pkg_invariants
pkg_client_ui_settings_models --> pkg_api_remotes
pkg_client_ui_settings_models --> pkg_client_connection
@@ -1236,6 +1251,14 @@ flowchart TD
pkg_client_ui_layout --> pkg_client_runtime
pkg_client_ui_layout --> pkg_client_ui_theme
pkg_client_ui_layout --> pkg_invariants
pkg_client_ui_reference --> pkg_api_remotes
pkg_client_ui_reference --> pkg_client_locale
pkg_client_ui_reference --> pkg_client_runtime
pkg_client_ui_reference --> pkg_client_ui_input_trigger
pkg_client_ui_reference --> pkg_file_reference
pkg_client_ui_reference --> pkg_invariants
pkg_client_ui_reference --> pkg_session_reference
pkg_client_ui_reference --> pkg_typert_protocol
pkg_cordis_client_runner --> pkg_api_remotes
pkg_cordis_client_runner --> pkg_client_connection
pkg_cordis_client_runner --> pkg_client_modules
@@ -1495,6 +1518,7 @@ flowchart TD
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) |
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
@@ -1555,7 +1579,8 @@ flowchart TD
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
| [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) |
| [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | `extensions` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
| [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`tools`](../packages/core/tools) |
| [`tool-call-timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
@@ -1605,18 +1630,19 @@ flowchart TD
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
| [`client-runtime`](../packages/client/runtime) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-title`](../packages/session/session-title), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry) |
| [`client-ui-renderer`](../packages/client/ui-renderer) | `client` | [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`client-runtime`](../packages/client/runtime), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-slots`](../packages/client/ui-slots), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol) |
| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-settings`](../packages/client/ui-settings), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools) |
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-layout`](../packages/client/ui-layout), [`invariants`](../packages/runtime-diagnostics/invariants) |
+29 -3
View File
@@ -141,6 +141,7 @@ flowchart TD
pkg_client_ui_permission_presets["client-ui-permission-presets"]
pkg_client_ui_plan["client-ui-plan"]
pkg_client_ui_primitives["client-ui-primitives"]
pkg_client_ui_reference["client-ui-reference"]
pkg_client_ui_renderer["client-ui-renderer"]
pkg_client_ui_settings["client-ui-settings"]
pkg_client_ui_settings_general["client-ui-settings-general"]
@@ -172,6 +173,8 @@ flowchart TD
end
subgraph group_context["packages/context"]
pkg_agent_instructions["agent-instructions"]
pkg_file_reference["file-reference"]
pkg_file_reference_local["file-reference-local"]
pkg_session_reference["session-reference"]
pkg_time_context["time-context"]
pkg_tmux_context["tmux-context"]
@@ -514,6 +517,9 @@ flowchart TD
pkg_web_search_deepseek --> pkg_web
pkg_spill_local --> pkg_invariants
pkg_spill_local --> pkg_spill
pkg_file_reference --> pkg_agent
pkg_file_reference --> pkg_invariants
pkg_file_reference --> pkg_typert_protocol
pkg_time_context --> pkg_agent
pkg_time_context --> pkg_invariants
pkg_time_context --> pkg_session
@@ -838,6 +844,11 @@ flowchart TD
pkg_agent_instructions --> pkg_llm
pkg_agent_instructions --> pkg_session
pkg_agent_instructions --> pkg_tools
pkg_file_reference_local --> pkg_agent
pkg_file_reference_local --> pkg_file_reference
pkg_file_reference_local --> pkg_invariants
pkg_file_reference_local --> pkg_system_prompt
pkg_file_reference_local --> pkg_tools
pkg_session_reference --> pkg_agent
pkg_session_reference --> pkg_compaction
pkg_session_reference --> pkg_invariants
@@ -845,6 +856,7 @@ flowchart TD
pkg_session_reference --> pkg_output_retention
pkg_session_reference --> pkg_session
pkg_session_reference --> pkg_session_query
pkg_session_reference --> pkg_typert_protocol
pkg_cordis_host_runner --> pkg_agent
pkg_cordis_host_runner --> pkg_brand
pkg_cordis_host_runner --> pkg_invariants
@@ -1165,6 +1177,7 @@ flowchart TD
pkg_api_remotes --> pkg_commands
pkg_api_remotes --> pkg_cordis_host_runner
pkg_api_remotes --> pkg_credentials
pkg_api_remotes --> pkg_file_reference
pkg_api_remotes --> pkg_goal
pkg_api_remotes --> pkg_host_plugin_inventory
pkg_api_remotes --> pkg_invariants
@@ -1172,6 +1185,7 @@ flowchart TD
pkg_api_remotes --> pkg_message_feedback
pkg_api_remotes --> pkg_session
pkg_api_remotes --> pkg_session_persistence
pkg_api_remotes --> pkg_session_reference
pkg_api_remotes --> pkg_settings
pkg_api_remotes --> pkg_typert_registry
pkg_client_runtime --> pkg_agent
@@ -1209,6 +1223,7 @@ flowchart TD
pkg_client_test_runtime --> pkg_invariants
pkg_client_ui_input_trigger --> pkg_client_locale
pkg_client_ui_input_trigger --> pkg_client_runtime
pkg_client_ui_input_trigger --> pkg_file_reference
pkg_client_ui_input_trigger --> pkg_invariants
pkg_client_ui_settings_models --> pkg_api_remotes
pkg_client_ui_settings_models --> pkg_client_connection
@@ -1238,6 +1253,14 @@ flowchart TD
pkg_client_ui_layout --> pkg_client_runtime
pkg_client_ui_layout --> pkg_client_ui_theme
pkg_client_ui_layout --> pkg_invariants
pkg_client_ui_reference --> pkg_api_remotes
pkg_client_ui_reference --> pkg_client_locale
pkg_client_ui_reference --> pkg_client_runtime
pkg_client_ui_reference --> pkg_client_ui_input_trigger
pkg_client_ui_reference --> pkg_file_reference
pkg_client_ui_reference --> pkg_invariants
pkg_client_ui_reference --> pkg_session_reference
pkg_client_ui_reference --> pkg_typert_protocol
pkg_cordis_client_runner --> pkg_api_remotes
pkg_cordis_client_runner --> pkg_client_connection
pkg_cordis_client_runner --> pkg_client_modules
@@ -1497,6 +1520,7 @@ flowchart TD
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) |
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
@@ -1557,7 +1581,8 @@ flowchart TD
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
| [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) |
| [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | `extensions` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
| [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`tools`](../packages/core/tools) |
| [`tool-call-timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
@@ -1607,18 +1632,19 @@ flowchart TD
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
| [`client-runtime`](../packages/client/runtime) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-title`](../packages/session/session-title), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry) |
| [`client-ui-renderer`](../packages/client/ui-renderer) | `client` | [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`client-runtime`](../packages/client/runtime), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-slots`](../packages/client/ui-slots), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol) |
| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-settings`](../packages/client/ui-settings), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools) |
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-layout`](../packages/client/ui-layout), [`invariants`](../packages/runtime-diagnostics/invariants) |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/persistence-catalog.md
persistence-catalog.md: b680bccf22f7840663e5268eb3feeb6b16f7fd42
persistence-catalog.zh.md: 4b50582fa55dbdc672d8c45debe108b97a55f0e2
persistence-catalog.md: cbe9cec195ad899ba27dd08743d64090ae7277f2
persistence-catalog.zh.md: 8057a0910e505b95872c341283d1acdbf033faaf
+14 -10
View File
@@ -90,7 +90,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
}[T]
```
Sources: [`packages/core/session/src/types.ts:336`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:343`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:372`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:404`](../packages/core/session/src/types.ts)
Sources: [`packages/core/session/src/types.ts:340`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:347`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:376`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:408`](../packages/core/session/src/types.ts)
## Events
@@ -226,14 +226,18 @@ Source: [`packages/core/session/src/types.ts:266`](../packages/core/session/src/
* Assembled assistant message for one step (derived history uses this).
* Carries the step's `usage` when the adapter reported token accounting, so
* the model output and its accounting travel together (there is no separate
* usage record). `usage` is absent when the adapter reported none.
* usage record). `usage` is absent when the adapter reported none. A turn
* cancelled mid-stream finalizes its delivered text/reasoning prefix as this
* event with `interrupted: true`; undispatched tool calls are absent. The
* marker distinguishes that prefix without re-deriving interruption from turn
* boundaries. An aborted turn with no such event streamed no visible content.
*/
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage }
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true }
```
Types: [TokenUsage](subsystems/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:273`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:277`](../packages/core/session/src/types.ts)
### `command/*`
@@ -543,7 +547,7 @@ Source: [`packages/plan/plan-mode/src/index.ts:54`](../packages/plan/plan-mode/s
'request/context': RequestContext
```
Source: [`packages/core/session/src/types.ts:309`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:313`](../packages/core/session/src/types.ts)
<a id="requestheader--log-only"></a>
@@ -557,7 +561,7 @@ Source: [`packages/core/session/src/types.ts:309`](../packages/core/session/src/
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
```
Source: [`packages/core/session/src/types.ts:304`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:308`](../packages/core/session/src/types.ts)
### `sandbox/*`
@@ -632,7 +636,7 @@ Source: [`packages/schedule/schedule/src/types.ts:219`](../packages/schedule/sch
'session/end-seed': Record<string, never>
```
Source: [`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:336`](../packages/core/session/src/types.ts)
<a id="sessiontitle--log-only"></a>
@@ -778,7 +782,7 @@ Source: [`packages/experimental/team/src/types.ts:208`](../packages/experimental
Types: [TodoItem](subsystems/session.md)
Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:303`](../packages/core/session/src/types.ts)
### `tool/*`
@@ -797,7 +801,7 @@ Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/
Types: [CallId](subsystems/core.md)
Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:283`](../packages/core/session/src/types.ts)
<a id="toolcode-dispatch--log-only"></a>
@@ -872,7 +876,7 @@ Source: [`packages/core/tools/src/types.ts:40`](../packages/core/tools/src/types
}
```
Source: [`packages/core/session/src/types.ts:291`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:295`](../packages/core/session/src/types.ts)
### `tool-workflow/*`
+14 -10
View File
@@ -92,7 +92,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
}[T]
```
来源:[`packages/core/session/src/types.ts:336`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:343`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:372`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:404`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:340`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:347`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:376`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:408`](../packages/core/session/src/types.ts)
## 事件
@@ -228,14 +228,18 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
* Assembled assistant message for one step (derived history uses this).
* Carries the step's `usage` when the adapter reported token accounting, so
* the model output and its accounting travel together (there is no separate
* usage record). `usage` is absent when the adapter reported none.
* usage record). `usage` is absent when the adapter reported none. A turn
* cancelled mid-stream finalizes its delivered text/reasoning prefix as this
* event with `interrupted: true`; undispatched tool calls are absent. The
* marker distinguishes that prefix without re-deriving interruption from turn
* boundaries. An aborted turn with no such event streamed no visible content.
*/
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage }
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true }
```
类型:[TokenUsage](subsystems/llm-streaming.md)
来源:[`packages/core/session/src/types.ts:273`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:277`](../packages/core/session/src/types.ts)
### `command/*`
@@ -545,7 +549,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'request/context': RequestContext
```
来源:[`packages/core/session/src/types.ts:309`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:313`](../packages/core/session/src/types.ts)
<a id="requestheader--log-only"></a>
@@ -559,7 +563,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
```
来源:[`packages/core/session/src/types.ts:304`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:308`](../packages/core/session/src/types.ts)
### `sandbox/*`
@@ -634,7 +638,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'session/end-seed': Record<string, never>
```
来源:[`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:336`](../packages/core/session/src/types.ts)
<a id="sessiontitle--log-only"></a>
@@ -780,7 +784,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
类型:[TodoItem](subsystems/session.md)
来源:[`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:303`](../packages/core/session/src/types.ts)
### `tool/*`
@@ -799,7 +803,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
类型:[CallId](subsystems/core.md)
来源:[`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:283`](../packages/core/session/src/types.ts)
<a id="toolcode-dispatch--log-only"></a>
@@ -874,7 +878,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
}
```
来源:[`packages/core/session/src/types.ts:291`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:295`](../packages/core/session/src/types.ts)
### `tool-workflow/*`
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/llm-streaming.md
llm-streaming.md: c1b2ab5f1e0926864f25409c021691d078df9e0f
llm-streaming.zh.md: 7bf04bf7a4a7de63d20eb67461842ff49f3b189d
llm-streaming.md: 8731a535267bddd2bf99a06a2beee92d3083232d
llm-streaming.zh.md: b9e71bba7428dafbbc7a366a97c2d7ddc9a87d46
+10 -1
View File
@@ -298,7 +298,8 @@ One keep/drop decision covers content and metadata together: a `max-tokens` fini
* {@link ContentBlock}s and a final assistant {@link Message}.
*
* The agent loop feeds it while logging raw chunks for replay fidelity, then
* reads `blocks()` / `message()` / `usage` / `finish` once the stream ends.
* reads `blocks()` / `message()` / `usage` / `finish` once the stream ends,
* or `interruptedBlocks()` when cancellation cut the stream short.
*
* Tolerant of delta-only protocols (no block-start/end); deltas arriving for
* an index already closed by `block-end` are ignored (malformed stream) so a
@@ -317,6 +318,14 @@ declare class BlockAssembler {
* its accumulated deltas (an unknown block type never closed by `block-end` throws).
*/
blocks(): ContentBlock[];
/**
* Assemble the prefix an interrupted stream can safely finalize: closed and
* open text/reasoning blocks with non-whitespace content, in stream order.
* Tool calls are omitted because interruption precedes dispatch; retaining
* one would require a fabricated result. Open unknown blocks are also omitted.
* @returns the kept blocks; empty when nothing streamed before the interruption.
*/
interruptedBlocks(): ContentBlock[];
/** Usage from the `usage` chunk; undefined until one arrives. */
get usage(): TokenUsage | undefined;
/** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */
+10 -1
View File
@@ -304,7 +304,8 @@ interface TokenUsage {
* {@link ContentBlock}s and a final assistant {@link Message}.
*
* The agent loop feeds it while logging raw chunks for replay fidelity, then
* reads `blocks()` / `message()` / `usage` / `finish` once the stream ends.
* reads `blocks()` / `message()` / `usage` / `finish` once the stream ends,
* or `interruptedBlocks()` when cancellation cut the stream short.
*
* Tolerant of delta-only protocols (no block-start/end); deltas arriving for
* an index already closed by `block-end` are ignored (malformed stream) so a
@@ -323,6 +324,14 @@ declare class BlockAssembler {
* its accumulated deltas (an unknown block type never closed by `block-end` throws).
*/
blocks(): ContentBlock[];
/**
* Assemble the prefix an interrupted stream can safely finalize: closed and
* open text/reasoning blocks with non-whitespace content, in stream order.
* Tool calls are omitted because interruption precedes dispatch; retaining
* one would require a fabricated result. Open unknown blocks are also omitted.
* @returns the kept blocks; empty when nothing streamed before the interruption.
*/
interruptedBlocks(): ContentBlock[];
/** Usage from the `usage` chunk; undefined until one arrives. */
get usage(): TokenUsage | undefined;
/** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/session-reference.md
session-reference.md: 4140eb2a27d79ee0975d97ae9e96a1b502d74187
session-reference.zh.md: 9dcfdbd07d4df1c54c51dc06d36af2f052969783
session-reference.md: e3ef2c00b7ddbeed9fa3f9d074df436d8c0cc7b5
session-reference.zh.md: 74063c1525a1f7f67663e9b11148e18902b22748
+71 -5
View File
@@ -2,9 +2,23 @@
English | [中文](session-reference.zh.md)
Structured cross-session reference requests and prepared message contexts. The [package contract](../../packages/context/session-reference) defines canonical URIs, current-surface projection, tag-safe JSON and byte retention, stable errors, and the untrusted model prompt. Host adapters use these types instead of passing their UI mention syntax into the agent core.
Host-backed file discovery plus structured cross-session reference requests and prepared message contexts. The [file-reference contract](../../packages/context/file-reference) owns path-only completion records and grammar; the [session-reference contract](../../packages/context/session-reference) defines canonical URIs, current-surface projection, tag-safe JSON and byte retention, stable errors, and the untrusted model prompt. Host adapters use these types instead of passing their UI mention syntax into the agent core.
Source: [`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
Sources: [`packages/context/file-reference/src/types.ts`](../../packages/context/file-reference/src/types.ts) · [`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
## File candidates
`FileReferenceCandidate` is the path-only discovery result. The addressed agent supplies the working-directory scope; providers decide ranking and namespace access without reading file contents.
```ts type-equiv
/** One path-only completion candidate inside the target session cwd. */
interface FileReferenceCandidate {
/** User-facing path accepted by normal prompts and filesystem tools. */
path: string
/** Directories keep completion open; files finish the mention. */
kind: 'file' | 'directory'
}
```
## Inputs and candidates
@@ -36,6 +50,16 @@ interface SessionReferenceCandidate {
}
```
The `sessionReferenceResolver/candidates` Remote method serves the same discovery to browser consumers and attaches each candidate's canonical prompt mention.
```ts type-equiv
/** One discovery candidate carrying its canonical prompt mention. */
interface SessionReferenceMentionCandidate extends SessionReferenceCandidate {
/** Canonical `@[label](dsh-session:…)` mention serialized into the prompt draft. */
mention: string
}
```
## Prepared messages
Preparation preserves readable current-message content and returns at most one aggregated context.
@@ -74,6 +98,37 @@ type SessionReferenceErrorCode =
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`) — this section is byte-identical in both language sides of the page. 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).
<a id="ctxfilereferences--filereferenceservice-abstract-seam"></a>
### `ctx.fileReferences``FileReferenceService` (abstract seam)
Host capability for cancellable file-reference discovery.
```ts cordis-catalog
/**
* List file and directory candidates for one agent's working directory.
* @param agent - target agent whose session cwd bounds discovery.
* @param query - path text following `@` or `@"`.
* @param signal - caller cancellation.
* @returns deterministic path-only candidates.
*/
abstract list( agent: Agent, query: string, signal: AbortSignal, ): Promise<FileReferenceCandidate[]>
/**
* Remote face of {@link list}; the decorator cannot mark the abstract
* member, so this concrete adapter carries the identical contract.
* @param agent - target agent whose session cwd bounds discovery.
* @param query - path text following `@` or `@"`.
* @param signal - caller cancellation.
* @returns deterministic path-only candidates.
*/
@Remote('list') remoteExportList( agent: Agent, query: string, signal: AbortSignal, ): Promise<FileReferenceCandidate[]>
```
Types: [Agent](core.md)
Source: [`packages/context/file-reference/src/index.ts:27`](../../packages/context/file-reference/src/index.ts)
<a id="ctxsessionreferenceresolver--sessionreferenceresolver"></a>
### `ctx.sessionReferenceResolver``SessionReferenceResolver`
@@ -92,11 +147,22 @@ Exact-read consumer that prepares immutable cross-session message context.
async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise<SessionReferenceCandidate[]>
/**
* Snapshot all references before enqueue and return one aggregated durable context.
* Remote face of {@link listCandidates}: the configured candidate limit
* applies, and every candidate carries the canonical mention a host inserts
* into the prompt draft.
* @param agent - target agent; self is excluded and its cwd drives ranking.
* @param query - optional case-insensitive session-id/cwd/title substring.
* @param signal - caller cancellation.
* @returns mention-carrying candidates in rank order.
*/
@Remote('candidates') async remoteExportCandidates( agent: Agent, query: string, signal: AbortSignal, ): Promise<SessionReferenceMentionCandidate[]>
/**
* Snapshot all references for one accepted direct message and return one aggregated durable context.
* @param agent - target agent; references to it are rejected.
* @param content - already host-normalized readable message content.
* @param references - structured source sessions in mention order.
* @param signal - optional cancellation boundary for host request teardown.
* @param signal - optional cancellation boundary for the active turn.
* @returns detached content and optional referenced-session context.
*/
async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise<PreparedReferencedMessage>
@@ -104,5 +170,5 @@ async prepare( agent: Agent, content: ContentBlock[], references: SessionReferen
Types: [Agent](core.md) · [ContentBlock](llm-streaming.md)
Source: [`packages/context/session-reference/src/index.ts:70`](../../packages/context/session-reference/src/index.ts)
Source: [`packages/context/session-reference/src/index.ts:75`](../../packages/context/session-reference/src/index.ts)
<!-- END GENERATED cordis-surface -->
+71 -5
View File
@@ -2,9 +2,23 @@
[English](session-reference.md) | 中文
结构化的跨会话引用请求与准备后的消息上下文。[约定](../../packages/context/session-reference) 定义规范 URI、当前表层投影、标签安全的 JSON 与字节保留、稳定错误和不可信的模型提示词。宿主适配器使用这些类型,而不会把各自 UI 的提及语法传入 agent(智能体)核心。
由 Host 支撑的文件发现,以及结构化的跨会话引用请求与准备后的消息上下文。[文件引用约定](../../packages/context/file-reference)负责仅含路径的补全记录与语法;[会话引用约定](../../packages/context/session-reference)定义规范 URI、当前表层投影、标签安全的 JSON 与字节保留、稳定错误和不可信的模型提示词。宿主适配器使用这些类型,而不会把各自 UI 的提及语法传入 agent(智能体)核心。
来源:[`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
来源:[`packages/context/file-reference/src/types.ts`](../../packages/context/file-reference/src/types.ts) · [`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
## 文件候选项
`FileReferenceCandidate` 是仅含路径的发现结果。被寻址的 agent 提供工作目录范围;提供方负责排序和命名空间访问,但不会读取文件内容。
```ts type-equiv
/** One path-only completion candidate inside the target session cwd. */
interface FileReferenceCandidate {
/** User-facing path accepted by normal prompts and filesystem tools. */
path: string
/** Directories keep completion open; files finish the mention. */
kind: 'file' | 'directory'
}
```
## 输入与候选项
@@ -36,6 +50,16 @@ interface SessionReferenceCandidate {
}
```
`sessionReferenceResolver/candidates` Remote 方法向浏览器消费方提供同一发现能力,并为每个候选附上规范提示词 mention。
```ts type-equiv
/** One discovery candidate carrying its canonical prompt mention. */
interface SessionReferenceMentionCandidate extends SessionReferenceCandidate {
/** Canonical `@[label](dsh-session:…)` mention serialized into the prompt draft. */
mention: string
}
```
## 准备后的消息
准备过程保留可读的当前消息内容,并最多返回一个聚合上下文。
@@ -74,6 +98,37 @@ type SessionReferenceErrorCode =
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`) — this section is byte-identical in both language sides of the page. 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).
<a id="ctxfilereferences--filereferenceservice-abstract-seam"></a>
### `ctx.fileReferences``FileReferenceService` (abstract seam)
Host capability for cancellable file-reference discovery.
```ts cordis-catalog
/**
* List file and directory candidates for one agent's working directory.
* @param agent - target agent whose session cwd bounds discovery.
* @param query - path text following `@` or `@"`.
* @param signal - caller cancellation.
* @returns deterministic path-only candidates.
*/
abstract list( agent: Agent, query: string, signal: AbortSignal, ): Promise<FileReferenceCandidate[]>
/**
* Remote face of {@link list}; the decorator cannot mark the abstract
* member, so this concrete adapter carries the identical contract.
* @param agent - target agent whose session cwd bounds discovery.
* @param query - path text following `@` or `@"`.
* @param signal - caller cancellation.
* @returns deterministic path-only candidates.
*/
@Remote('list') remoteExportList( agent: Agent, query: string, signal: AbortSignal, ): Promise<FileReferenceCandidate[]>
```
Types: [Agent](core.md)
Source: [`packages/context/file-reference/src/index.ts:27`](../../packages/context/file-reference/src/index.ts)
<a id="ctxsessionreferenceresolver--sessionreferenceresolver"></a>
### `ctx.sessionReferenceResolver``SessionReferenceResolver`
@@ -92,11 +147,22 @@ Exact-read consumer that prepares immutable cross-session message context.
async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise<SessionReferenceCandidate[]>
/**
* Snapshot all references before enqueue and return one aggregated durable context.
* Remote face of {@link listCandidates}: the configured candidate limit
* applies, and every candidate carries the canonical mention a host inserts
* into the prompt draft.
* @param agent - target agent; self is excluded and its cwd drives ranking.
* @param query - optional case-insensitive session-id/cwd/title substring.
* @param signal - caller cancellation.
* @returns mention-carrying candidates in rank order.
*/
@Remote('candidates') async remoteExportCandidates( agent: Agent, query: string, signal: AbortSignal, ): Promise<SessionReferenceMentionCandidate[]>
/**
* Snapshot all references for one accepted direct message and return one aggregated durable context.
* @param agent - target agent; references to it are rejected.
* @param content - already host-normalized readable message content.
* @param references - structured source sessions in mention order.
* @param signal - optional cancellation boundary for host request teardown.
* @param signal - optional cancellation boundary for the active turn.
* @returns detached content and optional referenced-session context.
*/
async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise<PreparedReferencedMessage>
@@ -104,5 +170,5 @@ async prepare( agent: Agent, content: ContentBlock[], references: SessionReferen
Types: [Agent](core.md) · [ContentBlock](llm-streaming.md)
Source: [`packages/context/session-reference/src/index.ts:70`](../../packages/context/session-reference/src/index.ts)
Source: [`packages/context/session-reference/src/index.ts:75`](../../packages/context/session-reference/src/index.ts)
<!-- END GENERATED cordis-surface -->
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/session.md
session.md: aea9d00b38e384e7a973ce168c3a75a62e70a8bb
session.zh.md: 8c56029af5144569f1ab6df73a8fe2278f9ef5b4
session.md: 3dc8762468efd2f1515ae10e7400a7d85941b461
session.zh.md: 3edbe86c3dea7dcfaa3c1fef6a5ca9e4a4b2c36a
+6 -2
View File
@@ -59,9 +59,13 @@ interface SessionEventMap {
* Assembled assistant message for one step (derived history uses this).
* Carries the step's `usage` when the adapter reported token accounting, so
* the model output and its accounting travel together (there is no separate
* usage record). `usage` is absent when the adapter reported none.
* usage record). `usage` is absent when the adapter reported none. A turn
* cancelled mid-stream finalizes its delivered text/reasoning prefix as this
* event with `interrupted: true`; undispatched tool calls are absent. The
* marker distinguishes that prefix without re-deriving interruption from turn
* boundaries. An aborted turn with no such event streamed no visible content.
*/
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage }
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true }
/**
* The model requested one tool invocation: `name` with the raw `arguments`
* JSON string exactly as the model produced it (unparsed). `callId` pairs the
+6 -2
View File
@@ -59,9 +59,13 @@ interface SessionEventMap {
* Assembled assistant message for one step (derived history uses this).
* Carries the step's `usage` when the adapter reported token accounting, so
* the model output and its accounting travel together (there is no separate
* usage record). `usage` is absent when the adapter reported none.
* usage record). `usage` is absent when the adapter reported none. A turn
* cancelled mid-stream finalizes its delivered text/reasoning prefix as this
* event with `interrupted: true`; undispatched tool calls are absent. The
* marker distinguishes that prefix without re-deriving interruption from turn
* boundaries. An aborted turn with no such event streamed no visible content.
*/
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage }
'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true }
/**
* The model requested one tool invocation: `name` with the raw `arguments`
* JSON string exactly as the model produced it (unparsed). `callId` pairs the
@@ -57,6 +57,7 @@
{"type":"user/message","seq":55,"time":0,"data":{"content":[{"type":"text","text":"<goal_round>\nObjective: \"Finish the ACP goal-round-driver snapshot proof\"\nRound: 2/2\n\nContinue working toward the objective in this same session. Treat the current workspace, tool results, and durable session state as authoritative; inspect them instead of assuming earlier narration is still current. Make concrete progress and verify the result. Before claiming completion, gather evidence that the whole objective is achieved, read the current goal, and mark it complete. If work remains, leave the goal active for the next round. Follow the configured goal-tool policy before reporting a blocker.\n</goal_round>"}],"source":{"kind":"goal","goalId":"goal-{{sessionId}}","revision":1,"round":2},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":56,"time":0,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":57,"time":0,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":0,"text":"partial"}}}
{"type":"step/end","seq":58,"time":0,"data":{"turn":3,"step":1}}
{"type":"turn/end","seq":59,"time":0,"data":{"turn":3,"reason":{"kind":"aborted","reason":{"kind":"user"}}}}
{"type":"goal/change","seq":60,"time":0,"data":{"kind":"goal/change","version":1,"operation":"pause","goal":{"id":"goal-{{sessionId}}","revision":2,"objective":"Finish the ACP goal-round-driver snapshot proof","phase":"paused","maxGoalRounds":2},"roundsStarted":2,"createdAt":0,"updatedAt":0}}
{"type":"assistant/message","seq":58,"time":0,"data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"partial"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"interrupted":true},"sourceEventSeqs":[56,57],"surfaceOp":"append"}
{"type":"step/end","seq":59,"time":0,"data":{"turn":3,"step":1}}
{"type":"turn/end","seq":60,"time":0,"data":{"turn":3,"reason":{"kind":"aborted","reason":{"kind":"user"}}}}
{"type":"goal/change","seq":61,"time":0,"data":{"kind":"goal/change","version":1,"operation":"pause","goal":{"id":"goal-{{sessionId}}","revision":2,"objective":"Finish the ACP goal-round-driver snapshot proof","phase":"paused","maxGoalRounds":2},"roundsStarted":2,"createdAt":0,"updatedAt":0}}
@@ -3,3 +3,4 @@
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"GOAL READY"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"GOAL ROUND ONE"}}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"partial"}}}}
@@ -10,5 +10,6 @@
{"type":"request/context","seq":8,"time":1785730444532,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"assistant/chunk","seq":9,"time":1785498791456,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":10,"time":1785730444541,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"partial"}}}
{"type":"step/end","seq":11,"time":1785730444547,"data":{"turn":1,"step":1}}
{"type":"turn/end","seq":12,"time":1785730444547,"data":{"turn":1,"reason":{"kind":"aborted","reason":{"kind":"user"}}}}
{"type":"assistant/message","seq":11,"time":1786334791338,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"partial"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"104e9294-f9b8-4248-b7df-0b7e2a069c0a"},"interrupted":true},"sourceEventSeqs":[9,10],"surfaceOp":"append"}
{"type":"step/end","seq":12,"time":1786334791338,"data":{"turn":1,"step":1}}
{"type":"turn/end","seq":13,"time":1786334791338,"data":{"turn":1,"reason":{"kind":"aborted","reason":{"kind":"user"}}}}
@@ -1,3 +1,4 @@
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"partial"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"cancelled"}}
+10
View File
@@ -502,6 +502,16 @@
"tests/**/*.ts"
]
},
"packages/context/file-reference": {
"ignoreDependencies": [
"zod"
]
},
"packages/context/session-reference": {
"ignoreDependencies": [
"zod"
]
},
"packages/session/session-checkpoint-policy": {
"entry": [
"tests/**/*.spec.ts",
+3 -1
View File
@@ -464,7 +464,9 @@ describe('ACP prompt lifecycle', () => {
await expect(harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'two' }] }))
.resolves.toEqual({ stopReason: 'end_turn' })
await vi.waitFor(() => { expect(messageText(harness!)).toBe('next') })
// 'partial' is the cancelled turn's finalized prefix update; 'next' proves
// the second prompt settled independently of the aborted turn's late end.
await vi.waitFor(() => { expect(messageText(harness!)).toBe('partialnext') })
})
it('a retry turn adopts the prompt instead of rejecting at the failed turn end', async () => {
+12 -8
View File
@@ -58,37 +58,41 @@
"@deepseek-ai/dsh-typert-protocol": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-presets": "workspace:^",
"@deepseek-ai/dsh-api-gateway": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-file-reference": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-agent-presets": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-message-feedback": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^"
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-presets": "workspace:^",
"@deepseek-ai/dsh-api-gateway": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-file-reference": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-agent-presets": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-message-feedback": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
+10 -1
View File
@@ -4,16 +4,20 @@ import type { Context } from '@deepseek-ai/cordis'
import commandsRemote from '@deepseek-ai/dsh-commands/remote'
import goalsRemote from '@deepseek-ai/dsh-goal/remote'
import dynamicRemote from '@deepseek-ai/dsh-cordis-host-runner/remote'
import fileReferencesRemote from '@deepseek-ai/dsh-file-reference/remote'
import pluginInventoryRemote from '@deepseek-ai/dsh-host-plugin-inventory/remote'
import messageFeedbackRemote from '@deepseek-ai/dsh-message-feedback/remote'
import sessionReferencesRemote from '@deepseek-ai/dsh-session-reference/remote'
import type { TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol'
export type { TypertClientRemote as ClientRemote } from '@deepseek-ai/dsh-typert-protocol'
export type { PluginInventorySnapshot } from '@deepseek-ai/dsh-host-plugin-inventory/types'
export type {} from '@deepseek-ai/dsh-commands/remote'
export type {} from '@deepseek-ai/dsh-file-reference/remote'
export type {} from '@deepseek-ai/dsh-goal/remote'
export type {} from '@deepseek-ai/dsh-host-plugin-inventory/remote'
export type {} from '@deepseek-ai/dsh-message-feedback/remote'
export type {} from '@deepseek-ai/dsh-session-reference/remote'
// The forwarded-event allowlist's selection seat: without it in the consumer's
// compilation face `TypertRemoteEvent` is `never` and every `$on` call fails.
export type { ApiRemoteForwardedEvent } from '../types.ts'
@@ -86,6 +90,10 @@ export type {
// reason: a Client contribution names what it sends without importing a Host
// package, and this assembly is where both planes legitimately meet.
export type { JsonValue } from '@deepseek-ai/dsh-session/types'
// Reference-discovery result vocabulary for the fileReferences and
// sessionReferenceResolver namespaces.
export type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types'
export type { SessionReferenceMentionCandidate } from '@deepseek-ai/dsh-session-reference/types'
declare module '@deepseek-ai/cordis' {
interface Context {
@@ -106,7 +114,8 @@ export async function apply(ctx: Context): Promise<() => Promise<void>> {
const disposers: Array<() => Promise<void>> = []
try {
for (const contribution of [
commandsRemote, goalsRemote, dynamicRemote, pluginInventoryRemote, messageFeedbackRemote,
commandsRemote, goalsRemote, dynamicRemote, fileReferencesRemote,
pluginInventoryRemote, messageFeedbackRemote, sessionReferencesRemote,
]) {
disposers.push(await ctx.remote.$mount(contribution))
}
@@ -24,6 +24,12 @@
"path": "../../credentials/credentials"
},
{
"path": "../../context/file-reference"
},
{
"path": "../../context/session-reference"
},
{
"path": "../../extensions/cordis-host-runner"
},
+2 -2
View File
@@ -100,11 +100,11 @@ const BOOTSTRAP_NAMES = new Set([
'PERL5OPT', 'PERL5LIB', 'PYTHONSTARTUP', 'PYTHONPATH', 'RUBYOPT', 'RUBYLIB',
'JAVA_TOOL_OPTIONS', '_JAVA_OPTIONS', 'JDK_JAVA_OPTIONS',
'PYTHONHOME',
// Version-control command hooks and config redirects.
// Version-control hooks, config redirects, and ambient command selectors.
'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_EXTERNAL_DIFF', 'GIT_PAGER', 'GIT_EDITOR',
'GIT_ASKPASS', 'SSH_ASKPASS',
'GIT_CONFIG_GLOBAL', 'GIT_CONFIG_SYSTEM', 'GIT_CONFIG_COUNT',
'EDITOR', 'VISUAL', 'PAGER',
'EDITOR', 'VISUAL', 'PAGER', 'BROWSER',
// Network reach and trust.
'DEEPSEEK_BASE_URL', 'DEEPSEEK_SEARCH_BASE_URL',
'SSL_CERT_FILE', 'SSL_CERT_DIR',
@@ -133,6 +133,7 @@ describe('loadLayeredEnv', () => {
['a skill root', 'DSH_AGENTS_HOME=/tmp/injected\n'],
['a network proxy', 'HTTPS_PROXY=http://attacker.example\n'],
['a lowercase network proxy', 'https_proxy=http://attacker.example\n'],
['a browser command', 'BROWSER=./script\n'],
])('refuses to launch when a .env sets %s, before applying anything', (_case, content) => {
const home = tmp()
const project = tmp()
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/bundle/web-app/README.md
README.md: 28fb5b3dcfc7fbb912493a6b97495e2ed5a3eece
README.zh.md: 92157f05497e53c48506666a3a53d638d98a7c9e
README.md: c8a6874bc01696fc7c9ca65faf772da81ac1e964
README.zh.md: f36115cd97ccfeabd9bbb1a408be89c4c1df8723
+4 -1
View File
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The dsh browser-surface bundle. [`cordis.patch.yml`](cordis.patch.yml) rides over [`dsh-base`](../base/README.md): it sets the coding persona, inserts the Web host rows (webserver, API gateway, workspace, projection cache, storage) and the browser plugin roster, the always-on client-plugin reload chain ([`dsh-client-hmr`](../../client/hmr/README.md), idle until a rebuild watcher rewrites client bundles), and mounts this package's `web-runtime` glue plugin (config `{printUrl, surfaceContext, trustedHosts}`). That plugin resolves the built frontend dist through `@deepseek-ai/dsh-web-frontend`'s exports, samples bind-dependent LAN trust once, provides it as `webRuntime` to the browser-trust fence and client roster, mounts the [`frontend-static`](../../host/frontend-static/README.md) fallback owner, registers the harness-source and web-surface prompt sections plus the bash-visible `DSH_WEB_URL` runtime variable when `surfaceContext` is true, and prints the `dsh web:` URL line when `printUrl` is true, after its Loader tree settles so a sibling failure cannot announce a dead app. This bundle also owns the app command line: the ordinary `web-startup` provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), parses `--host`, `--port`, repeatable `--trusted-host`, and the app's `--help`, then provides `webStartup`. It rejects `--host 0.0.0.0` before publishing that service because the CLI intentionally does not support all-interfaces binding yet. Flag-configured rows inject the service and read it directly from lazy config, so nothing binds a port before argument resolution and `dsh --profile web --help` starts no server. [`dsh-headless`](../headless/README.md) is a sibling surface over the same base and does not mount this bundle.
The dsh browser-surface bundle. [`cordis.patch.yml`](cordis.patch.yml) rides over [`dsh-base`](../base/README.md): it sets the coding persona, inserts the Web host rows (webserver, API gateway, workspace, projection cache, storage) and the browser plugin roster, the always-on client-plugin reload chain ([`dsh-client-hmr`](../../client/hmr/README.md), idle until a rebuild watcher rewrites client bundles), and mounts this package's `web-runtime` glue plugin (config `{openBrowser, printUrl, surfaceContext, trustedHosts}`). That plugin resolves the built frontend dist through `@deepseek-ai/dsh-web-frontend`'s exports, samples bind-dependent LAN trust once, provides it as `webRuntime` to the browser-trust fence and client roster, mounts the [`frontend-static`](../../host/frontend-static/README.md) fallback owner, and registers the harness-source and web-surface prompt sections plus the bash-visible `DSH_WEB_URL` runtime variable when `surfaceContext` is true. After its Loader tree settles, it prints the `dsh web:` URL line when `printUrl` is true and opens the canonical host URL in the default browser when `openBrowser` is true and the inherited `SSH_CONNECTION` and `SSH_TTY` are blank or absent. An SSH launch keeps the URL line but suppresses browser handoff because the SSH client or editor owns the local forwarded address. Immediately before a handoff, the runtime prints `dsh web: opening the default browser; pass --no-open to disable`. A short-lived Node helper runs the maintained platform opener with the canonical scrubbed child environment. On Windows it stays alive until the short-lived PowerShell launcher exits, because `open` reports spawn before that launcher has handed the URL to the shell; elsewhere the helper stops after the opener accepts spawn. A helper failure writes a diagnostic with its reason and the manual URL to stderr without stopping the server, and no path waits for the browser to exit. This bundle also owns the app command line: the ordinary `web-startup` provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), parses `--host`, `--port`, repeatable `--trusted-host`, `--no-open`, and the app's `--help`, then provides `webStartup`; browser opening defaults on for local launches, and `--no-open` turns it off for this invocation. It rejects `--host 0.0.0.0` before publishing that service because the CLI intentionally does not support all-interfaces binding yet. Flag-configured rows inject the service and read it directly from lazy config, so nothing binds a port before argument resolution and `dsh --profile web --help` starts no server. [`dsh-headless`](../headless/README.md) is a sibling surface over the same base and does not mount this bundle.
## Model retry defaults
@@ -28,3 +28,6 @@ The prompt section sits near the system prompt's head and is stable for the life
- **The frontend dist must be built**`require.resolve` of the dist fails loud at activation with a build hint; there is no source-serving fallback.
- **`lanAddresses` is a boot-time snapshot** — interface changes after boot are not re-advertised; the printed LAN URL always matches the configured trust fence.
- **Only handoff startup is observable** — observation ends when the platform opener accepts spawn, except that Windows waits for its short-lived PowerShell launcher to exit; a later browser exit is not reported, and the printed URL remains the manual fallback.
- **SSH forwarding owns the browser URL** — the printed canonical URL names the remote host's loopback endpoint; automatic handoff is suppressed, and the SSH client or editor must expose and open its local forwarded address.
- **Browser command overrides are launch-only** — a discovered `.env` may not set `BROWSER`; only an inherited value may reach an opener path that honors the variable, so a checkout cannot choose an executable for automatic handoff.
+4 -1
View File
@@ -2,7 +2,7 @@
[English](README.md) | 中文
dsh 浏览器表层组合包。[`cordis.patch.yml`](cordis.patch.yml) 叠加在 [`dsh-base`](../base/README.md) 之上:设置 coding persona,插入 Web 宿主行(webserver、API 网关、workspace、投影缓存、存储)、浏览器插件名录与始终挂载的客户端插件重载链([`dsh-client-hmr`](../../client/hmr/README.md),在重建 watcher 改写客户端 bundle 之前保持空闲),并挂载本包的 `web-runtime` 粘合插件(配置为 `{printUrl, surfaceContext, trustedHosts}`)。该插件通过 `@deepseek-ai/dsh-web-frontend` 的 exports 解析已构建的前端 dist,只采样一次依赖 bind 的 LAN 信任信息并将其作为 `webRuntime` 提供给浏览器信任栅栏和客户端名录,挂载 [`frontend-static`](../../host/frontend-static/README.md) 回退席位所有者,在 `surfaceContext` 为 true 时注册 Harness 源码与 Web 表层提示词段落,以及 bash 可见的 `DSH_WEB_URL` 运行时变量,并`printUrl` 为 true 时等自身的 Loader 配置树结算后再打印 `dsh web:` URL 行,避免兄弟行失败时公告一个已失效的应用。本组合包还持有应用命令行:普通 `web-startup` 提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`[`dsh-cmdline`](../../boot/cmdline/README.md)),解析 `--host``--port`、可重复的 `--trusted-host` 以及应用自己的 `--help`,再提供 `webStartup`。它会在发布该服务前拒绝 `--host 0.0.0.0`,因为 CLI 目前有意不支持绑定所有网络接口。由 flag 配置的行会注入该服务,并在惰性配置中直接读取它,因此参数解析完成前不会有任何东西绑定端口,`dsh --profile web --help` 也不会启动服务器。[`dsh-headless`](../headless/README.md) 是同一 base 之上的同级表层,不挂载本组合包。
dsh 浏览器表层组合包。[`cordis.patch.yml`](cordis.patch.yml) 叠加在 [`dsh-base`](../base/README.md) 之上:设置 coding persona,插入 Web 宿主行(webserver、API 网关、workspace、投影缓存、存储)、浏览器插件名录与始终挂载的客户端插件重载链([`dsh-client-hmr`](../../client/hmr/README.md),在重建 watcher 改写客户端 bundle 之前保持空闲),并挂载本包的 `web-runtime` 粘合插件(配置为 `{openBrowser, printUrl, surfaceContext, trustedHosts}`)。该插件通过 `@deepseek-ai/dsh-web-frontend` 的 exports 解析已构建的前端 dist,只采样一次依赖 bind 的 LAN 信任信息并将其作为 `webRuntime` 提供给浏览器信任栅栏和客户端名录,挂载 [`frontend-static`](../../host/frontend-static/README.md) 回退席位所有者,`surfaceContext` 为 true 时注册 Harness 源码与 Web 表层提示词段落,以及 bash 可见的 `DSH_WEB_URL` 运行时变量。自身 Loader 配置树结算后,它`printUrl` 为 true 时打印 `dsh web:` URL 行;`openBrowser` 为 true 且继承的 `SSH_CONNECTION``SSH_TTY` 均为空或不存在时,才会用默认浏览器打开规范宿主机 URL。SSH 启动仍保留 URL 行,但会跳过浏览器交接,因为本地转发地址由 SSH 客户端或编辑器持有。交接前,运行时会打印英文提示 `dsh web: opening the default browser; pass --no-open to disable`。短生命周期 Node helper 使用规范的脱敏子进程环境运行受维护的平台 opener。在 Windows 上,helper 会保持存活,直至短生命周期的 PowerShell launcher 退出,因为 `open` 会在 launcher 把 URL 交给 shell 之前、仅在 spawn 时返回;其他平台则在 opener 接受 spawn 后结束。helper 失败时会向 stderr 写入包含原因和手动访问 URL 的诊断,不会停止服务器,且任何路径都不会等待浏览器退出。本组合包还持有应用命令行:普通 `web-startup` 提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`[`dsh-cmdline`](../../boot/cmdline/README.md)),解析 `--host``--port`、可重复的 `--trusted-host``--no-open` 以及应用自己的 `--help`,再提供 `webStartup`;本机启动默认会打开浏览器,`--no-open` 则只对本次调用关闭该行为。它会在发布该服务前拒绝 `--host 0.0.0.0`,因为 CLI 目前有意不支持绑定所有网络接口。由 flag 配置的行会注入该服务,并在惰性配置中直接读取它,因此参数解析完成前不会有任何东西绑定端口,`dsh --profile web --help` 也不会启动服务器。[`dsh-headless`](../headless/README.md) 是同一 base 之上的同级表层,不挂载本组合包。
## 模型重试默认值
@@ -28,3 +28,6 @@ Web 使用共享的有界 normal 默认值,在首次请求后最多再重试
- **前端 dist 必须已构建**:对 dist 的 `require.resolve` 在激活时明确报错并给出构建提示;没有从源码直接服务的回退路径。
- **`lanAddresses` 是启动期快照**:启动后的网卡变化不会重新公告;打印的 LAN URL 始终与配置的信任栅栏一致。
- **只观测交接启动**:平台 opener 接受 spawn 后即结束观察,但 Windows 会等待其短生命周期 PowerShell launcher 退出;之后的浏览器退出不会上报,已打印 URL 仍是手动访问的回退路径。
- **SSH 转发持有浏览器 URL**:打印出的规范 URL 指向远端宿主机 loopback 端点;自动交接会被跳过,SSH 客户端或编辑器必须暴露并打开其本地转发地址。
- **浏览器命令覆盖只能来自启动环境**:被发现的 `.env` 不得设置 `BROWSER`;只有继承值可以抵达会读取该变量的 opener 路径,避免 checkout 为自动交接选择可执行文件。
+17 -6
View File
@@ -79,6 +79,12 @@
writeEveryEvents: 200
writeIntervalMs: 5000
- id: session-reference
name: '@deepseek-ai/dsh-session-reference'
- id: file-reference-local
name: '@deepseek-ai/dsh-file-reference-local'
# Whole-log turn/step counts for the chat stats strip (the sessionStats
# projection key); the projection registry itself is a base-layer row.
- id: session-stats
@@ -122,15 +128,17 @@
# Web glue owned by this bundle: resolves the built frontend dist (an
# assembly fact of dsh-web-app, never user config), mounts the
# frontend-static fallback owner, registers the web-surface prompt
# section and the bash runtime variable, and prints the URL line. The
# webStartup provider supplies invocation-only values; after the server
# binds, this row samples LAN trust once and provides `webRuntime`. A
# complete agent-preset persona suppresses the prompt section for that
# agent while retaining the host-owned shell variable.
# section and the bash runtime variable, prints the URL line, and opens the
# canonical local URL after the full tree settles. The webStartup provider
# supplies invocation-only values; after the server binds, this row samples
# LAN trust once and provides `webRuntime`. A complete agent-preset persona
# suppresses the prompt section for that agent while retaining the host-owned
# shell variable.
- id: web-runtime
name: '@deepseek-ai/dsh-web-app'
inject: [webStartup]
config:
openBrowser: !!js ctx.webStartup.openBrowser
printUrl: true
surfaceContext: true
trustedHosts: !!js ctx.webStartup.trustedHosts
@@ -226,7 +234,7 @@
name: '@deepseek-ai/dsh-client-ui-workspace'
# Input triggers: the '/' | '@' pipeline (ui-input-trigger), the command surface over
# it (ui-commands), and the two reference sources (ui-skill / ui-subagent).
# it (ui-commands), and the reference sources (ui-skill / ui-reference).
- id: ui-input-trigger
name: '@deepseek-ai/dsh-client-ui-input-trigger'
@@ -239,6 +247,9 @@
- id: ui-subagent
name: '@deepseek-ai/dsh-client-ui-subagent'
- id: ui-reference
name: '@deepseek-ai/dsh-client-ui-reference'
# Background jobs: the session-header list over the jobsBySession mirror.
- id: ui-jobs
name: '@deepseek-ai/dsh-client-ui-jobs'
+8 -1
View File
@@ -76,6 +76,7 @@
"@deepseek-ai/dsh-client-ui-sidebar": "workspace:^",
"@deepseek-ai/dsh-client-ui-skill": "workspace:^",
"@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
"@deepseek-ai/dsh-client-ui-reference": "workspace:^",
"@deepseek-ai/dsh-client-ui-subagent": "workspace:^",
"@deepseek-ai/dsh-client-ui-jobs": "workspace:^",
"@deepseek-ai/dsh-client-ui-theme": "workspace:^",
@@ -95,16 +96,22 @@
"@deepseek-ai/dsh-host-directory-picker-native": "workspace:^",
"@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
"@deepseek-ai/dsh-host-webserver": "workspace:^",
"@deepseek-ai/dsh-file-reference": "workspace:^",
"@deepseek-ai/dsh-file-reference-local": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-message-feedback": "workspace:^",
"@deepseek-ai/dsh-session-projection-cache": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-session-log-export": "workspace:^",
"@deepseek-ai/dsh-session-stats": "workspace:^",
"@deepseek-ai/dsh-storage": "workspace:^",
"@deepseek-ai/dsh-storage-domain": "workspace:^",
"@deepseek-ai/dsh-storage-json": "workspace:^",
"@deepseek-ai/dsh-subprocess": "workspace:^",
"@deepseek-ai/dsh-workspace": "workspace:^",
"@deepseek-ai/schemastery": "workspace:^",
"commander": "^15.0.0"
"commander": "^15.0.0",
"open": "^11.0.0"
},
"peerDependencies": {
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
+123 -17
View File
@@ -5,11 +5,13 @@
* the built frontend dist (workspace knowledge of this bundle, never user
* config), mounts the `frontend-static` fallback owner over it, registers the
* harness-source and web-surface prompt sections, the bash-visible web runtime
* variable, and the URL line. App command-line values arrive through the
* `webStartup` service expressions in the bundle patch.
* variable, the URL line, and the default-browser handoff. App command-line
* values arrive through the `webStartup` service expressions in the bundle
* patch.
* @module @deepseek-ai/dsh-web-app
*/
import { spawn, type ChildProcess } from 'node:child_process'
import { createRequire } from 'node:module'
import { networkInterfaces } from 'node:os'
import { fileURLToPath } from 'node:url'
@@ -17,6 +19,8 @@ import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { addHarnessSourceSection } from '@deepseek-ai/dsh-app-boot'
import * as FrontendStatic from '@deepseek-ai/dsh-host-frontend-static'
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess'
import type {} from '@deepseek-ai/cordis-plugin-loader'
import type {} from '@deepseek-ai/dsh-host-webserver'
import type {} from '@deepseek-ai/dsh-system-prompt'
@@ -36,6 +40,8 @@ export const inject = ['webServer']
/** Plugin config: composed deployment settings plus per-invocation command-line values. */
export interface Config {
/** Permit default-browser handoff after the Loader tree settles; an SSH launch suppresses it. */
openBrowser: boolean
/** Print the URL line on activation; a non-interactive layer can turn it off. */
printUrl: boolean
/**
@@ -50,6 +56,7 @@ export interface Config {
}
export const Config: z<Config> = z.object({
openBrowser: z.boolean().default(true),
printUrl: z.boolean().default(true),
surfaceContext: z.boolean().default(true),
trustedHosts: z.array(String).default([]),
@@ -72,6 +79,46 @@ const LOOPBACK_HOST = '127.0.0.1'
/** The webserver schema's all-interfaces bind literal. */
const ALL_INTERFACES_HOST = '0.0.0.0'
/** Whether this process was launched through SSH, including a forwarded-port session. */
function launchedThroughSsh(ctx: Context): boolean {
const environment = launchEnvironmentOf(ctx)
return ['SSH_CONNECTION', 'SSH_TTY'].some((name) => {
const value = environment.getFrom(name, ['process'])?.value
return value !== undefined && value !== ''
})
}
const BROWSER_OPENER_MODULE = import.meta.resolve('open')
const BROWSER_OPENER_PROGRAM = `
try {
const { default: open } = await import(${JSON.stringify(BROWSER_OPENER_MODULE)})
const launcher = await open(process.argv[1])
if (process.platform === 'win32') {
// open resolves at PowerShell spawn; keep it referenced until that launcher hands the URL to Windows.
const code = launcher.exitCode ?? await new Promise((resolve, reject) => {
function onError(error) {
launcher.off('close', onClose)
reject(error)
}
function onClose(code) {
launcher.off('error', onError)
resolve(code)
}
launcher.ref()
launcher.once('error', onError)
launcher.once('close', onClose)
})
if (code !== 0) throw new Error('browser operating-system launcher exited with code ' + String(code))
}
process.exitCode = 0
} catch (error) {
// The parent turns this exit into the manual-URL warning.
console.error(error)
process.exitCode = 1
}
`
/**
* Resolve one LAN-trust snapshot from the active server bind.
*
@@ -123,17 +170,64 @@ function resolveDistIndex(): string {
}
}
/** Test hook: hosts with no built frontend dist substitute the resolver; production never touches this. */
export const internals: { resolveDistIndex: () => string } = { resolveDistIndex }
/** Start the maintained platform opener without forwarding Harness credentials. */
function spawnBrowserLauncher(url: string): ChildProcess {
return spawn(process.execPath, [
'--input-type=module',
'--eval', BROWSER_OPENER_PROGRAM,
'--', url,
], {
env: scrubbedParentEnv(),
stdio: ['ignore', 'inherit', 'pipe'],
})
}
/** Hand one URL to the operating system's default browser. */
async function openBrowser(url: string): Promise<void> {
const launcher = spawnBrowserLauncher(url)
let launcherStderr = ''
launcher.stderr?.setEncoding('utf8')
launcher.stderr?.on('data', (chunk: string) => { launcherStderr += chunk })
await new Promise<void>((resolve, reject) => {
function onError(error: Error): void {
launcher.off('close', onClose)
reject(error)
}
function onClose(code: number | null): void {
launcher.off('error', onError)
if (code !== 0) {
const firstLine = launcherStderr.trim().split(/\r?\n/u)[0]
const reason = firstLine === undefined || firstLine === ''
? `browser launcher exited with code ${String(code)}`
: firstLine.replace(/^(?:[A-Za-z]*Error):\s*/u, '')
reject(new Error(reason))
return
}
if (launcherStderr !== '') process.stderr.write(launcherStderr)
resolve()
}
launcher.once('error', onError)
launcher.once('close', onClose)
})
}
/** Test hooks for the built dist and native browser handoff; production never mutates them. */
export const internals: {
resolveDistIndex: () => string
openBrowser: (url: string) => Promise<void>
} = { resolveDistIndex, openBrowser }
/**
* Mount the Web runtime: dist serving, surface prompt, the bash runtime
* variable, and the URL line.
* variable, the URL line, and the default-browser handoff.
* @param ctx - plugin context carrying the webServer service.
* @param config - validated {@link Config}.
*/
export function apply(ctx: Context, config: Config): void {
const runtime = resolveLanTrust(ctx.webServer.host, config.trustedHosts)
// The loopback URL belongs to this host. Under SSH, the operator reaches it
// through a local forwarding address that this process cannot derive.
const handoffBrowser = config.openBrowser && !launchedThroughSsh(ctx)
// Release dependent rows only after bind-dependent trust has been sampled once.
ctx.provide(WEB_RUNTIME_SERVICE, runtime)
ctx.plugin(FrontendStatic, { distIndex: internals.resolveDistIndex() })
@@ -156,28 +250,40 @@ export function apply(ctx: Context, config: Config): void {
})
})
}
if (config.printUrl) {
// The URL line is a readiness signal: supervisors (and the keyless CLI
// smoke) RPC as soon as they observe it, so it must not print while
// sibling rows (the /api route owner) are still mounting. Await Loader
// settlement first; a hand-built tree without a Loader prints at once.
const printUrl = (): void => {
if (config.printUrl || handoffBrowser) {
// The URL line and browser handoff are readiness signals: supervisors RPC
// as soon as they observe the line, while a browser requests the page as
// soon as it opens. Neither may run while sibling rows such as the /api
// route owner are still mounting. Await Loader settlement first; a
// hand-built tree without a Loader is already the complete tree.
const announceReady = (): void => {
const webUrl = localWebUrl(ctx)
// Reuse the exact LAN snapshot provided to the /api trust fence.
const lanCandidate = runtime.lanAddresses[0]
const port = ctx.webServer.port
console.log(`dsh web: ${localWebUrl(ctx)}${lanCandidate === undefined ? '' : ` (LAN: http://${lanCandidate}:${String(port)})`}`)
if (config.printUrl) {
console.log(`dsh web: ${webUrl}${lanCandidate === undefined ? '' : ` (LAN: http://${lanCandidate}:${String(port)})`}`)
}
if (handoffBrowser) {
console.log('dsh web: opening the default browser; pass --no-open to disable')
void internals.openBrowser(webUrl).catch((error: unknown) => {
const reason = error instanceof Error ? error.message : String(error)
console.error(`web-app: could not open the default browser because ${reason}; visit ${webUrl} manually`)
})
}
}
// This row's own activation can precede a sibling failure. The app owns
// readiness by waiting for its Loader tree, or prints at once in a
// readiness by waiting for its Loader tree, or announces at once in a
// hand-built context without Loader.
const settled = ctx.get('loader')?.await()
if (settled === undefined) printUrl()
if (settled === undefined) announceReady()
else {
void settled.then(() => {
// The tree can be disposed while the boot was in flight (early
// SIGTERM); a URL line for a dead server would only mislead, and
// reading the torn-down port would turn a clean shutdown into a crash.
if (ctx.get('webServer') !== undefined) printUrl()
// SIGTERM); a URL line or browser tab for a dead server would only
// mislead, and reading the torn-down port would turn a clean shutdown
// into a crash.
if (ctx.get('webServer') !== undefined) announceReady()
// Loader reports a failed boot; this row only stays quiet.
}, () => {})
}
+7 -1
View File
@@ -1,6 +1,6 @@
/**
* The web app's command-line provider: it parses the `dsh --profile web` flag
* family (`--host`, `--port`, `--trusted-host`) and its `--help`
* family (`--host`, `--port`, `--trusted-host`, `--no-open`) and its `--help`
* text, then provides the immutable values as {@link WEB_STARTUP_SERVICE}.
* Ordinary rows inject that service before reading it from lazy config.
* @module @deepseek-ai/dsh-web-app/startup
@@ -21,6 +21,8 @@ export const WEB_STARTUP_SERVICE = 'webStartup'
/** What the web rows read from {@link WEB_STARTUP_SERVICE}. */
export interface WebStartupValues {
/** Whether this invocation opens the default browser after startup. */
openBrowser: boolean
/** `--host`, absent when the invocation did not name one. */
host?: string
/** `--port`, absent when the invocation did not name one. */
@@ -32,6 +34,7 @@ export interface WebStartupValues {
/** The web flag family, as commander parsed it. */
interface WebOptions {
host?: string
open: boolean
port?: string
trustedHost?: string[]
}
@@ -46,11 +49,13 @@ function webCommand(): Command {
.description('Serve the DeepSeek Harness browser UI.')
.helpOption('-h, --help', 'show this help')
.option('--host <host>', 'bind host')
.option('--no-open', 'do not open the Web UI in the default browser')
.option('--port <port>', 'listen port; pass 0 to let the OS pick a free one')
.option('--trusted-host <authority...>', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)')
.addHelpText('after', `
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
`)
}
@@ -73,6 +78,7 @@ export function apply(ctx: Context): void {
program.error(`error: --port must be a number, got ${JSON.stringify(options.port)}`)
}
ctx.provide(WEB_STARTUP_SERVICE, {
openBrowser: options.open,
...options.host !== undefined && { host: options.host },
...options.port !== undefined && { port: Number(options.port) },
trustedHosts: options.trustedHost ?? [],
@@ -0,0 +1,101 @@
/** Default-browser startup over a real Loader tree and listening Web server. */
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Include from '@deepseek-ai/cordis-plugin-include'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import WebServer from '@deepseek-ai/dsh-host-webserver'
import { apply, internals } from '../src/index.ts'
const contexts: Context[] = []
const tempRoots: string[] = []
const originalResolveDistIndex = internals.resolveDistIndex
const originalOpenBrowser = internals.openBrowser
beforeEach(() => {
vi.stubEnv('SSH_CONNECTION', '')
vi.stubEnv('SSH_TTY', '')
})
afterEach(async () => {
for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
for (const root of tempRoots.splice(0)) rmSync(root, { recursive: true, force: true })
internals.resolveDistIndex = originalResolveDistIndex
internals.openBrowser = originalOpenBrowser
vi.unstubAllEnvs()
Reflect.deleteProperty(globalThis, '__dshWebAppApply')
Reflect.deleteProperty(globalThis, '__dshWebServer')
})
describe('web app browser startup', () => {
it('opens the canonical URL only after the complete page is reachable', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-web-browser-open-'))
tempRoots.push(root)
const dist = join(root, 'dist')
mkdirSync(dist)
const index = join(dist, 'index.html')
writeFileSync(index, '<!doctype html><title>ready</title>')
internals.resolveDistIndex = () => index
const webserverModule = join(root, 'webserver.mjs')
const webAppModule = join(root, 'web-app.mjs')
writeFileSync(webserverModule, 'export default globalThis.__dshWebServer\n')
writeFileSync(webAppModule, [
"export const name = 'fixture-web-app'",
"export const inject = ['webServer']",
'export const apply = (ctx, config) => globalThis.__dshWebAppApply(ctx, config)',
'',
].join('\n'))
const config = join(root, 'cordis.yml')
writeFileSync(config, [
'- id: webserver',
` name: ${pathToFileURL(webserverModule).href}`,
' config:',
' host: 127.0.0.1',
' port: 0',
'- id: web-app',
` name: ${pathToFileURL(webAppModule).href}`,
' config:',
' openBrowser: true',
' printUrl: false',
' surfaceContext: false',
' trustedHosts: []',
'',
].join('\n'))
const globals = globalThis as unknown as {
__dshWebAppApply: typeof apply
__dshWebServer: typeof WebServer
}
globals.__dshWebAppApply = apply
globals.__dshWebServer = WebServer
let openedUrl: string | undefined
let openedStatus: number | undefined
let resolveOpened!: () => void
const opened = new Promise<void>((resolve) => { resolveOpened = resolve })
internals.openBrowser = async (url) => {
openedUrl = url
openedStatus = (await fetch(url)).status
resolveOpened()
}
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
await ctx.loader.create({
name: 'cordis:include',
config: { path: pathToFileURL(config).href },
})
await ctx.loader.await()
await opened
expect(openedUrl).toBe(`http://127.0.0.1:${String(ctx.webServer.port)}`)
expect(openedStatus).toBe(200)
})
})
@@ -56,6 +56,7 @@ export const apply = ctx => globalThis.__webStartupApply(ctx)
` inject: [${WEB_STARTUP_SERVICE}]`,
' config:',
" host: !!js ctx.webStartup.host ?? '127.0.0.1'",
' openBrowser: !!js ctx.webStartup.openBrowser',
' port: !!js ctx.webStartup.port ?? 3080',
' trustedHosts: !!js ctx.webStartup.trustedHosts',
'- id: provider',
@@ -89,12 +90,14 @@ describe('web command-line provider', () => {
it('publishes each flag and releases direct service expressions', async () => {
const { values, observed } = await bootProvider([
'--host', '127.0.0.1',
'--no-open',
'--port', '8080',
'--trusted-host', 'lab.internal', 'lab-2.internal',
'--trusted-host', '10.0.0.9',
])
expect(values).toEqual({
host: '127.0.0.1',
openBrowser: false,
port: 8080,
trustedHosts: ['lab.internal', 'lab-2.internal', '10.0.0.9'],
})
@@ -104,9 +107,10 @@ describe('web command-line provider', () => {
it('leaves deployment values to each consumer when flags omit them', async () => {
const { values, observed } = await bootProvider([])
expect(values).toEqual({ trustedHosts: [] })
expect(values).toEqual({ openBrowser: true, trustedHosts: [] })
expect(observed.readerConfig).toEqual({
host: '127.0.0.1',
openBrowser: true,
port: 3080,
trustedHosts: [],
})
@@ -115,6 +119,7 @@ describe('web command-line provider', () => {
it('prints its own help and leaves the consumer pending', async () => {
const { values, observed } = await bootProvider(['--help'])
expect(observed.out).toContain('dsh --profile web')
expect(observed.out).toContain('--no-open')
expect(observed.out).toContain('--trusted-host')
expect(values).toBeUndefined()
expect(observed.readerConfig).toBeUndefined()
+164 -17
View File
@@ -1,19 +1,28 @@
/**
* Web runtime glue behavior: dist resolution through the bundle's own hook,
* the frontend-static child claiming the fallback seat, the web-surface
* prompt section and bash runtime variables, and URL-line printing with the
* runtime's bind-dependent LAN snapshot.
* prompt section and bash runtime variables, and readiness publication through
* the URL line and default-browser handoff.
*/
import { EventEmitter } from 'node:events'
import { spawn, type ChildProcess } from 'node:child_process'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { PassThrough } from 'node:stream'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { createLaunchEnvironmentSnapshot, DSH_LAUNCH_ENVIRONMENT_KEY } from '@deepseek-ai/dsh-launch-environment'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import type { WebServer } from '@deepseek-ai/dsh-host-webserver'
import { apply, Config, internals } from '../src/index.ts'
vi.mock('node:child_process', async importOriginal => ({
...await importOriginal<typeof import('node:child_process')>(),
spawn: vi.fn(),
}))
vi.mock('node:os', async importOriginal => ({
...await importOriginal<typeof import('node:os')>(),
networkInterfaces: () => ({
@@ -24,14 +33,30 @@ vi.mock('node:os', async importOriginal => ({
let dist: string | undefined
beforeEach(() => {
vi.stubEnv('SSH_CONNECTION', '')
vi.stubEnv('SSH_TTY', '')
})
afterEach(() => {
vi.restoreAllMocks()
vi.mocked(spawn).mockReset()
vi.unstubAllEnvs()
internals.resolveDistIndex = originalResolve
internals.openBrowser = originalOpenBrowser
if (dist !== undefined) rmSync(dist, { recursive: true, force: true })
dist = undefined
})
const originalResolve = internals.resolveDistIndex
const originalOpenBrowser = internals.openBrowser
type BrowserLauncher = ChildProcess & { stderr: PassThrough }
/** Minimal browser-launcher process for the native handoff adapter. */
function launcher(): BrowserLauncher {
return Object.assign(new EventEmitter(), { stderr: new PassThrough() }) as unknown as BrowserLauncher
}
/** Stage a dist fixture and point the bundle's resolver at it. */
function stageDist(): string {
@@ -70,9 +95,14 @@ interface BashContribution {
}
describe('web-app runtime glue', () => {
it('mounts dist serving, prompt section, bash variables, and prints the URL with the LAN snapshot', async () => {
it('mounts dist serving, prompt section, bash variables, and publishes the URL with the LAN snapshot', async () => {
stageDist()
const ctx = new Context()
// Editor markers and a project .env SSH value do not establish a remote launch.
ctx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, createLaunchEnvironmentSnapshot([
{ source: 'process', values: { VSCODE_IPC_HOOK_CLI: '/tmp/local-vscode-ipc' } },
{ source: 'project-env', path: '/work/.env', values: { SSH_CONNECTION: 'stale-project-value' } },
]))
const { server, seat } = fakeHttpServer('0.0.0.0')
ctx.provide('webServer', server)
const contributions: BashContribution[] = []
@@ -83,8 +113,11 @@ describe('web-app runtime glue', () => {
},
} as never)
provideLoader(ctx)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(ctx, new Config({ printUrl: true, surfaceContext: true, trustedHosts: ['lab.internal'] }))
const lifecycle: string[] = []
const log = vi.spyOn(console, 'log').mockImplementation((message) => { lifecycle.push(String(message)) })
const openBrowser = vi.fn(async (url: string) => { lifecycle.push(`open:${url}`) })
internals.openBrowser = openBrowser
apply(ctx, new Config({ openBrowser: true, printUrl: true, surfaceContext: true, trustedHosts: ['lab.internal'] }))
await ctx.plugin(SystemPrompt, { persona: '' })
// Settle the injected registrations.
await new Promise(resolve => setTimeout(resolve, 0))
@@ -95,6 +128,13 @@ describe('web-app runtime glue', () => {
trustedHosts: ['192.168.1.5', 'lab.internal'],
})
expect(log).toHaveBeenCalledWith('dsh web: http://127.0.0.1:4567 (LAN: http://192.168.1.5:4567)')
expect(log).toHaveBeenCalledWith('dsh web: opening the default browser; pass --no-open to disable')
expect(openBrowser).toHaveBeenCalledWith('http://127.0.0.1:4567')
expect(lifecycle).toEqual([
'dsh web: http://127.0.0.1:4567 (LAN: http://192.168.1.5:4567)',
'dsh web: opening the default browser; pass --no-open to disable',
'open:http://127.0.0.1:4567',
])
const assembly = await ctx.systemPrompt.assemble()
expect(assembly.sections.find(entry => entry.name === 'harness:source')?.text).toContain('DeepSeek Harness implementation checkout')
const section = assembly.sections.find(entry => entry.name === 'app:web-surface')
@@ -107,15 +147,18 @@ describe('web-app runtime glue', () => {
await ctx.fiber.dispose()
})
it('stays quiet with printUrl off', async () => {
it('publishes no readiness side effect when printing and browser opening are disabled', async () => {
stageDist()
const ctx = new Context()
ctx.provide('webServer', fakeHttpServer().server)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(ctx, new Config({ printUrl: false, surfaceContext: true, trustedHosts: [] }))
const openBrowser = vi.fn(async () => {})
internals.openBrowser = openBrowser
apply(ctx, new Config({ openBrowser: false, printUrl: false, surfaceContext: true, trustedHosts: [] }))
await ctx.plugin(SystemPrompt, { persona: '' })
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).not.toHaveBeenCalled()
expect(openBrowser).not.toHaveBeenCalled()
const assembly = await ctx.systemPrompt.assemble()
expect(assembly.sections.find(entry => entry.name === 'app:web-surface')?.text)
.toContain('rebuilding the affected Web artifacts')
@@ -133,7 +176,7 @@ describe('web-app runtime glue', () => {
return () => {}
},
} as never)
apply(ctx, new Config({ printUrl: false, surfaceContext: false, trustedHosts: [] }))
apply(ctx, new Config({ openBrowser: false, printUrl: false, surfaceContext: false, trustedHosts: [] }))
await ctx.plugin(SystemPrompt, { persona: '' })
await new Promise(resolve => setTimeout(resolve, 0))
const assembly = await ctx.systemPrompt.assemble()
@@ -148,44 +191,69 @@ describe('web-app runtime glue', () => {
const ctx = new Context()
ctx.provide('webServer', fakeHttpServer().server)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(ctx, new Config({ printUrl: true, surfaceContext: true, trustedHosts: [] }))
apply(ctx, new Config({ openBrowser: false, printUrl: true, surfaceContext: true, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).toHaveBeenCalledWith('dsh web: http://127.0.0.1:4567')
await ctx.fiber.dispose()
})
it('defers the URL line until Loader settlement and drops it on failure or teardown', async () => {
it.each([
['SSH_CONNECTION', '10.0.0.2 55000 10.0.0.9 22'],
['SSH_TTY', '/dev/pts/3'],
] as const)('prints the host URL but skips browser handoff when %s marks an SSH launch', async (name, value) => {
vi.stubEnv(name, value)
stageDist()
// Settlement path: the line waits for loader.await() so supervisors can
// RPC immediately after observing it.
const ctx = new Context()
ctx.provide('webServer', fakeHttpServer().server)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
const openBrowser = vi.fn(async () => {})
internals.openBrowser = openBrowser
apply(ctx, new Config({ openBrowser: true, printUrl: true, surfaceContext: false, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).toHaveBeenCalledWith('dsh web: http://127.0.0.1:4567')
expect(openBrowser).not.toHaveBeenCalled()
await ctx.fiber.dispose()
})
it('defers readiness publication until Loader settlement and drops it on failure or teardown', async () => {
stageDist()
const openBrowser = vi.fn(async () => {})
internals.openBrowser = openBrowser
// Settlement path: both actions wait for loader.await() so their consumers
// can request the complete app immediately.
const settled = new Context()
settled.provide('webServer', fakeHttpServer().server)
let release: () => void
const settlement = new Promise<void>((resolve) => { release = resolve })
provideLoader(settled, () => settlement)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(settled, new Config({ printUrl: true, surfaceContext: true, trustedHosts: [] }))
apply(settled, new Config({ openBrowser: true, printUrl: true, surfaceContext: true, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).not.toHaveBeenCalled()
expect(openBrowser).not.toHaveBeenCalled()
release!()
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).toHaveBeenCalledWith('dsh web: http://127.0.0.1:4567')
expect(openBrowser).toHaveBeenCalledWith('http://127.0.0.1:4567')
await settled.fiber.dispose()
// Failed path: Loader reports the sibling failure; the app prints no URL
// for a process that is about to exit.
log.mockClear()
openBrowser.mockClear()
const failed = new Context()
failed.provide('webServer', fakeHttpServer().server)
provideLoader(failed, async () => { throw new Error('boot failed') })
apply(failed, new Config({ printUrl: true, surfaceContext: true, trustedHosts: [] }))
apply(failed, new Config({ openBrowser: true, printUrl: true, surfaceContext: true, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).not.toHaveBeenCalled()
expect(openBrowser).not.toHaveBeenCalled()
await failed.fiber.dispose()
// Torn-down path: settlement resolves after the webserver is gone — no
// line, no crash.
log.mockClear()
openBrowser.mockClear()
const torn = new Context()
const child = torn.plugin((childCtx: Context) => {
childCtx.provide('webServer', fakeHttpServer().server)
@@ -194,11 +262,12 @@ describe('web-app runtime glue', () => {
let releaseTorn: () => void
const tornSettlement = new Promise<void>((resolve) => { releaseTorn = resolve })
provideLoader(torn, () => tornSettlement)
apply(torn, new Config({ printUrl: true, surfaceContext: true, trustedHosts: [] }))
apply(torn, new Config({ openBrowser: true, printUrl: true, surfaceContext: true, trustedHosts: [] }))
await child.dispose() // the webServer service goes away
releaseTorn!()
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).not.toHaveBeenCalled()
expect(openBrowser).not.toHaveBeenCalled()
await torn.fiber.dispose()
})
@@ -210,7 +279,7 @@ describe('web-app runtime glue', () => {
const { server } = fakeHttpServer()
Object.defineProperty(server, 'port', { get: () => undefined })
ctx.provide('webServer', server)
apply(ctx, new Config({ printUrl: false, surfaceContext: true, trustedHosts: [] }))
apply(ctx, new Config({ openBrowser: false, printUrl: false, surfaceContext: true, trustedHosts: [] }))
await ctx.plugin(SystemPrompt, { persona: '' })
await new Promise(resolve => setTimeout(resolve, 0))
await expect(ctx.systemPrompt.assemble()).rejects.toThrow('webServer service missing')
@@ -228,4 +297,82 @@ describe('web-app runtime glue', () => {
expect((error as Error).message).toContain('frontend dist not built')
}
})
it.each([
['Error', new Error('no desktop'), 'no desktop'],
['non-Error', 'desktop unavailable', 'desktop unavailable'],
] as const)('keeps the server running and reports the manual URL when a browser failure is %s', async (_kind, failure, reason) => {
stageDist()
const ctx = new Context()
ctx.provide('webServer', fakeHttpServer().server)
internals.openBrowser = vi.fn(async () => { throw failure })
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
const diagnostic = vi.spyOn(console, 'error').mockImplementation(() => {})
apply(ctx, new Config({ openBrowser: true, printUrl: false, surfaceContext: false, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).toHaveBeenCalledWith('dsh web: opening the default browser; pass --no-open to disable')
expect(diagnostic).toHaveBeenCalledWith(
`web-app: could not open the default browser because ${reason}; visit http://127.0.0.1:4567 manually`,
)
expect(ctx.get('webServer')).toBeDefined()
await ctx.fiber.dispose()
})
it('scrubs the helper environment and reports helper spawn or exit failures', async () => {
vi.stubEnv('DEEPSEEK_API_KEY', 'must-not-reach-browser')
vi.stubEnv('DSH_HOME', '/must-not-reach-browser')
const completed = launcher()
vi.mocked(spawn).mockReturnValueOnce(completed)
const completion = originalOpenBrowser('http://127.0.0.1:4567')
const [command, args, options] = vi.mocked(spawn).mock.calls[0]!
expect(command).toBe(process.execPath)
expect(args).toEqual([
'--input-type=module',
'--eval', expect.stringContaining('await import('),
'--', 'http://127.0.0.1:4567',
])
expect(args?.[2]).toContain("if (process.platform === 'win32')")
expect(args?.[2]).toContain('launcher.ref()')
expect(options?.env).not.toHaveProperty('DEEPSEEK_API_KEY')
expect(options?.env).not.toHaveProperty('DSH_HOME')
expect(options?.env?.PATH).toBe(process.env.PATH)
expect(options?.stdio).toEqual(['ignore', 'inherit', 'pipe'])
completed.emit('close', 0)
await expect(completion).resolves.toBeUndefined()
expect(completed.listenerCount('error')).toBe(0)
const completedWithStderr = launcher()
vi.mocked(spawn).mockReturnValueOnce(completedWithStderr)
const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
const completionWithStderr = originalOpenBrowser('http://127.0.0.1:4567')
completedWithStderr.stderr?.write('launcher note\n')
completedWithStderr.emit('close', 0)
await expect(completionWithStderr).resolves.toBeUndefined()
expect(stderr).toHaveBeenCalledWith('launcher note\n')
const failedWithReason = launcher()
vi.mocked(spawn).mockReturnValueOnce(failedWithReason)
const reasonFailure = originalOpenBrowser('http://127.0.0.1:4567')
const reasonAssertion = expect(reasonFailure).rejects.toThrow('desktop unavailable')
failedWithReason.stderr?.write('Error: desktop unavailable\n at fixture')
failedWithReason.emit('close', 1)
await reasonAssertion
const failed = launcher()
vi.mocked(spawn).mockReturnValueOnce(failed)
const failure = originalOpenBrowser('http://127.0.0.1:4567')
const failureAssertion = expect(failure).rejects.toThrow('exited with code 3')
await Promise.resolve()
failed.emit('close', 3)
await failureAssertion
const errored = launcher()
vi.mocked(spawn).mockReturnValueOnce(errored)
const error = originalOpenBrowser('http://127.0.0.1:4567')
const errorAssertion = expect(error).rejects.toThrow('spawn failed')
await Promise.resolve()
errored.emit('error', new Error('spawn failed'))
await errorAssertion
expect(errored.listenerCount('close')).toBe(0)
})
})
+6
View File
@@ -29,12 +29,18 @@
{
"path": "../../host/webserver"
},
{
"path": "../../util/launch-environment"
},
{
"path": "../../core/system-prompt"
},
{
"path": "../../shell/shell-env"
},
{
"path": "../../subprocess/subprocess"
},
{
"path": "../../runtime-diagnostics/invariants"
}
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/README.md
README.md: cff74ddb048df8b37643f4c44f01e2bda4160a77
README.zh.md: 5e5d623509b9152fb7b59ea228cfa42720b737af
README.md: fe57d58a0f4fa1c9bff2699ffb363c80197fc5ed
README.zh.md: c721710c9f20ba10f20392c207e1de4169ec0e12
+1
View File
@@ -29,6 +29,7 @@ The browser side of the dsh web GUI: shell boot, browser-host communication, sha
| [`ui-commands/`](ui-commands/README.md) | Provides session-aware command discovery and dispatch. |
| [`ui-input-trigger/`](ui-input-trigger/README.md) | Coordinates inline command and reference suggestions. |
| [`ui-skill/`](ui-skill/README.md) | Adds skill references to inline suggestions. |
| [`ui-reference/`](ui-reference/README.md) | Unified Web `@file` / `@session` reference source. |
| [`ui-subagent/`](ui-subagent/README.md) | Provides subagent navigation, child transcript states, and inline references. |
| [`ui-jobs/`](ui-jobs/README.md) | Lists this session's background jobs in the conversation header. |
| [`ui-model-selection/`](ui-model-selection/README.md) | Provides model selection in conversation surfaces. |
+1
View File
@@ -29,6 +29,7 @@ dsh web GUI 的浏览器侧:shell 启动、浏览器与宿主通信、共享 U
| [`ui-commands/`](ui-commands/README.md) | 提供会话感知的命令发现与分发。 |
| [`ui-input-trigger/`](ui-input-trigger/README.md) | 协调内联命令和引用建议。 |
| [`ui-skill/`](ui-skill/README.md) | 向内联建议添加 skill(技能)引用。 |
| [`ui-reference/`](ui-reference/README.md) | 统一的 Web `@file` / `@session` 引用 source。 |
| [`ui-subagent/`](ui-subagent/README.md) | 提供 subagent(子 agent)导航、子级 transcript(文本记录)的状态和内联引用。 |
| [`ui-jobs/`](ui-jobs/README.md) | 在会话标题栏列出当前会话的后台任务。 |
| [`ui-model-selection/`](ui-model-selection/README.md) | 在对话界面中提供模型选择。 |
@@ -1866,6 +1866,51 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
})
/** Canonical fixture implementation of the generated Goal Remote contract. */
/** Canonical fixture implementation of the generated reference-discovery Remote contracts. */
const referenceRemotes = {
files(id: SessionId, query: string): RpcResult<{ path: string; kind: 'file' | 'directory' }[]> {
const missing = requireGoalSession(id)
if (missing !== undefined) return missing
const needle = query.toLocaleLowerCase()
const items = [
{ path: 'notes', kind: 'directory' as const },
{ path: 'README.md', kind: 'file' as const },
{ path: 'notes/demo.txt', kind: 'file' as const },
].filter(item => item.path.toLocaleLowerCase().includes(needle))
return { ok: true, value: items }
},
sessions(id: SessionId, query: string): RpcResult<{
sessionId: SessionId
label: string
cwd?: string
createdAt: number
mention: string
}[]> {
const missing = requireGoalSession(id)
if (missing !== undefined) return missing
const needle = query.toLocaleLowerCase()
const value = sessions
.filter(item => item.sessionId !== id)
.filter(item => String(item.sessionId).toLocaleLowerCase().includes(needle)
|| item.cwd?.toLocaleLowerCase().includes(needle) === true)
.map((item) => {
const label = item.sessionId === sid('fx-beta') ? 'Fixture child session' : String(item.sessionId)
const encoded = btoa(JSON.stringify(item.sessionId))
.replaceAll('+', '-')
.replaceAll('/', '_')
.replace(/=+$/u, '')
return {
sessionId: item.sessionId,
label,
...item.cwd === undefined ? {} : { cwd: item.cwd },
createdAt: item.updatedAt,
mention: `@[${label}](dsh-session:${encoded})`,
}
})
return { ok: true, value }
},
}
const goalRemotes = {
create(id: SessionId, request: { objective: string; maxGoalRounds?: number }): RpcResult<{ ref: FxGoalRef }> {
const missing = requireGoalSession(id)
@@ -3053,6 +3098,7 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
args: {
agentId: SessionId
line?: string
query?: string
images?: readonly unknown[]
ref?: { id: string; revision: number }
request?: { objective?: string; maxGoalRounds?: number }
@@ -3062,6 +3108,8 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
switch (endpoint) {
case 'commands/list': return Promise.resolve(commandRemotes.list(sessionId))
case 'commands/execute': return Promise.resolve(commandRemotes.execute(sessionId, args.line as string, args.images ?? []))
case 'fileReferences/list': return Promise.resolve(referenceRemotes.files(sessionId, args.query ?? ''))
case 'sessionReferenceResolver/candidates': return Promise.resolve(referenceRemotes.sessions(sessionId, args.query ?? ''))
case 'goals/create': return Promise.resolve(goalRemotes.create(sessionId, {
objective: args.request?.objective as string,
...args.request?.maxGoalRounds === undefined ? {} : { maxGoalRounds: args.request.maxGoalRounds },
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/runtime/README.md
README.md: 7d0e90809aaffb9503f2222e1f7426b13d72d7d5
README.zh.md: d0a43c32b4861d02786aa0c42e1af72047c9032a
README.md: eae49163c14f122a3603bcaa8a61aaac7a9a7270
README.zh.md: 9e8f82a0bcf0d2021c9b16bd51acf3c72dd04cb2
+1 -1
View File
@@ -56,7 +56,7 @@ The Chat builder keeps one mutable keyed store per Session. Content updates noti
## Trajectory request data
Trajectory Definitions assemble one chronological, purpose-discriminated provider-request stream. Assistant requests always carry their numeric `turn` and `step`; compaction requests carry `step: 0` and a `turn` owner that may be `null`. That null owner means a manual compaction ran standalone between turns, not that it belongs to either adjacent turn. A `session/end-seed` boundary closes an unmatched compaction request as an error at the boundary time with `Compaction was interrupted before completion.`; a later start projects as an independent request instead of overwriting the orphan.
Trajectory Definitions assemble one chronological, purpose-discriminated provider-request stream. Assistant requests always carry their numeric `turn` and `step`; compaction requests carry `step: 0` and a `turn` owner that may be `null`. That null owner means a manual compaction ran standalone between turns, not that it belongs to either adjacent turn. A cancellation-finalized `assistant/message` retains its durable result seq and provider provenance but does not complete the request; `step/end` classifies that request as an error. A `session/end-seed` boundary closes an unmatched compaction request as an error at the boundary time with `Compaction was interrupted before completion.`; a later start projects as an independent request instead of overwriting the orphan.
## Code Mode child-call tree
+1 -1
View File
@@ -56,7 +56,7 @@ Chat builder 为每个 Session 保留一个 mutable keyed store。内容更新
## Trajectory 请求数据
Trajectory Definition 组装出一条按时间顺序排列、以用途为判别字段的提供方请求流。助手请求始终携带数值型 `turn``step`;压缩请求携带 `step: 0`,其 `turn` 所有者可以是 `null`。这个 null 所有者表示手动压缩独立运行在两个轮次之间,并不表示它属于任一相邻轮次。`session/end-seed` 边界会在边界时刻将未匹配的压缩请求以错误状态结束,错误固定为 `Compaction was interrupted before completion.`;后续 start 会投影为独立请求,而不会覆盖这项遗留的未匹配请求。
Trajectory Definition 组装出一条按时间顺序排列、以用途为判别字段的提供方请求流。助手请求始终携带数值型 `turn``step`;压缩请求携带 `step: 0`,其 `turn` 所有者可以是 `null`。这个 null 所有者表示手动压缩独立运行在两个轮次之间,并不表示它属于任一相邻轮次。由取消定稿的 `assistant/message` 会保留持久结果 seq 和提供方信息,但不会将请求标记为完成;`step/end` 会把该请求归类为错误。`session/end-seed` 边界会在边界时刻将未匹配的压缩请求以错误状态结束,错误固定为 `Compaction was interrupted before completion.`;后续 start 会投影为独立请求,而不会覆盖这项遗留的未匹配请求。
## Code Mode 子调用树
@@ -38,7 +38,11 @@ export interface ISession {
* @param mode - 'queue' appends a turn; 'steer' interrupts the running one.
* @returns acceptance, or the business error (also mirrored into snapshot.promptError).
*/
prompt(content: PromptContentPart[], mode: 'queue' | 'steer'): Promise<RpcResult<{ accepted: true }>>
prompt(
content: PromptContentPart[],
mode: 'queue' | 'steer',
signal?: AbortSignal,
): Promise<RpcResult<{ accepted: true }>>
/**
* Resolve one durable image referenced by this session.
* @param attachmentId - opaque id found in the folded session log.
+1 -1
View File
@@ -85,7 +85,7 @@ export {
} from './sessions/conversation.ts'
export { emptyAssistantBlock } from './sessions/partial.ts'
export { isTokenDelta } from './sessions/assistant-timing.ts'
export { contextForm, contextProvenance } from './sessions/context-provenance.ts'
export { contextForm, contextProvenance, sessionRecallLabels } from './sessions/context-provenance.ts'
export { displayFailureMessage } from './sessions/failure-display.ts'
export type {
ConversationContext, ConversationContextOriginKind,
@@ -58,6 +58,19 @@ function joined(names: string[]): string | null {
return names.length > 0 ? names.join(', ') : null
}
/**
* The referenced-session labels of one durable `session-reference` recall
* source, in first-seen order; empty for every other source shape, including
* a foreign or older log whose reference entries carry no readable label.
* @param source - the logged `user/message` source, exactly as recorded.
* @returns distinct non-empty reference labels.
*/
export function sessionRecallLabels(source: unknown): string[] {
const record = asRecord(source)
if (record === null || readString(record, 'kind') !== 'session-reference') return []
return collect(record, 'references', 'label')
}
/**
* Project one durable message source onto its transcript role and producer name.
*
@@ -92,14 +92,14 @@ export interface AssistantTiming {
completedTime: number
}
/** A finalized (or interruption-frozen) assistant message. */
/** A finalized assistant message or an interruption-frozen streaming prefix. */
export interface AssistantMessageNode {
kind: 'assistant'
seq: number
/**
* Stable identity of the finalized model output, carried from the
* `assistant/message` event. Absent on interruption-frozen partials: those
* were never finalized, so they address no durable message.
* Stable identity carried from the `assistant/message` event. Absent only on
* synthetic interruption fallbacks assembled from chunks without a durable
* assistant message.
*/
messageId?: MessageId
/** Unix epoch ms from the source session event (or turn/end when frozen from a partial). */
@@ -112,8 +112,9 @@ export interface AssistantMessageNode {
requestConfig?: AssistantRequestConfig
/** Timing derived from the recorded step/chunk/message event sequence. */
timing?: AssistantTiming
/** Frozen partial of an aborted turn (no finalize ever arrives): rendered with a marker.
* Synthetic seq (fractional, derived from the turn/end seq) keeps it ordered inside the flow. */
/** Prefix of an aborted turn, rendered with a marker. A durable
* finalized prefix uses its event seq; a chunk-only fallback uses a fractional
* seq derived from the closing boundary to keep it ordered inside the flow. */
interrupted?: true
}
@@ -187,7 +187,11 @@ export class Session implements SessionFace {
* @param mode - queue appends after the current turn; steer interrupts it.
* @returns the prompt result (also mirrored into promptError on failure).
*/
async prompt(content: PromptContentPart[], mode: 'queue' | 'steer'): Promise<RpcResult<{ accepted: true }>> {
async prompt(
content: PromptContentPart[],
mode: 'queue' | 'steer',
signal?: AbortSignal,
): Promise<RpcResult<{ accepted: true }>> {
this.promptError = null
this.lastAgentError = null
// Synchronous, before the first await: the blank → engaging edge must be
@@ -204,7 +208,7 @@ export class Session implements SessionFace {
mode,
content,
clientTimeZone: resolvedClientTimeZone(),
})).result
}, signal)).result
} else if (this.address.mode === 'one-shot') {
result = {
ok: false,
@@ -231,7 +235,7 @@ export class Session implements SessionFace {
? [{ type: 'text' as const, text: part.text }]
: []),
clientTimeZone: resolvedClientTimeZone(),
})).result
}, signal)).result
result = routed.ok ? { ok: true, value: { accepted: true } } : routed
}
}

Some files were not shown because too many files have changed in this diff Show More