diff --git a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml index 511aa0f0c3..68e6475a11 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md -2026-06-20-generic-long-running-tool-runtime.md: 457eaac7de92ea37287803349672271148d59185 -2026-06-20-generic-long-running-tool-runtime.zh.md: 9a86c4cf2b49973212adfd4b9acdb54150af01d1 +2026-06-20-generic-long-running-tool-runtime.md: c34d0708ee88b94fcf9b9294fe002a9c3e081cfd +2026-06-20-generic-long-running-tool-runtime.zh.md: 311fd271ccbd139f7d82270eb56e2471a34a4bb3 diff --git a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md index 457eaac7de..c34d0708ee 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md +++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md @@ -77,7 +77,7 @@ A producer loaded without any controller would let callers start work they canno Stream reads share one task-scoped consuming cursor because the owning model is the intended reader. A UI or multiple independent readers need a separate non-consuming observation API; sharing this cursor would let readers consume one another's output. -The system prompt tells the model to retain task ids, continue independent work instead of busy-polling or duplicating a running task, collect relevant tasks before its final answer, and kill work that no longer matters. Completion injects a logged `context/message` into the exact owner's session; it becomes durable context for the next request but does not wake an idle agent. +The system prompt tells the model to retain task ids, continue independent work instead of busy-polling or duplicating a running task, collect relevant tasks before its final answer, and kill work that no longer matters. Completion delivers a logged message to the exact owner's session. A busy owner is injected; an idle owner is woken, under the bounded policy the [idle-owner wake decision](../feature/2026-08-11-background-task-completion-wakes-an-idle-owner.md) owns. The runtime marks a terminal task `reported` when a read or wait delivers it, when a live waiter has claimed delivery at settlement, or when the model explicitly kills it. Reported tasks do not inject redundant completion notices. Listener failures are logged independently, do not stop later listeners, and are not awaited by waiters or teardown. When a snapshot carries `outputLimitBytes`, `dsh-tool-tasks` preserves UTF-8 boundaries and reuses an existing producer truncation marker rather than duplicating it. Reads reserve status suffixes and retain the output tail; completion notices reserve the stable `background task ` prefix and `task_output` instruction before truncating variable kind, label, status, detail, or the truncation marker itself, so the minimum PTY cap still identifies the task to collect. The task controller resolves the caller-visible producer cap in a prepended pre-execute listener before policy can deny or short-circuit dispatch, then applies it through the task definitions' last-mile `finalizeContent` callback so normalized tool errors, outer pipeline failures, and single-text policy results cannot escape the bound; deliberately structured multi-block policy results retain policy ownership of their shape and size. @@ -131,4 +131,4 @@ Unit coverage pins preflight atomicity, per-kind ids, output-limit validation an Bash commands and subagents share one id vocabulary, listing, notice format, prompt habit, and set of control tools. New long-running producers implement execution hooks instead of another registry and tool family. The [tool cookbook](../../../../docs/cookbook/adding-a-tool.md) points producers to this contract. -Owned background bash now stops with its agent instead of surviving it. Background processes have no executor timeout; callers must kill irrelevant work or rely on owner/service disposal. Stream reads support one consuming reader, completion notices do not wake idle agents, and a producer that returns from `cancel` without settling `done` can still stall teardown. Durable jobs, independent observation cursors, and foreground promotion remain separate designs. +Owned background bash now stops with its agent instead of surviving it. Background processes have no executor timeout; callers must kill irrelevant work or rely on owner/service disposal. Stream reads support one consuming reader, and a producer that returns from `cancel` without settling `done` can still stall teardown. Durable jobs, independent observation cursors, and foreground promotion remain separate designs. diff --git a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md index 9a86c4cf2b..311fd271cc 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md @@ -77,7 +77,7 @@ task id 在运行时全局可见且可预测,因此注册表会授权每次访 流式读取共享一个任务作用域内的消费游标,因为所属模型是预期读取方。UI 或多个独立读取方需要单独的非消费式观察 API;共享该游标会让读取方彼此消费对方的输出。 -系统提示词要求模型保留 task id、在后台工作运行时继续处理独立工作而非忙轮询或重复启动同一任务、在给出最终答案前收集相关任务,并终止不再重要的工作。完成时,系统会向确切所有者的会话注入一条已记录的 `context/message`;它会成为下一个请求的持久上下文,但不会唤醒空闲的 agent。 +系统提示词要求模型保留 task id、在后台工作运行时继续处理独立工作而非忙轮询或重复启动同一任务、在给出最终答案前收集相关任务,并终止不再重要的工作。完成时,系统会向确切所有者的会话交付一条已记录的消息:繁忙的所有者走注入,空闲的所有者会被唤醒,其有界策略由[空闲所有者唤醒决策](../feature/2026-08-11-background-task-completion-wakes-an-idle-owner.md)负责。 当读取或等待交付终止任务、实时等待方在结算时认领了投递,或模型显式终止任务时,运行时将终止任务标为 `reported`。已报告的任务不会注入冗余的完成通知。监听器失败会独立记录,不会阻止后续监听器,也不会被等待方或资源销毁过程等待。当快照携带 `outputLimitBytes` 时,`dsh-tool-tasks` 会保持 UTF-8 边界,并复用生产方已有的截断标记,而不会重复添加。读取会为状态后缀预留空间并保留输出尾部;完成通知会先为稳定的 `background task ` 前缀与 `task_output` 指令预留空间,再截断可变的 kind、label、status、detail,乃至截断标记本身,因此 PTY 的最小上限仍能标识需要收集的任务。任务控制器在策略有机会拒绝或短路分发之前,于最先执行的 pre-execute 监听器中解析调用方可见的生产方上限;随后通过任务定义最后一道的 `finalizeContent` 回调应用该上限,使规范化的工具错误、外层流水线失败与单文本策略结果都无法绕过该边界;经特意结构化的多块策略结果仍由策略拥有其形状与大小。 @@ -131,4 +131,4 @@ bash seam 暴露 `resolve`、`run` 和 `start`。`start(spec)` 返回一个 `Bas bash 命令与 subagent 共享一套 id 词汇、列表、通知格式、提示词习惯和控制工具。新的长时间运行生产方只需实现执行钩子,而不必再实现一套注册表与工具族。[工具实操手册](../../../../docs/cookbook/adding-a-tool.md)将生产方指向本约定。 -有所属后台 bash 会随其 agent 一起停止,不再比 agent 存活更久。后台进程没有执行器超时;调用方必须终止无关工作,或依赖所有者/服务释放。流式读取只支持一个消费方,完成通知不会唤醒空闲 agent;生产方的 `cancel` 返回后如果未使 `done` 完成,仍可能阻塞资源销毁。持久任务、独立观察游标和前台提升仍属于单独设计。 +有所属后台 bash 会随其 agent 一起停止,不再比 agent 存活更久。后台进程没有执行器超时;调用方必须终止无关工作,或依赖所有者/服务释放。流式读取只支持一个消费方;生产方的 `cancel` 返回后如果未使 `done` 完成,仍可能阻塞资源销毁。持久任务、独立观察游标和前台提升仍属于单独设计。 diff --git a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml index 9e32647a60..49df91f1fd 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md -2026-06-21-mandatory-app-attribution-headers.md: 39050a53ec76e8c5a6cac4d8e31fa15b992c406e -2026-06-21-mandatory-app-attribution-headers.zh.md: bfd6aa2540022f68cf9f69ccc2c12b3bc0978960 +2026-06-21-mandatory-app-attribution-headers.md: c90b136a556da81e1c15828926f18b865cea4ece +2026-06-21-mandatory-app-attribution-headers.zh.md: f68cad7e9f642319dc508de9261e33180bfe5748 diff --git a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md index 39050a53ec..c90b136a55 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md +++ b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md @@ -24,7 +24,7 @@ The immediate prompt came from OpenRouter's [App Attribution](https://openrouter ## Decision -Provider request attribution is mandatory at the LLM adapter boundary, using the standard `User-Agent` header only. The rule: every product LLM adapter sends a static, non-secret application identity on every provider HTTP request, and every adapter has tests proving that `User-Agent` reaches the wire (a mock server asserting received headers; for a library-backed adapter, the library's header hook feeding the same mock-server assertion). +Provider-neutral app attribution is mandatory at the LLM adapter boundary, using the standard `User-Agent` header only. The rule: every product LLM adapter sends a static, non-secret application identity on every provider HTTP request, and every adapter has tests proving that `User-Agent` reaches the wire (a mock server asserting received headers; for a library-backed adapter, the library's header hook feeding the same mock-server assertion). This rule governs app attribution, not provider-specific request identity: [the DeepSeek request-identity decision](../feature/2026-08-11-deepseek-request-user-id-header.md) separately owns its user and session headers. OpenRouter app attribution is deliberately not implemented. `HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`, and `X-OpenRouter-Categories` are OpenRouter-specific product-surface headers, not provider-neutral model-request attribution. They can be proposed later by an OpenRouter adapter or explicit OpenRouter mode, with its own privacy/product decision, tests, and docs. Until then, even requests pointed at OpenRouter send only the shared `User-Agent` attribution from this decision. @@ -41,7 +41,7 @@ Wire mapping (`attributionHeaders`; header names lowercase in code - HTTP field | Target | Mapping | |---|---| | All HTTP-based adapters | `User-Agent: {product}/{version} (+{url})` - the parenthesized `+url` comment stays within RFC 9110's conservative product/comment syntax. | -| Direct DeepSeek endpoint | `User-Agent`; do not send OpenRouter-only headers unless DeepSeek documents an equivalent contract. | +| Direct DeepSeek endpoint | `User-Agent` for app attribution; `x-deepseek-harness-user-id` and conditional `x-deepseek-harness-session-id` are separate request identity under the DeepSeek-specific decision. Do not send OpenRouter-only headers unless DeepSeek documents an equivalent contract. | | OpenRouter endpoints | `User-Agent` only for now. Do not send `HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`, or `X-OpenRouter-Categories` under this decision. | | Future providers | `User-Agent` only unless a later provider-specific Agent Note accepts additional headers. Do not reuse `HTTP-Referer` by analogy. | diff --git a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md index bfd6aa2540..f68cad7e9f 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md @@ -24,7 +24,7 @@ LLM(大语言模型)提供方请求应当标识发出请求的产品。这 ## 决策 -在 LLM 适配器边界,提供方请求归属是强制的,且仅使用标准 `User-Agent` 头部。规则:每个产品级 LLM 适配器在每个提供方 HTTP 请求上发送一个静态、非机密的应用身份,且每个适配器都有测试证明 `User-Agent` 到达了线路(mock 服务器断言收到的头部;对于基于库的适配器,通过库的头部钩子馈入同一个 mock 服务器断言)。 +在 LLM 适配器边界,提供方无关的应用归属是强制的,且仅使用标准 `User-Agent` 头部。规则:每个产品级 LLM 适配器在每个提供方 HTTP 请求上发送一个静态、非机密的应用身份,且每个适配器都有测试证明 `User-Agent` 到达了线路(mock 服务器断言收到的头部;对于基于库的适配器,通过库的头部钩子馈入同一个 mock 服务器断言)。这条规则约束应用归属,不约束提供方特有的请求身份;[DeepSeek 请求身份决策](../feature/2026-08-11-deepseek-request-user-id-header.md)另行负责其用户与会话头部。 OpenRouter 应用归属刻意未实现。`HTTP-Referer`、`X-OpenRouter-Title`、`X-Title` 和 `X-OpenRouter-Categories` 是 OpenRouter 特有的产品展示头部,不是提供方无关的模型请求归属。它们可以后续由 OpenRouter 适配器或显式 OpenRouter 模式提出,附带自己的隐私/产品决策、测试和文档。在此之前,即使请求指向 OpenRouter,也只发送本决策定义的共享 `User-Agent` 归属。 @@ -41,7 +41,7 @@ OpenRouter 应用归属刻意未实现。`HTTP-Referer`、`X-OpenRouter-Title` | 目标 | 映射 | |---|---| | 所有基于 HTTP 的适配器 | `User-Agent: {product}/{version} (+{url})`——括号中的 `+url` 注释符合 RFC 9110 保守的 product/comment 语法。 | -| 直连 DeepSeek 端点 | `User-Agent`;除非 DeepSeek 文档化了等效约定,否则不发送 OpenRouter 特有头部。 | +| 直连 DeepSeek 端点 | `User-Agent` 用于应用归属;`x-deepseek-harness-user-id` 与条件性的 `x-deepseek-harness-session-id` 由 DeepSeek 特有决策作为独立请求身份管理。除非 DeepSeek 文档化了等效约定,否则不发送 OpenRouter 特有头部。 | | OpenRouter 端点 | 目前仅 `User-Agent`。本决策下不发送 `HTTP-Referer`、`X-OpenRouter-Title`、`X-Title` 或 `X-OpenRouter-Categories`。 | | 未来提供方 | 仅 `User-Agent`,除非后续提供方特有的 Agent Note 接受额外头部。不要类比复用 `HTTP-Referer`。 | diff --git a/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.i18n.yaml index 418853a3d7..0372b716d1 100644 --- a/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.md -2026-08-07-shared-feedback-telemetry-user-id.md: 5ef487646c94808177e1263b94b8c23a9a045d97 -2026-08-07-shared-feedback-telemetry-user-id.zh.md: 1ee433396f83a8caf5e35c9cfd1b4f3c04d6c59f +2026-08-07-shared-feedback-telemetry-user-id.md: 9e4bd02d78b11858163c36376dbb438fca8aef73 +2026-08-07-shared-feedback-telemetry-user-id.zh.md: ecc451718812eb0a8b7e50bc949137fe8571ea09 diff --git a/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.md b/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.md index 5ef487646c..9e4bd02d78 100644 --- a/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.md +++ b/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.md @@ -1,4 +1,4 @@ -# Agent Note: Shared feedback and telemetry anonymous user id +# Agent Note: Shared anonymous user id across telemetry, feedback, and DeepSeek requests Status: implemented @@ -8,11 +8,11 @@ English | [中文](2026-08-07-shared-feedback-telemetry-user-id.zh.md) The OpenTelemetry backend already persisted one anonymous UUID in `$DSH_HOME/.userid`. `/feedback` now needs to report both the receiving session id and a user id so an operator can correlate the acknowledgement with exported records. Duplicating or independently generating that identity would make the reported user meaningless, while importing it from `session-telemetry-otel` would make a direct command depend on an exporter backend and create a dependency cycle when feedback export is mounted by telemetry. -The earlier [anonymous-user-id decision](../feature/2026-07-31-telemetry-anonymous-user-id.md) deliberately kept the helper inside the OTel backend until a second real consumer existed. Feedback is that consumer. +The earlier [anonymous-user-id decision](../feature/2026-07-31-telemetry-anonymous-user-id.md) deliberately kept the helper inside the OTel backend until a second real consumer existed. Feedback became that second consumer. [Direct DeepSeek request identity](../feature/2026-08-11-deepseek-request-user-id-header.md) is the third. ## Decision -`@deepseek-ai/dsh-user-id` owns `getOrCreateAnonymousUserId()` and the `$DSH_HOME/.userid` storage contract. `session-telemetry-otel` uses the returned id as OpenTelemetry Resource `user.id`; the `/feedback` success acknowledgement reports `Feedback recorded for session {sessionId}` followed by `User: {userId}` on a second line, which keeps both identifiers available through the generic command row's expandable body. Invalid feedback is rejected before resolving the id, so an empty command does not create `.userid`. +`@deepseek-ai/dsh-user-id` owns `getOrCreateAnonymousUserId()` and the `$DSH_HOME/.userid` storage contract. `session-telemetry-otel` uses the returned id as OpenTelemetry Resource `user.id`; the `/feedback` success acknowledgement reports `Feedback recorded for session {sessionId}` followed by `User: {userId}` on a second line; and direct DeepSeek requests carry it as `x-deepseek-harness-user-id`. Invalid feedback is rejected before resolving the id, and the DeepSeek adapter resolves it only after credentials succeed, so neither an empty command nor a credential failure creates `.userid`. The extraction preserves the existing random UUID, home resolution, process memo, exclusive-create concurrency, corruption replacement, and best-effort write semantics. @@ -26,7 +26,7 @@ The extraction preserves the existing random UUID, home resolution, process memo ## Consequences -- One harness home now has one anonymous id shared by feedback acknowledgements and session telemetry exports. +- One harness home has one anonymous id shared by feedback acknowledgements, session telemetry exports, and direct DeepSeek requests. - The feedback package depends only on the identity capability, not the telemetry seam or OTel SDK. -- The new package is a justified shared seam with two consumers; its empty invariant companion explains why reading the private file is not a useful runtime relationship check. +- The package is a justified shared library with three consumers; its empty invariant companion explains why reading the private file is not a useful runtime relationship check. - The original anonymous-user-id Note remains authoritative for storage and privacy semantics, while this Note supersedes only its OTel-local ownership decision. diff --git a/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.zh.md b/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.zh.md index 1ee433396f..ecc4517188 100644 --- a/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.zh.md @@ -1,4 +1,4 @@ -# Agent Note: 反馈与遥测共享匿名用户 id +# Agent Note: 遥测、反馈与 DeepSeek 请求共享匿名用户 id Status: implemented @@ -8,11 +8,11 @@ Status: implemented OpenTelemetry 后端已在 `$DSH_HOME/.userid` 中持久化一个匿名 UUID。`/feedback` 需要同时报告接收反馈的会话 id 与用户 id,以便运维人员将确认文本与导出的记录相关联。复制该身份或单独生成身份会使报告的用户失去意义;从 `session-telemetry-otel` 导入身份则会让直接命令依赖导出后端,并在遥测侧挂载反馈导出时形成依赖环。 -早先的[匿名用户 id 决策](../feature/2026-07-31-telemetry-anonymous-user-id.md)刻意将辅助函数留在 OTel 后端内,直至出现第二个真实消费方。反馈就是这个消费方。 +早先的[匿名用户 id 决策](../feature/2026-07-31-telemetry-anonymous-user-id.md)刻意将辅助函数留在 OTel 后端内,直至出现第二个真实消费方。反馈成为第二个消费方,[直连 DeepSeek 请求身份](../feature/2026-08-11-deepseek-request-user-id-header.md)则是第三个。 ## 决策 -`@deepseek-ai/dsh-user-id` 负责 `getOrCreateAnonymousUserId()` 和 `$DSH_HOME/.userid` 存储契约。`session-telemetry-otel` 将返回的 id 用作 OpenTelemetry Resource 的 `user.id`;`/feedback` 的成功确认先报告 `Feedback recorded for session {sessionId}`,再在第二行显示 `User: {userId}`,使两个标识符都可通过通用命令行的可展开正文查看。系统在获取 id 前拒绝无效反馈,因此空命令不会创建 `.userid`。 +`@deepseek-ai/dsh-user-id` 负责 `getOrCreateAnonymousUserId()` 和 `$DSH_HOME/.userid` 存储契约。`session-telemetry-otel` 将返回的 id 用作 OpenTelemetry Resource 的 `user.id`;`/feedback` 的成功确认先报告 `Feedback recorded for session {sessionId}`,再在第二行显示 `User: {userId}`;直连 DeepSeek 请求则通过 `x-deepseek-harness-user-id` 携带它。系统在获取 id 前拒绝无效反馈,DeepSeek 适配器也仅在凭据解析成功后获取 id,因此空命令和凭据失败都不会创建 `.userid`。 此次抽取保留既有的随机 UUID、home 解析、进程内缓存、独占创建并发、损坏文件替换与 best-effort 写入语义。 @@ -26,7 +26,7 @@ OpenTelemetry 后端已在 `$DSH_HOME/.userid` 中持久化一个匿名 UUID。` ## 后果 -- 一个 harness home 只有一个匿名 id,由反馈确认与会话遥测导出共享。 +- 一个 harness home 只有一个匿名 id,由反馈确认、会话遥测导出与直连 DeepSeek 请求共享。 - 反馈包只依赖身份能力,不依赖遥测 seam 或 OTel SDK。 -- 新包由两个消费方使用,成为有充分依据的共享 seam;其空不变式伴生插件解释了为何读取私有文件并非有用的运行时关系检查。 +- 该包由三个消费方使用,成为有充分依据的共享库;其空不变式伴生插件解释了为何读取私有文件并非有用的运行时关系检查。 - 原始匿名用户 id Note 仍是存储与隐私语义的权威记录;本 Note 仅取代其中由 OTel 本地拥有身份的决策。 diff --git a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.i18n.yaml index a00ad7057e..ed6abe743a 100644 --- a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md -2026-07-31-telemetry-anonymous-user-id.md: 4d5d14cb63ebbf9d9f71c729da998ad9804e56bf -2026-07-31-telemetry-anonymous-user-id.zh.md: 69c48076ceb97a4bbc3cc35a2aaa3e632b1b1b06 +2026-07-31-telemetry-anonymous-user-id.md: 6231065126d0884abe3ba6f45a40c42365fe520b +2026-07-31-telemetry-anonymous-user-id.zh.md: 4f6bab8df5d479d2df42194581a4229c6de221e6 diff --git a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md index 4d5d14cb63..6231065126 100644 --- a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md +++ b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.md @@ -10,7 +10,7 @@ Session telemetry is mounted by default ([default-mount Note](2026-07-31-web-tel ## Decision -`getOrCreateAnonymousUserId()` returns the bare UUID line in `$DSH_HOME/.userid` (resolved by `resolveDshHome`, `$DSH_HOME` > `~/.dsh`), minting and persisting a random UUID v4 on first use; the backend constructor carries it as the Resource's `user.id` (the OTel semconv user attribute), once per export batch. The original implementation lived inside `session-telemetry-otel` because no second real consumer existed. `/feedback` later became that consumer, so [the shared-id decision](../architecture/2026-08-07-shared-feedback-telemetry-user-id.md) moves ownership to `@deepseek-ai/dsh-user-id` without changing the storage, anonymity, concurrency, or loss semantics recorded here. +`getOrCreateAnonymousUserId()` returns the bare UUID line in `$DSH_HOME/.userid` (resolved by `resolveDshHome`, `$DSH_HOME` > `~/.dsh`), minting and persisting a random UUID v4 on first use; the backend constructor carries it as the Resource's `user.id` (the OTel semconv user attribute), once per export batch. The original implementation lived inside `session-telemetry-otel` because no second real consumer existed. `/feedback` later became that consumer, so [the shared-id decision](../architecture/2026-08-07-shared-feedback-telemetry-user-id.md) moves ownership to `@deepseek-ai/dsh-user-id` without changing the storage, anonymity, concurrency, or loss semantics recorded here. [Direct DeepSeek request identity](2026-08-11-deepseek-request-user-id-header.md) is a third consumer of the same id. | Ruling | Value | Rationale | |---|---|---| @@ -22,8 +22,8 @@ Session telemetry is mounted by default ([default-mount Note](2026-07-31-web-tel | Write failure | Best-effort: return the in-memory id | Telemetry is never blocked by a read-only home | | Report position | Resource attribute, not per-record attributes | Once per batch suffices for Resource-dimension aggregation; per-record injection would touch the seam contract and grow the wire | | semconv dependency | `@opentelemetry/semantic-conventions` is not imported | One string constant does not justify a dependency | -| Home | `@deepseek-ai/dsh-user-id`, shared by the OTel backend and `/feedback` | The second real consumer now exists; direct feedback must not depend on an exporter backend | -| Separate switch | None | Either consumer can create the identity; `DSH_TELEMETRY_DISABLED` stops telemetry reporting but does not disable feedback acknowledgement | +| Home | `@deepseek-ai/dsh-user-id`, shared by the OTel backend, `/feedback`, and direct DeepSeek requests | Consumers share one storage contract without depending on an exporter backend | +| Separate switch | None | Any consumer can create the identity; `DSH_TELEMETRY_DISABLED` stops telemetry reporting but does not disable feedback acknowledgement or the DeepSeek request header | ## Alternatives considered @@ -38,6 +38,6 @@ Session telemetry is mounted by default ([default-mount Note](2026-07-31-web-tel ## Consequences - One `$DSH_HOME` is one stable user in the OTel feed; separate homes are separate users by construction, with no cross-home linking mechanism. -- The OTel feed and `/feedback` share `.userid`. +- The OTel feed, `/feedback`, and direct DeepSeek requests share `.userid`. - Deleting `.userid` resets the identity (effective next launch); on an unwritable home each process holds its own in-memory id until the home becomes writable. - The [default-mount Note](2026-07-31-web-telemetry-default-mount.md)'s identity follow-up is closed for the anonymous-user-id part by this decision; hostname/surface dimensions, the redaction rule, and the usage-metrics track remain open. diff --git a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.zh.md b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.zh.md index 69c48076ce..4f6bab8df5 100644 --- a/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.zh.md @@ -10,7 +10,7 @@ session telemetry 已默认挂载([默认挂载 Note](2026-07-31-web-telemetry ## Decision -`getOrCreateAnonymousUserId()` 返回 `$DSH_HOME/.userid`(`resolveDshHome` 解析,`$DSH_HOME` > `~/.dsh`)中的裸 UUID 行,首用生成随机 UUID v4 并落盘;后端构造时把它作为 Resource 的 `user.id`(OTel semconv 标准用户属性)随每批导出携带一次。原始实现位于 `session-telemetry-otel`,因为当时不存在第二个真实消费方。`/feedback` 后来成为该消费方,因此[共享 id 决策](../architecture/2026-08-07-shared-feedback-telemetry-user-id.md)将所有权移交给 `@deepseek-ai/dsh-user-id`,但不改变本 Note 记录的存储、匿名、并发与丢失语义。 +`getOrCreateAnonymousUserId()` 返回 `$DSH_HOME/.userid`(`resolveDshHome` 解析,`$DSH_HOME` > `~/.dsh`)中的裸 UUID 行,首用生成随机 UUID v4 并落盘;后端构造时把它作为 Resource 的 `user.id`(OTel semconv 标准用户属性)随每批导出携带一次。原始实现位于 `session-telemetry-otel`,因为当时不存在第二个真实消费方。`/feedback` 后来成为该消费方,因此[共享 id 决策](../architecture/2026-08-07-shared-feedback-telemetry-user-id.md)将所有权移交给 `@deepseek-ai/dsh-user-id`,但不改变本 Note 记录的存储、匿名、并发与丢失语义。[直连 DeepSeek 请求身份](2026-08-11-deepseek-request-user-id-header.md)是同一 id 的第三个消费方。 | 裁定 | 取值 | 理由 | |---|---|---| @@ -22,8 +22,8 @@ session telemetry 已默认挂载([默认挂载 Note](2026-07-31-web-telemetry | 写失败 | best-effort 返回内存 id | telemetry 永不因 home 只读被阻塞 | | 上报位置 | Resource 属性,非逐条 attributes | 每批一次即够接收端按 Resource 维度聚合;逐条注入要动 seam 约定且涨 wire 体积 | | semconv 依赖 | 不引 `@opentelemetry/semantic-conventions` 包 | 一个字符串常量不值一个依赖 | -| 落点 | `@deepseek-ai/dsh-user-id`,由 OTel 后端与 `/feedback` 共享 | 第二个真实消费方已经出现;直接反馈不能依赖导出后端 | -| 单独开关 | 无 | 任一消费方都可创建该身份;`DSH_TELEMETRY_DISABLED` 会停止遥测上报,但不会禁用反馈确认 | +| 落点 | `@deepseek-ai/dsh-user-id`,由 OTel 后端、`/feedback` 与直连 DeepSeek 请求共享 | 消费方共用同一存储契约,且不依赖导出后端 | +| 单独开关 | 无 | 任一消费方都可创建该身份;`DSH_TELEMETRY_DISABLED` 会停止遥测上报,但不会禁用反馈确认或 DeepSeek 请求头 | ## Alternatives considered @@ -38,6 +38,6 @@ session telemetry 已默认挂载([默认挂载 Note](2026-07-31-web-telemetry ## Consequences - 一个 `$DSH_HOME` 在 OTel 回流中是一个稳定用户;不同 home 在构造上就是不同用户,无跨 home 关联机制。 -- OTel 回流与 `/feedback` 共享 `.userid`。 +- OTel 回流、`/feedback` 与直连 DeepSeek 请求共享 `.userid`。 - 删除 `.userid` 即重置身份(下次启动生效);home 不可写时每进程各自持有一个内存 id 直至恢复可写。 - [默认挂载 Note](2026-07-31-web-telemetry-default-mount.md) 的身份 follow-up 中「匿名用户 id」项由本决定关闭;hostname/surface 维度与脱敏规则、usage-metrics track 仍是待办。 diff --git a/.agents/notes/implemented/feature/2026-08-11-background-task-completion-wakes-an-idle-owner.i18n.yaml b/.agents/notes/implemented/feature/2026-08-11-background-task-completion-wakes-an-idle-owner.i18n.yaml new file mode 100644 index 0000000000..0f4b9e952f --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-11-background-task-completion-wakes-an-idle-owner.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-11-background-task-completion-wakes-an-idle-owner.md +2026-08-11-background-task-completion-wakes-an-idle-owner.md: cbebf1fbe82d47db4ba6e39b318d2e8ed8d89e17 +2026-08-11-background-task-completion-wakes-an-idle-owner.zh.md: b5f3cdd594b04370dcfe99d83775f34ab8b8115c diff --git a/.agents/notes/implemented/feature/2026-08-11-background-task-completion-wakes-an-idle-owner.md b/.agents/notes/implemented/feature/2026-08-11-background-task-completion-wakes-an-idle-owner.md new file mode 100644 index 0000000000..cbebf1fbe8 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-11-background-task-completion-wakes-an-idle-owner.md @@ -0,0 +1,76 @@ +# Agent Note: Background task completion wakes an idle owner + +Status: implemented + +English | [中文](2026-08-11-background-task-completion-wakes-an-idle-owner.zh.md) + +## Problem + +`tool-tasks` promised the model "You are notified in-session when a task finishes — do not busy-poll or sleep on one." The promise held only while the model was still working. Completion delivered through `agent.inject()`, which appends to the next-step inbox without reserving a driver, so a task settling after its turn closed left the notice parked until something unrelated woke the agent. The common shape is exactly the one that breaks: the model starts a long command, tells the user it started it, ends its turn, and the command finishes into an inbox nobody will claim. The prompt told the model not to poll, and then nothing arrived. + +The gap was recorded as a limitation rather than reasoned about, so the fallback was `task_output(wait: true)` — the blocking wait the same prompt discourages. + +This supersedes one fact of the [background-task runtime decision](../architecture/2026-06-20-generic-long-running-tool-runtime.md) — that completion never wakes an idle owner — and adds teardown as a `reported` setter. That note keeps every other task-runtime decision and is updated in place rather than replaced. + +The delivery machinery was never the obstacle. `Agent.send(message, target, wakeup)` has covered the `target` × `wakeup` matrix since the [unified send decision](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md), and `wakeDriver()` already handles idle, maintenance, and cancelled-converging phases. The missing piece was the policy choice of which lane a completion takes, plus the bound that choice needs. + +## Decision + +An unreported completion picks its lane from what the owner is doing. A busy owner is injected, unchanged. An idle owner is woken with `followup()`. + +This adopts the delivery rule the [continuation manager](2026-08-06-manager-owned-subagent-settlement-delivery.md) already ships for subagent settlement, where "steering rather than injecting is deliberate … This is a correctness rule, not a deployment preference." The two paths do not overlap: `tool-subagent` registers a Task only for a one-shot background child and returns `continuable` before reaching that code, so a child is delivered by exactly one of the two mechanisms. + +### The busy owner keeps injection + +For a driver that is genuinely running, `steer()` and `inject()` are the same delivery: `wakeDriver()` returns early without latching for a running, unaborted phase. They differ only for an owner whose turn is cancelled but has not yet converged, where steering redirects to the next turn and replays the wake at convergence. + +Injection is correct there. A cancelled turn is a user pressing stop, and reopening one on their behalf launders an interrupt into a model request they did not ask for. The turn loop already covers the ordinary case: it cannot close while the next-step inbox holds anything, so a notice arriving before that check extends the current turn, and several tasks settling together cost one step rather than one turn each. + +### Waking is bounded, and the bound is not time + +`maxConsecutiveWakes` (default 3) caps the turns one owner may open this way; beyond it a notice degrades to injection and waits for the next turn. Claiming any user-authored message restores the budget — claiming, not arrival, because that is the point human input actually enters a step. Notices this plugin queued never refill it. + +The bound exists because this chain is self-exciting in a way subagent settlement is not. Settlement is bounded by how many children the model spawned; a woken turn can start the background task whose completion wakes it again, with nobody watching. `dsh run` needs no separate policy: its one user message is claimed in the first turn and never repeats, so the budget is spent monotonically and the process terminates. + +`completionDelivery: quiet` restores the old lane for idle owners. It exists for deterministic transcripts, and mirrors the `reportDelivery` switch on `tool-subagent-report` in name, values, and default. + +### Teardown claims the report + +`cancelForTeardown` now marks the record `reported`, exactly as `kill()` does after cancelling. The asymmetry was invisible while the notice was a harmless inject; a waking reporter turns it into one model request per teardown layer, on agents the host is destroying. + +`reported` was already the right bit — "a kill, read, or wait has reported or committed to report the terminal state" — and teardown is a kill without a caller. Using it keeps every observer of the settlement intact: `onTaskDone` still fires, so runtime invariants and the force-fail path stay covered, and only notice reporters go quiet. + +### Completion is announced last + +`settle()` released waiters, marked the record settled, and published the visible-set change *after* running completion listeners. A reporter that opens a turn does so synchronously, so that order let a woken turn's `turn/start` land before the settlement it was reacting to was committed, and before any `onTasksChanged` observer had seen it. Announcing completion last makes the reporter the final observer of a settlement every other observer has already seen. + +## Alternatives considered + +**A producer-declared wake bit on `TaskStart`,** matching Codex's `trigger_turn` and Kimi's `admission` enum. It is the better long-run shape — a `tail -f` stream and a two-hour build want different answers — but no current producer distinguishes them, and the repository requires a current owner and need for public surface. The natural trigger to add it is the first producer that wants one task to wake and another not to. + +**A general unsolicited-input queue** with priority lanes, as Claude Code uses to merge background tasks, cron, MCP push, and hooks into one drain. DSH's inbox already is that queue — durable `agent/inbox/spliced` splices over `next-turn`/`next-step` — so this would add a layer above an existing one to decide a single bit. + +**Refusing to reopen a turn that already produced a visible answer,** Codex's `MailboxDeliveryPhase` latch. That latch is the default this decision deliberately inverts: waking after the model has spoken is the entire point, and the wake budget is the bound instead. + +**A wall-clock window** on top of the counter. For an interactive agent the slow case is the wanted one — an hour-long build finishing and the agent resuming is the feature — and `dsh run` is already bounded by the counter it cannot refill. Worth revisiting only if an unattended long-lived deployment appears. + +**Suppressing `onTaskDone` entirely during owner drain,** symmetric with the service-wide `listenersClosed`. It reads cleaner and removes a signal that is not only for notices: the force-fail record and the runtime invariant both observe teardown settlements. The `reported` bit denies exactly the reporters and nothing else. + +## Consequences + +- Default behavior changes: an idle owner now spends a model request per completion, capped at `maxConsecutiveWakes` per owner between user messages. Deployments that want the old behavior set `completionDelivery: quiet`. +- The `tool-tasks` prompt section needs no edit; "You are notified in-session when a task finishes" became true rather than aspirational. +- `TaskSnapshot.reported` gains teardown as a fourth setter, documented at the Service Definition and in [the subsystem reference](../../../../docs/subsystems/tasks.md). +- `settle()` announces completion after committing the record and publishing the visible-set change. Any listener relying on running before waiters were released or before `onTasksChanged` now runs after both. +- The `tool-bash` real-composition test dropped its second user message: settlement alone carries the notice into a turn that collects the output. It asserts the durable outcome rather than a turn boundary, because whether the command outlives its turn is a race; the lane choice is pinned in `tool-tasks` unit tests instead. +- Unit coverage pins idle wake, busy injection, quiet delivery, budget exhaustion, budget restore on user input, non-restore on plugin notices, and teardown silence. + +### Accepted risks + +A spent budget is restored only by user input. An unattended agent that exhausts it collects its remaining notices whenever something else opens a turn, and nothing re-arms it in the meantime. + +A notice pending on an idle owner under `quiet` still dies with that owner's disposal, unchanged from before: the disposal cancel clears the unclaimed inbox and the log keeps the insert/cancel pair as the record. The [settlement delivery note](2026-08-06-manager-owned-subagent-settlement-delivery.md) owns the offline-mailbox discussion this would need. + +Whether a completion extends the running turn or opens a new one is a genuine race for short-lived tasks, so no authored transcript can hold both orders. Assembled coverage asserts the outcome; the lane choice is pinned in unit tests. + +One microtask window survives: a settlement landing after the turn loop's last inbox check but before the driver commits its idle phase still reads `status === 'running'`, so it injects and nothing wakes. Steering would not close it either — `wakeDriver()` latches only for maintenance and post-cancel phases, not for a driver between its final check and its own retirement. Closing it needs an `agent-loop` boundary that publishes retirement before the last claim, which is a core-agent decision rather than a delivery-policy one. diff --git a/.agents/notes/implemented/feature/2026-08-11-background-task-completion-wakes-an-idle-owner.zh.md b/.agents/notes/implemented/feature/2026-08-11-background-task-completion-wakes-an-idle-owner.zh.md new file mode 100644 index 0000000000..b5f3cdd594 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-11-background-task-completion-wakes-an-idle-owner.zh.md @@ -0,0 +1,76 @@ +# Agent Note: Background task completion wakes an idle owner + +Status: implemented + +[English](2026-08-11-background-task-completion-wakes-an-idle-owner.md) | 中文 + +## 问题 + +`tool-tasks` 对模型承诺「任务完成时你会在会话内收到通知——不要忙轮询,也不要 sleep 等待」。这个承诺只在模型仍在工作时成立。完成经由 `agent.inject()` 交付,它只向 next-step inbox 追加而不预留 driver,因此在轮次结束之后才结算的任务会把通知搁在那里,直到某件无关的事情唤醒 agent。最常见的形态恰恰就是会失效的那一种:模型启动一条长命令,告诉用户已经启动,结束轮次,而命令完成后进入了一个无人领取的 inbox。提示词让模型不要轮询,然后什么也没到。 + +这个缺口被记为一条限制,而不是被推敲过,于是退路成了 `task_output(wait: true)`——同一段提示词并不鼓励的阻塞等待。 + +本决策取代[后台任务运行时决策](../architecture/2026-06-20-generic-long-running-tool-runtime.md)中的一条事实——完成永不唤醒空闲所有者——并把 teardown 加为 `reported` 的置位方。那份 note 仍拥有其余全部任务运行时决策,因此就地更新而非替换。 + +交付机制从来不是障碍。自[统一 send 决策](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md)起,`Agent.send(message, target, wakeup)` 就覆盖了 `target` × `wakeup` 矩阵,`wakeDriver()` 也已经处理 idle、maintenance 和已取消未收敛三种相位。缺的是「一次完成走哪条通道」这一策略选择,以及该选择所需的界。 + +## 决策 + +尚未报告的完成按所有者当时在做什么来选择通道。繁忙的所有者走注入,保持原样。空闲的所有者用 `followup()` 唤醒。 + +这采纳了[延续管理器](2026-08-06-manager-owned-subagent-settlement-delivery.md)已经为 subagent 结算所采用的交付规则,那里写着「用 steer 而非 inject 是刻意的……这是一条正确性规则,不是部署偏好」。两条路径不重叠:`tool-subagent` 只为一次性后台子 agent 注册 Task,而 continuable 分支在抵达那段代码之前就已返回,因此一个子 agent 恰好由两种机制中的一种交付。 + +### 繁忙的所有者保留注入 + +对真正在运行的 driver 而言,`steer()` 与 `inject()` 是同一次交付:对于运行中且未中止的相位,`wakeDriver()` 会提前返回且不设置 latch。二者只在一种所有者上有区别——轮次已取消但尚未收敛,此时 steer 会重定向到下一轮并在收敛时重放唤醒。 + +在那里注入才是对的。轮次被取消意味着用户按了停止,替他们重新开一轮等于把一次中断洗成了他们没有要求的模型请求。普通情形已由轮次循环覆盖:只要 next-step inbox 还有内容,轮次就无法结束,因此在该检查之前抵达的通知会延长当前轮次,同时结算的多个任务只花掉一步而不是各占一轮。 + +### 唤醒有界,且该界不是时间 + +`maxConsecutiveWakes`(默认 3)限制一个所有者由此开启的轮数;超出后通知降级为注入,等待下一轮。领取任何用户撰写的消息都会恢复预算——是领取而非抵达,因为那才是人类输入真正进入某一步的时刻。本插件自己排队的通知永远不会补充它。 + +设界是因为这条链会自激,而 subagent 结算不会。结算受限于模型派生了多少子 agent;被唤醒的一轮却可能启动某个后台任务,而它的完成又会唤醒同一个所有者,且无人旁观。`dsh run` 不需要单独策略:它唯一的用户消息在第一轮就被领取且不会重复,因此预算单调消耗,进程必然终止。 + +`completionDelivery: quiet` 为空闲所有者恢复旧通道。它的存在是为了确定性 transcript,并在名称、取值与默认值上都对齐 `tool-subagent-report` 的 `reportDelivery` 开关。 + +### 销毁自行认领报告 + +`cancelForTeardown` 现在会把记录标记为 `reported`,与 `kill()` 在取消之后所做的完全一致。当通知只是一次无害的注入时,这处不对称看不出来;而会唤醒的报告方会把它变成每个 teardown 层级一次模型请求,作用在宿主正要销毁的 agent 上。 + +`reported` 本来就是正确的那个 bit——「kill、read 或 wait 已报告或承诺报告终止状态」——而 teardown 是一次没有调用方的 kill。用它可以让该结算的每一个观察者都保持完整:`onTaskDone` 仍会触发,因此运行时不变量与强制失败路径依旧被覆盖,只有通知报告方会安静下来。 + +### 完成是最后才宣布的 + +`settle()` 此前释放等待方、标记记录已结算并发布可见集变更的时机,都排在运行完成监听器**之后**。开启轮次的报告方是同步执行的,因此那个顺序会让被唤醒轮次的 `turn/start` 抢在它所响应的那次结算被提交之前落地,也抢在任何 `onTasksChanged` 观察者看到它之前。把完成放到最后宣布,使报告方成为该结算的最后一个观察者,而其他观察者都已先看到它。 + +## 被否决的替代方案 + +**在 `TaskStart` 上加生产方声明的唤醒位**,对应 Codex 的 `trigger_turn` 与 Kimi 的 `admission` 枚举。从长期看这是更好的形状——`tail -f` 流与两小时构建想要不同答案——但当前没有任何生产方需要区分它们,而仓库要求公共面必须有当下的所有者与需求。加它的自然触发点,是第一个「要让某个任务唤醒而另一个不唤醒」的生产方出现时。 + +**一个通用的非请求输入队列**并带优先级通道,正如 Claude Code 用来把后台任务、cron、MCP 推送与 hook 合并进同一次排空。DSH 的 inbox 本身就是那个队列——`next-turn`/`next-step` 之上的持久 `agent/inbox/spliced` splice——因此这等于在既有层之上再加一层,只为决定一个 bit。 + +**拒绝重开一个已经产出可见答复的轮次**,即 Codex 的 `MailboxDeliveryPhase` 闩锁。那条闩锁正是本决策刻意反转的默认值:在模型已经说完话之后唤醒它就是本特性的全部意义,界由唤醒预算来承担。 + +**在计数之上再加墙钟窗口**。对交互式 agent 而言,慢的那种情形恰恰是想要的——一小时的构建结束、agent 接着干下去,这就是特性本身——而 `dsh run` 已被它无法补充的计数封顶。只有当出现无人值守的长生命周期部署时才值得重新考虑。 + +**在 owner 排空期间整体压制 `onTaskDone`**,与服务级的 `listenersClosed` 对称。它读起来更干净,但会移走一个不只服务于通知的信号:强制失败记录与运行时不变量都会观察 teardown 结算。`reported` 位恰好只否决报告方,别的什么也不否决。 + +## 影响 + +- 默认行为改变:空闲所有者现在每次完成会花掉一次模型请求,按所有者、在两次用户消息之间由 `maxConsecutiveWakes` 封顶。想要旧行为的部署设置 `completionDelivery: quiet`。 +- `tool-tasks` 的提示词段落无需改动;「任务完成时你会在会话内收到通知」从愿景变成了事实。 +- `TaskSnapshot.reported` 新增 teardown 作为第四个置位方,记录在 Service Definition 与[子系统参考](../../../../docs/subsystems/tasks.md)中。 +- `settle()` 在提交记录并发布可见集变更之后才宣布完成。任何依赖「在释放等待方之前或在 `onTasksChanged` 之前运行」的监听器现在都排在两者之后。 +- `tool-bash` 的 real-composition 测试去掉了第二条用户消息:仅靠结算就能把通知带入一个收集输出的轮次。它断言持久结果而非轮次边界,因为命令是否活得比它的轮次久是一场竞态;通道选择改由 `tool-tasks` 单元测试钉住。 +- 单元覆盖钉住:空闲唤醒、繁忙注入、quiet 交付、预算耗尽、用户输入恢复预算、插件通知不恢复预算,以及 teardown 静默。 + +### 已接受的风险 + +已花掉的预算只由用户输入恢复。耗尽预算的无人值守 agent 要等到其他原因开启轮次时才收走剩余通知,在此期间没有任何机制为它重新充能。 + +在 `quiet` 下待领于空闲所有者的通知仍会随该所有者释放而消亡,与此前一致:释放时的取消会清空未领取的 inbox,日志保留插入/取消这一对作为记录。[结算交付 note](2026-08-06-manager-owned-subagent-settlement-delivery.md) 承载这需要的离线信箱讨论。 + +对短命任务而言,完成究竟是延长运行中的轮次还是开启新轮次是一场真实竞态,因此没有哪份编写的 transcript 能同时容纳两种顺序。组装态覆盖断言结果;通道选择由单元测试钉住。 + +还残留一个微任务窗口:结算若落在轮次循环最后一次检查 inbox 之后、driver 提交 idle 相位之前,读到的仍是 `status === 'running'`,于是走注入且无人唤醒。改用 steer 也堵不上——`wakeDriver()` 只为 maintenance 与取消后的相位设置 latch,不为「最后一次检查与自身退休之间」的 driver 设置。要堵上它需要 `agent-loop` 在最后一次领取之前就发布退休状态,那属于核心 agent 的决策,而非交付策略。 diff --git a/.agents/notes/implemented/feature/2026-08-11-deepseek-request-user-id-header.i18n.yaml b/.agents/notes/implemented/feature/2026-08-11-deepseek-request-user-id-header.i18n.yaml new file mode 100644 index 0000000000..a05b51e5b6 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-11-deepseek-request-user-id-header.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-11-deepseek-request-user-id-header.md +2026-08-11-deepseek-request-user-id-header.md: 246021d3ea69a6341def90f81051da78a267f051 +2026-08-11-deepseek-request-user-id-header.zh.md: 6861d2b6ed1294c8e29efb7c883f56f8aa1d31ca diff --git a/.agents/notes/implemented/feature/2026-08-11-deepseek-request-user-id-header.md b/.agents/notes/implemented/feature/2026-08-11-deepseek-request-user-id-header.md new file mode 100644 index 0000000000..246021d3ea --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-11-deepseek-request-user-id-header.md @@ -0,0 +1,44 @@ +# Agent Note: DeepSeek request user and session identity headers + +Status: implemented + +English | [中文](2026-08-11-deepseek-request-user-id-header.zh.md) + +## Problem + +Direct DeepSeek requests already carried `x-deepseek-harness-session-id` when the caller supplied `GenerateOptions.sessionId`, which lets provider-side support and diagnostics correlate turns within one conversation. They lacked a stable identity across sessions even though the harness already persists an anonymous user id for telemetry and feedback. A separate id would break correlation, while putting it in the provider-neutral attribution helper would send a stable per-user identifier through every HTTP adapter. + +The user id is transport metadata, not model input. It must not enter the request body, prompt, token accounting, KV-cache identity, or session log. The destination is the adapter's resolved `baseURL`, which can be DeepSeek itself or a configured gateway, so the privacy boundary must be explicit. + +## Decision + +`dsh-llm-deepseek` sends `x-deepseek-harness-user-id` on every provider request sent after successful credential resolution. The value comes from `@deepseek-ai/dsh-user-id` and therefore matches the OpenTelemetry Resource `user.id` and `/feedback` acknowledgement for the same `$DSH_HOME`. The adapter continues to send `x-deepseek-harness-session-id` only when `GenerateOptions.sessionId` is present; the agent loop supplies the current durable `Session.id` for ordinary agent, title-generation, and compaction requests. + +The plugin resolves the user id lazily after credentials succeed and memoizes it for that plugin instance. A missing credential therefore does not create `.userid`, while the first authorized provider request can create it even when `DSH_TELEMETRY_DISABLED` is set. The direct adapter constructor accepts a `resolveUserId` dependency so wire behavior remains deterministic in unit tests. + +Both headers are model-hidden HTTP metadata sent to the resolved `baseURL`. They are absent from the JSON request body and do not become model-visible inputs or session events. A configured gateway receives them. Telemetry sharing controls only telemetry export and does not disable provider request identity. + +## Verification + +- The mock provider asserts that an authorized request carries the same user id returned by `getOrCreateAnonymousUserId()` and omits the session header when no session id is supplied. +- The session-identity wire test asserts both headers and preserves the exact supplied session id. +- A direct-adapter test asserts that user-id resolution happens once per stream, while the keyless configuration test proves a credential failure does not create `.userid`. +- The real Loader composition test asserts that the assembled plugin uses the shared user-id package rather than a test-only value. +- No keyless snapshot changes because the headers are not model-visible or user-visible transcript content. + +## Alternatives considered + +| Rejected | Reason | +|---|---| +| Add the id to generic `attributionHeaders()` | That helper is provider-neutral and static; a per-user value there would reach unrelated providers and violate its app-identity privacy contract | +| Configure a fixed custom header in `cordis.yml` | Deployment configuration cannot derive the current session id and would expose a stable identity as mutable config instead of using its owning runtime contract | +| Mint a DeepSeek-specific user id | Provider requests could not correlate with telemetry and feedback for the same harness home | +| Disable the header with telemetry sharing | Provider request identity and telemetry export have different recipients and purposes; one switch would hide the actual privacy boundary | +| Put the id in OpenAI-compatible `user` or `metadata` request fields | Body fields can affect provider schema, logging, caching, tokenization, or model-visible reconstruction; HTTP metadata preserves the intended boundary | + +## Consequences + +- DeepSeek support can correlate requests across sessions by one anonymous harness-home id and within a conversation by the durable session id. +- The first authorized DeepSeek request may create `$DSH_HOME/.userid` independently of telemetry export. +- Custom DeepSeek gateways receive the stable user id and any available session id, so operators must treat the configured `baseURL` as an identity recipient. +- The request body, prompt, token count, KV-cache identity, and session log remain unchanged. diff --git a/.agents/notes/implemented/feature/2026-08-11-deepseek-request-user-id-header.zh.md b/.agents/notes/implemented/feature/2026-08-11-deepseek-request-user-id-header.zh.md new file mode 100644 index 0000000000..6861d2b6ed --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-11-deepseek-request-user-id-header.zh.md @@ -0,0 +1,44 @@ +# Agent Note: DeepSeek 请求用户与会话身份头部 + +Status: implemented + +[English](2026-08-11-deepseek-request-user-id-header.md) | 中文 + +## 问题 + +当调用方提供 `GenerateOptions.sessionId` 时,直连 DeepSeek 请求已携带 `x-deepseek-harness-session-id`,让提供方侧支持与诊断可以关联同一对话中的多个轮次。但请求缺少跨会话的稳定身份,而 harness 已为遥测与反馈持久化匿名用户 id。另行生成 id 会破坏关联;把它放进提供方无关的归属辅助函数,则会让每个 HTTP 适配器都发送稳定的逐用户标识。 + +用户 id 是传输元数据,不是模型输入。它不得进入请求体、提示词、token 计量、KV cache 身份或会话日志。发送目标是适配器解析后的 `baseURL`,既可能是 DeepSeek 自身,也可能是配置的网关,因此必须明确隐私边界。 + +## 决策 + +`dsh-llm-deepseek` 在凭据解析成功后发出的每个提供方请求上发送 `x-deepseek-harness-user-id`。该值来自 `@deepseek-ai/dsh-user-id`,因此与同一 `$DSH_HOME` 的 OpenTelemetry Resource `user.id` 及 `/feedback` 确认一致。适配器继续仅在存在 `GenerateOptions.sessionId` 时发送 `x-deepseek-harness-session-id`;普通 agent、标题生成与压缩请求由 agent loop 提供当前持久化 `Session.id`。 + +插件在凭据解析成功后惰性获取用户 id,并在该插件实例内缓存。缺少凭据不会创建 `.userid`;即使设置了 `DSH_TELEMETRY_DISABLED`,首个已授权的提供方请求仍可能创建它。直连适配器构造函数接收 `resolveUserId` 依赖,使线路行为可在单元测试中保持确定性。 + +两个头部都是发送到解析后 `baseURL` 的模型不可见 HTTP 元数据。它们不在 JSON 请求体中,也不会成为模型可见输入或会话事件。配置的网关会收到它们。遥测共享只控制遥测导出,不会禁用提供方请求身份。 + +## 验证 + +- mock 提供方断言已授权请求携带 `getOrCreateAnonymousUserId()` 返回的同一用户 id,并在未提供会话 id 时省略会话头部。 +- 会话身份线路测试断言两个头部都存在,并原样保留传入的会话 id。 +- 直连适配器测试断言每条 stream 仅解析一次用户 id,keyless 配置测试则证明凭据失败不会创建 `.userid`。 +- 真实 Loader 组合测试断言组装后的插件使用共享 user-id 包,而非测试专用值。 +- 无需修改 keyless snapshot,因为这些头部不是模型可见或用户可见的 transcript 内容。 + +## 考虑过的替代方案 + +| 已否决 | 原因 | +|---|---| +| 把 id 加进通用 `attributionHeaders()` | 该辅助函数是提供方无关且静态的;加入逐用户值会把它发送给无关提供方,并违反其应用身份隐私契约 | +| 在 `cordis.yml` 中配置固定自定义头部 | 部署配置无法推导当前会话 id,且会把稳定身份暴露为可变配置,而不是使用其所属运行时契约 | +| 生成 DeepSeek 专用用户 id | 提供方请求将无法与同一 harness home 的遥测和反馈关联 | +| 随遥测共享关闭该头部 | 提供方请求身份与遥测导出的接收方和目的不同;共用开关会掩盖真实隐私边界 | +| 把 id 放进 OpenAI 兼容的 `user` 或 `metadata` 请求字段 | body 字段可能影响提供方 schema、日志、缓存、token 化或模型可见重建;HTTP 元数据可保留预期边界 | + +## 后果 + +- DeepSeek 支持可以通过一个匿名 harness-home id 跨会话关联请求,并通过持久化 session id 关联同一对话。 +- 首个已授权 DeepSeek 请求可独立于遥测导出创建 `$DSH_HOME/.userid`。 +- 自定义 DeepSeek 网关会收到稳定用户 id 与可用的会话 id,因此运维方必须将配置的 `baseURL` 视为身份接收方。 +- 请求体、提示词、token 数、KV cache 身份和会话日志保持不变。 diff --git a/.agents/notes/proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.i18n.yaml b/.agents/notes/proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.i18n.yaml new file mode 100644 index 0000000000..64daa7cd4a --- /dev/null +++ b/.agents/notes/proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md +2026-08-11-repository-naming-contract-and-rename-ledger.md: 8edb23f645cc34254cd49bed0d83767596ed42d7 +2026-08-11-repository-naming-contract-and-rename-ledger.zh.md: 9879316d207508efd96cd5064e6268c47df772d4 diff --git a/.agents/notes/proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md b/.agents/notes/proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md new file mode 100644 index 0000000000..8edb23f645 --- /dev/null +++ b/.agents/notes/proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md @@ -0,0 +1,370 @@ +# Agent Note: Repository naming contract and pre-release rename ledger + +Status: proposed + +English | [中文](2026-08-11-repository-naming-contract-and-rename-ledger.zh.md) + +## Problem + +The repository has grown faster than some names. Several package names still describe the first implementation instead of the capability. Several classes use `Service` even when they are registries, runtimes, engines, controllers, or resolvers. Some `ctx` keys are singular for registries and plural for one engine. Some provider names say `local` when they use replaceable filesystem or subprocess services and can run in another execution world. + +These names are not harmless. A name tells a contributor where a responsibility starts and stops. `Store` suggests data access. `Registry` suggests registrations and lookup. `Runtime` suggests live execution and lifecycle. When one word is used for all three, callers cannot tell which object owns policy, work, or state without reading the implementation. + +The repository also uses `SDK` in two meanings. The supported Python and TypeScript clients use the JSON-RPC SDK protocol. The project as a whole is DeepSeek Harness, not an SDK project. The removed SDK project toolchain made the broad meaning obsolete, but current prose and names still preserve parts of it. + +This is the last pre-release point at which repository-wide renames are cheap. Keeping weak names would turn accidental vocabulary into a compatibility contract. + +## Proposal + +Apply the rename ledger in this note before the first tagged release. Change names only. Do not change package responsibilities, service boundaries, behavior, defaults, or data models in a rename change. If a name exposes a bad boundary, write a separate proposed Agent Note for that boundary change. + +Each accepted family rename is atomic. Update its directory, npm package name, imports, Cordis plugin name, `ctx` key, public types, directly coupled event or tool identifiers, configuration, tests, fixtures, examples, generated references, and current documentation where the ledger names those surfaces. Do not leave an alias, compatibility package, duplicate service key, dual event name, or fallback parser. The repository is pre-release and rejects the old name. + +Implementation can use more than one PR to keep review practical. One PR must not leave one family with two public vocabularies. The final state must satisfy the complete ledger. + +### Use `SDK` for one thing + +`SDK` means the JSON-RPC-based client/server protocol used by the supported Python and TypeScript SDKs. Keep `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and the wire identity `deepseek-harness-sdk-runtime`. Rename the JSON-RPC server into that family. Do not call DeepSeek Harness itself an SDK, and do not restore the removed project generator, launcher, helper, or launcher telemetry packages. + +If accepted, this proposal will partially supersede three active decisions. It replaces the retained `bash/`, `pty/`, and `self-modification/` group names and both deferred package targets in the [package-regrouping decision](../../implemented/architecture/2026-07-29-package-regrouping.md). It replaces only the repository-wide SDK claim in the [SDK project toolchain removal](../../implemented/simplification/2026-08-11-remove-sdk-project-toolchain.md), which remains the owner of the deletion and the surviving runtime SDK. It replaces only the package-name rationale in the [tool-call timeout policy](../../implemented/architecture/2026-07-07-tool-call-timeout-policy.md); the timeout mechanism and its `guard/timeout-policy/` home remain unchanged. + +Other implemented notes that use a renamed package, path, or type are not superseded when their boundary and rationale remain intact. The implementation updates those factual names. It adds reciprocal links only where this proposal changes a decision; a proposal does not make current implemented notes describe unshipped names. + +### Name the role that exists + +Use a common, concrete noun. Name the stable responsibility, not the first implementation, the current folder, or a possible future expansion. Do not add a word that carries no information. Do not shorten a name by deleting the word that distinguishes its scope. + +An interface package names the capability. An implementation package adds the mechanism, protocol, environment, or vendor that distinguishes that implementation. Use `local` only when same-host execution is part of the contract. Do not use it for a provider that happens to read local-looking paths through replaceable `ctx.fs` or starts work through replaceable `ctx.subprocess`. + +Use a singular `ctx` key for one engine, runtime, policy, controller, resolver, store, or current configuration. Use a plural key for a registry or a service that owns multiple named members. The class role and key number must agree. A plural key does not by itself make an object a registry; its operations and ownership do. + +Use `Service` only when no sharper role is honest. `GoalService` and `SessionTitleService` are valid retained names because each owns a domain service whose work is not accurately reduced to storage, registration, or one execution mechanism. + +### Role words are contracts + +| Word | Use it when | Do not use it when | +|---|---|---| +| `Controller` | The object accepts commands or user intent and changes one existing domain or presentation state. It coordinates a bounded state transition. | The object executes arbitrary work, owns a provider fleet, or only converts values for display. | +| `Store` | The object owns one data set and mainly provides create, read, update, delete, snapshot, or subscription operations for that data. | It validates a state machine, arbitrates authority, dispatches work, owns provider precedence, or coordinates several domains. A map inside a class does not make the class a store. | +| `Directory` | The object exposes entries for discovery or selection. Its consumer asks what choices exist and reads their metadata. | Producers register arbitrary implementations into it, or callers execute work through it. A directory can be backed by a registry, but the two faces are not the same. | +| `Presenter` | The object is a pure conversion from domain values or tool arguments to render intent. It has no I/O, subscription, mutation, or lifecycle ownership. | It reads services, changes state, or controls when work runs. Those jobs belong to a controller or runtime. | +| `Registry` | The object owns a dynamic set of named registrations. It defines lookup, duplicate or precedence rules, registration lifetime, and disposal. | The main caller contract is dispatch, execution, cancellation, policy enforcement, or orchestration. A runtime can contain a registry as an internal part. | +| `Runtime` | The object runs live work. It owns dispatch, cancellation, provider coordination, or operation lifecycle across calls. | The object only stores records, returns a catalog, resolves one value, or holds configuration. `Runtime` is not a generic replacement for `Service`. | +| `Resolver` | The object computes or locates one answer from supplied inputs, usually without owning the answer's lifecycle. | It owns a mutable collection or a long-running execution lifecycle. | +| `Binder` | The object attaches one declared interface to the caller's context or lifecycle and returns the bound value. | It owns the bound value as a collection, controls its domain state, or merely converts data. | +| `Engine` | The object implements a domain algorithm or stateful execution model, such as workflow, compaction, or query evaluation. | It only selects a provider or forwards a request across a protocol boundary. | +| `Policy` | The object decides what is allowed, selected, limited, or observed. | It performs the mechanism that the decision permits. Keep policy and executor names separate. | +| `Executor` | The object runs an explicit request or resolved specification in one capability. | It owns a broad application lifecycle or a catalog of providers. | +| `Gateway` | The object adapts a process, network, RPC, or API boundary and translates between the two sides. | It only registers same-process services or stores metadata. | +| `Provider` | The object supplies one implementation of a capability definition. Add a mechanism or vendor qualifier when more than one provider can exist. | It is the capability definition, the registry of providers, or the consumer-facing runtime. | +| `Backend` | The object implements a replaceable lower-level persistence, transport, or execution backend behind a defined interface. | It is a user-facing service or only a returned reference to one live object. | +| `Handle` | The value is a reference to one live resource and controls or observes that resource. | The object creates and manages the whole resource pool. Do not use `Owner` or the vague `Resource` when `Handle` or a sharper manager role fits. | +| `Config` | The object owns one resolved configuration value or one tightly bounded configuration record and its update contract. | It stores a general collection, executes work, or exposes unrelated settings. | +| `Service` | The object owns a cohesive domain service whose authority cannot be stated honestly as one of the sharper roles above. | The name is used only because the class extends Cordis `Service`, or because choosing the real role takes more thought. | + +The practical tests are direct. If callers mainly call `register()` and receive a disposer, use `Registry`. If callers mainly call `run()`, `dispatch()`, `cancel()`, or `execute()`, use `Runtime`, `Engine`, or `Executor`. If callers mainly browse choices, use `Directory`. If an object mainly binds one specification to caller-owned context and lifetime, use `Binder`. If the object only maps domain data to UI data, use `Presenter`. If it also changes state, it is not a presenter. + +### Use qualifiers that add information + +Keep a protocol or dialect name when it distinguishes implementations. Keep `Bash`, `Pwsh`, `JSON-RPC`, `SQLite`, `JSONL`, `OpenTelemetry`, `Claude Code`, and `E2B` where the implementation depends on that mechanism. Do not put `LLM` into a compaction backend name when every current backend already uses the LLM seam; `basic` is the honest neutral name until a more specific algorithm name exists. + +Do not invent a `process sandbox` concept. The current `sandbox` family already names its product responsibility, and the accepted sweep does not change that boundary. + +Use title case for initialisms inside PascalCase identifiers: `Ui`, `Llm`, `JsonRpc`, and `ApiProxy`. Use the conventional uppercase form in prose and package names where applicable: UI, LLM, JSON-RPC, and API. `Typert` is the exact product spelling in identifiers and prose; do not write `TypeRT`, `TypeRt`, or `Typert` with another internal split. + +Do not remove an intentional vendor qualifier to avoid repetition. `dsh-subagent-dsh-sdk` names the DeepSeek Harness SDK provider and avoids confusion with another SDK. Its private class becomes `SdkSubagentProvider` because the class also needs to say what it provides. + +### Put the rule in project documentation + +When the rename implementation lands, add the full role-word contract to the paired package-creation guide at `docs/cookbook/adding-a-package.md`. Add a short linked rule to `packages/AGENTS.md`. Update the terminology table and the root project description so `SDK` and `Typert` have one meaning. The Agent Note owns the rationale and rejected alternatives; the guide owns the rule contributors follow. + +## Rename ledger + +The tables list public and repository-wide names. Private local variables follow the new vocabulary when they refer to the same role. A retained low-level or product-visible name is stated where a broad replacement would be wrong. + +### Runtime SDK + +| Current | Proposed | Reason | +|---|---|---| +| `@deepseek-ai/dsh-jsonrpc` | `@deepseek-ai/dsh-sdk-jsonrpc-server` | It is the server half of the SDK protocol. `jsonrpc` alone names an encoding; `sdk-jsonrpc-server` gives the family, mechanism, and role. | +| `HarnessSdkServer` | `HarnessSdkJsonRpcServer` | The class is one JSON-RPC server implementation, not every possible SDK server. | + +Keep `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and `deepseek-harness-sdk-runtime`. Exclude `@deepseek-ai/create-sdk`, `@deepseek-ai/dsh-scripts`, `@deepseek-ai/dsh-helper`, and `@deepseek-ai/dsh-telemetry`; the separate removal decision deletes them and their support graph. + +### Shell and terminal + +| Current | Proposed | Reason | +|---|---|---| +| `packages/bash/` | `packages/shell/` | The group contains the dialect-neutral executor seam, Bash and PowerShell implementations, environment support, and shell tools. | +| `@deepseek-ai/dsh-bash`, `ctx.bash` | `@deepseek-ai/dsh-shell`, `ctx.shell` | PowerShell already implements this seam. The capability is shell execution, not Bash. | +| Dialect-neutral `BashExecutor`, `BashExecRequest`, `BashExecSpec`, `BashProcess`, `BashRunResult`, `BashSandboxInfo`, `BashProcessRead`, and `BashProcessStatus` names | Corresponding `Shell*` names | These types cross both Bash and PowerShell implementations. Leaf types that describe Bash syntax or behavior keep `Bash`. | +| `BASH_SETTINGS_NAMESPACE`, settings namespace `bash` | `SHELL_SETTINGS_NAMESPACE`, settings namespace `shell` | Both shell providers register this capability-owned settings section. The constant and durable namespace must use the capability name. | +| `@deepseek-ai/dsh-bash-env`, `ctx.bashEnv`, `BashEnvRegistry` | `@deepseek-ai/dsh-shell-env`, `ctx.shellEnv`, `ShellEnvRegistry` | The environment registry is shared by Bash and PowerShell tools. | +| `docs/subsystems/bash.md` | `docs/subsystems/shell.md` | The subsystem page documents the dialect-neutral capability. | +| `packages/pty/` | `packages/terminal/` | The package family owns persistent terminal sessions. Raw PTY allocation remains in the subprocess layer. | +| `@deepseek-ai/dsh-pty`, `ctx.pty`, `PtyService` | `@deepseek-ai/dsh-terminal`, `ctx.terminals`, `TerminalSessionService` | Callers manage multiple named terminal sessions. They do not allocate raw PTYs through this service. | +| Public high-level `Pty*` session and backend names | `Terminal*` names | The public abstraction is a terminal session. Keep low-level `SubprocessTerminal*` names because they already name the substrate. | +| `@deepseek-ai/dsh-pty-local`, `LocalPtyBackend` | `@deepseek-ai/dsh-terminal-bash`, `BashTerminalBackend` | The provider depends on Bash prompt and shell behavior. `local` hides the actual dialect. | +| `@deepseek-ai/dsh-tool-pty` | `@deepseek-ai/dsh-tool-terminal` | The model-facing tools are already `terminal_*`; the package should use the same product noun. | +| `packages/pty/tool-bash-persistent` | `shell/tool-bash-persistent/` | The tool is a Bash tool and belongs with shell tools. Keep its npm name: `persistent` distinguishes it from one-shot `bash`, while `bash-terminal` would blur the product tool with the terminal-session family. | +| `docs/subsystems/pty.md` | `docs/subsystems/terminal.md` | The page documents terminal sessions, not raw PTY allocation. | + +Keep the Bash- and PowerShell-specific leaf packages, plugin ids, types, and tools. Their dialect names are accurate. + +### Language server and jobs + +| Current | Proposed | Reason | +|---|---|---| +| `@deepseek-ai/dsh-lsp-local` | `@deepseek-ai/dsh-lsp-stdio` | The provider speaks LSP over stdio through replaceable filesystem and subprocess services. It is not necessarily local. | +| `packages/tasks/` | `packages/jobs/` | The family owns detached tool jobs. `jobs` is short and avoids collision with user task or todo concepts. | +| `@deepseek-ai/dsh-tasks`, `ctx.tasks`, `TaskService` | `@deepseek-ai/dsh-jobs`, `ctx.jobs`, `JobRegistry` | The service registers, owns, observes, waits for, and cancels multiple background jobs. It is a registry, not a general task service. | +| Public `TaskId`, `TaskKindMap`, `TaskStart`, `TaskHooks`, `TaskOutcome`, `TaskSnapshot`, `TaskRead`, and `TaskDoneListener` names | Corresponding `Job*` names | These types belong to the renamed job domain. `JobId` is shorter and clearer than `BackgroundTaskId` or `BgTaskId`. | +| `@deepseek-ai/dsh-tasks-local`, `LocalTaskService` | `@deepseek-ai/dsh-jobs-local`, `LocalJobRegistry` | This is the process-local provider of the job registry. Here `local` is meaningful because the jobs and callbacks live in one process. | +| `@deepseek-ai/dsh-tool-tasks` | `@deepseek-ai/dsh-tool-jobs` | The consumer controls the job registry and should use the same domain noun. | +| `task_output`, `task_list`, `task_kill` | `job_output`, `job_list`, `job_kill` | These model tools act on jobs, not user tasks. `run_in_background` returns a `JobId`. | +| `@deepseek-ai/dsh-client-ui-task`, `client/ui-task/` | `@deepseek-ai/dsh-client-ui-jobs`, `client/ui-jobs/` | The client package presents the background-job collection. It is not one user task. | +| `TaskView`, wire frame `session/tasks`, `tasksBySession` | `JobView`, wire frame `session/jobs`, `jobsBySession` | The browser contract and its mirror expose the same job domain as the registry and tools. | +| `docs/subsystems/tasks.md` | `docs/subsystems/jobs.md` | The subsystem page must use the public job vocabulary. | + +Keep the base LSP package, `ctx.lsp`, LSP protocol types, and the LSP tool. The seam deliberately exposes language-server semantics; only its provider qualifier is wrong. + +### Input triggers, tool presentation, permission presets, and user questions + +| Current | Proposed | Reason | +|---|---|---| +| `@deepseek-ai/dsh-client-ui-slash`, `ui-slash/` | `@deepseek-ai/dsh-client-ui-input-trigger`, `ui-input-trigger/` | The client handles `/`, `@`, keyboard arbitration, candidate menus, and programmatic launch. It is not only slash commands. | +| `ctx.slash`, `SlashService`, `SlashController`, `SlashSource` | `ctx.inputTriggers`, `InputTriggerService`, `InputTriggerController`, `InputTriggerSource` | The names cover every supported trigger and keep the existing service, controller, and source roles. Coupled locale and public type names follow `InputTrigger`. | +| `@deepseek-ai/dsh-agent-tool-mode`, plugin `tool-mode` | `@deepseek-ai/dsh-agent-tool-presentation`, plugin `tool-presentation` | The plugin changes how tools are presented to the model. It does not change execution behavior. Keep local `Config.mode` and `ToolPresentationMode`. | +| `packages/interaction/permission/` | `packages/interaction/permission-presets/` | The package owns named combinations of sandbox and approval settings, not permission enforcement. | +| `@deepseek-ai/dsh-permission`, `ctx.permission`, `PermissionService` | `@deepseek-ai/dsh-permission-presets`, `ctx.permissionPresets`, `PermissionPresetService` | The service selects and persists presets. Sandbox and approval services enforce the result. | +| `@deepseek-ai/dsh-client-ui-permission` | `@deepseek-ai/dsh-client-ui-permission-presets` | The UI edits and selects permission presets. | +| `docs/subsystems/permission.md` | `docs/subsystems/permission-presets.md` | The page documents preset selection, not permission enforcement. | +| `@deepseek-ai/dsh-user-interaction`, `user-interaction/` | `@deepseek-ai/dsh-user-questions`, `user-questions/` | The seam supports question batches and answers only. Approval, commands, and directory picking are separate interaction seams. | +| `ctx.userInteraction`, `UserInteractionService`, `UserInteractionProvider`, `UserInteractionError` | `ctx.userQuestions`, `UserQuestionService`, `UserQuestionProvider`, `UserQuestionError` | These names state the one supported interaction form. Keep `AskUserQuestion*`, the `ask_user_question` tool, and `@deepseek-ai/dsh-tool-ask-user`. | +| `docs/subsystems/user-interaction.md` | `docs/subsystems/user-questions.md` | The page documents questions and answers only. | + +Keep `/permission`, the `permissions` projection, the `permission` settings namespace, and `permission/preset`; they are accurate product or durable vocabulary. Keep the full `PermissionPresetSettingsController` name. Dropping `Preset` would remove the word that limits its authority. Plan a separate proposal to remove the `both` tool-presentation mode; this rename does not remove behavior. + +### Typert, API gateway, and tools + +| Current | Proposed | Reason | +|---|---|---| +| `packages/typert/type-meta/`, `@deepseek-ai/dsh-type-meta` | `typert/protocol/`, `@deepseek-ai/dsh-typert-protocol` | The package owns the Typert Remote protocol, decorators, bindings, codecs, lookups, and context contracts. It is not generic type metadata. | +| `GatewayService` in the protocol package | `TypertRemoteService` | The base class marks a same-process service for Remote export. It is not the API gateway. | +| `bindTypeRTGateway`, `typertGateway` binding | `bindTypertRemote`, `typertRemote` | These bindings expose Typert Remote services, not the concrete API gateway service. | +| Public `TypeRT*` and camel-case `typeRT*` identifiers | `Typert*` and `typert*` | `Typert` is the one canonical product spelling. | +| Protocol interface `TypeRTService` | `TypertRegistryContract` | The protocol-owned interface is the dependency-inverted face implemented by the existing concrete `TypertRegistry`. A distinct suffix prevents an import and declaration collision. | +| `ToolRegistry` | `ToolRuntime` | The class owns presentation, approval and guard policy, dispatch, cancellation, validation, finalization, and observation. Registration is only one internal part. | +| `ToolRegistryScheduler`, `TOOL_REGISTRY_SCHEDULER` | `ToolRuntimeScheduler`, `TOOL_RUNTIME_SCHEDULER` | The scheduler controls runtime dispatch, not registration. | + +Keep `@deepseek-ai/dsh-tools` and `ctx.tools`. Keep `@deepseek-ai/dsh-api-gateway`, its `gateway/` folder, `ctx.typertGateway`, and `TypertGatewayService`; that service is a real API gateway. Its internal `TypeRT*` identifiers still follow the `Typert*` spelling rule. + +### Workspace instructions, telemetry, identity, and launch environment + +| Current | Proposed | Reason | +|---|---|---| +| Host `ctx.workspace` | Host `ctx.workspaces` | `WorkspaceRegistry` owns multiple workspaces. The plural key matches the registry role. The existing Client `ctx.workspaces` runs in a separate Cordis context, so the shared spelling is intentional and cannot collide at runtime. Keep `@deepseek-ai/dsh-workspace`, `WorkspaceRegistry`, `Workspace`, and `workspace.*` wire names. | +| `@deepseek-ai/dsh-workspace-context`, `context/workspace-context/` | `@deepseek-ai/dsh-agent-instructions`, `context/agent-instructions/` | The package loads hierarchical `AGENTS.md` and `CLAUDE.md` files for the agent. It is not general workspace context. | +| Plugin and durable source names `workspace-context` and `workspace-instructions` | `agent-instructions` | The recorded source is a specific class of agent instructions. `AgentInstruction*` replaces public `WorkspaceInstruction*` names. This term does not include system, developer, or user messages. | +| `ctx.telemetry`, abstract `Telemetry` | `ctx.sessionTelemetry`, `SessionTelemetryBackend` | The service captures session-ledger telemetry and hands it to a reporting backend. It is not a repository-wide metrics or tracing service. | +| `TelemetryBackend` | `SessionTelemetrySink` | This lower layer receives emitted records. `Sink` distinguishes it from the coordinating backend service. | +| `TelemetryCoordinator`, `TelemetryRecord`, `TelemetrySeverity`, `TelemetrySharingStatus`, and `TelemetryCapture` | Corresponding `SessionTelemetry*` names | These public types belong only to session telemetry. | +| `telemetry/record` | `session-telemetry/record` | The event name must state its owning domain. | +| `TelemetryOtel`, `TelemetryMode`, plugin `telemetry-otel` | `OpenTelemetrySessionBackend`, `SessionTelemetryMode`, plugin `session-telemetry-otel` | The provider name states both the OpenTelemetry mechanism and session scope. Keep the package names `dsh-session-telemetry` and `dsh-session-telemetry-otel`. | +| `docs/subsystems/telemetry.md` | `docs/subsystems/session-telemetry.md` | The page documents session telemetry, not repository-wide observability. | +| `session/user-id/`, `@deepseek-ai/dsh-user-id` | `session/anonymous-user-id/`, `@deepseek-ai/dsh-anonymous-user-id` | The value is a random correlation id, not an authenticated user identity. | +| `USER_ID_FILE_NAME`, `.userid`, feedback label `User` | `ANONYMOUS_USER_ID_FILE_NAME`, `.anonymous-user-id`, feedback label `Anonymous user` | The file and UI must not imply account identity. Keep the existing `AnonymousUserId` functions and the standard OTel attribute `user.id`. | +| `util/environment/`, `@deepseek-ai/dsh-environment` | `util/launch-environment/`, `@deepseek-ai/dsh-launch-environment` | The package captures one immutable layered snapshot at launch. It is not a general environment API. | +| Public `Environment*`, `createEnvironmentSnapshot`, `environmentOf`, `DSH_ENVIRONMENT_KEY` | `LaunchEnvironment*`, `createLaunchEnvironmentSnapshot`, `launchEnvironmentOf`, `DSH_LAUNCH_ENVIRONMENT_KEY` | The names state the snapshot's lifetime and purpose. | +| `ctx.launcherEnvironment` | `ctx.launchEnvironment` | The value describes the application launch, not only a launcher component. Keep source labels `process`, `project-env`, and `user-env`. | + +### Schedule, workflow, goals, and compaction + +| Current | Proposed | Reason | +|---|---|---| +| `ScheduleOwner` | `ScheduleRuntime` | The per-agent object runs live timers, durable projection, dispatch, idle waits, and disposal. `Owner` does not state that execution role. Coupled private `owner*` names follow `runtime*`. | +| `WorkflowService`, `ctx.workflows` | `WorkflowEngine`, `ctx.workflowEngine` | One engine parses and executes workflow programs. The plural key wrongly suggests a registry. Keep `@deepseek-ai/dsh-workflow` and workflow events and tools. | +| `@deepseek-ai/dsh-workflow-workerthread`, `WorkerWorkflowEngine` | `@deepseek-ai/dsh-workflow-worker-thread`, `WorkerThreadWorkflowEngine` | `worker thread` is the precise Node mechanism and the repository spelling uses the full words. | +| `@deepseek-ai/dsh-goal-session`, `goal/goal-session/` | `@deepseek-ai/dsh-goal-round-driver`, `goal/goal-round-driver/` | The plugin drives same-session Goal Rounds. It neither stores goals nor defines sessions. Keep `GoalService`, goal source, events, and contracts. | +| `packages/compact/` | `packages/compaction/` | The group is a noun-domain family. `compact` remains the user command verb. | +| `@deepseek-ai/dsh-compact`, `ctx.compact`, `CompactService` | `@deepseek-ai/dsh-compaction`, `ctx.compaction`, `CompactionEngine` | The object runs the compaction algorithm and lifecycle. It is an engine, not a generic service. | +| `compact/*` events and public domain prefixes | `compaction/*` | Events and domain types use the noun. Keep verb-shaped operations such as `compactNow`, `compactRegion`, and `compactIfNeeded`. | +| `@deepseek-ai/dsh-compact-basic`, `BasicCompactService`, public `BasicCompact*` | `@deepseek-ai/dsh-compaction-basic`, `BasicCompactionEngine`, corresponding `BasicCompaction*` | `basic` is plain but honest. `compaction-llm` adds no information because LLM use is already part of the current implementation family. | +| `@deepseek-ai/dsh-compact-tool-result-prune`, `ToolResultPruneService`, `ctx.toolResultPrune` | `@deepseek-ai/dsh-compaction-tool-result-pruner`, `ToolResultPruner`, `ctx.toolResultPruner` | The plugin is an actor that prunes tool results. The noun `pruner` names that role. | + +Keep `/compact`, the command package, and the rejected decision to preserve separate compaction definition and provider packages. The rename changes vocabulary, not that package boundary. + +### Settings, credentials, client modules, and small core roles + +| Current | Proposed | Reason | +|---|---|---| +| Abstract `Settings` | `SettingsProvider` | The class supplies settings through a replaceable capability. Keep the package, key, and events. | +| `@deepseek-ai/dsh-settings-local`, `SettingsLocal` | `@deepseek-ai/dsh-settings-file`, `FileSettingsProvider` | The implementation is file-backed through the filesystem seam. `file` states the mechanism; `local` does not. | +| Abstract `Credentials` | `CredentialProvider` | The class resolves credential references. Keep package names, keys, and events. | +| `CredentialsLocal` | `LocalCredentialProvider` | This provider reads the host process and `.env` state, so local execution is part of its contract. | +| `ClientModuleHostService`, `ctx.clientModuleHost` | `ClientModuleRegistry`, `ctx.clientModules` | The service owns multiple registered client modules. Keep the package and the browser `ClientModuleLoader`. | +| `AgentDefaultModelService` | `AgentDefaultModelConfig` | The object stores one default model selection. It does not run a service or general registry. Keep its package, key, settings namespace, and type. | +| `SessionReferenceService`, `ctx.sessionReferences` | `SessionReferenceResolver`, `ctx.sessionReferenceResolver` | It resolves one session reference from a URI or input. It does not own a reference collection. | +| `SessionQueryService`, `SessionQuerySqlite` | `SessionQueryEngine`, `SqliteSessionQueryEngine` | The classes execute a query model and its SQLite implementation. Keep package names, key, and tool. | +| `CommandService` in the host command package | `CommandRuntime` | The object registers and executes host commands across live calls. Keep its package, key, types, and events. | +| `TokenMeterService` | `TokenMeter` | The object measures token use. `Service` adds no scope. | +| `LlmService` | `LlmRuntime` | The object selects providers and runs live model requests. Keep the package, key, adapters, and events. | + +### Host web server, session data, and code execution + +| Current | Proposed | Reason | +|---|---|---| +| `HttpServerService`, `ctx.httpServer` | `WebServer`, `ctx.webServer` | The server owns HTTP routes and WebSocket upgrade routes. `Web` leaves room for both; `Http` is too narrow here. Keep `packages/host/webserver`, `@deepseek-ai/dsh-host-webserver`, `WebRoute`, and `WebUpgradeRoute`. | +| Documentation subsystem label `http-server` | `web-server` | The subsystem must use the same scope as the service. | +| `SessionPersistenceJsonl` | `JsonlSessionPersistence` | Put the implementation qualifier first and keep the capability role intact. | +| `SessionPersistenceSqlite` | `SqliteSessionPersistence` | Use the same provider naming order as JSONL. | +| `@deepseek-ai/dsh-session-title-first-message-llm`, cadence `first-message` | `@deepseek-ai/dsh-session-title-first-prompt-llm`, cadence `first-prompt` | The trigger is the first user prompt, not any message in the session log. | +| `@deepseek-ai/dsh-session-title-all-messages-llm`, cadence `all-user-messages` | `@deepseek-ai/dsh-session-title-all-prompts-llm`, cadence `all-prompts` | The backend refreshes from user prompts. `all messages` wrongly includes assistant and tool events. | +| `@deepseek-ai/dsh-code-runtime-worker`, `WorkerCodeRuntime` | `@deepseek-ai/dsh-code-runtime-worker-thread`, `WorkerThreadCodeRuntime` | The implementation uses a Node worker thread. `worker` alone is too broad. | +| `SubprocessService` | `SubprocessRuntime` | The service owns live child-process execution and lifecycle. Keep its package and key. | +| `LocalSubprocessService` | `LocalSubprocessRuntime` | The provider runs same-host processes and process trees. | +| `E2BSubprocessService` | `E2BSubprocessRuntime` | The provider runs subprocesses in the E2B runtime. | + +Keep the complete session projection family and `SessionProjection*` vocabulary. A projection is a maintained read model; `Reducer` would name only its fold operation and would understate caching and lookup. Keep `SessionTitleService`, checkpoint policy, persistence package names, time context, and tmux context. + +### Filesystem, skill, subagent, and web providers + +| Current | Proposed | Reason | +|---|---|---| +| `@deepseek-ai/dsh-fs-policy` | `@deepseek-ai/dsh-fs-observation-policy` | The package defines which filesystem observations authorize later effects. It is not the complete filesystem or sandbox policy. | +| `FsPolicyExec` | `FsObservationActor` | The value names the actor whose observations and effects the policy relates. It does not execute the policy itself. | +| `SkillService` | `SkillRegistry` | The service registers providers and resolves skills from their catalogs. | +| `@deepseek-ai/dsh-skill-local`, `LocalSkillProvider`, provider id `local` | `@deepseek-ai/dsh-skill-filesystem`, `FileSystemSkillProvider`, provider id `filesystem` | The provider discovers skill files through `ctx.fs`, which can be local or remote. The mechanism is filesystem access, not locality. | +| `SubagentService` | `SubagentRuntime` | The service selects providers and owns live spawn, resume, follow-up, cancellation, and settlement behavior. | +| `@deepseek-ai/dsh-subagent-spawn`, `SpawnProvider` | `@deepseek-ai/dsh-subagent-spawn-in-process`, `SpawnInProcessProvider` | This provider starts a child agent in the current process. The configured provider id remains `spawn`. | +| `@deepseek-ai/dsh-subagent-fork`, `ForkProvider` | `@deepseek-ai/dsh-subagent-fork-in-process`, `ForkInProcessProvider` | This provider forks an agent in the current process. The configured provider id remains `fork`. | +| `@deepseek-ai/dsh-subagent-inprocess`, `subagent-inprocess/` | `@deepseek-ai/dsh-subagent-in-process-driver`, `subagent-in-process-driver/` | The package contains common in-process driving logic, not a third provider. | +| Private `SdkProvider` in `dsh-subagent-dsh-sdk` | `SdkSubagentProvider` | The repeated package qualifier is intentional, and the class must say that it provides subagents through the SDK. | +| `WebService`, `WebServiceConfig` | `WebRuntime`, `WebRuntimeConfig` | The object selects providers and runs live search and fetch operations. Keep the package, key, provider packages, and model tool. | +| `@deepseek-ai/dsh-web-fetch-local`, `LocalFetchProvider`, `LocalFetchLimits`, provider id `local-http` | `@deepseek-ai/dsh-web-fetch-http`, `HttpFetchProvider`, `HttpFetchLimits`, provider id `http` | This provider performs direct HTTP fetches. `local` says where code happens to run, not which mechanism it provides. | + +Keep `@deepseek-ai/dsh-subagent-dsh-sdk`, its provider id `dsh-sdk`, external ACP, Codex, and Claude Code provider families, the subagent tool package names, the main filesystem package and backends, filesystem tools and events, and the skill badge and tool packages. + +### Hooks, guards, plan mode, extensions, and diagnostics + +| Current | Proposed | Reason | +|---|---|---| +| `@deepseek-ai/dsh-hooks-claude`, `ClaudeHookConfig`, `parseClaudeConfig`, dialect `claude` | `@deepseek-ai/dsh-hooks-claude-code`, `ClaudeCodeHookConfig`, `parseClaudeCodeConfig`, dialect `claude-code` | The hook bridge targets Claude Code, not every Anthropic or Claude product. | +| `@deepseek-ai/dsh-repeat-tool-guard`, plugin/source `repeat-tool-guard` | `@deepseek-ai/dsh-repeat-tool-reminder`, plugin/source `repeat-tool-reminder` | The plugin adds a model reminder. It does not block or enforce a guard decision. | +| `@deepseek-ai/dsh-timeout-policy` | `@deepseek-ai/dsh-tool-call-timeout-policy` | The full `tool-call` qualifier names what the policy limits without calling the plugin a model-facing tool. Keep its `guard/timeout-policy/` directory and plugin id `timeout-policy`; the `packages/*/tool-*` catalog convention still applies only to packages that register tools. | +| `PlanModeService` | `PlanModeController` | The object controls transitions into and out of plan mode. It is not a general execution runtime. | +| `packages/self-modification/` | `packages/extensions/` | The group contains repository plugin inspection and mounting tools. `extensions` states the stable package role without asserting that the agent modifies itself. Keep the package names `tool-cordis` and repository-plugin names. | +| `packages/support/` | `packages/test-support/` | The group is test-only infrastructure. Its path must say so. | +| `packages/support/invariants/` | `runtime-diagnostics/invariants/` | Invariants can run in production diagnostics even though shipped presets omit them. They are not test support. | +| `InvariantService` | `InvariantRegistry` | The object owns registered invariant checks. Keep `@deepseek-ai/dsh-invariants` and `ctx.invariants`. | +| `packages/client/test-runtime/` | `packages/test-support/client-runtime/` | The package is client test infrastructure. Keep its npm name if it already states that contract. | + +Keep MCP, Todo, and the Plan Mode package, key, events, and tool names. The accepted change concerns the controller class, not the product feature. + +### Utilities, E2B, host, bundles, examples, and applications + +| Current | Proposed | Reason | +|---|---|---| +| `util/paths/`, `@deepseek-ai/dsh-paths` | `util/home-paths/`, `@deepseek-ai/dsh-home-paths` | The helpers resolve paths under the Harness home. They are not a general path library. Keep the individual function names when they already state the returned path. | +| `util/retention/`, `@deepseek-ai/dsh-retention` | `util/output-retention/`, `@deepseek-ai/dsh-output-retention` | The policy retains command and tool output. It is not a general data-retention framework. | +| `E2BSandboxService` | `E2BRuntime` | The class creates, reuses, and disposes the E2B execution environment used by filesystem and subprocess adapters. It is broader than one sandbox handle and narrower than a generic owner. Keep `@deepseek-ai/dsh-e2b`, `ctx.e2b`, and the `e2b/` group. | +| `@deepseek-ai/dsh-frontend-static` | `@deepseek-ai/dsh-host-frontend-static` | The package is the Host plugin that serves the frontend assets. The prefix distinguishes it from frontend application code. | +| `@deepseek-ai/dsh-jsonrpc-demo` | `@deepseek-ai/dsh-sdk-jsonrpc-demo` | The example demonstrates the runtime SDK over JSON-RPC. It belongs to the one SDK meaning. | +| `@deepseek-ai/dsh-frontend` | `@deepseek-ai/dsh-web-frontend` | The application is the web frontend. Keep its physical `apps/web/` folder. | + +Keep atomic-write, brand, native-command, timeout utility, directory-picker, `dsh-base`, `dsh-web-app`, app boot, CLI names, and the `headless` package, bundle, and example identity. `headless` is the intended product essence and may later support more than one-shot execution. + +### Client runtime and UI + +| Current | Proposed | Reason | +|---|---|---| +| `SlotsService` | `SlotRegistry` | The object owns named slot declarations and registrations. | +| `SessionsService` | `SessionRuntime` | The object owns live client session coordination, not a passive session list. | +| `WorkspacesService` | `WorkspaceRuntime` | The client object coordinates live workspace selection and operations. Existing `ctx` keys stay where the ledger does not name a key change. | +| `LocaleService` | `LocaleRuntime` | The object coordinates locale definitions, selection, persistence, and change publication. | +| `ThemeService` | `ThemeRuntime` | The object coordinates themes, preference resolution, system sensing, and change publication. | +| `LayoutService` | `LayoutController` | The object controls the current UI layout state. | +| `@deepseek-ai/dsh-client-ui-model` | `@deepseek-ai/dsh-client-ui-model-selection` | The package controls the model selection for a session. The singular `model` name is too broad. | +| `ModelService`, `ctx.models` | `ModelDirectoryResolver`, `ctx.modelDirectories` | Its only public operation, `directoryFor(sessionId)`, resolves and retains one directory per live session. It has no registration API, so `Registry` would be false. Each `ModelDirectory` remains the consumer-facing catalog of selectable models. | +| `SettingsScopeService` | `SettingsScopeBinder` | Its sole operation binds one namespace specification to the caller's transport and lifecycle and returns a `SettingsScopeController`. Keep `ctx.settingsScope`; it names the singular binding capability, not a collection of scopes. | +| `@deepseek-ai/dsh-client-ui-models` | `@deepseek-ai/dsh-client-ui-settings-models` | This package owns the Models settings panel. Keep `ModelsSettingsStore`; it holds one settings view model with data operations and subscriptions and is a real store. | +| `@deepseek-ai/dsh-client-ui-plugin-config`, `client/ui-plugin-config/` | `@deepseek-ai/dsh-client-ui-settings-plugins`, `client/ui-settings-plugins/` | This package owns the Plugins settings section, not a general plugin-configuration system. The target joins the `ui-settings-*` family and uses the section's plural product name. | +| `PluginConfigSection`, `PluginConfigSectionProps`, `PluginConfigSectionInjected`, `settings.pluginConfig` | `PluginsSettingsSection`, `PluginsSettingsSectionProps`, `PluginsSettingsSectionInjected`, `settings.plugins` | These names describe the Plugins settings presentation. Each card still edits one plugin's configuration, but the section itself is a settings UI. | +| `agent-loop-store.ts`, `bash-store.ts`, `web-search-store.ts` | `agent-loop-card-controller.ts`, `bash-card-controller.ts`, `web-search-card-controller.ts` | Each module exports a card controller. A private `SnapshotStore` field does not make the module a store. | +| `card-store.ts` | `card-form.ts` | The module owns the staged form, field conversion, and form actions. The snapshot stores it returns are presentation adapters, not the module's main role. | +| `@deepseek-ai/dsh-client-ui-question` | `@deepseek-ai/dsh-client-ui-user-questions` | The UI presents the user-question seam, not an arbitrary question domain. | +| `@deepseek-ai/dsh-client-ui-command`, `ui-command/` | `@deepseek-ai/dsh-client-ui-commands`, `ui-commands/` | The package presents and runs a collection of commands. | +| Client `ctx.command`, `CommandService`, `CommandServiceContract` | `ctx.commandUi`, `CommandUiRuntime`, `CommandUiContract` | The host already owns `ctx.commands`. The client service is the UI runtime for command discovery and execution. Existing `CommandUiSpec` fixes the `Ui` casing. | +| `ConversationService` | `ConversationController` | The object controls the active conversation state and user actions. | +| `InputService` | `SessionInputResolver` | The interface resolves the input facade for one session scope. It is neither a global input registry nor an execution service. Keep `InputHub` as the concrete hub and `ctx.conversation.input` as the published face. | + +Use `Ui`, not `UI`, inside PascalCase identifiers. Keep the remaining client package names unless this ledger names them. Keep the deprecated client connection and Host `ApiProxy` vocabulary for now; the API plane will replace them, and a rename would add churn to a surface scheduled for removal. + +## Explicit non-renames + +The following debated names stay unchanged because the current scope is accurate or a rename would create a false concept: + +- Keep the complete sandbox family and `ctx.sandbox`. Do not introduce `processSandbox`. +- Keep `@deepseek-ai/dsh-api-gateway`, `ctx.typertGateway`, and `TypertGatewayService`. +- Keep session projection names. A projection is not only a reducer function. +- Keep `GoalService`; it owns the goal state machine, authority, compare-and-set behavior, events, and remote operations. It is not just a store. +- Keep `SessionTitleService`; its role is a domain service shared by title providers. +- Keep `PermissionPresetSettingsController` even though it is long. Every word limits the role. +- Keep `ModelsSettingsStore`; its main contract is one settings data model with store operations. +- Keep `InputHub`; it is the concrete hub that backs `SessionInputResolver`. +- Keep `dsh-subagent-dsh-sdk` and provider id `dsh-sdk`; the repeated qualifier prevents ambiguity. +- Keep `headless`; the product identity is accurate even if the runtime later supports more than one-shot use. +- Keep deprecated Host `ApiProxy` and client connection names until the API replacement removes them. +- Keep `Web` for the Host server and the provider-neutral web capability. Use `HTTP` only for the direct fetch provider. +- Keep `E2B`, not `E2B sandbox`, as the package and context name. +- Keep MCP, Todo, directory picker, app boot, base bundle, web-app bundle, and CLI names. + +## Alternatives considered + +**Keep the current names and add a glossary.** Rejected. A glossary cannot make `BashExecutor` truthful when PowerShell implements it, or make `ToolRegistry` disclose that it enforces and executes tools. The identifier must carry the useful distinction. + +**Prefix every npm package with its group.** Rejected. Flat npm names do not need a copy of the directory tree. A mechanical prefix adds length without explaining the package role. + +**Call the whole repository an SDK.** Rejected. The project is an agent harness. SDK is the supported JSON-RPC client/server stack used by Python and TypeScript clients. Two meanings make package names and product prose ambiguous. + +**Use `Service` for every Cordis service class.** Rejected. Cordis inheritance is an implementation fact. The class name must tell callers whether the object registers, stores, resolves, controls, or runs work. + +**Use `Runtime` as the standard replacement for `Service`.** Rejected. `Runtime` is correct only when the object owns live execution or lifecycle. Registries, stores, directories, controllers, resolvers, engines, and configuration objects keep their sharper roles. + +**Prefer the shortest possible name.** Rejected. Short is useful only after scope is clear. `PermissionPresetSettingsController` keeps `Preset`; `JobId` is short because `Job` already carries the domain; `BgTaskId` is short but cryptic. + +**Use broad names for possible future features.** Rejected. Name the stable current role. A future boundary change can rename the object again before release or use a new proposal after release. Vague names charge every current reader for an unbuilt future. + +**Rename `dsh-compact-basic` to `dsh-compaction-llm`.** Rejected. `LLM` adds no distinction in the current backend family. `basic` is less ambitious and does not claim an algorithm that does not exist. + +**Rename session projections to reducers.** Rejected. Reduction is how a projection is built. The package also owns the read-model value, cache, and lookup contract. + +**Rename the persistent Bash tool to `bash-terminal`.** Rejected. That name collides with the terminal-session family. Moving `tool-bash-persistent` under `shell/` fixes its home while its current name continues to distinguish it from the one-shot Bash tool. + +**Rename or split boundaries while applying the ledger.** Rejected. Reviewers must be able to see that behavior did not change. A real boundary defect needs its own proposal, tests, and consequences. + +**Keep aliases for old names.** Rejected. No released consumer needs them. Aliases would preserve two vocabularies and make the first release carry a migration that never had a user. + +## Acceptance criteria + +- Every mapping in the ledger is applied, or this proposal is amended before implementation to explain a changed decision. +- Each family has one public vocabulary. No compatibility package, re-export alias, duplicate `ctx` key within one Cordis context, dual plugin id, dual event id, old tool alias, or fallback parser remains. +- The change is rename-only. Runtime behavior, package boundaries, defaults, policy, durable semantics, and model behavior stay equivalent except where an identifier is itself visible. +- Package directories, npm names, imports, manifests, TypeScript references and paths, Cordis config, plugin ids, service keys, events, tools, RPC names, persisted names named by the ledger, fixtures, snapshots, examples, generated catalogs, and current prose agree with the new vocabulary. +- Current implemented Agent Notes are updated with factual name and path changes when the implementation lands. The package-regrouping note records the new group inventory and package targets, the SDK removal note does not call the repository an SDK, and the timeout-policy note records the new package-name rationale. Notes whose architectural decision remains current are not rewritten into new decisions. +- The paired package-creation guide contains the role-word contract, `packages/AGENTS.md` links to it, the terminology table records the chosen words and `Typert` spelling, and root project prose calls the product DeepSeek Harness rather than DeepSeek Harness SDK. +- The removed SDK project toolchain stays absent. +- Focused tests cover each renamed family; source-plane typecheck, build, package hygiene, generated-reference gates, snapshots affected by visible identifiers, translation pairing, `doc-sync`, and lint pass on the complete implementation. +- The proposed note moves to `implemented/` only after the full ledger and documentation contract are true. Its implementation form describes the final state, not a migration checklist. + +## Risks + +The sweep changes many imports, paths, configuration strings, generated references, and model-visible names. A missed string can compile but fail at load or replay time. Implementation must use both type-directed edits and exact old-name searches across source, config, tests, fixtures, docs, and generated inputs. + +Concurrent work will conflict with moved paths and renamed symbols. This is temporary pre-release cost. Splitting the implementation by coherent family can reduce review and merge conflicts, but each family must remain atomic. + +Some names will become longer. The extra word is intentional when it prevents a false claim about authority or mechanism. Long names are still a failure when every word does not constrain the role. + +The role words can be applied mechanically if reviewers check suffixes without checking behavior. The guide must keep the direct tests in this note: inspect what callers do, what lifetime the object owns, and what failure or policy it controls. + +Old on-disk names, wire values, tool names, and configuration entries named in the ledger will stop working. This is accepted before release. The implementation must fail clearly on stale configuration where the owning parser can identify it; it must not silently accept both forms. diff --git a/.agents/notes/proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.zh.md b/.agents/notes/proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.zh.md new file mode 100644 index 0000000000..9879316d20 --- /dev/null +++ b/.agents/notes/proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.zh.md @@ -0,0 +1,370 @@ +# Agent Note: 仓库命名约定与预发布重命名清单 + +Status: proposed + +[English](2026-08-11-repository-naming-contract-and-rename-ledger.md) | 中文 + +## 问题 + +仓库的发展速度超过了部分名称的演进速度。一些包名仍在描述最初的实现,而非所提供的能力。若干类即使实际承担注册表、运行时、引擎、控制器或解析器的职责,名称仍使用 `Service`。部分 `ctx` 键以单数命名注册表,却以复数命名单个引擎。还有一些提供方明明通过可替换的文件系统或子进程服务工作,可以在另一执行环境中运行,名称却使用 `local`。 + +这些名称并非无关紧要。名称会告诉贡献者一项职责从哪里开始、到哪里结束。`Store` 表示数据访问。`Registry` 表示注册与查找。`Runtime` 表示实时执行和生命周期。如果同一个词同时表示这三者,调用方就必须阅读实现,才能判断哪个对象拥有策略、工作或状态。 + +仓库还在两种含义下使用 `SDK`。受支持的 Python 和 TypeScript 客户端使用 JSON-RPC SDK 协议。项目整体是 DeepSeek Harness,而不是 SDK 项目。已移除的 SDK 项目工具链使宽泛的含义失去依据,但现有文案和名称仍保留了部分旧用法。 + +这是首次发布带标签版本之前,最后一个能够低成本完成仓库级重命名的时点。保留含义不清的名称,会让偶然形成的词汇变成兼容性约定。 + +## 提案 + +在首次发布带标签版本之前,应用本说明中的重命名清单。只更改名称,不得在重命名变更中改变包职责、服务边界、行为、默认值或数据模型。如果某个名称暴露出不合理的边界,应另写一份 proposed Agent Note,专门提议边界变更。 + +每组获准的系列重命名都必须原子完成。清单点名某一表面时,应一并更新其目录、NPM 包名、导入、Cordis 插件名称、`ctx` 键、公开类型、直接耦合的事件或工具标识符、配置、测试、fixture(测试前置数据)、示例、生成的参考资料以及当前文档。不得留下别名、兼容包、重复的服务键、双重事件名称或回退解析器。仓库尚未发布,旧名称应被拒绝。 + +实现可以拆分为多个 PR(Pull Request),以保持评审可行。任何一个 PR 都不得让同一系列同时存在两套公开词汇。最终状态必须满足完整清单。 + +### `SDK` 只表示一件事 + +`SDK` 表示受支持的 Python 和 TypeScript SDK 所使用、基于 JSON-RPC 的客户端/服务器协议。保留 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol` 和协议身份 `deepseek-harness-sdk-runtime`。将 JSON-RPC 服务器重命名并纳入该系列。不得将 DeepSeek Harness 本身称为 SDK,也不得恢复已移除的项目生成器、启动器、辅助工具或启动器遥测包。 + +如果本提案获准,它将部分取代三项现行决策。它会替换[包重新分组决策](../../implemented/architecture/2026-07-29-package-regrouping.md)中保留的 `bash/`、`pty/` 和 `self-modification/` 组名,以及两项暂定包名。它只替换[移除 SDK 项目工具链](../../implemented/simplification/2026-08-11-remove-sdk-project-toolchain.md)中将整个仓库称为 SDK 的说法;后者仍负责说明删除范围和保留的运行时 SDK。它只替换[工具调用超时策略](../../implemented/architecture/2026-07-07-tool-call-timeout-policy.md)中的包名理由;超时机制及其 `guard/timeout-policy/` 归属保持不变。 + +如果其他已实现说明中的包、路径或类型被重命名,而其边界和理由保持不变,则本提案不会取代这些说明。实现阶段只更新其中的事实名称。只有本提案改变既有决策时才添加双向链接;尚未获准的提案不会让现行的已实现说明提前描述未交付名称。 + +### 按实际职责命名 + +使用常见且具体的名词。名称应描述稳定职责,而不是最初的实现、当前目录或未来可能出现的扩展。不得添加不传递任何信息的词。不得为了缩短名称而删除用于限定作用域的词。 + +接口包以能力命名。实现包增加机制、协议、环境或供应商限定词,以区分不同实现。只有同主机执行属于约定时,才能使用 `local`。如果提供方只是通过可替换的 `ctx.fs` 读取看似本地的路径,或通过可替换的 `ctx.subprocess` 启动工作,就不得使用该词。 + +如果对象是单个引擎、运行时、策略、控制器、解析器、存储或当前配置,使用单数 `ctx` 键。如果对象是注册表,或服务拥有多个具名成员,使用复数键。类的职责和键的单复数必须一致。复数键本身不能证明对象是注册表;应由其操作和所有权决定。 + +仅当没有更精确的职责词能够如实描述对象时,才使用 `Service`。`GoalService` 和 `SessionTitleService` 是保留的有效名称,因为它们各自拥有领域服务,其工作无法准确归约为存储、注册或单一执行机制。 + +### 职责词即约定 + +| 词 | 适用场景 | 不适用场景 | +|---|---|---| +| `Controller` | 对象接受命令或用户意图,并更改一项已有的领域状态或呈现状态。它协调有界的状态转换。 | 对象执行任意工作、管理一组提供方,或仅将值转换为显示形式。 | +| `Store` | 对象拥有一组数据,主要对这些数据提供创建、读取、更新、删除、快照或订阅操作。 | 对象验证状态机、行使裁决权、分派工作、决定提供方优先级,或协调多个领域。类内部存在映射并不会让该类成为存储。 | +| `Directory` | 对象公开条目,供发现或选择。消费方会查询有哪些选项,并读取其元数据。 | 生产方可向其中注册任意实现,或调用方通过它执行工作。目录可以由注册表支撑,但两者的对外职责并不相同。 | +| `Presenter` | 对象只负责将领域值或工具参数转换为渲染意图。它不拥有 I/O、订阅、变更或生命周期。 | 对象读取服务、更改状态或控制工作运行时机。这些职责属于控制器或运行时。 | +| `Registry` | 对象拥有一组动态的具名注册项。它定义查找规则、重复项或优先级规则、注册生命周期和资源释放。 | 调用方的主要约定是分派、执行、取消、策略执行或编排。运行时可以在内部包含注册表。 | +| `Runtime` | 对象运行实时工作。它跨调用拥有分派、取消、提供方协调或操作生命周期。 | 对象只存储记录、返回目录、解析单个值或保存配置。`Runtime` 不是 `Service` 的通用替代词。 | +| `Resolver` | 对象根据所提供的输入计算或定位一个答案,通常不拥有答案的生命周期。 | 对象拥有可变集合或长时间运行的执行生命周期。 | +| `Binder` | 对象将一个已声明接口附加到调用方的上下文或生命周期,并返回绑定后的值。 | 对象以集合形式拥有绑定值、控制其领域状态,或仅转换数据。 | +| `Engine` | 对象实现领域算法或有状态执行模型,例如工作流、压缩或查询求值。 | 对象只选择提供方,或跨协议边界转发请求。 | +| `Policy` | 对象决定允许、选择、限制或观察什么。 | 对象执行决策所允许的机制。策略和执行器必须分别命名。 | +| `Executor` | 对象在一项能力内运行明确的请求或已解析的规范。 | 对象拥有宽泛的应用生命周期或提供方目录。 | +| `Gateway` | 对象适配进程、网络、RPC 或 API 边界,并在两侧之间转换。 | 对象只注册同进程服务或存储元数据。 | +| `Provider` | 对象为一项能力定义提供一种实现。如果可以存在多个提供方,应增加机制或供应商限定词。 | 对象是能力定义、提供方注册表或面向消费方的运行时。 | +| `Backend` | 对象在已定义接口之后,实现可替换的底层持久化、传输或执行后端。 | 对象是面向用户的服务,或只是对某个实时对象返回的引用。 | +| `Handle` | 该值是对一个实时资源的引用,并控制或观察该资源。 | 对象创建并管理整个资源池。不得使用 `Owner` 或含义模糊的 `Resource`;如果 `Handle` 或更精确的管理职责合适,就应采用后者。 | +| `Config` | 对象拥有一个已解析的配置值,或一份边界严格受限的配置记录及其更新约定。 | 对象存储通用集合、执行工作或公开不相关的设置。 | +| `Service` | 对象拥有一项职责内聚的领域服务,且以上更精确的职责词都无法如实描述其职责范围。 | 仅因为类继承自 Cordis `Service` 而使用该名称,或因为确定真正的职责需要进一步思考。 | + +实用判断方式很直接。如果调用方主要调用 `register()` 并收到资源释放函数,应使用 `Registry`。如果调用方主要调用 `run()`、`dispatch()`、`cancel()` 或 `execute()`,应使用 `Runtime`、`Engine` 或 `Executor`。如果调用方主要浏览选项,应使用 `Directory`。如果对象主要将一份规范绑定到调用方拥有的上下文和生命周期,应使用 `Binder`。如果对象只将领域数据映射为 UI 数据,应使用 `Presenter`。如果它还会更改状态,就不是呈现器。 + +### 使用能够补充信息的限定词 + +如果协议或方言名称能够区分实现,就应保留。实现依赖相应机制时,保留 `Bash`、`Pwsh`、`JSON-RPC`、`SQLite`、`JSONL`、`OpenTelemetry`、`Claude Code` 和 `E2B`。每个当前后端都已使用 LLM(大语言模型)seam 时,不要在压缩后端名称中加入 `LLM`;在出现更具体的算法名称之前,`basic` 才是如实且中性的名称。 + +不得虚构 `process sandbox` 概念。当前 `sandbox` 系列已经准确命名其产品职责,获准的整体重命名不会改变该边界。 + +PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`、`Llm`、`JsonRpc` 和 `ApiProxy`。在文案和适用的包名中使用惯例规定的全大写形式:UI、LLM、JSON-RPC 和 API。`Typert` 是标识符和文案中的唯一准确产品拼写;不得写成 `TypeRT`、`TypeRt`,也不得对 `Typert` 作其他内部拆分。 + +不得为了避免重复而删除有意保留的供应商限定词。`dsh-subagent-dsh-sdk` 表示 DeepSeek Harness SDK 提供方,可避免与其他 SDK 混淆。其私有类改名为 `SdkSubagentProvider`,因为类名还需要说明它提供什么。 + +### 将规则写入项目文档 + +重命名实现合入时,应将完整的职责词约定加入配对的包创建指南 `docs/cookbook/adding-a-package.md`。在 `packages/AGENTS.md` 中添加一条简短规则并链接到该指南。更新术语表和根项目说明,使 `SDK` 和 `Typert` 各自只有一种含义。Agent Note 负责记录理由和被否决的替代方案;指南负责记录贡献者应遵循的规则。 + +## 重命名清单 + +以下表格列出公开名称和仓库级名称。引用相同职责的私有局部变量也应采用新词汇。若宽泛替换并不正确,清单会明确指出保留的底层名称或产品可见名称。 + +### 运行时 SDK + +| 当前名称 | 提议名称 | 理由 | +|---|---|---| +| `@deepseek-ai/dsh-jsonrpc` | `@deepseek-ai/dsh-sdk-jsonrpc-server` | 它是 SDK 协议的服务器端。单独使用 `jsonrpc` 只说明编码;`sdk-jsonrpc-server` 则同时说明所属系列、机制和职责。 | +| `HarnessSdkServer` | `HarnessSdkJsonRpcServer` | 该类是 JSON-RPC 服务器的一种实现,并不代表所有可能的 SDK 服务器。 | + +保留 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol` 和 `deepseek-harness-sdk-runtime`。排除 `@deepseek-ai/create-sdk`、`@deepseek-ai/dsh-scripts`、`@deepseek-ai/dsh-helper` 和 `@deepseek-ai/dsh-telemetry`;单独的移除决策负责删除这些包及其支撑依赖图。 + +### Shell 与终端 + +| 当前名称 | 提议名称 | 理由 | +|---|---|---| +| `packages/bash/` | `packages/shell/` | 该组包含方言无关的执行器 seam、Bash 和 PowerShell 实现、环境支持以及 shell 工具。 | +| `@deepseek-ai/dsh-bash`, `ctx.bash` | `@deepseek-ai/dsh-shell`, `ctx.shell` | PowerShell 已经实现该 seam。此项能力是 shell 执行,而不是 Bash。 | +| 方言无关的 `BashExecutor`、`BashExecRequest`、`BashExecSpec`、`BashProcess`、`BashRunResult`、`BashSandboxInfo`、`BashProcessRead` 和 `BashProcessStatus` 名称 | 对应的 `Shell*` 名称 | 这些类型横跨 Bash 和 PowerShell 实现。描述 Bash 语法或行为的叶层类型保留 `Bash`。 | +| `BASH_SETTINGS_NAMESPACE`,设置命名空间 `bash` | `SHELL_SETTINGS_NAMESPACE`,设置命名空间 `shell` | 两个 shell 提供方都注册这项由能力拥有的设置分区。常量和持久化命名空间必须使用能力名称。 | +| `@deepseek-ai/dsh-bash-env`, `ctx.bashEnv`, `BashEnvRegistry` | `@deepseek-ai/dsh-shell-env`, `ctx.shellEnv`, `ShellEnvRegistry` | Bash 和 PowerShell 工具共享该环境注册表。 | +| `docs/subsystems/bash.md` | `docs/subsystems/shell.md` | 该子系统页面记录方言无关的能力。 | +| `packages/pty/` | `packages/terminal/` | 该包系列负责持久终端会话。原始 PTY 分配仍位于子进程层。 | +| `@deepseek-ai/dsh-pty`, `ctx.pty`, `PtyService` | `@deepseek-ai/dsh-terminal`, `ctx.terminals`, `TerminalSessionService` | 调用方管理多个具名终端会话,而不是通过该服务分配原始 PTY。 | +| 公开的高层 `Pty*` 会话和后端名称 | `Terminal*` 名称 | 公开抽象是终端会话。保留底层 `SubprocessTerminal*` 名称,因为它们已经说明底层机制。 | +| `@deepseek-ai/dsh-pty-local`, `LocalPtyBackend` | `@deepseek-ai/dsh-terminal-bash`, `BashTerminalBackend` | 该提供方依赖 Bash 提示符和 shell 行为。`local` 隐藏了实际方言。 | +| `@deepseek-ai/dsh-tool-pty` | `@deepseek-ai/dsh-tool-terminal` | 面向模型的工具已使用 `terminal_*`;包应采用相同的产品名词。 | +| `packages/pty/tool-bash-persistent` | `shell/tool-bash-persistent/` | 该工具是 Bash 工具,应与 shell 工具放在一起。保留其 NPM 名称:`persistent` 将它与一次性 `bash` 区分开来,而 `bash-terminal` 会混淆产品工具与终端会话系列。 | +| `docs/subsystems/pty.md` | `docs/subsystems/terminal.md` | 该页面记录终端会话,而不是原始 PTY 分配。 | + +保留 Bash 和 PowerShell 专用的叶层包、插件 id、类型和工具。这些方言名称准确无误。 + +### 语言服务器与作业 + +| 当前名称 | 提议名称 | 理由 | +|---|---|---| +| `@deepseek-ai/dsh-lsp-local` | `@deepseek-ai/dsh-lsp-stdio` | 该提供方通过可替换的文件系统和子进程服务,以 stdio 传输 LSP。它不一定在本地运行。 | +| `packages/tasks/` | `packages/jobs/` | 该系列负责脱离前台运行的工具作业。`jobs` 简短,并可避免与用户任务或 todo 概念冲突。 | +| `@deepseek-ai/dsh-tasks`, `ctx.tasks`, `TaskService` | `@deepseek-ai/dsh-jobs`, `ctx.jobs`, `JobRegistry` | 该服务注册、拥有、观察、等待并取消多个后台作业。它是注册表,而不是通用任务服务。 | +| 公开的 `TaskId`、`TaskKindMap`、`TaskStart`、`TaskHooks`、`TaskOutcome`、`TaskSnapshot`、`TaskRead` 和 `TaskDoneListener` 名称 | 对应的 `Job*` 名称 | 这些类型属于重命名后的作业领域。`JobId` 比 `BackgroundTaskId` 或 `BgTaskId` 更短、更清晰。 | +| `@deepseek-ai/dsh-tasks-local`, `LocalTaskService` | `@deepseek-ai/dsh-jobs-local`, `LocalJobRegistry` | 这是作业注册表的进程内提供方。此处的 `local` 有明确含义,因为作业和回调都存在于同一进程。 | +| `@deepseek-ai/dsh-tool-tasks` | `@deepseek-ai/dsh-tool-jobs` | 消费方控制作业注册表,应使用相同的领域名词。 | +| `task_output`, `task_list`, `task_kill` | `job_output`, `job_list`, `job_kill` | 这些模型工具操作的是作业,而不是用户任务。`run_in_background` 返回 `JobId`。 | +| `@deepseek-ai/dsh-client-ui-task`、`client/ui-task/` | `@deepseek-ai/dsh-client-ui-jobs`、`client/ui-jobs/` | 该客户端包呈现后台作业集合,而不是一项用户任务。 | +| `TaskView`、线路帧 `session/tasks`、`tasksBySession` | `JobView`、线路帧 `session/jobs`、`jobsBySession` | 浏览器约定及其镜像应采用与注册表和工具相同的作业领域名称。 | +| `docs/subsystems/tasks.md` | `docs/subsystems/jobs.md` | 该子系统页面必须采用公开的作业词汇。 | + +保留基础 LSP 包、`ctx.lsp`、LSP 协议类型和 LSP 工具。该 seam 有意公开语言服务器语义;错误的只有提供方限定词。 + +### 输入触发器、工具呈现、权限预设和用户问题 + +| 当前名称 | 提议名称 | 理由 | +|---|---|---| +| `@deepseek-ai/dsh-client-ui-slash`, `ui-slash/` | `@deepseek-ai/dsh-client-ui-input-trigger`, `ui-input-trigger/` | 客户端处理 `/`、`@`、键盘仲裁、候选菜单和程序化启动,并非只处理斜杠命令。 | +| `ctx.slash`、`SlashService`、`SlashController`、`SlashSource` | `ctx.inputTriggers`、`InputTriggerService`、`InputTriggerController`、`InputTriggerSource` | 这些名称覆盖所有受支持的触发器,并保留现有的服务、控制器和来源职责。耦合的区域设置和公开类型名称也改用 `InputTrigger`。 | +| `@deepseek-ai/dsh-agent-tool-mode`,插件 `tool-mode` | `@deepseek-ai/dsh-agent-tool-presentation`,插件 `tool-presentation` | 该插件改变工具向模型呈现的方式,而不改变执行行为。保留局部 `Config.mode` 和 `ToolPresentationMode`。 | +| `packages/interaction/permission/` | `packages/interaction/permission-presets/` | 该包拥有沙箱与审批设置的具名组合,而不负责执行权限。 | +| `@deepseek-ai/dsh-permission`, `ctx.permission`, `PermissionService` | `@deepseek-ai/dsh-permission-presets`, `ctx.permissionPresets`, `PermissionPresetService` | 该服务选择并持久化预设。沙箱和审批服务负责执行结果。 | +| `@deepseek-ai/dsh-client-ui-permission` | `@deepseek-ai/dsh-client-ui-permission-presets` | UI 编辑和选择权限预设。 | +| `docs/subsystems/permission.md` | `docs/subsystems/permission-presets.md` | 该页面记录预设选择,而不是权限执行。 | +| `@deepseek-ai/dsh-user-interaction`, `user-interaction/` | `@deepseek-ai/dsh-user-questions`, `user-questions/` | 该 seam 仅支持批量问题和答案。审批、命令和目录选择属于其他交互 seam。 | +| `ctx.userInteraction`, `UserInteractionService`, `UserInteractionProvider`, `UserInteractionError` | `ctx.userQuestions`, `UserQuestionService`, `UserQuestionProvider`, `UserQuestionError` | 这些名称说明唯一受支持的交互形式。保留 `AskUserQuestion*`、`ask_user_question` 工具和 `@deepseek-ai/dsh-tool-ask-user`。 | +| `docs/subsystems/user-interaction.md` | `docs/subsystems/user-questions.md` | 该页面只记录问题和答案。 | + +保留 `/permission`、`permissions` 投影、`permission` 设置命名空间和 `permission/preset`;它们都是准确的产品词汇或持久化词汇。保留完整名称 `PermissionPresetSettingsController`。删除 `Preset` 会去掉限定其权限的词。另行制定提案以移除 `both` 工具呈现模式;本次重命名不移除行为。 + +### Typert、API 网关与工具 + +| 当前名称 | 提议名称 | 理由 | +|---|---|---| +| `packages/typert/type-meta/`, `@deepseek-ai/dsh-type-meta` | `typert/protocol/`, `@deepseek-ai/dsh-typert-protocol` | 该包拥有 Typert Remote 协议、装饰器、绑定、编解码器、查找逻辑和上下文约定。它不是通用类型元数据。 | +| 协议包中的 `GatewayService` | `TypertRemoteService` | 该基类标记要导出为 Remote 的同进程服务。它不是 API 网关。 | +| `bindTypeRTGateway`、`typertGateway` 绑定 | `bindTypertRemote`、`typertRemote` | 这些绑定公开 Typert Remote 服务,而非具体的 API 网关服务。 | +| 公开的 `TypeRT*` 标识符和小驼峰形式的 `typeRT*` 标识符 | `Typert*` 和 `typert*` | `Typert` 是唯一规范的产品拼写。 | +| 协议接口 `TypeRTService` | `TypertRegistryContract` | 该协议拥有的接口是现有具体类 `TypertRegistry` 所实现的依赖倒置接口。不同的后缀可避免导入和声明冲突。 | +| `ToolRegistry` | `ToolRuntime` | 该类拥有呈现、审批与防护策略、分派、取消、验证、终结和观察。注册只是内部组成部分。 | +| `ToolRegistryScheduler`, `TOOL_REGISTRY_SCHEDULER` | `ToolRuntimeScheduler`, `TOOL_RUNTIME_SCHEDULER` | 调度器控制运行时分派,而不是注册。 | + +保留 `@deepseek-ai/dsh-tools` 和 `ctx.tools`。保留 `@deepseek-ai/dsh-api-gateway`、其 `gateway/` 目录、`ctx.typertGateway` 以及 `TypertGatewayService`;该服务是真正的 API 网关。其内部的 `TypeRT*` 标识符仍应遵循 `Typert*` 拼写规则。 + +### 工作区指令、遥测、身份和启动环境 + +| 当前名称 | 提议名称 | 理由 | +|---|---|---| +| Host `ctx.workspace` | Host `ctx.workspaces` | `WorkspaceRegistry` 拥有多个工作区。复数键与注册表职责一致。现有 Client `ctx.workspaces` 在独立的 Cordis 上下文中运行,因此共享拼写是有意设计的,不会在运行时冲突。保留 `@deepseek-ai/dsh-workspace`、`WorkspaceRegistry`、`Workspace` 和 `workspace.*` 协议名称。 | +| `@deepseek-ai/dsh-workspace-context`, `context/workspace-context/` | `@deepseek-ai/dsh-agent-instructions`, `context/agent-instructions/` | 该包为 agent(智能体)加载分层的 `AGENTS.md` 和 `CLAUDE.md` 文件。它并非通用工作区上下文。 | +| 插件名称和持久来源名称 `workspace-context` 与 `workspace-instructions` | `agent-instructions` | 记录的来源是一类具体的 agent 指令。以 `AgentInstruction*` 替换公开的 `WorkspaceInstruction*` 名称。该术语不包括系统消息、开发者消息或用户消息。 | +| `ctx.telemetry`、抽象类 `Telemetry` | `ctx.sessionTelemetry`、`SessionTelemetryBackend` | 该服务捕获会话账本遥测,并交给报告后端。它不是仓库级指标或追踪服务。 | +| `TelemetryBackend` | `SessionTelemetrySink` | 该底层接收已发出的记录。`Sink` 用于将它与协调型后端服务区分开。 | +| `TelemetryCoordinator`、`TelemetryRecord`、`TelemetrySeverity`、`TelemetrySharingStatus` 和 `TelemetryCapture` | 对应的 `SessionTelemetry*` 名称 | 这些公开类型只属于会话遥测。 | +| `telemetry/record` | `session-telemetry/record` | 事件名称必须说明所属领域。 | +| `TelemetryOtel`、`TelemetryMode`,插件 `telemetry-otel` | `OpenTelemetrySessionBackend`、`SessionTelemetryMode`,插件 `session-telemetry-otel` | 提供方名称同时说明 OpenTelemetry 机制和会话作用域。保留包名 `dsh-session-telemetry` 和 `dsh-session-telemetry-otel`。 | +| `docs/subsystems/telemetry.md` | `docs/subsystems/session-telemetry.md` | 该页面记录会话遥测,而不是仓库级可观测性。 | +| `session/user-id/`, `@deepseek-ai/dsh-user-id` | `session/anonymous-user-id/`, `@deepseek-ai/dsh-anonymous-user-id` | 该值是随机关联 id,并非经过身份验证的用户身份。 | +| `USER_ID_FILE_NAME`、`.userid`,反馈标签 `User` | `ANONYMOUS_USER_ID_FILE_NAME`、`.anonymous-user-id`,反馈标签 `Anonymous user` | 文件和 UI 不得暗示账户身份。保留现有 `AnonymousUserId` 函数和标准 OTel 属性 `user.id`。 | +| `util/environment/`, `@deepseek-ai/dsh-environment` | `util/launch-environment/`, `@deepseek-ai/dsh-launch-environment` | 该包在启动时捕获一份不可变的分层快照。它不是通用环境 API。 | +| 公开的 `Environment*`、`createEnvironmentSnapshot`、`environmentOf`、`DSH_ENVIRONMENT_KEY` | `LaunchEnvironment*`、`createLaunchEnvironmentSnapshot`、`launchEnvironmentOf`、`DSH_LAUNCH_ENVIRONMENT_KEY` | 这些名称说明快照的生命周期和用途。 | +| `ctx.launcherEnvironment` | `ctx.launchEnvironment` | 该值描述应用启动,而不只描述启动器组件。保留来源标签 `process`、`project-env` 和 `user-env`。 | + +### 日程、工作流、目标与压缩 + +| 当前名称 | 提议名称 | 理由 | +|---|---|---| +| `ScheduleOwner` | `ScheduleRuntime` | 该逐 agent 对象运行实时定时器、持久化投影、分派、空闲等待和资源释放。`Owner` 没有说明这一执行职责。耦合的私有 `owner*` 名称也改用 `runtime*`。 | +| `WorkflowService`, `ctx.workflows` | `WorkflowEngine`, `ctx.workflowEngine` | 一个引擎负责解析并执行工作流程序。复数键错误地暗示这是注册表。保留 `@deepseek-ai/dsh-workflow` 以及工作流事件和工具。 | +| `@deepseek-ai/dsh-workflow-workerthread`, `WorkerWorkflowEngine` | `@deepseek-ai/dsh-workflow-worker-thread`, `WorkerThreadWorkflowEngine` | `worker thread` 是准确的 Node 机制,仓库拼写要求使用完整单词。 | +| `@deepseek-ai/dsh-goal-session`, `goal/goal-session/` | `@deepseek-ai/dsh-goal-round-driver`, `goal/goal-round-driver/` | 该插件驱动同一会话内的 Goal Rounds。它既不存储目标,也不定义会话。保留 `GoalService`、目标来源、事件和约定。 | +| `packages/compact/` | `packages/compaction/` | 该组是以名词命名的领域系列。`compact` 仍作为面向用户的命令动词。 | +| `@deepseek-ai/dsh-compact`, `ctx.compact`, `CompactService` | `@deepseek-ai/dsh-compaction`, `ctx.compaction`, `CompactionEngine` | 该对象运行压缩(compaction)算法和生命周期。它是引擎,而不是通用服务。 | +| `compact/*` 事件和公开领域前缀 | `compaction/*` | 事件和领域类型使用名词形式。保留动词形式的操作,例如 `compactNow`、`compactRegion` 和 `compactIfNeeded`。 | +| `@deepseek-ai/dsh-compact-basic`、`BasicCompactService`、公开的 `BasicCompact*` | `@deepseek-ai/dsh-compaction-basic`、`BasicCompactionEngine`、对应的 `BasicCompaction*` | `basic` 朴素但准确。`compaction-llm` 没有增加信息,因为当前实现系列已使用 LLM。 | +| `@deepseek-ai/dsh-compact-tool-result-prune`, `ToolResultPruneService`, `ctx.toolResultPrune` | `@deepseek-ai/dsh-compaction-tool-result-pruner`, `ToolResultPruner`, `ctx.toolResultPruner` | 该插件是剪除工具结果的执行主体。名词 `pruner` 说明了这一职责。 | + +保留 `/compact`、命令包,以及维持压缩定义包与提供方包分离的既有决策。本次重命名只改变词汇,不改变该包边界。 + +### 设置、凭据、客户端模块和较小的核心职责 + +| 当前名称 | 提议名称 | 理由 | +|---|---|---| +| 抽象类 `Settings` | `SettingsProvider` | 该类通过可替换能力提供设置。保留包、键和事件。 | +| `@deepseek-ai/dsh-settings-local`, `SettingsLocal` | `@deepseek-ai/dsh-settings-file`, `FileSettingsProvider` | 该实现通过文件系统 seam 以文件为后端。`file` 说明机制,`local` 则不能。 | +| 抽象类 `Credentials` | `CredentialProvider` | 该类解析凭据引用。保留包名、键和事件。 | +| `CredentialsLocal` | `LocalCredentialProvider` | 该提供方读取宿主进程和 `.env` 状态,因此本地执行属于其约定。 | +| `ClientModuleHostService`, `ctx.clientModuleHost` | `ClientModuleRegistry`, `ctx.clientModules` | 该服务拥有多个已注册的客户端模块。保留包和浏览器端的 `ClientModuleLoader`。 | +| `AgentDefaultModelService` | `AgentDefaultModelConfig` | 该对象存储一项默认模型选择。它不运行服务,也不是通用注册表。保留其包、键、设置命名空间和类型。 | +| `SessionReferenceService`, `ctx.sessionReferences` | `SessionReferenceResolver`, `ctx.sessionReferenceResolver` | 它从 URI 或输入解析一个会话引用,并不拥有引用集合。 | +| `SessionQueryService`, `SessionQuerySqlite` | `SessionQueryEngine`, `SqliteSessionQueryEngine` | 这些类执行查询模型及其 SQLite 实现。保留包名、键和工具。 | +| 宿主命令包中的 `CommandService` | `CommandRuntime` | 该对象跨实时调用注册并执行宿主命令。保留其包、键、类型和事件。 | +| `TokenMeterService` | `TokenMeter` | 该对象测量 token 用量。`Service` 没有补充作用域信息。 | +| `LlmService` | `LlmRuntime` | 该对象选择提供方并运行实时模型请求。保留包、键、适配器和事件。 | + +### Host Web 服务器、会话数据与代码执行 + +| 当前名称 | 提议名称 | 理由 | +|---|---|---| +| `HttpServerService`, `ctx.httpServer` | `WebServer`, `ctx.webServer` | 该服务器拥有 HTTP 路由和 WebSocket 升级路由。`Web` 可以同时涵盖两者;此处的 `Http` 作用域过窄。保留 `packages/host/webserver`、`@deepseek-ai/dsh-host-webserver`、`WebRoute` 和 `WebUpgradeRoute`。 | +| 文档子系统标签 `http-server` | `web-server` | 子系统标签必须与服务采用相同作用域。 | +| `SessionPersistenceJsonl` | `JsonlSessionPersistence` | 将实现限定词放在前面,同时完整保留能力职责。 | +| `SessionPersistenceSqlite` | `SqliteSessionPersistence` | 采用与 JSONL 相同的提供方命名顺序。 | +| `@deepseek-ai/dsh-session-title-first-message-llm`,触发周期 `first-message` | `@deepseek-ai/dsh-session-title-first-prompt-llm`,触发周期 `first-prompt` | 触发条件是第一条用户提示词,而不是会话日志中的任意消息。 | +| `@deepseek-ai/dsh-session-title-all-messages-llm`,触发周期 `all-user-messages` | `@deepseek-ai/dsh-session-title-all-prompts-llm`,触发周期 `all-prompts` | 后端根据用户提示词刷新。`all messages` 会错误地包含助手消息和工具事件。 | +| `@deepseek-ai/dsh-code-runtime-worker`, `WorkerCodeRuntime` | `@deepseek-ai/dsh-code-runtime-worker-thread`, `WorkerThreadCodeRuntime` | 该实现使用 Node 工作线程。单独的 `worker` 作用域过宽。 | +| `SubprocessService` | `SubprocessRuntime` | 该服务拥有实时子进程的执行和生命周期。保留其包和键。 | +| `LocalSubprocessService` | `LocalSubprocessRuntime` | 该提供方运行同主机进程和进程树。 | +| `E2BSubprocessService` | `E2BSubprocessRuntime` | 该提供方在 E2B 运行时中运行子进程。 | + +保留完整的会话投影系列和 `SessionProjection*` 词汇。投影是持续维护的读取模型;`Reducer` 只说明其折叠操作,会淡化缓存和查找职责。保留 `SessionTitleService`、检查点策略、持久化包名、时间上下文和 tmux 上下文。 + +### 文件系统、skill、subagent 和 Web 提供方 + +| 当前名称 | 提议名称 | 理由 | +|---|---|---| +| `@deepseek-ai/dsh-fs-policy` | `@deepseek-ai/dsh-fs-observation-policy` | 该包定义哪些文件系统观察可以授权后续操作。它不是完整的文件系统策略或沙箱策略。 | +| `FsPolicyExec` | `FsObservationActor` | 该值表示策略所关联的观察与操作的执行主体。它本身不执行策略。 | +| `SkillService` | `SkillRegistry` | 该服务注册提供方,并从其目录解析 skill(技能)。 | +| `@deepseek-ai/dsh-skill-local`、`LocalSkillProvider`,提供方 id `local` | `@deepseek-ai/dsh-skill-filesystem`、`FileSystemSkillProvider`,提供方 id `filesystem` | 该提供方通过可位于本地或远端的 `ctx.fs` 发现 skill 文件。其机制是文件系统访问,而不是本地性。 | +| `SubagentService` | `SubagentRuntime` | 该服务选择提供方,并拥有实时 spawn、恢复、跟进、取消和结算行为。 | +| `@deepseek-ai/dsh-subagent-spawn`, `SpawnProvider` | `@deepseek-ai/dsh-subagent-spawn-in-process`, `SpawnInProcessProvider` | 该提供方在当前进程内启动子 agent。配置的提供方 id 仍为 `spawn`。 | +| `@deepseek-ai/dsh-subagent-fork`, `ForkProvider` | `@deepseek-ai/dsh-subagent-fork-in-process`, `ForkInProcessProvider` | 该提供方在当前进程内 fork 一个 agent。配置的提供方 id 仍为 `fork`。 | +| `@deepseek-ai/dsh-subagent-inprocess`, `subagent-inprocess/` | `@deepseek-ai/dsh-subagent-in-process-driver`, `subagent-in-process-driver/` | 该包包含通用的进程内驱动逻辑,而不是第三个提供方。 | +| 私有的 `SdkProvider`,位于 `dsh-subagent-dsh-sdk` 中 | `SdkSubagentProvider` | 重复的包限定词是有意保留的,类名还必须说明它通过 SDK 提供 subagent。 | +| `WebService`, `WebServiceConfig` | `WebRuntime`, `WebRuntimeConfig` | 该对象选择提供方并运行实时搜索和抓取操作。保留包、键、提供方包和模型工具。 | +| `@deepseek-ai/dsh-web-fetch-local`、`LocalFetchProvider`、`LocalFetchLimits`,提供方 id `local-http` | `@deepseek-ai/dsh-web-fetch-http`、`HttpFetchProvider`、`HttpFetchLimits`,提供方 id `http` | 该提供方执行直接 HTTP 抓取。`local` 只说明代码恰好在哪里运行,并未说明它提供哪种机制。 | + +保留 `@deepseek-ai/dsh-subagent-dsh-sdk`、其提供方 id `dsh-sdk`、外部 ACP(Agent Client Protocol)、Codex 和 Claude Code 提供方系列、subagent 工具包名、主文件系统包和后端、文件系统工具和事件,以及 skill 徽章和工具包。 + +### 钩子、防护、Plan Mode、扩展与诊断 + +| 当前名称 | 提议名称 | 理由 | +|---|---|---| +| `@deepseek-ai/dsh-hooks-claude`、`ClaudeHookConfig`、`parseClaudeConfig`,方言 `claude` | `@deepseek-ai/dsh-hooks-claude-code`、`ClaudeCodeHookConfig`、`parseClaudeCodeConfig`,方言 `claude-code` | 该钩子桥接面向 Claude Code,而非所有 Anthropic 或 Claude 产品。 | +| `@deepseek-ai/dsh-repeat-tool-guard`,插件/来源 `repeat-tool-guard` | `@deepseek-ai/dsh-repeat-tool-reminder`,插件/来源 `repeat-tool-reminder` | 该插件向模型添加提醒,并不阻止工具调用,也不执行防护决策。 | +| `@deepseek-ai/dsh-timeout-policy` | `@deepseek-ai/dsh-tool-call-timeout-policy` | 完整的 `tool-call` 限定词说明该策略限制的对象,而不会把插件称为面向模型的工具。保留其 `guard/timeout-policy/` 目录和插件 id `timeout-policy`;`packages/*/tool-*` 目录约定仍只适用于注册工具的包。 | +| `PlanModeService` | `PlanModeController` | 该对象控制进入和退出计划模式的状态转换,而不是通用执行运行时。 | +| `packages/self-modification/` | `packages/extensions/` | 该组包含仓库插件检查和挂载工具。`extensions` 说明稳定的包职责,但不声称 agent 会修改自身。保留包名 `tool-cordis` 和仓库插件名称。 | +| `packages/support/` | `packages/test-support/` | 该组仅包含测试基础设施,其路径必须明确说明这一点。 | +| `packages/support/invariants/` | `runtime-diagnostics/invariants/` | 尽管交付预设未包含不变量检查,它们仍可在生产诊断中运行,因此不属于测试支持。 | +| `InvariantService` | `InvariantRegistry` | 该对象拥有已注册的不变量检查。保留 `@deepseek-ai/dsh-invariants` 和 `ctx.invariants`。 | +| `packages/client/test-runtime/` | `packages/test-support/client-runtime/` | 该包是客户端测试基础设施。如果现有 NPM 名称已经说明这一约定,则予以保留。 | + +保留 MCP、Todo、Plan Mode 包、键、事件和工具名称。获准的变更针对控制器类,而不是产品功能。 + +### 实用工具、E2B、Host、组合包、示例与应用 + +| 当前名称 | 提议名称 | 理由 | +|---|---|---| +| `util/paths/`, `@deepseek-ai/dsh-paths` | `util/home-paths/`, `@deepseek-ai/dsh-home-paths` | 这些辅助函数解析 Harness 主目录下的路径,并非通用路径库。已准确说明返回路径的函数名保持不变。 | +| `util/retention/`, `@deepseek-ai/dsh-retention` | `util/output-retention/`, `@deepseek-ai/dsh-output-retention` | 该策略保留命令和工具输出,而不是通用数据保留框架。 | +| `E2BSandboxService` | `E2BRuntime` | 该类创建、复用和释放文件系统与子进程适配器所使用的 E2B 执行环境。它比单个沙箱句柄的职责更广,又比通用所有者更具体。保留 `@deepseek-ai/dsh-e2b`、`ctx.e2b` 和 `e2b/` 组。 | +| `@deepseek-ai/dsh-frontend-static` | `@deepseek-ai/dsh-host-frontend-static` | 该包是提供前端资源的 Host 插件。此前缀可将它与前端应用代码区分开。 | +| `@deepseek-ai/dsh-jsonrpc-demo` | `@deepseek-ai/dsh-sdk-jsonrpc-demo` | 该示例演示通过 JSON-RPC 使用运行时 SDK,属于 SDK 的唯一含义。 | +| `@deepseek-ai/dsh-frontend` | `@deepseek-ai/dsh-web-frontend` | 该应用是 Web 前端。保留其物理目录 `apps/web/`。 | + +保留 atomic-write、brand、native-command、timeout 实用工具、目录选择器、`dsh-base`、`dsh-web-app`、应用启动、CLI(命令行界面)名称,以及 `headless` 包、组合包和示例身份。`headless` 是预期的产品本质,未来也可以支持不止一次性执行。 + +### 客户端运行时与 UI + +| 当前名称 | 提议名称 | 理由 | +|---|---|---| +| `SlotsService` | `SlotRegistry` | 该对象拥有具名 slot 声明和注册项。 | +| `SessionsService` | `SessionRuntime` | 该对象拥有实时客户端会话协调职责,而不是被动的会话列表。 | +| `WorkspacesService` | `WorkspaceRuntime` | 该客户端对象协调实时工作区选择和操作。如果清单未点名更改某个现有 `ctx` 键,则该键保持不变。 | +| `LocaleService` | `LocaleRuntime` | 该对象协调区域设置定义、选择、持久化和变更发布。 | +| `ThemeService` | `ThemeRuntime` | 该对象协调主题、偏好解析、系统感知和变更发布。 | +| `LayoutService` | `LayoutController` | 该对象控制当前 UI 布局状态。 | +| `@deepseek-ai/dsh-client-ui-model` | `@deepseek-ai/dsh-client-ui-model-selection` | 该包控制会话的模型选择。单数 `model` 名称作用域过宽。 | +| `ModelService`, `ctx.models` | `ModelDirectoryResolver`, `ctx.modelDirectories` | 它唯一的公开操作 `directoryFor(sessionId)` 为每个实时会话解析并保留一个目录。它没有注册 API,因此使用 `Registry` 并不准确。每个 `ModelDirectory` 仍是面向消费方的可选模型目录。 | +| `SettingsScopeService` | `SettingsScopeBinder` | 它唯一的操作把一份命名空间规范绑定到调用方的传输层和生命周期,并返回 `SettingsScopeController`。保留 `ctx.settingsScope`;它命名的是单一绑定能力,而不是 scope 集合。 | +| `@deepseek-ai/dsh-client-ui-models` | `@deepseek-ai/dsh-client-ui-settings-models` | 该包拥有 Models 设置面板。保留 `ModelsSettingsStore`;它保存一个具有数据操作和订阅能力的设置视图模型,确实是存储。 | +| `@deepseek-ai/dsh-client-ui-plugin-config`、`client/ui-plugin-config/` | `@deepseek-ai/dsh-client-ui-settings-plugins`、`client/ui-settings-plugins/` | 该包拥有 Plugins 设置分区,而不是通用的插件配置系统。目标名称归入 `ui-settings-*` 系列,并采用该分区的复数产品名。 | +| `PluginConfigSection`、`PluginConfigSectionProps`、`PluginConfigSectionInjected`、`settings.pluginConfig` | `PluginsSettingsSection`、`PluginsSettingsSectionProps`、`PluginsSettingsSectionInjected`、`settings.plugins` | 这些名称描述 Plugins 设置呈现。每张卡片仍编辑一个插件的配置,但该分区本身是设置 UI。 | +| `agent-loop-store.ts`、`bash-store.ts`、`web-search-store.ts` | `agent-loop-card-controller.ts`、`bash-card-controller.ts`、`web-search-card-controller.ts` | 每个模块都导出一个卡片控制器。私有 `SnapshotStore` 字段不会让模块成为存储。 | +| `card-store.ts` | `card-form.ts` | 该模块拥有暂存表单、字段转换和表单操作。它返回的快照存储是呈现适配器,而不是模块的主要职责。 | +| `@deepseek-ai/dsh-client-ui-question` | `@deepseek-ai/dsh-client-ui-user-questions` | UI 呈现用户问题 seam,而不是任意问题领域。 | +| `@deepseek-ai/dsh-client-ui-command`, `ui-command/` | `@deepseek-ai/dsh-client-ui-commands`, `ui-commands/` | 该包呈现并运行一组命令。 | +| 客户端 `ctx.command`、`CommandService`、`CommandServiceContract` | `ctx.commandUi`、`CommandUiRuntime`、`CommandUiContract` | Host 已拥有 `ctx.commands`。该客户端服务是命令发现和执行的 UI 运行时。现有 `CommandUiSpec` 确立了 `Ui` 大小写格式。 | +| `ConversationService` | `ConversationController` | 该对象控制当前对话状态和用户操作。 | +| `InputService` | `SessionInputResolver` | 该接口为一个会话作用域解析输入外观。它既不是全局输入注册表,也不是执行服务。保留 `InputHub` 作为具体中枢,并保留 `ctx.conversation.input` 作为对外接口。 | + +PascalCase 标识符内部使用 `Ui`,不要使用 `UI`。除非清单明确要求重命名,否则保留其余客户端包名。暂时保留已弃用的客户端连接和 Host `ApiProxy` 词汇;API 平面将替换它们,而在计划移除的表面上重命名只会增加改动量。 + +## 明确保留的名称 + +以下经过讨论的名称保持不变,因为当前作用域准确,或重命名会制造虚假概念: + +- 保留完整的 sandbox 系列和 `ctx.sandbox`。不得引入 `processSandbox`。 +- 保留 `@deepseek-ai/dsh-api-gateway`、`ctx.typertGateway` 和 `TypertGatewayService`。 +- 保留会话投影名称。投影并不只是归约函数。 +- 保留 `GoalService`;它拥有目标状态机、裁决权、比较并设置行为、事件和远程操作,不只是存储。 +- 保留 `SessionTitleService`;它的职责是由多个标题提供方共享的领域服务。 +- 保留 `PermissionPresetSettingsController`,即使它很长。每个词都在限定其职责。 +- 保留 `ModelsSettingsStore`;其主要约定是一个具有存储操作的设置数据模型。 +- 保留 `InputHub`;它是支撑 `SessionInputResolver` 的具体中枢。 +- 保留 `dsh-subagent-dsh-sdk` 和提供方 id `dsh-sdk`;重复的限定词可避免歧义。 +- 保留 `headless`;即使运行时以后支持不止一次性使用,该产品身份仍然准确。 +- 保留已弃用的 Host `ApiProxy` 和客户端连接名称,直至 API 替代方案将其移除。 +- Host 服务器和提供方无关的 Web 能力都保留 `Web`。仅直接抓取提供方使用 `HTTP`。 +- 保留 `E2B` 作为包名和上下文名称,不改为 `E2B sandbox`。 +- 保留 MCP、Todo、目录选择器、应用启动、基础组合包、web-app 组合包和 CLI 名称。 + +## 考虑过的替代方案 + +**保留现有名称并添加词汇表。**不予采纳。词汇表无法让由 PowerShell 实现的 `BashExecutor` 名副其实,也无法让 `ToolRegistry` 表明它会执行并强制实施工具策略。标识符本身必须承载有用的区别。 + +**为每个 NPM 包添加所属组前缀。**不予采纳。扁平的 NPM 名称不需要复刻目录树。机械添加前缀只会增加长度,无法解释包的职责。 + +**将整个仓库称为 SDK。**不予采纳。该项目是 agent harness(智能体框架)。SDK 是 Python 和 TypeScript 客户端使用的、受支持的 JSON-RPC 客户端/服务器栈。一词两义会使包名和产品文案产生歧义。 + +**所有 Cordis 服务类都使用 `Service`。**不予采纳。Cordis 继承只是实现事实。类名必须告诉调用方该对象负责注册、存储、解析、控制还是运行工作。 + +**统一使用 `Runtime` 替换 `Service`。**不予采纳。只有对象拥有实时执行或生命周期时,`Runtime` 才正确。注册表、存储、目录、控制器、解析器、引擎和配置对象都应保留更精确的职责名。 + +**优先使用最短的名称。**不予采纳。只有作用域明确之后,简短才有价值。`PermissionPresetSettingsController` 保留 `Preset`;`JobId` 简短,是因为 `Job` 已经表明领域;`BgTaskId` 虽短,却晦涩难懂。 + +**为未来可能出现的功能使用宽泛名称。**不予采纳。应按稳定的当前职责命名。未来若要改变边界,可以在发布前再次重命名对象,或在发布后另写提案。含义模糊的名称会让每位当前读者为尚未构建的未来付出理解成本。 + +**将 `dsh-compact-basic` 重命名为 `dsh-compaction-llm`。**不予采纳。`LLM` 没有在当前后端系列中增加区别。`basic` 意图更克制,也不会声称存在一个实际并不存在的算法。 + +**将会话投影重命名为归约器。**不予采纳。归约只是构建投影的方式。该包还拥有读取模型值、缓存和查找约定。 + +**将持久 Bash 工具重命名为 `bash-terminal`。**不予采纳。该名称与终端会话系列冲突。将 `tool-bash-persistent` 移到 `shell/` 可以纠正其归属位置,同时现有名称仍能将其与一次性 Bash 工具区分开。 + +**应用清单时一并重命名或拆分边界。**不予采纳。评审人必须能够确认行为没有改变。真正的边界缺陷需要独立提案、测试和后果分析。 + +**为旧名称保留别名。**不予采纳。没有已发布的消费方需要这些别名。别名会保留两套词汇,使首次发布携带一项从未有用户需要的迁移。 + +## 验收标准 + +- 应用清单中的每项映射;如果决策发生变化,则必须在实施前修订本提案并说明原因。 +- 每个系列只有一套公开词汇。同一个 Cordis 上下文中不得留下兼容包、重新导出别名、重复的 `ctx` 键、双重插件 id、双重事件 id、旧工具别名或回退解析器。 +- 变更只能重命名。运行时行为、包边界、默认值、策略、持久化语义和模型行为必须保持等价,只有标识符本身可见时除外。 +- 包目录、NPM 名称、导入、manifest(元数据清单)、TypeScript 引用和路径、Cordis 配置、插件 id、服务键、事件、工具、RPC 名称、清单点名的持久化名称、fixture、快照、示例、生成的目录和当前文案都采用新词汇。 +- 实现合入时,以事实性名称和路径变更更新当前处于 implemented 状态的 Agent Note。包重新分组说明应记录新的分组清单和包名目标,SDK 移除说明不得再将仓库称为 SDK,超时策略说明应记录新的包名理由。架构决策仍然有效的说明,不得被重写为新的决策。 +- 配对的包创建指南应包含职责词约定,`packages/AGENTS.md` 应链接到该约定,术语表应记录选定用词和 `Typert` 拼写,根项目文案应将产品称为 DeepSeek Harness,而不是 DeepSeek Harness SDK。 +- 已移除的 SDK 项目工具链继续保持不存在。 +- 聚焦测试覆盖每个重命名系列;在完整实现上,源代码平面的类型检查、构建、包卫生检查、生成参考资料门禁、受可见标识符影响的快照、翻译配对、`doc-sync` 和 lint 均应通过。 +- 只有完整清单和文档约定全部成为事实后,才能将该 proposed 说明移入 `implemented/`。其实现版应描述最终状态,而不是迁移检查清单。 + +## 风险 + +这轮整体重命名会更改大量导入、路径、配置字符串、生成的参考资料和模型可见名称。遗漏的字符串可能通过编译,却在加载或回放时失败。实现时必须结合类型驱动的修改,并在源代码、配置、测试、fixture、文档和生成输入中精确搜索旧名称。 + +并行工作会与路径移动和符号重命名产生冲突。这是发布前的临时成本。按职责一致的系列拆分实现,可以降低评审成本和合并冲突,但每个系列都必须保持原子性。 + +一些名称会变得更长。额外增加的词只有在防止误述权限或机制时才有意义。如果名称中的词不能全部限定职责,长名称仍然是失败的命名。 + +如果评审人只检查后缀而不检查行为,职责词就会被机械套用。指南必须保留本说明中的直接判断方式:检查调用方执行什么操作、对象拥有什么生命周期,以及对象控制什么失败或策略。 + +清单点名的旧磁盘名称、协议值、工具名称和配置项将停止工作。发布前接受这一后果。如果负责解析的解析器能够识别陈旧配置,实现必须明确报错;不得静默接受两种形式。 diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 3617485b58..8bd0b11000 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: 0f81ea3279a7c52b6769bf5008dd832736c1398f -config-catalog.zh.md: 1db10a199fcab548726d75cc031c1b41d4f2aeb3 +config-catalog.md: b29df04dc35e6fe472f425c4ef81d1d406445bde +config-catalog.zh.md: 14a654d1e02fe1c1690e5cc7bf3a4bc40359f23d diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 0f81ea3279..b29df04dc3 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -783,7 +783,7 @@ export interface DeepSeekCatalogModel { Depends on: [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) -Source: [`packages/llm/llm-deepseek/src/index.ts:61`](../packages/llm/llm-deepseek/src/index.ts) +Source: [`packages/llm/llm-deepseek/src/index.ts:62`](../packages/llm/llm-deepseek/src/index.ts) ## `@deepseek-ai/dsh-llm-pi-ai` @@ -2385,16 +2385,32 @@ Source: [`packages/subagent/tool-subagent-report/src/index.ts:27`](../packages/s Requires: `tools` · `tasks` · `systemPrompt` ```ts config-catalog -/** Configures bounded `task_output` waits. */ +/** Configures bounded `task_output` waits and completion-notice delivery. */ export interface Config { /** Wait duration applied when `task_output` sets `wait` without `timeout_ms` (default 30s). */ waitTimeoutMs?: number /** Hard cap on any single wait; a larger model-supplied `timeout_ms` is clamped down to it (default 10min). */ maxWaitTimeoutMs?: number + /** Whether a completion opens a turn on an idle owner (default `wakeup`). */ + completionDelivery?: CompletionDelivery + /** + * Turns one owner may have opened by completion wakes before the next + * notice degrades to injection, reset by any user-authored input (default 3). + * Bounds the self-exciting chain where a woken turn starts the task whose + * completion wakes it again. + */ + maxConsecutiveWakes?: number } + +/** + * How an unreported completion reaches an owner that is already idle: `wakeup` + * opens a turn for it, `quiet` leaves it pending until something else wakes the + * owner. A busy owner is injected either way. + */ +export type CompletionDelivery = 'quiet' | 'wakeup' ``` -Source: [`packages/tasks/tool-tasks/src/index.ts:23`](../packages/tasks/tool-tasks/src/index.ts) +Source: [`packages/tasks/tool-tasks/src/index.ts:32`](../packages/tasks/tool-tasks/src/index.ts) ## `@deepseek-ai/dsh-tool-todo` diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 1db10a199f..14a654d1e0 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -785,7 +785,7 @@ export interface DeepSeekCatalogModel { 依赖:[`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) -来源:[`packages/llm/llm-deepseek/src/index.ts:61`](../packages/llm/llm-deepseek/src/index.ts) +来源:[`packages/llm/llm-deepseek/src/index.ts:62`](../packages/llm/llm-deepseek/src/index.ts) ## `@deepseek-ai/dsh-llm-pi-ai` @@ -2386,16 +2386,32 @@ export interface Config { 需要:`tools` · `tasks` · `systemPrompt` ```ts config-catalog -/** Configures bounded `task_output` waits. */ +/** Configures bounded `task_output` waits and completion-notice delivery. */ export interface Config { /** Wait duration applied when `task_output` sets `wait` without `timeout_ms` (default 30s). */ waitTimeoutMs?: number /** Hard cap on any single wait; a larger model-supplied `timeout_ms` is clamped down to it (default 10min). */ maxWaitTimeoutMs?: number + /** Whether a completion opens a turn on an idle owner (default `wakeup`). */ + completionDelivery?: CompletionDelivery + /** + * Turns one owner may have opened by completion wakes before the next + * notice degrades to injection, reset by any user-authored input (default 3). + * Bounds the self-exciting chain where a woken turn starts the task whose + * completion wakes it again. + */ + maxConsecutiveWakes?: number } + +/** + * How an unreported completion reaches an owner that is already idle: `wakeup` + * opens a turn for it, `quiet` leaves it pending until something else wakes the + * owner. A busy owner is injected either way. + */ +export type CompletionDelivery = 'quiet' | 'wakeup' ``` -来源:[`packages/tasks/tool-tasks/src/index.ts:23`](../packages/tasks/tool-tasks/src/index.ts) +来源:[`packages/tasks/tool-tasks/src/index.ts:32`](../packages/tasks/tool-tasks/src/index.ts) ## `@deepseek-ai/dsh-tool-todo` diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index 240c1248db..12daef3078 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/event-producer-consumer.md -event-producer-consumer.md: 52b2b50b6e9817c9fe906bde9457cd5bc2363f9f -event-producer-consumer.zh.md: bff0d3345b1b195ec4f2448384cb1d5b36c55371 +event-producer-consumer.md: c272087b03c6d27599b76ad6d6f197b023a36579 +event-producer-consumer.zh.md: b5634785e58134df453f0022a3b46ea926f3a526 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 52b2b50b6e..c272087b03 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -12,7 +12,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-session`](../packages/goal/goal-session), [`tool-schedule`](../packages/schedule/tool-schedule) | | `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) | | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`session-telemetry`](../packages/session/session-telemetry) | -| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) | +| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent), [`tool-tasks`](../packages/tasks/tool-tasks) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session) | | `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index bff0d3345b..b5634785e5 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -14,7 +14,7 @@ | `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-session`](../packages/goal/goal-session), [`tool-schedule`](../packages/schedule/tool-schedule) | | `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) | | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`session-telemetry`](../packages/session/session-telemetry) | -| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) | +| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent), [`tool-tasks`](../packages/tasks/tool-tasks) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session) | | `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) | diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index f11d7b0426..767e6c6657 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/module-graph.md -module-graph.md: c57619850cd155b0cdc4a178dc75cefe3897c46e -module-graph.zh.md: a062c0d59b4f7c635c46bd2724da3210398f416e +module-graph.md: e68b0ca0f7e7a3694985827822c71e923af6053f +module-graph.zh.md: a5fe499c53234fc29bd83515ce565b458df519f3 diff --git a/docs/module-graph.md b/docs/module-graph.md index c57619850c..e68b0ca0f7 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -395,6 +395,7 @@ flowchart TD pkg_llm_deepseek --> pkg_llm pkg_llm_deepseek --> pkg_settings pkg_llm_deepseek --> pkg_timeout + pkg_llm_deepseek --> pkg_user_id pkg_llm_pi_ai --> pkg_attachment pkg_llm_pi_ai --> pkg_credentials pkg_llm_pi_ai --> pkg_environment @@ -1373,7 +1374,7 @@ flowchart TD | [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | | [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) | -| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) | +| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout), [`user-id`](../packages/session/user-id) | | [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) | | [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`type-meta`](../packages/typert/type-meta) | | [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index a062c0d59b..a5fe499c53 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -397,6 +397,7 @@ flowchart TD pkg_llm_deepseek --> pkg_llm pkg_llm_deepseek --> pkg_settings pkg_llm_deepseek --> pkg_timeout + pkg_llm_deepseek --> pkg_user_id pkg_llm_pi_ai --> pkg_attachment pkg_llm_pi_ai --> pkg_credentials pkg_llm_pi_ai --> pkg_environment @@ -1375,7 +1376,7 @@ flowchart TD | [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | | [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) | -| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) | +| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout), [`user-id`](../packages/session/user-id) | | [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) | | [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`type-meta`](../packages/typert/type-meta) | | [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | diff --git a/docs/subsystems/llm-streaming.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml index 667fe8028a..8c144d72c3 100644 --- a/docs/subsystems/llm-streaming.i18n.yaml +++ b/docs/subsystems/llm-streaming.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/llm-streaming.md -llm-streaming.md: bd2c9b856d34e625b023a20c8be331eb7ea89880 -llm-streaming.zh.md: 34420669ca8cbdf9d9a0d8b38e67bc196c290cc8 +llm-streaming.md: 41ae943b1945209bc7996379dc326434b5803d17 +llm-streaming.zh.md: b67cdfd4eb5a3c1320e7afe333d4d06516295f7d diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md index bd2c9b856d..41ae943b19 100644 --- a/docs/subsystems/llm-streaming.md +++ b/docs/subsystems/llm-streaming.md @@ -484,8 +484,8 @@ interface GenerateOptions { stop?: string[] signal?: AbortSignal /** - * Session identity stamped by the loop for listener routing. Adapters ignore - * it; replay uses it to keep concurrent parent and child cursors independent. + * Session identity stamped by the loop for request routing. Replay uses it + * to separate cursors; adapters may map it to model-hidden transport metadata. */ sessionId?: Branded<'SessionId'> /** diff --git a/docs/subsystems/llm-streaming.zh.md b/docs/subsystems/llm-streaming.zh.md index 34420669ca..b67cdfd4eb 100644 --- a/docs/subsystems/llm-streaming.zh.md +++ b/docs/subsystems/llm-streaming.zh.md @@ -492,8 +492,8 @@ interface GenerateOptions { stop?: string[] signal?: AbortSignal /** - * Session identity stamped by the loop for listener routing. Adapters ignore - * it; replay uses it to keep concurrent parent and child cursors independent. + * Session identity stamped by the loop for request routing. Replay uses it + * to separate cursors; adapters may map it to model-hidden transport metadata. */ sessionId?: Branded<'SessionId'> /** diff --git a/docs/subsystems/tasks.i18n.yaml b/docs/subsystems/tasks.i18n.yaml index a73e8129c9..40a15cc5ac 100644 --- a/docs/subsystems/tasks.i18n.yaml +++ b/docs/subsystems/tasks.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/tasks.md -tasks.md: 2798eb2963e83d42321a343d3c0a6a2bb658be41 -tasks.zh.md: 8d4539514cb4bc654dd86031d51417c4f79f8982 +tasks.md: 37807bc446f607c3e1635a49432670e86760f2a4 +tasks.zh.md: 39014acb13f6051431a40ae084c1e03298ae8993 diff --git a/docs/subsystems/tasks.md b/docs/subsystems/tasks.md index 2798eb2963..37807bc446 100644 --- a/docs/subsystems/tasks.md +++ b/docs/subsystems/tasks.md @@ -97,7 +97,7 @@ interface TaskOutcome { ## Consumer views -Snapshots are fresh read-only projections. `ownerSession` carries the shared `SessionId` used for authorization; completion listeners separately receive the exact owner object used for lifecycle cleanup. `reported` suppresses a completion notice after another reporter has delivered or committed to deliver the terminal state. +Snapshots are fresh read-only projections. `ownerSession` carries the shared `SessionId` used for authorization; completion listeners separately receive the exact owner object used for lifecycle cleanup. `reported` suppresses a completion notice after another reporter has delivered or committed to deliver the terminal state, including the teardown cancel that drains an owner or the service. ```ts type-equiv /** @@ -128,8 +128,11 @@ interface TaskSnapshot { /** Epoch ms when the task settled; absent while `running`/`stopping`. */ finishedAt?: number /** - * True when a kill, read, or wait has reported or committed to report the - * terminal state. Completion reporters suppress redundant notices when set. + * True when a kill, read, wait, or teardown cancel has reported or committed + * to report the terminal state. Completion reporters suppress redundant + * notices when set. Teardown claims it because the owner or service being + * destroyed leaves no reader: a reporter that opens a turn on notice would + * otherwise spend a model request per teardown layer. */ reported: boolean } @@ -169,9 +172,9 @@ Abstract background task registry. Subclass, implement the abstract methods, and Implementations must honor these semantics: -- Registrations outlive producer and controller fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record. +- Registrations outlive producer and controller fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record. Teardown cancellation also marks the record reported, because a record its owner is being destroyed for has no reader left. - Owned-task access is fenced by the owner's session id. Ids are predictable, so authorization — not secrecy — is the boundary. -- Settlement is first-wins: one terminal record, one round of contained listener notification, and released waiters, even against a late producer outcome. +- Settlement is first-wins: one terminal record, released waiters, and one round of contained listener notification, even against a late producer outcome. Completion is announced last, after the record is committed and every other observer of the settlement has seen it, because a reporter may open a model turn synchronously. - start refuses work while no attached task controller serves the spec's owner, so a producer cannot start work that owner cannot collect or stop. One registry serves every composition in the process, so this question — and completion-listener delivery — is owner-relative rather than process-wide: registrations made from an unscoped context serve every owner, and registrations made under an agent composition's scope serve exactly the agents composed under it. ```ts cordis-catalog @@ -282,5 +285,5 @@ abstract attachController(name: string): () => void Types: [Agent](core.md) -Source: [`packages/tasks/tasks/src/index.ts:58`](../../packages/tasks/tasks/src/index.ts) +Source: [`packages/tasks/tasks/src/index.ts:62`](../../packages/tasks/tasks/src/index.ts) diff --git a/docs/subsystems/tasks.zh.md b/docs/subsystems/tasks.zh.md index 8d4539514c..39014acb13 100644 --- a/docs/subsystems/tasks.zh.md +++ b/docs/subsystems/tasks.zh.md @@ -97,7 +97,7 @@ interface TaskOutcome { ## 消费方视图 -快照是每次新建的只读投影。`ownerSession` 携带用于授权的共享 `SessionId`;完成监听器则会另行收到用于生命周期清理的确切拥有者对象。另一个接口已经交付终止状态或承诺交付时,`reported` 会抑制完成通知。 +快照是每次新建的只读投影。`ownerSession` 携带用于授权的共享 `SessionId`;完成监听器则会另行收到用于生命周期清理的确切拥有者对象。另一个接口已经交付终止状态或承诺交付时,`reported` 会抑制完成通知;排空 owner 或服务的 teardown 取消同样计入。 ```ts type-equiv /** @@ -128,8 +128,11 @@ interface TaskSnapshot { /** Epoch ms when the task settled; absent while `running`/`stopping`. */ finishedAt?: number /** - * True when a kill, read, or wait has reported or committed to report the - * terminal state. Completion reporters suppress redundant notices when set. + * True when a kill, read, wait, or teardown cancel has reported or committed + * to report the terminal state. Completion reporters suppress redundant + * notices when set. Teardown claims it because the owner or service being + * destroyed leaves no reader: a reporter that opens a turn on notice would + * otherwise spend a model request per teardown layer. */ reported: boolean } @@ -169,9 +172,9 @@ Abstract background task registry. Subclass, implement the abstract methods, and Implementations must honor these semantics: -- Registrations outlive producer and controller fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record. +- Registrations outlive producer and controller fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record. Teardown cancellation also marks the record reported, because a record its owner is being destroyed for has no reader left. - Owned-task access is fenced by the owner's session id. Ids are predictable, so authorization — not secrecy — is the boundary. -- Settlement is first-wins: one terminal record, one round of contained listener notification, and released waiters, even against a late producer outcome. +- Settlement is first-wins: one terminal record, released waiters, and one round of contained listener notification, even against a late producer outcome. Completion is announced last, after the record is committed and every other observer of the settlement has seen it, because a reporter may open a model turn synchronously. - start refuses work while no attached task controller serves the spec's owner, so a producer cannot start work that owner cannot collect or stop. One registry serves every composition in the process, so this question — and completion-listener delivery — is owner-relative rather than process-wide: registrations made from an unscoped context serve every owner, and registrations made under an agent composition's scope serve exactly the agents composed under it. ```ts cordis-catalog @@ -282,5 +285,5 @@ abstract attachController(name: string): () => void Types: [Agent](core.md) -Source: [`packages/tasks/tasks/src/index.ts:58`](../../packages/tasks/tasks/src/index.ts) +Source: [`packages/tasks/tasks/src/index.ts:62`](../../packages/tasks/tasks/src/index.ts) diff --git a/packages/bash/pwsh-local/tests/executor.spec.ts b/packages/bash/pwsh-local/tests/executor.spec.ts index 6f38af8153..f84cc461d4 100644 --- a/packages/bash/pwsh-local/tests/executor.spec.ts +++ b/packages/bash/pwsh-local/tests/executor.spec.ts @@ -31,10 +31,10 @@ const hasPwsh = spawnSync(resolvePwshPath(), ['-NoLogo', '-NoProfile', '-NonInte /** Normalize PowerShell's platform line endings (CRLF on Windows, LF elsewhere). */ const lf = (text: string): string => text.replace(/\r\n/g, '\n') -/** Case-insensitive path equality on Windows (Get-Location may re-case the drive). */ +/** Filesystem path equality across macOS temp symlinks and Windows drive-letter casing. */ function samePath(actual: string, expected: string): boolean { const norm = (value: string) => ( - process.platform === 'win32' ? realpathSync.native(value).toLowerCase() : value + process.platform === 'win32' ? realpathSync.native(value).toLowerCase() : realpathSync.native(value) ) return norm(actual) === norm(expected) } diff --git a/packages/bash/tool-bash/tests/integration.spec.ts b/packages/bash/tool-bash/tests/integration.spec.ts index 425fa0644f..ad873a9ddc 100644 --- a/packages/bash/tool-bash/tests/integration.spec.ts +++ b/packages/bash/tool-bash/tests/integration.spec.ts @@ -1,7 +1,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm' import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' @@ -174,11 +174,24 @@ describe('bash tool through the agent loop', () => { expect(resultText(toolResult)).toContain('[exit code: 9]') }) - it('background: start ack → pending completion notice → task_output collects it', async () => { + it('background: start ack → completion wakes the idle agent → task_output collects it', async () => { + // The command blocks on a sentinel this test creates only after the agent + // has gone idle, so settlement cannot fold into the still-running turn. + // Without that fence a fast command can settle before step 2's pre-step + // claim, which folds the notice into a turn whose scripted reply is final: + // the turn then closes with an empty next-step inbox and the collection + // entries are never reached. + const dir = mkdtempSync(join(tmpdir(), 'dsh-bg-')) + dirs.push(dir) + const sentinel = join(dir, 'release') // The task id is deterministic (a fresh LocalTaskService counts per kind from 1), // so the script can name `bash-1` without threading a generated id. const adapter = new MockAdapter([ - toolCallResponse('call-1', 'bash', { command: 'echo bg-ok', description: 'test command', run_in_background: true }), + toolCallResponse('call-1', 'bash', { + command: `while [ ! -f ${JSON.stringify(sentinel)} ]; do sleep 0.02; done; echo bg-ok`, + description: 'test command', + run_in_background: true, + }), textResponse('Started it in the background.'), toolCallResponse('call-2', 'task_output', { task_id: 'bash-1' }), textResponse('Background task finished.'), @@ -192,29 +205,35 @@ describe('bash tool through the agent loop', () => { const firstResult = findEvent(events(agent), 'tool/result') expect(firstResult.data.message.content[0].isError).toBe(false) expect(resultText(firstResult)).toBe('started background task bash-1') - - // The task settles on its own; the tool-tasks notice listener injects a - // pending next-step message without waking the idle agent. + // The turn closed with the task still running, so the notice cannot exist yet. const isNotice = (e: SessionEvent): e is SessionEvent<'user/message'> => e.type === 'user/message' && e.data.source.kind === 'plugin' - await pollUntil(() => agent.inbox.nextStep.some(message => message.source.kind === 'plugin')) - const pendingNotice = agent.inbox.nextStep.find(message => message.source.kind === 'plugin')! - expect(pendingNotice.content.some( - block => block.type === 'text' && block.text.includes('background task bash-1 (bash: echo bg-ok) finished'), - )).toBe(true) - expect(pendingNotice.source).toEqual({ + expect(events(agent).some(isNotice)).toBe(false) + + // Releasing the command now settles it against a provably idle owner. No + // second user message: the wake alone opens the turn that collects it. + writeFileSync(sentinel, '') + const lastResultText = (): string => { + const found = events(agent).findLast(event => event.type === 'tool/result') + return found === undefined ? '' : resultText(found) + } + await pollUntil(() => events(agent).some(isNotice) && lastResultText().includes('bg-ok')) + // Two turns: the user's, then the one the completion opened by itself. + expect(events(agent).filter(event => event.type === 'turn/start')).toHaveLength(2) + + // The notice carries the gated command as its label, so this pins the id, + // the terminal status, and the producer identity; the verbatim notice text + // and its bounding are pinned in the tool-tasks unit tests. + const notice = events(agent).find(isNotice)! + const noticeText = notice.data.content + .filter(block => block.type === 'text').map(block => block.text).join('') + expect(noticeText).toContain('background task bash-1 (bash: ') + expect(noticeText).toContain('finished [status: completed, exit code: 0]') + expect(notice.data.source).toMatchObject({ kind: 'plugin', plugin: 'tool-tasks', form: 'notice', - summary: 'bash echo bg-ok [status: completed, exit code: 0]', }) - - // The next turn first admits that notice as user/message, then collects - // the output through the generic task tool. - agent.followup(createUserMessage({ content: [{ type: 'text', text: 'collect it' }], source: { kind: 'user' } })) - await waitForIdle(ctx, agent) - const notice = events(agent).find(isNotice)! - expect(notice.data).toEqual(pendingNotice) const readResult = findEvent(events(agent), 'tool/result', 'last') expect(readResult.data.message.content[0].isError).toBe(false) expect(resultText(readResult)).toContain('bg-ok') diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml index 23b6cf6305..a8e12e911c 100644 --- a/packages/llm/llm-deepseek/README.i18n.yaml +++ b/packages/llm/llm-deepseek/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md -README.md: a21f9f0464e9d43d2091bd446eb123d4d0990c3d -README.zh.md: 2c45f2144694785590b339642cb62b62a1aa4198 +README.md: 1fde02dc8c764a189eb78226de4c194bbf0b8a5e +README.zh.md: 711bc92101c4df0abd40375e3d73f993105ea81c diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index a21f9f0464..1fde02dc8c 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -62,6 +62,8 @@ The plugin also declares its route in the configurable-provider directory (`ctx. Every request carries the shared attribution header from dsh-llm's `attributionHeaders()` - the mandatory `User-Agent` baseline identifying the harness (see [dsh-llm § App attribution](../llm/README.md#app-attribution-attributionts)). Direct DeepSeek requests and OpenAI-compatible gateway requests get no provider-specific app-attribution headers under this adapter contract; OpenRouter app attribution is deferred to a future explicit OpenRouter adapter or mode. A request whose `GenerateOptions.purpose` is `compaction` (dsh-compact-basic's auxiliary summarization call) additionally carries `x-deepseek-harness-compact: 1`, so the host can separate compaction traffic from conversation requests. +DeepSeek request identity is separate from app attribution. After credential resolution, every provider request carries `x-deepseek-harness-user-id` with the stable anonymous id from [`@deepseek-ai/dsh-user-id`](../../session/user-id/README.md); a request carrying `GenerateOptions.sessionId` also sends that exact value as `x-deepseek-harness-session-id`, while a direct call without a session omits the session header. Both headers go to the resolved `baseURL`, including a configured gateway, and remain outside the request body and model-visible content. + ## Wire-format notes - Streaming only (`stream_options.include_usage` always on). `usage` may arrive attached to the finish chunk or as a trailing usage-only chunk — the translator defers both to `[DONE]`, so `usage` always precedes `finish` and nothing follows `finish`. diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md index 2c45f21446..711bc92101 100644 --- a/packages/llm/llm-deepseek/README.zh.md +++ b/packages/llm/llm-deepseek/README.zh.md @@ -62,6 +62,8 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器: 每个请求都携带 dsh-llm `attributionHeaders()` 的共享归因标头,即用于识别 harness 的必需 `User-Agent` 基线(见 [dsh-llm § 应用归因](../llm/README.md#app-attribution-attributionts))。在该适配器约定(adapter contract)下,直接 DeepSeek 请求与 OpenAI 兼容 gateway 请求都不会获得提供方特定应用归因标头;OpenRouter 应用归因暂缓到未来的显式 OpenRouter 适配器或模式。`GenerateOptions.purpose` 为 `compaction` 的请求(dsh-compact-basic 的辅助摘要调用)还会携带 `x-deepseek-harness-compact: 1`,让宿主可以将压缩流量与会话请求分开。 +DeepSeek 请求身份独立于应用归因。凭据解析成功后,每个提供方请求都会通过 `x-deepseek-harness-user-id` 携带来自 [`@deepseek-ai/dsh-user-id`](../../session/user-id/README.md) 的稳定匿名 id;携带 `GenerateOptions.sessionId` 的请求还会通过 `x-deepseek-harness-session-id` 发送该确切值,缺少会话的直接调用则省略会话标头。两个标头都会发送至解析后的 `baseURL`(包括已配置的 gateway),且不会进入请求正文或模型可见内容。 + ## 协议格式说明 - 只支持流式输出(`stream_options.include_usage` 始终开启)。`usage` 可能附着在 finish 分片上,也可能作为尾随的纯 usage 分片到达;转换器会将两者都延迟到 `[DONE]`,因此 `usage` 始终位于 `finish` 之前,`finish` 之后不会出现任何内容。 diff --git a/packages/llm/llm-deepseek/package.json b/packages/llm/llm-deepseek/package.json index 834e6c5387..52b50283a8 100644 --- a/packages/llm/llm-deepseek/package.json +++ b/packages/llm/llm-deepseek/package.json @@ -38,6 +38,7 @@ "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-settings": "workspace:^", "@deepseek-ai/dsh-timeout": "workspace:^", + "@deepseek-ai/dsh-user-id": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, "dependencies": { @@ -51,6 +52,7 @@ "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-settings": "workspace:^", "@deepseek-ai/dsh-timeout": "workspace:^", + "@deepseek-ai/dsh-user-id": "workspace:^", "@deepseek-ai/cordis": "workspace:^" } } diff --git a/packages/llm/llm-deepseek/src/adapter.ts b/packages/llm/llm-deepseek/src/adapter.ts index aa4d73593f..b8e3625b05 100644 --- a/packages/llm/llm-deepseek/src/adapter.ts +++ b/packages/llm/llm-deepseek/src/adapter.ts @@ -19,6 +19,7 @@ import type { } from '@deepseek-ai/dsh-llm' import type { CredentialRef } from '@deepseek-ai/dsh-credentials' import { idleWatchdog, timeoutOf } from '@deepseek-ai/dsh-timeout' +import type { AnonymousUserId } from '@deepseek-ai/dsh-user-id' import { serializeRequest } from './serialize.ts' import type { RequestDefaults } from './serialize.ts' import { parseSse } from './sse.ts' @@ -69,7 +70,7 @@ export interface DeepSeekConnectionOptions { retryPolicy: ResolvedRetryPolicy } -/** Constructor options for {@link DeepSeekAdapter}: the two resolution hooks the plugin owns. */ +/** Constructor options for {@link DeepSeekAdapter}: the operation-local resolution hooks the plugin owns. */ export interface DeepSeekAdapterOptions { /** Current validated connection facts; called once per operation. */ options: () => DeepSeekConnectionOptions @@ -80,6 +81,8 @@ export interface DeepSeekAdapterOptions { * `MISSING_CREDENTIAL` when no key is available anywhere. */ resolveApiKey: (connection: DeepSeekConnectionOptions) => Promise + /** Resolve the harness-home anonymous id shared with telemetry and feedback. */ + resolveUserId: () => AnonymousUserId } /** Default maximum idle interval while an adapter stream read is outstanding. */ @@ -216,6 +219,7 @@ export class DeepSeekAdapter extends LlmAdapter { // sent to it can never come from different configuration generations. const connection = this.config.options() const apiKey = await this.config.resolveApiKey(connection) + const userId = this.config.resolveUserId() const consumer = new AbortController() const upstream = options.signal === undefined ? consumer.signal @@ -226,6 +230,7 @@ export class DeepSeekAdapter extends LlmAdapter { watchdog.signal, connection, apiKey, + userId, () => { watchdog.pulse() }, )[Symbol.asyncIterator]() let exhausted = false @@ -268,6 +273,7 @@ export class DeepSeekAdapter extends LlmAdapter { signal: AbortSignal, connection: DeepSeekConnectionOptions, apiKey: string, + userId: AnonymousUserId, onComment: () => void, ): AsyncIterable { const body = serializeRequest(options, connection.defaults) @@ -279,6 +285,7 @@ export class DeepSeekAdapter extends LlmAdapter { 'content-type': 'application/json', 'accept': 'text/event-stream', ...attributionHeaders(), + 'x-deepseek-harness-user-id': String(userId), ...options.sessionId !== undefined ? { 'x-deepseek-harness-session-id': String(options.sessionId) } : {}, diff --git a/packages/llm/llm-deepseek/src/index.ts b/packages/llm/llm-deepseek/src/index.ts index f77b36a9cc..f40fd2535f 100644 --- a/packages/llm/llm-deepseek/src/index.ts +++ b/packages/llm/llm-deepseek/src/index.ts @@ -19,6 +19,7 @@ import { credentialRef } from '@deepseek-ai/dsh-credentials' import { environmentOf, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment' import { deepEqualJson, installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings' import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout' +import { getOrCreateAnonymousUserId, type AnonymousUserId } from '@deepseek-ai/dsh-user-id' import { DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, @@ -244,7 +245,9 @@ export function apply(ctx: Context, config: Config): void { ) } - const adapter = new DeepSeekAdapter({ options, resolveApiKey }) + let userId: AnonymousUserId | undefined + const resolveUserId = (): AnonymousUserId => userId ??= getOrCreateAnonymousUserId() + const adapter = new DeepSeekAdapter({ options, resolveApiKey, resolveUserId }) ctx.llm.registerConfigurableProviders([ { provider: PROVIDER, displayName: 'DeepSeek', settingsNs: NS, settingsPath: [] }, ]) diff --git a/packages/llm/llm-deepseek/tests/adapter.e2e.ts b/packages/llm/llm-deepseek/tests/adapter.e2e.ts index e8eeb7b403..82500e8691 100644 --- a/packages/llm/llm-deepseek/tests/adapter.e2e.ts +++ b/packages/llm/llm-deepseek/tests/adapter.e2e.ts @@ -1,7 +1,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import LlmService, { createUserMessage, CallId, ReasoningEffortId , createMessage } from '@deepseek-ai/dsh-llm' import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm' @@ -19,6 +19,12 @@ import { assemble, type AssembledResult } from './assemble.ts' const FLASH = 'deepseek-v4-flash' const PRO = 'deepseek-v4-pro' const contexts: Context[] = [] +let identityHome: string + +beforeEach(async () => { + identityHome = await mkdtemp(join(tmpdir(), 'dsh-e2e-user-id-')) + vi.stubEnv('DSH_HOME', identityHome) +}) async function harness(_model: string, config: Partial = {}) { const ctx = new Context() @@ -30,6 +36,8 @@ async function harness(_model: string, config: Partial = {}) { afterEach(async () => { await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose())) + vi.unstubAllEnvs() + await rm(identityHome, { recursive: true, force: true }) }) function ask(text: string): Message[] { diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts index 32a19776b2..78baf986d7 100644 --- a/packages/llm/llm-deepseek/tests/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts @@ -1,4 +1,7 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' import { createEnvironmentSnapshot } from '@deepseek-ai/dsh-environment' import LlmService, { createUserMessage, @@ -9,6 +12,7 @@ import LlmService, { createUserMessage, userAgent, } from '@deepseek-ai/dsh-llm' import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout' +import { getOrCreateAnonymousUserId, type AnonymousUserId } from '@deepseek-ai/dsh-user-id' import { SessionId } from '@deepseek-ai/dsh-session' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import { DeepSeekAdapter, resolveAdapterOptions } from '@deepseek-ai/dsh-llm-deepseek' @@ -17,10 +21,19 @@ import { assemble } from './assemble.ts' import { closeMockServers, mockServer, textEvents } from './mock-server.ts' import type { Behavior } from './mock-server.ts' +const TEST_USER_ID = '00000000-0000-4000-8000-000000000001' as AnonymousUserId +let testHome: string + +beforeEach(() => { + testHome = mkdtempSync(join(tmpdir(), 'dsh-llm-deepseek-')) + vi.stubEnv('DSH_HOME', testHome) +}) + afterEach(async () => { await closeMockServers() vi.unstubAllEnvs() vi.useRealTimers() + rmSync(testHome, { recursive: true, force: true }) }) async function harness(baseURL: string, config: object = {}) { @@ -39,6 +52,7 @@ function adapterOf(config: Partial & { apiKey?: string } = { return new DeepSeekAdapter({ options: () => resolveAdapterOptions(rest), resolveApiKey: () => Promise.resolve(apiKey ?? 'k'), + resolveUserId: () => TEST_USER_ID, }) } @@ -66,9 +80,10 @@ describe('DeepSeekAdapter against a mock server', () => { stream: true, stream_options: { include_usage: true }, }) - // Attribution reaches the wire: the exact shared User-Agent, and no - // provider-specific headers under the User-Agent-only contract. + // App attribution and DeepSeek request identity are independent wire facts. expect(server.headers[0]?.['user-agent']).toBe(userAgent()) + expect(server.headers[0]?.['x-deepseek-harness-user-id']).toBe(getOrCreateAnonymousUserId()) + expect(server.headers[0]).not.toHaveProperty('x-deepseek-harness-session-id') expect(server.headers[0]).not.toHaveProperty('http-referer') expect(server.headers[0]).not.toHaveProperty('x-openrouter-title') expect(server.headers[0]).not.toHaveProperty('x-openrouter-categories') @@ -93,7 +108,7 @@ describe('DeepSeekAdapter against a mock server', () => { expect(kinds).toEqual(['block-start', 'text-delta', 'block-end', 'usage', 'finish']) }) - it('forwards the harness session id for host-side trajectory routing', async () => { + it('forwards the harness user and session ids for host-side trajectory routing', async () => { const server = await mockServer([{ kind: 'sse', events: textEvents }]) const ctx = await harness(server.url) @@ -107,6 +122,7 @@ describe('DeepSeekAdapter against a mock server', () => { }) expect(server.headers[0]?.['x-deepseek-harness-session-id']).toBe('child-session') + expect(server.headers[0]?.['x-deepseek-harness-user-id']).toBe(getOrCreateAnonymousUserId()) }) it('marks the auxiliary compaction call on the wire', async () => { @@ -997,12 +1013,14 @@ describe('plugin registration and config', () => { const server = await mockServer([{ kind: 'sse', events: textEvents }]) const options = vi.fn(() => resolveAdapterOptions({ baseURL: server.url })) const resolveApiKey = vi.fn(() => Promise.resolve('per-request-key')) - const adapter = new DeepSeekAdapter({ options, resolveApiKey }) + const resolveUserId = vi.fn(() => TEST_USER_ID) + const adapter = new DeepSeekAdapter({ options, resolveApiKey, resolveUserId }) for await (const _chunk of adapter.stream({ provider: 'deepseek-official', model: 'm', messages: [] })) { /* drain */ } expect(options).toHaveBeenCalledTimes(1) expect(resolveApiKey).toHaveBeenCalledTimes(1) + expect(resolveUserId).toHaveBeenCalledTimes(1) expect(server.headers[0]?.authorization).toBe('Bearer per-request-key') }) diff --git a/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts b/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts index ec6da80b17..3f9b997dc8 100644 --- a/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts +++ b/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { access, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import LlmService, { INVALID_CREDENTIAL_CODE } from '@deepseek-ai/dsh-llm' @@ -41,6 +41,7 @@ interface Harness { * file watching is the providers' own covered concern. */ async function boot(dir: string, config: object): Promise { + vi.stubEnv('DSH_HOME', dir) const ctx = new Context() cleanups.push(async () => { await ctx.fiber.dispose() @@ -86,9 +87,11 @@ describe('request-level dynamic configuration', () => { const keyless = await prompt(ctx) expect(keyless.finish).toMatchObject({ kind: 'error', failure: { code: 'MISSING_CREDENTIAL' } }) + await expect(access(join(dir, '.userid'))).rejects.toMatchObject({ code: 'ENOENT' }) await ctx.credentials.set(KEY_REF, 'sk-arrived') await prompt(ctx) expect(server.headers[0]?.authorization).toBe('Bearer sk-arrived') + await expect(access(join(dir, '.userid'))).resolves.toBeUndefined() }) it('rejects a stored credential no header can carry, never echoing it in the failure', async () => { diff --git a/packages/llm/llm-deepseek/tests/loader-composition.spec.ts b/packages/llm/llm-deepseek/tests/loader-composition.spec.ts index d87c6bfe6c..89e6fabc1c 100644 --- a/packages/llm/llm-deepseek/tests/loader-composition.spec.ts +++ b/packages/llm/llm-deepseek/tests/loader-composition.spec.ts @@ -21,6 +21,7 @@ import { credentialRef } from '@deepseek-ai/dsh-credentials' import CredentialsLocal from '@deepseek-ai/dsh-credentials-local' import { settingsNamespace } from '@deepseek-ai/dsh-settings' import SettingsLocal from '@deepseek-ai/dsh-settings-local' +import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import { assemble } from './assemble.ts' import { closeMockServers, mockServer, textEvents } from './mock-server.ts' @@ -47,6 +48,7 @@ async function loadComposition( // exactly as the previous process left them. const fresh = options.reuseRoot === undefined root = options.reuseRoot ?? await mkdtemp(join(tmpdir(), 'dsh-llm-composition-')) + vi.stubEnv('DSH_HOME', root) const settingsPath = join(root, 'settings.yaml') const credentialsPath = join(root, '.credentials.yaml') if (options.withDynamic && fresh) { @@ -115,6 +117,7 @@ describe('llm-deepseek real dynamic composition', () => { expect(ctx.get('settings')!.describe().map(entry => entry.ns)).toEqual([NS]) await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] }) expect(serverA.headers[0]?.authorization).toBe('Bearer boot-key') + expect(serverA.headers[0]?.['x-deepseek-harness-user-id']).toBe(getOrCreateAnonymousUserId()) // External edits, exactly as a user or the web UI would leave them on disk. await writeFile(settingsPath, `llm-deepseek:\n baseURL: ${serverB.url}\n`) diff --git a/packages/llm/llm-deepseek/tsconfig.json b/packages/llm/llm-deepseek/tsconfig.json index 0b524a257b..cb079cd789 100644 --- a/packages/llm/llm-deepseek/tsconfig.json +++ b/packages/llm/llm-deepseek/tsconfig.json @@ -34,6 +34,9 @@ }, { "path": "../../util/timeout" + }, + { + "path": "../../session/user-id" } ] } diff --git a/packages/llm/llm/src/types.ts b/packages/llm/llm/src/types.ts index 0334637518..07c927fd85 100644 --- a/packages/llm/llm/src/types.ts +++ b/packages/llm/llm/src/types.ts @@ -343,8 +343,8 @@ export interface GenerateOptions { stop?: string[] signal?: AbortSignal /** - * Session identity stamped by the loop for listener routing. Adapters ignore - * it; replay uses it to keep concurrent parent and child cursors independent. + * Session identity stamped by the loop for request routing. Replay uses it + * to separate cursors; adapters may map it to model-hidden transport metadata. */ sessionId?: Branded<'SessionId'> /** diff --git a/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts b/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts index 6b7e547099..299a14d867 100644 --- a/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts +++ b/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts @@ -33,7 +33,10 @@ const WORKSPACE_CLOSURE = [ // from the registry). 'packages/sandbox/sandbox-windows-acl', 'packages/sandbox/sandbox', + 'packages/core/session', + 'packages/core/scope', 'packages/llm/llm', + 'packages/typert/type-meta', 'packages/attachment/attachment', 'packages/util/brand', 'packages/util/timeout', diff --git a/packages/session/user-id/README.i18n.yaml b/packages/session/user-id/README.i18n.yaml index 2427f8bdc9..d2ec2fe689 100644 --- a/packages/session/user-id/README.i18n.yaml +++ b/packages/session/user-id/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/user-id/README.md -README.md: eb50bb06af52b3d977068b73388361bc1c25087f -README.zh.md: 54287676c4e524c2458e4bab7e6bb3f52850ff25 +README.md: 55bc54e4a5b666880f1908f4ccdf720e1122fc7a +README.zh.md: 7816b62e581959ac3c5f3f277b86b147280fce17 diff --git a/packages/session/user-id/README.md b/packages/session/user-id/README.md index eb50bb06af..55bc54e4a5 100644 --- a/packages/session/user-id/README.md +++ b/packages/session/user-id/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Shared anonymous identity for session telemetry and direct feedback acknowledgement. `getOrCreateAnonymousUserId()` returns a random UUID v4 scoped to one harness home, persisted as the bare line `$DSH_HOME/.userid` (`~/.dsh/.userid` when `DSH_HOME` is unset). The OpenTelemetry backend reports it as Resource `user.id`; `/feedback` includes the same value in its acknowledgement so an operator can correlate a submitted session and user with exported telemetry. +Shared anonymous identity for session telemetry, direct feedback acknowledgement, and DeepSeek provider requests. `getOrCreateAnonymousUserId()` returns a random UUID v4 scoped to one harness home, persisted as the bare line `$DSH_HOME/.userid` (`~/.dsh/.userid` when `DSH_HOME` is unset). The OpenTelemetry backend reports it as Resource `user.id`; `/feedback` includes the same value in its acknowledgement; and `dsh-llm-deepseek` sends it as `x-deepseek-harness-user-id`, allowing the receiving systems to correlate records without independently generated identities. The identity is never derived from the hostname, network address, git remote, or another identifying source. Deleting `.userid` resets the identity on the next process launch. Separate harness homes have separate identities. @@ -12,18 +12,19 @@ Reads and writes are synchronous because both boot-time telemetry construction a ## Composition -This package is a shared library, not a Cordis plugin. Consumers import `getOrCreateAnonymousUserId()` directly. Its invariant companion is intentionally empty because the package owns no event stream or public mutable relation that can be checked without creating the identity as a side effect. +This package is a shared library, not a Cordis plugin. Consumers import `getOrCreateAnonymousUserId()` directly. Its invariant companion is intentionally empty because the package owns no event stream or public mutable relation that can be checked without creating the identity as a side effect. `DSH_TELEMETRY_DISABLED` stops telemetry export only; it does not suppress direct feedback acknowledgement or the DeepSeek provider header. ## Model Experience -None, as the identifier is used only in telemetry metadata and a direct human command response; it never enters a model request. +None, as the identifier reaches DeepSeek only as model-hidden HTTP transport metadata and never enters the request body, prompt, or model-visible content. #### KV Cache effect -None; this package never contributes to a model request. +None; the transport header changes neither tokens nor the model-visible prefix. ## Known Limitations and Deferred Work - **No recovery after deletion** — loss mints a new anonymous identity by design; recovery would require stable derivation material that weakens anonymity. - **Best-effort concurrency** — a reader landing in the narrow interval between a concurrent process's exclusive create and completed write can use a different in-memory UUID for that run; later launches converge on the persisted value. - **No cross-home identity** — different `$DSH_HOME` values cannot be correlated. +- **Configured DeepSeek gateways receive the id** — `dsh-llm-deepseek` sends the stable header to its resolved `baseURL`, including deployment overrides, independently of telemetry sharing mode. diff --git a/packages/session/user-id/README.zh.md b/packages/session/user-id/README.zh.md index 54287676c4..7816b62e58 100644 --- a/packages/session/user-id/README.zh.md +++ b/packages/session/user-id/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -会话遥测与直接反馈确认共用的匿名身份。`getOrCreateAnonymousUserId()` 返回一个限定于单个 harness home 的随机 UUID v4,并以裸行形式持久化到 `$DSH_HOME/.userid`(未设置 `DSH_HOME` 时为 `~/.dsh/.userid`)。OpenTelemetry 后端将其作为 Resource 的 `user.id` 上报;`/feedback` 在确认文本中包含同一个值,以便运维人员将所报告的会话和用户与导出的遥测相关联。 +会话遥测、直接反馈确认与 DeepSeek 提供方请求共用的匿名身份。`getOrCreateAnonymousUserId()` 返回一个限定于单个 harness home 的随机 UUID v4,并以裸行形式持久化到 `$DSH_HOME/.userid`(未设置 `DSH_HOME` 时为 `~/.dsh/.userid`)。OpenTelemetry 后端将其作为 Resource 的 `user.id` 上报;`/feedback` 在确认文本中包含同一个值;`dsh-llm-deepseek` 则通过 `x-deepseek-harness-user-id` 发送该值,使接收系统无需独立生成身份即可关联记录。 该身份绝不从 hostname、网络地址、git remote 或其他可用于识别身份的来源派生。删除 `.userid` 后,下次启动进程时会重置身份。不同 harness home 拥有不同身份。 @@ -12,18 +12,19 @@ ## 组合 -本包是共享库,并非 Cordis 插件。消费方直接导入 `getOrCreateAnonymousUserId()`。其不变式伴生插件刻意留空,因为本包既不拥有事件流,也不拥有任何可以在不触发创建身份这一副作用的情况下检查的公开可变关系。 +本包是共享库,并非 Cordis 插件。消费方直接导入 `getOrCreateAnonymousUserId()`。其不变式伴生插件刻意留空,因为本包既不拥有事件流,也不拥有任何可以在不触发创建身份这一副作用的情况下检查的公开可变关系。`DSH_TELEMETRY_DISABLED` 只会停止遥测导出,不会禁止直接反馈确认或 DeepSeek 提供方标头。 ## 模型体验 -无,因为该标识符只用于遥测元数据和面向用户的直接命令响应;它绝不会进入模型请求。 +无,因为该标识符只会作为模型不可见的 HTTP 传输元数据发送给 DeepSeek,绝不会进入请求正文、提示词或模型可见内容。 #### KV Cache 影响 -无;本包绝不会向模型请求贡献任何内容。 +无;该传输标头既不会改变 token,也不会改变模型可见前缀。 ## 已知限制与暂缓工作 - **删除后无法恢复**:身份丢失后会按设计生成新的匿名身份;若要恢复身份,就需要稳定的派生材料,这会削弱匿名性。 - **Best-effort 并发**:如果读取方恰好落在并发进程完成独占创建但尚未写完的狭窄时间窗内,本次运行可能使用不同的内存 UUID;后续启动会收敛到已持久化的值。 - **没有跨 home 身份**:不同 `$DSH_HOME` 值之间无法关联。 +- **已配置的 DeepSeek gateway 会收到该 id**:`dsh-llm-deepseek` 会把稳定标头发送至解析后的 `baseURL`(包括部署覆盖),且不受遥测共享模式影响。 diff --git a/packages/tasks/tasks-local/README.i18n.yaml b/packages/tasks/tasks-local/README.i18n.yaml index 8b86718b22..869b5f1b05 100644 --- a/packages/tasks/tasks-local/README.i18n.yaml +++ b/packages/tasks/tasks-local/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/tasks/tasks-local/README.md -README.md: 6f9e1bf2524d7db0aff8dac0a43c86e82a20fbab -README.zh.md: 5e62a8289e9c08ac9d745d576d1893b71bcb2f7b +README.md: cc2e8422c367eeacfc5fc504298ecd6bfeae4c67 +README.zh.md: 81fc0a5b1e12b2b15705370bb0748b1733b312aa diff --git a/packages/tasks/tasks-local/README.md b/packages/tasks/tasks-local/README.md index 6f9e1bf252..cc2e8422c3 100644 --- a/packages/tasks/tasks-local/README.md +++ b/packages/tasks/tasks-local/README.md @@ -10,7 +10,7 @@ Tasks belong to their owner and backend, not the producer tool fiber, so produce Service disposal closes listeners, cancels all live tasks, awaits their records, and detaches effects from surviving owner scopes. If teardown cancellation throws, the service force-fails the record and warns that work may be orphaned instead of deadlocking. A cancellation that returns but never settles `done` remains indistinguishable from a slow stop and can stall teardown. -Settlement is first-wins: the earliest terminal outcome — producer settlement, a rejected `done` contained as `failed`, or a teardown force-failure — records once, notifies listeners once with per-listener containment, and releases waiters. Pending waits mark the task reported before listeners run so completion reporters do not duplicate notices. +Settlement is first-wins: the earliest terminal outcome — producer settlement, a rejected `done` contained as `failed`, or a teardown force-failure — records once, releases waiters, and notifies listeners once with per-listener containment. Pending waits mark the task reported before listeners run so completion reporters do not duplicate notices, and a teardown cancel marks it for the same reason: nothing will read a notice addressed to an owner being destroyed. Completion is the last thing a settlement announces, after the record is committed and the visible-set change is published, because a reporter may open a model turn synchronously and every other observer must already have seen the settled record. Controllers and listeners are layered by the scope that registered them, in the tools-registry shape: a registration files into its registering context's scope, and a read unions the global layer with the owner's scope chain. One process-wide registry therefore answers per-owner questions per owner — `start()` refuses `background tasks unavailable: no task controller serves this agent (load @deepseek-ai/dsh-tool-tasks in its composition)` for an owner whose own composition attaches none, however many other compositions attach theirs, and a settlement reaches only the listeners its owner's composition registered. diff --git a/packages/tasks/tasks-local/README.zh.md b/packages/tasks/tasks-local/README.zh.md index 5e62a8289e..81fc0a5b1e 100644 --- a/packages/tasks/tasks-local/README.zh.md +++ b/packages/tasks/tasks-local/README.zh.md @@ -10,7 +10,7 @@ 服务 dispose 会关闭监听器、取消所有存活任务、等待其记录完成,并从仍存活的所有者 scope 中分离 effect。如果销毁期间的取消操作抛出异常,服务会强制将记录标为失败,并警告工作可能成为孤立工作,而不会死锁。取消操作已返回但 `done` 始终未结算时,系统无法将其与缓慢停止区分开,销毁过程可能因此停滞。 -结算遵循首次结算优先原则:最早出现的终止结果(生产方结算、作为 `failed` 隔离处理的 `done` 拒绝,或销毁时的强制失败)只记录一次,也只通知监听器一次;各监听器的故障会单独隔离,随后释放等待方。挂起的等待会在监听器运行前把任务标记为已报告,因此完成报告方不会重复发出通知。 +结算遵循首次结算优先原则:最早出现的终止结果(生产方结算、作为 `failed` 隔离处理的 `done` 拒绝,或销毁时的强制失败)只记录一次,随后释放等待方,再只通知监听器一次;各监听器的故障会单独隔离。挂起的等待会在监听器运行前把任务标记为已报告,因此完成报告方不会重复发出通知;销毁时的取消出于同样的理由也会标记:面向正在被销毁的所有者的通知不会有人读到。完成是一次结算最后才宣布的事情,排在记录提交与可见集变更发布之后,因为报告方可能同步开启一个模型轮次,而该结算的其他所有观察者都必须已经看到已结算的记录。 控制器与监听器按注册方所在的 scope 分层,形状与 tools 注册表一致:一次注册归档到其注册上下文的 scope,一次读取则把全局层与所有者的 scope 链求并集。因此一个进程级注册表能逐所有者地回答逐所有者的问题——对自身组合未附加任何控制器的所有者,无论其他组合附加了多少,`start()` 都会拒绝并抛出 `background tasks unavailable: no task controller serves this agent (load @deepseek-ai/dsh-tool-tasks in its composition)`;一次结算也只会抵达其所有者所属组合注册的监听器。 diff --git a/packages/tasks/tasks-local/src/index.ts b/packages/tasks/tasks-local/src/index.ts index 5a493fcdaa..73c56b9ca6 100644 --- a/packages/tasks/tasks-local/src/index.ts +++ b/packages/tasks/tasks-local/src/index.ts @@ -224,11 +224,12 @@ export class LocalTaskService extends TaskService { } const onAbort = (): void => { task.waitResolvers.delete(onSettled) + // A settled task cannot reach here: settlement releases every waiter + // before it announces completion, and each released waiter detaches + // this listener in the same synchronous span, so nothing that reacts + // to a settlement can abort a wait the settlement already owed. if (timeoutOf(d.signal, TASK_WAIT_TIMEOUT) !== undefined) { resolve() - } else if (isTerminal(task.status)) { - // Settlement suppressed the notice for this waiter; deliver it. - resolve() } else { uncount() reject(new Error('wait aborted')) @@ -364,9 +365,12 @@ export class LocalTaskService extends TaskService { } /** - * Record the first terminal outcome, notify contained listeners, and release - * waiters. First-wins preserves a teardown force-failure against late producer - * settlement. Pending waits mark the task reported before listeners run. + * Record the first terminal outcome, release waiters, then announce + * completion. First-wins preserves a teardown force-failure against late + * producer settlement. Pending waits mark the task reported before listeners + * run. Completion is announced last because a reporter may open a model turn + * synchronously: every other observer of this settlement must already have + * seen the committed record. */ private settle(task: TrackedTask, outcome: TaskOutcome): void { if (isTerminal(task.status)) return @@ -375,24 +379,23 @@ export class LocalTaskService extends TaskService { task.output = outcome.output task.finishedAt = Date.now() if (task.waiters > 0) task.reported = true - if (!this.listenersClosed) { - const snapshot = this.snapshot(task) - for (const listener of this.listenersFor(task.owner)) { - try { - const returned = listener(snapshot, task.owner) - void Promise.resolve(returned).catch((error: unknown) => { - this.selfCtx.logger.warn(`tasks: onTaskDone listener rejected for ${task.id}: ${String(error)}`) - }) - } catch (error: unknown) { - this.selfCtx.logger.warn(`tasks: onTaskDone listener threw for ${task.id}: ${String(error)}`) - } - } - } + const snapshot = this.snapshot(task) const waitResolvers = [...task.waitResolvers] task.waitResolvers.clear() for (const resolveWait of waitResolvers) resolveWait() task.markSettled() this.notifyChanged(task.owner) + if (this.listenersClosed) return + for (const listener of this.listenersFor(task.owner)) { + try { + const returned = listener(snapshot, task.owner) + void Promise.resolve(returned).catch((error: unknown) => { + this.selfCtx.logger.warn(`tasks: onTaskDone listener rejected for ${task.id}: ${String(error)}`) + }) + } catch (error: unknown) { + this.selfCtx.logger.warn(`tasks: onTaskDone listener threw for ${task.id}: ${String(error)}`) + } + } } /** @@ -463,6 +466,14 @@ export class LocalTaskService extends TaskService { private cancelForTeardown(tasks: TrackedTask[], reason: string): void { for (const task of tasks) { if (isTerminal(task.status)) continue + // Teardown cancellation is a kill without a caller, so it claims the + // terminal report the same way `kill()` does. Nothing will read a notice + // for a task whose owner or service is being destroyed, and a waking + // reporter would spend a model request per teardown layer. This is + // decided before the producer runs: the force-failure below settles the + // record too, so a throwing cancel must not be the one path that + // announces an unreported completion into a disposing owner. + task.reported = true try { task.cancel(reason) task.status = 'stopping' diff --git a/packages/tasks/tasks-local/tests/tasks.spec.ts b/packages/tasks/tasks-local/tests/tasks.spec.ts index 78fdd1f5af..56c3fa9181 100644 --- a/packages/tasks/tasks-local/tests/tasks.spec.ts +++ b/packages/tasks/tasks-local/tests/tasks.spec.ts @@ -444,8 +444,9 @@ describe('LocalTaskService.wait', () => { const ctx = await harness() const controller = new AbortController() const seen: TaskSnapshot[] = [] - // The listener aborts after settlement has assigned delivery to this waiter - // but before its resolve microtask; the waiter must still receive the result. + // The listener aborts after settlement released this waiter but before its + // resolve microtask runs. Releasing waiters ahead of the announcement is + // what makes that abort harmless; this is the guard on that ordering. ctx.tasks.onTaskDone((snapshot) => { seen.push(snapshot) controller.abort() @@ -593,6 +594,51 @@ describe('LocalTaskService owner cleanup', () => { expect(ctx.tasks.list(owner)).toEqual([]) }) + it('publishes the settled visible set before announcing completion', async () => { + const ctx = await harness() + const owner = stubAgent(ctx, 'owner') + ctx.agents.register(owner) + const p = producer({ owner }) + ctx.tasks.start(p.spec) + // Registered after start so only the settlement's notifications are ordered. + const order: string[] = [] + ctx.tasks.onTasksChanged(() => void order.push('changed')) + ctx.tasks.onTaskDone(() => void order.push('done')) + + p.settle({ status: 'completed' }) + await tick() + + // A completion reporter may open a turn synchronously. Announcing before + // the visible set is published would let a client render that turn while + // its task row still reads `running`. + expect(order).toEqual(['changed', 'done']) + }) + + it('reports a teardown-cancelled record so completion reporters stay quiet', async () => { + const ctx = await harness() + const owner = stubAgent(ctx, 'owner') + ctx.agents.register(owner) + const seen: TaskSnapshot[] = [] + ctx.tasks.onTaskDone(snapshot => void seen.push(snapshot)) + + let settle!: (outcome: TaskOutcome) => void + ctx.tasks.start({ + kind: 'subagent', + label: 'long research', + owner, + run: () => ({ + cancel() { settle({ status: 'killed' }) }, + done: new Promise((res) => { settle = res }), + }), + }) + + // Observers still receive the terminal record; the report bit is what + // keeps a notice reporter from addressing an owner being destroyed. + await disposeAgentScope(owner) + expect(seen).toHaveLength(1) + expect(seen[0]?.reported).toBe(true) + }) + it('attaches one cleanup per owner and drains all owned tasks with the scope', async () => { const ctx = await harness() const owner = stubAgent(ctx, 'owner') diff --git a/packages/tasks/tasks/src/index.ts b/packages/tasks/tasks/src/index.ts index 114e0a9dbf..ba5ce7eb2f 100644 --- a/packages/tasks/tasks/src/index.ts +++ b/packages/tasks/tasks/src/index.ts @@ -41,12 +41,16 @@ declare module '@deepseek-ai/cordis' { * Implementations must honor these semantics: * - Registrations outlive producer and controller fibers. Owner and * service disposal cancel live work and await compliant producers; a - * throwing teardown cancel force-fails only the record. + * throwing teardown cancel force-fails only the record. Teardown + * cancellation also marks the record reported, because a record its owner + * is being destroyed for has no reader left. * - Owned-task access is fenced by the owner's session id. Ids are * predictable, so authorization — not secrecy — is the boundary. - * - Settlement is first-wins: one terminal record, one round of contained - * listener notification, and released waiters, even against a late - * producer outcome. + * - Settlement is first-wins: one terminal record, released waiters, and one + * round of contained listener notification, even against a late producer + * outcome. Completion is announced last, after the record is committed and + * every other observer of the settlement has seen it, because a reporter + * may open a model turn synchronously. * - {@link start} refuses work while no attached task controller serves the * spec's owner, so a producer cannot start work that owner cannot collect * or stop. One registry serves every composition in the process, so this diff --git a/packages/tasks/tasks/src/types.ts b/packages/tasks/tasks/src/types.ts index 288805aa18..af7b747307 100644 --- a/packages/tasks/tasks/src/types.ts +++ b/packages/tasks/tasks/src/types.ts @@ -118,8 +118,11 @@ export interface TaskSnapshot { /** Epoch ms when the task settled; absent while `running`/`stopping`. */ finishedAt?: number /** - * True when a kill, read, or wait has reported or committed to report the - * terminal state. Completion reporters suppress redundant notices when set. + * True when a kill, read, wait, or teardown cancel has reported or committed + * to report the terminal state. Completion reporters suppress redundant + * notices when set. Teardown claims it because the owner or service being + * destroyed leaves no reader: a reporter that opens a turn on notice would + * otherwise spend a model request per teardown layer. */ reported: boolean } diff --git a/packages/tasks/tool-tasks/README.i18n.yaml b/packages/tasks/tool-tasks/README.i18n.yaml index a605f3d8ce..f5e7dbef20 100644 --- a/packages/tasks/tool-tasks/README.i18n.yaml +++ b/packages/tasks/tool-tasks/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/tasks/tool-tasks/README.md -README.md: 76607351aba6b482817b890056254da737e7b9ed -README.zh.md: 8d27510aa3be3b87ca167d88d85fc374fb1eb4f1 +README.md: a899fbaeb402096f523e230ee03d6f422d321810 +README.zh.md: d0d4b7bdc66d5261d914ae8380488c36c17a0f0a diff --git a/packages/tasks/tool-tasks/README.md b/packages/tasks/tool-tasks/README.md index 76607351ab..a899fbaeb4 100644 --- a/packages/tasks/tool-tasks/README.md +++ b/packages/tasks/tool-tasks/README.md @@ -18,7 +18,11 @@ When a producer supplies `outputLimitBytes`, `task_output`, terminal `task_kill` ## Completion notices -An unreported completion injects `background task (: