@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getChildWorkflowRunId, getWorkflowRunLineage } from "../../be/db/workflow-runs";
|
|
3
3
|
import type { ExecutorMeta, WorkflowRun } from "../../types";
|
|
4
|
-
import { startWorkflowExecution } from "../engine";
|
|
4
|
+
import { ParentStepNotCurrentError, startWorkflowExecution } from "../engine";
|
|
5
5
|
import type { ExecutorDependencies, ExecutorResult } from "./base";
|
|
6
6
|
import { BaseExecutor } from "./base";
|
|
7
7
|
import type { ExecutorRegistry } from "./registry";
|
|
@@ -35,15 +35,21 @@ const NON_NODE_CONTEXT_KEYS = new Set(["trigger", "input", "swarm", "workflow"])
|
|
|
35
35
|
/**
|
|
36
36
|
* The parent step's outcome for a child run: its result once the child
|
|
37
37
|
* completed, an error once it failed, was cancelled or was skipped, and null
|
|
38
|
-
* while it is still running or waiting.
|
|
38
|
+
* while it is still running or waiting. Only a failed child is retryable: a
|
|
39
|
+
* cancel or a skip is a decision, and a node retry must not override it. The
|
|
40
|
+
* synchronous (executor) and the parked (`resumeFromChildRun`) paths both
|
|
41
|
+
* apply this, so the same child end gets the same policy whenever it lands.
|
|
39
42
|
*/
|
|
40
43
|
export function childRunOutcome(
|
|
41
44
|
child: WorkflowRun,
|
|
42
|
-
): { output: SubWorkflowOutput } | { error: string } | null {
|
|
45
|
+
): { output: SubWorkflowOutput } | { error: string; retryable: boolean } | null {
|
|
43
46
|
if (child.status === "running" || child.status === "waiting") return null;
|
|
44
47
|
if (child.status !== "completed") {
|
|
45
48
|
const reason = child.error ? `: ${child.error}` : "";
|
|
46
|
-
return {
|
|
49
|
+
return {
|
|
50
|
+
error: `Child workflow run ${child.id} ${child.status}${reason}`,
|
|
51
|
+
retryable: child.status === "failed",
|
|
52
|
+
};
|
|
47
53
|
}
|
|
48
54
|
const outputs: Record<string, unknown> = {};
|
|
49
55
|
for (const [key, value] of Object.entries(child.context ?? {})) {
|
|
@@ -56,8 +62,9 @@ export function childRunOutcome(
|
|
|
56
62
|
|
|
57
63
|
/**
|
|
58
64
|
* Runs another workflow as a child run and waits for it. The child run records
|
|
59
|
-
* this step as its parent, so a re-executed
|
|
60
|
-
* starting a second child
|
|
65
|
+
* this step and its attempt (`retryCount`) as its parent, so a re-executed or
|
|
66
|
+
* recovered step reconnects to it instead of starting a second child, while a
|
|
67
|
+
* retry starts a new one. The resume path is `resumeFromChildRun`.
|
|
61
68
|
*/
|
|
62
69
|
export class SubWorkflowExecutor extends BaseExecutor<
|
|
63
70
|
typeof SubWorkflowConfigSchema,
|
|
@@ -82,9 +89,12 @@ export class SubWorkflowExecutor extends BaseExecutor<
|
|
|
82
89
|
): Promise<ExecutorResult<SubWorkflowOutput>> {
|
|
83
90
|
const { db } = this.deps;
|
|
84
91
|
|
|
85
|
-
|
|
92
|
+
// The retry poller re-executes a failed step under the same id with a
|
|
93
|
+
// higher retryCount; that attempt must not settle on the previous child.
|
|
94
|
+
const attempt = (await db.getWorkflowRunStep(meta.stepId))?.retryCount ?? 0;
|
|
95
|
+
const existingId = await getChildWorkflowRunId(meta.stepId, attempt);
|
|
86
96
|
const existing = existingId ? await db.getWorkflowRun(existingId) : null;
|
|
87
|
-
if (existing) return this.settle(existing);
|
|
97
|
+
if (existing) return this.settle(existing, config.workflowId);
|
|
88
98
|
|
|
89
99
|
// Capped at the max depth, so a long chain fails closed instead of
|
|
90
100
|
// hiding a cycle past the end of the scan.
|
|
@@ -109,17 +119,34 @@ export class SubWorkflowExecutor extends BaseExecutor<
|
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
// Returns once the child run exists; the child walks on its own. A child
|
|
112
|
-
// that already ended (cooldown, readiness, input failure) settles here
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
// that already ended (cooldown, readiness, input failure) settles here,
|
|
123
|
+
// and a concurrent execution of this attempt gets the child it created.
|
|
124
|
+
let childRunId: string;
|
|
125
|
+
try {
|
|
126
|
+
childRunId = await startWorkflowExecution(workflow, config.inputs ?? {}, this.registry, {
|
|
127
|
+
requestedByUserId: meta.requestedByUserId,
|
|
128
|
+
parentStepId: meta.stepId,
|
|
129
|
+
parentStepAttempt: attempt,
|
|
130
|
+
});
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (err instanceof ParentStepNotCurrentError) return { status: "failed", error: err.message };
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
117
135
|
const child = await db.getWorkflowRun(childRunId);
|
|
118
136
|
if (!child) return { status: "failed", error: `Child workflow run ${childRunId} not found` };
|
|
119
|
-
return this.settle(child);
|
|
137
|
+
return this.settle(child, config.workflowId);
|
|
120
138
|
}
|
|
121
139
|
|
|
122
|
-
private settle(child: WorkflowRun): ExecutorResult<SubWorkflowOutput> {
|
|
140
|
+
private settle(child: WorkflowRun, workflowId: string): ExecutorResult<SubWorkflowOutput> {
|
|
141
|
+
// The attempt already started a child of another workflow (the config
|
|
142
|
+
// changed under a recovered step). That is a conflict, not a reason to
|
|
143
|
+
// start a replacement next to it.
|
|
144
|
+
if (child.workflowId !== workflowId) {
|
|
145
|
+
return {
|
|
146
|
+
status: "failed",
|
|
147
|
+
error: `Child workflow run ${child.id} runs workflow ${child.workflowId}, not ${workflowId}`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
123
150
|
const outcome = childRunOutcome(child);
|
|
124
151
|
if (!outcome) {
|
|
125
152
|
return {
|
|
@@ -129,7 +156,9 @@ export class SubWorkflowExecutor extends BaseExecutor<
|
|
|
129
156
|
correlationId: child.id,
|
|
130
157
|
} as ExecutorResult<SubWorkflowOutput>;
|
|
131
158
|
}
|
|
132
|
-
if ("error" in outcome)
|
|
159
|
+
if ("error" in outcome) {
|
|
160
|
+
return { status: "failed", error: outcome.error, retryable: outcome.retryable };
|
|
161
|
+
}
|
|
133
162
|
return { status: "success", output: outcome.output, nextPort: "success" };
|
|
134
163
|
}
|
|
135
164
|
}
|
|
@@ -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";
|
|
@@ -20,6 +24,7 @@ import { FAILED_TASK_OUTPUT_PREFIX } from "./constants";
|
|
|
20
24
|
import {
|
|
21
25
|
failRunOnUnreadableReplay,
|
|
22
26
|
findReadyNodes,
|
|
27
|
+
isStepExecuting,
|
|
23
28
|
isWorkflowRunActive,
|
|
24
29
|
walkGraph,
|
|
25
30
|
} from "./engine";
|
|
@@ -76,6 +81,9 @@ export async function recoverIncompleteRuns(registry: ExecutorRegistry): Promise
|
|
|
76
81
|
// --- Case 6: foreach fan-outs with queued or orphaned items ---
|
|
77
82
|
recovered += await recoverForeachFanouts(registry);
|
|
78
83
|
|
|
84
|
+
// --- Case 7: sub-workflow steps a crash left running ---
|
|
85
|
+
recovered += await recoverOrphanedSubWorkflowSteps(registry);
|
|
86
|
+
|
|
79
87
|
if (recovered > 0) {
|
|
80
88
|
console.log(`[workflows] Recovered ${recovered} incomplete run(s)`);
|
|
81
89
|
}
|
|
@@ -98,6 +106,13 @@ async function recoverRunningRuns(registry: ExecutorRegistry): Promise<number> {
|
|
|
98
106
|
if (isWorkflowRunActive(runId)) continue;
|
|
99
107
|
const run = await getWorkflowRun(runId);
|
|
100
108
|
if (!run || run.status !== "running") continue;
|
|
109
|
+
// A child run commits with its initial context. One without it was
|
|
110
|
+
// created before that held and lost its trigger and inputs in a crash;
|
|
111
|
+
// walking it from an empty context would run its nodes on blanks.
|
|
112
|
+
if (run.parentStepId && run.context === undefined) {
|
|
113
|
+
await failRunningRun(runId, "Child workflow run lost its initial context in a restart");
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
101
116
|
|
|
102
117
|
const workflow = await getWorkflow(run.workflowId);
|
|
103
118
|
if (!workflow) continue;
|
|
@@ -146,6 +161,57 @@ async function recoverRunningRuns(registry: ExecutorRegistry): Promise<number> {
|
|
|
146
161
|
return recovered;
|
|
147
162
|
}
|
|
148
163
|
|
|
164
|
+
/** Fail a run that is still `running`; a cancel or finish that committed first wins. */
|
|
165
|
+
async function failRunningRun(runId: string, error: string): Promise<void> {
|
|
166
|
+
await getDbClient().transaction(async () => {
|
|
167
|
+
const run = await getWorkflowRun(runId);
|
|
168
|
+
if (run?.status !== "running") return;
|
|
169
|
+
await updateWorkflowRun(runId, {
|
|
170
|
+
status: "failed",
|
|
171
|
+
error,
|
|
172
|
+
finishedAt: new Date().toISOString(),
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Re-enter `sub-workflow` steps a crash left `running`. The running-run case
|
|
179
|
+
* misses two shapes: a run another branch already parked in `waiting`, and a
|
|
180
|
+
* loop iteration of a node that completed before. Walking the node lets
|
|
181
|
+
* executeStep re-claim the same step row, so the executor reconnects to the
|
|
182
|
+
* child that attempt already started. Foreach items are left to the fan-out
|
|
183
|
+
* case: their synthetic node ids are not in the definition.
|
|
184
|
+
*/
|
|
185
|
+
async function recoverOrphanedSubWorkflowSteps(registry: ExecutorRegistry): Promise<number> {
|
|
186
|
+
let recovered = 0;
|
|
187
|
+
for (const row of await getRunningSubWorkflowSteps()) {
|
|
188
|
+
try {
|
|
189
|
+
if (isWorkflowRunActive(row.runId) || isStepExecuting(row.stepId)) continue;
|
|
190
|
+
const run = await getWorkflowRun(row.runId);
|
|
191
|
+
if (!run || (run.status !== "running" && run.status !== "waiting")) continue;
|
|
192
|
+
const workflow = await getWorkflow(run.workflowId);
|
|
193
|
+
const node = workflow?.definition.nodes.find((n) => n.id === row.nodeId);
|
|
194
|
+
if (!workflow || !node) continue;
|
|
195
|
+
// DB reads above yield; a resume may have acquired the run meanwhile.
|
|
196
|
+
if (isWorkflowRunActive(row.runId)) continue;
|
|
197
|
+
await walkGraph(
|
|
198
|
+
workflow.definition,
|
|
199
|
+
row.runId,
|
|
200
|
+
(run.context ?? {}) as Record<string, unknown>,
|
|
201
|
+
[node],
|
|
202
|
+
registry,
|
|
203
|
+
workflow.id,
|
|
204
|
+
getSecretInputKeys(workflow.input),
|
|
205
|
+
);
|
|
206
|
+
recovered++;
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (await failRunOnUnreadableReplay(row.runId, err)) continue;
|
|
209
|
+
console.error(`[workflows] Failed to recover sub-workflow step ${row.stepId}:`, err);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return recovered;
|
|
213
|
+
}
|
|
214
|
+
|
|
149
215
|
/** Nodes with a failed step the retry poller will pick up (`nextRetryAt` set). */
|
|
150
216
|
function retryPendingNodeIds(steps: WorkflowRunStep[]): Set<string> {
|
|
151
217
|
const nodeIds = new Set<string>();
|
|
@@ -165,6 +231,7 @@ async function completeIfSettled(runId: string, ctx: Record<string, unknown>): P
|
|
|
165
231
|
return getDbClient().transaction(async () => {
|
|
166
232
|
const run = await getWorkflowRun(runId);
|
|
167
233
|
if (run?.status !== "running") return false;
|
|
234
|
+
if (await getRunLineageBlock(runId)) return false;
|
|
168
235
|
const steps = await getWorkflowRunStepsByRunId(runId);
|
|
169
236
|
if (retryPendingNodeIds(steps).size > 0) return false;
|
|
170
237
|
const latest = new Map<string, WorkflowRunStep>();
|