@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
@@ -30,6 +30,9 @@ import {
30
30
  SwarmScriptExecutor,
31
31
  } from "../workflows/executors/swarm-script";
32
32
  import { interpolate } from "../workflows/template";
33
+ import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
34
+
35
+ const skip = test.skipIf(SKIP_SANDBOX_SPAWN_TESTS);
33
36
 
34
37
  const TEST_DB_PATH = "./test-workflow-swarm-script.sqlite";
35
38
  const API_KEY = "test-workflow-swarm-script-key-1234567890";
@@ -185,7 +188,7 @@ describe("SwarmScriptExecutor", () => {
185
188
  ).toBe(SWARM_SCRIPT_MAX_TIMEOUT_MS);
186
189
  });
187
190
 
188
- test("A workflow with one swarm-script node resolves by name + runs + returns result", async () => {
191
+ skip("A workflow with one swarm-script node resolves by name + runs + returns result", async () => {
189
192
  await saveScript(
190
193
  "scratch-add-one-a1b2c3d4",
191
194
  `export default async (args: { value: number }) => ({ value: args.value + 1 });`,
@@ -248,7 +251,7 @@ describe("SwarmScriptExecutor", () => {
248
251
  expect(result.output).toBeUndefined();
249
252
  });
250
253
 
251
- test("named script source may contain literal mustache template data", async () => {
254
+ skip("named script source may contain literal mustache template data", async () => {
252
255
  await saveScript(
253
256
  "literal-mustache-source",
254
257
  `export default async () => ({ template: "Hello {{customer_name}}" });`,
@@ -272,7 +275,7 @@ describe("SwarmScriptExecutor", () => {
272
275
  expect(result.output?.result).toEqual({ template: "Hello {{customer_name}}" });
273
276
  });
274
277
 
275
- test("unrelated upstream node names do not turn literal source templates into workflow tokens", async () => {
278
+ skip("unrelated upstream node names do not turn literal source templates into workflow tokens", async () => {
276
279
  await saveScript(
277
280
  "literal-upstream-name",
278
281
  `export default async () => ({ template: "Hello {{customer.name}}" });`,
@@ -325,7 +328,7 @@ describe("SwarmScriptExecutor", () => {
325
328
  expect(result.error).toContain("{{customer.name}}");
326
329
  });
327
330
 
328
- test("swarm-script executor includes API connection descriptors in ctx.api", async () => {
331
+ skip("swarm-script executor includes API connection descriptors in ctx.api", async () => {
329
332
  await upsertScriptConnection({
330
333
  slug: "workflowVendor",
331
334
  kind: "openapi",
@@ -367,7 +370,7 @@ describe("SwarmScriptExecutor", () => {
367
370
  expect(result.output?.result).toEqual({ apiKeys: ["workflowVendor"] });
368
371
  });
369
372
 
370
- test("pinHash correctly resolves to a historic script_versions row", async () => {
373
+ skip("pinHash correctly resolves to a historic script_versions row", async () => {
371
374
  const first = await saveScript("versioned", `export default async () => ({ version: "old" });`);
372
375
  await saveScript("versioned", `export default async () => ({ version: "new" });`);
373
376
 
@@ -391,7 +394,7 @@ describe("SwarmScriptExecutor", () => {
391
394
  expect(result.output?.version).toBe(1);
392
395
  });
393
396
 
394
- test("inputs mapping from a predecessor node correctly populates args", async () => {
397
+ skip("inputs mapping from a predecessor node correctly populates args", async () => {
395
398
  await saveScript(
396
399
  "from-input",
397
400
  `export default async (args: { value: string }) => ({ seen: args.value });`,
@@ -439,7 +442,7 @@ describe("SwarmScriptExecutor", () => {
439
442
  expect(echoStep?.output).toEqual({ value: '{"a":1}' });
440
443
  });
441
444
 
442
- test("{{path}} args: object arg is injected as raw object, not JSON string", async () => {
445
+ skip("{{path}} args: object arg is injected as raw object, not JSON string", async () => {
443
446
  await saveScript(
444
447
  "echo-obj",
445
448
  `export default async (args: { data: Record<string, unknown> }) => ({ isObject: typeof args.data === "object" && !Array.isArray(args.data), keys: Object.keys(args.data ?? {}) });`,
@@ -467,7 +470,7 @@ describe("SwarmScriptExecutor", () => {
467
470
  expect(scriptStep?.output).toMatchObject({ result: { isObject: true, keys: ["a", "b"] } });
468
471
  });
469
472
 
470
- test("{{path}} args: array arg is injected as raw array, not JSON string", async () => {
473
+ skip("{{path}} args: array arg is injected as raw array, not JSON string", async () => {
471
474
  await saveScript(
472
475
  "echo-arr",
473
476
  `export default async (args: { items: string[] }) => ({ isArray: Array.isArray(args.items), length: args.items.length });`,
@@ -495,7 +498,7 @@ describe("SwarmScriptExecutor", () => {
495
498
  expect(scriptStep?.output).toMatchObject({ result: { isArray: true, length: 3 } });
496
499
  });
497
500
 
498
- test("{{path}} args: empty array is injected as raw empty array with length 0, not '[]' string", async () => {
501
+ skip("{{path}} args: empty array is injected as raw empty array with length 0, not '[]' string", async () => {
499
502
  await saveScript(
500
503
  "echo-empty-arr",
501
504
  `export default async (args: { items: string[] }) => ({ isArray: Array.isArray(args.items), length: args.items.length });`,
@@ -523,7 +526,7 @@ describe("SwarmScriptExecutor", () => {
523
526
  expect(scriptStep?.output).toMatchObject({ result: { isArray: true, length: 0 } });
524
527
  });
525
528
 
526
- test("{{path}} args: string scalar arg is injected as the string value", async () => {
529
+ skip("{{path}} args: string scalar arg is injected as the string value", async () => {
527
530
  await saveScript(
528
531
  "echo-str",
529
532
  `export default async (args: { name: string }) => ({ isString: typeof args.name === "string", value: args.name });`,
@@ -557,7 +560,7 @@ describe("SwarmScriptExecutor", () => {
557
560
  });
558
561
  });
559
562
 
560
- test("{{path}} args: number scalar arg is injected as a number, not a string", async () => {
563
+ skip("{{path}} args: number scalar arg is injected as a number, not a string", async () => {
561
564
  await saveScript(
562
565
  "echo-num",
563
566
  `export default async (args: { count: number }) => ({ isNumber: typeof args.count === "number", value: args.count });`,
@@ -585,7 +588,7 @@ describe("SwarmScriptExecutor", () => {
585
588
  expect(scriptStep?.output).toMatchObject({ result: { isNumber: true, value: 3 } });
586
589
  });
587
590
 
588
- test("{{path}} args: boolean scalar arg is injected as a boolean, not a string", async () => {
591
+ skip("{{path}} args: boolean scalar arg is injected as a boolean, not a string", async () => {
589
592
  await saveScript(
590
593
  "echo-bool",
591
594
  `export default async (args: { enabled: boolean }) => ({ isBoolean: typeof args.enabled === "boolean", value: args.enabled });`,
@@ -613,7 +616,7 @@ describe("SwarmScriptExecutor", () => {
613
616
  expect(scriptStep?.output).toMatchObject({ result: { isBoolean: true, value: false } });
614
617
  });
615
618
 
616
- test("{{path}} args: mixed string template still produces a string via interpolation", async () => {
619
+ skip("{{path}} args: mixed string template still produces a string via interpolation", async () => {
617
620
  await saveScript(
618
621
  "echo-mixed",
619
622
  `export default async (args: { label: string }) => ({ isString: typeof args.label === "string", value: args.label });`,
@@ -643,7 +646,7 @@ describe("SwarmScriptExecutor", () => {
643
646
  });
644
647
  });
645
648
 
646
- test("fsMode workspace-rw is rejected at config validation with a clear error message", async () => {
649
+ skip("fsMode workspace-rw is rejected at config validation with a clear error message", async () => {
647
650
  await saveScript("noop", `export default async () => ({ ok: true });`);
648
651
  const executor = new SwarmScriptExecutor(deps);
649
652
  const wf = await makeWorkflow({ nodes: [] });
@@ -676,7 +679,7 @@ describe("SwarmScriptExecutor", () => {
676
679
  expect(success.status).toBe("success");
677
680
  });
678
681
 
679
- test("timeoutMs not set — script completes with the default 30s window", async () => {
682
+ skip("timeoutMs not set — script completes with the default 30s window", async () => {
680
683
  await saveScript("quick", `export default async () => ({ done: true });`);
681
684
  const executor = new SwarmScriptExecutor(deps);
682
685
  const wf = await makeWorkflow({ nodes: [] });
@@ -696,7 +699,7 @@ describe("SwarmScriptExecutor", () => {
696
699
  expect(result.output?.result).toEqual({ done: true });
697
700
  });
698
701
 
699
- test("timeoutMs set — a long-running script is killed before it finishes", async () => {
702
+ skip("timeoutMs set — a long-running script is killed before it finishes", async () => {
700
703
  await saveScript(
701
704
  "sleeper",
702
705
  `export default async () => { await new Promise(r => setTimeout(r, 3000)); return { done: true }; };`,
@@ -719,7 +722,7 @@ describe("SwarmScriptExecutor", () => {
719
722
  expect(result.output?.exitCode).not.toBe(0);
720
723
  });
721
724
 
722
- test("Failure in the script surfaces as a workflow-node failure", async () => {
725
+ skip("Failure in the script surfaces as a workflow-node failure", async () => {
723
726
  await saveScript("throws", `export default async () => { throw new Error("boom"); };`);
724
727
  const executor = new SwarmScriptExecutor(deps);
725
728
  const wf = await makeWorkflow({ nodes: [] });
@@ -40,6 +40,7 @@ const looseRoutingAffinitySchema = z.looseObject({
40
40
  role: z.string().optional(),
41
41
  harnessProvider: z.string().optional(),
42
42
  capabilities: z.array(z.string()).optional(),
43
+ leadOnly: z.boolean().optional(),
43
44
  });
44
45
 
45
46
  export const looseAgentTaskOutputSchema = z.looseObject({
@@ -1,6 +1,11 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
- import { createMcpServer, getAgentById, installMcpServer } from "@/be/db";
3
+ import {
4
+ createMcpServer,
5
+ emitMcpServerConnectedTelemetry,
6
+ getAgentById,
7
+ installMcpServer,
8
+ } from "@/be/db";
4
9
  import { can } from "@/rbac";
5
10
  import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
6
11
 
@@ -102,6 +107,7 @@ export const registerMcpServerCreateTool = (server: McpServer) => {
102
107
  headerConfigKeys: args.headerConfigKeys,
103
108
  extraAuthorizeParams: args.extraAuthorizeParams,
104
109
  });
110
+ await emitMcpServerConnectedTelemetry();
105
111
 
106
112
  // Auto-install for the creating agent
107
113
  await installMcpServer(requestInfo.agentId, created.id);
@@ -4,7 +4,11 @@ import { getAgentById } from "@/be/db";
4
4
  import { getEmbeddingProvider, getMemoryStore } from "@/be/memory";
5
5
  import { CANDIDATE_SET_MULTIPLIER } from "@/be/memory/constants";
6
6
  import { expandCandidatesWithGraph } from "@/be/memory/graph-expansion";
7
- import { recordRetrievals } from "@/be/memory/raters/retrieval";
7
+ import {
8
+ dedupeMemoryDocumentIds,
9
+ recordMemoryAccesses,
10
+ recordRetrievals,
11
+ } from "@/be/memory/raters/retrieval";
8
12
  import { rerank } from "@/be/memory/reranker";
9
13
  import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
10
14
  import type { AgentMemorySource } from "@/types";
@@ -26,6 +30,7 @@ type MemorySearchResult = {
26
30
  retrievalSource?: string;
27
31
  tags?: string[];
28
32
  createdAt: string;
33
+ accessCount?: number;
29
34
  rateHint?: string;
30
35
  };
31
36
 
@@ -57,6 +62,7 @@ export const memorySearchOutputSchema = swarmToolOutputSchema({
57
62
  retrievalSource: z.enum(["vec", "fts", "hybrid", "fallback", "graph"]).optional(),
58
63
  tags: z.array(z.string()).optional(),
59
64
  createdAt: z.string().optional(),
65
+ accessCount: z.number().int().optional(),
60
66
  rateHint: z.string().optional(),
61
67
  }),
62
68
  )
@@ -70,7 +76,8 @@ export const registerMemorySearchTool = (server: McpServer) => {
70
76
  title: "Search memories",
71
77
  description:
72
78
  "Search your accumulated memories using natural language. Returns summaries with IDs — use memory-get to retrieve full content.",
73
- annotations: { readOnlyHint: true },
79
+ // Search updates access telemetry for every logical document returned.
80
+ annotations: { readOnlyHint: false },
74
81
 
75
82
  inputSchema: z.object({
76
83
  query: z.string().min(1).describe("Natural language search query."),
@@ -148,6 +155,14 @@ export const registerMemorySearchTool = (server: McpServer) => {
148
155
  }
149
156
  }
150
157
 
158
+ const consumedIds = dedupeMemoryDocumentIds(ranked);
159
+ try {
160
+ await recordMemoryAccesses(consumedIds);
161
+ } catch (err) {
162
+ console.error("[memory-search] recordMemoryAccesses failed:", (err as Error).message);
163
+ }
164
+ const consumedIdSet = new Set(consumedIds);
165
+
151
166
  const inTaskContext = !!requestInfo.sourceTaskId;
152
167
  const mapped = ranked.map((r) => ({
153
168
  id: r.id,
@@ -159,6 +174,7 @@ export const registerMemorySearchTool = (server: McpServer) => {
159
174
  retrievalSource: r.retrievalSource,
160
175
  tags: r.tags,
161
176
  createdAt: r.createdAt,
177
+ accessCount: (r.accessCount ?? 0) + (consumedIdSet.has(r.id) ? 1 : 0),
162
178
  ...(inTaskContext && NUDGE_ELIGIBLE_SOURCES.has(r.source as AgentMemorySource)
163
179
  ? { rateHint: rateHintFor(r.id) }
164
180
  : {}),
@@ -180,6 +196,14 @@ export const registerMemorySearchTool = (server: McpServer) => {
180
196
  source,
181
197
  });
182
198
 
199
+ const consumedIds = dedupeMemoryDocumentIds(recent);
200
+ try {
201
+ await recordMemoryAccesses(consumedIds);
202
+ } catch (err) {
203
+ console.error("[memory-search] recordMemoryAccesses failed:", (err as Error).message);
204
+ }
205
+ const consumedIdSet = new Set(consumedIds);
206
+
183
207
  const mapped = recent.map((r) => ({
184
208
  id: r.id,
185
209
  name: r.name,
@@ -188,6 +212,7 @@ export const registerMemorySearchTool = (server: McpServer) => {
188
212
  scope: r.scope,
189
213
  tags: r.tags,
190
214
  createdAt: r.createdAt,
215
+ accessCount: (r.accessCount ?? 0) + (consumedIdSet.has(r.id) ? 1 : 0),
191
216
  }));
192
217
 
193
218
  return toolOk(
@@ -62,8 +62,12 @@ export const sendTaskInputSchema = z
62
62
  requiredCapabilities: z
63
63
  .array(z.string())
64
64
  .optional()
65
+ .describe("Capabilities required for pool routing."),
66
+ leadOnly: z
67
+ .boolean()
68
+ .default(false)
65
69
  .describe(
66
- "Capabilities a claiming agent must have (declared via join-swarm/update-profile) to be pool-eligible for this task. Written into the created task's routingAffinity (role is left unset — only enforced when the pool auto-claim/claim-tool paths check it). Most useful when omitting agentId (unassigned pool task); a no-op for a task with an explicit agentId, which bypasses the pool gate entirely.",
70
+ "Structured authorization constraint for merge or other privileged work. Only Lead agents may be assigned, offered, or claim it; never inferred from task text.",
67
71
  ),
68
72
  priority: z.number().int().min(0).max(100).optional().describe("Priority 0-100 (default: 50)."),
69
73
  dependsOn: z.array(z.uuid()).optional().describe("Task IDs this task depends on."),
@@ -198,6 +202,7 @@ export async function sendTaskHandler(
198
202
  taskType,
199
203
  tags,
200
204
  requiredCapabilities,
205
+ leadOnly,
201
206
  priority,
202
207
  dependsOn,
203
208
  dir,
@@ -240,6 +245,14 @@ export async function sendTaskHandler(
240
245
  const effectiveParentTask = effectiveParentTaskId
241
246
  ? await getTaskById(effectiveParentTaskId)
242
247
  : null;
248
+ if (effectiveParentTask?.routingAffinityInvalid) {
249
+ return toolErr("Cannot continue a task with an invalid routing affinity.", {
250
+ data: { yourAgentId: creatorAgentId },
251
+ });
252
+ }
253
+ // A public continuation cannot accidentally declassify its parent before
254
+ // createTaskExtended performs the authoritative merge.
255
+ const effectiveLeadOnly = leadOnly || effectiveParentTask?.routingAffinity?.leadOnly === true;
243
256
 
244
257
  // Slack-routing coherence guard: reject a hand-typed slackChannelId/slackThreadTs
245
258
  // that disagrees with the parent task or the contextKey this child will inherit.
@@ -423,13 +436,10 @@ export async function sendTaskHandler(
423
436
  slackUserId,
424
437
  overrideSlackContext,
425
438
  followUpConfig,
426
- // Only meaningful here: a pool task's routingAffinity gates
427
- // claimTask/autoAssignPoolTasks. offer/direct-assign below bypass the
428
- // pool gate entirely via an explicit agentId, so requiredCapabilities
429
- // is a no-op there.
430
- routingAffinity: requiredCapabilities?.length
431
- ? { capabilities: requiredCapabilities }
432
- : undefined,
439
+ routingAffinity:
440
+ effectiveLeadOnly || requiredCapabilities?.length
441
+ ? { leadOnly: effectiveLeadOnly, capabilities: requiredCapabilities ?? [] }
442
+ : undefined,
433
443
  });
434
444
  await transferTrackerSyncToResumeChild({
435
445
  parentTaskId: effectiveParentTaskId,
@@ -453,10 +463,10 @@ export async function sendTaskHandler(
453
463
  };
454
464
  }
455
465
 
456
- if (agent.isLead) {
466
+ if (effectiveLeadOnly && !agent.isLead) {
457
467
  return {
458
468
  success: false,
459
- message: `Cannot assign tasks to the lead agent "${agent.name}", wtf?`,
469
+ message: `Lead-only task requires a Lead agent; "${agent.name}" is not a Lead.`,
460
470
  };
461
471
  }
462
472
 
@@ -492,6 +502,10 @@ export async function sendTaskHandler(
492
502
  slackUserId,
493
503
  overrideSlackContext,
494
504
  followUpConfig,
505
+ routingAffinity:
506
+ effectiveLeadOnly || requiredCapabilities?.length
507
+ ? { leadOnly: effectiveLeadOnly, capabilities: requiredCapabilities ?? [] }
508
+ : undefined,
495
509
  });
496
510
  await transferTrackerSyncToResumeChild({
497
511
  parentTaskId: effectiveParentTaskId,
@@ -528,6 +542,10 @@ export async function sendTaskHandler(
528
542
  slackUserId,
529
543
  overrideSlackContext,
530
544
  followUpConfig,
545
+ routingAffinity:
546
+ effectiveLeadOnly || requiredCapabilities?.length
547
+ ? { leadOnly: effectiveLeadOnly, capabilities: requiredCapabilities ?? [] }
548
+ : undefined,
531
549
  });
532
550
  await transferTrackerSyncToResumeChild({
533
551
  parentTaskId: effectiveParentTaskId,
@@ -102,8 +102,12 @@ export const taskActionInputSchema = z.object({
102
102
  requiredCapabilities: z
103
103
  .array(z.string())
104
104
  .optional()
105
+ .describe("Capabilities required for pool routing."),
106
+ leadOnly: z
107
+ .boolean()
108
+ .default(false)
105
109
  .describe(
106
- "Capabilities a claiming agent must have (declared via join-swarm/update-profile) to be pool-eligible for this task. Written into the created task's routingAffinity (role is left unset). Only used with 'create' action.",
110
+ "Structured authorization constraint: only Lead agents may claim this privileged task.",
107
111
  ),
108
112
  });
109
113
 
@@ -193,6 +197,7 @@ export async function taskActionHandler(
193
197
  dir,
194
198
  model,
195
199
  requiredCapabilities,
200
+ leadOnly,
196
201
  } = input;
197
202
  const normalizedModel = splitLegacyModelAlias({ model, modelTier: input.modelTier });
198
203
 
@@ -296,9 +301,10 @@ export async function taskActionHandler(
296
301
  model: normalizedModel.model,
297
302
  modelTier: normalizedModel.modelTier,
298
303
  effort: input.effort,
299
- routingAffinity: requiredCapabilities?.length
300
- ? { capabilities: requiredCapabilities }
301
- : undefined,
304
+ routingAffinity:
305
+ leadOnly || requiredCapabilities?.length
306
+ ? { leadOnly, capabilities: requiredCapabilities ?? [] }
307
+ : undefined,
302
308
  });
303
309
  return {
304
310
  success: true,
@@ -349,7 +355,9 @@ export async function taskActionHandler(
349
355
  if (claimingAgent && !isAgentEligibleForTask(claimingAgent, existingTask)) {
350
356
  return {
351
357
  success: false,
352
- message: `Task "${taskId}" requires role "${existingTask.routingAffinity?.role ?? "(unspecified)"}"; yours is "${claimingAgent.role ?? "(unspecified)"}". Cannot claim.`,
358
+ message: existingTask.routingAffinity?.leadOnly
359
+ ? `Task "${taskId}" is Lead-only; you are not authorized to claim it.`
360
+ : `Task "${taskId}" requires role "${existingTask.routingAffinity?.role ?? "(unspecified)"}"; yours is "${claimingAgent.role ?? "(unspecified)"}". Cannot claim.`,
353
361
  };
354
362
  }
355
363
  // Atomic claim — only one agent can win this race
package/src/types.ts CHANGED
@@ -152,6 +152,16 @@ export const DEFAULT_MODEL_TIER_MAP: Record<ProviderName, Record<ModelTier, stri
152
152
  smart: "devin",
153
153
  ultra: "devin",
154
154
  },
155
+ // A generic ACP target owns its own model selection — the swarm never sends
156
+ // one over the wire. Empty strings resolve to `{ source: "none" }`, while
157
+ // `MODEL_TIER_*` / `MODEL_TIER_MAP` env overrides still apply for targets
158
+ // that do accept a model.
159
+ acp: {
160
+ smol: "",
161
+ regular: "",
162
+ smart: "",
163
+ ultra: "",
164
+ },
155
165
  };
156
166
 
157
167
  export function parseModelTier(value: string | null | undefined): ModelTier | undefined {
@@ -344,6 +354,7 @@ export const ProviderNameSchema = z.enum([
344
354
  "devin",
345
355
  "claude-managed",
346
356
  "opencode",
357
+ "acp",
347
358
  ]);
348
359
  export type ProviderName = z.infer<typeof ProviderNameSchema>;
349
360
 
@@ -433,6 +444,10 @@ export const PROVIDER_STEER_CAPABILITIES: Record<ProviderName, SteerMode[]> = {
433
444
  // hook `additionalContext`, so the runner must leave codex rows `pending`
434
445
  // (`ProviderSession.steeringDeliveredExternally`).
435
446
  codex: ["queue"],
447
+ // The ACP adapter implements no steering primitive: `session/prompt` is a
448
+ // single in-flight turn and the only interrupt is `session/cancel` (abort).
449
+ // Advertise nothing rather than promise semantics we can't honor.
450
+ acp: [],
436
451
  };
437
452
 
438
453
  export type DevinProviderMeta = {
@@ -451,6 +466,7 @@ export type ProviderMetaMap = {
451
466
  pi: NoProviderMeta;
452
467
  "claude-managed": NoProviderMeta;
453
468
  opencode: NoProviderMeta;
469
+ acp: NoProviderMeta;
454
470
  };
455
471
 
456
472
  export const FollowUpConfigSchema = z
@@ -475,6 +491,8 @@ export const RoutingAffinitySchema = z
475
491
  role: z.string().max(100).optional(),
476
492
  harnessProvider: ProviderNameSchema.optional(),
477
493
  capabilities: z.array(z.string()).default([]),
494
+ /** Explicit authorization boundary for merge and other control-plane work. */
495
+ leadOnly: z.boolean().optional(),
478
496
  })
479
497
  .openapi("RoutingAffinity");
480
498
  export type RoutingAffinity = z.infer<typeof RoutingAffinitySchema>;
@@ -631,6 +649,9 @@ export const AgentTaskSchema = z
631
649
  // behavior. Inherited from parentTaskId when not explicitly set (see
632
650
  // `createTaskExtended` in src/be/db.ts). See `isAgentEligibleForTask`.
633
651
  routingAffinity: RoutingAffinitySchema.optional(),
652
+ // Stored affinity that fails validation is quarantined rather than silently
653
+ // treated as an ordinary, claimable task. Internal read-path signal.
654
+ routingAffinityInvalid: z.boolean().optional(),
634
655
  })
635
656
  .openapi("AgentTask");
636
657
 
@@ -722,8 +743,10 @@ export const CreateTaskOptionsSchema = z.object({
722
743
  */
723
744
  bypassTrackerContextDedup: z.boolean().optional(),
724
745
  /**
725
- * Routing-affinity snapshot gating pool eligibility (see
726
- * `isAgentEligibleForTask`). Inherited from the parent (via `parentTaskId`)
746
+ * Routing-affinity snapshot gating task authorization (see
747
+ * `isAgentEligibleForTask`). `leadOnly: true` is an explicit, structured
748
+ * constraint: only an agent with `isLead` may be assigned, offered, claim,
749
+ * or recover this task. It is never inferred from task text. Inherited from the parent (via `parentTaskId`)
727
750
  * when not explicitly set — same treatment as `vcsRepo`/`contextKey`.
728
751
  */
729
752
  routingAffinity: RoutingAffinitySchema.optional(),
@@ -1296,6 +1319,8 @@ export const AgentLogEventTypeSchema = z.enum([
1296
1319
  "task_rejected",
1297
1320
  "task_claimed",
1298
1321
  "task_claim_rejected_affinity",
1322
+ "task_authorization_rejected",
1323
+ "task_recovery_authorization",
1299
1324
  "task_released",
1300
1325
  "channel_message",
1301
1326
  // Service registry events
@@ -2897,6 +2922,10 @@ export const PricingProviderSchema = z.enum([
2897
2922
  "opencode",
2898
2923
  "devin",
2899
2924
  "gemini",
2925
+ // No seeded rate rows: a generic ACP target owns its own billing and the
2926
+ // adapter reports `totalCostUsd: 0`, so these rows settle at
2927
+ // `costSource: 'unpriced'`. Accepted here so the row is recorded at all.
2928
+ "acp",
2900
2929
  ]);
2901
2930
  export type PricingProvider = z.infer<typeof PricingProviderSchema>;
2902
2931
 
@@ -27,6 +27,8 @@ export const CONTEXT_FORMULA = "input-cache-output" as const;
27
27
 
28
28
  const CONTEXT_WINDOW_DEFAULTS: Record<string, number> = {
29
29
  // Anthropic Fable / Mythos tier
30
+ "claude-fable-5-1": 1_000_000,
31
+ "claude-mythos-5-1": 1_000_000,
30
32
  "claude-opus-5": 1_000_000,
31
33
  "claude-fable-5": 1_000_000,
32
34
  "claude-mythos-5": 1_000_000,
@@ -0,0 +1,72 @@
1
+ import {
2
+ type ApprovalRequest,
3
+ claimApprovalCancellationNotification,
4
+ completeApprovalCancellationNotificationClaim,
5
+ releaseApprovalCancellationNotificationClaim,
6
+ } from "../be/db";
7
+ import { getSlackApp } from "../slack/app";
8
+ import { scrubSecrets } from "../utils/secret-scrubber";
9
+
10
+ export interface ApprovalSlackClient {
11
+ chat: {
12
+ postMessage(input: {
13
+ channel: string;
14
+ thread_ts: string;
15
+ text: string;
16
+ unfurl_links: false;
17
+ unfurl_media: false;
18
+ }): Promise<unknown>;
19
+ };
20
+ }
21
+
22
+ export async function postApprovalCancellationUpdates(
23
+ approvals: ApprovalRequest[],
24
+ reason: string,
25
+ client: ApprovalSlackClient | undefined = getSlackApp()?.client,
26
+ ): Promise<void> {
27
+ if (!client) return;
28
+
29
+ const text = `This approval is no longer actionable: ${reason}`;
30
+ for (const approval of approvals) {
31
+ const channels = approval.notificationChannels as Array<{
32
+ channel: string;
33
+ target: string;
34
+ messageTs?: string;
35
+ }> | null;
36
+ const slackThreads = (channels ?? []).filter(
37
+ (notification) => notification.channel === "slack" && notification.messageTs,
38
+ );
39
+ if (slackThreads.length === 0) continue;
40
+
41
+ for (const notification of slackThreads) {
42
+ if (notification.channel !== "slack" || !notification.messageTs) continue;
43
+ const notificationKey = `${notification.target}:${notification.messageTs}`;
44
+ const claim = await claimApprovalCancellationNotification(approval.id, notificationKey);
45
+ if (!claim) continue;
46
+ try {
47
+ await client.chat.postMessage({
48
+ channel: notification.target,
49
+ thread_ts: notification.messageTs,
50
+ text,
51
+ unfurl_links: false,
52
+ unfurl_media: false,
53
+ });
54
+ await completeApprovalCancellationNotificationClaim(
55
+ approval.id,
56
+ notificationKey,
57
+ claim.leaseToken,
58
+ );
59
+ } catch (error) {
60
+ await releaseApprovalCancellationNotificationClaim(
61
+ approval.id,
62
+ notificationKey,
63
+ claim.leaseToken,
64
+ );
65
+ console.error(
66
+ `[HITL] Failed to post cancellation update for approval ${approval.id}:`,
67
+ scrubSecrets(error instanceof Error ? error.message : String(error)),
68
+ );
69
+ }
70
+ }
71
+ }
72
+ }
@@ -83,16 +83,23 @@ export async function checkpointStepWaiting(
83
83
  runId: string,
84
84
  stepId: string,
85
85
  ctx: Record<string, unknown>,
86
- ): Promise<void> {
87
- await getDbClient().transaction(async () => {
88
- await updateWorkflowRunStep(stepId, {
89
- status: "waiting",
90
- });
86
+ ): Promise<boolean> {
87
+ return getDbClient().transaction(async () => {
88
+ const step = await getDbClient().get<{ id: string }>(
89
+ `UPDATE workflow_run_steps
90
+ SET status = 'waiting'
91
+ WHERE id = ? AND runId = ? AND status = 'running'
92
+ AND EXISTS (
93
+ SELECT 1 FROM workflow_runs
94
+ WHERE id = ? AND status IN ('running', 'waiting')
95
+ )
96
+ RETURNING id`,
97
+ [stepId, runId, runId],
98
+ );
99
+ if (!step) return false;
91
100
 
92
- await updateWorkflowRun(runId, {
93
- status: "waiting",
94
- context: ctx,
95
- });
101
+ await updateWorkflowRun(runId, { status: "waiting", context: ctx });
102
+ return true;
96
103
  });
97
104
  }
98
105
 
@@ -0,0 +1,31 @@
1
+ import { getLatestStepForNode } from "../be/db";
2
+ import type { WorkflowDefinition } from "../types";
3
+ import { getSuccessors } from "./definition";
4
+ import { resolveForeachParent } from "./foreach-join";
5
+
6
+ type CompletedStep = NonNullable<Awaited<ReturnType<typeof getLatestStepForNode>>>;
7
+
8
+ export async function loadCompletedStepRouting(
9
+ def: WorkflowDefinition,
10
+ runId: string,
11
+ completedNodeIds: Set<string>,
12
+ ): Promise<{ activeEdges: Set<string>; stepsByNodeId: Map<string, CompletedStep> }> {
13
+ const activeEdges = new Set<string>();
14
+ const stepsByNodeId = new Map<string, CompletedStep>();
15
+
16
+ for (const nodeId of completedNodeIds) {
17
+ if (resolveForeachParent(def, nodeId)) continue;
18
+
19
+ const step = await getLatestStepForNode(runId, nodeId);
20
+ if (step) stepsByNodeId.set(nodeId, step);
21
+
22
+ const successors = step?.nextPort
23
+ ? getSuccessors(def, nodeId, step.nextPort)
24
+ : getSuccessors(def, nodeId);
25
+ for (const successor of successors) {
26
+ activeEdges.add(`${nodeId}→${successor.id}`);
27
+ }
28
+ }
29
+
30
+ return { activeEdges, stepsByNodeId };
31
+ }