@desplega.ai/agent-swarm 2.5.2 → 2.5.3
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-gr2fcyyj.js} +3 -3
- package/dist/{actions-6xx079d1.js → actions-wv766ghf.js} +18 -18
- package/dist/{amp-adapter-8n6f0hz8.js → amp-adapter-speb906t.js} +3 -3
- package/dist/{app-ft6r6v23.js → app-ag29n14f.js} +3 -3
- package/dist/{assistant-vt2cyfzd.js → assistant-3x8bz3eg.js} +19 -19
- package/dist/{boot-reembed-r3e808ky.js → boot-reembed-5b3d2eea.js} +1 -1
- package/dist/{boot-reembed-tgrfy27t.js → boot-reembed-jsmfrpzf.js} +1 -1
- package/dist/{boot-scrub-sweep-masdwxkm.js → boot-scrub-sweep-s73gerxy.js} +2 -2
- package/dist/{boot-seeds-0n82qc7q.js → boot-seeds-5zmax2wr.js} +4 -4
- package/dist/{claude-adapter-s7j47k06.js → claude-adapter-hswdnm3t.js} +3 -3
- package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-cvbzj1h6.js} +2 -2
- package/dist/{claude-managed-setup-3hzrgrnk.js → claude-managed-setup-3k8m3hhc.js} +2 -2
- package/dist/{claude-sdk-session-0zrdzyx2.js → claude-sdk-session-fkbyk66x.js} +2 -2
- package/dist/{cli-d172svcw.js → cli-1fc85mvt.js} +4 -4
- package/dist/{cli-3e9vs93g.js → cli-210xd1f4.js} +4 -4
- package/dist/{cli-vaxw3bg0.js → cli-25j01dpj.js} +3 -3
- package/dist/{cli-n6kmqngw.js → cli-35gtk1p6.js} +1 -1
- package/dist/{cli-fgn7yg5r.js → cli-3ngdk5te.js} +1 -1
- package/dist/{cli-7ccf7akh.js → cli-40jsnrjm.js} +2 -2
- package/dist/{cli-b1tv22wr.js → cli-502eqv93.js} +1 -1
- package/dist/{cli-ydg6ep51.js → cli-6n8axjv1.js} +1 -1
- package/dist/{cli-f55b6ky0.js → cli-8s5wx3nz.js} +2 -2
- package/dist/{cli-86zpfje5.js → cli-99xgw26r.js} +1 -1
- package/dist/{cli-xw9q173h.js → cli-9s05422w.js} +1 -1
- package/dist/{cli-h5d22pfs.js → cli-aecdcr77.js} +119 -282
- package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
- package/dist/{cli-15xd0j7w.js → cli-ce0ak9ms.js} +11 -11
- package/dist/{cli-y3dzwyy3.js → cli-e2ar4bp1.js} +71 -20
- package/dist/{cli-b6tnjdvx.js → cli-e8jyq7d8.js} +1 -1
- package/dist/{cli-6y43v79h.js → cli-egjggehm.js} +1 -1
- package/dist/{cli-cf63dnxj.js → cli-ewm3pwbq.js} +2 -2
- package/dist/{cli-bpbcpd8b.js → cli-fsdprwrg.js} +1 -1
- package/dist/{cli-qrjv7t18.js → cli-fzcn3d87.js} +1 -1
- package/dist/{cli-14v1sxxy.js → cli-gpsz7g88.js} +6 -6
- package/dist/{cli-wadzapwb.js → cli-hcpj2xqz.js} +1 -1
- package/dist/{cli-jmcjbjkt.js → cli-hhxjnjzs.js} +1 -1
- package/dist/{cli-j3dwd4kz.js → cli-jjznk3f7.js} +718 -108
- package/dist/{cli-csh8fnbg.js → cli-kxawjrxg.js} +147 -28
- package/dist/{cli-mntxtkhn.js → cli-mz9afxxf.js} +2 -1
- package/dist/{cli-0vgy5wfw.js → cli-n45mr7zh.js} +2 -2
- package/dist/{cli-aaqnqpyw.js → cli-p0rzjkya.js} +2 -2
- package/dist/{cli-3qv8naq8.js → cli-p6zs9h4y.js} +827 -107
- package/dist/{cli-e91vshb1.js → cli-p9rafdrb.js} +4 -4
- package/dist/{cli-edtm741g.js → cli-qgf8emrn.js} +5 -5
- package/dist/{cli-ha22h0eg.js → cli-qzme5949.js} +26 -26
- package/dist/{cli-xzge46sj.js → cli-s49hpn37.js} +2 -2
- package/dist/{cli-b63d08rs.js → cli-s53vp709.js} +1 -1
- package/dist/{cli-cyfgxnvb.js → cli-svrj71pg.js} +18 -16
- package/dist/{cli-4qkg9968.js → cli-vwn81jbs.js} +3 -3
- package/dist/{cli-f3pfn4x3.js → cli-w1zwctj2.js} +1 -1
- package/dist/{cli-kcnwn85g.js → cli-y561h5a9.js} +4 -4
- package/dist/{cli-n2918sed.js → cli-z21whrrg.js} +15 -15
- package/dist/cli.js +16 -16
- package/dist/{codex-adapter-brh2tcxr.js → codex-adapter-n8ka66vf.js} +2 -2
- package/dist/{codex-session-runner-0dnj21qs.js → codex-session-runner-fsebts37.js} +1 -1
- package/dist/{commands-1cnvseqx.js → commands-t04rt2pc.js} +2 -2
- package/dist/{controller-kc7s9xhn.js → controller-rgna04r2.js} +2 -2
- package/dist/{controllers-connect-jzx7t4bn.js → controllers-connect-bqm1rtft.js} +20 -20
- package/dist/{cursor-adapter-qpwp5sfg.js → cursor-adapter-swr2k95y.js} +1 -1
- package/dist/{db-v5ytg5pw.js → db-24ck09xy.js} +1 -1
- package/dist/{db-4vq88jkc.js → db-a4m7haw0.js} +3 -3
- package/dist/{dispatcher-trtcrqae.js → dispatcher-nwf9ss0q.js} +17 -17
- package/dist/{dsh-adapter-f20xwn1z.js → dsh-adapter-kpdnhjst.js} +2 -2
- package/dist/{external-channel-n9bymc2n.js → external-channel-j1e2p34p.js} +2 -2
- package/dist/{grok-adapter-4kpmmnr6.js → grok-adapter-qtjz0whk.js} +4 -4
- package/dist/{handlers-ptsraqa8.js → handlers-19xr6dnz.js} +20 -20
- package/dist/{heartbeat-60qtxtjf.js → heartbeat-vtesda5d.js} +20 -20
- package/dist/{hook-rbktcstg.js → hook-8wqaszda.js} +1 -1
- package/dist/{hook-ngtvh729.js → hook-b2hhj6rd.js} +1 -1
- package/dist/{http-z8xy72gp.js → http-mxcqhhcs.js} +2757 -2448
- package/dist/{index-a9nsavhx.js → index-1r8wfpzs.js} +1 -1
- package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
- package/dist/{index-s193j5fh.js → index-kw2mmat7.js} +20 -20
- package/dist/{index-06qkmcx6.js → index-n1vbvta9.js} +17 -9
- package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
- package/dist/{index-1cscynkb.js → index-spwxjgay.js} +17 -17
- package/dist/{index-r70q0xjx.js → index-srg522m6.js} +17 -17
- package/dist/{keepalive-a9srcphj.js → keepalive-dsd8rtgx.js} +3 -3
- package/dist/{lead-3devneqy.js → lead-zyerk1x2.js} +30 -30
- package/dist/{maintenance-0er16h2y.js → maintenance-jqmqwnet.js} +1 -1
- package/dist/{oauth-refresh-sweep-b65jcyw2.js → oauth-refresh-sweep-q2c3bnvm.js} +2 -2
- package/dist/{onboard-wqywz5py.js → onboard-w4yd7s8t.js} +2 -2
- package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-9tppvxar.js} +2 -2
- package/dist/{otel-impl-vk32s884.js → otel-impl-f36byzbd.js} +1 -1
- package/dist/{pricing-refresh-t27anv4z.js → pricing-refresh-xvak5p4v.js} +3 -3
- package/dist/{rbac-roles-bmcgsxjn.js → rbac-roles-682k2mt5.js} +1 -1
- package/dist/{rbac-roles-452665y0.js → rbac-roles-gfn30fb8.js} +2 -2
- package/dist/{render-v2-0ttsk2j4.js → render-v2-hgytz8nn.js} +3 -3
- package/dist/{resume-a26kvr41.js → resume-2dvq348g.js} +7 -5
- package/dist/{session-access-dgh9nkwk.js → session-access-7f21ekq9.js} +4 -4
- package/dist/{setup-9vc9qahm.js → setup-epffxtbk.js} +1 -1
- package/dist/{work-objects-vn8nkxwn.js → work-objects-xz1a5rqk.js} +20 -20
- package/dist/{worker-1x1h8vnm.js → worker-42t8emjt.js} +30 -30
- package/openapi.json +3045 -2456
- package/package.json +4 -4
- package/src/be/approval-config-question.ts +389 -0
- package/src/be/automation-preflight.ts +37 -3
- 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/workflow-runs.ts +54 -7
- package/src/be/db.ts +25 -5
- 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/swarm-config-guard.ts +23 -0
- package/src/extensions/catalog.generated.json +4 -4
- package/src/http/approval-requests.ts +209 -38
- package/src/http/config.ts +22 -37
- package/src/http/sessions.ts +27 -2
- package/src/integrations/chat-sdk/inbound.ts +5 -4
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/tests/approval-config-question.test.ts +702 -0
- package/src/tests/chat-sdk-inbound.test.ts +20 -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/list-endpoint-slimming.test.ts +33 -0
- package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
- package/src/tests/script-connections.test.ts +6 -0
- package/src/tests/sessions-tree.test.ts +168 -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-sub-workflow.test.ts +538 -1
- package/src/tools/request-human-input.ts +27 -5
- package/src/types.ts +7 -0
- package/src/workflows/approval-notifications.ts +10 -3
- package/src/workflows/checkpoint.ts +69 -35
- package/src/workflows/engine.ts +234 -87
- package/src/workflows/executors/base.ts +6 -0
- package/src/workflows/executors/human-in-the-loop.ts +25 -1
- package/src/workflows/executors/sub-workflow.ts +45 -16
- package/src/workflows/recovery.ts +63 -1
- package/src/workflows/resume.ts +60 -21
- package/src/workflows/retry-poller.ts +25 -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/turso-interaction/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +8 -1
- /package/dist/{cli-stxdrqs2.js → cli-avdvgcay.js} +0 -0
- /package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-tqsge1ay.js} +0 -0
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
import { getDbClient, getWorkflowRun, updateWorkflowRun, updateWorkflowRunStep } from "../be/db";
|
|
2
2
|
import type { RetryPolicy } from "../types";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The execution a checkpoint belongs to: the step attempt (`retryCount`) that
|
|
6
|
+
* claimed the row. A fenced checkpoint writes only while that attempt still
|
|
7
|
+
* owns the row: the step is `running` with that `retryCount` in a `running` or
|
|
8
|
+
* `waiting` run. A cancel, a newer attempt or another settle that committed
|
|
9
|
+
* meanwhile wins, and the stale execution writes nothing.
|
|
10
|
+
*/
|
|
11
|
+
export type StepFence = { retryCount: number };
|
|
12
|
+
|
|
13
|
+
async function holdsFence(runId: string, stepId: string, fence: StepFence): Promise<boolean> {
|
|
14
|
+
const row = await getDbClient().get<{ found: number }>(
|
|
15
|
+
`SELECT 1 AS found
|
|
16
|
+
FROM workflow_run_steps s
|
|
17
|
+
JOIN workflow_runs r ON r.id = s.runId
|
|
18
|
+
WHERE s.id = ? AND s.runId = ? AND s.status = 'running' AND s.retryCount = ?
|
|
19
|
+
AND r.status IN ('running', 'waiting')`,
|
|
20
|
+
[stepId, runId, fence.retryCount],
|
|
21
|
+
);
|
|
22
|
+
return row != null;
|
|
23
|
+
}
|
|
24
|
+
|
|
4
25
|
/**
|
|
5
26
|
* Checkpoint a successful step — atomic DB write of step result + run context.
|
|
27
|
+
* Returns false, writing nothing, when `fence` no longer holds.
|
|
6
28
|
*/
|
|
7
29
|
export async function checkpointStep(
|
|
8
30
|
runId: string,
|
|
@@ -10,8 +32,10 @@ export async function checkpointStep(
|
|
|
10
32
|
nodeId: string,
|
|
11
33
|
result: { output?: unknown; nextPort?: string },
|
|
12
34
|
ctx: Record<string, unknown>,
|
|
13
|
-
|
|
14
|
-
|
|
35
|
+
options: { fence?: StepFence } = {},
|
|
36
|
+
): Promise<boolean> {
|
|
37
|
+
return getDbClient().transaction(async () => {
|
|
38
|
+
if (options.fence && !(await holdsFence(runId, stepId, options.fence))) return false;
|
|
15
39
|
await updateWorkflowRunStep(stepId, {
|
|
16
40
|
status: "completed",
|
|
17
41
|
output: result.output,
|
|
@@ -24,11 +48,14 @@ export async function checkpointStep(
|
|
|
24
48
|
await updateWorkflowRun(runId, {
|
|
25
49
|
context: ctx,
|
|
26
50
|
});
|
|
51
|
+
return true;
|
|
27
52
|
});
|
|
28
53
|
}
|
|
29
54
|
|
|
30
55
|
/**
|
|
31
56
|
* Checkpoint a step failure — marks step failed, calculates retry if applicable.
|
|
57
|
+
* With `fence`, a stale execution writes nothing and gets `stale: true`.
|
|
58
|
+
* `output` is kept on the failed step for observability, in the same write.
|
|
32
59
|
*/
|
|
33
60
|
export async function checkpointStepFailure(
|
|
34
61
|
runId: string,
|
|
@@ -36,49 +63,56 @@ export async function checkpointStepFailure(
|
|
|
36
63
|
error: string,
|
|
37
64
|
retryCount: number,
|
|
38
65
|
retryPolicy?: RetryPolicy,
|
|
39
|
-
options?: { markRunFailed?: boolean },
|
|
40
|
-
): Promise<{ shouldRetry: boolean }> {
|
|
66
|
+
options?: { markRunFailed?: boolean; fence?: StepFence; output?: unknown },
|
|
67
|
+
): Promise<{ shouldRetry: boolean; stale?: true }> {
|
|
41
68
|
const now = new Date().toISOString();
|
|
42
69
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
70
|
+
return getDbClient().transaction(async () => {
|
|
71
|
+
if (options?.fence && !(await holdsFence(runId, stepId, options.fence))) {
|
|
72
|
+
return { shouldRetry: false, stale: true as const };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (retryPolicy && retryCount < retryPolicy.maxRetries) {
|
|
76
|
+
const delay = calculateBackoff(retryPolicy, retryCount);
|
|
77
|
+
const nextRetryAt = new Date(Date.now() + delay).toISOString();
|
|
78
|
+
|
|
79
|
+
await updateWorkflowRunStep(stepId, {
|
|
80
|
+
status: "failed",
|
|
81
|
+
error,
|
|
82
|
+
output: options?.output,
|
|
83
|
+
retryCount: retryCount + 1,
|
|
84
|
+
maxRetries: retryPolicy.maxRetries,
|
|
85
|
+
nextRetryAt,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return { shouldRetry: true };
|
|
89
|
+
}
|
|
46
90
|
|
|
91
|
+
// No retries left — mark step failed, and optionally the run too
|
|
92
|
+
// Clear nextRetryAt so the poller stops picking this step up
|
|
47
93
|
await updateWorkflowRunStep(stepId, {
|
|
48
94
|
status: "failed",
|
|
49
95
|
error,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
nextRetryAt,
|
|
96
|
+
output: options?.output,
|
|
97
|
+
finishedAt: now,
|
|
98
|
+
nextRetryAt: null,
|
|
53
99
|
});
|
|
54
100
|
|
|
55
|
-
|
|
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 () => {
|
|
101
|
+
const markRunFailed = options?.markRunFailed ?? true;
|
|
102
|
+
if (markRunFailed) {
|
|
103
|
+
// A cancel or finalization that committed while the step ran wins.
|
|
71
104
|
const run = await getWorkflowRun(runId);
|
|
72
|
-
if (run?.status
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
105
|
+
if (run?.status === "running" || run?.status === "waiting") {
|
|
106
|
+
await updateWorkflowRun(runId, {
|
|
107
|
+
status: "failed",
|
|
108
|
+
error: `Step failed: ${error}`,
|
|
109
|
+
finishedAt: now,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
80
113
|
|
|
81
|
-
|
|
114
|
+
return { shouldRetry: false };
|
|
115
|
+
});
|
|
82
116
|
}
|
|
83
117
|
|
|
84
118
|
/**
|
package/src/workflows/engine.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { notifyAutomationPreflightFailure } from "../automation-preflight-alert";
|
|
2
2
|
import {
|
|
3
3
|
getAutomationSetupStates,
|
|
4
|
+
isFirstWorkflowPreflightFailureToday,
|
|
4
5
|
preflightAutomation,
|
|
5
6
|
recordWorkflowPreflightFailure,
|
|
6
7
|
renderAutomationTokens,
|
|
@@ -20,10 +21,16 @@ import {
|
|
|
20
21
|
updateWorkflowRun,
|
|
21
22
|
updateWorkflowRunStep,
|
|
22
23
|
} from "../be/db";
|
|
24
|
+
import { getChildWorkflowRunId, isParentStepAttemptCurrent } from "../be/db/workflow-runs";
|
|
23
25
|
import { WorkflowReplayStateError } from "../be/workflow-replay";
|
|
24
26
|
import { telemetry } from "../telemetry";
|
|
25
27
|
import type { Workflow, WorkflowDefinition, WorkflowNode, WorkflowRunStep } from "../types";
|
|
26
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
checkpointStep,
|
|
30
|
+
checkpointStepFailure,
|
|
31
|
+
checkpointStepWaiting,
|
|
32
|
+
type StepFence,
|
|
33
|
+
} from "./checkpoint";
|
|
27
34
|
import { loadCompletedStepRouting } from "./completed-step-routing";
|
|
28
35
|
import { FOREACH_ITEMS_WAIT_FOR } from "./constants";
|
|
29
36
|
import { shouldSkipCooldown } from "./cooldown";
|
|
@@ -58,6 +65,8 @@ export interface WorkflowExecutionOptions {
|
|
|
58
65
|
triggerType?: "schedule" | "manual" | "event" | "api";
|
|
59
66
|
/** Set when a `sub-workflow` step starts this run as its child. */
|
|
60
67
|
parentStepId?: string;
|
|
68
|
+
/** The parent step's `retryCount`; each attempt gets its own child run. */
|
|
69
|
+
parentStepAttempt?: number;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
/**
|
|
@@ -87,11 +96,9 @@ async function resolveRenderedWorkflowInputs(
|
|
|
87
96
|
/**
|
|
88
97
|
* Start executing a workflow from scratch.
|
|
89
98
|
*
|
|
90
|
-
* 1. Check cooldown
|
|
91
|
-
* 2. Create workflow run
|
|
92
|
-
* 3.
|
|
93
|
-
* 4. Find entry nodes
|
|
94
|
-
* 5. Walk the graph
|
|
99
|
+
* 1. Check cooldown, readiness and inputs
|
|
100
|
+
* 2. Create the workflow run with its initial state (one transaction)
|
|
101
|
+
* 3. Walk the graph from the entry nodes
|
|
95
102
|
*/
|
|
96
103
|
export async function startWorkflowExecution(
|
|
97
104
|
workflow: Workflow,
|
|
@@ -103,7 +110,7 @@ export async function startWorkflowExecution(
|
|
|
103
110
|
workflowPreflightInput(workflow),
|
|
104
111
|
await getAutomationSetupStates(),
|
|
105
112
|
);
|
|
106
|
-
if (preflight.state === "needs_setup") {
|
|
113
|
+
if (preflight.state === "needs_setup" && !options.parentStepId) {
|
|
107
114
|
const { runId, recorded } = await recordWorkflowPreflightFailure({
|
|
108
115
|
workflowId: workflow.id,
|
|
109
116
|
triggerType: options.triggerType ?? "manual",
|
|
@@ -115,63 +122,126 @@ export async function startWorkflowExecution(
|
|
|
115
122
|
return runId;
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
125
|
+
// Settle everything the run starts from before its row exists, so the row
|
|
126
|
+
// commits together with its initial context or terminal status. A crash
|
|
127
|
+
// right after the commit leaves a run that recovery can walk, never a
|
|
128
|
+
// `running` row with an empty context.
|
|
129
|
+
let initial: InitialRunState;
|
|
130
|
+
if (preflight.state === "needs_setup") {
|
|
131
|
+
// A child keeps its parent link and attempt even when it cannot start:
|
|
132
|
+
// the day-deduped top-level failure run is shared and unlinked, so a
|
|
133
|
+
// retry or recovery of the step would never find it.
|
|
134
|
+
initial = {
|
|
135
|
+
status: "failed",
|
|
136
|
+
error: preflight.failureReason!,
|
|
137
|
+
finishedAt: new Date().toISOString(),
|
|
138
|
+
preflight: true,
|
|
139
|
+
};
|
|
140
|
+
} else {
|
|
141
|
+
// Templates can consume install params outside the graph definition (most
|
|
142
|
+
// importantly workflow.input). Render the complete runtime snapshot once;
|
|
143
|
+
// exact-token values retain their JSON type, including COMPETITORS arrays.
|
|
144
|
+
workflow = renderAutomationTokens(workflow, workflow.params ?? {});
|
|
145
|
+
|
|
146
|
+
// Validate trigger data against triggerSchema (before any DB writes)
|
|
147
|
+
if (workflow.triggerSchema) {
|
|
148
|
+
const validationErrors = validateJsonSchema(workflow.triggerSchema, triggerData);
|
|
149
|
+
if (validationErrors.length > 0) {
|
|
150
|
+
throw new TriggerSchemaError(validationErrors);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
122
153
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
throw new TriggerSchemaError(validationErrors);
|
|
154
|
+
if (workflow.cooldown && (await shouldSkipCooldown(workflow.id, workflow.cooldown))) {
|
|
155
|
+
initial = { status: "skipped", error: "cooldown", finishedAt: new Date().toISOString() };
|
|
156
|
+
} else {
|
|
157
|
+
initial = await buildInitialRunState(workflow, triggerData, registry, options);
|
|
128
158
|
}
|
|
129
159
|
}
|
|
130
160
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
161
|
+
// Held from before the commit until the walk holds the run itself, so a
|
|
162
|
+
// recovery sweep never walks the new run alongside it.
|
|
163
|
+
const newRunId = crypto.randomUUID();
|
|
164
|
+
const release = holdWorkflowRun(newRunId);
|
|
165
|
+
try {
|
|
166
|
+
const { runId, created, preflightAlert } = await createRunWithInitialState(
|
|
167
|
+
workflow,
|
|
168
|
+
triggerData,
|
|
169
|
+
options,
|
|
170
|
+
initial,
|
|
171
|
+
newRunId,
|
|
172
|
+
);
|
|
173
|
+
if (!created) return runId;
|
|
174
|
+
if (initial.status === "failed" && initial.preflight) {
|
|
175
|
+
if (preflightAlert) await notifyAutomationPreflightFailure(preflight);
|
|
176
|
+
return runId;
|
|
177
|
+
}
|
|
178
|
+
if (initial.status === "skipped") return runId;
|
|
179
|
+
telemetry.workflow("started", {
|
|
136
180
|
workflowId: workflow.id,
|
|
181
|
+
nodeCount: workflow.definition.nodes.length,
|
|
137
182
|
triggerType: options.triggerType ?? "manual",
|
|
138
|
-
triggerData,
|
|
139
|
-
createdBy: options.requestedByUserId,
|
|
140
|
-
parentStepId: options.parentStepId,
|
|
141
|
-
});
|
|
142
|
-
await updateWorkflowRun(runId, {
|
|
143
|
-
status: "skipped",
|
|
144
|
-
error: "cooldown",
|
|
145
|
-
finishedAt: new Date().toISOString(),
|
|
146
183
|
});
|
|
184
|
+
if (initial.status) return runId;
|
|
185
|
+
|
|
186
|
+
const walk = walkGraph(
|
|
187
|
+
workflow.definition,
|
|
188
|
+
runId,
|
|
189
|
+
initial.context,
|
|
190
|
+
findEntryNodes(workflow.definition),
|
|
191
|
+
registry,
|
|
192
|
+
workflow.id,
|
|
193
|
+
getSecretInputKeys(workflow.input),
|
|
194
|
+
options,
|
|
195
|
+
);
|
|
196
|
+
// A child run walks on its own, outside the parent executor's timeout. Its
|
|
197
|
+
// terminal status wakes the parent step (`resumeFromChildRun`).
|
|
198
|
+
if (options.parentStepId) {
|
|
199
|
+
walk.catch((err) => console.error(`[workflows] Child workflow run ${runId} failed:`, err));
|
|
200
|
+
return runId;
|
|
201
|
+
}
|
|
202
|
+
await walk;
|
|
147
203
|
return runId;
|
|
204
|
+
} finally {
|
|
205
|
+
release();
|
|
148
206
|
}
|
|
207
|
+
}
|
|
149
208
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
triggerType: options.triggerType ?? "manual",
|
|
163
|
-
});
|
|
209
|
+
/**
|
|
210
|
+
* Thrown when a `sub-workflow` step asks for a child run while the step's
|
|
211
|
+
* attempt no longer executes (cancelled, re-attempted or already parked).
|
|
212
|
+
*/
|
|
213
|
+
export class ParentStepNotCurrentError extends Error {
|
|
214
|
+
constructor(parentStepId: string, attempt: number) {
|
|
215
|
+
super(
|
|
216
|
+
`Parent step ${parentStepId} (attempt ${attempt}) is no longer running; no child run started`,
|
|
217
|
+
);
|
|
218
|
+
this.name = "ParentStepNotCurrentError";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
164
221
|
|
|
222
|
+
/**
|
|
223
|
+
* A new run's first committed state: a terminal status, or the context it
|
|
224
|
+
* walks from. `preflight` marks a setup failure, which alerts once a day.
|
|
225
|
+
*/
|
|
226
|
+
type InitialRunState =
|
|
227
|
+
| { status: "skipped" | "failed"; error: string; finishedAt: string; preflight?: true }
|
|
228
|
+
| { status?: undefined; context: Record<string, unknown> };
|
|
229
|
+
|
|
230
|
+
async function buildInitialRunState(
|
|
231
|
+
workflow: Workflow,
|
|
232
|
+
triggerData: unknown,
|
|
233
|
+
registry: ExecutorRegistry,
|
|
234
|
+
options: WorkflowExecutionOptions,
|
|
235
|
+
): Promise<InitialRunState> {
|
|
165
236
|
// An executor that cannot run (for example a system-one-decision node with no API key) fails
|
|
166
237
|
// the run here, before any node has side effects, instead of partway through.
|
|
167
238
|
const notReady = await findWorkflowReadinessProblems(workflow.definition, registry);
|
|
168
239
|
if (notReady.length > 0) {
|
|
169
|
-
|
|
240
|
+
return {
|
|
170
241
|
status: "failed",
|
|
171
242
|
error: formatReadinessRunError(notReady),
|
|
172
243
|
finishedAt: new Date().toISOString(),
|
|
173
|
-
}
|
|
174
|
-
return runId;
|
|
244
|
+
};
|
|
175
245
|
}
|
|
176
246
|
|
|
177
247
|
// Resolve inputs and merge into initial context
|
|
@@ -190,35 +260,62 @@ export async function startWorkflowExecution(
|
|
|
190
260
|
const resolved = await resolveRenderedWorkflowInputs(workflow.input);
|
|
191
261
|
Object.assign(ctx, { input: resolved });
|
|
192
262
|
} catch (err) {
|
|
193
|
-
|
|
263
|
+
return {
|
|
194
264
|
status: "failed",
|
|
195
265
|
error: `Input resolution failed: ${err}`,
|
|
196
266
|
finishedAt: new Date().toISOString(),
|
|
197
|
-
}
|
|
198
|
-
return runId;
|
|
267
|
+
};
|
|
199
268
|
}
|
|
200
269
|
}
|
|
270
|
+
return { context: ctx };
|
|
271
|
+
}
|
|
201
272
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Insert the run with its initial state in one transaction. For a child run
|
|
275
|
+
* the same transaction checks that the parent step's attempt is still
|
|
276
|
+
* executing and returns the child that attempt already started instead of a
|
|
277
|
+
* second one; the unique (parentStepId, parentStepAttempt) index backs this
|
|
278
|
+
* across processes. A setup failure claims the day's alert in the same
|
|
279
|
+
* transaction, so concurrent failures alert exactly once.
|
|
280
|
+
*/
|
|
281
|
+
async function createRunWithInitialState(
|
|
282
|
+
workflow: Workflow,
|
|
283
|
+
triggerData: unknown,
|
|
284
|
+
options: WorkflowExecutionOptions,
|
|
285
|
+
initial: InitialRunState,
|
|
286
|
+
runId: string,
|
|
287
|
+
): Promise<{ runId: string; created: boolean; preflightAlert?: boolean }> {
|
|
288
|
+
return getDbClient().transaction(async () => {
|
|
289
|
+
const { parentStepId } = options;
|
|
290
|
+
const attempt = options.parentStepAttempt ?? 0;
|
|
291
|
+
if (parentStepId) {
|
|
292
|
+
if (!(await isParentStepAttemptCurrent(parentStepId, attempt))) {
|
|
293
|
+
throw new ParentStepNotCurrentError(parentStepId, attempt);
|
|
294
|
+
}
|
|
295
|
+
const existingId = await getChildWorkflowRunId(parentStepId, attempt);
|
|
296
|
+
if (existingId) return { runId: existingId, created: false };
|
|
297
|
+
}
|
|
298
|
+
await createWorkflowRun({
|
|
299
|
+
id: runId,
|
|
300
|
+
workflowId: workflow.id,
|
|
301
|
+
triggerType: options.triggerType ?? "manual",
|
|
302
|
+
triggerData,
|
|
303
|
+
createdBy: options.requestedByUserId,
|
|
304
|
+
parentStepId,
|
|
305
|
+
parentStepAttempt: parentStepId ? attempt : undefined,
|
|
306
|
+
});
|
|
307
|
+
await updateWorkflowRun(
|
|
308
|
+
runId,
|
|
309
|
+
initial.status
|
|
310
|
+
? { status: initial.status, error: initial.error, finishedAt: initial.finishedAt }
|
|
311
|
+
: { context: initial.context },
|
|
312
|
+
);
|
|
313
|
+
const preflightAlert =
|
|
314
|
+
initial.status === "failed" && initial.preflight
|
|
315
|
+
? await isFirstWorkflowPreflightFailureToday(workflow.id, runId)
|
|
316
|
+
: undefined;
|
|
317
|
+
return { runId, created: true, preflightAlert };
|
|
318
|
+
});
|
|
222
319
|
}
|
|
223
320
|
|
|
224
321
|
// ─── Graph Walker ──────────────────────────────────────────
|
|
@@ -270,6 +367,11 @@ export function isWorkflowRunActive(runId: string): boolean {
|
|
|
270
367
|
return activeWalks.has(runId);
|
|
271
368
|
}
|
|
272
369
|
|
|
370
|
+
/** True while an executeStep call in this process owns the step row. */
|
|
371
|
+
export function isStepExecuting(stepId: string): boolean {
|
|
372
|
+
return executingSteps.has(stepId);
|
|
373
|
+
}
|
|
374
|
+
|
|
273
375
|
/**
|
|
274
376
|
* Mark the run as owned by this process until the returned release runs.
|
|
275
377
|
* Recovery skips owned runs, so an executor outside walkGraph (the retry
|
|
@@ -705,6 +807,20 @@ async function executeStep(
|
|
|
705
807
|
if (current && (current.status !== "running" || executingSteps.has(current.id))) {
|
|
706
808
|
return { current };
|
|
707
809
|
}
|
|
810
|
+
// An orphaned `sub-workflow` row is this same invocation: it covers the
|
|
811
|
+
// current predecessor completions, so it is not a new loop iteration.
|
|
812
|
+
// Re-enter it under its step id, key and retry count, so the executor
|
|
813
|
+
// reconnects to the child it already started instead of a second one.
|
|
814
|
+
if (current && node.type === "sub-workflow") {
|
|
815
|
+
claimedStepId = current.id;
|
|
816
|
+
executingSteps.add(current.id);
|
|
817
|
+
return {
|
|
818
|
+
existingStep: current,
|
|
819
|
+
stepId: current.id,
|
|
820
|
+
fence: { retryCount: current.retryCount },
|
|
821
|
+
deduped: false,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
708
824
|
}
|
|
709
825
|
|
|
710
826
|
// Count existing steps for this node to determine the current iteration.
|
|
@@ -717,7 +833,12 @@ async function executeStep(
|
|
|
717
833
|
existingStep &&
|
|
718
834
|
(existingStep.status === "completed" || existingStep.status === "waiting")
|
|
719
835
|
) {
|
|
720
|
-
return {
|
|
836
|
+
return {
|
|
837
|
+
existingStep,
|
|
838
|
+
stepId: existingStep.id,
|
|
839
|
+
fence: { retryCount: existingStep.retryCount },
|
|
840
|
+
deduped: true,
|
|
841
|
+
};
|
|
721
842
|
}
|
|
722
843
|
// For "pending" or "failed" steps, fall through to re-execute
|
|
723
844
|
|
|
@@ -748,7 +869,8 @@ async function executeStep(
|
|
|
748
869
|
// this `running` row without also seeing it owned.
|
|
749
870
|
claimedStepId = stepId;
|
|
750
871
|
executingSteps.add(stepId);
|
|
751
|
-
|
|
872
|
+
const fence = { retryCount: reusableForeachStep?.retryCount ?? 0 };
|
|
873
|
+
return { existingStep, stepId, fence, deduped: false };
|
|
752
874
|
};
|
|
753
875
|
const dedup = await getDbClient()
|
|
754
876
|
.transaction(claimStep)
|
|
@@ -787,6 +909,7 @@ async function executeStep(
|
|
|
787
909
|
registry,
|
|
788
910
|
dedup.existingStep,
|
|
789
911
|
dedup.stepId,
|
|
912
|
+
dedup.fence,
|
|
790
913
|
workflowId,
|
|
791
914
|
options,
|
|
792
915
|
);
|
|
@@ -800,7 +923,11 @@ async function executeStep(
|
|
|
800
923
|
}
|
|
801
924
|
}
|
|
802
925
|
|
|
803
|
-
/**
|
|
926
|
+
/**
|
|
927
|
+
* Steps 3-10 of executeStep, for a step row this call claimed. Every result
|
|
928
|
+
* write is fenced on the claim: once a cancel, a newer attempt or another
|
|
929
|
+
* settle moved the row, this execution writes nothing and routes nothing.
|
|
930
|
+
*/
|
|
804
931
|
async function runClaimedStep(
|
|
805
932
|
def: WorkflowDefinition,
|
|
806
933
|
runId: string,
|
|
@@ -809,9 +936,11 @@ async function runClaimedStep(
|
|
|
809
936
|
registry: ExecutorRegistry,
|
|
810
937
|
existingStep: WorkflowRunStep | null | undefined,
|
|
811
938
|
stepId: string,
|
|
939
|
+
fence: StepFence,
|
|
812
940
|
workflowId?: string,
|
|
813
941
|
options: WorkflowExecutionOptions = {},
|
|
814
942
|
): Promise<StepResult> {
|
|
943
|
+
const stale: StepResult = { outcome: "failed", successors: [] };
|
|
815
944
|
// 3. Get executor
|
|
816
945
|
const executor = registry.get(node.type);
|
|
817
946
|
|
|
@@ -820,7 +949,9 @@ async function runClaimedStep(
|
|
|
820
949
|
// requests and multiply paid attempts. Fail before any request is built.
|
|
821
950
|
const systemOneRetryProblems = systemOneRetryViolations(node);
|
|
822
951
|
if (systemOneRetryProblems.length > 0) {
|
|
823
|
-
await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0
|
|
952
|
+
await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0, undefined, {
|
|
953
|
+
fence,
|
|
954
|
+
});
|
|
824
955
|
return { outcome: "failed", successors: [] };
|
|
825
956
|
}
|
|
826
957
|
|
|
@@ -835,7 +966,10 @@ async function runClaimedStep(
|
|
|
835
966
|
);
|
|
836
967
|
if (inputErrors.length > 0) {
|
|
837
968
|
const errorMsg = `Input schema validation failed: ${inputErrors.join("; ")}`;
|
|
838
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0
|
|
969
|
+
const { stale: lost } = await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
|
|
970
|
+
fence,
|
|
971
|
+
});
|
|
972
|
+
if (lost) return stale;
|
|
839
973
|
throw new Error(errorMsg);
|
|
840
974
|
}
|
|
841
975
|
}
|
|
@@ -852,7 +986,7 @@ async function runClaimedStep(
|
|
|
852
986
|
|
|
853
987
|
if (scriptBodyUnresolved && scriptBodyUnresolved.length > 0) {
|
|
854
988
|
const errorMsg = scriptBodyInterpolationError(node.id, scriptBodyUnresolved);
|
|
855
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0);
|
|
989
|
+
await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, { fence });
|
|
856
990
|
return { outcome: "failed", successors: [] };
|
|
857
991
|
}
|
|
858
992
|
|
|
@@ -863,6 +997,8 @@ async function runClaimedStep(
|
|
|
863
997
|
stepId,
|
|
864
998
|
systemOneUnresolvedError(node.id, strictUnresolved, node.type),
|
|
865
999
|
0,
|
|
1000
|
+
undefined,
|
|
1001
|
+
{ fence },
|
|
866
1002
|
);
|
|
867
1003
|
return { outcome: "failed", successors: [] };
|
|
868
1004
|
}
|
|
@@ -905,14 +1041,16 @@ async function runClaimedStep(
|
|
|
905
1041
|
// Apply retry policy if configured
|
|
906
1042
|
const retryPolicy = node.retry || executor.retryPolicy;
|
|
907
1043
|
const currentRetryCount = existingStep?.retryCount || 0;
|
|
908
|
-
const { shouldRetry } = await checkpointStepFailure(
|
|
1044
|
+
const { shouldRetry, stale: lost } = await checkpointStepFailure(
|
|
909
1045
|
runId,
|
|
910
1046
|
stepId,
|
|
911
1047
|
errorMsg,
|
|
912
1048
|
currentRetryCount,
|
|
913
1049
|
retryPolicy,
|
|
1050
|
+
{ fence },
|
|
914
1051
|
);
|
|
915
1052
|
|
|
1053
|
+
if (lost) return stale;
|
|
916
1054
|
if (!shouldRetry) {
|
|
917
1055
|
throw err; // Will be caught by walkGraph
|
|
918
1056
|
}
|
|
@@ -922,20 +1060,21 @@ async function runClaimedStep(
|
|
|
922
1060
|
|
|
923
1061
|
// 6. Handle result
|
|
924
1062
|
if (result.status === "failed") {
|
|
925
|
-
|
|
1063
|
+
// A failure the executor marks non-retryable skips the policy here as on
|
|
1064
|
+
// every other path that settles the same outcome.
|
|
1065
|
+
const retryPolicy = result.retryable === false ? undefined : node.retry || executor.retryPolicy;
|
|
926
1066
|
const currentRetryCount = existingStep?.retryCount || 0;
|
|
927
|
-
|
|
1067
|
+
// Output is persisted for observability even on failure (e.g. script
|
|
1068
|
+
// nodes keep {exitCode, stdout, stderr}), in the same fenced write.
|
|
1069
|
+
const { shouldRetry, stale: lost } = await checkpointStepFailure(
|
|
928
1070
|
runId,
|
|
929
1071
|
stepId,
|
|
930
1072
|
result.error || "Executor returned failed status",
|
|
931
1073
|
currentRetryCount,
|
|
932
1074
|
retryPolicy,
|
|
1075
|
+
{ fence, output: result.output },
|
|
933
1076
|
);
|
|
934
|
-
|
|
935
|
-
// Persist output for observability even on failure (e.g. script nodes keep {exitCode, stdout, stderr})
|
|
936
|
-
if (result.output !== undefined) {
|
|
937
|
-
await updateWorkflowRunStep(stepId, { output: result.output });
|
|
938
|
-
}
|
|
1077
|
+
if (lost) return stale;
|
|
939
1078
|
|
|
940
1079
|
if (!shouldRetry) {
|
|
941
1080
|
throw new Error(result.error || "Step execution failed");
|
|
@@ -976,7 +1115,10 @@ async function runClaimedStep(
|
|
|
976
1115
|
);
|
|
977
1116
|
if (outputErrors.length > 0) {
|
|
978
1117
|
const errorMsg = `Output schema validation failed: ${outputErrors.join("; ")}`;
|
|
979
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0
|
|
1118
|
+
const { stale: lost } = await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
|
|
1119
|
+
fence,
|
|
1120
|
+
});
|
|
1121
|
+
if (lost) return stale;
|
|
980
1122
|
throw new Error(errorMsg);
|
|
981
1123
|
}
|
|
982
1124
|
}
|
|
@@ -988,7 +1130,10 @@ async function runClaimedStep(
|
|
|
988
1130
|
|
|
989
1131
|
if (validationResult.outcome === "halt") {
|
|
990
1132
|
const errorMsg = "Validation failed (mustPass)";
|
|
991
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
|
|
1133
|
+
await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
|
|
1134
|
+
markRunFailed: false,
|
|
1135
|
+
fence,
|
|
1136
|
+
});
|
|
992
1137
|
return { outcome: "failed", successors: [] };
|
|
993
1138
|
}
|
|
994
1139
|
|
|
@@ -1001,13 +1146,15 @@ async function runClaimedStep(
|
|
|
1001
1146
|
}
|
|
1002
1147
|
const retryPolicy = node.validation.retry || node.retry;
|
|
1003
1148
|
const currentRetryCount = existingStep?.retryCount || 0;
|
|
1004
|
-
await checkpointStepFailure(
|
|
1149
|
+
const { stale: lost } = await checkpointStepFailure(
|
|
1005
1150
|
runId,
|
|
1006
1151
|
stepId,
|
|
1007
1152
|
"Validation failed, retrying",
|
|
1008
1153
|
currentRetryCount,
|
|
1009
1154
|
retryPolicy,
|
|
1155
|
+
{ fence },
|
|
1010
1156
|
);
|
|
1157
|
+
if (lost) return stale;
|
|
1011
1158
|
return { outcome: "completed", successors: [] }; // Retry handled by poller
|
|
1012
1159
|
}
|
|
1013
1160
|
}
|
|
@@ -1020,7 +1167,7 @@ async function runClaimedStep(
|
|
|
1020
1167
|
result.nextPort = resolveValidationPort(node.next, result.nextPort, validationResult?.passed);
|
|
1021
1168
|
|
|
1022
1169
|
// 9. Checkpoint success
|
|
1023
|
-
await checkpointStep(runId, stepId, node.id, result, ctx);
|
|
1170
|
+
if (!(await checkpointStep(runId, stepId, node.id, result, ctx, { fence }))) return stale;
|
|
1024
1171
|
|
|
1025
1172
|
// 10. Determine successors based on nextPort
|
|
1026
1173
|
// If executor returned a specific port, use it. Otherwise, get all successors
|