@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.
Files changed (206) hide show
  1. package/dist/{acp-adapter-g3ads55s.js → acp-adapter-bp91d37f.js} +4 -4
  2. package/dist/{actions-zwfykqqk.js → actions-yj0eeaqh.js} +18 -17
  3. package/dist/{amp-adapter-z3s0ssdt.js → amp-adapter-4pg9h34q.js} +1 -1
  4. package/dist/{app-vmp4fgep.js → app-909e97cr.js} +3 -3
  5. package/dist/{assistant-vwde653f.js → assistant-qdyb5fyy.js} +24 -22
  6. package/dist/{boot-reembed-375t7tbk.js → boot-reembed-fk0s9z18.js} +1 -1
  7. package/dist/{boot-reembed-gq9rcpnj.js → boot-reembed-gs1rzrnh.js} +1 -1
  8. package/dist/{boot-scrub-sweep-nd938kyf.js → boot-scrub-sweep-4qfd4kfx.js} +2 -2
  9. package/dist/{boot-seeds-aykx51v3.js → boot-seeds-yr00k13d.js} +4 -4
  10. package/dist/{boxd-k1j26g3h.js → boxd-vdmfssd9.js} +4 -4
  11. package/dist/{claude-adapter-8j4hm099.js → claude-adapter-qta1f1xn.js} +3 -3
  12. package/dist/{claude-managed-setup-0wyynpn1.js → claude-managed-setup-sc6j4dm0.js} +1 -1
  13. package/dist/{claude-sdk-session-q6wj5hmy.js → claude-sdk-session-zgrz6yvc.js} +9169 -9070
  14. package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
  15. package/dist/{cli-vqypxzh4.js → cli-0at8w34w.js} +7 -7
  16. package/dist/{cli-7a42nfy0.js → cli-0qatpy8q.js} +3 -3
  17. package/dist/{cli-p599f1qm.js → cli-0yv5z2pg.js} +1 -1
  18. package/dist/{cli-0gey5fzy.js → cli-386tyxxg.js} +2 -2
  19. package/dist/{cli-5pag6rmn.js → cli-3ypk9vk1.js} +1 -1
  20. package/dist/{cli-qxnf8z2z.js → cli-436532ck.js} +2 -2
  21. package/dist/{cli-z8eh3ex8.js → cli-4necwtsv.js} +4 -4
  22. package/dist/{cli-zh3fp8v2.js → cli-54t00gt8.js} +1 -1
  23. package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
  24. package/dist/{cli-nn4d8dne.js → cli-89knwags.js} +2 -2
  25. package/dist/{cli-spknvxbq.js → cli-8bc1w0tw.js} +10 -10
  26. package/dist/{cli-rqkxrmgw.js → cli-8snvgs5r.js} +38 -13
  27. package/dist/{cli-rhw40jgb.js → cli-bjb3z51z.js} +2 -2
  28. package/dist/{cli-xtjhfmpc.js → cli-c60vetmm.js} +1 -1
  29. package/dist/{cli-0731hvbw.js → cli-decfb6v8.js} +2 -2
  30. package/dist/{cli-bhjt02p8.js → cli-drr5v6av.js} +1 -1
  31. package/dist/{cli-fqzx7gwa.js → cli-dxae2yg7.js} +2 -2
  32. package/dist/{cli-tqgg40ek.js → cli-e3r2neck.js} +8 -7
  33. package/dist/{cli-zppwg9p9.js → cli-ekwks2vs.js} +1 -1
  34. package/dist/{cli-nzxr556s.js → cli-es6va8jk.js} +26 -26
  35. package/dist/{cli-a47xy8eq.js → cli-faxe6thg.js} +1 -1
  36. package/dist/cli-gdhpcbhf.js +55 -0
  37. package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
  38. package/dist/{cli-8veytvbh.js → cli-he9wchme.js} +1 -1
  39. package/dist/{cli-pywf3eh5.js → cli-hgaazk69.js} +1 -1
  40. package/dist/{cli-0bxqnvq9.js → cli-hr2e2fft.js} +28 -6
  41. package/dist/{cli-1ayk1eas.js → cli-k2gzwsj2.js} +135 -26
  42. package/dist/{cli-xbkdm710.js → cli-kqdg1bsb.js} +3 -3
  43. package/dist/cli-m2z8jagn.js +12078 -0
  44. package/dist/{cli-g61qxmge.js → cli-m48g6p95.js} +3 -3
  45. package/dist/{cli-7wspm5gm.js → cli-nhrvpaj7.js} +20 -4
  46. package/dist/{cli-afrrppsw.js → cli-nwj0ee2w.js} +2 -2
  47. package/dist/{cli-3h4nhjn8.js → cli-p5gp8pcx.js} +9 -7
  48. package/dist/{cli-rm6hqj3z.js → cli-pd7vagab.js} +33 -139
  49. package/dist/{cli-mjzynrse.js → cli-q0k1w5zd.js} +1 -1
  50. package/dist/{cli-8hs41zy2.js → cli-rmmmrjkd.js} +1 -1
  51. package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
  52. package/dist/{cli-1338796d.js → cli-shpbx5ee.js} +142 -98
  53. package/dist/{cli-82ae98pz.js → cli-svj5090z.js} +2 -2
  54. package/dist/{cli-bnz7a7qf.js → cli-vvpx3gsx.js} +1 -1
  55. package/dist/{cli-mmwm7zbf.js → cli-w6dt9ged.js} +1 -1
  56. package/dist/{cli-wvaawacd.js → cli-waqbxg08.js} +5230 -66
  57. package/dist/{cli-j3jvnpe8.js → cli-wp9sp1qb.js} +1 -1
  58. package/dist/{cli-nmbmy64z.js → cli-xbbndvjy.js} +3 -3
  59. package/dist/{cli-vbyfwh92.js → cli-xds5f77d.js} +2 -2
  60. package/dist/{cli-7wcyk2s5.js → cli-xnqgpshy.js} +1 -1
  61. package/dist/{cli-rpm2sgv0.js → cli-xnrwazav.js} +19 -2
  62. package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
  63. package/dist/{cli-mry0qzym.js → cli-yn4kxs06.js} +123 -10
  64. package/dist/{cli-hn9hxmz9.js → cli-yx4kfngt.js} +1 -1
  65. package/dist/{cli-y24sc29k.js → cli-yy3rhsdw.js} +30 -8
  66. package/dist/{cli-6s7vdcta.js → cli-zbbwez0r.js} +4 -4
  67. package/dist/{cli-z6jkx23q.js → cli-zm72anhg.js} +9 -9
  68. package/dist/cli.js +22 -21
  69. package/dist/{codex-adapter-q6k3gnyg.js → codex-adapter-ayd1fb6h.js} +2 -2
  70. package/dist/{codex-session-runner-3mwmk3d9.js → codex-session-runner-jesa32xc.js} +2 -2
  71. package/dist/{commands-evrenjn9.js → commands-hanewgf2.js} +9 -3
  72. package/dist/{controller-xjp8vswh.js → controller-dt1n3c1q.js} +8 -3
  73. package/dist/{controllers-connect-pp5jnxvc.js → controllers-connect-xx8x64n5.js} +20 -19
  74. package/dist/{cursor-adapter-yf47w4t0.js → cursor-adapter-9s4dwwdw.js} +1 -1
  75. package/dist/{db-zxpw6aht.js → db-3vttcbv4.js} +3 -3
  76. package/dist/{db-rgfsh91f.js → db-9dv0g1ab.js} +11 -3
  77. package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
  78. package/dist/{dispatcher-smasgws7.js → dispatcher-tx9hd1sv.js} +17 -16
  79. package/dist/{docker-rrjhaqt9.js → docker-c1a3gqmf.js} +89 -22
  80. package/dist/{dsh-adapter-ftyy8dnp.js → dsh-adapter-007y4ejs.js} +1 -1
  81. package/dist/{e2b-qcaj675y.js → e2b-a19d28rz.js} +2 -2
  82. package/dist/{e2b-w1p0bnpm.js → e2b-we6hs6q5.js} +4 -4
  83. package/dist/{easter-egg-3d-yyts7p16.js → easter-egg-3d-rgbrqb8s.js} +1 -1
  84. package/dist/{embedding-space-1z0mbg95.js → embedding-space-ny1e2940.js} +6 -6
  85. package/dist/{embeddings-ygeg7tm7.js → embeddings-2t386t13.js} +1 -1
  86. package/dist/{execute-14qhqp4d.js → execute-pksh0ceh.js} +1 -1
  87. package/dist/{external-channel-ttgrs3sx.js → external-channel-phgmhmd7.js} +2 -2
  88. package/dist/{fx-adapter-gt6pncj2.js → fx-adapter-fy8rr921.js} +5 -5
  89. package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-8y3m3k4b.js} +1 -1
  90. package/dist/{google-vertex-jrtdh9gs.js → google-vertex-w3v98n7b.js} +1 -1
  91. package/dist/{grok-adapter-sm4m5k4g.js → grok-adapter-q0abvpaj.js} +5 -5
  92. package/dist/handlers-by3sfxyx.js +32 -0
  93. package/dist/{heartbeat-qahz4p62.js → heartbeat-kadyek8p.js} +20 -19
  94. package/dist/{hook-kj3d398m.js → hook-5zs3msf6.js} +2 -2
  95. package/dist/{hook-4fd5wq5w.js → hook-9k551xnj.js} +2 -2
  96. package/dist/{http-1ted0j1k.js → http-mhz9vw1f.js} +213 -80
  97. package/dist/index-4f2pvxae.js +37 -0
  98. package/dist/index-98gz7tmp.js +30 -0
  99. package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
  100. package/dist/index-k08z2cak.js +33 -0
  101. package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
  102. package/dist/{index-6rq3r5zb.js → index-q361cg2f.js} +1 -1
  103. package/dist/{index-7v51awbn.js → index-rvr00sg7.js} +20 -13
  104. package/dist/{index-q4b917zs.js → index-vdygbq10.js} +1 -1
  105. package/dist/{keepalive-6cf311t5.js → keepalive-w56f32cg.js} +3 -3
  106. package/dist/{lead-fjy418cw.js → lead-enq35nqp.js} +31 -30
  107. package/dist/{maintenance-a4bm1bmt.js → maintenance-7mdgxcea.js} +1 -1
  108. package/dist/{oauth-refresh-sweep-3h0dfw1k.js → oauth-refresh-sweep-29txy9vk.js} +2 -2
  109. package/dist/{onboard-myn5hraa.js → onboard-kpsrkarj.js} +4 -4
  110. package/dist/{opencode-adapter-p2nkb5am.js → opencode-adapter-j25vm3rr.js} +3 -3
  111. package/dist/{otel-impl-a1a0ef6b.js → otel-impl-6kkct7a8.js} +1 -1
  112. package/dist/{pi-mono-adapter-rzmpb72a.js → pi-mono-adapter-exhkzhn2.js} +4 -4
  113. package/dist/{pricing-refresh-83drrnxj.js → pricing-refresh-n2x7jh6k.js} +3 -3
  114. package/dist/{rbac-roles-s8y6ecb5.js → rbac-roles-81ypx6es.js} +1 -1
  115. package/dist/{rbac-roles-0fb6e7pj.js → rbac-roles-b5htkjby.js} +2 -2
  116. package/dist/{render-v2-twmbgrz4.js → render-v2-qn6jmpk4.js} +3 -3
  117. package/dist/{resume-zfqc36xj.js → resume-wd30wx23.js} +6 -6
  118. package/dist/{runtime-qwggdpqr.js → runtime-4hsg7h60.js} +1 -1
  119. package/dist/{session-access-sc9pn9da.js → session-access-5qvfevhk.js} +4 -4
  120. package/dist/{session-source-backfill-qh07xr8g.js → session-source-backfill-n1czavbr.js} +2 -2
  121. package/dist/{setup-4fr0m7a1.js → setup-dnb56qy5.js} +3 -3
  122. package/dist/{virtual-modules-fh4ha15b.js → virtual-modules-76xxv9fs.js} +2 -2
  123. package/dist/{work-objects-0wr8zxct.js → work-objects-4n21tfkm.js} +20 -19
  124. package/dist/{worker-follow-up-ktyyazyp.js → worker-follow-up-77hvzxww.js} +17 -16
  125. package/dist/{worker-0c5tyzqe.js → worker-gg1576j3.js} +31 -30
  126. package/openapi.json +93 -1
  127. package/package.json +8 -7
  128. package/src/be/db/script-runs.ts +23 -0
  129. package/src/be/db/tasks/read.ts +122 -3
  130. package/src/be/db-client.ts +8 -5
  131. package/src/be/db.ts +15 -4
  132. package/src/be/memory/providers/sqlite-store.ts +49 -12
  133. package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
  134. package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
  135. package/src/be/migrations/263_task_list_filter_index.sql +11 -0
  136. package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
  137. package/src/be/migrations/266_script_runs_source_task.sql +11 -0
  138. package/src/be/runtime-pools/cache-cleanup.ts +109 -0
  139. package/src/be/runtime-pools/db.ts +65 -6
  140. package/src/be/runtime-pools/remote-controller.ts +5 -0
  141. package/src/be/runtime-pools/supervisor.ts +33 -2
  142. package/src/be/scripts/typecheck.ts +22 -0
  143. package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
  144. package/src/commands/controller.ts +4 -0
  145. package/src/commands/entrypoint-api-readiness.ts +74 -0
  146. package/src/http/index.ts +1 -1
  147. package/src/http/list-total-cache.ts +14 -7
  148. package/src/http/memory.ts +38 -9
  149. package/src/http/script-runs.ts +174 -5
  150. package/src/http/status.ts +10 -2
  151. package/src/http/tasks.ts +4 -2
  152. package/src/providers/amp-adapter.ts +1 -1
  153. package/src/providers/fx-adapter.ts +1 -1
  154. package/src/runtime-providers/controller-protocol.ts +14 -1
  155. package/src/runtime-providers/controller.ts +5 -0
  156. package/src/runtime-providers/docker.ts +108 -23
  157. package/src/runtime-providers/types.ts +16 -0
  158. package/src/script-workflows/workflow-ctx.ts +8 -0
  159. package/src/scripts-runtime/types/stdlib.d.ts +22 -0
  160. package/src/scripts-runtime/types/swarm-sdk.d.ts +22 -0
  161. package/src/server-user.ts +1 -0
  162. package/src/slack/commands.ts +5 -1
  163. package/src/slack/handlers.ts +37 -2
  164. package/src/slack/left-threads.ts +27 -4
  165. package/src/tasks/follow-up-redelegation-guard.ts +155 -0
  166. package/src/tests/claude-artifacts-read-script.test.ts +209 -0
  167. package/src/tests/db-client.test.ts +21 -0
  168. package/src/tests/db-task-read-contract.test.ts +8 -2
  169. package/src/tests/db-task-write-contract.test.ts +5 -1
  170. package/src/tests/entrypoint-api-readiness.test.ts +416 -210
  171. package/src/tests/explicit-model-validation.test.ts +14 -8
  172. package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
  173. package/src/tests/list-total-cache.test.ts +27 -2
  174. package/src/tests/memory-fts-populate.test.ts +11 -9
  175. package/src/tests/memory-retrieval-purge.test.ts +80 -0
  176. package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
  177. package/src/tests/query-audit-plans.test.ts +27 -0
  178. package/src/tests/runtime-cache-cleanup.test.ts +367 -0
  179. package/src/tests/runtime-e2b-pools.test.ts +41 -0
  180. package/src/tests/runtime-pools-db.test.ts +98 -0
  181. package/src/tests/runtime-pools-http.test.ts +29 -0
  182. package/src/tests/runtime-providers-docker.test.ts +253 -0
  183. package/src/tests/runtime-remote-controller.test.ts +66 -2
  184. package/src/tests/runtime-supervisor.test.ts +66 -2
  185. package/src/tests/script-connections.test.ts +11 -0
  186. package/src/tests/script-runs-http.test.ts +684 -2
  187. package/src/tests/seed-scripts.test.ts +26 -0
  188. package/src/tests/slack-leave-thread.test.ts +145 -5
  189. package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
  190. package/src/tests/status.test.ts +11 -0
  191. package/src/tests/tasks-readable-row.test.ts +155 -0
  192. package/src/tools/send-task.ts +35 -47
  193. package/src/tools/slack-leave-thread.ts +1 -1
  194. package/src/tools/task-dedup.ts +45 -21
  195. package/src/types.ts +38 -0
  196. package/templates/skills/composio-google-docs/content.md +10 -0
  197. package/templates/skills/script-workflows/content.md +19 -1
  198. package/templates/skills/swarm-docs/content.md +1 -0
  199. package/templates/skills/swarm-scripts/SKILL.md +2 -0
  200. package/templates/skills/swarm-scripts/content.md +2 -0
  201. package/dist/cli-dznrgaep.js +0 -30
  202. package/dist/cli-w3hcrxht.js +0 -6985
  203. package/dist/handlers-vwmba1m2.js +0 -31
  204. package/dist/index-5r71rf98.js +0 -32
  205. package/dist/index-anmwxs87.js +0 -29
  206. package/dist/index-spp0ra5c.js +0 -36
@@ -15,6 +15,10 @@ async function executeCommand(
15
15
  command: ControllerCommand,
16
16
  ): Promise<unknown> {
17
17
  switch (command.op) {
18
+ case "release-cache":
19
+ if (!controller.releaseCache) throw new Error("Controller does not support cache cleanup");
20
+ await controller.releaseCache(command.payload);
21
+ return {};
18
22
  case "provision":
19
23
  return await controller.provision(command.payload);
20
24
  case "terminate":
@@ -0,0 +1,74 @@
1
+ /** Worker boot step; deliberately standalone so both worker images can run it before setup. */
2
+ export interface ApiReadinessDependencies {
3
+ fetch: (url: string, init: RequestInit) => Promise<Response>;
4
+ now: () => number;
5
+ sleep: (ms: number) => Promise<void>;
6
+ log: (message: string) => void;
7
+ }
8
+
9
+ const defaults: ApiReadinessDependencies = {
10
+ fetch: (url, init) => fetch(url, init),
11
+ now: () => performance.now(),
12
+ sleep: (ms) => Bun.sleep(ms),
13
+ log: (message) => console.log(message),
14
+ };
15
+
16
+ /** Poll the unauthenticated health endpoint before any provider reads configuration. */
17
+ export async function waitForApiReady(
18
+ baseUrl: string,
19
+ timeoutValue = "90",
20
+ dependencies: Partial<ApiReadinessDependencies> = {},
21
+ ): Promise<boolean> {
22
+ const { fetch: probe, now, sleep, log } = { ...defaults, ...dependencies };
23
+ const raw = timeoutValue || "90";
24
+ const seconds = Number(raw);
25
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(seconds) || seconds <= 0) {
26
+ log(
27
+ `[entrypoint] FATAL: WORKER_API_READY_TIMEOUT_SECONDS must be a positive integer, got '${raw}'; exiting.`,
28
+ );
29
+ return false;
30
+ }
31
+
32
+ const url = `${baseUrl.replace(/\/$/, "")}/health`;
33
+ const deadline = now() + seconds * 1000;
34
+ log(`[entrypoint] Waiting for API readiness at ${url} (timeout ${raw}s)...`);
35
+ while (true) {
36
+ const controller = new AbortController();
37
+ const timer = setTimeout(
38
+ () => controller.abort(),
39
+ Math.min(3000, Math.max(1, deadline - now())),
40
+ );
41
+ try {
42
+ const response = await probe(url, { redirect: "manual", signal: controller.signal });
43
+ // curl -o /dev/null waited for the whole response, without retaining its body.
44
+ if (response.body) {
45
+ for await (const _chunk of response.body) {
46
+ // Discard health output, including error bodies, within the request timeout.
47
+ }
48
+ }
49
+ // Match curl -f without -L: redirects are successful and are not followed.
50
+ if (response.status > 0 && response.status < 400 && now() <= deadline) {
51
+ log(`[entrypoint] API ready at ${url}`);
52
+ return true;
53
+ }
54
+ } catch {
55
+ // The API can still be booting; retry connection, HTTP and timeout failures alike.
56
+ } finally {
57
+ clearTimeout(timer);
58
+ }
59
+ if (now() >= deadline) {
60
+ log(`[entrypoint] FATAL: API readiness timed out after ${raw}s waiting for ${url}; exiting.`);
61
+ return false;
62
+ }
63
+ await sleep(Math.min(1000, deadline - now()));
64
+ }
65
+ }
66
+
67
+ if (import.meta.main) {
68
+ process.exitCode = (await waitForApiReady(
69
+ Bun.argv[2] ?? "http://host.docker.internal:3013",
70
+ Bun.argv[3],
71
+ ))
72
+ ? 0
73
+ : 1;
74
+ }
package/src/http/index.ts CHANGED
@@ -877,7 +877,7 @@ httpServer
877
877
  // Delete expired MCP overflow values (10-min tick, off the spill path)
878
878
  startMcpOverflowSweep();
879
879
 
880
- // Start expired-memory garbage collector (1-hour tick, immediate first run)
880
+ // Start expired-memory garbage collector (1-hour tick, first run 2 min after boot)
881
881
  await startMemoryGc();
882
882
 
883
883
  // Start the opt-in DB retention sweep after config hydration. Every key is
@@ -3,8 +3,11 @@
3
3
  *
4
4
  * A list total is a COUNT over the same filters as the page, and on the large
5
5
  * tables behind the sessions and task lists it costs as much as the page
6
- * itself. Page 1 (offset 0) always counts, so a write (a new session, a
7
- * triage move) shows at once where the user looks first. Later pages reuse
6
+ * itself. By default page 1 (offset 0) always counts, so a write (a new
7
+ * session, a triage move) shows at once where the user looks first. A list
8
+ * whose count is expensive and whose total may lag can pass
9
+ * `firstPageTtlMs`: page 1 then reuses a count younger than that, so
10
+ * dashboard tabs that poll the same list share one count. Later pages reuse
8
11
  * that count for 30 s. A cold later page (deep link, expired entry) counts
9
12
  * too, so `total` is always set.
10
13
  */
@@ -13,7 +16,7 @@ export const LIST_TOTAL_TTL_MS = 30_000;
13
16
  const MAX_ENTRIES = 500;
14
17
 
15
18
  interface Entry {
16
- expiresAt: number;
19
+ countedAt: number;
17
20
  total: Promise<number>;
18
21
  }
19
22
 
@@ -34,19 +37,23 @@ function filterKey(value: unknown): string {
34
37
  );
35
38
  }
36
39
 
37
- export function createListTotalCache(ttlMs = LIST_TOTAL_TTL_MS): ListTotalCache {
40
+ export function createListTotalCache(
41
+ ttlMs = LIST_TOTAL_TTL_MS,
42
+ firstPageTtlMs = 0,
43
+ ): ListTotalCache {
38
44
  const entries = new Map<string, Entry>();
39
45
  return {
40
46
  get(filters, offset, compute) {
41
47
  const key = filterKey(filters);
42
48
  const now = Date.now();
43
49
  const hit = entries.get(key);
44
- if (hit && hit.expiresAt > now && (offset ?? 0) > 0) return hit.total;
50
+ const maxAge = (offset ?? 0) > 0 ? ttlMs : Math.min(ttlMs, firstPageTtlMs);
51
+ if (hit && hit.countedAt + maxAge > now) return hit.total;
45
52
  if (hit) entries.delete(key);
46
53
 
47
- // Cache the promise, so a later page that arrives mid-count shares it.
54
+ // Cache the promise, so a request that arrives mid-count shares it.
48
55
  const total = compute();
49
- entries.set(key, { expiresAt: now + ttlMs, total });
56
+ entries.set(key, { countedAt: now, total });
50
57
  total.catch(() => {
51
58
  if (entries.get(key)?.total === total) entries.delete(key);
52
59
  });
@@ -1568,17 +1568,31 @@ const MEMORY_GC_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
1568
1568
  let memoryGcTimer: ReturnType<typeof setInterval> | null = null;
1569
1569
 
1570
1570
  const SEARCH_RETRIEVAL_TTL_DAYS = 90;
1571
+ /** Rows per purge statement. The purge yields the event loop between batches. */
1572
+ const SEARCH_RETRIEVAL_PURGE_BATCH = 2000;
1571
1573
 
1572
- async function purgeStaleSearchRetrievals(): Promise<number> {
1574
+ export async function purgeStaleSearchRetrievals(
1575
+ batchSize = SEARCH_RETRIEVAL_PURGE_BATCH,
1576
+ ): Promise<number> {
1573
1577
  try {
1574
1578
  const cutoff = new Date(
1575
1579
  Date.now() - SEARCH_RETRIEVAL_TTL_DAYS * 24 * 60 * 60 * 1000,
1576
1580
  ).toISOString();
1577
- const result = await getDbClient().run(
1578
- "DELETE FROM memory_retrieval WHERE eventType = 'search' AND retrievedAt < ?",
1579
- [cutoff],
1580
- );
1581
- return result.changes;
1581
+ let purged = 0;
1582
+ for (;;) {
1583
+ // `+eventType` keeps the planner on the retrievedAt range. Without
1584
+ // sqlite_stat1 it prefers the eventType equality, which reads every
1585
+ // search row to test its date, even when nothing is stale.
1586
+ const result = await getDbClient().run(
1587
+ `DELETE FROM memory_retrieval WHERE rowid IN (
1588
+ SELECT rowid FROM memory_retrieval
1589
+ WHERE +eventType = 'search' AND retrievedAt < ? LIMIT ?)`,
1590
+ [cutoff, batchSize],
1591
+ );
1592
+ purged += result.changes;
1593
+ if (result.changes < batchSize) return purged;
1594
+ await new Promise<void>((resolve) => setImmediate(resolve));
1595
+ }
1582
1596
  } catch (err) {
1583
1597
  console.error("[memory-gc] Search retrieval purge failed:", (err as Error).message);
1584
1598
  return 0;
@@ -1602,11 +1616,22 @@ async function runMemoryGcTick(label: "Initial" | "Periodic"): Promise<void> {
1602
1616
  }
1603
1617
  }
1604
1618
 
1605
- export async function startMemoryGc(intervalMs = MEMORY_GC_INTERVAL_MS): Promise<void> {
1619
+ /** The first purge waits this long, so it does not compete with boot work. */
1620
+ const MEMORY_GC_INITIAL_DELAY_MS = 2 * 60 * 1000;
1621
+ let memoryGcInitialTimer: ReturnType<typeof setTimeout> | null = null;
1622
+
1623
+ export async function startMemoryGc(
1624
+ intervalMs = MEMORY_GC_INTERVAL_MS,
1625
+ initialDelayMs = MEMORY_GC_INITIAL_DELAY_MS,
1626
+ ): Promise<void> {
1606
1627
  if (memoryGcTimer) return;
1607
1628
 
1608
- // Run immediately on startup to clear any backlog
1609
- await runMemoryGcTick("Initial");
1629
+ // Clear any backlog soon after startup, but off the boot path.
1630
+ memoryGcInitialTimer = setTimeout(() => {
1631
+ memoryGcInitialTimer = null;
1632
+ void runMemoryGcTick("Initial");
1633
+ }, initialDelayMs);
1634
+ if (typeof memoryGcInitialTimer?.unref === "function") memoryGcInitialTimer.unref();
1610
1635
 
1611
1636
  memoryGcTimer = setInterval(() => {
1612
1637
  void runMemoryGcTick("Periodic");
@@ -1615,6 +1640,10 @@ export async function startMemoryGc(intervalMs = MEMORY_GC_INTERVAL_MS): Promise
1615
1640
  }
1616
1641
 
1617
1642
  export function stopMemoryGc(): void {
1643
+ if (memoryGcInitialTimer) {
1644
+ clearTimeout(memoryGcInitialTimer);
1645
+ memoryGcInitialTimer = null;
1646
+ }
1618
1647
  if (memoryGcTimer) {
1619
1648
  clearInterval(memoryGcTimer);
1620
1649
  memoryGcTimer = null;
@@ -13,10 +13,12 @@ import {
13
13
  getLatestScriptRunStepTaskByContextKey,
14
14
  getScriptRun,
15
15
  getScriptRunByIdempotencyKey,
16
+ getScriptRunFollowUpGuard,
16
17
  getScriptRunJournalStep,
17
18
  getTaskById,
18
19
  listScriptRunJournalSteps,
19
20
  listScriptRuns,
21
+ setScriptRunFollowUpGuard,
20
22
  updateScriptRun,
21
23
  updateScriptRunIfNotTerminal,
22
24
  updateScriptRunIfRunning,
@@ -24,6 +26,7 @@ import {
24
26
  } from "../be/db";
25
27
  import { explicitModelErrorForAgent } from "../be/model-validation";
26
28
  import { refuseSourceWithSecrets } from "../be/scripts/source-secrets";
29
+ import { can } from "../rbac";
27
30
  import { lintWorkflowLabels } from "../script-workflows/label-lint";
28
31
  import { scriptRunMaxAgentTasks, scriptRunMaxSteps } from "../script-workflows/limits";
29
32
  import {
@@ -32,6 +35,12 @@ import {
32
35
  terminateScriptRunProcess,
33
36
  } from "../script-workflows/supervisor";
34
37
  import {
38
+ evaluateFollowUpRedelegation,
39
+ type FollowUpRedelegationBlock,
40
+ followUpThreadForSourceTask,
41
+ } from "../tasks/follow-up-redelegation-guard";
42
+ import {
43
+ type AgentTask,
35
44
  AgentTaskStatusSchema,
36
45
  RoutingReasonSchema,
37
46
  ScriptRunJournalEntrySchema,
@@ -111,6 +120,8 @@ const agentTaskBodySchema = z.object({
111
120
  parentTaskId: z.string().uuid().optional(),
112
121
  requestedByUserId: z.string().optional(),
113
122
  outputSchema: z.record(z.string(), z.unknown()).optional(),
123
+ /** Dispatch even when the follow-up re-delegation guard blocks it (same flag as send-task). */
124
+ allowRedelegation: z.boolean().optional(),
114
125
  });
115
126
 
116
127
  const scriptRunCreatedSchema = z.object({
@@ -298,10 +309,70 @@ const agentTaskRoute = route({
298
309
  description: "Agent task created or still running",
299
310
  schema: z.object({ taskId: z.string(), status: AgentTaskStatusSchema }),
300
311
  },
312
+ 403: {
313
+ description:
314
+ "Script run belongs to another agent, or the run's agent may not create a child task under parentTaskId",
315
+ },
301
316
  404: { description: "Script run not found" },
317
+ 409: { description: "Agent task failed, was cancelled, or was superseded" },
318
+ 422: { description: "Blocked by the follow-up re-delegation guard" },
302
319
  },
303
320
  });
304
321
 
322
+ /**
323
+ * Send-task's task.parent.attach decision for a `ctx.step.agentTask` step: a
324
+ * step inherits its parent's contextKey (its Slack or chat thread) and
325
+ * requester, so the run's agent needs the right to hold a child under it.
326
+ * Returns the 403 message, or null when allowed. A parent id with no task row
327
+ * passes: a step under it inherits nothing, as on send-task.
328
+ */
329
+ async function stepParentRefusal(runAgentId: string, parentTaskId: string): Promise<string | null> {
330
+ const parentTask = await getTaskById(parentTaskId);
331
+ if (!parentTask) return null;
332
+ const runAgent = await getAgentById(runAgentId);
333
+ const decision = can({
334
+ principal: { kind: "agent", agentId: runAgentId, isLead: runAgent?.isLead ?? false },
335
+ verb: "task.parent.attach",
336
+ resource: {
337
+ kind: "task",
338
+ taskId: parentTask.id,
339
+ requestedByUserId: parentTask.requestedByUserId,
340
+ creatorAgentId: parentTask.creatorAgentId,
341
+ agentId: parentTask.agentId,
342
+ },
343
+ source: "http",
344
+ });
345
+ return decision.allow
346
+ ? null
347
+ : `You don't have context for parent task ${parentTaskId}. Only its assignee, its creator, or a lead can create a child task under it.`;
348
+ }
349
+
350
+ /**
351
+ * The launching session's task (X-Source-Task-Id), kept only when it belongs
352
+ * to the launching agent so a run cannot borrow another agent's session.
353
+ */
354
+ async function resolveRunSourceTaskId(
355
+ req: IncomingMessage,
356
+ agentId: string,
357
+ ): Promise<string | undefined> {
358
+ const raw = req.headers["x-source-task-id"];
359
+ const sourceTaskId = Array.isArray(raw) ? raw[0] : raw;
360
+ if (!sourceTaskId) return undefined;
361
+ const task = await getTaskById(sourceTaskId);
362
+ return task?.agentId === agentId ? task.id : undefined;
363
+ }
364
+
365
+ /** One `ctx.step.agentTask` dispatch: the step task, or the guard rule that blocked it. */
366
+ type StepDispatch =
367
+ | {
368
+ task: AgentTask;
369
+ /** True when this call inserted the step, false on a replay. */
370
+ created?: boolean;
371
+ blocked?: undefined;
372
+ override?: FollowUpRedelegationBlock["rule"];
373
+ }
374
+ | { task?: undefined; blocked: FollowUpRedelegationBlock; override?: undefined };
375
+
305
376
  async function requireAgent(res: ServerResponse, agentId: string | undefined) {
306
377
  if (!agentId) {
307
378
  jsonError(res, "X-Agent-ID required for script runs API", 400);
@@ -408,6 +479,7 @@ export async function handleScriptRuns(
408
479
  // Cap check and insert commit together: N concurrent POSTs otherwise all
409
480
  // read cap-1 active runs and all insert, exceeding the cap.
410
481
  const cap = scriptRunConcurrencyCap();
482
+ const sourceTaskId = await resolveRunSourceTaskId(req, agent.id);
411
483
  const creation = await getDbClient().transaction(async () => {
412
484
  if ((await countActiveScriptRuns()) >= cap) return null;
413
485
  return await createScriptRun({
@@ -418,6 +490,7 @@ export async function handleScriptRuns(
418
490
  scriptName: parsed.body.scriptName,
419
491
  idempotencyKey: parsed.body.idempotencyKey,
420
492
  requestedByUserId: parsed.body.requestedByUserId,
493
+ sourceTaskId,
421
494
  createdBy: parsed.body.requestedByUserId,
422
495
  });
423
496
  });
@@ -629,9 +702,29 @@ export async function handleScriptRuns(
629
702
  return true;
630
703
  }
631
704
 
705
+ // Only the run's own harness dispatches its steps (it sends the run's
706
+ // agent id). Step tasks carry that agent as creator, so another agent
707
+ // must not create them.
708
+ if (agentId !== run.agentId) {
709
+ jsonError(res, "Script run belongs to another agent", 403);
710
+ return true;
711
+ }
712
+
713
+ // The requested parent, checked before any lookup or insert so a refused
714
+ // call creates nothing.
715
+ if (parsed.body.parentTaskId) {
716
+ const refusal = await stepParentRefusal(run.agentId, parsed.body.parentTaskId);
717
+ if (refusal) {
718
+ jsonError(res, refusal, 403);
719
+ return true;
720
+ }
721
+ }
722
+
632
723
  const contextKey = `script-run:${run.id}:${parsed.body.stepKey}`;
633
724
  let task = await getLatestScriptRunStepTaskByContextKey(contextKey);
725
+ let dispatchedHere = false;
634
726
  if (!task) {
727
+ const taskDescription = parsed.body.template ?? parsed.body.task ?? parsed.body.stepKey;
635
728
  const modelError = await explicitModelErrorForAgent({
636
729
  model: parsed.body.model,
637
730
  allowCustomModel: parsed.body.allowCustomModel,
@@ -641,10 +734,47 @@ export async function handleScriptRuns(
641
734
  jsonError(res, modelError, 400);
642
735
  return true;
643
736
  }
737
+ const followUpThread = await followUpThreadForSourceTask(run.sourceTaskId);
738
+ // The replay lookup, the guard decision and the insert share one write
739
+ // transaction. It serializes this step against the run's other steps,
740
+ // other runs and send-task dispatches into the same thread.
741
+ let dispatched: StepDispatch;
644
742
  try {
645
- task = await createTaskExtended(
646
- parsed.body.template ?? parsed.body.task ?? parsed.body.stepKey,
647
- {
743
+ dispatched = await getDbClient().transaction(async (): Promise<StepDispatch> => {
744
+ const replayed = await getLatestScriptRunStepTaskByContextKey(contextKey);
745
+ if (replayed) return { task: replayed };
746
+
747
+ // Follow-up re-delegation guard, as in send-task. A run is one
748
+ // dispatch: its first new step decides for the whole run, and every
749
+ // later step follows that decision. Later steps never compare against
750
+ // the run's own completed steps, a long run cannot use up the thread's
751
+ // loop cap, and parallel steps are dispatched all or none.
752
+ let override: FollowUpRedelegationBlock["rule"] | undefined;
753
+ if (followUpThread) {
754
+ const decision = await getScriptRunFollowUpGuard(run.id);
755
+ if (decision !== "passed") {
756
+ const blocked: FollowUpRedelegationBlock | null =
757
+ decision === null
758
+ ? await evaluateFollowUpRedelegation({
759
+ ...followUpThread,
760
+ creatorAgentId: run.agentId,
761
+ taskDescription,
762
+ targetAgentId: parsed.body.agentId,
763
+ })
764
+ : {
765
+ rule: decision,
766
+ message: `Blocked by the follow-up re-delegation guard (${decision} rule): an earlier step of this script run was blocked, and a run counts as one dispatch. If it must run, pass allowRedelegation: true.`,
767
+ };
768
+ if (blocked && !parsed.body.allowRedelegation) {
769
+ if (decision === null) await setScriptRunFollowUpGuard(run.id, blocked.rule);
770
+ return { blocked };
771
+ }
772
+ override = blocked?.rule;
773
+ await setScriptRunFollowUpGuard(run.id, "passed");
774
+ }
775
+ }
776
+
777
+ const created = await createTaskExtended(taskDescription, {
648
778
  agentId: parsed.body.agentId,
649
779
  routingReason:
650
780
  parsed.body.routingReason ?? (parsed.body.agentId ? "human_pinned" : undefined),
@@ -660,6 +790,20 @@ export async function handleScriptRuns(
660
790
  offeredTo: parsed.body.offerMode ? parsed.body.agentId : undefined,
661
791
  taskType: "script-run-step",
662
792
  source: "mcp",
793
+ // Counts the run toward the launcher's follow-up loop cap.
794
+ creatorAgentId: run.agentId,
795
+ // With no parent, the step inherits the launching session's Slack
796
+ // thread, as a send-task from that session does.
797
+ sourceTaskId: run.sourceTaskId,
798
+ // A guarded run's steps are its dispatches into the follow-up's
799
+ // thread, whatever their parent, so the guard sees them there.
800
+ ...(followUpThread
801
+ ? {
802
+ slackChannelId: followUpThread.channelId,
803
+ slackThreadTs: followUpThread.threadTs,
804
+ slackUserId: followUpThread.userId,
805
+ }
806
+ : {}),
663
807
  dir: parsed.body.dir,
664
808
  vcsRepo: parsed.body.vcsRepo,
665
809
  model: parsed.body.model,
@@ -667,8 +811,9 @@ export async function handleScriptRuns(
667
811
  requestedByUserId: parsed.body.requestedByUserId ?? run.requestedByUserId,
668
812
  outputSchema: parsed.body.outputSchema,
669
813
  contextKey,
670
- },
671
- );
814
+ });
815
+ return { task: created, created: true, override };
816
+ });
672
817
  } catch (error) {
673
818
  if (error instanceof ExtensionAgentAssignmentError) {
674
819
  jsonError(res, error.message, 400);
@@ -676,6 +821,30 @@ export async function handleScriptRuns(
676
821
  }
677
822
  throw error;
678
823
  }
824
+ if (dispatched.override) {
825
+ console.log(
826
+ `[script-runs] follow-up re-delegation override: rule=${dispatched.override} runId=${run.id} creatorAgentId=${run.agentId} sourceTaskId=${run.sourceTaskId} thread=${followUpThread?.channelId}/${followUpThread?.threadTs}`,
827
+ );
828
+ }
829
+ if (!dispatched.task) {
830
+ json(res, { error: dispatched.blocked.message, rule: dispatched.blocked.rule }, 422);
831
+ return true;
832
+ }
833
+ task = dispatched.task;
834
+ dispatchedHere = dispatched.created === true;
835
+ }
836
+
837
+ // A replay answers from the selected step, whatever parent this call sent
838
+ // (or omitted), so authorize the parent the step actually holds. Every
839
+ // replay lookup above lands here before any output is returned. A step this
840
+ // call just dispatched was authorized above; refusing it now would leave
841
+ // the work running behind a 403.
842
+ if (!dispatchedHere && task.parentTaskId) {
843
+ const refusal = await stepParentRefusal(run.agentId, task.parentTaskId);
844
+ if (refusal) {
845
+ jsonError(res, refusal, 403);
846
+ return true;
847
+ }
679
848
  }
680
849
 
681
850
  const deadline = Date.now() + 30_000;
@@ -45,6 +45,7 @@ import { getSlackConnectionState } from "../slack/connection-state";
45
45
  import { isTelemetryEnabled } from "../telemetry";
46
46
  import { type AgentCredStatus, AutomationIntegrationIdSchema, ProviderNameSchema } from "../types";
47
47
  import { isRuntimePoolsEnabled } from "../utils/runtime-pools-flag";
48
+ import { resolveAppUrl } from "./config-values";
48
49
  import { route } from "./route-def";
49
50
  import { json, jsonError } from "./utils";
50
51
 
@@ -116,6 +117,12 @@ export const StatusIdentitySchema = z.object({
116
117
  * cloud installs can be sliced server-side.
117
118
  */
118
119
  org_id: z.string().nullable(),
120
+ /**
121
+ * The swarm's public dashboard URL (`APP_URL`, then `DASHBOARD_URL`, with
122
+ * swarm_config precedence). Null when neither is set. The dashboard reads
123
+ * links to it in agent output as its own. Absent on older APIs.
124
+ */
125
+ dashboard_url: z.string().nullable().optional(),
119
126
  });
120
127
  export type StatusIdentity = z.infer<typeof StatusIdentitySchema>;
121
128
 
@@ -334,7 +341,7 @@ export async function rollupCredStatusForProvider(provider: string): Promise<Cre
334
341
 
335
342
  // ─── Identity ────────────────────────────────────────────────────────────────
336
343
 
337
- function buildIdentity(): StatusIdentity {
344
+ function buildIdentity(dashboardUrl: string | null): StatusIdentity {
338
345
  const cloudRaw = process.env.SWARM_CLOUD;
339
346
  const hideRaw = process.env.SWARM_HIDE_CLOUD_PROMO;
340
347
  return {
@@ -345,6 +352,7 @@ function buildIdentity(): StatusIdentity {
345
352
  marketing_url: process.env.SWARM_MARKETING_URL?.trim() || null,
346
353
  hide_cloud_promo: hideRaw === "true" || hideRaw === "1",
347
354
  org_id: process.env.SWARM_ORG_ID?.trim() || null,
355
+ dashboard_url: dashboardUrl,
348
356
  };
349
357
  }
350
358
 
@@ -759,7 +767,7 @@ export async function buildStatusPayload(): Promise<StatusResponse> {
759
767
  const instance = await getInstanceFeedbackSignals();
760
768
  return {
761
769
  runtime_pools: { enabled: isRuntimePoolsEnabled(), supervisor: runtimeSupervisorStatus() },
762
- identity: buildIdentity(),
770
+ identity: buildIdentity(await resolveAppUrl()),
763
771
  setup,
764
772
  activity: { ...activity, leads_ready: readyLeads },
765
773
  agent_fs: {
package/src/http/tasks.ts CHANGED
@@ -84,7 +84,7 @@ import {
84
84
  import { ensure } from "../utils/business-use";
85
85
  import { getRequestAuth } from "../utils/request-auth-context";
86
86
  import { scrubSecrets } from "../utils/secret-scrubber";
87
- import { createListTotalCache } from "./list-total-cache";
87
+ import { createListTotalCache, LIST_TOTAL_TTL_MS } from "./list-total-cache";
88
88
  import {
89
89
  agentFirstPrincipal,
90
90
  authUserId,
@@ -224,7 +224,9 @@ const SupersedeTaskResponseSchema = z.object({
224
224
 
225
225
  // ─── Route Definitions ───────────────────────────────────────────────────────
226
226
 
227
- const taskTotals = createListTotalCache();
227
+ // The task list total scans every visible task, and only the pager shows it,
228
+ // so page 1 polls reuse a total younger than 15 s.
229
+ const taskTotals = createListTotalCache(LIST_TOTAL_TTL_MS, 15_000);
228
230
 
229
231
  const listTasks = route({
230
232
  method: "get",
@@ -45,7 +45,7 @@ import type {
45
45
  * default; the image installs exactly this version and the per-task settings
46
46
  * set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
47
47
  */
48
- export const AMP_PACKAGE = "@ampcode/cli@0.0.1793275344-g17bf52";
48
+ export const AMP_PACKAGE = "@ampcode/cli@0.0.1793363705-g5c9ae5";
49
49
 
50
50
  /** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
51
51
  const AMP_MCP_SERVER_NAME = "agent-swarm";
@@ -21,7 +21,7 @@ import type {
21
21
  * Dockerfile.worker; bump the two together). Every ACP shape this adapter
22
22
  * relies on was measured against it.
23
23
  */
24
- export const FX_PINNED_VERSION = "0.0.15";
24
+ export const FX_PINNED_VERSION = "0.0.16";
25
25
 
26
26
  /** OpenAI's Chat Completions prefix; fx appends `/chat/completions`. */
27
27
  export const FX_OPENAI_BASE_URL = "https://api.openai.com/v1";
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { RuntimeProviderKindSchema } from "../types";
2
+ import { RuntimePoolCacheSchema, RuntimeProviderKindSchema } from "../types";
3
3
 
4
4
  export const CONTROLLER_PROTOCOL_VERSION = 1;
5
5
  export const CONTROLLER_COMMAND_TIMEOUT_MS = 60_000;
@@ -16,6 +16,7 @@ const traitsSchema = z.object({
16
16
  canProvision: z.boolean(),
17
17
  ephemeralDisk: z.boolean(),
18
18
  supportsPause: z.boolean(),
19
+ persistentCache: z.boolean().optional(),
19
20
  maxTtlSec: z.number().positive().optional(),
20
21
  maxContinuousSec: z.number().positive().optional(),
21
22
  });
@@ -31,9 +32,21 @@ const provisionSchema = z.object({
31
32
  .object({ cpu: z.number().positive().optional(), memoryMb: z.number().positive().optional() })
32
33
  .optional(),
33
34
  ttlSec: z.number().positive().optional(),
35
+ cache: RuntimePoolCacheSchema.extend({
36
+ key: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/),
37
+ }).optional(),
34
38
  });
35
39
  const command = { type: z.literal("cmd"), id: z.string().min(1) };
36
40
  export const ControllerCommandSchema = z.discriminatedUnion("op", [
41
+ z.object({
42
+ ...command,
43
+ op: z.literal("release-cache"),
44
+ payload: z.object({
45
+ key: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/),
46
+ poolId: z.string().min(1),
47
+ ownerId: z.string().min(1),
48
+ }),
49
+ }),
37
50
  z.object({ ...command, op: z.literal("provision"), payload: provisionSchema }),
38
51
  z.object({
39
52
  ...command,
@@ -1,6 +1,7 @@
1
1
  import { scrubSecrets } from "../utils/secret-scrubber";
2
2
  import type {
3
3
  ProvisionRequest,
4
+ RuntimeCacheReference,
4
5
  RuntimeHandle,
5
6
  RuntimeProvider,
6
7
  RuntimeProviderTraits,
@@ -25,6 +26,7 @@ export interface ControllerCommands {
25
26
  terminate(handle: RuntimeHandle, reason: string): Promise<void>;
26
27
  status(handles: RuntimeHandle[]): Promise<Record<string, RuntimeStatus>>;
27
28
  list(ownerId: string): Promise<RuntimeHandle[]>;
29
+ releaseCache?(cache: RuntimeCacheReference): Promise<void>;
28
30
  renew?(handle: RuntimeHandle, ttlSec: number): Promise<{ endAt: string }>;
29
31
  pause?(handle: RuntimeHandle): Promise<void>;
30
32
  resume?(handle: RuntimeHandle): Promise<void>;
@@ -36,6 +38,7 @@ export class BuiltinController implements ControllerCommands {
36
38
  readonly renew?: ControllerCommands["renew"];
37
39
  readonly pause?: ControllerCommands["pause"];
38
40
  readonly resume?: ControllerCommands["resume"];
41
+ readonly releaseCache?: ControllerCommands["releaseCache"];
39
42
 
40
43
  constructor(
41
44
  readonly id: string,
@@ -47,6 +50,8 @@ export class BuiltinController implements ControllerCommands {
47
50
  this.workerApiUrl = workerApiUrl;
48
51
  if (provider.renew)
49
52
  this.renew = (handle, ttlSec) => this.call(() => provider.renew!(target, handle, ttlSec));
53
+ if (provider.releaseCache)
54
+ this.releaseCache = (cache) => this.call(() => provider.releaseCache!(target, cache));
50
55
  if (provider.pause) this.pause = (handle) => this.call(() => provider.pause!(target, handle));
51
56
  if (provider.resume)
52
57
  this.resume = (handle) => this.call(() => provider.resume!(target, handle));