@desplega.ai/agent-swarm 2.5.3 → 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-gr2fcyyj.js → acp-adapter-ek38cwsv.js} +4 -4
- package/dist/{actions-wv766ghf.js → actions-92mepq03.js} +17 -17
- package/dist/{amp-adapter-speb906t.js → amp-adapter-9gaxg015.js} +2 -2
- package/dist/{app-ag29n14f.js → app-ctn8gsgx.js} +3 -3
- package/dist/{assistant-3x8bz3eg.js → assistant-zyr5kz47.js} +19 -19
- package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-dwzfttdr.js} +1 -1
- package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-w75maga2.js} +1 -1
- package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-e9kq43kf.js} +3 -3
- package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-x28v3fhb.js} +4 -4
- package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-f0afqnne.js} +4 -4
- package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-x81a7r65.js} +3 -3
- package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-bqzwjd9x.js} +1 -1
- package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-7b7whkaq.js} +9307 -9254
- package/dist/{cli-a5ebc95n.js → cli-0shx9j93.js} +16 -13
- package/dist/{cli-vwn81jbs.js → cli-19202kew.js} +3 -3
- package/dist/{cli-svrj71pg.js → cli-1r5ad7j5.js} +7 -7
- package/dist/{cli-hhxjnjzs.js → cli-25n3mv5d.js} +8 -6
- package/dist/cli-2655k16q.js +140 -0
- package/dist/{cli-hcpj2xqz.js → cli-277c31dw.js} +1 -1
- package/dist/{cli-35gtk1p6.js → cli-4b2z6n0m.js} +29 -7
- package/dist/{cli-40jsnrjm.js → cli-5bnsc0e1.js} +7 -5
- package/dist/{cli-fzcn3d87.js → cli-621ce7kw.js} +1 -1
- package/dist/{cli-s49hpn37.js → cli-64mh9mf3.js} +19 -5
- package/dist/{cli-e8jyq7d8.js → cli-6a1xg1p7.js} +6 -2
- package/dist/{cli-p9rafdrb.js → cli-7p4jxgtt.js} +4 -4
- package/dist/{cli-w1zwctj2.js → cli-9j129brq.js} +9 -3
- package/dist/{cli-ce0ak9ms.js → cli-abs8ndtr.js} +11 -11
- package/dist/{cli-aecdcr77.js → cli-asq5ydm4.js} +123 -50
- package/dist/{cli-n45mr7zh.js → cli-bvpgr9ny.js} +2 -2
- package/dist/{cli-210xd1f4.js → cli-c61mbdds.js} +3 -3
- 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-s53vp709.js → cli-eftzyh9y.js} +1 -1
- package/dist/{cli-3ngdk5te.js → cli-ethgy7ha.js} +1 -1
- package/dist/{cli-99xgw26r.js → cli-ff1tm9n9.js} +1 -1
- package/dist/{cli-ewm3pwbq.js → cli-g9vd9ece.js} +37 -12
- package/dist/{cli-jjznk3f7.js → cli-gbfxp48k.js} +62 -28
- package/dist/{cli-z21whrrg.js → cli-gn5af0x5.js} +14 -14
- package/dist/{cli-1fc85mvt.js → cli-gwhjmh8g.js} +4 -4
- package/dist/{cli-avdvgcay.js → cli-hh5we352.js} +24 -5
- package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
- package/dist/{cli-kxawjrxg.js → cli-jb30dzvk.js} +332 -36
- package/dist/{cli-25j01dpj.js → cli-jbnn4vxb.js} +3 -3
- package/dist/{cli-qzme5949.js → cli-k4tjsdx9.js} +44 -39
- package/dist/{cli-8s5wx3nz.js → cli-m4ecdxvz.js} +40 -13
- package/dist/{cli-ta8bd7th.js → cli-pzatvw94.js} +1 -1
- package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
- package/dist/{cli-502eqv93.js → cli-s682zgqz.js} +1 -1
- package/dist/{cli-fsdprwrg.js → cli-sbwsxvtd.js} +1 -1
- package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
- package/dist/{cli-egjggehm.js → cli-tsma7p8v.js} +1 -1
- package/dist/{cli-qgf8emrn.js → cli-wghjkr0e.js} +5 -5
- package/dist/{cli-p0rzjkya.js → cli-wx2nj19m.js} +29 -10
- package/dist/{cli-e2ar4bp1.js → cli-x0f2v69c.js} +111 -42
- package/dist/{cli-y561h5a9.js → cli-xtrz6bzs.js} +5 -5
- package/dist/{cli-gpsz7g88.js → cli-xzfq4at3.js} +7 -7
- package/dist/cli.js +18 -18
- package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-wch12q99.js} +2 -2
- package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
- package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-41pfcr95.js} +2 -2
- package/dist/{commands-t04rt2pc.js → commands-9849dkj6.js} +2 -2
- package/dist/{controller-rgna04r2.js → controller-ja0sd95p.js} +27 -1
- package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-xp05dj6x.js} +16 -16
- package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-623qxqew.js} +1 -2
- package/dist/{db-a4m7haw0.js → db-47vj40dy.js} +3 -3
- package/dist/{db-24ck09xy.js → db-w759qmx0.js} +2 -2
- package/dist/{dispatcher-nwf9ss0q.js → dispatcher-7b8e4z2j.js} +15 -15
- package/dist/{docker-nfy2gc8t.js → docker-s84hjs91.js} +2 -2
- package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-2ezw3xea.js} +1 -2
- package/dist/{external-channel-j1e2p34p.js → external-channel-e2s028da.js} +2 -2
- package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-ys7gaqee.js} +5 -5
- package/dist/{handlers-19xr6dnz.js → handlers-wy1dr6ja.js} +18 -18
- package/dist/{heartbeat-vtesda5d.js → heartbeat-w3mntghj.js} +17 -17
- package/dist/{hook-8wqaszda.js → hook-3jjywnpb.js} +9 -3
- package/dist/{hook-b2hhj6rd.js → hook-g7kvv6x9.js} +3 -2
- package/dist/{http-mxcqhhcs.js → http-tkp7d4kb.js} +267 -75
- package/dist/{index-srg522m6.js → index-1ef1myq2.js} +15 -15
- package/dist/{index-1r8wfpzs.js → index-3n8m9ea2.js} +1 -1
- package/dist/{index-kw2mmat7.js → index-nmm79qgr.js} +17 -17
- package/dist/{index-n1vbvta9.js → index-q7m26kvs.js} +24 -22
- package/dist/{index-spwxjgay.js → index-xdaahthb.js} +15 -15
- package/dist/{keepalive-dsd8rtgx.js → keepalive-v4ta8wgg.js} +3 -3
- package/dist/{lead-zyerk1x2.js → lead-sawnyqxk.js} +33 -33
- package/dist/{maintenance-jqmqwnet.js → maintenance-84n1ex21.js} +1 -1
- package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-vjnnandw.js} +2 -2
- package/dist/{onboard-w4yd7s8t.js → onboard-sfmpe4np.js} +2 -2
- package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-jmpst4q4.js} +3 -3
- package/dist/{otel-impl-f36byzbd.js → otel-impl-v6cc0zve.js} +1 -1
- package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-fk8fkcf6.js} +33 -6
- package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-fsk1e1zc.js} +3 -3
- package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-prmkdnkw.js} +2 -2
- package/dist/{rbac-roles-682k2mt5.js → rbac-roles-zxmxqa39.js} +1 -1
- package/dist/{render-v2-hgytz8nn.js → render-v2-pjwxvnkw.js} +3 -3
- package/dist/{resume-2dvq348g.js → resume-frdtgfa0.js} +8 -6
- package/dist/{session-access-7f21ekq9.js → session-access-fpez4y5q.js} +4 -4
- package/dist/{setup-epffxtbk.js → setup-mq6m3m8q.js} +1 -1
- package/dist/{work-objects-xz1a5rqk.js → work-objects-kfahhjsq.js} +18 -18
- package/dist/{worker-42t8emjt.js → worker-v5e5rxj3.js} +33 -33
- package/openapi.json +68 -3
- package/package.json +5 -5
- package/plugin/commands/create-pr.md +2 -2
- package/plugin/pi-skills/create-pr/SKILL.md +2 -2
- package/src/be/db/approvals.ts +48 -0
- 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 +318 -7
- package/src/be/db.ts +67 -17
- 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 +2 -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/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/config.ts +48 -16
- package/src/http/idempotency-key.ts +117 -0
- package/src/http/workflows.ts +129 -24
- 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/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-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/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/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/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 +3 -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 +37 -0
- 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/tools/workflows/cancel-workflow-run.ts +1 -1
- package/src/utils/context-window.ts +8 -6
- package/src/workflows/approval-cancel.ts +15 -13
- package/src/workflows/approval-notifications.ts +5 -1
- package/src/workflows/checkpoint.ts +6 -3
- package/src/workflows/engine.ts +30 -8
- package/src/workflows/executors/foreach.ts +2 -2
- package/src/workflows/foreach-driver.ts +10 -3
- package/src/workflows/foreach-join.ts +29 -6
- package/src/workflows/recovery.ts +6 -1
- package/src/workflows/resume.ts +168 -56
- package/src/workflows/retry-poller.ts +49 -3
- 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/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/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
|
@@ -10,7 +10,7 @@ export const registerCancelWorkflowRunTool = (server: McpServer) => {
|
|
|
10
10
|
title: "Cancel Workflow Run",
|
|
11
11
|
annotations: { destructiveHint: true },
|
|
12
12
|
description:
|
|
13
|
-
"Cancel a running or waiting workflow run. Cancels
|
|
13
|
+
"Cancel a running or waiting workflow run and every child run below it. Cancels their non-terminal steps, tasks, pending approvals and queued retries in one transaction; finished child runs keep their status. Repeating the cancel on a cancelled run finishes any child runs still open.",
|
|
14
14
|
inputSchema: z.object({
|
|
15
15
|
runId: z.string().uuid().describe("Workflow run ID to cancel"),
|
|
16
16
|
reason: z.string().optional().describe("Optional reason for cancellation"),
|
|
@@ -67,7 +67,12 @@ function catalogContext(provider: string, model: string): number | undefined {
|
|
|
67
67
|
* Unknown models get a conservative 200k so percent math stays finite.
|
|
68
68
|
*/
|
|
69
69
|
export function getContextWindowSize(model: string): number {
|
|
70
|
-
|
|
70
|
+
return getKnownContextWindowSize(model) ?? DEFAULT_CONTEXT_WINDOW;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** {@link getContextWindowSize} without the fallback: undefined when the catalog has no window. */
|
|
74
|
+
export function getKnownContextWindowSize(model: string | undefined): number | undefined {
|
|
75
|
+
if (!model) return undefined;
|
|
71
76
|
const slash = model.indexOf("/");
|
|
72
77
|
if (slash > 0) {
|
|
73
78
|
const scoped = catalogContext(model.slice(0, slash), model.slice(slash + 1));
|
|
@@ -83,11 +88,8 @@ export function getContextWindowSize(model: string): number {
|
|
|
83
88
|
if (dated) return dated;
|
|
84
89
|
}
|
|
85
90
|
const shortname = resolveClaudeShortname(model);
|
|
86
|
-
if (shortname)
|
|
87
|
-
|
|
88
|
-
if (resolved) return resolved;
|
|
89
|
-
}
|
|
90
|
-
return DEFAULT_CONTEXT_WINDOW;
|
|
91
|
+
if (shortname) return catalogContext("anthropic", shortname);
|
|
92
|
+
return undefined;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
/**
|
|
@@ -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
|
|
134
|
-
if (
|
|
135
|
-
|
|
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
|
|
143
|
-
// Other requests the
|
|
144
|
-
|
|
145
|
-
|
|
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 };
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
releaseApprovalCancellationNotificationClaim,
|
|
10
10
|
updateApprovalRequestNotifications,
|
|
11
11
|
} from "../be/db";
|
|
12
|
+
import { approvalCancellationNotificationKey } from "../be/db/approvals";
|
|
12
13
|
import { resolveTemplate } from "../prompts/resolver";
|
|
13
14
|
import { getSlackApp } from "../slack/app";
|
|
14
15
|
import { resolveSlackConversationId } from "../slack/dm";
|
|
@@ -263,7 +264,10 @@ export async function postApprovalCancellationUpdates(
|
|
|
263
264
|
|
|
264
265
|
for (const notification of slackThreads) {
|
|
265
266
|
if (notification.channel !== "slack" || !notification.messageTs) continue;
|
|
266
|
-
const notificationKey =
|
|
267
|
+
const notificationKey = approvalCancellationNotificationKey(
|
|
268
|
+
notification.target,
|
|
269
|
+
notification.messageTs,
|
|
270
|
+
);
|
|
267
271
|
const claim = await claimApprovalCancellationNotification(approval.id, notificationKey);
|
|
268
272
|
if (!claim) continue;
|
|
269
273
|
try {
|
|
@@ -1,12 +1,14 @@
|
|
|
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
|
|
|
4
5
|
/**
|
|
5
6
|
* The execution a checkpoint belongs to: the step attempt (`retryCount`) that
|
|
6
7
|
* claimed the row. A fenced checkpoint writes only while that attempt still
|
|
7
8
|
* owns the row: the step is `running` with that `retryCount` in a `running` or
|
|
8
|
-
* `waiting` run. A cancel, a newer attempt or
|
|
9
|
-
* meanwhile wins, and the stale execution
|
|
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.
|
|
10
12
|
*/
|
|
11
13
|
export type StepFence = { retryCount: number };
|
|
12
14
|
|
|
@@ -19,7 +21,7 @@ async function holdsFence(runId: string, stepId: string, fence: StepFence): Prom
|
|
|
19
21
|
AND r.status IN ('running', 'waiting')`,
|
|
20
22
|
[stepId, runId, fence.retryCount],
|
|
21
23
|
);
|
|
22
|
-
return row != null;
|
|
24
|
+
return row != null && (await getRunLineageBlock(runId)) == null;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -129,6 +131,7 @@ export async function checkpointStepWaiting(
|
|
|
129
131
|
options: { keepRunStatus?: boolean } = {},
|
|
130
132
|
): Promise<boolean> {
|
|
131
133
|
return getDbClient().transaction(async () => {
|
|
134
|
+
if (await getRunLineageBlock(runId)) return false;
|
|
132
135
|
const step = await getDbClient().get<{ id: string }>(
|
|
133
136
|
`UPDATE workflow_run_steps
|
|
134
137
|
SET status = 'waiting'
|
package/src/workflows/engine.ts
CHANGED
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
updateWorkflowRun,
|
|
22
22
|
updateWorkflowRunStep,
|
|
23
23
|
} from "../be/db";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
getChildWorkflowRunId,
|
|
26
|
+
getRunLineageBlock,
|
|
27
|
+
isParentStepAttemptCurrent,
|
|
28
|
+
} from "../be/db/workflow-runs";
|
|
25
29
|
import { WorkflowReplayStateError } from "../be/workflow-replay";
|
|
26
30
|
import { telemetry } from "../telemetry";
|
|
27
31
|
import type { Workflow, WorkflowDefinition, WorkflowNode, WorkflowRunStep } from "../types";
|
|
@@ -67,6 +71,13 @@ export interface WorkflowExecutionOptions {
|
|
|
67
71
|
parentStepId?: string;
|
|
68
72
|
/** The parent step's `retryCount`; each attempt gets its own child run. */
|
|
69
73
|
parentStepAttempt?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Runs inside the transaction that commits a top-level run (or reuses the
|
|
76
|
+
* day's setup-failure run), after the run row is written. A throw rolls the
|
|
77
|
+
* run back and nothing else happens: no walk, no alert, no telemetry. The
|
|
78
|
+
* HTTP trigger records its `Idempotency-Key` here.
|
|
79
|
+
*/
|
|
80
|
+
accept?: (run: { runId: string; skipped: boolean }) => Promise<void>;
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
/**
|
|
@@ -111,12 +122,16 @@ export async function startWorkflowExecution(
|
|
|
111
122
|
await getAutomationSetupStates(),
|
|
112
123
|
);
|
|
113
124
|
if (preflight.state === "needs_setup" && !options.parentStepId) {
|
|
114
|
-
const { runId, recorded } = await
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
const { runId, recorded } = await getDbClient().transaction(async () => {
|
|
126
|
+
const failure = await recordWorkflowPreflightFailure({
|
|
127
|
+
workflowId: workflow.id,
|
|
128
|
+
triggerType: options.triggerType ?? "manual",
|
|
129
|
+
triggerData,
|
|
130
|
+
failureReason: preflight.failureReason!,
|
|
131
|
+
createdBy: options.requestedByUserId,
|
|
132
|
+
});
|
|
133
|
+
await options.accept?.({ runId: failure.runId, skipped: false });
|
|
134
|
+
return failure;
|
|
120
135
|
});
|
|
121
136
|
if (recorded) await notifyAutomationPreflightFailure(preflight);
|
|
122
137
|
return runId;
|
|
@@ -276,7 +291,8 @@ async function buildInitialRunState(
|
|
|
276
291
|
* executing and returns the child that attempt already started instead of a
|
|
277
292
|
* second one; the unique (parentStepId, parentStepAttempt) index backs this
|
|
278
293
|
* across processes. A setup failure claims the day's alert in the same
|
|
279
|
-
* transaction, so concurrent failures alert exactly once.
|
|
294
|
+
* transaction, so concurrent failures alert exactly once. `options.accept`
|
|
295
|
+
* runs last in it.
|
|
280
296
|
*/
|
|
281
297
|
async function createRunWithInitialState(
|
|
282
298
|
workflow: Workflow,
|
|
@@ -314,6 +330,7 @@ async function createRunWithInitialState(
|
|
|
314
330
|
initial.status === "failed" && initial.preflight
|
|
315
331
|
? await isFirstWorkflowPreflightFailureToday(workflow.id, runId)
|
|
316
332
|
: undefined;
|
|
333
|
+
await options.accept?.({ runId, skipped: initial.status === "skipped" });
|
|
317
334
|
return { runId, created: true, preflightAlert };
|
|
318
335
|
});
|
|
319
336
|
}
|
|
@@ -606,6 +623,8 @@ async function walkGraphOwned(
|
|
|
606
623
|
await getDbClient().transaction(async () => {
|
|
607
624
|
const run = await getWorkflowRun(runId);
|
|
608
625
|
if (!run || run.status !== "running") return;
|
|
626
|
+
// Inside a cancelled tree the run's outcome is the cancel, not its steps.
|
|
627
|
+
if (await getRunLineageBlock(runId)) return;
|
|
609
628
|
|
|
610
629
|
const finalSteps = await getWorkflowRunStepsByRunId(runId);
|
|
611
630
|
// Another walker is still executing a branch; it finalizes the run.
|
|
@@ -793,6 +812,9 @@ async function executeStep(
|
|
|
793
812
|
if (!run || (run.status !== "running" && run.status !== "waiting")) {
|
|
794
813
|
return { halted: true as const };
|
|
795
814
|
}
|
|
815
|
+
// A live run under a cancelled ancestor (or with a broken parent link)
|
|
816
|
+
// starts no step: the cancel covers its whole tree.
|
|
817
|
+
if (await getRunLineageBlock(runId)) return { halted: true as const };
|
|
796
818
|
|
|
797
819
|
// Another walker already started or finished this node for the same
|
|
798
820
|
// predecessor completions (two branch completions both walking the join,
|
|
@@ -6,7 +6,7 @@ import { getSuccessors } from "../definition";
|
|
|
6
6
|
import { foreachBodyTypeError } from "../foreach-body";
|
|
7
7
|
import {
|
|
8
8
|
buildForeachAggregate,
|
|
9
|
-
|
|
9
|
+
isForeachChildSettled,
|
|
10
10
|
parseSyntheticNodeId,
|
|
11
11
|
} from "../foreach-join";
|
|
12
12
|
import { getMaxWorkflowStepsPerRun } from "../limits";
|
|
@@ -144,7 +144,7 @@ export class ForeachExecutor extends BaseExecutor<
|
|
|
144
144
|
const childSteps = (await this.deps.db.getWorkflowRunStepsByRunId(meta.runId)).filter(
|
|
145
145
|
(step) => parseSyntheticNodeId(step.nodeId)?.parentNodeId === meta.nodeId,
|
|
146
146
|
);
|
|
147
|
-
if (childSteps.every(
|
|
147
|
+
if (childSteps.every(isForeachChildSettled)) {
|
|
148
148
|
return { status: "success", output: buildForeachAggregate(childSteps) };
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
updateWorkflowRun,
|
|
12
12
|
updateWorkflowRunStep,
|
|
13
13
|
} from "../be/db";
|
|
14
|
+
import { getRunLineageBlock, getWorkflowRunStatus } from "../be/db/workflow-runs";
|
|
14
15
|
import type {
|
|
15
16
|
Workflow,
|
|
16
17
|
WorkflowDefinition,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
cancelQueuedForeachChildren,
|
|
42
43
|
FOREACH_TERMINAL_STEP_STATUSES,
|
|
43
44
|
type ForeachChildOutcome,
|
|
45
|
+
isRetryPending,
|
|
44
46
|
latestForeachChildren,
|
|
45
47
|
settleForeachChild,
|
|
46
48
|
} from "./foreach-join";
|
|
@@ -352,8 +354,8 @@ async function parkRunIfRunning(runId: string): Promise<void> {
|
|
|
352
354
|
|
|
353
355
|
/**
|
|
354
356
|
* Flip `pending` children to `running`, in item order, while fewer than the
|
|
355
|
-
* fan-out's `concurrency` cap are in flight (`running` or
|
|
356
|
-
* attempt rows only), and collect unowned `running` ones. Ownership is registered before commit, so no other
|
|
357
|
+
* fan-out's `concurrency` cap are in flight (`running`, `waiting` or queued
|
|
358
|
+
* for a retry, latest attempt rows only), and collect unowned `running` ones. Ownership is registered before commit, so no other
|
|
357
359
|
* drive can see a claimed row without also seeing it owned.
|
|
358
360
|
*/
|
|
359
361
|
async function claimChildren(
|
|
@@ -367,6 +369,10 @@ async function claimChildren(
|
|
|
367
369
|
// so concurrent drives (a settlement's drive and the recovery sweep)
|
|
368
370
|
// serialize here and never exceed the cap together.
|
|
369
371
|
await getDbClient().transaction(async () => {
|
|
372
|
+
// A cancel of the run (or of an ancestor) that committed first wins.
|
|
373
|
+
const status = await getWorkflowRunStatus(runId);
|
|
374
|
+
if (status !== "running" && status !== "waiting") return;
|
|
375
|
+
if (await getRunLineageBlock(runId)) return;
|
|
370
376
|
const children = latestForeachChildren(
|
|
371
377
|
def,
|
|
372
378
|
parentNode.id,
|
|
@@ -379,7 +385,8 @@ async function claimChildren(
|
|
|
379
385
|
}
|
|
380
386
|
}
|
|
381
387
|
const inFlight = children.filter(
|
|
382
|
-
(child) =>
|
|
388
|
+
(child) =>
|
|
389
|
+
child.status === "running" || child.status === "waiting" || isRetryPending(child),
|
|
383
390
|
).length;
|
|
384
391
|
let free = foreachConcurrencyCap(parentNode) - inFlight;
|
|
385
392
|
for (const child of children) {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
updateWorkflowRun,
|
|
8
8
|
updateWorkflowRunStep,
|
|
9
9
|
} from "../be/db";
|
|
10
|
+
import { getRunLineageBlock } from "../be/db/workflow-runs";
|
|
10
11
|
import type {
|
|
11
12
|
WorkflowDefinition,
|
|
12
13
|
WorkflowNode,
|
|
@@ -27,6 +28,22 @@ export const FOREACH_TERMINAL_STEP_STATUSES = new Set([
|
|
|
27
28
|
"skipped",
|
|
28
29
|
]);
|
|
29
30
|
|
|
31
|
+
/**
|
|
32
|
+
* A failed step queued for the retry poller (`nextRetryAt` set). A foreach
|
|
33
|
+
* child in this state is still in flight: it is not settled and holds its
|
|
34
|
+
* concurrency slot until the poller re-queues it.
|
|
35
|
+
*/
|
|
36
|
+
export function isRetryPending(step: Pick<WorkflowRunStep, "status" | "nextRetryAt">): boolean {
|
|
37
|
+
return step.status === "failed" && step.nextRetryAt != null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A foreach child whose outcome is final: terminal and not queued for a retry. */
|
|
41
|
+
export function isForeachChildSettled(
|
|
42
|
+
step: Pick<WorkflowRunStep, "status" | "nextRetryAt">,
|
|
43
|
+
): boolean {
|
|
44
|
+
return FOREACH_TERMINAL_STEP_STATUSES.has(step.status) && !isRetryPending(step);
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
export interface SyntheticNodeId {
|
|
31
48
|
parentNodeId: string;
|
|
32
49
|
itemKey: string;
|
|
@@ -74,8 +91,9 @@ export async function joinForeach(
|
|
|
74
91
|
|
|
75
92
|
const children = latestForeachChildren(def, parent.id, await getWorkflowRunStepsByRunId(runId));
|
|
76
93
|
|
|
77
|
-
// `pending` (queued, not started)
|
|
78
|
-
|
|
94
|
+
// `pending` (queued, not started) and a child queued for a retry are
|
|
95
|
+
// non-terminal like running and waiting.
|
|
96
|
+
if (children.some((step) => !isForeachChildSettled(step))) {
|
|
79
97
|
return { joined: false, parentNodeId: parent.id, successors: [] };
|
|
80
98
|
}
|
|
81
99
|
|
|
@@ -244,6 +262,8 @@ export async function settleForeachChild(
|
|
|
244
262
|
});
|
|
245
263
|
|
|
246
264
|
if (run.status !== "running" && run.status !== "waiting") return unclaimed;
|
|
265
|
+
// The outcome stays on the item as history; a cancelled tree routes nothing.
|
|
266
|
+
if (await getRunLineageBlock(runId)) return unclaimed;
|
|
247
267
|
|
|
248
268
|
if (outcome.status !== "completed" && effectiveFailurePolicy(parent, def) === "fail-fast") {
|
|
249
269
|
const reason = options.runError ?? outcome.error ?? `foreach item ${current.nodeId} failed`;
|
|
@@ -270,8 +290,9 @@ export async function settleForeachChild(
|
|
|
270
290
|
|
|
271
291
|
/**
|
|
272
292
|
* Fail-fast: cancel every queued (`pending`) child of the fan-out so none of
|
|
273
|
-
* them starts
|
|
274
|
-
*
|
|
293
|
+
* them starts, and every child queued for a retry so the retry poller does not
|
|
294
|
+
* revive the failed run. In-flight children are left to finish and record their
|
|
295
|
+
* outcome. A run retry gives each cancelled item a new attempt.
|
|
275
296
|
*/
|
|
276
297
|
export async function cancelQueuedForeachChildren(
|
|
277
298
|
def: WorkflowDefinition,
|
|
@@ -294,9 +315,11 @@ export async function cancelQueuedForeachChildren(
|
|
|
294
315
|
await getWorkflowRunStepsByRunId(runId),
|
|
295
316
|
);
|
|
296
317
|
for (const child of children) {
|
|
297
|
-
if (child.status !== "pending") continue;
|
|
318
|
+
if (child.status !== "pending" && !isRetryPending(child)) continue;
|
|
298
319
|
const row = await getDbClient().get<{ id: string }>(
|
|
299
|
-
|
|
320
|
+
`UPDATE workflow_run_steps SET status = 'cancelled', error = ?, finishedAt = ?, nextRetryAt = NULL
|
|
321
|
+
WHERE id = ? AND (status = 'pending' OR (status = 'failed' AND nextRetryAt IS NOT NULL))
|
|
322
|
+
RETURNING id`,
|
|
300
323
|
[error, finishedAt, child.id],
|
|
301
324
|
);
|
|
302
325
|
if (row) cancelled++;
|
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
resolveApprovalRequest,
|
|
13
13
|
updateWorkflowRun,
|
|
14
14
|
} from "../be/db";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
getRunLineageBlock,
|
|
17
|
+
getRunningSubWorkflowSteps,
|
|
18
|
+
getSettledChildRunParentSteps,
|
|
19
|
+
} from "../be/db/workflow-runs";
|
|
16
20
|
import type { WorkflowRunStep } from "../types";
|
|
17
21
|
import { shapeApprovalResolution } from "./approval-resolution";
|
|
18
22
|
import { loadCompletedStepRouting } from "./completed-step-routing";
|
|
@@ -227,6 +231,7 @@ async function completeIfSettled(runId: string, ctx: Record<string, unknown>): P
|
|
|
227
231
|
return getDbClient().transaction(async () => {
|
|
228
232
|
const run = await getWorkflowRun(runId);
|
|
229
233
|
if (run?.status !== "running") return false;
|
|
234
|
+
if (await getRunLineageBlock(runId)) return false;
|
|
230
235
|
const steps = await getWorkflowRunStepsByRunId(runId);
|
|
231
236
|
if (retryPendingNodeIds(steps).size > 0) return false;
|
|
232
237
|
const latest = new Map<string, WorkflowRunStep>();
|