@desplega.ai/agent-swarm 1.167.0 → 1.169.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 (184) hide show
  1. package/dist/{acp-adapter-zpvy6321.js → acp-adapter-m47zfx4a.js} +2 -2
  2. package/dist/{actions-fq2zrwtf.js → actions-ws7k3tvq.js} +25 -19
  3. package/dist/{amp-adapter-5gbk7501.js → amp-adapter-nvv8k8j5.js} +1 -1
  4. package/dist/{app-nctetn1e.js → app-80w6qspx.js} +5 -5
  5. package/dist/{assistant-0aq8gjvj.js → assistant-jtq439kw.js} +29 -20
  6. package/dist/{boot-reembed-1ze5853n.js → boot-reembed-3nc0aaed.js} +4 -4
  7. package/dist/{boot-reembed-fmp5ww69.js → boot-reembed-feda8wn8.js} +4 -4
  8. package/dist/{boot-scrub-sweep-cbp8xd4e.js → boot-scrub-sweep-grsa2er0.js} +5 -5
  9. package/dist/{boot-seeds-22tx8a53.js → boot-seeds-gcfgq44f.js} +7 -7
  10. package/dist/{claude-adapter-0jhgeh1d.js → claude-adapter-pjv2q7t8.js} +7 -7
  11. package/dist/{claude-managed-adapter-tzwxy803.js → claude-managed-adapter-0wa8se82.js} +2 -2
  12. package/dist/{claude-managed-setup-nrhfx5b0.js → claude-managed-setup-zxa9p9tx.js} +1 -1
  13. package/dist/{claude-sdk-session-wh77rh83.js → claude-sdk-session-c9j6k9kx.js} +8772 -8663
  14. package/dist/{cli-c2tpw9ck.js → cli-3ry5gx4f.js} +1 -1
  15. package/dist/{cli-wydz3feg.js → cli-61tbnjpf.js} +214 -2472
  16. package/dist/{cli-cexctgn5.js → cli-7vz49g7h.js} +101 -34
  17. package/dist/{cli-6qxmjpem.js → cli-8xjst8q2.js} +34 -22
  18. package/dist/{cli-tx2zvn23.js → cli-91r17hb1.js} +3 -3
  19. package/dist/{cli-577bxkf6.js → cli-a8vps458.js} +182 -2208
  20. package/dist/{cli-tc5zec7v.js → cli-b7y8113e.js} +1 -1
  21. package/dist/{cli-69egj654.js → cli-bqrs5ycj.js} +1 -1
  22. package/dist/{cli-cf2vgn9d.js → cli-dv76vq5d.js} +30 -28
  23. package/dist/{cli-g5erf0zt.js → cli-e2prcx5m.js} +31 -0
  24. package/dist/{cli-c8r99akr.js → cli-e5qk3m2a.js} +209 -31
  25. package/dist/{cli-z1dqhc9y.js → cli-eqfbsxsn.js} +3 -3
  26. package/dist/{cli-kw0jcfje.js → cli-fyeft04q.js} +1 -1
  27. package/dist/{cli-5qkcdv8c.js → cli-gmdajfw5.js} +3 -3
  28. package/dist/{cli-ypgba1a8.js → cli-jkfxpg1b.js} +1 -1
  29. package/dist/{cli-mxvnnv5q.js → cli-jv36y8wz.js} +1 -1
  30. package/dist/{cli-m7vmeepg.js → cli-km8559dm.js} +6 -6
  31. package/dist/{cli-ykt8f7va.js → cli-kp3vfknv.js} +1 -1
  32. package/dist/{cli-ymmc1zxg.js → cli-m23r88re.js} +8 -5
  33. package/dist/{cli-v1pwxreq.js → cli-n4z21pt8.js} +1 -1
  34. package/dist/{cli-mxry91sa.js → cli-qg2zhjfc.js} +73 -4
  35. package/dist/{cli-1yrcfa5s.js → cli-rbr91whd.js} +4 -4
  36. package/dist/{cli-mkdsy1nq.js → cli-rg1c8ccw.js} +93 -7
  37. package/dist/cli-rh44xx46.js +2659 -0
  38. package/dist/cli-s0k2j7q9.js +2084 -0
  39. package/dist/{cli-ty4bzkqr.js → cli-sajr098b.js} +1 -1
  40. package/dist/{cli-6vfz44sr.js → cli-swrz3mj8.js} +1 -1
  41. package/dist/{cli-p8rt8spp.js → cli-vejds37t.js} +2 -2
  42. package/dist/{cli-jbzbr36n.js → cli-w6xa5nf5.js} +1 -1
  43. package/dist/{cli-9q3wt23f.js → cli-x6z90nq9.js} +1 -1
  44. package/dist/{cli-qzmzwegq.js → cli-ysnkvdx6.js} +102 -9
  45. package/dist/{cli-y0kjpfm9.js → cli-yvxfaem2.js} +2 -2
  46. package/dist/{cli-pkj1w46r.js → cli-z48ggfyq.js} +1 -1
  47. package/dist/{cli-6bpvv8h4.js → cli-zzev32jw.js} +1 -1
  48. package/dist/cli.js +20 -17
  49. package/dist/{codex-adapter-cgesnjkr.js → codex-adapter-0kpg09ca.js} +3 -3
  50. package/dist/{codex-hook-15dgc4cb.js → codex-hook-295jpbfa.js} +2 -2
  51. package/dist/{codex-session-runner-8kd7ddhr.js → codex-session-runner-8n672hxh.js} +3 -3
  52. package/dist/{commands-h6zaaxym.js → commands-mhmg5n0x.js} +4 -4
  53. package/dist/{cursor-adapter-v8mm6bp5.js → cursor-adapter-wn8n5zzk.js} +2 -2
  54. package/dist/{db-qg3myn53.js → db-wa6s1mwv.js} +4 -4
  55. package/dist/{dispatcher-7vnwj9qw.js → dispatcher-nhs2pkr8.js} +16 -14
  56. package/dist/{dsh-adapter-tz6hfp5r.js → dsh-adapter-5423p70m.js} +1 -1
  57. package/dist/{grok-adapter-jdt2dz2s.js → grok-adapter-28pkdt80.js} +3 -3
  58. package/dist/{handlers-2qtkpncr.js → handlers-5rtf9nng.js} +19 -17
  59. package/dist/{hook-51yfcgsv.js → hook-krjfkpzy.js} +4 -4
  60. package/dist/{hook-qnbdgwvd.js → hook-qkm73zk6.js} +5 -5
  61. package/dist/{http-jsmtag08.js → http-a397xcn4.js} +4845 -3484
  62. package/dist/{index-2t9kesn1.js → index-ac0v8v9d.js} +4 -4
  63. package/dist/{index-ekywyv2v.js → index-kqre45fr.js} +16 -14
  64. package/dist/{index-bt8wkv1w.js → index-rtxg15ah.js} +17 -15
  65. package/dist/{index-t4g06w93.js → index-vvsq7v45.js} +26 -23
  66. package/dist/{index-cx1sb9bq.js → index-ytdhtdry.js} +50 -11
  67. package/dist/{keepalive-4aezx4bj.js → keepalive-psfh8ysx.js} +6 -6
  68. package/dist/{lead-r44bkpm5.js → lead-m6rppjn7.js} +29 -27
  69. package/dist/{maintenance-grz5jrzh.js → maintenance-41f837ke.js} +4 -4
  70. package/dist/{node-qm9eb6z0.js → node-fkajaq96.js} +1 -1
  71. package/dist/{oauth-refresh-sweep-wm4ctjfj.js → oauth-refresh-sweep-6hr55hm8.js} +5 -5
  72. package/dist/{onboard-v60zgqf0.js → onboard-ktrtd7cz.js} +2 -2
  73. package/dist/{otel-impl-ghh4dyf8.js → otel-impl-dq4f2k3m.js} +1 -1
  74. package/dist/{pi-mono-adapter-0eg8kw2c.js → pi-mono-adapter-e5v9w0hv.js} +3 -2
  75. package/dist/{pricing-refresh-mjrqvnvw.js → pricing-refresh-kmk7j1ra.js} +6 -6
  76. package/dist/{rbac-roles-fd5tskj9.js → rbac-roles-9w1dkkxd.js} +4 -4
  77. package/dist/{rbac-roles-axv4d74x.js → rbac-roles-9ykx9tc5.js} +5 -5
  78. package/dist/{render-v2-k50j33qd.js → render-v2-ycqm53np.js} +5 -5
  79. package/dist/resume-cq2pkp8q.js +70 -0
  80. package/dist/{setup-4syh0pd2.js → setup-0dcmdvbn.js} +2 -2
  81. package/dist/{virtual-modules-ftr9kwe8.js → virtual-modules-1x9k4jcs.js} +1 -1
  82. package/dist/{work-objects-jcdbg8rr.js → work-objects-x01x2fh8.js} +19 -17
  83. package/dist/{worker-x8j8c9sx.js → worker-py1ssb1h.js} +29 -27
  84. package/openapi.json +15622 -15042
  85. package/package.json +8 -5
  86. package/src/apps/store.ts +1 -0
  87. package/src/artifact-sdk/feedback-overlay.ts +528 -0
  88. package/src/be/db/workflow-runs.ts +95 -0
  89. package/src/be/db-queries/slack-inbound.ts +489 -0
  90. package/src/be/db.ts +9 -2
  91. package/src/be/extensions/assets.ts +1 -1
  92. package/src/be/migrations/203_workflow_run_parent_step.sql +10 -0
  93. package/src/be/migrations/204_slack_inbound_receipts.sql +51 -0
  94. package/src/be/script-connections.ts +41 -7
  95. package/src/be/scripts/run-saved.ts +40 -20
  96. package/src/be/swarm-config-guard.ts +4 -0
  97. package/src/extensions/catalog.generated.json +17 -0
  98. package/src/extensions/catalog.ts +1 -1
  99. package/src/extensions/dispatcher.ts +3 -3
  100. package/src/heartbeat/heartbeat.ts +152 -2
  101. package/src/heartbeat/templates.ts +16 -0
  102. package/src/http/all-routes.ts +1 -0
  103. package/src/http/index.ts +2 -0
  104. package/src/http/integrations.ts +3 -1
  105. package/src/http/mcp-bridge.ts +1 -0
  106. package/src/http/pages-public.ts +7 -2
  107. package/src/http/pages.ts +178 -0
  108. package/src/http/script-connections.ts +35 -0
  109. package/src/http/slack-inbound.ts +107 -0
  110. package/src/http/tasks.ts +45 -27
  111. package/src/http/users.ts +1 -1
  112. package/src/http/webhooks.ts +20 -20
  113. package/src/prompts/session-templates.ts +32 -0
  114. package/src/providers/acp-targets.ts +1 -1
  115. package/src/providers/amp-adapter.ts +2 -2
  116. package/src/providers/claude-adapter.ts +6 -0
  117. package/src/providers/claude-background-keepalive.ts +100 -0
  118. package/src/providers/claude-managed-adapter.ts +2 -11
  119. package/src/providers/claude-sdk-session.ts +7 -1
  120. package/src/providers/dsh-adapter.ts +1 -1
  121. package/src/providers/opencode-adapter.ts +1 -2
  122. package/src/providers/pi-mono-adapter.ts +3 -3
  123. package/src/realtime/browser.generated.txt +8 -8
  124. package/src/realtime/browser.ts +3 -3
  125. package/src/scheduler/scheduler.ts +82 -13
  126. package/src/scripts-runtime/executors/quickjs-pool-config.ts +26 -0
  127. package/src/scripts-runtime/executors/quickjs.ts +9 -2
  128. package/src/scripts-runtime/executors/registry.ts +1 -1
  129. package/src/server.ts +1 -0
  130. package/src/slack/actions.ts +6 -2
  131. package/src/slack/app.ts +5 -0
  132. package/src/slack/assistant.ts +15 -5
  133. package/src/slack/egress-scrub.ts +3 -2
  134. package/src/slack/handlers.ts +36 -18
  135. package/src/slack/inbound-dispatch.ts +549 -0
  136. package/src/slack/inbound-files.ts +2 -0
  137. package/src/telemetry-context.ts +1 -0
  138. package/src/tests/claude-background-keepalive-wiring.test.ts +220 -0
  139. package/src/tests/claude-background-keepalive.fixture.ts +162 -0
  140. package/src/tests/claude-background-keepalive.test.ts +139 -0
  141. package/src/tests/codex-oauth-device.test.ts +1 -1
  142. package/src/tests/connection-embedded-auth.test.ts +51 -0
  143. package/src/tests/docs-index-footprint.test.js +3 -3
  144. package/src/tests/dsh-adapter.test.ts +1 -1
  145. package/src/tests/extensions-catalog.test.ts +1 -0
  146. package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -0
  147. package/src/tests/heartbeat-checklist.test.ts +243 -0
  148. package/src/tests/pages-feedback.test.ts +218 -0
  149. package/src/tests/schedule-target-type.test.ts +136 -0
  150. package/src/tests/script-connections-http.test.ts +45 -0
  151. package/src/tests/script-connections-mcp.test.ts +41 -0
  152. package/src/tests/script-connections.test.ts +2 -0
  153. package/src/tests/script-executor-quickjs-pool.test.ts +106 -0
  154. package/src/tests/script-executor-registry.test.ts +56 -0
  155. package/src/tests/send-task-self-send.test.ts +107 -0
  156. package/src/tests/slack-inbound-diagnostics-auth.test.ts +93 -0
  157. package/src/tests/slack-inbound-receipts.test.ts +684 -0
  158. package/src/tests/slack-session-status.test.ts +4 -2
  159. package/src/tests/ui-e2e-ingest-payload.test.ts +5 -1
  160. package/src/tests/user-token-routes.test.ts +1 -1
  161. package/src/tests/workflow-executors.test.ts +3 -2
  162. package/src/tests/workflow-sub-workflow.test.ts +363 -0
  163. package/src/tools/script-connections/tool.ts +35 -3
  164. package/src/tools/send-task.ts +9 -1
  165. package/src/utils/internal-ai/complete-structured.ts +6 -3
  166. package/src/utils/runtime-model-catalog.ts +1 -1
  167. package/src/workflows/engine.ts +17 -1
  168. package/src/workflows/executors/agent-task.ts +2 -2
  169. package/src/workflows/executors/human-in-the-loop.ts +3 -3
  170. package/src/workflows/executors/registry.ts +2 -0
  171. package/src/workflows/executors/script.ts +1 -1
  172. package/src/workflows/executors/sub-workflow.ts +135 -0
  173. package/src/workflows/executors/wait.ts +3 -3
  174. package/src/workflows/recovery.ts +11 -1
  175. package/src/workflows/resume.ts +77 -0
  176. package/src/workflows/wait-filter.ts +5 -7
  177. package/templates/extensions/README.md +1 -0
  178. package/templates/extensions/slack-reply-gate/README.md +98 -0
  179. package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
  180. package/templates/extensions/slack-reply-gate/manifest.json +8 -0
  181. package/templates/skills/heartbeat-runbook/content.md +3 -1
  182. package/templates/skills/pages/content.md +25 -0
  183. package/templates/skills/swarm-scripts/SKILL.md +10 -0
  184. package/templates/skills/swarm-scripts/content.md +10 -0
@@ -101,7 +101,11 @@ function propertyKeys(path: string[]): string[] {
101
101
  return Object.keys((current as { properties: Record<string, unknown> }).properties).sort();
102
102
  }
103
103
 
104
- function expectSchemaKeys(actual: object, path: string[], required: string[]): void {
104
+ function expectSchemaKeys<Actual extends object>(
105
+ actual: Actual,
106
+ path: string[],
107
+ required: string[],
108
+ ): void {
105
109
  const actualKeys = Object.keys(actual).sort();
106
110
  expect(actualKeys).toEqual(required.slice().sort());
107
111
  expect(actualKeys.every((key) => propertyKeys(path).includes(key))).toBe(true);
@@ -228,7 +228,7 @@ describe("ChatGPT connector codes", () => {
228
228
 
229
229
  type CodeBody = { code: string; expiresAt: string; connectUrl: string };
230
230
 
231
- async function createCode(userId: string, body: object = {}): Promise<CodeBody> {
231
+ async function createCode(userId: string, body: Record<string, unknown> = {}): Promise<CodeBody> {
232
232
  const response = await authedFetch(`/api/users/${userId}/connector-codes`, {
233
233
  method: "POST",
234
234
  body: JSON.stringify(body),
@@ -984,7 +984,7 @@ describe("ValidateExecutor", () => {
984
984
  // ─── Registry Wiring ─────────────────────────────────────────
985
985
 
986
986
  describe("createExecutorRegistry", () => {
987
- test("registers all 13 executors (9 instant + 4 async)", () => {
987
+ test("registers all 14 executors (9 instant + 5 async)", () => {
988
988
  const registry = createExecutorRegistry(mockDeps);
989
989
  const types = registry.types();
990
990
 
@@ -1001,7 +1001,8 @@ describe("createExecutorRegistry", () => {
1001
1001
  expect(types).toContain("foreach");
1002
1002
  expect(types).toContain("human-in-the-loop");
1003
1003
  expect(types).toContain("wait");
1004
- expect(types).toHaveLength(13);
1004
+ expect(types).toContain("sub-workflow");
1005
+ expect(types).toHaveLength(14);
1005
1006
  });
1006
1007
 
1007
1008
  test("instant executors have mode instant, async executors have mode async", () => {
@@ -0,0 +1,363 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { z } from "zod";
4
+ import * as db from "../be/db";
5
+ import {
6
+ closeDb,
7
+ createWorkflow,
8
+ getDbClient,
9
+ getWaitStateByStepId,
10
+ getWorkflowRun,
11
+ getWorkflowRunStepsByRunId,
12
+ initDb,
13
+ } from "../be/db";
14
+ import type { Workflow, WorkflowDefinition, WorkflowRun } from "../types";
15
+ import { startWorkflowExecution } from "../workflows/engine";
16
+ import { workflowEventBus } from "../workflows/event-bus";
17
+ import {
18
+ BaseExecutor,
19
+ type ExecutorDependencies,
20
+ type ExecutorInput,
21
+ type ExecutorResult,
22
+ } from "../workflows/executors/base";
23
+ import { createExecutorRegistry } from "../workflows/executors/registry";
24
+ import { MAX_SUB_WORKFLOW_DEPTH, SubWorkflowExecutor } from "../workflows/executors/sub-workflow";
25
+ import { recoverIncompleteRuns } from "../workflows/recovery";
26
+ import {
27
+ cancelWorkflowRun,
28
+ resumeWaitState,
29
+ setupWorkflowResumeListener,
30
+ } from "../workflows/resume";
31
+
32
+ const TEST_DB_PATH = "./test-workflow-sub-workflow.sqlite";
33
+
34
+ const deps: ExecutorDependencies = {
35
+ db,
36
+ eventBus: workflowEventBus,
37
+ interpolate: (t: string) => t,
38
+ };
39
+ const registry = createExecutorRegistry(deps);
40
+ let teardown: (() => void) | undefined;
41
+
42
+ /** Holds every `test-gate` node open until the test resolves it. */
43
+ let gate = Promise.withResolvers<void>();
44
+
45
+ /** An instant node that runs until the test opens the gate. */
46
+ class GateExecutor extends BaseExecutor<z.ZodObject, z.ZodObject> {
47
+ readonly type = "test-gate";
48
+ readonly mode = "instant" as const;
49
+ readonly configSchema = z.object({});
50
+ readonly outputSchema = z.object({});
51
+
52
+ protected async execute(): Promise<ExecutorResult<Record<string, never>>> {
53
+ await gate.promise;
54
+ return { status: "success", output: {} };
55
+ }
56
+ }
57
+ registry.register(new GateExecutor(deps));
58
+
59
+ /** Runs `fn` with the live resume listener off, then turns it back on. */
60
+ async function withoutListener(fn: () => Promise<void>): Promise<void> {
61
+ teardown?.();
62
+ try {
63
+ await fn();
64
+ } finally {
65
+ teardown = setupWorkflowResumeListener(workflowEventBus, registry);
66
+ }
67
+ }
68
+
69
+ async function makeWorkflow(def: WorkflowDefinition): Promise<Workflow> {
70
+ return createWorkflow({ name: `wf-${crypto.randomUUID()}`, definition: def });
71
+ }
72
+
73
+ /** A one-node child that checks `trigger.n === 1`. */
74
+ const instantChild: WorkflowDefinition = {
75
+ nodes: [
76
+ {
77
+ id: "check",
78
+ type: "property-match",
79
+ config: { conditions: [{ field: "trigger.n", op: "eq", value: 1 }] },
80
+ },
81
+ ],
82
+ };
83
+
84
+ /** A child whose only instant node runs until the gate opens. */
85
+ const gatedChild: WorkflowDefinition = {
86
+ nodes: [{ id: "gate", type: "test-gate", config: {} }],
87
+ };
88
+
89
+ /** A child that parks on a long time wait, so it stays `waiting`. */
90
+ const waitingChild: WorkflowDefinition = {
91
+ nodes: [{ id: "pause", type: "wait", config: { mode: "time", durationMs: 3_600_000 } }],
92
+ };
93
+
94
+ function parentOf(childId: string): WorkflowDefinition {
95
+ return {
96
+ nodes: [
97
+ {
98
+ id: "child",
99
+ type: "sub-workflow",
100
+ config: { workflowId: childId, inputs: { n: 1 } },
101
+ },
102
+ ],
103
+ };
104
+ }
105
+
106
+ async function childRunsOf(workflowId: string): Promise<{ id: string; parentStepId: string }[]> {
107
+ return getDbClient().query<{ id: string; parentStepId: string }>(
108
+ "SELECT id, parentStepId FROM workflow_runs WHERE workflowId = ?",
109
+ [workflowId],
110
+ );
111
+ }
112
+
113
+ async function waitForRun(
114
+ runId: string,
115
+ done: (run: WorkflowRun) => boolean,
116
+ ): Promise<WorkflowRun> {
117
+ for (let i = 0; i < 250; i++) {
118
+ const run = await getWorkflowRun(runId);
119
+ if (run && done(run)) return run;
120
+ await Bun.sleep(20);
121
+ }
122
+ throw new Error(`run ${runId} did not settle`);
123
+ }
124
+
125
+ const isDone = (run: WorkflowRun) => run.status !== "running" && run.status !== "waiting";
126
+
127
+ async function childRunOf(workflowId: string, status: string): Promise<WorkflowRun> {
128
+ for (let i = 0; i < 250; i++) {
129
+ const [row] = await childRunsOf(workflowId);
130
+ if (row) return waitForRun(row.id, (r) => r.status === status);
131
+ await Bun.sleep(20);
132
+ }
133
+ throw new Error(`no child run of ${workflowId}`);
134
+ }
135
+
136
+ beforeAll(() => {
137
+ initDb(TEST_DB_PATH);
138
+ teardown = setupWorkflowResumeListener(workflowEventBus, registry);
139
+ });
140
+
141
+ afterAll(async () => {
142
+ teardown?.();
143
+ closeDb();
144
+ for (const suffix of ["", "-wal", "-shm"]) {
145
+ await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
146
+ }
147
+ });
148
+
149
+ describe("sub-workflow node", () => {
150
+ test("runs an instant child and exposes its node outputs", async () => {
151
+ const child = await makeWorkflow(instantChild);
152
+ const parent = await makeWorkflow(parentOf(child.id));
153
+
154
+ const runId = await startWorkflowExecution(parent, {}, registry);
155
+
156
+ const run = await waitForRun(runId, isDone);
157
+ expect(run?.status).toBe("completed");
158
+ const [childRun] = await childRunsOf(child.id);
159
+ const output = run?.context?.child as { runId: string; outputs: Record<string, unknown> };
160
+ expect(output.runId).toBe(childRun!.id);
161
+ expect(output.outputs.check).toMatchObject({ passed: true });
162
+ expect(output.outputs.trigger).toBeUndefined();
163
+ });
164
+
165
+ test("a failed child fails the node and the parent run", async () => {
166
+ const child = await makeWorkflow({
167
+ nodes: [{ id: "bad", type: "property-match", config: { conditions: [] } }],
168
+ });
169
+ const parent = await makeWorkflow(parentOf(child.id));
170
+
171
+ const runId = await startWorkflowExecution(parent, {}, registry);
172
+
173
+ const run = await waitForRun(runId, isDone);
174
+ expect(run?.status).toBe("failed");
175
+ const [step] = await getWorkflowRunStepsByRunId(runId);
176
+ expect(step?.status).toBe("failed");
177
+ expect(step?.error).toContain("failed");
178
+ });
179
+
180
+ test("parent waits for an async child and resumes when it completes", async () => {
181
+ const child = await makeWorkflow(waitingChild);
182
+ const parent = await makeWorkflow(parentOf(child.id));
183
+
184
+ const runId = await startWorkflowExecution(parent, {}, registry);
185
+ expect((await getWorkflowRun(runId))?.status).toBe("waiting");
186
+ const childRun = await childRunOf(child.id, "waiting");
187
+
188
+ const [childStep] = await getWorkflowRunStepsByRunId(childRun.id);
189
+ const wait = await getWaitStateByStepId(childStep!.id);
190
+ await resumeWaitState(wait!.id, "fired", undefined, registry);
191
+
192
+ const run = await waitForRun(runId, (r) => r.status !== "waiting");
193
+ expect(run.status).toBe("completed");
194
+ expect((run.context?.child as { runId: string }).runId).toBe(childRun.id);
195
+ });
196
+
197
+ test("a cancelled child fails the waiting parent step", async () => {
198
+ const child = await makeWorkflow(waitingChild);
199
+ const parent = await makeWorkflow(parentOf(child.id));
200
+
201
+ const runId = await startWorkflowExecution(parent, {}, registry);
202
+ const childRun = await childRunOf(child.id, "waiting");
203
+ await cancelWorkflowRun(childRun.id, "stop");
204
+
205
+ const run = await waitForRun(runId, (r) => r.status !== "waiting");
206
+ expect(run.status).toBe("failed");
207
+ expect(run.error).toContain(`Child workflow run ${childRun.id} cancelled`);
208
+ });
209
+
210
+ test("re-executing the step reconnects to the existing child; recovery resumes it", () =>
211
+ // No live listener for this test: the child finishing is only seen by recovery.
212
+ withoutListener(async () => {
213
+ const child = await makeWorkflow(waitingChild);
214
+ const parent = await makeWorkflow(parentOf(child.id));
215
+
216
+ const runId = await startWorkflowExecution(parent, {}, registry);
217
+ const [parentStep] = await getWorkflowRunStepsByRunId(runId);
218
+ const childRun = await childRunOf(child.id, "waiting");
219
+
220
+ // A restart re-runs the executor for the same step.
221
+ const again = await registry.get("sub-workflow").run({
222
+ config: { workflowId: child.id, inputs: { n: 1 } },
223
+ context: {},
224
+ meta: {
225
+ runId,
226
+ stepId: parentStep!.id,
227
+ nodeId: "child",
228
+ workflowId: parent.id,
229
+ dryRun: false,
230
+ },
231
+ });
232
+ expect(again).toMatchObject({ async: true, correlationId: childRun.id });
233
+ expect(await childRunsOf(child.id)).toHaveLength(1);
234
+
235
+ // The child finishes while nothing listens.
236
+ const [childStep] = await getWorkflowRunStepsByRunId(childRun.id);
237
+ const wait = await getWaitStateByStepId(childStep!.id);
238
+ await resumeWaitState(wait!.id, "fired", undefined, registry);
239
+ expect((await getWorkflowRun(childRun.id))?.status).toBe("completed");
240
+ expect((await getWorkflowRun(runId))?.status).toBe("waiting");
241
+
242
+ await recoverIncompleteRuns(registry);
243
+ expect((await getWorkflowRun(runId))?.status).toBe("completed");
244
+ expect(await childRunsOf(child.id)).toHaveLength(1);
245
+ }));
246
+
247
+ test("a slow instant child runs outside the parent executor's timeout", async () => {
248
+ // The gate holds the child's instant node open for as long as the test
249
+ // wants, past any watchdog. The parent's executor call returns regardless.
250
+ gate = Promise.withResolvers<void>();
251
+ const child = await makeWorkflow(gatedChild);
252
+ const parent = await makeWorkflow(parentOf(child.id));
253
+
254
+ const runId = await startWorkflowExecution(parent, {}, registry);
255
+ expect((await getWorkflowRun(runId))?.status).toBe("waiting");
256
+ const [childRun] = await childRunsOf(child.id);
257
+ expect((await getWorkflowRun(childRun!.id))?.status).toBe("running");
258
+
259
+ gate.resolve();
260
+ const run = await waitForRun(runId, isDone);
261
+ expect(run.status).toBe("completed");
262
+ expect((run.context?.child as { runId: string }).runId).toBe(childRun!.id);
263
+ });
264
+
265
+ test("a child that finishes before the parent step parks still resumes it", () =>
266
+ // No listener: the child's terminal event is lost, as in the race where it
267
+ // fires before the step is waiting. Only the post-park recheck can resume.
268
+ withoutListener(async () => {
269
+ gate = Promise.withResolvers<void>();
270
+ // Delays the executor's result, so parking happens after the child ends.
271
+ class LateParkExecutor extends SubWorkflowExecutor {
272
+ override async run(input: ExecutorInput) {
273
+ const result = await super.run(input);
274
+ const { correlationId } = result as { correlationId?: string };
275
+ gate.resolve();
276
+ if (correlationId) await waitForRun(correlationId, isDone);
277
+ return result;
278
+ }
279
+ }
280
+ const lateRegistry = createExecutorRegistry(deps);
281
+ lateRegistry.register(new GateExecutor(deps));
282
+ lateRegistry.register(new LateParkExecutor(deps, lateRegistry));
283
+ const child = await makeWorkflow(gatedChild);
284
+ const parent = await makeWorkflow(parentOf(child.id));
285
+
286
+ const runId = await startWorkflowExecution(parent, {}, lateRegistry);
287
+
288
+ expect((await getWorkflowRun(runId))?.status).toBe("completed");
289
+ }));
290
+
291
+ test("deleting an active child's workflow fails the waiting parent step", async () => {
292
+ const child = await makeWorkflow(waitingChild);
293
+ const parent = await makeWorkflow(parentOf(child.id));
294
+ const runId = await startWorkflowExecution(parent, {}, registry);
295
+ await childRunOf(child.id, "waiting");
296
+
297
+ await db.deleteWorkflow(child.id);
298
+
299
+ const run = await waitForRun(runId, isDone);
300
+ expect(run.status).toBe("failed");
301
+ expect(run.error).toContain("was deleted");
302
+ });
303
+
304
+ test("recovery fails a waiting parent step whose child row is gone", () =>
305
+ withoutListener(async () => {
306
+ const child = await makeWorkflow(waitingChild);
307
+ const parent = await makeWorkflow(parentOf(child.id));
308
+ const runId = await startWorkflowExecution(parent, {}, registry);
309
+ await childRunOf(child.id, "waiting");
310
+
311
+ await db.deleteWorkflow(child.id);
312
+ expect((await getWorkflowRun(runId))?.status).toBe("waiting");
313
+
314
+ await recoverIncompleteRuns(registry);
315
+ const run = await getWorkflowRun(runId);
316
+ expect(run?.status).toBe("failed");
317
+ expect(run?.error).toContain("was deleted");
318
+ }));
319
+
320
+ test("rejects a workflow that invokes itself", async () => {
321
+ const self = await makeWorkflow({ nodes: [] });
322
+ await db.updateWorkflow(self.id, { definition: parentOf(self.id) });
323
+ const wf = await db.getWorkflow(self.id);
324
+
325
+ const runId = await startWorkflowExecution(wf!, {}, registry);
326
+
327
+ const run = await getWorkflowRun(runId);
328
+ expect(run?.status).toBe("failed");
329
+ const [step] = await getWorkflowRunStepsByRunId(runId);
330
+ expect(step?.error).toContain("Sub-workflow recursion");
331
+ expect(await childRunsOf(self.id)).toHaveLength(1); // only the parent run itself
332
+ });
333
+
334
+ test("rejects an indirect cycle through an ancestor run", async () => {
335
+ const a = await makeWorkflow({ nodes: [] });
336
+ const b = await makeWorkflow(parentOf(a.id));
337
+ await db.updateWorkflow(a.id, { definition: parentOf(b.id) });
338
+ const wfA = await db.getWorkflow(a.id);
339
+
340
+ const runId = await startWorkflowExecution(wfA!, {}, registry);
341
+
342
+ const run = await waitForRun(runId, isDone);
343
+ expect(run.status).toBe("failed");
344
+ expect(run.error).toContain("Sub-workflow recursion");
345
+ expect(await childRunsOf(b.id)).toHaveLength(1);
346
+ expect(await childRunsOf(a.id)).toHaveLength(1);
347
+ });
348
+
349
+ test("fails closed past the max nesting depth", async () => {
350
+ // A chain of distinct workflows, one level deeper than allowed.
351
+ let next = await makeWorkflow(instantChild);
352
+ for (let i = 0; i <= MAX_SUB_WORKFLOW_DEPTH; i++) next = await makeWorkflow(parentOf(next.id));
353
+ const root = next;
354
+
355
+ const runId = await startWorkflowExecution(root, {}, registry);
356
+
357
+ expect((await waitForRun(runId, isDone)).status).toBe("failed");
358
+ const rows = await getDbClient().query<{ n: number }>(
359
+ "SELECT COUNT(*) AS n FROM workflow_run_steps WHERE error LIKE 'Sub-workflow nesting exceeds%'",
360
+ );
361
+ expect(rows[0]?.n).toBe(1);
362
+ });
363
+ });
@@ -4,6 +4,7 @@ import { getAgentById } from "@/be/db";
4
4
  import {
5
5
  ConnectionAuthInputSchema,
6
6
  connectionAuthInputFromFlat,
7
+ deleteScriptConnection,
7
8
  getScriptConnectionById,
8
9
  listScriptConnections,
9
10
  refreshScriptConnection,
@@ -16,13 +17,23 @@ import { resolveScopedResourceId, scopedResourceScopeIdSchema } from "@/utils/sc
16
17
 
17
18
  const scriptConnectionsInputSchema = z.object({
18
19
  action: z
19
- .enum(["list", "upsert-openapi", "upsert-mcp", "upsert-graphql", "refresh", "disable"])
20
- .describe("List, create/update, refresh, or disable a script connection."),
20
+ .enum([
21
+ "list",
22
+ "upsert-openapi",
23
+ "upsert-mcp",
24
+ "upsert-graphql",
25
+ "refresh",
26
+ "disable",
27
+ "delete",
28
+ ])
29
+ .describe(
30
+ "List, create/update, refresh, disable, or delete a script connection. delete is permanent: it removes the connection, its managed credential binding, and any inline secret it owns.",
31
+ ),
21
32
  id: z
22
33
  .string()
23
34
  .uuid()
24
35
  .optional()
25
- .describe("Existing connection ID for update, refresh, or disable."),
36
+ .describe("Existing connection ID for update, refresh, disable, or delete."),
26
37
  slug: z
27
38
  .string()
28
39
  .min(1)
@@ -193,6 +204,27 @@ export const registerScriptConnectionsTool = (server: McpServer) => {
193
204
  });
194
205
  }
195
206
 
207
+ if (args.action === "delete") {
208
+ if (!args.id) {
209
+ return toolErr("id is required for delete.", {
210
+ data: {
211
+ yourAgentId: requestInfo.agentId,
212
+ connections: listScriptConnections({ includeDisabled: true, allScopes: true }),
213
+ },
214
+ });
215
+ }
216
+ const deleted = await deleteScriptConnection(args.id);
217
+ const connections = listScriptConnections({ includeDisabled: true, allScopes: true });
218
+ if (!deleted) {
219
+ return toolErr(`Script connection ${args.id} not found.`, {
220
+ data: { yourAgentId: requestInfo.agentId, connections },
221
+ });
222
+ }
223
+ return toolOk(`Script connection ${deleted.slug} deleted.`, {
224
+ data: { yourAgentId: requestInfo.agentId, connections },
225
+ });
226
+ }
227
+
196
228
  if (args.action === "refresh") {
197
229
  if (!args.id) {
198
230
  return toolErr("id is required for refresh.", {
@@ -355,7 +355,15 @@ export async function sendTaskHandler(
355
355
  });
356
356
  }
357
357
 
358
- if (ctx.kind === "owner" && requestedAgentId === ctx.agentId) {
358
+ // The guard stops an agent session from handing work back to itself. A script runs as its
359
+ // owner (a scheduled script as the schedule's creator), so a Lead-owned poller that files
360
+ // work for the Lead is legitimate. The bridge marks script calls `script-sdk`; a caller that
361
+ // spoofs it can only send itself a task, which grants nothing it could not already do.
362
+ if (
363
+ ctx.kind === "owner" &&
364
+ requestedAgentId === ctx.agentId &&
365
+ ctx.requestInfo?.callOrigin !== "script-sdk"
366
+ ) {
359
367
  return toolErr("Cannot send a task to yourself, are you drunk?", {
360
368
  data: { yourAgentId: ctx.agentId },
361
369
  });
@@ -66,7 +66,7 @@ export interface CompleteStructuredOptions<TZod extends z.ZodTypeAny> {
66
66
  prompt: string,
67
67
  model: string,
68
68
  signal?: AbortSignal,
69
- jsonSchema?: object,
69
+ jsonSchema?: Record<string, unknown>,
70
70
  ) => Promise<string>;
71
71
  /**
72
72
  * Bypass `resolveCredential` entirely — opencode auth path (and tests)
@@ -111,7 +111,7 @@ export async function defaultSpawnClaudeCli(
111
111
  prompt: string,
112
112
  model: string,
113
113
  signal?: AbortSignal,
114
- jsonSchema?: object,
114
+ jsonSchema?: Record<string, unknown>,
115
115
  ): Promise<string> {
116
116
  // SWARM_USE_CLAUDE_BRIDGE mirrors the main claude adapter's subscription-pool
117
117
  // routing. Otherwise CLAUDE_BINARY may be a single binary, an absolute path,
@@ -260,7 +260,10 @@ export async function completeStructuredWithModel<TZod extends z.ZodTypeAny>(
260
260
  // The CLI flag alone is unreliable — claude sometimes asks "where's
261
261
  // the schema?" if the prompt doesn't reference one.
262
262
  // The pinned Claude executable validates schemas with Draft 7.
263
- const jsonSchema = z.toJSONSchema(opts.zodSchema, { target: "draft-7" }) as object;
263
+ const jsonSchema = z.toJSONSchema(opts.zodSchema, { target: "draft-7" }) as Record<
264
+ string,
265
+ unknown
266
+ >;
264
267
  const schemaStr = JSON.stringify(jsonSchema);
265
268
  const claudeUserPrompt = `${opts.userPrompt}\n\nRespond with ONLY a JSON object (no prose, no code fences) matching this schema:\n${schemaStr}`;
266
269
  let lastErr: unknown = null;
@@ -38,7 +38,7 @@ export interface RuntimeCatalogProvider {
38
38
 
39
39
  export type RuntimeCatalog = Record<string, RuntimeCatalogProvider | undefined>;
40
40
 
41
- const SNAPSHOT = modelsDevSnapshot as unknown as RuntimeCatalog;
41
+ const SNAPSHOT = modelsDevSnapshot as RuntimeCatalog;
42
42
 
43
43
  /** Default freshness window for the worker-side HTTP pull. */
44
44
  export const RUNTIME_CATALOG_TTL_MS = 10 * 60_000;
@@ -54,6 +54,8 @@ function maxIterations(): number {
54
54
  export interface WorkflowExecutionOptions {
55
55
  requestedByUserId?: string;
56
56
  triggerType?: "schedule" | "manual" | "event" | "api";
57
+ /** Set when a `sub-workflow` step starts this run as its child. */
58
+ parentStepId?: string;
57
59
  }
58
60
 
59
61
  /**
@@ -133,6 +135,7 @@ export async function startWorkflowExecution(
133
135
  triggerType: options.triggerType ?? "manual",
134
136
  triggerData,
135
137
  createdBy: options.requestedByUserId,
138
+ parentStepId: options.parentStepId,
136
139
  });
137
140
  await updateWorkflowRun(runId, {
138
141
  status: "skipped",
@@ -149,6 +152,7 @@ export async function startWorkflowExecution(
149
152
  triggerType: options.triggerType ?? "manual",
150
153
  triggerData,
151
154
  createdBy: options.requestedByUserId,
155
+ parentStepId: options.parentStepId,
152
156
  });
153
157
  telemetry.workflow("started", {
154
158
  workflowId: workflow.id,
@@ -195,7 +199,7 @@ export async function startWorkflowExecution(
195
199
 
196
200
  const entryNodes = findEntryNodes(workflow.definition);
197
201
  const secretKeys = getSecretInputKeys(workflow.input);
198
- await walkGraph(
202
+ const walk = walkGraph(
199
203
  workflow.definition,
200
204
  runId,
201
205
  ctx,
@@ -205,6 +209,13 @@ export async function startWorkflowExecution(
205
209
  secretKeys,
206
210
  options,
207
211
  );
212
+ // A child run walks on its own, outside the parent executor's timeout. Its
213
+ // terminal status wakes the parent step (`resumeFromChildRun`).
214
+ if (options.parentStepId) {
215
+ walk.catch((err) => console.error(`[workflows] Child workflow run ${runId} failed:`, err));
216
+ return runId;
217
+ }
218
+ await walk;
208
219
  return runId;
209
220
  }
210
221
 
@@ -939,6 +950,11 @@ async function runClaimedStep(
939
950
  // Check for async result
940
951
  if ("async" in result && (result as AsyncExecutorResult).async) {
941
952
  const waiting = await checkpointStepWaiting(runId, stepId, ctx);
953
+ // A child run that finished before the step parked had its event dropped.
954
+ if (waiting && (result as AsyncExecutorResult).waitFor === "workflow.child.finished") {
955
+ const { resumeFromChildRun } = await import("./resume");
956
+ await resumeFromChildRun(stepId, registry);
957
+ }
942
958
  return { outcome: waiting ? "waiting" : "completed", successors: [] };
943
959
  }
944
960
 
@@ -83,7 +83,7 @@ export class AgentTaskExecutor extends BaseExecutor<
83
83
  async: true,
84
84
  waitFor: "task.completed",
85
85
  correlationId: existingTask.id,
86
- } as unknown as ExecutorResult<AgentTaskOutput>;
86
+ } as ExecutorResult<AgentTaskOutput>;
87
87
  }
88
88
 
89
89
  // 2. Inherit workflow-level dir/vcsRepo when node config doesn't specify them
@@ -154,6 +154,6 @@ export class AgentTaskExecutor extends BaseExecutor<
154
154
  async: true,
155
155
  waitFor: "task.completed",
156
156
  correlationId: task.id,
157
- } as unknown as ExecutorResult<AgentTaskOutput>;
157
+ } as ExecutorResult<AgentTaskOutput>;
158
158
  }
159
159
  }
@@ -286,7 +286,7 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
286
286
  async: true,
287
287
  waitFor: "approval.resolved",
288
288
  correlationId: existing.id,
289
- } as unknown as ExecutorResult<HITLOutput>;
289
+ } as ExecutorResult<HITLOutput>;
290
290
  }
291
291
 
292
292
  // 2. Create the approval request. `run` already replaced a dynamic token
@@ -314,7 +314,7 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
314
314
  async: true,
315
315
  waitFor: "approval.resolved",
316
316
  correlationId: requestId,
317
- } as unknown as ExecutorResult<HITLOutput>;
317
+ } as ExecutorResult<HITLOutput>;
318
318
  }
319
319
 
320
320
  // 3. Dispatch notifications (fire-and-forget — failures must not block the workflow)
@@ -333,7 +333,7 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
333
333
  async: true,
334
334
  waitFor: "approval.resolved",
335
335
  correlationId: requestId,
336
- } as unknown as ExecutorResult<HITLOutput>;
336
+ } as ExecutorResult<HITLOutput>;
337
337
  }
338
338
 
339
339
  /** Dispatch notifications for each configured channel. Updates DB with messageTs on success. */
@@ -8,6 +8,7 @@ import { NotifyExecutor } from "./notify";
8
8
  import { PropertyMatchExecutor } from "./property-match";
9
9
  import { RawLlmExecutor } from "./raw-llm";
10
10
  import { ScriptExecutor } from "./script";
11
+ import { SubWorkflowExecutor } from "./sub-workflow";
11
12
  import { SwarmScriptExecutor } from "./swarm-script";
12
13
  import { SystemOneDecisionExecutor } from "./system-one-decision";
13
14
  import { ValidateExecutor } from "./validate";
@@ -81,6 +82,7 @@ export function createExecutorRegistry(deps: ExecutorDependencies): ExecutorRegi
81
82
  registry.register(new ForeachExecutor(deps));
82
83
  registry.register(new HumanInTheLoopExecutor(deps));
83
84
  registry.register(new WaitExecutor(deps));
85
+ registry.register(new SubWorkflowExecutor(deps, registry));
84
86
 
85
87
  return registry;
86
88
  }
@@ -98,7 +98,7 @@ export class ScriptExecutor extends BaseExecutor<
98
98
  return {
99
99
  status: "failed",
100
100
  error: result.stderr || `Script exited with code ${result.exitCode}`,
101
- output: result as unknown as z.infer<typeof ScriptOutputSchema>,
101
+ output: result as z.infer<typeof ScriptOutputSchema>,
102
102
  };
103
103
  }
104
104