@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
package/src/be/db.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import pkg from "../../package.json";
3
- import { defaultAssetKey, normalizeAssetKey, sessionGroupKey } from "../assets/key";
3
+ import { defaultAssetKey, normalizeAssetKey } from "../assets/key";
4
+ import { sessionSourceKey, sessionThreadGroupKey } from "../assets/source-key";
4
5
  import {
5
6
  generateDefaultClaudeMd,
6
7
  generateDefaultIdentityMd,
@@ -207,6 +208,7 @@ import {
207
208
  sessionCountQuery,
208
209
  sessionPageFields,
209
210
  sessionSummaryQuery,
211
+ threadRootsSql,
210
212
  } from "./db/session-tree";
211
213
  import { searchSkillRowsRanked } from "./db/skill-search";
212
214
  import { classifyTaskHumanFree, reclassifyTaskHumanFree } from "./db/tasks/human-free";
@@ -225,8 +227,15 @@ import { configureTaskWriteDependencies, failTask } from "./db/tasks/write";
225
227
  import { tombstoneWorkflowIdempotencyKeys } from "./db/workflow-idempotency";
226
228
  import {
227
229
  emitChildRunFinished,
230
+ emitRunFinished,
228
231
  getRunLineageBlock,
229
232
  getStepExecutionBlock,
233
+ isTerminalWorkflowRunStatus,
234
+ rowToWorkflowRun,
235
+ rowToWorkflowRunStep,
236
+ WORKFLOW_RUN_SUMMARY_COLUMNS,
237
+ type WorkflowRunRow,
238
+ type WorkflowRunStepRow,
230
239
  WorkflowStepNotExecutableError,
231
240
  wakeParentsOfDeletedRuns,
232
241
  } from "./db/workflow-runs";
@@ -242,7 +251,7 @@ import {
242
251
  } from "./swarm-config-guard";
243
252
  import { emitTaskStarted } from "./task-lifecycle-events";
244
253
  import { publishWorkerConfigNudge, publishWorkerNudge } from "./worker-nudges";
245
- import { defineWorkflowPayload, type WorkflowPayloadView } from "./workflow-replay";
254
+ import type { WorkflowPayloadView } from "./workflow-replay";
246
255
 
247
256
  export {
248
257
  refreshActiveSessionOnActivity,
@@ -285,7 +294,7 @@ export {
285
294
  updateAgentStatus,
286
295
  updateAgentStatusFromCapacity,
287
296
  } from "./db/agents";
288
- export { recordKeySeatMismatch } from "./db/api-keys";
297
+ export { recordKeySeatMismatch, recordKeyValidation } from "./db/api-keys";
289
298
  export {
290
299
  type ApprovalRequestSummary,
291
300
  type ApprovalVote,
@@ -2497,6 +2506,8 @@ export async function createTaskExtended(
2497
2506
  );
2498
2507
  }
2499
2508
  let requestedByUserIdInherited = false;
2509
+ // The parent key is read again inside the INSERT transaction (see below).
2510
+ let keyInherited = false;
2500
2511
  // True only when `options.routingAffinity` ends up populated purely via
2501
2512
  // the plain parent-fallback inherit below (child declared no affinity of
2502
2513
  // its own, the parent wasn't lead-only, AND the parent's affinity actually
@@ -2661,6 +2672,7 @@ export async function createTaskExtended(
2661
2672
  }
2662
2673
  if (parent.key && !options.key) {
2663
2674
  options.key = parent.key;
2675
+ keyInherited = true;
2664
2676
  }
2665
2677
  if (parent.contextKey && !options.contextKey) {
2666
2678
  options.contextKey = parent.contextKey;
@@ -2850,7 +2862,18 @@ export async function createTaskExtended(
2850
2862
  }
2851
2863
 
2852
2864
  const auditUserId = getCurrentRequestUserId() ?? null;
2853
- const assetKey = normalizeAssetKey(options?.key ?? defaultAssetKey("task", id));
2865
+ const assetKey = normalizeAssetKey(
2866
+ options?.key ??
2867
+ (options?.parentTaskId
2868
+ ? null
2869
+ : sessionSourceKey({
2870
+ id,
2871
+ slackChannelId: options?.slackChannelId,
2872
+ slackThreadTs: options?.slackThreadTs,
2873
+ contextKey: options?.contextKey,
2874
+ })) ??
2875
+ defaultAssetKey("task", id),
2876
+ );
2854
2877
  // The Linear tracker dedup guard and the INSERT commit as one unit. Two
2855
2878
  // distinct webhook deliveries for the same issue are dispatched
2856
2879
  // fire-and-forget (src/linear/webhook.ts), nothing serializes them, and
@@ -2875,6 +2898,19 @@ export async function createTaskExtended(
2875
2898
  : await findExistingLinearTrackerContextWork(options?.contextKey);
2876
2899
  if (existingTrackerWork) return { existing: existingTrackerWork };
2877
2900
 
2901
+ // A chain move (a user move, the source key backfill) can commit between
2902
+ // the parent read above and this write lock. Its chain query did not see
2903
+ // this child, so take the parent's key as it is now.
2904
+ const parentKey =
2905
+ keyInherited && options?.parentTaskId
2906
+ ? (
2907
+ await getDbClient().get<{ key: string }>('SELECT "key" FROM agent_tasks WHERE id = ?', [
2908
+ options.parentTaskId,
2909
+ ])
2910
+ )?.key
2911
+ : undefined;
2912
+ const insertKey = parentKey ? normalizeAssetKey(parentKey) : assetKey;
2913
+
2878
2914
  let routingDecisionSnapshot: RoutingDecisionSnapshot | null = null;
2879
2915
  try {
2880
2916
  routingDecisionSnapshot = await captureRoutingDecisionSnapshot(options);
@@ -2905,7 +2941,7 @@ export async function createTaskExtended(
2905
2941
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
2906
2942
  [
2907
2943
  id,
2908
- assetKey,
2944
+ insertKey,
2909
2945
  options?.agentId ?? null,
2910
2946
  options?.creatorAgentId ?? null,
2911
2947
  // Free text from every task source; scrub at the single INSERT.
@@ -7859,53 +7895,6 @@ export async function getWorkflowsByScheduleId(scheduleId: string): Promise<Work
7859
7895
  // Workflow Run CRUD
7860
7896
  // ============================================================================
7861
7897
 
7862
- type WorkflowRunRow = {
7863
- id: string;
7864
- workflowId: string;
7865
- status: string;
7866
- triggerType: string;
7867
- triggerData: string | null;
7868
- /** Absent from list rows: see `WORKFLOW_RUN_SUMMARY_COLUMNS`. */
7869
- context?: string | null;
7870
- /** Sealed exact `context` (migration 201). Absent from list rows. */
7871
- context_replay?: string | null;
7872
- error: string | null;
7873
- created_by: string | null;
7874
- startedAt: string;
7875
- lastUpdatedAt: string;
7876
- finishedAt: string | null;
7877
- /** The `sub-workflow` step that started this run (migration 203). */
7878
- parentStepId?: string | null;
7879
- /** That step's attempt (`retryCount`) when it started this run (migration 240). */
7880
- parentStepAttempt?: number | null;
7881
- };
7882
-
7883
- /**
7884
- * `view` picks the `context` copy: `replay` (default) is the exact sealed value
7885
- * the engine resumes from; `display` is the scrubbed column for API, MCP and UI
7886
- * responses. See `src/be/workflow-replay.ts`.
7887
- */
7888
- function rowToWorkflowRun(row: WorkflowRunRow, view: WorkflowPayloadView = "replay"): WorkflowRun {
7889
- const run: WorkflowRun = {
7890
- id: row.id,
7891
- workflowId: row.workflowId,
7892
- status: row.status as WorkflowRunStatus,
7893
- triggerData: row.triggerData ? JSON.parse(row.triggerData) : undefined,
7894
- context: undefined,
7895
- error: row.error ?? undefined,
7896
- createdBy: row.created_by ?? undefined,
7897
- startedAt: normalizeDateRequired(row.startedAt),
7898
- lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
7899
- finishedAt: normalizeDate(row.finishedAt) ?? undefined,
7900
- parentStepId: row.parentStepId ?? undefined,
7901
- parentStepAttempt: row.parentStepId ? (row.parentStepAttempt ?? 0) : undefined,
7902
- };
7903
- return defineWorkflowPayload(run, "context", row.context, row.context_replay, view, {
7904
- runId: row.id,
7905
- target: "its context",
7906
- });
7907
- }
7908
-
7909
7898
  export async function createWorkflowRun(data: {
7910
7899
  id: string;
7911
7900
  workflowId: string;
@@ -8007,16 +7996,33 @@ export async function updateWorkflowRun(
8007
7996
  updates.push("lastUpdatedAt = ?");
8008
7997
  params.push(new Date().toISOString());
8009
7998
  params.push(id);
8010
- const row = await getDbClient().get<WorkflowRunRow>(
8011
- `UPDATE workflow_runs SET ${updates.join(", ")} WHERE id = ? RETURNING *`,
8012
- params,
8013
- );
7999
+ const update = () =>
8000
+ getDbClient().get<WorkflowRunRow>(
8001
+ `UPDATE workflow_runs SET ${updates.join(", ")} WHERE id = ? RETURNING *`,
8002
+ params,
8003
+ );
8004
+ const endsRun = data.status !== undefined && isTerminalWorkflowRunStatus(data.status);
8005
+ // A run that ends reads the status it replaces in the same transaction, so
8006
+ // only a change out of a live status announces the end: a later terminal
8007
+ // write, same status or not, does not announce it again.
8008
+ const [previousStatus, row] = endsRun
8009
+ ? await getDbClient().transaction(async () => {
8010
+ const previous = await getDbClient().get<{ status: WorkflowRunStatus }>(
8011
+ "SELECT status FROM workflow_runs WHERE id = ?",
8012
+ [id],
8013
+ );
8014
+ return [previous?.status, await update()] as const;
8015
+ })
8016
+ : [undefined, await update()];
8014
8017
  if (!row) return null;
8015
8018
  const run = rowToWorkflowRun(row);
8016
8019
  if (data.status === "completed" || data.status === "failed") {
8017
8020
  emitWorkflowTerminalTelemetry(run);
8018
8021
  }
8019
8022
  if (row.parentStepId) emitChildRunFinished(id, row.parentStepId, run.status);
8023
+ if (endsRun && previousStatus && !isTerminalWorkflowRunStatus(previousStatus)) {
8024
+ emitRunFinished(id, run.workflowId, run.status);
8025
+ }
8020
8026
  return run;
8021
8027
  }
8022
8028
 
@@ -8031,12 +8037,6 @@ export type WorkflowRunListOptions = {
8031
8037
  includeContext?: boolean;
8032
8038
  };
8033
8039
 
8034
- // Everything but `context`. `triggerData` stays: it is small, and stored
8035
- // scripts read it off list rows (dedupe by trigger, failure reports). The
8036
- // parent link and attempt stay so a list row identifies a child's invocation.
8037
- const WORKFLOW_RUN_SUMMARY_COLUMNS =
8038
- "id, workflowId, status, triggerType, triggerData, error, created_by, startedAt, lastUpdatedAt, finishedAt, parentStepId, parentStepAttempt";
8039
-
8040
8040
  export type WorkflowRunPage = {
8041
8041
  runs: WorkflowRun[];
8042
8042
  page: {
@@ -8121,56 +8121,6 @@ export async function listWorkflowRunsPage(
8121
8121
  // Workflow Run Step CRUD
8122
8122
  // ============================================================================
8123
8123
 
8124
- type WorkflowRunStepRow = {
8125
- id: string;
8126
- runId: string;
8127
- nodeId: string;
8128
- nodeType: string;
8129
- status: string;
8130
- input: string | null;
8131
- output: string | null;
8132
- /** Sealed exact `output` (migration 201). */
8133
- output_replay: string | null;
8134
- error: string | null;
8135
- startedAt: string;
8136
- finishedAt: string | null;
8137
- retryCount: number;
8138
- maxRetries: number;
8139
- nextRetryAt: string | null;
8140
- idempotencyKey: string | null;
8141
- diagnostics: string | null;
8142
- nextPort: string | null;
8143
- };
8144
-
8145
- /** `view` picks the `output` copy, as in `rowToWorkflowRun`. */
8146
- function rowToWorkflowRunStep(
8147
- row: WorkflowRunStepRow,
8148
- view: WorkflowPayloadView = "replay",
8149
- ): WorkflowRunStep {
8150
- const step: WorkflowRunStep = {
8151
- id: row.id,
8152
- runId: row.runId,
8153
- nodeId: row.nodeId,
8154
- nodeType: row.nodeType,
8155
- status: row.status as WorkflowRunStepStatus,
8156
- input: row.input ? JSON.parse(row.input) : undefined,
8157
- output: undefined,
8158
- error: row.error ?? undefined,
8159
- startedAt: normalizeDateRequired(row.startedAt),
8160
- finishedAt: normalizeDate(row.finishedAt) ?? undefined,
8161
- retryCount: row.retryCount,
8162
- maxRetries: row.maxRetries,
8163
- nextRetryAt: normalizeDate(row.nextRetryAt) ?? undefined,
8164
- idempotencyKey: row.idempotencyKey ?? undefined,
8165
- diagnostics: row.diagnostics ?? undefined,
8166
- nextPort: row.nextPort ?? undefined,
8167
- };
8168
- return defineWorkflowPayload(step, "output", row.output, row.output_replay, view, {
8169
- runId: row.runId,
8170
- target: `the output of step ${row.id} (node ${row.nodeId})`,
8171
- });
8172
- }
8173
-
8174
8124
  export async function createWorkflowRunStep(data: {
8175
8125
  id: string;
8176
8126
  runId: string;
@@ -8284,7 +8234,7 @@ export async function getWorkflowRunStepsByRunId(
8284
8234
  view: WorkflowPayloadView = "replay",
8285
8235
  ): Promise<WorkflowRunStep[]> {
8286
8236
  const rows = await getDbClient().query<WorkflowRunStepRow>(
8287
- "SELECT * FROM workflow_run_steps WHERE runId = ? ORDER BY startedAt ASC",
8237
+ "SELECT * FROM workflow_run_steps WHERE runId = ? ORDER BY startedAt ASC, rowid ASC",
8288
8238
  [runId],
8289
8239
  );
8290
8240
  return rows.map((row) => rowToWorkflowRunStep(row, view));
@@ -8400,7 +8350,7 @@ export async function getLatestStepForNode(
8400
8350
  nodeId: string,
8401
8351
  ): Promise<WorkflowRunStep | null> {
8402
8352
  const row = await getDbClient().get<WorkflowRunStepRow>(
8403
- "SELECT * FROM workflow_run_steps WHERE runId = ? AND nodeId = ? ORDER BY startedAt DESC LIMIT 1",
8353
+ "SELECT * FROM workflow_run_steps WHERE runId = ? AND nodeId = ? ORDER BY startedAt DESC, rowid DESC LIMIT 1",
8404
8354
  [runId, nodeId],
8405
8355
  );
8406
8356
  return row ? rowToWorkflowRunStep(row) : null;
@@ -11742,6 +11692,8 @@ export interface ApiKeyStatus {
11742
11692
  /** Auth failures in a row since the last success or clear. */
11743
11693
  consecutiveAuthFailures: number;
11744
11694
  lastAuthFailureAt: string | null;
11695
+ lastValidationError: string | null;
11696
+ lastValidationFailedAt: string | null;
11745
11697
  createdAt: string;
11746
11698
  updatedAt: string;
11747
11699
  }
@@ -11767,6 +11719,7 @@ function rowToApiKeyStatus(row: ApiKeyStatusRow): ApiKeyStatus {
11767
11719
 
11768
11720
  export interface AvailableKeyIndicesResult {
11769
11721
  availableIndices: number[];
11722
+ rejectedIndices: number[];
11770
11723
  /** Indices excluded only by an active model-scoped window block (not key-wide). */
11771
11724
  modelBlockedIndices: number[];
11772
11725
  /** ISO of the earliest resetsAt among modelBlockedIndices, or null when none. */
@@ -11787,6 +11740,14 @@ export interface AvailableKeyIndicesResult {
11787
11740
  * whose `plan` cannot run the family (`planAllowsModelFamily`) is excluded
11788
11741
  * and reported in `seatBlockedIndices`, also when it is key-wide or
11789
11742
  * model-window blocked; a seat block has no reset time.
11743
+ *
11744
+ * With `keySuffixes` (the caller's own pool, in order), rows are matched by
11745
+ * key suffix instead of the stored `keyIndex`, across every scope: a key has
11746
+ * one state wherever it is pooled, and a key at the same index in another
11747
+ * agent's pool is never blocked by it. A suffix listed more than once is read
11748
+ * by `keyIndex` in `scope`/`scopeId`, as without the list. Returned indices
11749
+ * are positions in `keySuffixes`. Without it, rows of `scope`/`scopeId` are
11750
+ * read by `keyIndex`.
11790
11751
  */
11791
11752
  export async function getAvailableKeyIndices(
11792
11753
  keyType: string,
@@ -11794,36 +11755,60 @@ export async function getAvailableKeyIndices(
11794
11755
  scope = "global",
11795
11756
  scopeId: string | null = null,
11796
11757
  modelFamily?: ModelFamily,
11758
+ keySuffixes?: string[],
11797
11759
  ): Promise<AvailableKeyIndicesResult> {
11798
11760
  const now = new Date().toISOString();
11799
11761
  const nowMs = Date.now();
11800
11762
  const client = getDbClient();
11801
11763
  const effectiveScopeId = scopeId ?? "";
11764
+ const suffixes = keySuffixes?.length ? keySuffixes : undefined;
11765
+ const rowFilter = suffixes
11766
+ ? `keyType = ? AND keySuffix IN (${suffixes.map(() => "?").join(", ")})`
11767
+ : "keyType = ? AND scope = ? AND scopeId = ?";
11768
+ const rowParams = suffixes ? [keyType, ...suffixes] : [keyType, scope, effectiveScopeId];
11802
11769
 
11803
11770
  // Auto-clear expired rate limits. Never an auth bench: only `clearAuthBench` lifts it.
11804
11771
  await client.run(
11805
11772
  `UPDATE api_key_status
11806
11773
  SET status = 'available', rateLimitedUntil = NULL, updatedAt = ?
11807
- WHERE keyType = ? AND scope = ? AND scopeId = ?
11774
+ WHERE ${rowFilter}
11808
11775
  AND status = 'rate_limited' AND rateLimitedUntil <= ? AND consecutiveAuthFailures < ?`,
11809
- [now, keyType, scope, effectiveScopeId, now, CODEX_AUTH_FAILURE_BENCH_THRESHOLD],
11776
+ [now, ...rowParams, now, CODEX_AUTH_FAILURE_BENCH_THRESHOLD],
11810
11777
  );
11811
11778
 
11812
11779
  const rows = await client.query<{
11813
11780
  keyIndex: number;
11781
+ keySuffix: string;
11782
+ scope: string;
11783
+ scopeId: string;
11814
11784
  status: string;
11815
11785
  rateLimitWindows: string | null;
11816
11786
  plan: string | null;
11817
11787
  authFailureSeq: number;
11788
+ lastValidationFailedAt: string | null;
11818
11789
  }>(
11819
- `SELECT keyIndex, status, rateLimitWindows, plan, authFailureSeq FROM api_key_status
11820
- WHERE keyType = ? AND scope = ? AND scopeId = ?`,
11821
- [keyType, scope, effectiveScopeId],
11822
- );
11790
+ `SELECT keyIndex, keySuffix, scope, scopeId, status, rateLimitWindows, plan, authFailureSeq, lastValidationFailedAt FROM api_key_status
11791
+ WHERE ${rowFilter}`,
11792
+ rowParams,
11793
+ );
11794
+ // Pool positions a row applies to: the position holding its suffix, or its stored index.
11795
+ // A suffix held by several pool members cannot tell them apart, so it falls back to
11796
+ // the index read: only a row of the requested scope, at its stored index.
11797
+ const positionsOf = (row: (typeof rows)[number]): number[] => {
11798
+ if (!suffixes) return [row.keyIndex];
11799
+ const positions = suffixes.flatMap((suffix, i) => (suffix === row.keySuffix ? [i] : []));
11800
+ if (positions.length < 2) return positions;
11801
+ const sameScope = row.scope === scope && row.scopeId === effectiveScopeId;
11802
+ return sameScope && positions.includes(row.keyIndex) ? [row.keyIndex] : [];
11803
+ };
11823
11804
 
11824
11805
  const blockedIndices = new Set(
11825
- rows.filter((r) => r.status === "rate_limited").map((r) => r.keyIndex),
11806
+ rows.filter((r) => r.status === "rate_limited").flatMap(positionsOf),
11826
11807
  );
11808
+ const rejectedIndices = [
11809
+ ...new Set(rows.filter((row) => row.lastValidationFailedAt !== null).flatMap(positionsOf)),
11810
+ ];
11811
+ for (const index of rejectedIndices) blockedIndices.add(index);
11827
11812
 
11828
11813
  const modelBlockedIndices: number[] = [];
11829
11814
  let earliestModelResetsAtSec: number | undefined;
@@ -11832,14 +11817,17 @@ export async function getAvailableKeyIndices(
11832
11817
  // A key already blocked key-wide doesn't need a separate model-block
11833
11818
  // entry — it's excluded from availableIndices either way, and
11834
11819
  // modelBlockedIndices means "excluded only by a model-scoped block".
11835
- if (blockedIndices.has(row.keyIndex)) continue;
11820
+ const positions = positionsOf(row).filter(
11821
+ (i) => !blockedIndices.has(i) && !modelBlockedIndices.includes(i),
11822
+ );
11823
+ if (positions.length === 0) continue;
11836
11824
  const block = activeModelBlock(
11837
11825
  parseRateLimitWindowsJson(row.rateLimitWindows),
11838
11826
  modelFamily,
11839
11827
  nowMs,
11840
11828
  );
11841
11829
  if (!block) continue;
11842
- modelBlockedIndices.push(row.keyIndex);
11830
+ modelBlockedIndices.push(...positions);
11843
11831
  if (earliestModelResetsAtSec === undefined || block.resetsAt < earliestModelResetsAtSec) {
11844
11832
  earliestModelResetsAtSec = block.resetsAt;
11845
11833
  }
@@ -11853,7 +11841,10 @@ export async function getAvailableKeyIndices(
11853
11841
  const seatBlockedIndices: number[] = [];
11854
11842
  if (modelFamily) {
11855
11843
  for (const row of rows) {
11856
- if (!planAllowsModelFamily(row.plan, modelFamily)) seatBlockedIndices.push(row.keyIndex);
11844
+ if (planAllowsModelFamily(row.plan, modelFamily)) continue;
11845
+ for (const i of positionsOf(row)) {
11846
+ if (!seatBlockedIndices.includes(i)) seatBlockedIndices.push(i);
11847
+ }
11857
11848
  }
11858
11849
  }
11859
11850
  const seatBlockedSet = new Set(seatBlockedIndices);
@@ -11866,6 +11857,7 @@ export async function getAvailableKeyIndices(
11866
11857
 
11867
11858
  return {
11868
11859
  availableIndices,
11860
+ rejectedIndices,
11869
11861
  modelBlockedIndices,
11870
11862
  seatBlockedIndices,
11871
11863
  earliestModelResetAt:
@@ -12201,22 +12193,30 @@ export async function clearKeyRateLimit(
12201
12193
  const effectiveScopeId = scopeId ?? "";
12202
12194
  const { keyIndex = -1, authFence = null } = opts;
12203
12195
  const clearAuthBench = opts.clearAuthBench === true && authFence !== null;
12204
- const key = [keyType, keySuffix, scope, effectiveScopeId];
12205
12196
  return await getDbClient().transaction(async (tx) => {
12197
+ // The key's rows in every scope: `getAvailableKeyIndices` reads them all by suffix.
12206
12198
  const result = await tx.run(
12207
12199
  `UPDATE api_key_status
12208
12200
  SET status = 'available', rateLimitedUntil = NULL, updatedAt = ?
12209
- WHERE keyType = ? AND keySuffix = ? AND scope = ? AND scopeId = ?
12201
+ WHERE keyType = ? AND keySuffix = ?
12210
12202
  AND status = 'rate_limited' AND ((? AND authFailureSeq <= ?) OR consecutiveAuthFailures < ?)`,
12211
- [now, ...key, clearAuthBench ? 1 : 0, authFence, CODEX_AUTH_FAILURE_BENCH_THRESHOLD],
12203
+ [
12204
+ now,
12205
+ keyType,
12206
+ keySuffix,
12207
+ clearAuthBench ? 1 : 0,
12208
+ authFence,
12209
+ CODEX_AUTH_FAILURE_BENCH_THRESHOLD,
12210
+ ],
12212
12211
  );
12213
12212
  const cleared = result.changes > 0;
12214
12213
  if (!clearAuthBench) return cleared;
12215
12214
  const lifted = await tx.query<{ keySuffix: string }>(
12216
12215
  `UPDATE api_key_status SET consecutiveAuthFailures = 0, updatedAt = ?1,
12217
12216
  status = CASE WHEN keySuffix = ?2 THEN status ELSE 'available' END, rateLimitedUntil = CASE WHEN keySuffix = ?2 THEN rateLimitedUntil END
12218
- WHERE keyType = ?3 AND scope = ?4 AND scopeId = ?5 AND (consecutiveAuthFailures > 0 OR status = 'rate_limited')
12219
- AND ((keySuffix = ?2 AND authFailureSeq <= ?7) OR (keySuffix != ?2 AND keyIndex = ?6)) RETURNING keySuffix`,
12217
+ WHERE keyType = ?3 AND (consecutiveAuthFailures > 0 OR status = 'rate_limited')
12218
+ AND ((keySuffix = ?2 AND authFailureSeq <= ?7)
12219
+ OR (keySuffix != ?2 AND keyIndex = ?6 AND scope = ?4 AND scopeId = ?5)) RETURNING keySuffix`,
12220
12220
  [now, keySuffix, keyType, scope, effectiveScopeId, keyIndex, authFence],
12221
12221
  );
12222
12222
  const retired = lifted.filter((row) => row.keySuffix !== keySuffix);
@@ -12997,6 +12997,7 @@ export async function listRecentSessions(
12997
12997
  opts: ListRecentSessionsOpts = {},
12998
12998
  ): Promise<SessionListItem[] | SessionListItemSummary[]> {
12999
12999
  const query = sessionSummaryQuery(opts);
13000
+ const threadRoots = threadRootsSql();
13000
13001
  const rootRows = await getDbClient().query<
13001
13002
  AgentTaskRow & {
13002
13003
  __chainCount: number;
@@ -13005,15 +13006,17 @@ export async function listRecentSessions(
13005
13006
  __readState: SessionReadState | null;
13006
13007
  __snoozeUntil: string | null;
13007
13008
  __needsYou: number;
13009
+ __threadRoots: number;
13008
13010
  }
13009
13011
  >(
13010
13012
  `SELECT r.*, a.chainCount AS __chainCount, a.lastActivityAt AS __lastActivityAt,
13011
13013
  a.latestStatus AS __latestStatus, a.readState AS __readState,
13012
- a.snoozeUntil AS __snoozeUntil, a.needsYou AS __needsYou
13014
+ a.snoozeUntil AS __snoozeUntil, a.needsYou AS __needsYou,
13015
+ ${threadRoots.sql} AS __threadRoots
13013
13016
  FROM (${query.sql}) a CROSS JOIN agent_tasks r ON r.id = a.root_id
13014
13017
  ORDER BY ${opts.triage === "later" ? "a.snoozeUntil ASC," : ""}
13015
13018
  a.lastActivityAt DESC, r.createdAt DESC, r.id DESC LIMIT ? OFFSET ?`,
13016
- [...query.params, opts.limit ?? 25, opts.offset ?? 0],
13019
+ [...threadRoots.params, ...query.params, opts.limit ?? 25, opts.offset ?? 0],
13017
13020
  );
13018
13021
  const fields = await sessionPageFields(
13019
13022
  rootRows.map((row) => ({
@@ -13029,7 +13032,7 @@ export async function listRecentSessions(
13029
13032
  ...(row.__readState ? { readState: row.__readState } : {}),
13030
13033
  snoozeUntil: row.__snoozeUntil,
13031
13034
  needsYou: !!row.__needsYou,
13032
- groupKey: sessionGroupKey(row.key),
13035
+ groupKey: sessionThreadGroupKey(row.key, row.__threadRoots),
13033
13036
  ...fields.get(row.id),
13034
13037
  }));
13035
13038
  }
@@ -1,4 +1,5 @@
1
1
  import OpenAI from "openai";
2
+ import { selectCredential } from "../../../utils/credentials";
2
3
  import { getOpenRouterAttributionHeaders } from "../../../utils/openrouter-base-url";
3
4
  import { DEFAULT_EMBEDDING_MODEL, EMBEDDING_DIMENSIONS } from "../constants";
4
5
  import type { EmbeddingProvider } from "../types";
@@ -46,7 +47,10 @@ export class OpenAIEmbeddingProvider implements EmbeddingProvider {
46
47
  private readonly apiKey: string | undefined;
47
48
 
48
49
  constructor(config?: OpenAIEmbeddingConfig) {
49
- this.apiKey = config?.apiKey ?? process.env.EMBEDDING_API_KEY ?? process.env.OPENAI_API_KEY;
50
+ const apiKey = config?.apiKey ?? process.env.EMBEDDING_API_KEY ?? process.env.OPENAI_API_KEY;
51
+ this.apiKey = apiKey
52
+ ? selectCredential(apiKey, undefined, "EMBEDDING_API_KEY").selected
53
+ : undefined;
50
54
 
51
55
  this.model = config?.model ?? process.env.EMBEDDING_MODEL ?? "text-embedding-3-small";
52
56
 
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Learnings captured through `store-progress`: one swarm-scope `manual` memory
3
+ * per learning, keyed `/learnings/<workstream>/<slug>` and tied to its task by
4
+ * `sourceTaskId`, so "what did we learn doing X" is one `keyPrefix` search.
5
+ *
6
+ * Source stays `manual` so nightly curation (memtax B1 reads only manual and
7
+ * file_index rows) picks learnings up without a new source value.
8
+ */
9
+ import { getDbClient } from "@/be/db";
10
+ import { indexMemoryContent } from "@/be/memory/index-content";
11
+ import { contentSha256 } from "@/commands/profile-sync";
12
+ import type { AgentTask } from "@/types";
13
+ import { scrubSecrets } from "@/utils/secret-scrubber";
14
+
15
+ /** ~500 tokens at ceil(chars / 4); also keeps a learning to one memory chunk. */
16
+ export const LEARNING_MAX_CHARS = 2000;
17
+ export const LEARNING_MAX_TAGS = 5;
18
+ const LEARNINGS_ROOT = "/learnings";
19
+ const SEGMENT_MAX_CHARS = 60;
20
+ const KEY_ATTEMPTS = 4;
21
+
22
+ export type TaskLearningInput = string | { text: string; workstream?: string; tags?: string[] };
23
+
24
+ export type TaskLearningResult =
25
+ | { status: "stored"; memoryId: string; key: string; workstream: string }
26
+ | { status: "replayed"; memoryId: string; key: string }
27
+ | { status: "failed"; error: string };
28
+
29
+ /** Lowercase kebab slug: `Agent Swarm!` -> `agent-swarm`. Empty when nothing usable is left. */
30
+ export function toKebab(value: string, maxChars = SEGMENT_MAX_CHARS): string {
31
+ return value
32
+ .normalize("NFKD")
33
+ .toLowerCase()
34
+ .replace(/[^a-z0-9]+/g, "-")
35
+ .replace(/^-+/, "")
36
+ .slice(0, maxChars)
37
+ .replace(/-+$/, "");
38
+ }
39
+
40
+ /** Explicit workstream, else the repo name from `vcsRepo` (`owner/name` or a URL), else `general`. */
41
+ export function resolveLearningWorkstream(
42
+ explicit: string | undefined,
43
+ vcsRepo: string | undefined,
44
+ ): string {
45
+ const fromExplicit = explicit ? toKebab(explicit) : "";
46
+ if (fromExplicit) return fromExplicit;
47
+ const repoName = vcsRepo
48
+ ?.trim()
49
+ .replace(/\/+$/, "")
50
+ .split("/")
51
+ .pop()
52
+ ?.replace(/\.git$/, "");
53
+ const fromRepo = repoName ? toKebab(repoName) : "";
54
+ return fromRepo || "general";
55
+ }
56
+
57
+ export async function storeTaskLearning(params: {
58
+ agentId: string;
59
+ isLead: boolean;
60
+ task: Pick<AgentTask, "id" | "vcsRepo">;
61
+ learning: TaskLearningInput;
62
+ }): Promise<TaskLearningResult> {
63
+ const { agentId, isLead, task } = params;
64
+ const input = typeof params.learning === "string" ? { text: params.learning } : params.learning;
65
+ // Scrub before anything is derived: a token in the title, workstream or a tag
66
+ // would otherwise survive (kebab-cased) in the key and tags, and the replay
67
+ // hash must match the hash of the scrubbed content the store persists.
68
+ const text = scrubSecrets(input.text).trim();
69
+ if (!text) return { status: "failed", error: "learning text is empty" };
70
+ if (text.length > LEARNING_MAX_CHARS) {
71
+ return {
72
+ status: "failed",
73
+ error: `learning text is over ~500 tokens (${LEARNING_MAX_CHARS} characters); shorten it`,
74
+ };
75
+ }
76
+ const tags = [
77
+ ...new Set((input.tags ?? []).map((tag) => toKebab(scrubSecrets(tag))).filter(Boolean)),
78
+ ].filter((tag) => tag !== "learning");
79
+ if (tags.length > LEARNING_MAX_TAGS) {
80
+ return { status: "failed", error: `learning takes at most ${LEARNING_MAX_TAGS} tags` };
81
+ }
82
+
83
+ const workstream = resolveLearningWorkstream(
84
+ input.workstream === undefined ? undefined : scrubSecrets(input.workstream),
85
+ task.vcsRepo === undefined ? undefined : scrubSecrets(task.vcsRepo),
86
+ );
87
+
88
+ try {
89
+ // One write transaction for the replay check and the insert, so two
90
+ // overlapping calls with the same learning store it once.
91
+ return await getDbClient().transaction(() =>
92
+ insertLearning({ agentId, isLead, taskId: task.id, text, tags, workstream }),
93
+ );
94
+ } catch (err) {
95
+ return {
96
+ status: "failed",
97
+ error: scrubSecrets(err instanceof Error ? err.message : String(err)),
98
+ };
99
+ }
100
+ }
101
+
102
+ async function insertLearning(params: {
103
+ agentId: string;
104
+ isLead: boolean;
105
+ taskId: string;
106
+ text: string;
107
+ tags: string[];
108
+ workstream: string;
109
+ }): Promise<TaskLearningResult> {
110
+ const { agentId, isLead, taskId, text, tags, workstream } = params;
111
+ // indexMemoryContent stores text this short as one trimmed chunk, so this
112
+ // hash is the row's contentHash: a replayed call finds its own earlier write.
113
+ const contentHash = contentSha256(text);
114
+ const replay = await getDbClient().get<{ id: string; key: string }>(
115
+ `SELECT id, key FROM agent_memory
116
+ WHERE sourceTaskId = ? AND contentHash = ? AND key LIKE '${LEARNINGS_ROOT}/%'
117
+ LIMIT 1`,
118
+ [taskId, contentHash],
119
+ );
120
+ if (replay) return { status: "replayed", memoryId: replay.id, key: replay.key };
121
+
122
+ const title = text.split(/\r?\n/, 1)[0]!.trim().slice(0, 200);
123
+ const baseKey = `${LEARNINGS_ROOT}/${workstream}/${toKebab(title) || "learning"}`;
124
+
125
+ // Keys are unique per owner and a reused key fails rather than upserts.
126
+ // Keep the path unique across the swarm: on a taken key, add a short suffix.
127
+ for (let attempt = 0; attempt < KEY_ATTEMPTS; attempt++) {
128
+ const key = attempt === 0 ? baseKey : `${baseKey}-${crypto.randomUUID().slice(0, 6)}`;
129
+ const taken = await getDbClient().get<{ id: string }>(
130
+ "SELECT id FROM agent_memory WHERE scope = 'swarm' AND key = ? LIMIT 1",
131
+ [key],
132
+ );
133
+ if (taken) continue;
134
+ try {
135
+ const result = await indexMemoryContent({
136
+ agentId,
137
+ content: text,
138
+ name: title,
139
+ scope: "swarm",
140
+ source: "manual",
141
+ sourceTaskId: taskId,
142
+ tags: ["learning", ...tags],
143
+ intent: "store-progress learning",
144
+ key,
145
+ writer: { principal: { kind: "agent", agentId, isLead }, source: "mcp" },
146
+ });
147
+ return { status: "stored", memoryId: result.memoryIds[0]!, key, workstream };
148
+ } catch (err) {
149
+ // The key is taken after all: try a suffixed one.
150
+ if (/UNIQUE constraint failed/i.test((err as Error).message)) continue;
151
+ throw err;
152
+ }
153
+ }
154
+ return { status: "failed", error: `no free key under ${baseKey}` };
155
+ }
@@ -0,0 +1,4 @@
1
+ -- Last provider rejection of a pooled key by a worker live test (HTTP 401).
2
+ -- A key with lastValidationFailedAt set is left out of /api/keys/available.
3
+ ALTER TABLE api_key_status ADD COLUMN lastValidationError TEXT;
4
+ ALTER TABLE api_key_status ADD COLUMN lastValidationFailedAt TEXT;