@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
@@ -17,7 +17,12 @@ import {
17
17
  OnboardingStateSchema,
18
18
  updateOnboardingAiFromCodexDevice,
19
19
  } from "../be/onboarding";
20
- import { _resetAutoReloadForTests, handleCore } from "../http/core";
20
+ import {
21
+ __resetInjectedEnvTracking,
22
+ _resetAutoReloadForTests,
23
+ handleCore,
24
+ loadGlobalConfigsIntoEnv,
25
+ } from "../http/core";
21
26
  import { handleOnboarding } from "../http/onboarding";
22
27
  import { getPathSegments, parseQueryParams } from "../http/utils";
23
28
  import { telemetry } from "../telemetry";
@@ -32,12 +37,16 @@ const ENV_KEYS = [
32
37
  "EMBEDDING_API_KEY",
33
38
  "EMBEDDING_API_BASE_URL",
34
39
  "EMBEDDING_MODEL",
40
+ "EMBEDDING_QUERY_PREFIX",
41
+ "EMBEDDING_DOCUMENT_PREFIX",
35
42
  "GITHUB_TOKEN",
36
43
  "GITLAB_TOKEN",
37
44
  "BITBUCKET_TOKEN",
38
45
  "SLACK_BOT_TOKEN",
39
46
  "SLACK_APP_TOKEN",
40
47
  "SLACK_MODE",
48
+ "COMB_ENABLED",
49
+ "AGENT_FS_API_URL",
41
50
  ];
42
51
 
43
52
  let server: Server;
@@ -176,9 +185,22 @@ function embeddingResponse(dimensions: number): Response {
176
185
 
177
186
  describe("onboarding state", () => {
178
187
  test("fresh install creates active onboarding state", async () => {
188
+ process.env.COMB_ENABLED = "true";
189
+ process.env.AGENT_FS_API_URL = "http://agent-fs.example.test";
179
190
  const response = await request("/api/onboarding");
180
191
  expect(response.status).toBe(200);
181
- const body = (await response.json()) as { state: OnboardingState };
192
+ const body = (await response.json()) as {
193
+ state: OnboardingState;
194
+ signals: { files: { enabled: boolean } };
195
+ };
196
+
197
+ expect(body.signals.files).toEqual({ enabled: true });
198
+ expect(body.state.steps.files).toEqual({
199
+ status: "todo",
200
+ at: null,
201
+ method: null,
202
+ errorClass: null,
203
+ });
182
204
 
183
205
  expect(body.state.autoCompleted).toBe(false);
184
206
  expect(body.state.completedAt).toBeNull();
@@ -235,6 +257,7 @@ describe("onboarding state", () => {
235
257
  null,
236
258
  null,
237
259
  null,
260
+ null,
238
261
  ]);
239
262
  });
240
263
 
@@ -273,6 +296,39 @@ describe("onboarding state", () => {
273
296
  expect(capturedTelemetry.filter((entry) => entry.event === "started")).toHaveLength(0);
274
297
  });
275
298
 
299
+ test("a row stored before the files step back-fills it as todo", async () => {
300
+ process.env.COMB_ENABLED = "true";
301
+ process.env.AGENT_FS_API_URL = "http://agent-fs.example.test";
302
+ const state = (await (await request("/api/onboarding")).json()) as {
303
+ state: OnboardingState;
304
+ };
305
+ const stored = structuredClone(state.state) as unknown as {
306
+ startedAt: string;
307
+ steps: Record<string, unknown>;
308
+ };
309
+ stored.steps.integrations = {
310
+ status: "skipped",
311
+ at: stored.startedAt,
312
+ method: null,
313
+ errorClass: null,
314
+ };
315
+ delete stored.steps.files;
316
+ await upsertSwarmConfig({
317
+ scope: "global",
318
+ key: "onboarding_state",
319
+ value: JSON.stringify(stored),
320
+ });
321
+
322
+ const body = (await (await request("/api/onboarding")).json()) as { state: OnboardingState };
323
+ expect(body.state.steps.integrations.status).toBe("skipped");
324
+ expect(body.state.steps.files).toEqual({
325
+ status: "todo",
326
+ at: null,
327
+ method: null,
328
+ errorClass: null,
329
+ });
330
+ });
331
+
276
332
  test("a user row marks the install as existing", async () => {
277
333
  await createUser({ name: "Existing operator" });
278
334
  await expectExistingInstall();
@@ -321,15 +377,69 @@ describe("onboarding state", () => {
321
377
  test("GET derives the authenticated connection", async () => {
322
378
  const body = (await (await request("/api/onboarding")).json()) as {
323
379
  state: OnboardingState;
324
- signals: { providers: unknown[]; embeddings: { configured: boolean; dimensions: number } };
380
+ signals: { providers: unknown[]; embeddings: Record<string, unknown> };
325
381
  };
326
382
 
327
383
  expect(body.state.steps.connect).toMatchObject({ status: "done", method: "api_key" });
328
384
  expect(body.signals.providers).toEqual([]);
329
- expect(body.signals.embeddings).toEqual({ configured: false, dimensions: 512 });
385
+ expect(body.signals.embeddings).toEqual({
386
+ configured: false,
387
+ dimensions: 512,
388
+ model: null,
389
+ local: false,
390
+ provider: null,
391
+ localDefault: null,
392
+ });
330
393
  expect((await getState()).steps.connect.status).toBe("done");
331
394
  });
332
395
 
396
+ test("embedding signals name the model and say whether the endpoint is local", async () => {
397
+ const cases: Array<[string | undefined, boolean, string]> = [
398
+ ["http://tei:80/v1", true, "local"],
399
+ ["http://localhost:8080/v1", true, "local"],
400
+ ["http://127.0.0.1:8091/v1", true, "local"],
401
+ ["http://10.0.4.2/v1", true, "local"],
402
+ ["http://[::1]:8080/v1", true, "local"],
403
+ ["https://api.openai.com/v1", false, "openai"],
404
+ ["https://openrouter.ai/api/v1", false, "openrouter"],
405
+ ["https://ai-gateway.vercel.sh/v1", false, "vercel"],
406
+ ["https://my-resource.services.ai.azure.com/openai/v1", false, "azure"],
407
+ ["http://[2001:db8::1]/v1", false, "custom"],
408
+ [undefined, false, "openai"],
409
+ ];
410
+ process.env.EMBEDDING_API_KEY = "local-tei";
411
+ process.env.EMBEDDING_MODEL = "nomic-ai/nomic-embed-text-v1.5";
412
+ for (const [baseUrl, local, provider] of cases) {
413
+ if (baseUrl === undefined) delete process.env.EMBEDDING_API_BASE_URL;
414
+ else process.env.EMBEDDING_API_BASE_URL = baseUrl;
415
+ const body = (await (await request("/api/onboarding")).json()) as {
416
+ signals: { embeddings: Record<string, unknown> };
417
+ };
418
+ expect({ baseUrl, embeddings: body.signals.embeddings }).toEqual({
419
+ baseUrl,
420
+ embeddings: {
421
+ configured: true,
422
+ dimensions: 512,
423
+ model: "nomic-ai/nomic-embed-text-v1.5",
424
+ local,
425
+ provider,
426
+ // The environment is the deployment's: a local endpoint is its local default.
427
+ localDefault: local ? { model: "nomic-ai/nomic-embed-text-v1.5" } : null,
428
+ },
429
+ });
430
+ // The signal never carries the key or the URL.
431
+ expect(JSON.stringify(body.signals.embeddings)).not.toContain("local-tei");
432
+ if (baseUrl) expect(JSON.stringify(body.signals.embeddings)).not.toContain(baseUrl);
433
+ }
434
+
435
+ // No model set: the provider's default model.
436
+ delete process.env.EMBEDDING_MODEL;
437
+ const fallback = (await (await request("/api/onboarding")).json()) as {
438
+ signals: { embeddings: { model: string | null } };
439
+ };
440
+ expect(fallback.signals.embeddings.model).toBe("text-embedding-3-small");
441
+ });
442
+
333
443
  test("agent signals count idle or busy agents as ready", async () => {
334
444
  await createAgent({ name: "ready-lead", isLead: true, status: "idle" });
335
445
  await createAgent({ name: "busy-worker", isLead: false, status: "busy" });
@@ -580,7 +690,67 @@ describe("onboarding state", () => {
580
690
  ).toBe(404);
581
691
  });
582
692
 
693
+ test("without Comb the files step reads as skipped, unpersisted and with no event", async () => {
694
+ const first = (await (await request("/api/onboarding")).json()) as {
695
+ state: OnboardingState;
696
+ signals: { files: { enabled: boolean } };
697
+ };
698
+ expect(first.signals.files).toEqual({ enabled: false });
699
+ expect(first.state.steps.files.status).toBe("skipped");
700
+ // A write for another step persists the row: files must stay todo in storage.
701
+ expect((await put({ action: "skip", step: "name" })).status).toBe(200);
702
+ expect((await getState()).steps.files.status).toBe("todo");
703
+ await Bun.sleep(10);
704
+ expect(
705
+ capturedTelemetry.filter(
706
+ (entry) => entry.event === "step_skipped" && entry.properties.step === "files",
707
+ ),
708
+ ).toHaveLength(0);
709
+
710
+ // Turning Comb on brings the step back.
711
+ process.env.COMB_ENABLED = "true";
712
+ process.env.AGENT_FS_API_URL = "http://agent-fs.example.test";
713
+ const after = (await (await request("/api/onboarding")).json()) as { state: OnboardingState };
714
+ expect(after.state.steps.files.status).toBe("todo");
715
+ });
716
+
717
+ test("PUT completes the files step with created or connected", async () => {
718
+ process.env.COMB_ENABLED = "true";
719
+ process.env.AGENT_FS_API_URL = "http://agent-fs.example.test";
720
+ await request("/api/onboarding");
721
+ capturedTelemetry.length = 0;
722
+
723
+ const created = await put({ action: "complete", step: "files", method: "created" });
724
+ expect(created.status).toBe(200);
725
+ expect(((await created.json()) as { state: OnboardingState }).state.steps.files).toMatchObject({
726
+ status: "done",
727
+ method: "created",
728
+ });
729
+ await Bun.sleep(10);
730
+ expect(capturedTelemetry).toContainEqual(
731
+ expect.objectContaining({
732
+ event: "step_completed",
733
+ properties: expect.objectContaining({ step: "files", method: "created" }),
734
+ }),
735
+ );
736
+
737
+ const connected = await put({ action: "complete", step: "files", method: "connected" });
738
+ expect(connected.status).toBe(200);
739
+ expect((await getState()).steps.files).toMatchObject({ status: "done", method: "connected" });
740
+ });
741
+
742
+ test("a completed files step stays done when Comb is off", async () => {
743
+ process.env.COMB_ENABLED = "true";
744
+ process.env.AGENT_FS_API_URL = "http://agent-fs.example.test";
745
+ await put({ action: "complete", step: "files", method: "connected" });
746
+ delete process.env.COMB_ENABLED;
747
+ const body = (await (await request("/api/onboarding")).json()) as { state: OnboardingState };
748
+ expect(body.state.steps.files).toMatchObject({ status: "done", method: "connected" });
749
+ });
750
+
583
751
  test("PUT rejects a bad completion method and connect skip", async () => {
752
+ expect((await put({ action: "complete", step: "files", method: "pasted" })).status).toBe(400);
753
+ expect((await put({ action: "complete", step: "files", method: "api_key" })).status).toBe(400);
584
754
  expect((await put({ action: "complete", step: "ai", method: "not-a-provider" })).status).toBe(
585
755
  400,
586
756
  );
@@ -1044,6 +1214,68 @@ describe("onboarding memory probe", () => {
1044
1214
  }
1045
1215
  });
1046
1216
 
1217
+ test("existing preset probes a private configured endpoint outside local development", async () => {
1218
+ let hits = 0;
1219
+ const fake = startEmbeddingServer(() => {
1220
+ hits += 1;
1221
+ return embeddingResponse(512);
1222
+ });
1223
+ const originalNodeEnv = process.env.NODE_ENV;
1224
+ process.env.NODE_ENV = "production";
1225
+ process.env.EMBEDDING_API_BASE_URL = fake.baseUrl;
1226
+ process.env.EMBEDDING_MODEL = "test-embedding-model";
1227
+ process.env.EMBEDDING_API_KEY = "local-tei";
1228
+ try {
1229
+ const existing = await request("/api/onboarding/memory", {
1230
+ method: "POST",
1231
+ body: { preset: "existing" },
1232
+ });
1233
+ expect(await existing.json()).toMatchObject({ ok: true, dimensions: 512 });
1234
+ expect(hits).toBe(1);
1235
+
1236
+ // The same private URL from the request body is still blocked.
1237
+ const custom = await request("/api/onboarding/memory", {
1238
+ method: "POST",
1239
+ body: {
1240
+ preset: "custom",
1241
+ baseUrl: fake.baseUrl,
1242
+ model: "test-embedding-model",
1243
+ apiKey: "local-tei",
1244
+ },
1245
+ });
1246
+ expect(await custom.json()).toMatchObject({
1247
+ ok: false,
1248
+ errorClass: "network",
1249
+ error: "The endpoint URL is not allowed.",
1250
+ });
1251
+ expect(hits).toBe(1);
1252
+ } finally {
1253
+ fake.server.stop(true);
1254
+ if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
1255
+ else process.env.NODE_ENV = originalNodeEnv;
1256
+ }
1257
+ });
1258
+
1259
+ test("existing preset with no configured endpoint keeps the SSRF check", async () => {
1260
+ const originalNodeEnv = process.env.NODE_ENV;
1261
+ process.env.NODE_ENV = "production";
1262
+ process.env.EMBEDDING_API_KEY = "local-tei";
1263
+ try {
1264
+ const response = await request("/api/onboarding/memory", {
1265
+ method: "POST",
1266
+ body: { preset: "existing", baseUrl: "http://127.0.0.1:9/v1" },
1267
+ });
1268
+ expect(await response.json()).toMatchObject({
1269
+ ok: false,
1270
+ errorClass: "network",
1271
+ error: "The endpoint URL is not allowed.",
1272
+ });
1273
+ } finally {
1274
+ if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
1275
+ else process.env.NODE_ENV = originalNodeEnv;
1276
+ }
1277
+ });
1278
+
1047
1279
  test("successful memory retry records the successful preset after a failure", async () => {
1048
1280
  let dimensions = 512;
1049
1281
  const fake = startEmbeddingServer(() => embeddingResponse(dimensions));
@@ -1090,3 +1322,218 @@ describe("onboarding memory probe", () => {
1090
1322
  });
1091
1323
  });
1092
1324
  });
1325
+
1326
+ describe("onboarding memory: the deployment's local setup", () => {
1327
+ const NOMIC = "nomic-ai/nomic-embed-text-v1.5";
1328
+
1329
+ /** The bundled install: a local endpoint, its model, a key, and nomic's task prefixes. */
1330
+ function setDeploymentEnv(localUrl: string) {
1331
+ process.env.EMBEDDING_API_BASE_URL = localUrl;
1332
+ process.env.EMBEDDING_MODEL = NOMIC;
1333
+ process.env.EMBEDDING_API_KEY = "local-tei";
1334
+ process.env.EMBEDDING_QUERY_PREFIX = "search_query: ";
1335
+ process.env.EMBEDDING_DOCUMENT_PREFIX = "search_document: ";
1336
+ }
1337
+
1338
+ async function storedEmbeddingRows(): Promise<Record<string, string>> {
1339
+ const rows = await getSwarmConfigs({ scope: "global" });
1340
+ return Object.fromEntries(
1341
+ rows.filter((row) => row.key.startsWith("EMBEDDING_")).map((row) => [row.key, row.value]),
1342
+ );
1343
+ }
1344
+
1345
+ async function embeddingSignals(): Promise<Record<string, unknown>> {
1346
+ const body = (await (await request("/api/onboarding")).json()) as {
1347
+ signals: { embeddings: Record<string, unknown> };
1348
+ };
1349
+ return body.signals.embeddings;
1350
+ }
1351
+
1352
+ test("a hosted save stores empty prefixes over the local model's prefixes", async () => {
1353
+ const hosted = startEmbeddingServer(() => embeddingResponse(512));
1354
+ setDeploymentEnv("http://tei:80/v1");
1355
+ try {
1356
+ const response = await request("/api/onboarding/memory", {
1357
+ method: "POST",
1358
+ body: {
1359
+ preset: "custom",
1360
+ baseUrl: hosted.baseUrl,
1361
+ model: "test-embedding-model",
1362
+ apiKey: "example-hosted-key",
1363
+ },
1364
+ });
1365
+ expect(await response.json()).toMatchObject({ ok: true });
1366
+ expect(await storedEmbeddingRows()).toEqual({
1367
+ EMBEDDING_API_BASE_URL: hosted.baseUrl,
1368
+ EMBEDDING_MODEL: "test-embedding-model",
1369
+ EMBEDDING_API_KEY: "example-hosted-key",
1370
+ EMBEDDING_QUERY_PREFIX: "",
1371
+ EMBEDDING_DOCUMENT_PREFIX: "",
1372
+ });
1373
+
1374
+ // The deployment's own model keeps its prefixes: the empty rows go.
1375
+ const same = await request("/api/onboarding/memory", {
1376
+ method: "POST",
1377
+ body: { preset: "custom", baseUrl: hosted.baseUrl, model: NOMIC, apiKey: "example-key" },
1378
+ });
1379
+ expect(await same.json()).toMatchObject({ ok: true });
1380
+ const rows = await storedEmbeddingRows();
1381
+ expect(rows.EMBEDDING_QUERY_PREFIX).toBeUndefined();
1382
+ expect(rows.EMBEDDING_DOCUMENT_PREFIX).toBeUndefined();
1383
+ } finally {
1384
+ hosted.server.stop(true);
1385
+ }
1386
+ });
1387
+
1388
+ test("without deployment prefixes a hosted save stores none", async () => {
1389
+ const hosted = startEmbeddingServer(() => embeddingResponse(512));
1390
+ try {
1391
+ const response = await request("/api/onboarding/memory", {
1392
+ method: "POST",
1393
+ body: {
1394
+ preset: "custom",
1395
+ baseUrl: hosted.baseUrl,
1396
+ model: "test-embedding-model",
1397
+ apiKey: "example-hosted-key",
1398
+ },
1399
+ });
1400
+ expect(await response.json()).toMatchObject({ ok: true });
1401
+ expect(Object.keys(await storedEmbeddingRows()).sort()).toEqual([
1402
+ "EMBEDDING_API_BASE_URL",
1403
+ "EMBEDDING_API_KEY",
1404
+ "EMBEDDING_MODEL",
1405
+ ]);
1406
+ } finally {
1407
+ hosted.server.stop(true);
1408
+ }
1409
+ });
1410
+
1411
+ test("a stored hosted setup keeps the local default, and restore brings it back", async () => {
1412
+ let localHits = 0;
1413
+ const local = startEmbeddingServer(() => {
1414
+ localHits += 1;
1415
+ return embeddingResponse(512);
1416
+ });
1417
+ setDeploymentEnv(local.baseUrl);
1418
+ try {
1419
+ // A hosted setup stored earlier, applied as the boot or a reload does.
1420
+ await upsertSwarmConfig({
1421
+ scope: "global",
1422
+ key: "EMBEDDING_API_BASE_URL",
1423
+ value: "https://openrouter.ai/api/v1",
1424
+ });
1425
+ await upsertSwarmConfig({
1426
+ scope: "global",
1427
+ key: "EMBEDDING_MODEL",
1428
+ value: "google/gemini-embedding-2",
1429
+ });
1430
+ await upsertSwarmConfig({
1431
+ scope: "global",
1432
+ key: "EMBEDDING_API_KEY",
1433
+ value: "example-openrouter-key",
1434
+ isSecret: true,
1435
+ });
1436
+ await upsertSwarmConfig({ scope: "global", key: "EMBEDDING_QUERY_PREFIX", value: "" });
1437
+ await upsertSwarmConfig({ scope: "global", key: "EMBEDDING_DOCUMENT_PREFIX", value: "" });
1438
+ await loadGlobalConfigsIntoEnv(true);
1439
+ resetEmbeddingProvider();
1440
+
1441
+ expect(await embeddingSignals()).toEqual({
1442
+ configured: true,
1443
+ dimensions: 512,
1444
+ model: "google/gemini-embedding-2",
1445
+ local: false,
1446
+ provider: "openrouter",
1447
+ localDefault: { model: NOMIC },
1448
+ });
1449
+
1450
+ const response = await request("/api/onboarding/memory", {
1451
+ method: "POST",
1452
+ body: { preset: "existing", restoreDefault: true },
1453
+ });
1454
+ expect(await response.json()).toMatchObject({ ok: true, dimensions: 512 });
1455
+ // It tested the deployment's endpoint, not the stored one.
1456
+ expect(localHits).toBe(1);
1457
+ expect(await storedEmbeddingRows()).toEqual({});
1458
+ expect(process.env.EMBEDDING_API_BASE_URL).toBe(local.baseUrl);
1459
+ expect(process.env.EMBEDDING_QUERY_PREFIX).toBe("search_query: ");
1460
+ expect(await embeddingSignals()).toMatchObject({
1461
+ model: NOMIC,
1462
+ local: true,
1463
+ provider: "local",
1464
+ localDefault: { model: NOMIC },
1465
+ });
1466
+ expect((await getState()).steps.memory).toMatchObject({ status: "done", method: "existing" });
1467
+ } finally {
1468
+ local.server.stop(true);
1469
+ __resetInjectedEnvTracking();
1470
+ }
1471
+ });
1472
+
1473
+ test("a stored private endpoint keeps the SSRF check", async () => {
1474
+ let hits = 0;
1475
+ const stored = startEmbeddingServer(() => {
1476
+ hits += 1;
1477
+ return embeddingResponse(512);
1478
+ });
1479
+ const originalNodeEnv = process.env.NODE_ENV;
1480
+ process.env.NODE_ENV = "production";
1481
+ setDeploymentEnv("https://embeddings.example.com/v1");
1482
+ try {
1483
+ // A config writer stores a loopback URL over the deployment's endpoint.
1484
+ await upsertSwarmConfig({
1485
+ scope: "global",
1486
+ key: "EMBEDDING_API_BASE_URL",
1487
+ value: stored.baseUrl,
1488
+ });
1489
+ await loadGlobalConfigsIntoEnv(true);
1490
+ expect(process.env.EMBEDDING_API_BASE_URL).toBe(stored.baseUrl);
1491
+ const response = await request("/api/onboarding/memory", {
1492
+ method: "POST",
1493
+ body: { preset: "existing" },
1494
+ });
1495
+ expect(await response.json()).toMatchObject({
1496
+ ok: false,
1497
+ errorClass: "network",
1498
+ error: "The endpoint URL is not allowed.",
1499
+ });
1500
+ expect(hits).toBe(0);
1501
+ } finally {
1502
+ stored.server.stop(true);
1503
+ __resetInjectedEnvTracking();
1504
+ if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
1505
+ else process.env.NODE_ENV = originalNodeEnv;
1506
+ }
1507
+ });
1508
+
1509
+ test("a failed restore test keeps the stored setup", async () => {
1510
+ const local = startEmbeddingServer(() => new Response("model loading", { status: 503 }));
1511
+ setDeploymentEnv(local.baseUrl);
1512
+ try {
1513
+ await upsertSwarmConfig({ scope: "global", key: "EMBEDDING_MODEL", value: "stored-model" });
1514
+ await loadGlobalConfigsIntoEnv(true);
1515
+ const response = await request("/api/onboarding/memory", {
1516
+ method: "POST",
1517
+ body: { preset: "existing", restoreDefault: true },
1518
+ });
1519
+ expect(await response.json()).toMatchObject({ ok: false });
1520
+ expect(await storedEmbeddingRows()).toEqual({ EMBEDDING_MODEL: "stored-model" });
1521
+ expect(process.env.EMBEDDING_MODEL).toBe("stored-model");
1522
+ } finally {
1523
+ local.server.stop(true);
1524
+ __resetInjectedEnvTracking();
1525
+ }
1526
+ });
1527
+
1528
+ test("restoreDefault needs the existing preset", async () => {
1529
+ const response = await request("/api/onboarding/memory", {
1530
+ method: "POST",
1531
+ body: {
1532
+ preset: "custom",
1533
+ baseUrl: "https://embeddings.example.com/v1",
1534
+ restoreDefault: true,
1535
+ },
1536
+ });
1537
+ expect(response.status).toBe(400);
1538
+ });
1539
+ });
@@ -1,10 +1,15 @@
1
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
- import { OpenAIEmbeddingProvider } from "../be/memory/providers/openai-embedding";
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ MAX_EMBEDDING_INPUT_CHARS,
4
+ OpenAIEmbeddingProvider,
5
+ } from "../be/memory/providers/openai-embedding";
3
6
 
4
7
  // A gateway that ignores `encoding_format` and always answers with plain floats.
5
8
  let server: ReturnType<typeof Bun.serve>;
6
9
  const bodies: Array<Record<string, unknown>> = [];
7
10
  const previousBaseUrl = process.env.EMBEDDING_API_BASE_URL;
11
+ const PREFIX_ENVS = ["EMBEDDING_QUERY_PREFIX", "EMBEDDING_DOCUMENT_PREFIX"] as const;
12
+ const previousPrefixes = new Map(PREFIX_ENVS.map((key) => [key, process.env[key]]));
8
13
 
9
14
  beforeAll(() => {
10
15
  server = Bun.serve({
@@ -35,6 +40,36 @@ afterAll(() => {
35
40
  else process.env.EMBEDDING_API_BASE_URL = previousBaseUrl;
36
41
  });
37
42
 
43
+ function clearPrefixes(): void {
44
+ for (const key of PREFIX_ENVS) delete process.env[key];
45
+ }
46
+
47
+ beforeEach(() => {
48
+ bodies.length = 0;
49
+ clearPrefixes();
50
+ });
51
+
52
+ afterEach(() => {
53
+ clearPrefixes();
54
+ for (const [key, value] of previousPrefixes) {
55
+ if (value !== undefined) process.env[key] = value;
56
+ }
57
+ });
58
+
59
+ function makeProvider(): OpenAIEmbeddingProvider {
60
+ return new OpenAIEmbeddingProvider({
61
+ apiKey: "test-key",
62
+ model: "test-embedding-model",
63
+ dimensions: 4,
64
+ });
65
+ }
66
+
67
+ function sentInputs(): string[] {
68
+ return bodies.flatMap((body) =>
69
+ Array.isArray(body.input) ? body.input : [body.input],
70
+ ) as string[];
71
+ }
72
+
38
73
  describe("OpenAIEmbeddingProvider", () => {
39
74
  test("requests float vectors, so a plain-float reply decodes correctly", async () => {
40
75
  const provider = new OpenAIEmbeddingProvider({
@@ -53,4 +88,72 @@ describe("OpenAIEmbeddingProvider", () => {
53
88
  [0.25, 0.5, 0.75, 1],
54
89
  ]);
55
90
  });
91
+
92
+ describe("task prefixes", () => {
93
+ function setNomicPrefixes(): void {
94
+ process.env.EMBEDDING_QUERY_PREFIX = "search_query: ";
95
+ process.env.EMBEDDING_DOCUMENT_PREFIX = "search_document: ";
96
+ }
97
+
98
+ test("kind query gets the query prefix", async () => {
99
+ setNomicPrefixes();
100
+ const provider = makeProvider();
101
+ await provider.embed("who owns billing?", { kind: "query" });
102
+ await provider.embedBatch(["a", "b"], { kind: "query" });
103
+
104
+ expect(sentInputs()).toEqual([
105
+ "search_query: who owns billing?",
106
+ "search_query: a",
107
+ "search_query: b",
108
+ ]);
109
+ });
110
+
111
+ test("kind document gets the document prefix", async () => {
112
+ setNomicPrefixes();
113
+ const provider = makeProvider();
114
+ await provider.embed("billing is owned by Ada", { kind: "document" });
115
+ await provider.embedBatch(["a", "", "b"], { kind: "document" });
116
+
117
+ // An empty input is skipped, never embedded as a bare prefix.
118
+ expect(sentInputs()).toEqual([
119
+ "search_document: billing is owned by Ada",
120
+ "search_document: a",
121
+ "search_document: b",
122
+ ]);
123
+ });
124
+
125
+ test("no prefix when the envs are unset", async () => {
126
+ const provider = makeProvider();
127
+ await provider.embed("hello", { kind: "query" });
128
+ await provider.embedBatch(["a"], { kind: "document" });
129
+
130
+ expect(sentInputs()).toEqual(["hello", "a"]);
131
+ });
132
+
133
+ test("no prefix without a kind", async () => {
134
+ setNomicPrefixes();
135
+ const provider = makeProvider();
136
+ await provider.embed("hello");
137
+ await provider.embedBatch(["a"]);
138
+
139
+ expect(sentInputs()).toEqual(["hello", "a"]);
140
+ });
141
+
142
+ test("the prefixes are read when the provider is constructed", async () => {
143
+ const provider = makeProvider();
144
+ setNomicPrefixes();
145
+ await provider.embed("hello", { kind: "query" });
146
+
147
+ expect(sentInputs()).toEqual(["hello"]);
148
+ });
149
+
150
+ test("prefix plus input stays within the char cap", async () => {
151
+ setNomicPrefixes();
152
+ await makeProvider().embed("x".repeat(MAX_EMBEDDING_INPUT_CHARS + 500), { kind: "document" });
153
+
154
+ const [sent] = sentInputs();
155
+ expect(sent!.startsWith("search_document: x")).toBe(true);
156
+ expect(sent!.length).toBe(MAX_EMBEDDING_INPUT_CHARS);
157
+ });
158
+ });
56
159
  });
@@ -675,7 +675,7 @@ describe("OpencodeSession — persisted rows render in the dashboard", () => {
675
675
  const { items } = normalizeSessionLogs(rows);
676
676
 
677
677
  const visible = items
678
- .filter((item) => item.kind !== "lifecycle")
678
+ .filter((item) => item.kind !== "lifecycle" && item.kind !== "call_usage")
679
679
  .map((item) =>
680
680
  item.kind === "text"
681
681
  ? `${item.role} text: ${item.text}`
@@ -695,6 +695,7 @@ describe("OpencodeSession — persisted rows render in the dashboard", () => {
695
695
  "system text: opencode error: provider overloaded",
696
696
  ]);
697
697
  expect(items.some((item) => item.kind === "unknown")).toBe(false);
698
+ expect(items.filter((item) => item.kind === "call_usage")).toHaveLength(1);
698
699
  });
699
700
  });
700
701