Merge branch 'master' into fix/issue-4803-webkit-json
This commit is contained in:
commit
4e223acb8a
1284 files changed
+15084
-7477
No files matched your search
@@ -12,8 +12,8 @@
|
||||
en: 055a5e49f9af3dc2
|
||||
zh: 9f7c78409cccfded
|
||||
/agent-notes/archiving-and-deletion:
|
||||
en: 6c393c7058c787d7
|
||||
zh: 70ce9ca3bd9c2133
|
||||
en: 57ccc5fec461ebb0
|
||||
zh: 69e8f9e79abc53aa
|
||||
/agent-notes/when-to-write-one:
|
||||
en: 2beb1b94d8fb633c
|
||||
zh: 889b5542bb771205
|
||||
|
||||
@@ -39,7 +39,7 @@ Delete an implemented Agent Note that only describes small UI adjustments or pur
|
||||
|
||||
The archive is path-encoded as `archived/{class}/yyyy-mm-dd-topic-title.md`; `implemented` is deliberately absent because only implemented notes can enter it. An archival change moves the complete English/Chinese/sidecar triplet, retains `Status: implemented`, inserts the same `Archived: YYYY-MM-DD` line immediately below that status in both language files, re-records the sidecar, and repairs or deletes inbound links. These are the only permitted content changes during archival. Existing title punctuation, blank-line layout, and language-switcher wording do not block archival; preserve them with the body.
|
||||
|
||||
Once sealed, every archived triplet is permanently frozen. Do not edit, translate, reformat, update, move, or delete it, and do not treat it as authority for current behavior. Documentation gates skip archived sources, including their outbound links; active prose may still link into an archived note when it intentionally cites history. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) enforces the closed class tree, complete triplets, archive metadata, sidecar hashes, and the append-only frozen-content manifest. The [archive-policy Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.md) owns the rationale.
|
||||
Once sealed, every archived triplet is permanently frozen. Do not edit, translate, reformat, update, move, or delete it, and do not treat it as authority for current behavior. Documentation gates skip archived sources, including their outbound links; active prose may still link into an archived note when it intentionally cites history. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) enforces the closed class tree, complete triplets, archive metadata, sidecar hashes, and the append-only frozen-content manifest, except for the exact seal transitions documented in [archive instructions](archived/AGENTS.md). The [archive-policy Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.md) owns the rationale.
|
||||
|
||||
## When to write one
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
归档路径编码为 `archived/{class}/yyyy-mm-dd-topic-title.md`;其中有意省略 `implemented`,因为只有 implemented Agent Note 可以进入归档。归档变更会移动完整的英文、中文和伴随记录三个文件,保留 `Status: implemented`,在两种语言的文件中紧接该状态行插入相同的 `Archived: YYYY-MM-DD` 行,重新记录伴随记录,并修复或删除入站链接。归档时只允许对内容做这些更改。 既有标题标点、空行布局和语言切换栏措辞不阻止归档;与正文一起保留。
|
||||
|
||||
封存后,每组归档文件都永久冻结。禁止编辑、翻译、重新格式化、更新、移动或删除,也不得将其视为当前行为的权威依据。文档门禁会跳过归档源文件,包括其中的出站链接;当活跃文档有意引用历史时,仍可链接到归档 Agent Note。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 强制执行封闭的类别目录树、完整的三文件配对、归档元数据、伴随记录 hash,以及仅追加的冻结内容 manifest(元数据清单)。[归档政策 Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.zh.md) 记录了设计依据。
|
||||
封存后,每组归档文件都永久冻结。禁止编辑、翻译、重新格式化、更新、移动或删除,也不得将其视为当前行为的权威依据。文档门禁会跳过归档源文件,包括其中的出站链接;当活跃文档有意引用历史时,仍可链接到归档 Agent Note。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 强制执行封闭的类别目录树、完整的三文件配对、归档元数据、伴随记录 hash,以及仅追加的冻结内容 manifest(元数据清单),但[归档说明](archived/AGENTS.md)中记录的精确封存哈希转换除外。[归档政策 Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.zh.md) 记录了设计依据。
|
||||
|
||||
<a id="when-to-write-one"></a>
|
||||
|
||||
|
||||
@@ -5,3 +5,5 @@ Archived Agent Note triplets under the kind directories are frozen historical sn
|
||||
The archival change may only relocate a complete English/Chinese/sidecar triplet, insert the identical `Archived: YYYY-MM-DD` line below both `Status: implemented` lines, re-record the sidecar, and repair or delete inbound links. Do not inspect, verify, or repair links out of archived notes.
|
||||
|
||||
Run the [`dsh-archive-agent-notes`](../../skills/dsh-archive-agent-notes/SKILL.md) workflow and append new artifact hashes with `pnpm run verify-archived-agent-notes --write`. The normal verifier rejects changed or missing sealed artifacts, incomplete triplets, unknown kind folders, and invalid archive metadata.
|
||||
|
||||
The authorized Figma-link removal from `feature/2026-08-10-durable-workflow-runs-in-chat` has three exact old-to-new seal exceptions in `scripts/archived-agent-notes.ts` for its English, Chinese, and pairing files. These exceptions permit no other content change, reversal, or deletion.
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-09-24-web-default-schedule-composition.md: 3f27bb09ebc00d42c32563e17a1de82c8e525be1
|
||||
2026-09-24-web-default-schedule-composition.zh.md: 9e0e1fb77ce89b579e1bc8b0d4d0a1b9f4e9fea5
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
# Agent Note: Schedule in the shipped Web composition
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-09-25
|
||||
|
||||
English | [中文](2026-09-24-web-default-schedule-composition.zh.md)
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
# Agent Note: 默认 Web 组合中的 Schedule
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-09-25
|
||||
|
||||
[English](2026-09-24-web-default-schedule-composition.md) | 中文
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/archived/feature/2026-08-10-durable-workflow-runs-in-chat.md
|
||||
2026-08-10-durable-workflow-runs-in-chat.md: 01c59ef25996d6349ab2904f148bf4e72fdd5f09
|
||||
2026-08-10-durable-workflow-runs-in-chat.zh.md: 5f6b39b59beca5e2363261605ec58fbdcf71ea6f
|
||||
2026-08-10-durable-workflow-runs-in-chat.md: a49cb75e44bd65ef694679eeb295cfef564c7545
|
||||
2026-08-10-durable-workflow-runs-in-chat.zh.md: 753e02238cd8c00d0494cb673ba9896d61ff4cd8
|
||||
@@ -25,7 +25,7 @@ The renderer gives each level a distinct visual responsibility. The run uses a 3
|
||||
|
||||
Navigation is derived from two current authorities rather than persisted. A member row is interactive only while its durable member state is running and the current ordinary Session list contains the same id with `origin: 'subagent'`, `parentId` equal to the displayed parent, and `running: true`. Underlined member text is the only visible affordance; keyboard focus draws a two-pixel business-primary ring around the name area, and the fixed status label remains the lifecycle word rather than an action instruction. The renderer invokes only the injected ordinary `sessions.open(id)` callback. Addressed-only, remote, wrong-parent, and terminal members remain visible but static.
|
||||
|
||||
The [seven-state Figma reference](https://www.figma.com/design/tguwzZRmHCjbq58mfsqT0M?node-id=5-2) fixes the information hierarchy for running expanded/collapsed, completed history/expanded, failed plus cancelled, interrupted recovery, and dark narrow presentation. Repository `DisclosureRow`, `StateDot`, icons, semantic tokens, and keyed-node behavior remain the implementation authority; the reference introduces no runtime field or state owner.
|
||||
The seven-state Figma reference fixes the information hierarchy for running expanded/collapsed, completed history/expanded, failed plus cancelled, interrupted recovery, and dark narrow presentation. Repository `DisclosureRow`, `StateDot`, icons, semantic tokens, and keyed-node behavior remain the implementation authority; the reference introduces no runtime field or state owner.
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ renderer 为每一层分配不同视觉职责。运行使用 32 像素 module-pl
|
||||
|
||||
导航从两个当前权威派生,不写入持久记录。只有持久成员状态仍为运行中,且当前普通 Session 列表包含同一 id、`origin: 'subagent'`、`parentId` 等于当前父 Session、`running: true` 时,成员行才可交互。带下划线的成员文字是唯一可见提示;键盘聚焦时,名称区显示 2 像素 business-primary 焦点环,固定状态列继续只表达生命周期,而不写动作说明。renderer 只调用注入的普通 `sessions.open(id)` 回调。仅地址化、远程、父级不符或终态成员继续可见,但保持静态。
|
||||
|
||||
[七状态 Figma 参考](https://www.figma.com/design/tguwzZRmHCjbq58mfsqT0M?node-id=5-2)固定运行展开/收起、完成历史/展开、失败与取消、恢复后中断以及暗色窄列的信息层级。仓库的 `DisclosureRow`、`StateDot`、图标、语义 token 和 keyed-node 行为仍是实现权威;参考稿不引入运行时字段或状态 owner。
|
||||
七状态 Figma 参考固定运行展开/收起、完成历史/展开、失败与取消、恢复后中断以及暗色窄列的信息层级。仓库的 `DisclosureRow`、`StateDot`、图标、语义 token 和 keyed-node 行为仍是实现权威;参考稿不引入运行时字段或状态 owner。
|
||||
|
||||
## 验证
|
||||
|
||||
|
||||
@@ -376,6 +376,9 @@
|
||||
"architecture/2026-09-11-wasm-preview-font-and-image-budgets.i18n.yaml": "sha256:af027c7dae53a181e4d73fd2309f69e9ae3c48df0d3b1dfc53cae274aa60d69e",
|
||||
"architecture/2026-09-11-wasm-preview-font-and-image-budgets.md": "sha256:44dd52839cdb67644c0dbcbc4beb6f36802628c4a8d17a0aad11db1f9be649ed",
|
||||
"architecture/2026-09-11-wasm-preview-font-and-image-budgets.zh.md": "sha256:8285432ee1676be8b79a05953c4b6ac4a7ca5d8e701173b3a2b533608b584d0a",
|
||||
"architecture/2026-09-24-web-default-schedule-composition.i18n.yaml": "sha256:8c3d5c614924d0e81f361a299427ac697aefc6189c41eb3f4151f5c42e371ca0",
|
||||
"architecture/2026-09-24-web-default-schedule-composition.md": "sha256:3a139593ade13f95cbb67094c1eec66cb0a2c14bcc154512799803b2a7d22ab4",
|
||||
"architecture/2026-09-24-web-default-schedule-composition.zh.md": "sha256:59908f7987058742d1226dc8a801d03c265c84d51172249ba51716b91f084f0c",
|
||||
"bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.i18n.yaml": "sha256:8be5b0afd8820c593e2ec254d35fb8c384267814104f6234bb9af824c5f974ba",
|
||||
"bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.md": "sha256:861e6130e893489271478def5f54f212a106a1580b744e6e40f56356f73c7e10",
|
||||
"bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.zh.md": "sha256:3549257e06f074591de580ebfaa71284c7c384fb0ffb1963c30258d7a7c26db7",
|
||||
@@ -1264,9 +1267,9 @@
|
||||
"feature/2026-08-10-creator-guidance-introduce-cue.i18n.yaml": "sha256:74f519839f0cf82c7304bdeae41ae1cab8bb930bfb94f79ab44708acd3b72128",
|
||||
"feature/2026-08-10-creator-guidance-introduce-cue.md": "sha256:3e25409dda498de150de18943ee332e1760963e377a40a365902b66f667fdc9f",
|
||||
"feature/2026-08-10-creator-guidance-introduce-cue.zh.md": "sha256:203847010cab9e9d13c3969f17921d3a5aa0d69377eccfef555c7ff96572f162",
|
||||
"feature/2026-08-10-durable-workflow-runs-in-chat.i18n.yaml": "sha256:0db3c21a3c8785e56d5d7b90df990b158069cd1cea664e62fc4786c6d7588450",
|
||||
"feature/2026-08-10-durable-workflow-runs-in-chat.md": "sha256:f9f5290cd880908d17b1080ae5776f22e182f5253471f90cf8bead418b33b502",
|
||||
"feature/2026-08-10-durable-workflow-runs-in-chat.zh.md": "sha256:f4ffd2700bc9c2a0cc4b9d90f84e1d0a576939e2e6f0b130c5b142fd8cd518b0",
|
||||
"feature/2026-08-10-durable-workflow-runs-in-chat.i18n.yaml": "sha256:03eadb60c8ba4fde262cd61e3f911d8cd68ea4a58f57adeb8dd7bb9a97d08ca2",
|
||||
"feature/2026-08-10-durable-workflow-runs-in-chat.md": "sha256:6018de4a89ca99d6cbfd618aff1c8136b23d8e4854a30f7b12a602f0417cbdc2",
|
||||
"feature/2026-08-10-durable-workflow-runs-in-chat.zh.md": "sha256:0080859221773122022d5880efa3d52f29c0872b52d63e170250d94f214ff37a",
|
||||
"feature/2026-08-10-minimal-read-image-tool.i18n.yaml": "sha256:2311b6453b90a1f81d4fcbdaa723ab233cc0998f34393956364c8b92bc4d1213",
|
||||
"feature/2026-08-10-minimal-read-image-tool.md": "sha256:debe6b570d11634f57a80eaa5891e4fd1e7a5aac19f10fb7dd1683c316515150",
|
||||
"feature/2026-08-10-minimal-read-image-tool.zh.md": "sha256:572d080340682b354cb1f8a0f74af153d6542bd3dffbe3b72b32591e5d5d3baf",
|
||||
|
||||
+6
-6
@@ -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
|
||||
+3
-3
@@ -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.
|
||||
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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 录制验证各自声明的上传策略,无需重写已提交的会话代际。
|
||||
@@ -15,5 +15,5 @@
|
||||
en: 21d7ec9a9ff3cbf2
|
||||
zh: 93d03b8b3012aa7e
|
||||
/agent-note-host-owned-scheduled-messages/consequences:
|
||||
en: 0358aab730e4bf56
|
||||
zh: 61e89e0374891f8a
|
||||
en: a48f13f89a1944b1
|
||||
zh: f8fa677122ce2e12
|
||||
@@ -58,7 +58,7 @@ Session inbox persistence and the task put occur in separate persistence systems
|
||||
|
||||
**Keep the mock's hourly choice out of the stored vocabulary.** The approved mock lists 每小时整点 beside the daily, weekly and one-shot choices. Hourly and fixed-rate intervals are the same elapsed-time rule, so the card maps that menu slot onto the existing interval kind instead of adding a persisted kind. A dedicated hourly kind would need a new durable variant, its decoder, its target resolution, and its tool schema, all to express what an interval already expresses.
|
||||
|
||||
Task discovery and timers do not depend on loaded Sessions. The after, absolute-time, and fixed-rate selectors retain their semantics alongside Daily, Weekly, and Cron; reading existing saved tasks does not rewrite them. The Host must have its ordinary Session persistence and recovery services available, and the Web bundle mounts Schedule in its host row list. Explicit deletion stops future delivery and removes the task from `list` and `catalog` without deleting conversation messages; the removed row takes its saved receipts with it. History is pruned to the configured window and record cap when an acknowledgment is appended, loaded into Host memory, and rewritten with the task on each acknowledgment. Pagination limits the returned record count; the plugin configuration (`deliveryHistoryDays`, default 30, and `deliveryHistoryRecords`, default 200) limits storage growth. Earlier overwritten receipts and deliveries whose task write failed cannot be reconstructed. Pause and execution-history operations remain unsupported.
|
||||
Task discovery and timers do not depend on loaded Sessions. The after, absolute-time, and fixed-rate selectors retain their semantics alongside Daily, Weekly, and Cron; reading existing saved tasks does not rewrite them. The Host must have its ordinary Session persistence and recovery services available, and enabling the optional experimental bundle `@deepseek-ai/dsh-experimental-schedule-bundle` mounts Schedule; the Web composition carries its row disabled. Explicit deletion stops future delivery and removes the task from `list` and `catalog` without deleting conversation messages; the removed row takes its saved receipts with it. History is pruned to the configured window and record cap when an acknowledgment is appended, loaded into Host memory, and rewritten with the task on each acknowledgment. Pagination limits the returned record count; the plugin configuration (`deliveryHistoryDays`, default 30, and `deliveryHistoryRecords`, default 200) limits storage growth. Earlier overwritten receipts and deliveries whose task write failed cannot be reconstructed. Pause and execution-history operations remain unsupported.
|
||||
|
||||
Required coverage includes startup from stored tasks, missing-status normalization, cold Session restoration, latest-only catch-up, inactive-task retention without repeat dispatch, real message receipts, deletion across restart, removal of a deleted task's saved deliveries, queue/persistence failures, and disposal. Wall-clock obligations include gaps and skipped dates, earlier-only overlaps, fractional seconds, UTC-range edges, committed-target and alias preservation, and post-restoration clock rollback. Weekly adds weekday normalization and validation, next-target selection across a gap and an overlap, and stored set preservation. Historical decoders must reject Daily, Weekly, and Cron while Host storage accepts them. Client coverage includes every recurring kind, exact rule time, zone, and weekday set, All/Enabled/Inactive status filters, retained inactive details and original-Session navigation, authoritative refresh after confirmed deletion and notification, empty lists, reconnect, and late responses. Presentation coverage includes the page's New action starting a Session, the transcript card's narrowing of persisted result JSON including an incomplete or replayed result, the header clock's direct open for exactly one task and its count label, the idle-only row mark with the hover-card two-task limit and omission line. History coverage includes legacy byte preservation, append and restart, immutable sent instructions, strict latest-receipt consistency, stable cursor pagination, binding checks, failed persistence, and late page replies during refresh or task changes. Cross-store crash duplication remains an explicit limitation rather than a completion claim.
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ Session 收件箱持久化和任务 put 发生在两个持久化系统中。收
|
||||
|
||||
**不把 mock 的「每小时整点」引入持久词表。** 设计稿把它与每天、每周、仅一次并列。每小时整点与固定间隔在语义上是同一种按经过时间计量的规则,因此详情里的该项直接映射到既有的间隔类型,而不是新增一个持久种类。若真要新增,需要新的持久变体、解码器、目标解析与工具 schema,而它表达的能力现有间隔已经覆盖。
|
||||
|
||||
任务发现和 timer 不依赖已加载的 Session。延迟、绝对时间和固定速率选择器在与 Daily、Weekly、Cron 并存时保留自身语义;读取已有任务不会重写它们。Host 必须提供普通 Session 持久化和恢复服务,Web bundle 在其宿主行列表中挂载 Schedule。显式删除会阻止后续投递,并将任务从 `list` 和 `catalog` 中移除,不删除对话消息;被移除的行会带走其已保存回执。历史在追加确认时按配置的窗口与条数上限裁剪,随后加载到 Host 内存,并在每次确认时随任务一起重写。分页限制返回记录数量,不限制存储增长或指令字节数。此前被覆盖的回执,以及任务写入失败的投递记录,无法重建。暂停和执行历史操作仍不受支持。
|
||||
任务发现和 timer 不依赖已加载的 Session。延迟、绝对时间和固定速率选择器在与 Daily、Weekly、Cron 并存时保留自身语义;读取已有任务不会重写它们。Host 必须提供普通 Session 持久化和恢复服务,而挂载 Schedule 需要启用可选实验 bundle `@deepseek-ai/dsh-experimental-schedule-bundle`;Web 组合以禁用状态携带其条目。显式删除会阻止后续投递,并将任务从 `list` 和 `catalog` 中移除,不删除对话消息;被移除的行会带走其已保存回执。历史在追加确认时按配置的窗口与条数上限裁剪,随后加载到 Host 内存,并在每次确认时随任务一起重写。分页限制返回记录数量,不限制存储增长或指令字节数。此前被覆盖的回执,以及任务写入失败的投递记录,无法重建。暂停和执行历史操作仍不受支持。
|
||||
|
||||
必要覆盖范围包括从已存储任务启动、缺失状态规范化、冷 Session 恢复、仅补发最新发生时点、保留未运行任务且不重复投递、真实消息回执、重启后的删除结果、删除会移除已保存发送记录、排队或持久化失败,以及销毁。钟表规则的验证要求包括缺口和跳过的日期、仅选择较早时点的重叠、小数秒、UTC 范围边缘、已提交目标和别名保留,以及恢复后的时钟回拨。Weekly 另需覆盖星期集合的规范化与校验、跨缺口与重叠的下一目标选择,以及已存集合的保留。历史解码器必须拒绝 Daily、Weekly 与 Cron,而宿主存储接受它们。客户端覆盖范围包括每种重复规则、精确规则时间、时区和星期集合、全部/已开启/未运行状态筛选、保留未运行任务详情和原 Session 导航、确认删除和通知后的权威刷新、空列表、重新连接和晚到响应。呈现覆盖范围包括页面“新建”操作打开新会话、transcript 卡片从不完整或回放的结果 JSON 中收窄任务、页头时钟在恰好一个任务时直接打开并给出数量标签、仅空闲行标记及其悬浮卡片两个任务上限与遗漏行。历史覆盖范围包括旧文件字节保留、追加与重启、不可变的发送指令、最近回执一致性严格校验、稳定游标分页、绑定校验、持久化失败,以及刷新或切换任务期间晚到的分页响应。跨存储崩溃导致的重复投递仍是明确限制,不能被表述为完成保证。
|
||||
|
||||
|
||||
+4
-4
@@ -9,11 +9,11 @@
|
||||
en: 2ba6c43b20a3d347
|
||||
zh: 5647179ddaa38ad9
|
||||
/agent-note-ship-experimental-capabilities-as-optional-bundles/decision:
|
||||
en: 1c5a43b3ce2d3056
|
||||
zh: b59f549a12cf67d1
|
||||
en: fdd54ede661f7a83
|
||||
zh: a41ed1a872bb8ba3
|
||||
/agent-note-ship-experimental-capabilities-as-optional-bundles/alternatives-considered:
|
||||
en: a65d84004e48b3ef
|
||||
zh: 17221f08e7a8a89b
|
||||
/agent-note-ship-experimental-capabilities-as-optional-bundles/consequences:
|
||||
en: d52ed0a248131a37
|
||||
zh: 779b8d415281c5da
|
||||
en: e7fb2a93a89ba9b6
|
||||
zh: 37ed9f0a3ca44583
|
||||
+2
-2
@@ -10,7 +10,7 @@ The Web plugin page offered two optional bundles, Agent Teams and voice input. A
|
||||
|
||||
## Decision
|
||||
|
||||
`OPTIONAL_BUNDLES` lists Agent Teams, voice input, and Auto review. Every optional bundle declares `icon` and exports `./locale/*.json` with `meta.title` and `meta.description`, so the Official group renders a localized title, description, and image; `verify-default-product-isolation` rejects an optional bundle without them. The Inspector's `cordis.patch.yml` names the package instead of a built-artifact path, so the same file is the bundle patch and the `--patch` overlay for a built Web launch.
|
||||
`OPTIONAL_BUNDLES` lists Agent Teams, voice input, Auto review, and the Schedule bundle. Every optional bundle declares `icon` and exports `./locale/*.json` with `meta.title` and `meta.description`, so the Official group renders a localized title, description, and image; `verify-default-product-isolation` rejects an optional bundle without them. The Inspector's `cordis.patch.yml` names the package instead of a built-artifact path, so the same file is the bundle patch and the `--patch` overlay for a built Web launch.
|
||||
|
||||
An optional bundle is a runtime dependency of the installation, so its dependency graph is downloaded by every `dsh` install. The list therefore admits only packages whose graph is already in the installation's closure; Auto review adds nothing. The Inspector stays available through explicit installation and does not appear in the default plugin list. The browser-use and computer-use providers stay explicit compositions: Playwright MCP, Chrome DevTools MCP, and the native Cua Driver runtime binaries add about 85 MB and 21 packages to every install whether or not the bundle is enabled, and a shipped Cua Driver MCP switch would offer a capability whose executable the installation does not carry. Those provider packages keep locale display metadata for their component rows. Two more packages stay out for other reasons: `ptc-runtime-python` replaces the PTC runtime, and `workflow-ptc` rejects a non-TypeScript runtime at load while the Web presets carry `workflow-ptc` rows a bundle patch cannot reach; `browser-use-stagehand-native` requires a native model name and API key at schema validation with no configuration form on the page.
|
||||
|
||||
@@ -22,4 +22,4 @@ An optional bundle is a runtime dependency of the installation, so its dependenc
|
||||
|
||||
## Consequences
|
||||
|
||||
The Official group grows from two to three entries, each tagged experimental by its experimental name. The runtime dependency closure of the installation is unchanged. A browser-use or computer-use provider still needs its Service Definition and the provider in a profile patch or composition.
|
||||
The Official group carries four optional-bundle entries, each tagged experimental by its experimental name. The runtime dependency closure of the installation is unchanged. A browser-use or computer-use provider still needs its Service Definition and the provider in a profile patch or composition.
|
||||
+2
-2
@@ -10,7 +10,7 @@ Web 插件页只提供两个可选 bundle:Agent Teams 与语音输入。Auto r
|
||||
|
||||
## 决策
|
||||
|
||||
`OPTIONAL_BUNDLES` 列出 Agent Teams、语音输入与 Auto review。每个可选 bundle 都声明 `icon` 并导出带 `meta.title` 与 `meta.description` 的 `./locale/*.json`,官方分组因此能渲染本地化标题、描述和图片;`verify-default-product-isolation` 会拒绝缺少这些声明的可选 bundle。Inspector 的 `cordis.patch.yml` 直接写包名而非构建产物路径,所以同一文件既是 bundle patch,也是已构建 Web 启动的 `--patch` overlay。
|
||||
`OPTIONAL_BUNDLES` 列出 Agent Teams、语音输入、Auto review 与 Schedule bundle。每个可选 bundle 都声明 `icon` 并导出带 `meta.title` 与 `meta.description` 的 `./locale/*.json`,官方分组因此能渲染本地化标题、描述和图片;`verify-default-product-isolation` 会拒绝缺少这些声明的可选 bundle。Inspector 的 `cordis.patch.yml` 直接写包名而非构建产物路径,所以同一文件既是 bundle patch,也是已构建 Web 启动的 `--patch` overlay。
|
||||
|
||||
可选 bundle 是安装的运行时依赖,其依赖图会随每次 `dsh` 安装一起下载。因此名单只接纳依赖图已在安装闭包内的包;Auto review 不增加任何依赖。Inspector 保留显式安装方式,不在默认插件列表中显示。浏览器操作与电脑操作提供方仍保持显式组合:Playwright MCP、Chrome DevTools MCP 与原生 Cua Driver 运行时二进制会让每次安装多出约 85 MB、21 个包,无论 bundle 是否启用;而随附的 Cua Driver MCP 开关会提供一个安装本身并不携带其可执行文件的能力。这些提供方包保留 locale 显示元数据,供组件行使用。另有两个包因其他原因不纳入:`ptc-runtime-python` 会替换 PTC 运行时,而 `workflow-ptc` 在加载时拒绝非 TypeScript 运行时,且 Web preset 内含 bundle patch 无法触及的 `workflow-ptc` 行;`browser-use-stagehand-native` 在 schema 校验时就要求原生模型名称与 API 密钥,而插件页没有对应的配置表单。
|
||||
|
||||
@@ -22,4 +22,4 @@ Web 插件页只提供两个可选 bundle:Agent Teams 与语音输入。Auto r
|
||||
|
||||
## 后果
|
||||
|
||||
官方分组从两项增至三项,每项都因实验包名而带实验性标签。安装的运行时依赖闭包保持不变。浏览器操作或电脑操作提供方仍需在 profile patch 或组合中同时挂载其 Service Definition 与提供方。
|
||||
官方分组包含四个可选 bundle 条目,每项都因实验包名而带实验性标签。安装的运行时依赖闭包保持不变。浏览器操作或电脑操作提供方仍需在 profile patch 或组合中同时挂载其 Service Definition 与提供方。
|
||||
+19
@@ -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 的适配器上的同一回退。
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-09-24-schedule-opt-in-optional-bundle.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-schedule-opt-in-optional-bundle.md
|
||||
/agent-note-schedule-as-an-opt-in-optional-bundle:
|
||||
en: 8c2381fd778a9888
|
||||
zh: 8877255cb980eea1
|
||||
/agent-note-schedule-as-an-opt-in-optional-bundle/problem:
|
||||
en: bb23c2f7f93a462b
|
||||
zh: 9fb600fc8e203323
|
||||
/agent-note-schedule-as-an-opt-in-optional-bundle/decision:
|
||||
en: 483d1dd51b9cc073
|
||||
zh: b069e2e2385c8132
|
||||
/agent-note-schedule-as-an-opt-in-optional-bundle/alternatives-considered:
|
||||
en: 8e5f4d8fa8c2e78d
|
||||
zh: 3353c7042c6a1d6b
|
||||
/agent-note-schedule-as-an-opt-in-optional-bundle/consequences:
|
||||
en: 36cd0eb71abeedbe
|
||||
zh: 7c134095cc19fcfd
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Schedule as an opt-in optional bundle
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-24-schedule-opt-in-optional-bundle.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Schedule adds four tool schemas to every live root Agent request and one durable clock message per eligible step, so the shipped Web composition must not mount it by default. A person using the shipped Web profile still needs a product-surface switch to turn it on; a profile patch layer or a `--patch` overlay is a configuration file that person cannot reach.
|
||||
|
||||
## Decision
|
||||
|
||||
`packages/bundle/web-app/cordis.patch.yml` carries none of `time-context`, `schedule`, and `ui-schedule`, so the shipped Web composition exposes no Schedule surface: no `schedule_*` tools, no Session reminder catalog, no Automation tasks page, and no per-step clock reading.
|
||||
|
||||
`@deepseek-ai/dsh-experimental-schedule-bundle` (`packages/experimental/schedule-bundle/`) inserts the three rows in its `cordis.patch.yml` and depends on their packages, as every other optional bundle inserts the rows it ships. `OPTIONAL_BUNDLES` in `packages/boot/app-boot/src/profile.ts` names the package, and `apps/cli` declares it as a runtime dependency, so every installation ships it switched off. The [experimental-as-optional-bundles decision](2026-09-21-experimental-capabilities-as-optional-bundles.md) owns the `OPTIONAL_BUNDLES` conventions and the localized `icon` and `meta.title` / `meta.description` metadata the Web Plugins page renders in its Official group.
|
||||
|
||||
Enabling the bundle inserts `time-context` (a per-step clock reading with the sampled instant, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message), `schedule` (durable reminders plus `schedule_create`, `schedule_list`, `schedule_update`, and `schedule_delete` on live root Agents), and `ui-schedule` (the Session reminder catalog and the Automation tasks page). The bundle carries `time-context` with `schedule` because a reminder request states a wall-clock target: the sampled instant is what lets the model turn a request such as "tomorrow at nine" into an offset-bearing `at` value. The [Schedule subsystem](../../../../docs/subsystems/schedule.md) owns the durable records, delivery, and management operations; the [Web bundle](../../../../packages/bundle/web-app/README.md) owns the composition the bundle adds the rows to. Disabling the bundle restores the shipped composition, and the Host stops scheduling while the `schedule` row is absent; stored task records remain in the Schedule domain.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Ship Schedule on by default.** Every Web session then pays four tool schemas in each request header and one durable user message per eligible step, and a conversation that never creates a reminder pays both costs. The arrangement also left the capability without an off switch on the product surface.
|
||||
|
||||
**Keep a `--patch` overlay file.** An overlay is a launch-time argument rather than a product-surface switch, so a person using the shipped Web profile cannot reach it. Re-declaring the rows through its `insert` list does not override them: `applyEntryPatches` appends that list without de-duplicating ids, and the Loader collapses the composed list to one entry per id with the last declaration winning, so the overlay replaces the Web bundle's rows instead of setting fields on them.
|
||||
|
||||
**Keep the rows in the Web composition with `disabled: true` and switch them on with id-targeted patches.** The bundle would then set fields on existing rows, but the plugin manager lists only the rows a bundle inserts, so the bundle's page reported no components and needed a second listing path for overridden rows, while every other optional bundle inserts its rows. A profile that also declares one of the ids resolves the same way as for any other bundle: the Loader keeps the last declaration.
|
||||
|
||||
**Lock the three rows to the bundle switch.** A bundle manifest field that makes the plugin manager refuse to switch the rows one by one adds a plugin-manager concept only this bundle would use. The bundle's page offers a switch per row, as for every other bundle, and the bundle README records that the three rows work only together.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A default `dsh web` session carries no Schedule tool schemas, no Session reminder catalog, no Automation tasks page, and no per-step clock reading. An installation that wants them enables the optional bundle from the Plugins page or lists the package in a profile's `dsh.profile.bundles`.
|
||||
- An enabled installation adds four tool schemas to every live root Agent and one durable clock message per eligible step; the reading is model-visible and durable, so it replays, compacts, and appears in exported Session logs like any other user message.
|
||||
- The Plugins page lists the three rows under the bundle with the titles their packages' `locale/*.json` declare, their state, and, while the bundle is on, a switch per row; switching one of them off leaves Schedule without that part.
|
||||
- A profile patch or `--patch` overlay that targets one of the three ids matches no row while the bundle is not selected, and the loader warns `patch: entry <id> not found` for it; selecting the bundle replaces switching the rows on by id.
|
||||
- The switch is configuration-only: `src/index.ts` is an empty module, the patch carries the runtime content, and the package owns no mutable runtime state, so it publishes no invariant companion.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Schedule 作为按需开启的可选 bundle
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-24-schedule-opt-in-optional-bundle.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
Schedule 会给每个活跃根 Agent 的请求增加四个工具 schema,并在每个符合条件的步骤追加一条持久时钟消息,因此随发行版交付的 Web 组合不能默认挂载它。使用随发行版 Web profile 的人仍需要产品界面上的开关来打开它;profile patch 层或 `--patch` overlay 都是这个人无法触及的配置文件。
|
||||
|
||||
## Decision
|
||||
|
||||
`packages/bundle/web-app/cordis.patch.yml` 不含 `time-context`、`schedule` 与 `ui-schedule` 中的任何一行,因此随发行版交付的 Web 组合不提供任何定时能力:没有 `schedule_*` 工具、没有 Session 提醒目录、没有自动化任务页面,也没有逐步时钟读数。
|
||||
|
||||
`@deepseek-ai/dsh-experimental-schedule-bundle`(`packages/experimental/schedule-bundle/`)在其 `cordis.patch.yml` 中插入这三行并依赖它们的包,与其他可选 bundle 插入各自随附的行相同。`packages/boot/app-boot/src/profile.ts` 的 `OPTIONAL_BUNDLES` 列出该包,`apps/cli` 将其声明为运行时依赖,因此每次安装都随包携带且默认关闭。[实验性能力作为可选 bundle 的决策](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)负责 `OPTIONAL_BUNDLES` 的约定,以及 Web 插件管理页在“官方”分组中渲染的本地化 `icon` 与 `meta.title` / `meta.description` 元数据。
|
||||
|
||||
启用该 bundle 会插入 `time-context`(每个步骤的时钟读数,包含采样瞬时、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长)、`schedule`(持久提醒,以及每个活跃根 Agent 上的 `schedule_create`、`schedule_list`、`schedule_update` 与 `schedule_delete`)与 `ui-schedule`(Session 提醒目录与自动化任务页面)。该 bundle 把 `time-context` 与 `schedule` 一起携带,因为提醒请求给出的是挂钟时间目标:采样瞬时正是让模型把“明天九点”这类请求转换成带偏移量的 `at` 值的依据。持久记录、投递与管理操作由 [Schedule 子系统](../../../../docs/subsystems/schedule.zh.md)负责;该 bundle 向其加入这些行的组合由 [Web bundle](../../../../packages/bundle/web-app/README.zh.md) 负责。禁用该 bundle 会恢复随发行版交付的组合,`schedule` 行不存在期间 Host 不再调度;已存储的任务记录保留在 Schedule domain 中。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**默认发布 Schedule。** 那样每个 Web 会话都要在每个请求头中带上四个工具 schema,并在每个符合条件的步骤追加一条持久 user 消息,从不创建提醒的对话也要承担这两项成本。这种安排还让该能力在产品界面上没有关闭开关。
|
||||
|
||||
**保留一个 `--patch` overlay 文件。** overlay 是启动时的参数,而不是产品界面上的开关,因此使用随发行版 `web` profile 的人无法触及它。通过它的 `insert` 列表重新声明这些条目并不能覆盖它们:`applyEntryPatches` 追加该列表时不对 id 去重,而 Loader 会把组合出的列表按 id 收敛为一个条目、最后一个声明生效,因此该 overlay 会替换 Web Bundle 的条目,而不是给它们设值。
|
||||
|
||||
**把这三行以 `disabled: true` 留在 Web 组合中,用按 id 定位的 patch 打开。** 这样 bundle 只是给已有的行设值,但插件管理器只列出 bundle 插入的行,因此该 bundle 的页面显示不含任何组件,需要为被覆盖的行另建一条列出路径;而其他可选 bundle 都插入各自的行。某个 profile 若也声明了其中某个 id,处理方式与任何其他 bundle 相同:Loader 保留最后一份声明。
|
||||
|
||||
**把这三行锁定在 bundle 开关上。** 让插件管理器拒绝逐行开关的 bundle 清单字段,会给插件管理器增加一个只有这个 bundle 使用的概念。该 bundle 的页面像其他所有 bundle 一样为每行提供开关,由 bundle README 说明这三行只能一起工作。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 默认的 `dsh web` 会话不带 Schedule 工具 schema、Session 提醒目录、自动化任务页面与逐步时钟读数。需要它们的安装可从插件管理页启用该可选 bundle,或把该包列进 profile 的 `dsh.profile.bundles`。
|
||||
- 启用后的安装会给每个活跃根 Agent 增加四个工具 schema,并在每个符合条件的步骤追加一条持久时钟消息;该读数对模型可见且持久,因此它与其他 user 消息一样参与回放、压缩,并出现在导出的 Session 日志中。
|
||||
- 插件管理页在该 bundle 下列出这三行,显示各自包的 `locale/*.json` 声明的标题、它们的状态,并在 bundle 打开时为每行提供开关;关掉其中一行会让 Schedule 缺少这一部分。
|
||||
- 未选中该 bundle 时,按 id 定位这三行之一的 profile 补丁或 `--patch` overlay 匹配不到任何条目,加载器会为它报告 `patch: entry <id> not found` 警告;选中该 bundle 即可取代按 id 打开这些行。
|
||||
- 该开关只涉及配置:`src/index.ts` 是空模块,运行时内容由 patch 承载,包本身不拥有可变的运行时状态,因此不发布不变量伴随模块。
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
# Bilingual-pair consistency record for 2026-09-24-web-default-schedule-composition.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-web-default-schedule-composition.md
|
||||
/agent-note-schedule-in-the-shipped-web-composition:
|
||||
en: 39229d7ab6ed433b
|
||||
zh: 54287471d371a11c
|
||||
/agent-note-schedule-in-the-shipped-web-composition/problem:
|
||||
en: 2871fd5ed881c007
|
||||
zh: d03c78d402d186da
|
||||
/agent-note-schedule-in-the-shipped-web-composition/decision:
|
||||
en: bb19ad12b2ab0ffc
|
||||
zh: 971f33e95474a36c
|
||||
/agent-note-schedule-in-the-shipped-web-composition/recorded-sessions:
|
||||
en: 5539a42052f4ddb7
|
||||
zh: 98cc190ae63b4549
|
||||
/agent-note-schedule-in-the-shipped-web-composition/alternatives-considered:
|
||||
en: ca032a407c2ee8cb
|
||||
zh: 748a4fcf2485a3da
|
||||
/agent-note-schedule-in-the-shipped-web-composition/consequences:
|
||||
en: 4f23bda8fda37987
|
||||
zh: 9f7c4a3538bd2979
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-09-25-session-log-otel-byte-limits.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-25-session-log-otel-byte-limits.md
|
||||
/agent-note-session-log-otlp-strings-and-bounded-requests:
|
||||
en: 041f7e2b55202ea3
|
||||
zh: e01834ae6c9fc7bc
|
||||
/agent-note-session-log-otlp-strings-and-bounded-requests/problem:
|
||||
en: 20f659a15fb55352
|
||||
zh: 49d39c95da1b8aa5
|
||||
/agent-note-session-log-otlp-strings-and-bounded-requests/decision:
|
||||
en: 0f0866232326658b
|
||||
zh: aa608d17ba712d8a
|
||||
/agent-note-session-log-otlp-strings-and-bounded-requests/alternatives-considered:
|
||||
en: 0dc933b18a1a3573
|
||||
zh: 77655740322c4e45
|
||||
/agent-note-session-log-otlp-strings-and-bounded-requests/consequences:
|
||||
en: 3859569c9cdea30c
|
||||
zh: 517449b8c49807f6
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: Session-log OTLP strings and bounded requests
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-25-session-log-otel-byte-limits.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Session logs contain nested JSON and large tool results. The product collector accepts named records with string content and limits request size; count-only SDK batching cannot bound bytes after UTF-8 encoding and JSON escaping.
|
||||
|
||||
## Decision
|
||||
|
||||
The shared `otel` Cordis plugin owns ordinary-event and Session-log transport implementations. Business adapters inject it and create independent caller-owned channels; their existing UI/RPC entry points retain authorization, redaction, identity, configuration, and bounded shutdown. Mounting the shared service creates no unused queues. Each canonical event becomes `eventName: "session-log"`, with `sessionId` and JSON-stringified event values in `content`; serialization does not preserve JSONL key ordering. Redaction runs before serialization and the copied envelope excludes the payload.
|
||||
|
||||
Session logs use a separate provider and byte/count queue. Each record is measured once with the SDK serializer including a full envelope; the sum conservatively bounds a combined request. Greedy packing avoids full-candidate serialization and recursive re-encoding. Single oversized records receive one diagnostic and do not block later events. One HTTP request owns the transport slot until settlement, even after its watchdog fires; shutdown expiry stops the remaining queue. Scope name/version and explicit transport options remain owned by the backend.
|
||||
|
||||
The [telemetry revival](../feature/2026-07-23-session-telemetry-otel-revival.md) remains authoritative for redaction, capture, and best-effort handoff. This decision replaces its SDK-only queue/batching rule: SDK batch deadlines cannot own an operation split into multiple HTTP requests. The shared Session-log channel schedules complete requests; the SDK still owns transport/retry. Feedback authorization and the separate DeepSeek model-request contribution remain unchanged.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**One string for the entire Session.** Rejected because one large Session would exceed the request ceiling even when each event fits. One event per record preserves event identity and permits batching.
|
||||
|
||||
**Truncate oversized records or add client-only fragments.** Rejected because truncation loses content and fragments require a collector reassembly protocol. Explicit rejection retains the one-event-per-record meaning.
|
||||
|
||||
**Count compressed bytes or characters.** Rejected because collector limits can apply after decompression, and characters omit UTF-8 and escaping overhead. Measuring full uncompressed requests gives a conservative bound.
|
||||
|
||||
## Consequences
|
||||
|
||||
Product and Session records never share a request or queue, and neither package imports the other. Conservative per-record envelope accounting can produce smaller batches than exact combined accounting. Queue overflow, oversize rejection, exit, and transport failure can lose data; no durable outbox or acknowledgement is added. Tests cover byte limits, linear measurement, slow-request serialization, metadata, redaction-copy isolation, and feedback-authorized replay.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: Session 日志的 OTLP 字符串与有界请求
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-25-session-log-otel-byte-limits.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Session 日志包含嵌套 JSON 和大型工具结果。产品采集端接收带名称和字符串内容的记录,并限制请求大小;仅按数量分批的 SDK 无法约束 UTF-8 编码与 JSON 转义后的字节数。
|
||||
|
||||
## 决策
|
||||
|
||||
共享 `otel` Cordis 插件负责普通事件和 Session 日志的传输实现。业务适配器注入它并创建独立、由调用方持有的通道;现有 UI/RPC 入口保留授权、脱敏、身份、配置和有期限的关闭。只挂载共享服务不会创建无用队列。每条规范事件转换为 `eventName: "session-log"`,带有 `sessionId` 和在 `content` 中 JSON 字符串化的事件值;序列化不保留 JSONL 键顺序。脱敏先于序列化,复制的 envelope 不包含 payload。
|
||||
|
||||
Session 日志使用独立 provider 和字节/条数队列。每条记录使用 SDK 序列化器计量一次,包含完整 envelope;其总和保守约束合并请求大小。顺序组包避免候选批次的完整序列化和递归重复编码。单条超限记录只产生一次诊断,不阻塞后续事件。每个 HTTP 请求在结束前始终占有传输槽位,即使其监测期限已经触发;关闭期限到期后停止剩余队列。scope 名称/版本和显式传输选项仍由后端拥有。
|
||||
|
||||
[遥测恢复决策](../feature/2026-07-23-session-telemetry-otel-revival.zh.md) 继续约束脱敏、捕获和尽力交接。本决策替代其仅使用 SDK 队列/聚合的规则:SDK 批次期限无法拥有拆成多个 HTTP 请求的操作。共享 Session 日志通道调度完整请求,SDK 仍负责传输和重试。反馈授权和独立的 DeepSeek 模型请求附加路径不变。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**整个 Session 合为一个字符串。** 拒绝,因为即使每条事件都能放入请求,大型 Session 仍会超过请求上限。每条事件对应一条记录,既保留事件身份又允许分批。
|
||||
|
||||
**截断超限记录或仅在客户端分片。** 拒绝,因为截断会丢失内容,而分片需要采集端重组协议。显式拒绝保留一条事件对应一条记录的含义。
|
||||
|
||||
**计算压缩字节数或字符数。** 拒绝,因为采集端限制可能作用于解压后内容,而字符数不包含 UTF-8 和转义开销。计量完整未压缩请求提供保守上限。
|
||||
|
||||
## 影响
|
||||
|
||||
产品事件和 Session 记录不共享请求或队列,两个包也不相互导入。保守的逐记录 envelope 计量可能比精确合并计量产生更小批次。队列溢出、超限拒绝、退出和传输失败可能丢失数据;不新增持久 outbox 或确认机制。测试覆盖字节上限、线性计量、慢请求串行调度、元数据、脱敏复制隔离和反馈授权回放。
|
||||
+19
@@ -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 两端都跑。
|
||||
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-09-19-failed-step-tool-results.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-19-failed-step-tool-results.md
|
||||
/agent-note-record-unanswered-tool-results-before-closing-a-failed-step:
|
||||
en: 8fd2a50a041b1b18
|
||||
zh: 01aed07a6425a322
|
||||
/agent-note-record-unanswered-tool-results-before-closing-a-failed-step/problem:
|
||||
en: 1fa7525f83af9035
|
||||
zh: 401ce968e3dd5434
|
||||
/agent-note-record-unanswered-tool-results-before-closing-a-failed-step/decision:
|
||||
en: 3d58367a05183b4a
|
||||
zh: f9ccc3a7011a6240
|
||||
/agent-note-record-unanswered-tool-results-before-closing-a-failed-step/alternatives-considered:
|
||||
en: 37da4d9c8533b736
|
||||
zh: e52a59763ae2b62e
|
||||
/agent-note-record-unanswered-tool-results-before-closing-a-failed-step/consequences:
|
||||
en: 1ea72421a98551d8
|
||||
zh: c01e02ddac8a3454
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: Record unanswered tool results before closing a failed step
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-19-failed-step-tool-results.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A committed assistant message can request tools whose scheduler fails before all results are recorded. Closing the step and turn with those calls unanswered makes later model requests invalid for providers that require paired tool calls and results. An error turn is already closed, so crash-tail repair cannot recover it on resume.
|
||||
|
||||
## Decision
|
||||
|
||||
The AgentLoop driver records recovery results for unanswered calls before it closes a failed step, then propagates the original failure. Live step recovery and `interruptedTurnClosers` share incremental pending-call tracking and result construction; the live driver still owns its step and turn events. A step-local observer retains pending identities from committed events without copying session history. The scheduler stops replenishment and drains started dispatches before rejecting, so result recovery cannot race a live dispatch.
|
||||
|
||||
Recovery preserves committed results and follows the assistant's call order. A recorded `tool/call` without a result receives `TOOL_OUTCOME_UNKNOWN`; its text warns against blindly retrying an operation with possible side effects. An assistant request with no call record receives `TOOL_NOT_STARTED`. A call record precedes preparation and does not establish that the tool body ran, so unknown outcomes remain conservative.
|
||||
|
||||
This decision supersedes only the missing-result policy in [parallel tool-call execution](../feature/2026-07-10-parallel-tool-call-execution.md). Its classification, concurrency limit, ordering, and quiescence rules remain applicable.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Leave calls unanswered to avoid inventing tool outcomes.** An unanswered call invalidates later provider requests. An explicit unknown-outcome error records the limit of the available evidence without claiming success, cancellation, or absence of side effects.
|
||||
|
||||
**Repair only when resuming or forking.** The ordinary failure path already records `step/end` and `turn/end`; open-tail repair intentionally preserves closed history. Recovery belongs before those records, while the driver owns the failed step.
|
||||
|
||||
**Repair inside the tool scheduler alone.** Failure can occur after the assistant message is recorded but before scheduling starts. The step driver covers that interval and reuses the existing session repair semantics rather than adding a second implementation of call/result matching.
|
||||
|
||||
**Retry automatically.** A dispatch can have side effects even when its result is missing. The error result supplies retry guidance, while the turn retains the original failure and waits for later work.
|
||||
|
||||
## Consequences
|
||||
|
||||
Later requests, resume, and fork retain a usable call/result history after a live step failure. Recovery appends results; it does not remove assistant messages, replace completed results, execute tools, or convert the failed turn into success. The log uses existing event types and recovery codes, with no format change.
|
||||
|
||||
Already-closed inconsistent historical turns remain unchanged. The live recovery path requires the session to accept the missing result records; it cannot promise recovery from a failed log append.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: 在关闭失败步骤前补齐工具结果
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-19-failed-step-tool-results.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
已提交的 assistant 消息可能请求多个工具,而调度器在全部结果记录完成之前失败。如果关闭步骤和轮次时仍有调用尚无结果,后续模型请求就不满足要求工具调用与结果配对的提供方。错误轮次已经关闭,因此恢复会话时的崩溃尾部修复无法恢复它。
|
||||
|
||||
## 决策
|
||||
|
||||
AgentLoop 驱动器在关闭失败步骤之前,为尚无结果的调用记录恢复结果,随后传播原始失败。实时步骤恢复和 `interruptedTurnClosers` 共享增量的待处理调用跟踪与结果构造;实时驱动器仍拥有自己的步骤和轮次事件。步骤本地观察器从已提交事件中保留待处理调用标识,不复制会话历史。调度器在拒绝之前停止补充池并排空已启动的派发,因此结果恢复不会与运行中的派发竞争。
|
||||
|
||||
恢复保留已提交的结果,并遵循 assistant 的调用顺序。已有 `tool/call` 记录但尚无结果的调用获得 `TOOL_OUTCOME_UNKNOWN`;其文本警告不要盲目重试可能存在副作用的操作。没有调用记录的 assistant 请求获得 `TOOL_NOT_STARTED`。调用记录先于准备阶段,不能证明工具主体已经运行,因此未知结果保持保守。
|
||||
|
||||
本决策仅取代[并行工具调用执行](../feature/2026-07-10-parallel-tool-call-execution.zh.md)中的缺失结果策略。其中的分类、并发上限、顺序和完全停稳规则继续适用。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**让调用保持无结果,避免虚构工具执行结果。** 尚无结果的调用会使后续提供方请求无效。显式的未知结果错误记录可用证据的局限,而不声称成功、取消或没有副作用。
|
||||
|
||||
**仅在恢复或 fork 时修复。** 普通失败路径已经记录 `step/end` 和 `turn/end`;开放尾部修复有意保留已关闭历史。恢复应在这些记录之前执行,此时驱动器仍拥有失败步骤。
|
||||
|
||||
**仅在工具调度器内部修复。** 失败可能发生在 assistant 消息记录之后、调度开始之前。步骤驱动器覆盖这段区间,并复用已有的会话修复语义,避免另行实现调用/结果匹配。
|
||||
|
||||
**自动重试。** 即使结果缺失,派发也可能产生副作用。错误结果提供重试指引,而轮次保留原始失败并等待后续工作。
|
||||
|
||||
## 影响
|
||||
|
||||
实时步骤失败后,后续请求、恢复和 fork 会保留可用的调用/结果历史。恢复追加结果;它不移除 assistant 消息、不替换已完成结果、不执行工具,也不将失败轮次改为成功。日志使用已有事件类型和恢复代码,无需格式变更。
|
||||
|
||||
此前已关闭的不一致历史轮次保持不变。实时恢复路径要求会话能够接纳缺失结果记录;它不能保证从日志追加失败中恢复。
|
||||
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-09-24-exited-holder-lock-takeover.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-24-exited-holder-lock-takeover.md
|
||||
/agent-note-taking-over-a-writer-lock-whose-holder-exited:
|
||||
en: ecf6fb35c9eed3ca
|
||||
zh: 59c29b64e23608e4
|
||||
/agent-note-taking-over-a-writer-lock-whose-holder-exited/problem:
|
||||
en: cf143bc56530fe17
|
||||
zh: a4e3ee8fa3b8c465
|
||||
/agent-note-taking-over-a-writer-lock-whose-holder-exited/decision:
|
||||
en: af22bae72ff97ef8
|
||||
zh: 428c09871bba95d1
|
||||
/agent-note-taking-over-a-writer-lock-whose-holder-exited/alternatives-considered:
|
||||
en: ee800ed269cfc4a2
|
||||
zh: df983b552ef25273
|
||||
/agent-note-taking-over-a-writer-lock-whose-holder-exited/testing:
|
||||
en: 7eb7c5818049415a
|
||||
zh: f84c39c55bb3bc5a
|
||||
/agent-note-taking-over-a-writer-lock-whose-holder-exited/consequences:
|
||||
en: 03f4cda9290188e5
|
||||
zh: 07f800776c061218
|
||||
@@ -0,0 +1,51 @@
|
||||
# Agent Note: Taking over a writer lock whose holder exited
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-24-exited-holder-lock-takeover.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`withFileLock` in [dsh-atomic-write](../../../../packages/util/atomic-write/README.md) creates `<file>.lock` with exclusive create and removes it in a `finally`. A process that ends without running that `finally` leaves the lock behind, and every later writer of the file times out until someone deletes it by hand. The [original decision](../../archived/architecture/2026-07-30-settings-write-path-integrity.md) accepted this because file age cannot distinguish a crashed holder from a paused one.
|
||||
|
||||
The profile package lock (`<profile>/package.json.lock`) made the cost visible. `dsh plugin` installs no signal handler, so Ctrl-C, SIGTERM, or closing its terminal ends the process while pnpm runs, and the lock stays. `dsh` and `dsh web` exit right after application disposal resolves, while a cancelled installation is still restoring files and has not released the lock. A crash, SIGKILL, or out-of-memory kill has the same effect. After any of these, every plugin operation on that profile fails after `lockWaitMs` (two minutes by default) without saying that the holder is gone.
|
||||
|
||||
## Decision
|
||||
|
||||
- The lock keeps the `<pid>\n` record earlier releases wrote, so locks left before this change are taken over and processes of both releases interoperate.
|
||||
- A contender that finds a lock reads its record. When a signal probe of the PID fails with `ESRCH`, the holder is proven gone and the lock is taken over. `EPERM` means the process exists under another user and the lock is kept. A record naming the contender's own process is kept too: that process is running, and the browser Worker's process shim reports its own PID as absent.
|
||||
- Contenders that read the same record serialize on a claim file, `<file>.lock.takeover-<first 16 hex digits of the record's SHA-256>`, created with `wx`. The claimant re-reads the lock and probes its PID again, removes it only if it still holds that record and the PID is still absent, removes the claim, and retries acquisition at once. The record's holder cannot release it once its PID is absent, another takeover needs the same claim, and a new holder that reused the PID fails the second probe, so a claimant never removes a lock that another contender acquired after the exited holder's.
|
||||
- A record that is empty, incomplete, not a decimal PID, or names PID 0 or a PID beyond the int32 range the probe accepts is waited for. None of these proves that the holder stopped.
|
||||
- Takeover proves only that the recorded process exited, not that the processes it started stopped writing. The [Plugin Manager](../../../../packages/boot/plugin-manager/README.md) records each pnpm run in `.plugin-manager/run.json` while the run lasts; an operation that finds that record waits up to five seconds for the recorded run to stop and otherwise refuses to run, naming the process and the record.
|
||||
- The probe runs on the contender's host. Writers on several hosts sharing one `DSH_HOME` over a network filesystem are unsupported; the `flock` that guards Session files does not reliably exclude them either.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Remove a lock older than a fixed age.** Age cannot separate a crashed holder from one running a ten-minute pnpm installation, and the plugin manager legitimately holds the lock that long.
|
||||
|
||||
**Kernel-released locks.** The kernel releases them when the holder dies, which removes the problem instead of detecting it. The [Session write lease](../../../../packages/session/session-persistence-jsonl/src/lease.ts) already holds one on both platforms: `flock` through `@deepseek-ai/node-addon-system` on POSIX and a named semaphore through `koffi` on Windows. Moving `withFileLock` onto it would give the zero-dependency `dsh-atomic-write` both dependencies, and processes of earlier releases still exclude each other only through `<file>.lock`, so a transition would have to hold both locks. This stays the stronger fix if PID reuse or shared-filesystem deployments turn out to matter.
|
||||
|
||||
**Rename the stale lock aside and restore it when the renamed record differs.** Restoring can race a third contender that acquires the empty path in between, which leaves two holders. The claim file prevents that race without restoring anything.
|
||||
|
||||
**Record the hostname and a nonce.** A hostname would keep a writer on another host from probing a PID that is not its own, but no supported deployment shares these files across hosts, and macOS changes its hostname when networks change, which would leave locks from before the change in place. A nonce would make each record unique, but the second probe under the claim already rejects a holder that reused the PID.
|
||||
|
||||
**Terminate the recorded pnpm run instead of refusing.** Its PID can have been reused by an unrelated process, which a signal would then kill. Refusing leaves that case to the operator, whom the diagnostic names the process and the record.
|
||||
|
||||
**Signal handlers in `dsh plugin` only.** This covers one of the paths that leave a lock and leaves crashes, forced exits, and the other lock files behind.
|
||||
|
||||
## Testing
|
||||
|
||||
| Evidence | Behaviour |
|
||||
|---|---|
|
||||
| [atomic-write.spec.ts](../../../../packages/util/atomic-write/tests/atomic-write.spec.ts) | Takeover of an exited holder's record, through an injected probe everywhere and a really exited process on POSIX; eight contenders over one exited holder never overlap; live, own-process, other-user, empty, incomplete, non-PID, process-group, and out-of-range records are waited for; a holder that reused the PID after the claim, an unreadable lock, a claim held by another contender, and a record replaced after the claim are left in place; a lock that cannot be removed is waited for; an `EPERM` claim refusal is retried, another claim failure surfaces, and a claim that cannot be removed does not fail the operation. |
|
||||
| [operations.spec.ts](../../../../packages/boot/plugin-manager/tests/operations.spec.ts) | CLI, service, and repair runs are recorded while they run and the record is removed afterwards; an active recorded run refuses the operation before pnpm starts; a stopped one's record is removed; a record that names no run refuses the operation. |
|
||||
| [operations-process.spec.ts](../../../../packages/boot/plugin-manager/tests/operations-process.spec.ts) | A real recorded run that is still writing finishes before the next operation's pnpm starts. |
|
||||
|
||||
## Consequences
|
||||
|
||||
- Locks left by an exited process, including those written before this change, no longer need an operator; the next writer proceeds without waiting for its deadline.
|
||||
- A PID that a live process reused after a reboot keeps the lock in place until an operator removes it, as before.
|
||||
- Processes that share the lock file but not a PID namespace, such as containers over one volume or hosts over a network filesystem, can see a live holder as exited. Such deployments are not supported.
|
||||
- A contender that crashes while it holds a claim leaves that claim, and the named lock is then not taken over automatically.
|
||||
- A pnpm run left by an exited operation blocks later operations until it stops or an operator removes its record.
|
||||
- A holder that is still alive keeps its lock, so this does not bound how long a live operation holds it.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Agent Note: 接管持有者已退出的写锁
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-24-exited-holder-lock-takeover.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[dsh-atomic-write](../../../../packages/util/atomic-write/README.zh.md) 的 `withFileLock` 以独占创建方式创建 `<file>.lock`,并在 `finally` 中移除它。进程若未执行该 `finally` 就结束,锁会留在原地,该文件后续的每个写入方都会超时,直到有人手动删除它。[原始决策](../../archived/architecture/2026-07-30-settings-write-path-integrity.md)接受了这一点,因为文件存续时间无法区分已崩溃的持有者与暂停的持有者。
|
||||
|
||||
profile 包锁(`<profile>/package.json.lock`)让这个代价显现出来。`dsh plugin` 不安装信号处理器,因此在 pnpm 运行期间按 Ctrl-C、发送 SIGTERM 或关闭其终端都会结束进程,锁随之留下。`dsh` 与 `dsh web` 在应用销毁完成后立即退出,而此时被取消的安装仍在恢复文件,尚未释放锁。崩溃、SIGKILL 或内存不足被杀也有同样效果。发生上述任一情况后,该 profile 上的每个插件操作都会在 `lockWaitMs`(默认两分钟)后失败,且不说明持有者已不存在。
|
||||
|
||||
## 决策
|
||||
|
||||
- 锁沿用早期版本写入的 `<pid>\n` 记录,因此本变更之前遗留的锁也会被接管,新旧版本的进程可以互操作。
|
||||
- 竞争者发现锁时读取其记录。若对该 PID 的信号探测以 `ESRCH` 失败,则证明持有者已不存在,锁会被接管。`EPERM` 表示该进程以其他用户身份存在,锁保留。指向竞争者自身进程的记录同样保留:该进程正在运行,而浏览器 Worker 的进程模拟层会把自身 PID 报告为不存在。
|
||||
- 读到同一条记录的竞争者通过一个以 `wx` 创建的认领文件 `<file>.lock.takeover-<记录 SHA-256 的前 16 位十六进制>` 串行化。认领者重新读取锁并再次探测其 PID,仅当锁仍是该记录且该 PID 仍不存在时才移除它,随后移除认领文件并立即重试获取。PID 不存在时记录的持有者无法再释放它,另一次接管需要同一个认领文件,复用了该 PID 的新持有者无法通过第二次探测,因此认领者绝不会移除另一个竞争者在已退出持有者之后获得的锁。
|
||||
- 记录为空、不完整、不是十进制 PID,或指向 PID 0 或超出探测所接受的 int32 范围的 PID 时,竞争者继续等待。这些情况都无法证明持有者已经停止。
|
||||
- 接管只能证明记录中的进程已退出,不能证明它启动的进程已停止写入。[Plugin Manager](../../../../packages/boot/plugin-manager/README.zh.md) 在每次 pnpm 运行期间把它记录在 `.plugin-manager/run.json` 中;发现该记录的操作最多等待五秒让记录中的运行停止,否则拒绝运行,并指明该进程与记录文件。
|
||||
- 探测在竞争者所在主机上进行。不支持多台主机通过网络文件系统共享同一个 `DSH_HOME` 写入;保护 Session 文件的 `flock` 同样无法可靠地排除它们。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**移除超过固定时长的锁。** 时长无法区分已崩溃的持有者与正在运行十分钟 pnpm 安装的持有者,而插件管理器会合理地持锁这么久。
|
||||
|
||||
**由内核释放的锁。** 持有者死亡时内核会释放它们,从根源上消除问题而不是检测它。[Session 写租约](../../../../packages/session/session-persistence-jsonl/src/lease.ts)已在两个平台上持有这种锁:POSIX 上通过 `@deepseek-ai/node-addon-system` 使用 `flock`,Windows 上通过 `koffi` 使用命名信号量。把 `withFileLock` 迁移到它上面,会让零依赖的 `dsh-atomic-write` 引入这两个依赖;而早期版本的进程之间只通过 `<file>.lock` 互斥,因此过渡期必须同时持有两种锁。若 PID 复用或共享文件系统部署变得重要,这仍是更强的修复。
|
||||
|
||||
**把陈旧锁重命名到别处,若重命名后的记录不同再恢复。** 恢复可能与在此间隙获取空路径的第三个竞争者竞态,导致出现两个持有者。认领文件无需任何恢复即可防止这一竞态。
|
||||
|
||||
**记录主机名和随机数。** 主机名能让其他主机上的写入方不去探测不属于自己的 PID,但没有受支持的部署会跨主机共享这些文件,而且 macOS 切换网络时会改变主机名,导致变更前留下的锁无法接管。随机数能让每条记录唯一,但认领后的第二次探测已经能排除复用了该 PID 的持有者。
|
||||
|
||||
**终止记录中的 pnpm 运行而不是拒绝。** 其 PID 可能已被无关进程复用,发送信号会杀死该进程。拒绝则把这种情况交给操作者,诊断会指明该进程与记录文件。
|
||||
|
||||
**只在 `dsh plugin` 中安装信号处理器。** 这只覆盖留下锁的其中一条路径,崩溃、强制退出和其他锁文件仍会遗留。
|
||||
|
||||
## 验证
|
||||
|
||||
| 证据 | 行为 |
|
||||
|---|---|
|
||||
| [atomic-write.spec.ts](../../../../packages/util/atomic-write/tests/atomic-write.spec.ts) | 接管已退出持有者的记录,各平台通过注入的探测验证,POSIX 上另用真实已退出的进程验证;八个竞争者同时接管一个已退出持有者时从不重叠;存活、自身进程、其他用户、空、不完整、非 PID、进程组和超出范围的记录均继续等待;认领后复用了该 PID 的持有者、无法读取的锁、被另一竞争者持有的认领、认领后被替换的记录都保留原样;无法移除的锁会继续等待;以 `EPERM` 拒绝的认领会重试,其他认领失败会抛出,认领无法移除时操作不失败。 |
|
||||
| [operations.spec.ts](../../../../packages/boot/plugin-manager/tests/operations.spec.ts) | CLI、service 与修复运行在运行期间被记录,结束后删除记录;仍活动的记录中运行会在 pnpm 启动前拒绝操作;已停止运行的记录被删除;不指向任何运行的记录会拒绝操作。 |
|
||||
| [operations-process.spec.ts](../../../../packages/boot/plugin-manager/tests/operations-process.spec.ts) | 仍在写入的真实记录中运行会在下一个操作的 pnpm 启动前结束。 |
|
||||
|
||||
## 后果
|
||||
|
||||
- 已退出进程留下的锁(包括本变更之前留下的)不再需要操作者处理;下一个写入方无需等到期限即可继续。
|
||||
- 重启后被存活进程复用的 PID 会让锁保持原样,直到操作者移除,与之前相同。
|
||||
- 共享锁文件但不共享 PID 命名空间的进程,例如共享同一卷的容器或通过网络文件系统共享的多台主机,可能把存活持有者视为已退出。不支持此类部署。
|
||||
- 竞争者在持有认领文件时崩溃会留下该认领文件,对应的锁随后不会被自动接管。
|
||||
- 已退出操作留下的 pnpm 运行会阻止后续操作,直到它停止或操作者删除其记录。
|
||||
- 仍存活的持有者会保留其锁,因此本变更不限制存活操作的持锁时长。
|
||||
@@ -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)负责该上下文的理由与边界。
|
||||
|
||||
#### 升级机制:拒绝后一次经批准的更宽重试
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
en: 532bb7268d9b93ce
|
||||
zh: ad47162233222ab0
|
||||
/agent-note-parallel-tool-call-execution-by-per-call-safety/scheduling-and-ordering:
|
||||
en: f0906d03fef98e7b
|
||||
zh: f9e43c4c832c9a05
|
||||
en: a485c530ea4366de
|
||||
zh: 6f9915d726de9ac7
|
||||
/agent-note-parallel-tool-call-execution-by-per-call-safety/safety-contract:
|
||||
en: 16c4ee08b65ead23
|
||||
zh: df80d6e49982615b
|
||||
@@ -24,8 +24,8 @@
|
||||
en: eb5a6aa84980f0ab
|
||||
zh: eed2680a160e8ed2
|
||||
/agent-note-parallel-tool-call-execution-by-per-call-safety/alternatives-considered:
|
||||
en: 44f8fb5851ba4e39
|
||||
zh: 4d75c937ac5277f7
|
||||
en: 17becc038bb7df7c
|
||||
zh: 673d9a9bdfa39edf
|
||||
/agent-note-parallel-tool-call-execution-by-per-call-safety/consequences:
|
||||
en: 5e5b2a1c7d850f83
|
||||
zh: b63311f28eb69366
|
||||
en: a5508057bc03f4f6
|
||||
zh: 96b46c59402474c3
|
||||
@@ -46,7 +46,7 @@ Only dispatch and the tool body overlap. `tools/pre-execute` and `tools/post-exe
|
||||
|
||||
Each started call appends `tool/call` immediately before its pre-execute gate. Completed dispatches occupy model-order slots, and a commit cursor appends `tool/result` and collects `additionalContexts` only when the next slot is ready. Live surfaces may show several pending calls, but results and post-tool context remain model-ordered.
|
||||
|
||||
An abort before a group starts records no calls from that group. An abort during a group stops replenishment, waits for already-started calls, commits their results in order, drains accepted batch context after those results, and then ends the step through the existing abort path. Calls that never start have no audit event. An unexpected scheduler failure stops new dispatches, waits for every already-started dispatch to settle, and rethrows the first failure. Because that failure is terminal internal state rather than a tool outcome, the loop does not invent tool results for rejected or uncommitted calls.
|
||||
An abort stops replenishment, waits for already-started calls, commits their results in order, drains accepted batch context after those results, and records synthetic aborted call/result pairs for undispatched calls. An unexpected scheduler failure stops new dispatches, waits for every already-started dispatch to settle, and rethrows the first failure. The driver records missing results before closing the failed step under the [failed-step recovery decision](../bug-fix/2026-09-19-failed-step-tool-results.md).
|
||||
|
||||
PTC mode remains outside this scheduler because the model emits one native `run_code` call. `run_code` and its internal dispatch queue remain serial; native sibling calls in `mode: 'both'` use the normal scheduler.
|
||||
|
||||
@@ -84,7 +84,7 @@ Snapshot coverage pins the visible multi-call transcript: pending calls may over
|
||||
|
||||
**Expose staged methods or a scheduling waterfall.** Public `prepare` / `dispatch` / `finalize` methods or a `tools/execution-mode` event add extension surface before another consumer needs it. The loop uses an internal scheduler view, while `executionMode(exec)` leaves an insertion point for a policy hook.
|
||||
|
||||
**Convert scheduler failures into tool results.** AgentLoop cannot determine whether a rejected dispatch invoked the tool body; ToolRuntime owns body-invocation state and typed tool outcomes. Internal scheduler failures therefore remain terminal instead of being reclassified as `ABORTED` results.
|
||||
**Convert scheduler failures into ordinary tool results.** AgentLoop cannot determine whether a rejected dispatch invoked the tool body; ToolRuntime owns body-invocation state and typed tool outcomes. Internal scheduler failures remain terminal instead of being reclassified as `ABORTED` results. Explicit recovery results preserve this uncertainty without leaving unanswered calls.
|
||||
|
||||
**Start calls while the model streams.** This may reduce latency further but changes assistant-message authority, replay, and call/result pairing. The scheduler starts only after the assistant message is complete.
|
||||
|
||||
@@ -96,7 +96,7 @@ Snapshot coverage pins the visible multi-call transcript: pending calls may over
|
||||
|
||||
The design is fail-closed and simple for tool authors, but it cannot exploit concurrency whose safety depends on comparing siblings. A tool that opts in too broadly can expose latent shared-state races.
|
||||
|
||||
Parallel calls may begin in cases where serial execution would have aborted before reaching them. The scheduler therefore records only started calls, drains them on abort, and never starts replacements after cancellation.
|
||||
Parallel calls may begin in cases where serial execution would have aborted before reaching them. The scheduler drains them on abort and never starts replacements after cancellation; undispatched calls receive synthetic aborted call/result pairs.
|
||||
|
||||
Ordered commits may hold a fast result behind a slow earlier sibling. This preserves replay and model-history order while live surfaces still show pending progress.
|
||||
|
||||
@@ -104,4 +104,4 @@ Concurrent external calls can compete for quota or process capacity. Providers o
|
||||
|
||||
Tool registration is a scheduling boundary. Registry mutations affect not-yet-started calls because the scheduler reclassifies after each barrier and before every pool replenishment. Already-started calls retain the scheduling decision under which they entered the pool.
|
||||
|
||||
A terminal scheduler failure may leave recorded calls without results before the failed step closes. Waiting for live dispatches preserves quiescence without misreporting those internal failures as tool outcomes.
|
||||
A terminal scheduler failure reaches step recovery only after live dispatches settle. Recovery reports uncommitted outcomes as unknown, preserving quiescence and the original failure while keeping later model history usable.
|
||||
@@ -46,7 +46,7 @@ Status: implemented
|
||||
|
||||
每个已启动的调用都会在进入 pre-execute 门禁之前立即追加 `tool/call`。已完成的派发占据模型顺序的槽位;提交游标只有在下一个槽位就绪时,才会追加 `tool/result` 并收集 `additionalContexts`。实时界面可以显示多个待处理调用,但结果和工具执行后的上下文仍按模型顺序排列。
|
||||
|
||||
如果在一组启动前中止,系统不会记录该组的任何调用。如果在一组执行期间中止,系统会停止补充池,等待已启动的调用,按顺序提交其结果,在这些结果之后排空已接受的批次上下文,然后通过现有中止路径结束该步骤。从未启动的调用没有审计事件。调度器发生意外故障时,会停止新的派发,等待每项已启动的派发结算,并重新抛出第一个故障。由于该故障是内部终态,而非工具结果,循环不会为被拒绝或未提交的调用虚构工具结果。
|
||||
中止会停止补充池,等待已启动的调用,按顺序提交其结果,在这些结果之后排空已接受的批次上下文,并为未派发的调用记录合成的中止调用/结果对。调度器发生意外故障时,会停止新的派发,等待每项已启动的派发结算,并重新抛出第一个故障。驱动器按照[失败步骤恢复决策](../bug-fix/2026-09-19-failed-step-tool-results.zh.md),在关闭失败步骤之前记录缺失结果。
|
||||
|
||||
PTC mode 仍不使用此调度器,因为模型只会发出一个原生 `run_code` 调用。`run_code` 及其内部派发队列仍按串行方式执行;`mode: 'both'` 中的原生并列调用使用常规调度器。
|
||||
|
||||
@@ -84,7 +84,7 @@ PTC mode 仍不使用此调度器,因为模型只会发出一个原生 `run_co
|
||||
|
||||
**公开分阶段方法或调度 waterfall(瀑布式事件)。** 公开的 `prepare` / `dispatch` / `finalize` 方法或 `tools/execution-mode` 事件,会在另一个消费方需要它之前扩大扩展接口。循环使用内部调度器视图,而 `executionMode(exec)` 为策略钩子保留了插入点。
|
||||
|
||||
**将调度器故障转换为工具结果。** AgentLoop 无法判断被拒绝的派发是否已调用工具主体;ToolRuntime 负责工具主体调用状态和类型化工具结果。因此,内部调度器故障保持为终态,而不会被重新分类为 `ABORTED` 结果。
|
||||
**将调度器故障转换为普通工具结果。** AgentLoop 无法判断被拒绝的派发是否已调用工具主体;ToolRuntime 负责工具主体调用状态和类型化工具结果。内部调度器故障保持为终态,而不会被重新分类为 `ABORTED` 结果。显式恢复结果保留这种不确定性,同时避免留下尚无结果的调用。
|
||||
|
||||
**在模型流式输出时启动调用。** 这可能进一步降低延迟,但会改变 assistant 消息的权威性、回放以及调用/结果配对。调度器只在 assistant 消息完成后才启动。
|
||||
|
||||
@@ -96,7 +96,7 @@ PTC mode 仍不使用此调度器,因为模型只会发出一个原生 `run_co
|
||||
|
||||
该设计遵循安全侧原则,对工具作者而言也很简单,但无法利用必须通过比较并列调用才能确认安全的并发性。工具过于宽泛地选择并行,可能暴露潜在的共享状态竞态。
|
||||
|
||||
在某些情形下,并行调用会先行启动,而串行执行原本会在轮到这些调用之前中止。因此,调度器只记录已启动的调用,在中止时将其排空,且取消后绝不启动替换调用。
|
||||
在某些情形下,并行调用会先行启动,而串行执行原本会在轮到这些调用之前中止。调度器在中止时将其排空,且取消后绝不启动替换调用;未派发的调用获得合成的中止调用/结果对。
|
||||
|
||||
有序提交可能会让快速结果等待较慢的早期并列调用。这保留了回放和模型历史顺序,同时实时界面仍可显示待处理进度。
|
||||
|
||||
@@ -104,4 +104,4 @@ PTC mode 仍不使用此调度器,因为模型只会发出一个原生 `run_co
|
||||
|
||||
工具注册是调度边界。调度器会在每个屏障之后以及每次补充池之前重新分类,因此注册表变更会影响尚未启动的调用。已启动的调用保留它们进入池时所依据的调度决策。
|
||||
|
||||
终态调度器故障可能会在故障步骤关闭前留下已记录但没有结果的调用。等待仍在运行的派发可确保完全停稳,而不会将这些内部故障误报为工具结果。
|
||||
终态调度器故障只有在运行中的派发结算后才会进入步骤恢复。恢复将未提交的执行结果报告为未知,保留完全停稳与原始失败,同时让后续模型历史保持可用。
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: d3383b495424d118
|
||||
zh: 4e6ffc8ebfa7f741
|
||||
/agent-note-session-telemetry-seam-with-mandatory-redaction-and-the-otel-backend/decision:
|
||||
en: 85b950a67315023c
|
||||
zh: cb4c08338c30b226
|
||||
en: 739b871ae583dcbf
|
||||
zh: d17053e3b0a35942
|
||||
/agent-note-session-telemetry-seam-with-mandatory-redaction-and-the-otel-backend/alternatives-considered:
|
||||
en: 01838eefca2bc117
|
||||
zh: fc71f5449d095b27
|
||||
|
||||
@@ -16,6 +16,8 @@ Every deployment that wants harness sessions in an observability stack must hand
|
||||
- **The `session-telemetry/record` waterfall** — the delta over the branch version and the seam's redaction extension point. Every record passes it before reaching any backend; the seam ships NO rules of its own — the innermost `next()` is a pass-through, deployments mount their rules as listeners (stacking by transforming `next()`'s return value), and a throwing rule withholds the record fail-closed. Redaction applies to the exported copy only; the canonical log is never rewritten.
|
||||
- **`@deepseek-ai/dsh-session-telemetry-otel`** — the reference backend: OTel JS SDK log pipeline (`LoggerProvider` → `BatchLogRecordProcessor` → OTLP/HTTP exporter), configured verbatim through `exporter`/`processor` passthroughs. `DISABLED` is the plugin default and constructs no transport. `FEEDBACK_ONLY` requires `exporter.url`; the [explicit-feedback policy](../architecture/2026-09-05-nonofficial-feedback-otel.md) requires a new submission for every bounded capture, for all providers. [Buffer-free feedback replay](../../archived/simplification/2026-08-06-buffer-free-feedback-telemetry.md) avoids a second in-memory copy of the session prefix.
|
||||
|
||||
The [Session-log request limits](../architecture/2026-09-25-session-log-otel-byte-limits.md) supersede the SDK-only batching rule below for the OTel Session sender; capture, redaction, and handoff rationale remain applicable.
|
||||
|
||||
The boundary axiom holds: the harness's aspect ends at `emit()`. Batching, retry, queueing, and loss policy are the reporting SDK's, configured through passthroughs. Delivery is best-effort: a crash can lose queued records. Backend retry or losing the same live object's module-scope cursor can duplicate lifecycle-local rows, so receivers deduplicate on `(session.id, session.format_version, event.seq)`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -16,6 +16,8 @@ Status: implemented
|
||||
- **`session-telemetry/record` waterfall(瀑布式事件)** —— 相对分支版本的增量,也是该 seam 的脱敏扩展点。每条记录抵达任何后端前必经此处;seam 自身不带任何规则——最内层 `next()` 原样透传,部署方以监听器挂载自己的规则(通过变换 `next()` 的返回值堆叠),抛异常的规则将该记录 fail-closed 扣下。脱敏只作用于导出副本;canonical log 永不改写。
|
||||
- **`@deepseek-ai/dsh-session-telemetry-otel`** —— 参考后端:OTel JS SDK 日志流水线(`LoggerProvider` → `BatchLogRecordProcessor` → OTLP/HTTP exporter),经 `exporter`/`processor` passthrough 原样配置。`DISABLED` 是插件默认值,且不构造任何传输。`FEEDBACK_ONLY` 要求 `exporter.url`;[显式反馈策略](../architecture/2026-09-05-nonofficial-feedback-otel.zh.md)要求每次有界捕获都由新提交触发,适用于所有提供方。[无缓冲反馈回放](../../archived/simplification/2026-08-06-buffer-free-feedback-telemetry.md)避免在内存中创建会话前缀的第二份副本。
|
||||
|
||||
[Session 日志请求上限](../architecture/2026-09-25-session-log-otel-byte-limits.zh.md) 在 OTel Session 发送器中替代下述仅由 SDK 分批的规则;采集、脱敏和交接的理由仍然适用。
|
||||
|
||||
边界公理保持不变:harness 的职责止于 `emit()`。批处理、重试、排队与丢失策略属于 reporting SDK,并经 passthrough 配置。投递是尽力而为:崩溃可能丢失已排队记录。后端重试或丢失同一 live 对象的模块作用域游标可能重复生命周期本地 row,因此接收端基于 `(session.id, session.format_version, event.seq)` 去重。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 476b4a1b9ada2d8a
|
||||
zh: 15b8051d22bb1dd0
|
||||
/agent-note-in-process-subagent-policy-inheritance-the-child-starts-under-the-parent-s-sandbox-override/decision:
|
||||
en: dc11e7e52cdd4704
|
||||
zh: e000adedb13f2a98
|
||||
en: 1f99951bc562cce4
|
||||
zh: d68ee315b7aea792
|
||||
/agent-note-in-process-subagent-policy-inheritance-the-child-starts-under-the-parent-s-sandbox-override/decision/what-a-blocked-child-experiences:
|
||||
en: 3b6ff196a06c2420
|
||||
zh: c04cb8bd81eb1c4b
|
||||
|
||||
@@ -10,7 +10,7 @@ The Auto or Full access preset identity plus sandbox and approval overrides are
|
||||
|
||||
## Decision
|
||||
|
||||
The delegation boundary calls the shared child-agent helpers (`captureDelegatedPolicyOverrides`/`appendDelegatedPolicyOverrides` in `dsh-subagent`) before its first await; the one-shot driver and the [continuable start](../../../../packages/subagent/subagent/README.md) both use them. The capture copies the current `permission/preset` identity when `permissionPresets.current(parent.session)` is `auto` or `danger-full-access`, snapshots `sandboxPolicy.overrideOf(parent.session)`, and pins the child approval policy to `'never'`. A later parent switch belongs to the parent's future; cancel-and-redelegate takes a new snapshot. The permission-preset and sandbox-policy services are optional: only the Auto/Full-access shared-bundle identity and the explicit sandbox session override are copied, never deployment defaults or one-shot grants. The approval policy is not inherited — the [approvals-pinned decision](2026-08-10-subagent-approval-pinned-never.md) supersedes this note's original approval-override inheritance.
|
||||
The delegation boundary calls the shared child-agent helpers (`captureDelegatedPolicyOverrides`/`appendDelegatedPolicyOverrides` in `dsh-subagent`) before its first await; the one-shot driver and the [continuable start](../../../../packages/subagent/subagent/README.md) both use them. The capture copies the current `permission/preset` identity when `permissionPresets.current(parent.session)` is `auto` or `danger-full-access`, snapshots `sandboxPolicy.overrideOf(parent.session)`, and pins the child approval policy to `'never'`. A later parent switch belongs to the parent's future; cancel-and-redelegate takes a new snapshot. The permission-preset and sandbox-policy services are optional: only the Auto or Full access identity and the explicit sandbox session override are copied, never deployment defaults or one-shot grants. The approval policy is not inherited — the [approvals-pinned decision](2026-08-10-subagent-approval-pinned-never.md) supersedes this note's original approval-override inheritance.
|
||||
|
||||
An inherited Auto or Full access identity becomes a `permission/preset` event, while captured sandbox and pinned approval values become source-tagged `sandbox/mode` and `approval/policy` events during the child factory's unpublished setup. The session constructor has already fixed `Session.firstLiveSeq` after the constructor seed, while `Session.inheritedEventCount` keeps the exact fork-prefix length, so the inherited facts follow fork history without changing its lineage cut. Lifecycle-local telemetry starts at `firstLiveSeq`, so it excludes the constructor seed and includes these unpublished-setup events. Existing last-event-wins folds therefore make the delegation snapshot beat stale fork history and let a later child switch beat the snapshot. A grandchild folds its parent's logged state, so the rule composes without another inheritance mechanism.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Auto 或 Full access 预设身份以及沙箱与审批覆盖项都是按会话
|
||||
|
||||
## 决策
|
||||
|
||||
委派边界会在第一次 await 之前调用共享的子 agent 辅助函数(`dsh-subagent` 中的 `captureDelegatedPolicyOverrides`/`appendDelegatedPolicyOverrides`);一次性驱动器与[可继续启动](../../../../packages/subagent/subagent/README.zh.md)都会使用它们。当 `permissionPresets.current(parent.session)` 为 `auto` 或 `danger-full-access` 时,捕获会复制当前 `permission/preset` 身份,同时对 `sandboxPolicy.overrideOf(parent.session)` 获取快照,并把子 agent 的审批策略钉定为 `'never'`。父级后续的切换属于父级的未来;取消后重新委派会取得新快照。权限预设与沙箱策略服务都是可选的:只复制 Auto/Full access 共用旋钮组合的身份与显式沙箱会话覆盖项,绝不复制部署默认值或一次性授权。审批策略不继承——[审批钉定决策](2026-08-10-subagent-approval-pinned-never.zh.md)取代了本 note 原先的审批覆盖项继承。
|
||||
委派边界会在第一次 await 之前调用共享的子 agent 辅助函数(`dsh-subagent` 中的 `captureDelegatedPolicyOverrides`/`appendDelegatedPolicyOverrides`);一次性驱动器与[可继续启动](../../../../packages/subagent/subagent/README.zh.md)都会使用它们。当 `permissionPresets.current(parent.session)` 为 `auto` 或 `danger-full-access` 时,捕获会复制当前 `permission/preset` 身份,同时对 `sandboxPolicy.overrideOf(parent.session)` 获取快照,并把子 agent 的审批策略钉定为 `'never'`。父级后续的切换属于父级的未来;取消后重新委派会取得新快照。权限预设与沙箱策略服务都是可选的:只复制 Auto 或 Full access 身份与显式沙箱会话覆盖项,绝不复制部署默认值或一次性授权。审批策略不继承——[审批钉定决策](2026-08-10-subagent-approval-pinned-never.zh.md)取代了本 note 原先的审批覆盖项继承。
|
||||
|
||||
继承的 Auto 或 Full access 身份会成为一条 `permission/preset` 事件,捕获的沙箱值与钉定的审批值则会在子 agent 工厂的未发布设置阶段成为带来源标记的 `sandbox/mode` 与 `approval/policy` 事件。会话构造函数已把 `Session.firstLiveSeq` 固定在 constructor seed 之后,而 `Session.inheritedEventCount` 保留精确的 fork 前缀长度,因此继承事实会排在 fork 历史之后,却不改变其谱系 cut。生命周期本地遥测从 `firstLiveSeq` 开始,因此排除 constructor seed,并包含这些未发布设置事件。因此,既有的末事件胜出折叠会让委派快照压过陈旧的 fork 历史,并让子 agent 后续的切换压过该快照。孙代 agent 会折叠其父级已记录的状态,因此无需另一套继承机制即可组合此规则。
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
en: 8892eea7f62d45ed
|
||||
zh: 18f64966ff53e38b
|
||||
/agent-note-web-subagent-catalog-and-human-continuation/design-context:
|
||||
en: 3c44fc5af90bc807
|
||||
zh: 478538d028a52858
|
||||
en: da3858c86cf685ff
|
||||
zh: b13ee4f45e3dff7d
|
||||
/agent-note-web-subagent-catalog-and-human-continuation/product-contract:
|
||||
en: 2060d607d1cd1ef3
|
||||
zh: e7100887358625c4
|
||||
|
||||
@@ -28,7 +28,7 @@ This decision covers Web discovery, transcript viewing, and parent-authorized hu
|
||||
|
||||
## Design context
|
||||
|
||||
The Figma [subagent list](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5J8/Harness?node-id=383-14602&p=f), [hierarchical expansion](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5J8/Harness?node-id=383-15917&p=f), and [child conversation](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5J8/Harness?node-id=388-18584&p=f) frames are non-normative interaction and visual references. This note owns lifecycle, wire, and failure semantics.
|
||||
The Figma subagent list, hierarchical expansion, and child conversation frames are non-normative interaction and visual references. This note owns lifecycle, wire, and failure semantics.
|
||||
|
||||
| Design intent | Shipped contract |
|
||||
| --- | --- |
|
||||
|
||||
@@ -28,7 +28,7 @@ Web 产品通过页头的当前 title 谱系区域公开选中会话中由会话
|
||||
|
||||
## 设计上下文
|
||||
|
||||
Figma 中的 [subagent 列表](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5J8/Harness?node-id=383-14602&p=f)、[层级展开](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5J8/Harness?node-id=383-15917&p=f)与 [child 对话](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5J8/Harness?node-id=388-18584&p=f)画框是非规范性的交互与视觉参考。本记录负责生命周期、协议与失败语义。
|
||||
Figma 中的 subagent 列表、层级展开与 child 对话画框是非规范性的交互与视觉参考。本记录负责生命周期、协议与失败语义。
|
||||
|
||||
| 设计意图 | 已交付约定 |
|
||||
| --- | --- |
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 492ba2355360d81a
|
||||
zh: 825c0e7ab1cf4beb
|
||||
/agent-note-experimental-auto-review-before-each-tool-call/decision:
|
||||
en: 8bf58a9d5c800af6
|
||||
zh: 3bb386279301fbbf
|
||||
en: fd4103b65b24f6e2
|
||||
zh: 5c1a364355b7e9ab
|
||||
/agent-note-experimental-auto-review-before-each-tool-call/decision/effects-and-authority:
|
||||
en: 55a79d8572fb4332
|
||||
zh: f63d03d0f3bb5811
|
||||
@@ -18,8 +18,8 @@
|
||||
en: d93bd9b8af6b0bfc
|
||||
zh: 9514c2e5554a721e
|
||||
/agent-note-experimental-auto-review-before-each-tool-call/decision/result-and-cancellation:
|
||||
en: 7952d0f88e7b7753
|
||||
zh: ea85c96f9d0b78a1
|
||||
en: 237018634b5f9dcf
|
||||
zh: cbc49d9c1b794065
|
||||
/agent-note-experimental-auto-review-before-each-tool-call/decision/process-catalog-and-children:
|
||||
en: c305e3506fb4febc
|
||||
zh: aecc99cda8be3de2
|
||||
|
||||
@@ -10,9 +10,9 @@ Full access lets useful project work proceed without repeated approvals, but it
|
||||
|
||||
## Decision
|
||||
|
||||
[`dsh-experimental-auto-review`](../../../../packages/experimental/auto-review/README.md) is an explicitly installed experimental Web layer, published under the [experimental package publication decision](../process/2026-09-12-publish-all-experimental-packages.md). Default Web retains Read Only, Workspace Write, and Full access. The layer contributes current-session `auto`, whose only durable identity is `permission/preset:auto`; it shares Full access's unchanged `danger-full-access + never` knobs and tool definitions. Headless, General settings, and new-session defaults exclude the integration.
|
||||
[`dsh-experimental-auto-review`](../../../../packages/experimental/auto-review/README.md) is an explicitly installed experimental Web layer, published under the [experimental package publication decision](../process/2026-09-12-publish-all-experimental-packages.md). Default Web retains Read Only, Workspace Write, and Full access. The layer contributes current-session `auto`, whose only durable identity is `permission/preset:auto`; it uses Full access's `danger-full-access` sandbox and tool definitions with the approval policy chosen by [the user-approval fallback decision](2026-09-24-auto-review-user-approval-fallback.md). Headless, General settings, and new-session defaults exclude the integration.
|
||||
|
||||
Every native call and started PTC `tools.*` inner call receives one review before its body. The outer `run_code` transport and direct Node effects in a PTC program remain outside this guarantee. There are no tool-name exemptions, cached grants, retries, configurable policy, second authorization check, or manual fallback. A repeated call receives a fresh review.
|
||||
Every native call and started PTC `tools.*` inner call receives one review before its body. The outer `run_code` transport and direct Node effects in a PTC program remain outside this guarantee. There are no tool-name exemptions, cached grants, retries, configurable policy, or second authorization check; [the user-approval fallback decision](2026-09-24-auto-review-user-approval-fallback.md) owns what follows a denial. A repeated call receives a fresh review.
|
||||
|
||||
### Effects and authority
|
||||
|
||||
@@ -46,11 +46,11 @@ The main agent's V3 `system/message` nodes, assistant text/reasoning, and tool r
|
||||
|
||||
### Result and cancellation
|
||||
|
||||
The reviewer may emit reasoning blocks followed by exactly one JSON text block and terminal `stop`. The closed object admits only `low + allow`, `medium + allow/deny`, and `high + deny`; only deny may carry a string `reason`. Extra fields, duplicate members, invalid combinations, other blocks or termination, and provider failures share the ordinary Auto denial outcome. Risk and reviewer traces are not durable state.
|
||||
The reviewer may emit reasoning blocks followed by exactly one JSON text block and terminal `stop`. The closed object admits only `low + allow`, `medium + allow/deny`, and `high + deny`; only deny may carry a string `reason`. Extra fields, duplicate members, invalid combinations, other blocks or termination, and provider failures are reviewer failures, which [the user-approval fallback decision](2026-09-24-auto-review-user-approval-fallback.md) reports with their specific error. Risk and reviewer traces are not durable state.
|
||||
|
||||
Native results and PTC settle events carry the same structured `AutoReviewDeniedError` / `AUTO_REVIEW_DENIED` and optional raw reason. The main agent receives only `Auto review rejected tool "<name>"; its body was not executed` through ordinary failure rendering. PTC retains the existing program exception/catch behavior; catching a denial does not elevate it to an outer failure. The generic Web tool card supplies the denial identity for the collapsed row and one not-executed output line for the expanded row, with no input body. Only that display trims and collapses line separators or supplies the localized empty-reason fallback; persistence and both SDKs preserve the complete raw reason, without a new length or redaction rule.
|
||||
A final denial, which [the user-approval fallback decision](2026-09-24-auto-review-user-approval-fallback.md) limits to the `never` approval policy, gives native results and PTC settle events the same structured `AutoReviewDeniedError` / `AUTO_REVIEW_DENIED` and optional raw reason. For a final denial the main agent receives only `Auto review rejected tool "<name>"; its body was not executed` through ordinary failure rendering. PTC retains the existing program exception/catch behavior; catching a denial does not elevate it to an outer failure. The generic Web tool card supplies the denial identity for the collapsed row and one not-executed output line for the expanded row, with no input body. Only that display trims and collapses line separators or supplies the localized empty-reason fallback; persistence and both SDKs preserve the complete raw reason, without a new length or redaction rule.
|
||||
|
||||
Admission and active-review enrollment occur synchronously before the first await. The integration owns one lifecycle controller and one set of active operations. Unload closes new selection/review admission, changes live Auto Sessions to Full access through the existing preset writer without changing knobs or closing terminals, then aborts and drains reviews before removing listener and contribution. After provider settlement, a lifecycle abort always produces canonical pre-dispatch cancellation, including late allow, deny, or failure. Caller cancellation retains ToolRuntime's priority: late allow cancels before dispatch; late deny or failure retains that outcome. No cancelled review starts a tool body.
|
||||
Admission and active-review enrollment occur synchronously before the first await. The integration owns one lifecycle controller and one set of active operations. Unload closes new selection/review admission, changes live Auto Sessions to Full access through the existing preset writer, which writes the `never` approval policy without changing the sandbox value or closing terminals, then aborts and drains reviews before removing listener and contribution. After provider settlement, a lifecycle abort always produces canonical pre-dispatch cancellation, including late allow, deny, or failure. Caller cancellation retains ToolRuntime's priority: late allow cancels before dispatch; a late failure or final denial retains that outcome, and a late denial under `ask` produces a cancelled approval and canonical pre-dispatch cancellation. No cancelled review starts a tool body.
|
||||
|
||||
A persisted Auto Session cannot publish when the complete integration is absent or failed. Its log is not rewritten, and no background retry runs. Reinstallation allows a user to reopen it; live Sessions migrated to Full access remain there until explicitly switched.
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ Full access 让有用的项目工作无需反复审批即可继续,但也允
|
||||
|
||||
## 决策
|
||||
|
||||
[`dsh-experimental-auto-review`](../../../../packages/experimental/auto-review/README.zh.md)是显式安装的实验性 Web 层,按[实验包发布决策](../process/2026-09-12-publish-all-experimental-packages.zh.md)参与发布。默认 Web 保持 Read Only、Workspace Write 与 Full access。此层贡献仅限当前会话的 `auto`,唯一持久身份为 `permission/preset:auto`;它共用 Full access 未改变的 `danger-full-access + never` 旋钮与工具定义。Headless、通用设置与新会话默认值都排除此 integration。
|
||||
[`dsh-experimental-auto-review`](../../../../packages/experimental/auto-review/README.zh.md)是显式安装的实验性 Web 层,按[实验包发布决策](../process/2026-09-12-publish-all-experimental-packages.zh.md)参与发布。默认 Web 保持 Read Only、Workspace Write 与 Full access。此层贡献仅限当前会话的 `auto`,唯一持久身份为 `permission/preset:auto`;它使用 Full access 的 `danger-full-access` 沙箱与工具定义,审批策略由[用户审批兜底决策](2026-09-24-auto-review-user-approval-fallback.zh.md)决定。Headless、通用设置与新会话默认值都排除此 integration。
|
||||
|
||||
每个原生调用与已开始的 PTC `tools.*` inner call 都在 body 前接受一次审查。外层 `run_code` transport 与 PTC 程序内直接 Node 效果不在保证范围内。不提供按工具名豁免、缓存 grant、重试、可配置策略、第二授权检查或人工 fallback。重复调用也重新审查。
|
||||
每个原生调用与已开始的 PTC `tools.*` inner call 都在 body 前接受一次审查。外层 `run_code` transport 与 PTC 程序内直接 Node 效果不在保证范围内。不提供按工具名豁免、缓存 grant、重试、可配置策略或第二授权检查;拒绝之后的行为由[用户审批兜底决策](2026-09-24-auto-review-user-approval-fallback.zh.md)负责。重复调用也重新审查。
|
||||
|
||||
### 效果与权威
|
||||
|
||||
@@ -46,11 +46,11 @@ reviewer 从 Session 的完整动作历史构建这两个动作分节:授权
|
||||
|
||||
### 结果与取消
|
||||
|
||||
Reviewer 可以输出 reasoning blocks,随后恰好一个 JSON text block 和终态 `stop`。封闭对象只允许 `low + allow`、`medium + allow/deny` 与 `high + deny`;只有 deny 可携带字符串 `reason`。额外字段、重复成员、非法组合、其他 block 或终态以及 provider 失败均使用普通 Auto 拒绝结果。风险与 reviewer trace 不成为持久状态。
|
||||
Reviewer 可以输出 reasoning blocks,随后恰好一个 JSON text block 和终态 `stop`。封闭对象只允许 `low + allow`、`medium + allow/deny` 与 `high + deny`;只有 deny 可携带字符串 `reason`。额外字段、重复成员、非法组合、其他 block 或终态以及 provider 失败都是 reviewer 失败,由[用户审批兜底决策](2026-09-24-auto-review-user-approval-fallback.zh.md)以具体错误报告。风险与 reviewer trace 不成为持久状态。
|
||||
|
||||
原生结果与 PTC 结算事件携带同形结构化 `AutoReviewDeniedError`/`AUTO_REVIEW_DENIED` 及可选原始理由。主 agent 通过普通失败渲染只收到 `Auto review rejected tool "<name>"; its body was not executed`。PTC 保留既有程序异常/catch 行为;捕获拒绝不会将其提升为外层失败。通用 Web 工具卡片为折叠行提供拒绝身份,为展开行提供一行未执行输出,不提供输入正文。只有该显示过程会 trim、折叠行分隔符,或提供本地化空理由 fallback;持久化与两套 SDK 保留完整原始理由,不增加长度或脱敏规则。
|
||||
最终拒绝([用户审批兜底决策](2026-09-24-auto-review-user-approval-fallback.zh.md)将其限定在 `never` 审批策略下)让原生结果与 PTC 结算事件携带同形结构化 `AutoReviewDeniedError`/`AUTO_REVIEW_DENIED` 及可选原始理由。对最终拒绝,主 agent 通过普通失败渲染只收到 `Auto review rejected tool "<name>"; its body was not executed`。PTC 保留既有程序异常/catch 行为;捕获拒绝不会将其提升为外层失败。通用 Web 工具卡片为折叠行提供拒绝身份,为展开行提供一行未执行输出,不提供输入正文。只有该显示过程会 trim、折叠行分隔符,或提供本地化空理由 fallback;持久化与两套 SDK 保留完整原始理由,不增加长度或脱敏规则。
|
||||
|
||||
准入与在途 review 登记在首次 await 前同步完成。Integration 拥有一个生命周期 controller 和一个在途操作集合。卸载先关闭新选择/review admission,经由既有 preset writer 把存活 Auto Session 切为 Full access,不改变旋钮、不关闭终端,然后中止并等待 review 结清,最后移除 listener 与 contribution。Provider 结算后,lifecycle abort 始终形成规范的 dispatch 前取消,包括晚到 allow、deny 或 failure。Caller 取消保留 ToolRuntime 优先级:晚到 allow 在 dispatch 前取消;晚到 deny 或 failure 保留原结果。被取消的 review 不启动工具 body。
|
||||
准入与在途 review 登记在首次 await 前同步完成。Integration 拥有一个生命周期 controller 和一个在途操作集合。卸载先关闭新选择/review admission,经由既有 preset writer 把存活 Auto Session 切为 Full access,该 writer 写入 `never` 审批策略,不改变沙箱值、不关闭终端,然后中止并等待 review 结清,最后移除 listener 与 contribution。Provider 结算后,lifecycle abort 始终形成规范的 dispatch 前取消,包括晚到 allow、deny 或 failure。Caller 取消保留 ToolRuntime 优先级:晚到 allow 在 dispatch 前取消;晚到 failure 或最终拒绝保留原结果,`ask` 下晚到的拒绝产生已取消的审批与规范的 dispatch 前取消。被取消的 review 不启动工具 body。
|
||||
|
||||
完整 integration 缺失或失败时,持久 Auto Session 不能发布。日志不改写,也不后台重试。重装后用户可以重新打开它;已经迁移为 Full access 的存活 Session 保持原状,直到显式切换。
|
||||
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-09-24-auto-review-user-approval-fallback.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-24-auto-review-user-approval-fallback.md
|
||||
/agent-note-auto-review-asks-the-user-after-a-denial:
|
||||
en: 5591f8e34bda222a
|
||||
zh: 7d433ca541e6988f
|
||||
/agent-note-auto-review-asks-the-user-after-a-denial/problem:
|
||||
en: ed725dff16cd2339
|
||||
zh: 555d4ba70af96790
|
||||
/agent-note-auto-review-asks-the-user-after-a-denial/decision:
|
||||
en: 4782a7609717bac3
|
||||
zh: f3ebb3581b807f3f
|
||||
/agent-note-auto-review-asks-the-user-after-a-denial/alternatives-considered:
|
||||
en: 01f71dd10849f925
|
||||
zh: 8b79a7e149a9a60c
|
||||
/agent-note-auto-review-asks-the-user-after-a-denial/consequences:
|
||||
en: 822a4241ddf39448
|
||||
zh: dd05997932eed142
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: Auto review asks the user after a denial
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-24-auto-review-user-approval-fallback.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
[Auto review](2026-08-28-auto-review.md) made every reviewer denial final and reported every reviewer failure as that same denial. A person watching the Session had no way to let a denied call run, even when the reviewer misjudged an action the person wanted, so the only recovery was to switch the whole Session to Full access. A malformed reviewer response, a provider error, or a missing Session fact reached the model and the user as `Auto review rejected tool "<name>"`, so neither could tell a policy decision from a technical failure or act on the cause.
|
||||
|
||||
## Decision
|
||||
|
||||
A reviewer denial returns the tools pipeline's existing `ask` decision when the Session's approval policy is `ask`. `ToolRuntime` sends it through the approval service with the reason `Auto review denied tool "<name>"`, followed by `: <reviewer reason>` when the reviewer gave one. `allowed-once` executes the call; `rejected`, `cancelled`, and `unavailable` deny it through the approval service's ordinary messages, and the body does not run. The listener asks only after later `tools/pre-execute` listeners allow the call, so a downstream denial or cancellation wins without prompting.
|
||||
|
||||
Selecting Auto writes `danger-full-access` with the `ask` approval policy. A recorded Auto selection also matches the `never` policy. A [delegated in-process child pins `never`](2026-08-10-subagent-approval-pinned-never.md), so a child Auto keeps the final `AutoReviewDeniedError` denial and its reason, and a Session recorded under the earlier `never` bundle still resolves to Auto instead of Full access. Selecting Auto again writes `ask`.
|
||||
|
||||
A reviewer failure denies the call with `Auto review of tool "<name>" failed; its body was not executed: <error>` and no structured error info. A provider failure's message carries the finish kind, failure code, and provider message, so the model and the generic tool card show the actual cause.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Ask the user after every reviewer failure too.** A failure says nothing about the action's risk; asking would present a technical error as a policy question and would hide a broken reviewer route behind repeated prompts.
|
||||
|
||||
**Always return `ask` and let the `never` policy reject it.** The approval service reports that rejection as `the user rejected tool "<name>"`, which is false for a child, and it drops the reviewer's reason.
|
||||
|
||||
**Keep Auto on `never` and let the reviewer bypass the approval policy.** The approval service enforces `never` before any answerer so no listener can bypass it; the model-context policy text would also tell the main agent that no approval can happen.
|
||||
|
||||
## Consequences
|
||||
|
||||
- An interactive Auto Session can pause on an approval prompt, so Auto no longer guarantees unattended progress.
|
||||
- The raw reviewer reason is visible to the user in the approval prompt and the approval audit events; the main model sees only the approval outcome.
|
||||
- A reviewer failure's text, including provider messages, becomes model-visible tool error content.
|
||||
- Switching between Auto and Full access now also changes the approval policy, so the live-agent switch path queues the approval-policy change notice.
|
||||
- Unit tests cover the approval outcomes, the downstream-denial ordering, the child `never` path, and the specific failure messages; the shipped Web composition test covers a user rejection after a real reviewer denial.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: Auto review 拒绝后请求用户审批
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-24-auto-review-user-approval-fallback.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
[Auto review](2026-08-28-auto-review.zh.md) 把每次 reviewer 拒绝都当作最终结果,并把每次 reviewer 失败也报告为同样的拒绝。旁观 Session 的人无法让被拒绝的调用继续执行,即使 reviewer 误判了此人想要的动作;唯一的补救是把整个 Session 切换到 Full access。Reviewer 响应不合法、provider 出错或 Session 事实缺失时,模型与用户看到的都是 `Auto review rejected tool "<name>"`,因此两者都无法区分策略决定与技术失败,也无法针对原因采取行动。
|
||||
|
||||
## Decision
|
||||
|
||||
当 Session 的审批策略为 `ask` 时,reviewer 拒绝返回 tools 流水线既有的 `ask` 决定。`ToolRuntime` 通过审批服务发送该请求,理由为 `Auto review denied tool "<name>"`;reviewer 给出理由时再追加 `: <reviewer reason>`。`allowed-once` 执行调用;`rejected`、`cancelled` 与 `unavailable` 通过审批服务的普通消息拒绝调用,body 不执行。只有后续 `tools/pre-execute` listener 放行调用后,listener 才请求审批,因此下游拒绝或取消优先且不弹出审批。
|
||||
|
||||
选择 Auto 会写入 `danger-full-access` 与 `ask` 审批策略。已记录的 Auto 选择也匹配 `never` 策略。[进程内委派 child 固定 `never`](2026-08-10-subagent-approval-pinned-never.zh.md),因此 child 的 Auto 保留最终的 `AutoReviewDeniedError` 拒绝及其理由;按先前 `never` 组合记录的 Session 仍解析为 Auto,而不是 Full access。再次选择 Auto 会写入 `ask`。
|
||||
|
||||
Reviewer 失败以 `Auto review of tool "<name>" failed; its body was not executed: <error>` 拒绝调用,不带结构化错误信息。Provider 失败消息包含 finish 类型、失败代码与 provider 消息,因此模型与通用工具卡片显示实际原因。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Reviewer 失败后也请求用户审批。** 失败不说明动作风险;请求审批会把技术错误呈现为策略问题,并用反复弹出的审批掩盖损坏的 reviewer 路由。
|
||||
|
||||
**始终返回 `ask`,由 `never` 策略拒绝。** 审批服务把该拒绝报告为 `the user rejected tool "<name>"`,这对 child 不成立,而且会丢失 reviewer 理由。
|
||||
|
||||
**Auto 保持 `never`,由 reviewer 绕过审批策略。** 审批服务在任何应答者之前执行 `never`,因此没有 listener 能绕过它;模型上下文中的策略文本也会告诉主 agent 不可能发生审批。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 交互式 Auto Session 可能停在审批提示上,因此 Auto 不再保证无人值守地推进。
|
||||
- 用户可以在审批提示与审批审计事件中看到原始 reviewer 理由;主模型只看到审批结果。
|
||||
- Reviewer 失败文本(包括 provider 消息)成为模型可见的工具错误内容。
|
||||
- 在 Auto 与 Full access 之间切换现在也会改变审批策略,因此存活 agent 的切换路径会排入审批策略变更通知。
|
||||
- 单元测试覆盖各审批结果、下游拒绝的先后顺序、child 的 `never` 路径以及具体失败消息;shipped Web 组合测试覆盖真实 reviewer 拒绝之后的用户拒绝。
|
||||
@@ -9,8 +9,8 @@
|
||||
en: cef0f05fd8dfd6a1
|
||||
zh: 228107eb6c00a91a
|
||||
/agent-note-freeze-low-future-value-agent-notes-outside-the-active-corpus/decision:
|
||||
en: 453cf66046816477
|
||||
zh: af5bb77e849416a1
|
||||
en: f0d9c292c3b8dd3b
|
||||
zh: fd928084dfd333e2
|
||||
/agent-note-freeze-low-future-value-agent-notes-outside-the-active-corpus/alternatives-considered:
|
||||
en: 7637deef67f3ec14
|
||||
zh: 7f43606e487fab67
|
||||
|
||||
@@ -18,7 +18,7 @@ The root `.rgignore` excludes the archive from searches that traverse a parent d
|
||||
|
||||
After archival, the triplet is permanently frozen and is historical context rather than current authority. It is not updated for renamed packages, changed behavior, translation standards, formatting rules, broken outbound links, or later documentation contracts. Active prose may intentionally link into an archived note, redirect that link to current authority, or delete it. Repository gates therefore validate links into archived files but never treat archived files as link sources.
|
||||
|
||||
[`verify-archived-agent-notes`](../../../../scripts/verify-archived-agent-notes.ts) owns the frozen boundary. It accepts only the closed set of Agent Note kinds, requires a complete triplet with implemented status and matching valid archive dates, verifies the sidecar against both current Git blob hashes, and seals every artifact by path and SHA-256 content hash in an append-only manifest. Its `--write` mode first proves every existing seal unchanged and then appends only newly archived artifacts. Pull-request CI supplies the trusted base SHA and checks out complete history before running the verifier, so a reused runner's shallow checkout cannot omit the baseline manifest. The ordinary Agent Note format, translation-pairing, wrapping, Markdown-link, package-path, Mermaid, documentation-TypeScript, and type-equivalence gates exclude archive sources; their evolving standards cannot create pressure to edit history.
|
||||
[`verify-archived-agent-notes`](../../../../scripts/verify-archived-agent-notes.ts) owns the frozen boundary. It accepts only the closed set of Agent Note kinds, requires a complete triplet with implemented status and matching valid archive dates, verifies the sidecar against both current Git blob hashes, and seals every artifact by path and SHA-256 content hash in an append-only manifest, with only the exact seal-transition exceptions recorded in [archive instructions](../../archived/AGENTS.md). Its `--write` mode first proves every existing seal unchanged and then appends only newly archived artifacts. Pull-request CI supplies the trusted base SHA and checks out complete history before running the verifier, so a reused runner's shallow checkout cannot omit the baseline manifest. The ordinary Agent Note format, translation-pairing, wrapping, Markdown-link, package-path, Mermaid, documentation-TypeScript, and type-equivalence gates exclude archive sources; their evolving standards cannot create pressure to edit history.
|
||||
|
||||
The [`dsh-archive-agent-notes`](../../../skills/dsh-archive-agent-notes/SKILL.md) workflow owns classification. It requires a semantic note-by-note audit, uses code and current documentation to identify present authority, treats word count only as triage, carries calibrated keep/archive/delete examples, and reports genuinely borderline outcomes for review.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ implemented Agent Note 作为当前决策记录持续维护,因此活跃记录
|
||||
|
||||
归档后,这三个文件永久冻结,只作为历史背景,不再是当前权威依据。不得因为包重命名、行为变化、翻译标准、格式规则、出站链接失效或后续文档约定而更新归档文件。活跃文档可以有意链接到归档 Agent Note,也可以把该链接重定向到当前权威依据,或直接删除。仓库门禁因此会校验指向归档文件的链接,但绝不把归档文件作为链接源来校验。
|
||||
|
||||
[`verify-archived-agent-notes`](../../../../scripts/verify-archived-agent-notes.ts) 负责维护冻结边界。它只接受封闭集合中的 Agent Note 类别,要求三个配对文件完整、状态为 implemented,且归档日期有效并互相匹配;它还会用双方当前的 Git blob hash 校验伴随记录,并在仅追加的 manifest(元数据清单)中按路径和 SHA-256 内容 hash 封存每项产物。其 `--write` 模式会先证明每条现有封存记录对应的内容都未改变,再仅追加新归档的产物。拉取请求 CI 会提供可信的基准 SHA,并在运行校验器前检出完整历史,因此复用运行器上的浅克隆检出无法漏掉基线 manifest。普通的 Agent Note 格式、翻译配对、换行、Markdown 链接、包路径、Mermaid、文档 TypeScript 和类型等价门禁都排除归档源文件,因此这些门禁持续演进的标准不会产生修改历史记录的压力。
|
||||
[`verify-archived-agent-notes`](../../../../scripts/verify-archived-agent-notes.ts) 负责维护冻结边界。它只接受封闭集合中的 Agent Note 类别,要求三个配对文件完整、状态为 implemented,且归档日期有效并互相匹配;它还会用双方当前的 Git blob hash 校验伴随记录,并在仅追加的 manifest(元数据清单)中按路径和 SHA-256 内容 hash 封存每项产物,仅允许[归档说明](../../archived/AGENTS.md)中记录的精确封存哈希转换例外。其 `--write` 模式会先证明每条现有封存记录对应的内容都未改变,再仅追加新归档的产物。拉取请求 CI 会提供可信的基准 SHA,并在运行校验器前检出完整历史,因此复用运行器上的浅克隆检出无法漏掉基线 manifest。普通的 Agent Note 格式、翻译配对、换行、Markdown 链接、包路径、Mermaid、文档 TypeScript 和类型等价门禁都排除归档源文件,因此这些门禁持续演进的标准不会产生修改历史记录的压力。
|
||||
|
||||
[`dsh-archive-agent-notes`](../../../skills/dsh-archive-agent-notes/SKILL.md) 工作流负责分类判断。它要求逐份 Agent Note 做语义审计,使用代码和当前文档识别现行权威依据,仅把字数作为初步筛选手段,收录经过校准的保留、归档和删除示例,并报告真正处于边界的结果,以供评审。
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: b3ec2245644b46e6
|
||||
zh: 83de1cb620f9ddcf
|
||||
/agent-note-make-lefthook-installation-worktree-local/decision:
|
||||
en: 577d90ff7e03ebaf
|
||||
zh: dc680155c0c3a2a1
|
||||
en: b53a2425f88a6031
|
||||
zh: 41f459b00803b3f9
|
||||
/agent-note-make-lefthook-installation-worktree-local/alternatives-considered:
|
||||
en: d7100b0d2c95a3f9
|
||||
zh: 070c38c4d0d1331a
|
||||
|
||||
@@ -14,7 +14,7 @@ Lefthook-generated hooks prefer an absolute binary path captured from the instal
|
||||
|
||||
Hook installation is worktree-scoped. With `CI=true` or `GITHUB_ACTIONS=true`, the installer returns before Git discovery or mutation because automated jobs do not consume contributor hooks. Otherwise, it requires Git 2.26 or newer so `git config --show-scope` can report which scope supplied a value, upgrades a format-0 repository to format 1, enables `extensions.worktreeConfig`, and assigns the current worktree an absolute `core.hooksPath` at `$GIT_DIR/dsh-hooks`.
|
||||
|
||||
Before upgrading format 0, the installer refuses direct common-config `extensions.*`; it also refuses direct `core.worktree` or `core.bare=true` and non-empty dormant worktree configs that enabling the extension would activate. The migration removes direct `core.bare=false` because false is Git's default. The common repository config and every existing `config.worktree` must be regular files. These checks disable include expansion because Git's repository-format parser also ignores included targets. A repository-scoped lock serializes migration and hook writes; its process ID, random token, file identity, and exact contents must still match at release. Dead or invalid locks require manual recovery rather than automatic breaking.
|
||||
Before upgrading format 0, the installer refuses direct common-config `extensions.*`; it also refuses direct `core.worktree` or `core.bare=true` and non-empty dormant worktree configs that enabling the extension would activate. The migration removes direct `core.bare=false` because false is Git's default. The common repository config and every existing `config.worktree` must be regular files. These checks disable include expansion because Git's repository-format parser also ignores included targets. A repository-scoped lock serializes migration and hook writes; its process ID, random token, file identity, and exact contents must still match at release. Dead or invalid locks require manual recovery rather than automatic breaking. On Windows, acquiring or inspecting a lock retries EPERM within the existing 30-second acquisition deadline because deleting a lock can temporarily deny access while readers close their handles; other errors and expired deadlines still fail.
|
||||
|
||||
Each hook directory carries a JSON ownership marker containing the absolute path last published to worktree config. After a checkout moves, that marker permits replacement of only the exact stale owned value. Git seeds a new linked worktree's `config.worktree` from the main worktree; when that seed contains the marker-backed reserved hook path of a registered worktree, the installer replaces only the new worktree's config with its own path. Before Lefthook runs, the marker and every existing generated hook must be unaliased regular files. The installer resolves the effective scope, origin, and value of `core.hooksPath`, including active `config.worktree` includes; it refuses command-scoped paths, unowned worktree-scoped paths, and unowned reserved directories. An inherited system, global, or common-repository path requires `DSH_LEFTHOOK_ALLOW_HOOKS_PATH_OVERRIDE=1`, which opts only the current worktree into Lefthook. Inactive `includeIf` targets are not recursively inspected because they do not affect the current configuration. Command-scoped Git configuration is removed from the Lefthook subprocess environment after validation.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Lefthook 生成的钩子会优先使用安装时从对应 worktree 记录的绝
|
||||
|
||||
钩子安装以 worktree 为作用域。当 `CI=true` 或 `GITHUB_ACTIONS=true` 时,安装程序会在探测 Git 或做出任何变更之前返回,因为自动化任务不会使用贡献者钩子。否则,安装程序要求 Git 2.26 或更高版本,使 `git config --show-scope` 可以报告由哪个作用域提供配置值;它会将格式版本为 0 的仓库升级到格式版本 1,启用 `extensions.worktreeConfig`,并将当前 worktree 的 `core.hooksPath` 设为指向 `$GIT_DIR/dsh-hooks` 的绝对路径。
|
||||
|
||||
升级格式 0 之前,安装程序会拒绝共用配置中直接设置的 `extensions.*`;它还会拒绝直接设置的 `core.worktree` 或 `core.bare=true`,以及启用扩展后将被激活的非空休眠 worktree 配置。迁移会移除直接设置的 `core.bare=false`,因为 false 是 Git 的默认值。共用仓库配置和每个已有的 `config.worktree` 都必须是常规文件。这些检查会禁用 include 展开,因为 Git 的仓库格式解析器也会忽略 include 目标。仓库级锁会串行化迁移和钩子写入;释放时,锁的进程 ID、随机令牌、文件身份和完整内容必须仍然匹配。所属进程已结束或内容无效的锁必须人工介入恢复,而不能自动强制解除。
|
||||
升级格式 0 之前,安装程序会拒绝共用配置中直接设置的 `extensions.*`;它还会拒绝直接设置的 `core.worktree` 或 `core.bare=true`,以及启用扩展后将被激活的非空休眠 worktree 配置。迁移会移除直接设置的 `core.bare=false`,因为 false 是 Git 的默认值。共用仓库配置和每个已有的 `config.worktree` 都必须是常规文件。这些检查会禁用 include 展开,因为 Git 的仓库格式解析器也会忽略 include 目标。仓库级锁会串行化迁移和钩子写入;释放时,锁的进程 ID、随机令牌、文件身份和完整内容必须仍然匹配。所属进程已结束或内容无效的锁必须人工介入恢复,而不能自动强制解除。在 Windows 上,获取或检查锁时遇到 EPERM 会在原有的 30 秒获取期限内重试,因为删除锁时,读取者关闭句柄前可能暂时无法访问该文件;其他错误及超时仍会导致失败。
|
||||
|
||||
每个钩子目录都有一个 JSON 所有权标记,其中包含上次写入 worktree 配置的绝对路径。检出目录移动后,该标记只允许替换确切的陈旧自有值。Git 会以主 worktree 的配置为新链接 worktree 初始化 `config.worktree`;当该初始配置包含某个已注册 worktree 中由所有权标记佐证的保留钩子路径时,安装程序只会在新 worktree 的配置中将其替换为新 worktree 自有的路径。Lefthook 运行前,所有权标记和每个已有的生成钩子都必须是不带别名的常规文件。安装程序会解析 `core.hooksPath` 的生效作用域、来源和值,包括通过当前生效的 `config.worktree` include 加载的值;它会拒绝命令作用域路径、非自有的 worktree 作用域路径以及非自有的保留目录。继承自系统、全局或共用仓库配置的路径必须设置 `DSH_LEFTHOOK_ALLOW_HOOKS_PATH_OVERRIDE=1`,从而只让当前 worktree 显式启用 Lefthook。未生效的 `includeIf` 目标不会被递归检查,因为它们不影响当前配置。完成验证后,Lefthook 子进程的环境会移除命令作用域的 Git 配置。
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -35,7 +35,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
pages: read
|
||||
|
||||
@@ -56,7 +56,6 @@ External packages installed for runtime use or distributed inside the prebuilt b
|
||||
| [`@opentelemetry/api`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
| [`@opentelemetry/api-logs`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
| [`@opentelemetry/core`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
| [`@opentelemetry/exporter-logs-otlp-http`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
| [`@opentelemetry/otlp-exporter-base`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
| [`@opentelemetry/otlp-transformer`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
| [`@opentelemetry/resources`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
|
||||
@@ -74,6 +74,8 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_storage_domain
|
||||
plugin_dsh_base_session_projection_cache["session-projection-cache<br/>@deepseek-ai/dsh-session-projection-cache"]
|
||||
cfg --> plugin_dsh_base_session_projection_cache
|
||||
plugin_dsh_base_otel["otel<br/>@deepseek-ai/dsh-otel"]
|
||||
cfg --> plugin_dsh_base_otel
|
||||
plugin_dsh_base_session_telemetry_otel["session-telemetry-otel<br/>@deepseek-ai/dsh-session-telemetry-otel"]
|
||||
cfg --> plugin_dsh_base_session_telemetry_otel
|
||||
plugin_dsh_base_subprocess["subprocess<br/>@deepseek-ai/dsh-subprocess-local"]
|
||||
@@ -231,6 +233,7 @@ flowchart LR
|
||||
| `storage-json` | `@deepseek-ai/dsh-storage-json` |
|
||||
| `storage-domain` | `@deepseek-ai/dsh-storage-domain` |
|
||||
| `session-projection-cache` | `@deepseek-ai/dsh-session-projection-cache` |
|
||||
| `otel` | `@deepseek-ai/dsh-otel` |
|
||||
| `session-telemetry-otel` | `@deepseek-ai/dsh-session-telemetry-otel` |
|
||||
| `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
|
||||
| `sandbox` | `@deepseek-ai/dsh-sandbox-local` |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh",
|
||||
"description": "dsh CLI: profile launch, plugin management, and configuration inspection",
|
||||
"version": "0.1.7-rc.1",
|
||||
"version": "0.1.7-rc.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -39,6 +39,7 @@
|
||||
"@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:*",
|
||||
"@deepseek-ai/dsh-cordis-client-runner": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-agent-team-profile": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-schedule-bundle": "workspace:*",
|
||||
"@deepseek-ai/dsh-fs-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-goal": "workspace:*",
|
||||
"@deepseek-ai/dsh-goal-round-driver": "workspace:*",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -28,7 +28,11 @@ 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-')
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { DailyScheduleRecord, ScheduleCatalogEntry } from '@deepseek-ai/dsh-schedule'
|
||||
import { expect, it } from 'vitest'
|
||||
import { withDefaultWeb, webGet, webRequest } from './default-web-process.ts'
|
||||
|
||||
/** The optional Schedule bundle is the switch this built-Web scenario turns on. */
|
||||
const patches = [fileURLToPath(new URL('../../../../../../packages/experimental/schedule-bundle/cordis.patch.yml', import.meta.url))]
|
||||
|
||||
const id = 'retained-remote-task' as DailyScheduleRecord['id']
|
||||
const sessionId = 'unloaded-owner' as ScheduleCatalogEntry['sessionId']
|
||||
const record: DailyScheduleRecord = {
|
||||
@@ -82,7 +86,7 @@ it('edits a retained task through built Remote bindings and preserves its identi
|
||||
expect(await rpc('schedule/update', { request: { sessionId: 'other-owner', id, expected: updated, change } }))
|
||||
.toEqual({ ok: true, value: { id, updated: false, code: 'schedule_not_found' } })
|
||||
expect(await readFile(taskPath(root), 'utf8')).toBe(persisted)
|
||||
}, { prepare: root => seed(root, persisted) })
|
||||
}, { patches, prepare: root => seed(root, persisted) })
|
||||
|
||||
await withDefaultWeb(test, async ({ root, url }) => {
|
||||
const rpc = await connect(url, test.signal)
|
||||
@@ -92,7 +96,7 @@ it('edits a retained task through built Remote bindings and preserves its identi
|
||||
sessionId, id, expected: updated, change: { kind: 'daily', daily: { time: updated.time, time_zone: 'Etc/UTC' } },
|
||||
} })).toEqual({ ok: true, value: { id, updated: false, record: updated } })
|
||||
expect(await readFile(taskPath(root), 'utf8')).toBe(persisted)
|
||||
}, { prepare: root => seed(root, persisted) })
|
||||
}, { patches, prepare: root => seed(root, persisted) })
|
||||
})
|
||||
|
||||
it('creates and idempotently adopts the explicit Session id bound to seeded task data without touching its task file', async (test) => {
|
||||
@@ -114,5 +118,5 @@ it('creates and idempotently adopts the explicit Session id bound to seeded task
|
||||
expect(await rpc('schedule/list', { request: { sessionId } })).toEqual(expectedList)
|
||||
expect(await rpc('schedule/history', { request: { sessionId, id, limit: 20 } })).toEqual(expectedHistory)
|
||||
expect(await readFile(taskPath(root), 'utf8')).toBe(persisted)
|
||||
}, { prepare: root => seed(root, persisted) })
|
||||
}, { patches, prepare: root => seed(root, persisted) })
|
||||
})
|
||||
@@ -8,7 +8,7 @@ import { withDefaultWeb, webGet } from './default-web-process.ts'
|
||||
|
||||
const experimentalName = '@deepseek-ai/dsh-experimental-client-ui-agent-team'
|
||||
|
||||
it('boots default Web without experimental modules or an active built-in Browser', async (test) => {
|
||||
it('boots default Web without experimental modules, scheduling, time context, or an active built-in Browser', async (test) => {
|
||||
await withDefaultWeb(test, async ({ url, request }) => {
|
||||
const auth = await webGet(url, test.signal)
|
||||
const cookie = auth.headers['set-cookie']?.[0]?.split(';', 1)[0]
|
||||
@@ -24,21 +24,20 @@ it('boots default Web without experimental modules or an active built-in Browser
|
||||
expect(roster.entries).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ name: '@deepseek-ai/dsh-host-webserver', state: FiberState.ACTIVE }),
|
||||
expect.objectContaining({ name: '@deepseek-ai/dsh-client-modules', state: FiberState.ACTIVE }),
|
||||
// The Loader rewrites an inserted row's id (`include:ui-schedule`), so
|
||||
// the shipped rows are pinned by name and settled state.
|
||||
expect.objectContaining({ name: '@deepseek-ai/dsh-time-context', state: FiberState.ACTIVE }),
|
||||
expect.objectContaining({ name: '@deepseek-ai/dsh-schedule', state: FiberState.ACTIVE }),
|
||||
expect.objectContaining({ name: '@deepseek-ai/dsh-client-ui-schedule', state: FiberState.ACTIVE }),
|
||||
]))
|
||||
expect(roster.entries.some(entry => entry.name.endsWith('/runtime-roster-observer.js') && entry.state === FiberState.ACTIVE)).toBe(true)
|
||||
expect(roster.plugins.length).toBeGreaterThan(roster.entries.length)
|
||||
expect(roster.modules.some(url => modulePackage(url) === '@deepseek-ai/dsh')).toBe(true)
|
||||
expect(roster.client.entries.length).toBeGreaterThan(0)
|
||||
const browserName = '@deepseek-ai/dsh-client-ui-sidebar-browser'
|
||||
const browserEntry = roster.entries.find(entry => entry.name === browserName)
|
||||
expect(browserEntry).toBeDefined()
|
||||
expect(browserEntry!.state).toBeUndefined()
|
||||
expect(delivered.entries.some(entry => entry.id === browserName)).toBe(false)
|
||||
const browser = roster.entries.find(entry => entry.name === '@deepseek-ai/dsh-client-ui-sidebar-browser')
|
||||
expect(browser).toBeDefined()
|
||||
expect(browser!.state).toBeUndefined()
|
||||
expect(delivered.entries.some(entry => entry.id === '@deepseek-ai/dsh-client-ui-sidebar-browser')).toBe(false)
|
||||
// The optional Schedule bundle inserts these rows; the shipped composition carries none of them.
|
||||
for (const name of ['@deepseek-ai/dsh-time-context', '@deepseek-ai/dsh-schedule', '@deepseek-ai/dsh-client-ui-schedule']) {
|
||||
expect(roster.entries.some(entry => entry.name === name), name).toBe(false)
|
||||
expect(delivered.entries.some(entry => entry.id === name), name).toBe(false)
|
||||
}
|
||||
expect(experimentalRuntimeReferences(roster)).toEqual([])
|
||||
|
||||
const contaminated = await request('mount-experimental')
|
||||
|
||||
@@ -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: [{
|
||||
|
||||
@@ -23,9 +23,6 @@ import { dump, load } from 'js-yaml'
|
||||
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { bundlePatchPaths, composeEntries } from '@deepseek-ai/dsh-app-boot'
|
||||
/** Profile entry ids whose volatile fields these scenarios edit through Settings. */
|
||||
/** Host Schedule tools every live root Agent receives from the shipped Web bundle. */
|
||||
const SCHEDULE_TOOLS = ['schedule_create', 'schedule_delete', 'schedule_list', 'schedule_update']
|
||||
|
||||
const SETTINGS_NAMESPACE = 'agent-preset-registry'
|
||||
const SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE = 'subagent-model-selection-settings'
|
||||
import { applyChildComposition, childSessionMeta } from '@deepseek-ai/dsh-subagent'
|
||||
@@ -136,7 +133,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: [],
|
||||
@@ -266,8 +263,8 @@ describe('the shipped Web composition', () => {
|
||||
// depend on ripgrep being present on the machine.
|
||||
expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([
|
||||
'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
|
||||
'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'present', 'read', 'read_image', 'schedule_create',
|
||||
'schedule_delete', 'schedule_list', 'schedule_update', 'send_message', 'skill',
|
||||
'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'present', 'read', 'read_image',
|
||||
'send_message', 'skill',
|
||||
'subagent', 'subagent_fork', 'todo_write', 'update_goal', 'web_fetch', 'web_search',
|
||||
'workflow', 'write',
|
||||
])
|
||||
@@ -321,7 +318,7 @@ describe('the shipped Web composition', () => {
|
||||
expect(assembly.sections).toEqual([
|
||||
{ name: 'deployment:persona-prefix', text: MINIMAL_PROMPT },
|
||||
])
|
||||
expect(assembly.tools.map(tool => tool.name)).toEqual(['bash', ...SCHEDULE_TOOLS])
|
||||
expect(assembly.tools.map(tool => tool.name)).toEqual(['bash'])
|
||||
expect(assembly.tools.find(tool => tool.name === 'bash')?.description).toBe(MINIMAL_BASH_DESCRIPTION)
|
||||
expect(ctx.commands.find(handle.agent, 'goal')).toBeUndefined()
|
||||
// serviceFor reports preset-owned providers; unisolated consumers inherit the host fs.
|
||||
@@ -345,7 +342,7 @@ describe('the shipped Web composition', () => {
|
||||
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
|
||||
})
|
||||
try {
|
||||
expect(toolNames(ctx, minimal.agent)).toEqual(['bash', ...SCHEDULE_TOOLS])
|
||||
expect(toolNames(ctx, minimal.agent)).toEqual(['bash'])
|
||||
expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10)
|
||||
|
||||
await minimal.dispose()
|
||||
@@ -511,7 +508,7 @@ describe('the shipped Web composition', () => {
|
||||
// stays the preset's choice — minimal mounts no `tool-skill`, so its
|
||||
// tool table has no loader even though the global layer is readable.
|
||||
expect((await ctx.skills.list({ scope: handle.agent })).map(skill => skill.name)).toContain('dsh-badge')
|
||||
expect(toolNames(ctx, handle.agent)).toEqual(['bash', ...SCHEDULE_TOOLS])
|
||||
expect(toolNames(ctx, handle.agent)).toEqual(['bash'])
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
@@ -866,7 +863,7 @@ describe('the default preset as a user setting', () => {
|
||||
try {
|
||||
// `mount()` with no id resolves the effective default. One tool, not
|
||||
// `standard`'s catalog: the setting decided the composition.
|
||||
expect(toolNames(ctx, handle.agent)).toEqual(['bash', ...SCHEDULE_TOOLS])
|
||||
expect(toolNames(ctx, handle.agent)).toEqual(['bash'])
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
@@ -912,7 +909,7 @@ describe('a profile patch stored before Developer tools owned preset selection',
|
||||
setup: agentCtx => legacy.agentPresets.mount(agentCtx).then(() => undefined),
|
||||
})
|
||||
try {
|
||||
expect(toolNames(legacy, handle.agent)).toEqual(['bash', ...SCHEDULE_TOOLS])
|
||||
expect(toolNames(legacy, handle.agent)).toEqual(['bash'])
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-desktop-host",
|
||||
"description": "Private Node-mode host process for the Electron desktop application",
|
||||
"version": "0.1.7-rc.1",
|
||||
"version": "0.1.7-rc.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -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,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',
|
||||
|
||||
@@ -9,6 +9,7 @@ const state: AccountView = {
|
||||
}
|
||||
|
||||
class Account extends DeepSeekAccount {
|
||||
getDeviceIdentity = async () => ({ osVersion: 'test-os' })
|
||||
fail = false
|
||||
ended = Promise.withResolvers<undefined>()
|
||||
reading = Promise.withResolvers<undefined>()
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write apps/desktop/README.md
|
||||
/deepseek-harness-desktop:
|
||||
en: 45561fc0dd3a1b20
|
||||
zh: f4b03355d4bd0693
|
||||
en: b8e4f357cf330721
|
||||
zh: cdaaf35a061f41b1
|
||||
/deepseek-harness-desktop/closing-the-window-and-quitting:
|
||||
en: fb852f3bd70d8602
|
||||
zh: 3d2b0a8d580d7f0a
|
||||
en: c323e02e3f0305fe
|
||||
zh: 3c730db632136443
|
||||
/deepseek-harness-desktop/key-technical-decisions:
|
||||
en: 069fdfd72d2ea26a
|
||||
zh: 76ae549d43c99bb4
|
||||
@@ -15,20 +15,20 @@
|
||||
en: 31cbb7452be8e1ad
|
||||
zh: 502ec556e2ec301e
|
||||
/deepseek-harness-desktop/installation-ownership:
|
||||
en: 8307baf6c5b691d0
|
||||
zh: 86e46b9552017ff6
|
||||
en: d186d3cfc9597688
|
||||
zh: 5f7094355c5f562c
|
||||
/deepseek-harness-desktop/installation-ownership/runtime-and-plugin-activation:
|
||||
en: 0d11af8639977a6d
|
||||
zh: 86b086b12d2210d3
|
||||
/deepseek-harness-desktop/develop:
|
||||
en: 9b9b362a9865e763
|
||||
zh: c748901168e2db8c
|
||||
en: 40d2ad4014bf445b
|
||||
zh: 6a08de06df9ed042
|
||||
/deepseek-harness-desktop/develop/startup-onboarding:
|
||||
en: 5e2d62207a603d81
|
||||
zh: 5fb863d4906f4a67
|
||||
en: a51346b8d880ddba
|
||||
zh: 7913214c1c66063c
|
||||
/deepseek-harness-desktop/develop/welcome-window-appearance:
|
||||
en: 7220801ccc3b2e6b
|
||||
zh: 042dc97976aef979
|
||||
en: ae319a496639b38d
|
||||
zh: ef044103240ac9da
|
||||
/deepseek-harness-desktop/package:
|
||||
en: 4cd8a8da6977928d
|
||||
zh: 9f624855a2edac37
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Desktop analytics follows the [product collection policy](../../packages/client/product-analytics/README.md), including its live application setting. Web usage is excluded. Update installation waits for its local analytics intake before locking API admission and stopping the Host. Intake has a one-second deadline; failure does not prevent installation, and collector delivery is not awaited.
|
||||
|
||||
The desktop application is an Electron shell around the complete dsh Web application. An Electron RunAsNode child starts the shared profile runner, and Electron immediately loads the packaged Web entry at `dsh-app://app/`. Its shared loading page waits for Host boot injections, then starts the client without navigating to another document. Electron forwards application HTTP requests to the authenticated Web Host, dropping connection-level response headers (`transfer-encoding`, `connection`, `keep-alive`) that describe the Node fetch rather than the resource, and marking plugin bundle responses `no-store` because their per-launch revisions would only accumulate in Chromium's disk cache; WebSocket streams connect to that Host with credentials attached only for the owned application window. Node IPC carries boot injections, readiness, and shutdown. Desktop defaults to port `19387`, separate from Web’s `3080`; a `webserver.config.port` patch can override it.
|
||||
|
||||
The first application-menu command, **About DeepSeek Harness**, opens Electron's native About panel with the application icon, product name, and installed release version. The menu follows the Desktop shell locale. On macOS, Hide, Hide Others, Show All, and Quit use localized labels; Hide and Quit include the DeepSeek Harness product name. These commands retain their native actions and shortcuts. macOS reads the icon from its application bundle, so an unpackaged development launch displays Electron's icon; Windows receives the packaged PNG.
|
||||
@@ -28,6 +30,8 @@ Windows keeps a tray icon for the whole run. Its tooltip is the product name, a
|
||||
|
||||
Every ordinary quit entry — ⌘Q, the application menu, the Dock menu, the Windows tray and caption Application menu, and the quits caused by closing the mandatory-update or welcome window — asks the Host what it would interrupt. The Host answers over the private IPC channel with two facts: active tasks under the same rule as the update restart check (running agents including subagents and approval waits, queued messages, running or stopping jobs), and armed scheduled reminders reported by the `schedule` family of `workspace/session-activity` for the sessions loaded during this run. Neither fact, the quit proceeds silently. Otherwise a native message box without an owner window — hidden windows stay hidden — shows **Quit DeepSeek Harness?** with one of three localized explanations: running tasks will be interrupted, scheduled tasks will not run while the application is closed, or both. Quit is the default button and Cancel answers Esc; macOS places Cancel left of Quit, Windows places Quit left of Cancel, and the Windows task dialog shows the application icon and stays light regardless of the application theme. A Host that has not reached ready or has failed cannot run tasks, so the quit proceeds without asking. An inspection failure or a Host that misses the two-second deadline is treated as running tasks. While the box is open, another quit request joins it instead of stacking a second one (macOS also raises it; Electron exposes no handle to the Windows task dialog), the explanation does not change when work starts or ends, Quit stops the application without re-inspecting, and Cancel changes nothing. Cancelling a quit that closing the welcome window started shows the welcome window again.
|
||||
|
||||
The mandatory update confirmation includes the installation wait notice on Windows; macOS uses the shorter restart explanation.
|
||||
|
||||
The confirmation is skipped when the installer restart already confirmed task interruption, when the fatal-recovery dialog exits or restarts, when the development Restart App and Host command runs, and during operating-system shutdown, restart, or log-off: Windows sets this on the definitive session-end message; macOS sets it on the power-off notification, and because another application can still cancel that shutdown, the next focus or show of the main window clears it. Installer-owned quit cancels any pending ordinary quit decision; late inspection results and dialog answers cannot open another confirmation or repeat cleanup. A user-initiated update download that finishes while the window is hidden defers its Install and Restart confirmation until the window is shown again; the mandatory flow keeps its taskbar and Dock attention. The Windows installer and uninstaller tell a user whose application is still running to quit it from the system tray. Desktop does not enable scheduled tasks by default, so the scheduled-task explanations appear only once that feature is on; reminders fire only in loaded sessions, and unloaded sessions are neither counted nor resumed until they are opened.
|
||||
|
||||
The tray renderer enlarges the whale by 20% around the tile center while preserving the background and aspect ratio; application and installer icons retain their original proportions.
|
||||
@@ -84,6 +88,8 @@ The `dsh-app://shell/` origin serves packaged update documents, scripts, and sty
|
||||
|
||||
The product UI retains Web actions, including "Open In..." through the shared authenticated HTTP routes. Desktop uses Web's automatic directory-picker selection and initializes new profiles with the shared Web template's bundles.
|
||||
|
||||
Update copy preserves the complete version, including prerelease suffixes, without adding v or V. When no update is available, the dialog title reports the result and the body shows the current version.
|
||||
|
||||
Electron chooses typed English or Chinese shell copy from its application locale and falls back to English. The macOS application bundle declares English and Simplified Chinese in `CFBundleLocalizations`, allowing macOS to match the initial application language to the user’s preferred languages. Saved Client UI language preferences still take precedence for the main interface. On Windows, the main document's language updates desktop menus, recovery and update prompts. The repository Client UI i18n gate checks desktop sources.
|
||||
|
||||
Windows uses a 40-DIP caption with native window controls and colors synchronized from the application palette. Localized Application and Edit entries beside the sidebar toggle open native popup menus. They mount only after the application frame publishes its shell overlay seat, and remain absent during startup loading. Application provides Check for Updates and Exit; Edit provides undo, redo, cut, copy, paste, delete, and select all by sending the corresponding keys to the focused editor, independently of custom shortcut bindings. Plugin management uses the main application's Plugins page. No separate native menu row appears on Alt. Other platforms retain their native menus. Editable fields retain keyboard commands and a context menu without shortcut labels; Chromium supplies command availability, and selected read-only text offers Copy.
|
||||
@@ -130,7 +136,7 @@ pnpm run start:desktop
|
||||
|
||||
The Web counterparts are `pnpm run dev:web` and `pnpm run start:web`, documented in the [development guide](../../docs/development.md). Workspace development runs the current CLI and private Desktop Host packages under Electron RunAsNode. Plugin management and recovery use `$DSH_HOME/profiles/desktop`, separate from the disposable workspace runtime. The Host uses runtime module resolution in both development and packaged builds without creating official-package fallback links; developer-installed packages, including links, retain native priority. Use an unpacked application to exercise Electron RunAsNode, bundled pnpm, bundled dsh resources, plugin installation and repair paths.
|
||||
|
||||
The [native/renderer keyboard tests](tests/keyboard.spec.ts) compile in [their own Client test project](../../tsconfig.desktop-keyboard-tests.json), reached by the repository Client typecheck. Its Desktop imports are limited to Cordis-free input, persistence, IPC, browser-guest, and overlay modules.
|
||||
The [native/renderer keyboard tests](tests/keyboard.spec.ts) compile as part of the repository Client typecheck. Their Desktop imports are limited to Cordis-free input, persistence, IPC, browser-guest, and overlay modules.
|
||||
|
||||
### Startup onboarding
|
||||
|
||||
@@ -138,7 +144,7 @@ The API-key input starts empty and uses `autocomplete="new-password"` to ask Chr
|
||||
|
||||
Repeated launches and `dsh://open` keep the workspace hidden until the startup credential check or a welcome action permits entry. Entering from Welcome places keyboard focus on the document without selecting a sidebar control; Tab navigation remains available.
|
||||
|
||||
Desktop checks configured model API-key references after the Host starts and before opening the workspace. With no configured key, the welcome window offers the [API-key page](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5J8/Harness?node-id=2138-44626). Save and continue writes through the existing credential service using the official DeepSeek provider's configured reference, then opens the workspace. Set up later opens the workspace without saving the draft or a completion setting; the next process launch checks credentials again. Back to sign in returns to the entry and clears the unsaved key and validation message. Buttons keep their labels and block competing actions while saving or opening the workspace. The Desktop preload marker suppresses the Web credential dialog, while retaining the Models settings page and the welcome notice.
|
||||
Desktop checks configured model API-key references after the Host starts and before opening the workspace. With no configured key, the welcome window offers the API-key page. Save and continue writes through the existing credential service using the official DeepSeek provider's configured reference, then opens the workspace. Set up later opens the workspace without saving the draft or a completion setting; the next process launch checks credentials again. Back to sign in returns to the entry and clears the unsaved key and validation message. Buttons keep their labels and block competing actions while saving or opening the workspace. The Desktop preload marker suppresses the Web credential dialog, while retaining the Models settings page and the welcome notice.
|
||||
|
||||
The welcome window reads the shared `locale.preference` before it appears. An explicit English or Chinese choice wins; otherwise Desktop picks the first supported OS language and falls back to English. The main UI reads the same preference and OS language order through its isolated preload before mounting. Language changes in Settings update the shell’s current dictionary and menu; automatic selection writes no preference. The welcome window has no language selector.
|
||||
|
||||
@@ -146,7 +152,7 @@ The browser-login waiting page offers a copy-link action for the current pending
|
||||
|
||||
### Welcome window appearance
|
||||
|
||||
The welcome window follows system appearance with the design’s Platform light/dark colors and shows the 600 × 700 [entry layout](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5J8/Harness?node-id=2121-39334) and the API-key form, with native window controls, a draggable title area, a local brand SVG, system sans-serif fallbacks, and locally bundled Montserrat Light for non-button text. The window uses macOS menu vibrancy or Windows acrylic with the onboarding window tint: 40% white in light mode and 50% rgb(24 25 28) in dark mode. The local React welcome entry bundles React and the shared `StateDot` loading indicator with its CSS; it uses the isolated preload without loading the main Web application. The entry, sign-in status and API-key pages share a fixed bottom action row; the back-to-sign-in link sits below it. Buttons share the platform motion timings, and Reduce Motion disables their transitions. The OS owns blur strength and outer corners. macOS Reduce Transparency suppresses translucency, and Increase Contrast forces that setting on. Save and continue writes to the development credential store; Set up later opens the real workspace without saving a key or completion flag. The generated project links the declared workspace dependency closure as well as pnpm’s hoisted packages, so unhoisted configured plugins remain resolvable. [The window note](../../.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.md) owns the material and onboarding decisions.
|
||||
The welcome window follows system appearance with the design’s Platform light/dark colors and shows the 600 × 700 entry layout and the API-key form, with native window controls, a draggable title area, a local brand SVG, system sans-serif fallbacks, and locally bundled Montserrat Light for non-button text. The window uses macOS menu vibrancy or Windows acrylic with the onboarding window tint: 40% white in light mode and 50% rgb(24 25 28) in dark mode. The local React welcome entry bundles React and the shared `StateDot` loading indicator with its CSS; it uses the isolated preload without loading the main Web application. The entry, sign-in status and API-key pages share a fixed bottom action row; the back-to-sign-in link sits below it. Buttons share the platform motion timings, and Reduce Motion disables their transitions. The OS owns blur strength and outer corners. macOS Reduce Transparency suppresses translucency, and Increase Contrast forces that setting on. Save and continue writes to the development credential store; Set up later opens the real workspace without saving a key or completion flag. The generated project links the declared workspace dependency closure as well as pnpm’s hoisted packages, so unhoisted configured plugins remain resolvable. [The window note](../../.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.md) owns the material and onboarding decisions.
|
||||
|
||||
## Package
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
桌面埋点遵循[产品采集策略](../../packages/client/product-analytics/README.zh.md)及其动态应用配置,不包含 Web 使用情况。安装更新会等待该操作的本地埋点接收请求结束,再锁定 API 准入并停止 Host。接收请求的时限为一秒,失败不会阻止安装,也不等待收集端完成发送。
|
||||
|
||||
桌面应用是完整 dsh Web 应用外的一层 Electron 壳。Electron RunAsNode 子进程启动共享 profile runner,Electron 立即从 `dsh-app://app/` 加载打包内的 Web 入口。共享加载页等待 Host 启动注入,然后在同一文档中启动客户端。Electron 将应用 HTTP 请求转发给已认证的 Web Host,转发时丢弃描述 Node fetch 连接而非资源本身的响应头(`transfer-encoding`、`connection`、`keep-alive`),并把插件 bundle 响应标记为 `no-store`,因为其每次启动都变化的 revision 只会在 Chromium 磁盘缓存中累积;WebSocket 流连接到该 Host,仅为归属的应用窗口附加凭据。Node IPC 承载启动注入、就绪与关闭。Desktop 默认使用端口 `19387`,与 Web 的 `3080` 分开;可通过 `webserver.config.port` patch 覆盖。
|
||||
|
||||
应用菜单第一项“**关于 DeepSeek Harness**”打开 Electron 原生关于面板,展示应用图标、产品名称和当前安装的发布版本。菜单文案跟随桌面壳的语言。macOS 的隐藏、隐藏其他、显示全部和退出条目使用本地化文案,隐藏和退出条目包含 DeepSeek Harness 产品名称。这些条目保留原生动作和快捷键。macOS 从应用包读取图标,因此未打包的开发启动会显示 Electron 图标;Windows 使用随包分发的 PNG。
|
||||
@@ -28,6 +30,8 @@ Windows 在整个运行期间常驻托盘图标。悬停提示为产品名,单
|
||||
|
||||
所有普通退出入口——⌘Q、应用菜单、Dock 菜单、Windows 托盘和标题栏"应用程序"菜单,以及关闭强制更新窗口或欢迎窗口引起的退出——都先向 Host 查询退出会中断什么。Host 通过私有 IPC 通道回答两项事实:与更新重启检查同一口径的运行中任务(运行中的 agent,包括子代理和等待审批的回合、排队消息、运行中或停止中的后台任务),以及本次运行中已加载会话里由 `workspace/session-activity` 的 `schedule` family 报告的已挂定时器的提醒。两项都没有时直接退出,不弹框。否则弹出一个没有父窗口的原生消息框——隐藏的窗口保持隐藏——标题为**退出 DeepSeek Harness?**,正文为三种本地化说明之一:正在运行的任务将会中断、应用关闭期间定时任务不会运行,或两者兼有。"退出"是默认按钮,Esc 等同"取消";macOS 上"取消"在"退出"左侧,Windows 上"退出"在"取消"左侧,Windows 任务对话框显示应用图标且不跟随应用主题、始终为浅色。Host 尚未就绪或已失败时不可能有任务在跑,直接退出。查询失败或 Host 超过两秒截止时间未答复,按运行中任务处理。弹框打开期间,再次请求退出只会并入同一弹框而不叠加新弹框(macOS 上还会把它提到前面;Electron 不暴露 Windows 任务对话框的句柄);任务开始或结束不会改变文案;点"退出"不再重新查询即停止应用;点"取消"不发生任何变化。取消由关闭欢迎窗口引起的退出时,欢迎窗口会重新显示。
|
||||
|
||||
强制更新的安装确认在 Windows 上额外显示等待提示,macOS 只显示简短的重启说明。
|
||||
|
||||
以下情况跳过确认:安装更新的重启已确认过任务中断、致命错误恢复对话框中的退出或重启、开发版"重启应用与 Host"命令,以及操作系统关机、重启或注销:Windows 在确定性的会话结束消息上设置该状态;macOS 在关机通知上设置,而其他应用仍可能取消这次关机,因此主窗口下一次获得焦点或显示时会清除它。安装器接管退出时会取消尚未结束的普通退出决策;晚到的查询结果和弹框答复不会再次打开确认框或重复清理。窗口隐藏期间完成的用户主动发起的更新下载,把"安装并重启"确认推迟到窗口再次显示时;强制更新流程沿用其任务栏和 Dock 提醒。Windows 安装程序和卸载程序在应用仍在运行时提示用户先在系统托盘中退出。Desktop 默认未开启定时任务,定时任务的说明只在该功能开启后出现;提醒只在已加载的会话中触发,未加载的会话既不计入,也要等到打开后才会继续。
|
||||
|
||||
托盘渲染器以底板中心为基准将鲸鱼放大 20%,保留背景和宽高比;应用和安装器图标保持原有比例。
|
||||
@@ -86,6 +90,8 @@ Electron 拥有 `$DSH_HOME/profiles/desktop`。其 `dependencies` 包含 pnpm
|
||||
|
||||
产品 UI 保留 Web 操作,包括通过共享认证 HTTP 路由执行的“打开方式…”。Desktop 使用 Web 的自动目录选择机制,并以共享 Web 模板的 bundle 列表初始化新 profile。
|
||||
|
||||
升级文案完整展示版本号(包括预发布后缀),不额外添加 v 或 V。无可用更新时,弹窗标题显示检查结果,正文显示当前版本。
|
||||
|
||||
Electron 根据应用语言选择类型化的英文或中文 shell 文案,并回退到英文。macOS 应用包通过 `CFBundleLocalizations` 声明支持英语和简体中文,让 macOS 根据用户的首选语言匹配初始应用语言。主界面仍优先使用已保存的 Client UI 语言偏好。在 Windows 上,主文档的语言会更新桌面菜单、恢复与更新提示。仓库 Client UI i18n 检查覆盖桌面端源码。
|
||||
|
||||
Windows 使用 40 DIP 顶栏,保留原生窗口按钮,颜色随应用调色板同步。侧栏开关旁的本地化“应用”和“编辑”入口打开原生弹出菜单。仅当应用框架发布 shell overlay 席位后才挂载菜单,启动加载期间不显示。“应用”提供检查更新和退出;“编辑”向当前编辑器发送对应按键,提供撤销、重做、剪切、复制、粘贴、删除和全选,不受自定义快捷键绑定影响。插件管理使用主应用的“插件”页面。按 Alt 不会出现额外的原生菜单行。其他平台保留原生菜单。可编辑区域保留快捷键和不带快捷键标注的右键菜单;命令可用状态由 Chromium 提供,选中的只读文本提供“复制”命令。
|
||||
@@ -132,7 +138,7 @@ pnpm run start:desktop
|
||||
|
||||
Web 侧的对应命令是 `pnpm run dev:web` 与 `pnpm run start:web`,见[开发指南](../../docs/development.zh.md)。Workspace 开发使用 Electron RunAsNode 运行当前 CLI 与私有 Desktop Host 包,插件管理和恢复使用 `$DSH_HOME/profiles/desktop`,与一次性工作区运行时分离。Host 在开发与打包构建中都使用 runtime 模块解析,不创建官方包的 fallback 链接;开发者安装的包(包括链接)保留原生优先级。需要验证 Electron RunAsNode、内置 pnpm、内置 dsh 资源、插件安装和修复时,应运行未封装安装器的应用目录。
|
||||
|
||||
[原生输入与渲染进程键盘测试](tests/keyboard.spec.ts)在[独立的 Client 测试项目](../../tsconfig.desktop-keyboard-tests.json)中编译,由仓库 Client 类型检查纳入。它只导入不依赖 Cordis 的 Desktop 输入、持久化、IPC、浏览器 guest 和蒙层模块。
|
||||
[原生输入与渲染进程键盘测试](tests/keyboard.spec.ts)直接纳入仓库 Client 类型检查。它只导入不依赖 Cordis 的 Desktop 输入、持久化、IPC、浏览器 guest 和蒙层模块。
|
||||
|
||||
### 启动引导
|
||||
|
||||
@@ -140,7 +146,7 @@ API Key 输入框初始为空,并通过 `autocomplete="new-password"` 请求 C
|
||||
|
||||
重复启动和 `dsh://open` 会保持工作区隐藏,直到启动凭据检查或欢迎页操作允许进入。从 Welcome 进入时,键盘焦点落在文档上,不选中侧边栏控件;Tab 导航仍可使用。
|
||||
|
||||
Desktop 在 Host 启动后、打开工作区前检查模型 API Key 引用是否已配置。没有已配置的密钥时,欢迎窗口提供 [API Key 页面](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5J8/Harness?node-id=2138-44626)。“保存并继续”通过现有凭证服务写入 DeepSeek 官方提供方配置的引用,然后打开工作区。“稍后配置”打开工作区,但不保存草稿或完成标记;下次进程启动时会重新检查凭证。“返回登录”回到入口并清空未保存的密钥和校验提示。保存或打开工作区期间,按钮保持原文案并禁用竞争操作。Desktop preload 标记使 Web 凭证弹窗不再显示,同时保留模型设置页和欢迎须知。
|
||||
Desktop 在 Host 启动后、打开工作区前检查模型 API Key 引用是否已配置。没有已配置的密钥时,欢迎窗口提供 API Key 页面。“保存并继续”通过现有凭证服务写入 DeepSeek 官方提供方配置的引用,然后打开工作区。“稍后配置”打开工作区,但不保存草稿或完成标记;下次进程启动时会重新检查凭证。“返回登录”回到入口并清空未保存的密钥和校验提示。保存或打开工作区期间,按钮保持原文案并禁用竞争操作。Desktop preload 标记使 Web 凭证弹窗不再显示,同时保留模型设置页和欢迎须知。
|
||||
|
||||
欢迎窗口在显示前读取共享的 `locale.preference`。用户明确选择的英文或中文优先;否则 Desktop 按系统语言顺序匹配支持的语言,并以英文兜底。主界面在挂载前通过隔离 preload 读取同一偏好和系统语言顺序。在设置中切换语言会更新桌面壳的当前词典和菜单;自动选择不会写入偏好。欢迎窗口不提供语言切换入口。
|
||||
|
||||
@@ -148,7 +154,7 @@ Desktop 在 Host 启动后、打开工作区前检查模型 API Key 引用是否
|
||||
|
||||
### 欢迎窗口外观
|
||||
|
||||
欢迎窗口使用设计稿的 Platform light/dark 颜色跟随系统外观,展示 600 × 700 的[入口布局](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5J8/Harness?node-id=2121-39334)和 API Key 表单,包含原生窗口控件、可拖动标题区域、本地品牌 SVG、系统无衬线字体回退,以及非按钮文字使用的本地 Montserrat Light 字体。窗口使用 macOS menu vibrancy 或 Windows acrylic,叠加 onboarding 的窗口背景色:浅色模式为 40% 白色,深色模式为 50% rgb(24 25 28)。本地 React 欢迎入口将 React、公共 `StateDot` 加载指示器及其 CSS 一起打包;它通过隔离 preload 工作,不加载主 Web 应用。入口、登录状态和 API Key 页面共用固定的底部操作行;“返回登录”链接位于操作行下方。按钮共用平台的过渡时序,开启“减少动态效果”会禁用过渡。操作系统控制模糊强度和外部圆角。macOS 的“降低透明度”会抑制半透明效果,“增强对比度”会强制开启该设置。“保存并继续”写入开发环境的凭证存储;“稍后配置”打开真实工作区,不保存密钥或完成标记。生成的开发项目同时链接已声明的 workspace 依赖闭包和 pnpm 提升的包,因此未提升的配置插件仍能解析。[窗口记录](../../.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.zh.md)负责材质与引导决策。
|
||||
欢迎窗口使用设计稿的 Platform light/dark 颜色跟随系统外观,展示 600 × 700 的入口布局和 API Key 表单,包含原生窗口控件、可拖动标题区域、本地品牌 SVG、系统无衬线字体回退,以及非按钮文字使用的本地 Montserrat Light 字体。窗口使用 macOS menu vibrancy 或 Windows acrylic,叠加 onboarding 的窗口背景色:浅色模式为 40% 白色,深色模式为 50% rgb(24 25 28)。本地 React 欢迎入口将 React、公共 `StateDot` 加载指示器及其 CSS 一起打包;它通过隔离 preload 工作,不加载主 Web 应用。入口、登录状态和 API Key 页面共用固定的底部操作行;“返回登录”链接位于操作行下方。按钮共用平台的过渡时序,开启“减少动态效果”会禁用过渡。操作系统控制模糊强度和外部圆角。macOS 的“降低透明度”会抑制半透明效果,“增强对比度”会强制开启该设置。“保存并继续”写入开发环境的凭证存储;“稍后配置”打开真实工作区,不保存密钥或完成标记。生成的开发项目同时链接已声明的 workspace 依赖闭包和 pnpm 提升的包,因此未提升的配置插件仍能解析。[窗口记录](../../.agents/notes/implemented/architecture/2026-09-08-desktop-welcome-window-material.zh.md)负责材质与引导决策。
|
||||
|
||||
## 打包
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-desktop",
|
||||
"description": "Electron desktop shell for a bundled dsh runtime and external plugins",
|
||||
"version": "0.1.7-rc.1",
|
||||
"version": "0.1.7-rc.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -75,6 +75,7 @@
|
||||
"vite": "^6.0.0",
|
||||
"@deepseek-ai/dsh-client-ui-theme": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-shortcuts": "workspace:*",
|
||||
"@deepseek-ai/dsh-atomic-write": "workspace:*"
|
||||
"@deepseek-ai/dsh-atomic-write": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-product-analytics": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,8 @@
|
||||
#copy { min-height: 32px; padding: 3px 0; background: transparent; color: #4d6bfe; font-size: 12px; }
|
||||
#copy-message { color: #d93025 !important; }
|
||||
#manual-copy { display: block; margin-top: 8px; }
|
||||
#progress { display: block; width: 100%; height: 4px; margin-top: 12px; border: 0; accent-color: #4d6bfe; }
|
||||
#progress { display: block; width: 100%; height: 4px; margin-top: 12px; border: 0; appearance: none; -webkit-appearance: none; background: #e6e7eb; accent-color: #0f1115; }
|
||||
|
||||
#progress::-webkit-progress-bar { background: #e6e7eb; }
|
||||
#progress::-webkit-progress-value { background: #0f1115; }
|
||||
#progress::-moz-progress-bar { background: #0f1115; }
|
||||
@@ -67,9 +67,13 @@ export interface DesktopAccountBackend {
|
||||
* @param listener - state recipient.
|
||||
* @param failed - stream failure recipient.
|
||||
* @param expired - live credential-expiry recipient.
|
||||
* @param onAnalyticsEnabledChanged - optional Desktop collection-policy recipient; disconnected streams publish false.
|
||||
* @returns stream disposer.
|
||||
*/
|
||||
watch(listener: (state: AccountView) => void, failed: () => void, expired: () => void): () => void
|
||||
watch(
|
||||
listener: (state: AccountView) => void, failed: () => void, expired: () => void,
|
||||
onAnalyticsEnabledChanged?: (enabled: boolean) => void,
|
||||
): () => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,19 +90,21 @@ export function desktopAccountBackend(origin: string, invoke: AccountInvoke, coo
|
||||
state: () => call('getState'),
|
||||
start: client => call('startSignIn', { client, callbackOrigin: new URL(origin).origin, loginSource: 'desktop' }),
|
||||
cancel: attemptId => call('cancelSignIn', { attemptId }), signOut: client => call('signOut', { client }),
|
||||
watch(listener, failed, expired) {
|
||||
watch(listener, failed, expired, onAnalyticsEnabledChanged) {
|
||||
let closed = false
|
||||
let socket: WebSocket | undefined
|
||||
let retry: ReturnType<typeof setTimeout> | undefined
|
||||
const connect = (): void => {
|
||||
const streamId = randomUUID()
|
||||
const expiryStreamId = randomUUID()
|
||||
const analyticsPolicyStreamId = randomUUID()
|
||||
void cookies().then((cookie) => {
|
||||
if (closed) return
|
||||
const url = new URL(REMOTE_STREAM_MUX_PATH, origin)
|
||||
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
socket = new WebSocket(url, { headers: { cookie, origin }, maxPayload: 65_536 })
|
||||
socket.on('open', () => {
|
||||
if (onAnalyticsEnabledChanged !== undefined) socket?.send(JSON.stringify({ type: 'open', streamId: analyticsPolicyStreamId, endpoint: 'productAnalytics/watchPolicy', payload: { args: {} } }))
|
||||
socket?.send(JSON.stringify({ type: 'open', streamId: expiryStreamId, endpoint: 'account/watchExpiry', payload: { args: {} } }))
|
||||
socket?.send(JSON.stringify({ type: 'open', streamId, endpoint: 'account/watch', payload: { args: {} } }))
|
||||
})
|
||||
@@ -106,6 +112,11 @@ export function desktopAccountBackend(origin: string, invoke: AccountInvoke, coo
|
||||
try {
|
||||
const bytes = Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data)
|
||||
const frame = parseRemoteStreamServerMessage(bytes.toString('utf8'))
|
||||
if (frame.streamId === analyticsPolicyStreamId) {
|
||||
if (frame.type === 'item' && typeof frame.value === 'boolean') onAnalyticsEnabledChanged?.(frame.value)
|
||||
else onAnalyticsEnabledChanged?.(false)
|
||||
return
|
||||
}
|
||||
if (frame.streamId === expiryStreamId && frame.type === 'item' && frame.value === 'session-expired') { expired(); return }
|
||||
if (frame.streamId !== streamId) throw new Error('desktop account: unexpected stream')
|
||||
if (frame.type === 'item') listener(accountView(frame.value))
|
||||
@@ -113,11 +124,11 @@ export function desktopAccountBackend(origin: string, invoke: AccountInvoke, coo
|
||||
} catch { socket?.close() }
|
||||
})
|
||||
socket.on('error', () => { socket?.close() })
|
||||
socket.on('close', () => { if (!closed) { failed(); retry = setTimeout(connect, 1000) } })
|
||||
}).catch(() => { if (!closed) { failed(); retry = setTimeout(connect, 1000) } })
|
||||
socket.on('close', () => { if (!closed) { onAnalyticsEnabledChanged?.(false); failed(); retry = setTimeout(connect, 1000) } })
|
||||
}).catch(() => { if (!closed) { onAnalyticsEnabledChanged?.(false); failed(); retry = setTimeout(connect, 1000) } })
|
||||
}
|
||||
connect()
|
||||
return () => { closed = true; clearTimeout(retry); socket?.close() }
|
||||
return () => { closed = true; onAnalyticsEnabledChanged?.(false); clearTimeout(retry); socket?.close() }
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ export function Welcome({ api }: { api: WelcomeApi }) {
|
||||
const [expiryNotice, setExpiryNotice] = useState(false)
|
||||
const [page, setPage] = useState<Page>('entry')
|
||||
const pageRef = useRef<Page>('entry')
|
||||
const visiblePage = useRef<Page>('entry')
|
||||
const [attempt, setAttempt] = useState<AccountView['attempt']>(null)
|
||||
const attemptRef = useRef<AccountView['attempt']>(null)
|
||||
const [starting, setStarting] = useState(false)
|
||||
@@ -69,12 +70,14 @@ export function Welcome({ api }: { api: WelcomeApi }) {
|
||||
}, [api, m.welcomeTitle])
|
||||
|
||||
useEffect(() => {
|
||||
if (page === 'entry' && visiblePage.current !== 'entry') void api.analytics?.('auth_page_view', {})
|
||||
visiblePage.current = page
|
||||
if (page === 'key') input.current?.focus()
|
||||
else if (page === 'entry' && focusEntry.current) {
|
||||
focusEntry.current = false
|
||||
keyButton.current?.focus()
|
||||
}
|
||||
}, [page])
|
||||
}, [page, api])
|
||||
|
||||
useEffect(() => {
|
||||
if (copyState !== 'copied' && copyState !== 'failed') return
|
||||
@@ -85,6 +88,7 @@ export function Welcome({ api }: { api: WelcomeApi }) {
|
||||
async function saveKey(event: FormEvent) {
|
||||
event.preventDefault()
|
||||
if (busyRef.current) return
|
||||
void api.analytics?.('api_key_save_click', {})
|
||||
const value = draft.trim()
|
||||
if (!/^[\x21-\x7e]+$/.test(value) || /^[A-Z][A-Z0-9_]*=[^=]/.test(value)
|
||||
|| ((value.startsWith('"') || value.startsWith("'") || value.charCodeAt(0) === 96) && value.at(-1) === value[0])) {
|
||||
@@ -198,15 +202,15 @@ export function Welcome({ api }: { api: WelcomeApi }) {
|
||||
<button id="auth-loading" className="primary" type="button" hidden={failed} disabled aria-label={m.welcomeAuthExchanging}>
|
||||
<StateDot state="ongoing" size={16} className="welcome-loading" />
|
||||
</button>
|
||||
<button id="auth-retry" className="primary" type="button" hidden={!failed} onClick={() => { void start() }}>{m.welcomeAuthRetry}</button>
|
||||
<button id="auth-api-key" className="secondary" type="button" hidden={!failed} onClick={() => { navigate('key') }}>{m.welcomeApiKey}</button>
|
||||
<button id="auth-retry" className="primary" type="button" hidden={!failed} onClick={() => { void api.analytics?.('auth_page_click', { button_name: 'sign_in' }); void start() }}>{m.welcomeAuthRetry}</button>
|
||||
<button id="auth-api-key" className="secondary" type="button" hidden={!failed} onClick={() => { void api.analytics?.('auth_page_click', { button_name: 'api-key' }); navigate('key') }}>{m.welcomeApiKey}</button>
|
||||
<button id="auth-cancel" className="secondary" type="button" hidden={failed}
|
||||
disabled={cancelling || phase === 'committing' || phase === 'succeeded' || (phase === 'initializing' && !attempt?.id)}
|
||||
onClick={() => { void cancel() }}>{m.welcomeAuthCancel}</button>
|
||||
</div>
|
||||
<div id="entry-actions" className="actions" hidden={page !== 'entry'}>
|
||||
<button id="sign-in" className="primary" type="button" onClick={() => { void start() }}>{m.welcomeSignIn}</button>
|
||||
<button ref={keyButton} id="api-key" className="secondary" type="button" onClick={() => { navigate('key') }}>{m.welcomeApiKey}</button>
|
||||
<button id="sign-in" className="primary" type="button" onClick={() => { void api.analytics?.('auth_page_click', { button_name: 'sign_in' }); void start() }}>{m.welcomeSignIn}</button>
|
||||
<button ref={keyButton} id="api-key" className="secondary" type="button" onClick={() => { void api.analytics?.('auth_page_click', { button_name: 'api-key' }); navigate('key') }}>{m.welcomeApiKey}</button>
|
||||
</div>
|
||||
<div id="key-actions" className="actions" hidden={page !== 'key'}>
|
||||
<button id="save-key" className="primary" type="submit" form="key-form" disabled={busy || draft.trim() === ''}>{m.welcomeKeySave}</button>
|
||||
|
||||
+65
-65
@@ -77,17 +77,17 @@ export const en = {
|
||||
updateCheckFailed: 'Could not check for updates. Please try again later.',
|
||||
updateDownloadFailed: 'Could not download the update. Please try again.',
|
||||
updateInstallFailed: 'Could not install the update. Please try again later.',
|
||||
updateCheckNetworkFailed: 'Could not check for updates. Please try again later. The connection was interrupted. Check your network and try again.',
|
||||
updateDownloadNetworkFailed: 'Could not download the update. Please try again. The connection was interrupted. Check your network and try again.',
|
||||
updateInstallNetworkFailed: 'Could not install the update. Please try again later. The connection was interrupted. Check your network and try again.',
|
||||
updateCheckNetworkFailed: 'Could not check for updates. Check your connection and try again.',
|
||||
updateDownloadNetworkFailed: 'Could not download the update. Check your connection and try again.',
|
||||
updateInstallNetworkFailed: 'Could not install the update. Check your connection and try again.',
|
||||
unknownError: 'Unknown error',
|
||||
updateCheckTitle: 'Check for Updates',
|
||||
updateCurrent: 'No updates available. Current version: V{version}',
|
||||
updateCurrentDetail: 'Current version: {version}',
|
||||
updateCurrent: 'You’re up to date!',
|
||||
updateChecking: 'Checking for updates…',
|
||||
updateDownload: 'Download update',
|
||||
updateDownloadedTitle: 'DeepSeek Harness v{version} downloaded',
|
||||
updateDownloadedDetail: 'The update package has downloaded. Select “Install and Restart” to restart the app and begin installation.',
|
||||
updateDownloadedTitleWindows: 'New version v{version} is ready',
|
||||
updateDownloadedTitle: 'Version {version} is ready to install',
|
||||
updateDownloadedDetail: 'The app will close during the update and reopen automatically when it is complete.',
|
||||
updateDownloadedDetailWindows: 'The app will close temporarily during the update and reopen automatically when it is complete.\n\nThe update may take some time. Please wait and do not launch the app again during installation.',
|
||||
updateClose: 'Close',
|
||||
updateAcknowledge: 'OK',
|
||||
@@ -97,47 +97,47 @@ export const en = {
|
||||
updateInstalling: 'Preparing to restart…',
|
||||
updateRetry: 'Retry update',
|
||||
updateActiveTasks: 'Tasks are still in progress',
|
||||
updateActiveTasksDetail: 'Restarting to update may interrupt these tasks. Continue updating?',
|
||||
updateActiveTasksDetail: 'Updating will stop the tasks in progress and restart the app. Continue?',
|
||||
updateStopTasks: 'Stop tasks and update',
|
||||
updateTasksChanged: 'New tasks started. Review the update confirmation again.',
|
||||
updateTasksChanged: 'New tasks have started. Confirm again to stop the tasks and update.',
|
||||
updateTasksUnavailable: 'Task status is unavailable. Try updating again when the workspace is ready.',
|
||||
updateStopFailed: 'Tasks could not be stopped safely. The update was not installed. Please try again later.',
|
||||
updateStopFailed: 'Could not safely stop the tasks. The update has not been installed. Please try again later.',
|
||||
updateTechnicalDetails: 'View technical details',
|
||||
updateTitle: 'DeepSeek Harness Update',
|
||||
updateAvailable: 'An update is available',
|
||||
updateDetail: 'DeepSeek Harness {version}\n\nDeepSeek Harness will restart to complete the update.',
|
||||
updateAvailable: 'New version available: {version}',
|
||||
updateDetail: 'Once the download is complete, you can install the update and restart the app.',
|
||||
installAndRestart: 'Install and Restart',
|
||||
later: 'Later',
|
||||
updateFailedTitle: 'Update Failed',
|
||||
mandatoryTitle: 'Update required',
|
||||
mandatoryDetail: 'This version is no longer supported. Update to continue. Existing tasks can keep running until you approve a restart.',
|
||||
mandatoryUnavailable: 'The update requirement could not be checked. Retry when the connection is available.',
|
||||
mandatoryTitle: 'Update to continue',
|
||||
mandatoryDetail: 'This version is no longer supported. Update to continue. Tasks in progress will keep running until you confirm installation and restart.',
|
||||
mandatoryUnavailable: 'Could not check update requirements. Please try again later.',
|
||||
policyLoginTitle: 'Sign in to the test environment',
|
||||
policyLoginRequired: 'This is a test build. Checking update requirements needs Feishu sign-in. Signing in does not download or install an update.',
|
||||
policyLoginRequired: 'Sign in with Feishu to check update requirements for this test build. Signing in will not download or install an update.',
|
||||
policyLogin: 'Sign in with Feishu',
|
||||
policyLoginFailed: 'Test environment sign-in did not complete. Please check your connection and try again.',
|
||||
policyLoginFailed: 'Feishu sign-in did not complete. Please try again.',
|
||||
policyLoginLoading: 'Loading sign-in page…',
|
||||
mandatoryNoRelease: 'No applicable update is available. Check again or contact support.',
|
||||
mandatoryNoRelease: 'No compatible update was found. Check again or contact support.',
|
||||
mandatoryRefresh: 'Check again',
|
||||
mandatoryPage: 'Open download page',
|
||||
mandatoryCopy: 'Copy download address',
|
||||
mandatoryPageFailed: 'The download page could not be opened. Copy the address below and open it in your browser.',
|
||||
mandatoryActionFailed: 'The update action failed. Retry; the update requirement remains active.',
|
||||
mandatoryPage: 'Download from the official website',
|
||||
mandatoryCopy: 'Copy download link',
|
||||
mandatoryPageFailed: 'Could not open the official download page. Copy the link and open it in your browser.',
|
||||
mandatoryActionFailed: 'The action failed. Please try again. You can use the app again after the update is complete.',
|
||||
mandatoryReady: 'Update ready',
|
||||
mandatoryVersion: 'V{version}',
|
||||
mandatoryReadyDetail: 'Installing the update will restart the application.',
|
||||
mandatoryDeferred: 'Existing tasks can keep running. Update to continue using the application.',
|
||||
mandatoryContinue: 'Continue installing update',
|
||||
mandatoryVersion: 'New version: {version}',
|
||||
mandatoryReadyDetail: 'The app will close during the update and reopen automatically when it is complete.',
|
||||
mandatoryDeferred: 'Tasks in progress will keep running. Complete the update before using the app again.',
|
||||
mandatoryContinue: 'Continue update',
|
||||
mandatoryInspecting: 'Checking tasks…',
|
||||
mandatoryStopping: 'Safely stopping tasks in the application.',
|
||||
mandatoryRestarting: 'The application will restart shortly. Please wait.',
|
||||
mandatoryStopping: 'Stopping tasks…',
|
||||
mandatoryRestarting: 'The app will restart shortly. Please wait.',
|
||||
mandatoryDownloadFailed: 'The update files could not be downloaded or prepared. Please retry.',
|
||||
mandatoryInstallFailed: 'The update was not installed. Check tasks again and retry.',
|
||||
mandatoryOpenHelp: 'If the page did not open, you can',
|
||||
mandatoryReopen: 'Open download page again',
|
||||
mandatoryInstallFailed: 'The update has not been installed. Check the tasks again and retry.',
|
||||
mandatoryOpenHelp: 'Page didn’t open?',
|
||||
mandatoryReopen: 'Open the official download page again',
|
||||
mandatoryCopied: 'Link copied',
|
||||
mandatoryCopyFailed: 'Copy failed. Select and copy the address below manually.',
|
||||
mandatoryAddress: 'Download address',
|
||||
mandatoryCopyFailed: 'Could not copy the link. Select and copy it below.',
|
||||
mandatoryAddress: 'Download link',
|
||||
mandatoryNotification: 'Return to the application to confirm installation and restart.',
|
||||
} as const
|
||||
|
||||
@@ -221,17 +221,17 @@ export const zh = {
|
||||
updateCheckFailed: '检查更新失败,请稍后重试。',
|
||||
updateDownloadFailed: '下载更新失败,请重试。',
|
||||
updateInstallFailed: '安装更新失败,请稍后重试。',
|
||||
updateCheckNetworkFailed: '检查更新失败,请稍后重试。网络连接异常,请检查网络后重试。',
|
||||
updateDownloadNetworkFailed: '下载更新失败,请重试。网络连接异常,请检查网络后重试。',
|
||||
updateInstallNetworkFailed: '安装更新失败,请稍后重试。网络连接异常,请检查网络后重试。',
|
||||
updateCheckNetworkFailed: '检查更新失败,请检查网络连接后重试。',
|
||||
updateDownloadNetworkFailed: '下载更新失败,请检查网络连接后重试。',
|
||||
updateInstallNetworkFailed: '安装更新失败,请检查网络连接后重试。',
|
||||
unknownError: '未知错误',
|
||||
updateCheckTitle: '检查更新',
|
||||
updateCurrent: '当前暂无可用更新。当前版本:V{version}',
|
||||
updateCurrentDetail: '当前版本:{version}',
|
||||
updateCurrent: '已是最新版本',
|
||||
updateChecking: '正在检查更新…',
|
||||
updateDownload: '下载更新',
|
||||
updateDownloadedTitle: 'DeepSeek Harness v{version} 下载完成',
|
||||
updateDownloadedDetail: '安装包已下载完毕,点击“安装并重启”,即刻重启客户端,开始部署。',
|
||||
updateDownloadedTitleWindows: '新版本 v{version} 已准备就绪',
|
||||
updateDownloadedTitle: '新版本 {version} 已准备就绪',
|
||||
updateDownloadedDetail: '更新期间应用将暂时关闭,完成后会自动打开。',
|
||||
updateDownloadedDetailWindows: '更新期间应用将暂时关闭,完成后会自动打开。\n\n更新可能需要一些时间,请耐心等待,期间请勿重复启动应用。',
|
||||
updateClose: '关闭',
|
||||
updateAcknowledge: '确定',
|
||||
@@ -241,47 +241,47 @@ export const zh = {
|
||||
updateInstalling: '正在准备重启…',
|
||||
updateRetry: '重试更新',
|
||||
updateActiveTasks: '仍有进行中的任务',
|
||||
updateActiveTasksDetail: '重启更新可能中断这些任务,是否要继续更新?',
|
||||
updateActiveTasksDetail: '更新将停止进行中的任务并重启应用,是否继续?',
|
||||
updateStopTasks: '停止任务并更新',
|
||||
updateTasksChanged: '有新任务开始,请重新确认更新。',
|
||||
updateTasksChanged: '有新任务开始运行,请重新确认是否停止任务并更新。',
|
||||
updateTasksUnavailable: '无法确认任务状态,请在工作区就绪后重试更新。',
|
||||
updateStopFailed: '未能安全停止任务,更新未安装。请稍后重试。',
|
||||
updateStopFailed: '未能安全停止任务,更新尚未安装,请稍后重试。',
|
||||
updateTechnicalDetails: '查看技术详情',
|
||||
updateTitle: 'DeepSeek Harness 更新',
|
||||
updateAvailable: '发现可用更新',
|
||||
updateDetail: 'DeepSeek Harness {version}\n\nDeepSeek Harness 将重启以完成更新。',
|
||||
updateAvailable: '发现新版本 {version}',
|
||||
updateDetail: '下载完成后,可安装并重启应用。',
|
||||
installAndRestart: '安装并重启',
|
||||
later: '稍后',
|
||||
updateFailedTitle: '更新失败',
|
||||
mandatoryTitle: '需要更新',
|
||||
mandatoryDetail: '当前版本已停止支持,请更新后继续使用。在您确认重启之前,现有任务可以继续运行。',
|
||||
mandatoryUnavailable: '暂时无法检查更新要求,请在网络恢复后重试。',
|
||||
mandatoryTitle: '请更新后继续使用',
|
||||
mandatoryDetail: '当前版本已停止支持,请更新后继续使用。确认安装并重启前,进行中的任务会继续运行。',
|
||||
mandatoryUnavailable: '暂时无法检查更新要求,请稍后重试。',
|
||||
policyLoginTitle: '登录测试环境',
|
||||
policyLoginRequired: '这是测试版应用,检查更新要求需要先通过飞书登录。登录不会下载或安装更新。',
|
||||
policyLoginRequired: '此测试版需要先通过飞书登录,才能检查更新要求。登录不会下载或安装更新。',
|
||||
policyLogin: '通过飞书登录',
|
||||
policyLoginFailed: '测试环境登录未完成,请检查网络后重试。',
|
||||
policyLoginFailed: '飞书登录未完成,请重试。',
|
||||
policyLoginLoading: '正在加载登录页面…',
|
||||
mandatoryNoRelease: '暂时没有可用的更新,请重新检查或联系支持人员。',
|
||||
mandatoryNoRelease: '暂未找到适用的更新,请重新检查或联系支持人员。',
|
||||
mandatoryRefresh: '重新检查',
|
||||
mandatoryPage: '前往官网下载',
|
||||
mandatoryCopy: '复制下载链接',
|
||||
mandatoryPageFailed: '无法打开浏览器,请复制下载链接后手动打开。',
|
||||
mandatoryActionFailed: '更新操作失败,请重试;应用仍需更新后才能继续使用。',
|
||||
mandatoryPageFailed: '无法打开官网下载页面,请复制链接后在浏览器中打开。',
|
||||
mandatoryActionFailed: '操作失败,请重试。完成更新后才能继续使用应用。',
|
||||
mandatoryReady: '更新已准备就绪',
|
||||
mandatoryVersion: 'V{version}',
|
||||
mandatoryReadyDetail: '安装后将重新启动应用。',
|
||||
mandatoryDeferred: '现有任务可以继续运行。完成更新后才能继续操作应用。',
|
||||
mandatoryContinue: '继续安装更新',
|
||||
mandatoryVersion: '新版本:{version}',
|
||||
mandatoryReadyDetail: '更新期间应用将暂时关闭,完成后会自动打开。',
|
||||
mandatoryDeferred: '进行中的任务会继续运行。请完成更新后再操作应用。',
|
||||
mandatoryContinue: '继续更新',
|
||||
mandatoryInspecting: '正在检查任务状态…',
|
||||
mandatoryStopping: '正在安全结束应用中的任务。',
|
||||
mandatoryStopping: '正在停止任务…',
|
||||
mandatoryRestarting: '应用即将重启,请稍候。',
|
||||
mandatoryDownloadFailed: '更新文件下载或准备失败,请重试。',
|
||||
mandatoryInstallFailed: '更新尚未安装,请重新检查任务后重试。',
|
||||
mandatoryOpenHelp: '若页面未打开,可',
|
||||
mandatoryReopen: '重新打开官网',
|
||||
mandatoryCopied: '已复制链接',
|
||||
mandatoryCopyFailed: '复制失败,请手动选择下方地址复制。',
|
||||
mandatoryAddress: '下载地址',
|
||||
mandatoryOpenHelp: '页面未打开?',
|
||||
mandatoryReopen: '重新前往官网下载',
|
||||
mandatoryCopied: '链接已复制',
|
||||
mandatoryCopyFailed: '复制失败,请手动选择并复制下方链接。',
|
||||
mandatoryAddress: '下载链接',
|
||||
mandatoryNotification: '返回应用确认安装并重启。',
|
||||
} as const satisfies DesktopMessages
|
||||
|
||||
@@ -325,7 +325,7 @@ export function formatDesktopMessage(
|
||||
/**
|
||||
* Select localized copy for an ordinary downloaded-update confirmation.
|
||||
* @param messages - Selected Desktop dictionary.
|
||||
* @param version - Prepared update version without a leading v.
|
||||
* @param version - Prepared update version, including any prerelease suffix.
|
||||
* @param platform - Operating system presenting the confirmation.
|
||||
* @returns The versioned title and installation guidance.
|
||||
*/
|
||||
@@ -335,7 +335,7 @@ export function desktopUpdateReadyConfirmation(
|
||||
platform: string,
|
||||
): { message: string; detail: string } {
|
||||
return {
|
||||
message: formatDesktopMessage(platform === 'win32' ? messages.updateDownloadedTitleWindows : messages.updateDownloadedTitle, { version }),
|
||||
message: formatDesktopMessage(messages.updateDownloadedTitle, { version }),
|
||||
detail: platform === 'win32' ? messages.updateDownloadedDetailWindows : messages.updateDownloadedDetail,
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ProductEventMap, ProductEvent } from '@deepseek-ai/dsh-client-product-analytics/types'
|
||||
import { WINDOWS_TITLEBAR_HEIGHT } from './windows-layout.ts'
|
||||
/** Electron shell: desktop project ownership, custom protocol, windows, and lifecycle. */
|
||||
|
||||
@@ -44,7 +45,7 @@ import { DesktopUpdateSchedule, resolveDesktopUpdateScheduleConfig } from './upd
|
||||
import { desktopUpdateErrorSummary, presentDesktopUpdate } from './update-presentation.ts'
|
||||
import { desktopErrorState } from './startup-error.ts'
|
||||
import { DesktopMandatoryUpdatePolicy, resolveDesktopPolicyConfig, type DesktopPolicyState } from './mandatory-update-policy.ts'
|
||||
import { desktopClientMetadata } from './client-metadata.ts'
|
||||
import { desktopClientMetadata, desktopClientVersion } from './client-metadata.ts'
|
||||
import { DesktopMandatoryUpdateWindow } from './mandatory-update-window.ts'
|
||||
import { DesktopPolicyTestAuth } from './policy-test-auth.ts'
|
||||
import { DesktopUpdateDialog, type UpdateDialogOptions } from './update-dialog.ts'
|
||||
@@ -376,6 +377,14 @@ async function main(): Promise<void> {
|
||||
const browserGuests = new DesktopBrowserGuests(() => hostUrl)
|
||||
let injections: readonly unknown[] = []
|
||||
let welcomeBackend: DesktopWelcomeBackend | undefined
|
||||
let reportedLaunch = false
|
||||
let analyticsEnabled = false
|
||||
const track = async <K extends keyof ProductEventMap>(eventName: K, attributes: ProductEventMap[K]): Promise<void> => {
|
||||
const event = { eventName, attributes, timestamp: Date.now() } as ProductEvent
|
||||
try {
|
||||
if (analyticsEnabled) await welcomeBackend?.report(event)
|
||||
} catch (_error) { /* Analytics cannot interrupt native actions. */ }
|
||||
}
|
||||
let stopAccount: (() => void) | undefined
|
||||
let openedAttempt: string | undefined
|
||||
let returnedAttempt: string | undefined
|
||||
@@ -408,7 +417,7 @@ async function main(): Promise<void> {
|
||||
const backend = new DesktopBackendController((onFailure) => {
|
||||
const hostInspectPort = developmentHostInspectPort(development)
|
||||
const host = new DesktopHostProcess(resources.node, resources.dsh, activeProject,
|
||||
hostInspectPort, process.env, onFailure,
|
||||
hostInspectPort, { ...process.env, DSH_CLIENT_VERSION: desktopClientVersion() }, onFailure,
|
||||
primaryRuntime,
|
||||
resources, (next) => { platformView.setSession(next) })
|
||||
return {
|
||||
@@ -419,6 +428,8 @@ async function main(): Promise<void> {
|
||||
if (ready.injections === undefined) throw new Error('Desktop Host did not provide boot injections')
|
||||
injections = ready.injections
|
||||
welcomeBackend = await connectDesktopWelcome(ready.url, (input, init) => net.fetch(input, init), async () => (await session.defaultSession.cookies.get({ url: ready.url })).map(cookie => `${cookie.name}=${cookie.value}`).join('; '))
|
||||
analyticsEnabled = await welcomeBackend.analyticsEnabled().catch(() => false)
|
||||
if (!reportedLaunch) { reportedLaunch = true; void track('desktop_app_launch', {}) }
|
||||
stopAccount?.()
|
||||
const accountBackend = welcomeBackend.account
|
||||
stopAccount = accountBackend.watch((state) => {
|
||||
@@ -456,9 +467,11 @@ async function main(): Promise<void> {
|
||||
const state = await accountBackend.state()
|
||||
if (welcomeWindow !== undefined && !welcomeWindow.isDestroyed()) welcomeWindow.webContents.send(WELCOME_IPC.state, state)
|
||||
}).catch(() => undefined)
|
||||
})
|
||||
}, (enabled) => { analyticsEnabled = enabled })
|
||||
},
|
||||
stop: async () => {
|
||||
analyticsEnabled = false
|
||||
stopAccount?.()
|
||||
try { await host.stop(requireCleanStop) }
|
||||
catch (error) {
|
||||
if (!requireCleanStop || !(error instanceof DesktopHostUncleanExitError)) throw error
|
||||
@@ -567,6 +580,8 @@ async function main(): Promise<void> {
|
||||
const result = await updateDialog.show(parent, confirmation)
|
||||
if (result.response !== 0 || isMandatory()) return false
|
||||
}
|
||||
// The update lock rejects new HTTP requests, including analytics intake.
|
||||
await track('desktop_upgrade_install_restart_click', {})
|
||||
if (backend.host !== host) throw new DesktopUpdatePreparationError('tasks-unavailable', locale.messages.updateTasksUnavailable)
|
||||
try {
|
||||
const stillActive = await host.updateTasks('lock')
|
||||
@@ -591,11 +606,14 @@ async function main(): Promise<void> {
|
||||
}
|
||||
return true
|
||||
},
|
||||
undefined, undefined, undefined,
|
||||
(success, reason) => { void track('desktop_upgrade_download_result', { is_success: success, ...reason === undefined ? {} : { error_reason: reason } }) },
|
||||
)
|
||||
|
||||
const updateSchedule = new DesktopUpdateSchedule(updates, resolveDesktopUpdateScheduleConfig(process.env))
|
||||
|
||||
const downloadUpdate = async (version: string): Promise<DesktopUpdateState> => {
|
||||
void track('desktop_upgrade_click', {})
|
||||
updateJournal?.action('download-requested')
|
||||
const state = await updates.download(version)
|
||||
if (state.phase !== 'ready' || quitting) return state
|
||||
@@ -778,7 +796,8 @@ async function main(): Promise<void> {
|
||||
if (state.phase === 'error' && state.failedOperation === 'check') { await showUpdateFailure(state); return }
|
||||
if (state.phase === 'idle') {
|
||||
await ordinaryMessageBox({ type: 'info', title: locale.messages.updateCheckTitle,
|
||||
message: formatDesktopMessage(locale.messages.updateCurrent, { version: app.getVersion() }) })
|
||||
message: locale.messages.updateCurrent,
|
||||
detail: formatDesktopMessage(locale.messages.updateCurrentDetail, { version: app.getVersion() }) })
|
||||
return
|
||||
}
|
||||
if (state.phase === 'ready' || (state.phase === 'error' && state.failedOperation === 'install')) {
|
||||
@@ -790,8 +809,9 @@ async function main(): Promise<void> {
|
||||
}
|
||||
if (state.phase !== 'available' && !(state.phase === 'error' && state.failedOperation === 'download')) return
|
||||
if (manual) {
|
||||
const result = await ordinaryMessageBox({ title: locale.messages.updateCheckTitle, message: locale.messages.updateAvailable,
|
||||
detail: formatDesktopMessage(locale.messages.updateDetail, { version: state.version ?? '' }),
|
||||
const result = await ordinaryMessageBox({ title: locale.messages.updateCheckTitle,
|
||||
message: formatDesktopMessage(locale.messages.updateAvailable, { version: state.version ?? '' }),
|
||||
detail: locale.messages.updateDetail,
|
||||
buttons: [locale.messages.updateDownload], cancelId: 1 })
|
||||
if (result.response !== 0) return
|
||||
}
|
||||
@@ -1083,12 +1103,15 @@ async function main(): Promise<void> {
|
||||
const showWelcome = (): Promise<void> => {
|
||||
if (quitting) return Promise.resolve()
|
||||
if (welcomeWindow !== undefined && !welcomeWindow.isDestroyed()) {
|
||||
if (!welcomeWindow.isVisible()) void track('auth_page_view', {})
|
||||
welcomeWindow.show()
|
||||
welcomeWindow.focus()
|
||||
return Promise.resolve()
|
||||
}
|
||||
openingWelcome ??= (async () => {
|
||||
welcomeWindow = await openWelcomeWindow(locale, {
|
||||
analytics: track,
|
||||
analyticsEnabled: () => Promise.resolve(analyticsEnabled),
|
||||
takeNotice: () => {
|
||||
const notice = pendingWelcomeNotice
|
||||
pendingWelcomeNotice = undefined
|
||||
|
||||
@@ -243,7 +243,11 @@ export class DesktopMandatoryUpdateWindow {
|
||||
}
|
||||
|
||||
private view(): MandatoryUpdateView {
|
||||
return { locale: this.options.locale, policy: this.options.policy(), update: this.options.update(),
|
||||
const locale = process.platform === 'win32'
|
||||
? { ...this.options.locale, messages: { ...this.options.locale.messages,
|
||||
mandatoryReadyDetail: this.options.locale.messages.updateDownloadedDetailWindows } }
|
||||
: this.options.locale
|
||||
return { locale, policy: this.options.policy(), update: this.options.update(),
|
||||
deferred: this.deferred,
|
||||
...(this.confirmation === undefined ? {}
|
||||
: { confirmation: { version: this.confirmation.version, active: this.confirmation.active, revision: this.confirmation.revision } }),
|
||||
|
||||
@@ -10,6 +10,10 @@ const locale = process.argv.find(argument => argument.startsWith(prefix))?.slice
|
||||
if (locale === undefined) throw new Error('desktop welcome: missing window locale')
|
||||
const api: WelcomeApi = {
|
||||
...resolveDesktopLocale(locale),
|
||||
analytics: async (eventName, attributes) => {
|
||||
if (await api.analyticsEnabled()) await ipcRenderer.invoke(WELCOME_IPC.analytics, eventName, attributes)
|
||||
},
|
||||
analyticsEnabled: () => ipcRenderer.invoke(WELCOME_IPC.analyticsEnabled) as Promise<boolean>,
|
||||
takeNotice: () => ipcRenderer.invoke(WELCOME_IPC.takeNotice) as Promise<WelcomeNotice | undefined>,
|
||||
startSignIn: () => ipcRenderer.invoke(WELCOME_IPC.start) as Promise<AccountView>,
|
||||
cancelSignIn: (id: SignInAttemptId) => ipcRenderer.invoke(WELCOME_IPC.cancel, id) as Promise<AccountView>,
|
||||
|
||||
@@ -45,6 +45,7 @@ export class DesktopUpdateCoordinator {
|
||||
* @param updater - Process-owned Electron updater, replaceable at the network/platform test boundary.
|
||||
* @param enabled - Whether this process has a packaged update source.
|
||||
* @param currentVersion - Actual installed application version.
|
||||
* @param downloadResult - Once per completed download attempt, including platform preparation failures.
|
||||
*/
|
||||
constructor(
|
||||
private readonly publish: (state: DesktopUpdateState) => DesktopUpdateState,
|
||||
@@ -52,6 +53,7 @@ export class DesktopUpdateCoordinator {
|
||||
private readonly updater: AppUpdater = autoUpdater,
|
||||
private readonly enabled: () => boolean = () => app.isPackaged && existsSync(join(process.resourcesPath, 'app-update.yml')),
|
||||
private readonly currentVersion: () => string = () => app.getVersion(),
|
||||
private readonly downloadResult?: (success: boolean, reason?: string) => void,
|
||||
) {
|
||||
if (updater === autoUpdater) {
|
||||
// electron-updater omits this internal transport property from its public declarations.
|
||||
@@ -108,9 +110,11 @@ export class DesktopUpdateCoordinator {
|
||||
try {
|
||||
await this.updater.downloadUpdate()
|
||||
if (!this.downloaded) throw new Error('desktop update: platform preparation did not report readiness')
|
||||
this.downloadResult?.(true)
|
||||
return this.setState({ phase: 'ready', version })
|
||||
} catch (error) {
|
||||
this.downloaded = false
|
||||
this.downloadResult?.(false, error instanceof DesktopUpdatePreparationError ? error.kind : 'download_failed')
|
||||
return this.setState(this.failure(error, 'download'))
|
||||
}
|
||||
}).finally(() => { this.downloadOperation = undefined })
|
||||
|
||||
Loaded 100 of 1284 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user