@whaletalk/dsh-codex-collab 0.1.7 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +197 -26
  3. package/codex-plugin/plugins/dsh/.codex-plugin/plugin.json +1 -1
  4. package/codex-plugin/plugins/dsh/skills/dsh-collab/SKILL.md +102 -6
  5. package/codex-plugin/plugins/dsh/skills/dsh-collab/scripts/review.mjs +91 -6
  6. package/codex-plugin/plugins/dsh/skills/dsh-collab/scripts/task.mjs +571 -44
  7. package/docs/CODEX_DSH_APPROVAL_DELEGATION_PLAN.md +130 -0
  8. package/docs/CODEX_DSH_APPROVAL_M0_EVIDENCE.md +285 -0
  9. package/docs/CODEX_DSH_DELIVERY_CONTROL_PLAN.md +81 -0
  10. package/docs/CODEX_DSH_MODEL_ROUTING_PLAN.md +75 -0
  11. package/docs/CODEX_DSH_TASK_REPLY_PLAN.md +206 -0
  12. package/docs/DSH_APPROVAL_DELEGATION.md +142 -0
  13. package/docs/DSH_DELIVERY_M0_EVIDENCE.md +69 -0
  14. package/docs/DSH_MODEL_ROUTING_M0_EVIDENCE.md +166 -0
  15. package/docs/DSH_TO_CODEX_INBOX.md +167 -0
  16. package/docs/DSH_TO_CODEX_MESSAGING_PLAN.md +140 -0
  17. package/docs/codex-desktop-attachment-evidence.md +116 -0
  18. package/harness/audit-tail.mjs +32 -0
  19. package/harness/codex-approvals.mjs +1078 -0
  20. package/harness/codex-associations.mjs +297 -0
  21. package/harness/codex-outbox.mjs +432 -0
  22. package/harness/dsh-bridge.mjs +2429 -167
  23. package/harness/fs-lock.mjs +208 -0
  24. package/harness/model-route-host.mjs +530 -0
  25. package/harness/model-routing.mjs +500 -0
  26. package/harness/session-observer.mjs +217 -0
  27. package/harness/session-target.mjs +291 -2
  28. package/harness/task-table.mjs +34 -0
  29. package/mcp/codex-desktop-probe.mjs +407 -0
  30. package/mcp/codex-inbox.mjs +271 -0
  31. package/mcp/dsh-mcp.mjs +616 -24
  32. package/package.json +4 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ ## 0.1.10 — 2026-10-20
4
+
5
+ - 修复大对话每 2.5 秒全量读取:同会话共享 SDK 增量事件/有界 follow,低成本观察只用于初始化或补缺;慢检查互斥、失败退避,空闲 worker 零历史读取。
6
+ - 审批/模型选择复用连续证据索引,关键决定仍做新鲜核验;缺口、卸载、缓冲溢出时安全停止取证,不以过期缓存放行。
7
+ - 模型审计状态读取改为缓存最近 1 MiB,保留完整原始审计文件;debug 新增观察计数与审计尾部统计。
8
+ - 兼容 DSH 0.2.1 的 `startActivation`(同时保留 0.2.0 入口);worker 未确认时保留唯一归属,迟到的官方结束仍结算原任务。
9
+ - 一并发布已验收的审批委托、来源绑定的 queue/steer、模型目录及 DeepSeek 优先自动选择/有界故障切换;MCP 工具共 9 个。
10
+ - 增加共享观察、快照补缺、慢读竞态、失败退避、缓冲上限和审计回归;保留上游署名和 MIT 许可。
11
+
12
+ ## 0.1.9 — 2026-10-13
13
+
14
+ > **0.1.8 从未发布到 npm。** `v0.1.8` tag 建立后,CI 的 Linux gate 在 `test/codex-associations.test.mjs` 暴露了一个**仅测试代码**的跨平台断言缺陷:
15
+ > 读取"目录位置被文件占住"的路径时,Linux 报 `ENOTDIR`(抛 `store-unreadable`)、Windows 报 `ENOENT`(落 `unknown-task`),而原断言只认后者。
16
+ > 已把该断言改为跨平台不变量(写失败必须抛错;读失败必须响亮、且不得带出 record)。按"不得覆盖既有 tag/version"的规定,本版改用 **0.1.9** 发布;
17
+ > **功能内容与 0.1.8 完全相同**(tag `v0.1.8` 仍指向当时的提交,未被移动)。
18
+
19
+ ### 来源关联(R1):Codex 派活 → DSH 执行 → 按来源精确收回
20
+
21
+ - **持久绑定**:`dsh_task` / `dsh_review`(MCP)、`POST /api/dsh-bridge/task|review` 与 CLI 新增可选 `originThreadId`(发起这次派活的 Codex 对话 id)。
22
+ 网关在**执行前**把「来源对话 → taskId → 执行目标」落盘到 `~/.dsh-codex-task-assoc/<namespace>/bindings.jsonl`(追加型 JSONL + 独占写锁,`DSH_CODEX_TASK_ASSOC_DIR` / `DSH_BRIDGE_ASSOC_NAMESPACE` 可覆盖);
23
+ **首次绑定失败就不启动执行**,终态/失败/取消/交付确认都写回。
24
+ - **跨回合/跨进程收取**:`dsh_task_status`(MCP)与 `dsh-task --my-tasks` / `--status <taskId> --origin <id>`(CLI)按 `origin + taskId` 精确取值,或只给来源列出该对话登记过的任务(找回丢失的 taskId)。
25
+ 缺来源 → `origin-required`、错来源 → `origin-mismatch`,**绝不返回别的对话的结果**;旧的无来源调用保持兼容。
26
+ - **会话任务按请求回合关联结果**(不再依赖"派发前基线 + 全会话 idle"):`prompt.requestId` → `user/message.source.rpcId` → 按 `seq` 有序跟踪未闭合 turn → 只收**自己那个 turn** 的助手文本;
27
+ **只有 `turn/end` 的 `reason.kind === completed` 算成功**,`aborted/blocked/error/max-tokens/interrupted/forked` 是明确失败(部分文字进 `partialResult`)。
28
+ follow 开场的 `snapshot.records` 也会被检查(快任务可能已跑完);`api-session/status` 只当作"读一次日志的机会",**不是**完成证明;没有证据 → `status:"unknown"` + `resend:false`(不猜 done、不重发)。
29
+ - **带来源时拒绝 `steer`**(`origin-steer-unsupported`,执行前拒绝、零投递):若干请求会共用同一个 turn,无法为单个请求保证归属。
30
+ - **能力握手**:`/status`、`/tasks`、`/debug` 上报 `capabilities.originBinding`;MCP/CLI 带来源时先只读核验(不靠包版本猜),旧网关 → **零执行拒绝**;
31
+ 网关接受请求但未回显匹配的 `originThreadId` → 按"未确认、不要重发"上报(CLI 退出码 8)。
32
+ - **同通道单在途**:同一 `cwd + lane`(评审=同目录)有在途任务时明确 `worker-bucket-busy` / `reviewer-bucket-busy` 拒绝,避免完成结果归属靠猜;不同 lane/目录仍并行。
33
+
34
+ ### DSH → Codex 收件箱与只读探针
35
+
36
+ - `mcp/codex-inbox.mjs` + `harness/codex-outbox.mjs`:追加型事件日志 + **单把独占写锁**(`harness/fs-lock.mjs`,与关联存储共用同一实现;在线竞争期间绝不自动回收,死持有者/不可确认元数据都 fail closed 并给人工清理步骤)。
37
+ - `mcp/codex-desktop-probe.mjs`:只读能力探针;采集层 `-ErrorAction Stop` + 显式 payload 标记,**非终止错误不会被当成"扫描成功且为空"**;
38
+ 桌面身份要求祖先链先遇到安装式桌面 App(穿过另一份 app-server 的派生实例单列)。
39
+ - **桌面自动唤醒仍为 blocked**(M0:本机桌面 app-server 以 `stdio://` 由 `ChatGPT.exe` 私有持有,无可附着端点);`canWakeIdle` 恒为 `false`,本版不宣称自动唤醒。
40
+
41
+ ### 验证
42
+
43
+ `node --test test/*.test.mjs`:**117 项**通过(含真实 `http.Server` + 独立 MCP/CLI 进程的跨回合收取、重启恢复、旧网关零执行拒绝等端到端用例);
44
+ 另通过 Codex 侧独立正向契约检查(入口 12/12、会话关联 4/4)与真实 canary。
package/README.md CHANGED
@@ -4,6 +4,12 @@
4
4
 
5
5
  # DSH Collab — Codex × DeepSeek Harness 双向编码协作
6
6
 
7
+ 本项目 fork 自 [FuJackGao/dsh-collab](https://github.com/FuJackGao/dsh-collab),在上游基础上由 [Whale-Talk](https://github.com/Whale-Talk) 继续维护与扩展。感谢原作者及上游贡献者。
8
+
9
+ [安装与下载](#安装) · [升级方法](#升级到新版本) · [使用示例](#使用) · [npm 包](https://www.npmjs.com/package/@whaletalk/dsh-codex-collab) · [GitHub 源码](https://github.com/Whale-Talk/dsh-codex-collab)
10
+
11
+ > **版本说明**:0.1.10 提供审批委托、queue/steer 投递控制、已配置模型自动选择与故障切换,以及大对话增量观察修复。升级时同时更新 DSH 网关和 Codex 侧 MCP/CLI,并重启 DSH 使新模块生效。
12
+
7
13
  **Codex 主导,DeepSeek 执行,Codex 验收。** 一个插件体系,让 ChatGPT(Codex)像项目经理一样把编码任务派给本机 DeepSeek Harness 的编码子代理,实时回传结果,读文件验收,并由独立评审员实际跑通构建/测试后出具评审报告。
8
14
 
9
15
  派活有**两种目标**,必须显式选一种——"接进用户正在用的那条对话"和"新建一个干活的子代理"是两件事:
@@ -55,15 +61,25 @@ flowchart LR
55
61
 
56
62
  ---
57
63
 
64
+ ## 大对话性能与排障
65
+
66
+ 同一会话的任务共享增量观察;健康的同进程事件或 follow 连接不会再每 2.5 秒全量读取历史。启动和断流补缺使用 SDK 的 `observeSession({ projectionMode: "none" })`,及时释放观察租约;旧 SDK 才降级到串行的 `readSession`。后台失败会退避,慢检查不会重叠,空闲 worker 不读取历史。
67
+
68
+ 兼容 DSH 0.2.0 与 0.2.1 的子代理入口。worker 长时间安静时只报告未确认,继续保留该任务的执行占用,收到官方结束后再回填结果;不会把旧结果交给后来派发的任务。
69
+
70
+ 审批和人工模型选择仍要求新鲜证据。事件缺口、读取失败或缓冲溢出会补缺或返回未确认,不会把缓存当作批准依据。增量回放与慢读缓冲各有 4 MiB 上限,不保留工具结果正文;任务自身的最终回答仍按原有方式返回。
71
+
72
+ 只读访问 `GET /api/dsh-bridge/debug` 可查看 `observation` 的完整读取/观察/缺口/失败计数,以及 `routeAuditTail` 的读取字节与截断标志。模型审计的状态回显只解码最近 1 MiB,原始 JSONL 仍完整保留,可用于历史回溯。冷会话第一次载入仍可能较慢;这次修复针对重复读取,不代表 DSH 的其他界面或插件性能问题均已解决。
73
+
58
74
  ## 组件
59
75
 
60
76
  | 目录 | 内容 | 说明 |
61
77
  |---|---|---|
62
78
  | `codex-plugin/` | Codex 本地市场插件包 | 标准 marketplace 结构(`.agents/plugins/marketplace.json` + 插件目录),含协作技能 `dsh-collab` |
63
- | `mcp/dsh-mcp.mjs` | MCP 服务器(零依赖 Node) | stdio 传输(Codex `[mcp_servers]` 直连)+ Streamable HTTP 传输(`--http`,供 Secure MCP Tunnel / cloudflared 桥接 ChatGPT);6 个工具:`dsh_task` / `dsh_sessions` / `dsh_task_status` / `dsh_task_cancel` / `dsh_review` / `dsh_read_file` |
79
+ | `mcp/dsh-mcp.mjs` | MCP 服务器(零依赖 Node) | stdio 传输(Codex `[mcp_servers]` 直连)+ Streamable HTTP 传输(`--http`,供 Secure MCP Tunnel / cloudflared 桥接 ChatGPT);9 个工具:`dsh_task` / `dsh_sessions` / `dsh_task_status` / `dsh_task_cancel` / `dsh_review` / `dsh_read_file` / `dsh_approvals` / `dsh_approval_decide` / `dsh_models` |
64
80
  | `harness/dsh-bridge.mjs` | DeepSeek Harness 宿主组合网关插件 | 提供 HTTP 网关(`/api/dsh-bridge/*`)、WS 推送、原生工具;管理编码/评审子代理,并把任务投递进已有会话。**必须装进 harness 宿主组合**(见下文) |
65
- | `harness/session-target.mjs` | 会话目标的纯决策层 | 目标判定、候选挑选(0/1/N 不猜)、RemoteError→稳定 reason、基线切分、汇报选文。无 DSH import,可单测;`dsh-bridge.mjs` 依赖它,**两个文件必须一起部署** |
66
- | `test/` | 25 项 Node 测试 | `session-target` 纯函数单测 + `dsh-bridge` 路由级回归测试(假 DSH 运行时加载真实 bridge)。CI 每次都跑 |
81
+ | `harness/session-target.mjs` | 会话目标的纯决策层 | 目标判定、候选挑选(0/1/N 不猜)、RemoteError→稳定 reason、基线切分、汇报选文。无 DSH import,可单测;`dsh-bridge.mjs` 依赖它,**源码手工部署时保留整个 `harness/` 目录** |
82
+ | `test/` | Node 行为回归测试 | `session-target` 纯函数单测 + `dsh-bridge` 路由级回归测试(假 DSH 运行时加载真实 bridge)。CI 每次都跑 |
67
83
  | `templates/codex-mcp/` | 反向通道配置模板 | 把 Codex CLI 当 MCP 服务端挂进 DSH(得到 `mcp__codex__*` 工具),让 DSH 侧也能调用 Codex。见下文「反向通道」 |
68
84
  | `cordis.patch.yml` | bundle patch | 把网关插件挂进 DSH 宿主组合;`dsh.bundle.patch` 指向它,所以 `dsh plugin add` 能自动 reconcile |
69
85
 
@@ -81,7 +97,17 @@ flowchart LR
81
97
 
82
98
  ## 安装
83
99
 
84
- ### 0. 一条命令装全部(推荐,npm)
100
+ ### npm 安装(推荐)
101
+
102
+ 前提:Node.js **20.11 或更新版本**、npm,以及已安装的 DeepSeek Harness(desktop 或 web profile)。本包提供插件和协作工具,DSH 本体需另行安装。
103
+
104
+ 安装包名为 **`@whaletalk/dsh-codex-collab`**。可先查询 npm 已发布版本:
105
+
106
+ ```sh
107
+ npm view @whaletalk/dsh-codex-collab version
108
+ ```
109
+
110
+ 下面两步分别安装 DSH 网关和 Codex 侧工具,两端都需要:
85
111
 
86
112
  ```sh
87
113
  # 1) 网关插件装进 harness profile(自动 reconcile bundles,无需手工复制文件)
@@ -109,12 +135,13 @@ curl http://127.0.0.1:3080/api/dsh-bridge/status
109
135
 
110
136
  装完**必须重启 profile 进程**——`dsh plugin add` 改的是 bundles 列表与 node_modules,HMR 不监控这两处;**替换包内容也不会热重载**(ESM 缓存),只有重启进程才加载新版本。
111
137
 
112
- ### 升级到新版本(有坑,照做)
138
+ ### 升级到新版本
113
139
 
114
140
  ```sh
115
- # 1) 带精确版本号安装:不带版本号时 lockfile 会把旧版按回来
116
- dsh plugin --profile desktop add @whaletalk/dsh-codex-collab@<新版本>
117
- # 2) 重启 profile 进程:不重启不会加载新的 JS 模块
141
+ # 将 X.Y.Z 替换成 npm 已发布的新版本号;两端使用同一版本
142
+ dsh plugin --profile desktop add @whaletalk/dsh-codex-collab@X.Y.Z
143
+ npm install -g @whaletalk/dsh-codex-collab@X.Y.Z
144
+ # 保存当前工作后,重启 DSH profile,并重新加载 Codex 的 MCP 连接
118
145
  ```
119
146
 
120
147
  - **不要用"卸载 → 重装"**:卸载会把这个包从 `dsh.profile.bundles` 移除;重装后若没被重新选中,插件根本不会挂载(表现:所有 `/api/dsh-bridge/*` 都落到网关鉴权层返回 **401**)。真遇到了就在客户端插件页把它的开关打开。
@@ -135,7 +162,7 @@ dsh plugin --profile desktop add @whaletalk/dsh-codex-collab@<新版本>
135
162
  name: './harness/dsh-bridge.mjs'
136
163
  ```
137
164
 
138
- > `dsh-bridge.mjs` 会 `import './session-target.mjs'`——**两个文件必须在一起**,只复制前者会直接加载失败。
165
+ > 网关依赖同目录下的会话关联、审批和锁模块。**复制整个 `harness/` 目录并保留结构**,不要只挑 `dsh-bridge.mjs` 和 `session-target.mjs`。
139
166
 
140
167
  重启 harness。默认共享工作目录为 `D:\Harness`,可用环境变量 `DSH_BRIDGE_WORKSPACE` 覆盖。验证:
141
168
 
@@ -147,12 +174,12 @@ GET http://127.0.0.1:3080/api/dsh-bridge/status
147
174
 
148
175
  ### 2. Codex 侧(二选一或都用)
149
176
 
150
- **A. MCP 工具(推荐,走 Codex 官方通道)**:把 `mcp/dsh-mcp.mjs` 放到本机任意位置,在 `~/.codex/config.toml` 追加:
177
+ **A. MCP 工具**:保留完整源码目录(包括 `mcp/` 和 `harness/`),在 `~/.codex/config.toml` 追加;不要把 `dsh-mcp.mjs` 单独拷走,它会导入 `../harness/codex-approvals.mjs`:
151
178
 
152
179
  ```toml
153
180
  [mcp_servers.dsh]
154
181
  command = 'node'
155
- args = ['<绝对路径>/dsh-mcp.mjs']
182
+ args = ['<项目绝对路径>/mcp/dsh-mcp.mjs']
156
183
  startup_timeout_sec = 120
157
184
  ```
158
185
 
@@ -204,12 +231,15 @@ tunnel-client run --profile dsh
204
231
  ```jsonc
205
232
  // 1) 找到会话(只读)
206
233
  dsh_sessions({ "query": "按文档启动OKX策略实验计划" })
207
- // → { "items": [{ "sessionId": "session-8d481ad9-…", "snippet": "按文档启动OKX策略实验计划",
234
+ // → { "items": [{ "sessionId": "session-example-…", "snippet": "按文档启动OKX策略实验计划",
208
235
  // "agentAvailable": true }], "matchedBy": "list" }
209
236
 
210
- // 2) 投递进那条会话
211
- dsh_task({ "instruction": "继续推进第 3 步…", "sessionId": "session-8d481ad9-…" })
212
- // deliver 可选 "queue"(默认,排队)或 "steer"(插入当前回合)
237
+ // 2) 排队派活,记录返回的 taskId(来源必须显式传当前 Codex 对话 id)
238
+ dsh_task({ "instruction": "继续推进第 3 步…", "sessionId": "session-example-…", "originThreadId": "<当前 Codex 对话 id>", "deliver": "queue", "wait": false })
239
+
240
+ // 3) 当前源码支持:向正在运行的父任务补充(需控制凭据与投递能力)
241
+ dsh_task({ "instruction": "给原任务补充验收要求…", "sessionId": "session-example-…", "originThreadId": "<当前 Codex 对话 id>", "deliver": "steer", "parentTaskId": "<上一步 taskId>", "wait": false })
242
+ // 返回的是补充回执;最终结果仍按原 originThreadId + 父 taskId 查询。父任务尚在排队或已结束时不能 steer。
213
243
  ```
214
244
 
215
245
  **命令行**(npm 安装后直接用;源码方式用 `node <脚本路径>`):
@@ -221,8 +251,10 @@ dsh-task --in "<指令>" --cwd "<目录>" [--lane backend] [--model fast|pro] [-
221
251
  # 接进已有会话:先找(或先空跑验证),再派
222
252
  dsh-task --sessions "<标题关键词>" # 只读列候选,拿 sessionId
223
253
  dsh-task --dry-run --find "<标题关键词>" # 只解析目标、回显命中,不投递任何消息
224
- dsh-task --in "<指令>" --session "<sessionId>" [--steer]
225
- dsh-task --in "<指令>" --find "<标题关键词>" # 唯一命中才派;否则退出码 4
254
+ dsh-task --in "<指令>" --session "<sessionId>" --origin "<当前 Codex 对话 id>"
255
+ # 当前源码:补充一条正在运行的父任务;--parent 同时选择 steer
256
+ dsh-task --in "<补充>" --session "<sessionId>" --origin "<当前 Codex 对话 id>" --parent "<父 taskId>"
257
+ dsh-task --in "<指令>" --find "<标题关键词>" --origin "<当前 Codex 对话 id>" # 唯一命中才派;否则退出码 4
226
258
 
227
259
  dsh-review --cwd "<目录>" [--diff git|@文件|"diff文本"] [--focus "<重点>"]
228
260
  dsh-task --list | --status <taskId> | --cancel <taskId> [--force]
@@ -233,7 +265,7 @@ dsh-task --list | --status <taskId> | --cancel <taskId> [--force]
233
265
  ```bash
234
266
  # 1) 找到(只读,不创建任何东西)
235
267
  dsh-task --sessions "按文档启动OKX策略实验计划"
236
- # → {"items":[{"sessionId":"session-8d481ad9-…","snippet":"按文档启动OKX策略实验计划",
268
+ # → {"items":[{"sessionId":"session-example-…","snippet":"按文档启动OKX策略实验计划",
237
269
  # "agentAvailable":true}],"total":1,"matchedBy":"list"}
238
270
 
239
271
  # 1b) 没命中时会给近似候选(按标题相似度 + 最近更新排序),换词重查或改用 sessionId——
@@ -243,12 +275,12 @@ dsh-task --dry-run --find "量化框架"
243
275
  # "candidates":[{"sessionId":"1c0f99f8-…","snippet":"你是一个量化研究项目的技术","score":0.333}, …]}
244
276
 
245
277
  # 2) 接通(投递进那条会话;这一轮的助手回复作为汇报返回,只回报本轮新增文本)
246
- dsh-task --in "<继续推进的指令>" --session "session-8d481ad9-…"
278
+ dsh-task --in "<继续推进的指令>" --session "session-example-…" --origin "<当前 Codex 对话 id>"
247
279
  ```
248
280
 
249
281
  `matchedBy` 说明"找到"走了哪条路:`search` = 会话搜索索引;`list` = 索引被部署禁用时退回遍历列表 + 标题投影(都不激活 Agent)。
250
282
 
251
- **`delivered` 与排队语义**(重要):`prompt` 一旦被目标会话接受,就表示消息**已经进了它的队列**,响应里会带 `delivered: true`;此后即便我们的观察超时(会话可能正跑一个很长的回合),也**不要重发**——重发会让同一份内容在用户会话里出现两次。`queue` 模式不再用 120s 判失败:没在 120s 内开跑只记一条 note 并继续等,最终由总预算(默认 1h)收口为 `session-timeout` + `delivered: true`。
283
+ **`delivered` 与排队语义**(重要):`prompt` 一旦被目标会话接受,就表示消息**已经进了它的队列**,响应里会带 `delivered: true`;此后即便我们的观察超时(会话可能正跑一个很长的回合),也**不要重发**——重发会让同一份内容在用户会话里出现两次。已接收的 `queue` 任务未在 120s 内开跑只记 note;带来源任务必须观察到自身回合结束,观察窗口到期而证据不足时返回 `unknown` + `resend:false`(不把观察超时当执行失败)。有效审批委托按其授权窗口观察。
252
284
 
253
285
  失败返回稳定 `reason` 且**不创建任何会话/工作区**:`session-query-empty`(没命中,附 `candidates`)、`session-ambiguous`(多条命中,附 `candidates`)、`session-not-found`、`session-busy`、`session-writer-held`(会话被别的写入方占用)、`session-archived`、`session-not-accepted`(仅当 prompt **未被接受**)、`session-timeout`、`session-cancel-refused`(取消会话目标默认被拒,需 `--force`)。
254
286
 
@@ -304,6 +336,129 @@ codex exec "把刚才的 diff 审一遍,只回结论" --cd E:\proj
304
336
 
305
337
  > 注意:插件自带的 `dsh_collab_send` / `dsh_collab_review` 是 **DSH 侧入口**,只回到同一个网关,**不经过 Codex**。要"叫 Codex 干活"用上面 A/B;要"让 Codex 那条对话继续",只能等 Codex 自己下一轮(`dsh_task --wait` 或 `dsh_task_status`)。
306
338
 
339
+ ## DSH → Codex 收件箱(桌面自动唤醒:blocked)
340
+
341
+ 目标形态是"DSH 主动往 Codex Desktop 正在用的那条聊天发一条消息,并让它开始处理"。**M0 实测本机做不到**:桌面 app-server 以默认 `stdio://` 启动、端点归 `ChatGPT.exe` 私有,既没有 `--listen` 出的 ws/TCP 端点,也没有文档所说的 app-server 控制套接字——详见 [`docs/codex-desktop-attachment-evidence.md`](docs/codex-desktop-attachment-evidence.md)。
342
+
343
+ 因此先交付可独立验证的一半(用法见 [`docs/DSH_TO_CODEX_INBOX.md`](docs/DSH_TO_CODEX_INBOX.md)):
344
+
345
+ ```bash
346
+ node mcp/codex-desktop-probe.mjs --schema # 只读能力探针:还要不要继续谈唤醒,看它
347
+ node mcp/codex-inbox.mjs capabilities # 能力如实上报:deliveryMode=inbox, canWakeIdle=false
348
+ node mcp/codex-inbox.mjs bind --binding <id> --thread <codexThreadId> [--source <DSH会话id>]
349
+ node mcp/codex-inbox.mjs send --binding <id> --text "<正文>" [--key <幂等键>]
350
+ node mcp/codex-inbox.mjs pending # Codex 侧/用户主动来读
351
+ ```
352
+
353
+ - 收件箱:追加型事件日志 + **单把独占写锁**(幂等检查 / seq 分配 / 绑定更新 / 追加都在锁内)。
354
+ 锁是 `<root>/.write.lock` 这个共享路径上的**一次原子独占创建**(硬链接发布 / `wx`),谁创建成功谁进临界区;
355
+ **在线竞争期间绝不自动回收**("检查共享路径再删除"无法原子化,会删掉别人刚建的新锁)。
356
+ 持有者活着 → 有限等待后 `lock-timeout`;持有者已死 → **二次确认**后 `lock-stale-holder`;元数据不可确认 → `lock-unverified-holder`;
357
+ 后两者都是**可恢复错误 + 人工清理**(文档给了步骤),宁可阻塞也不冒双写者风险。
358
+ 追加前修复**记录边界**,残尾不会吞掉后续写入;状态机 `queued → delivered → acked`(另有 `failed` / `delivery-unknown`);
359
+ 存储损坏明确报错,不伪装成空收件箱。
360
+ - **未绑定一律拒绝**,不猜目标、不从最近使用里挑默认;授权线程 ID 只存在于本地绑定,不作为发布默认值。
361
+ - CLI 参数严格:带值旗标缺值或未知旗标一律 exit 2,且在打开存储之前拒绝。
362
+ - 明确不宣称:**自动唤醒、观察回合事件均未实现**;死持有者需人工清理(非自动恢复)、断电级持久性、
363
+ 跨机(网络文件系统)锁语义均未验证/不自动(见用法文档 §4)。
364
+ - 现有六个 MCP 工具与 `dsh_collab_send` 语义未改动。
365
+
366
+ 验证:`node test/codex-outbox.test.mjs`(24 项,含真多进程并发、6 进程压测、活持有者期间零写入)、
367
+ `node test/codex-inbox-cli.test.mjs`(6 项)、`node test/codex-desktop-probe.test.mjs`(11 项,含采集层失败判定)、
368
+ `node mcp/codex-desktop-probe.mjs --schema`(现场能力报告)。
369
+
370
+ ## 来源关联:Codex 对话 → 任务 → 结果(跨回合精确收取)
371
+
372
+ "本来就是 Codex 驱使 DSH 派活"这件事现在有**显式身份**了:派活时带上发起对话 id(`originThreadId`),
373
+ 网关在**执行前**把「来源对话 → taskId → 执行目标」持久落盘;之后任何回合(哪怕是新的 MCP 进程、网关重载后的新实例)
374
+ 都能按 `originThreadId + taskId` 精确取回这次任务的结果。详见 [`docs/CODEX_DSH_TASK_REPLY_PLAN.md`](docs/CODEX_DSH_TASK_REPLY_PLAN.md)。
375
+
376
+ ```bash
377
+ # 派活(MCP 工具 dsh_task / dsh_review 的 originThreadId 参数同理,必须显式传当前对话 id)
378
+ dsh-task --in "<指令>" --cwd "<目录>" --origin "<当前 Codex 对话 id>"
379
+
380
+ # 下一个回合:只凭来源找回 taskId,再精确取结果
381
+ dsh-task --my-tasks --origin "<当前 Codex 对话 id>"
382
+ dsh-task --status "<taskId>" --origin "<当前 Codex 对话 id>"
383
+ ```
384
+
385
+ - **持久关联记录**:`{originThreadId, taskId, target, status, createdAt, updatedAt, delivered, result/error}`,
386
+ 追加型 JSONL + 与收件箱**同一套独占写锁**;位置在用户数据目录 `~/.dsh-codex-task-assoc/<namespace>/bindings.jsonl`
387
+ (`DSH_CODEX_TASK_ASSOC_DIR` / `DSH_BRIDGE_ASSOC_NAMESPACE` 可覆盖;**测试全部用临时 root,绝不默认写仓库**)。
388
+ namespace 是**校验过的短标识**且 origin/taskId 从不参与路径拼接;日志追加 + taskId 全局唯一 ⇒ 多个网关不会互相覆盖记录。
389
+ - **绑定不可改投**:同一 taskId 换来源直接拒绝;`originThreadId` 必须是合法非空字符串——**字段未传**才是旧的无关联模式,
390
+ 显式给空串 / null / 纯空白 / 非字符串一律在执行前拒绝(`invalid-origin`),绝不静默回落或改用环境变量。
391
+ - **执行前落盘**:首次持久绑定失败就**不启动 DSH 执行**(绝不出现"执行了却没绑定");终态/失败/取消/交付确认都写回。
392
+ - **派活前只读核验网关能力**:`/status`、`/tasks`、`/debug` 上报 `capabilities.originBinding.supported`;
393
+ MCP/CLI 带来源时必须先确认它为 true(**不靠包版本猜**)——旧网关会忽略 `originThreadId` 却照常返回 accepted,
394
+ 从而产生未绑定任务,所以不支持时**零执行拒绝**。旧的无来源调用完全兼容。
395
+ - **会话任务的结果归属**(带来源时):不能再用"派发前基线 + 全会话 idle"(那会把别的请求的输出算给当前 taskId)。
396
+ 改用 `prompt.requestId`(=taskId)→ DSH 的 `user/message.source.rpcId` → 按 `seq` 有序跟踪未闭合回合 → 只收**自己那个 turn** 的助手文本,
397
+ 并且**只有该 turn 的 `turn/end.reason.kind === 'completed'` 才算成功**(其余 aborted/blocked/error/max-tokens/interrupted/forked 是明确失败,
398
+ 部分文字放 `partialResult`)。follow 开场的 `snapshot.records` 也会被检查(快任务可能已经跑完);`api-session/status` 只当作"读一次日志的机会",
399
+ **不是**完成证明。落盘并回显(内存分支与恢复分支都给):`requestId`(=taskId)与匹配到的 `dshTurn`;
400
+ `turnEndKind`/`partialResult` 以及未确认时的 `resend`/`note` **只在活任务的内存响应里**出现(不在持久记录里;
401
+ 重启后非终态记录由恢复分支合成 `unknown` + `resend:false`)。
402
+ - **带来源的 `steer` 是父任务补充回执**:必须显式给同来源、同会话且正在运行的父 `taskId`,并有控制凭据。回执只报告自身是否被观察、是否偏离目标回合;不承载父结果、不成为审批 owner。旧网关没有 `capabilities.delivery` 时,客户端在派活前拒绝。
403
+ - **没有证据就是未确认**:无匹配 `rpcId` 的终态、quiet/预算到期 → `status:"unknown"` + `resend:false`,绝不猜 done、绝不重发。
404
+ - **网关必须回显并匹配 `originThreadId` 才算确认绑定/取值**:能力检查通过≠这次绑定成功(网关可能中途重载/旧 handler)。
405
+ 未确认时 MCP/CLI 都按"未确认 + 禁止重发"上报(CLI 退出码 8),**不返回**那种响应里的结果。
406
+ - **读结果必须匹配来源**:缺来源 → `origin-required`、错来源 → `origin-mismatch`(内存与重启后一致,`/cancel` 同样检查),
407
+ **绝不返回别的对话的结果**;只有真正未关联的旧任务保持"只给 taskId"兼容查询。
408
+ - **同一通道(cwd+lane,评审=目录)同时只允许一个在途任务**:第二个明确 `worker-bucket-busy` / `reviewer-bucket-busy` 拒绝——
409
+ 完成事件只能归属到通道里最老的那个任务,并发派活会让结果归属变成猜测。不同 lane/目录仍并行;上一任务结束后可继续复用同一子代理。
410
+ - **恢复边界**:已完成结果在网关重载/新实例后仍能按 `origin+taskId` 取回;**没跑完**的任务(精确查询或只给来源列表)一律返回
411
+ `status: "unknown"` + `resend: false`(执行/交付状态未能确认,保留已知 `delivered` 与原目标),**不谎报完成、不自动重发、不去猜 DSH 最近助手文本**。
412
+ 内存 200 条上限只淘汰**终态**任务(活任务永不被挤掉),持久记录不受影响。
413
+ - 不按"最近/标题/时间"猜发起对话;`taskId` 是关联标识,**不是授权凭据**,也不等于线程 id。
414
+ - **自动唤醒仍然 `false`**:来源关联解决的是"派活回报精确归属",不是"唤醒空闲桌面聊天"。
415
+
416
+ 验证:`node test/codex-r1-contracts.test.mjs`(8 项:非法来源零执行、已关联任务缺/错来源拒绝、通道 busy、旧网关零执行拒绝、能力上报、
417
+ 能力通过但未确认来源 → 未确认+禁止重发、列表 unknown 语义、CLI 严格参数)、
418
+ `node test/codex-session-correlation.test.mjs`(8 项:真实 SDK 字段的请求↔回合关联:排队请求忽略先前回合、只有自己 turn/end 才完成、
419
+ error/blocked 不得 done、snapshot.records、重复投递不串文本、快照缺前半段回填、无匹配 rpcId → 未确认、无有效父任务的 steer 拒绝)、
420
+ `node test/codex-association-e2e.test.mjs`(3 项:**真实 HTTP + 独立 MCP 进程 + 独立 CLI 进程** 的跨回合收取与重启恢复)、
421
+ `node test/codex-associations.test.mjs`(13 项:存储/不可改投/来源校验/失败不泄漏 record/跨重启/命名空间隔离/真多进程并发/任务表淘汰)、
422
+ `node test/dsh-bridge.test.mjs`(13 项:非法来源 400、执行前落盘、新实例恢复、200+ 记录、存储故障拒绝派活)。
423
+
424
+ ## 审批委托:Codex 接手 DSH 单次审批(needs-approval 闭环)
425
+
426
+ 当 DSH 执行需要一次审批(沙箱升级等)而你不在电脑前时,受委托且正在等待/处理任务的 Codex 对话可以**逐次**决定。前提是网关与客户端都已加载支持该功能的版本,控制凭据、委托范围与租约有效;它不会自动唤醒空闲的 Codex 聊天。超出范围、证据不足、断线/过期时交还人工链。
427
+
428
+ ```bash
429
+ # 0) 只做一次:离线初始化控制凭据(仓库/工作区之外的私有目录;raw key 不回显、不上线)
430
+ dsh-task --approval-key-init # 轮换:--approval-key-rotate(= 撤销全部旧 grant)
431
+
432
+ # 1) 派活时显式开启委托(grant 与 task+origin 绑定**先于** prompt;任何失败=零派活)
433
+ dsh-task --in "<指令>" --session <会话id> --origin <当前对话id> --delegate [--authorization "本次授权范围"]
434
+
435
+ # 2) 任务在等审批时以退出码 9 及时返回(非终态)→ 取真实动作 → 单次决定
436
+ dsh-task --approvals --origin <id> --task <taskId> --grant <grantId>
437
+ dsh-task --approve <approvalKey> --origin <id> --task <taskId> --grant <grantId> --fingerprint <fp>
438
+ # 3) 继续收结果(等待期间认证续租;长任务可 --renew-grant 在原窗口内显式续期)
439
+ dsh-task --status <taskId> --origin <id> --grant <grantId> --status-wait
440
+ ```
441
+
442
+ - **MCP**:`dsh_task { delegate: true, authorization }`、`dsh_approvals { originThreadId, taskId }`、`dsh_approval_decide { originThreadId, taskId, approvalKey, grantId, fingerprint, decision }`、`dsh_task_status { wait: true, grantId }`、`dsh_models { force? }`(工具集 6 → 9:旧 6 个原样保留 + 审批 2 个 + 模型目录 1 个)。委托派活/状态响应带**非秘密** `grantId`/`grantExpiresAt`/`grantLeaseUntil`,等待循环据此**自适应认证续租**(短租约也不会过期,不扩大永久授权)。
443
+ - **动态模型路由(第五轮新功能,`model:auto` 默认)**:插件管理的任务(编码/评审子代理、有明确来源的普通会话 queue 任务)在**派活前**从本机**已注册** provider 目录里选模型,并在模型失效时有界切换:
444
+ - 输入:`model` 支持 `auto`(默认)/`fast`/`pro`(兼容偏好)/`default`(继承当前选择)/精确模型 id;精确指定可配 `provider`(跨 provider 同名模型**不合并**,缺 provider 时明确拒绝并给候选);`modelFallback:false` 关闭本任务切换(精确指定默认固定)。
445
+ - **四个入口同一契约**:HTTP `/api/dsh-bridge/task`、`/api/dsh-bridge/review`、MCP `dsh_task`/`dsh_review`、CLI `task.mjs`/`review.mjs`(`--session`/`--find`/`--cwd` 三种目标一致;评审与编码同 kind=review/worker 语义,评审也支持 `--origin`、模型预检与逐次 status 来源核对)。原生工具 `dsh_collab_send`/`dsh_collab_review` 走同一任务级机制。`provider+model` 是**一对身份**,任何入口都不会只发一半。
446
+ - 只读目录:`GET /api/dsh-bridge/models`、MCP `dsh_models`、CLI `dsh-task --models`;`capabilities.modelRouting` 供客户端**预检**(旧网关遇到新语义零派活)。
447
+ - 归属与恢复:首请求用官方 `agent/inbox/claimed` 按派活前登记的 requestId 认领(不靠事后日志);失败只用**稳定 failure.code** 分类,同 turn/step 内切换(不重派 prompt、不新建 child、不重跑已执行工具);默认最多 3 个不同候选、每候选与整任务都有时限;用户取消/安全拦截/审批拒绝/工具错误/未知异常**不**靠切模型绕过。
448
+ - 边界:目录可见 ≠ 凭据/额度/远端健康已验证(没有质量/价格评分,`toolUpdate` 不是"支持工具"布尔);不使用 `selectModel`/`saveSelection`,不改 readonly options/全局 preset;任务收口时**条件恢复**用户原选择(人类后选的模型优先)。
449
+ - 证据:[docs/DSH_MODEL_ROUTING_M0_EVIDENCE.md](docs/DSH_MODEL_ROUTING_M0_EVIDENCE.md);审计(稳定 code,无正文/密钥)落 `<assoc>/model-routing.jsonl`,任务状态可查 `route` 与 `routeAudit`。
450
+ - **认证**:`X-Dsh-Control` 传**私有 raw key**(MCP/CLI 内部读取;服务端只存域分离单向摘要,**摘要不能用于登录**);缺 key = 能力关闭;不存在未认证的 enable/bootstrap/领 token 路由。
451
+ - **授权**:`origin/task/session/callId` 只是定位符,**不构成授权**;真正的授权 = 绑定 task/origin/session/版本/期限的不可变 grant + 服务端按**不可变宿主快照**核对的指纹 + 单次状态机 + 审计写成功才放行。审批查询/决定必须显式带 `originThreadId`+`taskId`(服务端硬核对,绝不跨来源泄露)。
452
+ - **投递控制 queue / steer(第四轮新功能)**:DSH 忙时不再只有一个"busy 拒绝"——Codex 可按任务关系**自己选择**:
453
+ - `queue`(默认):排进**官方原生 FIFO**,当前任务完成后执行;忙时也可靠接收(202),每个任务有独立 taskId/requestId/回合/结果,互不覆盖。官方 `claim()` 一次只取一条 next-turn(`dsh-agent-loop:103-109`),所以排队任务各自独占自己的回合。
454
+ - `steer`:作为**补充回执**插进**显式父任务**的当前回合(官方 `next-step`,同 turn 被消费)。必须给 `parentTaskId`(可选 `expectedTurn`),父必须同 origin/同会话**正在执行**;回执有独立 taskId/observedTurn,**不签发也不扩大父 grant、不成为审批 owner、不伪装独立结果**(`resultScope: parent-task`),父任务的最终结果仍归父任务。
455
+ - 由 Codex 在工具参数里显式选 `deliver`(服务器**不按关键词猜**急迫程度);`--steer` 兼容保留但语义改为"父回执"。HTTP/MCP/CLI/DSH 原生工具都支持;steer 复用既有离线控制 key 做内部鉴权(MCP/CLI 自动带头,**缺凭据零插入**),旧网关缺 `capabilities.delivery` 时**零派活**拒绝。旧无来源 queue 路径行为不变(忙锁保持)。
456
+ - 官方契约证据:[docs/DSH_DELIVERY_M0_EVIDENCE.md](docs/DSH_DELIVERY_M0_EVIDENCE.md)(真实 SDK 队列顺序/消费时机/竞态)。
457
+ - **语义边界**:只 `approve-once`(官方唯一许可,且由那一次调用消费);官方 ask 身份必须唯一认领(同 callId 两次 ask = 两条独立 pending、两次独立决定);`next()` 交还人工、绝不吞链;**完整执行身份**(toolName/root/parent/cwd/真实参数 + 日志实时 turn/step)任一变化即拒绝旧审批,身份拿不到时明确 unsupported 而不放行;有效认证委托按 **grant 有效期**观察(不被 quiet/总预算误收口),失联/撤销/到期后有界退出并交还人工;`needs-approval` 保持父任务在途及审批所有权;带来源的新 queue 可排队,旧无来源路径仍保留忙锁;活跃审批绝不重复列进 `unanswerable`(重启恢复语义不变:submitted→decision-unknown、未决定→unanswerable);到期/失联/取消/卸载/内部错误 → 有界收口或 `next()` 且**先关闭可答状态**(迟到批准被拒),**绝不凭超时放行**;自己不写 `approval/asked|decided`;官方 `decided` 的 outcome 决定状态(cancelled/rejected 绝不算批准,reject 的终态是 `rejected` 而非 `confirmed`);不动 `approval/policy`/preset/never。
458
+ - **如实报 false 的能力**:worker 子代理目标(child 身份未证明,委托派活前拒绝)、PTC(callId/root 未证明,嵌套调用一律 `next()`)、空闲自动唤醒(仍 blocked)。
459
+ - **加载前提**:答复者是宿主 bundle 插件——**安装更新包 + 重启 profile** 后才在真实桌面生效。
460
+ - 详见 [docs/DSH_APPROVAL_DELEGATION.md](docs/DSH_APPROVAL_DELEGATION.md);官方接口取证见 [docs/CODEX_DSH_APPROVAL_M0_EVIDENCE.md](docs/CODEX_DSH_APPROVAL_M0_EVIDENCE.md)。
461
+
307
462
  ## 环境变量
308
463
 
309
464
  | 变量 | 默认 | 说明 |
@@ -312,9 +467,13 @@ codex exec "把刚才的 diff 审一遍,只回结论" --cd E:\proj
312
467
  | `DSH_BRIDGE_WORKSPACE` | `D:\Harness` | 网关默认共享工作目录 |
313
468
  | `DSH_BRIDGE_ROOT` | `D:\Harness` | `dsh_read_file` 相对路径基准 |
314
469
  | `DSH_BRIDGE_HISTORY` | 多级降级 | 协作历史文件位置(默认 D 盘 → 用户目录 → 临时目录逐级降级) |
315
- | `DSH_BRIDGE_SESSION_DISPATCH_TIMEOUT_MS` | `120000` | 会话任务被接受后、迟迟没开始跑的容忍时间(仍在排队则报 `session-not-accepted`) |
316
- | `DSH_BRIDGE_SESSION_QUIET_TIMEOUT_MS` | `600000` | 见过 running 之后多久没有新助手文本算跑完 |
470
+ | `DSH_BRIDGE_SESSION_DISPATCH_TIMEOUT_MS` | `120000` | 未开跑的观察阈值;prompt 已被接受时仅提示排队,不据此判执行失败 |
471
+ | `DSH_BRIDGE_SESSION_QUIET_TIMEOUT_MS` | `600000` | 安静观察阈值;带来源任务缺少自身 turn/end 时只能报 unknown,不能据此判完成;有效委托不因 quiet 提前收口 |
317
472
  | `DSH_BRIDGE_SESSION_TOTAL_TIMEOUT_MS` | `3600000` | 单个会话任务的总预算 |
473
+ | `DSH_CODEX_TASK_ASSOC_DIR` | `~/.dsh-codex-task-assoc` | 任务关联记录的存储根(**默认只在用户目录,绝不写仓库**) |
474
+ | `DSH_APPROVAL_CONTROL_KEY_PATH` | `~/.dsh-codex-approval/control.key` | 审批控制凭据文件(**仓库/工作区之外**,64-hex;宿主与 MCP/CLI 内部读取,HTTP 以 `X-Dsh-Control` 传私有 raw key)。缺文件或格式不合规=审批能力关闭 |
475
+ | `DSH_APPROVAL_DIR` | `~/.dsh-codex-approval` | 审批 grant/审计存储根(JSONL;测试用独立临时根) |
476
+ | `DSH_BRIDGE_ASSOC_NAMESPACE` | `default` | 关联记录的命名空间(`[A-Za-z0-9._-]{1,64}`);多 profile 可各自一个来分隔存储 |
318
477
 
319
478
  ## 已知限制
320
479
 
@@ -322,11 +481,15 @@ codex exec "把刚才的 diff 审一遍,只回结论" --cd E:\proj
322
481
  - **`sessionController.search` 可能被部署禁用**(`session-query` 索引 `openAt: "never"`)。此时按标题查找自动退回 `list`(读持久化 header + 标题投影,同样不激活 Agent),响应里的 `matchedBy` 标明走了哪条路
323
482
  - **不能投递进子代理会话**:`resolveAgent` 对 subagent 路由拥有的会话返回 `session-busy: owned by subagent routing`;只能接普通会话
324
483
  - `POST /api/dsh-bridge/sessions {"create": true, "cwd": "..."}` 可新建一条空白会话(探针/一次性任务用),它不会碰任何已有会话
325
- - **同一会话同时只允许一个在途 bridge 任务**:DSH 自己会排队,但无法可靠地把两次并发派活的回报各自归属,所以第二个会被明确拒绝(`session-busy`)而不是猜
484
+ - **带来源的会话任务支持多个 queue**,各自绑定请求和回合;旧无来源任务仍有忙锁,并拒绝与带来源任务在同一会话混用。补充必须指定正在运行的父任务;无法逐项安全撤回队列/回执时拒绝取消,避免清掉别人的队列。
326
485
  - **冷会话可能接不上**:DSH 激活 Agent 需要会话投影(`Agent activation requires a projected Session observation`),未激活的会话会返回 `session-not-activatable`;归档会话的回合会以 `blocked` 收口(`session-archived`)
327
486
  - **评审不支持会话目标**:评审员必须与编码会话隔离,"独立评审"才成立
328
487
  - session 目标不使用 `cwd`/`lane`/`model`/`--commit`(会话自带这些语义),传了会被判 `invalid-target`
329
- - 网关重启会丢失内存中的任务表(含 session 观察器),这是既有设计
488
+ - 网关重启会丢失内存中的任务表(含 session 观察器),这是既有设计;**但来源关联任务的终态结果在持久记录里**,重启后仍可按 `origin+taskId` 取回;没跑完的会明确返回 `unknown` + `resend:false`(不谎报、不重发)
489
+ - 关联记录只对**显式带 `originThreadId`** 的派活生效;不带来源的旧任务是"无关联"的(只能按 taskId 查活在内存里的状态,查询到就返回 0,查不到退出码 6)
490
+ - 恢复路径**不重新观察**已失去运行时上下文的活任务(未实现),因此这类任务只能得到 `unknown`;要去现实里确认就自己去看目标会话/工作目录——**不要重发**
491
+ - **同一通道同时只允许一个在途任务**(含评审:同一目录一个):第二个被 `worker-bucket-busy` / `reviewer-bucket-busy` 拒绝,避免完成结果被猜给错误的任务。这是为了保护结果归属,代价是"同 lane 快速连发"需要等前一条结束(或换 lane)
492
+ - **MCP 侧用 `fetch` 调网关**:若网关端口落在 fetch 规范的禁用端口(`1/7/9/…/6000/6665-6669/6697/10080` 等),undici 会直接以 `bad port` 拒绝连接(表现为"网关在跑但 MCP 全部失败、且没有任何网关日志")。默认端口(3080 / GUI 端口)不受影响;CLI 用 `node:http`,不受该限制。
330
493
  - **替换包内容不会热重载**:升级后必须重启 profile 进程,否则跑的还是旧 JS 模块(HMR 只重挂组合,不复用新代码)
331
494
  - **组合包被取消选中 = 插件完全不挂载**:此时所有 `/api/dsh-bridge/*` 都落到网关鉴权层返回 401(不是 404,容易误判成没装)
332
495
  - **DSH profile 的 pnpm 供应链冷却**:新发布的版本约 24 小时内会被 `minimumReleaseAge` 拒绝安装(`ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`);管理器会把它写进 `pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude` 才放行。刚发版就装不上属正常,不是包的问题
@@ -340,13 +503,21 @@ codex exec "把刚才的 diff 审一遍,只回结论" --cd E:\proj
340
503
  ```sh
341
504
  node test/session-target.test.mjs # 纯函数:目标判定 / 候选挑选 / 错误映射 / 基线切分 / 汇报选文
342
505
  node test/dsh-bridge.test.mjs # 路由级:假 DSH 运行时 + 桩 peer,加载真实 bridge 跑真实路由
343
- node --test test/*.test.mjs # 两个一起跑(CI 用的是这条)
506
+ node --test test/*.test.mjs # 全部行为回归(CI 用的是这条)
344
507
  ```
345
508
 
346
509
  - 路由级测试会断言 **worker 分支确实调用了 `startContinuable`**、**session 分支只调用 `prompt` 且不建 owner/子代理**——这类"参数遮蔽/串线"缺陷只有在这一层才抓得到(v0.1.2~0.1.4 的 `deliver` 遮蔽 bug 就是它抓回来的)。
347
- - CI(`.github/workflows/publish.yml`)每次 tag 都跑:语法检查 → 单测 → 包契约 → 版本号三处一致 → tarball 清单 → 干净目录安装 + CLI/MCP 冒烟 → 6 工具握手断言 → bridge 导出契约。**任何一步失败都不会发布**。
510
+ - CI(`.github/workflows/publish.yml`)每次 tag 都跑:语法检查 → 单测 → 包契约 → 版本号三处一致 → tarball 清单 → 干净目录安装 + CLI/MCP 冒烟 → 8 工具握手断言 → bridge 导出契约。**任何一步失败都不会发布**。
348
511
  - 发布:`package.json` / `.codex-plugin/plugin.json` / `mcp/dsh-mcp.mjs` 的 `SERVER_VERSION` 三处同步改版本 → commit → 打 tag `vX.Y.Z` → 推 tag,CI 自动带 provenance 发布。
349
512
 
513
+ ## 项目来源与鸣谢
514
+
515
+ - **上游原项目**:[FuJackGao/dsh-collab](https://github.com/FuJackGao/dsh-collab)。
516
+ - **原作者**:[FuJackGao](https://github.com/FuJackGao),以及参与上游项目的所有贡献者。感谢他们提供原始协作实现与开源基础。
517
+ - **本 fork**:[Whale-Talk/dsh-codex-collab](https://github.com/Whale-Talk/dsh-codex-collab)。后续工作在上游基础上开展,包括 npm 包分发、任务与来源对话关联、审批委托及 queue/steer 投递控制等扩展;各功能是否已发布,以本文的发布状态和 npm 实际版本为准。
518
+
519
+ 本项目保留原有版权与许可声明,许可证见 [LICENSE](LICENSE)。
520
+
350
521
  ## License
351
522
 
352
523
  MIT
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
4
4
  "description": "Codex 主导的双向编码协作插件:ChatGPT 把编码任务下发给本机 DeepSeek Harness 的持久编码子代理执行,DeepSeek 在共享工作目录 D:\\Harness 中写代码并实时回传进度与汇报,Codex 读文件验收、反馈修正,直到通过。",
5
5
  "author": {
6
6
  "name": "FuJackGao"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: dsh-collab
3
- description: "与 DeepSeek Harness 编码协作。当用户要写代码、建项目、改代码时使用:把编码任务交给本机 DeepSeek Harness 执行(DeepSeek 写代码,你负责验收)。两种派活方式——默认在 --cwd 指定的工作目录新建编码子代理;也可以把任务投递进**用户正在用的那一条已有会话**(保留它自己的上下文,不新建会话/工作区)。DeepSeek 完成后可自动 git 提交,并可用独立评审子代理审查 diff(评审员会实际跑通构建/测试)。不通过则发修正指令迭代,直到通过再向用户汇报。"
3
+ description: "与 DeepSeek Harness 编码协作。当用户要写代码、建项目、改代码时使用:把编码任务交给本机 DeepSeek Harness 执行(DeepSeek 写代码,你负责验收)。两种派活方式——默认在 --cwd 指定的工作目录新建编码子代理;也可以把任务投递进**用户正在用的那一条已有会话**(保留它自己的上下文,不新建会话/工作区)。派活时带 --origin(当前 Codex 对话 id)把任务与来源对话持久绑定,之后任何回合都能按 origin+taskId 精确取回这次任务的结果。DeepSeek 完成后可自动 git 提交,并可用独立评审子代理审查 diff(评审员会实际跑通构建/测试)。不通过则发修正指令迭代,直到通过再向用户汇报。"
4
4
  ---
5
5
 
6
6
  # DeepSeek Harness 编码协作
@@ -46,21 +46,62 @@ dsh-task --dry-run --find "<标题关键词>" # 只解析目标、回
46
46
 
47
47
  ```bash
48
48
  # 1) 新建子代理(原有行为)
49
- dsh-task --in "<完整中文指令>" --cwd "<工作目录>" [--lane <通道>] [--model fast|pro] [--commit] [--timeout 秒]
49
+ dsh-task --in "<完整中文指令>" --cwd "<工作目录>" [--lane <通道>] [--model fast|pro] [--commit] [--timeout 秒] [--origin <Codex对话id>]
50
50
 
51
51
  # 2) 投递进已有会话
52
- dsh-task --in "<完整中文指令>" --session "<sessionId>" [--steer] [--timeout 秒]
52
+ dsh-task --in "<完整中文指令>" --session "<sessionId>" [--steer] [--timeout 秒] [--origin <Codex对话id>]
53
53
  dsh-task --in "<完整中文指令>" --find "<标题关键词>" # 唯一命中才派发,否则退出码 4
54
54
  ```
55
55
 
56
+ - `--origin <id>`:**来源关联**——发起这次派活的 Codex 对话 id。**每个回合派活都要显式传它**(传当前这条对话的 id):任务会在执行前把「来源对话 → taskId → 执行目标」持久落盘,之后任何回合都能按 `origin + taskId` 精确取回这次任务的结果。未显式传时,CLI 会退回本次调用的环境变量 `CODEX_THREAD_ID`;两者都没有就按"无关联"派活(旧行为)。`--require-origin` 表示"没有来源就拒绝派活",不会静默降级。
56
57
  - `--cwd`:**默认传 Codex 当前打开的项目目录**(绝对路径,正斜杠);不存在时自动创建并注册为 harness 工作区。与 `--session`/`--find` 互斥。
57
58
  - `--lane`:并行通道名(默认 main)。同一目录不同 lane 的任务**并行执行**;跨目录天然并行。
58
59
  - `--model`:`fast`(deepseek-v4-flash,快而省)或 `pro`(deepseek-v4-pro,质量优先);默认继承当前模型。session 目标不使用此参数(会话自己有模型)。
59
60
  - `--commit`:任务完成后在 `--cwd` 自动 `git add -A` 并提交;非 git 仓库自动跳过。session 目标禁止使用(会话自带工作目录,本工具不替它提交)。
60
- - `--steer`:仅 session 目标。默认 `queue`(排队,等它忙完再跑);`--steer` 会插入当前回合、改变它正在做的事——**只在用户明确要求打断时用**。
61
+ - `--steer` / `--parent <父taskId>`:仅 session 目标。**默认 `queue`**(排进官方 FIFO,等当前任务忙完再跑);
62
+ `--parent`(等价 `--steer --parent`)表示"**作为补充回执插进那条父任务的当前回合**"。用户已授权你在同一任务范围内**自主选择**:
63
+ 需要排队就 `queue`,需要补充/纠正正在执行的父任务就 `steer`;**用户显式指定的模式优先**。steer 必须给正确父任务(同来源/同会话/正在执行/回合可证)并自带控制鉴权,否则执行前零投递拒绝。
61
64
  - 指令必须包含:目标与产出物、验收标准、约束。`--wait` 为默认行为(结果直接回本对话),长任务加 `--timeout 1800`。
62
65
 
63
- 退出码:`0` 成功 / `1` 失败 / `2` 用法 / `3` 超时 / `4` 会话未命中或不唯一 / `5` session 目标取消被拒。
66
+ 退出码:`0` 成功 / `1` 失败 / `2` 用法(含缺少来源、`--origin` 缺值、网关不支持来源绑定) / `3` 超时 / `4` 会话未命中或不唯一 / `5` session 目标取消被拒 / `6` 按 taskId 查询取不到(未知任务 / 已关联但缺来源 / 来源不匹配) / `7` 派活被拒(通道忙) / `8` 网关未确认来源绑定(可能已执行,**不要重发**)。
67
+
68
+ ## 跨回合精确收取(来源关联,推荐每个回合都做)
69
+
70
+ ```bash
71
+ # 派活时带来源(显式 --origin,或本次调用环境里的 CODEX_THREAD_ID)
72
+ dsh-task --in "<指令>" --cwd "<目录>" --origin "<当前对话id>"
73
+
74
+ # 下一个回合:只凭来源找回这次对话派过的任务(找回丢失的 taskId)
75
+ dsh-task --my-tasks --origin "<当前对话id>"
76
+
77
+ # 按来源 + taskId 精确取结果
78
+ dsh-task --status "<taskId>" --origin "<当前对话id>"
79
+ ```
80
+
81
+ - **归属靠来源,收取靠 taskId**:来源对话 id 决定"这次任务属于哪条对话",taskId 决定"是哪一次派活"。两者都不匹配时查询**明确拒绝**(退出码 6,绝不返回别的对话的结果)。
82
+ - `taskId` 是**关联标识**,不是授权凭据,也不是线程 id;`originThreadId` 只是关联元数据。
83
+ - **来源必须在派活前核验网关真的支持**(只读能力查询,不是猜版本):旧网关会忽略 `originThreadId` 却照常返回 accepted,从而产生"未绑定"任务——所以带来源的派活/查询在**不支持时直接拒绝、零执行**。旧的无来源调用仍然兼容。
84
+ - **显式来源必须合法**:空串 / null / 纯空白 / 非字符串一律拒绝(退出码 2 / `invalid-origin`),**不会**回落到无关联模式,也不会改用环境变量。
85
+ - **同一通道(`--cwd` + `--lane`)同时只允许一个在途任务**:第二个会被明确拒绝(`worker-bucket-busy` / `reviewer-bucket-busy`,退出码 7)——因为完成事件只能归属到"通道里最老的那个任务"。等它结束,或换一个 `--lane` 并行。不同 lane / 不同目录天然并行。
86
+ - **已关联任务必须带来源才能读**:缺来源 → `origin-required`、错来源 → `origin-mismatch`(退出码 6),内存与重启后一致;只有旧的未关联任务保持"只给 taskId"兼容查询。
87
+ - **授递进已有会话时的结果归属(带 `--origin`)**:任务靠 `requestId`(=taskId,写进 DSH 的 `user/message.source.rpcId`)关联到**自己那个 DSH 回合**,
88
+ 只收该回合的助手文本;只有该回合并以 `completed` 结束才算成功,`aborted/blocked/error/max-tokens/interrupted/forked`
89
+ 一律是**明确失败**(部分文字放在 `partialResult`,仅内存可见;落盘的是 `status`/`error`/`reason`/`requestId`/`dshTurn`)。
90
+ 因此"会话里别的回合跑完"不会把你的任务判成完成。
91
+ - **带来源时的 `--steer`(第四轮投递控制)**:来源绑定 steer **是支持的**,但必须是**父任务回执**——
92
+ 显式给 `--parent <父taskId>`(可选 `--expected-turn`),父任务须同来源/同会话/**正在执行且回合可证**,
93
+ 且本机要有控制 key(steer 改动既有任务,缺 key 零投递)。
94
+ 预检不通过(无父 / 父已结束 / 父还在排队 / 来源不符 / 回合不符 / 缺凭据)→ **执行前拒绝、零投递**。
95
+ 回执只报**自身**状态(`receipt-observed` / `receipt-deviated` / `receipt-unconfirmed` / `receipt-pending`,`resultScope=parent-task`),
96
+ **不代表父任务结果、也不参与审批归属**;未观察到时 `resend:false`,父任务结果请查父 task。
97
+ 非 session 目标(worker)不走 steer。
98
+ - **没有证据就是未确认**:`api-session/status` 只表示 agent 是否在跑,**不是**你的请求完成的证明。
99
+ 若一直没观察到匹配回合的结束(例如快照/日志里缺前半段),到时会返回 `status: "unknown"` + `resend: false`——**不要重发**。
100
+ - **网关必须回显来源才算确认**:派活/取值的响应里必须带**匹配的** `originThreadId`;若网关接受了请求却没回显(例如中途重载/旧 handler),
101
+ 一律按"未确认、不要重发"上报(退出码 8),绝不当成功、也绝不把那种响应里的结果当精确取值。
102
+ - **超时但 `delivered: true`**:消息已在目标会话队列里 → **查原 taskId,不要重发**。
103
+ - **网关重启后**:已完成的关联任务仍能按 `origin + taskId` 取回(持久记录);没跑完的任务会返回 `status: "unknown"` + `resend: false`(执行/交付状态未能确认,**不要重发**,也不要去猜"最近一条助手文本")。只给来源列任务时同样:没有本实例运行上下文的记录一律显示 `unknown`,不会谎报"还在跑"。
104
+ - 不按"最近使用的对话"猜目标,也不自动重发。
64
105
 
65
106
  ## 读取汇报与验收
66
107
 
@@ -81,14 +122,69 @@ dsh-review --cwd "<工作目录>" [--diff git|git-staged|@文件|"diff 文本"]
81
122
  ## 任务管理
82
123
 
83
124
  ```bash
84
- dsh-task --list # 最近任务列表
125
+ dsh-task --list # 最近任务列表(可加 --origin <id> 只看本对话的)
85
126
  dsh-task --status <taskId> # 查询单个任务(含 target 回显)
127
+ dsh-task --status <taskId> --origin <id> # 按来源精确取值(推荐;取不到时退出码 6)
128
+ dsh-task --my-tasks --origin <id> # 列出该来源登记过的任务(找回丢失的 taskId)
86
129
  dsh-task --cancel <taskId> # 取消 worker 任务
87
130
  dsh-task --cancel <taskId> --force # 取消 session 目标任务(会打断用户自己的回合,默认被拒)
88
131
  ```
89
132
 
133
+ ## 审批委托(任务等审批时由你逐次决定,用户不在也能继续)
134
+
135
+ ```bash
136
+ # 0) 只做一次:离线初始化控制凭据(仓库/工作区之外的私有目录;raw key 不回显、不上线)
137
+ dsh-task --approval-key-init # 轮换用 --approval-key-rotate(会让全部旧 grant 立即失效)
138
+
139
+ # 1) 派活时显式开启委托(grant 与 task+origin 绑定先于 prompt;失败=零派活)
140
+ dsh-task --in "<指令>" --session <id> --origin <对话id> --delegate [--delegate-ttl 秒] [--delegate-lease 秒] [--authorization "本次授权范围"]
141
+ # 初次默认等待与下面的 --status-wait 是同一条有界等待路径:带 origin 逐次核对回显、按租约自适应认证续租、
142
+ # 遇到 needs-approval 立即 exit 9(不必先手动查 grant,也不必再调一次 --status-wait 才能续租)。
143
+
144
+ # 2) 任务等你审批时会以退出码 9 立即返回(不是失败、不是完成)
145
+ dsh-task --status <taskId> --origin <id> # status=needs-approval 时看 needsApproval/approvalPendingCount
146
+ dsh-task --approvals --origin <id> --task <taskId> [--grant <grantId>] [--wait-approval 秒]
147
+
148
+ # 3) 逐条单次决定(approvalKey/grantId/fingerprint 都来自上一步;多余字段与参数副本一律被拒)
149
+ dsh-task --approve <approvalKey> --origin <id> --task <taskId> --grant <grantId> --fingerprint <fp> [--idem 幂等键]
150
+ dsh-task --reject <approvalKey> --origin <id> --task <taskId> --grant <grantId> --fingerprint <fp>
151
+
152
+ # 4) 决定后继续收结果(等待期间会做认证续租)
153
+ dsh-task --status <taskId> --origin <id> [--grant <grantId>] --status-wait
154
+
155
+ # 撤销 / 在原授权窗口内显式续期
156
+ dsh-task --revoke-grant <grantId> --origin <id> --task <taskId> [--reason "…"]
157
+ dsh-task --renew-grant <grantId> --origin <id> --task <taskId> [--extend 3600]
158
+ ```
159
+
160
+ - **MCP 等价**:`dsh_task { delegate: true, authorization }` / `dsh_approvals { originThreadId, taskId, … }` /
161
+ `dsh_approval_decide { originThreadId, taskId, approvalKey, grantId, fingerprint, decision: "approve-once"|"reject" }`;
162
+ `dsh_task_status { wait: true, grantId }` 可阻塞轮询到终态(遇 `needs-approval` 会立即返回,等待期间认证续租)。
163
+ - **`origin`+`task` 是必填定位**(服务端还会与 pending/grant 硬核对);它们**不是授权**,绝不能只凭它们放行。
164
+ - 控制凭据只在 MCP/CLI 内部读取并作为 `X-Dsh-Control` 头发出;raw key 不出现在参数、结果、错误或审计里。
165
+ - **看到 `needs-approval` 不要重发任务**:它是非终态,只差你对挂起项做一次决定。
166
+ - `submitted ≠ confirmed`:再查一次看到 `state=confirmed`(官方 `approval/decided=allowed-once` 已观测)才算确认;`decision-unknown` 表示确认丢失——**不要重发放行**;若官方 outcome 是 cancelled/rejected,绝不会被记成批准。
167
+ - 只接手能严格证明属于本委托的请求:无 callId / 无宿主参数快照 / 工具名不符 / turn·step 证明不了 / 不是自己那个 turn / PTC 嵌套(`…:ptc:<n>`)/ 父子混淆 / 官方 ask 身份歧义 → 一律交还人工(`next()`);worker 与 PTC 能力如实报 false。
168
+ - 审批类退出码:`9` = 等待审批(非终态);`0-8` 语义不变;缺本机 key / 预检不支持 → `2` 且**零派活**。
169
+
170
+ - **投递模式(queue / steer,第四轮)**:DSH 忙时由**你**按任务关系选,服务器不猜急迫程度:
171
+ - `queue`(默认):排进官方原生 FIFO,当前任务完成后执行;忙时也可靠接收。每个任务独立 taskId/requestId/回合/结果。
172
+ - `steer`:**补充/纠正当前任务**时用,插进父任务的当前回合(同 turn 消费)。**必须显式给父任务**:
173
+ `dsh-task --in "<补充>" --session <id> --origin <对话id> --parent <父taskId> [--expected-turn N]`;
174
+ MCP: `dsh_task { deliver: "steer", parentTaskId, expectedTurn }`。父必须同 origin/同会话**正在执行**;父未开始/已结束/来源不符/回合不符 → 零投递拒绝。
175
+ - 回执语义:有独立 taskId 与 `observedTurn`,`resultScope=parent-task`;**不等于**父任务结果,也**不签发/扩大**父 grant、不自动决定旧 pending。回执 `receipt-unconfirmed` 表示"官方已接收但没观察到落进父回合"——**不要重发**;父任务结果查 `--status <父taskId> --origin <id> --status-wait`。
176
+ - CLI/MCP/**DSH 原生工具**都会校验控制凭据(steer 改动既有任务):CLI/MCP 自动带 `X-Dsh-Control` 头,原生工具只读**本机已有**可信 key(绝不接受调用方传入的 raw key);缺 key/坏 key → **零插入**。普通非委托 queue 不增加凭据前提。旧网关缺 `capabilities.delivery` → 零派活拒绝。
177
+
90
178
  ## 故障排查
91
179
 
180
+ - **模型选择(第五轮,`model:auto` 为默认)**:派活时**不用**再猜固定型号——插件会从本机**已注册** provider 目录里选并回显:
181
+ - 缺省/`--model auto`:按真实能力(模态/上下文/effort 兼容)+ DeepSeek 偏好 + 任务类型自动选;`fast`/`pro` 仍是兼容偏好;`--model default` 表示**继承**当前选择(不覆盖)。
182
+ - `--model <精确id> [--provider <p>]`:精确指定;同一模型名在多个 provider 出现时**必须**给 `--provider`,否则拒绝并列出候选。精确指定默认**不**自动降级,要降级须显式 `--model-fallback`;`--no-model-fallback` 可显式关闭。
183
+ - 只读目录:`dsh-task --models`(或 MCP `dsh_models`)列出候选 pair、目录失败隔离、已知临时失效与冷却;**目录可见 ≠ 凭据/额度/远端健康已验证**,也没有质量/价格评分。
184
+ - 失效切换边界:只按**稳定 failure.code** 分类;认证/额度/模型失效可切,限流/网络先有界重试再切;**用户取消、拒答/安全、审批拒绝、工具错误、未知插件异常都不切**。默认最多 3 个不同候选,且同 turn/step 内恢复(不重派提示、不丢已执行工具、不新建子代理)。
185
+ - 任务状态可查 `route`(实际选择/模式/是否允许降级)与 `routeAudit`(切换次数/原因/终态,跨重启保留;只存稳定 code,无正文/密钥)。
186
+ - **评审与编码同一契约**:`dsh_review` / `review.mjs` / 原生 `dsh_collab_review` 默认也是 `auto`(优先 DeepSeek),同样支持 `--model/--provider/--model-fallback`;`review.mjs` 还要求**显式来源**(`--origin` 或本次调用的 `CODEX_THREAD_ID`)并逐次 status 核对来源,旧网关遇到模型新语义**零派活**拒绝。`--session`/`--find`/`--cwd` 三种目标的模型参数都成对发送(`provider+model` 是一对身份,不允许只发一半)。
187
+
92
188
  - 连接失败:DeepSeek Harness 未运行(网关应监听本机端口,默认 3080;挂在 web/desktop profile 时也可能就是 GUI 的端口)。
93
189
  - 派 session 任务返回 `session-writer-held` / `session-busy`:那条会话被别的写入方或另一个 bridge 任务占用;稍后重试,或改用 worker 目标。
94
190
  - 返回 `session-busy: owned by subagent routing`:那是子代理会话,**不能作为投递目标**;只能接普通会话。