@desplega.ai/agent-swarm 1.173.0 → 2.1.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.
- package/README.md +1 -1
- package/dist/{acp-adapter-ccm9xrep.js → acp-adapter-kr4abrsk.js} +17 -16
- package/dist/{actions-sgeyv08k.js → actions-eh4g6w2r.js} +39 -34
- package/dist/{amp-adapter-gpxp3kc9.js → amp-adapter-7h7wdt8q.js} +8 -7
- package/dist/{app-km9qnv2f.js → app-7k8s9w5p.js} +18 -14
- package/dist/{assistant-5r8tcvvd.js → assistant-wswt2y7p.js} +40 -35
- package/dist/{boot-reembed-16rv9843.js → boot-reembed-9apm9qw6.js} +10 -9
- package/dist/{boot-reembed-swwzdgd7.js → boot-reembed-whzbdthj.js} +10 -9
- package/dist/{boot-scrub-sweep-zkr6vkqb.js → boot-scrub-sweep-ndzmvmgt.js} +11 -10
- package/dist/{boot-seeds-d8sp49t2.js → boot-seeds-e9sta0yt.js} +16 -13
- package/dist/{claude-adapter-yzzzh8jp.js → claude-adapter-4mqd3r6r.js} +19 -18
- package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-9wswb52m.js} +7 -5
- package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-tpe1effx.js} +6 -4
- package/dist/{claude-sdk-session-4whxere4.js → claude-sdk-session-q49ghmh9.js} +12402 -11850
- package/dist/{cli-ne7smj1b.js → cli-01xvj1rq.js} +10 -10
- package/dist/{cli-nwrdx840.js → cli-035vekcw.js} +3 -3
- package/dist/{cli-9xf7fdtp.js → cli-0pd8hzhc.js} +29 -6
- package/dist/{cli-tmhsxhb8.js → cli-0veb92a3.js} +28 -6
- package/dist/{cli-c3rk1v0x.js → cli-10jesr25.js} +3 -3
- package/dist/{cli-6thk4jz3.js → cli-1y7yywmj.js} +6 -6
- package/dist/{cli-6r2f7edk.js → cli-20krdhz5.js} +47 -19
- package/dist/{cli-rfs4vddv.js → cli-29rja109.js} +39 -20
- package/dist/cli-2m1ktfzz.js +17795 -0
- package/dist/{cli-na2pxe4f.js → cli-2pwa0q4d.js} +15 -4
- package/dist/{cli-7a93cv8c.js → cli-4mengw46.js} +441 -43
- package/dist/{cli-94b48jhk.js → cli-5twdke5x.js} +16 -14
- package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
- package/dist/cli-6cvfxs8r.js +47 -0
- package/dist/{cli-w1apjecb.js → cli-711vwbxn.js} +2 -2
- package/dist/{cli-r4fqfpwf.js → cli-78vv7446.js} +50 -27
- package/dist/{cli-srqq3gc5.js → cli-7n6s5man.js} +5 -5
- package/dist/{cli-m89t9jdf.js → cli-7x0g4sn1.js} +15416 -16024
- package/dist/{cli-2bw3w47j.js → cli-8wwxw8a3.js} +1 -1
- package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
- package/dist/{cli-ywm1azg9.js → cli-9jzdj90z.js} +8 -4
- package/dist/cli-9traj3dn.js +638 -0
- package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
- package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
- package/dist/cli-cvv9k0hh.js +10143 -0
- package/dist/{cli-anzdk0z4.js → cli-d9286m9h.js} +2 -2
- package/dist/cli-e3g5mbkz.js +776 -0
- package/dist/cli-f527ce74.js +781 -0
- package/dist/{cli-gmdajfw5.js → cli-gxzkg1de.js} +5020 -1729
- package/dist/{cli-451ks6aw.js → cli-h1arqn31.js} +1 -1
- package/dist/cli-hjwmhcg1.js +10622 -0
- package/dist/{cli-jpc4r224.js → cli-hqr88ew6.js} +19 -3
- package/dist/{cli-h7k0xtry.js → cli-hvqbtna4.js} +4 -1256
- package/dist/{cli-cd0hv9nh.js → cli-j67mh2vg.js} +2 -2
- package/dist/cli-jta2eyzc.js +619 -0
- package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
- package/dist/cli-kceah1eh.js +92 -0
- package/dist/{cli-d603qf86.js → cli-ksphgh48.js} +10 -8
- package/dist/{cli-3nxdmjrc.js → cli-mgz4nbgg.js} +4 -4
- package/dist/{cli-saqyage0.js → cli-mz5as5ha.js} +179 -27
- package/dist/cli-n2wq4j7m.js +6985 -0
- package/dist/{cli-ppy4nnnh.js → cli-n8ndw8j7.js} +2 -2
- package/dist/{cli-1w0vj3gw.js → cli-naqzr17j.js} +7 -7
- package/dist/{cli-0a7mm3zp.js → cli-nm1p8jfr.js} +25 -21
- package/dist/{cli-wrh08zjd.js → cli-p3zbc1rw.js} +5 -18
- package/dist/{cli-hsem16hb.js → cli-pe05f8yk.js} +29 -6
- package/dist/{cli-5wpw4rab.js → cli-qmzsbkvy.js} +5 -5
- package/dist/{cli-bv2etw2y.js → cli-qnm4e9e4.js} +99 -590
- package/dist/{cli-1x7da0de.js → cli-qqy4q48b.js} +8 -8
- package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
- package/dist/{cli-5er5gjbm.js → cli-r35rw5de.js} +15 -15
- package/dist/{cli-v3zhnxsb.js → cli-r610p9bs.js} +2 -2
- package/dist/{cli-7jykv7ef.js → cli-rc6pf5kv.js} +1 -1
- package/dist/{cli-x119kdba.js → cli-rdxm77k3.js} +1 -1
- package/dist/cli-rhs9pz33.js +294 -0
- package/dist/{cli-0p3enk35.js → cli-s69nfyeh.js} +9 -1
- package/dist/{cli-0kxf81jj.js → cli-sawv63e8.js} +2 -2
- package/dist/{cli-e3pmyt39.js → cli-sg19gbf0.js} +1 -1
- package/dist/{cli-d161g69q.js → cli-shk7r5cs.js} +57 -6
- package/dist/{cli-7pz3b7x5.js → cli-sksetb2q.js} +36 -1
- package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
- package/dist/{cli-db3m6zdt.js → cli-t02z59f2.js} +1 -1
- package/dist/{cli-b07hr3bt.js → cli-txqb9s38.js} +2 -2
- package/dist/{cli-s8z6x4b4.js → cli-v7g551te.js} +225 -113
- package/dist/{cli-rfz2ve55.js → cli-vgqzp4em.js} +3341 -955
- package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
- package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
- package/dist/cli-wfnemr1t.js +86 -0
- package/dist/cli-wmgrkscj.js +23254 -0
- package/dist/{cli-qm1448f9.js → cli-wms6x9db.js} +8 -8
- package/dist/{cli-tadfgd0t.js → cli-x1r545x8.js} +33 -4
- package/dist/{cli-xc7p7s3g.js → cli-xczsgshn.js} +6 -6
- package/dist/{cli-fmhj1ehj.js → cli-y3167wt1.js} +882 -271
- package/dist/{cli-r0byd61h.js → cli-yjmqpxt3.js} +17 -16
- package/dist/{cli-81r1wjyv.js → cli-z780ec7c.js} +1 -1
- package/dist/{cli-phcqp1b9.js → cli-zm9t2vde.js} +2 -2
- package/dist/cli.js +34 -49
- package/dist/{codex-adapter-r3dn4fp5.js → codex-adapter-4z16v9n0.js} +15 -14
- package/dist/{codex-login-841kv5fk.js → codex-login-pkepy2j1.js} +3 -2
- package/dist/{codex-session-runner-fwt793h4.js → codex-session-runner-rvms9y2x.js} +15 -14
- package/dist/{commands-z8fq9qna.js → commands-2a9rtyat.js} +11 -10
- package/dist/{controller-hq5ggpcp.js → controller-3k3madbn.js} +5 -4
- package/dist/{controllers-connect-3mf2btyj.js → controllers-connect-v9yn8zb9.js} +42 -37
- package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-98gbhyzn.js} +7 -6
- package/dist/{db-qfpq4bt7.js → db-eywwsnjw.js} +30 -9
- package/dist/{db-yyhqqrbf.js → db-ncypk9np.js} +12 -11
- package/dist/{devin-adapter-xgtj680j.js → devin-adapter-zeq8b9gz.js} +3 -2
- package/dist/{dispatcher-x0x7qfcq.js → dispatcher-w94kc575.js} +38 -33
- package/dist/{docker-f503k9p0.js → docker-b1e4cqab.js} +3 -2
- package/dist/{dsh-adapter-34etvb5m.js → dsh-adapter-47ztej36.js} +7 -6
- package/dist/{e2b-66xra4az.js → e2b-bbt8ey45.js} +4 -3
- package/dist/{e2b-xwhyj8cw.js → e2b-dp804sen.js} +4 -3
- package/dist/{event-bus-vjtv82gm.js → event-bus-j2jdg3qb.js} +3 -2
- package/dist/{external-channel-h1fjecwe.js → external-channel-zrkg8ar9.js} +11 -10
- package/dist/{google-generative-ai-szdxbpqg.js → google-generative-ai-g8db7x0f.js} +4 -2
- package/dist/{google-vertex-qzw3p36d.js → google-vertex-r2692q0r.js} +4 -2
- package/dist/{grok-adapter-e7mr287e.js → grok-adapter-s9ncp9vy.js} +18 -17
- package/dist/{handlers-znb9n7d4.js → handlers-kqftvjgw.js} +41 -36
- package/dist/{heartbeat-bm4qkn8d.js → heartbeat-9q96p8v9.js} +43 -38
- package/dist/{hook-254r4mza.js → hook-56qe10dp.js} +11 -23
- package/dist/{hook-xaa3q80s.js → hook-yy6rw42k.js} +12 -11
- package/dist/{http-3dgrgp8y.js → http-r2dnb8f8.js} +2411 -275
- package/dist/{index-fx3jqq1e.js → index-3gtmkhc6.js} +38 -33
- package/dist/{index-bdsqa799.js → index-3k5hkz7r.js} +43 -38
- package/dist/index-52j1aja4.js +3465 -0
- package/dist/{index-g2byveez.js → index-5e101240.js} +38 -33
- package/dist/{index-2ybk7khy.js → index-5wyfjc2c.js} +845 -683
- package/dist/index-9gw58n4k.js +193 -0
- package/dist/index-9j87mcm8.js +56305 -0
- package/dist/index-bsmcresk.js +79424 -0
- package/dist/index-mh85qhec.js +475 -0
- package/dist/{index-jd7sejm2.js → index-vxvhxxbn.js} +10 -9
- package/dist/index-we27kd31.js +12810 -0
- package/dist/{keepalive-6ywhpja1.js → keepalive-psj6j54c.js} +12 -11
- package/dist/{lead-cw45n21s.js → lead-hajz4688.js} +61 -56
- package/dist/{maintenance-ss1a1ge9.js → maintenance-ps02sb5m.js} +10 -9
- package/dist/{node-fkajaq96.js → node-mwb5r697.js} +2 -1
- package/dist/{oauth-refresh-sweep-3c6f8vyy.js → oauth-refresh-sweep-0284h93b.js} +11 -10
- package/dist/{onboard-8hzdhc1k.js → onboard-rbf6bg01.js} +11 -3
- package/dist/{opencode-adapter-qbchbcfz.js → opencode-adapter-13dcatgq.js} +18 -13
- package/dist/{otel-impl-e6mnvc4v.js → otel-impl-zvqcbhp7.js} +242 -868
- package/dist/{pi-mono-adapter-3c9g9snh.js → pi-mono-adapter-xxy47q4v.js} +244 -106
- package/dist/{pricing-refresh-gedkjdw0.js → pricing-refresh-kefx9v3t.js} +12 -11
- package/dist/{rbac-roles-dt4vh1yy.js → rbac-roles-3m36rxm2.js} +11 -10
- package/dist/{rbac-roles-eszdg6pm.js → rbac-roles-z76xv0hq.js} +12 -11
- package/dist/{render-v2-907cy6sa.js → render-v2-wz73bs25.js} +18 -14
- package/dist/{resume-42stcaz0.js → resume-k4wzge8w.js} +25 -21
- package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
- package/dist/{setup-adbc1c10.js → setup-2fp2pypp.js} +2 -2
- package/dist/{virtual-modules-rtmt494q.js → virtual-modules-9gkxqrap.js} +6 -5
- package/dist/{work-objects-wpj8ck95.js → work-objects-6k0ywz3m.js} +41 -36
- package/dist/{worker-bzdra9rf.js → worker-5v3rxsdh.js} +61 -56
- package/dist/{x-qfmygqqv.js → x-4mtdeym1.js} +3 -2
- package/openapi.json +1279 -329
- package/package.json +15 -15
- package/src/agentmail/handlers.ts +21 -11
- package/src/apps/definition.ts +16 -0
- package/src/azure-devops/api.ts +52 -2
- package/src/azure-devops/boards-sync.ts +50 -20
- package/src/azure-devops/index.ts +1 -0
- package/src/azure-devops/outbound.ts +95 -0
- package/src/be/boot-seeds.ts +11 -0
- package/src/be/config-refs.ts +60 -0
- package/src/be/db/model-profiles.ts +315 -0
- package/src/be/db/page-versions.ts +15 -0
- package/src/be/db/runtime.ts +35 -8
- package/src/be/db/skill-name-resolution.ts +36 -0
- package/src/be/db/tasks/read.ts +237 -0
- package/src/be/db/tasks/write.ts +7 -0
- package/src/be/db.ts +87 -36
- package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
- package/src/be/migrations/216_task_resolved_effort.sql +5 -0
- package/src/be/migrations/217_chat_sdk_state.sql +104 -0
- package/src/be/migrations/219_relabel_pi_context_compactions.sql +22 -0
- package/src/be/migrations/220_model_profiles.sql +137 -0
- package/src/be/model-catalog-store.ts +5 -1
- package/src/be/model-tier-resolution.ts +155 -10
- package/src/be/model-validation.ts +12 -2
- package/src/be/models-catalog.ts +26 -1
- package/src/be/modelsdev-cache.json +198 -63
- package/src/be/modelsdev-cache.ts +9 -1
- package/src/be/script-credential-broker.ts +39 -2
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +22 -8
- package/src/be/secret-registry.ts +6 -2
- package/src/be/seed/runner.ts +13 -1
- package/src/be/seed/state-db.ts +5 -0
- package/src/be/seed/types.ts +6 -0
- package/src/be/seed-model-profiles.ts +41 -0
- package/src/be/seed-pricing.ts +19 -2
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +368 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/seed-scripts/index.ts +9 -0
- package/src/be/seed-skills/bundled-files.generated.json +0 -18
- package/src/be/seed-skills/index.ts +104 -3
- package/src/be/swarm-config-guard.ts +53 -0
- package/src/cli.tsx +0 -16
- package/src/commands/onboard/non-interactive.ts +4 -2
- package/src/commands/provider-credentials.ts +52 -6
- package/src/commands/runner.ts +193 -71
- package/src/e2b/env.ts +5 -0
- package/src/extensions/catalog.generated.json +17 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +45 -0
- package/src/extensions/dispatcher.ts +7 -0
- package/src/extensions/loader.ts +10 -5
- package/src/extensions/post-bridge.ts +40 -0
- package/src/github/handlers.ts +11 -4
- package/src/heartbeat/heartbeat.ts +2 -1
- package/src/heartbeat/templates.ts +8 -5
- package/src/hooks/hook.ts +0 -26
- package/src/http/agents.ts +49 -2
- package/src/http/all-routes.ts +1 -1
- package/src/http/apps.ts +30 -3
- package/src/http/config.ts +62 -19
- package/src/http/core.ts +8 -0
- package/src/http/index.ts +25 -3
- package/src/http/integrations.ts +88 -1
- package/src/http/mcp.ts +15 -5
- package/src/http/model-profiles.ts +303 -0
- package/src/http/page-proxy.ts +2 -4
- package/src/http/pages-public.ts +13 -7
- package/src/http/pages.ts +3 -13
- package/src/http/poll.ts +42 -71
- package/src/http/requested-by.ts +54 -0
- package/src/http/script-runs.ts +1 -1
- package/src/http/scripts.ts +26 -2
- package/src/http/stats.ts +1 -30
- package/src/http/tasks.ts +79 -5
- package/src/http/utils.ts +14 -0
- package/src/http/webhooks.ts +160 -3
- package/src/http/x.ts +1 -0
- package/src/integrations/chat-sdk/adapters.ts +129 -0
- package/src/integrations/chat-sdk/attachments.ts +232 -0
- package/src/integrations/chat-sdk/background.ts +36 -0
- package/src/integrations/chat-sdk/commands.ts +194 -0
- package/src/integrations/chat-sdk/config.ts +198 -0
- package/src/integrations/chat-sdk/discord-api.ts +63 -0
- package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
- package/src/integrations/chat-sdk/handlers.ts +28 -0
- package/src/integrations/chat-sdk/inbound.ts +426 -0
- package/src/integrations/chat-sdk/limits.ts +48 -0
- package/src/integrations/chat-sdk/logger.ts +81 -0
- package/src/integrations/chat-sdk/outbound.ts +123 -0
- package/src/integrations/chat-sdk/relay.ts +172 -0
- package/src/integrations/chat-sdk/router.ts +169 -0
- package/src/integrations/chat-sdk/runtime.ts +351 -0
- package/src/integrations/chat-sdk/schema.ts +245 -0
- package/src/integrations/chat-sdk/state.ts +243 -0
- package/src/integrations/chat-sdk/status.ts +244 -0
- package/src/integrations/chat-sdk/steering.ts +58 -0
- package/src/integrations/chat-sdk/teams-origin.ts +77 -0
- package/src/integrations/chat-sdk/telegram-api.ts +56 -0
- package/src/integrations/chat-sdk/templates.ts +16 -0
- package/src/integrations/chat-sdk/test-hooks.ts +37 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
- package/src/integrations/chat-sdk/typing.ts +65 -0
- package/src/{artifact-sdk → pages}/feedback-overlay.ts +65 -6
- package/src/pages/version.ts +4 -5
- package/src/prompts/base-prompt.ts +111 -45
- package/src/providers/acp-target-catalog.ts +6 -0
- package/src/providers/acp-targets.ts +56 -3
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +1 -1
- package/src/providers/claude-flag-settings.ts +17 -5
- package/src/providers/claude-managed-adapter.ts +1 -1
- package/src/providers/claude-session-events.ts +6 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/opencode-adapter.ts +243 -26
- package/src/providers/opencode-swarm-plugin.ts +31 -0
- package/src/providers/pi-mono-adapter.ts +398 -153
- package/src/providers/pi-mono-extension.ts +17 -3
- package/src/providers/pricing-sources.md +33 -2
- package/src/providers/reasoning-effort.ts +37 -0
- package/src/providers/types.ts +6 -2
- package/src/rbac/legacy-policy.ts +16 -2
- package/src/rbac/permissions.ts +26 -0
- package/src/scheduler/deferred-task-waits.ts +18 -5
- package/src/scheduler/scheduler.ts +11 -3
- package/src/scripts-runtime/executors/quickjs-prelude.ts +75 -6
- package/src/scripts-runtime/executors/quickjs-runner.ts +85 -8
- package/src/scripts-runtime/executors/quickjs-web-shims.ts +1041 -0
- package/src/scripts-runtime/extract-args-schema.ts +9 -8
- package/src/scripts-runtime/loader.ts +19 -7
- package/src/scripts-runtime/quickjs-compat.ts +464 -0
- package/src/scripts-runtime/sdk-allowlist.ts +12 -6
- package/src/scripts-runtime/swarm-sdk.ts +0 -11
- package/src/scripts-runtime/types/stdlib.d.ts +39 -15
- package/src/scripts-runtime/types/swarm-extension.d.ts +66 -1
- package/src/scripts-runtime/types/swarm-sdk.d.ts +39 -15
- package/src/server.ts +62 -19
- package/src/slack/blocks.ts +4 -2
- package/src/slack/event-dedup.ts +1 -1
- package/src/slack/reaction-shortcode.ts +14 -0
- package/src/slack/render-v2.ts +12 -7
- package/src/slack/responses.ts +5 -4
- package/src/slack/thread-buffer.ts +41 -22
- package/src/tasks/additive-buffer.ts +4 -2
- package/src/tasks/context-key.ts +60 -1
- package/src/tasks/model-pin.ts +67 -0
- package/src/tasks/worker-follow-up.ts +15 -33
- package/src/tests/acp-adapter.test.ts +42 -0
- package/src/tests/agentmail-filters.test.ts +47 -0
- package/src/tests/api-drain-worker-e2e.test.ts +1 -6
- package/src/tests/apps-spike2.test.ts +35 -0
- package/src/tests/azure-devops-outbound.test.ts +420 -0
- package/src/tests/base-prompt.test.ts +58 -54
- package/src/tests/chat-sdk-commands.test.ts +191 -0
- package/src/tests/chat-sdk-config.test.ts +334 -0
- package/src/tests/chat-sdk-discord-api.test.ts +68 -0
- package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
- package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
- package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
- package/src/tests/chat-sdk-harness.ts +146 -0
- package/src/tests/chat-sdk-inbound.test.ts +363 -0
- package/src/tests/chat-sdk-relay.test.ts +470 -0
- package/src/tests/chat-sdk-reload.test.ts +128 -0
- package/src/tests/chat-sdk-shutdown.test.ts +220 -0
- package/src/tests/chat-sdk-slack.test.ts +268 -0
- package/src/tests/chat-sdk-state.test.ts +163 -0
- package/src/tests/chat-sdk-status-route.test.ts +382 -0
- package/src/tests/chat-sdk-teams.test.ts +277 -0
- package/src/tests/chat-sdk-tools.test.ts +520 -0
- package/src/tests/chat-sdk-typing.test.ts +120 -0
- package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
- package/src/tests/claude-adapter-binary.test.ts +9 -4
- package/src/tests/claude-flag-settings.test.ts +11 -3
- package/src/tests/claude-managed-adapter.test.ts +1 -1
- package/src/tests/claude-sdk-lifecycle.test.ts +9 -3
- package/src/tests/claude-thinking-tokens.test.ts +57 -0
- package/src/tests/claude-worker-parity.test.ts +2 -7
- package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
- package/src/tests/config-refs.test.ts +66 -0
- package/src/tests/context-key.test.ts +19 -0
- package/src/tests/cors-allowlist.test.ts +25 -0
- package/src/tests/create-page-tool.test.ts +18 -0
- package/src/tests/credential-check.test.ts +139 -0
- package/src/tests/credentials.test.ts +21 -0
- package/src/tests/db-runtime.test.ts +61 -0
- package/src/tests/db-task-read-contract.test.ts +22 -0
- package/src/tests/db-task-write-contract.test.ts +11 -0
- package/src/tests/deferred-task-wake.test.ts +78 -1
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/enabled-capabilities.test.ts +20 -0
- package/src/tests/entrypoint-api-readiness.test.ts +14 -8
- package/src/tests/entrypoint-config-env-export.test.ts +5 -2
- package/src/tests/entrypoint-export-to-env.test.ts +78 -0
- package/src/tests/env-flag.test.ts +1 -0
- package/src/tests/explicit-model-validation.test.ts +105 -1
- package/src/tests/extensions-catalog.test.ts +1 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +413 -0
- package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
- package/src/tests/extensions-loader.test.ts +15 -0
- package/src/tests/extensions-post-bridge.test.ts +51 -0
- package/src/tests/fetch-resolved-env.test.ts +128 -0
- package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
- package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
- package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
- package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
- package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
- package/src/tests/github-handlers.test.ts +35 -0
- package/src/tests/harness-provider-resolution.test.ts +12 -0
- package/src/tests/heartbeat-checklist.test.ts +3 -2
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +168 -0
- package/src/tests/heartbeat-jev-gate.test.ts +385 -0
- package/src/tests/hook-runtime-close.test.ts +0 -2
- package/src/tests/http-api-integration.test.ts +0 -25
- package/src/tests/migration-046-budgets.test.ts +17 -16
- package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
- package/src/tests/model-profile-backcompat.test.ts +327 -0
- package/src/tests/model-profile-providers.test.ts +302 -0
- package/src/tests/model-profile-runtime-secrets.test.ts +223 -0
- package/src/tests/model-profiles-api.test.ts +614 -0
- package/src/tests/model-tier-resolution.test.ts +93 -0
- package/src/tests/models-catalog.test.ts +42 -1
- package/src/tests/openai-compatible-providers.test.ts +134 -0
- package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
- package/src/tests/opencode-adapter.test.ts +216 -2
- package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
- package/src/tests/opencode-shell-env.test.ts +145 -0
- package/src/tests/openrouter-base-url.test.ts +101 -151
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/pages-browser-sdk.test.ts +76 -0
- package/src/tests/pages-feedback-keys.test.ts +121 -0
- package/src/tests/pages-feedback.test.ts +4 -1
- package/src/tests/paused-tasks-requested-by.test.ts +102 -0
- package/src/tests/pi-context-compaction.test.ts +193 -0
- package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
- package/src/tests/pi-mono-adapter.test.ts +385 -5
- package/src/tests/pi-shell-env.test.ts +170 -0
- package/src/tests/profile-claim.test.ts +269 -0
- package/src/tests/prompt-template-remaining.test.ts +35 -0
- package/src/tests/quickjs-compat.test.ts +213 -0
- package/src/tests/rbac-engine.test.ts +32 -3
- package/src/tests/runner-exit-after-tasks.test.ts +0 -4
- package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
- package/src/tests/script-connections.test.ts +6 -0
- package/src/tests/script-executor-conformance.test.ts +252 -1
- package/src/tests/script-executor-quickjs.test.ts +96 -0
- package/src/tests/script-runs-http.test.ts +89 -0
- package/src/tests/scripts-http.test.ts +217 -1
- package/src/tests/scripts-only-gating.test.ts +2 -3
- package/src/tests/scripts-runtime.test.ts +58 -40
- package/src/tests/secret-scrubber.test.ts +96 -0
- package/src/tests/seed-model-profiles.test.ts +174 -0
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/seed-skills-bundled-files.test.ts +45 -53
- package/src/tests/seed-skills-retire.test.ts +182 -0
- package/src/tests/send-task-requester-gate.test.ts +58 -0
- package/src/tests/session-cost-recompute-unit.test.ts +46 -0
- package/src/tests/skill-get-name-resolution.test.ts +137 -0
- package/src/tests/skill-invocations.test.ts +4 -2
- package/src/tests/slack-blocks.test.ts +36 -0
- package/src/tests/slack-inline-output.test.ts +14 -0
- package/src/tests/slack-reaction-config.test.ts +20 -0
- package/src/tests/slack-render-v2.test.ts +37 -0
- package/src/tests/slack-thread-buffer.test.ts +98 -1
- package/src/tests/sonnet-5-5-registry.test.ts +9 -1
- package/src/tests/swarm-config-export-to-env.test.ts +267 -0
- package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
- package/src/tests/swarm-diff.test.ts +1 -1
- package/src/tests/task-model-effort-rerun.test.ts +164 -0
- package/src/tests/task-session-prompts.test.ts +132 -0
- package/src/tests/task-tool-manifest.test.ts +14 -0
- package/src/tests/task-tools-ctx.test.ts +2 -0
- package/src/tests/tool-annotations.test.ts +6 -6
- package/src/tests/ui-logs-parser.test.ts +43 -0
- package/src/tests/used-by-route.test.ts +6 -0
- package/src/tests/vercel-ai-gateway.test.ts +149 -0
- package/src/tests/workflow-llm.test.ts +61 -0
- package/src/tests/workflow-swarm-script.test.ts +38 -0
- package/src/tests/workflow-wait-filter.test.ts +5 -3
- package/src/tools/chat-access.ts +41 -0
- package/src/tools/chat-open-dm.ts +59 -0
- package/src/tools/chat-post.ts +74 -0
- package/src/tools/chat-read-thread.ts +100 -0
- package/src/tools/chat-reply.ts +75 -0
- package/src/tools/create-page.ts +3 -13
- package/src/tools/model-catalog/index.ts +2 -1
- package/src/tools/model-profiles/list-model-profiles.ts +54 -0
- package/src/tools/register-chat-channel.ts +139 -0
- package/src/tools/request-human-input.ts +11 -3
- package/src/tools/script-apis.ts +5 -1
- package/src/tools/script-upsert.ts +4 -1
- package/src/tools/send-task.ts +48 -1
- package/src/tools/skills/skill-get.ts +17 -9
- package/src/tools/swarm-config/get-config.ts +2 -1
- package/src/tools/swarm-config/list-config.ts +2 -1
- package/src/tools/swarm-config/set-config.ts +16 -3
- package/src/tools/system-one-decide.ts +104 -0
- package/src/tools/task-action.ts +22 -1
- package/src/tools/tool-config.ts +14 -6
- package/src/types.ts +48 -3
- package/src/utils/config-env-export.ts +74 -0
- package/src/utils/credentials.test.ts +92 -0
- package/src/utils/credentials.ts +52 -4
- package/src/utils/harness-provider.ts +15 -6
- package/src/utils/runtime-model-catalog.ts +11 -1
- package/src/utils/secret-scrubber.ts +86 -10
- package/src/utils/task-tool-manifest.ts +19 -2
- package/src/utils/vcs-repo-match.ts +29 -0
- package/src/workflows/executors/raw-llm.ts +8 -3
- package/src/workflows/executors/swarm-script.ts +1 -0
- package/src/workflows/executors/system-one-decision.ts +21 -11
- package/src/workflows/executors/workflow-llm.ts +22 -0
- package/src/workflows/index.ts +21 -12
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/comms-adherence-gate/README.md +88 -0
- package/templates/extensions/comms-adherence-gate/hooks.ts +489 -0
- package/templates/extensions/comms-adherence-gate/manifest.json +8 -0
- package/templates/skills/agent-browser/SKILL.md +1 -1
- package/templates/skills/agent-browser/content.md +1 -1
- package/templates/skills/apps/SKILL.md +8 -1
- package/templates/skills/apps/content.md +8 -1
- package/templates/skills/chat-sdk/config.json +14 -0
- package/templates/skills/chat-sdk/content.md +84 -0
- package/templates/skills/composio-gmail/content.md +7 -0
- package/templates/skills/composio-google-docs/content.md +1 -1
- package/templates/skills/email-domain-health/config.json +14 -0
- package/templates/skills/email-domain-health/content.md +131 -0
- package/templates/skills/kv-storage/content.md +2 -2
- package/templates/skills/pages/content.md +8 -10
- package/templates/skills/swarm-extensions/content.md +2 -0
- package/templates/skills/swarm-scripts/SKILL.md +4 -2
- package/templates/skills/swarm-scripts/config.json +1 -1
- package/templates/skills/swarm-scripts/content.md +4 -2
- package/templates/skills/workflow-iterate/content.md +38 -0
- package/dist/artifact-eczsknc3.js +0 -3843
- package/dist/cli-42qhyvwx.js +0 -33
- package/dist/cli-bd957dgb.js +0 -153
- package/dist/cli-q51yprfq.js +0 -1104
- package/dist/cli-t1ze64h3.js +0 -811
- package/dist/cli-tcc4wd5d.js +0 -56471
- package/dist/index-9ch1n761.js +0 -1102
- package/src/artifact-sdk/index.ts +0 -2
- package/src/artifact-sdk/localtunnel.d.ts +0 -20
- package/src/artifact-sdk/port.ts +0 -12
- package/src/artifact-sdk/server.ts +0 -183
- package/src/artifact-sdk/tunnel.ts +0 -19
- package/src/commands/artifact.ts +0 -249
- package/src/http/ecosystem.ts +0 -77
- package/src/tests/artifact-commands.test.ts +0 -92
- package/src/tests/artifact-sdk.test.ts +0 -793
- package/src/tests/register-service-security.test.ts +0 -194
- package/src/tools/list-services.ts +0 -107
- package/src/tools/register-service.ts +0 -177
- package/src/tools/unregister-service.ts +0 -73
- package/src/tools/update-service-status.ts +0 -96
- package/templates/skills/artifacts/config.json +0 -14
- package/templates/skills/artifacts/content.md +0 -344
- package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
- package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
- package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
- package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
- /package/src/{artifact-sdk → pages}/browser-sdk.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
|
+
}
|