@desplega.ai/agent-swarm 1.131.1 → 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 (250) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-jgcy477k.js → actions-cskpayd7.js} +11 -9
  3. package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
  4. package/dist/{app-5da78bd4.js → app-ang77nbb.js} +8 -6
  5. package/dist/{assistant-f2vkk1hj.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-q7mp8hcr.js → boot-reembed-ps2aftpb.js} +9 -7
  8. package/dist/{boot-reembed-xmw8wcx6.js → boot-reembed-znyk3nxb.js} +10 -8
  9. package/dist/{boot-scrub-logs-dsqrv79n.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
  10. package/dist/{claude-adapter-q6x7yspx.js → claude-adapter-3ct8kgxw.js} +13 -9
  11. package/dist/{claude-managed-adapter-wsg37bt8.js → claude-managed-adapter-akx4ntf2.js} +2 -2
  12. package/dist/{cli-114fvtmj.js → cli-13w2hwkh.js} +1 -1
  13. package/dist/{cli-4rkfg1wx.js → cli-1532szb1.js} +8 -6
  14. package/dist/{cli-g74vpjp2.js → cli-23d3rseb.js} +33 -17
  15. package/dist/{cli-3ty0t082.js → cli-2a75z9c5.js} +196 -7
  16. package/dist/{cli-pxfxkx5m.js → cli-2t30rj0d.js} +1 -1
  17. package/dist/{cli-rgrfzg8z.js → cli-3c3tdgab.js} +10 -10
  18. package/dist/{cli-6dp3ydxa.js → cli-3zzxa9jz.js} +15 -11
  19. package/dist/{cli-2f07m0xr.js → cli-4k6233gc.js} +1 -1
  20. package/dist/{cli-t8d349cy.js → cli-5p2tz05r.js} +2 -2
  21. package/dist/{cli-w2wxd56v.js → cli-5z2yhzvd.js} +127 -49
  22. package/dist/{cli-kd1ck4rx.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-ccf9vy7q.js → cli-9nfc2es8.js} +6 -6
  26. package/dist/{cli-ycrnynpw.js → cli-9ta3pwms.js} +11 -4
  27. package/dist/{cli-7dm2zqy2.js → cli-avr0ns7z.js} +2 -2
  28. package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
  29. package/dist/{cli-gecy3z2s.js → cli-dgk20ft7.js} +4 -4
  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-6pcz9h15.js → cli-ftwpv451.js} +170 -387
  34. package/dist/{cli-nrkkknat.js → cli-g854s2dw.js} +1 -1
  35. package/dist/{cli-793jvbsh.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-sy7jk3tx.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-se2p6rkb.js → cli-r6tpm37h.js} +2 -2
  45. package/dist/{cli-g1re8r05.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-aca5pz9f.js → cli-sfd9v7sj.js} +1 -1
  49. package/dist/{cli-2rk0cx7s.js → cli-sh3ytsyj.js} +49 -39
  50. package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
  51. package/dist/{cli-6dpytdr2.js → cli-xdb4mpnp.js} +2 -2
  52. package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
  53. package/dist/{cli-jbcva0xj.js → cli-xv8t8p3c.js} +56 -31
  54. package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
  55. package/dist/{cli-jevgrb4w.js → cli-y08jvxzx.js} +601 -140
  56. package/dist/{cli-50vbt00w.js → cli-zfbf3srj.js} +650 -245
  57. package/dist/{cli-ygrbmjja.js → cli-zmq89k34.js} +6 -6
  58. package/dist/cli.js +22 -22
  59. package/dist/{codex-adapter-htjkn22b.js → codex-adapter-bef93pq4.js} +9 -7
  60. package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
  61. package/dist/{codex-session-runner-wjfc7smz.js → codex-session-runner-5pkxys7x.js} +9 -7
  62. package/dist/{commands-xhrfwqsk.js → commands-e23s1bff.js} +9 -7
  63. package/dist/{db-anvwb22f.js → db-m3nabatn.js} +17 -5
  64. package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
  65. package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
  66. package/dist/{handlers-b9kbc621.js → handlers-akazedvr.js} +16 -13
  67. package/dist/{hook-xkvn6c5h.js → hook-ze4cpf0v.js} +55 -41
  68. package/dist/{hook-cgk4wxmx.js → hook-zfqyyenw.js} +10 -7
  69. package/dist/{http-9xh6ta4r.js → http-sja0n6kw.js} +608 -147
  70. package/dist/index-0gr8vd6k.js +39 -0
  71. package/dist/{index-eaxyqcmj.js → index-354vkhdv.js} +154 -74
  72. package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
  73. package/dist/{index-25wjsdv2.js → index-95t3cqq5.js} +16 -14
  74. package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
  75. package/dist/{index-ywds4812.js → index-vpn36mma.js} +15 -13
  76. package/dist/{keepalive-vc7s2aa6.js → keepalive-3sp3meqj.js} +16 -10
  77. package/dist/{lead-r2yqfmx7.js → lead-mefeewkf.js} +39 -36
  78. package/dist/{maintenance-mw70k468.js → maintenance-8tbrbjfg.js} +10 -8
  79. package/dist/mistral-conversations-00zrp7g4.js +717 -0
  80. package/dist/{oauth-refresh-sweep-yecgjpk4.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
  81. package/dist/{onboard-xj0f73f9.js → onboard-mt2tgyjg.js} +7 -7
  82. package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
  83. package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
  84. package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
  85. package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
  86. package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
  87. package/dist/{otel-impl-nxg0n0b3.js → otel-impl-z6pxt3j9.js} +224 -9
  88. package/dist/{pi-mono-adapter-4g5kby31.js → pi-mono-adapter-btvqmj58.js} +1138 -408
  89. package/dist/{pricing-refresh-2cn6effr.js → pricing-refresh-kyq8cezh.js} +9 -7
  90. package/dist/{rbac-roles-1hcxdtg7.js → rbac-roles-3ryfrfna.js} +8 -6
  91. package/dist/{rbac-roles-vynday5g.js → rbac-roles-6qfvbee1.js} +9 -7
  92. package/dist/{render-v2-tc0bh8kk.js → render-v2-p61ppjey.js} +8 -6
  93. package/dist/{seed-pricing-8kceb43f.js → seed-pricing-j56bbj4y.js} +8 -6
  94. package/dist/{setup-46xtgq3c.js → setup-tpr6c1kn.js} +2 -2
  95. package/dist/{worker-qrbz6hmv.js → worker-9w1v0mbb.js} +39 -36
  96. package/openapi.json +542 -16
  97. package/package.json +6 -6
  98. package/src/be/budget-refusal-notify.ts +14 -7
  99. package/src/be/db-queries/task-artifacts.ts +182 -0
  100. package/src/be/db.ts +783 -169
  101. package/src/be/events.ts +5 -0
  102. package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
  103. package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
  104. package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
  105. package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
  106. package/src/be/migrations/136_task_requester_provenance.sql +37 -0
  107. package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
  108. package/src/be/multi-runtime.ts +430 -0
  109. package/src/be/scripts/extract-schema.ts +3 -1
  110. package/src/be/scripts/typecheck.ts +3 -2
  111. package/src/be/seed-skills/index.ts +7 -0
  112. package/src/be/steering.ts +1 -1
  113. package/src/be/swarm-config-guard.ts +18 -0
  114. package/src/commands/credential-wait.ts +54 -0
  115. package/src/commands/onboard/steps/core-credentials.tsx +1 -1
  116. package/src/commands/onboard/steps/custom-templates.tsx +1 -1
  117. package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
  118. package/src/commands/onboard/steps/health-check.tsx +1 -1
  119. package/src/commands/onboard/steps/start.tsx +1 -1
  120. package/src/commands/profile-sync-audit.ts +56 -0
  121. package/src/commands/profile-sync.ts +287 -15
  122. package/src/commands/provider-credentials.ts +46 -19
  123. package/src/commands/runner.ts +196 -33
  124. package/src/commands/templates.ts +22 -0
  125. package/src/github/handlers.ts +55 -9
  126. package/src/heartbeat/heartbeat.ts +71 -12
  127. package/src/hooks/hook.ts +49 -33
  128. package/src/http/active-sessions.ts +11 -1
  129. package/src/http/agents.ts +261 -30
  130. package/src/http/all-routes.ts +1 -0
  131. package/src/http/config.ts +13 -2
  132. package/src/http/core.ts +102 -4
  133. package/src/http/db-query-bounded.ts +313 -0
  134. package/src/http/db-query-shared.ts +249 -0
  135. package/src/http/db-query.ts +54 -42
  136. package/src/http/events.ts +2 -0
  137. package/src/http/index.ts +22 -2
  138. package/src/http/mcp-bridge.ts +37 -3
  139. package/src/http/mcp.ts +12 -0
  140. package/src/http/memory.ts +13 -2
  141. package/src/http/openapi.ts +1 -0
  142. package/src/http/poll.ts +70 -27
  143. package/src/http/route-def.ts +43 -9
  144. package/src/http/schedules.ts +2 -2
  145. package/src/http/scripts.ts +10 -1
  146. package/src/http/session-data.ts +44 -0
  147. package/src/http/tasks.ts +2 -2
  148. package/src/http/users.ts +74 -52
  149. package/src/http/webhooks.ts +5 -2
  150. package/src/http/workflows.ts +1 -4
  151. package/src/integrations/kapso/inbound.ts +1 -1
  152. package/src/jira/outbound.ts +39 -8
  153. package/src/jira/webhook-lifecycle.ts +13 -2
  154. package/src/linear/outbound.ts +43 -10
  155. package/src/oauth/keepalive.ts +13 -2
  156. package/src/prompts/base-prompt.ts +1 -1
  157. package/src/prompts/session-templates.ts +45 -7
  158. package/src/providers/claude-adapter.ts +6 -0
  159. package/src/providers/codex-adapter.ts +6 -0
  160. package/src/providers/opencode-adapter.ts +6 -0
  161. package/src/providers/pi-mono-adapter.ts +8 -0
  162. package/src/providers/pricing-sources.md +24 -0
  163. package/src/queue-stall-alarm.ts +216 -0
  164. package/src/scheduler/scheduler.ts +7 -0
  165. package/src/script-workflows/executor.ts +2 -1
  166. package/src/scripts-runtime/executors/native.ts +2 -1
  167. package/src/scripts-runtime/executors/types.ts +5 -0
  168. package/src/scripts-runtime/loader.ts +5 -0
  169. package/src/scripts-runtime/swarm-config.ts +8 -0
  170. package/src/scripts-runtime/swarm-sdk.ts +7 -0
  171. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  172. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  173. package/src/tests/budget-claim-gate.test.ts +20 -0
  174. package/src/tests/claude-adapter.test.ts +32 -0
  175. package/src/tests/codex-adapter.test.ts +30 -0
  176. package/src/tests/create-task-validation.test.ts +128 -0
  177. package/src/tests/credential-wait.test.ts +67 -1
  178. package/src/tests/db-query.test.ts +711 -2
  179. package/src/tests/http-api-integration.test.ts +81 -0
  180. package/src/tests/http-users.test.ts +166 -1
  181. package/src/tests/identity-field-budget.test.ts +176 -0
  182. package/src/tests/mcp-tools-user.test.ts +34 -0
  183. package/src/tests/mcp-user-route.test.ts +17 -0
  184. package/src/tests/metrics-http.test.ts +44 -0
  185. package/src/tests/multi-runtime-registration.test.ts +2703 -0
  186. package/src/tests/openapi-response-contract.test.ts +24 -0
  187. package/src/tests/openapi-route-headers.test.ts +83 -0
  188. package/src/tests/opencode-adapter.test.ts +40 -0
  189. package/src/tests/outbound-listener-rejection.test.ts +138 -0
  190. package/src/tests/pi-mono-adapter.test.ts +98 -0
  191. package/src/tests/profile-sync-warning.test.ts +277 -0
  192. package/src/tests/profile-sync.test.ts +232 -6
  193. package/src/tests/prompt-template-session.test.ts +32 -3
  194. package/src/tests/queue-stall-alarm.test.ts +226 -0
  195. package/src/tests/requester-comms.test.ts +32 -0
  196. package/src/tests/runner-requester-profile.test.ts +38 -0
  197. package/src/tests/schedule-target-type.test.ts +38 -0
  198. package/src/tests/script-connections.test.ts +13 -1
  199. package/src/tests/scripts-runtime-identity.test.ts +375 -0
  200. package/src/tests/scripts-runtime.test.ts +4 -3
  201. package/src/tests/sdk-allowlist.test.ts +47 -0
  202. package/src/tests/send-task-requested-by.test.ts +9 -0
  203. package/src/tests/session-costs.test.ts +506 -1
  204. package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
  205. package/src/tests/steering-core.test.ts +28 -0
  206. package/src/tests/task-artifacts.test.ts +386 -0
  207. package/src/tests/task-attachments-schema.test.ts +17 -1
  208. package/src/tests/task-completion-idempotency.test.ts +57 -0
  209. package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
  210. package/src/tests/update-profile-api.test.ts +67 -39
  211. package/src/tests/update-profile-auth.test.ts +48 -4
  212. package/src/tests/user-attribution-migration.test.ts +54 -0
  213. package/src/tests/user-identity.test.ts +72 -0
  214. package/src/tests/workflow-async-v2.test.ts +39 -1
  215. package/src/tests/workflow-http-v2.test.ts +3 -2
  216. package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
  217. package/src/tests/workflow-retry-v2.test.ts +45 -1
  218. package/src/tools/db-query.ts +18 -8
  219. package/src/tools/get-task-details.ts +24 -3
  220. package/src/tools/manage-user.ts +36 -3
  221. package/src/tools/poll-task.ts +93 -14
  222. package/src/tools/script-common.ts +5 -0
  223. package/src/tools/send-task.ts +1 -4
  224. package/src/tools/store-progress.ts +13 -2
  225. package/src/tools/swarm-config/delete-config.ts +9 -0
  226. package/src/tools/swarm-config/set-config.ts +3 -2
  227. package/src/tools/task-action.ts +30 -0
  228. package/src/tools/task-tool-ctx.ts +8 -1
  229. package/src/tools/update-profile.ts +16 -21
  230. package/src/tools/utils.ts +5 -0
  231. package/src/tools/workflows/trigger-workflow.ts +7 -1
  232. package/src/types.ts +165 -1
  233. package/src/utils/github-pull-request.ts +76 -0
  234. package/src/utils/identity-field-budget.ts +73 -0
  235. package/src/utils/multi-runtime.ts +20 -0
  236. package/src/utils/requester-comms.ts +24 -0
  237. package/src/workflows/engine.ts +22 -2
  238. package/src/workflows/resume.ts +19 -11
  239. package/src/workflows/retry-poller.ts +1 -0
  240. package/src/workflows/triggers.ts +4 -0
  241. package/templates/skills/db-query-guidance/config.json +14 -0
  242. package/templates/skills/db-query-guidance/content.md +57 -0
  243. package/dist/cli-3te4jdkm.js +0 -225
  244. package/dist/cli-bmze986d.js +0 -7
  245. package/dist/cli-kh3ba2zf.js +0 -607
  246. package/dist/index-vg4snay4.js +0 -36
  247. package/dist/mistral-conversations-81fwav0j.js +0 -30578
  248. package/dist/otel-nqa12wf3.js +0 -3149
  249. package/dist/{cli-wz9115x0.js → cli-zr9q011v.js} +6 -6
  250. package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
@@ -1,15 +1,30 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
- import { getDb } from "../be/db";
3
+ import { executeReadOnlyQueryBounded } from "./db-query-bounded";
4
+ import {
5
+ assertSingleStatement,
6
+ DbQueryConcurrencyCapError,
7
+ type DbQueryResult,
8
+ DbQueryTimeoutError,
9
+ executeReadOnlyQuery,
10
+ getDbQueryHttpBudgetMs,
11
+ getDbQueryHttpMaxRows,
12
+ isDbQueryBoundedEnabled,
13
+ stripTrailingSemicolon,
14
+ warnDbQueryBoundedDisabledOnce,
15
+ } from "./db-query-shared";
4
16
  import { route } from "./route-def";
5
17
  import { json, jsonError } from "./utils";
6
18
 
7
- export interface DbQueryResult {
8
- columns: string[];
9
- rows: unknown[][];
10
- elapsed: number;
11
- total: number;
12
- }
19
+ export type { DbQueryResult } from "./db-query-shared";
20
+ export {
21
+ assertSingleStatement,
22
+ executeReadOnlyQuery,
23
+ getDbQueryHttpBudgetMs,
24
+ getDbQueryHttpMaxRows,
25
+ getDbQueryMcpBudgetMs,
26
+ isDbQueryBoundedEnabled,
27
+ } from "./db-query-shared";
13
28
 
14
29
  export const DbQueryInputShape = {
15
30
  sql: z.string().min(1).max(10_000).optional(),
@@ -29,17 +44,6 @@ export function resolveDbQuerySql(input: Pick<DbQueryInput, "sql" | "query">): s
29
44
  return input.sql ?? input.query ?? "";
30
45
  }
31
46
 
32
- function stripTrailingSemicolon(sql: string): string {
33
- return sql.trim().replace(/;\s*$/, "").trim();
34
- }
35
-
36
- function assertSingleStatement(sql: string): void {
37
- const stripped = stripTrailingSemicolon(sql);
38
- if (stripped.includes(";")) {
39
- throw new Error("Only one SQL statement is allowed");
40
- }
41
- }
42
-
43
47
  export function assertSelectOnlyQuery(sql: string): void {
44
48
  assertSingleStatement(sql);
45
49
  const normalized = stripTrailingSemicolon(sql).toLowerCase();
@@ -49,34 +53,23 @@ export function assertSelectOnlyQuery(sql: string): void {
49
53
  }
50
54
 
51
55
  /**
52
- * Execute a read-only SQL query against the swarm database.
53
- * Detects write statements via bun:sqlite's columnNames (empty for INSERT/UPDATE/DELETE/DROP).
56
+ * Gate in front of the bounded executor (Fix 1). `DB_QUERY_BOUNDED_ENABLED`
57
+ * (default on) picks the path: enabled runs the bounded child-process
58
+ * executor unchanged; disabled restores the pre-fix synchronous path with no
59
+ * wall-clock budget and logs a one-time warning, so turning off the
60
+ * protection can't happen silently.
54
61
  */
55
- export function executeReadOnlyQuery(
62
+ export async function executeReadOnlyQueryGated(
56
63
  sql: string,
57
64
  params: unknown[] = [],
65
+ budgetMs: number,
58
66
  maxRows?: number,
59
- ): DbQueryResult {
60
- assertSingleStatement(sql);
61
- const stmt = getDb().prepare(sql);
62
-
63
- // bun:sqlite: columnNames is empty for write statements, populated for SELECT/PRAGMA/EXPLAIN
64
- if (stmt.columnNames.length === 0) {
65
- throw new Error("Only read-only queries are allowed");
67
+ ): Promise<DbQueryResult> {
68
+ if (isDbQueryBoundedEnabled()) {
69
+ return executeReadOnlyQueryBounded(sql, params, budgetMs, maxRows);
66
70
  }
67
-
68
- const columns = stmt.columnNames as string[];
69
- const start = performance.now();
70
- const rows = (params.length > 0 ? stmt.all(...(params as [string])) : stmt.all()) as Record<
71
- string,
72
- unknown
73
- >[];
74
- const elapsed = Math.round(performance.now() - start);
75
-
76
- const capped = maxRows ? rows.slice(0, maxRows) : rows;
77
- const rowArrays = capped.map((row) => columns.map((col) => row[col]));
78
-
79
- return { columns, rows: rowArrays, elapsed, total: rows.length };
71
+ warnDbQueryBoundedDisabledOnce();
72
+ return executeReadOnlyQuery(sql, params, maxRows);
80
73
  }
81
74
 
82
75
  const dbQueryRoute = route({
@@ -97,6 +90,8 @@ const dbQueryRoute = route({
97
90
  }),
98
91
  },
99
92
  400: { description: "Invalid or disallowed SQL" },
93
+ 408: { description: "Query exceeded its wall-clock budget and was terminated" },
94
+ 429: { description: "Too many concurrent bounded db-query executions; retry shortly" },
100
95
  },
101
96
  auth: { apiKey: true },
102
97
  });
@@ -115,9 +110,26 @@ export async function handleDbQuery(
115
110
  if (!parsed) return true;
116
111
 
117
112
  try {
118
- const result = executeReadOnlyQuery(resolveDbQuerySql(parsed.body), parsed.body.params);
113
+ const result = await executeReadOnlyQueryGated(
114
+ resolveDbQuerySql(parsed.body),
115
+ parsed.body.params,
116
+ getDbQueryHttpBudgetMs(),
117
+ getDbQueryHttpMaxRows(),
118
+ );
119
119
  json(res, result);
120
120
  } catch (err: unknown) {
121
+ if (err instanceof DbQueryConcurrencyCapError) {
122
+ // Machine-readable code + Retry-After so a caller can back off instead
123
+ // of treating this like a malformed request (the pre-existing generic
124
+ // 400 every other error path here returns).
125
+ res.setHeader("Retry-After", "1");
126
+ json(res, { error: "db_query_concurrency_cap", message: err.message }, 429);
127
+ return true;
128
+ }
129
+ if (err instanceof DbQueryTimeoutError) {
130
+ json(res, { error: "db_query_timeout", message: err.message }, 408);
131
+ return true;
132
+ }
121
133
  const message = err instanceof Error ? err.message : String(err);
122
134
  jsonError(res, message);
123
135
  }
@@ -82,6 +82,7 @@ const getEventsRoute = route({
82
82
  agentId: z.string().optional(),
83
83
  taskId: z.string().optional(),
84
84
  sessionId: z.string().optional(),
85
+ dataField: z.string().optional(),
85
86
  since: z.string().optional(),
86
87
  until: z.string().optional(),
87
88
  limit: z.coerce.number().int().min(1).max(1000).optional(),
@@ -191,6 +192,7 @@ export async function handleEvents(
191
192
  agentId: parsed.query.agentId || undefined,
192
193
  taskId: parsed.query.taskId || undefined,
193
194
  sessionId: parsed.query.sessionId || undefined,
195
+ dataField: parsed.query.dataField || undefined,
194
196
  since: parsed.query.since || undefined,
195
197
  until: parsed.query.until || undefined,
196
198
  limit: parsed.query.limit ?? 100,
package/src/http/index.ts CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  withRemoteContext,
32
32
  withSpanContext,
33
33
  } from "../otel";
34
+ import { startQueueStallAlarm, stopQueueStallAlarm } from "../queue-stall-alarm";
34
35
  import { clearAuditSink, isRbacEnabled, setAuditSink } from "../rbac";
35
36
  import { startScriptRunSupervisor, stopScriptRunSupervisor } from "../script-workflows/supervisor";
36
37
  import { getServerSessionsProcessed } from "../server-runtime-counters";
@@ -424,6 +425,9 @@ async function shutdown() {
424
425
  // Stop heartbeat triage
425
426
  stopHeartbeat();
426
427
 
428
+ // Stop the out-of-band queue alarm before disconnecting its Slack notifier.
429
+ stopQueueStallAlarm();
430
+
427
431
  // Stop durable script workflow subprocesses
428
432
  stopScriptRunSupervisor();
429
433
 
@@ -485,13 +489,26 @@ async function shutdown() {
485
489
  // Only register signal handlers once (avoid duplicates on hot reload)
486
490
  if (!globalState.__sigintRegistered) {
487
491
  globalState.__sigintRegistered = true;
492
+ // A rejected shutdown must not become an unhandled rejection: the signal
493
+ // handler is the last code that runs, so an error here is the only chance to
494
+ // learn why the process failed to exit cleanly.
488
495
  process.on("SIGINT", () => {
489
496
  shutdownSignal = "SIGINT";
490
- shutdown();
497
+ shutdown().catch((err) =>
498
+ console.error(
499
+ "[shutdown] SIGINT shutdown failed:",
500
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
501
+ ),
502
+ );
491
503
  });
492
504
  process.on("SIGTERM", () => {
493
505
  shutdownSignal = "SIGTERM";
494
- shutdown();
506
+ shutdown().catch((err) =>
507
+ console.error(
508
+ "[shutdown] SIGTERM shutdown failed:",
509
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
510
+ ),
511
+ );
495
512
  });
496
513
  }
497
514
 
@@ -608,6 +625,9 @@ httpServer
608
625
  // Start Slack bot (if configured)
609
626
  await startSlackApp();
610
627
 
628
+ // Independent of workers, scheduler targets, and heartbeat agent tasks.
629
+ startQueueStallAlarm();
630
+
611
631
  // Initialize GitHub webhook handler (if configured)
612
632
  initGitHub();
613
633
 
@@ -1,10 +1,16 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import {
4
+ type AnySchema,
5
+ getParseErrorMessage,
6
+ normalizeObjectSchema,
7
+ safeParseAsync,
8
+ } from "@modelcontextprotocol/sdk/server/zod-compat.js";
3
9
  import { z } from "zod";
4
10
  import { createServer } from "@/server";
5
11
  import { isMcpToolAllowedForScripts } from "../scripts-runtime/sdk-allowlist";
6
12
  import { markScriptSdkRequestOrigin } from "../tools/utils";
7
- import { route } from "./route-def";
13
+ import { route, runtimeInstanceHeader } from "./route-def";
8
14
  import { json, jsonError } from "./utils";
9
15
 
10
16
  // Lazy singleton — created once on first bridge call to avoid boot-time cost.
@@ -23,7 +29,7 @@ function getBridgeServer(): McpServer {
23
29
 
24
30
  type RegisteredTool = {
25
31
  handler: (argsOrExtra: unknown, extra?: unknown) => unknown | Promise<unknown>;
26
- inputSchema?: unknown;
32
+ inputSchema?: AnySchema;
27
33
  enabled?: boolean;
28
34
  };
29
35
 
@@ -35,6 +41,7 @@ const mcpBridgeRoute = route({
35
41
  pattern: ["api", "mcp-bridge"],
36
42
  summary: "Generic MCP tool proxy for the scripts SDK bridge",
37
43
  tags: ["Scripts"],
44
+ headers: runtimeInstanceHeader("acquire work through bridged tools"),
38
45
  body: z.object({
39
46
  tool: z.string().min(1).max(200),
40
47
  args: z.record(z.string(), z.unknown()).default({}),
@@ -87,6 +94,11 @@ export async function handleMcpBridge(
87
94
  const sourceTaskId = Array.isArray(req.headers["x-source-task-id"])
88
95
  ? req.headers["x-source-task-id"][0]
89
96
  : (req.headers["x-source-task-id"] as string | undefined);
97
+ // Runtime identity rides the bridge like the agent identity so the
98
+ // work-acquisition gates in bridged tools see the invoking worker process.
99
+ const runtimeInstanceId = Array.isArray(req.headers["x-runtime-instance-id"])
100
+ ? req.headers["x-runtime-instance-id"][0]
101
+ : (req.headers["x-runtime-instance-id"] as string | undefined);
90
102
 
91
103
  const extra = markScriptSdkRequestOrigin({
92
104
  sessionId: "mcp-bridge",
@@ -94,13 +106,35 @@ export async function handleMcpBridge(
94
106
  headers: {
95
107
  "x-agent-id": myAgentId ?? "",
96
108
  ...(sourceTaskId ? { "x-source-task-id": sourceTaskId } : {}),
109
+ ...(runtimeInstanceId ? { "x-runtime-instance-id": runtimeInstanceId } : {}),
97
110
  },
98
111
  },
99
112
  });
100
113
 
114
+ // Mirror the SDK's own tools/call validation (`validateToolInput` in
115
+ // @modelcontextprotocol/sdk server/mcp.js). The bridge bypasses the MCP
116
+ // transport, so without this parse raw script args reach handlers
117
+ // unchecked (2026-08-18 priority='high' incident) and the schema's
118
+ // `.default()`/`.transform()` never apply.
119
+ let handlerArgs: unknown = args;
120
+ if (tool.inputSchema) {
121
+ const inputObj = normalizeObjectSchema(tool.inputSchema);
122
+ const parseResult = await safeParseAsync(inputObj ?? tool.inputSchema, args);
123
+ if (!parseResult.success) {
124
+ const parseError = "error" in parseResult ? parseResult.error : "Unknown error";
125
+ jsonError(
126
+ res,
127
+ `Invalid arguments for tool '${toolName}': ${getParseErrorMessage(parseError)}`,
128
+ 400,
129
+ );
130
+ return true;
131
+ }
132
+ handlerArgs = parseResult.data;
133
+ }
134
+
101
135
  try {
102
136
  const result = tool.inputSchema
103
- ? await Promise.resolve(tool.handler(args, extra))
137
+ ? await Promise.resolve(tool.handler(handlerArgs, extra))
104
138
  : await Promise.resolve(tool.handler(extra));
105
139
 
106
140
  if (result && typeof result === "object" && "structuredContent" in result) {
package/src/http/mcp.ts CHANGED
@@ -118,6 +118,18 @@ export async function handleMcp(
118
118
  }
119
119
  const body = JSON.parse(Buffer.concat(chunks).toString());
120
120
 
121
+ if (!sessionId && body.method === "server/discover") {
122
+ res.writeHead(200, { "Content-Type": "application/json" });
123
+ res.end(
124
+ JSON.stringify({
125
+ jsonrpc: "2.0",
126
+ error: { code: -32601, message: "Method not found" },
127
+ id: body.id ?? null,
128
+ }),
129
+ );
130
+ return true;
131
+ }
132
+
121
133
  let transport: StreamableHTTPServerTransport;
122
134
 
123
135
  if (sessionId && transports[sessionId]) {
@@ -21,6 +21,7 @@ import { getRetrievalsForAgent, hasRetrievalForTask } from "../be/memory/retriev
21
21
  import { getUsefulnessStats } from "../be/memory/usefulness-stats";
22
22
  import { shouldPersistAutomaticTaskMemory } from "../memory/automatic-task-gate";
23
23
  import { AgentMemorySchema, AgentMemoryScopeSchema, AgentMemorySourceSchema } from "../types";
24
+ import { scrubSecrets } from "../utils/secret-scrubber";
24
25
  import { route } from "./route-def";
25
26
  import { jsonError, parseQueryParams } from "./utils";
26
27
 
@@ -725,7 +726,12 @@ export async function handleMemory(
725
726
  } catch (err) {
726
727
  console.error("[memory] Batch embedding failed:", (err as Error).message);
727
728
  }
728
- })();
729
+ })().catch((err) =>
730
+ console.error(
731
+ "[memory] batch embed failed:",
732
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
733
+ ),
734
+ );
729
735
 
730
736
  indexMemory.respond(res, 202, { queued: true, memoryIds: memories.map((m) => m.id) });
731
737
  return true;
@@ -1041,7 +1047,12 @@ export async function handleMemory(
1041
1047
  }
1042
1048
  }
1043
1049
  console.log(`[memory] Re-embedding complete: ${memories.length} memories`);
1044
- })();
1050
+ })().catch((err) =>
1051
+ console.error(
1052
+ "[memory] re-embed batch failed:",
1053
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
1054
+ ),
1055
+ );
1045
1056
 
1046
1057
  return true;
1047
1058
  }
@@ -65,6 +65,7 @@ export function generateOpenApiSpec(opts: OpenApiOptions): string {
65
65
  const request: Record<string, unknown> = {};
66
66
  if (routeDef.params) request.params = routeDef.params;
67
67
  if (routeDef.query) request.query = routeDef.query;
68
+ if (routeDef.headers) request.headers = routeDef.headers;
68
69
  if (routeDef.body) {
69
70
  request.body = {
70
71
  content: { "application/json": { schema: routeDef.body } },
package/src/http/poll.ts CHANGED
@@ -23,9 +23,11 @@ import {
23
23
  hasCapacity,
24
24
  recordBudgetRefusalNotification,
25
25
  startTask,
26
+ updateAgentStatusFromCapacity,
26
27
  upsertChannelActivityCursor,
27
28
  } from "../be/db";
28
29
  import { renderIdentity, resolveIdentity } from "../be/identity";
30
+ import { touchRuntimeInstance } from "../be/multi-runtime";
29
31
  import { hasCapability } from "../server";
30
32
  import { fetchChannelActivity } from "../slack/channel-activity";
31
33
  import { telemetry } from "../telemetry";
@@ -33,9 +35,12 @@ import {
33
35
  AgentTaskSchema,
34
36
  BudgetRefusedTriggerSchema,
35
37
  TaskAttachmentSchema,
38
+ UserCommsPrefsSchema,
36
39
  UserSchema,
37
40
  } from "../types";
38
- import { route } from "./route-def";
41
+ import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
42
+ import { getUserCommsPrefs } from "../utils/requester-comms";
43
+ import { route, runtimeInstanceHeader } from "./route-def";
39
44
  import { jsonError } from "./utils";
40
45
 
41
46
  // ─── Budget-refused trigger envelope ────────────────────────────────────────
@@ -93,12 +98,16 @@ const PollRequestedBySchema = UserSchema.pick({
93
98
  email: true,
94
99
  role: true,
95
100
  notes: true,
101
+ }).extend({
102
+ // Structured communication preferences from `users.metadata.comms`.
103
+ comms: UserCommsPrefsSchema.optional(),
96
104
  });
97
105
 
98
106
  const PollTaskOfferedTriggerSchema = z.object({
99
107
  type: z.literal("task_offered"),
100
108
  taskId: z.string(),
101
109
  task: AgentTaskSchema,
110
+ requestedBy: PollRequestedBySchema.optional(),
102
111
  });
103
112
 
104
113
  const PollTaskAssignedTriggerSchema = z.object({
@@ -155,6 +164,7 @@ const pollTriggers = route({
155
164
  summary: "Poll for triggers (tasks, mentions)",
156
165
  tags: ["Poll"],
157
166
  auth: { apiKey: true, agentId: true },
167
+ headers: runtimeInstanceHeader("poll for work"),
158
168
  responses: {
159
169
  200: { description: "Trigger data or null", schema: pollResponseSchema },
160
170
  400: { description: "Missing X-Agent-ID" },
@@ -171,6 +181,33 @@ function getRequesterNotes(notes: string | undefined): string | undefined {
171
181
  return typeof notes === "string" && notes.trim().length > 0 ? notes : undefined;
172
182
  }
173
183
 
184
+ /**
185
+ * Resolve the requester projection for a task trigger. If the task carries a
186
+ * machine-recorded external id (today: the Slack user field) but no
187
+ * requestedByUserId, render the explicit UNKNOWN sentinel instead of silently
188
+ * omitting the section: never a substituted human (Rule 33 /
189
+ * provenance-or-silence).
190
+ */
191
+ function buildTriggerRequestedBy(task: {
192
+ requestedByUserId?: string | null;
193
+ slackUserId?: string | null;
194
+ }): z.infer<typeof PollRequestedBySchema> | undefined {
195
+ const user = task.requestedByUserId ? getUserById(task.requestedByUserId) : undefined;
196
+ if (user) {
197
+ return {
198
+ name: user.name,
199
+ email: user.email,
200
+ role: user.role,
201
+ notes: getRequesterNotes(user.notes),
202
+ comms: getUserCommsPrefs(user),
203
+ };
204
+ }
205
+ if (task.slackUserId) {
206
+ return { name: renderIdentity(resolveIdentity("slack", task.slackUserId)) };
207
+ }
208
+ return undefined;
209
+ }
210
+
174
211
  /**
175
212
  * Slim attachment projection for the `task_assigned` poll trigger — just
176
213
  * enough (id, name, mimeType, sizeBytes) for the worker to build a one-shot
@@ -223,6 +260,9 @@ export async function handlePoll(
223
260
  queryParams: URLSearchParams,
224
261
  myAgentId: string | undefined,
225
262
  ): Promise<boolean> {
263
+ const runtimeInstanceId = ((h) => (Array.isArray(h) ? h[0] : h))(
264
+ req.headers["x-runtime-instance-id"],
265
+ );
226
266
  // Handle cursor commit endpoint
227
267
  if (commitCursorsRoute.match(req.method, pathSegments)) {
228
268
  const parsed = await commitCursorsRoute.parse(req, res, pathSegments, queryParams);
@@ -266,18 +306,37 @@ export async function handlePoll(
266
306
  return { error: "Agent not found", status: 404 };
267
307
  }
268
308
 
309
+ // A process whose runtime has been retired must not be handed work: it
310
+ // would execute alongside whatever replaced it. Dispatch is gated on a
311
+ // live runtime identity rather than on X-Agent-ID alone, which only
312
+ // names the logical agent. Polling is worker activity, so this also
313
+ // refreshes liveness — it cannot revive a retired runtime, since the
314
+ // update only matches one that is still live.
315
+ if (
316
+ isMultiRuntimeEnabled() &&
317
+ !(runtimeInstanceId && touchRuntimeInstance(runtimeInstanceId, agent.id))
318
+ ) {
319
+ return { trigger: null };
320
+ }
321
+
269
322
  // Check for offered tasks first (highest priority for both workers and leads)
270
- // Atomically claim the task for review to prevent duplicate processing
271
- const offeredTasks = getOfferedTasksForAgent(myAgentId);
323
+ // Atomically claim the task for review to prevent duplicate processing.
324
+ // Capacity is checked in the same transaction as the claim: with
325
+ // several runtimes serving one agent these polls run concurrently, and
326
+ // an unguarded claim here would let each of them take a task past the
327
+ // agent's logical limit.
328
+ const offeredTasks = hasCapacity(myAgentId) ? getOfferedTasksForAgent(myAgentId) : [];
272
329
  const firstOfferedTask = offeredTasks[0];
273
330
  if (firstOfferedTask) {
274
331
  const claimedTask = claimOfferedTask(firstOfferedTask.id, myAgentId);
275
332
  if (claimedTask) {
333
+ const offeredRequestedBy = buildTriggerRequestedBy(claimedTask);
276
334
  return {
277
335
  trigger: {
278
336
  type: "task_offered",
279
337
  taskId: claimedTask.id,
280
338
  task: claimedTask,
339
+ ...(offeredRequestedBy && { requestedBy: offeredRequestedBy }),
281
340
  },
282
341
  };
283
342
  }
@@ -337,6 +396,7 @@ export async function handlePoll(
337
396
 
338
397
  // Mark task as in_progress immediately to prevent duplicate polling
339
398
  startTask(pendingTask.id);
399
+ updateAgentStatusFromCapacity(myAgentId);
340
400
 
341
401
  ensure({
342
402
  id: "started",
@@ -360,19 +420,9 @@ export async function handlePoll(
360
420
  agentId: myAgentId,
361
421
  });
362
422
 
363
- // Resolve requesting user if available. If the task carries a
364
- // machine-recorded external id (today: the Slack user field) but
365
- // no requestedByUserId, render the explicit UNKNOWN sentinel
366
- // instead of silently omitting the section — never a substituted
367
- // human (Rule 33 / provenance-or-silence).
368
- const requestedByUser = pendingTask.requestedByUserId
369
- ? getUserById(pendingTask.requestedByUserId)
370
- : undefined;
371
- const requestedByNotes = getRequesterNotes(requestedByUser?.notes);
372
- const requestedByUnknownName =
373
- !requestedByUser && pendingTask.slackUserId
374
- ? renderIdentity(resolveIdentity("slack", pendingTask.slackUserId))
375
- : undefined;
423
+ // Resolve requesting user if available (UNKNOWN sentinel handling
424
+ // lives in buildTriggerRequestedBy).
425
+ const assignedRequestedBy = buildTriggerRequestedBy(pendingTask);
376
426
 
377
427
  return {
378
428
  trigger: {
@@ -383,17 +433,7 @@ export async function handlePoll(
383
433
  status: "in_progress",
384
434
  attachments: attachmentsForTrigger(pendingTask.id),
385
435
  },
386
- ...(requestedByUser && {
387
- requestedBy: {
388
- name: requestedByUser.name,
389
- email: requestedByUser.email,
390
- role: requestedByUser.role,
391
- notes: requestedByNotes,
392
- },
393
- }),
394
- ...(requestedByUnknownName && {
395
- requestedBy: { name: requestedByUnknownName },
396
- }),
436
+ ...(assignedRequestedBy && { requestedBy: assignedRequestedBy }),
397
437
  },
398
438
  };
399
439
  }
@@ -499,16 +539,19 @@ export async function handlePoll(
499
539
  for (const candidateId of unassignedIds) {
500
540
  const claimed = claimTask(candidateId, myAgentId);
501
541
  if (claimed) {
542
+ updateAgentStatusFromCapacity(myAgentId);
502
543
  telemetry.taskEvent("claimed", {
503
544
  taskId: claimed.id,
504
545
  source: claimed.source,
505
546
  agentId: myAgentId,
506
547
  });
548
+ const claimedRequestedBy = buildTriggerRequestedBy(claimed);
507
549
  return {
508
550
  trigger: {
509
551
  type: "task_assigned",
510
552
  taskId: claimed.id,
511
553
  task: { ...claimed, attachments: attachmentsForTrigger(claimed.id) },
554
+ ...(claimedRequestedBy && { requestedBy: claimedRequestedBy }),
512
555
  },
513
556
  };
514
557
  }
@@ -1,6 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import type { PermissionVerb } from "../rbac";
4
+ import { scrubSecrets } from "../utils/secret-scrubber";
4
5
  import { jsonError, matchRoute, parseBody } from "./utils";
5
6
 
6
7
  // ─── Types ───────────────────────────────────────────────────────────────────
@@ -25,6 +26,24 @@ type ResponsesDef = Record<number, RouteResponseDef>;
25
26
  /** Extracts the payload type respond() accepts for one response def. */
26
27
  type ResponseData<R> = R extends { schema: infer S extends z.ZodType } ? z.input<S> : never;
27
28
 
29
+ /**
30
+ * Optional `X-Runtime-Instance-ID` header shared by the lifecycle and poll
31
+ * routes. Optional in the schema because the requirement is conditional:
32
+ * only multi-runtime mode needs it, and legacy deployments must keep working
33
+ * without it.
34
+ */
35
+ export const runtimeInstanceHeader = (operation: string) =>
36
+ z.object({
37
+ "X-Runtime-Instance-ID": z
38
+ .string()
39
+ .optional()
40
+ .describe(
41
+ "Identifies the concrete runtime instance (worker process) making the call, " +
42
+ `as generated at its boot. Required to ${operation} when multi-runtime mode ` +
43
+ "(MULTI_RUNTIME_ENABLED) is on; ignored otherwise.",
44
+ ),
45
+ });
46
+
28
47
  export interface RouteDef<
29
48
  TParams extends z.ZodType = z.ZodType,
30
49
  TQuery extends z.ZodType = z.ZodType,
@@ -41,6 +60,14 @@ export interface RouteDef<
41
60
  tags: string[];
42
61
  params?: TParams;
43
62
  query?: TQuery;
63
+ /**
64
+ * Request headers to publish as OpenAPI parameters, beyond the global
65
+ * `Authorization`/`X-Agent-ID` security schemes. Declaration-only: handlers
66
+ * still read headers off `req`, so adding this to a route changes the
67
+ * generated spec and nothing else. Mark each property `.optional()` unless
68
+ * the header is unconditionally required.
69
+ */
70
+ headers?: z.ZodObject;
44
71
  body?: TBody;
45
72
  responses: TResponses;
46
73
  auth?: {
@@ -187,10 +214,14 @@ export function describeRequestRoute(
187
214
  // ─── Factory ─────────────────────────────────────────────────────────────────
188
215
 
189
216
  /**
190
- * Runtime response validation gate for `respond()`. NODE_ENV is deliberately
191
- * unset on prod deploys, so this is opt-in outside `bun test` (which sets
192
- * NODE_ENV=test): response schemas must never introduce a production failure
193
- * mode — they fail loudly in tests and (opt-in) local dev instead.
217
+ * Runtime response validation gate for `respond()`. Only `bun test`
218
+ * (NODE_ENV=test) fails hard on a violation: one bad row must never take an
219
+ * endpoint down outside tests (2026-08-18 incident — a single priority='high'
220
+ * row 500'd every task listing). When the gate is on outside tests
221
+ * (NODE_ENV=development, or VALIDATE_HTTP_RESPONSES=true), `respond()` logs
222
+ * the violation and sends the response anyway. Note: the compiled Bun binary
223
+ * defaults NODE_ENV to "development", so deploys must also set
224
+ * NODE_ENV=production explicitly (see the deploy-env change in the fix PR).
194
225
  */
195
226
  const VALIDATE_RESPONSES =
196
227
  process.env.NODE_ENV === "test" ||
@@ -216,12 +247,15 @@ export function route<
216
247
  if (schema) {
217
248
  const result = schema.safeParse(data);
218
249
  if (!result.success) {
219
- throw new Error(
250
+ const detail =
220
251
  `Response schema violation: ${def.method.toUpperCase()} ${def.path} ${code} — ` +
221
- result.error.issues
222
- .map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
223
- .join("; "),
224
- );
252
+ result.error.issues
253
+ .map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
254
+ .join("; ");
255
+ if (process.env.NODE_ENV === "test") {
256
+ throw new Error(detail);
257
+ }
258
+ console.error(`[route-def] ${scrubSecrets(detail)}`);
225
259
  }
226
260
  }
227
261
  }
@@ -40,7 +40,7 @@ const scheduleUpdateBodySchema = z.object({
40
40
  taskTemplate: z.string().optional(),
41
41
  taskType: z.string().optional(),
42
42
  tags: z.array(z.string()).optional(),
43
- priority: z.number().int().optional(),
43
+ priority: z.number().int().min(0).max(100).optional(),
44
44
  targetAgentId: z.string().nullable().optional(),
45
45
  enabled: z.boolean().optional(),
46
46
  timezone: z.string().optional(),
@@ -96,7 +96,7 @@ const createSchedule = route({
96
96
  taskTemplate: z.string().min(1).optional(),
97
97
  taskType: z.string().optional(),
98
98
  tags: z.array(z.string()).optional(),
99
- priority: z.number().int().optional(),
99
+ priority: z.number().int().min(0).max(100).optional(),
100
100
  targetAgentId: z.string().optional(),
101
101
  enabled: z.boolean().optional(),
102
102
  timezone: z.string().optional(),