@sema-agent/server 1.323.0 → 2.0.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 (539) hide show
  1. package/dist/approval-hmac.d.ts +40 -0
  2. package/dist/approval-hmac.js +67 -0
  3. package/dist/approval.d.ts +31 -0
  4. package/dist/approval.js +72 -4
  5. package/dist/audit.d.ts +26 -0
  6. package/dist/audit.js +40 -1
  7. package/dist/auth-bridge.d.ts +10 -0
  8. package/dist/auth-bridge.js +47 -4
  9. package/dist/auth-keys.d.ts +19 -0
  10. package/dist/auth-keys.js +13 -0
  11. package/dist/bake-runner/main.d.ts +5 -0
  12. package/dist/bake-runner/main.js +43 -4
  13. package/dist/bake-runner/protocol.d.ts +57 -0
  14. package/dist/bake-runner/protocol.js +58 -3
  15. package/dist/bake-runner/runner.d.ts +54 -0
  16. package/dist/bake-runner/runner.js +87 -5
  17. package/dist/bench/l8/artifact.d.ts +63 -0
  18. package/dist/bench/l8/artifact.js +61 -2
  19. package/dist/bench/l8/escape.d.ts +36 -0
  20. package/dist/bench/l8/escape.js +25 -0
  21. package/dist/bench/l8/index.d.ts +14 -0
  22. package/dist/bench/l8/index.js +14 -0
  23. package/dist/bench/l8/probes.d.ts +74 -0
  24. package/dist/bench/l8/probes.js +60 -0
  25. package/dist/bench/l8/run-probes.d.ts +43 -0
  26. package/dist/bench/l8/run-probes.js +45 -2
  27. package/dist/bench/s1/arms.d.ts +126 -0
  28. package/dist/bench/s1/arms.js +188 -13
  29. package/dist/bench/s1/live-deps.d.ts +66 -0
  30. package/dist/bench/s1/live-deps.js +249 -11
  31. package/dist/bench/s1/oracle.d.ts +49 -0
  32. package/dist/bench/s1/oracle.js +36 -1
  33. package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
  34. package/dist/bench/s1/repair-oracle-adapter.js +30 -0
  35. package/dist/bench/s1/reviewer.d.ts +38 -0
  36. package/dist/bench/s1/reviewer.js +34 -0
  37. package/dist/bench/s1/row.d.ts +77 -0
  38. package/dist/bench/s1/row.js +62 -0
  39. package/dist/bench/s1/run-firm.d.ts +47 -0
  40. package/dist/bench/s1/run-firm.js +70 -4
  41. package/dist/bench/s1/runner-ctx.d.ts +71 -0
  42. package/dist/bench/s1/runner-ctx.js +36 -0
  43. package/dist/bench/s1/tasks.d.ts +108 -0
  44. package/dist/bench/s1/tasks.js +115 -0
  45. package/dist/boot-reclaim.d.ts +19 -0
  46. package/dist/boot-reclaim.js +20 -0
  47. package/dist/brain.d.ts +25 -0
  48. package/dist/brain.js +61 -1
  49. package/dist/budget.d.ts +66 -0
  50. package/dist/budget.js +128 -4
  51. package/dist/capabilities/builtin-tools.d.ts +3 -0
  52. package/dist/capabilities/builtin-tools.js +3 -0
  53. package/dist/capabilities/center-plugins.d.ts +17 -1
  54. package/dist/capabilities/center-plugins.js +45 -4
  55. package/dist/capabilities/center-prompts.d.ts +46 -0
  56. package/dist/capabilities/center-prompts.js +57 -3
  57. package/dist/capabilities/code-review-council.d.ts +28 -0
  58. package/dist/capabilities/code-review-council.js +40 -7
  59. package/dist/capabilities/collab-wire.d.ts +2 -0
  60. package/dist/capabilities/collab-workflows.d.ts +7 -0
  61. package/dist/capabilities/collab-workflows.js +44 -1
  62. package/dist/capabilities/oa-tools.d.ts +6 -0
  63. package/dist/capabilities/oa-tools.js +10 -0
  64. package/dist/capabilities/prompt.d.ts +14 -0
  65. package/dist/capabilities/prompt.js +14 -0
  66. package/dist/capabilities/prompts/code-review.d.ts +14 -0
  67. package/dist/capabilities/prompts/code-review.js +14 -0
  68. package/dist/capabilities/prompts/identity.d.ts +6 -0
  69. package/dist/capabilities/prompts/identity.js +6 -0
  70. package/dist/capabilities/prompts/team.d.ts +4 -0
  71. package/dist/capabilities/prompts/team.js +4 -0
  72. package/dist/capabilities/repo-tools.d.ts +11 -0
  73. package/dist/capabilities/repo-tools.js +8 -0
  74. package/dist/capabilities/sandbox-file-send.d.ts +95 -0
  75. package/dist/capabilities/sandbox-file-send.js +91 -3
  76. package/dist/capabilities/scenarios.d.ts +130 -0
  77. package/dist/capabilities/scenarios.js +180 -7
  78. package/dist/capabilities/select-environment-tool.d.ts +7 -0
  79. package/dist/capabilities/select-environment-tool.js +30 -0
  80. package/dist/capabilities/send-user-file-tool.d.ts +33 -0
  81. package/dist/capabilities/send-user-file-tool.js +65 -1
  82. package/dist/capabilities/skills.d.ts +29 -0
  83. package/dist/capabilities/skills.js +13 -2
  84. package/dist/capabilities/team.d.ts +22 -0
  85. package/dist/capabilities/team.js +22 -2
  86. package/dist/capabilities/tool-defer.d.ts +26 -0
  87. package/dist/capabilities/tool-defer.js +3 -0
  88. package/dist/config-center/apply-effective.d.ts +65 -0
  89. package/dist/config-center/apply-effective.js +204 -12
  90. package/dist/config-center/http-client.d.ts +34 -0
  91. package/dist/config-center/http-client.js +68 -0
  92. package/dist/config-center/restart-signal.d.ts +22 -0
  93. package/dist/config-center/restart-signal.js +39 -0
  94. package/dist/config-center/skills-mcp.d.ts +20 -1
  95. package/dist/config-center/skills-mcp.js +42 -7
  96. package/dist/config-center/types.d.ts +92 -1
  97. package/dist/config-lkg.d.ts +12 -0
  98. package/dist/config-lkg.js +41 -0
  99. package/dist/config-provider.d.ts +78 -0
  100. package/dist/config-provider.js +165 -2
  101. package/dist/config-types.d.ts +546 -0
  102. package/dist/config.d.ts +64 -0
  103. package/dist/config.js +455 -60
  104. package/dist/degenerate-instrument.d.ts +38 -0
  105. package/dist/degenerate-instrument.js +55 -3
  106. package/dist/elicitation.d.ts +39 -0
  107. package/dist/elicitation.js +87 -3
  108. package/dist/env-facts.d.ts +61 -0
  109. package/dist/env-facts.js +73 -7
  110. package/dist/fleet/fleet-bus.d.ts +185 -0
  111. package/dist/fleet/fleet-bus.js +333 -27
  112. package/dist/fleet/subagent-tail-bus.d.ts +11 -0
  113. package/dist/fleet/subagent-tail-bus.js +33 -2
  114. package/dist/fleet-client.d.ts +38 -0
  115. package/dist/fleet-client.js +44 -9
  116. package/dist/fleet-lease.d.ts +46 -0
  117. package/dist/fleet-lease.js +73 -5
  118. package/dist/hooks/branch-transcript.d.ts +8 -0
  119. package/dist/hooks/branch-transcript.js +33 -0
  120. package/dist/hooks/cc-stop-prompt.d.ts +42 -0
  121. package/dist/hooks/cc-stop-prompt.js +43 -1
  122. package/dist/hooks/hook-llm.d.ts +15 -0
  123. package/dist/hooks/hook-llm.js +47 -0
  124. package/dist/hooks/hook-runner.d.ts +89 -0
  125. package/dist/hooks/hook-runner.js +327 -18
  126. package/dist/http/idempotency.d.ts +31 -0
  127. package/dist/http/idempotency.js +34 -0
  128. package/dist/http/principal-gate.d.ts +17 -0
  129. package/dist/http/principal-gate.js +35 -3
  130. package/dist/http/route-ctx.d.ts +36 -0
  131. package/dist/http/routes/approvals-assistant.d.ts +25 -0
  132. package/dist/http/routes/approvals-assistant.js +253 -23
  133. package/dist/http/routes/attachments.js +22 -4
  134. package/dist/http/routes/capabilities.d.ts +7 -0
  135. package/dist/http/routes/capabilities.js +202 -1
  136. package/dist/http/routes/fleet.d.ts +6 -0
  137. package/dist/http/routes/fleet.js +94 -5
  138. package/dist/http/routes/images.d.ts +17 -0
  139. package/dist/http/routes/images.js +222 -14
  140. package/dist/http/routes/leader.d.ts +7 -0
  141. package/dist/http/routes/leader.js +10 -0
  142. package/dist/http/routes/memory-policy.d.ts +6 -0
  143. package/dist/http/routes/memory-policy.js +38 -6
  144. package/dist/http/routes/notify-wake.d.ts +10 -0
  145. package/dist/http/routes/notify-wake.js +19 -3
  146. package/dist/http/routes/observability.d.ts +6 -0
  147. package/dist/http/routes/observability.js +19 -2
  148. package/dist/http/routes/runs.d.ts +15 -0
  149. package/dist/http/routes/runs.js +464 -41
  150. package/dist/http/routes/session-sync.d.ts +15 -0
  151. package/dist/http/routes/session-sync.js +229 -19
  152. package/dist/http/routes/sessions-list.d.ts +6 -0
  153. package/dist/http/routes/sessions-list.js +35 -4
  154. package/dist/http/routes/sessions.d.ts +14 -0
  155. package/dist/http/routes/sessions.js +176 -31
  156. package/dist/http/routes/side-query.d.ts +8 -0
  157. package/dist/http/routes/side-query.js +26 -1
  158. package/dist/http/routes/tasks.d.ts +18 -0
  159. package/dist/http/routes/tasks.js +501 -38
  160. package/dist/http/routes/trace-usage.d.ts +10 -0
  161. package/dist/http/routes/trace-usage.js +66 -14
  162. package/dist/http/routes/workflows.d.ts +12 -0
  163. package/dist/http/routes/workflows.js +180 -19
  164. package/dist/http/run-meta.d.ts +11 -0
  165. package/dist/http/run-meta.js +6 -0
  166. package/dist/http/send.d.ts +19 -0
  167. package/dist/http/send.js +11 -0
  168. package/dist/http/server.d.ts +255 -0
  169. package/dist/http/server.js +921 -75
  170. package/dist/http/sse-log.d.ts +15 -0
  171. package/dist/http/sse-log.js +16 -0
  172. package/dist/http/tar.d.ts +9 -0
  173. package/dist/http/tar.js +21 -5
  174. package/dist/http/wire-gate.d.ts +9 -0
  175. package/dist/http/wire-gate.js +9 -0
  176. package/dist/http/wire-types.d.ts +187 -0
  177. package/dist/http/workspace-content.d.ts +8 -0
  178. package/dist/http/workspace-content.js +10 -0
  179. package/dist/images/bake-validate.d.ts +63 -0
  180. package/dist/images/bake-validate.js +71 -1
  181. package/dist/images/manifest.d.ts +19 -0
  182. package/dist/images/manifest.js +6 -1
  183. package/dist/index.d.ts +1 -5
  184. package/dist/index.js +10 -5
  185. package/dist/key-resolver.d.ts +23 -0
  186. package/dist/key-resolver.js +27 -2
  187. package/dist/leader/diffout.d.ts +23 -0
  188. package/dist/leader/diffout.js +8 -1
  189. package/dist/leader/diffup.d.ts +32 -0
  190. package/dist/leader/diffup.js +43 -0
  191. package/dist/leader/endpoint.d.ts +20 -0
  192. package/dist/leader/endpoint.js +25 -2
  193. package/dist/leader/fanout.d.ts +60 -0
  194. package/dist/leader/fanout.js +29 -3
  195. package/dist/leader/grader-env-factory.d.ts +74 -0
  196. package/dist/leader/grader-env-factory.js +62 -3
  197. package/dist/leader/leader.d.ts +97 -0
  198. package/dist/leader/leader.js +151 -4
  199. package/dist/leader/merge.d.ts +94 -0
  200. package/dist/leader/merge.js +112 -9
  201. package/dist/leader/planner.d.ts +52 -0
  202. package/dist/leader/planner.js +47 -3
  203. package/dist/leader/repair-oracle.d.ts +61 -0
  204. package/dist/leader/repair-oracle.js +59 -1
  205. package/dist/leader/repair-wire.d.ts +78 -0
  206. package/dist/leader/repair-wire.js +79 -2
  207. package/dist/leader/wire.d.ts +79 -0
  208. package/dist/leader/wire.js +255 -8
  209. package/dist/lsp/e2b-bridge.d.ts +16 -0
  210. package/dist/lsp/e2b-bridge.js +32 -4
  211. package/dist/lsp/e2b-manager.d.ts +10 -0
  212. package/dist/lsp/e2b-manager.js +18 -2
  213. package/dist/lsp/lsp-frames.d.ts +12 -0
  214. package/dist/lsp/lsp-frames.js +12 -0
  215. package/dist/lsp/manager.d.ts +12 -0
  216. package/dist/lsp/manager.js +35 -2
  217. package/dist/lsp/types.d.ts +10 -0
  218. package/dist/lsp/ws-transport.d.ts +12 -0
  219. package/dist/lsp/ws-transport.js +53 -5
  220. package/dist/lsp-evict.d.ts +14 -0
  221. package/dist/lsp-evict.js +15 -1
  222. package/dist/main.js +2132 -96
  223. package/dist/memory-export.d.ts +1 -0
  224. package/dist/memory-export.js +4 -0
  225. package/dist/memory-scope.d.ts +49 -0
  226. package/dist/memory-scope.js +87 -4
  227. package/dist/memory-sync-client.d.ts +24 -0
  228. package/dist/memory-sync-client.js +41 -2
  229. package/dist/memory-sync.d.ts +23 -0
  230. package/dist/memory-sync.js +70 -1
  231. package/dist/model-select.d.ts +39 -0
  232. package/dist/model-select.js +42 -1
  233. package/dist/observability/cost-quota.d.ts +20 -0
  234. package/dist/observability/cost-quota.js +4 -0
  235. package/dist/observability/cost-taxonomy.d.ts +45 -0
  236. package/dist/observability/cost-taxonomy.js +34 -0
  237. package/dist/observability/logger.d.ts +1 -0
  238. package/dist/observability/logger.js +6 -0
  239. package/dist/observability/metrics.d.ts +13 -0
  240. package/dist/observability/metrics.js +63 -0
  241. package/dist/observability/otel-exporter.d.ts +9 -0
  242. package/dist/observability/otel-exporter.js +11 -3
  243. package/dist/observability/principal-context.d.ts +9 -0
  244. package/dist/observability/principal-context.js +9 -0
  245. package/dist/observability/prompt-manifest.d.ts +37 -0
  246. package/dist/observability/prompt-manifest.js +35 -1
  247. package/dist/observability/rate-limit.d.ts +10 -0
  248. package/dist/observability/rate-limit.js +1 -0
  249. package/dist/observability/tool-trace.d.ts +24 -0
  250. package/dist/observability/tool-trace.js +41 -1
  251. package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
  252. package/dist/orchestration/hardened-vm-runner.js +119 -4
  253. package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
  254. package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
  255. package/dist/orchestration/hardened-vm-worker.js +27 -0
  256. package/dist/orchestration/subagent-steer.d.ts +44 -0
  257. package/dist/orchestration/subagent-steer.js +45 -1
  258. package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
  259. package/dist/orchestration/workflow-agent-steer.js +80 -1
  260. package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
  261. package/dist/orchestration/workflow-completion-inbox.js +283 -32
  262. package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
  263. package/dist/orchestration/workflow-notify-journal.js +187 -13
  264. package/dist/parked-decide.d.ts +61 -0
  265. package/dist/parked-decide.js +58 -1
  266. package/dist/per-task-image.d.ts +54 -0
  267. package/dist/per-task-image.js +41 -0
  268. package/dist/plan-cache-probe.d.ts +35 -0
  269. package/dist/plan-cache-probe.js +18 -3
  270. package/dist/plugins/approval-exemption-store.d.ts +7 -0
  271. package/dist/plugins/approval-exemption-store.js +28 -2
  272. package/dist/plugins/approval-store-sql.d.ts +64 -0
  273. package/dist/plugins/approval-store-sql.js +32 -0
  274. package/dist/plugins/background-agent-store-sql.d.ts +55 -0
  275. package/dist/plugins/background-agent-store-sql.js +91 -4
  276. package/dist/plugins/background-shell-support.d.ts +98 -0
  277. package/dist/plugins/background-shell-support.js +117 -13
  278. package/dist/plugins/blob-backend.d.ts +104 -0
  279. package/dist/plugins/blob-backend.js +163 -6
  280. package/dist/plugins/breaker-state-sql.d.ts +65 -1
  281. package/dist/plugins/breaker-state-sql.js +46 -8
  282. package/dist/plugins/caching-session-store.d.ts +42 -0
  283. package/dist/plugins/caching-session-store.js +63 -3
  284. package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
  285. package/dist/plugins/checkpoint-store-sql.js +239 -11
  286. package/dist/plugins/file-outcome-sink.d.ts +4 -0
  287. package/dist/plugins/file-outcome-sink.js +9 -0
  288. package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
  289. package/dist/plugins/file-resume-anchor-store.js +43 -4
  290. package/dist/plugins/file-run-store.d.ts +90 -0
  291. package/dist/plugins/file-run-store.js +145 -13
  292. package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
  293. package/dist/plugins/file-snapshot-store-sql.js +178 -10
  294. package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
  295. package/dist/plugins/file-workflow-journal-store.js +10 -0
  296. package/dist/plugins/fork-routing-session-store.d.ts +47 -0
  297. package/dist/plugins/fork-routing-session-store.js +51 -3
  298. package/dist/plugins/host-platform.d.ts +69 -0
  299. package/dist/plugins/host-platform.js +81 -2
  300. package/dist/plugins/image-bake-store-sql.d.ts +136 -1
  301. package/dist/plugins/image-bake-store-sql.js +241 -7
  302. package/dist/plugins/image-index-sql.d.ts +46 -0
  303. package/dist/plugins/image-index-sql.js +113 -4
  304. package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
  305. package/dist/plugins/k8s-bg-scripts.js +88 -4
  306. package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
  307. package/dist/plugins/k8s-exec-protocol.js +41 -0
  308. package/dist/plugins/local-checkpoint-store.d.ts +40 -0
  309. package/dist/plugins/local-checkpoint-store.js +86 -7
  310. package/dist/plugins/local-session-store.d.ts +157 -0
  311. package/dist/plugins/local-session-store.js +289 -34
  312. package/dist/plugins/local-task-attachment-store.js +14 -2
  313. package/dist/plugins/mailbox-store-sql.d.ts +38 -0
  314. package/dist/plugins/mailbox-store-sql.js +39 -6
  315. package/dist/plugins/memory-engine-pg.d.ts +30 -0
  316. package/dist/plugins/memory-engine-pg.js +122 -4
  317. package/dist/plugins/memory-engine-tidb.d.ts +26 -0
  318. package/dist/plugins/memory-engine-tidb.js +115 -5
  319. package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
  320. package/dist/plugins/memory-engine-vector-util.js +5 -0
  321. package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
  322. package/dist/plugins/memory-resume-anchor-store.js +17 -0
  323. package/dist/plugins/memory-run-store.d.ts +63 -0
  324. package/dist/plugins/memory-run-store.js +73 -8
  325. package/dist/plugins/memory-session-policy-store.d.ts +17 -0
  326. package/dist/plugins/memory-session-policy-store.js +17 -0
  327. package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
  328. package/dist/plugins/memory-sync-store-pg.js +45 -2
  329. package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
  330. package/dist/plugins/memory-sync-store-tidb.js +31 -1
  331. package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
  332. package/dist/plugins/outcome-ledger-sql.js +97 -3
  333. package/dist/plugins/pg-approval-store.d.ts +7 -0
  334. package/dist/plugins/pg-approval-store.js +7 -0
  335. package/dist/plugins/pg-breaker-state.d.ts +6 -0
  336. package/dist/plugins/pg-breaker-state.js +6 -0
  337. package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
  338. package/dist/plugins/pg-checkpoint-store.js +8 -0
  339. package/dist/plugins/pg-cost-quota.d.ts +17 -0
  340. package/dist/plugins/pg-cost-quota.js +6 -0
  341. package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
  342. package/dist/plugins/pg-file-snapshot-store.js +6 -0
  343. package/dist/plugins/pg-image-bake.d.ts +9 -0
  344. package/dist/plugins/pg-image-bake.js +9 -0
  345. package/dist/plugins/pg-image-index.d.ts +9 -0
  346. package/dist/plugins/pg-image-index.js +9 -0
  347. package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
  348. package/dist/plugins/pg-outcome-ledger.js +9 -0
  349. package/dist/plugins/pg-pool.d.ts +50 -0
  350. package/dist/plugins/pg-pool.js +78 -0
  351. package/dist/plugins/pg-query.d.ts +11 -0
  352. package/dist/plugins/pg-rate-limiter.d.ts +23 -0
  353. package/dist/plugins/pg-rate-limiter.js +12 -2
  354. package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
  355. package/dist/plugins/pg-resume-anchor-store.js +5 -0
  356. package/dist/plugins/pg-run-store.d.ts +7 -0
  357. package/dist/plugins/pg-run-store.js +7 -0
  358. package/dist/plugins/pg-safe-json.d.ts +13 -0
  359. package/dist/plugins/pg-safe-json.js +40 -4
  360. package/dist/plugins/pg-session-policy-store.d.ts +5 -0
  361. package/dist/plugins/pg-session-policy-store.js +5 -0
  362. package/dist/plugins/pg-session-storage.d.ts +72 -0
  363. package/dist/plugins/pg-session-storage.js +152 -20
  364. package/dist/plugins/pg-session-store.d.ts +10 -0
  365. package/dist/plugins/pg-session-store.js +10 -0
  366. package/dist/plugins/pg-tool-result-store.d.ts +7 -0
  367. package/dist/plugins/pg-tool-result-store.js +7 -0
  368. package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
  369. package/dist/plugins/pg-workflow-journal-store.js +7 -0
  370. package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
  371. package/dist/plugins/pg-workflow-run-store.js +7 -0
  372. package/dist/plugins/posix-shell-fs.d.ts +3 -0
  373. package/dist/plugins/posix-shell-fs.js +31 -1
  374. package/dist/plugins/remote-env-adb.d.ts +25 -0
  375. package/dist/plugins/remote-env-adb.js +95 -12
  376. package/dist/plugins/remote-env-e2b.d.ts +146 -0
  377. package/dist/plugins/remote-env-e2b.js +371 -40
  378. package/dist/plugins/remote-env-file-error.d.ts +34 -0
  379. package/dist/plugins/remote-env-file-error.js +37 -0
  380. package/dist/plugins/remote-env-host.d.ts +151 -0
  381. package/dist/plugins/remote-env-host.js +468 -70
  382. package/dist/plugins/remote-env-k8s.d.ts +135 -0
  383. package/dist/plugins/remote-env-k8s.js +318 -32
  384. package/dist/plugins/remote-env-local-docker.d.ts +65 -0
  385. package/dist/plugins/remote-env-local-docker.js +149 -24
  386. package/dist/plugins/remote-env-ssh.d.ts +35 -0
  387. package/dist/plugins/remote-env-ssh.js +117 -20
  388. package/dist/plugins/remote-scratchpad.d.ts +38 -0
  389. package/dist/plugins/remote-scratchpad.js +32 -2
  390. package/dist/plugins/remote-shell.d.ts +22 -0
  391. package/dist/plugins/remote-shell.js +27 -1
  392. package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
  393. package/dist/plugins/resume-anchor-store-sql.js +11 -0
  394. package/dist/plugins/roster-store-sql.d.ts +27 -0
  395. package/dist/plugins/roster-store-sql.js +54 -0
  396. package/dist/plugins/run-store-sql.d.ts +194 -0
  397. package/dist/plugins/run-store-sql.js +210 -9
  398. package/dist/plugins/s3-presign.d.ts +20 -0
  399. package/dist/plugins/s3-presign.js +49 -2
  400. package/dist/plugins/scheduler-support.d.ts +12 -0
  401. package/dist/plugins/scheduler-support.js +75 -3
  402. package/dist/plugins/send-file-ledger.d.ts +15 -0
  403. package/dist/plugins/send-file-ledger.js +53 -4
  404. package/dist/plugins/send-user-file.d.ts +77 -0
  405. package/dist/plugins/send-user-file.js +92 -4
  406. package/dist/plugins/session-policy-store-sql.d.ts +15 -0
  407. package/dist/plugins/session-policy-store-sql.js +75 -3
  408. package/dist/plugins/session-store.d.ts +45 -0
  409. package/dist/plugins/session-store.js +51 -0
  410. package/dist/plugins/sql-driver.d.ts +77 -0
  411. package/dist/plugins/sql-driver.js +4 -0
  412. package/dist/plugins/sql-escape.d.ts +8 -0
  413. package/dist/plugins/sql-escape.js +8 -0
  414. package/dist/plugins/sql-row-helpers.d.ts +25 -0
  415. package/dist/plugins/sql-row-helpers.js +25 -0
  416. package/dist/plugins/store-backend.d.ts +103 -0
  417. package/dist/plugins/store-backend.js +120 -27
  418. package/dist/plugins/store-contracts.d.ts +62 -0
  419. package/dist/plugins/store-contracts.js +3 -0
  420. package/dist/plugins/task-attachment-store.d.ts +60 -0
  421. package/dist/plugins/task-attachment-store.js +49 -3
  422. package/dist/plugins/task-list-store-sql.d.ts +25 -0
  423. package/dist/plugins/task-list-store-sql.js +21 -0
  424. package/dist/plugins/tidb-approval-store.d.ts +6 -0
  425. package/dist/plugins/tidb-approval-store.js +6 -0
  426. package/dist/plugins/tidb-breaker-state.d.ts +5 -0
  427. package/dist/plugins/tidb-breaker-state.js +5 -0
  428. package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
  429. package/dist/plugins/tidb-checkpoint-store.js +7 -0
  430. package/dist/plugins/tidb-cost-quota.d.ts +6 -0
  431. package/dist/plugins/tidb-cost-quota.js +1 -0
  432. package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
  433. package/dist/plugins/tidb-file-snapshot-store.js +6 -0
  434. package/dist/plugins/tidb-image-bake.d.ts +8 -0
  435. package/dist/plugins/tidb-image-bake.js +8 -0
  436. package/dist/plugins/tidb-image-index.d.ts +8 -0
  437. package/dist/plugins/tidb-image-index.js +8 -0
  438. package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
  439. package/dist/plugins/tidb-outcome-ledger.js +10 -0
  440. package/dist/plugins/tidb-pool.d.ts +56 -0
  441. package/dist/plugins/tidb-pool.js +181 -1
  442. package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
  443. package/dist/plugins/tidb-rate-limiter.js +5 -2
  444. package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
  445. package/dist/plugins/tidb-resume-anchor-store.js +5 -0
  446. package/dist/plugins/tidb-run-store.d.ts +7 -0
  447. package/dist/plugins/tidb-run-store.js +7 -0
  448. package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
  449. package/dist/plugins/tidb-session-policy-store.js +5 -0
  450. package/dist/plugins/tidb-session-storage.d.ts +43 -0
  451. package/dist/plugins/tidb-session-storage.js +62 -3
  452. package/dist/plugins/tidb-session-store.d.ts +142 -0
  453. package/dist/plugins/tidb-session-store.js +264 -16
  454. package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
  455. package/dist/plugins/tidb-tool-result-store.js +7 -0
  456. package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
  457. package/dist/plugins/tidb-workflow-journal-store.js +7 -0
  458. package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
  459. package/dist/plugins/tidb-workflow-run-store.js +8 -0
  460. package/dist/plugins/tool-result-store-sql.d.ts +58 -0
  461. package/dist/plugins/tool-result-store-sql.js +38 -2
  462. package/dist/plugins/web-search.d.ts +35 -0
  463. package/dist/plugins/web-search.js +45 -9
  464. package/dist/plugins/workflow-journal-limits.d.ts +10 -0
  465. package/dist/plugins/workflow-journal-limits.js +10 -0
  466. package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
  467. package/dist/plugins/workflow-journal-store-sql.js +58 -2
  468. package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
  469. package/dist/plugins/workflow-run-store-sql.js +84 -6
  470. package/dist/plugins/worktree-isolation.d.ts +45 -0
  471. package/dist/plugins/worktree-isolation.js +105 -6
  472. package/dist/plugins/write-behind-counter.d.ts +56 -1
  473. package/dist/plugins/write-behind-counter.js +53 -14
  474. package/dist/principal-jwt.d.ts +24 -0
  475. package/dist/principal-jwt.js +60 -5
  476. package/dist/project-memory.d.ts +22 -0
  477. package/dist/project-memory.js +144 -15
  478. package/dist/prompts-domain-validate.d.ts +28 -0
  479. package/dist/prompts-domain-validate.js +42 -1
  480. package/dist/question.d.ts +33 -0
  481. package/dist/question.js +69 -2
  482. package/dist/resource-suspend.d.ts +21 -0
  483. package/dist/resource-suspend.js +16 -0
  484. package/dist/router/route-orchestration.d.ts +78 -0
  485. package/dist/router/route-orchestration.js +77 -0
  486. package/dist/run-local.d.ts +32 -0
  487. package/dist/run-local.js +172 -8
  488. package/dist/runs.d.ts +189 -4
  489. package/dist/runs.js +465 -24
  490. package/dist/runtime-caps-resolver.d.ts +106 -0
  491. package/dist/runtime-caps-resolver.js +89 -4
  492. package/dist/runtime-governance.d.ts +84 -0
  493. package/dist/runtime-governance.js +127 -1
  494. package/dist/sandbox-pkg-source.d.ts +23 -0
  495. package/dist/sandbox-pkg-source.js +37 -0
  496. package/dist/sealed-key.d.ts +54 -0
  497. package/dist/sealed-key.js +68 -3
  498. package/dist/security.d.ts +108 -0
  499. package/dist/security.js +145 -6
  500. package/dist/sema-registry.d.ts +21 -0
  501. package/dist/sema-registry.js +21 -0
  502. package/dist/session-leaf-bus.d.ts +31 -0
  503. package/dist/session-leaf-bus.js +32 -0
  504. package/dist/session-sync-content.d.ts +46 -0
  505. package/dist/session-sync-content.js +47 -1
  506. package/dist/session-sync-kernel.d.ts +89 -0
  507. package/dist/session-sync-kernel.js +48 -3
  508. package/dist/session-sync.d.ts +125 -0
  509. package/dist/session-sync.js +181 -8
  510. package/dist/session-titler.d.ts +26 -0
  511. package/dist/session-titler.js +41 -7
  512. package/dist/session-watch.d.ts +53 -1
  513. package/dist/session-watch.js +73 -9
  514. package/dist/sighup-idle.d.ts +30 -0
  515. package/dist/sighup-idle.js +12 -1
  516. package/dist/spec-fields.d.ts +91 -3
  517. package/dist/spec-fields.js +116 -4
  518. package/dist/task-cwd.d.ts +50 -0
  519. package/dist/task-cwd.js +68 -1
  520. package/dist/task-mcp.d.ts +47 -0
  521. package/dist/task-mcp.js +29 -0
  522. package/dist/task-settings.d.ts +175 -0
  523. package/dist/task-settings.js +226 -11
  524. package/dist/task-workflow.d.ts +67 -0
  525. package/dist/task-workflow.js +75 -8
  526. package/dist/tool-approval.d.ts +91 -0
  527. package/dist/tool-approval.js +215 -0
  528. package/dist/trace/artifacts.d.ts +9 -0
  529. package/dist/trace/artifacts.js +30 -5
  530. package/dist/trace/core-keyset-guard.d.ts +18 -0
  531. package/dist/trace/ledger-sink.d.ts +43 -0
  532. package/dist/trace/ledger-sink.js +64 -5
  533. package/dist/trace/project.d.ts +133 -0
  534. package/dist/trace/project.js +189 -2
  535. package/dist/trace/redact.d.ts +19 -0
  536. package/dist/trace/redact.js +66 -9
  537. package/dist/usage-analytics.d.ts +26 -0
  538. package/dist/usage-analytics.js +26 -3
  539. package/package.json +1 -1
@@ -16,6 +16,8 @@ export async function handleTasks(req, res, url, ctx) {
16
16
  await handleTasksBody(req, res, url, ctx, miss);
17
17
  return !miss.fell;
18
18
  }
19
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(A9 的唯一改动:去缩进;B3 的唯一改动:
20
+ * 给两个匿名 `idemCache.run` 回调与三条驱动腿起名)。裸 `return;` = 「本域已应答」;走到函数尾才是「没匹配上」。 */
19
21
  async function handleTasksBody(req, res, url, ctx, miss) {
20
22
  const { deps } = ctx;
21
23
  const { idemCache, inflightRuns, cancelledViaVerb, steerableRuns, counters } = ctx.registry;
@@ -24,15 +26,23 @@ async function handleTasksBody(req, res, url, ctx, miss) {
24
26
  const reqState = ctx.req;
25
27
  const source = ctx.req.source;
26
28
  if (req.method === "POST" && (url === "/v1/tasks" || url === "/v1/tasks/stream")) {
29
+ // Idempotency-Key replay BEFORE the rate/quota gates (council blocker): a retry with a known key must get
30
+ // the SAME response, not a fresh 429 because the window filled meanwhile. No-op without a key / on a miss.
27
31
  const rawIdem = headerStr(req.headers["idempotency-key"]);
32
+ // Scope by identity only when there IS a key (BL-3) — avoids reading the principal header on every
33
+ // submit, and keeps the no-key path independent of principal config.
34
+ // F-fix: scope on the VERIFIED principal (gatedPrincipal) — on a direct door a spoofable header would let a
35
+ // caller poison/collide another principal's idempotency cache. (BFF/gated: identical to principalFrom.)
28
36
  const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, gatedPrincipal(req, deps.config)) : undefined;
29
37
  const cached = idemKey ? idemCache.peek(idemKey) : undefined;
30
38
  if (cached) {
31
39
  if (url === "/v1/tasks/stream") {
40
+ // The cached entry may be IN-FLIGHT (a retry arriving during the original stream) → set up the heartbeat
41
+ // BEFORE awaiting so a long wait doesn't idle-timeout, then replay the terminal result.
32
42
  sseHeaders(res);
33
43
  const hb = setInterval(() => {
34
44
  if (!res.writableEnded)
35
- res.write(`event: heartbeat\ndata: {}\n\n`);
45
+ res.write(`event: heartbeat\ndata: {}\n\n`); // real frame (not an SSE comment) — a per-frame-parsing BFF drops comments, so downstream saw a zero-frame window; EventSource clients without a heartbeat listener ignore it (zero break)
36
46
  }, 15_000);
37
47
  try {
38
48
  const resp = await cached;
@@ -55,32 +65,70 @@ async function handleTasksBody(req, res, url, ctx, miss) {
55
65
  const prepared = await prepareSpec(req, res);
56
66
  if (!prepared)
57
67
  return;
68
+ // withPrincipal threads the caller's identity through the run so the cost tracer attributes spend
69
+ // (incl. council/team sub-tasks) to this principal's cumulative quota.
58
70
  const principal = prepared.auth?.principal;
59
71
  if (url === "/v1/tasks/stream") {
60
72
  if (prepared.verify || prepared.cascade) {
61
73
  sendError(res, 400, "request.field_conflict", "verify / cascade are not supported on /v1/tasks/stream (both are multi-attempt, not a single stream) — use /v1/tasks or /v1/runs");
62
74
  return;
63
75
  }
76
+ // R8 (CC parity, fixed for the AgentEvent contract — workflow re-review #2): mint the durable rewind
77
+ // handle (taskId) BEFORE the SSE headers so it rides as an `X-Task-Id` RESPONSE HEADER. The shell reads it even
78
+ // on a Ctrl+C before `done`, WITHOUT an out-of-contract `run_started` data frame (the SDK AgentEvent stream is a
79
+ // CLOSED oneOf; a synthetic data frame makes exhaustive consumers assertNever-throw). createRun below CLAIMS this id.
64
80
  const earlyDurableTid = deps.runStore && prepared.spec.sessionId ? uuidv7() : undefined;
81
+ // [854]①b per-request opt-in 断连宽限:请求头 `x-detach-on-disconnect: true`(选头不选 body 字段——
82
+ // 这是本条连接的传输层语义,不该进持久化 body / 不该随 resume 重放;与 idempotency-key 同类)。
83
+ // 默认行为不动:交互 Esc = 断连即杀的反 token-burn 契约保留(下方 onDisconnect 的 abort 路径)。
84
+ // opt-in 后 client 断连不 abort,run 转后台跑完,结果按既有 durable 路径可查(X-Task-Id → GET /v1/runs/:id)。
85
+ // 无 durable 账本(无 runStore 或无 sessionId)时 400 fail-loud 而非静默忽略:忽略 = 断连仍杀 run,
86
+ // 与 caller 声明的语义相反;就算不杀,跑完的结果也无处可查(纯烧 token)—— body.model silent-drop 同类,
87
+ // 「不做临时方案」判 400 是真版。SSE 头未发,还能干净 400。
65
88
  const detachOnDisconnect = headerStr(req.headers["x-detach-on-disconnect"]) === "true";
66
89
  if (detachOnDisconnect && earlyDurableTid === undefined) {
67
90
  sendError(res, 400, "request.precondition_unmet", "x-detach-on-disconnect requires a durable run (a run store + sessionId) — without one the detached result would be unqueryable");
68
91
  return;
69
92
  }
70
- reqState.streamTaskId = earlyDurableTid;
93
+ reqState.streamTaskId = earlyDurableTid; // [854]⑥: 499 断连日志带 task 上下文(record 只在 aborted 行读它)
94
+ // E8 双腿对齐:detach 腿的 modelUsage 归因键(sessionId 域)——声明在此层,leak-fence clear 在本层 finally。
71
95
  let usageKey;
96
+ // [854]①b/⑥:opt-in 一被受理就置 detached 标记(而非等断连时置)——record 的 res 'close' listener
97
+ // 注册在 onDisconnect 之前,断连时 record 先跑;在这里置位才能让 499 行真带上 detached:true。
98
+ // 语义不变:opt-in 受理后的任何断连都是 detach(run 不杀)。
72
99
  reqState.streamDetached = detachOnDisconnect;
73
100
  sseHeaders(res, earlyDurableTid ? { "x-task-id": earlyDurableTid } : undefined);
101
+ // G15([1833]/[1840]§五):taskId 一等信号进流首帧——浏览器 EventSource 与部分代理面读不到响应头,
102
+ // 消费端此前只能靠 x-task-id(cli liveClient 亲测取头)。meta 帧与头同源同条件(durable 腿才有 id,
103
+ // 无 durable ⇒ 头/帧同缺席,不铸空值);sessionId 同帧(server 铸新 session 时 caller 需要它绑定)。
104
+ // additive:未知 event 型按 SSE 语义被旧消费端忽略。
74
105
  if (earlyDurableTid) {
75
106
  res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", taskId: earlyDurableTid, ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}) })}\n\n`);
76
107
  }
108
+ // Abort the underlying run (and stop the heartbeat) when the client disconnects — otherwise a dropped
109
+ // relay keeps burning billable tokens to completion (council). Mirrors the closed-flag pattern of the
110
+ // poll-based SSE endpoints, and additionally cancels the generator via spec.signal.
77
111
  const ac = new AbortController();
112
+ // probe-stoppedby: a client DISCONNECT on the sync stream lane is a USER stop too —
113
+ // Ctrl+C on the shell drops the SSE and this abort kills the run, but the mark was only wired on the
114
+ // cancel-verb/resume legs, so background children reaped by THIS abort read "parent"/"system" instead
115
+ // of "user". Same registration discipline as runs.ts (FIRST on the signal, before core's teardown
116
+ // listeners). Owner = sessionId (the sync leg's core-canonical taskId — resolveSpec sets no spec.taskId,
117
+ // same key as the resume leg); skipSessionScoped inside the helper keeps CC Backgrounded children unmarked.
78
118
  if (prepared.spec.sessionId)
79
119
  markChildrenStoppedByUserOnAbort(ac.signal, prepared.spec.sessionId, prepared.auth?.principal);
80
120
  let closed = false;
81
- let detachLogged = false;
121
+ let detachLogged = false; // [854]①b:detach 断连 info 只打一条(req/res 两个 close listener 都可能进来)
82
122
  const onDisconnect = () => {
123
+ // [854]①b:detach opt-in 的断连分支 —— 不置 closed(for-await 继续消费到 done,终态走既有
124
+ // setTerminal 路径落 durable 账本)、不 abort(run 不杀)。断连后的 res.write 落在已 destroy 的
125
+ // socket 上被 Node 丢弃;防御:挂一个 res 'error' swallow(destroy 后的 write 在部分 Node 版本
126
+ // 经 nextTick 发 ERR_STREAM_DESTROYED,无 listener 会变 uncaught)—— 只在 detach 已断连后挂,
127
+ // 正常车道的 res 错误面不被吞。task_notification 的 deliverable 谓词含 res.destroyed,断连后
128
+ // 自动改走 durable inbox(既有行为,正合 detach 语义)。
83
129
  if (detachOnDisconnect) {
130
+ // 正常完成后的 req 'close' 不是断连(默认车道靠 abort-after-done 无害才容得下无 guard 的
131
+ // req listener;detach 车道要防它打出误导的 detached info)。
84
132
  if (res.writableEnded)
85
133
  return;
86
134
  if (!detachLogged) {
@@ -94,22 +142,58 @@ async function handleTasksBody(req, res, url, ctx, miss) {
94
142
  ac.abort();
95
143
  };
96
144
  req.on("close", onDisconnect);
145
+ // 🔴 req 'close' does NOT fire on a client disconnect once the request body has been fully consumed
146
+ // (prepareSpec read it before we get here; verified against node 24) — only res 'close' does. With only
147
+ // the req listener the disconnect-abort above never fired on this route, i.e. the dropped-relay token
148
+ // burn it exists to stop went right on burning. writableEnded guards the normal-completion close
149
+ // (end() already called → that close is not a disconnect).
97
150
  res.on("close", () => {
98
151
  if (!res.writableEnded)
99
152
  onDisconnect();
100
153
  });
154
+ // Heartbeat so a long no-token gap (e.g. a debate phase) doesn't trip a mesh/ingress idle timeout on
155
+ // the relay (center). SSE comment lines are ignored by EventSource but keep the socket alive.
101
156
  const hb = setInterval(() => {
157
+ // [854]①b: `!res.destroyed` — detach 断连后 run 可能再跑很久,别每 15s 往已死 socket 写心跳
158
+ // (destroyed = client 已断,写了也只是被丢弃/触发 error;默认车道断连即 abort,窗口极短,不受影响)。
102
159
  if (!res.writableEnded && !res.destroyed)
103
- res.write(`event: heartbeat\ndata: {}\n\n`);
160
+ res.write(`event: heartbeat\ndata: {}\n\n`); // real frame (not an SSE comment) — a per-frame-parsing BFF drops comments, so downstream saw a zero-frame window; EventSource clients without a heartbeat listener ignore it (zero break)
104
161
  }, 15_000);
162
+ // Wrap the whole stream in idemCache.run so the IN-FLIGHT promise is stored BEFORE work begins — a retry
163
+ // that arrives WHILE this stream is still running (relay timeout → re-send same key) is then caught by the
164
+ // peek above and replays this result instead of starting a second billable stream (council). `ranLive`
165
+ // tells the caller that actually produced the stream (wrote events live) apart from a concurrent caller the
166
+ // cache deduplicated — the latter got no live events, so it must be handed the terminal result.
105
167
  let ranLive = false;
106
168
  let resp;
169
+ // ── B3 ── 原文此处是 `idemCache.run` 的第二个实参、一个 **703 行匿名 async**。提为具名闭包:
170
+ // plain-stream 腿。捕获的自由变量(`ranLive`/`prepared`/`principal`/`res`/`ac`/`closed`/`hb`/
171
+ // `earlyDurableTid`/`usageKey`/`reqState`…)与原匿名回调完全同一批,故行为逐字。
107
172
  const runStreamSubmitLeg = async () => {
108
173
  ranLive = true;
109
174
  let finalResult;
175
+ // [868]③: true once the post-stream settle block below recorded the run row's park/terminal state.
176
+ // A throw OUT of the stream (abort mid-iteration, or any plain Error before `done`) skips that whole
177
+ // block — the finally's backstop then settles the leaked `running` row (the durable twin of the
178
+ // fleet-row leak C fixed below; the fleet half got its settleFleet("failed") backstop, the run row
179
+ // was left to reapStale's stale-timeout with a misleading "run stalled" verdict).
110
180
  let runRowSettled = false;
181
+ // Durable F4/ask on the stream path. Two things the sync/async paths do that the stream path
182
+ // used to skip (audit functional gap; both caught live on the gate#3 P3 cross-replica drill):
183
+ // 1. putCtx BEFORE the run → a suspend here (e.g. an AskUserQuestion gate, TC-5.4) is resumable
184
+ // on any replica; without it the operator's resume fails "resume context missing".
185
+ // 2. createRun a task_active claim at SUBMIT (+ heartbeat) → reapCtx's task_active guard then
186
+ // protects the ctx for the WHOLE first leg. The ctx's updated_at is stamped at submit and
187
+ // never refreshed, so a multi-minute first leg (a 5-min gradle build outlived the 120s
188
+ // REAP_RUN_STALE_SEC) ages the ctx past the cutoff and reapCtx deletes it MID-RUN — before
189
+ // the suspend's pending checkpoint can protect it. setSuspended KEEPS the claim (session
190
+ // stays locked submit→resume); a clean completion releases it via setTerminal.
111
191
  let durableTaskId;
112
192
  let durableHeartbeat;
193
+ // R8 (CC parity, interactive rewind on disconnect): the user-message entryId is captured INSIDE
194
+ // streamBody, but the rewind anchor must be writable from BOTH the done branch AND the disconnect branch
195
+ // (Ctrl+C before done) — so hoist it + a once-guarded put to this scope. Without this, a turn cancelled
196
+ // before `done` left NO anchor → the shell, even holding the early taskId, 404'd on rewind.
113
197
  let userMsgEntryId;
114
198
  let anchorPut = false;
115
199
  const putRewindAnchor = () => {
@@ -120,20 +204,47 @@ async function handleTasksBody(req, res, url, ctx, miss) {
120
204
  .put(prepared.spec.sessionId, durableTaskId, userMsgEntryId, principal ?? null)
121
205
  .catch(() => deps.metrics?.inc("resume_anchor_capture_failed"));
122
206
  };
207
+ // R8 (CC parity): the run-row claim + `durableTaskId` (the REWIND anchor key + the unique
208
+ // result.taskId re-stamp) is gated on `runStore` ALONE; the durable-suspend resume ctx (`putCtx`) on
209
+ // `checkpointStore`. They were bundled under ONE checkpointStore gate, over-coupling REWIND to
210
+ // DURABLE_APPROVAL — but E18 resume-at needs NO checkpoint (main.ts:815). Now matches the /v1/runs bg path:
211
+ // rewind works with just runStore (DB_BACKEND=tidb/pg), no approval. Claim-first preserved (session locked
212
+ // submit→resume; a concurrent submit → 409, never a silent 2nd leg, and a rejected submit never clobbers the ctx).
123
213
  if (deps.runStore && prepared.spec.sessionId) {
124
- const tid = earlyDurableTid ?? uuidv7();
214
+ const tid = earlyDurableTid ?? uuidv7(); // reuse the id already emitted as the X-Task-Id header (defensive mint if absent)
125
215
  const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
126
216
  if (created.ok)
127
- deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective);
217
+ deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective); // fire-and-forget; in-titler dedupe
128
218
  if (!created.ok) {
129
219
  const conflict = { error: "session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)", errorCode: "conflict.session_active_run", activeTaskId: created.activeTaskId };
220
+ // Headers are already SSE — encode the rejection as the stream's terminal event; the 409 in
221
+ // the returned resp is for a deduplicated concurrent caller (and is never idem-cached).
130
222
  res.write(`data: ${JSON.stringify({ type: "done", result: { status: "failed", errorMessage: conflict.error, activeTaskId: conflict.activeTaskId } })}\n\n`);
131
223
  return { status: 409, body: conflict };
132
224
  }
133
225
  durableTaskId = tid;
134
226
  }
227
+ // [900]①/[906]③ detach 车道逐事件落账:sync 腿历史上只 createRun+setTerminal(events 账本恒空,
228
+ // detach 后壳只能轮询终局)。opt-in 车道把 for-await 的每个事件喂给与 bg 腿(runs.ts)共享的
229
+ // LedgerSink(src/trace/ledger-sink.ts —— 单一 switch,落账 type/data 形状与 GET /v1/runs/:id/events
230
+ // 的 bg 产出完全一致),detach 后壳换道 events tail 全帧续收。非 detach 车道恒 undefined = 零改动
231
+ // (不落账,byte-compat)。reasoning gate 与 bg 腿同一配置源(config.traceThinking / TRACE_THINKING)。
232
+ // 本腿账面差异(如实;[1840]§五车后已缩两项):suggestions 不落(本腿不调 stream.suggestions)、
233
+ // elicitation/question/approval 帧走 live emitAsk 面不落账、stream-open inbox drain 与 observer 臂
234
+ // 的 task_notification 走 wire/durable-inbox 既有链(post-detach 自动 park 进 inbox,下一腿 drain
235
+ // 补投)——不在本账本。已对齐:model_usage(E8 三件,下方 register/turn_end delta/终局 attach)、
236
+ // file_link(live+durable 双写,emit 回调处)。
135
237
  let ledgerSink;
238
+ // [900]①: a terminal/park event already landed in the ledger — the failure backstops below must
239
+ // never write `failed` over it (bg 腿 `reached` 同款纪律).
136
240
  let ledgerTerminal = false;
241
+ // model_usage 双腿对齐([1840]§五,W9 同根):detach 账本腿补上 bg 腿的 E8 三件(register /
242
+ // turn_end delta 落账 / 终局 SUM 进 stats.modelUsage)。归因键=**sessionId**,不是 durableTaskId:
243
+ // core 的 taskId mint = `spec.taskId ?? sessionId`(dist 亲读 runtask.js:773),本腿 resolveSpec
244
+ // 不设 spec.taskId ⇒ tracer 的 brain.call e.taskId = sessionId。账本键仍是 durableTaskId(两域
245
+ // 分家——attach 时 getEvents 按账本键包一层)。非 detach 车道照旧不登记(下方 E8 INTENTIONAL
246
+ // 排除注释仍准确——那腿无账本消费者);2107 行的「账面差异」清单相应少了 model_usage 一项。
247
+ // (usageKey 声明在外层——leak-fence clear 在外层 finally,须覆盖 throw 路径。)
137
248
  if (detachOnDisconnect && durableTaskId && deps.runStore) {
138
249
  const rs = deps.runStore;
139
250
  const tid = durableTaskId;
@@ -143,16 +254,20 @@ async function handleTasksBody(req, res, url, ctx, miss) {
143
254
  deps.modelUsage?.register(usageKey);
144
255
  }
145
256
  if (deps.checkpointStore && prepared.spec.sessionId) {
257
+ // putCtx AFTER the claim (resumable). If this throws, the claim row self-heals: heartbeat hasn't
258
+ // started, so reapStale flips it failed + releases task_active after runStaleSec.
146
259
  await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
147
260
  }
148
261
  if (durableTaskId && deps.runStore) {
149
262
  const rs = deps.runStore;
150
263
  const tid = durableTaskId;
151
- const owner = principal ?? null;
264
+ const owner = principal ?? null; // run created with this owner — pass it as the single-DB null-safe heartbeat guard
152
265
  const abortFromVerb = () => {
153
- cancelledViaVerb.add(tid);
266
+ cancelledViaVerb.add(tid); // H2: label the settle (errorCode "cancelled"), see the set's decl
154
267
  ac.abort();
155
268
  };
269
+ // [1.207 codex H1] heartbeat ALSO polls the durable cancel flag (bg-leg parity, runs.ts) — a
270
+ // cancel landing on ANOTHER replica only sets the flag, and this leg previously had no consumer.
156
271
  durableHeartbeat = setInterval(() => {
157
272
  void rs.heartbeat(tid, owner).catch(() => undefined);
158
273
  if (!ac.signal.aborted) {
@@ -161,14 +276,36 @@ async function handleTasksBody(req, res, url, ctx, miss) {
161
276
  }
162
277
  }, HEARTBEAT_MS);
163
278
  durableHeartbeat.unref?.();
279
+ // [888]① interactive live-run cancel: register this leg's abort controller under the SAME
280
+ // in-flight map the bg/resume legs use — POST /v1/runs/:id/cancel's fast path (`inflightRuns
281
+ // .get(taskId)?.abort()`) previously only ever hit bg legs, so cancelling an interactive run
282
+ // flipped the durable flag that NOTHING on this leg consumed (release deferred to reapStale).
283
+ // Abort rides the leg's existing disconnect-kill chain (spec.signal), which settles the row.
164
284
  inflightRuns.set(tid, ac);
285
+ // [1.207 codex H1] registration-window catch-up: a cancel that landed between createRun and this
286
+ // point (the awaited putCtx window) flipped the flag while the fast path found no entry — check
287
+ // once so it is honored now, not at the first heartbeat.
165
288
  void Promise.resolve(rs.isCancelRequested?.(tid, owner)).then((c) => { if (c && !ac.signal.aborted)
166
289
  abortFromVerb(); }).catch(() => undefined);
167
290
  }
291
+ // MF-Fleet: publish this run's row transitions to the fleet bus (a tracked/durable run becomes a
292
+ // fleet row; turn_end accrues tokens; core `task_progress` adds subagent child rows; terminal removes
293
+ // it). No-op for an ephemeral sync run (no durableTaskId) or when no fleetBus is wired.
294
+ // [1.211 codex L] fleet 装配纳入清理保护:heartbeat/inflightRuns 注册在上面已发生,而外层
295
+ // try/finally 从更后面才开始——fleetRunPublisher/onStart 若在这窗内 throw,计时器与 cancel 句柄
296
+ // 无人清理(SSE 头已发,请求以 500 帧终止但资源漏)。let+窄 try/catch 清理后 rethrow 补上这段。
168
297
  let fleetPub;
169
298
  try {
170
299
  fleetPub = durableTaskId
171
- ? fleetRunPublisher(deps.fleetBus, { runId: durableTaskId, scope: gatedPrincipal(req, deps.config) ?? "default", rootTaskId: prepared.spec.sessionId, ...fleetRunLabels(prepared.spec.objective) })
300
+ // core 1.154 nested-subagent tree: rootTaskId = the run's CANONICAL core taskId (`spec.taskId ?? sessionId`).
301
+ // The sync leg does NOT set spec.taskId, so canonical == sessionId — a forwarded subagent whose parentTaskId
302
+ // equals it nests under the run; deeper subagents nest under their parent subagent's row.
303
+ // 🔒 Scope the fleet row with the SAME identity GET /v1/fleet/stream FILTERS by — `gatedPrincipal`
304
+ // (crypto-verified on a direct door), NOT the cost-attribution `principal` (= the spoofable header).
305
+ // On a direct door the two diverge: a spoofed header would tag the row under a victim's scope (cross-
306
+ // tenant leak) or under "default" while the verified owner's stream filters it OUT (the row vanishes
307
+ // from its real owner's view). Tenant-isolation parity with the stream filter (TOC review #6).
308
+ ? fleetRunPublisher(deps.fleetBus, { runId: durableTaskId, scope: gatedPrincipal(req, deps.config) ?? "default", rootTaskId: prepared.spec.sessionId, ...fleetRunLabels(prepared.spec.objective) }) // BC-1: name = short objective preview (description = live-activity, set by the publisher onEvent tool_start)
172
309
  : undefined;
173
310
  }
174
311
  catch (e) {
@@ -176,41 +313,72 @@ async function handleTasksBody(req, res, url, ctx, miss) {
176
313
  clearInterval(durableHeartbeat);
177
314
  if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
178
315
  inflightRuns.delete(durableTaskId);
179
- cancelledViaVerb.delete(durableTaskId);
316
+ cancelledViaVerb.delete(durableTaskId); // [1.211 codex L] 标记集同窗清理
180
317
  }
181
318
  throw e;
182
319
  }
320
+ // C (TOC adversarial review): settle the fleet row EXACTLY once. A plain Error thrown mid-stream
321
+ // (not done/CheckpointError/cancel) escapes the loop before any onTerminal fires → the row leaks as a
322
+ // stale "running" forever. Route every onTerminal through this guard so the finally below can settle a
323
+ // row no terminal branch reached, without ever double-settling one that did.
183
324
  let fleetSettled = false;
184
325
  const settleFleet = (status, residuals) => {
185
326
  if (fleetSettled)
186
327
  return;
187
328
  fleetSettled = true;
188
- fleetPub?.onTerminal(status, residuals);
329
+ fleetPub?.onTerminal(status, residuals); // [1839]①/(a):真终态帧带残局键(publisher 侧对 parked 再挡一层)
189
330
  };
331
+ // 1.207: THIS leg's live TaskStream (assigned inside streamBody) — held here so the outer finally can
332
+ // identity-guard its deregistration (only delete the handle WE registered; see the finally).
190
333
  let liveStreamRef;
334
+ // design/122 ③: this leg's subagent-handle unregister thunks — run at leg end (finally), not on settle.
191
335
  const subagentHandleEvictions = [];
336
+ // false once this leg settles — a bg subagent/bash completion after that has no live
337
+ // harness (idle) → the observer parks it in the durable inbox (see fwdInternals below).
192
338
  let syncLegLive = true;
339
+ // option A: taskNotificationStreamKey (域:task_id:status[#seq], core [784]④a + codex F2 域二分)
340
+ // keys this leg already delivered (turn-open inbox drain, stream branch OR observer) — each arm
341
+ // skips keys another arm already carried, so a notification isn't double-written to the SSE (the
342
+ // turn-TAIL reap notification, which never reaches the stream, is the one the observer catches;
343
+ // core's pend-drain replay of a double-parked idle arrival is the one the drain-registration
344
+ // catches — core [784]④b). codex F1: NotifiedKeys (Map keyed on write outcome) for three-leg
345
+ // parity — this leg's writes are all synchronous res.write (no async failure window), so every
346
+ // claim registers resolved(true) and the has/skip semantics are unchanged here.
193
347
  const syncNotifiedKeys = new NotifiedKeys();
194
348
  try {
195
- fleetPub?.onStart();
349
+ fleetPub?.onStart(); // [1.211 codex L] 同窗保护:subscriber throw 不再漏计时器/句柄
196
350
  }
197
351
  catch (e) {
198
352
  if (durableHeartbeat)
199
353
  clearInterval(durableHeartbeat);
200
354
  if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
201
355
  inflightRuns.delete(durableTaskId);
202
- cancelledViaVerb.delete(durableTaskId);
356
+ cancelledViaVerb.delete(durableTaskId); // [1.211 codex L] 标记集同窗清理
203
357
  }
204
- settleFleet("failed");
358
+ settleFleet("failed"); // [1.211 codex M2] publishTask 先写行再 emit——subscriber throw 时行已在,不清即永久 running 幽灵行
205
359
  throw e;
206
360
  }
361
+ // R8 rewind handle is now the X-Task-Id RESPONSE HEADER (set with earlyDurableTid above), not an
362
+ // out-of-contract `run_started` data frame — the shell reads it from the response headers even on an early
363
+ // disconnect, and the SDK's CLOSED AgentEvent stream stays unpolluted (workflow re-review #2 caught the drift).
207
364
  try {
208
365
  const streamBody = async () => {
209
366
  await withPrincipal(principal, async () => {
367
+ // core 1.154 nested-subagent live tree: opt into the forward sink — a subagent's task_progress
368
+ // lives in its ISOLATED child stream (never on THIS top stream; core's only emitter is
369
+ // parentToolCallId-gated), so this sink is the ONLY path a tick reaches the top-run consumers.
370
+ // 1.78 (review HIGH): fan it out to the LIVE SSE too (shared whitelist+redact builder, carrying
371
+ // the E2 identity the shell binds concurrent ticks by) — previously it fed ONLY the fleet bus,
372
+ // so the conversation lane never saw a real tick and the in-loop task_progress branch below is
373
+ // forward-compat only (dead against core ≤1.208). core swallow-guards a throwing sink.
210
374
  const fwdInternals = {
211
375
  onForwardEvent: (e) => {
212
376
  fleetPub?.onForwardEvent(e);
377
+ // [900]① detach 车道:子代理 forward 事件(tool_start/end + task_progress)也落账 —— bg 腿
378
+ // 同姿势(共享 sink 的 fire-and-forget 门,child deltas 有意不逐块落,见 LedgerSink.onForwardEvent)。
213
379
  ledgerSink?.onForwardEvent(e);
380
+ // S2 live tail(core 1.370 bgAgentId;bg 腿 twin 在 runs.ts):句柄分发在 res 存活判之前
381
+ // ——主流断了 tail 订阅者照收(tail 是独立连接)。
214
382
  {
215
383
  const bg = e.bgAgentId;
216
384
  if (bg !== undefined && defaultSubagentTailBus.hasSubscribers(bg)) {
@@ -226,36 +394,73 @@ async function handleTasksBody(req, res, url, ctx, miss) {
226
394
  res.write(`data: ${JSON.stringify({ type: "task_progress", ...taskProgressEventData(e) })}\n\n`);
227
395
  }
228
396
  else if (t === "tool_start") {
397
+ // C1 (core 1.219, spec.forwardSubagentEvents): a delegated child's live CONTENT events now
398
+ // ride the forward sink (each stamped parentToolCallId — the shell nests them under the
399
+ // delegation card). Echo through the SAME shared whitelist+redact builders as the top
400
+ // stream (§E1 — forwarded events are UNTRUSTED RAW; redaction is ours at the wire).
229
401
  const tsv = e;
230
402
  res.write(`data: ${JSON.stringify({ type: "tool_start", ...toolStartEventData(tsv) })}\n\n`);
231
403
  }
232
404
  else if (t === "tool_end") {
233
405
  const te = e;
234
- res.write(`data: ${JSON.stringify({ type: "tool_end", ...toolEndEventData(te) })}\n\n`);
406
+ res.write(`data: ${JSON.stringify({ type: "tool_end", ...toolEndEventData(te) })}\n\n`); // label 现由 builder 自带,外拼冗余已清
235
407
  }
236
408
  else if (t === "text_delta") {
409
+ // Child text is plain model output (same trust class as the top stream's text_delta, which
410
+ // rides verbatim); parentToolCallId attributes it. Whitelist the three fields explicitly.
237
411
  const td = e;
238
412
  res.write(`data: ${JSON.stringify({ type: "text_delta", delta: td.delta, ...(td.eventId ? { eventId: td.eventId } : {}), ...(td.parentToolCallId ? { parentToolCallId: td.parentToolCallId } : {}) })}\n\n`);
239
413
  }
240
414
  else if (t === "reasoning_delta") {
415
+ // §E1 parity with the top stream's live CoT: redactSecrets before echo.
241
416
  const rd = e;
242
417
  res.write(`data: ${JSON.stringify({ type: "reasoning_delta", delta: redactSecrets(rd.delta), ...(rd.eventId ? { eventId: rd.eventId } : {}), ...(rd.parentToolCallId ? { parentToolCallId: rd.parentToolCallId } : {}) })}\n\n`);
243
418
  }
244
419
  },
420
+ // background-completion observer — flip the child's fleet
421
+ // row (task_progress never emits a terminal tick) + park an idle-arrival in the durable
422
+ // inbox (workflow-type skipped: own durable chain). Rationale in runs.ts (bg-leg twin).
423
+ // core 1.283: `seq` = the per-stop-cycle counter (stop→wake→stop mints 1,2,…) — it MUST
424
+ // ride into the dedup key below (core [784]④a) and into the parked inbox entry.
425
+ // 白名单排查(2026-07-24,「彻查 as {} 连环坑」):手写窄类型换 core 真实类型,理由同 runs.ts twin。
245
426
  onTaskNotification: (n) => {
246
427
  if (n.task_type === "workflow")
247
428
  return;
429
+ // S2 复审 #3:中途终态信号——task_settled 帧让订阅者不再 heartbeat 永挂(路由收帧即收流)。
248
430
  if (defaultSubagentTailBus.hasSubscribers(n.task_id)) {
249
431
  defaultSubagentTailBus.publish(n.task_id, { type: "task_settled", taskId: n.task_id, status: n.status, ...(typeof n.seq === "number" ? { seq: n.seq } : {}), ...(n.summary ? { summary: redactSecrets(n.summary) } : {}) });
250
432
  }
433
+ // id-domain alias: flip by payload.sessionId (= the tick's uuid domain), fallback task_id.
251
434
  const hadRow = fleetPub?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false;
435
+ // 🔴 对抗评审 2026-07-11(HIGH,「下一 turn 0 帧」最强根因):the teardown WINDOW — after the
436
+ // client disconnects (the user killed the turn → ac.abort → core reaps the bg child → THIS
437
+ // notification fires) but BEFORE the outer finally flips `syncLegLive=false` (several awaits sit
438
+ // between the for-await break and the finally). In that window the old predicate neither parked
439
+ // (legLive still true) nor wrote (socket dead) — the notification vanished with no inbox copy and
440
+ // no frame. Fold the dead-socket state into the park predicate: the leg is only DELIVERABLE when
441
+ // it is live AND the socket can still be written. 🔴 `res.destroyed` is the CLIENT-disconnect
442
+ // signal (`writableEnded` only flips when WE call res.end()) — the real-HTTP retest caught the
443
+ // writableEnded-only predicate still writing into the dead socket.
252
444
  const deliverable = syncLegLive && !res.writableEnded && !res.destroyed;
253
445
  const parked = !deliverable && Boolean(deps.workflowCompletionInbox && prepared.spec.sessionId);
446
+ // diagnosability (rationale in runs.ts twin).
254
447
  deps.logger?.info?.("task_notification_observed", { route: "sync-stream", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive: syncLegLive, parkedDurable: parked });
255
448
  if (parked) {
449
+ // 对抗评审 2026-07-11(MED):the fire-and-forget swallow hid EVERY enqueue failure (SQL blip,
450
+ // fence race) while the diag line above had already printed parkedDurable:true — a false park✓
451
+ // fingerprint. Log the failure; behavior (never fault the callback) unchanged.
256
452
  void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(prepared.spec.sessionId, askOwner, n, Date.now(), durableTaskId)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "sync-stream", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
257
453
  }
258
454
  else if (deliverable) {
455
+ // option A (turn-TAIL killed receipt must be IMMEDIATE): a teardown-time reap
456
+ // notification fires while the SSE is still open but past the stream's delivery window —
457
+ // write the display frame directly, skipping keys the stream already carried (mid-turn
458
+ // dedup; the shell's dedup stays the outer belt). core [784]④a: the key folds `seq` in
459
+ // (shared taskNotificationStreamKey) — the old seq-less `${task_id}:${status}` shape
460
+ // REVERSE-swallowed a second stop-cycle's same-status frame on this arm (stop→wake→stop:
461
+ // the seq=2 completed frame collided with cycle 1's key and the user never saw it).
462
+ // codex F1: res.write is a synchronous enqueue (no async failure window — a dead socket
463
+ // is already folded into `deliverable` above), so the claim registers resolved(true).
259
464
  const key = taskNotificationStreamKey(n);
260
465
  if (syncNotifiedKeys.get(key) === undefined) {
261
466
  syncNotifiedKeys.set(key, Promise.resolve(true));
@@ -263,6 +468,10 @@ async function handleTasksBody(req, res, url, ctx, miss) {
263
468
  }
264
469
  }
265
470
  },
471
+ // C2 (core 1.219) + design/122 (core 1.225 ③): register each sync delegation's steer handle
472
+ // under this run's durable taskId (the X-Task-Id the shell holds). NOT evicted on settle —
473
+ // resume is only legal after settle; evicted at leg end via the collected identity-guarded
474
+ // thunks (outer finally), so a settled child stays steer-409/resume-able while the run lives.
266
475
  ...(deps.subagentSteerRegistry && durableTaskId
267
476
  ? {
268
477
  onSubagentSpawn: (handle) => {
@@ -271,6 +480,15 @@ async function handleTasksBody(req, res, url, ctx, miss) {
271
480
  }
272
481
  : {}),
273
482
  };
483
+ // R8 (CC parity, interactive rewind): the sync `tasks.stream` path (the REPL's lane) must ALSO
484
+ // leave a per-prompt rewind anchor, else an interactive-turn rewind 404s (the bg + resume legs capture
485
+ // it, this one didn't). Buffer the committed USER-message entryId on the HOISTED `userMsgEntryId` (so
486
+ // the disconnect branch can anchor too); the done branch anchors here, the disconnect branch below.
487
+ // core 1.207: hold the LIVE TaskStream handle + register it (keyed by durableTaskId = the
488
+ // X-Task-Id the shell already holds) so the replica-local verbs — POST /v1/runs/:id/detach
489
+ // (mid-flight ctrl+b) and, as a natural consequence, steer/compact — now reach a mid-turn
490
+ // INTERACTIVE run too (previously only the bg + resume legs registered). Deregistered in the
491
+ // outer finally (with the fleet settle), so an early throw can't leak a stale handle.
274
492
  const liveStream = deps.runner.runTaskStream({ ...prepared.spec, signal: ac.signal }, undefined, fwdInternals);
275
493
  liveStreamRef = liveStream;
276
494
  if (durableTaskId)
@@ -281,19 +499,45 @@ async function handleTasksBody(req, res, url, ctx, miss) {
281
499
  if (ev.type === "message_committed" && ev.role === "user")
282
500
  userMsgEntryId = ev.entryId;
283
501
  if (ev.type === "done") {
502
+ // Never stream the durable-resume capability token to the client: a
503
+ // durable suspend's done event carries it. Strip it from BOTH the live write and the cached
504
+ // body (handed to a concurrent deduplicated caller below). No-op for a normal result.
284
505
  finalResult = stripCheckpointToken(ev.result);
506
+ // R8 (CC parity, interactive rewind): on the sync leg core sets result.taskId = sessionId
507
+ // (spec.taskId is left unset so a forwarded subagent nests under the run — server.ts above), which
508
+ // is NOT unique per prompt → it can't be a per-turn rewind handle (every turn would collide). Re-
509
+ // stamp the WIRE result.taskId to the unique durable run id (uuidv7, == the bg path's result.taskId
510
+ // → consistent across paths) so the shell gets a unique per-prompt handle. Internal canonical
511
+ // (fleet/subagent nesting) keeps sessionId — only the external done-frame value changes. Durable-run
512
+ // only (ephemeral has no anchor store ⇒ no rewind anyway), so the fallback keeps sessionId.
285
513
  if (durableTaskId && finalResult)
286
514
  finalResult = { ...finalResult, taskId: durableTaskId };
515
+ // [1.207 codex H2] cancel-verb abort label: core's external-signal abort yields
516
+ // failed WITHOUT an errorCode — project `cancelled` onto the wire+persisted form so
517
+ // /cancel's outcome is uniform (first-writer-wins would pin the unlabeled form). Only
518
+ // an abort-shaped FAILED result is labeled — a completion that raced the abort stands.
287
519
  if (durableTaskId && cancelledViaVerb.has(durableTaskId) && finalResult?.status === "failed" && !finalResult.errorCode) {
288
520
  finalResult = { ...finalResult, errorCode: "cancelled" };
289
521
  }
290
522
  finalizeTaskResult(ev.result, principal, prepared.spec.objective, prepared.spec.sessionId);
523
+ // Anchor this interactive prompt (taskId → user-message entryId) keyed by the SAME unique id the
524
+ // shell now holds (done.result.taskId == durableTaskId). entryId came straight from
525
+ // `message_committed{role:"user"}` (no getLeafId). Best-effort + once-guarded — a capture failure
526
+ // only makes THIS turn non-rewindable, never faults the run; the disconnect branch shares the guard.
291
527
  putRewindAnchor();
292
- settleFleet(finalResult?.status ?? "completed", fleetRunResiduals(finalResult));
528
+ settleFleet(finalResult?.status ?? "completed", fleetRunResiduals(finalResult)); // [1839]①/(a):终态残局键
529
+ // [900]① detach 车道:done 也落账(flush 攒批 + suspended/needs_review/done 投影,bg 词汇;
530
+ // finalResult 已 stripCheckpointToken + taskId 重印 + cancel 标注 —— 账本形 == wire 形)。
531
+ // 放在 res.write 之前:壳收到 wire done 后立刻换道 events tail 也必见终局帧。既有
532
+ // finalize/settleFleet/res.write/setTerminal 顺序不动(落账是旁路)。
293
533
  if (ledgerSink && finalResult) {
534
+ // E8 双腿对齐:终局 flush 最后一拍 delta + SUM 全账本 model_usage 进 stats.modelUsage
535
+ // (bg 腿 withModelUsage 同形)。⚠️ 键两域分家:drain 键=usageKey(sessionId,core 归因),
536
+ // 账本读键=durableTaskId——attachModelUsage 的 taskId 双用途在此不成立,getEvents 包一层。
294
537
  if (usageKey && deps.modelUsage && deps.runStore && durableTaskId) {
295
538
  const rs2 = deps.runStore;
296
539
  const tid2 = durableTaskId;
540
+ // attach 失败(SUM 读瞬断)不许炸掉 onDone——终局帧比 modelUsage 汇总重要,回落原 result。
297
541
  const fr = finalResult;
298
542
  finalResult = await attachModelUsage(fr, { append: ledgerSink.append, getEvents: (_id, after) => rs2.getEvents(tid2, after), modelUsage: deps.modelUsage, taskId: usageKey }).catch(() => fr);
299
543
  }
@@ -303,54 +547,116 @@ async function handleTasksBody(req, res, url, ctx, miss) {
303
547
  res.write(`data: ${JSON.stringify({ ...ev, result: finalResult })}\n\n`);
304
548
  continue;
305
549
  }
550
+ // [900]① detach 车道逐事件落账(await 保序 —— 与 bg 腿同代价姿势;message_committed/未知型
551
+ // sink 内部 no-op)。非 detach 车道 ledgerSink 恒 undefined = 零改动。
306
552
  if (ledgerSink) {
307
553
  await ledgerSink.onEvent(ev);
554
+ // E8 双腿对齐:turn 边界 drain per-model delta 落账(bg 腿 flushModelUsage 同位同形)。
308
555
  if (ev.type === "turn_end" && usageKey)
309
556
  await appendModelUsageDelta(ledgerSink.append, deps.modelUsage, usageKey);
310
557
  }
311
- fleetPub?.onEvent(ev);
558
+ fleetPub?.onEvent(ev); // MF-Fleet: turn_end → tokens; task_progress → subagent child row
559
+ // S4 P0-a: count brain-liveness phases on the sync leg too. The bg/resume legs count at
560
+ // their append sites.
312
561
  if (ev.type === "status")
313
562
  deps.metrics?.inc("brain_retry_total", { phase: String(ev.phase) });
563
+ // E1 (shell-host contract): tool_end.output AND (core 1.203) tool_end.structured are
564
+ // UNTRUSTED RAW from core (file/shell/API bodies; a structured card carries `originalFile`) —
565
+ // redact at the live wire boundary before echo (core only SIZE-bounds; redaction is ours, same
566
+ // contract as the durable path). 🔒 EXPLICIT WHITELIST, not `{...ev}` (the 1.72 task_progress
567
+ // lesson, re-proven by `structured`: a spread forwards a future core field RAW — 1.203 landing
568
+ // meant `structured` rode this leg unredacted until this build). Known-safe fields verbatim:
569
+ // ids/name/label/isError/truncated + E2 identity. NB the old guard also skipped redaction when
570
+ // `output` was undefined (→ raw catch-all) — the whitelist path now covers every tool_end.
314
571
  if (ev.type === "status") {
572
+ // TaskEvent 三腿审查(steering_injected 跟进批):`status`(BrainStatus.detail,UNTRUSTED —
573
+ // 可能回显 provider 的 rate-limit/retry 报文)此前落裸 catch-all 未脱敏,与 durable 两腿
574
+ // (ledger-sink.onEvent + resume switch)共用的 brainStatusEventData 脱敏不一致——同类
575
+ // 「留在 catch-all」教训(1.72 task_progress),现挪显式分支,三腿同门同形。
315
576
  res.write(`data: ${JSON.stringify({ type: "status", ...brainStatusEventData(ev) })}\n\n`);
316
577
  }
317
578
  else if (ev.type === "tool_end") {
318
579
  const te = ev;
319
- res.write(`data: ${JSON.stringify({ type: "tool_end", ...toolEndEventData(te) })}\n\n`);
580
+ res.write(`data: ${JSON.stringify({ type: "tool_end", ...toolEndEventData(te) })}\n\n`); // label 现由 builder 自带,外拼冗余已清
320
581
  }
321
582
  else if (ev.type === "tool_start") {
583
+ // 🔒 E1 parity (TOC adversarial review): redact tool_start.args on the LIVE wire too — the model's
584
+ // tool-call inputs can echo a secret it read (e.g. a value from a redacted tool_end fed into the
585
+ // next call). Whitelist for the same reason as tool_end (shared durable builder = one shape).
322
586
  const tsv = ev;
323
587
  res.write(`data: ${JSON.stringify({ type: "tool_start", ...toolStartEventData(tsv) })}\n\n`);
324
588
  }
325
589
  else if (ev.type === "task_notification") {
590
+ // core 1.202 (design/115 P2): a background task (workflow) finished while THIS harness was live —
591
+ // core already injected the <task-notification> XML into the model in-process; this is the display
592
+ // projection for the shell. Whitelist + redact via the shared builder (summary/result are UNTRUSTED
593
+ // free text). The shell dedups against the inbox-drain `workflow_complete` lane by run id.
594
+ // core [784]④b(跨层双泊车): a notification that arrived while the previous leg was idle is
595
+ // parked TWICE — core pends it in-Runner (pendingSessionNotifications) AND our sync observer
596
+ // parks it in the durable inbox. On this turn-open, our inbox drain (below the streamBody def)
597
+ // already wrote the frame and REGISTERED its key; core's pend-drain replays the same payload as
598
+ // a stream `task_notification` event (runtask.js queue.push) — skip it here instead of writing
599
+ // the wire twice. Same check also folds the mid-turn observer-then-stream echo. A frame whose
600
+ // key is NOT yet on this stream writes exactly as before (the first copy is never eaten).
601
+ // codex F1/F2: same await-the-claim shape as the bg/resume twins (on THIS leg every claim
602
+ // is resolved(true) — sync writes — so the await is immediate); task_type rides the key.
326
603
  const tnv = ev.notification;
327
604
  const tnvKey = tnv?.task_id ? taskNotificationStreamKey({ task_type: tnv.task_type, task_id: tnv.task_id, status: String(tnv.status), seq: tnv.seq }) : undefined;
328
605
  const tnvPrior = tnvKey !== undefined ? syncNotifiedKeys.get(tnvKey) : undefined;
329
606
  if (tnvPrior === undefined || !(await tnvPrior)) {
330
607
  if (tnvKey !== undefined)
331
- syncNotifiedKeys.set(tnvKey, Promise.resolve(true));
608
+ syncNotifiedKeys.set(tnvKey, Promise.resolve(true)); // stream delivered this key
332
609
  res.write(`data: ${JSON.stringify({ type: "task_notification", ...taskNotificationEventData(ev) })}\n\n`);
333
610
  }
334
611
  }
335
612
  else if (ev.type === "reasoning_delta" && typeof ev.delta === "string") {
613
+ // 🔒 E1 parity (TOC adversarial review): redact the CoT on the LIVE wire too. The agent's
614
+ // reasoning can echo a secret it read (core [R57]: CoT may carry reasoned-about secrets). The
615
+ // DURABLE path redacts it (runs.ts → redactSecrets, gated by persistThinking/TRACE_THINKING);
616
+ // the live mirror MUST match. Keep STREAMING the (redacted) thinking so live CoT still renders.
617
+ // 🔒 EXPLICIT WHITELIST, not `{...ev}` (TaskEvent 三腿审查跟进批, 1.72 task_progress lesson
618
+ // re-proven here: a spread forwards an unknown/future field RAW; this arm was the one
619
+ // sibling branch that still spread — the FORWARDED twin below [t === "reasoning_delta"]
620
+ // already used the narrow form, this now matches it byte-for-byte).
336
621
  res.write(`data: ${JSON.stringify({ type: "reasoning_delta", delta: redactSecrets(ev.delta), ...(ev.eventId ? { eventId: ev.eventId } : {}), ...(ev.parentToolCallId ? { parentToolCallId: ev.parentToolCallId } : {}) })}\n\n`);
337
622
  }
338
623
  else if (ev.type === "task_progress") {
624
+ // 🔒 E1 live-wire parity: explicit whitelist via the SHARED builder (UNTRUSTED
625
+ // `name` redacted; 1.78: E2 identity rides — the shell binds concurrent subagent ticks by
626
+ // eventId/parentToolCallId) — parity with the durable runs.ts + resume legs.
339
627
  res.write(`data: ${JSON.stringify({ type: "task_progress", ...taskProgressEventData(ev) })}\n\n`);
340
628
  }
341
629
  else if (ev.type === "compacted") {
630
+ // core 1.219 (attachWorkingFiles default ON): `compacted` now carries `attachedFiles` — route it
631
+ // through the SHARED whitelist builder (paths redactSecrets'd) instead of the raw catch-all, so
632
+ // the live wire matches the durable legs' shape exactly (one builder, no drift).
342
633
  res.write(`data: ${JSON.stringify({ type: "compacted", ...compactedEventData(ev) })}\n\n`);
343
634
  }
344
635
  else if (ev.type === "diagnostics") {
636
+ // core 1.220 (design/121): the shell's render source for CC's diagnostics row.
637
+ // Shared whitelist builder (§E1: language-server output UNTRUSTED RAW → redactSecrets).
345
638
  res.write(`data: ${JSON.stringify({ type: "diagnostics", ...diagnosticsEventData(ev) })}\n\n`);
346
639
  }
347
640
  else if (ev.type === "steering_injected") {
641
+ // [1550]/[1552]: core's mid-run system-reminder injection — shared whitelist builder
642
+ // (§E1: preview is UNTRUSTED free text → redactSecrets). Previously fell through to the
643
+ // raw catch-all below (unredacted `{...ev}`) — moved to an explicit builder for the same
644
+ // reason every other content type left the catch-all (the 1.72 task_progress lesson).
348
645
  res.write(`data: ${JSON.stringify({ type: "steering_injected", ...steeringInjectedEventData(ev) })}\n\n`);
349
646
  }
350
647
  else if (ev.type === "workspace_changed") {
648
+ // [1559]二 core 裁定:bash cd/EnterWorktree 移动任务逻辑 cwd 后一帧,给 shell UI 的路径
649
+ // 提示行。cwd 与已展示的工具 args 同信任级 → 共享白名单构造器(redactSecrets + bound)。
351
650
  res.write(`data: ${JSON.stringify({ type: "workspace_changed", ...workspaceChangedEventData(ev) })}\n\n`);
352
651
  }
353
652
  else if (ev.type === "text_delta" || ev.type === "turn_end" || ev.type === "message_committed" || ev.type === "context_usage") {
653
+ // 裸 catch-all 白名单化收官(probe-wire-1 表征钉的根治半场):最后四个已知臂离开
654
+ // `JSON.stringify(ev)`。**零字节变化**白名单——已知键(含 E2 四 identity 键:live 现状
655
+ // 带全四键,消费端可能已依赖,与 durable builder 的「刻意排除」是两腿两策)逐一挑,
656
+ // 只挡 core 未来新键的裸穿透(structured 在 1.203 就真穿透过本腿)。text_delta 的 delta
657
+ // 不做内容变换:逐 token 片段上 redactSecrets 对跨 chunk 的秘密无效还破流式——durable
658
+ // 腿攒批后 redact 是那半的正解,两腿口径注在 ledger-sink.flush。catch-all(下方 else)
659
+ // 从此只剩**真未知臂**:L1 臂集门(core-keyset-guard ⑤)在 core 加臂时 tsc 红逼显式处置。
354
660
  const e = ev;
355
661
  const ident = {
356
662
  ...(e.eventId !== undefined ? { eventId: e.eventId } : {}),
@@ -373,35 +679,78 @@ async function handleTasksBody(req, res, url, ctx, miss) {
373
679
  }
374
680
  });
375
681
  };
682
+ // E23 + §4④: wrap the synchronous stream in the per-run elicitation AND AskUserQuestion contexts (nested
683
+ // — each uses its own ALS). emit = a named SSE frame on THIS open connection; taskId keys each throttle
684
+ // (the durable run id, else a synthetic internal id); owner gates the respond (the VERIFIED principal —
685
+ // NOT the cost-attribution `principal`, which is the spoofable header and null on a direct door → would
686
+ // park owner=null and let any tenant answer); ac.signal cancels a parked ask on client disconnect. Each
687
+ // wrap is a no-op when its coordinator is off; the two share the identical id/owner/emit/signal.
376
688
  const askTaskId = durableTaskId ?? uuidv7();
377
689
  const askOwner = gatedPrincipal(req, deps.config) ?? null;
378
690
  const emitAsk = (frame) => {
379
691
  if (!res.writableEnded)
380
692
  res.write(`event: ${frame.type}\ndata: ${JSON.stringify(frame)}\n\n`);
381
693
  };
694
+ // P1 ①②: this new turn is a stream-open for the session — push any async-workflow completions that
695
+ // finished since (frames ride the SAME connection, before the run's own events; the shell dedups by
696
+ // runId). `askOwner` = the VERIFIED principal → only this caller's own completions are emitted.
697
+ // 🔒 THROW on a dead/ended stream instead of emitAsk's silent no-op (1.80 review: a disconnect
698
+ // between request-send and this drain made the two-phase ack a FALSE success — the entry was
699
+ // removed with no frame ever written; throwing keeps it pending for the next leg). Only THIS
700
+ // drain wrapper throws — the shared emitAsk (question/elicit frames) keeps its no-op semantics.
701
+ // 🔴 drain 案: write these as ANONYMOUS `data:` frames, NOT emitAsk's NAMED
702
+ // `event: <type>` form — the run's own live-leg task_notification frames on this SAME connection
703
+ // are anonymous (L1392/L1474), so a shell parsing the default message channel silently never saw
704
+ // a drained frame (the Monitor "park✓ drain✗" fingerprint). question/elicit stay on emitAsk
705
+ // (their own established named-event contract).
382
706
  await emitPendingWorkflowCompletions(deps.workflowCompletionInbox, prepared.spec.sessionId, askOwner, (frame) => {
383
707
  if (closed || res.writableEnded || res.destroyed)
384
708
  throw new Error("stream ended before the completion frame was written");
709
+ // core [784]④b: register the drained frame's key on THIS stream — the same idle-arrival was
710
+ // ALSO parked core-side (pendingSessionNotifications), and its pend-drain replay lands on this
711
+ // very stream as a task_notification event; the loop's key check folds that second wire copy.
712
+ // codex F1: resolved(true) — the res.write below is synchronous (the dead-stream guard above
713
+ // already threw). codex F2: task_type (restored from the entry extras) rides into the key —
714
+ // an external notify's caller-chosen task_id must not pre-claim an internal task's key.
385
715
  const f = frame;
386
716
  if (f.type === "task_notification" && f.task_id)
387
717
  syncNotifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), Promise.resolve(true));
388
718
  res.write(`data: ${JSON.stringify(frame)}\n\n`);
389
- }, { route: "sync-stream-open", connection: "live-sse", log: (m, x) => deps.logger?.info?.(m, x) });
719
+ },
720
+ // emit-target diagnostics — this drain rides the caller's OWN live SSE response.
721
+ { route: "sync-stream-open", connection: "live-sse", log: (m, x) => deps.logger?.info?.(m, x) });
390
722
  const withElicit = deps.elicitation
391
723
  ? () => deps.elicitation.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitAsk, abortSignal: ac.signal }, streamBody)
392
724
  : streamBody;
393
725
  const withQuestion = () => deps.question
394
726
  ? deps.question.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitAsk, abortSignal: ac.signal }, withElicit)
395
727
  : withElicit();
728
+ // [816]/[820]②: the live tool-approval context — SAME id/owner/emit/signal as question/elicit
729
+ // (nested ALS, each its own store). sessionId keys the "allow all edits this session" state.
730
+ // [1535] 正名:本 sync 腿仍是唯一 runWithContext 包裹,但不再是子代唯一到达路——下方
731
+ // spec.onAsk 的 bound 闭包经 core 继承链给了 bg/嵌套子代直达宿主流的路;真正无宿主流的
732
+ // 残余腿(durable-submit/headless resume)才落 "unavailable" → durable park / core deny。
733
+ // 🔒 approval 帧不用共享 emitAsk 的静默 no-op 语义:res 已结束时 no-op = 卡「假投递成功」,
734
+ // coordinator 等满 TTL 落 deny —— G1 "unavailable"→park 腿在真 wiring 里就不可达(cross-review
735
+ // 抓获)。这里 THROW,coordinator 的 emit-catch 按「无活人可达」返 "unavailable"(durable 部署
736
+ // park;非 durable 由 core fail-closed deny,行为不劣于旧 no-op)。仅 approval 帧收紧;question/
737
+ // elicit 的共享 emitAsk 保持 no-op(它们各有 TTL/abort 语义,另议)。
396
738
  const emitApproval = (frame) => {
397
739
  if (res.writableEnded || res.destroyed)
398
740
  throw new Error("live stream ended — approval card undeliverable");
399
741
  emitAsk(frame);
400
742
  };
743
+ // [1535] server 半场([1539] 定谳断点①的修):同一 ctx 双装——ALS 包裹(宿主自身 ask 的
744
+ // 既有路)+ **spec.onAsk = boundAsk(ctx)**(core 1.294 继承链 parentConstraints 冻结
745
+ // `spec.onAsk ?? deps.onAsk` 逐 ancestor 传给每个委派子代:bg 子代 lane 无 ALS ctx 时不再
746
+ // 「unavailable/零帧 fail-closed deny」,权限卡带 sourceTaskId 浮到宿主本流)。suspendAsk
747
+ // 退位条件的「onAsk 在场」真值不变(deps.onAsk 恒 wire),durable 部署行为零漂移。
401
748
  const approvalCtx = deps.toolApproval
402
749
  ? { taskId: askTaskId, owner: askOwner, emit: emitApproval, abortSignal: ac.signal, ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}) }
403
750
  : undefined;
404
751
  if (approvalCtx && deps.toolApproval) {
752
+ // [1546] HIGH-1:闭包只携身份——emit 经 broker 取「当前」活跃流(宿主重连的新 SSE 在
753
+ // runWithContext 注册时覆盖指针),internalsSnapshot 冻结的闭包不再携死 response/死 signal。
405
754
  prepared.spec.onAsk = deps.toolApproval.boundAsk({
406
755
  owner: askOwner,
407
756
  taskId: askTaskId,
@@ -409,6 +758,10 @@ async function handleTasksBody(req, res, url, ctx, miss) {
409
758
  });
410
759
  }
411
760
  const withApproval = () => approvalCtx && deps.toolApproval ? deps.toolApproval.runWithContext(approvalCtx, withQuestion) : withQuestion();
761
+ // SendUserFile:sync 腿 file_link 走 live out-of-band 帧面(emitAsk)。detach 车道([1840]§五
762
+ // file_link 修):**同落账**——断连后 live 面写在已死 socket 上被丢,壳换道 events tail 必须
763
+ // 能拿到文件卡(bg 腿本就 durable append,双腿对齐);live+durable 双写与 text/tool 帧的换道
764
+ // 语义同栏(壳按换道协议处理重放)。非 detach 车道 ledgerSink 恒 undefined = live-only 字节不变。
412
765
  await (deps.sendUserFile
413
766
  ? deps.sendUserFile.runWithContext({
414
767
  taskId: askTaskId,
@@ -416,33 +769,50 @@ async function handleTasksBody(req, res, url, ctx, miss) {
416
769
  void (await emitAsk(f));
417
770
  if (ledgerSink) {
418
771
  const { type, ...rest } = f;
419
- await ledgerSink.append(type, rest).catch(() => undefined);
772
+ await ledgerSink.append(type, rest).catch(() => undefined); // 账本瞬断不 fault 工具面(live 帧已出)
420
773
  }
421
774
  },
422
775
  }, withApproval)
423
776
  : withApproval());
777
+ // Drive the parked run row to its terminal/suspended state (releases or keeps task_active).
424
778
  if (durableTaskId && deps.runStore) {
779
+ // R8 decouple defense-in-depth (review LOW): only KEEP the lock (setSuspended/needs_review) when
780
+ // checkpointStore exists — without it there's no resume ctx, so a suspend (unreachable per durableEnabled,
781
+ // but fail-safe matters) FALLS THROUGH to setTerminal which RELEASES the claim. Mirrors the sync path.
425
782
  if (finalResult?.status === "suspended" && deps.checkpointStore)
426
783
  await deps.runStore.setSuspended(durableTaskId);
427
784
  else if (finalResult?.status === "needs_review" && deps.checkpointStore)
428
- await deps.runStore.setNeedsReview(durableTaskId);
785
+ await deps.runStore.setNeedsReview(durableTaskId); // D-B: park a review pause, keep the lock (resumable via /plan_review)
429
786
  else if (finalResult)
430
787
  await deps.runStore.setTerminal(durableTaskId, finalResult.status, finalResult, finalResult.errorMessage ?? null);
431
788
  else {
789
+ // No done event observed — the client disconnected (closed → break) and ac.abort() killed the
790
+ // run. Race guard: the task may have SUSPENDED right before the abort (checkpoint already
791
+ // persisted, its done event unread) — a pending checkpoint outlives the abort, so park the row
792
+ // suspended (session stays locked for the operator's /decide). Otherwise record an honest
793
+ // failure: never "completed" for a run whose terminal state was never seen.
794
+ // R8: anchor the rewind handle even on disconnect — if the user-message entryId was committed
795
+ // before the Ctrl+C, the shell (holding the early run_started taskId) can still rewind to this turn.
432
796
  putRewindAnchor();
433
797
  const pending = await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId);
434
798
  if (pending) {
799
+ // [1.211 codex M1] detach 账本补 park 终局帧(done 未读到但 checkpoint 已在——账本没有
800
+ // suspended 帧则 events tail 无终局,壳等空;gate 内容未读到 done 无从知,落 gate:null)。
435
801
  if (ledgerSink && !ledgerTerminal) {
436
802
  ledgerTerminal = true;
437
803
  await ledgerSink.flush().catch(() => undefined);
438
804
  await ledgerSink.appendParked("suspended", {}).catch(() => undefined);
439
805
  }
440
806
  await deps.runStore.setSuspended(durableTaskId);
441
- settleFleet("suspended");
807
+ settleFleet("suspended"); // parked → stays in fleet as "waiting"
442
808
  }
443
809
  else {
810
+ // codex L1 对齐:同一类用户中止(disconnect abort)不再因 iterator 正常 return / throw 而给
811
+ // 两种终局形——与 throw 路兜底、resume-abort settle 同为 errorCode "cancelled" 的取消终局。
444
812
  const dcErr = "stream client disconnected before completion (run aborted)";
445
813
  const dc = { taskId: durableTaskId, sessionId: prepared.spec.sessionId ?? "", status: "failed", errorCode: "cancelled", errorMessage: dcErr, stats: { turns: 0, tokens: 0 } };
814
+ // [900]① detach 车道:账本也要有终局帧(bg 腿 cancelled 终局同形;detach 下本分支仅在
815
+ // generator 无 done 早收/cancel-verb 干净收尾时可达)。best-effort:落账失败不改行既有 settle。
446
816
  if (ledgerSink && !ledgerTerminal) {
447
817
  ledgerTerminal = true;
448
818
  await ledgerSink.flush().catch(() => undefined);
@@ -452,46 +822,66 @@ async function handleTasksBody(req, res, url, ctx, miss) {
452
822
  settleFleet("failed");
453
823
  }
454
824
  }
455
- runRowSettled = true;
825
+ runRowSettled = true; // [868]③: every branch above recorded the row — the finally backstop stands down
456
826
  }
457
827
  }
458
828
  finally {
459
- syncLegLive = false;
829
+ syncLegLive = false; // notifications from here on take the durable-inbox path
460
830
  if (durableHeartbeat)
461
831
  clearInterval(durableHeartbeat);
832
+ // 1.207: drop the live verb handle — IDENTITY-GUARDED (review LOW): a suspend→fast-resume can
833
+ // re-register the SAME taskId from the resume leg before this finally runs; an unconditional
834
+ // delete would clobber the fresh handle (verbs would 409 "another replica" until re-registration).
462
835
  if (durableTaskId && steerableRuns.get(durableTaskId) === liveStreamRef)
463
836
  steerableRuns.delete(durableTaskId);
837
+ // [888]① same identity guard for the cancel handle (a fast-resume registers its own controller).
464
838
  if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
465
839
  inflightRuns.delete(durableTaskId);
466
- cancelledViaVerb.delete(durableTaskId);
840
+ cancelledViaVerb.delete(durableTaskId); // H2 label lifecycle rides the registration
467
841
  }
468
842
  for (const evict of subagentHandleEvictions)
469
- evict();
843
+ evict(); // design/122 ③: leg-end handle eviction (identity-guarded)
844
+ // [868]③: the DURABLE twin of leak C — a throw out of the stream (abort mid-iteration / plain
845
+ // Error) also skipped the run-row settle block, leaving a `running` zombie that only reapStale
846
+ // would flip (after runStaleSec, as a misleading "run stalled (instance lost?)"). Settle it here:
847
+ // re-read the row (a concurrent path may have settled it — then this is a no-op), honour a
848
+ // suspend that raced the abort (pending checkpoint ⇒ park, the disconnect branch's exact guard),
849
+ // else fail it — errorCode "cancelled" when the leg was aborted (user kill / disconnect), bare
850
+ // failed for a genuine mid-stream error. Best-effort: the backstop must never mask the real throw.
851
+ // 🔴 runs BEFORE the leak-C fleet backstop below (codex M3): the run-row verdict here (park vs
852
+ // fail) must drive the fleet verdict — the old order settled the fleet row "failed" first and a
853
+ // subsequently PARKED run row contradicted it ("waiting" run under a failed fleet row).
470
854
  if (!runRowSettled && durableTaskId && deps.runStore) {
471
855
  try {
472
856
  const row = await deps.runStore.getRun(durableTaskId);
473
857
  if (row?.status === "running") {
474
858
  const pending = prepared.spec.sessionId ? await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId) : undefined;
475
859
  if (pending) {
860
+ // [1.211 codex M1] detach 账本补 park 终局帧(finally 兜底路同款,gate 未知落 null)。
476
861
  if (ledgerSink && !ledgerTerminal) {
477
862
  ledgerTerminal = true;
478
863
  await ledgerSink.flush().catch(() => undefined);
479
864
  await ledgerSink.appendParked("suspended", {}).catch(() => undefined);
480
865
  }
481
866
  await deps.runStore.setSuspended(durableTaskId);
482
- settleFleet("suspended");
867
+ settleFleet("suspended"); // parked → the fleet row stays honest ("waiting", disconnect-branch parity)
483
868
  }
484
869
  else if (finalResult) {
870
+ // [1.211 codex H1] done 已观测(账本/fleet 可能已按它落)但行仍 running = 首次 setTerminal
871
+ // 抖动——兜底必须重试「原终局」,绝不写 failed(否则账本 done+fleet completed+行 failed
872
+ // 三面矛盾;bg 腿 `reached` 重试同款纪律)。账本侧若 onDone 也没落成,这里补一发。
485
873
  if (ledgerSink && !ledgerTerminal) {
486
874
  ledgerTerminal = true;
487
875
  await ledgerSink.onDone(finalResult).catch(() => undefined);
488
876
  }
489
877
  await deps.runStore.setTerminal(durableTaskId, finalResult.status, finalResult, finalResult.errorMessage ?? null);
490
- settleFleet(finalResult.status, fleetRunResiduals(finalResult));
878
+ settleFleet(finalResult.status, fleetRunResiduals(finalResult)); // once-guard 下通常 no-op;[1839]①/(a) 残局键同带
491
879
  }
492
880
  else if (ac.signal.aborted) {
493
881
  const err = "stream aborted before completion (run cancelled)";
494
882
  const c = { taskId: durableTaskId, sessionId: prepared.spec.sessionId ?? "", status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 } };
883
+ // [900]① detach 车道:throw 路兜底同样补账本终局帧(bg 腿 catch 路 append("failed") 同形;
884
+ // ledgerTerminal 防对已落 done/park 的账本覆写 failed —— bg 腿 `reached` 同款纪律)。
495
885
  if (ledgerSink && !ledgerTerminal) {
496
886
  ledgerTerminal = true;
497
887
  await ledgerSink.flush().catch(() => undefined);
@@ -511,17 +901,28 @@ async function handleTasksBody(req, res, url, ctx, miss) {
511
901
  }
512
902
  }
513
903
  catch {
904
+ /* best-effort — reapStale remains the last-resort net; the original throw stays the story */
514
905
  }
515
906
  }
907
+ // C: a plain Error thrown mid-stream skipped every onTerminal branch above — settle the leaked
908
+ // "running" row to "failed" before the throw propagates (no-op when any branch — incl. the [868]③
909
+ // park above — already settled; settleFleet is once-guarded).
516
910
  settleFleet("failed");
517
911
  }
518
912
  return { status: 200, body: finalResult };
519
913
  };
914
+ // B3 复审(codex HIGH):无 runStore 的部署 durableTaskId 恒缺 → 这条活流从不进 steerableRuns,
915
+ // inflight() 全程读 0,SIGHUP 空闲窗(以及 SIGTERM drain)会把它当空闲斩掉。整腿显式计数,
916
+ // 与 steerableRuns 的短暂双计无害(两者同在终局递减,>0 语义只看非零)。
520
917
  counters.uncountedBillableInflight++;
521
918
  try {
522
- resp = await idemCache.run(idemKey, runStreamSubmitLeg, (r) => r.body?.status === "completed");
919
+ resp = await idemCache.run(idemKey, runStreamSubmitLeg,
920
+ // Cache only a clean completion; a failed/aborted stream is evicted so a retry re-runs.
921
+ (r) => r.body?.status === "completed");
523
922
  }
524
923
  catch (e) {
924
+ // Log unconditionally (don't silently drop a runner error that races with the client close — council);
925
+ // re-throw only while the client is still connected so the outer handler can surface it.
525
926
  deps.logger?.error("stream_error", { closed, err: e instanceof Error ? e.message : String(e) });
526
927
  if (!closed)
527
928
  throw e;
@@ -529,40 +930,67 @@ async function handleTasksBody(req, res, url, ctx, miss) {
529
930
  finally {
530
931
  counters.uncountedBillableInflight--;
531
932
  clearInterval(hb);
933
+ // E8 leak fence(双腿对齐的对偶):detach 腿注册过的归因键在 leg 收尾必 clear——终局 attach 已
934
+ // drain 过时这是幂等 no-op;errored/aborted leg 则丢掉未 drain 的累积(bg 腿 finally 同款)。
532
935
  if (usageKey)
533
936
  deps.modelUsage?.clear(usageKey);
534
937
  }
938
+ // A concurrent caller the cache deduplicated (ranLive=false) received no live events — hand it the terminal.
535
939
  if (!ranLive && resp && !res.writableEnded) {
536
940
  res.write(`data: ${JSON.stringify({ type: "done", result: resp.body, replay: true })}\n\n`);
537
941
  }
538
942
  res.end();
539
943
  }
540
944
  else {
945
+ // Idempotency-Key dedup (center blocker: exactly-once billing) — concurrent + retried same-key submits
946
+ // share one run; the whole run+record is wrapped so metrics/cost are counted exactly once on a replay.
947
+ // ── B3 ── 同上:原文此处是同步腿的 **128 行匿名 async**,提为具名闭包。
541
948
  const runSyncSubmitLeg = async () => {
949
+ // Durable F4/ask on the sync path — same two things as the stream path (audit functional gap):
950
+ // 1. createRun a task_active claim at SUBMIT (+ heartbeat) so reapCtx's task_active guard protects
951
+ // the ctx for the WHOLE first leg (its updated_at is stamped at submit and never refreshed; a
952
+ // multi-minute leg would otherwise age it past REAP_RUN_STALE_SEC and get it reaped MID-RUN).
953
+ // setSuspended on suspend keeps the claim (session locked submit→resume); setTerminal releases it.
954
+ // 2. putCtx AFTER the claim → a suspend here is resumable on any replica. Claim-first is load-bearing
955
+ // (parity with /v1/runs): a lost claim is a 409, never a silent second leg on a locked session —
956
+ // and a rejected submit must not clobber the active run's resume ctx (putCtx is sessionId-keyed).
542
957
  let durableTaskId;
543
958
  let durableHeartbeat;
959
+ // R8 (CC parity): the run-row claim + `durableTaskId` (the REWIND anchor key + the unique result.taskId
960
+ // re-stamp) is gated on `runStore` ALONE; the durable-suspend resume ctx (`putCtx`) on `checkpointStore`. These
961
+ // were bundled under ONE checkpointStore gate, which over-coupled REWIND to DURABLE_APPROVAL — but E18 resume-at
962
+ // needs NO checkpoint (main.ts:815). Now matches the already-decoupled /v1/runs bg path: rewind works with just
963
+ // runStore (DB_BACKEND=tidb/pg), no approval. Claim-first preserved (a lost claim → 409, never a silent 2nd leg).
544
964
  if (deps.runStore && prepared.spec.sessionId) {
545
965
  const tid = uuidv7();
546
966
  const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
547
967
  if (!created.ok)
548
968
  return { status: 409, body: { error: "session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)", errorCode: "conflict.session_active_run", activeTaskId: created.activeTaskId } };
549
- deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective);
969
+ deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective); // fire-and-forget session auto-title
550
970
  durableTaskId = tid;
551
971
  }
552
972
  if (deps.checkpointStore && prepared.spec.sessionId) {
973
+ // putCtx AFTER the claim (resumable). If this throws, the claim row self-heals: no heartbeat yet → reapStale flips it failed + releases.
553
974
  await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
554
975
  }
976
+ // [888]① interactive live-run cancel, non-stream twin: this leg previously ran with NO abort seam at
977
+ // all (bare runTask, no spec.signal) — a cancel flipped the durable flag nothing consumed. Wire a
978
+ // controller through spec.signal (same seam as the stream leg's disconnect-kill) + register it in the
979
+ // in-flight map so the cancel fast path lands. Children reaped by this abort are a USER stop (parity
980
+ // with the stream leg's disconnect marker).
555
981
  const syncCancelCtrl = new AbortController();
556
982
  if (prepared.spec.sessionId)
557
983
  markChildrenStoppedByUserOnAbort(syncCancelCtrl.signal, prepared.spec.sessionId, prepared.auth?.principal);
558
984
  if (durableTaskId && deps.runStore) {
559
985
  const rs = deps.runStore;
560
986
  const tid = durableTaskId;
561
- const owner = principal ?? null;
987
+ const owner = principal ?? null; // run created with this owner — pass it as the single-DB null-safe heartbeat guard
562
988
  const abortFromVerb = () => {
563
- cancelledViaVerb.add(tid);
989
+ cancelledViaVerb.add(tid); // H2: label the settle (errorCode "cancelled")
564
990
  syncCancelCtrl.abort();
565
991
  };
992
+ // [1.207 codex H1] heartbeat ALSO polls the durable cancel flag (bg-leg parity) — cross-replica
993
+ // cancel only sets the flag; this leg previously had no consumer.
566
994
  durableHeartbeat = setInterval(() => {
567
995
  void rs.heartbeat(tid, owner).catch(() => undefined);
568
996
  if (!syncCancelCtrl.signal.aborted) {
@@ -572,6 +1000,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
572
1000
  }, HEARTBEAT_MS);
573
1001
  durableHeartbeat.unref?.();
574
1002
  inflightRuns.set(tid, syncCancelCtrl);
1003
+ // [1.207 codex H1] registration-window catch-up (flag flipped during the awaited putCtx window).
575
1004
  void Promise.resolve(rs.isCancelRequested?.(tid, owner)).then((c) => { if (c && !syncCancelCtrl.signal.aborted)
576
1005
  abortFromVerb(); }).catch(() => undefined);
577
1006
  }
@@ -579,10 +1008,21 @@ async function handleTasksBody(req, res, url, ctx, miss) {
579
1008
  inflightRuns.set(durableTaskId, syncCancelCtrl);
580
1009
  }
581
1010
  const specWithSignal = { ...prepared.spec, signal: syncCancelCtrl.signal };
1011
+ // Developer mode (1.44): verify = adversarial verify→fix gate. Cascade (1.45): cheap→strong ladder,
1012
+ // escalate on a failed gate (default = didn't complete). Both opt-in, mutually exclusive, multi-attempt.
582
1013
  let result;
583
- let cancelVerbLabel = false;
584
- counters.uncountedBillableInflight++;
1014
+ let cancelVerbLabel = false; // [1.207 codex H2] set in the finally below while the label entry is alive
1015
+ // E8 (shell-host contract): the SYNCHRONOUS /v1/tasks path is INTENTIONALLY excluded from the per-task `modelUsage`
1016
+ // echo — it does NOT register() with the ModelUsageTracker, so the register-gate makes its brain.call usage a
1017
+ // no-op (NOT a leak; the register-gate prevents the prior unbounded growth). modelUsage is the
1018
+ // async durable /v1/runs feature (cap `modelUsage` is gated on the run store + tracker); a sync caller gets
1019
+ // its result synchronously and reads aggregate cost from stats. (Wiring sync = register + a withModelUsage
1020
+ // attach pass; deferred as low-value for the synchronous path.)
1021
+ counters.uncountedBillableInflight++; // B-1: the sync await is real model work — drain must see it
585
1022
  try {
1023
+ // ── B3 ── 原文此处是一处三层三元,把三条模型驱动腿挤在五行里。三腿具名化后判别顺序
1024
+ // (verify → cascade → plain)、短路语义、传参**逐字不变**;`runWithVerification` 仍是整对象
1025
+ // 直传(快审 F1:cost 顶不在重建中丢失),`runCascade` 仍从操作员梯子 + 本任务预算现算配置。
586
1026
  const verifyLeg = (v) => runWithVerification(deps.runner, specWithSignal, v);
587
1027
  const cascadeLeg = () => runCascade(deps.runner, specWithSignal, cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd));
588
1028
  const plainLeg = () => deps.runner.runTask(specWithSignal);
@@ -593,36 +1033,59 @@ async function handleTasksBody(req, res, url, ctx, miss) {
593
1033
  if (durableHeartbeat)
594
1034
  clearInterval(durableHeartbeat);
595
1035
  if (durableTaskId && inflightRuns.get(durableTaskId) === syncCancelCtrl) {
596
- inflightRuns.delete(durableTaskId);
597
- cancelVerbLabel = cancelledViaVerb.has(durableTaskId);
1036
+ inflightRuns.delete(durableTaskId); // [888]① identity-guarded
1037
+ cancelVerbLabel = cancelledViaVerb.has(durableTaskId); // H2: capture before dropping (projection below)
598
1038
  cancelledViaVerb.delete(durableTaskId);
599
1039
  }
600
1040
  }
1041
+ // [1.207 codex H2] cancel-verb abort label (non-stream twin of the SSE done-frame projection):
1042
+ // core's external-signal abort yields failed WITHOUT an errorCode — label it so /cancel's outcome is
1043
+ // uniform across legs. Only an abort-shaped FAILED result; a completion that raced the abort stands.
601
1044
  if (cancelVerbLabel && result.status === "failed" && !result.errorCode) {
602
1045
  result = { ...result, errorCode: "cancelled" };
603
1046
  }
1047
+ // E18: a resume-at caller error (a stale/non-message anchor target) comes back as a failed result carrying a
1048
+ // resume_at.* code — a caller mistake that BILLED NOTHING. Handle it BEFORE the suspend/terminal/finalize
1049
+ // logic below: release the submit-time claim (unlock the session) but do NOT finalizeTaskResult (it must not
1050
+ // count as a billed failure / plan-cache miss), and return a 4xx (caller fault) instead of a 200-with-failed
1051
+ // body. NOT idem-cached (the predicate below caches only success-range responses) — a fixed anchor can retry.
604
1052
  const resumeAtStatus = resumeAtHttpStatus(result);
605
1053
  if (resumeAtStatus) {
606
1054
  if (durableTaskId && deps.runStore)
607
1055
  await deps.runStore.setTerminal(durableTaskId, "failed", stripCheckpointToken(result), result.errorMessage ?? null);
608
1056
  return { status: resumeAtStatus, body: { errorCode: result.errorCode, error: result.errorMessage ?? "resume-at failed" } };
609
1057
  }
1058
+ // Durable suspend on the sync path: PARK it as an async run + return a pollable taskId, NEVER the
1059
+ // capability token. The submitter switches to GET /v1/runs/:id. Cost is recorded
1060
+ // by the RESUMED run (mirrors runInBackground, which also parks-without-record on suspend) — not here,
1061
+ // so the pre-suspend tokens aren't double-counted on resume.
610
1062
  if (result.status === "suspended" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
1063
+ // The submit-time row — guaranteed when runStore is present (a lost claim was 409'd before running).
611
1064
  if (durableTaskId)
612
1065
  await deps.runStore.setSuspended(durableTaskId);
613
1066
  return { status: 200, body: { taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "suspended" } };
614
1067
  }
1068
+ // design/80 D-B: a review pause (plan_review / dry_run_review) on the sync path — PARK it like a suspend
1069
+ // (keep task_active) + return a pollable taskId so the /v1/assistant/tasks/:id/plan_review resume can
1070
+ // claim it. NEVER the capability token (parity with the suspended branch above).
615
1071
  if (result.status === "needs_review" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
616
1072
  if (durableTaskId)
617
1073
  await deps.runStore.setNeedsReview(durableTaskId);
618
1074
  return { status: 200, body: { taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "needs_review" } };
619
1075
  }
1076
+ // Not suspended → release the submit-time claim so the session unlocks + the ctx becomes reapable.
620
1077
  if (durableTaskId && deps.runStore)
621
1078
  await deps.runStore.setTerminal(durableTaskId, result.status, stripCheckpointToken(result), result.errorMessage ?? null);
1079
+ // A completed OR failed result both BILLED → cache it so a retry replays the outcome (no double-bill);
1080
+ // a thrown error / transient conflict is evicted by the cache so a genuine retry re-runs. (The async
1081
+ // /v1/runs caches only its 202 receipt — a different object, queried separately — hence the asymmetry.)
622
1082
  finalizeTaskResult(result, principal, prepared.spec.objective, prepared.spec.sessionId);
1083
+ // A verify/cascade NESTED suspend is mapped by core to status:"failed" while still carrying the
1084
+ // capability token (the genuine top-level suspend is parked above and never reaches here). Strip it so
1085
+ // the resume credential never goes in the response body OR the idempotency cache.
623
1086
  return { status: 200, body: stripCheckpointToken(result) };
624
1087
  };
625
- const resp = await idemCache.run(idemKey, runSyncSubmitLeg, (r) => r.status < 400);
1088
+ const resp = await idemCache.run(idemKey, runSyncSubmitLeg, (r) => r.status < 400); // cache only success-range outcomes (a completed/failed BILLED result, replay to avoid double-bill); never a session-active 409 or a resume-at 4xx (billed nothing — a retry must re-check)
626
1089
  sendJson(res, resp.status, resp.body);
627
1090
  }
628
1091
  return;