@desplega.ai/agent-swarm 2.7.4 → 2.7.6

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 (326) hide show
  1. package/README.md +1 -1
  2. package/dist/acp-adapter-bp91d37f.js +15 -0
  3. package/dist/{actions-gfrr9vb7.js → actions-yj0eeaqh.js} +19 -18
  4. package/dist/{amp-adapter-0s58gm46.js → amp-adapter-4pg9h34q.js} +3 -3
  5. package/dist/{app-stvq692g.js → app-909e97cr.js} +3 -3
  6. package/dist/{assistant-5s37fr9n.js → assistant-qdyb5fyy.js} +32 -23
  7. package/dist/{boot-reembed-yzy1841n.js → boot-reembed-fk0s9z18.js} +1 -1
  8. package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gs1rzrnh.js} +1 -1
  9. package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-4qfd4kfx.js} +4 -4
  10. package/dist/{boot-seeds-ftt9htah.js → boot-seeds-yr00k13d.js} +4 -4
  11. package/dist/{boxd-515v1t8z.js → boxd-vdmfssd9.js} +5 -5
  12. package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-qta1f1xn.js} +7 -7
  13. package/dist/{claude-managed-adapter-g6vmjccb.js → claude-managed-adapter-dvs2xw5n.js} +2 -2
  14. package/dist/{claude-managed-setup-afpa82f7.js → claude-managed-setup-sc6j4dm0.js} +3 -3
  15. package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-zgrz6yvc.js} +9175 -9076
  16. package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
  17. package/dist/{cli-edmm9d9y.js → cli-0at8w34w.js} +7 -7
  18. package/dist/{cli-a81qevyg.js → cli-0qatpy8q.js} +20 -36
  19. package/dist/{cli-bcz074zx.js → cli-0yv5z2pg.js} +1 -1
  20. package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
  21. package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
  22. package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
  23. package/dist/{cli-jkt52mgt.js → cli-386tyxxg.js} +2 -2
  24. package/dist/{cli-az0bpf8p.js → cli-3ypk9vk1.js} +3 -3
  25. package/dist/{cli-wk9md5px.js → cli-436532ck.js} +4 -4
  26. package/dist/{cli-7n5sbw0z.js → cli-4necwtsv.js} +5 -5
  27. package/dist/cli-54t00gt8.js +151 -0
  28. package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
  29. package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
  30. package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
  31. package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
  32. package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
  33. package/dist/{cli-wcmnan2g.js → cli-89knwags.js} +5 -5
  34. package/dist/{cli-pwyevs29.js → cli-8bc1w0tw.js} +22 -21
  35. package/dist/{cli-t282jgab.js → cli-8snvgs5r.js} +41 -16
  36. package/dist/{cli-vqgnyh76.js → cli-98rw5dt5.js} +15 -3
  37. package/dist/{cli-9cctx1tn.js → cli-a22h291q.js} +1 -1
  38. package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
  39. package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
  40. package/dist/{cli-je8yeyc5.js → cli-bjb3z51z.js} +5 -5
  41. package/dist/{cli-mc97xy2s.js → cli-c60vetmm.js} +16 -4
  42. package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
  43. package/dist/{cli-wndv9p7g.js → cli-decfb6v8.js} +6 -6
  44. package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
  45. package/dist/{cli-8yy525ad.js → cli-drr5v6av.js} +2 -2
  46. package/dist/{cli-5gr1g711.js → cli-dxae2yg7.js} +2 -2
  47. package/dist/{cli-pe09p6dv.js → cli-e3r2neck.js} +8 -7
  48. package/dist/{cli-13jvmew3.js → cli-ekwks2vs.js} +7 -7
  49. package/dist/{cli-jg4p5t36.js → cli-es6va8jk.js} +121 -51
  50. package/dist/{cli-j4kt1mpb.js → cli-faxe6thg.js} +3 -3
  51. package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
  52. package/dist/cli-fw2w0kva.js +47 -0
  53. package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
  54. package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
  55. package/dist/cli-gdhpcbhf.js +55 -0
  56. package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
  57. package/dist/{cli-8veytvbh.js → cli-he9wchme.js} +1 -1
  58. package/dist/{cli-k76tgq03.js → cli-hgaazk69.js} +2 -2
  59. package/dist/{cli-d2pzqej1.js → cli-hr2e2fft.js} +741 -7
  60. package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
  61. package/dist/{cli-1s1dqn1j.js → cli-k2gzwsj2.js} +297 -140
  62. package/dist/{cli-b1afgtj3.js → cli-kqdg1bsb.js} +3 -3
  63. package/dist/cli-m2z8jagn.js +12078 -0
  64. package/dist/{cli-mvckssyq.js → cli-m48g6p95.js} +5 -5
  65. package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
  66. package/dist/{cli-41z8xwj0.js → cli-nhrvpaj7.js} +34 -8
  67. package/dist/{cli-ahd90bwt.js → cli-nwj0ee2w.js} +6 -6
  68. package/dist/{cli-kvtc7aez.js → cli-p5gp8pcx.js} +44 -14
  69. package/dist/{cli-avgcz4ra.js → cli-pd7vagab.js} +436 -171
  70. package/dist/{cli-zn15c46z.js → cli-q0k1w5zd.js} +1 -1
  71. package/dist/{cli-bq2pcphn.js → cli-rmmmrjkd.js} +1 -1
  72. package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
  73. package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
  74. package/dist/{cli-1yekbyqm.js → cli-shpbx5ee.js} +283 -82
  75. package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
  76. package/dist/{cli-byqfbnjw.js → cli-svj5090z.js} +9 -9
  77. package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
  78. package/dist/{cli-v1yw3wha.js → cli-vvpx3gsx.js} +4 -4
  79. package/dist/{cli-a1fhj4n9.js → cli-w6dt9ged.js} +2 -2
  80. package/dist/{cli-1gm8jcyr.js → cli-waqbxg08.js} +5232 -68
  81. package/dist/{cli-5hjced2v.js → cli-wp9sp1qb.js} +6 -6
  82. package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
  83. package/dist/{cli-s0exq74r.js → cli-xbbndvjy.js} +3 -3
  84. package/dist/{cli-f255yaqn.js → cli-xds5f77d.js} +3 -3
  85. package/dist/{cli-0sbkss7h.js → cli-xnqgpshy.js} +2 -2
  86. package/dist/{cli-0fgr1w9b.js → cli-xnrwazav.js} +20 -3
  87. package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
  88. package/dist/{cli-cq1pf336.js → cli-yn4kxs06.js} +124 -11
  89. package/dist/{cli-224epwwf.js → cli-yx4kfngt.js} +3 -3
  90. package/dist/{cli-eqx6k993.js → cli-yy3rhsdw.js} +47 -13
  91. package/dist/{cli-36g23z21.js → cli-zbbwez0r.js} +4 -4
  92. package/dist/{cli-ktmvb20s.js → cli-zm72anhg.js} +12 -12
  93. package/dist/cli.js +26 -25
  94. package/dist/{codex-adapter-g0gctw29.js → codex-adapter-ayd1fb6h.js} +5 -5
  95. package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
  96. package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
  97. package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-jesa32xc.js} +6 -6
  98. package/dist/{commands-1p07cvff.js → commands-hanewgf2.js} +9 -3
  99. package/dist/{controller-z0s7sv6r.js → controller-dt1n3c1q.js} +9 -4
  100. package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-xx8x64n5.js} +22 -21
  101. package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-9s4dwwdw.js} +2 -2
  102. package/dist/{db-capk2raj.js → db-3vttcbv4.js} +3 -3
  103. package/dist/{db-rsh04kh3.js → db-9dv0g1ab.js} +12 -4
  104. package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
  105. package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
  106. package/dist/{dispatcher-304q6fwa.js → dispatcher-tx9hd1sv.js} +18 -17
  107. package/dist/{docker-7rxt0rry.js → docker-c1a3gqmf.js} +91 -24
  108. package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-007y4ejs.js} +2 -2
  109. package/dist/{e2b-renc0k8q.js → e2b-a19d28rz.js} +4 -4
  110. package/dist/{e2b-7fc9b9bd.js → e2b-we6hs6q5.js} +6 -6
  111. package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-rgbrqb8s.js} +1 -1
  112. package/dist/{embedding-space-s68v49ct.js → embedding-space-ny1e2940.js} +6 -6
  113. package/dist/{embeddings-4ms3604w.js → embeddings-2t386t13.js} +1 -1
  114. package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
  115. package/dist/{execute-mcdgccjs.js → execute-pksh0ceh.js} +1 -1
  116. package/dist/{external-channel-9dxwfztn.js → external-channel-phgmhmd7.js} +2 -2
  117. package/dist/fx-adapter-fy8rr921.js +18 -0
  118. package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-8y3m3k4b.js} +1 -1
  119. package/dist/{google-vertex-jrtdh9gs.js → google-vertex-w3v98n7b.js} +1 -1
  120. package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-q0abvpaj.js} +8 -8
  121. package/dist/handlers-by3sfxyx.js +32 -0
  122. package/dist/{heartbeat-3n94heh3.js → heartbeat-kadyek8p.js} +21 -20
  123. package/dist/{hook-aadrn69x.js → hook-5zs3msf6.js} +6 -6
  124. package/dist/{hook-zxwje86c.js → hook-9k551xnj.js} +4 -4
  125. package/dist/{http-xzxk9mst.js → http-mhz9vw1f.js} +272 -103
  126. package/dist/index-4f2pvxae.js +37 -0
  127. package/dist/index-98gz7tmp.js +30 -0
  128. package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
  129. package/dist/index-k08z2cak.js +33 -0
  130. package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
  131. package/dist/{index-d34f6r87.js → index-q361cg2f.js} +1 -1
  132. package/dist/{index-9j4zwcht.js → index-rvr00sg7.js} +168 -675
  133. package/dist/{index-qeb82z37.js → index-vdygbq10.js} +1 -1
  134. package/dist/{keepalive-w4aakbyk.js → keepalive-w56f32cg.js} +4 -4
  135. package/dist/lead-enq35nqp.js +70 -0
  136. package/dist/{maintenance-hbptpk6p.js → maintenance-7mdgxcea.js} +1 -1
  137. package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-29txy9vk.js} +3 -3
  138. package/dist/{onboard-vz2s5df3.js → onboard-kpsrkarj.js} +5 -5
  139. package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-j25vm3rr.js} +5 -5
  140. package/dist/{otel-impl-aefave37.js → otel-impl-6kkct7a8.js} +2 -2
  141. package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-exhkzhn2.js} +117 -23
  142. package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-n2x7jh6k.js} +3 -3
  143. package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-81ypx6es.js} +1 -1
  144. package/dist/{rbac-roles-svj72cdf.js → rbac-roles-b5htkjby.js} +2 -2
  145. package/dist/{render-v2-amwbnfxh.js → render-v2-qn6jmpk4.js} +3 -3
  146. package/dist/{resume-9cd6wf6z.js → resume-wd30wx23.js} +7 -7
  147. package/dist/{runtime-fp4vrx7m.js → runtime-4hsg7h60.js} +1 -1
  148. package/dist/{session-access-21t6yjyg.js → session-access-5qvfevhk.js} +4 -4
  149. package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-n1czavbr.js} +3 -3
  150. package/dist/{setup-4592tqz4.js → setup-dnb56qy5.js} +3 -3
  151. package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-76xxv9fs.js} +2 -2
  152. package/dist/{work-objects-t9tcf174.js → work-objects-4n21tfkm.js} +21 -20
  153. package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-77hvzxww.js} +18 -17
  154. package/dist/worker-gg1576j3.js +70 -0
  155. package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
  156. package/openapi.json +189 -32
  157. package/package.json +8 -7
  158. package/src/be/db/script-runs.ts +23 -0
  159. package/src/be/db/session-summaries.ts +8 -2
  160. package/src/be/db/session-tree.ts +174 -61
  161. package/src/be/db/session-visibility.ts +5 -4
  162. package/src/be/db/tasks/read.ts +122 -3
  163. package/src/be/db-client.ts +8 -5
  164. package/src/be/db.ts +23 -8
  165. package/src/be/memory/providers/sqlite-store.ts +49 -12
  166. package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
  167. package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
  168. package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
  169. package/src/be/migrations/263_task_list_filter_index.sql +11 -0
  170. package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
  171. package/src/be/migrations/266_script_runs_source_task.sql +11 -0
  172. package/src/be/model-validation.ts +5 -1
  173. package/src/be/pricing-normalize.ts +7 -2
  174. package/src/be/runtime-pools/cache-cleanup.ts +109 -0
  175. package/src/be/runtime-pools/db.ts +65 -6
  176. package/src/be/runtime-pools/remote-controller.ts +5 -0
  177. package/src/be/runtime-pools/supervisor.ts +33 -2
  178. package/src/be/scripts/typecheck.ts +23 -0
  179. package/src/be/seed/schedules-seeder.ts +135 -9
  180. package/src/be/seed/workflows-seeder.ts +65 -1
  181. package/src/be/seed-pricing.ts +23 -0
  182. package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
  183. package/src/be/steering.ts +50 -3
  184. package/src/be/swarm-config-guard.ts +1 -0
  185. package/src/commands/controller.ts +4 -0
  186. package/src/commands/entrypoint-api-readiness.ts +74 -0
  187. package/src/commands/provider-credentials.ts +29 -2
  188. package/src/commands/runner.ts +86 -5
  189. package/src/extensions/contract-types.generated.ts +1 -1
  190. package/src/extensions/contract.ts +27 -1
  191. package/src/extensions/dispatcher.ts +4 -0
  192. package/src/http/agents.ts +1 -0
  193. package/src/http/index.ts +1 -1
  194. package/src/http/list-total-cache.ts +14 -7
  195. package/src/http/mcp-bridge.ts +4 -0
  196. package/src/http/memory.ts +38 -9
  197. package/src/http/script-runs.ts +174 -5
  198. package/src/http/status.ts +10 -2
  199. package/src/http/tasks.ts +4 -2
  200. package/src/integrations/chat-sdk/inbound.ts +15 -5
  201. package/src/integrations/chat-sdk/steering.ts +3 -1
  202. package/src/kv-reserved-namespaces.ts +2 -0
  203. package/src/prompts/base-prompt.ts +4 -1
  204. package/src/prompts/session-templates.ts +26 -0
  205. package/src/providers/acp-adapter.ts +190 -26
  206. package/src/providers/acp-targets.ts +306 -6
  207. package/src/providers/amp-adapter.ts +1 -1
  208. package/src/providers/codex-adapter.ts +9 -44
  209. package/src/providers/codex-oauth/flow.ts +24 -3
  210. package/src/providers/codex-oauth/storage.ts +56 -17
  211. package/src/providers/codex-oauth/subscription.ts +67 -0
  212. package/src/providers/fx-adapter.ts +211 -0
  213. package/src/providers/index.ts +5 -1
  214. package/src/providers/mcp-loopback-proxy.ts +94 -0
  215. package/src/providers/pi-mono-adapter.ts +98 -13
  216. package/src/providers/pricing-sources.md +8 -0
  217. package/src/providers/reasoning-effort.ts +17 -1
  218. package/src/providers/types.ts +2 -1
  219. package/src/runtime-providers/controller-protocol.ts +14 -1
  220. package/src/runtime-providers/controller.ts +5 -0
  221. package/src/runtime-providers/docker.ts +108 -23
  222. package/src/runtime-providers/types.ts +16 -0
  223. package/src/scheduler/scheduler.ts +36 -3
  224. package/src/script-workflows/workflow-ctx.ts +8 -0
  225. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  226. package/src/scripts-runtime/types/stdlib.d.ts +27 -0
  227. package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
  228. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -0
  229. package/src/server-user.ts +1 -0
  230. package/src/server.ts +7 -0
  231. package/src/slack/assistant.ts +17 -1
  232. package/src/slack/commands.ts +5 -1
  233. package/src/slack/handlers.ts +53 -5
  234. package/src/slack/left-threads.ts +88 -0
  235. package/src/slack/steering.ts +3 -0
  236. package/src/slack/thread-buffer.ts +40 -16
  237. package/src/tasks/follow-up-redelegation-guard.ts +155 -0
  238. package/src/tests/acp-adapter.test.ts +79 -0
  239. package/src/tests/base-prompt.test.ts +12 -0
  240. package/src/tests/chat-sdk-inbound.test.ts +30 -0
  241. package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
  242. package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
  243. package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
  244. package/src/tests/claude-artifacts-read-script.test.ts +209 -0
  245. package/src/tests/codex-oauth-credential-info.test.ts +40 -0
  246. package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
  247. package/src/tests/context-window.test.ts +7 -0
  248. package/src/tests/db-client.test.ts +21 -0
  249. package/src/tests/db-task-read-contract.test.ts +8 -2
  250. package/src/tests/db-task-write-contract.test.ts +5 -1
  251. package/src/tests/entrypoint-api-readiness.test.ts +416 -210
  252. package/src/tests/explicit-model-validation.test.ts +113 -8
  253. package/src/tests/extensions-run-context.test.ts +11 -0
  254. package/src/tests/extensions-steering.test.ts +353 -0
  255. package/src/tests/extensions-validate.test.ts +5 -0
  256. package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
  257. package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
  258. package/src/tests/fixtures/fake-fx.ts +167 -0
  259. package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
  260. package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
  261. package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
  262. package/src/tests/fx-adapter.test.ts +771 -0
  263. package/src/tests/grok-adapter.test.ts +36 -0
  264. package/src/tests/list-total-cache.test.ts +27 -2
  265. package/src/tests/mcp-loopback-proxy.test.ts +193 -0
  266. package/src/tests/memory-fts-populate.test.ts +11 -9
  267. package/src/tests/memory-retrieval-purge.test.ts +80 -0
  268. package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
  269. package/src/tests/model-tier-resolution.test.ts +3 -3
  270. package/src/tests/pricing-refresh.test.ts +5 -5
  271. package/src/tests/prompt-template-session.test.ts +5 -3
  272. package/src/tests/provider-registration.test.ts +2 -1
  273. package/src/tests/query-audit-plans.test.ts +30 -2
  274. package/src/tests/runtime-cache-cleanup.test.ts +367 -0
  275. package/src/tests/runtime-e2b-pools.test.ts +41 -0
  276. package/src/tests/runtime-pools-db.test.ts +98 -0
  277. package/src/tests/runtime-pools-http.test.ts +29 -0
  278. package/src/tests/runtime-providers-docker.test.ts +253 -0
  279. package/src/tests/runtime-remote-controller.test.ts +66 -2
  280. package/src/tests/runtime-supervisor.test.ts +66 -2
  281. package/src/tests/script-connections.test.ts +13 -0
  282. package/src/tests/script-runs-http.test.ts +684 -2
  283. package/src/tests/seed-model-profiles.test.ts +2 -2
  284. package/src/tests/seed-schedules.test.ts +138 -0
  285. package/src/tests/seed-scripts.test.ts +26 -0
  286. package/src/tests/session-cost-recompute-unit.test.ts +39 -0
  287. package/src/tests/session-costs-golden.test.ts +26 -0
  288. package/src/tests/session-summaries-plans.test.ts +120 -0
  289. package/src/tests/session-summaries.test.ts +72 -12
  290. package/src/tests/sessions-tree.test.ts +11 -3
  291. package/src/tests/slack-leave-thread.test.ts +571 -0
  292. package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
  293. package/src/tests/status.test.ts +11 -0
  294. package/src/tests/tasks-readable-row.test.ts +155 -0
  295. package/src/tests/tool-annotations.test.ts +2 -1
  296. package/src/tests/workflow-engine-v2.test.ts +42 -0
  297. package/src/tests/workflow-script-principal.test.ts +207 -7
  298. package/src/tools/send-task.ts +35 -47
  299. package/src/tools/slack-leave-thread.ts +104 -0
  300. package/src/tools/steer-task.ts +3 -2
  301. package/src/tools/task-dedup.ts +45 -21
  302. package/src/tools/tool-config.ts +2 -1
  303. package/src/types.ts +57 -0
  304. package/src/utils/codex-auth-failure.ts +30 -0
  305. package/src/utils/context-window.ts +4 -1
  306. package/src/utils/credentials.ts +7 -0
  307. package/src/utils/fx-models.ts +43 -0
  308. package/src/workflows/cooldown.ts +3 -2
  309. package/src/workflows/engine.ts +24 -9
  310. package/templates/schedules/dreaming-daily/config.json +1 -1
  311. package/templates/schedules/dreaming-daily/content.md +6 -14
  312. package/templates/skills/composio-google-docs/content.md +10 -0
  313. package/templates/skills/script-workflows/content.md +19 -1
  314. package/templates/skills/swarm-docs/content.md +2 -0
  315. package/templates/skills/swarm-scripts/SKILL.md +2 -0
  316. package/templates/skills/swarm-scripts/content.md +2 -0
  317. package/templates/workflows/dreaming/content.md +5 -1
  318. package/dist/acp-adapter-96dc64m2.js +0 -15
  319. package/dist/cli-dznrgaep.js +0 -30
  320. package/dist/cli-w3hcrxht.js +0 -6985
  321. package/dist/handlers-jg7v74ty.js +0 -31
  322. package/dist/index-8c1zbz59.js +0 -29
  323. package/dist/index-8k8tmfxd.js +0 -32
  324. package/dist/index-hq617xdj.js +0 -36
  325. package/dist/lead-567kw7x3.js +0 -68
  326. package/dist/worker-fjdqra6q.js +0 -68
@@ -145,6 +145,8 @@ class FakeGrok {
145
145
  }
146
146
  async newSession(params) {
147
147
  capture.newSession = { meta: params._meta ?? null, mcpServers: params.mcpServers.map((s) => s.name) };
148
+ capture.newSessionKeys = Object.keys(params).sort();
149
+ capture.mcpUrls = params.mcpServers.map((s) => s.url);
148
150
  writeFileSync("capture.json", JSON.stringify(capture));
149
151
  if (${JSON.stringify(mode)} === "auth-required") throw RequestError.authRequired();
150
152
  return { sessionId: "grok-session-1" };
@@ -425,6 +427,40 @@ describe("GrokAdapter", () => {
425
427
  }
426
428
  });
427
429
 
430
+ test("the opt-in ACP hooks leave grok's session/new payload, MCP URL and steering unchanged", async () => {
431
+ const dir = makeTempDir();
432
+ const grok = writeFakeGrok(dir, "ok");
433
+ const { server, apiUrl } = await startTokenStub();
434
+ // Reachable but plain-HTTP and non-loopback: a target with requiresSecureMcpUrl would proxy it.
435
+ const publicStyleUrl = apiUrl.replace("127.0.0.1", "0.0.0.0");
436
+ try {
437
+ const config = baseConfig({
438
+ apiUrl: publicStyleUrl,
439
+ resumeSessionId: "ignored-without-a-resume-hook",
440
+ env: {
441
+ PATH: process.env.PATH ?? "",
442
+ HOME: makeTempDir(),
443
+ GROK_BINARY: grok,
444
+ XAI_API_KEY: "xai-test-key",
445
+ },
446
+ });
447
+ const adapter = new GrokAdapter();
448
+ const session = await adapter.createSession(config);
449
+ await session.waitForCompletion();
450
+ expect(session.deliverSteering).toBeUndefined();
451
+ expect(await adapter.canResume("grok-session-1")).toBe(false);
452
+ const capture = JSON.parse(await Bun.file(join(config.cwd, "capture.json")).text());
453
+ expect(capture.newSession).toEqual({
454
+ meta: { rules: "You are a swarm worker.", yoloMode: true },
455
+ mcpServers: ["swarm"],
456
+ });
457
+ expect(capture.newSessionKeys).toEqual(["_meta", "cwd", "mcpServers"]);
458
+ expect(capture.mcpUrls).toEqual([`${publicStyleUrl}/mcp`]);
459
+ } finally {
460
+ await new Promise<void>((resolve) => server.close(() => resolve()));
461
+ }
462
+ });
463
+
428
464
  test("runs an openrouter/ model as an OpenAI-compatible model on OPENROUTER_API_KEY", async () => {
429
465
  const dir = makeTempDir();
430
466
  // A BYOK route reports the endpoint's id and no ticks.
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { createListTotalCache } from "../http/list-total-cache";
2
+ import { createListTotalCache, LIST_TOTAL_TTL_MS } from "../http/list-total-cache";
3
3
 
4
4
  function counter(values: number[]) {
5
5
  let calls = 0;
@@ -10,7 +10,7 @@ function counter(values: number[]) {
10
10
  }
11
11
 
12
12
  describe("list total cache", () => {
13
- test("page 1 always counts; later pages reuse its count", async () => {
13
+ test("page 1 always counts by default; later pages reuse its count", async () => {
14
14
  const cache = createListTotalCache();
15
15
  const count = counter([10, 11, 12]);
16
16
 
@@ -24,6 +24,31 @@ describe("list total cache", () => {
24
24
  expect(count.calls()).toBe(2);
25
25
  });
26
26
 
27
+ test("page 1 polls within the first-page TTL share one count", async () => {
28
+ const cache = createListTotalCache(LIST_TOTAL_TTL_MS, 50);
29
+ const count = counter([10, 11]);
30
+ expect(await cache.get({ q: "a" }, 0, count.compute)).toBe(10);
31
+ expect(await cache.get({ q: "a" }, 0, count.compute)).toBe(10);
32
+ expect(count.calls()).toBe(1);
33
+ await Bun.sleep(60);
34
+ expect(await cache.get({ q: "a" }, 0, count.compute)).toBe(11);
35
+ expect(count.calls()).toBe(2);
36
+ });
37
+
38
+ test("with a first-page TTL, concurrent page 1 requests share an in-flight count", async () => {
39
+ const cache = createListTotalCache(LIST_TOTAL_TTL_MS, 1000);
40
+ let calls = 0;
41
+ const compute = async () => {
42
+ calls++;
43
+ await Bun.sleep(5);
44
+ return 4;
45
+ };
46
+ expect(await Promise.all([cache.get({}, 0, compute), cache.get({}, 0, compute)])).toEqual([
47
+ 4, 4,
48
+ ]);
49
+ expect(calls).toBe(1);
50
+ });
51
+
27
52
  test("a cold later page counts, so total is always set", async () => {
28
53
  const cache = createListTotalCache();
29
54
  const count = counter([7]);
@@ -0,0 +1,193 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { secureMcpServers } from "../providers/acp-adapter";
3
+ import {
4
+ type McpLoopbackProxy,
5
+ needsLoopbackProxy,
6
+ startMcpLoopbackProxy,
7
+ } from "../providers/mcp-loopback-proxy";
8
+ import { getFreePort } from "./test-net";
9
+
10
+ type Upstream = ReturnType<typeof Bun.serve>;
11
+
12
+ const cleanups: Array<() => Promise<void> | void> = [];
13
+
14
+ afterEach(async () => {
15
+ for (const cleanup of cleanups.splice(0).reverse()) await cleanup();
16
+ });
17
+
18
+ function track(proxy: McpLoopbackProxy): McpLoopbackProxy {
19
+ cleanups.push(() => proxy.stop());
20
+ return proxy;
21
+ }
22
+
23
+ /** A stand-in MCP endpoint: JSON echo, a slow SSE stream and a redirect. */
24
+ function startUpstream(): Upstream {
25
+ const server = Bun.serve({
26
+ hostname: "127.0.0.1",
27
+ port: 0,
28
+ idleTimeout: 0,
29
+ async fetch(req) {
30
+ const url = new URL(req.url);
31
+ if (url.pathname === "/prefix/mcp" && req.method === "POST") {
32
+ return Response.json({
33
+ path: url.pathname,
34
+ search: url.search,
35
+ method: req.method,
36
+ body: await req.json(),
37
+ authorization: req.headers.get("authorization"),
38
+ agentId: req.headers.get("x-agent-id"),
39
+ host: req.headers.get("host"),
40
+ });
41
+ }
42
+ if (url.pathname === "/prefix/sse") {
43
+ const encoder = new TextEncoder();
44
+ const stream = new ReadableStream<Uint8Array>({
45
+ async start(controller) {
46
+ for (const n of [1, 2, 3]) {
47
+ controller.enqueue(encoder.encode(`event: message\ndata: {"n":${n}}\n\n`));
48
+ await Bun.sleep(30);
49
+ }
50
+ controller.close();
51
+ },
52
+ });
53
+ return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });
54
+ }
55
+ if (url.pathname === "/prefix/redirect") {
56
+ return new Response(null, { status: 302, headers: { Location: "https://example.test/" } });
57
+ }
58
+ return new Response("not found", { status: 404 });
59
+ },
60
+ });
61
+ cleanups.push(() => server.stop(true));
62
+ return server;
63
+ }
64
+
65
+ describe("needsLoopbackProxy", () => {
66
+ test("only plain HTTP to a non-loopback host needs the forwarder", () => {
67
+ expect(needsLoopbackProxy("http://api:3013/mcp")).toBe(true);
68
+ expect(needsLoopbackProxy("http://10.0.0.5/mcp")).toBe(true);
69
+ // fx accepts only these loopback names; 127.0.0.2 is rejected (fx 0.0.15).
70
+ expect(needsLoopbackProxy("http://127.0.0.2:3013/mcp")).toBe(true);
71
+ expect(needsLoopbackProxy("http://127.0.0.1:3013/mcp")).toBe(false);
72
+ expect(needsLoopbackProxy("http://localhost:3013/mcp")).toBe(false);
73
+ expect(needsLoopbackProxy("http://[::1]:3013/mcp")).toBe(false);
74
+ expect(needsLoopbackProxy("https://swarm.example/mcp")).toBe(false);
75
+ expect(needsLoopbackProxy("not a url")).toBe(false);
76
+ });
77
+ });
78
+
79
+ describe("startMcpLoopbackProxy", () => {
80
+ test("round-trips a JSON POST with its headers, under the upstream path prefix", async () => {
81
+ const upstream = startUpstream();
82
+ const proxy = track(await startMcpLoopbackProxy(`http://127.0.0.1:${upstream.port}/prefix/`));
83
+ expect(proxy.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
84
+
85
+ const response = await fetch(`${proxy.url}/mcp?session=1`, {
86
+ method: "POST",
87
+ headers: {
88
+ Authorization: "Bearer aseph_proxytest",
89
+ "X-Agent-ID": "agent-1",
90
+ "Content-Type": "application/json",
91
+ },
92
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
93
+ });
94
+ expect(response.status).toBe(200);
95
+ expect(await response.json()).toEqual({
96
+ path: "/prefix/mcp",
97
+ search: "?session=1",
98
+ method: "POST",
99
+ body: { jsonrpc: "2.0", id: 1, method: "tools/list" },
100
+ authorization: "Bearer aseph_proxytest",
101
+ agentId: "agent-1",
102
+ // The upstream sees its own host, not the forwarder's.
103
+ host: `127.0.0.1:${upstream.port}`,
104
+ });
105
+ });
106
+
107
+ test("streams SSE events in order as they arrive", async () => {
108
+ const upstream = startUpstream();
109
+ const proxy = track(await startMcpLoopbackProxy(`http://127.0.0.1:${upstream.port}/prefix`));
110
+ const response = await fetch(`${proxy.url}/sse`);
111
+ expect(response.headers.get("content-type")).toContain("text/event-stream");
112
+ const reader = response.body!.getReader();
113
+ const decoder = new TextDecoder();
114
+ let text = "";
115
+ let reads = 0;
116
+ while (true) {
117
+ const { done, value } = await reader.read();
118
+ if (done) break;
119
+ reads += 1;
120
+ text += decoder.decode(value, { stream: true });
121
+ }
122
+ expect([...text.matchAll(/"n":(\d)/g)].map((m) => m[1])).toEqual(["1", "2", "3"]);
123
+ // Delivered incrementally, not buffered into one body.
124
+ expect(reads).toBeGreaterThan(1);
125
+ });
126
+
127
+ test("passes a redirect through instead of following it", async () => {
128
+ const upstream = startUpstream();
129
+ const proxy = track(await startMcpLoopbackProxy(`http://127.0.0.1:${upstream.port}/prefix`));
130
+ const response = await fetch(`${proxy.url}/redirect`, { redirect: "manual" });
131
+ expect(response.status).toBe(302);
132
+ expect(response.headers.get("location")).toBe("https://example.test/");
133
+ });
134
+
135
+ test("answers 502 when the upstream is down", async () => {
136
+ const port = await getFreePort();
137
+ const proxy = track(await startMcpLoopbackProxy(`http://127.0.0.1:${port}`));
138
+ const response = await fetch(`${proxy.url}/mcp`, { method: "POST", body: "{}" });
139
+ expect(response.status).toBe(502);
140
+ expect(((await response.json()) as { error: string }).error).toContain(
141
+ "MCP loopback proxy: upstream failed",
142
+ );
143
+ });
144
+
145
+ test("stop() frees the port and is idempotent", async () => {
146
+ const upstream = startUpstream();
147
+ const proxy = await startMcpLoopbackProxy(`http://127.0.0.1:${upstream.port}/prefix`);
148
+ await proxy.stop();
149
+ await proxy.stop();
150
+ await expect(fetch(`${proxy.url}/sse`)).rejects.toThrow();
151
+ });
152
+ });
153
+
154
+ describe("secureMcpServers", () => {
155
+ test("forwards plain-HTTP non-loopback servers, one forwarder per origin, and drops SSE", async () => {
156
+ const proxies: McpLoopbackProxy[] = [];
157
+ cleanups.push(async () => {
158
+ for (const proxy of proxies) await proxy.stop();
159
+ });
160
+ const servers = await secureMcpServers(
161
+ [
162
+ { type: "http", name: "swarm", url: "http://api:3013/mcp", headers: [] },
163
+ { type: "http", name: "same-origin", url: "http://api:3013/other?x=1", headers: [] },
164
+ { type: "http", name: "secure", url: "https://mcp.example/mcp", headers: [] },
165
+ { type: "http", name: "local", url: "http://127.0.0.1:9/mcp", headers: [] },
166
+ { type: "sse", name: "legacy", url: "https://sse.example/sse", headers: [] },
167
+ { name: "stdio", command: "true", args: [], env: [] },
168
+ ],
169
+ false,
170
+ proxies,
171
+ );
172
+ expect(proxies).toHaveLength(1);
173
+ const forwarded = proxies[0]!.url;
174
+ expect(servers.map((server) => [server.name, "url" in server ? server.url : null])).toEqual([
175
+ ["swarm", `${forwarded}/mcp`],
176
+ ["same-origin", `${forwarded}/other?x=1`],
177
+ ["secure", "https://mcp.example/mcp"],
178
+ ["local", "http://127.0.0.1:9/mcp"],
179
+ ["stdio", null],
180
+ ]);
181
+ });
182
+
183
+ test("keeps SSE servers when the agent supports them", async () => {
184
+ const proxies: McpLoopbackProxy[] = [];
185
+ const servers = await secureMcpServers(
186
+ [{ type: "sse", name: "legacy", url: "https://sse.example/sse", headers: [] }],
187
+ true,
188
+ proxies,
189
+ );
190
+ expect(servers.map((server) => server.name)).toEqual(["legacy"]);
191
+ expect(proxies).toEqual([]);
192
+ });
193
+ });
@@ -2,8 +2,8 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
3
  import { closeDb, createAgent, getDb, getDbClient, initDb } from "../be/db";
4
4
  import {
5
- FTS_DELETE_EXTRA_SQL,
6
- FTS_MISSING_IDS_SQL,
5
+ FTS_SCAN_BATCH_SQL,
6
+ MEMORY_ID_BATCH_SQL,
7
7
  SqliteMemoryStore,
8
8
  } from "../be/memory/providers/sqlite-store";
9
9
 
@@ -62,16 +62,18 @@ describe("memory_fts boot backfill", () => {
62
62
  // Positive control: the old anti-join scans memory_fts once per memory.
63
63
  expect(planOf(QUADRATIC_BACKFILL_SQL).join(" | ")).toContain("CORRELATED");
64
64
 
65
- for (const sql of [FTS_MISSING_IDS_SQL, FTS_DELETE_EXTRA_SQL]) {
66
- const plan = planOf(sql).join(" | ");
67
- expect(plan).not.toContain("CORRELATED");
68
- // Exactly one pass over memory_fts per statement.
69
- expect(plan.match(/SCAN memory_fts/g)?.length).toBe(1);
70
- }
65
+ // Each batch seeks a rowid range of one table. The orphan check is a
66
+ // lookup on the agent_memory primary key, never a scan of memory_fts.
67
+ const scan = planOf(FTS_SCAN_BATCH_SQL.replace(/\?/g, "0")).join(" | ");
68
+ expect(scan).toMatch(/SCAN memory_fts VIRTUAL TABLE INDEX \d+:>/);
69
+ expect(scan.match(/memory_fts/g)?.length).toBe(1);
70
+ expect(scan).toContain("SEARCH m USING COVERING INDEX sqlite_autoindex_agent_memory_1 (id=?)");
71
+ const ids = planOf(MEMORY_ID_BATCH_SQL.replace(/\?/g, "0")).join(" | ");
72
+ expect(ids).toContain("SEARCH agent_memory USING INTEGER PRIMARY KEY (rowid>?)");
71
73
  });
72
74
 
73
75
  test("a new store backfills missing rows across batches and drops orphans", async () => {
74
- const total = 1050; // crosses two 500-row batch boundaries
76
+ const total = 1050; // crosses two 500-row insert batches and one 1000-row scan batch
75
77
  const seed = await store.store({
76
78
  agentId,
77
79
  scope: "agent",
@@ -0,0 +1,80 @@
1
+ import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { closeDb, getDb, getDbClient, initDb } from "../be/db";
4
+ import { purgeStaleSearchRetrievals, startMemoryGc, stopMemoryGc } from "../http/memory";
5
+
6
+ const TEST_DB_PATH = "./test-memory-retrieval-purge.sqlite";
7
+ const DAY = 24 * 60 * 60 * 1000;
8
+
9
+ async function removeDbFiles(): Promise<void> {
10
+ for (const suffix of ["", "-wal", "-shm"])
11
+ await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => undefined);
12
+ }
13
+
14
+ async function insertRetrieval(id: string, eventType: string, ageDays: number): Promise<void> {
15
+ await getDbClient().run(
16
+ `INSERT INTO memory_retrieval (id, agentId, memoryId, retrievedAt, eventType)
17
+ VALUES (?, 'agent-1', 'memory-1', ?, ?)`,
18
+ [id, new Date(Date.now() - ageDays * DAY).toISOString(), eventType],
19
+ );
20
+ }
21
+
22
+ describe("purgeStaleSearchRetrievals", () => {
23
+ beforeAll(async () => {
24
+ await removeDbFiles();
25
+ initDb(TEST_DB_PATH);
26
+ });
27
+
28
+ afterAll(async () => {
29
+ closeDb();
30
+ await removeDbFiles();
31
+ });
32
+
33
+ test("deletes stale search rows in batches and keeps fresh and get rows", async () => {
34
+ for (let i = 0; i < 5; i++) await insertRetrieval(`stale-search-${i}`, "search", 120);
35
+ await insertRetrieval("fresh-search", "search", 10);
36
+ await insertRetrieval("stale-get", "get", 120);
37
+
38
+ const run = spyOn(getDbClient(), "run");
39
+ let purged: number;
40
+ let calls: (typeof run.mock.calls)[number][];
41
+ try {
42
+ purged = await purgeStaleSearchRetrievals(2);
43
+ calls = [...run.mock.calls];
44
+ } finally {
45
+ run.mockRestore();
46
+ }
47
+ expect(purged).toBe(5);
48
+ // Batches of 2: 2 + 2 + 1, and the short batch ends the loop.
49
+ expect(calls).toHaveLength(3);
50
+
51
+ const left = await getDbClient().query<{ id: string }>(
52
+ "SELECT id FROM memory_retrieval ORDER BY id",
53
+ );
54
+ expect(left.map((row) => row.id)).toEqual(["fresh-search", "stale-get"]);
55
+
56
+ // Without sqlite_stat1 the plan must stay on the retrievedAt range, not
57
+ // read every search row through the eventType index.
58
+ const [sql, params] = calls[0]!;
59
+ const plan = getDb()
60
+ .query<{ detail: string }, any[]>(`EXPLAIN QUERY PLAN ${sql}`)
61
+ .all(...((params ?? []) as any[]))
62
+ .map((row) => row.detail)
63
+ .join("\n");
64
+ expect(plan).toContain("USING INDEX idx_memret_retrieved_at (retrievedAt<?)");
65
+ expect(plan).not.toContain("idx_memret_event_type");
66
+ });
67
+ });
68
+
69
+ describe("startMemoryGc", () => {
70
+ test("defers the first purge off the boot path", async () => {
71
+ const timeout = spyOn(globalThis, "setTimeout");
72
+ try {
73
+ await startMemoryGc(60 * 60 * 1000, 120_000);
74
+ expect(timeout.mock.calls.some(([, ms]) => ms === 120_000)).toBe(true);
75
+ } finally {
76
+ stopMemoryGc();
77
+ timeout.mockRestore();
78
+ }
79
+ });
80
+ });
@@ -0,0 +1,118 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { expect, test } from "bun:test";
3
+ import { runMigrations } from "../be/migrations/runner";
4
+ import {
5
+ ControllerClientMessageSchema,
6
+ ControllerCommandSchema,
7
+ } from "../runtime-providers/controller-protocol";
8
+
9
+ test("migration 260 adds an optional JSON cache and the release table to a fresh database", () => {
10
+ const db = new Database(":memory:");
11
+ try {
12
+ runMigrations(db);
13
+ expect(db.query("PRAGMA table_info(runtime_pools)").all()).toContainEqual(
14
+ expect.objectContaining({ name: "cache", type: "TEXT", notnull: 0 }),
15
+ );
16
+ expect(db.query("PRAGMA foreign_key_check").all()).toEqual([]);
17
+ for (const name of ["runtime_ids", "state", "created_by", "updated_by"])
18
+ expect(db.query("PRAGMA table_info(runtime_cache_releases)").all()).toContainEqual(
19
+ expect.objectContaining({ name, type: "TEXT" }),
20
+ );
21
+ } finally {
22
+ db.close();
23
+ }
24
+ });
25
+
26
+ test("migration 260 preserves existing pools with caching disabled", async () => {
27
+ const db = new Database(":memory:");
28
+ try {
29
+ db.exec(
30
+ "CREATE TABLE _migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL, checksum TEXT NOT NULL)",
31
+ );
32
+ for (const file of [...new Bun.Glob("*.sql").scanSync("src/be/migrations")]
33
+ .sort()
34
+ .filter((file) => Number(file.split("_")[0]) <= 259)) {
35
+ const sql = await Bun.file(`src/be/migrations/${file}`).text();
36
+ db.transaction(() => {
37
+ db.exec(sql);
38
+ db.run("INSERT INTO _migrations VALUES (?, ?, ?, ?)", [
39
+ Number(file.split("_")[0]),
40
+ file.replace(".sql", ""),
41
+ new Date().toISOString(),
42
+ new Bun.CryptoHasher("sha256").update(sql).digest("hex"),
43
+ ]);
44
+ }).immediate();
45
+ }
46
+ db.run(
47
+ "INSERT INTO runtime_pools (id, name, kind, placements, image, resources, env, lifecycle) VALUES (?, ?, ?, ?, ?, ?, ?, 'idle')",
48
+ [
49
+ "existing-pool",
50
+ "existing",
51
+ "docker",
52
+ '["controller"]',
53
+ "worker:test",
54
+ '{"cpu":2}',
55
+ '{"CUSTOM":"kept"}',
56
+ ],
57
+ );
58
+ runMigrations(db);
59
+ expect(db.query("SELECT name, image, resources, env, cache FROM runtime_pools").get()).toEqual({
60
+ name: "existing",
61
+ image: "worker:test",
62
+ resources: '{"cpu":2}',
63
+ env: '{"CUSTOM":"kept"}',
64
+ cache: null,
65
+ });
66
+ expect(() => db.run("UPDATE runtime_pools SET cache = 'invalid json'")).toThrow();
67
+ expect(db.query("SELECT COUNT(*) AS n FROM runtime_cache_releases").get()).toEqual({ n: 0 });
68
+ } finally {
69
+ db.close();
70
+ }
71
+ });
72
+
73
+ test("cache release runtime IDs accept arrays and reject other JSON values", () => {
74
+ const db = new Database(":memory:");
75
+ try {
76
+ runMigrations(db);
77
+ db.run(
78
+ "INSERT INTO runtime_controllers (id, name, kind, placement) VALUES ('controller', 'controller', 'docker', 'builtin')",
79
+ );
80
+ db.run(`INSERT INTO runtime_cache_releases (id, pool_id, controller_id, owner_id, cache_key, runtime_ids)
81
+ VALUES ('release', 'pool', 'controller', 'owner', 'cache', '[]')`);
82
+ for (const invalid of ["null", "{}", "42", "true", '"runtime"', "invalid json"])
83
+ expect(() =>
84
+ db.run("UPDATE runtime_cache_releases SET runtime_ids = ?", [invalid]),
85
+ ).toThrow();
86
+ db.run("UPDATE runtime_cache_releases SET runtime_ids = ?", ['["runtime-a","runtime-b"]']);
87
+ expect(db.query("SELECT runtime_ids FROM runtime_cache_releases").get()).toEqual({
88
+ runtime_ids: '["runtime-a","runtime-b"]',
89
+ });
90
+ } finally {
91
+ db.close();
92
+ }
93
+ });
94
+
95
+ test("controller protocol accepts legacy hello messages and retains optional cache metadata", () => {
96
+ const legacy = ControllerClientMessageSchema.parse({
97
+ type: "hello",
98
+ controllerId: "controller",
99
+ kind: "docker",
100
+ version: 1,
101
+ traits: { canProvision: true, ephemeralDisk: true, supportsPause: false },
102
+ });
103
+ if (legacy.type !== "hello") throw new Error("Expected hello");
104
+ expect(legacy.traits.persistentCache).toBeUndefined();
105
+ const payload = {
106
+ runtimeId: "runtime",
107
+ agentId: "agent",
108
+ poolId: "pool",
109
+ ownerId: "owner",
110
+ image: "worker:test",
111
+ env: {},
112
+ labels: {},
113
+ cache: { key: "swarm-cache-pool", mountPath: "/cache" },
114
+ };
115
+ expect(
116
+ ControllerCommandSchema.parse({ type: "cmd", id: "command", op: "provision", payload }),
117
+ ).toMatchObject({ payload: { cache: payload.cache } });
118
+ });
@@ -674,8 +674,8 @@ describe("previewModelTiers", () => {
674
674
 
675
675
  // acp has no portable tier mapping, so it has nothing to preview.
676
676
  expect(rows.some((r) => r.provider === "acp")).toBe(false);
677
- expect(new Set(rows.map((r) => r.provider)).size).toBe(10);
678
- expect(rows).toHaveLength(40);
677
+ expect(new Set(rows.map((r) => r.provider)).size).toBe(11);
678
+ expect(rows).toHaveLength(44);
679
679
  });
680
680
 
681
681
  test("an alias that resolves to nothing reports the default it falls back to", async () => {
@@ -730,7 +730,7 @@ describe("GET /api/models-catalog/tiers", () => {
730
730
  expect(handled).toBe(true);
731
731
  expect(status).toBe(200);
732
732
  const body = JSON.parse(bodyStr) as { tiers: { provider: string; tier: string }[] };
733
- expect(body.tiers).toHaveLength(40);
733
+ expect(body.tiers).toHaveLength(44);
734
734
  expect(body.tiers.find((t) => t.provider === "claude" && t.tier === "smart")).toMatchObject({
735
735
  key: "MODEL_TIER_CLAUDE_SMART",
736
736
  source: "tier-config",
@@ -102,14 +102,14 @@ describe("models.dev runtime pricing refresh", () => {
102
102
  VALUES ('codex', 'gpt-refresh-test', 'input', 0, 1, 0, 0)`,
103
103
  );
104
104
 
105
- // codex, pi, cursor and amp each project the OpenAI rows: 4 providers x 2 token classes.
105
+ // codex, pi, cursor, amp and fx each project the OpenAI rows: 5 providers x 2 token classes.
106
106
  const first = await refreshPricingFromModelsDev({
107
107
  now: 1_000,
108
108
  fetchImpl: async () => responseFor(openAiCache(2, 8), '"etag-1"'),
109
109
  });
110
110
  expect(first.status).toBe("refreshed");
111
- expect(first.candidateRows).toBe(8);
112
- expect(first.inserted).toBe(8);
111
+ expect(first.candidateRows).toBe(10);
112
+ expect(first.inserted).toBe(10);
113
113
  expect(first.unchanged).toBe(0);
114
114
 
115
115
  const activeChanged = await getActivePricingRow("codex", "gpt-refresh-test", "input", 1_000);
@@ -121,7 +121,7 @@ describe("models.dev runtime pricing refresh", () => {
121
121
  fetchImpl: async () => responseFor(openAiCache(2, 8), '"etag-2"'),
122
122
  });
123
123
  expect(second.inserted).toBe(0);
124
- expect(second.unchanged).toBe(8);
124
+ expect(second.unchanged).toBe(10);
125
125
 
126
126
  const rows = await getDbClient().query<{ effective_from: number }>(
127
127
  `SELECT effective_from FROM pricing
@@ -278,7 +278,7 @@ describe("models.dev runtime pricing refresh", () => {
278
278
 
279
279
  const logs = await getLogsByEventType("pricing.refresh");
280
280
  expect(logs).toHaveLength(1);
281
- expect(logs[0]?.newValue).toContain("inserted=8");
281
+ expect(logs[0]?.newValue).toContain("inserted=10");
282
282
  expect(logs[0]?.metadata).toContain('"etag":"\\"etag-log\\""');
283
283
  });
284
284
  });
@@ -24,6 +24,7 @@ const SYSTEM_TEMPLATES = [
24
24
  "system.agent.compaction.goal_reminder",
25
25
  "system.agent.context.bound_note",
26
26
  "system.agent.cursor.first_message",
27
+ "system.agent.fx.system_prompt_file",
27
28
  "system.agent.lead",
28
29
  "system.agent.lead.docs",
29
30
  "system.agent.memory",
@@ -42,6 +43,7 @@ const SYSTEM_TEMPLATES = [
42
43
  "system.agent.tool_discovery.search",
43
44
  "system.agent.tool_discovery.amp",
44
45
  "system.agent.tool_discovery.codex",
46
+ "system.agent.tool_discovery.fx",
45
47
  "system.agent.tool_discovery.opencode",
46
48
  "system.agent.tool_discovery.direct",
47
49
  "system.agent.tools_skills",
@@ -121,11 +123,11 @@ describe("Session templates: registration", () => {
121
123
  expect(sessionSystemEventTypes("session")).toEqual([...SESSION_TEMPLATES].sort());
122
124
  });
123
125
 
124
- test("registers 35 system and session templates in total", () => {
126
+ test("registers 37 system and session templates in total", () => {
125
127
  const all = getAllTemplateDefinitions();
126
128
  const sessionSystem = all.filter((d) => d.category === "system" || d.category === "session");
127
- // 30 system blocks + 5 session composites.
128
- expect(sessionSystem.length).toBe(35);
129
+ // 32 system blocks + 5 session composites.
130
+ expect(sessionSystem.length).toBe(37);
129
131
  });
130
132
 
131
133
  test("drops the v1 blocks that prompt v2 deleted", () => {
@@ -235,6 +235,7 @@ const BOOTSTRAP: Record<ProviderName, { expect: string } | { exempt: string }> =
235
235
  amp: { expect: "Warning: amp provider has no credentials yet" },
236
236
  cursor: { expect: "Warning: cursor provider has no credentials yet" },
237
237
  grok: { expect: "Warning: grok provider has no credentials yet" },
238
+ fx: { expect: "Warning: fx provider has no credentials yet" },
238
239
  pi: { expect: "Warning: pi provider has no credentials yet" },
239
240
  opencode: { expect: "Warning: opencode provider has no credentials yet" },
240
241
  "claude-managed": { expect: "Warning: claude-managed provider missing:" },
@@ -284,7 +285,7 @@ function extractMarkedBlock(name: string): string {
284
285
  /** Resolvable no-op stubs for every binary the verify chain may look up. */
285
286
  function makeBinaryStubDir(): string {
286
287
  const dir = mkdtempSync(join(tmpdir(), "provider-registration-bin-"));
287
- for (const name of ["claude", "codex", "opencode", "dsh", "amp", "grok"]) {
288
+ for (const name of ["claude", "codex", "opencode", "dsh", "amp", "grok", "fx"]) {
288
289
  const file = join(dir, name);
289
290
  writeFileSync(file, "#!/bin/sh\nexit 0\n");
290
291
  chmodSync(file, 0o755);