Merge remote-tracking branch 'origin/master' into fix/truncate-without-splitting-surrogates

This commit is contained in:
mektpoy committed 2026-09-24 20:03:02 +08:00
commit ea3667175d
660 files changed
+65859 -45655

No files matched your search

@@ -9,11 +9,11 @@
en: 4945ce54f66df55c
zh: 9fbd833c1d8122a7
/agent-note-deepseek-llm-api-request-extensions-for-session-logs-and-plugin-packages/decision:
en: 75b5f9e642b6c25c
zh: c775a113d5ea0626
en: 2388519a1afaac92
zh: 6bf758f0560e29a5
/agent-note-deepseek-llm-api-request-extensions-for-session-logs-and-plugin-packages/incremental-session-log-field:
en: edae015c24a2bc42
zh: 0c7e23be17d9d424
en: 4e3138fbdd94259b
zh: 8fda801522045b88
/agent-note-deepseek-llm-api-request-extensions-for-session-logs-and-plugin-packages/plugin-package-field:
en: 0e51b8d8a1bd60fc
zh: dd18fd176c3f101f
@@ -33,5 +33,5 @@
en: b6eff63f49220dcc
zh: 8e4a6c785ed2256a
/agent-note-deepseek-llm-api-request-extensions-for-session-logs-and-plugin-packages/consequences:
en: c9a0a5323480eb3d
zh: 5df6e88de50038ce
en: 144fc01ad3b2b9bd
zh: 0acff935bd7835de
@@ -14,13 +14,13 @@ Both values belong only on the official DeepSeek adapter path. Adding them to `G
## Decision
`@deepseek-ai/dsh-deepseek-llm-api-extensions` registers `ctx.deepseekLlmApiExtensions`, an additive registry of top-level fields for `deepseek-official` request bodies. A contributor claims one declaration-merged field with `register()`. The adapter invokes `prepare()` after serializing the exact wire messages, passes the request cancellation signal, rejects preparation or base-field collision before HTTP, merges the detached fields, and calls the captured `accept()` transaction after HTTP 2xx. The registry stops awaiting preparation after cancellation even if a contributor ignores the signal. Acceptance failures remain request failures under `REQUEST_EXTENSION`; transport and non-2xx failures never accept a contribution. A composition without the registry retains the reusable base adapter. Shipped compositions mount the registry and both contributors: both package metadata and Session-log upload are enabled by default; `session-log-deepseek.enabled: false` disables log upload under the [default-upload decision](2026-09-14-session-log-upload-default.md). Keyless `deepseek-official` replay invokes preparation with a synthetic empty base body and the same acceptance transaction before its first recorded chunk, preserving post-2xx extension side effects rather than field bytes.
`@deepseek-ai/dsh-deepseek-llm-api-extensions` registers `ctx.deepseekLlmApiExtensions`, an additive registry of top-level fields for `deepseek-official` request bodies. A contributor claims one declaration-merged field with `register()`. The adapter invokes `prepare()` after serializing the exact wire messages, passes the request cancellation signal, rejects preparation or base-field collision before HTTP, merges the detached fields, and calls the captured `accept()` transaction after HTTP 2xx. A merged body that fails to serialize is sent without extension fields or acceptance under the [bounded-upload decision](2026-09-24-bounded-session-log-upload.md). The registry stops awaiting preparation after cancellation even if a contributor ignores the signal. Acceptance failures remain request failures under `REQUEST_EXTENSION`; transport and non-2xx failures never accept a contribution. A composition without the registry retains the reusable base adapter. Shipped compositions mount the registry and both contributors: both package metadata and Session-log upload are enabled by default; `session-log-deepseek.enabled: false` disables log upload under the [default-upload decision](2026-09-14-session-log-upload-default.md). Keyless `deepseek-official` replay invokes preparation with a synthetic empty base body and the same acceptance transaction before its first recorded chunk, preserving post-2xx extension side effects rather than field bytes.
The provider-neutral `llm` package and `llm-pi-ai` contain no extension type, service lookup, field merge, or acceptance call.
## Incremental session-log field
`@deepseek-ai/dsh-session-log-deepseek` owns the default-on `dsh_session_log` field. When enabled, each request carrying a live Session id sends the contiguous canonical event suffix after the greatest durable `session-log-deepseek/delivery-accepted` watermark for that same Session identity. The field includes the immutable Session header and complete event envelopes. A 2xx appends a new watermark for the transmitted `throughSeq`; that event enters the following request's suffix. Forked logs retain parent watermark ids, so a child starts from sequence zero under its own identity. Concurrent acceptances may arrive out of order, and the maximum watermark remains authoritative. A process-local fold scans each Session event once and incrementally consumes later appends; a new Session object or HMR generation rebuilds the fold from durable history.
`@deepseek-ai/dsh-session-log-deepseek` owns the default-on `dsh_session_log` field. When enabled, each request carrying a live Session id sends the longest contiguous canonical event run after the greatest durable `session-log-deepseek/delivery-accepted` watermark for that same Session identity that fits the `maxBytes` limit of the [bounded-upload decision](2026-09-24-bounded-session-log-upload.md). The field includes the immutable Session header and complete event envelopes. A 2xx appends a new watermark for the transmitted `throughSeq`; that event enters the following request's suffix. Forked logs retain parent watermark ids, so a child starts from sequence zero under its own identity. Concurrent acceptances may arrive out of order, and the maximum watermark remains authoritative. A process-local fold scans each Session event once and incrementally consumes later appends; a new Session object or HMR generation rebuilds the fold from durable history.
The failure direction is at least once. A transport or provider rejection records no watermark. A crash after remote acceptance but before the watermark persists causes replay after resume, never a skipped sequence. Existing session checkpoints persist the event; the upload plugin owns no second store.
@@ -85,7 +85,7 @@ About 98% of the measured v1 real-session events were `assistant/chunk`. Omittin
## Consequences
Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. Unless Session-log upload is disabled, each eligible request also carries the complete newly unaccepted Session suffix. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Manifest resolution, field collision, acceptance logging, or provider schema rejection fails the model request rather than silently dropping metadata.
Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. Unless Session-log upload is disabled, each eligible request also carries newly unaccepted Session events, up to `maxBytes` per request. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Manifest resolution, field collision, acceptance logging, or provider schema rejection fails the model request rather than silently dropping metadata. A merged body that fails to serialize is the exception: the request proceeds without extension fields and logs a warning.
The `delivery-accepted` event becomes part of the canonical log and is itself delivered on a later request. Crash recovery can duplicate a suffix but does not infer acceptance from assistant output or create a second local cursor store. Direct calls without a live Session omit the session field; host package inventory remains available.
@@ -14,13 +14,13 @@ Status: implemented
## 决策
`@deepseek-ai/dsh-deepseek-llm-api-extensions` 注册 `ctx.deepseekLlmApiExtensions`,即 `deepseek-official` 请求正文顶层字段的增量注册表。贡献方通过 `register()` 认领一个经声明合并的字段。适配器在序列化确切协议消息后调用 `prepare()`、传入请求取消信号,在 HTTP 前拒绝准备失败或基础字段冲突,合并分离字段,并在 HTTP 2xx 后调用捕获的 `accept()` 事务。即使贡献方忽略信号,注册表也会在取消后停止等待准备。接受失败仍以 `REQUEST_EXTENSION` 使请求失败;传输失败与非 2xx 失败绝不会接受贡献。未挂载注册表的组合会保留可复用基础适配器。随附组合会挂载注册表与两个贡献方:插件包元数据和会话日志上传均默认开启;按[默认上传决策](2026-09-14-session-log-upload-default.zh.md),设置 `session-log-deepseek.enabled: false` 可关闭日志上传。无密钥 `deepseek-official` 回放会使用合成的空基础正文执行准备,并在第一个已记录分片前调用同一接受事务;它保持的是 2xx 后扩展副作用,而非字段字节。
`@deepseek-ai/dsh-deepseek-llm-api-extensions` 注册 `ctx.deepseekLlmApiExtensions`,即 `deepseek-official` 请求正文顶层字段的增量注册表。贡献方通过 `register()` 认领一个经声明合并的字段。适配器在序列化确切协议消息后调用 `prepare()`、传入请求取消信号,在 HTTP 前拒绝准备失败或基础字段冲突,合并分离字段,并在 HTTP 2xx 后调用捕获的 `accept()` 事务。合并后的正文无法序列化时,按[有上限上传决策](2026-09-24-bounded-session-log-upload.zh.md)不带扩展字段发送,也不执行接受。即使贡献方忽略信号,注册表也会在取消后停止等待准备。接受失败仍以 `REQUEST_EXTENSION` 使请求失败;传输失败与非 2xx 失败绝不会接受贡献。未挂载注册表的组合会保留可复用基础适配器。随附组合会挂载注册表与两个贡献方:插件包元数据和会话日志上传均默认开启;按[默认上传决策](2026-09-14-session-log-upload-default.zh.md),设置 `session-log-deepseek.enabled: false` 可关闭日志上传。无密钥 `deepseek-official` 回放会使用合成的空基础正文执行准备,并在第一个已记录分片前调用同一接受事务;它保持的是 2xx 后扩展副作用,而非字段字节。
提供方无关的 `llm` 包与 `llm-pi-ai` 不包含任何扩展类型、服务查找、字段合并或接受调用。
## 增量会话日志字段
`@deepseek-ai/dsh-session-log-deepseek` 拥有默认开启的 `dsh_session_log` 字段。启用后,每个携带存活会话 id 的请求都会发送该确切会话身份最大持久 `session-log-deepseek/delivery-accepted` 水位之后的连续权威事件后缀。该字段包含不可变会话 header 与完整事件信封。2xx 会为已发送的 `throughSeq` 追加新水位;该事件会进入下一次请求的后缀。Fork 日志会保留父级水位 id,因此子会话会在自己的身份下从序列零开始。并发接受可能乱序到达,最大水位仍保持权威。进程内 fold 会让每条会话事件只被扫描一次,并增量消费后续追加;新的会话对象或 HMR generation 会从持久历史重建该 fold。
`@deepseek-ai/dsh-session-log-deepseek` 拥有默认开启的 `dsh_session_log` 字段。启用后,每个携带存活会话 id 的请求都会发送该确切会话身份最大持久 `session-log-deepseek/delivery-accepted` 水位之后、能放进[有上限上传决策](2026-09-24-bounded-session-log-upload.zh.md)中 `maxBytes` 上限的最长连续权威事件段。该字段包含不可变会话 header 与完整事件信封。2xx 会为已发送的 `throughSeq` 追加新水位;该事件会进入下一次请求的后缀。Fork 日志会保留父级水位 id,因此子会话会在自己的身份下从序列零开始。并发接受可能乱序到达,最大水位仍保持权威。进程内 fold 会让每条会话事件只被扫描一次,并增量消费后续追加;新的会话对象或 HMR generation 会从持久历史重建该 fold。
失败方向为至少一次。传输失败或提供方拒绝不会记录水位。远端接受后、水位持久化前发生崩溃,会在恢复后触发重放,绝不会跳过序列。现有会话检查点会持久化该事件;上传插件不拥有第二份存储。
@@ -85,7 +85,7 @@ Status: implemented
## 后果
DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。除非关闭会话日志上传,否则符合条件的请求还会携带完整的未接受会话新后缀。这些字段对模型不可见,不增加提示词 token,也不改变 KV Cache,但可能显著增大 HTTP 正文。Manifest 解析、字段冲突、接受记录或提供方 schema 拒绝会使模型请求失败,而不会静默丢弃元数据。
DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。除非关闭会话日志上传,否则符合条件的请求还会携带新的未接受会话事件,每次最多 `maxBytes`。这些字段对模型不可见,不增加提示词 token,也不改变 KV Cache,但可能显著增大 HTTP 正文。Manifest 解析、字段冲突、接受记录或提供方 schema 拒绝会使模型请求失败,而不会静默丢弃元数据。合并后的正文无法序列化是例外:请求不带扩展字段继续发送,并记录一条告警。
`delivery-accepted` 事件会成为权威日志的一部分,并在后续请求中自行交付。崩溃恢复可能重复后缀,但不会根据 assistant 输出推断接受,也不会创建第二份本地游标存储。缺少存活会话的直接调用会省略会话字段;宿主包清单仍然可用。
@@ -15,14 +15,14 @@
en: 59c3648960630ff8
zh: 1891f8659e660e45
/agent-note-workspace-file-service/decision/the-workspacefiles-file-operations:
en: cb489857d97a1b11
zh: b1a7880df5607c05
en: efdecd9cf80c03e7
zh: 5ea40e48ab0559a2
/agent-note-workspace-file-service/decision/paths-on-the-wire:
en: 7b5a96adc08177bc
zh: ae4692e5e62e7de0
/agent-note-workspace-file-service/decision/file-checks-and-workspace-containment:
en: 4399dcc29336f886
zh: 69cf2fc174dc83a2
en: 1b588f346590a0fd
zh: 1cdac02b967b157f
/agent-note-workspace-file-service/decision/failures:
en: 0e419dd7bdcf38ba
zh: 6cb588a77593913c
@@ -45,8 +45,8 @@
en: 246bb7aed62f6c9b
zh: debf6d4129ee477b
/agent-note-workspace-file-service/testing:
en: 3ab0dabf8ef85258
zh: c3d0a634bb5e2928
en: 776c380b6150065e
zh: df3f0f1050966ff7
/agent-note-workspace-file-service/deferred:
en: b033218338ca0122
zh: aa48fa67f9eb36c3
@@ -41,7 +41,7 @@ Every Host method takes `WorkspaceFileScope` first. The `workspaceFileScope` Gat
- **`read`** returns one window of lines, `WorkspaceFileText = WorkspaceFileStat & { offset, text, lines, eof }`; `lines` counts the page's lines, so a page holding one empty line (`text: ''`, `lines: 1`) and a page past the end (`lines: 0`) read differently. `range.offset` is the 1-based first line and defaults to 1; `range.limit` is the largest number of lines and defaults to `maxLines`, which it may not exceed. Lines end at `\n` and a final `\n` terminates the last line rather than opening an empty one; `text` joins the page's lines with `\n` and carries no terminator; `eof` is true when the page includes the last line, and an offset past the end returns an empty page with `eof` true. The pager walks `streamText`, counts the lines before the window without keeping them, admits each in-window segment against `maxBytes` before buffering it, and returns at the first character past the window, so a file of any size costs one page of memory. The `version` and `bytes` on a page are the stat's, taken before the stream.
- **`readBytes`** returns native bytes with file metadata, offset, and EOF. The required options object selects a complete read under `maxFileBytes` when `range` is absent, or a bounded window under `maxBytes` when present. `range: {}` uses offset zero and the configured window cap; windows can read files above the complete-file cap. The [binary transfer decision](2026-09-17-workspace-file-binary-transfer.md) owns the Remote response encoding.
- **`baseFile`** selects target resolution independently of `range`: the target `path` must be relative and resolves from the base file's directory. The Host applies regular-file checks to both files and preserves backend read authority. [Document Preview](2026-09-08-document-preview-operations.md) owns content loading and address semantics.
- **`list`** returns `WorkspaceDirectoryListing { path, entries, truncated }`: the listed directory as a workspace path relative to the root (empty for the root), its direct children in the backend's stable name order as `{ name, type, size? }`, and whether `maxEntries` cut the list. `type` is `file`, `directory`, or `other`; a symlink child reports the type of what it points to and a dangling one is `other`, while opening such a child still fails the link gate below. Dotfiles are listed; nothing is filtered.
- **`list`** returns `WorkspaceDirectoryListing { path, entries, truncated }`: the listed directory as a workspace path relative to the root (empty for the root), its direct children in the backend's stable name order as `{ name, type, size? }`, and whether `maxEntries` cut the list. `type` is `file`, `directory`, or `other`; a symlink child reports the type of what it points to and a dangling one is `other`. A child the listing reports as a directory opens through its resolved target, including a Windows junction, and the listing then names that target; [directory link listing](../bug-fix/2026-09-18-windows-directory-junction-listing.md) owns the rule. Dotfiles are listed; nothing is filtered.
- **`changes`** yields `WorkspaceFileWatchFrame`: `{ kind: 'ready' }` after the observation queue is registered and the workspace root resolves, followed by `{ kind: 'change', change }`. The `WorkspaceFileChange` payload is `{ absolutePath, version }` for a present file or `{ absolutePath, absent: true }` for one observed gone. Its source is `fs/observed` inside the workspace root, never an OS watcher. Observations after the first pull are queued, including during root resolution; cancellation or plugin disposal ends the generation.
### Paths on the wire
@@ -54,12 +54,12 @@ Two path vocabularies leave the service, and each method uses exactly one. `read
`read`, `readBytes`, and `stat` share regular-file checks and then rely on the filesystem backend's read authority. `list` shares path inspection but also checks workspace containment, while `changes` filters observations to the workspace root. The service applies the following checks:
1. **The path itself.** `lstat` inspects the path before anything follows it: a missing path is `not-found`, and a symlink — wherever it points, including back inside the workspace — is `not-regular-file` (kind `symlink`) for the file methods and `not-directory` for `list`. An empty path is a `gateway/bad-request`.
1. **The path itself.** `lstat` inspects the path before anything follows it: a missing path is `not-found`, and a symlink — wherever it points, including back inside the workspace — is `not-regular-file` (kind `symlink`) for the file methods. `list` instead follows a final link and judges the resolved target: the target must stat as a directory, and a link to a file or a link whose target is gone is `not-directory` (kind `symlink`). An empty path is a `gateway/bad-request`.
2. **Workspace containment for `list`.** The directory resolves to a target and `ctx.fs.contains(root, target)` decides, where `root` is the `WorkspaceFileScope.workspaceRoot` resolved from the selected Session header. A `..` traversal or an absolute directory outside the root is `outside-workspace`. `changes` applies the same backend containment predicate to observed targets.
3. **The caps.** A page or window above `maxBytes`, or a `read` asking for more than `maxLines`, is refused, never shortened, because a silently cut page reads as the whole page; a listing above `maxEntries` is cut and says so. Complete and related-file reads are refused above `maxFileBytes`.
4. **Text.** For `read` only: content that is not UTF-8 up to the end of the page, a NUL byte in the backend's 8 KiB opening sample, or a NUL byte anywhere in the page is `not-text`; bytes past the page are not inspected.
After path inspection the file methods `stat` the resolved target once more, because the file may have gone or changed kind before the read: a vanished file is `not-found` and a replaced one `not-regular-file` with the new kind. For `list`, an outside entry whose type already disqualifies it reports its kind before its position.
After path inspection the file methods `stat` the resolved target once more, because the file may have gone or changed kind before the read: a vanished file is `not-found` and a replaced one `not-regular-file` with the new kind. For `list`, an outside entry whose type already disqualifies it reports its kind before its position; a final link resolves and passes containment before its target kind is read.
### Failures
@@ -140,7 +140,7 @@ The [resource model](2026-09-05-client-resource-model.md) owns `ctx.resources`,
## Testing
Host specs in `packages/api/workspace-files/tests` exercise header-only scope resolution for live and cold subagent Sessions, the deployment fallback, missing identities, and lookup disposal; the paged read (whole file, nested path, empty file, multi-byte UTF-8, the line window's edges, defaults and refused limits, carriage returns kept); the byte window (defaults, a middle window with more following, tail windows exact and short, past-end and empty files, NUL and invalid UTF-8 round-tripping through `Uint8Array`, version parity with `stat`, the cap as `too-large`, bad ranges, a window of a file far above the cap, and `eof` inferred without a size); `stat`; outside-workspace reads and backend refusals; `list` with containment, truncation, symlink children, and `not-directory`; and the `changes` stream driven by `fs/observed` and filtered by root. Client specs cover the provider's frames, the change feed, unsupported addresses, and registration and disposal. `fs/fs` and `fs-local` specs pin `readByteRange`; `dsh-util-workspace-path` specs pin the file-address grammar. The connection fixture serves `stat`, paged `read`, `list`, and an opt-in `changes` frame for the web e2e suite.
Host specs in `packages/api/workspace-files/tests` exercise header-only scope resolution for live and cold subagent Sessions, the deployment fallback, missing identities, and lookup disposal; the paged read (whole file, nested path, empty file, multi-byte UTF-8, the line window's edges, defaults and refused limits, carriage returns kept); the byte window (defaults, a middle window with more following, tail windows exact and short, past-end and empty files, NUL and invalid UTF-8 round-tripping through `Uint8Array`, version parity with `stat`, the cap as `too-large`, bad ranges, a window of a file far above the cap, and `eof` inferred without a size); `stat`; outside-workspace reads and backend refusals; `list` with containment, truncation, directory links, links whose target is gone, and `not-directory`; and the `changes` stream driven by `fs/observed` and filtered by root. Client specs cover the provider's frames, the change feed, unsupported addresses, and registration and disposal. `fs/fs` and `fs-local` specs pin `readByteRange`; `dsh-util-workspace-path` specs pin the file-address grammar. The connection fixture serves `stat`, paged `read`, `list`, and an opt-in `changes` frame for the web e2e suite.
## Deferred
@@ -41,7 +41,7 @@ Web 客户端需要从一个未必在 Host 机器上的浏览器查看会话工
- **`read`** 返回一个行窗口 `WorkspaceFileText = WorkspaceFileStat & { offset, text, lines, eof }`;`lines` 计页内行数,使只含一个空行的页(`text: ''`、`lines: 1`)与越过文件末尾的页(`lines: 0`)可区分。`range.offset` 是 1 起算的首行,缺省 1;`range.limit` 是最多行数,缺省 `maxLines` 且不得超过。行以 `\n` 结束,末尾的 `\n` 终止最后一行而不是开启一空行;`text` 以 `\n` 连接本页各行且不带终止符;页含最后一行时 `eof` 为 true,越过末尾的 offset 返回 `eof` 为 true 的空页。切页器沿 `streamText` 前进,数过窗口前的行而不保留,把每个窗内片段先按 `maxBytes` 核准再缓冲,并在越过窗口的第一个字符处返回,因此任意大小的文件只花一页内存。页上的 `version` 与 `bytes` 来自流之前的那次 stat。
- **`readBytes`** 返回原生字节、文件元数据、偏移量和 EOF。必填选项对象未传 `range` 时,在 `maxFileBytes` 上限内完整读取;传入时,在 `maxBytes` 上限内读取有界窗口。`range: {}` 使用零偏移和配置的窗口上限;窗口可读取超过完整文件上限的文件。[二进制传输决议](2026-09-17-workspace-file-binary-transfer.zh.md)负责 Remote 响应编码。
- **`baseFile`** 独立于 `range` 选择目标解析方式:目标 `path` 必须为相对路径,从基准文件所在目录解析。Host 对两个文件执行普通文件检查,并保留后端读取权限。[Document Preview](2026-09-08-document-preview-operations.zh.md)负责内容加载与地址语义。
- **`list`** 返回 `WorkspaceDirectoryListing { path, entries, truncated }`:被列目录相对根的工作区路径(根为空串)、其直接子项按后端的稳定名序以 `{ name, type, size? }` 给出,以及 `maxEntries` 是否截断了列表。`type` 为 `file`、`directory` 或 `other`;符号链接子项报告其指向目标的类型,悬空者为 `other`,而打开这样的子项仍会在下文的链接关被拒。dotfile 照常列出,不做任何过滤。
- **`list`** 返回 `WorkspaceDirectoryListing { path, entries, truncated }`:被列目录相对根的工作区路径(根为空串)、其直接子项按后端的稳定名序以 `{ name, type, size? }` 给出,以及 `maxEntries` 是否截断了列表。`type` 为 `file`、`directory` 或 `other`;符号链接子项报告其指向目标的类型,悬空者为 `other`。列表报告为目录的子项经其解析目标打开,Windows 目录联接亦然,此时列表以该目标命名;[目录链接列举](../bug-fix/2026-09-18-windows-directory-junction-listing.zh.md)拥有该规则。dotfile 照常列出,不做任何过滤。
- **`changes`** 产出 `WorkspaceFileWatchFrame`:在观察队列注册且工作区根解析完成后先发 `{ kind: 'ready' }`,随后为 `{ kind: 'change', change }`。载荷 `WorkspaceFileChange` 对存在的文件为 `{ absolutePath, version }`,对消失的文件为 `{ absolutePath, absent: true }`。来源是工作区根内的 `fs/observed`,不监视操作系统。首次拉取后的观察都会排队,包括根解析期间的观察;取消或插件释放会结束该代流。
### 线路上的路径
@@ -54,12 +54,12 @@ Web 客户端需要从一个未必在 Host 机器上的浏览器查看会话工
`read`、`readBytes` 与 `stat` 共享普通文件检查,之后依赖文件系统后端的读取权限。`list` 共享路径检查,但还会检查工作区包含关系;`changes` 则把观察过滤到工作区根内。服务执行以下检查:
1. **路径本身。** `lstat` 在跟随任何东西之前检查路径:缺失路径为 `not-found`;符号链接——不论指向哪里,包括指回工作区内——对文件方法为 `not-regular-file`(kind 为 `symlink`),对 `list` 为 `not-directory`。空路径是 `gateway/bad-request`。
1. **路径本身。** `lstat` 在跟随任何东西之前检查路径:缺失路径为 `not-found`;符号链接——不论指向哪里,包括指回工作区内——对文件方法为 `not-regular-file`(kind 为 `symlink`)。`list` 改为跟随末端链接并判定解析后的目标:目标经 stat 必须为目录,指向文件的链接或目标已消失的链接为 `not-directory`(kind 为 `symlink`)。空路径是 `gateway/bad-request`。
2. **`list` 的工作区包含。** 目录解析为目标,由 `ctx.fs.contains(root, target)` 判定,其中 `root` 是从所选 Session header 解析出的 `WorkspaceFileScope.workspaceRoot`。`..` 爬出或根外绝对目录为 `outside-workspace`。`changes` 对观察到的目标使用相同的后端包含判定。
3. **上限。** 超过 `maxBytes` 的页或窗口,或 `read` 索要超过 `maxLines` 的行数,一律拒绝、绝不截短,因为悄悄截短的页读起来就像整页;超过 `maxEntries` 的列表被截断并如实报告。全文及关联文件读取超过 `maxFileBytes` 时被拒绝。
4. **文本。** 仅限 `read`:到页末为止不是 UTF-8 的内容、后端 8 KiB 开头样本里的 NUL 字节,或页内任何位置的 NUL 字节,都是 `not-text`;页之后的字节不检查。
路径检查之后,文件方法再对解析出的目标 `stat` 一次,因为文件可能在读取前已消失或换了种类:消失者为 `not-found`,被替换者为带新种类的 `not-regular-file`。对 `list` 而言,根外条目若类型本身已不合格,会先报告其种类而不是位置。
路径检查之后,文件方法再对解析出的目标 `stat` 一次,因为文件可能在读取前已消失或换了种类:消失者为 `not-found`,被替换者为带新种类的 `not-regular-file`。对 `list` 而言,根外条目若类型本身已不合格,会先报告其种类而不是位置;末端链接先解析并通过包含检查,再读取其目标种类。
### 失败
@@ -140,7 +140,7 @@ Client 导出向 `ctx.resources` 注册一个 `ResourceProvider<'file'>`,存
## Testing
`packages/api/workspace-files/tests` 中的 Host spec 覆盖 live 与 cold subagent Session 的 header-only scope 解析、部署 fallback、缺失身份与 lookup 释放;分页读取(整文件、嵌套路径、空文件、多字节 UTF-8、行窗口边界、缺省与拒绝的 limit、保留回车);字节窗口(缺省、中段与尾窗、越界与空文件、`Uint8Array` 往返、版本、上限、坏范围以及无大小时的 `eof`);`stat`;工作区外读取及后端拒绝;`list` 的包含、截断、符号链接与 `not-directory`;以及由 `fs/observed` 驱动并按根过滤的 `changes`。Client spec 覆盖提供者帧、变更流、不支持地址及注册与释放。`fs/fs` 与 `fs-local` spec 钉住 `readByteRange`;`dsh-util-workspace-path` spec 钉住文件地址语法。connection fixture 为 web e2e 套件提供 `stat`、分页 `read`、`list` 与一帧可选启用的 `changes`。
`packages/api/workspace-files/tests` 中的 Host spec 覆盖 live 与 cold subagent Session 的 header-only scope 解析、部署 fallback、缺失身份与 lookup 释放;分页读取(整文件、嵌套路径、空文件、多字节 UTF-8、行窗口边界、缺省与拒绝的 limit、保留回车);字节窗口(缺省、中段与尾窗、越界与空文件、`Uint8Array` 往返、版本、上限、坏范围以及无大小时的 `eof`);`stat`;工作区外读取及后端拒绝;`list` 的包含、截断、目录链接、目标已消失的链接与 `not-directory`;以及由 `fs/observed` 驱动并按根过滤的 `changes`。Client spec 覆盖提供者帧、变更流、不支持地址及注册与释放。`fs/fs` 与 `fs-local` spec 钉住 `readByteRange`;`dsh-util-workspace-path` spec 钉住文件地址语法。connection fixture 为 web e2e 套件提供 `stat`、分页 `read`、`list` 与一帧可选启用的 `changes`。
## Deferred
@@ -15,5 +15,5 @@
en: 4cc4b61d76f84ff3
zh: bdd29ea66037f744
/agent-note-default-deepseek-session-log-upload/consequences:
en: 1302d84a3d2b168b
zh: aec67b793539f898
en: 31d0c8deb33dbe3f
zh: 8af3905630c629a0
@@ -23,6 +23,6 @@ This supersedes only the opt-in default in the [request-extension decision](2026
## Consequences
Eligible requests send the complete unaccepted canonical log suffix, including message text, tool arguments and results, workspace paths, and feedback, to the resolved DeepSeek endpoint or configured gateway. No prompt tokens or model-visible content are added. Request bodies can grow substantially, and provider rejection still fails the request. OTel remains independent; disabling OTel does not disable this contribution.
Eligible requests send the unaccepted canonical log suffix, up to `maxBytes` per request under the [bounded-upload decision](2026-09-24-bounded-session-log-upload.md), including message text, tool arguments and results, workspace paths, and feedback, to the resolved DeepSeek endpoint or configured gateway. No prompt tokens or model-visible content are added. Request bodies grow by up to that limit, and provider rejection still fails the request. OTel remains independent; disabling OTel does not disable this contribution.
Configuration tests cover default-on and explicit overrides with and without test environment markers. Both DeepSeek protocol Loader cases observe the default request field and recorded acceptance watermark, and explicit-off cases observe their absence. Existing headless, ACP, Web, and SDK recordings validate their declared upload policies without rewriting committed Session generations.
@@ -23,6 +23,6 @@ Status: implemented
## 后果
符合条件的请求会向解析后的 DeepSeek 端点或已配置网关发送完整的未接受规范日志后缀,包括消息文本、工具参数与结果、工作区路径和反馈。不增加提示词 token 或模型可见内容。请求正文可能显著增大,提供方拒绝仍会使请求失败。OTel 保持独立,关闭 OTel 不会关闭此贡献。
符合条件的请求会向解析后的 DeepSeek 端点或已配置网关发送未接受的规范日志后缀,按[有上限上传决策](2026-09-24-bounded-session-log-upload.zh.md)每次最多 `maxBytes`,包括消息文本、工具参数与结果、工作区路径和反馈。不增加提示词 token 或模型可见内容。请求正文最多增加该上限,提供方拒绝仍会使请求失败。OTel 保持独立,关闭 OTel 不会关闭此贡献。
配置测试覆盖有无测试环境标记时的默认开启与显式覆盖。两种 DeepSeek 协议的 Loader 用例观察默认请求字段和已记录的接受水位,显式关闭用例观察两者均不存在。现有 headless、ACP、Web 和 SDK 录制验证各自声明的上传策略,无需重写已提交的会话代际。
@@ -33,8 +33,8 @@
en: c46c9ca54ea9eaad
zh: 918dc8eebff5edf3
/agent-note-client-session-references-reference-sources-and-ui-status/decision/presets-and-creation-flows:
en: 790eda215b64ee37
zh: a4b5d3a61cc80cbc
en: 70ef26a3cd399b14
zh: 5c81515ff896f91a
/agent-note-client-session-references-reference-sources-and-ui-status/decision/host-event-context-ownership-and-typert:
en: eb3be89cf9431c44
zh: bfc26899286b37ad
@@ -166,7 +166,7 @@ Workspace connection preserves its navigation guards and panel-switch invalidati
Preset directories and deployment defaults may be shared. A bound Session's preset is read or changed through its Provider binding; preset controllers are cached by `SessionBinding` rather than managed by one root current-Session follower. The hero preset seat uses the `session-maybe` Provider: it shows the creation-flow choice without a Session and operates on the exact bound blank Session after one arrives. Header labels read the same Provider-bound Session projection.
A preset chosen before Session creation remains in the main Conversation's `session-maybe` preset surface. After Workspace creation or reuse establishes the main Provider over a blank Session, that surface applies the choice to its Provider-bound Session. When Settings changes the default preset or picker setting, the preset service selects the blank Session whose established Provider binding carries `mainView` ownership and updates that Session. Non-blank main Sessions, independent Sidebar Providers, and other background references remain unchanged; preset subtrees do not read the main reference or use a global current follower to find their target.
A preset chosen before Session creation remains in the main Conversation's `session-maybe` preset surface. After Workspace creation or reuse establishes the main Provider over a blank Session, that surface applies the choice to its Provider-bound Session. When Settings changes the default preset, the preset service selects the blank Session whose established Provider binding carries `mainView` ownership and updates that Session. Non-blank main Sessions, independent Sidebar Providers, and other background references remain unchanged; preset subtrees do not read the main reference or use a global current follower to find their target.
### Host-event Context ownership and Typert
@@ -166,7 +166,7 @@ Workspace 连接保持既有导航检查与面板切换失效规则。侧边栏
预设目录与部署默认值可以共享。已绑定 Session 的预设通过 Provider 的 binding 读取或修改;预设控制器按 `SessionBinding` 缓存,不由根级 current-Session 跟随器管理。Hero 的 preset seat 使用 `session-maybe` Provider:没有 Session 时显示创建流程选择,绑定空白 Session 后操作该确切 Session。标题标签读取同一 Provider 绑定 Session 的投影。
Session 创建前选择的 preset 保留在主 Conversation 的 `session-maybe` preset surface 中。Workspace 创建或复用空白 Session 并建立主 Provider 后,该 surface 将选择应用到 Provider 绑定的 Session。设置页修改默认 preset 或 picker 设置时,preset 服务从 Provider 已建立的 binding 缓存中选择带 `mainView` 所有权标记的空白 Session,并更新该 Session。非空白主 Session、Sidebar 的独立 Provider 与其他后台引用均不受该设置动作影响;preset 子树不读取主引用,也不通过全局 current follower 寻找目标。
Session 创建前选择的 preset 保留在主 Conversation 的 `session-maybe` preset surface 中。Workspace 创建或复用空白 Session 并建立主 Provider 后,该 surface 将选择应用到 Provider 绑定的 Session。设置页修改默认 preset 时,preset 服务从 Provider 已建立的 binding 缓存中选择带 `mainView` 所有权标记的空白 Session,并更新该 Session。非空白主 Session、Sidebar 的独立 Provider 与其他后台引用均不受该设置动作影响;preset 子树不读取主引用,也不通过全局 current follower 寻找目标。
### Host 事件 Context 所有权与 Typert
@@ -9,8 +9,8 @@
en: 725f773bb350218e
zh: dd8fbc09ef48208d
/agent-note-declarative-agent-presets-and-retained-revisions/decision:
en: fa2ac428de2ba3d0
zh: c75da98e1cf076ef
en: 2a2489303fc76fc0
zh: 1a760e9a5edfccc8
/agent-note-declarative-agent-presets-and-retained-revisions/alternatives-considered:
en: 09f40eab4659e9f6
zh: 7a70da0d0ae6845b
@@ -16,7 +16,7 @@ The registry owns each revision's scope and Loader tree. Agents link their own s
The Web editor and `agent_preset` tool accept only child plugin YAML. Saving writes the active profile's user patch, preserving display metadata and unrelated configuration. Revision checks and profile locking prevent overwriting concurrent saves; validation rejects a higher-priority override that would defeat the edit. Persistence and activation outcomes are reported separately. Tool writes require approval or full access because configuration executes in the Host.
Session data continues to record the preset identity. Restart resolves that identity against current configuration and rejects a missing definition. Old executable revisions are process-local, not serialized. The existing chooser policy and blank-session selection remain: hiding the chooser uses the deployment default without deleting the saved user preference.
Session data continues to record the preset identity. Restart resolves that identity against current configuration and rejects a missing definition. Old executable revisions are process-local, not serialized. The saved user default and blank-session selection remain; `selectedDefault` is what an unnamed new session resolves, and the Client's Developer tools preference decides only whether the choice is shown.
## Alternatives considered
@@ -16,7 +16,7 @@ Preset 目录重复承担 Cordis 配置的所有权。独立的发现、元数
Web 编辑器与 `agent_preset` 工具只接受子插件 YAML。保存写入当前 profile 的用户补丁,保留显示元数据和无关配置。版本检查和 profile 锁防止覆盖并发保存;验证拒绝会使编辑失效的更高优先级覆盖。持久化与激活结果分别报告。配置在 Host 中执行,因此工具写入需要批准或完全访问权限。
会话数据继续记录 preset 标识。重启时根据当前配置解析该标识,定义缺失则拒绝恢复。旧的可执行代际仅保留在进程内,不会序列化。现有选择器策略和空白会话选择保持不变:隐藏选择器使用部署默认值,但不删除保存的用户偏好。
会话数据继续记录 preset 标识。重启时根据当前配置解析该标识,定义缺失则拒绝恢复。旧的可执行代际仅保留在进程内,不会序列化。保存的用户默认值与空白会话选择保持不变:未指定 preset 的新会话解析 `selectedDefault`,客户端的开发者工具偏好只决定是否展示这项选择。
## Alternatives considered
@@ -0,0 +1,43 @@
# Bilingual-pair consistency record for 2026-09-19-timed-user-question-two-settlements.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-19-timed-user-question-two-settlements.md
/agent-note-timed-user-questions-as-two-settlements:
en: 5b6fa73158a11213
zh: b55c32342d6e6692
/agent-note-timed-user-questions-as-two-settlements/problem:
en: f665127a9b5b8225
zh: b19845f62310f3a2
/agent-note-timed-user-questions-as-two-settlements/decision:
en: ce869bf2eed04e81
zh: 5b1fcf44c44c6137
/agent-note-timed-user-questions-as-two-settlements/decision/modes-and-scope:
en: d23e622ee4643b48
zh: 3732c10c12344755
/agent-note-timed-user-questions-as-two-settlements/decision/ownership:
en: 0543678ad9d26fc5
zh: aa6ba207e661c567
/agent-note-timed-user-questions-as-two-settlements/decision/foreground-wait-and-claim-lifetime:
en: 447713bbdf1ab4d8
zh: 2757678ae47822aa
/agent-note-timed-user-questions-as-two-settlements/decision/durable-state-and-late-answers:
en: b8729d06ab7814ed
zh: acaa8f4174701248
/agent-note-timed-user-questions-as-two-settlements/decision/card-state-and-delivery-races:
en: 8c55b693e7a5bca4
zh: aea6e966ce3667e3
/agent-note-timed-user-questions-as-two-settlements/decision/local-countdown-and-panel-actions:
en: 4c0c9ec59fc51217
zh: 322870eeb49176c7
/agent-note-timed-user-questions-as-two-settlements/decision/reply-node-and-grouping:
en: 10323b0e0340a40b
zh: a17fc3a4d4d820a7
/agent-note-timed-user-questions-as-two-settlements/alternatives-considered:
en: ce8ff9d5bc4b85b7
zh: 49024fe3a5b194b2
/agent-note-timed-user-questions-as-two-settlements/verification:
en: bd768c23270702f0
zh: f18615e7b6ea3631
/agent-note-timed-user-questions-as-two-settlements/consequences:
en: 12027f56af79db3c
zh: 0a74c0aeaad2aad9
@@ -0,0 +1,115 @@
# Agent Note: Timed user questions as two settlements
Status: implemented
English | [中文](2026-09-19-timed-user-question-two-settlements.zh.md)
## Problem
A question can block an agent even when useful work does not depend on the answer. Releasing the tool call after a wait must not discard the question: the user may answer after the agent has continued or after reopening the Session. A pending result is neither a skipped answer nor permission to proceed with work that requires approval.
The foreground tool result and the eventual answer have different lifetimes. A `user-questions/request` waterfall settles once; keeping it alive after returning pending creates a second answer path that outlives its caller. Countdown, focus, and editing also have different owners from durable question state.
## Decision
Foreground answers use the existing Remote Event waterfall. A timeout ends that request and returns pending; a later answer uses a business RPC that steers a durable user message. The claim stream controls the foreground wait only. It neither transports answers nor replaces the Remote Event.
### Modes and scope
- The default `mode: legacy` retains the blocking schema and `ask()` path. Explicit `mode: timed` adds `timeout`, using the configured default of 120 seconds when omitted.
- A positive integer timeout selects `askTimed()`; `-1` selects blocking `ask()`. A blocking call made with the timed schema still participates in timed-question projection and review; it is not identical to a legacy call.
- Plan review retains `ask()`, its `plan-review` intent, and `BAD_INTENT` validation. This decision does not move plan review to another package.
- Human interaction requires the exact live runtime root when an Agent is supplied. A stale instance fails with `CALLER_NOT_LIVE`; an owned child fails with `DELEGATED_CALLER`. Durable Session lineage does not prevent a resumed runtime root from asking.
### Ownership
| Owner | Responsibility |
|---|---|
| `tool-ask-user` | Model schema, timeout validation, service selection, and pending-result text. |
| `UserQuestionService` | Foreground waits and Client claims, unattended deadlines, late-answer validation and steering, and the durable question projection. |
| Gateway and Remote runtime | Existing request delivery, settlement, cancellation, and Remote stream transport; no question-specific timer or event-name branch. |
| `ui-user-questions` | One card per Session and call, answer-channel selection, local countdown and drafts, and the reply Definition and renderer. |
| `ui-chat` | Message-id-based presentation aggregation and ordinary process/Turn folding. |
| Question-specific tool row | Pending/answered presentation and panel actions, composed in `QuestionToolRow` without adding `rowAction` to generic `ToolRow`. |
The [question service](../../../../packages/interaction/user-questions/README.md), [tool](../../../../packages/interaction/tool-ask-user/README.md), and [questions UI](../../../../packages/client/ui-user-questions/README.md) own their package contracts.
### Foreground wait and claim lifetime
`TimedQuestionWait` owns a private cancellation signal, the original Host deadline, and Client claims. The Host runs a timer only while no answer UI holds a claim. Delivery to a transport queue is not a claim. Losing the last claim restores the original deadline, with immediate expiry if it has passed.
A timed request carries `wait: { callId, timed: true }`. Before attaching its answer channel, the Client opens `attachWait`, a business Remote stream that yields one Host-computed `remainingMs` frame and stays open for the claim's lifetime. Focus, edits, and heartbeats do not travel through this stream.
The Client retains its claim through delivery of the waterfall outcome to the Host. Returning an answer locally is not delivery: releasing the claim then could let an overdue Host timer beat that answer. The Host closes the claim when the foreground request settles. Explicit delegation releases the claim before `next()`; plugin teardown delegates and cancels its stream.
Client countdown expiry rejects the waterfall with `ASK_TIMED_OUT`. Unattended Host expiry aborts only the private wait signal with the same business error. `askTimed()` maps only that error to pending, maps parent cancellation to `ASK_ABORTED`, and propagates other failures. `NO_PROVIDER` waits on the same bounded wait lifetime, not a permanently unresolved promise. Foreground completion releases timers, claims, and the wait registry entry.
### Durable state and late answers
The `userQuestions` projection reconstructs question state from existing Session events. It recognises the timed schema by the `timeout` parameter recorded in `request/header`; legacy calls are not tracked. A valid `tool/call` opens a question. Its `tool/result` makes it continued for pending or `TOOL_OUTCOME_UNKNOWN`, settles it with a valid answer batch, or removes it for another result or failure.
A continued question accepts `answer(agent, callId, answer)`. The method returns `false` for an unknown or non-continued call and throws `BAD_ANSWER` unless the batch names each question exactly once. Accepted answers use `agent.steer(createUserMessage(...))`, source `{ kind: 'user-question-reply', callId, outcome: 'answered' }`, and JSON text containing `answer_to_pending_question`, the call id, original questions, and answers. Remote Agent resolution from the Session id resumes a root when needed; the `answer()` body does not own that resume operation.
The inbox message is the durable reply. `agent/inbox/spliced` settles the question when the message is queued; the later admitted `user/message` is idempotent for that settlement. Settled answer batches remain in the projection so the original tool row can show late answers even though its own result remains pending. Unusable recorded reply text settles with an empty batch.
No new Session event type or wait-state log is needed. The message-source extension requires the repository's persistence-type acknowledgement. `dismissed` remains readable for historical records but has no producer: hiding a panel does not manufacture a reply.
### Card state and delivery races
`QuestionCards` shares one `PendingQuestion` between the live request and the continued projection, keyed by Session and `callId`. A blocking request without a call id has its own per-request card. An open card submits through its waterfall; without that channel it disables submission. A continued card uses the late-answer RPC.
A waterfall submission has no delivery acknowledgement. The Gateway can discard an outcome that loses a settlement race. The card therefore retains its draft and stays busy until projection reconciliation. If the question becomes continued during submission, the controls re-arm with a resubmit hint; the user can submit the retained draft through RPC. This is not automatic retry or a guarantee that every local submission reaches the model.
A tool-call-keyed card is removed when its call is absent from the active projection and no waterfall remains. Removal closes the card and clears its draft. Neither a local submission nor a transport cancel frame alone decides that the question is answered. An unkeyed blocking card ends with its waterfall; closing that blocking panel rejects with `ASK_CANCELLED`.
### Local countdown and panel actions
The Client computes `Date.now() + remainingMs`. Manual focus on a pristine answer surface pauses its countdown, and blur resumes the preserved remainder. Focus before the claim handshake remains effective when the deadline arrives; blur before the handshake does not create a timer. First edit or Take time freezes the countdown, with that disposition saved beside the browser-local draft.
Autofocus applies only when the answer channel is ready, no countdown is active, and the card is not locked. Hiding a timed-call panel leaves its claim and countdown running; its tool row can reopen it. A settled call opens a read-only panel from recorded questions and answers, not another answer channel.
### Reply node and grouping
The questions UI identifies a late reply from `source.kind`, then parses its recorded JSON for question-and-answer presentation. Pasting identical JSON in an ordinary user message does not identify it as a reply. Unreadable recorded payloads retain their raw-text presentation.
The ordinary message Definition and the question-reply Definition both match the appended message and retain separate Node Store projections with the same message id. Chat business grouping omits the `turn-trigger` presentation when a `question-reply` with that id is present. The duplicate trigger neither renders nor splits a group; the reply renders once. The same aggregation applies to unscoped historical input.
The reply participates in ordinary process grouping and whole-Turn folding. It is not independently visible: reading it can require expanding both its Turn and process group. Turn disclosure stays before the group it controls, including a reply that opens the Turn.
This decision adds no process-role field, independent-layout flag, user-message structure change, or target-local fallback dispatch. The independent-kind rules stay in Chat's existing grouping logic. A node-data-owned declaration of grouping and overlap semantics is deferred infrastructure, not part of the question feature.
## Alternatives considered
**Unconditional Host deadline or Gateway-owned question timers.** An unconditional deadline overrides local editing and Take time. Putting the exception into pending Remote Events or branching on the question event in Remote dispatch makes generic transport own business policy. Claims keep that policy in the question service.
**Keep the waterfall alive after returning pending.** The request outlives the tool caller, and late waterfall answers compete with the RPC. Ending the foreground request gives each settlement one answer path.
**Delegate every timed request immediately.** Dispatching to an intentionally empty answerer set wastes the existing blocking-answer path and requires special handling of `NO_PROVIDER`. Delegation is for an unavailable answerer, not the timed mode itself.
**Persist a whole-value wait/focus/edit stream or use Host focus leases.** Durable question status is derivable from tool and inbox events. Focus and editing are local UI state; logging them or synchronising focus leases adds Host state without making multiple Clients share one editing session.
**Reclassify JSON inside generic `MessageItem`, borrow the relay form, or use the default context row.** Parsing text to identify the business message confuses its source with its content; the relay form describes another agent's message, and a generic context row exposes JSON. A dedicated source, Definition, and renderer own the question presentation.
**Independent reply layout through generic node metadata.** It broadens a question feature into a conversation-infrastructure change. Ordinary folding and Chat-owned aggregation are sufficient for this decision; independently declared node-data grouping semantics remain a separate design task.
**A log-only close event or a manufactured dismissal message on panel close.** A log-only close hides the user's decision from the model and adds another durable mechanism. Manufacturing a reply treats hiding the panel as an answer. Panel close therefore leaves a timed question answerable.
**A dedicated late-reply Session event beside the inbox message.** The inbox splice and admitted user message already record the reply. A third event duplicates the same fact and adds another persistence type.
## Verification
- [Service tests](../../../../packages/interaction/user-questions/tests/user-questions.spec.ts) and [wait tests](../../../../packages/interaction/user-questions/tests/timed-wait.spec.ts) cover claimed and unclaimed waits, last-claim disconnect, cancellation, timeout error mapping, continued-only answers, and exact answer-id validation.
- [Projection tests](../../../../packages/interaction/user-questions/tests/projection.spec.ts) distinguish legacy and timed schemas and cover pending, answered, other failures, resume repair, and duplicate inbox/message settlement.
- [Client listener tests](../../../../packages/client/ui-user-questions/tests/browser-plugin.client.spec.ts) and [composer tests](../../../../packages/client/ui-user-questions/tests/user-questions-composer.client.spec.tsx) cover claim retention through Host acceptance, focus before handshake, indefinite waits, draft retention, RPC resubmission, and panel actions.
- [Grouping tests](../../../../packages/client/ui-chat/tests/process-groups.client.spec.ts) and [browser replay](../../../../apps/web/tests/question-composer.e2e.ts) cover one rendered reply with both projections retained, ordinary collapsed placement, disclosure ordering, and the original tool row's read-only late-answer panel.
- Legacy round-trip, cancellation, plan-review intent, and Session-remount regressions remain covered by their owning tests. Live multi-Client delivery races and real Host restart remain integration coverage gaps beyond isolated lifecycle and projection tests.
## Consequences
The model can continue independent work while the question remains answerable. Business-owned timing and reply rendering reuse the existing transport and conversation APIs, at the cost of local rather than globally coordinated waiting and ordinary folded visibility.
- Take time affects only one Client. Another Client can answer or expire the shared waterfall; local focus does not establish a global hold.
- A terminal claim-stream failure can reject the foreground question rather than produce pending. No unattended-wait fallback is provided for that failure.
- Host and Client clocks need not align, but Client system-clock changes can move expiry and transport latency can delay the local countdown's start.
- The duplicate projections remain stored. Aggregation adds linear scans during structural grouping rebuilds; ordinary text-only streaming updates retain their incremental path. This is an algorithmic bound, not a measured latency claim.
- Unanswered continued questions can remain after reopening a Session. Panel close records nothing, and visibility follows normal folding rather than an always-visible reply guarantee.
@@ -0,0 +1,115 @@
# Agent Note: 计时用户提问的两次结算
Status: implemented
[English](2026-09-19-timed-user-question-two-settlements.md) | 中文
## Problem
即使还有不依赖回答的工作可做,问题仍可能阻塞 agent(智能体)。等待后放行工具调用不能丢弃问题:用户可能在 agent 继续执行后,或重新打开 Session 后才回答。pending 结果既不是跳过回答,也不是开展需要批准的工作的授权。
前台工具结果与最终回答的生命周期不同。`user-questions/request` waterfall(瀑布式事件)只结算一次;返回 pending 后仍保留它,会产生一条活过调用方的第二回答路径。倒计时、聚焦和编辑的归属也不同于持久问题状态。
## Decision
前台回答继续使用现有 Remote Event waterfall。超时结束该请求并返回 pending;迟到回答通过业务 RPC steer 一条持久用户消息。接手 stream 只控制前台等待,既不传递回答,也不替代 Remote Event。
### 模式与范围
- 默认 `mode: legacy` 保留阻塞式 schema 和 `ask()` 路径。显式 `mode: timed` 增加 `timeout`,省略时使用配置默认值 120 秒。
- 正整数超时选择 `askTimed()`;`-1` 选择阻塞式 `ask()`。timed schema 发出的阻塞调用仍参与计时问题投影和答案查看,并不等同于 legacy 调用。
- 计划评审保留 `ask()`、`plan-review` 意图及 `BAD_INTENT` 校验。本决策不把计划评审迁移到其他包。
- 提供 Agent 时,人机交互要求它是运行时中确切的存活根实例。陈旧实例以 `CALLER_NOT_LIVE` 失败;被其他 agent 拥有的子实例以 `DELEGATED_CALLER` 失败。持久 Session 的血缘关系不妨碍恢复后的运行时根实例提问。
### 职责归属
| 归属方 | 职责 |
|---|---|
| `tool-ask-user` | 模型 schema、超时校验、服务选择和 pending 结果文本。 |
| `UserQuestionService` | 前台等待与 Client 接手、无人接手时的 deadline、迟到回答校验与 steering(中途引导),以及持久问题投影。 |
| Gateway 与 Remote 运行时 | 现有请求投递、结算、取消及 Remote stream 传输;不包含问题专属计时器或按事件名分支。 |
| `ui-user-questions` | 每个 Session 和调用的一张卡片、回答通道选择、本地倒计时与草稿,以及回复 Definition 和渲染器。 |
| `ui-chat` | 基于消息 id 的呈现聚合,以及普通过程/轮次折叠。 |
| 问题专用工具行 | pending/已回答呈现与面板操作,在 `QuestionToolRow` 中组合,不向通用 `ToolRow` 添加 `rowAction`。 |
[问题服务](../../../../packages/interaction/user-questions/README.zh.md)、[工具](../../../../packages/interaction/tool-ask-user/README.zh.md)和[问题 UI](../../../../packages/client/ui-user-questions/README.zh.md)分别拥有各包的约定。
### 前台等待与接手生命周期
`TimedQuestionWait` 拥有专属取消 signal、原始 Host deadline 和 Client 接手记录。Host 只在没有回答 UI 接手时运行计时器。投递到传输队列不算接手。最后一个接手方离开后恢复原 deadline,已经过期则立即到期。
计时请求携带 `wait: { callId, timed: true }`。Client 在挂接回答通道前打开业务 Remote stream `attachWait`,接收一帧由 Host 计算的 `remainingMs`,并在接手期间保持 stream 打开。聚焦、编辑和心跳不经过该 stream。
Client 将接手保留到 waterfall 结果交付给 Host。本地返回回答不等于交付:此时释放接手会让已经到期的 Host 计时器抢在回答之前结算。Host 在前台请求结算时关闭接手。显式委托在 `next()` 前释放接手;插件拆卸会委托请求并取消自己的 stream。
Client 倒计时到期以 `ASK_TIMED_OUT` 拒绝 waterfall。无人接手时,Host 到期只以同一业务错误中止等待专属 signal。`askTimed()` 只把该错误映射为 pending,把父级取消映射为 `ASK_ABORTED`,其余失败继续传播。`NO_PROVIDER` 等待同一个有界等待生命周期,而不是永不结算的 Promise。前台完成时释放计时器、接手记录及等待注册项。
### 持久状态与迟到回答
`userQuestions` 投影从现有 Session 事件重建问题状态。它通过 `request/header` 记录的 `timeout` 参数识别 timed schema,不跟踪 legacy 调用。有效的 `tool/call` 打开问题。其 `tool/result` 在 pending 或 `TOOL_OUTCOME_UNKNOWN` 时将问题标为已继续,在有效回答批次时结算并保存答案,其他结果或失败则移除问题。
已继续的问题接受 `answer(agent, callId, answer)`。未知或非已继续调用返回 `false`;回答批次没有恰好包含每个问题一次时抛出 `BAD_ANSWER`。接受的回答通过 `agent.steer(createUserMessage(...))` 送入 agent,来源为 `{ kind: 'user-question-reply', callId, outcome: 'answered' }`,JSON 文本包含 `answer_to_pending_question`、调用 id、原问题及答案。Remote 根据 Session id 解析 Agent 时按需恢复根实例;恢复操作不属于 `answer()` 方法体。
inbox 消息就是持久回复。`agent/inbox/spliced` 在消息入队时结算问题;随后接纳的 `user/message` 对该结算是幂等的。已结算答案批次保留在投影中,让原工具行即使自身结果仍是 pending,也能显示迟到回答。无法使用的已记录回复文本以空答案批次结算。
无需新增 Session 事件类型或等待状态日志。消息来源扩展需要仓库的持久化类型变更确认。`dismissed` 为历史记录保留可读性,但没有生产者:隐藏面板不会伪造回复。
### 卡片状态与投递竞争
`QuestionCards` 按 Session 和 `callId`,让实时请求与已继续投影共享一个 `PendingQuestion`。没有调用 id 的阻塞请求拥有独立的逐请求卡片。开放卡片通过 waterfall 提交;缺少该通道时禁用提交。已继续卡片使用迟到回答 RPC。
waterfall 提交没有送达确认。Gateway 可能丢弃输掉结算竞争的结果,因此卡片保留草稿并保持忙碌,等待投影同步。若提交期间问题变为已继续,控件带重提提示重新可用,用户可以通过 RPC 提交保留的草稿。这不是自动重试,也不保证每次本地提交都能到达模型。
按工具调用标识的卡片在调用不属于活动投影且没有 waterfall 时移除。移除会关闭卡片并清理草稿。本地提交和传输取消帧都不能单独决定问题已经回答。无调用标识的阻塞卡片随 waterfall 结束;关闭这种阻塞面板以 `ASK_CANCELLED` 拒绝请求。
### 本地倒计时与面板操作
Client 计算 `Date.now() + remainingMs`。手动聚焦未编辑的回答区会暂停倒计时,失焦后从保留的剩余时间继续。接手握手前的聚焦在 deadline 到达时仍生效;握手前失焦不会制造计时器。首次编辑或「慢慢回答」冻结倒计时,该状态与浏览器本地草稿一起保存。
只有回答通道就绪、没有活动倒计时且卡片未锁定时才自动聚焦。隐藏计时调用的面板会保留接手和倒计时,工具行可以重新打开面板。已结算调用依据记录的问题与答案打开只读面板,不产生另一条回答通道。
### 回复节点与分组
问题 UI 通过 `source.kind` 识别迟到回复,再解析已记录的 JSON 来呈现问答。普通用户消息粘贴相同 JSON 不会被识别为回复。无法读取的已记录载荷保留原始文本呈现。
普通消息 Definition 和问答回复 Definition 都匹配追加的消息,在 Node Store 中保留消息 id 相同的独立投影。存在同 id 的 `question-reply` 时,Chat 业务分组省略 `turn-trigger` 呈现。重复触发节点既不渲染,也不切开分组,回复只显示一次。相同聚合也适用于无轮次归属的历史输入。
回复参与普通过程分组和整轮折叠,不保证独立可见:阅读时可能需要展开所属轮次与过程分组。轮次折叠控件始终在所控制的分组之前,包括由回复开启轮次的情况。
本决策不添加 process-role 字段、独立布局标记、用户消息结构变更或目标内 fallback 派发。独立 kind 规则保留在 Chat 现有分组逻辑中。由节点 data 声明分组与重叠语义的机制是留待拆分的基础设施,不属于问题功能。
## Alternatives considered
**无条件 Host deadline 或 Gateway 拥有问题计时器。** 无条件 deadline 会覆盖本地编辑和「慢慢回答」。把例外放入 pending Remote Events,或在 Remote 派发中按问题事件分支,会让通用传输拥有业务策略。接手机制将策略留在问题服务中。
**返回 pending 后继续保留 waterfall。** 请求活过工具调用方,迟到的 waterfall 回答与 RPC 竞争。结束前台请求让每次结算各有一条回答路径。
**立即委托所有计时请求。** 向故意留空的回答方集合派发请求,浪费现有阻塞回答路径,还要求特殊处理 `NO_PROVIDER`。委托表示回答方不可用,不代表计时模式本身。
**持久化整值等待/聚焦/编辑流,或使用 Host 聚焦租约。** 持久问题状态可从工具与 inbox 事件推导。聚焦和编辑属于本地 UI 状态;记录它们或同步聚焦租约会增加 Host 状态,却不能让多个 Client 共享同一编辑会话。
**在通用 `MessageItem` 内重新分类 JSON、借用 relay form,或使用默认 context 行。** 解析文本来识别业务消息混淆了来源与内容;relay form 表示其他 agent 的消息,通用 context 行则会暴露 JSON。专用来源、Definition 和渲染器拥有问题呈现。
**通过通用节点元数据让回复独立布局。** 这会把问题功能扩大为对话基础设施变更。普通折叠与 Chat 内聚合足以满足本决策;由节点 data 独立声明分组语义仍是单独的设计任务。
**面板关闭时写仅入日志的关闭事件,或伪造放弃消息。** 仅入日志会让模型看不到用户决定,还增加另一套持久机制。伪造回复则把隐藏面板当成回答。因此关闭面板仍让计时问题保持可回答。
**在 inbox 消息之外增加专用迟到回复 Session 事件。** inbox 插入与接纳的用户消息已经记录回复。第三个事件重复同一事实,并增加另一种持久化类型。
## 验证
- [服务测试](../../../../packages/interaction/user-questions/tests/user-questions.spec.ts)与[等待测试](../../../../packages/interaction/user-questions/tests/timed-wait.spec.ts)覆盖有人与无人接手、最后接手方断开、取消、超时错误映射、仅接受已继续问题的回答,以及答案 id 的精确校验。
- [投影测试](../../../../packages/interaction/user-questions/tests/projection.spec.ts)区分 legacy 与 timed schema,并覆盖 pending、已回答、其他失败、恢复修复及 inbox/消息重复结算。
- [Client 监听器测试](../../../../packages/client/ui-user-questions/tests/browser-plugin.client.spec.ts)与[composer 测试](../../../../packages/client/ui-user-questions/tests/user-questions-composer.client.spec.tsx)覆盖接手保留到 Host 接受结果、握手前聚焦、无限期等待、草稿保留、RPC 重提及面板操作。
- [分组测试](../../../../packages/client/ui-chat/tests/process-groups.client.spec.ts)与[浏览器回放](../../../../apps/web/tests/question-composer.e2e.ts)覆盖保留两个投影但只渲染一次回复、普通折叠位置、折叠控件顺序,以及原工具行的迟到答案只读面板。
- legacy 问答往返、取消、计划评审意图及 Session 重新挂载的回归仍由各自测试覆盖。真实多 Client 投递竞争与真实 Host 重启仍是独立生命周期和投影测试之外的集成覆盖缺口。
## Consequences
模型可以继续独立工作,问题同时保持可回答。业务层拥有的计时与回复渲染复用现有传输和对话 API,代价是等待由本地而非全局协调,可见性遵循普通折叠。
- 「慢慢回答」只影响一个 Client。其他 Client 可以回答或让共享 waterfall 到期,本地聚焦不构成全局暂停。
- 接手 stream 的终止性失败可能让前台问题失败,而不是产生 pending。不为该失败提供无人接手等待的 fallback。
- Host 与 Client 时钟不必对齐,但 Client 系统时钟变化可能移动到期时间,传输延迟可能推迟本地倒计时的开始。
- 重复投影仍保留在存储中。聚合在结构性分组重建时增加线性扫描;普通纯文本 stream 更新保留增量路径。这是算法复杂度界限,不是实测延迟结论。
- 无人回答的已继续问题可以在重新打开 Session 后继续存在。关闭面板不写记录,可见性遵循普通折叠,而不是保证回复始终可见。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-09-24-bounded-session-log-upload.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-24-bounded-session-log-upload.md
/agent-note-bounded-deepseek-session-log-upload:
en: d35839d24f5704a7
zh: 8bc7e460fd3bd936
/agent-note-bounded-deepseek-session-log-upload/problem:
en: b6b54d7ee8810f7e
zh: b0fdfa6c2c3cc224
/agent-note-bounded-deepseek-session-log-upload/decision:
en: d2fe09351a82f7d6
zh: ad0741f63e14a8ad
/agent-note-bounded-deepseek-session-log-upload/alternatives-considered:
en: 88985b4e1f5c7a4f
zh: 32617f7ff75ee825
/agent-note-bounded-deepseek-session-log-upload/consequences:
en: e19fb6136ffd2250
zh: 01dacd7a44c3f837
@@ -0,0 +1,30 @@
# Agent Note: Bounded DeepSeek Session-log upload
Status: implemented
English | [中文](2026-09-24-bounded-session-log-upload.zh.md)
## Problem
Without a watermark of the current Session format generation, a request carries the whole canonical log: the first request after upload becomes enabled, the first request after a format migration, and the first request of a fork all do. A long log can exceed the V8 string limit of 2^29 − 24 UTF-16 code units on 64-bit hosts. The adapter's merged `JSON.stringify` then throws `RangeError`, the adapter reports `TRANSPORT`, the default retry policy repeats the same serialization, and no request reaches the 2xx that would advance the watermark, so every model request in that Session fails. Below the limit, one request can still carry hundreds of MiB: on Node v24.17.0 and macOS arm64, a 269 MiB first upload of a 96,341-event Session waited 34 s for its 2xx.
## Decision
`session-log-deepseek.Config.maxBytes`, 8 MiB by default, bounds the complete serialized `dsh_session_log` field in UTF-8 bytes. Each request carries the longest contiguous run after the watermark whose field, including the header and numeric envelope fields, fits the limit. Acceptance advances the watermark to that run's `throughSeq`, and later requests continue from there. When the first pending event alone exceeds the limit, the request omits the field, the plugin logs a warning, and the watermark stays before that event until `maxBytes` admits it. An event whose JSON text exceeds the runtime's string limit counts as exceeding every limit, so it blocks upload without failing the request, and the plugin serializes it once per request.
The DeepSeek adapter treats a merged request body that fails to serialize as an extension failure rather than a transport failure. It sends the base body without any extension field, skips the joint acceptance transaction, and reports the omitted field names through the provider plugin's logger, so contributors resend unaccepted state on a later request. A base body that cannot serialize still fails the request.
This supersedes complete-suffix delivery and fail-closed serialization in the [request-extension decision](2026-08-21-deepseek-llm-api-request-extensions.md). That note still owns field ownership, cancellation, acceptance, and the remaining fail-closed failures.
## Alternatives considered
**Serialize the whole pending run first and bound it only on overflow.** Measuring bytes requires serializing every candidate event either way. In a synthetic benchmark, a 14 KiB step took 14.3 µs whole versus 13.8 µs per event, while a 135 MiB backlog took 182 ms to serialize before any fallback versus 10 ms to fill 8 MiB. The whole-run attempt also still overflows the string limit for the logs that motivated this change.
**Send an oversized first event alone.** Exceeding the configured limit reintroduces the provider body-size rejections that still fail the model request. Omission keeps every request bounded, and the warning names the blocked event.
**Omit only the field that fails to serialize.** Acceptance is one joint transaction across fields, so per-field omission would split the registry's acceptance contract. Dropping every extension field on this path only leaves the small `dsh_plugin_packages` inventory out of one request.
## Consequences
A backlog drains at up to 8 MiB per accepted request by default. At the end-to-end rate measured for the 269 MiB upload, about 8 MiB/s, an 8 MiB field adds about 1 s to each request while a backlog drains, and a 0.86 GB backlog drains in about 100 requests. In the same 96,341-event Session, the median per-step append is 11 KiB and the largest event is 414 KiB, so ordinary steps stay far below the limit and only an unusual event or a small configured limit reaches the oversized-event path. While a backlog drains, the endpoint receives a `throughSeq` behind the Session's current tip. Preparation failures, acceptance failures, and provider rejections still fail the model request.
Plugin tests cover the exact byte boundary, UTF-8 counting, backlog draining across accepted requests, the oversized-event warning with its unchanged watermark, an event past the string limit, and the 8 MiB default through plugin configuration. Adapter tests cover the base-body fallback through the provider plugin's logger and through an adapter without a reporter.
@@ -0,0 +1,30 @@
# Agent Note: 有上限的 DeepSeek 会话日志上传
Status: implemented
[English](2026-09-24-bounded-session-log-upload.md) | 中文
## 问题
没有当前会话格式代的水位时,请求会携带整份规范日志:开启上传后的第一次请求、格式迁移后的第一次请求,以及 fork 的第一次请求都是如此。长会话日志可能超过 64 位宿主上 V8 的字符串上限,即 2^29 − 24 个 UTF-16 码元。此时适配器合并正文的 `JSON.stringify` 会抛出 `RangeError`,适配器报告 `TRANSPORT`,默认重试策略重复同样的序列化,任何请求都无法拿到推进水位所需的 2xx,于是该会话的每次模型请求都会失败。即使低于上限,单次请求仍可能携带数百 MiB:在 Node v24.17.0 与 macOS arm64 上,一个 96,341 条事件的会话首次上传 269 MiB,等待 34 秒才收到 2xx。
## 决策
`session-log-deepseek.Config.maxBytes` 默认 8 MiB,按 UTF-8 字节限制完整的序列化 `dsh_session_log` 字段。每次请求携带水位之后字段能放进上限的最长连续事件段,字段大小包括 header 与数值型 envelope 字段。接受操作把水位推进到该段的 `throughSeq`,后续请求从那里继续。第一条待发事件本身就超过上限时,请求省略该字段,插件记录一条告警,水位停在该事件之前,直到 `maxBytes` 能容纳它。JSON 文本超过运行时字符串上限的事件视为超过任何上限,因此它会阻塞上传但不会使请求失败,插件每次请求只序列化它一次。
DeepSeek 适配器把合并后请求正文的序列化失败视为扩展失败,而非传输失败。它发送不含任何扩展字段的基础正文,跳过联合接受事务,并通过提供方插件的 logger 报告被省略的字段名,因此贡献方会在之后的请求中重发未接受的状态。基础正文本身无法序列化时,请求仍然失败。
本决策取代[请求扩展决策](2026-08-21-deepseek-llm-api-request-extensions.zh.md)中的完整后缀交付与 fail-closed 序列化。该记录仍负责字段所有权、取消、接受以及其余 fail-closed 失败。
## 考虑过的替代方案
**先序列化整个待发事件段,只在超限时再按上限截取。** 无论哪种方式,量字节都要序列化每个候选事件。合成基准中,14 KiB 的一步整体序列化耗时 14.3 µs,逐条累加耗时 13.8 µs;135 MiB 的积压在回退前整体序列化就要 182 ms,逐条装满 8 MiB 只需 10 ms。对促成本改动的日志,整体尝试仍会超过字符串上限。
**单独发送超限的第一条事件。** 超过配置上限会重新引入提供方请求体大小拒绝,而这类拒绝仍会使模型请求失败。省略字段能让每次请求都有上限,告警也会指出被阻塞的事件。
**只省略序列化失败的那个字段。** 接受是跨字段的一个联合事务,按字段省略会拆分注册表的接受契约。在这条路径上丢弃全部扩展字段,只会让体积很小的 `dsh_plugin_packages` 清单缺席一次请求。
## 后果
默认情况下,积压按每次已接受请求最多 8 MiB 补传。按那次 269 MiB 上传实测的端到端速度(约 8 MiB/s),补传期间 8 MiB 的字段让每次请求多约 1 秒,0.86 GB 的积压约 100 次请求补完。在同一个 96,341 条事件的会话中,每步追加量的中位数是 11 KiB,最大单条事件是 414 KiB,因此普通步骤远低于上限,只有异常事件或很小的配置上限才会走到超限事件路径。积压补传期间,端点收到的 `throughSeq` 会落后于会话当前末尾。准备失败、接受失败与提供方拒绝仍会使模型请求失败。
插件测试覆盖精确字节边界、UTF-8 计数、跨已接受请求的积压补传、超限事件告警及其不变的水位、超过字符串上限的事件,以及经插件配置生效的 8 MiB 默认值。适配器测试覆盖经提供方插件 logger 报告的基础正文回退,以及没有 reporter 的适配器上的同一回退。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-09-18-windows-directory-junction-listing.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-18-windows-directory-junction-listing.md
/agent-note-list-directory-links-in-the-workspace-file-tree:
en: 70fbeeae3fa4a6e6
zh: 6ed08f09c9d1d02e
/agent-note-list-directory-links-in-the-workspace-file-tree/problem:
en: eec5c2e8edd0e0ff
zh: 03c0f3490bb891c0
/agent-note-list-directory-links-in-the-workspace-file-tree/decision:
en: ad36ceb185d15d69
zh: 71847eaa4641a9d7
/agent-note-list-directory-links-in-the-workspace-file-tree/alternatives-considered:
en: 8a9daf7bd2d78b65
zh: 69f46ee1108c2747
/agent-note-list-directory-links-in-the-workspace-file-tree/consequences:
en: 3cbc15d04a639292
zh: 4a9c94a1a1e8aeca
@@ -0,0 +1,27 @@
# Agent Note: List directory links in the workspace file tree
Status: implemented
English | [中文](2026-09-18-windows-directory-junction-listing.zh.md)
## Problem
Windows profile directories such as `Documents\My Music` are directory junctions: reparse points that `lstat` reports as symlinks rather than directories. The `list` endpoint gated on `lstat` before resolving, so it refused every final link with `workspace-file/not-directory`, while its own listing reported those same children as `directory` because `listDir` resolves each child. The file tree therefore listed the junctions and refused to open them, and the Sidebar reported `My Music`, `My Pictures`, and `My Videos` as not a directory.
## Decision
`list` follows a final link and judges what it resolves to. The resolved target must pass workspace containment and must stat as a directory, so a directory link inside the workspace lists through its target, and the returned workspace path names that target. A link resolving outside the workspace is `workspace-file/outside-workspace`; a link to a non-directory, or one whose target is gone, is `workspace-file/not-directory` with kind `symlink`.
`read`, `readBytes`, `readAll`, `readRelated`, and `stat` keep their `lstat` gate that refuses a final symlink, so no read path follows a link. The [Workspace Files service](../architecture/2026-09-05-workspace-files-service.md) keeps ownership of the service; its earlier rule that `list` rejects a final symlink is superseded here.
## Alternatives considered
**Drop the `lstat` gate for every method.** Rejected: reading through a link is a separate exposure decision, and the file methods' no-follow rule is a trust rule the report asked to keep.
**Special-case Windows junctions.** Rejected: `fs-local` already reports a junction as a `symlink` path entry, and the resolve-then-contain rule is identical for a junction and a POSIX directory symlink.
**Keep the gate and report a linked directory from the parent listing only.** Rejected: the listing already reports the resolved type, so the endpoint contradicted itself and a reader could not open what the parent presented as a directory.
## Consequences
A directory link inside the workspace now lists like its target, and the Windows profile junctions expand in the Sidebar. A link resolving outside the workspace reports `outside-workspace` instead of `not-directory`, which names the actual reason. Read access is unchanged. Tests cover a directory link inside the workspace, a link resolving outside it, and a link whose target is deleted; the spec creates a junction with `symlink(target, path, 'junction')` on Windows and a directory symlink elsewhere, so one spec runs on both.
@@ -0,0 +1,27 @@
# Agent Note: 列举工作区文件树中的目录链接
Status: implemented
[English](2026-09-18-windows-directory-junction-listing.md) | 中文
## Problem
Windows 的个人目录如 `Documents\My Music` 是目录联接:`lstat` 把这类重解析点报告为符号链接而不是目录。`list` 端点在解析之前先用 `lstat` 把关,于是把所有末端链接都以 `workspace-file/not-directory` 拒绝;而它自己的列举又把同样的子项报告为 `directory`,因为 `listDir` 会解析每个子项。文件树因此既列出这些联接、又拒绝打开它们,侧栏把 `My Music`、`My Pictures`、`My Videos` 报告为非目录。
## Decision
`list` 跟随末端链接并判定其解析结果。解析后的目标必须通过工作区包含检查,且经 stat 必须为目录,因此工作区内的目录链接经其目标列举,返回的工作区路径也以该目标命名。解析到工作区外的链接为 `workspace-file/outside-workspace`;指向非目录的链接、或目标已消失的链接,为 kind 为 `symlink` 的 `workspace-file/not-directory`。
`read`、`readBytes`、`readAll`、`readRelated` 与 `stat` 保留拒绝末端符号链接的 `lstat` 把关,因此没有任何读取路径跟随链接。[工作区文件服务](../architecture/2026-09-05-workspace-files-service.zh.md)继续拥有该服务;它此前「`list` 拒绝末端符号链接」的规则由本记录取代。
## Alternatives considered
**对所有方法去掉 `lstat` 把关。** 被否:经链接读取是另一项暴露决策,文件方法的「不跟随」规则是报告方要求保留的信任规则。
**单独处理 Windows 目录联接。** 被否:`fs-local` 已把目录联接报告为 `symlink` 路径项,且「先解析再判包含」的规则对目录联接与 POSIX 目录符号链接完全相同。
**保留把关,只靠父层列举报告被链接的目录。** 被否:列举本来就报告解析后的类型,端点因此自相矛盾,读取方无法打开父层呈现为目录的条目。
## Consequences
工作区内的目录链接现在与其目标一样可列举,Windows 个人目录中的联接可在侧栏展开。解析到工作区外的链接报告 `outside-workspace` 而不是 `not-directory`,如实说明原因。读取权限不变。测试覆盖工作区内的目录链接、解析到工作区外的链接,以及目标被删除的链接;spec 在 Windows 上用 `symlink(target, path, 'junction')` 创建目录联接,在其他平台创建目录符号链接,因此同一 spec 两端都跑。
@@ -18,8 +18,8 @@
en: a6d7884e4359af3a
zh: e72588705942000a
/agent-note-the-approval-seam-one-shot-permission-decisions-over-a-waterfall-of-answerers/decision/design-detail/the-seam-mechanism-and-policy-split:
en: 0aa6f3a90921d1ea
zh: c4a734f84800dae8
en: 53da7800d8744d9e
zh: 316fef884853ca5d
/agent-note-the-approval-seam-one-shot-permission-decisions-over-a-waterfall-of-answerers/decision/design-detail/ask-routing-in-dsh-tools:
en: 1d4478059f608fd2
zh: 4a1d12e4ccb766b5
@@ -55,7 +55,7 @@ After validation and a successful `approval/asked` append, the service resolves
Answerers are `approval/request` waterfall listeners. Zero listeners fall through to `unavailable`; a recognizing listener occupies the first-wins decision slot, while an unrecognized agent must delegate with `next()`. Listeners dispose with their fibers, so an unloaded channel fails closed. Because sibling registration order is not deterministic, a deployment composes one terminal answerer and reserves `prepend` for decide-or-delegate gates.
`ApprovalRequest` carries the asking `agent`, `toolName`, optional exact `callId`, human-readable `reason`, and optional `signal`. It uses the `ToolCallId` brand without importing `dsh-tools`, which depends on this seam. Channel adapters correlate any richer call state by `callId`; the approval request does not duplicate tool arguments.
`ApprovalRequest` carries the asking `agent`, `toolName`, optional exact `callId`, human-readable `reason`, optional localized `displayReason`, and optional `signal`. It uses the `ToolCallId` brand without importing `dsh-tools`, which depends on this seam. Channel adapters correlate any richer call state by `callId`; the approval request does not duplicate tool arguments.
#### Ask routing in dsh-tools
@@ -55,7 +55,7 @@ tool/result "escalated" — this one call ran under the wider mode; the gra
应答者是 `approval/request` waterfall 监听器。零监听器会直接落到 `unavailable`;识别该 agent 的监听器占用先到先得的决策槽,而不识别的监听器必须调用 `next()` 委派。监听器会随其 fiber 一同 dispose(资源释放),因此卸载通道后,请求会在故障时默认被拒绝。由于兄弟插件的注册顺序不确定,部署应组合一个终端应答者,并保留 `prepend` 给「决策或委派」门禁。
`ApprovalRequest` 携带发起请求的 `agent`、`toolName`、可选的精确 `callId`、人类可读的 `reason` 和可选的 `signal`。它使用 `ToolCallId` brand 而不导入依赖本 seam 的 `dsh-tools`。通道适配器可按 `callId` 关联任何更丰富的调用状态;审批请求本身不重复携带工具参数。
`ApprovalRequest` 携带发起请求的 `agent`、`toolName`、可选的精确 `callId`、人类可读的 `reason`、可选的本地化 `displayReason` 和可选的 `signal`。它使用 `ToolCallId` brand 而不导入依赖本 seam 的 `dsh-tools`。通道适配器可按 `callId` 关联任何更丰富的调用状态;审批请求本身不重复携带工具参数。
#### dsh-tools 中的 Ask 路由
@@ -30,8 +30,8 @@
en: 7bc0fab85c7c91d0
zh: 3eafcde6b1e3249c
/agent-note-the-subprocess-sandbox-confinement-seam-native-runners-escalation-and-per-session-modes/decision/design-detail/the-bash-consumer:
en: af561a23104227cd
zh: ac6fd59518d1bb55
en: f7f359c89a0e8fd6
zh: 07d0d551a35716e0
/agent-note-the-subprocess-sandbox-confinement-seam-native-runners-escalation-and-per-session-modes/decision/design-detail/escalation-one-approved-wider-retry-after-a-denial:
en: 035640e276225e97
zh: 09fd95921529369b
@@ -78,7 +78,7 @@ Provider-owned I/O requires asynchronous confinement. `confine(argv, policy, sig
`dsh-bash-sandbox` extends `LocalBashExecutor`, awaits `ctx.sandbox` for the exact `['bash', '-c', command]` argv with the execution signal, rechecks cancellation, and directly spawns the provider result. This leaves shell semantics and `BASH_ENV` on the inner Bash after the shipped native runner establishes confinement. A provider error propagates unchanged. An asynchronous rejection counts as a runner failure only when the caller-owned workdir is independently usable and Node reports `ENOENT` or `EACCES` with either an `error.path` equal to provider argv[0] or, when `error.path` is absent, an exact `syscall: 'spawn <runner>'`; a present path also requires `syscall: 'spawn'` or the exact `spawn <runner>`. Other codes, invalid workdirs, resource failures, unrelated syscalls, and unstructured rejections retain stage-neutral provider-failure semantics. Foreground execution converts attributable runner failures to `SANDBOX_UNAVAILABLE` with the original detail; an attributable asynchronous background rejection stamps `runnerFailed: true`, `denied: false`. A `SubprocessRuntime` that synchronously throws the same runner-identifying shape makes background start reject with `SANDBOX_UNAVAILABLE` before handle publication, while other synchronous errors propagate unchanged. After a direct outcome exists, foreground and background use one runner-failure classifier that requires the rule's exit-code check and a remaining fatal line after informational exclusions. A match takes priority over denial: foreground execution throws `SANDBOX_UNAVAILABLE` with that fatal line as detail; a settled `ShellProcess` stamps `sandbox.runnerFailed`, and the bash producer renders it through generic `job_output`.
The model sees the current effective file policy in the owner-derived `sandbox:policy` context, while the static tool description explains the denial marker (`[sandbox: file access denied under <mode> mode]`), encourages attempting commands that may be denied, and forbids retrying around a denial; when the escalation fields are advertised, a denied result additionally carries the escalation hint itself, so the sanctioned same-turn retry is prompted at the decision point rather than depending on the model recalling the description (§ Escalation). [The current-policy decision](../../archived/feature/2026-07-30-current-sandbox-policy-context.md) owns the context's rationale and boundaries.
The model sees the current effective file policy in the owner-derived `sandbox:policy` context, while the static tool description explains the denial marker (`[sandbox: file access denied under <mode> mode]`) and forbids retrying around a denial, and the `sandbox:policy` context tells the model to try an available tool normally; a rejected escalation's error text says the action stays denied; when the escalation fields are advertised, a denied result additionally carries the escalation hint itself, so the sanctioned same-turn retry is prompted at the decision point rather than depending on the model recalling the description (§ Escalation). [The current-policy decision](../../archived/feature/2026-07-30-current-sandbox-policy-context.md) owns the context's rationale and boundaries.
#### Escalation: one approved wider retry after a denial
@@ -78,7 +78,7 @@ Landlock launcher 源码和包家族位于 `native/system`,与 harness 消费
`dsh-bash-sandbox` 扩展 `LocalBashExecutor`,携带执行信号等待 `ctx.sandbox` 为精确的 `['bash', '-c', command]` argv 完成限制准备,重新检查取消后直接 spawn 提供方返回的 argv。这样,随附的原生 runner 建立约束后,shell 语义与 `BASH_ENV` 仍由内层 Bash 处理。提供方错误原样传播。异步 rejection 只有在调用方拥有的 workdir 经独立验证可用,Node 报告 `ENOENT` 或 `EACCES`,并且错误符合以下一种形态时,才判定为 runner 失败:`error.path` 等于提供方返回的 `argv[0]`,同时 `syscall` 为 `'spawn'` 或精确的 `'spawn <runner>'`;或者 `error.path` 不存在,同时 `syscall` 为精确的 `'spawn <runner>'`。其他错误码、无效 workdir、资源失败、无关 syscall 与无结构 rejection 保留不声明阶段的 provider-failure 语义。前台执行会将可归因的 runner failure 转为 `SANDBOX_UNAVAILABLE` 并附上原始详情;可归因的异步后台 rejection 则盖章 `runnerFailed: true`、`denied: false`。如果 `SubprocessRuntime` 同步抛出同样能指明 runner 的形态,后台启动会在发布句柄前以 `SANDBOX_UNAVAILABLE` 拒绝;其他同步错误原样传播。direct outcome 已存在后,前台与后台共用一个 runner failure 分类器:先排除信息性行,再要求规则的退出码检查与余下的一行致命诊断同时匹配。匹配结果优先于拒绝:前台执行抛出 `SANDBOX_UNAVAILABLE`,并以该致命行作为详情;结算后的 `ShellProcess` 会盖章 `sandbox.runnerFailed`,bash 生产者再通过通用 `job_output` 渲染它。
模型会在归属方派生的 `sandbox:policy` 上下文中看到当前有效的文件策略;静态工具描述则解释拒绝标记(`[sandbox: file access denied under <mode> mode]`),鼓励尝试可能被拒绝的命令,并禁止绕过拒绝重试。当升级字段被公布时,被拒绝的结果还会携带升级提示本身,使被认可的同轮次重试在决策点获得提示,而非依赖模型回忆描述(§ 升级机制)。[当前策略决策](../../archived/feature/2026-07-30-current-sandbox-policy-context.md)负责该上下文的理由与边界。
模型会在归属方派生的 `sandbox:policy` 上下文中看到当前有效的文件策略;静态工具描述则解释拒绝标记(`[sandbox: file access denied under <mode> mode]`)并禁止绕过拒绝重试,`sandbox:policy` 上下文要求模型正常尝试可用工具;被拒绝的升级请求的错误文本说明该操作仍被拒绝。当升级字段被公布时,被拒绝的结果还会携带升级提示本身,使被认可的同轮次重试在决策点获得提示,而非依赖模型回忆描述(§ 升级机制)。[当前策略决策](../../archived/feature/2026-07-30-current-sandbox-policy-context.md)负责该上下文的理由与边界。
#### 升级机制:拒绝后一次经批准的更宽重试
@@ -9,8 +9,8 @@
en: e22ee243b1503319
zh: b53c1a932d547f55
/agent-note-shared-developer-tool-settings/decision:
en: 3fc14af35b025970
zh: 8359262a5c5d3805
en: 62f71b49af7b9971
zh: f0f12da4cb0c7fc0
/agent-note-shared-developer-tool-settings/alternatives-considered:
en: df1b80e5b8d2a178
zh: 0136d16fc44d57bf
@@ -12,9 +12,9 @@ Diagnostic views, preset selection and changed-file summaries add complexity to
The settings-owned `ctx.configForms.developerTools` preference uses a shared entry form and exposes one accepted `ui-settings.enabled` preference. The initial default-off choice reduces routine-task complexity; [Developer tools default on after preference resolution](2026-09-20-developer-tools-default-on.md) supersedes that default and owns startup enablement. General Settings offers the same switch on Web and desktop. The existing settings transport owns validation, ordered writes, persistence and recovery; loopback Web and desktop follow Host acceptance. Remote Web owns one browser-local observable shared by all consumers until reload, with no Host writes.
Off hides the Trajectory View, hides tool Inspect actions when no visible View declares tool-call inspection, hides the conversation View tab bar while fewer than two Views are available, hides the new-session preset chip and the settings row that allows switching Agent modes, and omits the changed-files card and its summary read. Views registered by other plugins stay available. Turning it off while Trajectory is active activates Chat. The preset composition, saved default, Session events and explicit delivery cards remain unchanged. This preference is presentation policy, not Host authorization.
Off hides the Trajectory View, hides tool Inspect actions when no visible View declares tool-call inspection, hides the conversation View tab bar while fewer than two Views are available, hides the new-session preset chip and refuses the Agent-preset cards' selection, and omits the changed-files card and its summary read. Views registered by other plugins stay available. Turning it off while Trajectory is active activates Chat. The preset composition, saved default, Session events and explicit delivery cards remain unchanged. This preference is presentation policy, not Host authorization.
Plugin assembly translates the shared preference into component-specific capabilities (`showCodeDiff`, `showPresetPicker`, and `interactivePreview`). Conversation supplies the inspection callback using the visible target's `toolCallFocus`; Chat does not name the target. The builtin HTML renderer uses DOMPurify for complete-document sanitization and parser-mutation defenses. Off preserves root styling, removes active documents, resource hints, SVG animation and all `href` attributes, and mounts an iframe with no sandbox permissions and a CSP denying scripts and external resources. It reads no related files. On retains the opaque scripted Blob renderer and its bounded related-file reads. Switching modes replaces the browsing context and disposes pending reads. The [document-preview decision](../architecture/2026-09-08-document-preview-operations.md) and [filesystem-read authority](../architecture/2026-09-09-workspace-file-read-authority.md) remain authoritative for advanced rendering and Host read access.
Plugin assembly translates the shared preference into component-specific capabilities (`showCodeDiff`, the Agent-preset surfaces' `developerTools`, and `interactivePreview`). Conversation supplies the inspection callback using the visible target's `toolCallFocus`; Chat does not name the target. The builtin HTML renderer uses DOMPurify for complete-document sanitization and parser-mutation defenses. Off preserves root styling, removes active documents, resource hints, SVG animation and all `href` attributes, and mounts an iframe with no sandbox permissions and a CSP denying scripts and external resources. It reads no related files. On retains the opaque scripted Blob renderer and its bounded related-file reads. Switching modes replaces the browsing context and disposes pending reads. The [document-preview decision](../architecture/2026-09-08-document-preview-operations.md) and [filesystem-read authority](../architecture/2026-09-09-workspace-file-read-authority.md) remain authoritative for advanced rendering and Host read access.
## Alternatives considered
@@ -12,9 +12,9 @@ Status: implemented
设置域内的 `ctx.configForms.developerTools` 偏好对象使用共享条目表单,提供一个已接受的 `ui-settings.enabled` 偏好。最初默认关闭的选择用于降低日常任务的复杂度;[偏好解析后默认开启开发者工具](2026-09-20-developer-tools-default-on.zh.md)取代该默认值并负责启动时的启用策略。Web 和桌面端的通用设置提供同一个开关。现有设置传输负责验证、有序写入、持久化和恢复;回环 Web 和桌面端跟随 Host 接受的状态。远程 Web 持有一个所有消费者共享的浏览器本地可观察值,刷新后重置,不发送 Host 写入。
关闭时隐藏 Trajectory View,在没有可见 View 声明工具调用检查能力时隐藏 Inspect 操作,可见 View 少于两个时隐藏会话 View 标签栏,隐藏新会话预设入口和「允许切换 agent 模式」设置行,并省略改动文件卡片及其摘要读取。其他插件注册的 View 保持可用。在 Trajectory 中关闭开关会激活 Chat。预设组合、保存的默认值、Session 事件和显式交付卡片保持不变。此偏好是展示策略,不是 Host 授权。
关闭时隐藏 Trajectory View,在没有可见 View 声明工具调用检查能力时隐藏 Inspect 操作,可见 View 少于两个时隐藏会话 View 标签栏,隐藏新会话预设入口并拒绝 Agent 预设卡片的选择操作,并省略改动文件卡片及其摘要读取。其他插件注册的 View 保持可用。在 Trajectory 中关闭开关会激活 Chat。预设组合、保存的默认值、Session 事件和显式交付卡片保持不变。此偏好是展示策略,不是 Host 授权。
插件组装层将共享偏好转换为组件自身的能力(`showCodeDiff`、`showPresetPicker` 和 `interactivePreview`)。Conversation 根据可见目标的 `toolCallFocus` 提供检查回调;Chat 不再指定目标名称。内置 HTML 渲染器使用 DOMPurify 清理完整文档并防护解析变异。关闭时保留根节点样式,移除主动文档、资源提示、SVG 动画及所有 `href` 属性,再放入不授予沙箱权限的 iframe,并通过 CSP 禁止脚本和外部资源。不读取关联文件。开启时保留不透明源的脚本 Blob 渲染器及其有界关联文件读取。切换模式会替换浏览上下文并清理待处理读取。[文档预览决策](../architecture/2026-09-08-document-preview-operations.zh.md)和[文件系统读取权限](../architecture/2026-09-09-workspace-file-read-authority.zh.md)继续定义高级渲染与 Host 读取访问。
插件组装层将共享偏好转换为组件自身的能力(`showCodeDiff`、Agent 预设各界面读取的 `developerTools`,以及 `interactivePreview`)。Conversation 根据可见目标的 `toolCallFocus` 提供检查回调;Chat 不再指定目标名称。内置 HTML 渲染器使用 DOMPurify 清理完整文档并防护解析变异。关闭时保留根节点样式,移除主动文档、资源提示、SVG 动画及所有 `href` 属性,再放入不授予沙箱权限的 iframe,并通过 CSP 禁止脚本和外部资源。不读取关联文件。开启时保留不透明源的脚本 Blob 渲染器及其有界关联文件读取。切换模式会替换浏览上下文并清理待处理读取。[文档预览决策](../architecture/2026-09-08-document-preview-operations.zh.md)和[文件系统读取权限](../architecture/2026-09-09-workspace-file-read-authority.zh.md)继续定义高级渲染与 Host 读取访问。
## Alternatives considered
+9 -1
View File
@@ -1,6 +1,6 @@
---
name: agent-experience
description: Use when designing agent tools, skills, context loading, or multi-step workflows to make information discoverable and use context efficiently.
description: Use when writing or changing a model-facing tool definition (name, description, parameter schema, or its system-prompt section), and when designing skills, context loading, or multi-step workflows, to make information discoverable and use context efficiently.
metadata:
date: "2026-09-11"
---
@@ -11,3 +11,11 @@ metadata:
- **Prefer bounded outputs:** return concise results with identifiers or paths for retrieving details; avoid dumping entire logs or documents.
- **Use locality:** include a bounded amount of likely needed adjacent context with an operation’s result. For example, deliver a thread reply with a few preceding messages. Mark omissions and truncation explicitly, and provide a way to retrieve more.
- **Evaluate total work:** saving context is useful only if it does not cause more searches, repeated reads, or mistakes.
## Tool definitions
- **Delete obvious constraints.** Omit rules the model learns from the call result, such as a missing file failing to read or a failed send not being delivered.
- **Describe behavior, not implementation.** State what the tool does and returns; omit internal mechanisms, output markers the model sees in results, and enforcement details.
- **Put parameter rules on the parameter.** Defaults, ranges, pairing, and when-to-set rules belong in that parameter's description or schema; a description that varies with configuration usually means the varying part belongs to a parameter.
- **Say each fact once.** Do not repeat the tool definition in its system-prompt section, a parameter description in the tool description, or one tool's rules in another tool.
- **Measure the change.** Compare first-turn prompt tokens before and after a tool-definition change.
+1 -1
View File
@@ -35,7 +35,7 @@ export async function runDumpConfigSchema(
// Trusted module diagnostics must not precede the JSON document on stdout.
process.stdout.write = process.stderr.write.bind(process.stderr)
try {
dump = await generateConfigSchema(NAME, loaded, layers.map(layer => layer.patches), INSTALL_ANCHOR)
dump = await generateConfigSchema(loaded, layers.map(layer => layer.patches), INSTALL_ANCHOR)
} finally {
process.stdout.write = stdoutWrite
}
+2
View File
@@ -23,6 +23,7 @@ import {
installFailLoud,
loadOverlayPatches,
loadProfile,
reportSkippedBundles,
PluginPackages,
PROFILE_PATCH_FILENAME,
PROFILE_TEMPLATES,
@@ -166,6 +167,7 @@ export function initializeProfileFromDefault(
export function prepareProfile(name: string, userLayer = true, fromDefaultProfile?: string): Profile {
if (fromDefaultProfile !== undefined) initializeProfileFromDefault(name, fromDefaultProfile)
const profile = loadProfile(NAME, name, INSTALL_ANCHOR, undefined, { userLayer })
reportSkippedBundles(NAME, profile)
writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG)
return profile
}
+1 -1
View File
@@ -28,7 +28,7 @@ it.each([false, true])('settles startup after parent IPC disconnect (boot failur
}
writeFileSync(join(root, 'package.json'), '{"type":"module"}')
writeFileSync(join(modules, 'dsh-app-boot', 'package.json'), '{"type":"module","exports":"./index.js"}')
writeFileSync(join(modules, 'dsh-app-boot', 'index.js'), 'export const loadProfileDirectory = () => ({}); export const loadLayeredEnv = () => ({})')
writeFileSync(join(modules, 'dsh-app-boot', 'index.js'), 'export const loadProfileDirectory = () => ({ skippedBundles: [] }); export const reportSkippedBundles = () => {}; export const loadLayeredEnv = () => ({})')
writeFileSync(join(modules, 'dsh', 'package.json'), '{"type":"module","exports":{"./profile-boot":"./profile-boot.js"}}')
writeFileSync(join(modules, 'dsh', 'profile-boot.js'), `
import { writeFileSync } from 'node:fs';
@@ -108,7 +108,7 @@ it.skipIf(!process.env.DEEPSEEK_API_KEY || process.platform === 'win32')(`agent
expect(existsSync(decoyMarker), 'the agent must not invoke system Office when the bundled CLI is supplied').toBe(false)
expect(commandText, 'the skill supplies an entry; searching for other Office binaries is unnecessary').not.toMatch(/(?:which|whereis|command -v|find).{0,100}(?:libreoffice|soffice)/iu)
const capabilities = JSON.parse(await readFile(join(root, 'capabilities.json'), 'utf8')) as { runtime: { version: string; cliPath: string } }
expect(capabilities.runtime.version).toBe('0.1.0')
expect(capabilities.runtime.version).toBe('0.1.1')
expect(capabilities.runtime.cliPath).toMatch(/lib[/\\]cli\.js$/u)
const pdf = await readFile(join(root, 'result.pdf'))
expect(pdf.subarray(0, 5).toString()).toBe('%PDF-')
+2 -2
View File
@@ -60,7 +60,7 @@ describe('runProfile with an application-owned profile', () => {
throw failure
})
if (stage === 'composition') vi.mocked(createRuntimeResolution).mockRejectedValueOnce(failure)
const profile: Profile = {
const profile: Profile = { skippedBundles: [],
name: 'desktop', dir: home, patchPath: join(home, 'cordis.patch.yml'),
patches: [], layers: [],
}
@@ -121,7 +121,7 @@ describe('runProfile with an application-owned profile', () => {
writeFileSync(profilePatch, '- id: target\n config: { profile: true, priority: profile }\n')
writeFileSync(overlay, '- id: target\n config: { overlay: true, priority: overlay }\n')
writeFileSync(join(home, 'cordis.yml'), '- id: stale\n')
const profile: Profile = {
const profile: Profile = { skippedBundles: [],
name: 'desktop', dir: home, patchPath: profilePatch,
patches: [{ id: 'target', config: { profile: true, priority: 'profile' } }],
layers: [{
+39 -2
View File
@@ -136,7 +136,7 @@ async function bootWeb(
await mkdir(dirname(link), { recursive: true })
await symlink(packageDir, link, 'junction')
}
let profile: Profile = {
let profile: Profile = { skippedBundles: [],
name: 'spec',
dir: profileDir,
layers: [],
@@ -853,7 +853,6 @@ describe('a delegated child', () => {
describe('the default preset as a user setting', () => {
it('composes an unnamed session from the stored default, not the composed one', async () => {
expect((await ctx.agentPresets.remoteExportList()).modeSelectionEnabled).toBe(true)
expect(ctx.agentPresets.defaultId).toBe('standard')
await ctx.settings.update(SETTINGS_NAMESPACE, { selectedDefault: 'minimal' })
@@ -882,6 +881,44 @@ describe('the default preset as a user setting', () => {
})
})
describe('a profile patch stored before Developer tools owned preset selection', () => {
let legacy: Context
let legacyHome: string
beforeAll(async () => {
legacyHome = await mkdtemp(join(tmpdir(), 'dsh-web-presets-legacy-'))
// A stored configuration from before the switch moved to Developer tools:
// it carries the retired key beside the default the user had saved. The
// Loader resolves the declared fields and leaves the extra one alone.
legacy = await bootWeb(legacyHome, [{
id: SETTINGS_NAMESPACE,
config: { default: 'standard', selectedDefault: 'minimal', modeSelectionEnabled: false },
}])
}, 120_000)
afterAll(async () => {
await legacy?.fiber.dispose()
await rm(legacyHome, { recursive: true, force: true })
})
it('starts, keeps the retired key inert and composes new sessions from the saved default', async () => {
expect(legacy.agentPresets.defaultId).toBe('minimal')
expect((await legacy.agentPresets.remoteExportList()).presets.find(row => row.id === 'minimal')?.isDefault).toBe(true)
// Settings projects the declared fields, so the retired key is neither
// shown nor rewritten; the user's saved default is.
expect(legacy.settings.describe().find(row => row.ns === SETTINGS_NAMESPACE)?.value)
.toEqual({ selectedDefault: 'minimal' })
const handle = await legacy.agents.create({
sessionId: SessionId('preset-legacy-patch'),
setup: agentCtx => legacy.agentPresets.mount(agentCtx).then(() => undefined),
})
try {
expect(toolNames(legacy, handle.agent)).toEqual(['bash', ...SCHEDULE_TOOLS])
} finally {
await handle.dispose()
}
})
})
describe('a session keeps the preset it was created with', () => {
it('refuses to adopt a live session under a different preset', async () => {
const handle = await ctx.agents.create({
+1 -1
View File
@@ -10,7 +10,7 @@
"lib/index.js"
],
"dependencies": {
"@deepseek-ai/libreoffice-kit": "^0.1.0",
"@deepseek-ai/libreoffice-kit": "^0.1.1",
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh": "workspace:*",
"@deepseek-ai/dsh-agent": "workspace:*",
+2 -1
View File
@@ -2,7 +2,7 @@
import { delimiter, join } from 'node:path'
import { inspect } from 'node:util'
import { loadLayeredEnv, loadProfileDirectory } from '@deepseek-ai/dsh-app-boot'
import { loadLayeredEnv, loadProfileDirectory, reportSkippedBundles } from '@deepseek-ai/dsh-app-boot'
import { runProfile } from '@deepseek-ai/dsh/profile-boot'
import type {} from '@deepseek-ai/dsh-client-connection'
import type {} from '@deepseek-ai/dsh-host-webserver'
@@ -21,6 +21,7 @@ async function main(): Promise<void> {
installOfficeEngineResolution(runtimeDir)
const installAnchor = join(runtimeDir, 'node_modules', '@deepseek-ai', 'dsh', 'package.json')
const profile = loadProfileDirectory('dsh', projectDir, installAnchor)
reportSkippedBundles('dsh', profile)
const application = runProfile({
environment: loadLayeredEnv('dsh'),
profile: 'desktop',
+14 -14
View File
@@ -249,7 +249,7 @@ describe('web e2e: agent-preset selection', () => {
await rm(fixtureRoot, { recursive: true, force: true })
})
it('starts with mode selection shown on the Standard default', async () => {
it('starts on the Standard default with the roster editable in Settings', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-hero'))
await connectFreshWorkspace(page, scaffold.workspaceCwd)
await page.getByRole('button', { name: 'Standard mode', exact: true }).waitFor({ timeout: 10_000 })
@@ -257,9 +257,9 @@ describe('web e2e: agent-preset selection', () => {
await openSettings(page, 'en')
const dialog = page.getByRole('dialog', { name: 'Settings' })
await dialog.getByRole('button', { name: 'Agent presets' }).click()
const toggle = dialog.getByRole('switch', { name: 'Choose a mode for new tasks' })
await dialog.getByRole('button', { name: 'New task default: Standard mode' }).waitFor({ timeout: 10_000 })
expect(await toggle.getAttribute('aria-checked')).toBe('true')
expect(await dialog.getByRole('switch').count()).toBe(0)
await dialog.getByRole('button', { name: 'Set as new task default: Minimal mode' }).waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: 'Close' }).last().click()
const snapshot = await captureStableAria(page, '[class*="heroWorkspaceRow"]', scaffold.workspaceCwd)
@@ -343,7 +343,7 @@ describe('web e2e: agent-preset selection', () => {
await writeComposerDraft(page, composer, '')
}, 90_000)
it('aligns the current blank task and restores its saved default when re-enabled', async () => {
it('keeps the saved default composing sessions while Developer tools only gate the choice', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-disabled'))
await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('standard')
@@ -353,23 +353,23 @@ describe('web e2e: agent-preset selection', () => {
await dialog.getByRole('button', { name: 'Set as new task default: Minimal mode' }).click()
await dialog.getByRole('button', { name: 'New task default: Minimal mode' }).waitFor({ timeout: 10_000 })
await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('minimal')
const toggle = dialog.getByRole('switch', { name: 'Choose a mode for new tasks' })
await toggle.click()
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false')
await dialog.getByRole('button', { name: 'Application default: Standard mode' }).waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: 'General', exact: true }).click()
const developerTools = dialog.getByRole('switch', { name: 'Coding Tools' })
await developerTools.click()
await expect.poll(() => developerTools.getAttribute('aria-checked')).toBe('false')
await dialog.getByRole('button', { name: 'Close' }).last().click()
// The gate hides the choice; the blank task keeps its saved composition.
await expect.poll(() => page.getByRole('button', { name: / mode$/ }).count()).toBe(0)
await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('standard')
await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('minimal')
// The switch controls availability only: re-enabling restores the saved
// default and aligns this same still-blank task with it.
await openSettings(page, 'en')
const reopened = page.getByRole('dialog', { name: 'Settings' })
await reopened.getByRole('button', { name: 'General', exact: true }).click()
const reopenedDeveloperTools = reopened.getByRole('switch', { name: 'Coding Tools' })
await reopenedDeveloperTools.click()
await expect.poll(() => reopenedDeveloperTools.getAttribute('aria-checked')).toBe('true')
await reopened.getByRole('button', { name: 'Agent presets' }).click()
const reopenedToggle = reopened.getByRole('switch', { name: 'Choose a mode for new tasks' })
await reopenedToggle.click()
await expect.poll(() => reopenedToggle.getAttribute('aria-checked')).toBe('true')
await reopened.getByRole('button', { name: 'New task default: Minimal mode' }).waitFor({ timeout: 10_000 })
await reopened.getByRole('button', { name: 'Close' }).last().click()
await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('minimal')
+2 -2
View File
@@ -175,8 +175,8 @@ describe('web e2e: a git workspace turn ends with its changed files', () => {
expect(compactHeight).toBeLessThan(60)
await openSettings(page, 'zh')
const settings = page.getByRole('dialog', { name: '设置' })
await settings.getByRole('switch', { name: '开发者工具' }).click()
await expect.poll(() => settings.getByRole('switch', { name: '开发者工具' }).getAttribute('aria-checked')).toBe('true')
await settings.getByRole('switch', { name: '代码工作工具' }).click()
await expect.poll(() => settings.getByRole('switch', { name: '代码工作工具' }).getAttribute('aria-checked')).toBe('true')
await settings.getByRole('button', { name: '关闭', exact: true }).click()
await header.getByRole('tablist').waitFor({ state: 'visible' })
expect(await header.evaluate(element => element.getBoundingClientRect().height)).toBeGreaterThan(compactHeight)
+18 -4
View File
@@ -372,10 +372,24 @@ describe('web e2e: clickable links gallery', () => {
// — TOOL_VARIANTS has no entry for it, so it falls to the generic row with
// no openable path even though its create still joins the produced chips.
expect(await page.locator('button[class*="fileLink"]').count()).toBe(7)
// The collapsed web-fetch row's summary is its URL, opened in a new tab.
const fetchSummaryLink = page.locator(`a[class*="fileLink"][href="${FETCH_URL}"]`)
expect(await fetchSummaryLink.count()).toBe(1)
expect(await fetchSummaryLink.getAttribute('target')).toBe('_blank')
// A row-center click can hit the nested fileLink button and invoke the
// Host opener; right-edge clicks expand the card itself.
await expandTurnProcesses(page)
// Clicking the collapsed summary link opens the page without expanding the row.
const fetchPopupPromise = page.waitForEvent('popup')
await fetchSummaryLink.click()
const fetchPopup = await fetchPopupPromise
try {
await fetchPopup.waitForURL(FETCH_URL)
} finally {
await fetchPopup.close()
}
expect(await page.locator('[data-web="fetch"]').count()).toBe(0)
// A row-center click can hit a nested summary link and invoke its opener;
// right-edge clicks expand the card itself.
for (const row of [
/^Search clickable link styles/,
/^Fetch /,
@@ -400,7 +414,7 @@ describe('web e2e: clickable links gallery', () => {
const sourceLink = page.locator(`a[href="${SOURCE_URL}"]`)
await expect.poll(() => sourceLink.count(), { timeout: 10_000 }).toBe(1)
expect(await page.locator('a[href^="ftp:"]').count()).toBe(0)
expect(await page.locator(`a[href="${FETCH_URL}"]`).count()).toBe(1)
expect(await page.locator(`a[href="${FETCH_URL}"]`).count()).toBe(2)
expect(await page.locator(`a[href="${GUIDE_URL}"]`).count()).toBe(2)
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
@@ -423,7 +437,7 @@ describe('web e2e: clickable links gallery', () => {
['markdown anchor', guideLink],
['file mention', mentions.first()],
['search source', sourceLink.first()],
['fetch url', page.locator(`a[href="${FETCH_URL}"]`).first()],
['fetch url', page.locator(`[data-web="fetch"] a[href="${FETCH_URL}"]`)],
] as const) {
expect.soft(await styleOf(link, 'color'), `${name} color`).toBe(LINK_BLUE)
expect.soft(await styleOf(link, 'font-weight'), `${name} weight`).toBe('500')
+12 -10
View File
@@ -14,7 +14,7 @@ it('persists developer tools in the Host settings document and restores the acce
const page = await newEnglishPage(browser)
await page.goto(scaffold.authenticatedUrl)
await openSettings(page, 'en')
const toggle = page.getByRole('switch', { name: 'Developer tools' })
const toggle = page.getByRole('switch', { name: 'Coding Tools' })
expect(await toggle.getAttribute('aria-checked')).toBe('true')
expect(await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).not.toMatch(/id: ui-settings(?:\r?\n|$)/)
await toggle.click()
@@ -25,16 +25,18 @@ it('persists developer tools in the Host settings document and restores the acce
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false')
await page.getByRole('button', { name: 'Agent presets', exact: true }).click()
const pickerToggle = page.getByRole('switch', { name: 'Choose a mode for new tasks' })
await expect.poll(() => page.getByRole('heading', { name: 'Agent presets', exact: true }).count()).toBe(1)
expect(await pickerToggle.count()).toBe(0)
// The page owns no selection switch: only the General one gates the choice.
const section = page.locator('section')
.filter({ has: page.getByRole('heading', { name: 'Agent presets', exact: true }) })
const refused = section.getByRole('button', { name: /^Turn on Coding Tools in General settings to choose a default: / })
expect(await section.getByRole('switch').count()).toBe(0)
await expect.poll(() => refused.count()).toBeGreaterThan(0)
await scaffold.ctx.settings.update('ui-settings', { enabled: true })
await expect.poll(() => pickerToggle.count()).toBe(1)
await expect.poll(() => pickerToggle.isEnabled()).toBe(true)
const pickerEnabled = await pickerToggle.getAttribute('aria-checked')
await expect.poll(() => section.getByRole('button', { name: 'Set as new task default: Minimal mode' }).count()).toBe(1)
await expect.poll(() => refused.count()).toBe(0)
expect(await section.getByRole('switch').count()).toBe(0)
await scaffold.ctx.settings.update('ui-settings', { enabled: false })
await expect.poll(() => pickerToggle.count()).toBe(0)
await scaffold.ctx.settings.update('ui-settings', { enabled: true })
await expect.poll(() => pickerToggle.isEnabled()).toBe(true)
await expect.poll(() => pickerToggle.getAttribute('aria-checked')).toBe(pickerEnabled)
await expect.poll(() => refused.count()).toBeGreaterThan(0)
expect(await section.getByRole('switch').count()).toBe(0)
})
+2 -2
View File
@@ -837,8 +837,8 @@ else process.exit(1);
].join('\n'))
await openSettings(page, 'en')
const settings = page.getByRole('dialog', { name: 'Settings' })
await settings.getByRole('switch', { name: 'Developer tools' }).click()
await expect.poll(() => settings.getByRole('switch', { name: 'Developer tools' }).getAttribute('aria-checked')).toBe('true')
await settings.getByRole('switch', { name: 'Coding Tools' }).click()
await expect.poll(() => settings.getByRole('switch', { name: 'Coding Tools' }).getAttribute('aria-checked')).toBe('true')
await successShot(page, 'developer-tools-setting')
await settings.getByRole('button', { name: 'Close', exact: true }).click()
await expect.poll(() => iframe.getAttribute('sandbox')).toBe('allow-scripts')
@@ -9,9 +9,6 @@
- button "关闭"
- heading "Agent 预设" [level=2]
- paragraph: 选择 Agent 的工具和工作方式。日常任务用「标准模式」,扩展 DSH 的能力用「创造模式」。
- text: 新任务可选择模式 实验性
- paragraph: 开启后,可为每个新任务选择模式,并在这里设置默认值。关闭后,新任务使用应用配置的默认预设。已有任务不受影响。
- switch "新任务可选择模式" [checked]
- heading "内置" [level=3]
- list:
- listitem:
@@ -83,7 +83,10 @@
- text: One cursor token, one focus ring.
- listitem: Mirror spec (non-http) A non-http source renders inert.
- button "Inspect"
- button "Fetch https://docs.example.test/tokens" [expanded]
- button "Fetch https://docs.example.test/tokens" [expanded]:
- text: Fetch
- link "https://docs.example.test/tokens":
- /url: https://docs.example.test/tokens
- link "https://docs.example.test/tokens":
- /url: https://docs.example.test/tokens
- text: HTTP 200
@@ -22,11 +22,11 @@
- button "标准"
- text: 性能与用量 选择性能与用量信息展示的详细程度
- button "详细"
- text: 开发者工具 显示用于调试和排查问题的工具与信息
- switch "开发者工具" [checked]
- text: 代码工作工具 开启后显示轨迹、本轮代码差异,新对话中的 Agent 预设切换
- switch "代码工作工具" [checked]
- text: 快捷键
- paragraph: 查看当前可用的快捷键和输入操作
- button "查看快捷键"
- paragraph: 查看和编辑当前可用的快捷键和输入操作
- button "编辑快捷键"
- text: 繁忙时的发送行为 智能体运行时 Enter 键和发送按钮的行为;Cmd/Ctrl+Enter 使用另一行为
- button "排队发送"
- text: 当前版本:{{version}}
@@ -22,11 +22,11 @@
- button "Standard"
- text: Performance & usage Choose how much performance and usage information to show
- button "Detailed"
- text: Developer tools Show tools and information for debugging and troubleshooting
- switch "Developer tools" [checked]
- text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats
- switch "Coding Tools" [checked]
- text: Keyboard shortcuts
- paragraph: Browse available shortcuts and input actions
- button "View shortcuts"
- paragraph: View and edit available shortcuts and input actions
- button "Edit shortcuts"
- text: Send behavior while busy What Enter and the Send button do while the agent is running; Cmd/Ctrl+Enter uses the other behavior
- button "Queue"
- text: "Current version: {{version}}"
@@ -24,8 +24,8 @@
- button "查看 Agent 循环": Agent 循环
- text: 控制 Agent 派发工具调用的方式。
- listitem:
- button "查看 Subagent": Subagent
- text: 设置 Subagent 的递归层级、数量和模型。
- button "查看 子智能体": 子智能体
- text: 设置子智能体的递归层级、数量和模型。
- listitem:
- button "查看 网页搜索": 网页搜索
- text: 设置 DeepSeek 的搜索提供方。
@@ -1,6 +1,6 @@
- button "返回插件列表": 插件列表
- heading "Subagent" [level=3]
- paragraph: 设置 Subagent 的递归层级、数量和模型。
- heading "子智能体" [level=3]
- paragraph: 设置子智能体的递归层级、数量和模型。
- region "运行限制":
- heading "运行限制" [level=3]
- text: 最大递归深度
@@ -10,16 +10,16 @@
- textbox "最大递归深度":
- /placeholder: ""
- text: "2"
- text: Subagent 并行数量上限
- button "Subagent 并行数量上限说明"
- text: 子智能体并行数量上限
- button "子智能体并行数量上限说明"
- text: 已覆盖
- button "恢复默认"
- textbox "Subagent 并行数量上限":
- textbox "子智能体并行数量上限":
- /placeholder: ""
- text: "12"
- region "模型选择":
- heading "模型选择" [level=3]
- text: 允许 Agent 为 Subagent 选择模型
- switch "允许 Agent 为 Subagent 选择模型"
- paragraph: 关闭后,Subagent 使用配置的默认模型或继承父 Agent 的模型;已选模型会保留。
- text: 允许 Agent 为子智能体选择模型
- switch "允许 Agent 为子智能体选择模型"
- paragraph: 关闭后,子智能体使用配置的默认模型或继承父 Agent 的模型;已选模型会保留。
- button "保存" [disabled]
@@ -24,8 +24,8 @@
- button "查看 Agent 循环": Agent 循环
- text: 控制 Agent 派发工具调用的方式。
- listitem:
- button "查看 Subagent": Subagent
- text: 设置 Subagent 的递归层级、数量和模型。
- button "查看 子智能体": 子智能体
- text: 设置子智能体的递归层级、数量和模型。
- listitem:
- button "查看 网页搜索": 网页搜索
- text: 设置 DeepSeek 的搜索提供方。
@@ -24,8 +24,8 @@
- button "查看 Agent 循环": Agent 循环
- text: 控制 Agent 派发工具调用的方式。
- listitem:
- button "查看 Subagent": Subagent
- text: 设置 Subagent 的递归层级、数量和模型。
- button "查看 子智能体": 子智能体
- text: 设置子智能体的递归层级、数量和模型。
- listitem:
- button "查看 网页搜索": 网页搜索
- text: 设置 DeepSeek 的搜索提供方。
@@ -22,11 +22,11 @@
- button "Standard"
- text: Performance & usage Choose how much performance and usage information to show
- button "Detailed"
- text: Developer tools Show tools and information for debugging and troubleshooting
- switch "Developer tools"
- text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats
- switch "Coding Tools"
- text: Keyboard shortcuts
- paragraph: Browse available shortcuts and input actions
- button "View shortcuts"
- paragraph: View and edit available shortcuts and input actions
- button "Edit shortcuts"
- text: Send behavior while busy What Enter and the Send button do while the agent is running; Cmd/Ctrl+Enter uses the other behavior
- button "Queue"
- text: "Current version: {{version}}"
@@ -22,11 +22,11 @@
- button "Standard"
- text: Performance & usage Choose how much performance and usage information to show
- button "Detailed"
- text: Developer tools Show tools and information for debugging and troubleshooting
- switch "Developer tools"
- text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats
- switch "Coding Tools"
- text: Keyboard shortcuts
- paragraph: Browse available shortcuts and input actions
- button "View shortcuts"
- paragraph: View and edit available shortcuts and input actions
- button "Edit shortcuts"
- text: Send behavior while busy What Enter and the Send button do while the agent is running; Cmd/Ctrl+Enter uses the other behavior
- button "Queue"
- text: "Current version: {{version}}"
@@ -22,11 +22,11 @@
- button "标准"
- text: 性能与用量 选择性能与用量信息展示的详细程度
- button "详细"
- text: 开发者工具 显示用于调试和排查问题的工具与信息
- switch "开发者工具"
- text: 代码工作工具 开启后显示轨迹、本轮代码差异,新对话中的 Agent 预设切换
- switch "代码工作工具"
- text: 快捷键
- paragraph: 查看当前可用的快捷键和输入操作
- button "查看快捷键"
- paragraph: 查看和编辑当前可用的快捷键和输入操作
- button "编辑快捷键"
- text: 繁忙时的发送行为 智能体运行时 Enter 键和发送按钮的行为;Cmd/Ctrl+Enter 使用另一行为
- button "排队发送"
- text: 当前版本:{{version}}
@@ -1,5 +1,3 @@
- listitem:
- text: New Session
- button "Remove shortcut for New Session"
- button "Edit shortcut for New Session"
- button "New Session A B": A B
- text: New Session A B
@@ -1,5 +1,3 @@
- listitem:
- text: New Session
- button "Remove shortcut for New Session"
- button "Edit shortcut for New Session"
- button "New Session ⌘ C": ⌘ C
- text: New Session ⌘ C
@@ -1,5 +1,3 @@
- listitem:
- text: New Session
- button "Remove shortcut for New Session"
- button "Edit shortcut for New Session"
- button "New Session Ctrl + C": Ctrl + C
- text: New Session Ctrl + C
@@ -7,126 +7,72 @@
- region "Application":
- list:
- listitem:
- text: Open keyboard shortcuts
- button "Remove shortcut for Open keyboard shortcuts"
- button "Edit shortcut for Open keyboard shortcuts"
- button "Open keyboard shortcuts ⌘ /": ⌘ /
- text: Open keyboard shortcuts ⌘ /
- listitem:
- text: New Session
- button "Remove shortcut for New Session"
- button "Edit shortcut for New Session"
- button "New Session ⌥ ⌘ N": ⌥ ⌘ N
- text: New Session ⌥ ⌘ N
- listitem:
- text: Toggle left sidebar
- button "Remove shortcut for Toggle left sidebar"
- button "Edit shortcut for Toggle left sidebar"
- button "Toggle left sidebar ⌥ ⌘ B": ⌥ ⌘ B
- text: Toggle left sidebar ⌥ ⌘ B
- listitem:
- text: Search sessions
- button "Remove shortcut for Search sessions"
- button "Edit shortcut for Search sessions"
- button "Search sessions ⌥ ⌘ K": ⌥ ⌘ K
- text: Search sessions ⌥ ⌘ K
- listitem:
- text: Add workspace
- button "Remove shortcut for Add workspace"
- button "Edit shortcut for Add workspace"
- button "Add workspace ⌥ ⌘ O": ⌥ ⌘ O
- text: Add workspace ⌥ ⌘ O
- listitem:
- text: Rename session
- button "Remove shortcut for Rename session"
- button "Edit shortcut for Rename session"
- button "Rename session ⇧ ⌘ R": ⇧ ⌘ R
- text: Rename session ⇧ ⌘ R
- listitem:
- text: Fork session
- button "Remove shortcut for Fork session"
- button "Edit shortcut for Fork session"
- button "Fork session ⇧ ⌘ F": ⇧ ⌘ F
- text: Fork session ⇧ ⌘ F
- listitem:
- text: Archive session
- button "Remove shortcut for Archive session"
- button "Edit shortcut for Archive session"
- button "Archive session ⌥ ⌘ A": ⌥ ⌘ A
- text: Archive session ⌥ ⌘ A
- listitem:
- text: Open settings
- button "Remove shortcut for Open settings"
- button "Edit shortcut for Open settings"
- button "Open settings ⌘ ,": ⌘ ,
- text: Open settings ⌘ ,
- listitem:
- text: Toggle right sidebar
- button "Remove shortcut for Toggle right sidebar"
- button "Edit shortcut for Toggle right sidebar"
- button "Toggle right sidebar ⇧ ⌘ B": ⇧ ⌘ B
- text: Toggle right sidebar ⇧ ⌘ B
- listitem:
- text: Workspace files
- button "Remove shortcut for Workspace files"
- button "Edit shortcut for Workspace files"
- button "Workspace files ⌥ ⌘ P": ⌥ ⌘ P
- text: Workspace files ⌥ ⌘ P
- listitem:
- text: New terminal
- button "Remove shortcut for New terminal"
- button "Edit shortcut for New terminal"
- 'button "New terminal ⌃ `"': "⌃ `"
- text: "New terminal ⌃ `"
- listitem:
- text: Split
- button "Remove shortcut for Split"
- button "Edit shortcut for Split"
- button "Split ⌘ \\": ⌘ \
- text: Split ⌘ \
- listitem:
- text: Toggle panel fullscreen
- button "Remove shortcut for Toggle panel fullscreen"
- button "Edit shortcut for Toggle panel fullscreen"
- button "Toggle panel fullscreen ⌥ ⌘ Enter": ⌥ ⌘ Enter
- text: Toggle panel fullscreen ⌥ ⌘ Enter
- listitem:
- text: Refresh current page
- button "Edit shortcut for Refresh current page"
- button "Refresh current page No shortcut": No shortcut
- text: Refresh current page No shortcut
- listitem:
- text: Close current page or window
- button "Remove shortcut for Close current page or window"
- button "Edit shortcut for Close current page or window"
- button "Close current page or window ⌥ ⌘ W": ⌥ ⌘ W
- listitem:
- text: Stop generating
- button "Stop generating Esc Esc" [disabled]: Esc Esc
- text: Close current page or window ⌥ ⌘ W
- region "Message input":
- heading "Message input" [level=3]
- list:
- listitem:
- text: Use the complementary Queue / Steer action
- button "Use the complementary Queue / Steer action ⌘ Enter" [disabled]: ⌘ Enter
- listitem:
- text: Open reference menu
- button "Open reference menu @" [disabled]: "@"
- listitem:
- text: New line
- button "New line ⇧ Enter" [disabled]: ⇧ Enter
- listitem:
- text: Send message
- button "Send message Enter" [disabled]: Enter
- listitem:
- text: Open command menu
- button "Open command menu /" [disabled]: /
- listitem: Use the complementary Queue / Steer action ⌘ Enter
- listitem: Open reference menu @
- listitem: New line ⇧ Enter
- listitem: Send message Enter
- listitem: Open command menu /
- listitem: Stop generating Esc Esc
- region "Menus and dialogs":
- heading "Menus and dialogs" [level=3]
- list:
- listitem:
- text: Close menu or top dialog
- button "Close menu or top dialog Esc" [disabled]: Esc
- listitem:
- text: Move menu selection
- button "Move menu selection ↑ ↓" [disabled]: ↑ ↓
- listitem:
- text: Select menu item
- button "Select menu item Enter" [disabled]: Enter
- listitem: Close menu or top dialog Esc
- listitem: Move menu selection ↑ ↓
- listitem: Select menu item Enter
- region "Approval area":
- heading "Approval area" [level=3]
- list:
- listitem:
- text: Allow once
- button "Allow once Enter" [disabled]: Enter
- listitem:
- text: Reject
- button "Reject Esc" [disabled]: Esc
- listitem: Allow once Enter
- listitem: Reject Esc
- contentinfo:
- button "Restore all defaults" [disabled]
- text: 0 customized
@@ -4,6 +4,7 @@
- button "Close keyboard shortcuts"
- search "Search shortcuts":
- searchbox "Search shortcuts": abc
- button "Clear search"
- status: No matching shortcuts
- contentinfo:
- button "Restore all defaults"
@@ -7,127 +7,72 @@
- region "Application":
- list:
- listitem:
- text: Open keyboard shortcuts
- button "Remove shortcut for Open keyboard shortcuts"
- button "Edit shortcut for Open keyboard shortcuts"
- button "Open keyboard shortcuts Ctrl + /": Ctrl + /
- text: Open keyboard shortcuts Ctrl + /
- listitem:
- text: New Session
- button "Remove shortcut for New Session"
- button "Edit shortcut for New Session"
- button "New Session Ctrl + Alt + N": Ctrl + Alt + N
- text: New Session Ctrl + Alt + N
- listitem:
- text: Toggle left sidebar
- button "Remove shortcut for Toggle left sidebar"
- button "Edit shortcut for Toggle left sidebar"
- button "Toggle left sidebar Ctrl + Alt + B": Ctrl + Alt + B
- text: Toggle left sidebar Ctrl + Alt + B
- listitem:
- text: Search sessions
- button "Remove shortcut for Search sessions"
- button "Edit shortcut for Search sessions"
- button "Search sessions Ctrl + Alt + K": Ctrl + Alt + K
- text: Search sessions Ctrl + Alt + K
- listitem:
- text: Add workspace
- button "Remove shortcut for Add workspace"
- button "Edit shortcut for Add workspace"
- button "Add workspace Ctrl + Alt + O": Ctrl + Alt + O
- text: Add workspace Ctrl + Alt + O
- listitem:
- text: Rename session
- button "Remove shortcut for Rename session"
- button "Edit shortcut for Rename session"
- button "Rename session Ctrl + Shift + R": Ctrl + Shift + R
- text: Rename session Ctrl + Shift + R
- listitem:
- text: Fork session
- button "Remove shortcut for Fork session"
- button "Edit shortcut for Fork session"
- button "Fork session Ctrl + Shift + F": Ctrl + Shift + F
- text: Fork session Ctrl + Shift + F
- listitem:
- text: Archive session
- button "Remove shortcut for Archive session"
- button "Edit shortcut for Archive session"
- button "Archive session Ctrl + Alt + A": Ctrl + Alt + A
- text: Archive session Ctrl + Alt + A
- listitem:
- text: Open settings
- button "Remove shortcut for Open settings"
- button "Edit shortcut for Open settings"
- button "Open settings Ctrl + ,": Ctrl + ,
- text: Open settings Ctrl + ,
- listitem:
- text: Toggle right sidebar
- button "Remove shortcut for Toggle right sidebar"
- button "Edit shortcut for Toggle right sidebar"
- button "Toggle right sidebar Ctrl + Shift + B": Ctrl + Shift + B
- text: Toggle right sidebar Ctrl + Shift + B
- listitem:
- text: Workspace files
- button "Remove shortcut for Workspace files"
- button "Edit shortcut for Workspace files"
- button "Workspace files Ctrl + Alt + P": Ctrl + Alt + P
- text: Workspace files Ctrl + Alt + P
- listitem:
- text: New terminal
- button "Remove shortcut for New terminal"
- button "Edit shortcut for New terminal"
- 'button "New terminal Ctrl + `"': "Ctrl + `"
- text: "New terminal Ctrl + `"
- listitem:
- text: Split
- button "Remove shortcut for Split"
- button "Edit shortcut for Split"
- button "Split Ctrl + \\": Ctrl + \
- text: Split Ctrl + \
- listitem:
- text: Toggle panel fullscreen
- button "Remove shortcut for Toggle panel fullscreen"
- button "Edit shortcut for Toggle panel fullscreen"
- button "Toggle panel fullscreen Ctrl + Alt + Enter": Ctrl + Alt + Enter
- text: Toggle panel fullscreen Ctrl + Alt + Enter
- listitem:
- text: Refresh current page
- button "Remove shortcut for Refresh current page"
- button "Edit shortcut for Refresh current page"
- button "Refresh current page Ctrl + Alt + R": Ctrl + Alt + R
- text: Refresh current page Ctrl + Alt + R
- listitem:
- text: Close current page or window
- button "Remove shortcut for Close current page or window"
- button "Edit shortcut for Close current page or window"
- button "Close current page or window Ctrl + Alt + W": Ctrl + Alt + W
- listitem:
- text: Stop generating
- button "Stop generating Esc Esc" [disabled]: Esc Esc
- text: Close current page or window Ctrl + Alt + W
- region "Message input":
- heading "Message input" [level=3]
- list:
- listitem:
- text: Use the complementary Queue / Steer action
- button "Use the complementary Queue / Steer action Ctrl + Enter" [disabled]: Ctrl + Enter
- listitem:
- text: Open reference menu
- button "Open reference menu @" [disabled]: "@"
- listitem:
- text: New line
- button "New line Shift + Enter" [disabled]: Shift + Enter
- listitem:
- text: Send message
- button "Send message Enter" [disabled]: Enter
- listitem:
- text: Open command menu
- button "Open command menu /" [disabled]: /
- listitem: Use the complementary Queue / Steer action Ctrl + Enter
- listitem: Open reference menu @
- listitem: New line Shift + Enter
- listitem: Send message Enter
- listitem: Open command menu /
- listitem: Stop generating Esc Esc
- region "Menus and dialogs":
- heading "Menus and dialogs" [level=3]
- list:
- listitem:
- text: Close menu or top dialog
- button "Close menu or top dialog Esc" [disabled]: Esc
- listitem:
- text: Move menu selection
- button "Move menu selection ↑ ↓" [disabled]: ↑ ↓
- listitem:
- text: Select menu item
- button "Select menu item Enter" [disabled]: Enter
- listitem: Close menu or top dialog Esc
- listitem: Move menu selection ↑ ↓
- listitem: Select menu item Enter
- region "Approval area":
- heading "Approval area" [level=3]
- list:
- listitem:
- text: Allow once
- button "Allow once Enter" [disabled]: Enter
- listitem:
- text: Reject
- button "Reject Esc" [disabled]: Esc
- listitem: Allow once Enter
- listitem: Reject Esc
- contentinfo:
- button "Restore all defaults" [disabled]
- text: 0 customized
@@ -4,6 +4,7 @@
- button "关闭快捷键"
- search "搜索快捷键":
- searchbox "搜索快捷键": abc
- button "清空搜索"
- status: 没有匹配的快捷键
- contentinfo:
- button "恢复全部默认"
@@ -7,126 +7,72 @@
- region "应用操作":
- list:
- listitem:
- text: 快捷键速查
- button "移除快捷键速查快捷键"
- button "修改快捷键速查快捷键"
- button "快捷键速查 ⌘ /": ⌘ /
- text: 快捷键速查 ⌘ /
- listitem:
- text: 新会话
- button "移除新会话快捷键"
- button "修改新会话快捷键"
- button "新会话 ⌥ ⌘ N": ⌥ ⌘ N
- text: 新会话 ⌥ ⌘ N
- listitem:
- text: 展开/收起左侧栏
- button "移除展开/收起左侧栏快捷键"
- button "修改展开/收起左侧栏快捷键"
- button "展开/收起左侧栏 ⌥ ⌘ B": ⌥ ⌘ B
- text: 展开/收起左侧栏 ⌥ ⌘ B
- listitem:
- text: 搜索会话
- button "移除搜索会话快捷键"
- button "修改搜索会话快捷键"
- button "搜索会话 ⌥ ⌘ K": ⌥ ⌘ K
- text: 搜索会话 ⌥ ⌘ K
- listitem:
- text: 添加工作区
- button "移除添加工作区快捷键"
- button "修改添加工作区快捷键"
- button "添加工作区 ⌥ ⌘ O": ⌥ ⌘ O
- text: 添加工作区 ⌥ ⌘ O
- listitem:
- text: 重命名会话
- button "移除重命名会话快捷键"
- button "修改重命名会话快捷键"
- button "重命名会话 ⇧ ⌘ R": ⇧ ⌘ R
- text: 重命名会话 ⇧ ⌘ R
- listitem:
- text: 分叉会话
- button "移除分叉会话快捷键"
- button "修改分叉会话快捷键"
- button "分叉会话 ⇧ ⌘ F": ⇧ ⌘ F
- text: 分叉会话 ⇧ ⌘ F
- listitem:
- text: 归档会话
- button "移除归档会话快捷键"
- button "修改归档会话快捷键"
- button "归档会话 ⌥ ⌘ A": ⌥ ⌘ A
- text: 归档会话 ⌥ ⌘ A
- listitem:
- text: 打开设置
- button "移除打开设置快捷键"
- button "修改打开设置快捷键"
- button "打开设置 ⌘ ,": ⌘ ,
- text: 打开设置 ⌘ ,
- listitem:
- text: 展开/收起右侧栏
- button "移除展开/收起右侧栏快捷键"
- button "修改展开/收起右侧栏快捷键"
- button "展开/收起右侧栏 ⇧ ⌘ B": ⇧ ⌘ B
- text: 展开/收起右侧栏 ⇧ ⌘ B
- listitem:
- text: 工作区文件
- button "移除工作区文件快捷键"
- button "修改工作区文件快捷键"
- button "工作区文件 ⌥ ⌘ P": ⌥ ⌘ P
- text: 工作区文件 ⌥ ⌘ P
- listitem:
- text: 新建终端
- button "移除新建终端快捷键"
- button "修改新建终端快捷键"
- 'button "新建终端 ⌃ `"': "⌃ `"
- text: "新建终端 ⌃ `"
- listitem:
- text: 分栏
- button "移除分栏快捷键"
- button "修改分栏快捷键"
- button "分栏 ⌘ \\": ⌘ \
- text: 分栏 ⌘ \
- listitem:
- text: 面板全屏/退出全屏
- button "移除面板全屏/退出全屏快捷键"
- button "修改面板全屏/退出全屏快捷键"
- button "面板全屏/退出全屏 ⌥ ⌘ Enter": ⌥ ⌘ Enter
- text: 面板全屏/退出全屏 ⌥ ⌘ Enter
- listitem:
- text: 刷新当前页面
- button "修改刷新当前页面快捷键"
- button "刷新当前页面 暂无快捷键": 暂无快捷键
- text: 刷新当前页面 暂无快捷键
- listitem:
- text: 关闭当前页面/窗口
- button "移除关闭当前页面/窗口快捷键"
- button "修改关闭当前页面/窗口快捷键"
- button "关闭当前页面/窗口 ⌥ ⌘ W": ⌥ ⌘ W
- listitem:
- text: 停止生成
- button "停止生成 Esc Esc" [disabled]: Esc Esc
- text: 关闭当前页面/窗口 ⌥ ⌘ W
- region "消息输入":
- heading "消息输入" [level=3]
- list:
- listitem:
- text: 使用互补的 Queue/Steer 方式
- button "使用互补的 Queue/Steer 方式 ⌘ Enter" [disabled]: ⌘ Enter
- listitem:
- text: 打开引用菜单
- button "打开引用菜单 @" [disabled]: "@"
- listitem:
- text: 换行
- button "换行 ⇧ Enter" [disabled]: ⇧ Enter
- listitem:
- text: 发送消息
- button "发送消息 Enter" [disabled]: Enter
- listitem:
- text: 打开命令菜单
- button "打开命令菜单 /" [disabled]: /
- listitem: 使用互补的 Queue/Steer 方式 ⌘ Enter
- listitem: 打开引用菜单 @
- listitem: 换行 ⇧ Enter
- listitem: 发送消息 Enter
- listitem: 打开命令菜单 /
- listitem: 停止生成 Esc Esc
- region "菜单与弹层":
- heading "菜单与弹层" [level=3]
- list:
- listitem:
- text: 关闭菜单或顶层弹窗
- button "关闭菜单或顶层弹窗 Esc" [disabled]: Esc
- listitem:
- text: 移动菜单选择
- button "移动菜单选择 ↑ ↓" [disabled]: ↑ ↓
- listitem:
- text: 选择菜单项
- button "选择菜单项 Enter" [disabled]: Enter
- listitem: 关闭菜单或顶层弹窗 Esc
- listitem: 移动菜单选择 ↑ ↓
- listitem: 选择菜单项 Enter
- region "审批区域":
- heading "审批区域" [level=3]
- list:
- listitem:
- text: 允许一次
- button "允许一次 Enter" [disabled]: Enter
- listitem:
- text: 拒绝
- button "拒绝 Esc" [disabled]: Esc
- listitem: 允许一次 Enter
- listitem: 拒绝 Esc
- contentinfo:
- button "恢复全部默认" [disabled]
- text: 0 项已自定义
@@ -1 +1 @@
Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description](<path/to/image.png>) when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present when a separate file card helps the user open the complete deliverable, including images, Office documents, spreadsheets, and slide decks. Each presented file adds a card below the reply, with preview and native-open actions. Usually select the 1-2 most important deliverables; include more when needed, but at most 4 files in a single present call. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.
Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description](<path/to/image.png>) when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present when a separate file card helps the user open the complete deliverable, including images, Office documents, spreadsheets, and slide decks. Each presented file adds a card below the reply, with preview and native-open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.
+1 -2
View File
@@ -181,8 +181,7 @@
"trust": "user",
"isDefault": false
}
],
"modeSelectionEnabled": true
]
}
},
"commands": {
@@ -0,0 +1,4 @@
# Browser-zone assertions need a fresh reading after the non-browser preparation turn.
- id: time-context
config:
refreshIntervalMs: 0
+15 -15
View File
@@ -94,7 +94,7 @@ describe('web e2e: plugin configuration pages', () => {
const official = panel.locator('[data-plugin-group="official"]')
expect(await official.locator('[data-plugin-package]').count()).toBe(OPTIONAL_BUNDLES.length)
expect(await official.locator('[data-plugin-item]').count()).toBe(4)
for (const title of ['终端', 'Agent 循环', 'Subagent', '网页搜索']) {
for (const title of ['终端', 'Agent 循环', '子智能体', '网页搜索']) {
expect(await official.getByRole('button', { name: `查看 ${title}`, exact: true }).count()).toBe(1)
}
// A card carries the one-liner; the fields wait for the page.
@@ -108,9 +108,9 @@ describe('web e2e: plugin configuration pages', () => {
it('saves subagent limits and resets them to the deployment defaults', async () => {
const panel = await openPlugins()
await openPage(panel, 'Subagent')
await openPage(panel, '子智能体')
const depth = panel.getByLabel('最大递归深度', { exact: true })
const capacity = panel.getByLabel('Subagent 并行数量上限', { exact: true })
const capacity = panel.getByLabel('子智能体并行数量上限', { exact: true })
expect(await depth.inputValue()).toBe('1')
expect(await capacity.inputValue()).toBe('8')
await depth.fill('2')
@@ -120,7 +120,7 @@ describe('web e2e: plugin configuration pages', () => {
await expect.poll(settingsDocument).toContain('maxActiveSubagents: 12')
await expect.poll(settingsDocument).toContain('maxDepth: 2')
await openPlugins()
await openPage(panel, 'Subagent')
await openPage(panel, '子智能体')
const snapshot = await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd)
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'subagent.expected.md'), snapshot, MODE)
const controlHeight = await depth.evaluate(element => element.getBoundingClientRect().height)
@@ -133,7 +133,7 @@ describe('web e2e: plugin configuration pages', () => {
await panel.getByRole('button', { name: '保存', exact: true }).click()
await expect.poll(() => panel.getByRole('button', { name: '保存', exact: true }).isDisabled()).toBe(true)
await openPlugins()
await openPage(panel, 'Subagent')
await openPage(panel, '子智能体')
expect(await depth.inputValue()).toBe('1')
expect(await capacity.inputValue()).toBe('8')
await panel.getByRole('button', { name: '返回插件列表', exact: true }).click()
@@ -141,7 +141,7 @@ describe('web e2e: plugin configuration pages', () => {
it('opens field explanations with the keyboard and retains unsaved edits', async () => {
const panel = await openPlugins()
await openPage(panel, 'Subagent')
await openPage(panel, '子智能体')
const depth = panel.getByLabel('最大递归深度', { exact: true })
await depth.fill('2')
const depthHelp = panel.getByRole('button', { name: '最大递归深度说明', exact: true })
@@ -149,27 +149,27 @@ describe('web e2e: plugin configuration pages', () => {
await depthHelp.press('Enter')
const depthRules = panel.getByRole('region', { name: '最大递归深度说明', exact: true })
await depthRules.waitFor()
expect(await depthRules.getByText('限制 Agent 创建 Subagent 的递归层级。', { exact: true }).count()).toBe(1)
expect(await depthRules.getByText('限制 Agent 创建子智能体的递归层级。', { exact: true }).count()).toBe(1)
const depthTable = depthRules.getByRole('table', { name: '最大递归深度说明', exact: true })
expect(await depthTable.getByRole('row', { name: '0 禁用 Subagent', exact: true }).count()).toBe(1)
expect(await depthTable.getByRole('row', { name: '1 仅允许主 Agent 创建 Subagent', exact: true }).count()).toBe(1)
expect(await depthTable.getByRole('row', { name: '0 禁用子智能体', exact: true }).count()).toBe(1)
expect(await depthTable.getByRole('row', { name: '1 仅允许主 Agent 创建子智能体', exact: true }).count()).toBe(1)
expect(await depthRules.getByText('如果某个工具单独设置了最大递归深度,以该工具的设置为准。', { exact: true }).count()).toBe(1)
await depthHelp.press('Enter')
expect(await depthRules.count()).toBe(0)
expect(await depth.inputValue()).toBe('2')
await panel.getByRole('button', { name: 'Subagent 并行数量上限说明', exact: true }).click()
const capacityRules = panel.getByRole('region', { name: 'Subagent 并行数量上限说明', exact: true })
expect(await capacityRules.getByText('同一主 Agent 下,所有递归层级同时存活的 Subagent 总数,主 Agent 不计入。达到上限时,新的启动请求会被拒绝。', { exact: true }).count()).toBe(1)
await panel.getByRole('button', { name: '子智能体并行数量上限说明', exact: true }).click()
const capacityRules = panel.getByRole('region', { name: '子智能体并行数量上限说明', exact: true })
expect(await capacityRules.getByText('同一主 Agent 下,所有递归层级同时存活的子智能体总数,主 Agent 不计入。达到上限时,新的启动请求会被拒绝。', { exact: true }).count()).toBe(1)
await panel.getByRole('button', { name: '返回插件列表', exact: true }).click()
await openPage(panel, 'Subagent')
await openPage(panel, '子智能体')
expect(await depth.inputValue()).toBe('1')
})
it('saves limits and the model allowlist together from the shared card', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-config-subagent-model-selection'))
const panel = await openPlugins()
await openPage(panel, 'Subagent')
const toggle = panel.getByRole('switch', { name: '允许 Agent 为 Subagent 选择模型' })
await openPage(panel, '子智能体')
const toggle = panel.getByRole('switch', { name: '允许 Agent 为子智能体选择模型' })
await panel.getByLabel('最大递归深度', { exact: true }).fill('2')
await toggle.click()
+259 -37
View File
@@ -15,6 +15,7 @@ import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import { TIMED_WAIT_PARAMETER } from '@deepseek-ai/dsh-user-questions'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
@@ -33,8 +34,12 @@ const COMPOSED_EXPECTED = join(SNAPSHOT_DIR, 'composed.expected.md')
const ANSWERED_EXPECTED = join(SNAPSHOT_DIR, 'answered.expected.md')
const CANCELLED_EXPECTED = join(SNAPSHOT_DIR, 'cancelled.expected.md')
const ANSWERED_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'answered-expanded.expected.md')
// The timed tool's second settlement: the wait expired, and the answer arrived
// afterwards. Its golden owns the state the reported symptom lands in.
const LATE_ANSWERED_EXPECTED = join(SNAPSHOT_DIR, 'late-answered.expected.md')
const MODE = webSnapshotMode()
const CANCELLED_SEED_ID = 'ask-question-cancelled-row-web-e2e'
const LATE_ANSWERED_SEED_ID = 'ask-question-late-answered-row-web-e2e'
// The composer's own growth cap, in text lines (QuestionComposer.module.css
// .fieldMirror). Asserted as TEXT lines, not as a box height: the two variants
@@ -70,8 +75,100 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
/** Replace the successful Tool settlement and omit the answer-dependent model step. */
function cancelledFixture(fixture: string): string {
/** The batch the derived late reply submits, distinct from the in-time answer. */
const LATE_ANSWERS = [{ id: 'color', selected: ['Green'], custom: 'Answered after the timeout' }]
/**
* The `ask_user_question` schema as the timed tool declares it, in the shape a
* request header records. A late reply can only follow a timed call, and the
* `userQuestions` projection tells a timed call from a legacy one by exactly
* this schema in the header in effect; the recorded fixture's header carries
* the placeholder token of a legacy recording, so the derived fixture must
* carry the timed schema for its call to be tracked at all.
*/
const TIMED_ASK_USER_QUESTION_SCHEMA = {
name: 'ask_user_question',
description: 'Ask brief, direct, self-contained questions about missing information, preferences, or decisions.',
parameters: {
type: 'object',
properties: {
questions: { type: 'array', items: { type: 'object' } },
[TIMED_WAIT_PARAMETER]: { type: 'integer' },
},
required: ['questions'],
},
}
/**
* What the timed tool records when its foreground wait expires. Both readers of
* a recorded result take `pending` and `callId` out of this one JSON object —
* the `userQuestions` projection to keep the call answerable, the question row
* to tell a timeout from an answer batch; the `message` is the model's own
* continuation instruction and no reader parses it.
* @param callId - the call whose questions stay answerable.
* @returns the recorded result text.
*/
function pendingResultText(callId: string): string {
return JSON.stringify({
pending: true,
callId,
message: 'No answer batch arrived before the timeout. This is pending, not a skipped answer.',
})
}
/**
* The message a late answer lands as: the timed question's `callId` names it,
* and it carries both the questions it answers and the batch itself.
* @param callId - the call the reply settles.
* @param questions - the call's questions, echoed from its recorded arguments.
* @returns the reply, as the inbox splice and the durable message both record it.
*/
function lateReplyMessage(callId: string, questions: unknown): Record<string, unknown> {
return {
content: [{
type: 'text',
text: JSON.stringify({
kind: 'answer_to_pending_question', tool: 'ask_user_question', callId, questions, answers: LATE_ANSWERS,
}),
}],
source: { kind: 'user-question-reply', callId, outcome: 'answered' },
role: 'user',
id: '{{message:7}}',
}
}
/**
* The turn a late reply opens: the reply enters the agent inbox, the next turn
* claims it, and it lands as this Session's durable user message.
* @param callId - the call the reply settles.
* @param questions - the call's questions, echoed from its recorded arguments.
* @returns the appended fixture lines, ending in the closing `turn/end`.
*/
function lateReplyTail(callId: string, questions: unknown): string[] {
const message = lateReplyMessage(callId, questions)
return [
{ type: 'agent/inbox/spliced', data: { target: 'next-turn', start: 0, inserted: [message] } },
{ type: 'turn/start', data: { turn: 2 } },
{ type: 'agent/inbox/spliced', data: { target: 'next-turn', start: 0, removedCount: 1, inserted: [] } },
{ type: 'step/start', data: { turn: 2, step: 1 } },
{ type: 'user/message', data: message, surfaceOp: 'append' },
{ type: 'step/end', data: { turn: 2, step: 1 } },
{ type: 'turn/end', data: { turn: 2, reason: { kind: 'completed' } } },
].map(event => JSON.stringify(event))
}
/**
* Derive one settlement other than the recorded answer batch. The model step
* that follows the Tool settlement reads the answer, so every derivation drops
* it and keeps the recorded turn closed.
* @param fixture - the recorded round trip.
* @param outcome - `cancelled` settles the call as the user's own dismissal;
* `late-answered` settles it as the timed tool's expired wait and appends the
* reply that answers it a turn later.
* @returns the derived fixture text.
*/
function resettledFixture(fixture: string, outcome: 'cancelled' | 'late-answered'): string {
let asked: { callId: string; questions: unknown } | undefined
let replaced = false
const lines: string[] = []
for (const line of fixture.trimEnd().split('\n')) {
@@ -90,6 +187,26 @@ function cancelledFixture(fixture: string): string {
|| event.type === 'turn/end') lines.push(line)
continue
}
if (event.type === 'request/header' && outcome === 'late-answered') {
// The recording was made under the blocking legacy tool; the derived
// timed settlement needs the header that tool's timed sibling records.
const data = event.data
if (!isRecord(data) || !isRecord(data.header)) throw new Error('question fixture request/header is invalid')
data.header.tools = [TIMED_ASK_USER_QUESTION_SCHEMA]
lines.push(JSON.stringify(event))
continue
}
if (event.type === 'tool/call') {
const data = event.data
if (!isRecord(data) || typeof data.callId !== 'string' || typeof data.arguments !== 'string') {
throw new Error('question fixture tool/call is invalid')
}
const args: unknown = JSON.parse(data.arguments)
if (!isRecord(args)) throw new Error('question fixture tool/call arguments are invalid')
asked = { callId: data.callId, questions: args.questions }
lines.push(line)
continue
}
if (event.type !== 'tool/result') {
lines.push(line)
continue
@@ -100,20 +217,58 @@ function cancelledFixture(fixture: string): string {
if (!isRecord(message) || !Array.isArray(message.content) || !isRecord(message.content[0])) {
throw new Error('question fixture tool/result message is invalid')
}
message.content[0].content = [{
type: 'text',
text: 'Error: the user cancelled ask_user_question',
}]
message.content[0].isError = true
data.error = {
name: 'UserQuestionError',
code: 'ASK_CANCELLED',
if (asked === undefined) throw new Error('question fixture settles a call it never made')
if (outcome === 'cancelled') {
message.content[0].content = [{
type: 'text',
text: 'Error: the user cancelled ask_user_question',
}]
message.content[0].isError = true
data.error = {
name: 'UserQuestionError',
code: 'ASK_CANCELLED',
}
} else {
message.content[0].content = [{ type: 'text', text: pendingResultText(asked.callId) }]
}
replaced = true
lines.push(JSON.stringify(event))
}
if (!replaced) throw new Error('question fixture has no tool/result event')
return `${lines.join('\n')}\n`
if (!replaced || asked === undefined) throw new Error('question fixture has no settled ask_user_question call')
const tail = outcome === 'cancelled' ? [] : lateReplyTail(asked.callId, asked.questions)
return `${[...lines, ...tail].join('\n')}\n`
}
/** One derived transcript, seeded into its own scaffold and opened in the browser. */
interface SeededTranscript {
readonly scaffold: WebScaffold
readonly browser: Browser
readonly page: Page
readonly tripwire: ReturnType<typeof watchConsole>
}
/**
* Seed one derived fixture and open its Session in a fresh browser.
* @param fixtureText - the derived fixture to persist.
* @param seedId - the seeded Session id, which its golden tokenizes.
* @returns the scaffold, browser, page, and console tripwire teardown closes.
*/
async function openSeededTranscript(fixtureText: string, seedId: string): Promise<SeededTranscript> {
const scaffold = await launchWebScaffold({})
await seedSession(scaffold, fixtureText, seedId)
const browser = await chromium.launch()
const page = await newEnglishPage(browser)
const tripwire = watchConsole(page)
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
const groupRow = page.locator('[role="treeitem"]').first()
await groupRow.waitFor({ timeout: 15_000 })
await groupRow.click()
const sessionRow = page.locator('[role="treeitem"]').nth(1)
await sessionRow.waitFor({ timeout: 10_000 })
await sessionRow.click()
return { scaffold, browser, page, tripwire }
}
describe('web e2e: resident question composer round trip', () => {
@@ -252,6 +407,15 @@ describe('web e2e: resident question composer round trip', () => {
expect(await blue.getAttribute('aria-checked')).toBe('true')
expect(await custom.inputValue()).toBe('Include accessibility notes')
if (MODE !== 'record') {
// Replay only, like the measurements above: record mode must reach the
// recording write. This fixture composes the shipped legacy tool, whose
// request carries no call id and whose row offers no pill: the projection
// never lists a legacy call, so the transcript reads exactly as it did
// before timed questions existed. Reopening a hidden panel belongs to the
// timed tool; the late-answered scenario below and the ui-user-questions
// unit specs cover it.
expect(await page.getByRole('button', { name: 'Answer', exact: true }).count()).toBe(0)
// A strict Session-slot switch remounts the composer. Open a fresh blank
// Session, then return to the still-waiting request and require its
// Session-scoped store to restore both option and free-text drafts.
@@ -302,12 +466,15 @@ describe('web e2e: resident question composer round trip', () => {
// Keep the ask_user_question card's readable answer in the expanded golden
// even though Compact mode hides the process by default.
await expandTurnProcesses(page)
// A legacy call's answered row carries no pill and no read-only panel; its
// accessible name is the bare summary, as before timed questions existed.
const answeredRow = page.getByRole('button', { name: 'Ask question 1/1 answered', exact: true })
await answeredRow.click()
await page.getByText('Which color do you prefer?', { exact: true }).waitFor({ timeout: 10_000 })
expect(await page.getByText('Blue', { exact: true }).count()).toBeGreaterThanOrEqual(1)
expect(await page.getByText('Include accessibility notes', { exact: true }).count()).toBe(1)
expect(await page.getByText(/"answers"/).count()).toBe(0)
expect(await page.getByRole('button', { name: 'View answers', exact: true }).count()).toBe(0)
const expanded = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(ANSWERED_EXPANDED_EXPECTED, expanded, MODE)
expect(tripwire.pageErrors).toEqual([])
@@ -358,38 +525,22 @@ describe('web e2e: resident question composer round trip', () => {
})
describe.skipIf(MODE === 'record')('web e2e: cancelled question transcript', () => {
let cancelledScaffold: WebScaffold
let cancelledBrowser: Browser
let cancelledPage: Page
let cancelledTripwire: ReturnType<typeof watchConsole>
let seeded: SeededTranscript
beforeAll(async () => {
cancelledScaffold = await launchWebScaffold({})
await seedSession(
cancelledScaffold,
cancelledFixture(await readFile(FIXTURE, 'utf8')),
seeded = await openSeededTranscript(
resettledFixture(await readFile(FIXTURE, 'utf8'), 'cancelled'),
CANCELLED_SEED_ID,
)
cancelledBrowser = await chromium.launch()
cancelledPage = await newEnglishPage(cancelledBrowser)
cancelledTripwire = watchConsole(cancelledPage)
await cancelledPage.goto(cancelledScaffold.authenticatedUrl, { waitUntil: 'load' })
await cancelledPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
const groupRow = cancelledPage.locator('[role="treeitem"]').first()
await groupRow.waitFor({ timeout: 15_000 })
await groupRow.click()
const sessionRow = cancelledPage.locator('[role="treeitem"]').nth(1)
await sessionRow.waitFor({ timeout: 10_000 })
await sessionRow.click()
}, 120_000)
afterAll(async () => {
await cancelledBrowser?.close()
await cancelledScaffold?.close()
await seeded?.browser.close()
await seeded?.scaffold.close()
})
it('expands to the cancellation verdict and original questions', async () => {
const { page: cancelledPage, scaffold, tripwire } = seeded
onTestFailed(() => saveFailureShot(cancelledPage, 'web-e2e-question-cancelled-row'))
await expandTurnProcesses(cancelledPage)
const row = cancelledPage.getByRole('button', { name: 'Ask question cancelled', exact: true })
@@ -407,11 +558,81 @@ describe.skipIf(MODE === 'record')('web e2e: cancelled question transcript', ()
const snapshot = (await captureStableAria(
cancelledPage,
'[class*="centerCol"]',
cancelledScaffold.workspaceCwd,
scaffold.workspaceCwd,
)).split(CANCELLED_SEED_ID).join('{{seededId}}')
await compareOrRefreshGolden(CANCELLED_EXPECTED, snapshot, MODE)
expect(cancelledTripwire.pageErrors).toEqual([])
expect(cancelledTripwire.warnings).toEqual([])
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
})
// The timed tool's own second settlement. The recorded result is the expired
// wait, so the answers live in the reply that landed a turn later and reach the
// row only through the `userQuestions` projection — the whole chain the reported
// symptom broke on, where an answered question kept no way back to its answers.
describe.skipIf(MODE === 'record')('web e2e: late-answered question transcript', () => {
let seeded: SeededTranscript
beforeAll(async () => {
seeded = await openSeededTranscript(
resettledFixture(await readFile(FIXTURE, 'utf8'), 'late-answered'),
LATE_ANSWERED_SEED_ID,
)
}, 120_000)
afterAll(async () => {
await seeded?.browser.close()
await seeded?.scaffold.close()
})
it('reads a late answer as an answered question and reopens its answers', async () => {
const { page: latePage, scaffold, tripwire } = seeded
onTestFailed(() => saveFailureShot(latePage, 'web-e2e-question-late-answered-row'))
const reply = latePage.locator('[data-question-reply]')
await reply.waitFor({ state: 'attached', timeout: 15_000 })
expect(await reply.count()).toBe(1)
expect(await latePage.locator('[data-chat-flow-kind="turn-trigger"]').count()).toBe(0)
expect(await reply.isVisible()).toBe(false)
// Verbatim the accessible name of the in-time answered row above: whether
// the answer beat the timeout is the agent's pacing, not something a reader
// of the row has to reason about.
await expandTurnProcesses(latePage)
await reply.waitFor({ state: 'visible', timeout: 15_000 })
const row = latePage.getByRole('button', { name: 'Ask question 1/1 answered View answers', exact: true })
await row.waitFor({ timeout: 15_000 })
// The pill owns its own clicks — press near the leading glyph so this stays
// the row's disclosure.
await row.click({ position: { x: 5, y: 5 } })
await latePage.getByText('Which color do you prefer?', { exact: true }).waitFor({ timeout: 10_000 })
expect(await latePage.getByText('Green', { exact: true }).count()).toBeGreaterThanOrEqual(1)
expect(await latePage.getByText('Answered after the timeout', { exact: true }).count()).toBeGreaterThanOrEqual(1)
// The recorded timeout is the row's state, never its transcript copy.
expect(await latePage.getByText(/"pending"/).count()).toBe(0)
// The same read-only panel an in-time answer reopens, over the answers the
// late reply carried.
await latePage.getByRole('button', { name: 'View answers', exact: true }).click()
const review = latePage.locator('[data-question-key]')
await review.waitFor({ timeout: 10_000 })
await review.getByText('Answered', { exact: true }).waitFor({ timeout: 10_000 })
const recorded = review.getByRole('checkbox', { name: 'Green' })
expect(await recorded.getAttribute('aria-checked')).toBe('true')
expect(await recorded.isDisabled()).toBe(true)
expect(await review.getByRole('checkbox', { name: 'Blue' }).getAttribute('aria-checked')).toBe('false')
const recordedCustom = review.getByRole('textbox')
expect(await recordedCustom.inputValue()).toBe('Answered after the timeout')
expect(await recordedCustom.isDisabled()).toBe(true)
expect(await review.getByRole('button', { name: 'Submit' }).count()).toBe(0)
expect(await review.getByRole('button', { name: 'Skip' }).count()).toBe(0)
const snapshot = (await captureStableAria(
latePage,
'[class*="centerCol"]',
scaffold.workspaceCwd,
)).split(LATE_ANSWERED_SEED_ID).join('{{seededId}}')
await compareOrRefreshGolden(LATE_ANSWERED_EXPECTED, snapshot, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 60_000)
it('keeps the fixture inventory closed', async () => {
@@ -423,6 +644,7 @@ describe.skipIf(MODE === 'record')('web e2e: cancelled question transcript', ()
'answered.expected.md',
'cancelled.expected.md',
'answered-expanded.expected.md',
'late-answered.expected.md',
])
})
})
+1 -1
View File
@@ -734,7 +734,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
patches: [],
}
}))
const profile: Profile = {
const profile: Profile = { skippedBundles: [],
name: 'scaffold',
dir: profileDir,
layers: extraLayers,
+3 -1
View File
@@ -271,7 +271,9 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
const everyAdapter = new EveryReminderAdapter()
beforeAll(async () => {
scaffold = await launchWebScaffold()
scaffold = await launchWebScaffold({
extraOverlayPath: fileURLToPath(new URL('./fixtures/time-context-every-step.patch.yml', import.meta.url)),
})
scaffold.ctx.effect(
() => scaffold.ctx.llm.registerAdapter([AFTER_PROVIDER], afterAdapter),
'Schedule Web After adapter',
+16 -3
View File
@@ -23,9 +23,9 @@ async function bind(page: Page, primary: string, label: string, previous?: strin
expect(await row.getByText('Unavailable', { exact: true }).count()).toBe(0)
}
if (previous !== undefined) {
const remove = dialog.getByRole('button', { name: `Remove shortcut for ${previous}`, exact: true })
await remove.click()
await remove.waitFor({ state: 'hidden' })
await dialog.getByRole('button', { name: `Edit shortcut for ${previous}`, exact: true }).click()
await dialog.getByRole('button', { name: 'Remove', exact: true }).click()
await dialog.getByRole('group').waitFor({ state: 'hidden' })
}
await dialog.getByRole('button', { name: `Edit shortcut for ${label}`, exact: true }).click()
const recorder = dialog.getByRole('group', { name: label, exact: true })
@@ -125,6 +125,19 @@ describe.skipIf(process.platform === 'win32')('Web sidebar shortcuts', () => {
await terminalGuide.focus()
expect(await page.getByRole('tooltip').count()).toBe(0)
const terminalEntry = panel.locator('[data-sidebar-right-guide-entry="terminal"]')
const shellMenu = terminalEntry.getByRole('button', { name: 'Choose shell', exact: true })
const titleBounds = await terminalEntry.getByText('New terminal', { exact: true }).boundingBox()
const arrowBounds = await shellMenu.boundingBox()
expect(arrowBounds!.x).toBeGreaterThanOrEqual(titleBounds!.x + titleBounds!.width)
expect(arrowBounds!.x - titleBounds!.x - titleBounds!.width).toBeLessThan(8)
await shellMenu.click()
await page.getByRole('menu').waitFor()
expect(await panel.locator('[data-sidebar-terminal]').count()).toBe(0)
await page.keyboard.press('Escape')
await page.getByRole('menu').waitFor({ state: 'detached' })
expect(await shellMenu.evaluate(element => element === document.activeElement)).toBe(true)
await bind(page, primary, 'Browser', 'Toggle right sidebar')
const browserGuide = panel.locator('[data-sidebar-right-guide-entry="browser"]')
await browserGuide.hover()
+3 -2
View File
@@ -61,8 +61,9 @@ describe.skipIf(mode === 'record')('web e2e: workspace shortcuts', () => {
expect(await row.getByText('Unavailable', { exact: true }).count()).toBe(0)
}
if (previous !== undefined) {
await reference.getByRole('button', { name: `Remove shortcut for ${previous}`, exact: true }).click()
await expect.poll(() => reference.getByRole('button', { name: `Remove shortcut for ${previous}`, exact: true }).count()).toBe(0)
await reference.getByRole('button', { name: `Edit shortcut for ${previous}`, exact: true }).click()
await reference.getByRole('button', { name: 'Remove', exact: true }).click()
await reference.getByRole('group').waitFor({ state: 'hidden' })
}
await reference.getByRole('button', { name: `Edit shortcut for ${label}`, exact: true }).click()
await page.keyboard.press('Meta+Shift+Comma')
+97 -38
View File
@@ -19,8 +19,8 @@ describe('web e2e: shortcut reference', () => {
afterAll(async () => { await browser?.close(); await scaffold?.close() })
it.each([
{ locale: 'zh-CN', platform: 'MacIntel', title: '快捷键', settings: '设置', view: '查看快捷键', search: '搜索快捷键', key: 'Meta' },
{ locale: 'en-US', platform: 'Win32', title: 'Keyboard shortcuts', settings: 'Settings', view: 'View shortcuts', search: 'Search shortcuts', key: 'Control' },
{ locale: 'zh-CN', platform: 'MacIntel', title: '快捷键', settings: '设置', view: '编辑快捷键', search: '搜索快捷键', key: 'Meta' },
{ locale: 'en-US', platform: 'Win32', title: 'Keyboard shortcuts', settings: 'Settings', view: 'Edit shortcuts', search: 'Search shortcuts', key: 'Control' },
])('opens, searches, and restores nested focus in $locale', async ({ locale, platform, title, settings, view, search, key }) => {
const referenceKey = platform === 'Win32' ? 'Control+Slash' : 'Meta+Slash'
const context = await browser.newContext({ locale, colorScheme: 'light', viewport: { width: 1440, height: 1000 } })
@@ -46,14 +46,13 @@ describe('web e2e: shortcut reference', () => {
const resetAll = dialog.getByRole('button', { name: locale === 'zh-CN' ? '恢复全部默认' : 'Restore all defaults', exact: true })
expect(await resetAll.isDisabled()).toBe(true)
expect(await dialog.boundingBox()).toMatchObject({ width: 480, height: 600 })
expect(await dialog.getByRole('search').boundingBox()).toMatchObject({ width: 432, height: 36 })
expect(await dialog.getByRole('search').boundingBox()).toMatchObject({ width: 440, height: 36 })
const rows = dialog.getByRole('listitem')
const stopRow = dialog.getByRole('region', { name: locale === 'zh-CN' ? '应用操作' : 'Application', exact: true })
const stopRow = dialog.getByRole('region', { name: locale === 'zh-CN' ? '消息输入' : 'Message input', exact: true })
.getByRole('listitem').last()
await stopRow.getByText(locale === 'zh-CN' ? '停止生成' : 'Stop generating', { exact: true }).waitFor()
const stopButtons = stopRow.getByRole('button')
expect(await stopButtons.count()).toBe(1)
expect(await stopButtons.isDisabled()).toBe(true)
expect(await stopButtons.count()).toBe(0)
expect(new Set(await rows.evaluateAll(items => items.map(row => row.getBoundingClientRect().height))))
.toEqual(new Set([42]))
expect(await rows.evaluateAll(items => items.every(row => row.getBoundingClientRect().height >= 42
@@ -72,29 +71,22 @@ describe('web e2e: shortcut reference', () => {
const dialogShadow = await dialog.evaluate(element => getComputedStyle(element).boxShadow)
await compareOrRefreshGolden(join(expected, `${locale}.expected.md`),
await captureStableAria(page, '[data-shortcut-modal="shortcuts"]', scaffold.workspaceCwd), mode)
const fixedButtons = dialog.getByRole('button', { disabled: true })
const fixedButton = fixedButtons.first()
expect(await fixedButton.evaluate(element => getComputedStyle(element).opacity)).toBe('0.45')
const fixedBackground = await fixedButton.locator('span').evaluate(element => getComputedStyle(element).backgroundColor)
await fixedButton.hover()
expect(await fixedButton.locator('span').evaluate(element => getComputedStyle(element).backgroundColor)).toBe(fixedBackground)
const fixedBackground = await stopRow.evaluate(element => getComputedStyle(element).backgroundColor)
await stopRow.hover()
expect(await stopRow.evaluate(element => getComputedStyle(element).backgroundColor)).toBe(fixedBackground)
const editLabel = locale === 'zh-CN' ? '修改展开/收起左侧栏快捷键' : 'Edit shortcut for Toggle left sidebar'
const removeLabel = locale === 'zh-CN' ? '移除展开/收起左侧栏快捷键' : 'Remove shortcut for Toggle left sidebar'
const row = dialog.getByRole('listitem').filter({ has: page.getByRole('button', { name: editLabel, exact: true }) })
const editButton = page.getByRole('button', { name: editLabel, exact: true })
expect(await editButton.evaluate(element => getComputedStyle(element.parentElement!).opacity)).toBe('0')
expect(await row.locator('[class*="rowActions"]').evaluate(element => getComputedStyle(element).opacity)).toBe('0')
const boundRow = dialog.getByRole('listitem').filter({ has: page.getByText(
locale === 'zh-CN' ? '快捷键速查' : 'Open keyboard shortcuts', { exact: true },
) })
const badge = boundRow.getByRole('button').last()
const badgeColor = () => badge.locator('span').evaluate(element => getComputedStyle(element).backgroundColor)
const restingColor = await badgeColor()
await boundRow.locator(':scope > span').first().hover()
expect(await badgeColor()).toBe(restingColor)
await badge.hover()
expect(await badgeColor()).toBe('rgba(38, 49, 72, 0.14)')
await boundRow.getByRole('button').first().hover()
expect(await badgeColor()).toBe(restingColor)
const badge = boundRow.getByRole('button', { name: locale === 'zh-CN' ? '修改快捷键速查快捷键' : 'Edit shortcut for Open keyboard shortcuts', exact: true })
const restingColor = await boundRow.evaluate(element => getComputedStyle(element).backgroundColor)
await badge.hover({ position: { x: 12, y: 12 } })
expect(await boundRow.evaluate(element => getComputedStyle(element).backgroundColor)).not.toBe(restingColor)
expect(await boundRow.evaluate(element => getComputedStyle(element).borderRadius)).toBe('12px')
expect(await badge.boundingBox()).toEqual(await boundRow.boundingBox())
await badge.click()
await boundRow.getByRole('group').waitFor()
expect(await searchSurfaceStyle()).toEqual(focusedSearchSurface)
@@ -104,7 +96,7 @@ describe('web e2e: shortcut reference', () => {
await page.keyboard.press('Escape')
await boundRow.getByRole('group').waitFor({ state: 'hidden' })
await dialog.getByRole('heading', { name: title, exact: true }).hover()
const boundActionsOpacity = () => boundRow.getByRole('button').first().evaluate(element => getComputedStyle(element.parentElement!).opacity)
const boundActionsOpacity = () => boundRow.locator('[class*="rowActions"]').evaluate(element => getComputedStyle(element).opacity)
expect(await boundActionsOpacity()).toBe('0')
expect(await dialog.evaluate(element => element === document.activeElement)).toBe(true)
expect(await dialog.evaluate(element => getComputedStyle(element).outlineStyle)).toBe('none')
@@ -118,14 +110,17 @@ describe('web e2e: shortcut reference', () => {
await dialog.getByRole('heading', { name: title, exact: true }).hover()
expect(await boundActionsOpacity()).toBe('0')
await row.hover()
expect(await editButton.evaluate(element => getComputedStyle(element.parentElement!).opacity)).toBe('1')
expect(await row.getByRole('button', { name: removeLabel, exact: true }).count()).toBe(1)
expect(await row.locator('[class*="rowActions"]').evaluate(element => getComputedStyle(element).opacity)).toBe('1')
expect(await row.getByRole('button').count()).toBe(1)
await editButton.click()
const inline = dialog.getByRole('group', { name: locale === 'zh-CN' ? '展开/收起左侧栏' : 'Toggle left sidebar', exact: true })
const recorder = inline.getByRole('button', { name: locale === 'zh-CN' ? '按下快捷键' : 'Press a shortcut', exact: true })
expect(await inline.getByRole('button', { name: locale === 'zh-CN' ? '移除' : 'Remove', exact: true }).count()).toBe(1)
expect(await page.getByRole('dialog').count()).toBe(2)
expect(await recorder.evaluate(element => getComputedStyle(element).color)).toBe('rgb(129, 133, 140)')
expect(await recorder.evaluate(element => ({
color: getComputedStyle(element).color, border: getComputedStyle(element).borderColor,
})))
.toEqual({ color: 'rgb(97, 102, 107)', border: 'rgba(0, 0, 0, 0.16)' })
expect(await recorder.evaluate(element => ({
focused: element === document.activeElement,
outline: getComputedStyle(element).outlineStyle, shadow: getComputedStyle(element).boxShadow,
@@ -155,7 +150,7 @@ describe('web e2e: shortcut reference', () => {
expect(await dialog.evaluate(element => getComputedStyle(element).boxShadow)).toBe(dialogShadow)
expect(await editButton.evaluate(element => element.matches(':focus-visible'))).toBe(false)
await dialog.getByRole('heading', { name: title, exact: true }).hover()
expect(await editButton.evaluate(element => getComputedStyle(element.parentElement!).opacity)).toBe('0')
expect(await row.locator('[class*="rowActions"]').evaluate(element => getComputedStyle(element).opacity)).toBe('0')
await page.keyboard.press('Shift+Tab')
expect(await resetAll.evaluate(element => element === document.activeElement)).toBe(true)
expect(await resetAll.evaluate(element => getComputedStyle(element).outlineStyle)).not.toBe('none')
@@ -168,12 +163,14 @@ describe('web e2e: shortcut reference', () => {
}))).toEqual({ background: 'rgb(67, 69, 74)', color: 'rgb(255, 255, 255)' })
await page.emulateMedia({ colorScheme: 'light' })
await expect.poll(() => successToast.evaluate(element => getComputedStyle(element).backgroundColor)).toBe('rgb(53, 54, 56)')
await page.getByRole('button', { name: removeLabel, exact: true }).click()
await editButton.click()
await inline.getByRole('button', { name: locale === 'zh-CN' ? '移除' : 'Remove', exact: true }).click()
await inline.waitFor({ state: 'hidden' })
const unbound = row.getByText(locale === 'zh-CN' ? '暂无快捷键' : 'No shortcut', { exact: true })
await unbound.waitFor()
expect(await unbound.evaluate(element => getComputedStyle(element).color)).toBe('rgb(129, 133, 140)')
expect(await row.getByRole('button', { name: removeLabel, exact: true }).count()).toBe(0)
await unbound.click()
expect(await row.getByRole('button').count()).toBe(1)
await editButton.click({ position: { x: 12, y: 12 } })
await inline.waitFor()
expect(await inline.getByRole('button', { name: locale === 'zh-CN' ? '移除' : 'Remove', exact: true }).count()).toBe(0)
await page.keyboard.press('Escape')
@@ -182,7 +179,7 @@ describe('web e2e: shortcut reference', () => {
expect(await dialog.evaluate(element => getComputedStyle(element).outlineStyle)).toBe('none')
expect(await dialog.evaluate(element => getComputedStyle(element).boxShadow)).toBe(dialogShadow)
await dialog.getByRole('heading', { name: title, exact: true }).hover()
expect(await editButton.evaluate(element => getComputedStyle(element.parentElement!).opacity)).toBe('0')
expect(await row.getByRole('button').count()).toBe(1)
expect(await page.getByRole('dialog').count()).toBe(2)
for (const query of ['abc', 'sendEnter']) {
await page.getByRole('searchbox').fill(query)
@@ -245,11 +242,13 @@ describe('web e2e: shortcut reference', () => {
await page.setViewportSize({ width: 1440, height: 1000 })
await openSettings(page, locale === 'zh-CN' ? 'zh' : 'en')
const viewButton = page.getByRole('button', { name: view, exact: true })
const viewHint = `${view} ${key === 'Meta' ? '⌘ /' : 'Ctrl + /'}`
const viewHint = `${locale === 'zh-CN' ? '全局唤起查看' : 'Open from anywhere'} ${key === 'Meta' ? '⌘ /' : 'Ctrl + /'}`
await viewButton.hover()
expect(await page.getByRole('tooltip').textContent()).toBe(viewHint)
expect(await page.getByRole('tooltip').getAttribute('aria-label')).toBe(viewHint)
await viewButton.click()
await boundRow.getByRole('button').first().click()
await badge.click()
await boundRow.getByRole('button', { name: locale === 'zh-CN' ? '移除' : 'Remove', exact: true }).click()
await boundRow.getByRole('group').waitFor({ state: 'hidden' })
await boundRow.getByText(locale === 'zh-CN' ? '暂无快捷键' : 'No shortcut', { exact: true }).waitFor()
await page.keyboard.press('Escape')
await dialog.waitFor({ state: 'hidden' })
@@ -266,7 +265,7 @@ describe('web e2e: shortcut reference', () => {
await page.keyboard.press('Escape')
await dialog.waitFor({ state: 'hidden' })
await viewButton.hover()
expect(await page.getByRole('tooltip').textContent()).toBe(viewHint)
expect(await page.getByRole('tooltip').getAttribute('aria-label')).toBe(viewHint)
await viewButton.click()
await page.getByRole('searchbox').fill('abc')
expect(await dialog.getByRole('listitem').count()).toBe(0)
@@ -286,7 +285,7 @@ describe('web e2e: shortcut reference', () => {
await confirm.getByRole('button', { name: locale === 'zh-CN' ? '恢复默认' : 'Restore default', exact: true }).click()
await confirm.waitFor({ state: 'hidden' })
expect(await resetAll.isDisabled()).toBe(true)
expect(await dialog.locator('footer').textContent()).toContain(locale === 'zh-CN' ? '0 项已自定义' : '0 customized')
expect(await dialog.locator('footer').textContent()).toBe(locale === 'zh-CN' ? '恢复全部默认' : 'Restore all defaults')
expect(await page.getByRole('searchbox').evaluate(element => element === document.activeElement)).toBe(true)
expect(await searchInput.evaluate(element => getComputedStyle(element).outlineStyle)).toBe('none')
expect(await searchSurfaceStyle()).toEqual(focusedSearchSurface)
@@ -322,6 +321,65 @@ describe('web e2e: shortcut reference', () => {
} finally { await context.close() }
})
it.each([
{ platform: 'Win32', primary: 'Control', keys: 'Ctrl + Alt + N' },
{ platform: 'MacIntel', primary: 'Meta', keys: '⌥ ⇧ ⌘ F12' },
])('shows the shortcut inside New Session on hover and keyboard focus at wide and narrow widths on $platform', async ({ platform, primary, keys }) => {
const context = await browser.newContext({ locale: 'en-US', viewport: { width: 1440, height: 1000 } })
try {
await context.addInitScript((value) => { Object.defineProperty(navigator, 'platform', { value }) }, platform)
const page = await context.newPage()
await page.goto(scaffold.authenticatedUrl)
const newSession = page.getByRole('button', { name: 'New session', exact: true }).filter({ hasText: 'New Session' })
await newSession.waitFor()
if (platform === 'MacIntel') await page.evaluate(() => { document.documentElement.dataset.platform = 'darwin' })
await page.keyboard.press(`${primary}+/`)
await page.getByRole('button', { name: 'Edit shortcut for New Session', exact: true }).click()
await page.keyboard.press(platform === 'Win32' ? 'Control+Alt+N' : 'Meta+Alt+Shift+F12')
await page.getByRole('group', { name: 'New Session', exact: true }).waitFor({ state: 'hidden' })
await page.keyboard.press('Escape')
for (const colorScheme of ['light', 'dark'] as const) {
await page.emulateMedia({ colorScheme })
for (const width of [420, 264]) {
const handle = (await page.locator('[data-side="sidebar"]').boundingBox())!
await page.mouse.move(handle.x + handle.width / 2, 240)
await page.mouse.down()
await page.mouse.move(width, 240, { steps: 10 })
await page.mouse.up()
await expect.poll(() => newSession.locator('kbd').allTextContents()).toEqual(keys.split(' '))
const centerOffset = () => newSession.evaluate((element) => {
const button = element.getBoundingClientRect()
const content = element.querySelector('[class*="newSessionContent"]')!.getBoundingClientRect()
return Math.abs(button.x + button.width / 2 - content.x - content.width / 2)
})
expect(await centerOffset()).toBeLessThan(1)
const hintStyle = () => newSession.locator('[class*="newSessionShortcut"]').evaluate(element => ({
opacity: getComputedStyle(element).opacity, mask: getComputedStyle(element.previousElementSibling!).maskImage,
}))
expect(await hintStyle()).toEqual({ opacity: '0', mask: 'none' })
await newSession.hover()
expect(await centerOffset()).toBeLessThan(1)
const mask = await newSession.locator('[class*="newSessionLabelMask"]').boundingBox()
const hint = await newSession.locator('[class*="newSessionShortcut"]').boundingBox()
expect(mask!.x + mask!.width).toBeLessThanOrEqual(hint!.x + 1)
expect((await hintStyle()).opacity).toBe('1')
expect((await hintStyle()).mask).toContain('linear-gradient')
await newSession.screenshot({ path: fileURLToPath(new URL(`../../../.artifacts/new-session-${process.pid}-${platform}-${colorScheme}-${width}.png`, import.meta.url)) })
expect(await page.getByRole('tooltip').count()).toBe(0)
await page.mouse.move(700, 100)
expect(await hintStyle()).toEqual({ opacity: '0', mask: 'none' })
await newSession.focus()
await page.keyboard.press('Shift+Tab')
await page.keyboard.press('Tab')
expect(await newSession.evaluate(element => element.matches(':focus-visible'))).toBe(true)
expect((await hintStyle()).opacity).toBe('1')
expect(await page.getByRole('tooltip').count()).toBe(0)
await newSession.evaluate((element) => { element.blur() })
}
}
} finally { await context.close() }
})
it.each([
{ platform: 'Win32', modifier: 'Control', defaultBinding: 'Control+Alt+B' },
{ platform: 'MacIntel', modifier: 'Meta', defaultBinding: 'Alt+Meta+B' },
@@ -371,7 +429,8 @@ describe('web e2e: shortcut reference', () => {
await other.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await other.getByRole('button', { name: 'Settings', exact: true }).waitFor()
await other.keyboard.press(referenceKey)
await other.getByRole('button', { name: 'Remove shortcut for Toggle left sidebar', exact: true }).click()
await other.getByRole('button', { name: 'Edit shortcut for Toggle left sidebar', exact: true }).click()
await other.getByRole('button', { name: 'Remove', exact: true }).click()
await editor.getByText('Shortcut configuration or available commands changed. Review the latest bindings before saving.', { exact: true }).waitFor()
await page.bringToFront()
await editor.getByRole('button', { name: 'Press a shortcut', exact: true }).focus()
+3 -2
View File
@@ -1176,8 +1176,9 @@ describe('web e2e: shipped right Sidebar', () => {
}
expect(await terminalEntry.evaluate(node => getComputedStyle(node).overflow)).toBe('hidden')
const terminalActions = terminalEntry.getByRole('button')
for (const action of await terminalActions.all()) {
expect(await action.evaluate(node => getComputedStyle(node).borderRadius)).toBe('0px')
const terminalButtons = await terminalActions.all()
for (const [index, action] of terminalButtons.entries()) {
expect(await action.evaluate(node => getComputedStyle(node).borderRadius)).toBe(index === 0 ? '0px' : '4px')
await action.hover()
expect(await action.evaluate(node => getComputedStyle(node).backgroundColor)).not.toBe('rgba(0, 0, 0, 0)')
}
+3 -2
View File
@@ -341,9 +341,10 @@ describe.skipIf(process.platform === 'win32')('Web sidebar terminal', () => {
const entry = page.locator('[data-sidebar-right-guide-entry="terminal"]')
await page.locator('[data-sidebar-right-guide]').screenshot({ path: `${shots}/terminal-guide.png`, animations: 'disabled' })
const selector = entry.getByRole('button', { name: 'Choose shell', exact: true })
const cardBox = (await entry.boundingBox())!
const titleBox = (await entry.getByText('New terminal', { exact: true }).boundingBox())!
const triggerBox = (await selector.boundingBox())!
expect(Math.abs(cardBox.x + cardBox.width - triggerBox.x - triggerBox.width)).toBeLessThanOrEqual(2)
expect(triggerBox.x).toBeGreaterThanOrEqual(titleBox.x + titleBox.width)
expect(triggerBox.x - titleBox.x - titleBox.width).toBeLessThan(8)
await page.emulateMedia({ colorScheme: 'dark' })
await selector.click()
await page.getByRole('menuitem', { name: 'bash', exact: true }).waitFor()
+61 -3
View File
@@ -5,7 +5,7 @@ import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { chromium, type Browser } from 'playwright'
import { expect, it, onTestFinished, vi } from 'vitest'
import type { SpeechInput, SpeechProviderId } from '@deepseek-ai/dsh-experimental-speech-to-text/types'
import type { SpeechInput, SpeechPreparationState, SpeechProviderId } from '@deepseek-ai/dsh-experimental-speech-to-text/types'
import type {} from '@deepseek-ai/dsh-experimental-speech-to-text'
import {
captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, launchWebScaffold, webSnapshotMode, watchConsole,
@@ -19,7 +19,7 @@ const interruptedExpected = fileURLToPath(new URL('../../../snapshots/web/voice-
const recordingExpected = fileURLToPath(new URL('../../../snapshots/web/voice-input/recording.expected.md', import.meta.url))
const bundle = fileURLToPath(new URL('../../../packages/experimental/voice-input-bundle', import.meta.url))
it.skipIf(webSnapshotMode() === 'record')('records from cached standby and submits only the reviewed transcript through Session replay', async () => {
it.skipIf(webSnapshotMode() === 'record')('guides voice setup, records from standby and submits only the reviewed transcript through Session replay', async () => {
const scratch = await mkdtemp(join(tmpdir(), 'dsh-voice-browser-'))
const resources: { scaffold?: WebScaffold; browser?: Browser } = {}
onTestFinished(async () => {
@@ -40,11 +40,14 @@ it.skipIf(webSnapshotMode() === 'record')('records from cached standby and submi
expect(input.audio.byteLength).toBeGreaterThan(44)
return { text: prompt.slice(prefix.length), audioSeconds: 1, inferenceSeconds: 0.1 }
})
let preparation: SpeechPreparationState = { phase: 'unprepared' }
const readinessListeners = new Set<() => void>()
await scaffold.ctx.plugin({ inject: ['speechToText'], apply(ctx) {
ctx.effect(() => ctx.speechToText.register({
info: { id: 'sensevoice-local' as SpeechProviderId, name: 'Recorded recognizer', location: 'host-local', languages: ['auto'] },
preparation: {
snapshot: () => ({ phase: 'standby' }), subscribe: () => () => {},
snapshot: () => preparation,
subscribe: (listener) => { readinessListeners.add(listener); return () => { readinessListeners.delete(listener) } },
prepare: () => { throw new Error('Cached resources must allow recording without preparation') },
cancel: async () => {},
},
@@ -68,6 +71,61 @@ it.skipIf(webSnapshotMode() === 'record')('records from cached standby and submi
await page.goto(scaffold.authenticatedUrl)
await connectFreshWorkspace(page, scaffold.workspaceCwd)
const input = page.locator('[data-composer-input]'), mic = page.getByRole('button', { name: 'Start recording', exact: true })
const setupMic = page.getByRole('button', { name: 'Open voice input setup', exact: true })
await input.fill(prefix)
await setupMic.click()
const setup = page.getByRole('dialog', { name: 'Set up voice input before recording', exact: true })
await setup.waitFor()
const setupAction = setup.getByRole('button', { name: 'Go to setup', exact: true })
await expect.poll(() => setupAction.evaluate(element => element === document.activeElement)).toBe(true)
await page.keyboard.type('NoDraftEdits')
expect(await input.innerText()).toBe(prefix)
expect(messages).toBe(0)
await page.keyboard.press('Tab')
expect(await setup.getByRole('button', { name: 'Cancel', exact: true }).evaluate(element => element === document.activeElement)).toBe(true)
await page.keyboard.press('Shift+Tab')
expect(await setupAction.evaluate(element => element === document.activeElement)).toBe(true)
await compareOrRefreshGolden(fileURLToPath(new URL('../../../snapshots/web/voice-input/setup.expected.md', import.meta.url)),
await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd), webSnapshotMode())
await setup.getByRole('button', { name: 'Later', exact: true }).click()
expect(await setup.count()).toBe(0)
expect(await input.evaluate(element => element === document.activeElement)).toBe(true)
expect(recognize).not.toHaveBeenCalled()
preparation = { phase: 'checking', step: 'check', startedAt: 0 }
for (const listener of readinessListeners) listener()
await setupMic.click()
const unavailable = page.getByRole('dialog', { name: 'Speech recognition is not ready', exact: true })
await unavailable.waitFor()
await compareOrRefreshGolden(fileURLToPath(new URL('../../../snapshots/web/voice-input/unavailable.expected.md', import.meta.url)),
await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd), webSnapshotMode())
await page.setViewportSize({ width: 420, height: 900 })
const dialogBox = await unavailable.boundingBox()
expect(dialogBox!.x).toBeGreaterThanOrEqual(0)
expect(dialogBox!.x + dialogBox!.width).toBeLessThanOrEqual(420)
await page.keyboard.press('Escape')
await unavailable.waitFor({ state: 'hidden' })
expect(await input.evaluate(element => element === document.activeElement)).toBe(true)
expect(await input.innerText()).toBe(prefix)
preparation = { phase: 'failed', message: 'Model preparation failed' }
for (const listener of readinessListeners) listener()
await setupMic.click()
const detailsAction = unavailable.getByRole('button', { name: 'Open voice plugin settings', exact: true })
await expect.poll(() => detailsAction.evaluate(element => element === document.activeElement)).toBe(true)
await page.keyboard.press('Enter')
await page.setViewportSize({ width: 1280, height: 900 })
await page.locator('[data-plugin-detail="@deepseek-ai/dsh-experimental-voice-input-bundle"]')
.getByRole('button', { name: 'Retry preparation', exact: true }).waitFor()
expect(await unavailable.count()).toBe(0)
expect(messages).toBe(0)
expect(recognize).not.toHaveBeenCalled()
preparation = { phase: 'standby' }
for (const listener of readinessListeners) listener()
await page.getByRole('button', { name: 'New session', exact: true }).filter({ hasText: 'New Session' }).click()
await mic.waitFor()
await page.getByRole('button', { name: 'Plugins', exact: true }).click()
await page.locator('[data-plugin-package="@deepseek-ai/dsh-experimental-voice-input-bundle"]').waitFor()
expect(await page.locator('[data-plugin-detail]').count()).toBe(0)
await page.getByRole('button', { name: 'New session', exact: true }).filter({ hasText: 'New Session' }).click()
await mic.waitFor()
const micBox = await mic.boundingBox(), modelBox = await page.getByRole('button', { name: /^Select model, current/ }).boundingBox()
const sendBox = await page.getByRole('button', { name: 'Send message', exact: true }).boundingBox()
+28 -6
View File
@@ -148,10 +148,8 @@ export type Config = PresetDefinition
export interface Config {
/** Deployment default when the caller omits a preset. */
default: string
/** User-selected default while the chooser is shown; edited through Settings. */
/** User-selected default; edited through Settings. */
selectedDefault: Volatile<string | undefined>
/** Whether new-session surfaces expose preset selection and the saved default applies. */
modeSelectionEnabled: Volatile<boolean>
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-agent-preset-registry -->
@@ -2538,13 +2536,19 @@ export interface JsonRpcConfig {
## `@deepseek-ai/dsh-session-log-deepseek`
- `inject`: `deepseekLlmApiExtensions` · `sessions`
- `source`: [`packages/session/session-log-deepseek/src/index.ts:38`](../packages/session/session-log-deepseek/src/index.ts)
- `source`: [`packages/session/session-log-deepseek/src/index.ts:39`](../packages/session/session-log-deepseek/src/index.ts)
```ts config-catalog
/** Session-log request contribution configuration. */
export interface Config {
/** Contribute `dsh_session_log` to official DeepSeek requests. Defaults to `true`. */
enabled?: boolean
/**
* Largest serialized `dsh_session_log` field, in UTF-8 bytes, that one request carries.
* A request uploads the longest pending event prefix that fits; later requests continue
* after its acceptance. Defaults to 8 MiB.
*/
maxBytes?: number
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-session-log-deepseek -->
@@ -3383,7 +3387,7 @@ export type ShellDialect = 'bash' | 'pwsh'
export interface Config {
/** Fallback display zone when the open turn has no unique browser zone. Omit to use the process zone. */
timeZone?: string
/** Minimum milliseconds between durable injections in one session. Omit or set to 0 to inject at every eligible step. */
/** Minimum milliseconds between durable injections in one session. Defaults to 600000 (10 minutes); 0 injects at every eligible step. */
refreshIntervalMs?: number
}
```
@@ -3420,6 +3424,25 @@ export type TokenMeterConfig = Record<string, never>
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-token-meter -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-ask-user -->
<a id="deepseek-aidsh-tool-ask-user"></a>
## `@deepseek-ai/dsh-tool-ask-user`
- `inject`: `tools` · `userQuestions`
- `source`: [`packages/interaction/tool-ask-user/src/index.ts:19`](../packages/interaction/tool-ask-user/src/index.ts)
```ts config-catalog
/** Default wait in seconds; -1 retains the blocking tool behavior. */
export interface Config {
/** Tool definition selected by this Cordis row. Defaults to the blocking legacy tool. */
mode?: 'legacy' | 'timed'
/** Foreground wait before automatic continuation. Defaults to 120 seconds. */
timeout?: number
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-ask-user -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-bash -->
<a id="deepseek-aidsh-tool-bash"></a>
@@ -4362,7 +4385,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
| `@deepseek-ai/dsh-subprocess-local` | — | [`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts) |
| `@deepseek-ai/dsh-subprocess-ssh` | `ssh` | [`packages/ssh/subprocess-ssh/src/index.ts`](../packages/ssh/subprocess-ssh/src/index.ts) |
| `@deepseek-ai/dsh-terminal` | — | [`packages/terminal/terminal/src/index.ts`](../packages/terminal/terminal/src/index.ts) |
| `@deepseek-ai/dsh-tool-ask-user` | `tools` · `userQuestions` | [`packages/interaction/tool-ask-user/src/index.ts`](../packages/interaction/tool-ask-user/src/index.ts) |
| `@deepseek-ai/dsh-tool-call-timeout-policy` | `tools` | [`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts) |
| `@deepseek-ai/dsh-tool-cordis` | `tools` · `cordisInspect` | [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts) |
| `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) |
+28 -6
View File
@@ -150,10 +150,8 @@ export type Config = PresetDefinition
export interface Config {
/** Deployment default when the caller omits a preset. */
default: string
/** User-selected default while the chooser is shown; edited through Settings. */
/** User-selected default; edited through Settings. */
selectedDefault: Volatile<string | undefined>
/** Whether new-session surfaces expose preset selection and the saved default applies. */
modeSelectionEnabled: Volatile<boolean>
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-agent-preset-registry -->
@@ -2540,13 +2538,19 @@ export interface JsonRpcConfig {
## `@deepseek-ai/dsh-session-log-deepseek`
- `inject`: `deepseekLlmApiExtensions` · `sessions`
- `source`: [`packages/session/session-log-deepseek/src/index.ts:38`](../packages/session/session-log-deepseek/src/index.ts)
- `source`: [`packages/session/session-log-deepseek/src/index.ts:39`](../packages/session/session-log-deepseek/src/index.ts)
```ts config-catalog
/** Session-log request contribution configuration. */
export interface Config {
/** Contribute `dsh_session_log` to official DeepSeek requests. Defaults to `true`. */
enabled?: boolean
/**
* Largest serialized `dsh_session_log` field, in UTF-8 bytes, that one request carries.
* A request uploads the longest pending event prefix that fits; later requests continue
* after its acceptance. Defaults to 8 MiB.
*/
maxBytes?: number
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-session-log-deepseek -->
@@ -3385,7 +3389,7 @@ export type ShellDialect = 'bash' | 'pwsh'
export interface Config {
/** Fallback display zone when the open turn has no unique browser zone. Omit to use the process zone. */
timeZone?: string
/** Minimum milliseconds between durable injections in one session. Omit or set to 0 to inject at every eligible step. */
/** Minimum milliseconds between durable injections in one session. Defaults to 600000 (10 minutes); 0 injects at every eligible step. */
refreshIntervalMs?: number
}
```
@@ -3422,6 +3426,25 @@ export type TokenMeterConfig = Record<string, never>
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-token-meter -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-ask-user -->
<a id="deepseek-aidsh-tool-ask-user"></a>
## `@deepseek-ai/dsh-tool-ask-user`
- `inject`: `tools` · `userQuestions`
- `source`: [`packages/interaction/tool-ask-user/src/index.ts:19`](../packages/interaction/tool-ask-user/src/index.ts)
```ts config-catalog
/** Default wait in seconds; -1 retains the blocking tool behavior. */
export interface Config {
/** Tool definition selected by this Cordis row. Defaults to the blocking legacy tool. */
mode?: 'legacy' | 'timed'
/** Foreground wait before automatic continuation. Defaults to 120 seconds. */
timeout?: number
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-ask-user -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-bash -->
<a id="deepseek-aidsh-tool-bash"></a>
@@ -4364,7 +4387,6 @@ export interface Config {
| `@deepseek-ai/dsh-subprocess-local` | — | [`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts) |
| `@deepseek-ai/dsh-subprocess-ssh` | `ssh` | [`packages/ssh/subprocess-ssh/src/index.ts`](../packages/ssh/subprocess-ssh/src/index.ts) |
| `@deepseek-ai/dsh-terminal` | — | [`packages/terminal/terminal/src/index.ts`](../packages/terminal/terminal/src/index.ts) |
| `@deepseek-ai/dsh-tool-ask-user` | `tools` · `userQuestions` | [`packages/interaction/tool-ask-user/src/index.ts`](../packages/interaction/tool-ask-user/src/index.ts) |
| `@deepseek-ai/dsh-tool-call-timeout-policy` | `tools` | [`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts) |
| `@deepseek-ai/dsh-tool-cordis` | `tools` · `cordisInspect` | [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts) |
| `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) |
@@ -12,14 +12,14 @@
en: 740e92e903ecb594
zh: f934d3c9e5c56bfe
/official-deepseek-llm-api-wire-extensions/body-extension-transaction:
en: 8c05b525a3f1358e
zh: 4c5bccf12f000038
en: f0b1443c364a431c
zh: 16d16ddf5c451e8e
/official-deepseek-llm-api-wire-extensions/dsh-plugin-packages:
en: 49d6f011e9d985a7
zh: bbf61b4e4480c555
/official-deepseek-llm-api-wire-extensions/dsh-session-log:
en: 5dc19f72ee3db8ab
zh: df9997b805cd01fa
en: de1a3f2c377d74e3
zh: 7c803345f71117c5
/official-deepseek-llm-api-wire-extensions/dsh-session-log/wire-session-header:
en: ccd8fcd773df21c4
zh: 7ea020068ad093ae
@@ -27,8 +27,8 @@
en: 008a21df5e314154
zh: 8855710a37542ec5
/official-deepseek-llm-api-wire-extensions/dsh-session-log/acceptance-watermark-and-at-least-once-delivery:
en: 64ba792a2a6af122
zh: 0f560c3610e07e81
en: 4d84cb1e61f9cf58
zh: 8a63ce3069e2f0b3
/official-deepseek-llm-api-wire-extensions/exposure-and-receiver-requirements:
en: 9000a8295ff540ac
zh: b56802d83a04175d
en: ae93d9a44245b12f
zh: 8b95fc66c6e653c2
+6 -6
View File
@@ -34,7 +34,7 @@ Credential failure happens before anonymous-user-id resolution, so an unauthoriz
The adapter serializes the complete base body, including the exact `messages`, before it asks registered providers to prepare fields. A provider receives that immutable body, the request cancellation signal, and optional `sessionId` and auxiliary-call `purpose`. Returning `undefined` omits that provider's field for the request.
Prepared JSON values are detached from provider-owned state, merged as top-level siblings of the base fields, and serialized in the same HTTP body. Preparation or collision failure prevents the request. A composition without the registry sends the unextended base body.
Prepared JSON values are detached from provider-owned state, merged as top-level siblings of the base fields, and serialized in the same HTTP body. Preparation or collision failure prevents the request. If the merged body fails to serialize, the adapter sends the base body without any extension field, skips the acceptance transaction so contributors resend their state on a later request, and logs the omitted field names. A composition without the registry sends the unextended base body.
After the configured endpoint returns HTTP 2xx, the adapter runs the prepared `accept()` transaction before reading the SSE response body. Transport failures and non-2xx responses do not accept any contribution. An acceptance failure fails the model request even though the endpoint returned 2xx. Acceptance records endpoint-level HTTP success; it does not assert that an SSE stream completed or that the endpoint persisted an extension.
@@ -73,7 +73,7 @@ An enabled inventory with no qualifying entries sends `packages: []`; disabling
## `dsh_session_log`
[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.md) contributes one contiguous suffix of the canonical Session log. The field is enabled by default. It applies to a request with a live Session and at least one event; a direct request, a stale Session id, or an empty log omits the field, and a composition disables it with `enabled: false`. The examples below use logical Session format 2 only to illustrate the wire fields; they do not identify the [current writer format](session-format-status.md).
[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.md) contributes one contiguous suffix of the canonical Session log. The field is enabled by default. It applies to a request with a live Session and at least one event; a direct request, a stale Session id, or an empty log omits the field, as does a request whose first pending event alone exceeds `maxBytes` or cannot be serialized; a composition disables it with `enabled: false`. The examples below use logical Session format 2 only to illustrate the wire fields; they do not identify the [current writer format](session-format-status.md).
```json
{
@@ -110,7 +110,7 @@ An enabled inventory with no qualifying entries sends `packages: []`; disabling
| `throughSeq` | non-negative integer | Greatest sequence represented by this request |
| `events` | array | Contiguous events from `afterSeq + 1` through `throughSeq` |
The first upload uses `afterSeq: -1` and carries the complete current log. Each later upload starts after the greatest accepted watermark for the same Session id. The sender snapshots the event array once per request; appends after that snapshot belong to a later request.
The first upload uses `afterSeq: -1`, and each later upload starts after the greatest accepted watermark for the same Session id. Each upload carries the longest contiguous run from that point whose serialized field fits `maxBytes`, 8 MiB by default and counted in UTF-8 bytes including the header and numeric fields, so a backlog above the limit spans several accepted requests. The sender snapshots the event array once per request; appends after that snapshot belong to a later request.
### Wire Session header
@@ -151,12 +151,12 @@ After the endpoint returns HTTP 2xx, the contribution appends this canonical eve
`delivery-accepted` means that the configured endpoint returned HTTP 2xx for the containing LLM request. It does not assert SSE completion or remote persistence. The event's `throughSeq` must identify an earlier event, its `sessionId` identifies the Session whose suffix was sent, and `sessionFormatVersion` binds the watermark to that exact logical generation. Absence means historical format v0.
The sender folds the greatest matching `throughSeq` for the current Session id and format generation, so concurrent accepted requests cannot move the cursor backward and a watermark from another generation cannot authorize the current suffix. A resumed process rebuilds the cursor from the durable log. A fork ignores inherited watermarks that name another Session, and therefore sends its own complete inherited prefix before advancing under the child id. The watermark event itself belongs to the next unsent suffix.
The sender folds the greatest matching `throughSeq` for the current Session id and format generation, so concurrent accepted requests cannot move the cursor backward and a watermark from another generation cannot authorize the current suffix. A resumed process rebuilds the cursor from the durable log. A fork ignores inherited watermarks that name another Session, and therefore uploads its own inherited prefix from sequence zero, in requests bounded by `maxBytes`, before advancing under the child id. The watermark event itself belongs to the next unsent suffix.
Transport and non-2xx failures append no watermark. A crash after endpoint acceptance but before local persistence may resend an already accepted range; uncertainty produces duplicates, never a sequence gap. There is no independent upload store, size cap, or truncation path.
Transport failures, non-2xx responses, and requests sent without extension fields after a serialization failure append no watermark. A crash after endpoint acceptance but before local persistence may resend an already accepted range; uncertainty produces duplicates, never a sequence gap. There is no independent upload store or truncation path; `maxBytes` bounds each request's field instead.
## Exposure and receiver requirements
The request headers expose the Harness application version, one anonymous Harness-home identity, and an optional Session identity. `dsh_plugin_packages` exposes active npm package names and versions. Unless a composition disables it, `dsh_session_log` may expose the Session working directory, system-prompt snapshots, user and Assistant content, embedded Assistant streams, failed-attempt output, tool arguments and results, compaction summaries, feedback, and plugin-owned events. Adapter API keys are not Session events and therefore do not enter the field. A gateway selected through `baseURL` receives the same values as the official endpoint.
Receivers address extension fields by name, dispatch each field by its own `version`, preserve distinct package versions, and ignore JSON member ordering. A session-log receiver validates the contiguous sequence range before interpreting event types. An unrecognized canonical event without `ignorable: true` prevents lossless reconstruction. The base request remains usable without either the registry or a particular contribution; field absence means that contribution did not apply to that request.
Receivers address extension fields by name, dispatch each field by its own `version`, preserve distinct package versions, and ignore JSON member ordering. A session-log receiver validates the contiguous sequence range before interpreting event types. An unrecognized canonical event without `ignorable: true` prevents lossless reconstruction. The base request remains usable without either the registry or a particular contribution; field absence means that contribution did not apply to that request. While a session-log backlog drains, `throughSeq` trails the Session's latest event, so a 2xx does not show that the receiver holds the current log.
+6 -6
View File
@@ -34,7 +34,7 @@
适配器先序列化包括确切 `messages` 在内的完整基础正文,再让已注册提供方准备字段。提供方会收到该不可变正文、请求取消信号,以及可选的 `sessionId` 和辅助调用 `purpose`。提供方返回 `undefined` 时,本次请求会省略其字段。
系统将已准备的 JSON 值与提供方持有的状态分离,再将其作为基础字段的顶层同级成员合并,并序列化到同一个 HTTP 正文中。准备失败或冲突会阻止请求。组合未挂载注册表时,适配器发送未经扩展的基础正文。
系统将已准备的 JSON 值与提供方持有的状态分离,再将其作为基础字段的顶层同级成员合并,并序列化到同一个 HTTP 正文中。准备失败或冲突会阻止请求。合并后的正文无法序列化时,适配器发送不含任何扩展字段的基础正文,跳过接受事务,让贡献方在之后的请求中重发自身状态,并记录被省略的字段名。组合未挂载注册表时,适配器发送未经扩展的基础正文。
已配置端点返回 HTTP 2xx 后,适配器会在读取 SSE 正文之前运行已准备的 `accept()` 事务。传输失败和非 2xx 响应不会接受任何贡献。即使端点返回 2xx,接受失败仍会使模型请求失败。接受仅记录端点级 HTTP 成功,不表示 SSE 流已完整结束,也不表示端点已持久化扩展。
@@ -73,7 +73,7 @@
## `dsh_session_log`
[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.zh.md) 贡献权威会话日志的一段连续后缀。该字段默认启用。它适用于携带存活会话且至少存在一个事件的请求;直接请求、陈旧会话 id 或空日志会省略该字段,组合可用 `enabled: false` 禁用它。下方示例使用逻辑 Session 格式 2 仅为说明协议字段,并不标识[当前写入格式](session-format-status.zh.md)。
[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.zh.md) 贡献权威会话日志的一段连续后缀。该字段默认启用。它适用于携带存活会话且至少存在一个事件的请求;直接请求、陈旧会话 id 或空日志会省略该字段,第一条待发事件本身超过 `maxBytes` 或无法序列化的请求也会省略该字段;组合可用 `enabled: false` 禁用它。下方示例使用逻辑 Session 格式 2 仅为说明协议字段,并不标识[当前写入格式](session-format-status.zh.md)。
```json
{
@@ -110,7 +110,7 @@
| `throughSeq` | 非负整数 | 本次请求所表示的最大序号 |
| `events` | 数组 | 从 `afterSeq + 1` 到 `throughSeq` 的连续事件 |
首次上传使用 `afterSeq: -1`,并携带当前的完整日志。此后每次上传都从同一会话 id 的最大已接受水位(watermark)之后开始。发送方为每次请求仅快照一次事件数组;快照后的追加内容属于后续请求。
首次上传使用 `afterSeq: -1`,此后每次上传都从同一会话 id 的最大已接受水位(watermark)之后开始。每次上传携带从该位置起、序列化后能放进 `maxBytes` 的最长连续事件段;`maxBytes` 默认 8 MiB,按 UTF-8 字节计算,包括 header 与数值字段,因此超过上限的积压会分多次已接受的请求传完。发送方为每次请求仅快照一次事件数组;快照后的追加内容属于后续请求。
### Session 协议 header
@@ -151,12 +151,12 @@
`delivery-accepted` 表示已配置端点为包含该字段的 LLM 请求返回 HTTP 2xx。它不表示 SSE 已完整结束,也不表示远端已经持久化。该事件的 `throughSeq` 必须标识一项更早的事件,`sessionId` 标识已发送后缀所属的 Session,`sessionFormatVersion` 则把水位绑定到该逻辑 generation。缺少该字段表示历史格式 v0。
发送方只会为当前 Session id 与格式 generation 折叠最大的匹配 `throughSeq`,因此并发已接受请求无法使游标倒退,其他 generation 的水位也不能授权当前后缀。恢复后的进程会从持久日志重建游标。fork 会忽略命名其他 Session 的继承水位,因此先发送自身完整的继承前缀,再以子会话 id 推进。水位事件自身属于下一段未发送后缀。
发送方只会为当前 Session id 与格式 generation 折叠最大的匹配 `throughSeq`,因此并发已接受请求无法使游标倒退,其他 generation 的水位也不能授权当前后缀。恢复后的进程会从持久日志重建游标。fork 会忽略命名其他 Session 的继承水位,因此从序号零开始、按 `maxBytes` 分多次请求上传自身的继承前缀,再以子会话 id 推进。水位事件自身属于下一段未发送后缀。
传输失败和非 2xx 响应不会追加水位。端点接受后、本地持久化前发生崩溃时,系统可能重新发送已接受范围;不确定性只会产生重复,绝不会产生序号缺口。系统没有独立上传存储、大小上限或截断路径。
传输失败、非 2xx 响应,以及序列化失败后不带扩展字段发出的请求,都不会追加水位。端点接受后、本地持久化前发生崩溃时,系统可能重新发送已接受范围;不确定性只会产生重复,绝不会产生序号缺口。系统没有独立上传存储或截断路径;`maxBytes` 负责限制每次请求的字段大小。
## 暴露内容与接收方要求
请求标头会暴露 Harness 应用版本、一个匿名 Harness-home 身份和可选的会话身份。`dsh_plugin_packages` 会暴露存活 npm 包的名称与版本。除非组合禁用该字段,`dsh_session_log` 可能暴露会话工作目录、系统提示词快照、用户与 Assistant 内容、嵌入式 Assistant stream、失败 attempt 输出、工具参数与结果、压缩摘要、反馈和插件持有的事件。适配器 API key 不是会话事件,因此不会进入该字段。通过 `baseURL` 选择的网关会收到与官方端点相同的值。
接收方按名称定位扩展字段,按各字段自己的 `version` 分派,保留不同的包版本,并忽略 JSON 成员顺序。会话日志接收方必须先校验连续序号范围,再解释事件类型。遇到不带 `ignorable: true` 的未知权威事件时,接收方无法进行无损重建。即使缺少注册表或某项贡献,基础请求仍然可用;字段缺失表示该项贡献不适用于本次请求。
接收方按名称定位扩展字段,按各字段自己的 `version` 分派,保留不同的包版本,并忽略 JSON 成员顺序。会话日志接收方必须先校验连续序号范围,再解释事件类型。遇到不带 `ignorable: true` 的未知权威事件时,接收方无法进行无损重建。即使缺少注册表或某项贡献,基础请求仍然可用;字段缺失表示该项贡献不适用于本次请求。会话日志积压补传期间,`throughSeq` 落后于会话的最新事件,因此 2xx 并不表示接收方已持有当前完整日志。
+3 -3
View File
@@ -9,7 +9,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:238`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:71`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
@@ -28,7 +28,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:597`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:604`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:52`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
| `compaction/summary-error` | `waterfall` | [`packages/compaction/compaction/src/index.ts:106`](../packages/compaction/compaction/src/index.ts) | [`compaction-basic`](../packages/compaction/compaction-basic) (`waterfall`) | [`compaction-image-offload`](../packages/compaction/compaction-image-offload) |
@@ -79,7 +79,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:153`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:190`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:198`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`tool-present`](../packages/deliverables/tool-present) |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:88`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:160`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
+3 -3
View File
@@ -11,7 +11,7 @@
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:238`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:71`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
@@ -30,7 +30,7 @@
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:597`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:604`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:52`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
| `compaction/summary-error` | `waterfall` | [`packages/compaction/compaction/src/index.ts:106`](../packages/compaction/compaction/src/index.ts) | [`compaction-basic`](../packages/compaction/compaction-basic) (`waterfall`) | [`compaction-image-offload`](../packages/compaction/compaction-image-offload) |
@@ -81,7 +81,7 @@
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:153`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:190`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:198`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`tool-present`](../packages/deliverables/tool-present) |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:88`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:160`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
+4 -1
View File
@@ -662,6 +662,9 @@ flowchart TD
pkg_user_questions --> pkg_agent
pkg_user_questions --> pkg_llm
pkg_user_questions --> pkg_scope
pkg_user_questions --> pkg_session
pkg_user_questions --> pkg_session_projection
pkg_user_questions --> pkg_typert_protocol
pkg_jobs --> pkg_agent
pkg_jobs --> pkg_brand
pkg_jobs --> pkg_invariants
@@ -1560,7 +1563,7 @@ flowchart TD
| [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
| [`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) |
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`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), [`system-prompt`](../packages/core/system-prompt) |
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
| [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`workspace`](../packages/workspace/workspace) |
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`session`](../packages/core/session) |
+4 -1
View File
@@ -664,6 +664,9 @@ flowchart TD
pkg_user_questions --> pkg_agent
pkg_user_questions --> pkg_llm
pkg_user_questions --> pkg_scope
pkg_user_questions --> pkg_session
pkg_user_questions --> pkg_session_projection
pkg_user_questions --> pkg_typert_protocol
pkg_jobs --> pkg_agent
pkg_jobs --> pkg_brand
pkg_jobs --> pkg_invariants
@@ -1562,7 +1565,7 @@ flowchart TD
| [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
| [`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) |
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`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), [`system-prompt`](../packages/core/system-prompt) |
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
| [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`workspace`](../packages/workspace/workspace) |
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`session`](../packages/core/session) |
+78 -63
View File
@@ -9,8 +9,8 @@
en: e7c80d1de4d328b4
zh: d9f368c27fd9f998
/session-persistence-event-catalog/persistence-type-fingerprints:
en: 0254fc43535dfaad
zh: 2f592cfae60e10ac
en: 31841c2d7c75bcc4
zh: 67cb5231a59d18bc
/session-persistence-event-catalog/event-envelope:
en: 7ebb1a2e0dd1f916
zh: 8946f415dda5c1b8
@@ -318,8 +318,11 @@
en: 6c3effe2abc1aa53
zh: 6c3effe2abc1aa53
/session-persistence-event-catalog/resolved-persistence-types/always:
en: e318559064b455fb
zh: e318559064b455fb
en: 4cdaf9853769f3da
zh: 4cdaf9853769f3da
/session-persistence-event-catalog/resolved-persistence-types/answered:
en: 9b5e8719c6d3d7ff
zh: 9b5e8719c6d3d7ff
/session-persistence-event-catalog/resolved-persistence-types/append:
en: 02f63fd38e94120b
zh: 02f63fd38e94120b
@@ -438,8 +441,11 @@
en: 42389e30f99e210f
zh: 42389e30f99e210f
/session-persistence-event-catalog/resolved-persistence-types/developer-message:
en: e9a6abc2549ddf91
zh: e9a6abc2549ddf91
en: e2f7f261557b02eb
zh: e2f7f261557b02eb
/session-persistence-event-catalog/resolved-persistence-types/dismissed:
en: b14bbe98cead8849
zh: b14bbe98cead8849
/session-persistence-event-catalog/resolved-persistence-types/dispatch:
en: aae8924fd5eb9013
zh: aae8924fd5eb9013
@@ -864,8 +870,11 @@
en: f01476bd214bb78f
zh: f01476bd214bb78f
/session-persistence-event-catalog/resolved-persistence-types/user-approval:
en: 7c6de40d31cf20a5
zh: 7c6de40d31cf20a5
en: 1c8f591ed011f359
zh: 1c8f591ed011f359
/session-persistence-event-catalog/resolved-persistence-types/user-question-reply:
en: ce169abc5bc0ad1f
zh: ce169abc5bc0ad1f
/session-persistence-event-catalog/resolved-persistence-types/user-message:
en: ecc3b25a07772288
zh: ecc3b25a07772288
@@ -951,11 +960,11 @@
en: 586743dfeab78c55
zh: 26ccef4a02dd7a58
/session-persistence-event-catalog/resolved-persistence-types/deepseeksearchllmrequest:
en: b3df6dce576b01a2
zh: f9c67e338cc6e449
en: 5b173514ba22f99a
zh: 45be438c1c8a580c
/session-persistence-event-catalog/resolved-persistence-types/developermessage:
en: 3df6e6aaeb92c0fa
zh: 29988ebcdd9e62e1
en: 43fa1595d4287c14
zh: 48a837250a80d0f1
/session-persistence-event-catalog/resolved-persistence-types/epochheader:
en: d31020fa1fda6fa4
zh: 77984c4bf09443fb
@@ -1053,11 +1062,11 @@
en: 6dbe099c62f7c226
zh: 6fd788f63c038c91
/session-persistence-event-catalog/resolved-persistence-types/llmretrystartedeventdata:
en: 7854b9694cc84c3a
zh: 319ec2e2e6ffdaec
en: b0a0d363567cc73d
zh: 3fbd5a13a542169c
/session-persistence-event-catalog/resolved-persistence-types/message:
en: 45a296c31664de87
zh: 8f235285b2f985f9
en: c8a1668f362c7917
zh: 417905691156e5f3
/session-persistence-event-catalog/resolved-persistence-types/messagefeedbackdelete:
en: c582953270d4ca44
zh: 966ccfe2e7660a2a
@@ -1068,14 +1077,14 @@
en: f58575a1fc96df42
zh: 9f0c6ed9033e5c9d
/session-persistence-event-catalog/resolved-persistence-types/messagefeedbackrating:
en: 490a91f1da1fc870
zh: 2f2b9e972e22d51f
en: b89e5af2d3b4da77
zh: 2e7fb3b98cfb1dac
/session-persistence-event-catalog/resolved-persistence-types/messagesource:
en: 7d0eda01d2c3076f
zh: ce3dbfc1bd7fd176
en: 0b83d49688e10273
zh: f0f2b92a36753a2f
/session-persistence-event-catalog/resolved-persistence-types/message~2:
en: 8e53b2a339808ffc
zh: d453e93e45f9e752
en: 70558eab25fefa3f
zh: b14a21a6b8bbe0ba
/session-persistence-event-catalog/resolved-persistence-types/modelmessagesource:
en: 3b677f021d7dd4d3
zh: 2a875742b22879a2
@@ -1116,8 +1125,8 @@
en: 93c4403f94ecdc14
zh: 2b2eb0c400881d71
/session-persistence-event-catalog/resolved-persistence-types/sandboxmode:
en: 13071a796a3bfcd2
zh: 93e5fbb1cdf78a79
en: c9fa5343ea99733b
zh: d6de41351c57a73f
/session-persistence-event-catalog/resolved-persistence-types/schedulechange:
en: 16be53be76c16720
zh: 38fd164dde86ccac
@@ -1134,11 +1143,11 @@
en: 9a54c4478ad3b620
zh: 02c4c1fd7e252770
/session-persistence-event-catalog/resolved-persistence-types/sessiontitleeventdata:
en: 415ce281c096b4c3
zh: 39c5bf9dfaa95579
en: 8d27d0fcf7b4c492
zh: 620f967622e4e63a
/session-persistence-event-catalog/resolved-persistence-types/sessiontitlellmrequesteventdata:
en: 0e539718e4bc8ed8
zh: 060007f55759f065
en: 05c6b11740a3b06c
zh: bfe01ac682d9f938
/session-persistence-event-catalog/resolved-persistence-types/sessiontitlemodelidentity:
en: 9531468264499498
zh: 9da23c0ad2776563
@@ -1245,14 +1254,14 @@
en: 544292eddeff5faa
zh: 4e36e575a5030fb2
/session-persistence-event-catalog/resolved-persistence-types/turnendreason:
en: 083f0fef130bb976
zh: dafd261a95708bec
en: 4577fec1a00cf57d
zh: f61550f7a11c4099
/session-persistence-event-catalog/resolved-persistence-types/usermessage:
en: 88b294a9b0bdf0e4
zh: 0597f465b9d45b27
en: 2623f0d05f65f7d9
zh: ddbd4b32a1cedeba
/session-persistence-event-catalog/resolved-persistence-types/usermessage~2:
en: 31aaab9c9f53ed9c
zh: c5cd866b3f2b27e0
en: 770f9f9f427167aa
zh: eab99a7dd69c5fd4
/session-persistence-event-catalog/resolved-persistence-types/workflowagentoutcome:
en: d0a3f9aab18c3de5
zh: 76ac69a835ca4e29
@@ -1296,12 +1305,15 @@
en: d4d63e6e0832fd5b
zh: 3af90f9fc14f48da
/session-persistence-event-catalog/resolved-persistence-types/union-2-variants:
en: f692844b29923b2e
zh: 72f12bca08d63267
en: b81872c733894f03
zh: 04b6221ce19a233e
/session-persistence-event-catalog/resolved-persistence-types/union-2-variants~2:
en: 4db67a1c537b0d84
zh: 810e334b2b214238
/session-persistence-event-catalog/resolved-persistence-types/union-2-variants~3:
en: 69d360cf5a1ae6b4
zh: f23faf39a2a0e6f1
/session-persistence-event-catalog/resolved-persistence-types/union-2-variants~3:
/session-persistence-event-catalog/resolved-persistence-types/union-2-variants~4:
en: 5a1af9849b0967ea
zh: 1085d793bbc47360
/session-persistence-event-catalog/resolved-persistence-types/union-3-variants:
@@ -1422,11 +1434,11 @@
en: 51af619ed8532a43
zh: a5ad878c63d0e046
/session-persistence-event-catalog/resolved-persistence-types/header-reason-startsseries:
en: 62ca405eba4d2757
zh: c6a8861533363ae9
en: f0ff6e8b547e59a1
zh: c17ac6f98ef72c50
/session-persistence-event-catalog/resolved-persistence-types/headerseq-message-step-turn:
en: 76e7be9a80a4082c
zh: c06aaa42a8488aea
en: 5eb089927e206419
zh: 9c9bcfa3b59e3a93
/session-persistence-event-catalog/resolved-persistence-types/height-width:
en: a5df50710083ac25
zh: aad3ccf6f6de70e4
@@ -1443,11 +1455,11 @@
en: 41f73c58a77dfa28
zh: af77f4aee3cb4a40
/session-persistence-event-catalog/resolved-persistence-types/inherited:
en: f538b60f99becc0a
zh: 0c8efa059a1e6569
en: fd7cfa6b80e3e1a7
zh: 2dacc297f1aa9840
/session-persistence-event-catalog/resolved-persistence-types/inserted-outcome-removedcount-start:
en: 3985827b3ead1d92
zh: c535166ccae6c8a3
en: ac06e73a90aaef38
zh: 71a6013618e43e22
/session-persistence-event-catalog/resolved-persistence-types/interrupted-message-step-stream:
en: f460572284c92755
zh: bae7561ce887b040
@@ -1776,8 +1788,11 @@
en: eaffa616c5b9dc9a
zh: 2eb138fcf5936e20
/session-persistence-event-catalog/resolved-persistence-types/kind-user-approval-form-snapshot:
en: 58c4ff5b5d70cc02
zh: 080be72a31b3205e
en: cc6497e06528ca95
zh: d03e6225274369d1
/session-persistence-event-catalog/resolved-persistence-types/kind-user-question-reply:
en: aed630ccf32bc001
zh: a9142a26c19e933f
/session-persistence-event-catalog/resolved-persistence-types/kind-webhook-form-notice:
en: 4cd5dc7523fefebb
zh: f467561fbd0ae394
@@ -1836,11 +1851,11 @@
en: a79b2ab16b9ed215
zh: 764cd1400ee8db63
/session-persistence-event-catalog/resolved-persistence-types/type-agent-preset-selected:
en: d59bb505610b862f
zh: e2637fbbb8365bdf
en: 10f1d820ae610d45
zh: d5cfa158baee2bb0
/session-persistence-event-catalog/resolved-persistence-types/type-agent-inbox-spliced:
en: 109d942de30d9227
zh: 6c6fc06634ad7945
en: af155a8697274223
zh: 6ca0d7888c1b1651
/session-persistence-event-catalog/resolved-persistence-types/type-approval-asked:
en: 9c77f18b0dad22f0
zh: 8894cf2878fe90db
@@ -1884,11 +1899,11 @@
en: ba73fb10b7e2ffc9
zh: 8d5c97e63e5f16da
/session-persistence-event-catalog/resolved-persistence-types/type-deliverables-presented:
en: 49de6f547778d916
zh: ce2a1af1ae934f68
en: a9d07eb72229dc21
zh: 747169a607ff869e
/session-persistence-event-catalog/resolved-persistence-types/type-developer-message:
en: 686d26ced9945d4b
zh: 535b9e92eb7535fb
en: aa1d22db9502eaeb
zh: a3caf25d34c1d72c
/session-persistence-event-catalog/resolved-persistence-types/type-feedback-message-delete:
en: 204561e2311db97e
zh: 77264396bd5e25c4
@@ -1953,11 +1968,11 @@
en: acf7e21e4d21ebff
zh: fb83a853634d204a
/session-persistence-event-catalog/resolved-persistence-types/type-session-title:
en: ba2f901c9981835f
zh: ce0358c7d16b0beb
en: e429676476efea69
zh: fca1dd77216a7eca
/session-persistence-event-catalog/resolved-persistence-types/type-session-title-llm-request:
en: 7515d391badb4d0a
zh: 9eb19def7a3ed752
en: 7b1a43a5da8444f0
zh: 15d92132c6fdbcb0
/session-persistence-event-catalog/resolved-persistence-types/type-step-end:
en: 7fc233c4a569dae2
zh: bc2b3fed4a397c00
@@ -2034,11 +2049,11 @@
en: 1b045de96172be4b
zh: 8a15c6f35b576b37
/session-persistence-event-catalog/resolved-persistence-types/type-usage:
en: a01e61480accb34b
zh: 6387a3cc7666aa7e
en: 5d9c18f4533569a6
zh: f69e1f3fd9dc0188
/session-persistence-event-catalog/resolved-persistence-types/type-user-message:
en: 58d3e616db84f840
zh: 9aed64a03347caad
en: 5a9360fd7cba2042
zh: a159049cc36b7c37
/session-persistence-event-catalog/resolved-persistence-types/type-web-deepseek-search-llm-request:
en: f109997e295af37d
zh: b2dcfd599f73b61c
+96 -46
View File
@@ -21,7 +21,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover
| `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`HeaderLine`](#persistence-type-sha256-18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f) |
| `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`union (3 variants)`](#persistence-type-sha256-5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc) |
| `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`{ type: "agent-preset/selected" }`](#persistence-type-sha256-a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b) |
| `event:agent/inbox/spliced` | event | `1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c) |
| `event:agent/inbox/spliced` | event | `99a3e14f0b0ff0e38d345e7cb7d8d81ee8d11c51d611d9f91b1a82c8ea481fc4` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-99a3e14f0b0ff0e38d345e7cb7d8d81ee8d11c51d611d9f91b1a82c8ea481fc4) |
| `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`{ type: "approval/asked" }`](#persistence-type-sha256-3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1) |
| `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`{ type: "approval/decided" }`](#persistence-type-sha256-bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3) |
| `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`{ type: "approval/policy" }`](#persistence-type-sha256-26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78) |
@@ -34,7 +34,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover
| `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`{ type: "compaction/start" }`](#persistence-type-sha256-db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965) |
| `event:compaction/summary` | event | `e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602` | [`{ type: "compaction/summary" }`](#persistence-type-sha256-e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602) |
| `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`{ type: "deliverables/presented" }`](#persistence-type-sha256-13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176) |
| `event:developer/message` | event | `eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80` | [`{ type: "developer/message" }`](#persistence-type-sha256-eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80) |
| `event:developer/message` | event | `5d588f99e950c42bdd55a27fa41e98f5c86db5b0523622d85b99604c7ddb0107` | [`{ type: "developer/message" }`](#persistence-type-sha256-5d588f99e950c42bdd55a27fa41e98f5c86db5b0523622d85b99604c7ddb0107) |
| `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`{ type: "feedback/message-delete" }`](#persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796) |
| `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`{ type: "feedback/message-put" }`](#persistence-type-sha256-b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf) |
| `event:feedback/record` | event | `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` | [`{ type: "feedback/record" }`](#persistence-type-sha256-b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8) |
@@ -54,7 +54,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover
| `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) |
| `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) |
| `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) |
| `event:session/title-llm-request` | event | `fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d) |
| `event:session/title-llm-request` | event | `64170a859320a56b572372f9ab16e31c4ae9c62454a31418e900ebbf90053c68` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-64170a859320a56b572372f9ab16e31c4ae9c62454a31418e900ebbf90053c68) |
| `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) |
| `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) |
| `event:subagent/catalog` | event | `3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604) |
@@ -76,7 +76,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover
| `event:tool/result` | event | `7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397` | [`{ type: "tool/result" }`](#persistence-type-sha256-7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397) |
| `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) |
| `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) |
| `event:user/message` | event | `3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761` | [`{ type: "user/message" }`](#persistence-type-sha256-3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761) |
| `event:user/message` | event | `1d1697092d04c08f9fcb99015ac082e58b01b7de9e41611fa6aebf2879c3d083` | [`{ type: "user/message" }`](#persistence-type-sha256-1d1697092d04c08f9fcb99015ac082e58b01b7de9e41611fa6aebf2879c3d083) |
| `event:web/deepseek-search-llm-request` | event | `cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331) |
| `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) |
@@ -1357,6 +1357,14 @@ SHA-256: `142c37d302aa22a0a57915b5255939d3ca91c1811ca13fddcf84cf76ea00758a`
`"always"`
<a id="persistence-type-sha256-85d6d96ff884297410ec0026bdacba835f10e56cbcc5a3b9d670739f4aa0494b"></a>
### `"answered"`
SHA-256: `85d6d96ff884297410ec0026bdacba835f10e56cbcc5a3b9d670739f4aa0494b`
`"answered"`
<a id="persistence-type-sha256-e438a9d4fb883b3e4a8d9fc8913aa1550ed50eb384bfbf943100a5605b537498"></a>
### `"append"`
@@ -1677,6 +1685,14 @@ SHA-256: `1379f0b72275994d20f1ddca2d60a96ab16ec80397ca9f1af21bdc67ba5c269c`
`"developer/message"`
<a id="persistence-type-sha256-39a6d6b657a33d9e62e6fe1ce35b44df38b6a0fef72a87ee45ec318ebfe8f4b6"></a>
### `"dismissed"`
SHA-256: `39a6d6b657a33d9e62e6fe1ce35b44df38b6a0fef72a87ee45ec318ebfe8f4b6`
`"dismissed"`
<a id="persistence-type-sha256-26c879bdd130e8b8683a6ce588414fad2244783d6573ca399fa0de184a2b0bb1"></a>
### `"dispatch"`
@@ -2819,6 +2835,14 @@ SHA-256: `f68c335306535a001127dae6685ece493a0ee7f4e2a851197b6054d8a9ff7394`
`"user-approval"`
<a id="persistence-type-sha256-9645cf93769a6dfb227bd400bd6cb9fa7bab9140f4a851e4ae92514ade6e3114"></a>
### `"user-question-reply"`
SHA-256: `9645cf93769a6dfb227bd400bd6cb9fa7bab9140f4a851e4ae92514ade6e3114`
`"user-question-reply"`
<a id="persistence-type-sha256-de25352d2dc4196309bd84b5bac6319f6d5c898ea0e82bb5ae66c5738d0d426f"></a>
### `"user/message"`
@@ -3207,7 +3231,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web
| `body` | required | [`{ max_tokens, messages, model, tools }`](#persistence-type-sha256-930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731) |
| `endpoint` | required | `string` |
<a id="persistence-type-sha256-f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b"></a>
<a id="persistence-type-sha256-19ac4d9fb655ae9f2d9e8993d083d3eb9aede910d0454b892eb6e1290f55179c"></a>
<a id="persistence-type-developermessage"></a>
@@ -3215,7 +3239,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web
### `DeveloperMessage`
SHA-256: `f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b`
SHA-256: `19ac4d9fb655ae9f2d9e8993d083d3eb9aede910d0454b892eb6e1290f55179c`
Sources: [`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message.ts)
@@ -3224,11 +3248,11 @@ Sources: [`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message
| `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) |
| `id` | required | `string` |
| `role` | required | `"developer"` |
| `source` | required | [`MessageSource`](#persistence-type-sha256-40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945) |
| `source` | required | [`MessageSource`](#persistence-type-sha256-b1a819c6930a332d1018838361073d9b3e754476e5dd345d479821554446d644) |
Source compatibility: `source` — `session-source-attribution` v1; `session.developer-message.source`; `kind`; `preserve`.
Attribution-only additions: `tmux-context`.
Attribution-only additions: `tmux-context`, `user-question-reply`.
<a id="persistence-type-sha256-9a930283b91d493d6347146f2475bb107294b379415ca02162354f46a64164f7"></a>
@@ -3841,7 +3865,7 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr
| `step` | required | `number` |
| `turn` | required | `number` |
<a id="persistence-type-sha256-691c0f641cd3e8072fdacd6fa8c400253cf75a0ce9285d8ce89141bbb24f2fb6"></a>
<a id="persistence-type-sha256-453230f850102bbad10d17b227431d749e58d084f8c15a17dd79ce4f100c52ca"></a>
<a id="persistence-type-message"></a>
@@ -3849,15 +3873,15 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr
### `Message`
SHA-256: `691c0f641cd3e8072fdacd6fa8c400253cf75a0ce9285d8ce89141bbb24f2fb6`
SHA-256: `453230f850102bbad10d17b227431d749e58d084f8c15a17dd79ce4f100c52ca`
Sources: [`packages/llm/llm/src/message.ts:196`](../packages/llm/llm/src/message.ts)
One of:
- [`ToolResultMessage`](#persistence-type-sha256-4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731)
- [`DeveloperMessage`](#persistence-type-sha256-f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b)
- [`UserMessage`](#persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586)
- [`DeveloperMessage`](#persistence-type-sha256-19ac4d9fb655ae9f2d9e8993d083d3eb9aede910d0454b892eb6e1290f55179c)
- [`UserMessage`](#persistence-type-sha256-7865a04b33ddf40314f467043e50388a3b6ba3a8b51de7c4ef61ad9910bafcec)
- [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be)
- [`SystemMessage`](#persistence-type-sha256-09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363)
@@ -3934,7 +3958,7 @@ One of:
- `"negative"`
- `"positive"`
<a id="persistence-type-sha256-40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945"></a>
<a id="persistence-type-sha256-b1a819c6930a332d1018838361073d9b3e754476e5dd345d479821554446d644"></a>
<a id="persistence-type-messagesource"></a>
@@ -3942,7 +3966,7 @@ One of:
### `MessageSource`
SHA-256: `40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945`
SHA-256: `b1a819c6930a332d1018838361073d9b3e754476e5dd345d479821554446d644`
Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message.ts)
@@ -4056,15 +4080,16 @@ Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message
| `user-approval` | `"relay"` | none | [`{ kind: "user-approval", form: "relay" }`](#persistence-type-sha256-5cdbf342b7748d152bc987b68f58e565e5bba4b227e076e2d2120d87662e45cd) |
| `user-approval` | `"snapshot"` | `sections`: [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) | [`{ kind: "user-approval", form: "snapshot" }`](#persistence-type-sha256-5654685d7557ec747e74887237065e626a5bdfa426ca43f7fbb0b6fee9b0916c) |
| `user-approval` | not declared | none | [`{ kind: "user-approval" }`](#persistence-type-sha256-6292bcbedaa97feaec5b48a03bb8144b581b93309d9d6c905f26da7a6a7a0a4f) |
| `user-question-reply` | not declared | `callId`: `string`; `outcome`: [`union (2 variants)`](#persistence-type-sha256-3e8ed906c20694e13769d14e7f510ea650bc1977d318ad3375eb5e25d75a8dc3) | [`{ kind: "user-question-reply" }`](#persistence-type-sha256-a88baeb50f7e9485fbdc6d4dfaf26d04210be3c699bca84b97a0e2abafc9bd25) |
| `webhook` | `"notice"` | `deliveryId`: `string`; `provider`: `string`; `ruleId`: `string`; `source`: `string`; `summary`: `string` | [`{ kind: "webhook", form: "notice" }`](#persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94) |
<a id="persistence-type-sha256-fc8242857caf7450d7f68a1f509dce83e237bd21571666006895124cb2246a83"></a>
<a id="persistence-type-sha256-ad6c4e4a7270450d096793e75298c39ec6f91d5e30f211fb8ff24b72695f8dc1"></a>
### `Message[]`
SHA-256: `fc8242857caf7450d7f68a1f509dce83e237bd21571666006895124cb2246a83`
SHA-256: `ad6c4e4a7270450d096793e75298c39ec6f91d5e30f211fb8ff24b72695f8dc1`
Array of [`Message`](#persistence-type-sha256-691c0f641cd3e8072fdacd6fa8c400253cf75a0ce9285d8ce89141bbb24f2fb6).
Array of [`Message`](#persistence-type-sha256-453230f850102bbad10d17b227431d749e58d084f8c15a17dd79ce4f100c52ca).
<a id="persistence-type-sha256-719c51f67074f97034a6c94297fe1f4b5d3c8d392c57586dab732aa5398c7a91"></a>
@@ -4307,7 +4332,7 @@ One of:
SHA-256: `fab2bb09bc5e3b9db0685e8f5500b029af1a2b33e70bc703c1d5d0f596fbc1fd`
Sources: [`packages/sandbox/sandbox/src/index.ts:29`](../packages/sandbox/sandbox/src/index.ts)
Sources: [`packages/sandbox/sandbox/src/index.ts:30`](../packages/sandbox/sandbox/src/index.ts)
One of:
@@ -4431,7 +4456,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/
| `source` | required | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) |
| `title` | required | `string` |
<a id="persistence-type-sha256-9bc85364d1f37d9c837db6a3bdd3d1d007d923cdc7401a3322be9f12fdc225fd"></a>
<a id="persistence-type-sha256-3e94e9061e5069552df9a5568f90c89bec379a9cdf0dc3c122127746312111e9"></a>
<a id="persistence-type-packagessessionsession-title-llmsrcindextssessiontitlellmrequesteventdata"></a>
@@ -4439,7 +4464,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/
### `SessionTitleLlmRequestEventData`
SHA-256: `9bc85364d1f37d9c837db6a3bdd3d1d007d923cdc7401a3322be9f12fdc225fd`
SHA-256: `3e94e9061e5069552df9a5568f90c89bec379a9cdf0dc3c122127746312111e9`
Sources: [`packages/session/session-title-llm/src/index.ts:34`](../packages/session/session-title-llm/src/index.ts)
@@ -4447,7 +4472,7 @@ Sources: [`packages/session/session-title-llm/src/index.ts:34`](../packages/sess
|---|---|---|
| `maxTokens` | required | `number` |
| `messageSeqs` | required | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) |
| `messages` | required | [`Message[]`](#persistence-type-sha256-fc8242857caf7450d7f68a1f509dce83e237bd21571666006895124cb2246a83) |
| `messages` | required | [`Message[]`](#persistence-type-sha256-ad6c4e4a7270450d096793e75298c39ec6f91d5e30f211fb8ff24b72695f8dc1) |
| `route` | required | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) |
| `system` | required | `string` |
| `titleProvider` | required | `string` |
@@ -5101,7 +5126,7 @@ Sources: [`packages/core/session/src/types.ts:232`](../packages/core/session/src
| `interrupted` | not declared | none | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) |
| `max-tokens` | not declared | none | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) |
<a id="persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586"></a>
<a id="persistence-type-sha256-7865a04b33ddf40314f467043e50388a3b6ba3a8b51de7c4ef61ad9910bafcec"></a>
<a id="persistence-type-packagesllmllmsrcmessagetsusermessage"></a>
@@ -5109,7 +5134,7 @@ Sources: [`packages/core/session/src/types.ts:232`](../packages/core/session/src
### `UserMessage`
SHA-256: `64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586`
SHA-256: `7865a04b33ddf40314f467043e50388a3b6ba3a8b51de7c4ef61ad9910bafcec`
Sources: [`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message.ts)
@@ -5118,19 +5143,19 @@ Sources: [`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message
| `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) |
| `id` | required | `string` |
| `role` | required | `"user"` |
| `source` | required | [`MessageSource`](#persistence-type-sha256-40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945) |
| `source` | required | [`MessageSource`](#persistence-type-sha256-b1a819c6930a332d1018838361073d9b3e754476e5dd345d479821554446d644) |
Source compatibility: `source` — `session-source-attribution` v1; `session.user-message.source`; `kind`; `preserve`.
Attribution-only additions: `tmux-context`.
Attribution-only additions: `tmux-context`, `user-question-reply`.
<a id="persistence-type-sha256-d1f27cf1650e5f59d5d6e95ce6dfd92b31757af076a8ab6418a70b6dbdd53621"></a>
<a id="persistence-type-sha256-94408de87611d1469c83ff7f1a8c6366e4e0ada349ff090c2bd408a024bab080"></a>
### `UserMessage[]`
SHA-256: `d1f27cf1650e5f59d5d6e95ce6dfd92b31757af076a8ab6418a70b6dbdd53621`
SHA-256: `94408de87611d1469c83ff7f1a8c6366e4e0ada349ff090c2bd408a024bab080`
Array of [`UserMessage`](#persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586).
Array of [`UserMessage`](#persistence-type-sha256-7865a04b33ddf40314f467043e50388a3b6ba3a8b51de7c4ef61ad9910bafcec).
<a id="persistence-type-sha256-c2deb7c9183735d4e0fffddf5c623f4e34daa1c8e96b111982e957dfcbbcf896"></a>
@@ -5379,6 +5404,17 @@ One of:
- `"error"`
- `"success"`
<a id="persistence-type-sha256-3e8ed906c20694e13769d14e7f510ea650bc1977d318ad3375eb5e25d75a8dc3"></a>
### `union (2 variants)`
SHA-256: `3e8ed906c20694e13769d14e7f510ea650bc1977d318ad3375eb5e25d75a8dc3`
One of:
- `"answered"`
- `"dismissed"`
<a id="persistence-type-sha256-50cf02d875f8a1f1411f4a6fbcffab9120ccef5f74a47331fbf29480e5f68f48"></a>
### `union (2 variants)`
@@ -5973,18 +6009,18 @@ Sources: [`packages/core/session/src/types.ts:390`](../packages/core/session/src
| `reason` | required | [`RequestHeaderReason`](#persistence-type-sha256-530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6) |
| `startsSeries` | optional | `true` |
<a id="persistence-type-sha256-51c78ccc9023ce6642d07fb9a10cbf942ff7e313d5b304ddc77218df71da708f"></a>
<a id="persistence-type-sha256-c4d3620755d2defbe487d6e64259886b87b5dcff7bd58c8f9d85a06fa6f9bce0"></a>
### `{ headerSeq?, message, step, turn }`
SHA-256: `51c78ccc9023ce6642d07fb9a10cbf942ff7e313d5b304ddc77218df71da708f`
SHA-256: `c4d3620755d2defbe487d6e64259886b87b5dcff7bd58c8f9d85a06fa6f9bce0`
Sources: [`packages/core/session/src/types.ts:311`](../packages/core/session/src/types.ts)
| Property | Presence | Type |
|---|---|---|
| `headerSeq` | optional | `number` |
| `message` | required | [`DeveloperMessage`](#persistence-type-sha256-f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b) |
| `message` | required | [`DeveloperMessage`](#persistence-type-sha256-19ac4d9fb655ae9f2d9e8993d083d3eb9aede910d0454b892eb6e1290f55179c) |
| `step` | required | `number` |
| `turn` | required | `number` |
@@ -6068,17 +6104,17 @@ Sources: [`packages/core/session/src/types.ts:427`](../packages/core/session/src
|---|---|---|
| `inherited` | optional | `true` |
<a id="persistence-type-sha256-dc4bd7ac827f4b1f9236f9a157a4b4cc171a0588064a611e43edd95f62c68130"></a>
<a id="persistence-type-sha256-b7ae3ff2cea93132e51e04be70f73590c91509e32bcaa34f3260d5503cfa0d3c"></a>
### `{ inserted, outcome?, removedCount?, start, … }`
SHA-256: `dc4bd7ac827f4b1f9236f9a157a4b4cc171a0588064a611e43edd95f62c68130`
SHA-256: `b7ae3ff2cea93132e51e04be70f73590c91509e32bcaa34f3260d5503cfa0d3c`
Sources: [`packages/core/agent/src/types.ts:96`](../packages/core/agent/src/types.ts)
| Property | Presence | Type |
|---|---|---|
| `inserted` | required | [`UserMessage[]`](#persistence-type-sha256-d1f27cf1650e5f59d5d6e95ce6dfd92b31757af076a8ab6418a70b6dbdd53621) |
| `inserted` | required | [`UserMessage[]`](#persistence-type-sha256-94408de87611d1469c83ff7f1a8c6366e4e0ada349ff090c2bd408a024bab080) |
| `outcome` | optional | `"canceled"` |
| `removedCount` | optional | `number` |
| `start` | required | `number` |
@@ -7337,6 +7373,20 @@ SHA-256: `5654685d7557ec747e74887237065e626a5bdfa426ca43f7fbb0b6fee9b0916c`
| `kind` | required | `"user-approval"` |
| `sections` | required | [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) |
<a id="persistence-type-sha256-a88baeb50f7e9485fbdc6d4dfaf26d04210be3c699bca84b97a0e2abafc9bd25"></a>
### `{ kind: "user-question-reply" }`
SHA-256: `a88baeb50f7e9485fbdc6d4dfaf26d04210be3c699bca84b97a0e2abafc9bd25`
Sources: [`packages/interaction/user-questions/src/types.ts:120`](../packages/interaction/user-questions/src/types.ts)
| Property | Presence | Type |
|---|---|---|
| `callId` | required | `string` |
| `kind` | required | `"user-question-reply"` |
| `outcome` | required | [`union (2 variants)`](#persistence-type-sha256-3e8ed906c20694e13769d14e7f510ea650bc1977d318ad3375eb5e25d75a8dc3) |
<a id="persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94"></a>
### `{ kind: "webhook", form: "notice" }`
@@ -7612,17 +7662,17 @@ SHA-256: `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b`
| `time` | required | `number` |
| `type` | required | `"agent-preset/selected"` |
<a id="persistence-type-sha256-1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c"></a>
<a id="persistence-type-sha256-99a3e14f0b0ff0e38d345e7cb7d8d81ee8d11c51d611d9f91b1a82c8ea481fc4"></a>
<a id="persistence-type-eventagentinboxspliced"></a>
### `{ type: "agent/inbox/spliced" }`
SHA-256: `1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c`
SHA-256: `99a3e14f0b0ff0e38d345e7cb7d8d81ee8d11c51d611d9f91b1a82c8ea481fc4`
| Property | Presence | Type |
|---|---|---|
| `data` | required | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-dc4bd7ac827f4b1f9236f9a157a4b4cc171a0588064a611e43edd95f62c68130) |
| `data` | required | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-b7ae3ff2cea93132e51e04be70f73590c91509e32bcaa34f3260d5503cfa0d3c) |
| `ignorable` | optional | `true` |
| `seq` | required | `number` |
| `time` | required | `number` |
@@ -7863,17 +7913,17 @@ SHA-256: `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176`
| `time` | required | `number` |
| `type` | required | `"deliverables/presented"` |
<a id="persistence-type-sha256-eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80"></a>
<a id="persistence-type-sha256-5d588f99e950c42bdd55a27fa41e98f5c86db5b0523622d85b99604c7ddb0107"></a>
<a id="persistence-type-eventdevelopermessage"></a>
### `{ type: "developer/message" }`
SHA-256: `eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80`
SHA-256: `5d588f99e950c42bdd55a27fa41e98f5c86db5b0523622d85b99604c7ddb0107`
| Property | Presence | Type |
|---|---|---|
| `data` | required | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-51c78ccc9023ce6642d07fb9a10cbf942ff7e313d5b304ddc77218df71da708f) |
| `data` | required | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-c4d3620755d2defbe487d6e64259886b87b5dcff7bd58c8f9d85a06fa6f9bce0) |
| `ignorable` | optional | `true` |
| `seq` | required | `number` |
| `sourceEventSeqs` | optional | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) |
@@ -8229,17 +8279,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5`
| `time` | required | `number` |
| `type` | required | `"session/title"` |
<a id="persistence-type-sha256-fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d"></a>
<a id="persistence-type-sha256-64170a859320a56b572372f9ab16e31c4ae9c62454a31418e900ebbf90053c68"></a>
<a id="persistence-type-eventsessiontitle-llm-request"></a>
### `{ type: "session/title-llm-request" }`
SHA-256: `fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d`
SHA-256: `64170a859320a56b572372f9ab16e31c4ae9c62454a31418e900ebbf90053c68`
| Property | Presence | Type |
|---|---|---|
| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-9bc85364d1f37d9c837db6a3bdd3d1d007d923cdc7401a3322be9f12fdc225fd) |
| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-3e94e9061e5069552df9a5568f90c89bec379a9cdf0dc3c122127746312111e9) |
| `ignorable` | optional | `true` |
| `seq` | required | `number` |
| `time` | required | `number` |
@@ -8662,17 +8712,17 @@ Sources: [`packages/llm/llm/src/types.ts:458`](../packages/llm/llm/src/types.ts)
| `type` | required | `"usage"` |
| `usage` | required | [`TokenUsage`](#persistence-type-sha256-fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394) |
<a id="persistence-type-sha256-3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761"></a>
<a id="persistence-type-sha256-1d1697092d04c08f9fcb99015ac082e58b01b7de9e41611fa6aebf2879c3d083"></a>
<a id="persistence-type-eventusermessage"></a>
### `{ type: "user/message" }`
SHA-256: `3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761`
SHA-256: `1d1697092d04c08f9fcb99015ac082e58b01b7de9e41611fa6aebf2879c3d083`
| Property | Presence | Type |
|---|---|---|
| `data` | required | [`UserMessage`](#persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586) |
| `data` | required | [`UserMessage`](#persistence-type-sha256-7865a04b33ddf40314f467043e50388a3b6ba3a8b51de7c4ef61ad9910bafcec) |
| `ignorable` | optional | `true` |
| `seq` | required | `number` |
| `sourceEventSeqs` | optional | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) |
+96 -46
View File
@@ -23,7 +23,7 @@
| `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`HeaderLine`](#persistence-type-sha256-18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f) |
| `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`union (3 variants)`](#persistence-type-sha256-5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc) |
| `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`{ type: "agent-preset/selected" }`](#persistence-type-sha256-a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b) |
| `event:agent/inbox/spliced` | event | `1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c) |
| `event:agent/inbox/spliced` | event | `99a3e14f0b0ff0e38d345e7cb7d8d81ee8d11c51d611d9f91b1a82c8ea481fc4` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-99a3e14f0b0ff0e38d345e7cb7d8d81ee8d11c51d611d9f91b1a82c8ea481fc4) |
| `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`{ type: "approval/asked" }`](#persistence-type-sha256-3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1) |
| `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`{ type: "approval/decided" }`](#persistence-type-sha256-bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3) |
| `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`{ type: "approval/policy" }`](#persistence-type-sha256-26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78) |
@@ -36,7 +36,7 @@
| `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`{ type: "compaction/start" }`](#persistence-type-sha256-db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965) |
| `event:compaction/summary` | event | `e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602` | [`{ type: "compaction/summary" }`](#persistence-type-sha256-e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602) |
| `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`{ type: "deliverables/presented" }`](#persistence-type-sha256-13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176) |
| `event:developer/message` | event | `eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80` | [`{ type: "developer/message" }`](#persistence-type-sha256-eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80) |
| `event:developer/message` | event | `5d588f99e950c42bdd55a27fa41e98f5c86db5b0523622d85b99604c7ddb0107` | [`{ type: "developer/message" }`](#persistence-type-sha256-5d588f99e950c42bdd55a27fa41e98f5c86db5b0523622d85b99604c7ddb0107) |
| `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`{ type: "feedback/message-delete" }`](#persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796) |
| `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`{ type: "feedback/message-put" }`](#persistence-type-sha256-b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf) |
| `event:feedback/record` | event | `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` | [`{ type: "feedback/record" }`](#persistence-type-sha256-b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8) |
@@ -56,7 +56,7 @@
| `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) |
| `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) |
| `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) |
| `event:session/title-llm-request` | event | `fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d) |
| `event:session/title-llm-request` | event | `64170a859320a56b572372f9ab16e31c4ae9c62454a31418e900ebbf90053c68` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-64170a859320a56b572372f9ab16e31c4ae9c62454a31418e900ebbf90053c68) |
| `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) |
| `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) |
| `event:subagent/catalog` | event | `3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604) |
@@ -78,7 +78,7 @@
| `event:tool/result` | event | `7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397` | [`{ type: "tool/result" }`](#persistence-type-sha256-7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397) |
| `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) |
| `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) |
| `event:user/message` | event | `3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761` | [`{ type: "user/message" }`](#persistence-type-sha256-3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761) |
| `event:user/message` | event | `1d1697092d04c08f9fcb99015ac082e58b01b7de9e41611fa6aebf2879c3d083` | [`{ type: "user/message" }`](#persistence-type-sha256-1d1697092d04c08f9fcb99015ac082e58b01b7de9e41611fa6aebf2879c3d083) |
| `event:web/deepseek-search-llm-request` | event | `cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331) |
| `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) |
@@ -1359,6 +1359,14 @@ SHA-256: `142c37d302aa22a0a57915b5255939d3ca91c1811ca13fddcf84cf76ea00758a`
`"always"`
<a id="persistence-type-sha256-85d6d96ff884297410ec0026bdacba835f10e56cbcc5a3b9d670739f4aa0494b"></a>
### `"answered"`
SHA-256: `85d6d96ff884297410ec0026bdacba835f10e56cbcc5a3b9d670739f4aa0494b`
`"answered"`
<a id="persistence-type-sha256-e438a9d4fb883b3e4a8d9fc8913aa1550ed50eb384bfbf943100a5605b537498"></a>
### `"append"`
@@ -1679,6 +1687,14 @@ SHA-256: `1379f0b72275994d20f1ddca2d60a96ab16ec80397ca9f1af21bdc67ba5c269c`
`"developer/message"`
<a id="persistence-type-sha256-39a6d6b657a33d9e62e6fe1ce35b44df38b6a0fef72a87ee45ec318ebfe8f4b6"></a>
### `"dismissed"`
SHA-256: `39a6d6b657a33d9e62e6fe1ce35b44df38b6a0fef72a87ee45ec318ebfe8f4b6`
`"dismissed"`
<a id="persistence-type-sha256-26c879bdd130e8b8683a6ce588414fad2244783d6573ca399fa0de184a2b0bb1"></a>
### `"dispatch"`
@@ -2821,6 +2837,14 @@ SHA-256: `f68c335306535a001127dae6685ece493a0ee7f4e2a851197b6054d8a9ff7394`
`"user-approval"`
<a id="persistence-type-sha256-9645cf93769a6dfb227bd400bd6cb9fa7bab9140f4a851e4ae92514ade6e3114"></a>
### `"user-question-reply"`
SHA-256: `9645cf93769a6dfb227bd400bd6cb9fa7bab9140f4a851e4ae92514ade6e3114`
`"user-question-reply"`
<a id="persistence-type-sha256-de25352d2dc4196309bd84b5bac6319f6d5c898ea0e82bb5ae66c5738d0d426f"></a>
### `"user/message"`
@@ -3209,7 +3233,7 @@ SHA-256: `2517ba143a271508d3ca35126d5aca8f7f1facf5aaddce4adf011832042fa2b9`
| `body` | 必需 | [`{ max_tokens, messages, model, tools }`](#persistence-type-sha256-930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731) |
| `endpoint` | 必需 | `string` |
<a id="persistence-type-sha256-f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b"></a>
<a id="persistence-type-sha256-19ac4d9fb655ae9f2d9e8993d083d3eb9aede910d0454b892eb6e1290f55179c"></a>
<a id="persistence-type-developermessage"></a>
@@ -3217,7 +3241,7 @@ SHA-256: `2517ba143a271508d3ca35126d5aca8f7f1facf5aaddce4adf011832042fa2b9`
### `DeveloperMessage`
SHA-256: `f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b`
SHA-256: `19ac4d9fb655ae9f2d9e8993d083d3eb9aede910d0454b892eb6e1290f55179c`
来源:[`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message.ts)
@@ -3226,11 +3250,11 @@ SHA-256: `f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b`
| `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) |
| `id` | 必需 | `string` |
| `role` | 必需 | `"developer"` |
| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945) |
| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-b1a819c6930a332d1018838361073d9b3e754476e5dd345d479821554446d644) |
消息源兼容策略:`source` — `session-source-attribution` v1; `session.developer-message.source`; `kind`; `preserve`.
仅表示归属的新增 kind:`tmux-context`.
仅表示归属的新增 kind:`tmux-context`, `user-question-reply`.
<a id="persistence-type-sha256-9a930283b91d493d6347146f2475bb107294b379415ca02162354f46a64164f7"></a>
@@ -3843,7 +3867,7 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20`
| `step` | 必需 | `number` |
| `turn` | 必需 | `number` |
<a id="persistence-type-sha256-691c0f641cd3e8072fdacd6fa8c400253cf75a0ce9285d8ce89141bbb24f2fb6"></a>
<a id="persistence-type-sha256-453230f850102bbad10d17b227431d749e58d084f8c15a17dd79ce4f100c52ca"></a>
<a id="persistence-type-message"></a>
@@ -3851,15 +3875,15 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20`
### `Message`
SHA-256: `691c0f641cd3e8072fdacd6fa8c400253cf75a0ce9285d8ce89141bbb24f2fb6`
SHA-256: `453230f850102bbad10d17b227431d749e58d084f8c15a17dd79ce4f100c52ca`
来源:[`packages/llm/llm/src/message.ts:196`](../packages/llm/llm/src/message.ts)
以下类型之一:
- [`ToolResultMessage`](#persistence-type-sha256-4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731)
- [`DeveloperMessage`](#persistence-type-sha256-f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b)
- [`UserMessage`](#persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586)
- [`DeveloperMessage`](#persistence-type-sha256-19ac4d9fb655ae9f2d9e8993d083d3eb9aede910d0454b892eb6e1290f55179c)
- [`UserMessage`](#persistence-type-sha256-7865a04b33ddf40314f467043e50388a3b6ba3a8b51de7c4ef61ad9910bafcec)
- [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be)
- [`SystemMessage`](#persistence-type-sha256-09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363)
@@ -3936,7 +3960,7 @@ SHA-256: `390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2`
- `"negative"`
- `"positive"`
<a id="persistence-type-sha256-40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945"></a>
<a id="persistence-type-sha256-b1a819c6930a332d1018838361073d9b3e754476e5dd345d479821554446d644"></a>
<a id="persistence-type-messagesource"></a>
@@ -3944,7 +3968,7 @@ SHA-256: `390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2`
### `MessageSource`
SHA-256: `40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945`
SHA-256: `b1a819c6930a332d1018838361073d9b3e754476e5dd345d479821554446d644`
来源:[`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message.ts)
@@ -4058,15 +4082,16 @@ SHA-256: `40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945`
| `user-approval` | `"relay"` | 无 | [`{ kind: "user-approval", form: "relay" }`](#persistence-type-sha256-5cdbf342b7748d152bc987b68f58e565e5bba4b227e076e2d2120d87662e45cd) |
| `user-approval` | `"snapshot"` | `sections`: [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) | [`{ kind: "user-approval", form: "snapshot" }`](#persistence-type-sha256-5654685d7557ec747e74887237065e626a5bdfa426ca43f7fbb0b6fee9b0916c) |
| `user-approval` | 未声明 | 无 | [`{ kind: "user-approval" }`](#persistence-type-sha256-6292bcbedaa97feaec5b48a03bb8144b581b93309d9d6c905f26da7a6a7a0a4f) |
| `user-question-reply` | 未声明 | `callId`: `string`; `outcome`: [`union (2 variants)`](#persistence-type-sha256-3e8ed906c20694e13769d14e7f510ea650bc1977d318ad3375eb5e25d75a8dc3) | [`{ kind: "user-question-reply" }`](#persistence-type-sha256-a88baeb50f7e9485fbdc6d4dfaf26d04210be3c699bca84b97a0e2abafc9bd25) |
| `webhook` | `"notice"` | `deliveryId`: `string`; `provider`: `string`; `ruleId`: `string`; `source`: `string`; `summary`: `string` | [`{ kind: "webhook", form: "notice" }`](#persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94) |
<a id="persistence-type-sha256-fc8242857caf7450d7f68a1f509dce83e237bd21571666006895124cb2246a83"></a>
<a id="persistence-type-sha256-ad6c4e4a7270450d096793e75298c39ec6f91d5e30f211fb8ff24b72695f8dc1"></a>
### `Message[]`
SHA-256: `fc8242857caf7450d7f68a1f509dce83e237bd21571666006895124cb2246a83`
SHA-256: `ad6c4e4a7270450d096793e75298c39ec6f91d5e30f211fb8ff24b72695f8dc1`
[`Message`](#persistence-type-sha256-691c0f641cd3e8072fdacd6fa8c400253cf75a0ce9285d8ce89141bbb24f2fb6) 的数组。
[`Message`](#persistence-type-sha256-453230f850102bbad10d17b227431d749e58d084f8c15a17dd79ce4f100c52ca) 的数组。
<a id="persistence-type-sha256-719c51f67074f97034a6c94297fe1f4b5d3c8d392c57586dab732aa5398c7a91"></a>
@@ -4309,7 +4334,7 @@ SHA-256: `530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6`
SHA-256: `fab2bb09bc5e3b9db0685e8f5500b029af1a2b33e70bc703c1d5d0f596fbc1fd`
来源:[`packages/sandbox/sandbox/src/index.ts:29`](../packages/sandbox/sandbox/src/index.ts)
来源:[`packages/sandbox/sandbox/src/index.ts:30`](../packages/sandbox/sandbox/src/index.ts)
以下类型之一:
@@ -4433,7 +4458,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4`
| `source` | 必需 | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) |
| `title` | 必需 | `string` |
<a id="persistence-type-sha256-9bc85364d1f37d9c837db6a3bdd3d1d007d923cdc7401a3322be9f12fdc225fd"></a>
<a id="persistence-type-sha256-3e94e9061e5069552df9a5568f90c89bec379a9cdf0dc3c122127746312111e9"></a>
<a id="persistence-type-packagessessionsession-title-llmsrcindextssessiontitlellmrequesteventdata"></a>
@@ -4441,7 +4466,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4`
### `SessionTitleLlmRequestEventData`
SHA-256: `9bc85364d1f37d9c837db6a3bdd3d1d007d923cdc7401a3322be9f12fdc225fd`
SHA-256: `3e94e9061e5069552df9a5568f90c89bec379a9cdf0dc3c122127746312111e9`
来源:[`packages/session/session-title-llm/src/index.ts:34`](../packages/session/session-title-llm/src/index.ts)
@@ -4449,7 +4474,7 @@ SHA-256: `9bc85364d1f37d9c837db6a3bdd3d1d007d923cdc7401a3322be9f12fdc225fd`
|---|---|---|
| `maxTokens` | 必需 | `number` |
| `messageSeqs` | 必需 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) |
| `messages` | 必需 | [`Message[]`](#persistence-type-sha256-fc8242857caf7450d7f68a1f509dce83e237bd21571666006895124cb2246a83) |
| `messages` | 必需 | [`Message[]`](#persistence-type-sha256-ad6c4e4a7270450d096793e75298c39ec6f91d5e30f211fb8ff24b72695f8dc1) |
| `route` | 必需 | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) |
| `system` | 必需 | `string` |
| `titleProvider` | 必需 | `string` |
@@ -5103,7 +5128,7 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402`
| `interrupted` | 未声明 | 无 | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) |
| `max-tokens` | 未声明 | 无 | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) |
<a id="persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586"></a>
<a id="persistence-type-sha256-7865a04b33ddf40314f467043e50388a3b6ba3a8b51de7c4ef61ad9910bafcec"></a>
<a id="persistence-type-packagesllmllmsrcmessagetsusermessage"></a>
@@ -5111,7 +5136,7 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402`
### `UserMessage`
SHA-256: `64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586`
SHA-256: `7865a04b33ddf40314f467043e50388a3b6ba3a8b51de7c4ef61ad9910bafcec`
来源:[`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message.ts)
@@ -5120,19 +5145,19 @@ SHA-256: `64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586`
| `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) |
| `id` | 必需 | `string` |
| `role` | 必需 | `"user"` |
| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945) |
| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-b1a819c6930a332d1018838361073d9b3e754476e5dd345d479821554446d644) |
消息源兼容策略:`source` — `session-source-attribution` v1; `session.user-message.source`; `kind`; `preserve`.
仅表示归属的新增 kind:`tmux-context`.
仅表示归属的新增 kind:`tmux-context`, `user-question-reply`.
<a id="persistence-type-sha256-d1f27cf1650e5f59d5d6e95ce6dfd92b31757af076a8ab6418a70b6dbdd53621"></a>
<a id="persistence-type-sha256-94408de87611d1469c83ff7f1a8c6366e4e0ada349ff090c2bd408a024bab080"></a>
### `UserMessage[]`
SHA-256: `d1f27cf1650e5f59d5d6e95ce6dfd92b31757af076a8ab6418a70b6dbdd53621`
SHA-256: `94408de87611d1469c83ff7f1a8c6366e4e0ada349ff090c2bd408a024bab080`
[`UserMessage`](#persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586) 的数组。
[`UserMessage`](#persistence-type-sha256-7865a04b33ddf40314f467043e50388a3b6ba3a8b51de7c4ef61ad9910bafcec) 的数组。
<a id="persistence-type-sha256-c2deb7c9183735d4e0fffddf5c623f4e34daa1c8e96b111982e957dfcbbcf896"></a>
@@ -5381,6 +5406,17 @@ SHA-256: `1ab2ef1296f157ba96998f179bb5daa9d21a82480da2901127aeab2730797c92`
- `"error"`
- `"success"`
<a id="persistence-type-sha256-3e8ed906c20694e13769d14e7f510ea650bc1977d318ad3375eb5e25d75a8dc3"></a>
### `union (2 variants)`
SHA-256: `3e8ed906c20694e13769d14e7f510ea650bc1977d318ad3375eb5e25d75a8dc3`
以下类型之一:
- `"answered"`
- `"dismissed"`
<a id="persistence-type-sha256-50cf02d875f8a1f1411f4a6fbcffab9120ccef5f74a47331fbf29480e5f68f48"></a>
### `union (2 variants)`
@@ -5975,18 +6011,18 @@ SHA-256: `ad0c38a0adb82a94c925456df41586d9b5f9202fca18593eae2213d4445b4585`
| `reason` | 必需 | [`RequestHeaderReason`](#persistence-type-sha256-530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6) |
| `startsSeries` | 可选 | `true` |
<a id="persistence-type-sha256-51c78ccc9023ce6642d07fb9a10cbf942ff7e313d5b304ddc77218df71da708f"></a>
<a id="persistence-type-sha256-c4d3620755d2defbe487d6e64259886b87b5dcff7bd58c8f9d85a06fa6f9bce0"></a>
### `{ headerSeq?, message, step, turn }`
SHA-256: `51c78ccc9023ce6642d07fb9a10cbf942ff7e313d5b304ddc77218df71da708f`
SHA-256: `c4d3620755d2defbe487d6e64259886b87b5dcff7bd58c8f9d85a06fa6f9bce0`
来源:[`packages/core/session/src/types.ts:311`](../packages/core/session/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
| `headerSeq` | 可选 | `number` |
| `message` | 必需 | [`DeveloperMessage`](#persistence-type-sha256-f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b) |
| `message` | 必需 | [`DeveloperMessage`](#persistence-type-sha256-19ac4d9fb655ae9f2d9e8993d083d3eb9aede910d0454b892eb6e1290f55179c) |
| `step` | 必需 | `number` |
| `turn` | 必需 | `number` |
@@ -6070,17 +6106,17 @@ SHA-256: `17d1afb770d9941936130996da00dc86782cfef731d8d6526162c301256a4ac3`
|---|---|---|
| `inherited` | 可选 | `true` |
<a id="persistence-type-sha256-dc4bd7ac827f4b1f9236f9a157a4b4cc171a0588064a611e43edd95f62c68130"></a>
<a id="persistence-type-sha256-b7ae3ff2cea93132e51e04be70f73590c91509e32bcaa34f3260d5503cfa0d3c"></a>
### `{ inserted, outcome?, removedCount?, start, … }`
SHA-256: `dc4bd7ac827f4b1f9236f9a157a4b4cc171a0588064a611e43edd95f62c68130`
SHA-256: `b7ae3ff2cea93132e51e04be70f73590c91509e32bcaa34f3260d5503cfa0d3c`
来源:[`packages/core/agent/src/types.ts:96`](../packages/core/agent/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
| `inserted` | 必需 | [`UserMessage[]`](#persistence-type-sha256-d1f27cf1650e5f59d5d6e95ce6dfd92b31757af076a8ab6418a70b6dbdd53621) |
| `inserted` | 必需 | [`UserMessage[]`](#persistence-type-sha256-94408de87611d1469c83ff7f1a8c6366e4e0ada349ff090c2bd408a024bab080) |
| `outcome` | 可选 | `"canceled"` |
| `removedCount` | 可选 | `number` |
| `start` | 必需 | `number` |
@@ -7339,6 +7375,20 @@ SHA-256: `5654685d7557ec747e74887237065e626a5bdfa426ca43f7fbb0b6fee9b0916c`
| `kind` | 必需 | `"user-approval"` |
| `sections` | 必需 | [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) |
<a id="persistence-type-sha256-a88baeb50f7e9485fbdc6d4dfaf26d04210be3c699bca84b97a0e2abafc9bd25"></a>
### `{ kind: "user-question-reply" }`
SHA-256: `a88baeb50f7e9485fbdc6d4dfaf26d04210be3c699bca84b97a0e2abafc9bd25`
来源:[`packages/interaction/user-questions/src/types.ts:120`](../packages/interaction/user-questions/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
| `callId` | 必需 | `string` |
| `kind` | 必需 | `"user-question-reply"` |
| `outcome` | 必需 | [`union (2 variants)`](#persistence-type-sha256-3e8ed906c20694e13769d14e7f510ea650bc1977d318ad3375eb5e25d75a8dc3) |
<a id="persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94"></a>
### `{ kind: "webhook", form: "notice" }`
@@ -7614,17 +7664,17 @@ SHA-256: `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b`
| `time` | 必需 | `number` |
| `type` | 必需 | `"agent-preset/selected"` |
<a id="persistence-type-sha256-1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c"></a>
<a id="persistence-type-sha256-99a3e14f0b0ff0e38d345e7cb7d8d81ee8d11c51d611d9f91b1a82c8ea481fc4"></a>
<a id="persistence-type-eventagentinboxspliced"></a>
### `{ type: "agent/inbox/spliced" }`
SHA-256: `1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c`
SHA-256: `99a3e14f0b0ff0e38d345e7cb7d8d81ee8d11c51d611d9f91b1a82c8ea481fc4`
| 属性 | 存在性 | 类型 |
|---|---|---|
| `data` | 必需 | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-dc4bd7ac827f4b1f9236f9a157a4b4cc171a0588064a611e43edd95f62c68130) |
| `data` | 必需 | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-b7ae3ff2cea93132e51e04be70f73590c91509e32bcaa34f3260d5503cfa0d3c) |
| `ignorable` | 可选 | `true` |
| `seq` | 必需 | `number` |
| `time` | 必需 | `number` |
@@ -7865,17 +7915,17 @@ SHA-256: `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176`
| `time` | 必需 | `number` |
| `type` | 必需 | `"deliverables/presented"` |
<a id="persistence-type-sha256-eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80"></a>
<a id="persistence-type-sha256-5d588f99e950c42bdd55a27fa41e98f5c86db5b0523622d85b99604c7ddb0107"></a>
<a id="persistence-type-eventdevelopermessage"></a>
### `{ type: "developer/message" }`
SHA-256: `eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80`
SHA-256: `5d588f99e950c42bdd55a27fa41e98f5c86db5b0523622d85b99604c7ddb0107`
| 属性 | 存在性 | 类型 |
|---|---|---|
| `data` | 必需 | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-51c78ccc9023ce6642d07fb9a10cbf942ff7e313d5b304ddc77218df71da708f) |
| `data` | 必需 | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-c4d3620755d2defbe487d6e64259886b87b5dcff7bd58c8f9d85a06fa6f9bce0) |
| `ignorable` | 可选 | `true` |
| `seq` | 必需 | `number` |
| `sourceEventSeqs` | 可选 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) |
@@ -8231,17 +8281,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5`
| `time` | 必需 | `number` |
| `type` | 必需 | `"session/title"` |
<a id="persistence-type-sha256-fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d"></a>
<a id="persistence-type-sha256-64170a859320a56b572372f9ab16e31c4ae9c62454a31418e900ebbf90053c68"></a>
<a id="persistence-type-eventsessiontitle-llm-request"></a>
### `{ type: "session/title-llm-request" }`
SHA-256: `fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d`
SHA-256: `64170a859320a56b572372f9ab16e31c4ae9c62454a31418e900ebbf90053c68`
| 属性 | 存在性 | 类型 |
|---|---|---|
| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-9bc85364d1f37d9c837db6a3bdd3d1d007d923cdc7401a3322be9f12fdc225fd) |
| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-3e94e9061e5069552df9a5568f90c89bec379a9cdf0dc3c122127746312111e9) |
| `ignorable` | 可选 | `true` |
| `seq` | 必需 | `number` |
| `time` | 必需 | `number` |
@@ -8664,17 +8714,17 @@ SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515`
| `type` | 必需 | `"usage"` |
| `usage` | 必需 | [`TokenUsage`](#persistence-type-sha256-fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394) |
<a id="persistence-type-sha256-3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761"></a>
<a id="persistence-type-sha256-1d1697092d04c08f9fcb99015ac082e58b01b7de9e41611fa6aebf2879c3d083"></a>
<a id="persistence-type-eventusermessage"></a>
### `{ type: "user/message" }`
SHA-256: `3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761`
SHA-256: `1d1697092d04c08f9fcb99015ac082e58b01b7de9e41611fa6aebf2879c3d083`
| 属性 | 存在性 | 类型 |
|---|---|---|
| `data` | 必需 | [`UserMessage`](#persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586) |
| `data` | 必需 | [`UserMessage`](#persistence-type-sha256-7865a04b33ddf40314f467043e50388a3b6ba3a8b51de7c4ef61ad9910bafcec) |
| `ignorable` | 可选 | `true` |
| `seq` | 必需 | `number` |
| `sourceEventSeqs` | 可选 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) |
@@ -0,0 +1,28 @@
# Bilingual-pair consistency record for 2026-09-21-user-question-reply.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-09-21-user-question-reply.md
/:
en: 3d5a424ae7d7e46b
zh: c22a8e4a62d8cc1a
/2026-09-21-user-question-reply:
en: de475e4778124528
zh: 2b619b0c0f3a6a6e
/2026-09-21-user-question-reply/summary:
en: 1c7af44ba7eb2983
zh: 05ae869290bb641d
/2026-09-21-user-question-reply/table-of-contents:
en: 6cf314a069ceb451
zh: 9adff678e9ecab3b
/2026-09-21-user-question-reply/declaration:
en: 6907136f28e48c90
zh: 02f8298485f3d8e7
/2026-09-21-user-question-reply/compatibility:
en: 48563f61d63b6259
zh: da7dbdc92f73fed3
/2026-09-21-user-question-reply/verification:
en: c66824c0cdb87d86
zh: d88c29df87fc607b
/2026-09-21-user-question-reply/dev-note:
en: b75c8458928b72b9
zh: 003ba4e17b4b5269
@@ -0,0 +1,60 @@
---
description: "Records a persistence type transition and its compatibility acknowledgement."
kind: persistence-change
---
# 2026-09-21-user-question-reply
English | [中文](2026-09-21-user-question-reply.zh.md)
## Summary
Adds the qualified user-question-reply message source that carries a late answer or dismissal of a continued ask_user_question call into the agent inbox.
## Table of Contents
- [Declaration](#declaration)
- [Compatibility](#compatibility)
- [Verification](#verification)
- [Dev Note](#dev-note)
<a id="declaration"></a>
## Declaration
```yaml persistence-change
schemaVersion: 1
id: 2026-09-21-user-question-reply
baseline: false
changes:
- root: "event:agent/inbox/spliced"
previous: "2026-09-16-session-format-v4"
after: "99a3e14f0b0ff0e38d345e7cb7d8d81ee8d11c51d611d9f91b1a82c8ea481fc4"
decision: same-version
- root: "event:developer/message"
previous: "2026-09-16-session-format-v4"
after: "5d588f99e950c42bdd55a27fa41e98f5c86db5b0523622d85b99604c7ddb0107"
decision: same-version
- root: "event:session/title-llm-request"
previous: "2026-09-16-session-format-v4"
after: "64170a859320a56b572372f9ab16e31c4ae9c62454a31418e900ebbf90053c68"
decision: same-version
- root: "event:user/message"
previous: "2026-09-16-session-format-v4"
after: "1d1697092d04c08f9fcb99015ac082e58b01b7de9e41611fa6aebf2879c3d083"
decision: same-version
```
<a id="compatibility"></a>
## Compatibility
Existing logs contain no such source and remain valid. The kind is a qualified attribution: a reader without dsh-user-questions preserves the user message and its callId and outcome metadata unchanged and derives history from the message content alone. Only the producer's userQuestions projection reads the kind, to close the question it names; it imposes no validation, replay, or authority requirement. No event type is added and the Session header is unchanged.
<a id="verification"></a>
## Verification
pnpm run typecheck: Host and Client faces pass. pnpm --silent run verify-persistence-changes --json: the four affected roots classify as attribution-kind-added without a version bump. Projection and Remote behavior tests are pending.
<a id="dev-note"></a>
## Dev Note
None.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,60 @@
---
description: "记录持久化类型更改及其兼容性确认。"
kind: persistence-change
---
# 2026-09-21-user-question-reply
[English](2026-09-21-user-question-reply.md) | 中文
## 概述
新增受限定的 user-question-reply 消息来源,把已继续的 ask_user_question 调用的迟到回答或放弃送入 agent inbox。
## 目录
- [声明](#declaration)
- [兼容性](#compatibility)
- [验证](#verification)
- [开发备注](#dev-note)
<a id="declaration"></a>
## 声明
```yaml persistence-change
schemaVersion: 1
id: 2026-09-21-user-question-reply
baseline: false
changes:
- root: "event:agent/inbox/spliced"
previous: "2026-09-16-session-format-v4"
after: "99a3e14f0b0ff0e38d345e7cb7d8d81ee8d11c51d611d9f91b1a82c8ea481fc4"
decision: same-version
- root: "event:developer/message"
previous: "2026-09-16-session-format-v4"
after: "5d588f99e950c42bdd55a27fa41e98f5c86db5b0523622d85b99604c7ddb0107"
decision: same-version
- root: "event:session/title-llm-request"
previous: "2026-09-16-session-format-v4"
after: "64170a859320a56b572372f9ab16e31c4ae9c62454a31418e900ebbf90053c68"
decision: same-version
- root: "event:user/message"
previous: "2026-09-16-session-format-v4"
after: "1d1697092d04c08f9fcb99015ac082e58b01b7de9e41611fa6aebf2879c3d083"
decision: same-version
```
<a id="compatibility"></a>
## 兼容性
已有日志不含该来源,仍然有效。该类型是受限定的归属:没有 dsh-user-questions 的读取方原样保留这条用户消息及其 callId 与 outcome 元数据,仅凭消息内容推导历史。只有生产方的 userQuestions projection 读取该类型,用来关闭它指名的问题;它不施加任何校验、回放或权限要求。不新增事件类型,Session header 不变。
<a id="verification"></a>
## 验证
pnpm run typecheck:Host 与 Client 两个 face 通过。pnpm --silent run verify-persistence-changes --json:四个受影响的根均判为 attribution-kind-added,无需版本升级。projection 与 Remote 的行为测试待补。
<a id="dev-note"></a>
## 开发备注
无。
@@ -15,8 +15,8 @@
en: 14c57f30e676f6e3
zh: b1a0ccf26fab3727
/session-persistence-formats/format-references:
en: 45dba01dd6d69c80
zh: c366d1a3991118c9
en: 289041c0dac8994b
zh: a18be2ba190635d4
/session-persistence-formats/scope-and-evidence:
en: b220062139ddac1e
zh: 7c4c0efe05dafa9c
@@ -32,7 +32,7 @@ The index is generated from validated snapshots and the current writer constant.
| 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 |
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 |
| 3 | PR #4320 | [V3](v3.md) | [JSON](v3.schema.json) | 60 / 467 |
| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 62 / 587 |
| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 62 / 592 |
<!-- persistence-format-index:end -->
@@ -32,7 +32,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式
| 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 |
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 |
| 3 | PR #4320 | [V3](v3.zh.md) | [JSON](v3.schema.json) | 60 / 467 |
| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 62 / 587 |
| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 62 / 592 |
<!-- persistence-format-index:end -->
+40340 -39499
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -12,8 +12,8 @@
en: 5e51c6e37c5e30c3
zh: 84bad36357332539
/user-approval/approval-request:
en: dc3647525133ff2f
zh: 8365b809a93ec81d
en: 044ddfc382eb745c
zh: c0010b23c94ab3af
/user-approval/dispatch-and-audit:
en: bd2c2cc1eaaaf2d6
zh: 67bfbed1b7499ffb
Loaded 100 of 660 files, more files were not shown because too many files have changed in this diff. Show more