@desplega.ai/agent-swarm 1.131.0 → 1.133.1

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 (265) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-w53mbns9.js → actions-cskpayd7.js} +11 -9
  3. package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
  4. package/dist/{app-m9gwqsqv.js → app-ang77nbb.js} +8 -6
  5. package/dist/{assistant-bp65m6f3.js → assistant-b4exebdw.js} +16 -13
  6. package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-m3921t4y.js} +8 -8
  7. package/dist/{boot-reembed-62p0at2k.js → boot-reembed-ps2aftpb.js} +9 -7
  8. package/dist/{boot-reembed-xmfp6wdh.js → boot-reembed-znyk3nxb.js} +10 -8
  9. package/dist/{boot-scrub-logs-rr8q2dms.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
  10. package/dist/{claude-adapter-2y47mrkd.js → claude-adapter-3ct8kgxw.js} +13 -9
  11. package/dist/{claude-managed-adapter-gv19tdew.js → claude-managed-adapter-akx4ntf2.js} +2 -2
  12. package/dist/{cli-t1xze5qh.js → cli-13w2hwkh.js} +1 -1
  13. package/dist/{cli-efxfnq6q.js → cli-1532szb1.js} +8 -6
  14. package/dist/{cli-d2tfkcga.js → cli-23d3rseb.js} +33 -17
  15. package/dist/{cli-nm5gtp1x.js → cli-2a75z9c5.js} +196 -7
  16. package/dist/{cli-fywms69x.js → cli-2t30rj0d.js} +1 -1
  17. package/dist/{cli-cvgjwnh1.js → cli-3c3tdgab.js} +10 -10
  18. package/dist/{cli-pm6xbh84.js → cli-3zzxa9jz.js} +46 -16
  19. package/dist/{cli-cdeg08wx.js → cli-4k6233gc.js} +1 -1
  20. package/dist/{cli-t55yw3a1.js → cli-5p2tz05r.js} +2 -2
  21. package/dist/{cli-0rcyq6d9.js → cli-5z2yhzvd.js} +125 -47
  22. package/dist/{cli-3kf51j15.js → cli-6zfq31nj.js} +9 -3
  23. package/dist/{cli-er3a4bb0.js → cli-8m9de10a.js} +6 -1
  24. package/dist/{cli-gm9cbhbq.js → cli-8t14vg9w.js} +27 -8
  25. package/dist/{cli-72q8m8gk.js → cli-9nfc2es8.js} +6 -6
  26. package/dist/{cli-5ze9rv97.js → cli-9ta3pwms.js} +11 -4
  27. package/dist/{cli-78xwvkhj.js → cli-avr0ns7z.js} +2 -2
  28. package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
  29. package/dist/{cli-2q72qz2s.js → cli-dgk20ft7.js} +15 -6
  30. package/dist/cli-et1bbwqf.js +13 -0
  31. package/dist/{cli-bqw0mt1n.js → cli-f5w27pj3.js} +3 -3
  32. package/dist/{cli-2wadx7p2.js → cli-f8dre4bz.js} +2 -2
  33. package/dist/{cli-e7ckcg4v.js → cli-ftwpv451.js} +170 -387
  34. package/dist/{cli-g6b8x1h8.js → cli-g854s2dw.js} +1 -1
  35. package/dist/{cli-5kx36evn.js → cli-ggmdxeqa.js} +13 -8
  36. package/dist/cli-hkwfe3qj.js +530 -0
  37. package/dist/{cli-5dsbcjjw.js → cli-jqwnqx9m.js} +1 -1
  38. package/dist/{cli-9dr5qzc0.js → cli-ny122vh4.js} +21 -10
  39. package/dist/{cli-5v4rw6yd.js → cli-pvzqhbs9.js} +2 -2
  40. package/dist/cli-pzq5rjym.js +15 -0
  41. package/dist/{cli-xpxde7rm.js → cli-qes8ef67.js} +48 -30
  42. package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
  43. package/dist/cli-qys3rhhz.js +27 -0
  44. package/dist/{cli-rzbpmxdr.js → cli-r6tpm37h.js} +2 -2
  45. package/dist/{cli-qv61hp37.js → cli-rch80c76.js} +2 -2
  46. package/dist/{cli-0258203w.js → cli-rpp0x4zy.js} +137 -17
  47. package/dist/{cli-5n6e5yfn.js → cli-s1k0wxhe.js} +12 -7
  48. package/dist/{cli-psh3c4va.js → cli-sfd9v7sj.js} +1 -1
  49. package/dist/{cli-70xqj0z3.js → cli-sh3ytsyj.js} +49 -39
  50. package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
  51. package/dist/{cli-d3x3x1c9.js → cli-xdb4mpnp.js} +2 -2
  52. package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
  53. package/dist/{cli-yw8vghx2.js → cli-xv8t8p3c.js} +65 -32
  54. package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
  55. package/dist/{cli-t76vw5db.js → cli-y08jvxzx.js} +601 -140
  56. package/dist/{cli-a4jzdpa3.js → cli-zfbf3srj.js} +650 -245
  57. package/dist/{cli-a40rnf71.js → cli-zmq89k34.js} +6 -6
  58. package/dist/{cli-qmdnhpej.js → cli-zr9q011v.js} +23 -13
  59. package/dist/cli.js +22 -22
  60. package/dist/{codex-adapter-d24q75wk.js → codex-adapter-bef93pq4.js} +9 -7
  61. package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
  62. package/dist/{codex-session-runner-g50vcsex.js → codex-session-runner-5pkxys7x.js} +9 -7
  63. package/dist/{commands-c3nrc5hg.js → commands-e23s1bff.js} +9 -7
  64. package/dist/{db-nt7fg3tw.js → db-m3nabatn.js} +17 -5
  65. package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
  66. package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
  67. package/dist/{handlers-fkr6swat.js → handlers-akazedvr.js} +16 -13
  68. package/dist/{hook-77wpvnjc.js → hook-ze4cpf0v.js} +55 -41
  69. package/dist/{hook-g019yehy.js → hook-zfqyyenw.js} +10 -7
  70. package/dist/{http-jewj0syk.js → http-sja0n6kw.js} +730 -150
  71. package/dist/index-0gr8vd6k.js +39 -0
  72. package/dist/{index-7n2xpbd4.js → index-354vkhdv.js} +154 -74
  73. package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
  74. package/dist/{index-hfvggd0z.js → index-95t3cqq5.js} +16 -14
  75. package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
  76. package/dist/{index-6gm5he8h.js → index-vpn36mma.js} +15 -13
  77. package/dist/{keepalive-j0fjvqfd.js → keepalive-3sp3meqj.js} +16 -10
  78. package/dist/{lead-1n8x30xe.js → lead-mefeewkf.js} +39 -36
  79. package/dist/{maintenance-gvev11sa.js → maintenance-8tbrbjfg.js} +10 -8
  80. package/dist/mistral-conversations-00zrp7g4.js +717 -0
  81. package/dist/{oauth-refresh-sweep-s9ecsvh0.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
  82. package/dist/{onboard-pvm5b38w.js → onboard-mt2tgyjg.js} +7 -7
  83. package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
  84. package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
  85. package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
  86. package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
  87. package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
  88. package/dist/{otel-impl-p6jvgd9c.js → otel-impl-z6pxt3j9.js} +224 -9
  89. package/dist/{pi-mono-adapter-t091s27g.js → pi-mono-adapter-btvqmj58.js} +1138 -408
  90. package/dist/{pricing-refresh-k49rcyed.js → pricing-refresh-kyq8cezh.js} +9 -7
  91. package/dist/{rbac-roles-cr80g96t.js → rbac-roles-3ryfrfna.js} +8 -6
  92. package/dist/{rbac-roles-30pspvy7.js → rbac-roles-6qfvbee1.js} +9 -7
  93. package/dist/{render-v2-xm457ng1.js → render-v2-p61ppjey.js} +8 -6
  94. package/dist/{seed-pricing-yhpv35fr.js → seed-pricing-j56bbj4y.js} +8 -6
  95. package/dist/{setup-nytnmwwr.js → setup-tpr6c1kn.js} +2 -2
  96. package/dist/{worker-msytr2cw.js → worker-9w1v0mbb.js} +39 -36
  97. package/openapi.json +542 -16
  98. package/package.json +6 -6
  99. package/src/be/budget-refusal-notify.ts +14 -7
  100. package/src/be/db-queries/task-artifacts.ts +182 -0
  101. package/src/be/db.ts +783 -169
  102. package/src/be/events.ts +5 -0
  103. package/src/be/migrations/130_scratch_script_retention_grace.sql +6 -0
  104. package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
  105. package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
  106. package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
  107. package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
  108. package/src/be/migrations/136_task_requester_provenance.sql +37 -0
  109. package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
  110. package/src/be/multi-runtime.ts +430 -0
  111. package/src/be/scripts/db.ts +43 -1
  112. package/src/be/scripts/extract-schema.ts +3 -1
  113. package/src/be/scripts/retention.ts +164 -0
  114. package/src/be/scripts/run-saved.ts +16 -1
  115. package/src/be/scripts/typecheck.ts +3 -2
  116. package/src/be/seed-skills/index.ts +7 -0
  117. package/src/be/steering.ts +1 -1
  118. package/src/be/swarm-config-guard.ts +18 -0
  119. package/src/commands/credential-wait.ts +54 -0
  120. package/src/commands/onboard/steps/core-credentials.tsx +1 -1
  121. package/src/commands/onboard/steps/custom-templates.tsx +1 -1
  122. package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
  123. package/src/commands/onboard/steps/health-check.tsx +1 -1
  124. package/src/commands/onboard/steps/start.tsx +1 -1
  125. package/src/commands/profile-sync-audit.ts +56 -0
  126. package/src/commands/profile-sync.ts +287 -15
  127. package/src/commands/provider-credentials.ts +46 -19
  128. package/src/commands/runner.ts +196 -33
  129. package/src/commands/templates.ts +22 -0
  130. package/src/github/handlers.ts +55 -9
  131. package/src/heartbeat/heartbeat.ts +71 -12
  132. package/src/hooks/hook.ts +49 -33
  133. package/src/http/active-sessions.ts +11 -1
  134. package/src/http/agents.ts +261 -30
  135. package/src/http/all-routes.ts +1 -0
  136. package/src/http/config.ts +13 -2
  137. package/src/http/core.ts +102 -4
  138. package/src/http/db-query-bounded.ts +313 -0
  139. package/src/http/db-query-shared.ts +249 -0
  140. package/src/http/db-query.ts +54 -42
  141. package/src/http/events.ts +2 -0
  142. package/src/http/index.ts +27 -2
  143. package/src/http/mcp-bridge.ts +37 -3
  144. package/src/http/mcp.ts +12 -0
  145. package/src/http/memory.ts +13 -2
  146. package/src/http/openapi.ts +1 -0
  147. package/src/http/poll.ts +70 -27
  148. package/src/http/route-def.ts +43 -9
  149. package/src/http/schedules.ts +2 -2
  150. package/src/http/scripts.ts +31 -4
  151. package/src/http/session-data.ts +44 -0
  152. package/src/http/tasks.ts +2 -2
  153. package/src/http/users.ts +74 -52
  154. package/src/http/webhooks.ts +5 -2
  155. package/src/http/workflows.ts +1 -4
  156. package/src/http/x.ts +14 -0
  157. package/src/integrations/kapso/inbound.ts +1 -1
  158. package/src/jira/outbound.ts +39 -8
  159. package/src/jira/webhook-lifecycle.ts +13 -2
  160. package/src/linear/outbound.ts +43 -10
  161. package/src/oauth/keepalive.ts +13 -2
  162. package/src/prompts/base-prompt.ts +1 -1
  163. package/src/prompts/session-templates.ts +45 -7
  164. package/src/providers/claude-adapter.ts +6 -0
  165. package/src/providers/claude-managed-models.ts +34 -10
  166. package/src/providers/codex-adapter.ts +6 -0
  167. package/src/providers/opencode-adapter.ts +6 -0
  168. package/src/providers/pi-mono-adapter.ts +8 -0
  169. package/src/providers/pricing-sources.md +24 -0
  170. package/src/queue-stall-alarm.ts +216 -0
  171. package/src/scheduler/scheduler.ts +7 -0
  172. package/src/script-workflows/executor.ts +2 -1
  173. package/src/scripts-runtime/executors/native.ts +2 -1
  174. package/src/scripts-runtime/executors/types.ts +5 -0
  175. package/src/scripts-runtime/loader.ts +5 -0
  176. package/src/scripts-runtime/swarm-config.ts +8 -0
  177. package/src/scripts-runtime/swarm-sdk.ts +7 -0
  178. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  179. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  180. package/src/tests/budget-claim-gate.test.ts +20 -0
  181. package/src/tests/claude-adapter.test.ts +32 -0
  182. package/src/tests/claude-managed-adapter.test.ts +38 -7
  183. package/src/tests/codex-adapter.test.ts +30 -0
  184. package/src/tests/create-task-validation.test.ts +128 -0
  185. package/src/tests/credential-wait.test.ts +67 -1
  186. package/src/tests/db-query.test.ts +711 -2
  187. package/src/tests/http-api-integration.test.ts +81 -0
  188. package/src/tests/http-users.test.ts +166 -1
  189. package/src/tests/identity-field-budget.test.ts +176 -0
  190. package/src/tests/mcp-tools-user.test.ts +34 -0
  191. package/src/tests/mcp-user-route.test.ts +17 -0
  192. package/src/tests/metrics-http.test.ts +44 -0
  193. package/src/tests/multi-runtime-registration.test.ts +2703 -0
  194. package/src/tests/openapi-response-contract.test.ts +24 -0
  195. package/src/tests/openapi-route-headers.test.ts +83 -0
  196. package/src/tests/opencode-adapter.test.ts +40 -0
  197. package/src/tests/outbound-listener-rejection.test.ts +138 -0
  198. package/src/tests/pi-mono-adapter.test.ts +98 -0
  199. package/src/tests/profile-sync-warning.test.ts +277 -0
  200. package/src/tests/profile-sync.test.ts +232 -6
  201. package/src/tests/prompt-template-session.test.ts +32 -3
  202. package/src/tests/queue-stall-alarm.test.ts +226 -0
  203. package/src/tests/requester-comms.test.ts +32 -0
  204. package/src/tests/runner-requester-profile.test.ts +38 -0
  205. package/src/tests/sandboxed-process.test.ts +45 -0
  206. package/src/tests/schedule-target-type.test.ts +38 -0
  207. package/src/tests/script-connections.test.ts +16 -3
  208. package/src/tests/scripts-external-api.test.ts +8 -3
  209. package/src/tests/scripts-http.test.ts +12 -0
  210. package/src/tests/scripts-retention.test.ts +384 -0
  211. package/src/tests/scripts-runtime-identity.test.ts +375 -0
  212. package/src/tests/scripts-runtime.test.ts +4 -3
  213. package/src/tests/sdk-allowlist.test.ts +47 -0
  214. package/src/tests/send-task-requested-by.test.ts +9 -0
  215. package/src/tests/session-costs.test.ts +506 -1
  216. package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
  217. package/src/tests/steering-core.test.ts +28 -0
  218. package/src/tests/task-artifacts.test.ts +386 -0
  219. package/src/tests/task-attachments-schema.test.ts +17 -1
  220. package/src/tests/task-completion-idempotency.test.ts +57 -0
  221. package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
  222. package/src/tests/update-profile-api.test.ts +67 -39
  223. package/src/tests/update-profile-auth.test.ts +48 -4
  224. package/src/tests/user-attribution-migration.test.ts +54 -0
  225. package/src/tests/user-identity.test.ts +72 -0
  226. package/src/tests/workflow-async-v2.test.ts +39 -1
  227. package/src/tests/workflow-http-v2.test.ts +3 -2
  228. package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
  229. package/src/tests/workflow-retry-v2.test.ts +45 -1
  230. package/src/tests/workflow-swarm-script.test.ts +14 -5
  231. package/src/tools/db-query.ts +18 -8
  232. package/src/tools/get-task-details.ts +24 -3
  233. package/src/tools/manage-user.ts +36 -3
  234. package/src/tools/poll-task.ts +93 -14
  235. package/src/tools/script-common.ts +5 -0
  236. package/src/tools/send-task.ts +1 -4
  237. package/src/tools/store-progress.ts +13 -2
  238. package/src/tools/swarm-config/delete-config.ts +9 -0
  239. package/src/tools/swarm-config/set-config.ts +3 -2
  240. package/src/tools/task-action.ts +30 -0
  241. package/src/tools/task-tool-ctx.ts +8 -1
  242. package/src/tools/update-profile.ts +16 -21
  243. package/src/tools/utils.ts +5 -0
  244. package/src/tools/workflows/trigger-workflow.ts +7 -1
  245. package/src/types.ts +165 -1
  246. package/src/utils/github-pull-request.ts +76 -0
  247. package/src/utils/identity-field-budget.ts +73 -0
  248. package/src/utils/multi-runtime.ts +20 -0
  249. package/src/utils/requester-comms.ts +24 -0
  250. package/src/utils/sandboxed-process.ts +40 -14
  251. package/src/workflows/engine.ts +22 -2
  252. package/src/workflows/executors/swarm-script.ts +24 -1
  253. package/src/workflows/resume.ts +19 -11
  254. package/src/workflows/retry-poller.ts +1 -0
  255. package/src/workflows/triggers.ts +4 -0
  256. package/templates/schedules/daily-compounding-reflection/content.md +12 -3
  257. package/templates/skills/db-query-guidance/config.json +14 -0
  258. package/templates/skills/db-query-guidance/content.md +57 -0
  259. package/dist/cli-3te4jdkm.js +0 -225
  260. package/dist/cli-bmze986d.js +0 -7
  261. package/dist/cli-kh3ba2zf.js +0 -607
  262. package/dist/index-hp1kdbjj.js +0 -36
  263. package/dist/mistral-conversations-81fwav0j.js +0 -30578
  264. package/dist/otel-nqa12wf3.js +0 -3149
  265. package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Bounded db-query execution — Fix 1 for the API event-loop freeze.
3
+ *
4
+ * `bun:sqlite` is synchronous and exposes no interrupt, progress handler, or
5
+ * statement timeout (verified two ways — see the design doc referenced from
6
+ * the PR). A single expensive query therefore blocks the whole API process
7
+ * until SQLite finishes. Running the query in a short-lived child process
8
+ * lets the parent SIGKILL it on a wall-clock budget: the child dies and its
9
+ * CPU is reclaimed immediately, unlike a worker thread's `terminate()`,
10
+ * which detaches the main thread's view of the worker but leaves the native
11
+ * SQLite call running to completion.
12
+ *
13
+ * SQL and params travel over the child's stdin (not argv), so query text
14
+ * never appears in `ps` output. The child opens its own read-only
15
+ * connection to the same on-disk WAL database file and loads the
16
+ * sqlite-vec extension itself, from the same path the parent process uses
17
+ * (env var first, npm resolver fallback in dev) — the child cannot see the
18
+ * parent's already-loaded extension across the process boundary.
19
+ *
20
+ * `maxRows` travels in the same payload and is applied in the child, before
21
+ * `rowArrays` is built and written to stdout — a query against a huge table
22
+ * with a small `maxRows` transfers and parses a payload bounded by
23
+ * `maxRows`, not by the full match count. `total` still reports every row
24
+ * SQLite matched, so truncation is still detectable by comparing the two.
25
+ *
26
+ * If no `bun` executable can be spawned at all (no separate Bun CLI on
27
+ * $PATH), the query falls back to the synchronous in-process path instead
28
+ * of failing outright — see runBoundedQueryChild's catch around Bun.spawn.
29
+ *
30
+ * The child's own connection sets `PRAGMA busy_timeout` to the same
31
+ * `budgetMs` the parent uses for its SIGKILL timer, not a fixed value — a
32
+ * lock wait that would out-wait the overall budget anyway should surface as
33
+ * SQLite's own busy error (or the parent's timeout) rather than waiting
34
+ * longer than the query could ever be allowed to run.
35
+ */
36
+
37
+ import { getDb, resolveSqliteVecExtensionPath } from "../be/db";
38
+ import type { DbQueryResult } from "./db-query-shared";
39
+ import {
40
+ assertSingleStatement,
41
+ DbQueryConcurrencyCapError,
42
+ DbQueryTimeoutError,
43
+ executeReadOnlyQuery,
44
+ getDbQueryConcurrencyCap,
45
+ warnDbQuerySpawnUnavailableOnce,
46
+ } from "./db-query-shared";
47
+
48
+ interface ChildPayload {
49
+ file: string;
50
+ sql: string;
51
+ params: unknown[];
52
+ vecExtensionPath?: string;
53
+ maxRows?: number;
54
+ busyTimeoutMs: number;
55
+ }
56
+
57
+ interface ChildSuccess {
58
+ columns: string[];
59
+ rows: unknown[][];
60
+ elapsed: number;
61
+ total: number;
62
+ }
63
+
64
+ /** Exit code the child uses when the query is a write statement. */
65
+ const WRITE_REJECTED_EXIT_CODE = 2;
66
+
67
+ // Plain JS (no TypeScript syntax) — this string is executed directly by
68
+ // `bun -e`, not compiled first. Mirrors executeReadOnlyQuery's read-only
69
+ // guard exactly, so `total` keeps meaning "rows SQLite actually returned"
70
+ // regardless of which path ran the query.
71
+ //
72
+ // Iterates the statement rather than calling `stmt.all()` — Codex review,
73
+ // PR #1192 thread 3815922598: `stmt.all()` still materializes every matching
74
+ // row as a JS object before anything gets to look at `maxRows`, even though
75
+ // the earlier fix already bounded what crosses the child/parent boundary.
76
+ // Iterating keeps that same boundary bound, but only ever holds one row
77
+ // object at a time plus the (at most `maxRows`) retained arrays — a query
78
+ // against a huge table with a small `maxRows` no longer allocates one JS
79
+ // object per matched row just to discard almost all of them. `total` is
80
+ // still incremented for every row visited, so truncation stays detectable
81
+ // by comparing it against `rows.length`.
82
+ //
83
+ // BLOB columns (e.g. memories.embedding, script_embeddings.embedding) come
84
+ // back from bun:sqlite as a Uint8Array — Codex review, PR #1192 thread
85
+ // 3816302384. JSON.stringify on a Uint8Array serializes it as an indexed
86
+ // object ({"0":1,"1":2,...}), not an array, because TypedArrays have no
87
+ // toJSON and JSON.stringify only special-cases real Arrays. The parent's
88
+ // JSON.parse then hands the MCP table renderer that indexed object, and its
89
+ // `String(v)` prints "[object Object]" instead of the byte list "1,2,3" the
90
+ // pre-child-process path produced. Converting to a plain Array here keeps
91
+ // `String()` on the round-tripped value identical to `String()` on the
92
+ // original Uint8Array (both join with commas) — the fix belongs in the
93
+ // child, once, rather than every consumer of DbQueryResult guessing which
94
+ // columns might be BLOBs.
95
+ const CHILD_SCRIPT = `
96
+ const { Database } = require("bun:sqlite");
97
+
98
+ (async () => {
99
+ let payload = "";
100
+ for await (const chunk of Bun.stdin.stream()) {
101
+ payload += Buffer.from(chunk).toString("utf8");
102
+ }
103
+
104
+ const { file, sql, params, vecExtensionPath, maxRows, busyTimeoutMs } = JSON.parse(payload);
105
+
106
+ try {
107
+ const db = new Database(file, { readonly: true });
108
+ // Bound by the same wall-clock budget the parent uses for its SIGKILL
109
+ // timer, not a fixed value — a lock wait that would out-wait the overall
110
+ // budget anyway should surface as SQLite's own busy error (or the
111
+ // parent's timeout) rather than waiting longer than the query could ever
112
+ // be allowed to run.
113
+ db.exec("PRAGMA busy_timeout = " + Math.trunc(busyTimeoutMs) + ";");
114
+ if (vecExtensionPath) {
115
+ try {
116
+ db.loadExtension(vecExtensionPath);
117
+ } catch {
118
+ // Non-fatal: only queries that reference vec0 tables/functions need it.
119
+ }
120
+ }
121
+
122
+ const stmt = db.prepare(sql);
123
+ if (stmt.columnNames.length === 0) {
124
+ process.stderr.write("Only read-only queries are allowed");
125
+ process.exit(${WRITE_REJECTED_EXIT_CODE});
126
+ }
127
+
128
+ const columns = stmt.columnNames;
129
+ const start = performance.now();
130
+ const rowArrays = [];
131
+ let total = 0;
132
+ for (const row of stmt.iterate(...(params || []))) {
133
+ total++;
134
+ if (!maxRows || rowArrays.length < maxRows) {
135
+ rowArrays.push(
136
+ columns.map((col) => {
137
+ const value = row[col];
138
+ return value instanceof Uint8Array ? Array.from(value) : value;
139
+ }),
140
+ );
141
+ }
142
+ }
143
+ const elapsed = Math.round(performance.now() - start);
144
+
145
+ process.stdout.write(JSON.stringify({ columns, rows: rowArrays, elapsed, total }));
146
+ } catch (err) {
147
+ process.stderr.write(err && err.message ? err.message : String(err));
148
+ process.exit(1);
149
+ }
150
+ })();
151
+ `;
152
+
153
+ /**
154
+ * Concurrency cap for in-flight bounded child-process queries.
155
+ *
156
+ * Before this cap, N concurrent callers spawned N children in parallel, each
157
+ * materializing its own full result set — peak memory went from roughly 1x
158
+ * (the old synchronous path serialized callers on one thread) to roughly
159
+ * 3x-per-query and unbounded in N (the child's own rows array, the parent's
160
+ * raw stdout buffer, and the parent's parsed JS object all live at once per
161
+ * in-flight query).
162
+ *
163
+ * The default is `DB_QUERY_CONCURRENCY_CAP_DEFAULT` (see db-query-shared.ts
164
+ * for the sizing math against the API pod's real 1 GiB memory limit);
165
+ * overridable per deployment via `DB_QUERY_CONCURRENCY_CAP`, read dynamically
166
+ * on every call (never captured in a module-level const) so a `swarm_config`
167
+ * edit takes effect on the next request with no restart — mirrors
168
+ * `isDbQueryBoundedEnabled`. Rejects immediately when full rather than
169
+ * queueing — an unbounded queue would just move the memory growth from "many
170
+ * children" to "many pending callers," and a caller that's told to retry can
171
+ * back off, whereas a caller stuck in an in-process queue can't.
172
+ */
173
+ let activeBoundedQueryCount = 0;
174
+
175
+ function acquireBoundedQuerySlot(): void {
176
+ const cap = getDbQueryConcurrencyCap();
177
+ if (activeBoundedQueryCount >= cap) {
178
+ throw new DbQueryConcurrencyCapError(
179
+ `Too many concurrent db-query executions in flight (cap ${cap}). Retry shortly.`,
180
+ );
181
+ }
182
+ activeBoundedQueryCount++;
183
+ }
184
+
185
+ function releaseBoundedQuerySlot(): void {
186
+ activeBoundedQueryCount--;
187
+ }
188
+
189
+ /**
190
+ * Run one read-only query in a bounded `bun -e` child process, SIGKILLing it
191
+ * if it runs past `budgetMs`. `total` semantics match executeReadOnlyQuery
192
+ * exactly: the child visits every matching row to count `total`, but only
193
+ * converts and serializes the first `maxRows` of them (in the child, see
194
+ * CHILD_SCRIPT), so `total` still means "rows SQLite returned," not "rows
195
+ * delivered" (callers such as src/tools/db-query.ts:41 and
196
+ * src/http/metrics.ts:168 compare the two to decide whether a result was
197
+ * truncated).
198
+ */
199
+ export async function executeReadOnlyQueryBounded(
200
+ sql: string,
201
+ params: unknown[] = [],
202
+ budgetMs: number,
203
+ maxRows?: number,
204
+ ): Promise<DbQueryResult> {
205
+ assertSingleStatement(sql);
206
+ acquireBoundedQuerySlot();
207
+
208
+ try {
209
+ return await runBoundedQueryChild(sql, params, budgetMs, maxRows);
210
+ } finally {
211
+ releaseBoundedQuerySlot();
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Whether a fired timer should be reported to the caller as a timeout.
217
+ *
218
+ * A complete, correct result can land in the same tick the budget timer
219
+ * fires — the SIGKILL is a no-op on an already-exited child, and its stdout
220
+ * is still the real answer. Only report a timeout when the kill genuinely
221
+ * prevented a clean result (no successful, non-empty stdout); pulled out as
222
+ * its own function so both halves of the condition can be pinned directly in
223
+ * tests instead of only through process-timing races.
224
+ */
225
+ export function isReportableTimeout(timedOut: boolean, exitCode: number, stdout: string): boolean {
226
+ return timedOut && !(exitCode === 0 && stdout.length > 0);
227
+ }
228
+
229
+ async function runBoundedQueryChild(
230
+ sql: string,
231
+ params: unknown[],
232
+ budgetMs: number,
233
+ maxRows?: number,
234
+ ): Promise<DbQueryResult> {
235
+ const payload: ChildPayload = {
236
+ file: getDb().filename,
237
+ sql,
238
+ params,
239
+ vecExtensionPath: resolveSqliteVecExtensionPath(),
240
+ maxRows,
241
+ busyTimeoutMs: budgetMs,
242
+ };
243
+
244
+ let proc: Bun.Subprocess<"pipe", "pipe", "pipe">;
245
+ try {
246
+ proc = Bun.spawn(["bun", "-e", CHILD_SCRIPT], {
247
+ stdin: "pipe",
248
+ stdout: "pipe",
249
+ stderr: "pipe",
250
+ });
251
+ } catch (err) {
252
+ // No `bun` executable resolvable on $PATH to spawn as a child — e.g. the
253
+ // standalone `dist/agent-swarm` binary, which embeds the Bun runtime for
254
+ // its own execution but does not bundle a separate spawnable `bun`
255
+ // binary. Every other documented deployment surface (Docker, npm/bunx,
256
+ // the systemd installer) ships or requires `bun` on $PATH, so this is
257
+ // expected to be rare; fall back to the in-process path rather than
258
+ // failing every db-query outright.
259
+ const message = err instanceof Error ? err.message : String(err);
260
+ if (!/executable not found/i.test(message) && !/enoent/i.test(message)) {
261
+ throw err;
262
+ }
263
+ warnDbQuerySpawnUnavailableOnce();
264
+ return executeReadOnlyQuery(sql, params, maxRows);
265
+ }
266
+
267
+ proc.stdin.write(JSON.stringify(payload));
268
+ await proc.stdin.end();
269
+
270
+ let timedOut = false;
271
+ const timer = setTimeout(() => {
272
+ timedOut = true;
273
+ proc.kill("SIGKILL");
274
+ }, budgetMs);
275
+
276
+ let stdout: string;
277
+ let stderr: string;
278
+ let exitCode: number;
279
+ try {
280
+ [stdout, stderr, exitCode] = await Promise.all([
281
+ new Response(proc.stdout).text(),
282
+ new Response(proc.stderr).text(),
283
+ proc.exited,
284
+ ]);
285
+ } finally {
286
+ clearTimeout(timer);
287
+ }
288
+
289
+ if (isReportableTimeout(timedOut, exitCode, stdout)) {
290
+ throw new DbQueryTimeoutError(
291
+ `Query exceeded the ${budgetMs}ms budget and was terminated. Filter on an indexed column and add a LIMIT. Aggregates such as COUNT(*), SUM(...) or typeof() over session_logs, agent_log, events or task_context_snapshots read every row in range and cannot be made cheap by chunking on rowid.`,
292
+ );
293
+ }
294
+
295
+ if (exitCode === WRITE_REJECTED_EXIT_CODE) {
296
+ throw new Error(stderr.trim() || "Only read-only queries are allowed");
297
+ }
298
+
299
+ if (exitCode !== 0) {
300
+ throw new Error(stderr.trim() || `Query failed with exit code ${exitCode}`);
301
+ }
302
+
303
+ // The child already applies maxRows before serializing (see CHILD_SCRIPT) —
304
+ // parsed.rows is already capped, so no second slice is needed here.
305
+ const parsed = JSON.parse(stdout) as ChildSuccess;
306
+
307
+ return {
308
+ columns: parsed.columns,
309
+ rows: parsed.rows,
310
+ elapsed: parsed.elapsed,
311
+ total: parsed.total,
312
+ };
313
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Primitives shared between the synchronous db-query path (db-query.ts) and
3
+ * the bounded child-process path (db-query-bounded.ts). Split out to avoid a
4
+ * circular import: db-query.ts's HTTP handler calls into the bounded
5
+ * executor, and the bounded executor reuses the same single-statement guard.
6
+ *
7
+ * Also holds the Fix-1 operator knobs (the `DB_QUERY_*` kill switch and
8
+ * budget/row overrides), since both db-query.ts (HTTP) and tools/db-query.ts
9
+ * (MCP) need to read them without importing each other.
10
+ */
11
+
12
+ import { getDb } from "../be/db";
13
+ import { isEnvFlagEnabled } from "../utils/env-flag";
14
+
15
+ export interface DbQueryResult {
16
+ columns: string[];
17
+ rows: unknown[][];
18
+ elapsed: number;
19
+ total: number;
20
+ }
21
+
22
+ /**
23
+ * Thrown when a bounded query is killed after exceeding its wall-clock
24
+ * budget. A distinct class (not a plain `Error`) so `handleDbQuery` can
25
+ * return a documented, machine-readable status instead of a generic 400.
26
+ */
27
+ export class DbQueryTimeoutError extends Error {
28
+ constructor(message: string) {
29
+ super(message);
30
+ this.name = "DbQueryTimeoutError";
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Thrown when `DB_QUERY_CONCURRENCY_CAP` in-flight bounded queries are
36
+ * already running. A distinct class so `handleDbQuery` can return 429 with
37
+ * retry guidance instead of a generic 400 — the caller isn't wrong, the
38
+ * server is just saturated right now.
39
+ */
40
+ export class DbQueryConcurrencyCapError extends Error {
41
+ constructor(message: string) {
42
+ super(message);
43
+ this.name = "DbQueryConcurrencyCapError";
44
+ }
45
+ }
46
+
47
+ export function stripTrailingSemicolon(sql: string): string {
48
+ return sql.trim().replace(/;\s*$/, "").trim();
49
+ }
50
+
51
+ export function assertSingleStatement(sql: string): void {
52
+ const stripped = stripTrailingSemicolon(sql);
53
+ if (stripped.includes(";")) {
54
+ throw new Error("Only one SQL statement is allowed");
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Execute a read-only SQL query against the swarm database, synchronously,
60
+ * on the caller's own thread.
61
+ *
62
+ * Lives here (not db-query.ts) so db-query-bounded.ts can fall back to it
63
+ * without a circular import — db-query.ts already imports the bounded
64
+ * executor. Two callers rely on this fallback path: the
65
+ * `DB_QUERY_BOUNDED_ENABLED=false` kill switch, and runBoundedQueryChild when
66
+ * no `bun` executable can be spawned (see warnDbQuerySpawnUnavailableOnce).
67
+ */
68
+ export function executeReadOnlyQuery(
69
+ sql: string,
70
+ params: unknown[] = [],
71
+ maxRows?: number,
72
+ ): DbQueryResult {
73
+ assertSingleStatement(sql);
74
+ const stmt = getDb().prepare(sql);
75
+
76
+ // bun:sqlite: columnNames is empty for write statements, populated for SELECT/PRAGMA/EXPLAIN
77
+ if (stmt.columnNames.length === 0) {
78
+ throw new Error("Only read-only queries are allowed");
79
+ }
80
+
81
+ const columns = stmt.columnNames as string[];
82
+ const start = performance.now();
83
+ // Iterate rather than `stmt.all()` + slice: a query against a huge table
84
+ // with a small maxRows would otherwise hold every matched row's object in
85
+ // memory at once just to throw most of them away. Iterating still visits
86
+ // every row (so `total` keeps meaning "rows SQLite matched"), but only the
87
+ // first `maxRows` are converted and retained.
88
+ const rowArrays: unknown[][] = [];
89
+ let total = 0;
90
+ for (const row of stmt.iterate(...(params as [string]))) {
91
+ total++;
92
+ if (maxRows === undefined || rowArrays.length < maxRows) {
93
+ rowArrays.push(columns.map((col) => (row as Record<string, unknown>)[col]));
94
+ }
95
+ }
96
+ const elapsed = Math.round(performance.now() - start);
97
+
98
+ return { columns, rows: rowArrays, elapsed, total };
99
+ }
100
+
101
+ /** Hardcoded pre-flag defaults — kept equal to the values PR #87 shipped with, so adding the flag changes no behaviour out of the box. */
102
+ export const DB_QUERY_HTTP_BUDGET_MS_DEFAULT = 10_000;
103
+ export const DB_QUERY_HTTP_MAX_ROWS_DEFAULT = 1000;
104
+ export const DB_QUERY_MCP_BUDGET_MS_DEFAULT = 5_000;
105
+ export const DB_QUERY_MCP_MAX_ROWS_DEFAULT = 100;
106
+
107
+ /**
108
+ * Default concurrency cap for in-flight bounded child-process queries (see
109
+ * `acquireBoundedQuerySlot` in db-query-bounded.ts).
110
+ *
111
+ * Codex review, PR #1192 thread 3816146995: the deployment guide's real API
112
+ * memory limit is 1 GiB (`docs-site/.../guides/performance-resource-sizing.mdx`
113
+ * — the `10Gi` size in `charts/agent-swarm/values.yaml` is PVC storage, not a
114
+ * memory limit). Sized against the largest payload measured against this code
115
+ * path — a 68MB result set — and a ~3x-per-query worst-case footprint (the
116
+ * child's own rows array, the parent's raw stdout buffer, and the parent's
117
+ * parsed JS object all live at once per in-flight query, ~200MB): a cap of 3
118
+ * bounds worst-case fan-out to ~600MB, leaving headroom in the 1 GiB limit for
119
+ * the API process's own baseline footprint. Configurable via
120
+ * `DB_QUERY_CONCURRENCY_CAP` for operators running a larger API pod.
121
+ */
122
+ export const DB_QUERY_CONCURRENCY_CAP_DEFAULT = 3;
123
+
124
+ /**
125
+ * Master kill switch for the bounded child-process execution path (Fix 1).
126
+ * Enabled by default — the fix ships active; an operator sets this to
127
+ * `false` to restore the pre-fix synchronous, unbounded path. Read
128
+ * dynamically on every call (never captured in a module-level const) so a
129
+ * `swarm_config` edit takes effect on the next request with no restart —
130
+ * mirrors `isSteeringEnabled` / `isPoolAffinityEnforcementEnabled`.
131
+ */
132
+ export function isDbQueryBoundedEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
133
+ return isEnvFlagEnabled("DB_QUERY_BOUNDED_ENABLED", true, env);
134
+ }
135
+
136
+ /**
137
+ * Node/Bun's `setTimeout` takes a 32-bit signed delay internally; a value
138
+ * above this silently wraps instead of waiting that long (observed as the
139
+ * timer firing almost immediately). A budget read from an env var must never
140
+ * cross it. See https://nodejs.org/api/timers.html#settimeoutcallback-delay-args.
141
+ */
142
+ export const MAX_TIMER_DELAY_MS = 2_147_483_647;
143
+
144
+ /**
145
+ * Read a positive-integer override, falling back to `defaultValue` for
146
+ * anything absent, non-integer (including fractions — `"500.5"` is rejected,
147
+ * not truncated to 500), <= 0, or above `maxValue`. Mirrors the `^\d+$`
148
+ * integer check `integerValidators` in swarm-config-guard.ts applies to the
149
+ * same keys when set via `swarm_config` — this function is the path that
150
+ * check does NOT cover, since these getters read `process.env` directly.
151
+ */
152
+ function readPositiveIntEnv(
153
+ key: string,
154
+ defaultValue: number,
155
+ env: NodeJS.ProcessEnv = process.env,
156
+ maxValue: number = Number.MAX_SAFE_INTEGER,
157
+ ): number {
158
+ const raw = env[key];
159
+ if (raw === undefined) return defaultValue;
160
+ const trimmed = raw.trim();
161
+ if (!/^\d+$/.test(trimmed)) return defaultValue;
162
+ const parsed = Number(trimmed);
163
+ if (!Number.isSafeInteger(parsed) || parsed <= 0 || parsed > maxValue) return defaultValue;
164
+ return parsed;
165
+ }
166
+
167
+ /** Wall-clock budget for `/api/db-query`. Overridable via `DB_QUERY_HTTP_BUDGET_MS`. */
168
+ export function getDbQueryHttpBudgetMs(env: NodeJS.ProcessEnv = process.env): number {
169
+ return readPositiveIntEnv(
170
+ "DB_QUERY_HTTP_BUDGET_MS",
171
+ DB_QUERY_HTTP_BUDGET_MS_DEFAULT,
172
+ env,
173
+ MAX_TIMER_DELAY_MS,
174
+ );
175
+ }
176
+
177
+ /** Row cap for `/api/db-query`. Overridable via `DB_QUERY_HTTP_MAX_ROWS`. */
178
+ export function getDbQueryHttpMaxRows(env: NodeJS.ProcessEnv = process.env): number {
179
+ return readPositiveIntEnv("DB_QUERY_HTTP_MAX_ROWS", DB_QUERY_HTTP_MAX_ROWS_DEFAULT, env);
180
+ }
181
+
182
+ /** Wall-clock budget for the MCP `db-query` tool. Overridable via `DB_QUERY_MCP_BUDGET_MS`. */
183
+ export function getDbQueryMcpBudgetMs(env: NodeJS.ProcessEnv = process.env): number {
184
+ return readPositiveIntEnv(
185
+ "DB_QUERY_MCP_BUDGET_MS",
186
+ DB_QUERY_MCP_BUDGET_MS_DEFAULT,
187
+ env,
188
+ MAX_TIMER_DELAY_MS,
189
+ );
190
+ }
191
+
192
+ /** Concurrency cap for in-flight bounded child-process queries. Overridable via `DB_QUERY_CONCURRENCY_CAP`. */
193
+ export function getDbQueryConcurrencyCap(env: NodeJS.ProcessEnv = process.env): number {
194
+ return readPositiveIntEnv("DB_QUERY_CONCURRENCY_CAP", DB_QUERY_CONCURRENCY_CAP_DEFAULT, env);
195
+ }
196
+
197
+ /** Row cap for the MCP `db-query` tool. Overridable via `DB_QUERY_MCP_MAX_ROWS`. */
198
+ export function getDbQueryMcpMaxRows(env: NodeJS.ProcessEnv = process.env): number {
199
+ return readPositiveIntEnv("DB_QUERY_MCP_MAX_ROWS", DB_QUERY_MCP_MAX_ROWS_DEFAULT, env);
200
+ }
201
+
202
+ let hasWarnedBoundedDisabled = false;
203
+ let hasWarnedSpawnUnavailable = false;
204
+
205
+ /** Exposed for tests only — resets the one-time warning between cases. */
206
+ export function resetDbQueryBoundedWarningForTests(): void {
207
+ hasWarnedBoundedDisabled = false;
208
+ }
209
+
210
+ /** Exposed for tests only — resets the one-time warning between cases. */
211
+ export function resetDbQuerySpawnUnavailableWarningForTests(): void {
212
+ hasWarnedSpawnUnavailable = false;
213
+ }
214
+
215
+ /**
216
+ * Logs once per process the first time a query runs the legacy unbounded
217
+ * path because `DB_QUERY_BOUNDED_ENABLED=false`. An operator can turn the
218
+ * protection off, but not silently — this is the trace that they did.
219
+ */
220
+ export function warnDbQueryBoundedDisabledOnce(): void {
221
+ if (hasWarnedBoundedDisabled) return;
222
+ hasWarnedBoundedDisabled = true;
223
+ console.warn(
224
+ "[db-query] DB_QUERY_BOUNDED_ENABLED=false — db-query timeout protection is disabled. " +
225
+ "Queries now run in-process with no wall-clock budget and can freeze the API event loop " +
226
+ "until they finish. Set DB_QUERY_BOUNDED_ENABLED=true (or delete the override) to restore " +
227
+ "the bounded child-process path.",
228
+ );
229
+ }
230
+
231
+ /**
232
+ * Logs once per process the first time the bounded path can't spawn a `bun`
233
+ * executable at all (no separate Bun CLI on $PATH — e.g. the standalone
234
+ * `dist/agent-swarm` binary built by `bun run build:binary`, which embeds
235
+ * the Bun runtime for its own execution but does not bundle a
236
+ * spawnable `bun` binary alongside it). Every other documented deployment
237
+ * path (Docker images, npm/bunx, the systemd installer) ships or requires a
238
+ * separate `bun` CLI, so this is expected to be rare.
239
+ */
240
+ export function warnDbQuerySpawnUnavailableOnce(): void {
241
+ if (hasWarnedSpawnUnavailable) return;
242
+ hasWarnedSpawnUnavailable = true;
243
+ console.warn(
244
+ "[db-query] No spawnable `bun` executable found on $PATH — db-query timeout protection is " +
245
+ "unavailable on this host. Falling back to the in-process path with no wall-clock budget; " +
246
+ "queries can freeze the API event loop until they finish. Install the `bun` CLI alongside " +
247
+ "this process to restore the bounded child-process path.",
248
+ );
249
+ }