@desplega.ai/agent-swarm 2.5.2 → 2.5.4

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 (236) hide show
  1. package/dist/{acp-adapter-csh0zz3b.js → acp-adapter-ek38cwsv.js} +5 -5
  2. package/dist/{actions-6xx079d1.js → actions-92mepq03.js} +19 -19
  3. package/dist/{amp-adapter-8n6f0hz8.js → amp-adapter-9gaxg015.js} +4 -4
  4. package/dist/{app-ft6r6v23.js → app-ctn8gsgx.js} +3 -3
  5. package/dist/{assistant-vt2cyfzd.js → assistant-zyr5kz47.js} +21 -21
  6. package/dist/{boot-reembed-tgrfy27t.js → boot-reembed-dwzfttdr.js} +1 -1
  7. package/dist/{boot-reembed-r3e808ky.js → boot-reembed-w75maga2.js} +1 -1
  8. package/dist/{boot-scrub-sweep-masdwxkm.js → boot-scrub-sweep-e9kq43kf.js} +3 -3
  9. package/dist/{boot-seeds-0n82qc7q.js → boot-seeds-x28v3fhb.js} +4 -4
  10. package/dist/{claude-adapter-s7j47k06.js → claude-adapter-f0afqnne.js} +5 -5
  11. package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-x81a7r65.js} +3 -3
  12. package/dist/{claude-managed-setup-3hzrgrnk.js → claude-managed-setup-bqzwjd9x.js} +2 -2
  13. package/dist/{claude-sdk-session-0zrdzyx2.js → claude-sdk-session-7b7whkaq.js} +9307 -9254
  14. package/dist/{cli-a5ebc95n.js → cli-0shx9j93.js} +16 -13
  15. package/dist/{cli-4qkg9968.js → cli-19202kew.js} +4 -4
  16. package/dist/{cli-cyfgxnvb.js → cli-1r5ad7j5.js} +19 -17
  17. package/dist/{cli-jmcjbjkt.js → cli-25n3mv5d.js} +9 -7
  18. package/dist/cli-2655k16q.js +140 -0
  19. package/dist/{cli-wadzapwb.js → cli-277c31dw.js} +1 -1
  20. package/dist/{cli-n6kmqngw.js → cli-4b2z6n0m.js} +29 -7
  21. package/dist/{cli-7ccf7akh.js → cli-5bnsc0e1.js} +9 -7
  22. package/dist/{cli-qrjv7t18.js → cli-621ce7kw.js} +1 -1
  23. package/dist/{cli-xzge46sj.js → cli-64mh9mf3.js} +20 -6
  24. package/dist/{cli-b6tnjdvx.js → cli-6a1xg1p7.js} +6 -2
  25. package/dist/{cli-ydg6ep51.js → cli-6n8axjv1.js} +1 -1
  26. package/dist/{cli-e91vshb1.js → cli-7p4jxgtt.js} +5 -5
  27. package/dist/{cli-f3pfn4x3.js → cli-9j129brq.js} +9 -3
  28. package/dist/{cli-xw9q173h.js → cli-9s05422w.js} +1 -1
  29. package/dist/{cli-15xd0j7w.js → cli-abs8ndtr.js} +12 -12
  30. package/dist/{cli-h5d22pfs.js → cli-asq5ydm4.js} +219 -309
  31. package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
  32. package/dist/{cli-0vgy5wfw.js → cli-bvpgr9ny.js} +2 -2
  33. package/dist/{cli-3e9vs93g.js → cli-c61mbdds.js} +4 -4
  34. package/dist/{cli-e5bxnjps.js → cli-c952c7ac.js} +5 -5
  35. package/dist/{cli-p859hcc8.js → cli-dew6x255.js} +1 -1
  36. package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
  37. package/dist/{cli-gpv60278.js → cli-e42vss7b.js} +2 -2
  38. package/dist/{cli-b63d08rs.js → cli-eftzyh9y.js} +1 -1
  39. package/dist/{cli-fgn7yg5r.js → cli-ethgy7ha.js} +1 -1
  40. package/dist/{cli-86zpfje5.js → cli-ff1tm9n9.js} +1 -1
  41. package/dist/{cli-cf63dnxj.js → cli-g9vd9ece.js} +37 -12
  42. package/dist/{cli-j3dwd4kz.js → cli-gbfxp48k.js} +770 -126
  43. package/dist/{cli-n2918sed.js → cli-gn5af0x5.js} +21 -21
  44. package/dist/{cli-d172svcw.js → cli-gwhjmh8g.js} +4 -4
  45. package/dist/{cli-stxdrqs2.js → cli-hh5we352.js} +24 -5
  46. package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
  47. package/dist/{cli-csh8fnbg.js → cli-jb30dzvk.js} +473 -58
  48. package/dist/{cli-vaxw3bg0.js → cli-jbnn4vxb.js} +3 -3
  49. package/dist/{cli-ha22h0eg.js → cli-k4tjsdx9.js} +45 -40
  50. package/dist/{cli-f55b6ky0.js → cli-m4ecdxvz.js} +40 -13
  51. package/dist/{cli-mntxtkhn.js → cli-mz9afxxf.js} +2 -1
  52. package/dist/{cli-ta8bd7th.js → cli-pzatvw94.js} +1 -1
  53. package/dist/{cli-3qv8naq8.js → cli-rr7zx19j.js} +1591 -353
  54. package/dist/{cli-b1tv22wr.js → cli-s682zgqz.js} +1 -1
  55. package/dist/{cli-bpbcpd8b.js → cli-sbwsxvtd.js} +1 -1
  56. package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
  57. package/dist/{cli-6y43v79h.js → cli-tsma7p8v.js} +1 -1
  58. package/dist/{cli-edtm741g.js → cli-wghjkr0e.js} +5 -5
  59. package/dist/{cli-aaqnqpyw.js → cli-wx2nj19m.js} +30 -11
  60. package/dist/{cli-y3dzwyy3.js → cli-x0f2v69c.js} +180 -60
  61. package/dist/{cli-kcnwn85g.js → cli-xtrz6bzs.js} +7 -7
  62. package/dist/{cli-14v1sxxy.js → cli-xzfq4at3.js} +7 -7
  63. package/dist/cli.js +20 -20
  64. package/dist/{codex-adapter-brh2tcxr.js → codex-adapter-wch12q99.js} +3 -3
  65. package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
  66. package/dist/{codex-session-runner-0dnj21qs.js → codex-session-runner-41pfcr95.js} +2 -2
  67. package/dist/{commands-1cnvseqx.js → commands-9849dkj6.js} +2 -2
  68. package/dist/{controller-kc7s9xhn.js → controller-ja0sd95p.js} +29 -3
  69. package/dist/{controllers-connect-jzx7t4bn.js → controllers-connect-xp05dj6x.js} +20 -20
  70. package/dist/{cursor-adapter-qpwp5sfg.js → cursor-adapter-623qxqew.js} +1 -2
  71. package/dist/{db-4vq88jkc.js → db-47vj40dy.js} +3 -3
  72. package/dist/{db-v5ytg5pw.js → db-w759qmx0.js} +2 -2
  73. package/dist/{dispatcher-trtcrqae.js → dispatcher-7b8e4z2j.js} +17 -17
  74. package/dist/{docker-nfy2gc8t.js → docker-s84hjs91.js} +2 -2
  75. package/dist/{dsh-adapter-f20xwn1z.js → dsh-adapter-2ezw3xea.js} +2 -3
  76. package/dist/{external-channel-n9bymc2n.js → external-channel-e2s028da.js} +2 -2
  77. package/dist/{grok-adapter-4kpmmnr6.js → grok-adapter-ys7gaqee.js} +6 -6
  78. package/dist/{handlers-ptsraqa8.js → handlers-wy1dr6ja.js} +20 -20
  79. package/dist/{heartbeat-60qtxtjf.js → heartbeat-w3mntghj.js} +20 -20
  80. package/dist/{hook-rbktcstg.js → hook-3jjywnpb.js} +9 -3
  81. package/dist/{hook-ngtvh729.js → hook-g7kvv6x9.js} +3 -2
  82. package/dist/{http-z8xy72gp.js → http-tkp7d4kb.js} +2984 -2483
  83. package/dist/{index-r70q0xjx.js → index-1ef1myq2.js} +17 -17
  84. package/dist/{index-a9nsavhx.js → index-3n8m9ea2.js} +1 -1
  85. package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
  86. package/dist/{index-s193j5fh.js → index-nmm79qgr.js} +20 -20
  87. package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
  88. package/dist/{index-06qkmcx6.js → index-q7m26kvs.js} +33 -23
  89. package/dist/{index-1cscynkb.js → index-xdaahthb.js} +17 -17
  90. package/dist/{keepalive-a9srcphj.js → keepalive-v4ta8wgg.js} +3 -3
  91. package/dist/{lead-3devneqy.js → lead-sawnyqxk.js} +35 -35
  92. package/dist/{maintenance-0er16h2y.js → maintenance-84n1ex21.js} +1 -1
  93. package/dist/{oauth-refresh-sweep-b65jcyw2.js → oauth-refresh-sweep-vjnnandw.js} +2 -2
  94. package/dist/{onboard-wqywz5py.js → onboard-sfmpe4np.js} +2 -2
  95. package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-jmpst4q4.js} +4 -4
  96. package/dist/{otel-impl-vk32s884.js → otel-impl-v6cc0zve.js} +1 -1
  97. package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-fk8fkcf6.js} +33 -6
  98. package/dist/{pricing-refresh-t27anv4z.js → pricing-refresh-fsk1e1zc.js} +3 -3
  99. package/dist/{rbac-roles-452665y0.js → rbac-roles-prmkdnkw.js} +2 -2
  100. package/dist/{rbac-roles-bmcgsxjn.js → rbac-roles-zxmxqa39.js} +1 -1
  101. package/dist/{render-v2-0ttsk2j4.js → render-v2-pjwxvnkw.js} +3 -3
  102. package/dist/{resume-a26kvr41.js → resume-frdtgfa0.js} +9 -5
  103. package/dist/{session-access-dgh9nkwk.js → session-access-fpez4y5q.js} +4 -4
  104. package/dist/{setup-9vc9qahm.js → setup-mq6m3m8q.js} +1 -1
  105. package/dist/{work-objects-vn8nkxwn.js → work-objects-kfahhjsq.js} +20 -20
  106. package/dist/{worker-1x1h8vnm.js → worker-v5e5rxj3.js} +35 -35
  107. package/openapi.json +3078 -2424
  108. package/package.json +7 -7
  109. package/plugin/commands/create-pr.md +2 -2
  110. package/plugin/pi-skills/create-pr/SKILL.md +2 -2
  111. package/src/be/approval-config-question.ts +389 -0
  112. package/src/be/automation-preflight.ts +37 -3
  113. package/src/be/db/approvals.ts +48 -0
  114. package/src/be/db/session-tree.ts +73 -15
  115. package/src/be/db/skill-search.ts +87 -0
  116. package/src/be/db/tasks/read.ts +29 -2
  117. package/src/be/db/tasks/write.ts +15 -5
  118. package/src/be/db/workflow-idempotency.ts +183 -0
  119. package/src/be/db/workflow-runs.ts +366 -8
  120. package/src/be/db.ts +92 -22
  121. package/src/be/migrations/239_skills_fts.sql +40 -0
  122. package/src/be/migrations/240_workflow_run_parent_step_attempt.sql +37 -0
  123. package/src/be/migrations/241_skills_meta_fts.sql +39 -0
  124. package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
  125. package/src/be/sealed-json.ts +1 -0
  126. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
  127. package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
  128. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  129. package/src/be/sql-connections.ts +7 -1
  130. package/src/be/swarm-config-guard.ts +25 -0
  131. package/src/be/users.ts +9 -0
  132. package/src/commands/controller.ts +33 -0
  133. package/src/commands/runner.ts +6 -2
  134. package/src/extensions/apply-task-create.ts +57 -11
  135. package/src/extensions/catalog.generated.json +4 -4
  136. package/src/extensions/contract-types.generated.ts +1 -1
  137. package/src/extensions/contract.ts +25 -0
  138. package/src/extensions/dispatcher.ts +4 -0
  139. package/src/extensions/post-bridge.ts +8 -0
  140. package/src/hooks/context-bound-note.ts +196 -0
  141. package/src/hooks/hook.ts +12 -0
  142. package/src/http/approval-requests.ts +209 -38
  143. package/src/http/config.ts +70 -53
  144. package/src/http/idempotency-key.ts +117 -0
  145. package/src/http/sessions.ts +27 -2
  146. package/src/http/workflows.ts +129 -24
  147. package/src/integrations/chat-sdk/inbound.ts +5 -4
  148. package/src/prompts/context-bound-note.ts +48 -0
  149. package/src/prompts/session-templates.ts +25 -1
  150. package/src/providers/amp-adapter.ts +30 -8
  151. package/src/providers/claude-adapter.ts +29 -4
  152. package/src/providers/claude-managed-adapter.ts +26 -0
  153. package/src/providers/claude-sdk-session.ts +10 -0
  154. package/src/providers/claude-session-events.ts +13 -0
  155. package/src/providers/codex-adapter.ts +12 -0
  156. package/src/providers/dsh-adapter.ts +1 -1
  157. package/src/providers/opencode-adapter.ts +18 -0
  158. package/src/providers/pi-mono-adapter.ts +35 -0
  159. package/src/runtime-providers/docker.ts +6 -2
  160. package/src/scripts-runtime/types/swarm-extension.d.ts +35 -0
  161. package/src/tests/amp-adapter.test.ts +97 -0
  162. package/src/tests/approval-config-question.test.ts +702 -0
  163. package/src/tests/approval-requests.test.ts +10 -7
  164. package/src/tests/approval-sweeps.test.ts +12 -6
  165. package/src/tests/base-prompt.test.ts +1 -1
  166. package/src/tests/chat-sdk-inbound.test.ts +20 -0
  167. package/src/tests/claude-hook-output.test.ts +79 -1
  168. package/src/tests/claude-managed-adapter.test.ts +275 -0
  169. package/src/tests/codex-adapter.test.ts +163 -0
  170. package/src/tests/context-bound-note.test.ts +269 -0
  171. package/src/tests/dsh-adapter.test.ts +1 -1
  172. package/src/tests/entrypoint-gitlab-credential-helper.test.ts +273 -0
  173. package/src/tests/extensions-example-comms-adherence-gate.test.ts +321 -12
  174. package/src/tests/extensions-pre-task.test.ts +273 -0
  175. package/src/tests/fetch-resolved-env.test.ts +17 -0
  176. package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
  177. package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
  178. package/src/tests/fixtures/fake-amp.ts +22 -8
  179. package/src/tests/fixtures/held-template-render.ts +42 -0
  180. package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
  181. package/src/tests/list-endpoint-slimming.test.ts +33 -0
  182. package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
  183. package/src/tests/openapi-route-headers.test.ts +2 -0
  184. package/src/tests/opencode-adapter.test.ts +134 -0
  185. package/src/tests/pi-mono-adapter.test.ts +105 -0
  186. package/src/tests/prompt-template-session.test.ts +4 -3
  187. package/src/tests/runner-push-delivery.test.ts +25 -0
  188. package/src/tests/runtime-controller-liveness.test.ts +133 -0
  189. package/src/tests/runtime-providers-docker.test.ts +2 -0
  190. package/src/tests/script-connections.test.ts +9 -0
  191. package/src/tests/script-run-config-scope.test.ts +91 -3
  192. package/src/tests/session-logs.test.ts +70 -0
  193. package/src/tests/sessions-tree.test.ts +205 -1
  194. package/src/tests/skill-search-long-content.test.ts +193 -0
  195. package/src/tests/skill-search.test.ts +173 -22
  196. package/src/tests/workflow-cascade-cancel.test.ts +1169 -0
  197. package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
  198. package/src/tests/workflow-idempotency.test.ts +915 -0
  199. package/src/tests/workflow-sub-workflow.test.ts +538 -1
  200. package/src/tools/request-human-input.ts +27 -5
  201. package/src/tools/workflows/cancel-workflow-run.ts +1 -1
  202. package/src/types.ts +7 -0
  203. package/src/utils/context-window.ts +8 -6
  204. package/src/workflows/approval-cancel.ts +15 -13
  205. package/src/workflows/approval-notifications.ts +15 -4
  206. package/src/workflows/checkpoint.ts +72 -35
  207. package/src/workflows/engine.ts +262 -93
  208. package/src/workflows/executors/base.ts +6 -0
  209. package/src/workflows/executors/foreach.ts +2 -2
  210. package/src/workflows/executors/human-in-the-loop.ts +25 -1
  211. package/src/workflows/executors/sub-workflow.ts +45 -16
  212. package/src/workflows/foreach-driver.ts +10 -3
  213. package/src/workflows/foreach-join.ts +29 -6
  214. package/src/workflows/recovery.ts +68 -1
  215. package/src/workflows/resume.ts +228 -77
  216. package/src/workflows/retry-poller.ts +74 -7
  217. package/src/workflows/task-step-routing.ts +29 -7
  218. package/templates/ai-toolbox.manifest.json +8 -8
  219. package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
  220. package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
  221. package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
  222. package/templates/extensions/comms-adherence-gate/README.md +38 -13
  223. package/templates/extensions/comms-adherence-gate/hooks.ts +272 -69
  224. package/templates/extensions/comms-adherence-gate/manifest.json +2 -2
  225. package/templates/skills/agent-browser/SKILL.md +4 -4
  226. package/templates/skills/agent-browser/content.md +4 -4
  227. package/templates/skills/code-quality/config.json +1 -1
  228. package/templates/skills/code-quality/content.md +2 -2
  229. package/templates/skills/delegate-work/content.md +29 -25
  230. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
  231. package/templates/skills/qa/content.md +2 -2
  232. package/templates/skills/researching/content.md +2 -0
  233. package/templates/skills/turso-interaction/content.md +1 -1
  234. package/templates/skills/work-on-task/SKILL.md +1 -1
  235. package/templates/skills/work-on-task/content.md +1 -1
  236. package/templates/skills/workflow-iterate/content.md +8 -1
@@ -1,6 +1,7 @@
1
1
  import { notifyAutomationPreflightFailure } from "../automation-preflight-alert";
2
2
  import {
3
3
  getAutomationSetupStates,
4
+ isFirstWorkflowPreflightFailureToday,
4
5
  preflightAutomation,
5
6
  recordWorkflowPreflightFailure,
6
7
  renderAutomationTokens,
@@ -20,10 +21,20 @@ import {
20
21
  updateWorkflowRun,
21
22
  updateWorkflowRunStep,
22
23
  } from "../be/db";
24
+ import {
25
+ getChildWorkflowRunId,
26
+ getRunLineageBlock,
27
+ isParentStepAttemptCurrent,
28
+ } from "../be/db/workflow-runs";
23
29
  import { WorkflowReplayStateError } from "../be/workflow-replay";
24
30
  import { telemetry } from "../telemetry";
25
31
  import type { Workflow, WorkflowDefinition, WorkflowNode, WorkflowRunStep } from "../types";
26
- import { checkpointStep, checkpointStepFailure, checkpointStepWaiting } from "./checkpoint";
32
+ import {
33
+ checkpointStep,
34
+ checkpointStepFailure,
35
+ checkpointStepWaiting,
36
+ type StepFence,
37
+ } from "./checkpoint";
27
38
  import { loadCompletedStepRouting } from "./completed-step-routing";
28
39
  import { FOREACH_ITEMS_WAIT_FOR } from "./constants";
29
40
  import { shouldSkipCooldown } from "./cooldown";
@@ -58,6 +69,15 @@ export interface WorkflowExecutionOptions {
58
69
  triggerType?: "schedule" | "manual" | "event" | "api";
59
70
  /** Set when a `sub-workflow` step starts this run as its child. */
60
71
  parentStepId?: string;
72
+ /** The parent step's `retryCount`; each attempt gets its own child run. */
73
+ parentStepAttempt?: number;
74
+ /**
75
+ * Runs inside the transaction that commits a top-level run (or reuses the
76
+ * day's setup-failure run), after the run row is written. A throw rolls the
77
+ * run back and nothing else happens: no walk, no alert, no telemetry. The
78
+ * HTTP trigger records its `Idempotency-Key` here.
79
+ */
80
+ accept?: (run: { runId: string; skipped: boolean }) => Promise<void>;
61
81
  }
62
82
 
63
83
  /**
@@ -87,11 +107,9 @@ async function resolveRenderedWorkflowInputs(
87
107
  /**
88
108
  * Start executing a workflow from scratch.
89
109
  *
90
- * 1. Check cooldown
91
- * 2. Create workflow run
92
- * 3. Resolve inputs
93
- * 4. Find entry nodes
94
- * 5. Walk the graph
110
+ * 1. Check cooldown, readiness and inputs
111
+ * 2. Create the workflow run with its initial state (one transaction)
112
+ * 3. Walk the graph from the entry nodes
95
113
  */
96
114
  export async function startWorkflowExecution(
97
115
  workflow: Workflow,
@@ -103,75 +121,142 @@ export async function startWorkflowExecution(
103
121
  workflowPreflightInput(workflow),
104
122
  await getAutomationSetupStates(),
105
123
  );
106
- if (preflight.state === "needs_setup") {
107
- const { runId, recorded } = await recordWorkflowPreflightFailure({
108
- workflowId: workflow.id,
109
- triggerType: options.triggerType ?? "manual",
110
- triggerData,
111
- failureReason: preflight.failureReason!,
112
- createdBy: options.requestedByUserId,
124
+ if (preflight.state === "needs_setup" && !options.parentStepId) {
125
+ const { runId, recorded } = await getDbClient().transaction(async () => {
126
+ const failure = await recordWorkflowPreflightFailure({
127
+ workflowId: workflow.id,
128
+ triggerType: options.triggerType ?? "manual",
129
+ triggerData,
130
+ failureReason: preflight.failureReason!,
131
+ createdBy: options.requestedByUserId,
132
+ });
133
+ await options.accept?.({ runId: failure.runId, skipped: false });
134
+ return failure;
113
135
  });
114
136
  if (recorded) await notifyAutomationPreflightFailure(preflight);
115
137
  return runId;
116
138
  }
117
139
 
118
- // Templates can consume install params outside the graph definition (most
119
- // importantly workflow.input). Render the complete runtime snapshot once;
120
- // exact-token values retain their JSON type, including COMPETITORS arrays.
121
- workflow = renderAutomationTokens(workflow, workflow.params ?? {});
140
+ // Settle everything the run starts from before its row exists, so the row
141
+ // commits together with its initial context or terminal status. A crash
142
+ // right after the commit leaves a run that recovery can walk, never a
143
+ // `running` row with an empty context.
144
+ let initial: InitialRunState;
145
+ if (preflight.state === "needs_setup") {
146
+ // A child keeps its parent link and attempt even when it cannot start:
147
+ // the day-deduped top-level failure run is shared and unlinked, so a
148
+ // retry or recovery of the step would never find it.
149
+ initial = {
150
+ status: "failed",
151
+ error: preflight.failureReason!,
152
+ finishedAt: new Date().toISOString(),
153
+ preflight: true,
154
+ };
155
+ } else {
156
+ // Templates can consume install params outside the graph definition (most
157
+ // importantly workflow.input). Render the complete runtime snapshot once;
158
+ // exact-token values retain their JSON type, including COMPETITORS arrays.
159
+ workflow = renderAutomationTokens(workflow, workflow.params ?? {});
160
+
161
+ // Validate trigger data against triggerSchema (before any DB writes)
162
+ if (workflow.triggerSchema) {
163
+ const validationErrors = validateJsonSchema(workflow.triggerSchema, triggerData);
164
+ if (validationErrors.length > 0) {
165
+ throw new TriggerSchemaError(validationErrors);
166
+ }
167
+ }
122
168
 
123
- // Validate trigger data against triggerSchema (before any DB writes)
124
- if (workflow.triggerSchema) {
125
- const validationErrors = validateJsonSchema(workflow.triggerSchema, triggerData);
126
- if (validationErrors.length > 0) {
127
- throw new TriggerSchemaError(validationErrors);
169
+ if (workflow.cooldown && (await shouldSkipCooldown(workflow.id, workflow.cooldown))) {
170
+ initial = { status: "skipped", error: "cooldown", finishedAt: new Date().toISOString() };
171
+ } else {
172
+ initial = await buildInitialRunState(workflow, triggerData, registry, options);
128
173
  }
129
174
  }
130
175
 
131
- // Cooldown check
132
- if (workflow.cooldown && (await shouldSkipCooldown(workflow.id, workflow.cooldown))) {
133
- const runId = crypto.randomUUID();
134
- await createWorkflowRun({
135
- id: runId,
176
+ // Held from before the commit until the walk holds the run itself, so a
177
+ // recovery sweep never walks the new run alongside it.
178
+ const newRunId = crypto.randomUUID();
179
+ const release = holdWorkflowRun(newRunId);
180
+ try {
181
+ const { runId, created, preflightAlert } = await createRunWithInitialState(
182
+ workflow,
183
+ triggerData,
184
+ options,
185
+ initial,
186
+ newRunId,
187
+ );
188
+ if (!created) return runId;
189
+ if (initial.status === "failed" && initial.preflight) {
190
+ if (preflightAlert) await notifyAutomationPreflightFailure(preflight);
191
+ return runId;
192
+ }
193
+ if (initial.status === "skipped") return runId;
194
+ telemetry.workflow("started", {
136
195
  workflowId: workflow.id,
196
+ nodeCount: workflow.definition.nodes.length,
137
197
  triggerType: options.triggerType ?? "manual",
138
- triggerData,
139
- createdBy: options.requestedByUserId,
140
- parentStepId: options.parentStepId,
141
- });
142
- await updateWorkflowRun(runId, {
143
- status: "skipped",
144
- error: "cooldown",
145
- finishedAt: new Date().toISOString(),
146
198
  });
199
+ if (initial.status) return runId;
200
+
201
+ const walk = walkGraph(
202
+ workflow.definition,
203
+ runId,
204
+ initial.context,
205
+ findEntryNodes(workflow.definition),
206
+ registry,
207
+ workflow.id,
208
+ getSecretInputKeys(workflow.input),
209
+ options,
210
+ );
211
+ // A child run walks on its own, outside the parent executor's timeout. Its
212
+ // terminal status wakes the parent step (`resumeFromChildRun`).
213
+ if (options.parentStepId) {
214
+ walk.catch((err) => console.error(`[workflows] Child workflow run ${runId} failed:`, err));
215
+ return runId;
216
+ }
217
+ await walk;
147
218
  return runId;
219
+ } finally {
220
+ release();
148
221
  }
222
+ }
149
223
 
150
- const runId = crypto.randomUUID();
151
- await createWorkflowRun({
152
- id: runId,
153
- workflowId: workflow.id,
154
- triggerType: options.triggerType ?? "manual",
155
- triggerData,
156
- createdBy: options.requestedByUserId,
157
- parentStepId: options.parentStepId,
158
- });
159
- telemetry.workflow("started", {
160
- workflowId: workflow.id,
161
- nodeCount: workflow.definition.nodes.length,
162
- triggerType: options.triggerType ?? "manual",
163
- });
224
+ /**
225
+ * Thrown when a `sub-workflow` step asks for a child run while the step's
226
+ * attempt no longer executes (cancelled, re-attempted or already parked).
227
+ */
228
+ export class ParentStepNotCurrentError extends Error {
229
+ constructor(parentStepId: string, attempt: number) {
230
+ super(
231
+ `Parent step ${parentStepId} (attempt ${attempt}) is no longer running; no child run started`,
232
+ );
233
+ this.name = "ParentStepNotCurrentError";
234
+ }
235
+ }
236
+
237
+ /**
238
+ * A new run's first committed state: a terminal status, or the context it
239
+ * walks from. `preflight` marks a setup failure, which alerts once a day.
240
+ */
241
+ type InitialRunState =
242
+ | { status: "skipped" | "failed"; error: string; finishedAt: string; preflight?: true }
243
+ | { status?: undefined; context: Record<string, unknown> };
164
244
 
245
+ async function buildInitialRunState(
246
+ workflow: Workflow,
247
+ triggerData: unknown,
248
+ registry: ExecutorRegistry,
249
+ options: WorkflowExecutionOptions,
250
+ ): Promise<InitialRunState> {
165
251
  // An executor that cannot run (for example a system-one-decision node with no API key) fails
166
252
  // the run here, before any node has side effects, instead of partway through.
167
253
  const notReady = await findWorkflowReadinessProblems(workflow.definition, registry);
168
254
  if (notReady.length > 0) {
169
- await updateWorkflowRun(runId, {
255
+ return {
170
256
  status: "failed",
171
257
  error: formatReadinessRunError(notReady),
172
258
  finishedAt: new Date().toISOString(),
173
- });
174
- return runId;
259
+ };
175
260
  }
176
261
 
177
262
  // Resolve inputs and merge into initial context
@@ -190,35 +275,64 @@ export async function startWorkflowExecution(
190
275
  const resolved = await resolveRenderedWorkflowInputs(workflow.input);
191
276
  Object.assign(ctx, { input: resolved });
192
277
  } catch (err) {
193
- await updateWorkflowRun(runId, {
278
+ return {
194
279
  status: "failed",
195
280
  error: `Input resolution failed: ${err}`,
196
281
  finishedAt: new Date().toISOString(),
197
- });
198
- return runId;
282
+ };
199
283
  }
200
284
  }
285
+ return { context: ctx };
286
+ }
201
287
 
202
- const entryNodes = findEntryNodes(workflow.definition);
203
- const secretKeys = getSecretInputKeys(workflow.input);
204
- const walk = walkGraph(
205
- workflow.definition,
206
- runId,
207
- ctx,
208
- entryNodes,
209
- registry,
210
- workflow.id,
211
- secretKeys,
212
- options,
213
- );
214
- // A child run walks on its own, outside the parent executor's timeout. Its
215
- // terminal status wakes the parent step (`resumeFromChildRun`).
216
- if (options.parentStepId) {
217
- walk.catch((err) => console.error(`[workflows] Child workflow run ${runId} failed:`, err));
218
- return runId;
219
- }
220
- await walk;
221
- return runId;
288
+ /**
289
+ * Insert the run with its initial state in one transaction. For a child run
290
+ * the same transaction checks that the parent step's attempt is still
291
+ * executing and returns the child that attempt already started instead of a
292
+ * second one; the unique (parentStepId, parentStepAttempt) index backs this
293
+ * across processes. A setup failure claims the day's alert in the same
294
+ * transaction, so concurrent failures alert exactly once. `options.accept`
295
+ * runs last in it.
296
+ */
297
+ async function createRunWithInitialState(
298
+ workflow: Workflow,
299
+ triggerData: unknown,
300
+ options: WorkflowExecutionOptions,
301
+ initial: InitialRunState,
302
+ runId: string,
303
+ ): Promise<{ runId: string; created: boolean; preflightAlert?: boolean }> {
304
+ return getDbClient().transaction(async () => {
305
+ const { parentStepId } = options;
306
+ const attempt = options.parentStepAttempt ?? 0;
307
+ if (parentStepId) {
308
+ if (!(await isParentStepAttemptCurrent(parentStepId, attempt))) {
309
+ throw new ParentStepNotCurrentError(parentStepId, attempt);
310
+ }
311
+ const existingId = await getChildWorkflowRunId(parentStepId, attempt);
312
+ if (existingId) return { runId: existingId, created: false };
313
+ }
314
+ await createWorkflowRun({
315
+ id: runId,
316
+ workflowId: workflow.id,
317
+ triggerType: options.triggerType ?? "manual",
318
+ triggerData,
319
+ createdBy: options.requestedByUserId,
320
+ parentStepId,
321
+ parentStepAttempt: parentStepId ? attempt : undefined,
322
+ });
323
+ await updateWorkflowRun(
324
+ runId,
325
+ initial.status
326
+ ? { status: initial.status, error: initial.error, finishedAt: initial.finishedAt }
327
+ : { context: initial.context },
328
+ );
329
+ const preflightAlert =
330
+ initial.status === "failed" && initial.preflight
331
+ ? await isFirstWorkflowPreflightFailureToday(workflow.id, runId)
332
+ : undefined;
333
+ await options.accept?.({ runId, skipped: initial.status === "skipped" });
334
+ return { runId, created: true, preflightAlert };
335
+ });
222
336
  }
223
337
 
224
338
  // ─── Graph Walker ──────────────────────────────────────────
@@ -270,6 +384,11 @@ export function isWorkflowRunActive(runId: string): boolean {
270
384
  return activeWalks.has(runId);
271
385
  }
272
386
 
387
+ /** True while an executeStep call in this process owns the step row. */
388
+ export function isStepExecuting(stepId: string): boolean {
389
+ return executingSteps.has(stepId);
390
+ }
391
+
273
392
  /**
274
393
  * Mark the run as owned by this process until the returned release runs.
275
394
  * Recovery skips owned runs, so an executor outside walkGraph (the retry
@@ -504,6 +623,8 @@ async function walkGraphOwned(
504
623
  await getDbClient().transaction(async () => {
505
624
  const run = await getWorkflowRun(runId);
506
625
  if (!run || run.status !== "running") return;
626
+ // Inside a cancelled tree the run's outcome is the cancel, not its steps.
627
+ if (await getRunLineageBlock(runId)) return;
507
628
 
508
629
  const finalSteps = await getWorkflowRunStepsByRunId(runId);
509
630
  // Another walker is still executing a branch; it finalizes the run.
@@ -691,6 +812,9 @@ async function executeStep(
691
812
  if (!run || (run.status !== "running" && run.status !== "waiting")) {
692
813
  return { halted: true as const };
693
814
  }
815
+ // A live run under a cancelled ancestor (or with a broken parent link)
816
+ // starts no step: the cancel covers its whole tree.
817
+ if (await getRunLineageBlock(runId)) return { halted: true as const };
694
818
 
695
819
  // Another walker already started or finished this node for the same
696
820
  // predecessor completions (two branch completions both walking the join,
@@ -705,6 +829,20 @@ async function executeStep(
705
829
  if (current && (current.status !== "running" || executingSteps.has(current.id))) {
706
830
  return { current };
707
831
  }
832
+ // An orphaned `sub-workflow` row is this same invocation: it covers the
833
+ // current predecessor completions, so it is not a new loop iteration.
834
+ // Re-enter it under its step id, key and retry count, so the executor
835
+ // reconnects to the child it already started instead of a second one.
836
+ if (current && node.type === "sub-workflow") {
837
+ claimedStepId = current.id;
838
+ executingSteps.add(current.id);
839
+ return {
840
+ existingStep: current,
841
+ stepId: current.id,
842
+ fence: { retryCount: current.retryCount },
843
+ deduped: false,
844
+ };
845
+ }
708
846
  }
709
847
 
710
848
  // Count existing steps for this node to determine the current iteration.
@@ -717,7 +855,12 @@ async function executeStep(
717
855
  existingStep &&
718
856
  (existingStep.status === "completed" || existingStep.status === "waiting")
719
857
  ) {
720
- return { existingStep, stepId: existingStep.id, deduped: true };
858
+ return {
859
+ existingStep,
860
+ stepId: existingStep.id,
861
+ fence: { retryCount: existingStep.retryCount },
862
+ deduped: true,
863
+ };
721
864
  }
722
865
  // For "pending" or "failed" steps, fall through to re-execute
723
866
 
@@ -748,7 +891,8 @@ async function executeStep(
748
891
  // this `running` row without also seeing it owned.
749
892
  claimedStepId = stepId;
750
893
  executingSteps.add(stepId);
751
- return { existingStep, stepId, deduped: false };
894
+ const fence = { retryCount: reusableForeachStep?.retryCount ?? 0 };
895
+ return { existingStep, stepId, fence, deduped: false };
752
896
  };
753
897
  const dedup = await getDbClient()
754
898
  .transaction(claimStep)
@@ -787,6 +931,7 @@ async function executeStep(
787
931
  registry,
788
932
  dedup.existingStep,
789
933
  dedup.stepId,
934
+ dedup.fence,
790
935
  workflowId,
791
936
  options,
792
937
  );
@@ -800,7 +945,11 @@ async function executeStep(
800
945
  }
801
946
  }
802
947
 
803
- /** Steps 3-10 of executeStep, for a step row this call inserted and owns. */
948
+ /**
949
+ * Steps 3-10 of executeStep, for a step row this call claimed. Every result
950
+ * write is fenced on the claim: once a cancel, a newer attempt or another
951
+ * settle moved the row, this execution writes nothing and routes nothing.
952
+ */
804
953
  async function runClaimedStep(
805
954
  def: WorkflowDefinition,
806
955
  runId: string,
@@ -809,9 +958,11 @@ async function runClaimedStep(
809
958
  registry: ExecutorRegistry,
810
959
  existingStep: WorkflowRunStep | null | undefined,
811
960
  stepId: string,
961
+ fence: StepFence,
812
962
  workflowId?: string,
813
963
  options: WorkflowExecutionOptions = {},
814
964
  ): Promise<StepResult> {
965
+ const stale: StepResult = { outcome: "failed", successors: [] };
815
966
  // 3. Get executor
816
967
  const executor = registry.get(node.type);
817
968
 
@@ -820,7 +971,9 @@ async function runClaimedStep(
820
971
  // requests and multiply paid attempts. Fail before any request is built.
821
972
  const systemOneRetryProblems = systemOneRetryViolations(node);
822
973
  if (systemOneRetryProblems.length > 0) {
823
- await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0);
974
+ await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0, undefined, {
975
+ fence,
976
+ });
824
977
  return { outcome: "failed", successors: [] };
825
978
  }
826
979
 
@@ -835,7 +988,10 @@ async function runClaimedStep(
835
988
  );
836
989
  if (inputErrors.length > 0) {
837
990
  const errorMsg = `Input schema validation failed: ${inputErrors.join("; ")}`;
838
- await checkpointStepFailure(runId, stepId, errorMsg, 0);
991
+ const { stale: lost } = await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
992
+ fence,
993
+ });
994
+ if (lost) return stale;
839
995
  throw new Error(errorMsg);
840
996
  }
841
997
  }
@@ -852,7 +1008,7 @@ async function runClaimedStep(
852
1008
 
853
1009
  if (scriptBodyUnresolved && scriptBodyUnresolved.length > 0) {
854
1010
  const errorMsg = scriptBodyInterpolationError(node.id, scriptBodyUnresolved);
855
- await checkpointStepFailure(runId, stepId, errorMsg, 0);
1011
+ await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, { fence });
856
1012
  return { outcome: "failed", successors: [] };
857
1013
  }
858
1014
 
@@ -863,6 +1019,8 @@ async function runClaimedStep(
863
1019
  stepId,
864
1020
  systemOneUnresolvedError(node.id, strictUnresolved, node.type),
865
1021
  0,
1022
+ undefined,
1023
+ { fence },
866
1024
  );
867
1025
  return { outcome: "failed", successors: [] };
868
1026
  }
@@ -905,14 +1063,16 @@ async function runClaimedStep(
905
1063
  // Apply retry policy if configured
906
1064
  const retryPolicy = node.retry || executor.retryPolicy;
907
1065
  const currentRetryCount = existingStep?.retryCount || 0;
908
- const { shouldRetry } = await checkpointStepFailure(
1066
+ const { shouldRetry, stale: lost } = await checkpointStepFailure(
909
1067
  runId,
910
1068
  stepId,
911
1069
  errorMsg,
912
1070
  currentRetryCount,
913
1071
  retryPolicy,
1072
+ { fence },
914
1073
  );
915
1074
 
1075
+ if (lost) return stale;
916
1076
  if (!shouldRetry) {
917
1077
  throw err; // Will be caught by walkGraph
918
1078
  }
@@ -922,20 +1082,21 @@ async function runClaimedStep(
922
1082
 
923
1083
  // 6. Handle result
924
1084
  if (result.status === "failed") {
925
- const retryPolicy = node.retry || executor.retryPolicy;
1085
+ // A failure the executor marks non-retryable skips the policy here as on
1086
+ // every other path that settles the same outcome.
1087
+ const retryPolicy = result.retryable === false ? undefined : node.retry || executor.retryPolicy;
926
1088
  const currentRetryCount = existingStep?.retryCount || 0;
927
- const { shouldRetry } = await checkpointStepFailure(
1089
+ // Output is persisted for observability even on failure (e.g. script
1090
+ // nodes keep {exitCode, stdout, stderr}), in the same fenced write.
1091
+ const { shouldRetry, stale: lost } = await checkpointStepFailure(
928
1092
  runId,
929
1093
  stepId,
930
1094
  result.error || "Executor returned failed status",
931
1095
  currentRetryCount,
932
1096
  retryPolicy,
1097
+ { fence, output: result.output },
933
1098
  );
934
-
935
- // Persist output for observability even on failure (e.g. script nodes keep {exitCode, stdout, stderr})
936
- if (result.output !== undefined) {
937
- await updateWorkflowRunStep(stepId, { output: result.output });
938
- }
1099
+ if (lost) return stale;
939
1100
 
940
1101
  if (!shouldRetry) {
941
1102
  throw new Error(result.error || "Step execution failed");
@@ -976,7 +1137,10 @@ async function runClaimedStep(
976
1137
  );
977
1138
  if (outputErrors.length > 0) {
978
1139
  const errorMsg = `Output schema validation failed: ${outputErrors.join("; ")}`;
979
- await checkpointStepFailure(runId, stepId, errorMsg, 0);
1140
+ const { stale: lost } = await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
1141
+ fence,
1142
+ });
1143
+ if (lost) return stale;
980
1144
  throw new Error(errorMsg);
981
1145
  }
982
1146
  }
@@ -988,7 +1152,10 @@ async function runClaimedStep(
988
1152
 
989
1153
  if (validationResult.outcome === "halt") {
990
1154
  const errorMsg = "Validation failed (mustPass)";
991
- await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, { markRunFailed: false });
1155
+ await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
1156
+ markRunFailed: false,
1157
+ fence,
1158
+ });
992
1159
  return { outcome: "failed", successors: [] };
993
1160
  }
994
1161
 
@@ -1001,13 +1168,15 @@ async function runClaimedStep(
1001
1168
  }
1002
1169
  const retryPolicy = node.validation.retry || node.retry;
1003
1170
  const currentRetryCount = existingStep?.retryCount || 0;
1004
- await checkpointStepFailure(
1171
+ const { stale: lost } = await checkpointStepFailure(
1005
1172
  runId,
1006
1173
  stepId,
1007
1174
  "Validation failed, retrying",
1008
1175
  currentRetryCount,
1009
1176
  retryPolicy,
1177
+ { fence },
1010
1178
  );
1179
+ if (lost) return stale;
1011
1180
  return { outcome: "completed", successors: [] }; // Retry handled by poller
1012
1181
  }
1013
1182
  }
@@ -1020,7 +1189,7 @@ async function runClaimedStep(
1020
1189
  result.nextPort = resolveValidationPort(node.next, result.nextPort, validationResult?.passed);
1021
1190
 
1022
1191
  // 9. Checkpoint success
1023
- await checkpointStep(runId, stepId, node.id, result, ctx);
1192
+ if (!(await checkpointStep(runId, stepId, node.id, result, ctx, { fence }))) return stale;
1024
1193
 
1025
1194
  // 10. Determine successors based on nextPort
1026
1195
  // If executor returned a specific port, use it. Otherwise, get all successors
@@ -51,6 +51,12 @@ export interface ExecutorResult<TOutput = unknown> {
51
51
  output?: TOutput;
52
52
  nextPort?: string;
53
53
  error?: string;
54
+ /**
55
+ * False on a failure no retry can change (a child run that was cancelled,
56
+ * for example). The engine then skips the node's retry policy. Unset means
57
+ * retryable.
58
+ */
59
+ retryable?: boolean;
54
60
  }
55
61
 
56
62
  export interface AsyncExecutorResult<TOutput = unknown> extends ExecutorResult<TOutput> {
@@ -6,7 +6,7 @@ import { getSuccessors } from "../definition";
6
6
  import { foreachBodyTypeError } from "../foreach-body";
7
7
  import {
8
8
  buildForeachAggregate,
9
- FOREACH_TERMINAL_STEP_STATUSES,
9
+ isForeachChildSettled,
10
10
  parseSyntheticNodeId,
11
11
  } from "../foreach-join";
12
12
  import { getMaxWorkflowStepsPerRun } from "../limits";
@@ -144,7 +144,7 @@ export class ForeachExecutor extends BaseExecutor<
144
144
  const childSteps = (await this.deps.db.getWorkflowRunStepsByRunId(meta.runId)).filter(
145
145
  (step) => parseSyntheticNodeId(step.nodeId)?.parentNodeId === meta.nodeId,
146
146
  );
147
- if (childSteps.every((step) => FOREACH_TERMINAL_STEP_STATUSES.has(step.status))) {
147
+ if (childSteps.every(isForeachChildSettled)) {
148
148
  return { status: "success", output: buildForeachAggregate(childSteps) };
149
149
  }
150
150
 
@@ -1,4 +1,8 @@
1
1
  import { z } from "zod";
2
+ import {
3
+ ConfigQuestionSettingsSchema,
4
+ normalizeApprovalQuestions,
5
+ } from "../../be/approval-config-question";
2
6
  import type { ExecutorMeta } from "../../types";
3
7
  import { scrubSecrets } from "../../utils/secret-scrubber";
4
8
  import { dispatchApprovalNotifications } from "../approval-notifications";
@@ -56,6 +60,15 @@ const QuestionSchema = z.discriminatedUnion("type", [
56
60
  description: z.string().optional(),
57
61
  defaultValue: z.boolean().optional(),
58
62
  }),
63
+ z.object({
64
+ id: z.string(),
65
+ type: z.literal("config"),
66
+ // Defaults to the config key during normalization in `run`.
67
+ label: z.string().optional(),
68
+ required: z.boolean().default(true),
69
+ description: z.string().optional(),
70
+ config: ConfigQuestionSettingsSchema,
71
+ }),
59
72
  ]);
60
73
 
61
74
  export const ApproverConfigSchema = z.object({
@@ -207,7 +220,18 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
207
220
  override async run(input: ExecutorInput): Promise<ExecutorResult<HITLOutput>> {
208
221
  const resolved = resolveHitlQuestions(input.config?.questions);
209
222
  if (!resolved.ok) return { status: "failed", error: resolved.error };
210
- return super.run({ ...input, config: { ...input.config, questions: resolved.questions } });
223
+ // Config questions: the same create-time rules as the tool and REST paths.
224
+ const approvers = input.config?.approvers as { policy?: unknown } | undefined;
225
+ const normalized = await normalizeApprovalQuestions(resolved.questions, {
226
+ approvers: { policy: approvers?.policy },
227
+ });
228
+ if (!normalized.ok) {
229
+ return {
230
+ status: "failed",
231
+ error: `human-in-the-loop questions are invalid: ${normalized.error}`,
232
+ };
233
+ }
234
+ return super.run({ ...input, config: { ...input.config, questions: normalized.questions } });
211
235
  }
212
236
 
213
237
  protected async execute(