@desplega.ai/agent-swarm 2.6.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (324) hide show
  1. package/README.md +2 -0
  2. package/dist/{acp-adapter-dgfakfpw.js → acp-adapter-8dn5tq56.js} +6 -6
  3. package/dist/{actions-fbqma8aa.js → actions-3r6gw7dy.js} +18 -18
  4. package/dist/{amp-adapter-en0jy6hs.js → amp-adapter-wjtq413p.js} +3 -3
  5. package/dist/{anthropic-messages-txs190dd.js → anthropic-messages-n4cby1rz.js} +4 -4
  6. package/dist/{app-k8a5cgpp.js → app-d53yhykk.js} +3 -3
  7. package/dist/{assistant-a16433gr.js → assistant-ksrawj3k.js} +20 -20
  8. package/dist/{boot-reembed-6c1sjfnr.js → boot-reembed-fxfw17m2.js} +11 -3
  9. package/dist/{boot-reembed-f4nn561n.js → boot-reembed-k5y5xbj0.js} +15 -8
  10. package/dist/{boot-scrub-sweep-d4w9gajf.js → boot-scrub-sweep-se41j9fd.js} +3 -3
  11. package/dist/{boot-seeds-9bmkspyw.js → boot-seeds-r3kwwv8b.js} +4 -4
  12. package/dist/{claude-adapter-nys1kjxa.js → claude-adapter-ahmyf231.js} +8 -8
  13. package/dist/{claude-managed-adapter-55207973.js → claude-managed-adapter-5knamegc.js} +3 -3
  14. package/dist/{claude-managed-setup-3cm35gey.js → claude-managed-setup-mdc7dy6b.js} +3 -3
  15. package/dist/{claude-sdk-session-nw5yd8cp.js → claude-sdk-session-n5wen9zk.js} +8729 -8600
  16. package/dist/{cli-683y0b3a.js → cli-0dqtv2jg.js} +2 -2
  17. package/dist/{cli-a34pr3w1.js → cli-0tww7dnz.js} +2 -2
  18. package/dist/{cli-w2656ka1.js → cli-0w4e4v58.js} +7 -7
  19. package/dist/{cli-5sckpqcx.js → cli-2sjm3t4r.js} +1 -1
  20. package/dist/{cli-gf2850q7.js → cli-3cywb44q.js} +18 -1
  21. package/dist/{cli-8wkb09q5.js → cli-3k9jej0c.js} +2 -2
  22. package/dist/{cli-3aqd4jaa.js → cli-3v2cyqb8.js} +3 -3
  23. package/dist/{cli-emhjcj2y.js → cli-5qt9vzmq.js} +89 -20
  24. package/dist/{cli-abkm71vr.js → cli-5rp1w4cr.js} +1 -1
  25. package/dist/{cli-gf95q3bm.js → cli-6vwjsetx.js} +4 -4
  26. package/dist/{cli-k21gx5ht.js → cli-6w2yrrc0.js} +10 -10
  27. package/dist/{cli-m70bajzr.js → cli-75tdydgv.js} +1 -1
  28. package/dist/{cli-5rj83bam.js → cli-8feswf2v.js} +47 -36
  29. package/dist/{cli-sxgtf5fw.js → cli-8wd3mbyj.js} +1 -1
  30. package/dist/{cli-ca6banwz.js → cli-9csfthbw.js} +10 -8
  31. package/dist/{cli-dqygeeh5.js → cli-b41p764a.js} +31 -3
  32. package/dist/{cli-m5a9m0ak.js → cli-c1t533hh.js} +3 -3
  33. package/dist/{cli-2sjhpsns.js → cli-c28jxpcv.js} +1 -1
  34. package/dist/{cli-bqztfyve.js → cli-cb50g9cd.js} +1 -1
  35. package/dist/{cli-ag2yrnt5.js → cli-d8gaphtr.js} +2 -2
  36. package/dist/{cli-9abdgn5t.js → cli-dgtfysne.js} +3 -3
  37. package/dist/{cli-nq677csy.js → cli-dscnhn18.js} +1 -1
  38. package/dist/{cli-3fwqy71j.js → cli-dv11pgpv.js} +39 -32
  39. package/dist/{cli-v49fkghp.js → cli-ekptgk2m.js} +6 -6
  40. package/dist/{cli-c2habvsf.js → cli-f3mzy662.js} +17 -17
  41. package/dist/{cli-zqne1qcj.js → cli-fk7k8s9x.js} +2 -2
  42. package/dist/{cli-1tem6ns9.js → cli-gt4h8cxw.js} +3 -2
  43. package/dist/{cli-vbhgkw9h.js → cli-h3e7bhvn.js} +3 -3
  44. package/dist/{cli-ffvsj2qw.js → cli-hkxqdmnb.js} +3 -3
  45. package/dist/{cli-a2rr0fmg.js → cli-hpkvgdad.js} +7 -4
  46. package/dist/{cli-art7c1zh.js → cli-hq2v4rr5.js} +37 -9
  47. package/dist/{cli-sqma6vw1.js → cli-k1zb7raw.js} +5 -5
  48. package/dist/{cli-4rvr7zfx.js → cli-k4awgvzm.js} +5 -0
  49. package/dist/{cli-3ndmpxxv.js → cli-k7d3hvya.js} +2 -2
  50. package/dist/{cli-3gfvmzht.js → cli-kbtdftbw.js} +74 -26
  51. package/dist/{cli-hfeb68z4.js → cli-kj69b9tw.js} +34 -6
  52. package/dist/{cli-az25fn5f.js → cli-kk9jy4nm.js} +2 -2
  53. package/dist/{cli-39aet655.js → cli-m52t4cta.js} +1 -1
  54. package/dist/{cli-cz37jqss.js → cli-mdadewqp.js} +5 -5
  55. package/dist/{cli-esakngh8.js → cli-nf6k60sn.js} +1 -1
  56. package/dist/{cli-j6rktc91.js → cli-nzdf09js.js} +1 -1
  57. package/dist/{cli-rr7zx19j.js → cli-prcyzecg.js} +12 -12
  58. package/dist/{cli-4hxrf0p4.js → cli-pzafyqtk.js} +393 -62
  59. package/dist/{cli-g8ka5j4j.js → cli-qvaa1gjp.js} +173 -52
  60. package/dist/{cli-d2dpf95n.js → cli-rg4b2g6j.js} +1 -1
  61. package/dist/{cli-kf4jree9.js → cli-rm66vqdz.js} +1 -1
  62. package/dist/{cli-4rf0f5vg.js → cli-s504c3w7.js} +5 -3
  63. package/dist/{cli-nn6vx2mq.js → cli-swkx9jm2.js} +7 -5
  64. package/dist/{cli-g20jjwt4.js → cli-thsaq4pd.js} +16 -16
  65. package/dist/{cli-aj8djyqt.js → cli-twjnyvs8.js} +2 -2
  66. package/dist/{cli-03gg9pd1.js → cli-typdpe8g.js} +4 -4
  67. package/dist/{cli-mtnj4fwa.js → cli-vg8pwmsd.js} +6 -6
  68. package/dist/{cli-gnsb1zxa.js → cli-vhpckzvp.js} +1 -1
  69. package/dist/{cli-7enjwya9.js → cli-vz78s5wm.js} +1 -1
  70. package/dist/{cli-dv97kdzv.js → cli-wdxgsdz5.js} +2 -2
  71. package/dist/{cli-7a6p4e65.js → cli-wtjg6je3.js} +1 -1
  72. package/dist/{cli-6y51ryva.js → cli-x3cngb1h.js} +44 -12
  73. package/dist/{cli-n7asmce6.js → cli-xbz6mcg4.js} +3 -3
  74. package/dist/{cli-24j1jeeb.js → cli-y4tdxsqb.js} +3 -3
  75. package/dist/{cli-mvjz5an3.js → cli-yqrsp3ny.js} +7 -7
  76. package/dist/cli.js +31 -21
  77. package/dist/{codex-adapter-d95q4qsn.js → codex-adapter-7wepzq15.js} +6 -6
  78. package/dist/{codex-hook-brpnycn5.js → codex-hook-pbdh4xhx.js} +2 -2
  79. package/dist/{codex-session-runner-gdwd8h0c.js → codex-session-runner-02sb49e4.js} +6 -6
  80. package/dist/{commands-jqmgk54b.js → commands-3rcspxct.js} +2 -2
  81. package/dist/{controller-z3jq589f.js → controller-8ar4avd6.js} +3 -3
  82. package/dist/{controllers-connect-r2za39ef.js → controllers-connect-ajvh9h0v.js} +19 -19
  83. package/dist/{cursor-adapter-4mr4jgkw.js → cursor-adapter-gd0sa7bh.js} +1 -1
  84. package/dist/{db-w36wwb5g.js → db-dv9j7ts5.js} +2 -2
  85. package/dist/{db-ppv2tze7.js → db-z26vgdvj.js} +3 -3
  86. package/dist/{dispatcher-pv5h77vf.js → dispatcher-8m0htjq8.js} +17 -17
  87. package/dist/{dsh-adapter-d4pdpb3r.js → dsh-adapter-y8gyk130.js} +1 -1
  88. package/dist/{e2b-fn3zacvt.js → e2b-74vg9b7e.js} +1 -1
  89. package/dist/{e2b-222hsajc.js → e2b-jyay8wzn.js} +1 -1
  90. package/dist/{easter-egg-3d-4sp4dvmc.js → easter-egg-3d-28f880xx.js} +1 -1
  91. package/dist/{execute-jev839y1.js → execute-9seb79vh.js} +1 -1
  92. package/dist/{external-channel-y6nkzhnn.js → external-channel-smz7jyth.js} +2 -2
  93. package/dist/{getMachineId-bsd-brn0xym3.js → getMachineId-bsd-whfre6gr.js} +3 -3
  94. package/dist/{getMachineId-darwin-t339sfxm.js → getMachineId-darwin-5zjynb19.js} +3 -3
  95. package/dist/{getMachineId-linux-5eq95d8b.js → getMachineId-linux-8mksrrsq.js} +3 -3
  96. package/dist/{getMachineId-unsupported-qavrtc7j.js → getMachineId-unsupported-fxtqsdt2.js} +3 -3
  97. package/dist/{getMachineId-win-q7fe1wx7.js → getMachineId-win-nzfmz857.js} +3 -3
  98. package/dist/{grok-adapter-t08b9ntm.js → grok-adapter-z0nnvtbn.js} +7 -7
  99. package/dist/handlers-pspryh6g.js +41 -0
  100. package/dist/{heartbeat-vn31xwdk.js → heartbeat-f928aqyp.js} +20 -20
  101. package/dist/{hook-cpjs8q51.js → hook-d7c605r4.js} +7 -7
  102. package/dist/{hook-6d0zbvvt.js → hook-q9hv5xf9.js} +5 -5
  103. package/dist/{http-9c6medda.js → http-q6j4k989.js} +1000 -251
  104. package/dist/{index-k0yt1tvq.js → index-5nb266jr.js} +17 -17
  105. package/dist/{index-ysd45kgc.js → index-8fxt5eah.js} +10 -10
  106. package/dist/{index-zgvqzjsw.js → index-cekzwhez.js} +5 -5
  107. package/dist/{index-38t35h0n.js → index-ch0c1xff.js} +17 -17
  108. package/dist/{index-pkma1yf8.js → index-ebnjhxh1.js} +1 -1
  109. package/dist/{index-yqgbfgaj.js → index-ed8dy8rn.js} +20 -20
  110. package/dist/{index-qhjf2htm.js → index-ejqdkkqx.js} +262 -96
  111. package/dist/{index-dr94zbnh.js → index-farm1zsn.js} +32661 -29443
  112. package/dist/{index-g6k9tctj.js → index-yn1yxg85.js} +1 -1
  113. package/dist/{keepalive-em0117bp.js → keepalive-zzc3hks1.js} +3 -3
  114. package/dist/lead-c0pjcaw4.js +79 -0
  115. package/dist/{maintenance-kbme3qy9.js → maintenance-kqsxzpzx.js} +1 -1
  116. package/dist/{oauth-refresh-sweep-xysj4yse.js → oauth-refresh-sweep-7qt0g55z.js} +8 -3
  117. package/dist/{onboard-p6q0vz97.js → onboard-k7x13fqg.js} +121 -8
  118. package/dist/{opencode-adapter-esv3k9yn.js → opencode-adapter-tqvrtter.js} +5 -5
  119. package/dist/{otel-impl-54qhqh1z.js → otel-impl-sqkf6k3b.js} +97 -97
  120. package/dist/{pi-mono-adapter-mymbwhj0.js → pi-mono-adapter-m2qnafxm.js} +89 -14
  121. package/dist/{pricing-refresh-yb75wv33.js → pricing-refresh-wkpwhgvs.js} +3 -3
  122. package/dist/{rbac-roles-dexhc9x8.js → rbac-roles-dpn0c0wj.js} +7 -4
  123. package/dist/{rbac-roles-1vp2mw4p.js → rbac-roles-wk7xd6a6.js} +2 -2
  124. package/dist/{render-v2-tjay5hhf.js → render-v2-ex2m4g2k.js} +3 -3
  125. package/dist/{resume-8ayf37g5.js → resume-725g9b0b.js} +7 -7
  126. package/dist/{runtime-hqe5cqcd.js → runtime-kajas15g.js} +1 -1
  127. package/dist/{session-access-fdhp987s.js → session-access-0j2ezjje.js} +8 -4
  128. package/dist/{session-source-backfill-qgtc86z1.js → session-source-backfill-62202awe.js} +2 -2
  129. package/dist/{setup-4rrmqcq3.js → setup-9sj11ax6.js} +1 -1
  130. package/dist/{virtual-modules-52km00d6.js → virtual-modules-c6fcq6zr.js} +2 -2
  131. package/dist/{work-objects-p28fgd70.js → work-objects-0y07495y.js} +20 -20
  132. package/dist/worker-d4q9sqbc.js +79 -0
  133. package/openapi.json +1536 -407
  134. package/package.json +7 -5
  135. package/src/be/automation-preflight.ts +2 -0
  136. package/src/be/connector-codes.ts +65 -22
  137. package/src/be/db/api-keys.ts +9 -0
  138. package/src/be/db/workflow-runs.ts +36 -5
  139. package/src/be/db.ts +80 -22
  140. package/src/be/memory/boot-reembed.ts +4 -1
  141. package/src/be/memory/constants.ts +6 -0
  142. package/src/be/memory/embedding-recovery.ts +48 -0
  143. package/src/be/memory/index-content.ts +5 -2
  144. package/src/be/memory/providers/openai-embedding.ts +65 -14
  145. package/src/be/memory/task-learning.ts +257 -0
  146. package/src/be/memory/types.ts +13 -2
  147. package/src/be/migrations/245_script_executor.sql +3 -0
  148. package/src/be/migrations/246_connector_code_kind.sql +11 -0
  149. package/src/be/oauth-refresh-sweep.ts +2 -1
  150. package/src/be/onboarding.ts +125 -15
  151. package/src/be/rbac-roles.ts +4 -1
  152. package/src/be/scripts/boot-reembed.ts +2 -0
  153. package/src/be/scripts/db.ts +14 -4
  154. package/src/be/scripts/embeddings.ts +4 -2
  155. package/src/be/scripts/run-saved.ts +1 -0
  156. package/src/be/scripts/typecheck.ts +9 -3
  157. package/src/be/seed/agent-fs-provision.ts +75 -14
  158. package/src/be/seed-skills/index.ts +7 -0
  159. package/src/be/swarm-config-guard.ts +3 -0
  160. package/src/cli.tsx +9 -0
  161. package/src/comb/config.ts +6 -1
  162. package/src/commands/onboard/agent-fs-version.ts +5 -0
  163. package/src/commands/onboard/compose-generator.ts +117 -4
  164. package/src/commands/onboard/env-generator.ts +9 -0
  165. package/src/commands/onboard/tei-image.ts +12 -0
  166. package/src/commands/onboard/types.ts +7 -0
  167. package/src/commands/onboard.tsx +11 -1
  168. package/src/commands/provider-credentials.ts +2 -1
  169. package/src/commands/runner.ts +14 -1
  170. package/src/heartbeat/heartbeat.ts +14 -1
  171. package/src/heartbeat/script-runner-probe.ts +47 -0
  172. package/src/http/agentfs-proxy.ts +369 -0
  173. package/src/http/all-routes.ts +1 -0
  174. package/src/http/api-keys.ts +24 -9
  175. package/src/http/codex-oauth-device.ts +8 -1
  176. package/src/http/fs.ts +64 -10
  177. package/src/http/idempotency-key.ts +1 -1
  178. package/src/http/inbox-state.ts +27 -1
  179. package/src/http/index.ts +7 -4
  180. package/src/http/memory.ts +7 -4
  181. package/src/http/onboarding.ts +139 -11
  182. package/src/http/pages-public.ts +38 -5
  183. package/src/http/scripts.ts +27 -0
  184. package/src/http/session-access.ts +24 -0
  185. package/src/http/sessions.ts +10 -6
  186. package/src/http/tasks.ts +85 -11
  187. package/src/http/telemetry-ui.ts +1 -1
  188. package/src/http/user-roles.ts +4 -1
  189. package/src/http/users.ts +272 -3
  190. package/src/http/utils.ts +22 -3
  191. package/src/http/workflows.ts +10 -0
  192. package/src/http/x.ts +5 -0
  193. package/src/prompts/base-prompt.ts +45 -10
  194. package/src/prompts/session-templates.ts +43 -5
  195. package/src/providers/amp-adapter.ts +1 -1
  196. package/src/providers/codex-adapter.ts +6 -1
  197. package/src/providers/cursor-adapter.ts +1 -1
  198. package/src/providers/dsh-adapter.ts +1 -1
  199. package/src/providers/pi-mono-adapter.ts +99 -14
  200. package/src/rbac/legacy-policy.ts +1 -0
  201. package/src/rbac/permissions.ts +4 -0
  202. package/src/scheduler/scheduler.ts +2 -1
  203. package/src/scripts-runtime/eval-harness.ts +8 -1
  204. package/src/scripts-runtime/executors/native.ts +22 -16
  205. package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
  206. package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
  207. package/src/scripts-runtime/executors/quickjs.ts +7 -0
  208. package/src/scripts-runtime/executors/registry.ts +32 -0
  209. package/src/scripts-runtime/executors/types.ts +7 -0
  210. package/src/scripts-runtime/host-validated-args.ts +133 -0
  211. package/src/scripts-runtime/loader.ts +30 -3
  212. package/src/scripts-runtime/swarm-sdk.ts +9 -2
  213. package/src/scripts-runtime/types/stdlib.d.ts +9 -1
  214. package/src/scripts-runtime/types/swarm-sdk.d.ts +9 -1
  215. package/src/tests/agent-fs-provision-seeder.test.ts +261 -92
  216. package/src/tests/agentfs-proxy.test.ts +1106 -0
  217. package/src/tests/api-key-status-by-suffix.test.ts +242 -0
  218. package/src/tests/base-prompt.test.ts +139 -4
  219. package/src/tests/codex-adapter.test.ts +46 -8
  220. package/src/tests/codex-oauth-device.test.ts +12 -1
  221. package/src/tests/comb-config-guard.test.ts +20 -0
  222. package/src/tests/comb-links.test.ts +79 -1
  223. package/src/tests/comb-status.test.ts +44 -0
  224. package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
  225. package/src/tests/credential-check.test.ts +4 -2
  226. package/src/tests/docs-index-footprint.test.js +3 -3
  227. package/src/tests/docs-index.test.ts +135 -0
  228. package/src/tests/dsh-adapter.test.ts +1 -1
  229. package/src/tests/fetch-resolved-env.test.ts +16 -2
  230. package/src/tests/fixtures/onboard/baseline-states.ts +68 -0
  231. package/src/tests/fixtures/onboard/content.compose.yml.txt +190 -0
  232. package/src/tests/fixtures/onboard/content.env.txt +29 -0
  233. package/src/tests/fixtures/onboard/dev.compose.yml.txt +155 -0
  234. package/src/tests/fixtures/onboard/dev.env.txt +28 -0
  235. package/src/tests/fixtures/onboard/full-all.compose.yml.txt +582 -0
  236. package/src/tests/fixtures/onboard/full-all.env.txt +59 -0
  237. package/src/tests/fixtures/onboard/full.compose.yml.txt +435 -0
  238. package/src/tests/fixtures/onboard/full.env.txt +36 -0
  239. package/src/tests/fixtures/onboard/research.compose.yml.txt +155 -0
  240. package/src/tests/fixtures/onboard/research.env.txt +28 -0
  241. package/src/tests/fixtures/onboard/solo.compose.yml.txt +85 -0
  242. package/src/tests/fixtures/onboard/solo.env.txt +26 -0
  243. package/src/tests/heartbeat-checklist.test.ts +89 -0
  244. package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
  245. package/src/tests/http-log-scrubbing.test.ts +34 -1
  246. package/src/tests/http-task-write-ownership.test.ts +5 -1
  247. package/src/tests/invites-http.test.ts +464 -0
  248. package/src/tests/memory-embedding-input-cap.test.ts +122 -1
  249. package/src/tests/memory-embedding-recovery.test.ts +137 -0
  250. package/src/tests/oauth-refresh-sweep.test.ts +34 -1
  251. package/src/tests/onboard-compose.test.ts +162 -0
  252. package/src/tests/onboard-env.test.ts +24 -0
  253. package/src/tests/onboard-non-interactive.test.ts +29 -0
  254. package/src/tests/onboard-tei-image.test.ts +36 -0
  255. package/src/tests/onboarding.test.ts +451 -4
  256. package/src/tests/openai-embedding-provider.test.ts +105 -2
  257. package/src/tests/opencode-adapter.test.ts +2 -1
  258. package/src/tests/pages-frame-ancestors.test.ts +162 -0
  259. package/src/tests/pi-codemode.test.ts +2 -1
  260. package/src/tests/pi-mono-adapter.test.ts +54 -0
  261. package/src/tests/pi-mono-session-cost-on-abort.test.ts +270 -0
  262. package/src/tests/pi-native-mcp.test.ts +13 -1
  263. package/src/tests/pi-tool-exposure.test.ts +8 -8
  264. package/src/tests/preload-env.test.ts +4 -0
  265. package/src/tests/preload.ts +4 -0
  266. package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
  267. package/src/tests/prompt-template-session.test.ts +20 -3
  268. package/src/tests/rbac-admission-e2e.test.ts +22 -6
  269. package/src/tests/rbac-admission.test.ts +342 -4
  270. package/src/tests/rbac-engine.test.ts +1 -0
  271. package/src/tests/rbac-roles.test.ts +9 -1
  272. package/src/tests/request-composition-derive.test.ts +329 -0
  273. package/src/tests/script-connections.test.ts +2 -0
  274. package/src/tests/script-executor-conformance.test.ts +36 -1
  275. package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
  276. package/src/tests/script-executor-quickjs.test.ts +140 -0
  277. package/src/tests/script-host-validated-args-native.test.ts +196 -0
  278. package/src/tests/script-per-script-executor.test.ts +415 -0
  279. package/src/tests/scripts-external-api.test.ts +45 -0
  280. package/src/tests/sdk-allowlist.test.ts +3 -2
  281. package/src/tests/self-improvement.test.ts +4 -4
  282. package/src/tests/send-task-requester-gate.test.ts +64 -3
  283. package/src/tests/sessions-move.test.ts +39 -1
  284. package/src/tests/store-progress-learning.test.ts +381 -0
  285. package/src/tests/ui-logs-parser.test.ts +180 -0
  286. package/src/tests/whoami-route.test.ts +41 -0
  287. package/src/tests/workflow-engine-v2.test.ts +116 -1
  288. package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
  289. package/src/tests/workflow-run-finished-event.test.ts +263 -0
  290. package/src/tests/workflow-swarm-script.test.ts +34 -0
  291. package/src/tools/inject-learning.ts +1 -1
  292. package/src/tools/memory-edit.ts +1 -1
  293. package/src/tools/memory-search.ts +1 -1
  294. package/src/tools/script-common.ts +1 -0
  295. package/src/tools/script-run.ts +7 -0
  296. package/src/tools/script-upsert.ts +7 -0
  297. package/src/tools/store-progress.ts +71 -1
  298. package/src/tools/utils.ts +12 -1
  299. package/src/tools/workflows/trigger-workflow.ts +104 -10
  300. package/src/types.ts +12 -1
  301. package/src/utils/constants.ts +39 -0
  302. package/src/utils/credentials.ts +5 -2
  303. package/src/utils/task-attachment-links.ts +15 -3
  304. package/src/workflows/engine.ts +18 -16
  305. package/src/workflows/executors/swarm-script.ts +5 -0
  306. package/src/workflows/foreach-driver.ts +5 -11
  307. package/src/workflows/node-timeout.ts +30 -0
  308. package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
  309. package/templates/community/linear-delivery-loop/README.md +38 -0
  310. package/templates/community/linear-delivery-loop/config.example.json +31 -0
  311. package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
  312. package/templates/community/linear-delivery-loop/schedule.json +10 -0
  313. package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
  314. package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
  315. package/templates/skills/swarm-docs/config.json +14 -0
  316. package/templates/skills/swarm-docs/content.md +144 -0
  317. package/dist/handlers-eqsnmv95.js +0 -41
  318. package/dist/lead-23e7z59t.js +0 -79
  319. package/dist/worker-e3w7vye1.js +0 -79
  320. package/dist/{azure-openai-responses-9kfwhmn7.js → azure-openai-responses-43asa23s.js} +0 -0
  321. package/dist/{node-vxx896fa.js → node-t28cc56b.js} +0 -0
  322. package/dist/{openai-codex-responses-79s8xnbh.js → openai-codex-responses-jhr3sjtm.js} +3 -3
  323. package/dist/{openai-completions-tdbyts0x.js → openai-completions-gs6yn37k.js} +3 -3
  324. /package/dist/{openai-responses-dm3t5vqp.js → openai-responses-jqm9vwwk.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "2.6.0",
3
+ "version": "2.7.1",
4
4
  "description": "agent-swarm.dev — an open-source operating system for AI work: a lead agent breaks goals into tasks, routes them to specialized workers such as Claude Code or Codex, runs each worker in an isolated container, and preserves shared memory, tools, schedules, and review gates so delegated work compounds across sessions.",
5
5
  "keywords": [
6
6
  "agent-swarm.dev",
@@ -80,6 +80,8 @@
80
80
  "sync:ai-toolbox-skills": "bun scripts/sync-ai-toolbox-skills.ts",
81
81
  "check:ai-toolbox-skills": "bun scripts/sync-ai-toolbox-skills.ts --check",
82
82
  "check:skill-sources": "bun scripts/check-skill-sources.ts",
83
+ "build:docs-index": "bun scripts/build-docs-index.ts",
84
+ "check:docs-index": "bun scripts/build-docs-index.ts --check",
83
85
  "check:operator-skill": "bun scripts/check-operator-skill.ts",
84
86
  "sync:telemetry-contract": "bun scripts/sync-telemetry-contract.ts",
85
87
  "check:telemetry-contract": "bun scripts/sync-telemetry-contract.ts --check",
@@ -185,7 +187,7 @@
185
187
  "@oxlint/plugins": "1.78.0",
186
188
  "@types/bun": "latest",
187
189
  "dependency-cruiser": "^18.0.0",
188
- "happy-dom": "^20.14.5",
190
+ "happy-dom": "^21.0.4",
189
191
  "konsistent": "1.0.0-beta.1",
190
192
  "openapi-typescript": "^7",
191
193
  "oxlint": "1.78.0",
@@ -201,7 +203,7 @@
201
203
  "dependencies": {
202
204
  "@agentclientprotocol/sdk": "2.0.2",
203
205
  "@ai-sdk/openai": "^3.0.41",
204
- "@anthropic-ai/claude-agent-sdk": "0.3.310",
206
+ "@anthropic-ai/claude-agent-sdk": "0.3.311",
205
207
  "@anthropic-ai/sdk": "^1.3.0",
206
208
  "@asteasolutions/zod-to-openapi": "^8.0.0",
207
209
  "@aws-sdk/client-bedrock": "3.1048.0",
@@ -209,8 +211,8 @@
209
211
  "@chat-adapter/slack": "4.41.1",
210
212
  "@chat-adapter/teams": "4.41.1",
211
213
  "@chat-adapter/telegram": "4.41.1",
212
- "@cursor/sdk": "1.0.40",
213
- "@desplega.ai/agent-fs-just-bash": "0.18.5",
214
+ "@cursor/sdk": "1.1.0",
215
+ "@desplega.ai/agent-fs-just-bash": "0.18.9",
214
216
  "@desplega.ai/business-use": "^0.4.2",
215
217
  "@earendil-works/pi-agent-core": "^1.4.0",
216
218
  "@earendil-works/pi-ai": "^1.4.0",
@@ -3,6 +3,7 @@ import { getSlackConfiguration, isSlackConfigured } from "../slack/config";
3
3
  import { isEnvFlagEnabled } from "../utils/env-flag";
4
4
  import { scrubSecrets } from "../utils/secret-scrubber";
5
5
  import { getDbClient } from "./db";
6
+ import { emitRunFinished } from "./db/workflow-runs";
6
7
  import { getOAuthApp, getOAuthTokens } from "./db-queries/oauth";
7
8
  import { scrubJsonValue } from "./scrub-json";
8
9
 
@@ -446,6 +447,7 @@ export async function recordWorkflowPreflightFailure(input: {
446
447
  timestamp,
447
448
  ],
448
449
  );
450
+ emitRunFinished(runId, input.workflowId, "failed");
449
451
  // Claimed in this transaction, which also sees every child failure run
450
452
  // committed before it: concurrent failures alert exactly once a day.
451
453
  return {
@@ -1,21 +1,34 @@
1
1
  /**
2
- * Single-use connect codes for the agent-swarm.dev ChatGPT connector.
2
+ * Single-use codes that a browser or connector trades once for an `aswt_` token.
3
3
  *
4
- * The dashboard asks for a code (`POST /api/users/:id/connector-codes`) and
5
- * opens the connector's connect page with it. The connector trades the code
6
- * once (`POST /api/connector/exchange`) for a user-bound `aswt_` token. The
7
- * token is minted only at exchange time, through the regular `mintToken`
8
- * path, so an unused code never leaves a token behind.
4
+ * Two kinds share the `connector_codes` table:
5
+ *
6
+ * - `connector`: the agent-swarm.dev ChatGPT connector. The dashboard asks for
7
+ * a code (`POST /api/users/:id/connector-codes`) and opens the connector's
8
+ * connect page with it. The connector trades the code once
9
+ * (`POST /api/connector/exchange`).
10
+ * - `invite`: a teammate invite (`POST /api/invites`). The teammate's browser
11
+ * trades it once (`POST /api/invites/exchange`). The exchange also flips the
12
+ * user from `invited` to `active`, because `aswt_` auth admits only active
13
+ * users.
14
+ *
15
+ * The token is minted only at exchange time, through the regular `mintToken`
16
+ * path, so an unused code never leaves a token behind. Each exchange redeems
17
+ * only its own kind.
9
18
  */
10
19
 
11
20
  import { createHash, randomBytes } from "node:crypto";
12
21
  import { getDbClient } from "./db";
13
- import { type IdentityActor, mintToken } from "./users";
22
+ import { type IdentityActor, mintToken, recordIdentityEvent } from "./users";
23
+
24
+ export type ConnectorCodeKind = "connector" | "invite";
14
25
 
15
26
  export const CONNECTOR_CODE_TTL_MS = 10 * 60 * 1000;
27
+ export const INVITE_CODE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
16
28
  /** Expired rows stay this long before the heartbeat sweep deletes them. */
17
29
  const CONNECTOR_CODE_RETENTION_MS = 60 * 60 * 1000;
18
30
  export const DEFAULT_CONNECTOR_CODE_LABEL = "ChatGPT connector";
31
+ export const DEFAULT_INVITE_CODE_LABEL = "Dashboard invite";
19
32
 
20
33
  function sha256Hex(input: string): string {
21
34
  return createHash("sha256").update(input).digest("hex");
@@ -27,27 +40,34 @@ export async function createConnectorCode(
27
40
  label: string,
28
41
  actor: IdentityActor,
29
42
  now = new Date(),
43
+ opts: { kind?: ConnectorCodeKind; ttlMs?: number } = {},
30
44
  ): Promise<{ code: string; expiresAt: string }> {
45
+ const kind = opts.kind ?? "connector";
46
+ const ttlMs = opts.ttlMs ?? CONNECTOR_CODE_TTL_MS;
31
47
  const code = randomBytes(32).toString("base64url");
32
- const expiresAt = new Date(now.getTime() + CONNECTOR_CODE_TTL_MS).toISOString();
48
+ const expiresAt = new Date(now.getTime() + ttlMs).toISOString();
33
49
  await getDbClient().run(
34
50
  `INSERT INTO connector_codes
35
- (code_hash, user_id, label, actor_kind, actor_id, created_at, expires_at)
36
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
37
- [sha256Hex(code), userId, label, actor.kind, actor.id, now.toISOString(), expiresAt],
51
+ (code_hash, user_id, label, actor_kind, actor_id, created_at, expires_at, kind)
52
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
53
+ [sha256Hex(code), userId, label, actor.kind, actor.id, now.toISOString(), expiresAt, kind],
38
54
  );
39
55
  return { code, expiresAt };
40
56
  }
41
57
 
42
58
  /**
43
59
  * Consume a code and mint its token in one transaction. Returns null when the
44
- * code is unknown, expired, or already used (callers must not tell these apart).
60
+ * code is unknown, expired, already used, or of another kind (callers must not
61
+ * tell these apart). For an invite code, also returns null when the user is
62
+ * no longer `invited` (suspended or merged since): the code is spent and no
63
+ * token is minted.
45
64
  */
46
65
  export async function exchangeConnectorCode(
47
66
  code: string,
48
- now = new Date(),
67
+ opts: { kind?: ConnectorCodeKind; now?: Date } = {},
49
68
  ): Promise<{ token: string; userId: string } | null> {
50
- const nowIso = now.toISOString();
69
+ const kind = opts.kind ?? "connector";
70
+ const nowIso = (opts.now ?? new Date()).toISOString();
51
71
  return getDbClient().transaction(async (tx) => {
52
72
  const row = await tx.get<{
53
73
  user_id: string;
@@ -56,21 +76,44 @@ export async function exchangeConnectorCode(
56
76
  actor_id: string;
57
77
  }>(
58
78
  `UPDATE connector_codes SET used_at = ?
59
- WHERE code_hash = ? AND used_at IS NULL AND expires_at > ?
79
+ WHERE code_hash = ? AND used_at IS NULL AND expires_at > ? AND kind = ?
60
80
  RETURNING user_id, label, actor_kind, actor_id`,
61
- [nowIso, sha256Hex(code), nowIso],
81
+ [nowIso, sha256Hex(code), nowIso, kind],
62
82
  );
63
83
  if (!row) return null;
64
- const { plaintext } = await mintToken(
65
- row.user_id,
66
- row.label,
67
- { kind: row.actor_kind, id: row.actor_id },
68
- { source: "connector_exchange" },
69
- );
84
+ const actor: IdentityActor = { kind: row.actor_kind, id: row.actor_id };
85
+
86
+ if (kind === "invite") {
87
+ const activated = await tx.run(
88
+ "UPDATE users SET status = 'active', lastUpdatedAt = ? WHERE id = ? AND status = 'invited'",
89
+ [nowIso, row.user_id],
90
+ );
91
+ if (activated.changes === 0) return null;
92
+ await recordIdentityEvent(
93
+ row.user_id,
94
+ "status_changed",
95
+ actor,
96
+ { status: "invited" },
97
+ { status: "active", source: "invite_exchange" },
98
+ );
99
+ }
100
+
101
+ const { plaintext } = await mintToken(row.user_id, row.label, actor, {
102
+ source: kind === "invite" ? "invite_exchange" : "connector_exchange",
103
+ });
70
104
  return { token: plaintext, userId: row.user_id };
71
105
  });
72
106
  }
73
107
 
108
+ /** Delete a user's unused invite codes, so a re-invite leaves one live link. */
109
+ export async function deleteUnusedInviteCodes(userId: string): Promise<number> {
110
+ const result = await getDbClient().run(
111
+ "DELETE FROM connector_codes WHERE user_id = ? AND kind = 'invite' AND used_at IS NULL",
112
+ [userId],
113
+ );
114
+ return result.changes;
115
+ }
116
+
74
117
  /** Delete codes that expired more than 1h ago. Returns the number of deleted rows. */
75
118
  export async function deleteExpiredConnectorCodes(now = new Date()): Promise<number> {
76
119
  const cutoff = new Date(now.getTime() - CONNECTOR_CODE_RETENTION_MS).toISOString();
@@ -110,6 +110,15 @@ export async function recordKeyValidation(
110
110
  now,
111
111
  ],
112
112
  );
113
+ // A passing test clears the key's rejection in every scope, since the
114
+ // suffix read in `getAvailableKeyIndices` matches them all.
115
+ if (key.ok) {
116
+ await tx.run(
117
+ `UPDATE api_key_status SET lastValidationError = NULL, lastValidationFailedAt = NULL, updatedAt = ?
118
+ WHERE keyType = ? AND keySuffix = ? AND lastValidationFailedAt IS NOT NULL`,
119
+ [now, keyType, key.keySuffix],
120
+ );
121
+ }
113
122
  }
114
123
  if (options.retireAbsent === false) return;
115
124
  const placeholders = keys.map(() => "?").join(", ");
@@ -465,7 +465,8 @@ export async function disarmWorkflowTreeRetries(
465
465
  /**
466
466
  * Mark the given runs that are still running or waiting `cancelled`, without
467
467
  * the per-run `workflow.child.finished` wake-up: it is for runs whose parent
468
- * step the same cancel closes. A terminal run keeps its status and reason.
468
+ * step the same cancel closes. Each run it cancels still emits
469
+ * `workflow.run.finished`. A terminal run keeps its status and reason.
469
470
  */
470
471
  export async function cancelLiveWorkflowTreeRuns(
471
472
  runIds: string[],
@@ -473,12 +474,14 @@ export async function cancelLiveWorkflowTreeRuns(
473
474
  finishedAt: string,
474
475
  ): Promise<void> {
475
476
  await forEachChunk(runIds, async (chunk, placeholders) => {
476
- await getDbClient().run(
477
+ const cancelled = await getDbClient().query<{ id: string; workflowId: string }>(
477
478
  `UPDATE workflow_runs
478
479
  SET status = 'cancelled', error = ?, finishedAt = ?, lastUpdatedAt = ?
479
- WHERE id IN (${placeholders}) AND status IN ('running', 'waiting')`,
480
+ WHERE id IN (${placeholders}) AND status IN ('running', 'waiting')
481
+ RETURNING id, workflowId`,
480
482
  [scrubSecrets(reason), finishedAt, finishedAt, ...chunk],
481
483
  );
484
+ for (const run of cancelled) emitRunFinished(run.id, run.workflowId, "cancelled");
482
485
  });
483
486
  }
484
487
 
@@ -643,14 +646,42 @@ export function emitChildRunFinished(
643
646
  }
644
647
 
645
648
  function emitAfterCommit(childRunId: string, parentStepId: string): void {
649
+ emitBusEventAfterCommit("workflow.child.finished", { childRunId, parentStepId });
650
+ }
651
+
652
+ /** Bus event every run emits once when it turns terminal. */
653
+ export const WORKFLOW_RUN_FINISHED_EVENT = "workflow.run.finished";
654
+
655
+ export function isTerminalWorkflowRunStatus(status: WorkflowRunStatus): boolean {
656
+ return TERMINAL_STATUSES.has(status);
657
+ }
658
+
659
+ /**
660
+ * Announce after commit that a run turned terminal, top-level and child runs
661
+ * alike, so an event-mode `wait` can resume when any run ends. Callers emit
662
+ * only when a live run (running or waiting) turns terminal, never on a later
663
+ * update of a run that is already terminal. The id is `runId`, never `workflowRunId` or
664
+ * `_runId`: those would put the finished run in the waiting run's own `run`
665
+ * scope, so waiters use scope `global` and filter on `runId`.
666
+ */
667
+ export function emitRunFinished(
668
+ runId: string,
669
+ workflowId: string,
670
+ status: WorkflowRunStatus,
671
+ ): void {
672
+ if (!TERMINAL_STATUSES.has(status)) return;
673
+ emitBusEventAfterCommit(WORKFLOW_RUN_FINISHED_EVENT, { runId, workflowId, status });
674
+ }
675
+
676
+ function emitBusEventAfterCommit(event: string, payload: Record<string, unknown>): void {
646
677
  getDbClient().afterCommit(() => {
647
678
  import("../../workflows/event-bus")
648
679
  .then(({ workflowEventBus }) => {
649
- workflowEventBus.emit("workflow.child.finished", { childRunId, parentStepId });
680
+ workflowEventBus.emit(event, payload);
650
681
  })
651
682
  .catch((err) =>
652
683
  console.error(
653
- "[db] workflow.child.finished event not emitted:",
684
+ `[db] ${event} event not emitted:`,
654
685
  scrubSecrets(err instanceof Error ? err.message : String(err)),
655
686
  ),
656
687
  );
package/src/be/db.ts CHANGED
@@ -227,8 +227,10 @@ import { configureTaskWriteDependencies, failTask } from "./db/tasks/write";
227
227
  import { tombstoneWorkflowIdempotencyKeys } from "./db/workflow-idempotency";
228
228
  import {
229
229
  emitChildRunFinished,
230
+ emitRunFinished,
230
231
  getRunLineageBlock,
231
232
  getStepExecutionBlock,
233
+ isTerminalWorkflowRunStatus,
232
234
  rowToWorkflowRun,
233
235
  rowToWorkflowRunStep,
234
236
  WORKFLOW_RUN_SUMMARY_COLUMNS,
@@ -7994,16 +7996,33 @@ export async function updateWorkflowRun(
7994
7996
  updates.push("lastUpdatedAt = ?");
7995
7997
  params.push(new Date().toISOString());
7996
7998
  params.push(id);
7997
- const row = await getDbClient().get<WorkflowRunRow>(
7998
- `UPDATE workflow_runs SET ${updates.join(", ")} WHERE id = ? RETURNING *`,
7999
- params,
8000
- );
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()];
8001
8017
  if (!row) return null;
8002
8018
  const run = rowToWorkflowRun(row);
8003
8019
  if (data.status === "completed" || data.status === "failed") {
8004
8020
  emitWorkflowTerminalTelemetry(run);
8005
8021
  }
8006
8022
  if (row.parentStepId) emitChildRunFinished(id, row.parentStepId, run.status);
8023
+ if (endsRun && previousStatus && !isTerminalWorkflowRunStatus(previousStatus)) {
8024
+ emitRunFinished(id, run.workflowId, run.status);
8025
+ }
8007
8026
  return run;
8008
8027
  }
8009
8028
 
@@ -11721,6 +11740,14 @@ export interface AvailableKeyIndicesResult {
11721
11740
  * whose `plan` cannot run the family (`planAllowsModelFamily`) is excluded
11722
11741
  * and reported in `seatBlockedIndices`, also when it is key-wide or
11723
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`.
11724
11751
  */
11725
11752
  export async function getAvailableKeyIndices(
11726
11753
  keyType: string,
@@ -11728,41 +11755,58 @@ export async function getAvailableKeyIndices(
11728
11755
  scope = "global",
11729
11756
  scopeId: string | null = null,
11730
11757
  modelFamily?: ModelFamily,
11758
+ keySuffixes?: string[],
11731
11759
  ): Promise<AvailableKeyIndicesResult> {
11732
11760
  const now = new Date().toISOString();
11733
11761
  const nowMs = Date.now();
11734
11762
  const client = getDbClient();
11735
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];
11736
11769
 
11737
11770
  // Auto-clear expired rate limits. Never an auth bench: only `clearAuthBench` lifts it.
11738
11771
  await client.run(
11739
11772
  `UPDATE api_key_status
11740
11773
  SET status = 'available', rateLimitedUntil = NULL, updatedAt = ?
11741
- WHERE keyType = ? AND scope = ? AND scopeId = ?
11774
+ WHERE ${rowFilter}
11742
11775
  AND status = 'rate_limited' AND rateLimitedUntil <= ? AND consecutiveAuthFailures < ?`,
11743
- [now, keyType, scope, effectiveScopeId, now, CODEX_AUTH_FAILURE_BENCH_THRESHOLD],
11776
+ [now, ...rowParams, now, CODEX_AUTH_FAILURE_BENCH_THRESHOLD],
11744
11777
  );
11745
11778
 
11746
11779
  const rows = await client.query<{
11747
11780
  keyIndex: number;
11781
+ keySuffix: string;
11782
+ scope: string;
11783
+ scopeId: string;
11748
11784
  status: string;
11749
11785
  rateLimitWindows: string | null;
11750
11786
  plan: string | null;
11751
11787
  authFailureSeq: number;
11752
11788
  lastValidationFailedAt: string | null;
11753
11789
  }>(
11754
- `SELECT keyIndex, status, rateLimitWindows, plan, authFailureSeq, lastValidationFailedAt FROM api_key_status
11755
- WHERE keyType = ? AND scope = ? AND scopeId = ?`,
11756
- [keyType, scope, effectiveScopeId],
11757
- );
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
+ };
11758
11804
 
11759
11805
  const blockedIndices = new Set(
11760
- rows.filter((r) => r.status === "rate_limited").map((r) => r.keyIndex),
11806
+ rows.filter((r) => r.status === "rate_limited").flatMap(positionsOf),
11761
11807
  );
11762
11808
  const rejectedIndices = [
11763
- ...new Set(
11764
- rows.filter((row) => row.lastValidationFailedAt !== null).map((row) => row.keyIndex),
11765
- ),
11809
+ ...new Set(rows.filter((row) => row.lastValidationFailedAt !== null).flatMap(positionsOf)),
11766
11810
  ];
11767
11811
  for (const index of rejectedIndices) blockedIndices.add(index);
11768
11812
 
@@ -11773,14 +11817,17 @@ export async function getAvailableKeyIndices(
11773
11817
  // A key already blocked key-wide doesn't need a separate model-block
11774
11818
  // entry — it's excluded from availableIndices either way, and
11775
11819
  // modelBlockedIndices means "excluded only by a model-scoped block".
11776
- 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;
11777
11824
  const block = activeModelBlock(
11778
11825
  parseRateLimitWindowsJson(row.rateLimitWindows),
11779
11826
  modelFamily,
11780
11827
  nowMs,
11781
11828
  );
11782
11829
  if (!block) continue;
11783
- modelBlockedIndices.push(row.keyIndex);
11830
+ modelBlockedIndices.push(...positions);
11784
11831
  if (earliestModelResetsAtSec === undefined || block.resetsAt < earliestModelResetsAtSec) {
11785
11832
  earliestModelResetsAtSec = block.resetsAt;
11786
11833
  }
@@ -11794,7 +11841,10 @@ export async function getAvailableKeyIndices(
11794
11841
  const seatBlockedIndices: number[] = [];
11795
11842
  if (modelFamily) {
11796
11843
  for (const row of rows) {
11797
- 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
+ }
11798
11848
  }
11799
11849
  }
11800
11850
  const seatBlockedSet = new Set(seatBlockedIndices);
@@ -12143,22 +12193,30 @@ export async function clearKeyRateLimit(
12143
12193
  const effectiveScopeId = scopeId ?? "";
12144
12194
  const { keyIndex = -1, authFence = null } = opts;
12145
12195
  const clearAuthBench = opts.clearAuthBench === true && authFence !== null;
12146
- const key = [keyType, keySuffix, scope, effectiveScopeId];
12147
12196
  return await getDbClient().transaction(async (tx) => {
12197
+ // The key's rows in every scope: `getAvailableKeyIndices` reads them all by suffix.
12148
12198
  const result = await tx.run(
12149
12199
  `UPDATE api_key_status
12150
12200
  SET status = 'available', rateLimitedUntil = NULL, updatedAt = ?
12151
- WHERE keyType = ? AND keySuffix = ? AND scope = ? AND scopeId = ?
12201
+ WHERE keyType = ? AND keySuffix = ?
12152
12202
  AND status = 'rate_limited' AND ((? AND authFailureSeq <= ?) OR consecutiveAuthFailures < ?)`,
12153
- [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
+ ],
12154
12211
  );
12155
12212
  const cleared = result.changes > 0;
12156
12213
  if (!clearAuthBench) return cleared;
12157
12214
  const lifted = await tx.query<{ keySuffix: string }>(
12158
12215
  `UPDATE api_key_status SET consecutiveAuthFailures = 0, updatedAt = ?1,
12159
12216
  status = CASE WHEN keySuffix = ?2 THEN status ELSE 'available' END, rateLimitedUntil = CASE WHEN keySuffix = ?2 THEN rateLimitedUntil END
12160
- WHERE keyType = ?3 AND scope = ?4 AND scopeId = ?5 AND (consecutiveAuthFailures > 0 OR status = 'rate_limited')
12161
- 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`,
12162
12220
  [now, keySuffix, keyType, scope, effectiveScopeId, keyIndex, authFence],
12163
12221
  );
12164
12222
  const retired = lifted.filter((row) => row.keySuffix !== keySuffix);
@@ -56,7 +56,10 @@ export async function runBootReembed(): Promise<void> {
56
56
  for (let i = 0; i < rows.length; i += BATCH_SIZE) {
57
57
  const batch = rows.slice(i, i + BATCH_SIZE);
58
58
  try {
59
- const embeddings = await provider.embedBatch(batch.map((m) => m.content));
59
+ const embeddings = await provider.embedBatch(
60
+ batch.map((m) => m.content),
61
+ { kind: "document" },
62
+ );
60
63
  for (let j = 0; j < embeddings.length; j++) {
61
64
  if (embeddings[j]) {
62
65
  await store.updateEmbedding(batch[j]!.id, embeddings[j]!, provider.name);
@@ -72,6 +72,12 @@ export function minSimilarity(): number {
72
72
  return numEnv("MEMORY_MIN_SIMILARITY", 0.1);
73
73
  }
74
74
 
75
+ // Raw cosine at or above which a store-progress learning is a near duplicate of
76
+ // an existing swarm memory and is not stored. Measured on production
77
+ // `/learnings` rows: a confirmed twin pair scored 0.877, the closest distinct
78
+ // pair 0.705.
79
+ export const LEARNING_DUPLICATE_MIN_SIMILARITY = 0.8;
80
+
75
81
  // Reranking parameters
76
82
  export function accessBoostMaxMultiplier(): number {
77
83
  return numEnv("MEMORY_ACCESS_BOOST_MAX", 1.5);
@@ -0,0 +1,48 @@
1
+ /**
2
+ * A memory or script written while the embeddings endpoint is down keeps no
3
+ * vector, and the backfills (`runBootReembed`, `runBootReembedScripts`) run
4
+ * only at boot or when a config reload turns embeddings on. When an embedding
5
+ * call works again after a failed one (for example, a local TEI restarted),
6
+ * run both backfills once more, so those rows do not wait for the next API
7
+ * restart.
8
+ */
9
+
10
+ let failedSinceBackfill = false;
11
+ let backfill: Promise<void> | null = null;
12
+
13
+ /** An embedding call failed (network, server error, rejected request). */
14
+ export function noteEmbeddingFailure(): void {
15
+ failedSinceBackfill = true;
16
+ }
17
+
18
+ /** An embedding call returned a vector. Starts the backfill after a failure. */
19
+ export function noteEmbeddingSuccess(): void {
20
+ if (!failedSinceBackfill || backfill) return;
21
+ failedSinceBackfill = false;
22
+ const report = (what: string) => (err: unknown) => {
23
+ console.error(
24
+ `[memory] ${what} backfill after embeddings recovered failed:`,
25
+ (err as Error).message,
26
+ );
27
+ };
28
+ backfill = Promise.all([
29
+ import("./boot-reembed").then(({ runBootReembed }) => runBootReembed()).catch(report("Memory")),
30
+ import("../scripts/boot-reembed")
31
+ .then(({ runBootReembedScripts }) => runBootReembedScripts())
32
+ .catch(report("Script")),
33
+ ])
34
+ .then(() => undefined)
35
+ .finally(() => {
36
+ backfill = null;
37
+ });
38
+ }
39
+
40
+ /** The backfill started by the last recovery, if it still runs. For tests. */
41
+ export function pendingRecoveryBackfill(): Promise<void> | null {
42
+ return backfill;
43
+ }
44
+
45
+ /** Forget a failure seen by an earlier test. */
46
+ export function resetEmbeddingRecoveryForTests(): void {
47
+ failedSinceBackfill = false;
48
+ }
@@ -104,7 +104,7 @@ export async function indexMemoryContent(
104
104
  intent: "re-index memory source path",
105
105
  changedByAgentId: agentId,
106
106
  });
107
- const embedding = await provider.embed(result.memory.content);
107
+ const embedding = await provider.embed(result.memory.content, { kind: "document" });
108
108
  if (embedding) await store.updateEmbedding(result.memory.id, embedding, provider.name);
109
109
  try {
110
110
  // Re-index of an existing memory: prune stale content-derived links.
@@ -162,7 +162,10 @@ export async function indexMemoryContent(
162
162
  // Async batch embed (fire and forget)
163
163
  (async () => {
164
164
  try {
165
- const embeddings = await provider.embedBatch(memories.map((m) => m.content));
165
+ const embeddings = await provider.embedBatch(
166
+ memories.map((m) => m.content),
167
+ { kind: "document" },
168
+ );
166
169
  for (let i = 0; i < embeddings.length; i++) {
167
170
  if (embeddings[i]) {
168
171
  await store.updateEmbedding(memories[i]!.id, embeddings[i]!, provider.name);