@desplega.ai/agent-swarm 2.7.5 → 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/dist/{acp-adapter-g3ads55s.js → acp-adapter-bp91d37f.js} +4 -4
- package/dist/{actions-zwfykqqk.js → actions-yj0eeaqh.js} +18 -17
- package/dist/{amp-adapter-z3s0ssdt.js → amp-adapter-4pg9h34q.js} +1 -1
- package/dist/{app-vmp4fgep.js → app-909e97cr.js} +3 -3
- package/dist/{assistant-vwde653f.js → assistant-qdyb5fyy.js} +24 -22
- package/dist/{boot-reembed-375t7tbk.js → boot-reembed-fk0s9z18.js} +1 -1
- package/dist/{boot-reembed-gq9rcpnj.js → boot-reembed-gs1rzrnh.js} +1 -1
- package/dist/{boot-scrub-sweep-nd938kyf.js → boot-scrub-sweep-4qfd4kfx.js} +2 -2
- package/dist/{boot-seeds-aykx51v3.js → boot-seeds-yr00k13d.js} +4 -4
- package/dist/{boxd-k1j26g3h.js → boxd-vdmfssd9.js} +4 -4
- package/dist/{claude-adapter-8j4hm099.js → claude-adapter-qta1f1xn.js} +3 -3
- package/dist/{claude-managed-setup-0wyynpn1.js → claude-managed-setup-sc6j4dm0.js} +1 -1
- package/dist/{claude-sdk-session-q6wj5hmy.js → claude-sdk-session-zgrz6yvc.js} +9169 -9070
- package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
- package/dist/{cli-vqypxzh4.js → cli-0at8w34w.js} +7 -7
- package/dist/{cli-7a42nfy0.js → cli-0qatpy8q.js} +3 -3
- package/dist/{cli-p599f1qm.js → cli-0yv5z2pg.js} +1 -1
- package/dist/{cli-0gey5fzy.js → cli-386tyxxg.js} +2 -2
- package/dist/{cli-5pag6rmn.js → cli-3ypk9vk1.js} +1 -1
- package/dist/{cli-qxnf8z2z.js → cli-436532ck.js} +2 -2
- package/dist/{cli-z8eh3ex8.js → cli-4necwtsv.js} +4 -4
- package/dist/{cli-zh3fp8v2.js → cli-54t00gt8.js} +1 -1
- package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
- package/dist/{cli-nn4d8dne.js → cli-89knwags.js} +2 -2
- package/dist/{cli-spknvxbq.js → cli-8bc1w0tw.js} +10 -10
- package/dist/{cli-rqkxrmgw.js → cli-8snvgs5r.js} +38 -13
- package/dist/{cli-rhw40jgb.js → cli-bjb3z51z.js} +2 -2
- package/dist/{cli-xtjhfmpc.js → cli-c60vetmm.js} +1 -1
- package/dist/{cli-0731hvbw.js → cli-decfb6v8.js} +2 -2
- package/dist/{cli-bhjt02p8.js → cli-drr5v6av.js} +1 -1
- package/dist/{cli-fqzx7gwa.js → cli-dxae2yg7.js} +2 -2
- package/dist/{cli-tqgg40ek.js → cli-e3r2neck.js} +8 -7
- package/dist/{cli-zppwg9p9.js → cli-ekwks2vs.js} +1 -1
- package/dist/{cli-nzxr556s.js → cli-es6va8jk.js} +26 -26
- package/dist/{cli-a47xy8eq.js → cli-faxe6thg.js} +1 -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-pywf3eh5.js → cli-hgaazk69.js} +1 -1
- package/dist/{cli-0bxqnvq9.js → cli-hr2e2fft.js} +28 -6
- package/dist/{cli-1ayk1eas.js → cli-k2gzwsj2.js} +135 -26
- package/dist/{cli-xbkdm710.js → cli-kqdg1bsb.js} +3 -3
- package/dist/cli-m2z8jagn.js +12078 -0
- package/dist/{cli-g61qxmge.js → cli-m48g6p95.js} +3 -3
- package/dist/{cli-7wspm5gm.js → cli-nhrvpaj7.js} +20 -4
- package/dist/{cli-afrrppsw.js → cli-nwj0ee2w.js} +2 -2
- package/dist/{cli-3h4nhjn8.js → cli-p5gp8pcx.js} +9 -7
- package/dist/{cli-rm6hqj3z.js → cli-pd7vagab.js} +33 -139
- package/dist/{cli-mjzynrse.js → cli-q0k1w5zd.js} +1 -1
- package/dist/{cli-8hs41zy2.js → cli-rmmmrjkd.js} +1 -1
- package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
- package/dist/{cli-1338796d.js → cli-shpbx5ee.js} +142 -98
- package/dist/{cli-82ae98pz.js → cli-svj5090z.js} +2 -2
- package/dist/{cli-bnz7a7qf.js → cli-vvpx3gsx.js} +1 -1
- package/dist/{cli-mmwm7zbf.js → cli-w6dt9ged.js} +1 -1
- package/dist/{cli-wvaawacd.js → cli-waqbxg08.js} +5230 -66
- package/dist/{cli-j3jvnpe8.js → cli-wp9sp1qb.js} +1 -1
- package/dist/{cli-nmbmy64z.js → cli-xbbndvjy.js} +3 -3
- package/dist/{cli-vbyfwh92.js → cli-xds5f77d.js} +2 -2
- package/dist/{cli-7wcyk2s5.js → cli-xnqgpshy.js} +1 -1
- package/dist/{cli-rpm2sgv0.js → cli-xnrwazav.js} +19 -2
- package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
- package/dist/{cli-mry0qzym.js → cli-yn4kxs06.js} +123 -10
- package/dist/{cli-hn9hxmz9.js → cli-yx4kfngt.js} +1 -1
- package/dist/{cli-y24sc29k.js → cli-yy3rhsdw.js} +30 -8
- package/dist/{cli-6s7vdcta.js → cli-zbbwez0r.js} +4 -4
- package/dist/{cli-z6jkx23q.js → cli-zm72anhg.js} +9 -9
- package/dist/cli.js +22 -21
- package/dist/{codex-adapter-q6k3gnyg.js → codex-adapter-ayd1fb6h.js} +2 -2
- package/dist/{codex-session-runner-3mwmk3d9.js → codex-session-runner-jesa32xc.js} +2 -2
- package/dist/{commands-evrenjn9.js → commands-hanewgf2.js} +9 -3
- package/dist/{controller-xjp8vswh.js → controller-dt1n3c1q.js} +8 -3
- package/dist/{controllers-connect-pp5jnxvc.js → controllers-connect-xx8x64n5.js} +20 -19
- package/dist/{cursor-adapter-yf47w4t0.js → cursor-adapter-9s4dwwdw.js} +1 -1
- package/dist/{db-zxpw6aht.js → db-3vttcbv4.js} +3 -3
- package/dist/{db-rgfsh91f.js → db-9dv0g1ab.js} +11 -3
- package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
- package/dist/{dispatcher-smasgws7.js → dispatcher-tx9hd1sv.js} +17 -16
- package/dist/{docker-rrjhaqt9.js → docker-c1a3gqmf.js} +89 -22
- package/dist/{dsh-adapter-ftyy8dnp.js → dsh-adapter-007y4ejs.js} +1 -1
- package/dist/{e2b-qcaj675y.js → e2b-a19d28rz.js} +2 -2
- package/dist/{e2b-w1p0bnpm.js → e2b-we6hs6q5.js} +4 -4
- package/dist/{easter-egg-3d-yyts7p16.js → easter-egg-3d-rgbrqb8s.js} +1 -1
- package/dist/{embedding-space-1z0mbg95.js → embedding-space-ny1e2940.js} +6 -6
- package/dist/{embeddings-ygeg7tm7.js → embeddings-2t386t13.js} +1 -1
- package/dist/{execute-14qhqp4d.js → execute-pksh0ceh.js} +1 -1
- package/dist/{external-channel-ttgrs3sx.js → external-channel-phgmhmd7.js} +2 -2
- package/dist/{fx-adapter-gt6pncj2.js → fx-adapter-fy8rr921.js} +5 -5
- 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-sm4m5k4g.js → grok-adapter-q0abvpaj.js} +5 -5
- package/dist/handlers-by3sfxyx.js +32 -0
- package/dist/{heartbeat-qahz4p62.js → heartbeat-kadyek8p.js} +20 -19
- package/dist/{hook-kj3d398m.js → hook-5zs3msf6.js} +2 -2
- package/dist/{hook-4fd5wq5w.js → hook-9k551xnj.js} +2 -2
- package/dist/{http-1ted0j1k.js → http-mhz9vw1f.js} +213 -80
- 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-6rq3r5zb.js → index-q361cg2f.js} +1 -1
- package/dist/{index-7v51awbn.js → index-rvr00sg7.js} +20 -13
- package/dist/{index-q4b917zs.js → index-vdygbq10.js} +1 -1
- package/dist/{keepalive-6cf311t5.js → keepalive-w56f32cg.js} +3 -3
- package/dist/{lead-fjy418cw.js → lead-enq35nqp.js} +31 -30
- package/dist/{maintenance-a4bm1bmt.js → maintenance-7mdgxcea.js} +1 -1
- package/dist/{oauth-refresh-sweep-3h0dfw1k.js → oauth-refresh-sweep-29txy9vk.js} +2 -2
- package/dist/{onboard-myn5hraa.js → onboard-kpsrkarj.js} +4 -4
- package/dist/{opencode-adapter-p2nkb5am.js → opencode-adapter-j25vm3rr.js} +3 -3
- package/dist/{otel-impl-a1a0ef6b.js → otel-impl-6kkct7a8.js} +1 -1
- package/dist/{pi-mono-adapter-rzmpb72a.js → pi-mono-adapter-exhkzhn2.js} +4 -4
- package/dist/{pricing-refresh-83drrnxj.js → pricing-refresh-n2x7jh6k.js} +3 -3
- package/dist/{rbac-roles-s8y6ecb5.js → rbac-roles-81ypx6es.js} +1 -1
- package/dist/{rbac-roles-0fb6e7pj.js → rbac-roles-b5htkjby.js} +2 -2
- package/dist/{render-v2-twmbgrz4.js → render-v2-qn6jmpk4.js} +3 -3
- package/dist/{resume-zfqc36xj.js → resume-wd30wx23.js} +6 -6
- package/dist/{runtime-qwggdpqr.js → runtime-4hsg7h60.js} +1 -1
- package/dist/{session-access-sc9pn9da.js → session-access-5qvfevhk.js} +4 -4
- package/dist/{session-source-backfill-qh07xr8g.js → session-source-backfill-n1czavbr.js} +2 -2
- package/dist/{setup-4fr0m7a1.js → setup-dnb56qy5.js} +3 -3
- package/dist/{virtual-modules-fh4ha15b.js → virtual-modules-76xxv9fs.js} +2 -2
- package/dist/{work-objects-0wr8zxct.js → work-objects-4n21tfkm.js} +20 -19
- package/dist/{worker-follow-up-ktyyazyp.js → worker-follow-up-77hvzxww.js} +17 -16
- package/dist/{worker-0c5tyzqe.js → worker-gg1576j3.js} +31 -30
- package/openapi.json +93 -1
- package/package.json +8 -7
- package/src/be/db/script-runs.ts +23 -0
- package/src/be/db/tasks/read.ts +122 -3
- package/src/be/db-client.ts +8 -5
- package/src/be/db.ts +15 -4
- package/src/be/memory/providers/sqlite-store.ts +49 -12
- 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/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 +22 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
- package/src/commands/controller.ts +4 -0
- package/src/commands/entrypoint-api-readiness.ts +74 -0
- package/src/http/index.ts +1 -1
- package/src/http/list-total-cache.ts +14 -7
- 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/providers/amp-adapter.ts +1 -1
- package/src/providers/fx-adapter.ts +1 -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/script-workflows/workflow-ctx.ts +8 -0
- package/src/scripts-runtime/types/stdlib.d.ts +22 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +22 -0
- package/src/server-user.ts +1 -0
- package/src/slack/commands.ts +5 -1
- package/src/slack/handlers.ts +37 -2
- package/src/slack/left-threads.ts +27 -4
- package/src/tasks/follow-up-redelegation-guard.ts +155 -0
- package/src/tests/claude-artifacts-read-script.test.ts +209 -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 +14 -8
- package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
- package/src/tests/list-total-cache.test.ts +27 -2
- 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/query-audit-plans.test.ts +27 -0
- 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 +11 -0
- package/src/tests/script-runs-http.test.ts +684 -2
- package/src/tests/seed-scripts.test.ts +26 -0
- package/src/tests/slack-leave-thread.test.ts +145 -5
- 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/tools/send-task.ts +35 -47
- package/src/tools/slack-leave-thread.ts +1 -1
- package/src/tools/task-dedup.ts +45 -21
- package/src/types.ts +38 -0
- 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 +1 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -0
- package/templates/skills/swarm-scripts/content.md +2 -0
- package/dist/cli-dznrgaep.js +0 -30
- package/dist/cli-w3hcrxht.js +0 -6985
- package/dist/handlers-vwmba1m2.js +0 -31
- package/dist/index-5r71rf98.js +0 -32
- package/dist/index-anmwxs87.js +0 -29
- package/dist/index-spp0ra5c.js +0 -36
package/src/be/db.ts
CHANGED
|
@@ -313,7 +313,11 @@ export {
|
|
|
313
313
|
getContextVersionHistory,
|
|
314
314
|
getLatestContextVersion,
|
|
315
315
|
} from "./db/context-versions";
|
|
316
|
-
export {
|
|
316
|
+
export {
|
|
317
|
+
getScriptRunExecutionArgs,
|
|
318
|
+
getScriptRunFollowUpGuard,
|
|
319
|
+
setScriptRunFollowUpGuard,
|
|
320
|
+
} from "./db/script-runs";
|
|
317
321
|
export {
|
|
318
322
|
AssetKeyMovesBlockedError,
|
|
319
323
|
assertAssetKeyMovesAllowed,
|
|
@@ -336,12 +340,14 @@ export {
|
|
|
336
340
|
} from "./db/runtime";
|
|
337
341
|
|
|
338
342
|
export {
|
|
343
|
+
countAgentDispatchesInThread,
|
|
339
344
|
type ExistingTrackerContextWork,
|
|
340
345
|
type ExistingTrackerContextWorkReason,
|
|
341
346
|
findActiveTasksByVcs,
|
|
342
|
-
|
|
347
|
+
findCompletedTasksInThread,
|
|
343
348
|
findExistingLinearTrackerContextWork,
|
|
344
349
|
findRecentCancelledTaskInThread,
|
|
350
|
+
findRecentFailedTaskInThread,
|
|
345
351
|
findTaskByGitHub,
|
|
346
352
|
findTaskByVcs,
|
|
347
353
|
getAgentCurrentTask,
|
|
@@ -14116,6 +14122,7 @@ type ScriptRunRow = {
|
|
|
14116
14122
|
last_heartbeat_at: string | null;
|
|
14117
14123
|
idempotencyKey: string | null;
|
|
14118
14124
|
requestedByUserId: string | null;
|
|
14125
|
+
sourceTaskId: string | null;
|
|
14119
14126
|
created_by: string | null;
|
|
14120
14127
|
updated_by: string | null;
|
|
14121
14128
|
};
|
|
@@ -14160,6 +14167,7 @@ function rowToScriptRun(row: ScriptRunRow): ScriptRun {
|
|
|
14160
14167
|
lastHeartbeatAt: row.last_heartbeat_at ?? undefined,
|
|
14161
14168
|
idempotencyKey: row.idempotencyKey ?? undefined,
|
|
14162
14169
|
requestedByUserId: row.requestedByUserId ?? undefined,
|
|
14170
|
+
sourceTaskId: row.sourceTaskId ?? undefined,
|
|
14163
14171
|
};
|
|
14164
14172
|
}
|
|
14165
14173
|
|
|
@@ -14188,6 +14196,7 @@ export async function createScriptRun(data: {
|
|
|
14188
14196
|
scriptName?: string;
|
|
14189
14197
|
idempotencyKey?: string;
|
|
14190
14198
|
requestedByUserId?: string;
|
|
14199
|
+
sourceTaskId?: string;
|
|
14191
14200
|
createdBy?: string;
|
|
14192
14201
|
updatedBy?: string;
|
|
14193
14202
|
}): Promise<{ run: ScriptRun; existing: boolean }> {
|
|
@@ -14202,8 +14211,9 @@ export async function createScriptRun(data: {
|
|
|
14202
14211
|
|
|
14203
14212
|
const row = await client.get<ScriptRunRow>(
|
|
14204
14213
|
`INSERT INTO script_runs
|
|
14205
|
-
(id, agentId, scriptName, source, args, idempotencyKey, requestedByUserId,
|
|
14206
|
-
|
|
14214
|
+
(id, agentId, scriptName, source, args, idempotencyKey, requestedByUserId, sourceTaskId,
|
|
14215
|
+
created_by, updated_by)
|
|
14216
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
14207
14217
|
RETURNING *`,
|
|
14208
14218
|
[
|
|
14209
14219
|
data.id,
|
|
@@ -14216,6 +14226,7 @@ export async function createScriptRun(data: {
|
|
|
14216
14226
|
sealJson(data.args ?? null),
|
|
14217
14227
|
data.idempotencyKey ?? null,
|
|
14218
14228
|
data.requestedByUserId ?? null,
|
|
14229
|
+
data.sourceTaskId ?? null,
|
|
14219
14230
|
data.createdBy ?? null,
|
|
14220
14231
|
data.updatedBy ?? data.createdBy ?? null,
|
|
14221
14232
|
],
|
|
@@ -45,14 +45,18 @@ function armFetchLimit(limit: number): number {
|
|
|
45
45
|
// lookup against it is a full table scan per outer row. The old backfill used
|
|
46
46
|
// `WHERE NOT EXISTS (SELECT 1 FROM memory_fts f WHERE f.memory_id = m.id)`,
|
|
47
47
|
// which is N x N: at ~18k memories it held the main connection for minutes at
|
|
48
|
-
// boot.
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
// boot. The diff below reads each table once, in rowid batches (FTS5 seeks a
|
|
49
|
+
// rowid range), and yields the event loop between batches: one whole-table
|
|
50
|
+
// statement held the loop for seconds on a large DB. A memory_fts row is an
|
|
51
|
+
// orphan when its memory is gone; that check is a lookup on the agent_memory
|
|
52
|
+
// primary key. A NULL memory_id is never an orphan, as with the old NOT IN.
|
|
53
|
+
export const FTS_SCAN_BATCH_SQL = `SELECT rowid AS rowid, memory_id AS memoryId,
|
|
54
|
+
memory_id IS NOT NULL
|
|
55
|
+
AND NOT EXISTS (SELECT 1 FROM agent_memory m WHERE m.id = memory_fts.memory_id) AS orphan
|
|
56
|
+
FROM memory_fts WHERE rowid > ? ORDER BY rowid LIMIT ?`;
|
|
57
|
+
export const MEMORY_ID_BATCH_SQL = `SELECT rowid AS rowid, id FROM agent_memory
|
|
58
|
+
WHERE rowid > ? ORDER BY rowid LIMIT ?`;
|
|
59
|
+
const FTS_SCAN_BATCH_SIZE = 1000;
|
|
56
60
|
const FTS_POPULATE_BATCH_SIZE = 500;
|
|
57
61
|
|
|
58
62
|
export type AgentMemoryRow = {
|
|
@@ -251,12 +255,45 @@ export class SqliteMemoryStore implements MemoryStore {
|
|
|
251
255
|
private async populateFtsTable(): Promise<void> {
|
|
252
256
|
const client = getDbClient();
|
|
253
257
|
const startedAt = performance.now();
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
258
|
+
const yieldLoop = () => new Promise<void>((resolve) => setImmediate(resolve));
|
|
259
|
+
|
|
260
|
+
// Drop orphan rows and collect the memory ids the index holds.
|
|
261
|
+
const indexed = new Set<string>();
|
|
262
|
+
let removed = 0;
|
|
263
|
+
for (let after = 0; ; ) {
|
|
264
|
+
const rows = await client.query<{ rowid: number; memoryId: string | null; orphan: number }>(
|
|
265
|
+
FTS_SCAN_BATCH_SQL,
|
|
266
|
+
[after, FTS_SCAN_BATCH_SIZE],
|
|
267
|
+
);
|
|
268
|
+
if (rows.length === 0) break;
|
|
269
|
+
after = rows[rows.length - 1]!.rowid;
|
|
270
|
+
const orphans = rows.filter((row) => row.orphan).map((row) => row.rowid);
|
|
271
|
+
if (orphans.length > 0) {
|
|
272
|
+
await client.run(
|
|
273
|
+
`DELETE FROM memory_fts WHERE rowid IN (${orphans.map(() => "?").join(",")})`,
|
|
274
|
+
orphans,
|
|
275
|
+
);
|
|
276
|
+
removed += orphans.length;
|
|
277
|
+
}
|
|
278
|
+
for (const row of rows) if (!row.orphan && row.memoryId) indexed.add(row.memoryId);
|
|
279
|
+
await yieldLoop();
|
|
280
|
+
}
|
|
281
|
+
if (removed > 0) {
|
|
282
|
+
console.warn(`[memory-fts] removed_extra_rows count=${removed}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const missing: { id: string }[] = [];
|
|
286
|
+
for (let after = 0; ; ) {
|
|
287
|
+
const rows = await client.query<{ rowid: number; id: string }>(MEMORY_ID_BATCH_SQL, [
|
|
288
|
+
after,
|
|
289
|
+
FTS_SCAN_BATCH_SIZE,
|
|
290
|
+
]);
|
|
291
|
+
if (rows.length === 0) break;
|
|
292
|
+
after = rows[rows.length - 1]!.rowid;
|
|
293
|
+
for (const row of rows) if (!indexed.has(row.id)) missing.push(row);
|
|
294
|
+
await yieldLoop();
|
|
257
295
|
}
|
|
258
296
|
|
|
259
|
-
const missing = await client.query<{ id: string }>(FTS_MISSING_IDS_SQL);
|
|
260
297
|
for (let i = 0; i < missing.length; i += FTS_POPULATE_BATCH_SIZE) {
|
|
261
298
|
const ids = missing.slice(i, i + FTS_POPULATE_BATCH_SIZE).map((row) => row.id);
|
|
262
299
|
const placeholders = ids.map(() => "?").join(",");
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- NULL preserves the ephemeral-only behavior of existing pools.
|
|
2
|
+
ALTER TABLE runtime_pools ADD COLUMN cache TEXT
|
|
3
|
+
CHECK (cache IS NULL OR json_valid(cache));
|
|
4
|
+
|
|
5
|
+
-- Retain release intent across API restarts and pool deletion.
|
|
6
|
+
CREATE TABLE runtime_cache_releases (
|
|
7
|
+
id TEXT PRIMARY KEY,
|
|
8
|
+
pool_id TEXT NOT NULL,
|
|
9
|
+
controller_id TEXT NOT NULL REFERENCES runtime_controllers(id) ON DELETE RESTRICT,
|
|
10
|
+
owner_id TEXT NOT NULL,
|
|
11
|
+
cache_key TEXT NOT NULL,
|
|
12
|
+
runtime_ids TEXT NOT NULL CHECK (json_valid(runtime_ids) AND json_type(runtime_ids) = 'array'),
|
|
13
|
+
state TEXT NOT NULL DEFAULT 'pending' CHECK (state IN ('pending', 'deleting')),
|
|
14
|
+
last_error TEXT,
|
|
15
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
16
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
17
|
+
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
18
|
+
updated_by TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
19
|
+
UNIQUE(controller_id, owner_id, cache_key)
|
|
20
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Covering indexes for the session visibility walk (readableSessionCondition
|
|
2
|
+
-- in src/be/db/tasks/read.ts). Every task list and count a dashboard user
|
|
3
|
+
-- loads computes the set of tasks in sessions the user cannot read: roots
|
|
4
|
+
-- owned by someone else, plus all their descendants.
|
|
5
|
+
--
|
|
6
|
+
-- agent_tasks rows carry large `task` text, and parentTaskId, scheduleId and
|
|
7
|
+
-- requestedByUserId sit after it, in the overflow pages. Without these
|
|
8
|
+
-- indexes the root step read every unscheduled task row and the recursive
|
|
9
|
+
-- step read the row of every descendant. With them, both steps stay in the
|
|
10
|
+
-- index. Production has no sqlite_stat1; the planner picks both without it.
|
|
11
|
+
|
|
12
|
+
-- Root step: `scheduleId IS NULL` plus the root, owner and id columns.
|
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_session_owner
|
|
14
|
+
ON agent_tasks(scheduleId, parentTaskId, requestedByUserId, id);
|
|
15
|
+
|
|
16
|
+
-- Recursive step: the children of a hidden task, by id.
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_parentTaskId_id
|
|
18
|
+
ON agent_tasks(parentTaskId, id);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- Covering index for the default task list count (getTasksCount in
|
|
2
|
+
-- src/be/db/tasks/read.ts). The task list hides heartbeat tasks by default:
|
|
3
|
+
--
|
|
4
|
+
-- IFNULL(taskType, '') NOT IN (...) AND tags NOT LIKE '%"heartbeat"%'
|
|
5
|
+
--
|
|
6
|
+
-- A NOT IN and a NOT LIKE cannot seek, so the count scans. taskType and tags
|
|
7
|
+
-- sit after the large `task` text, in the overflow pages, so the scan read
|
|
8
|
+
-- every task body. This index holds the two filter columns and `id` (the
|
|
9
|
+
-- session visibility filter), so the count scans the index only.
|
|
10
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_list_filter
|
|
11
|
+
ON agent_tasks(taskType, tags, id);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Covering index for the Slack render-v2 watcher sweep (getSlackTreeMessages
|
|
2
|
+
-- in src/be/db.ts), which runs on every watcher tick, even when idle. For
|
|
3
|
+
-- each stored tree it tests the thread's tasks by createdAt, status, source
|
|
4
|
+
-- and lastUpdatedAt. Those columns sit after the large `task` text, in the
|
|
5
|
+
-- overflow pages, so each test read whole task rows. This index holds them,
|
|
6
|
+
-- so the tests stay in the index. Partial: only Slack tasks carry a channel.
|
|
7
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_slack_thread_cover
|
|
8
|
+
ON agent_tasks(slackChannelId, slackThreadTs, createdAt, status, source, lastUpdatedAt, id)
|
|
9
|
+
WHERE slackChannelId IS NOT NULL;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- The task whose session launched a durable script run (the caller's
|
|
2
|
+
-- X-Source-Task-Id, kept only when that task belongs to the launching agent).
|
|
3
|
+
-- The follow-up re-delegation guard reads it when the run's ctx.step.agentTask
|
|
4
|
+
-- dispatches, the same way send-task reads the caller's source task.
|
|
5
|
+
ALTER TABLE script_runs ADD COLUMN sourceTaskId TEXT;
|
|
6
|
+
|
|
7
|
+
-- The run's follow-up re-delegation guard decision, taken once for the whole
|
|
8
|
+
-- run at its first new step: NULL (not decided yet), 'passed', or the rule that
|
|
9
|
+
-- blocked it ('same-work' or 'loop-cap'). Every later step follows it, so
|
|
10
|
+
-- parallel steps of one run are dispatched all or none.
|
|
11
|
+
ALTER TABLE script_runs ADD COLUMN followUpGuard TEXT;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { ControllerCommands } from "../../runtime-providers/controller";
|
|
2
|
+
import { poolCacheKey, type RuntimeCacheReference } from "../../runtime-providers/types";
|
|
3
|
+
import type { RuntimePool } from "../../types";
|
|
4
|
+
import { scrubSecrets } from "../../utils/secret-scrubber";
|
|
5
|
+
import { getDbClient, getSwarmConfigs } from "../db";
|
|
6
|
+
|
|
7
|
+
/** The stored key is private API metadata; legacy caches retain their original name. */
|
|
8
|
+
export async function getPoolCache(poolId: string): Promise<{
|
|
9
|
+
key: string;
|
|
10
|
+
mountPath: string;
|
|
11
|
+
controllerId: string;
|
|
12
|
+
} | null> {
|
|
13
|
+
const row = await getDbClient().get<{ cache: string | null; controllerId: string }>(
|
|
14
|
+
"SELECT cache, json_extract(placements, '$[0]') AS controllerId FROM runtime_pools WHERE id = ?",
|
|
15
|
+
[poolId],
|
|
16
|
+
);
|
|
17
|
+
if (!row?.cache) return null;
|
|
18
|
+
const cache = JSON.parse(row.cache);
|
|
19
|
+
return {
|
|
20
|
+
key: cache.key ?? poolCacheKey(poolId),
|
|
21
|
+
mountPath: cache.mountPath,
|
|
22
|
+
controllerId: row.controllerId,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Call in the pool write transaction; no infrastructure calls occur here. */
|
|
27
|
+
export async function queuePoolCacheRelease(
|
|
28
|
+
pool: RuntimePool,
|
|
29
|
+
actor: string | null,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
if (!pool.cache) return;
|
|
32
|
+
const owner = (await getSwarmConfigs({ scope: "global", key: "runtime_owner_id" }))[0];
|
|
33
|
+
// No runtime could have created an owned cache before the owner was allocated.
|
|
34
|
+
if (!owner) return;
|
|
35
|
+
const cache = (await getPoolCache(pool.id))!;
|
|
36
|
+
const runtimes = await getDbClient().query<{ id: string }>(
|
|
37
|
+
`SELECT id FROM runtime_instances WHERE pool_id = ?
|
|
38
|
+
AND (controller_id = ? OR controller_id IS NULL) AND status != 'offline'`,
|
|
39
|
+
[pool.id, pool.placements[0]!],
|
|
40
|
+
);
|
|
41
|
+
await getDbClient().run(
|
|
42
|
+
`INSERT INTO runtime_cache_releases
|
|
43
|
+
(id, pool_id, controller_id, owner_id, cache_key, runtime_ids, created_by, updated_by)
|
|
44
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(controller_id, owner_id, cache_key) DO NOTHING`,
|
|
45
|
+
[
|
|
46
|
+
crypto.randomUUID(),
|
|
47
|
+
pool.id,
|
|
48
|
+
pool.placements[0]!,
|
|
49
|
+
owner.value,
|
|
50
|
+
cache.key,
|
|
51
|
+
JSON.stringify(runtimes.map((r) => r.id)),
|
|
52
|
+
actor,
|
|
53
|
+
actor,
|
|
54
|
+
],
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface CacheRelease extends RuntimeCacheReference {
|
|
59
|
+
id: string;
|
|
60
|
+
controllerId: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const RELEASE_LEASE_MS = 5 * 60_000;
|
|
64
|
+
|
|
65
|
+
/** Claims are atomic across reconcilers; all network calls are outside DB transactions. */
|
|
66
|
+
export async function reconcileCacheReleases(
|
|
67
|
+
commands: (id: string) => Promise<ControllerCommands>,
|
|
68
|
+
signal: AbortSignal,
|
|
69
|
+
now?: Date,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
const jobs = await getDbClient().query<CacheRelease>(
|
|
72
|
+
`SELECT id, pool_id AS poolId, controller_id AS controllerId, owner_id AS ownerId,
|
|
73
|
+
cache_key AS key FROM runtime_cache_releases ORDER BY created_at`,
|
|
74
|
+
);
|
|
75
|
+
for (const job of jobs) {
|
|
76
|
+
if (signal.aborted) return;
|
|
77
|
+
const claimed = now ?? new Date();
|
|
78
|
+
const claimedAt = claimed.toISOString();
|
|
79
|
+
// Reclaim abandoned work after five minutes; generation keys fence late provider calls.
|
|
80
|
+
const reserved = await getDbClient().run(
|
|
81
|
+
`UPDATE runtime_cache_releases SET state = 'deleting', updated_at = ?
|
|
82
|
+
WHERE id = ? AND (state = 'pending' OR updated_at < ?) AND NOT EXISTS (
|
|
83
|
+
SELECT 1 FROM runtime_instances WHERE status != 'offline'
|
|
84
|
+
AND id IN (SELECT value FROM json_each(runtime_cache_releases.runtime_ids))
|
|
85
|
+
)`,
|
|
86
|
+
[claimedAt, job.id, new Date(claimed.getTime() - RELEASE_LEASE_MS).toISOString()],
|
|
87
|
+
);
|
|
88
|
+
if (!reserved.changes) continue;
|
|
89
|
+
try {
|
|
90
|
+
signal.throwIfAborted();
|
|
91
|
+
const controller = await commands(job.controllerId);
|
|
92
|
+
if (!controller.releaseCache)
|
|
93
|
+
throw new Error("Controller cannot release caches; upgrade it before cleanup");
|
|
94
|
+
await controller.releaseCache({ key: job.key, poolId: job.poolId, ownerId: job.ownerId });
|
|
95
|
+
await getDbClient().run(
|
|
96
|
+
"DELETE FROM runtime_cache_releases WHERE id = ? AND state = 'deleting' AND updated_at = ?",
|
|
97
|
+
[job.id, claimedAt],
|
|
98
|
+
);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const message = scrubSecrets(String(error));
|
|
101
|
+
await getDbClient().run(
|
|
102
|
+
`UPDATE runtime_cache_releases SET state = 'pending', last_error = ?, updated_at = ?
|
|
103
|
+
WHERE id = ? AND state = 'deleting' AND updated_at = ?`,
|
|
104
|
+
[message, new Date().toISOString(), job.id, claimedAt],
|
|
105
|
+
);
|
|
106
|
+
if (!signal.aborted) console.error("[runtime-pools] Cache cleanup deferred:", message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createRuntimeProvider,
|
|
5
5
|
type RuntimeTargetConfig,
|
|
6
6
|
} from "../../runtime-providers";
|
|
7
|
+
import { poolCacheKey } from "../../runtime-providers/types";
|
|
7
8
|
import type { Agent, DeliveryMode, RuntimeController, RuntimePool } from "../../types";
|
|
8
9
|
import {
|
|
9
10
|
RuntimeControllerCreateSchema,
|
|
@@ -17,6 +18,7 @@ import { decryptSecret, encryptSecret, getEncryptionKey } from "../crypto";
|
|
|
17
18
|
import { createAgent, getAgentById, getAllAgents, getDbClient, upsertSwarmConfig } from "../db";
|
|
18
19
|
import { type AgentRow, rowToAgent } from "../db/agents";
|
|
19
20
|
import { registerStoredSecret } from "../secret-registry";
|
|
21
|
+
import { getPoolCache, queuePoolCacheRelease } from "./cache-cleanup";
|
|
20
22
|
import { RuntimePoolError } from "./errors";
|
|
21
23
|
import { revokeControllerTokens } from "./tokens";
|
|
22
24
|
|
|
@@ -29,10 +31,11 @@ type ControllerRow = Omit<
|
|
|
29
31
|
config: string;
|
|
30
32
|
secretConfig: string | null;
|
|
31
33
|
};
|
|
32
|
-
type PoolRow = Omit<RuntimePool, "placements" | "env" | "resources" | "enabled"> & {
|
|
34
|
+
type PoolRow = Omit<RuntimePool, "placements" | "env" | "resources" | "cache" | "enabled"> & {
|
|
33
35
|
placements: string;
|
|
34
36
|
env: string;
|
|
35
37
|
resources: string | null;
|
|
38
|
+
cache: string | null;
|
|
36
39
|
enabled: number;
|
|
37
40
|
};
|
|
38
41
|
const controllerColumns = `id, name, kind, placement, config, secret_config AS secretConfig,
|
|
@@ -40,7 +43,7 @@ const controllerColumns = `id, name, kind, placement, config, secret_config AS s
|
|
|
40
43
|
created_by AS createdBy, updated_by AS updatedBy, created_at AS createdAt, updated_at AS updatedAt`;
|
|
41
44
|
const poolColumns = `id, name, kind, placements, image, lifecycle, idle_minutes AS idleMinutes,
|
|
42
45
|
slots_per_runtime AS slotsPerRuntime, max_runtimes AS maxRuntimes, min_runtimes AS minRuntimes,
|
|
43
|
-
delivery, env, resources, ttl_sec AS ttlSec, resolved_image AS resolvedImage, enabled,
|
|
46
|
+
delivery, env, resources, cache, ttl_sec AS ttlSec, resolved_image AS resolvedImage, enabled,
|
|
44
47
|
consecutive_failures AS consecutiveFailures, circuit_open_until AS circuitOpenUntil,
|
|
45
48
|
last_error AS lastError, last_error_at AS lastErrorAt, created_by AS createdBy,
|
|
46
49
|
updated_by AS updatedBy, created_at AS createdAt, updated_at AS updatedAt`;
|
|
@@ -65,11 +68,13 @@ function rowToController(row: ControllerRow): RuntimeController {
|
|
|
65
68
|
};
|
|
66
69
|
}
|
|
67
70
|
function rowToPool(row: PoolRow): RuntimePool {
|
|
71
|
+
const cache = row.cache ? JSON.parse(row.cache) : null;
|
|
68
72
|
return {
|
|
69
73
|
...row,
|
|
70
74
|
placements: JSON.parse(row.placements),
|
|
71
75
|
env: JSON.parse(row.env),
|
|
72
76
|
resources: row.resources ? JSON.parse(row.resources) : null,
|
|
77
|
+
cache: cache ? { mountPath: cache.mountPath } : null,
|
|
73
78
|
enabled: row.enabled === 1,
|
|
74
79
|
};
|
|
75
80
|
}
|
|
@@ -121,6 +126,17 @@ export async function getRuntimeControllerTarget(
|
|
|
121
126
|
E2B_MAX_CONTINUOUS_SEC: env.E2B_MAX_CONTINUOUS_SEC,
|
|
122
127
|
RUNTIME_WORKER_API_URL: env.RUNTIME_WORKER_API_URL,
|
|
123
128
|
PUBLIC_MCP_BASE_URL: env.PUBLIC_MCP_BASE_URL,
|
|
129
|
+
...(controller.kind === "boxd" && controller.placement === "builtin"
|
|
130
|
+
? {
|
|
131
|
+
BOXD_ORG: env.BOXD_ORG,
|
|
132
|
+
BOXD_BASE_URL: env.BOXD_BASE_URL,
|
|
133
|
+
BOXD_SNAPSHOT: env.BOXD_SNAPSHOT,
|
|
134
|
+
BOXD_DELETE_AFTER_SEC: env.BOXD_DELETE_AFTER_SEC,
|
|
135
|
+
BOXD_START_TIMEOUT_SEC: env.BOXD_START_TIMEOUT_SEC,
|
|
136
|
+
BOXD_STATUS_TIMEOUT_SEC: env.BOXD_STATUS_TIMEOUT_SEC,
|
|
137
|
+
BOXD_TEARDOWN_TIMEOUT_SEC: env.BOXD_TEARDOWN_TIMEOUT_SEC,
|
|
138
|
+
}
|
|
139
|
+
: {}),
|
|
124
140
|
...controller.config,
|
|
125
141
|
...secrets,
|
|
126
142
|
};
|
|
@@ -234,11 +250,32 @@ export async function updateRuntimeController(
|
|
|
234
250
|
)
|
|
235
251
|
throw new RuntimePoolError("Controller name already exists", 409);
|
|
236
252
|
const pools = await listRuntimePools();
|
|
253
|
+
const oldConfig: Record<string, string> = JSON.parse(row.config);
|
|
254
|
+
const configChanged =
|
|
255
|
+
Object.keys(data.config).length !== Object.keys(oldConfig).length ||
|
|
256
|
+
Object.entries(data.config).some(([key, value]) => oldConfig[key] !== value);
|
|
257
|
+
const secrets = patch.secretConfig !== undefined ? controllerSecrets(row) : {};
|
|
258
|
+
const secretsChanged = Object.entries(patch.secretConfig ?? {}).some(([key, value]) =>
|
|
259
|
+
value === null ? key in secrets : secrets[key] !== value,
|
|
260
|
+
);
|
|
261
|
+
if (
|
|
262
|
+
(data.kind !== row.kind ||
|
|
263
|
+
data.placement !== row.placement ||
|
|
264
|
+
configChanged ||
|
|
265
|
+
secretsChanged) &&
|
|
266
|
+
(pools.some((pool) => pool.cache && pool.placements.includes(id)) ||
|
|
267
|
+
(await tx.get("SELECT id FROM runtime_cache_releases WHERE controller_id = ? LIMIT 1", [
|
|
268
|
+
id,
|
|
269
|
+
])))
|
|
270
|
+
)
|
|
271
|
+
throw new RuntimePoolError(
|
|
272
|
+
"Disable pool caches and wait for cleanup before changing controller configuration or placement",
|
|
273
|
+
409,
|
|
274
|
+
);
|
|
237
275
|
if (pools.some((pool) => pool.placements.includes(id) && pool.kind !== data.kind))
|
|
238
276
|
throw new RuntimePoolError("Pool kind must equal controller kind");
|
|
239
277
|
let encrypted = row.secretConfig;
|
|
240
278
|
if (patch.secretConfig !== undefined) {
|
|
241
|
-
const secrets = controllerSecrets(row);
|
|
242
279
|
for (const [key, value] of Object.entries(patch.secretConfig)) {
|
|
243
280
|
if (value === null) delete secrets[key];
|
|
244
281
|
else secrets[key] = value;
|
|
@@ -272,6 +309,8 @@ export async function deleteRuntimeController(id: string): Promise<void> {
|
|
|
272
309
|
if (!(await getRuntimeController(id))) throw new RuntimePoolError("Controller not found", 404);
|
|
273
310
|
if ((await listRuntimePools()).some((pool) => pool.placements.includes(id)))
|
|
274
311
|
throw new RuntimePoolError("Controller has runtime pools", 409);
|
|
312
|
+
if (await tx.get("SELECT id FROM runtime_cache_releases WHERE controller_id = ? LIMIT 1", [id]))
|
|
313
|
+
throw new RuntimePoolError("Controller has pending cache cleanup", 409);
|
|
275
314
|
await revokeControllerTokens(id);
|
|
276
315
|
await tx.run("DELETE FROM runtime_controllers WHERE id = ?", [id]);
|
|
277
316
|
});
|
|
@@ -291,6 +330,9 @@ export async function getRuntimePool(id: string): Promise<RuntimePool | null> {
|
|
|
291
330
|
}
|
|
292
331
|
async function validatePool(data: z.output<typeof RuntimePoolCreateSchema>): Promise<void> {
|
|
293
332
|
supportedKind(data.kind);
|
|
333
|
+
if (data.cache && !(await createRuntimeProvider(data.kind)).traits.persistentCache) {
|
|
334
|
+
throw new RuntimePoolError(`${data.kind} does not support persistent pool caches`);
|
|
335
|
+
}
|
|
294
336
|
const controller = await getRuntimeController(data.placements[0]!);
|
|
295
337
|
if (!controller) throw new RuntimePoolError("Controller not found");
|
|
296
338
|
if (data.kind !== controller.kind)
|
|
@@ -316,7 +358,7 @@ export async function createRuntimePool(
|
|
|
316
358
|
if (await tx.get("SELECT id FROM runtime_pools WHERE name = ? OR id = ?", [data.name, id]))
|
|
317
359
|
throw new RuntimePoolError("Pool already exists", 409);
|
|
318
360
|
await tx.run(
|
|
319
|
-
`INSERT INTO runtime_pools (id, name, kind, placements, image, lifecycle, idle_minutes, slots_per_runtime, max_runtimes, min_runtimes, delivery, env, resources, ttl_sec, enabled, created_by, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
361
|
+
`INSERT INTO runtime_pools (id, name, kind, placements, image, lifecycle, idle_minutes, slots_per_runtime, max_runtimes, min_runtimes, delivery, env, resources, cache, ttl_sec, enabled, created_by, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
320
362
|
[
|
|
321
363
|
id,
|
|
322
364
|
data.name,
|
|
@@ -331,6 +373,9 @@ export async function createRuntimePool(
|
|
|
331
373
|
data.delivery,
|
|
332
374
|
JSON.stringify(data.env),
|
|
333
375
|
data.resources ? JSON.stringify(data.resources) : null,
|
|
376
|
+
data.cache
|
|
377
|
+
? JSON.stringify({ ...data.cache, key: poolCacheKey(id, crypto.randomUUID()) })
|
|
378
|
+
: null,
|
|
334
379
|
data.ttlSec,
|
|
335
380
|
Number(data.enabled),
|
|
336
381
|
actor,
|
|
@@ -364,8 +409,19 @@ export async function updateRuntimePool(
|
|
|
364
409
|
await validatePool(data);
|
|
365
410
|
if (await tx.get("SELECT id FROM runtime_pools WHERE name = ? AND id != ?", [data.name, id]))
|
|
366
411
|
throw new RuntimePoolError("Pool name already exists", 409);
|
|
412
|
+
const cache = data.cache
|
|
413
|
+
? {
|
|
414
|
+
...data.cache,
|
|
415
|
+
key:
|
|
416
|
+
existing.cache && data.placements[0] === existing.placements[0]
|
|
417
|
+
? (await getPoolCache(id))!.key
|
|
418
|
+
: poolCacheKey(id, crypto.randomUUID()),
|
|
419
|
+
}
|
|
420
|
+
: null;
|
|
421
|
+
if (existing.cache && (!data.cache || data.placements[0] !== existing.placements[0]))
|
|
422
|
+
await queuePoolCacheRelease(existing, actor);
|
|
367
423
|
await tx.run(
|
|
368
|
-
`UPDATE runtime_pools SET name = ?, kind = ?, placements = ?, image = ?, resolved_image = ?, lifecycle = ?, idle_minutes = ?, slots_per_runtime = ?, max_runtimes = ?, min_runtimes = ?, delivery = ?, env = ?, resources = ?, ttl_sec = ?, enabled = ?, updated_by = COALESCE(?, updated_by), updated_at = ? WHERE id = ?`,
|
|
424
|
+
`UPDATE runtime_pools SET name = ?, kind = ?, placements = ?, image = ?, resolved_image = ?, lifecycle = ?, idle_minutes = ?, slots_per_runtime = ?, max_runtimes = ?, min_runtimes = ?, delivery = ?, env = ?, resources = ?, cache = ?, ttl_sec = ?, enabled = ?, updated_by = COALESCE(?, updated_by), updated_at = ? WHERE id = ?`,
|
|
369
425
|
[
|
|
370
426
|
data.name,
|
|
371
427
|
data.kind,
|
|
@@ -384,6 +440,7 @@ export async function updateRuntimePool(
|
|
|
384
440
|
data.delivery,
|
|
385
441
|
JSON.stringify(data.env),
|
|
386
442
|
data.resources ? JSON.stringify(data.resources) : null,
|
|
443
|
+
cache ? JSON.stringify(cache) : null,
|
|
387
444
|
data.ttlSec,
|
|
388
445
|
Number(data.enabled),
|
|
389
446
|
actor,
|
|
@@ -400,7 +457,8 @@ export async function deleteRuntimePool(
|
|
|
400
457
|
actor: string | null = null,
|
|
401
458
|
): Promise<void> {
|
|
402
459
|
await getDbClient().transaction(async (tx) => {
|
|
403
|
-
|
|
460
|
+
const pool = await getRuntimePool(id);
|
|
461
|
+
if (!pool) throw new RuntimePoolError("Pool not found", 404);
|
|
404
462
|
if (
|
|
405
463
|
await tx.get(
|
|
406
464
|
"SELECT id FROM runtime_instances WHERE pool_id = ? AND status != 'offline' LIMIT 1",
|
|
@@ -419,6 +477,7 @@ export async function deleteRuntimePool(
|
|
|
419
477
|
[new Date().toISOString(), actor, id],
|
|
420
478
|
);
|
|
421
479
|
}
|
|
480
|
+
await queuePoolCacheRelease(pool, actor);
|
|
422
481
|
await tx.run("DELETE FROM runtime_pools WHERE id = ?", [id]);
|
|
423
482
|
});
|
|
424
483
|
}
|
|
@@ -53,6 +53,7 @@ export class RemoteController implements ControllerCommands {
|
|
|
53
53
|
readonly renew?: ControllerCommands["renew"];
|
|
54
54
|
readonly pause?: ControllerCommands["pause"];
|
|
55
55
|
readonly resume?: ControllerCommands["resume"];
|
|
56
|
+
readonly releaseCache?: ControllerCommands["releaseCache"];
|
|
56
57
|
|
|
57
58
|
constructor(
|
|
58
59
|
readonly id: string,
|
|
@@ -61,6 +62,10 @@ export class RemoteController implements ControllerCommands {
|
|
|
61
62
|
readonly workerApiUrl?: string,
|
|
62
63
|
private readonly timeouts: { commandTimeoutMs?: number; provisionTimeoutMs?: number } = {},
|
|
63
64
|
) {
|
|
65
|
+
if (traits.persistentCache)
|
|
66
|
+
this.releaseCache = async (cache) => {
|
|
67
|
+
await this.call({ op: "release-cache", payload: cache });
|
|
68
|
+
};
|
|
64
69
|
if (traits.maxTtlSec)
|
|
65
70
|
this.renew = async (handle, ttlSec) =>
|
|
66
71
|
z
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
runtimeStaleThresholdMinutes,
|
|
30
30
|
} from "../multi-runtime";
|
|
31
31
|
import { mintRuntimeToken } from "../users";
|
|
32
|
+
import { getPoolCache, reconcileCacheReleases } from "./cache-cleanup";
|
|
32
33
|
import {
|
|
33
34
|
clearRuntimeOwnerIdCache,
|
|
34
35
|
ensureRuntimeOwnerId,
|
|
@@ -245,6 +246,13 @@ export async function provisionRuntime(
|
|
|
245
246
|
if (pool.lifecycle === "per-task") env.SWARM_EXIT_AFTER_TASKS = "1";
|
|
246
247
|
if (templateId) env.TEMPLATE_ID = templateId;
|
|
247
248
|
const ttlSec = pool.kind === "e2b" ? poolLeaseTtl(pool, controller) : undefined;
|
|
249
|
+
const currentPoolCache = await getPoolCache(pool.id);
|
|
250
|
+
const currentCache =
|
|
251
|
+
currentPoolCache?.controllerId === pool.placements[0] ? currentPoolCache : null;
|
|
252
|
+
const cache =
|
|
253
|
+
currentCache && controller.traits.persistentCache
|
|
254
|
+
? { key: currentCache.key, mountPath: currentCache.mountPath }
|
|
255
|
+
: undefined;
|
|
248
256
|
signal?.throwIfAborted();
|
|
249
257
|
const handle = await controller.provision({
|
|
250
258
|
runtimeId,
|
|
@@ -255,6 +263,7 @@ export async function provisionRuntime(
|
|
|
255
263
|
...(ttlSec === undefined ? {} : { ttlSec }),
|
|
256
264
|
env,
|
|
257
265
|
resources: pool.resources ?? undefined,
|
|
266
|
+
...(cache ? { cache } : {}),
|
|
258
267
|
labels: {
|
|
259
268
|
[RUNTIME_LABELS.runtimeId]: runtimeId,
|
|
260
269
|
[RUNTIME_LABELS.poolId]: pool.id,
|
|
@@ -267,6 +276,25 @@ export async function provisionRuntime(
|
|
|
267
276
|
await markRuntimeTerminated(runtimeId, "Supervisor stopped during provision");
|
|
268
277
|
return false;
|
|
269
278
|
}
|
|
279
|
+
const cacheWarning = currentCache
|
|
280
|
+
? !controller.traits.persistentCache
|
|
281
|
+
? "Controller does not support persistent caches; runtime started without cache"
|
|
282
|
+
: handle.meta?.cacheWarning
|
|
283
|
+
: undefined;
|
|
284
|
+
// A cache is optional: surface degradation without counting a provision failure.
|
|
285
|
+
const cacheErrorPrefix = "Persistent cache: ";
|
|
286
|
+
const cacheError = cacheWarning ? cacheErrorPrefix + scrubSecrets(cacheWarning) : null;
|
|
287
|
+
await getDbClient().run(
|
|
288
|
+
`UPDATE runtime_pools SET last_error = ?, last_error_at = ?
|
|
289
|
+
WHERE id = ? AND (last_error IS NULL OR substr(last_error, 1, ?) = ?)`,
|
|
290
|
+
[
|
|
291
|
+
cacheError,
|
|
292
|
+
cacheError ? new Date().toISOString() : null,
|
|
293
|
+
pool.id,
|
|
294
|
+
cacheErrorPrefix.length,
|
|
295
|
+
cacheErrorPrefix,
|
|
296
|
+
],
|
|
297
|
+
);
|
|
270
298
|
await getDbClient().run(
|
|
271
299
|
"UPDATE runtime_instances SET external_handle = ?, lease_expires_at = ?, created_at = COALESCE(?, created_at), updated_at = ? WHERE id = ?",
|
|
272
300
|
[
|
|
@@ -886,11 +914,14 @@ export async function reconcileOnce(
|
|
|
886
914
|
opts: { now?: Date; getControllerCommands?: typeof getControllerCommands } = {},
|
|
887
915
|
): Promise<void> {
|
|
888
916
|
if (!isRuntimePoolsEnabled() || !isMultiRuntimeEnabled() || inFlight) return;
|
|
889
|
-
|
|
917
|
+
const tick = new AbortController();
|
|
918
|
+
tickAbort = tick;
|
|
890
919
|
inFlight = reconcilePools(
|
|
891
920
|
opts.now ?? new Date(),
|
|
892
921
|
opts.getControllerCommands ?? getControllerCommands,
|
|
893
|
-
|
|
922
|
+
tick.signal,
|
|
923
|
+
).then(() =>
|
|
924
|
+
reconcileCacheReleases(opts.getControllerCommands ?? getControllerCommands, tick.signal),
|
|
894
925
|
);
|
|
895
926
|
try {
|
|
896
927
|
await inFlight;
|
|
@@ -65,6 +65,8 @@ export type AgentTaskStepConfig = {
|
|
|
65
65
|
parentTaskId?: string;
|
|
66
66
|
requestedByUserId?: string;
|
|
67
67
|
outputSchema?: Record<string, unknown>;
|
|
68
|
+
/** Dispatch even when the follow-up re-delegation guard blocks this step (a run launched from a follow-up task in a Slack thread, checked at its first new step). */
|
|
69
|
+
allowRedelegation?: boolean;
|
|
68
70
|
/** Wait for the dispatched task to reach a terminal status before resolving. Default: true. */
|
|
69
71
|
waitForCompletion?: boolean;
|
|
70
72
|
/** Max ms to wait for a terminal status before throwing. Default: 2h. Only used when waitForCompletion is true. */
|
|
@@ -497,6 +499,26 @@ export interface ScriptWorkflowSteps {
|
|
|
497
499
|
label: string,
|
|
498
500
|
config: { prompt: string; model?: string; schema?: Record<string, unknown> },
|
|
499
501
|
): Promise<unknown>;
|
|
502
|
+
/**
|
|
503
|
+
* Dispatch a task and journal its result under \`label\`.
|
|
504
|
+
*
|
|
505
|
+
* Resolves to the task's OUTPUT on success: the stored output string (JSON
|
|
506
|
+
* text under \`outputSchema\`, so parse it yourself), or \`null\` when the task
|
|
507
|
+
* stored none. It is never a task record: on a string, \`result.taskId\` and
|
|
508
|
+
* \`result.status\` are undefined; on \`null\`, reading them throws a TypeError.
|
|
509
|
+
* Other shapes:
|
|
510
|
+
* - \`failOnTaskFailure: false\` and the task ends failed/cancelled/superseded:
|
|
511
|
+
* \`{ taskId, status: "failed", error }\`.
|
|
512
|
+
* - \`waitForCompletion: false\`: the first call still waits up to ~30s while
|
|
513
|
+
* the server polls the task. Done in that window: the output, as on
|
|
514
|
+
* success. Failed or cancelled in that window: throws, or
|
|
515
|
+
* \`{ taskId, status: "failed", error }\` with \`failOnTaskFailure: false\`.
|
|
516
|
+
* Still running: \`{ taskId, status }\`.
|
|
517
|
+
*
|
|
518
|
+
* To get a step's task id, query by its context key:
|
|
519
|
+
* \`SELECT id, status, output FROM agent_tasks WHERE contextKey = 'script-run:<runId>:<label>'
|
|
520
|
+
* AND taskType = 'script-run-step' ORDER BY createdAt DESC, rowid DESC LIMIT 1\`.
|
|
521
|
+
*/
|
|
500
522
|
agentTask(
|
|
501
523
|
label: string,
|
|
502
524
|
config: AgentTaskStepConfig,
|
|
@@ -30,7 +30,11 @@ const REASON_PATTERNS: any[] = [
|
|
|
30
30
|
{ key: "context-window", re: /context (window|limit|saturat)|peakcontext|compact/i },
|
|
31
31
|
{ key: "reboot-sweep", re: /reboot sweep/i },
|
|
32
32
|
{ key: "not-found", re: /not found|404|missing|no such/i },
|
|
33
|
-
{
|
|
33
|
+
{
|
|
34
|
+
key: "output-limit",
|
|
35
|
+
re: /\bmax_(?:output_)?tokens\b|\b(?:maximum|max) output (?:length|tokens)\b|\boutput (?:token|length) limit\b/i,
|
|
36
|
+
},
|
|
37
|
+
{ key: "auth/credentials", re: /unauthorized|401|403|credential|\btoken\b|forbidden/i },
|
|
34
38
|
{ key: "ci/checks-failed", re: /\bci\b|check.?s? fail|lint|tsc|test.?s? fail/i },
|
|
35
39
|
{ key: "network", re: /network|econn|fetch failed|socket|dns|502|503|504/i },
|
|
36
40
|
{ key: "cancelled", re: /cancel|aborted/i },
|