@desplega.ai/agent-swarm 1.136.0 → 1.138.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 (210) hide show
  1. package/README.md +12 -3
  2. package/dist/acp-adapter-6nttxe2q.js +4787 -0
  3. package/dist/{actions-agcbc8nd.js → actions-z9dzgh3f.js} +7 -8
  4. package/dist/{app-g2s78xya.js → app-wb6bc7tt.js} +4 -5
  5. package/dist/{assistant-yd3jkb9d.js → assistant-08pd6pgy.js} +9 -10
  6. package/dist/{boot-reembed-6spqw4q0.js → boot-reembed-hstr4e2c.js} +4 -4
  7. package/dist/{boot-reembed-b22s9w5x.js → boot-reembed-y7p7qjbf.js} +5 -5
  8. package/dist/{boot-scrub-logs-6h3cqkc0.js → boot-scrub-logs-azbnxwhn.js} +3 -3
  9. package/dist/{claude-adapter-ng5y8zd7.js → claude-adapter-rwaq6eda.js} +10 -9
  10. package/dist/{claude-managed-adapter-5zr3n86v.js → claude-managed-adapter-vtseey1x.js} +3 -3
  11. package/dist/{cli-qckgdqmr.js → cli-005yr57a.js} +2 -2
  12. package/dist/{cli-54ht104h.js → cli-0cka8dnm.js} +3 -3
  13. package/dist/{cli-gpdtxzar.js → cli-23cpnqk9.js} +1 -1
  14. package/dist/{cli-qhfkxrpg.js → cli-2ypjdmqg.js} +223 -474
  15. package/dist/{cli-bnq8h7ea.js → cli-3719ggyf.js} +17 -10
  16. package/dist/cli-390sgxn5.js +257 -0
  17. package/dist/{cli-z6e8eqrm.js → cli-47esr8ht.js} +2 -2
  18. package/dist/{cli-jjhnma34.js → cli-4jaj24py.js} +248 -101
  19. package/dist/{cli-6gwgzdgw.js → cli-69377c2a.js} +5 -5
  20. package/dist/{cli-ads0hfw2.js → cli-6v4d8r8c.js} +205 -75
  21. package/dist/{cli-mgef08pc.js → cli-8rtyss6g.js} +1 -1
  22. package/dist/{cli-35jknh9e.js → cli-9w7vsmbm.js} +5 -5
  23. package/dist/{cli-m3z53bed.js → cli-b4zdc3x6.js} +1 -1
  24. package/dist/{cli-fpjmcbhz.js → cli-ba87cw29.js} +4 -4
  25. package/dist/{cli-4gzv54ss.js → cli-bzk75kdc.js} +43 -11
  26. package/dist/{cli-d5ygfv18.js → cli-cywkhzqs.js} +35 -11
  27. package/dist/{cli-0crn5kaz.js → cli-d62cd9qs.js} +16 -4
  28. package/dist/{cli-5bnz1t86.js → cli-f9skf1v7.js} +51 -7
  29. package/dist/{cli-06ghj6pe.js → cli-fytpjraz.js} +4 -4
  30. package/dist/{cli-j7qp8gtf.js → cli-h7c5dam1.js} +16 -11
  31. package/dist/{cli-4r4jkqep.js → cli-kkn46gfr.js} +9 -3
  32. package/dist/{cli-tmn19cdz.js → cli-kq9htgbr.js} +415 -62
  33. package/dist/{cli-8v57p983.js → cli-pkmqqxp8.js} +45 -69
  34. package/dist/{cli-84vrhesc.js → cli-qf34ej1d.js} +1 -1
  35. package/dist/{cli-v3qda694.js → cli-rnyttbh0.js} +0 -5
  36. package/dist/cli-rx5cxzn2.js +7 -0
  37. package/dist/{cli-cw70hymf.js → cli-s25avqg9.js} +2 -0
  38. package/dist/{cli-tee8w7tq.js → cli-s6t1qv67.js} +15 -10
  39. package/dist/{cli-68vayryq.js → cli-tb4csb08.js} +2 -2
  40. package/dist/{cli-y16cesaq.js → cli-vy48y9bx.js} +2 -2
  41. package/dist/{cli-3y553gws.js → cli-y294regh.js} +1 -1
  42. package/dist/{cli-spf767vc.js → cli-y43043ws.js} +1 -1
  43. package/dist/{cli-hz5hp6wn.js → cli-yh2eybyt.js} +1 -1
  44. package/dist/{cli-edwrx42s.js → cli-zmyxt25p.js} +5 -5
  45. package/dist/cli.js +40 -18
  46. package/dist/{codex-adapter-kw11ps3j.js → codex-adapter-yzep6mvb.js} +7 -6
  47. package/dist/{codex-session-runner-sffb69v7.js → codex-session-runner-25e8tn72.js} +7 -6
  48. package/dist/{commands-0xxp4tz8.js → commands-0vv72d6a.js} +3 -3
  49. package/dist/{db-0npp39zd.js → db-x61ncfa1.js} +19 -3
  50. package/dist/{google-generative-ai-6xxdj4t3.js → google-generative-ai-zyhy3dab.js} +1 -1
  51. package/dist/{google-vertex-zma18wb0.js → google-vertex-t25h65yd.js} +1 -1
  52. package/dist/{handlers-5p7a1jx6.js → handlers-fwdcaahf.js} +9 -10
  53. package/dist/{hook-jjpxn4se.js → hook-mpwfgynm.js} +6 -5
  54. package/dist/{hook-hpefr0a8.js → hook-x18504ja.js} +8 -7
  55. package/dist/{http-wfp33653.js → http-83m6qdds.js} +590 -192
  56. package/dist/{index-pe58mf1r.js → index-1nsdwvn5.js} +2 -1
  57. package/dist/{index-17h657rg.js → index-38f98ck3.js} +9 -9
  58. package/dist/{index-m2p7ctyh.js → index-7kxvtd38.js} +24 -12
  59. package/dist/{index-racb4wfq.js → index-a4naswjn.js} +23 -10
  60. package/dist/{index-p13cfsc2.js → index-agykc6k2.js} +2 -1
  61. package/dist/{index-zh66wm65.js → index-dawtzfhw.js} +22 -9
  62. package/dist/{keepalive-wxjd633a.js → keepalive-y0m8g5wn.js} +5 -5
  63. package/dist/{lead-359ec2z0.js → lead-ka18ef6a.js} +36 -36
  64. package/dist/{maintenance-rr03qjtb.js → maintenance-x585p56c.js} +5 -5
  65. package/dist/{oauth-refresh-sweep-jwj22yb8.js → oauth-refresh-sweep-kdm9vj7a.js} +4 -4
  66. package/dist/{onboard-asnhchcv.js → onboard-5v229nvt.js} +664 -248
  67. package/dist/{opencode-adapter-sx38y7f7.js → opencode-adapter-46nnmnvv.js} +3 -3
  68. package/dist/{otel-impl-54cra6mw.js → otel-impl-k47aj4sd.js} +73 -4
  69. package/dist/{pi-mono-adapter-29g7axgj.js → pi-mono-adapter-wawfj09v.js} +42 -40
  70. package/dist/{pricing-refresh-mkyy7c2y.js → pricing-refresh-ky2gjp05.js} +5 -5
  71. package/dist/{rbac-roles-kb3afm95.js → rbac-roles-0j0f7s8r.js} +4 -4
  72. package/dist/{rbac-roles-ak56tr75.js → rbac-roles-az4wc15e.js} +3 -3
  73. package/dist/{render-v2-434asg49.js → render-v2-seyxq61z.js} +4 -5
  74. package/dist/{seed-pricing-nbmd7ah8.js → seed-pricing-fev611hx.js} +4 -4
  75. package/dist/{setup-wh4qmjs4.js → setup-hy404zwh.js} +2 -2
  76. package/dist/{worker-j1d27tjv.js → worker-g1j5hn16.js} +36 -36
  77. package/openapi.json +197 -27
  78. package/package.json +9 -3
  79. package/src/be/db-client.ts +58 -5
  80. package/src/be/db-retention.ts +550 -121
  81. package/src/be/db.ts +367 -55
  82. package/src/be/memory/raters/retrieval.ts +40 -0
  83. package/src/be/migrations/140_approval_request_cancelled_status.sql +64 -0
  84. package/src/be/pricing-normalize.ts +3 -0
  85. package/src/be/script-connections.ts +26 -6
  86. package/src/be/seed-pricing.ts +18 -8
  87. package/src/be/swarm-config-guard.ts +33 -0
  88. package/src/cli.tsx +20 -1
  89. package/src/commands/onboard/compose-generator.ts +63 -7
  90. package/src/commands/onboard/env-generator.ts +32 -4
  91. package/src/commands/onboard/manifest.ts +4 -1
  92. package/src/commands/onboard/non-interactive.ts +159 -0
  93. package/src/commands/onboard/steps/generate.tsx +1 -34
  94. package/src/commands/onboard/steps/harness-credentials.tsx +181 -1
  95. package/src/commands/onboard/steps/harness.tsx +20 -6
  96. package/src/commands/onboard/steps/review.tsx +3 -2
  97. package/src/commands/onboard/types.ts +48 -1
  98. package/src/commands/onboard.tsx +18 -13
  99. package/src/commands/provider-credentials.ts +17 -3
  100. package/src/commands/runner.ts +1 -0
  101. package/src/heartbeat/heartbeat.ts +47 -7
  102. package/src/http/approval-requests.ts +53 -8
  103. package/src/http/index.ts +9 -1
  104. package/src/http/mcp-servers.ts +2 -0
  105. package/src/http/memory.ts +27 -1
  106. package/src/http/oauth-callback.ts +33 -0
  107. package/src/http/repos.ts +6 -0
  108. package/src/http/script-connections.ts +77 -0
  109. package/src/http/session-data.ts +5 -1
  110. package/src/http/stats.ts +21 -30
  111. package/src/http/tasks.ts +10 -0
  112. package/src/oauth/presets.ts +29 -0
  113. package/src/otel-impl.ts +96 -1
  114. package/src/otel.ts +34 -0
  115. package/src/prompts/memories.ts +2 -2
  116. package/src/providers/acp-adapter.ts +325 -0
  117. package/src/providers/acp-swarm-events.ts +113 -0
  118. package/src/providers/acp-targets.ts +103 -0
  119. package/src/providers/claude-managed-models.ts +17 -1
  120. package/src/providers/codex-adapter.ts +3 -3
  121. package/src/providers/codex-oauth/auth-json-fs.ts +2 -2
  122. package/src/providers/codex-oauth/home.ts +13 -0
  123. package/src/providers/index.ts +5 -1
  124. package/src/providers/types.ts +1 -1
  125. package/src/server-user.ts +1 -0
  126. package/src/slack/app.ts +5 -1
  127. package/src/tasks/worker-follow-up.ts +78 -7
  128. package/src/telemetry.ts +151 -0
  129. package/src/tests/acp-adapter.test.ts +192 -0
  130. package/src/tests/acp-swarm-events.test.ts +132 -0
  131. package/src/tests/agents-list-model-display.test.ts +20 -0
  132. package/src/tests/approval-cancellation-migration.test.ts +134 -0
  133. package/src/tests/approval-requests.test.ts +456 -2
  134. package/src/tests/apps-spike2.test.ts +4 -1
  135. package/src/tests/apps-sync-engine.test.ts +22 -18
  136. package/src/tests/claude-managed-adapter.test.ts +12 -0
  137. package/src/tests/codex-oauth-auth-json-fs.test.ts +19 -0
  138. package/src/tests/connection-embedded-auth.test.ts +4 -1
  139. package/src/tests/context-window.test.ts +2 -0
  140. package/src/tests/credential-check.test.ts +6 -0
  141. package/src/tests/db-client-busy-retry.test.ts +18 -0
  142. package/src/tests/db-client.test.ts +23 -0
  143. package/src/tests/db-retention-telemetry.test.ts +569 -0
  144. package/src/tests/db-retention.test.ts +456 -117
  145. package/src/tests/heartbeat.test.ts +140 -0
  146. package/src/tests/memory-http-recall-gating.test.ts +120 -13
  147. package/src/tests/memory-store.test.ts +41 -0
  148. package/src/tests/migration-046-budgets.test.ts +27 -5
  149. package/src/tests/oauth-presets.test.ts +17 -5
  150. package/src/tests/onboard-compose.test.ts +99 -0
  151. package/src/tests/onboard-env.test.ts +43 -0
  152. package/src/tests/onboard-manifest.test.ts +17 -1
  153. package/src/tests/onboard-non-interactive.test.ts +89 -0
  154. package/src/tests/pool-affinity.test.ts +100 -0
  155. package/src/tests/promote-draft-task-route.test.ts +32 -1
  156. package/src/tests/sandbox-spawn-test-helpers.ts +3 -0
  157. package/src/tests/sandboxed-process.test.ts +2 -2
  158. package/src/tests/schedule-target-type.test.ts +6 -3
  159. package/src/tests/script-connections-http.test.ts +4 -3
  160. package/src/tests/script-connections.test.ts +6 -1
  161. package/src/tests/script-workflows-runtime-e2e.test.ts +36 -28
  162. package/src/tests/scripts-external-api.test.ts +8 -5
  163. package/src/tests/scripts-http.test.ts +6 -3
  164. package/src/tests/scripts-mcp-e2e.test.ts +65 -60
  165. package/src/tests/scripts-retention.test.ts +6 -3
  166. package/src/tests/scripts-runtime-identity.test.ts +4 -2
  167. package/src/tests/scripts-runtime.test.ts +18 -16
  168. package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
  169. package/src/tests/slack-read-boundaries.test.ts +3 -1
  170. package/src/tests/task-lifecycle-telemetry.test.ts +1 -0
  171. package/src/tests/task-tools-ctx.test.ts +88 -1
  172. package/src/tests/telemetry-init.test.ts +61 -0
  173. package/src/tests/tool-annotations.test.ts +4 -1
  174. package/src/tests/vendored-openapi.test.ts +15 -2
  175. package/src/tests/workflow-e2e.test.ts +4 -1
  176. package/src/tests/workflow-engine-v2.test.ts +5 -2
  177. package/src/tests/workflow-executors.test.ts +24 -19
  178. package/src/tests/workflow-http-v2.test.ts +17 -2
  179. package/src/tests/workflow-llm.test.ts +57 -0
  180. package/src/tests/workflow-retry-v2.test.ts +142 -1
  181. package/src/tests/workflow-swarm-script.test.ts +20 -17
  182. package/src/tools/get-task-details.ts +1 -0
  183. package/src/tools/mcp-servers/mcp-server-create.ts +7 -1
  184. package/src/tools/memory-search.ts +27 -2
  185. package/src/tools/send-task.ts +28 -10
  186. package/src/tools/task-action.ts +13 -5
  187. package/src/types.ts +31 -2
  188. package/src/utils/context-window.ts +2 -0
  189. package/src/workflows/approval-notifications.ts +72 -0
  190. package/src/workflows/checkpoint.ts +16 -9
  191. package/src/workflows/completed-step-routing.ts +31 -0
  192. package/src/workflows/engine.ts +59 -43
  193. package/src/workflows/executors/human-in-the-loop.ts +38 -4
  194. package/src/workflows/executors/raw-llm.ts +8 -6
  195. package/src/workflows/executors/validate.ts +6 -5
  196. package/src/workflows/executors/workflow-llm.ts +35 -0
  197. package/src/workflows/resume.ts +90 -13
  198. package/src/workflows/retry-poller.ts +8 -1
  199. package/vendored-openapi/README.md +4 -2
  200. package/vendored-openapi/manifest.json +33 -0
  201. package/vendored-openapi/microsoft-graph.json +781 -0
  202. package/dist/cli-5tnr64pm.js +0 -29
  203. package/dist/{anthropic-messages-pm7pp23c.js → anthropic-messages-tvgm75yc.js} +3 -3
  204. package/dist/{azure-openai-responses-cwesc845.js → azure-openai-responses-hed7waek.js} +4 -4
  205. package/dist/{cli-s9gpxh9q.js → cli-5kmzp1wx.js} +0 -0
  206. package/dist/{cli-s73zwwqn.js → cli-xjx3fbtn.js} +4 -4
  207. package/dist/{openai-codex-responses-xxca1qr1.js → openai-codex-responses-gv3n8rg0.js} +3 -3
  208. package/dist/{openai-completions-yzftqrft.js → openai-completions-dkrj9433.js} +8 -8
  209. package/dist/{openai-responses-e4dcwm9g.js → openai-responses-spk6t7kh.js} +11 -11
  210. package/dist/{openrouter-images-asqqnbvh.js → openrouter-images-71hthwh9.js} +3 -3
@@ -111,6 +111,7 @@ describe("vendored OpenAPI", () => {
111
111
  "linear",
112
112
  "jira",
113
113
  "gmail",
114
+ "microsoft-graph",
114
115
  ]);
115
116
  for (const entry of manifest.integrations) {
116
117
  const specText = readFileSync(path.join(directory, entry.specFile), "utf-8");
@@ -139,6 +140,17 @@ describe("vendored OpenAPI", () => {
139
140
  const refreshed = await refreshScriptConnection(connection.id);
140
141
  expect(refreshed?.generationError).toBeNull();
141
142
  expect(refreshed?.version).toBe(connection.version + 1);
143
+
144
+ const microsoftGraph = await upsertScriptConnection({
145
+ slug: "microsoftGraph",
146
+ kind: "openapi",
147
+ openapiSpecSourceKind: "vendored",
148
+ openapiSpecSource: "microsoft-graph",
149
+ });
150
+ expect(microsoftGraph.baseUrl).toBe("https://graph.microsoft.com/v1.0");
151
+ expect(microsoftGraph.generationError).toBeNull();
152
+ expect(microsoftGraph.generatedTypes).toContain("sendChannelMessage");
153
+ expect(microsoftGraph.generatedTypes).toContain('"team-id"');
142
154
  });
143
155
 
144
156
  test("resolves vendored specs from the module location when cwd is unrelated (npm-package install)", () => {
@@ -187,7 +199,7 @@ describe("vendored OpenAPI", () => {
187
199
  partial: boolean;
188
200
  };
189
201
  expect(down.partial).toBe(true);
190
- expect(down.entries).toHaveLength(5);
202
+ expect(down.entries).toHaveLength(6);
191
203
  expect(down.entries.every((entry) => entry.feeds.includes("blessed"))).toBe(true);
192
204
 
193
205
  globalThis.fetch = (async () =>
@@ -209,13 +221,14 @@ describe("vendored OpenAPI", () => {
209
221
  partial: boolean;
210
222
  };
211
223
  expect(merged.partial).toBe(false);
212
- expect(merged.entries.slice(0, 5).every((entry) => entry.feeds.includes("blessed"))).toBe(true);
224
+ expect(merged.entries.slice(0, 6).every((entry) => entry.feeds.includes("blessed"))).toBe(true);
213
225
  expect(merged.entries.map((entry) => entry.slug)).toEqual([
214
226
  "github",
215
227
  "slack",
216
228
  "linear",
217
229
  "jira",
218
230
  "gmail",
231
+ "microsoft-graph",
219
232
  "stripe",
220
233
  ]);
221
234
  });
@@ -21,6 +21,9 @@ import { ExecutorRegistry } from "../workflows/executors/registry";
21
21
  import { SwarmScriptExecutor } from "../workflows/executors/swarm-script";
22
22
  import { setupWorkflowResumeListener } from "../workflows/resume";
23
23
  import { interpolate } from "../workflows/template";
24
+ import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
25
+
26
+ const spawnDescribe = describe.skipIf(SKIP_SANDBOX_SPAWN_TESTS);
24
27
 
25
28
  const TEST_DB_PATH = "./test-workflow-e2e.sqlite";
26
29
  const API_KEY = "test-workflow-e2e-key-1234567890";
@@ -133,7 +136,7 @@ beforeEach(async () => {
133
136
  await client.run("DELETE FROM workflows");
134
137
  });
135
138
 
136
- describe("workflow e2e swarm-script", () => {
139
+ spawnDescribe("workflow e2e swarm-script", () => {
137
140
  test("swarm-script full workflow run executes through the engine", async () => {
138
141
  await saveScript(
139
142
  "square",
@@ -25,6 +25,9 @@ import {
25
25
  import { ExecutorRegistry } from "../workflows/executors/registry";
26
26
  import { ScriptExecutor } from "../workflows/executors/script";
27
27
  import { resolveInputs } from "../workflows/input";
28
+ import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
29
+
30
+ const skip = test.skipIf(SKIP_SANDBOX_SPAWN_TESTS);
28
31
 
29
32
  const TEST_DB_PATH = "./test-workflow-engine-v2.sqlite";
30
33
 
@@ -412,7 +415,7 @@ describe("Workflow Engine v2 (Phase 3)", () => {
412
415
  // RLIMIT_AS), so the script dies before the watchdog behavior under test.
413
416
  // Linux CI is the enforcing environment; the skip only unblocks local
414
417
  // macOS pushes now that pre-push tests are blocking (#1216).
415
- test.skipIf(process.platform === "darwin")(
418
+ test.skipIf(process.platform === "darwin" || SKIP_SANDBOX_SPAWN_TESTS)(
416
419
  "inline script timeout also extends the workflow step watchdog",
417
420
  async () => {
418
421
  const registry = createTestRegistry();
@@ -828,7 +831,7 @@ describe("Workflow Engine v2 (Phase 3)", () => {
828
831
  expect(step?.output).toBeUndefined();
829
832
  });
830
833
 
831
- test("bundled ralph-loop scripts execute with trigger and upstream values passed as args", async () => {
834
+ skip("bundled ralph-loop scripts execute with trigger and upstream values passed as args", async () => {
832
835
  const receipt = (await Bun.file(
833
836
  new URL("../../docs-site/public/receipts/workflows/ralph-loop.json", import.meta.url),
834
837
  ).json()) as { definition: WorkflowDefinition };
@@ -20,6 +20,9 @@ import { createExecutorRegistry } from "../workflows/executors/registry";
20
20
  import { ScriptExecutor, ScriptOutputSchema } from "../workflows/executors/script";
21
21
  import { ValidateExecutor, ValidateOutputSchema } from "../workflows/executors/validate";
22
22
  import { VcsExecutor, VcsOutputSchema } from "../workflows/executors/vcs";
23
+ import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
24
+
25
+ const skip = test.skipIf(SKIP_SANDBOX_SPAWN_TESTS);
23
26
 
24
27
  const TEST_DB_PATH = "./test-workflow-executors.sqlite";
25
28
 
@@ -482,7 +485,7 @@ describe("ScriptExecutor", () => {
482
485
  ).toBe(false);
483
486
  });
484
487
 
485
- test("runs bash script and captures stdout", async () => {
488
+ skip("runs bash script and captures stdout", async () => {
486
489
  const result = await executor.run(input({ runtime: "bash", script: "echo 'hello world'" }, {}));
487
490
  expect(result.status).toBe("success");
488
491
  const out = result.output as { exitCode: number; stdout: string; stderr: string };
@@ -492,7 +495,7 @@ describe("ScriptExecutor", () => {
492
495
  expect(result.nextPort).toBe("success");
493
496
  });
494
497
 
495
- test("marks step failed and captures stderr on non-zero exit", async () => {
498
+ skip("marks step failed and captures stderr on non-zero exit", async () => {
496
499
  const result = await executor.run(
497
500
  input({ runtime: "bash", script: "echo err >&2; exit 1" }, {}),
498
501
  );
@@ -503,7 +506,7 @@ describe("ScriptExecutor", () => {
503
506
  expect(out.stderr).toBe("err");
504
507
  });
505
508
 
506
- test("marks step failed on non-zero exit code (exit 1)", async () => {
509
+ skip("marks step failed on non-zero exit code (exit 1)", async () => {
507
510
  const result = await executor.run(input({ runtime: "bash", script: "exit 1" }, {}));
508
511
  expect(result.status).toBe("failed");
509
512
  expect(result.error).toBe("Script exited with code 1");
@@ -511,7 +514,7 @@ describe("ScriptExecutor", () => {
511
514
  expect(out?.exitCode).toBe(1);
512
515
  });
513
516
 
514
- test("marks step failed with exit code in error when no stderr (exit 42)", async () => {
517
+ skip("marks step failed with exit code in error when no stderr (exit 42)", async () => {
515
518
  const result = await executor.run(input({ runtime: "bash", script: "exit 42" }, {}));
516
519
  expect(result.status).toBe("failed");
517
520
  expect(result.error).toBe("Script exited with code 42");
@@ -519,7 +522,7 @@ describe("ScriptExecutor", () => {
519
522
  expect(out?.exitCode).toBe(42);
520
523
  });
521
524
 
522
- test("runs TypeScript script via bun", async () => {
525
+ skip("runs TypeScript script via bun", async () => {
523
526
  const result = await executor.run(
524
527
  input({ runtime: "ts", script: "console.log('ts works')" }, {}),
525
528
  );
@@ -534,7 +537,7 @@ describe("ScriptExecutor", () => {
534
537
  expect(valid.success).toBe(true);
535
538
  });
536
539
 
537
- test("keeps raw {exitCode, stdout, stderr} when stdout is not valid JSON", async () => {
540
+ skip("keeps raw {exitCode, stdout, stderr} when stdout is not valid JSON", async () => {
538
541
  const result = await executor.run(
539
542
  input({ runtime: "bash", script: "echo 'not-json {at all'" }, {}),
540
543
  );
@@ -549,7 +552,7 @@ describe("ScriptExecutor", () => {
549
552
  expect(Object.keys(out).sort()).toEqual(["exitCode", "stderr", "stdout"]);
550
553
  });
551
554
 
552
- test("populates structured output on timeout instead of leaving it null", async () => {
555
+ skip("populates structured output on timeout instead of leaving it null", async () => {
553
556
  const result = await executor.run(
554
557
  input({ runtime: "bash", script: "sleep 5", timeout: 1000 }, {}),
555
558
  );
@@ -562,7 +565,7 @@ describe("ScriptExecutor", () => {
562
565
  expect(out.stderr).toContain("Script timed out after 1000ms");
563
566
  });
564
567
 
565
- test("a timed-out script is TERMINATED, not just abandoned (Promise.race regression)", async () => {
568
+ skip("a timed-out script is TERMINATED, not just abandoned (Promise.race regression)", async () => {
566
569
  // A `sleep` is not bounded by the CPU ulimit, so before this fix the
567
570
  // wall-clock timeout only abandoned the promise and the child kept
568
571
  // running — long enough to finish its side effect after the step failed.
@@ -589,7 +592,7 @@ describe("ScriptExecutor", () => {
589
592
 
590
593
  // ─── Sandbox regression tests (superagent.sh c27edfd7, finding b132d7c5) ──
591
594
 
592
- test("child process never inherits the server's secrets — env is scrubbed, not passed through", async () => {
595
+ skip("child process never inherits the server's secrets — env is scrubbed, not passed through", async () => {
593
596
  const savedKey = process.env.AGENT_SWARM_API_KEY;
594
597
  process.env.AGENT_SWARM_API_KEY = "super-secret-operator-bearer";
595
598
  process.env.SOME_OTHER_SERVER_SECRET = "also-should-not-leak";
@@ -618,9 +621,11 @@ describe("ScriptExecutor", () => {
618
621
  // RLIMIT_AS); the spawned script fails before the behavior under test runs.
619
622
  // Linux CI is the enforcing environment for these paths; the skips only
620
623
  // unblock local macOS pushes now that pre-push tests are blocking (#1216).
621
- const skipOnMacOS = test.skipIf(process.platform === "darwin");
624
+ const spawnTestUnlessMacOS = test.skipIf(
625
+ process.platform === "darwin" || SKIP_SANDBOX_SPAWN_TESTS,
626
+ );
622
627
 
623
- skipOnMacOS(
628
+ spawnTestUnlessMacOS(
624
629
  "resource ulimits actually apply to the spawned process (not just documented)",
625
630
  async () => {
626
631
  const result = await executor.run(input({ runtime: "bash", script: "ulimit -v" }, {}));
@@ -632,7 +637,7 @@ describe("ScriptExecutor", () => {
632
637
  },
633
638
  );
634
639
 
635
- test("no explicit cwd: runs in a scoped tmpdir, not the server's working directory", async () => {
640
+ skip("no explicit cwd: runs in a scoped tmpdir, not the server's working directory", async () => {
636
641
  const result = await executor.run(input({ runtime: "bash", script: "pwd" }, {}));
637
642
  expect(result.status).toBe("success");
638
643
  const out = result.output as { stdout: string };
@@ -642,7 +647,7 @@ describe("ScriptExecutor", () => {
642
647
 
643
648
  // ─── Codex review follow-ups (PR #1112, review 4876200033) ──────────────
644
649
 
645
- skipOnMacOS(
650
+ spawnTestUnlessMacOS(
646
651
  "truncated stdout carries an explicit marker instead of silently presenting a partial result as complete (PRRT_kwDOQr3Tmc6XCRu1)",
647
652
  async () => {
648
653
  const result = await executor.run(
@@ -658,7 +663,7 @@ describe("ScriptExecutor", () => {
658
663
  },
659
664
  );
660
665
 
661
- skipOnMacOS(
666
+ spawnTestUnlessMacOS(
662
667
  "drain-deadline snapshot keeps the partial output already read instead of discarding it as empty (PRRT_kwDOQr3Tmc6XCRuy)",
663
668
  async () => {
664
669
  // The direct child prints known output, backgrounds a descendant that
@@ -692,7 +697,7 @@ describe("ScriptExecutor", () => {
692
697
  // running them: an interpolated arg literally named `--eval=<code>` was a
693
698
  // second, attacker-controlled script.
694
699
 
695
- test("bun runtime: an arg shaped like --eval=<code> is inert data, not executed", async () => {
700
+ skip("bun runtime: an arg shaped like --eval=<code> is inert data, not executed", async () => {
696
701
  const result = await executor.run(
697
702
  input(
698
703
  {
@@ -713,7 +718,7 @@ describe("ScriptExecutor", () => {
713
718
  expect(JSON.parse(out.stdout)).toEqual(["--eval=console.log('INJECTED')"]);
714
719
  });
715
720
 
716
- test("bun runtime: an arg shaped like --preload=<path> is inert data, not loaded", async () => {
721
+ skip("bun runtime: an arg shaped like --preload=<path> is inert data, not loaded", async () => {
717
722
  const result = await executor.run(
718
723
  input(
719
724
  {
@@ -736,7 +741,7 @@ describe("ScriptExecutor", () => {
736
741
  ]);
737
742
  });
738
743
 
739
- test("bun runtime: normal args keep their previous argv indexing after the -- fix", async () => {
744
+ skip("bun runtime: normal args keep their previous argv indexing after the -- fix", async () => {
740
745
  const result = await executor.run(
741
746
  input(
742
747
  {
@@ -753,7 +758,7 @@ describe("ScriptExecutor", () => {
753
758
  expect(JSON.parse(out.stdout)).toEqual(["hello", "world"]);
754
759
  });
755
760
 
756
- test("bash runtime: an arg shaped like --eval=<code> was already inert (documents why -- is not added)", async () => {
761
+ skip("bash runtime: an arg shaped like --eval=<code> was already inert (documents why -- is not added)", async () => {
757
762
  // bash -c script [$0 [$1 ...]] binds the first trailing arg to $0, not to
758
763
  // an option — there is no flag-reparsing surface to close here, and
759
764
  // adding `--` would shift $0 into args[0], breaking existing workflows.
@@ -772,7 +777,7 @@ describe("ScriptExecutor", () => {
772
777
  expect(out.stdout).toBe("[--eval=INJECTED][second]");
773
778
  });
774
779
 
775
- test("python runtime: an arg shaped like -c <code> was already inert (documents why -- is not added)", async () => {
780
+ skip("python runtime: an arg shaped like -c <code> was already inert (documents why -- is not added)", async () => {
776
781
  // python3 -c code also stops option parsing after the -c operand —
777
782
  // trailing args (even another literal -c) land verbatim in sys.argv.
778
783
  const result = await executor.run(
@@ -9,9 +9,11 @@ import {
9
9
  } from "node:http";
10
10
  import {
11
11
  closeDb,
12
+ createApprovalRequest,
12
13
  createUser,
13
14
  createWorkflowRun,
14
15
  createWorkflowRunStep,
16
+ getApprovalRequestById,
15
17
  getDbClient,
16
18
  getWorkflowRun,
17
19
  getWorkflowVersions,
@@ -964,11 +966,20 @@ describe("Workflow HTTP API v2", () => {
964
966
  await createWorkflowRun({ id: runId, workflowId: workflow.id });
965
967
 
966
968
  // Create a step in 'running' state
969
+ const stepId = crypto.randomUUID();
967
970
  await createWorkflowRunStep({
968
- id: crypto.randomUUID(),
971
+ id: stepId,
969
972
  runId,
970
973
  nodeId: "n1",
971
- nodeType: "notify",
974
+ nodeType: "human-in-the-loop",
975
+ });
976
+ const approval = await createApprovalRequest({
977
+ id: crypto.randomUUID(),
978
+ title: "Approve release",
979
+ questions: [{ id: "approve", type: "approval", label: "Approve?" }],
980
+ approvers: { policy: "any" },
981
+ workflowRunId: runId,
982
+ workflowRunStepId: stepId,
972
983
  });
973
984
 
974
985
  // Cancel the run
@@ -988,6 +999,10 @@ describe("Workflow HTTP API v2", () => {
988
999
  expect(run.status).toBe("cancelled");
989
1000
  expect(run.error).toBe("Test cancellation");
990
1001
  expect(run.finishedAt).toBeDefined();
1002
+ expect(await getApprovalRequestById(approval.id)).toMatchObject({
1003
+ status: "cancelled",
1004
+ resolutionReason: "Test cancellation",
1005
+ });
991
1006
  });
992
1007
 
993
1008
  test("returns 400 for already completed run", async () => {
@@ -0,0 +1,57 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { resolveWorkflowLlmConfig } from "../workflows/executors/workflow-llm";
3
+
4
+ describe("resolveWorkflowLlmConfig", () => {
5
+ test("routes OpenRouter credentials through the configured gateway", async () => {
6
+ const config = await resolveWorkflowLlmConfig(undefined, {
7
+ OPENROUTER_API_KEY: "sk-or-test",
8
+ OPENROUTER_BASE_URL: "https://gateway.example.test/v1",
9
+ });
10
+
11
+ expect(config).toEqual({
12
+ apiKey: "sk-or-test",
13
+ baseURL: "https://gateway.example.test/v1",
14
+ model: "google/gemini-3-flash-preview",
15
+ });
16
+ });
17
+
18
+ test("routes OpenAI credentials to the SDK default endpoint", async () => {
19
+ const config = await resolveWorkflowLlmConfig(undefined, {
20
+ OPENAI_API_KEY: "sk-openai-test",
21
+ });
22
+
23
+ expect(config).toEqual({
24
+ apiKey: "sk-openai-test",
25
+ baseURL: undefined,
26
+ model: "gpt-5.4-mini",
27
+ });
28
+ });
29
+
30
+ test("does not reuse the memory-rater model as a workflow default", async () => {
31
+ const previous = process.env.MEMORY_RATER_MODEL;
32
+ process.env.MEMORY_RATER_MODEL = "openrouter/anthropic/claude-sonnet-4-5";
33
+ try {
34
+ const config = await resolveWorkflowLlmConfig(undefined, {
35
+ OPENAI_API_KEY: "sk-openai-test",
36
+ });
37
+ expect(config.model).toBe("gpt-5.4-mini");
38
+ } finally {
39
+ if (previous === undefined) delete process.env.MEMORY_RATER_MODEL;
40
+ else process.env.MEMORY_RATER_MODEL = previous;
41
+ }
42
+ });
43
+
44
+ test("preserves an explicit provider-compatible model", async () => {
45
+ const config = await resolveWorkflowLlmConfig("openai/gpt-5.4", {
46
+ OPENAI_API_KEY: "sk-openai-test",
47
+ });
48
+
49
+ expect(config.model).toBe("gpt-5.4");
50
+ });
51
+
52
+ test("rejects credential kinds without an OpenAI-compatible endpoint", async () => {
53
+ await expect(
54
+ resolveWorkflowLlmConfig(undefined, { ANTHROPIC_API_KEY: "sk-ant-test" }),
55
+ ).rejects.toThrow("do not support the resolved anthropic credential yet");
56
+ });
57
+ });
@@ -26,6 +26,7 @@ import {
26
26
  } from "../workflows/executors/base";
27
27
  import { ExecutorRegistry } from "../workflows/executors/registry";
28
28
  import { recoverIncompleteRuns } from "../workflows/recovery";
29
+ import { retryFailedRun } from "../workflows/resume";
29
30
  import { calculateDelay, startRetryPoller, stopRetryPoller } from "../workflows/retry-poller";
30
31
  import { interpolate } from "../workflows/template";
31
32
 
@@ -40,7 +41,10 @@ class FailOnceExecutor extends BaseExecutor<
40
41
  typeof FailOnceExecutor.schema,
41
42
  typeof FailOnceExecutor.outSchema
42
43
  > {
43
- static readonly schema = z.object({ failUntilAttempt: z.number().default(1) });
44
+ static readonly schema = z.object({
45
+ failUntilAttempt: z.number().default(1),
46
+ message: z.string().min(1).optional(),
47
+ });
44
48
  static readonly outSchema = z.object({ attempt: z.number() });
45
49
 
46
50
  readonly type = "fail-once";
@@ -91,10 +95,13 @@ class NotifyStubExecutor extends BaseExecutor<
91
95
  readonly outputSchema = NotifyStubExecutor.outSchema;
92
96
 
93
97
  protected async execute(): Promise<ExecutorResult<z.infer<typeof NotifyStubExecutor.outSchema>>> {
98
+ notifyCounter++;
94
99
  return { status: "success", output: { sent: true } };
95
100
  }
96
101
  }
97
102
 
103
+ let notifyCounter = 0;
104
+
98
105
  // ─── Mock Dependencies ───────────────────────────────────────
99
106
 
100
107
  import * as db from "../be/db";
@@ -227,6 +234,59 @@ describe("Workflow Retry v2 (Phase 4)", () => {
227
234
  });
228
235
 
229
236
  describe("Retry Poller", () => {
237
+ test("rehydrates completed step outputs before interpolating retry inputs", async () => {
238
+ const workflow = await makeWorkflow({
239
+ nodes: [
240
+ {
241
+ id: "producer",
242
+ type: "echo",
243
+ config: { message: "release payload" },
244
+ next: "consumer",
245
+ },
246
+ {
247
+ id: "consumer",
248
+ type: "fail-once",
249
+ inputs: { payload: "producer.echo" },
250
+ config: { failUntilAttempt: 1, message: "{{payload}}" },
251
+ retry: {
252
+ strategy: "static",
253
+ maxRetries: 2,
254
+ baseDelayMs: 1,
255
+ maxDelayMs: 1,
256
+ },
257
+ },
258
+ ],
259
+ });
260
+
261
+ failCounter = 0;
262
+ const runId = await startWorkflowExecution(workflow, {}, registry);
263
+ const initialRun = (await getWorkflowRun(runId))!;
264
+ const consumerStep = (await getWorkflowRunStepsByRunId(runId)).find(
265
+ (step) => step.nodeId === "consumer",
266
+ )!;
267
+ expect(consumerStep.status).toBe("failed");
268
+ expect(consumerStep.input).toMatchObject({ producer: { echo: "release payload" } });
269
+
270
+ const { producer: _lostOutput, ...contextWithoutProducer } = initialRun.context as Record<
271
+ string,
272
+ unknown
273
+ >;
274
+ await updateWorkflowRun(runId, { context: contextWithoutProducer });
275
+
276
+ try {
277
+ startRetryPoller(registry, 5);
278
+ for (let i = 0; i < 100 && (await getWorkflowRun(runId))?.status !== "completed"; i++) {
279
+ await new Promise((resolve) => setTimeout(resolve, 5));
280
+ }
281
+ } finally {
282
+ stopRetryPoller();
283
+ }
284
+
285
+ const completedRun = await getWorkflowRun(runId);
286
+ expect(completedRun?.status).toBe("completed");
287
+ expect(completedRun?.context).toMatchObject({ producer: { echo: "release payload" } });
288
+ });
289
+
230
290
  test("rehydrates requester attribution before retrying an executor", async () => {
231
291
  const user = await createUser({ name: "Retry Requester" });
232
292
  const workflow = await makeWorkflow({
@@ -445,4 +505,85 @@ describe("Workflow Retry v2 (Phase 4)", () => {
445
505
  expect(run!.status).toBe("completed");
446
506
  });
447
507
  });
508
+
509
+ describe("External retry", () => {
510
+ test("does not execute a node behind a gate port that was not taken", async () => {
511
+ const workflow = await makeWorkflow({
512
+ nodes: [
513
+ {
514
+ id: "gate",
515
+ type: "echo",
516
+ config: { message: "gate" },
517
+ next: { true: "inactive", false: "taken" },
518
+ },
519
+ {
520
+ id: "inactive",
521
+ type: "notify",
522
+ config: { channel: "test", template: "must not run" },
523
+ },
524
+ {
525
+ id: "taken",
526
+ type: "echo",
527
+ config: { message: "taken" },
528
+ },
529
+ ],
530
+ });
531
+ const runId = crypto.randomUUID();
532
+ await createWorkflowRun({ id: runId, workflowId: workflow.id, triggerData: {} });
533
+
534
+ const gateStepId = crypto.randomUUID();
535
+ await createWorkflowRunStep({
536
+ id: gateStepId,
537
+ runId,
538
+ nodeId: "gate",
539
+ nodeType: "echo",
540
+ input: {},
541
+ });
542
+ await updateWorkflowRunStep(gateStepId, {
543
+ status: "completed",
544
+ output: { echo: "gate" },
545
+ nextPort: "false",
546
+ finishedAt: new Date().toISOString(),
547
+ });
548
+
549
+ const takenStepId = crypto.randomUUID();
550
+ await createWorkflowRunStep({
551
+ id: takenStepId,
552
+ runId,
553
+ nodeId: "taken",
554
+ nodeType: "echo",
555
+ input: {},
556
+ });
557
+ await updateWorkflowRunStep(takenStepId, {
558
+ status: "completed",
559
+ output: { echo: "taken" },
560
+ finishedAt: new Date().toISOString(),
561
+ });
562
+
563
+ const failedStepId = crypto.randomUUID();
564
+ await createWorkflowRunStep({
565
+ id: failedStepId,
566
+ runId,
567
+ nodeId: "inactive",
568
+ nodeType: "notify",
569
+ input: {},
570
+ });
571
+ await updateWorkflowRunStep(failedStepId, {
572
+ status: "failed",
573
+ error: "retry target failed",
574
+ finishedAt: new Date().toISOString(),
575
+ });
576
+ await updateWorkflowRun(runId, { status: "failed", error: "retry target failed" });
577
+
578
+ notifyCounter = 0;
579
+ await retryFailedRun(runId, registry);
580
+
581
+ expect(notifyCounter).toBe(0);
582
+ expect((await getWorkflowRun(runId))?.status).toBe("completed");
583
+ expect(
584
+ (await getWorkflowRunStepsByRunId(runId)).find((step) => step.nodeId === "inactive")
585
+ ?.status,
586
+ ).toBe("cancelled");
587
+ });
588
+ });
448
589
  });