@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
|
@@ -152,8 +152,25 @@ const CASES: Array<{
|
|
|
152
152
|
name: "countSessions by source",
|
|
153
153
|
match: /COUNT/,
|
|
154
154
|
run: () => countSessions({ source: ["slack"] }),
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
// Counts read session_summaries since migration 259.
|
|
156
|
+
uses: ["COVERING INDEX idx_session_summaries_source (source=?)"],
|
|
157
|
+
revert: { dropIndex: "idx_session_summaries_source" },
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
// A user's task count hides other users' sessions. Both steps of that
|
|
161
|
+
// walk stay in an index (migration 262) instead of reading task rows.
|
|
162
|
+
name: "getTasksCount readableBy, root step",
|
|
163
|
+
match: /COUNT/,
|
|
164
|
+
run: () => getTasksCount({ status: "in_progress", readableBy: { userId: "viewer-1" } }),
|
|
165
|
+
uses: ["SEARCH r USING COVERING INDEX idx_agent_tasks_session_owner (scheduleId=?)"],
|
|
166
|
+
revert: { dropIndex: "idx_agent_tasks_session_owner" },
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "getTasksCount readableBy, recursive step",
|
|
170
|
+
match: /COUNT/,
|
|
171
|
+
run: () => getTasksCount({ status: "in_progress", readableBy: { userId: "viewer-1" } }),
|
|
172
|
+
uses: ["SEARCH c USING COVERING INDEX idx_agent_tasks_parentTaskId_id (parentTaskId=?)"],
|
|
173
|
+
revert: { dropIndex: "idx_agent_tasks_parentTaskId_id" },
|
|
157
174
|
},
|
|
158
175
|
{
|
|
159
176
|
// Few hits: the page reads the agent_tasks_fts candidates by rowid. The
|
|
@@ -220,6 +237,17 @@ describe("query audit plans without sqlite_stat1", () => {
|
|
|
220
237
|
});
|
|
221
238
|
}
|
|
222
239
|
|
|
240
|
+
test("the default task count scans the filter index, not the task rows", async () => {
|
|
241
|
+
// The heartbeat filter is a NOT IN and a NOT LIKE, so the count scans.
|
|
242
|
+
// Migration 263 lets it scan the narrow index instead of every task body.
|
|
243
|
+
const count = await capturedSql(/COUNT/, () =>
|
|
244
|
+
getTasksCount({ readableBy: { userId: "viewer-1" } }),
|
|
245
|
+
);
|
|
246
|
+
const uses = ["SCAN agent_tasks USING COVERING INDEX idx_agent_tasks_list_filter"];
|
|
247
|
+
expect(missing(plan(count), uses)).toEqual([]);
|
|
248
|
+
expect(missing(planWithoutIndex(count, "idx_agent_tasks_list_filter"), uses)).not.toEqual([]);
|
|
249
|
+
});
|
|
250
|
+
|
|
223
251
|
test("task search with many hits pages by lastUpdatedAt and counts from the index", async () => {
|
|
224
252
|
await getDbClient().transaction(async (tx) => {
|
|
225
253
|
for (let i = 0; i < 501; i++) {
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { closeDb, createAgent, getDbClient, initDb, upsertSwarmConfig } from "../be/db";
|
|
3
|
+
import { insertProvisioningRuntime, markRuntimeTerminated } from "../be/multi-runtime";
|
|
4
|
+
import { getPoolCache, reconcileCacheReleases } from "../be/runtime-pools/cache-cleanup";
|
|
5
|
+
import {
|
|
6
|
+
createRuntimeController,
|
|
7
|
+
createRuntimePool,
|
|
8
|
+
deleteRuntimeController,
|
|
9
|
+
deleteRuntimePool,
|
|
10
|
+
getRuntimePool,
|
|
11
|
+
updateRuntimeController,
|
|
12
|
+
updateRuntimePool,
|
|
13
|
+
} from "../be/runtime-pools/db";
|
|
14
|
+
import { provisionRuntime, reconcileOnce } from "../be/runtime-pools/supervisor";
|
|
15
|
+
import type { ControllerCommands } from "../runtime-providers/controller";
|
|
16
|
+
import type { ProvisionRequest, RuntimeCacheReference } from "../runtime-providers/types";
|
|
17
|
+
import { registerVolatileSecret } from "../utils/secret-scrubber";
|
|
18
|
+
|
|
19
|
+
const path = `./test-runtime-cache-cleanup-${process.pid}.sqlite`;
|
|
20
|
+
const testGlobals = globalThis as typeof globalThis & { __testMigrationTemplate?: Uint8Array };
|
|
21
|
+
let savedTemplate: Uint8Array | undefined;
|
|
22
|
+
const ownerId = "cache-cleanup-owner";
|
|
23
|
+
let controllerId: string;
|
|
24
|
+
let calls: RuntimeCacheReference[];
|
|
25
|
+
let release: (cache: RuntimeCacheReference) => Promise<void>;
|
|
26
|
+
let provisions: ProvisionRequest[];
|
|
27
|
+
let logSpy: ReturnType<typeof spyOn<typeof console, "error">>;
|
|
28
|
+
const commands: ControllerCommands = {
|
|
29
|
+
traits: { canProvision: true, ephemeralDisk: true, supportsPause: false, persistentCache: true },
|
|
30
|
+
workerApiUrl: "http://api.example.test",
|
|
31
|
+
provision: async (request) => {
|
|
32
|
+
provisions.push(request);
|
|
33
|
+
return { kind: "docker", externalId: request.runtimeId };
|
|
34
|
+
},
|
|
35
|
+
terminate: async () => {},
|
|
36
|
+
status: async () => ({}),
|
|
37
|
+
list: async () => [],
|
|
38
|
+
releaseCache: async (cache) => {
|
|
39
|
+
calls.push(cache);
|
|
40
|
+
await release(cache);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const drain = () => reconcileCacheReleases(async () => commands, new AbortController().signal);
|
|
44
|
+
const jobs = () =>
|
|
45
|
+
getDbClient().query<{ id: string; state: string; last_error: string | null }>(
|
|
46
|
+
"SELECT id, state, last_error FROM runtime_cache_releases",
|
|
47
|
+
);
|
|
48
|
+
const pool = () =>
|
|
49
|
+
createRuntimePool({
|
|
50
|
+
name: "cached",
|
|
51
|
+
kind: "docker",
|
|
52
|
+
placements: [controllerId],
|
|
53
|
+
image: "worker:test",
|
|
54
|
+
cache: { mountPath: "/cache" },
|
|
55
|
+
});
|
|
56
|
+
async function runtime(poolId: string) {
|
|
57
|
+
const agent = await createAgent({ name: crypto.randomUUID(), status: "idle", isLead: false });
|
|
58
|
+
const id = crypto.randomUUID();
|
|
59
|
+
await insertProvisioningRuntime({
|
|
60
|
+
id,
|
|
61
|
+
agentId: agent.id,
|
|
62
|
+
poolId,
|
|
63
|
+
controllerId,
|
|
64
|
+
reportedSlots: 1,
|
|
65
|
+
});
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
beforeAll(() => {
|
|
70
|
+
savedTemplate = testGlobals.__testMigrationTemplate;
|
|
71
|
+
testGlobals.__testMigrationTemplate = undefined;
|
|
72
|
+
closeDb();
|
|
73
|
+
initDb(path);
|
|
74
|
+
logSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
75
|
+
});
|
|
76
|
+
beforeEach(async () => {
|
|
77
|
+
for (const table of [
|
|
78
|
+
"runtime_cache_releases",
|
|
79
|
+
"runtime_instances",
|
|
80
|
+
"session_tokens",
|
|
81
|
+
"agents",
|
|
82
|
+
"runtime_pools",
|
|
83
|
+
"runtime_controllers",
|
|
84
|
+
"swarm_config",
|
|
85
|
+
])
|
|
86
|
+
await getDbClient().run(`DELETE FROM ${table}`);
|
|
87
|
+
await upsertSwarmConfig({ scope: "global", key: "runtime_owner_id", value: ownerId });
|
|
88
|
+
controllerId = (await createRuntimeController({ name: "cache-controller", kind: "docker" })).id;
|
|
89
|
+
calls = [];
|
|
90
|
+
provisions = [];
|
|
91
|
+
release = async () => {};
|
|
92
|
+
});
|
|
93
|
+
afterAll(async () => {
|
|
94
|
+
logSpy.mockRestore();
|
|
95
|
+
closeDb();
|
|
96
|
+
testGlobals.__testMigrationTemplate = savedTemplate;
|
|
97
|
+
for (const suffix of ["", "-wal", "-shm"])
|
|
98
|
+
await Bun.file(path + suffix)
|
|
99
|
+
.delete()
|
|
100
|
+
.catch(() => {});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("disable defers release for existing runtimes but not new cache-free runtimes", async () => {
|
|
104
|
+
const p = await pool();
|
|
105
|
+
const key = (await getPoolCache(p.id))!.key;
|
|
106
|
+
const old = await runtime(p.id);
|
|
107
|
+
await updateRuntimePool(p.id, { cache: null });
|
|
108
|
+
const fresh = await runtime(p.id);
|
|
109
|
+
await drain();
|
|
110
|
+
expect(calls).toEqual([]);
|
|
111
|
+
expect((await jobs())[0]?.state).toBe("pending");
|
|
112
|
+
await markRuntimeTerminated(old, "stopped");
|
|
113
|
+
await drain();
|
|
114
|
+
expect(calls).toEqual([{ key, poolId: p.id, ownerId }]);
|
|
115
|
+
expect(await jobs()).toEqual([]);
|
|
116
|
+
expect(
|
|
117
|
+
await getDbClient().get(
|
|
118
|
+
"SELECT id FROM runtime_instances WHERE id = ? AND status != 'offline'",
|
|
119
|
+
[fresh],
|
|
120
|
+
),
|
|
121
|
+
).not.toBeNull();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("pending release survives pool deletion and keeps its controller available", async () => {
|
|
125
|
+
const p = await pool();
|
|
126
|
+
await deleteRuntimePool(p.id);
|
|
127
|
+
expect(await getRuntimePool(p.id)).toBeNull();
|
|
128
|
+
await expect(
|
|
129
|
+
getDbClient().run("DELETE FROM runtime_controllers WHERE id = ?", [controllerId]),
|
|
130
|
+
).rejects.toThrow();
|
|
131
|
+
await expect(deleteRuntimeController(controllerId)).rejects.toThrow("pending cache cleanup");
|
|
132
|
+
await expect(
|
|
133
|
+
updateRuntimeController(controllerId, { config: { DOCKER_HOST: "unix:///another.sock" } }),
|
|
134
|
+
).rejects.toThrow("wait for cleanup");
|
|
135
|
+
await updateRuntimeController(controllerId, { name: "renamed" });
|
|
136
|
+
await drain();
|
|
137
|
+
expect(calls[0]?.poolId).toBe(p.id);
|
|
138
|
+
await deleteRuntimeController(controllerId);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("a failed release is scrubbed, persisted over restart, and retried", async () => {
|
|
142
|
+
const p = await pool();
|
|
143
|
+
const secret = "cache-cleanup-private-value";
|
|
144
|
+
registerVolatileSecret(secret, "cleanup-test");
|
|
145
|
+
release = async () => {
|
|
146
|
+
throw new Error(`in use ${secret}`);
|
|
147
|
+
};
|
|
148
|
+
await updateRuntimePool(p.id, { cache: null });
|
|
149
|
+
await drain();
|
|
150
|
+
expect((await jobs())[0]).toMatchObject({
|
|
151
|
+
state: "pending",
|
|
152
|
+
last_error: expect.stringContaining("in use"),
|
|
153
|
+
});
|
|
154
|
+
expect((await jobs())[0]?.last_error).not.toContain(secret);
|
|
155
|
+
const id = (await jobs())[0]!.id;
|
|
156
|
+
closeDb();
|
|
157
|
+
initDb(path);
|
|
158
|
+
expect((await jobs())[0]?.id).toBe(id);
|
|
159
|
+
release = async () => {};
|
|
160
|
+
await drain();
|
|
161
|
+
expect(calls).toHaveLength(2);
|
|
162
|
+
expect(await jobs()).toEqual([]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("concurrent cache reconciles release a cache only once", async () => {
|
|
166
|
+
const p = await pool();
|
|
167
|
+
await updateRuntimePool(p.id, { cache: null });
|
|
168
|
+
const entered = Promise.withResolvers<void>();
|
|
169
|
+
const duplicate = Promise.withResolvers<void>();
|
|
170
|
+
const finish = Promise.withResolvers<void>();
|
|
171
|
+
release = async () => {
|
|
172
|
+
entered.resolve();
|
|
173
|
+
if (calls.length > 1) duplicate.resolve();
|
|
174
|
+
await finish.promise;
|
|
175
|
+
};
|
|
176
|
+
const first = drain();
|
|
177
|
+
await entered.promise;
|
|
178
|
+
const second = drain();
|
|
179
|
+
try {
|
|
180
|
+
await Promise.race([second, duplicate.promise]);
|
|
181
|
+
expect(calls).toHaveLength(1);
|
|
182
|
+
} finally {
|
|
183
|
+
finish.resolve();
|
|
184
|
+
await Promise.all([first, second]);
|
|
185
|
+
}
|
|
186
|
+
expect(await jobs()).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("re-enabling uses a new generation; edits retain the current generation", async () => {
|
|
190
|
+
const p = await pool();
|
|
191
|
+
const oldKey = (await getPoolCache(p.id))!.key;
|
|
192
|
+
await updateRuntimePool(p.id, { name: "renamed", cache: { mountPath: "/cache/changed" } });
|
|
193
|
+
closeDb();
|
|
194
|
+
initDb(path);
|
|
195
|
+
expect((await getPoolCache(p.id))!.key).toBe(oldKey);
|
|
196
|
+
await updateRuntimePool(p.id, { cache: null });
|
|
197
|
+
await updateRuntimePool(p.id, { cache: { mountPath: "/cache" } });
|
|
198
|
+
const newKey = (await getPoolCache(p.id))!.key;
|
|
199
|
+
expect(newKey).not.toBe(oldKey);
|
|
200
|
+
expect((await getRuntimePool(p.id))!.cache).toEqual({ mountPath: "/cache" });
|
|
201
|
+
await drain();
|
|
202
|
+
expect(calls.map((c) => c.key)).toEqual([oldKey]);
|
|
203
|
+
expect(await jobs()).toEqual([]);
|
|
204
|
+
expect((await getPoolCache(p.id))!.key).toBe(newKey);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("re-enabling during a release leaves the retired generation isolated", async () => {
|
|
208
|
+
const p = await pool();
|
|
209
|
+
const oldKey = (await getPoolCache(p.id))!.key;
|
|
210
|
+
await updateRuntimePool(p.id, { cache: null });
|
|
211
|
+
const entered = Promise.withResolvers<void>();
|
|
212
|
+
const finish = Promise.withResolvers<void>();
|
|
213
|
+
release = async () => {
|
|
214
|
+
entered.resolve();
|
|
215
|
+
await finish.promise;
|
|
216
|
+
};
|
|
217
|
+
const draining = drain();
|
|
218
|
+
await entered.promise;
|
|
219
|
+
try {
|
|
220
|
+
await updateRuntimePool(p.id, { cache: { mountPath: "/cache" } });
|
|
221
|
+
expect((await getPoolCache(p.id))!.key).not.toBe(oldKey);
|
|
222
|
+
expect((await jobs())[0]?.state).toBe("deleting");
|
|
223
|
+
} finally {
|
|
224
|
+
finish.resolve();
|
|
225
|
+
await draining;
|
|
226
|
+
}
|
|
227
|
+
expect(calls[0]?.key).toBe(oldKey);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("legacy cache names remain readable until re-enabling rotates the key", async () => {
|
|
231
|
+
const p = await pool();
|
|
232
|
+
await getDbClient().run("UPDATE runtime_pools SET cache = ? WHERE id = ?", [
|
|
233
|
+
JSON.stringify({ mountPath: "/cache" }),
|
|
234
|
+
p.id,
|
|
235
|
+
]);
|
|
236
|
+
expect((await getPoolCache(p.id))!.key).toBe(`swarm-cache-${p.id}`);
|
|
237
|
+
await updateRuntimePool(p.id, { cache: null });
|
|
238
|
+
await updateRuntimePool(p.id, { cache: { mountPath: "/cache" } });
|
|
239
|
+
expect((await getPoolCache(p.id))!.key).not.toBe(`swarm-cache-${p.id}`);
|
|
240
|
+
await drain();
|
|
241
|
+
expect(calls[0]?.key).toBe(`swarm-cache-${p.id}`);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("an abandoned deleting lease is recovered after restart", async () => {
|
|
245
|
+
const p = await pool();
|
|
246
|
+
await updateRuntimePool(p.id, { cache: null });
|
|
247
|
+
await getDbClient().run("UPDATE runtime_cache_releases SET state = 'deleting', updated_at = ?", [
|
|
248
|
+
new Date(Date.now() - 6 * 60_000).toISOString(),
|
|
249
|
+
]);
|
|
250
|
+
closeDb();
|
|
251
|
+
initDb(path);
|
|
252
|
+
await drain();
|
|
253
|
+
expect(calls).toHaveLength(1);
|
|
254
|
+
expect(await jobs()).toEqual([]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
for (const outcome of ["success", "failure"] as const) {
|
|
258
|
+
test(`an expired worker's ${outcome} cannot overwrite a newer cleanup claim`, async () => {
|
|
259
|
+
const p = await pool();
|
|
260
|
+
await updateRuntimePool(p.id, { cache: null });
|
|
261
|
+
const firstEntered = Promise.withResolvers<void>();
|
|
262
|
+
const secondEntered = Promise.withResolvers<void>();
|
|
263
|
+
const firstFinish = Promise.withResolvers<void>();
|
|
264
|
+
const secondFinish = Promise.withResolvers<void>();
|
|
265
|
+
release = async () => {
|
|
266
|
+
const first = calls.length === 1;
|
|
267
|
+
(first ? firstEntered : secondEntered).resolve();
|
|
268
|
+
await (first ? firstFinish : secondFinish).promise;
|
|
269
|
+
if (first && outcome === "failure") throw new Error("late failure");
|
|
270
|
+
};
|
|
271
|
+
const first = drain();
|
|
272
|
+
await firstEntered.promise;
|
|
273
|
+
const second = reconcileCacheReleases(
|
|
274
|
+
async () => commands,
|
|
275
|
+
new AbortController().signal,
|
|
276
|
+
new Date(Date.now() + 6 * 60_000),
|
|
277
|
+
);
|
|
278
|
+
await secondEntered.promise;
|
|
279
|
+
try {
|
|
280
|
+
firstFinish.resolve();
|
|
281
|
+
await first;
|
|
282
|
+
expect((await jobs())[0]).toMatchObject({ state: "deleting", last_error: null });
|
|
283
|
+
} finally {
|
|
284
|
+
firstFinish.resolve();
|
|
285
|
+
secondFinish.resolve();
|
|
286
|
+
await Promise.all([first, second]);
|
|
287
|
+
}
|
|
288
|
+
expect(await jobs()).toEqual([]);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
test("an unsupported or offline controller retains cleanup for a later retry", async () => {
|
|
293
|
+
const p = await pool();
|
|
294
|
+
await updateRuntimePool(p.id, { cache: null });
|
|
295
|
+
await reconcileCacheReleases(async () => {
|
|
296
|
+
throw new Error("controller offline");
|
|
297
|
+
}, new AbortController().signal);
|
|
298
|
+
expect((await jobs())[0]?.last_error).toContain("offline");
|
|
299
|
+
const { releaseCache: _, ...legacy } = commands;
|
|
300
|
+
await reconcileCacheReleases(async () => legacy, new AbortController().signal);
|
|
301
|
+
expect((await jobs())[0]?.last_error).toContain("upgrade");
|
|
302
|
+
await drain();
|
|
303
|
+
expect(await jobs()).toEqual([]);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("caches still configured on a disabled pool are retained for future use", async () => {
|
|
307
|
+
const p = await pool();
|
|
308
|
+
await updateRuntimePool(p.id, { enabled: false });
|
|
309
|
+
await drain();
|
|
310
|
+
expect(await jobs()).toEqual([]);
|
|
311
|
+
expect(calls).toEqual([]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("the controller target cannot change while a cache exists", async () => {
|
|
315
|
+
const p = await pool();
|
|
316
|
+
await expect(updateRuntimeController(controllerId, { placement: "builtin" })).rejects.toThrow(
|
|
317
|
+
"Disable pool caches",
|
|
318
|
+
);
|
|
319
|
+
await updateRuntimeController(controllerId, { config: {}, secretConfig: {} });
|
|
320
|
+
await expect(
|
|
321
|
+
updateRuntimeController(controllerId, { secretConfig: { KEY: "changed" } }),
|
|
322
|
+
).rejects.toThrow("Disable pool caches");
|
|
323
|
+
await updateRuntimePool(p.id, { cache: null });
|
|
324
|
+
await drain();
|
|
325
|
+
await updateRuntimeController(controllerId, { config: { DOCKER_HOST: "unix:///new.sock" } });
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("a stale pool snapshot does not mount a disabled cache during provision", async () => {
|
|
329
|
+
const p = await pool();
|
|
330
|
+
await updateRuntimePool(p.id, { cache: null });
|
|
331
|
+
const agent = await createAgent({ name: "fresh", status: "idle", isLead: false });
|
|
332
|
+
expect(await provisionRuntime(p, agent, commands, ownerId)).toBe(true);
|
|
333
|
+
expect(provisions).toHaveLength(1);
|
|
334
|
+
expect(provisions[0]?.cache).toBeUndefined();
|
|
335
|
+
await drain();
|
|
336
|
+
expect(calls[0]?.poolId).toBe(p.id);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("the supervisor tick releases a deleted pool cache", async () => {
|
|
340
|
+
const p = await pool();
|
|
341
|
+
await deleteRuntimePool(p.id);
|
|
342
|
+
const previousPools = process.env.RUNTIME_POOLS_ENABLED;
|
|
343
|
+
const previousMulti = process.env.MULTI_RUNTIME_ENABLED;
|
|
344
|
+
process.env.RUNTIME_POOLS_ENABLED = "true";
|
|
345
|
+
process.env.MULTI_RUNTIME_ENABLED = "true";
|
|
346
|
+
try {
|
|
347
|
+
await reconcileOnce({ getControllerCommands: async () => commands });
|
|
348
|
+
expect(calls[0]?.poolId).toBe(p.id);
|
|
349
|
+
expect(await jobs()).toEqual([]);
|
|
350
|
+
} finally {
|
|
351
|
+
if (previousPools === undefined) delete process.env.RUNTIME_POOLS_ENABLED;
|
|
352
|
+
else process.env.RUNTIME_POOLS_ENABLED = previousPools;
|
|
353
|
+
if (previousMulti === undefined) delete process.env.MULTI_RUNTIME_ENABLED;
|
|
354
|
+
else process.env.MULTI_RUNTIME_ENABLED = previousMulti;
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("a stale provision cannot recreate the cache on a previous placement", async () => {
|
|
359
|
+
const p = await pool();
|
|
360
|
+
const next = await createRuntimeController({ name: "next", kind: "docker" });
|
|
361
|
+
await updateRuntimePool(p.id, { placements: [next.id] });
|
|
362
|
+
const agent = await createAgent({ name: "moved", status: "idle", isLead: false });
|
|
363
|
+
expect(await provisionRuntime(p, agent, commands, ownerId)).toBe(true);
|
|
364
|
+
expect(provisions[0]?.cache).toBeUndefined();
|
|
365
|
+
await drain();
|
|
366
|
+
expect(calls[0]?.poolId).toBe(p.id);
|
|
367
|
+
});
|
|
@@ -226,6 +226,47 @@ async function busy(options: Partial<Parameters<typeof createRuntimePool>[0]> =
|
|
|
226
226
|
return state;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
test("boxd env defaults apply only to builtin boxd targets and stored config wins", async () => {
|
|
230
|
+
const defaults = {
|
|
231
|
+
BOXD_ORG: "env-org",
|
|
232
|
+
BOXD_BASE_URL: "https://boxd-env.test",
|
|
233
|
+
BOXD_SNAPSHOT: "env-snapshot",
|
|
234
|
+
BOXD_DELETE_AFTER_SEC: "3600",
|
|
235
|
+
BOXD_START_TIMEOUT_SEC: "120",
|
|
236
|
+
BOXD_STATUS_TIMEOUT_SEC: "30",
|
|
237
|
+
BOXD_TEARDOWN_TIMEOUT_SEC: "60",
|
|
238
|
+
};
|
|
239
|
+
const storedKey = `stored-${crypto.randomUUID()}`;
|
|
240
|
+
const env = { ...defaults, BOXD_API_KEY: `env-${crypto.randomUUID()}` };
|
|
241
|
+
const builtin = await createRuntimeController({
|
|
242
|
+
name: "builtin-boxd",
|
|
243
|
+
kind: "boxd",
|
|
244
|
+
placement: "builtin",
|
|
245
|
+
secretConfig: { BOXD_API_KEY: storedKey },
|
|
246
|
+
});
|
|
247
|
+
expect(await getRuntimeControllerTarget(builtin, env)).toMatchObject({
|
|
248
|
+
...defaults,
|
|
249
|
+
BOXD_API_KEY: storedKey,
|
|
250
|
+
});
|
|
251
|
+
const configured = await updateRuntimeController(builtin.id, {
|
|
252
|
+
config: { BOXD_BASE_URL: "https://boxd-config.test" },
|
|
253
|
+
});
|
|
254
|
+
expect((await getRuntimeControllerTarget(configured, env)).BOXD_BASE_URL).toBe(
|
|
255
|
+
"https://boxd-config.test",
|
|
256
|
+
);
|
|
257
|
+
const remote = await createRuntimeController({
|
|
258
|
+
name: "remote-boxd",
|
|
259
|
+
kind: "boxd",
|
|
260
|
+
placement: "remote",
|
|
261
|
+
});
|
|
262
|
+
for (const controller of [remote, (await getRuntimeController(controllerId))!]) {
|
|
263
|
+
const target = await getRuntimeControllerTarget(controller, env);
|
|
264
|
+
for (const key of [...Object.keys(defaults), "BOXD_API_KEY"]) {
|
|
265
|
+
expect(target[key]).toBeUndefined();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
229
270
|
test("pending work provisions an E2B runtime with a short lease", async () => {
|
|
230
271
|
const { pool, agent } = await setup();
|
|
231
272
|
await tick();
|
|
@@ -52,6 +52,104 @@ beforeEach(async () => {
|
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
describe("runtime pools persistence", () => {
|
|
55
|
+
test("pool caches default off, persist across edits, and can be disabled explicitly", async () => {
|
|
56
|
+
const controller = await createRuntimeController({ name: "cache-controller", kind: "docker" });
|
|
57
|
+
const base = {
|
|
58
|
+
name: "cached-pool",
|
|
59
|
+
kind: "docker" as const,
|
|
60
|
+
placements: [controller.id],
|
|
61
|
+
image: "worker:test",
|
|
62
|
+
};
|
|
63
|
+
const pool = await createRuntimePool(base);
|
|
64
|
+
expect(pool.cache).toBeNull();
|
|
65
|
+
expect((await updateRuntimePool(pool.id, { cache: { mountPath: "/cache" } })).cache).toEqual({
|
|
66
|
+
mountPath: "/cache",
|
|
67
|
+
});
|
|
68
|
+
expect((await updateRuntimePool(pool.id, { name: "renamed-pool" })).cache).toEqual({
|
|
69
|
+
mountPath: "/cache",
|
|
70
|
+
});
|
|
71
|
+
expect((await getRuntimePool(pool.id))?.cache).toEqual({ mountPath: "/cache" });
|
|
72
|
+
expect((await updateRuntimePool(pool.id, { cache: null })).cache).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("pool cache validation rejects unsafe mount paths and unsupported providers", async () => {
|
|
76
|
+
const docker = await createRuntimeController({
|
|
77
|
+
name: "docker-cache-validation",
|
|
78
|
+
kind: "docker",
|
|
79
|
+
});
|
|
80
|
+
for (const mountPath of [
|
|
81
|
+
"/",
|
|
82
|
+
"//",
|
|
83
|
+
"relative",
|
|
84
|
+
"/cache/..",
|
|
85
|
+
"/./cache",
|
|
86
|
+
"/cache\0bad",
|
|
87
|
+
"/home",
|
|
88
|
+
"/home/worker",
|
|
89
|
+
"/home/worker/",
|
|
90
|
+
"//home//worker//",
|
|
91
|
+
"/workspace",
|
|
92
|
+
"/workspace/personal",
|
|
93
|
+
"/workspace/shared",
|
|
94
|
+
"/workspace/personal/cache",
|
|
95
|
+
"//workspace//personal//cache/",
|
|
96
|
+
"/workspace/shared/.cache",
|
|
97
|
+
"/workspace/shared/repo/.git",
|
|
98
|
+
"/root",
|
|
99
|
+
"/home/worker/.codex",
|
|
100
|
+
"/home/worker/.local/share/opencode",
|
|
101
|
+
"/home/worker/.claude.json",
|
|
102
|
+
"/home/worker/.ssh/keys",
|
|
103
|
+
"/home/worker/.config/cache",
|
|
104
|
+
"/root/.claude",
|
|
105
|
+
"/root/.local/share/opencode/cache",
|
|
106
|
+
]) {
|
|
107
|
+
await expect(
|
|
108
|
+
createRuntimePool({
|
|
109
|
+
name: "invalid-path",
|
|
110
|
+
kind: "docker",
|
|
111
|
+
placements: [docker.id],
|
|
112
|
+
image: "worker:test",
|
|
113
|
+
cache: { mountPath },
|
|
114
|
+
}),
|
|
115
|
+
).rejects.toThrow("mountPath");
|
|
116
|
+
}
|
|
117
|
+
for (const kind of ["static", "e2b", "boxd"] as const) {
|
|
118
|
+
const controller = await createRuntimeController({ name: `${kind}-cache-validation`, kind });
|
|
119
|
+
await expect(
|
|
120
|
+
createRuntimePool({
|
|
121
|
+
name: "unsupported-cache",
|
|
122
|
+
kind,
|
|
123
|
+
placements: [controller.id],
|
|
124
|
+
image: "worker:test",
|
|
125
|
+
cache: { mountPath: "/cache" },
|
|
126
|
+
}),
|
|
127
|
+
).rejects.toThrow("does not support persistent pool caches");
|
|
128
|
+
}
|
|
129
|
+
expect(await getDbClient().query("SELECT id FROM runtime_pools")).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("cache mount validation accepts isolated cache subdirectories", async () => {
|
|
133
|
+
const controller = await createRuntimeController({ name: "safe-cache-paths", kind: "docker" });
|
|
134
|
+
for (const [index, mountPath] of [
|
|
135
|
+
"/cache",
|
|
136
|
+
"/home/worker/.cache",
|
|
137
|
+
"/home/worker/.cache/bun",
|
|
138
|
+
"/workspace/cache",
|
|
139
|
+
"/root/.cache",
|
|
140
|
+
"/workspace/personal-cache",
|
|
141
|
+
].entries()) {
|
|
142
|
+
const pool = await createRuntimePool({
|
|
143
|
+
name: `safe-cache-${index}`,
|
|
144
|
+
kind: "docker",
|
|
145
|
+
placements: [controller.id],
|
|
146
|
+
image: "worker:test",
|
|
147
|
+
cache: { mountPath },
|
|
148
|
+
});
|
|
149
|
+
expect(pool.cache?.mountPath).toBe(mountPath);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
55
153
|
test("maps persisted task wake and agent provisioning state without changing routing", async () => {
|
|
56
154
|
const agent = await createAgent({ name: "worker", status: "offline", isLead: false });
|
|
57
155
|
expect(agent).toMatchObject({ runtimeProvisionAttempts: 0, runtimeProvisionRetryAt: null });
|
|
@@ -70,6 +70,35 @@ async function dispatch(
|
|
|
70
70
|
).toBe(true);
|
|
71
71
|
return { status, text, data: JSON.parse(text) };
|
|
72
72
|
}
|
|
73
|
+
test("operator can configure a pool cache through HTTP without selecting provider volume names", async () => {
|
|
74
|
+
const controller = await dispatch("/api/runtime-controllers", "POST", {
|
|
75
|
+
name: "cache-controller",
|
|
76
|
+
kind: "docker",
|
|
77
|
+
});
|
|
78
|
+
const input = {
|
|
79
|
+
name: "cache-pool",
|
|
80
|
+
kind: "docker",
|
|
81
|
+
placements: [controller.data.controller.id],
|
|
82
|
+
image: "worker:test",
|
|
83
|
+
cache: { mountPath: "/cache" },
|
|
84
|
+
};
|
|
85
|
+
const created = await dispatch("/api/runtime-pools", "POST", input);
|
|
86
|
+
expect(created.status).toBe(200);
|
|
87
|
+
expect(created.data.pool.cache).toEqual({ mountPath: "/cache" });
|
|
88
|
+
const id = created.data.pool.id;
|
|
89
|
+
const renamed = await dispatch(`/api/runtime-pools/${id}`, "PATCH", {
|
|
90
|
+
name: "renamed-cache-pool",
|
|
91
|
+
});
|
|
92
|
+
expect(renamed.data.pool.cache).toEqual({ mountPath: "/cache" });
|
|
93
|
+
const rejected = await dispatch(`/api/runtime-pools/${id}`, "PATCH", {
|
|
94
|
+
cache: { mountPath: "/cache", key: "foreign-volume" },
|
|
95
|
+
});
|
|
96
|
+
expect(rejected.status).toBe(400);
|
|
97
|
+
const disabled = await dispatch(`/api/runtime-pools/${id}`, "PATCH", { cache: null });
|
|
98
|
+
expect(disabled.status).toBe(200);
|
|
99
|
+
expect(disabled.data.pool.cache).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
73
102
|
beforeAll(async () => {
|
|
74
103
|
await cleanup();
|
|
75
104
|
initDb(path);
|