@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
@@ -2,6 +2,7 @@ import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
3
  import { createServer as createHttpServer, type Server } from "node:http";
4
4
  import {
5
+ cancelApprovalRequestById,
5
6
  cancelPendingApprovalRequestsForRun,
6
7
  claimApprovalCancellationNotification,
7
8
  closeDb,
@@ -18,6 +19,9 @@ import {
18
19
  getApprovalRequestByStepId,
19
20
  getDbClient,
20
21
  getExpiredPendingApprovals,
22
+ getTaskById,
23
+ getWorkflowRun,
24
+ getWorkflowRunStep,
21
25
  initDb,
22
26
  listApprovalRequests,
23
27
  releaseApprovalCancellationNotificationClaim,
@@ -370,6 +374,26 @@ describe("Approval Requests", () => {
370
374
  expect(result!.resolvedAt).toBeTruthy();
371
375
  });
372
376
 
377
+ test("stores a resolutionReason when given", async () => {
378
+ const data = makeApprovalData();
379
+ await createApprovalRequest(data);
380
+
381
+ const result = await resolveApprovalRequest(data.id, {
382
+ status: "timeout",
383
+ resolutionReason: "x",
384
+ });
385
+ expect(result!.status).toBe("timeout");
386
+ expect(result!.resolutionReason).toBe("x");
387
+ });
388
+
389
+ test("leaves resolutionReason NULL when absent", async () => {
390
+ const data = makeApprovalData();
391
+ await createApprovalRequest(data);
392
+
393
+ const result = await resolveApprovalRequest(data.id, { status: "approved" });
394
+ expect(result!.resolutionReason).toBeNull();
395
+ });
396
+
373
397
  test("resolves a pending request to rejected", async () => {
374
398
  const data = makeApprovalData();
375
399
  await createApprovalRequest(data);
@@ -740,6 +764,78 @@ describe("Approval Requests", () => {
740
764
  });
741
765
  });
742
766
 
767
+ describe("DB: cancelApprovalRequestById", () => {
768
+ test("cancels a pending request with reason and actor", async () => {
769
+ const data = makeApprovalData();
770
+ await createApprovalRequest(data);
771
+
772
+ const result = await cancelApprovalRequestById(data.id, {
773
+ reason: "not needed",
774
+ resolvedBy: "user-1",
775
+ });
776
+ expect(result).not.toBeNull();
777
+ expect(result!.status).toBe("cancelled");
778
+ expect(result!.resolutionReason).toBe("not needed");
779
+ expect(result!.resolvedBy).toBe("user-1");
780
+ expect(result!.resolvedAt).toBeTruthy();
781
+ });
782
+
783
+ test("leaves resolvedBy NULL when no actor is given", async () => {
784
+ const data = makeApprovalData();
785
+ await createApprovalRequest(data);
786
+
787
+ const result = await cancelApprovalRequestById(data.id, {
788
+ reason: "sweep",
789
+ resolvedBy: null,
790
+ });
791
+ expect(result!.status).toBe("cancelled");
792
+ expect(result!.resolvedBy).toBeNull();
793
+ });
794
+
795
+ test("a second call returns null and changes nothing", async () => {
796
+ const data = makeApprovalData();
797
+ await createApprovalRequest(data);
798
+ const first = await cancelApprovalRequestById(data.id, { reason: "first", resolvedBy: null });
799
+
800
+ const second = await cancelApprovalRequestById(data.id, {
801
+ reason: "second",
802
+ resolvedBy: "user-2",
803
+ });
804
+ expect(second).toBeNull();
805
+ const row = await getApprovalRequestById(data.id);
806
+ expect(row!.resolutionReason).toBe("first");
807
+ expect(row!.resolvedBy).toBeNull();
808
+ expect(row!.resolvedAt).toBe(first!.resolvedAt);
809
+ });
810
+
811
+ test("returns null for approved and timeout requests", async () => {
812
+ const approved = makeApprovalData();
813
+ await createApprovalRequest(approved);
814
+ await resolveApprovalRequest(approved.id, { status: "approved" });
815
+ const timedOut = makeApprovalData();
816
+ await createApprovalRequest(timedOut);
817
+ await resolveApprovalRequest(timedOut.id, { status: "timeout" });
818
+
819
+ expect(
820
+ await cancelApprovalRequestById(approved.id, { reason: "x", resolvedBy: null }),
821
+ ).toBeNull();
822
+ expect(
823
+ await cancelApprovalRequestById(timedOut.id, { reason: "x", resolvedBy: null }),
824
+ ).toBeNull();
825
+ expect((await getApprovalRequestById(approved.id))!.status).toBe("approved");
826
+ expect((await getApprovalRequestById(timedOut.id))!.status).toBe("timeout");
827
+ });
828
+
829
+ test("a cancelled row matches the Slack cancellation claim", async () => {
830
+ const data = makeApprovalData();
831
+ await createApprovalRequest(data);
832
+ await cancelApprovalRequestById(data.id, { reason: "gone", resolvedBy: null });
833
+
834
+ const claim = await claimApprovalCancellationNotification(data.id, "slack:C1:1.0");
835
+ expect(claim).not.toBeNull();
836
+ });
837
+ });
838
+
743
839
  describe("DB: getExpiredPendingApprovals", () => {
744
840
  test("returns empty for non-expired requests", async () => {
745
841
  // All our test requests with timeout have expiresAt in the future
@@ -1538,4 +1634,303 @@ describe("Approval Requests", () => {
1538
1634
  expect(fetched!.notificationChannels).toEqual(updatedChannels);
1539
1635
  });
1540
1636
  });
1637
+
1638
+ describe("HTTP (production handler): late answers and cancel", () => {
1639
+ let prodServer: Server;
1640
+ let prodUrl = "";
1641
+ let lead: { id: string };
1642
+ let owner: { id: string };
1643
+ let worker: { id: string };
1644
+
1645
+ beforeAll(async () => {
1646
+ prodServer = createProductionApprovalServer();
1647
+ prodUrl = `http://127.0.0.1:${await listenOnFreePort(prodServer)}`;
1648
+ lead = await createAgent({ name: "cancel-lead", isLead: true, status: "idle" });
1649
+ owner = await createAgent({ name: "cancel-owner", isLead: false, status: "idle" });
1650
+ worker = await createAgent({ name: "cancel-other", isLead: false, status: "idle" });
1651
+ });
1652
+
1653
+ afterAll(async () => {
1654
+ await new Promise<void>((resolve) => prodServer.close(() => resolve()));
1655
+ });
1656
+
1657
+ async function post(path: string, body: unknown, agentId?: string) {
1658
+ const response = await fetch(`${prodUrl}${path}`, {
1659
+ method: "POST",
1660
+ headers: {
1661
+ "Content-Type": "application/json",
1662
+ ...(agentId ? { "X-Agent-ID": agentId } : {}),
1663
+ },
1664
+ body: JSON.stringify(body),
1665
+ });
1666
+ return { status: response.status, body: (await response.json()) as Record<string, any> };
1667
+ }
1668
+
1669
+ async function sourceTask(status = "in_progress") {
1670
+ const task = await createTaskExtended(`approval source ${crypto.randomUUID()}`, {
1671
+ agentId: owner.id,
1672
+ source: "mcp",
1673
+ });
1674
+ await getDbClient().run("UPDATE agent_tasks SET status = ? WHERE id = ?", [status, task.id]);
1675
+ return task.id;
1676
+ }
1677
+
1678
+ async function followUps(parentTaskId: string) {
1679
+ return getDbClient().query<{ task: string }>(
1680
+ "SELECT task FROM agent_tasks WHERE taskType = 'hitl-follow-up' AND parentTaskId = ?",
1681
+ [parentTaskId],
1682
+ );
1683
+ }
1684
+
1685
+ async function setTimes(id: string, times: { expiresAt?: string | null; createdAt?: string }) {
1686
+ if (times.expiresAt !== undefined) {
1687
+ await getDbClient().run("UPDATE approval_requests SET expiresAt = ? WHERE id = ?", [
1688
+ times.expiresAt,
1689
+ id,
1690
+ ]);
1691
+ }
1692
+ if (times.createdAt) {
1693
+ await getDbClient().run("UPDATE approval_requests SET createdAt = ? WHERE id = ?", [
1694
+ times.createdAt,
1695
+ id,
1696
+ ]);
1697
+ }
1698
+ }
1699
+
1700
+ const approve = { responses: { q1: { approved: true } } };
1701
+
1702
+ test("a respond on a standalone request still creates the hitl.follow_up task", async () => {
1703
+ const taskId = await sourceTask();
1704
+ const data = makeApprovalData({ sourceTaskId: taskId });
1705
+ await createApprovalRequest(data);
1706
+
1707
+ const res = await post(`/api/approval-requests/${data.id}/respond`, approve);
1708
+
1709
+ expect(res.status).toBe(200);
1710
+ const tasks = await followUps(taskId);
1711
+ expect(tasks).toHaveLength(1);
1712
+ expect(tasks[0]!.task).toContain("Human responded to your approval request");
1713
+ });
1714
+
1715
+ test("a respond still creates the hitl.follow_up task after the source task completes", async () => {
1716
+ const taskId = await sourceTask("completed");
1717
+ const data = makeApprovalData({ sourceTaskId: taskId });
1718
+ await createApprovalRequest(data);
1719
+
1720
+ const res = await post(`/api/approval-requests/${data.id}/respond`, approve);
1721
+
1722
+ expect(res.status).toBe(200);
1723
+ expect(res.body.approvalRequest.status).toBe("approved");
1724
+ const tasks = await followUps(taskId);
1725
+ expect(tasks).toHaveLength(1);
1726
+ expect(tasks[0]!.task).toContain("Human responded to your approval request");
1727
+ });
1728
+
1729
+ test("a late answer gets 409 and the request becomes timeout", async () => {
1730
+ const taskId = await sourceTask();
1731
+ const data = makeApprovalData({ sourceTaskId: taskId });
1732
+ await createApprovalRequest(data);
1733
+ await setTimes(data.id, { expiresAt: new Date(Date.now() - 60_000).toISOString() });
1734
+
1735
+ const res = await post(`/api/approval-requests/${data.id}/respond`, approve);
1736
+
1737
+ expect(res.status).toBe(409);
1738
+ expect(res.body.error).toContain("Approval request expired at");
1739
+ const row = await getApprovalRequestById(data.id);
1740
+ expect(row!.status).toBe("timeout");
1741
+ expect(row!.responses).toBeNull();
1742
+ expect(row!.resolutionReason).toStartWith("Timed out: the answer arrived after the deadline");
1743
+ const tasks = await followUps(taskId);
1744
+ expect(tasks).toHaveLength(1);
1745
+ expect(tasks[0]!.task).toContain("timed out with no answer");
1746
+ expect(tasks[0]!.task).not.toContain("Human responded");
1747
+
1748
+ const again = await post(`/api/approval-requests/${data.id}/respond`, approve);
1749
+ expect(again.status).toBe(409);
1750
+ expect(again.body.error).toContain("already resolved with status: timeout");
1751
+ });
1752
+
1753
+ test("an answer before expiresAt is accepted", async () => {
1754
+ const data = makeApprovalData({ timeoutSeconds: 3600 });
1755
+ await createApprovalRequest(data);
1756
+ const res = await post(`/api/approval-requests/${data.id}/respond`, approve);
1757
+ expect(res.status).toBe(200);
1758
+ expect(res.body.approvalRequest.status).toBe("approved");
1759
+ });
1760
+
1761
+ test("age alone never refuses an answer", async () => {
1762
+ const data = makeApprovalData();
1763
+ await createApprovalRequest(data);
1764
+ await setTimes(data.id, { createdAt: new Date(Date.now() - 100 * 86_400_000).toISOString() });
1765
+ const res = await post(`/api/approval-requests/${data.id}/respond`, approve);
1766
+ expect(res.status).toBe(200);
1767
+ });
1768
+
1769
+ test("cancel sets cancelled with reason and caller, then is idempotent", async () => {
1770
+ const data = makeApprovalData();
1771
+ await createApprovalRequest(data);
1772
+
1773
+ const first = await post(
1774
+ `/api/approval-requests/${data.id}/cancel`,
1775
+ { reason: "no longer needed" },
1776
+ lead.id,
1777
+ );
1778
+ expect(first.status).toBe(200);
1779
+ expect(first.body.approvalRequest.status).toBe("cancelled");
1780
+ expect(first.body.approvalRequest.resolutionReason).toBe("no longer needed");
1781
+ expect(first.body.approvalRequest.resolvedBy).toBe(lead.id);
1782
+ expect(first.body.alreadyCancelled).toBe(false);
1783
+ expect(first.body.runCancelled).toBe(false);
1784
+
1785
+ const second = await post(`/api/approval-requests/${data.id}/cancel`, {}, lead.id);
1786
+ expect(second.status).toBe(200);
1787
+ expect(second.body.alreadyCancelled).toBe(true);
1788
+ });
1789
+
1790
+ test("cancel on a sweep-closed request reports alreadyCancelled", async () => {
1791
+ const data = makeApprovalData();
1792
+ await createApprovalRequest(data);
1793
+ await cancelApprovalRequestById(data.id, {
1794
+ reason: "Auto-cancelled by the approval sweep after 7 days with no response",
1795
+ resolvedBy: null,
1796
+ });
1797
+
1798
+ const res = await post(`/api/approval-requests/${data.id}/cancel`, {}, lead.id);
1799
+ expect(res.status).toBe(200);
1800
+ expect(res.body.alreadyCancelled).toBe(true);
1801
+
1802
+ const respond = await post(`/api/approval-requests/${data.id}/respond`, approve);
1803
+ expect(respond.status).toBe(409);
1804
+ expect(respond.body.error).toContain(
1805
+ "Auto-cancelled by the approval sweep after 7 days with no response",
1806
+ );
1807
+ });
1808
+
1809
+ test("cancel on approved or timeout requests returns 409", async () => {
1810
+ for (const status of ["approved", "timeout"] as const) {
1811
+ const data = makeApprovalData();
1812
+ await createApprovalRequest(data);
1813
+ await resolveApprovalRequest(data.id, { status });
1814
+ const res = await post(`/api/approval-requests/${data.id}/cancel`, {}, lead.id);
1815
+ expect(res.status).toBe(409);
1816
+ }
1817
+ });
1818
+
1819
+ test("cancel on an unknown id returns 404", async () => {
1820
+ const res = await post(`/api/approval-requests/${crypto.randomUUID()}/cancel`, {}, lead.id);
1821
+ expect(res.status).toBe(404);
1822
+ });
1823
+
1824
+ test("only a lead or the source task owner agent may cancel", async () => {
1825
+ const taskId = await sourceTask();
1826
+ const denied = makeApprovalData({ sourceTaskId: taskId });
1827
+ await createApprovalRequest(denied);
1828
+ expect((await post(`/api/approval-requests/${denied.id}/cancel`, {}, worker.id)).status).toBe(
1829
+ 403,
1830
+ );
1831
+ expect((await post(`/api/approval-requests/${denied.id}/cancel`, {}, owner.id)).status).toBe(
1832
+ 200,
1833
+ );
1834
+
1835
+ const byLead = makeApprovalData({ sourceTaskId: taskId });
1836
+ await createApprovalRequest(byLead);
1837
+ expect((await post(`/api/approval-requests/${byLead.id}/cancel`, {}, lead.id)).status).toBe(
1838
+ 200,
1839
+ );
1840
+ expect(await followUps(taskId)).toHaveLength(0);
1841
+ });
1842
+
1843
+ async function makeRun(status: "waiting" | "cancelled" | "completed") {
1844
+ const workflow = await createWorkflow({
1845
+ name: `approval-cancel-${crypto.randomUUID()}`,
1846
+ definition: { nodes: [] },
1847
+ });
1848
+ const runId = crypto.randomUUID();
1849
+ const stepId = crypto.randomUUID();
1850
+ await createWorkflowRun({ id: runId, workflowId: workflow.id });
1851
+ await createWorkflowRunStep({
1852
+ id: stepId,
1853
+ runId,
1854
+ nodeId: "approval",
1855
+ nodeType: "human-in-the-loop",
1856
+ });
1857
+ await updateWorkflowRunStep(stepId, { status: "waiting" });
1858
+ const approval = await createApprovalRequest(
1859
+ makeApprovalData({ workflowRunId: runId, workflowRunStepId: stepId }),
1860
+ );
1861
+ await updateWorkflowRun(runId, { status });
1862
+ return { runId, stepId, approvalId: approval.id };
1863
+ }
1864
+
1865
+ test("cancel on a waiting run cancels the run, its step, and its task", async () => {
1866
+ const { runId, stepId, approvalId } = await makeRun("waiting");
1867
+ const task = await createTaskExtended("step task", {
1868
+ agentId: owner.id,
1869
+ source: "mcp",
1870
+ workflowRunId: runId,
1871
+ workflowRunStepId: stepId,
1872
+ });
1873
+ await getDbClient().run("UPDATE agent_tasks SET status = 'in_progress' WHERE id = ?", [
1874
+ task.id,
1875
+ ]);
1876
+
1877
+ const res = await post(
1878
+ `/api/approval-requests/${approvalId}/cancel`,
1879
+ { reason: "stop the run" },
1880
+ lead.id,
1881
+ );
1882
+
1883
+ expect(res.status).toBe(200);
1884
+ expect(res.body.approvalRequest.status).toBe("cancelled");
1885
+ expect(res.body.approvalRequest.resolvedBy).toBe(lead.id);
1886
+ expect(res.body.runCancelled).toBe(true);
1887
+ const run = await getWorkflowRun(runId);
1888
+ expect(run!.status).toBe("cancelled");
1889
+ expect(run!.error).toBe("stop the run");
1890
+ expect((await getWorkflowRunStep(stepId))!.status).toBe("cancelled");
1891
+ expect((await getTaskById(task.id))!.status).toBe("cancelled");
1892
+ });
1893
+
1894
+ test("cancel on an expired pending request returns 409 and times it out", async () => {
1895
+ const taskId = await sourceTask();
1896
+ const data = makeApprovalData({ sourceTaskId: taskId });
1897
+ await createApprovalRequest(data);
1898
+ await setTimes(data.id, { expiresAt: new Date(Date.now() - 60_000).toISOString() });
1899
+
1900
+ const res = await post(`/api/approval-requests/${data.id}/cancel`, {}, lead.id);
1901
+
1902
+ expect(res.status).toBe(409);
1903
+ expect(res.body.error).toContain("Approval request expired at");
1904
+ const row = await getApprovalRequestById(data.id);
1905
+ expect(row!.status).toBe("timeout");
1906
+ expect(row!.resolutionReason).toStartWith(
1907
+ "Timed out: the cancellation arrived after the deadline",
1908
+ );
1909
+ const tasks = await followUps(taskId);
1910
+ expect(tasks).toHaveLength(1);
1911
+ expect(tasks[0]!.task).toContain("timed out with no answer");
1912
+ });
1913
+
1914
+ test("cancel on an expired request of a waiting run leaves the run for its timeout branch", async () => {
1915
+ const { runId, stepId, approvalId } = await makeRun("waiting");
1916
+ await setTimes(approvalId, { expiresAt: new Date(Date.now() - 60_000).toISOString() });
1917
+
1918
+ const res = await post(`/api/approval-requests/${approvalId}/cancel`, {}, lead.id);
1919
+
1920
+ expect(res.status).toBe(409);
1921
+ expect((await getApprovalRequestById(approvalId))!.status).toBe("timeout");
1922
+ expect((await getWorkflowRun(runId))!.status).toBe("waiting");
1923
+ expect((await getWorkflowRunStep(stepId))!.status).toBe("waiting");
1924
+ });
1925
+
1926
+ test("cancel on a dead run leaves the run alone", async () => {
1927
+ for (const status of ["cancelled", "completed"] as const) {
1928
+ const { runId, approvalId } = await makeRun(status);
1929
+ const res = await post(`/api/approval-requests/${approvalId}/cancel`, {}, lead.id);
1930
+ expect(res.status).toBe(200);
1931
+ expect(res.body.runCancelled).toBe(false);
1932
+ expect((await getWorkflowRun(runId))!.status).toBe(status);
1933
+ }
1934
+ });
1935
+ });
1541
1936
  });
@@ -0,0 +1,47 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { approvalRequestAutoCancellationDays } from "../be/approval-sweeps";
3
+ import { validateConfigValue } from "../be/swarm-config-guard";
4
+
5
+ const KEY = "APPROVAL_REQUEST_AUTO_CANCELLATION_DAYS";
6
+ const original = process.env[KEY];
7
+
8
+ afterEach(() => {
9
+ if (original === undefined) delete process.env[KEY];
10
+ else process.env[KEY] = original;
11
+ });
12
+
13
+ describe("approvalRequestAutoCancellationDays", () => {
14
+ test("defaults to 7 when unset", () => {
15
+ delete process.env[KEY];
16
+ expect(approvalRequestAutoCancellationDays()).toBe(7);
17
+ });
18
+
19
+ test("reads a positive integer", () => {
20
+ process.env[KEY] = "3";
21
+ expect(approvalRequestAutoCancellationDays()).toBe(3);
22
+ });
23
+
24
+ test("reads 0 as disabled", () => {
25
+ process.env[KEY] = "0";
26
+ expect(approvalRequestAutoCancellationDays()).toBe(0);
27
+ });
28
+
29
+ test("falls back to 7 for invalid values", () => {
30
+ process.env[KEY] = "abc";
31
+ expect(approvalRequestAutoCancellationDays()).toBe(7);
32
+ process.env[KEY] = "-1";
33
+ expect(approvalRequestAutoCancellationDays()).toBe(7);
34
+ });
35
+ });
36
+
37
+ describe("APPROVAL_REQUEST_AUTO_CANCELLATION_DAYS validator", () => {
38
+ test("accepts a non-negative integer", () => {
39
+ expect(validateConfigValue(KEY, "7")).toBeNull();
40
+ expect(validateConfigValue(KEY, "0")).toBeNull();
41
+ });
42
+
43
+ test("rejects negative and non-numeric values", () => {
44
+ expect(typeof validateConfigValue(KEY, "-1")).toBe("string");
45
+ expect(typeof validateConfigValue(KEY, "x")).toBe("string");
46
+ });
47
+ });