@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,16 +4,15 @@ import {
4
4
  getApprovalRequestById,
5
5
  getDbClient,
6
6
  getTaskById,
7
- getWorkflowRun,
8
- listCancelledApprovalRequestsForRun,
9
7
  resolveApprovalRequest,
10
8
  } from "../be/db";
9
+ import { getWorkflowRunStatus } from "../be/db/workflow-runs";
11
10
  import { can, type RbacPrincipal } from "../rbac";
12
11
  import {
13
12
  createApprovalFollowUpTask,
14
13
  postApprovalCancellationUpdates,
15
14
  } from "./approval-notifications";
16
- import { cancelWorkflowRunRows } from "./resume";
15
+ import { cancelWorkflowRunRows, postTreeApprovalCancellations } from "./resume";
17
16
 
18
17
  export type CancelApprovalRequestResult =
19
18
  | { ok: true; request: ApprovalRequest; alreadyCancelled: boolean; runCancelled: boolean }
@@ -126,25 +125,28 @@ export async function cancelApprovalRequestAndRun(
126
125
  if (!request) return null;
127
126
 
128
127
  // The request is cancelled first so it keeps this reason; the run cancel
129
- // then skips it because it is no longer pending.
128
+ // then skips it because it is no longer pending. The run's child tree is
129
+ // cancelled with it.
130
130
  let runCancelled = false;
131
+ let treeRunIds: string[] = [];
131
132
  const runId = request.workflowRunId;
132
133
  if (runId) {
133
- const run = await getWorkflowRun(runId);
134
- if (run && LIVE_RUN_STATUSES.has(run.status)) {
135
- runCancelled = await cancelWorkflowRunRows(runId, data.reason);
134
+ const status = await getWorkflowRunStatus(runId);
135
+ if (status && LIVE_RUN_STATUSES.has(status)) {
136
+ const result = await cancelWorkflowRunRows(runId, data.reason);
137
+ runCancelled = result.cancelled;
138
+ treeRunIds = result.runIds;
136
139
  }
137
140
  }
138
141
 
139
142
  const slackReason = data.slackReason ?? data.reason;
140
143
  client.afterCommit(async () => {
141
144
  await postApprovalCancellationUpdates([request], slackReason);
142
- if (runCancelled && runId) {
143
- // Other requests the run cancel closed get the run's reason.
144
- const others = (await listCancelledApprovalRequestsForRun(runId)).filter(
145
- (row) => row.id !== request.id,
146
- );
147
- await postApprovalCancellationUpdates(others, data.reason);
145
+ if (runCancelled) {
146
+ // Other requests the tree cancel closed get the run's reason.
147
+ await postTreeApprovalCancellations(treeRunIds, data.reason, {
148
+ exceptRequestId: request.id,
149
+ });
148
150
  }
149
151
  });
150
152
  return { request, runCancelled };
@@ -1,3 +1,4 @@
1
+ import { formatConfigResult } from "../be/approval-config-question";
1
2
  import {
2
3
  type ApprovalRequest,
3
4
  claimApprovalCancellationNotification,
@@ -8,6 +9,7 @@ import {
8
9
  releaseApprovalCancellationNotificationClaim,
9
10
  updateApprovalRequestNotifications,
10
11
  } from "../be/db";
12
+ import { approvalCancellationNotificationKey } from "../be/db/approvals";
11
13
  import { resolveTemplate } from "../prompts/resolver";
12
14
  import { getSlackApp } from "../slack/app";
13
15
  import { resolveSlackConversationId } from "../slack/dm";
@@ -34,11 +36,15 @@ function escapeSlackMrkdwn(text: string): string {
34
36
  * question stays answerable on the dashboard page the card's button opens.
35
37
  */
36
38
  export function buildSlackQuestionsSummary(
37
- questions: ReadonlyArray<{ label: string }>,
39
+ questions: ReadonlyArray<{ label?: string; type?: string }>,
38
40
  timeoutText = "",
39
41
  ): string {
40
42
  const header = "*Questions:*\n";
41
- const lines = questions.map((q) => `• ${escapeSlackMrkdwn(q.label.replace(/\s+/g, " ").trim())}`);
43
+ // A config question is only ever answered on the dashboard, never in Slack.
44
+ const lines = questions.map(
45
+ (q) =>
46
+ `• ${escapeSlackMrkdwn((q.label ?? "").replace(/\s+/g, " ").trim())}${q.type === "config" ? " (set on dashboard)" : ""}`,
47
+ );
42
48
  const fits = (shown: string[], omitted: number) => {
43
49
  const tail =
44
50
  omitted > 0 ? `\n_…and ${omitted} more. Answer all of them on the dashboard._` : "";
@@ -71,7 +77,7 @@ export interface ApprovalNotificationChannel {
71
77
  export interface ApprovalNotificationsInput {
72
78
  requestId: string;
73
79
  title: string;
74
- questions: ReadonlyArray<{ label: string }>;
80
+ questions: ReadonlyArray<{ label?: string; type?: string }>;
75
81
  timeoutSeconds?: number;
76
82
  notifications: ReadonlyArray<ApprovalNotificationChannel>;
77
83
  }
@@ -258,7 +264,10 @@ export async function postApprovalCancellationUpdates(
258
264
 
259
265
  for (const notification of slackThreads) {
260
266
  if (notification.channel !== "slack" || !notification.messageTs) continue;
261
- const notificationKey = `${notification.target}:${notification.messageTs}`;
267
+ const notificationKey = approvalCancellationNotificationKey(
268
+ notification.target,
269
+ notification.messageTs,
270
+ );
262
271
  const claim = await claimApprovalCancellationNotification(approval.id, notificationKey);
263
272
  if (!claim) continue;
264
273
  try {
@@ -355,6 +364,8 @@ function formatResponses(
355
364
  let answerText: string;
356
365
  if (answer == null) {
357
366
  answerText = "(no answer)";
367
+ } else if (q.type === "config") {
368
+ answerText = formatConfigResult(answer) ?? "(unreadable config result)";
358
369
  } else if (q.type === "approval") {
359
370
  const a = answer as { approved?: boolean; comment?: string };
360
371
  answerText = a.approved ? "Approved" : "Rejected";
@@ -1,8 +1,32 @@
1
1
  import { getDbClient, getWorkflowRun, updateWorkflowRun, updateWorkflowRunStep } from "../be/db";
2
+ import { getRunLineageBlock } from "../be/db/workflow-runs";
2
3
  import type { RetryPolicy } from "../types";
3
4
 
5
+ /**
6
+ * The execution a checkpoint belongs to: the step attempt (`retryCount`) that
7
+ * claimed the row. A fenced checkpoint writes only while that attempt still
8
+ * owns the row: the step is `running` with that `retryCount` in a `running` or
9
+ * `waiting` run with no cancelled ancestor. A cancel, a newer attempt or
10
+ * another settle that committed meanwhile wins, and the stale execution
11
+ * writes nothing.
12
+ */
13
+ export type StepFence = { retryCount: number };
14
+
15
+ async function holdsFence(runId: string, stepId: string, fence: StepFence): Promise<boolean> {
16
+ const row = await getDbClient().get<{ found: number }>(
17
+ `SELECT 1 AS found
18
+ FROM workflow_run_steps s
19
+ JOIN workflow_runs r ON r.id = s.runId
20
+ WHERE s.id = ? AND s.runId = ? AND s.status = 'running' AND s.retryCount = ?
21
+ AND r.status IN ('running', 'waiting')`,
22
+ [stepId, runId, fence.retryCount],
23
+ );
24
+ return row != null && (await getRunLineageBlock(runId)) == null;
25
+ }
26
+
4
27
  /**
5
28
  * Checkpoint a successful step — atomic DB write of step result + run context.
29
+ * Returns false, writing nothing, when `fence` no longer holds.
6
30
  */
7
31
  export async function checkpointStep(
8
32
  runId: string,
@@ -10,8 +34,10 @@ export async function checkpointStep(
10
34
  nodeId: string,
11
35
  result: { output?: unknown; nextPort?: string },
12
36
  ctx: Record<string, unknown>,
13
- ): Promise<void> {
14
- await getDbClient().transaction(async () => {
37
+ options: { fence?: StepFence } = {},
38
+ ): Promise<boolean> {
39
+ return getDbClient().transaction(async () => {
40
+ if (options.fence && !(await holdsFence(runId, stepId, options.fence))) return false;
15
41
  await updateWorkflowRunStep(stepId, {
16
42
  status: "completed",
17
43
  output: result.output,
@@ -24,11 +50,14 @@ export async function checkpointStep(
24
50
  await updateWorkflowRun(runId, {
25
51
  context: ctx,
26
52
  });
53
+ return true;
27
54
  });
28
55
  }
29
56
 
30
57
  /**
31
58
  * Checkpoint a step failure — marks step failed, calculates retry if applicable.
59
+ * With `fence`, a stale execution writes nothing and gets `stale: true`.
60
+ * `output` is kept on the failed step for observability, in the same write.
32
61
  */
33
62
  export async function checkpointStepFailure(
34
63
  runId: string,
@@ -36,49 +65,56 @@ export async function checkpointStepFailure(
36
65
  error: string,
37
66
  retryCount: number,
38
67
  retryPolicy?: RetryPolicy,
39
- options?: { markRunFailed?: boolean },
40
- ): Promise<{ shouldRetry: boolean }> {
68
+ options?: { markRunFailed?: boolean; fence?: StepFence; output?: unknown },
69
+ ): Promise<{ shouldRetry: boolean; stale?: true }> {
41
70
  const now = new Date().toISOString();
42
71
 
43
- if (retryPolicy && retryCount < retryPolicy.maxRetries) {
44
- const delay = calculateBackoff(retryPolicy, retryCount);
45
- const nextRetryAt = new Date(Date.now() + delay).toISOString();
72
+ return getDbClient().transaction(async () => {
73
+ if (options?.fence && !(await holdsFence(runId, stepId, options.fence))) {
74
+ return { shouldRetry: false, stale: true as const };
75
+ }
76
+
77
+ if (retryPolicy && retryCount < retryPolicy.maxRetries) {
78
+ const delay = calculateBackoff(retryPolicy, retryCount);
79
+ const nextRetryAt = new Date(Date.now() + delay).toISOString();
80
+
81
+ await updateWorkflowRunStep(stepId, {
82
+ status: "failed",
83
+ error,
84
+ output: options?.output,
85
+ retryCount: retryCount + 1,
86
+ maxRetries: retryPolicy.maxRetries,
87
+ nextRetryAt,
88
+ });
89
+
90
+ return { shouldRetry: true };
91
+ }
46
92
 
93
+ // No retries left — mark step failed, and optionally the run too
94
+ // Clear nextRetryAt so the poller stops picking this step up
47
95
  await updateWorkflowRunStep(stepId, {
48
96
  status: "failed",
49
97
  error,
50
- retryCount: retryCount + 1,
51
- maxRetries: retryPolicy.maxRetries,
52
- nextRetryAt,
98
+ output: options?.output,
99
+ finishedAt: now,
100
+ nextRetryAt: null,
53
101
  });
54
102
 
55
- return { shouldRetry: true };
56
- }
57
-
58
- // No retries left — mark step failed, and optionally the run too
59
- // Clear nextRetryAt so the poller stops picking this step up
60
- await updateWorkflowRunStep(stepId, {
61
- status: "failed",
62
- error,
63
- finishedAt: now,
64
- nextRetryAt: null,
65
- });
66
-
67
- const markRunFailed = options?.markRunFailed ?? true;
68
- if (markRunFailed) {
69
- // A cancel or finalization that committed while the step ran wins.
70
- await getDbClient().transaction(async () => {
103
+ const markRunFailed = options?.markRunFailed ?? true;
104
+ if (markRunFailed) {
105
+ // A cancel or finalization that committed while the step ran wins.
71
106
  const run = await getWorkflowRun(runId);
72
- if (run?.status !== "running" && run?.status !== "waiting") return;
73
- await updateWorkflowRun(runId, {
74
- status: "failed",
75
- error: `Step failed: ${error}`,
76
- finishedAt: now,
77
- });
78
- });
79
- }
107
+ if (run?.status === "running" || run?.status === "waiting") {
108
+ await updateWorkflowRun(runId, {
109
+ status: "failed",
110
+ error: `Step failed: ${error}`,
111
+ finishedAt: now,
112
+ });
113
+ }
114
+ }
80
115
 
81
- return { shouldRetry: false };
116
+ return { shouldRetry: false };
117
+ });
82
118
  }
83
119
 
84
120
  /**
@@ -95,6 +131,7 @@ export async function checkpointStepWaiting(
95
131
  options: { keepRunStatus?: boolean } = {},
96
132
  ): Promise<boolean> {
97
133
  return getDbClient().transaction(async () => {
134
+ if (await getRunLineageBlock(runId)) return false;
98
135
  const step = await getDbClient().get<{ id: string }>(
99
136
  `UPDATE workflow_run_steps
100
137
  SET status = 'waiting'