@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
@@ -0,0 +1,104 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getAgentById, getTaskById } from "@/be/db";
4
+ import { can } from "@/rbac";
5
+ import {
6
+ isSlackLeaveThreadDisabled,
7
+ leaveSlackThread,
8
+ SLACK_LEAVE_THREAD_DISABLE,
9
+ } from "@/slack/left-threads";
10
+ import { parseSlackTs } from "@/slack/message-text";
11
+ import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
12
+
13
+ export const registerSlackLeaveThreadTool = (server: McpServer) => {
14
+ createToolRegistrar(server)(
15
+ "slack-leave-thread",
16
+ {
17
+ title: "Leave a Slack thread",
18
+ description:
19
+ "Stop following a Slack thread until someone @mentions the bot there again. Messages in the thread that do not @mention the bot are then ignored, with no task and no reply. Posts nothing to Slack. Pass taskId for the thread of your Slack-originated task, or (lead only) channelId and threadTs.",
20
+ annotations: { openWorldHint: true, idempotentHint: true },
21
+
22
+ inputSchema: z.object({
23
+ taskId: z.uuid().optional().describe("A task whose Slack thread to leave."),
24
+ channelId: z.string().min(1).optional().describe("The Slack channel ID of the thread."),
25
+ threadTs: z
26
+ .string()
27
+ .min(1)
28
+ .optional()
29
+ .describe("Timestamp of the thread root (dotted, 'p' deep-link, or full permalink URL)."),
30
+ }),
31
+ outputSchema: swarmToolOutputSchema({
32
+ channelId: z.string().optional(),
33
+ threadTs: z.string().optional(),
34
+ }),
35
+ },
36
+ async ({ taskId, channelId, threadTs }, requestInfo, _meta) => {
37
+ // Sessions created before the flag was set still list the tool.
38
+ if (isSlackLeaveThreadDisabled()) {
39
+ return toolErr(`slack-leave-thread is disabled by ${SLACK_LEAVE_THREAD_DISABLE}.`);
40
+ }
41
+
42
+ if (!requestInfo.agentId) {
43
+ return toolErr("Agent ID not found.");
44
+ }
45
+
46
+ const agent = await getAgentById(requestInfo.agentId);
47
+ if (!agent) {
48
+ return toolErr("Agent not found.");
49
+ }
50
+
51
+ // Any thread is open to leads, like slack-post. Task Slack context and
52
+ // source can be set by any agent (send-task, POST /api/tasks), so a non-lead
53
+ // may only leave the thread of a task assigned to it that Slack ingress
54
+ // created: only the Slack handlers write slackTriggerMessageTs.
55
+ const mayLeaveAnyThread = can({
56
+ principal: { kind: "agent", agentId: agent.id, isLead: agent.isLead },
57
+ verb: "integration.slack.post",
58
+ resource: { kind: "none" },
59
+ source: "mcp",
60
+ }).allow;
61
+
62
+ let slackChannelId: string | undefined;
63
+ let slackThreadTs: string | undefined;
64
+
65
+ if (taskId) {
66
+ if (channelId || threadTs) {
67
+ return toolErr("Provide either taskId or channelId and threadTs, not both.");
68
+ }
69
+ const task = await getTaskById(taskId);
70
+ if (!task) {
71
+ return toolErr("Task not found.");
72
+ }
73
+ if (
74
+ !mayLeaveAnyThread &&
75
+ (task.agentId !== agent.id || task.source !== "slack" || !task.slackTriggerMessageTs)
76
+ ) {
77
+ return toolErr(
78
+ "Only a lead or the agent assigned a Slack-originated task can leave its thread.",
79
+ );
80
+ }
81
+ slackChannelId = task.slackChannelId;
82
+ slackThreadTs = task.slackThreadTs;
83
+ } else if (channelId && threadTs) {
84
+ if (!mayLeaveAnyThread) {
85
+ return toolErr("Leaving a thread by channelId and threadTs requires lead privileges.");
86
+ }
87
+ slackChannelId = channelId;
88
+ slackThreadTs = parseSlackTs(threadTs);
89
+ } else {
90
+ return toolErr("Must provide taskId, or both channelId and threadTs.");
91
+ }
92
+
93
+ if (!slackChannelId || !slackThreadTs) {
94
+ return toolErr("No Slack thread context available.");
95
+ }
96
+
97
+ await leaveSlackThread(slackChannelId, slackThreadTs, { agentId: agent.id });
98
+ return toolOk(
99
+ `Left Slack thread ${slackChannelId}/${slackThreadTs}. Messages there are ignored until someone @mentions the bot.`,
100
+ { data: { channelId: slackChannelId, threadTs: slackThreadTs } },
101
+ );
102
+ },
103
+ );
104
+ };
@@ -99,7 +99,8 @@ export async function steerTaskHandler(
99
99
  message,
100
100
  mode,
101
101
  onUnsupported,
102
- source: "mcp",
102
+ source:
103
+ ctx.kind === "owner" && ctx.requestInfo?.callOrigin === "script-sdk" ? "script" : "mcp",
103
104
  createdByKind: ctx.kind === "owner" ? "agent" : "user",
104
105
  createdByAgentId: ctx.kind === "owner" ? ctx.agentId : undefined,
105
106
  createdByUserId: ctx.kind === "user" ? ctx.userId : undefined,
@@ -126,7 +127,7 @@ export const registerSteerTaskTool = (server: McpServer) => {
126
127
  {
127
128
  title: "Steer Task",
128
129
  description:
129
- 'Send a message to a task that is already running. `mode:"steer"` is honored on pi, claude-managed, codex and cursor; claude, devin, opencode and amp support queue only (amp delivery lands at the next tool result). Pass `onUnsupported:"fail"` to get an error instead of a downgrade.',
130
+ 'Send a message to a task that is already running. `mode:"steer"` is honored on pi, claude-managed, codex, cursor and fx (fx supports steer only); claude, devin, opencode and amp support queue only (amp delivery lands at the next tool result). Pass `onUnsupported:"fail"` to get an error instead of a downgrade.',
130
131
  annotations: { destructiveHint: true },
131
132
  inputSchema: steerTaskInputSchema,
132
133
  outputSchema: steerTaskOutputSchema,
@@ -34,11 +34,53 @@ const HIGH_SIMILARITY_THRESHOLD = 0.8;
34
34
  /** Lower threshold used when the tasks also target the same agent */
35
35
  const SAME_AGENT_SIMILARITY_THRESHOLD = 0.6;
36
36
 
37
+ /**
38
+ * Rows one duplicate check scans. The JS similarity match needs more than the
39
+ * query's default 50 (an older duplicate can sit behind newer unrelated tasks),
40
+ * but the scan must stay bounded: send-task runs it twice per call, once inside
41
+ * the write transaction.
42
+ */
43
+ export const DEDUP_SCAN_LIMIT = 500;
44
+
37
45
  export interface DedupMatch {
38
46
  task: AgentTask;
39
47
  reason: string;
40
48
  }
41
49
 
50
+ /**
51
+ * The description-similarity rules shared by the duplicate check and the
52
+ * follow-up re-delegation guard: a very similar description matches any
53
+ * target, a moderately similar one matches only the same target agent.
54
+ */
55
+ export function matchSimilarTask(
56
+ existing: AgentTask,
57
+ opts: { taskDescription: string; targetAgentId?: string },
58
+ ): DedupMatch | null {
59
+ const similarity = jaccardSimilarity(existing.task, opts.taskDescription);
60
+
61
+ // Very similar task description: likely duplicate regardless of target
62
+ if (similarity > HIGH_SIMILARITY_THRESHOLD) {
63
+ return {
64
+ task: existing,
65
+ reason: `similar task description (${(similarity * 100).toFixed(0)}% match)`,
66
+ };
67
+ }
68
+
69
+ // Same target agent + moderately similar task: lower threshold
70
+ if (
71
+ opts.targetAgentId &&
72
+ existing.agentId === opts.targetAgentId &&
73
+ similarity > SAME_AGENT_SIMILARITY_THRESHOLD
74
+ ) {
75
+ return {
76
+ task: existing,
77
+ reason: `similar task to same agent (${(similarity * 100).toFixed(0)}% match)`,
78
+ };
79
+ }
80
+
81
+ return null;
82
+ }
83
+
42
84
  /**
43
85
  * Check for duplicate tasks created recently.
44
86
  * Returns the first matching duplicate, or null if none found.
@@ -54,6 +96,7 @@ export async function findDuplicateTask(opts: {
54
96
  const recentTasks = await findRecentSimilarTasks({
55
97
  creatorAgentId: opts.creatorAgentId,
56
98
  windowMinutes: opts.windowMinutes ?? 10,
99
+ limit: DEDUP_SCAN_LIMIT,
57
100
  });
58
101
 
59
102
  for (const existing of recentTasks) {
@@ -70,27 +113,8 @@ export async function findDuplicateTask(opts: {
70
113
  };
71
114
  }
72
115
 
73
- const similarity = jaccardSimilarity(existing.task, opts.taskDescription);
74
-
75
- // 2. Very similar task description — likely duplicate regardless of target
76
- if (similarity > HIGH_SIMILARITY_THRESHOLD) {
77
- return {
78
- task: existing,
79
- reason: `similar task description (${(similarity * 100).toFixed(0)}% match)`,
80
- };
81
- }
82
-
83
- // 3. Same target agent + moderately similar task — lower threshold
84
- if (
85
- opts.targetAgentId &&
86
- existing.agentId === opts.targetAgentId &&
87
- similarity > SAME_AGENT_SIMILARITY_THRESHOLD
88
- ) {
89
- return {
90
- task: existing,
91
- reason: `similar task to same agent (${(similarity * 100).toFixed(0)}% match)`,
92
- };
93
- }
116
+ const match = matchSimilarTask(existing, opts);
117
+ if (match) return match;
94
118
  }
95
119
 
96
120
  return null;
@@ -96,7 +96,7 @@ export const DEFERRED_TOOLS = new Set([
96
96
  "read-messages",
97
97
  "post-message",
98
98
 
99
- // Slack (12): task-context gated; keep out of CORE_TOOLS
99
+ // Slack (13): task-context gated; keep out of CORE_TOOLS
100
100
  "slack-reply",
101
101
  "slack-read",
102
102
  "slack-upload-file",
@@ -109,6 +109,7 @@ export const DEFERRED_TOOLS = new Set([
109
109
  "slack-archive-channel",
110
110
  "slack-delete",
111
111
  "slack-update",
112
+ "slack-leave-thread",
112
113
 
113
114
  // Channel management (2)
114
115
  "create-channel",
package/src/types.ts CHANGED
@@ -185,6 +185,14 @@ export const DEFAULT_MODEL_TIER_MAP: Record<ProviderName, Record<ModelTier, stri
185
185
  smart: "grok-4.6",
186
186
  ultra: "grok-4.7",
187
187
  },
188
+ // fx runs these on its `openrouter` custom connection (OPENROUTER_API_KEY),
189
+ // pi's tested OpenRouter set; each ran under fx 0.0.15 (2026-10-29 probe).
190
+ fx: {
191
+ smol: "openrouter/deepseek/deepseek-v4.1-flash",
192
+ regular: "openrouter/deepseek/deepseek-v4.1-flash",
193
+ smart: "openrouter/deepseek/deepseek-v4-pro-0813",
194
+ ultra: "openrouter/anthropic/claude-opus-5.5",
195
+ },
188
196
  // ACP has no portable tier-to-model mapping. Operators may set an explicit
189
197
  // MODEL_OVERRIDE, which the adapter applies through an advertised `model`
190
198
  // config option with a target-specific startup fallback.
@@ -436,6 +444,7 @@ export const ProviderNameSchema = z.enum([
436
444
  "amp",
437
445
  "cursor",
438
446
  "grok",
447
+ "fx",
439
448
  ]);
440
449
  export type ProviderName = z.infer<typeof ProviderNameSchema>;
441
450
 
@@ -596,6 +605,10 @@ export const PROVIDER_STEER_CAPABILITIES: Record<ProviderName, SteerMode[]> = {
596
605
  // Grok runs on the ACP client: one `session/prompt` turn, interrupted only
597
606
  // by `session/cancel`. No Grok surface (ACP or headless) exposes a steer.
598
607
  grok: [],
608
+ // `fx acp` steers natively: a `session/prompt` with `_meta.fx.steer` during a
609
+ // running turn lands at the next tool boundary without cancelling it (fx
610
+ // 0.0.15 probe, 2026-10-29). Queue is not measured, so it is not advertised.
611
+ fx: ["steer"],
599
612
  };
600
613
 
601
614
  export type DevinProviderMeta = {
@@ -619,6 +632,7 @@ export type ProviderMetaMap = {
619
632
  amp: NoProviderMeta;
620
633
  cursor: NoProviderMeta;
621
634
  grok: NoProviderMeta;
635
+ fx: NoProviderMeta;
622
636
  };
623
637
 
624
638
  export const FollowUpConfigSchema = z
@@ -2198,6 +2212,39 @@ export const RuntimeControllerSchema = RuntimeControllerCreateSchema.omit({ secr
2198
2212
  .openapi("RuntimeController");
2199
2213
  export type RuntimeController = z.infer<typeof RuntimeControllerSchema>;
2200
2214
 
2215
+ export const RuntimePoolCacheSchema = z
2216
+ .object({
2217
+ mountPath: z
2218
+ .string()
2219
+ .min(2)
2220
+ .refine((path) => {
2221
+ const parts = path.split("/").filter(Boolean);
2222
+ const normalized = `/${parts.join("/")}`;
2223
+ const protectedPaths = [
2224
+ "/workspace/personal",
2225
+ "/workspace/shared",
2226
+ ...["/home/worker", "/root"].flatMap((home) =>
2227
+ [".ssh", ".codex", ".claude", ".claude.json", ".pi", ".config", ".local"].map(
2228
+ (directory) => `${home}/${directory}`,
2229
+ ),
2230
+ ),
2231
+ ];
2232
+ return (
2233
+ path.startsWith("/") &&
2234
+ parts.length > 0 &&
2235
+ !path.includes("\0") &&
2236
+ !parts.some((part) => part === "." || part === "..") &&
2237
+ !protectedPaths.some(
2238
+ (protectedPath) =>
2239
+ protectedPath === normalized ||
2240
+ protectedPath.startsWith(`${normalized}/`) ||
2241
+ normalized.startsWith(`${protectedPath}/`),
2242
+ )
2243
+ );
2244
+ }, "Cache mountPath must be absolute, without . or .., and must not cover worker homes, workspaces or credential directories"),
2245
+ })
2246
+ .strict();
2247
+
2201
2248
  export const RuntimePoolCreateSchema = z
2202
2249
  .object({
2203
2250
  name: z.string().min(1).max(200),
@@ -2212,6 +2259,7 @@ export const RuntimePoolCreateSchema = z
2212
2259
  delivery: DeliveryModeSchema.nullable().default(null),
2213
2260
  env: z.record(z.string(), z.string()).default({}),
2214
2261
  resources: RuntimeResourcesSchema.nullable().default(null),
2262
+ cache: RuntimePoolCacheSchema.nullable().default(null),
2215
2263
  ttlSec: z.number().int().positive().nullable().default(null),
2216
2264
  enabled: z.boolean().default(true),
2217
2265
  })
@@ -2225,6 +2273,7 @@ export const RuntimePoolUpdateSchema = RuntimePoolCreateSchema.partial().extend(
2225
2273
  delivery: DeliveryModeSchema.nullable().optional(),
2226
2274
  env: z.record(z.string(), z.string()).optional(),
2227
2275
  resources: RuntimeResourcesSchema.nullable().optional(),
2276
+ cache: RuntimePoolCacheSchema.nullable().optional(),
2228
2277
  ttlSec: z.number().int().positive().nullable().optional(),
2229
2278
  enabled: z.boolean().optional(),
2230
2279
  });
@@ -2606,6 +2655,10 @@ export const CooldownConfigSchema = z
2606
2655
  hours: z.number().min(0).optional(),
2607
2656
  minutes: z.number().min(0).optional(),
2608
2657
  seconds: z.number().min(0).optional(),
2658
+ // "finish" (default): also wait out the window after the last successful
2659
+ // run finished. "start": count only from run starts, so a long run never
2660
+ // pushes the next fixed-time trigger out.
2661
+ from: z.enum(["finish", "start"]).optional(),
2609
2662
  })
2610
2663
  .refine((v) => v.hours !== undefined || v.minutes !== undefined || v.seconds !== undefined, {
2611
2664
  message: "At least one of hours, minutes, or seconds is required",
@@ -3096,6 +3149,8 @@ export const ScriptRunSchema = z
3096
3149
  lastHeartbeatAt: z.string().optional(),
3097
3150
  idempotencyKey: z.string().optional(),
3098
3151
  requestedByUserId: z.string().optional(),
3152
+ /** Task whose session launched the run; the follow-up re-delegation guard reads it. */
3153
+ sourceTaskId: z.string().optional(),
3099
3154
  })
3100
3155
  .openapi("ScriptRun");
3101
3156
  export type ScriptRun = z.infer<typeof ScriptRunSchema>;
@@ -3104,6 +3159,7 @@ export const ScriptRunListItemSchema = ScriptRunSchema.omit({
3104
3159
  source: true,
3105
3160
  args: true,
3106
3161
  output: true,
3162
+ sourceTaskId: true,
3107
3163
  });
3108
3164
  export type ScriptRunListItem = z.infer<typeof ScriptRunListItemSchema>;
3109
3165
 
@@ -3838,6 +3894,7 @@ export const PricingProviderSchema = z.enum([
3838
3894
  "amp",
3839
3895
  "cursor",
3840
3896
  "grok",
3897
+ "fx",
3841
3898
  ]);
3842
3899
  export type PricingProvider = z.infer<typeof PricingProviderSchema>;
3843
3900
 
@@ -32,3 +32,33 @@ export function isCodexAuthFailureReason(failureReason: string | undefined | nul
32
32
  const lower = failureReason.toLowerCase();
33
33
  return !TRANSIENT_AUTH_ERROR_MARKERS.some((m) => lower.includes(m));
34
34
  }
35
+
36
+ /**
37
+ * Turn-level texts of a rejected ChatGPT/Codex credential: a bad API key, or
38
+ * a pool login revoked while its access token is unexpired. That death mode
39
+ * never enters `getValidCodexOAuth` (the token looks fresh), so this error is
40
+ * its only artifact. The adapters tag a match `[auth-error]`.
41
+ */
42
+ const REJECTED_ACCESS_PATTERNS = [
43
+ "unauthorized",
44
+ "http 401",
45
+ " 401 ",
46
+ "invalid api key",
47
+ "invalid_api_key",
48
+ "missing api key",
49
+ "no api key",
50
+ "authentication failed",
51
+ "failed to refresh token",
52
+ "invalid 'refresh_token'",
53
+ // ChatGPT backend 401 bodies (pi on the subscription surfaces them as-is).
54
+ "token_invalidated",
55
+ "authentication token has been invalidated",
56
+ "authentication token is expired",
57
+ "refresh token was revoked",
58
+ "could not parse your authentication token",
59
+ ];
60
+
61
+ export function isCodexRejectedAccessError(text: string): boolean {
62
+ const lower = text.toLowerCase();
63
+ return REJECTED_ACCESS_PATTERNS.some((p) => lower.includes(p));
64
+ }
@@ -20,7 +20,7 @@
20
20
  * deprecated; new code should use `computeContextUsedUnified`.
21
21
  */
22
22
 
23
- import { buildClaudeShortnameMap } from "@desplega/model-catalog";
23
+ import { buildClaudeShortnameMap, chatgptSubscriptionModelId } from "@desplega/model-catalog";
24
24
  import { runtimeCatalogModel, runtimeCatalogSection } from "./runtime-model-catalog";
25
25
 
26
26
  /**
@@ -73,6 +73,9 @@ export function getContextWindowSize(model: string): number {
73
73
  /** {@link getContextWindowSize} without the fallback: undefined when the catalog has no window. */
74
74
  export function getKnownContextWindowSize(model: string | undefined): number | undefined {
75
75
  if (!model) return undefined;
76
+ // `openai-codex/<id>` (pi on the ChatGPT subscription) is the OpenAI `<id>`.
77
+ const subscriptionId = chatgptSubscriptionModelId(model);
78
+ if (subscriptionId) return catalogContext("openai", subscriptionId);
76
79
  const slash = model.indexOf("/");
77
80
  if (slash > 0) {
78
81
  const scoped = catalogContext(model.slice(0, slash), model.slice(slash + 1));
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import {
3
+ fxModelRoute,
3
4
  openAICompatibleProviders,
4
5
  providerForKeyVar,
5
6
  providerForModel,
@@ -61,6 +62,7 @@ export const PROVIDER_CREDENTIAL_VARS: Record<string, readonly string[]> = {
61
62
  amp: ["AMP_API_KEY"],
62
63
  cursor: ["CURSOR_API_KEY"],
63
64
  grok: ["XAI_API_KEY", "OPENROUTER_API_KEY"],
65
+ fx: ["OPENROUTER_API_KEY", "OPENAI_API_KEY", "AI_GATEWAY_API_KEY"],
64
66
  opencode: ["OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
65
67
  };
66
68
 
@@ -100,6 +102,11 @@ export function getModelAwareCredentialVars(provider: string, model?: string): r
100
102
  if (provider === "grok" && model) {
101
103
  return model.startsWith("openrouter/") ? ["OPENROUTER_API_KEY"] : ["XAI_API_KEY"];
102
104
  }
105
+ // fx routes by namespace (`src/utils/fx-models.ts`); each route reads one key.
106
+ if (provider === "fx" && model) {
107
+ const route = fxModelRoute(model);
108
+ return "error" in route ? base : [route.keyVar];
109
+ }
103
110
  if (!SLASH_MODEL_PROVIDERS.has(provider) || !model) return base;
104
111
  // OpenAI-compatible registry providers own their key: `nan/<id>` and
105
112
  // `helmcode/<id>` never pool an OpenRouter or Anthropic key.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Model routing for the `fx` harness. Pure, so the API server can validate a
3
+ * model and the worker can route it without importing the adapter.
4
+ *
5
+ * | Swarm model string | fx provider (`FX_PROVIDER`) | `fx acp --model` | Key |
6
+ * |------------------------------|-----------------------------------|------------------|----------------------|
7
+ * | `openrouter/<vendor>/<id>` | `openrouter` (custom connection) | `<vendor>/<id>` | `OPENROUTER_API_KEY` |
8
+ * | `vercel/<vendor>/<id>` | `gateway` (built in) | `<vendor>/<id>` | `AI_GATEWAY_API_KEY` |
9
+ * | bare `<id>` (no slash) | `openai` (custom connection) | `<id>` | `OPENAI_API_KEY` |
10
+ *
11
+ * Anything else (`nan/…`, `helmcode/…`, `anthropic/…`) is rejected with a reason.
12
+ */
13
+ import { type FxRoute, fxModelRoute } from "@desplega/model-catalog";
14
+ import { DEFAULT_MODEL_TIER_MAP } from "../types";
15
+
16
+ export type { FxRoute };
17
+
18
+ /** An fx route plus the swarm model string it came from. */
19
+ export type ResolvedFxRoute = FxRoute & { model: string };
20
+
21
+ /** Every key an fx route can read, in route-table order. */
22
+ export const FX_ROUTE_KEY_VARS = [
23
+ "OPENROUTER_API_KEY",
24
+ "OPENAI_API_KEY",
25
+ "AI_GATEWAY_API_KEY",
26
+ ] as const;
27
+
28
+ /**
29
+ * The fx route for a swarm model string, or the reason it has none. An empty
30
+ * model means the regular tier default.
31
+ */
32
+ export function resolveFxRoute(model: string | undefined): ResolvedFxRoute | { error: string } {
33
+ const value = model?.trim() || DEFAULT_MODEL_TIER_MAP.fx.regular;
34
+ const route = fxModelRoute(value);
35
+ return "error" in route ? route : { ...route, model: value };
36
+ }
37
+
38
+ /** {@link resolveFxRoute}, throwing the rejection reason. */
39
+ export function requireFxRoute(model: string | undefined): ResolvedFxRoute {
40
+ const route = resolveFxRoute(model);
41
+ if ("error" in route) throw new Error(route.error);
42
+ return route;
43
+ }
@@ -15,7 +15,8 @@ function cooldownToMs(cooldown: CooldownConfig): number {
15
15
  /**
16
16
  * Check if a workflow should be skipped due to cooldown.
17
17
  * Returns true if:
18
- * - the last successful run finished within the cooldown window, OR
18
+ * - the last successful run finished within the cooldown window (skipped for
19
+ * `from: "start"`), OR
19
20
  * - any run (including failed ones) started within the cooldown window.
20
21
  *
21
22
  * The second condition prevents runaway re-triggering when all runs fail
@@ -30,7 +31,7 @@ export async function shouldSkipCooldown(
30
31
  const now = Date.now();
31
32
 
32
33
  // Skip if last successful run finished within the cooldown window
33
- const lastSuccess = await getLastSuccessfulRun(workflowId);
34
+ const lastSuccess = cooldown.from === "start" ? null : await getLastSuccessfulRun(workflowId);
34
35
  if (lastSuccess?.finishedAt) {
35
36
  const lastFinished = new Date(lastSuccess.finishedAt).getTime();
36
37
  if (now - lastFinished < cooldownMs) return true;
@@ -192,7 +192,7 @@ export async function startWorkflowExecution(
192
192
  const newRunId = crypto.randomUUID();
193
193
  const release = holdWorkflowRun(newRunId);
194
194
  try {
195
- const { runId, created, preflightAlert } = await createRunWithInitialState(
195
+ const { runId, created, preflightAlert, status } = await createRunWithInitialState(
196
196
  workflow,
197
197
  triggerData,
198
198
  options,
@@ -204,7 +204,7 @@ export async function startWorkflowExecution(
204
204
  if (preflightAlert) await notifyAutomationPreflightFailure(preflight);
205
205
  return runId;
206
206
  }
207
- if (initial.status === "skipped") return runId;
207
+ if (status === "skipped") return runId;
208
208
  telemetry.workflow("started", {
209
209
  workflowId: workflow.id,
210
210
  nodeCount: workflow.definition.nodes.length,
@@ -317,7 +317,12 @@ async function createRunWithInitialState(
317
317
  options: WorkflowExecutionOptions,
318
318
  initial: InitialRunState,
319
319
  runId: string,
320
- ): Promise<{ runId: string; created: boolean; preflightAlert?: boolean }> {
320
+ ): Promise<{
321
+ runId: string;
322
+ created: boolean;
323
+ preflightAlert?: boolean;
324
+ status?: InitialRunState["status"];
325
+ }> {
321
326
  return getDbClient().transaction(async () => {
322
327
  const { parentStepId } = options;
323
328
  const attempt = options.parentStepAttempt ?? 0;
@@ -328,6 +333,16 @@ async function createRunWithInitialState(
328
333
  const existingId = await getChildWorkflowRunId(parentStepId, attempt);
329
334
  if (existingId) return { runId: existingId, created: false };
330
335
  }
336
+ // The cooldown check before this transaction is only a fast path: two
337
+ // triggers can both pass it before either run exists. The write lock is
338
+ // held from BEGIN, across processes too, so this recheck sees every run
339
+ // committed before it and the losing trigger records a skipped run.
340
+ const state: InitialRunState =
341
+ !initial.status &&
342
+ workflow.cooldown &&
343
+ (await shouldSkipCooldown(workflow.id, workflow.cooldown))
344
+ ? { status: "skipped", error: "cooldown", finishedAt: new Date().toISOString() }
345
+ : initial;
331
346
  await createWorkflowRun({
332
347
  id: runId,
333
348
  workflowId: workflow.id,
@@ -340,16 +355,16 @@ async function createRunWithInitialState(
340
355
  });
341
356
  await updateWorkflowRun(
342
357
  runId,
343
- initial.status
344
- ? { status: initial.status, error: initial.error, finishedAt: initial.finishedAt }
345
- : { context: initial.context },
358
+ state.status
359
+ ? { status: state.status, error: state.error, finishedAt: state.finishedAt }
360
+ : { context: state.context },
346
361
  );
347
362
  const preflightAlert =
348
- initial.status === "failed" && initial.preflight
363
+ state.status === "failed" && state.preflight
349
364
  ? await isFirstWorkflowPreflightFailureToday(workflow.id, runId)
350
365
  : undefined;
351
- await options.accept?.({ runId, skipped: initial.status === "skipped" });
352
- return { runId, created: true, preflightAlert };
366
+ await options.accept?.({ runId, skipped: state.status === "skipped" });
367
+ return { runId, created: true, preflightAlert, status: state.status };
353
368
  });
354
369
  }
355
370
 
@@ -4,7 +4,7 @@
4
4
  "displayName": "Dreaming",
5
5
  "slug": "dreaming-daily",
6
6
  "title": "Dreaming Daily Trigger",
7
- "description": "Trigger the dreaming workflow once a day, unless a dreaming run already started in the last 20 hours.",
7
+ "description": "Start the dreaming workflow once a day, directly as a workflow schedule. The workflow's 20-hour cooldown skips a second run.",
8
8
  "version": "1.0.0",
9
9
  "category": "schedules",
10
10
  "placeholders": [],
@@ -2,7 +2,9 @@
2
2
 
3
3
  Trigger the `dreaming` workflow once a day. It replaces the Daily Evolution schedule (`daily-compounding-reflection`), which now seeds disabled.
4
4
 
5
- A seeded schedule cannot target a workflow directly, so this schedule creates one small pool task, and the worker that claims it triggers the workflow. It runs after the Daily Blocker Digest (02:05 UTC), whose digest the workflow reads.
5
+ This is a workflow schedule: each fire starts a `dreaming` run directly, with no agent task in between. A seeded schedule has no creator, so the run acts as the swarm's lead, resolved when it fires; with no lead the fire fails. It runs after the Daily Blocker Digest (02:05 UTC), whose digest the workflow reads.
6
+
7
+ The `dreaming` workflow carries the double-run guard as a 20-hour cooldown: a run that would start within 20 hours of another `dreaming` run start (any status, manual runs included) is recorded as skipped. Dreaming runs share KV state, so they must never run twice in a day.
6
8
 
7
9
  ## Schedule
8
10
 
@@ -10,18 +12,8 @@ A seeded schedule cannot target a workflow directly, so this schedule creates on
10
12
  {
11
13
  "cron": "30 3 * * *",
12
14
  "timezone": "UTC",
13
- "enabled": true
15
+ "enabled": true,
16
+ "targetType": "workflow",
17
+ "workflow": "dreaming"
14
18
  }
15
19
  ```
16
-
17
- ## Scheduled Task
18
-
19
- Task Type: Dreaming trigger
20
-
21
- Trigger today's run of the `dreaming` workflow. Do nothing else: no report, no memory, no Slack.
22
-
23
- 1. Double-run guard. Run `db-query` with exactly this SQL:
24
- `SELECT w.name, r.id, r.status, r.startedAt FROM workflow_runs r JOIN workflows w ON w.id = r.workflowId WHERE w.name LIKE 'dreaming%' AND julianday(r.startedAt) >= julianday('now', '-20 hours') ORDER BY r.startedAt DESC LIMIT 5`
25
- If it returns any row, complete this task without triggering, with the output `skipped: workflow <name> run <id> started at <startedAt>, within 20 hours`. Dreaming workflows share KV state, so they must never run twice in a day. If the query fails, complete without triggering and give the error.
26
- 2. Find the workflow named exactly `dreaming` with `list-workflows`. If it is missing or disabled, complete with that reason and trigger nothing.
27
- 3. Call `trigger-workflow` with its id and no `triggerData`. Complete with the run id it returns.
@@ -80,6 +80,16 @@ Google Drive export, then preserve or reconstruct the structure explicitly.
80
80
  There is no `GET_DOCUMENT_MARKDOWN` tool; markdown exists on create/update, not
81
81
  on read.
82
82
 
83
+ ## Comments
84
+
85
+ Doc comments are a Drive feature, not a Docs one: there is no `GOOGLEDOCS_*`
86
+ comment slug. List them with `GOOGLEDRIVE_LIST_COMMENTS` (replies:
87
+ `GOOGLEDRIVE_LIST_REPLIES`) from the `googledrive` toolkit, so resolve the
88
+ user's `googledrive` connected account instead of their `googledocs` one.
89
+ Listing only works when the Drive read scopes are authorized on that account.
90
+ `GOOGLEDRIVE_CREATE_COMMENT` and the update/delete tools are write actions,
91
+ only on explicit request.
92
+
83
93
  ## Gotchas
84
94
 
85
95
  - Search returns Drive metadata, not document content — do a second