@sema-agent/server 7.43.0 → 7.44.0

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 (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -1,69 +1,13 @@
1
- /**
2
- * A-010.18 —— 收编弧跑动期间的**进程内静默闸**(design/183 I1「引擎先停」在本进程里的执法面)。
3
- *
4
- * ── 病 ──────────────────────────────────────────────────────────────────────────────────────────
5
- * 183 I1 写的是「发起收编之前必须先把引擎停掉」。那是一条**运维前提**,而本进程此前对它**零执法**:
6
- * `POST /v1/adoption` 收下请求、弧开始逐表 UPDATE 的同时,本副本的维护 tick 照常每几秒跑一轮,
7
- * 而那一轮里好几条腿写的正是被迁的那些表。最锋利的一条是**审批收敛器**(`approval-reconciler.ts`):
8
- * 它按 `owner` 读 PARKING/STREAM_PENDING 行并 CAS 结算。收编把 `approval_ask.owner` 从 A 改成 B 的
9
- * 那一瞬,收敛器手里可能正握着按**旧** owner 读出来的一批行 —— 结算下去就是把一只本该 PARKED 的 ask
10
- * 写成 **DENIED**,而 DENIED 是终局:收编回滚不了它,重跑也追认不回来。
11
- *
12
- * ── 修 ──────────────────────────────────────────────────────────────────────────────────────────
13
- * 弧跑动期间把维护 tick 整轮让开。判据不是「挑几条腿避让」而是**整轮**:
14
- * ① 维护 tick 是 best-effort 的周期动作(每条腿都幂等、都能等下一轮),让开几轮零代价;而逐腿判
15
- * 「这条腿碰不碰被迁的表」会立刻变成第二份需要与 `REBIND_LEGS` 同步的表 —— 那正是本仓反复吃过亏的
16
- * 「两份判据必然漂」形(漏判一条新腿是**静默**的:它照写不误)。
17
- * ② 弧本身是毫秒级的几条 UPDATE,库里常态在飞行数为 0 —— 让开的窗极短。
18
- *
19
- * ── 射程边界(如实登记,不假称已解)──────────────────────────────────────────────────────────────
20
- * 两条边界,都写在明处:
21
- * ① **进程内**。这道闸只挡得住**运行这条弧的那个副本**;多副本部署里别的副本的 tick 照跑,那部分仍然
22
- * 只能靠 183 I1 的运维前提(以及弧内那些 rev-CAS:载荷改写撞上并发写会响亮抛、整体回滚)。
23
- * 更强的形(跨副本静默)需要一枚库上的 quiesce 租约 + 每条腿的取租,那是一次独立设计,不在本条射程。
24
- * ② **排空有界**。等不到就带着读数继续(见下),所以「本进程零腿在写被迁的表」是**通常**成立而不是
25
- * 恒成立 —— 超时那一次由 `onDrainTimeout` 响亮说出来,不许静默当成已排空。
26
- * 本闸因此是**纵深防御**,不是把 I1 变成机器保证。
27
- *
28
- * ── 形态 ────────────────────────────────────────────────────────────────────────────────────────
29
- * 模块级计数器而不是布尔:并发同参 POST 是本车明确支持的形(UNIQUE 仲裁),两条弧可以叠在一起,
30
- * 布尔会被先结束的那条**提前**放开闸。计数器 + `try/finally` ⇒ 抛错也一定归零。
31
- */
32
- /** 当前正在本进程里跑的收编弧条数。**只**由 {@link withAdoptionQuiesce} 增减。 */
33
1
  let inFlight = 0;
34
- /** 本进程此刻是否有收编弧在跑(维护 tick 的让路判据)。 */
35
2
  export function isAdoptionQuiescing() {
36
3
  return inFlight > 0;
37
4
  }
38
- /**
39
- * **已经起飞**的维护腿(codex 对抗复审 R1 [high],验真后修)。
40
- *
41
- * 🔴 病:第一版的闸只在 tick 的**入口**判一次。可维护 tick 的每条腿都是 `void <promise>` 异步起飞的
42
- * —— tick 回调本身立刻返回。于是这个次序完全可能:`t0` tick 起飞了审批收敛器(它按旧 owner 读出一批
43
- * 行,正等库回话)→ `t0+ε` 收编弧开始改 `approval_ask.owner` → 收敛器拿着旧读数回来 CAS,把一只本该
44
- * PARKED 的 ask 结算成 **DENIED**。也就是说:**这正是本条声称要消灭的那一形**,闸只是让它更难撞上。
45
- * 我原来的钉也测不出来 —— 它先开闸再推进时钟,探针又是立即完成的,反向次序根本没被覆盖。
46
- *
47
- * 🔴 修:闸不止「拦住下一轮」,还要**排空这一轮**。每条腿起飞时在这里登记,收编弧进库之前先等它们落地。
48
- *
49
- * ⏱ **有界等待**是硬的:维护腿里有会长时间挂住的(`worktreeReap` 的 `git worktree prune` 随 worktree
50
- * 数增长,它自己的重入守卫就是为此而设)。无界 await 会把「一条腿卡住」升级成「收编永远起不来」——
51
- * 比原病更坏。所以排空是**尽力而为 + 响亮**:超时就带着读数继续,让运维看得见「这次收编没能等到 N 条
52
- * 腿落地」,而不是既没等到也没人知道。
53
- */
54
5
  const airborneLegs = new Set();
55
- /**
56
- * 登记一条**刚起飞**的维护腿。调用方仍然 `void` 它自己的链(错误处置不变);本函数只借用它的落地时刻。
57
- *
58
- * `catch` 是必须的:登记的这一份引用绝不能变成第二个未处理拒绝源(腿自己的链已经有 guard 接住了)。
59
- */
60
6
  export function trackMaintenanceLeg(leg) {
61
7
  const tracked = leg.catch(() => undefined);
62
8
  airborneLegs.add(tracked);
63
9
  void tracked.finally(() => {
64
10
  airborneLegs.delete(tracked);
65
- // 集合空了才是「排空」——通知所有在等的收编弧。这是**唯一**挂在腿身上的 handler(每条腿恰一个,
66
- // 在它起飞时挂),排空等待方一律等下面那只**自己的** waiter,不再往腿上加第二个 handler。
67
11
  if (airborneLegs.size === 0) {
68
12
  for (const wake of drainWaiters)
69
13
  wake();
@@ -71,23 +15,10 @@ export function trackMaintenanceLeg(leg) {
71
15
  }
72
16
  });
73
17
  }
74
- /**
75
- * 正在等排空的收编弧(codex 对抗复审 R2 [medium],验真后修)。
76
- *
77
- * 🔴 病:第一版每次排空都现铸 `Promise.allSettled([...airborneLegs])`。**超时那一路**里,那只 allSettled
78
- * 永远不会兑现,而它已经把 handler 挂在了每一条挂死的腿上 —— 且 `clearTimeout` 清不掉这些 promise
79
- * reaction。于是「有一条腿永久挂住」这个**本来就是有界逃逸要处理的**场景,会让每一次 POST / 每一次 boot
80
- * 续跑再挂一批清不掉的闭包上去:逃逸机制自己变成了泄漏源。
81
- *
82
- * 🔴 修:等待方注册一只**自己的** waiter,超时时把它注销掉(`finally` 里 delete)。腿身上的 handler 恒为
83
- * 「起飞时挂的那一个」,与等待次数无关 ⇒ 重复超时不再增长任何东西。
84
- */
85
18
  const drainWaiters = new Set();
86
- /** 取证口(测试用):此刻有几只等待方挂着。生产不读 —— 它存在是为了让「重复超时不泄漏」可被断言。 */
87
19
  export function drainWaiterCountForTest() {
88
20
  return drainWaiters.size;
89
21
  }
90
- /** 有界排空(见 {@link airborneLegs} 的注)。`timeoutMs <= 0` ⇒ 不等,直接如实回报在飞数。 */
91
22
  export async function drainMaintenanceLegs(timeoutMs) {
92
23
  if (airborneLegs.size === 0)
93
24
  return { drained: true, pending: 0 };
@@ -101,7 +32,7 @@ export async function drainMaintenanceLegs(timeoutMs) {
101
32
  let timer;
102
33
  const deadline = new Promise((resolve) => {
103
34
  timer = setTimeout(() => resolve("timeout"), timeoutMs);
104
- timer.unref?.(); // 排空的等待绝不该把进程吊住
35
+ timer.unref?.();
105
36
  });
106
37
  try {
107
38
  const outcome = await Promise.race([drained, deadline]);
@@ -110,36 +41,22 @@ export async function drainMaintenanceLegs(timeoutMs) {
110
41
  finally {
111
42
  if (timer !== undefined)
112
43
  clearTimeout(timer);
113
- // 🔴 注销是本修的**全部内容**:超时之后这只 waiter 不许继续挂着(见 drainWaiters 的注)。
114
44
  drainWaiters.delete(wake);
115
45
  }
116
46
  }
117
- /** 排空的默认上界。取值理由:维护腿的常态是毫秒级的几条 SQL;5s 足够让它们全部落地,又不会把一次
118
- * 收编吊死在一条卡住的腿上(卡住的那一条由它自己的重入守卫与 throttled-warn 负责被看见)。 */
119
47
  export const ADOPTION_DRAIN_TIMEOUT_MS = 5_000;
120
- /**
121
- * 在闸内跑一条收编弧:**先关闸,再排空,最后才动数据**。
122
- *
123
- * 三步的次序是承重的 —— 先关闸(此后不会再有新 tick 起飞新腿),再等已经在飞的那些落地,然后才把
124
- * 控制权交给弧。反过来(先排空再关闸)之间那道窗里正好可以起飞新的一轮。
125
- *
126
- * `finally` 归零是硬的:抛出去的异常照常上抛(调用方的错误语义一个字不改),但闸绝不能因为一次失败
127
- * 就永久关着 —— 那会把一次收编失败升级成「这台副本从此不再做任何后台维护」。
128
- */
129
48
  export async function withAdoptionQuiesce(fn, opts) {
130
- inFlight += 1; // ① 关闸:此后的 tick 一律让路
49
+ inFlight += 1;
131
50
  try {
132
- // ② 排空这一轮已经起飞的腿(有界;超时如实响亮)。
133
51
  const reading = await drainMaintenanceLegs(opts?.drainTimeoutMs ?? ADOPTION_DRAIN_TIMEOUT_MS);
134
52
  if (!reading.drained)
135
53
  opts?.onDrainTimeout?.(reading);
136
- return await fn(); // ③ 动数据
54
+ return await fn();
137
55
  }
138
56
  finally {
139
57
  inFlight -= 1;
140
58
  }
141
59
  }
142
- /** 测试用的复位口(用例之间零串味)。生产路径**不调** —— 它会把别的在飞弧的计数一起抹掉。 */
143
60
  export function resetAdoptionQuiesceForTest() {
144
61
  inFlight = 0;
145
62
  airborneLegs.clear();
@@ -1,25 +1,3 @@
1
- /**
2
- * design/183 §3.2 根级状态机的 **form b 实施**(纯身份重绑;数据已在 SQL、承运腿为零)。
3
- *
4
- * 一条弧的全部判断都在这里,店(`adoption-log-sql.ts`)只负责语句,计划表(`plan.ts`)只负责「迁什么」。
5
- *
6
- * ── 弧 ──────────────────────────────────────────────────────────────────────────────────────────
7
- * ① 取弧锁(advisory;取不到 ⇒ **不等死也不假装成功**,回 `stalled` 回执)
8
- * ② 意图落库(insert-or-fetch 原子形;`from_principal` UNIQUE 仲裁并发) ⇒ phase 2
9
- * ③ 目的地冲突 preflight(**任何 UPDATE 之前**)+ 全部重绑腿(同一事务) ⇒ phase 3
10
- * ④ 配置账物化(server 够不到的部署配置逐项点名,`migrated:false`) ⇒ phase 4
11
- * ⑤ 承运腿:form b **恒零**(阶段位显式在场,给 form a 上车留挂点) ⇒ phase 5
12
- * ⑥ 永久终态:`immutableReport` 一次落库,此后逐字节恒同 ⇒ phase 6
13
- *
14
- * ── 崩溃续跑 ────────────────────────────────────────────────────────────────────────────────────
15
- * 每一步都是「读当前 phase → 做这一步 → CAS 推进」。任一步崩溃,下一次(重发同参 POST、或副本 boot 的
16
- * 在飞扫描)从库里的 phase 处接着跑;腿本身幂等(重跑改到同值 = 零行变化),所以「续跑」与「重做」在
17
- * 数据面等价。**绝不**因为「上次跑到一半」就拒绝续跑 —— 那会把一次网络抖动变成永久半迁移。
18
- *
19
- * ── 拒绝 ────────────────────────────────────────────────────────────────────────────────────────
20
- * 目的地冲突 ⇒ `rejected` 终态 + typed 409,**源/目的地两侧字节零变更**(preflight 在任何 UPDATE 之前;
21
- * 跨轴组合形的漏网由事务里的唯一键违例整体回滚兜住)。不做逐表 merge —— 183 未裁,拒比合安全且可重来。
22
- */
23
1
  import { randomUUID } from "node:crypto";
24
2
  import { ADOPTION_PHASE, adoptionLockName, } from "../plugins/adoption-log-sql.js";
25
3
  import { policyKeyFor } from "../plugins/session-policy-store-sql.js";
@@ -30,23 +8,13 @@ import { buildBlobScanSql, buildBlobWriteSql, buildConflictProbeSql, buildMemory
30
8
  import { AdoptionConfigsSchema, AdoptionLegsSchema, AdoptionReceiptSchema, AdoptionRejectDetailSchema, AdoptionReportSchema, } from "./wire.js";
31
9
  import { formatUserScope, encodeScopeSegment } from "@sema-agent/core";
32
10
  import { isDupKeyError } from "../plugins/sql-errors.js";
33
- /** MySQL `ER_DUP_ENTRY` / PG `unique_violation` —— 跨轴组合冲突的最后一道 fail-closed 门。
34
- * 判据属主 = `plugins/sql-errors.ts`(A-032 P1-①:本站点旧形只认 errno,只带 `code` 的驱动错误漏判)。 */
35
11
  const isDupKey = isDupKeyError;
36
12
  function msgOf(e) {
37
13
  return e instanceof Error ? e.message : String(e);
38
14
  }
39
- /**
40
- * 一根身份轴上的「旧值 → 新值」对。`verbatim` 恒一对;`memory-scope` 由库里 DISTINCT 扫出来
41
- * (user 盘 1 个 + 每个 project 盘各 1 个)——**扫而不猜**,理由见 `sql.ts` 的 LIKE 段。
42
- */
43
15
  async function resolveValuePairs(exec, dialect, leg, fromPrincipal, toPrincipal) {
44
16
  if (leg.encoding === "verbatim")
45
17
  return [buildVerbatimPair(fromPrincipal, toPrincipal)];
46
- // 规则桶键:恒一对,**算**出来而不是扫出来(原像里只有 principal,没有逐行变量)。
47
- // 🔴 这一支同时服务 preflight 的冲突探针与 `countResidual` 的残留计数 —— 少了它,那两处会拿**明文**
48
- // principal 去比 `owner_key`,而那个谓词在这张表上永远匹配不到任何一行:preflight 恒说「无冲突」、
49
- // 残留恒报 0(A-010.16 / A-010.22 点名的正是后者)。
50
18
  if (leg.encoding === "rule-owner-key") {
51
19
  return [
52
20
  {
@@ -76,13 +44,6 @@ async function resolveValuePairs(exec, dialect, leg, fromPrincipal, toPrincipal)
76
44
  }
77
45
  export function createAdoptionRunner(deps) {
78
46
  const { store, dialect, now } = deps;
79
- /**
80
- * phase 0 的**列宽实证**(codex R2-F2,亲核属实):路由那道 `checkAdoptionWidths` 是纯函数,只看得见
81
- * `user:<enc(to)>`;而 memory 迁移还会重写 `proj:`/`userproj:` 键并**保留原有的项目段后缀**。源侧
82
- * principal 短 + 项目段长的键可以正好装进 VARCHAR(190),换上更长的目的地段之后就越界 —— 那时行已经
83
- * 铸好、失败发生在 phase 3 的腿里,弧永远停在 INTENT,每次重发和每次 boot 续跑都确定性地再撞一次。
84
- * ⇒ 在**任何 UPDATE 之前**拿库里真实存在的 scope 值算一遍,越界就 typed 拒(可重来,不留半迁移)。
85
- */
86
47
  async function unrepresentableScopes(exec, fromPrincipal, toPrincipal) {
87
48
  const bad = [];
88
49
  const seen = new Set();
@@ -98,12 +59,9 @@ export function createAdoptionRunner(deps) {
98
59
  }
99
60
  return bad;
100
61
  }
101
- /** phase 0 探针:逐迁移表族查目的地侧是否已有与源侧重叠的逻辑键。**零 UPDATE**。 */
102
62
  async function preflight(exec, fromPrincipal, toPrincipal) {
103
63
  const hits = [];
104
64
  for (const leg of REBIND_LEGS) {
105
- // residualKey === undefined = 没有任何唯一键含这根轴 ⇒ 重写**结构上**不可能撞键。
106
- // 这不是「跳过没查」:它是判定表里逐条写死的判据(plan.ts 每条腿的 residualKey 行内注记了它的唯一键)。
107
65
  if (leg.residualKey === undefined)
108
66
  continue;
109
67
  const pairs = await resolveValuePairs(exec, dialect, leg, fromPrincipal, toPrincipal);
@@ -116,20 +74,11 @@ export function createAdoptionRunner(deps) {
116
74
  }
117
75
  return hits;
118
76
  }
119
- /**
120
- * `blob-rewrite` 腿:逐行改写 JSON 载荷里的身份字段。
121
- *
122
- * 🔴 **只改值等于旧身份的字段**(不盲改):载荷里可能有同名但语义不同的字段,而且盲改会让「重跑」
123
- * 变成一次真写。计数按**内容真变了**算 —— 重跑时新载荷与库里逐字节相同,行数报 0(诚实的幂等读数)。
124
- * 载荷解析失败 ⇒ **原样上抛**:一条读不动的真源行,静默跳过就等于宣布它已迁移(fail-closed)。
125
- */
126
77
  async function scanBlobRows(exec, leg, fromValue, toValue) {
127
78
  const blob = leg.blob;
128
79
  const cells = 1 + (blob.alsoMatch?.length ?? 0);
129
- // 两个桶:旧值(还没迁的行)+ 新值(投影迁了而载荷停在旧身份的「劈开」行)。见 buildBlobScanSql 的注。
130
80
  return (await exec.query(buildBlobScanSql(dialect, leg), [...Array.from({ length: cells }, () => fromValue), ...Array.from({ length: cells }, () => toValue)])).rows;
131
81
  }
132
- /** 载荷 JSON 的解析(两方言:node-pg 对 jsonb 自己 parse,mysql2 对 JSON 列同样可能已 parse)。 */
133
82
  function parseBlob(leg, row) {
134
83
  const raw = row[leg.blob.column];
135
84
  const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
@@ -150,23 +99,15 @@ export function createAdoptionRunner(deps) {
150
99
  for (const field of blob.fields)
151
100
  if (next[field] === fromValue)
152
101
  next[field] = toValue;
153
- // 载荷内嵌的版本号跟着列上的 `rev + 1` 走(见 BlobRewriteSpec.revInPayload 的注:两处是同一个
154
- // CAS 的读写两端,让它们各说一个版本 = 下一次正常写的守卫读到的基准是假的)。
155
102
  const bumped = { ...next };
156
103
  const scannedRev = Number(row[blob.revColumn]);
157
- // 只有真读到数才同步载荷里的版本号。读不到时**不编一个**(写进 NaN/null 比不写更坏):
158
- // 列上的 `rev = rev + 1` 仍然是权威的那一次推进,而 CAS 的 `AND rev = ?` 会用同一个值,
159
- // 所以读不到数的行根本写不进去(响亮抛),不存在「悄悄写了个坏版本号」这条路。
160
104
  if (blob.revInPayload !== undefined && Number.isFinite(scannedRev))
161
105
  bumped[blob.revInPayload] = scannedRev + 1;
162
106
  const after = JSON.stringify(bumped);
163
- // 判「有没有变」只看**身份字段**那一半:rev 是我们自己推的,拿它去判会让每次重跑都变成一次真写。
164
107
  if (JSON.stringify(next) === before)
165
- continue; // 已是新身份(重跑/本来就属新主)⇒ 语义无操作,不计数
108
+ continue;
166
109
  const res = await exec.query(buildBlobWriteSql(dialect, leg), [after, ...blob.pk.map((c) => row[c]), row[blob.revColumn]]);
167
110
  if (res.affected !== 1) {
168
- // rev-CAS 落空 = 读到写之间有一次正常业务写提交(codex R2-F1)。**响亮抛** ⇒ 整个事务回滚
169
- // ⇒ 两侧字节零变更。绝不「重读再盖」:那正是把别人的写吞掉的那条路。
170
111
  throw new Error(`adoption: concurrent write on ${leg.table} (leg ${leg.leg}, pk=${blob.pk.map((c) => String(row[c])).join("/")}) — ` +
171
112
  "its rev moved between read and write, so the payload rewrite was refused and the whole rebind rolled back. " +
172
113
  "Adoption requires the source identity to be quiesced (design/183 I1: stop the engine first), then retry.");
@@ -175,7 +116,6 @@ export function createAdoptionRunner(deps) {
175
116
  }
176
117
  return rows;
177
118
  }
178
- /** phase 3 的腿:全部在**一个事务**里(见 store.rebindTransaction 的顶注)。 */
179
119
  async function runLegs(exec, fromPrincipal, toPrincipal) {
180
120
  const out = [];
181
121
  for (const leg of REBIND_LEGS) {
@@ -197,7 +137,6 @@ export function createAdoptionRunner(deps) {
197
137
  continue;
198
138
  }
199
139
  if (leg.kind === "rule-owner-rekey") {
200
- // 一对键算一次(见 buildRuleOwnerRekeySql 的注),一条等值 UPDATE 走完;`principal` 明文列同改。
201
140
  const [pair] = await resolveValuePairs(exec, dialect, leg, fromPrincipal, toPrincipal);
202
141
  if (pair === undefined)
203
142
  throw new Error(`runLegs: leg ${leg.leg} resolved no identity pair`);
@@ -220,31 +159,18 @@ export function createAdoptionRunner(deps) {
220
159
  deployment: t.deployment,
221
160
  key: t.key,
222
161
  requiredValue: t.requiredValue.replace("%TO%", toPrincipal),
223
- // 🔴 恒 false:这些配置住在别的部署单元,收编动作够不到 —— 183 §3.3「改不到的配置,不假装改了」。
224
162
  migrated: false,
225
163
  ack: false,
226
164
  witnessedAtMs: null,
227
165
  }));
228
166
  }
229
- // 🔴 库行 → 内域一律过 schema(禁裸 `as`,见 wire.ts 的 AdoptionLegsSchema 段)。
230
167
  function parseLegs(row) {
231
168
  return AdoptionLegsSchema.parse(JSON.parse(row.legsJson));
232
169
  }
233
170
  function parseConfigs(row) {
234
171
  return AdoptionConfigsSchema.parse(JSON.parse(row.configsJson));
235
172
  }
236
- /**
237
- * 回执的铸造。终态行 ⇒ `immutableReport` 是**落库的那份字节**原样解回(逐字节恒同的来源);
238
- * 非终态行 ⇒ 同形投影(`status:"stalled"` 就是「这还不是冻结快照」的机读位)。
239
- * `current` 一律**现算**(183 §6:史实与现势各住各的字段)。
240
- */
241
- /**
242
- * 仍挂在旧身份名下的行数(跨全部腿求和,只读)。
243
- * 每次回执都现算 —— 它是**现势**,不是史实(183 §6 的两半分家)。
244
- */
245
173
  async function countResidual(exec, fromPrincipal, toPrincipal) {
246
- // 🔴 按**表**聚合,不按腿(codex R3-F4):字段名承诺的是「行数」,而一条 background_agent 行可以同时
247
- // 在 owner 轴、scope 轴、载荷三处带旧身份 —— 逐腿累加会把它报成 3。一张表一次判定,行只算一次。
248
174
  const byTable = new Map();
249
175
  for (const leg of REBIND_LEGS) {
250
176
  const slot = byTable.get(leg.table) ?? { match: new Set() };
@@ -252,19 +178,11 @@ export function createAdoptionRunner(deps) {
252
178
  slot.blob = leg;
253
179
  else {
254
180
  slot.match.add(leg.matchColumn);
255
- // 🔴 A-010.16(验真后修):这里此前只认 `memory-scope` 一种**非明文**编码,别的编码一律
256
- // 落到下面那条 `[{ from: fromPrincipal, … }]` 兜底 —— 也就是拿**明文 principal** 去比一根存
257
- // 派生键的列。对 `permission_rule.owner_key`(sha256 原像里带身份)那个谓词**永远匹配不到
258
- // 任何一行**,于是残留计数在这张表上结构性恒 0:回执一边说「adopted」、一边把一整桶还挂在
259
- // 旧身份名下的活规则报成「零残留」。判据改成**编码非 verbatim 即需解析**,新编码天然进对臂。
260
181
  if (leg.encoding !== "verbatim")
261
182
  slot.codedLeg = leg;
262
183
  }
263
184
  byTable.set(leg.table, slot);
264
185
  }
265
- // 🔴 一张表内**混编码**会让下面那条「一条 COUNT + 一个值对」的形悄悄算错(OR 起来的每一列都会被
266
- // 绑上同一个值,而它们的编码不同 ⇒ 至少一列的谓词恒假)。今天没有这样的表;真出现时必须响亮拦下,
267
- // 而不是报一个偏小的残留数(偏小 = 说「没落下」,正是本条要消灭的谎)。
268
186
  for (const [table, slot] of byTable) {
269
187
  const encodings = new Set(REBIND_LEGS.filter((l) => l.table === table && l.kind !== "blob-rewrite").map((l) => l.encoding));
270
188
  if (encodings.size > 1) {
@@ -275,7 +193,6 @@ export function createAdoptionRunner(deps) {
275
193
  let n = 0;
276
194
  for (const [table, slot] of byTable) {
277
195
  if (slot.blob !== undefined) {
278
- // 载荷表:一次并集扫描(旧值桶 + 新值桶),行只要**投影或载荷**任一处还带旧身份就计一次。
279
196
  const blob = slot.blob;
280
197
  const projection = [...slot.match, blob.matchColumn, ...(blob.blob?.alsoMatch ?? [])];
281
198
  for (const row of await scanBlobRows(exec, blob, fromPrincipal, toPrincipal)) {
@@ -286,8 +203,6 @@ export function createAdoptionRunner(deps) {
286
203
  }
287
204
  continue;
288
205
  }
289
- // 普通表:一条 COUNT,谓词是该表全部身份轴的并。memory 族的值对逐对各来一次(不同 scope 键 =
290
- // 不同的行集合,不会重复计数同一行)。
291
206
  const cols = [...slot.match];
292
207
  const pairs = slot.codedLeg !== undefined
293
208
  ? await resolveValuePairs(exec, dialect, slot.codedLeg, fromPrincipal, toPrincipal)
@@ -318,19 +233,15 @@ export function createAdoptionRunner(deps) {
318
233
  immutableReport: report,
319
234
  current: {
320
235
  outstandingConfigs: configs.filter((c) => c.witnessedAtMs === null).map((c) => `${c.deployment}:${c.key}`),
321
- // form b 零承运 ⇒ 无拒收行(结构位在场的理由见 wire.ts 顶注差异②)。
322
236
  quarantined: 0,
323
237
  residualSourceRows,
324
238
  },
325
239
  });
326
240
  }
327
- /** 拒绝详情的落库形 = `{detail, conflicts}` 的 JSON(一列存两件,免得再加两个只有拒才用的列)。 */
328
241
  function rejectedOutcome(row) {
329
242
  let detail = "destination conflict";
330
243
  let conflicts = [];
331
244
  if (row.rejectDetail !== null) {
332
- // safeParse:一条**坏掉的**拒绝详情不该把「这条弧被拒了」这件更重要的事实吞掉 —— 拒仍然是拒,
333
- // 只是失去了逐表族的明细(fail-closed 的方向:降级成粗理由,绝不降级成「成功」)。
334
245
  const parsed = AdoptionRejectDetailSchema.safeParse(JSON.parse(row.rejectDetail));
335
246
  if (parsed.success) {
336
247
  detail = parsed.data.detail;
@@ -340,7 +251,6 @@ export function createAdoptionRunner(deps) {
340
251
  const code = row.rejectCode === "adoption.destination_unrepresentable" ? "adoption.destination_unrepresentable" : "adoption.destination_conflict";
341
252
  return { kind: "rejected", code, adoptionId: row.adoptionId, detail, conflicts };
342
253
  }
343
- /** 从任意 phase 把弧推到终态(幂等、可重入)。**必须在弧锁下调用**。 */
344
254
  async function drive(start, sweep, conn) {
345
255
  let row = start;
346
256
  const reread = async () => {
@@ -352,7 +262,6 @@ export function createAdoptionRunner(deps) {
352
262
  if (row.state === "rejected")
353
263
  return rejectedOutcome(row);
354
264
  if (row.state === "in_flight" && row.phase === ADOPTION_PHASE.INTENT) {
355
- // ③a preflight —— 任何 UPDATE 之前,只读。先证「装得下」,再查「撞不撞」。
356
265
  const unrepresentable = await unrepresentableScopes(conn, row.fromPrincipal, row.toPrincipal);
357
266
  if (unrepresentable.length > 0) {
358
267
  const detail = JSON.stringify({
@@ -368,15 +277,12 @@ export function createAdoptionRunner(deps) {
368
277
  await store.finalizeRejected(row.adoptionId, "adoption.destination_conflict", detail, now(), conn);
369
278
  return rejectedOutcome(await reread());
370
279
  }
371
- // ③b 全部腿 + phase CAS,同一个事务(任何异常 ⇒ 整体回滚 ⇒ 两侧字节零变更)。
372
280
  try {
373
281
  await store.rebindOn(conn, row.adoptionId, ADOPTION_PHASE.INTENT, ADOPTION_PHASE.REBOUND, now(), (exec) => runLegs(exec, row.fromPrincipal, row.toPrincipal), (legs) => JSON.stringify(legs));
374
282
  }
375
283
  catch (err) {
376
284
  if (!isDupKey(dialect, err))
377
285
  throw err;
378
- // 跨轴组合形:preflight 按迁移**前**的库态逐轴算,两根轴的组合可能在中途撞唯一键。
379
- // 事务已回滚 ⇒ 字节零变更;这里把它落成与 preflight 同码的拒(fail-closed,不 fail-open)。
380
286
  const detail = JSON.stringify({
381
287
  detail: `unique-key violation while rebinding (cross-axis destination conflict): ${msgOf(err)}`,
382
288
  conflicts: [],
@@ -391,7 +297,6 @@ export function createAdoptionRunner(deps) {
391
297
  row = await reread();
392
298
  }
393
299
  if (row.state === "in_flight" && row.phase === ADOPTION_PHASE.CONFIGS) {
394
- // ⑤ 承运腿:form b 恒零(数据本来就在 SQL)。阶段位显式推进 —— 见本文件顶注的弧图。
395
300
  await store.advancePhase(row.adoptionId, ADOPTION_PHASE.CONFIGS, ADOPTION_PHASE.CARRIED, now(), conn);
396
301
  row = await reread();
397
302
  }
@@ -411,23 +316,10 @@ export function createAdoptionRunner(deps) {
411
316
  }
412
317
  if (row.state === "rejected")
413
318
  return rejectedOutcome(row);
414
- // ⑦ 终态**之后**的迟到行清扫(codex R1-F1 的可检测/可收敛半)。见 sweepLate 的注。
415
319
  if (sweep && row.state === "adopted")
416
320
  await sweepLate(row, conn);
417
321
  return { kind: "receipt", adoptionId: row.adoptionId, receipt: receiptOf(row, await residualOf(row, conn)) };
418
322
  }
419
- /**
420
- * 把收编**结束之后**又落到旧身份下的行搬过去。
421
- *
422
- * 🔴 为什么需要它:收编不给普通写者上栅栏 —— 183 I1 的口径是「引擎必须先停」,那是**运维前提**不是
423
- * 机器保证(§3.2 明写「单靠锁文件是运维假设」)。真实部署里配置随迁是**别的部署单元**的活
424
- * (`affectedDeploymentConfigs` 全部 `migrated:false`),所以在配置改完之前,BFF/cli 会继续按旧身份
425
- * 注入、继续往旧身份下写行。终态 + 幂等短路会让回执一直说「adopted」,而那些行永远没人搬。
426
- *
427
- * 处置:**每次 POST**(写动词)在终态之后再跑一遍腿(幂等,干净时零行变化)。这不是「二次收编」——
428
- * `immutableReport` 一个字节不动,变的只有 `current.residualSourceRows` 这个现势读数。
429
- * 清扫本身不改 phase(终态是永久的),失败也不把弧打回在飞:留痕 + 让残留读数继续报真话。
430
- */
431
323
  async function sweepLate(row, conn) {
432
324
  try {
433
325
  const legs = await store.sweepOn(conn, (exec) => runLegs(exec, row.fromPrincipal, row.toPrincipal));
@@ -442,8 +334,6 @@ export function createAdoptionRunner(deps) {
442
334
  }
443
335
  }
444
336
  catch (err) {
445
- // 清扫是收敛动作,不是终态的一部分:失败不该把一个已完成的收编变成错误应答。
446
- // 但也**绝不静默** —— 留痕 + 残留读数会继续报非零。
447
337
  deps.logger?.warn?.("adoption_late_sweep_failed", { adoptionId: row.adoptionId, error: msgOf(err) });
448
338
  }
449
339
  }
@@ -453,21 +343,11 @@ export function createAdoptionRunner(deps) {
453
343
  }
454
344
  catch (err) {
455
345
  deps.logger?.warn?.("adoption_residual_count_failed", { adoptionId: row.adoptionId, error: msgOf(err) });
456
- // 数不出来 ⇒ **不报 0**(那是「一切干净」的字面断言)。报 -1 不合 schema,故原样上抛给调用方
457
- // 之外的唯一诚实选择是把它当成「至少有一行没数到」。fail-closed:宁可报 1 让人去查。
458
346
  return 1;
459
347
  }
460
348
  }
461
- /**
462
- * 🔴 A-010.18:**唯一**的静默闸挂点。三条入口(`start` / `get` 的在飞续跑 / boot 的 `resumeInFlight`)
463
- * 全部经由本函数动数据,所以闸挂在这里一处就覆盖全部会写被迁表的路径 —— 挂在 `start` 上会漏掉后两条,
464
- * 而那两条恰恰是「没人按重试、副本自己续跑」的场景,与维护 tick 撞车的概率并不更低。
465
- * 闸的语义、为什么让开**整轮** tick、以及它只挡本副本这条射程边界,逐字见 `adoption/quiesce.ts`。
466
- */
467
349
  async function driveUnderLock(row, sweep) {
468
350
  return withAdoptionQuiesce(() => driveUnderLockInner(row, sweep), {
469
- // 排空没等到 ⇒ **响亮**(codex R1 [high] 的第二半):等不到不等于可以装作等到了。这条 warn 是
470
- // 运维判断「那次收编期间有没有腿在跟它抢同一批行」的唯一读数。
471
351
  onDrainTimeout: (reading) => deps.logger?.warn?.("adoption_maintenance_drain_timeout", {
472
352
  adoptionId: row.adoptionId,
473
353
  pendingLegs: reading.pending,
@@ -479,15 +359,9 @@ export function createAdoptionRunner(deps) {
479
359
  });
480
360
  }
481
361
  async function driveUnderLockInner(row, sweep) {
482
- const out = await store.withLock(adoptionLockName(row.fromPrincipal),
483
- // 🔴 **拿到锁之后重读**(codex R2-F3):`row` 是排队**之前**读到的快照,而排队期间别的副本可能已经
484
- // 把这条弧推完或拒掉了。拿旧快照进状态机 = 按一个已经不成立的世界做决定(实测可达的形:一条已被
485
- // 拒的弧被排队者推去真迁数据 —— 拒**故意**停在 phase INTENT,只判 phase 的 CAS 对它是满足的)。
486
- // 库级守卫(rebind CAS 里的 `state='in_flight'`)是第二道;这一道让常路根本走不到那儿。
487
- async (conn) => drive((await store.getById(row.adoptionId, conn)) ?? row, sweep, conn), deps.lockAttempts, deps.lockSleepMs);
362
+ const out = await store.withLock(adoptionLockName(row.fromPrincipal), async (conn) => drive((await store.getById(row.adoptionId, conn)) ?? row, sweep, conn), deps.lockAttempts, deps.lockSleepMs);
488
363
  if (out !== undefined)
489
364
  return out;
490
- // 锁被别的副本持着 ⇒ 那条弧正在被推进。回**现势**回执(`stalled`),不等死也不假装成功。
491
365
  const cur = (await store.getById(row.adoptionId)) ?? row;
492
366
  deps.logger?.info?.("adoption_arc_locked_elsewhere", { adoptionId: cur.adoptionId, phase: cur.phase });
493
367
  if (cur.state === "rejected")
@@ -496,10 +370,8 @@ export function createAdoptionRunner(deps) {
496
370
  }
497
371
  return {
498
372
  async start(fromPrincipal, toPrincipal) {
499
- // 幂等身份 = DB 约束:并发同参 POST 由 `from_principal` UNIQUE 仲裁,禁 SELECT-then-INSERT。
500
373
  const { row } = await store.ensureAdoption({ adoptionId: randomUUID(), fromPrincipal, toPrincipal, nowMs: now() });
501
374
  if (row.toPrincipal !== toPrincipal) {
502
- // 同 from 异 to = 183 D7「二次收编/多租转让」,非本协议射程 ⇒ typed 409。
503
375
  return { kind: "source_already_bound", adoptionId: row.adoptionId, fromPrincipal, boundTo: row.toPrincipal };
504
376
  }
505
377
  return driveUnderLock(row, true);
@@ -510,11 +382,8 @@ export function createAdoptionRunner(deps) {
510
382
  return null;
511
383
  if (row.state === "rejected")
512
384
  return rejectedOutcome(row);
513
- // GET 是读动词:只报残留读数,**不**顺带清扫(清扫是 POST 的活)。
514
385
  if (row.state === "adopted")
515
386
  return { kind: "receipt", adoptionId, receipt: receiptOf(row, await residualOf(row)) };
516
- // 在飞行读面**顺手续跑**:一次崩溃之后没人再发 POST 的部署,读一次状态就把弧推完
517
- // (与 boot 扫描同一条 drive,不是第二套状态机)。
518
387
  return driveUnderLock(row, false);
519
388
  },
520
389
  async resumeInFlight() {
@@ -524,26 +393,20 @@ export function createAdoptionRunner(deps) {
524
393
  const rejected = [];
525
394
  const failed = [];
526
395
  for (const row of rows) {
527
- // 🔴 **逐弧兜住异常**(本车自测逮到):一条推不动的弧(库上有一行坏数据、一次并发写撞了 CAS…)
528
- // 若把整趟扫描一起掀翻,**别的**弧就再也没人续跑了 —— 一条坏行绑架了全部恢复。
529
- // 逐行 fail-loud(进 `failed` + 留痕),扫描继续。
530
396
  let out;
531
397
  try {
532
- out = await driveUnderLock(row, true); // boot 是写得起的场合:顺带把迟到行扫干净
398
+ out = await driveUnderLock(row, true);
533
399
  }
534
400
  catch (err) {
535
401
  failed.push(row.adoptionId);
536
402
  deps.logger?.warn?.("adoption_resume_failed", { adoptionId: row.adoptionId, error: msgOf(err) });
537
403
  continue;
538
404
  }
539
- // 闭集穷举:结局多一种 ⇒ 这里编译红,不会静默落进某个默认臂。
540
405
  switch (out.kind) {
541
406
  case "rejected":
542
407
  rejected.push(out.adoptionId);
543
408
  break;
544
409
  case "source_already_bound":
545
- // 到不了:`drive` 只从库里既有行推进,不铸新行 —— 这一支是 `start()` 的入口判决。
546
- // 留一条**响亮**的记账而不是 `default: break`(静默吞会把一次真的状态机漂移变成看不见的丢弧)。
547
410
  stalled.push(out.adoptionId);
548
411
  deps.logger?.warn?.("adoption_resume_unexpected_outcome", { adoptionId: out.adoptionId, kind: out.kind });
549
412
  break;