@sema-agent/server 1.323.0 → 2.0.1

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 (565) 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/budget-tracing.d.ts +48 -0
  46. package/dist/boot/budget-tracing.js +86 -0
  47. package/dist/boot/config-center.d.ts +62 -0
  48. package/dist/boot/config-center.js +995 -0
  49. package/dist/boot/coordinators.d.ts +33 -0
  50. package/dist/boot/coordinators.js +97 -0
  51. package/dist/boot/execution-env.d.ts +26 -0
  52. package/dist/boot/execution-env.js +370 -0
  53. package/dist/boot/leader.d.ts +27 -0
  54. package/dist/boot/leader.js +81 -0
  55. package/dist/boot/reapers.d.ts +53 -0
  56. package/dist/boot/reapers.js +252 -0
  57. package/dist/boot/resolve-spec.d.ts +70 -0
  58. package/dist/boot/resolve-spec.js +1072 -0
  59. package/dist/boot/runner-deps.d.ts +101 -0
  60. package/dist/boot/runner-deps.js +343 -0
  61. package/dist/boot/runtime-caps.d.ts +21 -0
  62. package/dist/boot/runtime-caps.js +62 -0
  63. package/dist/boot/session-faces.d.ts +57 -0
  64. package/dist/boot/session-faces.js +157 -0
  65. package/dist/boot/shutdown.d.ts +50 -0
  66. package/dist/boot/shutdown.js +129 -0
  67. package/dist/boot/stores.d.ts +32 -0
  68. package/dist/boot/stores.js +361 -0
  69. package/dist/boot/workflow-orchestration.d.ts +46 -0
  70. package/dist/boot/workflow-orchestration.js +150 -0
  71. package/dist/boot-reclaim.d.ts +19 -0
  72. package/dist/boot-reclaim.js +20 -0
  73. package/dist/brain.d.ts +25 -0
  74. package/dist/brain.js +61 -1
  75. package/dist/budget.d.ts +66 -0
  76. package/dist/budget.js +128 -4
  77. package/dist/capabilities/builtin-tools.d.ts +3 -0
  78. package/dist/capabilities/builtin-tools.js +3 -0
  79. package/dist/capabilities/center-plugins.d.ts +17 -1
  80. package/dist/capabilities/center-plugins.js +45 -4
  81. package/dist/capabilities/center-prompts.d.ts +46 -0
  82. package/dist/capabilities/center-prompts.js +57 -3
  83. package/dist/capabilities/code-review-council.d.ts +28 -0
  84. package/dist/capabilities/code-review-council.js +40 -7
  85. package/dist/capabilities/collab-wire.d.ts +2 -0
  86. package/dist/capabilities/collab-workflows.d.ts +7 -0
  87. package/dist/capabilities/collab-workflows.js +44 -1
  88. package/dist/capabilities/oa-tools.d.ts +6 -0
  89. package/dist/capabilities/oa-tools.js +10 -0
  90. package/dist/capabilities/prompt.d.ts +14 -0
  91. package/dist/capabilities/prompt.js +14 -0
  92. package/dist/capabilities/prompts/code-review.d.ts +14 -0
  93. package/dist/capabilities/prompts/code-review.js +14 -0
  94. package/dist/capabilities/prompts/identity.d.ts +6 -0
  95. package/dist/capabilities/prompts/identity.js +6 -0
  96. package/dist/capabilities/prompts/team.d.ts +4 -0
  97. package/dist/capabilities/prompts/team.js +4 -0
  98. package/dist/capabilities/repo-tools.d.ts +11 -0
  99. package/dist/capabilities/repo-tools.js +8 -0
  100. package/dist/capabilities/sandbox-file-send.d.ts +95 -0
  101. package/dist/capabilities/sandbox-file-send.js +91 -3
  102. package/dist/capabilities/scenarios.d.ts +130 -0
  103. package/dist/capabilities/scenarios.js +180 -7
  104. package/dist/capabilities/select-environment-tool.d.ts +7 -0
  105. package/dist/capabilities/select-environment-tool.js +30 -0
  106. package/dist/capabilities/send-user-file-tool.d.ts +33 -0
  107. package/dist/capabilities/send-user-file-tool.js +65 -1
  108. package/dist/capabilities/skills.d.ts +29 -0
  109. package/dist/capabilities/skills.js +13 -2
  110. package/dist/capabilities/team.d.ts +22 -0
  111. package/dist/capabilities/team.js +22 -2
  112. package/dist/capabilities/tool-defer.d.ts +26 -0
  113. package/dist/capabilities/tool-defer.js +3 -0
  114. package/dist/config-center/apply-effective.d.ts +65 -0
  115. package/dist/config-center/apply-effective.js +204 -12
  116. package/dist/config-center/http-client.d.ts +34 -0
  117. package/dist/config-center/http-client.js +68 -0
  118. package/dist/config-center/restart-signal.d.ts +22 -0
  119. package/dist/config-center/restart-signal.js +39 -0
  120. package/dist/config-center/skills-mcp.d.ts +20 -1
  121. package/dist/config-center/skills-mcp.js +42 -7
  122. package/dist/config-center/types.d.ts +92 -1
  123. package/dist/config-lkg.d.ts +12 -0
  124. package/dist/config-lkg.js +41 -0
  125. package/dist/config-provider.d.ts +78 -0
  126. package/dist/config-provider.js +165 -2
  127. package/dist/config-types.d.ts +585 -1
  128. package/dist/config.d.ts +78 -2
  129. package/dist/config.js +874 -301
  130. package/dist/degenerate-instrument.d.ts +38 -0
  131. package/dist/degenerate-instrument.js +55 -3
  132. package/dist/elicitation.d.ts +39 -0
  133. package/dist/elicitation.js +87 -3
  134. package/dist/env-facts.d.ts +61 -0
  135. package/dist/env-facts.js +73 -7
  136. package/dist/fleet/fleet-bus.d.ts +185 -0
  137. package/dist/fleet/fleet-bus.js +333 -27
  138. package/dist/fleet/subagent-tail-bus.d.ts +11 -0
  139. package/dist/fleet/subagent-tail-bus.js +33 -2
  140. package/dist/fleet-client.d.ts +38 -0
  141. package/dist/fleet-client.js +44 -9
  142. package/dist/fleet-lease.d.ts +46 -0
  143. package/dist/fleet-lease.js +73 -5
  144. package/dist/hooks/branch-transcript.d.ts +8 -0
  145. package/dist/hooks/branch-transcript.js +33 -0
  146. package/dist/hooks/cc-stop-prompt.d.ts +42 -0
  147. package/dist/hooks/cc-stop-prompt.js +43 -1
  148. package/dist/hooks/hook-llm.d.ts +15 -0
  149. package/dist/hooks/hook-llm.js +47 -0
  150. package/dist/hooks/hook-runner.d.ts +89 -0
  151. package/dist/hooks/hook-runner.js +327 -18
  152. package/dist/http/idempotency.d.ts +31 -0
  153. package/dist/http/idempotency.js +34 -0
  154. package/dist/http/principal-gate.d.ts +17 -0
  155. package/dist/http/principal-gate.js +35 -3
  156. package/dist/http/route-ctx.d.ts +36 -0
  157. package/dist/http/routes/approvals-assistant.d.ts +25 -0
  158. package/dist/http/routes/approvals-assistant.js +253 -23
  159. package/dist/http/routes/attachments.js +22 -4
  160. package/dist/http/routes/capabilities.d.ts +7 -0
  161. package/dist/http/routes/capabilities.js +202 -1
  162. package/dist/http/routes/fleet.d.ts +6 -0
  163. package/dist/http/routes/fleet.js +94 -5
  164. package/dist/http/routes/images.d.ts +17 -0
  165. package/dist/http/routes/images.js +222 -14
  166. package/dist/http/routes/leader.d.ts +7 -0
  167. package/dist/http/routes/leader.js +10 -0
  168. package/dist/http/routes/memory-policy.d.ts +6 -0
  169. package/dist/http/routes/memory-policy.js +38 -6
  170. package/dist/http/routes/notify-wake.d.ts +10 -0
  171. package/dist/http/routes/notify-wake.js +19 -3
  172. package/dist/http/routes/observability.d.ts +6 -0
  173. package/dist/http/routes/observability.js +19 -2
  174. package/dist/http/routes/runs.d.ts +15 -0
  175. package/dist/http/routes/runs.js +464 -41
  176. package/dist/http/routes/session-sync.d.ts +15 -0
  177. package/dist/http/routes/session-sync.js +229 -19
  178. package/dist/http/routes/sessions-list.d.ts +6 -0
  179. package/dist/http/routes/sessions-list.js +35 -4
  180. package/dist/http/routes/sessions.d.ts +14 -0
  181. package/dist/http/routes/sessions.js +176 -31
  182. package/dist/http/routes/side-query.d.ts +8 -0
  183. package/dist/http/routes/side-query.js +26 -1
  184. package/dist/http/routes/tasks.d.ts +18 -0
  185. package/dist/http/routes/tasks.js +501 -38
  186. package/dist/http/routes/trace-usage.d.ts +10 -0
  187. package/dist/http/routes/trace-usage.js +66 -14
  188. package/dist/http/routes/workflows.d.ts +12 -0
  189. package/dist/http/routes/workflows.js +180 -19
  190. package/dist/http/run-meta.d.ts +11 -0
  191. package/dist/http/run-meta.js +6 -0
  192. package/dist/http/send.d.ts +19 -0
  193. package/dist/http/send.js +11 -0
  194. package/dist/http/server.d.ts +255 -0
  195. package/dist/http/server.js +921 -75
  196. package/dist/http/sse-log.d.ts +15 -0
  197. package/dist/http/sse-log.js +16 -0
  198. package/dist/http/tar.d.ts +9 -0
  199. package/dist/http/tar.js +21 -5
  200. package/dist/http/wire-gate.d.ts +9 -0
  201. package/dist/http/wire-gate.js +9 -0
  202. package/dist/http/wire-types.d.ts +187 -0
  203. package/dist/http/workspace-content.d.ts +8 -0
  204. package/dist/http/workspace-content.js +10 -0
  205. package/dist/images/bake-validate.d.ts +63 -0
  206. package/dist/images/bake-validate.js +71 -1
  207. package/dist/images/manifest.d.ts +19 -0
  208. package/dist/images/manifest.js +6 -1
  209. package/dist/index.d.ts +1 -5
  210. package/dist/index.js +10 -5
  211. package/dist/key-resolver.d.ts +23 -0
  212. package/dist/key-resolver.js +27 -2
  213. package/dist/leader/diffout.d.ts +23 -0
  214. package/dist/leader/diffout.js +8 -1
  215. package/dist/leader/diffup.d.ts +32 -0
  216. package/dist/leader/diffup.js +43 -0
  217. package/dist/leader/endpoint.d.ts +20 -0
  218. package/dist/leader/endpoint.js +25 -2
  219. package/dist/leader/fanout.d.ts +60 -0
  220. package/dist/leader/fanout.js +29 -3
  221. package/dist/leader/grader-env-factory.d.ts +74 -0
  222. package/dist/leader/grader-env-factory.js +62 -3
  223. package/dist/leader/leader.d.ts +97 -0
  224. package/dist/leader/leader.js +151 -4
  225. package/dist/leader/merge.d.ts +94 -0
  226. package/dist/leader/merge.js +112 -9
  227. package/dist/leader/planner.d.ts +52 -0
  228. package/dist/leader/planner.js +47 -3
  229. package/dist/leader/repair-oracle.d.ts +61 -0
  230. package/dist/leader/repair-oracle.js +59 -1
  231. package/dist/leader/repair-wire.d.ts +78 -0
  232. package/dist/leader/repair-wire.js +79 -2
  233. package/dist/leader/wire.d.ts +79 -0
  234. package/dist/leader/wire.js +255 -8
  235. package/dist/lsp/e2b-bridge.d.ts +16 -0
  236. package/dist/lsp/e2b-bridge.js +32 -4
  237. package/dist/lsp/e2b-manager.d.ts +10 -0
  238. package/dist/lsp/e2b-manager.js +18 -2
  239. package/dist/lsp/lsp-frames.d.ts +12 -0
  240. package/dist/lsp/lsp-frames.js +12 -0
  241. package/dist/lsp/manager.d.ts +12 -0
  242. package/dist/lsp/manager.js +35 -2
  243. package/dist/lsp/types.d.ts +10 -0
  244. package/dist/lsp/ws-transport.d.ts +12 -0
  245. package/dist/lsp/ws-transport.js +53 -5
  246. package/dist/lsp-evict.d.ts +14 -0
  247. package/dist/lsp-evict.js +15 -1
  248. package/dist/main.js +530 -2129
  249. package/dist/memory-export.d.ts +1 -0
  250. package/dist/memory-export.js +4 -0
  251. package/dist/memory-scope.d.ts +49 -0
  252. package/dist/memory-scope.js +87 -4
  253. package/dist/memory-sync-client.d.ts +24 -0
  254. package/dist/memory-sync-client.js +41 -2
  255. package/dist/memory-sync.d.ts +23 -0
  256. package/dist/memory-sync.js +70 -1
  257. package/dist/model-select.d.ts +39 -0
  258. package/dist/model-select.js +42 -1
  259. package/dist/observability/cost-quota.d.ts +20 -0
  260. package/dist/observability/cost-quota.js +4 -0
  261. package/dist/observability/cost-taxonomy.d.ts +45 -0
  262. package/dist/observability/cost-taxonomy.js +34 -0
  263. package/dist/observability/logger.d.ts +1 -0
  264. package/dist/observability/logger.js +6 -0
  265. package/dist/observability/metrics.d.ts +13 -0
  266. package/dist/observability/metrics.js +63 -0
  267. package/dist/observability/otel-exporter.d.ts +9 -0
  268. package/dist/observability/otel-exporter.js +11 -3
  269. package/dist/observability/principal-context.d.ts +9 -0
  270. package/dist/observability/principal-context.js +9 -0
  271. package/dist/observability/prompt-manifest.d.ts +37 -0
  272. package/dist/observability/prompt-manifest.js +35 -1
  273. package/dist/observability/rate-limit.d.ts +10 -0
  274. package/dist/observability/rate-limit.js +1 -0
  275. package/dist/observability/tool-trace.d.ts +24 -0
  276. package/dist/observability/tool-trace.js +41 -1
  277. package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
  278. package/dist/orchestration/hardened-vm-runner.js +119 -4
  279. package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
  280. package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
  281. package/dist/orchestration/hardened-vm-worker.js +27 -0
  282. package/dist/orchestration/subagent-steer.d.ts +44 -0
  283. package/dist/orchestration/subagent-steer.js +45 -1
  284. package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
  285. package/dist/orchestration/workflow-agent-steer.js +80 -1
  286. package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
  287. package/dist/orchestration/workflow-completion-inbox.js +283 -32
  288. package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
  289. package/dist/orchestration/workflow-notify-journal.js +187 -13
  290. package/dist/parked-decide.d.ts +61 -0
  291. package/dist/parked-decide.js +58 -1
  292. package/dist/per-task-image.d.ts +54 -0
  293. package/dist/per-task-image.js +41 -0
  294. package/dist/plan-cache-probe.d.ts +35 -0
  295. package/dist/plan-cache-probe.js +18 -3
  296. package/dist/plugins/approval-exemption-store.d.ts +7 -0
  297. package/dist/plugins/approval-exemption-store.js +28 -2
  298. package/dist/plugins/approval-store-sql.d.ts +64 -0
  299. package/dist/plugins/approval-store-sql.js +32 -0
  300. package/dist/plugins/background-agent-store-sql.d.ts +55 -0
  301. package/dist/plugins/background-agent-store-sql.js +91 -4
  302. package/dist/plugins/background-shell-support.d.ts +98 -0
  303. package/dist/plugins/background-shell-support.js +117 -13
  304. package/dist/plugins/blob-backend.d.ts +104 -0
  305. package/dist/plugins/blob-backend.js +163 -6
  306. package/dist/plugins/breaker-state-sql.d.ts +65 -1
  307. package/dist/plugins/breaker-state-sql.js +46 -8
  308. package/dist/plugins/caching-session-store.d.ts +42 -0
  309. package/dist/plugins/caching-session-store.js +63 -3
  310. package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
  311. package/dist/plugins/checkpoint-store-sql.js +239 -11
  312. package/dist/plugins/file-outcome-sink.d.ts +4 -0
  313. package/dist/plugins/file-outcome-sink.js +9 -0
  314. package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
  315. package/dist/plugins/file-resume-anchor-store.js +43 -4
  316. package/dist/plugins/file-run-store.d.ts +90 -0
  317. package/dist/plugins/file-run-store.js +145 -13
  318. package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
  319. package/dist/plugins/file-snapshot-store-sql.js +178 -10
  320. package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
  321. package/dist/plugins/file-workflow-journal-store.js +10 -0
  322. package/dist/plugins/fork-routing-session-store.d.ts +47 -0
  323. package/dist/plugins/fork-routing-session-store.js +51 -3
  324. package/dist/plugins/host-platform.d.ts +69 -0
  325. package/dist/plugins/host-platform.js +81 -2
  326. package/dist/plugins/image-bake-store-sql.d.ts +136 -1
  327. package/dist/plugins/image-bake-store-sql.js +241 -7
  328. package/dist/plugins/image-index-sql.d.ts +46 -0
  329. package/dist/plugins/image-index-sql.js +113 -4
  330. package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
  331. package/dist/plugins/k8s-bg-scripts.js +88 -4
  332. package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
  333. package/dist/plugins/k8s-exec-protocol.js +41 -0
  334. package/dist/plugins/local-checkpoint-store.d.ts +40 -0
  335. package/dist/plugins/local-checkpoint-store.js +86 -7
  336. package/dist/plugins/local-session-store.d.ts +157 -0
  337. package/dist/plugins/local-session-store.js +289 -34
  338. package/dist/plugins/local-task-attachment-store.js +14 -2
  339. package/dist/plugins/mailbox-store-sql.d.ts +38 -0
  340. package/dist/plugins/mailbox-store-sql.js +39 -6
  341. package/dist/plugins/memory-engine-pg.d.ts +30 -0
  342. package/dist/plugins/memory-engine-pg.js +122 -4
  343. package/dist/plugins/memory-engine-tidb.d.ts +26 -0
  344. package/dist/plugins/memory-engine-tidb.js +115 -5
  345. package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
  346. package/dist/plugins/memory-engine-vector-util.js +5 -0
  347. package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
  348. package/dist/plugins/memory-resume-anchor-store.js +17 -0
  349. package/dist/plugins/memory-run-store.d.ts +63 -0
  350. package/dist/plugins/memory-run-store.js +73 -8
  351. package/dist/plugins/memory-session-policy-store.d.ts +17 -0
  352. package/dist/plugins/memory-session-policy-store.js +17 -0
  353. package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
  354. package/dist/plugins/memory-sync-store-pg.js +45 -2
  355. package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
  356. package/dist/plugins/memory-sync-store-tidb.js +31 -1
  357. package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
  358. package/dist/plugins/outcome-ledger-sql.js +97 -3
  359. package/dist/plugins/pg-approval-store.d.ts +7 -0
  360. package/dist/plugins/pg-approval-store.js +7 -0
  361. package/dist/plugins/pg-breaker-state.d.ts +6 -0
  362. package/dist/plugins/pg-breaker-state.js +6 -0
  363. package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
  364. package/dist/plugins/pg-checkpoint-store.js +8 -0
  365. package/dist/plugins/pg-cost-quota.d.ts +17 -0
  366. package/dist/plugins/pg-cost-quota.js +6 -0
  367. package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
  368. package/dist/plugins/pg-file-snapshot-store.js +6 -0
  369. package/dist/plugins/pg-image-bake.d.ts +9 -0
  370. package/dist/plugins/pg-image-bake.js +9 -0
  371. package/dist/plugins/pg-image-index.d.ts +9 -0
  372. package/dist/plugins/pg-image-index.js +9 -0
  373. package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
  374. package/dist/plugins/pg-outcome-ledger.js +9 -0
  375. package/dist/plugins/pg-pool.d.ts +50 -0
  376. package/dist/plugins/pg-pool.js +78 -0
  377. package/dist/plugins/pg-query.d.ts +11 -0
  378. package/dist/plugins/pg-rate-limiter.d.ts +23 -0
  379. package/dist/plugins/pg-rate-limiter.js +12 -2
  380. package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
  381. package/dist/plugins/pg-resume-anchor-store.js +5 -0
  382. package/dist/plugins/pg-run-store.d.ts +7 -0
  383. package/dist/plugins/pg-run-store.js +7 -0
  384. package/dist/plugins/pg-safe-json.d.ts +13 -0
  385. package/dist/plugins/pg-safe-json.js +40 -4
  386. package/dist/plugins/pg-session-policy-store.d.ts +5 -0
  387. package/dist/plugins/pg-session-policy-store.js +5 -0
  388. package/dist/plugins/pg-session-storage.d.ts +72 -0
  389. package/dist/plugins/pg-session-storage.js +152 -20
  390. package/dist/plugins/pg-session-store.d.ts +10 -0
  391. package/dist/plugins/pg-session-store.js +10 -0
  392. package/dist/plugins/pg-tool-result-store.d.ts +7 -0
  393. package/dist/plugins/pg-tool-result-store.js +7 -0
  394. package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
  395. package/dist/plugins/pg-workflow-journal-store.js +7 -0
  396. package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
  397. package/dist/plugins/pg-workflow-run-store.js +7 -0
  398. package/dist/plugins/posix-shell-fs.d.ts +3 -0
  399. package/dist/plugins/posix-shell-fs.js +31 -1
  400. package/dist/plugins/remote-env-adb.d.ts +25 -0
  401. package/dist/plugins/remote-env-adb.js +95 -12
  402. package/dist/plugins/remote-env-e2b.d.ts +146 -0
  403. package/dist/plugins/remote-env-e2b.js +371 -40
  404. package/dist/plugins/remote-env-file-error.d.ts +34 -0
  405. package/dist/plugins/remote-env-file-error.js +37 -0
  406. package/dist/plugins/remote-env-host.d.ts +151 -0
  407. package/dist/plugins/remote-env-host.js +468 -70
  408. package/dist/plugins/remote-env-k8s.d.ts +135 -0
  409. package/dist/plugins/remote-env-k8s.js +318 -32
  410. package/dist/plugins/remote-env-local-docker.d.ts +65 -0
  411. package/dist/plugins/remote-env-local-docker.js +149 -24
  412. package/dist/plugins/remote-env-ssh.d.ts +35 -0
  413. package/dist/plugins/remote-env-ssh.js +117 -20
  414. package/dist/plugins/remote-scratchpad.d.ts +38 -0
  415. package/dist/plugins/remote-scratchpad.js +32 -2
  416. package/dist/plugins/remote-shell.d.ts +22 -0
  417. package/dist/plugins/remote-shell.js +27 -1
  418. package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
  419. package/dist/plugins/resume-anchor-store-sql.js +11 -0
  420. package/dist/plugins/roster-store-sql.d.ts +27 -0
  421. package/dist/plugins/roster-store-sql.js +54 -0
  422. package/dist/plugins/run-store-sql.d.ts +194 -0
  423. package/dist/plugins/run-store-sql.js +210 -9
  424. package/dist/plugins/s3-presign.d.ts +20 -0
  425. package/dist/plugins/s3-presign.js +49 -2
  426. package/dist/plugins/scheduler-support.d.ts +12 -0
  427. package/dist/plugins/scheduler-support.js +75 -3
  428. package/dist/plugins/send-file-ledger.d.ts +15 -0
  429. package/dist/plugins/send-file-ledger.js +53 -4
  430. package/dist/plugins/send-user-file.d.ts +77 -0
  431. package/dist/plugins/send-user-file.js +92 -4
  432. package/dist/plugins/session-policy-store-sql.d.ts +15 -0
  433. package/dist/plugins/session-policy-store-sql.js +75 -3
  434. package/dist/plugins/session-store.d.ts +45 -0
  435. package/dist/plugins/session-store.js +51 -0
  436. package/dist/plugins/sql-driver.d.ts +77 -0
  437. package/dist/plugins/sql-driver.js +4 -0
  438. package/dist/plugins/sql-escape.d.ts +8 -0
  439. package/dist/plugins/sql-escape.js +8 -0
  440. package/dist/plugins/sql-row-helpers.d.ts +25 -0
  441. package/dist/plugins/sql-row-helpers.js +25 -0
  442. package/dist/plugins/store-backend.d.ts +103 -0
  443. package/dist/plugins/store-backend.js +120 -27
  444. package/dist/plugins/store-contracts.d.ts +62 -0
  445. package/dist/plugins/store-contracts.js +3 -0
  446. package/dist/plugins/task-attachment-store.d.ts +60 -0
  447. package/dist/plugins/task-attachment-store.js +49 -3
  448. package/dist/plugins/task-list-store-sql.d.ts +25 -0
  449. package/dist/plugins/task-list-store-sql.js +21 -0
  450. package/dist/plugins/tidb-approval-store.d.ts +6 -0
  451. package/dist/plugins/tidb-approval-store.js +6 -0
  452. package/dist/plugins/tidb-breaker-state.d.ts +5 -0
  453. package/dist/plugins/tidb-breaker-state.js +5 -0
  454. package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
  455. package/dist/plugins/tidb-checkpoint-store.js +7 -0
  456. package/dist/plugins/tidb-cost-quota.d.ts +6 -0
  457. package/dist/plugins/tidb-cost-quota.js +1 -0
  458. package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
  459. package/dist/plugins/tidb-file-snapshot-store.js +6 -0
  460. package/dist/plugins/tidb-image-bake.d.ts +8 -0
  461. package/dist/plugins/tidb-image-bake.js +8 -0
  462. package/dist/plugins/tidb-image-index.d.ts +8 -0
  463. package/dist/plugins/tidb-image-index.js +8 -0
  464. package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
  465. package/dist/plugins/tidb-outcome-ledger.js +10 -0
  466. package/dist/plugins/tidb-pool.d.ts +56 -0
  467. package/dist/plugins/tidb-pool.js +181 -1
  468. package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
  469. package/dist/plugins/tidb-rate-limiter.js +5 -2
  470. package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
  471. package/dist/plugins/tidb-resume-anchor-store.js +5 -0
  472. package/dist/plugins/tidb-run-store.d.ts +7 -0
  473. package/dist/plugins/tidb-run-store.js +7 -0
  474. package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
  475. package/dist/plugins/tidb-session-policy-store.js +5 -0
  476. package/dist/plugins/tidb-session-storage.d.ts +43 -0
  477. package/dist/plugins/tidb-session-storage.js +62 -3
  478. package/dist/plugins/tidb-session-store.d.ts +142 -0
  479. package/dist/plugins/tidb-session-store.js +264 -16
  480. package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
  481. package/dist/plugins/tidb-tool-result-store.js +7 -0
  482. package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
  483. package/dist/plugins/tidb-workflow-journal-store.js +7 -0
  484. package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
  485. package/dist/plugins/tidb-workflow-run-store.js +8 -0
  486. package/dist/plugins/tool-result-store-sql.d.ts +58 -0
  487. package/dist/plugins/tool-result-store-sql.js +38 -2
  488. package/dist/plugins/web-search.d.ts +35 -0
  489. package/dist/plugins/web-search.js +45 -9
  490. package/dist/plugins/workflow-journal-limits.d.ts +10 -0
  491. package/dist/plugins/workflow-journal-limits.js +10 -0
  492. package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
  493. package/dist/plugins/workflow-journal-store-sql.js +58 -2
  494. package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
  495. package/dist/plugins/workflow-run-store-sql.js +84 -6
  496. package/dist/plugins/worktree-isolation.d.ts +45 -0
  497. package/dist/plugins/worktree-isolation.js +105 -6
  498. package/dist/plugins/write-behind-counter.d.ts +56 -1
  499. package/dist/plugins/write-behind-counter.js +53 -14
  500. package/dist/principal-jwt.d.ts +24 -0
  501. package/dist/principal-jwt.js +60 -5
  502. package/dist/project-memory.d.ts +22 -0
  503. package/dist/project-memory.js +144 -15
  504. package/dist/prompts-domain-validate.d.ts +28 -0
  505. package/dist/prompts-domain-validate.js +42 -1
  506. package/dist/question.d.ts +33 -0
  507. package/dist/question.js +69 -2
  508. package/dist/resource-suspend.d.ts +21 -0
  509. package/dist/resource-suspend.js +16 -0
  510. package/dist/router/route-orchestration.d.ts +78 -0
  511. package/dist/router/route-orchestration.js +77 -0
  512. package/dist/run-local.d.ts +32 -0
  513. package/dist/run-local.js +172 -8
  514. package/dist/runs.d.ts +189 -4
  515. package/dist/runs.js +465 -24
  516. package/dist/runtime-caps-resolver.d.ts +106 -0
  517. package/dist/runtime-caps-resolver.js +89 -4
  518. package/dist/runtime-governance.d.ts +84 -0
  519. package/dist/runtime-governance.js +127 -1
  520. package/dist/sandbox-pkg-source.d.ts +23 -0
  521. package/dist/sandbox-pkg-source.js +37 -0
  522. package/dist/sealed-key.d.ts +54 -0
  523. package/dist/sealed-key.js +68 -3
  524. package/dist/security.d.ts +108 -0
  525. package/dist/security.js +145 -6
  526. package/dist/sema-registry.d.ts +21 -0
  527. package/dist/sema-registry.js +21 -0
  528. package/dist/session-leaf-bus.d.ts +31 -0
  529. package/dist/session-leaf-bus.js +32 -0
  530. package/dist/session-sync-content.d.ts +46 -0
  531. package/dist/session-sync-content.js +47 -1
  532. package/dist/session-sync-kernel.d.ts +89 -0
  533. package/dist/session-sync-kernel.js +48 -3
  534. package/dist/session-sync.d.ts +125 -0
  535. package/dist/session-sync.js +181 -8
  536. package/dist/session-titler.d.ts +26 -0
  537. package/dist/session-titler.js +41 -7
  538. package/dist/session-watch.d.ts +53 -1
  539. package/dist/session-watch.js +73 -9
  540. package/dist/sighup-idle.d.ts +30 -0
  541. package/dist/sighup-idle.js +12 -1
  542. package/dist/spec-fields.d.ts +91 -3
  543. package/dist/spec-fields.js +116 -4
  544. package/dist/task-cwd.d.ts +50 -0
  545. package/dist/task-cwd.js +68 -1
  546. package/dist/task-mcp.d.ts +47 -0
  547. package/dist/task-mcp.js +29 -0
  548. package/dist/task-settings.d.ts +175 -0
  549. package/dist/task-settings.js +226 -11
  550. package/dist/task-workflow.d.ts +67 -0
  551. package/dist/task-workflow.js +75 -8
  552. package/dist/tool-approval.d.ts +91 -0
  553. package/dist/tool-approval.js +215 -0
  554. package/dist/trace/artifacts.d.ts +9 -0
  555. package/dist/trace/artifacts.js +30 -5
  556. package/dist/trace/core-keyset-guard.d.ts +18 -0
  557. package/dist/trace/ledger-sink.d.ts +43 -0
  558. package/dist/trace/ledger-sink.js +64 -5
  559. package/dist/trace/project.d.ts +133 -0
  560. package/dist/trace/project.js +189 -2
  561. package/dist/trace/redact.d.ts +19 -0
  562. package/dist/trace/redact.js +66 -9
  563. package/dist/usage-analytics.d.ts +26 -0
  564. package/dist/usage-analytics.js +26 -3
  565. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -1,111 +1,54 @@
1
- import { readFileSync, existsSync, realpathSync } from "node:fs";
2
- import { setLeafAdvanceListener } from "./session-leaf-bus.js";
3
- import { createHash } from "node:crypto";
1
+ import { readFileSync } from "node:fs";
4
2
  import { homedir } from "node:os";
5
3
  import { join } from "node:path";
6
- import { loadRemoteExec } from "@sema-agent/registry-core/node";
7
- import { skillContentHash } from "@sema-agent/registry-core";
8
- import { Runner, TtlSessionStore, uuidv7, FileRosterStore, FileBackgroundAgentStore, FileMailboxStore, defaultTaskRegistry, combinePolicies, createDurableQuestionPolicy, createAllowDenyPolicy, tightenTaskSpec, isThinkingLevel, QUESTION_AWAITS_RESUME, FileWorkflowRunStore, InMemoryWorkflowRunStore, workflowsCapability, NodeLspManager, createFileWorkflowScriptStore, expandTiers, NodeExecutionEnv, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, verifyPromptArtifact, createWebFetchSummarizer, resolveTaskModel as coreResolveTaskModel } from "@sema-agent/core";
9
- import { PgMemoryEngineBackend, ensurePgMemoryEngineSchema } from "./plugins/memory-engine-pg.js";
10
- import { TiDBMemoryEngineBackend, ensureTiDBMemoryEngineSchema } from "./plugins/memory-engine-tidb.js";
11
- import { PgMemoryHistoryStore, ensurePgMemoryHistorySchema, PgMemorySyncStore, ensurePgMemorySyncSchema } from "./plugins/memory-sync-store-pg.js";
12
- import { TiDBMemoryHistoryStore, ensureTiDBMemoryHistorySchema, TiDBMemorySyncStore, ensureTiDBMemorySyncSchema } from "./plugins/memory-sync-store-tidb.js";
13
- import { resolveTaskModel, modelSupportsImages, matchCatalogModel } from "./model-select.js";
14
- import { derivePkgSourceEnv, customPkgSourceFromEnv } from "./sandbox-pkg-source.js";
4
+ import { Runner, TtlSessionStore, uuidv7, defaultTaskRegistry, combinePolicies, createDurableQuestionPolicy, createAllowDenyPolicy, workflowsCapability, createWebFetchSummarizer, resolveTaskModel as coreResolveTaskModel } from "@sema-agent/core";
15
5
  import { createSessionTitler } from "./session-titler.js";
16
- import { SessionWatchRegistry, posIntEnv } from "./session-watch.js";
17
- import { selectEnvironmentTool, SessionEnvironmentSelection } from "./capabilities/select-environment-tool.js";
18
- import { sendUserFileTool, SendUserFileEmitter, SEND_USER_FILE_MAX_BYTES, subagentSendUserFileExtraTools } from "./capabilities/send-user-file-tool.js";
19
- import { TaskEnvRegistry, createSandboxFileSend, sandboxSendLaneEnabled } from "./capabilities/sandbox-file-send.js";
20
- import { createSendUserFileIssuer } from "./plugins/send-user-file.js";
21
- import { withLedgerRecording } from "./plugins/send-file-ledger.js";
22
- import { basename, resolve } from "node:path";
23
- import { stat as fsStat, readFile as fsReadFile } from "node:fs/promises";
24
- import { acceptShellScratchpadDir, buildEnvFacts, egressForRemoteExec, ensureScratchpadDir, purgeScratchpadDir, sweepStaleScratchpads, resumeFactsForLane } from "./env-facts.js";
25
- import { normalizeSuggestNextPrompts, normalizeResilience, normalizeAttachments, normalizeResumeAtMode, resolveTaskLimits, taskAgentsSpecFragment, retainBackgroundProcessesFromBody, toolNameListFromBody, promptProfileFromBody } from "./spec-fields.js";
26
- import { createBrain, brainSummary } from "./brain.js";
27
- import { configLkgEnabled, loadConfig, logConfigDiagnostics, resolveBindHost } from "./config.js";
6
+ import { posIntEnv } from "./session-watch.js";
7
+ import { selectEnvironmentTool } from "./capabilities/select-environment-tool.js";
8
+ import { subagentSendUserFileExtraTools } from "./capabilities/send-user-file-tool.js";
9
+ import { resolve } from "node:path";
10
+ import { brainSummary } from "./brain.js";
11
+ import { loadConfig, logConfigDiagnostics, resolveBindHost } from "./config.js";
28
12
  import { drainNumEnvWarnings } from "./plugins/remote-shell.js";
29
- import { resourceSuspendOptIn } from "./resource-suspend.js";
30
- import { createSessionStore, ensureChildSessionDurableWithPromotion } from "./plugins/session-store.js";
13
+ import { ensureChildSessionDurableWithPromotion } from "./plugins/session-store.js";
31
14
  import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
32
- import { createStoreBackend, openStoreBackendWithFallback, assertCloudSnapshotBlobPosture } from "./plugins/store-backend.js";
33
- import { e2bExecutionEnvFactory } from "./plugins/remote-env-e2b.js";
34
- import { k8sExecutionEnvFactory } from "./plugins/remote-env-k8s.js";
35
- import { sshExecutionEnvFactory } from "./plugins/remote-env-ssh.js";
36
- import { adbExecutionEnvFactory } from "./plugins/remote-env-adb.js";
37
- import { hostExecutionEnvFactory, RemoteHostExecutionEnv } from "./plugins/remote-env-host.js";
38
15
  import { webSearchConfigFromEnv, createWebSearchBackend, setWebSearchBadPayloadObserver } from "./plugins/web-search.js";
39
- import { FileSchedulerBackend, defaultSchedulerStorePath } from "./plugins/scheduler-support.js";
40
- import { localDockerExecutionEnvFactory } from "./plugins/remote-env-local-docker.js";
41
- import { withWorktreeIsolation, reapOrphanWorktrees } from "./plugins/worktree-isolation.js";
42
- import { isRemoteScratchpadLane, remoteScratchpadDirFor, withRemoteScratchpad } from "./plugins/remote-scratchpad.js";
43
- import { createE2bLspManager } from "./lsp/e2b-manager.js";
44
- import { createAuthorizer, memoryEngineBackendFor, memoryEngineRemoteLanePosture, memorySpecForRequest, HttpError } from "./security.js";
45
- import { createOaApprovalPolicy, createDurableAskPolicy } from "./approval.js";
46
- import { buildSessionAuditPg, buildSessionAuditLocal, buildSessionAudit } from "./audit.js";
47
- import { makeDegenerateInstrument } from "./degenerate-instrument.js";
48
- import { PlanCacheProbe } from "./plan-cache-probe.js";
16
+ import { createAuthorizer } from "./security.js";
17
+ import { createDurableAskPolicy } from "./approval.js";
49
18
  import { loadSkills } from "./capabilities/skills.js";
50
- import { applyCenterPlugins } from "./capabilities/center-plugins.js";
51
19
  import { GiteaClient } from "./capabilities/repo-tools.js";
52
- import { buildScenarios, selectScenario, centerScenarios, mergeUserSkills, builtinScenarioDetails, centerScenarioDetails, gateScenarioRequest } from "./capabilities/scenarios.js";
53
- import { applyLongtailDefer } from "./capabilities/tool-defer.js";
54
- import { resolveCollabWorkflow, listCollabWorkflows } from "./capabilities/collab-workflows.js";
20
+ import { buildScenarios, builtinScenarioDetails } from "./capabilities/scenarios.js";
55
21
  import { createLogger } from "./observability/logger.js";
56
- import { createSighupIdleHandler } from "./sighup-idle.js";
57
22
  import { createToolTracer, createPermissionDeniedMeter } from "./observability/tool-trace.js";
58
23
  import { createRegistryJwtVerifier } from "./auth-bridge.js";
59
24
  import { createMetrics } from "./observability/metrics.js";
60
25
  import { setRedactionObserver, redactSecrets } from "./trace/redact.js";
61
26
  import { RateLimiter } from "./observability/rate-limit.js";
62
- import { createHttpServer, explicitOperator, } from "./http/server.js";
27
+ import { createHttpServer, explicitOperator } from "./http/server.js";
63
28
  import { exportMemoryScope } from "./memory-export.js";
64
29
  import { performMemorySync } from "./memory-sync.js";
65
- import { createMemorySyncRunner, createMemorySyncTransport } from "./memory-sync-client.js";
66
- import { createLeaderEndpoint } from "./leader/endpoint.js";
67
- import { createLeaderRunner } from "./leader/wire.js";
68
- import { WorkflowNotifyGate, JournalingWorkflowRunStore, FileWorkflowNotifyJournalStore, } from "./orchestration/workflow-notify-journal.js";
69
- import { FileWorkflowCompletionInbox, InMemoryWorkflowCompletionInbox, resolveCompletionRoute, resolveServedSession, } from "./orchestration/workflow-completion-inbox.js";
70
- import { buildPricing, createTracer, createSideQueryAccountant, cappedCeiling, ModelUsageTracker, PromptManifestTracker } from "./budget.js";
71
- import { CostQuota } from "./observability/cost-quota.js";
72
30
  import { startOtlpExporter } from "./observability/otel-exporter.js";
73
31
  import { HEARTBEAT_MS, backgroundAgentOutput, taskHandleOutput, taskHandleStop } from "./runs.js";
74
- import { ElicitationCoordinator } from "./elicitation.js";
75
- import { QuestionCoordinator } from "./question.js";
76
- import { ToolApprovalCoordinator } from "./tool-approval.js";
77
- import { applyEffective, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, mcpForScenario, restartReasons, modelPlaneChanged, planeHasActiveTiers, fetchPromptArtifact } from "./sema-registry.js";
78
- import { ensureSealedKeyStore, reportExecutionPublicKey } from "./sealed-key.js";
79
- import { createConfigProvider, raceBootFetch, BOOT_FETCH_DEFERRED } from "./config-provider.js";
80
- import { validatePromptsDomain, centerPromptProvider, centerIdentityAssembled, applyCatalogToSource, CORE_ENGINE_VERSION, withPromptArtifactBackfill } from "./capabilities/center-prompts.js";
81
- import { defaultLkgPath, defaultSkillCacheDir, saveLkg, loadLkg } from "./config-lkg.js";
82
- import { TiDBRosterStore, PgRosterStore, ensureTiDBRosterSchema, ensurePgRosterSchema } from "./plugins/roster-store-sql.js";
83
- import { TiDBTaskAttachmentStore, PgTaskAttachmentStore, ensureTiDBTaskAttachmentSchema, ensurePgTaskAttachmentSchema, bindAttachmentsForTask, materializeAttachmentsInto } from "./plugins/task-attachment-store.js";
84
- import { LocalTaskAttachmentStore } from "./plugins/local-task-attachment-store.js";
85
- import { MinioBlobBackend, SQL_BLOB_DEFAULT_MAX_BYTES } from "./plugins/blob-backend.js";
86
- import { TiDBBackgroundAgentStore, PgBackgroundAgentStore, ensureTiDBBackgroundAgentSchema, ensurePgBackgroundAgentSchema } from "./plugins/background-agent-store-sql.js";
87
- import { TiDBMailboxStore, PgMailboxStore, ensureTiDBMailboxSchema, ensurePgMailboxSchema } from "./plugins/mailbox-store-sql.js";
88
- import { createPrincipalEntitlementsClient, gateExecutionLane, scopedTokenNeedsWorker, applyObserverEnvOptIn } from "./runtime-caps-resolver.js";
89
- import { applyRuntimeGovernance, stripDelegationTools } from "./runtime-governance.js";
90
- import { parseTaskSettings, applyTaskSettings, coercePermissionMode, withPermissionMode, effectiveThinking, acceptAppendSystemPrompt, hasConstitutionAnchors, providerDropsAppend, MAX_SETTINGS_OUTPUT_STYLE_CHARS } from "./task-settings.js";
91
- import { createTaskHooks, composeHooks } from "./hooks/hook-runner.js";
32
+ import { SQL_BLOB_DEFAULT_MAX_BYTES } from "./plugins/blob-backend.js";
33
+ import { composeHooks } from "./hooks/hook-runner.js";
92
34
  import { createHookLlm } from "./hooks/hook-llm.js";
93
- import { selfOrchestrationFromBody, enableForkFromBody, workflowModelAllowlistFor, normalizeRetainSubagentSessions } from "./task-workflow.js";
94
- import { FleetUsageAccumulator, startFleetClientFromEnv } from "./fleet-client.js";
95
- import { createFleetLeaseFromEnv } from "./fleet-lease.js";
35
+ import { startFleetClientFromEnv } from "./fleet-client.js";
96
36
  import { reclaimOrphansAtBoot } from "./boot-reclaim.js";
97
- import { evictLspOnDestroy } from "./lsp-evict.js";
98
- import { makeLoadProjectMemory, makeProbeInstructionSources } from "./project-memory.js";
99
- import { cwdHonored, effectiveHostWorkspace, inProcessSingleUserLane, isValidCwd, parseAdditionalDirectories, satisfiedByProcessCwd, shellEnvMismatchCount } from "./task-cwd.js";
100
- import { resolveRequestMcp, mcpInjectionHonored } from "./task-mcp.js";
101
- import { PerTaskImageRegistry, resolveSandboxImageRef } from "./per-task-image.js";
102
- import { routeServiceTask, isIsolatedExecEnv, supPostureOverrides } from "./router/route-orchestration.js";
103
- import { createHardenedVmRunner } from "./orchestration/hardened-vm-runner.js";
104
- import { createWorkerHardenedVmRunner } from "./orchestration/hardened-vm-worker-runner.js";
105
- import { WorkflowAgentRegistry } from "./orchestration/workflow-agent-steer.js";
106
- import { SubagentSteerRegistry } from "./orchestration/subagent-steer.js";
107
- import { FleetEventBus, fleetBackgroundChildPublisher } from "./fleet/fleet-bus.js";
108
- import { createKeyResolver } from "./key-resolver.js";
37
+ import { createConfigCenterRuntime } from "./boot/config-center.js";
38
+ import { createResolveSpec } from "./boot/resolve-spec.js";
39
+ import { startReapers } from "./boot/reapers.js";
40
+ import { openStores } from "./boot/stores.js";
41
+ import { createBudgetAndTracing } from "./boot/budget-tracing.js";
42
+ import { createExecutionEnv } from "./boot/execution-env.js";
43
+ import { createWorkflowOrchestration } from "./boot/workflow-orchestration.js";
44
+ import { createLiveCoordinators } from "./boot/coordinators.js";
45
+ import { createRuntimeCaps } from "./boot/runtime-caps.js";
46
+ import { createRunnerDeps, createSharedRunnerDeps } from "./boot/runner-deps.js";
47
+ import { createSessionFaces } from "./boot/session-faces.js";
48
+ import { createLeaderFace } from "./boot/leader.js";
49
+ import { installShutdownHandlers } from "./boot/shutdown.js";
50
+ /** Service version for GET /v1/capabilities — read from the package.json shipped next to src/ in the
51
+ * image (Dockerfile COPYs it). Best-effort: an unreadable file reports "unknown", never crashes boot. */
109
52
  function serviceVersion() {
110
53
  try {
111
54
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
@@ -115,1033 +58,146 @@ function serviceVersion() {
115
58
  return "unknown";
116
59
  }
117
60
  }
61
+ /**
62
+ * Service entrypoint. **装配次序表**(design/158 A10)。
63
+ *
64
+ * 各 boot 段已按内聚下沉到 `src/boot/*.ts` 叶子(纯搬运,逐字);本函数保留的是**顺序本身**——
65
+ * 谁先谁后是行为(env 读取时序、诊断排空点、信号注册时点、工厂装饰顺序),每个叶子的文件头
66
+ * 都写明了它那条「位置即契约」。当前次序:
67
+ *
68
+ * loadConfig → logger/metrics → 诊断排空(logConfigDiagnostics + drainNumEnvWarnings)
69
+ * → boot/config-center(sealed-key/provider/prompt-epoch 店/effective 拉取/LKG 兜底/tolerant apply)
70
+ * → boot/config-center.applyLocalRemoteExec(local 形 config.d/remote-exec.json 覆写)
71
+ * → boot/stores(StoreBackend + memory + 四个 durable 店 + sessionStore + 拒启断言)
72
+ * → boot/budget-tracing(brain/pricing/quota/tracer)
73
+ * → boot/execution-env(remote-exec 工厂链 + LSP)
74
+ * → toolTracer/deploymentHooks
75
+ * → boot/workflow-orchestration → boot/coordinators → boot/runtime-caps
76
+ * → boot/runner-deps → new Runner → hook LLM/agent → subRunner
77
+ * → runStore/anchors/approval 面 → skills → boot/config-center.applyCenterCapabilities
78
+ * (center skills/plugins/boot-LKG/mcp)→ buildScenarios → .applyCenterScenarios
79
+ * → boot/reapers → OTLP → boot/config-center.startRefreshLoop → .initKeyResolver
80
+ * → boot/session-faces → boot/leader → ServiceDeps 七组 → boot/resolve-spec
81
+ * → createHttpServer → listen → fleet client → boot/shutdown(必须最后:信号注册时点即契约)
82
+ *
83
+ * A10 尾刀(config-center):`ccEtag/effective/latestEffective/appliedPlaneEff/planeDeferredNoHandoff/
84
+ * pendingRestart/bootSkillStale/keyResolver/centerPrompts/…` 这组跨段可变状态,已整体搬进
85
+ * `boot/config-center.ts` 那只工厂的**闭包**(仍是 `let`,每一处读写的文本与次序逐字未改 ——
86
+ * codex R10–R38 的重启风暴判据全部按原样保留);main 只经窄口读:`getKeyResolver/getCenterPrompts/
87
+ * modelReady/restartState/planeDeferredState/promptSource`,与本目录既有的晚绑 getter 惯例同款。
88
+ *
89
+ * Wires the three planes:
90
+ * Brain = external LLM gateway (createBrain)
91
+ * Session = pluggable store (TiDB durable event-log center, or in-memory)
92
+ * Hands = tools/MCP injected per request in resolveSpec (deployment-owned)
93
+ * Persistence (L1 sessions + L2 memory) lives in the external TiDB center; this container is stateless.
94
+ */
118
95
  async function main() {
119
96
  const config = loadConfig();
97
+ // Liveness invariant: the running instance refreshes updated_at every HEARTBEAT_MS; the reaper fails
98
+ // a run whose updated_at is older than runStaleSec. If the stale window were ≤ the heartbeat, the
99
+ // reaper would race a perfectly alive run. Fail fast on a misconfiguration instead of reaping live work.
120
100
  if (config.runStaleSec * 1000 <= HEARTBEAT_MS) {
121
101
  throw new Error(`REAP_RUN_STALE_SEC (${config.runStaleSec}s) must exceed the ${HEARTBEAT_MS / 1000}s liveness heartbeat`);
122
102
  }
123
103
  const logger = createLogger(config.logLevel);
124
104
  const metrics = createMetrics();
105
+ // S20 (SILENT-FALLBACK P1) + [792]④ boot notices: config parsing runs before the logger exists — drain both
106
+ // pre-logger collectors via the shared helper (F10: run-local uses the same one, so entrypoints can't drift).
125
107
  logConfigDiagnostics(logger);
108
+ // design/158 B4 交接小件:plugins 数值旋钮(HOST_BG_*/K8S_BG_*/E2B_*,模块级 import 时读)的非法
109
+ // 字面量告警——通道在 remote-shell.ts 自带(不造 plugins→config 依赖边),与上面同一 boot 诊断点排空。
126
110
  for (const w of drainNumEnvWarnings())
127
111
  logger.warn("config_env_invalid_using_default", { env: w.env, raw: w.raw });
112
+ // M1(接入审计 2026-07-08):core verify 契约=verifier 必须异于 implementer(去相关性是 🔴 部署契约)。
113
+ // verifier 角色落 "default" 时对抗验证者=实现者自评自批——比无验证更糟(假安全感)。曝光而非静默:
114
+ // 单模型部署配 MODEL_VERIFIER 指异构模型即消警(typo/不在 catalog 同样落到这里)。
128
115
  if ((config.roles?.verifier ?? "default") === "default") {
129
116
  logger.warn("verify_decorrelation_unavailable", {
130
117
  hint: "verifier role = default (= implementer) — adversarial verification grades its own work; set MODEL_VERIFIER=<catalog model id> to a heterogeneous model",
131
118
  ...(process.env.MODEL_VERIFIER ? { rejected: process.env.MODEL_VERIFIER, reason: "not in catalog" } : {}),
132
119
  });
133
120
  }
121
+ // S24: redaction rewrites by pattern; LOW: web-search non-array payloads by provider.
134
122
  setRedactionObserver((pattern, n) => metrics.inc("redactions_applied_total", { pattern }, n));
135
123
  setWebSearchBadPayloadObserver((provider) => {
136
124
  metrics.inc("web_search_bad_payload_total", { provider });
137
125
  logger.warn("web_search_non_array_payload", { provider });
138
126
  });
139
- let ccEtag;
140
- let effective;
141
- let latestEffective;
142
- let lastRejectedCandidate;
143
- let appliedPlaneEff;
144
- let planeDeferredNoHandoff;
145
- let bootSkillStale = false;
146
- let pendingRestart;
147
- const cc = config.configCenter;
127
+ // 🔴 ONE local root for BOTH the config provider (models/skills/mcp) AND the remoteExec loader (avoids a
128
+ // split-brain — createConfigProvider defaulted to process.cwd() while loadRemoteExec used
129
+ // AGENT_DATA_DIR, so config.d and remote-exec.json could resolve to DIFFERENT directories). run-local already
130
+ // computes this once; main.ts now does too. Shared with the config-center runtime and every boot leaf below.
148
131
  const localRoot = config.configLocalDir ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
149
- let sealedKeys;
150
- try {
151
- sealedKeys = await ensureSealedKeyStore(join(config.localDataRoot ?? localRoot, "sealed-key"), logger);
152
- void reportExecutionPublicKey({
153
- baseUrl: cc?.baseUrl,
154
- adminToken: process.env.SEMA_REGISTRY_ADMIN_TOKEN || process.env.REGISTRY_ADMIN_TOKEN,
155
- key: sealedKeys.latest,
156
- ...(cc?.worker ? { notes: `worker ${cc.worker}` } : {}),
157
- logger,
158
- });
159
- }
160
- catch (err) {
161
- logger.warn("sealed_key_store_unavailable", { err: String(err), note: "sealed api keys cannot be unsealed on this host — models carrying sealedApiKey will fail loud per-model" });
162
- }
163
- const configProvider = cc || config.configProvider === "local"
164
- ? createConfigProvider({
165
- ...(config.configProvider ? { provider: config.configProvider } : {}),
166
- ...(cc ? { configCenter: { baseUrl: cc.baseUrl, token: cc.token, worker: cc.worker, dryRun: cc.dryRun } } : {}),
167
- ...(config.configProvider === "local" ? { localDir: localRoot } : {}),
168
- })
169
- : undefined;
170
- const modelReadyState = { ready: !(cc && !cc.dryRun) || Boolean(process.env.MODEL_ID) };
171
- const markRosterLanded = (eff) => {
172
- if (!modelReadyState.ready && (eff.models?.models ?? []).some((m) => m.enabled !== false)) {
173
- modelReadyState.ready = true;
174
- logger.info("model_roster_landed", { note: "ready gate open — billable submissions accepted" });
175
- }
176
- };
177
- let centerPrompts;
178
- let pendingCatalogFaces;
179
- const retryPendingCatalog = async (phase) => {
180
- if (!pendingCatalogFaces)
181
- return;
182
- const { target } = pendingCatalogFaces;
183
- if (await applyCatalogToSource(promptSource, target, centerPrompts, logger, phase)) {
184
- centerPrompts = target;
185
- pendingCatalogFaces = undefined;
186
- }
187
- };
188
- const promptEpochDir = join(config.localDataRoot ?? localRoot, "prompt-epoch");
189
- const promptStateDir = join(promptEpochDir, "state", cc?.worker === undefined ? "global" : `worker-${createHash("sha256").update(cc.worker).digest("hex").slice(0, 16)}`);
190
- const withByDigestBackfill = (inner) => cc?.baseUrl ? withPromptArtifactBackfill(inner, (d) => fetchPromptArtifact(cc.baseUrl, cc.token, d), logger) : inner;
191
- const promptSource = (() => {
192
- try {
193
- return new CenterPromptSource({
194
- store: withByDigestBackfill(new FilePromptArtifactStore(promptEpochDir)),
195
- stateStore: new FilePromptSourceStateStore(promptStateDir),
196
- engineVersion: CORE_ENGINE_VERSION,
197
- });
198
- }
199
- catch (e) {
200
- logger.warn("center_prompts_store_unwritable", { dir: promptEpochDir, err: String(e), note: "file-backed prompt-epoch store unavailable — memory fallback (catalog candidate not restart-durable); built-ins/declaration axis unaffected" });
201
- return new CenterPromptSource({ store: withByDigestBackfill(new MemoryPromptArtifactStore()), stateStore: new MemoryPromptSourceStateStore(), engineVersion: CORE_ENGINE_VERSION });
202
- }
203
- })();
204
- if (configProvider && !cc?.dryRun) {
205
- try {
206
- await promptSource.restore();
207
- }
208
- catch (e) {
209
- logger.warn("center_prompts_catalog_restore_failed", { err: String(e), note: "disk LKG unavailable — catalog candidate absent until the next adopt; declaration axis and built-ins unaffected" });
210
- }
211
- }
212
- const adoptCenterPrompts = async (eff, phase) => {
213
- const raw = eff?.prompts;
214
- if (raw === undefined) {
215
- if (centerPrompts)
216
- logger.info("center_prompts_cleared", { phase, note: "effective carries no prompts key (publish gate off) — built-in providers resume" });
217
- const cleared = await applyCatalogToSource(promptSource, undefined, centerPrompts, logger, phase);
218
- if (cleared) {
219
- centerPrompts = undefined;
220
- pendingCatalogFaces = undefined;
221
- }
222
- else {
223
- pendingCatalogFaces = { target: undefined };
224
- }
225
- return;
226
- }
227
- const v = validatePromptsDomain(raw);
228
- if (!v.ok) {
229
- logger.warn("center_prompts_invalid", { phase, error: v.error, kept: centerPrompts?.declarations?.packId ?? (centerPrompts?.catalog ? "(catalog)" : "(builtin)"), note: "malformed prompts face rejected (core assemble would THROW per task) — keeping the previous pack/built-ins; fix the center payload" });
230
- return;
231
- }
232
- if (centerPrompts?.identity !== v.value.identity) {
233
- logger.info("center_prompts_adopted", {
234
- phase,
235
- axes: v.value.axes,
236
- ...(v.value.declarations
237
- ? { packId: v.value.declarations.packId, contentDigest: v.value.declarations.contentDigest, sections: v.value.declarations.sections.length, scenarioOverrides: Object.keys(v.value.declarations.scenarioOverrides ?? {}).length }
238
- : {}),
239
- ...(v.value.catalog ? { artifactDigest: v.value.catalog.artifact.artifactDigest, catalogDigest: v.value.catalog.artifact.payload.catalogDigest, sourceRevision: v.value.catalog.sourceRevision } : {}),
240
- note: "new tasks assemble the new pack (epoch re-pins at the next task boundary); in-flight sessions stay pinned",
241
- });
242
- }
243
- const transitioned = await applyCatalogToSource(promptSource, v.value, centerPrompts, logger, phase);
244
- if (transitioned) {
245
- centerPrompts = v.value;
246
- pendingCatalogFaces = undefined;
247
- }
248
- else {
249
- pendingCatalogFaces = { target: v.value };
250
- }
251
- };
252
- let bootConfigPending;
253
- const lkgEnabled = configProvider?.kind === "remote" && configLkgEnabled();
254
- const lkgPath = process.env.CONFIG_LKG_PATH ?? defaultLkgPath(cc?.worker);
255
- const lkgSurvivesRestart = lkgEnabled && process.env.CONFIG_LKG_DURABLE === "true";
256
- let lkgBooted = false;
257
- let bootLkgCandidate;
258
- let lastPrewarmBodiesVerified = false;
259
- const persistLkgDurable = async (effectiveObj, etag) => {
260
- lastPrewarmBodiesVerified = false;
261
- if (configProvider?.kind === "local")
262
- return true;
263
- if (!lkgEnabled)
264
- return false;
265
- try {
266
- if (effectiveObj.skills && config.configCenter) {
267
- await applyCenterSkills([], effectiveObj.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, defaultSkillCacheDir());
268
- }
269
- const enabledSkills = (effectiveObj.skills?.skills ?? []).filter((m) => m.enabled !== false);
270
- const cacheDir = defaultSkillCacheDir();
271
- for (const m of enabledSkills) {
272
- const hex = m.contentHash.replace(/^sha256:/, "").toLowerCase();
273
- if (!/^[0-9a-f]{64}$/.test(hex))
274
- return false;
275
- try {
276
- const text = await fsReadFile(join(cacheDir, hex), "utf8");
277
- if (`sha256:${createHash("sha256").update(text, "utf8").digest("hex")}` !== `sha256:${hex}`) {
278
- logger.warn("config_lkg_skill_cache_incomplete", { skill: m.name, hash: m.contentHash, note: "cached body fails its hash — LKG NOT replaced, handoff NOT confirmed (the previous good LKG keeps serving offline boots)" });
279
- return false;
280
- }
281
- }
282
- catch {
283
- logger.warn("config_lkg_skill_cache_incomplete", { skill: m.name, hash: m.contentHash, note: "enabled skill body not in the cache — LKG NOT replaced, handoff NOT confirmed" });
284
- return false;
285
- }
286
- }
287
- lastPrewarmBodiesVerified = true;
288
- await saveLkg(lkgPath, cc?.worker, effectiveObj, etag);
289
- return lkgSurvivesRestart;
290
- }
291
- catch (err) {
292
- logger.warn("config_lkg_save_failed", { path: lkgPath, err: String(err), note: "durable persist failed — restart signal proceeds anyway (a dead disk + dead center double-fault can re-loop; fix the disk)" });
293
- return false;
294
- }
295
- };
296
- if (configProvider) {
297
- const dryRun = cc?.dryRun ?? false;
298
- const bootFromLkg = async (why) => {
299
- if (!lkgEnabled || dryRun)
300
- return null;
301
- const read = await loadLkg(lkgPath, cc?.worker);
302
- if (!read.lkg) {
303
- if (read.reason !== "absent")
304
- logger.warn("config_lkg_unusable", { path: lkgPath, reason: read.reason, note: "ignoring the on-disk LKG — env fallback serves; the cache rewrites on the next clean candidate" });
305
- return null;
306
- }
307
- logger.info("config_loaded_lkg", { path: lkgPath, savedAt: read.lkg.savedAt, why, note: "booting from the persisted last-known-good (all restart-to-apply faces included) — the live pull continues in the background and reconciles on arrival" });
308
- return { effective: read.lkg.effective, ...(read.lkg.etag !== undefined ? { etag: read.lkg.etag } : {}) };
309
- };
310
- const bootApplyLkgInline = async (lkgR) => {
311
- applyEffective(config, lkgR.effective, logger, { sealedKeys });
312
- await adoptCenterPrompts(lkgR.effective, "boot-lkg");
313
- markRosterLanded(lkgR.effective);
314
- effective = lkgR.effective;
315
- appliedPlaneEff = lkgR.effective;
316
- latestEffective = lkgR.effective;
317
- if (lkgR.etag !== undefined)
318
- ccEtag = lkgR.etag;
319
- const cfgModels = (lkgR.effective.models?.models ?? []).filter((m) => m.enabled !== false).length;
320
- logger.info("config_loaded", { source: "lkg", version: lkgR.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels });
321
- };
322
- try {
323
- const bootFetch = configProvider.fetchEffective(undefined);
324
- const raced = await raceBootFetch(bootFetch, config.configBootFetchBudgetMs);
325
- let lkgFallback = null;
326
- if (raced === BOOT_FETCH_DEFERRED) {
327
- bootConfigPending = bootFetch;
328
- bootFetch.catch(() => { });
329
- lkgFallback = await bootFromLkg("boot fetch deferred (budget exceeded)");
330
- lkgBooted = lkgFallback !== null;
331
- logger.warn("config_boot_fetch_deferred", {
332
- source: configProvider.kind,
333
- budgetMs: config.configBootFetchBudgetMs,
334
- lkg: lkgBooted,
335
- note: lkgBooted
336
- ? "center slow — booting from the persisted LKG; the pull continues in the background and reconciles via the refresh path on arrival"
337
- : "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
- });
339
- }
340
- if (raced === BOOT_FETCH_DEFERRED && lkgFallback) {
341
- try {
342
- await bootApplyLkgInline(lkgFallback);
343
- }
344
- catch (applyErr) {
345
- lkgBooted = false;
346
- logger.warn("config_lkg_apply_failed", { err: String(applyErr), note: "persisted LKG failed to apply — env fallback serves; the cache rewrites on the next clean candidate" });
347
- }
348
- }
349
- const r = raced === BOOT_FETCH_DEFERRED ? null : raced;
350
- if (r) {
351
- const bootPromptsRaw = r.effective.prompts;
352
- const bootPromptsOk = bootPromptsRaw === undefined || validatePromptsDomain(bootPromptsRaw).ok;
353
- const bootClean = (r.domainErrors ?? []).length === 0 && bootPromptsOk;
354
- if (bootClean)
355
- ccEtag = r.etag;
356
- for (const de of r.domainErrors ?? [])
357
- 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
- if (dryRun) {
359
- logEffectiveDiff(config, r.effective, logger);
360
- }
361
- 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;
367
- if (bootClean)
368
- latestEffective = r.effective;
369
- if (bootClean && !lkgBooted)
370
- bootLkgCandidate = { effective: r.effective, etag: r.etag };
371
- const cfgModels = (r.effective.models?.models ?? []).filter((m) => m.enabled !== false).length;
372
- const cfgDir = configProvider.kind === "local" ? { dir: configProvider.root } : {};
373
- logger.info("config_loaded", { source: configProvider.kind, version: r.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels, ...cfgDir });
374
- if (configProvider.kind === "local" && cfgModels === 0)
375
- logger.warn("config_local_no_models", { ...cfgDir, hint: "config.d/models.json 空或 CONFIG_LOCAL_DIR 指错目录(选目录旋钮是 CONFIG_LOCAL_DIR,不是 LOCAL_DATA_ROOT)— 模型继续走 env 兜底" });
376
- }
377
- }
378
- }
379
- catch (err) {
380
- logger.warn("config_source_unreachable_using_env", { source: configProvider.kind, err: String(err) });
381
- const lkgFallback = await bootFromLkg(`boot fetch failed: ${String(err).slice(0, 120)}`);
382
- if (lkgFallback) {
383
- lkgBooted = true;
384
- try {
385
- await bootApplyLkgInline(lkgFallback);
386
- }
387
- catch (applyErr) {
388
- lkgBooted = false;
389
- logger.warn("config_lkg_apply_failed", { err: String(applyErr), note: "persisted LKG failed to apply — env fallback serves" });
390
- }
391
- }
392
- }
393
- }
394
- if (!modelReadyState.ready)
395
- 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" });
396
- if (configProvider?.kind === "local") {
397
- try {
398
- const fileRx = await loadRemoteExec(localRoot);
399
- if (fileRx) {
400
- if (fileRx.provider === "host") {
401
- config.remoteExec = {
402
- provider: "host",
403
- ...(fileRx.workdir ? { workspaceBase: fileRx.workdir } : {}),
404
- ...(fileRx.commandTimeoutMs != null ? { commandTimeoutMs: fileRx.commandTimeoutMs } : {}),
405
- };
406
- logger.info("remote_exec_from_file", { provider: "host", root: localRoot });
407
- }
408
- else if (fileRx.provider === "local-docker") {
409
- config.remoteExec = {
410
- provider: "local-docker",
411
- image: fileRx.image,
412
- ...(fileRx.mountPath ? { mountPath: fileRx.mountPath } : fileRx.workdir ? { mountPath: fileRx.workdir } : {}),
413
- ...(fileRx.memory ? { memory: fileRx.memory } : {}),
414
- ...(fileRx.cpus != null ? { cpus: fileRx.cpus } : {}),
415
- ...(fileRx.network ? { network: fileRx.network } : {}),
416
- };
417
- logger.info("remote_exec_from_file", { provider: "local-docker", root: localRoot, image: fileRx.image });
418
- }
419
- else {
420
- logger.warn("remote_exec_file_isolated_lane_unwired", { provider: fileRx.provider, note: "e2b/k8s/ssh/adb are sourced from REMOTE_EXEC env until file-source NAME→value resolution lands (center field map)" });
421
- }
422
- }
423
- }
424
- catch (err) {
425
- logger.error("remote_exec_file_invalid", { root: localRoot, err: String(err) });
426
- throw err;
427
- }
428
- }
429
- let backend;
430
- let storeBackendDegraded = false;
431
- const wantDb = config.sessionBackend === "tidb" ||
432
- config.dbBackend === "local" ||
433
- (config.sessionBackend === "auto" && !!(config.tidb || config.pg));
434
- if (wantDb) {
435
- const opened = await openStoreBackendWithFallback(config, logger);
436
- backend = opened.backend;
437
- storeBackendDegraded = opened.degraded;
438
- }
439
- metrics.setGauge("store_backend_degraded", storeBackendDegraded ? 1 : 0);
440
- {
441
- const minioReq = ["MINIO_ENDPOINT", "MINIO_ACCESS_KEY", "MINIO_SECRET_KEY"];
442
- const present = minioReq.filter((v) => (process.env[v] ?? "") !== "");
443
- if (present.length > 0 && !config.snapshotBlobStore) {
444
- logger.warn("snapshot_blob_store_partial_config_fallback_sql", { present, missing: minioReq.filter((v) => !present.includes(v)) });
445
- }
446
- metrics.setGauge("snapshot_blob_backend", 1, { backend: config.snapshotBlobStore ? "minio" : "sql" });
447
- }
448
- if (config.sessionBackend === "auto")
449
- config.sessionBackend = backend ? "tidb" : "memory";
450
- if (config.dbBackend === "local" && backend)
451
- config.sessionBackend = "tidb";
452
- let memoryEngine;
453
- let memorySyncCursors;
454
- if (config.memoryEngineBackend !== "file" && config.memoryEngineEnabled) {
455
- const dialect = config.memoryEngineBackend;
456
- const countedHistorySink = (store) => ({
457
- appendHistory: async (rows) => {
458
- try {
459
- await store.appendHistory(rows);
460
- }
461
- catch (err) {
462
- metrics.inc("memory_history_append_failed_total", { backend: dialect }, rows.length);
463
- throw err;
464
- }
465
- },
466
- });
467
- if (dialect === "pg") {
468
- const pool = backend?.pgPool();
469
- if (!pool)
470
- throw new Error("MEMORY_ENGINE_BACKEND=pg requires DB_BACKEND=pg (the memory engine binds the same PG pool) — refusing to start half-configured");
471
- const q = async (text, params) => { const r = await pool.query(text, params); return { rows: r.rows }; };
472
- await ensurePgMemoryEngineSchema(q);
473
- await ensurePgMemoryHistorySchema(q);
474
- await ensurePgMemorySyncSchema(q);
475
- memorySyncCursors = new PgMemorySyncStore(q);
476
- const pgMem = new PgMemoryEngineBackend(q, { history: countedHistorySink(new PgMemoryHistoryStore(q)) });
477
- memoryEngine = { backend: pgMem, root: join(config.localDataRoot ?? localRoot, "memory-work") };
478
- }
479
- else {
480
- const pool = backend?.mysqlPool();
481
- if (!pool)
482
- throw new Error("MEMORY_ENGINE_BACKEND=tidb requires DB_BACKEND=mysql/tidb — refusing to start half-configured");
483
- await ensureTiDBMemoryEngineSchema(pool);
484
- await ensureTiDBMemoryHistorySchema(pool);
485
- await ensureTiDBMemorySyncSchema(pool);
486
- memorySyncCursors = new TiDBMemorySyncStore(pool);
487
- memoryEngine = { backend: new TiDBMemoryEngineBackend(pool, { history: countedHistorySink(new TiDBMemoryHistoryStore(pool)) }), root: join(config.localDataRoot ?? localRoot, "memory-work") };
488
- }
489
- logger.info("memory_engine_enabled", { enabled: true, backend: dialect, multiTenant: config.requirePrincipal === true, workRoot: memoryEngine.root });
490
- }
491
- else {
492
- memoryEngine = memoryEngineBackendFor(config);
493
- if (memoryEngine && process.env.LOCAL_DATA_ROOT && !config.memoryEngineDir) {
494
- logger.warn("memory_engine_dir_defaulted", {
495
- root: memoryEngine.root,
496
- hint: "LOCAL_DATA_ROOT is set but MEMORY_ENGINE_DIR is not — the memory engine is sharing the machine-wide default; set MEMORY_ENGINE_DIR to isolate it under your data root",
497
- });
498
- }
499
- }
500
- let rosterStore;
501
- {
502
- const mysqlPool = backend?.mysqlPool?.();
503
- const pgPool = backend?.pgPool?.();
504
- if (pgPool) {
505
- const q = async (text, params) => { const r = await pgPool.query(text, params); return { rows: r.rows }; };
506
- await ensurePgRosterSchema(q);
507
- rosterStore = new PgRosterStore(q);
508
- }
509
- else if (mysqlPool) {
510
- await ensureTiDBRosterSchema(mysqlPool);
511
- rosterStore = new TiDBRosterStore(mysqlPool);
512
- }
513
- else if (backend?.kind === "local") {
514
- rosterStore = new FileRosterStore(join(config.localDataRoot ?? localRoot, "roster.json"));
515
- }
516
- if (rosterStore)
517
- logger.info("roster_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
518
- }
519
- let backgroundAgentStore;
520
- {
521
- const mysqlPool = backend?.mysqlPool?.();
522
- const pgPool = backend?.pgPool?.();
523
- if (pgPool) {
524
- const q = async (text, params) => { const r = await pgPool.query(text, params); return { rows: r.rows }; };
525
- await ensurePgBackgroundAgentSchema(q);
526
- backgroundAgentStore = new PgBackgroundAgentStore(q);
527
- }
528
- else if (mysqlPool) {
529
- await ensureTiDBBackgroundAgentSchema(mysqlPool);
530
- backgroundAgentStore = new TiDBBackgroundAgentStore(mysqlPool);
531
- }
532
- else if (backend?.kind === "local") {
533
- backgroundAgentStore = new FileBackgroundAgentStore(config.localDataRoot ?? localRoot);
534
- }
535
- if (backgroundAgentStore)
536
- logger.info("background_agent_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
537
- }
538
- let taskAttachmentStore;
539
- {
540
- const mysqlPool = backend?.mysqlPool?.();
541
- const pgPool = backend?.pgPool?.();
542
- if (pgPool || mysqlPool) {
543
- const minio = config.snapshotBlobStore;
544
- if (!minio) {
545
- logger.error("attachments_disabled_object_store_required", {
546
- hint: "cloud deployments must configure object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) — attachment routes will 501 until it is set",
547
- });
548
- }
549
- else {
550
- const bytes = new MinioBlobBackend({ ...minio, keyPrefix: `${minio.keyPrefix ?? ""}attachments/` });
551
- if (pgPool) {
552
- const q = async (text, params) => { const r = await pgPool.query(text, params); return { rows: r.rows }; };
553
- await ensurePgTaskAttachmentSchema(q);
554
- taskAttachmentStore = new PgTaskAttachmentStore(q, bytes);
555
- }
556
- else {
557
- await ensureTiDBTaskAttachmentSchema(mysqlPool);
558
- taskAttachmentStore = new TiDBTaskAttachmentStore(mysqlPool, bytes);
559
- }
560
- }
561
- }
562
- else if (backend?.kind === "local") {
563
- taskAttachmentStore = new LocalTaskAttachmentStore(config.localDataRoot ?? localRoot);
564
- }
565
- if (taskAttachmentStore)
566
- logger.info("task_attachment_store_enabled", { backend: pgPool ? "pg+minio" : mysqlPool ? "tidb+minio" : "file", maxBytes: config.attachmentMaxBytes });
567
- }
568
- let mailboxStore;
569
- {
570
- const mysqlPool = backend?.mysqlPool?.();
571
- const pgPool = backend?.pgPool?.();
572
- if (pgPool) {
573
- await ensurePgMailboxSchema(async (text, params) => pgPool.query(text, params));
574
- mailboxStore = new PgMailboxStore(pgPool);
575
- }
576
- else if (mysqlPool) {
577
- await ensureTiDBMailboxSchema(mysqlPool);
578
- mailboxStore = new TiDBMailboxStore(mysqlPool);
579
- }
580
- else if (backend?.kind === "local") {
581
- mailboxStore = new FileMailboxStore(config.localDataRoot ?? localRoot);
582
- }
583
- if (mailboxStore)
584
- logger.info("mailbox_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
585
- }
586
- const memoryExportBackend = config.memoryEngineBackend !== "file" ? memoryEngine?.backend : undefined;
587
- const memoryLane = memoryEngineRemoteLanePosture(config);
588
- if (config.memoryEngineBackend === "file" || !config.memoryEngineEnabled) {
589
- logger.info("memory_engine_enabled", memoryEngine
590
- ? { enabled: true, backend: "file", dir: memoryEngine.root }
591
- : {
592
- enabled: false,
593
- reason: config.requirePrincipal === true ? "multi-tenant on the FILE backend (no tenant isolation; set MEMORY_ENGINE_BACKEND=pg|tidb to light up)"
594
- : !config.memoryEngineEnabled ? "MEMORY_ENGINE=off"
595
- : `remote exec lane "${memoryLane?.lane}" (worker/sandbox file planes split; MEMORY_ENGINE_REMOTE_LANE=allow overrides)`,
596
- });
597
- }
598
- if (memoryLane && config.memoryEngineEnabled && config.requirePrincipal !== true) {
599
- logger.warn("memory_engine_remote_lane", memoryLane.posture === "dark"
600
- ? { 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
- : { 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
- }
603
- let memorySyncRunner;
604
- if (config.memorySync) {
605
- if (memoryEngine) {
606
- memorySyncRunner = createMemorySyncRunner({
607
- backend: memoryEngine.backend,
608
- scope: config.memorySync.scope,
609
- memoryRoot: memoryEngine.root,
610
- transport: createMemorySyncTransport({ url: config.memorySync.url, token: config.memorySync.token }),
611
- log: logger,
612
- ...(config.memorySync.maxPushEntries !== undefined ? { maxPushEntries: config.memorySync.maxPushEntries } : {}),
613
- ...(config.memorySync.maxPullEntries !== undefined ? { maxPullEntries: config.memorySync.maxPullEntries } : {}),
614
- });
615
- logger.info("memory_sync_enabled", { url: config.memorySync.url, scope: config.memorySync.scope, cursorPath: memorySyncRunner.cursorPath });
616
- memorySyncRunner.trigger("boot");
617
- }
618
- else {
619
- 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
- }
621
- }
622
- const sessionStore = createSessionStore(config, backend, metrics);
623
- if (config.requirePrincipal && !sessionStore.ownerOf) {
624
- throw new Error("REQUIRE_PRINCIPAL=true needs an owner-aware session store (SESSION_BACKEND=tidb); " +
625
- "the in-memory store cannot enforce session ownership.");
626
- }
627
- if (config.requirePrincipal && backend?.kind === "local") {
628
- throw new Error("REQUIRE_PRINCIPAL=true is not supported on the local file backend (DB_BACKEND=local): session/run CONTENT is " +
629
- "durable there, but OWNER attribution is process-local and lost on restart (store-backend.ts §0.5 — durable " +
630
- "owners = P1), so it cannot durably enforce multi-tenant session ownership. Use a SQL backend " +
631
- "(DB_BACKEND=mysql|pg) for multi-tenant, or run local single-user with REQUIRE_PRINCIPAL=false (a BFF may " +
632
- "still inject x-agent-principal per request for memory scoping and audit attribution).");
633
- }
634
- if (backend)
635
- assertCloudSnapshotBlobPosture(backend.kind, config);
636
- if (!config.requirePrincipal && sessionStore.ownerOf) {
637
- logger.warn("principal_optional", {
638
- 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
- });
640
- }
641
- metrics.dynamicGauge("sessions_warm", "Sessions held in the warm cache", () => sessionStore.size);
642
- const breakerState = backend?.breaker && config.resilience.circuitBreaker
643
- ? backend
644
- .breaker((streak) => {
645
- metrics.setGauge("counter_flush_fail_streak", streak, { table: "circuit_breaker", kind: "write_through" });
646
- if (streak > 0)
647
- metrics.inc("breaker_writethrough_failed_total", { backend: backend.kind });
648
- if (streak === 3)
649
- logger.warn("breaker_writethrough_degraded", { streak });
650
- else if (streak === 0)
651
- logger.info("breaker_writethrough_recovered", {});
652
- })
653
- .startRefresh()
654
- : undefined;
655
- const brain = createBrain(config, { breakerState });
656
- const pricing = buildPricing(config.models);
657
- const counterDegradeHook = (info) => {
658
- metrics.setGauge("counter_flush_fail_streak", info.streak, { table: info.table, kind: info.kind });
659
- if (info.streak > 0)
660
- metrics.inc("counter_flush_failed_total", { table: info.table, kind: info.kind });
661
- if (info.streak === 3)
662
- logger.warn("counter_flush_degraded", { table: info.table, kind: info.kind, streak: info.streak, ...(info.error ? { error: info.error } : {}) });
663
- else if (info.streak === 0 && info.prevStreak >= 3)
664
- logger.info("counter_flush_recovered", { table: info.table, kind: info.kind, prevStreak: info.prevStreak });
665
- };
666
- const costQuota = config.maxPrincipalCostUsd > 0
667
- ? backend?.costQuota
668
- ? backend.costQuota(Math.round(config.maxPrincipalCostUsd * 1e6), config.costQuotaWindowSec * 1000, counterDegradeHook).startRefresh()
669
- : new CostQuota(Math.round(config.maxPrincipalCostUsd * 1e6), config.costQuotaWindowSec * 1000)
670
- : undefined;
671
- const modelUsageTracker = new ModelUsageTracker();
672
- const promptManifestTracker = new PromptManifestTracker();
673
- const fleetUsage = config.configCenter && process.env.FLEET_ADVERTISE_ADDRESS ? new FleetUsageAccumulator() : undefined;
674
- const fleetLease = createFleetLeaseFromEnv(config, { logger, metrics });
675
- const tracer = createTracer(metrics, costQuota, modelUsageTracker, fleetUsage, fleetLease, (m) => config.modelQuotaWeights[m] ?? 1, promptManifestTracker);
676
- const sideQueryAccounting = createSideQueryAccountant(metrics, costQuota, fleetUsage, fleetLease, (m) => config.modelQuotaWeights[m] ?? 1);
677
- const toolResultStore = backend?.toolResult ? backend.toolResult() : undefined;
678
- const sessionPolicyStore = backend ? backend.sessionPolicy() : undefined;
679
- const fileSnapshotStore = backend ? backend.fileSnapshot() : undefined;
680
- if (fileSnapshotStore && "onSkippedVanished" in fileSnapshotStore) {
681
- fileSnapshotStore.onSkippedVanished = (count) => metrics.inc("snapshot_skipped_vanished_total", {}, count);
682
- }
683
- const perTaskImage = new PerTaskImageRegistry();
684
- const sessionEnvSelection = new SessionEnvironmentSelection();
685
- const MAX_CWD_SESSIONS = 4096;
686
- const perSessionCwd = new Map();
687
- const setSessionCwd = (sid, cwd) => {
688
- perSessionCwd.delete(sid);
689
- perSessionCwd.set(sid, cwd);
690
- if (perSessionCwd.size > MAX_CWD_SESSIONS)
691
- perSessionCwd.delete(perSessionCwd.keys().next().value);
692
- };
693
- const perSessionShellEnv = new Map();
694
- const setSessionShellEnv = (sid, env) => {
695
- perSessionShellEnv.delete(sid);
696
- perSessionShellEnv.set(sid, env);
697
- if (perSessionShellEnv.size > MAX_CWD_SESSIONS)
698
- perSessionShellEnv.delete(perSessionShellEnv.keys().next().value);
699
- };
700
- let executionEnvFactory;
701
- let worktreeReap;
702
- if (config.remoteExec?.provider === "e2b") {
703
- executionEnvFactory = e2bExecutionEnvFactory({
704
- apiKey: config.remoteExec.apiKey,
705
- ...(config.remoteExec.template ? { template: config.remoteExec.template } : {}),
706
- ...(config.remoteExec.timeoutMs != null ? { timeoutMs: config.remoteExec.timeoutMs } : {}),
707
- ...(config.remoteExec.livenessMs != null ? { livenessMs: config.remoteExec.livenessMs } : {}),
708
- ...(config.remoteExec.allowInternetAccess != null ? { allowInternetAccess: config.remoteExec.allowInternetAccess } : {}),
709
- ...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
710
- ...(() => {
711
- const merged = { ...derivePkgSourceEnv(config.sandboxPkgSource, customPkgSourceFromEnv()), ...(config.remoteExec.sandboxEnv ?? {}) };
712
- return Object.keys(merged).length > 0 ? { sandboxEnv: merged } : {};
713
- })(),
714
- logger,
715
- metrics,
716
- });
717
- }
718
- else if (config.remoteExec?.provider === "k8s") {
719
- const k8sCfg = {
720
- image: config.remoteExec.image,
721
- ...(config.remoteExec.apiUrl ? { apiUrl: config.remoteExec.apiUrl } : {}),
722
- ...(config.remoteExec.token ? { token: config.remoteExec.token } : {}),
723
- ...(config.remoteExec.caCert ? { caCert: config.remoteExec.caCert } : {}),
724
- ...(config.remoteExec.insecureTls ? { insecureTls: true } : {}),
725
- ...(config.remoteExec.namespace ? { namespace: config.remoteExec.namespace } : {}),
726
- ...(config.remoteExec.runtimeClass != null ? { runtimeClass: config.remoteExec.runtimeClass } : {}),
727
- ...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
728
- ...(config.remoteExec.timeoutMs != null ? { timeoutMs: config.remoteExec.timeoutMs } : {}),
729
- ...(config.remoteExec.memory ? { memory: config.remoteExec.memory } : {}),
730
- ...(config.remoteExec.cpu ? { cpu: config.remoteExec.cpu } : {}),
731
- ...(config.remoteExec.s3Snapshot ? { s3Snapshot: config.remoteExec.s3Snapshot } : {}),
732
- ...(() => {
733
- const derived = derivePkgSourceEnv(config.sandboxPkgSource, customPkgSourceFromEnv());
734
- return Object.keys(derived).length > 0 ? { podEnv: derived } : {};
735
- })(),
736
- };
737
- executionEnvFactory = (ctx) => k8sExecutionEnvFactory({ ...k8sCfg, image: perTaskImage.get(ctx.sessionId) ?? k8sCfg.image })(ctx);
738
- }
739
- else if (config.remoteExec?.provider === "ssh") {
740
- executionEnvFactory = sshExecutionEnvFactory({
741
- host: config.remoteExec.host,
742
- username: config.remoteExec.username,
743
- privateKey: config.remoteExec.privateKey,
744
- ...(config.remoteExec.port != null ? { port: config.remoteExec.port } : {}),
745
- ...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
746
- });
747
- }
748
- else if (config.remoteExec?.provider === "adb") {
749
- executionEnvFactory = adbExecutionEnvFactory({
750
- serial: config.remoteExec.serial,
751
- ...(config.remoteExec.adbPath ? { adbPath: config.remoteExec.adbPath } : {}),
752
- ...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
753
- });
754
- }
755
- else if (config.remoteExec?.provider === "host") {
756
- {
757
- const hostScheduler = config.schedulerEnabled && config.requirePrincipal !== true
758
- ? new FileSchedulerBackend({
759
- ...(config.schedulerStorePath ? { storePath: config.schedulerStorePath } : {}),
760
- ...(config.schedulerSessionWakeup === false ? { caps: { supportsSessionWakeup: false } } : {}),
761
- })
762
- : undefined;
763
- if (hostScheduler)
764
- logger.info(`R7: self-wake scheduler enabled (host lane), store=${config.schedulerStorePath ?? defaultSchedulerStorePath()}`);
765
- const hostCfg = {
766
- ...(config.remoteExec.workspaceBase ? { workspaceBase: config.remoteExec.workspaceBase } : {}),
767
- ...(config.remoteExec.commandTimeoutMs != null ? { commandTimeoutMs: config.remoteExec.commandTimeoutMs } : {}),
768
- ...(hostScheduler ? { scheduler: hostScheduler } : {}),
769
- backgroundShell: config.requirePrincipal !== true,
770
- };
771
- executionEnvFactory = (ctx) => {
772
- const cwd = effectiveHostWorkspace(perSessionCwd.get(ctx.sessionId), ctx);
773
- const shellEnv = perSessionShellEnv.get(ctx.sessionId);
774
- const env = hostExecutionEnvFactory({
775
- ...hostCfg,
776
- ...(cwd ? { workspaceDir: cwd } : {}),
777
- ...(shellEnv ? { env: shellEnv } : {}),
778
- })(ctx);
779
- return lspManager && !cwd ? evictLspOnDestroy(env, lspManager) : env;
780
- };
781
- }
782
- }
783
- else if (config.remoteExec?.provider === "local-docker") {
784
- executionEnvFactory = localDockerExecutionEnvFactory({
785
- image: config.remoteExec.image,
786
- ...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
787
- ...(config.remoteExec.dockerPath ? { dockerPath: config.remoteExec.dockerPath } : {}),
788
- ...(config.remoteExec.dockerHost ? { dockerHost: config.remoteExec.dockerHost } : {}),
789
- ...(config.remoteExec.memory ? { memory: config.remoteExec.memory } : {}),
790
- ...(config.remoteExec.cpus != null ? { cpus: config.remoteExec.cpus } : {}),
791
- ...(config.remoteExec.network ? { network: config.remoteExec.network } : {}),
792
- ...(config.remoteExec.commandTimeoutMs != null ? { commandTimeoutMs: config.remoteExec.commandTimeoutMs } : {}),
793
- ...(config.remoteExec.env ? { env: config.remoteExec.env } : {}),
794
- });
795
- }
796
- if (executionEnvFactory && isRemoteScratchpadLane(config.remoteExec?.provider)) {
797
- executionEnvFactory = withRemoteScratchpad(executionEnvFactory, logger);
798
- }
799
- if (config.worktreeIsolation && executionEnvFactory) {
800
- const wtRoot = config.worktreeIsolation.repoRoot;
801
- const wtValid = existsSync(wtRoot) && existsSync(join(wtRoot, ".git"));
802
- if (config.remoteExec?.provider === "host" && !wtValid) {
803
- logger.error("worktree_isolation_disabled_bad_repo_root", {
804
- repoRoot: wtRoot,
805
- reason: existsSync(wtRoot) ? "not a git repository (no .git)" : "path does not exist",
806
- note: "WORKTREE_ISOLATION_ENABLED is set but WORKTREE_REPO_ROOT is not a usable git repo — isolation left OFF; isolation:'worktree' agents run on the shared base env. Fix the path or unset the flag.",
807
- });
808
- }
809
- else if (config.remoteExec?.provider === "host") {
810
- const gitBaseEnv = new RemoteHostExecutionEnv({ workspaceDir: config.worktreeIsolation.repoRoot, inheritEnv: "all", backgroundShell: config.requirePrincipal !== true });
811
- executionEnvFactory = withWorktreeIsolation(executionEnvFactory, {
812
- repoRoot: config.worktreeIsolation.repoRoot,
813
- ...(config.worktreeIsolation.allowedRoots ? { allowedRoots: config.worktreeIsolation.allowedRoots } : {}),
814
- ...(config.worktreeIsolation.commit ? { commit: config.worktreeIsolation.commit } : {}),
815
- baseEnvForGit: gitBaseEnv,
816
- rootEnvAt: (dir) => {
817
- const env = new RemoteHostExecutionEnv({ workspaceDir: dir, backgroundShell: config.requirePrincipal !== true });
818
- return lspManager ? evictLspOnDestroy(env, lspManager) : env;
819
- },
820
- logger,
821
- });
822
- worktreeReap = () => reapOrphanWorktrees(gitBaseEnv, config.worktreeIsolation.repoRoot, logger);
823
- logger.info("worktree_isolation_enabled", { provider: "host", repoRoot: config.worktreeIsolation.repoRoot });
824
- void worktreeReap();
825
- }
826
- else {
827
- logger.warn("worktree_isolation_unsupported_lane", {
828
- provider: config.remoteExec?.provider,
829
- 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
- });
831
- }
832
- }
833
- const sendUserFileTaskEnvs = (() => {
834
- const provider = config.remoteExec?.provider;
835
- if (!config.sendUserFile || !executionEnvFactory || !sandboxSendLaneEnabled(provider, config.requirePrincipal))
836
- return undefined;
837
- const registry = new TaskEnvRegistry();
838
- executionEnvFactory = registry.wrapFactory(executionEnvFactory);
839
- return registry;
840
- })();
841
- if (taskAttachmentStore && executionEnvFactory) {
842
- const inner = executionEnvFactory;
843
- const attStore = taskAttachmentStore;
844
- executionEnvFactory = async (ctx) => {
845
- const env = await inner(ctx);
846
- const n = await materializeAttachmentsInto(env, attStore, ctx.sessionId);
847
- if (n > 0)
848
- logger.info("attachments_materialized", { sessionId: ctx.sessionId, count: n });
849
- return env;
850
- };
851
- }
852
- if (executionEnvFactory) {
853
- const isolated = config.remoteExec.provider === "e2b" ||
854
- config.remoteExec.provider === "k8s" ||
855
- config.remoteExec.provider === "local-docker";
856
- const suspendable = config.remoteExec.provider === "e2b" ||
857
- (config.remoteExec.provider === "k8s" && !!config.remoteExec.s3Snapshot);
858
- const sandboxEnvKeys = config.remoteExec.provider === "e2b" ? Object.keys(config.remoteExec.sandboxEnv ?? {}) : [];
859
- logger.info("remote_exec_enabled", { provider: config.remoteExec.provider, isolated, suspendable, ...(sandboxEnvKeys.length > 0 ? { sandboxEnvKeys } : {}) });
860
- if (!isolated)
861
- 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
- }
863
- const lspProvider = config.remoteExec?.provider;
864
- const lspManager = (lspProvider === "e2b" || lspProvider === "k8s") && config.lspEnabled
865
- ? createE2bLspManager({ log: (event, fields) => logger.info(event, fields), scheme: lspProvider === "k8s" ? "ws" : "wss" })
866
- :
867
- lspProvider === "host" && config.lspHostEnabled && config.requirePrincipal !== true
868
- ? new NodeLspManager({ log: (event, fields) => logger.info(event, fields) })
869
- : undefined;
870
- if (lspManager)
871
- logger.info("lsp_enabled", { provider: lspProvider });
132
+ // design/158 A10 尾刀:config-center 的 boot 半场(sealed-key 托管 → provider → prompt-epoch 店 →
133
+ // effective 拉取 / LKG 兜底 / tolerant apply)搬到 src/boot/config-center.ts。该段与 refresh 循环共享的
134
+ // 12 个跨段可变 `let` 现在是那只工厂的闭包私有字段(每一处读写的文本与次序逐字保留,见该文件头注);
135
+ // main 只经窄口读:getKeyResolver/getCenterPrompts/modelReady/restartState/planeDeferredState/promptSource。
136
+ // ⚠️ 位置即契约:必须在 openStores 之前 —— models/roles/teams 要在 Runner/持久层建起来之前应用进 config。
137
+ const configCenter = await createConfigCenterRuntime({ config, logger, metrics, localRoot });
138
+ // design/158 A10 尾刀:local 形的 config.d/remote-exec.json 覆写搬到 boot/config-center.ts(逐字)。
139
+ // ⚠️ 位置即契约:就地改写 config.remoteExec,必须早于 createExecutionEnv 这个消费点。
140
+ await configCenter.applyLocalRemoteExec();
141
+ // design/158 A10:持久层装配搬到 src/boot/stores.ts(逐字)。⚠️ 该段就地归一 `config.sessionBackend`
142
+ // 且承载三条 fail-loud 拒启断言 —— 位置即契约,理由见该文件头注。
143
+ const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, } = await openStores({ config, logger, metrics, localRoot });
144
+ // design/158 A10:计费/追踪/预算装配搬到 src/boot/budget-tracing.ts(逐字;tracer side-query sink 实例的
145
+ // 「同段构造」契约见该文件头注)。
146
+ const { brain, pricing, counterDegradeHook, costQuota, modelUsageTracker, promptManifestTracker, fleetUsage, fleetLease, tracer, sideQueryAccounting, toolResultStore, sessionPolicyStore, fileSnapshotStore, } = createBudgetAndTracing({ config, logger, metrics, backend, breakerState });
147
+ // design/158 A10:执行环境装配搬到 src/boot/execution-env.ts(逐字)。
148
+ // ⚠️ 工厂装饰顺序=行为(scratchpad worktree SendUserFile 登记 附件物化最外层),见该文件头注。
149
+ const { perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv, executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, } = createExecutionEnv({ config, logger, metrics, taskAttachmentStore });
150
+ // prove-before-scale stopgap: a no-op tool tracer (logs every tool call incl. resumed legs that
151
+ // /events hides) so we can diagnose single-agent coding behaviour. Off unless TOOL_TRACE=true.
872
152
  const toolTracer = config.toolTrace ? createToolTracer(logger) : undefined;
873
153
  if (toolTracer)
874
154
  logger.info("tool_trace_enabled", {});
155
+ // Goal B4 (2026-07-08): the ALWAYS-ON gate-deny meter (`permission_denied_total{source}`) — an
156
+ // operational/security signal that must not sit behind the TOOL_TRACE diagnostic switch. Composed
157
+ // UNDER the tracer (deployment slot order: meter first, tracer second — composeHooks double-runs the
158
+ // void permissionDenied slot); this composite is what every hooks site below uses as the deployment
159
+ // baseline, so a task-hooks fold (resolveSpec) shadows neither.
875
160
  const deploymentHooks = toolTracer ? composeHooks(createPermissionDeniedMeter(metrics), toolTracer) : createPermissionDeniedMeter(metrics);
876
- const sqlWorkflowRunStore = config.workflowRunStoreBackend === "auto" ? backend?.workflowRun?.() : undefined;
877
- const baseWorkflowRunStore = config.selfOrchestrationEnabled
878
- ? config.workflowRunStoreBackend === "memory"
879
- ? new InMemoryWorkflowRunStore()
880
- : (sqlWorkflowRunStore ?? new FileWorkflowRunStore(join(config.localDataRoot ?? localRoot, "workflows")))
881
- : undefined;
882
- const workflowNotifyJournal = config.selfOrchestrationEnabled && config.workflowRunStoreBackend !== "memory"
883
- ? (sqlWorkflowRunStore ? backend.notifyJournal() : new FileWorkflowNotifyJournalStore(join(config.localDataRoot ?? localRoot, "workflows")))
884
- : undefined;
885
- const workflowCompletionInbox = config.selfOrchestrationEnabled
886
- ? config.workflowRunStoreBackend === "memory"
887
- ? new InMemoryWorkflowCompletionInbox((msg, meta) => logger.warn(msg, meta))
888
- : (sqlWorkflowRunStore ? backend.completionInbox((msg, meta) => logger.warn(msg, meta)) : new FileWorkflowCompletionInbox(join(config.localDataRoot ?? localRoot, "workflows"), (msg, meta) => logger.warn(msg, meta)))
889
- : undefined;
890
- const deliverWorkflowCompletion = async (p) => {
891
- logger.info("workflow_completed", {
892
- runId: p.runId,
893
- status: p.status,
894
- ...(p.sourceTaskId ? { sourceTaskId: p.sourceTaskId } : {}),
895
- });
896
- metrics.inc("workflow_runs_total", { status: p.status });
897
- if (!workflowCompletionInbox)
898
- return;
899
- try {
900
- const route = await resolveCompletionRoute(p, runStore ? (id) => runStore.getRun(id) : undefined, config.requirePrincipal === true);
901
- if (!route)
902
- return;
903
- await workflowCompletionInbox.enqueue({
904
- sessionId: route.sessionId,
905
- owner: route.owner,
906
- runId: p.runId,
907
- status: p.status,
908
- summary: p.summary,
909
- enqueuedAt: Date.now(),
910
- });
911
- }
912
- catch (err) {
913
- logger.warn("workflow_completion_enqueue_failed", { runId: p.runId, err: String(err) });
914
- }
915
- };
916
- const workflowNotifyGate = baseWorkflowRunStore && workflowNotifyJournal
917
- ? new WorkflowNotifyGate(workflowNotifyJournal, baseWorkflowRunStore, deliverWorkflowCompletion, {
918
- onError: (stage, runId, err) => logger.warn("workflow_notify_failed", { stage, runId, err: String(err) }),
919
- })
920
- : undefined;
921
- const fleetBus = new FleetEventBus();
922
- const workflowRunStore = baseWorkflowRunStore
923
- ? new JournalingWorkflowRunStore(baseWorkflowRunStore, workflowNotifyGate, fleetBus)
924
- : undefined;
925
- const workflowJournalStore = config.selfOrchestrationEnabled && backend ? backend.workflowJournal() : undefined;
926
- const outcomeSink = backend?.outcomeSink();
927
- const workflowAgentRegistry = config.selfOrchestrationEnabled ? new WorkflowAgentRegistry() : undefined;
928
- const subagentSteerRegistry = new SubagentSteerRegistry();
929
- const elicitation = config.mcpElicitation.enabled ? new ElicitationCoordinator(config.mcpElicitation.throttle) : undefined;
930
- const question = config.askQuestionEnabled ? new QuestionCoordinator() : undefined;
931
- const toolApproval = config.toolApprovalEnabled ? new ToolApprovalCoordinator() : undefined;
932
- const durableEnabled = backend?.checkpoint !== undefined && config.durableApproval === true;
933
- const sendUserFileEmitter = (() => {
934
- if (!config.sendUserFile)
935
- return undefined;
936
- if (sendUserFileTaskEnvs)
937
- return new SendUserFileEmitter();
938
- const provider = config.remoteExec?.provider ?? "host";
939
- if (provider === "host" && config.requirePrincipal !== true)
940
- return new SendUserFileEmitter();
941
- return undefined;
942
- })();
943
- const sendFileLedger = config.sendUserFile && backend ? backend.sendFileLedger() : undefined;
944
- const sendUserFileToolSpec = (() => {
945
- if (!sendUserFileEmitter || !config.sendUserFile)
946
- return undefined;
947
- const issuer = createSendUserFileIssuer(config.sendUserFile);
948
- const rawSend = sendUserFileTaskEnvs
949
- ? createSandboxFileSend({
950
- registry: sendUserFileTaskEnvs,
951
- prepare: (filename, scope) => issuer.prepareDirectUpload(filename, undefined, scope),
952
- maxBytes: SEND_USER_FILE_MAX_BYTES,
953
- })
954
- : async (p, sendCtx) => {
955
- const resolved = resolve(p);
956
- const st = await fsStat(resolved).catch(() => undefined);
957
- if (!st || !st.isFile())
958
- throw new Error(`file not found (or not a regular file): ${p} — ls first; absolute paths are safest`);
959
- if (st.size > SEND_USER_FILE_MAX_BYTES)
960
- throw new Error(`file exceeds the ${Math.floor(SEND_USER_FILE_MAX_BYTES / (1024 * 1024))} MiB per-file cap (${st.size} bytes)`);
961
- const bytes = await fsReadFile(resolved);
962
- if (bytes.byteLength > SEND_USER_FILE_MAX_BYTES)
963
- throw new Error(`file exceeds the ${Math.floor(SEND_USER_FILE_MAX_BYTES / (1024 * 1024))} MiB per-file cap (${bytes.byteLength} bytes)`);
964
- return issuer.issue(bytes, basename(resolved), undefined, sendCtx.principal);
965
- };
966
- const send = sendFileLedger
967
- ? withLedgerRecording(rawSend, {
968
- ledger: sendFileLedger,
969
- deleteObject: (bucket, key) => issuer.deleteObject(bucket, key),
970
- warn: (msg, meta) => logger.warn(msg, meta),
971
- })
972
- : rawSend;
973
- return sendUserFileTool({ send, emitter: sendUserFileEmitter });
974
- })();
975
- if (config.configCenter && !config.configCenter.dryRun && scopedTokenNeedsWorker(config.configCenter.token, config.configCenter.worker)) {
976
- logger.warn("runtime_caps_scoped_token_no_worker", {
977
- 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
- });
979
- }
980
- const principalCaps = config.configCenter && !config.configCenter.dryRun
981
- ? createPrincipalEntitlementsClient({
982
- baseUrl: config.configCenter.baseUrl,
983
- token: config.configCenter.token,
984
- ...(config.configCenter.worker ? { worker: config.configCenter.worker } : {}),
985
- onError: (err, principal) => logger.warn("runtime_caps_resolve_failed", { principal, err: String(err) }),
986
- })
987
- : undefined;
988
- const centerRuntimeCapsResolver = principalCaps?.resolveRuntimeCaps;
989
- const runtimeCapsResolver = applyObserverEnvOptIn(centerRuntimeCapsResolver, {
990
- experimentalObserverAgents: config.experimentalObserverAgents,
991
- requirePrincipal: config.requirePrincipal,
992
- warn: (msg, fields) => logger.warn(msg, fields),
161
+ // design/158 A10:workflow 编排面搬到 src/boot/workflow-orchestration.ts(逐字;构造次序契约见该文件头注)
162
+ const { sqlWorkflowRunStore, workflowNotifyJournal, workflowCompletionInbox, deliverWorkflowCompletion, workflowNotifyGate, fleetBus, workflowRunStore, workflowJournalStore, outcomeSink, workflowAgentRegistry, subagentSteerRegistry, } = createWorkflowOrchestration({ config, logger, metrics, localRoot, backend, getRunStore: () => runStore });
163
+ // design/158 A10:活体协调器 + SendUserFile 工具面搬到 src/boot/coordinators.ts(逐字;durableEnabled
164
+ // 「必须早于 runnerDeps 求值」次序契约见该文件头注)
165
+ const { elicitation, question, toolApproval, durableEnabled, sendUserFileEmitter, sendFileLedger, sendUserFileToolSpec } = createLiveCoordinators({ config, logger, backend, sendUserFileTaskEnvs });
166
+ // design/158 A10:per-principal caps 段搬到 src/boot/runtime-caps.ts(逐字)。
167
+ const { principalCaps, centerRuntimeCapsResolver, runtimeCapsResolver } = createRuntimeCaps({ config, logger });
168
+ // design/158 A10:RunnerDeps 装配段搬到 src/boot/runner-deps.ts(逐字;runStore 晚绑改取值,见该文件头注)
169
+ const runnerDeps = createRunnerDeps({
170
+ config, logger, metrics, localRoot, promptSource: configCenter.promptSource, rosterStore, backgroundAgentStore, mailboxStore, brain,
171
+ pricing, tracer, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine,
172
+ memorySyncRunner, toolResultStore, sessionPolicyStore, runtimeCapsResolver, fileSnapshotStore,
173
+ executionEnvFactory, lspManager, fleetBus, deploymentHooks, workflowRunStore, workflowJournalStore,
174
+ workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion,
175
+ getRunStore: () => runStore,
993
176
  });
994
- const workflowModelAllowlist = workflowModelAllowlistFor(config);
995
- const selfOrchestrationDeps = config.selfOrchestrationEnabled
996
- ? {
997
- workflowScriptRunner: config.selfOrchestrationWorkerIsolation
998
- ? createWorkerHardenedVmRunner()
999
- : createHardenedVmRunner(),
1000
- workflowRunStore: workflowRunStore ? workflowRunStore : undefined,
1001
- workflowJournalStore: workflowJournalStore ? workflowJournalStore : undefined,
1002
- workflowScriptStore: (() => {
1003
- const fileStore = createFileWorkflowScriptStore(join(config.localDataRoot ?? localRoot, "workflow-scripts"));
1004
- return {
1005
- scopePartitioned: true,
1006
- persist: fileStore.persist.bind(fileStore),
1007
- load: fileStore.load.bind(fileStore),
1008
- resolveName: (name) => resolveCollabWorkflow(name) ?? fileStore.resolveName?.(name),
1009
- list: () => listCollabWorkflows(),
1010
- };
1011
- })(),
1012
- onWorkflowAgentSpawn: workflowAgentRegistry
1013
- ? (handle) => {
1014
- const unregister = workflowAgentRegistry.register(handle);
1015
- void Promise.resolve(handle.result()).then(unregister, unregister);
1016
- }
1017
- : undefined,
1018
- workflowLimits: config.workflowSizeGuideline ? { sizeGuideline: config.workflowSizeGuideline } : undefined,
1019
- workflowGovernanceBaseline: {
1020
- base: config.workflowAgentsReadOnly ? { handsReadOnly: true } : {},
1021
- worktreeBase: config.workflowAgentsReadOnly ? { handsReadOnly: false } : undefined,
1022
- workflowModelAllowlist: workflowModelAllowlist ? workflowModelAllowlist : undefined,
1023
- },
1024
- workflowCompletionNotifier: {
1025
- ...(workflowNotifyGate
1026
- ? workflowNotifyGate.buildNotifier()
1027
- : { notify: (input) => deliverWorkflowCompletion(input) }),
1028
- ackServed: async (input) => {
1029
- if (!workflowCompletionInbox)
1030
- return;
1031
- try {
1032
- const sid = await resolveServedSession(input, runStore ? (id) => runStore.getRun(id) : undefined);
1033
- if (sid) {
1034
- await workflowCompletionInbox.markTerminalServed(sid, input.runId);
1035
- logger.info("workflow_complete_ack_served", { route: "poll-served", sessionId: sid, runId: input.runId });
1036
- }
1037
- }
1038
- catch (err) {
1039
- logger.warn("workflow_ack_served_failed", { runId: input.runId, err: String(err) });
1040
- }
1041
- },
1042
- },
1043
- }
1044
- : {};
1045
- const runnerDeps = {
1046
- promptSource,
1047
- rosterStore: rosterStore ? rosterStore : undefined,
1048
- backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
1049
- mailboxStore: mailboxStore ? mailboxStore : undefined,
1050
- brain,
1051
- models: config.models,
1052
- roles: config.roles,
1053
- tiers: config.tiers,
1054
- pricing,
1055
- tracer,
1056
- hands: config.configProvider === "local" ? { commitCoAuthor: "Sema <noreply@vivi-ai.com>" } : undefined,
1057
- onTaskOutcome: outcomeSink
1058
- ? (o) => {
1059
- metrics.inc("task_outcomes_total", { status: o.status, green: String(o.oracle?.green ?? "unknown") });
1060
- void outcomeSink.recordCore(o).catch((err) => logger.warn("task_outcome_record_failed", { runId: o.runId, err: String(err) }));
1061
- }
1062
- : undefined,
1063
- onElicit: elicitation ? elicitation.elicit : undefined,
1064
- onQuestion: question ? question.question : undefined,
1065
- onAsk: toolApproval
1066
- ? (req, signal) => toolApproval.ask(req, signal)
1067
- : undefined,
1068
- autoMode: {
1069
- onBreakerOpen: (info) => {
1070
- metrics.inc("auto_mode_breaker_open_total");
1071
- logger.warn("auto_mode_breaker_open", { consecutiveFailures: info.consecutiveFailures, lastCause: info.lastCause });
1072
- },
1073
- },
1074
- sessionStore,
1075
- memoryBackend: memoryEngine ? memoryEngine.backend : undefined,
1076
- memoryEngineDir: memoryEngine ? memoryEngine.root : undefined,
1077
- onMemoryHarvestReport: memoryEngine
1078
- ? (report, info) => {
1079
- metrics.inc("memory_harvest_total", { ok: String(report.ok), phase: info.phase, incident: report.incident?.kind ?? "none" });
1080
- if (report.patches)
1081
- metrics.inc("memory_harvest_patches_total", { phase: info.phase }, (report.patches.add ?? 0) + (report.patches.update ?? 0));
1082
- if (report.incident)
1083
- logger.warn("memory_harvest_incident", { kind: report.incident.kind, phase: info.phase });
1084
- if (memorySyncRunner && report.ok && (report.patches?.add ?? 0) + (report.patches?.update ?? 0) > 0)
1085
- memorySyncRunner.trigger("harvest");
1086
- }
1087
- : undefined,
1088
- toolResultStore,
1089
- sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined,
1090
- runtimeCapsResolver: runtimeCapsResolver ? runtimeCapsResolver : undefined,
1091
- fileSnapshotStore: fileSnapshotStore ? fileSnapshotStore : undefined,
1092
- executionEnvFactory: executionEnvFactory ? executionEnvFactory : undefined,
1093
- lspManager: lspManager ? lspManager : undefined,
1094
- onBackgroundChildEvent: fleetBackgroundChildPublisher(fleetBus, (msg, fields) => logger.info(msg, fields)),
1095
- loadProjectMemory: config.requirePrincipal !== true && config.projectMemoryEnabled ? makeLoadProjectMemory({ logger }) : undefined,
1096
- probeInstructionSources: config.remoteExec?.provider === "host" && config.requirePrincipal !== true && config.projectMemoryEnabled
1097
- ? makeProbeInstructionSources()
1098
- : undefined,
1099
- hooks: deploymentHooks,
1100
- ...selfOrchestrationDeps,
1101
- onError: (err, ctx) => {
1102
- if (ctx.phase === "degraded") {
1103
- logger.warn("task_degraded", { sessionId: ctx.sessionId, info: String(err) });
1104
- return;
1105
- }
1106
- if (ctx.phase === "prompt-constitution") {
1107
- logger.warn("prompt_provider_needs_upgrade", { sessionId: ctx.sessionId, info: String(err) });
1108
- return;
1109
- }
1110
- if (ctx.phase === "rewind") {
1111
- const code = /\((\w+)\)/.exec(String(err))?.[1] ?? "unknown";
1112
- logger.warn("rewind_snapshot_failed", { sessionId: ctx.sessionId, code, info: String(err) });
1113
- metrics.inc("rewind_snapshot_failed_total", { code });
1114
- return;
1115
- }
1116
- if (ctx.phase === "memory") {
1117
- logger.warn("memory_consolidation_failed", { sessionId: ctx.sessionId, info: String(err) });
1118
- return;
1119
- }
1120
- if (ctx.phase === "mcp") {
1121
- logger.warn("mcp_server_unavailable", { sessionId: ctx.sessionId, info: String(err) });
1122
- metrics.inc("mcp_server_unavailable_total");
1123
- return;
1124
- }
1125
- if (ctx.phase === "prompt-cache") {
1126
- metrics.inc("prompt_cache_low_hit_total");
1127
- const fields = { sessionId: ctx.sessionId, ...(ctx.classification ? { classification: ctx.classification } : {}), err: String(err) };
1128
- if (ctx.classification === "server-or-ttl")
1129
- logger.warn("prompt_cache_break", fields);
1130
- else
1131
- logger.error("prompt_cache_break", fields);
1132
- return;
1133
- }
1134
- if (ctx.phase === "config") {
1135
- logger.warn("runner_config_advisory", { sessionId: ctx.sessionId, info: String(err) });
1136
- metrics.inc("runner_config_advisory_total");
1137
- return;
1138
- }
1139
- logger.error("runner_error", { phase: ctx.phase, sessionId: ctx.sessionId, err: String(err) });
1140
- },
1141
- };
1142
177
  const runner = new Runner(runnerDeps);
178
+ // codex R10: TRUE ⇒ the Runner just froze a PRIVATE tier-expanded catalog copy (core runtask.js constructor,
179
+ // dist-read) — in-place model-plane mutation no longer reaches it, so refresh-time plane changes must be
180
+ // DEFERRED to restart (see appliedPlaneEff / applyEffective deferModelPlane). Captured HERE, at the same
181
+ // config generation the constructor read.
1143
182
  const runnerTierFrozen = Object.keys(config.tiers).length > 0;
1144
- const { hookModelFor, hookLlm } = createHookLlm({ config, getKeyResolver: () => keyResolver, metrics });
183
+ // hooks 阶段三b(design/HOOKS-PHASE3B-PROMPT-AGENT.md):prompt/agent 条目的模型调用载体。
184
+ // - hookLlm(prompt 条目)= 直连网关一次非流式 completion——一次判定调用不起 core task(重炮打蚊子
185
+ // 且引入可重入面);无系统注入(hook 作者的 prompt 即全部)。默认模型=summarize 角色(cheap tier,
186
+ // council/压缩同款;缺角色→主模型);entry.model 覆盖须命中部署 catalog(单用户=自家目录全开,与
187
+ // workflow 模型 allowlist 同纪律),未命中=该条目 fail(非阻断记账)。费用不折 task budget(hook=
188
+ // 部署策略面开销;hook_llm_calls_total/hook_llm_cost 由 metrics 记)。
189
+ // - hookAgent(agent 条目)= runner.runTask 读-only 子代理:handsReadOnly + enableFork:false +
190
+ // 不装 hooks(🔴 可重入禁——结构性:这个 spec 永不携带 TaskSpec.hooks)+ maxTurns 10 + maxCostUsd
191
+ // 0.05 独立小预算,影响范围受限。final result 文本即输出(与 prompt 同一 SyncHookOutput 决策面)。
192
+ // hookModelFor/hookLlm 抽到 src/hooks/hook-llm.ts(可注入 fetch/keyResolver ⇒ 可单测):api 形分派
193
+ // (openai /chat/completions 与 anthropic /v1/messages 双腿)、缺省 baseUrl 按 api 分家、per-model key
194
+ // 恒胜 + foreign-no-key 连 prompt 都不发的 fail-closed 纪律,全在该模块内(注释含 audit 原文)。
195
+ // getKeyResolver 取函数:keyResolver 住在 config-center 运行时的闭包里,registry 热应用会整个换引用,
196
+ // 这里必须每次现取。
197
+ const { hookModelFor, hookLlm } = createHookLlm({ config, getKeyResolver: () => configCenter.getKeyResolver(), metrics });
198
+ // hook agent 的 session 绝不进 durable store——每次 runTask 会写 session_meta 且无人
199
+ // 清理(reaper 只扫 staging orphan),cap=4/事件 × 长会话=无主行堆积。专用 Runner 覆盖 sessionStore 为
200
+ // 进程内 TTL 店(1h 短 TTL,hook 子代理无 resume 语义,行随进程/TTL 消失);其余 deps 原样共享。
1145
201
  const hookAgentRunner = new Runner({ ...runnerDeps, sessionStore: new TtlSessionStore({ defaultTtlDays: 1 / 24 }) });
1146
202
  const hookAgent = async ({ prompt, model, timeoutMs }) => {
1147
203
  const pick = hookModelFor(model);
@@ -1156,7 +212,11 @@ async function main() {
1156
212
  enableFork: false,
1157
213
  maxCostUsd: 0.05,
1158
214
  limits: { maxTurns: 10, timeoutSec: Math.max(30, Math.ceil(timeoutMs / 1000)) },
215
+ // spec 无 toolPolicy → core `hasEffectAwareGate` 为假 → 每次 agent hook 触发一条
216
+ // error 级 UNGATED 警告(非致命但纯噪声)。挂已裁决的 auto-accept 基线(与 singleUserAutoAcceptBaseline
217
+ // 同构;hook 子代理本就 handsReadOnly + 单用户信任面,auto-accept 是既定姿态,只是让闸机制在场)。
1159
218
  toolPolicy: createAllowDenyPolicy({}),
219
+ // 🔴 可重入禁:此 spec 不带 hooks 字段(deps.hooks 是部署观测 tracer,void 观察者,无决策面)。
1160
220
  });
1161
221
  metrics.inc("hook_llm_calls_total", { type: "agent" });
1162
222
  if (result.status !== "completed")
@@ -1168,12 +228,15 @@ async function main() {
1168
228
  }
1169
229
  };
1170
230
  const clipHookErr = (s) => (s ?? "").slice(0, 300);
231
+ // Auto session titles — fire-and-forget at first
232
+ // submit, one cheap hookLlm call (summarize role → MODEL_CHEAP_ID when configured), write-once via the session
233
+ // store's setTitleIfNull (SQL twins + local sidecar carry it; the memory dev store doesn't → titler dark, honest).
1171
234
  const titleCapable = sessionStore;
1172
235
  const sessionTitler = config.sessionAutoTitle && titleCapable.setTitleIfNull && titleCapable.probeTitle
1173
236
  ? createSessionTitler({
1174
237
  setTitleIfNull: titleCapable.setTitleIfNull.bind(sessionStore),
1175
238
  probeTitle: titleCapable.probeTitle.bind(sessionStore),
1176
- redact: redactSecrets,
239
+ redact: redactSecrets, // audit 2026-07-13: scrub a model-echoed secret before persist/list/log
1177
240
  llm: (o) => hookLlm({ prompt: o.prompt, timeoutMs: o.timeoutMs }),
1178
241
  logger,
1179
242
  metrics,
@@ -1181,38 +244,112 @@ async function main() {
1181
244
  : undefined;
1182
245
  if (sessionTitler)
1183
246
  logger.info("session_auto_title_enabled", {});
247
+ // ① workflowsCapability SINGLE-SOURCE (core 1.155 `workflowsCapability(deps)`; ruling = SPLIT two caps):
248
+ // `capabilities.workflows` = core's own truth (`workflowScriptRunner.safeForUntrustedScripts` ∧ governance), boot-
249
+ // computed from the SAME RunnerDeps the engine uses = the ENGINE-CAN axis ("can the engine self-orchestrate /
250
+ // ultracode"), ORTHOGONAL to the durable-LIST axis (`Boolean(workflowRunStore)`, surfaced as `workflowsList`).
251
+ // ⚠️ Today these COINCIDE — `workflowRunStore` is built iff self-orch (InMemory/File, ~L527), the same gate that
252
+ // wires the safe runner — so this is core's FORWARD-LOOKING contract, not a live-divergence fix (the earlier
253
+ // "memory backend diverges" premise was wrong; double-review caught it — see the cap site in http/server.ts).
254
+ // Static per deployment (deps are boot-fixed).
1184
255
  const workflowsCapable = workflowsCapability(runnerDeps);
256
+ // Sub-task runner (code-review council lenses/arbiter, subagent/team children). [1364]①/[1367]①(core
257
+ // 1.350 fork 终修的部署要求):fork 子任务带 `requireExistingSession:true` 在这里执行而源会话在 HOST 店
258
+ // ——拆店=响亮 `resume.session_not_found`(cli fork 案的 server 半场)。但整店共享被 codex 判两 high
259
+ // (普通子任务铸 committed durable 会话:无 reaper 覆盖=无界增长+污染 /v1/sessions 枚举),故走
260
+ // ForkRoutingSessionStore:fork/resume 形(requireExisting)→ host 店优先,普通子任务 → 私有 TTL 店
261
+ // (throwaway 姿势保留)。hookAgentRunner 仍保独立 TtlSessionStore(1h)——纯瞬态 hook 车道,与 fork 无关。
262
+ // CC-parity (CRITICAL — 子代理 hands, root cause is the service spawn path): the subagent delegation
263
+ // tool runs child tasks on THIS subRunner. Without executionEnvFactory the child's `handsEnabled` (= ownedEnv ||
264
+ // deps.executionEnv, prepare-task) is FALSE → the child gets NO hands (Bash/Read/Edit) and can't run `pwd` — the
265
+ // "subagent couldn't run pwd" defect hit in real TUI use. core's createSubagentTool gives hands when the env is
266
+ // wired (gate3: opts.tools:[] still yields Bash), so the fix is to give the subRunner the SAME executionEnvFactory +
267
+ // lspManager as the main runner. Deliberately NOT inherited: selfOrchestration (no recursive workflow spawn),
268
+ // scheduler, memory, fileSnapshot, runtimeCaps — a delegated child is a short focused task, not a full session.
269
+ // codex-2 F1:promptSource 必须随子代——core 1.341 把父的 centerArtifactDigest 传给委派子任务并在子
270
+ // runner 上按 digest 解析;缺席=每个 catalog 任务的 Agent 子代 fail-loud prompt_snapshot_unavailable。
271
+ // backgroundAgentStore(1.248 复审 F2):core prepare-task 给每个 task 的 TaskOutput/TaskStop/
272
+ // AgentTranscript 装 durable 臂用的是所在 runner 的 deps——子代任务跑在 subRunner 上,缺席=子代读
273
+ // 工具无 durable 回落(第三消费点,与 RunnerDeps/scenarioDeps 同一实例)。
274
+ // [1543]§三族A(core 穿透审查,历史欠账键批——主 runner 挂了、subRunner 漏挂 = 子代 lane 静默降级;
275
+ // 全部直引 runnerDeps 同一实例,双点永不漂移):
276
+ // - tiers(H):模型目录双 runner 分叉——tier 词 AgentDefinition 在子代 lane 抛 unknown_model、Agent
277
+ // model 参数("opus")静默 modelNote 降级、S3c revive 臂 row.model 记 tier 词时复活换模型。
278
+ // - rosterStore(M):具名 spawn 不落 durable 名册 → tier-3 按名复活从子代不可达。
279
+ // - onBackgroundChildEvent(M):子代触发的 revive/孙代对 fleet 流全盲(同一 publisher 实例=单管道)。
280
+ // - loadProjectMemory/probeInstructionSources(M):委派子代 project-blind(core types 已为子代 ctx
281
+ // 设计 isSubagent/agentName;门禁条件随 runnerDeps 条件键自动一致)。
282
+ // - hooks/onError/toolResultStore/sessionPolicyStore(L):子代进 tracer/计量;best-effort 故障不静默;
283
+ // fork 子代跨 wake 保 offload 引用;fork 形(有 sessionId)会话规则可达。
284
+ // 1877 注释的「Deliberately NOT inherited」清单(selfOrchestration/scheduler/memory/fileSnapshot/
285
+ // runtimeCaps)不变——本批只补漏配,不动裁定。
286
+ // checkpointStore 构造(从 runStore 区上移,只依赖 backend/config——顺序安全;DURABLE_APPROVAL 仍是
287
+ // 唯一门,ALL three backends carry checkpoint(),local = core FileCheckpointStore + service half)。
288
+ // 三个消费点同一实例:主 runner 的 per-task spec 面(resolveSpec durableEnabled 分支)、scenarioDeps
289
+ // (subagent spawn 面 forwardDurableApproval)、subRunner deps(子代执行面 suspendAsk——[1584] 接线,
290
+ // 裁定见下方 subRunner 构造内注)。
1185
291
  const checkpointStore = backend?.checkpoint && config.durableApproval ? backend.checkpoint(logger) : undefined;
292
+ // [1593] 提名成变量:parked 子代的 ensureChildSessionDurable 晋升半场要拿它做 promoteToHost
293
+ // (子会话住私有 TTL 店,park 前整树拷进 host durable 店——只查不迁的 1.263 形在拆店生产形下恒
294
+ // 否决 park,cli 真机 14ms~2s expired 即此)。
1186
295
  const subRunnerSessions = new ForkRoutingSessionStore(sessionStore, new TtlSessionStore({ defaultTtlDays: 1 / 24 }));
1187
296
  const subRunner = new Runner({
1188
- brain,
1189
- models: config.models,
1190
- roles: config.roles,
1191
- tiers: config.tiers,
1192
- pricing,
1193
- tracer,
1194
- promptSource,
1195
- sessionStore: subRunnerSessions,
1196
- executionEnvFactory: executionEnvFactory ? executionEnvFactory : undefined,
1197
- lspManager: lspManager ? lspManager : undefined,
1198
- backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
1199
- mailboxStore: mailboxStore ? mailboxStore : undefined,
1200
- rosterStore: rosterStore ? rosterStore : undefined,
297
+ // [1543]§三族A 结构性根治(design/158 冲刺尾件):与主 runnerDeps 重复的 ~15 键统一走
298
+ // createSharedRunnerDeps 展开——「双点挂载」从人工纪律变结构性保证(新共享键漏配其一=不可能,
299
+ // 基座只有一份)。差异键在展开后显式列出,每个都有为何不同的理由(见 boot/runner-deps.ts 头注)。
300
+ ...createSharedRunnerDeps({
301
+ config,
302
+ brain,
303
+ pricing,
304
+ tracer,
305
+ promptSource: configCenter.promptSource,
306
+ executionEnvFactory,
307
+ lspManager,
308
+ backgroundAgentStore,
309
+ mailboxStore,
310
+ rosterStore,
311
+ deploymentHooks,
312
+ toolResultStore,
313
+ sessionPolicyStore,
314
+ }),
315
+ // ── 以下为 subRunner 差异键(不在共享基座;逐个有因)──────────────────────────────────────
316
+ sessionStore: subRunnerSessions, // 子代转录=私有短 TTL fork 路由店,生命周期异于宿主 durable 店
1201
317
  onBackgroundChildEvent: runnerDeps.onBackgroundChildEvent ? runnerDeps.onBackgroundChildEvent : undefined,
1202
318
  loadProjectMemory: runnerDeps.loadProjectMemory ? runnerDeps.loadProjectMemory : undefined,
1203
319
  probeInstructionSources: runnerDeps.probeInstructionSources ? runnerDeps.probeInstructionSources : undefined,
1204
- hooks: deploymentHooks,
1205
320
  onError: runnerDeps.onError ? runnerDeps.onError : undefined,
1206
- toolResultStore,
1207
- sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined,
321
+ // checkpointStore = 子代**执行面**的 park 设施([1582] cli TiDB 真验撞获缺口,[1584] core 定谳
322
+ // 接线步):core prepare-task `spec.checkpointStore ?? deps.checkpointStore` 里 spec 侧永不被
323
+ // subagent 设置,子代 suspendAsk 全靠这里——缺席 = 具名 bg 子代的 gated ask 只能 deny+continue,
324
+ // checkpoint 表零行(cli T2 复现原话「denied by an approval constraint」)。必须与 scenarioDeps
325
+ // (spawn 面 forwardDurableApproval 判定)**同一实例**:claim/expire/consumeParkedFlip 都作用于
326
+ // 同一行。旧 [1546] HIGH-2 撤载(「core 不转发 durableApproval + SAFETY ask 会 park 出搁浅
327
+ // checkpoint」)的前提被 core 1.388 件4(转发臂)消除,搁浅顾虑由件1-4+1.389 逐项闭合:watcher
328
+ // park 翻行 + reap 豁免 + reconciliation 兜孤儿;unattended SAFETY park 恒带缺省 deadline;
329
+ // TaskStop on parked 走 expire CAS 仲裁(1.389 F-1);/decide 可赎回。行为钉 =
330
+ // test/subagent-park-wiring.test.ts(SOURCE PIN + park 全链 + 旧形反向控制)。
1208
331
  checkpointStore: checkpointStore ? checkpointStore : undefined,
1209
332
  });
333
+ // S6: resolve identity + owned session from the authenticated channel (never the body).
1210
334
  const authorize = createAuthorizer(config, sessionStore);
335
+ // S1/S2: durable async run registry + replayable event log (TiDB-backed).
1211
336
  const runStore = backend ? backend.run() : undefined;
337
+ // E18 resume-at: the eventId→entryId anchor map. REQUIRED on every backend (works local — needs only the session
338
+ // tree + this map, no cloud-only checkpoint), so the only undefined case is the env-only/no-backend deploy (which
339
+ // also has no async runs — consistent). The shell sends body.resumeAt=eventId; resolveSpec resolves it here.
1212
340
  const resumeAnchorStore = backend ? backend.resumeAnchor() : undefined;
341
+ // Per-session approval exemption ("本会话不再询问") — REQUIRED on every backend like the anchor
342
+ // map; undefined only on the env-only/no-backend deploy (which has no durable approvals either — consistent).
1213
343
  const approvalExemptionStore = backend ? backend.approvalExemption() : undefined;
1214
344
  const instanceId = uuidv7();
345
+ // Per-task wall-clock override (seconds). 0/absent = keep the built-in 600 (1500 council/debate/team) —
346
+ // the override only ever RAISES (Math.max at the use site), so a misconfigured low value cannot shrink
347
+ // the council budget.
1215
348
  const taskTimeoutSec = Math.max(0, Math.floor(Number(process.env.TASK_TIMEOUT_SEC ?? "0")) || 0);
349
+ // [854]④ per-request 配速的运营方上限旋钮(可选;缺省不设=不封顶):body.limits 每键各自被对应旋钮
350
+ // Math.min 封顶(normalizeLimits)。多租部署想约束 caller 自报配速时才设;单用户 turnkey 通常留空。
351
+ // ⚠️ 语义=只钳「显式请求值」,不是全队默认限额:body 缺席(或重放体里的非法值被 defensive DROP)⇒ 该键
352
+ // 不设限,与升级前行为同形——要给所有任务强加限额是另一个旋钮(未建),别指望这三个(交叉评审 F1 判读)。
1216
353
  const capEnv = (name) => {
1217
354
  const n = Math.floor(Number(process.env[name] ?? "0"));
1218
355
  return Number.isFinite(n) && n > 0 ? n : undefined;
@@ -1222,12 +359,32 @@ async function main() {
1222
359
  maxOutputTokens: capEnv("TASK_MAX_OUTPUT_TOKENS_MAX"),
1223
360
  maxTurns: capEnv("TASK_MAX_TURNS_MAX"),
1224
361
  };
1225
- const approvalStore = backend?.approval ? backend.approval() : undefined;
362
+ // F4: human-in-the-loop approval gate (pending state durable so any instance can decide).
363
+ const approvalStore = backend?.approval ? backend.approval() : undefined; // local omits approval() → legacy poll-gate off (durable HITL needs checkpoint anyway)
1226
364
  const approvalEnabled = approvalStore && config.approvalRequire.length > 0;
365
+ // Finding #2 (edge case): the single-user auto-accept tool-policy BASELINE applies ONLY when the operator
366
+ // expressed NO gate intent. If they SET approval flags (APPROVAL_REQUIRE/APPROVAL_DENY/DURABLE_APPROVAL) but no
367
+ // backing store is wired (e.g. DB_BACKEND=local → no approvalStore/checkpointStore ⇒ approvalEnabled/durableEnabled
368
+ // false), that is a MISCONFIG — do NOT mask it with allow-all; leave toolPolicy undefined so core's UNGATED warning
369
+ // still surfaces it. So the baseline needs single-user turnkey ∧ zero expressed gate intent.
1227
370
  const operatorGateIntent = config.approvalRequire.length > 0 || config.approvalDeny.length > 0 || config.durableApproval;
1228
371
  const singleUserAutoAcceptBaseline = config.requirePrincipal !== true && !operatorGateIntent;
372
+ // design/45 durable-checkpoint (core 1.67): opt-in (DURABLE_APPROVAL + a pool). When on, F4 uses durable
373
+ // suspend/resume instead of the poll gate — a gated `ask` suspends the task (status:"suspended" + checkpoint)
374
+ // and an operator resumes it on any replica. Default off → unchanged poll-policy.
375
+ // checkpointStore 构造已上移到 subRunner 之前([1535] server 半场:子代 park 设施);本行原位注释保留。
376
+ // TOC integration ask③ (INTEGRATION-DESIGN §11): reclaim runs orphaned by a previous host-lane engine
377
+ // process (orphaned `running` → failed + release its EEXIST-blocking claim; exclude resume-in-progress rows). The
378
+ // logic + its null-safety (runStore undefined on the env-only/no-backend lane) live in the unit-tested
379
+ // `reclaimOrphansAtBoot` helper — extracted after a workflow re-review caught a boot crash there (HIGH, 1.49.0).
1229
380
  await reclaimOrphansAtBoot(runStore, checkpointStore, logger);
1230
- const imageIndex = backend?.imageIndex ? backend.imageIndex() : undefined;
381
+ // Sandbox-image-pool index (IMAGE-API-DESIGN.md §2): enables /v1/images* query + select. TiDB-backed projection
382
+ // of the bake manifests + publish lifecycle. Present whenever a pool is — read-only catalog until P2/P3 bakes.
383
+ const imageIndex = backend?.imageIndex ? backend.imageIndex() : undefined; // local omits imageIndex() → sandbox-image-pool is cloud/fleet-only
384
+ // RFC A2 (A 切片先行): the model-facing SelectEnvironment tool — mounted via spec.tools
385
+ // (RFC 判定②(a)) whenever the per-task image chain is actually usable (k8s backend + image catalog), opt-out
386
+ // via SELECT_ENVIRONMENT_TOOL=false. Viewer scope is rebuilt per-execute from the Runner-threaded read-only
387
+ // ctx.principal (auth-channel-derived — a tool argument can never widen visibility).
1231
388
  const selectEnvTool = config.selectEnvironmentTool && config.remoteExec?.provider === "k8s" && imageIndex
1232
389
  ? selectEnvironmentTool({
1233
390
  catalog: imageIndex,
@@ -1235,90 +392,123 @@ async function main() {
1235
392
  viewerFor: (principal) => ({ operator: explicitOperator(principal, config.operatorPrincipals), tenantId: principal ?? null }),
1236
393
  })
1237
394
  : undefined;
1238
- const imageBakes = backend?.imageBake && config.imageBakes.enabled ? backend.imageBake() : undefined;
395
+ // Sandbox-image-pool BAKE control plane (IMAGE-API-DESIGN.md §P2): enables /v1/images/bakes* when a pool exists
396
+ // AND IMAGE_BAKES_ENABLED. The config boot-invariant already fail-fasts an empty OPERATOR_PRINCIPALS (a
397
+ // world-writable build-host-RCE door). image-api stays the UNPRIVILEGED controller; the privileged build runs
398
+ // on the sema-registry-managed bake-runner worker on build-host (it claims + ingests over the internal routes).
399
+ const imageBakes = backend?.imageBake && config.imageBakes.enabled ? backend.imageBake() : undefined; // local omits imageBake() → cloud/fleet-only
1239
400
  if (imageBakes)
1240
401
  logger.info("image_bakes_enabled", { operators: config.operatorPrincipals.length, runner: config.imageBakes.runnerPrincipal });
402
+ // Durable HITL surface = checkpointStore ALONE (DURABLE_APPROVAL=true). It mounts AskUserQuestion-suspend (the
403
+ // agent asks the human + the portal approval center) AND the resumable checkpoint. The F4 TOOL-APPROVAL
404
+ // gate is an ADDITIVE layer driven by APPROVAL_REQUIRE — empty = no tool is gated, but AskUserQuestion HITL
405
+ // still works. (Decoupled 2026-06-13: previously these were coupled, forcing a placeholder APPROVAL_REQUIRE
406
+ // to get HITL — clay: OA writes don't gate (OA-backend RBAC is the authority), but portal HITL must work.)
407
+ // durableEnabled 已在 runnerDeps 前按同一条件求值([1.294 G1] 前置声明处)——checkpointStore 构造条件与其
408
+ // 同源,恒有 durableEnabled === !!checkpointStore。
1241
409
  if (durableEnabled)
1242
410
  logger.info("durable_approval_enabled", { toolApprovalGate: config.approvalRequire.length });
411
+ // 修4(三路复审 W3):the fs-write ask gate (resolveSpec, applyTaskSettings wiring below) mounts purely off the
412
+ // host-semantics lane — DELIBERATELY decoupled from any approval face, so no assembly path is ever gateless:
413
+ // with no live coordinator and no durable park, a permissionMode default/auto Write/Edit ask fail-closed
414
+ // auto-denies via core resolveAsk (onAsk absent ⇒ headless auto-deny, loud in-transcript). What that posture
415
+ // must NOT be is silent at boot: an operator who explicitly set TOOL_APPROVAL_ENABLED=false (or a multi-tenant
416
+ // default) would otherwise read the resulting hand-tool denials as an outage. Warn once, at boot.
1243
417
  if ((config.remoteExec === undefined || config.remoteExec.provider === "host") && !toolApproval && !durableEnabled) {
1244
418
  logger.warn("fs_write_gate_no_responder", {
1245
419
  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
420
  });
1247
421
  }
422
+ // [1245]/[1248]② the workflow ask leg attaches on EVERY lane (not fs-dependent — see createWorkflowAskPolicy),
423
+ // so a sandbox-lane deployment running self-orchestration with no approval face hits the same fail-closed
424
+ // auto-deny class. Same loud-at-boot posture; host lanes are already covered by the broader warn above.
1248
425
  if (config.selfOrchestrationEnabled && config.remoteExec !== undefined && config.remoteExec.provider !== "host" && !toolApproval && !durableEnabled) {
1249
426
  logger.warn("workflow_gate_no_responder", {
1250
427
  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
428
  });
1252
429
  }
430
+ // Finding #2: a single-user turnkey worker with no explicit approval/durable/commandPolicy gets an
431
+ // ADJUDICATED auto-accept tool-policy BASELINE (createAllowDenyPolicy — a PRESENT effect-aware policy, so core's
432
+ // write-capable hand tools are never UNGATED / unadjudicated). Log the posture so the auto-accept is OBSERVABLE,
433
+ // not silent; tighten irreversible ops via APPROVAL_REQUIRE / AUTONOMY / commandPolicy (layered tighten-only).
1253
434
  if (singleUserAutoAcceptBaseline) {
435
+ // Blanket auto-accept over ALL write/hand/MCP tool surfaces (core's per-surface UNGATED enumeration — hand band /
436
+ // destructive-MCP / first-party-write — is suppressed for single-user turnkey by design). Log it so the posture is
437
+ // observable; tighten (which also re-surfaces per-surface gating) via APPROVAL_REQUIRE / AUTONOMY / commandPolicy.
1254
438
  logger.info("tool_policy_single_user_default", {
1255
439
  posture: "adjudicated auto-accept — ALL write/hand/MCP tool surfaces auto-allowed (core per-surface UNGATED enumeration suppressed for single-user turnkey)",
1256
440
  tighten: "APPROVAL_REQUIRE / AUTONOMY / commandPolicy (re-surfaces + gates specific surfaces)",
1257
441
  });
1258
442
  }
443
+ // D-G: a boot-time signal that this worker is in crypto-bound direct-door mode (trusted-header path disabled).
444
+ // Pairs with the /health directDoorActive field for the DG-M2 go-live runbook (a positive go signal, not just
445
+ // a behavioral 401). The boot invariant in config.ts already fail-fasts a half-configured direct door.
1259
446
  if (config.directDoorActive)
1260
447
  logger.info("direct_door_active", { iss: config.principalJwtIss, aud: config.principalJwtAud, operators: config.operatorPrincipals.length });
448
+ // CC-parity (adversarial-review finding): a MULTI-TENANT deployment running self-orchestration WITHOUT worker
449
+ // isolation is an availability risk — the in-process hardened VM is ESCAPE-isolated but NOT RESOURCE-isolated, so an
450
+ // entitled tenant's workflow script can starve the host event loop (microtask loop / OOM), affecting every tenant
451
+ // (src/orchestration/hardened-vm-runner.ts documents this). Loud boot warning so the operator sets
452
+ // SELF_ORCHESTRATION_WORKER_ISOLATION=true. (Single-user is unaffected — the one principal owns its own worker.)
1261
453
  if (config.requirePrincipal && config.selfOrchestrationEnabled && !config.selfOrchestrationWorkerIsolation)
1262
454
  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." });
455
+ // 🔒 (systematic-audit HIGH fix, defense-in-depth): a multi-tenant deployment (requirePrincipal) with NO service/trace
456
+ // token now correctly fails CLOSED — fleet/trace/session-list are owner-gated, never fleet-wide (isFleetWide, server.ts).
457
+ // Warn the operator: without SERVICE_AUTH_TOKEN/authTokens/TRACE_TOKEN those endpoints only ever return the caller's own
458
+ // rows (safe, but may surprise an operator expecting cross-tenant ops visibility — set a token for ops-relay).
1263
459
  if (config.requirePrincipal && !config.authToken && Object.keys(config.authTokens ?? {}).length === 0 && !config.traceToken)
1264
460
  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." });
461
+ // Rate limiter: TiDB-backed (cross-replica SOFT limit, same write-behind counter as the cost quota)
462
+ // when a pool exists, else in-memory per-replica. Both satisfy RateGate. (design/30 P0-2)
1265
463
  const rateLimiter = config.rateLimitPerMin > 0
1266
- ? backend?.rateLimiter
464
+ ? backend?.rateLimiter // local omits rateLimiter() → in-memory per-process RateLimiter (single-replica)
1267
465
  ? backend.rateLimiter(config.rateLimitPerMin, counterDegradeHook).startRefresh()
1268
466
  : new RateLimiter(config.rateLimitPerMin)
1269
467
  : undefined;
468
+ // Capability layer: scenario → {tools (+ subagent roster), prompt, skills}. Heavy bits (skills,
469
+ // git client) are built ONCE here; per request we only select a scenario and bind coordinates.
1270
470
  let skills = loadSkills(config.skillsDir);
1271
- if (effective?.skills && config.configCenter) {
1272
- skills = await applyCenterSkills(skills, effective.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, configLkgEnabled() ? defaultSkillCacheDir() : undefined);
1273
- }
1274
- if (effective?.plugins && config.configCenter) {
1275
- const pluginOut = await applyCenterPlugins(skills, effective, {
1276
- cacheRoot: join(config.localDataRoot ?? join(homedir(), ".ai-agent"), "plugin-cache"),
1277
- allowHosts: config.pluginsAllowHosts,
1278
- logger,
1279
- }).catch((err) => {
1280
- logger.warn("center_plugins_apply_failed", { error: err instanceof Error ? err.message : String(err) });
1281
- return undefined;
1282
- });
1283
- if (pluginOut) {
1284
- skills = pluginOut.skills;
1285
- if (pluginOut.failures.length + pluginOut.letGo.length + pluginOut.lkgUsed.length > 0) {
1286
- logger.info("center_plugins_applied", { failures: pluginOut.failures, letGo: pluginOut.letGo, lkgUsed: pluginOut.lkgUsed });
1287
- }
1288
- }
1289
- }
1290
- if (bootLkgCandidate && lkgEnabled) {
1291
- const bootPublished = await persistLkgDurable(bootLkgCandidate.effective, bootLkgCandidate.etag).catch((err) => {
1292
- logger.warn("config_lkg_save_failed", { path: lkgPath, err: String(err), note: "boot LKG persist failed — retained for the per-tick retry" });
1293
- return false;
1294
- });
1295
- {
1296
- const enabledManifest = (bootLkgCandidate.effective.skills?.skills ?? []).filter((m) => m.enabled !== false);
1297
- const canonScenarios = (a) => [...(a ?? [])].sort().join("\u0000");
1298
- const liveSkillMiss = enabledManifest.some((m) => !skills.some((sk) => sk.spec.name === m.name &&
1299
- skillContentHash(sk.spec.content) === m.contentHash &&
1300
- sk.spec.description === m.description &&
1301
- canonScenarios(sk.scenarios) === canonScenarios(m.scenarios)));
1302
- if (liveSkillMiss)
1303
- bootSkillStale = true;
1304
- if (liveSkillMiss && (bootPublished || (!lkgSurvivesRestart && lastPrewarmBodiesVerified))) {
1305
- pendingRestart = { restartRequired: true, reasons: ["skills"], version: bootLkgCandidate.effective.version, since: Date.now() };
1306
- 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
- }
1308
- else if (liveSkillMiss || (!bootPublished && lkgSurvivesRestart)) {
1309
- planeDeferredNoHandoff = { version: bootLkgCandidate.effective.version, since: Date.now(), blocked: liveSkillMiss ? ["skills"] : [], candidate: bootLkgCandidate.effective, ...(bootLkgCandidate.etag !== undefined ? { candidateEtag: bootLkgCandidate.etag } : {}) };
1310
- logger.warn("config_lkg_boot_publication_retained", { version: bootLkgCandidate.effective.version, liveSkillMiss, durableDeclared: lkgSurvivesRestart, note: (lkgSurvivesRestart ? "boot LKG publication incomplete on a durable-declared deployment — candidate retained; the refresh loop retries after each confirming 304" : "live skill application incomplete on a non-durable deployment and the body cache could not be verified — skills debt retained WITHOUT a restart signal (codex R37: signaling now could restart-storm; each confirming 304 re-verifies and publishes once the bodies prove fetchable)") + (liveSkillMiss && lkgSurvivesRestart ? "; this process runs skill-stale, promotion will publish a skills restart" : "") });
1311
- }
1312
- }
1313
- }
1314
- if (effective?.mcp)
1315
- config.mcpServers = resolveMcpServers(effective.mcp, logger);
471
+ // design/158 A10 尾刀:center skills overlay + plugins 消费 + boot LKG 落盘/skill-stale 判定 + mcp
472
+ // 解析搬到 boot/config-center.ts(逐字)。⚠️ 位置即契约:loadSkills 之后、buildScenarios 之前 ——
473
+ // LKG 落盘点必须晚于 skill 正文装载(F7/codex R26),plugins 让位判据要求 plugins 晚于 center 直发 skills。
474
+ skills = await configCenter.applyCenterCapabilities(skills);
1316
475
  const repoClient = config.gitApiBaseUrl ? new GiteaClient(config.gitApiBaseUrl, config.gitApiToken) : undefined;
476
+ // CC-parity: deployment-injected WebSearch backend (the leg core leaves open). Absent WEB_SEARCH_PROVIDER →
477
+ // undefined → the default scenario doesn't assemble the WebSearch tool. The API key stays in the backend closure.
1317
478
  const webSearchCfg = webSearchConfigFromEnv();
1318
479
  const webSearch = webSearchCfg ? createWebSearchBackend(webSearchCfg) : undefined;
1319
480
  if (webSearch)
1320
481
  logger.info(`WebSearch enabled (provider=${webSearchCfg.provider}, maxResults=${webSearch.maxResults})`);
482
+ // core 1.382([1561] 提货单①②,design/153 parked 状态机)「同车必接」的 ensureChildSessionDurable 挂点:
483
+ // 只在 checkpointStore 真在场时构造(与 core parkEligible 门「两者全在场才成立」的判据同源——checkpoint
484
+ // 不在场时构造这个闭包毫无意义,连带把「local/无 durable backend 部署不该跑这条 acquire/release」的
485
+ // 边界直接编码进构造条件,不用额外判断)。实现体拆到 `ensureSessionDurable`(session-store.ts,可独立
486
+ // 测试;双复审②抓的 F-7 同族空壳缺口——host 店可能存在 `register()` 留下的零内容 session_meta 行——
487
+ // 的校验也在那边)。
488
+ // 🔴 [1575] F3 修(cli 复查,红先行确认真红):`checkpointStore` 在场 ⇏ `sessionStore` 就是 TiDB 真店
489
+ // ——这是两个独立 config 轴(DB_BACKEND 供 checkpoint/runs 等用途 vs SESSION_BACKEND 单独控 session
490
+ // 落哪),`SESSION_BACKEND` 显式留在默认值时 `sessionStore` 是纯内存 `TtlSessionStore`(无 repo 构造,
491
+ // core `release()` 语义在该形态下是**整删会话**,不是"归还租约")——探针的 finally release 会销毁它
492
+ // 刚证明存在的会话。追加门 `config.sessionBackend === "tidb"`(此刻已被前面装配区从 "auto" 归一,
493
+ // 恒是 "tidb"|"memory" 二选一,时序安全):唯一保留可达的店形正是 release() 真安全的那种
494
+ // (TiDBSessionStore.release 逐字注释「Durable history is retained; just drop any in-flight cache
495
+ // entry」)。非 tidb 场景下 undefined = 子代按 pre-153 settle failed,零回归(与 checkpointStore 缺席
496
+ // 同一处置)。
497
+ // [1593] 只查不迁 → 主动晋升(cli TiDB 真机撞获:bg 子会话住 subRunner 私有 TTL 店、从不在 host 店,
498
+ // 旧形恒 veto → core 当场 expire checkpoint(14ms~2s)+ settle failed,赎回窗口不存在)。晋升链=
499
+ // 快查 host → promoteToHost(整树拷贝+私店 forget+路由记 host)→ 同一严检复验(leaf 非空);任何
500
+ // 一步失败仍如实 veto(pre-153 行为,不放大)。tidb 门与 F3 裁定不变。
1321
501
  const ensureChildSessionDurable = checkpointStore && config.sessionBackend === "tidb" ? (sessionId) => ensureChildSessionDurableWithPromotion(sessionStore, subRunnerSessions, sessionId) : undefined;
502
+ // [1900]/[1904] WebFetch 摘要器:core 工厂 + summarize 角色模型(cheap tier;resolveTaskModel 自带
503
+ // 默认回落——与压缩线同族档位,不新造)。构造一次全场景共享;多租户形 WebFetch 整体被 filter,
504
+ // 本注入无效化由 scenarios 现行为保证。
505
+ // 🔴 复审 E-MED-2/E-MED-3 两修:
506
+ // ① **惰性解析**:模型每次调用现取——`config.models`/`config.roles` 会被 center 的 effective
507
+ // 热应用 mutateInPlace 整表替换,boot 期急求值会把摘要器永久 pin 在占位/旧模型上(故障形态是
508
+ // 静默退回上下文放大 + 每次一发无效 LLM 调用)。
509
+ // ② **fail-soft**:core resolveTaskModel 在无可解析角色时 throw,这是全仓唯一 boot 期裸调用;
510
+ // center 发一张既无 summarize 又无 default 的 roles 表就会拒启。解析失败 ⇒ 本次不摘要
511
+ // (core 的 summarize 抛错是 fail-open:回退原文+note),绝不崩 boot。
1322
512
  const webFetchSummarize = async (content, prompt, signal) => {
1323
513
  const model = (() => {
1324
514
  try {
@@ -1333,14 +523,29 @@ async function main() {
1333
523
  throw new Error("summarize model unresolved for this deployment");
1334
524
  return createWebFetchSummarizer(brain, model)(content, prompt, signal);
1335
525
  };
526
+ // design/158 S4:标注 `: ScenarioDeps` —— 无标注的字面量连自己的键名都不校验(打错/多写一个键
527
+ // 只是多一个没人读的属性,编译期无声),这是 1.304 落错家那一族的另一半土壤。
1336
528
  const scenarioDeps = {
1337
529
  runner, subRunner, model: "default", skills, repoClient, requirePrincipal: config.requirePrincipal,
1338
530
  webSearch: webSearch ? webSearch : undefined, metrics, logger, webFetchSummarize,
531
+ // core 1.364 写半场:与 RunnerDeps.backgroundAgentStore 同实例(组装区注释;半接=静默死特性)。
1339
532
  backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
533
+ // core 1.382 parked 状态机「同车必接」一对——checkpointStore 与 main runner 的 checkpointStore(subRunner
534
+ // 构造区上方 const)同实例;ensureChildSessionDurable 见上方构造闭包注释。
1340
535
  checkpointStore: checkpointStore ? checkpointStore : undefined,
1341
536
  ensureChildSessionDurable: ensureChildSessionDurable ? ensureChildSessionDurable : undefined,
1342
537
  oaApiBaseUrl: config.oaApiBaseUrl, oaServiceToken: config.oaServiceToken, oaIssue: config.oaIssue,
1343
538
  brandIdentity: config.configProvider === "local",
539
+ // core 1.287 extraTools seam ([802]③ server half): SendUserFile reaches the delegated-child roster. SAME
540
+ // existence gate as the top-level mount (the one sendUserFileToolSpec built above — lane/tenancy/config
541
+ // posture already encoded in its presence). The spec is execute-time late-bound (ctx.taskId →
542
+ // TaskEnvRegistry lookup; emit via ALS to the top-level frame tail), so returning the SAME instance per
543
+ // spawn is correct — per-spawn state lives in the registry, not the spec ([797] confirmed with core).
544
+ // codex F1/F2 gates (subagentSendUserFileExtraTools): (F1) the subRunner carries no toolPolicy and core
545
+ // does not propagate the parent's policy into child specs, so an APPROVAL_DENY/ask-gated SendUserFile was
546
+ // reachable from delegated children — the factory now pre-checks the LIVE deployment approval face
547
+ // (deny/require/neverAuto, canonical-space) and injects nothing when gated (conservative stopgap until a
548
+ // core child-policy seam lands); (F2) read-only builtin agents (Explore/Plan) never receive the tool.
1344
549
  subagentExtraTools: sendUserFileToolSpec
1345
550
  ? subagentSendUserFileExtraTools({
1346
551
  spec: sendUserFileToolSpec,
@@ -1349,7 +554,16 @@ async function main() {
1349
554
  : undefined,
1350
555
  };
1351
556
  const scenarios = buildScenarios(scenarioDeps);
557
+ // design/153 件3d:/decide parked 赎回腿的裸 Agent 工具——boot 期从**内建 default** 场景工厂取一份
558
+ // (必须在 center overlay 盖名之前取:赎回腿要的是与 spawn 同源的生产装配,subRunner/bg opts/
559
+ // checkpointStore 同实例天然成立;per-request 项如 webSearch 对 revive 无关——objective 在 core
560
+ // driveParkedResume 里被丢弃)。checkpointStore/agentStore 任一缺席 = park 本就不可达,分支不接线。
1352
561
  const parkedReviveTool = checkpointStore && backgroundAgentStore ? scenarios.default?.({})?.tools.find((t) => t.name === "Agent") : undefined;
562
+ // [1596]/[1597] 跨进程父约束重供席工厂:host 任务的 toolPolicy 解析槽按部署配置**同参重建**
563
+ // (resolveSpec durableEnabled 分支的同款折叠:durableQuestion + durableAsk 四旋钮),单层链
564
+ // count=1,durableMandate=true(host 有 durableApproval 且 durable 车道无 live onAsk 的恒形)。
565
+ // exempt 探针锚 = row.rootSessionId——与 parked decide 的 remember grant 锚同键([1591] 候裁②),
566
+ // 重建链上的豁免语义与原链一致。重建不出(无 root 锚)= 不供席 = core pre-CAS 门诚实拒(现状)。
1353
567
  const parkedReviveInheritedGate = parkedReviveTool && config.durableApproval
1354
568
  ? (row) => ({
1355
569
  parentConstraints: [{
@@ -1366,464 +580,55 @@ async function main() {
1366
580
  }],
1367
581
  })
1368
582
  : undefined;
583
+ // 场景详情:内建 details 必须在 overlay 合并【前】构建(探针要打纯内建工厂,不是被 center 顶掉的);
584
+ // center details 随 overlay 同判定源盖同名——source 语义与 selectScenario 实际取用永一致(约定①)。
1369
585
  const scenarioDetails = builtinScenarioDetails(scenarios, scenarioDeps);
1370
- const builtinScenarioNames = Object.keys(scenarioDetails);
1371
- if (effective?.scenarios) {
1372
- const { overlay, shadows } = centerScenarios(scenarioDeps, effective.scenarios.scenarios, Object.keys(scenarios), logger);
1373
- Object.assign(scenarios, overlay);
1374
- Object.assign(scenarioDetails, centerScenarioDetails(effective.scenarios.scenarios, builtinScenarioNames));
1375
- if (Object.keys(overlay).length > 0)
1376
- logger.info("sema_registry_scenarios", { scenarios: Object.keys(overlay), ...(shadows.length > 0 ? { shadowsBuiltin: shadows } : {}) });
1377
- }
586
+ const builtinScenarioNames = Object.keys(scenarioDetails); // overlay 前捕获(六内建名)
587
+ // design/158 A10 尾刀:center 声明场景 overlay 搬到 boot/config-center.ts(逐字)。⚠️ 位置即契约:
588
+ // 必须在内建 details + 六内建名捕获之后 —— 探针要打纯内建工厂,不是被 center 顶掉的。
589
+ configCenter.applyCenterScenarios({ scenarioDeps, scenarios, scenarioDetails, builtinScenarioNames });
1378
590
  logger.info("capabilities", { skills: skills.length, scenarios: Object.keys(scenarios), repoTools: Boolean(repoClient) });
591
+ // Background reapers: recover runs whose instance died, expire never-answered approvals, sweep
592
+ // rate windows. Unref'd so they never hold the process open.
593
+ // design/80 D-D (SLA-timer): the deny-sweep reuses the server's resumeCheckpoint, so it is wired in AFTER
594
+ // createHttpServer returns it (below) via this holder — the reaper is defined before the server.
1379
595
  let runDenySweep;
1380
- const reapCount = (metric, labels) => (n) => {
1381
- if (typeof n === "number" && n > 0) {
1382
- metrics.inc(metric, labels, n);
1383
- logger.info("reaper_swept", { metric, ...labels, count: n });
1384
- }
1385
- };
1386
- let wfRunReapInFlight = false;
1387
- let attachmentSweepInFlight = false;
1388
- let bgAgentReapInFlight = false;
1389
- const reapBgAgents = backgroundAgentStore
1390
- ? async () => {
1391
- const store = backgroundAgentStore;
1392
- const scopes = await (store.listScopes?.() ?? Promise.resolve(["default"]));
1393
- let rows = 0, sessions = 0, skipped = 0, failedScopes = 0;
1394
- for (const scope of scopes) {
1395
- try {
1396
- if (checkpointStore) {
1397
- const pr = await defaultTaskRegistry.reconcileParkedAgents({ agentStore: store, checkpointStore }, scope, Date.now(), { staleClaimMaxAgeMs: config.backgroundAgentParkClaimStaleMs });
1398
- if (pr.failed > 0 || pr.rolledBack > 0) {
1399
- metrics.inc("bg_agents_park_reconciled_failed_total", {}, pr.failed);
1400
- metrics.inc("bg_agents_park_reconciled_rolledback_total", {}, pr.rolledBack);
1401
- logger.info("reaper_swept", { metric: "bg_agents_park_reconciled", scope, failed: pr.failed, rolledBack: pr.rolledBack });
1402
- }
1403
- }
1404
- const r = await defaultTaskRegistry.reapDurableAgents(scope, { store, sessions: subRunner.sessions, ...(mailboxStore ? { mailbox: mailboxStore } : {}) }, {
1405
- maxAgeMs: config.backgroundAgentRetentionMs,
1406
- staleRunningMaxAgeMs: config.backgroundAgentStaleRunningMs,
1407
- });
1408
- rows += r.rowsReaped;
1409
- sessions += r.sessionsReleased;
1410
- skipped += r.skippedNoSessions;
1411
- }
1412
- catch (err) {
1413
- failedScopes++;
1414
- logger.warn("bg_agent_reap_scope_failed", { scope, err: err instanceof Error ? err.message : String(err) });
1415
- }
1416
- }
1417
- if (failedScopes > 0)
1418
- metrics.inc("bg_agent_reap_scope_failures_total", {}, failedScopes);
1419
- if (rows > 0 || sessions > 0) {
1420
- metrics.inc("bg_agents_reaped_total", {}, rows);
1421
- logger.info("reaper_swept", { metric: "bg_agents_reaped_total", count: rows, sessionsReleased: sessions, skippedNoSessions: skipped });
1422
- }
1423
- }
1424
- : undefined;
1425
- const probeClockSkew = async () => {
1426
- try {
1427
- const dbMs = await backend?.dbNowMs?.();
1428
- if (typeof dbMs === "number" && Number.isFinite(dbMs)) {
1429
- metrics.setGauge("fleet_counter_bucket_skew_ms", dbMs - Date.now());
1430
- metrics.setGauge("fleet_clock_probe_ok", 1);
1431
- }
1432
- }
1433
- catch {
1434
- metrics.setGauge("fleet_clock_probe_ok", 0);
1435
- }
1436
- };
1437
- const reaper = setInterval(() => {
1438
- void probeClockSkew().catch(() => undefined);
1439
- void runStore?.reapStale(config.runStaleSec * 1000).then(reapCount("runs_reaped_total", { kind: "stale" })).catch(() => undefined);
1440
- if (checkpointStore) {
1441
- void checkpointStore.reapExpired(Date.now()).then(reapCount("checkpoints_reaped_total", {})).catch(() => undefined);
1442
- void runStore?.failSuspendedWithExpiredCheckpoint().then(reapCount("runs_reaped_total", { kind: "expired_checkpoint" })).catch(() => undefined);
1443
- void runDenySweep?.(Date.now()).catch(() => undefined);
1444
- }
1445
- if (config.approvalTimeoutSec > 0) {
1446
- void approvalStore?.expireStale(config.approvalTimeoutSec * 1000).catch(() => undefined);
1447
- void runStore?.reapSuspended(config.approvalTimeoutSec * 1000).then(reapCount("runs_reaped_total", { kind: "suspended" })).catch(() => undefined);
1448
- }
1449
- void checkpointStore?.reapCtx(Date.now() - config.runStaleSec * 1000).catch(() => undefined);
1450
- rateLimiter?.sweep();
1451
- if (costQuota instanceof CostQuota)
1452
- costQuota.reap();
1453
- void toolResultStore?.reapOlderThan?.(Date.now() - config.toolResultTtlSec * 1000)?.catch(() => undefined);
1454
- void fileSnapshotStore?.sweepOrphanBlobs?.().catch(() => undefined);
1455
- void taskAttachmentStore?.reapUnbound(Date.now() - config.attachmentUnboundTtlMs).then(reapCount("attachments_reaped_total", {})).catch(() => undefined);
1456
- if (config.attachmentOrphanGraceMs > 0 && !attachmentSweepInFlight && taskAttachmentStore?.sweepOrphanObjects) {
1457
- attachmentSweepInFlight = true;
1458
- void taskAttachmentStore
1459
- .sweepOrphanObjects(config.attachmentOrphanGraceMs)
1460
- .then((n) => { if (n > 0) {
1461
- metrics.inc("attachment_orphan_objects_swept_total", {}, n);
1462
- logger.info("attachment_orphan_objects_swept", { removed: n });
1463
- } })
1464
- .catch((err) => logger.warn("attachment_orphan_sweep_failed", { err: String(err) }))
1465
- .finally(() => { attachmentSweepInFlight = false; });
1466
- }
1467
- void backend?.session()?.sweepStagingSessions?.().catch(() => undefined);
1468
- void imageBakes?.reapStaleBakes(config.imageBakes.staleMs).then(reapCount("bakes_reaped_total", {})).catch(() => undefined);
1469
- void worktreeReap?.();
1470
- void workflowNotifyGate?.recover({ orphanGraceMs: config.workflowOrphanGraceMs }).catch(() => undefined);
1471
- void workflowJournalStore?.reapExpired?.(Date.now(), config.workflowJournalRetentionMs).catch(() => undefined);
1472
- if (!wfRunReapInFlight) {
1473
- const sweep = sqlWorkflowRunStore?.reapAllScopes?.(Date.now(), { maxAgeMs: config.workflowRunRetentionMs });
1474
- if (sweep) {
1475
- wfRunReapInFlight = true;
1476
- void sweep.catch(() => undefined).finally(() => (wfRunReapInFlight = false));
1477
- }
1478
- void workflowNotifyJournal
1479
- ?.reapAcked?.(Date.now() - config.workflowRunRetentionMs)
1480
- .catch(() => undefined);
1481
- }
1482
- if (reapBgAgents && !bgAgentReapInFlight) {
1483
- bgAgentReapInFlight = true;
1484
- void reapBgAgents().catch(() => undefined).finally(() => (bgAgentReapInFlight = false));
1485
- }
1486
- void rosterStore
1487
- ?.reapOlderThan?.(Date.now() - config.rosterRetentionMs)
1488
- .then(reapCount("roster_rows_reaped_total", {}))
1489
- .catch(() => undefined);
1490
- if (config.scratchpadSweepTtlMs > 0) {
1491
- void sweepStaleScratchpads(config.localDataRoot ?? localRoot, { olderThanMs: config.scratchpadSweepTtlMs })
1492
- .then((removed) => {
1493
- if (removed > 0)
1494
- logger.info("scratchpads_swept", { removed });
1495
- })
1496
- .catch(() => undefined);
1497
- }
1498
- }, config.reapIntervalSec * 1000);
1499
- reaper.unref?.();
596
+ // design/158 A10:reaper 段搬到 src/boot/reapers.ts(逐字;deny-sweep 晚绑改取值,见该文件头注)
597
+ const reaper = startReapers({
598
+ config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, approvalStore,
599
+ rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap,
600
+ workflowNotifyGate, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore,
601
+ backgroundAgentStore, mailboxStore,
602
+ getRunDenySweep: () => runDenySweep,
603
+ });
604
+ // Optional OTLP/HTTP metrics export (1.37). Periodically pushes the registry to an OTel collector;
605
+ // best-effort (a collector outage is logged, never affects serving). /metrics stays available too.
1500
606
  const otelExporter = config.otel
1501
607
  ? startOtlpExporter(metrics, {
1502
608
  ...config.otel,
1503
609
  onError: (err) => logger.warn("otel_export_failed", { err: String(err) }),
1504
610
  })
1505
611
  : undefined;
1506
- if (configProvider) {
1507
- const ccRef = config.configCenter;
1508
- let refreshInFlight = false;
1509
- const refreshTick = async (prefetched) => {
1510
- if (refreshInFlight)
1511
- return;
1512
- refreshInFlight = true;
1513
- try {
1514
- await retryPendingCatalog("refresh-retry");
1515
- if (planeDeferredNoHandoff) {
1516
- metrics.inc("models_tiers_plane_deferred_stuck_total");
1517
- 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
- }
1519
- const r = prefetched !== undefined ? prefetched : await configProvider.fetchEffective(ccEtag);
1520
- if (r === null && planeDeferredNoHandoff) {
1521
- const skillsDebt = (planeDeferredNoHandoff.blocked ?? []).includes("skills");
1522
- const promoted = lkgSurvivesRestart || skillsDebt ? await persistLkgDurable(planeDeferredNoHandoff.candidate, planeDeferredNoHandoff.candidateEtag) : false;
1523
- const skillsProofPromotion = !promoted && !lkgSurvivesRestart && skillsDebt && lastPrewarmBodiesVerified && planeDeferredNoHandoff.planeDeferred !== true;
1524
- if (skillsProofPromotion) {
1525
- pendingRestart = { restartRequired: true, reasons: ["skills"], version: planeDeferredNoHandoff.version, since: Date.now() };
1526
- logger.info("config_boot_skill_debt_promoted", { version: planeDeferredNoHandoff.version, note: "skill bodies verified on a source-confirmed-current candidate — the withheld skills restart is now published (non-durable lane, repair proven)" });
1527
- planeDeferredNoHandoff = undefined;
1528
- }
1529
- else if (promoted) {
1530
- const promotedReasons = [...new Set([...(planeDeferredNoHandoff.blocked ?? []), ...(planeDeferredNoHandoff.planeDeferred ? ["models-tiers"] : [])])];
1531
- if (promotedReasons.length > 0) {
1532
- pendingRestart = { restartRequired: true, reasons: promotedReasons, version: planeDeferredNoHandoff.version, since: Date.now() };
1533
- }
1534
- logger.info("models_tiers_deferred_promoted", { version: planeDeferredNoHandoff.version, reasons: promotedReasons, note: promotedReasons.length > 0 ? "handoff persistence recovered on a source-confirmed-current candidate — the withheld restart signal is now published" : "boot publication retry completed — LKG now durable, no restart needed (nothing was withheld)" });
1535
- planeDeferredNoHandoff = undefined;
1536
- }
1537
- }
1538
- if (r) {
1539
- const promptsRaw = r.effective.prompts;
1540
- const promptsGate = promptsRaw !== undefined ? validatePromptsDomain(promptsRaw) : { ok: true };
1541
- const badDomains = [
1542
- ...new Set([...(r.domainErrors ?? []).map((de) => de.domain), ...(promptsGate.ok ? [] : ["prompts"])]),
1543
- ]
1544
- .sort()
1545
- .join(",");
1546
- if (badDomains) {
1547
- if (lastRejectedCandidate?.version !== r.effective.version || lastRejectedCandidate.domains !== badDomains) {
1548
- lastRejectedCandidate = { version: r.effective.version, domains: badDomains, at: Date.now() };
1549
- logger.warn("config_candidate_rejected", {
1550
- version: r.effective.version,
1551
- domains: badDomains,
1552
- errors: [
1553
- ...(r.domainErrors ?? []).map((de) => ({ domain: de.domain, error: de.error })),
1554
- ...(promptsGate.ok ? [] : [{ domain: "prompts", error: promptsGate.error }]),
1555
- ],
1556
- lkgVersion: latestEffective?.version ?? "(env)",
1557
- note: "candidate rejected whole (§9.5-5) — live config keeps the last-known-good; fix the named files to unblock",
1558
- });
1559
- }
1560
- return;
1561
- }
1562
- lastRejectedCandidate = undefined;
1563
- if (ccRef?.dryRun) {
1564
- logEffectiveDiff(config, r.effective, logger);
1565
- ccEtag = r.etag;
1566
- }
1567
- else {
1568
- const planeDeferred = (runnerTierFrozen || planeHasActiveTiers(r.effective)) && modelPlaneChanged(appliedPlaneEff, r.effective);
1569
- applyEffective(config, r.effective, logger, { teamsOnly: true, sealedKeys, ...(planeDeferred ? { deferModelPlane: true } : {}) });
1570
- if (planeDeferred) {
1571
- logger.warn("models_tiers_plane_deferred", { version: r.effective.version, note: "tier-frozen Runner: the changed model plane (models/roles/tiers/default) is NOT hot-applied — admission stays on the Runner's generation; restart applies the new plane (models-tiers restart signal rides /health)" });
1572
- }
1573
- else {
1574
- appliedPlaneEff = r.effective;
1575
- planeDeferredNoHandoff = undefined;
1576
- }
1577
- await adoptCenterPrompts(r.effective, "refresh");
1578
- if (!planeDeferred)
1579
- markRosterLanded(r.effective);
1580
- mutateInPlace(pricing, buildPricing(config.models));
1581
- keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1582
- const lkgPersisted = await persistLkgDurable(r.effective, r.etag);
1583
- const reasons = restartReasons(effective, r.effective);
1584
- const skillsChangeDetected = reasons.includes("skills");
1585
- if (bootSkillStale && !skillsChangeDetected)
1586
- reasons.push("skills");
1587
- if (planeDeferred) {
1588
- if (lkgPersisted) {
1589
- if (!reasons.includes("models-tiers"))
1590
- reasons.push("models-tiers");
1591
- planeDeferredNoHandoff = undefined;
1592
- }
1593
- else {
1594
- const blocked = reasons.splice(0, reasons.length);
1595
- 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
- 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
- }
1598
- }
1599
- else if (lkgSurvivesRestart && !lkgPersisted) {
1600
- const blocked = reasons.splice(0, reasons.length);
1601
- planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), blocked, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
1602
- 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
- }
1604
- else if (!planeDeferred && !lkgSurvivesRestart && bootSkillStale && !skillsChangeDetected && !lastPrewarmBodiesVerified) {
1605
- const i = reasons.indexOf("skills");
1606
- if (i >= 0) {
1607
- reasons.splice(i, 1);
1608
- planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), blocked: ["skills"], candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
1609
- logger.warn("config_boot_skill_debt_withheld", { version: r.effective.version, note: "boot skill debt still unproven on a non-durable deployment (body cache does not verify) — skills restart withheld this tick; re-verified each tick and published once the bodies prove fetchable" });
1610
- }
1611
- }
1612
- if (reasons.length === 0) {
1613
- pendingRestart = undefined;
1614
- }
1615
- else if (!pendingRestart || pendingRestart.reasons.join(",") !== reasons.join(",")) {
1616
- pendingRestart = { restartRequired: true, reasons, version: r.effective.version, since: Date.now() };
1617
- }
1618
- else {
1619
- pendingRestart = { ...pendingRestart, version: r.effective.version };
1620
- }
1621
- logger.info("sema_registry_refreshed", {
1622
- version: r.effective.version,
1623
- note: "teams + runtime governance + models/roles/roster hot-applied; skills/mcp/runtime-gates/scenarios are restart-to-apply",
1624
- ...(pendingRestart ? { restartRequired: true, restartReasons: pendingRestart.reasons } : {}),
1625
- });
1626
- latestEffective = r.effective;
1627
- ccEtag = r.etag;
1628
- }
1629
- }
1630
- }
1631
- catch (err) {
1632
- logger.warn("sema_registry_refresh_failed", { err: String(err), note: "etag NOT advanced — the same candidate is re-fetched and re-applied next poll" });
1633
- }
1634
- finally {
1635
- refreshInFlight = false;
1636
- }
1637
- };
1638
- const ccTimer = setInterval(() => void refreshTick(), 60_000);
1639
- ccTimer.unref?.();
1640
- const deferredBootApply = async (r) => {
1641
- if (!r)
1642
- return;
1643
- if (refreshInFlight)
1644
- return;
1645
- refreshInFlight = true;
1646
- try {
1647
- await deferredBootApplyInner(r);
1648
- }
1649
- finally {
1650
- refreshInFlight = false;
1651
- }
1652
- };
1653
- const deferredBootApplyInner = async (r) => {
1654
- const promptsRaw = r.effective.prompts;
1655
- const promptsGate = promptsRaw !== undefined ? validatePromptsDomain(promptsRaw) : { ok: true };
1656
- if ((r.domainErrors ?? []).length > 0 || !promptsGate.ok) {
1657
- logger.warn("config_boot_deferred_candidate_rejected", {
1658
- version: r.effective.version,
1659
- errors: [...(r.domainErrors ?? []).map((de) => ({ domain: de.domain, error: de.error })), ...(promptsGate.ok ? [] : [{ domain: "prompts", error: promptsGate.error }])],
1660
- note: "late boot pull carried an invalid candidate — env fallback keeps serving; the refresh cadence re-judges the same tree (etag not advanced)",
1661
- });
1662
- return;
1663
- }
1664
- if (ccRef?.dryRun) {
1665
- logEffectiveDiff(config, r.effective, logger);
1666
- ccEtag = r.etag;
1667
- return;
1668
- }
1669
- const planeDeferredLate = (runnerTierFrozen || planeHasActiveTiers(r.effective)) && modelPlaneChanged(appliedPlaneEff, r.effective);
1670
- applyEffective(config, r.effective, logger, { teamsOnly: true, sealedKeys, ...(planeDeferredLate ? { deferModelPlane: true } : {}) });
1671
- if (planeDeferredLate)
1672
- logger.warn("models_tiers_plane_deferred", { version: r.effective.version, note: "tier-frozen Runner (env tiers): the late-boot center model plane is NOT hot-applied — restart applies it" });
1673
- else {
1674
- appliedPlaneEff = r.effective;
1675
- planeDeferredNoHandoff = undefined;
1676
- }
1677
- await adoptCenterPrompts(r.effective, "boot-deferred");
1678
- if (!planeDeferredLate)
1679
- markRosterLanded(r.effective);
1680
- mutateInPlace(pricing, buildPricing(config.models));
1681
- keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1682
- effective = r.effective;
1683
- latestEffective = r.effective;
1684
- ccEtag = r.etag;
1685
- const lkgPersistedLate = await persistLkgDurable(r.effective, r.etag);
1686
- const reasons = restartReasons(undefined, r.effective);
1687
- if (planeDeferredLate) {
1688
- if (lkgPersistedLate) {
1689
- if (!reasons.includes("models-tiers"))
1690
- reasons.push("models-tiers");
1691
- planeDeferredNoHandoff = undefined;
1692
- }
1693
- else {
1694
- const blocked = reasons.splice(0, reasons.length);
1695
- 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
- 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
- }
1698
- }
1699
- else if (planeDeferredLate === false && lkgSurvivesRestart && !lkgPersistedLate) {
1700
- const blocked = reasons.splice(0, reasons.length);
1701
- planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), blocked, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
1702
- 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" });
1703
- }
1704
- if (reasons.length > 0)
1705
- pendingRestart = { restartRequired: true, reasons, version: r.effective.version, since: Date.now() };
1706
- 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
- };
1708
- if (bootConfigPending)
1709
- void bootConfigPending.then((r) => (lkgBooted ? refreshTick(r) : deferredBootApply(r)), () => { });
1710
- }
1711
- let keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1712
- if (process.env.ANTHROPIC_AUTH_TOKEN && !process.env.ANTHROPIC_API_KEY && (Object.keys(config.modelApiKeys).length > 0 || Object.keys(config.modelApiKeyEnv).length > 0)) {
1713
- logger.warn("per_model_key_bearer_gateway_combo", {
1714
- perModelKeys: Object.keys(config.modelApiKeys).length + Object.keys(config.modelApiKeyEnv).length,
1715
- 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
- });
1717
- }
1718
- const mysqlPool = backend?.mysqlPool();
1719
- const auditPgPool = backend?.pgPool();
1720
- const auditLocalRoot = backend?.kind === "local" ? (config.localDataRoot ?? localRoot) : undefined;
1721
- const ownerAware = sessionStore;
1722
- const sessionAudit = mysqlPool || auditPgPool || auditLocalRoot
1723
- ? async (sessionId) => {
1724
- const audit = mysqlPool
1725
- ? await buildSessionAudit(mysqlPool, sessionId)
1726
- : auditPgPool
1727
- ? await buildSessionAuditPg(auditPgPool, sessionId)
1728
- : await buildSessionAuditLocal(auditLocalRoot, sessionId);
1729
- if (!audit)
1730
- return undefined;
1731
- return { ...audit, owner: (await ownerAware.ownerOf?.(sessionId)) ?? null };
1732
- }
1733
- : undefined;
1734
- const sessionWatchRegistry = ownerAware.getHead && ownerAware.ownerOf
1735
- ? new SessionWatchRegistry(async (sid) => (await ownerAware.getHead(sid)) ?? { owner: undefined, leafId: null }, {
1736
- hotMs: posIntEnv(process.env.SESSION_EVENTS_HOT_MS, 200),
1737
- warmMs: posIntEnv(process.env.SESSION_EVENTS_WARM_MS, 2000),
1738
- maxWatchedSessions: posIntEnv(process.env.SESSION_EVENTS_MAX_PROBES, 512, 100_000),
1739
- })
1740
- : undefined;
1741
- setLeafAdvanceListener(sessionWatchRegistry ? (sid, leaf, owner, seq) => sessionWatchRegistry.notifyLocal(sid, leaf, owner, seq) : undefined);
1742
- const purgeSession = ownerAware.deleteSession
1743
- ? async (sessionId, owner) => {
1744
- const runs = runStore ? await runStore.deleteBySession(sessionId, owner) : { removed: 0 };
1745
- if ("active" in runs)
1746
- return { active: runs.active };
1747
- if (checkpointStore)
1748
- await checkpointStore.deleteBySession(sessionId, owner);
1749
- if (toolResultStore)
1750
- await toolResultStore.deleteBySession?.(sessionId);
1751
- if (resumeAnchorStore)
1752
- await resumeAnchorStore.deleteBySession(sessionId);
1753
- if (approvalExemptionStore)
1754
- await approvalExemptionStore.deleteBySession(sessionId);
1755
- if (sessionPolicyStore?.deleteBySession)
1756
- await sessionPolicyStore.deleteBySession(sessionId);
1757
- if (taskAttachmentStore)
1758
- await taskAttachmentStore.deleteBySession(sessionId);
1759
- if (fileSnapshotStore?.deleteBySession)
1760
- await fileSnapshotStore.deleteBySession(sessionId);
1761
- if (workflowCompletionInbox)
1762
- await workflowCompletionInbox.purge(sessionId, owner ?? null).catch((err) => logger.warn("workflow_completion_inbox_purge_failed", { sessionId, err: String(err) }));
1763
- await purgeScratchpadDir(config.localDataRoot ?? localRoot, sessionId);
1764
- try {
1765
- const reaped = defaultTaskRegistry.reapSessionBackground(sessionId, owner ?? "default");
1766
- if (reaped > 0)
1767
- logger.info("session_background_reaped", { sessionId, reaped });
1768
- }
1769
- catch (err) {
1770
- logger.warn("session_background_reap_failed", { sessionId, err: String(err) });
1771
- }
1772
- const historyDeleted = await ownerAware.deleteSession(sessionId, owner);
1773
- sessionWatchRegistry?.dropSession(sessionId);
1774
- return { deleted: historyDeleted || runs.removed > 0 };
1775
- }
1776
- : undefined;
1777
- const instrumentDegenerate = mysqlPool ? makeDegenerateInstrument(mysqlPool, metrics, logger) : undefined;
1778
- const planCacheProbe = new PlanCacheProbe(metrics, logger);
1779
- const leaderMinio = process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
1780
- ? {
1781
- s3: {
1782
- endpoint: process.env.MINIO_ENDPOINT,
1783
- bucket: process.env.MINIO_BUCKET ?? "workspaces",
1784
- accessKey: process.env.MINIO_ACCESS_KEY,
1785
- secretKey: process.env.MINIO_SECRET_KEY,
1786
- ...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
1787
- },
1788
- }
1789
- : {};
1790
- const leaderProvider = config.remoteExec?.provider;
1791
- const leaderEndpoint = config.leaderEnabled &&
1792
- (leaderProvider === "e2b" ||
1793
- (leaderProvider === "k8s" && "s3" in leaderMinio && executionEnvFactory) ||
1794
- (leaderProvider === "host" && executionEnvFactory))
1795
- ? createLeaderEndpoint(createLeaderRunner({
1796
- brain, models: config.models, roles: config.roles, pricing, logger,
1797
- fanoutEnabled: config.leaderFanoutEnabled,
1798
- ...((process.env.MODEL_ROUTER_ID || process.env.MODEL_CHEAP_ID)
1799
- ? { routerModel: (process.env.MODEL_ROUTER_ID || process.env.MODEL_CHEAP_ID) }
1800
- : {}),
1801
- ...(leaderProvider === "e2b" ? { e2bApiKey: config.remoteExec.apiKey } : {}),
1802
- ...(executionEnvFactory ? { envFactory: executionEnvFactory } : {}),
1803
- ...leaderMinio,
1804
- ...(toolResultStore ? { toolResultStore } : {}),
1805
- ...(leaderProvider === "k8s" ? { workspace: "/workspace" } : {}),
1806
- sessionStore,
1807
- ...(checkpointStore && config.approvalRequire.length > 0
1808
- ? {
1809
- durable: {
1810
- checkpointStore,
1811
- requireApproval: config.approvalRequire,
1812
- deny: config.approvalDeny,
1813
- autoBudget: config.approvalAutoBudget,
1814
- neverAuto: config.approvalNeverAuto,
1815
- ...(config.approvalTimeoutSec > 0 ? { ttlMs: config.approvalTimeoutSec * 1000 } : {}),
1816
- },
1817
- }
1818
- : {}),
1819
- }), {
1820
- logger,
1821
- requiredFields: ["objective", "durableRemote", "testCmd", "seedCmd", "baseSha"],
1822
- })
1823
- : undefined;
1824
- if (leaderEndpoint)
1825
- logger.info("leader_endpoint_enabled", { provider: leaderProvider, mode: "s3" in leaderMinio ? "factory" : "static" });
612
+ // design/158 A10 尾刀:60s refresh 循环 + boot-deferred 到货续接搬到 boot/config-center.ts(逐字)
613
+ // ⚠️ 位置即契约:必须在 new Runner 之后 —— runnerTierFrozen 与构造函数读的是同一 config 代际。
614
+ configCenter.startRefreshLoop({ runnerTierFrozen, pricing });
615
+ // design/158 A10 尾刀:per-model key resolver 首建(+ Bearer 网关组合告警)搬到 boot/config-center.ts。
616
+ // ⚠️ 位置即契约:首建读的必须是 boot apply **之后**的 config.modelApiKeyEnv/modelApiKeys。
617
+ configCenter.initKeyResolver();
618
+ // design/158 A10:session 只读/删除面搬到 src/boot/session-faces.ts(逐字;watch→purge 次序即契约,见该文件头注)。
619
+ const { ownerAware, sessionAudit, sessionWatchRegistry, purgeSession, instrumentDegenerate, planCacheProbe } = createSessionFaces({
620
+ config, logger, metrics, localRoot, backend, sessionStore, runStore, checkpointStore, toolResultStore,
621
+ resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileSnapshotStore,
622
+ workflowCompletionInbox,
623
+ });
624
+ // design/158 A10:leader 段搬到 src/boot/leader.ts(逐字)。
625
+ const leaderEndpoint = createLeaderFace({ config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore });
626
+ // Graceful drain: shared mutable state — SIGTERM flips `draining` (shutdown below), createHttpServer
627
+ // assigns `inflight` (live leg count on this instance), /health + the submit 503 gate read it.
1826
628
  const drainState = { draining: false };
629
+ // 鉴权桥(registry-core api/auth-bridge):SEMA_REGISTRY_URL 在位即点亮(零新配置——worker 已信
630
+ // registry 的 effective config,同一信任根延伸到其 JWKS);split 部署用 AUTH_BRIDGE_ISSUER/JWKS_URL/AUDIENCE
631
+ // 显式覆盖。缺 registry(纯 env 部署)=分支不存在,bearer 行为与今天字节一致。
1827
632
  const authBridgeIssuer = process.env.AUTH_BRIDGE_ISSUER || config.configCenter?.baseUrl;
1828
633
  const registryJwtVerifier = authBridgeIssuer
1829
634
  ? createRegistryJwtVerifier({
@@ -1836,520 +641,170 @@ async function main() {
1836
641
  : undefined;
1837
642
  if (registryJwtVerifier)
1838
643
  logger.info("auth_bridge_enabled", { issuer: authBridgeIssuer });
644
+ // asyncRewake wake 管道 holder:server 启动时装 deliver(session→本副本活流 steer);createTaskHooks 的
645
+ // ctx.wake 闭包经它投递(main 建壳/server 填实,双向不循环依赖)。
1839
646
  const hookWakeBus = {};
647
+ // ═══════════ design/158 A8:ServiceDeps 分组装配(七组窄接口)═══════════
648
+ // 每组一个**带类型标注**的中间 const —— 标注位触发 TS 多余属性检查,键放错组当场编译红。
649
+ // (平铺形做不到:69 键的全可选面里任何键名都「属于」它,于是键落错家/从未接线只能靠源码钉
650
+ // 逐个盯 —— 1.300/1.301/1.304/1.307/1.310 五起真缺陷都是这个病族。)组形状门见
651
+ // test/deps-literal-shape-gate.test.ts;消费点仍读平铺 `deps.x`(createHttpServer 入口摊平)。
652
+ /** durable 持久面(键 present ⇔ 路由/能力位在场;absent ⇒ 501 诚实缺席) */
1840
653
  const stores = {
1841
654
  runStore,
1842
- resumeAnchorStore: resumeAnchorStore ? resumeAnchorStore : undefined,
655
+ resumeAnchorStore: resumeAnchorStore ? resumeAnchorStore : undefined, // E18 resume-at eventId→entryId map (tidb/pg/local)
1843
656
  approvalStore,
1844
- approvalExemptionStore: approvalExemptionStore ? approvalExemptionStore : undefined,
657
+ approvalExemptionStore: approvalExemptionStore ? approvalExemptionStore : undefined, // decide remember="session" + list/revoke
1845
658
  checkpointStore,
1846
- sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined,
659
+ sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined, // E6 operator session-rule store (PUT/GET /v1/sessions/:id/policy)
660
+ // E19 cap: core's gate-split (1.134.0) REMOVED the isRemoteExecutionEnv skip — core now snapshots each
661
+ // completed turn + restores on resumeAt for ANY ExecutionEnv when fileSnapshotStore is wired (captureManifest/
662
+ // applyManifest run over any env's FileSystem ops; a 30s timeout bounds a slow remote walk). So rewind works for
663
+ // host/e2b/k8s/ssh/adb/local-docker AND the in-process worker → advertise `rewindFiles` whenever the store is wired.
1847
664
  fileSnapshotStore: fileSnapshotStore ? fileSnapshotStore : undefined,
1848
- taskAttachmentStore: taskAttachmentStore ? taskAttachmentStore : undefined,
665
+ taskAttachmentStore: taskAttachmentStore ? taskAttachmentStore : undefined, // D-1 上传/取回/删除三动词面
666
+ // design/153 件3d(/decide parked 赎回腿):durable bg 行店 + boot 裸 Agent 工具,与 RunnerDeps/
667
+ // scenarioDeps 同实例(claim/expire/consumeParkedFlip 作用于同一行)。任一缺席=分支不存在。
1849
668
  backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
669
+ // §0.5 session-ownership: the durable session abstraction (SessionRepo-style list/fork/delete) + the delete
670
+ // coordinator. `ownerAware` is the (possibly cache-wrapped) session store; both delegate to the DB-backed
671
+ // TiDB/PG store. Undefined-safe: on an in-memory dev store these carry no SessionRepo ops → the routes 501.
1850
672
  sessionStorage: ownerAware,
1851
673
  workflowRunStore: workflowRunStore ? workflowRunStore : undefined,
1852
- workflowJournalStore: workflowJournalStore ? workflowJournalStore : undefined,
674
+ workflowJournalStore: workflowJournalStore ? workflowJournalStore : undefined, // [1402] journal 读面(GET /v1/workflows/:id/journal 数据源,与 runner 同实例)
1853
675
  imageIndex,
1854
676
  imageBakes,
1855
- outcomeSink: outcomeSink ? outcomeSink : undefined,
1856
- sendFileLedger: sendFileLedger ? sendFileLedger : undefined,
677
+ outcomeSink: outcomeSink ? outcomeSink : undefined, // P2: GET /v1/outcomes 读面(design/73 §7.2 仪表盘底料)
678
+ sendFileLedger: sendFileLedger ? sendFileLedger : undefined, // SendUserFile 账本读面:GET /v1/sendfile-links(owner-scope 镜像 sessions list)
679
+ // 2c session-sync (P1d): the WHOLE backend so the /v1/sessions/:id/sync/* routes can call exportSession/
680
+ // importSession (they fan out across session entries + fileSnapshot + sessionPolicy + resumeAnchor, which must
681
+ // come from ONE backend in the §8 atomic order). Undefined-safe: the env-only/no-backend worker 501s the routes.
1857
682
  backend: backend ? backend : undefined,
1858
683
  };
684
+ /** 进程内活体协调面(本副本 only,重启即空:HITL 协调器/可 steer 句柄注册表/总线/收件箱) */
1859
685
  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,
686
+ elicitation: elicitation ? elicitation : undefined, // E23: live-only inbound-elicitation coordinator (respond route + ALS wraps)
687
+ question: question ? question : undefined, // §4④: AskUserQuestion live coordinator (respond route + ALS wraps on streaming legs)
688
+ toolApproval: toolApproval ? toolApproval : undefined, // [816]/[820]②: live tool-approval coordinator (respond route + ALS wrap on the sync streaming leg)
689
+ workflowAgentRegistry: workflowAgentRegistry ? workflowAgentRegistry : undefined, // SVC-5: steerable workflow-agent handle registry
690
+ subagentSteerRegistry, // C2 (core 1.219): steerable Task-subagent handle registry (POST /v1/runs/:id/subagents/:target/steer)
691
+ workflowCompletionInbox: workflowCompletionInbox ? workflowCompletionInbox : undefined, // P1 ①②: drain → workflow_complete frame on stream-open
1866
692
  fleetBus,
1867
- sessionWatch: sessionWatchRegistry ? sessionWatchRegistry : undefined,
693
+ // [1196] session SSE 订阅:watch registry(N 订阅共享一条探针,自适应拍距,无订阅即停)。谓词与
694
+ // /head 探针同门(getLeafId+ownerOf);缺 store 面不建=路由 501+capabilities.sessionEvents=false。
695
+ sessionWatch: sessionWatchRegistry ? sessionWatchRegistry : undefined, // [1196] 建于 purgeSession 之前(delete 即 dropSession 的同副本围栏)
1868
696
  hookWakeBus,
1869
- sendUserFile: sendUserFileEmitter ? sendUserFileEmitter : undefined,
697
+ sendUserFile: sendUserFileEmitter ? sendUserFileEmitter : undefined, // SendUserFile 切片2:file_link 帧 ALS(bg/sync 腿 wrap)
1870
698
  leaderEndpoint,
1871
- sessionTitler: sessionTitler ? sessionTitler : undefined,
699
+ sessionTitler: sessionTitler ? sessionTitler : undefined, // fire-and-forget auto-title at first submit
1872
700
  };
701
+ /** 函数/构件缝(main 把跨子系统的动作编译成一个可调用面交给路由) */
1873
702
  const seams = {
1874
703
  sessionAudit,
1875
704
  purgeSession: purgeSession ? purgeSession : undefined,
705
+ // [1488]③(b): bg agent-handle read face — GET /v1/runs/:id/subagents/:handle/output → the same registry
706
+ // face the TaskOutput tool reads (defaultTaskRegistry is the process singleton every run leg registers
707
+ // its background children into). Access tuple is derived from the run row in server.ts, never the caller.
708
+ // codex R2: kind-gated BEFORE the poll (backgroundAgentOutput) — generic pollTask has side effects on
709
+ // other kinds (workflow onServedTerminal ack, bash output-cursor advance).
1876
710
  subagentTaskOutput: (handle, access) => backgroundAgentOutput(defaultTaskRegistry, handle, access, backgroundAgentStore),
711
+ // [1499] CC TaskOutput/TaskStop 人侧对位 — the GENERIC task-handle verbs over the same registry singleton
712
+ // (kind gate at the seam: bash/monitor/agent pass, workflow refused — journal face owns it).
1877
713
  taskHandleOutput: (handle, access) => taskHandleOutput(defaultTaskRegistry, handle, access, backgroundAgentStore),
1878
714
  taskHandleStop: (handle, access) => taskHandleStop(defaultTaskRegistry, handle, access, backgroundAgentStore),
715
+ // 142-S5 §1.4: GET /v1/memory/export 数据源(DB memory plane only;file 形态 route 501)。
1879
716
  memoryExport: memoryExportBackend ? (scope) => exportMemoryScope(memoryExportBackend, scope) : undefined,
717
+ // 142-S2.5: POST /v1/memory/sync/:scope 执行位(同一 DB entry plane + sync_cursors 游标面;
718
+ // plan 随数据走——core 纯函数在 performMemorySync 内直调,file 形态 route 501)。
1880
719
  memorySync: memoryExportBackend && memorySyncCursors
1881
720
  ? (scope, syncReq) => performMemorySync(memoryExportBackend, memorySyncCursors, scope, syncReq)
1882
721
  : undefined,
722
+ // sessionMirror 观测面(server 非执法端,论证在 ServiceDeps.sessionMirrorRuling):
723
+ // 与 executionRuling 同车同缓存(零额外 center RTT);无 center/dry-run ⇒ 不接线,观测面暗、零行为差。
1883
724
  sessionMirrorRuling: principalCaps
1884
725
  ? async (p) => (await principalCaps.executionRuling(p))?.sessionMirror
1885
726
  : undefined,
1886
727
  instrumentDegenerate,
1887
- sideQueryAccounting,
728
+ sideQueryAccounting, // [1469] side-query 四路 sink 记账(与 tracer 同源装配)
1888
729
  parkedReviveTool: parkedReviveTool ? parkedReviveTool : undefined,
730
+ // design/158 S2(F-1)修:工厂在 :2308 造好、server.ts 消费分支在等,但从未进本实参段——HIGH-1
731
+ // 同族第二例(条件 spread 的键从不接线,编译期无声)。缺席后果=带 requiresParentConstraint 的
732
+ // parked checkpoint 重启后恒被 core pre-CAS 门拒,**不可赎回**。接线钉:test/parked-revive-gate-wiring.test.ts。
1889
733
  parkedReviveInheritedGate: parkedReviveInheritedGate ? parkedReviveInheritedGate : undefined,
1890
734
  };
735
+ /** 可观测面(全体 fail-open,观测失败绝不改业务面行为) */
1891
736
  const observability = {
1892
737
  logger,
1893
738
  metrics,
1894
- modelUsage: modelUsageTracker,
1895
- promptManifests: promptManifestTracker,
739
+ modelUsage: modelUsageTracker, // E8: shared with createTracer above → runs drain it into the modelUsage echo
740
+ promptManifests: promptManifestTracker, // [998]②: shared with createTracer → the durable legs drain prompt_assembled events
1896
741
  planCacheProbe,
1897
742
  };
743
+ /** 准入/治理面(每件都是拒绝的权力;缺席 = 该道门不存在,不是「默认放行的门」) */
1898
744
  const governance = {
1899
745
  authorize,
1900
746
  registryJwtVerifier: registryJwtVerifier ? registryJwtVerifier : undefined,
1901
747
  rateLimiter,
1902
748
  costQuota,
1903
- fleetLease: fleetLease ? fleetLease : undefined,
749
+ fleetLease: fleetLease ? fleetLease : undefined, // lease admission 门(提交面,镜像 quotaExceeded)
1904
750
  };
751
+ /** 部署自述面(/health + /v1/capabilities + 提交前置门读的部署事实;多为 live getter) */
1905
752
  const deployment = {
753
+ // 海外 pilot 实机发现:/health 曾 verbatim 回显 sessionBackend 枚举——auto/DB_BACKEND 收编把它
754
+ // coerce 成 LABEL "tidb"(泛指 durable),pg 部署上误读成 SQL-TiDB 带偏排障。回显有效 store(与 listening
755
+ // 日志 session 标签同款诚实修)。
1906
756
  sessionStoreLabel: config.sessionBackend === "tidb" && backend ? `durable(${backend.kind})` : config.sessionBackend,
1907
- modelReady: () => modelReadyState.ready,
1908
- scenarioDetails,
1909
- workflowsCapable,
757
+ modelReady: () => configCenter.modelReady(), // boot ready 门:roster 未落地=计费提交 503+health ready:false
758
+ scenarioDetails, // 场景详情只读面(内建+center 同判定源)
759
+ workflowsCapable, // ① core split: capabilities.workflows = engine-can (workflowsCapability); workflowsList = store (below)
1910
760
  instanceId,
761
+ // Capability discovery: the deployment facts only main knows. Store-derived booleans
762
+ // (asyncRuns/artifacts/approvals/leader) are computed inside the server from the deps themselves.
763
+ // pricingConfigured disambiguates costMicroUsd=0 ("free run" vs "MODEL_COST_* not configured") —
764
+ // it is worker-level config, so one flag here beats decorating every result's stats.
1911
765
  capabilities: {
1912
766
  version: serviceVersion(),
1913
767
  scenarios: Object.keys(scenarios),
1914
768
  pricingConfigured: Object.values(pricing).some((p) => Object.values(p).some((v) => typeof v === "number" && v > 0)),
1915
769
  },
1916
- restartState: () => pendingRestart,
1917
- planeDeferredState: () => planeDeferredNoHandoff,
770
+ // center: orchestrator polls GET /health for this — set by the sema-registry refresh loop when a
771
+ // pull's restart-to-apply slices differ from boot. A live getter (not a snapshot) so /health always reads current.
772
+ restartState: () => configCenter.restartState(),
773
+ planeDeferredState: () => configCenter.planeDeferredState(), // codex R18: /health.modelPlaneDeferred (stuck-deferred plane, no handoff)
774
+ // shared drain state — SIGTERM flips `draining`, createServer assigns `inflight`, /health mirrors it.
1918
775
  drainState,
1919
- storeDegraded: storeBackendDegraded,
776
+ storeDegraded: storeBackendDegraded, // S5: /health twin of the store_backend_degraded gauge
1920
777
  };
778
+ /** 数值旋钮(缺省写在消费点,此处只承载覆写) */
1921
779
  const knobs = {
780
+ // (a) SQL 默认店的 per-blob 帽 → PUT /sync/blobs 前置 413(与 SqlBlobBackend 方言默认同源:
781
+ // 显式 env > tidb 的 6MiB entry-限默认;pg 默认无帽=不前置)。
782
+ // 🔴 复审 2026-07-28(E-HIGH-1)修:此键是 **ServiceDeps** 字段,原先被 spread 进 `runnerDeps`
783
+ // (core 的 RunnerDeps 无此字段,条件 spread 绕过 TS 多余属性检查 ⇒ 编译期无声)⇒ HTTP 面恒
784
+ // undefined、413 前置是死枝、超帽仍走 502。落错家的死键病族(verify-cost 先例)——接线钉见
785
+ // test/review-2026-07-28-batch1.test.ts。
1922
786
  snapshotBlobSqlCapBytes: backend && backend.kind !== "local" && !config.snapshotBlobStore
1923
787
  ? (config.snapshotBlobSqlMaxBytes ?? (backend.kind === "mysql" ? SQL_BLOB_DEFAULT_MAX_BYTES : undefined))
1924
788
  : undefined,
789
+ // 帽只在 watch 面在场时才有意义(缺席时路由 501,读不到它)——保持与原条件段同门,不单独接线。
1925
790
  sessionEventsMaxConnections: sessionWatchRegistry ? posIntEnv(process.env.SESSION_EVENTS_MAX_CONNS, 256, 100_000) : undefined,
1926
791
  };
1927
- const resolveSpec = async (body, _req, auth, opts) => {
1928
- const requested = gateScenarioRequest(await principalCaps?.scenarioRuling(auth?.principal), body.scenario, config.defaultScenario);
1929
- gateExecutionLane(await principalCaps?.executionRuling(auth?.principal), config.remoteExec?.provider ?? "in-process");
1930
- const scenarioName = scenarios[requested] ? requested : "default";
1931
- if (scenarioName === "autonomous")
1932
- logger.warn("scenario_autonomous_deprecated", { alias: "code", note: "renamed by [891]; alias keeps finalVerification pinned — pass scenario:'code' (+ explicit finalVerification if wanted)" });
1933
- const cap = selectScenario(scenarios, scenarioName)(body, auth?.principal);
1934
- const centerDecls = centerPrompts?.declarations;
1935
- const appendLessPack = centerDecls
1936
- ? centerIdentityAssembled(centerDecls, scenarioName, hasConstitutionAnchors)
1937
- : providerDropsAppend(cap.promptProvider, typeof body.systemPrompt === "string" ? body.systemPrompt : undefined);
1938
- const acceptedAppend = acceptAppendSystemPrompt(body.appendSystemPrompt, (detail) => logger.warn("append_system_prompt_dropped", { detail, sessionId: auth?.sessionId ?? null }), appendLessPack);
1939
- const objective = typeof body.objective === "string" ? body.objective : "";
1940
- const parsedSettings = parseTaskSettings(body.settings);
1941
- if (opts?.leg === "fresh" && appendLessPack) {
1942
- const riderPresent = typeof body.appendSystemPrompt === "string" && body.appendSystemPrompt.length > 0;
1943
- if (riderPresent || parsedSettings.settings?.outputStyle) {
1944
- throw new HttpError(400, `${riderPresent ? "appendSystemPrompt" : "settings.outputStyle"} is not supported when the effective prompt is already assembled (constitution anchors in systemPrompt, or a center assembled-identity pack for this scenario): the assembler's pass-through path cannot mount the append slot — fold it into the assembled prompt instead`);
1945
- }
1946
- }
1947
- if (parsedSettings.deferred.length > 0) {
1948
- logger.warn("task_settings_deferred", { fields: parsedSettings.deferred, sessionId: auth?.sessionId ?? null });
1949
- }
1950
- let attachmentNotice;
1951
- {
1952
- const reqAtt = body.attachmentIds;
1953
- if (reqAtt !== undefined) {
1954
- if (!Array.isArray(reqAtt) || !reqAtt.every((x) => typeof x === "string" && x.length > 0 && x.length <= 64)) {
1955
- throw new HttpError(400, "`attachmentIds` must be an array of attachment ids (strings)");
1956
- }
1957
- if (reqAtt.length > 16)
1958
- throw new HttpError(400, "`attachmentIds` exceeds the per-task limit (16)");
1959
- if (reqAtt.length > 0) {
1960
- if (!taskAttachmentStore)
1961
- throw new HttpError(501, "attachmentIds require a store backend (DB_BACKEND=tidb|pg|local)");
1962
- if (!auth?.sessionId)
1963
- throw new HttpError(400, "`attachmentIds` requires a session-resolving deployment (no authorizer session)");
1964
- const sid = auth.sessionId;
1965
- const r = await bindAttachmentsForTask({
1966
- store: taskAttachmentStore,
1967
- owner: auth?.principal ?? "default",
1968
- ids: reqAtt,
1969
- sessionId: sid,
1970
- leg: opts?.leg === "fresh" ? "fresh" : "resume",
1971
- nowMs: Date.now(),
1972
- onMissing: (id) => {
1973
- if (opts?.leg === "fresh")
1974
- throw new HttpError(400, `attachment not found: ${id}`);
1975
- logger.warn("attachment_missing_on_resume", { sessionId: sid, id });
1976
- },
1977
- });
1978
- attachmentNotice = r.notice;
1979
- }
1980
- }
1981
- }
1982
- const execLane = config.remoteExec?.provider ?? "in-process";
1983
- if (typeof body.cwd === "string" && body.cwd.length > 0 && auth?.sessionId) {
1984
- if (cwdHonored(config) && isValidCwd(body.cwd))
1985
- setSessionCwd(auth.sessionId, body.cwd);
1986
- else if (inProcessSingleUserLane(config) && isValidCwd(body.cwd) && satisfiedByProcessCwd(body.cwd, realpathSync))
1987
- logger.debug("task_cwd_inherited", { lane: execLane, sessionId: auth.sessionId });
1988
- else
1989
- logger.warn("task_cwd_ignored", { honored: cwdHonored(config), lane: execLane, sessionId: auth.sessionId });
1990
- }
1991
- if (parsedSettings.settings?.shellEnv && auth?.sessionId) {
1992
- if (cwdHonored(config))
1993
- setSessionShellEnv(auth.sessionId, parsedSettings.settings.shellEnv);
1994
- else if (inProcessSingleUserLane(config)) {
1995
- const requested = parsedSettings.settings.shellEnv;
1996
- const mismatched = shellEnvMismatchCount(requested, process.env);
1997
- if (mismatched === 0)
1998
- logger.debug("task_shell_env_inherited", { lane: execLane, keys: Object.keys(requested).length, sessionId: auth.sessionId });
1999
- else
2000
- logger.warn("task_shell_env_ignored", { honored: false, lane: execLane, mismatchedKeys: mismatched, sessionId: auth.sessionId });
2001
- }
2002
- else
2003
- logger.warn("task_shell_env_ignored", { honored: false, lane: execLane, sessionId: auth.sessionId });
2004
- }
2005
- const taskHooks = (() => {
2006
- const hc = parsedSettings.settings?.hooks;
2007
- if (!hc)
2008
- return undefined;
2009
- if (config.requirePrincipal === true) {
2010
- logger.warn("task_hooks_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
2011
- return undefined;
2012
- }
2013
- const effMode = coercePermissionMode(body.permissionMode) ?? parsedSettings.settings?.permissions?.defaultMode;
2014
- return createTaskHooks(hc, {
2015
- logger,
2016
- sessionId: auth?.sessionId ?? "",
2017
- cwd: (auth?.sessionId ? perSessionCwd.get(auth.sessionId) : undefined) ?? process.cwd(),
2018
- ...(effMode ? { permissionMode: effMode } : {}),
2019
- ...(parsedSettings.settings?.shellEnv ? { shellEnv: parsedSettings.settings.shellEnv } : {}),
2020
- hookLlm,
2021
- hookAgent,
2022
- onHookNotice: (n) => fleetBus.publishHookNotice({
2023
- ...n,
2024
- ...(auth?.principal ? { ownerScope: auth.principal } : {}),
2025
- ...(auth?.sessionId ? { ownerSessionId: auth.sessionId } : {}),
2026
- }),
2027
- ...(auth?.sessionId
2028
- ? {
2029
- wake: async (text) => hookWakeBus.deliver
2030
- ?
2031
- hookWakeBus.deliver(auth.sessionId, `[hook asyncRewake] ${redactSecrets(text)}`)
2032
- : false,
2033
- }
2034
- : {}),
2035
- });
2036
- })();
2037
- const rawAddDirs = body.additionalDirectories;
2038
- const additionalDirectories = cwdHonored(config) ? parseAdditionalDirectories(rawAddDirs) : undefined;
2039
- if (rawAddDirs !== undefined && !cwdHonored(config)) {
2040
- logger.warn("task_additional_directories_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
2041
- }
2042
- const wireCatalog = expandTiers(config.models, config.tiers) ?? config.models;
2043
- const picked = resolveTaskModel(body.model ?? parsedSettings.settings?.model, objective, wireCatalog);
2044
- if (picked.unknownExplicit !== undefined) {
2045
- metrics.inc("task_model_unknown_fallback_total");
2046
- logger.warn("task_model_unknown_fallback", { requested: picked.unknownExplicit.slice(0, 120), fallback: picked.model, sessionId: auth?.sessionId ?? null });
2047
- }
2048
- if (Array.isArray(body.images) && body.images.length > 0 && !modelSupportsImages(wireCatalog[picked.model])) {
2049
- metrics.inc("images_omitted_total", { model: picked.model });
2050
- logger.warn("images_omitted_no_vision_model", { model: picked.model, count: body.images.length });
2051
- }
2052
- let resumeAtEntryId;
2053
- if (typeof body.resumeAt === "string" && body.resumeAt.length > 0) {
2054
- if (!auth?.sessionId)
2055
- throw new HttpError(422, "resumeAt requires a session to branch (resume_at.no_session)");
2056
- if (!resumeAnchorStore || !ownerAware.getLeafId)
2057
- throw new HttpError(501, "resume-at is not available on this worker (no session-store backend for the anchor map)");
2058
- resumeAtEntryId = await resumeAnchorStore.resolve(auth.sessionId, body.resumeAt, auth.principal ?? null);
2059
- if (resumeAtEntryId === undefined)
2060
- throw new HttpError(404, "resumeAt: no such message in this session (resume_at.unknown_event)");
2061
- }
2062
- let rewindFilesToEntryId;
2063
- if (resumeAtEntryId === undefined && typeof body.rewindFilesTo === "string" && body.rewindFilesTo.length > 0) {
2064
- if (!auth?.sessionId)
2065
- throw new HttpError(422, "rewindFilesTo requires a session (rewind_files_to.no_session)");
2066
- if (!resumeAnchorStore || !ownerAware.getLeafId)
2067
- throw new HttpError(501, "rewind-files-to is not available on this worker (no session-store backend for the anchor map)");
2068
- rewindFilesToEntryId = await resumeAnchorStore.resolve(auth.sessionId, body.rewindFilesTo, auth.principal ?? null);
2069
- if (rewindFilesToEntryId === undefined)
2070
- throw new HttpError(404, "rewindFilesTo: no such message in this session (rewind_files_to.unknown_event)");
2071
- }
2072
- const s4ProjectId = auth?.resolvedProjectId ?? (typeof body.projectId === "string" && body.projectId ? body.projectId : undefined);
2073
- const s4DefaultScopes = s4ProjectId ? config.projects[s4ProjectId]?.defaultScopes : undefined;
2074
- const spec = {
2075
- objective: attachmentNotice ? `${picked.cleanedObjective}\n\n${attachmentNotice}` : picked.cleanedObjective,
2076
- systemPrompt: typeof body.systemPrompt === "string" ? body.systemPrompt : undefined,
2077
- appendSystemPrompt: acceptedAppend,
2078
- sessionId: auth?.sessionId,
2079
- principal: auth?.principal,
2080
- images: body.images,
2081
- ...(body.clientContext ? { clientContext: { timeZone: body.clientContext.timeZone, userEmail: body.clientContext.userEmail } } : {}),
2082
- ...(() => {
2083
- const ct = body.compaction?.clampTolerance;
2084
- return typeof ct === "number" && Number.isFinite(ct) && ct >= 0 && ct <= 1 ? { compaction: { clampTolerance: ct } } : {};
2085
- })(),
2086
- outputSchema: body.outputSchema,
2087
- ...(typeof body.outputRetries === "number" && Number.isFinite(body.outputRetries) && body.outputRetries >= 1
2088
- ? { outputRetries: Math.min(10, Math.floor(body.outputRetries)) }
2089
- : {}),
2090
- resumeAt: resumeAtEntryId,
2091
- resumeAtMode: normalizeResumeAtMode(body.resumeAtMode, resumeAtEntryId !== undefined),
2092
- requireExistingSession: body.requireExistingSession === true ? true : undefined,
2093
- enableFork: enableForkFromBody(body, config, Boolean(centerRuntimeCapsResolver)),
2094
- suggestNextPrompts: normalizeSuggestNextPrompts(body.suggestNextPrompts),
2095
- rewindFiles: body.rewindFiles === true ? true : undefined,
2096
- rewindFilesTo: rewindFilesToEntryId,
2097
- additionalDirectories,
2098
- enablePlanMode: config.planModeEnabled ? true : undefined,
2099
- selfOrchestration: selfOrchestrationFromBody({ selfOrchestration: body.selfOrchestration === true || parsedSettings.settings?.ultracode === true }, config, Boolean(centerRuntimeCapsResolver)),
2100
- forwardSubagentEvents: body.forwardSubagentEvents === true ? true : undefined,
2101
- retainSubagentSessions: normalizeRetainSubagentSessions(body.retainSubagentSessions),
2102
- ...taskAgentsSpecFragment(body.agents, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })),
2103
- ...(retainBackgroundProcessesFromBody(body.retainBackgroundProcesses, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })) === true
2104
- ? { retainBackgroundProcesses: true }
2105
- : {}),
2106
- ...(() => { const v = toolNameListFromBody(body.excludeTools); return v ? { excludeTools: v } : {}; })(),
2107
- ...(() => { const v = toolNameListFromBody(body.deferTools); return v ? { deferTools: v } : {}; })(),
2108
- ...(() => { const v = promptProfileFromBody(body.promptProfile); return v ? { promptProfile: v } : {}; })(),
2109
- ...(typeof body.interactiveTools === "boolean"
2110
- ? { interactiveTools: body.interactiveTools }
2111
- : {}),
2112
- resilience: normalizeResilience(body.resilience, explicitOperator(auth?.principal, config.operatorPrincipals)),
2113
- finalVerification: cap.finalVerification === true || body.finalVerification === true ? true : undefined,
2114
- attachments: normalizeAttachments(body.attachments),
2115
- ...(taskHooks ? { hooks: composeHooks(deploymentHooks, taskHooks) } : {}),
2116
- model: picked.model,
2117
- ...((() => {
2118
- if (typeof body.compactionModel !== "string" || body.compactionModel.length === 0)
2119
- return {};
2120
- const cm = matchCatalogModel(body.compactionModel, wireCatalog);
2121
- if (cm === undefined) {
2122
- if (opts?.leg === "fresh") {
2123
- throw new HttpError(400, `unknown compactionModel "${body.compactionModel.slice(0, 120)}" — not in the configured catalog (name, tier word, or id; a catalog refresh may have removed it mid-request)`);
2124
- }
2125
- metrics.inc("task_model_unknown_fallback_total");
2126
- logger.warn("compaction_model_unknown_fallback", { requested: body.compactionModel.slice(0, 120), sessionId: auth?.sessionId ?? null });
2127
- return {};
2128
- }
2129
- return { compactionModel: cm };
2130
- })()),
2131
- thinking: effectiveThinking(body.reasoningEffort, parsedSettings.settings?.ultracode === true),
2132
- getApiKeyAndHeaders: keyResolver,
2133
- ...(() => {
2134
- const limits = resolveTaskLimits(body.limits, taskLimitCaps, taskTimeoutSec, config.requirePrincipal, body.council === true || body.debate === true || scenarioName === "team");
2135
- const declarations = [];
2136
- if (taskLimitCaps.timeoutSec !== undefined)
2137
- declarations.push({ key: "server.limits.timeoutSecCap", value: String(taskLimitCaps.timeoutSec), reason: "env TASK_TIMEOUT_MAX_SEC (operator ceiling on caller limits.timeoutSec)" });
2138
- if (taskLimitCaps.maxOutputTokens !== undefined)
2139
- declarations.push({ key: "server.limits.maxOutputTokensCap", value: String(taskLimitCaps.maxOutputTokens), reason: "env TASK_MAX_OUTPUT_TOKENS_MAX (operator ceiling)" });
2140
- if (taskLimitCaps.maxTurns !== undefined)
2141
- declarations.push({ key: "server.limits.maxTurnsCap", value: String(taskLimitCaps.maxTurns), reason: "env TASK_MAX_TURNS_MAX (operator ceiling)" });
2142
- if (config.maxTaskCostUsd > 0)
2143
- 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
- if (config.maxTaskTokens > 0)
2145
- 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)" });
2146
- if (config.requirePrincipal === true)
2147
- 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" });
2148
- return { ...(limits !== undefined ? { limits } : {}), ...(declarations.length > 0 ? { configOverrides: declarations } : {}) };
2149
- })(),
2150
- ...(config.requirePrincipal !== true ? { backgroundScope: "session" } : {}),
2151
- maxCostUsd: cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd),
2152
- maxTokens: cappedCeiling(body.maxTokens, config.maxTaskTokens),
2153
- degrade: (() => {
2154
- if (!config.degrade || cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd) === undefined)
2155
- return undefined;
2156
- if (Array.isArray(body.images) && body.images.length > 0 && !config.degrade.toSupportsImages) {
2157
- metrics.inc("degrade_dropped_total", { reason: "vision_target" });
2158
- logger.warn("degrade_dropped", { reason: "vision_target", model: picked.model, images: body.images.length });
2159
- return undefined;
2160
- }
2161
- return { to: config.degrade.to, atCostFraction: config.degrade.atCostFraction };
2162
- })(),
2163
- memory: memoryEngine ? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes) : undefined,
2164
- tools: ((base) => {
2165
- const extra = [...(selectEnvTool ? [selectEnvTool] : []), ...(sendUserFileToolSpec ? [sendUserFileToolSpec] : [])];
2166
- const merged = extra.length > 0 ? [...(base ?? []), ...extra] : base;
2167
- return merged && config.toolDeferLongtail ? applyLongtailDefer(merged, true) : merged;
2168
- })(cap.tools),
2169
- skills: mergeUserSkills(cap.skills, body.skills, logger),
2170
- mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger),
2171
- promptProvider: centerDecls ? centerPromptProvider(centerDecls, scenarioName) : cap.promptProvider,
2172
- toolPolicy: durableEnabled
2173
- ? combinePolicies(createDurableQuestionPolicy(), createDurableAskPolicy({
2174
- requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,
2175
- ...(approvalExemptionStore && auth?.sessionId
2176
- ? {
2177
- exempt: (toolName) => approvalExemptionStore.has(auth.sessionId, toolName),
2178
- onExempted: (toolName, rawToolName) => logger.info("approval_exempted", { sessionId: auth.sessionId, toolName, rawToolName }),
2179
- }
2180
- : {}),
2181
- }))
2182
- : approvalEnabled && approvalStore
2183
- ? createOaApprovalPolicy({
2184
- store: approvalStore,
2185
- requireApproval: config.approvalRequire,
2186
- deny: config.approvalDeny,
2187
- pollMs: config.approvalPollMs,
2188
- context: () => ({ sessionId: auth?.sessionId ?? null, owner: auth?.principal ?? null }),
2189
- neverAuto: config.approvalNeverAuto,
2190
- ...(approvalExemptionStore
2191
- ? {
2192
- exempt: (sessionId, toolName) => approvalExemptionStore.has(sessionId, toolName),
2193
- onExempted: (sessionId, toolName) => logger.info("approval_exempted", { sessionId, toolName }),
2194
- }
2195
- : {}),
2196
- })
2197
- : singleUserAutoAcceptBaseline
2198
- ? createAllowDenyPolicy({})
2199
- : undefined,
2200
- ...(durableEnabled
2201
- ? {
2202
- checkpointStore,
2203
- durableApproval: {
2204
- scope: auth?.principal ?? "_",
2205
- ...(config.approvalTimeoutSec > 0 ? { ttlMs: config.approvalTimeoutSec * 1000 } : {}),
2206
- },
2207
- ...((rs) => (rs ? { resourceSuspend: rs } : {}))(resourceSuspendOptIn({
2208
- enabled: config.resourceSuspend,
2209
- ttlSec: config.resourceSuspendTtlSec,
2210
- isVerify: body.verify === true,
2211
- isCascade: body.cascade === true,
2212
- scope: auth?.principal ?? "_",
2213
- })),
2214
- onQuestion: QUESTION_AWAITS_RESUME,
2215
- }
2216
- : {}),
2217
- };
2218
- const governedBase = applyRuntimeGovernance(spec, { autonomy: config.autonomy, commandPolicy: config.commandPolicy });
2219
- let governed = governedBase;
2220
- const bodyMode = coercePermissionMode(body.permissionMode);
2221
- let effectiveSettings = bodyMode ? withPermissionMode(parsedSettings.settings, bodyMode) : parsedSettings.settings;
2222
- if (appendLessPack && effectiveSettings?.outputStyle) {
2223
- 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
- const { outputStyle: _dropped, ...rest } = effectiveSettings;
2225
- effectiveSettings = Object.keys(rest).length > 0 ? rest : undefined;
2226
- }
2227
- if (effectiveSettings?.outputStyle && acceptedAppend && acceptedAppend.length + 2 + effectiveSettings.outputStyle.length > MAX_SETTINGS_OUTPUT_STYLE_CHARS) {
2228
- 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
- const { outputStyle: _dropped2, ...rest2 } = effectiveSettings;
2230
- effectiveSettings = Object.keys(rest2).length > 0 ? rest2 : undefined;
2231
- }
2232
- const hostSemanticsLane = config.remoteExec === undefined || config.remoteExec.provider === "host";
2233
- const shellScratchpad = await acceptShellScratchpadDir(body.scratchpadDir, {
2234
- requirePrincipal: config.requirePrincipal,
2235
- hostSemanticsLane,
2236
- warn: (msg, meta) => logger.warn(msg, { ...(meta ?? {}), sessionId: auth?.sessionId ?? null }),
2237
- });
2238
- const scratchpadDir = shellScratchpad ??
2239
- (hostSemanticsLane && auth?.sessionId ? await ensureScratchpadDir(config.localDataRoot ?? localRoot, auth.sessionId) : undefined);
2240
- if (effectiveSettings) {
2241
- const fsWriteGate = hostSemanticsLane
2242
- ? (() => {
2243
- const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
2244
- const gateCwd = sessionCwd ??
2245
- (config.remoteExec === undefined
2246
- ? process.cwd()
2247
- : join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted"));
2248
- return {
2249
- env: new NodeExecutionEnv({ cwd: gateCwd }),
2250
- cwd: gateCwd,
2251
- ...(scratchpadDir ? { scratchpadDir } : {}),
2252
- ...(config.sensitiveWritePatterns.length > 0 ? { sensitivePatterns: config.sensitiveWritePatterns } : {}),
2253
- ...(config.manualModeShellGate ? { shellGate: config.manualModeShellGate } : {}),
2254
- ...(approvalExemptionStore && auth?.sessionId
2255
- ? {
2256
- isExempt: async (toolName) => {
2257
- const hit = await approvalExemptionStore.has(auth.sessionId, toolName);
2258
- if (hit)
2259
- logger.info("fs_write_gate_exempted", { toolName, sessionId: auth.sessionId });
2260
- return hit;
2261
- },
2262
- }
2263
- : {}),
2264
- };
2265
- })()
2266
- : undefined;
2267
- const workflowGate = approvalExemptionStore && auth?.sessionId
2268
- ? {
2269
- isExempt: async (toolName) => {
2270
- const hit = await approvalExemptionStore.has(auth.sessionId, toolName);
2271
- if (hit)
2272
- logger.info("workflow_gate_exempted", { toolName, sessionId: auth.sessionId });
2273
- return hit;
2274
- },
2275
- }
2276
- : undefined;
2277
- try {
2278
- governed = applyTaskSettings(governedBase, effectiveSettings, fsWriteGate, workflowGate);
2279
- }
2280
- catch (e) {
2281
- throw new HttpError(422, `settings are tighten-only and cannot loosen the deployment policy: ${e.message}`);
2282
- }
2283
- }
2284
- if (governed.handsReadOnly === true && Array.isArray(governed.tools)) {
2285
- governed = { ...governed, tools: stripDelegationTools(governed.tools) };
2286
- }
2287
- const bodyProfile = typeof body.sandboxImageProfile === "string" && body.sandboxImageProfile.length > 0 ? body.sandboxImageProfile : undefined;
2288
- const taskImageProfile = bodyProfile ?? (body.cascade === true || body.verify === true ? undefined : sessionEnvSelection.get(auth?.sessionId));
2289
- let resolvedImageCaps;
2290
- if (taskImageProfile !== undefined) {
2291
- if (config.remoteExec?.provider !== "k8s") {
2292
- throw new HttpError(400, "sandboxImageProfile is only supported on the k8s sandbox backend");
2293
- }
2294
- if (!imageIndex) {
2295
- throw new HttpError(400, "sandboxImageProfile requires the image index (sema-registry backend not configured)");
2296
- }
2297
- if (!auth?.sessionId) {
2298
- throw new HttpError(400, "sandboxImageProfile requires a resolved session");
2299
- }
2300
- const capsNeeded = Array.isArray(body.capabilitiesNeeded)
2301
- ? body.capabilitiesNeeded.filter((c) => typeof c === "string")
2302
- : undefined;
2303
- const resolved = await resolveSandboxImageRef({
2304
- profile: taskImageProfile,
2305
- ...(capsNeeded && capsNeeded.length > 0 ? { capabilitiesNeeded: capsNeeded } : {}),
2306
- viewer: { operator: explicitOperator(auth.principal, config.operatorPrincipals), tenantId: auth.principal ?? null },
2307
- index: imageIndex,
2308
- });
2309
- if (!resolved.ok)
2310
- throw new HttpError(resolved.status, resolved.message);
2311
- perTaskImage.set(auth.sessionId, resolved.ref);
2312
- resolvedImageCaps = resolved.capabilities;
2313
- }
2314
- if (config.envFactsEnabled) {
2315
- const pkgSourceLane = config.remoteExec?.provider === "e2b" || config.remoteExec?.provider === "k8s";
2316
- const remoteScratchpadDir = !hostSemanticsLane && isRemoteScratchpadLane(config.remoteExec?.provider)
2317
- ? remoteScratchpadDirFor(auth?.sessionId)
2318
- : undefined;
2319
- const factScratchpadDir = scratchpadDir ?? remoteScratchpadDir;
2320
- const facts = buildEnvFacts({
2321
- profile: taskImageProfile,
2322
- capabilities: resolvedImageCaps,
2323
- ...(pkgSourceLane ? { pkgSource: config.sandboxPkgSource } : {}),
2324
- egress: egressForRemoteExec(config.remoteExec),
2325
- ...(factScratchpadDir ? { scratchpadDir: factScratchpadDir } : {}),
2326
- resumeFacts: resumeFactsForLane(config.remoteExec?.provider, config.remoteExec?.provider === "k8s" ? { k8sSnapshot: Boolean(config.remoteExec.s3Snapshot) } : undefined),
2327
- });
2328
- if (facts)
2329
- governed = { ...governed, envFacts: facts };
2330
- }
2331
- if (scratchpadDir !== undefined) {
2332
- const dirs = governed.additionalDirectories ?? [];
2333
- if (!dirs.includes(scratchpadDir))
2334
- governed = { ...governed, additionalDirectories: [...dirs, scratchpadDir] };
2335
- }
2336
- if (config.routerEnabled) {
2337
- const isolatedExecEnv = isIsolatedExecEnv(config.remoteExec?.provider, config.remoteExec?.provider === "k8s" ? config.remoteExec.runtimeClass : undefined);
2338
- const decision = routeServiceTask({
2339
- isolatedExecEnv,
2340
- hasExecutionEnv: config.remoteExec != null,
2341
- ...(config.autonomy ? { autonomy: config.autonomy } : {}),
2342
- explicitTeam: body.council === true || body.debate === true || scenarioName === "team",
2343
- });
2344
- logger.info("orchestration_routed", { mode: decision.mode, reason: decision.reason, sessionId: auth?.sessionId ?? null });
2345
- if (decision.mode === "supervisor") {
2346
- return tightenTaskSpec(governed, supPostureOverrides());
2347
- }
2348
- }
2349
- return governed;
2350
- };
792
+ // design/158 A10:resolveSpec 段搬到 src/boot/resolve-spec.ts(逐字;两处活引用改取值,见该文件头注)
793
+ const resolveSpec = createResolveSpec({
794
+ config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver,
795
+ getCenterPrompts: () => configCenter.getCenterPrompts(),
796
+ getKeyResolver: () => configCenter.getKeyResolver(),
797
+ taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv,
798
+ hookLlm, hookAgent, fleetBus, hookWakeBus, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec,
799
+ selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, approvalEnabled,
800
+ approvalStore, singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage,
801
+ sessionEnvSelection,
802
+ });
2351
803
  const server = createHttpServer({ runner, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
804
+ // D-D SLA-timer: wire the server's deny-sweep into the reaper holder declared above (the reaper is defined
805
+ // before the server, so it calls through this late-bound reference).
2352
806
  runDenySweep = server.denyExpiredApprovals;
807
+ // [1934]:绑址旋钮 + 无鉴权自收窄(理由见 resolveBindHost 顶注)。undefined = Node 默认全接口。
2353
808
  const bindHost = resolveBindHost(config);
2354
809
  await new Promise((resolve) => (bindHost ? server.listen(config.port, bindHost, resolve) : server.listen(config.port, resolve)));
2355
810
  logger.info("listening", {
@@ -2359,6 +814,9 @@ async function main() {
2359
814
  ? { note: "auto-narrowed to loopback: write face is unauthenticated (ALLOW_UNAUTHED_WRITES with no service token). Set BIND_HOST explicitly to override." }
2360
815
  : {}),
2361
816
  });
817
+ // fleet worker 接线(announce/heartbeat + usage 批报;lease 消费后一拍):boot 完成后
818
+ // 注册(=首次心跳)。门 = center lane 配置 ∧ FLEET_ADVERTISE_ADDRESS 显式声明(不自猜可达地址);任一缺 → undefined
819
+ // = 非 fleet 部署零行为。draining 翻转的即刻 announce 在 drainThenShutdown;退出 DELETE 在 hardShutdown。
2362
820
  const fleetClient = startFleetClientFromEnv(config, {
2363
821
  instanceId,
2364
822
  version: serviceVersion(),
@@ -2372,108 +830,51 @@ async function main() {
2372
830
  port: config.port,
2373
831
  model: config.model.id,
2374
832
  brain: brainSummary(config),
2375
- breakerState: config.resilience.circuitBreaker ? (breakerState ? `shared(${backend.kind})` : "in-process") : "off",
833
+ // shared = cross-replica TiDB breaker state; in-process = core's per-replica Map; off = breaker disabled
834
+ breakerState: config.resilience.circuitBreaker ? (breakerState ? `shared(${backend.kind})` : "in-process") : "off", // 1.108: was hard-coded "shared(tidb)" — lied under DB_BACKEND=pg
835
+ // On DB_BACKEND=local the wiring coerces sessionBackend to the LABEL "tidb" (meaning "the
836
+ // backend's durable store, generically") — printing it verbatim read as a SQL deployment on a file-everything
837
+ // box. Print the effective store instead (same honesty fix as breakerState above).
2376
838
  session: config.sessionBackend === "tidb" && backend ? `durable(${backend.kind})` : config.sessionBackend,
839
+ // design/138 S1: the file-based memory engine (single-user only); "off" = MEMORY_ENGINE=off or multi-tenant.
2377
840
  memory: memoryEngine ? `engine(${memoryEngine.root})` : "off",
2378
841
  instanceId,
2379
842
  rateLimitPerMin: config.rateLimitPerMin,
2380
843
  rateLimit: config.rateLimitPerMin > 0 ? (backend ? `shared()` : "in-process") : "off",
2381
844
  maxPrincipalCostUsd: config.maxPrincipalCostUsd || null,
2382
845
  costQuota: config.maxPrincipalCostUsd > 0 ? (backend ? `shared()` : "in-process") : "off",
846
+ // durable offload store for large tool results (cross-replica wake); else core's in-process default
2383
847
  toolResultStore: toolResultStore ? (backend?.kind === "local" ? "file(local)" : "shared(sql)") : "in-process",
2384
848
  otel: config.otel ? config.otel.endpoint : null,
2385
- perModelKeys: Object.keys(config.modelApiKeyEnv).length,
849
+ perModelKeys: Object.keys(config.modelApiKeyEnv).length, // models with their own upstream key (else gateway)
2386
850
  sealedModelKeys: Object.values(config.modelApiKeys).filter((v) => typeof v === "string").length,
2387
- sealedPoisonedModels: Object.values(config.modelApiKeys).filter((v) => typeof v !== "string").length,
851
+ sealedPoisonedModels: Object.values(config.modelApiKeys).filter((v) => typeof v !== "string").length, // sealed-box custody keys unsealed (count only — values never log)
852
+ // SVC-1: durable workflow record + at-least-once notify backend (file = crash-safe ledger; memory = ephemeral)
2388
853
  workflowRunStore: config.selfOrchestrationEnabled
2389
854
  ? config.workflowRunStoreBackend === "memory"
2390
855
  ? "in-process"
2391
856
  : sqlWorkflowRunStore
2392
- ? `db(${backend.kind})`
857
+ ? `db(${backend.kind})` // P1: cross-replica twin active
2393
858
  : "file"
2394
859
  : "off",
2395
860
  });
861
+ // SVC-1 at-least-once notify — CRASH-RECOVERY sweep: once at boot, re-derive every un-acked (started-but-not-
862
+ // delivered) workflow's terminal state from the durable run store and re-deliver the completion notify that a
863
+ // prior replica crash dropped (idempotent on the receiver). Fire-and-forget AFTER listen so startup isn't
864
+ // blocked on it (recovery only re-fires completion notifications — independent of serving new requests). A
865
+ // sweep throw is logged, never fatal.
2396
866
  if (workflowNotifyGate) {
2397
867
  void workflowNotifyGate
2398
868
  .recover({ orphanGraceMs: config.workflowOrphanGraceMs })
2399
869
  .then((tally) => logger.info("workflow_notify_recovered", tally))
2400
870
  .catch((err) => logger.warn("workflow_notify_recover_failed", { err: String(err) }));
2401
871
  }
2402
- let closing = false;
2403
- const hardShutdown = () => {
2404
- if (closing)
2405
- return;
2406
- closing = true;
2407
- clearInterval(reaper);
2408
- otelExporter?.stop();
2409
- breakerState?.stop();
2410
- if (costQuota && "stop" in costQuota)
2411
- costQuota.stop();
2412
- if (rateLimiter && "stop" in rateLimiter)
2413
- rateLimiter.stop();
2414
- runner.sessions.dispose();
2415
- subRunner.sessions.dispose();
2416
- void lspManager?.dispose?.();
2417
- void workflowNotifyJournal?.close?.();
2418
- try {
2419
- const reaped = defaultTaskRegistry.reapAllSessionBackground();
2420
- if (reaped > 0)
2421
- logger.info("shutdown_background_reaped", { reaped });
2422
- }
2423
- catch (err) {
2424
- logger.warn("shutdown_background_reap_failed", { err: String(err) });
2425
- }
2426
- void fleetClient?.stop();
2427
- server.close(() => {
2428
- void (backend ? backend.close() : Promise.resolve()).finally(() => process.exit(0));
2429
- });
2430
- const finalExit = setTimeout(() => { logger.warn("hard_shutdown_final_deadline", { afterMs: 15_000 }); process.exit(0); }, 15_000);
2431
- finalExit.unref?.();
2432
- server.closeIdleConnections?.();
2433
- const forceClose = setTimeout(() => server.closeAllConnections?.(), 10_000);
2434
- forceClose.unref?.();
2435
- };
2436
- let draining = false;
2437
- const drainThenShutdown = () => {
2438
- if (closing)
2439
- return;
2440
- if (draining) {
2441
- logger.info("drain_second_signal_hard_stop", {});
2442
- hardShutdown();
2443
- return;
2444
- }
2445
- draining = true;
2446
- drainState.draining = true;
2447
- drainState.since = Date.now();
2448
- void fleetClient?.announceNow();
2449
- const inflight = drainState.inflight?.() ?? 0;
2450
- logger.info("draining_started", { inflight, graceMs: config.drainGraceMs });
2451
- if (inflight === 0) {
2452
- hardShutdown();
2453
- return;
2454
- }
2455
- const t0 = performance.now();
2456
- const tick = setInterval(() => {
2457
- const n = drainState.inflight?.() ?? 0;
2458
- const elapsed = Math.round(performance.now() - t0);
2459
- if (n === 0 || elapsed >= config.drainGraceMs) {
2460
- clearInterval(tick);
2461
- logger.info("drain_complete", { inflight: n, elapsedMs: elapsed, timedOut: n > 0 });
2462
- hardShutdown();
2463
- }
2464
- }, 1_000);
2465
- };
2466
- process.on("SIGTERM", drainThenShutdown);
2467
- process.on("SIGINT", hardShutdown);
2468
- process.on("SIGHUP", createSighupIdleHandler({
2469
- inflight: () => drainState.inflight?.() ?? 0,
2470
- lastActivityAt: () => drainState.lastActivityAt?.() ?? 0,
2471
- isStopped: () => closing || draining,
2472
- escalate: drainThenShutdown,
2473
- shutdown: hardShutdown,
2474
- idleGraceMs: config.sighupIdleGraceMs,
2475
- log: (event, fields) => logger.info(event, fields),
2476
- }));
872
+ // design/158 A10:收尾段搬到 src/boot/shutdown.ts(逐字)。⚠️ 调用点必须留在 listen 之后 —— 位置即契约,
873
+ // 理由(信号注册时点/三信号相对次序/clearInterval 先于 server.close)见该文件头注。
874
+ installShutdownHandlers({
875
+ config, logger, server, reaper, otelExporter, breakerState, costQuota, rateLimiter,
876
+ runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState,
877
+ });
2477
878
  }
2478
879
  void main().catch((err) => {
2479
880
  createLogger().error("fatal", { err: String(err) });