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