@desplega.ai/agent-swarm 1.173.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-ccm9xrep.js → acp-adapter-sj2g0m84.js} +17 -16
  3. package/dist/{actions-sgeyv08k.js → actions-fegk44gr.js} +39 -34
  4. package/dist/{amp-adapter-gpxp3kc9.js → amp-adapter-ad8ft89x.js} +8 -7
  5. package/dist/{app-km9qnv2f.js → app-kfvgbvzy.js} +18 -14
  6. package/dist/{assistant-5r8tcvvd.js → assistant-z7zdy9ja.js} +40 -35
  7. package/dist/{boot-reembed-swwzdgd7.js → boot-reembed-5c2z0mbe.js} +10 -9
  8. package/dist/{boot-reembed-16rv9843.js → boot-reembed-j8xa181e.js} +10 -9
  9. package/dist/{boot-scrub-sweep-zkr6vkqb.js → boot-scrub-sweep-hnkn3rgt.js} +11 -10
  10. package/dist/{boot-seeds-d8sp49t2.js → boot-seeds-kwjq4d2z.js} +14 -13
  11. package/dist/{claude-adapter-yzzzh8jp.js → claude-adapter-9vmvpvgd.js} +19 -18
  12. package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-k9qae05j.js} +7 -5
  13. package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-q54zx5q2.js} +6 -4
  14. package/dist/{claude-sdk-session-4whxere4.js → claude-sdk-session-9jye6cxr.js} +11881 -11472
  15. package/dist/{cli-94b48jhk.js → cli-03gedhng.js} +16 -14
  16. package/dist/{cli-1w0vj3gw.js → cli-1e4b6bzh.js} +7 -7
  17. package/dist/{cli-d603qf86.js → cli-29jc5q7w.js} +10 -8
  18. package/dist/cli-2m1ktfzz.js +17795 -0
  19. package/dist/{cli-x119kdba.js → cli-3dj8k4vy.js} +1 -1
  20. package/dist/{cli-srqq3gc5.js → cli-3fbmmawm.js} +5 -5
  21. package/dist/{cli-gmdajfw5.js → cli-4jxm2qtz.js} +4987 -1708
  22. package/dist/{cli-tadfgd0t.js → cli-5hkm7whv.js} +33 -4
  23. package/dist/{cli-42qhyvwx.js → cli-5t4f29ez.js} +3 -3
  24. package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
  25. package/dist/cli-6cvfxs8r.js +47 -0
  26. package/dist/{cli-0kxf81jj.js → cli-6jyj59nb.js} +2 -2
  27. package/dist/{cli-w1apjecb.js → cli-6razj6t4.js} +2 -2
  28. package/dist/{cli-anzdk0z4.js → cli-74ahz61x.js} +2 -2
  29. package/dist/{cli-m89t9jdf.js → cli-7x0g4sn1.js} +15416 -16024
  30. package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
  31. package/dist/{cli-81r1wjyv.js → cli-9nfx44eh.js} +1 -1
  32. package/dist/cli-9traj3dn.js +638 -0
  33. package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
  34. package/dist/{cli-na2pxe4f.js → cli-apx87jne.js} +2 -2
  35. package/dist/{cli-rfz2ve55.js → cli-bzszjf2p.js} +2855 -862
  36. package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
  37. package/dist/{cli-hsem16hb.js → cli-cgtksawx.js} +7 -4
  38. package/dist/{cli-db3m6zdt.js → cli-d2qr7aj4.js} +1 -1
  39. package/dist/{cli-rfs4vddv.js → cli-d5f9ed5c.js} +5 -5
  40. package/dist/cli-e3g5mbkz.js +776 -0
  41. package/dist/{cli-7jykv7ef.js → cli-e6mpjhed.js} +1 -1
  42. package/dist/{cli-7a93cv8c.js → cli-ee9gtz70.js} +98 -23
  43. package/dist/{cli-3nxdmjrc.js → cli-eejsgvm4.js} +4 -4
  44. package/dist/{cli-fmhj1ehj.js → cli-ef1hg1wv.js} +470 -51
  45. package/dist/cli-f527ce74.js +781 -0
  46. package/dist/{cli-ppy4nnnh.js → cli-f7kr7svz.js} +2 -2
  47. package/dist/cli-hjwmhcg1.js +10622 -0
  48. package/dist/{cli-2bw3w47j.js → cli-hnhtk6cj.js} +1 -1
  49. package/dist/{cli-ywm1azg9.js → cli-hnmhtzvh.js} +6 -4
  50. package/dist/{cli-h7k0xtry.js → cli-hvqbtna4.js} +4 -1256
  51. package/dist/{cli-s8z6x4b4.js → cli-j0pfptdp.js} +218 -102
  52. package/dist/cli-jta2eyzc.js +619 -0
  53. package/dist/{cli-5er5gjbm.js → cli-k382tshb.js} +10 -11
  54. package/dist/{cli-v3zhnxsb.js → cli-k3dp5tpf.js} +2 -2
  55. package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
  56. package/dist/cli-kceah1eh.js +92 -0
  57. package/dist/{cli-ne7smj1b.js → cli-ke896fcf.js} +10 -10
  58. package/dist/{cli-xc7p7s3g.js → cli-kt6zqg0e.js} +6 -6
  59. package/dist/{cli-wrh08zjd.js → cli-m50t4309.js} +5 -18
  60. package/dist/cli-n2wq4j7m.js +6985 -0
  61. package/dist/{cli-bv2etw2y.js → cli-n3hzg89b.js} +46 -15
  62. package/dist/{cli-tmhsxhb8.js → cli-n5vwg842.js} +23 -6
  63. package/dist/{cli-qm1448f9.js → cli-ne236afy.js} +8 -8
  64. package/dist/{cli-d161g69q.js → cli-p1x85d2t.js} +50 -2
  65. package/dist/{cli-7pz3b7x5.js → cli-przpmdgr.js} +25 -0
  66. package/dist/{cli-0a7mm3zp.js → cli-q0ke1yww.js} +25 -21
  67. package/dist/{cli-e3pmyt39.js → cli-q50942rs.js} +1 -1
  68. package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
  69. package/dist/{cli-phcqp1b9.js → cli-r2pnvj8g.js} +2 -2
  70. package/dist/{cli-jpc4r224.js → cli-r62a35ea.js} +19 -3
  71. package/dist/{cli-cd0hv9nh.js → cli-ra0xzggj.js} +2 -2
  72. package/dist/cli-rhs9pz33.js +294 -0
  73. package/dist/{cli-r4fqfpwf.js → cli-s2sp957d.js} +50 -27
  74. package/dist/{cli-1x7da0de.js → cli-s4setq91.js} +8 -8
  75. package/dist/{cli-6thk4jz3.js → cli-se8w6wny.js} +6 -6
  76. package/dist/cli-skxvvpap.js +10140 -0
  77. package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
  78. package/dist/{cli-0p3enk35.js → cli-tqaazemc.js} +9 -1
  79. package/dist/{cli-nwrdx840.js → cli-tr4sxp68.js} +3 -3
  80. package/dist/{cli-r0byd61h.js → cli-tsde1hdr.js} +15 -15
  81. package/dist/{cli-b07hr3bt.js → cli-txqb9s38.js} +2 -2
  82. package/dist/{cli-6r2f7edk.js → cli-v1yb0rqb.js} +6 -6
  83. package/dist/{cli-saqyage0.js → cli-vatyhshy.js} +179 -27
  84. package/dist/{cli-5wpw4rab.js → cli-vdach5a9.js} +5 -5
  85. package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
  86. package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
  87. package/dist/{cli-c3rk1v0x.js → cli-wayw93nt.js} +3 -3
  88. package/dist/cli-wmgrkscj.js +23254 -0
  89. package/dist/{cli-9xf7fdtp.js → cli-z5jjpaxw.js} +29 -6
  90. package/dist/{cli-451ks6aw.js → cli-zkpks3r3.js} +1 -1
  91. package/dist/cli.js +29 -45
  92. package/dist/{codex-adapter-r3dn4fp5.js → codex-adapter-y81sssk0.js} +15 -14
  93. package/dist/{codex-login-841kv5fk.js → codex-login-f2rcfpp9.js} +3 -2
  94. package/dist/{codex-session-runner-fwt793h4.js → codex-session-runner-8vx4j6a9.js} +15 -14
  95. package/dist/{commands-z8fq9qna.js → commands-kcpvpcrr.js} +11 -10
  96. package/dist/{controller-hq5ggpcp.js → controller-ywrmt6yd.js} +5 -4
  97. package/dist/{controllers-connect-3mf2btyj.js → controllers-connect-kwpvj3rf.js} +42 -37
  98. package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-cehv1qns.js} +7 -6
  99. package/dist/{db-yyhqqrbf.js → db-k8ankj20.js} +12 -11
  100. package/dist/{db-qfpq4bt7.js → db-ww5d7jq3.js} +28 -9
  101. package/dist/{devin-adapter-xgtj680j.js → devin-adapter-x1022qb1.js} +3 -2
  102. package/dist/{dispatcher-x0x7qfcq.js → dispatcher-e8gq5m68.js} +38 -33
  103. package/dist/{docker-f503k9p0.js → docker-ff6y1190.js} +3 -2
  104. package/dist/{dsh-adapter-34etvb5m.js → dsh-adapter-3epjm3af.js} +7 -6
  105. package/dist/{e2b-66xra4az.js → e2b-8ygwhg0f.js} +4 -3
  106. package/dist/{e2b-xwhyj8cw.js → e2b-swhnpw3n.js} +4 -3
  107. package/dist/{event-bus-vjtv82gm.js → event-bus-6axnngdj.js} +3 -2
  108. package/dist/{external-channel-h1fjecwe.js → external-channel-h3359cwn.js} +11 -10
  109. package/dist/{google-generative-ai-szdxbpqg.js → google-generative-ai-g8db7x0f.js} +4 -2
  110. package/dist/{google-vertex-qzw3p36d.js → google-vertex-r2692q0r.js} +4 -2
  111. package/dist/{grok-adapter-e7mr287e.js → grok-adapter-0d4x7gkx.js} +18 -17
  112. package/dist/{handlers-znb9n7d4.js → handlers-an1gzv21.js} +41 -36
  113. package/dist/{heartbeat-bm4qkn8d.js → heartbeat-6dakeb61.js} +43 -38
  114. package/dist/{hook-254r4mza.js → hook-6v6cv6d5.js} +11 -23
  115. package/dist/{hook-xaa3q80s.js → hook-sa5xye4y.js} +12 -11
  116. package/dist/{http-3dgrgp8y.js → http-r7f3eky0.js} +1499 -251
  117. package/dist/index-52j1aja4.js +3465 -0
  118. package/dist/{index-fx3jqq1e.js → index-7ep4gzdk.js} +38 -33
  119. package/dist/index-9gw58n4k.js +193 -0
  120. package/dist/index-9j87mcm8.js +56305 -0
  121. package/dist/index-bsmcresk.js +79424 -0
  122. package/dist/index-mh85qhec.js +475 -0
  123. package/dist/{index-g2byveez.js → index-rmpw9a8q.js} +38 -33
  124. package/dist/{index-2ybk7khy.js → index-s2wxgzr0.js} +468 -683
  125. package/dist/{index-bdsqa799.js → index-v1v6n7hg.js} +43 -38
  126. package/dist/index-we27kd31.js +12810 -0
  127. package/dist/{index-jd7sejm2.js → index-zk96aq1k.js} +10 -9
  128. package/dist/{keepalive-6ywhpja1.js → keepalive-be9vwy5h.js} +12 -11
  129. package/dist/{lead-cw45n21s.js → lead-0861cwrf.js} +61 -56
  130. package/dist/{maintenance-ss1a1ge9.js → maintenance-mzzgc8bn.js} +10 -9
  131. package/dist/{node-fkajaq96.js → node-mn1zs7an.js} +2 -1
  132. package/dist/{oauth-refresh-sweep-3c6f8vyy.js → oauth-refresh-sweep-cpdnfwdj.js} +11 -10
  133. package/dist/{onboard-8hzdhc1k.js → onboard-fh8qs4r3.js} +11 -3
  134. package/dist/{opencode-adapter-qbchbcfz.js → opencode-adapter-nm42wwj8.js} +18 -13
  135. package/dist/{otel-impl-e6mnvc4v.js → otel-impl-x6c1c9g4.js} +242 -868
  136. package/dist/{pi-mono-adapter-3c9g9snh.js → pi-mono-adapter-9n2ya240.js} +244 -106
  137. package/dist/{pricing-refresh-gedkjdw0.js → pricing-refresh-qb0rv0ap.js} +12 -11
  138. package/dist/{rbac-roles-dt4vh1yy.js → rbac-roles-1gzdwbh1.js} +11 -10
  139. package/dist/{rbac-roles-eszdg6pm.js → rbac-roles-3mraey52.js} +12 -11
  140. package/dist/{render-v2-907cy6sa.js → render-v2-agpa45zz.js} +18 -14
  141. package/dist/{resume-42stcaz0.js → resume-x0ydy9s9.js} +25 -21
  142. package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
  143. package/dist/{setup-adbc1c10.js → setup-as9ga0d6.js} +2 -2
  144. package/dist/{virtual-modules-rtmt494q.js → virtual-modules-9gkxqrap.js} +6 -5
  145. package/dist/{work-objects-wpj8ck95.js → work-objects-y5g1y7ev.js} +41 -36
  146. package/dist/{worker-bzdra9rf.js → worker-860c3e38.js} +61 -56
  147. package/dist/{x-qfmygqqv.js → x-m477fr0h.js} +3 -2
  148. package/openapi.json +523 -344
  149. package/package.json +10 -11
  150. package/src/be/config-refs.ts +60 -0
  151. package/src/be/db/page-versions.ts +15 -0
  152. package/src/be/db/runtime.ts +35 -8
  153. package/src/be/db/skill-name-resolution.ts +36 -0
  154. package/src/be/db/tasks/read.ts +230 -0
  155. package/src/be/db/tasks/write.ts +7 -0
  156. package/src/be/db.ts +52 -7
  157. package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
  158. package/src/be/migrations/216_task_resolved_effort.sql +5 -0
  159. package/src/be/migrations/217_chat_sdk_state.sql +104 -0
  160. package/src/be/model-catalog-store.ts +5 -1
  161. package/src/be/model-tier-resolution.ts +12 -1
  162. package/src/be/model-validation.ts +5 -2
  163. package/src/be/models-catalog.ts +26 -1
  164. package/src/be/modelsdev-cache.json +198 -63
  165. package/src/be/modelsdev-cache.ts +9 -1
  166. package/src/be/script-credential-broker.ts +39 -2
  167. package/src/be/scripts/run-saved.ts +1 -0
  168. package/src/be/scripts/typecheck.ts +10 -8
  169. package/src/be/seed/runner.ts +13 -1
  170. package/src/be/seed/state-db.ts +5 -0
  171. package/src/be/seed/types.ts +6 -0
  172. package/src/be/seed-pricing.ts +19 -2
  173. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  174. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  175. package/src/be/seed-skills/bundled-files.generated.json +0 -18
  176. package/src/be/seed-skills/index.ts +104 -3
  177. package/src/be/swarm-config-guard.ts +52 -0
  178. package/src/cli.tsx +0 -16
  179. package/src/commands/onboard/non-interactive.ts +4 -2
  180. package/src/commands/provider-credentials.ts +52 -6
  181. package/src/commands/runner.ts +190 -54
  182. package/src/e2b/env.ts +5 -0
  183. package/src/extensions/contract-types.generated.ts +1 -1
  184. package/src/extensions/contract.ts +45 -0
  185. package/src/extensions/dispatcher.ts +7 -0
  186. package/src/extensions/post-bridge.ts +40 -0
  187. package/src/github/handlers.ts +11 -4
  188. package/src/heartbeat/templates.ts +8 -5
  189. package/src/hooks/hook.ts +0 -26
  190. package/src/http/all-routes.ts +0 -1
  191. package/src/http/config.ts +36 -17
  192. package/src/http/core.ts +4 -0
  193. package/src/http/index.ts +14 -2
  194. package/src/http/integrations.ts +88 -1
  195. package/src/http/mcp.ts +15 -5
  196. package/src/http/page-proxy.ts +2 -4
  197. package/src/http/pages-public.ts +3 -3
  198. package/src/http/pages.ts +3 -13
  199. package/src/http/poll.ts +28 -68
  200. package/src/http/requested-by.ts +54 -0
  201. package/src/http/scripts.ts +1 -0
  202. package/src/http/stats.ts +1 -30
  203. package/src/http/tasks.ts +61 -5
  204. package/src/http/utils.ts +14 -0
  205. package/src/http/webhooks.ts +159 -1
  206. package/src/http/x.ts +1 -0
  207. package/src/integrations/chat-sdk/adapters.ts +129 -0
  208. package/src/integrations/chat-sdk/attachments.ts +232 -0
  209. package/src/integrations/chat-sdk/background.ts +36 -0
  210. package/src/integrations/chat-sdk/commands.ts +194 -0
  211. package/src/integrations/chat-sdk/config.ts +198 -0
  212. package/src/integrations/chat-sdk/discord-api.ts +63 -0
  213. package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
  214. package/src/integrations/chat-sdk/handlers.ts +28 -0
  215. package/src/integrations/chat-sdk/inbound.ts +426 -0
  216. package/src/integrations/chat-sdk/limits.ts +48 -0
  217. package/src/integrations/chat-sdk/logger.ts +81 -0
  218. package/src/integrations/chat-sdk/outbound.ts +123 -0
  219. package/src/integrations/chat-sdk/relay.ts +172 -0
  220. package/src/integrations/chat-sdk/router.ts +169 -0
  221. package/src/integrations/chat-sdk/runtime.ts +351 -0
  222. package/src/integrations/chat-sdk/schema.ts +245 -0
  223. package/src/integrations/chat-sdk/state.ts +243 -0
  224. package/src/integrations/chat-sdk/status.ts +244 -0
  225. package/src/integrations/chat-sdk/steering.ts +58 -0
  226. package/src/integrations/chat-sdk/teams-origin.ts +77 -0
  227. package/src/integrations/chat-sdk/telegram-api.ts +56 -0
  228. package/src/integrations/chat-sdk/templates.ts +16 -0
  229. package/src/integrations/chat-sdk/test-hooks.ts +37 -0
  230. package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
  231. package/src/integrations/chat-sdk/typing.ts +65 -0
  232. package/src/pages/version.ts +4 -5
  233. package/src/prompts/base-prompt.ts +111 -45
  234. package/src/providers/acp-target-catalog.ts +6 -0
  235. package/src/providers/acp-targets.ts +55 -2
  236. package/src/providers/amp-adapter.ts +1 -1
  237. package/src/providers/claude-adapter.ts +1 -1
  238. package/src/providers/claude-flag-settings.ts +17 -5
  239. package/src/providers/claude-managed-adapter.ts +1 -1
  240. package/src/providers/claude-session-events.ts +6 -1
  241. package/src/providers/dsh-adapter.ts +1 -1
  242. package/src/providers/opencode-adapter.ts +243 -26
  243. package/src/providers/opencode-swarm-plugin.ts +31 -0
  244. package/src/providers/pi-mono-adapter.ts +398 -153
  245. package/src/providers/pricing-sources.md +33 -2
  246. package/src/providers/reasoning-effort.ts +27 -0
  247. package/src/providers/types.ts +6 -2
  248. package/src/rbac/legacy-policy.ts +8 -2
  249. package/src/rbac/permissions.ts +21 -0
  250. package/src/scheduler/scheduler.ts +4 -2
  251. package/src/scripts-runtime/loader.ts +19 -7
  252. package/src/scripts-runtime/sdk-allowlist.ts +8 -6
  253. package/src/scripts-runtime/swarm-sdk.ts +0 -11
  254. package/src/scripts-runtime/types/stdlib.d.ts +27 -15
  255. package/src/scripts-runtime/types/swarm-extension.d.ts +63 -1
  256. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -15
  257. package/src/server.ts +52 -18
  258. package/src/slack/event-dedup.ts +1 -1
  259. package/src/tasks/additive-buffer.ts +4 -2
  260. package/src/tasks/context-key.ts +60 -1
  261. package/src/tasks/model-pin.ts +67 -0
  262. package/src/tasks/worker-follow-up.ts +4 -31
  263. package/src/tests/acp-adapter.test.ts +42 -0
  264. package/src/tests/api-drain-worker-e2e.test.ts +1 -6
  265. package/src/tests/base-prompt.test.ts +58 -54
  266. package/src/tests/chat-sdk-commands.test.ts +191 -0
  267. package/src/tests/chat-sdk-config.test.ts +334 -0
  268. package/src/tests/chat-sdk-discord-api.test.ts +68 -0
  269. package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
  270. package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
  271. package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
  272. package/src/tests/chat-sdk-harness.ts +146 -0
  273. package/src/tests/chat-sdk-inbound.test.ts +363 -0
  274. package/src/tests/chat-sdk-relay.test.ts +470 -0
  275. package/src/tests/chat-sdk-reload.test.ts +128 -0
  276. package/src/tests/chat-sdk-shutdown.test.ts +220 -0
  277. package/src/tests/chat-sdk-slack.test.ts +268 -0
  278. package/src/tests/chat-sdk-state.test.ts +163 -0
  279. package/src/tests/chat-sdk-status-route.test.ts +382 -0
  280. package/src/tests/chat-sdk-teams.test.ts +277 -0
  281. package/src/tests/chat-sdk-tools.test.ts +520 -0
  282. package/src/tests/chat-sdk-typing.test.ts +120 -0
  283. package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
  284. package/src/tests/claude-adapter-binary.test.ts +9 -4
  285. package/src/tests/claude-flag-settings.test.ts +11 -3
  286. package/src/tests/claude-managed-adapter.test.ts +1 -1
  287. package/src/tests/claude-sdk-lifecycle.test.ts +9 -3
  288. package/src/tests/claude-thinking-tokens.test.ts +57 -0
  289. package/src/tests/claude-worker-parity.test.ts +2 -7
  290. package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
  291. package/src/tests/config-refs.test.ts +66 -0
  292. package/src/tests/context-key.test.ts +19 -0
  293. package/src/tests/cors-allowlist.test.ts +25 -0
  294. package/src/tests/create-page-tool.test.ts +18 -0
  295. package/src/tests/credential-check.test.ts +139 -0
  296. package/src/tests/credentials.test.ts +21 -0
  297. package/src/tests/db-runtime.test.ts +61 -0
  298. package/src/tests/db-task-read-contract.test.ts +20 -0
  299. package/src/tests/db-task-write-contract.test.ts +10 -0
  300. package/src/tests/docs-index-footprint.test.js +3 -3
  301. package/src/tests/dsh-adapter.test.ts +1 -1
  302. package/src/tests/enabled-capabilities.test.ts +20 -0
  303. package/src/tests/entrypoint-api-readiness.test.ts +14 -8
  304. package/src/tests/entrypoint-config-env-export.test.ts +5 -2
  305. package/src/tests/entrypoint-export-to-env.test.ts +78 -0
  306. package/src/tests/explicit-model-validation.test.ts +105 -1
  307. package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
  308. package/src/tests/extensions-post-bridge.test.ts +51 -0
  309. package/src/tests/fetch-resolved-env.test.ts +128 -0
  310. package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
  311. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
  312. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
  313. package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
  314. package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
  315. package/src/tests/github-handlers.test.ts +35 -0
  316. package/src/tests/harness-provider-resolution.test.ts +12 -0
  317. package/src/tests/heartbeat-checklist.test.ts +3 -2
  318. package/src/tests/hook-runtime-close.test.ts +0 -2
  319. package/src/tests/http-api-integration.test.ts +0 -25
  320. package/src/tests/migration-046-budgets.test.ts +17 -16
  321. package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
  322. package/src/tests/model-tier-resolution.test.ts +93 -0
  323. package/src/tests/models-catalog.test.ts +42 -1
  324. package/src/tests/openai-compatible-providers.test.ts +134 -0
  325. package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
  326. package/src/tests/opencode-adapter.test.ts +216 -2
  327. package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
  328. package/src/tests/opencode-shell-env.test.ts +145 -0
  329. package/src/tests/openrouter-base-url.test.ts +101 -151
  330. package/src/tests/page-proxy.test.ts +1 -1
  331. package/src/tests/pages-browser-sdk.test.ts +76 -0
  332. package/src/tests/pages-feedback.test.ts +1 -1
  333. package/src/tests/paused-tasks-requested-by.test.ts +102 -0
  334. package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
  335. package/src/tests/pi-mono-adapter.test.ts +385 -5
  336. package/src/tests/pi-shell-env.test.ts +170 -0
  337. package/src/tests/prompt-template-remaining.test.ts +35 -0
  338. package/src/tests/rbac-engine.test.ts +11 -3
  339. package/src/tests/runner-exit-after-tasks.test.ts +0 -4
  340. package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
  341. package/src/tests/script-connections.test.ts +2 -0
  342. package/src/tests/scripts-http.test.ts +201 -1
  343. package/src/tests/scripts-only-gating.test.ts +2 -3
  344. package/src/tests/scripts-runtime.test.ts +58 -40
  345. package/src/tests/secret-scrubber.test.ts +86 -0
  346. package/src/tests/seed-skills-bundled-files.test.ts +45 -53
  347. package/src/tests/seed-skills-retire.test.ts +182 -0
  348. package/src/tests/send-task-requester-gate.test.ts +58 -0
  349. package/src/tests/session-cost-recompute-unit.test.ts +46 -0
  350. package/src/tests/skill-get-name-resolution.test.ts +137 -0
  351. package/src/tests/sonnet-5-5-registry.test.ts +9 -1
  352. package/src/tests/swarm-config-export-to-env.test.ts +267 -0
  353. package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
  354. package/src/tests/swarm-diff.test.ts +1 -1
  355. package/src/tests/task-model-effort-rerun.test.ts +164 -0
  356. package/src/tests/task-session-prompts.test.ts +132 -0
  357. package/src/tests/task-tool-manifest.test.ts +14 -0
  358. package/src/tests/task-tools-ctx.test.ts +2 -0
  359. package/src/tests/tool-annotations.test.ts +6 -6
  360. package/src/tests/ui-logs-parser.test.ts +43 -0
  361. package/src/tests/used-by-route.test.ts +6 -0
  362. package/src/tests/vercel-ai-gateway.test.ts +149 -0
  363. package/src/tests/workflow-swarm-script.test.ts +38 -0
  364. package/src/tests/workflow-wait-filter.test.ts +5 -3
  365. package/src/tools/chat-access.ts +41 -0
  366. package/src/tools/chat-open-dm.ts +59 -0
  367. package/src/tools/chat-post.ts +74 -0
  368. package/src/tools/chat-read-thread.ts +100 -0
  369. package/src/tools/chat-reply.ts +75 -0
  370. package/src/tools/create-page.ts +3 -13
  371. package/src/tools/model-catalog/index.ts +2 -1
  372. package/src/tools/register-chat-channel.ts +139 -0
  373. package/src/tools/send-task.ts +24 -0
  374. package/src/tools/skills/skill-get.ts +17 -9
  375. package/src/tools/swarm-config/get-config.ts +2 -1
  376. package/src/tools/swarm-config/list-config.ts +2 -1
  377. package/src/tools/swarm-config/set-config.ts +16 -3
  378. package/src/tools/tool-config.ts +8 -6
  379. package/src/types.ts +12 -2
  380. package/src/utils/config-env-export.ts +74 -0
  381. package/src/utils/credentials.test.ts +92 -0
  382. package/src/utils/credentials.ts +52 -4
  383. package/src/utils/harness-provider.ts +15 -6
  384. package/src/utils/runtime-model-catalog.ts +11 -1
  385. package/src/utils/secret-scrubber.ts +71 -2
  386. package/src/utils/task-tool-manifest.ts +19 -2
  387. package/src/workflows/executors/swarm-script.ts +1 -0
  388. package/templates/skills/agent-browser/SKILL.md +1 -1
  389. package/templates/skills/agent-browser/content.md +1 -1
  390. package/templates/skills/apps/SKILL.md +8 -1
  391. package/templates/skills/apps/content.md +8 -1
  392. package/templates/skills/chat-sdk/config.json +14 -0
  393. package/templates/skills/chat-sdk/content.md +84 -0
  394. package/templates/skills/composio-gmail/content.md +7 -0
  395. package/templates/skills/composio-google-docs/content.md +1 -1
  396. package/templates/skills/email-domain-health/config.json +14 -0
  397. package/templates/skills/email-domain-health/content.md +131 -0
  398. package/templates/skills/kv-storage/content.md +2 -2
  399. package/templates/skills/pages/content.md +8 -10
  400. package/templates/skills/swarm-extensions/content.md +2 -0
  401. package/templates/skills/swarm-scripts/SKILL.md +4 -2
  402. package/templates/skills/swarm-scripts/config.json +1 -1
  403. package/templates/skills/swarm-scripts/content.md +4 -2
  404. package/templates/skills/workflow-iterate/content.md +38 -0
  405. package/dist/artifact-eczsknc3.js +0 -3843
  406. package/dist/cli-bd957dgb.js +0 -153
  407. package/dist/cli-q51yprfq.js +0 -1104
  408. package/dist/cli-t1ze64h3.js +0 -811
  409. package/dist/cli-tcc4wd5d.js +0 -56471
  410. package/dist/index-9ch1n761.js +0 -1102
  411. package/src/artifact-sdk/index.ts +0 -2
  412. package/src/artifact-sdk/localtunnel.d.ts +0 -20
  413. package/src/artifact-sdk/port.ts +0 -12
  414. package/src/artifact-sdk/server.ts +0 -183
  415. package/src/artifact-sdk/tunnel.ts +0 -19
  416. package/src/commands/artifact.ts +0 -249
  417. package/src/http/ecosystem.ts +0 -77
  418. package/src/tests/artifact-commands.test.ts +0 -92
  419. package/src/tests/artifact-sdk.test.ts +0 -793
  420. package/src/tests/register-service-security.test.ts +0 -194
  421. package/src/tools/list-services.ts +0 -107
  422. package/src/tools/register-service.ts +0 -177
  423. package/src/tools/unregister-service.ts +0 -73
  424. package/src/tools/update-service-status.ts +0 -96
  425. package/templates/skills/artifacts/config.json +0 -14
  426. package/templates/skills/artifacts/content.md +0 -344
  427. package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
  428. package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
  429. package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
  430. package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
  431. /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
  432. /package/src/{artifact-sdk → pages}/feedback-overlay.ts +0 -0
@@ -0,0 +1,172 @@
1
+ import {
2
+ getPendingChatRelayTasks,
3
+ giveUpStaleChatRelays,
4
+ markChatRelayAttempted,
5
+ markChatRelayDelivered,
6
+ markChatRelayGivenUp,
7
+ recordChatRelayError,
8
+ } from "@/be/db";
9
+ import { chatThreadOfContextKey } from "@/tasks/context-key";
10
+ import type { AgentTask } from "@/types";
11
+ import { scrubSecrets } from "@/utils/secret-scrubber";
12
+ import { workflowEventBus } from "@/workflows/event-bus";
13
+ import { errorMessage } from "./logger";
14
+ import { postChatMessage } from "./outbound";
15
+ import { getChatInstance, getChatSdkHealth } from "./runtime";
16
+ import { sweepExpired } from "./state";
17
+ import { stopTypingKeepalive } from "./typing";
18
+
19
+ /**
20
+ * Stop retrying a relay row this long after its obligation was created. Time,
21
+ * not attempts: with exponential backoff a short platform outage would
22
+ * otherwise burn a fixed attempt budget in minutes.
23
+ */
24
+ export const RELAY_GIVE_UP_AFTER_MS = 24 * 60 * 60 * 1000;
25
+
26
+ /**
27
+ * Run `sweepExpired` on every Nth tick, including event-triggered ticks (so
28
+ * about once a minute when only the 3 s interval ticks).
29
+ */
30
+ const SWEEP_EVERY_N_TICKS = 20;
31
+
32
+ /** Coalesce bursts of terminal-task events into one early tick. */
33
+ const EARLY_TICK_DEBOUNCE_MS = 250;
34
+
35
+ /** The text posted for a terminal task. Failed and cancelled tasks get a status line first. */
36
+ export function formatChatRelayText(task: AgentTask): string {
37
+ const body = (task.output || task.failureReason || "").trim();
38
+ if (task.status === "failed") return body ? `**Task failed.**\n\n${body}` : "**Task failed.**";
39
+ if (task.status === "cancelled") {
40
+ return body ? `**Task cancelled.**\n\n${body}` : "**Task cancelled.**";
41
+ }
42
+ return body || "Task completed.";
43
+ }
44
+
45
+ /**
46
+ * Deliver every pending, out-of-backoff chat relay row for a running adapter.
47
+ * Exported with an injectable clock so retry and give-up behavior is testable
48
+ * without waiting.
49
+ *
50
+ * Delivery is at-least-once: a chunk failure re-posts the earlier chunks on
51
+ * retry, and a crash between the send and `markChatRelayDelivered` re-sends the
52
+ * message.
53
+ *
54
+ * Once `signal` aborts, no further row is claimed. A send already in flight is
55
+ * not cancelled (`postChatMessage` takes no signal); its row keeps the claim
56
+ * and retries after the normal backoff.
57
+ */
58
+ export async function processPendingChatRelays(
59
+ now: number = Date.now(),
60
+ signal?: AbortSignal,
61
+ ): Promise<void> {
62
+ // Rows older than the give-up age are retired first, so rows of a stopped
63
+ // adapter do not stay pending forever.
64
+ const retired = await giveUpStaleChatRelays(now, RELAY_GIVE_UP_AFTER_MS);
65
+ if (retired > 0) console.warn(`[chat-sdk] Gave up relaying ${retired} stale task(s)`);
66
+
67
+ const running = Object.entries(getChatSdkHealth())
68
+ .filter(([, adapterHealth]) => adapterHealth?.status === "ok")
69
+ .map(([name]) => name);
70
+
71
+ for (const pending of await getPendingChatRelayTasks(now, 200, running)) {
72
+ if (signal?.aborted) return;
73
+ const { task } = pending;
74
+ const target = chatThreadOfContextKey(task.contextKey);
75
+ // `created_at` is ISO text with a `Z` suffix (migration 217 default).
76
+ const createdAtMs = Date.parse(pending.createdAt);
77
+
78
+ if (Number.isFinite(createdAtMs) && now - createdAtMs >= RELAY_GIVE_UP_AFTER_MS) {
79
+ const reason = pending.lastError ?? "no successful delivery within 24 hours";
80
+ if (await markChatRelayGivenUp(task.id, reason, now)) {
81
+ console.warn(scrubSecrets(`[chat-sdk] Gave up relaying task ${task.id}: ${reason}`));
82
+ }
83
+ if (target) stopTypingKeepalive(target.threadId);
84
+ continue;
85
+ }
86
+ if (!target) {
87
+ // The trigger only enqueues chat-sdk keys; a key that no longer parses
88
+ // can never be delivered.
89
+ await markChatRelayGivenUp(task.id, "task has no chat-sdk contextKey", now);
90
+ continue;
91
+ }
92
+ // Stopping adapter: skip without counting an attempt.
93
+ if (!getChatInstance(target.adapter)) continue;
94
+
95
+ if (!(await markChatRelayAttempted(task.id, now))) continue;
96
+ try {
97
+ await postChatMessage({
98
+ adapter: target.adapter,
99
+ threadId: target.threadId,
100
+ text: formatChatRelayText(task),
101
+ taskId: task.id,
102
+ });
103
+ stopTypingKeepalive(target.threadId);
104
+ await markChatRelayDelivered(task.id, now);
105
+ console.log(`[chat-sdk] Relayed ${task.status} task ${task.id} to ${target.adapter}`);
106
+ } catch (error) {
107
+ const message = errorMessage(error);
108
+ await recordChatRelayError(task.id, message, now);
109
+ console.error(`[chat-sdk] Relay of task ${task.id} failed: ${message}`);
110
+ }
111
+ }
112
+ }
113
+
114
+ let interval: ReturnType<typeof setInterval> | undefined;
115
+ let earlyTick: ReturnType<typeof setTimeout> | undefined;
116
+ let processing: Promise<void> | undefined;
117
+ let stopController: AbortController | undefined;
118
+ let tickCount = 0;
119
+
120
+ async function tick(): Promise<void> {
121
+ if (processing) return;
122
+ tickCount += 1;
123
+ const now = Date.now();
124
+ processing = (async () => {
125
+ try {
126
+ await processPendingChatRelays(now, stopController?.signal);
127
+ if (tickCount % SWEEP_EVERY_N_TICKS === 0) await sweepExpired(now);
128
+ } catch (error) {
129
+ console.error(`[chat-sdk] Relay tick failed: ${errorMessage(error)}`);
130
+ }
131
+ })();
132
+ try {
133
+ await processing;
134
+ } finally {
135
+ processing = undefined;
136
+ }
137
+ }
138
+
139
+ function scheduleEarlyTick(): void {
140
+ if (!interval || earlyTick) return;
141
+ earlyTick = setTimeout(() => {
142
+ earlyTick = undefined;
143
+ void tick();
144
+ }, EARLY_TICK_DEBOUNCE_MS);
145
+ }
146
+
147
+ const TERMINAL_EVENTS = ["task.completed", "task.failed", "task.cancelled"] as const;
148
+
149
+ /** Start the relay watcher. Idempotent. A terminal task event triggers an early tick. */
150
+ export function startChatRelayWatcher(intervalMs = 3000): void {
151
+ if (interval) return;
152
+ tickCount = 0;
153
+ stopController = new AbortController();
154
+ interval = setInterval(() => void tick(), intervalMs);
155
+ for (const event of TERMINAL_EVENTS) workflowEventBus.on(event, scheduleEarlyTick);
156
+ void tick();
157
+ }
158
+
159
+ /**
160
+ * Stop the watcher: no new tick, no new claim. The returned promise settles
161
+ * when an in-flight tick finishes, which an unanswered platform call can delay
162
+ * without bound, so shutdown waits for it under its own deadline.
163
+ */
164
+ export async function stopChatRelayWatcher(): Promise<void> {
165
+ stopController?.abort();
166
+ if (interval) clearInterval(interval);
167
+ interval = undefined;
168
+ if (earlyTick) clearTimeout(earlyTick);
169
+ earlyTick = undefined;
170
+ for (const event of TERMINAL_EVENTS) workflowEventBus.off(event, scheduleEarlyTick);
171
+ if (processing) await processing;
172
+ }
@@ -0,0 +1,169 @@
1
+ import {
2
+ deleteKv,
3
+ getAgentById,
4
+ getAgentWorkingOnContextKey,
5
+ getAllAgents,
6
+ getKv,
7
+ getLeadAgent,
8
+ upsertKv,
9
+ } from "@/be/db";
10
+ import { isAgentReachable } from "@/be/runtime-pools/reachability";
11
+ import { chatSdkContextKey } from "@/tasks/context-key";
12
+ import type { Agent, RoutingReason } from "@/types";
13
+ import { scrubSecrets } from "@/utils/secret-scrubber";
14
+
15
+ /** KV namespace of the channel-to-agent/workflow mapping, written by `register-chat-channel`. */
16
+ export const CHAT_CHANNELS_NAMESPACE = "integrations:chat:channels";
17
+
18
+ export interface ChatChannelMapping {
19
+ agentId?: string;
20
+ workflowId?: string;
21
+ name?: string;
22
+ createdAt?: string;
23
+ }
24
+
25
+ export interface ChatAgentMatch {
26
+ /** Null when no lead exists: the task is created unassigned and queued. */
27
+ agent: Agent | null;
28
+ reason: RoutingReason;
29
+ matchedText: string;
30
+ }
31
+
32
+ export type ChatRouteTarget =
33
+ | { kind: "agents"; matches: ChatAgentMatch[] }
34
+ | { kind: "workflow"; workflowId: string };
35
+
36
+ export interface ChatRouteContext {
37
+ adapter: string;
38
+ channelId: string;
39
+ threadId: string;
40
+ botMentioned: boolean;
41
+ isDirectMessage: boolean;
42
+ }
43
+
44
+ /**
45
+ * KV key of a channel mapping. SDK channel ids already carry the adapter
46
+ * prefix (`telegram:555001`, `slack:C123`); the prefix is added only when an
47
+ * adapter returns a bare id, so the key is always `<adapter>:<platform id>`.
48
+ */
49
+ export function chatChannelKey(adapter: string, channelId: string): string {
50
+ return channelId.startsWith(`${adapter}:`) ? channelId : `${adapter}:${channelId}`;
51
+ }
52
+
53
+ export async function getChatChannelMapping(
54
+ adapter: string,
55
+ channelId: string,
56
+ ): Promise<ChatChannelMapping | null> {
57
+ const row = await getKv(CHAT_CHANNELS_NAMESPACE, chatChannelKey(adapter, channelId));
58
+ if (!row || typeof row.value !== "object" || row.value === null) return null;
59
+ return row.value as ChatChannelMapping;
60
+ }
61
+
62
+ /** Store a channel mapping under `chatChannelKey(adapter, channelId)`. Returns the key. */
63
+ export async function putChatChannelMapping(
64
+ adapter: string,
65
+ channelId: string,
66
+ mapping: ChatChannelMapping,
67
+ ): Promise<string> {
68
+ const key = chatChannelKey(adapter, channelId);
69
+ await upsertKv({
70
+ namespace: CHAT_CHANNELS_NAMESPACE,
71
+ key,
72
+ value: mapping,
73
+ valueType: "json",
74
+ expiresAt: null,
75
+ });
76
+ return key;
77
+ }
78
+
79
+ export async function deleteChatChannelMapping(
80
+ adapter: string,
81
+ channelId: string,
82
+ ): Promise<boolean> {
83
+ return deleteKv(CHAT_CHANNELS_NAMESPACE, chatChannelKey(adapter, channelId));
84
+ }
85
+
86
+ /** Remove `swarm#<uuid>` and `swarm#all` routing tokens from task text. */
87
+ export function stripRoutingTokens(text: string): string {
88
+ return text
89
+ .replace(/swarm#[a-f0-9-]{36}/gi, "")
90
+ .replace(/swarm#all/gi, "")
91
+ .trim();
92
+ }
93
+
94
+ async function leadMatch(reachable: Agent[], matchedText: string): Promise<ChatAgentMatch> {
95
+ const lead = reachable.find((agent) => agent.isLead) ?? (await getLeadAgent());
96
+ return { agent: lead, reason: "skill", matchedText };
97
+ }
98
+
99
+ /**
100
+ * Port of `src/slack/router.ts` for Chat SDK threads. Order: `swarm#<uuid>`,
101
+ * `swarm#all`, KV channel mapping (agent, or workflow for a mention or DM), the
102
+ * thread owner (lead when the owner is offline), the lead for a mention or DM.
103
+ * Returns null when the message is not addressed to the swarm.
104
+ */
105
+ export async function routeChatMessage(
106
+ text: string,
107
+ ctx: ChatRouteContext,
108
+ ): Promise<ChatRouteTarget | null> {
109
+ const allAgents = await getAllAgents();
110
+ const reachableFlags = await Promise.all(allAgents.map((agent) => isAgentReachable(agent)));
111
+ const reachable = allAgents.filter((_, index) => reachableFlags[index]);
112
+
113
+ const matches: ChatAgentMatch[] = [];
114
+ for (const match of text.matchAll(/swarm#([a-f0-9-]{36})/gi)) {
115
+ const agentId = match[1];
116
+ if (!agentId || matches.some((m) => m.agent?.id === agentId)) continue;
117
+ const agent = await getAgentById(agentId);
118
+ if (agent && (await isAgentReachable(agent))) {
119
+ matches.push({ agent, reason: "human_pinned", matchedText: match[0] });
120
+ }
121
+ }
122
+ if (/swarm#all/i.test(text)) {
123
+ for (const agent of reachable) {
124
+ if (agent.isLead || matches.some((m) => m.agent?.id === agent.id)) continue;
125
+ matches.push({ agent, reason: "human_pinned", matchedText: "swarm#all" });
126
+ }
127
+ }
128
+ if (matches.length > 0) return { kind: "agents", matches };
129
+
130
+ const mapping = await getChatChannelMapping(ctx.adapter, ctx.channelId);
131
+ // A workflow run has no continuity: an unaddressed reply in the subscribed
132
+ // thread must not start another run. It still reaches a thread owner below.
133
+ if (mapping?.workflowId && (ctx.botMentioned || ctx.isDirectMessage)) {
134
+ return { kind: "workflow", workflowId: mapping.workflowId };
135
+ }
136
+ if (mapping?.agentId) {
137
+ const agent = await getAgentById(mapping.agentId);
138
+ if (agent) {
139
+ return {
140
+ kind: "agents",
141
+ matches: [{ agent, reason: "human_pinned", matchedText: "channel mapping" }],
142
+ };
143
+ }
144
+ console.warn(
145
+ scrubSecrets(
146
+ `[chat-sdk] Channel mapping for ${ctx.channelId} names unknown agent ${mapping.agentId}; using the default route`,
147
+ ),
148
+ );
149
+ }
150
+
151
+ const owner = await getAgentWorkingOnContextKey(chatSdkContextKey(ctx.threadId), "chat");
152
+ if (owner && (await isAgentReachable(owner))) {
153
+ return {
154
+ kind: "agents",
155
+ matches: [{ agent: owner, reason: "continuity", matchedText: "thread follow-up" }],
156
+ };
157
+ }
158
+ if (owner) {
159
+ return {
160
+ kind: "agents",
161
+ matches: [await leadMatch(reachable, "thread follow-up (lead fallback)")],
162
+ };
163
+ }
164
+
165
+ if (ctx.botMentioned || ctx.isDirectMessage) {
166
+ return { kind: "agents", matches: [await leadMatch(reachable, "@bot")] };
167
+ }
168
+ return null;
169
+ }
@@ -0,0 +1,351 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { Chat } from "chat";
3
+ import { isEnvFlagEnabled } from "@/utils/env-flag";
4
+ import { registerVolatileSecret, scrubSecrets } from "@/utils/secret-scrubber";
5
+ import { loadAdapter, watchSocketConnection } from "./adapters";
6
+ import { drainBackground } from "./background";
7
+ import { registerTelegramCommands } from "./commands";
8
+ import {
9
+ assertNoSlackClash,
10
+ CHAT_SDK_ADAPTER_NAMES,
11
+ type ChatSdkAdapterName,
12
+ type ChatSdkConfig,
13
+ getChatSdkConfig,
14
+ type ResolvedAdapterConfig,
15
+ } from "./config";
16
+ import {
17
+ type DiscordGatewayHealth,
18
+ getDiscordGatewayHealth,
19
+ isDiscordGatewayListener,
20
+ resetDiscordGatewayHealth,
21
+ startDiscordGateway,
22
+ stopDiscordGateway,
23
+ } from "./discord-gateway";
24
+ import { registerChatHandlers } from "./handlers";
25
+ import { createScrubbingLogger, errorMessage } from "./logger";
26
+ import { startChatRelayWatcher, stopChatRelayWatcher } from "./relay";
27
+ import { SqliteChatState } from "./state";
28
+ import { getChatSdkTestHooks } from "./test-hooks";
29
+ import { flushAllChatBuffers } from "./thread-buffer";
30
+ import { stopAllTypingKeepalives } from "./typing";
31
+
32
+ export interface ChatAdapterHealth {
33
+ status: "ok" | "error";
34
+ error?: string;
35
+ /** Last warn/error line the adapter logged, scrubbed. */
36
+ lastWarning?: string;
37
+ since: string;
38
+ /** Discord only: the supervised Gateway listener loop. */
39
+ gateway?: DiscordGatewayHealth;
40
+ }
41
+
42
+ /**
43
+ * One `Chat` per adapter. `Chat.initialize()` runs every adapter under one
44
+ * `Promise.all` and caches a failure forever, so a shared instance would let one
45
+ * broken adapter take every other adapter down. Health lives here, outside
46
+ * `Chat`, for the same reason.
47
+ */
48
+ const instances = new Map<ChatSdkAdapterName, Chat>();
49
+ const health = new Map<ChatSdkAdapterName, ChatAdapterHealth>();
50
+ let stopping = false;
51
+ /** Fingerprint of the config the last `startChatSdk` applied. */
52
+ let appliedFingerprint: string | undefined;
53
+
54
+ /** True while shutdown drains; the webhook path refuses new work. */
55
+ export function isChatSdkStopping(): boolean {
56
+ return stopping;
57
+ }
58
+
59
+ const DEFAULT_CHAT_USER_NAME = "swarm";
60
+
61
+ /** Total wait for every step of `stopChatSdk`, inside the 15 s SIGTERM budget. */
62
+ const SHUTDOWN_BUDGET_MS = 10_000;
63
+ /** A config reload waits this long for the adapters to come back before it answers. */
64
+ const RELOAD_START_BUDGET_MS = 15_000;
65
+
66
+ /** Fields whose string value is a credential, registered with the scrubber. */
67
+ const SECRET_FIELDS = ["botToken", "secretToken", "signingSecret", "appToken", "appPassword"];
68
+
69
+ function setHealth(
70
+ name: ChatSdkAdapterName,
71
+ status: "ok" | "error",
72
+ error: string | undefined,
73
+ lastWarning: string | undefined,
74
+ ): void {
75
+ health.set(name, { status, error, lastWarning, since: new Date().toISOString() });
76
+ }
77
+
78
+ /**
79
+ * Raw values in `CHAT_SDK_ADAPTERS` (env-only deployments) are not named by a
80
+ * sensitive env key, so the scrubber would not know them. `$config`-resolved
81
+ * secret rows are already registered by `loadGlobalConfigsIntoEnv`.
82
+ */
83
+ async function registerAdapterSecrets(
84
+ name: ChatSdkAdapterName,
85
+ config: ResolvedAdapterConfig,
86
+ ): Promise<void> {
87
+ for (const field of SECRET_FIELDS) {
88
+ const raw = config[field];
89
+ const value = typeof raw === "function" ? await (raw as () => Promise<string>)() : raw;
90
+ if (typeof value === "string") {
91
+ registerVolatileSecret(value, `chat-sdk:${name}:${field}`);
92
+ }
93
+ }
94
+ }
95
+
96
+ async function startAdapter(
97
+ name: ChatSdkAdapterName,
98
+ config: ResolvedAdapterConfig,
99
+ state: SqliteChatState,
100
+ ChatClass: typeof Chat,
101
+ ): Promise<void> {
102
+ let lastWarning: string | undefined;
103
+ // No level prefix on the stored line: the message already says what failed,
104
+ // and the status payload carries the tone separately.
105
+ const logger = createScrubbingLogger(name, (_level, message) => {
106
+ lastWarning = message;
107
+ const current = health.get(name);
108
+ if (current) health.set(name, { ...current, lastWarning });
109
+ });
110
+ try {
111
+ await registerAdapterSecrets(name, config);
112
+ const adapter = await loadAdapter(name, config, logger);
113
+ const chat = new ChatClass({
114
+ adapters: { [name]: adapter },
115
+ state,
116
+ // Chat requires a userName. The adapter only gets one when the operator
117
+ // set it, so Telegram can still take the real bot name from getMe.
118
+ userName: typeof config.userName === "string" ? config.userName : DEFAULT_CHAT_USER_NAME,
119
+ logger,
120
+ // The swarm buffers bursts itself (Phase 2). The SDK "queue" strategy
121
+ // would keep only the latest queued message and skip the rest.
122
+ concurrency: "concurrent",
123
+ });
124
+ registerChatHandlers(chat);
125
+ instances.set(name, chat);
126
+ await chat.initialize();
127
+ // Shutdown began while this adapter initialized: do not mark it ready or
128
+ // start the Gateway after `stopChatSdk` already stopped it.
129
+ if (stopping) {
130
+ console.log(`[chat-sdk] ${name} initialized during shutdown, not started`);
131
+ return;
132
+ }
133
+ watchSocketConnection(adapter, logger);
134
+ if (name === "telegram") {
135
+ // The `/` menu in Telegram clients. The commands work without it.
136
+ try {
137
+ await registerTelegramCommands(config);
138
+ } catch (error) {
139
+ logger.warn(`Could not register the Telegram commands: ${errorMessage(error)}`);
140
+ }
141
+ }
142
+ setHealth(name, "ok", undefined, lastWarning);
143
+ console.log(`[chat-sdk] ${name} ready`);
144
+ // Discord messages arrive only over the Gateway, never the webhook.
145
+ if (
146
+ name === "discord" &&
147
+ isDiscordGatewayListener(adapter) &&
148
+ !getChatSdkTestHooks().disableDiscordGateway
149
+ ) {
150
+ startDiscordGateway(adapter);
151
+ }
152
+ } catch (error) {
153
+ setHealth(name, "error", errorMessage(error), lastWarning);
154
+ console.error(`[chat-sdk] ${name} failed to start: ${errorMessage(error)}`);
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Boot the Chat SDK channel. Never throws for config or adapter problems: they
160
+ * are logged and recorded in health, and the API keeps booting.
161
+ */
162
+ export async function startChatSdk(): Promise<void> {
163
+ stopping = false;
164
+ const config = await getChatSdkConfig();
165
+ appliedFingerprint = await configFingerprint(config);
166
+ if (!config.enabled) {
167
+ console.log("[chat-sdk] Disabled (CHAT_SDK_ENABLED is not true)");
168
+ return;
169
+ }
170
+ if (config.error) {
171
+ console.error(`[chat-sdk] Not started: ${scrubSecrets(config.error)}`);
172
+ return;
173
+ }
174
+ try {
175
+ assertNoSlackClash(config);
176
+ } catch (error) {
177
+ console.error(`[chat-sdk] Not started: ${errorMessage(error)}`);
178
+ return;
179
+ }
180
+ for (const [name, reason] of Object.entries(config.incomplete ?? {})) {
181
+ console.warn(`[chat-sdk] ${name} not started, incomplete: ${scrubSecrets(reason)}`);
182
+ }
183
+ const { Chat: ChatClass } = await import("chat");
184
+ const state = new SqliteChatState();
185
+ // In parallel: one slow adapter (Slack's WebClient retries auth.test for
186
+ // up to ~30 min on errors) must not delay the others.
187
+ // Before the adapters: rows for an adapter still initializing are skipped
188
+ // without counting an attempt, and pending rows from before a restart drain
189
+ // as soon as their adapter is up.
190
+ startChatRelayWatcher();
191
+ await Promise.all(
192
+ CHAT_SDK_ADAPTER_NAMES.map(async (name) => {
193
+ const adapterConfig = config.adapters[name];
194
+ if (adapterConfig) await startAdapter(name, adapterConfig, state, ChatClass);
195
+ }),
196
+ );
197
+ }
198
+
199
+ /** Only adapters whose `initialize()` succeeded serve webhooks. */
200
+ export function getChatInstance(name: string): Chat | undefined {
201
+ const adapterName = name as ChatSdkAdapterName;
202
+ return health.get(adapterName)?.status === "ok" ? instances.get(adapterName) : undefined;
203
+ }
204
+
205
+ /** True when the adapter is configured (started, even if it failed). */
206
+ export function isChatAdapterConfigured(name: string): boolean {
207
+ return health.has(name as ChatSdkAdapterName);
208
+ }
209
+
210
+ export function getChatSdkHealth(): Partial<Record<ChatSdkAdapterName, ChatAdapterHealth>> {
211
+ const out: Partial<Record<ChatSdkAdapterName, ChatAdapterHealth>> = Object.fromEntries(health);
212
+ const gateway = getDiscordGatewayHealth();
213
+ if (out.discord && gateway) out.discord = { ...out.discord, gateway };
214
+ return out;
215
+ }
216
+
217
+ /**
218
+ * Wait for `work`, at most `ms`. Never rejects: a rejection is logged, and a
219
+ * timeout logs the abandoned step. Returns true when `work` settled in time.
220
+ */
221
+ async function withTimeout(work: Promise<unknown>, ms: number, step: string): Promise<boolean> {
222
+ let timer: ReturnType<typeof setTimeout> | undefined;
223
+ const timeout = new Promise<false>((resolve) => {
224
+ timer = setTimeout(() => resolve(false), ms);
225
+ });
226
+ const settled = work.then(
227
+ () => true as const,
228
+ (error: unknown) => {
229
+ console.error(`[chat-sdk] ${step} failed: ${errorMessage(error)}`);
230
+ return true as const;
231
+ },
232
+ );
233
+ try {
234
+ const done = await Promise.race([settled, timeout]);
235
+ if (!done) console.warn(`[chat-sdk] Shutdown budget spent, abandoned: ${step}`);
236
+ return done;
237
+ } finally {
238
+ clearTimeout(timer);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Stop the channel inside one deadline (`budgetMs`, injectable for tests). Every
244
+ * wait below uses the time that is left, so a stuck platform call cannot hold
245
+ * the API shutdown.
246
+ */
247
+ export async function stopChatSdk(budgetMs: number = SHUTDOWN_BUDGET_MS): Promise<void> {
248
+ // Ingress first: webhooks answer 503 and refuse new work from here.
249
+ stopping = true;
250
+ stopAllTypingKeepalives();
251
+ const deadline = Date.now() + budgetMs;
252
+ const remaining = (): number => Math.max(0, deadline - Date.now());
253
+ // No new relay tick or claim from here. The wait for an in-flight send comes
254
+ // after the inbound flush, so a stuck send cannot use the flush's budget.
255
+ const relayStopped = stopChatRelayWatcher();
256
+ // Discord has no `disconnect()`, so `chat.shutdown()` would leave the
257
+ // Gateway open. Abort it before the drain: the open window is tracked
258
+ // background work, so the drain bounds the wait for it to close.
259
+ stopDiscordGateway();
260
+ // Let in-flight handlers finish buffering, then turn waiting threads into
261
+ // tasks while adapters and the DB are still up. Buffered messages exist only
262
+ // in memory: threads not flushed in time are lost (logged).
263
+ let drained = await drainBackground(remaining());
264
+ drained =
265
+ (await withTimeout(
266
+ flushAllChatBuffers(),
267
+ remaining(),
268
+ "inbound buffer flush (threads not yet turned into tasks are lost)",
269
+ )) && drained;
270
+ drained = (await drainBackground(remaining())) && drained;
271
+ // The relay posts through the adapters, so it must end before they shut
272
+ // down. An abandoned send keeps its claimed row: it retries after restart.
273
+ await withTimeout(
274
+ relayStopped,
275
+ remaining(),
276
+ "in-flight relay send (its obligation stays pending and retries after restart)",
277
+ );
278
+ await withTimeout(
279
+ Promise.all(
280
+ [...instances].map(async ([name, chat]) => {
281
+ try {
282
+ await chat.shutdown();
283
+ } catch (error) {
284
+ console.error(`[chat-sdk] ${name} shutdown failed: ${errorMessage(error)}`);
285
+ }
286
+ }),
287
+ ),
288
+ remaining(),
289
+ "adapter shutdown",
290
+ );
291
+ instances.clear();
292
+ health.clear();
293
+ resetDiscordGatewayHealth();
294
+ if (!drained) {
295
+ console.warn(
296
+ `[chat-sdk] Background handlers still running after the ${budgetMs / 1000}s shutdown budget`,
297
+ );
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Hash of the resolved config plus `SLACK_DISABLE` (the Slack clash input).
303
+ * Resolver fields are functions, so their values are read first: a rotated
304
+ * `$config` secret changes the hash. The hashed JSON holds secrets: never log it.
305
+ */
306
+ async function configFingerprint(config: ChatSdkConfig): Promise<string> {
307
+ const adapters: Record<string, Record<string, unknown>> = {};
308
+ for (const [name, adapterConfig] of Object.entries(config.adapters)) {
309
+ const fields: Record<string, unknown> = {};
310
+ for (const [key, value] of Object.entries(adapterConfig ?? {})) {
311
+ fields[key] = typeof value === "function" ? await (value as () => Promise<unknown>)() : value;
312
+ }
313
+ adapters[name] = fields;
314
+ }
315
+ const slackDisabled = isEnvFlagEnabled("SLACK_DISABLE", false);
316
+ // Incomplete adapters are not part of the running state, so editing one
317
+ // does not restart the adapters that run.
318
+ const { incomplete: _incomplete, disabled: _disabled, ...rest } = config;
319
+ return createHash("sha256")
320
+ .update(JSON.stringify({ ...rest, adapters, slackDisabled }))
321
+ .digest("hex");
322
+ }
323
+
324
+ /**
325
+ * Config reload hook: restart the adapters when the config differs from the
326
+ * one the last `startChatSdk` applied, so a dashboard save applies without an
327
+ * API restart. Compared against the applied config, not the pre-reload one:
328
+ * `$config` secrets resolve from the DB, which already holds the new row. An
329
+ * equal config leaves Telegram polling and the Discord Gateway alone. Never
330
+ * throws. Returns true when the restart left at least one adapter configured.
331
+ */
332
+ export async function restartChatSdkIfConfigChanged(): Promise<boolean> {
333
+ try {
334
+ if ((await configFingerprint(await getChatSdkConfig())) === appliedFingerprint) return false;
335
+ console.log("[chat-sdk] Configuration changed, restarting adapters");
336
+ await stopChatSdk();
337
+ // Bounded: a Slack adapter can retry auth.test for a long time, and the
338
+ // reload must not hold every later config save behind it. The start
339
+ // continues in the background after the budget.
340
+ const started = await withTimeout(startChatSdk(), RELOAD_START_BUDGET_MS, "start after reload");
341
+ console.log(
342
+ started
343
+ ? `[chat-sdk] Restart done, adapters configured: ${health.size}`
344
+ : "[chat-sdk] Restart still in progress after the reload budget; check the card status",
345
+ );
346
+ return health.size > 0;
347
+ } catch (error) {
348
+ console.error(`[chat-sdk] Restart after configuration change failed: ${errorMessage(error)}`);
349
+ return false;
350
+ }
351
+ }