@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
@@ -1,7 +1,7 @@
1
1
  import http from "node:http";
2
2
  import { once } from "node:events";
3
3
  import { createHash } from "node:crypto";
4
- import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, HAND_TOOL_EFFECTS, canonicalToolName, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow } from "@sema-agent/core";
4
+ import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, HAND_TOOL_EFFECTS, canonicalToolName, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow } from "@sema-agent/core"; // canonicalToolName = core single-source (1.162; replaced the transitional service mirror)
5
5
  import { decideParkedAgent, findParkedAgentForCheckpoint } from "../parked-decide.js";
6
6
  import { matchCatalogModel } from "../model-select.js";
7
7
  import {} from "../sema-registry.js";
@@ -46,19 +46,30 @@ import { cascadeConfig } from "./run-meta.js";
46
46
  export { cascadeConfig };
47
47
  import { handleImages, createImagesLocal, coarseStatusForState, errorCodeForExit, BAKE_ID_RE, BAKE_EVENTS_RE, BAKE_CANCEL_RE, BAKE_CLAIM_RE, BAKE_INGEST_RE } from "./routes/images.js";
48
48
  export { coarseStatusForState, errorCodeForExit };
49
+ // design/158 A9:装配器缝——发送器/身份门下沉到 http/ 叶子模块,routes/* 与 server.ts 共用同一实现
50
+ // (routes/* 绝不可值 import server.ts:那条边会闭合运行时环,见 test/module-cycle-gate.test.ts)。
49
51
  import { sendJson, sendError, httpErrorCode, msg } from "./send.js";
50
52
  import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, isOperator, explicitOperator } from "./principal-gate.js";
51
53
  export { explicitOperatorOk, isOperator, explicitOperator };
54
+ /** 分组入参 → 平铺视图(createHttpServer 的第一件事)。一组都没给 ⇒ 原样返回(存量调用零开销、零形变)。 */
52
55
  export function flattenServiceDeps(deps) {
53
56
  const { stores, coordinators, seams, observability, governance, deployment, knobs, ...flat } = deps;
54
57
  if (!(stores || coordinators || seams || observability || governance || deployment || knobs))
55
58
  return deps;
56
59
  return { ...stores, ...coordinators, ...seams, ...observability, ...governance, ...deployment, ...knobs, ...flat };
57
60
  }
61
+ /** Personalization caps: server-pinned hard limits — a UI may be stricter, never looser. */
58
62
  export const MAX_USER_SKILLS = 10;
63
+ /** = core `SKILL_CONTENT_MAX_CHARS`(1MB 加载门,core 1.293 起 invoke 全文零截断、超 1MB 整拒;常量在
64
+ * dist/core/runner/synthetic-tools.js:84,未从 core 包根导出,故钉本地孪生+本出处注记)。修7 曾对齐旧
65
+ * 20k 截断门([845] 飞轮考据:20k 是压缩保留区口径,invoke 时刻用错),core 1.293 撤门后本门跟随回收
66
+ * ([853]① 收口)——HTTP 面与引擎加载门同值,超限 400 fail-loud 而非引擎侧整拒后难诊断。 */
59
67
  export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
60
68
  export const MAX_SYSTEM_PROMPT_CHARS = 16_384;
69
+ /** Cap on a request's `outputSchema` (JSON-serialized) — an uncapped schema bloats the prompt + the per-turn
70
+ * validation cost. 32KB comfortably holds a rich nested schema while bounding the abuse surface. */
61
71
  export const MAX_OUTPUT_SCHEMA_CHARS = 32_768;
72
+ /** Validate `body.skills` (caps + shape). Returns an error string (→ 400) or null when acceptable. */
62
73
  export function validateUserSkills(skills) {
63
74
  if (skills === undefined)
64
75
  return null;
@@ -78,17 +89,46 @@ export function validateUserSkills(skills) {
78
89
  }
79
90
  return null;
80
91
  }
92
+ /** Verification rounds requested by a caller, clamped to a safe [1,5] (default 2). */
81
93
  export function clampVerifyRounds(v) {
82
94
  const n = typeof v === "number" && Number.isFinite(v) ? Math.floor(v) : 2;
83
95
  return Math.min(5, Math.max(1, n));
84
96
  }
85
97
  const MAX_BODY = 8 * 1024 * 1024;
98
+ /** Caps on `body.images`: inline base64 `data` is otherwise passed RAW to the
99
+ * spec (URL images are SSRF+size-gated by core, inline aren't). The whole JSON body is already ≤ MAX_BODY, but with NO
100
+ * COUNT cap a request could carry thousands of tiny images (a decode/processing DoS), and there was no explicit
101
+ * per-image error. The two-axis gate belongs in the server (shell carries content, server gates it). */
86
102
  const MAX_IMAGES_PER_REQUEST = 20;
87
- const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
103
+ const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024; // a single inline image's base64 ≤ 6 MiB (< MAX_BODY, so REACHABLE — one image can't hog the whole 8 MiB budget); ~4.5 MiB decoded, generous for a screenshot/photo
104
+ // Task Trace API (S1): GET /v1/tasks (list) | /v1/tasks/:id/turns | /v1/tasks/:id/stream. The `:id` group +
105
+ // sub-route are both-or-neither, so a GET /v1/tasks/stream (no :id) does NOT match — the POST sync stream is unshadowed.
106
+ // Hand (executionEnv) tool names core mounts at the runner when an executionEnvFactory is configured — the
107
+ // authoritative set is core `HAND_TOOL_EFFECTS` (root-exported since 1.70). These are NEVER in spec.tools, so
108
+ // the durable-resume satisfiability pre-check must union them when remote exec is on.
88
109
  const HAND_TOOL_NAMES = Object.keys(HAND_TOOL_EFFECTS);
110
+ /** [1245] codex-3/4 — the REOPEN-class resume failures: core's runtask reopened the checkpoint (store.reopen,
111
+ * dist-read) BEFORE returning this failed result, so the park is STILL PENDING and decidable. Everything
112
+ * downstream must treat these as a re-park, never terminal: the durable event log writes `suspended` (not
113
+ * `done`), the run row re-parks (keeps task_active), and /decide surfaces retriable:true. A reopen that
114
+ * itself failed surfaces as `checkpoint.reopen_failed` — deliberately NOT matched here (terminal). */
89
115
  function isCheckpointReopenedFailure(r) {
90
116
  return r.status === "failed" && (r.errorCode === "resume.env_failed" || r.errorCode === "resume.tool_unavailable");
91
117
  }
118
+ /** The TRUSTED end-user principal for an owner-gated / per-tenant request. On a direct-door worker the
119
+ * trusted-header path is OFF (x-agent-principal is client-spoofable), so identity comes from the crypto-VERIFIED
120
+ * principal JWT (`x-approval-principal-token`, principal-only proof — no cnf.bnd action binding); otherwise the
121
+ * BFF-minted trusted header. undefined when absent/invalid (fail-closed → callers map to 401 / scope "__none__").
122
+ * 🔴 Use this for ANY endpoint that reads or gates PER-TENANT data — NOT `principalFrom` directly, which trusts
123
+ * the spoofable header on a direct door (/v1/usage + the trace owner-gate
124
+ * had drifted to a bare principalFrom, unlike the leader/images/approvals identity paths). One source of truth
125
+ * so a future per-tenant endpoint can't re-introduce the bypass. The action-BOUND decide/answer paths keep their
126
+ * own verifyDirectDoorProof (cnf.bnd) — this is the identity-only twin. */
127
+ // F-fix: a thin delegate to the single source of the verified identity (security.ts `verifiedPrincipal`). Kept as a
128
+ // named alias so the existing call sites read clearly; the JWT logic now lives in one place that createAuthorizer
129
+ // shares, so spec.principal (governance + cost) and these observability/owner sites can never drift apart again.
130
+ /** design/158 A9 装配表(顺序 = 拆分前 `handle()` 里各域出现的先后,逐行同序;真正的 `return` 在 handle 内,
131
+ * 这里只做**签名一致性**的编译期钉:任何域入口漂了形,是编译红,不是运行时静默 404)。 */
92
132
  const ROUTE_DOMAINS = [
93
133
  handleTraceUsage,
94
134
  handleWorkflows,
@@ -111,17 +151,73 @@ const ROUTE_DOMAINS = [
111
151
  handleNotifyWake,
112
152
  ];
113
153
  void ROUTE_DOMAINS;
154
+ /**
155
+ * Managed-Agents-style HTTP/SSE service surface.
156
+ * GET /health
157
+ * POST /v1/tasks → run to completion, TaskResult JSON (synchronous)
158
+ * POST /v1/tasks/stream → SSE of TaskEvent, live tokens (synchronous, same instance)
159
+ * POST /v1/runs → async: 202 {taskId, sessionId, status}, runs in background (S1)
160
+ * GET /v1/runs/:id → run status + result (poll)
161
+ * GET /v1/runs/:id/events → SSE replay from Last-Event-ID, then tail to terminal (S2)
162
+ * POST /v1/sessions/:id/wake→ 501 (resume is implicit: re-issue a task with the same sessionId)
163
+ */
114
164
  export function createHttpServer(rawDeps) {
165
+ // design/158 A8:分组装配 → 平铺视图。**分组是装配面的形,不是消费面的形**——下面 ~700 处 `deps.x`
166
+ // 消费点原样保留(平铺入参零形变、零开销:没给任何组时 flatten 原样返回同一个对象)。
115
167
  const deps = flattenServiceDeps(rawDeps);
168
+ // Idempotency-Key dedup for task submission (center review-routing blocker: exactly-once billing). One
169
+ // cache per server instance, shared by the sync /v1/tasks and async /v1/runs create paths.
116
170
  const idemCache = new IdempotencyCache();
171
+ // In-flight durable runs on THIS instance → their cancel controller. POST /v1/runs/:id/cancel landing here
172
+ // aborts immediately (fast path); a cancel landing on another replica uses the durable flag + heartbeat poll.
117
173
  const inflightRuns = new Map();
174
+ // [1.207 codex H2] taskIds whose abort came from the CANCEL VERB (fast path or flag poll) — the interactive
175
+ // legs consult this at settle time to label an abort-shaped failed result `errorCode:"cancelled"` (core's
176
+ // external-signal abort yields failed WITHOUT an errorCode; first-writer-wins would then pin the unlabeled
177
+ // form). A genuine completion that raced the abort is NOT relabeled (only status==="failed" gets the code).
178
+ // Entries are dropped in each leg's finally (same lifecycle as its inflightRuns registration).
118
179
  const cancelledViaVerb = new Set();
180
+ // design/80 seam #2 (preemption): in-flight PREEMPTIBLE runs on THIS instance → their preempt controller (the
181
+ // graceful-yield twin of inflightRuns' kill). POST /v1/assistant/tasks/:id/preempt landing here aborts it
182
+ // immediately (fast path → core durably suspends at the next clean turn boundary); a preempt landing on another
183
+ // replica uses the durable `preempt_requested` flag + heartbeat poll. Only plain-stream + resumed legs register.
119
184
  const preemptableRuns = new Map();
185
+ // design/47 + design/80 D-A (steering): in-flight STEERABLE runs on THIS instance → their live core TaskStream
186
+ // handle. POST /v1/runs/:id/steer landing here injects a mid-task message via the stream's `steer()` (applied at
187
+ // the next turn-boundary drain → 200). A run that is NOT here is either durably suspended (→ park the steer on its
188
+ // checkpoint via setPendingSteer → 202) or streaming on another replica (cross-replica live-steer is a fast-follow,
189
+ // single-replica today → 409). Only the plain-stream + resumed legs hold a live stream, so only they register
190
+ // (the verify/cascade legs return a result, not a stream — nothing to steer, parity with preemptableRuns).
120
191
  const steerableRuns = new Map();
192
+ // design/144 wake 半场:终态 steer 铸 park 的 per-session 串行链(复审 HIGH:tryPark→put 非原子,
193
+ // 并发双 steer 双铸两个 pending park)。单副本部署(本仓 steer 跨副本 seam 同款现实)进程锁即闭环;
194
+ // 跨副本残窗由 put 后赢家复核收敛兜底(见 (d) 分支),与 cross-replica live-steer 同列 fast-follow。
121
195
  const wakeParkMints = new Map();
196
+ // design/158 A9:两条「裸 number 登记簿」装盒——`let` 拆不出闭包,域模块要自增/自减必须共享同一个对象。
197
+ // 语义与计数时机逐字不变(uncountedBillableInflight = B-1: sync /v1/tasks + leader bg runs,无 map 身份,
198
+ // drain 靠它看见;admittedInflight = 修1 的 admission 期计数,见下方注释)。
122
199
  const counters = { uncountedBillableInflight: 0, admittedInflight: 0 };
200
+ // 修1(三路复审 B3 HIGH,迟登记窗):ADMISSION-time in-flight counter. Every non-probe request is counted
201
+ // SYNCHRONOUSLY in handle()'s first statements (before ANY await — auth/prepareSpec/lease/claim all happen
202
+ // later) and released once, on whichever of finish/close fires first. Closes the window where a request that
203
+ // just entered the handler was invisible to inflight() and a SIGHUP idle check (or SIGTERM drain read)
204
+ // misjudged the replica idle → hardShutdown mid-admission. Deliberately OVERLAPS the leg registries below
205
+ // (a registered leg counts twice) — every consumer only tests inflight()===0, so overcount is conservative,
206
+ // never a false-idle.
207
+ // drain: expose this replica's in-flight leg count as a LIVE getter on the shared drain state. Union of
208
+ // taskIds (a resumed leg registers in BOTH maps) — bg/resume durable legs (inflightRuns) + live streams
209
+ // (steerableRuns, sync leg included since 1.78). main.ts polls this after SIGTERM to know when drain is done.
210
+ // 对抗复查 B-1(CRITICAL):the SYNC /v1/tasks await and the leader background run register in NEITHER map —
211
+ // a SIGTERM during either read inflight=0 and hard-shut mid-model-work. Count them explicitly (a plain
212
+ // counter: they have no steer/resume identity to key a map on).
123
213
  if (deps.drainState)
124
214
  deps.drainState.inflight = () => new Set([...inflightRuns.keys(), ...steerableRuns.keys()]).size + counters.uncountedBillableInflight + counters.admittedInflight + (deps.leaderEndpoint?.inflight?.() ?? 0);
215
+ // B3 复审(codex HIGH):admission 计数之外再补一条粗粒度活性时钟:任何非探针请求(/health、/metrics*
216
+ // 除外——探针/抓取是常态噪音)都推进时间戳;SIGHUP 空闲窗要求「inflight 归零 + 活性静默」双满足才关停。
217
+ // 顺带把 idle-but-live 的 peer(还在 list/浏览的壳)也保住。
218
+ // ⚠️ 时基契约(三路复审修3):performance.now() 单调域,与 sighup-idle.ts 的 `now` seam 同域——挂钟
219
+ // (Date.now)跳变(NTP 回拨/夏令时)会把空闲窗判定拉爆。对外报告的 epoch 时间戳(请求日志/drainState.since)
220
+ // 不受影响,仍走 Date.now。
125
221
  let lastBillableActivityAt = performance.now();
126
222
  const noteActivity = (url) => {
127
223
  if (url === "/health" || url.startsWith("/metrics"))
@@ -130,13 +226,22 @@ export function createHttpServer(rawDeps) {
130
226
  };
131
227
  if (deps.drainState)
132
228
  deps.drainState.lastActivityAt = () => lastBillableActivityAt;
229
+ // asyncRewake wake 管道(hooks 阶段二挂账兑现):hook-runner 的 ctx.wake 经 main.ts 的 hookWakeBus holder
230
+ // 到这里——session → 本副本活流(steerableRuns 是 taskId 键,反查经 run 行;活流数=本副本并发数,量小;
231
+ // wake 是稀有事件[async hook exit 2],逐行 get 可接受)。找到即 steer(操作者 hook 的 stderr,trusted
232
+ // 与 explicitOperator 同源语义;下个 turn 边界注入)。无活流 → 挂起腿 park fallback(与 HTTP steer 的
233
+ // tryPark 同一原语:pending checkpoint 是权威,CAS on status='pending';落上=resume 时 core 注入)。
234
+ // park 也不上 → false(别副本 live/已结束——调用方记账)。
133
235
  if (deps.hookWakeBus) {
134
236
  deps.hookWakeBus.deliver = async (sessionId, text) => {
237
+ // hook stderr 不是操作者亲笔——是 hook 脚本运行时输出(可能回显下载物/工具输出),
238
+ // 【内容】不升 trusted(与 HTTP steer 的非-explicitOperator 姿势一致),且先过同一 validatePendingSteer
239
+ // 校验(break-out/控制字符即拒,不静默换流)。围栏 provenance 在 main.ts 闭包侧(注入前缀标明来源)。
135
240
  try {
136
241
  validatePendingSteer({ text, trusted: false });
137
242
  }
138
243
  catch {
139
- return false;
244
+ return false; // steering.invalid_content:内容不合法,不投递(调用方按 dropped 记账)
140
245
  }
141
246
  for (const [tid, stream] of steerableRuns) {
142
247
  try {
@@ -147,9 +252,12 @@ export function createHttpServer(rawDeps) {
147
252
  return true;
148
253
  }
149
254
  catch {
150
- continue;
255
+ continue; // not_running race / 行读失败 → 试下一个;全空 → 挂起腿 park
151
256
  }
152
257
  }
258
+ // 挂起腿:任务 durably suspended 时 async hook 才收场(如慢 lint 收尾)——
259
+ // wake park 到 session 的 pending checkpoint(镜像 POST /v1/runs/:id/steer 的 tryPark:scope/token
260
+ // 缺失或 CAS 失手=没有真挂起的 checkpoint → false)。resume 时 core 把它作为 pending steer 注入。
153
261
  try {
154
262
  const cs = deps.checkpointStore;
155
263
  if (!cs)
@@ -163,12 +271,14 @@ export function createHttpServer(rawDeps) {
163
271
  return await cs.setPendingSteer(token, scope, { text, trusted: false });
164
272
  }
165
273
  catch {
166
- return false;
274
+ return false; // park 面任何失败=诚实 false(调用方按 dropped 记账),绝不 throw 回 hook 收尾路径
167
275
  }
168
276
  };
169
277
  }
170
278
  const server = http.createServer((req, res) => {
171
279
  void handle(req, res).catch((err) => {
280
+ // A typed HttpError (e.g. a malformed JSON body → 400) carries its own status — a client mistake is a
281
+ // 4xx, not a 500. Everything else is a genuine internal error: log the cause, never echo it (info disclosure).
172
282
  if (err instanceof HttpError) {
173
283
  if (!res.headersSent)
174
284
  sendError(res, err.status, httpErrorCode(err.status, err.code), err.message, { ...(err.code ? { code: err.code } : {}), ...(err.extra ?? {}) });
@@ -183,22 +293,38 @@ export function createHttpServer(rawDeps) {
183
293
  res.end();
184
294
  });
185
295
  });
296
+ // design/158 A9 装配缝:服务级 ctx 建**一次**(deps 平铺视图 / 跨域运行期登记簿 / 无状态助手 / 单域状态)。
297
+ // 每请求只再加一格 `req`(见 handle 内)。助手全是 `function` 声明,提升后此处引用安全。
186
298
  const routeCtxBase = {
187
299
  deps,
188
300
  registry: { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters },
189
301
  helpers: { readJson, readRawBody, rateLimited, quotaExceeded, leaseDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
190
302
  local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal() },
303
+ // A9 尾批:提交/续跑「腿」只装引用(实现仍在本闭包里,逐字未动)——tasks/runs 共用 prepareSpec,
304
+ // approvals/assistant/notify-wake 共用 resume 家族;走这一格才不会造出 routes/* 互相值 import 的环。
191
305
  legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, resumePlanReview },
192
306
  };
193
307
  async function handle(req, res) {
194
308
  const startedAt = Date.now();
195
309
  const method = req.method ?? "GET";
196
310
  const url = (req.url ?? "/").split("?")[0];
197
- noteActivity(url);
311
+ noteActivity(url); // B3: admission-time activity stamp (before any await — no gap for the idle watch to miss)
312
+ // 修1(三路复审 B3 HIGH):admission-time inflight registration — SYNCHRONOUS, before the first await, so a
313
+ // request still in its pre-registration phase (auth/prepareSpec/lease/claim) is already visible to
314
+ // drainState.inflight(). Released exactly once in `record` below (finish/close latch — the same lifecycle
315
+ // that covers aborted SSE streams). Probes (/health, /metrics*) and CORS preflight stay uncounted (constant
316
+ // sampling noise must not hold the SIGHUP idle window open).
198
317
  const admitted = method !== "OPTIONS" && url !== "/health" && !url.startsWith("/metrics");
199
318
  if (admitted)
200
319
  counters.admittedInflight++;
320
+ // Record on whichever of finish/close fires FIRST (a `logged` latch prevents double-count): a client that
321
+ // aborts (esp. an SSE stream) fires `close` WITHOUT `finish`, so a finish-only handler never counts it —
322
+ // exactly the abnormal traffic most worth observing. An abort mid-response is logged as 499 (client closed).
201
323
  let logged = false;
324
+ // [854]⑥ 纯观测:tasks/stream 断连(499)日志此前无 task 上下文,排查「谁的流断了」只能对时间戳。
325
+ // 流路由把 X-Task-Id(earlyDurableTid)stash 到这两个 handle 作用域变量;record 只在 499 行带上——
326
+ // 不动计费/inflight(admittedInflight 的 latch 语义原样)。detached 标记见 [854]①b(断连宽限 opt-in)。
327
+ // design/158 A9:每请求局部量收进 reqState(域模块经 ctx.req 读写同一个盒子),ctx = 服务级四格 + 本格。
202
328
  const reqState = { streamTaskId: undefined, streamDetached: false, source: null };
203
329
  const ctx = { deps: routeCtxBase.deps, registry: routeCtxBase.registry, helpers: routeCtxBase.helpers, local: routeCtxBase.local, legs: routeCtxBase.legs, req: reqState };
204
330
  const record = (viaClose) => {
@@ -206,7 +332,7 @@ export function createHttpServer(rawDeps) {
206
332
  return;
207
333
  logged = true;
208
334
  if (admitted)
209
- counters.admittedInflight--;
335
+ counters.admittedInflight--; // 修1: paired release (latched — one decrement per admitted request)
210
336
  const route = routeLabel(method, url);
211
337
  const seconds = (Date.now() - startedAt) / 1000;
212
338
  const aborted = viaClose && !res.writableEnded;
@@ -225,27 +351,54 @@ export function createHttpServer(rawDeps) {
225
351
  return;
226
352
  }
227
353
  if (req.method === "GET" && url === "/health") {
354
+ // Sema-registry restart signal: additive, present ONLY when a restart-to-apply slice changed
355
+ // since boot — so a normal worker's /health shape is unchanged. `restartRequired` is the boolean the
356
+ // orchestrator polls; `restart` carries which slices + the version + when it first became required.
228
357
  const restart = deps.restartState?.();
358
+ // TOC integration (#1 /health contract — INTEGRATION-DESIGN): identity fields so the shell's Lifecycle
359
+ // Manager can verify it connected to the RIGHT engine (instance + data dir + config fingerprint + pid/port), not
360
+ // a stale/wrong one. configHash = a STABLE fingerprint of the restart-relevant config shape (NO secrets — only
361
+ // model id / backends / data root / provider / port), so the shell detects a config drift without reading values.
229
362
  const configHash = createHash("sha256")
230
363
  .update(JSON.stringify({ model: deps.config.model.id, sessionBackend: deps.config.sessionBackend, dbBackend: deps.config.dbBackend, dataRoot: deps.config.localDataRoot, configProvider: deps.config.configProvider, remoteExec: deps.config.remoteExec?.provider, port: deps.config.port }))
231
364
  .digest("hex")
232
365
  .slice(0, 16);
233
366
  sendJson(res, 200, {
234
367
  status: "ok",
368
+ // build self-description — "did the restart actually pick up new code" judgement for the
369
+ // shell/orchestrator (same source as GET /v1/capabilities.version; absent only if capabilities wiring is).
235
370
  ...(typeof deps.capabilities?.version === "string" ? { version: deps.capabilities.version } : {}),
371
+ // additive draining flag — k8s readiness / shell Lifecycle Manager摘流 signal. Absent when not draining.
236
372
  ...(deps.drainState?.draining ? { draining: true, ...(deps.drainState.since ? { drainingSince: deps.drainState.since } : {}) } : {}),
373
+ // S5: additive degradation flag — present ONLY when the boot auto-probe fell back to in-memory, so a
374
+ // healthy worker's /health shape is unchanged. durable:false = "this replica is NOT persisting although
375
+ // a DB was configured" (the orchestrator's restart/alarm signal; an in-memory-by-choice worker omits it).
237
376
  ...(deps.storeDegraded ? { durable: false, storeDegraded: true } : {}),
377
+ // boot ready 门(b):additive — absent = ready(正常形状不变);false = roster 未落地,
378
+ // 计费提交在 503(orchestrator/壳读此旗决定摘流/重试;编排器侧「pull 成功才 ready」增强不互斥)。
238
379
  ...(deps.modelReady && !deps.modelReady() ? { ready: false, readyReason: "model roster pending (first effective-config pull)" } : {}),
239
380
  model: deps.config.model.id,
240
- sessionBackend: deps.sessionStoreLabel ?? deps.config.sessionBackend,
381
+ sessionBackend: deps.sessionStoreLabel ?? deps.config.sessionBackend, // effective store, not the coerced enum
241
382
  sessions: deps.runner.sessions.size,
242
383
  ...(deps.instanceId ? { instanceId: deps.instanceId } : {}),
384
+ // 🔴 `dataRoot`/`pid` 是**公开承诺字段**(2026-07-28 起,USAGE「嵌入形契约」段):桌面宿主的
385
+ // 引擎身份验证(「这个 /health 是不是我起的那个进程」)整个押在这两个键上——pid 必须等于所
386
+ // spawn 子进程、dataRoot 必须等于所配数据根,缺任一=判外来。localDataRoot 的解析恒回退
387
+ // `~/.ai-agent`(config.ts,与 DB_BACKEND 无关)⇒ 此条件 spread 实为恒真;改动这两个键的
388
+ // 在场性/语义前先看 test/health-identity-contract.test.ts。
243
389
  ...(deps.config.localDataRoot ? { dataRoot: deps.config.localDataRoot } : {}),
244
390
  configHash,
245
391
  pid: process.pid,
246
392
  port: deps.config.port,
393
+ // D-G: a POSITIVE go-live signal that this worker is a crypto-bound direct-door worker (trusted-header
394
+ // path disabled) — additive, only present when active, so a normal worker's /health shape is unchanged.
395
+ // Surfaced for the DG-M2 go-live runbook (was previously inferrable only behaviorally via a 401).
247
396
  ...(deps.config.directDoorActive ? { directDoorActive: true } : {}),
248
397
  ...(restart ? { restartRequired: true, restart: { reasons: restart.reasons, version: restart.version, since: restart.since } } : {}),
398
+ // codex R18 (models-tiers deferral): additive DEGRADED state — the model plane is deferred but no
399
+ // next-boot handoff exists (LKG not restart-surviving), so no restart is forced and the plane stays
400
+ // unapplied. Present ⇒ the operator must either mount restart-surviving LKG storage (+ set
401
+ // CONFIG_LKG_DURABLE) or restart manually after ensuring the candidate will be re-pulled at boot.
249
402
  ...(() => {
250
403
  const stuck = deps.planeDeferredState?.();
251
404
  return stuck ? { modelPlaneDeferred: { version: stuck.version, since: stuck.since, noHandoff: true, ...(stuck.blocked ? { blockedReasons: stuck.blocked } : {}) } } : {};
@@ -253,6 +406,9 @@ export function createHttpServer(rawDeps) {
253
406
  });
254
407
  return;
255
408
  }
409
+ // Metrics (read-only): authorized by EITHER the full authToken OR a read-only metricsToken — so a
410
+ // control plane (sema-registry) can pull metrics fleet-wide with one token, never holding each
411
+ // worker's full authToken. Handled BEFORE the global gate so metricsToken-only callers aren't 401'd.
256
412
  if (req.method === "GET" && (url === "/metrics" || url === "/metrics/summary" || url === "/metrics/plan-cache")) {
257
413
  if (!deps.metrics) {
258
414
  sendError(res, 404, "feature.metrics_disabled", "metrics disabled");
@@ -269,6 +425,7 @@ export function createHttpServer(rawDeps) {
269
425
  sendJson(res, 200, { model: deps.config.model.id, ...deps.metrics.summarize() });
270
426
  return;
271
427
  }
428
+ // Per-scope objective-recurrence detail (core design/42 handoff). Same token gate as /metrics.
272
429
  if (url === "/metrics/plan-cache") {
273
430
  sendJson(res, 200, { scopes: deps.planCacheProbe?.dump() ?? {} });
274
431
  return;
@@ -277,7 +434,14 @@ export function createHttpServer(rawDeps) {
277
434
  res.end(deps.metrics.render());
278
435
  return;
279
436
  }
437
+ // 鉴权桥预认证:在任何 principal-gated 路由(trace/fleet/workflows/authorizer)读取身份之【前】,
438
+ // 若 Authorization bearer 形似 registry-JWT 且 verifier 在位,验它——通过则把 sub 挂进 request-local
439
+ // (setSsoPrincipal),verifiedPrincipal 单一真源随后优先读它(directDoor 与 BFF 两部署同效),并同时写入
440
+ // principalHeader(小写键)供 principalFrom 直读处使用。验证不通过=不设身份,后续 systemFor 因 bearer 不匹配
441
+ // 任何静态 token 而 401(恒 generic,R9:错误码只进日志)。非 JWT-形 bearer(静态 token)= looksLikeJwt
442
+ // 跳过,零开销,既有调用方字节不变。
280
443
  if (deps.registryJwtVerifier) {
444
+ // 只认 Bearer scheme(异源复审 INFO:`Basic a.b.c` 之前会白跑一次 verify + 一条 rejected 记账噪声)。
281
445
  const authz = req.headers.authorization ?? "";
282
446
  const rawBearer = /^Bearer\s/i.test(authz) ? authz.replace(/^Bearer\s+/i, "") : "";
283
447
  if (rawBearer && looksLikeJwt(rawBearer)) {
@@ -285,23 +449,35 @@ export function createHttpServer(rawDeps) {
285
449
  if (v.ok) {
286
450
  setSsoPrincipal(req, v.identity.principal);
287
451
  req.headers[deps.config.principalHeader.toLowerCase()] = v.identity.principal;
452
+ // scope claim 也挂 request-local(多租户活动租户;registry-core 0.3.0+ VerifiedIdentity.scope)。
453
+ // 用途=审计归因 + 未来 scope 级 quota/过滤的读取地基。同时记一条 info 审计锚(每认证请求一条):
454
+ // 此前 principal 入了 owner 但 scope 全程不落任何日志/审计(grep 实证零命中 scope)——补齐。
288
455
  if (v.identity.scope)
289
456
  setSsoScope(req, v.identity.scope);
290
457
  deps.logger?.info?.("auth_bridge_principal_accepted", { principal: v.identity.principal, ...(v.identity.scope ? { scope: v.identity.scope } : {}) });
291
458
  }
292
459
  }
293
460
  }
461
+ // design/158 A9 域模块:Task Trace 只读面 + 用量统计面(routes/trace-usage.ts)。
462
+ // ⚠️ 位置即契约:必须留在下面的全局 service-credential 门**之前**(traceToken-only 调用方否则先被 401)。
294
463
  if (await handleTraceUsage(req, res, url, ctx))
295
464
  return;
465
+ // design/158 A9 域模块:S8 自编排 workflow 只读三路(routes/workflows.ts)。
466
+ // 位置同 trace/fleet:全局 service-credential 门之前,块内 scope 门=这些读的唯一租户边界。
296
467
  if (await handleWorkflows(req, res, url, ctx))
297
468
  return;
469
+ // design/158 A9 域模块:MF-Fleet 实时舰队流(routes/fleet.ts)。服务凭据门之前,按 principal 自门。
298
470
  if (await handleFleet(req, res, url, ctx))
299
471
  return;
472
+ // Service-credential gate + SOURCE derivation: which system's token authenticated this request.
473
+ // `source` is set ONLY here, from the credential — a caller cannot claim another system's identity.
300
474
  let source = null;
301
475
  const anyServiceAuth = Boolean(deps.config.authToken) || Object.keys(deps.config.authTokens ?? {}).length > 0;
302
476
  if (anyServiceAuth) {
303
477
  const sys = systemFor(req, deps.config);
304
478
  if (sys === undefined) {
479
+ // 鉴权桥:静态 service-token 都不匹配 → 若入口预认证已把一个验过的 registry-JWT principal
480
+ // 挂进 request-local(ssoVerifiedPrincipal),即用户直连(source="sso");否则无凭据 401。
305
481
  if (ssoVerifiedPrincipal(req)) {
306
482
  source = "sso";
307
483
  }
@@ -314,7 +490,16 @@ export function createHttpServer(rawDeps) {
314
490
  source = sys;
315
491
  }
316
492
  }
493
+ // design/158 A9:`source` 的**唯一**赋值段就在上面;域模块经 ctx.req.source 读到同一个值(单向镜像,
494
+ // 此行之后 handle() 不再改它)。
317
495
  reqState.source = source;
496
+ // Mandatory service token on submission endpoints (center: a worker MUST validate a service-to-service
497
+ // token on task creation — not "if auth is configured" — else anything in-cluster can submit & bill
498
+ // directly, bypassing sema-registry's auth/audit). Fail-closed: the POST task/run/leader endpoints refuse
499
+ // when no authToken is set, unless explicitly opted out for local dev (ALLOW_UNAUTHED_WRITES=true).
500
+ // The bake door (POST /v1/images/bakes*) is build-host-RCE-capable and authed by the Bearer-token-no-cookie
501
+ // model (§P2.4b) — it MUST also refuse when no service token is configured (else a forged principal header
502
+ // alone could reach an operator-gated RCE door).
318
503
  if (req.method === "POST" &&
319
504
  !anyServiceAuth &&
320
505
  !deps.config.allowUnauthedWrites &&
@@ -322,16 +507,31 @@ export function createHttpServer(rawDeps) {
322
507
  sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting task submissions");
323
508
  return;
324
509
  }
510
+ // DRAINING — this instance is shutting down gracefully (SIGTERM received, in-flight turns being
511
+ // waited on). Refuse NEW model-running work (the same billable submit set: task/stream/runs/leader + the
512
+ // resume-flavored assistant verbs + approvals responds — each starts a model turn) with 503 + Retry-After so
513
+ // the shell/orchestrator retries against the replacement. Everything else stays open: reads (events/health/
514
+ // trace), cancel, steer/respond on in-flight runs — those help the drained turns finish, they don't start new ones.
325
515
  if (deps.drainState?.draining && req.method === "POST" && isBillableSubmitPath(url)) {
326
516
  res.setHeader("retry-after", "15");
517
+ // errorCode 补齐(2026-07-28 核查:cli 的 pre-stream drain 重试只能靠 `error` 文案字面判型——
518
+ // SDK 的 code 臂恒 undefined 是死码;server 改文案=对端重试静默失效)。`error:"draining"` 字面
519
+ // 自此视为**冻结的 wire 契约**(旧消费端判的就是它),errorCode 是机器码正门。
327
520
  sendError(res, 503, "draining", "draining", { message: "this instance is draining for shutdown/upgrade — retry against the replacement instance" });
328
521
  return;
329
522
  }
523
+ // boot ready 门(b)(E3 workers.model 废除的 fail-closed 半场):registry 部署且无显式 env 模型时,
524
+ // 首次 effective pull 落 roster 之前 worker 只有占位模型——计费提交打占位=垃圾任务,503 让编排器/壳重试
525
+ // 到 roster 落地(通常 <秒)。镜像 draining 姿势(同 billable 集合;读面/cancel/steer 全开);/health 带
526
+ // ready:false 加性旗。env 模型在(操作员显式声明)或非 registry 部署=恒 ready,现有部署零影响。
330
527
  if (deps.modelReady && !deps.modelReady() && req.method === "POST" && isBillableSubmitPath(url)) {
331
528
  res.setHeader("retry-after", "5");
332
529
  sendError(res, 503, "state.model_roster_pending", "model_roster_pending", { message: "this worker has no model yet (waiting for the first effective-config pull to land the roster) — retry shortly" });
333
530
  return;
334
531
  }
532
+ // E6 (review): the operator session-policy PUT is a mutating operator-gated write — apply the SAME fail-closed
533
+ // no-service-token guard as the POST submit paths (an un-authed worker must not accept policy writes from anything
534
+ // in-cluster; without it a forged principal header alone could tighten/DoS a session's tools).
335
535
  if (req.method === "PUT" &&
336
536
  !anyServiceAuth &&
337
537
  !deps.config.allowUnauthedWrites &&
@@ -339,62 +539,99 @@ export function createHttpServer(rawDeps) {
339
539
  sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting session-policy writes");
340
540
  return;
341
541
  }
542
+ // design/158 A9:能力/场景/模型目录域(routes/capabilities.ts)。位置=全局 service-credential 门之后、
543
+ // 任务提交面之前,与拆分前逐行同序。
342
544
  if (await handleCapabilities(req, res, url, ctx))
343
545
  return;
546
+ // design/158 A9 域模块:sideQuery 一次性问答面(routes/side-query.ts)。
344
547
  if (await handleSideQuery(req, res, url, ctx))
345
548
  return;
549
+ // design/158 A9 域模块:同步提交面 POST /v1/tasks · /v1/tasks/stream(routes/tasks.ts;B3 三腿具名化在内)。
346
550
  if (await handleTasks(req, res, url, ctx))
347
551
  return;
552
+ // design/158 A9 域模块:异步 run 面 + run 级动词(routes/runs.ts)。与 tasks 域共用 ctx.legs.prepareSpec。
348
553
  if (await handleRuns(req, res, url, ctx))
349
554
  return;
555
+ // design/158 A9 域模块:SVC-5 workflow agent steer(routes/workflows.ts,与上面的只读三路同域)。
350
556
  if (await handleWorkflowAgentSteer(req, res, url, ctx))
351
557
  return;
558
+ // design/158 A9 域模块:子代理 / 任务句柄 / 三条 HITL 应答口(routes/runs.ts,与上面的 run 面同域)。
352
559
  if (await handleRunVerbs(req, res, url, ctx))
353
560
  return;
561
+ // design/158 A9 域模块:v2 leader 端点(routes/leader.ts)。
354
562
  if (await handleLeader(req, res, url, ctx))
355
563
  return;
564
+ // design/158 A9:sandbox-image-pool 控制面(routes/images.ts;bakes 段在 images 段之前的顺序由域模块内部保住)。
356
565
  if (await handleImages(req, res, url, ctx))
357
566
  return;
567
+ // design/158 A9 域模块:人在环审批面(durable F4 + legacy)与 assistant-scheduler 三动词
568
+ // (routes/approvals-assistant.ts)。续跑一律经 ctx.legs 的 resume 家族。
358
569
  if (await handleApprovalsAssistant(req, res, url, ctx))
359
570
  return;
571
+ // design/158 A9:观测只读面(routes/observability.ts):/v1/outcomes + /v1/usage。
360
572
  if (await handleObservability(req, res, url, ctx))
361
573
  return;
574
+ // design/158 A9:memory 导出/同步 + /v1/policy 只读面(routes/memory-policy.ts)。
362
575
  if (await handleMemoryPolicy(req, res, url, ctx))
363
576
  return;
577
+ // design/158 A9:会话/发件链接列表面(routes/sessions-list.ts)。
364
578
  if (await handleSessionsList(req, res, url, ctx))
365
579
  return;
580
+ // design/158 A9:单会话面(routes/sessions.ts:init/settings/mcp/head/events/audit/fork/delete/policy/workspace)。
366
581
  if (await handleSessions(req, res, url, ctx))
367
582
  return;
583
+ // design/158 A9:2c session-sync 面(routes/session-sync.ts)。
368
584
  if (await handleSessionSync(req, res, url, ctx))
369
585
  return;
586
+ // design/158 A9:D-1 通用附件面(routes/attachments.ts)。
370
587
  if (await handleAttachments(req, res, url, ctx))
371
588
  return;
589
+ // design/158 A9 域模块:会话外事件注入 / 唤醒(routes/notify-wake.ts)。
372
590
  if (await handleNotifyWake(req, res, url, ctx))
373
591
  return;
374
592
  sendError(res, 404, "not_found.route", "not found");
375
593
  }
594
+ /** Read + validate the body, authorize, and build the spec. Returns null if it already responded. */
376
595
  async function prepareSpec(req, res) {
377
596
  const body = (await readJson(req));
378
597
  if (!body || typeof body.objective !== "string") {
379
598
  sendError(res, 400, "request.field_invalid", "missing 'objective' string");
380
599
  return null;
381
600
  }
601
+ // [920] 空毒帧第一道门:空/纯空白 objective 会成为持久历史里的空 user 帧——严格网关(anthropic/kimi)
602
+ // 对空 content 400,且一帧毒全 session(同角色 merge 后残留空 block,存量救不了)。core 半场(装配
603
+ // 自愈+intake 拒)在修;HTTP 面 fail-loud 是宪法「诚实优先」形:空任务无合法用例,静默吞掉才是坑。
382
604
  if (body.objective.trim().length === 0) {
383
605
  sendError(res, 400, "request.field_invalid", "objective must not be empty or whitespace-only (an empty user message poisons the session history on strict providers)");
384
606
  return null;
385
607
  }
608
+ // jobId (work-view correlation): optional, but if present must be a non-empty string ≤64 chars (the column
609
+ // width) — reject a malformed one up front rather than silently truncating/dropping it at the INSERT.
386
610
  if (body.jobId !== undefined && (typeof body.jobId !== "string" || body.jobId.length === 0 || body.jobId.length > 64)) {
387
611
  sendError(res, 400, "request.field_invalid", "jobId must be a non-empty string of at most 64 characters");
388
612
  return null;
389
613
  }
614
+ // [#15] sessionId: a client MAY supply an ARBITRARY id (deliberate asymmetry vs fork/delete — see security.ts
615
+ // `isUuidV7` doc: run-local `--session`, dev anonymous-share ids; the wire contract types sessionId as an opaque
616
+ // string), but it must fit the `session_id VARCHAR(64)` column. Reject an over-long one up front rather than
617
+ // 500/truncating at the INSERT (the authorizer's register() runs later in this function, after this block via the
618
+ // deps.authorize() call — so the cap fires before any INSERT). LENGTH-ONLY — NOT a uuidv7 shape
619
+ // check (that would break the permissive submit contract); submit stays lenient, fork/delete keep the uuidv7 gate.
390
620
  if (typeof body.sessionId === "string" && body.sessionId.length > 64) {
391
621
  sendError(res, 400, "request.field_invalid", "sessionId must be at most 64 characters");
392
622
  return null;
393
623
  }
624
+ // Personalization caps: once users can edit these, an uncapped prompt/skill body is a
625
+ // cost hole — reject up front, server-pinned (a UI may be stricter, never looser).
394
626
  if (typeof body.systemPrompt === "string" && body.systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS) {
395
627
  sendError(res, 400, "request.field_invalid", `systemPrompt must be at most ${MAX_SYSTEM_PROMPT_CHARS} characters`);
396
628
  return null;
397
629
  }
630
+ // appendSystemPrompt ([1478] R2 + codex R7 F2): fail-loud SHAPE gate on fresh submits — a defined value must
631
+ // be a non-empty string within the systemPrompt cap. A non-string/empty value would be silently normalized to
632
+ // "absent" by resolveSpec's defensive accept (a client serialization bug then runs the task WITHOUT its
633
+ // product-knowledge block — the silent-drop class again); the cap matches systemPrompt (same prompt surface,
634
+ // same per-turn cost hole). Stored resume bodies stay on the defensive drop+warn (acceptAppendSystemPrompt).
398
635
  if (body.appendSystemPrompt !== undefined) {
399
636
  if (typeof body.appendSystemPrompt !== "string" || body.appendSystemPrompt.length === 0) {
400
637
  sendError(res, 400, "request.field_invalid", "appendSystemPrompt must be a non-empty string");
@@ -405,10 +642,23 @@ export function createHttpServer(rawDeps) {
405
642
  return null;
406
643
  }
407
644
  }
645
+ // codex F3/R3/R4 (capability honesty): the append-less-pack combos (rider / settings.outputStyle meeting an
646
+ // already-assembled systemPrompt or a center assembled-identity pack) are adjudicated INSIDE resolveSpec —
647
+ // it alone derives the effective scenario (principal ruling + defaultScenario) and holds the center-pack
648
+ // snapshot the provider pick uses, so an HTTP-side probe could disagree with the actual assembly (R4:
649
+ // scenario mismatch / centerPrompts refresh TOCTOU). This route passes `leg:"fresh"` below, which makes
650
+ // resolveSpec 400 fail-loud (HttpError, surfaced by the catch around prepareSpec's caller); resume families
651
+ // omit it and get the drop+warn mirror instead.
652
+ // [#40 / TOC cwd seam] body.cwd must be an ABSOLUTE host path — fail-loud (a relative cwd would silently resolve
653
+ // against the SERVICE process cwd, not the user's project — TOC review #12). Whether it is HONORED is separately
654
+ // gated to the single-user host lane (resolveSpec); validating shape here is independent of that gate.
408
655
  if (body.cwd !== undefined && !isValidCwd(body.cwd)) {
409
656
  sendError(res, 400, "request.field_invalid", "cwd must be a non-empty absolute host path");
410
657
  return null;
411
658
  }
659
+ // design/119 (CC --add-dir) body.additionalDirectories — fail-loud shape on submit (mirrors cwd; each entry is a
660
+ // host path with the SAME absolute/no-`..` rule). Whether it is HONORED is separately gated to the host lane
661
+ // (resolveSpec). An empty array is fine (no-op); a non-array or an entry failing the cwd shape is a client error.
412
662
  const addDirsRaw = body.additionalDirectories;
413
663
  if (addDirsRaw !== undefined) {
414
664
  if (!Array.isArray(addDirsRaw)) {
@@ -425,6 +675,9 @@ export function createHttpServer(rawDeps) {
425
675
  return null;
426
676
  }
427
677
  }
678
+ // body.settings (client SemaSettings stamp) — fail-loud shape/size caps on submit, matching the sibling
679
+ // fields' posture (parseTaskSettings is also defensive on every path incl. resume; this 400s a bad submit early
680
+ // rather than silently coercing — adversarial review). The semantic projection (tighten-only) happens in resolveSpec.
428
681
  const settingsRaw = body.settings;
429
682
  if (settingsRaw !== undefined && settingsRaw !== null) {
430
683
  if (typeof settingsRaw !== "object" || Array.isArray(settingsRaw)) {
@@ -436,6 +689,8 @@ export function createHttpServer(rawDeps) {
436
689
  sendError(res, 400, "request.field_invalid", `settings.outputStyle must be at most ${MAX_SETTINGS_OUTPUT_STYLE_CHARS} characters`);
437
690
  return null;
438
691
  }
692
+ // codex R14: BOTH append carriers fold into ONE spec field (applyTaskSettings: `append\n\nstyle`) — two
693
+ // individually-valid values can exceed the documented cap combined. One aggregate gate, same bound.
439
694
  if (typeof st.outputStyle === "string" &&
440
695
  st.outputStyle.length > 0 &&
441
696
  typeof body.appendSystemPrompt === "string" &&
@@ -453,6 +708,8 @@ export function createHttpServer(rawDeps) {
453
708
  }
454
709
  }
455
710
  }
711
+ // settings.env (R-survey) — fail-loud on submit, parity with outputStyle/permissions (adversarial-review LOW). The
712
+ // defensive parse `cleanEnvMap` also caps these on every path (resume re-runs WITHOUT this gate), this just 400s a fresh submit.
456
713
  const envRaw = st.env;
457
714
  if (envRaw !== undefined && envRaw !== null) {
458
715
  if (typeof envRaw !== "object" || Array.isArray(envRaw)) {
@@ -471,10 +728,16 @@ export function createHttpServer(rawDeps) {
471
728
  }
472
729
  }
473
730
  }
731
+ // L2 ultracode (design/111): fail-loud on a non-boolean (parity with the other settings caps; parseTaskSettings
732
+ // silently ignores non-true on the resume paths the HTTP gate doesn't cover).
474
733
  if (st.ultracode !== undefined && typeof st.ultracode !== "boolean") {
475
734
  sendError(res, 400, "request.field_invalid", "settings.ultracode must be a boolean");
476
735
  return null;
477
736
  }
737
+ // settings.hooks(hook-runner 阶段一)— fail-loud on submit(131/132/133 邻居姿势):一个 malformed
738
+ // hooks 配置被静默丢弃 = 用户以为在生效的 deny 钩子其实没跑(安全预期破坏),必须 400。契约校验 =
739
+ // registry-core 0.1.51 HooksConfig + 服务侧上限(parseHooksConfig);resume 路径由 parseTaskSettings 防御
740
+ // (malformed → deferred 报告+warn)。是否 HONOR 另由单用户闸在 resolveSpec 决定(多租户=忽略非 400)。
478
741
  if (st.hooks !== undefined && st.hooks !== null) {
479
742
  const hooksParsed = parseHooksConfig(st.hooks);
480
743
  if (!hooksParsed.config) {
@@ -488,6 +751,9 @@ export function createHttpServer(rawDeps) {
488
751
  sendError(res, 400, "request.field_invalid", skillsErr);
489
752
  return null;
490
753
  }
754
+ // body.images (CC parity): cap COUNT + per-image inline base64 size → 413 (the inline
755
+ // `data` is otherwise passed raw to the spec; URL images are core-gated). MAX_BODY already bounds total bytes; this
756
+ // adds the missing count cap (anti decode-DoS) + an explicit per-image 413. Fail-loud parity with the settings caps.
491
757
  if (Array.isArray(body.images)) {
492
758
  if (body.images.length > MAX_IMAGES_PER_REQUEST) {
493
759
  sendError(res, 413, "request.payload_too_large", `at most ${MAX_IMAGES_PER_REQUEST} images per request`);
@@ -501,6 +767,10 @@ export function createHttpServer(rawDeps) {
501
767
  }
502
768
  }
503
769
  }
770
+ // design/112 C1: validate the client context shape + cap each field. The body is untrusted and timeZone/userEmail
771
+ // flow into core's env block — an object with optional string timeZone/userEmail; reject non-strings / over-long
772
+ // values (anti-DoS + anti env-block-injection bloat). core additionally validates timeZone semantically (invalid
773
+ // IANA zone → UTC, never mislabeled). locale was removed in core 1.186 (dead field) so it is not accepted here.
504
774
  if (body.clientContext !== undefined) {
505
775
  const cc = body.clientContext;
506
776
  if (typeof cc !== "object" || cc === null || Array.isArray(cc)) {
@@ -516,6 +786,9 @@ export function createHttpServer(rawDeps) {
516
786
  return null;
517
787
  }
518
788
  }
789
+ // §7 P0.5 sandbox image selection: shape-validate here (a non-empty profile string; capabilitiesNeeded a
790
+ // string[]). The per-principal visibility re-admit + profile→digest resolution happen in resolveSpec (it
791
+ // holds the imageIndex + the trusted principal); a 400/404 there surfaces via the HttpError catch below.
519
792
  if (body.sandboxImageProfile !== undefined && (typeof body.sandboxImageProfile !== "string" || body.sandboxImageProfile.length === 0 || body.sandboxImageProfile.length > 128)) {
520
793
  sendError(res, 400, "request.field_invalid", "sandboxImageProfile must be a non-empty string of at most 128 characters");
521
794
  return null;
@@ -524,14 +797,23 @@ export function createHttpServer(rawDeps) {
524
797
  sendError(res, 400, "request.field_invalid", "capabilitiesNeeded must be an array of strings");
525
798
  return null;
526
799
  }
800
+ // capabilitiesNeeded is a constraint ON the selected image — meaningless without a profile to select (it would
801
+ // otherwise be silently ignored while the task runs the unchecked worker-global default). Adversarial-review LOW.
527
802
  if (Array.isArray(body.capabilitiesNeeded) && body.capabilitiesNeeded.length > 0 && (typeof body.sandboxImageProfile !== "string" || body.sandboxImageProfile.length === 0)) {
528
803
  sendError(res, 400, "request.field_conflict", "capabilitiesNeeded requires sandboxImageProfile (it constrains the selected image)");
529
804
  return null;
530
805
  }
806
+ // §7 P0.5 + the multi-run orchestration gates don't compose in v1: the per-task image binds by sessionId, but
807
+ // `cascade` strips sessionId per rung and `verify` runs its verifier on a separate session — both would
808
+ // silently fall back to the worker-global default image. Reject the combo FAIL-LOUD rather than run the wrong
809
+ // image (adversarial-review round-2 MEDIUM). Lift when core propagates the image binding to those sub-runs.
531
810
  if (typeof body.sandboxImageProfile === "string" && body.sandboxImageProfile.length > 0 && (body.cascade === true || body.verify === true)) {
532
811
  sendError(res, 400, "request.field_conflict", "sandboxImageProfile is not supported together with cascade/verify in v1 (the per-task image would not bind to the cascade rungs / verifier sub-run)");
533
812
  return null;
534
813
  }
814
+ // Structured output (CC --json-schema): a plain JSON-schema OBJECT, size-capped (an uncapped schema is a
815
+ // prompt-bloat / cost hole, same posture as systemPrompt/skills). Deep schema validity is core's job — a
816
+ // malformed schema fails the task with a typed core error, not a 400 here. Threaded into spec by resolveSpec.
535
817
  if (body.outputSchema !== undefined) {
536
818
  if (typeof body.outputSchema !== "object" || body.outputSchema === null || Array.isArray(body.outputSchema)) {
537
819
  sendError(res, 400, "request.field_invalid", "outputSchema must be a JSON Schema object");
@@ -542,10 +824,19 @@ export function createHttpServer(rawDeps) {
542
824
  return null;
543
825
  }
544
826
  }
827
+ // E7 (shell-host contract): reasoning-effort selection (the `/effort` picker), mapped to core's ThinkingLevel in
828
+ // resolveSpec. Validate FAIL-LOUD — a provided-but-unknown level is a 400, never silently dropped (the body.model
829
+ // silent-drop bug class). The accepted set is core's tier scale; the picker's default set is advertised on
830
+ // /v1/models (supportedEffortLevels). Absent ⇒ core uses the resolved role's default thinking.
545
831
  if (body.reasoningEffort !== undefined && !isThinkingLevel(body.reasoningEffort)) {
546
832
  sendError(res, 400, "request.field_invalid", "reasoningEffort must be one of: off, minimal, low, medium, high, xhigh, max");
547
833
  return null;
548
834
  }
835
+ // E18 (shell-host contract): resume-at handle shape. A non-empty string ≤64 (the uuidv7 eventId / entry_id column
836
+ // width). resolveSpec resolves it to a SessionTreeEntry.id (eventId→entryId anchor map) — an unknown handle 4xx's
837
+ // there. Reject combos core forbids (resumeAt + a fresh verify/cascade leg replaces/strips the session per
838
+ // rung/sub-run, so resume-at is meaningless there — fail-loud rather than silently branch a session that's about
839
+ // to be replaced), mirroring the sandboxImageProfile+cascade/verify reject.
549
840
  if (body.resumeAt !== undefined) {
550
841
  if (typeof body.resumeAt !== "string" || body.resumeAt.length === 0 || body.resumeAt.length > 64) {
551
842
  sendError(res, 400, "request.field_invalid", "resumeAt must be a non-empty message eventId string (≤64 chars)");
@@ -556,6 +847,8 @@ export function createHttpServer(rawDeps) {
556
847
  return null;
557
848
  }
558
849
  }
850
+ // [833] rewind exclusive mode: two enum values only, and only riding a resumeAt (alone it qualifies nothing — a
851
+ // silent no-op is the body.model silent-drop bug class, so fail loud). Absent ⇒ core default "at" (zero regression).
559
852
  if (body.resumeAtMode !== undefined) {
560
853
  if (body.resumeAtMode !== "at" && body.resumeAtMode !== "before") {
561
854
  sendError(res, 400, "request.field_invalid", 'resumeAtMode must be "at" or "before"');
@@ -566,6 +859,9 @@ export function createHttpServer(rawDeps) {
566
859
  return null;
567
860
  }
568
861
  }
862
+ // E12 (shell-host contract): prompt-suggestions opt-in. `true`/`false` or an object `{count?, role?}`. Validate
863
+ // FAIL-LOUD (a malformed value is a 400, not a silent off — the body.model silent-drop bug class); resolveSpec then
864
+ // normalizes (and re-normalizes defensively on resume, which skips this HTTP validation).
569
865
  if (body.suggestNextPrompts !== undefined && typeof body.suggestNextPrompts !== "boolean") {
570
866
  const s = body.suggestNextPrompts;
571
867
  if (typeof s !== "object" || s === null || Array.isArray(s)) {
@@ -582,22 +878,33 @@ export function createHttpServer(rawDeps) {
582
878
  return null;
583
879
  }
584
880
  }
881
+ // E19 (shell-host contract): rewind-files opt-in must be a boolean (fail-loud, not silently dropped).
585
882
  if (body.rewindFiles !== undefined && typeof body.rewindFiles !== "boolean") {
586
883
  sendError(res, 400, "request.field_invalid", "rewindFiles must be a boolean");
587
884
  return null;
588
885
  }
886
+ // MF-30 memory PAUSE (option B): the per-request write toggle must be a boolean (fail-loud — a malformed value
887
+ // silently writing to memory when the user asked to pause is exactly the silent-drop bug class).
589
888
  if (body.memoryWrite !== undefined && typeof body.memoryWrite !== "boolean") {
590
889
  sendError(res, 400, "request.field_invalid", "memoryWrite must be a boolean (false = pause memory writes for this run)");
591
890
  return null;
592
891
  }
892
+ // design/114 Phase3: requireExistingSession opt-in must be a boolean (fail-loud — a truthy non-boolean silently
893
+ // NOT triggering the fail-loud is the very "silent fresh run" this flag exists to prevent).
593
894
  if (body.requireExistingSession !== undefined && typeof body.requireExistingSession !== "boolean") {
594
895
  sendError(res, 400, "request.field_invalid", "requireExistingSession must be a boolean");
595
896
  return null;
596
897
  }
898
+ // §4 (Fork): enableFork opt-in must be a boolean (fail-loud, not silently dropped — mirrors selfOrchestration).
597
899
  if (body.enableFork !== undefined && typeof body.enableFork !== "boolean") {
598
900
  sendError(res, 400, "request.field_invalid", "enableFork must be a boolean");
599
901
  return null;
600
902
  }
903
+ // design/131/132/133 (1.127.0): fresh-submit shape validation for the three new opt-ins — a
904
+ // malformed value must 400 here, not silently normalize to "off" (the body.model silent-drop bug class).
905
+ // resolveSpec's normalizers stay defensive for the RESUME path (which re-enters without HTTP validation).
906
+ // NOTE: `resilience.bypassBreaker=true` from a NON-operator is a shape-valid permission downgrade (dropped
907
+ // by the normalizer, not a 400) — the field's authz, not its type.
601
908
  if (body.resilience !== undefined) {
602
909
  const r = body.resilience;
603
910
  if (typeof r !== "object" || r === null || Array.isArray(r)) {
@@ -616,6 +923,11 @@ export function createHttpServer(rawDeps) {
616
923
  sendError(res, 400, "request.field_invalid", "finalVerification must be a boolean");
617
924
  return null;
618
925
  }
926
+ // [854]④: fresh-submit shape validation for per-request 配速 —— 已知三键必须是正整数,否则 400 fail-loud
927
+ // (body.model silent-drop 同类教训:静默 normalize 成「没配速」正是本件要防的假成功)。0 也拒绝:core 侧
928
+ // `timeoutSec > 0` 才生效(0=不设墙,会绕过 TASK_TIMEOUT_MAX_SEC 封顶),maxTurns/maxOutputTokens 的 0 无意义。
929
+ // 枚举外键与 resilience/attachments 同口径:容忍(只校验已知键)。resolveSpec 的 normalizeLimits 对 RESUME
930
+ // 重放路径保持 defensive(按键 DROP,不 throw)。
619
931
  if (body.limits !== undefined) {
620
932
  const l = body.limits;
621
933
  if (typeof l !== "object" || l === null || Array.isArray(l)) {
@@ -629,6 +941,9 @@ export function createHttpServer(rawDeps) {
629
941
  return null;
630
942
  }
631
943
  }
944
+ // 快审 F2(1.254):deadline 族三 opt-out 在场必须 literal false——"false"/0 等畸形值此前 200 后被
945
+ // normalizeLimits 静默丢=调用方以为关了其实恒开(B1 同类故障)。fresh fail-loud;resume 重放不过
946
+ // 此门,normalizeLimits 保持 defensive。
632
947
  for (const k of ["deadlineNudge", "callCapByDeadline", "gracefulFinalize"]) {
633
948
  const v = body.limits[k];
634
949
  if (v !== undefined && v !== false) {
@@ -637,6 +952,11 @@ export function createHttpServer(rawDeps) {
637
952
  }
638
953
  }
639
954
  }
955
+ // [876] per-task subagents(core 1.295 TaskSpec.agents):fresh-submit 白名单门 fail-loud。与 resilience/
956
+ // limits 的「容忍未知键」取舍相反 —— agents 是能力定义,静默丢字段=定义被悄悄削(丢一个 denyTools/
957
+ // isolation 就是放宽),400 点名未知键更安全。permissionMode 有意不纳(core 拍,独立批)——出现按未知键拒。
958
+ // 形状细则(name 必填且数组内唯一/各字段浅形状/≤32 条)单源在 spec-fields.ts validateTaskAgents;RESUME
959
+ // 重放不过这道门 → resolveSpec 侧 taskAgentsFromBody defensive(非法项按条 DROP+warn,不砖 resume)。
640
960
  if (body.agents !== undefined) {
641
961
  const agentsErr = validateTaskAgents(body.agents);
642
962
  if (agentsErr) {
@@ -644,14 +964,20 @@ export function createHttpServer(rawDeps) {
644
964
  return null;
645
965
  }
646
966
  }
967
+ // [922]①/[915]② interactiveTools(core 1.296 三态旋钮):boolean fail-loud 同邻居姿势。per-run 工具面
968
+ // 选择(AskUserQuestion/present_plan 挂载判据覆写),无跨租户面 ⇒ 不设租户门;undefined=core 自动判据。
647
969
  if (body.interactiveTools !== undefined && typeof body.interactiveTools !== "boolean") {
648
970
  sendError(res, 400, "request.field_invalid", "interactiveTools must be a boolean");
649
971
  return null;
650
972
  }
973
+ // [854]② retainBackgroundProcesses:boolean fail-loud(邻居姿势 —— 非 boolean 静默变「不驻留」正是
974
+ // silent-drop 病灶);单用户闸/多租忽略在 resolveSpec(retainBackgroundProcessesFromBody)。
651
975
  if (body.retainBackgroundProcesses !== undefined && typeof body.retainBackgroundProcesses !== "boolean") {
652
976
  sendError(res, 400, "request.field_invalid", "retainBackgroundProcesses must be a boolean");
653
977
  return null;
654
978
  }
979
+ // [1052]② excludeTools/deferTools:数组-of-非空-string fail-loud(邻居姿势——坏形静默丢=工具面照旧,
980
+ // 调用方以为收窄了却没收=silent-drop 病灶同族)。per-run 收窄无跨租户面 ⇒ 无租户门(interactiveTools 同)。
655
981
  for (const key of ["excludeTools", "deferTools"]) {
656
982
  const v = body[key];
657
983
  if (v !== undefined && (!Array.isArray(v) || v.some((n) => typeof n !== "string" || n.length === 0))) {
@@ -659,6 +985,7 @@ export function createHttpServer(rawDeps) {
659
985
  return null;
660
986
  }
661
987
  }
988
+ // [1144]/[1146] promptProfile:枚举 fail-loud(坏值静默丢=调用方以为切了形没切,silent-drop 病灶同族)。
662
989
  {
663
990
  const v = body.promptProfile;
664
991
  if (v !== undefined && v !== "simple" && v !== "classic") {
@@ -666,18 +993,30 @@ export function createHttpServer(rawDeps) {
666
993
  return null;
667
994
  }
668
995
  }
996
+ // [865]② fresh-submit 未知模型 400 fail-loud(clay 生产实锤:未知 body.model 静默落 default = 用户不知情
997
+ // 换模型)。🔴 门目录必须与 resolveSpec 同源 = expandTiers 增广视图(codex H1:裸 config.models 会把合法
998
+ // 档位词 "pro"/CC alias "sonnet" 当未知 400 误杀——SDK 契约明确允许它们;增广后 matchCatalogModel 对
999
+ // name/tier 词/alias/id 四形一体裁决)。restart 窗口注记与 main.ts wireCatalog 同款:tiers 热刷新 vs
1000
+ // Runner 构造期快照,门短暂超前=fail-loud 而非静默降级,诚实。"default" 是合法目录键(hasOwn 命中)。
1001
+ // RESUME 重放持久化 body 不过这道门(模型事后被移出目录时砖死 resume 比降级糟)——resolveSpec 侧对未知
1002
+ // ref warn 降级,双层与 [854] limits 同姿势。settings.model 保持 lenient(文档化,picker 路径 =
1003
+ // body.model 才是本病灶面)。
669
1004
  if (body.model !== undefined) {
670
1005
  if (typeof body.model !== "string" || body.model.length === 0) {
671
1006
  sendError(res, 400, "request.field_invalid", "model must be a non-empty string (a configured catalog name, tier word, or model id)");
672
1007
  return null;
673
1008
  }
674
- const bare = deps.config.models ?? {};
1009
+ const bare = deps.config.models ?? {}; // 防御:极简 stub deps 可无 models 域(真 loadConfig 恒有)
675
1010
  const catalog = expandTiers(bare, deps.config.tiers ?? {}) ?? bare;
676
1011
  if (matchCatalogModel(body.model, catalog) === undefined) {
677
1012
  sendError(res, 400, "request.unknown_reference", `unknown model "${body.model.slice(0, 120)}" — not in the configured catalog (name, tier word, or id)`, { available: Object.keys(catalog).filter((n) => n !== "default") });
678
1013
  return null;
679
1014
  }
680
1015
  }
1016
+ // [1479]① compactionModel — the SAME fail-loud posture + gate catalog as body.model (an unknown cheap-gear pick
1017
+ // silently burning the MAIN model's price is exactly the silent-drop bug class this train exists to kill).
1018
+ // RESUME replays skip this gate (resolveSpec drops+warns there — core's resolveModel throws on an unknown ref,
1019
+ // which would brick the resume).
681
1020
  if (body.compactionModel !== undefined) {
682
1021
  if (typeof body.compactionModel !== "string" || body.compactionModel.length === 0) {
683
1022
  sendError(res, 400, "request.field_invalid", "compactionModel must be a non-empty string (a configured catalog name, tier word, or model id)");
@@ -720,6 +1059,8 @@ export function createHttpServer(rawDeps) {
720
1059
  }
721
1060
  }
722
1061
  }
1062
+ // 历史复审轴A #3(1.254):verify 环补 cost 顶——cascade 腿一直映 costCeilingMicroUsd,verify 腿漏了
1063
+ // (每轮独立 runTask,总花费此前仅受 maxRounds 约束);同源 body.maxCostUsd,同换算。
723
1064
  const verify = body.verify === true
724
1065
  ? {
725
1066
  maxRounds: clampVerifyRounds(body.verifyRounds),
@@ -731,6 +1072,9 @@ export function createHttpServer(rawDeps) {
731
1072
  sendError(res, 400, "request.field_conflict", "verify and cascade are mutually exclusive (verify = adversarial gate; cascade = cheap→strong ladder)");
732
1073
  return null;
733
1074
  }
1075
+ // E12: suggestNextPrompts is meaningless on the verify/cascade wrappers — they return a TaskResult, not
1076
+ // a stream with core's post-completion suggestions() pass, so the suggestion event would never be produced. Reject
1077
+ // the combo fail-loud rather than silently accept-and-drop it (mirrors the resumeAt + verify/cascade reject above).
734
1078
  if (body.suggestNextPrompts && (verify || cascade)) {
735
1079
  sendError(res, 400, "request.field_conflict", "suggestNextPrompts is not supported with verify or cascade (those return a result, not a streamed run)");
736
1080
  return null;
@@ -745,18 +1089,62 @@ export function createHttpServer(rawDeps) {
745
1089
  }
746
1090
  catch (err) {
747
1091
  if (err instanceof HttpError) {
1092
+ // Typed rejection: echo the stable `code` + any structured extras (e.g. the scenario
1093
+ // allowlist) beside the human message — machine code on the wire, prose stays with the shell/web.
748
1094
  sendError(res, err.status, httpErrorCode(err.status, err.code), err.message, { ...(err.code ? { code: err.code } : {}), ...(err.extra ?? {}) });
749
1095
  return null;
750
1096
  }
751
1097
  throw err;
752
1098
  }
753
1099
  }
1100
+ // design/158 A9:`DecideBinding` 已上提到 `http/wire-types.ts` 叶子(approvals 域模块与 resume 腿同名一形),
1101
+ // 本处仅留指路;形状逐字未变。
1102
+ /**
1103
+ * The VERIFIED fleet scope for a resume publish (TOC review #6 parity with the create paths). Prefer the
1104
+ * crypto-verified `gatedPrincipal(req)` — the SAME identity GET /v1/fleet/stream filters by — on a real HTTP
1105
+ * door; fall back to the checkpoint's own scope (`auth.principal`) only when there is NO request (the internal
1106
+ * D-D SLA deny-sweep, which has no caller to verify). NEVER the spoofable `auth.principal` on a live door:
1107
+ * a spoofed header would re-publish the resumed row under a victim's scope or under "default" (vanishing from
1108
+ * its real owner's stream). Mirrors the sync/create sites' `gatedPrincipal(req, deps.config) ?? "default"`.
1109
+ *
1110
+ * ✅ FIDELITY EDGE — CLOSED by the F-fix (2026-07-01, direct-door principal spoofing). On the REQ-LESS leg (the
1111
+ * internal D-D SLA deny-sweep, `req===undefined`) we fall back to `auth.principal` = the checkpoint's stored scope.
1112
+ * Since the F-fix, createAuthorizer derives `auth.principal` (hence spec.principal, hence the checkpoint scope core
1113
+ * persists at suspend) from `verifiedPrincipal` — the VERIFIED JWT sub on a direct door, the SAME identity the
1114
+ * create path's `gatedPrincipal` tags the row with. So the earlier caveat — a misconfigured direct door where the
1115
+ * raw `x-agent-principal` ≠ the verified sub could surface a swept row under the wrong tenant's /v1/fleet/stream —
1116
+ * is GONE: the checkpoint scope is now always the verified identity, identical to what the create path tagged. The
1117
+ * "TRUE fix" once deferred here (persist the verified principal on the checkpoint at create) is exactly what the
1118
+ * F-fix did upstream at createAuthorizer; the req-less fallback and the create-path scope now agree on every posture.
1119
+ */
754
1120
  const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
755
- async function resumeCheckpoint(sessionId, decision, reason, req, answer, binding, onResumeCommitted) {
1121
+ /**
1122
+ * Durable F4 resume (design/45): map a session → its pending checkpoint token INTERNALLY,
1123
+ * rebuild the taskConfig via the same `resolveSpec` path (from the sessionId-keyed checkpoint_ctx), then
1124
+ * `runner.resume`. The capability token never leaves the service. Updates the parked run row to its new state.
1125
+ */
1126
+ async function resumeCheckpoint(sessionId, decision, reason, req, // absent on the internal D-D SLA deny-sweep (resolveSpec rebuilds auth from the checkpoint, not req)
1127
+ answer, binding, onResumeCommitted) {
756
1128
  const cs = deps.checkpointStore;
1129
+ // No scope filter here BY DESIGN (council): an operator may view/decide ANY tenant's pending approval.
1130
+ // This load-before-CAS only READS; the definitive multi-tenant guard is the
1131
+ // `AND scope = ?` inside the atomic resolve CAS — a wrong-scope decision can never EXECUTE.
757
1132
  const token = await cs.findPendingTokenBySession(sessionId);
1133
+ // D-1 approval_stale (opt-in, only when the operator echoed the checkpointToken they saw): if it no longer
1134
+ // matches the current pending — resolved, superseded by a re-mint, or expired — they are deciding a STALE
1135
+ // view. Reject distinctly from the markResuming session-CAS 409 below: the SDK keys ApprovalStaleError on
1136
+ // errorCode:"approval_stale" while the CAS 409 stays bare (→ ConflictError), so the two 409s never collide.
758
1137
  if (binding?.checkpointToken && binding.checkpointToken !== token) {
759
1138
  const seen = await cs.get(binding.checkpointToken).catch(() => null);
1139
+ // 🔴 `terminal` 只在该 token **确实属于本会话**时披露(2026-07-26 修)。
1140
+ // 此前是 `seen ? "resolved" : "expired_abort"` —— 而这次 `cs.get` 用的是**调用方给的** token 且
1141
+ // 存储层**没有 scope 谓词**(`WHERE token = ?`,连 status 都不过滤),这一步还跑在下面那个
1142
+ // `if (!token) → 404` **之前**。于是任何持凭据者都能拿一串 token 来问「它在库里吗」:
1143
+ // 命中 ⇒ `terminal:"resolved"`,不命中 ⇒ `terminal:"expired_abort"` —— **一位跨租户存在性 oracle**,
1144
+ // 与本文件自述的「no existence oracle」相反。token 本身不可猜(uuid 级),所以这不是枚举面;
1145
+ // 真实伤害是**泄漏出去的 token 可被免费确认有效性**(从日志/截图/工单捡到一串,不必也不能用它 resume,
1146
+ // 却能确认它对应一条真实 checkpoint 行、以及它是否已被解决)。
1147
+ // 归属不上就**省略这个键**(SDK 侧 `terminal?:` 本来可选)——不撒谎、也不泄露;真实原因照旧进日志。
760
1148
  const attributable = seen !== null && seen.sessionId === sessionId;
761
1149
  if (!attributable && seen !== null) {
762
1150
  deps.logger?.warn?.("approval_stale_token_foreign", { sessionId, tokenSession: seen.sessionId ?? null });
@@ -771,21 +1159,34 @@ export function createHttpServer(rawDeps) {
771
1159
  };
772
1160
  }
773
1161
  if (!token) {
1162
+ // 🔴 文案与 owner 门那条统一(2026-07-26 修)。此前两句可区分:owner 门拒 ⇒ "approval not found";
1163
+ // 本会话**没有** pending ⇒ 这条更详细的。而 `peekPendingScope` 在没有 pending 时返回 `null`、`null` 被当作
1164
+ // "开放"放过门 ⇒ 两句文案精确区分「这儿有**别人的** pending」与「这儿没有 pending」。
1165
+ // 后果:任何持凭据的租户可逐个 sessionId 判定「此刻有一条别人的待决审批」,拼出一张跨租户的
1166
+ // 「谁正卡在审批上」活动图(时序/规模情报)—— 而本文件自述承诺的正是不给这个。
1167
+ // 统一成同一句;**诊断不丢**,只是挪进日志(部署方有日志权限,外部调用方没有)。
774
1168
  deps.logger?.info?.("approval_decide_no_pending", { sessionId });
775
1169
  return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
776
1170
  }
777
- const cpPromise = cs.get(token);
1171
+ // get (checkpoint table) + getCtx (checkpoint_ctx table) are independent — load concurrently (council).
1172
+ const cpPromise = cs.get(token); // throws if the checkpoint format is newer than this build (version guard)
778
1173
  const ctxPromise = cs.getCtx(sessionId);
779
1174
  let cp;
780
1175
  try {
781
1176
  cp = await cpPromise;
782
1177
  }
783
1178
  catch (e) {
784
- void ctxPromise.catch(() => { });
1179
+ void ctxPromise.catch(() => { }); // don't leak an unhandled rejection from the parallel load
785
1180
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
786
1181
  }
787
1182
  if (!cp)
788
1183
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1184
+ // design/153 件3d parked 赎回腿([1574]/[1584]/[1588] core 裁定;src/parked-decide.ts):该 pending
1185
+ // checkpoint 若属于一个 parked 后台子代,decide 必须走 claim→裸 execute(reviveClaim) 链——legacy 腿的
1186
+ // resumeStream 会绕开 bg registry(无 consumeParkedFlip、行永 parked、生命周期分叉),故 parked-first。
1187
+ // 判别 miss(undefined)= 任务级 suspend,原样落到下方 legacy 腿(零回归)。⚠️ 门在 `req !== undefined`:
1188
+ // 内部 D-D SLA deny-sweep(req 缺席)对 parked 子代 cp 保持现状(getCtx null → 409 跳过本 tick)——
1189
+ // 过期 parked 的收割属 reaper 的 expire+reconcileParkedAgents 车道,sweep 不做重活赎回。
789
1190
  if (req !== undefined && deps.backgroundAgentStore !== undefined && deps.parkedReviveTool !== undefined) {
790
1191
  const parked = await decideParkedAgent({
791
1192
  agentStore: deps.backgroundAgentStore,
@@ -799,14 +1200,20 @@ export function createHttpServer(rawDeps) {
799
1200
  token, scope: cp.scope, sessionId: cp.sessionId, pendingAction: cp.pendingAction,
800
1201
  decision, ...(reason !== undefined ? { reason } : {}),
801
1202
  ...(binding !== undefined ? { binding: { ...(binding.boundCallId !== undefined ? { boundCallId: binding.boundCallId } : {}), ...(binding.boundInputHash !== undefined ? { boundInputHash: binding.boundInputHash } : {}), ...(binding.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}) } } : {}),
1203
+ // remember 的 grant 闭包(onResumeCommitted 的唯一现役来源)在 parked 腿以行的 root/host
1204
+ // 会话为锚透传([1591] 候裁② server 修——落子代会话则探针键永不相交,见 grantOnCommit 注);
1205
+ // answer 显式 400(复审 F6)。
802
1206
  ...(onResumeCommitted ? { grantRemember: (rootSessionId) => onResumeCommitted(rootSessionId) } : {}),
803
1207
  ...(answer !== undefined ? { answerPresent: true } : {}),
804
1208
  });
805
1209
  if (parked !== undefined) {
806
- void ctxPromise.catch(() => { });
1210
+ void ctxPromise.catch(() => { }); // 并行 ctx 读不再被消费,吞掉可能的 rejection
807
1211
  return parked;
808
1212
  }
809
1213
  }
1214
+ // Guard the ctx await too (council): if cpPromise resolved but ctxPromise REJECTS (a TiDB blip on the
1215
+ // checkpoint_ctx read), an unguarded await would escape to the generic 500 — inconsistent with the 409 the
1216
+ // cpPromise branch returns for the same class of transient load failure.
810
1217
  let ctx;
811
1218
  try {
812
1219
  ctx = await ctxPromise;
@@ -816,7 +1223,18 @@ export function createHttpServer(rawDeps) {
816
1223
  }
817
1224
  if (!ctx)
818
1225
  return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
1226
+ // Rebuild taskConfig via resolveSpec with auth reconstructed from the checkpoint (sessionId + scope).
819
1227
  const auth = { sessionId: cp.sessionId, principal: cp.scope === "_" ? undefined : cp.scope, memoryScope: ctx.memoryScope };
1228
+ // E18: STRIP resumeAt before re-resolving on a durable resume — the leaf is already set by the checkpoint, and
1229
+ // resumeAt + a durable resume is rejected by core (resume_at.conflicts_resume); also avoids re-resolving a stale
1230
+ // anchor (which could 404 if the session was purged). A resume-at run that suspended stored its body with resumeAt.
1231
+ // 对抗评审 2026-07-11(MED):resolveSpec re-runs the ADMISSION gates (scenario/execution ruling — resume does
1232
+ // NOT freeze governance, the established precedent posture; a deny is ALSO a model run, so it is gated too). But an
1233
+ // escaping HttpError here was (a) attributed to the OPERATOR's /decide request as a bare 400/403 — undiagnosable
1234
+ // from that contract — and (b) swallowed by the D-D SLA deny-sweep's catch, silently failing EVERY tick. Fold it
1235
+ // into a typed 409 result instead: the operator sees "this parked task is blocked by a policy change" (retry
1236
+ // after the policy is restored, or let terminal_at abort it); the sweep skips the row this tick (retried next
1237
+ // tick, terminal_at backstop — its documented per-call-failure semantics).
820
1238
  let spec;
821
1239
  try {
822
1240
  spec = await deps.resolveSpec({ ...ctx.body, resumeAt: undefined }, req, auth);
@@ -830,14 +1248,33 @@ export function createHttpServer(rawDeps) {
830
1248
  }
831
1249
  throw e;
832
1250
  }
1251
+ // Typed pre-check: the pending tool must still exist in the rebuilt config; a scenario
1252
+ // redeploy that removed it means the approval can no longer be applied — fail clearly, never a 500/silent.
1253
+ // The "hand" tools are NOT in spec.tools — core mounts them at the runner from the executionEnvFactory
1254
+ // (core `HAND_TOOL_EFFECTS`, not re-exported from the package root → mirrored here). When this deployment
1255
+ // has remote exec configured the rebuilt task WILL expose them, so union them in; else gating a hand tool
1256
+ // (the common code-agent case) would always 422 on resume. Keep in sync with core's HAND_TOOL_EFFECTS.
1257
+ // Q6 (core 1.161): canonicalize a PRE-Q6 persisted name (`bash`/`write_file`/…) to its CC-aligned new name
1258
+ // BEFORE matching the rebuilt (post-Q6) tool set — else an in-flight checkpoint suspended before the upgrade
1259
+ // would 422 here before core's own resume shim runs. New names map to themselves (idempotent).
833
1260
  const pendingTool = cp.pendingAction.toolName;
834
1261
  const pendingToolCanon = pendingTool ? canonicalToolName(pendingTool) : undefined;
835
1262
  const availableTools = new Set((spec.tools ?? []).map((t) => t.name));
836
1263
  if (deps.config.remoteExec)
837
1264
  for (const name of HAND_TOOL_NAMES)
838
1265
  availableTools.add(name);
1266
+ // AskUserQuestion (durable ask, TC-5.4) is ALSO not in spec.tools — core mounts it from `onQuestion`, which
1267
+ // the durable path always sets (QUESTION_AWAITS_RESUME on suspend, the answer closure on resume). Without
1268
+ // this it 422s on resume exactly like a hand tool would. Gated on checkpointStore = durable mode is active.
839
1269
  if (deps.checkpointStore)
840
1270
  availableTools.add("AskUserQuestion");
1271
+ // Workflow (codex round-2 on [1245]/[1248]②): `run_workflow` is ALSO mounted at the runner, never in
1272
+ // spec.tools — core mounts it when `spec.selfOrchestration === true` and the deployment wires the workflow
1273
+ // deps (isSelfOrchestrationActive, dist-read). Without this, approving a durably-parked workflow ask
1274
+ // (the new mode-derived leg) 422s here and the suspend can only ever be denied. Same over-approximation
1275
+ // posture as the hand-tools union above (a per-principal allowWorkflows=false revoked between suspend and
1276
+ // approve slips this pre-check; core's own resume path then fails loudly — the pre-check is a UX guard,
1277
+ // not the authority).
841
1278
  if (spec.selfOrchestration === true && deps.config.selfOrchestrationEnabled) {
842
1279
  availableTools.add("Workflow");
843
1280
  }
@@ -847,6 +1284,14 @@ export function createHttpServer(rawDeps) {
847
1284
  body: { error: `pending action no longer satisfiable: tool "${pendingToolCanon}" is not in the current task config (scenario changed since suspend)`, errorCode: "conflict.pending_action_unsatisfiable" },
848
1285
  };
849
1286
  }
1287
+ // Durable-ask request/state coherence (TC-5.4), all pre-CAS so a mismatched request never consumes the
1288
+ // checkpoint:
1289
+ // - approving a pending AskUserQuestion REQUIRES body.answer. Without it the resume re-mounts the
1290
+ // QUESTION_AWAITS_RESUME placeholder, the pending call executes against it and throws QuestionConfigError —
1291
+ // the operator's question is unrecoverably LOST (checkpoint consumed, no answer ever delivered). Fail the
1292
+ // request instead; the operator retries with the answer.
1293
+ // - an answer is only meaningful on an approve of a question gate; on a deny or a non-question gate it
1294
+ // signals the operator is deciding a different pending action than the one parked — reject, don't guess.
850
1295
  if (decision === "approve" && pendingTool === "AskUserQuestion" && !answer) {
851
1296
  return { status: 400, body: { error: "pending action is AskUserQuestion — approve must carry body.answer ({ answers: [{ header, selected: string[], note? }] })", errorCode: "request.field_conflict" } };
852
1297
  }
@@ -854,8 +1299,18 @@ export function createHttpServer(rawDeps) {
854
1299
  return { status: 400, body: { error: `body.answer is only valid when approving a pending AskUserQuestion (decision: "${decision}", pending: "${pendingTool ?? "unknown"}")`, errorCode: "request.field_conflict" } };
855
1300
  }
856
1301
  const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
1302
+ // Durable ask (TC-5.4, core 1.95): the resume re-mounts AskUserQuestion with the operator's answer
1303
+ // closure — the pending call executes against it and the model continues with "The user answered: …".
1304
+ // This OVERRIDES the suspend-side QUESTION_AWAITS_RESUME placeholder resolveSpec just rebuilt. Absent
1305
+ // answer ⇒ config unchanged (an F4 tool approval resumes exactly as before).
857
1306
  if (answer)
858
1307
  taskConfig.onQuestion = async () => answer;
1308
+ // D-1 decision-action binding (core 1.101, design/80 §2): echo the boundCallId + boundInputHash the operator
1309
+ // saw back into the outcome — core verifies opaque STRING equality against the persisted suspend-mint values
1310
+ // (fail-closed, PRE-CAS), so "saw action A, resolve B" is rejected. boundInputHash is NEVER recomputed here
1311
+ // (the engine minted it once via canonical-json; a second runtime re-serializing would false-mismatch and
1312
+ // fail-closed reject a legit approval). Legacy callers that omit the fields fall back to the current persisted
1313
+ // values (no human binding — pre-D-1 behavior). resource_limit pendings carry no action → empty (core gate_mismatch).
859
1314
  const pa = cp.pendingAction;
860
1315
  const persistedCallId = pa.kind === "tool_approval" ? pa.toolCallId : "";
861
1316
  const persistedHash = pa.kind === "tool_approval" ? (pa.boundInputHash ?? "") : "";
@@ -867,16 +1322,46 @@ export function createHttpServer(rawDeps) {
867
1322
  ...(decision === "approve" && binding?.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}),
868
1323
  ...(reason ? { reason } : {}),
869
1324
  };
1325
+ // Developer-mode verify (1.44) survives suspend/resume (core `resumeWithVerification`, design/51 P1-b): a
1326
+ // task submitted with verify:true that hit an F4 gate must STILL be gated by the adversarial verifier on
1327
+ // completion — a plain resumeStream would silently drop the gate. The flag rides in the persisted ctx.body
1328
+ // (same source the spec was rebuilt from). Like first-run verify it's multi-attempt (not a live stream), so
1329
+ // we await the result + write a single terminal event rather than streaming post-resume deltas.
870
1330
  const verifyRounds = ctx.body.verify === true
871
1331
  ? {
872
1332
  maxRounds: clampVerifyRounds(ctx.body.verifyRounds),
1333
+ // 快审 F1:resume 腿同样带 cost 顶(persisted body 的 maxCostUsd 同源;cascade 腿同换算)
873
1334
  ...(typeof ctx.body.maxCostUsd === "number" && ctx.body.maxCostUsd > 0 ? { costCeilingMicroUsd: Math.round(ctx.body.maxCostUsd * 1e6) } : {}),
874
1335
  }
875
1336
  : undefined;
1337
+ // taskId up-front: the resumed leg streams its post-resume events into the SAME durable run log, CONTINUING
1338
+ // the seq past the suspend (core 1.70 `resumeStream`) — so GET /v1/runs/:id/events covers post-resume too,
1339
+ // observability parity with runInBackground (closed by core 1.70).
1340
+ // ── Drive the resume into the durable run log via the SHARED machinery (driveResumeIntoRunLog): markResuming
1341
+ // CAS, the cancel + preempt controllers (same-replica fast path + cross-replica flag polls), the resume
1342
+ // heartbeat, the resumeStream / resumeWithVerification loop, CheckpointError classification, re-suspend vs
1343
+ // terminal drive. This HITL /decide path supplies a `policy_ask` outcome; the scheduler preempt path supplies
1344
+ // a `resource_limit`/continue one — sharing this prevents the two resume entries from drifting.
876
1345
  return driveResumeIntoRunLog({ token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(onResumeCommitted ? { onResumeCommitted } : {}) });
877
1346
  }
1347
+ /**
1348
+ * The resume-drive machinery SHARED by `resumeCheckpoint` (operator /decide, `policy_ask`) and `resumePreempted`
1349
+ * (scheduler resume, `resource_limit`/continue). Given the already-located `token`, the rebuilt
1350
+ * `taskConfig`/`resumeObjective`, the gate-specific `outcome`, and optional `verifyRounds`, it: markResuming-
1351
+ * CASes the parked row to `running` (idempotent across replicas), registers the leg in BOTH `inflightRuns`
1352
+ * (cancel) and `preemptableRuns` (re-preempt) for the same-replica fast path, runs a heartbeat that polls the
1353
+ * durable cancel + preempt flags (cross-replica), resumes via resumeStream / resumeWithVerification threading a
1354
+ * FRESH cancel signal AND a FRESH preemptSignal (so a resumed leg is itself cancel-able + RE-preempt-able —
1355
+ * never the consumed suspend-time signal), classifies a CheckpointError, handles a re-suspend
1356
+ * (setSuspended, keep the lock) vs a terminal (setTerminal), and returns the HTTP {status, body}.
1357
+ */
878
1358
  async function driveResumeIntoRunLog(args) {
879
1359
  const { token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds } = args;
1360
+ // 🔴 复审 C2:lease admission for EVERY resume family (/decide, /answer, /plan_review, preempt-resume)
1361
+ // funnels through here — keyed on `principal` = the CHECKPOINT OWNER, the SAME identity the resumed model spend
1362
+ // is billed to via `withPrincipal(principal)` below. The route guards deliberately do NOT lease-gate (they'd
1363
+ // key on the request principal = a possibly-cross-tenant operator). A live lease admits locally; center-judged
1364
+ // exhaustion of the OWNER's budget returns 429 (mirroring leaseDenied's shape); an unreachable center fails open.
880
1365
  if (deps.fleetLease && principal) {
881
1366
  const adm = await deps.fleetLease.admit(principal);
882
1367
  if (!adm.ok) {
@@ -885,22 +1370,48 @@ export function createHttpServer(rawDeps) {
885
1370
  }
886
1371
  }
887
1372
  const taskId = await deps.runStore?.getActiveTaskId(sessionId);
1373
+ // MF-Fleet (TOC review #7): a HITL resume (/decide, /answer, /plan_review, /preempt-resume, /wake) drives the
1374
+ // SAME durable run row that suspended — so it must re-publish to the fleet bus under the SAME runId, or the run
1375
+ // VANISHES from GET /v1/fleet/stream the moment it resumes (the original publisher's row was last touched at
1376
+ // suspend; nothing keeps it live or settles it after). Reuse `taskId` (the existing suspended run id) so the row
1377
+ // RE-APPEARS as the same fleet row; scope = `principal` (the checkpoint/run-owner scope persisted at suspend —
1378
+ // the original submitter, NOT spoofable; matches the run row's owner). rootTaskId = sessionId: the resume's
1379
+ // rebuilt taskConfig carries NO spec.taskId (resolveSpec omits it), so core's canonical taskId == sessionId,
1380
+ // exactly like the sync leg. No-op when no fleetBus / no taskId.
1381
+ // 🔒 scope = `fleetScope` (the VERIFIED `gatedPrincipal(req)` the calling route computed) — the SAME identity
1382
+ // GET /v1/fleet/stream filters by, mirroring the create-path #6 fix. NOT `principal` (= the spoofable
1383
+ // checkpoint/auth header from `principalFrom`): a spoofed header would re-publish the resumed row under a
1384
+ // victim's scope (cross-tenant leak) or under "default" while the real owner's stream filters it OUT (the row
1385
+ // vanishes on resume). Tenant-isolation parity with the stream filter on the direct resume door (TOC review #6).
1386
+ // onForwardEvent parity: core 1.210 added the RunInternals 4th arg to `resumeStream` — the
1387
+ // resume leg now feeds the forward sink like the sync/bg legs (fleet child rows at any depth + the durable
1388
+ // task_progress append; see the resumeStream call in driveResume).
888
1389
  const fleetPub = taskId && deps.fleetBus
889
1390
  ? fleetRunPublisher(deps.fleetBus, { runId: taskId, scope: fleetScope, rootTaskId: sessionId, ...fleetRunLabels(resumeObjective) })
890
1391
  : undefined;
1392
+ // C (TOC adversarial review): settle the resumed fleet row EXACTLY once. A plain Error thrown out of the
1393
+ // resume (not CheckpointError/cancel) hits the catch's `throw e` BEFORE the final onTerminal below → the
1394
+ // re-published row leaks as a stale "running". Route every onTerminal through this guard so the finally can
1395
+ // settle a row no branch reached, without double-settling one that did.
891
1396
  let fleetSettled = false;
892
1397
  const settleFleet = (status, residuals) => {
893
1398
  if (fleetSettled)
894
1399
  return;
895
1400
  fleetSettled = true;
896
- fleetPub?.onTerminal(status, residuals);
1401
+ fleetPub?.onTerminal(status, residuals); // [1839]①/(a):真终态帧带残局键(publisher 侧对 parked 再挡一层)
897
1402
  };
898
1403
  let result;
1404
+ // [1245] codex-5: the resume.* code ALONE does not prove the reopen landed — installed core (1.330) only
1405
+ // rewrites to checkpoint.reopen_failed when store.reopen returned FALSE; a THROWING reopen (plain DB error)
1406
+ // keeps the resume.* code with the checkpoint still resolved. Confirm against the store before re-parking:
1407
+ // this token pending again ⇒ real re-park; not pending ⇒ fall through to the terminal/done leg; probe error
1408
+ // ⇒ re-park anyway (the safer side under uncertainty: a wrongly-suspended row is reaper-bounded
1409
+ // (reapSuspended), a wrongly-terminal row orphans a live park until TTL — the round-3 bug shape).
899
1410
  let reopenConfirmedPark = false;
900
1411
  const confirmCheckpointReopened = async () => {
901
1412
  const store = deps.checkpointStore;
902
1413
  if (!store)
903
- return true;
1414
+ return true; // durable resume required the store; absence here = cannot confirm → the safer side
904
1415
  try {
905
1416
  return (await store.findPendingTokenBySession(sessionId)) === token;
906
1417
  }
@@ -908,22 +1419,52 @@ export function createHttpServer(rawDeps) {
908
1419
  return true;
909
1420
  }
910
1421
  };
1422
+ // Protect the in-flight resume from the reaper: flip the parked row to `running` + heartbeat for the
1423
+ // duration of the resume. The run row otherwise stays `suspended`/updated_at=suspend-time the whole time
1424
+ // (resume neither rewrites the row nor heartbeats), so `reapSuspended` would mark a live resume `failed`
1425
+ // and release its session lock once it crossed the approval TTL. Cleared in the finally below.
911
1426
  let resumeHeartbeat;
912
- let resumeManifestLease = 0;
1427
+ let resumeManifestLease = 0; // [998]②: this leg's tracker lease (assigned at register, checked by the finally's clear)
1428
+ // 1.78 (review LOW, pre-existing): THIS leg's live TaskStream — held so the finally can identity-guard its
1429
+ // steerableRuns deregistration (an unconditional delete could clobber a handle a FASTER sibling leg just
1430
+ // registered under the same taskId across a suspend→fast-resume).
913
1431
  let resumeStreamRef;
914
- let resumeLegLive = false;
1432
+ let resumeLegLive = false; // true only while the resumed leg is live (set at resumeStream, cleared in the finally)
1433
+ // design/122 ③: this leg's subagent-handle unregister thunks — run at leg end (finally), not on settle.
915
1434
  const subagentHandleEvictions = [];
916
1435
  let claimedRow = false;
1436
+ // Cancel parity with runInBackground: the resumed leg is registered in `inflightRuns` (same-replica fast
1437
+ // path) and its heartbeat polls the durable cancel flag (cross-replica) — without this a resumed run was
1438
+ // un-cancellable (cancel 202'd + flagged the row, but nothing honored it). markResuming reset the flag,
1439
+ // so only a cancel issued DURING this leg aborts it.
917
1440
  const cancelCtrl = new AbortController();
1441
+ // stoppedBy — resume leg NOW marked (core 1.256 delivered the mark-by-owner seam): on this
1442
+ // leg core's canonical taskId == sessionId (the rebuilt taskConfig carries no spec.taskId), so a
1443
+ // task-scoped child's owner KEY VALUE coincides with a session-scoped child's — the old `list()` path
1444
+ // couldn't tell them apart (no sessionScoped flag exposed) and took an honest degrade. The seam filters
1445
+ // by the EXPLICIT flag (`skipSessionScoped:true` inside the helper), so only task-scoped children take
1446
+ // the "user" marker; session-scoped ones a cancelled parent deliberately does NOT stop stay unmarked
1447
+ // (CC Backgrounded semantics). Owner = sessionId (this leg's canonical key), NOT the run-row taskId.
918
1448
  markChildrenStoppedByUserOnAbort(cancelCtrl.signal, sessionId, principal);
1449
+ // design/80 seam #2: a resumed leg is itself preempt-able. Register a FRESH preempt controller (markResuming
1450
+ // also reset preempt_requested), thread a LIVE preemptSignal into the resume taskConfig below, and poll the
1451
+ // durable preempt flag in the heartbeat — so the scheduler can preempt the RESUMED work once it makes
1452
+ // progress. 🔴 Never re-supply the consumed suspend-time signal (core strips a pre-aborted one).
919
1453
  const preemptCtrl = new AbortController();
920
1454
  if (taskId && deps.runStore) {
1455
+ // The CAS fails (false) only if the row is no longer `suspended` — the reaper already expired this
1456
+ // approval (and released the session lock), the run is terminal, or a concurrent resume already claimed
1457
+ // it. Bail rather than run a resume whose run row is gone / a sibling owns; the checkpoint reaper expires
1458
+ // the token in the same tick so resumeStream would reject anyway. Closes the window the heartbeat can't.
921
1459
  claimedRow = await deps.runStore.markResuming(taskId);
922
1460
  if (!claimedRow)
923
1461
  return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
1462
+ // MF-Fleet (#7): the row is now `running` again → re-publish it LIVE to the fleet (re-appears as the same row
1463
+ // that was sitting "waiting" while parked). After onStart only — never if markResuming lost the CAS (a sibling
1464
+ // owns the row). onTerminal at every settle below keeps the row's lifecycle in lock-step with the run-store row.
924
1465
  fleetPub?.onStart();
925
1466
  const rs = deps.runStore;
926
- const owner = principal ?? null;
1467
+ const owner = principal ?? null; // the resumed run's owner (the original submitter) — single-DB null-safe guard
927
1468
  inflightRuns.set(taskId, cancelCtrl);
928
1469
  preemptableRuns.set(taskId, preemptCtrl);
929
1470
  resumeHeartbeat = setInterval(() => {
@@ -938,75 +1479,137 @@ export function createHttpServer(rawDeps) {
938
1479
  }
939
1480
  }, HEARTBEAT_MS);
940
1481
  resumeHeartbeat.unref?.();
941
- deps.modelUsage?.register(taskId);
942
- resumeManifestLease = deps.promptManifests?.register(taskId) ?? 0;
1482
+ deps.modelUsage?.register(taskId); // E8 leak fence: track the resumed leg's brain.calls (sub-tasks excluded); cleared in the finally
1483
+ resumeManifestLease = deps.promptManifests?.register(taskId) ?? 0; // [998]②: the resumed prepare re-fires prompt.assembled — this leg drains its own manifest; lease guards the finally's clear (B-3 window)
943
1484
  }
1485
+ // codex M2 hook point: the CAS (when a run row exists) is decided above — a lost race already returned 409 and
1486
+ // never reaches here. Awaited BEFORE the model drive so the resumed leg's first ask observes the side-effects.
944
1487
  if (args.onResumeCommitted)
945
1488
  await args.onResumeCommitted();
946
1489
  try {
1490
+ // Wrap in withPrincipal so the resumed leg's LLM spend is attributed to the tenant's CostQuota — parity
1491
+ // with the sync/async paths (council: otherwise maxPrincipalCostUsd is silently bypassed on resume).
947
1492
  result = await withPrincipal(principal, async () => {
1493
+ // P1 ①② follow-on: drain the async-workflow completion inbox on the RESUME leg — hoisted
1494
+ // ABOVE the verify/stream fork so BOTH resume flavors get it (parity with runs.ts, whose drain runs
1495
+ // "before ANY branch"; review round 2 caught the verify-leg gap). A workflow that finished while this run
1496
+ // sat suspended surfaces as a durable `workflow_complete` event the tailing client replays (at-least-once
1497
+ // + shell runId-dedup). Owner gate = the original submitter (the checkpoint scope) — the identity the
1498
+ // entries were stamped with at enqueue. 🔒 ONLY when a durable log exists (`deps.runStore && taskId`):
1499
+ // without one there is nowhere to persist the frame, and `drain` REMOVES entries — draining there would
1500
+ // EAT the completions. One-shot appender off maxSeq; each branch below re-reads maxSeq, so seq stays safe.
1501
+ // option A: keys this leg already delivered (turn-open drain, loop case OR observer) — hoisted ABOVE the
1502
+ // drain so the drained frames' keys register here too (core [784]④b: the same idle-arrival is also parked
1503
+ // core-side; its pend-drain replay rides the resumed stream as a task_notification event — the loop's key
1504
+ // check below folds that second durable append). codex F1: NotifiedKeys — the observer arm appends
1505
+ // fire-and-forget, so claims carry the write's outcome promise (bg-leg twin's rationale).
948
1506
  const resumeNotifiedKeys = new NotifiedKeys();
949
1507
  if (deps.runStore && taskId) {
950
1508
  const rs0 = deps.runStore;
951
1509
  let drainSeq = (await rs0.maxSeq(taskId)) + 1;
952
1510
  await emitPendingWorkflowCompletions(deps.workflowCompletionInbox, sessionId, principal ?? null, (frame) => {
953
1511
  const { type, ...rest } = frame;
1512
+ // codex F1 + 修2(三路复审 notify-1):the claim must carry the WRITE OUTCOME, not a pre-resolved
1513
+ // true — a transient append failure with a `Promise.resolve(true)` claim would fake-suppress the
1514
+ // stream/core-side replay of the SAME key while the durable row never landed (the un-acked inbox
1515
+ // entry re-delivers next leg, but this leg's replay was eaten). Mirror of the bg-leg/runs.ts twin.
1516
+ // codex F2: task_type rides the key (external 帧独立键域 — bg twin's note).
954
1517
  const f = frame;
955
1518
  const write = rs0.appendEvent(taskId, drainSeq++, type, rest);
956
1519
  if (f.type === "task_notification" && f.task_id)
957
1520
  resumeNotifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), write.then(() => true, () => false));
958
1521
  return write;
959
- }, { route: "resume-open", connection: "durable-append", log: (m, x) => deps.logger?.info?.(m, x) });
1522
+ },
1523
+ // emit-target diagnostics — this drain appends to the resumed run's durable event log
1524
+ // (a tailing GET /v1/runs/:id/events client replays it; nothing is written to a live socket here).
1525
+ { route: "resume-open", connection: "durable-append", log: (m, x) => deps.logger?.info?.(m, x) });
960
1526
  }
1527
+ // Verify task → resume AND re-gate (core resumeWithVerification): resume the impl, and if it COMPLETES
1528
+ // run the identical adversarial verifier + fix loop; if it re-suspends it comes back failed-with-token
1529
+ // (handled like any re-suspend below). 🔴 Freshness (BUG5, design/53 §2.B): we pass NO `evidence` diff,
1530
+ // so the verifier judges the POST-resume working tree directly — there is no stale pre-suspend diff to
1531
+ // grade, which closes the "approve then plant a backdoor" window by construction.
961
1532
  if (verifyRounds !== undefined) {
962
- const vr = await resumeWithVerification(deps.runner, token, outcome, { ...taskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds);
1533
+ // resumeWithVerification runs the SAME pre-CAS guard + atomic CAS as resume() internally, so a lost
1534
+ // CAS rejects with CheckpointError before any work — handled by the catch below, same as the stream path.
1535
+ const vr = await resumeWithVerification(deps.runner, token, outcome, { ...taskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds); // 快审 F1:整对象直传
963
1536
  let safe = stripCheckpointToken(vr);
1537
+ // 🔴 A re-suspend on a LATER gate is NOT terminal: core maps it to status:"failed" +
1538
+ // verification.unverifiedReason:"suspended" with the checkpoint token still live. Treat it exactly like
1539
+ // the stream path's suspended branch — write a "suspended" event and return status:"suspended" so the
1540
+ // outer block calls setSuspended (keeps the session lock + resumable row). Driving it terminal would
1541
+ // release the lock and orphan the second checkpoint (next /decide → 409, task abandoned).
964
1542
  const reSuspended = vr.verification?.unverifiedReason === "suspended";
1543
+ // [1245] codex-4: reopen-class twin of the stream path (store-confirmed, codex-5) — the checkpoint
1544
+ // is pending again, so the durable log must record a park, never `done` (the outer row drive
1545
+ // re-parks via checkpointReopened; the response keeps status:"failed"+retriable, unlike reSuspended
1546
+ // which surfaces "suspended").
965
1547
  const vrReopened = isCheckpointReopenedFailure(vr) && (await confirmCheckpointReopened());
966
1548
  if (vrReopened)
967
1549
  reopenConfirmedPark = true;
968
1550
  if (taskId && deps.runStore) {
969
1551
  const rs2 = deps.runStore;
1552
+ // codex-6/7 (vrReopened leg): EVERYTHING here is observability — best-effort once the reopen is
1553
+ // confirmed (maxSeq + manifest + usage + suspended event): a thrown append must not rethrow past
1554
+ // the authoritative row re-park below (stream twin). reSuspended-only keeps its fail-loud shape.
970
1555
  try {
971
1556
  let seq = (await rs2.maxSeq(taskId)) + 1;
972
1557
  const ap = (type, data) => rs2.appendEvent(taskId, seq++, type, data);
973
- await appendPromptManifest(ap, deps.promptManifests, taskId);
1558
+ await appendPromptManifest(ap, deps.promptManifests, taskId); // [998]②: the verified-resume leg has no stream — persist the re-fired manifest before its terminal/suspend event
974
1559
  if (reSuspended || vrReopened) {
975
- await appendModelUsageDelta(ap, deps.modelUsage, taskId);
1560
+ await appendModelUsageDelta(ap, deps.modelUsage, taskId); // E8: persist pre-re-suspend per-model usage
976
1561
  await ap("suspended", { gate: vr.checkpointGate ?? null, ...(vrReopened ? { reopened: vr.errorCode ?? null } : {}) });
977
1562
  }
978
1563
  else {
1564
+ // E8: this verified-resume leg is the run's final done → flush the last delta + SUM model_usage into stats.modelUsage
979
1565
  safe = await attachModelUsage(safe, { append: ap, getEvents: (id, a) => rs2.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
980
- await ap("done", { result: safe });
1566
+ await ap("done", { result: safe }); // verify is not a live stream → one terminal event
981
1567
  }
982
1568
  }
983
1569
  catch (e) {
984
1570
  if (!vrReopened)
985
- throw e;
1571
+ throw e; // best-effort ONLY on the confirmed-reopen leg — see the boundary note above
986
1572
  }
987
1573
  }
988
1574
  if (reSuspended)
989
1575
  return { ...safe, status: "suspended" };
1576
+ // Count the gate verdict only when the verifier actually ran (a re-suspend never reached it).
990
1577
  if (vr.verification?.verdict)
991
1578
  deps.metrics?.inc("verifications_total", { verdict: vr.verification.verdict });
992
1579
  return safe;
993
1580
  }
1581
+ // resumeStream runs the SAME pre-CAS guard + atomic CAS as resume(), then returns a live stream; a
1582
+ // lost CAS rejects (CheckpointError) BEFORE any stream → never re-runs a resolved action.
1583
+ // E23: core's resumeStream is EAGER (runtask.js:1710 — once it resolves the resumed run is already
1584
+ // executing), so the per-run elicitation context MUST exist BEFORE the stream is created, or an inbound MCP
1585
+ // elicitation early in the resumed leg finds no ALS store and fail-closed declines. Hoist the durable-append
1586
+ // machinery, then create + drain the stream INSIDE runWithContext (emit = append → events tail). The owner is
1587
+ // the ORIGINAL submitter (`principal` = the checkpoint scope), NOT the operator who /decided — they answer.
994
1588
  const rs = deps.runStore;
995
1589
  let seq = rs && taskId ? await rs.maxSeq(taskId) : 0;
996
1590
  let text = "";
997
1591
  let reasoning = "";
998
1592
  const persistThinking = deps.config.traceThinking;
999
1593
  const append = (type, data) => (rs && taskId ? rs.appendEvent(taskId, ++seq, type, data) : Promise.resolve());
1594
+ // (resumeNotifiedKeys — this leg's delivered-notification keys — is hoisted above the inbox drain, see top)
1595
+ // E18: anchor POST-resume turns too (a resumed conversation's new messages are rewindable). Same capture as
1596
+ // runInBackground via the SHARED TurnAnchorCapture (only a settled assistant-text turn is anchored — see its
1597
+ // doc). owner = the original submitter (the checkpoint scope), not the operator who /decided. undefined
1598
+ // captureTurnAnchor ⇒ no anchor store / no getLeafId.
1000
1599
  const anchorOwner = principal ?? null;
1001
1600
  const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
1002
1601
  const captureTurnAnchor = deps.resumeAnchorStore && getLeafId
1003
1602
  ? async (eventId) => { const leaf = await getLeafId(sessionId); if (leaf)
1004
1603
  await deps.resumeAnchorStore.put(sessionId, eventId, leaf, anchorOwner); }
1005
1604
  : undefined;
1605
+ // R8 (CC-parity rewind): USER-message anchor keyed by taskId (parity with the first-run path). A resume leg
1606
+ // rarely commits a new user message, but wiring it is harmless (only `message_committed{role:"user"}` fires it).
1006
1607
  const captureUserMessageAnchor = deps.resumeAnchorStore && taskId
1007
1608
  ? async (entryId) => { await deps.resumeAnchorStore.put(sessionId, taskId, entryId, anchorOwner); }
1008
1609
  : undefined;
1009
1610
  const anchor = new TurnAnchorCapture(captureTurnAnchor, () => deps.metrics?.inc("resume_anchor_capture_failed"), captureUserMessageAnchor);
1611
+ // Parity with runInBackground (src/runs.ts): the resume leg must capture thinking + per-turn usage too,
1612
+ // else a suspend→resume loses them on post-resume turns. Reasoning flushed before content, redacted.
1010
1613
  const flush = async () => {
1011
1614
  if (reasoning) {
1012
1615
  await append("reasoning", { text: redactSecrets(reasoning) });
@@ -1015,13 +1618,21 @@ export function createHttpServer(rawDeps) {
1015
1618
  if (text) {
1016
1619
  await append("text", { text, ...(anchor.firstTextEventId ? { eventId: anchor.firstTextEventId } : {}) });
1017
1620
  text = "";
1018
- }
1621
+ } // E18: per-message resume handle
1019
1622
  };
1020
1623
  const driveResume = async () => {
1624
+ // (the workflow-completion inbox drain for this leg is hoisted ABOVE the verify/stream fork — see top)
1625
+ // core 1.210: the resume leg now has the RunInternals seam — fan a subagent's task_progress
1626
+ // tick (child-isolated stream; the forward sink is its ONLY exit) out to the fleet AND the durable log
1627
+ // (shared whitelist+redact builder + E2 identity), parity with the sync/bg legs. Fire-and-forget append
1628
+ // (`++seq` is a sync increment — sink-vs-loop appends get unique seqs); only when a durable log exists.
1629
+ // false once this leg settles — notifications after that take the durable-inbox path.
1021
1630
  resumeLegLive = true;
1022
1631
  const stream = await deps.runner.resumeStream(token, outcome, { ...taskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, {
1023
1632
  onForwardEvent: (e) => {
1024
1633
  fleetPub?.onForwardEvent(e);
1634
+ // S2 live tail(复审 #1:forward sink 有三条腿——resume 腿上 spawn 的 bg 子代同样带
1635
+ // bgAgentId,漏挂=同副本开流只见 heartbeat)。publish 在 rs/taskId 门之前(tail 独立于账本)。
1025
1636
  {
1026
1637
  const bg = e.bgAgentId;
1027
1638
  if (bg !== undefined && defaultSubagentTailBus.hasSubscribers(bg)) {
@@ -1037,31 +1648,47 @@ export function createHttpServer(rawDeps) {
1037
1648
  void append("task_progress", taskProgressEventData(e)).catch(() => undefined);
1038
1649
  }
1039
1650
  else if (t === "tool_start") {
1651
+ // C1 (core 1.219): a delegated child's forwarded tool lifecycle — durable via the SAME shared
1652
+ // whitelist+redact builders as the top stream (parity with the bg leg; deltas not persisted per-chunk).
1040
1653
  void append("tool_start", toolStartEventData(e)).catch(() => undefined);
1041
1654
  }
1042
1655
  else if (t === "tool_end") {
1043
1656
  void append("tool_end", toolEndEventData(e)).catch(() => undefined);
1044
1657
  }
1045
1658
  },
1659
+ // background-completion observer — flip the child's fleet row +
1660
+ // park an idle-arrival in the durable inbox (workflow-type skipped). Rationale in runs.ts (bg twin).
1661
+ // core 1.283 `seq` (per-stop-cycle counter) rides into the dedup key + parked entry — sync twin's note.
1662
+ // 白名单排查(2026-07-24,「彻查 as {} 连环坑」):手写窄类型换 core 真实类型,理由同 runs.ts twin。
1046
1663
  onTaskNotification: (n) => {
1047
1664
  if (n.task_type === "workflow")
1048
1665
  return;
1666
+ // S2 复审 #3(sync 腿同注):中途终态信号。
1049
1667
  if (defaultSubagentTailBus.hasSubscribers(n.task_id)) {
1050
1668
  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) } : {}) });
1051
1669
  }
1670
+ // id-domain alias: flip by payload.sessionId (= the tick's uuid domain), fallback task_id.
1052
1671
  const hadRow = fleetPub?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false;
1053
1672
  const parked = !resumeLegLive && Boolean(deps.workflowCompletionInbox && sessionId);
1673
+ // diagnosability (rationale in runs.ts twin).
1054
1674
  deps.logger?.info?.("task_notification_observed", { route: "resume", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive: resumeLegLive, parkedDurable: parked });
1055
1675
  if (parked) {
1676
+ // 对抗评审 2026-07-11(MED):log the enqueue failure (false park✓ fingerprint otherwise; sync twin's note).
1056
1677
  void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(sessionId, principal ?? null, n, Date.now(), taskId)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "resume", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
1057
1678
  }
1058
1679
  else if (resumeLegLive) {
1680
+ // option A: teardown-time reap notification → durable display frame NOW (bg-leg twin's note).
1681
+ // core [784]④a: seq-folded shared key — the seq-less shape reverse-swallowed cycle 2's frame.
1682
+ // codex F1 (HIGH): fire-and-forget append — claim the key with its REAL outcome so a failed
1683
+ // write doesn't eat the stream replay of the same key (bg twin's note).
1059
1684
  const key = taskNotificationStreamKey(n);
1060
1685
  if (resumeNotifiedKeys.get(key) === undefined) {
1061
1686
  resumeNotifiedKeys.set(key, append("task_notification", taskNotificationEventData({ notification: n })).then(() => true, () => false));
1062
1687
  }
1063
1688
  }
1064
1689
  },
1690
+ // C2 (core 1.219) + design/122 (core 1.225 ③): the resumed leg re-registers sync-delegation steer
1691
+ // handles under the SAME taskId. NOT evicted on settle (resume legality) — evicted at leg end.
1065
1692
  ...(deps.subagentSteerRegistry && taskId
1066
1693
  ? {
1067
1694
  onSubagentSpawn: (handle) => {
@@ -1072,17 +1699,17 @@ export function createHttpServer(rawDeps) {
1072
1699
  });
1073
1700
  resumeStreamRef = stream;
1074
1701
  if (taskId)
1075
- steerableRuns.set(taskId, stream);
1702
+ steerableRuns.set(taskId, stream); // the resumed leg is in-flight again → live-steerable (parity with the first-run plain-stream path; taskId-guarded like the finally)
1076
1703
  if (!taskId || !rs)
1077
- return await stream.result();
1704
+ return await stream.result(); // no durable log to persist into → just drain
1078
1705
  for await (const ev of stream) {
1079
- fleetPub?.onEvent(ev);
1080
- await appendPromptManifest(append, deps.promptManifests, taskId);
1706
+ fleetPub?.onEvent(ev); // MF-Fleet (#7): turn_end → tokens, tool_start → live activity, in-stream task_progress → child row (parity with the sync leg's onEvent)
1707
+ await appendPromptManifest(append, deps.promptManifests, taskId); // [998]②: the resumed prepare's manifest lands ahead of its first event (runs.ts twin)
1081
1708
  switch (ev.type) {
1082
1709
  case "text_delta":
1083
1710
  text += ev.delta;
1084
1711
  anchor.onText(ev.eventId);
1085
- break;
1712
+ break; // E18: latch first-text eventId = message handle
1086
1713
  case "reasoning_delta":
1087
1714
  if (persistThinking)
1088
1715
  reasoning += ev.delta;
@@ -1091,7 +1718,7 @@ export function createHttpServer(rawDeps) {
1091
1718
  anchor.onTool();
1092
1719
  await flush();
1093
1720
  await append("tool_start", toolStartEventData(ev));
1094
- break;
1721
+ break; // E18: tool ran → not anchorable
1095
1722
  case "tool_end":
1096
1723
  await flush();
1097
1724
  await append("tool_end", toolEndEventData(ev));
@@ -1102,57 +1729,65 @@ export function createHttpServer(rawDeps) {
1102
1729
  await flush();
1103
1730
  await append("status", brainStatusEventData(st));
1104
1731
  break;
1105
- }
1732
+ } // S4 P0-a: durable brain-liveness observation (rationale in runs.ts twin — flush first so a mid-text reconnect doesn't invert the timeline); design/158 B2: row type aligned to the live frame name ("status", was "brain_status" — ledger-sink.ts twin's note has the full rationale + compat contract); 白名单排查 2026-07-24: 手写字面量换 Parameters<> 防未来漂移
1106
1733
  case "context_usage":
1107
1734
  await flush();
1108
1735
  await append("context_usage", contextUsageEventData(ev));
1109
- break;
1736
+ break; /* core 1.414 一等分支;两条口径见 builder 旁注 */
1110
1737
  case "compacted":
1111
1738
  await flush();
1112
1739
  await append("compacted", compactedEventData(ev));
1113
- break;
1740
+ break; // MF-18 trigger + core 1.219 attachedFiles/preserved_segment (shared whitelist builder)
1114
1741
  case "diagnostics":
1115
1742
  await flush();
1116
1743
  await append("diagnostics", diagnosticsEventData(ev));
1117
- break;
1744
+ break; // core 1.220 design/121: NEW LSP diagnostics (shared whitelist builder, §E1 redact)
1118
1745
  case "steering_injected":
1119
1746
  await flush();
1120
1747
  await append("steering_injected", steeringInjectedEventData(ev));
1121
- break;
1748
+ break; // [1550]/[1552]: shared whitelist builder, §E1 redact — resume twin of the sync + ledger-sink legs
1122
1749
  case "workspace_changed":
1123
1750
  await flush();
1124
1751
  await append("workspace_changed", workspaceChangedEventData(ev));
1125
- break;
1752
+ break; // [1559]二: cwd move (bash cd/EnterWorktree) — shared whitelist builder, §E1 redact — resume twin of the sync + ledger-sink legs
1126
1753
  case "message_committed":
1127
1754
  await anchor.onMessageCommitted(ev.role, ev.entryId);
1128
- break;
1755
+ break; // R8: USER-message rewind anchor (CC "rewind to the prompt")
1129
1756
  case "task_progress":
1130
1757
  await append("task_progress", taskProgressEventData(ev));
1131
- break;
1758
+ break; // shared whitelist+redact builder (+E2 identity, 1.78) — parity with the runs.ts + sync legs
1132
1759
  case "task_notification": {
1133
1760
  await flush();
1761
+ // core [784]④a+④b: seq-folded shared key + skip keys this leg already delivered (turn-open inbox
1762
+ // drain or observer arm) — core's pend-drain replays a double-parked idle arrival as a stream event
1763
+ // on the resumed leg; appending it again doubles the frame in the durable events tail (sync twin).
1764
+ // codex F1: await the claiming write's outcome — a failed observer claim must not eat this
1765
+ // replay (write anyway + promote the key with THIS write's outcome; bg twin's note).
1134
1766
  const tnr = ev.notification;
1135
1767
  const tnrKey = tnr?.task_id ? taskNotificationStreamKey({ task_type: tnr.task_type, task_id: tnr.task_id, status: String(tnr.status), seq: tnr.seq }) : undefined;
1136
1768
  const tnrPrior = tnrKey !== undefined ? resumeNotifiedKeys.get(tnrKey) : undefined;
1137
1769
  if (tnrPrior === undefined || !(await tnrPrior)) {
1138
- const write = append("task_notification", taskNotificationEventData(ev));
1770
+ const write = append("task_notification", taskNotificationEventData(ev)); // core 1.202 design/115 P2: display projection (shared whitelist+redact builder — parity with the runs.ts + sync legs)
1139
1771
  if (tnrKey !== undefined)
1140
- resumeNotifiedKeys.set(tnrKey, write.then(() => true, () => false));
1772
+ resumeNotifiedKeys.set(tnrKey, write.then(() => true, () => false)); // stream delivered this key
1141
1773
  await write;
1142
1774
  }
1143
1775
  break;
1144
1776
  }
1145
1777
  case "turn_end": {
1146
1778
  await flush();
1147
- await append("turn_end", turnEndEventData(ev));
1779
+ await append("turn_end", turnEndEventData(ev)); /* 白名单 builder:此前手写 {usage},剥掉了 stopReason/usageMissing */
1148
1780
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
1149
1781
  await anchor.onTurnEnd();
1150
1782
  break;
1151
- }
1783
+ } // E18: anchor a settled assistant-text turn only
1784
+ // "done" handled below via result() + the run-row drive (terminal event written once)
1152
1785
  }
1153
1786
  }
1154
1787
  await flush();
1155
1788
  const r = await stream.result();
1789
+ // Close the durable stream with the matching terminal event (mirrors runInBackground). Never the
1790
+ // capability token — only the non-secret gate on a re-suspend.
1156
1791
  if (r.status === "suspended") {
1157
1792
  await append("suspended", { gate: r.checkpointGate ?? null });
1158
1793
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
@@ -1162,19 +1797,34 @@ export function createHttpServer(rawDeps) {
1162
1797
  await append("needs_review", { gate: r.checkpointGate ?? null });
1163
1798
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
1164
1799
  return r;
1165
- }
1800
+ } // D-B: a resumed plan_review re-gated to another review pause
1801
+ // [1245] codex-4: a CONFIRMED reopen-class failure (core reopened the checkpoint — store-verified,
1802
+ // codex-5) is NOT terminal: logging `done` here would make the append-only log say terminal while
1803
+ // the park stays decidable (trace projection + events tailers treat `done` as final and would miss
1804
+ // the retry). Log the park, keep E8's terminal usage-SUM reserved for genuinely terminal results
1805
+ // (`checkpoint.reopen_failed` and an unconfirmed reopen stay on the done leg).
1166
1806
  if (isCheckpointReopenedFailure(r) && (await confirmCheckpointReopened())) {
1167
1807
  reopenConfirmedPark = true;
1808
+ // codex-6: best-effort — the ROW re-park below is the authority once the reopen is confirmed. A
1809
+ // thrown append here would otherwise rethrow past the row drive and strand a `running` row (no
1810
+ // heartbeat) over a pending park until reapStale; a missing suspended event is merely a log gap
1811
+ // (nothing terminal was written — tailers keep waiting), strictly recoverable.
1168
1812
  try {
1169
1813
  await append("suspended", { gate: null, reopened: r.errorCode ?? null });
1170
1814
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
1171
1815
  }
1172
1816
  catch {
1817
+ /* row drive is authoritative */
1173
1818
  }
1174
1819
  return r;
1175
1820
  }
1176
- const safe = await attachModelUsage(stripCheckpointToken(r), { append, getEvents: (id, a) => rs.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
1821
+ // E8: terminal this resumed leg is the run's final done, so flush the last delta + SUM model_usage across
1822
+ // ALL legs (pre-suspend + post-resume) into stats.modelUsage. setTerminal (below, ~2619) stores this result.
1823
+ const safe = await attachModelUsage(stripCheckpointToken(r), { append, getEvents: (id, a) => rs.getEvents(id, a), modelUsage: deps.modelUsage, taskId }); // structural fence (strip no-op today)
1177
1824
  await append("done", { result: safe });
1825
+ // E12 (shell-host contract): a resumed leg that COMPLETES can also emit post-completion suggestions (parity with the
1826
+ // first-run path + the completed-only gate). suspended/needs_review returned above; a failed leg is
1827
+ // skipped here. Redact + persist as a `suggestions` event (UNTRUSTED UI-only, never re-fed to a model).
1178
1828
  if (taskConfig.suggestNextPrompts && safe.status === "completed") {
1179
1829
  const suggestions = await stream.suggestions().catch(() => []);
1180
1830
  if (suggestions.length)
@@ -1189,21 +1839,32 @@ export function createHttpServer(rawDeps) {
1189
1839
  }
1190
1840
  catch (e) {
1191
1841
  if (e instanceof CheckpointError) {
1842
+ // We flipped the row to `running` (claimedRow). Two CheckpointError classes diverge here (D-1, core 1.101):
1843
+ // • TERMINAL (already_resolved / not_found / gate_mismatch / unsupported_version): the checkpoint is
1844
+ // consumed or gone — drive the row terminal now (releasing task_active) instead of leaving a zombie
1845
+ // `running` row for reapStale to mislabel and hold the session lock for ~runStaleSec. (Original case.)
1846
+ // • RETRIABLE PRE-CAS (invalid_outcome = binding mismatch / reopen_revote = env_failed must replay the
1847
+ // persisted winner / reopened_concurrently = a concurrent resolve-reopen advanced the rev): the
1848
+ // checkpoint STAYS pending and the operator re-fetches + re-/decides. Driving the row terminal would
1849
+ // release task_active and ORPHAN the still-pending checkpoint (next /decide → 404). Re-park it
1850
+ // `suspended` so the corrected retry can claim it again.
1192
1851
  const retriable = e.code === "checkpoint.invalid_outcome" ||
1193
1852
  e.code === "checkpoint.reopen_revote" ||
1194
1853
  e.code === "checkpoint.reopened_concurrently" ||
1195
- e.code === "resume.parent_constraint_missing" ||
1196
- e.code === "resume.parent_constraint_mismatch" ||
1197
- e.code.startsWith("wake.");
1854
+ e.code === "resume.parent_constraint_missing" || // 历史复审轴A #4(1.254):core 新增两码同为
1855
+ e.code === "resume.parent_constraint_mismatch" || // PRE-CAS(checkpoint 保持 pending)——按本块 doctrine 归 retriable;当前 /decide 面只 resume 顶层 checkpoint 应不可达,前瞻补齐防嵌套 resume 面开放时踩中
1856
+ e.code.startsWith("wake."); // design/144:wake 拒绝(gate_pending/nothing_to_deliver)全是 PRE-CAS,checkpoint 保持 pending——驱 terminal 会孤儿化仍在等的 park
1198
1857
  if (claimedRow && taskId && deps.runStore) {
1858
+ // Re-park on the SAME gate family the outcome targeted: a plan_review retry must re-park `needs_review`
1859
+ // (keep the row's review status + the lock), not `suspended` — else GET /v1/assistant/tasks mislabels it.
1199
1860
  if (retriable) {
1200
1861
  await (outcome.gate === "plan_review" ? deps.runStore.setNeedsReview(taskId) : deps.runStore.setSuspended(taskId)).catch(() => undefined);
1201
1862
  settleFleet(outcome.gate === "plan_review" ? "needs_review" : "suspended");
1202
- }
1863
+ } // MF-Fleet (#7): re-park keeps the row
1203
1864
  else {
1204
1865
  await deps.runStore.setTerminal(taskId, "failed", null, e.message).catch(() => undefined);
1205
1866
  settleFleet("failed");
1206
- }
1867
+ } // MF-Fleet (#7): terminal removes the row
1207
1868
  }
1208
1869
  const status = e.code === "checkpoint.not_found" ? 404 : 409;
1209
1870
  return {
@@ -1211,6 +1872,12 @@ export function createHttpServer(rawDeps) {
1211
1872
  body: {
1212
1873
  error: e.message,
1213
1874
  code: e.code,
1875
+ // invalid_outcome means DIFFERENT things per gate: for policy_ask it IS the D-1 action-binding
1876
+ // mismatch; for plan_review/dry_run_review it is a content-sanitize reject (editedPlan/reason carried a
1877
+ // forbidden tag) — give those a gate-appropriate code so the UI routes to the right recovery, not the
1878
+ // action-binding flow (adversarial-review medium finding).
1879
+ // 归一(clay 大改版窗 2026-07-28):errorCode **恒在**——改铸臂保持优先,其余=原码镜像
1880
+ // (「code 单轨」历史形从此退役;SDK 的 ?? b.code 兜底只为老 server 保留)。
1214
1881
  errorCode: e.code === "checkpoint.invalid_outcome"
1215
1882
  ? (outcome.gate === "policy_ask" ? "approval_binding_mismatch" : "resume_outcome_invalid")
1216
1883
  : e.code,
@@ -1218,55 +1885,93 @@ export function createHttpServer(rawDeps) {
1218
1885
  },
1219
1886
  };
1220
1887
  }
1888
+ // Cancelled mid-resume (the abort threw out of the resumed stream): settle the row to the cancelled
1889
+ // terminal (parity with runInBackground) instead of bubbling a 500 that leaves a zombie `running` row
1890
+ // for the reaper. The operator's /decide gets the run's final state, not an opaque error. (A preempt mid-
1891
+ // resume is NOT an abort — it durably suspends and surfaces above as status:"suspended", not here.)
1221
1892
  if (cancelCtrl.signal.aborted) {
1222
1893
  if (taskId && deps.runStore) {
1223
1894
  const c = { taskId, sessionId, status: "failed", errorCode: "cancelled", errorMessage: "cancelled by user", stats: { turns: 0, tokens: 0 } };
1224
1895
  await deps.runStore.setTerminal(taskId, "failed", c, c.errorMessage ?? null).catch(() => undefined);
1225
1896
  }
1226
- settleFleet("failed");
1897
+ settleFleet("failed"); // MF-Fleet (#7): a cancel mid-resume settles the row terminal → removed from the fleet
1227
1898
  return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
1228
1899
  }
1900
+ // C: a plain Error (not CheckpointError, not a cancel) is about to propagate before the post-catch
1901
+ // `settleFleet(result.status)` can run → settle the re-published row to "failed" first so it doesn't leak
1902
+ // as a stale "running" (no-op if a CheckpointError/cancel branch above already settled it).
1229
1903
  settleFleet("failed");
1230
1904
  throw e;
1231
1905
  }
1232
1906
  finally {
1233
1907
  if (taskId) {
1908
+ // [1.207 codex H3] identity-guarded like steerableRuns below — a retriable-CheckpointError re-park
1909
+ // (setSuspended) opens a window where a fast sibling resume markResuming's + registers its OWN
1910
+ // controllers under the same taskId before this finally runs; an unconditional delete killed the
1911
+ // fresh handles (cancel/preempt would 409 "another replica" until re-registration).
1234
1912
  if (inflightRuns.get(taskId) === cancelCtrl)
1235
1913
  inflightRuns.delete(taskId);
1236
1914
  if (preemptableRuns.get(taskId) === preemptCtrl)
1237
1915
  preemptableRuns.delete(taskId);
1916
+ // Identity-guarded (1.78 review LOW): only delete the handle THIS leg registered (and only if it is
1917
+ // still ours) — a faster sibling resume may have re-registered the same taskId already; never registered
1918
+ // ⇒ nothing of ours to delete.
1238
1919
  if (resumeStreamRef !== undefined && steerableRuns.get(taskId) === resumeStreamRef)
1239
1920
  steerableRuns.delete(taskId);
1240
1921
  for (const evict of subagentHandleEvictions)
1241
- evict();
1242
- deps.modelUsage?.clear(taskId);
1243
- deps.promptManifests?.clear(taskId, resumeManifestLease);
1922
+ evict(); // design/122 ③: leg-end handle eviction (identity-guarded)
1923
+ deps.modelUsage?.clear(taskId); // E8: parity with runInBackground's finally — unregister + drop any un-drained accumulation (no leak / no cross-resume contamination)
1924
+ deps.promptManifests?.clear(taskId, resumeManifestLease); // [998]②: lease-guarded (a fast sibling's fresh registration survives our teardown)
1244
1925
  }
1245
1926
  if (resumeHeartbeat)
1246
1927
  clearInterval(resumeHeartbeat);
1247
- resumeLegLive = false;
1928
+ resumeLegLive = false; // notifications from here on take the durable-inbox path
1248
1929
  }
1930
+ // A session-CAS conflict can surface as a failed result on resume too — evict the stale warm-cache entry so
1931
+ // the next task on this session re-wakes from TiDB instead of re-conflicting (parity with runInBackground /
1932
+ // sync POST / cascade; the only terminal-result path that was missing this — council).
1249
1933
  evictIfConflict(deps.runner, sessionId, result);
1250
- const checkpointReopened = reopenConfirmedPark;
1934
+ // [1245] codex-3: the two REOPEN-class resume failures (`resume.env_failed` / `resume.tool_unavailable` —
1935
+ // core dist-read: runtask reopens the checkpoint via store.reopen BEFORE returning this failed result; a
1936
+ // reopen that itself fails surfaces as `checkpoint.reopen_failed`, NOT these codes) leave the checkpoint
1937
+ // PENDING. Driving the row terminal here would release task_active and orphan the still-decidable park
1938
+ // (the exact class the CheckpointError `retriable` branch above re-parks) — e.g. Workflow entitlement
1939
+ // revoked between suspend and approve. Re-park `suspended` so the operator's retry can claim it again.
1940
+ const checkpointReopened = reopenConfirmedPark; // codex-5: store-confirmed inside the resume paths — never inferred from the code alone
1941
+ // Drive the parked run row to its new state (terminal, or re-suspended on a further gate). Strip the token
1942
+ // from the persisted result — structural fence (no-op today; GET /v1/runs/:id reads this row back).
1251
1943
  if (taskId && deps.runStore) {
1252
1944
  if (result.status === "suspended" || checkpointReopened)
1253
1945
  await deps.runStore.setSuspended(taskId);
1254
1946
  else if (result.status === "needs_review")
1255
- await deps.runStore.setNeedsReview(taskId);
1947
+ await deps.runStore.setNeedsReview(taskId); // D-B: a resumed leg re-gated to a review pause — re-park, keep the lock
1256
1948
  else
1257
1949
  await deps.runStore.setTerminal(taskId, result.status, stripCheckpointToken(result), result.errorMessage ?? null);
1258
1950
  }
1259
- settleFleet(checkpointReopened ? "suspended" : result.status, fleetRunResiduals(result));
1951
+ // MF-Fleet (#7): settle the fleet row in lock-step with the run row — suspended/needs_review KEEP the row (it stays
1952
+ // "waiting"/"awaiting approval"); a terminal status REMOVES it (onTerminal does this mapping). Mirrors the sync leg.
1953
+ settleFleet(checkpointReopened ? "suspended" : result.status, fleetRunResiduals(result)); // [1839]①/(a):终态残局键(parked 时 publisher 不上帧)
1954
+ // Surface the failure code/message to the deciding operator (never the token): a resumed leg that fails
1955
+ // otherwise returns an opaque {status:"failed"} — undiagnosable without a run row (leader sub-workers).
1260
1956
  return {
1261
1957
  status: 200,
1262
1958
  body: {
1263
1959
  taskId, sessionId, status: result.status,
1264
1960
  ...(result.errorCode ? { errorCode: result.errorCode } : {}),
1265
1961
  ...(result.errorMessage ? { errorMessage: redactSecrets(result.errorMessage) } : {}),
1266
- ...(checkpointReopened ? { retriable: true } : {}),
1962
+ ...(checkpointReopened ? { retriable: true } : {}), // the park is still pending — re-fetch + re-decide
1267
1963
  },
1268
1964
  };
1269
1965
  }
1966
+ /**
1967
+ * design/80 seam #2 (scheduler resume): resume a task that durably SUSPENDED on a `resource_limit` gate — a
1968
+ * scheduler PREEMPT (reason "preempt") or a budget/turns/walltime slice boundary (design/74). Distinct from the
1969
+ * operator `/decide` path (`resumeCheckpoint`, `policy_ask`): there is NO human decision, NO binding, NO answer
1970
+ * — just `{gate:"resource_limit", decision:"continue"}` (core computes the next slice's allowance from the
1971
+ * ledger). 🔴 GATE-KIND GUARD: it REFUSES to resume a `policy_ask`/human gate via this path (that would bypass
1972
+ * the human approval) — a non-resource gate returns 409 and must go through `/decide`. Owner-scoped by the
1973
+ * caller; `scope` is the checkpoint's own (multi-tenant). Returns the HTTP {status, body}.
1974
+ */
1270
1975
  async function resumePreempted(sessionId, req) {
1271
1976
  const cs = deps.checkpointStore;
1272
1977
  const token = await cs.findPendingTokenBySession(sessionId);
@@ -1274,13 +1979,18 @@ export function createHttpServer(rawDeps) {
1274
1979
  return { status: 404, body: { error: "no resumable suspension for this task (already running, resumed, or expired)", errorCode: "not_found.suspension" } };
1275
1980
  let cp;
1276
1981
  try {
1277
- cp = await cs.get(token);
1982
+ cp = await cs.get(token); // throws if the checkpoint format is newer than this build (version guard)
1278
1983
  }
1279
1984
  catch (e) {
1280
1985
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
1281
1986
  }
1282
1987
  if (!cp)
1283
1988
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1989
+ // 🔴 GATE-KIND GUARD (security): this "continue" path must ONLY advance a resource_limit suspend. A
1990
+ // policy_ask / human / irreversible_ask gate requires a HUMAN decision via POST /v1/approvals/:id/decide —
1991
+ // resuming it here with decision:"continue" would BYPASS the approval. Reject it distinctly (core's resume
1992
+ // gate-match is the fail-closed backstop, but we reject pre-CAS for a clear error + to never consume the
1993
+ // checkpoint). dry_run_review / plan_review likewise are not "continue"-resumable here.
1284
1994
  const gateKind = cp.gate?.kind;
1285
1995
  if (gateKind !== "resource_limit") {
1286
1996
  return { status: 409, body: { error: `task is suspended on a '${gateKind ?? "unknown"}' gate, not a resumable resource/preempt suspension — an approval gate must be decided via POST /v1/approvals/:id/decide`, errorCode: "gate_not_resumable" } };
@@ -1288,27 +1998,38 @@ export function createHttpServer(rawDeps) {
1288
1998
  const ctx = await cs.getCtx(sessionId).catch(() => null);
1289
1999
  if (!ctx)
1290
2000
  return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
2001
+ // Rebuild taskConfig via resolveSpec with auth reconstructed from the checkpoint scope (mirrors resumeCheckpoint).
1291
2002
  const auth = { sessionId: cp.sessionId, principal: cp.scope === "_" ? undefined : cp.scope, memoryScope: ctx.memoryScope };
2003
+ // E18: STRIP resumeAt before re-resolving on a durable resume — the leaf is already set by the checkpoint, and
2004
+ // resumeAt + a durable resume is rejected by core (resume_at.conflicts_resume); also avoids re-resolving a stale
2005
+ // anchor (which could 404 if the session was purged). A resume-at run that suspended stored its body with resumeAt.
1292
2006
  const spec = await deps.resolveSpec({ ...ctx.body, resumeAt: undefined }, req, auth);
1293
2007
  const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
2008
+ // A resource-suspended task that was submitted with verify:true is re-gated on completion (parity with the
2009
+ // HITL path — the flag rides the persisted ctx.body).
1294
2010
  const verifyRounds = ctx.body.verify === true
1295
2011
  ? {
1296
2012
  maxRounds: clampVerifyRounds(ctx.body.verifyRounds),
2013
+ // 快审 F1:resume 腿同样带 cost 顶(persisted body 的 maxCostUsd 同源;cascade 腿同换算)
1297
2014
  ...(typeof ctx.body.maxCostUsd === "number" && ctx.body.maxCostUsd > 0 ? { costCeilingMicroUsd: Math.round(ctx.body.maxCostUsd * 1e6) } : {}),
1298
2015
  }
1299
2016
  : undefined;
1300
2017
  return driveResumeIntoRunLog({ token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds });
1301
2018
  }
2019
+ /** design/144 §3:wake 一个 task_done 纯 park——非门决策,解除 park 消费消息续跑。owner 门:非 operator
2020
+ * 只能 wake 自己 scope 的 park(404 无 oracle);core 端 gate_pending/nothing_to_deliver 双拒兜底。 */
1302
2021
  async function resumeWake(sessionId, message, caller, req) {
1303
2022
  const cs = deps.checkpointStore;
1304
2023
  const token = await cs.findPendingTokenBySession(sessionId);
1305
2024
  if (!token)
1306
2025
  return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
2026
+ // owner 门先于 get(复审 finding:get 对损坏/新版行会抛,若 gate 在后,非 owner 能用 409 错误详情
2027
+ // 区分「存在但坏」vs「不存在」= 存在性 oracle)。scope 走 peekPendingScope(轻读,不碰行体)。
1307
2028
  const operator = explicitOperatorOk(caller, deps.config.operatorPrincipals);
1308
2029
  if (!operator) {
1309
2030
  const scope = await cs.peekPendingScope(sessionId).catch(() => undefined);
1310
2031
  if (scope !== null && scope !== undefined && scope !== "_" && scope !== caller) {
1311
- return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
2032
+ return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } }; // 无 oracle
1312
2033
  }
1313
2034
  }
1314
2035
  let cp;
@@ -1321,13 +2042,14 @@ export function createHttpServer(rawDeps) {
1321
2042
  if (!cp)
1322
2043
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1323
2044
  if (!operator && cp.scope !== "_" && cp.scope !== caller) {
1324
- return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
2045
+ return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } }; // 纵深(peek 缺席面兜底)
1325
2046
  }
2047
+ // 预检(core 兜底同码):非纯 park = 有未决门,wake 永不绕审批。
1326
2048
  const gateKind = cp.gate?.kind;
1327
2049
  if (gateKind !== "task_done") {
1328
2050
  return { status: 409, body: { error: `cannot wake: this checkpoint awaits a pending gate decision (gate "${gateKind ?? "unknown"}") — resolve it through its own decide entry`, errorCode: "wake.gate_pending" } };
1329
2051
  }
1330
- const trusted = operator;
2052
+ const trusted = operator; // steer 同款:operator 消息可携 <system-reminder> 权威
1331
2053
  if (message !== undefined) {
1332
2054
  try {
1333
2055
  validatePendingSteer({ text: message, trusted });
@@ -1353,9 +2075,18 @@ export function createHttpServer(rawDeps) {
1353
2075
  taskConfig,
1354
2076
  resumeObjective,
1355
2077
  outcome: { gate: "wake", ...(message !== undefined ? { message: { text: message, trusted } } : {}) },
1356
- verifyRounds: undefined,
2078
+ verifyRounds: undefined, // wake 续跑不重开 verify 门(原任务已终局;新工作要 verify 走新 run)
1357
2079
  });
1358
2080
  }
2081
+ /**
2082
+ * design/80 D-B: resolve a PRE-ACTION `plan_review` gate (3-state approve | edit | reject) and drive the resumed
2083
+ * leg into the durable run log via the SHARED machinery. DISTINCT from `resumeCheckpoint` (a `policy_ask` tool
2084
+ * approval, allow/deny + binding) and `resumePreempted` (`resource_limit` continue). The `edit` decision carries
2085
+ * the operator's REVISED plan (core threads `editedPlan` into the resumed run). 🔴 GATE-KIND GUARD (security):
2086
+ * refuses any non-`plan_review` gate — an approve/edit/reject must NEVER resolve a tool-approval / resource_limit
2087
+ * suspend (that would bypass the correct /decide allow-deny + action binding). Rejects PRE-CAS so a wrong-gate
2088
+ * call never consumes the checkpoint (core's resume gate-match is the fail-closed backstop).
2089
+ */
1359
2090
  async function resumePlanReview(sessionId, decision, editedPlan, reason, req) {
1360
2091
  const cs = deps.checkpointStore;
1361
2092
  const token = await cs.findPendingTokenBySession(sessionId);
@@ -1363,13 +2094,16 @@ export function createHttpServer(rawDeps) {
1363
2094
  return { status: 404, body: { error: "no pending plan_review for this session (already decided or expired)", errorCode: "not_found.plan_review" } };
1364
2095
  let cp;
1365
2096
  try {
1366
- cp = await cs.get(token);
2097
+ cp = await cs.get(token); // throws if the checkpoint format is newer than this build (version guard)
1367
2098
  }
1368
2099
  catch (e) {
1369
2100
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
1370
2101
  }
1371
2102
  if (!cp)
1372
2103
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
2104
+ // 🔴 GATE-KIND GUARD: a plan_review decision must ONLY resolve a plan_review gate. A human/irreversible_ask
2105
+ // (tool approval) or resource_limit/needs_review gate routed here would let approve/edit/reject bypass the
2106
+ // correct wire (a tool approval needs /decide's allow/deny + action binding; resource_limit needs /resume).
1373
2107
  const gateKind = cp.gate?.kind;
1374
2108
  if (gateKind !== "plan_review") {
1375
2109
  return {
@@ -1383,12 +2117,19 @@ export function createHttpServer(rawDeps) {
1383
2117
  const ctx = await cs.getCtx(sessionId).catch(() => null);
1384
2118
  if (!ctx)
1385
2119
  return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
2120
+ // Rebuild taskConfig via resolveSpec with auth reconstructed from the checkpoint scope (mirrors resumeCheckpoint).
1386
2121
  const auth = { sessionId: cp.sessionId, principal: cp.scope === "_" ? undefined : cp.scope, memoryScope: ctx.memoryScope };
2122
+ // E18: STRIP resumeAt before re-resolving on a durable resume — the leaf is already set by the checkpoint, and
2123
+ // resumeAt + a durable resume is rejected by core (resume_at.conflicts_resume); also avoids re-resolving a stale
2124
+ // anchor (which could 404 if the session was purged). A resume-at run that suspended stored its body with resumeAt.
1387
2125
  const spec = await deps.resolveSpec({ ...ctx.body, resumeAt: undefined }, req, auth);
1388
2126
  const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
2127
+ // A plan_review-suspended task submitted with verify:true is re-gated on completion (parity with the other
2128
+ // resume paths — the flag rides the persisted ctx.body).
1389
2129
  const verifyRounds = ctx.body.verify === true
1390
2130
  ? {
1391
2131
  maxRounds: clampVerifyRounds(ctx.body.verifyRounds),
2132
+ // 快审 F1:resume 腿同样带 cost 顶(persisted body 的 maxCostUsd 同源;cascade 腿同换算)
1392
2133
  ...(typeof ctx.body.maxCostUsd === "number" && ctx.body.maxCostUsd > 0 ? { costCeilingMicroUsd: Math.round(ctx.body.maxCostUsd * 1e6) } : {}),
1393
2134
  }
1394
2135
  : undefined;
@@ -1406,26 +2147,39 @@ export function createHttpServer(rawDeps) {
1406
2147
  deps.metrics?.inc("task_tokens_total", {}, result.stats.tokens);
1407
2148
  if (result.stats.cacheHitRate !== undefined)
1408
2149
  deps.metrics?.observe("task_cache_hit_rate", result.stats.cacheHitRate);
2150
+ // ⑤ budget/limit cutoffs (1.37): count failures by their dotted code (budget.precall/exceeded,
2151
+ // limit.timeout/max_turns) so an operator can alert on tenants hitting the cost/token ceiling.
1409
2152
  const code = result.errorCode;
1410
2153
  if (code && (code.startsWith("budget.") || code.startsWith("limit."))) {
1411
2154
  deps.metrics?.inc("budget_exceeded_total", { code });
1412
2155
  }
2156
+ // Developer-mode verification gate (1.44): count the final verdict when this task was verified.
1413
2157
  const verdict = result.verification?.verdict;
1414
2158
  if (verdict)
1415
2159
  deps.metrics?.inc("verifications_total", { verdict });
2160
+ // Cascade (1.45): count the outcome (passed / exhausted) when this task ran the ladder.
1416
2161
  const cascadeOutcome = result.cascadeOutcome;
1417
2162
  if (cascadeOutcome)
1418
2163
  deps.metrics?.inc("cascade_total", { outcome: cascadeOutcome });
2164
+ // Degenerate-repetition salvage classifier (1.59): a/b/unknown — see degenerate-instrument.ts.
1419
2165
  deps.instrumentDegenerate?.(result);
1420
2166
  }
2167
+ /** The terminal-result side-effect trio (metrics, plan-cache recurrence probe, warm-cache eviction) in ONE
2168
+ * place so a terminal path can't silently omit one (council DESIGN-6 — the omissions caused tracking gaps). */
1421
2169
  function finalizeTaskResult(result, principal, objective, sessionId) {
1422
2170
  recordTaskResult(result);
1423
2171
  deps.planCacheProbe?.record(principal, objective, result.status === "completed");
1424
2172
  evictIfConflict(deps.runner, sessionId, result);
1425
2173
  }
2174
+ /** Per-principal cumulative cost quota: 429 + retry-after when the principal is over for the window.
2175
+ * No principal → not gated (the rate limiter + REQUIRE_PRINCIPAL still apply). */
1426
2176
  function quotaExceeded(req, res) {
1427
2177
  if (!deps.costQuota)
1428
2178
  return false;
2179
+ // 🔴 F-fix: the VERIFIED principal (gatedPrincipal), never the spoofable header. Spend ACCRUES to the verified
2180
+ // principal (budget.ts `costQuota.add(currentPrincipal())` = auth.principal), so checking a header-keyed bucket
2181
+ // while the verified bucket fills would let a direct-door caller dodge its quota by rotating x-agent-principal.
2182
+ // The sibling rateLimited keys on the socket on a direct door for the same reason — this was its overlooked twin.
1429
2183
  const principal = gatedPrincipal(req, deps.config);
1430
2184
  if (!principal)
1431
2185
  return false;
@@ -1437,6 +2191,12 @@ export function createHttpServer(rawDeps) {
1437
2191
  sendError(res, 429, "limit.cost_quota_exceeded", "cost quota exceeded", { usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec });
1438
2192
  return true;
1439
2193
  }
2194
+ /** lease admission(D4 AP)— the quotaExceeded sibling for lease-enforced principals: first
2195
+ * touch negotiates with center (single-flight; 409 not-lease-enforced → negative-cached pass-through to
2196
+ * the local CostQuota path), a live lease admits locally, center-judged exhaustion denies 429+Retry-After
2197
+ * (same shape as the cost-quota deny), and an unreachable center FAILS OPEN (AP posture — reconciliation
2198
+ * is the backstop). Keys on the VERIFIED principal only (gatedPrincipal — the quotaExceeded F-fix twin);
2199
+ * no principal (single-user turnkey) → no lease surface. */
1440
2200
  async function leaseDenied(req, res) {
1441
2201
  if (!deps.fleetLease)
1442
2202
  return false;
@@ -1448,12 +2208,18 @@ export function createHttpServer(rawDeps) {
1448
2208
  return false;
1449
2209
  deps.metrics?.inc("fleet_lease_rejected_total");
1450
2210
  res.setHeader("retry-after", String(adm.retryAfterSec));
2211
+ // E4 定稿:429 带 typed code + center 结构化字段 verbatim 透传(windowType/remaining/resetAt/
2212
+ // isEstimate/pool)——人话文案归壳/web(契约如此),error 串保留旧消费者兼容。
1451
2213
  sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", { code: "quota_exhausted", retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
1452
2214
  return true;
1453
2215
  }
2216
+ /** Apply the per-principal rate limit; responds 429 and returns true when the caller is over. */
1454
2217
  function rateLimited(req, res) {
1455
2218
  if (!deps.rateLimiter)
1456
2219
  return false;
2220
+ // 🔴 D-G (DeepSeek council Bug#2): on a direct-mode worker x-agent-principal is UNTRUSTED, so keying the limiter
2221
+ // on it lets a client rotate the header to evade its own bucket OR set a victim's principal to poison theirs
2222
+ // (DoS). Key on the socket address instead (the verified principal isn't known until post-body JWT verify).
1457
2223
  const key = (deps.config.directDoorActive ? undefined : principalFrom(req, deps.config)) ?? req.socket.remoteAddress ?? "anon";
1458
2224
  const d = deps.rateLimiter.check(key);
1459
2225
  if (d.allowed)
@@ -1463,6 +2229,14 @@ export function createHttpServer(rawDeps) {
1463
2229
  sendError(res, 429, "limit.rate_exceeded", "rate limit exceeded", { retryAfterSec: d.retryAfterSec });
1464
2230
  return true;
1465
2231
  }
2232
+ /**
2233
+ * §0.5 session-scoped owner scope — the SAME model as GET /v1/sessions, reused by the fork/delete routes so
2234
+ * all three agree on "who may touch this session". A system/traceToken caller is FLEET-WIDE (ops, any session);
2235
+ * a principal caller is PINNED to `gateOwner` (its own). Identity is `gatedPrincipal` (direct-door safe), never
2236
+ * the spoofable header. Caller checks: `!fleetWide && gateOwner===null` ⇒ 401; otherwise compare the session's
2237
+ * stored owner to `gateOwner` (principal path) or allow (fleetWide).
2238
+ */
2239
+ /** Decode a URL path segment, returning null on a malformed %-sequence (a client error → 400, never a 500). */
1466
2240
  function safeDecode(seg) {
1467
2241
  try {
1468
2242
  return decodeURIComponent(seg);
@@ -1471,6 +2245,11 @@ export function createHttpServer(rawDeps) {
1471
2245
  return null;
1472
2246
  }
1473
2247
  }
2248
+ // 🔒 Single source for the fleet-wide (all-tenant) gate. dev-open (no service/trace token ⇒ fleet-wide) is honored
2249
+ // ONLY on a single-user deployment. On MULTI-TENANT (requirePrincipal=true), the ABSENCE of a token must NOT open the
2250
+ // fleet — that would leak every tenant's sessions/runs/fleet rows to ANY caller (HIGH tenant-isolation gap, systematic
2251
+ // audit 2026-06-30). Multi-tenant without a token ⇒ NOT fleet-wide ⇒ owner-gated by principal (and main.ts boot-guards
2252
+ // it to fail FAST). A system authToken or the read-only traceToken still grant fleet-wide on any deployment.
1474
2253
  function isFleetWide(req) {
1475
2254
  const { authToken: at, traceToken: tt } = deps.config;
1476
2255
  const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
@@ -1481,6 +2260,11 @@ export function createHttpServer(rawDeps) {
1481
2260
  const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
1482
2261
  return { fleetWide, gateOwner };
1483
2262
  }
2263
+ /** 🔴 WRITE-grade fleet-wide (2026-07-13): the READ-ONLY traceToken must never authorize a
2264
+ * cross-tenant WRITE (delete/fork/sync import) — isFleetWide deliberately grants it fleet-wide for the trace/
2265
+ * list READ surfaces, but consuming that same bit on destructive routes turned a read credential into write
2266
+ * authority. Write routes use THIS gate: dev-open (single-user, no tokens) and the SYSTEM authToken keep their
2267
+ * authority; a traceToken-only caller falls through to the principal owner gate (or 401). */
1484
2268
  function sessionOwnerScopeForWrite(req) {
1485
2269
  const { authToken: at, traceToken: tt } = deps.config;
1486
2270
  const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
@@ -1488,18 +2272,36 @@ export function createHttpServer(rawDeps) {
1488
2272
  const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
1489
2273
  return { fleetWide, gateOwner };
1490
2274
  }
2275
+ /** Reject when the caller's principal does not own the run (null owner = open, dev). */
1491
2276
  function runOwnerOk(req, res, owner) {
2277
+ // 🔴 F-fix: the VERIFIED principal (gatedPrincipal), never the spoofable header — `owner` was persisted from the
2278
+ // verified principal at create, so comparing it to a spoofable header would let a direct-door caller read/cancel a
2279
+ // victim's run (and its event stream) by sending x-agent-principal:<victim> with NO JWT. Same model as
2280
+ // sessionOwnerScope. (BFF/gated: gatedPrincipal === principalFrom, byte-identical.)
1492
2281
  const principal = gatedPrincipal(req, deps.config);
1493
2282
  if (deps.config.requirePrincipal && !principal) {
1494
2283
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
1495
2284
  return false;
1496
2285
  }
1497
2286
  if (owner !== null && principal !== owner) {
1498
- sendError(res, 404, "not_found.run", "run not found");
2287
+ sendError(res, 404, "not_found.run", "run not found"); // don't reveal existence to non-owners
1499
2288
  return false;
1500
2289
  }
1501
2290
  return true;
1502
2291
  }
2292
+ /** [1499] G — session-scoping ACCEPT phase on the run faces ([1498]⑥ soft rollout, 第一拍). principal answers
2293
+ * "whose data" (authz axis); session answers "which conversation" (content axis, [1493]). These faces deliver
2294
+ * conversation content (poll/events) or act on a conversation (cancel/steer/compact/detach, subagent
2295
+ * steer/resume) but were principal-gated only. Enforcement (absent ⇒ 404, subagentOutput-style) needs every
2296
+ * caller migrated first, so THIS phase 404s ONLY a PRESENT-but-MISMATCHED `?session=`: an honest caller
2297
+ * asserting its own session never mismatches on its own run — a mismatch IS the accidental cross-session
2298
+ * touch. Absent ⇒ allowed + warn-once per face (migration observability for the enforcement train). NB the
2299
+ * operator branch below is only REACHABLE where the preceding owner gate admits the caller: on faces whose
2300
+ * owner gate carries its own trusted arm (steer/compact/detach/subagent verbs) it is fully live; on the
2301
+ * runOwnerOk faces (poll/events/cancel — no operator arm, [1498]⑥ ledger) a cross-owner operator 404s at
2302
+ * the owner gate first, so here it covers only owner-null runs or an operator who IS the owner. Widening
2303
+ * those owner gates is a deliberate [1498]⑥ decision, not this phase's. A session-unbound run has nothing
2304
+ * to assert against. */
1503
2305
  const sessionAssertAbsentWarned = new Set();
1504
2306
  function runSessionAcceptOk(req, res, run, face, notFoundError = "run not found") {
1505
2307
  if (!run.sessionId)
@@ -1516,7 +2318,7 @@ export function createHttpServer(rawDeps) {
1516
2318
  return true;
1517
2319
  if (explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals))
1518
2320
  return true;
1519
- sendError(res, 404, "not_found.run", notFoundError);
2321
+ sendError(res, 404, "not_found.run", notFoundError); // byte-identical to the face's own owner-gate wording (no distinguishable arm)
1520
2322
  return false;
1521
2323
  }
1522
2324
  async function readJson(req) {
@@ -1534,9 +2336,13 @@ export function createHttpServer(rawDeps) {
1534
2336
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
1535
2337
  }
1536
2338
  catch {
2339
+ // F1 (center K3S report): a malformed body is a client error → 400, not an uncaught 500.
1537
2340
  throw new HttpError(400, "invalid JSON body");
1538
2341
  }
1539
2342
  }
2343
+ /** 2c session-sync: read a RAW request body (binary blob bytes) with a running per-request size cap — the bytes are
2344
+ * NEVER buffered unboundedly (the §5 "reject, not OOM" guard). Throws HttpError(413) the instant `max` is exceeded
2345
+ * (mid-stream, before the whole body is in memory). Returns the concatenated Buffer (≤ max). */
1540
2346
  async function readRawBody(req, max) {
1541
2347
  const chunks = [];
1542
2348
  let size = 0;
@@ -1548,11 +2354,25 @@ export function createHttpServer(rawDeps) {
1548
2354
  }
1549
2355
  return Buffer.concat(chunks);
1550
2356
  }
2357
+ /**
2358
+ * design/80 D-D (SLA-timer deny-sweep): resolve-DENY every pending human/irreversible_ask approval past its
2359
+ * deadline (the per-approval SLA) — re-resume each with decision=deny so the model CONTINUES with the denial
2360
+ * (graceful; vs the abort the reaper's expire() gives resource_limit/needs_review). Reuses resumeCheckpoint,
2361
+ * so the markResuming CAS makes it idempotent across replicas (one replica wins each resume) and the parked
2362
+ * run row is driven correctly. A per-call failure (e.g. a redeployed scenario) is swallowed → the row stays
2363
+ * pending and is retried next tick, with the terminal_at backstop as the eventual abort if the deny never
2364
+ * succeeds. Bounded per tick by the store query's LIMIT. Wired into main.ts's reaper.
2365
+ */
1551
2366
  async function denyExpiredApprovals(now) {
1552
2367
  const cs = deps.checkpointStore;
1553
2368
  if (!cs)
1554
2369
  return;
1555
2370
  const expired = await cs.listExpiredApprovalGates(now).catch(() => []);
2371
+ // [1591] 候裁③ 静默臂:parked 后台子代的过期 cp 不走 sweep 的重活赎回(deadline→terminal_at 窗内
2372
+ // 每行每 tick 一条 warn 的噪声源)——其收割属 reaper 的 expire+reconcileParkedAgents 车道。判别在
2373
+ // resume 尝试之前做(省掉注定 409 的整条 ctx/resolveSpec 重建尝试;代价=每过期行两次店读,过期集
2374
+ // 本就被店查询 LIMIT 界住);命中聚合为单条 info 留痕。判别自身故障 ⇒ 按非 parked 处理(保留 warn,
2375
+ // fail-loud 方向)。
1556
2376
  const isParkedOwned = async (sessionId) => {
1557
2377
  if (deps.backgroundAgentStore === undefined)
1558
2378
  return false;
@@ -1571,6 +2391,9 @@ export function createHttpServer(rawDeps) {
1571
2391
  };
1572
2392
  let parkedSkipped = 0;
1573
2393
  for (const { sessionId } of expired) {
2394
+ // 对抗评审 2026-07-11: log the swallow — a per-tick failure retried forever (e.g. a policy change now folded
2395
+ // into a 409 by resumeCheckpoint, or a redeployed scenario) was fully silent; the row sat pending to its
2396
+ // terminal_at with zero operator-visible signal. Behavior unchanged (skip + retry next tick), now diagnosable.
1574
2397
  if (await isParkedOwned(sessionId)) {
1575
2398
  parkedSkipped += 1;
1576
2399
  continue;
@@ -1581,19 +2404,36 @@ export function createHttpServer(rawDeps) {
1581
2404
  if (parkedSkipped > 0)
1582
2405
  deps.logger?.info?.("deny_sweep_parked_skipped", { count: parkedSkipped });
1583
2406
  }
2407
+ // Augment the http.Server with the D-D deny-sweep handle (back-compat: all 12 callers keep using the return
2408
+ // value AS an http.Server — listen/close/etc. — while main.ts's reaper reads server.denyExpiredApprovals).
1584
2409
  return Object.assign(server, { denyExpiredApprovals });
1585
2410
  }
2411
+ /** SSE: replay durable events after Last-Event-ID, then tail until the run is terminal (or stale). */
2412
+ /**
2413
+ * The differences a concrete log (task_run | image_bake) feeds the ONE resumable SSE reader (P2.8). Everything
2414
+ * the reader does — Last-Event-ID/`?from=` resume, the 416 retention boundary, the per-poll concurrent
2415
+ * status+events read, the terminal re-fetch (the terminal event lands in the gap before the status flips), the
2416
+ * stale fallback, the 15-min cap, the 15s idle heartbeat — is provider-agnostic and lives in `streamSseLog`.
2417
+ * 🔴 The task_run provider MUST keep the existing wire bytes EXACTLY (center's relay + 730+ tests depend on it).
2418
+ */
2419
+ /** POST endpoints that trigger BILLABLE work — the fail-closed auth guard must cover ALL of them (council: the
2420
+ * guard's inline list had drifted from the handlers and missed `/v1/approvals/:id/decide`, which resumes a run
2421
+ * via resumeCheckpoint/store.decide → paid tokens). Keep this in sync when adding a billable POST route. */
1586
2422
  function isBillableSubmitPath(url) {
1587
- return (url === "/v1/side-query" ||
2423
+ return (url === "/v1/side-query" || // [1469] one-shot brain call — runs the model, so it rides every billable-submit gate
1588
2424
  url === "/v1/tasks" ||
1589
2425
  url === "/v1/tasks/stream" ||
1590
2426
  url === "/v1/runs" ||
1591
2427
  url === "/v1/leader" ||
1592
- ASSISTANT_RESUME_RE.test(url) ||
1593
- ASSISTANT_PLAN_REVIEW_RE.test(url) ||
1594
- RUN_SUBAGENT_RESUME_RE.test(url) ||
2428
+ ASSISTANT_RESUME_RE.test(url) || // resource_limit/preempt resume runs the model → BILLABLE (preempt is not)
2429
+ ASSISTANT_PLAN_REVIEW_RE.test(url) || // plan_review approve/edit resumes + runs the model → BILLABLE
2430
+ RUN_SUBAGENT_RESUME_RE.test(url) || // 对抗复查 B-2(HIGH):subagent revive (handle.resume) STARTS model work → BILLABLE
1595
2431
  url.startsWith("/v1/approvals"));
1596
2432
  }
2433
+ // ⚠️ SSE 帧纪律(2026-07-12):center BFF 中继凭「data 无 type 字段」识别心跳帧并吞掉
2434
+ // (AgentEvent 是 closed oneof 全带 type,故可控)。**未来新增任何 SSE data 帧必须带 type 字段**,否则会被
2435
+ // 中继当心跳吞掉、永远到不了前端。heartbeat 帧(`event: heartbeat` + `data: {}`)是唯一豁免。
2436
+ /** Stable, low-cardinality route label for metrics/logs (ids collapsed to `:id`). */
1597
2437
  function routeLabel(_method, url) {
1598
2438
  if (RUN_STEER_RE.test(url))
1599
2439
  return "/v1/runs/:id/steer";
@@ -1618,7 +2458,7 @@ function routeLabel(_method, url) {
1618
2458
  if (BAKE_ID_RE.test(url))
1619
2459
  return "/v1/images/bakes/:id";
1620
2460
  if (url === "/v1/approvals/stream")
1621
- return "/v1/approvals/stream";
2461
+ return "/v1/approvals/stream"; // native-push SSE — distinct from a /:id decide
1622
2462
  if (/^\/v1\/approvals\/[^/]+$/.test(url))
1623
2463
  return "/v1/approvals/:id";
1624
2464
  if (/^\/v1\/sessions\/[^/]+\/wake$/.test(url))
@@ -1640,9 +2480,13 @@ function routeLabel(_method, url) {
1640
2480
  return url;
1641
2481
  return "other";
1642
2482
  }
2483
+ /** CORS v2: allowlist semantics. A SINGLE configured origin keeps the v1 posture
2484
+ * (echoed unconditionally — non-browser callers see it too, byte-compat); MULTIPLE origins match the
2485
+ * request's Origin header (echo the matched one, `Vary: Origin` so a shared cache never serves origin-A's
2486
+ * grant to origin-B; no match ⇒ no allow-origin header ⇒ the browser refuses). Empty list = CORS off. */
1643
2487
  function cors(res, req, origins, principalHeader) {
1644
2488
  if (!origins || origins.length === 0)
1645
- return;
2489
+ return; // defensive undefined: a hand-built test config may omit the field
1646
2490
  let allow;
1647
2491
  if (origins.length === 1) {
1648
2492
  allow = origins[0];
@@ -1656,6 +2500,8 @@ function cors(res, req, origins, principalHeader) {
1656
2500
  return;
1657
2501
  res.setHeader("access-control-allow-origin", allow);
1658
2502
  res.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
2503
+ // Include the principal header so a browser front-end that sends it isn't rejected by the OPTIONS preflight
2504
+ // (which would otherwise drop identity → an owner=null session under requirePrincipal=false).
1659
2505
  res.setHeader("access-control-allow-headers", `content-type, authorization, last-event-id, ${principalHeader}`);
1660
2506
  }
1661
2507
  //# sourceMappingURL=server.js.map