@desplega.ai/agent-swarm 2.5.4 → 2.7.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 (354) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-ek38cwsv.js → acp-adapter-ma1js5n7.js} +9 -9
  3. package/dist/{actions-92mepq03.js → actions-1vhmtvg9.js} +21 -21
  4. package/dist/{amp-adapter-9gaxg015.js → amp-adapter-sxqqvmdk.js} +4 -4
  5. package/dist/{app-ctn8gsgx.js → app-mwx1d5a9.js} +3 -3
  6. package/dist/{assistant-zyr5kz47.js → assistant-yx1p6g82.js} +30 -26
  7. package/dist/{boot-reembed-w75maga2.js → boot-reembed-5h37a2c2.js} +1 -1
  8. package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-7h78vmvv.js} +2 -1
  9. package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-8mawd4gw.js} +4 -4
  10. package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-ca84dhnn.js} +4 -4
  11. package/dist/{claude-adapter-f0afqnne.js → claude-adapter-wqt4hfkq.js} +9 -9
  12. package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
  13. package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-rqv98sea.js} +10 -3
  14. package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-f29f3b2x.js} +11 -11
  15. package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
  16. package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
  17. package/dist/{cli-g9vd9ece.js → cli-1hhbzkbb.js} +14 -16
  18. package/dist/{cli-qqzmjxkf.js → cli-1m9x8dr5.js} +11 -6
  19. package/dist/{cli-jbnn4vxb.js → cli-2pvzmas1.js} +3 -3
  20. package/dist/{cli-x77gen2p.js → cli-2y7v133v.js} +7 -7
  21. package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
  22. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  23. package/dist/{cli-jb30dzvk.js → cli-48gxjmcz.js} +530 -190
  24. package/dist/{cli-pzatvw94.js → cli-54sj64fc.js} +2 -2
  25. package/dist/{cli-bvpgr9ny.js → cli-60sty27p.js} +5 -5
  26. package/dist/{cli-asq5ydm4.js → cli-6a9qfa8b.js} +411 -69
  27. package/dist/{cli-277c31dw.js → cli-6mgdba42.js} +225 -3
  28. package/dist/{cli-19202kew.js → cli-6srv653c.js} +5 -5
  29. package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
  30. package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
  31. package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
  32. package/dist/{cli-hh5we352.js → cli-86ehn4s5.js} +7 -7
  33. package/dist/{cli-9s05422w.js → cli-8mbc9za9.js} +1 -1
  34. package/dist/{cli-ff1tm9n9.js → cli-8wmj18cz.js} +1 -1
  35. package/dist/{cli-1r5ad7j5.js → cli-9gsm2rzp.js} +12 -12
  36. package/dist/{cli-mz9afxxf.js → cli-9pshx6rd.js} +17 -3
  37. package/dist/{cli-s682zgqz.js → cli-a6ds2epe.js} +1 -1
  38. package/dist/{cli-6a1xg1p7.js → cli-a89zdqz9.js} +2 -2
  39. package/dist/{cli-03vpq45j.js → cli-ay40dnj3.js} +2 -2
  40. package/dist/{cli-tsma7p8v.js → cli-aypd09nf.js} +2 -2
  41. package/dist/{cli-ethgy7ha.js → cli-b3yzyc57.js} +2 -2
  42. package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
  43. package/dist/{cli-mmhj8vd7.js → cli-c668vvcf.js} +11 -2
  44. package/dist/{cli-sbwsxvtd.js → cli-d20qvpr2.js} +2 -2
  45. package/dist/{cli-c952c7ac.js → cli-e1595bgx.js} +3 -3
  46. package/dist/{cli-621ce7kw.js → cli-f12mr20z.js} +3 -3
  47. package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
  48. package/dist/{cli-xzfq4at3.js → cli-f4dbh7a5.js} +16 -12
  49. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  50. package/dist/{cli-5bnsc0e1.js → cli-gsfk58nw.js} +5 -5
  51. package/dist/cli-gt4h8cxw.js +895 -0
  52. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  53. package/dist/{cli-25n3mv5d.js → cli-h5jeg46p.js} +6 -6
  54. package/dist/{cli-gn5af0x5.js → cli-he9gbdx0.js} +62 -12
  55. package/dist/{cli-gbfxp48k.js → cli-j0ppywdr.js} +42 -35
  56. package/dist/{cli-k4tjsdx9.js → cli-j3y0x2n4.js} +277 -199
  57. package/dist/{cli-7p4jxgtt.js → cli-j9bpmykh.js} +75 -27
  58. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  59. package/dist/{cli-wx2nj19m.js → cli-kv7sbc2c.js} +6 -6
  60. package/dist/{cli-abs8ndtr.js → cli-m30k9777.js} +26 -26
  61. package/dist/{cli-cz5nmdxt.js → cli-m5e802r7.js} +2 -2
  62. package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
  63. package/dist/{cli-xtrz6bzs.js → cli-ptfq202x.js} +5 -3
  64. package/dist/{cli-6n8axjv1.js → cli-qj442fws.js} +2 -2
  65. package/dist/{cli-c61mbdds.js → cli-rd5m70gj.js} +4 -4
  66. package/dist/{cli-eftzyh9y.js → cli-s09j9vp5.js} +2 -2
  67. package/dist/{cli-m4ecdxvz.js → cli-s0mgx214.js} +7 -7
  68. package/dist/{cli-wghjkr0e.js → cli-sgqm2582.js} +10 -7
  69. package/dist/{cli-2655k16q.js → cli-sjctjr48.js} +2 -2
  70. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  71. package/dist/{cli-x0f2v69c.js → cli-twctnkng.js} +12 -7
  72. package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
  73. package/dist/{cli-gx4rm0qx.js → cli-vz78s5wm.js} +1 -1
  74. package/dist/{cli-gwhjmh8g.js → cli-wqr7cea4.js} +5 -5
  75. package/dist/{cli-e42vss7b.js → cli-x37r7qwf.js} +16 -8
  76. package/dist/{cli-4b2z6n0m.js → cli-yd2nreqw.js} +17 -8
  77. package/dist/{cli-64mh9mf3.js → cli-ymjpkxgn.js} +69 -17
  78. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  79. package/dist/{cli-9j129brq.js → cli-yxbsmx56.js} +6 -6
  80. package/dist/cli.js +22 -20
  81. package/dist/{codex-adapter-wch12q99.js → codex-adapter-zk59x2zh.js} +7 -7
  82. package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
  83. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  84. package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-kh6s4j3n.js} +8 -8
  85. package/dist/{commands-9849dkj6.js → commands-68et1qv2.js} +2 -2
  86. package/dist/{controller-ja0sd95p.js → controller-fyrbnzg4.js} +4 -4
  87. package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-nep9h2xv.js} +23 -23
  88. package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-yss3pg7t.js} +2 -2
  89. package/dist/{db-w759qmx0.js → db-28gjyvxj.js} +4 -2
  90. package/dist/{db-47vj40dy.js → db-8ftwvzba.js} +3 -3
  91. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  92. package/dist/{dispatcher-7b8e4z2j.js → dispatcher-hayexmkr.js} +20 -20
  93. package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
  94. package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-46wawff9.js} +2 -2
  95. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  96. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  97. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-8d1e0h4m.js} +1 -1
  98. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  99. package/dist/{execute-0xpz8950.js → execute-cjrfxfnm.js} +1 -1
  100. package/dist/{external-channel-e2s028da.js → external-channel-xbmxcg09.js} +2 -2
  101. package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-ag4wks2a.js} +10 -10
  102. package/dist/handlers-tzmevqb5.js +41 -0
  103. package/dist/{heartbeat-w3mntghj.js → heartbeat-psfvcgwp.js} +23 -23
  104. package/dist/{hook-g7kvv6x9.js → hook-8pz04697.js} +6 -6
  105. package/dist/{hook-3jjywnpb.js → hook-cyq4mryn.js} +8 -8
  106. package/dist/{http-tkp7d4kb.js → http-haj1nd6k.js} +921 -169
  107. package/dist/{index-q7m26kvs.js → index-jwt2bj6f.js} +265 -97
  108. package/dist/{index-3n8m9ea2.js → index-q52f47pe.js} +1 -1
  109. package/dist/{index-31ceyf42.js → index-syvhnvtw.js} +1 -1
  110. package/dist/index-y1ajx04a.js +43 -0
  111. package/dist/{index-1ef1myq2.js → index-z5h22pes.js} +20 -20
  112. package/dist/{index-xdaahthb.js → index-zyq8ct52.js} +20 -20
  113. package/dist/{keepalive-v4ta8wgg.js → keepalive-xdvsamvj.js} +4 -4
  114. package/dist/lead-hqg57kdp.js +79 -0
  115. package/dist/{maintenance-84n1ex21.js → maintenance-4y2xzh3a.js} +1 -1
  116. package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-jd4r7zbs.js} +9 -4
  117. package/dist/{onboard-sfmpe4np.js → onboard-18447q9q.js} +6 -6
  118. package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-26b2frdf.js} +7 -7
  119. package/dist/{otel-impl-v6cc0zve.js → otel-impl-f9tgr8yw.js} +2 -2
  120. package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-qn6qbga1.js} +38 -17
  121. package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-dw2tgb53.js} +3 -3
  122. package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-aygm2760.js} +2 -2
  123. package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-s4nwj2m3.js} +1 -1
  124. package/dist/render-v2-yfvdfr0x.js +16 -0
  125. package/dist/{resume-frdtgfa0.js → resume-1bmvjbf3.js} +9 -9
  126. package/dist/{runtime-bt5pkpm1.js → runtime-f7tpzedy.js} +1 -1
  127. package/dist/{session-access-fpez4y5q.js → session-access-r5garej6.js} +4 -4
  128. package/dist/session-source-backfill-424wzv0r.js +110 -0
  129. package/dist/{setup-mq6m3m8q.js → setup-8tygvxkq.js} +1 -1
  130. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-xsw323b3.js} +2 -2
  131. package/dist/{work-objects-kfahhjsq.js → work-objects-ggnghpgf.js} +23 -23
  132. package/dist/worker-sqjd1nqf.js +79 -0
  133. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  134. package/openapi.json +1324 -14
  135. package/package.json +5 -3
  136. package/src/assets/source-key.ts +95 -0
  137. package/src/be/automation-preflight.ts +2 -0
  138. package/src/be/db/api-keys.ts +89 -0
  139. package/src/be/db/session-labels.ts +28 -0
  140. package/src/be/db/session-moves.ts +5 -6
  141. package/src/be/db/session-tree.ts +153 -14
  142. package/src/be/db/tasks/read.ts +11 -0
  143. package/src/be/db/tasks/write.ts +6 -1
  144. package/src/be/db/workflow-runs.ts +219 -6
  145. package/src/be/db-retention.ts +61 -6
  146. package/src/be/db.ts +135 -132
  147. package/src/be/memory/providers/openai-embedding.ts +5 -1
  148. package/src/be/memory/task-learning.ts +155 -0
  149. package/src/be/migrations/244_api_key_validation.sql +4 -0
  150. package/src/be/migrations/245_script_executor.sql +3 -0
  151. package/src/be/oauth-refresh-sweep.ts +2 -1
  152. package/src/be/onboarding.ts +4 -0
  153. package/src/be/scripts/boot-reembed.ts +2 -0
  154. package/src/be/scripts/db.ts +14 -4
  155. package/src/be/scripts/embeddings.ts +2 -0
  156. package/src/be/scripts/run-saved.ts +1 -0
  157. package/src/be/scripts/typecheck.ts +10 -4
  158. package/src/be/seed-skills/index.ts +7 -0
  159. package/src/be/session-source-backfill.ts +163 -0
  160. package/src/be/swarm-config-guard.ts +1 -0
  161. package/src/comb/review-batch.ts +28 -1
  162. package/src/comb/templates.ts +7 -2
  163. package/src/commands/claude-managed-setup.ts +7 -1
  164. package/src/commands/credential-refresh.ts +76 -1
  165. package/src/commands/credential-wait.ts +2 -2
  166. package/src/commands/onboard/compose-generator.ts +5 -3
  167. package/src/commands/provider-credentials.ts +212 -171
  168. package/src/commands/runner.ts +25 -4
  169. package/src/extensions/apply-task-create.ts +8 -1
  170. package/src/extensions/contract-types.generated.ts +1 -1
  171. package/src/extensions/contract.ts +5 -0
  172. package/src/heartbeat/heartbeat.ts +14 -1
  173. package/src/heartbeat/script-runner-probe.ts +47 -0
  174. package/src/http/all-routes.ts +2 -0
  175. package/src/http/api-keys.ts +77 -9
  176. package/src/http/assets.ts +15 -0
  177. package/src/http/codex-oauth-accounts.ts +191 -0
  178. package/src/http/comb.ts +15 -2
  179. package/src/http/config.ts +1 -1
  180. package/src/http/core.ts +6 -0
  181. package/src/http/fleet-benchmark.ts +5 -2
  182. package/src/http/idempotency-key.ts +1 -1
  183. package/src/http/index.ts +14 -3
  184. package/src/http/integrations.ts +5 -1
  185. package/src/http/kv.ts +8 -3
  186. package/src/http/onboarding.ts +6 -1
  187. package/src/http/pages-public.ts +38 -5
  188. package/src/http/pool-keys.ts +354 -0
  189. package/src/http/scripts.ts +27 -0
  190. package/src/http/sessions.ts +37 -12
  191. package/src/http/stats.ts +1 -0
  192. package/src/http/status.ts +26 -12
  193. package/src/http/tasks.ts +3 -0
  194. package/src/http/used-by.ts +7 -2
  195. package/src/http/workflows.ts +88 -9
  196. package/src/http/x.ts +5 -0
  197. package/src/integrations/chat-sdk/inbound.ts +27 -0
  198. package/src/kv-reserved-namespaces.ts +19 -0
  199. package/src/prompts/base-prompt.ts +21 -1
  200. package/src/prompts/session-templates.ts +17 -3
  201. package/src/providers/acp-targets.ts +1 -1
  202. package/src/providers/amp-adapter.ts +1 -1
  203. package/src/providers/claude-adapter.ts +5 -1
  204. package/src/providers/claude-managed-adapter.ts +16 -9
  205. package/src/providers/codex-adapter.ts +71 -11
  206. package/src/providers/dsh-adapter.ts +1 -1
  207. package/src/providers/pi-mono-adapter.ts +41 -11
  208. package/src/scheduler/scheduler.ts +5 -3
  209. package/src/scripts-runtime/eval-harness.ts +8 -1
  210. package/src/scripts-runtime/executors/native.ts +22 -16
  211. package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
  212. package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
  213. package/src/scripts-runtime/executors/quickjs.ts +7 -0
  214. package/src/scripts-runtime/executors/registry.ts +32 -0
  215. package/src/scripts-runtime/executors/types.ts +7 -0
  216. package/src/scripts-runtime/host-validated-args.ts +133 -0
  217. package/src/scripts-runtime/loader.ts +30 -3
  218. package/src/scripts-runtime/swarm-sdk.ts +16 -3
  219. package/src/scripts-runtime/types/stdlib.d.ts +10 -2
  220. package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
  221. package/src/scripts-runtime/types/swarm-sdk.d.ts +10 -2
  222. package/src/slack/assistant.ts +7 -0
  223. package/src/slack/channel-labels.ts +74 -0
  224. package/src/slack/handlers.ts +8 -0
  225. package/src/slack/inbound-files.ts +5 -2
  226. package/src/slack/thread-buffer.ts +4 -0
  227. package/src/tasks/sibling-awareness.ts +12 -3
  228. package/src/telemetry-context.ts +5 -6
  229. package/src/telemetry-identity.ts +3 -3
  230. package/src/tests/api-key-status-by-suffix.test.ts +242 -0
  231. package/src/tests/api-keys-validation.test.ts +170 -0
  232. package/src/tests/base-prompt.test.ts +70 -2
  233. package/src/tests/claude-managed-adapter.test.ts +46 -0
  234. package/src/tests/claude-routes-e2e.test.ts +7 -2
  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/community-linear-delivery-loop.test.ts +2099 -0
  240. package/src/tests/config-pool-keys.test.ts +533 -0
  241. package/src/tests/credential-check.test.ts +294 -4
  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/docs-index.test.ts +135 -0
  248. package/src/tests/dsh-adapter.test.ts +1 -1
  249. package/src/tests/extensions-pre-task.test.ts +53 -1
  250. package/src/tests/fetch-resolved-env.test.ts +16 -2
  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/fleet-benchmark.test.ts +8 -0
  254. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  255. package/src/tests/heartbeat-checklist.test.ts +89 -0
  256. package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
  257. package/src/tests/integrations-http.test.ts +32 -2
  258. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  259. package/src/tests/internal-ai/credentials.test.ts +23 -0
  260. package/src/tests/kv-http.test.ts +32 -0
  261. package/src/tests/kv-tool.test.ts +25 -0
  262. package/src/tests/multi-runtime-registration.test.ts +12 -12
  263. package/src/tests/oauth-refresh-sweep.test.ts +34 -1
  264. package/src/tests/onboard-compose.test.ts +20 -0
  265. package/src/tests/onboarding.test.ts +108 -1
  266. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  267. package/src/tests/opencode-adapter.test.ts +2 -1
  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/pages-frame-ancestors.test.ts +162 -0
  271. package/src/tests/pi-codemode.test.ts +2 -1
  272. package/src/tests/pi-mono-adapter.test.ts +54 -0
  273. package/src/tests/pi-native-mcp.test.ts +13 -1
  274. package/src/tests/pi-tool-exposure.test.ts +8 -8
  275. package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
  276. package/src/tests/prompt-template-session.test.ts +4 -3
  277. package/src/tests/provider-key-probes.test.ts +181 -0
  278. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  279. package/src/tests/rbac-e2e-helpers.ts +36 -11
  280. package/src/tests/request-composition-derive.test.ts +329 -0
  281. package/src/tests/script-connections.test.ts +4 -0
  282. package/src/tests/script-executor-conformance.test.ts +36 -1
  283. package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
  284. package/src/tests/script-executor-quickjs.test.ts +140 -0
  285. package/src/tests/script-host-validated-args-native.test.ts +196 -0
  286. package/src/tests/script-per-script-executor.test.ts +415 -0
  287. package/src/tests/script-runs-retention.test.ts +212 -0
  288. package/src/tests/scripts-external-api.test.ts +45 -0
  289. package/src/tests/sdk-allowlist.test.ts +42 -2
  290. package/src/tests/self-improvement.test.ts +4 -4
  291. package/src/tests/session-source-backfill.test.ts +429 -0
  292. package/src/tests/session-source-keys.test.ts +236 -0
  293. package/src/tests/session-source-labels.test.ts +419 -0
  294. package/src/tests/sessions-detail-batch.test.ts +2 -0
  295. package/src/tests/sessions-move.test.ts +131 -0
  296. package/src/tests/sessions-tree.test.ts +299 -2
  297. package/src/tests/slack-inbound-files.test.ts +103 -23
  298. package/src/tests/status.test.ts +101 -18
  299. package/src/tests/store-progress-learning.test.ts +288 -0
  300. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  301. package/src/tests/test-net.ts +31 -0
  302. package/src/tests/transcription.test.ts +8 -0
  303. package/src/tests/ui-logs-parser.test.ts +180 -0
  304. package/src/tests/used-by-route.test.ts +11 -0
  305. package/src/tests/workflow-cascade-cancel.test.ts +172 -0
  306. package/src/tests/workflow-engine-v2.test.ts +116 -1
  307. package/src/tests/workflow-http-v2.test.ts +131 -0
  308. package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
  309. package/src/tests/workflow-run-finished-event.test.ts +263 -0
  310. package/src/tests/workflow-step-order.test.ts +92 -0
  311. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  312. package/src/tests/workflow-swarm-script.test.ts +34 -0
  313. package/src/tools/get-tasks.ts +8 -0
  314. package/src/tools/kv/kv-read-auth.ts +4 -2
  315. package/src/tools/script-common.ts +1 -0
  316. package/src/tools/script-run.ts +7 -0
  317. package/src/tools/script-upsert.ts +7 -0
  318. package/src/tools/store-progress.ts +67 -1
  319. package/src/tools/task-action.ts +1 -1
  320. package/src/tools/workflows/get-workflow-run.ts +14 -4
  321. package/src/tools/workflows/trigger-workflow.ts +104 -10
  322. package/src/types.ts +36 -1
  323. package/src/utils/company-profile.ts +51 -1
  324. package/src/utils/credentials.ts +10 -4
  325. package/src/utils/internal-ai/complete-structured.ts +5 -0
  326. package/src/utils/internal-ai/credentials.ts +4 -3
  327. package/src/utils/provider-key-probes.ts +328 -0
  328. package/src/utils/transcription.ts +4 -1
  329. package/src/workflows/engine.ts +18 -16
  330. package/src/workflows/executors/sub-workflow.ts +8 -1
  331. package/src/workflows/executors/swarm-script.ts +5 -0
  332. package/src/workflows/foreach-driver.ts +5 -11
  333. package/src/workflows/node-timeout.ts +30 -0
  334. package/templates/ai-toolbox.manifest.json +2 -2
  335. package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
  336. package/templates/community/linear-delivery-loop/README.md +38 -0
  337. package/templates/community/linear-delivery-loop/config.example.json +31 -0
  338. package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
  339. package/templates/community/linear-delivery-loop/schedule.json +10 -0
  340. package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
  341. package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
  342. package/templates/skills/researching/content.md +2 -0
  343. package/templates/skills/swarm-docs/config.json +14 -0
  344. package/templates/skills/swarm-docs/content.md +144 -0
  345. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  346. package/templates/skills/swarm-scripts/content.md +1 -0
  347. package/dist/cli-7421jzsw.js +0 -84
  348. package/dist/cli-e45e4m6b.js +0 -413
  349. package/dist/cli-zw88zjba.js +0 -307
  350. package/dist/handlers-wy1dr6ja.js +0 -41
  351. package/dist/index-nmm79qgr.js +0 -43
  352. package/dist/lead-sawnyqxk.js +0 -79
  353. package/dist/render-v2-pjwxvnkw.js +0 -12
  354. package/dist/worker-v5e5rxj3.js +0 -79
@@ -1,8 +1,9 @@
1
- import { describe, expect, mock, test } from "bun:test";
1
+ import { afterEach, describe, expect, mock, test } from "bun:test";
2
2
  import { OPENAI_COMPATIBLE_PROVIDERS } from "@desplega/model-catalog";
3
3
  import {
4
4
  buildCredStatusReport,
5
5
  CREDENTIAL_PROVIDER_CHECKERS,
6
+ checkEachKey,
6
7
  checkOpenAICompatibleProvider,
7
8
  checkProviderCredentials,
8
9
  isBedrockMode,
@@ -27,6 +28,241 @@ function fsWith(present: Set<string>): { existsSync(p: string): boolean } {
27
28
 
28
29
  const noFiles = fsWith(new Set());
29
30
 
31
+ describe("pool live test", () => {
32
+ const realFetch = globalThis.fetch;
33
+ afterEach(() => {
34
+ globalThis.fetch = realFetch;
35
+ });
36
+
37
+ test("all members pass in parallel with the maximum latency", async () => {
38
+ let release!: () => void;
39
+ const gate = new Promise<void>((resolve) => {
40
+ release = resolve;
41
+ });
42
+ const check = mock(async (key: string) => {
43
+ await gate;
44
+ return { ok: true, latency_ms: key === "fake-abcde" ? 30 : 10 };
45
+ });
46
+ const pending = checkEachKey("OPENAI_API_KEY", " fake-abcde, fake-fghij ", check);
47
+ expect(check).toHaveBeenCalledTimes(2);
48
+ release();
49
+ expect(await pending).toEqual({
50
+ ok: true,
51
+ latency_ms: 30,
52
+ keys: [
53
+ { keyType: "OPENAI_API_KEY", index: 0, suffix: "abcde", ok: true, latency_ms: 30 },
54
+ { keyType: "OPENAI_API_KEY", index: 1, suffix: "fghij", ok: true, latency_ms: 10 },
55
+ ],
56
+ });
57
+ });
58
+
59
+ test("one failed member preserves pool order and reports a warning", async () => {
60
+ const result = await checkEachKey(
61
+ "ANTHROPIC_API_KEY",
62
+ "fake-first,fake-abcde,fake-third",
63
+ async (key) =>
64
+ key === "fake-abcde"
65
+ ? { ok: false, error: "Unauthorized", httpStatus: 401, latency_ms: 20 }
66
+ : { ok: true, latency_ms: 10 },
67
+ );
68
+ expect(result).toMatchObject({
69
+ ok: true,
70
+ latency_ms: 20,
71
+ warning: "1 of 3 keys failed: key ending abcde (HTTP 401).",
72
+ });
73
+ expect(result.keys).toEqual([
74
+ { keyType: "ANTHROPIC_API_KEY", index: 0, suffix: "first", ok: true, latency_ms: 10 },
75
+ {
76
+ keyType: "ANTHROPIC_API_KEY",
77
+ index: 1,
78
+ suffix: "abcde",
79
+ ok: false,
80
+ error: "Unauthorized",
81
+ httpStatus: 401,
82
+ latency_ms: 20,
83
+ },
84
+ { keyType: "ANTHROPIC_API_KEY", index: 2, suffix: "third", ok: true, latency_ms: 10 },
85
+ ]);
86
+ expect(result.error).toBeUndefined();
87
+ });
88
+
89
+ test("several failed members use HTTP status or the member error", async () => {
90
+ const result = await checkEachKey(
91
+ "OPENAI_API_KEY",
92
+ "fake-abcde,fake-fghij,fake-third",
93
+ async (key) => {
94
+ if (key === "fake-abcde") {
95
+ return { ok: false, error: "Unauthorized", httpStatus: 401, latency_ms: 10 };
96
+ }
97
+ if (key === "fake-fghij") return { ok: false, error: "timeout", latency_ms: 30 };
98
+ return { ok: true, latency_ms: 20 };
99
+ },
100
+ );
101
+ expect(result.ok).toBe(true);
102
+ expect(result.warning).toBe(
103
+ "2 of 3 keys failed: key ending abcde (HTTP 401), key ending fghij (timeout).",
104
+ );
105
+ expect(result.latency_ms).toBe(30);
106
+ });
107
+
108
+ test("all failed members produce an error", async () => {
109
+ const result = await checkEachKey("OPENAI_API_KEY", "fake-abcde,fake-fghij", async () => ({
110
+ ok: false,
111
+ error: "Unauthorized",
112
+ httpStatus: 401,
113
+ latency_ms: 10,
114
+ }));
115
+ expect(result).toMatchObject({
116
+ ok: false,
117
+ error: "All 2 keys failed: key ending abcde (HTTP 401), key ending fghij (HTTP 401).",
118
+ latency_ms: 10,
119
+ });
120
+ expect(result.keys).toHaveLength(2);
121
+ expect(result.warning).toBeUndefined();
122
+ expect(result.httpStatus).toBeUndefined();
123
+ });
124
+
125
+ test("one failed key preserves the member error and HTTP status", async () => {
126
+ const result = await checkEachKey("OPENAI_API_KEY", "fake-abcde", async () => ({
127
+ ok: false,
128
+ error: "HTTP 401: rejected",
129
+ httpStatus: 401,
130
+ latency_ms: 10,
131
+ }));
132
+ expect(result).toMatchObject({
133
+ ok: false,
134
+ error: "HTTP 401: rejected",
135
+ httpStatus: 401,
136
+ latency_ms: 10,
137
+ });
138
+ expect(result.keys).toHaveLength(1);
139
+ });
140
+
141
+ test("a thrown check becomes a scrubbed member failure", async () => {
142
+ const result = await checkEachKey("OPENAI_API_KEY", "fake-abcde,fake-fghij", async (key) => {
143
+ if (key === "fake-abcde") throw new Error("timeout");
144
+ return { ok: true, latency_ms: 0 };
145
+ });
146
+ expect(result.ok).toBe(true);
147
+ expect(result.warning).toBe("1 of 2 keys failed: key ending abcde (timeout).");
148
+ expect(result.keys?.[0]).toMatchObject({ ok: false, error: "timeout" });
149
+ expect(result.keys?.[0]?.latency_ms).toBeGreaterThanOrEqual(0);
150
+ });
151
+
152
+ test("a CODEX_OAUTH JSON blob remains one member", async () => {
153
+ const blob = '{"access":"fake-access","refresh":"fake-refresh","expires":123}';
154
+ const check = mock(async () => ({ ok: true, latency_ms: 0 }));
155
+ const result = await checkEachKey("CODEX_OAUTH", ` ${blob} `, check);
156
+ expect(check).toHaveBeenCalledTimes(1);
157
+ expect(check).toHaveBeenCalledWith(blob);
158
+ expect(result.keys).toHaveLength(1);
159
+ expect(result.keys?.[0]?.suffix).toBe(blob.slice(-5));
160
+ });
161
+
162
+ test("an empty split checks the trimmed input once", async () => {
163
+ const check = mock(async () => ({ ok: false, error: "Missing key", latency_ms: 0 }));
164
+ const result = await checkEachKey("OPENAI_API_KEY", " , , ", check);
165
+ expect(check).toHaveBeenCalledTimes(1);
166
+ expect(check).toHaveBeenCalledWith(", ,");
167
+ expect(result.error).toBe("Missing key");
168
+ });
169
+
170
+ test("all configured members skip the live test", async () => {
171
+ const result = await checkEachKey("ANTHROPIC_API_KEY", "fake-abcde,fake-fghij", async () => ({
172
+ ok: true,
173
+ skipped: true,
174
+ latency_ms: 10,
175
+ }));
176
+ expect(result).toEqual({ ok: true, skipped: true, latency_ms: 10 });
177
+ });
178
+
179
+ test("Claude API-key pools send a separate request for each member", async () => {
180
+ const fetchMock = mock(async (_url: string | URL | Request, init?: RequestInit) => {
181
+ const key = new Headers(init?.headers).get("x-api-key");
182
+ return new Response("{}", { status: key === "fake-good1" ? 200 : 401 });
183
+ });
184
+ globalThis.fetch = fetchMock as typeof fetch;
185
+ const result = await validateProviderCredentials("claude", {
186
+ ANTHROPIC_API_KEY: "fake-good1,fake-bad01",
187
+ });
188
+ expect(fetchMock).toHaveBeenCalledTimes(2);
189
+ expect(
190
+ fetchMock.mock.calls.map(([, init]) => new Headers(init?.headers).get("x-api-key")),
191
+ ).toEqual(["fake-good1", "fake-bad01"]);
192
+ expect(result.ok).toBe(true);
193
+ expect(result.warning).toBe("1 of 2 keys failed: key ending bad01 (HTTP 401).");
194
+ expect(result.keys?.[1]?.httpStatus).toBe(401);
195
+ });
196
+
197
+ test("Claude subscription pools check the OAuth profile for each member", async () => {
198
+ const fetchMock = mock(async (_url: string | URL | Request, _init?: RequestInit) => {
199
+ return new Response("{}", { status: 200 });
200
+ });
201
+ globalThis.fetch = fetchMock as typeof fetch;
202
+ const result = await validateProviderCredentials("claude", {
203
+ CLAUDE_CODE_OAUTH_TOKEN: "fake-token-a,fake-token-b",
204
+ });
205
+ expect(result.ok).toBe(true);
206
+ expect(result.keys).toHaveLength(2);
207
+ expect(fetchMock).toHaveBeenCalledTimes(2);
208
+ for (const [url, init] of fetchMock.mock.calls) {
209
+ expect(String(url)).toBe("https://api.anthropic.com/api/oauth/profile");
210
+ expect(init?.method).toBe("GET");
211
+ }
212
+ expect(
213
+ fetchMock.mock.calls.map(([, init]) => new Headers(init?.headers).get("Authorization")),
214
+ ).toEqual(["Bearer fake-token-a", "Bearer fake-token-b"]);
215
+ });
216
+
217
+ test("buildCredStatusReport copies the warning and per-key results", async () => {
218
+ globalThis.fetch = (async (_url: string | URL | Request, init?: RequestInit) => {
219
+ const key = new Headers(init?.headers).get("x-api-key");
220
+ return new Response("{}", { status: key === "fake-good1" ? 200 : 401 });
221
+ }) as typeof fetch;
222
+ const report = await buildCredStatusReport(
223
+ "claude",
224
+ { ANTHROPIC_API_KEY: "fake-good1,fake-bad01" },
225
+ {},
226
+ "boot",
227
+ );
228
+ expect(report.liveTest?.warning).toBe("1 of 2 keys failed: key ending bad01 (HTTP 401).");
229
+ expect(report.liveTest?.keys).toEqual([
230
+ {
231
+ keyType: "ANTHROPIC_API_KEY",
232
+ index: 0,
233
+ suffix: "good1",
234
+ ok: true,
235
+ latency_ms: expect.any(Number),
236
+ },
237
+ {
238
+ keyType: "ANTHROPIC_API_KEY",
239
+ index: 1,
240
+ suffix: "bad01",
241
+ ok: false,
242
+ error: "HTTP 401: {}",
243
+ httpStatus: 401,
244
+ latency_ms: expect.any(Number),
245
+ },
246
+ ]);
247
+ expect(AgentCredStatusSchema.parse(report).liveTest).toEqual(report.liveTest);
248
+ const success = await buildCredStatusReport(
249
+ "claude",
250
+ { ANTHROPIC_API_KEY: "fake-good1" },
251
+ {},
252
+ "boot",
253
+ );
254
+ expect(success.liveTest).not.toHaveProperty("warning");
255
+ const presenceOnly = await buildCredStatusReport(
256
+ "acp",
257
+ { ACP_COMMAND: "fake-command" },
258
+ {},
259
+ "boot",
260
+ );
261
+ expect(presenceOnly.liveTest).not.toHaveProperty("warning");
262
+ expect(presenceOnly.liveTest).not.toHaveProperty("keys");
263
+ });
264
+ });
265
+
30
266
  // ─── claude ──────────────────────────────────────────────────────────────────
31
267
 
32
268
  describe("checkClaudeCredentials", () => {
@@ -1087,10 +1323,12 @@ describe("NaN and Helmcode live probes", () => {
1087
1323
  // ─── Migration 055: report composition + opt-out ────────────────────────────
1088
1324
 
1089
1325
  describe("isCredCheckDisabled", () => {
1090
- test("true only when CRED_CHECK_DISABLE === '1'", () => {
1326
+ test("true when CRED_CHECK_DISABLE is '1' or 'true' (case-insensitive)", () => {
1091
1327
  expect(isCredCheckDisabled({})).toBe(false);
1092
1328
  expect(isCredCheckDisabled({ CRED_CHECK_DISABLE: "0" })).toBe(false);
1093
- expect(isCredCheckDisabled({ CRED_CHECK_DISABLE: "true" })).toBe(false);
1329
+ expect(isCredCheckDisabled({ CRED_CHECK_DISABLE: "false" })).toBe(false);
1330
+ expect(isCredCheckDisabled({ CRED_CHECK_DISABLE: "true" })).toBe(true);
1331
+ expect(isCredCheckDisabled({ CRED_CHECK_DISABLE: "TRUE" })).toBe(true);
1094
1332
  expect(isCredCheckDisabled({ CRED_CHECK_DISABLE: "1" })).toBe(true);
1095
1333
  });
1096
1334
  });
@@ -1174,7 +1412,10 @@ describe("buildCredStatusReport", () => {
1174
1412
  expect(failure.liveTest?.ok).toBe(false);
1175
1413
  expect(failure.liveTest?.error).toContain("HTTP 401");
1176
1414
  expect(fetchMock).toHaveBeenCalledTimes(2);
1177
- expect(String(fetchMock.mock.calls[0]?.[0])).toMatch(/\/models$/);
1415
+ // OpenRouter's /models is public, so its key is checked on /key.
1416
+ expect(String(fetchMock.mock.calls[0]?.[0])).toMatch(
1417
+ key === "OPENROUTER_API_KEY" ? /\/key$/ : /\/models$/,
1418
+ );
1178
1419
  } finally {
1179
1420
  globalThis.fetch = realFetch;
1180
1421
  }
@@ -1437,3 +1678,52 @@ describe("runBedrockSdkProbeAndEnumerate — intersection logic", () => {
1437
1678
  mock.restore();
1438
1679
  });
1439
1680
  });
1681
+
1682
+ describe("OpenRouter and DeepSeek live checks", () => {
1683
+ const realFetch = globalThis.fetch;
1684
+ afterEach(() => {
1685
+ globalThis.fetch = realFetch;
1686
+ });
1687
+
1688
+ /** Answers by path; records every URL. */
1689
+ function stubByPath(statusFor: (url: string) => number) {
1690
+ const urls: string[] = [];
1691
+ globalThis.fetch = (async (input: string | URL | Request) => {
1692
+ const url = input instanceof Request ? input.url : String(input);
1693
+ urls.push(url);
1694
+ return new Response("{}", { status: statusFor(url) });
1695
+ }) as typeof fetch;
1696
+ return urls;
1697
+ }
1698
+
1699
+ test("OpenRouter checks the key on /key, not the public /models", async () => {
1700
+ const urls = stubByPath((url) => (url.endsWith("/key") ? 401 : 200));
1701
+ const result = await validateProviderCredentials("pi", { OPENROUTER_API_KEY: "or-bad" });
1702
+ expect(result).toMatchObject({ ok: false, httpStatus: 401 });
1703
+ expect(urls).toEqual(["https://openrouter.ai/api/v1/key"]);
1704
+ });
1705
+
1706
+ test("a base URL without /key falls back to /models", async () => {
1707
+ const urls = stubByPath((url) => (url.endsWith("/key") ? 404 : 200));
1708
+ const result = await validateProviderCredentials("pi", { OPENROUTER_API_KEY: "or-key" });
1709
+ expect(result.ok).toBe(true);
1710
+ expect(urls).toEqual([
1711
+ "https://openrouter.ai/api/v1/key",
1712
+ "https://openrouter.ai/api/v1/models",
1713
+ ]);
1714
+ });
1715
+
1716
+ test("dsh checks DEEPSEEK_API_KEY on DeepSeek /models, else OpenRouter", async () => {
1717
+ const urls = stubByPath(() => 401);
1718
+ const deepseek = await validateProviderCredentials("dsh", {
1719
+ DEEPSEEK_API_KEY: "ds-bad",
1720
+ OPENROUTER_API_KEY: "or-key",
1721
+ });
1722
+ expect(deepseek).toMatchObject({ ok: false, httpStatus: 401 });
1723
+ expect(urls).toEqual(["https://api.deepseek.com/models"]);
1724
+
1725
+ const openrouter = await validateProviderCredentials("dsh", { OPENROUTER_API_KEY: "or-key" });
1726
+ expect(openrouter).toMatchObject({ ok: false, httpStatus: 401 });
1727
+ expect(urls.at(-1)).toBe("https://openrouter.ai/api/v1/key");
1728
+ });
1729
+ });
@@ -2,9 +2,14 @@ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:
2
2
  import {
3
3
  CREDENTIAL_RETRY_INTERVAL_MS,
4
4
  type CredentialRefreshState,
5
+ credentialFingerprint,
6
+ KEY_RECHECK_INTERVAL_MS,
7
+ recheckableFailures,
5
8
  refreshCredentialStatus,
9
+ reportKeyValidation,
6
10
  } from "../commands/credential-refresh";
7
11
  import * as credentials from "../commands/provider-credentials";
12
+ import { fetchResolvedEnv } from "../commands/runner";
8
13
  import type { AgentCredStatus } from "../types";
9
14
 
10
15
  const api = { apiUrl: "http://unused.test", apiKey: "test", agentId: crypto.randomUUID() };
@@ -27,7 +32,14 @@ describe("runner credential refresh", () => {
27
32
  let send: ReturnType<typeof spyOn<typeof credentials, "sendCredStatusReport">>;
28
33
 
29
34
  beforeEach(() => {
30
- state = { harnessProvider: "claude", ready: true, lastRefreshAt: 0, inFlight: false };
35
+ state = {
36
+ harnessProvider: "claude",
37
+ ready: true,
38
+ lastRefreshAt: 0,
39
+ inFlight: false,
40
+ fingerprint: credentialFingerprint({}),
41
+ rejectedKeys: 0,
42
+ };
31
43
  build = spyOn(credentials, "buildCredStatusReport").mockResolvedValue(ready);
32
44
  send = spyOn(credentials, "sendCredStatusReport").mockResolvedValue(undefined);
33
45
  });
@@ -43,6 +55,136 @@ describe("runner credential refresh", () => {
43
55
  expect(state.ready).toBe(true);
44
56
  });
45
57
 
58
+ test("a changed credential fingerprint refreshes a ready worker immediately", async () => {
59
+ const env = { ANTHROPIC_API_KEY: "test-pool-a,test-pool-b" };
60
+ await refreshCredentialStatus(api, state, "claude", env, 1);
61
+ expect(build).toHaveBeenCalledWith("claude", env, {}, "post_task");
62
+ expect(state.fingerprint).toBe(credentialFingerprint(env));
63
+ await refreshCredentialStatus(api, state, "claude", env, 2);
64
+ expect(build).toHaveBeenCalledTimes(1);
65
+ await refreshCredentialStatus(api, state, "claude", {}, 3);
66
+ expect(build).toHaveBeenCalledTimes(2);
67
+ });
68
+
69
+ test("an unchanged fingerprint does not refresh a ready worker", async () => {
70
+ await refreshCredentialStatus(api, state, "claude", {}, KEY_RECHECK_INTERVAL_MS);
71
+ expect(build).not.toHaveBeenCalled();
72
+ });
73
+
74
+ test("fingerprints ignore environment order and unrelated values", () => {
75
+ expect(credentialFingerprint({ ANTHROPIC_API_KEY: "test-a", PATH: "a" })).toBe(
76
+ credentialFingerprint({ PATH: "b", ANTHROPIC_API_KEY: "test-a" }),
77
+ );
78
+ for (const key of ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "OPENAI_API_KEY"]) {
79
+ expect(credentialFingerprint({ [key]: "changed" })).not.toBe(credentialFingerprint({}));
80
+ }
81
+ });
82
+
83
+ test("resolved credential env preserves the full pool before selection", async () => {
84
+ const originalFetch = globalThis.fetch;
85
+ globalThis.fetch = (async (url) => {
86
+ if (String(url).includes("/api/config/resolved")) {
87
+ return Response.json({
88
+ configs: [{ key: "ANTHROPIC_API_KEY", value: "test-aaa11,test-bbb22" }],
89
+ });
90
+ }
91
+ return Response.json({ availableIndices: [1], rejectedIndices: [0] });
92
+ }) as typeof fetch;
93
+ try {
94
+ const result = await fetchResolvedEnv(api.apiUrl, api.apiKey, api.agentId, {
95
+ HARNESS_PROVIDER: "claude",
96
+ });
97
+ expect(result.credentialEnv.ANTHROPIC_API_KEY).toBe("test-aaa11,test-bbb22");
98
+ expect(result.env.ANTHROPIC_API_KEY).toBe("test-bbb22");
99
+ expect(result.credentialEnv).not.toBe(result.env);
100
+ } finally {
101
+ globalThis.fetch = originalFetch;
102
+ }
103
+ });
104
+
105
+ test("rejected keys refresh after ten minutes, even when the worker is ready", async () => {
106
+ state.rejectedKeys = 1;
107
+ await refreshCredentialStatus(api, state, "claude", {}, KEY_RECHECK_INTERVAL_MS - 1);
108
+ expect(build).not.toHaveBeenCalled();
109
+ await refreshCredentialStatus(api, state, "claude", {}, KEY_RECHECK_INTERVAL_MS);
110
+ expect(build).toHaveBeenCalledTimes(1);
111
+ expect(state.rejectedKeys).toBe(0);
112
+ });
113
+
114
+ test("a failed live test without per-key results remains eligible for recheck", async () => {
115
+ build.mockResolvedValueOnce({
116
+ ...ready,
117
+ liveTest: { ok: false, error: "timeout", latency_ms: 1 },
118
+ });
119
+ await refreshCredentialStatus(api, state, "pi", {}, 1);
120
+ expect(state.rejectedKeys).toBe(1);
121
+ });
122
+
123
+ test("refresh reports per-key results once per credential type", async () => {
124
+ const keys = [
125
+ {
126
+ keyType: "ANTHROPIC_API_KEY",
127
+ index: 0,
128
+ suffix: "aaa11",
129
+ ok: false,
130
+ httpStatus: 401,
131
+ latency_ms: 1,
132
+ },
133
+ { keyType: "ANTHROPIC_API_KEY", index: 1, suffix: "bbb22", ok: true, latency_ms: 1 },
134
+ { keyType: "OPENAI_API_KEY", index: 0, suffix: "ccc33", ok: true, latency_ms: 1 },
135
+ ];
136
+ build.mockResolvedValueOnce({ ...ready, liveTest: { ok: false, latency_ms: 1, keys } });
137
+ const requests: Array<{ url: string; init?: RequestInit }> = [];
138
+ const originalFetch = globalThis.fetch;
139
+ globalThis.fetch = (async (url, init) => {
140
+ requests.push({ url: String(url), init });
141
+ return Response.json({ success: true });
142
+ }) as typeof fetch;
143
+ try {
144
+ await refreshCredentialStatus(api, state, "pi", {}, 1);
145
+ expect(state.rejectedKeys).toBe(1);
146
+ expect(requests).toHaveLength(2);
147
+ expect(requests[0]!.url).toBe(`${api.apiUrl}/api/keys/report-validation`);
148
+ expect(requests[0]!.init?.headers).toMatchObject({
149
+ Authorization: `Bearer ${api.apiKey}`,
150
+ "X-Agent-ID": api.agentId,
151
+ });
152
+ expect(JSON.parse(String(requests[0]!.init?.body))).toEqual({
153
+ keyType: "ANTHROPIC_API_KEY",
154
+ keys: [
155
+ { keyIndex: 0, keySuffix: "aaa11", ok: false, httpStatus: 401 },
156
+ { keyIndex: 1, keySuffix: "bbb22", ok: true },
157
+ ],
158
+ });
159
+ expect(JSON.parse(String(requests[1]!.init?.body))).toEqual({
160
+ keyType: "OPENAI_API_KEY",
161
+ keys: [{ keyIndex: 0, keySuffix: "ccc33", ok: true }],
162
+ });
163
+ expect(requests[0]!.init?.signal).toBeInstanceOf(AbortSignal);
164
+ } finally {
165
+ globalThis.fetch = originalFetch;
166
+ }
167
+ });
168
+
169
+ test("validation reporting contains HTTP and network failures", async () => {
170
+ const originalFetch = globalThis.fetch;
171
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
172
+ const fetchMock = mock<typeof fetch>()
173
+ .mockResolvedValueOnce(new Response(null, { status: 503 }))
174
+ .mockRejectedValueOnce(new Error("network unavailable"));
175
+ globalThis.fetch = fetchMock as typeof fetch;
176
+ try {
177
+ await reportKeyValidation(api, [
178
+ { keyType: "ANTHROPIC_API_KEY", index: 0, suffix: "aaa11", ok: true, latency_ms: 1 },
179
+ { keyType: "OPENAI_API_KEY", index: 0, suffix: "bbb22", ok: true, latency_ms: 1 },
180
+ ]);
181
+ expect(fetchMock).toHaveBeenCalledTimes(2);
182
+ expect(warn).toHaveBeenCalledTimes(2);
183
+ } finally {
184
+ globalThis.fetch = originalFetch;
185
+ }
186
+ });
187
+
46
188
  test("a failed readiness write is throttled and retried even when the snapshot is ready", async () => {
47
189
  send.mockRejectedValueOnce(new Error("HTTP 503"));
48
190
  await expect(refreshCredentialStatus(api, state, "pi", {}, 1)).rejects.toThrow("HTTP 503");
@@ -113,3 +255,32 @@ describe("runner credential refresh", () => {
113
255
  expect(build).toHaveBeenCalledTimes(1);
114
256
  });
115
257
  });
258
+
259
+ describe("recheckableFailures", () => {
260
+ const key = (keyType: string, ok: boolean) => ({
261
+ keyType,
262
+ index: 0,
263
+ suffix: "abcde",
264
+ ok,
265
+ latency_ms: 1,
266
+ });
267
+
268
+ test("a Claude subscription token never joins the periodic recheck", () => {
269
+ const liveTest = {
270
+ ok: false,
271
+ error: "rejected",
272
+ latency_ms: 1,
273
+ testedAt: 1,
274
+ keys: [key("CLAUDE_CODE_OAUTH_TOKEN", false), key("ANTHROPIC_API_KEY", false)],
275
+ };
276
+ expect(recheckableFailures(liveTest)).toBe(1);
277
+ expect(
278
+ recheckableFailures({ ...liveTest, keys: [key("CLAUDE_CODE_OAUTH_TOKEN", false)] }),
279
+ ).toBe(0);
280
+ });
281
+
282
+ test("a failed test without per-key results counts once", () => {
283
+ expect(recheckableFailures({ ok: false, error: "x", latency_ms: 1, testedAt: 1 })).toBe(1);
284
+ expect(recheckableFailures(null)).toBe(0);
285
+ });
286
+ });
@@ -19,8 +19,13 @@ import {
19
19
  const TEST_DB_PATH = "./test-db-retention.sqlite";
20
20
  const NOW = new Date("2026-08-23T12:00:00.000Z");
21
21
  const RETENTION_KEYS = DB_RETENTION_TABLES.map((table) => table.envKey);
22
- /** Time-horizon tables. Count-based context_versions has its own suite. */
23
- const AGE_TABLES = DB_RETENTION_TABLES.filter((table) => table.policy === "age");
22
+ /**
23
+ * Plain time-horizon tables. Count-based context_versions and status-filtered
24
+ * script_runs have their own suites.
25
+ */
26
+ const AGE_TABLES = DB_RETENTION_TABLES.filter(
27
+ (table) => table.policy === "age" && !("terminalStatuses" in table),
28
+ );
24
29
  type AgeTable = (typeof AGE_TABLES)[number];
25
30
  const TUNING_KEYS = [
26
31
  "DB_RETENTION_TICK_BUDGET_MS",
@@ -135,12 +140,13 @@ afterEach(async () => {
135
140
  });
136
141
 
137
142
  describe("DB retention", () => {
138
- test("keeps the closed allowlist limited to the four approved tables", () => {
143
+ test("keeps the closed allowlist limited to the five approved tables", () => {
139
144
  expect(DB_RETENTION_TABLES.map((table) => table.table)).toEqual([
140
145
  "session_logs",
141
146
  "agent_log",
142
147
  "events",
143
148
  "context_versions",
149
+ "script_runs",
144
150
  ]);
145
151
  });
146
152
 
@@ -529,6 +529,7 @@ value:recordKeyAuthFailure
529
529
  value:recordKeyRateLimitWindows
530
530
  value:recordKeySeatMismatch
531
531
  value:recordKeyUsage
532
+ value:recordKeyValidation
532
533
  value:recordSlackMessage
533
534
  value:recordTaskProviderIfUnset
534
535
  value:recordTaskPullRequestAttachments
@@ -552,6 +552,7 @@ value:recordKeyAuthFailure
552
552
  value:recordKeyRateLimitWindows
553
553
  value:recordKeySeatMismatch
554
554
  value:recordKeyUsage
555
+ value:recordKeyValidation
555
556
  value:recordSlackMessage
556
557
  value:recordTaskProviderIfUnset
557
558
  value:recordTaskPullRequestAttachments
@@ -684,7 +685,6 @@ STEERING_MESSAGE_SELECT
684
685
  UNATTRIBUTED_USER_ID
685
686
  VERSIONABLE_FIELDS
686
687
  WEEK_SECONDS
687
- WORKFLOW_RUN_SUMMARY_COLUMNS
688
688
  abandonSlackOutcomeDelivery
689
689
  acceptTask
690
690
  assertSkillFileLimits
@@ -1097,8 +1097,6 @@ rowToUser
1097
1097
  rowToUserFavorite
1098
1098
  rowToWaitState
1099
1099
  rowToWorkflow
1100
- rowToWorkflowRun
1101
- rowToWorkflowRunStep
1102
1100
  rowToWorkflowSummary
1103
1101
  rowToWorkflowVersion
1104
1102
  scriptRunUpdateSets
@@ -78,7 +78,7 @@ describe("docs API index footprint", () => {
78
78
  expect(tasks).toContain('"path":"/api/task-templates","method":"get"');
79
79
  });
80
80
 
81
- test("OpenAPI and generated pages keep all 449 operations", async () => {
81
+ test("OpenAPI and generated pages keep all 456 operations", async () => {
82
82
  const spec = await Bun.file(resolve(repoRoot, "openapi.json")).json();
83
83
  const operationKeys = [];
84
84
  const tags = new Set();
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
103
103
  expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
104
104
 
105
105
  const generatedKeys = await generatedOperationKeys();
106
- expect(operationKeys).toHaveLength(449);
107
- expect(generatedKeys).toHaveLength(449);
106
+ expect(operationKeys).toHaveLength(456);
107
+ expect(generatedKeys).toHaveLength(456);
108
108
  expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
109
109
  });
110
110