@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.
- package/dist/{acp-adapter-csh0zz3b.js → acp-adapter-ek38cwsv.js} +5 -5
- package/dist/{actions-6xx079d1.js → actions-92mepq03.js} +19 -19
- package/dist/{amp-adapter-8n6f0hz8.js → amp-adapter-9gaxg015.js} +4 -4
- package/dist/{app-ft6r6v23.js → app-ctn8gsgx.js} +3 -3
- package/dist/{assistant-vt2cyfzd.js → assistant-zyr5kz47.js} +21 -21
- package/dist/{boot-reembed-tgrfy27t.js → boot-reembed-dwzfttdr.js} +1 -1
- package/dist/{boot-reembed-r3e808ky.js → boot-reembed-w75maga2.js} +1 -1
- package/dist/{boot-scrub-sweep-masdwxkm.js → boot-scrub-sweep-e9kq43kf.js} +3 -3
- package/dist/{boot-seeds-0n82qc7q.js → boot-seeds-x28v3fhb.js} +4 -4
- package/dist/{claude-adapter-s7j47k06.js → claude-adapter-f0afqnne.js} +5 -5
- package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-x81a7r65.js} +3 -3
- package/dist/{claude-managed-setup-3hzrgrnk.js → claude-managed-setup-bqzwjd9x.js} +2 -2
- package/dist/{claude-sdk-session-0zrdzyx2.js → claude-sdk-session-7b7whkaq.js} +9307 -9254
- package/dist/{cli-a5ebc95n.js → cli-0shx9j93.js} +16 -13
- package/dist/{cli-4qkg9968.js → cli-19202kew.js} +4 -4
- package/dist/{cli-cyfgxnvb.js → cli-1r5ad7j5.js} +19 -17
- package/dist/{cli-jmcjbjkt.js → cli-25n3mv5d.js} +9 -7
- package/dist/cli-2655k16q.js +140 -0
- package/dist/{cli-wadzapwb.js → cli-277c31dw.js} +1 -1
- package/dist/{cli-n6kmqngw.js → cli-4b2z6n0m.js} +29 -7
- package/dist/{cli-7ccf7akh.js → cli-5bnsc0e1.js} +9 -7
- package/dist/{cli-qrjv7t18.js → cli-621ce7kw.js} +1 -1
- package/dist/{cli-xzge46sj.js → cli-64mh9mf3.js} +20 -6
- package/dist/{cli-b6tnjdvx.js → cli-6a1xg1p7.js} +6 -2
- package/dist/{cli-ydg6ep51.js → cli-6n8axjv1.js} +1 -1
- package/dist/{cli-e91vshb1.js → cli-7p4jxgtt.js} +5 -5
- package/dist/{cli-f3pfn4x3.js → cli-9j129brq.js} +9 -3
- package/dist/{cli-xw9q173h.js → cli-9s05422w.js} +1 -1
- package/dist/{cli-15xd0j7w.js → cli-abs8ndtr.js} +12 -12
- package/dist/{cli-h5d22pfs.js → cli-asq5ydm4.js} +219 -309
- package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
- package/dist/{cli-0vgy5wfw.js → cli-bvpgr9ny.js} +2 -2
- package/dist/{cli-3e9vs93g.js → cli-c61mbdds.js} +4 -4
- package/dist/{cli-e5bxnjps.js → cli-c952c7ac.js} +5 -5
- package/dist/{cli-p859hcc8.js → cli-dew6x255.js} +1 -1
- package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
- package/dist/{cli-gpv60278.js → cli-e42vss7b.js} +2 -2
- package/dist/{cli-b63d08rs.js → cli-eftzyh9y.js} +1 -1
- package/dist/{cli-fgn7yg5r.js → cli-ethgy7ha.js} +1 -1
- package/dist/{cli-86zpfje5.js → cli-ff1tm9n9.js} +1 -1
- package/dist/{cli-cf63dnxj.js → cli-g9vd9ece.js} +37 -12
- package/dist/{cli-j3dwd4kz.js → cli-gbfxp48k.js} +770 -126
- package/dist/{cli-n2918sed.js → cli-gn5af0x5.js} +21 -21
- package/dist/{cli-d172svcw.js → cli-gwhjmh8g.js} +4 -4
- package/dist/{cli-stxdrqs2.js → cli-hh5we352.js} +24 -5
- package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
- package/dist/{cli-csh8fnbg.js → cli-jb30dzvk.js} +473 -58
- package/dist/{cli-vaxw3bg0.js → cli-jbnn4vxb.js} +3 -3
- package/dist/{cli-ha22h0eg.js → cli-k4tjsdx9.js} +45 -40
- package/dist/{cli-f55b6ky0.js → cli-m4ecdxvz.js} +40 -13
- package/dist/{cli-mntxtkhn.js → cli-mz9afxxf.js} +2 -1
- package/dist/{cli-ta8bd7th.js → cli-pzatvw94.js} +1 -1
- package/dist/{cli-3qv8naq8.js → cli-rr7zx19j.js} +1591 -353
- package/dist/{cli-b1tv22wr.js → cli-s682zgqz.js} +1 -1
- package/dist/{cli-bpbcpd8b.js → cli-sbwsxvtd.js} +1 -1
- package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
- package/dist/{cli-6y43v79h.js → cli-tsma7p8v.js} +1 -1
- package/dist/{cli-edtm741g.js → cli-wghjkr0e.js} +5 -5
- package/dist/{cli-aaqnqpyw.js → cli-wx2nj19m.js} +30 -11
- package/dist/{cli-y3dzwyy3.js → cli-x0f2v69c.js} +180 -60
- package/dist/{cli-kcnwn85g.js → cli-xtrz6bzs.js} +7 -7
- package/dist/{cli-14v1sxxy.js → cli-xzfq4at3.js} +7 -7
- package/dist/cli.js +20 -20
- package/dist/{codex-adapter-brh2tcxr.js → codex-adapter-wch12q99.js} +3 -3
- package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
- package/dist/{codex-session-runner-0dnj21qs.js → codex-session-runner-41pfcr95.js} +2 -2
- package/dist/{commands-1cnvseqx.js → commands-9849dkj6.js} +2 -2
- package/dist/{controller-kc7s9xhn.js → controller-ja0sd95p.js} +29 -3
- package/dist/{controllers-connect-jzx7t4bn.js → controllers-connect-xp05dj6x.js} +20 -20
- package/dist/{cursor-adapter-qpwp5sfg.js → cursor-adapter-623qxqew.js} +1 -2
- package/dist/{db-4vq88jkc.js → db-47vj40dy.js} +3 -3
- package/dist/{db-v5ytg5pw.js → db-w759qmx0.js} +2 -2
- package/dist/{dispatcher-trtcrqae.js → dispatcher-7b8e4z2j.js} +17 -17
- package/dist/{docker-nfy2gc8t.js → docker-s84hjs91.js} +2 -2
- package/dist/{dsh-adapter-f20xwn1z.js → dsh-adapter-2ezw3xea.js} +2 -3
- package/dist/{external-channel-n9bymc2n.js → external-channel-e2s028da.js} +2 -2
- package/dist/{grok-adapter-4kpmmnr6.js → grok-adapter-ys7gaqee.js} +6 -6
- package/dist/{handlers-ptsraqa8.js → handlers-wy1dr6ja.js} +20 -20
- package/dist/{heartbeat-60qtxtjf.js → heartbeat-w3mntghj.js} +20 -20
- package/dist/{hook-rbktcstg.js → hook-3jjywnpb.js} +9 -3
- package/dist/{hook-ngtvh729.js → hook-g7kvv6x9.js} +3 -2
- package/dist/{http-z8xy72gp.js → http-tkp7d4kb.js} +2984 -2483
- package/dist/{index-r70q0xjx.js → index-1ef1myq2.js} +17 -17
- package/dist/{index-a9nsavhx.js → index-3n8m9ea2.js} +1 -1
- package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
- package/dist/{index-s193j5fh.js → index-nmm79qgr.js} +20 -20
- package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
- package/dist/{index-06qkmcx6.js → index-q7m26kvs.js} +33 -23
- package/dist/{index-1cscynkb.js → index-xdaahthb.js} +17 -17
- package/dist/{keepalive-a9srcphj.js → keepalive-v4ta8wgg.js} +3 -3
- package/dist/{lead-3devneqy.js → lead-sawnyqxk.js} +35 -35
- package/dist/{maintenance-0er16h2y.js → maintenance-84n1ex21.js} +1 -1
- package/dist/{oauth-refresh-sweep-b65jcyw2.js → oauth-refresh-sweep-vjnnandw.js} +2 -2
- package/dist/{onboard-wqywz5py.js → onboard-sfmpe4np.js} +2 -2
- package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-jmpst4q4.js} +4 -4
- package/dist/{otel-impl-vk32s884.js → otel-impl-v6cc0zve.js} +1 -1
- package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-fk8fkcf6.js} +33 -6
- package/dist/{pricing-refresh-t27anv4z.js → pricing-refresh-fsk1e1zc.js} +3 -3
- package/dist/{rbac-roles-452665y0.js → rbac-roles-prmkdnkw.js} +2 -2
- package/dist/{rbac-roles-bmcgsxjn.js → rbac-roles-zxmxqa39.js} +1 -1
- package/dist/{render-v2-0ttsk2j4.js → render-v2-pjwxvnkw.js} +3 -3
- package/dist/{resume-a26kvr41.js → resume-frdtgfa0.js} +9 -5
- package/dist/{session-access-dgh9nkwk.js → session-access-fpez4y5q.js} +4 -4
- package/dist/{setup-9vc9qahm.js → setup-mq6m3m8q.js} +1 -1
- package/dist/{work-objects-vn8nkxwn.js → work-objects-kfahhjsq.js} +20 -20
- package/dist/{worker-1x1h8vnm.js → worker-v5e5rxj3.js} +35 -35
- package/openapi.json +3078 -2424
- package/package.json +7 -7
- package/plugin/commands/create-pr.md +2 -2
- package/plugin/pi-skills/create-pr/SKILL.md +2 -2
- package/src/be/approval-config-question.ts +389 -0
- package/src/be/automation-preflight.ts +37 -3
- package/src/be/db/approvals.ts +48 -0
- package/src/be/db/session-tree.ts +73 -15
- package/src/be/db/skill-search.ts +87 -0
- package/src/be/db/tasks/read.ts +29 -2
- package/src/be/db/tasks/write.ts +15 -5
- package/src/be/db/workflow-idempotency.ts +183 -0
- package/src/be/db/workflow-runs.ts +366 -8
- package/src/be/db.ts +92 -22
- package/src/be/migrations/239_skills_fts.sql +40 -0
- package/src/be/migrations/240_workflow_run_parent_step_attempt.sql +37 -0
- package/src/be/migrations/241_skills_meta_fts.sql +39 -0
- package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
- package/src/be/sealed-json.ts +1 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
- package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/sql-connections.ts +7 -1
- package/src/be/swarm-config-guard.ts +25 -0
- package/src/be/users.ts +9 -0
- package/src/commands/controller.ts +33 -0
- package/src/commands/runner.ts +6 -2
- package/src/extensions/apply-task-create.ts +57 -11
- package/src/extensions/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +25 -0
- package/src/extensions/dispatcher.ts +4 -0
- package/src/extensions/post-bridge.ts +8 -0
- package/src/hooks/context-bound-note.ts +196 -0
- package/src/hooks/hook.ts +12 -0
- package/src/http/approval-requests.ts +209 -38
- package/src/http/config.ts +70 -53
- package/src/http/idempotency-key.ts +117 -0
- package/src/http/sessions.ts +27 -2
- package/src/http/workflows.ts +129 -24
- package/src/integrations/chat-sdk/inbound.ts +5 -4
- package/src/prompts/context-bound-note.ts +48 -0
- package/src/prompts/session-templates.ts +25 -1
- package/src/providers/amp-adapter.ts +30 -8
- package/src/providers/claude-adapter.ts +29 -4
- package/src/providers/claude-managed-adapter.ts +26 -0
- package/src/providers/claude-sdk-session.ts +10 -0
- package/src/providers/claude-session-events.ts +13 -0
- package/src/providers/codex-adapter.ts +12 -0
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/opencode-adapter.ts +18 -0
- package/src/providers/pi-mono-adapter.ts +35 -0
- package/src/runtime-providers/docker.ts +6 -2
- package/src/scripts-runtime/types/swarm-extension.d.ts +35 -0
- package/src/tests/amp-adapter.test.ts +97 -0
- package/src/tests/approval-config-question.test.ts +702 -0
- package/src/tests/approval-requests.test.ts +10 -7
- package/src/tests/approval-sweeps.test.ts +12 -6
- package/src/tests/base-prompt.test.ts +1 -1
- package/src/tests/chat-sdk-inbound.test.ts +20 -0
- package/src/tests/claude-hook-output.test.ts +79 -1
- package/src/tests/claude-managed-adapter.test.ts +275 -0
- package/src/tests/codex-adapter.test.ts +163 -0
- package/src/tests/context-bound-note.test.ts +269 -0
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/entrypoint-gitlab-credential-helper.test.ts +273 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +321 -12
- package/src/tests/extensions-pre-task.test.ts +273 -0
- package/src/tests/fetch-resolved-env.test.ts +17 -0
- package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
- package/src/tests/fixtures/fake-amp.ts +22 -8
- package/src/tests/fixtures/held-template-render.ts +42 -0
- package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
- package/src/tests/list-endpoint-slimming.test.ts +33 -0
- package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
- package/src/tests/openapi-route-headers.test.ts +2 -0
- package/src/tests/opencode-adapter.test.ts +134 -0
- package/src/tests/pi-mono-adapter.test.ts +105 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/runner-push-delivery.test.ts +25 -0
- package/src/tests/runtime-controller-liveness.test.ts +133 -0
- package/src/tests/runtime-providers-docker.test.ts +2 -0
- package/src/tests/script-connections.test.ts +9 -0
- package/src/tests/script-run-config-scope.test.ts +91 -3
- package/src/tests/session-logs.test.ts +70 -0
- package/src/tests/sessions-tree.test.ts +205 -1
- package/src/tests/skill-search-long-content.test.ts +193 -0
- package/src/tests/skill-search.test.ts +173 -22
- package/src/tests/workflow-cascade-cancel.test.ts +1169 -0
- package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
- package/src/tests/workflow-idempotency.test.ts +915 -0
- package/src/tests/workflow-sub-workflow.test.ts +538 -1
- package/src/tools/request-human-input.ts +27 -5
- package/src/tools/workflows/cancel-workflow-run.ts +1 -1
- package/src/types.ts +7 -0
- package/src/utils/context-window.ts +8 -6
- package/src/workflows/approval-cancel.ts +15 -13
- package/src/workflows/approval-notifications.ts +15 -4
- package/src/workflows/checkpoint.ts +72 -35
- package/src/workflows/engine.ts +262 -93
- package/src/workflows/executors/base.ts +6 -0
- package/src/workflows/executors/foreach.ts +2 -2
- package/src/workflows/executors/human-in-the-loop.ts +25 -1
- package/src/workflows/executors/sub-workflow.ts +45 -16
- package/src/workflows/foreach-driver.ts +10 -3
- package/src/workflows/foreach-join.ts +29 -6
- package/src/workflows/recovery.ts +68 -1
- package/src/workflows/resume.ts +228 -77
- package/src/workflows/retry-poller.ts +74 -7
- package/src/workflows/task-step-routing.ts +29 -7
- package/templates/ai-toolbox.manifest.json +8 -8
- package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
- package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
- package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
- package/templates/extensions/comms-adherence-gate/README.md +38 -13
- package/templates/extensions/comms-adherence-gate/hooks.ts +272 -69
- package/templates/extensions/comms-adherence-gate/manifest.json +2 -2
- package/templates/skills/agent-browser/SKILL.md +4 -4
- package/templates/skills/agent-browser/content.md +4 -4
- package/templates/skills/code-quality/config.json +1 -1
- package/templates/skills/code-quality/content.md +2 -2
- package/templates/skills/delegate-work/content.md +29 -25
- package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
- package/templates/skills/qa/content.md +2 -2
- package/templates/skills/researching/content.md +2 -0
- package/templates/skills/turso-interaction/content.md +1 -1
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +8 -1
package/src/workflows/resume.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type ApprovalRequest,
|
|
2
3
|
cancelPendingApprovalRequestsForRun,
|
|
3
4
|
cancelTask,
|
|
5
|
+
getApprovalRequestById,
|
|
4
6
|
getCompletedStepNodeIds,
|
|
5
7
|
getDbClient,
|
|
6
8
|
getPendingEventWaitNames,
|
|
@@ -11,19 +13,33 @@ import {
|
|
|
11
13
|
getWorkflowRun,
|
|
12
14
|
getWorkflowRunStep,
|
|
13
15
|
getWorkflowRunStepsByRunId,
|
|
14
|
-
listCancelledApprovalRequestsForRun,
|
|
15
16
|
listCancelledApprovalRequestsForStep,
|
|
16
17
|
resolveWaitState,
|
|
17
18
|
updateWorkflowRun,
|
|
18
19
|
updateWorkflowRunStep,
|
|
19
20
|
} from "../be/db";
|
|
20
|
-
import {
|
|
21
|
-
import
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
import { listUndeliveredCancelledApprovalRequestIds } from "../be/db/approvals";
|
|
22
|
+
import {
|
|
23
|
+
cancelLiveWorkflowTreeRuns,
|
|
24
|
+
cancelOpenWorkflowTreeSteps,
|
|
25
|
+
createForeachAttemptSteps,
|
|
26
|
+
disarmWorkflowTreeRetries,
|
|
27
|
+
getChildWorkflowRunId,
|
|
28
|
+
getOpenWorkflowTreeTaskIds,
|
|
29
|
+
getRunLineageBlock,
|
|
30
|
+
getTaskIdsWithDependents,
|
|
31
|
+
getWorkflowRunStatus,
|
|
32
|
+
getWorkflowRunTreeIds,
|
|
33
|
+
workflowRunTreeHasOpenWork,
|
|
34
|
+
} from "../be/db/workflow-runs";
|
|
35
|
+
import {
|
|
36
|
+
type RetryPolicy,
|
|
37
|
+
RetryPolicySchema,
|
|
38
|
+
type WaitStateRow,
|
|
39
|
+
type WorkflowDefinition,
|
|
40
|
+
type WorkflowNode,
|
|
41
|
+
type WorkflowRun,
|
|
42
|
+
type WorkflowRunStep,
|
|
27
43
|
} from "../types";
|
|
28
44
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
29
45
|
import {
|
|
@@ -31,6 +47,7 @@ import {
|
|
|
31
47
|
postApprovalCancellationUpdates,
|
|
32
48
|
} from "./approval-notifications";
|
|
33
49
|
import { shapeApprovalResolution } from "./approval-resolution";
|
|
50
|
+
import { checkpointStepFailure } from "./checkpoint";
|
|
34
51
|
import { loadCompletedStepRouting } from "./completed-step-routing";
|
|
35
52
|
import { FAILED_TASK_OUTPUT_PREFIX } from "./constants";
|
|
36
53
|
import { getNextTargets } from "./definition";
|
|
@@ -45,6 +62,7 @@ import { startForeachDrive } from "./foreach-driver";
|
|
|
45
62
|
import {
|
|
46
63
|
FOREACH_TERMINAL_STEP_STATUSES,
|
|
47
64
|
type ForeachChildOutcome,
|
|
65
|
+
isRetryPending,
|
|
48
66
|
parseSyntheticNodeId,
|
|
49
67
|
resolveForeachParent,
|
|
50
68
|
settleForeachChild,
|
|
@@ -275,12 +293,16 @@ export async function finalizeOrWait(runId: string): Promise<void> {
|
|
|
275
293
|
// move a step into `waiting` between the read and the write, and a run
|
|
276
294
|
// finalized from a stale snapshot strands that branch.
|
|
277
295
|
await getDbClient().transaction(async () => {
|
|
296
|
+
// A cancel (or another finalizer) that committed after the caller's claim
|
|
297
|
+
// wins: a terminal run is never re-parked or completed from here.
|
|
298
|
+
const status = await getWorkflowRunStatus(runId);
|
|
299
|
+
if (status !== "running" && status !== "waiting") return;
|
|
300
|
+
if (await getRunLineageBlock(runId)) return;
|
|
278
301
|
const steps = await getWorkflowRunStepsByRunId(runId);
|
|
279
302
|
// A branch still executing belongs to a live walk, which finalizes the
|
|
280
303
|
// run itself; its finalizer only acts on a `running` run.
|
|
281
304
|
if (hasRunningStep(steps)) {
|
|
282
|
-
|
|
283
|
-
if (run?.status === "waiting") await updateWorkflowRun(runId, { status: "running" });
|
|
305
|
+
if (status === "waiting") await updateWorkflowRun(runId, { status: "running" });
|
|
284
306
|
return;
|
|
285
307
|
}
|
|
286
308
|
// A step queued for the retry poller is still live, like a waiting one.
|
|
@@ -518,6 +540,10 @@ export async function retryFailedRun(runId: string, registry: ExecutorRegistry):
|
|
|
518
540
|
const attempts = await getDbClient().transaction(async () => {
|
|
519
541
|
const current = await getWorkflowRun(runId);
|
|
520
542
|
if (!current || current.status !== "failed") return null;
|
|
543
|
+
// A failed run inside a cancelled tree keeps its failure; the cancel
|
|
544
|
+
// forbids starting it again.
|
|
545
|
+
const blocked = await getRunLineageBlock(runId);
|
|
546
|
+
if (blocked) throw new Error(`Retry not started, run left failed: ${blocked}`);
|
|
521
547
|
// Fan-outs with failed or cancelled items get one new attempt row per such
|
|
522
548
|
// item; completed items are never rerun. Selected from the rows as of this
|
|
523
549
|
// transaction: an in-flight sibling of a fail-fast fan-out can record its
|
|
@@ -528,6 +554,13 @@ export async function retryFailedRun(runId: string, registry: ExecutorRegistry):
|
|
|
528
554
|
]);
|
|
529
555
|
const capError = foreachRetryCapError(currentSteps.length, attempts);
|
|
530
556
|
if (capError) throw new Error(`Retry not started, run left failed: ${capError}`);
|
|
557
|
+
// An item retry still queued when the run failed gets a new attempt row
|
|
558
|
+
// below. Retire the queued one, or the poller starts the old row beside it.
|
|
559
|
+
for (const step of currentSteps) {
|
|
560
|
+
if (isRetryPending(step) && resolveForeachParent(workflow.definition, step.nodeId)) {
|
|
561
|
+
await updateWorkflowRunStep(step.id, { nextRetryAt: null });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
531
564
|
// A foreach item retries as a new attempt row instead of a reset: a fresh
|
|
532
565
|
// step id is a fresh invocation identity for every body type, and the
|
|
533
566
|
// failed row stays as history.
|
|
@@ -636,82 +669,162 @@ function foreachRetryCapError(
|
|
|
636
669
|
return `foreach retry would reach ${maxSteps} total steps (WORKFLOW_MAX_STEPS_PER_RUN): ${existingSteps} existing + ${added} new attempts leaves no room for the retry walk`;
|
|
637
670
|
}
|
|
638
671
|
|
|
672
|
+
const TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "cancelled", "skipped"]);
|
|
673
|
+
|
|
674
|
+
/** What one `cancelWorkflowRunRows` call covered. */
|
|
675
|
+
export interface WorkflowRunCancelResult {
|
|
676
|
+
/** True when this call moved the root run to `cancelled`. */
|
|
677
|
+
cancelled: boolean;
|
|
678
|
+
/**
|
|
679
|
+
* The root and every run below it, root first, for the caller's Slack
|
|
680
|
+
* posts. Also set for a repeat cancel that found nothing open, so posts a
|
|
681
|
+
* failed earlier cancel left undelivered are retried. Empty when the root
|
|
682
|
+
* is missing or completed, failed or skipped.
|
|
683
|
+
*/
|
|
684
|
+
runIds: string[];
|
|
685
|
+
}
|
|
686
|
+
|
|
639
687
|
/**
|
|
640
|
-
* The DB writes of a run cancel, with no Slack post.
|
|
641
|
-
*
|
|
642
|
-
*
|
|
688
|
+
* The DB writes of a run cancel, with no Slack post. Cancels the run and its
|
|
689
|
+
* whole child tree (every attempt's child, through terminal runs) in one
|
|
690
|
+
* transaction:
|
|
691
|
+
* - every non-terminal step, its task, and the run's other open tasks;
|
|
692
|
+
* - pending approvals and queued step retries;
|
|
693
|
+
* - every non-terminal run. A terminal descendant keeps its status, result
|
|
694
|
+
* and reason; only its leftover open work is cancelled.
|
|
695
|
+
*
|
|
696
|
+
* A missing root, or one that is completed, failed or skipped, cancels
|
|
697
|
+
* nothing. An already cancelled root is walked again, so a repeat cancel
|
|
698
|
+
* finishes descendants an earlier cancel missed. A tree with a cycle or past
|
|
699
|
+
* its bounds throws and nothing commits.
|
|
700
|
+
*
|
|
701
|
+
* Called inside a caller's transaction, the writes join it (as a SAVEPOINT)
|
|
702
|
+
* and commit or roll back with it. Task lifecycle events and worker nudges
|
|
703
|
+
* queue behind COMMIT (afterCommit) and drop on rollback.
|
|
643
704
|
*/
|
|
644
|
-
export async function cancelWorkflowRunRows(
|
|
645
|
-
|
|
705
|
+
export async function cancelWorkflowRunRows(
|
|
706
|
+
runId: string,
|
|
707
|
+
cancelReason: string,
|
|
708
|
+
): Promise<WorkflowRunCancelResult> {
|
|
646
709
|
const now = new Date().toISOString();
|
|
647
710
|
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
const task = await getTaskByWorkflowRunStepId(step.id);
|
|
663
|
-
if (task) {
|
|
664
|
-
await cancelTask(task.id, cancelReason);
|
|
665
|
-
}
|
|
711
|
+
// A repeat cancel of a tree with nothing left open is answered by reads
|
|
712
|
+
// alone, without the write lock. The guards keep a cancelled tree from
|
|
713
|
+
// gaining new work, so the answer still holds when this returns.
|
|
714
|
+
// Its run ids still go back, so the caller retries Slack posts an earlier
|
|
715
|
+
// cancel could not deliver.
|
|
716
|
+
const settledTree = await getDbClient().transaction(
|
|
717
|
+
async () => {
|
|
718
|
+
if ((await getWorkflowRunStatus(runId)) !== "cancelled") return null;
|
|
719
|
+
const ids = await getWorkflowRunTreeIds(runId);
|
|
720
|
+
return (await workflowRunTreeHasOpenWork(ids)) ? null : ids;
|
|
721
|
+
},
|
|
722
|
+
{ readOnly: true },
|
|
723
|
+
);
|
|
724
|
+
if (settledTree) return { cancelled: false, runIds: settledTree };
|
|
666
725
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
726
|
+
// The tree snapshot and every write commit together. A child run, task or
|
|
727
|
+
// step that commits first is in the snapshot; one that commits after sees
|
|
728
|
+
// the cancelled rows in its own guard (getRunLineageBlock and the step
|
|
729
|
+
// claims) and is refused.
|
|
730
|
+
return await getDbClient().transaction(async () => {
|
|
731
|
+
const rootStatus = await getWorkflowRunStatus(runId);
|
|
732
|
+
if (!rootStatus || (TERMINAL_RUN_STATUSES.has(rootStatus) && rootStatus !== "cancelled")) {
|
|
733
|
+
return { cancelled: false, runIds: [] };
|
|
672
734
|
}
|
|
673
735
|
|
|
674
|
-
await
|
|
675
|
-
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
736
|
+
const runIds = await getWorkflowRunTreeIds(runId);
|
|
737
|
+
// Every step task carries the run link (indexed), and a retry detaches
|
|
738
|
+
// only the step link, so this covers current and earlier attempts' tasks.
|
|
739
|
+
// Dependents are looked up once for the whole tree, and only when needed.
|
|
740
|
+
const openTaskIds = await getOpenWorkflowTreeTaskIds(runIds);
|
|
741
|
+
const withDependents =
|
|
742
|
+
openTaskIds.length > 0 ? await getTaskIdsWithDependents() : new Set<string>();
|
|
743
|
+
for (const taskId of openTaskIds) {
|
|
744
|
+
await cancelTask(taskId, cancelReason, { cascadeDependents: withDependents.has(taskId) });
|
|
745
|
+
}
|
|
746
|
+
await cancelOpenWorkflowTreeSteps(runIds, cancelReason, now);
|
|
747
|
+
await disarmWorkflowTreeRetries(runIds, now);
|
|
748
|
+
for (const id of runIds) await cancelPendingApprovalRequestsForRun(id, cancelReason);
|
|
749
|
+
// Descendants' parent steps were just cancelled, so only the root's own
|
|
750
|
+
// parent (when the root is a child) needs the child-finished wake-up.
|
|
751
|
+
await cancelLiveWorkflowTreeRuns(runIds.slice(1), cancelReason, now);
|
|
752
|
+
if (rootStatus !== "cancelled") {
|
|
753
|
+
await updateWorkflowRun(runId, { status: "cancelled", error: cancelReason, finishedAt: now });
|
|
754
|
+
}
|
|
755
|
+
return { cancelled: rootStatus !== "cancelled", runIds };
|
|
683
756
|
});
|
|
684
757
|
}
|
|
685
758
|
|
|
759
|
+
// Cancels in flight in this process, by run id. The API process owns the
|
|
760
|
+
// database, so this bounds concurrent walks of one tree.
|
|
761
|
+
const inFlightCancels = new Map<string, Promise<void>>();
|
|
762
|
+
|
|
686
763
|
/**
|
|
687
|
-
* Cancel a workflow run and
|
|
688
|
-
*
|
|
764
|
+
* Cancel a workflow run and its child tree (see `cancelWorkflowRunRows`), then
|
|
765
|
+
* post the Slack updates for every approval the cancel closed. The posts run
|
|
766
|
+
* after COMMIT, and a failed post is logged without undoing the cancel.
|
|
767
|
+
* Concurrent calls for the same run share one cancel and its result.
|
|
689
768
|
*/
|
|
690
|
-
export
|
|
691
|
-
const
|
|
769
|
+
export function cancelWorkflowRun(runId: string, reason?: string): Promise<void> {
|
|
770
|
+
const inFlight = inFlightCancels.get(runId);
|
|
771
|
+
if (inFlight) return inFlight;
|
|
772
|
+
const cancel = cancelWorkflowRunOnce(runId, reason).finally(() => {
|
|
773
|
+
inFlightCancels.delete(runId);
|
|
774
|
+
});
|
|
775
|
+
inFlightCancels.set(runId, cancel);
|
|
776
|
+
return cancel;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function cancelWorkflowRunOnce(runId: string, reason?: string): Promise<void> {
|
|
780
|
+
const run = await getWorkflowRun(runId, "display");
|
|
692
781
|
if (!run) throw new Error("Workflow run not found");
|
|
693
782
|
|
|
694
|
-
|
|
695
|
-
if (terminalStatuses.includes(run.status) && run.status !== "cancelled") {
|
|
783
|
+
if (TERMINAL_RUN_STATUSES.has(run.status) && run.status !== "cancelled") {
|
|
696
784
|
throw new Error(`Cannot cancel run in '${run.status}' state`);
|
|
697
785
|
}
|
|
698
786
|
|
|
699
787
|
const cancelReason = reason ?? "Cancelled by user";
|
|
700
|
-
const
|
|
788
|
+
const { cancelled, runIds } = await cancelWorkflowRunRows(runId, cancelReason);
|
|
701
789
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const cancelledApprovals = await listCancelledApprovalRequestsForRun(runId);
|
|
705
|
-
const latestRun = await getWorkflowRun(runId);
|
|
706
|
-
if (!applied && latestRun?.status !== "cancelled") {
|
|
790
|
+
const latestRun = await getWorkflowRun(runId, "display");
|
|
791
|
+
if (!cancelled && latestRun?.status !== "cancelled") {
|
|
707
792
|
throw new Error(`Cannot cancel run in '${latestRun?.status ?? "missing"}' state`);
|
|
708
793
|
}
|
|
709
|
-
await
|
|
710
|
-
|
|
711
|
-
|
|
794
|
+
await postTreeApprovalCancellations(
|
|
795
|
+
runIds,
|
|
796
|
+
cancelled ? cancelReason : (latestRun?.error ?? cancelReason),
|
|
712
797
|
);
|
|
713
798
|
}
|
|
714
799
|
|
|
800
|
+
/**
|
|
801
|
+
* Post the cancellation update of every cancelled approval in the given runs
|
|
802
|
+
* whose Slack post is not delivered yet. Re-read after commit: the migration
|
|
803
|
+
* trigger also cancels the approvals of a directly cancelled step. Each post
|
|
804
|
+
* is claimed, and a failed post releases its claim, so the next cancel of the
|
|
805
|
+
* tree retries it and a delivered post is never repeated.
|
|
806
|
+
*/
|
|
807
|
+
export async function postTreeApprovalCancellations(
|
|
808
|
+
runIds: string[],
|
|
809
|
+
reason: string,
|
|
810
|
+
options: { exceptRequestId?: string } = {},
|
|
811
|
+
): Promise<void> {
|
|
812
|
+
try {
|
|
813
|
+
const approvals: ApprovalRequest[] = [];
|
|
814
|
+
for (const id of await listUndeliveredCancelledApprovalRequestIds(runIds)) {
|
|
815
|
+
if (id === options.exceptRequestId) continue;
|
|
816
|
+
const approval = await getApprovalRequestById(id);
|
|
817
|
+
if (approval) approvals.push(approval);
|
|
818
|
+
}
|
|
819
|
+
if (approvals.length > 0) await postApprovalCancellationUpdates(approvals, reason);
|
|
820
|
+
} catch (err) {
|
|
821
|
+
console.error(
|
|
822
|
+
"[workflows] approval cancellation updates not posted:",
|
|
823
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
715
828
|
/** Cancel one HITL step and close any approval request it gates. */
|
|
716
829
|
export async function cancelWorkflowRunStep(
|
|
717
830
|
stepId: string,
|
|
@@ -963,10 +1076,11 @@ async function resumeClaimedWait(
|
|
|
963
1076
|
}
|
|
964
1077
|
|
|
965
1078
|
/**
|
|
966
|
-
* Resume a waiting `sub-workflow` step once its child run is
|
|
967
|
-
* completed child completes the step, any other end (or a deleted
|
|
968
|
-
* the step and its run. Returns true when this call claimed the
|
|
969
|
-
* the live event and the recovery sweep both
|
|
1079
|
+
* Resume a waiting `sub-workflow` step once its current attempt's child run is
|
|
1080
|
+
* terminal: a completed child completes the step, any other end (or a deleted
|
|
1081
|
+
* child) fails the step and its run. Returns true when this call claimed the
|
|
1082
|
+
* step. Safe to call repeatedly: the live event and the recovery sweep both
|
|
1083
|
+
* route here, and only the call that claims the step walks its successors.
|
|
970
1084
|
*/
|
|
971
1085
|
export async function resumeFromChildRun(
|
|
972
1086
|
parentStepId: string,
|
|
@@ -974,13 +1088,6 @@ export async function resumeFromChildRun(
|
|
|
974
1088
|
): Promise<boolean> {
|
|
975
1089
|
const step = await getWorkflowRunStep(parentStepId);
|
|
976
1090
|
if (!step || step.status !== "waiting" || step.nodeType !== "sub-workflow") return false;
|
|
977
|
-
const childId = await getChildWorkflowRunId(parentStepId);
|
|
978
|
-
const child = childId ? await getWorkflowRun(childId) : null;
|
|
979
|
-
// The child row exists before the step parks, so a missing one was deleted.
|
|
980
|
-
const outcome = child
|
|
981
|
-
? childRunOutcome(child)
|
|
982
|
-
: { error: `Child workflow run of step ${parentStepId} was deleted` };
|
|
983
|
-
if (!outcome) return false;
|
|
984
1091
|
|
|
985
1092
|
let claimed = false;
|
|
986
1093
|
await failClosedOnUnreadableReplay(step.runId, step.id, async () => {
|
|
@@ -990,10 +1097,34 @@ export async function resumeFromChildRun(
|
|
|
990
1097
|
if (!workflow) return;
|
|
991
1098
|
|
|
992
1099
|
const ctx = (run.context ?? {}) as Record<string, unknown>;
|
|
993
|
-
//
|
|
994
|
-
//
|
|
995
|
-
|
|
996
|
-
|
|
1100
|
+
// The step's attempt, that attempt's child and the claim share one
|
|
1101
|
+
// transaction: a retry that re-parks the step under a newer attempt in
|
|
1102
|
+
// between must not be settled by the previous attempt's child.
|
|
1103
|
+
const routing = await getDbClient().transaction(async () => {
|
|
1104
|
+
const current = await getWorkflowRunStep(step.id);
|
|
1105
|
+
if (!current || current.status !== "waiting") return null;
|
|
1106
|
+
// Inside a cancelled tree a child's end settles nothing and queues no retry.
|
|
1107
|
+
if (await getRunLineageBlock(run.id)) return null;
|
|
1108
|
+
const childId = await getChildWorkflowRunId(step.id, current.retryCount);
|
|
1109
|
+
const child = childId ? await getWorkflowRun(childId) : null;
|
|
1110
|
+
// The child row exists before the step parks, so a missing one was deleted.
|
|
1111
|
+
const outcome = child
|
|
1112
|
+
? childRunOutcome(child)
|
|
1113
|
+
: { error: `Child workflow run of step ${parentStepId} was deleted`, retryable: false };
|
|
1114
|
+
if (!outcome) return null;
|
|
1115
|
+
// A retryable end (a failed child) is this attempt failing: the node's
|
|
1116
|
+
// retry policy applies exactly as on the executor path. The poller
|
|
1117
|
+
// re-executes the step under the next attempt, which starts a new child.
|
|
1118
|
+
if ("error" in outcome && outcome.retryable) {
|
|
1119
|
+
const policy = childStepRetryPolicy(workflow.definition, step.nodeId);
|
|
1120
|
+
if (policy && current.retryCount < policy.maxRetries) {
|
|
1121
|
+
await checkpointStepFailure(run.id, step.id, outcome.error, current.retryCount, policy);
|
|
1122
|
+
return CHILD_RETRY_SCHEDULED;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
// A foreach item's failure settles the item: under `onNodeFailure:
|
|
1126
|
+
// "continue"` it can close the join, so it routes like a success.
|
|
1127
|
+
return "error" in outcome
|
|
997
1128
|
? await failStepAndRunIfWaiting(
|
|
998
1129
|
workflow.definition,
|
|
999
1130
|
step.id,
|
|
@@ -1012,6 +1143,12 @@ export async function resumeFromChildRun(
|
|
|
1012
1143
|
ctx,
|
|
1013
1144
|
registry,
|
|
1014
1145
|
);
|
|
1146
|
+
});
|
|
1147
|
+
if (!routing) return;
|
|
1148
|
+
if (routing === CHILD_RETRY_SCHEDULED) {
|
|
1149
|
+
claimed = true;
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1015
1152
|
if (!routing.claimed || routing.runFailed) {
|
|
1016
1153
|
claimed = routing.claimed;
|
|
1017
1154
|
return;
|
|
@@ -1036,6 +1173,20 @@ export async function resumeFromChildRun(
|
|
|
1036
1173
|
return claimed;
|
|
1037
1174
|
}
|
|
1038
1175
|
|
|
1176
|
+
const CHILD_RETRY_SCHEDULED = {
|
|
1177
|
+
claimed: true,
|
|
1178
|
+
runFailed: false,
|
|
1179
|
+
successors: [] as WorkflowNode[],
|
|
1180
|
+
} as const;
|
|
1181
|
+
|
|
1182
|
+
/** The `node.retry` of a top-level `sub-workflow` node; foreach items retry through their parent. */
|
|
1183
|
+
function childStepRetryPolicy(def: WorkflowDefinition, nodeId: string): RetryPolicy | null {
|
|
1184
|
+
const node = def.nodes.find((candidate) => candidate.id === nodeId);
|
|
1185
|
+
if (!node?.retry) return null;
|
|
1186
|
+
const parsed = RetryPolicySchema.safeParse(node.retry);
|
|
1187
|
+
return parsed.success ? parsed.data : null;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1039
1190
|
// ─── 64KB firedPayload cap ──────────────────────────────────────────────────
|
|
1040
1191
|
|
|
1041
1192
|
const FIRED_PAYLOAD_BYTE_CAP = 64 * 1024; // 64KB
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getWorkflowRunStepsByRunId,
|
|
7
7
|
updateWorkflowRun,
|
|
8
8
|
} from "../be/db";
|
|
9
|
+
import { disarmWorkflowTreeRetries, getRunLineageBlock } from "../be/db/workflow-runs";
|
|
9
10
|
import type { RetryPolicy } from "../types";
|
|
10
11
|
import { checkpointStep, checkpointStepFailure, checkpointStepWaiting } from "./checkpoint";
|
|
11
12
|
import { getSuccessors, resolveValidationPort } from "./definition";
|
|
@@ -17,7 +18,8 @@ import {
|
|
|
17
18
|
} from "./engine";
|
|
18
19
|
import type { AsyncExecutorResult } from "./executors/base";
|
|
19
20
|
import type { ExecutorRegistry } from "./executors/registry";
|
|
20
|
-
import { FOREACH_ITEMS_WAIT_FOR, parkAndDriveForeach } from "./foreach-driver";
|
|
21
|
+
import { FOREACH_ITEMS_WAIT_FOR, parkAndDriveForeach, runForeachDrive } from "./foreach-driver";
|
|
22
|
+
import { resolveForeachParent } from "./foreach-join";
|
|
21
23
|
import { rebuildNodeExecutionInput } from "./step-input";
|
|
22
24
|
import { runStepValidation } from "./validation";
|
|
23
25
|
|
|
@@ -45,6 +47,19 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
|
|
|
45
47
|
const workflow = await getWorkflow(run.workflowId);
|
|
46
48
|
if (!workflow) continue;
|
|
47
49
|
|
|
50
|
+
// A foreach child goes back to its fan-out's queue: the driver
|
|
51
|
+
// rebuilds the per-item config and starts it under the concurrency
|
|
52
|
+
// cap, and the item settles into the join like any other.
|
|
53
|
+
const foreachParent = resolveForeachParent(workflow.definition, step.nodeId);
|
|
54
|
+
if (foreachParent) {
|
|
55
|
+
if (!(await claimRetry(run.id, step.id, { requeueForeachChild: true }))) continue;
|
|
56
|
+
console.log(
|
|
57
|
+
`[workflows] Retrying foreach item ${step.nodeId} (attempt ${step.retryCount}) for run ${step.runId}`,
|
|
58
|
+
);
|
|
59
|
+
await runForeachDrive(run.id, foreachParent.id, registry);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
// Find the node definition for this step
|
|
49
64
|
const node = workflow.definition.nodes.find((n) => n.id === step.nodeId);
|
|
50
65
|
if (!node) continue;
|
|
@@ -61,6 +76,9 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
|
|
|
61
76
|
// the run were read outside it: a cancel committing in between must
|
|
62
77
|
// win, or the step re-executes inside a cancelled run.
|
|
63
78
|
if (!(await claimRetry(run.id, step.id))) continue;
|
|
79
|
+
// Every result write below is fenced on this claim, so a cancel or
|
|
80
|
+
// another settle that commits while the executor runs wins.
|
|
81
|
+
const fence = { retryCount: step.retryCount };
|
|
64
82
|
|
|
65
83
|
// Rebuild `run.id`, upstream outputs and the node's declared-inputs
|
|
66
84
|
// context the way the original execution saw them. Without the alias
|
|
@@ -78,6 +96,8 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
|
|
|
78
96
|
step.id,
|
|
79
97
|
scriptBodyInterpolationError(node.id, scriptBodyUnresolved),
|
|
80
98
|
step.retryCount,
|
|
99
|
+
undefined,
|
|
100
|
+
{ fence },
|
|
81
101
|
);
|
|
82
102
|
continue;
|
|
83
103
|
}
|
|
@@ -104,14 +124,17 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
|
|
|
104
124
|
if (result.status === "failed") {
|
|
105
125
|
// Re-failed — use the EXISTING retryCount from the step.
|
|
106
126
|
// checkpointStepFailure handles marking run as failed if no retries left,
|
|
107
|
-
// or setting nextRetryAt for the next poll cycle.
|
|
108
|
-
|
|
127
|
+
// or setting nextRetryAt for the next poll cycle. A non-retryable
|
|
128
|
+
// failure skips the policy, as in executeStep.
|
|
129
|
+
const retryPolicy =
|
|
130
|
+
result.retryable === false ? undefined : node.retry || executor.retryPolicy;
|
|
109
131
|
await checkpointStepFailure(
|
|
110
132
|
run.id,
|
|
111
133
|
step.id,
|
|
112
134
|
result.error || "Retry failed",
|
|
113
135
|
step.retryCount,
|
|
114
136
|
retryPolicy,
|
|
137
|
+
{ fence, output: result.output },
|
|
115
138
|
);
|
|
116
139
|
} else if (
|
|
117
140
|
"async" in result &&
|
|
@@ -150,6 +173,12 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
|
|
|
150
173
|
// find the parent already advanced.
|
|
151
174
|
const waiting = await checkpointStepWaiting(run.id, step.id, ctx);
|
|
152
175
|
if (!waiting) continue;
|
|
176
|
+
// As in executeStep: a child run that finished before the step
|
|
177
|
+
// parked had its event dropped.
|
|
178
|
+
if ((result as AsyncExecutorResult).waitFor === "workflow.child.finished") {
|
|
179
|
+
const { resumeFromChildRun } = await import("./resume");
|
|
180
|
+
await resumeFromChildRun(step.id, registry);
|
|
181
|
+
}
|
|
153
182
|
} else {
|
|
154
183
|
// Success! Re-run validation if configured before checkpointing.
|
|
155
184
|
let validationPassed: boolean | undefined;
|
|
@@ -168,6 +197,8 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
|
|
|
168
197
|
step.id,
|
|
169
198
|
"Validation failed (mustPass)",
|
|
170
199
|
step.retryCount,
|
|
200
|
+
undefined,
|
|
201
|
+
{ fence },
|
|
171
202
|
);
|
|
172
203
|
continue;
|
|
173
204
|
}
|
|
@@ -186,6 +217,7 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
|
|
|
186
217
|
"Validation failed, retrying",
|
|
187
218
|
step.retryCount,
|
|
188
219
|
retryPolicy,
|
|
220
|
+
{ fence },
|
|
189
221
|
);
|
|
190
222
|
continue;
|
|
191
223
|
}
|
|
@@ -197,7 +229,9 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
|
|
|
197
229
|
result.nextPort = resolveValidationPort(node.next, result.nextPort, validationPassed);
|
|
198
230
|
|
|
199
231
|
// Validation passed (or no validation) — checkpoint and continue
|
|
200
|
-
await checkpointStep(run.id, step.id, step.nodeId, result, ctx)
|
|
232
|
+
if (!(await checkpointStep(run.id, step.id, step.nodeId, result, ctx, { fence }))) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
201
235
|
|
|
202
236
|
const port = result.nextPort || "default";
|
|
203
237
|
const successors = getSuccessors(workflow.definition, step.nodeId, port);
|
|
@@ -219,7 +253,9 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
|
|
|
219
253
|
if (await failRunOnUnreadableReplay(run.id, err, step.id)) continue;
|
|
220
254
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
221
255
|
const retryPolicy = node.retry || executor.retryPolicy;
|
|
222
|
-
await checkpointStepFailure(run.id, step.id, errorMsg, step.retryCount, retryPolicy
|
|
256
|
+
await checkpointStepFailure(run.id, step.id, errorMsg, step.retryCount, retryPolicy, {
|
|
257
|
+
fence,
|
|
258
|
+
});
|
|
223
259
|
}
|
|
224
260
|
} catch (err) {
|
|
225
261
|
if (await failRunOnUnreadableReplay(step.runId, err, step.id)) continue;
|
|
@@ -243,14 +279,44 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
|
|
|
243
279
|
/**
|
|
244
280
|
* Flip a retry-pending step to `running` only while it is still retry-pending
|
|
245
281
|
* and its run is live, and revive a run failed by this step.
|
|
282
|
+
*
|
|
283
|
+
* `requeueForeachChild` flips a foreach child to `pending` instead, for the
|
|
284
|
+
* driver to claim, and clears the cut's error: the join reads `error` on a
|
|
285
|
+
* completed child as a failed item, so a stale one would count the retry's
|
|
286
|
+
* success as a failure.
|
|
287
|
+
*
|
|
288
|
+
* A queued child never revives a failed run: its retry does not settle, so
|
|
289
|
+
* something else failed the run. The queued retry is retired instead, leaving
|
|
290
|
+
* the item `failed` with the cut's error for a run retry to give a new attempt.
|
|
291
|
+
*
|
|
292
|
+
* A run inside a cancelled tree is never revived: its queued retry is dropped.
|
|
246
293
|
*/
|
|
247
|
-
async function claimRetry(
|
|
294
|
+
async function claimRetry(
|
|
295
|
+
runId: string,
|
|
296
|
+
stepId: string,
|
|
297
|
+
options: { requeueForeachChild?: boolean } = {},
|
|
298
|
+
): Promise<boolean> {
|
|
248
299
|
return getDbClient().transaction(async () => {
|
|
249
300
|
const run = await getWorkflowRun(runId);
|
|
250
301
|
if (!run || !RETRYABLE_RUN_STATUSES.has(run.status)) return false;
|
|
302
|
+
if (await getRunLineageBlock(runId)) {
|
|
303
|
+
await disarmWorkflowTreeRetries([runId], new Date().toISOString());
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
if (options.requeueForeachChild && run.status === "failed") {
|
|
307
|
+
await getDbClient().run(
|
|
308
|
+
`UPDATE workflow_run_steps SET nextRetryAt = NULL, finishedAt = ?
|
|
309
|
+
WHERE id = ? AND runId = ? AND status = 'failed' AND nextRetryAt IS NOT NULL`,
|
|
310
|
+
[new Date().toISOString(), stepId, runId],
|
|
311
|
+
);
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
const set = options.requeueForeachChild
|
|
315
|
+
? "status = 'pending', nextRetryAt = NULL, error = NULL"
|
|
316
|
+
: "status = 'running', nextRetryAt = NULL";
|
|
251
317
|
const claimed = await getDbClient().get<{ id: string }>(
|
|
252
318
|
`UPDATE workflow_run_steps
|
|
253
|
-
SET
|
|
319
|
+
SET ${set}
|
|
254
320
|
WHERE id = ? AND runId = ? AND status = 'failed' AND nextRetryAt IS NOT NULL
|
|
255
321
|
RETURNING id`,
|
|
256
322
|
[stepId, runId],
|
|
@@ -272,6 +338,7 @@ async function completeRunIfSettled(runId: string, ctx: Record<string, unknown>)
|
|
|
272
338
|
await getDbClient().transaction(async () => {
|
|
273
339
|
const run = await getWorkflowRun(runId);
|
|
274
340
|
if (!run || (run.status !== "running" && run.status !== "waiting")) return;
|
|
341
|
+
if (await getRunLineageBlock(runId)) return;
|
|
275
342
|
const steps = await getWorkflowRunStepsByRunId(runId);
|
|
276
343
|
const live = steps.some(
|
|
277
344
|
(s) =>
|