@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
@@ -4,9 +4,26 @@ import {
4
4
  scrubJsonValue,
5
5
  init_scrub_json,
6
6
  getDbClient2,
7
+ getAgentById2,
8
+ updateAgentStatus2,
9
+ updateAgentMaxTasks2,
10
+ getActiveTaskCount2,
11
+ approvalCancellationNotificationKey,
12
+ init_approvals,
7
13
  getTaskById2,
14
+ getRunLineageBlock,
15
+ getWorkflowRunStatus,
16
+ getChildWorkflowRunId,
17
+ isParentStepAttemptCurrent,
8
18
  createForeachChildSteps,
9
19
  init_workflow_runs,
20
+ isApiOnlyConfigKey,
21
+ isInternalConfigKey,
22
+ internalConfigKeyError,
23
+ isReservedConfigKey,
24
+ reservedKeyError,
25
+ validateConfigEntry,
26
+ init_swarm_config_guard,
10
27
  getOAuthApp,
11
28
  getOAuthTokens,
12
29
  init_oauth,
@@ -14,6 +31,7 @@ import {
14
31
  init_workflow_replay,
15
32
  createTaskExtended2,
16
33
  getSwarmConfigs2,
34
+ upsertSwarmConfig2,
17
35
  getWorkflow2,
18
36
  createWorkflowRun2,
19
37
  getWorkflowRun2,
@@ -37,23 +55,30 @@ import {
37
55
  releaseApprovalCancellationNotificationClaim2,
38
56
  updateApprovalRequestNotifications2,
39
57
  init_db3
40
- } from "./cli-csh8fnbg.js";
58
+ } from "./cli-jb30dzvk.js";
59
+ import {
60
+ isMultiRuntimeEnabled
61
+ } from "./cli-e8xyjvfc.js";
41
62
  import {
42
63
  getSlackApp2
43
- } from "./cli-15xd0j7w.js";
64
+ } from "./cli-abs8ndtr.js";
44
65
  import {
45
66
  externalChannelToolError2,
46
67
  isBlockExternalEnabled2,
47
68
  getChannelBlockReason2
48
- } from "./cli-b63d08rs.js";
69
+ } from "./cli-eftzyh9y.js";
49
70
  import {
50
71
  DEFAULT_MODEL,
51
72
  resolveCredential
52
73
  } from "./cli-qqzmjxkf.js";
74
+ import {
75
+ revokeRuntimeTokensForRuntime,
76
+ init_users
77
+ } from "./cli-6a1xg1p7.js";
53
78
  import {
54
79
  TERMINAL_TASK_STATUSES,
55
80
  init_types
56
- } from "./cli-mntxtkhn.js";
81
+ } from "./cli-mz9afxxf.js";
57
82
  import {
58
83
  getSlackConfiguration,
59
84
  isSlackConfigured,
@@ -63,18 +88,18 @@ import {
63
88
  selectCredential,
64
89
  init_credentials
65
90
  } from "./cli-zw88zjba.js";
66
- import {
67
- deepInterpolate,
68
- init_template,
69
- resolveTemplate,
70
- init_resolver
71
- } from "./cli-tx00c0h6.js";
72
91
  import {
73
92
  DEFAULT_OPENROUTER_BASE_URL,
74
93
  getOpenRouterBaseUrl,
75
94
  getOpenRouterAttributionHeaders,
76
95
  init_openrouter_base_url
77
96
  } from "./cli-dfyv0jdy.js";
97
+ import {
98
+ deepInterpolate,
99
+ init_template,
100
+ resolveTemplate,
101
+ init_resolver
102
+ } from "./cli-dqygeeh5.js";
78
103
  import {
79
104
  init_zod
80
105
  } from "./cli-4chz1hye.js";
@@ -124,7 +149,7 @@ async function notifyAutomationPreflightFailure(preflight) {
124
149
  return;
125
150
  }
126
151
  try {
127
- const { getSlackApp } = await import("./app-ft6r6v23.js");
152
+ const { getSlackApp } = await import("./app-ctn8gsgx.js");
128
153
  const app = getSlackApp();
129
154
  if (!app) {
130
155
  console.warn("[Automation Preflight] Slack not available, cannot send notification");
@@ -362,6 +387,15 @@ function utcDayBounds(now) {
362
387
  const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
363
388
  return { start: start.toISOString(), end: new Date(start.getTime() + 86400000).toISOString() };
364
389
  }
390
+ async function isFirstWorkflowPreflightFailureToday(workflowId, runId, now = new Date) {
391
+ const bounds = utcDayBounds(now);
392
+ const row = await getDbClient2().get(`SELECT 1 AS found FROM workflow_runs
393
+ WHERE workflowId = ? AND id != ? AND status = 'failed'
394
+ AND error LIKE 'needs_setup:%'
395
+ AND startedAt >= ? AND startedAt < ?
396
+ LIMIT 1`, [workflowId, runId, bounds.start, bounds.end]);
397
+ return row == null;
398
+ }
365
399
  async function recordSchedulePreflightFailure(scheduleId, failureReason, now = new Date) {
366
400
  const message = scrubSecrets(failureReason);
367
401
  return await getDbClient2().transaction(async (tx) => {
@@ -382,7 +416,7 @@ async function recordWorkflowPreflightFailure(input) {
382
416
  const bounds = utcDayBounds(now);
383
417
  return await getDbClient2().transaction(async (tx) => {
384
418
  const existing = await tx.get(`SELECT id FROM workflow_runs
385
- WHERE workflowId = ? AND status = 'failed'
419
+ WHERE workflowId = ? AND status = 'failed' AND parentStepId IS NULL
386
420
  AND error LIKE 'needs_setup:%'
387
421
  AND startedAt >= ? AND startedAt < ?
388
422
  ORDER BY startedAt ASC LIMIT 1`, [input.workflowId, bounds.start, bounds.end]);
@@ -404,7 +438,10 @@ async function recordWorkflowPreflightFailure(input) {
404
438
  timestamp,
405
439
  timestamp
406
440
  ]);
407
- return { runId, recorded: true };
441
+ return {
442
+ runId,
443
+ recorded: await isFirstWorkflowPreflightFailureToday(input.workflowId, runId, now)
444
+ };
408
445
  });
409
446
  }
410
447
 
@@ -457,10 +494,475 @@ class BaseExecutor {
457
494
 
458
495
  // src/workflows/executors/human-in-the-loop.ts
459
496
  init_zod();
497
+
498
+ // src/be/approval-config-question.ts
499
+ init_zod();
500
+ init_db3();
501
+
502
+ // src/be/multi-runtime.ts
503
+ init_secret_scrubber();
504
+ init_db3();
505
+ init_users();
506
+ var AGENT_MAX_TASKS_CONFIG_KEY = "AGENT_MAX_TASKS";
507
+ function parseAgentMaxTasksValue(raw) {
508
+ const str = raw.trim();
509
+ if (!/^\d+$/.test(str))
510
+ return null;
511
+ const value = Number(str);
512
+ return value >= 1 && value <= 100 ? value : null;
513
+ }
514
+ function defaultMaxTasksForAgent(agent) {
515
+ return agent.isLead ? 2 : 1;
516
+ }
517
+ async function getAgentMaxTasksConfig(agentId) {
518
+ return (await getSwarmConfigs2({ scope: "agent", scopeId: agentId, key: AGENT_MAX_TASKS_CONFIG_KEY }))[0] ?? null;
519
+ }
520
+ async function reconcileAgentMaxTasksPolicy(agentId) {
521
+ const agent = await getAgentById2(agentId);
522
+ if (!agent)
523
+ return;
524
+ const existing = await getAgentMaxTasksConfig(agentId);
525
+ if (existing) {
526
+ const authoritative = parseAgentMaxTasksValue(existing.value);
527
+ if (authoritative !== null && authoritative !== (agent.maxTasks ?? 1)) {
528
+ await updateAgentMaxTasks2(agentId, authoritative);
529
+ }
530
+ return;
531
+ }
532
+ await upsertSwarmConfig2({
533
+ scope: "agent",
534
+ scopeId: agentId,
535
+ key: AGENT_MAX_TASKS_CONFIG_KEY,
536
+ value: String(agent.maxTasks ?? defaultMaxTasksForAgent(agent)),
537
+ description: "Logical agent max-tasks policy (seeded from agents.maxTasks)"
538
+ });
539
+ }
540
+ async function resetAgentMaxTasksMirror(agentId) {
541
+ const agent = await getAgentById2(agentId);
542
+ if (!agent)
543
+ return;
544
+ await updateAgentMaxTasks2(agentId, defaultMaxTasksForAgent(agent));
545
+ }
546
+ async function upsertSwarmConfigWithPolicyMirror(data) {
547
+ const scopeId = data.scope === "agent" ? data.scopeId : null;
548
+ if (!scopeId || data.key !== AGENT_MAX_TASKS_CONFIG_KEY) {
549
+ return upsertSwarmConfig2(data);
550
+ }
551
+ return await getDbClient2().transaction(async () => {
552
+ const row = await upsertSwarmConfig2(data);
553
+ const value = parseAgentMaxTasksValue(data.value);
554
+ if (value !== null)
555
+ await updateAgentMaxTasks2(scopeId, value);
556
+ return row;
557
+ });
558
+ }
559
+ function rowToRuntimeInstance(row) {
560
+ let metadata = null;
561
+ if (row.metadata) {
562
+ try {
563
+ metadata = JSON.parse(row.metadata);
564
+ } catch {
565
+ metadata = null;
566
+ }
567
+ }
568
+ return {
569
+ id: row.id,
570
+ agentId: row.agent_id,
571
+ poolId: row.pool_id ?? null,
572
+ controllerId: row.controller_id ?? null,
573
+ externalHandle: row.external_handle ? JSON.parse(row.external_handle) : null,
574
+ provisionAttempts: row.provision_attempts ?? 0,
575
+ provisionError: row.provision_error ?? null,
576
+ provisionRequestedAt: row.provision_requested_at ?? null,
577
+ draining: row.draining === 1,
578
+ idleSince: row.idle_since ?? null,
579
+ leaseExpiresAt: row.lease_expires_at ?? null,
580
+ taskId: row.task_id ?? null,
581
+ status: row.status,
582
+ reportedSlots: row.reported_slots,
583
+ credentialReady: row.credential_ready === null ? null : row.credential_ready === 1,
584
+ metadata,
585
+ lastSeenAt: row.last_seen_at,
586
+ createdAt: row.created_at,
587
+ updatedAt: row.updated_at
588
+ };
589
+ }
590
+ function runtimeStaleThresholdMinutes() {
591
+ return Number(process.env.RUNTIME_STALE_THRESHOLD_MIN) || 5;
592
+ }
593
+ function runtimeLivenessCutoff() {
594
+ return new Date(Date.now() - runtimeStaleThresholdMinutes() * 60 * 1000).toISOString();
595
+ }
596
+ async function upsertRuntimeInstance(instance) {
597
+ const metadataJson = instance.metadata ? JSON.stringify(instance.metadata) : null;
598
+ const row = await getDbClient2().transaction(async (tx) => {
599
+ const provisioning = await tx.get("SELECT id FROM runtime_instances WHERE id = ? AND status = 'provisioning' AND agent_id = ?", [instance.id, instance.agentId]);
600
+ const row = await tx.get(`INSERT INTO runtime_instances (id, agent_id, status, reported_slots, metadata)
601
+ VALUES (?, ?, 'active', ?, ?)
602
+ ON CONFLICT(id) DO UPDATE SET
603
+ status = 'active',
604
+ reported_slots = excluded.reported_slots,
605
+ metadata = COALESCE(excluded.metadata, runtime_instances.metadata),
606
+ last_seen_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
607
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
608
+ WHERE runtime_instances.agent_id = excluded.agent_id
609
+ RETURNING *`, [instance.id, instance.agentId, instance.reportedSlots, metadataJson]);
610
+ if (row?.pool_id && provisioning) {
611
+ await tx.run("UPDATE agents SET runtime_provision_attempts = 0, runtime_provision_retry_at = NULL WHERE id = ?", [row.agent_id]);
612
+ await tx.run("UPDATE runtime_pools SET consecutive_failures = 0, circuit_open_until = NULL WHERE id = ?", [row.pool_id]);
613
+ }
614
+ return row;
615
+ });
616
+ return row ? rowToRuntimeInstance(row) : null;
617
+ }
618
+ async function getRuntimeInstanceById(id) {
619
+ const row = await getDbClient2().get("SELECT * FROM runtime_instances WHERE id = ?", [id]);
620
+ return row ? rowToRuntimeInstance(row) : null;
621
+ }
622
+ async function insertProvisioningRuntime(instance) {
623
+ const row = await getDbClient2().get(`INSERT INTO runtime_instances (id, agent_id, pool_id, controller_id, status,
624
+ reported_slots, provision_requested_at)
625
+ VALUES (?, ?, ?, ?, 'provisioning', ?, ?) RETURNING *`, [
626
+ instance.id,
627
+ instance.agentId,
628
+ instance.poolId,
629
+ instance.controllerId,
630
+ instance.reportedSlots ?? 1,
631
+ new Date().toISOString()
632
+ ]);
633
+ return rowToRuntimeInstance(row);
634
+ }
635
+ async function markRuntimeTerminated(id, reason) {
636
+ await getDbClient2().transaction(async () => {
637
+ const runtime = await getRuntimeInstanceById(id);
638
+ if (!runtime)
639
+ return;
640
+ await markRuntimeInstanceOffline(id, runtime.agentId);
641
+ if (runtime.status === "provisioning") {
642
+ await getDbClient2().run("UPDATE runtime_instances SET provision_error = ? WHERE id = ?", [
643
+ scrubSecrets(reason),
644
+ id
645
+ ]);
646
+ }
647
+ await reconcileAgentStatusFromRuntimes(runtime.agentId);
648
+ await revokeRuntimeTokensForRuntime(id);
649
+ });
650
+ }
651
+ async function touchRuntimeInstance(id, agentId) {
652
+ const result = await getDbClient2().run(`UPDATE runtime_instances
653
+ SET last_seen_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
654
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
655
+ WHERE id = ? AND agent_id = ? AND status = 'active' AND last_seen_at >= ?`, [id, agentId, runtimeLivenessCutoff()]);
656
+ return result.changes > 0;
657
+ }
658
+ async function markRuntimeInstanceOffline(id, agentId) {
659
+ const row = await getDbClient2().get(`UPDATE runtime_instances
660
+ SET status = 'offline', updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
661
+ WHERE id = ? AND agent_id = ? RETURNING *`, [id, agentId]);
662
+ return row ? rowToRuntimeInstance(row) : null;
663
+ }
664
+ async function countActiveRuntimeInstancesForAgent(agentId) {
665
+ const result = await getDbClient2().get(`SELECT COUNT(*) as count FROM runtime_instances
666
+ WHERE agent_id = ? AND status = 'active' AND last_seen_at >= ?`, [agentId, runtimeLivenessCutoff()]);
667
+ return result?.count ?? 0;
668
+ }
669
+ async function listRuntimeInstancesForAgent(agentId) {
670
+ const rows = await getDbClient2().query(`SELECT *, (status = 'active' AND last_seen_at >= ?) AS is_live
671
+ FROM runtime_instances
672
+ WHERE agent_id = ?
673
+ ORDER BY created_at ASC, id ASC`, [runtimeLivenessCutoff(), agentId]);
674
+ return rows.map((row) => ({ ...rowToRuntimeInstance(row), isLive: row.is_live === 1 }));
675
+ }
676
+ async function setRuntimeCredentialReady(id, agentId, ready) {
677
+ const result = await getDbClient2().run(`UPDATE runtime_instances
678
+ SET credential_ready = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
679
+ WHERE id = ? AND agent_id = ? AND status = 'active' AND last_seen_at >= ?`, [ready ? 1 : 0, id, agentId, runtimeLivenessCutoff()]);
680
+ return result.changes > 0;
681
+ }
682
+ async function hasReadyLiveRuntime(agentId) {
683
+ const row = await getDbClient2().get(`SELECT COUNT(*) c FROM runtime_instances
684
+ WHERE agent_id = ? AND status = 'active' AND last_seen_at >= ?
685
+ AND (credential_ready IS NULL OR credential_ready = 1)`, [agentId, runtimeLivenessCutoff()]);
686
+ return (row?.c ?? 0) > 0;
687
+ }
688
+ async function reconcileAgentStatusFromRuntimes(agentId) {
689
+ await getDbClient2().transaction(async () => {
690
+ const agent = await getAgentById2(agentId);
691
+ if (!agent)
692
+ return;
693
+ if (await countActiveRuntimeInstancesForAgent(agentId) === 0) {
694
+ if (agent.status !== "offline")
695
+ await updateAgentStatus2(agentId, "offline");
696
+ return;
697
+ }
698
+ if (!await hasReadyLiveRuntime(agentId)) {
699
+ if (agent.status !== "waiting_for_credentials") {
700
+ await updateAgentStatus2(agentId, "waiting_for_credentials");
701
+ }
702
+ return;
703
+ }
704
+ const next = await getActiveTaskCount2(agentId) > 0 ? "busy" : "idle";
705
+ if (agent.status !== next)
706
+ await updateAgentStatus2(agentId, next);
707
+ });
708
+ }
709
+ async function agentsWithLiveRuntime() {
710
+ const rows = await getDbClient2().query(`SELECT DISTINCT agent_id FROM runtime_instances
711
+ WHERE status = 'active' AND last_seen_at >= ?`, [runtimeLivenessCutoff()]);
712
+ return new Set(rows.map((r) => r.agent_id));
713
+ }
714
+ async function expireStaleRuntimeInstances() {
715
+ if (!isMultiRuntimeEnabled()) {
716
+ return { expired: 0, agentsOffline: 0, pruned: 0 };
717
+ }
718
+ return await getDbClient2().transaction(async (tx) => {
719
+ const cutoff = runtimeLivenessCutoff();
720
+ const stale = await tx.query(`SELECT id, agent_id FROM runtime_instances
721
+ WHERE status = 'active' AND last_seen_at < ? AND pool_id IS NULL`, [cutoff]);
722
+ const pruned = (await tx.run("DELETE FROM runtime_instances WHERE last_seen_at < ? AND status != 'provisioning' AND (pool_id IS NULL OR status = 'offline')", [cutoff])).changes;
723
+ let agentsOffline = 0;
724
+ for (const agentId of new Set(stale.map((r) => r.agent_id))) {
725
+ await reconcileAgentStatusFromRuntimes(agentId);
726
+ if ((await getAgentById2(agentId))?.status === "offline")
727
+ agentsOffline++;
728
+ }
729
+ return { expired: stale.length, agentsOffline, pruned };
730
+ });
731
+ }
732
+ async function listRuntimeInstancesForPool(poolId) {
733
+ return (await getDbClient2().query("SELECT * FROM runtime_instances WHERE pool_id = ? ORDER BY created_at", [poolId])).map(rowToRuntimeInstance);
734
+ }
735
+
736
+ // src/be/approval-config-question.ts
737
+ init_swarm_config_guard();
738
+ var ConfigQuestionSettingsSchema = object2({
739
+ key: string2().min(1).max(255).regex(/^\S+$/, "must not contain whitespace").describe("The swarm config key to set, e.g. BITBUCKET_TOKEN"),
740
+ scope: _enum2(["global", "agent", "repo"]).describe("Config scope the value is written to"),
741
+ scopeId: string2().min(1).optional().describe("Agent or repo id for agent/repo scope. From request-human-input, agent scope defaults to the calling agent; pass it explicitly when asking on behalf of another agent."),
742
+ isSecret: boolean().optional().describe("Store encrypted and never show the value (default true). An existing secret row always stays secret."),
743
+ helpUrl: string2().url().refine((url) => /^https?:\/\//i.test(url), "must be an http(s) URL").optional().describe("Link shown next to the field, e.g. where to create the token"),
744
+ options: array2(object2({
745
+ value: string2(),
746
+ label: string2(),
747
+ description: string2().optional()
748
+ })).optional().describe("Allowed values for a non-secret key, rendered as a choice"),
749
+ defaultValue: string2().optional().describe("Prefilled value for a non-secret key")
750
+ });
751
+ var ConfigAnswerSchema = discriminatedUnion("action", [
752
+ object2({ action: literal2("set"), value: string2() }),
753
+ object2({ action: literal2("keep") }),
754
+ object2({ action: literal2("skip") })
755
+ ]);
756
+ var ConfigResultSchema = object2({
757
+ key: string2(),
758
+ scope: _enum2(["global", "agent", "repo"]),
759
+ scopeId: string2().nullable(),
760
+ isSecret: boolean(),
761
+ status: _enum2(["set", "kept", "skipped"]),
762
+ updatedAt: string2().nullable(),
763
+ value: string2().optional()
764
+ });
765
+ var ConfigStateSchema = object2({
766
+ exists: boolean(),
767
+ isSecret: boolean(),
768
+ updatedAt: string2().nullable(),
769
+ value: string2().optional().describe("Current value; present only for non-secret rows")
770
+ });
771
+ async function readConfigRow(settings) {
772
+ const scopeId = settings.scope === "global" ? null : settings.scopeId ?? null;
773
+ const row = await getDbClient2().get(`SELECT isSecret, lastUpdatedAt, envPath, description,
774
+ CASE WHEN isSecret = 1 THEN NULL ELSE value END AS plainValue
775
+ FROM swarm_config
776
+ WHERE scope = ? AND ${scopeId === null ? "scopeId IS NULL" : "scopeId = ?"} AND key = ?`, scopeId === null ? [settings.scope, settings.key] : [settings.scope, scopeId, settings.key]);
777
+ if (!row)
778
+ return null;
779
+ const isSecret = row.isSecret === 1 || isApiOnlyConfigKey(settings.key);
780
+ return {
781
+ isSecret,
782
+ updatedAt: row.lastUpdatedAt,
783
+ envPath: row.envPath,
784
+ description: row.description,
785
+ ...isSecret || row.plainValue === null ? {} : { value: row.plainValue }
786
+ };
787
+ }
788
+ async function readConfigState(settings) {
789
+ const row = await readConfigRow(settings);
790
+ if (!row)
791
+ return { exists: false, isSecret: settings.isSecret !== false, updatedAt: null };
792
+ const isSecret = row.isSecret || settings.isSecret !== false;
793
+ return {
794
+ exists: true,
795
+ isSecret,
796
+ updatedAt: row.updatedAt,
797
+ ...!isSecret && row.value !== undefined ? { value: row.value } : {}
798
+ };
799
+ }
800
+ async function normalizeConfigQuestion(question, ctx) {
801
+ const fail = (reason) => ({
802
+ ok: false,
803
+ error: `config question "${question.id}": ${reason}`
804
+ });
805
+ const parsed = ConfigQuestionSettingsSchema.safeParse(question.config);
806
+ if (!parsed.success) {
807
+ const issue = parsed.error.issues[0];
808
+ const path = issue?.path.length ? `config.${issue.path.map(String).join(".")}` : "config";
809
+ return fail(`${path}: ${issue?.message ?? "invalid"}`);
810
+ }
811
+ if (ctx.approvers.policy !== "any") {
812
+ return fail('a config question needs approvers.policy "any" (a config value has one author)');
813
+ }
814
+ const settings = { ...parsed.data };
815
+ if (settings.scope === "agent" && !settings.scopeId && ctx.callerAgentId) {
816
+ settings.scopeId = ctx.callerAgentId;
817
+ }
818
+ if (settings.scope === "global" && settings.scopeId) {
819
+ return fail("global scope must not have scopeId");
820
+ }
821
+ if (settings.scope !== "global" && !settings.scopeId) {
822
+ return fail(`${settings.scope} scope requires scopeId`);
823
+ }
824
+ if (isReservedConfigKey(settings.key))
825
+ return fail(reservedKeyError(settings.key).message);
826
+ if (isInternalConfigKey(settings.key))
827
+ return fail(internalConfigKeyError(settings.key).message);
828
+ const row = await readConfigRow(settings);
829
+ const isSecret = settings.isSecret !== false || row?.isSecret === true || isApiOnlyConfigKey(settings.key);
830
+ if (isSecret && (settings.options || settings.defaultValue !== undefined)) {
831
+ return fail("options and defaultValue are only allowed when isSecret is false (and the key is not already stored as a secret)");
832
+ }
833
+ if (settings.options?.length === 0)
834
+ return fail("options must not be empty");
835
+ if (settings.options && settings.defaultValue !== undefined && !settings.options.some((option) => option.value === settings.defaultValue)) {
836
+ return fail("defaultValue must be one of options");
837
+ }
838
+ const label = question.label?.trim() ? question.label : settings.key;
839
+ return {
840
+ ok: true,
841
+ question: {
842
+ ...question,
843
+ label,
844
+ required: question.required ?? true,
845
+ config: { ...settings, isSecret }
846
+ }
847
+ };
848
+ }
849
+ async function normalizeApprovalQuestions(questions, ctx) {
850
+ const out = [];
851
+ for (const question of questions) {
852
+ if (question.type !== "config") {
853
+ if (question.config !== undefined) {
854
+ return {
855
+ ok: false,
856
+ error: `question "${question.id}": the config block is only valid on type "config"`
857
+ };
858
+ }
859
+ if (typeof question.label !== "string") {
860
+ return { ok: false, error: `question "${question.id}": label is required` };
861
+ }
862
+ out.push({ ...question, label: question.label });
863
+ continue;
864
+ }
865
+ const normalized = await normalizeConfigQuestion(question, ctx);
866
+ if (!normalized.ok)
867
+ return normalized;
868
+ out.push(normalized.question);
869
+ }
870
+ return { ok: true, questions: out };
871
+ }
872
+ function isConfigQuestion(question) {
873
+ return question.type === "config" && typeof question.config === "object" && !!question.config;
874
+ }
875
+ function parseConfigAnswer(question, raw, state) {
876
+ const fail = (reason) => ({
877
+ ok: false,
878
+ error: `config question "${question.id}": ${reason}`
879
+ });
880
+ const required = question.required !== false;
881
+ if (raw === undefined || raw === null) {
882
+ return required ? fail("an answer is required") : { ok: true, answer: { action: "skip" } };
883
+ }
884
+ const parsed = ConfigAnswerSchema.safeParse(raw);
885
+ if (!parsed.success) {
886
+ return fail('answer must be { action: "set", value } , { action: "keep" } or { action: "skip" }');
887
+ }
888
+ const answer = parsed.data;
889
+ if (answer.action === "keep") {
890
+ return state.exists ? { ok: true, answer } : fail("cannot keep a key that is not set");
891
+ }
892
+ if (answer.action === "skip") {
893
+ return required ? fail("a required question cannot be skipped") : { ok: true, answer };
894
+ }
895
+ if (answer.value.trim().length === 0)
896
+ return fail("value must not be empty");
897
+ const { config } = question;
898
+ const isSecret = config.isSecret || state.isSecret;
899
+ if (!isSecret && config.options && !config.options.some((o) => o.value === answer.value)) {
900
+ return fail("value must be one of options");
901
+ }
902
+ const error = validateConfigEntry({
903
+ scope: config.scope,
904
+ scopeId: config.scopeId ?? null,
905
+ key: config.key,
906
+ value: answer.value
907
+ });
908
+ if (error)
909
+ return fail(isSecret ? `value rejected by ${config.key} validation` : error);
910
+ return { ok: true, answer };
911
+ }
912
+ function result(question, status, isSecret, updatedAt, value) {
913
+ const { key, scope, scopeId } = question.config;
914
+ return {
915
+ key,
916
+ scope,
917
+ scopeId: scope === "global" ? null : scopeId ?? null,
918
+ isSecret,
919
+ status,
920
+ updatedAt,
921
+ ...!isSecret && value !== undefined ? { value } : {}
922
+ };
923
+ }
924
+ function skippedConfigResult(question) {
925
+ return result(question, "skipped", question.config.isSecret, null);
926
+ }
927
+ async function keptConfigResult(question) {
928
+ const row = await readConfigRow(question.config);
929
+ if (!row)
930
+ return null;
931
+ const isSecret = question.config.isSecret || row.isSecret;
932
+ return result(question, "kept", isSecret, row.updatedAt, row.value);
933
+ }
934
+ async function applyConfigAnswer(question, value) {
935
+ const { config } = question;
936
+ const existing = await readConfigRow(config);
937
+ const isSecret = config.isSecret || existing?.isSecret === true;
938
+ const written = await upsertSwarmConfigWithPolicyMirror({
939
+ scope: config.scope,
940
+ scopeId: config.scope === "global" ? null : config.scopeId ?? null,
941
+ key: config.key,
942
+ value,
943
+ isSecret,
944
+ envPath: existing?.envPath ?? null,
945
+ description: existing?.description ?? null
946
+ });
947
+ return result(question, "set", isSecret, written.lastUpdatedAt, value);
948
+ }
949
+ function formatConfigResult(value) {
950
+ const parsed = ConfigResultSchema.safeParse(value);
951
+ if (!parsed.success)
952
+ return null;
953
+ const r = parsed.data;
954
+ const where = `${r.scope}${r.scopeId ? `:${r.scopeId}` : ""}`;
955
+ const updated = r.updatedAt ? `, updated ${r.updatedAt}` : "";
956
+ const shown = !r.isSecret && r.value !== undefined ? ` = ${r.value}` : "";
957
+ return `${r.key} (${where}, ${r.isSecret ? "secret" : "plain"}): ${r.status}${updated}${shown}`;
958
+ }
959
+
960
+ // src/workflows/executors/human-in-the-loop.ts
460
961
  init_secret_scrubber();
461
962
 
462
963
  // src/workflows/approval-notifications.ts
463
964
  init_db3();
965
+ init_approvals();
464
966
  init_resolver();
465
967
 
466
968
  // src/slack/dm.ts
@@ -489,7 +991,7 @@ function escapeSlackMrkdwn(text) {
489
991
  function buildSlackQuestionsSummary(questions, timeoutText = "") {
490
992
  const header = `*Questions:*
491
993
  `;
492
- const lines = questions.map((q) => `• ${escapeSlackMrkdwn(q.label.replace(/\s+/g, " ").trim())}`);
994
+ const lines = questions.map((q) => `• ${escapeSlackMrkdwn((q.label ?? "").replace(/\s+/g, " ").trim())}${q.type === "config" ? " (set on dashboard)" : ""}`);
493
995
  const fits = (shown, omitted) => {
494
996
  const tail = omitted > 0 ? `
495
997
  _…and ${omitted} more. Answer all of them on the dashboard._` : "";
@@ -627,7 +1129,7 @@ async function postApprovalCancellationUpdates(approvals, reason, client = getSl
627
1129
  for (const notification of slackThreads) {
628
1130
  if (notification.channel !== "slack" || !notification.messageTs)
629
1131
  continue;
630
- const notificationKey = `${notification.target}:${notification.messageTs}`;
1132
+ const notificationKey = approvalCancellationNotificationKey(notification.target, notification.messageTs);
631
1133
  const claim = await claimApprovalCancellationNotification2(approval.id, notificationKey);
632
1134
  if (!claim)
633
1135
  continue;
@@ -687,6 +1189,8 @@ function formatResponses(questions, responses) {
687
1189
  let answerText;
688
1190
  if (answer == null) {
689
1191
  answerText = "(no answer)";
1192
+ } else if (q.type === "config") {
1193
+ answerText = formatConfigResult(answer) ?? "(unreadable config result)";
690
1194
  } else if (q.type === "approval") {
691
1195
  const a = answer;
692
1196
  answerText = a.approved ? "Approved" : "Rejected";
@@ -750,6 +1254,14 @@ var QuestionSchema = discriminatedUnion("type", [
750
1254
  required: boolean().default(true),
751
1255
  description: string2().optional(),
752
1256
  defaultValue: boolean().optional()
1257
+ }),
1258
+ object2({
1259
+ id: string2(),
1260
+ type: literal2("config"),
1261
+ label: string2().optional(),
1262
+ required: boolean().default(true),
1263
+ description: string2().optional(),
1264
+ config: ConfigQuestionSettingsSchema
753
1265
  })
754
1266
  ]);
755
1267
  var ApproverConfigSchema = object2({
@@ -841,7 +1353,17 @@ class HumanInTheLoopExecutor extends BaseExecutor {
841
1353
  const resolved = resolveHitlQuestions(input.config?.questions);
842
1354
  if (!resolved.ok)
843
1355
  return { status: "failed", error: resolved.error };
844
- return super.run({ ...input, config: { ...input.config, questions: resolved.questions } });
1356
+ const approvers = input.config?.approvers;
1357
+ const normalized = await normalizeApprovalQuestions(resolved.questions, {
1358
+ approvers: { policy: approvers?.policy }
1359
+ });
1360
+ if (!normalized.ok) {
1361
+ return {
1362
+ status: "failed",
1363
+ error: `human-in-the-loop questions are invalid: ${normalized.error}`
1364
+ };
1365
+ }
1366
+ return super.run({ ...input, config: { ...input.config, questions: normalized.questions } });
845
1367
  }
846
1368
  async execute(config, _context, meta) {
847
1369
  const { db } = this.deps;
@@ -2924,11 +3446,23 @@ class SystemOneDecisionExecutor extends BaseExecutor {
2924
3446
 
2925
3447
  // src/workflows/foreach-join.ts
2926
3448
  init_db3();
3449
+ init_workflow_runs();
2927
3450
 
2928
3451
  // src/workflows/checkpoint.ts
2929
3452
  init_db3();
2930
- async function checkpointStep(runId, stepId, nodeId, result, ctx) {
2931
- await getDbClient2().transaction(async () => {
3453
+ init_workflow_runs();
3454
+ async function holdsFence(runId, stepId, fence) {
3455
+ const row = await getDbClient2().get(`SELECT 1 AS found
3456
+ FROM workflow_run_steps s
3457
+ JOIN workflow_runs r ON r.id = s.runId
3458
+ WHERE s.id = ? AND s.runId = ? AND s.status = 'running' AND s.retryCount = ?
3459
+ AND r.status IN ('running', 'waiting')`, [stepId, runId, fence.retryCount]);
3460
+ return row != null && await getRunLineageBlock(runId) == null;
3461
+ }
3462
+ async function checkpointStep(runId, stepId, nodeId, result, ctx, options = {}) {
3463
+ return getDbClient2().transaction(async () => {
3464
+ if (options.fence && !await holdsFence(runId, stepId, options.fence))
3465
+ return false;
2932
3466
  await updateWorkflowRunStep2(stepId, {
2933
3467
  status: "completed",
2934
3468
  output: result.output,
@@ -2939,45 +3473,53 @@ async function checkpointStep(runId, stepId, nodeId, result, ctx) {
2939
3473
  await updateWorkflowRun2(runId, {
2940
3474
  context: ctx
2941
3475
  });
3476
+ return true;
2942
3477
  });
2943
3478
  }
2944
3479
  async function checkpointStepFailure(runId, stepId, error, retryCount, retryPolicy, options) {
2945
3480
  const now = new Date().toISOString();
2946
- if (retryPolicy && retryCount < retryPolicy.maxRetries) {
2947
- const delay = calculateBackoff(retryPolicy, retryCount);
2948
- const nextRetryAt = new Date(Date.now() + delay).toISOString();
3481
+ return getDbClient2().transaction(async () => {
3482
+ if (options?.fence && !await holdsFence(runId, stepId, options.fence)) {
3483
+ return { shouldRetry: false, stale: true };
3484
+ }
3485
+ if (retryPolicy && retryCount < retryPolicy.maxRetries) {
3486
+ const delay = calculateBackoff(retryPolicy, retryCount);
3487
+ const nextRetryAt = new Date(Date.now() + delay).toISOString();
3488
+ await updateWorkflowRunStep2(stepId, {
3489
+ status: "failed",
3490
+ error,
3491
+ output: options?.output,
3492
+ retryCount: retryCount + 1,
3493
+ maxRetries: retryPolicy.maxRetries,
3494
+ nextRetryAt
3495
+ });
3496
+ return { shouldRetry: true };
3497
+ }
2949
3498
  await updateWorkflowRunStep2(stepId, {
2950
3499
  status: "failed",
2951
3500
  error,
2952
- retryCount: retryCount + 1,
2953
- maxRetries: retryPolicy.maxRetries,
2954
- nextRetryAt
3501
+ output: options?.output,
3502
+ finishedAt: now,
3503
+ nextRetryAt: null
2955
3504
  });
2956
- return { shouldRetry: true };
2957
- }
2958
- await updateWorkflowRunStep2(stepId, {
2959
- status: "failed",
2960
- error,
2961
- finishedAt: now,
2962
- nextRetryAt: null
2963
- });
2964
- const markRunFailed = options?.markRunFailed ?? true;
2965
- if (markRunFailed) {
2966
- await getDbClient2().transaction(async () => {
3505
+ const markRunFailed = options?.markRunFailed ?? true;
3506
+ if (markRunFailed) {
2967
3507
  const run = await getWorkflowRun2(runId);
2968
- if (run?.status !== "running" && run?.status !== "waiting")
2969
- return;
2970
- await updateWorkflowRun2(runId, {
2971
- status: "failed",
2972
- error: `Step failed: ${error}`,
2973
- finishedAt: now
2974
- });
2975
- });
2976
- }
2977
- return { shouldRetry: false };
3508
+ if (run?.status === "running" || run?.status === "waiting") {
3509
+ await updateWorkflowRun2(runId, {
3510
+ status: "failed",
3511
+ error: `Step failed: ${error}`,
3512
+ finishedAt: now
3513
+ });
3514
+ }
3515
+ }
3516
+ return { shouldRetry: false };
3517
+ });
2978
3518
  }
2979
3519
  async function checkpointStepWaiting(runId, stepId, ctx, options = {}) {
2980
3520
  return getDbClient2().transaction(async () => {
3521
+ if (await getRunLineageBlock(runId))
3522
+ return false;
2981
3523
  const step = await getDbClient2().get(`UPDATE workflow_run_steps
2982
3524
  SET status = 'waiting'
2983
3525
  WHERE id = ? AND runId = ? AND status = 'running'
@@ -3014,6 +3556,7 @@ function calculateBackoff(policy, attempt) {
3014
3556
 
3015
3557
  // src/workflows/foreach-driver.ts
3016
3558
  init_db3();
3559
+ init_workflow_runs();
3017
3560
 
3018
3561
  // src/workflows/constants.ts
3019
3562
  var FAILED_TASK_OUTPUT_PREFIX = "[FAILED:";
@@ -3021,6 +3564,7 @@ var FOREACH_ITEMS_WAIT_FOR = "foreach.items";
3021
3564
 
3022
3565
  // src/workflows/engine.ts
3023
3566
  init_db3();
3567
+ init_workflow_runs();
3024
3568
  init_workflow_replay();
3025
3569
  init_telemetry();
3026
3570
 
@@ -3408,64 +3952,90 @@ async function resolveRenderedWorkflowInputs(input) {
3408
3952
  }
3409
3953
  async function startWorkflowExecution2(workflow, triggerData, registry, options = {}) {
3410
3954
  const preflight = preflightAutomation(workflowPreflightInput(workflow), await getAutomationSetupStates());
3411
- if (preflight.state === "needs_setup") {
3412
- const { runId, recorded } = await recordWorkflowPreflightFailure({
3413
- workflowId: workflow.id,
3414
- triggerType: options.triggerType ?? "manual",
3415
- triggerData,
3416
- failureReason: preflight.failureReason,
3417
- createdBy: options.requestedByUserId
3955
+ if (preflight.state === "needs_setup" && !options.parentStepId) {
3956
+ const { runId, recorded } = await getDbClient2().transaction(async () => {
3957
+ const failure = await recordWorkflowPreflightFailure({
3958
+ workflowId: workflow.id,
3959
+ triggerType: options.triggerType ?? "manual",
3960
+ triggerData,
3961
+ failureReason: preflight.failureReason,
3962
+ createdBy: options.requestedByUserId
3963
+ });
3964
+ await options.accept?.({ runId: failure.runId, skipped: false });
3965
+ return failure;
3418
3966
  });
3419
3967
  if (recorded)
3420
3968
  await notifyAutomationPreflightFailure(preflight);
3421
3969
  return runId;
3422
3970
  }
3423
- workflow = renderAutomationTokens(workflow, workflow.params ?? {});
3424
- if (workflow.triggerSchema) {
3425
- const validationErrors = validateJsonSchema(workflow.triggerSchema, triggerData);
3426
- if (validationErrors.length > 0) {
3427
- throw new TriggerSchemaError(validationErrors);
3971
+ let initial;
3972
+ if (preflight.state === "needs_setup") {
3973
+ initial = {
3974
+ status: "failed",
3975
+ error: preflight.failureReason,
3976
+ finishedAt: new Date().toISOString(),
3977
+ preflight: true
3978
+ };
3979
+ } else {
3980
+ workflow = renderAutomationTokens(workflow, workflow.params ?? {});
3981
+ if (workflow.triggerSchema) {
3982
+ const validationErrors = validateJsonSchema(workflow.triggerSchema, triggerData);
3983
+ if (validationErrors.length > 0) {
3984
+ throw new TriggerSchemaError(validationErrors);
3985
+ }
3986
+ }
3987
+ if (workflow.cooldown && await shouldSkipCooldown(workflow.id, workflow.cooldown)) {
3988
+ initial = { status: "skipped", error: "cooldown", finishedAt: new Date().toISOString() };
3989
+ } else {
3990
+ initial = await buildInitialRunState(workflow, triggerData, registry, options);
3428
3991
  }
3429
3992
  }
3430
- if (workflow.cooldown && await shouldSkipCooldown(workflow.id, workflow.cooldown)) {
3431
- const runId = crypto.randomUUID();
3432
- await createWorkflowRun2({
3433
- id: runId,
3993
+ const newRunId = crypto.randomUUID();
3994
+ const release = holdWorkflowRun(newRunId);
3995
+ try {
3996
+ const { runId, created, preflightAlert } = await createRunWithInitialState(workflow, triggerData, options, initial, newRunId);
3997
+ if (!created)
3998
+ return runId;
3999
+ if (initial.status === "failed" && initial.preflight) {
4000
+ if (preflightAlert)
4001
+ await notifyAutomationPreflightFailure(preflight);
4002
+ return runId;
4003
+ }
4004
+ if (initial.status === "skipped")
4005
+ return runId;
4006
+ telemetry.workflow("started", {
3434
4007
  workflowId: workflow.id,
3435
- triggerType: options.triggerType ?? "manual",
3436
- triggerData,
3437
- createdBy: options.requestedByUserId,
3438
- parentStepId: options.parentStepId
3439
- });
3440
- await updateWorkflowRun2(runId, {
3441
- status: "skipped",
3442
- error: "cooldown",
3443
- finishedAt: new Date().toISOString()
4008
+ nodeCount: workflow.definition.nodes.length,
4009
+ triggerType: options.triggerType ?? "manual"
3444
4010
  });
4011
+ if (initial.status)
4012
+ return runId;
4013
+ const walk = walkGraph(workflow.definition, runId, initial.context, findEntryNodes(workflow.definition), registry, workflow.id, getSecretInputKeys(workflow.input), options);
4014
+ if (options.parentStepId) {
4015
+ walk.catch((err) => console.error(`[workflows] Child workflow run ${runId} failed:`, err));
4016
+ return runId;
4017
+ }
4018
+ await walk;
3445
4019
  return runId;
4020
+ } finally {
4021
+ release();
3446
4022
  }
3447
- const runId = crypto.randomUUID();
3448
- await createWorkflowRun2({
3449
- id: runId,
3450
- workflowId: workflow.id,
3451
- triggerType: options.triggerType ?? "manual",
3452
- triggerData,
3453
- createdBy: options.requestedByUserId,
3454
- parentStepId: options.parentStepId
3455
- });
3456
- telemetry.workflow("started", {
3457
- workflowId: workflow.id,
3458
- nodeCount: workflow.definition.nodes.length,
3459
- triggerType: options.triggerType ?? "manual"
3460
- });
4023
+ }
4024
+
4025
+ class ParentStepNotCurrentError extends Error {
4026
+ constructor(parentStepId, attempt) {
4027
+ super(`Parent step ${parentStepId} (attempt ${attempt}) is no longer running; no child run started`);
4028
+ this.name = "ParentStepNotCurrentError";
4029
+ }
4030
+ }
4031
+ async function buildInitialRunState(workflow, triggerData, registry, options) {
3461
4032
  const notReady = await findWorkflowReadinessProblems(workflow.definition, registry);
3462
4033
  if (notReady.length > 0) {
3463
- await updateWorkflowRun2(runId, {
4034
+ return {
3464
4035
  status: "failed",
3465
4036
  error: formatReadinessRunError(notReady),
3466
4037
  finishedAt: new Date().toISOString()
3467
- });
3468
- return runId;
4038
+ };
3469
4039
  }
3470
4040
  const ctx = { trigger: triggerData };
3471
4041
  if (options.requestedByUserId) {
@@ -3479,23 +4049,41 @@ async function startWorkflowExecution2(workflow, triggerData, registry, options
3479
4049
  const resolved = await resolveRenderedWorkflowInputs(workflow.input);
3480
4050
  Object.assign(ctx, { input: resolved });
3481
4051
  } catch (err) {
3482
- await updateWorkflowRun2(runId, {
4052
+ return {
3483
4053
  status: "failed",
3484
4054
  error: `Input resolution failed: ${err}`,
3485
4055
  finishedAt: new Date().toISOString()
3486
- });
3487
- return runId;
4056
+ };
3488
4057
  }
3489
4058
  }
3490
- const entryNodes = findEntryNodes(workflow.definition);
3491
- const secretKeys = getSecretInputKeys(workflow.input);
3492
- const walk = walkGraph(workflow.definition, runId, ctx, entryNodes, registry, workflow.id, secretKeys, options);
3493
- if (options.parentStepId) {
3494
- walk.catch((err) => console.error(`[workflows] Child workflow run ${runId} failed:`, err));
3495
- return runId;
3496
- }
3497
- await walk;
3498
- return runId;
4059
+ return { context: ctx };
4060
+ }
4061
+ async function createRunWithInitialState(workflow, triggerData, options, initial, runId) {
4062
+ return getDbClient2().transaction(async () => {
4063
+ const { parentStepId } = options;
4064
+ const attempt = options.parentStepAttempt ?? 0;
4065
+ if (parentStepId) {
4066
+ if (!await isParentStepAttemptCurrent(parentStepId, attempt)) {
4067
+ throw new ParentStepNotCurrentError(parentStepId, attempt);
4068
+ }
4069
+ const existingId = await getChildWorkflowRunId(parentStepId, attempt);
4070
+ if (existingId)
4071
+ return { runId: existingId, created: false };
4072
+ }
4073
+ await createWorkflowRun2({
4074
+ id: runId,
4075
+ workflowId: workflow.id,
4076
+ triggerType: options.triggerType ?? "manual",
4077
+ triggerData,
4078
+ createdBy: options.requestedByUserId,
4079
+ parentStepId,
4080
+ parentStepAttempt: parentStepId ? attempt : undefined
4081
+ });
4082
+ await updateWorkflowRun2(runId, initial.status ? { status: initial.status, error: initial.error, finishedAt: initial.finishedAt } : { context: initial.context });
4083
+ const preflightAlert = initial.status === "failed" && initial.preflight ? await isFirstWorkflowPreflightFailureToday(workflow.id, runId) : undefined;
4084
+ await options.accept?.({ runId, skipped: initial.status === "skipped" });
4085
+ return { runId, created: true, preflightAlert };
4086
+ });
3499
4087
  }
3500
4088
  var activeWalks = new Map;
3501
4089
  var executingSteps = new Set;
@@ -3518,6 +4106,9 @@ async function failRunOnUnreadableReplay(runId, err, stepId) {
3518
4106
  function isWorkflowRunActive(runId) {
3519
4107
  return activeWalks.has(runId);
3520
4108
  }
4109
+ function isStepExecuting(stepId) {
4110
+ return executingSteps.has(stepId);
4111
+ }
3521
4112
  function holdWorkflowRun(runId) {
3522
4113
  activeWalks.set(runId, (activeWalks.get(runId) ?? 0) + 1);
3523
4114
  let released = false;
@@ -3652,6 +4243,8 @@ async function walkGraphOwned(def, runId, ctx, startNodes, registry, workflowId,
3652
4243
  const run = await getWorkflowRun2(runId);
3653
4244
  if (!run || run.status !== "running")
3654
4245
  return;
4246
+ if (await getRunLineageBlock(runId))
4247
+ return;
3655
4248
  const finalSteps = await getWorkflowRunStepsByRunId2(runId);
3656
4249
  if (hasRunningStep(finalSteps))
3657
4250
  return;
@@ -3760,17 +4353,34 @@ async function executeStep(def, runId, ctx, node, registry, workflowId, secretKe
3760
4353
  if (!run || run.status !== "running" && run.status !== "waiting") {
3761
4354
  return { halted: true };
3762
4355
  }
4356
+ if (await getRunLineageBlock(runId))
4357
+ return { halted: true };
3763
4358
  if (node.type !== "foreach") {
3764
4359
  const current = await getCurrentStepForNode2(runId, node.id, getAllPredecessors(def, node.id));
3765
4360
  if (current && (current.status !== "running" || executingSteps.has(current.id))) {
3766
4361
  return { current };
3767
4362
  }
4363
+ if (current && node.type === "sub-workflow") {
4364
+ claimedStepId = current.id;
4365
+ executingSteps.add(current.id);
4366
+ return {
4367
+ existingStep: current,
4368
+ stepId: current.id,
4369
+ fence: { retryCount: current.retryCount },
4370
+ deduped: false
4371
+ };
4372
+ }
3768
4373
  }
3769
4374
  const iteration = await getStepCountForNode2(runId, node.id);
3770
4375
  const idempotencyKey = `${runId}:${node.id}:${iteration}`;
3771
4376
  const existingStep = await getStepByIdempotencyKey2(idempotencyKey);
3772
4377
  if (existingStep && (existingStep.status === "completed" || existingStep.status === "waiting")) {
3773
- return { existingStep, stepId: existingStep.id, deduped: true };
4378
+ return {
4379
+ existingStep,
4380
+ stepId: existingStep.id,
4381
+ fence: { retryCount: existingStep.retryCount },
4382
+ deduped: true
4383
+ };
3774
4384
  }
3775
4385
  const latestForeachStep = node.type === "foreach" ? await getLatestStepForNode2(runId, node.id) : undefined;
3776
4386
  const reusableForeachStep = latestForeachStep && !FOREACH_TERMINAL_STEP_STATUSES.has(latestForeachStep.status) ? latestForeachStep : undefined;
@@ -3787,7 +4397,8 @@ async function executeStep(def, runId, ctx, node, registry, workflowId, secretKe
3787
4397
  }
3788
4398
  claimedStepId = stepId;
3789
4399
  executingSteps.add(stepId);
3790
- return { existingStep, stepId, deduped: false };
4400
+ const fence = { retryCount: reusableForeachStep?.retryCount ?? 0 };
4401
+ return { existingStep, stepId, fence, deduped: false };
3791
4402
  };
3792
4403
  const dedup = await getDbClient2().transaction(claimStep).catch((err) => {
3793
4404
  if (claimedStepId)
@@ -3811,7 +4422,7 @@ async function executeStep(def, runId, ctx, node, registry, workflowId, secretKe
3811
4422
  return { outcome: "waiting", successors: [] };
3812
4423
  }
3813
4424
  try {
3814
- return await runClaimedStep(def, runId, ctx, node, registry, dedup.existingStep, dedup.stepId, workflowId, options);
4425
+ return await runClaimedStep(def, runId, ctx, node, registry, dedup.existingStep, dedup.stepId, dedup.fence, workflowId, options);
3815
4426
  } catch (err) {
3816
4427
  if (await failRunOnUnreadableReplay(runId, err, dedup.stepId)) {
3817
4428
  return { outcome: "failed", successors: [] };
@@ -3821,11 +4432,14 @@ async function executeStep(def, runId, ctx, node, registry, workflowId, secretKe
3821
4432
  executingSteps.delete(dedup.stepId);
3822
4433
  }
3823
4434
  }
3824
- async function runClaimedStep(def, runId, ctx, node, registry, existingStep, stepId, workflowId, options = {}) {
4435
+ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, stepId, fence, workflowId, options = {}) {
4436
+ const stale = { outcome: "failed", successors: [] };
3825
4437
  const executor = registry.get(node.type);
3826
4438
  const systemOneRetryProblems = systemOneRetryViolations(node);
3827
4439
  if (systemOneRetryProblems.length > 0) {
3828
- await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0);
4440
+ await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0, undefined, {
4441
+ fence
4442
+ });
3829
4443
  return { outcome: "failed", successors: [] };
3830
4444
  }
3831
4445
  const interpolationCtx = buildNodeInterpolationCtx(node, ctx);
@@ -3833,7 +4447,11 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
3833
4447
  const inputErrors = validateJsonSchema(node.inputSchema, interpolationCtx);
3834
4448
  if (inputErrors.length > 0) {
3835
4449
  const errorMsg = `Input schema validation failed: ${inputErrors.join("; ")}`;
3836
- await checkpointStepFailure(runId, stepId, errorMsg, 0);
4450
+ const { stale: lost } = await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
4451
+ fence
4452
+ });
4453
+ if (lost)
4454
+ return stale;
3837
4455
  throw new Error(errorMsg);
3838
4456
  }
3839
4457
  }
@@ -3847,11 +4465,11 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
3847
4465
  const executionCtx = { ...ctx, ...interpolationCtx };
3848
4466
  if (scriptBodyUnresolved && scriptBodyUnresolved.length > 0) {
3849
4467
  const errorMsg = scriptBodyInterpolationError(node.id, scriptBodyUnresolved);
3850
- await checkpointStepFailure(runId, stepId, errorMsg, 0);
4468
+ await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, { fence });
3851
4469
  return { outcome: "failed", successors: [] };
3852
4470
  }
3853
4471
  if (strictUnresolved && strictUnresolved.length > 0) {
3854
- await checkpointStepFailure(runId, stepId, systemOneUnresolvedError(node.id, strictUnresolved, node.type), 0);
4472
+ await checkpointStepFailure(runId, stepId, systemOneUnresolvedError(node.id, strictUnresolved, node.type), 0, undefined, { fence });
3855
4473
  return { outcome: "failed", successors: [] };
3856
4474
  }
3857
4475
  if (unresolved.length > 0) {
@@ -3884,19 +4502,20 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
3884
4502
  const errorMsg = err instanceof Error ? err.message : String(err);
3885
4503
  const retryPolicy = node.retry || executor.retryPolicy;
3886
4504
  const currentRetryCount = existingStep?.retryCount || 0;
3887
- const { shouldRetry } = await checkpointStepFailure(runId, stepId, errorMsg, currentRetryCount, retryPolicy);
4505
+ const { shouldRetry, stale: lost } = await checkpointStepFailure(runId, stepId, errorMsg, currentRetryCount, retryPolicy, { fence });
4506
+ if (lost)
4507
+ return stale;
3888
4508
  if (!shouldRetry) {
3889
4509
  throw err;
3890
4510
  }
3891
4511
  return { outcome: "completed", successors: [] };
3892
4512
  }
3893
4513
  if (result.status === "failed") {
3894
- const retryPolicy = node.retry || executor.retryPolicy;
4514
+ const retryPolicy = result.retryable === false ? undefined : node.retry || executor.retryPolicy;
3895
4515
  const currentRetryCount = existingStep?.retryCount || 0;
3896
- const { shouldRetry } = await checkpointStepFailure(runId, stepId, result.error || "Executor returned failed status", currentRetryCount, retryPolicy);
3897
- if (result.output !== undefined) {
3898
- await updateWorkflowRunStep2(stepId, { output: result.output });
3899
- }
4516
+ const { shouldRetry, stale: lost } = await checkpointStepFailure(runId, stepId, result.error || "Executor returned failed status", currentRetryCount, retryPolicy, { fence, output: result.output });
4517
+ if (lost)
4518
+ return stale;
3900
4519
  if (!shouldRetry) {
3901
4520
  throw new Error(result.error || "Step execution failed");
3902
4521
  }
@@ -3917,7 +4536,7 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
3917
4536
  }
3918
4537
  const waiting = await checkpointStepWaiting(runId, stepId, ctx);
3919
4538
  if (waiting && result.waitFor === "workflow.child.finished") {
3920
- const { resumeFromChildRun } = await import("./resume-a26kvr41.js");
4539
+ const { resumeFromChildRun } = await import("./resume-frdtgfa0.js");
3921
4540
  await resumeFromChildRun(stepId, registry);
3922
4541
  }
3923
4542
  return { outcome: waiting ? "waiting" : "completed", successors: [] };
@@ -3926,7 +4545,11 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
3926
4545
  const outputErrors = validateJsonSchema(node.outputSchema, result.output);
3927
4546
  if (outputErrors.length > 0) {
3928
4547
  const errorMsg = `Output schema validation failed: ${outputErrors.join("; ")}`;
3929
- await checkpointStepFailure(runId, stepId, errorMsg, 0);
4548
+ const { stale: lost } = await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
4549
+ fence
4550
+ });
4551
+ if (lost)
4552
+ return stale;
3930
4553
  throw new Error(errorMsg);
3931
4554
  }
3932
4555
  }
@@ -3935,7 +4558,10 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
3935
4558
  validationResult = await runStepValidation(registry, node, result.output, executionCtx, meta);
3936
4559
  if (validationResult.outcome === "halt") {
3937
4560
  const errorMsg = "Validation failed (mustPass)";
3938
- await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, { markRunFailed: false });
4561
+ await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
4562
+ markRunFailed: false,
4563
+ fence
4564
+ });
3939
4565
  return { outcome: "failed", successors: [] };
3940
4566
  }
3941
4567
  if (validationResult.outcome === "retry") {
@@ -3946,12 +4572,15 @@ async function runClaimedStep(def, runId, ctx, node, registry, existingStep, ste
3946
4572
  }
3947
4573
  const retryPolicy = node.validation.retry || node.retry;
3948
4574
  const currentRetryCount = existingStep?.retryCount || 0;
3949
- await checkpointStepFailure(runId, stepId, "Validation failed, retrying", currentRetryCount, retryPolicy);
4575
+ const { stale: lost } = await checkpointStepFailure(runId, stepId, "Validation failed, retrying", currentRetryCount, retryPolicy, { fence });
4576
+ if (lost)
4577
+ return stale;
3950
4578
  return { outcome: "completed", successors: [] };
3951
4579
  }
3952
4580
  }
3953
4581
  result.nextPort = resolveValidationPort(node.next, result.nextPort, validationResult?.passed);
3954
- await checkpointStep(runId, stepId, node.id, result, ctx);
4582
+ if (!await checkpointStep(runId, stepId, node.id, result, ctx, { fence }))
4583
+ return stale;
3955
4584
  const successors = result.nextPort ? getSuccessors(def, node.id, result.nextPort) : getSuccessors(def, node.id);
3956
4585
  return { outcome: "completed", successors };
3957
4586
  }
@@ -4199,7 +4828,7 @@ class ForeachExecutor extends BaseExecutor {
4199
4828
  };
4200
4829
  }));
4201
4830
  const childSteps = (await this.deps.db.getWorkflowRunStepsByRunId(meta.runId)).filter((step) => parseSyntheticNodeId(step.nodeId)?.parentNodeId === meta.nodeId);
4202
- if (childSteps.every((step) => FOREACH_TERMINAL_STEP_STATUSES.has(step.status))) {
4831
+ if (childSteps.every(isForeachChildSettled)) {
4203
4832
  return { status: "success", output: buildForeachAggregate(childSteps) };
4204
4833
  }
4205
4834
  return {
@@ -4369,7 +4998,7 @@ async function runForeachDrive(runId, parentNodeId, registry) {
4369
4998
  if (drive.successors.length > 0) {
4370
4999
  await walkGraph(workflow.definition, runId, ctx, drive.successors, registry, workflow.id, getSecretInputKeys(workflow.input));
4371
5000
  } else {
4372
- const { finalizeOrWait } = await import("./resume-a26kvr41.js");
5001
+ const { finalizeOrWait } = await import("./resume-frdtgfa0.js");
4373
5002
  await finalizeOrWait(runId);
4374
5003
  }
4375
5004
  return drive;
@@ -4408,6 +5037,11 @@ async function claimChildren(def, runId, parentNode) {
4408
5037
  const claimed = [];
4409
5038
  try {
4410
5039
  await getDbClient2().transaction(async () => {
5040
+ const status = await getWorkflowRunStatus(runId);
5041
+ if (status !== "running" && status !== "waiting")
5042
+ return;
5043
+ if (await getRunLineageBlock(runId))
5044
+ return;
4411
5045
  const children = latestForeachChildren(def, parentNode.id, await getWorkflowRunStepsByRunId2(runId));
4412
5046
  for (const child of children) {
4413
5047
  if (child.status === "running" && !ownedChildren.has(child.id)) {
@@ -4415,7 +5049,7 @@ async function claimChildren(def, runId, parentNode) {
4415
5049
  claimed.push(child);
4416
5050
  }
4417
5051
  }
4418
- const inFlight = children.filter((child) => child.status === "running" || child.status === "waiting").length;
5052
+ const inFlight = children.filter((child) => child.status === "running" || child.status === "waiting" || isRetryPending(child)).length;
4419
5053
  let free = foreachConcurrencyCap(parentNode) - inFlight;
4420
5054
  for (const child of children) {
4421
5055
  if (free <= 0)
@@ -4511,7 +5145,7 @@ async function startChild(input) {
4511
5145
  const parked = await getDbClient2().get("UPDATE workflow_run_steps SET status = 'waiting' WHERE id = ? AND status = 'running' RETURNING id", [child.id]);
4512
5146
  if (parked && result.waitFor === "workflow.child.finished") {
4513
5147
  ownedChildren.delete(child.id);
4514
- const { resumeFromChildRun } = await import("./resume-a26kvr41.js");
5148
+ const { resumeFromChildRun } = await import("./resume-frdtgfa0.js");
4515
5149
  await resumeFromChildRun(child.id, registry);
4516
5150
  }
4517
5151
  return null;
@@ -4552,6 +5186,12 @@ var FOREACH_TERMINAL_STEP_STATUSES = new Set([
4552
5186
  "cancelled",
4553
5187
  "skipped"
4554
5188
  ]);
5189
+ function isRetryPending(step) {
5190
+ return step.status === "failed" && step.nextRetryAt != null;
5191
+ }
5192
+ function isForeachChildSettled(step) {
5193
+ return FOREACH_TERMINAL_STEP_STATUSES.has(step.status) && !isRetryPending(step);
5194
+ }
4555
5195
  function parseSyntheticNodeId(nodeId) {
4556
5196
  const separatorIndex = nodeId.indexOf("#");
4557
5197
  if (separatorIndex <= 0 || separatorIndex === nodeId.length - 1)
@@ -4574,7 +5214,7 @@ async function joinForeach(def, runId, childStep, ctx) {
4574
5214
  throw new Error(`Step "${childStep.nodeId}" is not a foreach child`);
4575
5215
  }
4576
5216
  const children = latestForeachChildren(def, parent.id, await getWorkflowRunStepsByRunId2(runId));
4577
- if (children.some((step) => !FOREACH_TERMINAL_STEP_STATUSES.has(step.status))) {
5217
+ if (children.some((step) => !isForeachChildSettled(step))) {
4578
5218
  return { joined: false, parentNodeId: parent.id, successors: [] };
4579
5219
  }
4580
5220
  const parentStep = await getLatestStepForNode2(runId, parent.id);
@@ -4664,6 +5304,8 @@ async function settleForeachChild(def, runId, childStepId, outcome, ctx, options
4664
5304
  });
4665
5305
  if (run.status !== "running" && run.status !== "waiting")
4666
5306
  return unclaimed;
5307
+ if (await getRunLineageBlock(runId))
5308
+ return unclaimed;
4667
5309
  if (outcome.status !== "completed" && effectiveFailurePolicy(parent, def) === "fail-fast") {
4668
5310
  const reason = options.runError ?? outcome.error ?? `foreach item ${current.nodeId} failed`;
4669
5311
  await cancelQueuedForeachChildren(def, runId, parent.id, current.nodeId);
@@ -4693,9 +5335,11 @@ async function cancelQueuedForeachChildren(def, runId, parentNodeId, failedChild
4693
5335
  await getDbClient2().transaction(async () => {
4694
5336
  const children = latestForeachChildren(def, parentNodeId, await getWorkflowRunStepsByRunId2(runId));
4695
5337
  for (const child of children) {
4696
- if (child.status !== "pending")
5338
+ if (child.status !== "pending" && !isRetryPending(child))
4697
5339
  continue;
4698
- const row = await getDbClient2().get("UPDATE workflow_run_steps SET status = 'cancelled', error = ?, finishedAt = ? WHERE id = ? AND status = 'pending' RETURNING id", [error, finishedAt, child.id]);
5340
+ const row = await getDbClient2().get(`UPDATE workflow_run_steps SET status = 'cancelled', error = ?, finishedAt = ?, nextRetryAt = NULL
5341
+ WHERE id = ? AND (status = 'pending' OR (status = 'failed' AND nextRetryAt IS NOT NULL))
5342
+ RETURNING id`, [error, finishedAt, child.id]);
4699
5343
  if (row)
4700
5344
  cancelled++;
4701
5345
  }
@@ -5122,4 +5766,4 @@ function applyDefinitionPatch(def, patch) {
5122
5766
  return { definition: patchedDef, errors };
5123
5767
  }
5124
5768
 
5125
- export { notifyAutomationPreflightFailure, automationIntegrationFixUrl, isNeedsSetupFailure, preflightAutomation, renderAutomationTokens, getAutomationSetupStates, schedulePreflightInput, listEnabledAutomationPreflightInputs, recordSchedulePreflightFailure, BaseExecutor, resolveSlackConversationId, dispatchApprovalNotifications, postApprovalCancellationUpdates, createApprovalFollowUpTask, HumanInTheLoopExecutor, resolveWorkflowLlmConfig, SYSTEM_ONE_PROVIDER_IDS, SystemOneExtractionExecutor, SYSTEM_ONE_DECISION_NODE_TYPE, SystemOneDecisionConfigSchema, SystemOneDecisionExecutor, checkpointStep, checkpointStepFailure, checkpointStepWaiting, FAILED_TASK_OUTPUT_PREFIX, FOREACH_ITEMS_WAIT_FOR, loadCompletedStepRouting, resolveInputValue, getSecretInputKeys, validateJsonSchema, findJsonSchemaShapeErrors, getMaxWorkflowStepsPerRun, findWorkflowReadinessProblems, formatReadinessProblems, workflowSaveWarnings, withSaveWarnings, runStepValidation, TriggerSchemaError, startWorkflowExecution2, failRunOnUnreadableReplay, isWorkflowRunActive, holdWorkflowRun, walkGraph, hasRunningStep, findReadyNodes, scriptBodyInterpolationError, ForeachExecutor, rebuildNodeExecutionInput, isForeachChildOwned, parkAndDriveForeach, startForeachDrive, runForeachDrive, FOREACH_TERMINAL_STEP_STATUSES, parseSyntheticNodeId, resolveForeachParent, settleForeachChild, resolveStepNode, stepFailureMode, getNextTargets, generateEdges, resolveValidationPort, getSuccessors, definitionNodeIds, validateDefinition, applyDefinitionPatch };
5769
+ export { notifyAutomationPreflightFailure, automationIntegrationFixUrl, isNeedsSetupFailure, preflightAutomation, renderAutomationTokens, getAutomationSetupStates, schedulePreflightInput, listEnabledAutomationPreflightInputs, recordSchedulePreflightFailure, BaseExecutor, AGENT_MAX_TASKS_CONFIG_KEY, reconcileAgentMaxTasksPolicy, resetAgentMaxTasksMirror, upsertSwarmConfigWithPolicyMirror, runtimeStaleThresholdMinutes, upsertRuntimeInstance, getRuntimeInstanceById, insertProvisioningRuntime, markRuntimeTerminated, touchRuntimeInstance, markRuntimeInstanceOffline, countActiveRuntimeInstancesForAgent, listRuntimeInstancesForAgent, setRuntimeCredentialReady, reconcileAgentStatusFromRuntimes, agentsWithLiveRuntime, expireStaleRuntimeInstances, listRuntimeInstancesForPool, ConfigQuestionSettingsSchema, ConfigStateSchema, readConfigState, normalizeApprovalQuestions, isConfigQuestion, parseConfigAnswer, skippedConfigResult, keptConfigResult, applyConfigAnswer, resolveSlackConversationId, dispatchApprovalNotifications, postApprovalCancellationUpdates, createApprovalFollowUpTask, HumanInTheLoopExecutor, resolveWorkflowLlmConfig, SYSTEM_ONE_PROVIDER_IDS, SystemOneExtractionExecutor, SYSTEM_ONE_DECISION_NODE_TYPE, SystemOneDecisionConfigSchema, SystemOneDecisionExecutor, checkpointStep, checkpointStepFailure, checkpointStepWaiting, FAILED_TASK_OUTPUT_PREFIX, FOREACH_ITEMS_WAIT_FOR, loadCompletedStepRouting, resolveInputValue, getSecretInputKeys, validateJsonSchema, findJsonSchemaShapeErrors, getMaxWorkflowStepsPerRun, findWorkflowReadinessProblems, formatReadinessProblems, workflowSaveWarnings, withSaveWarnings, runStepValidation, TriggerSchemaError, startWorkflowExecution2, ParentStepNotCurrentError, failRunOnUnreadableReplay, isWorkflowRunActive, isStepExecuting, holdWorkflowRun, walkGraph, hasRunningStep, findReadyNodes, scriptBodyInterpolationError, ForeachExecutor, rebuildNodeExecutionInput, isForeachChildOwned, parkAndDriveForeach, startForeachDrive, runForeachDrive, FOREACH_TERMINAL_STEP_STATUSES, isRetryPending, parseSyntheticNodeId, resolveForeachParent, settleForeachChild, resolveStepNode, stepFailureMode, getNextTargets, generateEdges, resolveValidationPort, getSuccessors, definitionNodeIds, validateDefinition, applyDefinitionPatch };