@sema-agent/server 1.322.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 +59 -3
  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
package/dist/runs.js CHANGED
@@ -6,6 +6,44 @@ import { createLedgerSink } from "./trace/ledger-sink.js";
6
6
  import { fleetRunResiduals } from "./fleet/fleet-bus.js";
7
7
  import { defaultSubagentTailBus, projectTailFrame } from "./fleet/subagent-tail-bus.js";
8
8
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "./orchestration/workflow-completion-inbox.js";
9
+ /**
10
+ * stoppedBy (core 1.252): a service cancel is a USER stop — mark the run's still-running background
11
+ * children BEFORE core's teardown reaps them (bare abort = attribution falls back to "system"; core's own
12
+ * reap marks "parent", but first-marker-wins means our earlier "user" is the one that lands). Register this
13
+ * FIRST on the cancel signal so it runs ahead of any later-registered core listener (listener-order
14
+ * guarantee); one registration covers every abort route on that signal (same-replica fast path + the
15
+ * cross-replica flag poll). Advisory only — the cancel itself must never fail on attribution.
16
+ *
17
+ * Access = TASK-scoped children ONLY: owner = the parent taskId (on a RESUME leg: the sessionId — core's
18
+ * canonical taskId there), scope = `principal ?? "default"` (core registers children under
19
+ * `spec.principal ?? "default"`, so an anonymous run's children live in scope "default" and an access
20
+ * WITHOUT scope matched nothing). Session-scoped children are DELIBERATELY not marked: a cancelled parent
21
+ * does NOT take them down (CC Backgrounded semantics — core's teardown reaps with skipSessionScoped), so
22
+ * pre-marking them "user" would misattribute a stop that isn't happening.
23
+ *
24
+ * core 1.256: `markStopSourceForOwner` replaces the old `list()+markStopSource`
25
+ * per-row loop, closing BOTH documented holes — ① the EXPLICIT `sessionScoped` flag
26
+ * (`skipSessionScoped:true`) makes the resume leg safe (its owner key == sessionId COINCIDES with
27
+ * session-scoped children's; the old code couldn't tell them apart and took an honest degrade instead),
28
+ * ② a direct handle-map walk (no `list()` 500-row display cap under-marking >500-children runs). Covers
29
+ * core's full markable set (background_agent / background_bash / monitor); running-only +
30
+ * first-marker-wins guards live inside the seam.
31
+ */
32
+ /** [1488]③(b) + codex R2: the bg agent-handle OUTPUT read, kind-gated BEFORE any poll. `pollTask` is the
33
+ * generic registry face and polling is NOT side-effect-free for other kinds — a terminal workflow poll
34
+ * fires `onServedTerminal` (acknowledges the completion as served → suppresses its notification) and a
35
+ * bash poll advances the output cursor. `getAccessibleTask` is a pure lookup: resolve + access-check +
36
+ * type-check FIRST; only a `background_agent` row is ever polled (non-blocking). Everything else returns
37
+ * the registry's own not_found shape, indistinguishable from an unknown handle (the route 404s it).
38
+ *
39
+ * 1.250 durable arm (core 1.364, [1511] follow-up delivered): a FULL in-process miss (`!row`) falls
40
+ * through to `pollTask(…, { agentStore })` — core's durable arm only fires for `a*`-shaped handles
41
+ * (DURABLE_AGENT_HANDLE_RE) behind `canAccessAgentRecord`, and with NO in-process handle the live
42
+ * workflow/bash branches are unreachable, so the kind-gate's reason (onServedTerminal / cursor advance)
43
+ * cannot trigger. Terminal rows serve the durable snapshot cross-instance/post-restart; a foreign-writer
44
+ * running row answers the honest "outcome unknown here"; anything else is core's own not_found (byte-same
45
+ * route 404 as today). A row that IS in-process but of the wrong type stays a hard 404 — the process
46
+ * knows the truth, falling back would sidestep the kind gate. */
9
47
  export async function backgroundAgentOutput(registry, handle, access, agentStore) {
10
48
  const row = registry.getAccessibleTask(handle, access);
11
49
  if (!row || row.type !== "background_agent") {
@@ -15,20 +53,54 @@ export async function backgroundAgentOutput(registry, handle, access, agentStore
15
53
  }
16
54
  return registry.pollTask(handle, access);
17
55
  }
56
+ /** [1499] CC TaskOutput 人侧对位 — the GENERIC task-handle read face (b* bash stdout — NB whether a read
57
+ * consumes the output cursor depends on the handle's shape: spooled handles re-read in full [GET-idempotent],
58
+ * cursor-only handles return new bytes per read; the projection's flags are authoritative; m* monitor
59
+ * batches; a* agent final report). No filter option: caller-supplied regexes are refused at the route (a
60
+ * backtracking engine on the shared event loop must not run wire-supplied patterns; client-side filtering
61
+ * sees only the clipped projection — full-fidelity filtered reads stay an in-engine tool capability).
62
+ * Addressing is exact task_id only (the tools' name/legacy-id resolution stays off the wire). Kind-gate
63
+ * FIRST on the pure getAccessibleTask (codex-R2 shape — pollTask is not side-effect-free): `workflow` is
64
+ * refused — its HTTP read face is the workflow journal ([1491] contract), and a pollTask here would fire
65
+ * onServedTerminal (ack the completion as served) and suppress the workflow completion push. Refusals
66
+ * return the registry's own not_found shape, indistinguishable from an unknown handle (the route 404s it). */
18
67
  const TASK_HANDLE_KINDS = new Set(["background_bash", "monitor", "background_agent"]);
68
+ /** 1.250 复审 F1(HIGH):durable 回落的**形判门**——`getAccessibleTask` 是精确键(不 trim、不查 legacy
69
+ * 别名),而 core `pollTask/stopTask` 是通用解析面(先 trim、`resolveInternal` 含 legacyToTaskId、
70
+ * stopTask 还有 byName 臂)。裸 `!row` 回落会让垫充空白句柄(" "+wfId)/legacy shellId/agent 名字从
71
+ * 回落缝穿进 live 分支——kind 门要防的 onServedTerminal/游标消费/按名 stop 全部复活(复审实测复现)。
72
+ * 只有 **a\* 精确形**才回落:`DURABLE_AGENT_HANDLE_RE` = core 公共导出(core 1.369,[1518] ask 即答
73
+ * ——两边判词同源,镜像私拷已删)。残余(documented):恰为 a\* 形的 agent **名字**
74
+ * 在无 durable 行时仍会进 core stopTask 的 byName 臂——同 access 元组内、名字域与句柄域同形的病态窄面,
75
+ * 接受。 */
19
76
  const DURABLE_AGENT_HANDLE_SHAPE = DURABLE_AGENT_HANDLE_RE;
20
77
  export async function taskHandleOutput(registry, handle, access, agentStore) {
21
78
  const row = registry.getAccessibleTask(handle, access);
22
79
  if (!row || !TASK_HANDLE_KINDS.has(row.type)) {
80
+ // 1.250 durable arm — full in-process miss + a* 精确形才回落(F1 形判门,DURABLE_AGENT_HANDLE_SHAPE
81
+ // 顶注;b*/m* 无 durable 面照旧 not_found;进程内命中但型错 = 真相已知,不回落)。
23
82
  if (!row && agentStore && DURABLE_AGENT_HANDLE_SHAPE.test(handle))
24
83
  return registry.pollTask(handle, access, { agentStore });
25
84
  return { content: "", details: { task_id: handle, retrieval_status: "not_ready", error: "not_found" } };
26
85
  }
27
86
  return registry.pollTask(handle, access);
28
87
  }
88
+ /** [1499] CC TaskStop 人侧对位 — stop a background task handle (bash kill / monitor stop / agent abort).
89
+ * Same kind gate as the read face (a workflow stops via its own cancel face, never this verb). stopTask is
90
+ * idempotent on a terminal handle (the registry answers honestly); the stop is attributed "user" via the
91
+ * registry's existing markStopSource machinery (an HTTP stop is the human's hand — parity with the shell's
92
+ * TaskStop attribution). The pre-mark is first-write-wins and MUST come first (core's own stop path marks
93
+ * "parent" internally — marking after would lose the attribution race on success, the common path); the
94
+ * trade (codex R3) is that on a kill-that-didn't-land the pending "user" mark is not clearable through the
95
+ * public registry face — the route surfaces that arm as 409 stop.not_landed, and the atomic fix (a `source`
96
+ * option on stopTask) is a core ask. */
29
97
  export async function taskHandleStop(registry, handle, access, agentStore) {
30
98
  const row = registry.getAccessibleTask(handle, access);
31
99
  if (!row || !TASK_HANDLE_KINDS.has(row.type)) {
100
+ // 1.250 durable arm — core stopTask's agentStore branch answers honestly (terminal row: "already
101
+ // <status> — nothing to stop"; foreign-writer running row: not_local, no kill is attempted). NO
102
+ // markStopSource here: there is no in-process kill to attribute, and the durable answer never reads
103
+ // as a landed stop. F1 形判门同 output 面(stopTask 还有 byName 臂——非 a* 形一律不进通用解析面)。
32
104
  if (!row && agentStore && DURABLE_AGENT_HANDLE_SHAPE.test(handle))
33
105
  return registry.stopTask(handle, access, { agentStore });
34
106
  return { content: "", details: { task_id: handle, retrieval_status: "not_ready", error: "not_found" } };
@@ -42,9 +114,15 @@ export function markChildrenStoppedByUserOnAbort(signal, taskId, principal) {
42
114
  defaultTaskRegistry.markStopSourceForOwner({ owner: taskId, scope: principal ?? "default" }, "user", { skipSessionScoped: true });
43
115
  }
44
116
  catch {
117
+ /* attribution is advisory — the cancel itself must never fail on it */
45
118
  }
46
119
  }, { once: true });
47
120
  }
121
+ /**
122
+ * A session CAS conflict is swallowed by core into a failed `TaskResult` (it is not thrown from
123
+ * `runTaskStream`), so detect it from the result. Prefer the structured `errorCode` (core ≥1.8); fall
124
+ * back to the conflict wording only for older results without one.
125
+ */
48
126
  export function isSessionConflictResult(result) {
49
127
  if (result.status !== "failed")
50
128
  return false;
@@ -52,6 +130,23 @@ export function isSessionConflictResult(result) {
52
130
  return result.errorCode === "conflict";
53
131
  return /stale write|concurrent write|conflict/i.test(result.errorMessage ?? "");
54
132
  }
133
+ /**
134
+ * E18 resume-at CALLER error → an HTTP 4xx. A bad/stale resume-at target (the resolved entryId is gone or is not a
135
+ * settled message boundary) surfaces as a FAILED TaskResult carrying a `resume_at.*` errorCode (core swallows the
136
+ * prepare-throw into the result, exactly like a session conflict — runtask catch → status:"failed"+errorCode). The
137
+ * sync path otherwise 200s a failed result; map these to 4xx so a caller mistake reads as one. `not_found` (the
138
+ * entry is genuinely absent) → 404; everything else (not_a_message / conflicts_resume / no_session) → 422.
139
+ *
140
+ * design/114 Phase3: `requireExistingSession` on a genuinely-missing/purged session ALSO surfaces as a failed result
141
+ * (core prepare-task maps the store's `not_found` → `resume.session_not_found`); map it to 404 (the session is gone),
142
+ * the same "caller mistake reads as a 4xx, not a silent 200 fresh run" discipline.
143
+ *
144
+ * [833] rewind exclusive mode (core 1.292 resumeAtMode:"before"): its two edge rejections ride the same `resume_at.*`
145
+ * prefix (before_target_not_user / before_root_unsupported → 422 via the existing branch, errorCode passed through
146
+ * UNCHANGED for the shell). `rewind_snapshot.unresolvable` (rewindFiles + "before": no snapshot at/above the branch
147
+ * point) is core's third rejection with a DIFFERENT prefix — same caller-mistake shape (prepare-throw, nothing
148
+ * billed), map it to 422 explicitly so it doesn't fall through to a 200-with-failed body.
149
+ */
55
150
  export function resumeAtHttpStatus(result) {
56
151
  if (result.status !== "failed")
57
152
  return undefined;
@@ -63,31 +158,71 @@ export function resumeAtHttpStatus(result) {
63
158
  return undefined;
64
159
  return result.errorCode === "resume_at.not_found" ? 404 : 422;
65
160
  }
161
+ /**
162
+ * E18 resume-at — per-turn anchor capture, shared by runInBackground AND the resume-leg driveResume so the (single,
163
+ * tricky) capture rule can't drift between the two stream-drain loops.
164
+ *
165
+ * A "turn" in core = ONE assistant message + its tool batch. resume-at is valid ONLY at a SETTLED rewindable boundary
166
+ * — core's prepare-task accepts "user messages or finished assistant-text turns" and REJECTS an assistant message that
167
+ * ends mid-tool-call (resume_at.not_a_message). So we capture an anchor ONLY for a turn that produced text AND ran NO
168
+ * tools: then the turn's leaf at turn_end IS that assistant-text message (the rewindable target). For a tool-using
169
+ * turn neither candidate works — at turn_end the leaf is the turn's LAST tool-result (core appends tool results before
170
+ * emitting turn_end; a tool-result is stored as a `message` so core would SILENTLY branch AFTER the tools ran), and
171
+ * the assistant message itself ends mid-tool-call so core would reject it. The shell still gets a per-message eventId
172
+ * handle on every text event (E2 identity); a rewind to a non-captured (tool-using) message simply 404s — honest,
173
+ * never a silent wrong-point branch. (The adversarial-review HIGH: capturing at turn_end unconditionally mis-anchored
174
+ * every tool-using turn — the common agentic shape — to the tool-result leaf.)
175
+ *
176
+ * Best-effort: a capture failure must NEVER fail the run (it only makes that one turn non-rewindable). `onCaptureFail`
177
+ * surfaces the degrade (a metric) so a SYSTEMATIC failure — e.g. an un-migrated DB missing `resume_anchor` — is
178
+ * observable instead of a silent feature outage.
179
+ */
66
180
  export class TurnAnchorCapture {
67
181
  capture;
68
182
  onCaptureFail;
69
183
  captureUser;
184
+ /** The turn's FIRST text_delta eventId = the message's stable resume handle (also stamped on the durable text event). */
70
185
  firstTextEventId;
71
186
  turnHadTool = false;
72
- constructor(capture, onCaptureFail, captureUser) {
187
+ constructor(capture, onCaptureFail,
188
+ /** R8 (CC-parity rewind): capture a USER-message anchor straight from the
189
+ * `message_committed{role:"user"}` event — its `entryId` is the persisted user-message `SessionTreeEntry.id`,
190
+ * ALWAYS a valid `resumeAt` target (core accepts a user message; it rejects a tool-result / mid-tool-call
191
+ * assistant). This is the CC rewind target ("rewind to the prompt" = the code-restore parity path — a code
192
+ * change happens in a tool turn WITHIN a prompt's run, so rewinding to the prompt + rewindFiles undoes it).
193
+ * Keyed by the run's taskId (the handle the shell already holds from the POST response — no eventId→entryId
194
+ * side map / no `getLeafId` inference needed). The assistant-turn capture above stays as an additive bonus. */
195
+ captureUser) {
73
196
  this.capture = capture;
74
197
  this.onCaptureFail = onCaptureFail;
75
198
  this.captureUser = captureUser;
76
199
  }
200
+ /** A `message_committed` arrived — capture a USER-message rewind anchor (CC parity). Best-effort (a failure only
201
+ * makes that one prompt non-rewindable). Only `role==="user"`; assistant/toolResult committed entries are handled
202
+ * by the turn-text path (assistant) or deliberately not anchored (toolResult — `resumeAt` rejects them). */
77
203
  async onMessageCommitted(role, entryId) {
78
204
  if (role === "user" && entryId && this.captureUser) {
79
205
  await this.captureUser(entryId).catch(() => this.onCaptureFail?.());
80
206
  }
81
207
  }
208
+ /** A text_delta arrived — latch the first DEFINED eventId as the message handle (eventId is optional on the stream). */
82
209
  onText(eventId) {
83
210
  if (this.firstTextEventId === undefined) {
211
+ // First text of THIS turn. Any tool seen before now belonged to a PRIOR turn: on the resume leg, core streams
212
+ // the suspended turn's resolution tool_start/tool_end (the approved tool) with NO intervening turn_end before the
213
+ // continuation turn (runtask.js applyResumeDecision). Clearing turnHadTool here keeps a pure-text continuation
214
+ // turn anchorable. SAFE for normal turns: within a turn the assistant streams its text BEFORE its own tool calls,
215
+ // so a real same-turn tool always arrives AFTER this first text and re-sets the flag (guarded by `=== undefined`,
216
+ // so a later text part after a tool — [text, toolCall, text] — does NOT wrongly clear it).
84
217
  this.turnHadTool = false;
85
218
  }
86
219
  this.firstTextEventId ??= eventId;
87
220
  }
221
+ /** A tool ran this turn — mark it non-rewindable (its leaf will be a tool-result, not a settled assistant-text message). */
88
222
  onTool() {
89
223
  this.turnHadTool = true;
90
224
  }
225
+ /** turn_end — capture the anchor iff this was a settled assistant-TEXT turn, then reset for the next turn. */
91
226
  async onTurnEnd() {
92
227
  if (this.firstTextEventId && !this.turnHadTool && this.capture) {
93
228
  await this.capture(this.firstTextEventId).catch(() => this.onCaptureFail?.());
@@ -96,25 +231,109 @@ export class TurnAnchorCapture {
96
231
  this.turnHadTool = false;
97
232
  }
98
233
  }
234
+ /**
235
+ * Strip the durable-resume capability token from a result before it is persisted to the replayable event
236
+ * log / run row or returned to the submitter. The token IS the resume credential (token-as-auth, search
237
+ * [18] Q1b) and must never leave the service. Safe on any result (no-op when absent) — notably core maps a
238
+ * NESTED suspend under verify/cascade to status:"failed" + errorCode "unexpected.suspended" while still
239
+ * spreading `checkpointToken`, so that "failed" path (unlike the stream path's explicit suspend handling)
240
+ * would otherwise leak it into task_event / task_run.result / the sync 200 response. `checkpointGate` is
241
+ * non-secret and intentionally kept.
242
+ */
99
243
  export function stripCheckpointToken(result) {
244
+ // Narrow to a plain object (reject null / arrays) and an OWN `checkpointToken` key, so a non-plain or
245
+ // array input is returned unchanged rather than reshaped.
100
246
  if (result !== null && typeof result === "object" && !Array.isArray(result) && Object.hasOwn(result, "checkpointToken")) {
101
247
  const { checkpointToken: _drop, ...rest } = result;
102
248
  return rest;
103
249
  }
104
250
  return result;
105
251
  }
252
+ /** Evict a stale warm-cache entry after a cross-instance write conflict (broken-affinity backstop). */
106
253
  export function evictIfConflict(runner, sessionId, result) {
107
254
  if (sessionId && isSessionConflictResult(result)) {
108
255
  runner.sessions.evict?.(sessionId);
109
256
  }
110
257
  }
111
- export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests) {
258
+ /**
259
+ * Drive a task to completion in the background, persisting its events to the durable run log (S2).
260
+ *
261
+ * Text deltas are coalesced into one `text` event per turn (the durable stream is turn-grained for
262
+ * text); tool/lifecycle events are recorded individually. Per-token live streaming stays on the
263
+ * synchronous `/v1/tasks/stream` endpoint. Never throws — failures are recorded on the run.
264
+ *
265
+ * Cancellation (POST /v1/runs/:id/cancel): the run registers an AbortController in `inflight` (same-replica
266
+ * fast path — the cancel handler aborts it directly) and its heartbeat tick polls the durable
267
+ * `cancel_requested` flag (cross-replica, ≤HEARTBEAT_MS lag). Either way the run settles to
268
+ * status "failed" + errorCode "cancelled" (metric label "cancelled"); the entry is removed in `finally`.
269
+ *
270
+ * Preemption (POST /v1/assistant/tasks/:id/preempt, design/80 seam #2): a SECOND, INDEPENDENT controller
271
+ * (`preemptCtrl`, registered in `preemptable`) carries the scheduler's graceful "yield this task" — when raised,
272
+ * core durably SUSPENDS the task at the next clean turn boundary (gate resource_limit, reason "preempt") so it is
273
+ * RESUMABLE, the opposite of cancel's kill. It is wired ONLY on the plain-stream path: the verify/cascade
274
+ * branches finalize with `setTerminal` unconditionally (no `setSuspended`), so a preempt-suspend there would write
275
+ * a non-terminal "suspended" as terminal and release the session lock — and preempt's contract is graceful, not a
276
+ * failure. Cross-replica via the `preempt_requested` flag (same heartbeat tick). Eligibility (resourceSuspend +
277
+ * durable store + durable tool-results + remote env) is core's call: an ineligible task's preemptSignal no-ops.
278
+ *
279
+ * Steering (POST /v1/runs/:id/steer, design/47): the live core `TaskStream` is registered in `steerable` so the
280
+ * HTTP route can inject a mid-task message via `stream.steer()` (drained at the next turn boundary). Same-replica
281
+ * only — a suspended run is steered durably via the checkpoint's `setPendingSteer` instead, and only the
282
+ * plain-stream path holds a steerable stream (the verify/cascade branches return a result, not a stream).
283
+ */
284
+ export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, // 快审 F1:number 参升整对象——cost 顶随传
285
+ cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation,
286
+ /** E23: the VERIFIED principal (gatedPrincipal) that may answer this run's elicitations — matches the respond
287
+ * owner-gate's identity source (NOT the cost-attribution `principal`, which on a direct door is the spoofable
288
+ * header → can be null while a JWT principal exists). Falls back to the run owner when not supplied. */
289
+ elicitOwner,
290
+ /** E18 resume-at: capture the turn's (firstTextEventId → leaf entryId) anchor at each turn_end. Built by the caller
291
+ * closing over {sessionId, owner, sessionStore.getLeafId, resumeAnchorStore}; undefined ⇒ no anchor store wired. */
292
+ captureTurnAnchor,
293
+ /** MF-Fleet (shell-host contract): the run-scoped fleet-row publisher. `onStart` adds the live row; `onEvent` accrues
294
+ * tokens (turn_end) + subagent child rows (task_progress); `onTerminal` flips/removes it. Fired for ALL legs
295
+ * (verify/cascade/plain-stream) so a background run TRANSITIONS its `GET /v1/fleet/stream` row exactly like the
296
+ * sync leg. No-op when no fleet bus is wired (the publisher is a null-object). */
297
+ fleetPublisher,
298
+ /** R8 (CC-parity rewind): capture a USER-message anchor (keyed by this run's taskId) from `message_committed{role:"user"}`
299
+ * — the CC "rewind to the prompt" target. undefined ⇒ no anchor store wired. */
300
+ captureUserMessageAnchor,
301
+ /** §4④ : AskUserQuestion LIVE coordinator — establishes a per-run onQuestion ALS context (like `elicitation`)
302
+ * so an AskUserQuestion tool call routes to this run's stream + owner-gated respond. undefined ⇒ headless default. */
303
+ question,
304
+ /** P1 ①② follow-on (core): the async-workflow completion inbox, drained at the START of this background
305
+ * leg (parity with the sync stream-open drain) into durable `workflow_complete` events a tailing client replays.
306
+ * Owner gate = `elicitOwner ?? owner` (the same verified-principal-with-run-owner-fallback the elicit/question
307
+ * contexts use). undefined ⇒ no push half wired (the WorkflowStatus poll floor stands alone). */
308
+ workflowCompletionInbox,
309
+ /** C2 (core 1.219): the replica-local Task-subagent steer-handle registry. When present, this leg
310
+ * opts into core's `onSubagentSpawn` sink — each SYNC delegation's steer handle registers under THIS run's
311
+ * taskId (auto-evicted on child settle) so POST /v1/runs/:id/subagents/:target/steer can route into it. */
312
+ subagentSteer,
313
+ /** ask①: emit-target diagnostic sink for the completion-inbox drain below (route/connection/
314
+ * deliveryLagMs per delivered `workflow_complete`). runs.ts has no logger of its own — the caller closes over
315
+ * deps.logger. undefined ⇒ silent (behavior unchanged). */
316
+ completionDiagLog,
317
+ /** SendUserFile(切片2):per-run file_link 帧发射 ALS(question/elicitation 同款)。emit=durable append。 */
318
+ sendUserFile,
319
+ /** [998]② the shared prompt-manifest accumulator (tracer records at prepare; this leg drains the pending
320
+ * record into a durable `prompt_assembled` event — turns/stream read it). undefined ⇒ not wired. */
321
+ promptManifests) {
112
322
  const startedAt = Date.now();
113
323
  metrics?.addGauge("runs_active", 1);
114
- fleetPublisher?.onStart();
324
+ fleetPublisher?.onStart(); // MF-Fleet: the row goes live (running) the moment the background run starts
325
+ // Cancel (POST /v1/runs/:id/cancel): the controller aborts the run two ways — (a) same-replica fast path,
326
+ // the cancel handler looks this up in `inflight` and aborts now; (b) cross-replica, the heartbeat tick below
327
+ // polls the durable `cancel_requested` flag and aborts. Either way an aborted run finalizes to
328
+ // "failed" + errorCode "cancelled" (we do NOT extend TaskStatus — errorCode distinguishes user-cancel).
115
329
  const cancelCtrl = new AbortController();
116
330
  inflight?.set(taskId, cancelCtrl);
331
+ // stoppedBy (core 1.252): a service cancel is a USER stop — see markChildrenStoppedByUserOnAbort.
117
332
  markChildrenStoppedByUserOnAbort(cancelCtrl.signal, taskId, principal);
333
+ // design/80 seam #2 (preemption): only the plain-stream path can durably suspend-and-resume on a preempt (the
334
+ // verify/cascade branches setTerminal unconditionally). Register the preempt controller — and poll the durable
335
+ // flag — ONLY for that path, so the endpoint that looks a task up in `preemptable` can honestly tell a
336
+ // verify/cascade task apart (not preemptible) from a stream task.
118
337
  const preemptEligible = verify === undefined && cascade === undefined;
119
338
  const preemptCtrl = new AbortController();
120
339
  if (preemptEligible)
@@ -128,69 +347,157 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
128
347
  metrics?.inc("task_tokens_total", {}, result.stats.tokens);
129
348
  if (result?.stats.cacheHitRate !== undefined)
130
349
  metrics?.observe("task_cache_hit_rate", result.stats.cacheHitRate);
350
+ // Degenerate-output a/b instrument (1.59) — same as the sync path; no-op unless output.degenerate.
131
351
  if (result)
132
352
  instrumentDegenerate?.(result);
353
+ // Plan-cache recurrence probe (design/42): objective recurrence per scope. status "completed" feeds
354
+ // the history ring (mirrors the sync path); other terminal states still count toward the denominator.
133
355
  planCacheProbe?.record(principal, spec.objective, status === "completed");
134
356
  };
357
+ // Liveness heartbeat (independent of events): keeps a long-but-alive run — e.g. one blocked in a
358
+ // multi-minute subagent that streams no parent events — from being mistaken for a dead instance.
359
+ // The owning principal is this run's owner — thread it as the single-DB-fleet null-safe owner guard on every
360
+ // run-store mutation/poll below (the run row was created with owner = principal ?? null).
135
361
  const owner = principal ?? null;
136
362
  const heartbeat = setInterval(() => {
137
363
  void runStore.heartbeat(taskId, owner).catch(() => undefined);
364
+ // Cross-replica cancel: the cancel may have landed on another instance, which only set the durable flag.
365
+ // Poll it here so the OWNING instance aborts its in-flight run (bounded by HEARTBEAT_MS).
138
366
  if (!cancelCtrl.signal.aborted) {
139
367
  void runStore.isCancelRequested(taskId, owner).then((c) => { if (c)
140
368
  cancelCtrl.abort(); }).catch(() => undefined);
141
369
  }
370
+ // Cross-replica preempt (design/80 seam #2): a preempt may have landed on another instance (durable flag
371
+ // only). Poll it so the OWNING instance raises its preemptSignal → core suspends at the next clean turn
372
+ // boundary (resumable). Only the stream path wires the signal, so only poll there.
142
373
  if (preemptEligible && !preemptCtrl.signal.aborted) {
143
374
  void runStore.isPreemptRequested(taskId, owner).then((p) => { if (p)
144
375
  preemptCtrl.abort(); }).catch(() => undefined);
145
376
  }
146
377
  }, HEARTBEAT_MS);
147
378
  heartbeat.unref?.();
379
+ // E18 resume-at: per-turn anchor capture (the turn's first text_delta eventId = the stable per-message handle, which
380
+ // is also stamped on the durable `text` event so the shell sees ONE resume handle per message). The shared
381
+ // TurnAnchorCapture only anchors a SETTLED assistant-text turn (text + no tools) — see its doc; the metric surfaces
382
+ // a systematic capture failure (un-migrated DB) instead of a silent feature outage.
148
383
  const anchor = new TurnAnchorCapture(captureTurnAnchor, () => metrics?.inc("resume_anchor_capture_failed"), captureUserMessageAnchor);
384
+ // Once a terminal outcome's event is DURABLY appended, the run has reached that state — record it so a later
385
+ // failure (e.g. setTerminal throws after the `done` event is logged) re-attempts the SAME terminal write
386
+ // rather than clobbering a completed run with `failed` (which would log both `done` and `failed` and report a
387
+ // succeeded task as failed).
149
388
  let reached;
389
+ // 1.78 (review LOW): the plain-stream leg's live TaskStream — held so the finally can identity-guard the
390
+ // `steerable` deregistration (see the finally).
150
391
  let steerableStreamRef;
392
+ // false once this leg settles — a background subagent/bash completion arriving AFTER that has no live
393
+ // harness for core's in-process injection (the idle gap), so the observer below parks it durably instead.
151
394
  let legLive = true;
395
+ // option A: taskNotificationStreamKey (域:task_id:status[#seq], core [784]④a + codex F2 域二分) keys
396
+ // already carried by THIS leg (start-of-leg inbox drain, event-stream branch OR observer) — each arm skips
397
+ // keys another arm already delivered, so a notification isn't double-appended to the durable log (the
398
+ // turn-TAIL reap notification, which never reaches the stream, is exactly the one the observer catches;
399
+ // core's pend-drain replay of a double-parked idle arrival is exactly the one the drain-registration
400
+ // catches). The shell's dedup stays the outer belt. codex F1: value = the claiming write's outcome promise
401
+ // (the observer arm appends fire-and-forget; a failed claim must NOT eat the stream replay — see NotifiedKeys).
152
402
  const notifiedKeys = new NotifiedKeys();
403
+ // design/122 ③: THIS leg's subagent-handle unregister thunks (identity-guarded by the registry) — run at
404
+ // leg end (finally), NOT on child settle, so a settled child stays resumable while the parent run lives.
153
405
  const subagentHandleEvictions = [];
406
+ // design/80 D-B: PARK a `needs_review` result (a dry-run diff review OR a pre-action `plan_review` pause; core
407
+ // assemble-result slot 8.6) like a suspend — KEEP task_active + flip the row non-terminal so the resume
408
+ // (getActiveTaskId → markResuming) can claim it. Without this every finalize site setTerminal'd it (deleting the
409
+ // session lock), and the plan_review resume then got taskId=undefined → drove the model UNPROTECTED on an
410
+ // unlocked session with the row stuck `needs_review` (the adversarial-review HIGH finding). Mirrors the suspended
411
+ // branch (append only the non-secret gate, never the capability token).
154
412
  const parkNeedsReview = async (res) => {
155
413
  await sink.appendParked("needs_review", res);
156
- await flushModelUsage();
414
+ await flushModelUsage(); // E8: persist pre-park usage before the review pause (resume leg accumulates fresh)
157
415
  reached = { kind: "needs_review" };
158
416
  await runStore.setNeedsReview(taskId);
159
417
  metrics?.addGauge("runs_active", -1);
160
418
  metrics?.inc("tasks_total", { status: "needs_review" });
161
419
  };
420
+ // [900]①/[906]③: the event→ledger projection (text/reasoning coalescing + redaction + seq + the type/data
421
+ // whitelist shapes) lives in the SHARED LedgerSink — the sync /v1/tasks/stream detach lane consumes the same
422
+ // single switch, so the two ledgers can't drift. This leg keeps its non-ledger side-legs (anchor/metrics/
423
+ // model-usage/park-terminal/fleet) in place and drives the sink; `append`/`flush` below stay the leg-local
424
+ // names for every out-of-band row (elicit/question frames, model_usage deltas, suggestions, failed) so they
425
+ // share the run's single seq counter. `notifiedKeys` is the same per-leg instance the observer/drain arms use.
162
426
  const sink = createLedgerSink({ appendEvent: (seq, type, data) => runStore.appendEvent(taskId, seq, type, data), persistThinking, notifiedKeys });
163
427
  const append = (type, data) => sink.append(type, data);
164
428
  const flush = () => sink.flush();
429
+ // E8 (shell-host contract): drain the per-task model-usage accumulator into an append-only `model_usage` DELTA event, so
430
+ // the durable log is the resume-safe source of `TaskStats.modelUsage`. Called at every turn boundary AND before a
431
+ // park (suspend/needs_review) — a run that suspends has its pre-suspend usage persisted before the resume leg
432
+ // continues accumulating; the append-only deltas SUM across all legs (no overwrite).
165
433
  const flushModelUsage = () => appendModelUsageDelta(append, modelUsage, taskId);
434
+ // At a TERMINAL done: flush the final delta + SUM all `model_usage` events into result.stats.modelUsage (shared
435
+ // with the resume leg's terminal so a suspended→resumed run's total is summed across both legs).
166
436
  const withModelUsage = (result) => attachModelUsage(result, { append, getEvents: (id, after) => runStore.getEvents(id, after), modelUsage, taskId });
167
- modelUsage?.register(taskId);
437
+ modelUsage?.register(taskId); // E8 leak fence: only this top-level run's brain.calls are tracked (sub-tasks excluded)
438
+ // [998]②: same fence — sub-task manifests are never registered, never leak. The returned LEASE guards the
439
+ // finally's clear (B-3 window: a fast sibling resume re-registers this taskId before our finally runs —
440
+ // an unconditional clear would delete the NEW leg's registration/pending manifest).
168
441
  const manifestLease = promptManifests?.register(taskId) ?? 0;
442
+ // [998]②: no-op when nothing is pending (one Map.get) — fired per stream event so the manifest lands at the
443
+ // top of the log right after core's prepare records it, and before the terminal on the no-stream legs.
169
444
  const flushPromptManifest = () => appendPromptManifest(append, promptManifests, taskId);
445
+ // E23 (shell-host contract): an inbound MCP elicitation fired deep inside core's callTool surfaces via onElicit, which has
446
+ // no taskId — so establish a per-run elicitation context (ALS) carrying this run's emit (durable append → delivered
447
+ // on the events tail), owner (respond owner-gate) + cancel signal (a run abort cancels a parked elicitation). The
448
+ // elicitation append interleaves safely: onElicit is awaited SYNCHRONOUSLY inside callTool, so the loop is parked
449
+ // (not appending) while it runs. No-op when elicitation is off. `type` is the SSE event name; the rest is the data.
170
450
  const elicitEmit = (frame) => {
171
451
  const { type, ...rest } = frame;
172
452
  return append(type, rest);
173
453
  };
454
+ // §4④: same durable-append emit for AskUserQuestion frames (delivered on the events tail; the shell renders `question`
455
+ // + dismisses on `question_complete`). onQuestion is awaited SYNCHRONOUSLY inside core's callTool, so the loop parks
456
+ // (not appending) while it runs — the open frame's append is ordered ahead of the answer/terminal appends.
174
457
  const questionEmit = (frame) => {
175
458
  const { type, ...rest } = frame;
176
459
  return append(type, rest);
177
460
  };
178
461
  try {
462
+ // P1 ①② follow-on (core): drain this session's async-workflow completion inbox at the START of the
463
+ // background leg (before ANY branch — verify/cascade/plain-stream all get it; parity with the sync leg's
464
+ // stream-open drain) into durable `workflow_complete` events the tailing client (GET /v1/runs/:id/events)
465
+ // replays as <task-notification>s. Best-effort inside the helper (it never throws); inside the try so the
466
+ // heartbeat/gauge finally-cleanup would cover even a defensive miss. Owner gate mirrors the elicit/question owner.
179
467
  await emitPendingWorkflowCompletions(workflowCompletionInbox, spec.sessionId, elicitOwner ?? owner, (frame) => {
180
468
  const { type, ...rest } = frame;
469
+ // core [784]④b: register the drained frame's key on THIS leg — the same idle-arrival was ALSO parked
470
+ // core-side (pendingSessionNotifications); its pend-drain replay rides this run's stream as a
471
+ // task_notification event, and the loop's key check folds that second durable append (sync twin's note).
472
+ // 三路复审修2(MED,推翻旧 codex F1 注释的 resolved(true) 登记):emitPendingWorkflowCompletions
473
+ // 虽 await 这次 append,但它 CATCH 失败(留 row 不 ack)——键却已被 Promise.resolve(true) 认领;
474
+ // core 若把同一 pending 经 pend-drain 重放到本腿 stream 臂,stream 臂见「真成功」claim 抑制重写,
475
+ // 通知就延到下一次开腿(可能无限延)。登记真实 append 结果:失败 resolve false,stream 臂
476
+ // await 到 false 照写(咬合 NotifiedKeys 的 F1 语义);row 未 ack 的下腿重投仍是兜底。
477
+ // codex F2: task_type rides into the key(drained frames restore it from the entry extras)——
478
+ // external 帧独立键域,不与内部任务帧互吞.
181
479
  const f = frame;
182
480
  const write = append(type, rest);
183
481
  if (f.type === "task_notification" && f.task_id)
184
482
  notifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), write.then(() => true, () => false));
185
483
  return write;
186
- }, completionDiagLog ? { route: "bg-run-start", connection: "durable-append", log: completionDiagLog } : undefined);
484
+ },
485
+ // ask①: emit-target diagnostics — this drain appends to THIS background run's durable event log.
486
+ completionDiagLog ? { route: "bg-run-start", connection: "durable-append", log: completionDiagLog } : undefined);
487
+ // Thread the owning principal so the cost tracer attributes this run's spend (incl. council/team
488
+ // sub-tasks) to the principal's cumulative quota — mirrors the synchronous path.
187
489
  const runBody = async () => {
188
490
  if (verify !== undefined) {
491
+ // Developer-mode verification gate (1.44): multi-round (impl → adversarial verify → fix loop), not
492
+ // a stream — await the result and persist it terminal. The durable event log gets just the `done`
493
+ // (the verifier subtask runs on its own session). `result.verification` carries the verdict.
189
494
  const result = await runWithVerification(runner, { ...spec, taskId, signal: cancelCtrl.signal }, verify);
190
495
  await flush();
191
496
  evictIfConflict(runner, spec.sessionId, result);
192
- await flushPromptManifest();
193
- const safe = await withModelUsage(stripCheckpointToken(result));
497
+ // A nested suspend under the verify gate is mapped by core to status:"failed" carrying the capability
498
+ // token strip it before it touches the replayable log / run row (search [18] Q1b).
499
+ await flushPromptManifest(); // [998]②: the verify leg has no stream — persist the manifest before the terminal
500
+ const safe = await withModelUsage(stripCheckpointToken(result)); // E8: attach stats.modelUsage at terminal
194
501
  await append("done", { result: safe });
195
502
  reached = { kind: "done", result: safe };
196
503
  await runStore.setTerminal(taskId, safe.status, safe, safe.errorMessage ?? null);
@@ -198,21 +505,39 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
198
505
  return;
199
506
  }
200
507
  if (cascade !== undefined) {
508
+ // Cascade (1.45): cheap→strong ladder; multi-attempt cold re-runs, not a stream. Persist terminal.
201
509
  const result = await runCascade(runner, { ...spec, taskId, signal: cancelCtrl.signal }, cascade);
202
510
  await flush();
203
511
  evictIfConflict(runner, spec.sessionId, result);
204
- await flushPromptManifest();
205
- const safe = await withModelUsage(stripCheckpointToken(result));
512
+ await flushPromptManifest(); // [998]②: same no-stream posture as the verify leg above
513
+ const safe = await withModelUsage(stripCheckpointToken(result)); // see the verify branch; E8 modelUsage at terminal
206
514
  await append("done", { result: safe });
207
515
  reached = { kind: "done", result: safe };
208
516
  await runStore.setTerminal(taskId, safe.status, safe, safe.errorMessage ?? null);
209
517
  recordDone(safe, safe.status);
210
518
  return;
211
519
  }
520
+ // design/47 (mid-task steering): hold the live stream handle so POST /v1/runs/:id/steer can inject a steer via
521
+ // stream.steer() (drained at the next turn boundary). Only this plain-stream path is steerable — the verify/
522
+ // cascade branches above return a result, not a stream (parity with the preemptable registration). Deregistered
523
+ // in `finally`. Registering it AFTER the verify/cascade early-returns means those legs never enter the map.
524
+ // core 1.154 nested-subagent live tree: opt into the forward sink — a subagent's task_progress lives in the
525
+ // child run's ISOLATED stream (core's only emitter is parentToolCallId-gated and pushes onto the CHILD queue;
526
+ // the top stream NEVER carries it), so this sink is the ONLY path a tick reaches the top-run consumers. 1.78
527
+ // (review HIGH): fan it out to the DURABLE event log too (via the shared whitelist+redact builder, fire-and-
528
+ // forget — `++seq` is a sync increment so sink-vs-loop appends get unique seqs), not just the fleet bus —
529
+ // previously no fleet bus wired ⇒ ticks dropped entirely, and the stream-switch task_progress case below is
530
+ // forward-compat only (dead against core ≤1.208). core swallow-guards a throwing sink.
212
531
  const fwdInternals = {
213
532
  onForwardEvent: (e) => {
214
533
  fleetPublisher?.onForwardEvent(e);
534
+ // C1 (core 1.219): a delegated child's tool lifecycle + task_progress ticks persist through the SHARED
535
+ // sink (same whitelist+redact builders as the top stream — §E1, forwarded events are UNTRUSTED RAW;
536
+ // fire-and-forget with sync seq allocation). Rationale + the deliberate per-chunk-delta omission live
537
+ // on LedgerSink.onForwardEvent.
215
538
  sink.onForwardEvent(e);
539
+ // S2 live tail(core 1.370 bgAgentId,[1520]{core}):bg 子代帧按句柄分发给 per-agent 订阅者
540
+ // (sync 腿 twin 在 http/server.ts)。hasSubscribers 快门=没人听不投影;投影一次 N 订阅者共享。
216
541
  const bg = e.bgAgentId;
217
542
  if (bg !== undefined && defaultSubagentTailBus.hasSubscribers(bg)) {
218
543
  const f = projectTailFrame(e);
@@ -220,25 +545,60 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
220
545
  defaultSubagentTailBus.publish(bg, f);
221
546
  }
222
547
  },
548
+ // (durable half is the deployment's job): observe EVERY background
549
+ // completion notification. (1) flip the child's fleet row (task_progress never emits a terminal tick —
550
+ // without this a settled bg agent showed "running" until parent settle); (2) when the LEG already ended
551
+ // (idle — core's steer/followUp had nowhere to go), park it in the durable inbox; the next stream-open
552
+ // drain emits it as the same `task_notification` frame family. Workflow-type notifications are SKIPPED —
553
+ // they have their own durable chain (notify gate + inbox + serve fences); double-enqueueing would re-open
554
+ // the double-push. While the leg is LIVE, core injects in-process + the stream carries the display frame —
555
+ // no durable copy (a boundary-race notification that misses both is covered by the TaskOutput poll floor).
556
+ // core 1.283 `seq` (per-stop-cycle counter) rides into the dedup key + parked entry — core [784]④a.
557
+ // 白名单排查(clay 令,2026-07-24,「彻查 as {} 连环坑」):此前手写窄类型(仅 10/19 键),3 处
558
+ // (此处+http/server.ts 两处)复制粘贴、随 core payload 增field 逐渐过时——n 本身按引用直转
559
+ // taskNotificationEventData({notification:n}),运行时不丢字段(该函数自己的白名单是完整的),
560
+ // 但窄类型会让未来在这些回调体内新增读字段的人被 TS 误导「这个字段到不了这里」。改用 core
561
+ // 真实导出类型,杜绝副本漂移。
223
562
  onTaskNotification: (n) => {
224
563
  if (n.task_type === "workflow")
225
564
  return;
565
+ // S2 复审 #3(sync 腿同注):中途终态信号。
226
566
  if (defaultSubagentTailBus.hasSubscribers(n.task_id)) {
227
567
  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) } : {}) });
228
568
  }
569
+ // id-domain alias (core): fleet child rows key by the TICK's taskId = the child's sessionId
570
+ // (uuid domain); the notification's task_id is the a* domain — payload.sessionId IS the tick-domain
571
+ // alias, so flip by it (fallback task_id for pre-1.238 payloads / bash lane).
229
572
  const hadRow = fleetPublisher?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false;
230
573
  const parked = !legLive && Boolean(workflowCompletionInbox && spec.sessionId);
574
+ // diagnosability: one line per observed bg completion. hadFleetRow=false = the terminal frame was
575
+ // FLIP-THROUGH synthesized (the row was gone — parent settle removed it, or a bg BASH never ticked).
231
576
  completionDiagLog?.("task_notification_observed", { route: "bg-run-start", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive, parkedDurable: parked });
232
577
  if (parked) {
578
+ // 对抗评审 2026-07-11(MED):log the enqueue failure — the diag line above already printed
579
+ // parkedDurable:true, so a swallow made that a false park✓ fingerprint. Never faults the callback.
233
580
  void workflowCompletionInbox.enqueue(taskNotificationInboxEntry(spec.sessionId, elicitOwner ?? owner, n, Date.now(), taskId)).catch((err) => completionDiagLog?.("park_enqueue_failed", { route: "bg-run-start", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
234
581
  }
235
582
  else if (legLive) {
583
+ // option A (the turn-TAIL killed receipt must be IMMEDIATE): a teardown-time reap
584
+ // notification fires while the leg is still live but past the stream's delivery window — core's
585
+ // in-process steer is swallowed and the event never reaches the log. Append the display frame HERE,
586
+ // skipping keys the stream already delivered (mid-turn dedup); the tailing client renders the receipt
587
+ // this turn, not one turn late. core [784]④a: seq-folded shared key — the seq-less
588
+ // `${task_id}:${status}` shape reverse-swallowed a second stop-cycle's same-status frame here.
589
+ // codex F1 (HIGH): this append is fire-and-forget — claim the key with its REAL outcome, not
590
+ // unconditionally. A failed append (SQL blip) resolves the claim false, and the stream arm's
591
+ // await-then-write below re-delivers the same key instead of being swallowed by a dead claim.
236
592
  const key = taskNotificationStreamKey(n);
237
593
  if (notifiedKeys.get(key) === undefined) {
238
594
  notifiedKeys.set(key, append("task_notification", taskNotificationEventData({ notification: n })).then(() => true, () => false));
239
595
  }
240
596
  }
241
597
  },
598
+ // C2 (core 1.219) + design/122 (core 1.225 ③): register each SYNC child's steer handle under THIS
599
+ // run's taskId. NOT evicted on settle — `resume` is only legal AFTER the child settled (a settle-evict
600
+ // would make revival structurally unreachable). Evicted at LEG END via the collected identity-guarded
601
+ // thunks (finally below); a resume re-emits a FRESH handle under the same id (registry newest-wins).
242
602
  ...(subagentSteer
243
603
  ? {
244
604
  onSubagentSpawn: (handle) => {
@@ -248,30 +608,53 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
248
608
  : {}),
249
609
  };
250
610
  const stream = runner.runTaskStream({ ...spec, taskId, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, undefined, fwdInternals);
251
- steerableStreamRef = stream;
611
+ steerableStreamRef = stream; // held for the finally's identity-guarded delete (1.78 review LOW)
252
612
  steerable?.set(taskId, stream);
253
613
  for await (const ev of stream) {
614
+ // MF-Fleet: accrue this run's fleet-row tokens (turn_end usage). Fired before the switch so every event
615
+ // reaches it. No-op when no bus is wired. (Subagent task_progress arrives via the FORWARD sink above, not
616
+ // this top stream — the case below is forward-compat only; a stale comment here claimed otherwise.)
254
617
  fleetPublisher?.onEvent(ev);
618
+ // [998]② release-review round 2: drain the pending prompt manifest at the TOP of every iteration —
619
+ // BEFORE the done/suspend branches below. A done-first stream (post-prepare failure/abort, or an
620
+ // immediate suspend) would otherwise skip the flush entirely: the manifest recorded at prepare would
621
+ // be cleared by the finally (or, worse, survive a lease-guarded no-op clear into a fast sibling
622
+ // resume's queue and land AFTER the suspension event). Here it always precedes its own terminal.
255
623
  await flushPromptManifest();
624
+ // Non-ledger side-legs stay HERE (E18 anchor capture / S4 metrics) — the event→ledger projection itself
625
+ // is the shared sink below (single switch; the sync detach lane consumes the same one, so the persisted
626
+ // type/data vocabulary has ONE source). Ordering parity: anchor.onTool precedes the sink's tool_start
627
+ // flush (as before); the metric fires before the brain_status row lands (metric ≠ ledger, no row moves).
256
628
  switch (ev.type) {
257
629
  case "text_delta":
258
- anchor.onText(ev.eventId);
630
+ anchor.onText(ev.eventId); // E18: latch the turn's first-text eventId = the message's resume handle
259
631
  break;
260
632
  case "tool_start":
261
- anchor.onTool();
633
+ anchor.onTool(); // E18: a tool ran this turn → not a settled assistant-text boundary (don't anchor it)
262
634
  break;
263
635
  case "status":
636
+ // S4 (SILENT-FALLBACK P0-a): count the brain-layer liveness observation (the sink persists the row —
637
+ // previously: sync SSE only, async runs completely dark).
264
638
  metrics?.inc("brain_retry_total", { phase: String(ev.phase) });
265
639
  break;
266
640
  case "message_committed":
641
+ // R8 (CC-parity rewind): a persisted session entry was committed. Capture a USER-message rewind anchor
642
+ // (keyed by taskId) straight from its `entryId` — the CC "rewind to the prompt" target. assistant entries
643
+ // are anchored by the turn-text path; tool-results are deliberately not anchored (resumeAt rejects them).
644
+ // NOT forwarded to the durable trace / shell stream (core口径: replay walks the persisted session tree —
645
+ // the sink deliberately ignores this type).
267
646
  await anchor.onMessageCommitted(ev.role, ev.entryId);
268
647
  break;
269
648
  }
270
649
  if (ev.type === "done") {
271
650
  await flush();
272
651
  if (ev.result.status === "suspended") {
652
+ // Durable F4 pause (design/45): PARK the run (non-terminal), NOT a `done`. Never persist the
653
+ // capability `checkpointToken` to the replayable event log / run row (search [18] Q1b) — only the
654
+ // non-secret gate (sink.appendParked). The submitter polling GET /v1/runs/:id sees status:"suspended";
655
+ // an operator resumes via /v1/approvals (token looked up internally). The worker is freed (runs_active -1).
273
656
  await sink.appendParked("suspended", ev.result);
274
- await flushModelUsage();
657
+ await flushModelUsage(); // E8: persist pre-suspend usage before parking (resume leg accumulates fresh)
275
658
  reached = { kind: "suspended" };
276
659
  await runStore.setSuspended(taskId);
277
660
  metrics?.addGauge("runs_active", -1);
@@ -279,12 +662,18 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
279
662
  continue;
280
663
  }
281
664
  if (ev.result.status === "needs_review") {
665
+ // design/80 D-B: a review pause (plan_review / dry_run_review) — PARK it like a suspend (keep the lock)
666
+ // so the /v1/assistant/tasks/:id/plan_review resume can claim the row. NEVER persist the token.
282
667
  await parkNeedsReview(ev.result);
283
668
  continue;
284
669
  }
670
+ // A session CAS conflict arrives here as a failed result (core swallows it) — evict the
671
+ // now-stale warm-cache entry so the next turn re-wakes from TiDB instead of re-conflicting.
285
672
  evictIfConflict(runner, spec.sessionId, ev.result);
673
+ // Structural token fence (no-op today; this plain-stream path's only token-bearing result is the
674
+ // suspended branch above, handled separately) — symmetric with the verify/cascade + resume paths.
286
675
  {
287
- const safe = await withModelUsage(stripCheckpointToken(ev.result));
676
+ const safe = await withModelUsage(stripCheckpointToken(ev.result)); // E8: attach stats.modelUsage at terminal
288
677
  await sink.appendDone(safe);
289
678
  reached = { kind: "done", result: safe };
290
679
  await runStore.setTerminal(taskId, safe.status, safe, safe.errorMessage ?? null);
@@ -292,17 +681,37 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
292
681
  }
293
682
  continue;
294
683
  }
684
+ // The shared event→ledger projection (text/reasoning coalescing, whitelist+redact builders, flush-before
685
+ // discipline, task_notification dedup against this leg's `notifiedKeys`) — see LedgerSink.onEvent.
686
+ // (The prompt-manifest drain runs at the loop TOP — ahead of done/suspend too, see above.)
295
687
  await sink.onEvent(ev);
296
688
  if (ev.type === "turn_end") {
297
- await flushModelUsage();
689
+ await flushModelUsage(); // E8: persist this turn's per-model usage delta (resume-safe)
690
+ // E18 resume-at: anchor (this turn's message eventId → its leaf SessionTreeEntry.id), but ONLY for a settled
691
+ // assistant-TEXT turn (text + no tools) — for a tool-using turn the leaf here is the last tool-result, NOT
692
+ // the message the eventId names (see TurnAnchorCapture). Best-effort + reset for the next turn.
298
693
  await anchor.onTurnEnd();
299
694
  }
300
695
  }
696
+ // E12 (shell-host contract): after a COMPLETED run, pull core's opt-in post-completion prompt suggestions (a fire-and-forget
697
+ // pass in core; suggestions() never rejects + resolves to [] when off / not completed / empty). UNTRUSTED model
698
+ // text for the shell UI ONLY → redact + persist as a `suggestions` event so the shell reads it off the events tail
699
+ // (like model_usage); NEVER re-fed to a model. Gated on a COMPLETED terminal — a suspended/parked/failed leg has
700
+ // none (core also returns [] for a non-completed task; the explicit status gate avoids the needless call + pins the
701
+ // completed-only contract rather than depending on core's guard).
301
702
  if (spec.suggestNextPrompts && reached?.kind === "done" && reached.result.status === "completed") {
302
703
  const suggestions = await stream.suggestions().catch(() => []);
704
+ // 🔴 **空结果也要落事件**(2026-07-26):这个事件是「已结算」的唯一信号。此前 `if (suggestions.length)`
705
+ // 才 append ⇒ 「跑完了、确实一条都没有」在轮询面上与「还没跑完」完全同形(都是 `suggestions: undefined`)
706
+ // ⇒ 轮询方无从判断该不该停。而 `setTerminal`(上方)在本段**之前**执行,所以确实存在一个真实的
707
+ // pending 窗口 —— 这正是本仓刚在后台 shell `poll()` 上修过的**同一类**:消费方拿终态当停止条件,
708
+ // 而数据在终态之后才到。⇒ 事件在 = 已结算(哪怕是空);事件不在 = 仍在跑。
303
709
  await append("suggestions", { suggestions: suggestions.map((s) => redactSecrets(s)) });
304
710
  }
305
711
  };
712
+ // E23 + §4④: nest the elicitation AND AskUserQuestion per-run contexts (each its own ALS) around the run body — a
713
+ // background run's inbound elicitation AND the agent's AskUserQuestion both route to this run's durable events tail
714
+ // + owner-gated respond. Same id/owner/signal; the nesting order is immaterial (disjoint ALS).
306
715
  await withPrincipal(principal, () => {
307
716
  const withElicit = elicitation
308
717
  ? () => elicitation.runWithContext({ taskId, owner: elicitOwner ?? owner, emit: elicitEmit, abortSignal: cancelCtrl.signal }, runBody)
@@ -310,10 +719,13 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
310
719
  const withQuestion = () => question
311
720
  ? question.runWithContext({ taskId, owner: elicitOwner ?? owner, emit: questionEmit, abortSignal: cancelCtrl.signal }, withElicit)
312
721
  : withElicit();
722
+ // SendUserFile 帧与 question 帧同投递面(durable events tail,web 渲染文件卡);嵌套序无关(ALS 互不相交)。
313
723
  return sendUserFile
314
724
  ? sendUserFile.runWithContext({ taskId, emit: (frame) => { const { type, ...rest } = frame; return append(type, rest); } }, withQuestion)
315
725
  : withQuestion();
316
726
  });
727
+ // Cancelled mid-stream and the generator ended WITHOUT a terminal event (abort can end the stream cleanly
728
+ // rather than throwing) — finalize cancelled here so the run isn't left "running" for the reaper to catch.
317
729
  if (!reached && cancelCtrl.signal.aborted) {
318
730
  await flush();
319
731
  const c = cancelledResult();
@@ -326,29 +738,41 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
326
738
  catch (err) {
327
739
  const message = err instanceof Error ? err.message : String(err);
328
740
  try {
329
- await flush();
741
+ await flush(); // persist any buffered turn text before recording the failure (don't drop it)
330
742
  if (reached?.kind === "done") {
743
+ // The task already produced a terminal result (its `done` event is durably logged); only the run-row
744
+ // write failed. Re-attempt THAT write — never write `failed` over a completed run.
331
745
  await runStore.setTerminal(taskId, reached.result.status, reached.result, reached.result.errorMessage ?? null);
332
746
  }
333
747
  else if (reached?.kind === "suspended") {
334
- await runStore.setSuspended(taskId);
748
+ await runStore.setSuspended(taskId); // parked, not failed — re-attempt the suspend row write
335
749
  }
336
750
  else if (reached?.kind === "needs_review") {
337
- await runStore.setNeedsReview(taskId);
751
+ await runStore.setNeedsReview(taskId); // D-B: parked on a review gate — re-attempt the review-park row write
338
752
  }
339
753
  else if (cancelCtrl.signal.aborted) {
754
+ // Cancelled: the abort threw out of the stream/verify/cascade. Record a cancelled terminal (queryable
755
+ // errorCode) instead of a generic failure, so the portal can show "cancelled by user" not an error.
340
756
  const c = cancelledResult();
341
757
  await append("failed", { errorMessage: c.errorMessage ?? "cancelled by user", errorCode: "cancelled" });
342
758
  await runStore.setTerminal(taskId, "failed", c, c.errorMessage ?? null);
343
759
  }
344
760
  else {
761
+ // BL-12: redact the raw exception message before it lands in the durable event log / run row — a tool
762
+ // or transport error can carry a secret (a credentialed URL, a token) and the trace surface is served
763
+ // to TRACE_TOKEN holders. redactSecrets is best-effort + idempotent (the read side redacts again, BL-13).
345
764
  const safeMsg = redactSecrets(message);
346
765
  await append("failed", { errorMessage: safeMsg });
347
766
  await runStore.setTerminal(taskId, "failed", null, safeMsg);
348
767
  }
349
768
  }
350
769
  catch {
770
+ /* best-effort: the stale reaper will catch a run we couldn't finalize */
351
771
  }
772
+ // recordDone runs exactly once: the success paths call it inline (and return/break before the throw); we
773
+ // reach here only if the terminal-row write threw AFTER the event was logged, or on a genuine failure.
774
+ // A cancelled terminal keeps its distinct metric label even on this retry path (council B2: the result's
775
+ // STATUS is "failed" — labeling by status would count a user cancel as a failure).
352
776
  if (reached?.kind === "done")
353
777
  recordDone(reached.result, reached.result.errorCode === "cancelled" ? "cancelled" : reached.result.status);
354
778
  else if (reached?.kind === "suspended") {
@@ -356,15 +780,22 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
356
780
  metrics?.inc("tasks_total", { status: "suspended" });
357
781
  }
358
782
  else if (cancelCtrl.signal.aborted) {
359
- recordDone(null, "cancelled");
783
+ recordDone(null, "cancelled"); // distinct metric label; the run row is "failed"+errorCode:"cancelled"
360
784
  }
361
785
  else {
362
786
  recordDone(null, "failed");
363
787
  }
364
788
  }
365
789
  finally {
366
- legLive = false;
790
+ legLive = false; // notifications from here on take the durable-inbox path
791
+ // MF-Fleet: flip/remove the fleet row at the SINGLE finalization point — runs once for EVERY exit path
792
+ // (success / cancel / error / suspend / needs_review). A truly-terminal status leaves the fleet; a PARKED
793
+ // status (suspended→waiting / needs_review→awaiting approval) STAYS (the run is still active). A cancelled
794
+ // run carries status:"failed"+errorCode:"cancelled" — map it to "killed" so the row reads as a kill, not a
795
+ // failure. `reached` undefined ⇒ errored/aborted before any terminal event (catch recorded failed/cancelled).
367
796
  if (fleetPublisher) {
797
+ // [1839]①/(a) 案:真终态帧带残局键(usage=stats 映射 / cancel⇒stoppedBy:"user" / transcriptId=sessionId);
798
+ // parked(suspended/needs_review)不带(publisher 侧也再挡一层)。
368
799
  if (reached?.kind === "done")
369
800
  fleetPublisher.onTerminal(reached.result.errorCode === "cancelled" ? "cancelled" : reached.result.status, fleetRunResiduals(reached.result));
370
801
  else if (reached?.kind === "suspended")
@@ -373,22 +804,32 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
373
804
  fleetPublisher.onTerminal("needs_review");
374
805
  else
375
806
  fleetPublisher.onTerminal(cancelCtrl.signal.aborted ? "cancelled" : "failed", {
807
+ // errored/aborted before any terminal event — no TaskResult.stats to map; the transcript still exists.
376
808
  ...(cancelCtrl.signal.aborted ? { stoppedBy: "user" } : {}),
377
809
  ...(spec.sessionId ? { transcriptId: spec.sessionId } : {}),
378
810
  });
379
811
  }
812
+ // 对抗复查 B-3(HIGH):identity-guard these two like `steerable` below — the run row flips suspended
813
+ // BEFORE this finally runs, so a fast resume can re-register the SAME taskId (a fresh cancelCtrl /
814
+ // preemptCtrl) in the window; an unconditional delete then evicted the NEW leg's entry, and drain
815
+ // under-counted the resuming leg (its cancel/preempt handles also went unreachable).
380
816
  if (inflight?.get(taskId) === cancelCtrl)
381
817
  inflight.delete(taskId);
382
818
  if (preemptable?.get(taskId) === preemptCtrl)
383
819
  preemptable.delete(taskId);
820
+ // Identity-guarded (1.78 review LOW, parity with the sync/resume legs): only delete the handle THIS leg
821
+ // registered and only if it is still ours — a fast sibling leg re-registering the same taskId must not lose
822
+ // its fresh handle to our teardown; never registered ⇒ nothing of ours to delete.
384
823
  if (steerableStreamRef !== undefined && steerable?.get(taskId) === steerableStreamRef)
385
824
  steerable.delete(taskId);
386
825
  for (const evict of subagentHandleEvictions)
387
- evict();
388
- modelUsage?.clear(taskId);
389
- promptManifests?.clear(taskId, manifestLease);
826
+ evict(); // design/122 ③: leg-end handle eviction (identity-guarded — a fast sibling leg's fresh handles survive)
827
+ modelUsage?.clear(taskId); // E8: drop any un-drained accumulation (abandoned/errored leg) — no memory leak
828
+ promptManifests?.clear(taskId, manifestLease); // [998]②: lease-guarded — a stale leg never tears down the new leg's slot
390
829
  clearInterval(heartbeat);
391
830
  }
392
831
  }
832
+ /** How often a running instance refreshes its run's updated_at (liveness, independent of events).
833
+ * Must stay strictly below `runStaleSec` (asserted at startup) or the reaper would race live runs. */
393
834
  export const HEARTBEAT_MS = 30_000;
394
835
  //# sourceMappingURL=runs.js.map