@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
@@ -11,26 +11,48 @@ import { streamSseLog } from "../sse-log.js";
11
11
  import { normalizeRunEventType } from "../../trace/project.js";
12
12
  import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
13
13
  import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
14
+ // server.ts 侧的 routeLabel / isBillableSubmitPath 仍要用下面这些正则,故本模块导出(方向恒为 server.ts → routes/*)。
14
15
  export const RUN_ID_RE = /^\/v1\/runs\/([^/]+)(\/events)?$/;
15
16
  export const RUN_CANCEL_RE = /^\/v1\/runs\/([^/]+)\/cancel$/;
16
17
  export const RUN_STEER_RE = /^\/v1\/runs\/([^/]+)\/steer$/;
17
18
  export const RUN_DETACH_RE = /^\/v1\/runs\/([^/]+)\/detach$/;
18
19
  export const RUN_COMPACT_RE = /^\/v1\/runs\/([^/]+)\/compact$/;
20
+ // E23 (shell-host contract): the shell answers an inbound MCP elicitation here. Live-only + same-replica (the pending promise
21
+ // is in-memory) — a respond on the wrong replica / after answer/expiry → 404, like a steer on a non-live run.
22
+ /** PAIR-REVIEW F-10:runs 面 404 文案统一富形——a\* 形 id(background-agent registry 句柄,`a`+hex 短形)
23
+ * 是「结构性不在 run 店」的常客([1347]⑤ 记档裁量:不改路由,只给引导)。识别句法形即补引导;
24
+ * 非 a\* 形保通用富文案(cancel 腿 [888]① 同款)。 */
25
+ /** B7(design/158):文案逐字不动,只把「体」换成「文案」——两个调用点改走 `sendError(…, "not_found.run", …)`,
26
+ * 机器码由发送点携带(errorCode 恒在),这样错误码门不必为「体来自 helper」的站点开豁免。 */
19
27
  function runNotFoundMessage(id) {
28
+ // [1434]:前缀=域判别子既成契约——wa\*(workflow agent 观测行,1.356)与 a\* 同族放宽。
20
29
  const aStar = /^(?:a|wa)[0-9a-f]{4,}$/i.test(id);
21
30
  return aStar
22
31
  ? `run not found — "${id}" looks like an agent handle (a*/wa* domain), which is never in the run store; use the TaskOutput tool (or the workflow journal / subagents surface) to read it, and the run's durable taskId (X-Task-Id) for this endpoint`
23
32
  : "run not found — the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
24
33
  }
25
34
  export const ELICIT_RESPOND_RE = /^\/v1\/elicitations\/([^/]+)\/respond$/;
35
+ // §4④: the shell answers the agent's AskUserQuestion here. Same live-only + same-replica shape as elicit.
26
36
  export const QUESTION_RESPOND_RE = /^\/v1\/questions\/([^/]+)\/respond$/;
27
37
  export const TOOL_APPROVAL_RESPOND_RE = /^\/v1\/tool-approvals\/([^/]+)\/respond$/;
38
+ // C2 (core 1.219): steer a still-running Task SUB-AGENT of a run. `:target` = the child's
39
+ // parentToolCallId (spawn-time identity, unique) or its agentName (display name; 409 when ambiguous).
28
40
  export const RUN_SUBAGENT_STEER_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/steer$/;
41
+ // design/122 (core 1.225): REVIVE a SETTLED sub-agent with a new prompt (CC dfe parity).
29
42
  export const RUN_SUBAGENT_RESUME_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/resume$/;
43
+ // [1488]③(b): GET a BACKGROUND child's final report / current status (a… agent handle → core TaskRegistry,
44
+ // the TaskOutput-tool face over HTTP; background_agent ONLY — wa… workflow-agent rows are observation
45
+ // identities read via the workflow journal). Complements steer/resume (which cover SYNC children only).
30
46
  export const RUN_SUBAGENT_OUTPUT_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/output$/;
31
47
  export const RUN_SUBAGENT_STREAM_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/stream$/;
48
+ // [1499] CC TaskOutput/TaskStop 人侧对位 — the GENERIC task-handle verbs (b* bash stdout — cursor
49
+ // consumption depends on the handle's shape, see the handler; m* monitor batches, a* agent final report;
50
+ // `workflow` refused — journal face owns it). SESSION-enforced from birth (new face, zero consumers):
51
+ // a session-bound run requires the matching ?session=.
32
52
  export const RUN_TASK_OUTPUT_RE = /^\/v1\/runs\/([^/]+)\/tasks\/([^/]+)\/output$/;
33
53
  export const RUN_TASK_STOP_RE = /^\/v1\/runs\/([^/]+)\/tasks\/([^/]+)\/stop$/;
54
+ /** The task_run SSE — a thin wrapper over `streamSseLog` that PRESERVES the existing wire shape exactly
55
+ * (`data: {type, ...data}`, `event: failed` stale frame). Behavior-identical to the pre-refactor reader. */
34
56
  async function streamRunEvents(req, res, runStore, taskId, staleMs) {
35
57
  await streamSseLog(req, res, {
36
58
  statusOf: async (id) => {
@@ -39,6 +61,9 @@ async function streamRunEvents(req, res, runStore, taskId, staleMs) {
39
61
  },
40
62
  getEvents: (id, after) => runStore.getEvents(id, after),
41
63
  retainedFrom: (id) => runStore.retainedFrom(id),
64
+ // design/158 B2:唯一的读边界归一 —— 存量 `brain_status` 行按公开联合的名字(`status`)下发。
65
+ // `event:` 与 `data.type` 用**同一个**归一后的名字(此前也是同一个原始名),否则按 event 名分派的
66
+ // 消费端与按 data.type 分派的消费端会看到两套名字。除这一个键外全部原样透传(见 normalizeRunEventType)。
42
67
  formatEvent: (ev) => {
43
68
  const type = normalizeRunEventType(ev.type);
44
69
  return { id: ev.seq, event: type, data: { type, ...(ev.data ?? {}) } };
@@ -51,23 +76,34 @@ export async function handleRuns(req, res, url, ctx) {
51
76
  await handleRunsBody(req, res, url, ctx, miss);
52
77
  return !miss.fell;
53
78
  }
79
+ /** 子代理 / 任务句柄 / HITL 应答段——`handle()` 里它在 SVC-5 workflow steer **之后**,故是独立的域入口。 */
54
80
  export async function handleRunVerbs(req, res, url, ctx) {
55
81
  const miss = { fell: false };
56
82
  await handleRunVerbsBody(req, res, url, ctx, miss);
57
83
  return !miss.fell;
58
84
  }
85
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
86
+ * = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
59
87
  async function handleRunsBody(req, res, url, ctx, miss) {
60
88
  const { deps } = ctx;
61
89
  const { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints } = ctx.registry;
62
90
  const { readJson, rateLimited, quotaExceeded, leaseDenied, runOwnerOk, runSessionAcceptOk } = ctx.helpers;
63
91
  const { prepareSpec } = ctx.legs;
64
92
  const source = ctx.req.source;
93
+ // Async run: create + return ids immediately, execute in the background.
65
94
  if (req.method === "POST" && url === "/v1/runs") {
66
95
  if (!deps.runStore) {
67
96
  sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store (SESSION_BACKEND=tidb)");
68
97
  return;
69
98
  }
99
+ // Idempotency-Key dedup (center blocker): a retried create returns the SAME taskId instead of starting a
100
+ // second background run (= second bill). Concurrent same-key requests share the one in-flight create. The
101
+ // replay is checked BEFORE the rate/quota gates so a retry doesn't get a fresh 429 (council).
70
102
  const rawIdem = headerStr(req.headers["idempotency-key"]);
103
+ // Scope by identity only when there IS a key (BL-3) — avoids reading the principal header on every
104
+ // submit, and keeps the no-key path independent of principal config.
105
+ // F-fix: scope on the VERIFIED principal (gatedPrincipal) — on a direct door a spoofable header would let a
106
+ // caller poison/collide another principal's idempotency cache. (BFF/gated: identical to principalFrom.)
71
107
  const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, gatedPrincipal(req, deps.config)) : undefined;
72
108
  const cached = idemKey ? idemCache.peek(idemKey) : undefined;
73
109
  if (cached) {
@@ -75,12 +111,22 @@ async function handleRunsBody(req, res, url, ctx, miss) {
75
111
  sendJson(res, resp.status, resp.body);
76
112
  return;
77
113
  }
114
+ // 🔴 复审 C1:NO lease gate here — it must not sit ABOVE the durable-taskId idempotency replay
115
+ // below, or a retry of an already-created run (dispatch-gateway failover) would get 429 instead of its
116
+ // existing 202 receipt, breaking the exactly-once contract. The lease admits a genuinely-NEW run only
117
+ // (applied just before createRun). Rate/quota stay here (cheap pre-reject; a rate-limited replay is a
118
+ // transient the caller retries, unlike a persistent lease exhaustion).
78
119
  if (rateLimited(req, res) || quotaExceeded(req, res))
79
120
  return;
80
121
  const prepared = await prepareSpec(req, res);
81
122
  if (!prepared)
82
123
  return;
83
124
  const runStore = deps.runStore;
125
+ // dispatch-gateway failover prerequisite: OPTIONAL caller-minted `body.taskId` idempotency. Unlike the
126
+ // Idempotency-Key header (in-memory, per-instance), this replay reads the DURABLE run store, so a gateway that
127
+ // re-routes a submit to ANOTHER instance after a network error cannot start a second run. uuidv7-validated
128
+ // (same alphabet the server mints — a caller can't inject an arbitrary PK shape); owner-gated replay mirrors
129
+ // the GET /v1/runs/:id gate (a foreign taskId reads as 404-shaped conflict, not another tenant's run ids).
84
130
  const rawClientTaskId = prepared.body.taskId;
85
131
  if (rawClientTaskId !== undefined && (typeof rawClientTaskId !== "string" || !isUuidV7(rawClientTaskId))) {
86
132
  sendError(res, 400, "request.id_invalid", "body.taskId must be a uuidv7 string (caller-minted idempotency key)");
@@ -92,13 +138,15 @@ async function handleRunsBody(req, res, url, ctx, miss) {
92
138
  if (existing) {
93
139
  const verified = verifiedPrincipal(req, deps.config);
94
140
  if (existing.owner !== null && existing.owner !== verified) {
95
- sendError(res, 409, "conflict.run_exists", "taskId already exists");
141
+ sendError(res, 409, "conflict.run_exists", "taskId already exists"); // foreign owner: conflict, no replay (no cross-tenant oracle)
96
142
  return;
97
143
  }
98
144
  sendJson(res, 202, { taskId: existing.taskId, sessionId: existing.sessionId, status: existing.status });
99
145
  return;
100
146
  }
101
147
  }
148
+ // 🔴 复审 C1:lease gate for a genuinely-NEW submission (past the idempotency replay above). Keyed on the
149
+ // VERIFIED submitter = the principal billed for this run's spend (correct principal for the sync door).
102
150
  if (await leaseDenied(req, res))
103
151
  return;
104
152
  const resp = await idemCache.run(idemKey, async () => {
@@ -106,16 +154,23 @@ async function handleRunsBody(req, res, url, ctx, miss) {
106
154
  const taskId = clientTaskId ?? uuidv7();
107
155
  const created = await runStore.createRun(taskId, sessionId, prepared.auth?.principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
108
156
  if (created.ok)
109
- deps.sessionTitler?.maybeTitle(sessionId, prepared.spec.objective);
157
+ deps.sessionTitler?.maybeTitle(sessionId, prepared.spec.objective); // fire-and-forget session auto-title
110
158
  if (!created.ok) {
159
+ // race twin: two concurrent same-taskId submits (gateway retry racing the original) — the loser's
160
+ // session claim collides with ITS OWN taskId → idempotent 202 replay, not a 409 (the run exists, once).
111
161
  if (clientTaskId && created.activeTaskId === clientTaskId) {
112
162
  return { status: 202, body: { taskId: clientTaskId, sessionId, status: "running" } };
113
163
  }
114
164
  return { status: 409, body: { error: "session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)", errorCode: "conflict.session_active_run", activeTaskId: created.activeTaskId } };
115
165
  }
166
+ // Durable F4: persist the resume rebuild inputs (sessionId-keyed) so an operator can resume from any
167
+ // replica even after this worker is gone — core's checkpoint blob can't carry service scenario context.
116
168
  if (deps.checkpointStore) {
117
169
  await deps.checkpointStore.putCtx(sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
118
170
  }
171
+ // E18 resume-at: capture each completed turn's (message eventId → leaf entryId) anchor so the shell can later
172
+ // rewind to that message. Closes over THIS run's sessionId/owner + the session store's getLeafId + the anchor
173
+ // store. undefined ⇒ no anchor store / no getLeafId (the env-only deploy) → resume-at simply isn't captured.
119
174
  const anchorOwner = prepared.auth?.principal ?? null;
120
175
  const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
121
176
  const captureTurnAnchor = deps.resumeAnchorStore && getLeafId
@@ -125,18 +180,42 @@ async function handleRunsBody(req, res, url, ctx, miss) {
125
180
  await deps.resumeAnchorStore.put(sessionId, eventId, leaf, anchorOwner);
126
181
  }
127
182
  : undefined;
183
+ // R8 (CC-parity rewind): the USER-message anchor — keyed by THIS run's taskId (the handle the shell holds
184
+ // from the 202 response), value = the user-message `entryId` straight from `message_committed{role:"user"}`
185
+ // (always a valid resumeAt target). No `getLeafId` (the entryId is on the event), so it works even on a
186
+ // store that lacks getLeafId. This is the "rewind to the prompt" target = the code-restore parity path.
128
187
  const captureUserMessageAnchor = deps.resumeAnchorStore
129
188
  ? async (entryId) => { await deps.resumeAnchorStore.put(sessionId, taskId, entryId, anchorOwner); }
130
189
  : undefined;
190
+ // MF-Fleet: a background (202) run is a durable run → it gets a live fleet row exactly like the sync leg.
191
+ // runInBackground fires onStart/onEvent/onTerminal across ALL its legs (verify/cascade/plain-stream). No-op
192
+ // when no fleetBus is wired (the publisher is a null-object).
131
193
  const fleetPub = fleetRunPublisher(deps.fleetBus, {
132
194
  runId: taskId,
195
+ // 🔒 Scope the fleet row with the SAME identity GET /v1/fleet/stream FILTERS by — `gatedPrincipal`
196
+ // (crypto-verified on a direct door), NOT `prepared.auth.principal` (= the spoofable header). On a direct
197
+ // door the two diverge: a spoofed header tags the row under a victim's scope (cross-tenant leak) or under
198
+ // "default" while the verified owner's stream filters it OUT (the row vanishes). TOC review #6.
133
199
  scope: gatedPrincipal(req, deps.config) ?? "default",
200
+ // core 1.154 nested-subagent tree: rootTaskId = the run's CANONICAL core taskId (`spec.taskId ?? sessionId`).
201
+ // The background leg sets `spec.taskId = taskId` (runs.ts), so canonical == taskId == runId — a forwarded
202
+ // subagent whose `parentTaskId` equals this nests under the run; deeper ones nest under their parent subagent.
134
203
  rootTaskId: taskId,
135
- ...fleetRunLabels(prepared.spec.objective),
204
+ ...fleetRunLabels(prepared.spec.objective), // BC-1: name = short objective preview (description = live-activity, set by the publisher onEvent tool_start)
136
205
  });
137
- void runInBackground(deps.runner, { ...prepared.spec, sessionId }, runStore, taskId, deps.metrics, prepared.auth?.principal, prepared.verify, prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation, gatedPrincipal(req, deps.config) ?? null, captureTurnAnchor, fleetPub, captureUserMessageAnchor, deps.question, deps.workflowCompletionInbox, deps.subagentSteerRegistry, (m, x) => deps.logger?.info?.(m, x), deps.sendUserFile, deps.promptManifests);
206
+ void runInBackground(deps.runner, { ...prepared.spec, sessionId }, runStore, taskId, deps.metrics, prepared.auth?.principal, prepared.verify, prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation, // E23: per-run elicitation context (onElicit routes inbound MCP elicitations to this run's stream)
207
+ gatedPrincipal(req, deps.config) ?? null, // E23: the VERIFIED principal that may answer (same source the respond gate uses — never the spoofable header)
208
+ captureTurnAnchor, // E18: per-turn (message eventId → leaf entryId) anchor capture
209
+ fleetPub, // MF-Fleet: the run-scoped fleet-row publisher (onStart/onEvent/onTerminal across all legs)
210
+ captureUserMessageAnchor, // R8: per-prompt (taskId → user-message entryId) anchor — CC "rewind to the prompt"
211
+ deps.question, // §4④: per-run AskUserQuestion context (onQuestion routes the agent's asks to this run's stream)
212
+ deps.workflowCompletionInbox, // P1 ①② follow-on: drain pending workflow completions into this leg's durable log at start
213
+ deps.subagentSteerRegistry, // C2 (core 1.219): register sync-delegation steer handles under this run's taskId
214
+ (m, x) => deps.logger?.info?.(m, x), // emit-target diagnostics for the bg leg's inbox drain
215
+ deps.sendUserFile, // SendUserFile 切片2:file_link 帧走本腿 durable append
216
+ deps.promptManifests);
138
217
  return { status: 202, body: { taskId, sessionId, status: "running" } };
139
- }, (r) => r.status === 202);
218
+ }, (r) => r.status === 202); // don't cache a transient 409 (session-active, billed nothing) — let a retry re-run (council)
140
219
  sendJson(res, resp.status, resp.body);
141
220
  return;
142
221
  }
@@ -147,11 +226,18 @@ async function handleRunsBody(req, res, url, ctx, miss) {
147
226
  return;
148
227
  }
149
228
  const taskId = runMatch[1];
229
+ // Enforce the principal requirement BEFORE the lookup so the 401/404 ordering matches the approvals
230
+ // endpoint (no 404-vs-401 existence oracle for an unauthenticated caller under requirePrincipal).
231
+ // 🔴 F-fix: gatedPrincipal (VERIFIED), not the spoofable header. On a direct door the legit owner authenticates
232
+ // by the JWT (no x-agent-principal header), so a `principalFrom` presence check would 401 a VALID caller — and a
233
+ // spoofed header must not satisfy this gate either. Mirrors runOwnerOk's verified check below. (BFF/gated: same.)
150
234
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
151
235
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
152
236
  return;
153
237
  }
154
238
  const run = await deps.runStore.getRun(taskId);
239
+ // NB: `runOwnerOk` is short-circuited away when `!run`, so the unknown-run case MUST send its own
240
+ // response — otherwise the request never completes (hung socket, invisible to access log/metrics).
155
241
  if (!run) {
156
242
  sendError(res, 404, "not_found.run", runNotFoundMessage(taskId));
157
243
  return;
@@ -159,15 +245,25 @@ async function handleRunsBody(req, res, url, ctx, miss) {
159
245
  if (!runOwnerOk(req, res, run.owner))
160
246
  return;
161
247
  if (!runSessionAcceptOk(req, res, run, runMatch[2] ? "run.events" : "run.poll"))
162
- return;
248
+ return; // [1499] accept-phase
163
249
  if (runMatch[2]) {
164
250
  await streamRunEvents(req, res, deps.runStore, taskId, deps.config.runStaleSec * 1000);
165
251
  }
166
252
  else {
167
253
  const stale = run.status === "running" && Date.now() - new Date(run.updatedAt).getTime() > deps.config.runStaleSec * 1000;
254
+ // design/80 b (item 3): the supervisor cost surface = core's LLM costBreakdown + the service's infra axes.
255
+ // Only when infra pricing is configured (else the client reads stats.costBreakdown directly) AND the run is
256
+ // terminal with stats — reads the event log ONCE for the tool-call count + uses the run's wall-clock span as
257
+ // the sandbox-walltime proxy. Skipped for running/no-rate runs (no per-poll events read in the common case).
168
258
  const infraRates = deps.config.infraCostRates;
169
259
  const needCost = Boolean(run.result?.stats && !stale && infraRates && hasInfraPricing(infraRates));
260
+ // E12: surface post-completion prompt suggestions on the poll (the `await suggestions()` retrieval). They land
261
+ // as a `suggestions` event AFTER `done` (post-terminal, fire-and-forget) — so NO live events tail (which closes
262
+ // at the terminal status) can carry them; a poller reads them HERE once the pass settles. Only on a COMPLETED
263
+ // run (the sole status with suggestions), so a running poll never pays the events read.
170
264
  const needSuggestions = !stale && run.status === "completed";
265
+ // Read the event log ONCE if either consumer needs it (skipped for running / no-rate to avoid a per-poll read).
266
+ // getEvents is a RunStore staple, but guard its presence so a degraded/partial store just omits these extras.
171
267
  const events = (needCost || needSuggestions) && deps.runStore.getEvents ? await deps.runStore.getEvents(taskId, 0).catch(() => []) : undefined;
172
268
  let supervisorCost;
173
269
  if (needCost && events && run.result?.stats) {
@@ -178,8 +274,13 @@ async function handleRunsBody(req, res, url, ctx, miss) {
178
274
  }
179
275
  let suggestions;
180
276
  if (needSuggestions && events) {
181
- const sug = [...events].reverse().find((e) => e.type === "suggestions");
277
+ const sug = [...events].reverse().find((e) => e.type === "suggestions"); // last wins (a re-run could re-emit)
182
278
  const arr = sug?.data?.suggestions;
279
+ // 🔴 **事件在场 ⇒ 已结算**,空数组必须原样透出(2026-07-26)。此前这里要求 `arr.length`,
280
+ // 于是「跑完了、确实一条都没有」与「还没跑完」都变成 `undefined` ⇒ **轮询方无从判断该不该停**。
281
+ // 与发射侧那半(`runs.ts` 此前 `if (suggestions.length)` 才 append)是同一个态被两处叠加抹平。
282
+ // ⚠️ 这属于本仓刚修过的**同一类**:消费方拿终态当停止条件,而数据在终态之后才到
283
+ // (`setTerminal` 在 suggestions 落之前)。判据统一成:事件在 = 已结算;事件不在 = 仍在跑。
183
284
  if (Array.isArray(arr))
184
285
  suggestions = arr.map((s) => String(s));
185
286
  }
@@ -189,7 +290,11 @@ async function handleRunsBody(req, res, url, ctx, miss) {
189
290
  status: stale ? "failed" : run.status,
190
291
  result: run.result ?? undefined,
191
292
  supervisorCost,
192
- suggestions,
293
+ suggestions, // E12: present once the post-completion pass settled (undefined while pending / if none / off)
294
+ // Surface the structured failure code at the top level so async-run clients (OA / team skill) can
295
+ // branch (auth → stop, budget.*/limit.* → quota, network·server → retry/failover) without parsing
296
+ // the whole result. It already lives inside the persisted result JSON — no run_store schema change.
297
+ // A `_stale` infra timeout is not a core error, so it carries no errorCode.
193
298
  errorCode: stale ? undefined : (run.result?.errorCode ?? undefined),
194
299
  error: stale ? "run stalled (instance lost?)" : run.error ?? undefined,
195
300
  jobId: run.jobId ?? undefined,
@@ -198,12 +303,19 @@ async function handleRunsBody(req, res, url, ctx, miss) {
198
303
  }
199
304
  return;
200
305
  }
306
+ // POST /v1/runs/:id/cancel — stop a durable async run. Durable cross-replica flag (the owning instance's
307
+ // heartbeat tick honors it) + a same-replica AbortController fast path. Terminal = "failed"+errorCode
308
+ // "cancelled". Idempotent on a terminal run. [868] a SUSPENDED run is cancelled here too: settle the
309
+ // pending checkpoint (CAS expire) then terminalize the row — see cancelSuspended below.
201
310
  const cancelMatch = req.method === "POST" ? RUN_CANCEL_RE.exec(url) : null;
202
311
  if (cancelMatch) {
203
312
  if (!deps.runStore) {
204
313
  sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
205
314
  return;
206
315
  }
316
+ // Principal check BEFORE the lookup (parity with GET /v1/runs/:id): no 404-vs-401 existence oracle.
317
+ // 🔴 F-fix: gatedPrincipal (VERIFIED), not the spoofable header (parity with runOwnerOk below + the GET gate) —
318
+ // else a direct-door owner authenticating by JWT (no x-agent-principal header) is wrongly 401'd.
207
319
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
208
320
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
209
321
  return;
@@ -211,13 +323,27 @@ async function handleRunsBody(req, res, url, ctx, miss) {
211
323
  const taskId = cancelMatch[1];
212
324
  const run = await deps.runStore.getRun(taskId);
213
325
  if (!run) {
326
+ // [888]① 文案:这个 face 覆盖本部署 run store 里的全部 run(bg/durable + interactive sync 腿都建行、
327
+ // 都注册快路)——404 只剩「id 属于别的进程/别的 store」一类(如另起的 server 进程配 in-memory store)。
214
328
  sendError(res, 404, "not_found.run", "run not found — the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)");
215
329
  return;
216
330
  }
217
331
  if (!runOwnerOk(req, res, run.owner))
218
- return;
332
+ return; // owner mismatch → 404 (no existence oracle for non-owners)
219
333
  if (!runSessionAcceptOk(req, res, run, "run.cancel"))
220
- return;
334
+ return; // [1499] accept-phase
335
+ // [868] P0: cancel of a SUSPENDED run — the user's recovery handle for a locked session. Previously a flat
336
+ // 409 "deny the pending approval instead", which dead-ended when the deny itself failed or was swallowed
337
+ // (the incident: an AskUserQuestion durable park whose shell deny was lost kept the task_active claim with
338
+ // ZERO release paths on the file/memory lane — every later submit on the session 409'd forever). New
339
+ // semantics: settle the pending checkpoint FIRST via the CheckpointStore CAS `expire` (the reaper's
340
+ // "≈ deny" terminal — deliberately NOT resolve/resumeCheckpoint, which would drive a model continuation:
341
+ // cancel means the run DIES; a deny-RESUME feeds the refusal back to the model and burns tokens against
342
+ // the user's intent, and denying an AskUserQuestion is incoherent anyway — the reapExpired kind-split
343
+ // already encodes that), THEN terminalize the run row (setTerminal releases task_active = unlocks the
344
+ // session). Order is load-bearing: checkpoint-first means a late approve loses the CAS and is rejected
345
+ // honestly by /decide; terminal-first would open the window where an approve wins the CAS on a dead row
346
+ // and driveResume ghosts against markResuming (safe, but a lie in two ledgers).
221
347
  const rs = deps.runStore;
222
348
  const cancelSuspended = async () => {
223
349
  const cs = deps.checkpointStore;
@@ -226,20 +352,34 @@ async function handleRunsBody(req, res, url, ctx, miss) {
226
352
  const token = await cs.findPendingTokenBySession(run.sessionId);
227
353
  if (token) {
228
354
  const cp = await cs.get(token);
355
+ // CAS pending→expired; `false` = a concurrent decide (or the reaper) settled the gate first.
356
+ // Winning it means NO model leg can run off this gate: an approve that already won markResuming
357
+ // (row=running) has its resumeStream reject on the now-expired checkpoint BEFORE any model call and
358
+ // settles the row failed itself — our unconditional setTerminal below double-writes the same
359
+ // terminal (harmless; claim releases once). 双腿并跑 is impossible on this path (codex H1 review).
229
360
  const won = cp ? await cs.expire(token, cp.scope) : false;
230
361
  if (!won) {
362
+ // CAS lost — answer by the run's ACTUAL state, never pretend the cancel landed.
231
363
  const now = await rs.getRun(taskId);
232
364
  const st = now?.status;
233
365
  if (st === "suspended" || st === "needs_review" || st === "running") {
366
+ // 文案中性覆盖两因(复验镜头:输家可能是 decide 也可能是 reaper 的 expire——后者无人 resuming)
234
367
  sendError(res, 409, "conflict.approval_settled", "pending approval was settled concurrently (decided or expired) — re-check the run and retry cancel if it is still active", { taskId, status: st });
235
368
  }
236
369
  else {
237
- sendJson(res, 202, { taskId, status: st ?? "failed", note: "run already terminal — cancel is a no-op" });
370
+ sendJson(res, 202, { taskId, status: st ?? "failed", note: "run already terminal — cancel is a no-op" }); // idempotent
238
371
  }
239
372
  return;
240
373
  }
241
374
  }
242
375
  else {
376
+ // NO pending token. Two very different worlds share this face (codex H1):
377
+ // (a) the lockup fingerprint — deny lost / checkpoint expired-or-reaped, row parked forever;
378
+ // (b) a concurrent approve ALREADY resolved the checkpoint and its resume leg is running — an
379
+ // unconditional setTerminal here would kill/orphan that leg AND release the claim (双腿并跑).
380
+ // Disambiguate with the SAME CAS the resume path uses: markResuming flips ONLY a suspended row.
381
+ // Winning it = no resume leg exists nor can start (approve's own markResuming will now lose) — the
382
+ // row is OURS to terminalize. Losing it = answer by the run's actual state, never pretend.
243
383
  const claimed = await rs.markResuming(taskId);
244
384
  if (!claimed) {
245
385
  const now = await rs.getRun(taskId);
@@ -248,7 +388,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
248
388
  sendError(res, 409, "conflict.approval_settled", "pending approval was settled concurrently (decided or expired) — re-check the run and retry cancel if it is still active", { taskId, status: st });
249
389
  }
250
390
  else {
251
- sendJson(res, 202, { taskId, status: st ?? "failed", note: "run already terminal — cancel is a no-op" });
391
+ sendJson(res, 202, { taskId, status: st ?? "failed", note: "run already terminal — cancel is a no-op" }); // idempotent
252
392
  }
253
393
  return;
254
394
  }
@@ -256,15 +396,23 @@ async function handleRunsBody(req, res, url, ctx, miss) {
256
396
  }
257
397
  }
258
398
  else {
399
+ // Minimal deployments without a checkpoint store: the run row is the only ledger — release it, say so.
259
400
  note = "cancelled while suspended (no checkpoint store on this deployment — run row terminalized only)";
260
401
  }
402
+ // Run-row terminal AFTER the checkpoint settle. Two ledgers, no silent tear: if this write fails after
403
+ // the checkpoint already expired, report exactly how far the cancel got — a retry takes the
404
+ // no-pending-token path above and completes the release (idempotent recovery). The row's error text
405
+ // = the SAME per-path truth as the response note (a stale-park release must not claim it settled a gate).
261
406
  const err = note;
262
407
  try {
263
408
  const result = { taskId, sessionId: run.sessionId, status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 } };
409
+ // [888] 低危残账修:撕裂窗自愈——checkpoint 已 settle 后 setTerminal 一次瞬时失败(store 抖动)曾直接
410
+ // 500,把释放推给 reapStale 的整个 stale 窗(no-token 路的重试读到 running 只会挂 requestCancel 旗,
411
+ // 无人消费)。这里就地有界重试(2 次退避)把瞬时抖动吞掉;仍然失败才 500 交 reapStale 兜底。
264
412
  let lastErr;
265
413
  for (let attempt = 0;; attempt++) {
266
414
  try {
267
- await rs.setTerminal(taskId, "failed", result, err);
415
+ await rs.setTerminal(taskId, "failed", result, err); // TaskStatus has no 'cancelled' member — failed + errorCode 'cancelled' is the codebase-wide cancel terminal (same shape as the resume-abort settle)
268
416
  lastErr = undefined;
269
417
  break;
270
418
  }
@@ -279,9 +427,14 @@ async function handleRunsBody(req, res, url, ctx, miss) {
279
427
  throw lastErr;
280
428
  }
281
429
  catch (e) {
430
+ // 文案与 note 同源(复验镜头:no-token/无 cs 路没有 settle 任何 gate,固定文案会失真);另注:
431
+ // no-token 路的重试会读到 running 走 requestCancel 旗,实际释放由 reapStale 兜底(有界延迟)。
282
432
  sendError(res, 500, "internal.cancel_not_terminalized", `cancel progressed (${note}) but could not terminalize the run row (retried) — retry cancel; a stuck row is reaped after the stale window (${e instanceof Error ? e.message : String(e)})`, { taskId });
283
433
  return;
284
434
  }
435
+ // [1.207 codex M2] setTerminal 是 void 且 first-writer-wins 后 CAS 写打空不抛——退避窗内 reaper/
436
+ // 并发腿可能先终态化(如 approval.expired),我方写打空却仍答 "cancelled" 就是响应与账本不一致。
437
+ // 写后复读,照实际行应答(锁一样释放了;谁先写赢谁的 errorCode)。
285
438
  const finalRow = await rs.getRun(taskId).catch(() => undefined);
286
439
  if (finalRow && finalRow.errorCode !== "cancelled" && (finalRow.status === "failed" || finalRow.status === "completed" || finalRow.status === "blocked" || finalRow.status === "timeout")) {
287
440
  sendJson(res, 202, { taskId, status: finalRow.status, errorCode: finalRow.errorCode ?? null, note: `session unlocked; the run was terminalized concurrently (${finalRow.errorCode ?? finalRow.status}) before this cancel's write — reporting the actual ledger state` });
@@ -290,17 +443,22 @@ async function handleRunsBody(req, res, url, ctx, miss) {
290
443
  sendJson(res, 202, { taskId, status: "failed", errorCode: "cancelled", note });
291
444
  };
292
445
  if (run.status === "running") {
293
- const flagged = await deps.runStore.requestCancel(taskId, run.owner);
446
+ // Durable flag FIRST (only flips a still-running row), abort after: if the run suspended/terminal'd
447
+ // between the getRun above and here, the flag write affects 0 rows — re-read and answer by the run's
448
+ // ACTUAL state instead of lying "cancelling" about a run nothing will cancel.
449
+ const flagged = await deps.runStore.requestCancel(taskId, run.owner); // owner guard: single-DB defense-in-depth (HTTP gate already passed runOwnerOk)
294
450
  if (!flagged) {
295
451
  const now = await deps.runStore.getRun(taskId);
296
452
  if (now?.status === "suspended") {
297
- await cancelSuspended();
453
+ await cancelSuspended(); // [868] lost race INTO suspended — same recovery handle as the direct branch
298
454
  }
299
455
  else {
300
456
  sendJson(res, 202, { taskId, status: now?.status ?? "failed", note: "run already terminal — cancel is a no-op" });
301
457
  }
302
458
  return;
303
459
  }
460
+ // fast path: cancel landed on the running instance → abort now. Label FIRST ([1.207 codex H2]) so the
461
+ // interactive legs' settle projection sees the verb origin (bg legs already write "cancelled" themselves).
304
462
  if (inflightRuns.has(taskId)) {
305
463
  cancelledViaVerb.add(taskId);
306
464
  inflightRuns.get(taskId).abort();
@@ -308,27 +466,53 @@ async function handleRunsBody(req, res, url, ctx, miss) {
308
466
  sendJson(res, 202, { taskId, status: "cancelling" });
309
467
  }
310
468
  else if (run.status === "suspended" || run.status === "needs_review") {
469
+ // [868] settle the pending gate + terminalize + unlock (was a flat 409 dead-end). needs_review rides the
470
+ // SAME handle (codex M1): a plan-review park holds the claim identically and the old else branch lied
471
+ // "already terminal" about it (claim never released).
311
472
  await cancelSuspended();
312
473
  }
313
474
  else {
314
- sendJson(res, 202, { taskId, status: run.status, note: "run already terminal — cancel is a no-op" });
475
+ sendJson(res, 202, { taskId, status: run.status, note: "run already terminal — cancel is a no-op" }); // idempotent
315
476
  }
316
477
  return;
317
478
  }
479
+ // POST /v1/runs/:id/steer — inject a mid-task steering message (CC-parity "运行中插话", design/47 + design/80
480
+ // D-A). Delivery is picked by the run's CURRENT state, surfaced via the status code (matching the spec):
481
+ // • in-flight on THIS replica (a live core TaskStream in `steerableRuns`) → `stream.steer()` queues the
482
+ // message into the harness, drained at the next turn boundary → 200 (applied).
483
+ // • durably SUSPENDED (any replica) → `setPendingSteer` parks it on the pending checkpoint, injected by core
484
+ // on the resume continuation → 202 (queued).
485
+ // • running on ANOTHER replica (not live here, status "running") → 409 steering.not_running. Cross-replica
486
+ // live-steer (a durable steer-signal + heartbeat poll, mirroring cancel_requested/preempt_requested) is a
487
+ // fast-follow — single-replica-per-worker deployment today, so the live + parked paths cover the headline
488
+ // use case; this branch is the honest seam, never a silent drop.
489
+ // • terminal (done/failed/cancelled/needs_review) → 409 steering.not_running.
490
+ // Content carrying a `</system-reminder>` break-out or control chars → 422 steering.invalid_content, validated
491
+ // ONCE up front with core's `validatePendingSteer` (the same validator setPendingSteer runs) so the 422 is
492
+ // consistent across both delivery paths — deliberately STRICTER than core's live path (which would sanitize an
493
+ // untrusted break-out at injection rather than reject it), trading that edge case for a uniform wire contract.
318
494
  const steerMatch = req.method === "POST" ? RUN_STEER_RE.exec(url) : null;
319
495
  if (steerMatch) {
320
496
  if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
321
- return;
497
+ return; // mutating + hits TiDB / runs the model
322
498
  if (!deps.runStore) {
323
499
  sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
324
500
  return;
325
501
  }
502
+ // Per-tenant identity MUST come from gatedPrincipal (direct-door secure single point), NOT principalFrom:
503
+ // `trusted` carries OPERATOR authority (a trusted steer may ride a <system-reminder>), so deriving it from a
504
+ // spoofable header on a direct-door worker would be a privilege escalation (HANDOFF 铁律 + core §3 inv #1).
326
505
  const principal = gatedPrincipal(req, deps.config);
327
506
  if (deps.config.requirePrincipal && principal === undefined) {
328
507
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
329
508
  return;
330
509
  }
331
510
  const taskId = steerMatch[1];
511
+ // Parse + validate the REQUEST before the run lookup: a bad body is the caller's error regardless of run
512
+ // state, and validating once here (not per delivery path) keeps 400/422 identical whether the run is live or
513
+ // suspended. `text` required non-empty; `mode` (CC `--steering-mode`) is accepted + enum-checked but a v1
514
+ // no-op — core's public TaskStream.steer takes only { trusted } today (harness steeringMode is build-time),
515
+ // so per-call mode is forward-draft pending core wiring (raised to core).
332
516
  let body;
333
517
  try {
334
518
  body = (await readJson(req));
@@ -345,16 +529,23 @@ async function handleRunsBody(req, res, url, ctx, miss) {
345
529
  sendError(res, 400, "request.field_invalid", "mode must be 'all' or 'one-at-a-time' when present");
346
530
  return;
347
531
  }
532
+ // E15 (shell-host contract): per-message queue priority. Accepted + enum-validated FAIL-LOUD, but ADVISORY
533
+ // today — core's steer is single-slot last-writer-wins with no per-message priority / addressable drop, so the
534
+ // faithful priority queue + DELETE /v1/runs/:id/queue/:messageId wait on a core seam (raised to core).
348
535
  if (body.priority !== undefined && body.priority !== "now" && body.priority !== "next" && body.priority !== "later") {
349
536
  sendError(res, 400, "request.field_invalid", "priority must be 'now', 'next', or 'later' when present");
350
537
  return;
351
538
  }
352
539
  const text = body.text;
353
540
  const priority = body.priority;
541
+ // Mint a server-side messageId so the shell can reference this steer (its drop handle once the core queue seam
542
+ // lands). Honest + stable now even while `priority` is a no-op forward-draft.
354
543
  const messageId = uuidv7();
544
+ // trusted = explicit operator (NOT bare isOperator — its empty-list "true-for-all" would let any caller send a
545
+ // system-authority steer). A normal owner's steer reaches the model as a plain user message (trusted:false).
355
546
  const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
356
547
  try {
357
- validatePendingSteer({ text, trusted });
548
+ validatePendingSteer({ text, trusted }); // throws CheckpointError(steering.invalid_content) on break-out / control chars
358
549
  }
359
550
  catch (e) {
360
551
  if (e instanceof CheckpointError && e.code === "steering.invalid_content") {
@@ -368,12 +559,19 @@ async function handleRunsBody(req, res, url, ctx, miss) {
368
559
  sendError(res, 404, "not_found.run", runNotFoundMessage(taskId));
369
560
  return;
370
561
  }
562
+ // Owner-gate (parity with preempt): an EXPLICIT operator may steer any tenant's run (supervisor intervention);
563
+ // a normal caller only its own. 404 (not 403) for a non-owner — no existence oracle.
371
564
  if (!trusted && run.owner !== null && run.owner !== principal) {
372
565
  sendError(res, 404, "not_found.run", "run not found");
373
566
  return;
374
567
  }
375
568
  if (!runSessionAcceptOk(req, res, run, "run.steer"))
376
- return;
569
+ return; // [1499] accept-phase
570
+ // Durable-park helper: land the steer on this session's PENDING checkpoint. The checkpoint table — not the run
571
+ // row — is the AUTHORITY: core writes the pending checkpoint during a suspend (before the service flips the run
572
+ // row to "suspended"), and CASes on `status='pending'`, so this lands iff the run is genuinely parked. Read the
573
+ // checkpoint's ACTUAL scope ("_" for a no-principal run) then its token; a missing scope/token/CAS-loss means
574
+ // the checkpoint resolved/expired/never-existed → "no-checkpoint". "no-store" = no checkpoint store wired.
377
575
  const tryPark = async () => {
378
576
  const cs = deps.checkpointStore;
379
577
  if (!cs)
@@ -388,6 +586,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
388
586
  };
389
587
  const sendParked = () => sendJson(res, 202, { taskId, status: "suspended", delivery: "queued", messageId, ...(priority ? { priority } : {}), note: "steer parked on the checkpoint — injected when the run resumes" });
390
588
  const sendNotRunning = (error) => sendError(res, 409, "steering.not_running", error);
589
+ // (a) Same-replica live path: the run is streaming here → inject now (drained at the next turn boundary).
391
590
  const live = steerableRuns.get(taskId);
392
591
  if (live) {
393
592
  try {
@@ -402,7 +601,10 @@ async function handleRunsBody(req, res, url, ctx, miss) {
402
601
  return;
403
602
  }
404
603
  if (code !== "steering.not_running")
405
- throw e;
604
+ throw e; // unexpected → outer handler 500
605
+ // Raced: the stream finished between the map lookup and steer(). It may have durably SUSPENDED in the SAME
606
+ // tick (core already wrote the pending checkpoint) — fall through to the durable park rather than a spurious
607
+ // 409 (the live→suspended transition window). Only a genuinely-terminal run → not_running.
406
608
  if (await tryPark() === "parked") {
407
609
  sendParked();
408
610
  return;
@@ -411,6 +613,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
411
613
  return;
412
614
  }
413
615
  }
616
+ // (b) Durably suspended → park the steer on the pending checkpoint (drained on resume).
414
617
  if (run.status === "suspended") {
415
618
  const outcome = await tryPark();
416
619
  if (outcome === "no-store") {
@@ -424,16 +627,27 @@ async function handleRunsBody(req, res, url, ctx, miss) {
424
627
  sendNotRunning("run is no longer suspended (resolved or expired)");
425
628
  return;
426
629
  }
630
+ // (c) running-elsewhere (cross-replica, fast-follow seam) → not_running.
427
631
  if (run.status === "running") {
428
632
  sendNotRunning("run is active on another replica — cross-replica live-steer is not yet supported");
429
633
  return;
430
634
  }
635
+ // (d) design/144 wake 半场(core 1.283):TERMINAL run + steer = 不再一律 409——铸一个部署侧
636
+ // task_done 纯 park checkpoint(PendingAction {kind:"task_done"},deployment-minted per [770]①c),
637
+ // 把消息 park 上去,等 POST /v1/sessions/:id/wake({gate:"wake"})把它作为续跑首轮输入消费。
638
+ // 先 tryPark:session 若已有 pending checkpoint(needs_review park / 先前铸的 wake park)直接park上去,
639
+ // 绝不双铸(一 session 一 pending 不变式)。铸造前置=checkpoint store + session leaf(leafId 锚续跑);
640
+ // 缺任一 = 原 409 契约(env-only/无树面诚实拒绝)。
431
641
  if (deps.checkpointStore && deps.sessionStorage?.getLeafId) {
642
+ // 复审 MED(陈旧 run 守卫):目标 run 终态但同 session 另有 ACTIVE run(claim 在)时,消息该去
643
+ // 活 run(live steer / suspended park),给这条旧终态 run 铸 park 只会造出基于活 leaf 的陈旧
644
+ // checkpoint + wake 撞 markResuming 409。诚实拒绝并指路。
432
645
  const activeTaskId = await deps.runStore.getActiveTaskId?.(run.sessionId).catch(() => undefined);
433
646
  if (activeTaskId !== undefined && activeTaskId !== null && activeTaskId !== taskId) {
434
647
  sendNotRunning(`run is ${run.status} and its session has an ACTIVE run (${activeTaskId}) — steer that run instead`);
435
648
  return;
436
649
  }
650
+ // per-session 串行(复审 HIGH:并发双终态 steer 的 tryPark→put 窗口双铸两个 pending park)。
437
651
  const prior = wakeParkMints.get(run.sessionId) ?? Promise.resolve();
438
652
  const mintResult = { parked: false };
439
653
  const job = prior.then(async () => {
@@ -441,11 +655,11 @@ async function handleRunsBody(req, res, url, ctx, miss) {
441
655
  if (await tryPark() === "parked") {
442
656
  mintResult.parked = true;
443
657
  return;
444
- }
658
+ } // 锁内重查:前一位刚铸完 → 直接 park 上去
445
659
  const leafId = await Promise.resolve(deps.sessionStorage.getLeafId(run.sessionId)).catch(() => undefined);
446
660
  if (leafId === undefined || leafId === null)
447
661
  return;
448
- const cpScope = run.owner ?? "_";
662
+ const cpScope = run.owner ?? "_"; // 匿名提交哨兵,与 main.ts putCtx 的 principal ?? "_" 同域
449
663
  const wakeToken = mintCheckpointToken();
450
664
  await cs.put(wakeToken, {
451
665
  token: wakeToken,
@@ -459,6 +673,8 @@ async function handleRunsBody(req, res, url, ctx, miss) {
459
673
  createdAt: Date.now(),
460
674
  sourceTaskId: taskId,
461
675
  });
676
+ // 跨副本纵深:put 后复核赢家——若另一副本抢先铸了别的 pending,expire 我方孤儿并把消息
677
+ // park 到赢家上(收敛到单 park;店无序时收敛到店的稳定扫描序,残窗如实记 fast-follow)。
462
678
  const winner = await cs.findPendingTokenBySession(run.sessionId).catch(() => null);
463
679
  if (winner !== null && winner !== wakeToken) {
464
680
  await cs.expire(wakeToken, cpScope).catch(() => undefined);
@@ -475,6 +691,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
475
691
  await job;
476
692
  }
477
693
  finally {
694
+ // 链尾自清:没有更晚的排队者(Map 仍指向本包装)才删,防长期泄漏;有排队者则由其链尾清。
478
695
  if (wakeParkMints.get(run.sessionId) === wrapped)
479
696
  wakeParkMints.delete(run.sessionId);
480
697
  }
@@ -486,15 +703,20 @@ async function handleRunsBody(req, res, url, ctx, miss) {
486
703
  sendNotRunning(`run is ${run.status} — not accepting steers`);
487
704
  return;
488
705
  }
706
+ // K-1c (shell-host §K): POST /v1/runs/:id/compact — MANUAL compaction (un-greys the shell's `/compact`). core
707
+ // 1.156 added `TaskStream.compact()`: it forces a compaction at the next SAFE turn boundary (never mid-turn) and
708
+ // emits `compacted{trigger:"manual"}` on the run's OWN stream (the result rides that stream, like steer). So this
709
+ // verb only applies to a run LIVE on THIS replica (`steerableRuns`); a suspended/terminal/other-replica run has no
710
+ // live stream + there is NO durable "pending compact" in core (unlike steer's setPendingSteer) → 409.
489
711
  const compactMatch = req.method === "POST" ? RUN_COMPACT_RE.exec(url) : null;
490
712
  if (compactMatch) {
491
713
  if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
492
- return;
714
+ return; // mutating + runs the model (compaction summarizes)
493
715
  if (!deps.runStore) {
494
716
  sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
495
717
  return;
496
718
  }
497
- const principal = gatedPrincipal(req, deps.config);
719
+ const principal = gatedPrincipal(req, deps.config); // direct-door-secure identity, never the spoofable header
498
720
  if (deps.config.requirePrincipal && principal === undefined) {
499
721
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
500
722
  return;
@@ -505,15 +727,22 @@ async function handleRunsBody(req, res, url, ctx, miss) {
505
727
  sendError(res, 404, "not_found.run", "run not found");
506
728
  return;
507
729
  }
730
+ // Owner-gate (parity with steer/preempt): an explicit operator may compact any tenant's run; a normal caller
731
+ // only its own. 404 (not 403) for a non-owner — no existence oracle.
508
732
  const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
509
733
  if (!operator && run.owner !== null && run.owner !== principal) {
510
734
  sendError(res, 404, "not_found.run", "run not found");
511
735
  return;
512
736
  }
513
737
  if (!runSessionAcceptOk(req, res, run, "run.compact"))
514
- return;
738
+ return; // [1499] accept-phase
739
+ // core 1.293:compact(opts) 收 { instructions?, signal? }。instructions=壳 /compact 的定向摘要指令,
740
+ // 此处 additive 透传(空 body/无字段=现状);signal(取消腿)不在本 verb——取消需要独立动词+
741
+ // per-pending AbortController 台账,等壳侧 UX 契约定形再开(先到先记:[853] 知会件)。
742
+ // 校验在 live 查找之前(steer 同纪律):400 形状不依赖运行态,live/非 live 一致。
515
743
  let compactBody;
516
744
  try {
745
+ // HttpError(413 request body too large)原样透传,不降级成语义错误的 400(交叉评审 L5)
517
746
  const parsed = await readJson(req);
518
747
  if (parsed === null || typeof parsed !== "object") {
519
748
  sendError(res, 400, "request.invalid_json", "body must be a JSON object when present");
@@ -529,6 +758,9 @@ async function handleRunsBody(req, res, url, ctx, miss) {
529
758
  sendError(res, 400, "request.invalid_json", "invalid JSON body");
530
759
  return;
531
760
  }
761
+ // 上限=core COMPACTION_INSTRUCTIONS_MAX_CHARS(2048 码点,auto-compaction.js:9;超长 core 会静默
762
+ // 截断)——HTTP 面按同口径拒收,fail-loud 而非 202 后半截指令悄悄丢(交叉评审 M1)。码点计数与
763
+ // core 同形([...s] 展开),防代理对 emoji 类双单元字符的错杀/漏杀。
532
764
  if (compactBody.instructions !== undefined && (typeof compactBody.instructions !== "string" || compactBody.instructions.length === 0 || [...compactBody.instructions].length > 2_048)) {
533
765
  sendError(res, 400, "request.field_invalid", "instructions must be a non-empty string of at most 2048 characters (code points) when present — the engine caps compaction instructions there");
534
766
  return;
@@ -539,10 +771,28 @@ async function handleRunsBody(req, res, url, ctx, miss) {
539
771
  sendError(res, 409, "compact.not_running", run.status === "running" ? "run is active on another replica — manual compact is replica-local" : `run is ${run.status} — not accepting compaction`);
540
772
  return;
541
773
  }
542
- void live.compact(compactInstructions !== undefined ? { instructions: compactInstructions } : undefined).then((outcome) => {
774
+ // FIRE-AND-FORGET 202 (review): `compact()` resolves only once PROCESSED at the next SAFE turn boundary — which
775
+ // can be a whole in-flight turn away — so awaiting it would hold the HTTP connection (gateway-timeout risk + poor
776
+ // UX). Ack "accepted" immediately; the compaction runs at the boundary and a `compacted{trigger:"manual"}` event
777
+ // rides the run's OWN stream (which the client already consumes — this matches the shell's `compacting%` spinner
778
+ // waiting on that event). A NO-OP compaction (nothing to summarize) emits NO event (so the shell must time out its
779
+ // spinner, not block forever). Swallow the benign race where the stream finished between the lookup and the call
780
+ // (steering.not_running) + any compact error — best-effort; failure surfaces as the absence of the event, the user
781
+ // can retry. (core guarantees compact() never hangs — it resolves by task end at the latest.)
782
+ void live.compact(compactInstructions !== undefined ? { instructions: compactInstructions } : undefined).then(
783
+ // core 1.257 (MF-18 BREAKING): compact() now RESOLVES WITH a CompactOutcome instead of void
784
+ // — "processed" alone was silent when the attempt failed or was mooted (the 202-then-silence fingerprint).
785
+ // Consume the outcome: "compacted" is the only one that emits a `compacted{trigger:"manual"}` event on the
786
+ // stream; the other five (failed/mooted/noop/blocked/disabled) each have a `compaction.*` trace counterpart
787
+ // and produce NO event, so the shell's spinner must fall through. Log the discriminated outcome so a
788
+ // "202 → no event" incident reads its reason straight from the log (not a service-wire-vs-core guess).
789
+ (outcome) => {
543
790
  const level = outcome === "failed" ? "warn" : "info";
544
791
  deps.logger?.[level]?.("manual_compact_outcome", { taskId, outcome, note: outcome === "compacted" ? "a compacted{trigger:'manual'} event rode the run stream" : `no event (see compaction.${outcome === "mooted" ? "mooted" : outcome} trace / task terminal events)` });
545
792
  }, (e) => {
793
+ // The not_running rejection was fully silent — correct for the benign settle race it was
794
+ // designed for, but a SYSTEMATIC not_running (e.g. a verb/handle mismatch on one lane) then has zero
795
+ // observability. Keep it non-warn (it IS usually benign) but leave a fingerprint.
546
796
  if (e.code !== "steering.not_running") {
547
797
  deps.logger?.warn?.("manual_compact_failed", { taskId, err: e instanceof Error ? e.message : String(e) });
548
798
  }
@@ -553,15 +803,25 @@ async function handleRunsBody(req, res, url, ctx, miss) {
553
803
  sendJson(res, 202, { taskId, status: "running", delivery: "accepted", note: "compaction will run at the next turn boundary; a compacted{trigger:'manual'} event rides the run stream if anything is summarized" });
554
804
  return;
555
805
  }
806
+ // core 1.207 (design/116 detach, CC mid-flight ctrl+b): POST /v1/runs/:id/detach {toolCallId} —
807
+ // move a RUNNING tool call to the background via `TaskStream.detach(toolCallId)`. Route (a) of the sync-leg
808
+ // detach verb design ((b)'s durable-leg fallback
809
+ // is blocked on TOC-local having no runStore). Core semantics: fire-and-forget + race-safe (a request landing
810
+ // before the tool reads its signal still detaches; after it finished = no-op; unknown toolCallId = no-op) —
811
+ // the detached Bash settles early with "moved to background; task_id=b*" + a `{type:"bash", detached:true}`
812
+ // structured card, and the G2b completion notification/spool take over. Only a detach-capable env honors it
813
+ // (`backgroundCapabilities.supportsDetach`; TOB envs ignore = fail-safe). Like compact, this verb is LIVE-only
814
+ // (replica-local `steerableRuns` handle — the sync leg now registers too): a suspended run has no running tool
815
+ // and there is no durable "pending detach" → 409.
556
816
  const detachMatch = req.method === "POST" ? RUN_DETACH_RE.exec(url) : null;
557
817
  if (detachMatch) {
558
818
  if (rateLimited(req, res))
559
- return;
819
+ return; // mutating, but runs no model (no quota gate — parity with cancel, not steer)
560
820
  if (!deps.runStore) {
561
821
  sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
562
822
  return;
563
823
  }
564
- const principal = gatedPrincipal(req, deps.config);
824
+ const principal = gatedPrincipal(req, deps.config); // direct-door-secure identity, never the spoofable header
565
825
  if (deps.config.requirePrincipal && principal === undefined) {
566
826
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
567
827
  return;
@@ -576,6 +836,10 @@ async function handleRunsBody(req, res, url, ctx, miss) {
576
836
  return;
577
837
  }
578
838
  if (typeof body.toolCallId !== "string" || body.toolCallId.length === 0 || body.toolCallId.length > 256) {
839
+ // 🔒 The length cap is load-bearing (review MEDIUM): core's ToolDetachHub.request() is create-then-abort —
840
+ // an UNKNOWN id still allocates + retains a Map entry (keyed by the raw string) for the run's lifetime
841
+ // (gc only fires from a real tool's completion), so unbounded ids = per-run memory growth on a shared
842
+ // replica. Real core toolCallIds are short; 256 is generous.
579
843
  sendError(res, 400, "request.body_shape", "body must be { toolCallId: string (non-empty, ≤256 chars) }");
580
844
  return;
581
845
  }
@@ -584,27 +848,50 @@ async function handleRunsBody(req, res, url, ctx, miss) {
584
848
  sendError(res, 404, "not_found.run", "run not found");
585
849
  return;
586
850
  }
851
+ // Owner-gate (parity with steer/compact): an explicit operator may detach on any tenant's run; a normal
852
+ // caller only its own. 404 (not 403) for a non-owner — no existence oracle.
587
853
  const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
588
854
  if (!operator && run.owner !== null && run.owner !== principal) {
589
855
  sendError(res, 404, "not_found.run", "run not found");
590
856
  return;
591
857
  }
592
858
  if (!runSessionAcceptOk(req, res, run, "run.detach"))
593
- return;
859
+ return; // [1499] accept-phase
594
860
  const live = steerableRuns.get(taskId);
595
861
  if (!live) {
596
862
  sendError(res, 409, "detach.not_running", run.status === "running" ? "run is active on another replica — detach is replica-local" : `run is ${run.status} — no running tool call to detach`);
597
863
  return;
598
864
  }
865
+ // Fire-and-forget by CONTRACT (`detach(toolCallId): void`) — race-safety is core's; the outcome surfaces on
866
+ // the run's OWN stream (the early-settled tool_end with the `detached:true` structured card, then the b* task
867
+ // notification), which the client already consumes. 202 = "requested", honest about no confirmation here.
599
868
  live.detach(body.toolCallId);
600
869
  sendJson(res, 202, { taskId, toolCallId: body.toolCallId, delivery: "requested", note: "if the tool call is running in a detach-capable env it settles early with 'moved to background; task_id=b*'; otherwise the request is a no-op (fail-safe)" });
601
870
  return;
602
871
  }
603
872
  miss.fell = true;
604
873
  }
874
+ /** 同上纪律(子代理 / 任务句柄 / HITL 应答段)。 */
605
875
  async function handleRunVerbsBody(req, res, url, ctx, miss) {
606
876
  const { deps } = ctx;
607
877
  const { readJson, rateLimited, quotaExceeded, leaseDenied, safeDecode, runSessionAcceptOk } = ctx.helpers;
878
+ // C2 (core 1.219) + design/122 (core 1.225): the Task-SUB-AGENT verbs of a run —
879
+ // POST /v1/runs/:runId/subagents/:target/steer — inject fenced guidance into a STILL-RUNNING child;
880
+ // POST /v1/runs/:runId/subagents/:target/resume — REVIVE a SETTLED child with a new prompt (CC dfe parity;
881
+ // always async: the revived child runs in the background on its retained session, completion announced
882
+ // via the deployment notify sink — requires the parent run to have set `retainSubagentSessions`).
883
+ // Shared plumbing (one trust gate): addressed by the PARENT run's taskId + the child's parentToolCallId (or
884
+ // agentName when unambiguous); replica-local handle registry (the legs' `onSubagentSpawn` sink; design/122 ③
885
+ // handles now live until LEG END, not child settle — resume is only legal after settle); owner-gated via the
886
+ // parent run row; content through `redactSteerIn`. 🔐 `handle.childSessionId` is a continuation capability
887
+ // — never in any response. Sync delegations only (core r1-M3: bg children have no handle).
888
+ // [1488]③(b): GET /v1/runs/:runId/subagents/:handle/output — the BACKGROUND child read face. The shell's
889
+ // fleet viewer gets bg_notification summaries only; the child's FINAL assistant body lives in the core
890
+ // TaskRegistry (what the TaskOutput tool reads). Auth = the runs-face read pattern (verified principal →
891
+ // owner-gate via the parent run row → honest 404, no existence oracle); access into the registry is the
892
+ // REGISTRATION tuple derived from the run row (owner=runId | sessionScoped via sessionId, scope=run
893
+ // owner ?? "default") — NEVER caller-supplied. Replica-local (the registry is in-process, like steer);
894
+ // non-blocking read: a still-running child returns its current status honestly, no long-poll (v1).
608
895
  const subOutputMatch = req.method === "GET" ? RUN_SUBAGENT_OUTPUT_RE.exec(url) : null;
609
896
  if (subOutputMatch) {
610
897
  if (!deps.runStore || !deps.subagentTaskOutput) {
@@ -628,6 +915,22 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
628
915
  sendError(res, 404, "not_found.run", "run not found");
629
916
  return;
630
917
  }
918
+ // [1493] SESSION-scoped content read. principal answers "is this your data at all" (whose); session
919
+ // answers "which conversation" (which). This verb returns CONVERSATION CONTENT (a child's final body), so
920
+ // the principal owner-gate above is necessary but NOT sufficient: under a SHARED principal (single-user
921
+ // multi-shell — the default local posture, and any tenant that shares an identity) it passes for every
922
+ // co-principal shell, so a SIBLING session could read another conversation's child (clay's dual-shell
923
+ // repro). The run belongs to a conversation (run.sessionId); a non-operator caller must PROVE it owns that
924
+ // conversation by asserting the matching `?session=`. Threat model is ACCIDENTAL cross-read within one
925
+ // user's own shells (no adversary — the shell honestly reports its own session), so an honest
926
+ // caller-asserted session is correct AND sufficient here (no unguessable capability needed). fable5 R1
927
+ // (deliberate boundary, do not "harden" this into a capability): yes, a co-principal caller COULD forge the
928
+ // target's session — but co-principal means the SAME authz identity, i.e. the same user reading their own
929
+ // data through another window; session is a content-ROUTING axis inside one principal, not an authz wall.
930
+ // Deployments with genuinely different humans MUST give them distinct principals (requirePrincipal) — the
931
+ // principal owner-gate above is the security boundary, and it runs first. An
932
+ // operator/trace caller bypasses (as it does the owner-gate). A run with no sessionId is not
933
+ // conversation-bound → principal gate only. Fail-CLOSED: a session-bound run with no/mismatched assertion → 404.
631
934
  const callerSession = new URL(req.url ?? "", "http://x").searchParams.get("session");
632
935
  if (!trusted && run.sessionId && callerSession !== run.sessionId) {
633
936
  sendError(res, 404, "not_found.run", "run not found");
@@ -635,13 +938,28 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
635
938
  }
636
939
  const out = await deps.subagentTaskOutput(target, { owner: runId, scope: run.owner ?? "default", ...(run.sessionId ? { sessionId: run.sessionId } : {}) });
637
940
  const details = out.details;
941
+ // codex R1 (type gate): pollTask is the GENERIC registry face — same-tuple b*/m*/w* handles (bash
942
+ // stdout, monitor batches, workflow projections) would ride this verb otherwise, a broader egress
943
+ // than the documented agent-final-output contract. Only `background_agent` passes; everything else
944
+ // gets the SAME indistinguishable 404 (unknown / not-yours / wrong-kind — no oracle). wa* workflow-
945
+ // agent rows are OBSERVATION identities (never in the registry) — their read face is the workflow
946
+ // journal (GET /v1/workflows/:id/journal), and they land in the not_found arm here by construction.
638
947
  if (details?.error === "not_found" || details?.type !== "background_agent") {
639
948
  sendError(res, 404, "not_found.subagent", `no background agent "${target}" under this run (unknown handle, not this run's child, or already reaped — bg children live in the replica-local registry for the parent's lifetime; wa… workflow-agent rows are read via the workflow journal, not this verb)`);
640
949
  return;
641
950
  }
951
+ // Pass the registry's honest projection through: status/retrieval_status/partial flags verbatim,
952
+ // `content` = the TaskOutput-tool text (the child's final assistant body once terminal). UNTRUSTED
953
+ // model output, same posture as the runs face's `result`.
642
954
  sendJson(res, 200, { taskId: runId, target, content: out.content, output: out.details });
643
955
  return;
644
956
  }
957
+ // S2([1520]{core} 提货,core 1.370 `bgAgentId`):GET /v1/runs/:id/subagents/:handle/stream —— per-agent
958
+ // **live tail**(SSE)。「replay+tail」的 tail 半场:replay/终报=上面的 output 面(durable 臂 1.250),
959
+ // 本面=从连接时刻起的 content 帧(text/reasoning delta、tool_start/end、task_progress——投影与 sync 主流
960
+ // forward 分支同 builder 同纪律,subagent-tail-bus.ts 顶注)。帧只在宿主 run 所在副本产生 ⇒ live 帧
961
+ // replica-local(meta 如实声明;他实例 running 的行=只 heartbeat,不虚构)。门=output 面逐字同形
962
+ // (principal→owner→session fail-closed;a* 形判;探测经同 seam,未知/非本 run/型错=同 404 无 oracle)。
645
963
  const subStreamMatch = req.method === "GET" ? RUN_SUBAGENT_STREAM_RE.exec(url) : null;
646
964
  if (subStreamMatch) {
647
965
  if (!deps.runStore || !deps.subagentTaskOutput) {
@@ -665,12 +983,19 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
665
983
  sendError(res, 404, "not_found.run", "run not found");
666
984
  return;
667
985
  }
986
+ // [1493] session 门(output 面同注,出生即强制——新面零消费者无软拍需要)。
668
987
  const callerSession = new URL(req.url ?? "", "http://x").searchParams.get("session");
669
988
  if (!trusted && run.sessionId && callerSession !== run.sessionId) {
670
989
  sendError(res, 404, "not_found.run", "run not found");
671
990
  return;
672
991
  }
992
+ // 🔴 cli[1524] HIGH-2:**subscribe 先行,probe 在后**——原序(probe await→meta→subscribe)在
993
+ // probe 与 subscribe 之间留竞态窗:窗内子代 settle → task_settled 发布时无订阅者被丢(总线无
994
+ // replay/sticky)→ 订阅者只收 heartbeat 永挂。先建订阅,窗内 settle 自然落进队列;probe 判 404
995
+ // 时 it.return() 清订阅(未写任何帧,404 照常)。
673
996
  const it = defaultSubagentTailBus.subscribe(target);
997
+ // 归属+型探测经 output 同 seam(含 1.250 durable 臂——跨实例行也探得到 status);not_found/型错=
998
+ // 同一不可区分 404(无 oracle)。探测是纯读(a* 形在 seam 内先过形判门;background_agent poll 无副作用)。
674
999
  const probe = await deps.subagentTaskOutput(target, { owner: runId, scope: run.owner ?? "default", ...(run.sessionId ? { sessionId: run.sessionId } : {}) });
675
1000
  const probeDetails = probe.details;
676
1001
  if (probeDetails?.error === "not_found" || probeDetails?.type !== "background_agent") {
@@ -679,22 +1004,30 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
679
1004
  return;
680
1005
  }
681
1006
  sseHeaders(res);
1007
+ // meta 声明:live 帧 replica-local + replay 指引;status=探测快照(终态=不再有帧,发 meta 即收尾,
1008
+ // cli 拿到「走 replay 面」的信号而不是空挂)。seq=stop-cycle 世代号(settle 铸;core 1.377 起
1009
+ // **全域投影**——进程内 live 臂投当前复活周期号 [[1533] ask 兑现,[1548]①],durable 终态 serve
1010
+ // 同键同义;老 core 只在 durable 臂在场,缺省容忍)。1=spawn 周期,≥2=复活周期(retained revive
1011
+ // 或 tier-3 均 bump——codex 快审 #11 纠,勿窄读成仅 tier-3)。
682
1012
  res.write(`event: meta\ndata: ${JSON.stringify({ version: 1, runId, target, status: probeDetails.status ?? "running", ...(typeof probeDetails.seq === "number" ? { seq: probeDetails.seq } : {}), live: "replica-local", replayFace: "GET /v1/runs/:id/subagents/:handle/output" })}\n\n`);
1013
+ // core 1.382([1561] 提货单③):"parked" 非活非终态(durably suspended pending approval)——跟
1014
+ // running/pending 同归"还没完",tail 该继续等而不是收尾指去 replay 面(那是给真终态用的)。子代获批
1015
+ // 恢复后会在**同一条**活流上继续产帧,提前收尾会让客户端误判"已经结束",错过恢复后的后续内容。
683
1016
  if (probeDetails.status !== "running" && probeDetails.status !== "pending" && probeDetails.status !== "parked") {
684
1017
  void it.return?.();
685
- res.end();
1018
+ res.end(); // 终态:tail 无帧可等——replay 面才是读处(meta 已指路)
686
1019
  return;
687
1020
  }
688
1021
  let closed = false;
689
1022
  const hb = setInterval(() => {
690
1023
  if (!res.writableEnded)
691
- res.write(`event: heartbeat\ndata: {}\n\n`);
1024
+ res.write(`event: heartbeat\ndata: {}\n\n`); // workflow stream 同款真帧(BFF 丢注释帧案)
692
1025
  }, 15_000);
693
1026
  if (typeof hb.unref === "function")
694
1027
  hb.unref();
695
1028
  req.on("close", () => {
696
1029
  closed = true;
697
- void it.return?.();
1030
+ void it.return?.(); // parked next() 不观察 closed 旗——显式 return 解除 park + 清订阅(subscribeWorkflow 同案)
698
1031
  });
699
1032
  try {
700
1033
  for (;;) {
@@ -703,7 +1036,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
703
1036
  break;
704
1037
  res.write(`event: forward\ndata: ${JSON.stringify(n.value)}\n\n`);
705
1038
  if (n.value.type === "task_settled")
706
- break;
1039
+ break; // 复审 #3:终态帧即完整故事结束——收流,余下在 replay 面
707
1040
  }
708
1041
  }
709
1042
  finally {
@@ -714,6 +1047,16 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
714
1047
  }
715
1048
  return;
716
1049
  }
1050
+ // [1499] CC TaskOutput/TaskStop 人侧对位 — the GENERIC task-handle verbs. The narrow subagents/:h/output
1051
+ // face above stays byte-stable (公告过的 background_agent ONLY contract); THIS family serves the full
1052
+ // registry kind set the TaskOutput/TaskStop tools serve (background_bash stdout — NB whether a read
1053
+ // consumes the output cursor depends on the handle's shape: a spooled handle re-reads in full, a
1054
+ // cursor-only handle returns new bytes per read; the projection's own flags are authoritative; monitor
1055
+ // batches; background_agent final report), with `workflow` refused at the seam (the journal face owns
1056
+ // workflow reads; a poll here would fire onServedTerminal and suppress the completion push). Addressing
1057
+ // is EXACT task_id only — the tools' agent-name / legacy-shellId resolution is deliberately not on the
1058
+ // wire (fail-closed 404). Gates mirror subagentOutput: verified principal → owner via the run row →
1059
+ // SESSION (enforced from birth — new face, zero consumers, no soft phase needed).
717
1060
  const taskVerbMatch = req.method === "GET" ? RUN_TASK_OUTPUT_RE.exec(url) : req.method === "POST" ? RUN_TASK_STOP_RE.exec(url) : null;
718
1061
  if (taskVerbMatch) {
719
1062
  const stopVerb = req.method === "POST";
@@ -722,7 +1065,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
722
1065
  return;
723
1066
  }
724
1067
  if (stopVerb && rateLimited(req, res))
725
- return;
1068
+ return; // mutating, runs no model (parity with detach — rateLimited only, no quota/lease gate)
726
1069
  const principal = gatedPrincipal(req, deps.config);
727
1070
  if (deps.config.requirePrincipal && principal === undefined) {
728
1071
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
@@ -740,12 +1083,25 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
740
1083
  sendError(res, 404, "not_found.run", "run not found");
741
1084
  return;
742
1085
  }
1086
+ // SESSION-scoped content/action face ([1493] model): enforced fail-closed — same boundary rationale as
1087
+ // subagentOutput (principal = whose data, session = which conversation; honest caller-asserted session is
1088
+ // correct AND sufficient inside one principal; operators bypass; a session-unbound run is principal-only).
743
1089
  const q = new URL(req.url ?? "", "http://x").searchParams;
744
1090
  if (!trusted && run.sessionId && q.get("session") !== run.sessionId) {
745
1091
  sendError(res, 404, "not_found.run", "run not found");
746
1092
  return;
747
1093
  }
748
1094
  const access = { owner: runId, scope: run.owner ?? "default", ...(run.sessionId ? { sessionId: run.sessionId } : {}) };
1095
+ // ?filter= is REFUSED on BOTH verbs, not ignored (fail-loud 400; the face shipped unreleased, zero
1096
+ // consumers — and codex R2: a GET-only refusal would leave the stop verb silently accepting it). The
1097
+ // tools' filter is a caller-supplied RegExp run line-by-line on the replica's shared event loop — over
1098
+ // the wire that lets one request stall the whole replica for every tenant (pathological patterns on
1099
+ // long output lines take unbounded time in a backtracking engine). Client-side filtering of the fetched
1100
+ // output is the alternative, with an honest limit (codex R2): the wire serves the registry's CLIPPED
1101
+ // projection (core filters BEFORE clipping; a client can only filter after) — matches inside a clipped
1102
+ // middle are not recoverable over the wire, and a cursor-only bash leg consumes what it serves.
1103
+ // Full-fidelity filtered reads stay an in-engine tool capability; if a linear-time engine lands in
1104
+ // core, the wire filter can reopen deliberately.
749
1105
  if (q.get("filter") !== null) {
750
1106
  sendError(res, 400, "request.param_unsupported", "filter is not accepted on the wire — fetch the output and filter client-side (note: the wire serves the clipped projection)");
751
1107
  return;
@@ -758,10 +1114,28 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
758
1114
  sendError(res, 404, "not_found.task_handle", `no background task "${target}" under this run (unknown handle, not this run's task, already reaped — handles live in the replica-local registry for the parent's lifetime — or a workflow handle: workflow rows read via GET /v1/workflows/:id/journal; stopping a workflow is not on this wire)`);
759
1115
  return;
760
1116
  }
1117
+ // [1499] codex R3: a stop whose kill did NOT land must not read as success. Core keeps the handle
1118
+ // honest (status stays "running", error = the env failure code) — surface that as a 409 so a
1119
+ // status-driven client never mistakes a failed stop for a dead process. The projection rides along
1120
+ // verbatim for diagnosis. NB the "user" attribution pre-mark stays pending on this arm (first-write-
1121
+ // wins, no public per-handle clear) — if the task later dies of another cause it reads stoppedBy:
1122
+ // "user"; the atomic fix is a `source` option on core's stopTask (asked on the board), same residue
1123
+ // shape as core's own internal "parent" pre-mark.
1124
+ // core 1.382([1561] 提货单③):parked 行的两个新 stop 结局——`parked_pending_approval`(stop 撞见
1125
+ // 一个仍在 parked 的行,没有活跃可 kill 的进程)与 `park_resume_won`(stop 与"审批已落地→恢复"发生
1126
+ // 竞态,恢复那侧赢了,行不再是 parked,但 status 不一定翻回 "running"——error 码本身才是权威判据,
1127
+ // 不能靠 status 反推)。二者都不该读成 200 成功("kill 没落地"同一条纪律的延伸),各给各的
1128
+ // errorCode(同 409 家族,additive,与既有 not_local/not_landed 同姿势)。
1129
+ // [1596]/[1600] core 1.397 三分:仲裁器**抛错**(店不可达)此前被误吞成 park_resume_won 的假话——
1130
+ // 「输给了并发 resume」与「根本问不到仲裁结果」是两种不同的行动建议(前者重查状态,后者原样重试
1131
+ // stop),新码 `park_arbiter_unreachable` 让二者不再共用一个 errorCode。
761
1132
  const parkArbiterUnreachable = details?.error === "park_arbiter_unreachable";
762
1133
  const parkResumeWon = details?.error === "park_resume_won";
763
1134
  const stillParked = details?.error === "parked_pending_approval" || details?.status === "parked";
764
1135
  if (stopVerb && details?.error !== undefined && (details.status === "running" || stillParked || parkResumeWon || parkArbiterUnreachable)) {
1136
+ // 1.250:durable 回落臂(core stopTask agentStore 分支)对他实例 running 行应答 error="not_local"
1137
+ // (没有 kill 被尝试)——与「kill 尝试了没落地」(stop.not_landed)是不同的失败形,各给各的
1138
+ // errorCode(同 409 家族,additive)。
765
1139
  const notLocal = details.error === "not_local";
766
1140
  sendError(res, 409, notLocal
767
1141
  ? "stop.not_local"
@@ -782,6 +1156,14 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
782
1156
  : `stop did not land (${details.error}) — the process may still be running`, { taskId: runId, target, content: out.content, output: out.details });
783
1157
  return;
784
1158
  }
1159
+ // Registry projection passed through verbatim (status/retrieval_status/cursor flags); `content` is
1160
+ // UNTRUSTED tool/model output, same posture as subagentOutput.
1161
+ // G14([1833]/[1840]§五):顶层铸 `cursorSemantics` —— 此前「这次读是否消费了游标」只能从 content
1162
+ // 文本标记(`--- stdout (full, re-readable) ---` vs `--- new stdout ---`)反解,每个消费端都得抄
1163
+ // 字符串判别。判据(core task-registry dist 亲读):游标消费形(非 spool 的 pollBackground 路径)
1164
+ // 的内层 details.bytesDroppedBeforeCursor **恒在**(0 也发);spool bash / monitor / background_agent
1165
+ // 全是重读安全的 full 形。⚠️ 这仍是对 core 内形的一处嗅探——集中在 server 这一处替所有端挑担;
1166
+ // 已在黑板请 core 出一等 flag,到货换源、wire 键不变。error/not_ready 形不铸(没有输出体,别给假语义)。
785
1167
  const g14 = (() => {
786
1168
  const d = out.details;
787
1169
  if (!d || d.error !== undefined || d.retrieval_status === "not_ready")
@@ -790,7 +1172,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
790
1172
  return { cursorSemantics: d.details && "bytesDroppedBeforeCursor" in d.details ? "cursor" : "full" };
791
1173
  if (d.type === "monitor" || d.type === "background_agent")
792
1174
  return { cursorSemantics: "full" };
793
- return {};
1175
+ return {}; // 未知 kind:不猜
794
1176
  })();
795
1177
  sendJson(res, 200, { taskId: runId, target, content: out.content, output: out.details, ...g14 });
796
1178
  return;
@@ -799,11 +1181,13 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
799
1181
  if (subVerbMatch) {
800
1182
  const verb = url.endsWith("/resume") ? "resume" : "steer";
801
1183
  if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
802
- return;
1184
+ return; // mutating + drives a model agent
803
1185
  if (!deps.runStore) {
804
1186
  sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
805
1187
  return;
806
1188
  }
1189
+ // Identity from gatedPrincipal (direct-door-secure single point), NEVER the spoofable header — `trusted`
1190
+ // carries operator authority (parity with RUN steer / workflow-agent steer; HANDOFF 铁律 + core §3 inv #1).
807
1191
  const principal = gatedPrincipal(req, deps.config);
808
1192
  if (deps.config.requirePrincipal && principal === undefined) {
809
1193
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
@@ -827,19 +1211,28 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
827
1211
  sendError(res, 400, "request.body_shape", "body must be { content: string (non-empty) }");
828
1212
  return;
829
1213
  }
1214
+ // 入参上限(见 `STEER_IN_MAX_REQUEST_CHARS` 旁注):脱敏门是**同步**的,烧的是事件循环 ⇒
1215
+ // 一次超大 steer 卡住的是**整个副本**。**拒**而不是服务端先截 —— 先截会让引擎的
1216
+ // `[+N chars]` 披露低报,而"诚实标记不得低报"是本仓另一条钉守着的性质。
830
1217
  if (body.content.length > STEER_IN_MAX_REQUEST_CHARS) {
831
1218
  sendError(res, 413, "steer.content_too_large", `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`);
832
1219
  return;
833
1220
  }
1221
+ // trusted = explicit operator (NOT bare isOperator); a normal caller may drive only their OWN run's children.
834
1222
  const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
835
1223
  const run = await deps.runStore.getRun(runId);
1224
+ // Owner-gate via the parent run row (the child inherits the parent's tenancy — core threads the parent's
1225
+ // principal to the child, design/62). 404 (not 403) for a non-owner — no existence oracle.
836
1226
  if (!run || (!trusted && run.owner !== null && run.owner !== principal)) {
837
1227
  sendError(res, 404, "not_found.run", "run not found");
838
1228
  return;
839
1229
  }
840
1230
  if (!runSessionAcceptOk(req, res, run, `subagent.${verb}`))
841
- return;
1231
+ return; // [1499] accept-phase (child write verbs, same class)
842
1232
  const send409 = (errorCode, error) => sendError(res, 409, errorCode, error);
1233
+ // STEER-IN REDACTION GATE — same trust gate as the workflow-agent route (one owner: redactSteerIn): the
1234
+ // human content is UNTRUSTED (host-leak/size/fence) BEFORE it reaches the child; core's handle.steer/
1235
+ // resume fence AGAIN inside their trusted marker framing (defense in depth).
843
1236
  const redacted = redactSteerIn(body.content, target);
844
1237
  const resolution = deps.subagentSteerRegistry?.resolve(runId, target);
845
1238
  if (resolution && resolution.count > 1) {
@@ -850,6 +1243,8 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
850
1243
  if (handle) {
851
1244
  try {
852
1245
  if (verb === "resume") {
1246
+ // design/122: `handle.resume` is absent when the parent run did not retain child sessions — the
1247
+ // same condition core's own rejection names; report it with core's code so the shell renders one story.
853
1248
  if (!handle.resume) {
854
1249
  send409("resume.retain_off", "the parent run did not retain sub-agent sessions (set retainSubagentSessions on the run to enable revival)");
855
1250
  return;
@@ -859,11 +1254,14 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
859
1254
  return;
860
1255
  }
861
1256
  const marker = await handle.steer(redacted);
1257
+ // Receipt wording aligned with CC ("Message queued for delivery…").
862
1258
  sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", marker, note: `Message queued for delivery to ${handle.agentName ?? target} at its next tool round.` });
863
1259
  return;
864
1260
  }
865
1261
  catch (e) {
866
1262
  const code = e.code;
1263
+ // core's typed rejections → honest 409s with the core code verbatim (steer: not_running only;
1264
+ // resume: still_running / retain_off / evicted / cap / session_not_found — design/122 D2 contract).
867
1265
  if (code === "steering.not_running") {
868
1266
  send409(code, "sub-agent just finished — no longer accepting steers");
869
1267
  return;
@@ -876,22 +1274,32 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
876
1274
  send409(code, e instanceof Error ? e.message : "resume rejected");
877
1275
  return;
878
1276
  }
879
- throw e;
1277
+ throw e; // unexpected → outer handler 500
880
1278
  }
881
1279
  }
1280
+ // No handle here. Either the parent run is elsewhere/terminal, the child was never registered (bg
1281
+ // delegation — no handle by core's r1-M3 scope), or the owning leg already tore down. Honest 409.
882
1282
  send409("steering.not_running", run.status === "running"
883
1283
  ? "no sub-agent matches on this replica (it runs backgrounded, or the run is on another replica)"
884
1284
  : `run is ${run.status} — its sub-agents are no longer addressable`);
885
1285
  return;
886
1286
  }
1287
+ // E23 (shell-host contract): POST /v1/elicitations/:id/respond — the shell answers an inbound MCP elicitation. LIVE-ONLY +
1288
+ // same-replica (the parked promise is in-memory, like a live steer): the coordinator owner-gates (404 — no
1289
+ // existence oracle), validates the body (= core's McpElicitResponse), and resolves the promise so onElicit returns
1290
+ // to core, which replies to the MCP server on the still-open connection. A respond on the wrong replica / after
1291
+ // answer/TTL finds nothing → 404.
887
1292
  const elicitMatch = req.method === "POST" ? ELICIT_RESPOND_RE.exec(url) : null;
888
1293
  if (elicitMatch) {
889
1294
  if (rateLimited(req, res))
890
- return;
1295
+ return; // mutating, but only resolves an in-flight promise (no new billable work → no quota gate)
891
1296
  if (!deps.elicitation) {
892
1297
  sendError(res, 501, "feature.elicitation_disabled", "inbound MCP elicitation is not enabled on this worker (MCP_ELICITATION_ENABLED)");
893
1298
  return;
894
1299
  }
1300
+ // Identity from gatedPrincipal (direct-door-secure single point), NEVER the spoofable header — the answer is
1301
+ // owner-gated against the run's owner (the END USER the elicitation was routed to). No operator bypass: an
1302
+ // elicitation form is the user's to fill, not a supervisor's (unlike steer).
895
1303
  const principal = gatedPrincipal(req, deps.config);
896
1304
  if (deps.config.requirePrincipal && principal === undefined) {
897
1305
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
@@ -910,14 +1318,21 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
910
1318
  sendJson(res, status, respBody);
911
1319
  return;
912
1320
  }
1321
+ // §4④: POST /v1/questions/:id/respond — the shell answers the agent's AskUserQuestion. LIVE-ONLY +
1322
+ // same-replica (parked promise in-memory), the sibling of the elicit respond: owner-gated (404, no existence
1323
+ // oracle), body validated (= core's QuestionAnswer), resolves the promise so onQuestion returns to core and the
1324
+ // model continues with "The user answered: …". The security ANSWER-fence (selected ⊆ options, note untrusted-fenced)
1325
+ // is core's AskUserQuestion tool, not here. A respond on the wrong replica / after answer/TTL finds nothing → 404.
913
1326
  const questionMatch = req.method === "POST" ? QUESTION_RESPOND_RE.exec(url) : null;
914
1327
  if (questionMatch) {
915
1328
  if (rateLimited(req, res))
916
- return;
1329
+ return; // resolves an in-flight promise (no new billable work → no quota gate)
917
1330
  if (!deps.question) {
918
1331
  sendError(res, 501, "feature.ask_question_disabled", "AskUserQuestion live HITL is not enabled on this worker (ASK_QUESTION_ENABLED)");
919
1332
  return;
920
1333
  }
1334
+ // Same owner-gate identity as elicit: the VERIFIED principal (gatedPrincipal), never the spoofable header. The
1335
+ // question is the END USER's to answer (routed to their stream) — no operator bypass.
921
1336
  const principal = gatedPrincipal(req, deps.config);
922
1337
  if (deps.config.requirePrincipal && principal === undefined) {
923
1338
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
@@ -936,14 +1351,22 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
936
1351
  sendJson(res, status, respBody);
937
1352
  return;
938
1353
  }
1354
+ // [816]/[820]②: POST /v1/tool-approvals/:id/respond — the shell answers a policy `ask` (the CC three-choice
1355
+ // card: {decision: "allow" | "allow_session" | "deny"}). LIVE-ONLY + same-replica (parked promise in-memory),
1356
+ // the sibling of the question respond: owner-gated (404, no existence oracle), body validated first (400 is
1357
+ // existence-independent), resolves the promise so core's resolveAsk returns allow/deny and the tool call
1358
+ // proceeds/denies. `allow_session` additionally arms the coordinator's per-session allow-all (fs-write family
1359
+ // only — rationale in tool-approval.ts). A respond on the wrong replica / after settle/TTL finds nothing → 404.
939
1360
  const approvalMatch = req.method === "POST" ? TOOL_APPROVAL_RESPOND_RE.exec(url) : null;
940
1361
  if (approvalMatch) {
941
1362
  if (rateLimited(req, res))
942
- return;
1363
+ return; // resolves an in-flight promise (no new billable work → no quota gate)
943
1364
  if (!deps.toolApproval) {
944
1365
  sendError(res, 501, "feature.tool_approval_disabled", "live tool-approval HITL is not enabled on this worker (TOOL_APPROVAL_ENABLED)");
945
1366
  return;
946
1367
  }
1368
+ // Same owner-gate identity as question/elicit: the VERIFIED principal (gatedPrincipal), never the spoofable
1369
+ // header. The approval is the END USER's to answer (routed to their stream) — no operator bypass.
947
1370
  const principal = gatedPrincipal(req, deps.config);
948
1371
  if (deps.config.requirePrincipal && principal === undefined) {
949
1372
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);