@twinsearth/w2m-dsh-plugin 2.0.4 → 2.0.5

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 (69) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +8 -0
  4. package/bin/w2m-joint.mjs +69 -7
  5. package/codex/mcp-servers.toml +1 -1
  6. package/docs/acceptance/2.0.5/RELEASE-205.json +21 -0
  7. package/docs/acceptance/2.0.5/RELEASE-2050.json +22 -0
  8. package/docs/adr/0001-collaboration-crdt.md +59 -0
  9. package/lib/client.js +146 -8
  10. package/package.json +2 -2
  11. package/plugin.json +1 -1
  12. package/src/agent/agent.mjs +97 -36
  13. package/src/agent/artifact-delivery.mjs +89 -0
  14. package/src/agent/caps.mjs +16 -2
  15. package/src/agent/sinks/git-sink.mjs +78 -0
  16. package/src/agent/sinks/ssh-sink.mjs +83 -0
  17. package/src/agent/sse-cursor.mjs +16 -0
  18. package/src/agent/sse-watchdog.mjs +71 -0
  19. package/src/plugin/dataplane-sink-route.mjs +113 -0
  20. package/src/plugin/fleet-chat-route.mjs +11 -0
  21. package/src/plugin/joint-dev/dispatch.mjs +5 -1
  22. package/src/plugin/joint-dev/merge-approval.mjs +115 -0
  23. package/src/plugin/joint-dev/progress.mjs +107 -0
  24. package/src/plugin/joint-dev/relay-dispatch.mjs +52 -22
  25. package/src/plugin/project-stream-route.mjs +58 -0
  26. package/src/plugin/tools-joint.mjs +6 -1
  27. package/src/plugin/tools.mjs +25 -5
  28. package/src/relay/fleet-messages.mjs +48 -24
  29. package/src/relay/project-room.mjs +47 -2
  30. package/src/relay/server.mjs +11 -5
  31. package/src/relay/state.mjs +63 -3
  32. package/src/util/chat-crypto.mjs +70 -1
  33. package/docs/COLLAB/COLLABORATION-MODEL.md +0 -240
  34. package/docs/COLLAB/EXECUTION-AUTH-MODEL.md +0 -361
  35. package/docs/COLLAB/M1-ACCEPTANCE-RUNBOOK.md +0 -70
  36. package/docs/COLLAB/M1-MAINTENANCE.md +0 -313
  37. package/docs/COLLAB/M2-TASK-GRANT.md +0 -231
  38. package/docs/COLLAB/M3-GIT-NATIVE-TASKS.md +0 -291
  39. package/docs/COLLAB/M4-PROJECT-ROOM.md +0 -260
  40. package/docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md +0 -263
  41. package/docs/COLLAB/M6-CROSS-NETWORK-RELIABILITY.md +0 -231
  42. package/docs/COLLAB/M7-METRICS-ACCEPTANCE.md +0 -263
  43. package/docs/COLLAB/TASK-MODEL.md +0 -281
  44. package/docs/acceptance/1.9.10/RELEASE-1910.json +0 -21
  45. package/docs/acceptance/1.9.11/RELEASE-1911.json +0 -20
  46. package/docs/acceptance/1.9.12/RELEASE-1912.json +0 -20
  47. package/docs/acceptance/1.9.13/RELEASE-1913.json +0 -20
  48. package/docs/acceptance/1.9.14/RELEASE-1914.json +0 -20
  49. package/docs/acceptance/1.9.15/RELEASE-1915.json +0 -22
  50. package/docs/acceptance/1.9.16/RELEASE-1916.json +0 -21
  51. package/docs/acceptance/1.9.17/RELEASE-1917.json +0 -20
  52. package/docs/acceptance/1.9.19/RELEASE-1919.json +0 -21
  53. package/docs/acceptance/1.9.20/RELEASE-1920.json +0 -22
  54. package/docs/acceptance/1.9.4/RELEASE-194.json +0 -23
  55. package/docs/acceptance/1.9.5/RELEASE-195.json +0 -23
  56. package/docs/acceptance/1.9.6/RELEASE-196.json +0 -22
  57. package/docs/acceptance/1.9.7/RELEASE-197.json +0 -21
  58. package/docs/acceptance/1.9.8/RELEASE-198.json +0 -21
  59. package/docs/acceptance/1.9.9/RELEASE-199.json +0 -21
  60. package/lib/client.js.bak2 +0 -640
  61. package/src/plugin/dataflow.mjs +0 -72
  62. package/src/plugin/fleet-target.mjs.bak-arch2 +0 -812
  63. package/src/plugin/fleet-target.mjs.bak-archive +0 -812
  64. package/src/plugin/tofuture/report.mjs +0 -44
  65. package/src/plugin/tools.mjs.bak-c7 +0 -7029
  66. package/src/relay/tofuture-whitelist.mjs +0 -46
  67. package/src/util/data-plane.mjs +0 -180
  68. package/src/util/sealed-object.mjs +0 -141
  69. package/src/util/tofuture-hmac.mjs +0 -30
@@ -1,263 +0,0 @@
1
- # M5 多客户端原语(分支/文件锁、审查任务、通知)设计规格
2
-
3
- > 状态:**设计稿**。只定义锁、审查、通知三个原语的模型与语义,不含实现。
4
- > 版本:`1.3.6-beta.*`(GitHub prerelease,不入 npm)。
5
- > 上游:`docs/MULTI-MACHINE-COLLABORATION-PLAN.md` M5;依赖 M3(合并队列)、M4(项目房间/事件)。
6
-
7
- ---
8
-
9
- ## 1. 目标与非目标
10
-
11
- ### 目标
12
-
13
- 1. **避免互相踩**:分支锁与文件锁让两个客户端/机器在改同一处之前先协商;冲突在**发生前**暴露,而不是合并时才发现。
14
- 2. **审查成为一等任务**:补丁/改动可以要求审查;审查结论(批准/要求修改)进入项目账本,并可作为合并前置条件。
15
- 3. **通知可达且不丢**:任务分配、审查请求、锁冲突、合并结果、分支推进等事件能定向送达;离线客户端回来能补齐;按客户端维护已读水位。
16
- 4. **全部可审计**:锁的获取/续租/强制释放、审查结论、通知投递都进项目事件流与 ledger。
17
-
18
- ### 非目标(本版本不做)
19
-
20
- - 不做跨项目锁、不做目录级递归锁(只到分支 + 文件路径)。
21
- - 不做自动抢占/自动释放冲突的"聪明策略"——过期才可重入,强制释放必须显式且留痕。
22
- - 不做外部通知渠道(邮件/Webhook/IM)——本版本只有项目房间内通知(SSE + 未读水位)。
23
- - 不做多人账号体系(身份仍为 machine + operator,与 M4 一致)。
24
-
25
- ---
26
-
27
- ## 2. 原语一:分支锁 / 文件锁
28
-
29
- ### 2.1 锁对象
30
-
31
- ```json
32
- {
33
- "lock_id": "01M6…",
34
- "project_id": "…",
35
- "kind": "branch|file",
36
- "target": { "branch": "main" } | { "path": "src/a.mjs" },
37
- "mode": "exclusive",
38
- "holder": { "kind": "machine|operator", "id": "…" },
39
- "reason": "改 auth 模块(任务 01M6…)",
40
- "acquired_at": "…",
41
- "expires_at": "…",
42
- "lease_ms": 1800000,
43
- "renewed_at": "…"
44
- }
45
- ```
46
-
47
- - 分支锁:`(project_id, branch)` 唯一;文件锁:`(project_id, path)` 唯一。
48
- - 同一 target 同一时刻只允许一个 exclusive holder;无共享锁(读不需要锁)。
49
- - TTL 默认 30 分钟;持有人每 5 分钟续租(客户端心跳);过期自动释放(事件 `lock.expired`)。
50
- - 持有者可以主动释放;被他人持有不可抢占(除非过期)。
51
- - **强制释放**:owner 可 `force_release`,必须带 reason,事件 `lock.force_released` 记 who/why,通知原持有人。
52
-
53
- ### 2.2 强制级别(项目设置)
54
-
55
- | 模式 | 行为 |
56
- |---|---|
57
- | `advisory`(默认) | 只记录与展示;获取冲突时返回 `LOCK_HELD` 但调用方可选择继续(`force: true` 需有权限,事件记录) |
58
- | `enforced` | 获取失败即阻止:worktree 任务/合并提交在创建时检查锁,冲突直接拒绝(`LOCK_HELD`),不会进入队列 |
59
-
60
- 强制级别只对"声明了 target_branch / 涉及文件路径"的写任务生效(`workspace.kind=worktree` 且 `produce=patch/commit`);只读任务不需要锁。
61
-
62
- ### 2.3 与 M3 合并队列的关系
63
-
64
- - 合并队列的 `merge-slot` 是**系统级串行化**(保证同一时刻一个合并器)。
65
- - 分支锁是**用户级协调**(保证两个提交者不在同一分支上互相覆盖意图)。
66
- - 合并时若目标分支被他人持有锁且模式为 `enforced`:合并项排队等待(`waiting_on: lock:<id>`),锁释放后继续;等待超过 `expires_at` 则失败 `LOCK_WAIT_TIMEOUT`。
67
- - 文件锁在合并时用于**冲突预检**:队列里两个 item 的 `files_changed` 有交集且各自目标文件被 lock 覆盖 → 后一个 item 等待或提示冲突(`PATH_LOCK_CONFLICT`)。
68
-
69
- ### 2.4 API 与错误码
70
-
71
- ```
72
- GET /v1/projects/:id/locks
73
- POST /v1/projects/:id/locks {kind, target, lease_ms, reason, mode?}
74
- POST /v1/projects/:id/locks/:lock_id/renew
75
- DELETE /v1/projects/:id/locks/:lock_id (holder)
76
- DELETE /v1/projects/:id/locks/:lock_id?force=1&reason=… (owner)
77
- ```
78
-
79
- ```
80
- LOCK_HELD 目标已被持有(返回 holder + expires_at)
81
- LOCK_NOT_OWNER 非 holder 续租/释放
82
- LOCK_EXPIRED 操作时已过期
83
- LOCK_FORCE_DENIED 无 owner 权限或缺少 reason
84
- LOCK_WAIT_TIMEOUT 合并等待锁超时
85
- PATH_LOCK_CONFLICT 文件锁与队列内已有改动重叠
86
- LOCK_LIMIT 单项目/单持有者锁数量超限(默认 20/10)
87
- ```
88
-
89
- ---
90
-
91
- ## 3. 原语二:审查任务(review)
92
-
93
- ### 3.1 定位
94
-
95
- 审查是**控制面任务**(不需要目标机器执行命令):它指向一个制品/补丁,由有权限的审查者给出结论,结论进入项目事件流与 ledger,并可作为合并前置条件。
96
-
97
- ```json
98
- {
99
- "review_id": "01M6…",
100
- "project_id": "…",
101
- "target": { "kind": "patch|artifact", "sha256": "…", "source_task_id": "…", "diffstat": {…} },
102
- "requested_by": { "kind": "machine|operator", "id": "…" },
103
- "reviewers": [{ "kind": "machine|operator", "id": "…" }],
104
- "required_approvals": 1,
105
- "status": "pending|approved|changes_requested|expired|cancelled",
106
- "decisions": [
107
- { "reviewer": {…}, "decision": "approve|changes_requested|comment",
108
- "comment": "…", "findings": [{"file":"src/a.mjs","line":10,"severity":"high|medium|low","text":"…"}],
109
- "decided_at": "…" }
110
- ],
111
- "due_at": "…"
112
- }
113
- ```
114
-
115
- 规则:
116
-
117
- - 审查者必须具有 `maintainer` 或 `owner` 角色;`worker` 不能当审查者。
118
- - **禁止自审**:`requested_by` 与 reviewer 相同 → `REVIEW_SELF_NOT_ALLOWED`(owner 可 override,但事件标记 `self_review_override`)。
119
- - 批准数达到 `required_approvals` → `approved`;任一人 `changes_requested` → `changes_requested`(可由提交者重新提交新 revision 触发新一轮)。
120
- - 审查结论不可改(审计),改变结论需要新的 decision 记录(`REVIEW_ALREADY_DECIDED` 拒绝重复同向结论)。
121
- - 过期未处理 → `expired`(可重建新审查)。
122
- - 审查证据引用内容寻址制品(patch、测试结果、diffstat),审查者客户端按 sha 拉取。
123
-
124
- ### 3.2 与合并门禁
125
-
126
- 项目设置 `merge_requires_review: true` 时,M3 合并队列在应用前检查:同一 `patch_sha` 存在 `approved` 审查;否则 item 停在 `queued` 返回 `REVIEW_REQUIRED`。测试门禁(M3)与审查门禁相互独立,可同时开启。
127
-
128
- ### 3.3 API 与错误码
129
-
130
- ```
131
- POST /v1/projects/:id/reviews 创建(requested_by 有权限)
132
- GET /v1/projects/:id/reviews?status=…
133
- GET /v1/projects/:id/reviews/:review_id
134
- POST /v1/projects/:id/reviews/:review_id/decisions {decision, comment, findings}
135
- ```
136
- ```
137
- REVIEW_PERMISSION 无 maintainer/owner 角色
138
- REVIEW_SELF_NOT_ALLOWED 自审且未 override
139
- REVIEW_ALREADY_DECIDED 重复同向结论
140
- REVIEW_EXPIRED 过期后提交
141
- REVIEW_TARGET_UNKNOWN 目标 sha 不在制品索引
142
- REVIEW_REQUIRED 合并门禁挡住(M3 返回)
143
- ```
144
-
145
- ---
146
-
147
- ## 4. 原语三:通知(notifications)
148
-
149
- ### 4.1 通知对象
150
-
151
- ```json
152
- {
153
- "notification_id": "01M6…",
154
- "project_id": "…",
155
- "kind": "task.assigned|review.requested|review.decided|lock.conflict|merge.conflict|merge.merged|ref.updated|mention|maintenance.result",
156
- "target": { "kind": "machine|operator", "id": "…" } | { "kind": "broadcast" },
157
- "subject": "短标题",
158
- "refs": { "task_id": "…", "review_id": "…", "lock_id": "…", "merge_queue_id": "…", "sha256": "…" },
159
- "created_at": "…",
160
- "requires_action": true
161
- }
162
- ```
163
-
164
- ### 4.2 投递与已读
165
-
166
- - 投递路径:项目事件流 `notification.created`(SSE 实时)+ `GET /v1/projects/:id/notifications?since_ms=…`(离线补齐)。
167
- - 已读水位:按客户端身份(machine_id 或 operator name)维护 `read_watermark`(ms);`GET …/notifications/unread` 返回 `since read_watermark` 的计数与摘要。
168
- - 去重:同一 `(kind, refs 主键, target)` 60 秒内只产生一条。
169
- - 限流:单项目 ≤ 120 条/分钟,超出合并为 `digest`(摘要一条,含 count)。
170
- - 保留:通知保留 7 天或 2000 条/项目(取小);已读回执只存水位,不存逐条已读。
171
- - 权限:只有项目成员能读该项目通知;非成员不泄露。
172
-
173
- ### 4.3 API
174
-
175
- ```
176
- GET /v1/projects/:id/notifications?since_ms=…&limit=…
177
- GET /v1/projects/:id/notifications/unread
178
- POST /v1/projects/:id/notifications/read {watermark_ms}
179
- ```
180
- ```
181
- NOTIFICATION_RATE_LIMITED 被合并为 digest(不是错误,返回 digest id)
182
- NOTIFICATION_FORBIDDEN 非成员
183
- ```
184
-
185
- ---
186
-
187
- ## 5. 权限矩阵(叠加 M4 角色)
188
-
189
- | 动作 | owner | maintainer | worker | observer |
190
- |---|---|---|---|---|
191
- | 获取分支/文件锁 | ✅ | ✅ | ✅ | ❌ |
192
- | 强制释放锁 | ✅ | ❌ | ❌ | ❌ |
193
- | 创建审查 | ✅ | ✅ | ✅(请求他人审) | ❌ |
194
- | 作为审查者裁决 | ✅ | ✅ | ❌ | ❌ |
195
- | 读通知 | ✅ | ✅ | ✅ | ✅ |
196
- | 标记已读 | ✅ | ✅ | ✅ | ✅ |
197
-
198
- ---
199
-
200
- ## 6. 并发与一致性要点
201
-
202
- 1. **锁获取必须是原子 CAS**:relay 单线程处理请求,`(target)` 上已有未过期 holder 则失败;并发同请求只一个成功(集成测试覆盖 10 并发)。
203
- 2. **锁/审查/通知状态变更都写项目事件 + ledger**;重启后从状态文件 + ledger 恢复。
204
- 3. **过期清理**:relay 周期扫描(复用 lease sweep 节奏),过期锁写 `lock.expired` 并通知原持有人。
205
- 4. **客户端离线**:锁过期后回归即可再获取;被通知的事件按 since 补齐。
206
- 5. **时钟**:服务端时间权威;客户端只用水位比较,不自行判定过期。
207
-
208
- ---
209
-
210
- ## 7. 验收(真实任务)
211
-
212
- ### 正向
213
-
214
- 1. **分支锁**:客户端 A 获取 main 锁 → B 获取返回 `LOCK_HELD`(含 holder/expires)→ A 释放后 B 成功;SSE 两侧都收到 lock 事件。
215
- 2. **enforced 模式**:项目切到 enforced 后,B 在无锁情况下创建 worktree 写任务被拒(`LOCK_HELD`),A 释放后成功。
216
- 3. **锁过期**:A 持锁不续租,TTL 到期后 B 可获取;A 收到 `lock.expired` 通知。
217
- 4. **强制释放**:owner 强制释放 A 的锁,带 reason;A 收到通知;事件含 who/why。
218
- 5. **审查通过**:A 提交 patch → 创建审查指派 B → B 批准 → 合并门禁放行。
219
- 6. **审查拒绝**:B 提 `changes_requested` + 行级 findings → A 客户端看到→ 重新提交新 revision。
220
- 7. **自审禁止**:A 指派自己 → `REVIEW_SELF_NOT_ALLOWED`。
221
- 8. **通知补齐**:B 离线期间发生任务分配/审查请求 → 重新连上后 unread 计数正确、事件按 since 补齐、标记已读后归零。
222
-
223
- ### 负向(自动化)
224
-
225
- ```
226
- 10 并发抢同一分支锁 → 恰好 1 成功
227
- 非 holder 续租/释放 → LOCK_NOT_OWNER
228
- 非 owner 强制释放 → LOCK_FORCE_DENIED
229
- worker 审查裁决 → REVIEW_PERMISSION
230
- 重复 approve → REVIEW_ALREADY_DECIDED
231
- 非成员读通知 → NOTIFICATION_FORBIDDEN
232
- 通知风暴 500 条/分钟 → digest 合并且不丢事件
233
- 过期锁被操作 → LOCK_EXPIRED
234
- ```
235
-
236
- ---
237
-
238
- ## 8. 交付物清单(实现阶段)
239
-
240
- ```
241
- src/relay/locks.mjs 锁的原子获取/续租/释放/过期/强制释放、限流
242
- src/relay/reviews.mjs 审查对象、权限、自审、结论状态机、合并门禁查询
243
- src/relay/notifications.mjs 通知生成/去重/限流/digest/未读水位、SSE 事件
244
- src/relay/server.mjs /v1/projects/:id/{locks,reviews,notifications} 路由
245
- src/relay/state.mjs lock/review/notification 状态持久化 + 事件
246
- src/plugin/tools.mjs 锁/审查/通知的工具入口(实现时定名)+ 任务声明锁
247
- src/plugin/fleet-chat-route.mjs 宿主代理上述只读/操作接口
248
- lib/client.js 面板:锁列表、审查列表/裁决、通知铃铛与未读
249
- test/locks.test.mjs 并发/过期/强制释放/权限
250
- test/reviews.test.mjs 状态机/自审/合并门禁/权限
251
- test/notifications.test.mjs 去重/限流/digest/未读水位/离线补齐
252
- test/m5-e2e.test.mjs 双客户端:锁冲突→审查→合并门禁(模拟)
253
- docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md 本文
254
- ```
255
-
256
- ## 9. 开放问题(实现前确认)
257
-
258
- 1. **锁强制级别默认**:`advisory`(推荐,先展示再用)还是 `enforced`?
259
- 2. **文件锁粒度**:精确路径即可,还是需要 glob/目录模式(如 `src/**`)?
260
- 3. **审查者指派**:指定具体审查者(推荐)还是"具备 maintainer 角色的人皆可审"?
261
- 4. **必要批准数**:默认 1 还是按项目可配(1/2)?
262
- 5. **通知渠道**:本版本只做项目房间内通知,是否需要同时保留 fleet chat 提醒(复用 mentions)?
263
- 6. **锁等待策略**:合并队列遇到 enforced 锁是等待(带超时)还是直接失败让用户重试?
@@ -1,231 +0,0 @@
1
- # M6 跨网络可靠性设计规格
2
-
3
- > 状态:**设计稿**。只定义权威通道、分块/断点、离线队列与 P2P 降级语义,不含实现。
4
- > 版本:`1.3.7-beta.*`(GitHub prerelease,不入 npm)。
5
- > 上游:`docs/MULTI-MACHINE-COLLABORATION-PLAN.md` M6;承接 M3(大 patch 制品)与 M4(事件流)的传输需求。
6
-
7
- ---
8
-
9
- ## 1. 目标与非目标
10
-
11
- ### 目标
12
-
13
- 1. **正确性只依赖 relay**:任务、结果、事件、制品全部走 relay 通道;任何链路的短暂中断都表现为"重试/断点/补齐",而不是"任务丢失或重复执行"。
14
- 2. **大制品可传**:blob 支持分块上传、断点续传、Range 下载;打断后从缺失块继续,最终 sha256 校验通过才算成功。
15
- 3. **离线不丢**:机器离线期间任务不被丢(队列 + 重派发);机器产出的结果先进本地 outbox,重连后幂等投递;客户端事件按 seq 补齐。
16
- 4. **P2P 只做加速**:P2P 失败不影响任何任务的正确性;提供明确的模式开关与可观测的成功率,而不是把它当传输保障。
17
- 5. **可观测**:重试、断点、outbox 深度、SSE 重连、P2P 成功率、事件延迟全部有指标。
18
-
19
- ### 非目标(本版本不做)
20
-
21
- - 不做多 relay 集群/主备切换(relay 仍是单点;本版本只保证**重启后恢复**)。
22
- - 不做 P2P 数据通道传输大文件(P2P 本版本仍只用于 offer 推送加速)。
23
- - 不做端到端 TLS 终止(relay 当前 HTTP;TLS 由部署层反代决定,本版本只保证协议向后兼容)。
24
- - 不做跨 relay 的数据迁移。
25
-
26
- ---
27
-
28
- ## 2. 现状与缺口
29
-
30
- | 现有 | 缺口 |
31
- |---|---|
32
- | relay 持久化(tasks/devices/results/messages/ledger/blob) | 重启恢复已具备;缺**进行中传输**的恢复 |
33
- | blob 单次 PUT,帧上限 16 MiB,存储上限 8 MiB/256 个 | 无分块、无断点;>8 MiB 制品直接失败;中断即丢弃 |
34
- | SSE 事件流 + seq | 断线重连可用;但**事件保留窗口外的补齐**依赖快照(M4 已有) |
35
- | 任务 re-offer(设备重连时 re-offer offered/queued) | 已有;缺"结果本地 outbox + 幂等重投" |
36
- | P2P(STUN/UDP,auto/direct/relay) | 明文拒绝、NAT 不可测;当前把 P2P 当传输路径,失败才回落 |
37
- | 心跳/租约 | 已有;缺"离线机器的任务不丢"的明确语义(等待 vs 过期) |
38
-
39
- ---
40
-
41
- ## 3. 通道分级(权威性)
42
-
43
- ```
44
- 控制通道(权威):POST /v1/task、任务/租约/结果、项目事件、memory、通知 —— 全部走 relay HTTP
45
- 数据通道(权威):blob store(分块/断点后)—— 与任务解耦,内容寻址(sha256)
46
- 加速通道(机会性):P2P offer push —— 只影响"offer 到达得快不快",不影响正确性
47
- ```
48
-
49
- 不变量:
50
-
51
- 1. **结果只通过 relay 提交**(P2P 不承载结果)。
52
- 2. **任何任务可被重派发**:P2P 推送的 offer 与 relay SSE 的 offer 用同一 `(task_id, attempt, dedupe_key)` 去重(已有),所以 P2P 丢失/重复都不破坏 exactly-once 语义。
53
- 3. **制品以 sha256 为身份**:上传完成前不可引用;引用后内容不可变。
54
-
55
- ---
56
-
57
- ## 4. Blob 分块 / 断点
58
-
59
- ### 4.1 上传会话协议
60
-
61
- ```
62
- POST /v1/fleet/blob-sessions
63
- { sha256, size, chunk_bytes? } → 201 { session_id, chunk_bytes, chunks, received: [] }
64
- PUT /v1/fleet/blob-sessions/:sid/chunks/:index (octet-stream, ≤ chunk_bytes)
65
- → 200 { received: [..] } | 409 SESSION_COMMITTED | 413 CHUNK_TOO_LARGE
66
- GET /v1/fleet/blob-sessions/:sid → { received: [..], missing: [..], expires_at }
67
- POST /v1/fleet/blob-sessions/:sid/commit → 200 { sha256, bytes } | 400 BLOB_SHA_MISMATCH
68
- ```
69
-
70
- - `chunk_bytes` 默认 1 MiB(上限 8 MiB,必须整除 size 或最后一块短);`chunks ≤ 256`(即单 blob ≤ 256 MiB,默认配额另设)。
71
- - 会话 TTL 24h;commit 后会话删除,块拼成最终 blob 并校验 sha256;校验失败删除并返回 `BLOB_SHA_MISMATCH`。
72
- - 块 PUT 幂等:同一 index 重传覆盖;乱序允许;`received` 返回已收列表,客户端据此续传。
73
- - 并发:同一会话的块可并发(不同 index),commit 等待所有块到齐(否则 `BLOB_INCOMPLETE` + missing)。
74
- - 配额:单机器并发会话 ≤2、未完成会话总字节 ≤ 512 MiB;超限 `BLOB_QUOTA`。
75
- - 存储:块存临时目录;commit 后写入现有 blob store(复用现有 `MAX_BLOB_BYTES` 机制,但改为可配置上限,默认 64 MiB)。
76
-
77
- ### 4.2 下载断点
78
-
79
- ```
80
- GET /v1/fleet/blobs/:sha (全量;小文件)
81
- GET /v1/fleet/blobs/:sha?range=0-1048575 → 206 Partial Content + Content-Range
82
- ```
83
-
84
- - 客户端按需 Range 重试;下完校验 sha256,不符重下。
85
- - 大制品(M3 的 patch/conflict)优先 Range 下载,避免一次占满内存。
86
-
87
- ### 4.3 agent 集成
88
-
89
- - 上传决策:输出 > 内联阈值(64 KiB)→ 分块会话上传;≤ 阈值仍内联。
90
- - 断点续传:agent 在 state 目录持久化 `{session_id, sha, received[]}`;重连后 GET 会话 → 缺失块补传。
91
- - 重试策略:块级指数退避(1s/2s/4s/8s,上限 60s,最多 10 次);会话过期则重新开新会话并全量重传(不无限重试)。
92
- - 失败语义:上传最终失败 → 任务结果标记 `BLOB_UPLOAD_FAILED`(与 v1.3.1 一致),不引用不存在的 blob。
93
-
94
- ### 4.4 错误码
95
-
96
- ```
97
- BLOB_INCOMPLETE commit 时缺块(返回 missing)
98
- BLOB_SHA_MISMATCH 拼接后 sha 不符(删除会话)
99
- BLOB_QUOTA 会话/字节超配额
100
- CHUNK_TOO_LARGE 块超过 chunk_bytes
101
- SESSION_NOT_FOUND 会话过期/不存在
102
- SESSION_COMMITTED 已 commit 后继续写块
103
- BLOB_TOO_LARGE 声明 size 超过配置上限
104
- ```
105
-
106
- ---
107
-
108
- ## 5. 离线队列与结果 outbox
109
-
110
- ### 5.1 任务侧(relay)
111
-
112
- - 任务创建后若无在线可执行机器:任务保持 `queued`(不丢),机器上线时 re-offer(已有);`depends_on` 在依赖完成后释放(已有)。
113
- - 租约过期不等于任务失败:`expired` 是租约状态;relay 管理端看到任务仍可被重派发(`max_attempts` 策略决定是否终止)。
114
- - 明确语义:`halt` 策略(never/now)已有,保持;本版本不改语义,只补可观测与测试。
115
-
116
- ### 5.2 结果侧(agent outbox)
117
-
118
- - agent 把结果先写本地 outbox(state 目录 JSONL,原子写):`{result_id, task_id, attempt, dedupe_key, payload_sha, attempts, next_retry_ms}`。
119
- - 投递成功(relay 200/已 dedupe)→ 标记 ack 并清理;失败/断网 → 指数退避重试,跨重启继续。
120
- - 幂等:relay 按 `(task_id, attempt)` 结果去重(已有);outbox 重复投递安全。
121
- - 上限:outbox 200 条/16 MiB,超限只保留最近 + 告警(`OUTBOX_FULL`),避免磁盘无限增长。
122
- - 结果中的 blob 引用:先确保 blob 已 commit,再投递结果(顺序保证)。
123
-
124
- ### 5.3 客户端侧(DSH 插件)
125
-
126
- - SSE 断线重连(已有)+ `since_seq` 补齐(M4);超出保留窗口 → 快照。
127
- - 通知/项目事件按 seq 补齐;本地只存水位。
128
- - 读请求失败不阻塞界面,显示"连接中/离线"状态(M4 连接状态已有)。
129
-
130
- ---
131
-
132
- ## 6. 重启与恢复
133
-
134
- | 场景 | 期望行为 | 验收 |
135
- |---|---|---|
136
- | relay 重启(持久化开启) | 设备/任务/结果/事件恢复;租约重派发;seq 连续 | 重启前后任务不丢、事件 seq 连续 |
137
- | agent 重启 | outbox 恢复、未完成 blob 会话续传、worktree/产物状态检查 | 重启后任务重试成功且不重复执行 |
138
- | 客户端重启 | 水位持久化,未读通知仍正确 | 重启后 unread 计数一致 |
139
- | 网络闪断(<30s) | SSE 重连、请求重试;无用户可见失败 | 断网 30s 任务照常完成 |
140
- | 断网 60s(执行机) | 租约过期 → 机器回归后 re-offer;任务不丢 | 断网 60s 后任务完成或明确失败(不悬挂) |
141
-
142
- ---
143
-
144
- ## 7. P2P 降级为机会性加速
145
-
146
- ### 7.1 模式定义(保持现有三值,语义收紧)
147
-
148
- | 模式 | 行为 | 正确性依赖 |
149
- |---|---|---|
150
- | `relay`(推荐默认) | 不绑 UDP、不尝试 P2P;offer 只走 relay | 无 |
151
- | `auto`(保持现有默认) | 尝试 P2P push offer;失败/超时静默回落 relay;结果仍只走 relay | 无(P2P 只影响延迟) |
152
- | `direct` | 要求 P2P 成功,失败则拒绝 offer(`P2P_UNAVAILABLE`) | 有(仅用于可控内网/实验) |
153
-
154
- ### 7.2 修复与约束
155
-
156
- - **明文拒绝**:启用加密时,P2P 握手失败(对端明文)→ 只记录 `P2P_CRYPTO_PLAINTEXT_PEER` 并回落 relay,不重试明文通道。
157
- - **NAT 不可测**:`auto` 下 N-1 台机器之间无成功记录时,P2P 成功率指标会下降;不做"强制打洞"。
158
- - **去重**:P2P offer 与 relay offer 以 `(task_id, attempt, dedupe_key)` 去重(已有),重复到达不执行两次。
159
- - **默认值建议**:`auto` 保持默认以兼容现有配置,但文档明确"P2P 不承担正确性";`direct` 标记为实验性。
160
- - **指标**:`p2p_push_attempts / success / fallback`、`p2p_crypto_refusals`、offer 到达延迟(P2P vs relay)。
161
-
162
- ---
163
-
164
- ## 8. 可观测性指标(M6)
165
-
166
- ```
167
- blob_upload_sessions_open / committed / failed
168
- blob_chunks_retried / bytes_transferred / resume_count
169
- blob_download_range_requests / sha_mismatch
170
- agent_outbox_depth / retries / dropped
171
- sse_reconnects / events_replayed
172
- task_requeued_on_reconnect / lease_expired_total
173
- p2p_push_attempts / p2p_push_success_ratio / p2p_fallback_total
174
- relay_event_lag_ms(事件时间 vs 客户端应用时间)
175
- ```
176
-
177
- 全部经 `w2m_stats`/metrics 路由暴露;M7 统一验收。
178
-
179
- ---
180
-
181
- ## 9. 验收(真实任务 + 混沌)
182
-
183
- ### 正向
184
-
185
- 1. **大制品**:上传 40 MiB patch 制品(分块)→ 下载校验 sha 一致;同时验证 8 MiB 旧上限不再阻断。
186
- 2. **断点续传**:上传到 50% 断开 agent → 重连后只补缺失块,最终 commit 成功、sha 正确。
187
- 3. **离线结果**:执行机断网 60s 完成任务 → 结果进 outbox → 恢复后投递,任务只执行一次(ledger 无重复结果)。
188
- 4. **断网 30s**:SSE 客户端断网 30s → 重连补齐事件,无丢事件。
189
- 5. **relay 重启**:重启前后项目事件 seq 连续、任务不丢、租约重派发。
190
-
191
- ### 负向(自动化)
192
-
193
- ```
194
- 块缺失 commit → BLOB_INCOMPLETE(missing)
195
- 块被篡改 → BLOB_SHA_MISMATCH,blob 不存在
196
- 会话过期后写块 → SESSION_NOT_FOUND
197
- 超配额 → BLOB_QUOTA
198
- 重复块 PUT → 幂等(received 去重)
199
- P2P 明文对端 → 回落 relay,任务成功
200
- P2P direct 模式无通道 → P2P_UNAVAILABLE(仅此模式)
201
- outbox 满 → 保留最近 + OUTBOX_FULL 告警,进程不崩
202
- ```
203
-
204
- ---
205
-
206
- ## 10. 交付物清单(实现阶段)
207
-
208
- ```
209
- src/relay/blob-sessions.mjs 分块会话、commit 校验、配额、TTL 清理
210
- src/relay/server.mjs blob-sessions 路由、Range 下载、帧上限适配
211
- src/relay/state.mjs blob 元数据(size/上限可配)
212
- src/agent/blob-upload.mjs 分块上传、断点续传、持久化会话状态、重试
213
- src/agent/outbox.mjs 结果 outbox(原子写、退避、aek 清理、上限)
214
- src/agent/agent.mjs 接入 blob-upload/outbox;重连后恢复
215
- src/plugin/fleet-chat-route.mjs 面板连接状态(SSE 重连/事件延迟)
216
- src/plugin/tools.mjs w2m_stats 增加 M6 指标
217
- lib/client.js 连接状态显示(重连/补事件)
218
- test/blob-sessions.test.mjs 会话/缺块/篡改/配额/幂等
219
- test/outbox.test.mjs 重试/幂等/上限/跨重启
220
- test/reliability-e2e.test.mjs 断网/重启/大文件(本地模拟)
221
- docs/COLLAB/M6-CROSS-NETWORK-RELIABILITY.md 本文
222
- ```
223
-
224
- ## 11. 开放问题(实现前确认)
225
-
226
- 1. **blob 上限**:默认 64 MiB/单个是否够用(M3 的 patch 通常小,但二进制制品可能大)?总配额(每机器 512 MiB 未完成会话)是否合适?
227
- 2. **chunk 大小**:1 MiB 默认是否合适(帧上限 16 MiB,可调到 4 MiB)。
228
- 3. **P2P 默认模式**:保持 `auto` 还是改为 `relay`(更诚实但改变现有行为)?
229
- 4. **Range 下载**:本版本就做,还是只做全量下载 + 客户端分段拉取?
230
- 5. **outbox 上限**:200 条/16 MiB 是否够用?
231
- 6. **relay TLS**:是否把反代 TLS 作为 M6 部署验收的一部分(当前 HTTP 明文,跨网络安全性问题)?