@desplega.ai/agent-swarm 1.168.0 → 1.171.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (410) hide show
  1. package/README.md +11 -2
  2. package/dist/{acp-adapter-zhejfecx.js → acp-adapter-1z9p3fvx.js} +6 -6
  3. package/dist/{actions-7jg840nr.js → actions-rc2yjk8a.js} +41 -33
  4. package/dist/{amp-adapter-ye6rnetm.js → amp-adapter-ypyf5s1x.js} +6 -6
  5. package/dist/{anthropic-messages-13yn502s.js → anthropic-messages-etkbxhhy.js} +11 -11
  6. package/dist/{app-ffes2kyf.js → app-04ab2s0z.js} +10 -10
  7. package/dist/{artifact-fey7dvj9.js → artifact-eczsknc3.js} +6 -3
  8. package/dist/{assistant-fhm5smyz.js → assistant-vhmjd8wf.js} +47 -36
  9. package/dist/{azure-openai-responses-9st34pfz.js → azure-openai-responses-xap57x4b.js} +7 -7
  10. package/dist/{boot-reembed-jvfadhsk.js → boot-reembed-3p0m259x.js} +9 -9
  11. package/dist/{boot-reembed-20q3nzvg.js → boot-reembed-wew3gwk8.js} +9 -9
  12. package/dist/{boot-scrub-sweep-jtfajzve.js → boot-scrub-sweep-5ytwtq24.js} +10 -10
  13. package/dist/{boot-seeds-9qt0j8j2.js → boot-seeds-s8p2xry8.js} +13 -13
  14. package/dist/{claude-adapter-a8znhthz.js → claude-adapter-akrzg6jh.js} +16 -16
  15. package/dist/{claude-managed-adapter-tzwxy803.js → claude-managed-adapter-kqqqhp2q.js} +3 -3
  16. package/dist/{claude-managed-setup-nrhfx5b0.js → claude-managed-setup-bnmjarjc.js} +12 -9
  17. package/dist/{claude-sdk-session-7x7chdrs.js → claude-sdk-session-exq1jadx.js} +7818 -7721
  18. package/dist/{cli-g6gx2dn1.js → cli-01qndrvw.js} +17080 -19099
  19. package/dist/{cli-74ez9b66.js → cli-0d2rqhvq.js} +584 -75
  20. package/dist/cli-0p3enk35.js +188 -0
  21. package/dist/cli-1srmzpr3.js +16 -0
  22. package/dist/{cli-6bpvv8h4.js → cli-231av4y6.js} +7 -7
  23. package/dist/{cli-9q0bh411.js → cli-280gnz4v.js} +2 -2
  24. package/dist/{cli-mv2520nm.js → cli-2bw3w47j.js} +1 -1
  25. package/dist/{cli-fqq06kkb.js → cli-2qbs2tvf.js} +22 -14
  26. package/dist/{cli-efnd2kce.js → cli-2rbt7at8.js} +6 -1
  27. package/dist/{cli-xvc3q9br.js → cli-451ks6aw.js} +1 -1
  28. package/dist/{cli-0jngabtb.js → cli-4bm25xtw.js} +2 -2
  29. package/dist/{cli-zp38xvtm.js → cli-5rqrj7q0.js} +1 -1
  30. package/dist/{cli-km4x66j0.js → cli-5y67yrt6.js} +13 -13
  31. package/dist/{cli-nctfv2yy.js → cli-62bwwc2q.js} +8 -8
  32. package/dist/cli-6xxxb8qt.js +2084 -0
  33. package/dist/cli-6zx22mv6.js +0 -0
  34. package/dist/{cli-zp67qg5x.js → cli-7a0sde2x.js} +72 -1
  35. package/dist/{cli-v1w7pbd8.js → cli-7jykv7ef.js} +1 -1
  36. package/dist/{cli-rfphkzh5.js → cli-7pz3b7x5.js} +24 -4
  37. package/dist/{cli-mb75wpxf.js → cli-81r1wjyv.js} +1 -1
  38. package/dist/{cli-svah6b6w.js → cli-8mr695sb.js} +9 -5
  39. package/dist/{cli-f14fvzag.js → cli-a1b9b32n.js} +6 -1
  40. package/dist/cli-cd0hv9nh.js +32 -0
  41. package/dist/{cli-rr5z1j4p.js → cli-ctret352.js} +691 -213
  42. package/dist/{cli-wt7ccy4r.js → cli-d161g69q.js} +9 -1
  43. package/dist/cli-db3m6zdt.js +362 -0
  44. package/dist/{cli-g5erf0zt.js → cli-e2prcx5m.js} +31 -0
  45. package/dist/{cli-hf5s9pgf.js → cli-e3pmyt39.js} +1 -1
  46. package/dist/{cli-pgehz8e1.js → cli-ec7hrgqp.js} +404 -89
  47. package/dist/{cli-ng1pmdx6.js → cli-erbp6b4g.js} +10 -7
  48. package/dist/{cli-9k7mqz00.js → cli-ge593j7e.js} +30 -3
  49. package/dist/{cli-5qkcdv8c.js → cli-gmdajfw5.js} +3 -3
  50. package/dist/cli-h8w3fg83.js +17 -0
  51. package/dist/{cli-xvdgasgy.js → cli-hsem16hb.js} +114 -9
  52. package/dist/{cli-ytmmyj7f.js → cli-hsk5gd15.js} +9 -5
  53. package/dist/{cli-aj1e0h0d.js → cli-jxsgrnmq.js} +201 -37
  54. package/dist/{cli-tc5zec7v.js → cli-k2v5x13t.js} +2 -2
  55. package/dist/{cli-69egj654.js → cli-m4csbs67.js} +2 -2
  56. package/dist/{cli-43jg9xdw.js → cli-mzj3aqac.js} +3 -3
  57. package/dist/{cli-p5jqk8fr.js → cli-n805bb98.js} +1 -1
  58. package/dist/{cli-ew0c3a1j.js → cli-n8ybnv1k.js} +3 -3
  59. package/dist/{cli-j05x7pnb.js → cli-nkjwe4z7.js} +219 -41
  60. package/dist/{cli-jb931ba2.js → cli-p2pjfq1r.js} +2 -2
  61. package/dist/{cli-ee0402n1.js → cli-p8f7dqgt.js} +871 -2980
  62. package/dist/{cli-r7eeddkq.js → cli-ph3145b7.js} +3 -3
  63. package/dist/cli-ppy4nnnh.js +168 -0
  64. package/dist/cli-r455yd58.js +832 -0
  65. package/dist/{cli-mxvnnv5q.js → cli-rrbctenq.js} +2 -2
  66. package/dist/{cli-47zfzshm.js → cli-s88my3br.js} +40 -28
  67. package/dist/{cli-9q24yyvs.js → cli-srmej5tp.js} +3 -3
  68. package/dist/{cli-bcspch6h.js → cli-tg8q0b5y.js} +14 -11
  69. package/dist/cli-w0zx9ya1.js +1351 -0
  70. package/dist/cli-wfvedq5w.js +2659 -0
  71. package/dist/{cli-ewhwpyrb.js → cli-x207g4w1.js} +1 -1
  72. package/dist/{cli-fhj4y16f.js → cli-x3fy2tgp.js} +2 -2
  73. package/dist/{cli-7mhy3ths.js → cli-x61srfpp.js} +5 -5
  74. package/dist/{cli-9q3wt23f.js → cli-x6z90nq9.js} +1 -1
  75. package/dist/{cli-jbzbr36n.js → cli-y1j1ggbf.js} +8 -8
  76. package/dist/{cli-jvmtxqtc.js → cli-ywh2a87k.js} +7 -2
  77. package/dist/{cli-3qhyh31j.js → cli-ywm1azg9.js} +6 -0
  78. package/dist/{cli-z48ggfyq.js → cli-zxm9p3x4.js} +2 -2
  79. package/dist/cli.js +49 -28
  80. package/dist/{codex-adapter-n63k33d3.js → codex-adapter-9ag75whm.js} +13 -13
  81. package/dist/{codex-hook-15dgc4cb.js → codex-hook-7ba15hjd.js} +7 -5
  82. package/dist/{codex-login-g1cyt5zm.js → codex-login-841kv5fk.js} +7 -5
  83. package/dist/{codex-session-runner-xkxfd81a.js → codex-session-runner-9fp7y4cf.js} +13 -13
  84. package/dist/{commands-q4mfhn51.js → commands-d7eah0y9.js} +11 -11
  85. package/dist/controller-hq5ggpcp.js +193 -0
  86. package/dist/controllers-connect-8t0k65x2.js +265 -0
  87. package/dist/{cursor-adapter-v8mm6bp5.js → cursor-adapter-t4pr1m3c.js} +6 -6
  88. package/dist/db-2rmd23wq.js +71 -0
  89. package/dist/{db-1t0bj3pc.js → db-sp39qv72.js} +9 -9
  90. package/dist/{devin-adapter-bffjj5n4.js → devin-adapter-xgtj680j.js} +2 -2
  91. package/dist/{dispatcher-k1fhavcf.js → dispatcher-g7fsybsd.js} +32 -28
  92. package/dist/docker-f503k9p0.js +201 -0
  93. package/dist/{dsh-adapter-5423p70m.js → dsh-adapter-2h8m4cab.js} +5 -5
  94. package/dist/e2b-66xra4az.js +202 -0
  95. package/dist/{e2b-nd50eccg.js → e2b-xwhyj8cw.js} +43 -527
  96. package/dist/{event-bus-vnfthafg.js → event-bus-vjtv82gm.js} +2 -2
  97. package/dist/{grok-adapter-phrndeb8.js → grok-adapter-1err1me7.js} +7 -7
  98. package/dist/{handlers-59ywqwzh.js → handlers-c7yggm37.js} +35 -31
  99. package/dist/heartbeat-dkm924fk.js +124 -0
  100. package/dist/{hook-atj0swc2.js → hook-g8e7sfnf.js} +14 -14
  101. package/dist/{hook-mew1chq9.js → hook-wgx4exw6.js} +21 -18
  102. package/dist/{http-ef2fnn9v.js → http-1d23k928.js} +11499 -9018
  103. package/dist/{index-9fc2h7ww.js → index-gm10tmye.js} +32 -28
  104. package/dist/{index-w2yhye23.js → index-q7cj9bn2.js} +41 -29
  105. package/dist/{index-48pb9erw.js → index-va4zbvzp.js} +42 -37
  106. package/dist/{index-gr69c43q.js → index-vertaxs1.js} +9 -9
  107. package/dist/{index-h26wjcfy.js → index-y5vvczrs.js} +62 -24
  108. package/dist/{keepalive-qhcqz56v.js → keepalive-1jc5e7ct.js} +11 -11
  109. package/dist/{lead-hpk5r3b1.js → lead-yxw2f1sg.js} +52 -48
  110. package/dist/{maintenance-nvkyp2xe.js → maintenance-h7w2m10y.js} +9 -9
  111. package/dist/{node-8zkdq42h.js → node-c9we95xf.js} +9 -9
  112. package/dist/{node-qm9eb6z0.js → node-fkajaq96.js} +1 -1
  113. package/dist/{oauth-refresh-sweep-ngskthwh.js → oauth-refresh-sweep-wh9ypd1b.js} +10 -10
  114. package/dist/{onboard-yfnkqhxc.js → onboard-dfr9j762.js} +25 -15
  115. package/dist/{openai-codex-responses-jzxkta9b.js → openai-codex-responses-pgvtddvv.js} +3 -3
  116. package/dist/{openai-responses-m3g43rrt.js → openai-responses-9dyzwzkr.js} +9 -9
  117. package/dist/{opencode-adapter-yjyqynzf.js → opencode-adapter-f1zby94p.js} +3 -3
  118. package/dist/{otel-impl-hhm64r6m.js → otel-impl-and31m5s.js} +5 -5
  119. package/dist/{pi-mono-adapter-0eg8kw2c.js → pi-mono-adapter-fx6k5wwh.js} +40 -28
  120. package/dist/{pricing-refresh-35f91nx2.js → pricing-refresh-23zr6j3w.js} +11 -11
  121. package/dist/{rbac-roles-97w370qj.js → rbac-roles-nc7k9d0t.js} +11 -11
  122. package/dist/{rbac-roles-z6sje3gy.js → rbac-roles-wnx35x4x.js} +12 -12
  123. package/dist/{render-v2-n4v66bj3.js → render-v2-81cg1y52.js} +10 -10
  124. package/dist/resume-d4b6j2n2.js +70 -0
  125. package/dist/{setup-m3hhyv5f.js → setup-g68r1w28.js} +7 -5
  126. package/dist/static-t1gr2cr7.js +30 -0
  127. package/dist/{virtual-modules-ftr9kwe8.js → virtual-modules-rdjmcg7q.js} +3 -3
  128. package/dist/{work-objects-mxf2c842.js → work-objects-0zzdnbt9.js} +37 -33
  129. package/dist/{worker-9t07z1gq.js → worker-p1g8qm6h.js} +52 -48
  130. package/dist/{x-ma4gnfgy.js → x-qfmygqqv.js} +2 -2
  131. package/openapi.json +15455 -11731
  132. package/package.json +10 -7
  133. package/src/apps/store.ts +1 -0
  134. package/src/artifact-sdk/feedback-overlay.ts +528 -0
  135. package/src/be/db/agents.ts +6 -0
  136. package/src/be/db/instance-signals.ts +30 -0
  137. package/src/be/db/tasks/read.ts +2 -0
  138. package/src/be/db/tasks/write.ts +39 -5
  139. package/src/be/db/workflow-runs.ts +117 -0
  140. package/src/be/db-queries/slack-inbound.ts +489 -0
  141. package/src/be/db.ts +105 -42
  142. package/src/be/extensions/assets.ts +1 -1
  143. package/src/be/fleet-benchmark-stats.ts +39 -0
  144. package/src/be/migrations/203_workflow_run_parent_step.sql +10 -0
  145. package/src/be/migrations/204_slack_inbound_receipts.sql +51 -0
  146. package/src/be/migrations/205_favorites_per_dashboard_user.sql +29 -0
  147. package/src/be/migrations/206_runtime_pools.sql +62 -0
  148. package/src/be/migrations/207_runtime_session_tokens.sql +31 -0
  149. package/src/be/migrations/208_task_runtime_dispatch.sql +4 -0
  150. package/src/be/model-tier-resolution.ts +9 -2
  151. package/src/be/multi-runtime.ts +102 -13
  152. package/src/be/org-profile.ts +35 -0
  153. package/src/be/rbac-audit.ts +4 -0
  154. package/src/be/runtime-pools/controllers.ts +150 -0
  155. package/src/be/runtime-pools/db.ts +526 -0
  156. package/src/be/runtime-pools/demand.ts +65 -0
  157. package/src/be/runtime-pools/e2b-images.ts +109 -0
  158. package/src/be/runtime-pools/errors.ts +8 -0
  159. package/src/be/runtime-pools/lifecycle.ts +44 -0
  160. package/src/be/runtime-pools/reachability.ts +30 -0
  161. package/src/be/runtime-pools/recovery.ts +248 -0
  162. package/src/be/runtime-pools/remote-controller.ts +144 -0
  163. package/src/be/runtime-pools/routing.ts +304 -0
  164. package/src/be/runtime-pools/seed.ts +110 -0
  165. package/src/be/runtime-pools/supervisor.ts +981 -0
  166. package/src/be/runtime-pools/tokens.ts +57 -0
  167. package/src/be/script-connections.ts +1 -1
  168. package/src/be/scripts/run-saved.ts +40 -20
  169. package/src/be/scripts/typecheck.ts +48 -3
  170. package/src/be/secret-registry.ts +17 -1
  171. package/src/be/skill-usage.ts +467 -0
  172. package/src/be/swarm-config-guard.ts +62 -2
  173. package/src/be/telemetry-enable.ts +66 -0
  174. package/src/be/telemetry-ingest-anchor.ts +77 -0
  175. package/src/be/users.ts +173 -23
  176. package/src/be/worker-nudge-listener.ts +28 -0
  177. package/src/be/worker-nudges.ts +22 -0
  178. package/src/cli.tsx +22 -3
  179. package/src/commands/controller.ts +180 -0
  180. package/src/commands/onboard/non-interactive.ts +36 -12
  181. package/src/commands/onboard/types.ts +1 -1
  182. package/src/commands/onboard.tsx +3 -0
  183. package/src/commands/push-delivery.ts +230 -0
  184. package/src/commands/runner.ts +560 -276
  185. package/src/e2b/dispatch.ts +15 -1
  186. package/src/extensions/catalog.ts +1 -1
  187. package/src/extensions/contract-types.generated.ts +1 -1
  188. package/src/extensions/dispatcher.ts +3 -3
  189. package/src/fleet-benchmark.ts +157 -0
  190. package/src/heartbeat/heartbeat.ts +205 -137
  191. package/src/heartbeat/templates.ts +16 -0
  192. package/src/hooks/hook.ts +3 -2
  193. package/src/http/agents.ts +21 -0
  194. package/src/http/all-routes.ts +7 -0
  195. package/src/http/apps.ts +4 -2
  196. package/src/http/auth.ts +4 -7
  197. package/src/http/config.ts +58 -21
  198. package/src/http/controllers-connect.ts +206 -0
  199. package/src/http/core.ts +108 -14
  200. package/src/http/favorite-owner.ts +31 -1
  201. package/src/http/fleet-benchmark.ts +206 -0
  202. package/src/http/index.ts +40 -6
  203. package/src/http/integrations.ts +3 -1
  204. package/src/http/mcp-bridge.ts +1 -0
  205. package/src/http/mcp.ts +3 -8
  206. package/src/http/pages-public.ts +7 -2
  207. package/src/http/pages.ts +178 -0
  208. package/src/http/poll.ts +31 -2
  209. package/src/http/realtime.ts +1 -1
  210. package/src/http/request-principal.ts +2 -0
  211. package/src/http/runtime-pools.ts +527 -0
  212. package/src/http/session-data.ts +2 -0
  213. package/src/http/sessions.ts +100 -23
  214. package/src/http/skills.ts +173 -0
  215. package/src/http/slack-inbound.ts +107 -0
  216. package/src/http/status.ts +25 -1
  217. package/src/http/tasks.ts +59 -28
  218. package/src/http/telemetry-ui.ts +102 -0
  219. package/src/http/used-by.ts +140 -0
  220. package/src/http/users.ts +1 -1
  221. package/src/http/utils.ts +7 -1
  222. package/src/http/webhooks.ts +20 -20
  223. package/src/http/workflows.ts +10 -2
  224. package/src/jira/app.ts +1 -2
  225. package/src/kv-reserved-namespaces.ts +1 -0
  226. package/src/linear/app.ts +1 -2
  227. package/src/prompts/session-templates.ts +32 -0
  228. package/src/providers/acp-target-catalog.ts +75 -1
  229. package/src/providers/acp-targets.ts +115 -1
  230. package/src/providers/amp-adapter.ts +2 -2
  231. package/src/providers/claude-adapter.ts +1 -1
  232. package/src/providers/claude-managed-adapter.ts +2 -11
  233. package/src/providers/claude-sdk-session.ts +1 -1
  234. package/src/providers/codex-adapter.ts +5 -1
  235. package/src/providers/opencode-adapter.ts +3 -3
  236. package/src/providers/pi-mono-adapter.ts +44 -29
  237. package/src/providers/pricing-sources.md +2 -0
  238. package/src/providers/skill-invoke.ts +39 -3
  239. package/src/rbac/legacy-policy.ts +8 -0
  240. package/src/rbac/permissions.ts +16 -0
  241. package/src/rbac/types.ts +2 -0
  242. package/src/realtime/auth.ts +13 -0
  243. package/src/realtime/browser.generated.txt +8 -8
  244. package/src/realtime/browser.ts +3 -3
  245. package/src/realtime/channels.ts +24 -0
  246. package/src/realtime/transport.ts +94 -15
  247. package/src/realtime/worker-client.ts +176 -0
  248. package/src/runtime-providers/controller-protocol.ts +97 -0
  249. package/src/runtime-providers/controller.ts +86 -0
  250. package/src/runtime-providers/docker.ts +235 -0
  251. package/src/runtime-providers/e2b.ts +217 -0
  252. package/src/runtime-providers/index.ts +37 -0
  253. package/src/runtime-providers/static.ts +26 -0
  254. package/src/runtime-providers/types.ts +92 -0
  255. package/src/runtime-providers/worker-api-url.ts +11 -0
  256. package/src/scripts-runtime/executors/quickjs-pool-config.ts +26 -0
  257. package/src/scripts-runtime/executors/quickjs.ts +9 -2
  258. package/src/scripts-runtime/sdk-allowlist.ts +6 -0
  259. package/src/scripts-runtime/types/stdlib.d.ts +15 -0
  260. package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
  261. package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
  262. package/src/server.ts +13 -0
  263. package/src/slack/actions.ts +6 -2
  264. package/src/slack/app.ts +5 -0
  265. package/src/slack/assistant.ts +15 -5
  266. package/src/slack/egress-scrub.ts +3 -2
  267. package/src/slack/handlers.ts +36 -18
  268. package/src/slack/inbound-dispatch.ts +549 -0
  269. package/src/slack/inbound-files.ts +2 -0
  270. package/src/slack/router.ts +7 -5
  271. package/src/tasks/worker-follow-up.ts +80 -52
  272. package/src/telemetry-context.ts +7 -0
  273. package/src/telemetry-contract/EVENTS.md +14 -1
  274. package/src/telemetry-contract/SOURCE.json +4 -4
  275. package/src/telemetry-contract/catalog.json +155 -0
  276. package/src/telemetry-contract/types.gen.ts +248 -4
  277. package/src/telemetry-ui-events.ts +130 -0
  278. package/src/telemetry.ts +43 -3
  279. package/src/tests/acp-adapter.test.ts +129 -0
  280. package/src/tests/claude-adapter.test.ts +6 -2
  281. package/src/tests/codex-adapter.test.ts +6 -2
  282. package/src/tests/codex-oauth-device.test.ts +1 -1
  283. package/src/tests/config-internal-keys.test.ts +27 -0
  284. package/src/tests/config-session-auth.test.ts +19 -3
  285. package/src/tests/db-retention.test.ts +50 -10
  286. package/src/tests/docs-index-footprint.test.js +3 -3
  287. package/src/tests/e2b-dispatch.test.ts +28 -1
  288. package/src/tests/e2e-runtime-pools.test.ts +51 -0
  289. package/src/tests/entrypoint-acp-target-binary.test.ts +38 -1
  290. package/src/tests/favorites.test.ts +188 -0
  291. package/src/tests/fetch-resolved-env.test.ts +59 -1
  292. package/src/tests/fleet-benchmark.test.ts +453 -0
  293. package/src/tests/harness-model-support.test.ts +70 -2
  294. package/src/tests/heartbeat-checklist.test.ts +243 -0
  295. package/src/tests/heartbeat-orphan-resume.test.ts +24 -0
  296. package/src/tests/heartbeat-reboot-live.test.ts +31 -0
  297. package/src/tests/heartbeat-supersede-resume.test.ts +21 -0
  298. package/src/tests/hook-runtime-close.test.ts +64 -0
  299. package/src/tests/http-task-write-ownership.test.ts +8 -1
  300. package/src/tests/integration-disable-flags.test.ts +45 -0
  301. package/src/tests/migration-206-runtime-pools.test.ts +134 -0
  302. package/src/tests/migration-207-runtime-session-tokens.test.ts +45 -0
  303. package/src/tests/migration-208-task-runtime-dispatch.test.ts +30 -0
  304. package/src/tests/multi-runtime-registration.test.ts +219 -4
  305. package/src/tests/onboard-non-interactive.test.ts +104 -6
  306. package/src/tests/opencode-adapter.test.ts +6 -2
  307. package/src/tests/org-website.test.ts +115 -0
  308. package/src/tests/pages-feedback.test.ts +218 -0
  309. package/src/tests/pi-mono-adapter.test.ts +74 -10
  310. package/src/tests/rbac-admission-e2e.test.ts +4 -4
  311. package/src/tests/rbac-engine.test.ts +8 -1
  312. package/src/tests/realtime-room-auth.test.ts +49 -0
  313. package/src/tests/repo-allow-merge-gate.test.ts +8 -1
  314. package/src/tests/runner-exit-after-tasks.test.ts +163 -0
  315. package/src/tests/runner-finish-retry.test.ts +466 -0
  316. package/src/tests/runner-harness-telemetry-env.test.ts +105 -0
  317. package/src/tests/runner-push-delivery.test.ts +670 -0
  318. package/src/tests/runner-slot-reconcile.test.ts +89 -1
  319. package/src/tests/runtime-child-token-cleanup.test.ts +172 -0
  320. package/src/tests/runtime-controller-token.test.ts +254 -0
  321. package/src/tests/runtime-demand.test.ts +119 -0
  322. package/src/tests/runtime-e2b-pools.test.ts +847 -0
  323. package/src/tests/runtime-pool-reachability.test.ts +328 -0
  324. package/src/tests/runtime-pool-routing.test.ts +817 -0
  325. package/src/tests/runtime-pools-db.test.ts +275 -0
  326. package/src/tests/runtime-pools-http.test.ts +506 -0
  327. package/src/tests/runtime-pools-ownership-guards.test.ts +235 -0
  328. package/src/tests/runtime-pools-seed.test.ts +203 -0
  329. package/src/tests/runtime-pools-test-utils.ts +128 -0
  330. package/src/tests/runtime-provider-registration.test.ts +133 -0
  331. package/src/tests/runtime-providers-docker.test.ts +400 -0
  332. package/src/tests/runtime-providers-e2b-transport.test.ts +50 -0
  333. package/src/tests/runtime-providers-e2b.test.ts +335 -0
  334. package/src/tests/runtime-recovery.test.ts +745 -0
  335. package/src/tests/runtime-remote-controller.test.ts +846 -0
  336. package/src/tests/runtime-supervisor-failures.test.ts +387 -0
  337. package/src/tests/runtime-supervisor.test.ts +1039 -0
  338. package/src/tests/runtime-token-auth.test.ts +897 -0
  339. package/src/tests/script-connections.test.ts +9 -0
  340. package/src/tests/script-executor-quickjs-pool.test.ts +106 -0
  341. package/src/tests/script-executor-registry.test.ts +38 -0
  342. package/src/tests/secret-scrubber.test.ts +17 -2
  343. package/src/tests/session-costs-codex-recompute.test.ts +49 -0
  344. package/src/tests/session-tokens.test.ts +13 -3
  345. package/src/tests/skill-invoke.test.ts +142 -0
  346. package/src/tests/skill-usage-cohorts.test.ts +232 -0
  347. package/src/tests/skill-usage.test.ts +348 -0
  348. package/src/tests/slack-attachments-block.test.ts +23 -0
  349. package/src/tests/slack-inbound-diagnostics-auth.test.ts +93 -0
  350. package/src/tests/slack-inbound-receipts.test.ts +684 -0
  351. package/src/tests/slack-render-v2.test.ts +6 -0
  352. package/src/tests/slack-session-status.test.ts +4 -2
  353. package/src/tests/status-instance.test.ts +71 -0
  354. package/src/tests/task-supersede-resume.test.ts +87 -4
  355. package/src/tests/telemetry-enable.test.ts +113 -0
  356. package/src/tests/telemetry-ui-route.test.ts +195 -0
  357. package/src/tests/tool-annotations.test.ts +1 -1
  358. package/src/tests/ui-e2e-ingest-payload.test.ts +5 -1
  359. package/src/tests/used-by-route.test.ts +237 -0
  360. package/src/tests/user-token-routes.test.ts +1 -1
  361. package/src/tests/worker-nudges.test.ts +653 -0
  362. package/src/tests/worker-realtime-client.test.ts +188 -0
  363. package/src/tests/workflow-executors.test.ts +3 -2
  364. package/src/tests/workflow-sub-workflow.test.ts +385 -0
  365. package/src/tools/app-upsert.ts +1 -1
  366. package/src/tools/runtime-controller-list.ts +28 -0
  367. package/src/tools/runtime-pool-bind-agent.ts +29 -0
  368. package/src/tools/runtime-pool-common.ts +37 -0
  369. package/src/tools/runtime-pool-create-agent.ts +29 -0
  370. package/src/tools/runtime-pool-delete.ts +27 -0
  371. package/src/tools/runtime-pool-list.ts +27 -0
  372. package/src/tools/runtime-pool-upsert.ts +31 -0
  373. package/src/tools/script-common.ts +1 -1
  374. package/src/tools/send-task.ts +3 -2
  375. package/src/tools/tool-config.ts +6 -0
  376. package/src/types.ts +137 -2
  377. package/src/utils/config-secrets.ts +1 -0
  378. package/src/utils/internal-ai/complete-structured.ts +6 -3
  379. package/src/utils/lead-monitor-channels.ts +3 -0
  380. package/src/utils/org-website.ts +104 -0
  381. package/src/utils/request-auth-context.ts +10 -2
  382. package/src/utils/runtime-model-catalog.ts +1 -1
  383. package/src/utils/runtime-pools-flag.ts +11 -0
  384. package/src/utils/secret-scrubber.ts +9 -0
  385. package/src/utils/session-token-connections.ts +20 -0
  386. package/src/workflows/engine.ts +17 -1
  387. package/src/workflows/executors/agent-task.ts +2 -2
  388. package/src/workflows/executors/human-in-the-loop.ts +3 -3
  389. package/src/workflows/executors/registry.ts +2 -0
  390. package/src/workflows/executors/script.ts +1 -1
  391. package/src/workflows/executors/sub-workflow.ts +135 -0
  392. package/src/workflows/executors/wait.ts +3 -3
  393. package/src/workflows/recovery.ts +11 -1
  394. package/src/workflows/resume.ts +77 -0
  395. package/src/workflows/wait-filter.ts +5 -7
  396. package/templates/skills/apps/content.md +1 -1
  397. package/templates/skills/code-quality/content.md +9 -1
  398. package/templates/skills/db-query-guidance/content.md +9 -0
  399. package/templates/skills/heartbeat-runbook/content.md +3 -1
  400. package/templates/skills/pages/content.md +25 -0
  401. package/templates/skills/swarm-scripts/SKILL.md +1 -1
  402. package/templates/skills/swarm-scripts/content.md +1 -1
  403. package/templates/skills/work-on-task/SKILL.md +1 -1
  404. package/templates/skills/work-on-task/content.md +1 -1
  405. package/dist/{cli-qd97v6z2.js → cli-1qtgd1h1.js} +3 -3
  406. package/dist/{cli-cmxkbzzs.js → cli-j0w59p15.js} +6 -6
  407. package/dist/{google-generative-ai-b8zxrpax.js → google-generative-ai-pvhm0dq1.js} +1 -1
  408. package/dist/{google-vertex-nrdd697t.js → google-vertex-tek9sqe2.js} +4 -4
  409. package/dist/{mistral-conversations-059y51zt.js → mistral-conversations-rjxa9s1j.js} +3 -3
  410. package/dist/{openai-completions-663yncfq.js → openai-completions-44gb0zsk.js} +12 -12
@@ -0,0 +1,981 @@
1
+ import { canProvisionKind, type RuntimeHandle } from "../../runtime-providers";
2
+ import {
3
+ type ControllerCommands,
4
+ ControllerOfflineError,
5
+ } from "../../runtime-providers/controller";
6
+ import { RuntimeHandleSchema } from "../../runtime-providers/controller-protocol";
7
+ import { RUNTIME_LABELS } from "../../runtime-providers/types";
8
+ import { resolveWorkerApiUrl } from "../../runtime-providers/worker-api-url";
9
+ import { createWorkerTaskFollowUp } from "../../tasks/worker-follow-up";
10
+ import {
11
+ type Agent,
12
+ isTerminalTaskStatus,
13
+ type RuntimeInstance,
14
+ type RuntimePool,
15
+ TERMINAL_TASK_STATUSES,
16
+ } from "../../types";
17
+ import { isMultiRuntimeEnabled } from "../../utils/multi-runtime";
18
+ import { isRuntimePoolsEnabled } from "../../utils/runtime-pools-flag";
19
+ import { registerVolatileSecret, scrubSecrets } from "../../utils/secret-scrubber";
20
+ import { workflowEventBus } from "../../workflows/event-bus";
21
+ import { canClaim } from "../budget-admission";
22
+ import { checkDependencies, failTask, getDbClient, getResolvedConfig, getTaskById } from "../db";
23
+ import {
24
+ getRuntimeInstanceById,
25
+ insertProvisioningRuntime,
26
+ listRuntimeInstancesForAgent,
27
+ listRuntimeInstancesForPool,
28
+ markRuntimeTerminated,
29
+ runtimeStaleThresholdMinutes,
30
+ } from "../multi-runtime";
31
+ import { mintRuntimeToken } from "../users";
32
+ import {
33
+ clearRuntimeOwnerIdCache,
34
+ ensureRuntimeOwnerId,
35
+ getControllerCommands,
36
+ reconcileDisconnectedControllers,
37
+ reconcileRemoteProvisions,
38
+ } from "./controllers";
39
+ import {
40
+ getPoolForAgent,
41
+ getRuntimePool,
42
+ listAgentsForPool,
43
+ listRuntimePools,
44
+ resolvePoolDelivery,
45
+ } from "./db";
46
+ import { type AgentDemand, computeDesired, pendingWorkForAgent } from "./demand";
47
+ import { resolveE2bImage, stopE2bImageBuilds } from "./e2b-images";
48
+ import { isRuntimeWanted, rotatingRuntimes } from "./lifecycle";
49
+ import {
50
+ recoverPausedRuntime,
51
+ recoverRuntime,
52
+ serializeRuntimeAction,
53
+ startRuntimeRecovery,
54
+ stopRuntimeRecovery,
55
+ } from "./recovery";
56
+ import { remoteControllerSession } from "./remote-controller";
57
+
58
+ let timer: ReturnType<typeof setInterval> | null = null;
59
+ let kickTimer: ReturnType<typeof setTimeout> | null = null;
60
+ let inFlight: Promise<void> | null = null;
61
+ let tickAbort: AbortController | null = null;
62
+ let warnedMultiRuntime = false;
63
+ const orphanFirstSeen = new Map<string, number>();
64
+ const ORPHAN_GRACE_MS = 120_000;
65
+ const TERMINAL_TASK_STATUS_PLACEHOLDERS = TERMINAL_TASK_STATUSES.map(() => "?").join(", ");
66
+ const TASKLESS_TRIGGER_SQL = "'unread_mentions', 'pool_tasks_available', 'channel_activity'";
67
+
68
+ export function runtimeSupervisorStatus(): string {
69
+ if (!isRuntimePoolsEnabled()) return "disabled";
70
+ if (!isMultiRuntimeEnabled()) return "disabled: MULTI_RUNTIME_ENABLED=false";
71
+ return timer ? "running" : "stopped";
72
+ }
73
+
74
+ function reportSupervisorError(error: unknown): void {
75
+ if (error instanceof ControllerOfflineError) return;
76
+ console.error("[runtime-pools] Supervisor failed:", scrubSecrets(String(error)));
77
+ }
78
+
79
+ function runtimeNumber(key: string, fallback: number): number {
80
+ const value = Number(process.env[key]);
81
+ return Number.isInteger(value) && value > 0 ? value : fallback;
82
+ }
83
+
84
+ function poolLeaseTtl(pool: RuntimePool, controller: ControllerCommands): number {
85
+ return Math.min(
86
+ Math.max(pool.ttlSec ?? 900, runtimeNumber("RUNTIME_LEASE_RENEW_BEFORE_SEC", 300) + 60),
87
+ controller.traits.maxTtlSec ?? Infinity,
88
+ controller.traits.maxContinuousSec ?? Infinity,
89
+ );
90
+ }
91
+
92
+ async function recordProvisionFailure(
93
+ runtime: Pick<RuntimeInstance, "agentId" | "poolId" | "controllerId"> & { id: string | null },
94
+ reason: string,
95
+ now: Date,
96
+ countedFailures: Set<string>,
97
+ ): Promise<void> {
98
+ const error = scrubSecrets(reason);
99
+ const failedTask = await getDbClient().transaction(async (tx) => {
100
+ if (runtime.id) {
101
+ const retired = await tx.run(
102
+ "UPDATE runtime_instances SET provision_error = ? WHERE id = ? AND status = 'provisioning' AND provision_error IS NULL",
103
+ [error, runtime.id],
104
+ );
105
+ if (!retired.changes) return null;
106
+ await markRuntimeTerminated(runtime.id, error);
107
+ }
108
+ // Retire every failed runtime, but advance retry and circuit state once per agent per tick.
109
+ if (countedFailures.has(runtime.agentId)) return null;
110
+ countedFailures.add(runtime.agentId);
111
+ const pool = await tx.get<{ circuit_open_until: string | null }>(
112
+ `UPDATE runtime_pools SET consecutive_failures = consecutive_failures + 1,
113
+ last_error = ?, last_error_at = ?, updated_at = ?,
114
+ circuit_open_until = CASE WHEN consecutive_failures + 1 >= ? OR circuit_open_until IS NOT NULL
115
+ THEN ? ELSE circuit_open_until END WHERE id = ? RETURNING circuit_open_until`,
116
+ [
117
+ error,
118
+ now.toISOString(),
119
+ now.toISOString(),
120
+ runtimeNumber("RUNTIME_CIRCUIT_THRESHOLD", 5),
121
+ new Date(
122
+ now.getTime() + runtimeNumber("RUNTIME_CIRCUIT_OPEN_MIN", 10) * 60_000,
123
+ ).toISOString(),
124
+ runtime.poolId ?? null,
125
+ ],
126
+ );
127
+ await tx.run(
128
+ "UPDATE runtime_controllers SET last_error = ?, last_error_at = ?, updated_at = ? WHERE id = ?",
129
+ [error, now.toISOString(), now.toISOString(), runtime.controllerId ?? null],
130
+ );
131
+ const state = await tx.get<{ runtime_provision_attempts: number }>(
132
+ "UPDATE agents SET runtime_provision_attempts = runtime_provision_attempts + 1 WHERE id = ? RETURNING runtime_provision_attempts",
133
+ [runtime.agentId],
134
+ );
135
+ const attempts = state!.runtime_provision_attempts;
136
+ const delay = [30, 90, 180][Math.min(attempts - 1, 2)]!;
137
+ await tx.run("UPDATE agents SET runtime_provision_retry_at = ? WHERE id = ?", [
138
+ new Date(now.getTime() + delay * 1000).toISOString(),
139
+ runtime.agentId,
140
+ ]);
141
+ if (pool?.circuit_open_until)
142
+ await tx.run(
143
+ "UPDATE agents SET runtime_provision_attempts = 0, runtime_provision_retry_at = NULL WHERE runtime_pool_id = ?",
144
+ [runtime.poolId ?? null],
145
+ );
146
+ if (attempts < runtimeNumber("RUNTIME_PROVISION_MAX_ATTEMPTS", 3)) return null;
147
+ const pending = await tx.query<{ id: string }>(
148
+ "SELECT id FROM agent_tasks WHERE agentId = ? AND status = 'pending' ORDER BY createdAt, rowid",
149
+ [runtime.agentId],
150
+ );
151
+ for (const row of pending) {
152
+ if (!(await checkDependencies(row.id)).ready) continue;
153
+ const failed = await failTask(
154
+ row.id,
155
+ `provisioning failed after ${attempts} attempts: ${error}`,
156
+ );
157
+ if (!failed) continue;
158
+ await tx.run(
159
+ "UPDATE agents SET runtime_provision_attempts = 0, runtime_provision_retry_at = NULL WHERE id = ?",
160
+ [runtime.agentId],
161
+ );
162
+ return failed;
163
+ }
164
+ await tx.run(
165
+ "UPDATE agents SET runtime_provision_attempts = 0, runtime_provision_retry_at = NULL WHERE id = ?",
166
+ [runtime.agentId],
167
+ );
168
+ return null;
169
+ });
170
+ if (failedTask) {
171
+ try {
172
+ await createWorkerTaskFollowUp({
173
+ task: failedTask,
174
+ status: "failed",
175
+ failureReason: failedTask.failureReason ?? error,
176
+ });
177
+ } catch (followUpError) {
178
+ reportSupervisorError(followUpError);
179
+ }
180
+ }
181
+ }
182
+
183
+ export async function provisionRuntime(
184
+ pool: RuntimePool,
185
+ agent: Agent,
186
+ controller: ControllerCommands,
187
+ ownerId: string,
188
+ now = new Date(),
189
+ options: {
190
+ signal?: AbortSignal;
191
+ onFailure?: () => void;
192
+ countedFailures?: Set<string>;
193
+ } = {},
194
+ ): Promise<boolean> {
195
+ const { signal, onFailure, countedFailures = new Set<string>() } = options;
196
+ const runtimeId = crypto.randomUUID();
197
+ try {
198
+ const image =
199
+ pool.kind === "e2b"
200
+ ? await resolveE2bImage(pool.id, signal)
201
+ : (pool.resolvedImage ?? pool.image!);
202
+ if (signal?.aborted || image === null) return false;
203
+ const token = await getDbClient().transaction(async () => {
204
+ const credential = await mintRuntimeToken(agent.id, runtimeId);
205
+ await insertProvisioningRuntime({
206
+ id: runtimeId,
207
+ agentId: agent.id,
208
+ poolId: pool.id,
209
+ controllerId: pool.placements[0]!,
210
+ reportedSlots: pool.slotsPerRuntime,
211
+ });
212
+ await getDbClient().run(
213
+ `UPDATE runtime_instances SET provision_attempts =
214
+ (SELECT runtime_provision_attempts + 1 FROM agents WHERE id = ?),
215
+ provision_requested_at = ? WHERE id = ?`,
216
+ [agent.id, now.toISOString(), runtimeId],
217
+ );
218
+ signal?.throwIfAborted();
219
+ return credential.plaintext;
220
+ });
221
+ signal?.throwIfAborted();
222
+ registerVolatileSecret(token, `runtime:${runtimeId}`);
223
+ const templateId = (await getResolvedConfig(agent.id)).find(
224
+ (c) => c.key === "TEMPLATE_ID",
225
+ )?.value;
226
+ signal?.throwIfAborted();
227
+ const env: Record<string, string> = {
228
+ ...pool.env,
229
+ MCP_BASE_URL: resolveWorkerApiUrl({
230
+ WORKER_API_URL: controller.workerApiUrl,
231
+ RUNTIME_WORKER_API_URL: process.env.RUNTIME_WORKER_API_URL,
232
+ }),
233
+ AGENT_ID: agent.id,
234
+ AGENT_ROLE: agent.isLead ? "lead" : "worker",
235
+ HARNESS_PROVIDER: agent.harnessProvider ?? "claude",
236
+ API_KEY: token,
237
+ AGENT_SWARM_API_KEY: token,
238
+ SWARM_RUNTIME_INSTANCE_ID: runtimeId,
239
+ MAX_CONCURRENT_TASKS: String(pool.slotsPerRuntime),
240
+ SWARM_DELIVERY_MODE: resolvePoolDelivery(pool),
241
+ };
242
+ // Lifecycle and template identity belong to the API, even when absent.
243
+ delete env.SWARM_EXIT_AFTER_TASKS;
244
+ delete env.TEMPLATE_ID;
245
+ if (pool.lifecycle === "per-task") env.SWARM_EXIT_AFTER_TASKS = "1";
246
+ if (templateId) env.TEMPLATE_ID = templateId;
247
+ const ttlSec = pool.kind === "e2b" ? poolLeaseTtl(pool, controller) : undefined;
248
+ signal?.throwIfAborted();
249
+ const handle = await controller.provision({
250
+ runtimeId,
251
+ agentId: agent.id,
252
+ poolId: pool.id,
253
+ ownerId,
254
+ image,
255
+ ...(ttlSec === undefined ? {} : { ttlSec }),
256
+ env,
257
+ resources: pool.resources ?? undefined,
258
+ labels: {
259
+ [RUNTIME_LABELS.runtimeId]: runtimeId,
260
+ [RUNTIME_LABELS.poolId]: pool.id,
261
+ [RUNTIME_LABELS.agentId]: agent.id,
262
+ [RUNTIME_LABELS.owner]: ownerId,
263
+ },
264
+ });
265
+ if (signal?.aborted) {
266
+ await controller.terminate(handle, "Supervisor stopped during provision");
267
+ await markRuntimeTerminated(runtimeId, "Supervisor stopped during provision");
268
+ return false;
269
+ }
270
+ await getDbClient().run(
271
+ "UPDATE runtime_instances SET external_handle = ?, lease_expires_at = ?, created_at = COALESCE(?, created_at), updated_at = ? WHERE id = ?",
272
+ [
273
+ JSON.stringify(handle),
274
+ ttlSec === undefined
275
+ ? null
276
+ : (handle.meta?.leaseExpiresAt ?? new Date(now.getTime() + ttlSec * 1000).toISOString()),
277
+ pool.kind === "e2b" ? (handle.createdAt ?? now.toISOString()) : null,
278
+ new Date().toISOString(),
279
+ runtimeId,
280
+ ],
281
+ );
282
+ return true;
283
+ } catch (error) {
284
+ // Stop new requests before asynchronous failure cleanup commits.
285
+ onFailure?.();
286
+ if (signal?.aborted) {
287
+ await markRuntimeTerminated(runtimeId, "Supervisor stopped during provision");
288
+ return false;
289
+ }
290
+ reportSupervisorError(error);
291
+ // Keep an ambiguous remote provision reserved until reconnect or disconnect recovery.
292
+ if (error instanceof ControllerOfflineError) return false;
293
+ try {
294
+ const runtime = await getRuntimeInstanceById(runtimeId);
295
+ await recordProvisionFailure(
296
+ runtime ?? {
297
+ id: null,
298
+ agentId: agent.id,
299
+ poolId: pool.id,
300
+ controllerId: pool.placements[0]!,
301
+ },
302
+ String(error),
303
+ now,
304
+ countedFailures,
305
+ );
306
+ } catch (cleanupError) {
307
+ reportSupervisorError(cleanupError);
308
+ }
309
+ return false;
310
+ }
311
+ }
312
+
313
+ export function runtimeHandle(runtime: RuntimeInstance): RuntimeHandle | null {
314
+ return runtime.externalHandle ? RuntimeHandleSchema.parse(runtime.externalHandle) : null;
315
+ }
316
+
317
+ export async function terminateRuntime(
318
+ runtime: RuntimeInstance,
319
+ controller: ControllerCommands,
320
+ reason: string,
321
+ ): Promise<void> {
322
+ const handle = runtimeHandle(runtime);
323
+ try {
324
+ if (handle) await controller.terminate(handle, reason);
325
+ } catch (error) {
326
+ reportSupervisorError(error);
327
+ }
328
+ await markRuntimeTerminated(runtime.id, reason);
329
+ }
330
+
331
+ export async function reapPoolRuntimes(
332
+ pool: RuntimePool,
333
+ runtimes: RuntimeInstance[],
334
+ controller: ControllerCommands,
335
+ now: Date,
336
+ listedHandles?: RuntimeHandle[] | null,
337
+ rotatingIds?: ReadonlySet<string>,
338
+ countedFailures = new Set<string>(),
339
+ controllers: ReadonlyMap<string, ControllerCommands> = new Map([
340
+ [pool.placements[0]!, controller],
341
+ ]),
342
+ listings: ReadonlyMap<string, RuntimeHandle[] | null | undefined> = new Map([
343
+ [pool.placements[0]!, listedHandles],
344
+ ]),
345
+ ): Promise<void> {
346
+ const rotations =
347
+ rotatingIds ??
348
+ (await rotatingRuntimes(
349
+ pool.id,
350
+ new Date(
351
+ now.getTime() + runtimeNumber("RUNTIME_TOKEN_ROTATE_BEFORE_MIN", 60) * 60_000,
352
+ ).toISOString(),
353
+ now,
354
+ controller.traits.maxContinuousSec,
355
+ ));
356
+ const cutoff = now.getTime() - runtimeStaleThresholdMinutes() * 60_000;
357
+ const sessionCutoff = new Date(
358
+ now.getTime() - runtimeNumber("HEARTBEAT_STALE_CLEANUP_MIN", 30) * 60_000,
359
+ ).toISOString();
360
+ const remaining = new Map<string, number>();
361
+ const statusesByController = new Map<string, Awaited<ReturnType<ControllerCommands["status"]>>>();
362
+ for (const [controllerId, controller] of controllers) {
363
+ const handles = runtimes
364
+ .filter(
365
+ (r) =>
366
+ (r.controllerId ?? pool.placements[0]) === controllerId &&
367
+ r.status !== "offline" &&
368
+ (runtimeHandle(r)?.kind === "e2b" || Date.parse(r.lastSeenAt) >= cutoff),
369
+ )
370
+ .map(runtimeHandle)
371
+ .filter((h): h is RuntimeHandle => h !== null);
372
+ let statuses: Awaited<ReturnType<ControllerCommands["status"]>> = {};
373
+ try {
374
+ if (handles.length) statuses = await controller.status(handles);
375
+ } catch (error) {
376
+ reportSupervisorError(error);
377
+ // One lookup failure must not prevent sibling recovery.
378
+ for (const handle of handles) {
379
+ try {
380
+ Object.assign(statuses, await controller.status([handle]));
381
+ } catch (lookupError) {
382
+ reportSupervisorError(lookupError);
383
+ }
384
+ }
385
+ }
386
+ statusesByController.set(controllerId, statuses);
387
+ }
388
+ for (const runtime of runtimes)
389
+ if (
390
+ runtime.status === "provisioning" ||
391
+ (runtime.status === "active" &&
392
+ (Date.parse(runtime.lastSeenAt) >= cutoff ||
393
+ (runtimeHandle(runtime)?.kind === "e2b" &&
394
+ statusesByController.get(runtime.controllerId ?? pool.placements[0]!)?.[
395
+ runtimeHandle(runtime)?.externalId ?? ""
396
+ ] === "paused")))
397
+ )
398
+ remaining.set(runtime.agentId, (remaining.get(runtime.agentId) ?? 0) + 1);
399
+ for (const candidate of runtimes) {
400
+ try {
401
+ await serializeRuntimeAction(candidate.id, async () => {
402
+ const runtime = await getRuntimeInstanceById(candidate.id);
403
+ if (!runtime) return;
404
+ const staleSession =
405
+ pool.lifecycle === "per-task" && runtime.status !== "offline"
406
+ ? null
407
+ : await getDbClient().get<{ id: string }>(
408
+ `SELECT id FROM active_sessions WHERE runtimeInstanceId = ? AND NOT EXISTS (
409
+ SELECT 1 FROM agent_tasks WHERE id = active_sessions.taskId
410
+ AND status NOT IN (${TERMINAL_TASK_STATUS_PLACEHOLDERS})
411
+ ) AND (EXISTS (SELECT 1 FROM agent_tasks WHERE id = active_sessions.taskId)
412
+ OR triggerType NOT IN (${TASKLESS_TRIGGER_SQL}) OR lastHeartbeatAt < ?) LIMIT 1`,
413
+ [runtime.id, ...TERMINAL_TASK_STATUSES, sessionCutoff],
414
+ );
415
+ if (staleSession)
416
+ await getDbClient().run(
417
+ `DELETE FROM active_sessions WHERE runtimeInstanceId = ? AND NOT EXISTS (
418
+ SELECT 1 FROM agent_tasks WHERE id = active_sessions.taskId
419
+ AND status NOT IN (${TERMINAL_TASK_STATUS_PLACEHOLDERS})
420
+ ) AND (EXISTS (SELECT 1 FROM agent_tasks WHERE id = active_sessions.taskId)
421
+ OR triggerType NOT IN (${TASKLESS_TRIGGER_SQL}) OR lastHeartbeatAt < ?)`,
422
+ [runtime.id, ...TERMINAL_TASK_STATUSES, sessionCutoff],
423
+ );
424
+ if (runtime.status === "offline") {
425
+ const session = await getDbClient().get<{ taskId: string }>(
426
+ "SELECT taskId FROM active_sessions WHERE runtimeInstanceId = ? LIMIT 1",
427
+ [runtime.id],
428
+ );
429
+ if (session)
430
+ await recoverRuntime(runtime.id, runtime.provisionError ?? "Runtime offline");
431
+ return;
432
+ }
433
+ const controllerId = runtime.controllerId ?? pool.placements[0]!;
434
+ const controller = controllers.get(controllerId);
435
+ if (!controller) return;
436
+ const listedHandles = listings.get(controllerId);
437
+ const handle = runtimeHandle(runtime);
438
+ let status = handle
439
+ ? statusesByController.get(controllerId)?.[handle.externalId]
440
+ : undefined;
441
+ const isE2b = handle?.kind === "e2b";
442
+ if (isE2b && status === "paused" && handle)
443
+ status = (await controller.status([handle]))[handle.externalId];
444
+ const stale =
445
+ runtime.status === "active" &&
446
+ Date.parse(runtime.lastSeenAt) < cutoff &&
447
+ !(isE2b && status === "paused");
448
+ const missing =
449
+ handle &&
450
+ listedHandles &&
451
+ !listedHandles.some((h) => h.externalId === handle.externalId) &&
452
+ !(isE2b && status === "paused");
453
+ const timeoutSec = runtimeNumber("RUNTIME_PROVISION_TIMEOUT_SEC", 300);
454
+ const timedOut =
455
+ runtime.status === "provisioning" &&
456
+ now.getTime() - Date.parse(runtime.provisionRequestedAt ?? runtime.createdAt) >=
457
+ timeoutSec * 1000;
458
+ if (status === "exited" || status === "failed" || stale || missing || timedOut) {
459
+ const reason = timedOut
460
+ ? `runtime did not register within ${timeoutSec}s (image may lack the swarm runner, or WORKER_API_URL is unreachable)`
461
+ : stale
462
+ ? "Runtime stopped pinging"
463
+ : missing
464
+ ? "Runtime missing from provider list"
465
+ : `Runtime ${status}`;
466
+ if (runtime.status === "provisioning") {
467
+ await recordProvisionFailure(runtime, reason, now, countedFailures);
468
+ if ((await getRuntimeInstanceById(runtime.id))?.status !== "offline") return;
469
+ }
470
+ await terminateRuntime(runtime, controller, reason);
471
+ await recoverRuntime(runtime.id, reason);
472
+ if (!stale) remaining.set(runtime.agentId, (remaining.get(runtime.agentId) ?? 1) - 1);
473
+ return;
474
+ }
475
+ const busy = await getDbClient().get<{ taskId: string }>(
476
+ `SELECT s.taskId FROM active_sessions s LEFT JOIN agent_tasks t ON t.id = s.taskId
477
+ WHERE s.runtimeInstanceId = ? AND (t.status NOT IN (${TERMINAL_TASK_STATUS_PLACEHOLDERS})
478
+ OR (t.id IS NULL AND s.triggerType IN (${TASKLESS_TRIGGER_SQL}) AND s.lastHeartbeatAt >= ?)) LIMIT 1`,
479
+ [runtime.id, ...TERMINAL_TASK_STATUSES, sessionCutoff],
480
+ );
481
+ const task = runtime.taskId ? await getTaskById(runtime.taskId) : null;
482
+ const taskRunning = task?.status === "in_progress" || task?.status === "reviewing";
483
+ const rotating = rotations.has(runtime.id);
484
+ if (status === "paused" && isE2b) {
485
+ await recoverPausedRuntime(
486
+ runtime,
487
+ controller,
488
+ await isRuntimeWanted(
489
+ pool,
490
+ runtime,
491
+ rotating,
492
+ remaining.get(runtime.agentId) ?? 0,
493
+ now,
494
+ ),
495
+ !!busy || taskRunning,
496
+ now,
497
+ );
498
+ if ((await getRuntimeInstanceById(runtime.id))?.status === "offline")
499
+ remaining.set(runtime.agentId, (remaining.get(runtime.agentId) ?? 1) - 1);
500
+ return;
501
+ }
502
+ if (runtime.status !== "active" && pool.enabled) return;
503
+ if (busy || taskRunning) {
504
+ await getDbClient().run(
505
+ "UPDATE runtime_instances SET idle_since = NULL, draining = ?, task_id = COALESCE((SELECT id FROM agent_tasks WHERE id = ?), task_id) WHERE id = ?",
506
+ [rotating || !pool.enabled ? 1 : 0, busy?.taskId ?? null, runtime.id],
507
+ );
508
+ return;
509
+ }
510
+ if (pool.lifecycle === "per-task" && task && isTerminalTaskStatus(task.status)) {
511
+ const finishedAt = task.finishedAt ?? runtime.idleSince ?? now.toISOString();
512
+ if (!runtime.idleSince)
513
+ await getDbClient().run("UPDATE runtime_instances SET idle_since = ? WHERE id = ?", [
514
+ finishedAt,
515
+ runtime.id,
516
+ ]);
517
+ if (
518
+ now.getTime() - Date.parse(finishedAt) <
519
+ runtimeNumber("RUNTIME_PER_TASK_EXIT_GRACE_SEC", 60) * 1000
520
+ )
521
+ return;
522
+ await terminateRuntime(runtime, controller, "Task exit grace expired");
523
+ remaining.set(runtime.agentId, (remaining.get(runtime.agentId) ?? 1) - 1);
524
+ return;
525
+ }
526
+ if (rotating) {
527
+ if (!runtime.draining) {
528
+ await getDbClient().run("UPDATE runtime_instances SET draining = 1 WHERE id = ?", [
529
+ runtime.id,
530
+ ]);
531
+ } else {
532
+ await terminateRuntime(
533
+ runtime,
534
+ controller,
535
+ controller.traits.maxContinuousSec === undefined
536
+ ? "Runtime token expires soon"
537
+ : "Runtime rotation due",
538
+ );
539
+ remaining.set(runtime.agentId, (remaining.get(runtime.agentId) ?? 1) - 1);
540
+ }
541
+ return;
542
+ }
543
+ if (!pool.enabled) {
544
+ if (!runtime.draining) {
545
+ await getDbClient().run("UPDATE runtime_instances SET draining = 1 WHERE id = ?", [
546
+ runtime.id,
547
+ ]);
548
+ } else {
549
+ await terminateRuntime(runtime, controller, "Pool disabled");
550
+ remaining.set(runtime.agentId, (remaining.get(runtime.agentId) ?? 1) - 1);
551
+ }
552
+ return;
553
+ }
554
+ const idleSince = runtime.idleSince ?? now.toISOString();
555
+ if (!runtime.idleSince)
556
+ await getDbClient().run("UPDATE runtime_instances SET idle_since = ? WHERE id = ?", [
557
+ idleSince,
558
+ runtime.id,
559
+ ]);
560
+ if (pool.lifecycle !== "idle" || pool.idleMinutes === null) return;
561
+ if ((remaining.get(runtime.agentId) ?? 0) <= pool.minRuntimes) {
562
+ if (runtime.draining)
563
+ await getDbClient().run("UPDATE runtime_instances SET draining = 0 WHERE id = ?", [
564
+ runtime.id,
565
+ ]);
566
+ return;
567
+ }
568
+ if (now.getTime() - Date.parse(idleSince) < pool.idleMinutes * 60_000) return;
569
+ if (!runtime.draining) {
570
+ await getDbClient().run("UPDATE runtime_instances SET draining = 1 WHERE id = ?", [
571
+ runtime.id,
572
+ ]);
573
+ } else {
574
+ await terminateRuntime(runtime, controller, "Idle timeout");
575
+ remaining.set(runtime.agentId, (remaining.get(runtime.agentId) ?? 1) - 1);
576
+ }
577
+ });
578
+ } catch (error) {
579
+ reportSupervisorError(error);
580
+ }
581
+ }
582
+ }
583
+
584
+ async function renewPoolLeases(
585
+ pool: RuntimePool,
586
+ controllers: ReadonlyMap<string, ControllerCommands>,
587
+ now: Date,
588
+ ): Promise<void> {
589
+ for (const runtime of await listRuntimeInstancesForPool(pool.id)) {
590
+ const controller = controllers.get(runtime.controllerId ?? pool.placements[0]!);
591
+ const handle = runtimeHandle(runtime);
592
+ if (handle?.kind !== "e2b" || !controller?.renew) continue;
593
+ const renewBy =
594
+ now.getTime() +
595
+ Math.min(
596
+ runtimeNumber("RUNTIME_LEASE_RENEW_BEFORE_SEC", 300),
597
+ poolLeaseTtl(pool, controller) - Math.min(60, poolLeaseTtl(pool, controller) / 2),
598
+ ) *
599
+ 1000;
600
+ if (
601
+ runtime.status === "offline" ||
602
+ (runtime.leaseExpiresAt && Date.parse(runtime.leaseExpiresAt) > renewBy)
603
+ )
604
+ continue;
605
+ try {
606
+ const { endAt } = await controller.renew(handle, poolLeaseTtl(pool, controller));
607
+ if (!Number.isFinite(Date.parse(endAt)))
608
+ throw new Error("Provider returned an invalid lease expiry");
609
+ await getDbClient().run(
610
+ "UPDATE runtime_instances SET lease_expires_at = ?, updated_at = ? WHERE id = ? AND status != 'offline'",
611
+ [endAt, now.toISOString(), runtime.id],
612
+ );
613
+ console.debug(
614
+ scrubSecrets(`[runtime-pools] Runtime ${runtime.id} lease renewed until ${endAt}`),
615
+ );
616
+ } catch (error) {
617
+ reportSupervisorError(error);
618
+ // The next full reap checks provider death and preserves pool lifecycle rules.
619
+ }
620
+ }
621
+ }
622
+
623
+ export async function reconcileControllerOrphans(
624
+ controllerId: string,
625
+ controller: ControllerCommands,
626
+ ownerId: string,
627
+ now = new Date(),
628
+ listedHandles?: RuntimeHandle[] | null,
629
+ ): Promise<void> {
630
+ const rows = await getDbClient().query<{
631
+ id: string;
632
+ status: string;
633
+ external_handle: string | null;
634
+ }>("SELECT id, status, external_handle FROM runtime_instances WHERE controller_id = ?", [
635
+ controllerId,
636
+ ]);
637
+ const known = new Map<string, (typeof rows)[number]>();
638
+ for (const row of rows) {
639
+ if (!row.external_handle) continue;
640
+ try {
641
+ known.set(RuntimeHandleSchema.parse(JSON.parse(row.external_handle)).externalId, row);
642
+ } catch (error) {
643
+ reportSupervisorError(error);
644
+ }
645
+ }
646
+ const handles = listedHandles ?? (await controller.list(ownerId));
647
+ const listed = new Set(handles.map((handle) => `${controllerId}:${handle.externalId}`));
648
+ for (const key of orphanFirstSeen.keys())
649
+ if (key.startsWith(`${controllerId}:`) && !listed.has(key)) orphanFirstSeen.delete(key);
650
+ for (const handle of handles) {
651
+ try {
652
+ const row = known.get(handle.externalId);
653
+ const key = `${controllerId}:${handle.externalId}`;
654
+ if (row && row.status !== "offline") {
655
+ orphanFirstSeen.delete(key);
656
+ continue;
657
+ }
658
+ if (!row) {
659
+ const created = handle.createdAt ? Date.parse(handle.createdAt) : NaN;
660
+ const firstSeen = Number.isFinite(created)
661
+ ? created
662
+ : (orphanFirstSeen.get(key) ?? now.getTime());
663
+ orphanFirstSeen.set(key, firstSeen);
664
+ if (now.getTime() - firstSeen < ORPHAN_GRACE_MS) continue;
665
+ }
666
+ await controller.terminate(handle, "Orphan runtime");
667
+ if (row) await markRuntimeTerminated(row.id, "Runtime closed");
668
+ orphanFirstSeen.delete(key);
669
+ } catch (error) {
670
+ reportSupervisorError(error);
671
+ }
672
+ }
673
+ }
674
+
675
+ async function reconcilePools(
676
+ now: Date,
677
+ commands: typeof getControllerCommands,
678
+ signal: AbortSignal,
679
+ ): Promise<void> {
680
+ await reconcileDisconnectedControllers(recoverRuntime, now);
681
+ const ownerId = await ensureRuntimeOwnerId();
682
+ const controllers = new Map<string, ControllerCommands>();
683
+ const listed = new Map<string, RuntimeHandle[] | null>();
684
+ const countedFailures = new Set<string>();
685
+ const rotationCutoff = new Date(
686
+ now.getTime() + runtimeNumber("RUNTIME_TOKEN_ROTATE_BEFORE_MIN", 60) * 60_000,
687
+ ).toISOString();
688
+ for (const pool of await listRuntimePools()) {
689
+ if (signal.aborted) return;
690
+ if (!canProvisionKind(pool.kind)) continue;
691
+ const controllerId = pool.placements[0]!;
692
+ try {
693
+ const poolRuntimes = await listRuntimeInstancesForPool(pool.id);
694
+ const poolControllerIds = new Set([
695
+ controllerId,
696
+ ...poolRuntimes.map((r) => r.controllerId ?? controllerId),
697
+ ]);
698
+ const rotatingIds = new Set<string>();
699
+ for (const id of poolControllerIds) {
700
+ try {
701
+ const controller = controllers.get(id) ?? (await commands(id));
702
+ controllers.set(id, controller);
703
+ if (!controller.traits.canProvision) continue;
704
+ if (!listed.has(id)) {
705
+ try {
706
+ listed.set(id, await controller.list(ownerId));
707
+ } catch (error) {
708
+ reportSupervisorError(error);
709
+ listed.set(id, null);
710
+ }
711
+ const handles = listed.get(id);
712
+ if (handles && remoteControllerSession(id) === controller)
713
+ await reconcileRemoteProvisions(id, handles, ownerId);
714
+ if (handles) await reconcileControllerOrphans(id, controller, ownerId, now, handles);
715
+ }
716
+ const rotations = await rotatingRuntimes(
717
+ pool.id,
718
+ rotationCutoff,
719
+ now,
720
+ controller.traits.maxContinuousSec,
721
+ );
722
+ for (const runtime of poolRuntimes)
723
+ if ((runtime.controllerId ?? controllerId) === id && rotations.has(runtime.id))
724
+ rotatingIds.add(runtime.id);
725
+ } catch (error) {
726
+ reportSupervisorError(error);
727
+ }
728
+ }
729
+ const controller = controllers.get(controllerId);
730
+ const existingController =
731
+ controller ??
732
+ [...poolControllerIds].map((id) => controllers.get(id)).find((c) => c?.traits.canProvision);
733
+ if (!existingController) continue;
734
+ await reapPoolRuntimes(
735
+ pool,
736
+ await listRuntimeInstancesForPool(pool.id),
737
+ existingController,
738
+ now,
739
+ listed.get(controllerId),
740
+ rotatingIds,
741
+ countedFailures,
742
+ controllers,
743
+ listed,
744
+ );
745
+ await renewPoolLeases(pool, controllers, now);
746
+ if (signal.aborted) return;
747
+ if (!controller?.traits.canProvision) continue;
748
+ const currentPool = await getRuntimePool(pool.id);
749
+ const agents = await listAgentsForPool(pool.id);
750
+ const demands: AgentDemand[] = [];
751
+ const work = new Map<string, Awaited<ReturnType<typeof pendingWorkForAgent>>>();
752
+ for (const agent of agents) {
753
+ if (signal.aborted) return;
754
+ const pending = await pendingWorkForAgent(agent.id);
755
+ work.set(agent.id, pending);
756
+ if (!pending.work && (agent.runtimeProvisionAttempts || agent.runtimeProvisionRetryAt))
757
+ await getDbClient().run(
758
+ "UPDATE agents SET runtime_provision_attempts = 0, runtime_provision_retry_at = NULL WHERE id = ?",
759
+ [agent.id],
760
+ );
761
+ const runtimes = await listRuntimeInstancesForAgent(agent.id);
762
+ let freeSlots = 0;
763
+ let provisioning = 0;
764
+ for (const runtime of runtimes) {
765
+ if (runtime.status === "provisioning") provisioning++;
766
+ if (!runtime.isLive || runtime.draining || runtime.credentialReady === false) continue;
767
+ const task = runtime.taskId ? await getTaskById(runtime.taskId) : null;
768
+ if (pool.lifecycle === "per-task" && task && isTerminalTaskStatus(task.status)) continue;
769
+ const sessions = await getDbClient().get<{ count: number }>(
770
+ "SELECT COUNT(*) AS count FROM active_sessions WHERE runtimeInstanceId = ?",
771
+ [runtime.id],
772
+ );
773
+ let occupied = sessions?.count ?? 0;
774
+ if (occupied === 0 && runtime.taskId) {
775
+ if (task?.status === "in_progress" || task?.status === "reviewing") occupied = 1;
776
+ }
777
+ freeSlots += Math.max(0, runtime.reportedSlots - occupied);
778
+ }
779
+ demands.push({
780
+ agentId: agent.id,
781
+ maxTasks: agent.maxTasks ?? 1,
782
+ work: pending.work,
783
+ current: runtimes.filter(
784
+ (r) =>
785
+ r.status === "provisioning" || (r.isLive && !(r.draining && rotatingIds.has(r.id))),
786
+ ).length,
787
+ freeSlots,
788
+ provisioning,
789
+ });
790
+ }
791
+ if (
792
+ !pool.enabled ||
793
+ (currentPool?.circuitOpenUntil && Date.parse(currentPool.circuitOpenUntil) > now.getTime())
794
+ )
795
+ continue;
796
+ const poolCount = (await listRuntimeInstancesForPool(pool.id)).filter(
797
+ (r) =>
798
+ r.status === "provisioning" ||
799
+ (r.status === "active" &&
800
+ Date.parse(r.lastSeenAt) >= now.getTime() - runtimeStaleThresholdMinutes() * 60_000),
801
+ ).length;
802
+ const counted = demands.reduce((n, d) => n + d.current, 0);
803
+ const desired = computeDesired(
804
+ {
805
+ ...pool,
806
+ maxRuntimes:
807
+ pool.maxRuntimes === null
808
+ ? null
809
+ : Math.max(0, pool.maxRuntimes - Math.max(0, poolCount - counted)),
810
+ },
811
+ demands,
812
+ );
813
+ const provisions = new Set<Promise<void>>();
814
+ const failedAgents = new Set<string>();
815
+ const concurrency = runtimeNumber("RUNTIME_PROVISION_CONCURRENCY", 8);
816
+ try {
817
+ for (const agent of agents) {
818
+ const demand = demands.find((d) => d.agentId === agent.id)!;
819
+ const pending = work.get(agent.id)!;
820
+ for (let count = demand.current; count < (desired.get(agent.id) ?? 0); count++) {
821
+ if (provisions.size >= concurrency) await Promise.race(provisions);
822
+ if (signal.aborted) return;
823
+ if (failedAgents.has(agent.id)) break;
824
+ if (!(pool.resolvedImage ?? pool.image)) break;
825
+ if (!(await canClaim(agent.id, now, pending.topTask?.requestedByUserId)).allowed) break;
826
+ if (signal.aborted) return;
827
+ const retry = await getDbClient().get<{
828
+ runtime_provision_retry_at: string | null;
829
+ runtime_provision_attempts: number;
830
+ }>(
831
+ "SELECT runtime_provision_retry_at, runtime_provision_attempts FROM agents WHERE id = ?",
832
+ [agent.id],
833
+ );
834
+ if (
835
+ retry?.runtime_provision_retry_at &&
836
+ Date.parse(retry.runtime_provision_retry_at) > now.getTime()
837
+ )
838
+ break;
839
+ const retrying = (retry?.runtime_provision_attempts ?? 0) > 0;
840
+ if (retrying && demand.provisioning > 0) break;
841
+ const freshPool = await getRuntimePool(pool.id);
842
+ if (!freshPool?.enabled) break;
843
+ if (freshPool.circuitOpenUntil) {
844
+ if (Date.parse(freshPool.circuitOpenUntil) > now.getTime()) break;
845
+ if (
846
+ provisions.size ||
847
+ (await listRuntimeInstancesForPool(pool.id)).some(
848
+ (r) => r.status === "provisioning",
849
+ )
850
+ )
851
+ break;
852
+ }
853
+ if (signal.aborted) return;
854
+ if (failedAgents.has(agent.id)) break;
855
+ const provision = provisionRuntime(pool, agent, controller, ownerId, now, {
856
+ signal,
857
+ countedFailures,
858
+ onFailure: () => {
859
+ failedAgents.add(agent.id);
860
+ },
861
+ }).then(
862
+ (success) => {
863
+ provisions.delete(provision);
864
+ if (!success) failedAgents.add(agent.id);
865
+ },
866
+ (error) => {
867
+ provisions.delete(provision);
868
+ failedAgents.add(agent.id);
869
+ reportSupervisorError(error);
870
+ },
871
+ );
872
+ provisions.add(provision);
873
+ if (retrying) break;
874
+ }
875
+ }
876
+ } finally {
877
+ await Promise.all(provisions);
878
+ }
879
+ } catch (error) {
880
+ reportSupervisorError(error);
881
+ }
882
+ }
883
+ }
884
+
885
+ export async function reconcileOnce(
886
+ opts: { now?: Date; getControllerCommands?: typeof getControllerCommands } = {},
887
+ ): Promise<void> {
888
+ if (!isRuntimePoolsEnabled() || !isMultiRuntimeEnabled() || inFlight) return;
889
+ tickAbort = new AbortController();
890
+ inFlight = reconcilePools(
891
+ opts.now ?? new Date(),
892
+ opts.getControllerCommands ?? getControllerCommands,
893
+ tickAbort.signal,
894
+ );
895
+ try {
896
+ await inFlight;
897
+ } catch (error) {
898
+ reportSupervisorError(error);
899
+ } finally {
900
+ inFlight = null;
901
+ tickAbort = null;
902
+ }
903
+ }
904
+
905
+ const onTaskCreated = (data: unknown): void => {
906
+ const taskId = (data as { taskId?: unknown } | null)?.taskId;
907
+ if (typeof taskId !== "string" || !timer || !isRuntimePoolsEnabled() || !isMultiRuntimeEnabled())
908
+ return;
909
+ void (async () => {
910
+ const task = await getTaskById(taskId);
911
+ const agentId =
912
+ task?.status === "offered"
913
+ ? task.offeredTo
914
+ : task?.status === "pending"
915
+ ? task.agentId
916
+ : null;
917
+ if (!agentId || !(await getPoolForAgent(agentId)) || !timer) return;
918
+ if (kickTimer) clearTimeout(kickTimer);
919
+ kickTimer = setTimeout(() => {
920
+ kickTimer = null;
921
+ void reconcileOnce().catch(reportSupervisorError);
922
+ }, 250);
923
+ kickTimer.unref?.();
924
+ })().catch(reportSupervisorError);
925
+ };
926
+
927
+ export function startRuntimeSupervisor(
928
+ intervalMs = Number(process.env.RUNTIME_SUPERVISOR_INTERVAL_MS) || 5000,
929
+ ): void {
930
+ if (timer || !isRuntimePoolsEnabled()) return;
931
+ if (!isMultiRuntimeEnabled()) {
932
+ if (!warnedMultiRuntime) {
933
+ console.error("[runtime-pools] Supervisor requires MULTI_RUNTIME_ENABLED=true.");
934
+ warnedMultiRuntime = true;
935
+ }
936
+ return;
937
+ }
938
+ workflowEventBus.on("task.created", onTaskCreated);
939
+ startRuntimeRecovery();
940
+ timer = setInterval(
941
+ () => {
942
+ void reconcileOnce().catch(reportSupervisorError);
943
+ },
944
+ Math.max(1, intervalMs),
945
+ );
946
+ timer.unref?.();
947
+ }
948
+
949
+ export async function stopRuntimeSupervisor(timeoutMs = 10_000): Promise<void> {
950
+ if (timer) clearInterval(timer);
951
+ if (kickTimer) clearTimeout(kickTimer);
952
+ timer = null;
953
+ kickTimer = null;
954
+ workflowEventBus.off("task.created", onTaskCreated);
955
+ stopRuntimeRecovery();
956
+ tickAbort?.abort();
957
+ stopE2bImageBuilds();
958
+ let timeout: ReturnType<typeof setTimeout> | undefined;
959
+ try {
960
+ if (inFlight) {
961
+ await Promise.race([
962
+ inFlight.catch(reportSupervisorError),
963
+ new Promise<void>((resolve) => {
964
+ timeout = setTimeout(
965
+ () => {
966
+ console.error("[runtime-pools] Supervisor shutdown timed out.");
967
+ resolve();
968
+ },
969
+ Math.min(10_000, Math.max(0, timeoutMs)),
970
+ );
971
+ }),
972
+ ]);
973
+ }
974
+ } catch (error) {
975
+ reportSupervisorError(error);
976
+ } finally {
977
+ if (timeout) clearTimeout(timeout);
978
+ clearRuntimeOwnerIdCache();
979
+ orphanFirstSeen.clear();
980
+ }
981
+ }