@desplega.ai/agent-swarm 1.157.0 → 1.158.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/dist/{acp-adapter-qhqrk4q0.js → acp-adapter-xaq4n6pc.js} +1 -1
  2. package/dist/{actions-9rrg0zxm.js → actions-p9hz5yat.js} +15 -15
  3. package/dist/{app-enysy1zd.js → app-g26wdcz6.js} +4 -4
  4. package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
  5. package/dist/{assistant-zegamy48.js → assistant-wydfqkb3.js} +16 -16
  6. package/dist/{boot-reembed-r6shx5qx.js → boot-reembed-g5ckg9yb.js} +6 -6
  7. package/dist/{boot-reembed-zfxb937k.js → boot-reembed-se8zmdcp.js} +5 -5
  8. package/dist/{boot-scrub-logs-k5y0se6r.js → boot-scrub-logs-wfxqan61.js} +3 -3
  9. package/dist/{seed-pricing-vqxa06qf.js → boot-seeds-s3q01xj3.js} +10 -10
  10. package/dist/{claude-adapter-5yc7hxwv.js → claude-adapter-tv2x4f36.js} +7 -7
  11. package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
  12. package/dist/{claude-sdk-session-j4ywn3xf.js → claude-sdk-session-t442qz6f.js} +8 -8
  13. package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
  14. package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
  15. package/dist/{cli-yscm6cv8.js → cli-20cj37vt.js} +1866 -9
  16. package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
  17. package/dist/{cli-awwphhcs.js → cli-47m6dkd3.js} +362 -126
  18. package/dist/{cli-wb2zvbfg.js → cli-53sz8gkz.js} +1 -1
  19. package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
  20. package/dist/{cli-v53rbqp3.js → cli-65hjmev5.js} +57 -17
  21. package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
  22. package/dist/{cli-s98skd5r.js → cli-7gtvvpbh.js} +4 -4
  23. package/dist/{cli-y3nxwx0t.js → cli-7s3syk22.js} +18 -11
  24. package/dist/{cli-74h6f2bp.js → cli-8qmme6vd.js} +2 -2
  25. package/dist/{cli-wsed24mk.js → cli-987hg0aa.js} +2 -2
  26. package/dist/{cli-9n76jszt.js → cli-9httdpyq.js} +1 -1
  27. package/dist/{cli-f3va018d.js → cli-aky8tctm.js} +26 -93
  28. package/dist/{cli-sgyqyyxe.js → cli-c07r9a2m.js} +2 -1
  29. package/dist/{cli-c41wjx0r.js → cli-fgqxwrpv.js} +291 -12
  30. package/dist/{cli-01vfmq8c.js → cli-ftfvggzx.js} +607 -164
  31. package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
  32. package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
  33. package/dist/{cli-r9ee9c4w.js → cli-j55n64b6.js} +23 -22
  34. package/dist/{cli-9p33cz9h.js → cli-j57qkrpq.js} +86 -28
  35. package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
  36. package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
  37. package/dist/{cli-jytrw2na.js → cli-rjehvrts.js} +4 -4
  38. package/dist/{cli-tpv97m71.js → cli-rqz8qbw7.js} +245 -75
  39. package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
  40. package/dist/cli-t1ze64h3.js +811 -0
  41. package/dist/{cli-6jjzxv3b.js → cli-ws6rqycm.js} +3 -2
  42. package/dist/{cli-xf5b7y1f.js → cli-wscdvcqz.js} +13 -5
  43. package/dist/{cli-5j7bm4tb.js → cli-x24pshtq.js} +5 -5
  44. package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
  45. package/dist/{cli-vx9p3qdt.js → cli-xhaeg2nc.js} +1 -1
  46. package/dist/cli.js +29 -15
  47. package/dist/{codex-adapter-gns3s6c3.js → codex-adapter-cnm8wk11.js} +5 -5
  48. package/dist/{codex-session-runner-gqnxys5w.js → codex-session-runner-gfg547zj.js} +5 -5
  49. package/dist/{commands-hxy2y2sp.js → commands-e3bn0p75.js} +3 -3
  50. package/dist/{db-w8ghhsgf.js → db-03v9kzr2.js} +17 -3
  51. package/dist/{dispatcher-cp1rknjz.js → dispatcher-nrmg8yht.js} +14 -14
  52. package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
  53. package/dist/{handlers-egfhr351.js → handlers-aqcedbyb.js} +17 -17
  54. package/dist/{hook-x4kftwzt.js → hook-7vkskkre.js} +19 -11
  55. package/dist/{hook-fbxjqfjz.js → hook-ybvmrmhq.js} +4 -4
  56. package/dist/{http-c7jfx990.js → http-2eqa8t24.js} +432 -195
  57. package/dist/{index-4k9hct21.js → index-1yks3qcr.js} +14 -14
  58. package/dist/{index-jtvpgb8f.js → index-a3vn96n8.js} +30 -27
  59. package/dist/{index-dc2ek6bw.js → index-c6zsv1bd.js} +5 -5
  60. package/dist/{index-yy07ef2s.js → index-chgdkq36.js} +14 -14
  61. package/dist/{index-r6bty6tb.js → index-x71bvq0p.js} +15 -15
  62. package/dist/{keepalive-f7ekefyc.js → keepalive-vtkf5n99.js} +5 -5
  63. package/dist/{lead-h053syts.js → lead-h3sp357x.js} +26 -26
  64. package/dist/{maintenance-a3w6zs54.js → maintenance-jzeqtk3q.js} +6 -6
  65. package/dist/{oauth-refresh-sweep-vra1dhkd.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
  66. package/dist/{onboard-mfgzeqxs.js → onboard-tmsp0qbz.js} +278 -81
  67. package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
  68. package/dist/{otel-impl-2ez6xk57.js → otel-impl-9mz73q65.js} +416 -301
  69. package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
  70. package/dist/{pricing-refresh-6sw65tck.js → pricing-refresh-v1cbr9te.js} +5 -5
  71. package/dist/{rbac-roles-nm6924sy.js → rbac-roles-c9wqz83x.js} +4 -4
  72. package/dist/{render-v2-f026j6c4.js → render-v2-gryrh9h8.js} +4 -4
  73. package/dist/{setup-w606fr1q.js → setup-q9720bt6.js} +2 -2
  74. package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
  75. package/dist/{work-objects-ezmggdne.js → work-objects-b76x7deb.js} +17 -17
  76. package/dist/{worker-1mbyv29k.js → worker-6vrxbv41.js} +26 -26
  77. package/openapi.json +1626 -629
  78. package/package.json +2 -1
  79. package/src/apps/catalog.generated.json +229 -0
  80. package/src/apps/page-validator.ts +71 -16
  81. package/src/apps/row-store.ts +66 -11
  82. package/src/apps/sync.ts +59 -17
  83. package/src/artifact-sdk/browser-sdk.ts +436 -2
  84. package/src/be/approval-sweeps.ts +109 -0
  85. package/src/be/boot-seeds.ts +40 -0
  86. package/src/be/chunking.ts +1 -1
  87. package/src/be/db/agents.ts +20 -0
  88. package/src/be/db/approvals.ts +78 -0
  89. package/src/be/db/tasks/read.ts +197 -39
  90. package/src/be/db/tasks/write.ts +41 -9
  91. package/src/be/db-client.ts +107 -48
  92. package/src/be/db.ts +219 -76
  93. package/src/be/events.ts +33 -1
  94. package/src/be/memory/graph-expansion.ts +10 -3
  95. package/src/be/memory/providers/sqlite-store.ts +55 -13
  96. package/src/be/memory/raters/retrieval.ts +5 -2
  97. package/src/be/memory/reranker.ts +26 -2
  98. package/src/be/memory/types.ts +2 -0
  99. package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
  100. package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
  101. package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
  102. package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
  103. package/src/be/models-catalog.ts +2 -0
  104. package/src/be/modelsdev-cache.json +1857 -0
  105. package/src/be/modelsdev-cache.ts +14 -1
  106. package/src/be/pricing-normalize.ts +2 -0
  107. package/src/be/scripts/typecheck.ts +1 -0
  108. package/src/be/seed-pricing.ts +3 -1
  109. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  110. package/src/be/steering.ts +53 -3
  111. package/src/be/swarm-config-guard.ts +3 -0
  112. package/src/be/task-citations.ts +29 -0
  113. package/src/cli.tsx +14 -1
  114. package/src/commands/onboard/compose-generator.ts +8 -3
  115. package/src/commands/onboard/container-engine.ts +220 -0
  116. package/src/commands/onboard/manifest.ts +1 -0
  117. package/src/commands/onboard/steps/health-check.tsx +4 -1
  118. package/src/commands/onboard/steps/prereq-check.tsx +50 -54
  119. package/src/commands/onboard/steps/start.tsx +31 -16
  120. package/src/commands/onboard/types.ts +7 -2
  121. package/src/commands/onboard.tsx +39 -4
  122. package/src/commands/profile-sync.ts +79 -26
  123. package/src/commands/runner.ts +8 -1
  124. package/src/extensions/catalog.generated.json +17 -0
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/heartbeat/heartbeat.ts +263 -103
  127. package/src/hooks/hook.ts +21 -12
  128. package/src/http/agents.ts +35 -0
  129. package/src/http/approval-requests.ts +136 -64
  130. package/src/http/apps.ts +14 -3
  131. package/src/http/config.ts +15 -1
  132. package/src/http/events.ts +62 -2
  133. package/src/http/index.ts +6 -4
  134. package/src/http/memory.ts +21 -7
  135. package/src/http/schedules.ts +1 -1
  136. package/src/http/session-data.ts +9 -4
  137. package/src/http/sessions.ts +25 -18
  138. package/src/http/stats.ts +19 -7
  139. package/src/http/status.ts +11 -1
  140. package/src/http/tasks.ts +40 -8
  141. package/src/http/webhooks.ts +67 -46
  142. package/src/http/workflows.ts +15 -20
  143. package/src/linear/outbound.ts +37 -15
  144. package/src/otel-impl.ts +151 -1
  145. package/src/otel.ts +14 -3
  146. package/src/prompts/memories.ts +19 -2
  147. package/src/providers/claude-managed-models.ts +8 -0
  148. package/src/providers/modelsdev-reasoning.json +551 -0
  149. package/src/rbac/legacy-policy.ts +16 -0
  150. package/src/rbac/permissions.ts +4 -0
  151. package/src/scheduler/schedule-task.ts +3 -0
  152. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  153. package/src/scripts-runtime/swarm-sdk.ts +9 -0
  154. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  155. package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
  156. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  157. package/src/server.ts +12 -7
  158. package/src/slack/app.ts +35 -4
  159. package/src/slack/connection-state.ts +17 -0
  160. package/src/tasks/context-key.ts +1 -0
  161. package/src/tasks/worker-follow-up.ts +6 -2
  162. package/src/tests/agent-task-activity.test.ts +74 -0
  163. package/src/tests/agents-list-model-display.test.ts +6 -1
  164. package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
  165. package/src/tests/approval-requests.test.ts +395 -0
  166. package/src/tests/approval-sweeps-config.test.ts +47 -0
  167. package/src/tests/approval-sweeps.test.ts +508 -0
  168. package/src/tests/apps-kibo-blocks.test.ts +77 -0
  169. package/src/tests/apps-spike.test.ts +56 -1
  170. package/src/tests/apps-sync-engine.test.ts +20 -9
  171. package/src/tests/boot-seeds.test.ts +170 -0
  172. package/src/tests/cancel-approval-request-tool.test.ts +115 -0
  173. package/src/tests/claude-managed-adapter.test.ts +6 -0
  174. package/src/tests/config-key-filter.test.ts +80 -0
  175. package/src/tests/context-window.test.ts +1 -0
  176. package/src/tests/db-client-busy-retry.test.ts +96 -0
  177. package/src/tests/db-client.test.ts +60 -1
  178. package/src/tests/db-task-read-contract.test.ts +10 -0
  179. package/src/tests/db-task-write-contract.test.ts +9 -0
  180. package/src/tests/docs-index-footprint.test.js +3 -3
  181. package/src/tests/events-http.test.ts +103 -0
  182. package/src/tests/extensions-catalog.test.ts +22 -17
  183. package/src/tests/extensions-pre-task.test.ts +107 -0
  184. package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
  185. package/src/tests/follow-up-config-inheritance.test.ts +260 -0
  186. package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
  187. package/src/tests/heartbeat-reboot-live.test.ts +95 -0
  188. package/src/tests/heartbeat-stall-cas.test.ts +80 -0
  189. package/src/tests/heartbeat.test.ts +99 -3
  190. package/src/tests/hook-mcp-config.test.ts +29 -0
  191. package/src/tests/http-api-integration.test.ts +16 -2
  192. package/src/tests/kv-storage.test.ts +62 -0
  193. package/src/tests/linear-outbound-sync.test.ts +120 -5
  194. package/src/tests/list-endpoint-slimming.test.ts +91 -0
  195. package/src/tests/memory-graph-organic-route.test.ts +155 -0
  196. package/src/tests/memory-http-recall-gating.test.ts +14 -4
  197. package/src/tests/memory-hybrid.test.ts +161 -1
  198. package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
  199. package/src/tests/memory.test.ts +22 -0
  200. package/src/tests/onboard-container-engine.test.ts +240 -0
  201. package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
  202. package/src/tests/pricing-routes.test.ts +14 -0
  203. package/src/tests/profile-sync-warning.test.ts +8 -4
  204. package/src/tests/promote-draft-task-route.test.ts +19 -0
  205. package/src/tests/provider-registration.test.ts +296 -42
  206. package/src/tests/rbac-engine.test.ts +30 -0
  207. package/src/tests/reasoning-effort.test.ts +8 -0
  208. package/src/tests/script-connections.test.ts +7 -0
  209. package/src/tests/sdk-allowlist.test.ts +39 -0
  210. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  211. package/src/tests/sessions-detail-batch.test.ts +250 -0
  212. package/src/tests/sessions.test.ts +118 -0
  213. package/src/tests/slack-config.test.ts +22 -0
  214. package/src/tests/sonnet-5-5-registry.test.ts +44 -0
  215. package/src/tests/status.test.ts +36 -1
  216. package/src/tests/swarm-diff.test.ts +316 -10
  217. package/src/tests/task-completion-idempotency.test.ts +2 -1
  218. package/src/tests/tasks-requested-by-filter.test.ts +3 -1
  219. package/src/tests/tool-annotations.test.ts +1 -1
  220. package/src/tests/ui-list-slim-projections.test.ts +158 -0
  221. package/src/tests/webhooks-task-blocked.test.ts +127 -0
  222. package/src/tests/workflow-approval-recovery.test.ts +203 -0
  223. package/src/tests/workflow-hitl-routing.test.ts +22 -1
  224. package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
  225. package/src/tests/workflow-http-v2.test.ts +99 -8
  226. package/src/tests/workflow-retry-validation.test.ts +56 -0
  227. package/src/tests/workflow-tla-races.test.ts +389 -0
  228. package/src/tests/workflow-validation-port-routing.test.ts +113 -1
  229. package/src/tools/cancel-approval-request.ts +57 -0
  230. package/src/tools/memory-search.ts +1 -0
  231. package/src/tools/request-human-input.ts +4 -2
  232. package/src/tools/send-task.ts +4 -0
  233. package/src/tools/templates.ts +19 -0
  234. package/src/tools/tool-config.ts +2 -1
  235. package/src/tools/workflows/list-workflow-runs.ts +1 -0
  236. package/src/types.ts +38 -0
  237. package/src/utils/context-window.ts +1 -0
  238. package/src/workflows/approval-cancel.ts +152 -0
  239. package/src/workflows/approval-notifications.ts +84 -0
  240. package/src/workflows/checkpoint.ts +10 -5
  241. package/src/workflows/definition.ts +21 -0
  242. package/src/workflows/engine.ts +152 -21
  243. package/src/workflows/executors/human-in-the-loop.ts +1 -1
  244. package/src/workflows/recovery.ts +100 -45
  245. package/src/workflows/resume.ts +49 -18
  246. package/src/workflows/retry-poller.ts +71 -21
  247. package/templates/extensions/README.md +1 -0
  248. package/templates/extensions/github-sender-allowlist/README.md +18 -0
  249. package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
  250. package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
  251. package/templates/skills/apps/content.md +20 -1
  252. package/templates/skills/code-quality/content.md +26 -1
  253. package/templates/skills/pages/content.md +16 -4
  254. package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
  255. package/dist/cli-2daksqps.js +0 -378
  256. package/dist/rbac-roles-3gxmnb20.js +0 -37
@@ -26,7 +26,12 @@ import {
26
26
  withMutationLock,
27
27
  } from "../apps/row-store";
28
28
  import { createApp, getApp, updateApp } from "../apps/store";
29
- import { getAppSyncStatus, runAppSync, type SyncPassResult } from "../apps/sync";
29
+ import {
30
+ getAppSyncStatus,
31
+ RECONCILE_CHUNK_SIZE,
32
+ runAppSync,
33
+ type SyncPassResult,
34
+ } from "../apps/sync";
30
35
  import {
31
36
  closeDb,
32
37
  createAgent,
@@ -1277,17 +1282,20 @@ spawnDescribe("concurrency", () => {
1277
1282
  });
1278
1283
 
1279
1284
  test("a mid-pass write failure reports the committed churn instead of zero counts", async () => {
1280
- // Row writes are independent KV upserts, not a transaction: fail the
1281
- // SECOND create after the first already committed and the pass counts
1282
- // must say so instead of reporting the zero-count base.
1283
- const script = await fixtureScript("partial", [ghRecord(1), ghRecord(2)]);
1285
+ // Rows are written in chunked transactions: fail the second create of the
1286
+ // SECOND chunk after the first chunk committed. The pass counts must say
1287
+ // so instead of reporting the zero-count base, and the failing chunk must
1288
+ // roll back without leaving partial rows.
1289
+ const total = RECONCILE_CHUNK_SIZE + 2;
1290
+ const records = Array.from({ length: total }, (_, index) => ghRecord(index + 1));
1291
+ const script = await fixtureScript("partial", records);
1284
1292
  const appId = await createSyncApp(issueDefinition(script.id));
1285
1293
 
1286
1294
  const realCreate = rowStore.createAppRowUnlocked;
1287
1295
  let creates = 0;
1288
1296
  const spy = spyOn(rowStore, "createAppRowUnlocked").mockImplementation((...callArgs) => {
1289
1297
  creates += 1;
1290
- if (creates === 2) throw new Error("kv write failed (injected)");
1298
+ if (creates === RECONCILE_CHUNK_SIZE + 2) throw new Error("kv write failed (injected)");
1291
1299
  return realCreate(...callArgs);
1292
1300
  });
1293
1301
  try {
@@ -1296,9 +1304,12 @@ spawnDescribe("concurrency", () => {
1296
1304
 
1297
1305
  expect(result.ok).toBe(false);
1298
1306
  expect(pass.error).toContain("kv write failed (injected)");
1299
- expect(pass).toMatchObject({ pulled: 2, created: 1 });
1300
- expect((await rowsOf(appId)).map((row) => row.issueKey)).toEqual(["1"]);
1301
- expect(await getAppSyncStatus(appId, "issue", "gh")).toMatchObject({ ok: false, created: 1 });
1307
+ expect(pass).toMatchObject({ pulled: total, created: RECONCILE_CHUNK_SIZE });
1308
+ expect(await rowsOf(appId)).toHaveLength(RECONCILE_CHUNK_SIZE);
1309
+ expect(await getAppSyncStatus(appId, "issue", "gh")).toMatchObject({
1310
+ ok: false,
1311
+ created: RECONCILE_CHUNK_SIZE,
1312
+ });
1302
1313
  } finally {
1303
1314
  spy.mockRestore();
1304
1315
  }
@@ -0,0 +1,170 @@
1
+ import { afterAll, afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm, unlink, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { ensurePricingSeeded, ensureRbacSeeded } from "../be/boot-seeds";
6
+ import { closeDb, getDbClient, initDb } from "../be/db";
7
+ import { loadModelsDevCache } from "../be/modelsdev-cache";
8
+ import { BUILTIN_ROLES } from "../be/rbac-roles";
9
+ import { createServer } from "../server";
10
+
11
+ const TEST_DB_PATH = "./test-boot-seeds.sqlite";
12
+ const BUILTIN_ROLE_ID = "rbac-role-requester";
13
+
14
+ async function removeDbFiles(): Promise<void> {
15
+ for (const suffix of ["", "-wal", "-shm"]) {
16
+ try {
17
+ await unlink(TEST_DB_PATH + suffix);
18
+ } catch {
19
+ // File does not exist.
20
+ }
21
+ }
22
+ }
23
+
24
+ async function countPricingSeedRows(): Promise<number> {
25
+ const row = await getDbClient().get<{ n: number }>(
26
+ "SELECT COUNT(*) AS n FROM pricing WHERE effective_from = 0",
27
+ );
28
+ return row?.n ?? 0;
29
+ }
30
+
31
+ async function countBuiltinPermissions(): Promise<number> {
32
+ const row = await getDbClient().get<{ n: number }>(
33
+ "SELECT COUNT(*) AS n FROM role_permissions WHERE roleId = ?",
34
+ [BUILTIN_ROLE_ID],
35
+ );
36
+ return row?.n ?? 0;
37
+ }
38
+
39
+ // Fresh DB handle per test: the guards are keyed on the live handle.
40
+ beforeEach(async () => {
41
+ closeDb();
42
+ await removeDbFiles();
43
+ initDb(TEST_DB_PATH);
44
+ });
45
+
46
+ afterEach(() => {
47
+ closeDb();
48
+ });
49
+
50
+ afterAll(async () => {
51
+ await removeDbFiles();
52
+ });
53
+
54
+ describe("ensurePricingSeeded", () => {
55
+ test("seeds once per database handle, and again for a new handle", async () => {
56
+ ensurePricingSeeded();
57
+ const seeded = await countPricingSeedRows();
58
+ expect(seeded).toBeGreaterThan(0);
59
+
60
+ // A second call must not touch the table: wipe it and confirm it stays empty.
61
+ await getDbClient().run("DELETE FROM pricing WHERE effective_from = 0");
62
+ ensurePricingSeeded();
63
+ expect(await countPricingSeedRows()).toBe(0);
64
+
65
+ // Reopening the same file gives a new handle, which is seeded again.
66
+ closeDb();
67
+ initDb(TEST_DB_PATH);
68
+ ensurePricingSeeded();
69
+ expect(await countPricingSeedRows()).toBe(seeded);
70
+ });
71
+ });
72
+
73
+ describe("ensureRbacSeeded", () => {
74
+ test("syncs once per database handle, and again for a new handle", async () => {
75
+ ensureRbacSeeded();
76
+ const seeded = await countBuiltinPermissions();
77
+ expect(seeded).toBeGreaterThan(0);
78
+
79
+ await getDbClient().run("DELETE FROM role_permissions WHERE roleId = ?", [BUILTIN_ROLE_ID]);
80
+ ensureRbacSeeded();
81
+ expect(await countBuiltinPermissions()).toBe(0);
82
+
83
+ closeDb();
84
+ initDb(TEST_DB_PATH);
85
+ ensureRbacSeeded();
86
+ expect(await countBuiltinPermissions()).toBe(seeded);
87
+ });
88
+
89
+ test("a failed sync does not mark the handle done", async () => {
90
+ // Make the built-in role unrecoverable: its name is taken by another role id.
91
+ const builtin = BUILTIN_ROLES.find((role) => role.id === BUILTIN_ROLE_ID);
92
+ if (!builtin) throw new Error("test fixture: built-in role missing");
93
+ const client = getDbClient();
94
+ await client.run("PRAGMA foreign_keys = OFF");
95
+ await client.run("DELETE FROM roles WHERE id = ?", [BUILTIN_ROLE_ID]);
96
+ await client.run("PRAGMA foreign_keys = ON");
97
+ await client.run(
98
+ "INSERT INTO roles (id, name, description, isBuiltin, grantsAll) VALUES ('rbac-role-collision', ?, 'x', 0, 0)",
99
+ [builtin.name],
100
+ );
101
+
102
+ expect(() => ensureRbacSeeded()).toThrow(/already uses that name/);
103
+ // Not cached as done: the next caller retries and sees the same error.
104
+ expect(() => ensureRbacSeeded()).toThrow(/already uses that name/);
105
+
106
+ await client.run("DELETE FROM roles WHERE id = 'rbac-role-collision'");
107
+ ensureRbacSeeded();
108
+ expect(await countBuiltinPermissions()).toBeGreaterThan(0);
109
+ });
110
+ });
111
+
112
+ describe("createServer (one call per MCP session)", () => {
113
+ let savedDatabasePath: string | undefined;
114
+
115
+ beforeEach(() => {
116
+ savedDatabasePath = process.env.DATABASE_PATH;
117
+ process.env.DATABASE_PATH = TEST_DB_PATH;
118
+ });
119
+
120
+ afterEach(() => {
121
+ if (savedDatabasePath === undefined) delete process.env.DATABASE_PATH;
122
+ else process.env.DATABASE_PATH = savedDatabasePath;
123
+ });
124
+
125
+ test("seeds pricing and RBAC on the first session only", async () => {
126
+ await createServer();
127
+ expect(await countPricingSeedRows()).toBeGreaterThan(0);
128
+ expect(await countBuiltinPermissions()).toBeGreaterThan(0);
129
+
130
+ await getDbClient().run("DELETE FROM pricing WHERE effective_from = 0");
131
+ await getDbClient().run("DELETE FROM role_permissions WHERE roleId = ?", [BUILTIN_ROLE_ID]);
132
+
133
+ // A later session re-registers tools but must not re-run the seeds.
134
+ const second = await createServer();
135
+ expect(second).toBeDefined();
136
+ expect(await countPricingSeedRows()).toBe(0);
137
+ expect(await countBuiltinPermissions()).toBe(0);
138
+ });
139
+ });
140
+
141
+ describe("loadModelsDevCache", () => {
142
+ const saved = process.env.MODELSDEV_CACHE_PATH;
143
+ let dir: string | undefined;
144
+
145
+ afterEach(async () => {
146
+ if (saved === undefined) delete process.env.MODELSDEV_CACHE_PATH;
147
+ else process.env.MODELSDEV_CACHE_PATH = saved;
148
+ if (dir) await rm(dir, { recursive: true, force: true });
149
+ dir = undefined;
150
+ });
151
+
152
+ test("returns the same parsed object while it is alive", () => {
153
+ const first = loadModelsDevCache();
154
+ expect(first).not.toBeNull();
155
+ expect(loadModelsDevCache()).toBe(first);
156
+ });
157
+
158
+ test("keys the memo on the resolved path", async () => {
159
+ dir = await mkdtemp(path.join(tmpdir(), "modelsdev-cache-"));
160
+ const alt = path.join(dir, "cache.json");
161
+ await writeFile(alt, JSON.stringify({ anthropic: { id: "anthropic-alt", models: {} } }));
162
+
163
+ const bundled = loadModelsDevCache();
164
+ process.env.MODELSDEV_CACHE_PATH = alt;
165
+ const overridden = loadModelsDevCache();
166
+
167
+ expect(overridden?.anthropic?.id).toBe("anthropic-alt");
168
+ expect(overridden).not.toBe(bundled);
169
+ });
170
+ });
@@ -0,0 +1,115 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import {
5
+ closeDb,
6
+ createAgent,
7
+ createApprovalRequest,
8
+ createTaskExtended,
9
+ getApprovalRequestById,
10
+ getDbClient,
11
+ initDb,
12
+ } from "../be/db";
13
+ import { registerCancelApprovalRequestTool } from "../tools/cancel-approval-request";
14
+
15
+ const TEST_DB_PATH = "./test-cancel-approval-request-tool.sqlite";
16
+
17
+ type ToolResult = {
18
+ isError?: boolean;
19
+ structuredContent: { success: boolean; alreadyCancelled?: boolean; runCancelled?: boolean };
20
+ };
21
+
22
+ let callTool: (args: unknown, agentId: string) => Promise<ToolResult>;
23
+ let leadId = "";
24
+ let ownerId = "";
25
+ let workerId = "";
26
+
27
+ async function makeRequest() {
28
+ const sourceTask = await createTaskExtended("source", { agentId: ownerId, source: "mcp" });
29
+ const id = crypto.randomUUID();
30
+ await createApprovalRequest({
31
+ id,
32
+ title: "Tool cancel",
33
+ questions: [{ id: "q1", type: "approval", label: "Approve?", required: true }],
34
+ approvers: { policy: "any" },
35
+ sourceTaskId: sourceTask.id,
36
+ });
37
+ return id;
38
+ }
39
+
40
+ describe("cancel-approval-request tool", () => {
41
+ beforeAll(async () => {
42
+ initDb(TEST_DB_PATH);
43
+ leadId = (await createAgent({ name: "tool-lead", isLead: true, status: "idle" })).id;
44
+ ownerId = (await createAgent({ name: "tool-owner", isLead: false, status: "idle" })).id;
45
+ workerId = (await createAgent({ name: "tool-worker", isLead: false, status: "idle" })).id;
46
+ const server = new McpServer({ name: "cancel-approval-request-test", version: "1.0.0" });
47
+ registerCancelApprovalRequestTool(server);
48
+ const tool = (
49
+ server as unknown as {
50
+ _registeredTools: Record<
51
+ string,
52
+ { handler: (args: unknown, extra: unknown) => Promise<unknown> }
53
+ >;
54
+ }
55
+ )._registeredTools["cancel-approval-request"];
56
+ if (!tool) throw new Error("cancel-approval-request tool was not registered");
57
+ callTool = async (args, agentId) =>
58
+ (await tool.handler(args, {
59
+ sessionId: "cancel-approval-request-test",
60
+ requestInfo: { headers: { "x-agent-id": agentId } },
61
+ })) as ToolResult;
62
+ });
63
+
64
+ afterAll(async () => {
65
+ closeDb();
66
+ for (const suffix of ["", "-wal", "-shm"]) {
67
+ await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
68
+ }
69
+ });
70
+
71
+ test("a lead cancels a pending request, then a second call is a no-op", async () => {
72
+ const requestId = await makeRequest();
73
+
74
+ const first = await callTool({ requestId, reason: "not needed" }, leadId);
75
+ expect(first.isError).toBeFalsy();
76
+ expect(first.structuredContent.alreadyCancelled).toBe(false);
77
+ const row = await getApprovalRequestById(requestId);
78
+ expect(row!.status).toBe("cancelled");
79
+ expect(row!.resolvedBy).toBe(leadId);
80
+ expect(row!.resolutionReason).toBe("not needed");
81
+
82
+ const second = await callTool({ requestId }, leadId);
83
+ expect(second.isError).toBeFalsy();
84
+ expect(second.structuredContent.alreadyCancelled).toBe(true);
85
+ });
86
+
87
+ test("a worker that does not own the source task gets a tool error", async () => {
88
+ const requestId = await makeRequest();
89
+
90
+ const result = await callTool({ requestId }, workerId);
91
+
92
+ expect(result.isError).toBe(true);
93
+ expect((await getApprovalRequestById(requestId))!.status).toBe("pending");
94
+ });
95
+
96
+ test("the owner agent may cancel", async () => {
97
+ const requestId = await makeRequest();
98
+ const result = await callTool({ requestId }, ownerId);
99
+ expect(result.isError).toBeFalsy();
100
+ expect((await getApprovalRequestById(requestId))!.status).toBe("cancelled");
101
+ });
102
+
103
+ test("an expired pending request gets a tool error and becomes timeout", async () => {
104
+ const requestId = await makeRequest();
105
+ await getDbClient().run("UPDATE approval_requests SET expiresAt = ? WHERE id = ?", [
106
+ new Date(Date.now() - 60_000).toISOString(),
107
+ requestId,
108
+ ]);
109
+
110
+ const result = await callTool({ requestId }, leadId);
111
+
112
+ expect(result.isError).toBe(true);
113
+ expect((await getApprovalRequestById(requestId))!.status).toBe("timeout");
114
+ });
115
+ });
@@ -922,6 +922,12 @@ describe("ClaudeManagedAdapter (Phase 4) — repo provisioning + cost data", ()
922
922
  cacheReadPerMillion: 1.0,
923
923
  cacheWritePerMillion: 12.5,
924
924
  });
925
+ expect(CLAUDE_MANAGED_MODEL_PRICING["claude-sonnet-5-5"]).toEqual({
926
+ inputPerMillion: 2.0,
927
+ outputPerMillion: 10.0,
928
+ cacheReadPerMillion: 0.2,
929
+ cacheWritePerMillion: 2.5,
930
+ });
925
931
  expect(CLAUDE_MANAGED_MODEL_PRICING["claude-sonnet-5"]).toEqual({
926
932
  inputPerMillion: 2.0,
927
933
  outputPerMillion: 10.0,
@@ -0,0 +1,80 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { createServer, type Server } from "node:http";
3
+ import { closeDb, initDb, upsertSwarmConfig } from "../be/db";
4
+ import { handleConfig } from "../http/config";
5
+ import { handleCore } from "../http/core";
6
+ import { getPathSegments, parseQueryParams } from "../http/utils";
7
+ import { listenOnFreePort } from "./test-net";
8
+
9
+ // ctx.swarm.config_get / config_list send `key` to these routes. Both used to
10
+ // ignore it and return every row, so a script asking for one secret got all.
11
+
12
+ const API_KEY = "example-config-key-filter-test-key";
13
+
14
+ let server: Server;
15
+ let baseUrl: string;
16
+
17
+ beforeAll(async () => {
18
+ initDb(":memory:");
19
+ await upsertSwarmConfig({ scope: "global", key: "WANTED_KEY", value: "wanted" });
20
+ await upsertSwarmConfig({
21
+ scope: "global",
22
+ key: "OTHER_SECRET",
23
+ value: "other-secret-value",
24
+ isSecret: true,
25
+ });
26
+ await upsertSwarmConfig({ scope: "global", key: "PLAIN_KEY", value: "plain" });
27
+
28
+ server = createServer(async (req, res) => {
29
+ if (await handleCore(req, res, req.headers["x-agent-id"] as string | undefined, API_KEY)) {
30
+ return;
31
+ }
32
+ const segments = getPathSegments(req.url || "");
33
+ const query = parseQueryParams(req.url || "");
34
+ if (await handleConfig(req, res, segments, query)) return;
35
+ res.writeHead(404).end();
36
+ });
37
+ baseUrl = `http://127.0.0.1:${await listenOnFreePort(server)}`;
38
+ });
39
+
40
+ afterAll(async () => {
41
+ await new Promise<void>((resolve) => server.close(() => resolve()));
42
+ closeDb();
43
+ });
44
+
45
+ async function getKeys(path: string): Promise<string[]> {
46
+ const response = await fetch(`${baseUrl}${path}`, {
47
+ headers: { Authorization: `Bearer ${API_KEY}` },
48
+ });
49
+ expect(response.status).toBe(200);
50
+ const body = (await response.json()) as { configs: Array<{ key: string }> };
51
+ return body.configs.map((config) => config.key);
52
+ }
53
+
54
+ const ALL_KEYS = ["OTHER_SECRET", "PLAIN_KEY", "WANTED_KEY"];
55
+
56
+ describe.each([
57
+ ["/api/config/resolved", "?"],
58
+ ["/api/config", "?scope=global&"],
59
+ ])("GET %s key filter", (path, prefix) => {
60
+ test("?key=<existing> returns exactly that row", async () => {
61
+ expect(await getKeys(`${path}${prefix}key=WANTED_KEY&includeSecrets=true`)).toEqual([
62
+ "WANTED_KEY",
63
+ ]);
64
+ });
65
+
66
+ test("?key=<unknown> returns an empty list", async () => {
67
+ expect(await getKeys(`${path}${prefix}key=NO_SUCH_KEY&includeSecrets=true`)).toEqual([]);
68
+ });
69
+
70
+ test("no key returns every row", async () => {
71
+ expect((await getKeys(`${path}${prefix}includeSecrets=true`)).sort()).toEqual(ALL_KEYS);
72
+ });
73
+
74
+ test("an empty key is rejected instead of returning every row", async () => {
75
+ const response = await fetch(`${baseUrl}${path}${prefix}key=`, {
76
+ headers: { Authorization: `Bearer ${API_KEY}` },
77
+ });
78
+ expect(response.status).toBe(400);
79
+ });
80
+ });
@@ -27,6 +27,7 @@ describe("getContextWindowSize", () => {
27
27
  });
28
28
 
29
29
  test("returns 1M for sonnet models", () => {
30
+ expect(getContextWindowSize("claude-sonnet-5-5")).toBe(1_000_000);
30
31
  expect(getContextWindowSize("claude-sonnet-5")).toBe(1_000_000);
31
32
  expect(getContextWindowSize("claude-sonnet-4-6")).toBe(1_000_000);
32
33
  expect(getContextWindowSize("sonnet")).toBe(1_000_000);
@@ -202,6 +202,102 @@ describe("db-client SQLITE_BUSY retry", () => {
202
202
  expect(Date.now() - started).toBeLessThan(500);
203
203
  });
204
204
 
205
+ describe("under the production 5000ms ambient busy_timeout", () => {
206
+ /** Largest gap between 5ms ticks while `work` runs: the longest the loop was frozen. */
207
+ async function maxLoopFreezeMs(work: () => Promise<unknown>): Promise<number> {
208
+ let last = performance.now();
209
+ let maxGap = 0;
210
+ const ticker = setInterval(() => {
211
+ const now = performance.now();
212
+ maxGap = Math.max(maxGap, now - last);
213
+ last = now;
214
+ }, 5);
215
+ try {
216
+ await work();
217
+ // One more tick so a freeze that ended on the last attempt is seen.
218
+ await new Promise((resolve) => setTimeout(resolve, 20));
219
+ } finally {
220
+ clearInterval(ticker);
221
+ }
222
+ return maxGap;
223
+ }
224
+
225
+ beforeEach(() => {
226
+ main.exec("PRAGMA busy_timeout = 5000");
227
+ });
228
+
229
+ test("a contended write still succeeds via the async retry", async () => {
230
+ const released = holdWriteLock(300);
231
+ const freezeMs = await maxLoopFreezeMs(() =>
232
+ client.run("INSERT INTO items (name) VALUES (?)", ["contended"]),
233
+ );
234
+ await released;
235
+ const landed = await client.get<{ name: string }>(
236
+ "SELECT name FROM items WHERE name = 'contended'",
237
+ );
238
+ expect(landed?.name).toBe("contended");
239
+ // Every attempt spins at most attemptSpinMs (25ms) or finalAttemptSpinMs;
240
+ // the release happened during an async backoff sleep.
241
+ expect(freezeMs).toBeLessThan(1_000);
242
+ });
243
+
244
+ test("the final statement attempt spins finalAttemptSpinMs, not the ambient timeout", async () => {
245
+ const tight = createBunSqliteClient(() => main, {
246
+ maxWaitMs: 60,
247
+ backoffMs: [20, 20],
248
+ attemptSpinMs: 25,
249
+ finalAttemptSpinMs: 50,
250
+ });
251
+ // The holder releases on a timer, which cannot fire while the final
252
+ // attempt blocks the loop: pre-fix this froze for the full 5000ms.
253
+ const released = holdWriteLock(3_000);
254
+ let caught: unknown;
255
+ const startedAt = performance.now();
256
+ const freezeMs = await maxLoopFreezeMs(async () => {
257
+ try {
258
+ await tight.run("INSERT INTO items (name) VALUES (?)", ["never"]);
259
+ } catch (err) {
260
+ caught = err;
261
+ }
262
+ });
263
+ const wallMs = performance.now() - startedAt;
264
+ await released;
265
+ expect((caught as { code?: string }).code).toBe("SQLITE_BUSY");
266
+ expect(wallMs).toBeLessThan(2_000);
267
+ expect(freezeMs).toBeLessThan(1_000);
268
+ const row = main.query<{ timeout: number }, []>("PRAGMA busy_timeout").get();
269
+ expect(row?.timeout).toBe(5000);
270
+ });
271
+
272
+ test("the final BEGIN IMMEDIATE attempt spins finalAttemptSpinMs, not the ambient timeout", async () => {
273
+ const tight = createBunSqliteClient(() => main, {
274
+ maxWaitMs: 60,
275
+ backoffMs: [20, 20],
276
+ attemptSpinMs: 25,
277
+ finalAttemptSpinMs: 50,
278
+ });
279
+ const released = holdWriteLock(3_000);
280
+ let caught: unknown;
281
+ const startedAt = performance.now();
282
+ const freezeMs = await maxLoopFreezeMs(async () => {
283
+ try {
284
+ await tight.transaction(async (tx) => {
285
+ await tx.run("INSERT INTO items (name) VALUES (?)", ["never-tx"]);
286
+ });
287
+ } catch (err) {
288
+ caught = err;
289
+ }
290
+ });
291
+ const wallMs = performance.now() - startedAt;
292
+ await released;
293
+ expect((caught as { code?: string }).code).toBe("SQLITE_BUSY");
294
+ expect(wallMs).toBeLessThan(2_000);
295
+ expect(freezeMs).toBeLessThan(1_000);
296
+ const row = main.query<{ timeout: number }, []>("PRAGMA busy_timeout").get();
297
+ expect(row?.timeout).toBe(5000);
298
+ });
299
+ });
300
+
205
301
  test("reads proceed under an external write lock without needing the retry path", async () => {
206
302
  await client.run("INSERT INTO items (name) VALUES (?)", ["pre-existing"]);
207
303
  const released = holdWriteLock(150);
@@ -1,5 +1,5 @@
1
1
  import { Database } from "bun:sqlite";
2
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
3
3
  import { mkdtempSync, rmSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
@@ -427,3 +427,62 @@ describe("db-client transactions on a shared WAL file (second connection)", () =
427
427
  expect(row?.n).toBe(11);
428
428
  });
429
429
  });
430
+
431
+ describe("db-client slow statement warning", () => {
432
+ // A recursive CTE is pure CPU inside one driver call, so it blocks the
433
+ // event loop the same way a slow production query does.
434
+ const SLOW_SQL = `/* ${"padding ".repeat(40)} */ WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM c WHERE x < ?) SELECT count(*) AS n FROM c WHERE ? IS NOT NULL`;
435
+ const SLOW_ROWS = 3_000_000;
436
+ const PARAM_SECRET = "param-value-that-must-not-be-logged";
437
+
438
+ const slowWarnings = (spy: ReturnType<typeof spyOn>): string[] =>
439
+ spy.mock.calls
440
+ .map((args) => String(args[0]))
441
+ .filter((line) => line.includes("[db-client] slow sync statement"));
442
+
443
+ test("logs a statement that blocked the loop past 100ms, with truncated SQL and no params", async () => {
444
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
445
+ try {
446
+ const startedAt = performance.now();
447
+ const row = await client.get<{ n: number }>(SLOW_SQL, [SLOW_ROWS, PARAM_SECRET]);
448
+ const elapsedMs = performance.now() - startedAt;
449
+ expect(row?.n).toBe(SLOW_ROWS);
450
+ // Guard the premise: on a machine fast enough to finish under the
451
+ // threshold, the warning is correctly absent and this test says so.
452
+ expect(elapsedMs).toBeGreaterThan(100);
453
+ const lines = slowWarnings(warn);
454
+ expect(lines).toHaveLength(1);
455
+ expect(lines[0]).toMatch(/blocked the event loop for \d+ms: \/\* padding/);
456
+ expect(lines[0]).not.toContain(PARAM_SECRET);
457
+ expect(lines[0]).not.toContain("WITH RECURSIVE");
458
+ expect(lines[0]?.endsWith("…")).toBe(true);
459
+ } finally {
460
+ warn.mockRestore();
461
+ }
462
+ });
463
+
464
+ test("logs a slow statement run inside a transaction", async () => {
465
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
466
+ try {
467
+ await client.transaction(async (tx) => {
468
+ await tx.get(SLOW_SQL, [SLOW_ROWS, PARAM_SECRET]);
469
+ });
470
+ const lines = slowWarnings(warn);
471
+ expect(lines).toHaveLength(1);
472
+ expect(lines[0]).not.toContain(PARAM_SECRET);
473
+ } finally {
474
+ warn.mockRestore();
475
+ }
476
+ });
477
+
478
+ test("does not log fast statements", async () => {
479
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
480
+ try {
481
+ await client.run("INSERT INTO items (name) VALUES (?)", ["fast"]);
482
+ await client.query("SELECT name FROM items");
483
+ expect(slowWarnings(warn)).toHaveLength(0);
484
+ } finally {
485
+ warn.mockRestore();
486
+ }
487
+ });
488
+ });