@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/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,20 @@ import {
|
|
|
20
21
|
updateWorkflowRun,
|
|
21
22
|
updateWorkflowRunStep,
|
|
22
23
|
} from "../be/db";
|
|
24
|
+
import {
|
|
25
|
+
getChildWorkflowRunId,
|
|
26
|
+
getRunLineageBlock,
|
|
27
|
+
isParentStepAttemptCurrent,
|
|
28
|
+
} from "../be/db/workflow-runs";
|
|
23
29
|
import { WorkflowReplayStateError } from "../be/workflow-replay";
|
|
24
30
|
import { telemetry } from "../telemetry";
|
|
25
31
|
import type { Workflow, WorkflowDefinition, WorkflowNode, WorkflowRunStep } from "../types";
|
|
26
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
checkpointStep,
|
|
34
|
+
checkpointStepFailure,
|
|
35
|
+
checkpointStepWaiting,
|
|
36
|
+
type StepFence,
|
|
37
|
+
} from "./checkpoint";
|
|
27
38
|
import { loadCompletedStepRouting } from "./completed-step-routing";
|
|
28
39
|
import { FOREACH_ITEMS_WAIT_FOR } from "./constants";
|
|
29
40
|
import { shouldSkipCooldown } from "./cooldown";
|
|
@@ -58,6 +69,15 @@ export interface WorkflowExecutionOptions {
|
|
|
58
69
|
triggerType?: "schedule" | "manual" | "event" | "api";
|
|
59
70
|
/** Set when a `sub-workflow` step starts this run as its child. */
|
|
60
71
|
parentStepId?: string;
|
|
72
|
+
/** The parent step's `retryCount`; each attempt gets its own child run. */
|
|
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>;
|
|
61
81
|
}
|
|
62
82
|
|
|
63
83
|
/**
|
|
@@ -87,11 +107,9 @@ async function resolveRenderedWorkflowInputs(
|
|
|
87
107
|
/**
|
|
88
108
|
* Start executing a workflow from scratch.
|
|
89
109
|
*
|
|
90
|
-
* 1. Check cooldown
|
|
91
|
-
* 2. Create workflow run
|
|
92
|
-
* 3.
|
|
93
|
-
* 4. Find entry nodes
|
|
94
|
-
* 5. Walk the graph
|
|
110
|
+
* 1. Check cooldown, readiness and inputs
|
|
111
|
+
* 2. Create the workflow run with its initial state (one transaction)
|
|
112
|
+
* 3. Walk the graph from the entry nodes
|
|
95
113
|
*/
|
|
96
114
|
export async function startWorkflowExecution(
|
|
97
115
|
workflow: Workflow,
|
|
@@ -103,75 +121,142 @@ export async function startWorkflowExecution(
|
|
|
103
121
|
workflowPreflightInput(workflow),
|
|
104
122
|
await getAutomationSetupStates(),
|
|
105
123
|
);
|
|
106
|
-
if (preflight.state === "needs_setup") {
|
|
107
|
-
const { runId, recorded } = await
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
124
|
+
if (preflight.state === "needs_setup" && !options.parentStepId) {
|
|
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;
|
|
113
135
|
});
|
|
114
136
|
if (recorded) await notifyAutomationPreflightFailure(preflight);
|
|
115
137
|
return runId;
|
|
116
138
|
}
|
|
117
139
|
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
140
|
+
// Settle everything the run starts from before its row exists, so the row
|
|
141
|
+
// commits together with its initial context or terminal status. A crash
|
|
142
|
+
// right after the commit leaves a run that recovery can walk, never a
|
|
143
|
+
// `running` row with an empty context.
|
|
144
|
+
let initial: InitialRunState;
|
|
145
|
+
if (preflight.state === "needs_setup") {
|
|
146
|
+
// A child keeps its parent link and attempt even when it cannot start:
|
|
147
|
+
// the day-deduped top-level failure run is shared and unlinked, so a
|
|
148
|
+
// retry or recovery of the step would never find it.
|
|
149
|
+
initial = {
|
|
150
|
+
status: "failed",
|
|
151
|
+
error: preflight.failureReason!,
|
|
152
|
+
finishedAt: new Date().toISOString(),
|
|
153
|
+
preflight: true,
|
|
154
|
+
};
|
|
155
|
+
} else {
|
|
156
|
+
// Templates can consume install params outside the graph definition (most
|
|
157
|
+
// importantly workflow.input). Render the complete runtime snapshot once;
|
|
158
|
+
// exact-token values retain their JSON type, including COMPETITORS arrays.
|
|
159
|
+
workflow = renderAutomationTokens(workflow, workflow.params ?? {});
|
|
160
|
+
|
|
161
|
+
// Validate trigger data against triggerSchema (before any DB writes)
|
|
162
|
+
if (workflow.triggerSchema) {
|
|
163
|
+
const validationErrors = validateJsonSchema(workflow.triggerSchema, triggerData);
|
|
164
|
+
if (validationErrors.length > 0) {
|
|
165
|
+
throw new TriggerSchemaError(validationErrors);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
122
168
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
throw new TriggerSchemaError(validationErrors);
|
|
169
|
+
if (workflow.cooldown && (await shouldSkipCooldown(workflow.id, workflow.cooldown))) {
|
|
170
|
+
initial = { status: "skipped", error: "cooldown", finishedAt: new Date().toISOString() };
|
|
171
|
+
} else {
|
|
172
|
+
initial = await buildInitialRunState(workflow, triggerData, registry, options);
|
|
128
173
|
}
|
|
129
174
|
}
|
|
130
175
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
176
|
+
// Held from before the commit until the walk holds the run itself, so a
|
|
177
|
+
// recovery sweep never walks the new run alongside it.
|
|
178
|
+
const newRunId = crypto.randomUUID();
|
|
179
|
+
const release = holdWorkflowRun(newRunId);
|
|
180
|
+
try {
|
|
181
|
+
const { runId, created, preflightAlert } = await createRunWithInitialState(
|
|
182
|
+
workflow,
|
|
183
|
+
triggerData,
|
|
184
|
+
options,
|
|
185
|
+
initial,
|
|
186
|
+
newRunId,
|
|
187
|
+
);
|
|
188
|
+
if (!created) return runId;
|
|
189
|
+
if (initial.status === "failed" && initial.preflight) {
|
|
190
|
+
if (preflightAlert) await notifyAutomationPreflightFailure(preflight);
|
|
191
|
+
return runId;
|
|
192
|
+
}
|
|
193
|
+
if (initial.status === "skipped") return runId;
|
|
194
|
+
telemetry.workflow("started", {
|
|
136
195
|
workflowId: workflow.id,
|
|
196
|
+
nodeCount: workflow.definition.nodes.length,
|
|
137
197
|
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
198
|
});
|
|
199
|
+
if (initial.status) return runId;
|
|
200
|
+
|
|
201
|
+
const walk = walkGraph(
|
|
202
|
+
workflow.definition,
|
|
203
|
+
runId,
|
|
204
|
+
initial.context,
|
|
205
|
+
findEntryNodes(workflow.definition),
|
|
206
|
+
registry,
|
|
207
|
+
workflow.id,
|
|
208
|
+
getSecretInputKeys(workflow.input),
|
|
209
|
+
options,
|
|
210
|
+
);
|
|
211
|
+
// A child run walks on its own, outside the parent executor's timeout. Its
|
|
212
|
+
// terminal status wakes the parent step (`resumeFromChildRun`).
|
|
213
|
+
if (options.parentStepId) {
|
|
214
|
+
walk.catch((err) => console.error(`[workflows] Child workflow run ${runId} failed:`, err));
|
|
215
|
+
return runId;
|
|
216
|
+
}
|
|
217
|
+
await walk;
|
|
147
218
|
return runId;
|
|
219
|
+
} finally {
|
|
220
|
+
release();
|
|
148
221
|
}
|
|
222
|
+
}
|
|
149
223
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Thrown when a `sub-workflow` step asks for a child run while the step's
|
|
226
|
+
* attempt no longer executes (cancelled, re-attempted or already parked).
|
|
227
|
+
*/
|
|
228
|
+
export class ParentStepNotCurrentError extends Error {
|
|
229
|
+
constructor(parentStepId: string, attempt: number) {
|
|
230
|
+
super(
|
|
231
|
+
`Parent step ${parentStepId} (attempt ${attempt}) is no longer running; no child run started`,
|
|
232
|
+
);
|
|
233
|
+
this.name = "ParentStepNotCurrentError";
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* A new run's first committed state: a terminal status, or the context it
|
|
239
|
+
* walks from. `preflight` marks a setup failure, which alerts once a day.
|
|
240
|
+
*/
|
|
241
|
+
type InitialRunState =
|
|
242
|
+
| { status: "skipped" | "failed"; error: string; finishedAt: string; preflight?: true }
|
|
243
|
+
| { status?: undefined; context: Record<string, unknown> };
|
|
164
244
|
|
|
245
|
+
async function buildInitialRunState(
|
|
246
|
+
workflow: Workflow,
|
|
247
|
+
triggerData: unknown,
|
|
248
|
+
registry: ExecutorRegistry,
|
|
249
|
+
options: WorkflowExecutionOptions,
|
|
250
|
+
): Promise<InitialRunState> {
|
|
165
251
|
// An executor that cannot run (for example a system-one-decision node with no API key) fails
|
|
166
252
|
// the run here, before any node has side effects, instead of partway through.
|
|
167
253
|
const notReady = await findWorkflowReadinessProblems(workflow.definition, registry);
|
|
168
254
|
if (notReady.length > 0) {
|
|
169
|
-
|
|
255
|
+
return {
|
|
170
256
|
status: "failed",
|
|
171
257
|
error: formatReadinessRunError(notReady),
|
|
172
258
|
finishedAt: new Date().toISOString(),
|
|
173
|
-
}
|
|
174
|
-
return runId;
|
|
259
|
+
};
|
|
175
260
|
}
|
|
176
261
|
|
|
177
262
|
// Resolve inputs and merge into initial context
|
|
@@ -190,35 +275,64 @@ export async function startWorkflowExecution(
|
|
|
190
275
|
const resolved = await resolveRenderedWorkflowInputs(workflow.input);
|
|
191
276
|
Object.assign(ctx, { input: resolved });
|
|
192
277
|
} catch (err) {
|
|
193
|
-
|
|
278
|
+
return {
|
|
194
279
|
status: "failed",
|
|
195
280
|
error: `Input resolution failed: ${err}`,
|
|
196
281
|
finishedAt: new Date().toISOString(),
|
|
197
|
-
}
|
|
198
|
-
return runId;
|
|
282
|
+
};
|
|
199
283
|
}
|
|
200
284
|
}
|
|
285
|
+
return { context: ctx };
|
|
286
|
+
}
|
|
201
287
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Insert the run with its initial state in one transaction. For a child run
|
|
290
|
+
* the same transaction checks that the parent step's attempt is still
|
|
291
|
+
* executing and returns the child that attempt already started instead of a
|
|
292
|
+
* second one; the unique (parentStepId, parentStepAttempt) index backs this
|
|
293
|
+
* across processes. A setup failure claims the day's alert in the same
|
|
294
|
+
* transaction, so concurrent failures alert exactly once. `options.accept`
|
|
295
|
+
* runs last in it.
|
|
296
|
+
*/
|
|
297
|
+
async function createRunWithInitialState(
|
|
298
|
+
workflow: Workflow,
|
|
299
|
+
triggerData: unknown,
|
|
300
|
+
options: WorkflowExecutionOptions,
|
|
301
|
+
initial: InitialRunState,
|
|
302
|
+
runId: string,
|
|
303
|
+
): Promise<{ runId: string; created: boolean; preflightAlert?: boolean }> {
|
|
304
|
+
return getDbClient().transaction(async () => {
|
|
305
|
+
const { parentStepId } = options;
|
|
306
|
+
const attempt = options.parentStepAttempt ?? 0;
|
|
307
|
+
if (parentStepId) {
|
|
308
|
+
if (!(await isParentStepAttemptCurrent(parentStepId, attempt))) {
|
|
309
|
+
throw new ParentStepNotCurrentError(parentStepId, attempt);
|
|
310
|
+
}
|
|
311
|
+
const existingId = await getChildWorkflowRunId(parentStepId, attempt);
|
|
312
|
+
if (existingId) return { runId: existingId, created: false };
|
|
313
|
+
}
|
|
314
|
+
await createWorkflowRun({
|
|
315
|
+
id: runId,
|
|
316
|
+
workflowId: workflow.id,
|
|
317
|
+
triggerType: options.triggerType ?? "manual",
|
|
318
|
+
triggerData,
|
|
319
|
+
createdBy: options.requestedByUserId,
|
|
320
|
+
parentStepId,
|
|
321
|
+
parentStepAttempt: parentStepId ? attempt : undefined,
|
|
322
|
+
});
|
|
323
|
+
await updateWorkflowRun(
|
|
324
|
+
runId,
|
|
325
|
+
initial.status
|
|
326
|
+
? { status: initial.status, error: initial.error, finishedAt: initial.finishedAt }
|
|
327
|
+
: { context: initial.context },
|
|
328
|
+
);
|
|
329
|
+
const preflightAlert =
|
|
330
|
+
initial.status === "failed" && initial.preflight
|
|
331
|
+
? await isFirstWorkflowPreflightFailureToday(workflow.id, runId)
|
|
332
|
+
: undefined;
|
|
333
|
+
await options.accept?.({ runId, skipped: initial.status === "skipped" });
|
|
334
|
+
return { runId, created: true, preflightAlert };
|
|
335
|
+
});
|
|
222
336
|
}
|
|
223
337
|
|
|
224
338
|
// ─── Graph Walker ──────────────────────────────────────────
|
|
@@ -270,6 +384,11 @@ export function isWorkflowRunActive(runId: string): boolean {
|
|
|
270
384
|
return activeWalks.has(runId);
|
|
271
385
|
}
|
|
272
386
|
|
|
387
|
+
/** True while an executeStep call in this process owns the step row. */
|
|
388
|
+
export function isStepExecuting(stepId: string): boolean {
|
|
389
|
+
return executingSteps.has(stepId);
|
|
390
|
+
}
|
|
391
|
+
|
|
273
392
|
/**
|
|
274
393
|
* Mark the run as owned by this process until the returned release runs.
|
|
275
394
|
* Recovery skips owned runs, so an executor outside walkGraph (the retry
|
|
@@ -504,6 +623,8 @@ async function walkGraphOwned(
|
|
|
504
623
|
await getDbClient().transaction(async () => {
|
|
505
624
|
const run = await getWorkflowRun(runId);
|
|
506
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;
|
|
507
628
|
|
|
508
629
|
const finalSteps = await getWorkflowRunStepsByRunId(runId);
|
|
509
630
|
// Another walker is still executing a branch; it finalizes the run.
|
|
@@ -691,6 +812,9 @@ async function executeStep(
|
|
|
691
812
|
if (!run || (run.status !== "running" && run.status !== "waiting")) {
|
|
692
813
|
return { halted: true as const };
|
|
693
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 };
|
|
694
818
|
|
|
695
819
|
// Another walker already started or finished this node for the same
|
|
696
820
|
// predecessor completions (two branch completions both walking the join,
|
|
@@ -705,6 +829,20 @@ async function executeStep(
|
|
|
705
829
|
if (current && (current.status !== "running" || executingSteps.has(current.id))) {
|
|
706
830
|
return { current };
|
|
707
831
|
}
|
|
832
|
+
// An orphaned `sub-workflow` row is this same invocation: it covers the
|
|
833
|
+
// current predecessor completions, so it is not a new loop iteration.
|
|
834
|
+
// Re-enter it under its step id, key and retry count, so the executor
|
|
835
|
+
// reconnects to the child it already started instead of a second one.
|
|
836
|
+
if (current && node.type === "sub-workflow") {
|
|
837
|
+
claimedStepId = current.id;
|
|
838
|
+
executingSteps.add(current.id);
|
|
839
|
+
return {
|
|
840
|
+
existingStep: current,
|
|
841
|
+
stepId: current.id,
|
|
842
|
+
fence: { retryCount: current.retryCount },
|
|
843
|
+
deduped: false,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
708
846
|
}
|
|
709
847
|
|
|
710
848
|
// Count existing steps for this node to determine the current iteration.
|
|
@@ -717,7 +855,12 @@ async function executeStep(
|
|
|
717
855
|
existingStep &&
|
|
718
856
|
(existingStep.status === "completed" || existingStep.status === "waiting")
|
|
719
857
|
) {
|
|
720
|
-
return {
|
|
858
|
+
return {
|
|
859
|
+
existingStep,
|
|
860
|
+
stepId: existingStep.id,
|
|
861
|
+
fence: { retryCount: existingStep.retryCount },
|
|
862
|
+
deduped: true,
|
|
863
|
+
};
|
|
721
864
|
}
|
|
722
865
|
// For "pending" or "failed" steps, fall through to re-execute
|
|
723
866
|
|
|
@@ -748,7 +891,8 @@ async function executeStep(
|
|
|
748
891
|
// this `running` row without also seeing it owned.
|
|
749
892
|
claimedStepId = stepId;
|
|
750
893
|
executingSteps.add(stepId);
|
|
751
|
-
|
|
894
|
+
const fence = { retryCount: reusableForeachStep?.retryCount ?? 0 };
|
|
895
|
+
return { existingStep, stepId, fence, deduped: false };
|
|
752
896
|
};
|
|
753
897
|
const dedup = await getDbClient()
|
|
754
898
|
.transaction(claimStep)
|
|
@@ -787,6 +931,7 @@ async function executeStep(
|
|
|
787
931
|
registry,
|
|
788
932
|
dedup.existingStep,
|
|
789
933
|
dedup.stepId,
|
|
934
|
+
dedup.fence,
|
|
790
935
|
workflowId,
|
|
791
936
|
options,
|
|
792
937
|
);
|
|
@@ -800,7 +945,11 @@ async function executeStep(
|
|
|
800
945
|
}
|
|
801
946
|
}
|
|
802
947
|
|
|
803
|
-
/**
|
|
948
|
+
/**
|
|
949
|
+
* Steps 3-10 of executeStep, for a step row this call claimed. Every result
|
|
950
|
+
* write is fenced on the claim: once a cancel, a newer attempt or another
|
|
951
|
+
* settle moved the row, this execution writes nothing and routes nothing.
|
|
952
|
+
*/
|
|
804
953
|
async function runClaimedStep(
|
|
805
954
|
def: WorkflowDefinition,
|
|
806
955
|
runId: string,
|
|
@@ -809,9 +958,11 @@ async function runClaimedStep(
|
|
|
809
958
|
registry: ExecutorRegistry,
|
|
810
959
|
existingStep: WorkflowRunStep | null | undefined,
|
|
811
960
|
stepId: string,
|
|
961
|
+
fence: StepFence,
|
|
812
962
|
workflowId?: string,
|
|
813
963
|
options: WorkflowExecutionOptions = {},
|
|
814
964
|
): Promise<StepResult> {
|
|
965
|
+
const stale: StepResult = { outcome: "failed", successors: [] };
|
|
815
966
|
// 3. Get executor
|
|
816
967
|
const executor = registry.get(node.type);
|
|
817
968
|
|
|
@@ -820,7 +971,9 @@ async function runClaimedStep(
|
|
|
820
971
|
// requests and multiply paid attempts. Fail before any request is built.
|
|
821
972
|
const systemOneRetryProblems = systemOneRetryViolations(node);
|
|
822
973
|
if (systemOneRetryProblems.length > 0) {
|
|
823
|
-
await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0
|
|
974
|
+
await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0, undefined, {
|
|
975
|
+
fence,
|
|
976
|
+
});
|
|
824
977
|
return { outcome: "failed", successors: [] };
|
|
825
978
|
}
|
|
826
979
|
|
|
@@ -835,7 +988,10 @@ async function runClaimedStep(
|
|
|
835
988
|
);
|
|
836
989
|
if (inputErrors.length > 0) {
|
|
837
990
|
const errorMsg = `Input schema validation failed: ${inputErrors.join("; ")}`;
|
|
838
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0
|
|
991
|
+
const { stale: lost } = await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
|
|
992
|
+
fence,
|
|
993
|
+
});
|
|
994
|
+
if (lost) return stale;
|
|
839
995
|
throw new Error(errorMsg);
|
|
840
996
|
}
|
|
841
997
|
}
|
|
@@ -852,7 +1008,7 @@ async function runClaimedStep(
|
|
|
852
1008
|
|
|
853
1009
|
if (scriptBodyUnresolved && scriptBodyUnresolved.length > 0) {
|
|
854
1010
|
const errorMsg = scriptBodyInterpolationError(node.id, scriptBodyUnresolved);
|
|
855
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0);
|
|
1011
|
+
await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, { fence });
|
|
856
1012
|
return { outcome: "failed", successors: [] };
|
|
857
1013
|
}
|
|
858
1014
|
|
|
@@ -863,6 +1019,8 @@ async function runClaimedStep(
|
|
|
863
1019
|
stepId,
|
|
864
1020
|
systemOneUnresolvedError(node.id, strictUnresolved, node.type),
|
|
865
1021
|
0,
|
|
1022
|
+
undefined,
|
|
1023
|
+
{ fence },
|
|
866
1024
|
);
|
|
867
1025
|
return { outcome: "failed", successors: [] };
|
|
868
1026
|
}
|
|
@@ -905,14 +1063,16 @@ async function runClaimedStep(
|
|
|
905
1063
|
// Apply retry policy if configured
|
|
906
1064
|
const retryPolicy = node.retry || executor.retryPolicy;
|
|
907
1065
|
const currentRetryCount = existingStep?.retryCount || 0;
|
|
908
|
-
const { shouldRetry } = await checkpointStepFailure(
|
|
1066
|
+
const { shouldRetry, stale: lost } = await checkpointStepFailure(
|
|
909
1067
|
runId,
|
|
910
1068
|
stepId,
|
|
911
1069
|
errorMsg,
|
|
912
1070
|
currentRetryCount,
|
|
913
1071
|
retryPolicy,
|
|
1072
|
+
{ fence },
|
|
914
1073
|
);
|
|
915
1074
|
|
|
1075
|
+
if (lost) return stale;
|
|
916
1076
|
if (!shouldRetry) {
|
|
917
1077
|
throw err; // Will be caught by walkGraph
|
|
918
1078
|
}
|
|
@@ -922,20 +1082,21 @@ async function runClaimedStep(
|
|
|
922
1082
|
|
|
923
1083
|
// 6. Handle result
|
|
924
1084
|
if (result.status === "failed") {
|
|
925
|
-
|
|
1085
|
+
// A failure the executor marks non-retryable skips the policy here as on
|
|
1086
|
+
// every other path that settles the same outcome.
|
|
1087
|
+
const retryPolicy = result.retryable === false ? undefined : node.retry || executor.retryPolicy;
|
|
926
1088
|
const currentRetryCount = existingStep?.retryCount || 0;
|
|
927
|
-
|
|
1089
|
+
// Output is persisted for observability even on failure (e.g. script
|
|
1090
|
+
// nodes keep {exitCode, stdout, stderr}), in the same fenced write.
|
|
1091
|
+
const { shouldRetry, stale: lost } = await checkpointStepFailure(
|
|
928
1092
|
runId,
|
|
929
1093
|
stepId,
|
|
930
1094
|
result.error || "Executor returned failed status",
|
|
931
1095
|
currentRetryCount,
|
|
932
1096
|
retryPolicy,
|
|
1097
|
+
{ fence, output: result.output },
|
|
933
1098
|
);
|
|
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
|
-
}
|
|
1099
|
+
if (lost) return stale;
|
|
939
1100
|
|
|
940
1101
|
if (!shouldRetry) {
|
|
941
1102
|
throw new Error(result.error || "Step execution failed");
|
|
@@ -976,7 +1137,10 @@ async function runClaimedStep(
|
|
|
976
1137
|
);
|
|
977
1138
|
if (outputErrors.length > 0) {
|
|
978
1139
|
const errorMsg = `Output schema validation failed: ${outputErrors.join("; ")}`;
|
|
979
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0
|
|
1140
|
+
const { stale: lost } = await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
|
|
1141
|
+
fence,
|
|
1142
|
+
});
|
|
1143
|
+
if (lost) return stale;
|
|
980
1144
|
throw new Error(errorMsg);
|
|
981
1145
|
}
|
|
982
1146
|
}
|
|
@@ -988,7 +1152,10 @@ async function runClaimedStep(
|
|
|
988
1152
|
|
|
989
1153
|
if (validationResult.outcome === "halt") {
|
|
990
1154
|
const errorMsg = "Validation failed (mustPass)";
|
|
991
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
|
|
1155
|
+
await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, {
|
|
1156
|
+
markRunFailed: false,
|
|
1157
|
+
fence,
|
|
1158
|
+
});
|
|
992
1159
|
return { outcome: "failed", successors: [] };
|
|
993
1160
|
}
|
|
994
1161
|
|
|
@@ -1001,13 +1168,15 @@ async function runClaimedStep(
|
|
|
1001
1168
|
}
|
|
1002
1169
|
const retryPolicy = node.validation.retry || node.retry;
|
|
1003
1170
|
const currentRetryCount = existingStep?.retryCount || 0;
|
|
1004
|
-
await checkpointStepFailure(
|
|
1171
|
+
const { stale: lost } = await checkpointStepFailure(
|
|
1005
1172
|
runId,
|
|
1006
1173
|
stepId,
|
|
1007
1174
|
"Validation failed, retrying",
|
|
1008
1175
|
currentRetryCount,
|
|
1009
1176
|
retryPolicy,
|
|
1177
|
+
{ fence },
|
|
1010
1178
|
);
|
|
1179
|
+
if (lost) return stale;
|
|
1011
1180
|
return { outcome: "completed", successors: [] }; // Retry handled by poller
|
|
1012
1181
|
}
|
|
1013
1182
|
}
|
|
@@ -1020,7 +1189,7 @@ async function runClaimedStep(
|
|
|
1020
1189
|
result.nextPort = resolveValidationPort(node.next, result.nextPort, validationResult?.passed);
|
|
1021
1190
|
|
|
1022
1191
|
// 9. Checkpoint success
|
|
1023
|
-
await checkpointStep(runId, stepId, node.id, result, ctx);
|
|
1192
|
+
if (!(await checkpointStep(runId, stepId, node.id, result, ctx, { fence }))) return stale;
|
|
1024
1193
|
|
|
1025
1194
|
// 10. Determine successors based on nextPort
|
|
1026
1195
|
// If executor returned a specific port, use it. Otherwise, get all successors
|
|
@@ -51,6 +51,12 @@ export interface ExecutorResult<TOutput = unknown> {
|
|
|
51
51
|
output?: TOutput;
|
|
52
52
|
nextPort?: string;
|
|
53
53
|
error?: string;
|
|
54
|
+
/**
|
|
55
|
+
* False on a failure no retry can change (a child run that was cancelled,
|
|
56
|
+
* for example). The engine then skips the node's retry policy. Unset means
|
|
57
|
+
* retryable.
|
|
58
|
+
*/
|
|
59
|
+
retryable?: boolean;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
export interface AsyncExecutorResult<TOutput = unknown> extends ExecutorResult<TOutput> {
|
|
@@ -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
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
ConfigQuestionSettingsSchema,
|
|
4
|
+
normalizeApprovalQuestions,
|
|
5
|
+
} from "../../be/approval-config-question";
|
|
2
6
|
import type { ExecutorMeta } from "../../types";
|
|
3
7
|
import { scrubSecrets } from "../../utils/secret-scrubber";
|
|
4
8
|
import { dispatchApprovalNotifications } from "../approval-notifications";
|
|
@@ -56,6 +60,15 @@ const QuestionSchema = z.discriminatedUnion("type", [
|
|
|
56
60
|
description: z.string().optional(),
|
|
57
61
|
defaultValue: z.boolean().optional(),
|
|
58
62
|
}),
|
|
63
|
+
z.object({
|
|
64
|
+
id: z.string(),
|
|
65
|
+
type: z.literal("config"),
|
|
66
|
+
// Defaults to the config key during normalization in `run`.
|
|
67
|
+
label: z.string().optional(),
|
|
68
|
+
required: z.boolean().default(true),
|
|
69
|
+
description: z.string().optional(),
|
|
70
|
+
config: ConfigQuestionSettingsSchema,
|
|
71
|
+
}),
|
|
59
72
|
]);
|
|
60
73
|
|
|
61
74
|
export const ApproverConfigSchema = z.object({
|
|
@@ -207,7 +220,18 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
|
|
|
207
220
|
override async run(input: ExecutorInput): Promise<ExecutorResult<HITLOutput>> {
|
|
208
221
|
const resolved = resolveHitlQuestions(input.config?.questions);
|
|
209
222
|
if (!resolved.ok) return { status: "failed", error: resolved.error };
|
|
210
|
-
|
|
223
|
+
// Config questions: the same create-time rules as the tool and REST paths.
|
|
224
|
+
const approvers = input.config?.approvers as { policy?: unknown } | undefined;
|
|
225
|
+
const normalized = await normalizeApprovalQuestions(resolved.questions, {
|
|
226
|
+
approvers: { policy: approvers?.policy },
|
|
227
|
+
});
|
|
228
|
+
if (!normalized.ok) {
|
|
229
|
+
return {
|
|
230
|
+
status: "failed",
|
|
231
|
+
error: `human-in-the-loop questions are invalid: ${normalized.error}`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return super.run({ ...input, config: { ...input.config, questions: normalized.questions } });
|
|
211
235
|
}
|
|
212
236
|
|
|
213
237
|
protected async execute(
|