@desplega.ai/agent-swarm 2.5.3 → 2.6.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 (349) hide show
  1. package/README.md +2 -2
  2. package/dist/{acp-adapter-gr2fcyyj.js → acp-adapter-dgfakfpw.js} +9 -9
  3. package/dist/{actions-wv766ghf.js → actions-fbqma8aa.js} +21 -21
  4. package/dist/{amp-adapter-speb906t.js → amp-adapter-en0jy6hs.js} +4 -4
  5. package/dist/{app-ag29n14f.js → app-k8a5cgpp.js} +3 -3
  6. package/dist/{assistant-3x8bz3eg.js → assistant-a16433gr.js} +30 -26
  7. package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-6c1sjfnr.js} +1 -1
  8. package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-f4nn561n.js} +1 -1
  9. package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
  10. package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-9bmkspyw.js} +4 -4
  11. package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-nys1kjxa.js} +9 -9
  12. package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-55207973.js} +3 -3
  13. package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-3cm35gey.js} +11 -4
  14. package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-nw5yd8cp.js} +9313 -9260
  15. package/dist/{cli-210xd1f4.js → cli-03gg9pd1.js} +4 -4
  16. package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
  17. package/dist/cli-1tem6ns9.js +894 -0
  18. package/dist/{cli-25j01dpj.js → cli-24j1jeeb.js} +3 -3
  19. package/dist/{cli-ta8bd7th.js → cli-2sjhpsns.js} +2 -2
  20. package/dist/{cli-p859hcc8.js → cli-39aet655.js} +2 -2
  21. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  22. package/dist/{cli-e2ar4bp1.js → cli-3aqd4jaa.js} +121 -47
  23. package/dist/{cli-jjznk3f7.js → cli-3fwqy71j.js} +69 -35
  24. package/dist/{cli-p9rafdrb.js → cli-3gfvmzht.js} +7 -7
  25. package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
  26. package/dist/{cli-aecdcr77.js → cli-4hxrf0p4.js} +228 -74
  27. package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
  28. package/dist/{cli-qzme5949.js → cli-5rj83bam.js} +284 -202
  29. package/dist/{cli-fsdprwrg.js → cli-5sckpqcx.js} +2 -2
  30. package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
  31. package/dist/{cli-3ngdk5te.js → cli-6y51ryva.js} +2 -2
  32. package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
  33. package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
  34. package/dist/{cli-gx4rm0qx.js → cli-7enjwya9.js} +1 -1
  35. package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
  36. package/dist/{cli-avdvgcay.js → cli-9abdgn5t.js} +28 -9
  37. package/dist/{cli-99xgw26r.js → cli-a2rr0fmg.js} +1 -1
  38. package/dist/{cli-e5bxnjps.js → cli-a34pr3w1.js} +7 -7
  39. package/dist/{cli-502eqv93.js → cli-abkm71vr.js} +1 -1
  40. package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
  41. package/dist/{cli-n45mr7zh.js → cli-aj8djyqt.js} +5 -5
  42. package/dist/{cli-1fc85mvt.js → cli-art7c1zh.js} +5 -5
  43. package/dist/{cli-40jsnrjm.js → cli-az25fn5f.js} +10 -8
  44. package/dist/{cli-9s05422w.js → cli-bqztfyve.js} +1 -1
  45. package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
  46. package/dist/{cli-svrj71pg.js → cli-c2habvsf.js} +12 -12
  47. package/dist/{cli-s49hpn37.js → cli-ca6banwz.js} +85 -19
  48. package/dist/{cli-hhxjnjzs.js → cli-cz37jqss.js} +12 -10
  49. package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
  50. package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
  51. package/dist/{cli-z21whrrg.js → cli-emhjcj2y.js} +17 -17
  52. package/dist/{cli-e8jyq7d8.js → cli-esakngh8.js} +7 -3
  53. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  54. package/dist/{cli-35gtk1p6.js → cli-ffvsj2qw.js} +44 -13
  55. package/dist/{cli-ce0ak9ms.js → cli-g20jjwt4.js} +26 -26
  56. package/dist/{cli-kxawjrxg.js → cli-g8ka5j4j.js} +772 -194
  57. package/dist/{cli-vwn81jbs.js → cli-gf95q3bm.js} +5 -5
  58. package/dist/{cli-s53vp709.js → cli-gnsb1zxa.js} +2 -2
  59. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  60. package/dist/{cli-gpv60278.js → cli-hfeb68z4.js} +7 -7
  61. package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
  62. package/dist/{cli-ewm3pwbq.js → cli-k21gx5ht.js} +46 -23
  63. package/dist/{cli-egjggehm.js → cli-kf4jree9.js} +2 -2
  64. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  65. package/dist/{cli-p0rzjkya.js → cli-m5a9m0ak.js} +32 -13
  66. package/dist/{cli-fzcn3d87.js → cli-m70bajzr.js} +3 -3
  67. package/dist/{cli-w1zwctj2.js → cli-mtnj4fwa.js} +11 -5
  68. package/dist/{cli-gpsz7g88.js → cli-mvjz5an3.js} +16 -12
  69. package/dist/{cli-hcpj2xqz.js → cli-n7asmce6.js} +225 -3
  70. package/dist/{cli-y561h5a9.js → cli-nn6vx2mq.js} +5 -5
  71. package/dist/cli-nq677csy.js +140 -0
  72. package/dist/{cli-a5ebc95n.js → cli-pfk92eqz.js} +18 -15
  73. package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
  74. package/dist/{cli-qgf8emrn.js → cli-sqma6vw1.js} +10 -7
  75. package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
  76. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  77. package/dist/{cli-qqzmjxkf.js → cli-v49fkghp.js} +11 -6
  78. package/dist/{cli-x77gen2p.js → cli-w2656ka1.js} +7 -7
  79. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  80. package/dist/{cli-8s5wx3nz.js → cli-zqne1qcj.js} +44 -17
  81. package/dist/cli.js +22 -22
  82. package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-d95q4qsn.js} +7 -7
  83. package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
  84. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  85. package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-gdwd8h0c.js} +8 -8
  86. package/dist/{commands-t04rt2pc.js → commands-jqmgk54b.js} +2 -2
  87. package/dist/{controller-rgna04r2.js → controller-z3jq589f.js} +30 -4
  88. package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-r2za39ef.js} +23 -23
  89. package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-4mr4jgkw.js} +2 -3
  90. package/dist/{db-a4m7haw0.js → db-ppv2tze7.js} +3 -3
  91. package/dist/{db-24ck09xy.js → db-w36wwb5g.js} +4 -2
  92. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  93. package/dist/{dispatcher-nwf9ss0q.js → dispatcher-pv5h77vf.js} +20 -20
  94. package/dist/{docker-nfy2gc8t.js → docker-fdzyewpz.js} +4 -4
  95. package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-d4pdpb3r.js} +2 -3
  96. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  97. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  98. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-4sp4dvmc.js} +1 -1
  99. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  100. package/dist/{execute-0xpz8950.js → execute-jev839y1.js} +1 -1
  101. package/dist/{external-channel-j1e2p34p.js → external-channel-y6nkzhnn.js} +2 -2
  102. package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-t08b9ntm.js} +10 -10
  103. package/dist/handlers-eqsnmv95.js +41 -0
  104. package/dist/{heartbeat-vtesda5d.js → heartbeat-vn31xwdk.js} +23 -23
  105. package/dist/{hook-b2hhj6rd.js → hook-6d0zbvvt.js} +6 -5
  106. package/dist/{hook-8wqaszda.js → hook-cpjs8q51.js} +13 -7
  107. package/dist/{http-mxcqhhcs.js → http-9c6medda.js} +1085 -136
  108. package/dist/{index-spwxjgay.js → index-38t35h0n.js} +20 -20
  109. package/dist/{index-1r8wfpzs.js → index-g6k9tctj.js} +1 -1
  110. package/dist/{index-srg522m6.js → index-k0yt1tvq.js} +20 -20
  111. package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
  112. package/dist/{index-n1vbvta9.js → index-qhjf2htm.js} +29 -24
  113. package/dist/index-yqgbfgaj.js +43 -0
  114. package/dist/{keepalive-dsd8rtgx.js → keepalive-em0117bp.js} +4 -4
  115. package/dist/lead-23e7z59t.js +79 -0
  116. package/dist/{maintenance-jqmqwnet.js → maintenance-kbme3qy9.js} +1 -1
  117. package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
  118. package/dist/{onboard-w4yd7s8t.js → onboard-p6q0vz97.js} +3 -3
  119. package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-esv3k9yn.js} +7 -7
  120. package/dist/{otel-impl-f36byzbd.js → otel-impl-54qhqh1z.js} +2 -2
  121. package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-mymbwhj0.js} +41 -14
  122. package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-yb75wv33.js} +3 -3
  123. package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-1vp2mw4p.js} +2 -2
  124. package/dist/{rbac-roles-682k2mt5.js → rbac-roles-dexhc9x8.js} +1 -1
  125. package/dist/render-v2-tjay5hhf.js +16 -0
  126. package/dist/{resume-2dvq348g.js → resume-8ayf37g5.js} +11 -9
  127. package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
  128. package/dist/{session-access-7f21ekq9.js → session-access-fdhp987s.js} +4 -4
  129. package/dist/session-source-backfill-qgtc86z1.js +110 -0
  130. package/dist/{setup-epffxtbk.js → setup-4rrmqcq3.js} +1 -1
  131. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
  132. package/dist/{work-objects-xz1a5rqk.js → work-objects-p28fgd70.js} +23 -23
  133. package/dist/worker-e3w7vye1.js +79 -0
  134. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  135. package/openapi.json +1323 -16
  136. package/package.json +5 -5
  137. package/plugin/commands/create-pr.md +2 -2
  138. package/plugin/pi-skills/create-pr/SKILL.md +2 -2
  139. package/src/assets/source-key.ts +95 -0
  140. package/src/be/db/api-keys.ts +80 -0
  141. package/src/be/db/approvals.ts +48 -0
  142. package/src/be/db/session-labels.ts +28 -0
  143. package/src/be/db/session-moves.ts +5 -6
  144. package/src/be/db/session-tree.ts +153 -14
  145. package/src/be/db/tasks/read.ts +11 -0
  146. package/src/be/db/tasks/write.ts +21 -6
  147. package/src/be/db/workflow-idempotency.ts +183 -0
  148. package/src/be/db/workflow-runs.ts +500 -7
  149. package/src/be/db-retention.ts +61 -6
  150. package/src/be/db.ts +126 -131
  151. package/src/be/memory/providers/openai-embedding.ts +5 -1
  152. package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
  153. package/src/be/migrations/244_api_key_validation.sql +4 -0
  154. package/src/be/onboarding.ts +4 -0
  155. package/src/be/scripts/typecheck.ts +1 -1
  156. package/src/be/sealed-json.ts +1 -0
  157. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
  158. package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
  159. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  160. package/src/be/session-source-backfill.ts +163 -0
  161. package/src/be/sql-connections.ts +7 -1
  162. package/src/be/swarm-config-guard.ts +3 -0
  163. package/src/be/users.ts +9 -0
  164. package/src/comb/review-batch.ts +28 -1
  165. package/src/comb/templates.ts +7 -2
  166. package/src/commands/claude-managed-setup.ts +7 -1
  167. package/src/commands/controller.ts +33 -0
  168. package/src/commands/credential-refresh.ts +76 -1
  169. package/src/commands/credential-wait.ts +2 -2
  170. package/src/commands/provider-credentials.ts +210 -170
  171. package/src/commands/runner.ts +31 -6
  172. package/src/extensions/apply-task-create.ts +64 -11
  173. package/src/extensions/contract-types.generated.ts +1 -1
  174. package/src/extensions/contract.ts +30 -0
  175. package/src/extensions/dispatcher.ts +4 -0
  176. package/src/extensions/post-bridge.ts +8 -0
  177. package/src/hooks/context-bound-note.ts +196 -0
  178. package/src/hooks/hook.ts +12 -0
  179. package/src/http/all-routes.ts +2 -0
  180. package/src/http/api-keys.ts +53 -0
  181. package/src/http/assets.ts +15 -0
  182. package/src/http/codex-oauth-accounts.ts +191 -0
  183. package/src/http/comb.ts +15 -2
  184. package/src/http/config.ts +49 -17
  185. package/src/http/core.ts +6 -0
  186. package/src/http/fleet-benchmark.ts +5 -2
  187. package/src/http/idempotency-key.ts +117 -0
  188. package/src/http/index.ts +11 -0
  189. package/src/http/integrations.ts +5 -1
  190. package/src/http/kv.ts +8 -3
  191. package/src/http/onboarding.ts +6 -1
  192. package/src/http/pool-keys.ts +354 -0
  193. package/src/http/sessions.ts +37 -12
  194. package/src/http/stats.ts +1 -0
  195. package/src/http/status.ts +26 -12
  196. package/src/http/tasks.ts +3 -0
  197. package/src/http/used-by.ts +7 -2
  198. package/src/http/workflows.ts +207 -33
  199. package/src/integrations/chat-sdk/inbound.ts +27 -0
  200. package/src/kv-reserved-namespaces.ts +19 -0
  201. package/src/prompts/context-bound-note.ts +48 -0
  202. package/src/prompts/session-templates.ts +25 -1
  203. package/src/providers/acp-targets.ts +1 -1
  204. package/src/providers/amp-adapter.ts +30 -8
  205. package/src/providers/claude-adapter.ts +34 -5
  206. package/src/providers/claude-managed-adapter.ts +42 -9
  207. package/src/providers/claude-sdk-session.ts +10 -0
  208. package/src/providers/claude-session-events.ts +13 -0
  209. package/src/providers/codex-adapter.ts +83 -11
  210. package/src/providers/opencode-adapter.ts +18 -0
  211. package/src/providers/pi-mono-adapter.ts +35 -0
  212. package/src/runtime-providers/docker.ts +6 -2
  213. package/src/scheduler/scheduler.ts +3 -2
  214. package/src/scripts-runtime/swarm-sdk.ts +7 -1
  215. package/src/scripts-runtime/types/stdlib.d.ts +1 -1
  216. package/src/scripts-runtime/types/swarm-extension.d.ts +52 -8
  217. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -1
  218. package/src/slack/assistant.ts +7 -0
  219. package/src/slack/channel-labels.ts +74 -0
  220. package/src/slack/handlers.ts +8 -0
  221. package/src/slack/inbound-files.ts +5 -2
  222. package/src/slack/thread-buffer.ts +4 -0
  223. package/src/tasks/sibling-awareness.ts +12 -3
  224. package/src/telemetry-context.ts +5 -6
  225. package/src/telemetry-identity.ts +3 -3
  226. package/src/tests/amp-adapter.test.ts +97 -0
  227. package/src/tests/api-keys-validation.test.ts +170 -0
  228. package/src/tests/approval-requests.test.ts +10 -7
  229. package/src/tests/approval-sweeps.test.ts +12 -6
  230. package/src/tests/base-prompt.test.ts +1 -1
  231. package/src/tests/claude-hook-output.test.ts +79 -1
  232. package/src/tests/claude-managed-adapter.test.ts +321 -0
  233. package/src/tests/claude-routes-e2e.test.ts +7 -2
  234. package/src/tests/codex-adapter.test.ts +163 -0
  235. package/src/tests/codex-credential-pools.test.ts +120 -0
  236. package/src/tests/codex-gateway-route.test.ts +165 -0
  237. package/src/tests/codex-oauth-accounts.test.ts +357 -0
  238. package/src/tests/comb-review-batch.test.ts +63 -2
  239. package/src/tests/config-pool-keys.test.ts +533 -0
  240. package/src/tests/context-bound-note.test.ts +269 -0
  241. package/src/tests/credential-check.test.ts +290 -2
  242. package/src/tests/credential-refresh.test.ts +172 -1
  243. package/src/tests/db-retention.test.ts +9 -3
  244. package/src/tests/db-task-read-contract.test.ts +1 -0
  245. package/src/tests/db-task-write-contract.test.ts +1 -3
  246. package/src/tests/docs-index-footprint.test.js +3 -3
  247. package/src/tests/extensions-pre-task.test.ts +325 -0
  248. package/src/tests/fetch-resolved-env.test.ts +17 -0
  249. package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
  250. package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
  251. package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
  252. package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
  253. package/src/tests/fixtures/fake-amp.ts +22 -8
  254. package/src/tests/fixtures/held-template-render.ts +42 -0
  255. package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
  256. package/src/tests/fleet-benchmark.test.ts +8 -0
  257. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  258. package/src/tests/integrations-http.test.ts +32 -2
  259. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  260. package/src/tests/internal-ai/credentials.test.ts +23 -0
  261. package/src/tests/kv-http.test.ts +32 -0
  262. package/src/tests/kv-tool.test.ts +25 -0
  263. package/src/tests/multi-runtime-registration.test.ts +12 -12
  264. package/src/tests/onboarding.test.ts +108 -1
  265. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  266. package/src/tests/openapi-route-headers.test.ts +2 -0
  267. package/src/tests/opencode-adapter.test.ts +134 -0
  268. package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
  269. package/src/tests/org-website.test.ts +36 -0
  270. package/src/tests/pi-mono-adapter.test.ts +105 -0
  271. package/src/tests/prompt-template-session.test.ts +4 -3
  272. package/src/tests/provider-key-probes.test.ts +181 -0
  273. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  274. package/src/tests/rbac-e2e-helpers.ts +36 -11
  275. package/src/tests/runner-push-delivery.test.ts +25 -0
  276. package/src/tests/runtime-controller-liveness.test.ts +133 -0
  277. package/src/tests/runtime-providers-docker.test.ts +2 -0
  278. package/src/tests/script-connections.test.ts +5 -0
  279. package/src/tests/script-run-config-scope.test.ts +91 -3
  280. package/src/tests/script-runs-retention.test.ts +212 -0
  281. package/src/tests/sdk-allowlist.test.ts +39 -0
  282. package/src/tests/session-logs.test.ts +70 -0
  283. package/src/tests/session-source-backfill.test.ts +429 -0
  284. package/src/tests/session-source-keys.test.ts +236 -0
  285. package/src/tests/session-source-labels.test.ts +419 -0
  286. package/src/tests/sessions-detail-batch.test.ts +2 -0
  287. package/src/tests/sessions-move.test.ts +131 -0
  288. package/src/tests/sessions-tree.test.ts +336 -2
  289. package/src/tests/slack-inbound-files.test.ts +103 -23
  290. package/src/tests/status.test.ts +101 -18
  291. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  292. package/src/tests/test-net.ts +31 -0
  293. package/src/tests/transcription.test.ts +8 -0
  294. package/src/tests/used-by-route.test.ts +11 -0
  295. package/src/tests/workflow-cascade-cancel.test.ts +1341 -0
  296. package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
  297. package/src/tests/workflow-http-v2.test.ts +131 -0
  298. package/src/tests/workflow-idempotency.test.ts +915 -0
  299. package/src/tests/workflow-step-order.test.ts +92 -0
  300. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  301. package/src/tools/get-tasks.ts +8 -0
  302. package/src/tools/kv/kv-read-auth.ts +4 -2
  303. package/src/tools/task-action.ts +1 -1
  304. package/src/tools/workflows/cancel-workflow-run.ts +1 -1
  305. package/src/tools/workflows/get-workflow-run.ts +14 -4
  306. package/src/types.ts +24 -0
  307. package/src/utils/company-profile.ts +51 -1
  308. package/src/utils/context-window.ts +8 -6
  309. package/src/utils/credentials.ts +5 -2
  310. package/src/utils/internal-ai/complete-structured.ts +5 -0
  311. package/src/utils/internal-ai/credentials.ts +4 -3
  312. package/src/utils/provider-key-probes.ts +328 -0
  313. package/src/utils/transcription.ts +4 -1
  314. package/src/workflows/approval-cancel.ts +15 -13
  315. package/src/workflows/approval-notifications.ts +5 -1
  316. package/src/workflows/checkpoint.ts +6 -3
  317. package/src/workflows/engine.ts +30 -8
  318. package/src/workflows/executors/foreach.ts +2 -2
  319. package/src/workflows/executors/sub-workflow.ts +8 -1
  320. package/src/workflows/foreach-driver.ts +10 -3
  321. package/src/workflows/foreach-join.ts +29 -6
  322. package/src/workflows/recovery.ts +6 -1
  323. package/src/workflows/resume.ts +168 -56
  324. package/src/workflows/retry-poller.ts +49 -3
  325. package/src/workflows/task-step-routing.ts +29 -7
  326. package/templates/ai-toolbox.manifest.json +8 -8
  327. package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
  328. package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
  329. package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
  330. package/templates/skills/agent-browser/SKILL.md +4 -4
  331. package/templates/skills/agent-browser/content.md +4 -4
  332. package/templates/skills/code-quality/config.json +1 -1
  333. package/templates/skills/code-quality/content.md +2 -2
  334. package/templates/skills/delegate-work/content.md +29 -25
  335. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
  336. package/templates/skills/qa/content.md +2 -2
  337. package/templates/skills/researching/content.md +4 -0
  338. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  339. package/templates/skills/swarm-scripts/content.md +1 -0
  340. package/templates/skills/work-on-task/SKILL.md +1 -1
  341. package/templates/skills/work-on-task/content.md +1 -1
  342. package/dist/cli-7421jzsw.js +0 -84
  343. package/dist/cli-e45e4m6b.js +0 -413
  344. package/dist/cli-zw88zjba.js +0 -307
  345. package/dist/handlers-19xr6dnz.js +0 -41
  346. package/dist/index-kw2mmat7.js +0 -43
  347. package/dist/lead-zyerk1x2.js +0 -79
  348. package/dist/render-v2-hgytz8nn.js +0 -12
  349. package/dist/worker-42t8emjt.js +0 -79
@@ -0,0 +1,181 @@
1
+ import { afterEach, describe, expect, mock, test } from "bun:test";
2
+ import { checkProviderPoolKey } from "../utils/provider-key-probes";
3
+
4
+ describe("API pool key probes", () => {
5
+ const realFetch = globalThis.fetch;
6
+ const originalNodeEnv = process.env.NODE_ENV;
7
+ const originalPrivateUrls = process.env.ALLOW_PRIVATE_NETWORK_URLS;
8
+
9
+ afterEach(() => {
10
+ globalThis.fetch = realFetch;
11
+ if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
12
+ else process.env.NODE_ENV = originalNodeEnv;
13
+ if (originalPrivateUrls === undefined) delete process.env.ALLOW_PRIVATE_NETWORK_URLS;
14
+ else process.env.ALLOW_PRIVATE_NETWORK_URLS = originalPrivateUrls;
15
+ });
16
+
17
+ test("uses the supplied OpenRouter URL and forbids redirects", async () => {
18
+ const calls: Array<{ url: string; init?: RequestInit }> = [];
19
+ globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
20
+ calls.push({ url: String(url), init });
21
+ return new Response("{}", { status: calls.length === 1 ? 404 : 200 });
22
+ }) as typeof fetch;
23
+ expect(
24
+ await checkProviderPoolKey("OPENROUTER_API_KEY", "fake-abcde", {
25
+ OPENROUTER_BASE_URL: "https://gateway.example/v1/",
26
+ }),
27
+ ).toMatchObject({ ok: true });
28
+ expect(calls.map((call) => call.url)).toEqual([
29
+ "https://gateway.example/v1/key",
30
+ "https://gateway.example/v1/models",
31
+ ]);
32
+ expect(calls.every((call) => call.init?.redirect === "manual")).toBe(true);
33
+ });
34
+
35
+ test("rejects a private registry URL before sending credentials", async () => {
36
+ process.env.NODE_ENV = "production";
37
+ delete process.env.ALLOW_PRIVATE_NETWORK_URLS;
38
+ const fetch = mock(async () => new Response("{}"));
39
+ globalThis.fetch = fetch as unknown as typeof globalThis.fetch;
40
+ const result = await checkProviderPoolKey("NAN_API_KEY", "fake-abcde", {
41
+ NAN_BASE_URL: "https://127.0.0.1/v1",
42
+ });
43
+ expect(result.ok).toBe(false);
44
+ expect(result.httpStatus).toBeUndefined();
45
+ expect(fetch).not.toHaveBeenCalled();
46
+ });
47
+
48
+ test("supports registry aliases and configured URLs", async () => {
49
+ const fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
50
+ expect(String(url)).toBe("https://gateway.example/v1/models");
51
+ expect(init?.headers).toEqual({ Authorization: "Bearer fake-abcde" });
52
+ expect(init?.redirect).toBe("manual");
53
+ return new Response("Unauthorized", { status: 401 });
54
+ });
55
+ globalThis.fetch = fetch as typeof globalThis.fetch;
56
+ expect(
57
+ await checkProviderPoolKey("HELMCODE_API_NAN", "fake-abcde", {
58
+ NAN_BASE_URL: "https://gateway.example/v1",
59
+ }),
60
+ ).toMatchObject({ ok: false, httpStatus: 401 });
61
+ });
62
+
63
+ test.each([
64
+ [200, "{}", true, false],
65
+ [403, '{"error":{"type":"permission_error"}}', true, false],
66
+ [403, '{"error":{"type":"other"}}', false, false],
67
+ [401, "Unauthorized", false, false],
68
+ [429, "Throttled", true, true],
69
+ [503, "Unavailable", true, true],
70
+ ])("preserves Claude OAuth semantics for HTTP %i", async (status, body, ok, skipped) => {
71
+ globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
72
+ expect(String(url)).toBe("https://api.anthropic.com/api/oauth/profile");
73
+ expect(init?.redirect).toBe("manual");
74
+ expect(init?.signal).toBeInstanceOf(AbortSignal);
75
+ return new Response(body, { status });
76
+ }) as typeof fetch;
77
+ const result = await checkProviderPoolKey("CLAUDE_CODE_OAUTH_TOKEN", "fake-abcde", {});
78
+ expect(result.ok).toBe(ok);
79
+ expect(result.skipped === true).toBe(skipped);
80
+ if (!ok) expect(result.httpStatus).toBe(status);
81
+ });
82
+
83
+ test("checks an Anthropic key at its gateway, keeping the URL path prefix", async () => {
84
+ const calls: Array<{ url: string; init?: RequestInit }> = [];
85
+ globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
86
+ calls.push({ url: String(url), init });
87
+ return new Response("Unauthorized", { status: 401 });
88
+ }) as unknown as typeof fetch;
89
+ const result = await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
90
+ ANTHROPIC_BASE_URL: "https://gateway.example/api",
91
+ });
92
+ expect(result).toMatchObject({ ok: false, httpStatus: 401 });
93
+ expect(calls.map((call) => call.url)).toEqual(["https://gateway.example/api/v1/models"]);
94
+ expect(new Headers(calls[0]?.init?.headers).get("x-api-key")).toBe("fake-abcde");
95
+ expect(calls[0]?.init?.redirect).toBe("manual");
96
+
97
+ globalThis.fetch = mock(async () => new Response("{}")) as unknown as typeof fetch;
98
+ expect(
99
+ await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
100
+ ANTHROPIC_BASE_URL: "https://gateway.example/api",
101
+ }),
102
+ ).toMatchObject({ ok: true });
103
+ });
104
+
105
+ test("a gateway that lists no models leaves the Anthropic key unchecked", async () => {
106
+ globalThis.fetch = mock(
107
+ async () => new Response("Not found", { status: 404 }),
108
+ ) as unknown as typeof fetch;
109
+ expect(
110
+ await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
111
+ ANTHROPIC_BASE_URL: "https://gateway.example",
112
+ }),
113
+ ).toMatchObject({ ok: true, skipped: true });
114
+ });
115
+
116
+ test("an Anthropic key stays on api.anthropic.com when the gateway uses ANTHROPIC_AUTH_TOKEN", async () => {
117
+ const urls: string[] = [];
118
+ globalThis.fetch = mock(async (url: string | URL | Request) => {
119
+ urls.push(String(url));
120
+ return new Response("{}");
121
+ }) as unknown as typeof fetch;
122
+ expect(
123
+ await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
124
+ ANTHROPIC_BASE_URL: "https://gateway.example",
125
+ ANTHROPIC_AUTH_TOKEN: "gateway-token",
126
+ }),
127
+ ).toMatchObject({ ok: true });
128
+ expect(urls).toEqual(["https://api.anthropic.com/v1/models"]);
129
+ });
130
+
131
+ test("a private Anthropic gateway gets no credentials and no rejection", async () => {
132
+ process.env.NODE_ENV = "production";
133
+ delete process.env.ALLOW_PRIVATE_NETWORK_URLS;
134
+ const fetch = mock(async () => new Response("{}"));
135
+ globalThis.fetch = fetch as unknown as typeof globalThis.fetch;
136
+ const result = await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
137
+ ANTHROPIC_BASE_URL: "http://127.0.0.1:4000",
138
+ });
139
+ expect(result.ok).toBe(false);
140
+ expect(result.httpStatus).toBeUndefined();
141
+ expect(fetch).not.toHaveBeenCalled();
142
+ });
143
+
144
+ test("skips keys without an API-safe probe", async () => {
145
+ const fetch = mock(async () => new Response("{}"));
146
+ globalThis.fetch = fetch as unknown as typeof globalThis.fetch;
147
+ for (const keyType of ["DEVIN_API_KEY", "AMP_API_KEY", "CURSOR_API_KEY"]) {
148
+ expect(await checkProviderPoolKey(keyType, "fake-abcde", {})).toMatchObject({
149
+ ok: true,
150
+ skipped: true,
151
+ });
152
+ }
153
+ expect(fetch).not.toHaveBeenCalled();
154
+ });
155
+
156
+ test("reports network errors without exposing the submitted key", async () => {
157
+ globalThis.fetch = mock(async () => {
158
+ throw new Error("Rejected fake-abcde");
159
+ }) as unknown as typeof fetch;
160
+ const result = await checkProviderPoolKey("OPENAI_API_KEY", "fake-abcde", {});
161
+ expect(result.ok).toBe(false);
162
+ expect(result.error).not.toContain("fake-abcde");
163
+ });
164
+
165
+ test.each([
166
+ "ANTHROPIC_API_KEY",
167
+ "OPENAI_API_KEY",
168
+ "OPENROUTER_API_KEY",
169
+ "NAN_API_KEY",
170
+ "CLAUDE_CODE_OAUTH_TOKEN",
171
+ ])("redacts a long echoed %s before truncating the provider error", async (keyType) => {
172
+ const apiKey = "Z".repeat(240);
173
+ globalThis.fetch = mock(
174
+ async () => new Response(`Rejected ${apiKey}`, { status: 403 }),
175
+ ) as unknown as typeof fetch;
176
+ const result = await checkProviderPoolKey(keyType, apiKey, {});
177
+ expect(result).toMatchObject({ ok: false, httpStatus: 403 });
178
+ expect(result.error).toContain("[REDACTED]");
179
+ expect(result.error).not.toContain(apiKey.slice(0, 40));
180
+ });
181
+ });
@@ -1,9 +1,23 @@
1
1
  import { expect, test } from "bun:test";
2
2
  import { closeSync, openSync } from "node:fs";
3
3
  import { mkdtemp, rm } from "node:fs/promises";
4
+ import { createServer } from "node:net";
4
5
  import { tmpdir } from "node:os";
5
6
  import { join } from "node:path";
6
- import { type SwarmServer, waitForListen } from "./rbac-e2e-helpers";
7
+ import {
8
+ makeScratchDir,
9
+ removeScratchDir,
10
+ type SwarmServer,
11
+ spawnSwarmServer,
12
+ waitForListen,
13
+ } from "./rbac-e2e-helpers";
14
+ import {
15
+ getFreePort,
16
+ listenOnFreePort,
17
+ PortInUseError,
18
+ SERVER_BOOT_HOOK_TIMEOUT_MS,
19
+ startOnFreePort,
20
+ } from "./test-net";
7
21
 
8
22
  test("waitForListen includes a bounded server log when the process exits early", async () => {
9
23
  const dir = await mkdtemp(join(tmpdir(), "rbac-e2e-helper-"));
@@ -51,3 +65,76 @@ test("waitForListen includes a bounded server log when the process exits early",
51
65
  await rm(dir, { recursive: true, force: true });
52
66
  }
53
67
  });
68
+
69
+ test("startOnFreePort retries only PortInUseError, up to its attempt bound", async () => {
70
+ const ports = [41001, 41002, 41003, 41004];
71
+ const getPort = async () => ports.shift() ?? 0;
72
+ const tried: number[] = [];
73
+
74
+ const port = await startOnFreePort(
75
+ async (p) => {
76
+ tried.push(p);
77
+ if (p === 41001) throw new PortInUseError("taken");
78
+ return p;
79
+ },
80
+ { getPort },
81
+ );
82
+ expect(port).toBe(41002);
83
+ expect(tried).toEqual([41001, 41002]);
84
+
85
+ const other = await startOnFreePort(
86
+ async () => {
87
+ throw new Error("boot crashed");
88
+ },
89
+ { getPort },
90
+ ).catch((error: unknown) => error);
91
+ expect((other as Error).message).toBe("boot crashed");
92
+ expect(ports).toEqual([41004]);
93
+
94
+ let calls = 0;
95
+ const exhausted = await startOnFreePort(
96
+ async () => {
97
+ calls++;
98
+ throw new PortInUseError(`taken ${calls}`);
99
+ },
100
+ { getPort: async () => 0 },
101
+ ).catch((error: unknown) => error);
102
+ expect(exhausted).toBeInstanceOf(PortInUseError);
103
+ expect((exhausted as Error).message).toBe("taken 3");
104
+ expect(calls).toBe(3);
105
+ });
106
+
107
+ test(
108
+ "spawnSwarmServer starts again on a new port when its first port is taken",
109
+ async () => {
110
+ const dir = await makeScratchDir();
111
+ const holder = createServer();
112
+ const heldPort = await listenOnFreePort(holder);
113
+ let server: SwarmServer | undefined;
114
+ try {
115
+ const offered: number[] = [];
116
+ server = await spawnSwarmServer({
117
+ dbPath: join(dir, "test.sqlite"),
118
+ logPath: join(dir, "server.log"),
119
+ getPort: async () => {
120
+ const port = offered.length === 0 ? heldPort : await getFreePort();
121
+ offered.push(port);
122
+ return port;
123
+ },
124
+ });
125
+
126
+ expect(offered).toHaveLength(2);
127
+ expect(offered[0]).toBe(heldPort);
128
+ expect(server.port).toBe(offered[1]);
129
+ expect(server.port).not.toBe(heldPort);
130
+ expect((await fetch(`${server.base}/docs`)).ok).toBe(true);
131
+ const log = await Bun.file(server.logPath).text();
132
+ expect(log).toContain(`Is port ${heldPort} in use?`);
133
+ } finally {
134
+ await server?.stop();
135
+ holder.close();
136
+ await removeScratchDir(dir);
137
+ }
138
+ },
139
+ SERVER_BOOT_HOOK_TIMEOUT_MS * 2,
140
+ );
@@ -14,7 +14,7 @@ import { openSync } from "node:fs";
14
14
  import { mkdtemp, rm } from "node:fs/promises";
15
15
  import { tmpdir } from "node:os";
16
16
  import { dirname, join } from "node:path";
17
- import { getFreePort } from "./test-net";
17
+ import { getFreePort, PORT_IN_USE_LOG, PortInUseError, startOnFreePort } from "./test-net";
18
18
 
19
19
  // Re-exported for the suites that imported it from here before test-net.ts existed.
20
20
  export { getFreePort };
@@ -50,6 +50,9 @@ export async function removeScratchDir(dir: string): Promise<void> {
50
50
  * Spawn `bun src/http.ts` against `dbPath` on a free port. Integrations are
51
51
  * disabled; both API-key env vars are pinned so a developer's shell env can't
52
52
  * leak in (AGENT_SWARM_API_KEY takes precedence in getApiKey()).
53
+ *
54
+ * With waitForListen, a child that exits because another process took its
55
+ * port during boot is started again on a new port (startOnFreePort).
53
56
  */
54
57
  export async function spawnSwarmServer(opts: {
55
58
  dbPath: string;
@@ -57,9 +60,38 @@ export async function spawnSwarmServer(opts: {
57
60
  env?: Record<string, string>;
58
61
  /** Poll /docs until the server accepts requests (default true). */
59
62
  waitForListen?: boolean;
63
+ /** Port source for each attempt (default getFreePort). Tests use it to force a taken port. */
64
+ getPort?: () => Promise<number>;
60
65
  }): Promise<SwarmServer> {
61
- const port = await getFreePort();
62
66
  const logFd = openSync(opts.logPath, "a");
67
+ return startOnFreePort(
68
+ async (port) => {
69
+ const log = Bun.file(opts.logPath);
70
+ const logStart = (await log.exists()) ? log.size : 0;
71
+ const server = launchSwarmServer(opts, port, logFd);
72
+ if (opts.waitForListen === false) return server;
73
+ try {
74
+ await waitForListen(server);
75
+ } catch (error) {
76
+ if (server.proc.exitCode !== null) {
77
+ const attemptLog = await Bun.file(opts.logPath).slice(logStart).text();
78
+ if (PORT_IN_USE_LOG.test(attemptLog)) {
79
+ throw new PortInUseError((error as Error).message, { cause: error });
80
+ }
81
+ }
82
+ throw error;
83
+ }
84
+ return server;
85
+ },
86
+ { getPort: opts.getPort },
87
+ );
88
+ }
89
+
90
+ function launchSwarmServer(
91
+ opts: { dbPath: string; logPath: string; env?: Record<string, string> },
92
+ port: number,
93
+ logFd: number,
94
+ ): SwarmServer {
63
95
  const proc = Bun.spawn(["bun", "src/http.ts"], {
64
96
  cwd: REPO_ROOT,
65
97
  stdout: logFd,
@@ -93,12 +125,10 @@ export async function spawnSwarmServer(opts: {
93
125
  ...opts.env,
94
126
  },
95
127
  });
96
- const base = `http://localhost:${port}`;
97
-
98
- const server: SwarmServer = {
128
+ return {
99
129
  proc,
100
130
  port,
101
- base,
131
+ base: `http://localhost:${port}`,
102
132
  dbPath: opts.dbPath,
103
133
  logPath: opts.logPath,
104
134
  async stop() {
@@ -107,11 +137,6 @@ export async function spawnSwarmServer(opts: {
107
137
  return proc.exitCode;
108
138
  },
109
139
  };
110
-
111
- if (opts.waitForListen !== false) {
112
- await waitForListen(server);
113
- }
114
- return server;
115
140
  }
116
141
 
117
142
  export async function waitForListen(server: SwarmServer, deadlineMs = 90_000): Promise<void> {
@@ -139,6 +139,31 @@ async function nudge(kind: WorkerNudge["kind"], taskId?: string) {
139
139
  expect((await waiting)?.kind).toBe(kind);
140
140
  }
141
141
 
142
+ test("shutdown stops an open poll window before it can claim a resume", async () => {
143
+ let shuttingDown = false;
144
+ // The second poll would claim the resume created during shutdown.
145
+ pollTriggerAfter = 2;
146
+ const sleep = spyOn(Bun, "sleep").mockImplementation(async () => {
147
+ expect(requests.filter((r) => r === "GET /api/poll")).toHaveLength(1);
148
+ shuttingDown = true;
149
+ });
150
+ try {
151
+ const trigger = await pollForTrigger({
152
+ apiUrl,
153
+ apiKey,
154
+ agentId,
155
+ pollInterval: 2,
156
+ pollTimeout: 5000,
157
+ shouldStop: () => shuttingDown,
158
+ });
159
+ expect(shuttingDown).toBe(true);
160
+ expect(trigger).toBeNull();
161
+ expect(requests.filter((r) => r === "GET /api/poll")).toHaveLength(1);
162
+ } finally {
163
+ sleep.mockRestore();
164
+ }
165
+ });
166
+
142
167
  async function poll(now: number, mode: DeliveryMode = "push") {
143
168
  const checks = delivery.checks(mode, now);
144
169
  const schedule = delivery.schedule({
@@ -0,0 +1,133 @@
1
+ import { afterEach, expect, spyOn, test } from "bun:test";
2
+ import { createHash } from "node:crypto";
3
+ import { createServer as createHttpServer } from "node:http";
4
+ import { createServer as createNetServer, type Socket } from "node:net";
5
+ import { WebSocketServer } from "ws";
6
+ import { startControllerClient } from "../commands/controller";
7
+ import { CONTROLLER_PROTOCOL_VERSION } from "../runtime-providers/controller-protocol";
8
+ import type { RuntimeProvider } from "../runtime-providers/types";
9
+ import { listenOnFreePort } from "./test-net";
10
+
11
+ const LIVENESS_MS = 300;
12
+ const WELCOME = JSON.stringify({
13
+ type: "welcome",
14
+ controllerId: "controller-1",
15
+ version: CONTROLLER_PROTOCOL_VERSION,
16
+ });
17
+
18
+ const provider = {
19
+ kind: "docker",
20
+ traits: { canProvision: true, ephemeralDisk: true, supportsPause: false },
21
+ checkConfig: () => ({ ready: true, missing: [] }),
22
+ } as unknown as RuntimeProvider;
23
+
24
+ const cleanups: Array<() => void> = [];
25
+ afterEach(() => {
26
+ for (const cleanup of cleanups.splice(0).reverse()) cleanup();
27
+ });
28
+
29
+ // A raw TCP WebSocket peer. Under Bun, `ws` maps to a native shim that always answers
30
+ // pings, so it cannot model a dead link. This peer completes the handshake, sends
31
+ // welcome, and then never answers a frame: what a cut network looks like to the client.
32
+ async function startRawServer(options: { pingEveryMs?: number } = {}) {
33
+ let connections = 0;
34
+ const sockets: Socket[] = [];
35
+ const server = createNetServer((socket) => {
36
+ sockets.push(socket);
37
+ socket.once("data", (data) => {
38
+ const key = /sec-websocket-key: *(\S+)/i.exec(data.toString())?.[1];
39
+ const accept = createHash("sha1")
40
+ .update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
41
+ .digest("base64");
42
+ socket.write(
43
+ `HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`,
44
+ );
45
+ const payload = Buffer.from(WELCOME);
46
+ socket.write(Buffer.concat([Buffer.from([0x81, payload.length]), payload]));
47
+ connections++;
48
+ if (options.pingEveryMs) {
49
+ const timer = setInterval(
50
+ () => socket.write(Buffer.from([0x89, 0x00])),
51
+ options.pingEveryMs,
52
+ );
53
+ socket.once("close", () => clearInterval(timer));
54
+ }
55
+ });
56
+ socket.on("error", () => {});
57
+ });
58
+ const port = await listenOnFreePort(server);
59
+ cleanups.push(() => {
60
+ for (const socket of sockets) socket.destroy();
61
+ server.close();
62
+ });
63
+ return { apiUrl: `http://127.0.0.1:${port}`, connections: () => connections };
64
+ }
65
+
66
+ function startClient(apiUrl: string) {
67
+ const logSpy = spyOn(console, "log").mockImplementation(() => {});
68
+ const client = startControllerClient({
69
+ apiUrl,
70
+ token: "aseph_test-token",
71
+ provider,
72
+ target: {},
73
+ livenessTimeoutMs: LIVENESS_MS,
74
+ });
75
+ cleanups.push(() => {
76
+ client.stop();
77
+ logSpy.mockRestore();
78
+ });
79
+ return { logs: () => logSpy.mock.calls.flat().join("\n") };
80
+ }
81
+
82
+ async function until(check: () => boolean, timeoutMs: number) {
83
+ const deadline = Date.now() + timeoutMs;
84
+ while (!check()) {
85
+ if (Date.now() > deadline) throw new Error("Timed out waiting for condition");
86
+ await Bun.sleep(20);
87
+ }
88
+ }
89
+
90
+ test("a server that goes silent without closing makes the client reconnect", async () => {
91
+ const server = await startRawServer();
92
+ const { logs } = startClient(server.apiUrl);
93
+ await until(() => logs().includes("Connected as controller-1"), 2000);
94
+ const silentAt = Date.now();
95
+ // Detection within the timeout plus one check interval, then the first backoff (at most 1000ms).
96
+ await until(() => server.connections() === 2, LIVENESS_MS * 2 + 1500);
97
+ expect(logs()).toContain(`No traffic for ${LIVENESS_MS}ms`);
98
+ expect(logs()).toContain("Disconnected. Reconnect in");
99
+ expect(Date.now() - silentAt).toBeGreaterThanOrEqual(LIVENESS_MS);
100
+ });
101
+
102
+ test("server ping frames keep an idle controller connected", async () => {
103
+ // Matches the API: it pings on an interval and sends no app message to an idle controller.
104
+ // This peer never answers the client's pings, so only the server pings count.
105
+ const server = await startRawServer({ pingEveryMs: LIVENESS_MS / 3 });
106
+ const { logs } = startClient(server.apiUrl);
107
+ await until(() => logs().includes("Connected as controller-1"), 2000);
108
+ await Bun.sleep(LIVENESS_MS * 4);
109
+ expect(server.connections()).toBe(1);
110
+ expect(logs()).not.toContain("Disconnected");
111
+ });
112
+
113
+ test("pongs to client pings keep an idle controller connected", async () => {
114
+ // A healthy peer that sends nothing on its own still answers pings.
115
+ const http = createHttpServer();
116
+ const wss = new WebSocketServer({ server: http });
117
+ let connections = 0;
118
+ wss.on("connection", (ws) => {
119
+ connections++;
120
+ ws.send(WELCOME);
121
+ });
122
+ const port = await listenOnFreePort(http);
123
+ cleanups.push(() => {
124
+ for (const ws of wss.clients) ws.terminate();
125
+ wss.close();
126
+ http.close();
127
+ });
128
+ const { logs } = startClient(`http://127.0.0.1:${port}`);
129
+ await until(() => logs().includes("Connected as controller-1"), 2000);
130
+ await Bun.sleep(LIVENESS_MS * 4);
131
+ expect(connections).toBe(1);
132
+ expect(logs()).not.toContain("Disconnected");
133
+ });
@@ -310,6 +310,7 @@ describe("Docker runtime provider", () => {
310
310
  await provider.terminate(target, handle);
311
311
  expect(calls[0]?.init.method).toBe("DELETE");
312
312
  expect(calls[0]?.url.searchParams.get("force")).toBe("true");
313
+ expect(calls[0]?.url.searchParams.get("v")).toBe("true");
313
314
  });
314
315
 
315
316
  test("termination succeeds when the container is already absent", async () => {
@@ -376,6 +377,7 @@ describe("Docker runtime provider", () => {
376
377
  "DELETE /v1.47/containers/container-1",
377
378
  ]);
378
379
  expect(calls[2]?.url.searchParams.get("force")).toBe("true");
380
+ expect(calls[2]?.url.searchParams.get("v")).toBe("true");
379
381
  }
380
382
  });
381
383
 
@@ -570,6 +570,9 @@ describe("script connections", () => {
570
570
  markMigrationApplied(database, "151_repair_scheduled_task_required_params.sql");
571
571
  // 152 indexes task routing context absent from this migration-112-only fixture.
572
572
  markMigrationApplied(database, "152_routing_decisions.sql");
573
+ // 153 drops a column, so SQLite revalidates 144's agent_tasks triggers
574
+ // against this fixture's id-only agent_tasks (Bun < 1.4.4 hid the error).
575
+ markMigrationApplied(database, "153_deferred_task_wait_members.sql");
573
576
  // 156 alters inbox_item_state, which this migration-112-only fixture does not create.
574
577
  markMigrationApplied(database, "156_inbox_item_state_read_at.sql");
575
578
  // 157 alters scheduled_tasks and slack_messages, neither of which this
@@ -648,6 +651,8 @@ describe("script connections", () => {
648
651
  markMigrationApplied(database, "240_workflow_run_parent_step_attempt.sql");
649
652
  // 241 indexes skills, which this migration-112-only fixture does not create.
650
653
  markMigrationApplied(database, "241_skills_meta_fts.sql");
654
+ // 244 alters api_key_status, which this migration-112-only fixture does not create.
655
+ markMigrationApplied(database, "244_api_key_validation.sql");
651
656
 
652
657
  const id = crypto.randomUUID();
653
658
  const now = new Date().toISOString();