@sema-agent/server 1.323.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (539) hide show
  1. package/dist/approval-hmac.d.ts +40 -0
  2. package/dist/approval-hmac.js +67 -0
  3. package/dist/approval.d.ts +31 -0
  4. package/dist/approval.js +72 -4
  5. package/dist/audit.d.ts +26 -0
  6. package/dist/audit.js +40 -1
  7. package/dist/auth-bridge.d.ts +10 -0
  8. package/dist/auth-bridge.js +47 -4
  9. package/dist/auth-keys.d.ts +19 -0
  10. package/dist/auth-keys.js +13 -0
  11. package/dist/bake-runner/main.d.ts +5 -0
  12. package/dist/bake-runner/main.js +43 -4
  13. package/dist/bake-runner/protocol.d.ts +57 -0
  14. package/dist/bake-runner/protocol.js +58 -3
  15. package/dist/bake-runner/runner.d.ts +54 -0
  16. package/dist/bake-runner/runner.js +87 -5
  17. package/dist/bench/l8/artifact.d.ts +63 -0
  18. package/dist/bench/l8/artifact.js +61 -2
  19. package/dist/bench/l8/escape.d.ts +36 -0
  20. package/dist/bench/l8/escape.js +25 -0
  21. package/dist/bench/l8/index.d.ts +14 -0
  22. package/dist/bench/l8/index.js +14 -0
  23. package/dist/bench/l8/probes.d.ts +74 -0
  24. package/dist/bench/l8/probes.js +60 -0
  25. package/dist/bench/l8/run-probes.d.ts +43 -0
  26. package/dist/bench/l8/run-probes.js +45 -2
  27. package/dist/bench/s1/arms.d.ts +126 -0
  28. package/dist/bench/s1/arms.js +188 -13
  29. package/dist/bench/s1/live-deps.d.ts +66 -0
  30. package/dist/bench/s1/live-deps.js +249 -11
  31. package/dist/bench/s1/oracle.d.ts +49 -0
  32. package/dist/bench/s1/oracle.js +36 -1
  33. package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
  34. package/dist/bench/s1/repair-oracle-adapter.js +30 -0
  35. package/dist/bench/s1/reviewer.d.ts +38 -0
  36. package/dist/bench/s1/reviewer.js +34 -0
  37. package/dist/bench/s1/row.d.ts +77 -0
  38. package/dist/bench/s1/row.js +62 -0
  39. package/dist/bench/s1/run-firm.d.ts +47 -0
  40. package/dist/bench/s1/run-firm.js +70 -4
  41. package/dist/bench/s1/runner-ctx.d.ts +71 -0
  42. package/dist/bench/s1/runner-ctx.js +36 -0
  43. package/dist/bench/s1/tasks.d.ts +108 -0
  44. package/dist/bench/s1/tasks.js +115 -0
  45. package/dist/boot-reclaim.d.ts +19 -0
  46. package/dist/boot-reclaim.js +20 -0
  47. package/dist/brain.d.ts +25 -0
  48. package/dist/brain.js +61 -1
  49. package/dist/budget.d.ts +66 -0
  50. package/dist/budget.js +128 -4
  51. package/dist/capabilities/builtin-tools.d.ts +3 -0
  52. package/dist/capabilities/builtin-tools.js +3 -0
  53. package/dist/capabilities/center-plugins.d.ts +17 -1
  54. package/dist/capabilities/center-plugins.js +45 -4
  55. package/dist/capabilities/center-prompts.d.ts +46 -0
  56. package/dist/capabilities/center-prompts.js +57 -3
  57. package/dist/capabilities/code-review-council.d.ts +28 -0
  58. package/dist/capabilities/code-review-council.js +40 -7
  59. package/dist/capabilities/collab-wire.d.ts +2 -0
  60. package/dist/capabilities/collab-workflows.d.ts +7 -0
  61. package/dist/capabilities/collab-workflows.js +44 -1
  62. package/dist/capabilities/oa-tools.d.ts +6 -0
  63. package/dist/capabilities/oa-tools.js +10 -0
  64. package/dist/capabilities/prompt.d.ts +14 -0
  65. package/dist/capabilities/prompt.js +14 -0
  66. package/dist/capabilities/prompts/code-review.d.ts +14 -0
  67. package/dist/capabilities/prompts/code-review.js +14 -0
  68. package/dist/capabilities/prompts/identity.d.ts +6 -0
  69. package/dist/capabilities/prompts/identity.js +6 -0
  70. package/dist/capabilities/prompts/team.d.ts +4 -0
  71. package/dist/capabilities/prompts/team.js +4 -0
  72. package/dist/capabilities/repo-tools.d.ts +11 -0
  73. package/dist/capabilities/repo-tools.js +8 -0
  74. package/dist/capabilities/sandbox-file-send.d.ts +95 -0
  75. package/dist/capabilities/sandbox-file-send.js +91 -3
  76. package/dist/capabilities/scenarios.d.ts +130 -0
  77. package/dist/capabilities/scenarios.js +180 -7
  78. package/dist/capabilities/select-environment-tool.d.ts +7 -0
  79. package/dist/capabilities/select-environment-tool.js +30 -0
  80. package/dist/capabilities/send-user-file-tool.d.ts +33 -0
  81. package/dist/capabilities/send-user-file-tool.js +65 -1
  82. package/dist/capabilities/skills.d.ts +29 -0
  83. package/dist/capabilities/skills.js +13 -2
  84. package/dist/capabilities/team.d.ts +22 -0
  85. package/dist/capabilities/team.js +22 -2
  86. package/dist/capabilities/tool-defer.d.ts +26 -0
  87. package/dist/capabilities/tool-defer.js +3 -0
  88. package/dist/config-center/apply-effective.d.ts +65 -0
  89. package/dist/config-center/apply-effective.js +204 -12
  90. package/dist/config-center/http-client.d.ts +34 -0
  91. package/dist/config-center/http-client.js +68 -0
  92. package/dist/config-center/restart-signal.d.ts +22 -0
  93. package/dist/config-center/restart-signal.js +39 -0
  94. package/dist/config-center/skills-mcp.d.ts +20 -1
  95. package/dist/config-center/skills-mcp.js +42 -7
  96. package/dist/config-center/types.d.ts +92 -1
  97. package/dist/config-lkg.d.ts +12 -0
  98. package/dist/config-lkg.js +41 -0
  99. package/dist/config-provider.d.ts +78 -0
  100. package/dist/config-provider.js +165 -2
  101. package/dist/config-types.d.ts +546 -0
  102. package/dist/config.d.ts +64 -0
  103. package/dist/config.js +455 -60
  104. package/dist/degenerate-instrument.d.ts +38 -0
  105. package/dist/degenerate-instrument.js +55 -3
  106. package/dist/elicitation.d.ts +39 -0
  107. package/dist/elicitation.js +87 -3
  108. package/dist/env-facts.d.ts +61 -0
  109. package/dist/env-facts.js +73 -7
  110. package/dist/fleet/fleet-bus.d.ts +185 -0
  111. package/dist/fleet/fleet-bus.js +333 -27
  112. package/dist/fleet/subagent-tail-bus.d.ts +11 -0
  113. package/dist/fleet/subagent-tail-bus.js +33 -2
  114. package/dist/fleet-client.d.ts +38 -0
  115. package/dist/fleet-client.js +44 -9
  116. package/dist/fleet-lease.d.ts +46 -0
  117. package/dist/fleet-lease.js +73 -5
  118. package/dist/hooks/branch-transcript.d.ts +8 -0
  119. package/dist/hooks/branch-transcript.js +33 -0
  120. package/dist/hooks/cc-stop-prompt.d.ts +42 -0
  121. package/dist/hooks/cc-stop-prompt.js +43 -1
  122. package/dist/hooks/hook-llm.d.ts +15 -0
  123. package/dist/hooks/hook-llm.js +47 -0
  124. package/dist/hooks/hook-runner.d.ts +89 -0
  125. package/dist/hooks/hook-runner.js +327 -18
  126. package/dist/http/idempotency.d.ts +31 -0
  127. package/dist/http/idempotency.js +34 -0
  128. package/dist/http/principal-gate.d.ts +17 -0
  129. package/dist/http/principal-gate.js +35 -3
  130. package/dist/http/route-ctx.d.ts +36 -0
  131. package/dist/http/routes/approvals-assistant.d.ts +25 -0
  132. package/dist/http/routes/approvals-assistant.js +253 -23
  133. package/dist/http/routes/attachments.js +22 -4
  134. package/dist/http/routes/capabilities.d.ts +7 -0
  135. package/dist/http/routes/capabilities.js +202 -1
  136. package/dist/http/routes/fleet.d.ts +6 -0
  137. package/dist/http/routes/fleet.js +94 -5
  138. package/dist/http/routes/images.d.ts +17 -0
  139. package/dist/http/routes/images.js +222 -14
  140. package/dist/http/routes/leader.d.ts +7 -0
  141. package/dist/http/routes/leader.js +10 -0
  142. package/dist/http/routes/memory-policy.d.ts +6 -0
  143. package/dist/http/routes/memory-policy.js +38 -6
  144. package/dist/http/routes/notify-wake.d.ts +10 -0
  145. package/dist/http/routes/notify-wake.js +19 -3
  146. package/dist/http/routes/observability.d.ts +6 -0
  147. package/dist/http/routes/observability.js +19 -2
  148. package/dist/http/routes/runs.d.ts +15 -0
  149. package/dist/http/routes/runs.js +464 -41
  150. package/dist/http/routes/session-sync.d.ts +15 -0
  151. package/dist/http/routes/session-sync.js +229 -19
  152. package/dist/http/routes/sessions-list.d.ts +6 -0
  153. package/dist/http/routes/sessions-list.js +35 -4
  154. package/dist/http/routes/sessions.d.ts +14 -0
  155. package/dist/http/routes/sessions.js +176 -31
  156. package/dist/http/routes/side-query.d.ts +8 -0
  157. package/dist/http/routes/side-query.js +26 -1
  158. package/dist/http/routes/tasks.d.ts +18 -0
  159. package/dist/http/routes/tasks.js +501 -38
  160. package/dist/http/routes/trace-usage.d.ts +10 -0
  161. package/dist/http/routes/trace-usage.js +66 -14
  162. package/dist/http/routes/workflows.d.ts +12 -0
  163. package/dist/http/routes/workflows.js +180 -19
  164. package/dist/http/run-meta.d.ts +11 -0
  165. package/dist/http/run-meta.js +6 -0
  166. package/dist/http/send.d.ts +19 -0
  167. package/dist/http/send.js +11 -0
  168. package/dist/http/server.d.ts +255 -0
  169. package/dist/http/server.js +921 -75
  170. package/dist/http/sse-log.d.ts +15 -0
  171. package/dist/http/sse-log.js +16 -0
  172. package/dist/http/tar.d.ts +9 -0
  173. package/dist/http/tar.js +21 -5
  174. package/dist/http/wire-gate.d.ts +9 -0
  175. package/dist/http/wire-gate.js +9 -0
  176. package/dist/http/wire-types.d.ts +187 -0
  177. package/dist/http/workspace-content.d.ts +8 -0
  178. package/dist/http/workspace-content.js +10 -0
  179. package/dist/images/bake-validate.d.ts +63 -0
  180. package/dist/images/bake-validate.js +71 -1
  181. package/dist/images/manifest.d.ts +19 -0
  182. package/dist/images/manifest.js +6 -1
  183. package/dist/index.d.ts +1 -5
  184. package/dist/index.js +10 -5
  185. package/dist/key-resolver.d.ts +23 -0
  186. package/dist/key-resolver.js +27 -2
  187. package/dist/leader/diffout.d.ts +23 -0
  188. package/dist/leader/diffout.js +8 -1
  189. package/dist/leader/diffup.d.ts +32 -0
  190. package/dist/leader/diffup.js +43 -0
  191. package/dist/leader/endpoint.d.ts +20 -0
  192. package/dist/leader/endpoint.js +25 -2
  193. package/dist/leader/fanout.d.ts +60 -0
  194. package/dist/leader/fanout.js +29 -3
  195. package/dist/leader/grader-env-factory.d.ts +74 -0
  196. package/dist/leader/grader-env-factory.js +62 -3
  197. package/dist/leader/leader.d.ts +97 -0
  198. package/dist/leader/leader.js +151 -4
  199. package/dist/leader/merge.d.ts +94 -0
  200. package/dist/leader/merge.js +112 -9
  201. package/dist/leader/planner.d.ts +52 -0
  202. package/dist/leader/planner.js +47 -3
  203. package/dist/leader/repair-oracle.d.ts +61 -0
  204. package/dist/leader/repair-oracle.js +59 -1
  205. package/dist/leader/repair-wire.d.ts +78 -0
  206. package/dist/leader/repair-wire.js +79 -2
  207. package/dist/leader/wire.d.ts +79 -0
  208. package/dist/leader/wire.js +255 -8
  209. package/dist/lsp/e2b-bridge.d.ts +16 -0
  210. package/dist/lsp/e2b-bridge.js +32 -4
  211. package/dist/lsp/e2b-manager.d.ts +10 -0
  212. package/dist/lsp/e2b-manager.js +18 -2
  213. package/dist/lsp/lsp-frames.d.ts +12 -0
  214. package/dist/lsp/lsp-frames.js +12 -0
  215. package/dist/lsp/manager.d.ts +12 -0
  216. package/dist/lsp/manager.js +35 -2
  217. package/dist/lsp/types.d.ts +10 -0
  218. package/dist/lsp/ws-transport.d.ts +12 -0
  219. package/dist/lsp/ws-transport.js +53 -5
  220. package/dist/lsp-evict.d.ts +14 -0
  221. package/dist/lsp-evict.js +15 -1
  222. package/dist/main.js +2132 -96
  223. package/dist/memory-export.d.ts +1 -0
  224. package/dist/memory-export.js +4 -0
  225. package/dist/memory-scope.d.ts +49 -0
  226. package/dist/memory-scope.js +87 -4
  227. package/dist/memory-sync-client.d.ts +24 -0
  228. package/dist/memory-sync-client.js +41 -2
  229. package/dist/memory-sync.d.ts +23 -0
  230. package/dist/memory-sync.js +70 -1
  231. package/dist/model-select.d.ts +39 -0
  232. package/dist/model-select.js +42 -1
  233. package/dist/observability/cost-quota.d.ts +20 -0
  234. package/dist/observability/cost-quota.js +4 -0
  235. package/dist/observability/cost-taxonomy.d.ts +45 -0
  236. package/dist/observability/cost-taxonomy.js +34 -0
  237. package/dist/observability/logger.d.ts +1 -0
  238. package/dist/observability/logger.js +6 -0
  239. package/dist/observability/metrics.d.ts +13 -0
  240. package/dist/observability/metrics.js +63 -0
  241. package/dist/observability/otel-exporter.d.ts +9 -0
  242. package/dist/observability/otel-exporter.js +11 -3
  243. package/dist/observability/principal-context.d.ts +9 -0
  244. package/dist/observability/principal-context.js +9 -0
  245. package/dist/observability/prompt-manifest.d.ts +37 -0
  246. package/dist/observability/prompt-manifest.js +35 -1
  247. package/dist/observability/rate-limit.d.ts +10 -0
  248. package/dist/observability/rate-limit.js +1 -0
  249. package/dist/observability/tool-trace.d.ts +24 -0
  250. package/dist/observability/tool-trace.js +41 -1
  251. package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
  252. package/dist/orchestration/hardened-vm-runner.js +119 -4
  253. package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
  254. package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
  255. package/dist/orchestration/hardened-vm-worker.js +27 -0
  256. package/dist/orchestration/subagent-steer.d.ts +44 -0
  257. package/dist/orchestration/subagent-steer.js +45 -1
  258. package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
  259. package/dist/orchestration/workflow-agent-steer.js +80 -1
  260. package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
  261. package/dist/orchestration/workflow-completion-inbox.js +283 -32
  262. package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
  263. package/dist/orchestration/workflow-notify-journal.js +187 -13
  264. package/dist/parked-decide.d.ts +61 -0
  265. package/dist/parked-decide.js +58 -1
  266. package/dist/per-task-image.d.ts +54 -0
  267. package/dist/per-task-image.js +41 -0
  268. package/dist/plan-cache-probe.d.ts +35 -0
  269. package/dist/plan-cache-probe.js +18 -3
  270. package/dist/plugins/approval-exemption-store.d.ts +7 -0
  271. package/dist/plugins/approval-exemption-store.js +28 -2
  272. package/dist/plugins/approval-store-sql.d.ts +64 -0
  273. package/dist/plugins/approval-store-sql.js +32 -0
  274. package/dist/plugins/background-agent-store-sql.d.ts +55 -0
  275. package/dist/plugins/background-agent-store-sql.js +91 -4
  276. package/dist/plugins/background-shell-support.d.ts +98 -0
  277. package/dist/plugins/background-shell-support.js +117 -13
  278. package/dist/plugins/blob-backend.d.ts +104 -0
  279. package/dist/plugins/blob-backend.js +163 -6
  280. package/dist/plugins/breaker-state-sql.d.ts +65 -1
  281. package/dist/plugins/breaker-state-sql.js +46 -8
  282. package/dist/plugins/caching-session-store.d.ts +42 -0
  283. package/dist/plugins/caching-session-store.js +63 -3
  284. package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
  285. package/dist/plugins/checkpoint-store-sql.js +239 -11
  286. package/dist/plugins/file-outcome-sink.d.ts +4 -0
  287. package/dist/plugins/file-outcome-sink.js +9 -0
  288. package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
  289. package/dist/plugins/file-resume-anchor-store.js +43 -4
  290. package/dist/plugins/file-run-store.d.ts +90 -0
  291. package/dist/plugins/file-run-store.js +145 -13
  292. package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
  293. package/dist/plugins/file-snapshot-store-sql.js +178 -10
  294. package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
  295. package/dist/plugins/file-workflow-journal-store.js +10 -0
  296. package/dist/plugins/fork-routing-session-store.d.ts +47 -0
  297. package/dist/plugins/fork-routing-session-store.js +51 -3
  298. package/dist/plugins/host-platform.d.ts +69 -0
  299. package/dist/plugins/host-platform.js +81 -2
  300. package/dist/plugins/image-bake-store-sql.d.ts +136 -1
  301. package/dist/plugins/image-bake-store-sql.js +241 -7
  302. package/dist/plugins/image-index-sql.d.ts +46 -0
  303. package/dist/plugins/image-index-sql.js +113 -4
  304. package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
  305. package/dist/plugins/k8s-bg-scripts.js +88 -4
  306. package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
  307. package/dist/plugins/k8s-exec-protocol.js +41 -0
  308. package/dist/plugins/local-checkpoint-store.d.ts +40 -0
  309. package/dist/plugins/local-checkpoint-store.js +86 -7
  310. package/dist/plugins/local-session-store.d.ts +157 -0
  311. package/dist/plugins/local-session-store.js +289 -34
  312. package/dist/plugins/local-task-attachment-store.js +14 -2
  313. package/dist/plugins/mailbox-store-sql.d.ts +38 -0
  314. package/dist/plugins/mailbox-store-sql.js +39 -6
  315. package/dist/plugins/memory-engine-pg.d.ts +30 -0
  316. package/dist/plugins/memory-engine-pg.js +122 -4
  317. package/dist/plugins/memory-engine-tidb.d.ts +26 -0
  318. package/dist/plugins/memory-engine-tidb.js +115 -5
  319. package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
  320. package/dist/plugins/memory-engine-vector-util.js +5 -0
  321. package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
  322. package/dist/plugins/memory-resume-anchor-store.js +17 -0
  323. package/dist/plugins/memory-run-store.d.ts +63 -0
  324. package/dist/plugins/memory-run-store.js +73 -8
  325. package/dist/plugins/memory-session-policy-store.d.ts +17 -0
  326. package/dist/plugins/memory-session-policy-store.js +17 -0
  327. package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
  328. package/dist/plugins/memory-sync-store-pg.js +45 -2
  329. package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
  330. package/dist/plugins/memory-sync-store-tidb.js +31 -1
  331. package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
  332. package/dist/plugins/outcome-ledger-sql.js +97 -3
  333. package/dist/plugins/pg-approval-store.d.ts +7 -0
  334. package/dist/plugins/pg-approval-store.js +7 -0
  335. package/dist/plugins/pg-breaker-state.d.ts +6 -0
  336. package/dist/plugins/pg-breaker-state.js +6 -0
  337. package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
  338. package/dist/plugins/pg-checkpoint-store.js +8 -0
  339. package/dist/plugins/pg-cost-quota.d.ts +17 -0
  340. package/dist/plugins/pg-cost-quota.js +6 -0
  341. package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
  342. package/dist/plugins/pg-file-snapshot-store.js +6 -0
  343. package/dist/plugins/pg-image-bake.d.ts +9 -0
  344. package/dist/plugins/pg-image-bake.js +9 -0
  345. package/dist/plugins/pg-image-index.d.ts +9 -0
  346. package/dist/plugins/pg-image-index.js +9 -0
  347. package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
  348. package/dist/plugins/pg-outcome-ledger.js +9 -0
  349. package/dist/plugins/pg-pool.d.ts +50 -0
  350. package/dist/plugins/pg-pool.js +78 -0
  351. package/dist/plugins/pg-query.d.ts +11 -0
  352. package/dist/plugins/pg-rate-limiter.d.ts +23 -0
  353. package/dist/plugins/pg-rate-limiter.js +12 -2
  354. package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
  355. package/dist/plugins/pg-resume-anchor-store.js +5 -0
  356. package/dist/plugins/pg-run-store.d.ts +7 -0
  357. package/dist/plugins/pg-run-store.js +7 -0
  358. package/dist/plugins/pg-safe-json.d.ts +13 -0
  359. package/dist/plugins/pg-safe-json.js +40 -4
  360. package/dist/plugins/pg-session-policy-store.d.ts +5 -0
  361. package/dist/plugins/pg-session-policy-store.js +5 -0
  362. package/dist/plugins/pg-session-storage.d.ts +72 -0
  363. package/dist/plugins/pg-session-storage.js +152 -20
  364. package/dist/plugins/pg-session-store.d.ts +10 -0
  365. package/dist/plugins/pg-session-store.js +10 -0
  366. package/dist/plugins/pg-tool-result-store.d.ts +7 -0
  367. package/dist/plugins/pg-tool-result-store.js +7 -0
  368. package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
  369. package/dist/plugins/pg-workflow-journal-store.js +7 -0
  370. package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
  371. package/dist/plugins/pg-workflow-run-store.js +7 -0
  372. package/dist/plugins/posix-shell-fs.d.ts +3 -0
  373. package/dist/plugins/posix-shell-fs.js +31 -1
  374. package/dist/plugins/remote-env-adb.d.ts +25 -0
  375. package/dist/plugins/remote-env-adb.js +95 -12
  376. package/dist/plugins/remote-env-e2b.d.ts +146 -0
  377. package/dist/plugins/remote-env-e2b.js +371 -40
  378. package/dist/plugins/remote-env-file-error.d.ts +34 -0
  379. package/dist/plugins/remote-env-file-error.js +37 -0
  380. package/dist/plugins/remote-env-host.d.ts +151 -0
  381. package/dist/plugins/remote-env-host.js +468 -70
  382. package/dist/plugins/remote-env-k8s.d.ts +135 -0
  383. package/dist/plugins/remote-env-k8s.js +318 -32
  384. package/dist/plugins/remote-env-local-docker.d.ts +65 -0
  385. package/dist/plugins/remote-env-local-docker.js +149 -24
  386. package/dist/plugins/remote-env-ssh.d.ts +35 -0
  387. package/dist/plugins/remote-env-ssh.js +117 -20
  388. package/dist/plugins/remote-scratchpad.d.ts +38 -0
  389. package/dist/plugins/remote-scratchpad.js +32 -2
  390. package/dist/plugins/remote-shell.d.ts +22 -0
  391. package/dist/plugins/remote-shell.js +27 -1
  392. package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
  393. package/dist/plugins/resume-anchor-store-sql.js +11 -0
  394. package/dist/plugins/roster-store-sql.d.ts +27 -0
  395. package/dist/plugins/roster-store-sql.js +54 -0
  396. package/dist/plugins/run-store-sql.d.ts +194 -0
  397. package/dist/plugins/run-store-sql.js +210 -9
  398. package/dist/plugins/s3-presign.d.ts +20 -0
  399. package/dist/plugins/s3-presign.js +49 -2
  400. package/dist/plugins/scheduler-support.d.ts +12 -0
  401. package/dist/plugins/scheduler-support.js +75 -3
  402. package/dist/plugins/send-file-ledger.d.ts +15 -0
  403. package/dist/plugins/send-file-ledger.js +53 -4
  404. package/dist/plugins/send-user-file.d.ts +77 -0
  405. package/dist/plugins/send-user-file.js +92 -4
  406. package/dist/plugins/session-policy-store-sql.d.ts +15 -0
  407. package/dist/plugins/session-policy-store-sql.js +75 -3
  408. package/dist/plugins/session-store.d.ts +45 -0
  409. package/dist/plugins/session-store.js +51 -0
  410. package/dist/plugins/sql-driver.d.ts +77 -0
  411. package/dist/plugins/sql-driver.js +4 -0
  412. package/dist/plugins/sql-escape.d.ts +8 -0
  413. package/dist/plugins/sql-escape.js +8 -0
  414. package/dist/plugins/sql-row-helpers.d.ts +25 -0
  415. package/dist/plugins/sql-row-helpers.js +25 -0
  416. package/dist/plugins/store-backend.d.ts +103 -0
  417. package/dist/plugins/store-backend.js +120 -27
  418. package/dist/plugins/store-contracts.d.ts +62 -0
  419. package/dist/plugins/store-contracts.js +3 -0
  420. package/dist/plugins/task-attachment-store.d.ts +60 -0
  421. package/dist/plugins/task-attachment-store.js +49 -3
  422. package/dist/plugins/task-list-store-sql.d.ts +25 -0
  423. package/dist/plugins/task-list-store-sql.js +21 -0
  424. package/dist/plugins/tidb-approval-store.d.ts +6 -0
  425. package/dist/plugins/tidb-approval-store.js +6 -0
  426. package/dist/plugins/tidb-breaker-state.d.ts +5 -0
  427. package/dist/plugins/tidb-breaker-state.js +5 -0
  428. package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
  429. package/dist/plugins/tidb-checkpoint-store.js +7 -0
  430. package/dist/plugins/tidb-cost-quota.d.ts +6 -0
  431. package/dist/plugins/tidb-cost-quota.js +1 -0
  432. package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
  433. package/dist/plugins/tidb-file-snapshot-store.js +6 -0
  434. package/dist/plugins/tidb-image-bake.d.ts +8 -0
  435. package/dist/plugins/tidb-image-bake.js +8 -0
  436. package/dist/plugins/tidb-image-index.d.ts +8 -0
  437. package/dist/plugins/tidb-image-index.js +8 -0
  438. package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
  439. package/dist/plugins/tidb-outcome-ledger.js +10 -0
  440. package/dist/plugins/tidb-pool.d.ts +56 -0
  441. package/dist/plugins/tidb-pool.js +181 -1
  442. package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
  443. package/dist/plugins/tidb-rate-limiter.js +5 -2
  444. package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
  445. package/dist/plugins/tidb-resume-anchor-store.js +5 -0
  446. package/dist/plugins/tidb-run-store.d.ts +7 -0
  447. package/dist/plugins/tidb-run-store.js +7 -0
  448. package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
  449. package/dist/plugins/tidb-session-policy-store.js +5 -0
  450. package/dist/plugins/tidb-session-storage.d.ts +43 -0
  451. package/dist/plugins/tidb-session-storage.js +62 -3
  452. package/dist/plugins/tidb-session-store.d.ts +142 -0
  453. package/dist/plugins/tidb-session-store.js +264 -16
  454. package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
  455. package/dist/plugins/tidb-tool-result-store.js +7 -0
  456. package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
  457. package/dist/plugins/tidb-workflow-journal-store.js +7 -0
  458. package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
  459. package/dist/plugins/tidb-workflow-run-store.js +8 -0
  460. package/dist/plugins/tool-result-store-sql.d.ts +58 -0
  461. package/dist/plugins/tool-result-store-sql.js +38 -2
  462. package/dist/plugins/web-search.d.ts +35 -0
  463. package/dist/plugins/web-search.js +45 -9
  464. package/dist/plugins/workflow-journal-limits.d.ts +10 -0
  465. package/dist/plugins/workflow-journal-limits.js +10 -0
  466. package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
  467. package/dist/plugins/workflow-journal-store-sql.js +58 -2
  468. package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
  469. package/dist/plugins/workflow-run-store-sql.js +84 -6
  470. package/dist/plugins/worktree-isolation.d.ts +45 -0
  471. package/dist/plugins/worktree-isolation.js +105 -6
  472. package/dist/plugins/write-behind-counter.d.ts +56 -1
  473. package/dist/plugins/write-behind-counter.js +53 -14
  474. package/dist/principal-jwt.d.ts +24 -0
  475. package/dist/principal-jwt.js +60 -5
  476. package/dist/project-memory.d.ts +22 -0
  477. package/dist/project-memory.js +144 -15
  478. package/dist/prompts-domain-validate.d.ts +28 -0
  479. package/dist/prompts-domain-validate.js +42 -1
  480. package/dist/question.d.ts +33 -0
  481. package/dist/question.js +69 -2
  482. package/dist/resource-suspend.d.ts +21 -0
  483. package/dist/resource-suspend.js +16 -0
  484. package/dist/router/route-orchestration.d.ts +78 -0
  485. package/dist/router/route-orchestration.js +77 -0
  486. package/dist/run-local.d.ts +32 -0
  487. package/dist/run-local.js +172 -8
  488. package/dist/runs.d.ts +189 -4
  489. package/dist/runs.js +465 -24
  490. package/dist/runtime-caps-resolver.d.ts +106 -0
  491. package/dist/runtime-caps-resolver.js +89 -4
  492. package/dist/runtime-governance.d.ts +84 -0
  493. package/dist/runtime-governance.js +127 -1
  494. package/dist/sandbox-pkg-source.d.ts +23 -0
  495. package/dist/sandbox-pkg-source.js +37 -0
  496. package/dist/sealed-key.d.ts +54 -0
  497. package/dist/sealed-key.js +68 -3
  498. package/dist/security.d.ts +108 -0
  499. package/dist/security.js +145 -6
  500. package/dist/sema-registry.d.ts +21 -0
  501. package/dist/sema-registry.js +21 -0
  502. package/dist/session-leaf-bus.d.ts +31 -0
  503. package/dist/session-leaf-bus.js +32 -0
  504. package/dist/session-sync-content.d.ts +46 -0
  505. package/dist/session-sync-content.js +47 -1
  506. package/dist/session-sync-kernel.d.ts +89 -0
  507. package/dist/session-sync-kernel.js +48 -3
  508. package/dist/session-sync.d.ts +125 -0
  509. package/dist/session-sync.js +181 -8
  510. package/dist/session-titler.d.ts +26 -0
  511. package/dist/session-titler.js +41 -7
  512. package/dist/session-watch.d.ts +53 -1
  513. package/dist/session-watch.js +73 -9
  514. package/dist/sighup-idle.d.ts +30 -0
  515. package/dist/sighup-idle.js +12 -1
  516. package/dist/spec-fields.d.ts +91 -3
  517. package/dist/spec-fields.js +116 -4
  518. package/dist/task-cwd.d.ts +50 -0
  519. package/dist/task-cwd.js +68 -1
  520. package/dist/task-mcp.d.ts +47 -0
  521. package/dist/task-mcp.js +29 -0
  522. package/dist/task-settings.d.ts +175 -0
  523. package/dist/task-settings.js +226 -11
  524. package/dist/task-workflow.d.ts +67 -0
  525. package/dist/task-workflow.js +75 -8
  526. package/dist/tool-approval.d.ts +91 -0
  527. package/dist/tool-approval.js +215 -0
  528. package/dist/trace/artifacts.d.ts +9 -0
  529. package/dist/trace/artifacts.js +30 -5
  530. package/dist/trace/core-keyset-guard.d.ts +18 -0
  531. package/dist/trace/ledger-sink.d.ts +43 -0
  532. package/dist/trace/ledger-sink.js +64 -5
  533. package/dist/trace/project.d.ts +133 -0
  534. package/dist/trace/project.js +189 -2
  535. package/dist/trace/redact.d.ts +19 -0
  536. package/dist/trace/redact.js +66 -9
  537. package/dist/usage-analytics.d.ts +26 -0
  538. package/dist/usage-analytics.js +26 -3
  539. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -106,6 +106,8 @@ import { WorkflowAgentRegistry } from "./orchestration/workflow-agent-steer.js";
106
106
  import { SubagentSteerRegistry } from "./orchestration/subagent-steer.js";
107
107
  import { FleetEventBus, fleetBackgroundChildPublisher } from "./fleet/fleet-bus.js";
108
108
  import { createKeyResolver } from "./key-resolver.js";
109
+ /** Service version for GET /v1/capabilities — read from the package.json shipped next to src/ in the
110
+ * image (Dockerfile COPYs it). Best-effort: an unreadable file reports "unknown", never crashes boot. */
109
111
  function serviceVersion() {
110
112
  try {
111
113
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
@@ -115,37 +117,100 @@ function serviceVersion() {
115
117
  return "unknown";
116
118
  }
117
119
  }
120
+ /**
121
+ * Service entrypoint. Wires the three planes:
122
+ * Brain = external LLM gateway (createBrain)
123
+ * Session = pluggable store (TiDB durable event-log center, or in-memory)
124
+ * Hands = tools/MCP injected per request in resolveSpec (deployment-owned)
125
+ * Persistence (L1 sessions + L2 memory) lives in the external TiDB center; this container is stateless.
126
+ */
118
127
  async function main() {
119
128
  const config = loadConfig();
129
+ // Liveness invariant: the running instance refreshes updated_at every HEARTBEAT_MS; the reaper fails
130
+ // a run whose updated_at is older than runStaleSec. If the stale window were ≤ the heartbeat, the
131
+ // reaper would race a perfectly alive run. Fail fast on a misconfiguration instead of reaping live work.
120
132
  if (config.runStaleSec * 1000 <= HEARTBEAT_MS) {
121
133
  throw new Error(`REAP_RUN_STALE_SEC (${config.runStaleSec}s) must exceed the ${HEARTBEAT_MS / 1000}s liveness heartbeat`);
122
134
  }
123
135
  const logger = createLogger(config.logLevel);
124
136
  const metrics = createMetrics();
137
+ // S20 (SILENT-FALLBACK P1) + [792]④ boot notices: config parsing runs before the logger exists — drain both
138
+ // pre-logger collectors via the shared helper (F10: run-local uses the same one, so entrypoints can't drift).
125
139
  logConfigDiagnostics(logger);
140
+ // design/158 B4 交接小件:plugins 数值旋钮(HOST_BG_*/K8S_BG_*/E2B_*,模块级 import 时读)的非法
141
+ // 字面量告警——通道在 remote-shell.ts 自带(不造 plugins→config 依赖边),与上面同一 boot 诊断点排空。
126
142
  for (const w of drainNumEnvWarnings())
127
143
  logger.warn("config_env_invalid_using_default", { env: w.env, raw: w.raw });
144
+ // M1(接入审计 2026-07-08):core verify 契约=verifier 必须异于 implementer(去相关性是 🔴 部署契约)。
145
+ // verifier 角色落 "default" 时对抗验证者=实现者自评自批——比无验证更糟(假安全感)。曝光而非静默:
146
+ // 单模型部署配 MODEL_VERIFIER 指异构模型即消警(typo/不在 catalog 同样落到这里)。
128
147
  if ((config.roles?.verifier ?? "default") === "default") {
129
148
  logger.warn("verify_decorrelation_unavailable", {
130
149
  hint: "verifier role = default (= implementer) — adversarial verification grades its own work; set MODEL_VERIFIER=<catalog model id> to a heterogeneous model",
131
150
  ...(process.env.MODEL_VERIFIER ? { rejected: process.env.MODEL_VERIFIER, reason: "not in catalog" } : {}),
132
151
  });
133
152
  }
153
+ // S24: redaction rewrites by pattern; LOW: web-search non-array payloads by provider.
134
154
  setRedactionObserver((pattern, n) => metrics.inc("redactions_applied_total", { pattern }, n));
135
155
  setWebSearchBadPayloadObserver((provider) => {
136
156
  metrics.inc("web_search_bad_payload_total", { provider });
137
157
  logger.warn("web_search_non_array_payload", { provider });
138
158
  });
159
+ // Sema registry (sema-registry): pull the effective config on boot and apply it OVER the env
160
+ // defaults — env is the fallback. Unset or unreachable → keep the env-derived config. Applied BEFORE
161
+ // the Runner is built so models/roles/teams take effect. (Logical config only; secrets stay in env.)
139
162
  let ccEtag;
163
+ // Kept past the fetch so the deferred skills/mcp apply (which needs the live `skills` array + async
164
+ // content lazy-pull) can run right before buildScenarios, after applyEffective handled models/roles/teams.
165
+ // `effective` is the BOOT snapshot — the refresh loop never reassigns it, so restartReasons() compares each
166
+ // pull against what THIS process baked in (change-detection, not presence → no auto-restart loop).
140
167
  let effective;
168
+ // 提示词装配协议 §9.5-2 three-store split: `effective` above = the boot snapshot (diff baseline);
169
+ // `latestEffective` = the last candidate that passed validation AND was applied — the explicit
170
+ // last-known-good (LKG). A refresh candidate failing validation is REJECTED whole (§9.5-5): never
171
+ // applied, so the live config keeps serving this LKG instead of a domain falling to schema-default/
172
+ // empty. The third store (promptCandidate — a staged prompts-domain artifact awaiting activation)
173
+ // lands with the prompts domain itself; `lastRejectedCandidate` is its observability forerunner and
174
+ // the prompts-domain validation (core S1 schema) hooks into the same gate.
141
175
  let latestEffective;
142
176
  let lastRejectedCandidate;
177
+ // codex R10 (models-tiers 窗收口): the last EffectiveConfig whose MODEL PLANE (models/roles/tiers/default) was
178
+ // actually applied to the live config — undefined = env plane (no center plane applied yet). When the Runner is
179
+ // tier-frozen, a refresh whose plane differs from THIS is deferred to restart (applyEffective deferModelPlane),
180
+ // keeping admission and the Runner on one catalog generation through the restart window.
143
181
  let appliedPlaneEff;
182
+ // codex R18: the persistent "deferred WITHOUT next-boot handoff" state — the one-shot warn is not enough
183
+ // (the etag advances → later ticks 304 and never re-enter the defer branch). Set when a plane defers with no
184
+ // restart-surviving LKG; cleared when a plane finally applies or a forced restart reason ships. Surfaced on
185
+ // /health (modelPlaneDeferred) + re-warned every refresh tick (incl. 304s).
144
186
  let planeDeferredNoHandoff;
187
+ // codex R35: STICKY live-skill application debt — the boot-baked skill table never rebuilds inside this
188
+ // process, so once the live application is known incomplete the debt survives every refresh (a
189
+ // same-generation 200 recomputes reasons vs boot as [] and would otherwise silently clear the signal).
190
+ // Set at the boot writer (R34 arms); folded into every tick's reasons; only a restart resolves it.
145
191
  let bootSkillStale = false;
192
+ // Sema-registry restart signal: set by the refresh loop when a pull's restart-to-apply slices
193
+ // (skills/mcp/scenarios/runtime-gates) differ from boot; read by GET /health for orchestrator auto-restart.
146
194
  let pendingRestart;
195
+ // DUAL-MODE-DESIGN §4: the config SOURCE is a backend-selectable seam — remote (sema-registry HTTP, the fleet
196
+ // default) | local (.env + config.d via @sema-agent/registry-core FileConfigStore, the single-machine/TOC source).
197
+ // The provider exists when SEMA_REGISTRY_URL (legacy CONFIG_CENTER_URL) is set OR CONFIG_PROVIDER=local is explicit; neither = pure-env
198
+ // (byte-identical to before). The remote provider DELEGATES verbatim to fetchEffective, so the fleet path is
199
+ // unchanged; applyEffective consumes either source's EffectiveConfig identically (one resolver → no drift).
147
200
  const cc = config.configCenter;
201
+ // 🔴 ONE local root for BOTH the config provider (models/skills/mcp) AND the remoteExec loader (avoids a
202
+ // split-brain — createConfigProvider defaulted to process.cwd() while loadRemoteExec used
203
+ // AGENT_DATA_DIR, so config.d and remote-exec.json could resolve to DIFFERENT directories). run-local already
204
+ // computes this once; main.ts now does too. Used below for loadRemoteExec.
148
205
  const localRoot = config.configLocalDir ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
206
+ // Sealed-box key custody: ensure this host's X25519 keypair(s) under
207
+ // <localDataRoot>/sealed-key/ (dir 0700, files 0600; SEVERAL may coexist — rotation keeps old private
208
+ // keys decrypting old ciphertexts) BEFORE the first effective pull, so a roster carrying
209
+ // `sealedApiKey` unseals on the boot apply. The public-key report is best-effort fire-and-forget:
210
+ // the center's execution-keys face is admin-gated and a worker only holds the pull token, so without
211
+ // an operator-provided SEMA_REGISTRY_ADMIN_TOKEN it prints the full public key for manual
212
+ // registration instead (honest posture — see reportExecutionPublicKey). Store failure is non-fatal:
213
+ // sealed models then warn per-model in applyEffective (fail-loud, never a silent apiKeyEnv fallback).
149
214
  let sealedKeys;
150
215
  try {
151
216
  sealedKeys = await ensureSealedKeyStore(join(config.localDataRoot ?? localRoot, "sealed-key"), logger);
@@ -167,6 +232,9 @@ async function main() {
167
232
  ...(config.configProvider === "local" ? { localDir: localRoot } : {}),
168
233
  })
169
234
  : undefined;
235
+ // boot ready 门 latch:仅「远程 registry 部署(非 dryRun)+ 无显式 env MODEL_ID」时等 roster——
236
+ // 首次 effective 落 ≥1 enabled 模型即翻 true(one-way)。其余姿势(env 模型在/local provider/dryRun/纯 env)
237
+ // 恒 ready=现有部署零影响。E3 废除 workers.model 后,fleet worker 的占位模型窗口由此门 fail-closed。
170
238
  const modelReadyState = { ready: !(cc && !cc.dryRun) || Boolean(process.env.MODEL_ID) };
171
239
  const markRosterLanded = (eff) => {
172
240
  if (!modelReadyState.ready && (eff.models?.models ?? []).some((m) => m.enabled !== false)) {
@@ -174,7 +242,14 @@ async function main() {
174
242
  logger.info("model_roster_landed", { note: "ready gate open — billable submissions accepted" });
175
243
  }
176
244
  };
245
+ // center prompts ref([1057]/[1062] 投影腿):**热采用,新任务边界生效**——core 1.316([1068] 裁 a/
246
+ // [1070]d)把 provider 声明纳入 epoch artifactDigest:声明变=digest 变=下任务边界自动重 pin,进行中
247
+ // 会话由 session epoch pin 保稳,resume 记账诚实(1.222 的 restart-to-apply 是 core 件落地前的正确
248
+ // 过渡姿势,[1068]① 官方确认后随 1.316 采用降级为热生效——与 center「发布=新任务生效」产品语义对齐)。
249
+ // 判形通过才采用(形校验=防 core-assemble-THROW 底线,非语义复检 [1057]①f);坏形=warn+沿用旧包。
177
250
  let centerPrompts;
251
+ /** codex-3 F1:一次失败的 catalog 过渡的目标面(undefined 目标=欠 disable)。etag 已推进时 provider
252
+ * 304 短路 adopt——本 marker 让每个 refresh 拍(含 304 拍)独立重试,直到过渡落地。 */
178
253
  let pendingCatalogFaces;
179
254
  const retryPendingCatalog = async (phase) => {
180
255
  if (!pendingCatalogFaces)
@@ -185,8 +260,25 @@ async function main() {
185
260
  pendingCatalogFaces = undefined;
186
261
  }
187
262
  };
263
+ // e/b(catalog 轴消费面,[1236]① seam):core 的 CenterPromptSource——server 只给根路径,core 文件形
264
+ // 自管 `<root>/prompt-epoch/{artifacts,source-state.json}`;boot restore=磁盘 LKG(离线重启 candidate
265
+ // 存续/disabled 不复活,状态机全在 core)。RunnerDeps.promptSource 挂它;adopt 时经 applyCatalogToSource
266
+ // 推进两态过渡。restore 失败=warn(candidate 缺席,catalog 轴等下一次 adopt;declaration 轴/内置照跑)。
188
267
  const promptEpochDir = join(config.localDataRoot ?? localRoot, "prompt-epoch");
268
+ // codex-3 F2:effective 拉取是 worker 轴的(config LKG 同理 worker-keyed)——同根多 worker 进程共享
269
+ // content-addressed artifacts/(by-digest,天然安全),但 **candidate 状态必须按 worker 分箱**:共写一份
270
+ // source-state.json 会让 A 的撤下压掉 B 的 candidate、restart 恢复到别人 worker 的 catalog(错 worker
271
+ // 的系统提示词跑任务)。encodeURIComponent 消毒(worker 名进路径)。
272
+ // codex-5:无撞形分箱——缺席=字面 "global",配置了 worker= "worker-<sha256 前 16>"(哈希后天然单
273
+ // 段/无 dot-segment/与 "global" 不可能同名;worker 名任意字节也安全)。
189
274
  const promptStateDir = join(promptEpochDir, "state", cc?.worker === undefined ? "global" : `worker-${createHash("sha256").update(cc.worker).digest("hex").slice(0, 16)}`);
275
+ // codex-6 F1:File 店构造同步 mkdir——只读/不可写数据根会把「可选的 catalog 特性」变成整服务 boot
276
+ // 失败。守护工厂:文件形失败 ⇒ warn + Memory 店兜底(candidate 不跨重启持久,catalog 采用仍工作;
277
+ // 内置/declaration 轴完全不受影响)。
278
+ // [985]b / [1285] — §9.4 by-digest 补拉组合店:local miss(本副本从未采用过该 candidate / 盘失)⇒
279
+ // 从 center 按 digest 补拉 envelope ⇒ core verifyPromptArtifact 全管线(canonical 复算;**且回验
280
+ // 「拿到的=要的」digest 逐位**,center 答非所问=丢弃)⇒ best-effort 回写盘(下次直命中)。pinned
281
+ // resume 的 fail-loud miss 自此只剩「center 也没有」的真缺席。仅 remote 治理形(cc.baseUrl)挂补拉。
190
282
  const withByDigestBackfill = (inner) => cc?.baseUrl ? withPromptArtifactBackfill(inner, (d) => fetchPromptArtifact(cc.baseUrl, cc.token, d), logger) : inner;
191
283
  const promptSource = (() => {
192
284
  try {
@@ -201,6 +293,10 @@ async function main() {
201
293
  return new CenterPromptSource({ store: withByDigestBackfill(new MemoryPromptArtifactStore()), stateStore: new MemoryPromptSourceStateStore(), engineVersion: CORE_ENGINE_VERSION });
202
294
  }
203
295
  })();
296
+ // codex-2 F4:只有「有配置源在治理且非 dry-run」才 restore 磁盘 candidate——provider-off/dry-run 姿势下
297
+ // 旧部署遗留的 active candidate 不得继续影响新任务(publish-gate-off/compare-only 语义)。不 restore 只
298
+ // 影响 candidate 面;pinned resume 的 by-digest 解析走 FilePromptArtifactStore.get(不依赖 restore),
299
+ // 旧会话恢复不受损。
204
300
  if (configProvider && !cc?.dryRun) {
205
301
  try {
206
302
  await promptSource.restore();
@@ -212,6 +308,8 @@ async function main() {
212
308
  const adoptCenterPrompts = async (eff, phase) => {
213
309
  const raw = eff?.prompts;
214
310
  if (raw === undefined) {
311
+ // fail-closed 语义([1057]①d):publish 闸关=key 整体不出现 ⇒ 收敛回内置提示词(不是「保持旧包」——
312
+ // 闸关是显式撤下,沿用旧包会让「关闸」失效)。
215
313
  if (centerPrompts)
216
314
  logger.info("center_prompts_cleared", { phase, note: "effective carries no prompts key (publish gate off) — built-in providers resume" });
217
315
  const cleared = await applyCatalogToSource(promptSource, undefined, centerPrompts, logger, phase);
@@ -220,7 +318,7 @@ async function main() {
220
318
  pendingCatalogFaces = undefined;
221
319
  }
222
320
  else {
223
- pendingCatalogFaces = { target: undefined };
321
+ pendingCatalogFaces = { target: undefined }; // failed disable — retried by the refresh hook (codex-3 F1)
224
322
  }
225
323
  return;
226
324
  }
@@ -230,6 +328,7 @@ async function main() {
230
328
  return;
231
329
  }
232
330
  if (centerPrompts?.identity !== v.value.identity) {
331
+ // [985]a §3:axes 观测两轴共存窗;declaration 轴字段照旧,catalog 轴报 artifactDigest(内容身份)。
233
332
  logger.info("center_prompts_adopted", {
234
333
  phase,
235
334
  axes: v.value.axes,
@@ -241,6 +340,9 @@ async function main() {
241
340
  });
242
341
  }
243
342
  const transitioned = await applyCatalogToSource(promptSource, v.value, centerPrompts, logger, phase);
343
+ // codex-2 F2 + codex-3 F1:catalog 过渡失败 ⇒ 身份不推进(centerPrompts 保持 prev),且目标面记入
344
+ // pendingCatalogFaces——etag 会照常推进(其他域的采用不为 prompts 的盘错扣住),此后 provider 304
345
+ // 不再进本函数,重试由 refresh 拍上的 retryPendingCatalog 钩独立驱动(见 refresh 循环)。
244
346
  if (transitioned) {
245
347
  centerPrompts = v.value;
246
348
  pendingCatalogFaces = undefined;
@@ -249,15 +351,57 @@ async function main() {
249
351
  pendingCatalogFaces = { target: v.value };
250
352
  }
251
353
  };
354
+ // Boot-deferred pull (clay 2026-07-17): when the boot fetch outlives its budget, the SAME promise is
355
+ // stashed here and, once the refresh tick exists (defined further down), chained to trigger an immediate
356
+ // hot-apply on arrival — the server meanwhile starts on the env fallback in <1s instead of hanging up to
357
+ // ~8s on a black-holed center (真机实测 5.7s = the "local server takes 5s" report).
252
358
  let bootConfigPending;
359
+ // LKG 落盘(clay 拍 2026-07-18;config-lkg.ts 顶注=全案由):remote lane 的干净候选持久化——boot 的
360
+ // 第三级来源(中心 > 盘上 LKG > env)。`lkgBooted` 标志决定 deferred continuation 的形:LKG 起服=有
361
+ // 完整 boot 基线 → 到货走普通 refreshTick(候选门+hot+restartReasons 对 LKG 比较,通常零差异=无环);
362
+ // 无 LKG=env 起服 → 到货走「迟到 boot」路径(deferredBootApply)。
253
363
  const lkgEnabled = configProvider?.kind === "remote" && configLkgEnabled();
254
364
  const lkgPath = process.env.CONFIG_LKG_PATH ?? defaultLkgPath(cc?.worker);
365
+ // codex R17/R20: a successful LOCAL write proves nothing about the NEXT instance seeing it — a stateless pod
366
+ // replaces its filesystem on restart, so treating a pod-local rename as "handoff confirmed" re-opens the R16
367
+ // rolling-restart storm. Next-instance visibility is a DEPLOYMENT contract only the operator can assert, and
368
+ // ONLY via the explicit knob: CONFIG_LKG_DURABLE=true declares the LKG path rides restart-surviving storage
369
+ // (host disk / PVC / shared volume). CONFIG_LKG_PATH is strictly a LOCATION override (R20: it can point at an
370
+ // ephemeral container path — a location says nothing about durability). Unset ⇒ the models-tiers restart
371
+ // reason stays suppressed on a deferred plane (old generation keeps serving; /health modelPlaneDeferred +
372
+ // per-tick warn carry the state) while normal LKG writes continue (they still help same-instance restarts).
255
373
  const lkgSurvivesRestart = lkgEnabled && process.env.CONFIG_LKG_DURABLE === "true";
256
374
  let lkgBooted = false;
257
- let bootLkgCandidate;
375
+ let bootLkgCandidate; // F7:skills 装载后才落盘
376
+ // F1+F2(LKG 复审):awaited 落盘 + skill 正文预热——refresh 只落 manifest 的话,重启后 LKG-boot 在
377
+ // 中心不可达时新 skill 正文缓存缺失=软跳过(静默缺 skill)。预热=枚举 enabled manifest,把缺盘的正文
378
+ // 拉齐进 content-addressed 缓存(失败=warn 软降级,与 boot 的 skill_failed 同语义等级——skill 拉取
379
+ // 失败从不升级为配置拒绝);await 保证 restart 信号可见前 LKG+正文已落盘。
380
+ /** Returns TRUE iff the candidate is durably persisted for the NEXT boot (codex R16: the FORCED models-tiers
381
+ * restart reason must never fire without a confirmed handoff — LKG disabled/unwritable + over-budget boot
382
+ * pulls would otherwise turn the deferral into a rolling-restart storm: every restarted process boots the env
383
+ * plane again, defers again, re-signals). Change-DETECTED reasons still ride regardless (their loop is the
384
+ * pre-existing, documented no-LKG residual). */
385
+ // codex R37 loop-breaker proof: TRUE iff the LAST persistLkgDurable call hash-verified EVERY enabled skill
386
+ // body on disk. This is the "a restart can actually repair the live skill table" proof the NON-durable lane
387
+ // requires before publishing a skills restart — a successful manifest pull only proves the CONFIG endpoint
388
+ // works, not the SEPARATE body fetches; signaling on manifest-success alone lets a persistent body failure
389
+ // (404 / hash mismatch / timeout) restart-storm a stateless deployment (every replacement process fails the
390
+ // same body, signals again, is restarted again). With the proof, a storm cannot form: a boot that fails the
391
+ // body lacks the proof and only retains visible debt (no signal) until a later verify succeeds.
258
392
  let lastPrewarmBodiesVerified = false;
259
393
  const persistLkgDurable = async (effectiveObj, etag) => {
260
394
  lastPrewarmBodiesVerified = false;
395
+ // codex R21: the LOCAL provider's candidate IS the next-boot source (config.d on the same disk boot reads
396
+ // BEFORE Runner construction) — the handoff is inherent, no LKG needed. Without this arm a polled config.d
397
+ // edit that defers the plane (tier activation / tier-frozen change) could never authorize its own restart
398
+ // (lkgEnabled is remote-only, so even CONFIG_LKG_DURABLE couldn't help) — stuck in modelPlaneDeferred forever.
399
+ // codex R28 requalified: this TRUE covers the MODEL PLANE (what the deferral gate adjudicates). Skills are
400
+ // NOT part of the local handoff because the local lane does not consume a center-style skills domain AT ALL
401
+ // yet — the only boot consumer (applyCenterSkills) requires config.configCenter (remote HTTP), absent on
402
+ // CONFIG_PROVIDER=local; bodies live inline in config.d (LocalConfigProvider.fetchSkillContent) but no boot
403
+ // path reads them. Pre-existing local-lane gap (predates this train, nothing lost across restart that was
404
+ // ever applied); wiring boot skills through the ConfigProvider seam is the tracked follow-up (board).
261
405
  if (configProvider?.kind === "local")
262
406
  return true;
263
407
  if (!lkgEnabled)
@@ -266,6 +410,15 @@ async function main() {
266
410
  if (effectiveObj.skills && config.configCenter) {
267
411
  await applyCenterSkills([], effectiveObj.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, defaultSkillCacheDir());
268
412
  }
413
+ // codex R25/R26: skill BODIES are part of the handoff contract, and completeness gates PUBLICATION —
414
+ // applyCenterSkills swallows fetch/cache-write failures, so awaiting it proves nothing; verify every
415
+ // ENABLED manifest hash on disk (content-true) BEFORE the atomic LKG replace (R26: verifying after left
416
+ // an incomplete candidate as the boot-visible LKG — any crash/manual restart with the center down then
417
+ // boots that manifest, silently skips the uncached skills, and never re-signals because the manifest is
418
+ // the new boot baseline). On any miss/corruption the PREVIOUS good LKG stays in place; the per-tick
419
+ // retained-candidate retry re-attempts prewarm + verify + publish.
420
+ // Residual seam (honest, partial): the skill cache roots at SEMA_CONFIG_DIR while lkgPath can point
421
+ // elsewhere — CONFIG_LKG_DURABLE's contract requires BOTH on restart-surviving storage.
269
422
  const enabledSkills = (effectiveObj.skills?.skills ?? []).filter((m) => m.enabled !== false);
270
423
  const cacheDir = defaultSkillCacheDir();
271
424
  for (const m of enabledSkills) {
@@ -284,8 +437,11 @@ async function main() {
284
437
  return false;
285
438
  }
286
439
  }
287
- lastPrewarmBodiesVerified = true;
440
+ lastPrewarmBodiesVerified = true; // every enabled body verified on disk — the repair proof stands even if the LKG write below fails
288
441
  await saveLkg(lkgPath, cc?.worker, effectiveObj, etag);
442
+ // codex R17: TRUE = next-BOOT visibility, not merely a successful local rename — gated on the operator's
443
+ // restart-surviving-storage declaration (lkgSurvivesRestart above). A stateless pod's local write still
444
+ // helps same-instance restarts but must not authorize a forced restart.
289
445
  return lkgSurvivesRestart;
290
446
  }
291
447
  catch (err) {
@@ -294,7 +450,9 @@ async function main() {
294
450
  }
295
451
  };
296
452
  if (configProvider) {
297
- const dryRun = cc?.dryRun ?? false;
453
+ const dryRun = cc?.dryRun ?? false; // dry-run posture is remote-only (a local source has nothing to compare against)
454
+ // LKG 读腿(boot 超窗与硬失败两个入口共用):盘上有验证过的干净候选 → 当 boot 配置整套应用
455
+ // (skills/mcp 进 buildScenarios——这正是 env 兜底给不了的),etag 落位 → refresh 首拍 304 或正常增量。
298
456
  const bootFromLkg = async (why) => {
299
457
  if (!lkgEnabled || dryRun)
300
458
  return null;
@@ -312,7 +470,7 @@ async function main() {
312
470
  await adoptCenterPrompts(lkgR.effective, "boot-lkg");
313
471
  markRosterLanded(lkgR.effective);
314
472
  effective = lkgR.effective;
315
- appliedPlaneEff = lkgR.effective;
473
+ appliedPlaneEff = lkgR.effective; // model plane applied pre-Runner — this IS the Runner's generation
316
474
  latestEffective = lkgR.effective;
317
475
  if (lkgR.etag !== undefined)
318
476
  ccEtag = lkgR.etag;
@@ -325,7 +483,7 @@ async function main() {
325
483
  let lkgFallback = null;
326
484
  if (raced === BOOT_FETCH_DEFERRED) {
327
485
  bootConfigPending = bootFetch;
328
- bootFetch.catch(() => { });
486
+ bootFetch.catch(() => { }); // settled in the background — failure handling lives on the refresh cadence
329
487
  lkgFallback = await bootFromLkg("boot fetch deferred (budget exceeded)");
330
488
  lkgBooted = lkgFallback !== null;
331
489
  logger.warn("config_boot_fetch_deferred", {
@@ -337,6 +495,9 @@ async function main() {
337
495
  : "center slow/unreachable and no usable LKG — serving on the env fallback; the pull continues in the background (late-boot adoption on arrival). Raise CONFIG_BOOT_FETCH_BUDGET_MS to block boot instead.",
338
496
  });
339
497
  }
498
+ // F4(LKG 复审):LKG 载荷的 apply 用专属 catch——走主分支会让 applyEffective 半程炸落到外层
499
+ // catch 的「config_source_unreachable_using_env」误导日志(中心明明只是慢)。坏 LKG=点名+env 兜底
500
+ // (in-place mutate 的混态风险与 live 坏载荷同级,浅域判形在 loadLkg 已挡结构性坏形)。
340
501
  if (raced === BOOT_FETCH_DEFERRED && lkgFallback) {
341
502
  try {
342
503
  await bootApplyLkgInline(lkgFallback);
@@ -348,26 +509,41 @@ async function main() {
348
509
  }
349
510
  const r = raced === BOOT_FETCH_DEFERRED ? null : raced;
350
511
  if (r) {
512
+ // [898] tolerant read(registry-core 0.10.12,local lane):catalog 域坏文件不再连坐整包回落 env——
513
+ // 坏域按 default 落+per-domain warn 点名(文件路径在 error 文案里),好域照常生效。gate 域坏文件
514
+ // tolerant 仍 throw(READ 容错不放宽 gate fail-loud)⇒ 走下面既有整包 catch。
515
+ // §9.5-5 boot 与 refresh 的分工(codex F1:重启不得洗白坏候选):boot 照旧 tolerant APPLY(无 LKG
516
+ // 可保,带着好域起服务),但一个带错误的候选 ①不推进 ccEtag——refresh 首 poll 拿到同一棵树,候选门
517
+ // 当场整拒并持续 warn 到修复;②不写入 latestEffective——LKG 永远只指向完全干净的候选,拒绝日志的
518
+ // lkgVersion 也诚实(boot 带错=「(env)」)。markRosterLanded 保留:models 域自身干净才可能开门
519
+ // (坏 models 域 tolerant 落空=enabled 0=门不动),其他域的错误不构成扣住就绪门的理由。
351
520
  const bootPromptsRaw = r.effective.prompts;
352
521
  const bootPromptsOk = bootPromptsRaw === undefined || validatePromptsDomain(bootPromptsRaw).ok;
522
+ // 坏 prompts 面同 domainErrors 计入 not-clean(F6 boot 半场):etag 不进 → refresh 首拍同树整拒持续
523
+ // warn;LKG 不指向带坏 prompts 的候选。adoptCenterPrompts 自身会拒采用+点名。
353
524
  const bootClean = (r.domainErrors ?? []).length === 0 && bootPromptsOk;
354
525
  if (bootClean)
355
526
  ccEtag = r.etag;
356
527
  for (const de of r.domainErrors ?? [])
357
528
  logger.warn("config_domain_invalid", { domain: de.domain, error: de.error, note: "tolerated at BOOT (corrupt file → schema defaults; dangling ref → reported as-is); the refresh path rejects such a candidate whole and keeps the LKG (§9.5-5)" });
358
529
  if (dryRun) {
359
- logEffectiveDiff(config, r.effective, logger);
530
+ logEffectiveDiff(config, r.effective, logger); // compare-only: log the would-be changes, keep env
360
531
  }
361
532
  else {
362
- applyEffective(config, r.effective, logger, { sealedKeys });
363
- await adoptCenterPrompts(r.effective, "boot");
364
- markRosterLanded(r.effective);
365
- effective = r.effective;
366
- appliedPlaneEff = r.effective;
533
+ applyEffective(config, r.effective, logger, { sealedKeys }); // sealedKeys: unseal sealedApiKey → in-memory key pipeline
534
+ await adoptCenterPrompts(r.effective, "boot"); // [1057] additive prompts key(判形采用;缺席=内置)
535
+ markRosterLanded(r.effective); // boot pull 即落 roster 的常态:门在启动窗内就打开
536
+ effective = r.effective; // skills/mcp applied below, before buildScenarios (restart-to-apply)
537
+ appliedPlaneEff = r.effective; // model plane applied pre-Runner — this IS the Runner's generation
367
538
  if (bootClean)
368
- latestEffective = r.effective;
539
+ latestEffective = r.effective; // LKG seed (§9.5-2): only a fully clean candidate may become the LKG
540
+ // F7(LKG 复审三轮):boot 写点**延后**到 center skills 装载(applyCenterSkills+盘缓存)之后——
541
+ // 先落 LKG 会让「新 skill 的 manifest 可见但正文没进缓存」窗口存在(此窗内崩+中心断=离线 boot
542
+ // 静默缺 skill)。这里只记 candidate,真正落盘在 skills 装载后(main 后段)。
369
543
  if (bootClean && !lkgBooted)
370
544
  bootLkgCandidate = { effective: r.effective, etag: r.etag };
545
+ // [875]b:带上模型数与(local lane)真实目录来源——复验实录:CONFIG_LOCAL_DIR 误配(只设
546
+ // LOCAL_DATA_ROOT)时 config.d 静默读不到、0 模型且无告警,排障只能靠猜。
371
547
  const cfgModels = (r.effective.models?.models ?? []).filter((m) => m.enabled !== false).length;
372
548
  const cfgDir = configProvider.kind === "local" ? { dir: configProvider.root } : {};
373
549
  logger.info("config_loaded", { source: configProvider.kind, version: r.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels, ...cfgDir });
@@ -378,6 +554,7 @@ async function main() {
378
554
  }
379
555
  catch (err) {
380
556
  logger.warn("config_source_unreachable_using_env", { source: configProvider.kind, err: String(err) });
557
+ // 硬失败(fast-fail RST 等,非超窗)同样值得 LKG:上一进程的干净候选好过纯 env。
381
558
  const lkgFallback = await bootFromLkg(`boot fetch failed: ${String(err).slice(0, 120)}`);
382
559
  if (lkgFallback) {
383
560
  lkgBooted = true;
@@ -393,10 +570,21 @@ async function main() {
393
570
  }
394
571
  if (!modelReadyState.ready)
395
572
  logger.warn("model_roster_pending", { note: "registry deployment without an env model and no roster yet — billable submissions 503 until the first effective pull lands models" });
573
+ // DUAL-MODE §4/§5 (center #1): in LOCAL config mode, source remoteExec from the SHARED
574
+ // `config.d/remote-exec.json` via the registry `loadRemoteExec` — the SAME file + zod validation that the
575
+ // client's `agent-cli config` writes, so client doctor and the engine read one source of truth (zero drift).
576
+ // Semantics (center, deliberate): file MISSING → undefined → keep the env/host default (config.ts already
577
+ // defaults host in local mode); file PRESENT → it wins; file empty/corrupt → the loader throws → fail-closed
578
+ // (an empty file silently falling back to host would be an isolation downgrade if the user meant local-docker).
396
579
  if (configProvider?.kind === "local") {
397
580
  try {
398
- const fileRx = await loadRemoteExec(localRoot);
581
+ const fileRx = await loadRemoteExec(localRoot); // SAME root as the config provider above (no split-brain)
399
582
  if (fileRx) {
583
+ // 🔴 center contract: the package RemoteExecSpec is the CONTRACT form (env-NAME refs: apiKeyEnv/tokenEnv/…);
584
+ // our config.remoteExec is the RESOLVED form (values). `host` carries NO secrets so it maps 1:1 (contract
585
+ // `workdir` ≡ our `workspaceBase`). The ISOLATED lanes (e2b/k8s/ssh/adb) need a NAME→value resolution
586
+ // before use — a follow-on; until then they are sourced from REMOTE_EXEC env, so a file-source isolated
587
+ // arm is ignored (NOT cast verbatim — that would feed an env-NAME where a value is expected).
400
588
  if (fileRx.provider === "host") {
401
589
  config.remoteExec = {
402
590
  provider: "host",
@@ -406,6 +594,10 @@ async function main() {
406
594
  logger.info("remote_exec_from_file", { provider: "host", root: localRoot });
407
595
  }
408
596
  else if (fileRx.provider === "local-docker") {
597
+ // local-docker carries NO secret env-NAMEs (image/memory/cpus/network/workdir/mountPath only), so it
598
+ // maps 1:1 from the contract form — no NAME→value resolution needed (unlike e2b/k8s/ssh). The contract
599
+ // `workdir` is the in-container workspace ROOT ≡ our `mountPath`. DOCKER_HOST/secret env stay in the
600
+ // worker's .env (the adapter reads its own process.env via the env-forward, not the file).
409
601
  config.remoteExec = {
410
602
  provider: "local-docker",
411
603
  image: fileRx.image,
@@ -423,20 +615,31 @@ async function main() {
423
615
  }
424
616
  catch (err) {
425
617
  logger.error("remote_exec_file_invalid", { root: localRoot, err: String(err) });
426
- throw err;
618
+ throw err; // fail-closed (center semantics): a corrupt remote-exec.json must not boot a wrong/weaker lane
427
619
  }
428
620
  }
621
+ // One shared SQL store backend for L1 + L2 (TiDB/MySQL or PostgreSQL per DB_BACKEND). Owned here.
429
622
  let backend;
430
- let storeBackendDegraded = false;
623
+ let storeBackendDegraded = false; // S5: auto-probe degraded this replica to in-memory (surfaced via gauge + /health)
624
+ // `auto` probes a configured DB and degrades to in-memory if it's unreachable (local/intranet binaries:
625
+ // "use my DB if I can reach it, else memory"). Explicit `tidb` fails fast instead — silently dropping
626
+ // persistence the operator asked for would lose runs. (`tidb` here = "the SQL DB"; DB_BACKEND picks the engine.)
431
627
  const wantDb = config.sessionBackend === "tidb" ||
432
- config.dbBackend === "local" ||
628
+ config.dbBackend === "local" || // local (clay 2026-06-25): always build the DB-less in-memory/file StoreBackend
433
629
  (config.sessionBackend === "auto" && !!(config.tidb || config.pg));
434
630
  if (wantDb) {
631
+ // 构造 + ensureSchema 同罩一层降级臂(2026-07-28 修):原先 try 只罩 ensureSchema,而 local 形真正
632
+ // 会抛的是构造里的 mkdir + 数据根 BootLock ——「裸 boot 不得拒启」那条口径对 local 从未生效过。
633
+ // 降级/fail-loud 的判据与理由见 openStoreBackendWithFallback 顶注。
435
634
  const opened = await openStoreBackendWithFallback(config, logger);
436
635
  backend = opened.backend;
437
636
  storeBackendDegraded = opened.degraded;
438
637
  }
638
+ // S5 review LOW-1: always render the series (0 = healthy) — gauge absence is indistinguishable from
639
+ // "old build without this metric", which breaks `== 0`-style alert rules.
439
640
  metrics.setGauge("store_backend_degraded", storeBackendDegraded ? 1 : 0);
641
+ // S21 (SILENT-FALLBACK P1): MINIO_* partially set silently falls back to SQL-blob snapshots. Warn on the
642
+ // partial config and always render which backend blobs actually use.
440
643
  {
441
644
  const minioReq = ["MINIO_ENDPOINT", "MINIO_ACCESS_KEY", "MINIO_SECRET_KEY"];
442
645
  const present = minioReq.filter((v) => (process.env[v] ?? "") !== "");
@@ -445,14 +648,34 @@ async function main() {
445
648
  }
446
649
  metrics.setGauge("snapshot_blob_backend", 1, { backend: config.snapshotBlobStore ? "minio" : "sql" });
447
650
  }
651
+ // Resolve `auto` to the concrete backend the rest of the wiring understands (createSessionStore,
652
+ // runStore, the startup summary). A DB-backed L2 with no backend degrades to in-memory the same way.
448
653
  if (config.sessionBackend === "auto")
449
654
  config.sessionBackend = backend ? "tidb" : "memory";
655
+ // local backend: route the session store through backend.session() (LocalSessionStore — the §0.5 OwnerAware twin),
656
+ // NOT the bare TtlSessionStore the "memory" path returns; "tidb" here means "the backend's durable store" generically.
450
657
  if (config.dbBackend === "local" && backend)
451
658
  config.sessionBackend = "tidb";
659
+ // Long-term memory (design/138 S1, clay 2026-07-08): the injection-first file-based memory ENGINE is the
660
+ // only memory plane — `RunnerDeps.memoryBackend` is core's switch (materialize → session file ops →
661
+ // harvest; no remember/recall tools). SINGLE-USER TURNKEY ONLY: the file basement has no tenant
662
+ // isolation, so a multi-tenant deployment (requirePrincipal) gets `undefined` = memory dark, fail-closed
663
+ // (memoryEngineBackendFor + memoryScopeFor enforce the same gate). The legacy MemoryStore plane
664
+ // (MEMORY_BACKEND/EMBEDDING_*) was dropped without migration; `scripts/drop-memory-tables.sql` drops the table.
665
+ // S3-TOB(设计 §1.3,后经改判与边界重切):backend 选择器——file=现状(单用户/host);pg|tidb=
666
+ // DB durable 真身(零卷主档:memory/ 文件面=materialize 的 ephemeral 工作副本,harvest 回 DB=持久化
667
+ // 时点)。DB backend 是多租户点亮的唯一门(显式 opt-in);选了 DB 但池不可用/方言不匹配=fail-loud 拒启
668
+ // (no-stopgap:绝不静默降 File/dark)。
452
669
  let memoryEngine;
670
+ // 142-S2.5:per-(scope,peer) 同步游标面(sync_cursors)——POST /v1/memory/sync/:scope 的持久化半场。
671
+ // 只在 DB memory plane 上点亮(file 形态路由 501,不接);与 entry plane 同池同方言。
453
672
  let memorySyncCursors;
454
673
  if (config.memoryEngineBackend !== "file" && config.memoryEngineEnabled) {
455
674
  const dialect = config.memoryEngineBackend;
675
+ // 142-S5.1 production history sink: every applied patch appends one
676
+ // agent_memory_engine_history row via the backend's optional history opts. The wrapper's ONLY
677
+ // job is the metrics leg — count lost rows BEFORE rethrowing (the backend swallows + warns with
678
+ // its own cumulative count; the audit line's breakage must itself be auditable).
456
679
  const countedHistorySink = (store) => ({
457
680
  appendHistory: async (rows) => {
458
681
  try {
@@ -471,9 +694,11 @@ async function main() {
471
694
  const q = async (text, params) => { const r = await pool.query(text, params); return { rows: r.rows }; };
472
695
  await ensurePgMemoryEngineSchema(q);
473
696
  await ensurePgMemoryHistorySchema(q);
474
- await ensurePgMemorySyncSchema(q);
697
+ await ensurePgMemorySyncSchema(q); // 142-S2.5: sync_cursors(+push_queue)同池 ensure,幂等
475
698
  memorySyncCursors = new PgMemorySyncStore(q);
476
699
  const pgMem = new PgMemoryEngineBackend(q, { history: countedHistorySink(new PgMemoryHistoryStore(q)) });
700
+ // 工作面根:DB 形态下 memory/ 目录=per-worker ephemeral 物化区(丢了重建,控制面同);
701
+ // 复用 localDataRoot 下独立子树,绝不与 File backend 的持久 memory/ 混写。
477
702
  memoryEngine = { backend: pgMem, root: join(config.localDataRoot ?? localRoot, "memory-work") };
478
703
  }
479
704
  else {
@@ -482,7 +707,7 @@ async function main() {
482
707
  throw new Error("MEMORY_ENGINE_BACKEND=tidb requires DB_BACKEND=mysql/tidb — refusing to start half-configured");
483
708
  await ensureTiDBMemoryEngineSchema(pool);
484
709
  await ensureTiDBMemoryHistorySchema(pool);
485
- await ensureTiDBMemorySyncSchema(pool);
710
+ await ensureTiDBMemorySyncSchema(pool); // 142-S2.5: sync_cursors(+push_queue)同池 ensure,幂等
486
711
  memorySyncCursors = new TiDBMemorySyncStore(pool);
487
712
  memoryEngine = { backend: new TiDBMemoryEngineBackend(pool, { history: countedHistorySink(new TiDBMemoryHistoryStore(pool)) }), root: join(config.localDataRoot ?? localRoot, "memory-work") };
488
713
  }
@@ -490,6 +715,10 @@ async function main() {
490
715
  }
491
716
  else {
492
717
  memoryEngine = memoryEngineBackendFor(config);
718
+ // [1845]②(cli 桌面撞坑;壳侧此前同坑已各自修过——两个宿主都踩=第三个宿主还会踩):操作员用
719
+ // LOCAL_DATA_ROOT 明确要了隔离数据根,MEMORY_ENGINE_DIR 却缺省 ⇒ file 形 memory engine 落 core
720
+ // 默认链(AGENT_DATA_DIR → ~/.ai-agent)= **静默共享全机 memory 库**。不改行为(单机用户可能就要
721
+ // 共享),只把「你现在在共享」说出来。
493
722
  if (memoryEngine && process.env.LOCAL_DATA_ROOT && !config.memoryEngineDir) {
494
723
  logger.warn("memory_engine_dir_defaulted", {
495
724
  root: memoryEngine.root,
@@ -497,6 +726,10 @@ async function main() {
497
726
  });
498
727
  }
499
728
  }
729
+ // RosterStore(agent-team S1 server 半场,[1070]① 提货单;core 1.316 `RunnerDeps.rosterStore`):
730
+ // 具名子 agent 持久名册。形态跟 StoreBackend:tidb/pg=SQL twins(语义三条+真双库验证,
731
+ // roster-store-sql.ts 顶注);local=core FileRosterStore(dataRoot 下 roster.json,core 自带原子写/
732
+ // 损坏安全);无 backend(纯内存 dev)=不挂(core 用活注册表,跨 run 指针面自然缺席——诚实)。
500
733
  let rosterStore;
501
734
  {
502
735
  const mysqlPool = backend?.mysqlPool?.();
@@ -516,6 +749,15 @@ async function main() {
516
749
  if (rosterStore)
517
750
  logger.info("roster_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
518
751
  }
752
+ // BackgroundAgentStore(design/151 S1 server 半场,[1503] 提货单;core 1.364 durable background agents):
753
+ // 后台子代 `a*` 行的 durable 执行记录——settle 后转录 session 不再 eager release(TaskOutput/
754
+ // AgentTranscript 完成后照读全程),清理移交 reapDurableAgents(reaper 区)。形态跟 roster:
755
+ // tidb/pg=SQL twins(真双库验证,background-agent-store-sql.ts 顶注);local=core
756
+ // FileBackgroundAgentStore(dataRoot 下 background-agents/,core 自带 ledger+snapshot 原子写);
757
+ // 无 backend(纯内存 dev)=不挂(core 契约:store 不接线=pre-151 逐字节等价——单进程 live 注册表
758
+ // 已覆盖读面,诚实缺席)。⚠️ 同实例双点挂载:RunnerDeps.backgroundAgentStore(读半场)+
759
+ // ScenarioDeps→SubagentToolOptions.background.agentStore(写半场)都用这一个引用——engine 无法核对
760
+ // 配对,半接=静默死特性(RB-37①)。
519
761
  let backgroundAgentStore;
520
762
  {
521
763
  const mysqlPool = backend?.mysqlPool?.();
@@ -530,16 +772,28 @@ async function main() {
530
772
  backgroundAgentStore = new TiDBBackgroundAgentStore(mysqlPool);
531
773
  }
532
774
  else if (backend?.kind === "local") {
775
+ // core 1.368([1516]②)listScopes 落地:file 实现自带枚举——1.248 拍的「local+多租户 reap 缺口」
776
+ // warn-once 已撤(reaper 腿现在真枚举,不再假设单 scope)。
533
777
  backgroundAgentStore = new FileBackgroundAgentStore(config.localDataRoot ?? localRoot);
534
778
  }
535
779
  if (backgroundAgentStore)
536
780
  logger.info("background_agent_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
537
781
  }
782
+ // D-1 通用文件上传(clay 拍 2026-07-27):独立附件 store(不骑 snapshot blob 面——那套的 orphan GC
783
+ // 会把非 manifest 引用的 bytes 误收;生命周期也不同:附件跟 task/session 绑定)。形态跟 StoreBackend
784
+ // 三态:tidb/pg=SQL twins(真双库验证)、local=文件店;无 backend(纯内存 dev)=不挂 ⇒ 路由 501。
785
+ // 四半场:①上传/取回/删除(http/server.ts 路由)②提交时绑定+objective 告知+host lane 物化
786
+ // (resolveSpec 内,下方)③远程 lane env 建立时物化(executionEnvFactory 包装,下方)④生命周期
787
+ // (E21 purge 级联 + 未绑定 TTL reaper)。
538
788
  let taskAttachmentStore;
539
789
  {
540
790
  const mysqlPool = backend?.mysqlPool?.();
541
791
  const pgPool = backend?.pgPool?.();
542
792
  if (pgPool || mysqlPool) {
793
+ // clay 裁(2026-07-27):**云形态对象存储必配**——附件字节本体进 MinIO(SQL 只存 meta/门/生命周期)。
794
+ // 复用快照 lane 的同一 MinIO 部署配置(MINIO_ENDPOINT/ACCESS/SECRET,零重映射),但**附件专属
795
+ // keyPrefix**:对象键=sha256,与快照 blob 同 sha 不同生命周期,同名字空间会互删。未配 ⇒ 附件面
796
+ // 不接线(路由 501)+ error 级日志——这是部署配置错误,不静默降级回「字节进 DB」(该形已被裁掉)。
543
797
  const minio = config.snapshotBlobStore;
544
798
  if (!minio) {
545
799
  logger.error("attachments_disabled_object_store_required", {
@@ -565,6 +819,11 @@ async function main() {
565
819
  if (taskAttachmentStore)
566
820
  logger.info("task_attachment_store_enabled", { backend: pgPool ? "pg+minio" : mysqlPool ? "tidb+minio" : "file", maxBytes: config.attachmentMaxBytes });
567
821
  }
822
+ // S3c(core 1.374 行为车,[1531]):teammate 信箱——SendMessage 对 SETTLED named teammate 的 tier-3
823
+ // 懒复活链(durable 行解析→claim-CAS→mailbox append→reviveSpawn→attach 屏障后 ack)。激活三件套=
824
+ // RunnerDeps.mailboxStore + backgroundAgentStore 同挂 + delegation 工具在场,三缺一=1.373 文本字节
825
+ // 不变(诚实拒)。形态同上:tidb/pg=SQL twins(mailbox-store-sql.ts,发车复审八修顶注);local=
826
+ // core FileMailboxStore;纯内存 dev=不挂。回执句族(details.error 枚举)wire 透传零改动。
568
827
  let mailboxStore;
569
828
  {
570
829
  const mysqlPool = backend?.mysqlPool?.();
@@ -583,23 +842,35 @@ async function main() {
583
842
  if (mailboxStore)
584
843
  logger.info("mailbox_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
585
844
  }
845
+ // 142-S5 §1.4: the per-scope export READ face lights up only on the DB memory plane (the multi-tenant
846
+ // truth lives in the scope-partitioned tables; the file posture exports by copying the memory dir, so
847
+ // the route 501s honestly there). History-table WRITE wiring: inside the backend construction above
848
+ // (S5.1, opts.history — the write point lives in the handed-over backend code).
586
849
  const memoryExportBackend = config.memoryEngineBackend !== "file" ? memoryEngine?.backend : undefined;
587
- const memoryLane = memoryEngineRemoteLanePosture(config);
850
+ const memoryLane = memoryEngineRemoteLanePosture(config); // N0: worker/sandbox file-plane split posture
588
851
  if (config.memoryEngineBackend === "file" || !config.memoryEngineEnabled) {
589
852
  logger.info("memory_engine_enabled", memoryEngine
590
853
  ? { enabled: true, backend: "file", dir: memoryEngine.root }
591
854
  : {
592
855
  enabled: false,
856
+ // 复审 F-11:按最终门结果报因,多租户 file=隔离判据,其余照旧。
593
857
  reason: config.requirePrincipal === true ? "multi-tenant on the FILE backend (no tenant isolation; set MEMORY_ENGINE_BACKEND=pg|tidb to light up)"
594
858
  : !config.memoryEngineEnabled ? "MEMORY_ENGINE=off"
595
859
  : `remote exec lane "${memoryLane?.lane}" (worker/sandbox file planes split; MEMORY_ENGINE_REMOTE_LANE=allow overrides)`,
596
860
  });
597
861
  }
862
+ // N0 boot warn: loud in BOTH postures — "dark" so an upgrade that silently turns memory off is visible,
863
+ // "forced" so an operator override states what it depends on (harvest only sees the WORKER fs).
598
864
  if (memoryLane && config.memoryEngineEnabled && config.requirePrincipal !== true) {
599
865
  logger.warn("memory_engine_remote_lane", memoryLane.posture === "dark"
600
866
  ? { lane: memoryLane.lane, effect: "memory dark (fail-closed): the file engine works the worker's local fs while this lane routes model file tools to the sandbox fs — sandbox writes are never harvested. Set MEMORY_ENGINE_REMOTE_LANE=allow ONLY if both are one fs." }
601
867
  : { lane: memoryLane.lane, effect: "MEMORY_ENGINE_REMOTE_LANE=allow: memory engine ON over a remote lane — harvest only sees files landing on the WORKER fs; verify the lane really shares it." });
602
868
  }
869
+ // 142-S2.5-W1: TOC 同步 client 腿——只在 file memory 形态接线(loadConfig 已拒 DB backend
870
+ // 上的 MEMORY_SYNC_*,这条分支到不了)。boot 后 fire-and-forget 一轮(失败 warn 不阻断——纯本地现状
871
+ // 是安全降级面);之后 harvest 真有 patch 落地时再触发(onMemoryHarvestReport 站点,inflight 节流)。
872
+ // 引擎 dark(多租户/MEMORY_ENGINE=off/remote lane)⇒ 无本地盘可同步:warn 不 throw(dark 的三个成因
873
+ // 各有自己的 loud 日志在上方,这里补“sync 因此没跑”这半句,operator 可见不半配)。
603
874
  let memorySyncRunner;
604
875
  if (config.memorySync) {
605
876
  if (memoryEngine) {
@@ -609,6 +880,7 @@ async function main() {
609
880
  memoryRoot: memoryEngine.root,
610
881
  transport: createMemorySyncTransport({ url: config.memorySync.url, token: config.memorySync.token }),
611
882
  log: logger,
883
+ // S2.5 分批(core 1.284):未设=不分批 wire 字节不变;设了=续轮 loop 至收敛(MAX_SYNC_ROUNDS 警戒)。
612
884
  ...(config.memorySync.maxPushEntries !== undefined ? { maxPushEntries: config.memorySync.maxPushEntries } : {}),
613
885
  ...(config.memorySync.maxPullEntries !== undefined ? { maxPullEntries: config.memorySync.maxPullEntries } : {}),
614
886
  });
@@ -619,11 +891,18 @@ async function main() {
619
891
  logger.warn("memory_sync_configured_but_memory_dark", { note: "MEMORY_SYNC_URL is set but the file memory engine is dark (multi-tenant / MEMORY_ENGINE=off / remote exec lane) — no sync rounds will run" });
620
892
  }
621
893
  }
622
- const sessionStore = createSessionStore(config, backend, metrics);
894
+ const sessionStore = createSessionStore(config, backend, metrics); // S25: stale-affinity evict fingerprint
895
+ // S6 startup guard: multi-tenant isolation needs an owner-aware (TiDB) store. Refuse to start with
896
+ // REQUIRE_PRINCIPAL on an in-memory store that can't enforce session ownership.
623
897
  if (config.requirePrincipal && !sessionStore.ownerOf) {
624
898
  throw new Error("REQUIRE_PRINCIPAL=true needs an owner-aware session store (SESSION_BACKEND=tidb); " +
625
899
  "the in-memory store cannot enforce session ownership.");
626
900
  }
901
+ // The local in-memory backend HAS an ownerOf (LocalSessionStore), so the guard above passes — but its owner map is
902
+ // process-local + lost on restart, so it cannot DURABLY enforce multi-tenant ownership (post-restart a session id is
903
+ // re-claimable by whoever attaches first). Refuse REQUIRE_PRINCIPAL on it: a real multi-tenant
904
+ // boundary needs the durable DB backend; single-user local runs with REQUIRE_PRINCIPAL=false (the principal may
905
+ // still ride for memory-scoping). The file-backed follow-on (durable owners) can revisit this.
627
906
  if (config.requirePrincipal && backend?.kind === "local") {
628
907
  throw new Error("REQUIRE_PRINCIPAL=true is not supported on the local file backend (DB_BACKEND=local): session/run CONTENT is " +
629
908
  "durable there, but OWNER attribution is process-local and lost on restart (store-backend.ts §0.5 — durable " +
@@ -631,17 +910,27 @@ async function main() {
631
910
  "(DB_BACKEND=mysql|pg) for multi-tenant, or run local single-user with REQUIRE_PRINCIPAL=false (a BFF may " +
632
911
  "still inject x-agent-principal per request for memory scoping and audit attribution).");
633
912
  }
913
+ // (c)(clay 拍 a+c,2026-07-27)云形快照 blob 姿势门:mysql|pg 后端缺 MinIO ⇒ fail-loud(bytes-in-DB
914
+ // 撞包墙已两役实证);SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true 显式逃生(单机/测试台,吃 (a) 的 per-blob 帽)。
634
915
  if (backend)
635
916
  assertCloudSnapshotBlobPosture(backend.kind, config);
917
+ // Posture warning (audit B, security.ts): an owner-aware store without REQUIRE_PRINCIPAL means the
918
+ // principal layer is optional per request. Owned sessions are still protected (the authorizer rejects
919
+ // anonymous/mismatched attach), but new headerless submissions create anonymous sessions any token
920
+ // holder can attach to — a multi-tenant deployment should set REQUIRE_PRINCIPAL=true.
636
921
  if (!config.requirePrincipal && sessionStore.ownerOf) {
637
922
  logger.warn("principal_optional", {
638
923
  note: "owner-aware session store with REQUIRE_PRINCIPAL=false — owned sessions are protected, but headerless callers can create/share anonymous sessions; set REQUIRE_PRINCIPAL=true for multi-tenant",
639
924
  });
640
925
  }
641
926
  metrics.dynamicGauge("sessions_warm", "Sessions held in the warm cache", () => sessionStore.size);
927
+ // 1.38 cross-replica circuit-breaker state: shared via TiDB when a pool exists AND the breaker is
928
+ // enabled; otherwise core's per-process Map (single replica / no DB / breaker off). Refresh loop is
929
+ // unref'd so it never holds the process open.
642
930
  const breakerState = backend?.breaker && config.resilience.circuitBreaker
643
931
  ? backend
644
932
  .breaker((streak) => {
933
+ // LOW (SILENT-FALLBACK P1): cross-replica breaker write-through failures were a bare swallow.
645
934
  metrics.setGauge("counter_flush_fail_streak", streak, { table: "circuit_breaker", kind: "write_through" });
646
935
  if (streak > 0)
647
936
  metrics.inc("breaker_writethrough_failed_total", { backend: backend.kind });
@@ -651,9 +940,15 @@ async function main() {
651
940
  logger.info("breaker_writethrough_recovered", {});
652
941
  })
653
942
  .startRefresh()
654
- : undefined;
943
+ : undefined; // local omits breaker() → core's per-process Map
655
944
  const brain = createBrain(config, { breakerState });
945
+ // ③ Cost + ④ trace + ⑤ budget (1.37): inject per-model pricing (authoritative costMicroUsd), bridge
946
+ // core's content-free trace events to /metrics, and accumulate per-principal spend into the cost
947
+ // quota (the tracer reads the principal ALS so council/team sub-task cost is attributed too).
656
948
  const pricing = buildPricing(config.models);
949
+ // S9 (SILENT-FALLBACK P0-e): write-behind flush/refresh failures previously vanished in a bare catch —
950
+ // the fleet silently degrades to per-replica counting (soft limits stop being fleet-wide). Every failure
951
+ // ticks the counter + streak gauge; warn once per episode (streak hits 3), info on the recovery edge.
657
952
  const counterDegradeHook = (info) => {
658
953
  metrics.setGauge("counter_flush_fail_streak", info.streak, { table: info.table, kind: info.kind });
659
954
  if (info.streak > 0)
@@ -663,33 +958,79 @@ async function main() {
663
958
  else if (info.streak === 0 && info.prevStreak >= 3)
664
959
  logger.info("counter_flush_recovered", { table: info.table, kind: info.kind, prevStreak: info.prevStreak });
665
960
  };
961
+ // Per-principal cumulative cost quota: TiDB-backed (cross-replica, atomic write-behind) when a pool
962
+ // exists, else in-memory per-replica (single-replica / no DB). Both satisfy QuotaTracker. (design/30 P0-1)
666
963
  const costQuota = config.maxPrincipalCostUsd > 0
667
- ? backend?.costQuota
964
+ ? backend?.costQuota // local omits costQuota() → in-memory per-process CostQuota (single-replica)
668
965
  ? backend.costQuota(Math.round(config.maxPrincipalCostUsd * 1e6), config.costQuotaWindowSec * 1000, counterDegradeHook).startRefresh()
669
966
  : new CostQuota(Math.round(config.maxPrincipalCostUsd * 1e6), config.costQuotaWindowSec * 1000)
670
967
  : undefined;
968
+ // E8 (shell-host contract): per-task × per-model usage accumulator, fed by the tracer and drained by the durable run
969
+ // lifecycle into `model_usage` events (the `TaskStats.modelUsage` echo). One instance, shared tracer↔server.
671
970
  const modelUsageTracker = new ModelUsageTracker();
971
+ // [998]② web 观测切片 A 数据面:prompt.assembled 的 manifest 经 tracer 记录、由 durable run 生命周期
972
+ // drain 成 `prompt_assembled` 事件(turns/stream 可读)。与 modelUsageTracker 同款 register 篱笆/生命周期。
672
973
  const promptManifestTracker = new PromptManifestTracker();
974
+ // fleet 用量批报的 principal×model 窗口累计器 — 仅当 fleet 面可能启用(center lane 配置了)才建,
975
+ // 否则 tracer 走 undefined = 零行为(非 fleet 部署不多攒一份内存)。真正的启动门在 startFleetClientFromEnv。
673
976
  const fleetUsage = config.configCenter && process.env.FLEET_ADVERTISE_ADDRESS ? new FleetUsageAccumulator() : undefined;
977
+ // fleet lease 消费(worker 侧第三接线,D4 AP 模式):协商式 —— center lane 配齐即启(lease 是出站消费面,
978
+ // 不需要 FLEET_ADVERTISE_ADDRESS);非 lease-enforced principal 一次 409 负缓存后走既有本地 CostQuota,
979
+ // 单用户(无 principal)提交门早退 —— 两者零行为变化。
674
980
  const fleetLease = createFleetLeaseFromEnv(config, { logger, metrics });
981
+ // weight-at-burn 回调:读 config.modelQuotaWeights(registry hot-apply 的活表;env lane 空 ⇒ 1 兜底)。
675
982
  const tracer = createTracer(metrics, costQuota, modelUsageTracker, fleetUsage, fleetLease, (m) => config.modelQuotaWeights[m] ?? 1, promptManifestTracker);
983
+ // [1469] side-query 记账 seam:与 tracer 同一装配点、同一组 sink 实例(codex R3——sideQuery 不发
984
+ // brain.call,四路 sink 必须由 server 半场喂,两臂同语义见 createSideQueryAccountant 头注)。
676
985
  const sideQueryAccounting = createSideQueryAccountant(metrics, costQuota, fleetUsage, fleetLease, (m) => config.modelQuotaWeights[m] ?? 1);
677
- const toolResultStore = backend?.toolResult ? backend.toolResult() : undefined;
986
+ // Durable offload store (core 1.47/1.49): large tool results survive a cross-replica wake. Without a
987
+ // pool, core's task-scoped in-memory default applies (graceful: cross-wake fetch misses → preview stands).
988
+ const toolResultStore = backend?.toolResult ? backend.toolResult() : undefined; // tidb/pg = SQL twins; local = core's FileToolResultStore (core 1.219 — restart-durable refs); no backend → core's in-memory default
989
+ // E6 durable SessionPolicyStore — operator-tightened per-session tool rules core reads at prepare-time (subtract-only).
990
+ // Present on every backend (incl local = core's InMemorySessionPolicyStore); undefined only on the env-only/no-backend
991
+ // worker → feature OFF (core reads no rules). Wired into the PRIMARY Runner below + the PUT/GET policy route + E21 purge.
678
992
  const sessionPolicyStore = backend ? backend.sessionPolicy() : undefined;
993
+ // E19 rewind-files — working-tree snapshot/restore keyed by entryId (the same anchor as E18), + the 2c artifact-transfer
994
+ // store. core AUTOMATICALLY snapshots each completed turn + restores on resumeAt when `spec.rewindFiles` is set, for ANY
995
+ // ExecutionEnv when this store is wired (gate-split 1.134.0 removed the isRemoteExecutionEnv skip → host/local-mode
996
+ // + e2b/k8s/ssh/adb/local-docker all active). Durable on tidb/pg (cross-replica + 2c), local = core's
997
+ // InMemoryFileSnapshotStore. Wired into the PRIMARY Runner + the `rewindFiles` cap + E21 purge.
679
998
  const fileSnapshotStore = backend ? backend.fileSnapshot() : undefined;
999
+ // [805]③a / [806]④: E19-fix observability — core 1.287 captureManifest TOLERATES enumerate→read-window
1000
+ // vanished files (skip, not fail) and reports the count ONLY in its return value; the SQL twins expose it via
1001
+ // this optional sink. Metric = the live face of "the fix is absorbing real races" (a warn would be noise — the
1002
+ // skip is the CORRECT behavior). GAP (honest): the local lane uses core's own store, which drops the count
1003
+ // internally — surfacing it there needs a core seam (noted on the board, not faked here).
680
1004
  if (fileSnapshotStore && "onSkippedVanished" in fileSnapshotStore) {
681
1005
  fileSnapshotStore.onSkippedVanished = (count) => metrics.inc("snapshot_skipped_vanished_total", {}, count);
682
1006
  }
1007
+ // design/48 v1b: deployment-level remote execution(部署级路由 + 懒汉连接). When
1008
+ // REMOTE_EXEC=e2b, this deployment is a "code-agent worker" — each task's hand runs in a per-task E2B VM,
1009
+ // provisioned lazily on first hand use (a plan-only task pays zero VM cost). Unset → in-process stub env.
1010
+ // design/48 v1b + design/61: pick the remote-exec backend by provider (peer adapters). SSH/ADB target REAL
1011
+ // systems (not isolated/suspendable) — the leader's autonomy + the design/37 gate + HITL run accordingly.
1012
+ // §7 P0.5 per-task sandbox image: the trusted-control-plane bridge from resolveSpec (resolves the requested
1013
+ // profile → digest with the caller's principal, fail-closed) to the k8s factory (applies it per-pod). See
1014
+ // per-task-image.ts + resolveSpec below. Worker-global default image is the fallback when no profile is requested.
683
1015
  const perTaskImage = new PerTaskImageRegistry();
1016
+ // RFC A2: session-keyed environment selection (PROFILE intent, never a digest) written by the model-facing
1017
+ // SelectEnvironment tool; resolveSpec re-resolves it FAIL-CLOSED per task (same re-admit as a body profile).
684
1018
  const sessionEnvSelection = new SessionEnvironmentSelection();
1019
+ // [#40 / TOC cwd seam] per-session launch dir → the host lane's agent workspace (resolveSpec registers it gated by
1020
+ // cwdHonored; the host factory reads it by ctx.sessionId). Only ever written for the single-user host lane.
1021
+ // BOUNDED (avoid unbounded growth over the persistent local engine's lifetime): an LRU cap; the shell
1022
+ // re-sends cwd on every request, so evicting a stale session is harmless (it re-registers on next use).
685
1023
  const MAX_CWD_SESSIONS = 4096;
686
1024
  const perSessionCwd = new Map();
687
1025
  const setSessionCwd = (sid, cwd) => {
688
- perSessionCwd.delete(sid);
1026
+ perSessionCwd.delete(sid); // re-insert at the tail = most-recently-used
689
1027
  perSessionCwd.set(sid, cwd);
690
1028
  if (perSessionCwd.size > MAX_CWD_SESSIONS)
691
- perSessionCwd.delete(perSessionCwd.keys().next().value);
1029
+ perSessionCwd.delete(perSessionCwd.keys().next().value); // evict oldest
692
1030
  };
1031
+ // [R-survey / TOC shellEnv seam, core PLAN批注] per-session `settings.env` → the host lane's agent shell env
1032
+ // (resolveSpec registers it gated by cwdHonored — single-user host lane only; the host factory merges it by
1033
+ // ctx.sessionId). design/107 "env = capability axis". Same LRU bound + re-send-on-every-request semantics as cwd.
693
1034
  const perSessionShellEnv = new Map();
694
1035
  const setSessionShellEnv = (sid, env) => {
695
1036
  perSessionShellEnv.delete(sid);
@@ -698,6 +1039,8 @@ async function main() {
698
1039
  perSessionShellEnv.delete(perSessionShellEnv.keys().next().value);
699
1040
  };
700
1041
  let executionEnvFactory;
1042
+ // SVC-3 worktree isolation: the reaper (defined far below) reuses ONE long-lived git base env + repoRoot to
1043
+ // `git worktree prune` crash-orphaned worktrees. Holders are populated when the wrapper is wired (host lane).
701
1044
  let worktreeReap;
702
1045
  if (config.remoteExec?.provider === "e2b") {
703
1046
  executionEnvFactory = e2bExecutionEnvFactory({
@@ -706,7 +1049,11 @@ async function main() {
706
1049
  ...(config.remoteExec.timeoutMs != null ? { timeoutMs: config.remoteExec.timeoutMs } : {}),
707
1050
  ...(config.remoteExec.livenessMs != null ? { livenessMs: config.remoteExec.livenessMs } : {}),
708
1051
  ...(config.remoteExec.allowInternetAccess != null ? { allowInternetAccess: config.remoteExec.allowInternetAccess } : {}),
1052
+ // [1452]/[1454] CWD-A: workspace 根透传(E2B_MOUNT_PATH)——缺省仍 /home/user(adapter 默认)。
709
1053
  ...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
1054
+ // RFC B5: region package-source env set rides the operator-trusted sandboxEnv — ENV-FIRST, the derived
1055
+ // ecosystem overrides (pip/uv/npm/go/rustup/flutter/…) beat the image's baked CN ENV on existing images,
1056
+ // no rebake needed. Explicit E2B_SANDBOX_ENV keys win over derived on collision (deliberate override).
710
1057
  ...(() => {
711
1058
  const merged = { ...derivePkgSourceEnv(config.sandboxPkgSource, customPkgSourceFromEnv()), ...(config.remoteExec.sandboxEnv ?? {}) };
712
1059
  return Object.keys(merged).length > 0 ? { sandboxEnv: merged } : {};
@@ -726,14 +1073,26 @@ async function main() {
726
1073
  ...(config.remoteExec.runtimeClass != null ? { runtimeClass: config.remoteExec.runtimeClass } : {}),
727
1074
  ...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
728
1075
  ...(config.remoteExec.timeoutMs != null ? { timeoutMs: config.remoteExec.timeoutMs } : {}),
1076
+ // Resource-profile knobs. config.ts reads K8S_MEMORY/K8S_CPU; this passthrough was
1077
+ // missing, so the adapter silently stayed on its 2Gi default (Kata VM = default_memory 2G + limit 2G = 4G
1078
+ // MemTotal — task02b run9/10 integration gradle OOM'd there even after the env was set).
729
1079
  ...(config.remoteExec.memory ? { memory: config.remoteExec.memory } : {}),
730
1080
  ...(config.remoteExec.cpu ? { cpu: config.remoteExec.cpu } : {}),
731
1081
  ...(config.remoteExec.s3Snapshot ? { s3Snapshot: config.remoteExec.s3Snapshot } : {}),
1082
+ // RFC B5: region package-source env set → pod container env (operator-trusted, never task-controlled).
1083
+ // ENV-FIRST: pod env beats image ENV and reaches every exec — the derived ecosystem overrides switch
1084
+ // region on existing images; the SEMA_PKG_SOURCE marker drives the in-image hook (file-bound pieces).
732
1085
  ...(() => {
733
1086
  const derived = derivePkgSourceEnv(config.sandboxPkgSource, customPkgSourceFromEnv());
734
1087
  return Object.keys(derived).length > 0 ? { podEnv: derived } : {};
735
1088
  })(),
736
1089
  };
1090
+ // §7 P0.5: per-pod image override. resolveSpec resolved the requested profile→digest (with the caller's
1091
+ // principal, fail-closed) and registered it by sessionId — the ONLY identifier stable across the factory ctx
1092
+ // on every path (adversarial-review round-2: /v1/runs mints its own durable taskId that CLOBBERS spec.taskId,
1093
+ // so taskId-keying misses on the primary path; sessionId survives). get() is non-removing (the factory may be
1094
+ // invoked >once per logical task). Absent ⇒ the worker-global k8sCfg.image. Building the factory per task is a
1095
+ // cheap closure alloc; the ref is immutable (repo@digest). podSpecPatch can NOT carry the image (hard invariant).
737
1096
  executionEnvFactory = (ctx) => k8sExecutionEnvFactory({ ...k8sCfg, image: perTaskImage.get(ctx.sessionId) ?? k8sCfg.image })(ctx);
738
1097
  }
739
1098
  else if (config.remoteExec?.provider === "ssh") {
@@ -753,10 +1112,20 @@ async function main() {
753
1112
  });
754
1113
  }
755
1114
  else if (config.remoteExec?.provider === "host") {
1115
+ // DUAL-MODE §5: the TOC `host` lane — run on THIS machine, no container (isolation=none). Fan-out still
1116
+ // works (bounded by one box). Secrets are env-NAMEs the host resolves from its own process.env.
756
1117
  {
1118
+ // R7 self-wake: inject the self-wake SchedulerCapability backend on the single-user TOC host lane
1119
+ // (opt-in SCHEDULER_ENABLED). `hasScheduler(env)` then mounts CronCreate/CronDelete/CronList(旧名 CronCancel、Sleep 已 design/136 撤除——L8 注释订正,且挂载在 core prepare-task 非 createHandsToolkit); the TOC shell
1120
+ // daemon reads the SAME ~/.sema/scheduled_tasks.json store (registry-core /node binding) and fires due intents.
1121
+ // Multi-tenant (requirePrincipal) routes scheduling to center, NOT this host daemon → gate it off there. ONE
1122
+ // backend instance at boot → every host task shares the same store file.
757
1123
  const hostScheduler = config.schedulerEnabled && config.requirePrincipal !== true
758
1124
  ? new FileSchedulerBackend({
759
1125
  ...(config.schedulerStorePath ? { storePath: config.schedulerStorePath } : {}),
1126
+ // [1009]② host-signal: the spawning shell knows whether a resident daemon will honor session
1127
+ // wakeups — SCHEDULER_SESSION_WAKEUP=false flips the capability off at CONSTRUCTION (instance-
1128
+ // lifetime snapshot, core TOCTOU 契约) so core's ScheduleWakeup refuses up front.
760
1129
  ...(config.schedulerSessionWakeup === false ? { caps: { supportsSessionWakeup: false } } : {}),
761
1130
  })
762
1131
  : undefined;
@@ -766,21 +1135,37 @@ async function main() {
766
1135
  ...(config.remoteExec.workspaceBase ? { workspaceBase: config.remoteExec.workspaceBase } : {}),
767
1136
  ...(config.remoteExec.commandTimeoutMs != null ? { commandTimeoutMs: config.remoteExec.commandTimeoutMs } : {}),
768
1137
  ...(hostScheduler ? { scheduler: hostScheduler } : {}),
1138
+ // design/103 background shell: single-user host lane only (parity with scheduler/cwd). Multi-tenant host
1139
+ // lane (which shouldn't exist — host runs on the worker's own box) → INERT (run_in_background/etc. don't mount).
769
1140
  backgroundShell: config.requirePrincipal !== true,
770
1141
  };
1142
+ // [#40 / TOC cwd seam] per-request workspace: if resolveSpec registered a caller `cwd` for this session (gated by
1143
+ // cwdHonored — single-user host lane only), run the agent VERBATIM in that dir (persistent, never deleted — the
1144
+ // user's project). Else the boot-time host config (ephemeral random subdir). Mirrors the k8s perTaskImage wrap.
1145
+ // core 1.219 `ctx.parentCwd` (dogfood finding: sub-agents landed in an EMPTY sandbox): a DELEGATED child
1146
+ // (workflow ctx.agent / Task tool) now roots at its PARENT's working dir — CC parity — via core's TRUSTED
1147
+ // RunInternals channel. Precedence in effectiveHostWorkspace: session cwd → parentCwd (unless the child asked
1148
+ // for isolation:"worktree" — that wrapper wins below) → ephemeral. Container lanes never consult this.
771
1149
  executionEnvFactory = (ctx) => {
772
1150
  const cwd = effectiveHostWorkspace(perSessionCwd.get(ctx.sessionId), ctx);
773
1151
  const shellEnv = perSessionShellEnv.get(ctx.sessionId);
774
1152
  const env = hostExecutionEnvFactory({
775
1153
  ...hostCfg,
776
1154
  ...(cwd ? { workspaceDir: cwd } : {}),
777
- ...(shellEnv ? { env: shellEnv } : {}),
1155
+ ...(shellEnv ? { env: shellEnv } : {}), // R-survey: per-task shell env (merged UNDER per-command options.env by the host adapter)
778
1156
  })(ctx);
1157
+ // core 1.191: an EPHEMERAL host workspace (no honored `cwd` → a per-task `sema-host-<id>` dir destroy()
1158
+ // rm's) evicts its LSP servers at task-end so a busy server doesn't accumulate one heavy language server per
1159
+ // task. cwdHonored (persistent project) is NOT evicted — its stable root stays warm across turns (CC-parity);
1160
+ // worktree/fan-out is bounded by maxSessions=16. Only wraps when a NodeLspManager is actually active.
779
1161
  return lspManager && !cwd ? evictLspOnDestroy(env, lspManager) : env;
780
1162
  };
781
1163
  }
782
1164
  }
783
1165
  else if (config.remoteExec?.provider === "local-docker") {
1166
+ // DUAL-MODE §5: the TOC `local-docker` lane — a per-task container on THIS machine's docker daemon
1167
+ // (isolation:true, suspendable:false). The base image comes ENTIRELY from config (no docker.io default —
1168
+ // domestic-images iron rule). Secrets are resolved env-NAME→value here (config.ts already did the forward).
784
1169
  executionEnvFactory = localDockerExecutionEnvFactory({
785
1170
  image: config.remoteExec.image,
786
1171
  ...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
@@ -793,10 +1178,24 @@ async function main() {
793
1178
  ...(config.remoteExec.env ? { env: config.remoteExec.env } : {}),
794
1179
  });
795
1180
  }
1181
+ // [848] remote scratchpad: decorate the REMOTE sandbox lanes (e2b/k8s/local-docker/ssh) so each env
1182
+ // lazily `mkdir -p`s `/tmp/scratchpad/<sessionId>` on its FIRST exec — never at factory time (e2b is
1183
+ // lazy-VM; a factory-time exec would force-boot the VM). adb is skipped (no standard /tmp on Android);
1184
+ // host keeps its worker-local scratchpad (ensureScratchpadDir at the envFacts consumer). The envFacts
1185
+ // advertisement below uses the SAME remoteScratchpadDirFor rule, so fact and mkdir can never disagree.
796
1186
  if (executionEnvFactory && isRemoteScratchpadLane(config.remoteExec?.provider)) {
797
1187
  executionEnvFactory = withRemoteScratchpad(executionEnvFactory, logger);
798
1188
  }
1189
+ // SVC-3 (design/97 CORE-6): wrap the factory with git-worktree isolation. When core marks an agent
1190
+ // `isolation:"worktree"` (TRUSTED RunInternals — never a TaskSpec), the wrapper mints a per-agent detached
1191
+ // worktree under the operator-trusted repoRoot; otherwise it passes the base env through untouched. v1
1192
+ // covers the `host` lane (the TOC fan-out lane). Other lanes would need a worktree-ROOTED REMOTE env from
1193
+ // `rootEnvAt` (a follow-on; a host/NodeExecutionEnv worktree is not durable-suspendable — see the plugin
1194
+ // header) — they are left unwrapped here, isolation:"worktree" is then a no-op for them (the base env wins).
799
1195
  if (config.worktreeIsolation && executionEnvFactory) {
1196
+ // M4 (adversarial-review): a mis-wired WORKTREE_REPO_ROOT must fail LOUD at boot, not per-task at runtime. The
1197
+ // root must exist AND be a git repo (`.git` dir or worktree-link file). On a bad root, log an error + LEAVE
1198
+ // isolation OFF (the factory unwrapped) rather than wiring a factory that throws on every isolated agent.
800
1199
  const wtRoot = config.worktreeIsolation.repoRoot;
801
1200
  const wtValid = existsSync(wtRoot) && existsSync(join(wtRoot, ".git"));
802
1201
  if (config.remoteExec?.provider === "host" && !wtValid) {
@@ -807,29 +1206,56 @@ async function main() {
807
1206
  });
808
1207
  }
809
1208
  else if (config.remoteExec?.provider === "host") {
1209
+ // The SHARED base env that runs `git worktree add/remove/prune` (cwd:repoRoot is passed explicitly by
1210
+ // core on every git call, so this env's own root is irrelevant — it just needs `exec` + git on PATH).
1211
+ // ONE long-lived env per deployment; the reaper reuses it. `inheritEnv:"all"` so the operator's git
1212
+ // config/credentials are visible (this is the operator's own machine + own repo, the TOC posture).
1213
+ // systematic-audit (gate-fail-direction): every worktree-lane RemoteHostExecutionEnv carries the SAME
1214
+ // background-shell gate as the primary host factory (main.ts ~442) — not the env's `?? true` default. The
1215
+ // git-base env is git-ops only (agents run on rootEnvAt below), but gating it keeps the policy uniform / future-proof.
810
1216
  const gitBaseEnv = new RemoteHostExecutionEnv({ workspaceDir: config.worktreeIsolation.repoRoot, inheritEnv: "all", backgroundShell: config.requirePrincipal !== true });
811
1217
  executionEnvFactory = withWorktreeIsolation(executionEnvFactory, {
812
1218
  repoRoot: config.worktreeIsolation.repoRoot,
813
1219
  ...(config.worktreeIsolation.allowedRoots ? { allowedRoots: config.worktreeIsolation.allowedRoots } : {}),
814
1220
  ...(config.worktreeIsolation.commit ? { commit: config.worktreeIsolation.commit } : {}),
815
1221
  baseEnvForGit: gitBaseEnv,
1222
+ // Host-lane worktree-rooted env: a host adapter in PERSISTENT-DIR mode — its own destroy() does NOT
1223
+ // rm the dir (so core's `git worktree remove` owns teardown), and it keeps the host adapter's secret
1224
+ // scrub + timeout semantics (vs a bare NodeExecutionEnv). inheritEnv defaults to "scrub" (model-driven).
816
1225
  rootEnvAt: (dir) => {
817
1226
  const env = new RemoteHostExecutionEnv({ workspaceDir: dir, backgroundShell: config.requirePrincipal !== true });
1227
+ // A worktree root is per-agent + ephemeral (core's `git worktree remove` reclaims it at
1228
+ // task-end), so evict its LSP servers DETERMINISTICALLY too — not just via the maxSessions LRU backstop. The
1229
+ // worktree dir IS the LSP cache root (env.cwd), so evict(env.cwd) on destroy hits it. (`lspManager` is declared
1230
+ // below but this closure only runs per-task, well after it's initialized — same forward-ref as the host factory.)
818
1231
  return lspManager ? evictLspOnDestroy(env, lspManager) : env;
819
1232
  },
820
1233
  logger,
821
1234
  });
822
1235
  worktreeReap = () => reapOrphanWorktrees(gitBaseEnv, config.worktreeIsolation.repoRoot, logger);
823
1236
  logger.info("worktree_isolation_enabled", { provider: "host", repoRoot: config.worktreeIsolation.repoRoot });
1237
+ // BOOT recovery sweep: reap any worktrees orphaned by a crash BEFORE this process started (the periodic
1238
+ // reaper would otherwise wait a full interval). Best-effort, fire-and-forget; never blocks boot.
824
1239
  void worktreeReap();
825
1240
  }
826
1241
  else {
1242
+ // Not a no-op-silent: an operator who set WORKTREE_ISOLATION_ENABLED on a non-host lane should know it
1243
+ // doesn't take effect yet (the worktree-rooted REMOTE env is a follow-on). Fail-loud-ish via a warning.
827
1244
  logger.warn("worktree_isolation_unsupported_lane", {
828
1245
  provider: config.remoteExec?.provider,
829
1246
  note: "git-worktree isolation v1 covers only the 'host' lane — isolation:'worktree' is a no-op here (the base env is used). Worktree-rooted remote envs are a follow-on.",
830
1247
  });
831
1248
  }
832
1249
  }
1250
+ // SendUserFile v2(clay 拍 2026-07-14,Plan B 直传):沙箱 lane 的文件源需要「工具执行时拿到当前
1251
+ // 任务的 env」——core 把 per-task env 关在 prepare-task 闭包里(ToolExecuteContext 无 env 面),但
1252
+ // 工厂是我们装配的、工厂 ctx 自带 taskId,包一层登记 taskId→env(destroy 时注销)即可,零 core 改动
1253
+ // (withWorktreeIsolation 同款包装先例)。lane 门+租户门=sandboxSendLaneEnabled(判定依据在其 doc):
1254
+ // e2b/k8s=一任务一沙箱(沙箱文件系统=租户边界)任意租户可用;ssh(后补 2026-07-14)=同一 exec 契约
1255
+ // (timeout 秒×1000/Result shape 同形)直传链零适配,但 peer=一台跨任务共享的常驻真实主机
1256
+ // (sshExecutionEnvFactory 忽略 ctx、destroy=断连不删文件、isolation:false)→「沙箱 scoped 读=租户
1257
+ // 隔离」不成立,与 host 同门单用户 only(requirePrincipal!==true 恒关多租户,不留口子)。
1258
+ // 其余 lane 不包(零开销);签发面未配同样不包。
833
1259
  const sendUserFileTaskEnvs = (() => {
834
1260
  const provider = config.remoteExec?.provider;
835
1261
  if (!config.sendUserFile || !executionEnvFactory || !sandboxSendLaneEnabled(provider, config.requirePrincipal))
@@ -838,6 +1264,11 @@ async function main() {
838
1264
  executionEnvFactory = registry.wrapFactory(executionEnvFactory);
839
1265
  return registry;
840
1266
  })();
1267
+ // D-1 附件物化包装(半场③):env 每次建立(fresh + resume/重建沙箱)都把该 session 绑定的附件写进
1268
+ // 工作目录 `attachments/`。全 lane 统一(host/e2b/k8s/ssh/adb 都走 factory;ExecutionEnv.writeFile
1269
+ // 双向通用);路径与 objective 告知共用 materializedRelPaths(确定性,两处不漂)。**fail-loud**:
1270
+ // objective 已向模型宣告文件在场,静默缺文件=模型按幻影文件行动,比任务失败更糟 ⇒ 写失败即抛
1271
+ // (env 建立失败,任务带明确错误)。链尾最外层:worktree 隔离等内层先定 cwd,附件落在最终 cwd。
841
1272
  if (taskAttachmentStore && executionEnvFactory) {
842
1273
  const inner = executionEnvFactory;
843
1274
  const attStore = taskAttachmentStore;
@@ -850,43 +1281,103 @@ async function main() {
850
1281
  };
851
1282
  }
852
1283
  if (executionEnvFactory) {
1284
+ // k8s (Kata) is isolated; it becomes WORKSPACE-suspendable when an S3 snapshot store is configured
1285
+ // (tar→S3→fresh pod restore — files durable, in-VM memory not); e2b is fully suspendable.
853
1286
  const isolated = config.remoteExec.provider === "e2b" ||
854
1287
  config.remoteExec.provider === "k8s" ||
855
- config.remoteExec.provider === "local-docker";
1288
+ config.remoteExec.provider === "local-docker"; // a container is a real OS-level isolation boundary
856
1289
  const suspendable = config.remoteExec.provider === "e2b" ||
857
1290
  (config.remoteExec.provider === "k8s" && !!config.remoteExec.s3Snapshot);
1291
+ // Out-of-band sandbox env (e2b): log the KEY NAMES only (never the secret values) so ops can confirm
1292
+ // injection is active without leaking the credential.
858
1293
  const sandboxEnvKeys = config.remoteExec.provider === "e2b" ? Object.keys(config.remoteExec.sandboxEnv ?? {}) : [];
859
1294
  logger.info("remote_exec_enabled", { provider: config.remoteExec.provider, isolated, suspendable, ...(sandboxEnvKeys.length > 0 ? { sandboxEnvKeys } : {}) });
860
1295
  if (!isolated)
861
1296
  logger.warn("remote_exec_real_system", { provider: config.remoteExec.provider, note: "non-isolated target — actions are permanent; rely on the policy gate + HITL (design/61 §5)" });
862
1297
  }
1298
+ // LSP sidecar (design/64 §13.1, the 1.86.2 seam): ONE stateless manager on RunnerDeps — core passes each
1299
+ // task's env into sessionFor at the tool mount point, so the manager reaches the SAME sandbox the agent edits
1300
+ // (no sessionId registry / per-task construction). Opt-in (LSP_ENABLED — the SANDBOX lane's knob; the host lane
1301
+ // below has its own, LSP_HOST_ENABLED, because its default is the opposite); with the baked `sema-code-lsp`
1302
+ // template the first call is fast, without it the language server installs on first use (slow once per sandbox).
1303
+ // k8s lane (gate#2 alignment): same manager/bridge over the pod network — `ws://podIP:port` instead of E2B's
1304
+ // public wss proxy, so it needs an IN-CLUSTER worker and a sandbox image with node + the language servers baked
1305
+ // (no on-the-fly npm path on Kata; a miss degrades the lsp tool gracefully, same as E2B).
863
1306
  const lspProvider = config.remoteExec?.provider;
864
1307
  const lspManager = (lspProvider === "e2b" || lspProvider === "k8s") && config.lspEnabled
865
1308
  ? createE2bLspManager({ log: (event, fields) => logger.info(event, fields), scheme: lspProvider === "k8s" ? "ws" : "wss" })
866
- :
1309
+ : // TOC local LSP (core 1.190): the host lane runs on THIS machine, so core's `NodeLspManager` spawns the
1310
+ // language server as a LOCAL child_process over stdio (CC `services/lsp` parity). Its default resolveRoot uses
1311
+ // `env.cwd` — and core passes each task's executionEnv (the per-agent WORKTREE env, so the server roots in the
1312
+ // worktree not the base repo) into sessionFor. host ONLY: ssh/adb target another host/device and local-docker a
1313
+ // container, none of which a local child_process can reach (those keep the E2B-style bridge / stay degraded).
1314
+ // 🔒 `requirePrincipal !== true` gate = uniform with the sibling host powers (scheduler/backgroundShell/
1315
+ // loadProjectMemory): the host lane is single-user by design; a (discouraged) multi-tenant host fails SAFE to
1316
+ // grep/read rather than fanning a per-tenant×language×root pool of heavy language servers (double-review B).
867
1317
  lspProvider === "host" && config.lspHostEnabled && config.requirePrincipal !== true
868
1318
  ? new NodeLspManager({ log: (event, fields) => logger.info(event, fields) })
869
1319
  : undefined;
870
1320
  if (lspManager)
871
1321
  logger.info("lsp_enabled", { provider: lspProvider });
1322
+ // prove-before-scale stopgap: a no-op tool tracer (logs every tool call incl. resumed legs that
1323
+ // /events hides) so we can diagnose single-agent coding behaviour. Off unless TOOL_TRACE=true.
872
1324
  const toolTracer = config.toolTrace ? createToolTracer(logger) : undefined;
873
1325
  if (toolTracer)
874
1326
  logger.info("tool_trace_enabled", {});
1327
+ // Goal B4 (2026-07-08): the ALWAYS-ON gate-deny meter (`permission_denied_total{source}`) — an
1328
+ // operational/security signal that must not sit behind the TOOL_TRACE diagnostic switch. Composed
1329
+ // UNDER the tracer (deployment slot order: meter first, tracer second — composeHooks double-runs the
1330
+ // void permissionDenied slot); this composite is what every hooks site below uses as the deployment
1331
+ // baseline, so a task-hooks fold (resolveSpec) shadows neither.
875
1332
  const deploymentHooks = toolTracer ? composeHooks(createPermissionDeniedMeter(metrics), toolTracer) : createPermissionDeniedMeter(metrics);
1333
+ // S8 / SVC-1 workflow run store (design/97 S1b): records LLM-authored BACKGROUND workflow
1334
+ // runs for the /v1/workflows list+detail view (live subscribe is in-process via subscribeWorkflow,
1335
+ // store-independent). Only present when self-orchestration is enabled (else no runs to record).
1336
+ //
1337
+ // SVC-1: the DEFAULT is now `FileWorkflowRunStore` (durable, crash-safe ledger) — NOT InMemory — so a
1338
+ // background workflow's record SURVIVES a replica restart, which is the prerequisite for the at-least-once
1339
+ // completion notify below to re-derive a run's terminal state after a crash. InMemory stays available only for
1340
+ // an explicit ephemeral opt-out (WORKFLOW_RUN_STORE=memory); a durable cross-replica TiDB/PG WorkflowRunStore
1341
+ // is a clean follow-on port (the workflowRunStoreContract makes it a drop-in like the run-store).
1342
+ // P1 (fleet failover, 2026-07-05): `auto` (the default) prefers the SQL backend's cross-replica twin when
1343
+ // present — a failover-landed session sees the run's history + its pending completion push on ANY replica.
1344
+ // Explicit WORKFLOW_RUN_STORE=file/memory still wins (single-box File posture unchanged: local backend has
1345
+ // no workflowRun() so auto falls to File there).
876
1346
  const sqlWorkflowRunStore = config.workflowRunStoreBackend === "auto" ? backend?.workflowRun?.() : undefined;
877
1347
  const baseWorkflowRunStore = config.selfOrchestrationEnabled
878
1348
  ? config.workflowRunStoreBackend === "memory"
879
1349
  ? new InMemoryWorkflowRunStore()
880
1350
  : (sqlWorkflowRunStore ?? new FileWorkflowRunStore(join(config.localDataRoot ?? localRoot, "workflows")))
881
1351
  : undefined;
1352
+ // SVC-1 at-least-once completion-notify TRUST GATE (the half that is service's, [[core-service-boundary]]):
1353
+ // core's `run_workflow` fires an in-process at-MOST-once notify on terminal — a crash between terminal and the
1354
+ // receiver loses it. The gate makes it at-LEAST-once: a durable notify-journal records every STARTED run
1355
+ // (observed via the JournalingWorkflowRunStore decorator's `put`), the LIVE notify path delivers-then-acks, and
1356
+ // a BOOT recovery sweep re-derives every un-acked run's terminal state from the run store + re-delivers (the
1357
+ // receiver is idempotent on runId). The default journal is the zero-dependency crash-safe File ledger; with no
1358
+ // run store (self-orchestration off) there's nothing to journal, so the gate is absent.
1359
+ // 1.108 review fix (lens③ HIGH): the journal FOLLOWS the run-store axis like the inbox — a SQL run store +
1360
+ // inbox with a replica-LOCAL File journal meant a replica that died holding an un-acked notify stranded it
1361
+ // forever (no surviving replica could recover it: at-least-once silently degraded to at-most-once across
1362
+ // replica death — the exact fleet-failover gap P1 exists to close).
882
1363
  const workflowNotifyJournal = config.selfOrchestrationEnabled && config.workflowRunStoreBackend !== "memory"
883
1364
  ? (sqlWorkflowRunStore ? backend.notifyJournal() : new FileWorkflowNotifyJournalStore(join(config.localDataRoot ?? localRoot, "workflows")))
884
1365
  : undefined;
1366
+ // P1 ①②: the async-workflow COMPLETION INBOX — the push half. A finished background
1367
+ // workflow's completion is enqueued here keyed by the ORIGINATING session, and drained + emitted as a
1368
+ // `workflow_complete` out-of-band SSE frame when that session next opens a stream (see server.ts). File-backed
1369
+ // (crash-safe) when self-orchestration is on with a durable store; the ephemeral opt-out uses in-memory.
1370
+ // P1: the inbox FOLLOWS the run-store choice (one axis, no split-brain: a SQL run record with a File inbox
1371
+ // would re-open the cross-replica double-push the fence rows exist to close).
885
1372
  const workflowCompletionInbox = config.selfOrchestrationEnabled
886
1373
  ? config.workflowRunStoreBackend === "memory"
887
1374
  ? new InMemoryWorkflowCompletionInbox((msg, meta) => logger.warn(msg, meta))
888
1375
  : (sqlWorkflowRunStore ? backend.completionInbox((msg, meta) => logger.warn(msg, meta)) : new FileWorkflowCompletionInbox(join(config.localDataRoot ?? localRoot, "workflows"), (msg, meta) => logger.warn(msg, meta)))
889
1376
  : undefined;
1377
+ // The deployment's REAL completion delivery: log + meter, THEN push into the originating session's inbox so the
1378
+ // model that launched the workflow learns it finished (the `WorkflowStatus` poll is the deterministic floor;
1379
+ // this is the proactive push). The gate guards this so it fires at-least-once + at-most-once-per-run-steady.
1380
+ // `runStore` (below) is resolved at CALL time (post-boot) — a forward reference into the same boot scope.
890
1381
  const deliverWorkflowCompletion = async (p) => {
891
1382
  logger.info("workflow_completed", {
892
1383
  runId: p.runId,
@@ -894,12 +1385,20 @@ async function main() {
894
1385
  ...(p.sourceTaskId ? { sourceTaskId: p.sourceTaskId } : {}),
895
1386
  });
896
1387
  metrics.inc("workflow_runs_total", { status: p.status });
1388
+ // Routing (core 1.208): session = payload's `originatingSessionId` (lookup-free); owner = run-row
1389
+ // owner → payload `principal` (= VERIFIED spec.principal, the 1.55 F-fix invariant — covers the sync/resume
1390
+ // legs whose sourceTaskId is a sessionId and misses getRun) → multi-tenant fail-closed /
1391
+ // single-user null. Full rationale on `resolveCompletionRoute` (tested there).
897
1392
  if (!workflowCompletionInbox)
898
1393
  return;
899
1394
  try {
900
1395
  const route = await resolveCompletionRoute(p, runStore ? (id) => runStore.getRun(id) : undefined, config.requirePrincipal === true);
901
1396
  if (!route)
902
- return;
1397
+ return; // no route / unverifiable owner → the WorkflowStatus poll floor covers it
1398
+ // `p.summary` is HUMAN-READABLE at the source since core 1.232 (completed lane = bounded
1399
+ // one-liner with name/elapsed/agents + a TaskOutput pointer; failed lane = the bounded redacted error)
1400
+ // — pass through verbatim, single-sourced (the 1.101.0 service-side re-wrap was superseded by core's
1401
+ // own half and removed to avoid two drifting formats).
903
1402
  await workflowCompletionInbox.enqueue({
904
1403
  sessionId: route.sessionId,
905
1404
  owner: route.owner,
@@ -918,33 +1417,93 @@ async function main() {
918
1417
  onError: (stage, runId, err) => logger.warn("workflow_notify_failed", { stage, runId, err: String(err) }),
919
1418
  })
920
1419
  : undefined;
1420
+ // MF-Fleet (data contract): the in-process fleet aggregation bus backing GET /v1/fleet/stream. Created HERE
1421
+ // (before the workflow run store) so the JournalingWorkflowRunStore can publish workflow rows to it. Always
1422
+ // present (lightweight); the run (POST /v1/runs) + workflow lifecycle publish per-row deltas. Replica-local.
921
1423
  const fleetBus = new FleetEventBus();
1424
+ // The store handed to core (+ the /v1/workflows reader): ALWAYS wrapped when a run store exists ([1262]
1425
+ // fleet decoupling) — the wrap publishes the workflow's MF-Fleet row on each put/update (the write-observation
1426
+ // point the shell's /workflows panel lives on), and ADDITIONALLY journals the start-time `put` when the notify
1427
+ // gate is active (durable backends). The old `baseWorkflowRunStore && workflowNotifyGate` guard silently
1428
+ // dropped the FLEET half on the memory backend (gate needs a durable journal; fleet needs neither) — clay's
1429
+ // workflow panel went empty on exactly that shape.
922
1430
  const workflowRunStore = baseWorkflowRunStore
923
1431
  ? new JournalingWorkflowRunStore(baseWorkflowRunStore, workflowNotifyGate, fleetBus)
924
1432
  : undefined;
1433
+ // SVC-2 (core CORE-7/CORE-9 Part A): the durable resume journal. core 1.145.0 added `RunnerDeps.workflowJournalStore`
1434
+ // (twin of workflowRunStore) + auto-wires it into the run_workflow tool's startWorkflow(RunWorkflowOptions.journalStore);
1435
+ // the tool's `resumeFromRunId` input then replays the longest unchanged prefix. tidb/pg = durable cross-replica resume;
1436
+ // local = core's InMemory (single-process — in-process resume only, not restart-durable). Only when self-orchestration
1437
+ // is on AND a backend is present (the load-bearing journal needs a real store).
925
1438
  const workflowJournalStore = config.selfOrchestrationEnabled && backend ? backend.workflowJournal() : undefined;
1439
+ // design/73 §1 consumption sink (see RunnerDeps.onTaskOutcome below). Present whenever a StoreBackend is —
1440
+ // NOT gated on self-orchestration (goal-mode/harness emissions are orthogonal to workflows).
926
1441
  const outcomeSink = backend?.outcomeSink();
1442
+ // SVC-5 (CORE-9 Part B): the process-local registry of STEERABLE workflow-agent handles live on THIS replica,
1443
+ // backing POST /v1/workflows/:id/agents/:label/steer. core 1.145.0 added `RunnerDeps.onWorkflowAgentSpawn` (the
1444
+ // opt-in deployment handle-sink): when set, the run_workflow tool's `agent()` runs steerable + emits the handle
1445
+ // HERE (the handle is a core object handed only to the trusted deployment sink — it NEVER enters the script/runner,
1446
+ // so the script-sandbox host-context membrane stays closed). We register by runId+label; the steer route looks it up + calls
1447
+ // handle.steer (core fences via CORE-5) behind our steer-in redaction + owner gate.
927
1448
  const workflowAgentRegistry = config.selfOrchestrationEnabled ? new WorkflowAgentRegistry() : undefined;
1449
+ // C2 (core 1.219): the replica-local registry of STEERABLE Task-subagent handles, backing
1450
+ // POST /v1/runs/:runId/subagents/:target/steer. Core emits a handle to the per-run `onSubagentSpawn` sink
1451
+ // (wired in each run leg) the moment a SYNC delegation spawns; the handle never reaches the model. Always
1452
+ // present (lightweight Map) — the sink is only threaded where a durable runId exists to address it by.
928
1453
  const subagentSteerRegistry = new SubagentSteerRegistry();
1454
+ // E23 (shell-host contract): inbound MCP elicitation coordinator (live-only HITL). Present ONLY when MCP_ELICITATION_ENABLED
1455
+ // — absent ⇒ onElicit is not wired ⇒ core advertises no elicitation capability to any server (fail-closed). Shared
1456
+ // by the runner (the onElicit seam) and the HTTP layer (the respond route + the per-run ALS context wraps).
929
1457
  const elicitation = config.mcpElicitation.enabled ? new ElicitationCoordinator(config.mcpElicitation.throttle) : undefined;
1458
+ // §4④: AskUserQuestion LIVE-stream HITL coordinator — the sibling of `elicitation`. Present ⇒ wired onto
1459
+ // `RunnerDeps.onQuestion` (core mounts the AskUserQuestion tool + routes live asks here) AND consumed by the HTTP layer
1460
+ // (the `POST /v1/questions/:id/respond` route + the per-run ALS context wraps on the streaming legs). Absent ⇒ core
1461
+ // mounts the tool with the headless default (a run never hangs; the model just can't get a live answer).
930
1462
  const question = config.askQuestionEnabled ? new QuestionCoordinator() : undefined;
1463
+ // [816]/[820]②: the live tool-approval HITL coordinator — the sibling of `question` on core's `RunnerDeps.onAsk`
1464
+ // seam (1.290 sync-ask leg). Present ⇒ a policy `ask` on a LIVE streaming leg becomes a `tool_approval` frame the
1465
+ // shell renders as the CC three-choice card, answered via POST /v1/tool-approvals/:id/respond.
1466
+ // [1535] 正名(旧文「background/workflow ⇒ headless auto-deny stands」已过时):宿主 sync 腿现同时装
1467
+ // `spec.onAsk = boundAsk(ctx)`(server.ts 装配点)——core 继承链把闭包冻给委派子代,**bg/嵌套子代的
1468
+ // ask 浮到宿主 live 流**(带 sourceTaskId);真正无宿主流的腿(durable-submit/headless resume)才留
1469
+ // 「unavailable → durable park;无 park 设施 core fail-closed deny」。durable 部署 G1 语义不变。
931
1470
  const toolApproval = config.toolApprovalEnabled ? new ToolApprovalCoordinator() : undefined;
1471
+ // [1.294 G1] durable 姿势必须在 runnerDeps 字面量之前可判(onAsk 的「在场性」本身就是 core suspendAsk 的
1472
+ // 分路条件,不能再靠闭包惰性读)——判据与 checkpointStore 的构造条件同源(backend?.checkpoint 存在 ∧
1473
+ // DURABLE_APPROVAL;store 本体已上移至 subRunner 构造前,[1535] 断点② 双 Runner 挂载)。
932
1474
  const durableEnabled = backend?.checkpoint !== undefined && config.durableApproval === true;
1475
+ // SendUserFile(真 CC 契约,clay 2026-07-14)两种 lane 形态,其余缺席=诚实(工具不进 roster):
1476
+ // - host lane:本地盘读+服务端上传,【单用户 only】——host 多租户=任意路径读→公网 URL 的 exfil 面,
1477
+ // clay 拍永久关死,不留口子。路径=绝对或相对 process.cwd(用户目录语义,与 CC 一致)。
1478
+ // - e2b/k8s lane(v2,Plan B 直传):沙箱内 stat+`curl -T` 打 server 预签的单对象 PUT(字节不中转、
1479
+ // 凭据不进沙箱);沙箱自身文件系统=租户边界,故任意租户可用。
1480
+ // - ssh lane(后补 2026-07-14):同一直传链,但 peer=跨任务共享的真实主机(非租户边界)→
1481
+ // 【单用户 only】,租户门在 sandboxSendLaneEnabled(多租户下注册表不建,工具诚实缺席)。
1482
+ // adb/local-docker 仍后补。
1483
+ // 签发面缺 S3_PUBLIC_ENDPOINT 时 issue-time fail-loud(工具在但用即报修法);100MiB/文件帽在各 send 面。
933
1484
  const sendUserFileEmitter = (() => {
934
1485
  if (!config.sendUserFile)
935
1486
  return undefined;
936
1487
  if (sendUserFileTaskEnvs)
937
- return new SendUserFileEmitter();
1488
+ return new SendUserFileEmitter(); // e2b/k8s/ssh(单用户):注册表已就位(lane+租户+签发面判定)
1489
+ // unset REMOTE_EXEC(in-process/TOC 本地形态)有意归入 host 语义:exec 与文件都在本机,单用户门
1490
+ // 同样把关(codex 复审提出收紧到显式 "host";判定=不收——unset 单机+配了 S3 是合法 dev/TOC 场景,
1491
+ // 且与 1.187.0 已发行为一致;真正的红线是 requirePrincipal 多租户,这里恒关)。
938
1492
  const provider = config.remoteExec?.provider ?? "host";
939
1493
  if (provider === "host" && config.requirePrincipal !== true)
940
1494
  return new SendUserFileEmitter();
941
1495
  return undefined;
942
1496
  })();
1497
+ // SendUserFile 账本(多租户治理面,[768]②a 后半):key 里的 scope 段是哈希(URL 不泄 principal),
1498
+ // 按租户列/撤销的 scope↔对象映射记在 server 侧账本(四后端孪生,sendfile_link / JSONL)。有 backend 即建
1499
+ // (签发面也配了才有写点);无 backend(env-only worker)= 无账本 = 不记账不 501 治理面(诚实缺席,
1500
+ // capabilities.sendUserFileLedger=false)。
943
1501
  const sendFileLedger = config.sendUserFile && backend ? backend.sendFileLedger() : undefined;
944
1502
  const sendUserFileToolSpec = (() => {
945
1503
  if (!sendUserFileEmitter || !config.sendUserFile)
946
1504
  return undefined;
947
1505
  const issuer = createSendUserFileIssuer(config.sendUserFile);
1506
+ // 两条 lane 的 send 都把 ctx.principal(core VERIFIED)传给签发面 → key 带哈希 scope 段 + link 携原文 scope。
948
1507
  const rawSend = sendUserFileTaskEnvs
949
1508
  ? createSandboxFileSend({
950
1509
  registry: sendUserFileTaskEnvs,
@@ -959,10 +1518,13 @@ async function main() {
959
1518
  if (st.size > SEND_USER_FILE_MAX_BYTES)
960
1519
  throw new Error(`file exceeds the ${Math.floor(SEND_USER_FILE_MAX_BYTES / (1024 * 1024))} MiB per-file cap (${st.size} bytes)`);
961
1520
  const bytes = await fsReadFile(resolved);
1521
+ // 读后复验(codex 复审 MED):stat→read 间文件可增长——上传的是读到的字节,帽必须按真实字节数把关。
962
1522
  if (bytes.byteLength > SEND_USER_FILE_MAX_BYTES)
963
1523
  throw new Error(`file exceeds the ${Math.floor(SEND_USER_FILE_MAX_BYTES / (1024 * 1024))} MiB per-file cap (${bytes.byteLength} bytes)`);
964
1524
  return issuer.issue(bytes, basename(resolved), undefined, sendCtx.principal);
965
1525
  };
1526
+ // 账本写点(两 lane 同语义):上传+verify 成功后、file_link 帧前记账;记账失败=该文件 fail-loud +
1527
+ // best-effort 删已传对象(治理面必须有行;删失败 warn)。无账本(env-only)= 直通,行为与 1.189 一致。
966
1528
  const send = sendFileLedger
967
1529
  ? withLedgerRecording(rawSend, {
968
1530
  ledger: sendFileLedger,
@@ -972,11 +1534,43 @@ async function main() {
972
1534
  : rawSend;
973
1535
  return sendUserFileTool({ send, emitter: sendUserFileEmitter });
974
1536
  })();
1537
+ // design/99 §K (core 1.157 `RunnerDeps.runtimeCapsResolver`) — the ENFORCE last-link of the
1538
+ // three-stage workflows gate. core calls it once per task at prepare time with `spec.principal`; we resolve the
1539
+ // per-principal entitlement from center's `GET /api/config/effective?principal=` (cached + fail-closed). Wired
1540
+ // ONLY when a center is configured AND not in dry-run: in dry-run the service pulls+logs but does NOT APPLY
1541
+ // center config, so it must NOT ENFORCE center's per-principal caps either (enforcing would deny workflows = a
1542
+ // real behavior change, contradicting observe-only). Absent ⇒ core sees `undefined` ⇒ no per-principal
1543
+ // restriction (the deployment-level capability + toolPolicy gates still govern).
1544
+ //
1545
+ // 🔐 TRUST DEPENDENCY: the gate keys off `spec.principal` (= resolveSpec's `auth.principal`). F-fix (2026-07-01,
1546
+ // core-steered; contract-compliance per core 1.187's hardened `TaskSpec.principal` doc — it MUST be the
1547
+ // cryptographically VERIFIED identity, never a spoofable header): `auth.principal` now comes from
1548
+ // `verifiedPrincipal` (security.ts). On a GATED door that's `principalFrom` (the BFF already verified the header;
1549
+ // `gatedPrincipal === principalFrom` there); on a DIRECT door (`directDoorActive`) it's the verified
1550
+ // `x-approval-principal-token` JWT `sub`, NEVER the spoofable header. So `spec.principal` is ALWAYS the verified
1551
+ // identity — governance (this gate), cost, and isolation key off it safely on BOTH postures. The same verified
1552
+ // identity also fans out to the non-authorizer owner/cost sites that don't run createAuthorizer
1553
+ // (runOwnerOk / quotaExceeded / leader / idemKey — they call `gatedPrincipal` directly). The invariant:
1554
+ // spec.principal AND every principal-keyed security decision are VERIFIED. (Was a real direct-door spoof hole
1555
+ // before this fix; latent until a multi-tenant direct door ships, but it is the only wall against it.)
1556
+ // wpt_ scoped-token: RESOLVED (center ruling, sema-registry `aeb4979`). The earlier 1.14.0 guard
1557
+ // (skip the resolver on a `wpt_` token, because center 403'd it for `?principal=`) is GONE — center now lets a
1558
+ // worker-scoped `wpt_<self>` resolve its OWN worker's principal caps when we pass `?worker=<self>` (caps-only,
1559
+ // never others' config). So we ALWAYS wire the resolver (configCenter + !dryRun) and pass `worker` =
1560
+ // `SEMA_REGISTRY_WORKER` — it authorizes the wpt_ path and is harmless on a full token (caps are worker-
1561
+ // independent). Per-principal workflow gating now truly works on the scoped-token topology (no more silent
1562
+ // fleet-wide deny). (A wpt_ with no worker still 403s → fail-closed deny, the correct degrade.)
1563
+ // MISCONFIG diagnostic (review MEDIUM): a wpt_<self> token with NO SEMA_REGISTRY_WORKER will 403 every caps
1564
+ // fetch → fail-closed DENY ALL workflows (correct degrade, but otherwise only per-task warns). One-shot BOOT
1565
+ // warning so it's diagnosable. (The orchestrator normally injects the worker alongside a wpt_ — hand-misconfig.)
975
1566
  if (config.configCenter && !config.configCenter.dryRun && scopedTokenNeedsWorker(config.configCenter.token, config.configCenter.worker)) {
976
1567
  logger.warn("runtime_caps_scoped_token_no_worker", {
977
1568
  reason: "SEMA_REGISTRY_TOKEN is a worker-scoped wpt_ token but SEMA_REGISTRY_WORKER is unset → center 403s per-principal caps → ALL workflow self-orchestration will be fail-closed denied. Set SEMA_REGISTRY_WORKER (the orchestrator normally injects it) or use the full SERVICE_PULL_TOKEN.",
978
1569
  });
979
1570
  }
1571
+ // ONE per-principal caps client serves two faces off the same fetch/cache — `resolveRuntimeCaps`
1572
+ // (core's seam, fail-closed) and `scenarioRuling` (resolveSpec's scenario gate, fail-open; center attaches the
1573
+ // resolved {scenario, allowlist} to the caps body, single semantic source in center resolve-scenario.ts).
980
1574
  const principalCaps = config.configCenter && !config.configCenter.dryRun
981
1575
  ? createPrincipalEntitlementsClient({
982
1576
  baseUrl: config.configCenter.baseUrl,
@@ -985,23 +1579,55 @@ async function main() {
985
1579
  onError: (err, principal) => logger.warn("runtime_caps_resolve_failed", { principal, err: String(err) }),
986
1580
  })
987
1581
  : undefined;
1582
+ // 「entitlement resolver wired」语义源(enableForkFromBody / selfOrchestrationFromBody 的多租户 fail-close
1583
+ // 判别):只有 center 背书的 caps client 算数 —— 下面的 env observer 基线不是 entitlement 源,不得改变它。
988
1584
  const centerRuntimeCapsResolver = principalCaps?.resolveRuntimeCaps;
1585
+ // observer 开闸线 env 半场:EXPERIMENTAL_OBSERVER_AGENTS=true → 单用户部署把
1586
+ // allowObservers:true 作部署基线合成(center caps 带键则 center 赢);多租户不认 env(boot warn 一次,
1587
+ // 行为零变)。center caps 的 allowObservers 键本身在 toCoreRuntimeCaps 宽读透传(缺键=core 默认 OFF)。
989
1588
  const runtimeCapsResolver = applyObserverEnvOptIn(centerRuntimeCapsResolver, {
990
1589
  experimentalObserverAgents: config.experimentalObserverAgents,
991
1590
  requirePrincipal: config.requirePrincipal,
992
1591
  warn: (msg, fields) => logger.warn(msg, fields),
993
1592
  });
1593
+ // S8 (design/98) + SVC-1: mount core's `run_workflow` under the hardened-vm sandbox when
1594
+ // enabled (default OFF → core fail-closed, tool not mounted, zero impact). The tool STARTS a workflow and
1595
+ // returns its runId IMMEDIATELY (the background-run half — the caller never blocks); the workflow runs to
1596
+ // terminal in the background and fires core's in-process completion notify. Workflow-spawned children inherit
1597
+ // a CONSERVATIVE baseline (handsReadOnly — read-only by default; loosen deliberately); the script may pick
1598
+ // ONLY allow-listed model names (empty ⇒ it cannot pick → the workflow's default role).
1599
+ //
1600
+ // design/158 S4:三处装配点里**唯一**一组多键条件段 —— 提成带类型标注的中间 const 再整体展开进
1601
+ // runnerDeps。标注位 `Pick<RunnerDeps, …>` 本身触发 TS 多余属性检查(键名写错 / 键根本不属于
1602
+ // RunnerDeps ⇒ 编译期红),所以这不是把旁路挪个地方;组内可选键一律写成显式
1603
+ // `k: cond ? expr : undefined`,不再用 `...(cond ? { k } : {})`。
994
1604
  const workflowModelAllowlist = workflowModelAllowlistFor(config);
995
1605
  const selfOrchestrationDeps = config.selfOrchestrationEnabled
996
1606
  ? {
1607
+ // worker_thread isolation (heap cap + terminate) when opted in (multi-tenant DoS hardening), else
1608
+ // the faster in-process runner. Both run the same conformance-validated + probe-hardened membrane.
997
1609
  workflowScriptRunner: config.selfOrchestrationWorkerIsolation
998
1610
  ? createWorkerHardenedVmRunner()
999
1611
  : createHardenedVmRunner(),
1000
1612
  workflowRunStore: workflowRunStore ? workflowRunStore : undefined,
1613
+ // SVC-2 (CORE-9 Part A): durable resume journal — core auto-wires it into the tool's startWorkflow, the
1614
+ // `resumeFromRunId` tool input then replays the unchanged prefix (cross-replica when tidb/pg-backed). The
1615
+ // store enforces scope in its WHERE (CORE-9 audit BLOCKER: a cross-tenant resumeFromRunId → empty → live).
1001
1616
  workflowJournalStore: workflowJournalStore ? workflowJournalStore : undefined,
1617
+ // 切片 1.5 / design/140 §6: the named-workflow registry seam — a COMPOSITE store.
1618
+ // persist/load = core's file store under `<localDataRoot>/workflow-scripts` (the CC scriptPath-iterate
1619
+ // face: every invocation's script lands on disk, the model edits + re-invokes with {scriptPath};
1620
+ // containment is the file store's contract). resolveName consults the collab projection FIRST (center
1621
+ // collab templates as `{TEAM_DISCUSSION_SCRIPT, defaultArgs}` entries; a collab id equal to a built-in
1622
+ // name SHADOWS it — §6 1c) and falls back to the file store's `<name>.js` face (an operator-saved
1623
+ // script is a named workflow too). list() = collab entries only (the file store deliberately doesn't
1624
+ // enumerate — its dir mixes per-run scripts with saved names, core contract note); sync, so the
1625
+ // entries reach the statically-built Workflow tool card.
1002
1626
  workflowScriptStore: (() => {
1003
1627
  const fileStore = createFileWorkflowScriptStore(join(config.localDataRoot ?? localRoot, "workflow-scripts"));
1004
1628
  return {
1629
+ // core 1.366 (B-4) scope 分区一致性标:persist/load 直转 core file 实现(per-scope 子目录,
1630
+ // 真分区)——标是诚实声明;缺标=core 工具读侧拒/写侧跳(编译期必填字面量即为此设计)。
1005
1631
  scopePartitioned: true,
1006
1632
  persist: fileStore.persist.bind(fileStore),
1007
1633
  load: fileStore.load.bind(fileStore),
@@ -1009,22 +1635,55 @@ async function main() {
1009
1635
  list: () => listCollabWorkflows(),
1010
1636
  };
1011
1637
  })(),
1638
+ // SVC-5 (CORE-9 Part B): the opt-in steer handle-sink. When set, the tool's `agent()` runs STEERABLE and
1639
+ // emits the live handle HERE (never into the script/runner — the host-context membrane stays closed). We
1640
+ // register it by runId+label so POST /v1/workflows/:id/agents/:label/steer can route a steer to it, and
1641
+ // unregister when the agent settles (result() settles on stream completion; a stale handle would only
1642
+ // 409 `steering.not_running` anyway — this just bounds the map). A steered agent is NOT journaled (CORE-9.1
1643
+ // limitation: its resume re-runs live), which is correct + documented.
1012
1644
  onWorkflowAgentSpawn: workflowAgentRegistry
1013
1645
  ? (handle) => {
1014
1646
  const unregister = workflowAgentRegistry.register(handle);
1015
1647
  void Promise.resolve(handle.result()).then(unregister, unregister);
1016
1648
  }
1017
1649
  : undefined,
1650
+ // workflowSizeGuideline: advisory size guidance injected into the
1651
+ // Workflow tool card (core Wvs, CC 206 verbatim). Absent/unrestricted = byte-compat card.
1018
1652
  workflowLimits: config.workflowSizeGuideline ? { sizeGuideline: config.workflowSizeGuideline } : undefined,
1019
1653
  workflowGovernanceBaseline: {
1654
+ // [824]① clay 拍 A 案(workflow 权限全面 CC parity):默认基线撤 handsReadOnly 钳 —— 主 LLM 与
1655
+ // 子 agent 同 root 同信任域,主 LLM 本就能写这棵树([816] ask 门照管),单独钳子 agent 的安全增益≈0
1656
+ // (只防绕路不防直路的门不是边界);实测产品代价=[814]A 死锁(写型 workflow agent 永远只读)。并发
1657
+ // 写互踩按 CC 同姿用建议解决(worktree 契约话术+design/111 advisory),不用强制钳。要保守的 TOB
1658
+ // 部署自己开 WORKFLOW_AGENTS_READONLY=true(config.workflowAgentsReadOnly)加回旧钳。
1020
1659
  base: config.workflowAgentsReadOnly ? { handsReadOnly: true } : {},
1660
+ // [824]②/[826]-core 1.291 worktree seam(TOB 旋钮回归腿):旋钮开启时 worktreeBase 配成
1661
+ // overlay 形状 —— core 对 `agent({isolation:"worktree"})` 做 base 浅合并(非整体替换,
1662
+ // workflow-primitives effectiveBaseline:{...base,...worktreeBase}),{handsReadOnly:false} 语义=
1663
+ // 「只放写,其余基线全保留」。效果:保守部署里非隔离子 agent 仍只读,进了自有 worktree 的
1664
+ // agent 恢复可写([814]A 写型 workflow 在 TOB 形态下不死锁)。A 案默认(旋钮 off)不配 worktreeBase。
1021
1665
  worktreeBase: config.workflowAgentsReadOnly ? { handsReadOnly: false } : undefined,
1666
+ // Model allowlist for the script's `agent({model})` picks: explicit
1667
+ // SELF_ORCHESTRATION_MODELS wins; single-user defaults to the deployment's own catalog; multi-tenant
1668
+ // keeps core's fail-closed empty default. Decision + rationale in workflowModelAllowlistFor.
1022
1669
  workflowModelAllowlist: workflowModelAllowlist ? workflowModelAllowlist : undefined,
1023
1670
  },
1671
+ // SVC-1 at-least-once notify: route core's terminal notify THROUGH the gate (deliver-then-ack +
1672
+ // journal dedup) instead of straight to log/meter, so a crash mid-delivery re-delivers on the next boot
1673
+ // recovery sweep. When the gate is absent (memory backend / no journal) fall back to the bare
1674
+ // log+meter delivery (at-most-once, the prior behavior — honest about the lost-on-crash window).
1024
1675
  workflowCompletionNotifier: {
1676
+ // ⚠️ design/158 S4 有意保留的展开:两臂**都非空**(要么 gate 造的 notifier,要么裸 notify 兜底),
1677
+ // 不是「条件键」形——它合并的是同一必填键 `notify` 的两个来源,两来源都有类型,不构成
1678
+ // 多余属性检查旁路(缺 notify 反而当场编译红)。
1025
1679
  ...(workflowNotifyGate
1026
1680
  ? workflowNotifyGate.buildNotifier()
1027
1681
  : { notify: (input) => deliverWorkflowCompletion(input) }),
1682
+ // core 1.232 (the CORE half of the poll-then-also-notify dedup): pollWorkflow fires this
1683
+ // ONCE when the ORIGINATING session polls the run to a real terminal (timeout/cross-session polls
1684
+ // don't) — drop that (session, runId)'s pending inbox entry + arm the served fence, so the push
1685
+ // that would duplicate what the model just read is dead even across a shell restart. Session
1686
+ // attribution mirrors resolveCompletionRoute's session half (payload id, else the run row).
1028
1687
  ackServed: async (input) => {
1029
1688
  if (!workflowCompletionInbox)
1030
1689
  return;
@@ -1032,6 +1691,10 @@ async function main() {
1032
1691
  const sid = await resolveServedSession(input, runStore ? (id) => runStore.getRun(id) : undefined);
1033
1692
  if (sid) {
1034
1693
  await workflowCompletionInbox.markTerminalServed(sid, input.runId);
1694
+ // The THIRD way an inbox entry disappears (besides the three stream-open drains
1695
+ // and the owner-mismatch drop) — the model itself polled the run to terminal, so the push is
1696
+ // suppressed. An "enqueue → ack within ms, but the shell showed nothing" report with THIS line
1697
+ // means the completion went to the MODEL in-process (core injected it), not onto any stream.
1035
1698
  logger.info("workflow_complete_ack_served", { route: "poll-served", sessionId: sid, runId: input.runId });
1036
1699
  }
1037
1700
  }
@@ -1043,28 +1706,63 @@ async function main() {
1043
1706
  }
1044
1707
  : {};
1045
1708
  const runnerDeps = {
1709
+ // e/b([985]):center catalog 轴的 core 消费面——candidate/pinned-digest 解析走这里(prepare-task
1710
+ // dist:session_start 优先 candidate、resume 按 digest 解析、miss=fail-loud prompt_snapshot_unavailable)。
1046
1711
  promptSource,
1712
+ // [1070]① agent-team S1:持久名册 seam(具名 spawn advisory 写入;SendMessage 活注册表 miss 后咨询)。
1047
1713
  rosterStore: rosterStore ? rosterStore : undefined,
1714
+ // core 1.364 durable bg agents 读半场(写半场=scenarioDeps.backgroundAgentStore 同实例,组装区注释)。
1048
1715
  backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
1716
+ // S3c tier-3 懒复活(core 1.374):SendMessage 链内消费,与 backgroundAgentStore 同实例配套挂载。
1049
1717
  mailboxStore: mailboxStore ? mailboxStore : undefined,
1050
1718
  brain,
1051
1719
  models: config.models,
1052
1720
  roles: config.roles,
1721
+ // core 1.265: the active tier table (tier words + CC aliases → catalog keys, expandTiers at Runner
1722
+ // construction; empty = INERT by core contract). Same mutateInPlace reference applyEffective fills — a
1723
+ // refresh-time tier change is restart-to-apply, same tier as models.
1053
1724
  tiers: config.tiers,
1054
1725
  pricing,
1055
1726
  tracer,
1727
+ // [931]① clay 拍(core 1.300 BREAKING:缺省不署 Co-Authored-By,署名=产品身份资产归部署):
1728
+ // branded 形态(local provider = Sema 产品线,scenarios brandIdentity 同判据)commit 尾注接 Sema 署名;
1729
+ // 非 brand 部署维持 core 新缺省(不署)。seam=RunnerDeps.hands.commitCoAuthor。
1056
1730
  hands: config.configProvider === "local" ? { commitCoAuthor: "Sema <noreply@vivi-ai.com>" } : undefined,
1731
+ // design/73 §1 (core 1.226 seam, clay 拍 2026-07-04 接): consume mechanical TaskOutcome facts into the
1732
+ // outcome ledger — tidb/pg = SQL rows (coreOutcomeToLedgerRow mapping + verbatim `core_outcome` JSON so
1733
+ // red-line ② oracleHadRedRun survives lossless), local = owner-only JSONL. Read-only v1: records facts,
1734
+ // drives NO policy (§7.4 backtest gate stands). Fire-and-forget — a sink failure logs and never touches
1735
+ // the run (core's emitTaskOutcome swallow-guards too). Emitters today: core runGoal terminal + any
1736
+ // harness with a REAL mechanical oracle calling runner.emitTaskOutcome; plain runTask never auto-emits.
1057
1737
  onTaskOutcome: outcomeSink
1058
1738
  ? (o) => {
1059
1739
  metrics.inc("task_outcomes_total", { status: o.status, green: String(o.oracle?.green ?? "unknown") });
1060
1740
  void outcomeSink.recordCore(o).catch((err) => logger.warn("task_outcome_record_failed", { runId: o.runId, err: String(err) }));
1061
1741
  }
1062
1742
  : undefined,
1743
+ // E23: the live-only inbound-elicitation seam. core invokes it only for servers that opted in via
1744
+ // McpServerSpec.elicitation (default OFF) AND only when this is wired — both must hold (doubly fail-closed).
1063
1745
  onElicit: elicitation ? elicitation.elicit : undefined,
1746
+ // §4④: the live AskUserQuestion seam. core mounts the tool when onQuestion is present + routes each ask here; the
1747
+ // DURABLE leg's spec.onQuestion (QUESTION_AWAITS_RESUME) OVERRIDES this per-task so a disconnected-human ask suspends.
1064
1748
  onQuestion: question ? question.question : undefined,
1749
+ // [816]/[820]②: the live tool-approval seam (core `resolveAsk` — `spec.onAsk ?? deps.onAsk`). ALS-routed like
1750
+ // onQuestion: a leg wrapped by the coordinator's runWithContext reaches the human.
1751
+ //
1752
+ // [879] G1 终态(core 1.295 OnAsk 三值化):恒 wire。回调逐 ask 时刻判活人——ALS 附着腿 ⇒ 同步三选卡;
1753
+ // 无附着/卡送达失败 ⇒ 返 "unavailable",core 以 approverUnavailable 回路把该 ask 交回 suspendAsk 走
1754
+ // durable park(prepare-task 分路带 `approverUnavailable !== true` 豁免位,dist 亲读)——park 与 live 卡
1755
+ // 两全,1.199 的「durable 部署不 wire deps.onAsk」止血撤除。无 park 设施的部署 core 自己 fail-closed
1756
+ // deny(resolveAsk 的 unavailable 文案),与旧姿势同向。
1065
1757
  onAsk: toolApproval
1066
1758
  ? (req, signal) => toolApproval.ask(req, signal)
1067
1759
  : undefined,
1760
+ // [822]② auto permission mode, deployment half: RunnerDeps.autoMode is the operator TRUST face of core's
1761
+ // auto-mode classifier (1.276/1.277) — wiring it alone arms NOTHING (core requires the per-principal entitlement
1762
+ // runtimeCaps.autoMode === true from center, fail-closed dark by default; see runtime-caps-resolver.ts). With
1763
+ // both present, core screens every policy `ask` through the classifier (allow safe / deny hostile / leave the
1764
+ // rest on the ask path → this bridge or durable park). Defaults-only config (core's rules/window); breaker-open
1765
+ // is surfaced for observability.
1068
1766
  autoMode: {
1069
1767
  onBreakerOpen: (info) => {
1070
1768
  metrics.inc("auto_mode_breaker_open_total");
@@ -1072,8 +1770,14 @@ async function main() {
1072
1770
  },
1073
1771
  },
1074
1772
  sessionStore,
1773
+ // design/138 S1: the memory-engine switch — when present (single-user + engine on), core replaces the
1774
+ // legacy memory path WHOLESALE per task (materialize → session file ops → harvest; no remember/recall
1775
+ // tools). `memoryEngineDir` = the resolved config root (core derives the B3 control plane beside the
1776
+ // memory dir). Absent (multi-tenant / MEMORY_ENGINE=off) ⇒ no deps.memoryBackend ⇒ memory dark.
1075
1777
  memoryBackend: memoryEngine ? memoryEngine.backend : undefined,
1076
1778
  memoryEngineDir: memoryEngine ? memoryEngine.root : undefined,
1779
+ // S3-TOB 复审 F-9(operator 可观测底座):harvest 报告 → metrics(拒收/incident/patch 计数从此可见;
1780
+ // core swallow-guard 保证 throwing consumer 不伤边界)。
1077
1781
  onMemoryHarvestReport: memoryEngine
1078
1782
  ? (report, info) => {
1079
1783
  metrics.inc("memory_harvest_total", { ok: String(report.ok), phase: info.phase, incident: report.incident?.kind ?? "none" });
@@ -1081,47 +1785,89 @@ async function main() {
1081
1785
  metrics.inc("memory_harvest_patches_total", { phase: info.phase }, (report.patches.add ?? 0) + (report.patches.update ?? 0));
1082
1786
  if (report.incident)
1083
1787
  logger.warn("memory_harvest_incident", { kind: report.incident.kind, phase: info.phase });
1788
+ // 142-S2.5-W1: 成功 harvest 真有 patch 落地 = 本地记忆变了 ⇒ fire-and-forget 一轮同步
1789
+ // (trigger 自带 inflight 节流:上一轮在飞则跳过,漏掉的变更下一轮全量补上)。
1084
1790
  if (memorySyncRunner && report.ok && (report.patches?.add ?? 0) + (report.patches?.update ?? 0) > 0)
1085
1791
  memorySyncRunner.trigger("harvest");
1086
1792
  }
1087
1793
  : undefined,
1088
1794
  toolResultStore,
1795
+ // E6: operator-tightened session tool rules — core folds them into the ToolPolicy FIRST (subtract-only) for tasks
1796
+ // carrying a sessionId (a delegated subagent has none → inherits no rules). Opt-in: undefined ⇒ no rules read.
1089
1797
  sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined,
1798
+ // design/99 §K: per-principal runtime entitlements (allowWorkflows / forceDurableGate) resolved from center.
1799
+ // Opt-in: undefined ⇒ NO per-principal restriction (tighten-only; deployment default governs). See above.
1090
1800
  runtimeCapsResolver: runtimeCapsResolver ? runtimeCapsResolver : undefined,
1801
+ // E19: working-tree snapshot/restore for rewind (+ the 2c artifact store). core snapshots each completed turn +
1802
+ // restores on resumeAt when spec.rewindFiles is set, for ANY env when this store is wired (gate-split 1.134.0).
1091
1803
  fileSnapshotStore: fileSnapshotStore ? fileSnapshotStore : undefined,
1092
1804
  executionEnvFactory: executionEnvFactory ? executionEnvFactory : undefined,
1093
1805
  lspManager: lspManager ? lspManager : undefined,
1806
+ // design/129-B (core 1.240.0): the PROCESS-LEVEL background-child observer — spawn/tick/terminal
1807
+ // for every bg delegation child, never dying with a leg. Feeds the fleet rows (launch 即有行, session-scoped
1808
+ // children stay visible past the turn — the 缺口① fix) + the bg_notification frame on the
1809
+ // always-open fleet stream (缺口②: idle completion delivery is immediate). Wired once per process.
1094
1810
  onBackgroundChildEvent: fleetBackgroundChildPublisher(fleetBus, (msg, fields) => logger.info(msg, fields)),
1811
+ // design/113 C4: inject CLAUDE.md + git narrative so a chat opens project-aware (like CC). The helper reads cwd via
1812
+ // local fs/execFile — only meaningful when cwd is LOCAL (host/file lane); it stats the dir and returns null on a
1813
+ // remote-sandbox cwd, so this is safe to wire generally, but we gate it to single-user (cwdHonored posture — the
1814
+ // same lane where reading the local project for the caller isn't a confused-deputy hole) + the opt-out kill-switch.
1095
1815
  loadProjectMemory: config.requirePrincipal !== true && config.projectMemoryEnabled ? makeLoadProjectMemory({ logger }) : undefined,
1816
+ // core 1.302 instructions-change lane (板 [952]②): re-fingerprint the declared instruction file each turn so a
1817
+ // mid-run edit surfaces as a tail attachment. STRICTLY host lane — on a remote-sandbox lane the instruction file
1818
+ // lives in the container; a local re-read would fingerprint the wrong tree, so the probe is not wired there.
1096
1819
  probeInstructionSources: config.remoteExec?.provider === "host" && config.requirePrincipal !== true && config.projectMemoryEnabled
1097
1820
  ? makeProbeInstructionSources()
1098
1821
  : undefined,
1099
1822
  hooks: deploymentHooks,
1100
- ...selfOrchestrationDeps,
1823
+ ...selfOrchestrationDeps, // S8 self-orchestration 全家桶(成组条件段;类型标注在上方 const)
1824
+ // F8: surface best-effort compaction failures + the 1.22 prompt-cache low-hit warning (an
1825
+ // unstable/poisoned prefix tanking cost) instead of silently swallowing them.
1101
1826
  onError: (err, ctx) => {
1827
+ // "degraded" (1.40) is an operational event, NOT a failure — the task still completes (on a cheaper
1828
+ // model). Log it as a warning so it doesn't pollute error rates/alerts. The degraded_total metric
1829
+ // comes from the tracer's task.degraded event.
1102
1830
  if (ctx.phase === "degraded") {
1103
1831
  logger.warn("task_degraded", { sessionId: ctx.sessionId, info: String(err) });
1104
1832
  return;
1105
1833
  }
1834
+ // "prompt-constitution" (core 1.243): a stableSystem provider returned an ALREADY-
1835
+ // assembled prompt (constitution anchor found); core passed it through un-doubled. The task is safe
1836
+ // (guard un-doubles), but the provider is pre-1.243-shaped and should be upgraded to return only the
1837
+ // role base. Warn — paired with the tracer's prompt_constitution_total{mode="provider-assembled"}.
1106
1838
  if (ctx.phase === "prompt-constitution") {
1107
1839
  logger.warn("prompt_provider_needs_upgrade", { sessionId: ctx.sessionId, info: String(err) });
1108
1840
  return;
1109
1841
  }
1842
+ // "rewind" (E19 per-turn file snapshot): best-effort — a failed snapshot only makes that one turn
1843
+ // non-rewindable; the task itself is unaffected. `too_large` is the EXPECTED shape on a big working
1844
+ // tree (TOC user opening a 20G folder: enumerate hits the 256MB bound EVERY turn → an error-level
1845
+ // line per turn reads like the run is broken). Downgrade to warn + count by code so a real store
1846
+ // fault (enumerate_failed on a readable tree, blob-write errors) still stands out in the metric.
1110
1847
  if (ctx.phase === "rewind") {
1111
1848
  const code = /\((\w+)\)/.exec(String(err))?.[1] ?? "unknown";
1112
1849
  logger.warn("rewind_snapshot_failed", { sessionId: ctx.sessionId, code, info: String(err) });
1113
1850
  metrics.inc("rewind_snapshot_failed_total", { code });
1114
1851
  return;
1115
1852
  }
1853
+ // "memory" (1.62, design/41): a best-effort post-task consolidation pass failed. The task already
1854
+ // completed and its notes are safely appended (just not reconciled this round) — warn, fail-open,
1855
+ // don't pollute error rates. (Consolidation cost still lands in model_cost_micro_usd via the tracer.)
1116
1856
  if (ctx.phase === "memory") {
1117
1857
  logger.warn("memory_consolidation_failed", { sessionId: ctx.sessionId, info: String(err) });
1118
1858
  return;
1119
1859
  }
1860
+ // "mcp" (1.68): a broken MCP server was SKIPPED (fail-open) — the task still ran without
1861
+ // that server's tools. Surface it as a warning + metric so a misconfigured server is observable, not silent.
1120
1862
  if (ctx.phase === "mcp") {
1121
1863
  logger.warn("mcp_server_unavailable", { sessionId: ctx.sessionId, info: String(err) });
1122
1864
  metrics.inc("mcp_server_unavailable_total");
1123
1865
  return;
1124
1866
  }
1867
+ // "prompt-cache" (1.22 low-hit + design/31 break detector): since core 1.89 the detector passes its
1868
+ // root cause as ctx.classification — "server-or-ttl" is usually benign in agentic tasks (slow tools ⇒
1869
+ // 5min+ request gaps expire provider caches) → warn; the prefix-bug causes
1870
+ // (model-switch/tool-schema/tool-set/system-prefix) stay errors that warrant attention.
1125
1871
  if (ctx.phase === "prompt-cache") {
1126
1872
  metrics.inc("prompt_cache_low_hit_total");
1127
1873
  const fields = { sessionId: ctx.sessionId, ...(ctx.classification ? { classification: ctx.classification } : {}), err: String(err) };
@@ -1131,6 +1877,9 @@ async function main() {
1131
1877
  logger.error("prompt_cache_break", fields);
1132
1878
  return;
1133
1879
  }
1880
+ // "config" (core 1.300 [931]②): assembly-time advisories — e.g. toolPolicy 名单池审计
1881
+ // (config.toolpolicy.unmatched_names:名单里写了实挂宇宙不存在的工具名=typo 探测,enforcement
1882
+ // 不变)。观测性提示,warn+metric,不进 error 告警面。
1134
1883
  if (ctx.phase === "config") {
1135
1884
  logger.warn("runner_config_advisory", { sessionId: ctx.sessionId, info: String(err) });
1136
1885
  metrics.inc("runner_config_advisory_total");
@@ -1140,8 +1889,28 @@ async function main() {
1140
1889
  },
1141
1890
  };
1142
1891
  const runner = new Runner(runnerDeps);
1892
+ // codex R10: TRUE ⇒ the Runner just froze a PRIVATE tier-expanded catalog copy (core runtask.js constructor,
1893
+ // dist-read) — in-place model-plane mutation no longer reaches it, so refresh-time plane changes must be
1894
+ // DEFERRED to restart (see appliedPlaneEff / applyEffective deferModelPlane). Captured HERE, at the same
1895
+ // config generation the constructor read.
1143
1896
  const runnerTierFrozen = Object.keys(config.tiers).length > 0;
1897
+ // hooks 阶段三b(design/HOOKS-PHASE3B-PROMPT-AGENT.md):prompt/agent 条目的模型调用载体。
1898
+ // - hookLlm(prompt 条目)= 直连网关一次非流式 completion——一次判定调用不起 core task(重炮打蚊子
1899
+ // 且引入可重入面);无系统注入(hook 作者的 prompt 即全部)。默认模型=summarize 角色(cheap tier,
1900
+ // council/压缩同款;缺角色→主模型);entry.model 覆盖须命中部署 catalog(单用户=自家目录全开,与
1901
+ // workflow 模型 allowlist 同纪律),未命中=该条目 fail(非阻断记账)。费用不折 task budget(hook=
1902
+ // 部署策略面开销;hook_llm_calls_total/hook_llm_cost 由 metrics 记)。
1903
+ // - hookAgent(agent 条目)= runner.runTask 读-only 子代理:handsReadOnly + enableFork:false +
1904
+ // 不装 hooks(🔴 可重入禁——结构性:这个 spec 永不携带 TaskSpec.hooks)+ maxTurns 10 + maxCostUsd
1905
+ // 0.05 独立小预算,影响范围受限。final result 文本即输出(与 prompt 同一 SyncHookOutput 决策面)。
1906
+ // hookModelFor/hookLlm 抽到 src/hooks/hook-llm.ts(可注入 fetch/keyResolver ⇒ 可单测):api 形分派
1907
+ // (openai /chat/completions 与 anthropic /v1/messages 双腿)、缺省 baseUrl 按 api 分家、per-model key
1908
+ // 恒胜 + foreign-no-key 连 prompt 都不发的 fail-closed 纪律,全在该模块内(注释含 audit 原文)。
1909
+ // getKeyResolver 取函数:keyResolver 是下方 let,registry 热应用会整个换引用,这里必须每次现取。
1144
1910
  const { hookModelFor, hookLlm } = createHookLlm({ config, getKeyResolver: () => keyResolver, metrics });
1911
+ // hook agent 的 session 绝不进 durable store——每次 runTask 会写 session_meta 且无人
1912
+ // 清理(reaper 只扫 staging orphan),cap=4/事件 × 长会话=无主行堆积。专用 Runner 覆盖 sessionStore 为
1913
+ // 进程内 TTL 店(1h 短 TTL,hook 子代理无 resume 语义,行随进程/TTL 消失);其余 deps 原样共享。
1145
1914
  const hookAgentRunner = new Runner({ ...runnerDeps, sessionStore: new TtlSessionStore({ defaultTtlDays: 1 / 24 }) });
1146
1915
  const hookAgent = async ({ prompt, model, timeoutMs }) => {
1147
1916
  const pick = hookModelFor(model);
@@ -1156,7 +1925,11 @@ async function main() {
1156
1925
  enableFork: false,
1157
1926
  maxCostUsd: 0.05,
1158
1927
  limits: { maxTurns: 10, timeoutSec: Math.max(30, Math.ceil(timeoutMs / 1000)) },
1928
+ // spec 无 toolPolicy → core `hasEffectAwareGate` 为假 → 每次 agent hook 触发一条
1929
+ // error 级 UNGATED 警告(非致命但纯噪声)。挂已裁决的 auto-accept 基线(与 singleUserAutoAcceptBaseline
1930
+ // 同构;hook 子代理本就 handsReadOnly + 单用户信任面,auto-accept 是既定姿态,只是让闸机制在场)。
1159
1931
  toolPolicy: createAllowDenyPolicy({}),
1932
+ // 🔴 可重入禁:此 spec 不带 hooks 字段(deps.hooks 是部署观测 tracer,void 观察者,无决策面)。
1160
1933
  });
1161
1934
  metrics.inc("hook_llm_calls_total", { type: "agent" });
1162
1935
  if (result.status !== "completed")
@@ -1168,12 +1941,15 @@ async function main() {
1168
1941
  }
1169
1942
  };
1170
1943
  const clipHookErr = (s) => (s ?? "").slice(0, 300);
1944
+ // Auto session titles — fire-and-forget at first
1945
+ // submit, one cheap hookLlm call (summarize role → MODEL_CHEAP_ID when configured), write-once via the session
1946
+ // store's setTitleIfNull (SQL twins + local sidecar carry it; the memory dev store doesn't → titler dark, honest).
1171
1947
  const titleCapable = sessionStore;
1172
1948
  const sessionTitler = config.sessionAutoTitle && titleCapable.setTitleIfNull && titleCapable.probeTitle
1173
1949
  ? createSessionTitler({
1174
1950
  setTitleIfNull: titleCapable.setTitleIfNull.bind(sessionStore),
1175
1951
  probeTitle: titleCapable.probeTitle.bind(sessionStore),
1176
- redact: redactSecrets,
1952
+ redact: redactSecrets, // audit 2026-07-13: scrub a model-echoed secret before persist/list/log
1177
1953
  llm: (o) => hookLlm({ prompt: o.prompt, timeoutMs: o.timeoutMs }),
1178
1954
  logger,
1179
1955
  metrics,
@@ -1181,8 +1957,54 @@ async function main() {
1181
1957
  : undefined;
1182
1958
  if (sessionTitler)
1183
1959
  logger.info("session_auto_title_enabled", {});
1960
+ // ① workflowsCapability SINGLE-SOURCE (core 1.155 `workflowsCapability(deps)`; ruling = SPLIT two caps):
1961
+ // `capabilities.workflows` = core's own truth (`workflowScriptRunner.safeForUntrustedScripts` ∧ governance), boot-
1962
+ // computed from the SAME RunnerDeps the engine uses = the ENGINE-CAN axis ("can the engine self-orchestrate /
1963
+ // ultracode"), ORTHOGONAL to the durable-LIST axis (`Boolean(workflowRunStore)`, surfaced as `workflowsList`).
1964
+ // ⚠️ Today these COINCIDE — `workflowRunStore` is built iff self-orch (InMemory/File, ~L527), the same gate that
1965
+ // wires the safe runner — so this is core's FORWARD-LOOKING contract, not a live-divergence fix (the earlier
1966
+ // "memory backend diverges" premise was wrong; double-review caught it — see the cap site in http/server.ts).
1967
+ // Static per deployment (deps are boot-fixed).
1184
1968
  const workflowsCapable = workflowsCapability(runnerDeps);
1969
+ // Sub-task runner (code-review council lenses/arbiter, subagent/team children). [1364]①/[1367]①(core
1970
+ // 1.350 fork 终修的部署要求):fork 子任务带 `requireExistingSession:true` 在这里执行而源会话在 HOST 店
1971
+ // ——拆店=响亮 `resume.session_not_found`(cli fork 案的 server 半场)。但整店共享被 codex 判两 high
1972
+ // (普通子任务铸 committed durable 会话:无 reaper 覆盖=无界增长+污染 /v1/sessions 枚举),故走
1973
+ // ForkRoutingSessionStore:fork/resume 形(requireExisting)→ host 店优先,普通子任务 → 私有 TTL 店
1974
+ // (throwaway 姿势保留)。hookAgentRunner 仍保独立 TtlSessionStore(1h)——纯瞬态 hook 车道,与 fork 无关。
1975
+ // CC-parity (CRITICAL — 子代理 hands, root cause is the service spawn path): the subagent delegation
1976
+ // tool runs child tasks on THIS subRunner. Without executionEnvFactory the child's `handsEnabled` (= ownedEnv ||
1977
+ // deps.executionEnv, prepare-task) is FALSE → the child gets NO hands (Bash/Read/Edit) and can't run `pwd` — the
1978
+ // "subagent couldn't run pwd" defect hit in real TUI use. core's createSubagentTool gives hands when the env is
1979
+ // wired (gate3: opts.tools:[] still yields Bash), so the fix is to give the subRunner the SAME executionEnvFactory +
1980
+ // lspManager as the main runner. Deliberately NOT inherited: selfOrchestration (no recursive workflow spawn),
1981
+ // scheduler, memory, fileSnapshot, runtimeCaps — a delegated child is a short focused task, not a full session.
1982
+ // codex-2 F1:promptSource 必须随子代——core 1.341 把父的 centerArtifactDigest 传给委派子任务并在子
1983
+ // runner 上按 digest 解析;缺席=每个 catalog 任务的 Agent 子代 fail-loud prompt_snapshot_unavailable。
1984
+ // backgroundAgentStore(1.248 复审 F2):core prepare-task 给每个 task 的 TaskOutput/TaskStop/
1985
+ // AgentTranscript 装 durable 臂用的是所在 runner 的 deps——子代任务跑在 subRunner 上,缺席=子代读
1986
+ // 工具无 durable 回落(第三消费点,与 RunnerDeps/scenarioDeps 同一实例)。
1987
+ // [1543]§三族A(core 穿透审查,历史欠账键批——主 runner 挂了、subRunner 漏挂 = 子代 lane 静默降级;
1988
+ // 全部直引 runnerDeps 同一实例,双点永不漂移):
1989
+ // - tiers(H):模型目录双 runner 分叉——tier 词 AgentDefinition 在子代 lane 抛 unknown_model、Agent
1990
+ // model 参数("opus")静默 modelNote 降级、S3c revive 臂 row.model 记 tier 词时复活换模型。
1991
+ // - rosterStore(M):具名 spawn 不落 durable 名册 → tier-3 按名复活从子代不可达。
1992
+ // - onBackgroundChildEvent(M):子代触发的 revive/孙代对 fleet 流全盲(同一 publisher 实例=单管道)。
1993
+ // - loadProjectMemory/probeInstructionSources(M):委派子代 project-blind(core types 已为子代 ctx
1994
+ // 设计 isSubagent/agentName;门禁条件随 runnerDeps 条件键自动一致)。
1995
+ // - hooks/onError/toolResultStore/sessionPolicyStore(L):子代进 tracer/计量;best-effort 故障不静默;
1996
+ // fork 子代跨 wake 保 offload 引用;fork 形(有 sessionId)会话规则可达。
1997
+ // 1877 注释的「Deliberately NOT inherited」清单(selfOrchestration/scheduler/memory/fileSnapshot/
1998
+ // runtimeCaps)不变——本批只补漏配,不动裁定。
1999
+ // checkpointStore 构造(从 runStore 区上移,只依赖 backend/config——顺序安全;DURABLE_APPROVAL 仍是
2000
+ // 唯一门,ALL three backends carry checkpoint(),local = core FileCheckpointStore + service half)。
2001
+ // 三个消费点同一实例:主 runner 的 per-task spec 面(resolveSpec durableEnabled 分支)、scenarioDeps
2002
+ // (subagent spawn 面 forwardDurableApproval)、subRunner deps(子代执行面 suspendAsk——[1584] 接线,
2003
+ // 裁定见下方 subRunner 构造内注)。
1185
2004
  const checkpointStore = backend?.checkpoint && config.durableApproval ? backend.checkpoint(logger) : undefined;
2005
+ // [1593] 提名成变量:parked 子代的 ensureChildSessionDurable 晋升半场要拿它做 promoteToHost
2006
+ // (子会话住私有 TTL 店,park 前整树拷进 host durable 店——只查不迁的 1.263 形在拆店生产形下恒
2007
+ // 否决 park,cli 真机 14ms~2s expired 即此)。
1186
2008
  const subRunnerSessions = new ForkRoutingSessionStore(sessionStore, new TtlSessionStore({ defaultTtlDays: 1 / 24 }));
1187
2009
  const subRunner = new Runner({
1188
2010
  brain,
@@ -1193,6 +2015,9 @@ async function main() {
1193
2015
  tracer,
1194
2016
  promptSource,
1195
2017
  sessionStore: subRunnerSessions,
2018
+ // A7/S4 余量(design/158):core RunnerDeps 消费端对以下键均无在场性判定(无 "k" in deps /
2019
+ // Object.keys(deps) 分支——S4 已逐键核过全量 RunnerDeps 消费面),故显式可选键(undefined 与
2020
+ // 缺席同义)替代条件 spread,行为等价、可读性与主 runnerDeps 装配点(1687 行)同款。
1196
2021
  executionEnvFactory: executionEnvFactory ? executionEnvFactory : undefined,
1197
2022
  lspManager: lspManager ? lspManager : undefined,
1198
2023
  backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
@@ -1204,15 +2029,42 @@ async function main() {
1204
2029
  hooks: deploymentHooks,
1205
2030
  onError: runnerDeps.onError ? runnerDeps.onError : undefined,
1206
2031
  toolResultStore,
2032
+ // sessionPolicyStore:挂载无害(tighten-only 空查);如实注(复审 F5)——子代 sessionId 是新铸
2033
+ // uuid,E6 规则按宿主 id 键存 → 子代查自己恒空;规则真到子代靠 inheritedGate.ancestorRules(本就
2034
+ // 通)。真实受益面 = retained child 自己 session 被显式 PUT 过规则的边角。
1207
2035
  sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined,
2036
+ // checkpointStore = 子代**执行面**的 park 设施([1582] cli TiDB 真验撞获缺口,[1584] core 定谳
2037
+ // 接线步):core prepare-task `spec.checkpointStore ?? deps.checkpointStore` 里 spec 侧永不被
2038
+ // subagent 设置,子代 suspendAsk 全靠这里——缺席 = 具名 bg 子代的 gated ask 只能 deny+continue,
2039
+ // checkpoint 表零行(cli T2 复现原话「denied by an approval constraint」)。必须与 scenarioDeps
2040
+ // (spawn 面 forwardDurableApproval 判定)**同一实例**:claim/expire/consumeParkedFlip 都作用于
2041
+ // 同一行。旧 [1546] HIGH-2 撤载(「core 不转发 durableApproval + SAFETY ask 会 park 出搁浅
2042
+ // checkpoint」)的前提被 core 1.388 件4(转发臂)消除,搁浅顾虑由件1-4+1.389 逐项闭合:watcher
2043
+ // park 翻行 + reap 豁免 + reconciliation 兜孤儿;unattended SAFETY park 恒带缺省 deadline;
2044
+ // TaskStop on parked 走 expire CAS 仲裁(1.389 F-1);/decide 可赎回。行为钉 =
2045
+ // test/subagent-park-wiring.test.ts(SOURCE PIN + park 全链 + 旧形反向控制)。
1208
2046
  checkpointStore: checkpointStore ? checkpointStore : undefined,
1209
2047
  });
2048
+ // S6: resolve identity + owned session from the authenticated channel (never the body).
1210
2049
  const authorize = createAuthorizer(config, sessionStore);
2050
+ // S1/S2: durable async run registry + replayable event log (TiDB-backed).
1211
2051
  const runStore = backend ? backend.run() : undefined;
2052
+ // E18 resume-at: the eventId→entryId anchor map. REQUIRED on every backend (works local — needs only the session
2053
+ // tree + this map, no cloud-only checkpoint), so the only undefined case is the env-only/no-backend deploy (which
2054
+ // also has no async runs — consistent). The shell sends body.resumeAt=eventId; resolveSpec resolves it here.
1212
2055
  const resumeAnchorStore = backend ? backend.resumeAnchor() : undefined;
2056
+ // Per-session approval exemption ("本会话不再询问") — REQUIRED on every backend like the anchor
2057
+ // map; undefined only on the env-only/no-backend deploy (which has no durable approvals either — consistent).
1213
2058
  const approvalExemptionStore = backend ? backend.approvalExemption() : undefined;
1214
2059
  const instanceId = uuidv7();
2060
+ // Per-task wall-clock override (seconds). 0/absent = keep the built-in 600 (1500 council/debate/team) —
2061
+ // the override only ever RAISES (Math.max at the use site), so a misconfigured low value cannot shrink
2062
+ // the council budget.
1215
2063
  const taskTimeoutSec = Math.max(0, Math.floor(Number(process.env.TASK_TIMEOUT_SEC ?? "0")) || 0);
2064
+ // [854]④ per-request 配速的运营方上限旋钮(可选;缺省不设=不封顶):body.limits 每键各自被对应旋钮
2065
+ // Math.min 封顶(normalizeLimits)。多租部署想约束 caller 自报配速时才设;单用户 turnkey 通常留空。
2066
+ // ⚠️ 语义=只钳「显式请求值」,不是全队默认限额:body 缺席(或重放体里的非法值被 defensive DROP)⇒ 该键
2067
+ // 不设限,与升级前行为同形——要给所有任务强加限额是另一个旋钮(未建),别指望这三个(交叉评审 F1 判读)。
1216
2068
  const capEnv = (name) => {
1217
2069
  const n = Math.floor(Number(process.env[name] ?? "0"));
1218
2070
  return Number.isFinite(n) && n > 0 ? n : undefined;
@@ -1222,12 +2074,32 @@ async function main() {
1222
2074
  maxOutputTokens: capEnv("TASK_MAX_OUTPUT_TOKENS_MAX"),
1223
2075
  maxTurns: capEnv("TASK_MAX_TURNS_MAX"),
1224
2076
  };
1225
- const approvalStore = backend?.approval ? backend.approval() : undefined;
2077
+ // F4: human-in-the-loop approval gate (pending state durable so any instance can decide).
2078
+ const approvalStore = backend?.approval ? backend.approval() : undefined; // local omits approval() → legacy poll-gate off (durable HITL needs checkpoint anyway)
1226
2079
  const approvalEnabled = approvalStore && config.approvalRequire.length > 0;
2080
+ // Finding #2 (edge case): the single-user auto-accept tool-policy BASELINE applies ONLY when the operator
2081
+ // expressed NO gate intent. If they SET approval flags (APPROVAL_REQUIRE/APPROVAL_DENY/DURABLE_APPROVAL) but no
2082
+ // backing store is wired (e.g. DB_BACKEND=local → no approvalStore/checkpointStore ⇒ approvalEnabled/durableEnabled
2083
+ // false), that is a MISCONFIG — do NOT mask it with allow-all; leave toolPolicy undefined so core's UNGATED warning
2084
+ // still surfaces it. So the baseline needs single-user turnkey ∧ zero expressed gate intent.
1227
2085
  const operatorGateIntent = config.approvalRequire.length > 0 || config.approvalDeny.length > 0 || config.durableApproval;
1228
2086
  const singleUserAutoAcceptBaseline = config.requirePrincipal !== true && !operatorGateIntent;
2087
+ // design/45 durable-checkpoint (core 1.67): opt-in (DURABLE_APPROVAL + a pool). When on, F4 uses durable
2088
+ // suspend/resume instead of the poll gate — a gated `ask` suspends the task (status:"suspended" + checkpoint)
2089
+ // and an operator resumes it on any replica. Default off → unchanged poll-policy.
2090
+ // checkpointStore 构造已上移到 subRunner 之前([1535] server 半场:子代 park 设施);本行原位注释保留。
2091
+ // TOC integration ask③ (INTEGRATION-DESIGN §11): reclaim runs orphaned by a previous host-lane engine
2092
+ // process (orphaned `running` → failed + release its EEXIST-blocking claim; exclude resume-in-progress rows). The
2093
+ // logic + its null-safety (runStore undefined on the env-only/no-backend lane) live in the unit-tested
2094
+ // `reclaimOrphansAtBoot` helper — extracted after a workflow re-review caught a boot crash there (HIGH, 1.49.0).
1229
2095
  await reclaimOrphansAtBoot(runStore, checkpointStore, logger);
1230
- const imageIndex = backend?.imageIndex ? backend.imageIndex() : undefined;
2096
+ // Sandbox-image-pool index (IMAGE-API-DESIGN.md §2): enables /v1/images* query + select. TiDB-backed projection
2097
+ // of the bake manifests + publish lifecycle. Present whenever a pool is — read-only catalog until P2/P3 bakes.
2098
+ const imageIndex = backend?.imageIndex ? backend.imageIndex() : undefined; // local omits imageIndex() → sandbox-image-pool is cloud/fleet-only
2099
+ // RFC A2 (A 切片先行): the model-facing SelectEnvironment tool — mounted via spec.tools
2100
+ // (RFC 判定②(a)) whenever the per-task image chain is actually usable (k8s backend + image catalog), opt-out
2101
+ // via SELECT_ENVIRONMENT_TOOL=false. Viewer scope is rebuilt per-execute from the Runner-threaded read-only
2102
+ // ctx.principal (auth-channel-derived — a tool argument can never widen visibility).
1231
2103
  const selectEnvTool = config.selectEnvironmentTool && config.remoteExec?.provider === "k8s" && imageIndex
1232
2104
  ? selectEnvironmentTool({
1233
2105
  catalog: imageIndex,
@@ -1235,42 +2107,91 @@ async function main() {
1235
2107
  viewerFor: (principal) => ({ operator: explicitOperator(principal, config.operatorPrincipals), tenantId: principal ?? null }),
1236
2108
  })
1237
2109
  : undefined;
1238
- const imageBakes = backend?.imageBake && config.imageBakes.enabled ? backend.imageBake() : undefined;
2110
+ // Sandbox-image-pool BAKE control plane (IMAGE-API-DESIGN.md §P2): enables /v1/images/bakes* when a pool exists
2111
+ // AND IMAGE_BAKES_ENABLED. The config boot-invariant already fail-fasts an empty OPERATOR_PRINCIPALS (a
2112
+ // world-writable build-host-RCE door). image-api stays the UNPRIVILEGED controller; the privileged build runs
2113
+ // on the sema-registry-managed bake-runner worker on build-host (it claims + ingests over the internal routes).
2114
+ const imageBakes = backend?.imageBake && config.imageBakes.enabled ? backend.imageBake() : undefined; // local omits imageBake() → cloud/fleet-only
1239
2115
  if (imageBakes)
1240
2116
  logger.info("image_bakes_enabled", { operators: config.operatorPrincipals.length, runner: config.imageBakes.runnerPrincipal });
2117
+ // Durable HITL surface = checkpointStore ALONE (DURABLE_APPROVAL=true). It mounts AskUserQuestion-suspend (the
2118
+ // agent asks the human + the portal approval center) AND the resumable checkpoint. The F4 TOOL-APPROVAL
2119
+ // gate is an ADDITIVE layer driven by APPROVAL_REQUIRE — empty = no tool is gated, but AskUserQuestion HITL
2120
+ // still works. (Decoupled 2026-06-13: previously these were coupled, forcing a placeholder APPROVAL_REQUIRE
2121
+ // to get HITL — clay: OA writes don't gate (OA-backend RBAC is the authority), but portal HITL must work.)
2122
+ // durableEnabled 已在 runnerDeps 前按同一条件求值([1.294 G1] 前置声明处)——checkpointStore 构造条件与其
2123
+ // 同源,恒有 durableEnabled === !!checkpointStore。
1241
2124
  if (durableEnabled)
1242
2125
  logger.info("durable_approval_enabled", { toolApprovalGate: config.approvalRequire.length });
2126
+ // 修4(三路复审 W3):the fs-write ask gate (resolveSpec, applyTaskSettings wiring below) mounts purely off the
2127
+ // host-semantics lane — DELIBERATELY decoupled from any approval face, so no assembly path is ever gateless:
2128
+ // with no live coordinator and no durable park, a permissionMode default/auto Write/Edit ask fail-closed
2129
+ // auto-denies via core resolveAsk (onAsk absent ⇒ headless auto-deny, loud in-transcript). What that posture
2130
+ // must NOT be is silent at boot: an operator who explicitly set TOOL_APPROVAL_ENABLED=false (or a multi-tenant
2131
+ // default) would otherwise read the resulting hand-tool denials as an outage. Warn once, at boot.
1243
2132
  if ((config.remoteExec === undefined || config.remoteExec.provider === "host") && !toolApproval && !durableEnabled) {
1244
2133
  logger.warn("fs_write_gate_no_responder", {
1245
2134
  hint: "host-semantics lane with no approval face: permissionMode default/auto Write/Edit asks will auto-deny (fail-closed); wire TOOL_APPROVAL_ENABLED=true or DURABLE_APPROVAL=true to answer them",
1246
2135
  });
1247
2136
  }
2137
+ // [1245]/[1248]② the workflow ask leg attaches on EVERY lane (not fs-dependent — see createWorkflowAskPolicy),
2138
+ // so a sandbox-lane deployment running self-orchestration with no approval face hits the same fail-closed
2139
+ // auto-deny class. Same loud-at-boot posture; host lanes are already covered by the broader warn above.
1248
2140
  if (config.selfOrchestrationEnabled && config.remoteExec !== undefined && config.remoteExec.provider !== "host" && !toolApproval && !durableEnabled) {
1249
2141
  logger.warn("workflow_gate_no_responder", {
1250
2142
  hint: "self-orchestration with no approval face: permissionMode default/auto/acceptEdits run_workflow asks will auto-deny (fail-closed); wire TOOL_APPROVAL_ENABLED=true or DURABLE_APPROVAL=true to answer them",
1251
2143
  });
1252
2144
  }
2145
+ // Finding #2: a single-user turnkey worker with no explicit approval/durable/commandPolicy gets an
2146
+ // ADJUDICATED auto-accept tool-policy BASELINE (createAllowDenyPolicy — a PRESENT effect-aware policy, so core's
2147
+ // write-capable hand tools are never UNGATED / unadjudicated). Log the posture so the auto-accept is OBSERVABLE,
2148
+ // not silent; tighten irreversible ops via APPROVAL_REQUIRE / AUTONOMY / commandPolicy (layered tighten-only).
1253
2149
  if (singleUserAutoAcceptBaseline) {
2150
+ // Blanket auto-accept over ALL write/hand/MCP tool surfaces (core's per-surface UNGATED enumeration — hand band /
2151
+ // destructive-MCP / first-party-write — is suppressed for single-user turnkey by design). Log it so the posture is
2152
+ // observable; tighten (which also re-surfaces per-surface gating) via APPROVAL_REQUIRE / AUTONOMY / commandPolicy.
1254
2153
  logger.info("tool_policy_single_user_default", {
1255
2154
  posture: "adjudicated auto-accept — ALL write/hand/MCP tool surfaces auto-allowed (core per-surface UNGATED enumeration suppressed for single-user turnkey)",
1256
2155
  tighten: "APPROVAL_REQUIRE / AUTONOMY / commandPolicy (re-surfaces + gates specific surfaces)",
1257
2156
  });
1258
2157
  }
2158
+ // D-G: a boot-time signal that this worker is in crypto-bound direct-door mode (trusted-header path disabled).
2159
+ // Pairs with the /health directDoorActive field for the DG-M2 go-live runbook (a positive go signal, not just
2160
+ // a behavioral 401). The boot invariant in config.ts already fail-fasts a half-configured direct door.
1259
2161
  if (config.directDoorActive)
1260
2162
  logger.info("direct_door_active", { iss: config.principalJwtIss, aud: config.principalJwtAud, operators: config.operatorPrincipals.length });
2163
+ // CC-parity (adversarial-review finding): a MULTI-TENANT deployment running self-orchestration WITHOUT worker
2164
+ // isolation is an availability risk — the in-process hardened VM is ESCAPE-isolated but NOT RESOURCE-isolated, so an
2165
+ // entitled tenant's workflow script can starve the host event loop (microtask loop / OOM), affecting every tenant
2166
+ // (src/orchestration/hardened-vm-runner.ts documents this). Loud boot warning so the operator sets
2167
+ // SELF_ORCHESTRATION_WORKER_ISOLATION=true. (Single-user is unaffected — the one principal owns its own worker.)
1261
2168
  if (config.requirePrincipal && config.selfOrchestrationEnabled && !config.selfOrchestrationWorkerIsolation)
1262
2169
  logger.warn("self_orchestration_no_worker_isolation", { detail: "multi-tenant self-orchestration without SELF_ORCHESTRATION_WORKER_ISOLATION=true — a tenant workflow can starve the host event loop (availability risk). Set it to enable worker_thread resource isolation." });
2170
+ // 🔒 (systematic-audit HIGH fix, defense-in-depth): a multi-tenant deployment (requirePrincipal) with NO service/trace
2171
+ // token now correctly fails CLOSED — fleet/trace/session-list are owner-gated, never fleet-wide (isFleetWide, server.ts).
2172
+ // Warn the operator: without SERVICE_AUTH_TOKEN/authTokens/TRACE_TOKEN those endpoints only ever return the caller's own
2173
+ // rows (safe, but may surprise an operator expecting cross-tenant ops visibility — set a token for ops-relay).
1263
2174
  if (config.requirePrincipal && !config.authToken && Object.keys(config.authTokens ?? {}).length === 0 && !config.traceToken)
1264
2175
  logger.warn("multi_tenant_no_service_token", { detail: "REQUIRE_PRINCIPAL is on but no SERVICE_AUTH_TOKEN/authTokens/TRACE_TOKEN — fleet/trace/session-list are owner-gated only (never fleet-wide, tenant-isolation safe). Set a token for cross-tenant ops visibility." });
2176
+ // Rate limiter: TiDB-backed (cross-replica SOFT limit, same write-behind counter as the cost quota)
2177
+ // when a pool exists, else in-memory per-replica. Both satisfy RateGate. (design/30 P0-2)
1265
2178
  const rateLimiter = config.rateLimitPerMin > 0
1266
- ? backend?.rateLimiter
2179
+ ? backend?.rateLimiter // local omits rateLimiter() → in-memory per-process RateLimiter (single-replica)
1267
2180
  ? backend.rateLimiter(config.rateLimitPerMin, counterDegradeHook).startRefresh()
1268
2181
  : new RateLimiter(config.rateLimitPerMin)
1269
2182
  : undefined;
2183
+ // Capability layer: scenario → {tools (+ subagent roster), prompt, skills}. Heavy bits (skills,
2184
+ // git client) are built ONCE here; per request we only select a scenario and bind coordinates.
1270
2185
  let skills = loadSkills(config.skillsDir);
2186
+ // Sema-registry B1 (restart-to-apply, baked into scenarios below): overlay center skills (lazy-fetched by
2187
+ // hash + sha256-verified) over the image baseline, center-wins-by-name; resolve center MCP servers' env-NAME
2188
+ // refs to real values from this service's env. Both empty/unset → unchanged (image SKILLS_DIR, no MCP).
1271
2189
  if (effective?.skills && config.configCenter) {
1272
2190
  skills = await applyCenterSkills(skills, effective.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, configLkgEnabled() ? defaultSkillCacheDir() : undefined);
1273
2191
  }
2192
+ // plugins 域消费半场(契约 [1361]②/[1362]/[1366]①;2026-07-28 开工车):把 center 声明的插件引用
2193
+ // 物化(https-only+ALLOW_HOSTS 纵深复验/sha 钉扎/隔离 clone/LKG)并装载其 skills —— **在 center 直发
2194
+ // skills 之后**:撞名让位判据(收紧④,plugin 让位)以此刻的 skills 集为准。失败单条不连坐,永不拒启。
1274
2195
  if (effective?.plugins && config.configCenter) {
1275
2196
  const pluginOut = await applyCenterPlugins(skills, effective, {
1276
2197
  cacheRoot: join(config.localDataRoot ?? join(homedir(), ".ai-agent"), "plugin-cache"),
@@ -1287,21 +2208,44 @@ async function main() {
1287
2208
  }
1288
2209
  }
1289
2210
  }
2211
+ // F7:boot 的 LKG 落盘点。codex R26:走同一只 persistLkgDurable(完整性门在前)——此前裸 saveLkg 会把
2212
+ // applyCenterSkills 静默吞掉失败的「缺正文 manifest」发布成 boot 可见 LKG(离线重启=静默缺 skill 且不再
2213
+ // 有信号收敛)。校验失败=不落盘(warn 已在函数内),下一个干净候选/refresh 重试重写。
1290
2214
  if (bootLkgCandidate && lkgEnabled) {
1291
2215
  const bootPublished = await persistLkgDurable(bootLkgCandidate.effective, bootLkgCandidate.etag).catch((err) => {
1292
2216
  logger.warn("config_lkg_save_failed", { path: lkgPath, err: String(err), note: "boot LKG persist failed — retained for the per-tick retry" });
1293
2217
  return false;
1294
2218
  });
2219
+ // codex R30: a failed/incomplete boot publication must enter the SAME retained-candidate retry lane as the
2220
+ // refresh path — ccEtag has already advanced, so the next ticks 304 and nothing else would ever retry; a
2221
+ // restart during a center outage would then boot stale/env config instead of the generation this process
2222
+ // already applied. blocked=[] — everything WAS applied at boot, this is a publication-only retry (promotion
2223
+ // with no withheld reasons publishes NO restart signal, it just completes the LKG and clears the state).
1295
2224
  {
2225
+ // codex R32/R34: verify the boot's LIVE application UNCONDITIONALLY — applyCenterSkills skips an
2226
+ // unfetchable body (this process may run skill-stale / on an older same-name built-in), and the LKG
2227
+ // prewarm inside persistLkgDurable can succeed where the live fetch failed (bootPublished true, LKG
2228
+ // complete, process still stale). Any live miss ⇒ a skills restart: published immediately when the
2229
+ // handoff already stands, or carried in blocked for the 304-gated promotion when it doesn't.
1296
2230
  const enabledManifest = (bootLkgCandidate.effective.skills?.skills ?? []).filter((m) => m.enabled !== false);
2231
+ // codex R33: the FULL runtime projection must match, not just name+body — a same-body built-in retained
2232
+ // after a failed center fetch can carry different scenarios/description (e.g. global exposure where the
2233
+ // manifest restricts to one scenario), and the scenario bundles baked right after are then stale.
1297
2234
  const canonScenarios = (a) => [...(a ?? [])].sort().join("\u0000");
1298
2235
  const liveSkillMiss = enabledManifest.some((m) => !skills.some((sk) => sk.spec.name === m.name &&
1299
2236
  skillContentHash(sk.spec.content) === m.contentHash &&
1300
2237
  sk.spec.description === m.description &&
1301
2238
  canonScenarios(sk.scenarios) === canonScenarios(m.scenarios)));
1302
2239
  if (liveSkillMiss)
1303
- bootSkillStale = true;
2240
+ bootSkillStale = true; // codex R35: sticky — survives same-generation 200 recomputes
1304
2241
  if (liveSkillMiss && (bootPublished || (!lkgSurvivesRestart && lastPrewarmBodiesVerified))) {
2242
+ // Signal NOW only on the arms where a restart PROVABLY repairs (codex R34/R36/R37):
2243
+ // · durable + published — the complete LKG (manifest + hash-verified bodies) is the handoff;
2244
+ // · NON-durable + body proof — the prewarm inside persistLkgDurable just hash-verified every enabled
2245
+ // body on disk, so the miss was a TRANSIENT live-application failure and a restarted process
2246
+ // re-pulls successfully. WITHOUT the proof (persistent 404/hash mismatch) publishing here would
2247
+ // restart-storm a stateless deployment (codex R37) — that case falls to the retained-debt arm
2248
+ // below: visible on /health, retried per confirming 304, promoted only once the proof holds.
1305
2249
  pendingRestart = { restartRequired: true, reasons: ["skills"], version: bootLkgCandidate.effective.version, since: Date.now() };
1306
2250
  logger.warn("config_boot_skill_stale_restart", { version: bootLkgCandidate.effective.version, durableHandoff: bootPublished, note: bootPublished ? "live skill application incomplete but the durable LKG carries the full candidate — skills restart published (the next boot reads the complete LKG)" : "live skill application incomplete on a non-durable deployment but every enabled body is verified in the cache — skills restart published (transient failure proven repaired; the restarted process re-pulls live)" });
1307
2251
  }
@@ -1314,11 +2258,42 @@ async function main() {
1314
2258
  if (effective?.mcp)
1315
2259
  config.mcpServers = resolveMcpServers(effective.mcp, logger);
1316
2260
  const repoClient = config.gitApiBaseUrl ? new GiteaClient(config.gitApiBaseUrl, config.gitApiToken) : undefined;
2261
+ // CC-parity: deployment-injected WebSearch backend (the leg core leaves open). Absent WEB_SEARCH_PROVIDER →
2262
+ // undefined → the default scenario doesn't assemble the WebSearch tool. The API key stays in the backend closure.
1317
2263
  const webSearchCfg = webSearchConfigFromEnv();
1318
2264
  const webSearch = webSearchCfg ? createWebSearchBackend(webSearchCfg) : undefined;
1319
2265
  if (webSearch)
1320
2266
  logger.info(`WebSearch enabled (provider=${webSearchCfg.provider}, maxResults=${webSearch.maxResults})`);
2267
+ // core 1.382([1561] 提货单①②,design/153 parked 状态机)「同车必接」的 ensureChildSessionDurable 挂点:
2268
+ // 只在 checkpointStore 真在场时构造(与 core parkEligible 门「两者全在场才成立」的判据同源——checkpoint
2269
+ // 不在场时构造这个闭包毫无意义,连带把「local/无 durable backend 部署不该跑这条 acquire/release」的
2270
+ // 边界直接编码进构造条件,不用额外判断)。实现体拆到 `ensureSessionDurable`(session-store.ts,可独立
2271
+ // 测试;双复审②抓的 F-7 同族空壳缺口——host 店可能存在 `register()` 留下的零内容 session_meta 行——
2272
+ // 的校验也在那边)。
2273
+ // 🔴 [1575] F3 修(cli 复查,红先行确认真红):`checkpointStore` 在场 ⇏ `sessionStore` 就是 TiDB 真店
2274
+ // ——这是两个独立 config 轴(DB_BACKEND 供 checkpoint/runs 等用途 vs SESSION_BACKEND 单独控 session
2275
+ // 落哪),`SESSION_BACKEND` 显式留在默认值时 `sessionStore` 是纯内存 `TtlSessionStore`(无 repo 构造,
2276
+ // core `release()` 语义在该形态下是**整删会话**,不是"归还租约")——探针的 finally release 会销毁它
2277
+ // 刚证明存在的会话。追加门 `config.sessionBackend === "tidb"`(此刻已被前面装配区从 "auto" 归一,
2278
+ // 恒是 "tidb"|"memory" 二选一,时序安全):唯一保留可达的店形正是 release() 真安全的那种
2279
+ // (TiDBSessionStore.release 逐字注释「Durable history is retained; just drop any in-flight cache
2280
+ // entry」)。非 tidb 场景下 undefined = 子代按 pre-153 settle failed,零回归(与 checkpointStore 缺席
2281
+ // 同一处置)。
2282
+ // [1593] 只查不迁 → 主动晋升(cli TiDB 真机撞获:bg 子会话住 subRunner 私有 TTL 店、从不在 host 店,
2283
+ // 旧形恒 veto → core 当场 expire checkpoint(14ms~2s)+ settle failed,赎回窗口不存在)。晋升链=
2284
+ // 快查 host → promoteToHost(整树拷贝+私店 forget+路由记 host)→ 同一严检复验(leaf 非空);任何
2285
+ // 一步失败仍如实 veto(pre-153 行为,不放大)。tidb 门与 F3 裁定不变。
1321
2286
  const ensureChildSessionDurable = checkpointStore && config.sessionBackend === "tidb" ? (sessionId) => ensureChildSessionDurableWithPromotion(sessionStore, subRunnerSessions, sessionId) : undefined;
2287
+ // [1900]/[1904] WebFetch 摘要器:core 工厂 + summarize 角色模型(cheap tier;resolveTaskModel 自带
2288
+ // 默认回落——与压缩线同族档位,不新造)。构造一次全场景共享;多租户形 WebFetch 整体被 filter,
2289
+ // 本注入无效化由 scenarios 现行为保证。
2290
+ // 🔴 复审 E-MED-2/E-MED-3 两修:
2291
+ // ① **惰性解析**:模型每次调用现取——`config.models`/`config.roles` 会被 center 的 effective
2292
+ // 热应用 mutateInPlace 整表替换,boot 期急求值会把摘要器永久 pin 在占位/旧模型上(故障形态是
2293
+ // 静默退回上下文放大 + 每次一发无效 LLM 调用)。
2294
+ // ② **fail-soft**:core resolveTaskModel 在无可解析角色时 throw,这是全仓唯一 boot 期裸调用;
2295
+ // center 发一张既无 summarize 又无 default 的 roles 表就会拒启。解析失败 ⇒ 本次不摘要
2296
+ // (core 的 summarize 抛错是 fail-open:回退原文+note),绝不崩 boot。
1322
2297
  const webFetchSummarize = async (content, prompt, signal) => {
1323
2298
  const model = (() => {
1324
2299
  try {
@@ -1333,14 +2308,29 @@ async function main() {
1333
2308
  throw new Error("summarize model unresolved for this deployment");
1334
2309
  return createWebFetchSummarizer(brain, model)(content, prompt, signal);
1335
2310
  };
2311
+ // design/158 S4:标注 `: ScenarioDeps` —— 无标注的字面量连自己的键名都不校验(打错/多写一个键
2312
+ // 只是多一个没人读的属性,编译期无声),这是 1.304 落错家那一族的另一半土壤。
1336
2313
  const scenarioDeps = {
1337
2314
  runner, subRunner, model: "default", skills, repoClient, requirePrincipal: config.requirePrincipal,
1338
2315
  webSearch: webSearch ? webSearch : undefined, metrics, logger, webFetchSummarize,
2316
+ // core 1.364 写半场:与 RunnerDeps.backgroundAgentStore 同实例(组装区注释;半接=静默死特性)。
1339
2317
  backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
2318
+ // core 1.382 parked 状态机「同车必接」一对——checkpointStore 与 main runner 的 checkpointStore(subRunner
2319
+ // 构造区上方 const)同实例;ensureChildSessionDurable 见上方构造闭包注释。
1340
2320
  checkpointStore: checkpointStore ? checkpointStore : undefined,
1341
2321
  ensureChildSessionDurable: ensureChildSessionDurable ? ensureChildSessionDurable : undefined,
1342
2322
  oaApiBaseUrl: config.oaApiBaseUrl, oaServiceToken: config.oaServiceToken, oaIssue: config.oaIssue,
1343
2323
  brandIdentity: config.configProvider === "local",
2324
+ // core 1.287 extraTools seam ([802]③ server half): SendUserFile reaches the delegated-child roster. SAME
2325
+ // existence gate as the top-level mount (the one sendUserFileToolSpec built above — lane/tenancy/config
2326
+ // posture already encoded in its presence). The spec is execute-time late-bound (ctx.taskId →
2327
+ // TaskEnvRegistry lookup; emit via ALS to the top-level frame tail), so returning the SAME instance per
2328
+ // spawn is correct — per-spawn state lives in the registry, not the spec ([797] confirmed with core).
2329
+ // codex F1/F2 gates (subagentSendUserFileExtraTools): (F1) the subRunner carries no toolPolicy and core
2330
+ // does not propagate the parent's policy into child specs, so an APPROVAL_DENY/ask-gated SendUserFile was
2331
+ // reachable from delegated children — the factory now pre-checks the LIVE deployment approval face
2332
+ // (deny/require/neverAuto, canonical-space) and injects nothing when gated (conservative stopgap until a
2333
+ // core child-policy seam lands); (F2) read-only builtin agents (Explore/Plan) never receive the tool.
1344
2334
  subagentExtraTools: sendUserFileToolSpec
1345
2335
  ? subagentSendUserFileExtraTools({
1346
2336
  spec: sendUserFileToolSpec,
@@ -1349,7 +2339,16 @@ async function main() {
1349
2339
  : undefined,
1350
2340
  };
1351
2341
  const scenarios = buildScenarios(scenarioDeps);
2342
+ // design/153 件3d:/decide parked 赎回腿的裸 Agent 工具——boot 期从**内建 default** 场景工厂取一份
2343
+ // (必须在 center overlay 盖名之前取:赎回腿要的是与 spawn 同源的生产装配,subRunner/bg opts/
2344
+ // checkpointStore 同实例天然成立;per-request 项如 webSearch 对 revive 无关——objective 在 core
2345
+ // driveParkedResume 里被丢弃)。checkpointStore/agentStore 任一缺席 = park 本就不可达,分支不接线。
1352
2346
  const parkedReviveTool = checkpointStore && backgroundAgentStore ? scenarios.default?.({})?.tools.find((t) => t.name === "Agent") : undefined;
2347
+ // [1596]/[1597] 跨进程父约束重供席工厂:host 任务的 toolPolicy 解析槽按部署配置**同参重建**
2348
+ // (resolveSpec durableEnabled 分支的同款折叠:durableQuestion + durableAsk 四旋钮),单层链
2349
+ // count=1,durableMandate=true(host 有 durableApproval 且 durable 车道无 live onAsk 的恒形)。
2350
+ // exempt 探针锚 = row.rootSessionId——与 parked decide 的 remember grant 锚同键([1591] 候裁②),
2351
+ // 重建链上的豁免语义与原链一致。重建不出(无 root 锚)= 不供席 = core pre-CAS 门诚实拒(现状)。
1353
2352
  const parkedReviveInheritedGate = parkedReviveTool && config.durableApproval
1354
2353
  ? (row) => ({
1355
2354
  parentConstraints: [{
@@ -1366,8 +2365,13 @@ async function main() {
1366
2365
  }],
1367
2366
  })
1368
2367
  : undefined;
2368
+ // 场景详情:内建 details 必须在 overlay 合并【前】构建(探针要打纯内建工厂,不是被 center 顶掉的);
2369
+ // center details 随 overlay 同判定源盖同名——source 语义与 selectScenario 实际取用永一致(约定①)。
1369
2370
  const scenarioDetails = builtinScenarioDetails(scenarios, scenarioDeps);
1370
- const builtinScenarioNames = Object.keys(scenarioDetails);
2371
+ const builtinScenarioNames = Object.keys(scenarioDetails); // overlay 前捕获(六内建名)
2372
+ // Center-declared scenarios (composition-as-config): overlay over the built-ins, center wins — it MAY
2373
+ // shadow a built-in name (usability first), so shadows are logged loudly here for audit.
2374
+ // Restart-to-apply like skills/mcp (this runs once at boot; a refresh carrying scenarios only signals).
1371
2375
  if (effective?.scenarios) {
1372
2376
  const { overlay, shadows } = centerScenarios(scenarioDeps, effective.scenarios.scenarios, Object.keys(scenarios), logger);
1373
2377
  Object.assign(scenarios, overlay);
@@ -1376,23 +2380,59 @@ async function main() {
1376
2380
  logger.info("sema_registry_scenarios", { scenarios: Object.keys(overlay), ...(shadows.length > 0 ? { shadowsBuiltin: shadows } : {}) });
1377
2381
  }
1378
2382
  logger.info("capabilities", { skills: skills.length, scenarios: Object.keys(scenarios), repoTools: Boolean(repoClient) });
2383
+ // Background reapers: recover runs whose instance died, expire never-answered approvals, sweep
2384
+ // rate windows. Unref'd so they never hold the process open.
2385
+ // design/80 D-D (SLA-timer): the deny-sweep reuses the server's resumeCheckpoint, so it is wired in AFTER
2386
+ // createHttpServer returns it (below) via this holder — the reaper is defined before the server.
1379
2387
  let runDenySweep;
2388
+ // S7 (SILENT-FALLBACK P0-d): the sweeps' return counts were discarded — an instance death that batch-fails
2389
+ // N orphans was indistinguishable from organic failures. Count + log ONLY when a sweep flipped rows (>0),
2390
+ // so healthy ticks stay silent. Tolerant of void-returning stores (typeof guard).
1380
2391
  const reapCount = (metric, labels) => (n) => {
1381
2392
  if (typeof n === "number" && n > 0) {
1382
2393
  metrics.inc(metric, labels, n);
1383
2394
  logger.info("reaper_swept", { metric, ...labels, count: n });
1384
2395
  }
1385
2396
  };
1386
- let wfRunReapInFlight = false;
1387
- let attachmentSweepInFlight = false;
1388
- let bgAgentReapInFlight = false;
2397
+ let wfRunReapInFlight = false; // 1.108 review (lens③): serialize the all-scopes retention sweep across ticks
2398
+ let attachmentSweepInFlight = false; // 复审 F1:附件孤儿 sweep 的重入守卫(同上)
2399
+ let bgAgentReapInFlight = false; // core 1.364: same serialization for the durable bg-agent joint reap (per-scope serial loop)
2400
+ // core 1.364([1503] 提货单③):retention **只走** reapDurableAgents(联合 reap:条件删赢了才 release
2401
+ // 转录 session;裸 store.reap 会 strand 转录)。staleRunning 翻转在 core 编排内先行(store.reap 一步)。
2402
+ // scope 枚举:core 1.368 `BackgroundAgentStore.listScopes?()`([1516]② 交付,SQL twins/File 双实现
2403
+ // 都带)——1.248 拍的「file 腿只扫 default」假设已撤;无该方法的第三方 store 仍回落 "default"(可选
2404
+ // 接口成员,诚实回落)。
2405
+ // sessions=subRunner.sessions —— [1522] 裁定件(③ 显式裁定形,双店真相考据后拍):bg 子代转录会话
2406
+ // 是**双店**(ForkRoutingSessionStore):①非 fork 子代铸在 transient 店(TtlSessionStore,进程内
2407
+ // 1h TTL)——release=内存删=**真正终结可寻址性**(转录本就不 durable;跨实例读面读的是行内
2408
+ // finalOutput,非转录);②fork 子代铸在 host durable 店——release=lease 释放,durable 转录行**有意
2409
+ // 保留**,留存归 session 留存策略/E21 purge 生命周期管(行先删可接受:行是执行记录,转录是会话资产,
2410
+ // 两者生命周期本就不同)。core [1522] 复审只见 durable 腿判 HIGH——按其契约句「做不到终结可寻址性
2411
+ // 宁可不传」,transient 类做得到、fork 类是显式裁定,维持传入。sessionsReleased 计数=「store 侧
2412
+ // release 被调次数」(transient=真删,durable=lease 释放),非「durable 转录处置数」——如实注。
2413
+ // 复审 F4 如实注:条件删的赢者若非写者副本,fork-routing 对未知 id 回落 transient=本地 no-op,写者
2414
+ // 进程内 pinned 转录滞留到自身 TTL/进程退出——sessionsReleased 计数偏低是诚实读数,非缺陷。
1389
2415
  const reapBgAgents = backgroundAgentStore
1390
2416
  ? async () => {
1391
2417
  const store = backgroundAgentStore;
1392
2418
  const scopes = await (store.listScopes?.() ?? Promise.resolve(["default"]));
1393
2419
  let rows = 0, sessions = 0, skipped = 0, failedScopes = 0;
1394
2420
  for (const scope of scopes) {
2421
+ // [1522] LOW3:per-scope 错误隔离——一个 scope 抛(坏 record_json/权限)不饿死后续 scope;
2422
+ // 失败响亮(warn+计数),下 tick 重试。
1395
2423
  try {
2424
+ // core 1.382([1561] 提货单④,design/153 parked 状态机对账):先对账 parked 行(checkpoint
2425
+ // expired/missing → 行诚实翻 failed;stale claim → 回滚 parked),再让下面既有的
2426
+ // reapDurableAgents 按自然节奏处置(刚翻 failed 的行本 tick 不会立刻被 maxAge 删——它是
2427
+ // "刚失败"的新行,不是"失败很久"的老行)。**走 registry 面**(裂脑 fence 内建于
2428
+ // TaskRegistry.reconcileParkedAgents 内部,不裸调 store 面——那是无活实例场景专用,core
2429
+ // [1561]④ 措辞)。仅在 checkpointStore 真在场时跑(同 [1561]①②「同车必接」判据——没有
2430
+ // checkpointStore 就不可能有真正 parked 的行,调用本身没有意义)。
2431
+ // 🔴 [1575] F1 修(cli 复查,红先行确认):`opts.staleClaimMaxAgeMs` 必须显式传——core 把
2432
+ // 整个 stale-claim 清算块(claimer 崩死回滚 + resolved-未-finalize 诚实翻 failed)门在
2433
+ // `staleMs !== undefined` 上,缺席不是"跳过 stale-claim 这一小步",是**整块永不运行**
2434
+ // (此前漏传,`bg_agents_park_reconciled_rolledback_total` 恒 0,人已做出的审批决定会被
2435
+ // 静默丢弃——见 config.ts backgroundAgentParkClaimStaleMs 顶注)。
1396
2436
  if (checkpointStore) {
1397
2437
  const pr = await defaultTaskRegistry.reconcileParkedAgents({ agentStore: store, checkpointStore }, scope, Date.now(), { staleClaimMaxAgeMs: config.backgroundAgentParkClaimStaleMs });
1398
2438
  if (pr.failed > 0 || pr.rolledBack > 0) {
@@ -1401,6 +2441,8 @@ async function main() {
1401
2441
  logger.info("reaper_swept", { metric: "bg_agents_park_reconciled", scope, failed: pr.failed, rolledBack: pr.rolledBack });
1402
2442
  }
1403
2443
  }
2444
+ // deps.mailbox(core 1.374,[1533]②):行删联动 F-13——agent 行 reap 时同 drop 其信箱
2445
+ // (盒生命周期随行终结;不挂=信箱行无 retention 无界涨)。
1404
2446
  const r = await defaultTaskRegistry.reapDurableAgents(scope, { store, sessions: subRunner.sessions, ...(mailboxStore ? { mailbox: mailboxStore } : {}) }, {
1405
2447
  maxAgeMs: config.backgroundAgentRetentionMs,
1406
2448
  staleRunningMaxAgeMs: config.backgroundAgentStaleRunningMs,
@@ -1422,7 +2464,12 @@ async function main() {
1422
2464
  }
1423
2465
  }
1424
2466
  : undefined;
2467
+ // S10 (SILENT-FALLBACK P1): the write-behind counters bucket on LOCAL floor(now/windowMs) — replica clock
2468
+ // skew vs the DB splits a fleet window into disjoint buckets (soft-limit leak) with zero visibility. Probe
2469
+ // the DB clock each reaper tick; the gauge is the fleet-wide skew fingerprint (can be negative).
1425
2470
  const probeClockSkew = async () => {
2471
+ // Probe-ok gauge: a failing probe froze the skew gauge at its last value with zero signal —
2472
+ // "probe ran, skew=N" vs "probe failing for 30 min" were indistinguishable.
1426
2473
  try {
1427
2474
  const dbMs = await backend?.dbNowMs?.();
1428
2475
  if (typeof dbMs === "number" && Number.isFinite(dbMs)) {
@@ -1435,24 +2482,54 @@ async function main() {
1435
2482
  }
1436
2483
  };
1437
2484
  const reaper = setInterval(() => {
1438
- void probeClockSkew().catch(() => undefined);
2485
+ void probeClockSkew().catch(() => undefined); // S10
1439
2486
  void runStore?.reapStale(config.runStaleSec * 1000).then(reapCount("runs_reaped_total", { kind: "stale" })).catch(() => undefined);
2487
+ // Durable F4 (design/45) + design/80 §3 inv#3 crash-safe backstop: CAS-expire checkpoints past their
2488
+ // deadline OR their absolute terminal_at backstop (≈ deny), then fail the suspended run rows whose
2489
+ // checkpoint was thereby expired (release task_active = unlock the session). These run EVERY tick,
2490
+ // REGARDLESS of APPROVAL_TIMEOUT_SEC — terminal_at (stamped at put, never before an explicit deadline)
2491
+ // bounds even a NULL-deadline pending checkpoint, so the backstop is the always-on safety net (it was
2492
+ // inert when nested under the approvalTimeoutSec>0 guard — adversarial finding). The run-row half is
2493
+ // checkpoint-STATE-driven (not a uniform timer) so it aligns with the per-row terminal_at. Global +
2494
+ // idempotent across replicas, no election.
1440
2495
  if (checkpointStore) {
2496
+ // 轴A #5 注释落档(1.254):core CheckpointStore.reap 契约把弃置臂的 unpin 义务派给部署 reaper——
2497
+ // 真 pin 只存在于 in-memory TtlSessionStore;本部署 durable checkpoint 恒配 durable session 后端
2498
+ // (pin=no-op),deny-sweep 走 resumeCheckpoint=core 内部 unpin ✓。SESSION_BACKEND=memory+durable
2499
+ // checkpoint 的 niche dev 组合下 abort-expire 臂会把被钉会话泄到进程终——显式接受,不为 dev 形加腿。
1441
2500
  void checkpointStore.reapExpired(Date.now()).then(reapCount("checkpoints_reaped_total", {})).catch(() => undefined);
1442
2501
  void runStore?.failSuspendedWithExpiredCheckpoint().then(reapCount("runs_reaped_total", { kind: "expired_checkpoint" })).catch(() => undefined);
2502
+ // D-D SLA-timer: resolve-DENY human/irreversible_ask gates past their deadline (graceful — the model
2503
+ // continues with the denial), vs the abort reapExpired gives the other kinds. Bounded per tick.
1443
2504
  void runDenySweep?.(Date.now()).catch(() => undefined);
1444
2505
  }
2506
+ // The finer, OPT-IN per-approval TTL sweeps (APPROVAL_TIMEOUT_SEC): the F4 poll-gate store + the
2507
+ // time-based suspended-run reaper. Stay gated — reapSuspended with a 0 TTL would nuke ALL suspended rows
2508
+ // (cutoff = now). The absolute backstop above is the safety floor; this is the operator-chosen deadline.
1445
2509
  if (config.approvalTimeoutSec > 0) {
1446
2510
  void approvalStore?.expireStale(config.approvalTimeoutSec * 1000).catch(() => undefined);
1447
2511
  void runStore?.reapSuspended(config.approvalTimeoutSec * 1000).then(reapCount("runs_reaped_total", { kind: "suspended" })).catch(() => undefined);
1448
2512
  }
2513
+ // GC checkpoint_ctx rows whose checkpoint is gone (bound to the checkpoint lifecycle).
1449
2514
  void checkpointStore?.reapCtx(Date.now() - config.runStaleSec * 1000).catch(() => undefined);
1450
2515
  rateLimiter?.sweep();
1451
2516
  if (costQuota instanceof CostQuota)
1452
- costQuota.reap();
1453
- void toolResultStore?.reapOlderThan?.(Date.now() - config.toolResultTtlSec * 1000)?.catch(() => undefined);
2517
+ costQuota.reap(); // TiDB variant self-reaps in its flush loop
2518
+ void toolResultStore?.reapOlderThan?.(Date.now() - config.toolResultTtlSec * 1000)?.catch(() => undefined); // optional extra: SQL twins only (the local FileToolResultStore persists like transcripts)
2519
+ // 2c session-sync: GLOBALLY GC grace-window orphan blobs (standalone /sync/blobs PUTs that were never
2520
+ // imported are otherwise collected only on reap()/deleteBySession(), which never fire for a never-imported scope).
2521
+ // Bounds the standalone-PUT orphan-blob exhaustion (review finding) to the grace window. Durable twins only (local
2522
+ // omits the seam → optional-chained no-op).
1454
2523
  void fileSnapshotStore?.sweepOrphanBlobs?.().catch(() => undefined);
2524
+ // D-1 附件 TTL:上传后从未被任何 task 引用(session_id NULL)且超过 attachmentUnboundTtlMs 的行收割
2525
+ // (绑定行不在此收——随会话 E21 级联删)。best-effort,与其余 reaper 腿同姿。
1455
2526
  void taskAttachmentStore?.reapUnbound(Date.now() - config.attachmentUnboundTtlMs).then(reapCount("attachments_reaped_total", {})).catch(() => undefined);
2527
+ // D-1 孤儿**对象**彻底 GC(clay 拍 2026-07-28):对象存储 × meta 行对账,grace 默认 1h(上传先行窗
2528
+ // 保护;`ATTACHMENT_ORPHAN_GRACE_MS=0` 关掉本腿——同 tick 邻居都有旋钮,复审 F10)。
2529
+ // 列举失败=本轮 warn 跳过(「列不出来」绝不当「没有孤儿」),下轮再试。
2530
+ // 🔴 复审 F1:必须 in-flight 守卫——本腿时长随对象总数增长(20 万对象实测秒级、百万级可达 30s),
2531
+ // 无守卫时超过 tick 间隔即逐 tick 叠加(实测叠 6 层),同时压 LIST/SQL/DELETE 三面。邻居
2532
+ // wfRunReap/bgAgentReap 都有同款守卫。
1456
2533
  if (config.attachmentOrphanGraceMs > 0 && !attachmentSweepInFlight && taskAttachmentStore?.sweepOrphanObjects) {
1457
2534
  attachmentSweepInFlight = true;
1458
2535
  void taskAttachmentStore
@@ -1464,29 +2541,61 @@ async function main() {
1464
2541
  .catch((err) => logger.warn("attachment_orphan_sweep_failed", { err: String(err) }))
1465
2542
  .finally(() => { attachmentSweepInFlight = false; });
1466
2543
  }
2544
+ // P1d-β 2c session-sync: GC ABANDONED staged imports — staging-id session_event rows (`%#stg-%`) with NO
2545
+ // session_meta whose oldest row is older than the grace window (an in-flight stream stays fresh → never reaped).
2546
+ // Bounds the orphan-staging-row growth (a Phase B that opened a staging then never committed). Durable session
2547
+ // stores only (the local backend stages in memory → no durable rows; the seam is absent → optional-chained no-op).
1467
2548
  void backend?.session()?.sweepStagingSessions?.().catch(() => undefined);
2549
+ // Bake-runner backstop (§P2.7): fail any `running` bake whose lease went stale (the runner crashed mid-build),
2550
+ // append a synthetic terminal `done{failed}` so SSE readers settle, and force-release the single-flight lease.
2551
+ // staleMs (≈3× the 30s heartbeat) bounds a healthy slow build so it is never wrongly reaped.
1468
2552
  void imageBakes?.reapStaleBakes(config.imageBakes.staleMs).then(reapCount("bakes_reaped_total", {})).catch(() => undefined);
2553
+ // SVC-3 worktree isolation: deregister worktrees orphaned by a process crash (the Runner never reached
2554
+ // destroy → `git worktree remove` never ran). `git worktree prune` cleans registrations whose dirs are
2555
+ // already gone. Best-effort, userland (core ships no post-kill Runner hook); never throws. Unset = no-op.
1469
2556
  void worktreeReap?.();
2557
+ // SVC-1 (adversarial-review HIGH): PERIODIC notify-recovery sweep (not just at boot) — re-delivers a terminal
2558
+ // run whose in-process notify was lost, AND finalizes-as-abandoned a `running` run orphaned past the grace
2559
+ // window (core never resumes/reaps a prior `running` row, so nothing else would). Idempotent; best-effort.
1470
2560
  void workflowNotifyGate?.recover({ orphanGraceMs: config.workflowOrphanGraceMs }).catch(() => undefined);
2561
+ // SVC-2 (adversarial-review HIGH): time-based GC of the workflow_journal table (the heaviest, TaskResult-bearing
2562
+ // one) — the per-run deleteByRun has no run-store reap hook, so this bounded sweep is what stops unbounded
2563
+ // growth. A resume of a journal older than the retention window re-runs live (resume is an optimization).
1471
2564
  void workflowJournalStore?.reapExpired?.(Date.now(), config.workflowJournalRetentionMs).catch(() => undefined);
2565
+ // Retention (WorkflowRunStore never auto-purges, reap is explicit):
2566
+ // age out TERMINAL workflow_run rows across ALL scopes. SQL twins only (reapAllScopes is their DISTINCT-scope
2567
+ // extension; the contract itself has no cross-scope enumeration, and the File store keeps the transcripts-like
2568
+ // keep-everything posture). Running rows are untouched — the orphan-grace sweep above owns those.
2569
+ // In-flight guard (1.108 review, lens③): the sweep enumerates EVERY scope serially — under a short
2570
+ // REAP_INTERVAL_SEC + many scopes, overlapping sweeps would pile up on the pool. One at a time.
1472
2571
  if (!wfRunReapInFlight) {
1473
2572
  const sweep = sqlWorkflowRunStore?.reapAllScopes?.(Date.now(), { maxAgeMs: config.workflowRunRetentionMs });
1474
2573
  if (sweep) {
1475
2574
  wfRunReapInFlight = true;
1476
2575
  void sweep.catch(() => undefined).finally(() => (wfRunReapInFlight = false));
1477
2576
  }
2577
+ // 1.108: SQL notify-journal retention rides the same knob — ACKED rows are pure history (pending rows are
2578
+ // the recovery backlog and are NEVER reaped; the orphan-grace sweep retires a stuck pending run).
1478
2579
  void workflowNotifyJournal
1479
2580
  ?.reapAcked?.(Date.now() - config.workflowRunRetentionMs)
1480
2581
  .catch(() => undefined);
1481
2582
  }
2583
+ // core 1.364 durable bg-agent joint reap(定义在 interval 上方,契约注释在彼)。in-flight 守卫同
2584
+ // wfRun sweep(per-scope 串行循环,短 tick + 多 scope 下不叠罗汉)。
1482
2585
  if (reapBgAgents && !bgAgentReapInFlight) {
1483
2586
  bgAgentReapInFlight = true;
1484
2587
  void reapBgAgents().catch(() => undefined).finally(() => (bgAgentReapInFlight = false));
1485
2588
  }
2589
+ // [1522] MED2:agent_roster TTL 清理(core RB-23②③ 派给部署的半场)——SQL twins 扩展面
2590
+ // (reapOlderThan,duck probe;File/Memory 店 core 自带 maxAgeMs,无此面=no-op)。
1486
2591
  void rosterStore
1487
2592
  ?.reapOlderThan?.(Date.now() - config.rosterRetentionMs)
1488
2593
  .then(reapCount("roster_rows_reaped_total", {}))
1489
2594
  .catch(() => undefined);
2595
+ // 修8(三路复审 absorb-2,接线 (b)):periodic scratchpad sweep — 无 E21 purge 兜到的孤儿目录(session 从未
2596
+ // DELETE、purge 当次失败、local 后端无 purge coordinator)按 mtime 过期回收。SCRATCHPAD_SWEEP_TTL_MS
2597
+ // (default 7d,0=禁用)。activeSessionIds 不传:活跃判据的诚实边界在 env-facts.ts 的 sweep 文档——TTL 7d
2598
+ // 远大于任何在飞任务;一个 7 天零写入的 scratchpad 被回收是可接受的(它本就是临时区,fact 文案即如此宣示)。
1490
2599
  if (config.scratchpadSweepTtlMs > 0) {
1491
2600
  void sweepStaleScratchpads(config.localDataRoot ?? localRoot, { olderThanMs: config.scratchpadSweepTtlMs })
1492
2601
  .then((removed) => {
@@ -1497,29 +2606,69 @@ async function main() {
1497
2606
  }
1498
2607
  }, config.reapIntervalSec * 1000);
1499
2608
  reaper.unref?.();
2609
+ // Optional OTLP/HTTP metrics export (1.37). Periodically pushes the registry to an OTel collector;
2610
+ // best-effort (a collector outage is logged, never affects serving). /metrics stays available too.
1500
2611
  const otelExporter = config.otel
1501
2612
  ? startOtlpExporter(metrics, {
1502
2613
  ...config.otel,
1503
2614
  onError: (err) => logger.warn("otel_export_failed", { err: String(err) }),
1504
2615
  })
1505
2616
  : undefined;
2617
+ // Config refresh: re-pull with the ETag; teams hot-reload via the registry. A models/roles
2618
+ // change updates config but the live Runner keeps its startup set — logged so ops restart to apply.
2619
+ // 提示词装配协议 §9.5-1/-3: the loop goes through the ConfigProvider seam (never the raw HTTP fetch —
2620
+ // one resolver, no remote/local drift) and runs for the LOCAL provider too: config.d edits are picked
2621
+ // up by the same 60s poll (the local provider's etag = the store's raws hash, so an unchanged tree is
2622
+ // a cheap null and a corrupt↔fixed flip bumps the version). Local mode previously read config.d
2623
+ // exactly once at boot.
1506
2624
  if (configProvider) {
1507
2625
  const ccRef = config.configCenter;
2626
+ // Single-flight (codex F2): an async tick body under setInterval has no serialization — a slow read
2627
+ // (degraded disk/远端) outliving the 60s interval would let a LATER tick apply a newer candidate and
2628
+ // the stalled OLDER read then complete and roll the config (and ccEtag) back. Skip the tick while one
2629
+ // is in flight; the flag flips in `finally`, so a throw never wedges the loop shut.
1508
2630
  let refreshInFlight = false;
1509
2631
  const refreshTick = async (prefetched) => {
1510
2632
  if (refreshInFlight)
1511
2633
  return;
1512
2634
  refreshInFlight = true;
1513
2635
  try {
2636
+ // codex-3/4 F1:欠账的 catalog 过渡在**每个真拍**开头无条件重试——与 fetch 成败/候选好坏解耦
2637
+ // (304、fetch 抛错、候选被整拒的拍此前全都跳过重试=盘错恢复后过渡仍卡死)。次序安全:本拍
2638
+ // 若随后采用了更新候选,adopt 自会覆盖/清空 pending(先重试旧账、后采新目标,永不倒退)。
1514
2639
  await retryPendingCatalog("refresh-retry");
2640
+ // codex R18: the stuck-deferred state must not vanish behind the advanced etag — re-warn + count on
2641
+ // EVERY tick (incl. 304s and fetch-error ticks) while it persists; /health carries the same state.
2642
+ // The RECOVERY (persist retry + promotion) moved BELOW the fetch (codex R29): promoting before
2643
+ // adjudicating the current source generation could publish a SUPERSEDED candidate N as the
2644
+ // boot-visible LKG while N+1 exists — a restart in that window resurrects withdrawn config.
1515
2645
  if (planeDeferredNoHandoff) {
1516
2646
  metrics.inc("models_tiers_plane_deferred_stuck_total");
1517
2647
  logger.warn("models_tiers_deferred_no_handoff", { version: planeDeferredNoHandoff.version, since: planeDeferredNoHandoff.since, note: "candidate still retained without a next-boot handoff — mount restart-surviving LKG storage + CONFIG_LKG_DURABLE=true, or restart manually after ensuring the candidate is re-pulled at boot" });
1518
2648
  }
2649
+ // F3(LKG 复审):LKG-boot 的 deferred 到货把 unconditional 结果直接喂进来——丢弃它再按 LKG 的
2650
+ // etag 条件拉,会在「center 重建且 etag 值撞旧」的场景永远 304=陈旧配置钉死;消费原始结果则
2651
+ // 应用后 ccEtag=真实新值,陈旧面消除(顺带省一次拉取)。
1519
2652
  const r = prefetched !== undefined ? prefetched : await configProvider.fetchEffective(ccEtag);
2653
+ // codex R23/R29 recovery lane: a 304 (r === null) CONFIRMS the retained candidate's generation is
2654
+ // still the source's current one (its etag advanced when it was retained) — only then is it safe to
2655
+ // retry persistence and promote the withheld restart signal. A fetch that returned a NEWER candidate
2656
+ // takes the normal branches below (which replace/supersede the retained state); a fetch that THREW
2657
+ // never reaches here (freshness unknown ⇒ no promotion this tick). Retry only where promotion is
2658
+ // possible (durable-declared lane; R37 adds the non-durable skills-debt lane, whose retry doubles as
2659
+ // the repair proof) — elsewhere a per-tick write could never promote and just burns IO.
1520
2660
  if (r === null && planeDeferredNoHandoff) {
2661
+ // Retry where promotion is possible: the durable-declared lane (full handoff), and — codex R37 —
2662
+ // the NON-durable skills-debt lane, where the retry's body prewarm+verify is the repair PROOF that
2663
+ // gates the withheld skills restart (see lastPrewarmBodiesVerified).
1521
2664
  const skillsDebt = (planeDeferredNoHandoff.blocked ?? []).includes("skills");
1522
2665
  const promoted = lkgSurvivesRestart || skillsDebt ? await persistLkgDurable(planeDeferredNoHandoff.candidate, planeDeferredNoHandoff.candidateEtag) : false;
2666
+ // codex R37 proof-gated promotion (non-durable lane): every enabled body now hash-verifies on disk
2667
+ // ⇒ the center + body endpoint are BOTH live, a restarted process re-pulls successfully — publish
2668
+ // the skills restart exactly now, never on manifest-success alone (restart-storm breaker: a boot
2669
+ // that fails the body again lacks the proof and returns to visible-debt, not to signaling).
2670
+ // models-tiers never rides this arm (R16: a FORCED plane restart requires a durable handoff), so a
2671
+ // retained candidate that ALSO defers the plane keeps everything withheld until durability exists.
1523
2672
  const skillsProofPromotion = !promoted && !lkgSurvivesRestart && skillsDebt && lastPrewarmBodiesVerified && planeDeferredNoHandoff.planeDeferred !== true;
1524
2673
  if (skillsProofPromotion) {
1525
2674
  pendingRestart = { restartRequired: true, reasons: ["skills"], version: planeDeferredNoHandoff.version, since: Date.now() };
@@ -1527,6 +2676,10 @@ async function main() {
1527
2676
  planeDeferredNoHandoff = undefined;
1528
2677
  }
1529
2678
  else if (promoted) {
2679
+ // models-tiers joins only when the PLANE was deferred (codex R27: a skill-only retained candidate
2680
+ // must not manufacture a plane reason on promotion). codex R30: an EMPTY promotion (boot
2681
+ // publication retry — nothing was withheld, everything already applied) publishes NO restart
2682
+ // signal; it just completes the LKG and clears the state.
1530
2683
  const promotedReasons = [...new Set([...(planeDeferredNoHandoff.blocked ?? []), ...(planeDeferredNoHandoff.planeDeferred ? ["models-tiers"] : [])])];
1531
2684
  if (promotedReasons.length > 0) {
1532
2685
  pendingRestart = { restartRequired: true, reasons: promotedReasons, version: planeDeferredNoHandoff.version, since: Date.now() };
@@ -1536,6 +2689,17 @@ async function main() {
1536
2689
  }
1537
2690
  }
1538
2691
  if (r) {
2692
+ // §9.5-5 candidate gate: ANY invalid domain rejects the WHOLE candidate — the live config
2693
+ // keeps serving the LKG (`latestEffective`) instead of that domain falling to schema-default/
2694
+ // empty. ccEtag is deliberately NOT advanced: the bad tree stays visible (re-judged each poll),
2695
+ // and fixing the file bumps the version so the next poll lands. The dedupe key is the SORTED
2696
+ // de-duplicated domain set (codex F3: the collector's ordering isn't guaranteed, a raw join
2697
+ // would re-warn every poll), and the first warn carries the full per-domain error detail —
2698
+ // that's the actionable diagnosis, the key only mutes repeats. The prompts-domain artifact
2699
+ // validation (core S1) hooks into this same gate when it ships.
2700
+ // F6(二轮复审):prompts 面判形进候选门——坏 pack(重复 id/空数组/坏信封)若只在 boot 验,
2701
+ // refresh 会带着它推进 etag+发 restart 信号,重启后 boot 拒之回内置=好包 A 被坏包 B 顶掉。
2702
+ // 这里与 domainErrors 同门:坏形=整拒候选(etag 不进,LKG/现用包 A 继续服务,持续 warn 到修复)。
1539
2703
  const promptsRaw = r.effective.prompts;
1540
2704
  const promptsGate = promptsRaw !== undefined ? validatePromptsDomain(promptsRaw) : { ok: true };
1541
2705
  const badDomains = [
@@ -1560,11 +2724,28 @@ async function main() {
1560
2724
  return;
1561
2725
  }
1562
2726
  lastRejectedCandidate = undefined;
2727
+ // ccEtag advances at the END of each branch (codex lens-1 HIGH): advancing before apply meant
2728
+ // an applyEffective throw mid-way left a MIXED-generation live config AND a cached etag — the
2729
+ // next poll 304'd, so the failed candidate was never retried. Now a throw keeps the old etag,
2730
+ // the next poll re-fetches the same candidate, and the (idempotent) re-apply self-heals.
1563
2731
  if (ccRef?.dryRun) {
1564
- logEffectiveDiff(config, r.effective, logger);
2732
+ logEffectiveDiff(config, r.effective, logger); // compare-only: surface ongoing center changes, apply nothing
1565
2733
  ccEtag = r.etag;
1566
2734
  }
1567
2735
  else {
2736
+ // refresh: teams + runtime governance + MODELS/ROLES/ROSTER now hot. teamsOnly skips
2737
+ // only the boot-only runtime GATES (rate/cost/approval). applyEffective mutates config.models/roles
2738
+ // IN PLACE (the Runner's live ref) — but `pricing` (separate object the Runner also reads live) and
2739
+ // `keyResolver` (closes over modelApiKeyEnv) are ours to refresh.
2740
+ // codex R10: hot-apply of the MODEL PLANE holds ONLY when the Runner shares our map (tier-less at
2741
+ // construction). A tier-frozen Runner keeps a private expanded copy — hot-applying a changed plane
2742
+ // would split admission from execution (same-key retarget silently routes/bills the stale Model
2743
+ // object through the restart window). Defer the plane; the models-tiers slice below signals the
2744
+ // orchestrator; the restarted process applies it at boot BEFORE Runner construction.
2745
+ // codex R12: defer also when the CANDIDATE activates tiers on a tier-less-booted Runner — it never
2746
+ // re-runs expandTiers, so hot-applying the arriving tier table would let tier words pass the
2747
+ // expanded admission gate while core throws "Unknown model ref" until restart. Hot-apply is safe
2748
+ // only when BOTH generations are tier-less.
1568
2749
  const planeDeferred = (runnerTierFrozen || planeHasActiveTiers(r.effective)) && modelPlaneChanged(appliedPlaneEff, r.effective);
1569
2750
  applyEffective(config, r.effective, logger, { teamsOnly: true, sealedKeys, ...(planeDeferred ? { deferModelPlane: true } : {}) });
1570
2751
  if (planeDeferred) {
@@ -1572,36 +2753,87 @@ async function main() {
1572
2753
  }
1573
2754
  else {
1574
2755
  appliedPlaneEff = r.effective;
1575
- planeDeferredNoHandoff = undefined;
2756
+ planeDeferredNoHandoff = undefined; // plane landed — the stuck state (if any) is resolved
1576
2757
  }
1577
- await adoptCenterPrompts(r.effective, "refresh");
2758
+ await adoptCenterPrompts(r.effective, "refresh"); // 热采用(core 1.316 声明轴:新任务边界自动重 pin,见 ref 注释)
2759
+ // codex R13: readiness may open ONLY off an APPLIED plane — a deferred candidate's roster is not
2760
+ // installed (config.models still the old/placeholder generation), so opening the billable gate on
2761
+ // it would route/bill the wrong model until restart. Deferred ⇒ gate stays as-is; the restarted
2762
+ // process applies the candidate pre-Runner and opens it at boot.
1578
2763
  if (!planeDeferred)
1579
- markRosterLanded(r.effective);
1580
- mutateInPlace(pricing, buildPricing(config.models));
1581
- keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
2764
+ markRosterLanded(r.effective); // boot pull 失败/未发布时,refresh 落 roster 同样开门
2765
+ mutateInPlace(pricing, buildPricing(config.models)); // hot: cost/model changes; Runner reads this.deps.pricing live
2766
+ keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys); // hot: per-model key add/remove/change (env-ref + sealed)
2767
+ // skills/mcp/scenarios/runtime-gates are baked into the live process at boot (buildScenarios / boot
2768
+ // middleware) — a refresh carrying a DIFFERENT value can't hot-apply, only a restart re-reads them.
2769
+ // We compare against the BOOT snapshot (`effective`), NOT presence: an orchestrator auto-restarts on
2770
+ // a non-empty `reasons` WITHOUT a restart loop (same diff every 60s is idempotent, not a fresh trigger).
2771
+ // Sticky `since`: keep the first-detected timestamp while `reasons` is unchanged; reset it when the set
2772
+ // changes; CLEAR the signal when a later pull reverts to boot (reasons empty) — restart no longer needed.
2773
+ // F2(LKG 复审):落盘 **await 且先于** restart 信号可见——orchestrator 见信号即杀进程,
2774
+ // fire-and-forget 的 rename 可能没完成 → 下一进程读旧 LKG → 同差异重现 = 本功能要消的环。
2775
+ // 顺序=先持久化(含 skill 正文预热,F1)再发信号;持久化失败=warn+照发(不发=配置永不生效;
2776
+ // 「盘坏+中心挂」双故障下环重现,接受并点名)。
1582
2777
  const lkgPersisted = await persistLkgDurable(r.effective, r.etag);
1583
2778
  const reasons = restartReasons(effective, r.effective);
2779
+ // codex R35: the sticky boot skill debt rides EVERY tick's reasons — change-detection vs boot is
2780
+ // blind to it (the stale table IS the boot baseline). Under a no-handoff deferral it folds into
2781
+ // blocked (promoted later); otherwise it keeps pendingRestart alive until the restart happens.
2782
+ // codex R38: change-DETECTED skills (manifest actually changed) is distinguished from the sticky
2783
+ // rider — the former keeps the pre-existing documented posture (rides regardless; the restart
2784
+ // pulls the NEW generation live), the latter is proof-gated below on the non-durable lane.
1584
2785
  const skillsChangeDetected = reasons.includes("skills");
1585
2786
  if (bootSkillStale && !skillsChangeDetected)
1586
2787
  reasons.push("skills");
2788
+ // codex R15/R16/R19: a DEFERRED plane is adjudicated by HANDOFF STATUS alone — independent of
2789
+ // whether change-detection already produced the reason (R19: an already-present models-tiers with
2790
+ // no handoff sailed through = the exact storm R16 closed). With a confirmed next-boot handoff the
2791
+ // reason is ensured present (R15: env-baseline blind spot) and the stuck state clears; without one
2792
+ // the reason is SUPPRESSED (a restart the next process cannot satisfy — over-budget boot pull + no
2793
+ // LKG — is a rolling-restart storm) and the persistent stuck state + per-tick warn carry the
2794
+ // observability instead. Non-plane reasons (skills/mcp/…) keep riding either way.
1587
2795
  if (planeDeferred) {
1588
2796
  if (lkgPersisted) {
1589
2797
  if (!reasons.includes("models-tiers"))
1590
2798
  reasons.push("models-tiers");
1591
- planeDeferredNoHandoff = undefined;
2799
+ planeDeferredNoHandoff = undefined; // handoff exists — the restart signal carries observability
1592
2800
  }
1593
2801
  else {
2802
+ // codex R22: suppress the candidate's ENTIRE restart signal, not just models-tiers — a
2803
+ // co-occurring skills/mcp reason would still restart the worker, and with no handoff the
2804
+ // replacement can't land ANY of this candidate (same over-budget pull) = the same storm.
2805
+ // The blocked reasons stay observable (state + warn) and promote once a handoff exists.
1594
2806
  const blocked = reasons.splice(0, reasons.length);
1595
2807
  planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), ...(blocked.length > 0 ? { blocked } : {}), planeDeferred: true, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
1596
2808
  logger.warn("models_tiers_deferred_no_handoff", { version: r.effective.version, blockedReasons: blocked, note: "plane deferred but no NEXT-BOOT handoff (LKG disabled/unwritable, or storage not declared restart-surviving — set CONFIG_LKG_DURABLE=true) — the WHOLE restart signal is withheld (it would loop); the old generation keeps serving" });
1597
2809
  }
1598
2810
  }
1599
2811
  else if (lkgSurvivesRestart && !lkgPersisted) {
2812
+ // codex R27/R31: retention is NOT conditional on restart reasons — a hot-only candidate
2813
+ // (autonomy/prompts/teams/tier-less models) with a failed publication would otherwise advance
2814
+ // the etag and never retry (304s), and a restart during a center outage would roll back to the
2815
+ // older LKG (weaker governance). blocked may be [] = publication-only recovery (R30 promotion
2816
+ // publishes no signal for an empty set).
2817
+ // codex R27: a DURABLE-DECLARED deployment whose candidate publication is incomplete (skill-body
2818
+ // verify failed → LKG not replaced) must not signal restart off that candidate either — the next
2819
+ // boot would land the OLD LKG and re-signal while the center stays unavailable. Retain + withhold
2820
+ // + per-tick retry-promote, same machinery as the deferred plane (no models-tiers added on
2821
+ // promotion — the plane was NOT deferred here). Non-declared deployments keep the pre-existing
2822
+ // documented posture (restart converges off a live boot pull when the center is healthy).
1600
2823
  const blocked = reasons.splice(0, reasons.length);
1601
2824
  planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), blocked, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
1602
2825
  logger.warn("restart_candidate_publication_incomplete", { version: r.effective.version, blockedReasons: blocked, note: "durable LKG declared but the candidate's publication is incomplete (skill bodies unverified) — restart signal withheld; retried every tick until the cache completes" });
1603
2826
  }
1604
2827
  else if (!planeDeferred && !lkgSurvivesRestart && bootSkillStale && !skillsChangeDetected && !lastPrewarmBodiesVerified) {
2828
+ // codex R38: the NON-durable lane's sticky rider must clear the SAME proof gate as the boot/304
2829
+ // arms (R37) — a same-generation 200 (center omits/rotates ETags or ignores conditionals) lands
2830
+ // here with reasons=[skills] purely from the sticky debt, and publishing it unproven re-opens
2831
+ // the restart storm R37 closed (every replacement fails the same body, 200s again, signals
2832
+ // again). persistLkgDurable just ran for THIS candidate, so the proof flag is current: unproven
2833
+ // ⇒ strip the rider from the publishable set and retain it as visible debt (the same
2834
+ // planeDeferredNoHandoff machinery the 304 recovery lane re-verifies and proof-promotes).
2835
+ // Proven or durable-handoff ⇒ the rider rides as before. Change-DETECTED skills is exempt
2836
+ // (pre-existing posture — the restart pulls the NEW generation, a different repair claim).
1605
2837
  const i = reasons.indexOf("skills");
1606
2838
  if (i >= 0) {
1607
2839
  reasons.splice(i, 1);
@@ -1620,10 +2852,17 @@ async function main() {
1620
2852
  }
1621
2853
  logger.info("sema_registry_refreshed", {
1622
2854
  version: r.effective.version,
2855
+ // hot-applied: teams + runtime governance (autonomy/commandPolicy) + MODELS/ROLES/ROSTER (+pricing
2856
+ // +per-model keys). Still restart-to-apply: skills/mcp/scenarios + runtime GATES (rate/cost/approval).
1623
2857
  note: "teams + runtime governance + models/roles/roster hot-applied; skills/mcp/runtime-gates/scenarios are restart-to-apply",
2858
+ // Structured signal: orchestrator consumes the SAME object via GET /health.restart.
1624
2859
  ...(pendingRestart ? { restartRequired: true, restartReasons: pendingRestart.reasons } : {}),
1625
2860
  });
1626
- latestEffective = r.effective;
2861
+ // BOTH the LKG and the etag advance ONLY after the whole apply/pricing/keyResolver/restart
2862
+ // chain succeeded (codex lens-3): if any step above throws, latestEffective keeps pointing at
2863
+ // the last FULLY-applied candidate and the etag stays put, so the next poll re-fetches and
2864
+ // re-applies the same candidate (idempotent self-heal) — the LKG never names a half-applied one.
2865
+ latestEffective = r.effective; // LKG advance (§9.5-2): candidate validated + fully applied
1627
2866
  ccEtag = r.etag;
1628
2867
  }
1629
2868
  }
@@ -1637,9 +2876,21 @@ async function main() {
1637
2876
  };
1638
2877
  const ccTimer = setInterval(() => void refreshTick(), 60_000);
1639
2878
  ccTimer.unref?.();
2879
+ // Boot-deferred continuation(二轮复审 F5 改形):到货结果按「迟到的 boot」处理,而不是转普通 tick——
2880
+ // 普通 tick 的 restartReasons(undefined, r) 会把 prompts/skills 面全判为差异 → restart → 中心持续慢时
2881
+ // 下一进程又 deferred → 无限重启环。迟到 boot:①候选门同 F6(坏形=拒,cadence 重判);②热面
2882
+ // teamsOnly apply(boot-only gates/skills/mcp 已按 env 建,迟到不可应用——诚实);③prompts adopt
2883
+ // (进程刚起、会话面≈空,失真面⊆重启路径,且 adopt 后 prompts 不再贡献 restart 理由=prompts 无环);
2884
+ // ④一次性诚实 restart 信号=剩余 restart-to-apply 面(skills/mcp/scenarios/gates)非空才发;⑤基线
2885
+ // `effective`/LKG/etag 落位 → 后续 cadence 与到货值比较,不重复触发。⚠️ 残余环面(诚实记账):center
2886
+ // 发布了 skills/mcp 且中心**持续**慢于 boot 预算时,每个新进程仍会为真缺失的 skills 面发一次 restart
2887
+ // ——完整解=LKG 落盘(boot 直读上个进程的干净候选),已报 clay 裁量。
1640
2888
  const deferredBootApply = async (r) => {
1641
2889
  if (!r)
1642
2890
  return;
2891
+ // F6(LKG 复审三轮):与 cadence tick 同一 single-flight 车道——deferred 的 skill 预热可能拖过 60s,
2892
+ // 期间 cadence 落了更新的候选 B,A 迟到完成再覆盖盘/信号=回滚。inFlight 时直接让行(丢弃 deferred
2893
+ // 结果无损:cadence 已经/即将拉到更新的),持有 flag 至信号发布后。
1643
2894
  if (refreshInFlight)
1644
2895
  return;
1645
2896
  refreshInFlight = true;
@@ -1666,6 +2917,9 @@ async function main() {
1666
2917
  ccEtag = r.etag;
1667
2918
  return;
1668
2919
  }
2920
+ // codex R10 (deferred-boot twin of the refresh-lane guard): the Runner was built from the ENV plane before
2921
+ // this late arrival — if it froze a tier-expanded copy, the arriving center plane must not hot-apply
2922
+ // (admission/Runner split). Tier-less env boot (the common deferred-boot shape) keeps true hot-apply.
1669
2923
  const planeDeferredLate = (runnerTierFrozen || planeHasActiveTiers(r.effective)) && modelPlaneChanged(appliedPlaneEff, r.effective);
1670
2924
  applyEffective(config, r.effective, logger, { teamsOnly: true, sealedKeys, ...(planeDeferredLate ? { deferModelPlane: true } : {}) });
1671
2925
  if (planeDeferredLate)
@@ -1676,14 +2930,23 @@ async function main() {
1676
2930
  }
1677
2931
  await adoptCenterPrompts(r.effective, "boot-deferred");
1678
2932
  if (!planeDeferredLate)
1679
- markRosterLanded(r.effective);
2933
+ markRosterLanded(r.effective); // codex R13: same guard as the refresh lane — never open readiness off an unapplied plane
1680
2934
  mutateInPlace(pricing, buildPricing(config.models));
1681
2935
  keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1682
- effective = r.effective;
2936
+ effective = r.effective; // restart 比较基线=到货值(cadence 不再重复触发)
1683
2937
  latestEffective = r.effective;
1684
2938
  ccEtag = r.etag;
1685
- const lkgPersistedLate = await persistLkgDurable(r.effective, r.etag);
1686
- const reasons = restartReasons(undefined, r.effective);
2939
+ const lkgPersistedLate = await persistLkgDurable(r.effective, r.etag); // F2 parity:信号可见前落盘+skill 正文预热
2940
+ const reasons = restartReasons(undefined, r.effective); // (prompts 不在 restart slices——热采用已在上方 adopt)
2941
+ // codex R15 (late-boot twin): tiered env boot + tier-less late candidate ⇒ deferral fires but BOTH slice
2942
+ // fingerprints reduce to null (undefined baseline, tier-less candidate) — no models-tiers reason, plane
2943
+ // deferred forever. Deferral is the ground truth; force the reason so the orchestrator restarts and the
2944
+ // next process lands the candidate at boot. codex R16: forced ONLY with a confirmed LKG handoff — without
2945
+ // it the restarted process re-enters this exact lane (env plane, late candidate) = restart storm; keep
2946
+ // serving the old generation and warn instead.
2947
+ // codex R19 (late-boot twin): handoff status decides, regardless of whether change-detection already
2948
+ // produced the reason — an active-tier candidate over an env boot deterministically carries models-tiers
2949
+ // from restartReasons(undefined, candidate), and letting it through with no handoff is the R16 storm.
1687
2950
  if (planeDeferredLate) {
1688
2951
  if (lkgPersistedLate) {
1689
2952
  if (!reasons.includes("models-tiers"))
@@ -1691,12 +2954,16 @@ async function main() {
1691
2954
  planeDeferredNoHandoff = undefined;
1692
2955
  }
1693
2956
  else {
2957
+ // codex R22 (late-boot twin): withhold the WHOLE signal — co-occurring skills/mcp reasons would
2958
+ // restart into the same over-budget/no-LKG lane and loop.
1694
2959
  const blocked = reasons.splice(0, reasons.length);
1695
2960
  planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), ...(blocked.length > 0 ? { blocked } : {}), planeDeferred: true, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
1696
2961
  logger.warn("models_tiers_deferred_no_handoff", { version: r.effective.version, blockedReasons: blocked, note: "late-boot plane deferred but no NEXT-BOOT handoff (see CONFIG_LKG_DURABLE) — the WHOLE restart signal is withheld (it would loop); old generation keeps serving" });
1697
2962
  }
1698
2963
  }
1699
2964
  else if (planeDeferredLate === false && lkgSurvivesRestart && !lkgPersistedLate) {
2965
+ // codex R31 (late-boot twin): retain failed publications even with zero restart reasons.
2966
+ // codex R27 (late-boot twin): durable-declared + incomplete publication ⇒ retain + withhold + retry.
1700
2967
  const blocked = reasons.splice(0, reasons.length);
1701
2968
  planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), blocked, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
1702
2969
  logger.warn("restart_candidate_publication_incomplete", { version: r.effective.version, blockedReasons: blocked, note: "late-boot candidate publication incomplete on a durable-declared deployment — restart signal withheld; retried every tick" });
@@ -1705,19 +2972,32 @@ async function main() {
1705
2972
  pendingRestart = { restartRequired: true, reasons, version: r.effective.version, since: Date.now() };
1706
2973
  logger.info("config_loaded_deferred", { source: configProvider.kind, version: r.effective.version, models: (r.effective.models?.models ?? []).filter((m) => m.enabled !== false).length, ...(reasons.length > 0 ? { restartRequired: true, restartReasons: reasons } : {}) });
1707
2974
  };
2975
+ // LKG 起服的进程有完整 boot 基线 → 到货走普通 tick(候选门+hot+对 LKG 的 restartReasons,通常
2976
+ // 零差异=无信号无环);env 起服(无 LKG)才走「迟到 boot」路径。
1708
2977
  if (bootConfigPending)
1709
2978
  void bootConfigPending.then((r) => (lkgBooted ? refreshTick(r) : deferredBootApply(r)), () => { });
1710
2979
  }
2980
+ // Per-model API key (sema-registry `apiKeyEnv`): resolve each model's own upstream key per brain call /
2981
+ // cascade rung. undefined when no per-model keys are configured → spec field stays unset (core default).
2982
+ // `let` (not const): rebuilt on a sema-registry refresh so per-model key ADDITIONS hot-apply too (the
2983
+ // resolver is undefined when no per-model keys exist, so in-place mutation alone wouldn't cover empty→non-empty).
1711
2984
  let keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
2985
+ // PAIR-REVIEW F-6:Bearer-only 网关 × per-model key 组合死角的 boot 响亮告警——core 的 per-call auth 是
2986
+ // **整体替换**语义(1.345:per-call apiKey 在场即剥构造期 Authorization,x-api-key 成唯一凭据),而
2987
+ // key-resolver 只会产 { apiKey } 形;部署若以 ANTHROPIC_AUTH_TOKEN(Bearer)直连此类网关,则每个配了
2988
+ // per-model key 的模型必 401 且无从自诊。headers 形支持候 core/registry additive 席位(跨仓件);此前
2989
+ // 先把组合不可用讲清楚。只 warn 不拒:混合网关(两种头都收)是存在的,fail-open+可诊断。
1712
2990
  if (process.env.ANTHROPIC_AUTH_TOKEN && !process.env.ANTHROPIC_API_KEY && (Object.keys(config.modelApiKeys).length > 0 || Object.keys(config.modelApiKeyEnv).length > 0)) {
1713
2991
  logger.warn("per_model_key_bearer_gateway_combo", {
1714
2992
  perModelKeys: Object.keys(config.modelApiKeys).length + Object.keys(config.modelApiKeyEnv).length,
1715
2993
  note: "ANTHROPIC_AUTH_TOKEN (Bearer) deployment with per-model keys: core's per-call auth REPLACES the Authorization header with x-api-key for those models — a Bearer-only gateway will 401 them. Remove the per-model key for such models, or use a gateway accepting x-api-key.",
1716
2994
  });
1717
2995
  }
2996
+ // Audit回溯 + degenerate-output instrument are TiDB-specific raw-SQL consumers (outside the store
2997
+ // abstraction); they run only on the TiDB backend (the mysql2 pool), and are a no-op on PG until ported.
1718
2998
  const mysqlPool = backend?.mysqlPool();
1719
- const auditPgPool = backend?.pgPool();
1720
- const auditLocalRoot = backend?.kind === "local" ? (config.localDataRoot ?? localRoot) : undefined;
2999
+ const auditPgPool = backend?.pgPool(); // the audit face now has a PG twin — web session area lights up on DB_BACKEND=pg
3000
+ const auditLocalRoot = backend?.kind === "local" ? (config.localDataRoot ?? localRoot) : undefined; // the File leg
1721
3001
  const ownerAware = sessionStore;
1722
3002
  const sessionAudit = mysqlPool || auditPgPool || auditLocalRoot
1723
3003
  ? async (sessionId) => {
@@ -1731,6 +3011,24 @@ async function main() {
1731
3011
  return { ...audit, owner: (await ownerAware.ownerOf?.(sessionId)) ?? null };
1732
3012
  }
1733
3013
  : undefined;
3014
+ // §0.5 E21 — DELETE /v1/sessions/:id purge coordinator. Deletes the session abstraction's CONVERSATION HISTORY
3015
+ // (sessionStorage.deleteSession → session_meta/session_event) PLUS the service-owned operational rows for that
3016
+ // session: the runs ledger (runStore.deleteBySession → task_run/task_active/task_event, owner-guarded), the
3017
+ // durable checkpoints + resume-ctx (checkpointStore.deleteBySession, present only under DURABLE_APPROVAL), and
3018
+ // offloaded tool results (toolResultStore.deleteBySession). Wired only when the session store can actually
3019
+ // delete (a DB backend).
3020
+ //
3021
+ // 🔴 ORDERING (adversarial-review HIGH — privacy/right-to-delete + silent-success): the session HISTORY delete
3022
+ // (session_meta) is the `ownerOf` gate the DELETE route consults to decide idempotency. It MUST be the LAST
3023
+ // committed leg. If a privacy-relevant child leg (runs/checkpoint/tool-result) is deleted AFTER the meta row is
3024
+ // gone and then FAILS, the idempotent retry sees `ownerOf===undefined` (history already gone) → the route
3025
+ // short-circuits to 200 {deleted:false} and NEVER re-purges → those child rows are stranded while the API
3026
+ // reports success. So we purge the child legs FIRST and PROPAGATE any error (do NOT swallow): on a child
3027
+ // failure the history delete is SKIPPED, `session_meta` survives, the DELETE returns 500, and the idempotent
3028
+ // retry's `ownerOf` still resolves → it re-runs the FULL purge to convergence. `deleted` is computed from the
3029
+ // FINAL committed result (history-deleted || runsRemoved>0 — the privacy-relevant data that actually existed).
3030
+ // [1196] session-watch registry(SSE 订阅共享探针面)。谓词与 /head 探针同门(getLeafId+ownerOf);
3031
+ // env 旋钮经 posIntEnv 有界校验;getLeafId 箭头包一层=天然绑定宿主(SESSION_CACHE_TTL_SEC=0 裸类实例)。
1734
3032
  const sessionWatchRegistry = ownerAware.getHead && ownerAware.ownerOf
1735
3033
  ? new SessionWatchRegistry(async (sid) => (await ownerAware.getHead(sid)) ?? { owner: undefined, leafId: null }, {
1736
3034
  hotMs: posIntEnv(process.env.SESSION_EVENTS_HOT_MS, 200),
@@ -1738,29 +3036,73 @@ async function main() {
1738
3036
  maxWatchedSessions: posIntEnv(process.env.SESSION_EVENTS_MAX_PROBES, 512, 100_000),
1739
3037
  })
1740
3038
  : undefined;
3039
+ // S2 fast path([1208]③ 兑现):同副本 append 落点(tidb/pg persist post-commit)经 leaf-bus 直推
3040
+ // notifyLocal——单副本部署零延迟;owner 不随 bus 传(写点无廉价 owner 读),探针围栏仍是租户权威
3041
+ // (session-leaf-bus 契约注)。local lane 无写钩,探针道照旧。
1741
3042
  setLeafAdvanceListener(sessionWatchRegistry ? (sid, leaf, owner, seq) => sessionWatchRegistry.notifyLocal(sid, leaf, owner, seq) : undefined);
1742
3043
  const purgeSession = ownerAware.deleteSession
1743
3044
  ? async (sessionId, owner) => {
3045
+ // Runs-ledger FIRST: it transactionally re-asserts the no-active-run invariant (FOR UPDATE on task_active,
3046
+ // serializing against a concurrent createRun). If a run claimed the session in the route's check→purge
3047
+ // window it returns `{ active }` and deletes nothing → we bubble it up so the route 409s, BEFORE touching
3048
+ // any other table (no partial purge of a live session). Each leg propagates its error so a child failure
3049
+ // aborts the purge BEFORE the history-delete gate is removed (keeping the idempotent retry convergent).
1744
3050
  const runs = runStore ? await runStore.deleteBySession(sessionId, owner) : { removed: 0 };
1745
3051
  if ("active" in runs)
1746
3052
  return { active: runs.active };
3053
+ // checkpoint rows carry no owner column → owner-guarded via a session_meta.owner sub-select, so they MUST
3054
+ // run while session_meta still exists (before the history delete below); tool_result is keyed only by
3055
+ // `ref` so it stays route-guarded (see its deleteBySession doc).
1747
3056
  if (checkpointStore)
1748
3057
  await checkpointStore.deleteBySession(sessionId, owner);
1749
3058
  if (toolResultStore)
1750
- await toolResultStore.deleteBySession?.(sessionId);
3059
+ await toolResultStore.deleteBySession?.(sessionId); // optional extra: SQL twins only (local file store has no per-session purge index; E21 purge is a durable-backend contract)
3060
+ // E18 resume-at anchors are per-session privacy-relevant metadata → purge ALL of them with the session, scoped
3061
+ // by session_id ALONE (the route already proved session ownership at the DELETE gate). A per-ROW owner guard
3062
+ // here would LEAK: an anchor's owner is the per-run submitting principal, which diverges from the canonical
3063
+ // session owner when an anonymous session is later attached by a principal (REQUIRE_PRINCIPAL=false) — those
3064
+ // mixed-owner rows would survive the delete (a right-to-delete violation reported as success).
1751
3065
  if (resumeAnchorStore)
1752
3066
  await resumeAnchorStore.deleteBySession(sessionId);
3067
+ // Approval exemptions are per-session operator decisions → purge with the session (same
3068
+ // session_id-alone scoping rationale as the anchors above).
1753
3069
  if (approvalExemptionStore)
1754
3070
  await approvalExemptionStore.deleteBySession(sessionId);
3071
+ // E6 session-policy rows are per-session operator rules → purge them too, scoped by session_id (the route
3072
+ // owner-gated the session). Since core 1.423 the seam is on the INTERFACE (optional) and core's File/InMemory
3073
+ // stores carry it too — so this fires on EVERY backend now. (The old note here claimed local rules were
3074
+ // "process-ephemeral anyway" — false for the file-backed local store, whose policy rows survive restarts;
3075
+ // that right-to-delete hole is what [1796]§三 → core 1.423 closed.)
1755
3076
  if (sessionPolicyStore?.deleteBySession)
1756
3077
  await sessionPolicyStore.deleteBySession(sessionId);
3078
+ // D-1 附件随会话删(E21 级联;session_id 单键 scoping,与 anchors 同理由——路由已证会话所有权)。
1757
3079
  if (taskAttachmentStore)
1758
3080
  await taskAttachmentStore.deleteBySession(sessionId);
3081
+ // E19 rewind-files snapshots are per-session working-tree state → purge them too (scoped by sessionId; the
3082
+ // durable twins carry deleteBySession, local InMemory omits it → optional no-op on local).
1759
3083
  if (fileSnapshotStore?.deleteBySession)
1760
3084
  await fileSnapshotStore.deleteBySession(sessionId);
3085
+ // P1 ①②: drop any pending async-workflow completions for this session (else they orphan when a later
3086
+ // tenant reclaims the sessionId). Best-effort + non-fatal — a leftover entry is ALSO gated by the
3087
+ // drain-time owner check, so a purge failure never leaks; it must not abort the session delete.
3088
+ // `owner` scopes the purge fence (1.80): only the DELETED session's own late completions are fenced —
3089
+ // a new tenant legitimately re-claiming this sessionId within the window keeps its push (review MED).
1761
3090
  if (workflowCompletionInbox)
1762
3091
  await workflowCompletionInbox.purge(sessionId, owner ?? null).catch((err) => logger.warn("workflow_completion_inbox_purge_failed", { sessionId, err: String(err) }));
3092
+ // 修8(三路复审 absorb-2,接线 (a)):the session's scratchpad dir ([820]③, ensureScratchpadDir) rides
3093
+ // the session lifecycle — purge it with the session. purgeScratchpadDir SELF-SWALLOWS (env-facts.ts
3094
+ // contract: cleanup never faults the delete path; the periodic sweep below is the convergence backstop),
3095
+ // so it cannot strand the E21 ordering invariant. Same root the ensure used (config.localDataRoot ?? localRoot).
1763
3096
  await purgeScratchpadDir(config.localDataRoot ?? localRoot, sessionId);
3097
+ // design/129 (core 1.239): reap this session's SESSION-scoped background children — they
3098
+ // deliberately outlive turns (backgroundScope:"session"), so the session's DELETE is their lifecycle
3099
+ // end (core's parent-teardown reap skips them by design; without this they run to their forced timeout).
3100
+ // Sync + replica-local (the registry is in-process); best-effort — a reap of 0 on the wrong replica is
3101
+ // covered by core's forced child-timeout cap.
3102
+ // [1892]{core} 跨仓待办:scope 轴必传——core canAccess 对缺省 scope fail-closed(拒绝非通配,
3103
+ // 1.441 复审①正过极性),不传 = 四类后台任务在会话删除时恒 0 回收(潜伏至今,core 复审顺手
3104
+ // 核出)。铸值同源 runs.ts:25 契约:core 按 `spec.principal ?? "default"` 注册 ⇒ 此处
3105
+ // owner(会话主)?? "default" 对齐;错 scope 不越租户由 canAccess 保证(session-reap-scope 钉)。
1764
3106
  try {
1765
3107
  const reaped = defaultTaskRegistry.reapSessionBackground(sessionId, owner ?? "default");
1766
3108
  if (reaped > 0)
@@ -1769,13 +3111,26 @@ async function main() {
1769
3111
  catch (err) {
1770
3112
  logger.warn("session_background_reap_failed", { sessionId, err: String(err) });
1771
3113
  }
3114
+ // History/meta LAST: once committed, the `ownerOf` gate is gone and the delete is durably complete. The
3115
+ // owner is threaded into the SQL guard (session_meta.owner) for data-layer defense-in-depth.
1772
3116
  const historyDeleted = await ownerAware.deleteSession(sessionId, owner);
3117
+ // [1196] 三轮复审(租户轮转围栏,同副本半场):session 删除即终结其 SSE 订阅者+watch 条目——
3118
+ // 旧 owner 的活流不得跨越 delete/reclaim 边界收新租户的 head;跨副本半场=路由心跳期 owner 复核。
1773
3119
  sessionWatchRegistry?.dropSession(sessionId);
1774
3120
  return { deleted: historyDeleted || runs.removed > 0 };
1775
3121
  }
1776
3122
  : undefined;
3123
+ // Degenerate-output a/b instrument: needs the durable session log to inspect turn history (design/39
3124
+ // ② gating data). Only on the TiDB backend (mysql2 pool); env-only / PG deploys get no-op.
1777
3125
  const instrumentDegenerate = mysqlPool ? makeDegenerateInstrument(mysqlPool, metrics, logger) : undefined;
3126
+ // Plan-cache recurrence probe (core design/42, INSTRUMENT-FIRST): pure in-memory, no backend needed —
3127
+ // counts per-scope task-objective recurrence so core can decide whether to build plan caching.
1778
3128
  const planCacheProbe = new PlanCacheProbe(metrics, logger);
3129
+ // v2 leader endpoint (design/50 + design/68): wire when LEADER_ENABLED + an isolated remote-exec backend
3130
+ // (E2B or k8s/Kata — SSH/ADB are single-worker real-system backends, not fan-out targets). Default off →
3131
+ // zero prod impact. 🔴 a real run also needs `git` on the host PATH + a durable remote with write creds.
3132
+ // F2(a) factory mode (preferred) = the service's executionEnvFactory + MinIO self-upload diff-out; the k8s
3133
+ // lane REQUIRES it (no static k8s mode). E2B without MinIO falls back to the static compat mode.
1779
3134
  const leaderMinio = process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
1780
3135
  ? {
1781
3136
  s3: {
@@ -1788,6 +3143,9 @@ async function main() {
1788
3143
  }
1789
3144
  : {};
1790
3145
  const leaderProvider = config.remoteExec?.provider;
3146
+ // DUAL-MODE §5: orchestration is an ENGINE capability, not fleet-only — the TOC `host` lane runs leader fan-out
3147
+ // bounded by ONE box (isolation=none, NON-durable: no snapshot, so the durable sub-worker suspend block below is
3148
+ // skipped — host workers run to completion in-process-adjacent). e2b/k8s keep their isolated/suspendable posture.
1791
3149
  const leaderEndpoint = config.leaderEnabled &&
1792
3150
  (leaderProvider === "e2b" ||
1793
3151
  (leaderProvider === "k8s" && "s3" in leaderMinio && executionEnvFactory) ||
@@ -1795,21 +3153,37 @@ async function main() {
1795
3153
  ? createLeaderEndpoint(createLeaderRunner({
1796
3154
  brain, models: config.models, roles: config.roles, pricing, logger,
1797
3155
  fanoutEnabled: config.leaderFanoutEnabled,
3156
+ // Route the single-vs-fanout classification on the cheap model (deepseek-v4-flash): the heavy
3157
+ // reasoning worker model returns empty ~2/3 of the time on the route prompt → silent collapse to
3158
+ // single (observed 2026-06-14). MODEL_ROUTER_ID overrides; else the cheap model; else default.
1798
3159
  ...((process.env.MODEL_ROUTER_ID || process.env.MODEL_CHEAP_ID)
1799
3160
  ? { routerModel: (process.env.MODEL_ROUTER_ID || process.env.MODEL_CHEAP_ID) }
1800
3161
  : {}),
1801
3162
  ...(leaderProvider === "e2b" ? { e2bApiKey: config.remoteExec.apiKey } : {}),
1802
3163
  ...(executionEnvFactory ? { envFactory: executionEnvFactory } : {}),
1803
3164
  ...leaderMinio,
3165
+ // Durable offload store for sub-worker tool results — without it core REFUSES the durable
3166
+ // suspend (InMemory offload would not survive a cross-replica resume) and the gated call is
3167
+ // denied via the unwired onAsk fallback → the worker fails instead of suspending (drill c3).
1804
3168
  ...(toolResultStore ? { toolResultStore } : {}),
3169
+ // k8s lane: align the wire's workspace with the adapter's durable-snapshot root (/workspace) —
3170
+ // the E2B default (/home/user) is OUTSIDE the k8s suspend tar, so a suspended sub-worker's repo
3171
+ // would silently vanish on resume (drill c3: 105-byte empty snapshots).
1805
3172
  ...(leaderProvider === "k8s" ? { workspace: "/workspace" } : {}),
3173
+ // Same store as the main runner: a suspended sub-worker's session must be readable on /decide
3174
+ // resume (sub-runner-local in-memory sessions die with "Entry <leafId> not found" on resume).
1806
3175
  sessionStore,
3176
+ // design/68 C4: sub-workers can durably suspend when the service runs durable approvals.
1807
3177
  ...(checkpointStore && config.approvalRequire.length > 0
1808
3178
  ? {
1809
3179
  durable: {
1810
3180
  checkpointStore,
1811
3181
  requireApproval: config.approvalRequire,
1812
3182
  deny: config.approvalDeny,
3183
+ // design/80: the leader's orchestrated sub-agents adopt the SAME supervisor loop as the
3184
+ // top-level agent — the D-E auto-budget (auto-approve NORMAL asks up to the budget, then
3185
+ // escalate) + the never-auto safety set. Without these a sub-agent's gated ask always
3186
+ // suspends-to-human, defeating the budget circuit-breaker for the supervised team.
1813
3187
  autoBudget: config.approvalAutoBudget,
1814
3188
  neverAuto: config.approvalNeverAuto,
1815
3189
  ...(config.approvalTimeoutSec > 0 ? { ttlMs: config.approvalTimeoutSec * 1000 } : {}),
@@ -1818,12 +3192,20 @@ async function main() {
1818
3192
  : {}),
1819
3193
  }), {
1820
3194
  logger,
3195
+ // SDK 全量核查(2026-07-24,黑板 GD 组)撞获:wire.ts:368 的 runLeader 实际要求这五个字段全部
3196
+ // 非空,此前同步门只查 objective,不完整请求会先拿到 202 再在后台必然失败——同步前移,让错误
3197
+ // 尽早暴露而不是靠后续 GET 才发现。
1821
3198
  requiredFields: ["objective", "durableRemote", "testCmd", "seedCmd", "baseSha"],
1822
3199
  })
1823
3200
  : undefined;
1824
3201
  if (leaderEndpoint)
1825
3202
  logger.info("leader_endpoint_enabled", { provider: leaderProvider, mode: "s3" in leaderMinio ? "factory" : "static" });
3203
+ // Graceful drain: shared mutable state — SIGTERM flips `draining` (shutdown below), createHttpServer
3204
+ // assigns `inflight` (live leg count on this instance), /health + the submit 503 gate read it.
1826
3205
  const drainState = { draining: false };
3206
+ // 鉴权桥(registry-core api/auth-bridge):SEMA_REGISTRY_URL 在位即点亮(零新配置——worker 已信
3207
+ // registry 的 effective config,同一信任根延伸到其 JWKS);split 部署用 AUTH_BRIDGE_ISSUER/JWKS_URL/AUDIENCE
3208
+ // 显式覆盖。缺 registry(纯 env 部署)=分支不存在,bearer 行为与今天字节一致。
1827
3209
  const authBridgeIssuer = process.env.AUTH_BRIDGE_ISSUER || config.configCenter?.baseUrl;
1828
3210
  const registryJwtVerifier = authBridgeIssuer
1829
3211
  ? createRegistryJwtVerifier({
@@ -1836,108 +3218,212 @@ async function main() {
1836
3218
  : undefined;
1837
3219
  if (registryJwtVerifier)
1838
3220
  logger.info("auth_bridge_enabled", { issuer: authBridgeIssuer });
3221
+ // asyncRewake wake 管道 holder:server 启动时装 deliver(session→本副本活流 steer);createTaskHooks 的
3222
+ // ctx.wake 闭包经它投递(main 建壳/server 填实,双向不循环依赖)。
1839
3223
  const hookWakeBus = {};
3224
+ // ═══════════ design/158 A8:ServiceDeps 分组装配(七组窄接口)═══════════
3225
+ // 每组一个**带类型标注**的中间 const —— 标注位触发 TS 多余属性检查,键放错组当场编译红。
3226
+ // (平铺形做不到:69 键的全可选面里任何键名都「属于」它,于是键落错家/从未接线只能靠源码钉
3227
+ // 逐个盯 —— 1.300/1.301/1.304/1.307/1.310 五起真缺陷都是这个病族。)组形状门见
3228
+ // test/deps-literal-shape-gate.test.ts;消费点仍读平铺 `deps.x`(createHttpServer 入口摊平)。
3229
+ /** durable 持久面(键 present ⇔ 路由/能力位在场;absent ⇒ 501 诚实缺席) */
1840
3230
  const stores = {
1841
3231
  runStore,
1842
- resumeAnchorStore: resumeAnchorStore ? resumeAnchorStore : undefined,
3232
+ resumeAnchorStore: resumeAnchorStore ? resumeAnchorStore : undefined, // E18 resume-at eventId→entryId map (tidb/pg/local)
1843
3233
  approvalStore,
1844
- approvalExemptionStore: approvalExemptionStore ? approvalExemptionStore : undefined,
3234
+ approvalExemptionStore: approvalExemptionStore ? approvalExemptionStore : undefined, // decide remember="session" + list/revoke
1845
3235
  checkpointStore,
1846
- sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined,
3236
+ sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined, // E6 operator session-rule store (PUT/GET /v1/sessions/:id/policy)
3237
+ // E19 cap: core's gate-split (1.134.0) REMOVED the isRemoteExecutionEnv skip — core now snapshots each
3238
+ // completed turn + restores on resumeAt for ANY ExecutionEnv when fileSnapshotStore is wired (captureManifest/
3239
+ // applyManifest run over any env's FileSystem ops; a 30s timeout bounds a slow remote walk). So rewind works for
3240
+ // host/e2b/k8s/ssh/adb/local-docker AND the in-process worker → advertise `rewindFiles` whenever the store is wired.
1847
3241
  fileSnapshotStore: fileSnapshotStore ? fileSnapshotStore : undefined,
1848
- taskAttachmentStore: taskAttachmentStore ? taskAttachmentStore : undefined,
3242
+ taskAttachmentStore: taskAttachmentStore ? taskAttachmentStore : undefined, // D-1 上传/取回/删除三动词面
3243
+ // design/153 件3d(/decide parked 赎回腿):durable bg 行店 + boot 裸 Agent 工具,与 RunnerDeps/
3244
+ // scenarioDeps 同实例(claim/expire/consumeParkedFlip 作用于同一行)。任一缺席=分支不存在。
1849
3245
  backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
3246
+ // §0.5 session-ownership: the durable session abstraction (SessionRepo-style list/fork/delete) + the delete
3247
+ // coordinator. `ownerAware` is the (possibly cache-wrapped) session store; both delegate to the DB-backed
3248
+ // TiDB/PG store. Undefined-safe: on an in-memory dev store these carry no SessionRepo ops → the routes 501.
1850
3249
  sessionStorage: ownerAware,
1851
3250
  workflowRunStore: workflowRunStore ? workflowRunStore : undefined,
1852
- workflowJournalStore: workflowJournalStore ? workflowJournalStore : undefined,
3251
+ workflowJournalStore: workflowJournalStore ? workflowJournalStore : undefined, // [1402] journal 读面(GET /v1/workflows/:id/journal 数据源,与 runner 同实例)
1853
3252
  imageIndex,
1854
3253
  imageBakes,
1855
- outcomeSink: outcomeSink ? outcomeSink : undefined,
1856
- sendFileLedger: sendFileLedger ? sendFileLedger : undefined,
3254
+ outcomeSink: outcomeSink ? outcomeSink : undefined, // P2: GET /v1/outcomes 读面(design/73 §7.2 仪表盘底料)
3255
+ sendFileLedger: sendFileLedger ? sendFileLedger : undefined, // SendUserFile 账本读面:GET /v1/sendfile-links(owner-scope 镜像 sessions list)
3256
+ // 2c session-sync (P1d): the WHOLE backend so the /v1/sessions/:id/sync/* routes can call exportSession/
3257
+ // importSession (they fan out across session entries + fileSnapshot + sessionPolicy + resumeAnchor, which must
3258
+ // come from ONE backend in the §8 atomic order). Undefined-safe: the env-only/no-backend worker 501s the routes.
1857
3259
  backend: backend ? backend : undefined,
1858
3260
  };
3261
+ /** 进程内活体协调面(本副本 only,重启即空:HITL 协调器/可 steer 句柄注册表/总线/收件箱) */
1859
3262
  const coordinators = {
1860
- elicitation: elicitation ? elicitation : undefined,
1861
- question: question ? question : undefined,
1862
- toolApproval: toolApproval ? toolApproval : undefined,
1863
- workflowAgentRegistry: workflowAgentRegistry ? workflowAgentRegistry : undefined,
1864
- subagentSteerRegistry,
1865
- workflowCompletionInbox: workflowCompletionInbox ? workflowCompletionInbox : undefined,
3263
+ elicitation: elicitation ? elicitation : undefined, // E23: live-only inbound-elicitation coordinator (respond route + ALS wraps)
3264
+ question: question ? question : undefined, // §4④: AskUserQuestion live coordinator (respond route + ALS wraps on streaming legs)
3265
+ toolApproval: toolApproval ? toolApproval : undefined, // [816]/[820]②: live tool-approval coordinator (respond route + ALS wrap on the sync streaming leg)
3266
+ workflowAgentRegistry: workflowAgentRegistry ? workflowAgentRegistry : undefined, // SVC-5: steerable workflow-agent handle registry
3267
+ subagentSteerRegistry, // C2 (core 1.219): steerable Task-subagent handle registry (POST /v1/runs/:id/subagents/:target/steer)
3268
+ workflowCompletionInbox: workflowCompletionInbox ? workflowCompletionInbox : undefined, // P1 ①②: drain → workflow_complete frame on stream-open
1866
3269
  fleetBus,
1867
- sessionWatch: sessionWatchRegistry ? sessionWatchRegistry : undefined,
3270
+ // [1196] session SSE 订阅:watch registry(N 订阅共享一条探针,自适应拍距,无订阅即停)。谓词与
3271
+ // /head 探针同门(getLeafId+ownerOf);缺 store 面不建=路由 501+capabilities.sessionEvents=false。
3272
+ sessionWatch: sessionWatchRegistry ? sessionWatchRegistry : undefined, // [1196] 建于 purgeSession 之前(delete 即 dropSession 的同副本围栏)
1868
3273
  hookWakeBus,
1869
- sendUserFile: sendUserFileEmitter ? sendUserFileEmitter : undefined,
3274
+ sendUserFile: sendUserFileEmitter ? sendUserFileEmitter : undefined, // SendUserFile 切片2:file_link 帧 ALS(bg/sync 腿 wrap)
1870
3275
  leaderEndpoint,
1871
- sessionTitler: sessionTitler ? sessionTitler : undefined,
3276
+ sessionTitler: sessionTitler ? sessionTitler : undefined, // fire-and-forget auto-title at first submit
1872
3277
  };
3278
+ /** 函数/构件缝(main 把跨子系统的动作编译成一个可调用面交给路由) */
1873
3279
  const seams = {
1874
3280
  sessionAudit,
1875
3281
  purgeSession: purgeSession ? purgeSession : undefined,
3282
+ // [1488]③(b): bg agent-handle read face — GET /v1/runs/:id/subagents/:handle/output → the same registry
3283
+ // face the TaskOutput tool reads (defaultTaskRegistry is the process singleton every run leg registers
3284
+ // its background children into). Access tuple is derived from the run row in server.ts, never the caller.
3285
+ // codex R2: kind-gated BEFORE the poll (backgroundAgentOutput) — generic pollTask has side effects on
3286
+ // other kinds (workflow onServedTerminal ack, bash output-cursor advance).
1876
3287
  subagentTaskOutput: (handle, access) => backgroundAgentOutput(defaultTaskRegistry, handle, access, backgroundAgentStore),
3288
+ // [1499] CC TaskOutput/TaskStop 人侧对位 — the GENERIC task-handle verbs over the same registry singleton
3289
+ // (kind gate at the seam: bash/monitor/agent pass, workflow refused — journal face owns it).
1877
3290
  taskHandleOutput: (handle, access) => taskHandleOutput(defaultTaskRegistry, handle, access, backgroundAgentStore),
1878
3291
  taskHandleStop: (handle, access) => taskHandleStop(defaultTaskRegistry, handle, access, backgroundAgentStore),
3292
+ // 142-S5 §1.4: GET /v1/memory/export 数据源(DB memory plane only;file 形态 route 501)。
1879
3293
  memoryExport: memoryExportBackend ? (scope) => exportMemoryScope(memoryExportBackend, scope) : undefined,
3294
+ // 142-S2.5: POST /v1/memory/sync/:scope 执行位(同一 DB entry plane + sync_cursors 游标面;
3295
+ // plan 随数据走——core 纯函数在 performMemorySync 内直调,file 形态 route 501)。
1880
3296
  memorySync: memoryExportBackend && memorySyncCursors
1881
3297
  ? (scope, syncReq) => performMemorySync(memoryExportBackend, memorySyncCursors, scope, syncReq)
1882
3298
  : undefined,
3299
+ // sessionMirror 观测面(server 非执法端,论证在 ServiceDeps.sessionMirrorRuling):
3300
+ // 与 executionRuling 同车同缓存(零额外 center RTT);无 center/dry-run ⇒ 不接线,观测面暗、零行为差。
1883
3301
  sessionMirrorRuling: principalCaps
1884
3302
  ? async (p) => (await principalCaps.executionRuling(p))?.sessionMirror
1885
3303
  : undefined,
1886
3304
  instrumentDegenerate,
1887
- sideQueryAccounting,
3305
+ sideQueryAccounting, // [1469] side-query 四路 sink 记账(与 tracer 同源装配)
1888
3306
  parkedReviveTool: parkedReviveTool ? parkedReviveTool : undefined,
3307
+ // design/158 S2(F-1)修:工厂在 :2308 造好、server.ts 消费分支在等,但从未进本实参段——HIGH-1
3308
+ // 同族第二例(条件 spread 的键从不接线,编译期无声)。缺席后果=带 requiresParentConstraint 的
3309
+ // parked checkpoint 重启后恒被 core pre-CAS 门拒,**不可赎回**。接线钉:test/parked-revive-gate-wiring.test.ts。
1889
3310
  parkedReviveInheritedGate: parkedReviveInheritedGate ? parkedReviveInheritedGate : undefined,
1890
3311
  };
3312
+ /** 可观测面(全体 fail-open,观测失败绝不改业务面行为) */
1891
3313
  const observability = {
1892
3314
  logger,
1893
3315
  metrics,
1894
- modelUsage: modelUsageTracker,
1895
- promptManifests: promptManifestTracker,
3316
+ modelUsage: modelUsageTracker, // E8: shared with createTracer above → runs drain it into the modelUsage echo
3317
+ promptManifests: promptManifestTracker, // [998]②: shared with createTracer → the durable legs drain prompt_assembled events
1896
3318
  planCacheProbe,
1897
3319
  };
3320
+ /** 准入/治理面(每件都是拒绝的权力;缺席 = 该道门不存在,不是「默认放行的门」) */
1898
3321
  const governance = {
1899
3322
  authorize,
1900
3323
  registryJwtVerifier: registryJwtVerifier ? registryJwtVerifier : undefined,
1901
3324
  rateLimiter,
1902
3325
  costQuota,
1903
- fleetLease: fleetLease ? fleetLease : undefined,
3326
+ fleetLease: fleetLease ? fleetLease : undefined, // lease admission 门(提交面,镜像 quotaExceeded)
1904
3327
  };
3328
+ /** 部署自述面(/health + /v1/capabilities + 提交前置门读的部署事实;多为 live getter) */
1905
3329
  const deployment = {
3330
+ // 海外 pilot 实机发现:/health 曾 verbatim 回显 sessionBackend 枚举——auto/DB_BACKEND 收编把它
3331
+ // coerce 成 LABEL "tidb"(泛指 durable),pg 部署上误读成 SQL-TiDB 带偏排障。回显有效 store(与 listening
3332
+ // 日志 session 标签同款诚实修)。
1906
3333
  sessionStoreLabel: config.sessionBackend === "tidb" && backend ? `durable(${backend.kind})` : config.sessionBackend,
1907
- modelReady: () => modelReadyState.ready,
1908
- scenarioDetails,
1909
- workflowsCapable,
3334
+ modelReady: () => modelReadyState.ready, // boot ready 门:roster 未落地=计费提交 503+health ready:false
3335
+ scenarioDetails, // 场景详情只读面(内建+center 同判定源)
3336
+ workflowsCapable, // ① core split: capabilities.workflows = engine-can (workflowsCapability); workflowsList = store (below)
1910
3337
  instanceId,
3338
+ // Capability discovery: the deployment facts only main knows. Store-derived booleans
3339
+ // (asyncRuns/artifacts/approvals/leader) are computed inside the server from the deps themselves.
3340
+ // pricingConfigured disambiguates costMicroUsd=0 ("free run" vs "MODEL_COST_* not configured") —
3341
+ // it is worker-level config, so one flag here beats decorating every result's stats.
1911
3342
  capabilities: {
1912
3343
  version: serviceVersion(),
1913
3344
  scenarios: Object.keys(scenarios),
1914
3345
  pricingConfigured: Object.values(pricing).some((p) => Object.values(p).some((v) => typeof v === "number" && v > 0)),
1915
3346
  },
3347
+ // center: orchestrator polls GET /health for this — set by the sema-registry refresh loop when a
3348
+ // pull's restart-to-apply slices differ from boot. A live getter (not a snapshot) so /health always reads current.
1916
3349
  restartState: () => pendingRestart,
1917
- planeDeferredState: () => planeDeferredNoHandoff,
3350
+ planeDeferredState: () => planeDeferredNoHandoff, // codex R18: /health.modelPlaneDeferred (stuck-deferred plane, no handoff)
3351
+ // shared drain state — SIGTERM flips `draining`, createServer assigns `inflight`, /health mirrors it.
1918
3352
  drainState,
1919
- storeDegraded: storeBackendDegraded,
3353
+ storeDegraded: storeBackendDegraded, // S5: /health twin of the store_backend_degraded gauge
1920
3354
  };
3355
+ /** 数值旋钮(缺省写在消费点,此处只承载覆写) */
1921
3356
  const knobs = {
3357
+ // (a) SQL 默认店的 per-blob 帽 → PUT /sync/blobs 前置 413(与 SqlBlobBackend 方言默认同源:
3358
+ // 显式 env > tidb 的 6MiB entry-限默认;pg 默认无帽=不前置)。
3359
+ // 🔴 复审 2026-07-28(E-HIGH-1)修:此键是 **ServiceDeps** 字段,原先被 spread 进 `runnerDeps`
3360
+ // (core 的 RunnerDeps 无此字段,条件 spread 绕过 TS 多余属性检查 ⇒ 编译期无声)⇒ HTTP 面恒
3361
+ // undefined、413 前置是死枝、超帽仍走 502。落错家的死键病族(verify-cost 先例)——接线钉见
3362
+ // test/review-2026-07-28-batch1.test.ts。
1922
3363
  snapshotBlobSqlCapBytes: backend && backend.kind !== "local" && !config.snapshotBlobStore
1923
3364
  ? (config.snapshotBlobSqlMaxBytes ?? (backend.kind === "mysql" ? SQL_BLOB_DEFAULT_MAX_BYTES : undefined))
1924
3365
  : undefined,
3366
+ // 帽只在 watch 面在场时才有意义(缺席时路由 501,读不到它)——保持与原条件段同门,不单独接线。
1925
3367
  sessionEventsMaxConnections: sessionWatchRegistry ? posIntEnv(process.env.SESSION_EVENTS_MAX_CONNS, 256, 100_000) : undefined,
1926
3368
  };
3369
+ // Deployment-owned mapping. Tools/prompt/skills come from the selected scenario (assembled once,
3370
+ // bound per request); identity/session/policy stay server-side. Never taken from the request body.
3371
+ // design/158 A8:从 createHttpServer 实参里提出来(原地占该字面量 1,093 行中的 967 行)——
3372
+ // 类型标注 `ServiceDeps["resolveSpec"]` 顶替了原来的上下文推导,签名不变。
1927
3373
  const resolveSpec = async (body, _req, auth, opts) => {
3374
+ // per-principal scenario governance (assignment-as-default; an explicit
3375
+ // body.scenario is bounded to the allowlist — outside ⇒ typed 400 `scenario_not_allowed`). The ruling
3376
+ // {scenario, allowlist} is center-RESOLVED and rides the caps view (TTL-cached, same fetch as runtimeCaps);
3377
+ // no center / dry-run / anonymous / older center / caps blip ⇒ undefined ⇒ legacy body-or-default chain.
1928
3378
  const requested = gateScenarioRequest(await principalCaps?.scenarioRuling(auth?.principal), body.scenario, config.defaultScenario);
3379
+ // per-principal execution-lane policy — this worker's lane is a
3380
+ // PROCESS identity (REMOTE_EXEC boot wiring, no per-task switch), so admission is the gate: lane ∉
3381
+ // allowedLanes ⇒ typed 403 `execution_lane_not_allowed`. Same caps car as the scenario ruling (zero extra
3382
+ // RTT); fail-open + audit on blip. "in-process" = fleet's REMOTE_EXEC-unset core stub lane
3383
+ // (named distinctly from the explicit `host` lane — same machine, different posture).
1929
3384
  gateExecutionLane(await principalCaps?.executionRuling(auth?.principal), config.remoteExec?.provider ?? "in-process");
3385
+ // Resolve an unknown name to "default" so the WHOLE bundle stays coherent — tools/prompt AND
3386
+ // skills/mcp fall back together. (selectScenario already falls back for tools/prompt; without
3387
+ // resolving here, skills/mcp would still be filtered by the raw unknown name → any skill/MCP tagged
3388
+ // ["default"] silently drops. council finding.)
1930
3389
  const scenarioName = scenarios[requested] ? requested : "default";
3390
+ // [891] deprecation(一版过渡窗):"autonomous" 已改名 "code"(persona 同一,唯一差别=alias 仍
3391
+ // 定死 finalVerification:true 保存量行为)。warn 不 400——TB harness/存量部署平滑迁移。
1931
3392
  if (scenarioName === "autonomous")
1932
3393
  logger.warn("scenario_autonomous_deprecated", { alias: "code", note: "renamed by [891]; alias keeps finalVerification pinned — pass scenario:'code' (+ explicit finalVerification if wanted)" });
1933
3394
  const cap = selectScenario(scenarios, scenarioName)(body, auth?.principal);
3395
+ // codex R3/R4 (appendSystemPrompt 案): ONE center-pack snapshot per request — the append-less gate below
3396
+ // AND the provider pick (promptProvider field) both read THIS capture, never the mutable centerPrompts ref
3397
+ // again (R4 TOCTOU: a refresh between the two reads could make the final provider assembled while the gate
3398
+ // judged safe, restoring the in-core silent drop).
1934
3399
  const centerDecls = centerPrompts?.declarations;
3400
+ // TRUE when the EFFECTIVE assembly for this request takes core's pass-through arm (no core/role.append
3401
+ // section ⇒ any append rider/outputStyle silently discarded in core). codex R9: PROVIDER-AWARE — mirrors
3402
+ // core's actual triggers (stableBlocks identity decls / stableSystem OUTPUT carrying the anchors), because
3403
+ // a raw body.systemPrompt anchor heuristic false-positives on default-provider scenarios (unbranded
3404
+ // default / scan: no provider ⇒ core defaultPromptProvider retains role.append and the rider mounts even
3405
+ // with an anchored systemPrompt). center-wins mirrors the provider pick below (same snapshot, same
3406
+ // scenario derivation — principal ruling + defaultScenario already folded into scenarioName above).
1935
3407
  const appendLessPack = centerDecls
1936
3408
  ? centerIdentityAssembled(centerDecls, scenarioName, hasConstitutionAnchors)
1937
3409
  : providerDropsAppend(cap.promptProvider, typeof body.systemPrompt === "string" ? body.systemPrompt : undefined);
3410
+ // Accepted rider (defensive resume mirror inside; fresh malformed/over-cap shapes already 400'd in
3411
+ // prepareSpec). Hoisted to a const so the R14 combined-cap check below can see the accepted length.
1938
3412
  const acceptedAppend = acceptAppendSystemPrompt(body.appendSystemPrompt, (detail) => logger.warn("append_system_prompt_dropped", { detail, sessionId: auth?.sessionId ?? null }), appendLessPack);
3413
+ // @-model (1.24) + explicit picker: an end user may pick a CONFIGURED model via the CLI
3414
+ // `/model` picker / `run --model` flag (body.model) OR an inline `@name` in the objective. resolveTaskModel
3415
+ // gates BOTH against the catalog allow-list (config.models keys) — pick WHICH model, never inject one — with
3416
+ // body.model winning over @mention winning over `default`. Returns the objective with the @mention stripped.
1939
3417
  const objective = typeof body.objective === "string" ? body.objective : "";
3418
+ // TaskRequest.settings (client per-request SemaSettings stamp). Parsed DEFENSIVELY off the untrusted body
3419
+ // (every field shape-checked; `bypassPermissions`/unknown modes dropped). `env`/`hooks` are recorded as
3420
+ // received-but-deferred (never silent-dropped). Folded into the spec TIGHTEN-ONLY after applyRuntimeGovernance.
1940
3421
  const parsedSettings = parseTaskSettings(body.settings);
3422
+ // codex R4 (appendSystemPrompt 案) fresh-leg fail-loud: a NEW submit whose append payload (top-level rider
3423
+ // or settings.outputStyle) meets an append-less pack 400s HERE — inside resolveSpec, where the effective
3424
+ // scenario (principal ruling + defaultScenario) and the center-pack snapshot are the SAME ones the provider
3425
+ // pick uses (an HTTP-side gate could disagree with the actual assembly). Resume legs (opts.leg absent)
3426
+ // keep the drop+warn mirror below — a 4xx would brick a pre-1.243 stored task.
1941
3427
  if (opts?.leg === "fresh" && appendLessPack) {
1942
3428
  const riderPresent = typeof body.appendSystemPrompt === "string" && body.appendSystemPrompt.length > 0;
1943
3429
  if (riderPresent || parsedSettings.settings?.outputStyle) {
@@ -1945,8 +3431,26 @@ async function main() {
1945
3431
  }
1946
3432
  }
1947
3433
  if (parsedSettings.deferred.length > 0) {
3434
+ // No-silent-drop: a caller SENT env/hooks but this version does not wire them on remote (need a core seam /
3435
+ // a remote hook-runner — see task-settings.ts + capabilities). Log so the gap is visible, never silent.
1948
3436
  logger.warn("task_settings_deferred", { fields: parsedSettings.deferred, sessionId: auth?.sessionId ?? null });
1949
3437
  }
3438
+ // [#40 / TOC cwd seam] register the caller's launch dir so the HOST factory runs the agent there (read by
3439
+ // ctx.sessionId). 🔒 GATED: only the single-user host lane (cwdHonored) honors it; on any other lane / multi-tenant
3440
+ // a sent cwd is IGNORED (logged once, never silently — the shell learns via capabilities.projectContext). isValidCwd
3441
+ // re-checked here too (RESUME re-runs resolveSpec WITHOUT the prepareSpec 400 validation). Re-applies on resume.
3442
+ // [851]P3a 信号真实化:cwdHonored 只认显式 REMOTE_EXEC=host;provider 未设的 in-process 单用户 lane 没有
3443
+ // shell hands(core prepare-task:无 factory/executionEnv ⇒ StubExecutionEnv,亲读 dist 坐实),但进程级
3444
+ // 消费者(hooks 命令、C4 projectContext 本地读取)仍看引擎自身的 cwd/env——壳把引擎 spawn 在用户目录/用户
3445
+ // 环境时,请求的 override 往往「已被进程状态满足」,此前每任务打 "ignored" warn 与实际状态矛盾(飞轮双平台
3446
+ // 观测)。改法:已满足 ⇒ debug;真失配 ⇒ 保留 warn 且带 lane 字段(值与 run_local_start exec 口径一致:
3447
+ // provider ?? "in-process")。真 per-session honor 之路仍是 REMOTE_EXEC=host(host factory 消费
3448
+ // perSessionCwd/perSessionShellEnv)。
3449
+ // ── D-1 附件绑定半场(②):TaskRequest.attachments = 上传句柄数组 ──
3450
+ // fresh 腿硬验证(未知/他人 id → 400 typed;超数 → 400)——「引用了不存在的附件」必须在提交时
3451
+ // 拒,不能等 env 建立时才炸。resume 腿降级(acceptedAppend 同姿):缺行 warn+跳过,余下照常
3452
+ // (4xx 会砖存量 task;记录不可变 ⇒ 在场行的 notice 字节恒稳)。绑定=把行钉到 auth.sessionId
3453
+ // (authorizer 恒解析出一个;绑定幂等,重复提交/resume 重绑同值)。
1950
3454
  let attachmentNotice;
1951
3455
  {
1952
3456
  const reqAtt = body.attachmentIds;
@@ -1988,10 +3492,14 @@ async function main() {
1988
3492
  else
1989
3493
  logger.warn("task_cwd_ignored", { honored: cwdHonored(config), lane: execLane, sessionId: auth.sessionId });
1990
3494
  }
3495
+ // [R-survey / TOC shellEnv seam] register the caller's `settings.env` so the HOST factory injects it into the
3496
+ // agent's shell env. 🔒 GATED identically to cwd (single-user host lane only — design/107 env=capability axis);
3497
+ // on any cloud/multi-tenant lane a sent env is IGNORED (logged once, never silent). Re-applies on resume.
1991
3498
  if (parsedSettings.settings?.shellEnv && auth?.sessionId) {
1992
3499
  if (cwdHonored(config))
1993
3500
  setSessionShellEnv(auth.sessionId, parsedSettings.settings.shellEnv);
1994
3501
  else if (inProcessSingleUserLane(config)) {
3502
+ // 只记失配键「数量」,绝不记键值(秘密类)与键名(名字本身可带敏感语义)——shellEnvMismatchCount 文档同款红线。
1995
3503
  const requested = parsedSettings.settings.shellEnv;
1996
3504
  const mismatched = shellEnvMismatchCount(requested, process.env);
1997
3505
  if (mismatched === 0)
@@ -2002,6 +3510,11 @@ async function main() {
2002
3510
  else
2003
3511
  logger.warn("task_shell_env_ignored", { honored: false, lane: execLane, sessionId: auth.sessionId });
2004
3512
  }
3513
+ // hooks hook-runner(阶段一):settings.hooks(契约校验过)→ core TaskSpec.hooks 进程内回调。
3514
+ // 🔒 hook 命令跑在 WORKER HOST(CC 语义:hooks 跑在引擎所在机,哪怕工具在远端沙箱)= design/107 class ②
3515
+ // 能力授予 → 单用户闸 `requirePrincipal !== true`,与 scheduler/backgroundShell/lspHost/MCP 注入同一姿势;
3516
+ // 多租户 lane 收到只警告忽略(`task_hooks_ignored`,capabilities.taskSettings.hooks 同步 advertise),绝不执行。
3517
+ // Re-applies on resume(从 stored body 重解析,幂等)。
2005
3518
  const taskHooks = (() => {
2006
3519
  const hc = parsedSettings.settings?.hooks;
2007
3520
  if (!hc)
@@ -2010,55 +3523,122 @@ async function main() {
2010
3523
  logger.warn("task_hooks_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
2011
3524
  return undefined;
2012
3525
  }
3526
+ // payload 的 permission_mode 用【生效】模式——body.permissionMode(显式请求)优先于
3527
+ // settings.permissions.defaultMode(与 spec 的 withPermissionMode 同一优先序),否则钩子读到的模式与
3528
+ // 实际裁决模式不一致(如 body=plan 时钩子仍看到 default)。
2013
3529
  const effMode = coercePermissionMode(body.permissionMode) ?? parsedSettings.settings?.permissions?.defaultMode;
2014
3530
  return createTaskHooks(hc, {
2015
3531
  logger,
2016
3532
  sessionId: auth?.sessionId ?? "",
2017
3533
  cwd: (auth?.sessionId ? perSessionCwd.get(auth.sessionId) : undefined) ?? process.cwd(),
2018
3534
  ...(effMode ? { permissionMode: effMode } : {}),
3535
+ // 用户变量通道(hook 进程 env + http $NAME 插值源):同请求的 settings.env——hookEnv 绝不读
3536
+ // 裸 process.env 的非 allowlist 键(服务密钥不可被 allowedEnvVars 点名)。
2019
3537
  ...(parsedSettings.settings?.shellEnv ? { shellEnv: parsedSettings.settings.shellEnv } : {}),
3538
+ // 阶段三b:prompt/agent 条目的模型载体(boot 组装,见 hookLlm/hookAgent 定义处的影响范围说明)。
2020
3539
  hookLlm,
2021
3540
  hookAgent,
3541
+ // cli [1786] 点名的观测位:一次 hook 判定**未能完成**时,往常开的 fleet 流推一帧。
3542
+ // ⚠️ **纯 observe**(不拦、不续跑、不注入)——`additionalContext` 那条通道会让这一轮不结束,
3543
+ // 所以不能拿它当"提示一下"用(见 hook-runner 折叠处旁注)。
3544
+ // ⚠️ 带 ownerScope/ownerSessionId 供流侧做 **fail-CLOSED** 过滤(与 bg_notification 同一姿势):
3545
+ // 一条关于别人会话的通知落到这个壳上,会让用户以为是自己这轮出了问题。
2022
3546
  onHookNotice: (n) => fleetBus.publishHookNotice({
2023
3547
  ...n,
2024
3548
  ...(auth?.principal ? { ownerScope: auth.principal } : {}),
2025
3549
  ...(auth?.sessionId ? { ownerSessionId: auth.sessionId } : {}),
2026
3550
  }),
3551
+ // asyncRewake wake 管道(契约:exit 2 唤醒模型):session→本副本活流 steer(hookWakeBus,server
3552
+ // 启动装 deliver)。stderr 过 redactSecrets 再注入(hook 进程可能回显 env 密钥;与 trace 出口同纪律)。
3553
+ // 无 session(匿名一次性任务)不给 wake——没有可稳定寻址的流。
2027
3554
  ...(auth?.sessionId
2028
3555
  ? {
2029
3556
  wake: async (text) => hookWakeBus.deliver
2030
- ?
3557
+ ? // provenance 围栏:注入内容标明来源=asyncRewake hook 的运行时输出,
3558
+ // 模型按 hook 反馈对待而非操作者指令(trusted:false 在 server 侧同批落定)。
2031
3559
  hookWakeBus.deliver(auth.sessionId, `[hook asyncRewake] ${redactSecrets(text)}`)
2032
3560
  : false,
2033
3561
  }
2034
3562
  : {}),
2035
3563
  });
2036
3564
  })();
3565
+ // design/119 (CC --add-dir parity, core 1.215 `TaskSpec.additionalDirectories`): extra host dirs the FILE tools
3566
+ // may access beyond the containment root. 🔒 GATED identically to cwd/shellEnv (single-user host lane only —
3567
+ // these are the caller's own-machine paths; a cloud/multi-tenant lane must not let a caller widen the file-tool
3568
+ // containment onto operator/other-tenant host paths). Unlike cwd/shellEnv (registered per-session for the host
3569
+ // FACTORY to read at env construction), additionalDirectories is a plain TaskSpec field core reads directly, so
3570
+ // it rides onto the spec below. Ignored (logged, never silent) off the host lane. Re-applies on resume (from body).
2037
3571
  const rawAddDirs = body.additionalDirectories;
2038
3572
  const additionalDirectories = cwdHonored(config) ? parseAdditionalDirectories(rawAddDirs) : undefined;
2039
3573
  if (rawAddDirs !== undefined && !cwdHonored(config)) {
2040
3574
  logger.warn("task_additional_directories_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
2041
3575
  }
3576
+ // body.model (explicit /model picker or run --model) WINS over settings.model (the config default); both gate
3577
+ // against the catalog allow-list via resolveTaskModel (a settings.model not in the catalog is ignored, never injected).
3578
+ // 🔴 TOB 档位联跑曾 FAIL(坐标级):the gate must see the TIER-EXPANDED catalog — expandTiers
3579
+ // runs inside core's Runner on a PRIVATE deps copy (runtask.ts), so gating on bare config.models silently
3580
+ // degraded every tier word (pro/flash/@mention/CC alias) to default BEFORE the Runner could resolve it.
3581
+ // Expand per request (cheap: empty tiers returns the same reference; non-empty = one small spread) so the
3582
+ // gate's key set is exactly what a freshly-constructed Runner resolves — tier words, degrade chains, CC
3583
+ // aliases, catalog-SHADOW all from core's ONE implementation. Window note: tiers are restart-to-apply at
3584
+ // the Runner; a refresh-time tier change makes this gate briefly AHEAD of the old Runner snapshot (a new
3585
+ // tier word then fails loud in core instead of silently degrading — honest during the restart window).
2042
3586
  const wireCatalog = expandTiers(config.models, config.tiers) ?? config.models;
2043
3587
  const picked = resolveTaskModel(body.model ?? parsedSettings.settings?.model, objective, wireCatalog);
3588
+ // [865]② 降级永远可见:fresh submit 的未知 body.model 已在 HTTP 门 400(不会到这);走到这的未知 ref
3589
+ // 只剩 RESUME 重放(模型事后被移出目录)与 settings.model(lenient 文档面)——落 default 可跑,但必须
3590
+ // 有声(session 中途换模型是最恶性的上下文污染路径,静默=病灶本体)。
2044
3591
  if (picked.unknownExplicit !== undefined) {
2045
3592
  metrics.inc("task_model_unknown_fallback_total");
2046
3593
  logger.warn("task_model_unknown_fallback", { requested: picked.unknownExplicit.slice(0, 120), fallback: picked.model, sessionId: auth?.sessionId ?? null });
2047
3594
  }
3595
+ // no-vision images = PLACEHOLDER, not 422(会话可继续的 CC 心智). The old
3596
+ // fail-loud 422 here BLOCKED core's own degradation: since core 1.233.1 a serving model whose `input`
3597
+ // has no "image" gets image parts degraded to a bounded text placeholder ("[image omitted: …]"), per
3598
+ // CURRENT model (degrade chains stay correct) and re-applied on every history replay — so a previously
3599
+ // poisoned session self-heals. We pin ≥1.240.1, so pass the images through and let core do exactly that.
3600
+ // The GET /v1/models vision flag stays advertised (the shell's composer-warning half). A model
3601
+ // that never declared `input` keeps sending images verbatim (deliberate default — never wrongly strip
3602
+ // for a vision-capable deployment that skipped the metadata; same posture).
3603
+ // S6 (SILENT-FALLBACK P0-c): images headed for an EXPLICITLY text-only model — core (>=1.233.1) degrades
3604
+ // them to bounded text placeholders per current model (the pass-through posture above). That modality drop
3605
+ // was invisible fleet-side; count + warn at submit. modelSupportsImages is false-safe (undeclared input →
3606
+ // true → no signal), matching core's "never wrongly strip" posture — no false positives for verbatim
3607
+ // pass-through models. ⚠️ sema-registry lane caveat (review LOW-2): the roster maps `vision?: boolean` to
3608
+ // an EXPLICIT input list (sema-registry.ts) — a vision-capable roster model that FORGOT the flag becomes
3609
+ // declared text-only, so this signal fires AND core really does degrade its images (signal matches
3610
+ // behavior; the fix for that footgun is the roster entry, not this counter). The per-run durable event rides core's vision.placeholder trace kind once we
3611
+ // consume >=1.245 (the authoritative replacement-time signal), not this submit-time predictor.
2048
3612
  if (Array.isArray(body.images) && body.images.length > 0 && !modelSupportsImages(wireCatalog[picked.model])) {
2049
3613
  metrics.inc("images_omitted_total", { model: picked.model });
2050
3614
  logger.warn("images_omitted_no_vision_model", { model: picked.model, count: body.images.length });
2051
3615
  }
3616
+ // E18 resume-at: the shell sends body.resumeAt as the E2 message eventId; core's TaskSpec.resumeAt takes the
3617
+ // PERSISTED SessionTreeEntry.id, NOT the eventId ("a deployment that holds eventIds owns the eventId→entryId
3618
+ // map" — core never persists eventIds). Resolve eventId→entryId via the anchor store HERE, 4xx-ing BEFORE the
3619
+ // run on an UNKNOWN anchor (never hand core an unresolvable id). A KNOWN anchor whose entry was later compacted
3620
+ // away surfaces as a failed run RESULT (resume_at.not_found) — 4xx on the sync path, the run result on async (the
3621
+ // async contract is poll-the-result). RESUME re-runs resolveSpec from the stored body — the resume paths strip
3622
+ // resumeAt before re-resolving (else core would reject resumeAt + a durable resume), so this branch only fires on
3623
+ // a fresh submit. NEVER silent-drop (mirrors the body.model silent-drop bug class).
2052
3624
  let resumeAtEntryId;
2053
3625
  if (typeof body.resumeAt === "string" && body.resumeAt.length > 0) {
2054
3626
  if (!auth?.sessionId)
2055
3627
  throw new HttpError(422, "resumeAt requires a session to branch (resume_at.no_session)");
3628
+ // 501 gated on the SAME pair the `resumeAt` capability advertises (anchor store AND getLeafId) — capture is a
3629
+ // no-op without getLeafId, so a getLeafId-less backend has a permanently-empty map; report "not available"
3630
+ // (honest 501) rather than a misleading 404. Keeps "capability says yes ⟺ the route resolves" by construction.
2056
3631
  if (!resumeAnchorStore || !ownerAware.getLeafId)
2057
3632
  throw new HttpError(501, "resume-at is not available on this worker (no session-store backend for the anchor map)");
2058
3633
  resumeAtEntryId = await resumeAnchorStore.resolve(auth.sessionId, body.resumeAt, auth.principal ?? null);
2059
3634
  if (resumeAtEntryId === undefined)
2060
3635
  throw new HttpError(404, "resumeAt: no such message in this session (resume_at.unknown_event)");
2061
3636
  }
3637
+ // R8 code-only rewind (CC "Restore code" mode, core 1.166 `rewindFilesTo`): restore the working tree to a prior
3638
+ // USER message's snapshot WITHOUT forking the conversation. Resolve the SAME handle (the prompt's taskId) → entryId
3639
+ // via the SAME anchor store. Honored ONLY when resumeAt is ABSENT (core's contract: the two are mutually exclusive —
3640
+ // with resumeAt set, that branch's own rewindFiles governs); so we only resolve it on the code-only path. Same
3641
+ // fail-loud discipline as resumeAt (501 no-store / 404 unknown anchor — never silent-drop).
2062
3642
  let rewindFilesToEntryId;
2063
3643
  if (resumeAtEntryId === undefined && typeof body.rewindFilesTo === "string" && body.rewindFilesTo.length > 0) {
2064
3644
  if (!auth?.sessionId)
@@ -2069,51 +3649,204 @@ async function main() {
2069
3649
  if (rewindFilesToEntryId === undefined)
2070
3650
  throw new HttpError(404, "rewindFilesTo: no such message in this session (rewind_files_to.unknown_event)");
2071
3651
  }
3652
+ // 142-S4 defaultScopes 种子:projectId → center 项目登记簿(config.projects,registry 喂)→ 该项目
3653
+ // 声明的默认记忆 scope 词表(memory.scopes 额外 READ 层;写路由仍钉在派生 scope — memorySpecForRequest)。
3654
+ // projectId 优先取 auth.resolvedProjectId(authorizer 的 PROJECT_ID_REGEX 422 门通过后回传);RESUME 腿
3655
+ // 的 auth 从 checkpoint 重建、无此字段 → 回落持久化 body.projectId(原始提交已过形状门,与 memoryWrite
3656
+ // 的 rides-the-persisted-body 姿势一致),让种子在 resume 腿等价重放。查表 miss(未登记/已下架)= 无种子,
3657
+ // 不 fail(登记簿是加性配置面,不是准入门)。
2072
3658
  const s4ProjectId = auth?.resolvedProjectId ?? (typeof body.projectId === "string" && body.projectId ? body.projectId : undefined);
2073
3659
  const s4DefaultScopes = s4ProjectId ? config.projects[s4ProjectId]?.defaultScopes : undefined;
2074
3660
  const spec = {
3661
+ // D-1:附件告知随 objective 进 durable 流(只有名字/mime/尺寸——**内容永不进流**,这正是
3662
+ // 「拼进 objective 是伪方案」的账要划清的线;文件名已消毒为安全字符集,无注入面)。
2075
3663
  objective: attachmentNotice ? `${picked.cleanedObjective}\n\n${attachmentNotice}` : picked.cleanedObjective,
3664
+ // Decoupling seam: a trusted client (token-holder) injects its OWN business/UX system prompt
3665
+ // (persona, UI markers like ::ISSUE_FORM::, workflow rules). The service stays business-agnostic
3666
+ // — integrators evolve their UX here, not by changing scenario prompts in this repo. core's
3667
+ // stableSystem appends it after the scenario base, before <user_memory>.
2076
3668
  systemPrompt: typeof body.systemPrompt === "string" ? body.systemPrompt : undefined,
3669
+ // [1476] R1 / [1478] R2: top-level append rider → core TaskSpec.appendSystemPrompt (stable block after the
3670
+ // scenario base + systemPrompt, before the volatile tail). settings.outputStyle composes AFTER this via
3671
+ // applyTaskSettings (`append\n\nstyle`) — byte-stable order: knowledge block always first, style always
3672
+ // second (pinned by test; order flapping would shred the prefix cache). Resume re-enters here off the
3673
+ // persisted body, so the rider re-applies on resumed legs like systemPrompt does — DEFENSIVELY (codex F2):
3674
+ // the resume families skip prepareSpec's 400 gate and a pre-1.243 store kept unknown keys verbatim, so an
3675
+ // over-cap legacy value is dropped+warned here instead of becoming active prompt content.
2077
3676
  appendSystemPrompt: acceptedAppend,
3677
+ // sessionId comes from `auth` (ownership-checked / principal-derived), NEVER the body — the
3678
+ // authorizer always resolves one. (No `?? body.sessionId` fallback: that would re-open the
3679
+ // body-as-capability hole S6 closed if a deployment ran without an authorizer.)
2078
3680
  sessionId: auth?.sessionId,
3681
+ // The authenticated end-user principal (design/62, core 1.78). Read-only on the TaskSpec — core injects
3682
+ // it into a consumer MCP's `principalHeader` per-task so the MCP can enforce per-user RBAC (oa-mcp). It
3683
+ // comes from the auth channel (never the body/model), so the model can't substitute another identity.
2079
3684
  principal: auth?.principal,
2080
3685
  images: body.images,
3686
+ // design/112 C1: pass the client/gateway-supplied user context through to core (localizes the env block's date
3687
+ // to the user's zone + surfaces who the agent acts for). HTTP-validated + capped at submit (prepareSpec); on a
3688
+ // resume the body was already validated at its original submit. core re-validates timeZone (invalid → UTC) and
3689
+ // SANITIZES userEmail (inlineUntrusted: folds newlines, neutralizes <system-reminder>, caps 160cp — no injection;
3690
+ // double-review-confirmed). PICK only the two fields core reads — don't passthrough stray keys (a caller could
3691
+ // otherwise bloat the persisted run body with an uncapped junk field; core ignores them but the body stays tight).
2081
3692
  ...(body.clientContext ? { clientContext: { timeZone: body.clientContext.timeZone, userEmail: body.clientContext.userEmail } } : {}),
3693
+ // Structured output (CC --json-schema): a JSON Schema constraining the model's FINAL
3694
+ // answer. core injects a built-in `submit_output` tool from it + surfaces the validated object as
3695
+ // TaskResult.structuredOutput. HTTP layer shape+size-validated it (prepareSpec); core does deep validation.
3696
+ // RESUME re-runs resolveSpec ⇒ the structured-output constraint re-applies on resumed legs too.
3697
+ // 历史复审轴B #5(1.254):compactionModel 已接([1479]①)而 design/145 配套旋钮 clampTolerance
3698
+ // 够不着=不对称。窄收单键(0..1 数值;其余 compaction 键仍是操作方轴,有意不开)。
2082
3699
  ...(() => {
2083
3700
  const ct = body.compaction?.clampTolerance;
2084
3701
  return typeof ct === "number" && Number.isFinite(ct) && ct >= 0 && ct <= 1 ? { compaction: { clampTolerance: ct } } : {};
2085
3702
  })(),
2086
3703
  outputSchema: body.outputSchema,
3704
+ // 历史复审轴A #2(1.254):outputRetries 与 outputSchema 是成对的 caller 面旋钮(retry-on-invalid
3705
+ // 轮数),此前只接了 schema 半边。窄校验:1..10 整数,schema 在场才有意义(core 无 schema 时忽略)。
2087
3706
  ...(typeof body.outputRetries === "number" && Number.isFinite(body.outputRetries) && body.outputRetries >= 1
2088
3707
  ? { outputRetries: Math.min(10, Math.floor(body.outputRetries)) }
2089
3708
  : {}),
3709
+ // E18 resume-at: the resolved persistent SessionTreeEntry.id (from the eventId the shell sent). core branches
3710
+ // the session at this prior entry before running the new objective ("rewind to this message, ask differently").
2090
3711
  resumeAt: resumeAtEntryId,
3712
+ // [833] rewind exclusive mode (core 1.292): "before" branches at the target's PARENT (excludes the target —
3713
+ // "remove this prompt and everything after it"); absent/"at" = core default, zero regression. Defensive
3714
+ // normalize (RESUME re-enters resolveSpec without the HTTP 400 gate): only the two enum values ride, and only
3715
+ // WITH a resolved resumeAt (core ignores the field otherwise; keep the persisted spec tight). core's "before"
3716
+ // edge rejections (resume_at.before_target_not_user / resume_at.before_root_unsupported /
3717
+ // rewind_snapshot.unresolvable) surface as failed-result errorCodes mapped to 4xx by resumeAtHttpStatus,
3718
+ // errorCode passed through UNCHANGED for the shell to render.
2091
3719
  resumeAtMode: normalizeResumeAtMode(body.resumeAtMode, resumeAtEntryId !== undefined),
3720
+ // design/114 Phase3 (reuse-path warm-resume): require the session to already exist — core's acquire fails loud
3721
+ // (`resume.session_not_found`) on a genuinely-missing/purged session instead of silently starting a fresh one.
3722
+ // The service authorizer ALSO stops pre-registering a missing id under this flag (security.ts), so the store's
3723
+ // fail-loud actually fires (else the authorizer's claim-create would materialize the session first). Rides onto
3724
+ // resume legs (resolveSpec re-runs); a resumed session exists, so it's satisfied there.
2092
3725
  requireExistingSession: body.requireExistingSession === true ? true : undefined,
3726
+ // §4 (Fork): CC `/fork` — core 1.257 (design/136 BREAKING) retired the standalone Fork tool; a fork is now
3727
+ // Agent(subagent_type:"fork") gated by `enableFork`. 🔴 core FLIPPED enableFork to opt-OUT (undefined = available),
3728
+ // so enableForkFromBody now returns an EXPLICIT boolean (never undefined) — false where the service denies, else
3729
+ // fail-OPEN. DEFAULT ON single-user turnkey (clay 2026-07-01); per-task opt-OUT via body.enableFork:false;
3730
+ // multi-tenant honored ONLY with a per-principal entitlement resolver wired (core enforces `allowFork`), else
3731
+ // explicit false (fail-closed). core still requires a fork-capable store (`hasSessionFork`; TOC file backend
3732
+ // satisfies it via LocalSessionStore.fork), else inert.
3733
+ // NB: `centerRuntimeCapsResolver`(非合成后的 runtimeCapsResolver)—— env observer 基线不是
3734
+ // center 背书的 entitlement 源,多租户 fail-close 判别只认 center caps client。
2093
3735
  enableFork: enableForkFromBody(body, config, Boolean(centerRuntimeCapsResolver)),
3736
+ // E12 (shell-host contract): opt-in post-completion prompt suggestions. core runs ONE extra fire-and-forget LLM pass
3737
+ // after the task completes; the service surfaces the strings (redacted, UNTRUSTED UI-only) as a `suggestions`
3738
+ // event. Defensive normalize (RESUME re-runs resolveSpec without the HTTP 400 validation). OFF ⇒ zero extra LLM.
2094
3739
  suggestNextPrompts: normalizeSuggestNextPrompts(body.suggestNextPrompts),
3740
+ // E19 (shell-host contract): opt-in working-tree rewind. core snapshots each completed turn + restores the files on a
3741
+ // resumeAt branch, for any env when fileSnapshotStore is wired (gate-split 1.134.0). OFF ⇒ no snapshot.
2095
3742
  rewindFiles: body.rewindFiles === true ? true : undefined,
3743
+ // R8 code-only (core 1.166): the resolved target entry id for "Restore code" — core restores the working tree to
3744
+ // its snapshot WITHOUT branching the session. Mutually exclusive with resumeAt (only set on the code-only path).
2096
3745
  rewindFilesTo: rewindFilesToEntryId,
3746
+ // design/119 (CC --add-dir): extra host dirs the FILE tools may access (core canonicalizes each into the
3747
+ // containment allowlist + lists them in the `# Environment` block). Single-user host lane only (gated above).
2097
3748
  additionalDirectories,
3749
+ // EnterPlanMode (core 1.167): MODEL-DRIVEN plan — `enablePlanMode:true` alone (writable
3750
+ // start, NO handsReadOnly) auto-mounts `enter_plan_mode`/`present_plan` so the model can self-enter read-only
3751
+ // plan AT ANY TIME it judges a task needs planning. 🆕 DEFAULT ON (clay: resident; PLAN_MODE_ENABLED=false opts
3752
+ // out). USER-driven plan (permissionMode=plan) ADDS handsReadOnly on top via applyTaskSettings (which also sets
3753
+ // enablePlanMode, idempotent). Additive — widens nothing; the model self-selects when to use it.
2098
3754
  enablePlanMode: config.planModeEnabled ? true : undefined,
3755
+ // Workflow super-set unlock (same shape as the rank-1 roster gap): per-task activation of the
3756
+ // LLM-authored workflow engine (core `run_workflow` + the workflow.ts agent/parallel/pipeline orchestration).
3757
+ // The engine is BUILT but the HTTP API never mapped a body field to `spec.selfOrchestration`, so run_workflow
3758
+ // was per-task unreachable (capabilities advertised `workflows:true` but the LLM could never call it). Gated on
3759
+ // the DEPLOYMENT enabling it (`selfOrchestrationEnabled` → workflowScriptRunner/stores wired; else a set flag is
3760
+ // a harmless no-op core fail-closes). 🔒 Multi-tenant entitlement is NOT blanket-closed here (unlike mcp, which
3761
+ // has no engine entitlement) — core's `runtimeCapsResolver` enforces `allowWorkflows` (the third-stage
3762
+ // per-principal cap) ON THE ENGINE, so a multi-tenant principal WITHOUT the entitlement gets run_workflow
3763
+ // fail-closed by core (no resolver ⇒ fail-closed). Single-user honors directly. Three-gate: engine-can ∧
3764
+ // center-may(allowWorkflows) ∧ shell-show. (Logic + gates documented in `selfOrchestrationFromBody`.) 🔒 The
3765
+ // multi-tenant floor: pass whether an entitlement RESOLVER is wired — core's allowWorkflows is TIGHTEN-ONLY
3766
+ // (no resolver ⇒ fail-OPEN per-principal), so the helper fail-closes multi-tenant when none is present.
3767
+ // L2 ultracode (design/111): OR the preset's selfOrchestration:true into the body intent, passed THROUGH the
3768
+ // existing gate — it grants nothing a raw body.selfOrchestration:true couldn't (multi-tenant still fail-closes
3769
+ // without an entitlement resolver). The preset never bypasses the闸.
2099
3770
  selfOrchestration: selfOrchestrationFromBody({ selfOrchestration: body.selfOrchestration === true || parsedSettings.settings?.ultracode === true }, config, Boolean(centerRuntimeCapsResolver)),
3771
+ // C1 (core 1.219, subagent viewing pane): opt-in widening of the forward sink from
3772
+ // task_progress-only to a delegated child's live CONTENT events (text_delta/reasoning_delta/tool_start/
3773
+ // tool_end, each carrying `parentToolCallId` attribution). Purely a RENDER channel — core never merges the
3774
+ // child stream into the parent's model context. The service's forward-sink consumers redact per §E1 (shared
3775
+ // builders); default OFF = prior progress-only behavior. Strict `=== true` (never a truthy coercion).
2100
3776
  forwardSubagentEvents: body.forwardSubagentEvents === true ? true : undefined,
3777
+ // design/122 (core 1.225): opt-in retention of SETTLED sub-agent sessions so the operator/
3778
+ // shell can REVIVE them (POST /v1/runs/:id/subagents/:target/resume). Defensive normalize + clamp
3779
+ // (RESUME re-runs resolveSpec without HTTP validation): boolean passes; an object's ttlMs/max are clamped
3780
+ // to sane ceilings (retention pins live sessions in memory — a caller must not turn the knob into a
3781
+ // resource hold; core's own defaults are 30min/16, the run-scoped ledger releases everything at parent
3782
+ // end either way). Malformed values fall to undefined = OFF (core default), never a throw.
2101
3783
  retainSubagentSessions: normalizeRetainSubagentSessions(body.retainSubagentSessions),
3784
+ // [876] per-task custom subagents (core 1.295 TaskSpec.agents — CC `.claude/agents/` parity): the shell's
3785
+ // resolved AgentDefinition[] rides the spec verbatim (model already a real name/Model object — no alias
3786
+ // translation here). 🔒 TOB tenant gate + defensive per-item normalize live in taskAgentsFromBody
3787
+ // (spec-fields.ts): multi-tenant is fail-closed (warn task_agents_ignored — no per-tenant caps face yet,
3788
+ // capabilities.taskAgents advertises false); RESUME replays skip the HTTP whitelist 400 → illegal items
3789
+ // DROP+warn per entry (task_agent_dropped), never a bricked resume. Absent/empty ⇒ no key (byte-compat).
3790
+ // [1.211 codex L] 内联闭包换 spec-fields 真出口(taskAgentsSpecFragment)——测试直接 import 调真
3791
+ // fold,SOURCE PIN 收敛为「resolveSpec 内恰一次调用点」单锚(文本重写同构表达式的假绿面就此关)。
2102
3792
  ...taskAgentsSpecFragment(body.agents, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })),
3793
+ // [854]② (core 1.295): opt-out of task-end session-background reaping ("keep my dev server running").
3794
+ // Resource-residency grant on the host lane → single-user gate like backgroundShell/hooks (multi-tenant
3795
+ // warns task_retain_bg_ignored + ignores; capability advertises false). Only literal `true` rides (false =
3796
+ // core default, key omitted); non-boolean is 400 at submit, defensively dropped on a resume replay.
2103
3797
  ...(retainBackgroundProcessesFromBody(body.retainBackgroundProcesses, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })) === true
2104
3798
  ? { retainBackgroundProcesses: true }
2105
3799
  : {}),
3800
+ // [1052]② (core 1.314): per-run 工具面收窄两位——excludeTools=roster 真卸载,deferTools=延迟披露
3801
+ // (cli「Workflow 默认开不暴露」承载位)。tighten-only(core 继承 union 不变量),无租户门;defensive
3802
+ // normalize(resume 重放),提交面 400 在 HTTP 门。
2106
3803
  ...(() => { const v = toolNameListFromBody(body.excludeTools); return v ? { excludeTools: v } : {}; })(),
2107
3804
  ...(() => { const v = toolNameListFromBody(body.deferTools); return v ? { deferTools: v } : {}; })(),
3805
+ // [1144]/[1146] (core 1.328 R2): promptProfile 双形轴直通——纯呈现轴无租户门(deferTools 姿势);
3806
+ // 缺省不挂键(引擎缺省 simple);非法值由 HTTP 门 400,resume 重放 defensive DROP。
2108
3807
  ...(() => { const v = promptProfileFromBody(body.promptProfile); return v ? { promptProfile: v } : {}; })(),
3808
+ // [922]①/[915]② (core 1.296): interactiveTools 三态旋钮直通——boolean 上 spec(壳 -p 恒 stamp false=
3809
+ // A3 类「headless 提问 park」根治腿;plan 显式 true 恒赢),undefined=core 自动判据(送达面探测)。
3810
+ // per-run 工具面选择无跨租户面 ⇒ 无租户门;非 boolean 由 HTTP 门 400,resume 重放 typeof 检查天然 DROP。
2109
3811
  ...(typeof body.interactiveTools === "boolean"
2110
3812
  ? { interactiveTools: body.interactiveTools }
2111
3813
  : {}),
3814
+ // design/131 (core 1.246): per-task resilience INTENT flags. allowDegrade/allowFailover are
3815
+ // caller-facing (a bench/eval run wants true failure shapes); bypassBreaker is operator-only (normalizer
3816
+ // drops it for non-operators — it punches through a SHARED breaker). All-absent = byte-compat.
2112
3817
  resilience: normalizeResilience(body.resilience, explicitOperator(auth?.principal, config.operatorPrincipals)),
3818
+ // design/132 (core 1.249): one-shot end-game verification nudge ("re-run the final artifact through
3819
+ // its real entrypoint before finishing"). OPT-IN by core's own judgment (default ON lost the evidence case:
3820
+ // +1 turn on every interactive write task) — the AUTONOMY caller declares it (harness/scheduler lanes).
3821
+ // [849] scenario-declared leg: the `autonomous` scenario pins it in its bundle (cap.finalVerification —
3822
+ // picking that scenario IS the autonomy declaration, so this is not "inferring from scenario"); OR-folded
3823
+ // with the caller's explicit flag — a scenario can only ADD the closing verification turn, never strip a
3824
+ // caller's request. Children do not inherit (core semantics).
2113
3825
  finalVerification: cap.finalVerification === true || body.finalVerification === true ? true : undefined,
3826
+ // design/133 (core 1.251): turn-boundary attachment reminders (todo/changed-files/plan-mode),
3827
+ // literal-true unions — the normalizer drops `false`/garbage keys instead of forwarding them (core contract:
3828
+ // "off = delete the key, never send false"). Resume-safe by construction: the persisted body is full JSON
3829
+ // and this is the single translation point, so this line IS the whitelist inclusion.
2114
3830
  attachments: normalizeAttachments(body.attachments),
3831
+ // hooks(阶段一,gate 见上面 taskHooks 装配):core 是整槽覆盖(`spec.hooks ?? deps.hooks`,
3832
+ // runtask gateBaseline 注释点名),task hooks 一挂会 shadow deps.hooks 上的 TOOL_TRACE 观测 → composeHooks
3833
+ // 把部署基线折进来(部署槽先跑=观测看到真实执行)。无 task hooks 时不挂字段,deps 路径原样。
2115
3834
  ...(taskHooks ? { hooks: composeHooks(deploymentHooks, taskHooks) } : {}),
2116
3835
  model: picked.model,
3836
+ // [1479]① compactionModel (design/145 cheap compaction gear): catalog-gated to the SAME expanded view as
3837
+ // body.model (canonical name form). resolveSpec is the AUTHORITY (codex R5 — the prepareSpec 400 is only
3838
+ // a fast-fail UX layer; the catalog hot-refreshes in place, so a ref valid at the HTTP gate can be gone
3839
+ // after the authorize/resolve awaits): unknown on a FRESH leg → 400 fail-loud (the documented guarantee —
3840
+ // never a silent price change); unknown on a RESUME replay → drop + warn (core's resolveModel THROWS on
3841
+ // an unknown compactionModel, which would brick the resume; the fallback is the summarize role / main
3842
+ // model, the pre-field behavior).
3843
+ // Window note (codex R6, requalified): the resolveSpec→Runner handoff shares spec.model's DOCUMENTED
3844
+ // restart-window class ([865] / the wireCatalog note above) — the Runner resolves the canonical string
3845
+ // against ITS OWN constructor-time expanded snapshot, so a hot refresh between here and prepare can
3846
+ // desync the two views. Inside that window the failure mode is core's resolveModel TYPED THROW
3847
+ // ("Unknown model ref", roles.js dist-read) = a fail-loud task error, never a silent price change —
3848
+ // the same honest posture the sibling field ships with. Binding a Model OBJECT here instead would
3849
+ // diverge from spec.model's string-intent contract and freeze catalog bytes into the persisted body.
2117
3850
  ...((() => {
2118
3851
  if (typeof body.compactionModel !== "string" || body.compactionModel.length === 0)
2119
3852
  return {};
@@ -2128,10 +3861,28 @@ async function main() {
2128
3861
  }
2129
3862
  return { compactionModel: cm };
2130
3863
  })()),
3864
+ // E7 (shell-host contract): reasoning-effort selection threaded to core's ThinkingLevel. Defensive
3865
+ // isThinkingLevel guard — RESUME re-runs resolveSpec WITHOUT the HTTP-layer 400 validation, so an invalid/
3866
+ // absent value falls through to the resolved role's default thinking (core's RoleSpec.thinking), never throws.
3867
+ // L2 ultracode (design/111): the preset FLOORS thinking at the ultra tier (= max, via core resolveReasoningProfile);
3868
+ // awareness rides for free (core composes it from thinking∈{xhigh,max}). Else the explicit reasoningEffort.
2131
3869
  thinking: effectiveThinking(body.reasoningEffort, parsedSettings.settings?.ultracode === true),
3870
+ // Per-model auth (sema-registry apiKeyEnv): core calls this per brain call / cascade rung so each
3871
+ // model uses its own upstream key; a model without one falls back to the gateway key. undefined
3872
+ // when no per-model keys are configured → unchanged single-key behavior.
2132
3873
  getApiKeyAndHeaders: keyResolver,
3874
+ // [854]④ per-request 配速:body.limits.{timeoutSec,maxOutputTokens,maxTurns} 现在被收下(核对上游
3875
+ // TB2.0 实测诉求;旧姿势「body limits 一律忽略」作废)。合成规则在 resolveTaskLimits(spec-fields.ts):
3876
+ // - body.timeoutSec 给了就用 body(caller 显式配速,可低于内建墙;已被可选 TASK_TIMEOUT_MAX_SEC 封顶);
3877
+ // - body 缺席保持既有姿势 = tenancy 墙钟(单用户 turnkey 无墙 / 多租 2400s、大任务 3600s,
3878
+ // clay 2026-07-04 make-real 教训①)+ env TASK_TIMEOUT_SEC 只抬不降(taskWallClockSec 内 Math.max);
3879
+ // - maxOutputTokens/maxTurns 直透传(可选 TASK_MAX_OUTPUT_TOKENS_MAX / TASK_MAX_TURNS_MAX 同款封顶)。
3880
+ // resume 重放持久化 body 不过 HTTP 400 门 → normalizeLimits defensive(0/负/垃圾按键 DROP,不 throw)。
2133
3881
  ...(() => {
2134
3882
  const limits = resolveTaskLimits(body.limits, taskLimitCaps, taskTimeoutSec, config.requirePrincipal, body.council === true || body.debate === true || scenarioName === "team");
3883
+ // [1301]③ config catalog server 半场:env 封顶不再是「五层五值互不知情」的暗手——每个真在场
3884
+ // 的运营方旋钮以 configOverrides 声明进 spec(advisory,core 折进 config.assembled 的
3885
+ // overrideReasons;「谁设的顶」变成读帧不考古)。只声明 SET 了的键(缺省不设=不污染帧)。
2135
3886
  const declarations = [];
2136
3887
  if (taskLimitCaps.timeoutSec !== undefined)
2137
3888
  declarations.push({ key: "server.limits.timeoutSecCap", value: String(taskLimitCaps.timeoutSec), reason: "env TASK_TIMEOUT_MAX_SEC (operator ceiling on caller limits.timeoutSec)" });
@@ -2139,39 +3890,101 @@ async function main() {
2139
3890
  declarations.push({ key: "server.limits.maxOutputTokensCap", value: String(taskLimitCaps.maxOutputTokens), reason: "env TASK_MAX_OUTPUT_TOKENS_MAX (operator ceiling)" });
2140
3891
  if (taskLimitCaps.maxTurns !== undefined)
2141
3892
  declarations.push({ key: "server.limits.maxTurnsCap", value: String(taskLimitCaps.maxTurns), reason: "env TASK_MAX_TURNS_MAX (operator ceiling)" });
3893
+ // PAIR-REVIEW F-4:budget 族与墙钟基值的声明补齐——[1301]③ 首批只做了 limits 三键,而
3894
+ // MAX_TASK_COST_USD/MAX_TASK_TOKENS 在 caller 缺席时**直接成为** spec 值(cappedCeiling),core
3895
+ // 便记 provenance:"spec"=归因谎报(「谁设的 5 刀」重回考古)。同姿势:只声明 SET 了的键,
3896
+ // server 命名空间,绝不重复 core 目录键。
2142
3897
  if (config.maxTaskCostUsd > 0)
2143
3898
  declarations.push({ key: "server.budget.maxCostUsdCap", value: String(config.maxTaskCostUsd), reason: "env MAX_TASK_COST_USD (operator ceiling; becomes the effective value when the caller sends none)" });
2144
3899
  if (config.maxTaskTokens > 0)
2145
3900
  declarations.push({ key: "server.budget.maxTokensCap", value: String(config.maxTaskTokens), reason: "env MAX_TASK_TOKENS_MAX (operator ceiling; becomes the effective value when the caller sends none)" });
3901
+ // F-4 附:tenancy 墙钟基值(taskWallClockSec 在 multi-tenant 时注入 2400/3600 基值——同为
3902
+ // 「server 设的顶」,声明其来源;single-user 无墙=不声明)。
2146
3903
  if (config.requirePrincipal === true)
2147
3904
  declarations.push({ key: "server.limits.wallClockBaseSec", value: "2400/3600", reason: "tenancy wall-clock base (multi-tenant; big tasks 3600) — TASK_TIMEOUT_SEC raises, never shrinks" });
3905
+ // codex F1:core 目录自有键(limits.timeoutSec 等)**绝不重复声明**——core 已按真 provenance 发
3906
+ // 该字段,叠一条 host-declared 字符串值=同键双条矛盾帧(by-key 投影抹掉权威来源)。server 只
3907
+ // 声明自己命名空间的键;来源语义(caller vs 墙钟)由 timeoutSec 的 spec provenance + 上面的
3908
+ // Cap 声明组合可读。
2148
3909
  return { ...(limits !== undefined ? { limits } : {}), ...(declarations.length > 0 ? { configOverrides: declarations } : {}) };
2149
3910
  })(),
3911
+ // design/129: the TOC/interactive posture (single-user turnkey — the shell's
3912
+ // session lane) defaults background children to SESSION scope = CC Backgrounded semantics (a bg Agent/
3913
+ // Fork outlives the turn; completion arrives via the durable-inbox push). Multi-tenant / CI / workflow
3914
+ // deployments keep core's "task" default (no orphans burning tokens). Caller-trusted spec field
3915
+ // (systemPrompt tier), NEVER read from the request body. Same tenancy predicate as the wall clock above.
2150
3916
  ...(config.requirePrincipal !== true ? { backgroundScope: "session" } : {}),
3917
+ // ⑤ Per-task budget gate (1.37): honor a caller's requested ceiling but CAP it to the operator
3918
+ // ceiling (a request can ask for less, never more). core fails the task with errorCode budget.*
3919
+ // when crossed. budgetStreamCancel defaults true when maxCostUsd is set.
2151
3920
  maxCostUsd: cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd),
2152
3921
  maxTokens: cappedCeiling(body.maxTokens, config.maxTaskTokens),
3922
+ // 1.40 near-budget degradation: only meaningful with a cost ceiling (the fraction is of it).
3923
+ // When this task has one, switch to the cheaper model at atCostFraction instead of hard-failing.
3924
+ // vision precheck (adversarial-review finding): DROP degrade for an image-carrying task when the
3925
+ // degrade TARGET can't read images (toSupportsImages=false). The precheck only sees the picked model, not the
3926
+ // external degrade target — so without this a runtime degrade would send the images to a text-only gateway (the
3927
+ // opaque 400 the precheck prevents). The task keeps its vision-capable main model; near-budget it hard-fails on
3928
+ // cost instead of image-failing. MODEL_DEGRADE_TO_VISION=true opts back in.
2153
3929
  degrade: (() => {
2154
3930
  if (!config.degrade || cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd) === undefined)
2155
3931
  return undefined;
2156
3932
  if (Array.isArray(body.images) && body.images.length > 0 && !config.degrade.toSupportsImages) {
3933
+ // S14 (SILENT-FALLBACK P0-f): this drop was undetectable — the task keeps its vision-capable main
3934
+ // model and near-budget it hard-fails on cost instead of degrading (rationale above). Surface it.
2157
3935
  metrics.inc("degrade_dropped_total", { reason: "vision_target" });
2158
3936
  logger.warn("degrade_dropped", { reason: "vision_target", model: picked.model, images: body.images.length });
2159
3937
  return undefined;
2160
3938
  }
2161
3939
  return { to: config.degrade.to, atCostFraction: config.degrade.atCostFraction };
2162
3940
  })(),
3941
+ // Long-term memory (design/138 S1): enabled when the memory ENGINE is wired AND a scope was derived
3942
+ // (single-user only — multi-tenant derives none, memory dark). MF-30 PAUSE (option B, per-request —
3943
+ // clay 2026-06-27 confirmed with core): `body.memoryWrite:false` makes THIS run read-only over memory
3944
+ // (`writeScope:null` — the engine materializes/reads but harvest commits nothing). Per-request (not a
3945
+ // stored flag) → no new per-session state; the shell carries the toggle. RESUME re-runs resolveSpec
3946
+ // from the persisted body, so a paused run stays paused across resume legs.
2163
3947
  memory: memoryEngine ? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes) : undefined,
3948
+ // Scenario-provided capabilities (e.g. code-review = repo tools + reviewer subagents + prompt).
3949
+ // RFC A2: the SelectEnvironment tool rides after the scenario's tools (spec.tools is ADDITIVE to core's
3950
+ // built-in roster — prepare-task mounts first-party tools separately). Only when the image chain is live.
2164
3951
  tools: ((base) => {
2165
3952
  const extra = [...(selectEnvTool ? [selectEnvTool] : []), ...(sendUserFileToolSpec ? [sendUserFileToolSpec] : [])];
2166
3953
  const merged = extra.length > 0 ? [...(base ?? []), ...extra] : base;
3954
+ // [803]④ defer face (EXPERIMENTAL, default OFF): long-tail first-party tools get per-tool defer:true
3955
+ // (assessment + why NOT deferMode:"auto"/SendUserFile: src/capabilities/tool-defer.ts header).
2167
3956
  return merged && config.toolDeferLongtail ? applyLongtailDefer(merged, true) : merged;
2168
3957
  })(cap.tools),
3958
+ // Personalization: the caller's own skills ride after the scenario's; scenario
3959
+ // wins on a name clash (mergeUserSkills — the security baseline can't be shadowed). HTTP layer
3960
+ // already validated shape + caps (validateUserSkills).
2169
3961
  skills: mergeUserSkills(cap.skills, body.skills, logger),
3962
+ // Sema-registry MCP servers (B1) filtered to this scenario (env-NAME refs already resolved at startup),
3963
+ // PLUS the caller's gated per-request MCP (R3 — TOC local `.mcp.json`). resolveRequestMcp honors body.mcpServers
3964
+ // on any SINGLE-USER deployment (`requirePrincipal!==true` — the requester is the super-admin of their own
3965
+ // worker; the stdio MCP runs on the worker, not the exec env, so the lane is irrelevant → seamless TOC↔cloud);
3966
+ // a multi-tenant deployment returns the baseline unchanged (gate closed → ignored, advertised via
3967
+ // capabilities.mcpInjection). Baseline wins on a name clash (a caller can ADD a server, never SHADOW a configured one).
2170
3968
  mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger),
3969
+ // center prompts 投影([1057]①f):center 下发的场景终形(overrides[s] ?? 基线)赢过内置 provider
3970
+ // (center-wins,scenario overlay 同先例);无 pack/该场景空声明 ⇒ 内置照旧。声明数组逐字喂 typed
3971
+ // 钩子(contentHash 透传,core 1.315 校验入 manifest);packId 归因在采用日志(center_prompts_adopted)。
3972
+ // codex R4:读请求级快照 centerDecls(与 append-less 门同一份),不再回读活 ref——刷新竞态下门与
3973
+ // provider 才不会各看一版(TOCTOU:门判安全、pick 到已组装包=core 侧静默丢 rider 复活)。
2171
3974
  promptProvider: centerDecls ? centerPromptProvider(centerDecls, scenarioName) : cap.promptProvider,
3975
+ // F4: high-risk write approval gate. DURABLE (design/45, opt-in): the gated `ask` SUSPENDS the task
3976
+ // (durable checkpoint, resumable on any replica). Else the legacy POLL gate: the call waits on a
3977
+ // durable pending row any instance can decide via /v1/approvals, releasing at the task deadline.
3978
+ // Durable ask (TC-5.4, core 1.95): in durable mode an AskUserQuestion call suspends like an F4 gate —
3979
+ // the question policy adjudicates it `ask`, the operator answers out-of-band, and the resume carries
3980
+ // the QuestionAnswer (server.ts `body.answer` → onQuestion closure). Suspend side mounts the tool with
3981
+ // QUESTION_AWAITS_RESUME (it must never run pre-suspend; reaching it = wiring bug, typed throw).
2172
3982
  toolPolicy: durableEnabled
2173
3983
  ? combinePolicies(createDurableQuestionPolicy(), createDurableAskPolicy({
2174
3984
  requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,
3985
+ // The probe key is the CONTINUED session (auth.sessionId — the same id that keys the
3986
+ // durable checkpoint /decide route). A fresh session (no body.sessionId) has no exemptions by
3987
+ // construction; fail-closed inside the policy on a store error.
2175
3988
  ...(approvalExemptionStore && auth?.sessionId
2176
3989
  ? {
2177
3990
  exempt: (toolName) => approvalExemptionStore.has(auth.sessionId, toolName),
@@ -2186,7 +3999,8 @@ async function main() {
2186
3999
  deny: config.approvalDeny,
2187
4000
  pollMs: config.approvalPollMs,
2188
4001
  context: () => ({ sessionId: auth?.sessionId ?? null, owner: auth?.principal ?? null }),
2189
- neverAuto: config.approvalNeverAuto,
4002
+ neverAuto: config.approvalNeverAuto, // workflow audit 2026-07-13: the ordering lock must hold on the OA leg too
4003
+ // Same probe on the legacy poll gate (sessionId threaded via ctx here).
2190
4004
  ...(approvalExemptionStore
2191
4005
  ? {
2192
4006
  exempt: (sessionId, toolName) => approvalExemptionStore.has(sessionId, toolName),
@@ -2195,8 +4009,21 @@ async function main() {
2195
4009
  : {}),
2196
4010
  })
2197
4011
  : singleUserAutoAcceptBaseline
4012
+ // 🔴 (live finding: single-user host-lane Bash ran UNGATED, no adjudication): a
4013
+ // single-user turnkey worker with NO expressed gate intent previously left `toolPolicy` UNDEFINED →
4014
+ // core's write-capable hand tools mount UNADJUDICATED (core error-logs it every task; host lane has NO
4015
+ // sandbox to fall back on). CC's trust model is auto-accept, but the GATE MECHANISM must be PRESENT
4016
+ // (core 原则: "机制留、默认可更宽 auto-accept"). So wire an adjudicated auto-accept BASELINE
4017
+ // (`createAllowDenyPolicy({})` = a present, effect-aware policy that allows all): satisfies core's
4018
+ // `hasEffectAwareGate`, restores observability + a hook/tighten point, and the operator STILL tightens
4019
+ // irreversible ops via `AUTONOMY` / `commandPolicy` (layered TIGHTEN-ONLY by applyRuntimeGovernance).
4020
+ // Interactive approval routed to the shell HITL = follow-on. 🔴 `singleUserAutoAcceptBaseline` requires
4021
+ // ZERO gate intent — a single-user op who SET approval flags but wired no store falls to `undefined`
4022
+ // (core warns = real misconfig, not masked); multi-tenant likewise stays `undefined` (approval required).
2198
4023
  ? createAllowDenyPolicy({})
2199
4024
  : undefined,
4025
+ // Durable suspend needs both the store (here, per-task) and the opt-in scope key (multi-tenant =
4026
+ // principal; "_" when auth is off). core suspends on a policy `ask` only when these are present.
2200
4027
  ...(durableEnabled
2201
4028
  ? {
2202
4029
  checkpointStore,
@@ -2204,6 +4031,11 @@ async function main() {
2204
4031
  scope: auth?.principal ?? "_",
2205
4032
  ...(config.approvalTimeoutSec > 0 ? { ttlMs: config.approvalTimeoutSec * 1000 } : {}),
2206
4033
  },
4034
+ // design/80 seam #2: opt this task into resource/preempt durable-suspend (design/74 third state)
4035
+ // when RESOURCE_SUSPEND=true — a budget/turns/walltime limit OR a scheduler preempt durably SUSPENDS
4036
+ // (resumable) instead of failing. SAME scope key as durableApproval (the principal). The decision
4037
+ // (incl. the 🔴 verify/cascade EXCLUSION that keeps eligibility aligned with the preemptSignal wiring,
4038
+ // so an inner suspend can't orphan a VM+checkpoint) lives in the pure, unit-tested `resourceSuspendOptIn`.
2207
4039
  ...((rs) => (rs ? { resourceSuspend: rs } : {}))(resourceSuspendOptIn({
2208
4040
  enabled: config.resourceSuspend,
2209
4041
  ttlSec: config.resourceSuspendTtlSec,
@@ -2215,21 +4047,58 @@ async function main() {
2215
4047
  }
2216
4048
  : {}),
2217
4049
  };
4050
+ // Runtime governance "second baton" (center §10): compile the operator's autonomy + commandPolicy onto the
4051
+ // spec TIGHTEN-ONLY. commandPolicy layers via combinePolicies onto the approval/durable baseline above (NOT a
4052
+ // bare overwrite — TRAP #1), autonomy expands to handsReadOnly/shellGate (TRAP #2). Applied here so it also
4053
+ // re-applies identically on the resume paths (which rebuild via resolveSpec) — core requires shellGate/
4054
+ // toolPolicy be re-supplied on resume (omitting shellGate would leave a resumed run's bash UNGATED).
4055
+ // tightenTaskSpec THROWS on a loosening misconfig (fail-loud).
4056
+ //
4057
+ // 🔴 RESUME uses LIVE config (not a per-task frozen snapshot), IDENTICAL to how the approval baseline above
4058
+ // reads live `config.approvalRequire`. So if an operator LOOSENS the fleet governance while a task is
4059
+ // suspended, the resumed task picks up the looser policy (governance stays per-call tighten-only, so a
4060
+ // mid-suspend TIGHTENING is honored; a loosening is an explicit operator action). Freezing deployment policy
4061
+ // per-task across suspend is a larger, separate decision that must cover the approval baseline too — tracked
4062
+ // for center (adversarial-review HIGH-1), deliberately NOT a governance-only partial freeze here (that would
4063
+ // be an inconsistent false-safety: approval would still be live).
2218
4064
  const governedBase = applyRuntimeGovernance(spec, { autonomy: config.autonomy, commandPolicy: config.commandPolicy });
4065
+ // Fold the client's per-request settings stamp onto the governed spec, TIGHTEN-ONLY (deny-wins) — AFTER
4066
+ // the approval baseline + operator governance, so the order is deployment ⊇ operator ⊇ client (each can only
4067
+ // narrow). tightenTaskSpec THROWS if the client settings would LOOSEN a stricter baseline → a 422 client error
4068
+ // (a settings stamp is tighten-only by contract; never a silent weakening). Re-applies idempotently on resume.
2219
4069
  let governed = governedBase;
4070
+ // R4: fold a LIGHT top-level `body.permissionMode` onto the parsed settings (the explicit
4071
+ // per-turn intent WINS over a bundle `defaultMode`) → the SAME tighten-only governance path. Post-[816] all
4072
+ // five CC modes are honored as gate-SHAPE choices (see coercePermissionMode/deriveSettingsPolicy): `plan` ⇒
4073
+ // read-only hands + present_plan; `default`/`auto` ⇒ the fs-write ask gate; `acceptEdits` ⇒ the cwd-domain
4074
+ // variant; `bypassPermissions` ⇒ no mode gate (the deployment baseline is composed above and untouchable).
2220
4075
  const bodyMode = coercePermissionMode(body.permissionMode);
2221
4076
  let effectiveSettings = bodyMode ? withPermissionMode(parsedSettings.settings, bodyMode) : parsedSettings.settings;
4077
+ // codex R3 second bypass: settings.outputStyle ALSO lands in spec.appendSystemPrompt (applyTaskSettings
4078
+ // fold) — on an append-less pack core would discard it silently, exactly like the top-level rider. Same
4079
+ // disposition: fresh submits 400 at the HTTP gate; here (the resume mirror + any leg the gate didn't see)
4080
+ // strip + warn. Spread-copy, never mutate parsedSettings (it may be consulted elsewhere).
2222
4081
  if (appendLessPack && effectiveSettings?.outputStyle) {
2223
4082
  logger.warn("output_style_dropped", { detail: "the effective prompt pack cannot mount the append slot (already-assembled systemPrompt or center assembled-identity declaration)", sessionId: auth?.sessionId ?? null });
2224
4083
  const { outputStyle: _dropped, ...rest } = effectiveSettings;
2225
4084
  effectiveSettings = Object.keys(rest).length > 0 ? rest : undefined;
2226
4085
  }
4086
+ // codex R14 resume mirror of the aggregate cap: both carriers fold into ONE spec field — a pre-cap stored
4087
+ // body can be individually valid but combined over the bound (fresh submits 400'd this in prepareSpec).
4088
+ // Drop the STYLE (it composes second; the knowledge rider keeps priority) + warn, never silently oversize.
2227
4089
  if (effectiveSettings?.outputStyle && acceptedAppend && acceptedAppend.length + 2 + effectiveSettings.outputStyle.length > MAX_SETTINGS_OUTPUT_STYLE_CHARS) {
2228
4090
  logger.warn("output_style_dropped", { detail: `combined append carriers exceed the ${MAX_SETTINGS_OUTPUT_STYLE_CHARS} cap (rider ${acceptedAppend.length} + style ${effectiveSettings.outputStyle.length}) — style dropped (pre-cap stored body on a resume leg?)`, sessionId: auth?.sessionId ?? null });
2229
4091
  const { outputStyle: _dropped2, ...rest2 } = effectiveSettings;
2230
4092
  effectiveSettings = Object.keys(rest2).length > 0 ? rest2 : undefined;
2231
4093
  }
4094
+ // [820]③ scratchpadDir, hoisted from the envFacts block below so the SAME per-session dir feeds BOTH the
4095
+ // `# Environment` fact and the fs-write gate's exemptDirs (single-source path via scratchpadDirFor inside
4096
+ // ensureScratchpadDir). HOST-semantics lanes only + session required — rationale at the envFacts consumer.
2232
4097
  const hostSemanticsLane = config.remoteExec === undefined || config.remoteExec.provider === "host";
4098
+ // G3([816]③/[820]③,[1840]§四):壳供 body.scratchpadDir 优先——壳(CC 形)有自己的 per-session
4099
+ // scratchpad 约定路径,接受后事实/豁免/根围栏三处同源指向壳的目录(壳侧提示词与 server 写门不再
4100
+ // 两个 scratchpad)。验收门 fail-closed(多租户/远程 lane/浅路径全拒收,warn 后回落自算)——
4101
+ // 规则与理由集中在 acceptShellScratchpadDir。
2233
4102
  const shellScratchpad = await acceptShellScratchpadDir(body.scratchpadDir, {
2234
4103
  requirePrincipal: config.requirePrincipal,
2235
4104
  hostSemanticsLane,
@@ -2238,19 +4107,47 @@ async function main() {
2238
4107
  const scratchpadDir = shellScratchpad ??
2239
4108
  (hostSemanticsLane && auth?.sessionId ? await ensureScratchpadDir(config.localDataRoot ?? localRoot, auth.sessionId) : undefined);
2240
4109
  if (effectiveSettings) {
4110
+ // [816]/[820]① fs-write gate wiring — HOST-semantics lanes only: core's createFsWriteGatePolicy
4111
+ // canonicalizes每一个 target/dir 走给定 env 的真实 fs(exists/canonicalPath/readLink,dist 亲读),所以
4112
+ // env 必须就是 hand 工具真正写的那块盘。e2b/k8s/ssh/adb/local-docker 的沙箱 env 由 core 在 spec 之后
4113
+ // 才铸(executionEnvFactory),这里给 worker 本机 env 会拿错误的 fs 裁决(symlink/exists 全答错)——
4114
+ // 沙箱 lane 诚实不挂(gate=undefined ⇒ derive 回落 base 规则,pre-[816] 行为)。
4115
+ // NodeExecutionEnv 构造是纯字段赋值(不 spawn)。
4116
+ //
4117
+ // 修5(三路复审 W4,cwd 基准漂移):gate 的 cwd 基准=与 hands 真跑处同源,分三形:
4118
+ // · session 注册过 cwd(cwdHonored 已在注册处把关)⇒ 用它——与 host factory 的 effectiveHostWorkspace
4119
+ // 第一优先级同源(顶层 HTTP 任务无 parentCwd,resolver 即 session cwd)。
4120
+ // · `host` factory lane 且未注册 ⇒ hands 落在 factory 才铸的 EPHEMERAL per-task 目录(spec 期不可知)。
4121
+ // 此前回退 process.cwd()(服务自身启动目录)——acceptEdits 会把服务目录当 auto-allow 域、多任务共享
4122
+ // 进程时判定漂移(W4)。改为 fail-safe 哨兵:一个从不创建的目录 ⇒ acceptDirs/相对路径 canonicalize
4123
+ // 失败即 ask(fs-write-gate-policy dist 亲读:dir.ok=false 跳过、target 解析失败=ask);绝对路径裁决
4124
+ // (hand 工具的书面契约形)与 scratchpad exempt 均不受影响。
4125
+ // · provider 未设(in-process host / run-local)⇒ hands 就在本进程 cwd 跑(无 factory),process.cwd()
4126
+ // 正是真工作目录,保留。
2241
4127
  const fsWriteGate = hostSemanticsLane
2242
4128
  ? (() => {
2243
4129
  const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
2244
4130
  const gateCwd = sessionCwd ??
2245
4131
  (config.remoteExec === undefined
2246
4132
  ? process.cwd()
2247
- : join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted"));
4133
+ : join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted")); // never created — see above
2248
4134
  return {
2249
4135
  env: new NodeExecutionEnv({ cwd: gateCwd }),
2250
4136
  cwd: gateCwd,
2251
4137
  ...(scratchpadDir ? { scratchpadDir } : {}),
4138
+ // ③ (core 1.295) sensitive-path write deny set:组合进 gate 腿的同一 combinePolicies 折叠
4139
+ // (deny 恒赢 —— 豁免/acceptDirs 越不过;集合取舍 core 成文,server 只透传 config 旋钮:
4140
+ // 缺省 = core RECOMMENDED_SENSITIVE_PATTERNS,SENSITIVE_WRITE_PATTERNS 显式替换/off 关闭)。
2252
4141
  ...(config.sensitiveWritePatterns.length > 0 ? { sensitivePatterns: config.sensitiveWritePatterns } : {}),
4142
+ // [1557]§四 opt-in(cli[1555]②「Bash echo > file 绕写门」缺口的部署侧补丁):MANUAL_MODE_SHELL_GATE
4143
+ // 未设置时 config.manualModeShellGate 缺席,本行不传字段——deriveSettingsPolicy 的 default/
4144
+ // auto/acceptEdits 分支照旧不碰 TaskSpec.shellGate,零行为变化。
2253
4145
  ...(config.manualModeShellGate ? { shellGate: config.manualModeShellGate } : {}),
4146
+ // [841]① / core 1.294 exemption seam:gate 产 ask 前查同一只 approval_exemption 店(与 decide
4147
+ // remember="session" 的授予、ask 政策层探针同店同 canonical toolName 键空间)——「本会话不再询问」
4148
+ // 对 fs-write 门同 turn 即时生效(父+继承子任务,ask 路径不再重入)。has() reject ⇒ core 按
4149
+ // 未豁免处理(fail-closed 照 ask),与店契约的调用方纪律一致;命中打 info 审计(对齐
4150
+ // approval_exempted 的短路留痕,不记路径,路径留在 core 的 allow message 里)。
2254
4151
  ...(approvalExemptionStore && auth?.sessionId
2255
4152
  ? {
2256
4153
  isExempt: async (toolName) => {
@@ -2264,6 +4161,10 @@ async function main() {
2264
4161
  };
2265
4162
  })()
2266
4163
  : undefined;
4164
+ // [1248]②/codex F2 — the workflow ask leg's session-exemption probe, on EVERY lane (unlike fsWriteGate:
4165
+ // the workflow gate is name-keyed, not fs-adjudicated). Same store + canonical toolName key space as the
4166
+ // fs-gate probe — one remember="session" grant serves both; without it the durable lane re-parks every
4167
+ // later run_workflow call after an operator already granted "don't ask again this session".
2267
4168
  const workflowGate = approvalExemptionStore && auth?.sessionId
2268
4169
  ? {
2269
4170
  isExempt: async (toolName) => {
@@ -2281,12 +4182,36 @@ async function main() {
2281
4182
  throw new HttpError(422, `settings are tighten-only and cannot loosen the deployment policy: ${e.message}`);
2282
4183
  }
2283
4184
  }
4185
+ // subagent-hands follow-on (adversarial-review finding): core does NOT propagate
4186
+ // handsReadOnly/shellGate to a delegated subagent (SubagentToolOptions has NO read-only knob; subagent.js's child
4187
+ // runTask omits it). So a read-only (permissionMode=plan) parent that mounts the Task tool would let the CHILD get
4188
+ // WRITABLE hands — now reachable because subRunner has an executionEnvFactory (the hands fix). That defeats plan
4189
+ // mode's read-only contract (a within-tenant gap, not a cross-tenant breach). Until core propagates it (relayed),
4190
+ // strip the delegation tools when this task is hands-read-only — a read-only run delegates nothing writable.
4191
+ // `stripDelegationTools` (runtime-governance.ts, tested there) matches the subagent tool ("Agent", core 1.202
4192
+ // canonical; legacy alias "Task") + Fork in CANONICAL space — the pre-1.202 raw `!== "Task"` filter here
4193
+ // silently stopped matching when core renamed the tool (fail-open: a plan-mode parent could delegate a
4194
+ // WRITABLE-hands child).
2284
4195
  if (governed.handsReadOnly === true && Array.isArray(governed.tools)) {
2285
4196
  governed = { ...governed, tools: stripDelegationTools(governed.tools) };
2286
4197
  }
4198
+ // §7 P0.5 per-task sandbox image(用户按需选像): the caller sends a PROFILE
4199
+ // (intent) — NEVER a digest (a sha256 is enumerable + caller-unbound → trusting a caller digest is fail-OPEN;
4200
+ // a user could pass another tenant's digest). The TRUSTED control plane resolves profile→digest with the
4201
+ // caller's principal, FAIL-CLOSED (same visibility the /v1/images/select admission uses), and registers it by
4202
+ // sessionId for the k8s factory to apply per-pod. RESUME re-runs resolveSpec ⇒ re-resolves LIVE (no stale
4203
+ // frozen digest), consistent with the governance live-config discipline above.
4204
+ // RFC A2: a session-level SelectEnvironment binding (PROFILE intent, tool-written) is the fallback when the
4205
+ // request body carries no explicit profile. Body wins (an explicit caller choice for THIS task); either way
4206
+ // the profile is re-resolved FAIL-CLOSED right here — a session binding never skips re-admission, and the
4207
+ // F3 boundary contract holds structurally (binding is only read at task boundaries, children inherit
4208
+ // the registered ref at spawn).
2287
4209
  const bodyProfile = typeof body.sandboxImageProfile === "string" && body.sandboxImageProfile.length > 0 ? body.sandboxImageProfile : undefined;
4210
+ // cascade/verify mirror the HTTP-layer v1 reject (server.ts): those flows strip/replace the session, so a
4211
+ // session-level binding would not reach the rungs/verifier sub-run — silently mixed environments. A body
4212
+ // profile on cascade/verify is already 400-rejected up front; the session fallback simply does not apply.
2288
4213
  const taskImageProfile = bodyProfile ?? (body.cascade === true || body.verify === true ? undefined : sessionEnvSelection.get(auth?.sessionId));
2289
- let resolvedImageCaps;
4214
+ let resolvedImageCaps; // RFC A1: rides into envFacts
2290
4215
  if (taskImageProfile !== undefined) {
2291
4216
  if (config.remoteExec?.provider !== "k8s") {
2292
4217
  throw new HttpError(400, "sandboxImageProfile is only supported on the k8s sandbox backend");
@@ -2303,16 +4228,54 @@ async function main() {
2303
4228
  const resolved = await resolveSandboxImageRef({
2304
4229
  profile: taskImageProfile,
2305
4230
  ...(capsNeeded && capsNeeded.length > 0 ? { capabilitiesNeeded: capsNeeded } : {}),
4231
+ // principal comes from the auth channel (trusted header / verified JWT), NEVER the body — so a caller
4232
+ // cannot widen its own visibility. explicitOperator (NOT isOperator): an empty OPERATOR_PRINCIPALS must
4233
+ // yield operator=false here — isOperator([],p)=true-for-all would let any caller resolve tenant-scoped
4234
+ // images on an operator-less deployment (adversarial-review HIGH-1; the bake/direct-door boot guards do
4235
+ // NOT cover per-task image selection).
2306
4236
  viewer: { operator: explicitOperator(auth.principal, config.operatorPrincipals), tenantId: auth.principal ?? null },
2307
4237
  index: imageIndex,
2308
4238
  });
2309
4239
  if (!resolved.ok)
2310
4240
  throw new HttpError(resolved.status, resolved.message);
4241
+ // Register by sessionId — the factory reads it by ctx.sessionId (stable on every path; see the factory
4242
+ // wrapper). RESUME re-runs resolveSpec ⇒ re-resolves LIVE + re-registers (no stale frozen digest).
4243
+ // ⚠️ KNOWN narrow edge (NOT a security breach): two CONCURRENT requests for the SAME session (same owner —
4244
+ // session ownership is enforced) race this set(); a request the run-claim later rejects (409) can leave its
4245
+ // image bound for the active run. Both images are the SAME principal's own admitted images, so the worst
4246
+ // case is the user's run using the user's other selected image (a correctness glitch, not cross-tenant).
4247
+ // Hardening (register-after-claim, keyed by the durable run id) needs the registry threaded into the server
4248
+ // claim path — tracked as a follow-up; cascade/verify are rejected with a profile (see prepareSpec) because
4249
+ // they strip/replace the session and would silently fall back to the default image.
2311
4250
  perTaskImage.set(auth.sessionId, resolved.ref);
2312
4251
  resolvedImageCaps = resolved.capabilities;
2313
4252
  }
4253
+ // RFC A1 (core 1.240.0 `TaskSpec.envFacts`): compose the deployment-trusted sandbox facts
4254
+ // (profile/capabilities from the fail-closed resolution above, region pkgSource, exec-lane egress posture)
4255
+ // and ride them on the spec — core renders the `# Environment` block (sanitized+bounded, egress=none gets
4256
+ // the "downloads will fail" caveat). Facts only ride when known; nothing known ⇒ no field ⇒ block unchanged.
2314
4257
  if (config.envFactsEnabled) {
4258
+ // pkgSource is a FACT only where the derivePkgSourceEnv injection actually lands (the e2b/k8s sandbox
4259
+ // lanes — main.ts wiring above). host/ssh/adb/run-local get NO injection, so with the global default
4260
+ // (1.180.0) an unconditional read would tell the model "official sources" about an environment we never
4261
+ // touched (workflow audit 2026-07-13: honest-facts posture, same as egress's "don't declare what you
4262
+ // don't know").
2315
4263
  const pkgSourceLane = config.remoteExec?.provider === "e2b" || config.remoteExec?.provider === "k8s";
4264
+ // [820]③ scratchpadDir (core field + prompt chain in since 1.257.3; this fill was the missing half).
4265
+ // HOST-semantics lanes only (provider unset = in-process host, or explicit "host"): the advertised path
4266
+ // must be REAL to the executing hands — on e2b/k8s/ssh/adb/local-docker the tools run off this box, so a
4267
+ // worker-local path would be a lie (same honest-facts axis as pkgSource above; the remote lanes get their
4268
+ // IN-SANDBOX path via the [848] branch below instead). Session-scoped dir under
4269
+ // localDataRoot, mkdir'd at the HOISTED compute above the settings fold (fail → fact omitted) — the SAME
4270
+ // dir now feeds createFsWriteGatePolicy's exemptDirs ([820]① landed at core 1.290, wired in the
4271
+ // applyTaskSettings gate above). No sessionId (adhoc no-session task) ⇒ no per-session home ⇒ fact
4272
+ // omitted. Lifecycle/reaping: see ensureScratchpadDir docs (deferred).
4273
+ // [848] remote lanes' counterpart: on e2b/k8s/local-docker/ssh the withRemoteScratchpad-decorated
4274
+ // factory `mkdir -p`s exactly this path on the env's first exec, so advertising it is honest — with
4275
+ // the one best-effort caveat (a failed mkdir leaves the advertised path absent; see the decorator
4276
+ // header). Mutually exclusive with the host-lane fact above by construction (hostSemanticsLane owns
4277
+ // scratchpadDir; this branch is remote lanes only), and it applies the decorator's OWN validity rule
4278
+ // (remoteScratchpadDirFor: absent/unsafe sessionId ⇒ no decoration ⇒ no fact). adb stays factless.
2316
4279
  const remoteScratchpadDir = !hostSemanticsLane && isRemoteScratchpadLane(config.remoteExec?.provider)
2317
4280
  ? remoteScratchpadDirFor(auth?.sessionId)
2318
4281
  : undefined;
@@ -2323,17 +4286,36 @@ async function main() {
2323
4286
  ...(pkgSourceLane ? { pkgSource: config.sandboxPkgSource } : {}),
2324
4287
  egress: egressForRemoteExec(config.remoteExec),
2325
4288
  ...(factScratchpadDir ? { scratchpadDir: factScratchpadDir } : {}),
4289
+ // [1467]②(core 1.360)durable resume 铸句真值:按车道能力面,单真源 resumeFactsForLane
4290
+ // (e2b=双 preserved/k8s=按 s3Snapshot 有效能力分叉/ssh·adb·host=scratch preserved/
4291
+ // local-docker=不声明)。
2326
4292
  resumeFacts: resumeFactsForLane(config.remoteExec?.provider, config.remoteExec?.provider === "k8s" ? { k8sSnapshot: Boolean(config.remoteExec.s3Snapshot) } : undefined),
2327
4293
  });
2328
4294
  if (facts)
2329
4295
  governed = { ...governed, envFacts: facts };
2330
4296
  }
4297
+ // [1339] scratchpad 写门失效真因:提示词(envFacts.scratchpadDir)和 policy 层豁免(exemptDirs)都
4298
+ // 接了,但 core fs 工具的**根围栏**(resolveKey:taskRootPath+additionalDirectories)在 policy 之前
4299
+ // 短路——Write/Edit 到 scratchpad 直接 path_not_in_root,exemptDirs 根本没被问到(Bash 无根围栏,
4300
+ // 反而能写=口径矛盾还教模型绕门)。修:host 车道把同一 scratchpadDir 折进 spec.additionalDirectories
4301
+ // (server 自算的 per-session 路径,非租户输入,不走 cwdHonored 门限;compute 时已 mkdir,core 的
4302
+ // canonicalPath 必过)。remote 车道暂不折:in-sandbox 路径由 [848] 装饰器首 exec 才 mkdir,prepare 期
4303
+ // canonicalize 会失败被 core 静默跳过——那半场随 core 排序案另行。
4304
+ // codex R3(M2):**独立于 envFactsEnabled 门**——SANDBOX_ENV_FACTS=false 只关事实展示,写门围栏
4305
+ // 豁免是行为契约,关展示不得顺带关围栏(否则该配置下 [1339] 原病复发)。
2331
4306
  if (scratchpadDir !== undefined) {
2332
4307
  const dirs = governed.additionalDirectories ?? [];
2333
4308
  if (!dirs.includes(scratchpadDir))
2334
4309
  governed = { ...governed, additionalDirectories: [...dirs, scratchpadDir] };
2335
4310
  }
4311
+ // A value router (S1 verdict §6.3 cash-out, src/router/route-orchestration.ts): auto-decide this task's
4312
+ // orchestration. CONSERVATIVE + DETERMINISTIC — default single; escalate to the SUP prevention posture only
4313
+ // on a capability-danger signal (write/exec on a NON-isolated env — an isolated sandbox CONTAINS the harm,
4314
+ // so isolated tasks stay single, matching the verdict's default). caller-explicit team (council/debate/team)
4315
+ // is honored, never auto-produced. Flag-gated (ROUTER_ENABLED) — ships dark, enable after live validation.
2336
4316
  if (config.routerEnabled) {
4317
+ // Resolve isolation from provider AND runtimeClass — a k8s worker on plain runc is NOT VM-isolated
4318
+ // (review HIGH); fail-closed for runc/empty/unknown.
2337
4319
  const isolatedExecEnv = isIsolatedExecEnv(config.remoteExec?.provider, config.remoteExec?.provider === "k8s" ? config.remoteExec.runtimeClass : undefined);
2338
4320
  const decision = routeServiceTask({
2339
4321
  isolatedExecEnv,
@@ -2343,13 +4325,20 @@ async function main() {
2343
4325
  });
2344
4326
  logger.info("orchestration_routed", { mode: decision.mode, reason: decision.reason, sessionId: auth?.sessionId ?? null });
2345
4327
  if (decision.mode === "supervisor") {
4328
+ // SUP prevention posture: gate every bash (shellGate:"always") AND the direct write hand tools
4329
+ // (edit_file/write_file via toolPolicy) — the verdict's gate-deny on capability-danger. tighten-only
4330
+ // (composes with the governance/approval baseline; never loosens). The worker's existing approval
4331
+ // machinery (durable/poll) enforces the suspend — same precondition as autonomy "ask".
2346
4332
  return tightenTaskSpec(governed, supPostureOverrides());
2347
4333
  }
2348
4334
  }
2349
4335
  return governed;
2350
4336
  };
2351
4337
  const server = createHttpServer({ runner, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
4338
+ // D-D SLA-timer: wire the server's deny-sweep into the reaper holder declared above (the reaper is defined
4339
+ // before the server, so it calls through this late-bound reference).
2352
4340
  runDenySweep = server.denyExpiredApprovals;
4341
+ // [1934]:绑址旋钮 + 无鉴权自收窄(理由见 resolveBindHost 顶注)。undefined = Node 默认全接口。
2353
4342
  const bindHost = resolveBindHost(config);
2354
4343
  await new Promise((resolve) => (bindHost ? server.listen(config.port, bindHost, resolve) : server.listen(config.port, resolve)));
2355
4344
  logger.info("listening", {
@@ -2359,6 +4348,9 @@ async function main() {
2359
4348
  ? { note: "auto-narrowed to loopback: write face is unauthenticated (ALLOW_UNAUTHED_WRITES with no service token). Set BIND_HOST explicitly to override." }
2360
4349
  : {}),
2361
4350
  });
4351
+ // fleet worker 接线(announce/heartbeat + usage 批报;lease 消费后一拍):boot 完成后
4352
+ // 注册(=首次心跳)。门 = center lane 配置 ∧ FLEET_ADVERTISE_ADDRESS 显式声明(不自猜可达地址);任一缺 → undefined
4353
+ // = 非 fleet 部署零行为。draining 翻转的即刻 announce 在 drainThenShutdown;退出 DELETE 在 hardShutdown。
2362
4354
  const fleetClient = startFleetClientFromEnv(config, {
2363
4355
  instanceId,
2364
4356
  version: serviceVersion(),
@@ -2372,27 +4364,39 @@ async function main() {
2372
4364
  port: config.port,
2373
4365
  model: config.model.id,
2374
4366
  brain: brainSummary(config),
2375
- breakerState: config.resilience.circuitBreaker ? (breakerState ? `shared(${backend.kind})` : "in-process") : "off",
4367
+ // shared = cross-replica TiDB breaker state; in-process = core's per-replica Map; off = breaker disabled
4368
+ breakerState: config.resilience.circuitBreaker ? (breakerState ? `shared(${backend.kind})` : "in-process") : "off", // 1.108: was hard-coded "shared(tidb)" — lied under DB_BACKEND=pg
4369
+ // On DB_BACKEND=local the wiring coerces sessionBackend to the LABEL "tidb" (meaning "the
4370
+ // backend's durable store, generically") — printing it verbatim read as a SQL deployment on a file-everything
4371
+ // box. Print the effective store instead (same honesty fix as breakerState above).
2376
4372
  session: config.sessionBackend === "tidb" && backend ? `durable(${backend.kind})` : config.sessionBackend,
4373
+ // design/138 S1: the file-based memory engine (single-user only); "off" = MEMORY_ENGINE=off or multi-tenant.
2377
4374
  memory: memoryEngine ? `engine(${memoryEngine.root})` : "off",
2378
4375
  instanceId,
2379
4376
  rateLimitPerMin: config.rateLimitPerMin,
2380
4377
  rateLimit: config.rateLimitPerMin > 0 ? (backend ? `shared()` : "in-process") : "off",
2381
4378
  maxPrincipalCostUsd: config.maxPrincipalCostUsd || null,
2382
4379
  costQuota: config.maxPrincipalCostUsd > 0 ? (backend ? `shared()` : "in-process") : "off",
4380
+ // durable offload store for large tool results (cross-replica wake); else core's in-process default
2383
4381
  toolResultStore: toolResultStore ? (backend?.kind === "local" ? "file(local)" : "shared(sql)") : "in-process",
2384
4382
  otel: config.otel ? config.otel.endpoint : null,
2385
- perModelKeys: Object.keys(config.modelApiKeyEnv).length,
4383
+ perModelKeys: Object.keys(config.modelApiKeyEnv).length, // models with their own upstream key (else gateway)
2386
4384
  sealedModelKeys: Object.values(config.modelApiKeys).filter((v) => typeof v === "string").length,
2387
- sealedPoisonedModels: Object.values(config.modelApiKeys).filter((v) => typeof v !== "string").length,
4385
+ sealedPoisonedModels: Object.values(config.modelApiKeys).filter((v) => typeof v !== "string").length, // sealed-box custody keys unsealed (count only — values never log)
4386
+ // SVC-1: durable workflow record + at-least-once notify backend (file = crash-safe ledger; memory = ephemeral)
2388
4387
  workflowRunStore: config.selfOrchestrationEnabled
2389
4388
  ? config.workflowRunStoreBackend === "memory"
2390
4389
  ? "in-process"
2391
4390
  : sqlWorkflowRunStore
2392
- ? `db(${backend.kind})`
4391
+ ? `db(${backend.kind})` // P1: cross-replica twin active
2393
4392
  : "file"
2394
4393
  : "off",
2395
4394
  });
4395
+ // SVC-1 at-least-once notify — CRASH-RECOVERY sweep: once at boot, re-derive every un-acked (started-but-not-
4396
+ // delivered) workflow's terminal state from the durable run store and re-deliver the completion notify that a
4397
+ // prior replica crash dropped (idempotent on the receiver). Fire-and-forget AFTER listen so startup isn't
4398
+ // blocked on it (recovery only re-fires completion notifications — independent of serving new requests). A
4399
+ // sweep throw is logged, never fatal.
2396
4400
  if (workflowNotifyGate) {
2397
4401
  void workflowNotifyGate
2398
4402
  .recover({ orphanGraceMs: config.workflowOrphanGraceMs })
@@ -2408,13 +4412,20 @@ async function main() {
2408
4412
  otelExporter?.stop();
2409
4413
  breakerState?.stop();
2410
4414
  if (costQuota && "stop" in costQuota)
2411
- costQuota.stop();
4415
+ costQuota.stop(); // the DB-backed quota/limiter (TiDB|PG) have a refresh loop to stop; the in-memory ones don't
2412
4416
  if (rateLimiter && "stop" in rateLimiter)
2413
4417
  rateLimiter.stop();
2414
4418
  runner.sessions.dispose();
2415
4419
  subRunner.sessions.dispose();
4420
+ // TOC local LSP: NodeLspManager holds LOCAL child-process language servers — kill them on shutdown so a
4421
+ // restart doesn't orphan a fleet of stdio servers. dispose() isn't on the LspServerManager interface (only the
4422
+ // concrete managers have it), so probe it; the e2b/k8s bridge manager is a harmless no-op if it lacks one.
2416
4423
  void lspManager?.dispose?.();
4424
+ // SVC-1: release the File journal's append handle; the SQL twins hold no fd (probe — close isn't on the seam).
2417
4425
  void workflowNotifyJournal?.close?.();
4426
+ // core 1.270.1:hardShutdown 终点(drain 已完,in-flight 不再被破坏)全量收割 session 驻留
4427
+ // bg bash(detached 进程组,引擎死后 reparent PID 1 残留=实测;retain-declared 例外在原语内)。
4428
+ // TOB 容器形态同样调=无害且显式;SIGKILL 路径无钩=已知不可救记档。
2418
4429
  try {
2419
4430
  const reaped = defaultTaskRegistry.reapAllSessionBackground();
2420
4431
  if (reaped > 0)
@@ -2423,16 +4434,30 @@ async function main() {
2423
4434
  catch (err) {
2424
4435
  logger.warn("shutdown_background_reap_failed", { err: String(err) });
2425
4436
  }
2426
- void fleetClient?.stop();
4437
+ void fleetClient?.stop(); // stop heartbeat + flush final usage window + best-effort DELETE deregistration
2427
4438
  server.close(() => {
2428
4439
  void (backend ? backend.close() : Promise.resolve()).finally(() => process.exit(0));
2429
4440
  });
4441
+ // 对抗复查 B-4(HIGH):the exit above only fires after backend.close() SETTLES — a wedged pool teardown
4442
+ // (dead DB, hung socket) kept the "force-terminated" process alive indefinitely (the 10s closeAllConnections
4443
+ // below unblocks server.close(), but nothing bounded backend.close). FINAL deadline: whatever is still
4444
+ // holding after 15s, exit anyway (durable state is already flushed by then — checkpoint/terminal writes
4445
+ // happen before hardShutdown; the pool teardown is best-effort cleanup, not correctness).
2430
4446
  const finalExit = setTimeout(() => { logger.warn("hard_shutdown_final_deadline", { afterMs: 15_000 }); process.exit(0); }, 15_000);
2431
4447
  finalExit.unref?.();
4448
+ // `server.close()` waits for every connection to end — an open SSE stream (/v1/tasks/stream, /events) would
4449
+ // otherwise block it forever, so pool.end()/exit never run and the process hangs until SIGKILL. Drop idle
4450
+ // keep-alives now, then force-terminate any lingering connections after a grace period so `close()` can
4451
+ // resolve and shutdown completes. The timer is unref'd but still fires while the server holds connections.
2432
4452
  server.closeIdleConnections?.();
2433
4453
  const forceClose = setTimeout(() => server.closeAllConnections?.(), 10_000);
2434
4454
  forceClose.unref?.();
2435
4455
  };
4456
+ // SIGTERM = graceful DRAIN — flip `draining` (new billable submits 503+Retry-After, /health carries
4457
+ // draining:true for readiness摘流), then wait for this instance's in-flight legs (live streams + bg/resume) to
4458
+ // finish before the hard shutdown. Bounded by DRAIN_GRACE_MS (default 10min — an interactive turn is minutes;
4459
+ // the previous behavior was a 10s hard-cut that killed long turns). A SECOND SIGTERM or SIGINT (dev Ctrl-C)
4460
+ // skips the wait — k8s sends SIGKILL after terminationGracePeriodSeconds regardless, so the escape hatch is free.
2436
4461
  let draining = false;
2437
4462
  const drainThenShutdown = () => {
2438
4463
  if (closing)
@@ -2445,14 +4470,17 @@ async function main() {
2445
4470
  draining = true;
2446
4471
  drainState.draining = true;
2447
4472
  drainState.since = Date.now();
2448
- void fleetClient?.announceNow();
4473
+ void fleetClient?.announceNow(); // draining 翻转即刻再 announce — 第一时间自摘流,比任何观测都快
2449
4474
  const inflight = drainState.inflight?.() ?? 0;
2450
4475
  logger.info("draining_started", { inflight, graceMs: config.drainGraceMs });
2451
4476
  if (inflight === 0) {
2452
4477
  hardShutdown();
2453
4478
  return;
2454
4479
  }
4480
+ // 修3(三路复审 B3-2 时钟):drain elapsed/截止计算走 performance.now() 单调域(挂钟跳变不得吞掉/拉长
4481
+ // drainGraceMs 窗);对外报告的 drainState.since(/health drainingSince)保留 epoch(Date.now)。
2455
4482
  const t0 = performance.now();
4483
+ // NOT unref'd on purpose — this timer IS the shutdown driver (the open connections keep the loop alive anyway).
2456
4484
  const tick = setInterval(() => {
2457
4485
  const n = drainState.inflight?.() ?? 0;
2458
4486
  const elapsed = Math.round(performance.now() - t0);
@@ -2465,8 +4493,16 @@ async function main() {
2465
4493
  };
2466
4494
  process.on("SIGTERM", drainThenShutdown);
2467
4495
  process.on("SIGINT", hardShutdown);
4496
+ // SIGHUP(壳窗口关闭把信号打到整个进程组)≠「没人在用这只引擎」:共享引擎形态(同 config 多壳会话,
4497
+ // 引擎按 T11 防孤儿拍骑在首壳进程组里)下 peer 会话还活着——无条件 drain 会把 peer mid-turn 斩掉(499)
4498
+ // 且窗内新提交 503(B3,壳侧 [771]② 取证)。改 hup-pending 空闲窗:不翻 draining(peer 零感知),
4499
+ // inflight()(bg/resume legs + live streams)连续 sighupIdleGraceMs 为 0 才 hardShutdown,任何在飞 leg
4500
+ // 重置窗口。真孤儿(最后一窗关掉)有界自灭=T11 保留;第二个 SIGHUP 升级走既有 drain 路径(硬梯子)。
4501
+ // session 驻留 bash 的全量收割仍挂 hardShutdown 终点(reapAllSessionBackground)。
2468
4502
  process.on("SIGHUP", createSighupIdleHandler({
2469
4503
  inflight: () => drainState.inflight?.() ?? 0,
4504
+ // ⚠️ 时基契约(三路复审修3):lastActivityAt 由 server.ts 以 performance.now() 单调域打点,与
4505
+ // sighup-idle.ts 内部的 `now` seam(缺省 performance.now)同域——不得混入 Date.now 值。
2470
4506
  lastActivityAt: () => drainState.lastActivityAt?.() ?? 0,
2471
4507
  isStopped: () => closing || draining,
2472
4508
  escalate: drainThenShutdown,