@desplega.ai/agent-swarm 2.5.4 → 2.7.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 +4 -2
- package/dist/{acp-adapter-ek38cwsv.js → acp-adapter-ma1js5n7.js} +9 -9
- package/dist/{actions-92mepq03.js → actions-1vhmtvg9.js} +21 -21
- package/dist/{amp-adapter-9gaxg015.js → amp-adapter-sxqqvmdk.js} +4 -4
- package/dist/{app-ctn8gsgx.js → app-mwx1d5a9.js} +3 -3
- package/dist/{assistant-zyr5kz47.js → assistant-yx1p6g82.js} +30 -26
- package/dist/{boot-reembed-w75maga2.js → boot-reembed-5h37a2c2.js} +1 -1
- package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-7h78vmvv.js} +2 -1
- package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-8mawd4gw.js} +4 -4
- package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-ca84dhnn.js} +4 -4
- package/dist/{claude-adapter-f0afqnne.js → claude-adapter-wqt4hfkq.js} +9 -9
- package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
- package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-rqv98sea.js} +10 -3
- package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-f29f3b2x.js} +11 -11
- package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
- package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
- package/dist/{cli-g9vd9ece.js → cli-1hhbzkbb.js} +14 -16
- package/dist/{cli-qqzmjxkf.js → cli-1m9x8dr5.js} +11 -6
- package/dist/{cli-jbnn4vxb.js → cli-2pvzmas1.js} +3 -3
- package/dist/{cli-x77gen2p.js → cli-2y7v133v.js} +7 -7
- package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
- package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
- package/dist/{cli-jb30dzvk.js → cli-48gxjmcz.js} +530 -190
- package/dist/{cli-pzatvw94.js → cli-54sj64fc.js} +2 -2
- package/dist/{cli-bvpgr9ny.js → cli-60sty27p.js} +5 -5
- package/dist/{cli-asq5ydm4.js → cli-6a9qfa8b.js} +411 -69
- package/dist/{cli-277c31dw.js → cli-6mgdba42.js} +225 -3
- package/dist/{cli-19202kew.js → cli-6srv653c.js} +5 -5
- package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
- package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
- package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
- package/dist/{cli-hh5we352.js → cli-86ehn4s5.js} +7 -7
- package/dist/{cli-9s05422w.js → cli-8mbc9za9.js} +1 -1
- package/dist/{cli-ff1tm9n9.js → cli-8wmj18cz.js} +1 -1
- package/dist/{cli-1r5ad7j5.js → cli-9gsm2rzp.js} +12 -12
- package/dist/{cli-mz9afxxf.js → cli-9pshx6rd.js} +17 -3
- package/dist/{cli-s682zgqz.js → cli-a6ds2epe.js} +1 -1
- package/dist/{cli-6a1xg1p7.js → cli-a89zdqz9.js} +2 -2
- package/dist/{cli-03vpq45j.js → cli-ay40dnj3.js} +2 -2
- package/dist/{cli-tsma7p8v.js → cli-aypd09nf.js} +2 -2
- package/dist/{cli-ethgy7ha.js → cli-b3yzyc57.js} +2 -2
- package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
- package/dist/{cli-mmhj8vd7.js → cli-c668vvcf.js} +11 -2
- package/dist/{cli-sbwsxvtd.js → cli-d20qvpr2.js} +2 -2
- package/dist/{cli-c952c7ac.js → cli-e1595bgx.js} +3 -3
- package/dist/{cli-621ce7kw.js → cli-f12mr20z.js} +3 -3
- package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
- package/dist/{cli-xzfq4at3.js → cli-f4dbh7a5.js} +16 -12
- package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
- package/dist/{cli-5bnsc0e1.js → cli-gsfk58nw.js} +5 -5
- package/dist/cli-gt4h8cxw.js +895 -0
- package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
- package/dist/{cli-25n3mv5d.js → cli-h5jeg46p.js} +6 -6
- package/dist/{cli-gn5af0x5.js → cli-he9gbdx0.js} +62 -12
- package/dist/{cli-gbfxp48k.js → cli-j0ppywdr.js} +42 -35
- package/dist/{cli-k4tjsdx9.js → cli-j3y0x2n4.js} +277 -199
- package/dist/{cli-7p4jxgtt.js → cli-j9bpmykh.js} +75 -27
- package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
- package/dist/{cli-wx2nj19m.js → cli-kv7sbc2c.js} +6 -6
- package/dist/{cli-abs8ndtr.js → cli-m30k9777.js} +26 -26
- package/dist/{cli-cz5nmdxt.js → cli-m5e802r7.js} +2 -2
- package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
- package/dist/{cli-xtrz6bzs.js → cli-ptfq202x.js} +5 -3
- package/dist/{cli-6n8axjv1.js → cli-qj442fws.js} +2 -2
- package/dist/{cli-c61mbdds.js → cli-rd5m70gj.js} +4 -4
- package/dist/{cli-eftzyh9y.js → cli-s09j9vp5.js} +2 -2
- package/dist/{cli-m4ecdxvz.js → cli-s0mgx214.js} +7 -7
- package/dist/{cli-wghjkr0e.js → cli-sgqm2582.js} +10 -7
- package/dist/{cli-2655k16q.js → cli-sjctjr48.js} +2 -2
- package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
- package/dist/{cli-x0f2v69c.js → cli-twctnkng.js} +12 -7
- package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
- package/dist/{cli-gx4rm0qx.js → cli-vz78s5wm.js} +1 -1
- package/dist/{cli-gwhjmh8g.js → cli-wqr7cea4.js} +5 -5
- package/dist/{cli-e42vss7b.js → cli-x37r7qwf.js} +16 -8
- package/dist/{cli-4b2z6n0m.js → cli-yd2nreqw.js} +17 -8
- package/dist/{cli-64mh9mf3.js → cli-ymjpkxgn.js} +69 -17
- package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
- package/dist/{cli-9j129brq.js → cli-yxbsmx56.js} +6 -6
- package/dist/cli.js +22 -20
- package/dist/{codex-adapter-wch12q99.js → codex-adapter-zk59x2zh.js} +7 -7
- package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
- package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
- package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-kh6s4j3n.js} +8 -8
- package/dist/{commands-9849dkj6.js → commands-68et1qv2.js} +2 -2
- package/dist/{controller-ja0sd95p.js → controller-fyrbnzg4.js} +4 -4
- package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-nep9h2xv.js} +23 -23
- package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-yss3pg7t.js} +2 -2
- package/dist/{db-w759qmx0.js → db-28gjyvxj.js} +4 -2
- package/dist/{db-47vj40dy.js → db-8ftwvzba.js} +3 -3
- package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
- package/dist/{dispatcher-7b8e4z2j.js → dispatcher-hayexmkr.js} +20 -20
- package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
- package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-46wawff9.js} +2 -2
- package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
- package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
- package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-8d1e0h4m.js} +1 -1
- package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
- package/dist/{execute-0xpz8950.js → execute-cjrfxfnm.js} +1 -1
- package/dist/{external-channel-e2s028da.js → external-channel-xbmxcg09.js} +2 -2
- package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-ag4wks2a.js} +10 -10
- package/dist/handlers-tzmevqb5.js +41 -0
- package/dist/{heartbeat-w3mntghj.js → heartbeat-psfvcgwp.js} +23 -23
- package/dist/{hook-g7kvv6x9.js → hook-8pz04697.js} +6 -6
- package/dist/{hook-3jjywnpb.js → hook-cyq4mryn.js} +8 -8
- package/dist/{http-tkp7d4kb.js → http-haj1nd6k.js} +921 -169
- package/dist/{index-q7m26kvs.js → index-jwt2bj6f.js} +265 -97
- package/dist/{index-3n8m9ea2.js → index-q52f47pe.js} +1 -1
- package/dist/{index-31ceyf42.js → index-syvhnvtw.js} +1 -1
- package/dist/index-y1ajx04a.js +43 -0
- package/dist/{index-1ef1myq2.js → index-z5h22pes.js} +20 -20
- package/dist/{index-xdaahthb.js → index-zyq8ct52.js} +20 -20
- package/dist/{keepalive-v4ta8wgg.js → keepalive-xdvsamvj.js} +4 -4
- package/dist/lead-hqg57kdp.js +79 -0
- package/dist/{maintenance-84n1ex21.js → maintenance-4y2xzh3a.js} +1 -1
- package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-jd4r7zbs.js} +9 -4
- package/dist/{onboard-sfmpe4np.js → onboard-18447q9q.js} +6 -6
- package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-26b2frdf.js} +7 -7
- package/dist/{otel-impl-v6cc0zve.js → otel-impl-f9tgr8yw.js} +2 -2
- package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-qn6qbga1.js} +38 -17
- package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-dw2tgb53.js} +3 -3
- package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-aygm2760.js} +2 -2
- package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-s4nwj2m3.js} +1 -1
- package/dist/render-v2-yfvdfr0x.js +16 -0
- package/dist/{resume-frdtgfa0.js → resume-1bmvjbf3.js} +9 -9
- package/dist/{runtime-bt5pkpm1.js → runtime-f7tpzedy.js} +1 -1
- package/dist/{session-access-fpez4y5q.js → session-access-r5garej6.js} +4 -4
- package/dist/session-source-backfill-424wzv0r.js +110 -0
- package/dist/{setup-mq6m3m8q.js → setup-8tygvxkq.js} +1 -1
- package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-xsw323b3.js} +2 -2
- package/dist/{work-objects-kfahhjsq.js → work-objects-ggnghpgf.js} +23 -23
- package/dist/worker-sqjd1nqf.js +79 -0
- package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
- package/openapi.json +1324 -14
- package/package.json +5 -3
- package/src/assets/source-key.ts +95 -0
- package/src/be/automation-preflight.ts +2 -0
- package/src/be/db/api-keys.ts +89 -0
- package/src/be/db/session-labels.ts +28 -0
- package/src/be/db/session-moves.ts +5 -6
- package/src/be/db/session-tree.ts +153 -14
- package/src/be/db/tasks/read.ts +11 -0
- package/src/be/db/tasks/write.ts +6 -1
- package/src/be/db/workflow-runs.ts +219 -6
- package/src/be/db-retention.ts +61 -6
- package/src/be/db.ts +135 -132
- package/src/be/memory/providers/openai-embedding.ts +5 -1
- package/src/be/memory/task-learning.ts +155 -0
- package/src/be/migrations/244_api_key_validation.sql +4 -0
- package/src/be/migrations/245_script_executor.sql +3 -0
- package/src/be/oauth-refresh-sweep.ts +2 -1
- package/src/be/onboarding.ts +4 -0
- package/src/be/scripts/boot-reembed.ts +2 -0
- package/src/be/scripts/db.ts +14 -4
- package/src/be/scripts/embeddings.ts +2 -0
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +10 -4
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/session-source-backfill.ts +163 -0
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/comb/review-batch.ts +28 -1
- package/src/comb/templates.ts +7 -2
- package/src/commands/claude-managed-setup.ts +7 -1
- package/src/commands/credential-refresh.ts +76 -1
- package/src/commands/credential-wait.ts +2 -2
- package/src/commands/onboard/compose-generator.ts +5 -3
- package/src/commands/provider-credentials.ts +212 -171
- package/src/commands/runner.ts +25 -4
- package/src/extensions/apply-task-create.ts +8 -1
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +5 -0
- package/src/heartbeat/heartbeat.ts +14 -1
- package/src/heartbeat/script-runner-probe.ts +47 -0
- package/src/http/all-routes.ts +2 -0
- package/src/http/api-keys.ts +77 -9
- package/src/http/assets.ts +15 -0
- package/src/http/codex-oauth-accounts.ts +191 -0
- package/src/http/comb.ts +15 -2
- package/src/http/config.ts +1 -1
- package/src/http/core.ts +6 -0
- package/src/http/fleet-benchmark.ts +5 -2
- package/src/http/idempotency-key.ts +1 -1
- package/src/http/index.ts +14 -3
- package/src/http/integrations.ts +5 -1
- package/src/http/kv.ts +8 -3
- package/src/http/onboarding.ts +6 -1
- package/src/http/pages-public.ts +38 -5
- package/src/http/pool-keys.ts +354 -0
- package/src/http/scripts.ts +27 -0
- package/src/http/sessions.ts +37 -12
- package/src/http/stats.ts +1 -0
- package/src/http/status.ts +26 -12
- package/src/http/tasks.ts +3 -0
- package/src/http/used-by.ts +7 -2
- package/src/http/workflows.ts +88 -9
- package/src/http/x.ts +5 -0
- package/src/integrations/chat-sdk/inbound.ts +27 -0
- package/src/kv-reserved-namespaces.ts +19 -0
- package/src/prompts/base-prompt.ts +21 -1
- package/src/prompts/session-templates.ts +17 -3
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +5 -1
- package/src/providers/claude-managed-adapter.ts +16 -9
- package/src/providers/codex-adapter.ts +71 -11
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +41 -11
- package/src/scheduler/scheduler.ts +5 -3
- package/src/scripts-runtime/eval-harness.ts +8 -1
- package/src/scripts-runtime/executors/native.ts +22 -16
- package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
- package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
- package/src/scripts-runtime/executors/quickjs.ts +7 -0
- package/src/scripts-runtime/executors/registry.ts +32 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/host-validated-args.ts +133 -0
- package/src/scripts-runtime/loader.ts +30 -3
- package/src/scripts-runtime/swarm-sdk.ts +16 -3
- package/src/scripts-runtime/types/stdlib.d.ts +10 -2
- package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
- package/src/scripts-runtime/types/swarm-sdk.d.ts +10 -2
- package/src/slack/assistant.ts +7 -0
- package/src/slack/channel-labels.ts +74 -0
- package/src/slack/handlers.ts +8 -0
- package/src/slack/inbound-files.ts +5 -2
- package/src/slack/thread-buffer.ts +4 -0
- package/src/tasks/sibling-awareness.ts +12 -3
- package/src/telemetry-context.ts +5 -6
- package/src/telemetry-identity.ts +3 -3
- package/src/tests/api-key-status-by-suffix.test.ts +242 -0
- package/src/tests/api-keys-validation.test.ts +170 -0
- package/src/tests/base-prompt.test.ts +70 -2
- package/src/tests/claude-managed-adapter.test.ts +46 -0
- package/src/tests/claude-routes-e2e.test.ts +7 -2
- package/src/tests/codex-credential-pools.test.ts +120 -0
- package/src/tests/codex-gateway-route.test.ts +165 -0
- package/src/tests/codex-oauth-accounts.test.ts +357 -0
- package/src/tests/comb-review-batch.test.ts +63 -2
- package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
- package/src/tests/config-pool-keys.test.ts +533 -0
- package/src/tests/credential-check.test.ts +294 -4
- package/src/tests/credential-refresh.test.ts +172 -1
- package/src/tests/db-retention.test.ts +9 -3
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -3
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/docs-index.test.ts +135 -0
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/extensions-pre-task.test.ts +53 -1
- package/src/tests/fetch-resolved-env.test.ts +16 -2
- package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
- package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
- package/src/tests/fleet-benchmark.test.ts +8 -0
- package/src/tests/get-workflow-run-tool.test.ts +98 -0
- package/src/tests/heartbeat-checklist.test.ts +89 -0
- package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
- package/src/tests/integrations-http.test.ts +32 -2
- package/src/tests/internal-ai/complete-structured.test.ts +37 -1
- package/src/tests/internal-ai/credentials.test.ts +23 -0
- package/src/tests/kv-http.test.ts +32 -0
- package/src/tests/kv-tool.test.ts +25 -0
- package/src/tests/multi-runtime-registration.test.ts +12 -12
- package/src/tests/oauth-refresh-sweep.test.ts +34 -1
- package/src/tests/onboard-compose.test.ts +20 -0
- package/src/tests/onboarding.test.ts +108 -1
- package/src/tests/openai-embedding-credentials.test.ts +35 -0
- package/src/tests/opencode-adapter.test.ts +2 -1
- package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
- package/src/tests/org-website.test.ts +36 -0
- package/src/tests/pages-frame-ancestors.test.ts +162 -0
- package/src/tests/pi-codemode.test.ts +2 -1
- package/src/tests/pi-mono-adapter.test.ts +54 -0
- package/src/tests/pi-native-mcp.test.ts +13 -1
- package/src/tests/pi-tool-exposure.test.ts +8 -8
- package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/provider-key-probes.test.ts +181 -0
- package/src/tests/rbac-e2e-helpers.test.ts +88 -1
- package/src/tests/rbac-e2e-helpers.ts +36 -11
- package/src/tests/request-composition-derive.test.ts +329 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/script-executor-conformance.test.ts +36 -1
- package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
- package/src/tests/script-executor-quickjs.test.ts +140 -0
- package/src/tests/script-host-validated-args-native.test.ts +196 -0
- package/src/tests/script-per-script-executor.test.ts +415 -0
- package/src/tests/script-runs-retention.test.ts +212 -0
- package/src/tests/scripts-external-api.test.ts +45 -0
- package/src/tests/sdk-allowlist.test.ts +42 -2
- package/src/tests/self-improvement.test.ts +4 -4
- package/src/tests/session-source-backfill.test.ts +429 -0
- package/src/tests/session-source-keys.test.ts +236 -0
- package/src/tests/session-source-labels.test.ts +419 -0
- package/src/tests/sessions-detail-batch.test.ts +2 -0
- package/src/tests/sessions-move.test.ts +131 -0
- package/src/tests/sessions-tree.test.ts +299 -2
- package/src/tests/slack-inbound-files.test.ts +103 -23
- package/src/tests/status.test.ts +101 -18
- package/src/tests/store-progress-learning.test.ts +288 -0
- package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
- package/src/tests/test-net.ts +31 -0
- package/src/tests/transcription.test.ts +8 -0
- package/src/tests/ui-logs-parser.test.ts +180 -0
- package/src/tests/used-by-route.test.ts +11 -0
- package/src/tests/workflow-cascade-cancel.test.ts +172 -0
- package/src/tests/workflow-engine-v2.test.ts +116 -1
- package/src/tests/workflow-http-v2.test.ts +131 -0
- package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
- package/src/tests/workflow-run-finished-event.test.ts +263 -0
- package/src/tests/workflow-step-order.test.ts +92 -0
- package/src/tests/workflow-sub-workflow.test.ts +30 -0
- package/src/tests/workflow-swarm-script.test.ts +34 -0
- package/src/tools/get-tasks.ts +8 -0
- package/src/tools/kv/kv-read-auth.ts +4 -2
- package/src/tools/script-common.ts +1 -0
- package/src/tools/script-run.ts +7 -0
- package/src/tools/script-upsert.ts +7 -0
- package/src/tools/store-progress.ts +67 -1
- package/src/tools/task-action.ts +1 -1
- package/src/tools/workflows/get-workflow-run.ts +14 -4
- package/src/tools/workflows/trigger-workflow.ts +104 -10
- package/src/types.ts +36 -1
- package/src/utils/company-profile.ts +51 -1
- package/src/utils/credentials.ts +10 -4
- package/src/utils/internal-ai/complete-structured.ts +5 -0
- package/src/utils/internal-ai/credentials.ts +4 -3
- package/src/utils/provider-key-probes.ts +328 -0
- package/src/utils/transcription.ts +4 -1
- package/src/workflows/engine.ts +18 -16
- package/src/workflows/executors/sub-workflow.ts +8 -1
- package/src/workflows/executors/swarm-script.ts +5 -0
- package/src/workflows/foreach-driver.ts +5 -11
- package/src/workflows/node-timeout.ts +30 -0
- package/templates/ai-toolbox.manifest.json +2 -2
- package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
- package/templates/community/linear-delivery-loop/README.md +38 -0
- package/templates/community/linear-delivery-loop/config.example.json +31 -0
- package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
- package/templates/community/linear-delivery-loop/schedule.json +10 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
- package/templates/skills/researching/content.md +2 -0
- package/templates/skills/swarm-docs/config.json +14 -0
- package/templates/skills/swarm-docs/content.md +144 -0
- package/templates/skills/swarm-scripts/SKILL.md +1 -0
- package/templates/skills/swarm-scripts/content.md +1 -0
- package/dist/cli-7421jzsw.js +0 -84
- package/dist/cli-e45e4m6b.js +0 -413
- package/dist/cli-zw88zjba.js +0 -307
- package/dist/handlers-wy1dr6ja.js +0 -41
- package/dist/index-nmm79qgr.js +0 -43
- package/dist/lead-sawnyqxk.js +0 -79
- package/dist/render-v2-pjwxvnkw.js +0 -12
- package/dist/worker-v5e5rxj3.js +0 -79
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { defaultAssetKey } from "../assets/key";
|
|
5
|
+
import {
|
|
6
|
+
effectiveSource,
|
|
7
|
+
SESSION_SOURCES,
|
|
8
|
+
sessionSourceKey,
|
|
9
|
+
sourceMoveError,
|
|
10
|
+
sourceOfKey,
|
|
11
|
+
} from "../assets/source-key";
|
|
12
|
+
import { closeDb, createAgent, createTask, createTaskExtended, initDb } from "../be/db";
|
|
13
|
+
import { chatSdkContextKey } from "../tasks/context-key";
|
|
14
|
+
import { registerSendTaskTool } from "../tools/send-task";
|
|
15
|
+
|
|
16
|
+
const slack = { slackChannelId: "C0AR967K0KZ", slackThreadTs: "1729000000.123456" };
|
|
17
|
+
const slackKey = "shared/slack/c0ar967k0kz/thread:1729000000.123456/";
|
|
18
|
+
|
|
19
|
+
test("effective source uses a complete source prefix or retains the raw source", () => {
|
|
20
|
+
for (const source of SESSION_SOURCES) {
|
|
21
|
+
expect(effectiveSource(`shared/${source}/channel/thread:1/`, "mcp")).toBe(source);
|
|
22
|
+
expect(effectiveSource(`shared/${source}-other/`, "chat")).toBe("chat");
|
|
23
|
+
}
|
|
24
|
+
expect(effectiveSource("shared/research/", "slack")).toBe("slack");
|
|
25
|
+
expect(effectiveSource("shared/research/", "chat")).toBe("chat");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("source move guard permits only exact own source keys under source prefixes", () => {
|
|
29
|
+
const root = { id: "root", ...slack };
|
|
30
|
+
for (const destination of [null, "shared/research/", "shared/slack-other/", slackKey]) {
|
|
31
|
+
expect(sourceMoveError(destination, root)).toBeNull();
|
|
32
|
+
}
|
|
33
|
+
for (const destination of [
|
|
34
|
+
"shared/slack/",
|
|
35
|
+
"shared/slack/c0ar967k0kz/",
|
|
36
|
+
"shared/slack/c0ar967k0kz/thread:other/",
|
|
37
|
+
"shared/telegram/123/",
|
|
38
|
+
"shared/discord/c1/",
|
|
39
|
+
]) {
|
|
40
|
+
expect(sourceMoveError(destination, root)).toBe("Source groups are read-only.");
|
|
41
|
+
}
|
|
42
|
+
expect(sourceMoveError(slackKey, { id: "other" })).toBe("Source groups are read-only.");
|
|
43
|
+
const chatRoot = { id: "root", contextKey: chatSdkContextKey("telegram:-100") };
|
|
44
|
+
expect(sourceMoveError("shared/telegram/-100/task:root/", chatRoot)).toBeNull();
|
|
45
|
+
expect(sourceMoveError("shared/telegram/-100/", chatRoot)).toBe("Source groups are read-only.");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("session source key shapes", () => {
|
|
49
|
+
test("Slack fields produce a lowercase channel key", () => {
|
|
50
|
+
expect(sessionSourceKey({ id: "root", ...slack })).toBe(slackKey);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("Slack DMs use the same thread shape", () => {
|
|
54
|
+
expect(sessionSourceKey({ id: "root", ...slack, slackChannelId: "D123" })).toBe(
|
|
55
|
+
"shared/slack/d123/thread:1729000000.123456/",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test.each([
|
|
60
|
+
["slack:C123:1729000000.123456", "shared/slack/c123/thread:1729000000.123456/"],
|
|
61
|
+
["telegram:123:42", "shared/telegram/123/thread:42/"],
|
|
62
|
+
["telegram:123", "shared/telegram/123/task:root/"],
|
|
63
|
+
["telegram:-1001234567890:42", "shared/telegram/-1001234567890/thread:42/"],
|
|
64
|
+
["telegram:-1001234567890", "shared/telegram/-1001234567890/task:root/"],
|
|
65
|
+
["discord:guild:channel:thread", "shared/discord/channel/thread:thread/"],
|
|
66
|
+
["discord:guild:channel", "shared/discord/channel/task:root/"],
|
|
67
|
+
["discord:@me:dm:thread", "shared/discord/dm/thread:thread/"],
|
|
68
|
+
["discord:@me:dm", "shared/discord/dm/task:root/"],
|
|
69
|
+
["teams:conversation:thread", null],
|
|
70
|
+
["other:thread", null],
|
|
71
|
+
["slack:D123:", "shared/slack/d123/task:root/"],
|
|
72
|
+
["slack:D123", "shared/slack/d123/task:root/"],
|
|
73
|
+
["slack:C123::", null],
|
|
74
|
+
["telegram:chat:", null],
|
|
75
|
+
["discord:guild", null],
|
|
76
|
+
["discord:guild:a/b:thread", null],
|
|
77
|
+
])("chat-sdk %s", (threadId, expected) => {
|
|
78
|
+
expect(
|
|
79
|
+
sessionSourceKey({ id: "root", contextKey: chatSdkContextKey(threadId as string) }),
|
|
80
|
+
).toBe(expected);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("Slack fields take precedence over a chat context", () => {
|
|
84
|
+
expect(
|
|
85
|
+
sessionSourceKey({
|
|
86
|
+
id: "root",
|
|
87
|
+
...slack,
|
|
88
|
+
contextKey: chatSdkContextKey("telegram:123:42"),
|
|
89
|
+
}),
|
|
90
|
+
).toBe(slackKey);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("missing or malformed context produces no source key", () => {
|
|
94
|
+
for (const contextKey of [undefined, null, "", "task:chat-sdk:telegram:%ZZ", "task:ui:abc"]) {
|
|
95
|
+
expect(sessionSourceKey({ id: "root", contextKey })).toBeNull();
|
|
96
|
+
}
|
|
97
|
+
expect(sessionSourceKey({ id: "root", slackChannelId: "C123" })).toBeNull();
|
|
98
|
+
expect(sessionSourceKey({ id: "root", slackThreadTs: "1.2" })).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("normalization failures produce no source key", () => {
|
|
102
|
+
for (const slackChannelId of ["x".repeat(255), "..", "bad\\channel", "bad\0channel", "a/b"]) {
|
|
103
|
+
expect(sessionSourceKey({ id: "root", ...slack, slackChannelId })).toBeNull();
|
|
104
|
+
}
|
|
105
|
+
expect(
|
|
106
|
+
sessionSourceKey({
|
|
107
|
+
id: "root",
|
|
108
|
+
contextKey: chatSdkContextKey(`telegram:${"x".repeat(255)}`),
|
|
109
|
+
}),
|
|
110
|
+
).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("source prefixes require the complete source segment", () => {
|
|
114
|
+
for (const source of SESSION_SOURCES) {
|
|
115
|
+
expect(sourceOfKey(`shared/${source}/`)).toBe(source);
|
|
116
|
+
expect(sourceOfKey(`shared/${source}/channel/thread:123/`)).toBe(source);
|
|
117
|
+
expect(sourceOfKey(`shared/${source}-other/`)).toBeNull();
|
|
118
|
+
}
|
|
119
|
+
for (const key of ["shared/", "shared/slack", "personal/user/slack/", "shared/task:root/"]) {
|
|
120
|
+
expect(sourceOfKey(key)).toBeNull();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("session source keys at insert", () => {
|
|
126
|
+
beforeAll(() => initDb(":memory:"));
|
|
127
|
+
afterAll(() => closeDb());
|
|
128
|
+
|
|
129
|
+
test.each([
|
|
130
|
+
"slack",
|
|
131
|
+
"mcp",
|
|
132
|
+
] as const)("%s roots with Slack fields get a source key", async (source) => {
|
|
133
|
+
const root = await createTaskExtended("Slack root", { source, ...slack });
|
|
134
|
+
expect(root.key).toBe(slackKey);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a child inherits its Slack root key", async () => {
|
|
138
|
+
const root = await createTaskExtended("Slack root", { source: "slack", ...slack });
|
|
139
|
+
const child = await createTaskExtended("Child", { parentTaskId: root.id });
|
|
140
|
+
expect(child.key).toBe(root.key);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("send-task permits a child to inherit its Slack root key", async () => {
|
|
144
|
+
const agent = await createAgent({ name: "Source caller", status: "idle", maxTasks: 10 });
|
|
145
|
+
const target = await createAgent({ name: "Source target", status: "idle", maxTasks: 10 });
|
|
146
|
+
const root = await createTaskExtended("Slack root", {
|
|
147
|
+
agentId: agent.id,
|
|
148
|
+
source: "slack",
|
|
149
|
+
...slack,
|
|
150
|
+
});
|
|
151
|
+
const server = new McpServer({ name: "source-key-test", version: "1.0.0" });
|
|
152
|
+
registerSendTaskTool(server);
|
|
153
|
+
const tools = (
|
|
154
|
+
server as unknown as {
|
|
155
|
+
_registeredTools: Record<
|
|
156
|
+
string,
|
|
157
|
+
{
|
|
158
|
+
handler: (args: unknown, extra: unknown) => Promise<CallToolResult>;
|
|
159
|
+
}
|
|
160
|
+
>;
|
|
161
|
+
}
|
|
162
|
+
)._registeredTools;
|
|
163
|
+
const result = await tools["send-task"]!.handler(
|
|
164
|
+
{
|
|
165
|
+
agentId: target.id,
|
|
166
|
+
routingReason: "human_pinned",
|
|
167
|
+
routingNote: "The test selects the target worker.",
|
|
168
|
+
task: "Child in a Slack source group",
|
|
169
|
+
parentTaskId: root.id,
|
|
170
|
+
allowDuplicate: true,
|
|
171
|
+
},
|
|
172
|
+
{ requestInfo: { headers: { "x-agent-id": agent.id, "x-source-task-id": root.id } } },
|
|
173
|
+
);
|
|
174
|
+
expect(result.structuredContent).toMatchObject({
|
|
175
|
+
success: true,
|
|
176
|
+
task: { key: slackKey, parentTaskId: root.id },
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("a child with different Slack fields keeps its parent's custom key", async () => {
|
|
181
|
+
const root = await createTaskExtended("Custom root", { key: "shared/research/", ...slack });
|
|
182
|
+
const child = await createTaskExtended("Child", {
|
|
183
|
+
parentTaskId: root.id,
|
|
184
|
+
slackChannelId: "C_OTHER",
|
|
185
|
+
slackThreadTs: "123.456",
|
|
186
|
+
});
|
|
187
|
+
expect(child.key).toBe(root.key);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("a child with chat context inherits its parent's default key", async () => {
|
|
191
|
+
const root = await createTaskExtended("Default root");
|
|
192
|
+
const child = await createTaskExtended("Child", {
|
|
193
|
+
parentTaskId: root.id,
|
|
194
|
+
contextKey: chatSdkContextKey("telegram:123:42"),
|
|
195
|
+
});
|
|
196
|
+
expect(child.key).toBe(root.key);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("an explicit key wins for roots and children", async () => {
|
|
200
|
+
const root = await createTaskExtended("Custom root", { key: "shared/Custom/", ...slack });
|
|
201
|
+
const child = await createTaskExtended("Custom child", {
|
|
202
|
+
parentTaskId: root.id,
|
|
203
|
+
key: "shared/Other/",
|
|
204
|
+
});
|
|
205
|
+
expect(root.key).toBe("shared/custom/");
|
|
206
|
+
expect(child.key).toBe("shared/other/");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("a Telegram chat-sdk root gets its source key", async () => {
|
|
210
|
+
const root = await createTaskExtended("Telegram root", {
|
|
211
|
+
source: "chat",
|
|
212
|
+
contextKey: chatSdkContextKey("telegram:-1001234567890:42"),
|
|
213
|
+
});
|
|
214
|
+
expect(root.key).toBe("shared/telegram/-1001234567890/thread:42/");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("unsupported and invalid source keys retain the per-task default", async () => {
|
|
218
|
+
for (const options of [
|
|
219
|
+
{},
|
|
220
|
+
{ contextKey: chatSdkContextKey("teams:conversation:thread") },
|
|
221
|
+
{ ...slack, slackChannelId: "x".repeat(255) },
|
|
222
|
+
{ ...slack, parentTaskId: crypto.randomUUID() },
|
|
223
|
+
]) {
|
|
224
|
+
const root = await createTaskExtended("Default key", options);
|
|
225
|
+
expect(root.key).toBe(defaultAssetKey("task", root.id));
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("legacy createTask derives Slack keys and retains the default otherwise", async () => {
|
|
230
|
+
const agent = await createAgent({ name: "Source key agent", status: "idle", capabilities: [] });
|
|
231
|
+
const root = await createTask(agent.id, "Legacy Slack root", { source: "slack", ...slack });
|
|
232
|
+
const ordinary = await createTask(agent.id, "Legacy ordinary root");
|
|
233
|
+
expect(root.key).toBe(slackKey);
|
|
234
|
+
expect(ordinary.key).toBe(defaultAssetKey("task", ordinary.id));
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, expect, mock, spyOn, test } from "bun:test";
|
|
2
|
+
import { createServer, type Server } from "node:http";
|
|
3
|
+
import { closeDb, createAgent, createTaskExtended, getDbClient, initDb, upsertKv } from "../be/db";
|
|
4
|
+
import * as labelsDb from "../be/db/session-labels";
|
|
5
|
+
import { getSessionLabels, rememberSessionLabel } from "../be/db/session-labels";
|
|
6
|
+
import { handleSessions } from "../http/sessions";
|
|
7
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
8
|
+
import { flushChatThread } from "../integrations/chat-sdk/inbound";
|
|
9
|
+
import type { ChatBufferedMessage } from "../integrations/chat-sdk/thread-buffer";
|
|
10
|
+
import * as slackApp from "../slack/app";
|
|
11
|
+
import { warmSlackChannelLabels } from "../slack/channel-labels";
|
|
12
|
+
import { chatSdkContextKey } from "../tasks/context-key";
|
|
13
|
+
import { beginRequestAuthScope, setRequestAuth } from "../utils/request-auth-context";
|
|
14
|
+
import {
|
|
15
|
+
flushChatBuffers,
|
|
16
|
+
postTelegramUpdate,
|
|
17
|
+
startTelegramHarness,
|
|
18
|
+
telegramUpdate,
|
|
19
|
+
} from "./chat-sdk-harness";
|
|
20
|
+
import { listenOnFreePort } from "./test-net";
|
|
21
|
+
|
|
22
|
+
let server: Server;
|
|
23
|
+
let baseUrl: string;
|
|
24
|
+
const getApp = spyOn(slackApp, "getSlackApp");
|
|
25
|
+
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
initDb(":memory:");
|
|
28
|
+
await createAgent({ id: crypto.randomUUID(), name: "LabelsLead", isLead: true, status: "idle" });
|
|
29
|
+
server = createServer(async (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
beginRequestAuthScope();
|
|
32
|
+
setRequestAuth(req, { kind: "operator", fingerprint: "session-labels-test" });
|
|
33
|
+
if (
|
|
34
|
+
await handleSessions(
|
|
35
|
+
req,
|
|
36
|
+
res,
|
|
37
|
+
getPathSegments(req.url ?? ""),
|
|
38
|
+
parseQueryParams(req.url ?? ""),
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
return;
|
|
42
|
+
res.writeHead(404).end();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
res.writeHead(500).end(String(error));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
baseUrl = `http://127.0.0.1:${await listenOnFreePort(server)}`;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
getApp.mockReturnValue(null);
|
|
52
|
+
await getDbClient().run("DELETE FROM agent_tasks");
|
|
53
|
+
await getDbClient().run("DELETE FROM kv_entries WHERE namespace = 'session-labels'");
|
|
54
|
+
});
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
getApp.mockReturnValue(null);
|
|
57
|
+
});
|
|
58
|
+
afterAll(async () => {
|
|
59
|
+
getApp.mockRestore();
|
|
60
|
+
server.closeAllConnections();
|
|
61
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
62
|
+
closeDb();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
async function until(check: () => Promise<boolean> | boolean): Promise<void> {
|
|
66
|
+
for (let attempt = 0; attempt < 200; attempt++) {
|
|
67
|
+
if (await check()) return;
|
|
68
|
+
await Bun.sleep(5);
|
|
69
|
+
}
|
|
70
|
+
throw new Error("Background label lookup did not finish");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fakeSlack(
|
|
74
|
+
apiCall: (method: string, payload: Record<string, unknown>) => Promise<unknown>,
|
|
75
|
+
) {
|
|
76
|
+
const call = mock(apiCall);
|
|
77
|
+
getApp.mockReturnValue({ client: { apiCall: call } } as unknown as NonNullable<
|
|
78
|
+
ReturnType<typeof slackApp.getSlackApp>
|
|
79
|
+
>);
|
|
80
|
+
return call;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const keyOf = (id: string) => `shared/slack/${id}/`;
|
|
84
|
+
async function hasLabel(id: string, label: string) {
|
|
85
|
+
return (await getSessionLabels([keyOf(id)]))[keyOf(id)] === label;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
test("cache reads one batch, keeps plain strings and skips expired or foreign rows", async () => {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
await rememberSessionLabel("shared/telegram/-1/", "Life");
|
|
91
|
+
await rememberSessionLabel(keyOf("c1"), "#eng", 86_400_000);
|
|
92
|
+
await rememberSessionLabel(keyOf("expired"), "old", -1);
|
|
93
|
+
await upsertKv({
|
|
94
|
+
namespace: "other",
|
|
95
|
+
key: keyOf("foreign"),
|
|
96
|
+
value: "other",
|
|
97
|
+
valueType: "string",
|
|
98
|
+
});
|
|
99
|
+
const client = getDbClient();
|
|
100
|
+
const query = spyOn(client, "query");
|
|
101
|
+
try {
|
|
102
|
+
expect(await getSessionLabels([])).toEqual({});
|
|
103
|
+
expect(query).not.toHaveBeenCalled();
|
|
104
|
+
expect(
|
|
105
|
+
await getSessionLabels([
|
|
106
|
+
"shared/telegram/-1/",
|
|
107
|
+
keyOf("c1"),
|
|
108
|
+
keyOf("expired"),
|
|
109
|
+
keyOf("foreign"),
|
|
110
|
+
]),
|
|
111
|
+
).toEqual({
|
|
112
|
+
"shared/telegram/-1/": "Life",
|
|
113
|
+
[keyOf("c1")]: "#eng",
|
|
114
|
+
});
|
|
115
|
+
expect(query).toHaveBeenCalledTimes(1);
|
|
116
|
+
} finally {
|
|
117
|
+
query.mockRestore();
|
|
118
|
+
}
|
|
119
|
+
const row = await client.get<{ value: string; value_type: string; expires_at: number }>(
|
|
120
|
+
"SELECT value, value_type, expires_at FROM kv_entries WHERE namespace = 'session-labels' AND key = ?",
|
|
121
|
+
[keyOf("c1")],
|
|
122
|
+
);
|
|
123
|
+
expect(row).toMatchObject({ value: "#eng", value_type: "string" });
|
|
124
|
+
expect(row!.expires_at).toBeGreaterThanOrEqual(now + 86_400_000);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("Slack resolves a channel with uppercase id, a DM and a group DM with a 24 hour TTL", async () => {
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const call = fakeSlack(async (method, payload) => {
|
|
130
|
+
if (method === "users.info")
|
|
131
|
+
return { ok: true, user: { profile: { display_name: "Lena" }, real_name: "Elena" } };
|
|
132
|
+
if (payload.channel === "DLABEL") return { ok: true, channel: { is_im: true, user: "ULENA" } };
|
|
133
|
+
if (payload.channel === "GLABEL")
|
|
134
|
+
return { ok: true, channel: { is_mpim: true, name: "private" } };
|
|
135
|
+
return { ok: true, channel: { name: "eng" } };
|
|
136
|
+
});
|
|
137
|
+
warmSlackChannelLabels([keyOf("clabel"), keyOf("dlabel"), keyOf("glabel")]);
|
|
138
|
+
warmSlackChannelLabels([keyOf("clabel"), "shared/slack/", "shared/slack/clabel/thread:1.2/"]);
|
|
139
|
+
await until(() => hasLabel("glabel", "Group DM"));
|
|
140
|
+
expect(await hasLabel("clabel", "#eng")).toBe(true);
|
|
141
|
+
expect(await hasLabel("dlabel", "DM · Lena")).toBe(true);
|
|
142
|
+
expect(call).toHaveBeenCalledTimes(4);
|
|
143
|
+
expect(call).toHaveBeenCalledWith("conversations.info", { channel: "CLABEL" });
|
|
144
|
+
expect(call).toHaveBeenCalledWith("users.info", { user: "ULENA" });
|
|
145
|
+
const row = await getDbClient().get<{ expires_at: number }>(
|
|
146
|
+
"SELECT expires_at FROM kv_entries WHERE namespace = 'session-labels' AND key = ?",
|
|
147
|
+
[keyOf("clabel")],
|
|
148
|
+
);
|
|
149
|
+
expect(row!.expires_at).toBeGreaterThanOrEqual(now + 86_400_000);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("Slack uses the real name when the display name is empty", async () => {
|
|
153
|
+
fakeSlack(async (method) =>
|
|
154
|
+
method === "users.info"
|
|
155
|
+
? { ok: true, user: { profile: { display_name: "" }, real_name: "Lena" } }
|
|
156
|
+
: { ok: true, channel: { is_im: true, user: "ULENA" } },
|
|
157
|
+
);
|
|
158
|
+
warmSlackChannelLabels([keyOf("dreal")]);
|
|
159
|
+
await until(() => hasLabel("dreal", "DM · Lena"));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("Slack serializes work across calls and retries ratelimited errors", async () => {
|
|
163
|
+
let release!: () => void;
|
|
164
|
+
const gate = new Promise<void>((resolve) => {
|
|
165
|
+
release = resolve;
|
|
166
|
+
});
|
|
167
|
+
let attempts = 0;
|
|
168
|
+
const call = fakeSlack(async (_method, payload) => {
|
|
169
|
+
if (payload.channel === "CSERIAL") await gate;
|
|
170
|
+
else if (attempts++ === 0) throw { data: { error: "ratelimited", retry_after: 0 } };
|
|
171
|
+
return { ok: true, channel: { name: "eng" } };
|
|
172
|
+
});
|
|
173
|
+
try {
|
|
174
|
+
warmSlackChannelLabels([keyOf("cserial")]);
|
|
175
|
+
warmSlackChannelLabels([keyOf("cretry")]);
|
|
176
|
+
await until(() => call.mock.calls.length === 1);
|
|
177
|
+
expect(call.mock.calls[0]![1]).toEqual({ channel: "CSERIAL" });
|
|
178
|
+
} finally {
|
|
179
|
+
release();
|
|
180
|
+
}
|
|
181
|
+
await until(() => hasLabel("cretry", "#eng"));
|
|
182
|
+
expect(call).toHaveBeenCalledTimes(3);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("Slack caches a failure in memory for ten minutes without writing KV or logging names", async () => {
|
|
186
|
+
const warn = spyOn(console, "warn").mockImplementation(() => {});
|
|
187
|
+
const call = fakeSlack(async () => {
|
|
188
|
+
throw new Error("secret label text");
|
|
189
|
+
});
|
|
190
|
+
try {
|
|
191
|
+
warmSlackChannelLabels([keyOf("cfail")]);
|
|
192
|
+
await until(() => warn.mock.calls.length === 1);
|
|
193
|
+
warmSlackChannelLabels([keyOf("cfail")]);
|
|
194
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
195
|
+
expect(await getSessionLabels([keyOf("cfail")])).toEqual({});
|
|
196
|
+
expect(warn.mock.calls.flat().join(" ")).not.toContain("secret label text");
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
const clock = spyOn(Date, "now").mockReturnValue(now + 10 * 60_000 + 1);
|
|
199
|
+
try {
|
|
200
|
+
call.mockImplementation(async () => ({ ok: true, channel: { name: "recovered" } }));
|
|
201
|
+
warmSlackChannelLabels([keyOf("cfail")]);
|
|
202
|
+
await until(() => hasLabel("cfail", "#recovered"));
|
|
203
|
+
expect(call).toHaveBeenCalledTimes(2);
|
|
204
|
+
} finally {
|
|
205
|
+
clock.mockRestore();
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
warn.mockRestore();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("a null Slack client performs no work and does not reserve the id", async () => {
|
|
213
|
+
warmSlackChannelLabels([keyOf("cnull")]);
|
|
214
|
+
expect(await getSessionLabels([keyOf("cnull")])).toEqual({});
|
|
215
|
+
const call = fakeSlack(async () => ({ ok: true, channel: { name: "eng" } }));
|
|
216
|
+
warmSlackChannelLabels([keyOf("cnull")]);
|
|
217
|
+
await until(() => hasLabel("cnull", "#eng"));
|
|
218
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("Slack caps pending and active lookups across calls and accepts skipped ids on a later poll", async () => {
|
|
222
|
+
let release!: () => void;
|
|
223
|
+
const gate = new Promise<void>((resolve) => {
|
|
224
|
+
release = resolve;
|
|
225
|
+
});
|
|
226
|
+
const call = fakeSlack(async () => {
|
|
227
|
+
await gate;
|
|
228
|
+
return { ok: true, channel: { name: "bounded" } };
|
|
229
|
+
});
|
|
230
|
+
const keys = Array.from({ length: 60 }, (_, i) => keyOf(`cqueue${i}`));
|
|
231
|
+
try {
|
|
232
|
+
warmSlackChannelLabels(keys.slice(0, 30));
|
|
233
|
+
await until(() => call.mock.calls.length === 1);
|
|
234
|
+
warmSlackChannelLabels(keys.slice(30));
|
|
235
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
236
|
+
} finally {
|
|
237
|
+
release();
|
|
238
|
+
}
|
|
239
|
+
await until(() => hasLabel("cqueue49", "#bounded"));
|
|
240
|
+
expect(call).toHaveBeenCalledTimes(50);
|
|
241
|
+
expect(await getSessionLabels(keys.slice(50))).toEqual({});
|
|
242
|
+
warmSlackChannelLabels(keys.slice(50));
|
|
243
|
+
await until(() => hasLabel("cqueue59", "#bounded"));
|
|
244
|
+
expect(call).toHaveBeenCalledTimes(60);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("an explicit Slack key without matching Slack fields starts no lookup", async () => {
|
|
248
|
+
await createTaskExtended("explicit source key", { key: `${keyOf("cfake")}task:fake/` });
|
|
249
|
+
await createTaskExtended("mismatched source key", {
|
|
250
|
+
key: `${keyOf("cfake")}task:mismatch/`,
|
|
251
|
+
slackChannelId: "COTHER",
|
|
252
|
+
slackThreadTs: "123.456",
|
|
253
|
+
});
|
|
254
|
+
const call = fakeSlack(async () => ({ ok: true, channel: { name: "untrusted" } }));
|
|
255
|
+
for (let i = 0; i < 2; i++) {
|
|
256
|
+
const response = await fetch(`${baseUrl}/api/session-tree`);
|
|
257
|
+
expect(response.status).toBe(200);
|
|
258
|
+
const body = await response.json();
|
|
259
|
+
expect(body.groups.map((group: { key: string }) => group.key)).toEqual([keyOf("cfake")]);
|
|
260
|
+
expect(body.labels).toEqual({});
|
|
261
|
+
expect(body.slackChannels).toBeUndefined();
|
|
262
|
+
}
|
|
263
|
+
expect(call).not.toHaveBeenCalled();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("tree returns cached source labels and starts one background lookup for a Slack miss", async () => {
|
|
267
|
+
for (const channel of ["CTREECACHED", "CTREEMISS"]) {
|
|
268
|
+
await createTaskExtended("label tree", {
|
|
269
|
+
source: "slack",
|
|
270
|
+
slackChannelId: channel,
|
|
271
|
+
slackThreadTs: "123.456",
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
await createTaskExtended("ordinary group", { key: "shared/research/" });
|
|
275
|
+
await rememberSessionLabel(keyOf("ctreecached"), "#cached");
|
|
276
|
+
await rememberSessionLabel("shared/research/", "omit ordinary labels");
|
|
277
|
+
let release!: () => void;
|
|
278
|
+
const gate = new Promise<void>((resolve) => {
|
|
279
|
+
release = resolve;
|
|
280
|
+
});
|
|
281
|
+
const call = fakeSlack(async () => {
|
|
282
|
+
await gate;
|
|
283
|
+
return { ok: true, channel: { name: "found" } };
|
|
284
|
+
});
|
|
285
|
+
try {
|
|
286
|
+
for (let i = 0; i < 2; i++) {
|
|
287
|
+
const response = await fetch(`${baseUrl}/api/session-tree`);
|
|
288
|
+
expect(response.status).toBe(200);
|
|
289
|
+
expect((await response.json()).labels).toEqual({ [keyOf("ctreecached")]: "#cached" });
|
|
290
|
+
}
|
|
291
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
292
|
+
} finally {
|
|
293
|
+
release();
|
|
294
|
+
}
|
|
295
|
+
await until(() => hasLabel("ctreemiss", "#found"));
|
|
296
|
+
expect((await (await fetch(`${baseUrl}/api/session-tree`)).json()).labels).toEqual({
|
|
297
|
+
[keyOf("ctreecached")]: "#cached",
|
|
298
|
+
[keyOf("ctreemiss")]: "#found",
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("tree returns labels for source groups the filter hides", async () => {
|
|
303
|
+
await createTaskExtended("hidden label tree", {
|
|
304
|
+
source: "slack",
|
|
305
|
+
slackChannelId: "CTREEHIDDEN",
|
|
306
|
+
slackThreadTs: "123.456",
|
|
307
|
+
});
|
|
308
|
+
await rememberSessionLabel(keyOf("ctreehidden"), "#hidden");
|
|
309
|
+
const body = await (await fetch(`${baseUrl}/api/session-tree?showSource=ui`)).json();
|
|
310
|
+
expect(body.groups.some((group: { key: string }) => group.key === keyOf("ctreehidden"))).toBe(
|
|
311
|
+
false,
|
|
312
|
+
);
|
|
313
|
+
expect(body.labels[keyOf("ctreehidden")]).toBe("#hidden");
|
|
314
|
+
expect(body.sourceGroups).toBeUndefined();
|
|
315
|
+
expect(body.slackChannels).toBeUndefined();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("Telegram capture stores a group title and a DM sender under their source group keys without TTL", async () => {
|
|
319
|
+
const harness = await startTelegramHarness();
|
|
320
|
+
try {
|
|
321
|
+
await postTelegramUpdate(
|
|
322
|
+
telegramUpdate({ id: -9911, type: "group" }, "@swarm_test_bot first ask"),
|
|
323
|
+
);
|
|
324
|
+
await flushChatBuffers();
|
|
325
|
+
await postTelegramUpdate(telegramUpdate({ id: 9912, type: "private" }, "first ask"));
|
|
326
|
+
await flushChatBuffers();
|
|
327
|
+
expect(await getSessionLabels(["shared/telegram/-9911/", "shared/telegram/9912/"])).toEqual({
|
|
328
|
+
"shared/telegram/-9911/": "Swarm group",
|
|
329
|
+
"shared/telegram/9912/": "DM · Tester (@tester)",
|
|
330
|
+
});
|
|
331
|
+
const rows = await getDbClient().query<{ expires_at: number | null }>(
|
|
332
|
+
"SELECT expires_at FROM kv_entries WHERE namespace = 'session-labels'",
|
|
333
|
+
);
|
|
334
|
+
expect(rows.every((row) => row.expires_at === null)).toBe(true);
|
|
335
|
+
await postTelegramUpdate(
|
|
336
|
+
telegramUpdate({ id: -9911, type: "group" }, "@swarm_test_bot follow-up", {
|
|
337
|
+
chat: { id: -9911, type: "group", title: "Changed title" },
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
await flushChatBuffers();
|
|
341
|
+
expect((await getSessionLabels(["shared/telegram/-9911/"]))["shared/telegram/-9911/"]).toBe(
|
|
342
|
+
"Changed title",
|
|
343
|
+
);
|
|
344
|
+
} finally {
|
|
345
|
+
await harness.stop();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
function discordMessage(dm: boolean): ChatBufferedMessage {
|
|
350
|
+
return {
|
|
351
|
+
messageId: crypto.randomUUID(),
|
|
352
|
+
channelId: "discord:guild:channel",
|
|
353
|
+
text: "hello",
|
|
354
|
+
userId: "sender",
|
|
355
|
+
botMentioned: true,
|
|
356
|
+
isDirectMessage: dm,
|
|
357
|
+
addressed: dm ? "dm" : "mention",
|
|
358
|
+
facts: { chatType: dm ? "dm" : undefined, from: "Lena", replyToBot: false },
|
|
359
|
+
attachments: [],
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
test("Discord capture stores a DM label and leaves channel names absent", async () => {
|
|
364
|
+
await flushChatThread("discord:@me:dmchannel", [discordMessage(true)], true);
|
|
365
|
+
await flushChatThread("discord:guild:channel", [discordMessage(false)], true);
|
|
366
|
+
expect(await getSessionLabels(["shared/discord/dmchannel/", "shared/discord/channel/"])).toEqual({
|
|
367
|
+
"shared/discord/dmchannel/": "DM · Lena",
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("a label write failure does not fail chat task creation or log the label", async () => {
|
|
372
|
+
const save = spyOn(labelsDb, "rememberSessionLabel").mockRejectedValue(new Error("DM · private"));
|
|
373
|
+
const warn = spyOn(console, "warn").mockImplementation(() => {});
|
|
374
|
+
try {
|
|
375
|
+
await flushChatThread("discord:@me:failedlabel", [discordMessage(true)], true);
|
|
376
|
+
const row = await getDbClient().get<{ id: string }>(
|
|
377
|
+
"SELECT id FROM agent_tasks WHERE contextKey = ?",
|
|
378
|
+
[chatSdkContextKey("discord:@me:failedlabel")],
|
|
379
|
+
);
|
|
380
|
+
expect(row?.id).toBeTruthy();
|
|
381
|
+
expect(warn.mock.calls.flat().join(" ")).not.toContain("DM · private");
|
|
382
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
383
|
+
} finally {
|
|
384
|
+
save.mockRestore();
|
|
385
|
+
warn.mockRestore();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("a full Slack failure cache skips new ids without evicting live failures and reclaims expired entries", async () => {
|
|
390
|
+
const warn = spyOn(console, "warn").mockImplementation(() => {});
|
|
391
|
+
const now = Date.now();
|
|
392
|
+
const clock = spyOn(Date, "now").mockReturnValue(now);
|
|
393
|
+
const call = fakeSlack(async () => {
|
|
394
|
+
throw new Error("lookup failed");
|
|
395
|
+
});
|
|
396
|
+
const keys = Array.from({ length: 500 }, (_, i) => keyOf(`cfull${i}`));
|
|
397
|
+
try {
|
|
398
|
+
for (let offset = 0; offset < keys.length; offset += 50) {
|
|
399
|
+
warmSlackChannelLabels(keys.slice(offset, offset + 50));
|
|
400
|
+
await until(() => warn.mock.calls.length === offset + 50);
|
|
401
|
+
}
|
|
402
|
+
expect(call).toHaveBeenCalledTimes(500);
|
|
403
|
+
warmSlackChannelLabels([keyOf("cfullnew")]);
|
|
404
|
+
await Bun.sleep(0);
|
|
405
|
+
warmSlackChannelLabels(keys);
|
|
406
|
+
await Bun.sleep(0);
|
|
407
|
+
expect(call).toHaveBeenCalledTimes(500);
|
|
408
|
+
expect(await getSessionLabels(keys)).toEqual({});
|
|
409
|
+
|
|
410
|
+
clock.mockReturnValue(now + 10 * 60_000 + 1);
|
|
411
|
+
call.mockImplementation(async () => ({ ok: true, channel: { name: "recovered" } }));
|
|
412
|
+
warmSlackChannelLabels([keyOf("cfullnew")]);
|
|
413
|
+
await until(() => hasLabel("cfullnew", "#recovered"));
|
|
414
|
+
expect(call).toHaveBeenCalledTimes(501);
|
|
415
|
+
} finally {
|
|
416
|
+
clock.mockRestore();
|
|
417
|
+
warn.mockRestore();
|
|
418
|
+
}
|
|
419
|
+
});
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
getTaskById,
|
|
15
15
|
initDb,
|
|
16
16
|
} from "../be/db";
|
|
17
|
+
import { getSessionGroupKey } from "../be/db/session-tree";
|
|
17
18
|
import { getTaskSteeringFields, getTaskSteeringFieldsForTasks } from "../be/steering";
|
|
18
19
|
import { getTaskCitations, getTaskCitationsForTasks } from "../be/task-citations";
|
|
19
20
|
import { handleSessions } from "../http/sessions";
|
|
@@ -56,6 +57,7 @@ async function unbatchedSession(id: string) {
|
|
|
56
57
|
// Session sharing: the bare operator key bypasses access checks, and the fixture root has no owner.
|
|
57
58
|
access: "owner",
|
|
58
59
|
owner: null,
|
|
60
|
+
groupKey: await getSessionGroupKey(root),
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
63
|
|