@whaletalk/dsh-codex-collab 0.1.6 → 0.1.9

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.
@@ -0,0 +1,206 @@
1
+ # Codex → DSH 派活与回报关联方案(R0 核实 + R1 已实现)
2
+
3
+ - 日期:2026-10-12
4
+ - 仓库:`E:/11-Github/dsh-codex-collab`
5
+ - 关系:本文件是**补充方案**,不改动 `docs/DSH_TO_CODEX_MESSAGING_PLAN.md`(那份仍是 M0–M3 的原始计划,M0 结论仍为 blocked)。
6
+ - 一句话:主要使用方式收敛为 **Codex 发起任务 → DSH 执行 → Codex 按来源对话 + taskId 精确收取该任务回报**;这条链路**不依赖**"唤醒任意空闲桌面对话"。
7
+ - 状态:**R0 已核实、R1 已实现**(代码 + 文档 + 真实回归),R2 仍未做。
8
+
9
+ ## 1. 范围与结论
10
+
11
+ | | 结论 |
12
+ |---|---|
13
+ | 已实现 | **R1**:派活时绑定来源对话 id,持久记录「来源对话 → taskId → 执行目标 → 状态/结果」,跨回合、跨 MCP 进程、网关重启后仍能精确收取 |
14
+ | 不做且不需要 | 不需要"唤醒任意空闲桌面对话"才能完成派活回报——按来源 + taskId 的路径**已能正确收取** |
15
+ | 未做(后续) | R2:空闲自动唤醒(M0 仍 blocked);恢复时**重新观察**活任务(当前明确标 unknown,不猜) |
16
+ | 能力边界 | 桌面自动唤醒仍为 `canWakeIdle: false`;本条只讲派活回报的关联与收取 |
17
+
18
+ ## 2. 已具备的链路(R0 核实,代码位置已逐条核对)
19
+
20
+ ### 2.1 taskId:服务端生成、内存表、唯一
21
+
22
+ - 生成:`harness/dsh-bridge.mjs:82` —— `const uid = () => 't' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8)`
23
+ - 分配:会话目标 `:679`、worker(`enqueue`)`:785`、评审(`submitReview`)`:824`,均写入 `taskId: uid()`。
24
+ - 内存表:`const tasks = []`(`:210`)。**淘汰改用 `harness/task-table.mjs` 的 `rememberTask`**:只在超限时淘汰**终态**任务,
25
+ 永不淘汰 `accepted`/`running`(否则完成回调/会话观察器 `find(taskId)` 找不到它),全是活任务时宁可超过 200。
26
+ - 持久记录**不受**内存上限影响(见 §3),因此"超过 200 条已完成记录后旧结果仍可查"。
27
+
28
+ ### 2.2 MCP 侧按 taskId 等待与查询
29
+
30
+ - `mcp/dsh-mcp.mjs` 的 `waitTask(taskId, timeoutSec, originThreadId)`:每 3s `GET /status?taskId=…[&originThreadId=…]`;
31
+ 终态 `done|error|cancelled` **以及 `unknown`** 都立即停止轮询(不会无限等);超时返回 `status:'waiting'`。
32
+ - `dsh_task`:`wait:false` → `{ok:true, phase:'submitted', …}`;默认等待 → `{phase:'finished'}`;
33
+ 会话目标 `delivered=true` 但报错 → `{phase:'queued-then-failed', resend:false}`;**恢复到的未知状态** → `{phase:'unconfirmed', resend:false}`。
34
+ - `dsh_task_status`:两种用法——`taskId [+ originThreadId]` 精确取值;**只给 `originThreadId`** 列出该对话登记过的任务(找回丢失的 taskId)。
35
+ - 工具**仍然是 6 个**(未新增);`dsh_task` / `dsh_review` 的说明里明确要求**每个回合显式传当前对话 id**,并写明"不要用常驻进程环境或最近对话推断"。
36
+
37
+ ### 2.3 /status 回传的字段
38
+
39
+ ```
40
+ taskId, status, kind, cwd, lane, target, deliver, instruction(≤300),
41
+ result, error, reason, fallback,
42
+ delivered?, originThreadId?, associationError?, matchedBy?, candidates?,
43
+ watcher? / sawRunning? // 仅会话目标且仍在观察时
44
+ ```
45
+
46
+ - `status` ∈ `accepted | running | done | error | cancelled`,**另加恢复专用 `unknown`**。
47
+ - 会话目标带 `target: { kind:'session', sessionId }`(**这个 `sessionId` 就是"执行方 DSH 会话"**);worker 目标带 `target: { kind:'worker', cwd, lane }`。
48
+
49
+ ### 2.4 回报回到哪条 Codex 对话
50
+
51
+ MCP 是**客户端发起**的调用:Codex 在哪条对话里调用了 `dsh_task`,工具结果就回到那条对话——**等待模式今天就能正确收取**。
52
+ `originThreadId`(来源对话 id)解决的是另一件事:**同一条对话的多次派活彼此区分**,以及**跨回合/跨进程之后还能精确取回**。
53
+
54
+ ## 3. R1 实现:来源关联(已落地)
55
+
56
+ ### 3.1 接口
57
+
58
+ | 位置 | 参数/行为 |
59
+ |---|---|
60
+ | `POST /api/dsh-bridge/task`、`/review` | 可选 `originThreadId`(非空字符串 ≤200 字符、无控制字符);**非法值在执行前拒绝**(400 `invalid-origin`) |
61
+ | `dsh_task` / `dsh_review`(MCP) | `originThreadId` 显式透传;**不使用进程环境或"最近对话"推断** |
62
+ | `dsh_collab_send` / `dsh_collab_review`(DSH 侧原生工具)、WS 指令 | 同样支持 `originThreadId` |
63
+ | `GET /status?taskId=…&originThreadId=…` | 精确取值(内存优先;内存里没有则从**持久记录**恢复) |
64
+ | `GET /status?originThreadId=…`(或 `/tasks?originThreadId=…`) | 列出该来源登记过的任务(含只存在于持久记录里的) |
65
+ | `GET /tasks` | 不带来源时行为不变 |
66
+ | CLI `task.mjs` | `--origin <id>`(显式优先)→ 其次**本次调用**的 `CODEX_THREAD_ID`;`--my-tasks`、`--status <taskId> --origin <id>`、`--require-origin`;`--origin` 缺值按用法错误 |
67
+
68
+ 成功/等待/终态响应都回显 `originThreadId` + `taskId` + 执行目标(`taskResponse` 统一附加)。
69
+
70
+ ### 3.2 持久记录与存储
71
+
72
+ - 模块 `harness/codex-associations.mjs`;文件 `<root>/<namespace>/bindings.jsonl`(**追加型事件日志**,读取折叠成记录)。
73
+ 默认根 `~/.dsh-codex-task-assoc`(`DSH_CODEX_TASK_ASSOC_DIR` 可覆盖),namespace 由 `DSH_BRIDGE_ASSOC_NAMESPACE` 指定(默认 `default`)。
74
+ - 记录字段:`{originThreadId, taskId, kind, target, profile, status, createdAt, updatedAt, delivered, result, error, reason, stopReason}`。
75
+ - **执行前落盘**:`bind` 在派活 / 投递 prompt **之前**调用;**失败即拒绝执行**(映射为 `association-bind-failed`)——绝不出现"执行了却没绑定"。
76
+ - **终态写回**:`onChildEnd`(worker/review 完成)、`settleSessionTask`(会话完成/失败/取消/超时)、fallback 完成、异常路径、
77
+ `pollOne` 静默超时、`/cancel` 两条分支,全部调用 `persistTask`;写失败记到任务上的 `associationError` 并打日志(**不静默**)。
78
+ - **绑定不可改投**:同一 taskId 换来源 → `origin-conflict`。
79
+ - **隔离与路径安全**:namespace 只允许 `[A-Za-z0-9._-]{1,64}` 且排除 `.`/`..`;`originThreadId`/`taskId` **从不参与路径拼接**。
80
+ 日志追加型 + taskId 全局唯一 ⇒ 多个网关/多 profile 共用同一命名空间也不会互相覆盖记录;需要分隔时用命名空间。
81
+ - **读写失败明确报错**:读不动 → `store-unreadable`;文件有内容但一行都解析不出 → `store-corrupt`;**绝不降级成空记录**。
82
+ 写入串行化复用 `harness/fs-lock.mjs`(与收件箱**同一把独占锁算法**)。
83
+ - 写入前修**记录边界**(末字节不是换行就补一个),残尾不会吞掉后续写入。
84
+
85
+ ### 3.3 用户流程(含异常处置)
86
+
87
+ 1. **定目标**:确定执行方(`--session`/`--find` 只读查找;**不按"最近使用的聊天"猜**)。
88
+ 2. **派活并带来源**:`--origin <当前 Codex 对话 id>`(MCP:`originThreadId` 参数)——执行前完成持久绑定。
89
+ 3. **等待或后查同一个 taskId**:`--status <taskId> --origin <id>`;忘了 taskId 就用 `--my-tasks --origin <id>` 找回。
90
+ 4. **拿到 done → 验收 → 继续**:读 `result`,必要时 `dsh_review` / `dsh_read_file`。
91
+ 5. **异常处置**:
92
+ - 超时但 `delivered: true` → 消息已在目标会话队列里 → **查原任务,绝不重发**。
93
+ - **网关重启/新实例**:已完成 → 按 `origin+taskId` 取回结果;没跑完 → `status: "unknown"` + `resend: false` + 原目标 + 已知 `delivered`,
94
+ **不谎报完成、不重新观察、不自动重发**(要确认就自己去看目标会话/工作目录)。
95
+ - 错来源/缺来源 → 明确拒绝(`origin-mismatch` / `origin-required`),**绝不返回别的对话的结果**。
96
+ - 取消:`/cancel` 支持可选 `originThreadId`;已关联任务被别的来源取消 → 拒绝。
97
+
98
+ ### 3.4 会话任务的精确请求回合关联(带来源时)
99
+
100
+ 会话目标不能再用"派发前基线 + 全会话 running/idle"来收结果——那会把**别的请求**的输出算给当前 taskId。
101
+ 带来源的会话任务改为**按请求关联回合**(依据本机 controller 0.2.0-rc.2 实测字段):
102
+
103
+ | 事实 | 做法 |
104
+ |---|---|
105
+ | `prompt.requestId` 会写进 `user/message.data.source.rpcId` | 用 `taskId` 当 `requestId`;匹配 `rpcId === taskId` 的那条 user 消息 = 本请求 |
106
+ | **`user/message.data.turn` 不存在** | 按 `seq` 有序跟踪"已 `turn/start` 但未 `turn/end`"的回合;rpcId 匹配时归到当时**未闭合**的那个 turn |
107
+ | `assistant/message.data = {turn, step, message:{content}}` | 只收 `turn === 本请求 turn` 的助手文本 |
108
+ | `turn/end.data = {turn, reason:{kind}}` | 本请求的结束依据;`kind ∈ completed/aborted/blocked/error/max-tokens/interrupted/forked`,**只有 completed 算成功**,其余明确终态失败(保留部分文字为 `partialResult`) |
109
+ | follow 开场 = `{type:'snapshot', records:[{type:'event',event}], …}`(不是 `snapshot.events`) | **必须检查 records**:快任务可能在派发与订阅之间就跑完,答案只在这个快照里 |
110
+ | `api-session/status` 只表示 agent 是否 running(连续跑 queued turn 时可能没有新的 `running=true`) | 只当作"读一次日志"的机会;**绝不是**某请求完成的证明;quiet/总预算到期而无匹配 `turn/end` → 报未确认(`resend:false`),绝不猜 done |
111
+ | 事件可能被 snapshot / 轮询 / live 重复投递 | 按 `seq` 去重,但**只标记"真正产生效果"的事件**:当时因为还没关联上自己 turn 而无法使用的事件必须能在完整日志回填时被重新消费(否则快照缺前半段会永久丢答案) |
112
+ | `steer` 会把整批 next-step 合并进同一个 turn | 带来源的会话派发**执行前拒绝 steer**(`origin-steer-unsupported`,零 prompt);旧的无来源 steer 行为保留 |
113
+
114
+ **回显与持久化(按最终实现,区分落盘与仅内存)**:
115
+
116
+ - **落盘**(关联记录 `bindings.jsonl`;`/status` 在内存分支与恢复分支都回显):`originThreadId`、`taskId`、`kind`、`target`、
117
+ `status`、`delivered`、`result`、`error`、`reason`、`stopReason`、`requestId`(=taskId,会话任务)、`dshTurn`(匹配到的回合号)。
118
+ - **仅内存可见**(活任务的 `/status`,不落盘):`turnEndKind`、`partialResult`,以及未确认路径的 `resend`/`note`。
119
+ 恢复分支不带这三项,而是按记录状态自己给出:终态记录回放 `status`/`result`/`error`/`reason`;非终态记录合成
120
+ `status: "unknown"` + `reason: execution-state-unconfirmed` + `resend: false` + `note`(这是**恢复时的结论**,不是从盘里读出的字段)。
121
+
122
+ **未验证/限制**:相关性依赖有序日志(若日志缺 `turn/start`,只能归到最近未闭合的 turn);`api-session/error` 不带 requestId,因此**不**用它判定某个排队请求失败;"恢复后重新观察活任务"仍未实现(只报未确认)。
123
+
124
+ ## 4. 验收(本轮真实跑过)
125
+
126
+ ### 4.1 契约收紧(独立复现脚本 `review-r1-contracts.mjs` 曾 2/8,现 **8/8**)
127
+
128
+ | 契约 | 实现 |
129
+ |---|---|
130
+ | 显式非法来源零执行 | 只有**字段未传/undefined** 才是旧的无关联模式;显式 `''`/`null`/数字/布尔/数组/对象/纯空白 → `invalid-origin`,400 且零执行零关联。MCP 与 CLI 同样先本地拒绝(不丢字段、不改用 ENV;CLI `--origin` 缺值/下一项是旗标/空值 → 退出码 2,**请求前**报错) |
131
+ | 已关联任务必须带来源 | **内存与磁盘一致**:缺来源 → `origin-required`、错来源 → `origin-mismatch`,都不返回 `result`/`instruction`;`/cancel` 同样检查。`openAssociations.fetch` 失败时**不再带出 record** |
132
+ | 同通道并发必须可归属 | 同一 `cwd+lane`(评审=同目录)有在途任务时,第二个明确 `worker-bucket-busy` / `reviewer-bucket-busy` 拒绝:**零 followup、零二次执行**,绝不用"桶里最老的 pending"去猜归属。不同 lane 照旧并行;上一任务结束后可继续复用同一子代理 |
133
+ | 旧网关零执行 | `/status`(无参数)、`/tasks`、`/debug` 上报 `capabilities.originBinding.supported/version`;MCP/CLI 带来源时**先只读核验**(GET,不派活),不支持则拒绝且**一个派活 POST 都不发**。按来源查询也不把旧网关的普通响应当成精确恢复 |
134
+ | 列表不谎报 | 只给来源列任务时,本实例没有运行上下文的非终态记录一律 `status:"unknown"` + `persistedStatus` + `resend:false` + 说明(与精确查询同一套投影) |
135
+
136
+ ### 4.2 端到端(真实进程/真实请求)
137
+
138
+ **独立验收脚本**(Codex 侧,正向契约测试):`review-r1-contracts.mjs` **12/12**、
139
+ `review-session-correlation.mjs` **4/4**(含"排队请求忽略先前回合"、"只有自己 turn/end 才完成"、
140
+ "自己回合 error/blocked 不得 done"、"snapshot.records 里的快速完成"、"快照缺前半段回填"、
141
+ "旧网关零执行拒绝"、"能力通过但网关未确认来源 → 未确认 + 禁止重发")。
142
+
143
+ `test/codex-association-e2e.test.mjs` —— **真实 `http.Server` + 独立 MCP 进程 + 独立 CLI 进程**:
144
+
145
+ - 回合 1(MCP 进程 A)带 `originThreadId` 派活 → 执行前落盘;**环境里故意放一个 `CODEX_THREAD_ID`,MCP 不得采用它**(断言来源仍是显式传的那个)。
146
+ - 回合 2(**另一个** MCP 进程):只给来源 → 列出该任务;再按 `origin+taskId` → 取回结果;错来源 → `origin-mismatch` 且无结果;
147
+ 缺来源 → `origin-required` 且无结果;非法来源 → `invalid-origin` 且零派活。
148
+ - **网关新实例**(同持久 root、新端口、全新内存表):已完成 → `done` + 结果(`recovered: true`);未完成 → `unknown` + `resend:false` + 原目标;
149
+ 缺来源 → `origin-required`;**新实例零执行**(`starts`/`prompts` 均为 0)。
150
+ - CLI:`--origin` 派活 → 新进程 `--my-tasks` 找回 → `--status … --origin` 取结果;缺来源/错来源退出码 6;无来源 `--my-tasks` 退出码 2;
151
+ `--require-origin` 拒绝派活且零派活;`CODEX_THREAD_ID` 兜底可用;无来源的旧调用仍兼容;`--origin` 缺值按用法错误。
152
+ - 测试卫生:CLI 一律**异步 spawn**(网关 HTTP 服务器与测试同进程,同步 spawn 会自锁),MCP 收尾**等待子进程 close**,
153
+ 失败路径也 `finally` 关网关/杀子进程(宿主与清理统一在 `test/helpers/dsh-host.mjs`)。
154
+
155
+ ### 4.3 项目内回归
156
+
157
+ `test/codex-r1-contracts.test.mjs`(8 项,把独立场景固化:C1 非法来源、C2 缺/错来源、C3 通道 busy + 不同 lane 并行 + 评审 busy、
158
+ C4 旧网关零执行 + 能力上报、C4b 能力上报放行、C4c 能力通过但未确认来源 → 未确认 + 禁止重发、C5 列表 unknown 语义、CLI 严格参数);
159
+ `test/codex-session-correlation.test.mjs`(8 项,真实 SDK 字段:排队请求忽略先前回合、自己 turn/end 才完成且不需要新 running、
160
+ error/blocked/aborted/max-tokens 不得 done、snapshot.records、重复投递不串文本、快照缺前半段回填、无匹配 rpcId → 未确认、steer 拒绝 + 旧路径保留、worker/取消不受影响);
161
+ `test/session-target.test.mjs` 另有 5 项关联器纯函数单测;
162
+ `test/dsh-bridge.test.mjs`(13 项)与 `test/codex-associations.test.mjs`(13 项)另覆盖:执行前落盘、worker/评审/取消写回、
163
+ **200+ 已完成记录后旧关联结果仍可查且活关联任务仍能完成**、**关联存储不可用 → 拒绝派活(绝不假成功)**、绑定不可改投、
164
+ 命名空间隔离、fetch 失败不泄漏 record、真多进程并发 bind、任务表淘汰不丢活任务。
165
+
166
+ ## 5. 验收重点(对应关系)
167
+
168
+ | 要求 | 实现/证据 |
169
+ |---|---|
170
+ | 两个来源的任务不串号 | 错来源一律 `origin-mismatch`;e2e 用两个来源分别派活并交叉查询 |
171
+ | 同一对话多个任务可区分 | 靠 taskId;`--my-tasks` 列出该来源全部任务 |
172
+ | 超时继续查原 taskId、不重发 | `waiting` + `queued-then-failed(resend:false)` + `unknown(resend:false)` |
173
+ | 不按"最近聊天"猜目标 | 来源必须显式;缺来源一律拒绝;代码/测试/文档无任何真实对话 id 默认值 |
174
+ | 对话 id 负责归属、接收工具负责收取 | `originThreadId` 归属 + `dsh_task_status`/`--status` 收取 |
175
+ | 不把"支持来源"当成默认 | 派活/查询前只读核验 `capabilities.originBinding`;旧网关零执行拒绝 |
176
+ | 结果归属不能靠猜 | 同通道在途任务时明确 busy 拒绝(不 followup、不猜 pending) |
177
+ | 自动唤醒能力 | 仍为 `false`(未实现,未宣称) |
178
+
179
+ ## 6. 分阶段计划
180
+
181
+ | 阶段 | 内容 | 状态 |
182
+ |---|---|---|
183
+ | **R0** | 核实现有 taskId 回报链路并写清边界 | ✅ 完成 |
184
+ | **R1** | 显式来源元数据 + 持久关联记录 + 跨回合/跨进程/重启恢复 + CLI/MCP/文档 | ✅ **本轮完成** |
185
+ | **R2** | 只有在明确需要"空闲自动唤醒"**且**存在受支持桌面入口时,才接回 M0–M3;可另加"重新观察活任务" | ⏳ 未做(M0 仍 blocked) |
186
+
187
+ ## 7. 明确不做 / 未验证
188
+
189
+ - 不新增第 7 个 MCP 工具;不改动既有六个工具的语义与既有参数行为。
190
+ - **`originThreadId` 是关联元数据,不是认证/授权凭据**:本机 loopback 调用者知道 taskId+origin 就能查;本包不做权限模型。
191
+ - **未实现"重新观察活任务"**:恢复到的未完成任务只报 `unknown`,不读"最近助手文本"去猜。
192
+ - 不做空闲唤醒、不做自动重发、不按最近/标题/时间猜来源对话。
193
+ - **通道忙时的取舍**:同 `cwd+lane` 有在途任务就拒绝第二条(宁可让调用方等/换 lane,也不让完成结果归属变成猜测);
194
+ 这是有意的行为收紧,不是 bug。
195
+ - **能力握手靠网关自报**(`capabilities.originBinding`),不靠包版本号:从旧版本升级到本版本后,客户端会自动放行;
196
+ 若 profile 里装的是旧插件,客户端会拒绝来源模式(旧的无来源调用不受影响)。
197
+ - 关联日志不自动清理(追加型,增长很慢);归档属后续工作。
198
+ - 不写入任何真实对话 id 作为默认值(代码/测试/文档均无硬编码)。
199
+
200
+ ## 8. 与 standalone inbox 的关系(并列,不互相替代)
201
+
202
+ - `mcp/codex-inbox.mjs` + `harness/codex-outbox.mjs` 的**收件箱**是将来"存放待收消息"的基础设施(持久、幂等、按序、重启可恢复),
203
+ 与 **来源关联的 taskId 工具回报路径并列**,不是它的替代品;两者现在**共用同一套独占写锁算法**(`harness/fs-lock.mjs`)。
204
+ - 因此:**"桌面自动唤醒尚未完成"不等于"已有派活回报失效"**。前者是"DSH 主动叫醒 Codex"的能力(blocked);
205
+ 后者是"Codex 派活后按来源 + taskId 收结果"的能力(**已工作**)。
206
+ - 收件箱的当前能力仍如实标注:`deliveryMode: 'inbox'`、`canWakeIdle: false`。
@@ -0,0 +1,167 @@
1
+ # DSH → Codex 收件箱(M0 降级交付)
2
+
3
+ > **状态:收件箱已完成(含跨进程写事务与记录边界修复);桌面自动唤醒尚未完成。**
4
+ > M0 实测本机没有可附着的 Codex Desktop 端点(证据见
5
+ > [`codex-desktop-attachment-evidence.md`](codex-desktop-attachment-evidence.md)),
6
+ > 因此"DSH 主动发消息并**触发**那条聊天回复"被标记为 **blocked**。
7
+ > 本文交付的是它可独立验证的那一半:一个**跨进程安全**的本地消息收件箱。
8
+
9
+ ## 1. 它现在能做什么 / 不能做什么
10
+
11
+ | | 状态 |
12
+ |---|---|
13
+ | DSH 侧把要说的话写进本地持久化收件箱(写盘成功才算 `queued`) | ✅ 已完成 |
14
+ | 未绑定目标一律拒绝,绝不猜或静默新建 | ✅ 已完成 |
15
+ | 幂等(同键同文返回原 messageId)、冲突(同键异文明确报错) | ✅ 已完成 |
16
+ | **跨进程**:并发写入经一把文件锁串行化(幂等检查 / seq 分配 / 绑定更新 / 追加) | ✅ 已完成(真多进程验证) |
17
+ | **记录边界**:残尾(半行)不会吞掉后续写入 | ✅ 已完成 |
18
+ | 每个绑定内严格按序;重启后队列与状态恢复 | ✅ 已完成 |
19
+ | 状态机与结果关联(queued → delivered → acked,可回填回复与 turnId) | ✅ 已完成 |
20
+ | 存储损坏/不可读时**明确报错**,不伪装成空收件箱 | ✅ 已完成 |
21
+ | 能力如实上报(`canWakeIdle:false`、`deliveryMode:'inbox'`) | ✅ 已完成 |
22
+ | **自动唤醒 Codex Desktop 聊天、观察其回合事件** | ❌ **blocked**(M0 未通过) |
23
+
24
+ Codex 侧目前需要**主动来读**:`codex-inbox pending` → `codex-inbox ack`。
25
+ 这条路径不假装成唤醒——`capabilities` 里永远是 `deliveryMode: 'inbox'`。
26
+
27
+ ## 2. 快速上手
28
+
29
+ 存储默认在 `~/.dsh-codex-inbox`(`--root` 或 `DSH_CODEX_INBOX_DIR` 可改),**不写入仓库**。
30
+
31
+ ```bash
32
+ # 0) 先看能力(永远显示 inbox-only,不会谎报唤醒)
33
+ node mcp/codex-inbox.mjs capabilities
34
+
35
+ # 1) 显式绑定一条目标聊天(bindingId 自己起名;threadId 必须由人/Codex 明确给出)
36
+ node mcp/codex-inbox.mjs bind \
37
+ --binding dsh-codex-collab \
38
+ --thread 01a100f1-045c-7e10-84c7-ac8ec37eff3f \
39
+ --source session-dd9655aa-b99f-4e36-b914-a5035fd094e1 \
40
+ --note "本地实验绑定,不作为发布默认值"
41
+
42
+ # 2) DSH 侧发一条(写盘成功才返回 queued)
43
+ node mcp/codex-inbox.mjs send --binding dsh-codex-collab \
44
+ --text "请原样回显:DSH_TO_CODEX_xxxxxxxx" --key canary-001
45
+
46
+ # 3) Codex 侧/用户来读
47
+ node mcp/codex-inbox.mjs pending
48
+ node mcp/codex-inbox.mjs show <messageId>
49
+
50
+ # 4) 确认处理并回填结果
51
+ node mcp/codex-inbox.mjs delivered <messageId> --by codex-side
52
+ node mcp/codex-inbox.mjs ack <messageId> --reply "DSH_TO_CODEX_xxxxxxxx" --turn turn-xxx
53
+ ```
54
+
55
+ ## 3. 语义与退出码
56
+
57
+ **状态**:`queued` → `delivered` → `acked`;另有 `failed` 与 `delivery-unknown`
58
+ (后者留给将来接入适配器时"请求已发出但连接断了、无法确认服务端是否接受"——**故意不叫 failed**,
59
+ 免得调用方当成"没发出去"而盲目重发)。
60
+
61
+ **幂等**:同一 `bindingId` + 同一 `--key` 且正文一致 → 返回原 `messageId`(`duplicate:true`,不新增);
62
+ 正文不同 → `idempotency-conflict`(exit 4,不覆盖、不新建)。
63
+
64
+ **顺序**:每个绑定一个 `seq`,`pending` 按它返回;绑定之间互不影响。
65
+
66
+ **参数是严格的**:除 `--help`/`--json` 两个布尔旗标外,**带值旗标缺值、或出现未知旗标一律 exit 2**,
67
+ 且校验发生在打开存储、做任何变更**之前**——因此 `ack --reply`(缺值)不会把 `true` 当 undefined
68
+ 照常确认,`send --key`(缺值)不会静默丢掉幂等键,`--root`(缺值)不会回落到真实默认目录。
69
+
70
+ **退出码**:`0` 成功 / `1` 失败 / `2` 用法 / `3` 未绑定 / `4` 幂等冲突 / `5` 未知消息 / `6` 非法状态转移。
71
+
72
+ **失败原因(稳定字符串)**:`unbound-target`、`binding-thread-mismatch`、`invalid-input`、
73
+ `idempotency-conflict`、`illegal-state-transition`、`unknown-message`、`lock-timeout`(活持有者,可重试)、
74
+ `lock-stale-holder`(持有者已死,需人工清理)、`lock-unverified-holder`(元数据不可确认,需人工清理)、
75
+ `store-unreadable`、`store-corrupt`、`write-failed`。
76
+
77
+ ## 4. 可靠性:已验证到什么程度(不夸大)
78
+
79
+ **互斥就靠一条**:写锁是**一个共享路径** `<root>/.write.lock`,进入临界区只靠**一次原子独占创建**
80
+ (先把完整元数据写进唯一命名的 `pending-<token>.json`,再 `linkSync` 到锁路径;平台不支持硬链接时退化为
81
+ `wx` 独占创建 + 立即写入)。共享路径只能被创建成功一次,所以同一时刻至多一个写者——这一点不需要任何
82
+ "编号/选举/租约"机制。
83
+
84
+ **在线竞争期间绝不自动回收锁**。原因:在共享路径上"检查持有者 → 再删除/改名"无法原子化——回收者可能
85
+ 删掉别人刚创建的新锁(ABA/TOCTOU)。这一点在评审里被真实复现过两次(先删共享锁、后复用编号),所以现在
86
+ 不做任何自动回收:
87
+
88
+ | 情形 | 行为 |
89
+ |---|---|
90
+ | 持有者活着 | 等待到 `lockTimeoutMs`,报 `lock-timeout`(可重试)——**不抢** |
91
+ | 持有者已死 | **二次确认**(锁仍在、仍是同一把、pid 仍不可达)后报 `lock-stale-holder`(可恢复错误)——**不自动删** |
92
+ | 元数据空/损坏 | 报 `lock-unverified-holder`(可恢复错误)——**不按时间偷** |
93
+
94
+ "二次确认"是必需的:持有者可能**刚刚正常释放并退出**(我们读到内容之后、检查存活之前),此时若直接判 stale 就会误报;
95
+ 确认后发现锁已不在就重试获取。即便确认之后仍有极小窗口,最坏也只是报一次可恢复错误——**绝不会删除活锁,也不会产生双写者**。
96
+
97
+ **人工恢复步骤**(文档化的降级路径):确认**没有任何写入进程在运行**(收件箱使用者都是本机 CLI/进程,
98
+ 没有后台常驻写者),然后删除 `<root>/.write.lock` 即可继续。CLI 报错时会打印该文件路径。
99
+
100
+ 已验证(`node test/codex-outbox.test.mjs`,**23 项**;并发项都是**真多进程**):
101
+
102
+ - 残尾:写入一条后手工追加半个 JSON 行(不带换行),随后 `enqueue` 必须成功且能查到新 `messageId`;
103
+ 重开后两条都在,残尾作为坏行跳过。「完整但缺结尾换行」的记录也不丢、不串行。
104
+ - 4 进程同 key 同文并发 → 只有 1 条消息、只有 1 个 `duplicate:false`、`seq=1`。
105
+ - 4 进程异 key 并发 → 4 条、`seq` 恰好 `{1,2,3,4}`;**6 进程压测** → 6 条、`seq` 唯一连续 {1..6}。
106
+ - 并发绑定:不同 id 都落地;同 id 同 thread 不重复;同 id 异 thread 只成功一个;不残留临时文件。
107
+ - **活持有者期间**:两个真实进程只能等待,`messages.jsonl` **零写入**、锁文件**字节未变**;
108
+ 持有者释放后两者串行完成,同 key 仍只有 1 条消息。
109
+ - 死持有者 → `lock-stale-holder`:锁文件**原样保留**、零写入;按恢复步骤人工删除后可继续。
110
+ - **持有者在被读到时刚释放**(确定性钩子复现):必须二次确认后重试并成功,**不得误报 stale**(这条回归对旧逻辑是失败的)。
111
+ - 活持有者超时 → `lock-timeout`:锁文件**原样保留**、零写入。
112
+ - 元数据空 / 非法 JSON / 无 pid → `lock-unverified-holder`:文件**字节未变**、零写入。
113
+ - 绑定表损坏(非法 JSON / 形状不对)→ `store-corrupt`,**不退化**成"没有绑定"。
114
+ - 正常路径结束后:无锁文件残留、无 `pending-*` 残留。
115
+
116
+ **未验证 / 明确不声称**:
117
+
118
+ - **死持有者会一直阻塞**(直到人工清理):这是"宁可 fail closed 也不冒双写者"的代价,**不是自动恢复**。
119
+ - **断电级持久性**:写入是 append + 边界修复,没有 fsync 屏障,不承诺掉电后零丢失。
120
+ - **跨机器/网络文件系统**:锁依赖本机的原子独占创建(link/`O_EXCL` 语义);NFS 等共享文件系统未验证。
121
+ - 早期版本试过"编号 claim + 选举 + 自动回收",**已被判定为不成立并整块删除**:
122
+ 释放只是把 `<n>.claim` 改名为 `<n>.released`,同一个 `<n>.claim` 路径会重新可创建,
123
+ 迟到的发布者能复用已释放编号并插队(评审给了确定性复现)。现在不再有任何"编号/选举"机制。
124
+ - **真正的"自动唤醒"链路**(M0 仍 blocked)。
125
+
126
+ ## 5. 独立验证
127
+
128
+ ```bash
129
+ node test/codex-outbox.test.mjs # 24 项:语义 + 记录边界 + 真多进程并发/压测 + 独占写锁 + 存储损坏
130
+ node test/codex-inbox-cli.test.mjs # 6 项:CLI 进程级(exit code / 缺值不碰存储 / 状态不变)
131
+ node test/codex-desktop-probe.test.mjs # 11 项:doctor 纯判定 + 采集层解析 + 端到端只读运行
132
+ node mcp/codex-desktop-probe.mjs --schema # 现场能力报告(含祖先链归属与协议方法面)
133
+ ```
134
+
135
+ 实测记录(2026-10-12,本机 Windows):
136
+
137
+ - 队列 **24/24** 通过(连跑 5 次均为 24/24,无 flaky);含 4/6 进程并发、并发绑定、活持有者期间零写入、
138
+ 死持有者二次确认后 fail closed、元数据不可确认 fail closed、残尾续写、持有者刚释放的确定性回归。
139
+ - CLI **6/6** 通过:缺值/未知旗标 exit 2 且不创建存储、不改状态(queued 仍是 queued、无新消息)。
140
+ - 探针 **11/11** 通过:采集层"非终止错误 + exit 0 + 空 stdout"必须判为失败(不再是"扫描成功且为空");
141
+ 扫描失败 → `probe-inconclusive`;端点信号 → `endpoint-present-unverified`;
142
+ **桌面身份要求祖先链先遇到桌面 App**——先穿过另一份 app-server 的派生实例单列、不算桌面端点。
143
+ - 现场探针:只有直挂 `ChatGPT.exe` 的主实例算 desktop;两个经 `node_repl` 派生的实例单列 standalone;
144
+ 控制套接字不存在、daemon 连接失败 → `desktop-attachment-unavailable`。
145
+ - 根代理的三个故障演示脚本在新代码上**都不再复现**(都只是旧触发条件消失,**不作为通过依据**):
146
+ `review-inbox-reliability.mjs` 在其第一条断言处失败;
147
+ `review-inbox-stale-lock.mjs` 报 `Barrier timeout: a-read-stale`;
148
+ `review-inbox-ticket-reuse.mjs` 报 `barrier timeout: B-before-link`(它拦截的 `<n>.claim` 已不存在)。
149
+ 真正的依据是上表里新增的活持有者 / fail-closed / 高争用回归。
150
+
151
+ ## 6. 未实现的部分(以及为什么先不做)
152
+
153
+ 按计划"不为不可用的通道搭大框架":
154
+
155
+ - **没有** DSH 插件内的 `dsh_codex_send` / `dsh_codex_targets` / `dsh_codex_status` 三个工具与 HTTP 路由
156
+ (现有六个 MCP 工具与 `dsh_collab_send` 语义**未做任何改动**)。等 M0 成立、确有可用投递入口时再按 M1 契约加。
157
+ - **没有**桌面接收适配器(无端点可连)。
158
+ - **没有**因此更新 `package.json` 打包清单 / CI 断言——能力未落地的部分不进发布面。
159
+ - 未打 tag、未 npm 发布(按授权要求,先保留可审查 diff)。
160
+
161
+ ## 7. 什么时候能变成"真唤醒"
162
+
163
+ 一旦 `codex-desktop-probe` 报告 `endpoint-present-unverified`(例如桌面以 `--listen ws://…` 启动,
164
+ 或平台提供了文档化的本机控制端点),就按计划 M1–M3 补齐:显式目标绑定校验、
165
+ `dsh_codex_send/targets/status`、适配器(initialize → `thread/loadedList` 命中 → `turn.start` →
166
+ 观察 `turn.completed`)、busy 排队、`delivery-unknown` 恢复、回复关联与防循环,
167
+ 并在真实桌面做 canary 验收后,才把 `canWakeIdle` 置为 true。
@@ -0,0 +1,140 @@
1
+ # DSH 主动向当前 Codex 聊天发送消息:方案与实施计划
2
+
3
+ 日期:2026-10-10
4
+ 项目:E:/11-Github/dsh-codex-collab
5
+ 执行者:DSH 中已有的“dsh-codex-collab 项目”对话
6
+ 计划状态:可执行;桌面后端附着能力须先通过 M0 验证,尚未承诺当前 Windows 环境已可附着。
7
+
8
+ ## 1. 用户目标与完成标准
9
+
10
+ 用户在 DSH 对话中要求“告诉 Codex 某件事”,DSH 向已经明确绑定的一条 Codex Desktop 聊天发送消息;Codex 在同一条聊天中收到消息并开始处理。Codex 正忙时默认排队,保持原来的上下文、模型、工作目录和权限。
11
+
12
+ 完整验收必须同时满足:
13
+
14
+ 1. 用户原来打开的那条聊天收到带唯一标记的消息,并产生对应的 Codex 回复。
15
+ 2. 线程 ID 与绑定的目标一致,没有新建或 fork 另一条聊天。
16
+ 3. Codex 不必先主动派一个 DSH 任务,DSH 能主动发起投递。
17
+ 4. 忙时按队列顺序处理;没有明确的打断授权时不 steer、interrupt 或 cancel。
18
+ 5. 发件人能查到消息状态和失败原因,超时或重连不会悄悄重复启动回合。
19
+ 6. 重启后绑定、待投递消息和查询状态可以恢复;原有六个 MCP 工具和 DSH 派活/评审功能继续正常。
20
+
21
+ “调用另一个 Codex CLI”“写入 rollout 文件”“只有工具结果回传”“同 ID 在另一个进程能读到”“仅写入收件箱”均不满足完整验收。没有桌面接入能力时,可以交付独立可测试的收件箱,但必须明确报告完整目标仍被阻塞。
22
+
23
+ ## 2. 已有事实、来源和未知项
24
+
25
+ - 项目源码当前为 0.1.7;本机 npm/Codex 插件为 0.1.6。
26
+ - 已有 cordis.patch.yml 使用 DSH MCP 客户端启动 codex mcp-server,供 DSH 调用独立的 Codex CLI;这不是当前桌面聊天的投递入口。
27
+ - dsh_collab_send 实际回到本地 DSH enqueue;其 sessionId 指向 DSH 会话,不是 Codex 线程。保留该接口的既有语义。
28
+ - 全局 Codex CLI 为 0.147.0;桌面运行的是 bundled 0.162.0-alpha.17.2。本次观察到桌面 app-server 没有显式 --listen 参数;PID 和安装路径是临时现场信息,禁止硬编码。
29
+ - 全局 CLI 的 app-server daemon version 在 Windows 实际返回:daemon lifecycle is only supported on Unix platforms。proxy 帮助出现 --sock 并不能证明 Windows 或桌面后端支持。
30
+ - 当前任务的 Codex 线程 ID 可从受信任的 CODEX_THREAD_ID 取得。该值仅用于本次绑定/验收,禁止作为发布包默认目标。
31
+ - App Server 提供线程与回合协议;读取存储中的相同 ID 不能证明连接了桌面正在使用的同一后台。
32
+ - MCP Events 官方当前列出 Work web、桌面 Work+Cloud、dots;不能用它宣称已实现本地 Codex 聊天唤醒。
33
+ - 本项目不需要另申请 OpenAI API key;优先复用现有 Codex 后端的登录和权限。不要读取、复制或输出登录凭据。
34
+
35
+ 官方来源(实施时仍需与本机版本生成的 schema 对照):
36
+
37
+ - App Server 与 thread/turn 生命周期:https://learn.chatgpt.com/docs/app-server
38
+ - CLI 与 app-server/remote 能力:https://learn.chatgpt.com/docs/cli/reference
39
+ - MCP Events 的适用产品范围:https://developers.openai.com/plugins/build/mcp-events
40
+
41
+ 未知项:现有 Windows 桌面后台是否提供可用的第三方连接端点;第二连接是否能安全控制已加载的同一线程;桌面界面是否收到来自第二连接的消息与回合更新。这三个未知项决定能否交付完整功能。
42
+
43
+ ## 3. 选定的架构与约束
44
+
45
+ 链路:DSH 发消息工具 → 本机持久化 outbox → 已授权的目标绑定 → Codex Desktop 接收适配器 → 当前后台的指定线程 → 回合事件与回复 → 发件状态查询。
46
+
47
+ 将“可靠消息队列”与“桌面接入适配器”分开,保持两个职责清楚。首选适配器只连接已存在、能证明属于目标桌面的后台。默认不启动任何第二份 app-server,不使用 exec resume 同 ID 来冒充桌面投递。
48
+
49
+ 公开、可证的宿主投递接口或现有 app-server 连接均可成为适配器,但必须给出接口来源、端点归属、权限边界和真实桌面验收。不得使用私有 IPC 猜测、改写应用安装文件、直接写会话存储、模拟键盘点击或跳过鉴权来凑通路。
50
+
51
+ 应用不支持附着时,明确返回 desktop-attachment-unavailable / platform-unsupported,并保留待发消息。可以提供用户主动读取的 inbox 模式;该模式显示“需要主动接收”,不得显示“自动唤醒可用”。
52
+
53
+ ## 4. M0:先验证桌面接入(阻塞后续自动唤醒的前置门槛)
54
+
55
+ 产出:docs/codex-desktop-attachment-evidence.md 与一个只读 capabilities/doctor 探针。
56
+
57
+ 步骤:
58
+
59
+ 1. 使用现场实际的桌面 bundled CLI 查看帮助,必要时把协议 schema 生成到隔离临时目录;不修改全局默认 CLI、不自动升级。
60
+ 2. 查明可用端点及其实际所有者。记录平台、二进制版本、端点传输、PID 归属,避免记录令牌或用户聊天正文。
61
+ 3. 只连接已运行的目标后台,执行 initialize/initialized、thread/loaded/list 与 thread/read。目标必须在同一后台已加载,不能只在磁盘历史中命中。
62
+ 4. 如果某方法本机 schema 没有,记录确切不支持的能力,不凭最新网页猜一个方法。
63
+ 5. 在任何写入前,证明此连接能观察目标当前状态和后续回合事件,并确认不会竞争第二个执行器。
64
+ 6. 在用户已授权的目标线程空闲后,进行一条无副作用、唯一标记的 canary。只要求 Codex 原样回显,不改代码、配置或发布任何东西。验收要能在同一桌面聊天中看到消息与回复,记录 threadId、turnId、marker 和确认方式。
65
+ 7. 本次根代理仍在规划时不打断它;canary 等目标空闲后投递。
66
+
67
+ M0 通过:同一后台、同一目标、桌面可见消息和回复均有证据。
68
+ M0 未通过:记录精确限制及尝试证据;继续完成可独立验证的消息队列/能力报告,停止宣称桌面自动唤醒完成。不要自动开启远程控制、重启 Codex/DSH、修改权限模式或换到另一条聊天。
69
+
70
+ ## 5. M1:目标绑定与收发契约
71
+
72
+ 名称按仓库现有风格统一;建议新增以下三个 DSH 侧入口,保持 dsh_collab_send 的语义不变:
73
+
74
+ - dsh_codex_targets:只读列出已授权绑定的目标和接入能力。
75
+ - dsh_codex_send:向 bindingId 对应的 Codex 聊天发送 text,返回 messageId 和状态。
76
+ - dsh_codex_status:按 messageId 查询投递、运行与结果。
77
+
78
+ Codex 侧提供绑定与解除工具,例如 dsh_codex_bind / dsh_codex_unbind。由 Codex 侧明确提供目标 threadId 和用户授权的 DSH sourceSessionId,校验宿主/线程身份;不按标题猜目标,也不从最近使用记录选一条默认投递。
79
+
80
+ 最小绑定信息:bindingId、Codex threadId、宿主实例/端点标识、授权的 DSH sourceSessionId、能力、创建/更新状态。端点失效或宿主变化后重新验证,不向旧端点盲投。若当前工具执行上下文不能验证来源会话身份,要标明该限制,不能把调用者自己填写的 sessionId 当成已验证身份。
81
+
82
+ 最小发送信息:bindingId、messageId/idempotencyKey、sourceSessionId、text、createdAt、deliver=queue。生产默认 queue;steer 仅在用户明确授权打断这一目标时启用。未绑定、目标不存在、已归档、权限不足均给稳定的失败原因,不能静默新建目标。
83
+
84
+ 根代理本次明确授权:把这个功能交给“dsh-codex-collab 项目”已有对话实现,以及在本次指定 Codex 目标空闲时发送一条无副作用的验证消息。不要向其他聊天测试或广播。
85
+
86
+ ## 6. M2:可靠消息队列与接收适配器
87
+
88
+ 先复用仓库已有工具/存储方式;可以使用 Node 标准库文件存储,不预先添加依赖。若确需新的依赖,先写清必要性和可替代方案,避免架构膨胀。
89
+
90
+ - 每个绑定只有一个投递消费者,按序处理。
91
+ - 写盘成功后才返回 queued;启动对应回合并获得协议确认后才返回 running;观察到关联回合 completed 才返回 done。
92
+ - 回合 failed、目标阻塞、超时和连接中断分别报告,不把 JSON-RPC 接收成功当成任务执行成功。
93
+ - 发起请求后连接断开、无法确认服务端是否接受时,返回 delivery-unknown;先查历史/关联回合恢复,不能盲目重试导致重复执行。
94
+ - 同一绑定下相同 idempotencyKey 的重复发送返回原 messageId/结果;消息正文改变时明确冲突。
95
+ - 主机重启时恢复待发队列;对正在运行或未知投递状态重新核对,不假设未投递。
96
+ - 收件内容作为带来源标签的数据/请求传给 Codex,保留原线程的权限、模型和项目配置;不把 DSH 文本提升为 system/developer 指令。
97
+ - 回复默认只更新该 messageId 的结果,不自动把回复再次当新消息发回。带 origin 与关联 ID,限制自动往返,避免循环。
98
+ - 本机写入口与绑定授权做最小必要鉴权;敏感值仅留本地配置,日志、计划、源码和汇报均不输出令牌。保持既有网关对外范围不扩大。
99
+
100
+ 忙时实现:从真实线程状态/事件判断是否空闲;busy 则持久排队,完成后依次启动。不能通过轮询磁盘文件推断当前活跃回合,也不能调用 interrupt 来腾位置。
101
+
102
+ 适配器输出至少包括 available、deliveryMode、canWakeIdle、canObserveTurns 和 reason,清楚区分自动唤醒、在线长等待、用户主动收件三种能力。
103
+
104
+ ## 7. M3:针对本项目的验证与回归
105
+
106
+ 编写真正验证行为的测试,不用测试恒真 mock 来替代 M0。
107
+
108
+ - 未绑定、错误/失效目标:失败且不创建线程。
109
+ - DSH 来源不符合绑定:拒绝;只读列目标不暴露其他聊天正文。
110
+ - 同一消息重复发送:只出现一个回合;不同正文但同 key:冲突。
111
+ - busy 期间入队、恢复空闲后按序投递;默认不 steer/interrupt。
112
+ - 连接断开、投递未知、进程重启后恢复:不丢消息、不盲重发。
113
+ - 对应回合 done/failed/blocked 与发件状态正确关联。
114
+ - 自动往返不会形成反馈循环。
115
+ - 原有 worker/session 分发、只读会话查找、独立评审与六工具契约不回归。
116
+ - 新工具修改后同步 README、MCP 工具清单断言与打包文件清单;版本号三处保持一致。
117
+ - Windows 上进行真实桌面 canary;Linux CI 对消息队列/协议适配的纯测试、打包安装做验证,不能用 Linux 的通过替代 Windows 桌面验收。
118
+
119
+ 真实验收案例:DSH 发一条 DSH_TO_CODEX_<随机值>,绑定的原聊天收到并原样回复;记录对应 ID,确认没有新增线程,发件结果为 done。能力不满足则标记 blocked,并附上已完成部分与剩余限制。
120
+
121
+ ## 8. 建议的文件边界与执行顺序
122
+
123
+ 按已有仓库布局调整,不机械新增所有文件:
124
+
125
+ - docs/:本计划、M0 接入证据、最终使用与验收说明。
126
+ - harness/:新增 DSH 发件工具和绑定/队列服务接入;已有 dsh-bridge 的接口语义保持。
127
+ - mcp/:Codex 侧绑定/收件能力,以及与真实宿主接入的协议适配。队列和接入模块可按职责拆成少量文件。
128
+ - test/:消息队列、目标授权、真实路由契约、busy/重连/幂等回归。
129
+ - cordis.patch.yml、package.json、README、CI:仅在对应能力确实实现后更新。
130
+
131
+ 顺序:M0 证据 → 可用性结论 → M1 契约/绑定 → M2 队列与适配 → M3 回归/真实桌面验收 → 可审查 diff 与汇报。保持每一步能单独验证;接入不成立时不要为不可用的唤醒通道搭大框架。
132
+
133
+ ## 9. 执行约束与汇报
134
+
135
+ 在原 DSH 项目对话中继续,使用它已有的上下文、模型和工作目录。实际仓库必须是 E:/11-Github/dsh-codex-collab,发现既有改动时保留,不回滚他人文件。不要新建 DSH worker、工作区或另一条会话来代替。
136
+
137
+ 默认继续执行已授权的本地实现与测试,不等根代理再次确认常规步骤。不要打 tag、npm publish、修改上游仓库、强制推送或擅自扩大权限。先保留可审查的 diff 与证据,不自动发布本功能。
138
+
139
+ 回报必须包含:M0 结论和证据、所选接入机制、改动文件、测试命令与结果、真实 canary 是否进入指定桌面聊天、没有新增线程的证据、剩余限制。如果只能提供收件箱,明确写“收件箱已完成;桌面自动唤醒尚未完成”,不要笼统写功能已完成。
140
+