@crewx/workflow 0.3.22-rc.128 → 0.3.22-rc.129
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/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/src/engine.d.ts +1 -1
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +32 -2
- package/dist/src/engine.js.map +1 -1
- package/dist/src/failure.d.ts +41 -0
- package/dist/src/failure.d.ts.map +1 -0
- package/dist/src/failure.js +122 -0
- package/dist/src/failure.js.map +1 -0
- package/dist/src/parent-task-sync.d.ts +1 -1
- package/dist/src/parent-task-sync.d.ts.map +1 -1
- package/dist/src/parent-task-sync.js +8 -1
- package/dist/src/parent-task-sync.js.map +1 -1
- package/dist/src/retry-policy.d.ts +38 -0
- package/dist/src/retry-policy.d.ts.map +1 -0
- package/dist/src/retry-policy.js +140 -0
- package/dist/src/retry-policy.js.map +1 -0
- package/dist/src/run-manager.d.ts +14 -2
- package/dist/src/run-manager.d.ts.map +1 -1
- package/dist/src/run-manager.js +580 -183
- package/dist/src/run-manager.js.map +1 -1
- package/dist/src/types.d.ts +21 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -1
- package/dist/workflow-schema.json +20 -3
- package/package.json +4 -4
- package/workflow-schema.json +20 -3
package/dist/src/run-manager.js
CHANGED
|
@@ -40,24 +40,46 @@ exports.interpolateTemplate = interpolateTemplate;
|
|
|
40
40
|
exports.safeEvaluate = safeEvaluate;
|
|
41
41
|
exports.safeEvaluateBranchCondition = safeEvaluateBranchCondition;
|
|
42
42
|
exports.validateSafeBranchCondition = validateSafeBranchCondition;
|
|
43
|
+
exports.normalizeRunStatus = normalizeRunStatus;
|
|
43
44
|
exports.isRetryableFailure = isRetryableFailure;
|
|
44
45
|
const fs = __importStar(require("fs"));
|
|
45
46
|
const path = __importStar(require("path"));
|
|
46
47
|
const yaml = __importStar(require("js-yaml"));
|
|
47
48
|
const cp = __importStar(require("child_process"));
|
|
48
49
|
const sdk_1 = require("@crewx/sdk");
|
|
50
|
+
const failure_1 = require("./failure");
|
|
49
51
|
const file_lock_1 = require("./file-lock");
|
|
50
52
|
const output_format_1 = require("./utils/output-format");
|
|
51
53
|
const shell_security_1 = require("./utils/shell-security");
|
|
54
|
+
const retry_policy_1 = require("./retry-policy");
|
|
52
55
|
var file_lock_2 = require("./file-lock");
|
|
53
56
|
Object.defineProperty(exports, "FileLock", { enumerable: true, get: function () { return file_lock_2.FileLock; } });
|
|
54
57
|
exports.SKILL_TASK_DEFAULT_TIMEOUT = 600000;
|
|
55
58
|
exports.WORKFLOW_RUN_INTERRUPTED_ERROR = 'Workflow run interrupted: runner process exited before completion';
|
|
56
59
|
exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS = 10 * 60000;
|
|
60
|
+
const RETRY_WAIT_SLICE_MS = 1000;
|
|
61
|
+
function sleep(ms) {
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
setTimeout(resolve, ms);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
57
66
|
const KNOWN_AUTO_NODE_TYPES = new Set([
|
|
58
67
|
'agent_task', 'skill_task', 'shell_task', 'branch', 'expression', 'join', 'end', 'error',
|
|
59
68
|
]);
|
|
60
69
|
const SELF_ADVANCING_NODE_TYPES = new Set(['branch', 'expression', 'end', 'error']);
|
|
70
|
+
function resolveCrewxChildInvocation(env, cwd) {
|
|
71
|
+
const resolution = (0, sdk_1.resolveCrewxExecutable)({ env, cwd });
|
|
72
|
+
if (!resolution.ok) {
|
|
73
|
+
throw (0, failure_1.createWorkflowFailureError)('CLI_UNRESOLVABLE', (0, sdk_1.formatCrewxExecutableFailure)(resolution));
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
argv: resolution.argv,
|
|
77
|
+
env: {
|
|
78
|
+
...env,
|
|
79
|
+
CREWX_CLI_ARGV: JSON.stringify(resolution.argv),
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
61
83
|
function killProcessGroup(child) {
|
|
62
84
|
try {
|
|
63
85
|
if (process.platform === 'win32') {
|
|
@@ -93,12 +115,23 @@ async function runProcess(command, argv, opts) {
|
|
|
93
115
|
});
|
|
94
116
|
}
|
|
95
117
|
catch (err) {
|
|
96
|
-
resolve({
|
|
118
|
+
resolve({
|
|
119
|
+
status: null,
|
|
120
|
+
stdout: '',
|
|
121
|
+
stderr: '',
|
|
122
|
+
timedOut: false,
|
|
123
|
+
signal: null,
|
|
124
|
+
errorOrigin: 'spawn',
|
|
125
|
+
error: err,
|
|
126
|
+
});
|
|
97
127
|
return;
|
|
98
128
|
}
|
|
99
129
|
let stdout = '';
|
|
100
130
|
let stderr = '';
|
|
101
131
|
let settled = false;
|
|
132
|
+
let spawned = false;
|
|
133
|
+
let postSpawnError;
|
|
134
|
+
let stdinError;
|
|
102
135
|
const finish = (result) => {
|
|
103
136
|
if (settled)
|
|
104
137
|
return;
|
|
@@ -108,38 +141,120 @@ async function runProcess(command, argv, opts) {
|
|
|
108
141
|
};
|
|
109
142
|
const timer = setTimeout(() => {
|
|
110
143
|
killProcessGroup(child);
|
|
111
|
-
finish({
|
|
144
|
+
finish({
|
|
145
|
+
status: null,
|
|
146
|
+
stdout,
|
|
147
|
+
stderr,
|
|
148
|
+
timedOut: true,
|
|
149
|
+
signal: null,
|
|
150
|
+
errorOrigin: stdinError ? 'stdin' : postSpawnError ? 'post_spawn' : null,
|
|
151
|
+
...(stdinError || postSpawnError ? { error: stdinError ?? postSpawnError } : {}),
|
|
152
|
+
});
|
|
112
153
|
}, opts.timeout);
|
|
113
154
|
if (typeof timer.unref === 'function')
|
|
114
155
|
timer.unref();
|
|
115
156
|
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
116
157
|
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
158
|
+
child.once('spawn', () => { spawned = true; });
|
|
159
|
+
child.on('error', (err) => {
|
|
160
|
+
if (!spawned) {
|
|
161
|
+
finish({
|
|
162
|
+
status: null,
|
|
163
|
+
stdout,
|
|
164
|
+
stderr,
|
|
165
|
+
timedOut: false,
|
|
166
|
+
signal: null,
|
|
167
|
+
errorOrigin: 'spawn',
|
|
168
|
+
error: err,
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
postSpawnError = postSpawnError ?? err;
|
|
173
|
+
});
|
|
117
174
|
if (opts.stdin !== undefined) {
|
|
118
|
-
child.stdin?.
|
|
175
|
+
child.stdin?.on('error', (err) => {
|
|
176
|
+
stdinError = stdinError ?? err;
|
|
177
|
+
});
|
|
178
|
+
try {
|
|
179
|
+
child.stdin?.end(opts.stdin);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
stdinError = stdinError ?? err;
|
|
183
|
+
}
|
|
119
184
|
}
|
|
120
|
-
child.on('
|
|
121
|
-
|
|
185
|
+
child.on('close', (code, signal) => finish({
|
|
186
|
+
status: code,
|
|
187
|
+
stdout,
|
|
188
|
+
stderr,
|
|
189
|
+
timedOut: false,
|
|
190
|
+
signal,
|
|
191
|
+
errorOrigin: stdinError ? 'stdin' : postSpawnError ? 'post_spawn' : null,
|
|
192
|
+
...(stdinError || postSpawnError ? { error: stdinError ?? postSpawnError } : {}),
|
|
193
|
+
}));
|
|
122
194
|
});
|
|
123
195
|
}
|
|
124
|
-
|
|
125
|
-
|
|
196
|
+
const AUDIT_IDENTIFIER_RE = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
197
|
+
function safeAuditIdentifier(value) {
|
|
198
|
+
if (value === undefined)
|
|
199
|
+
return undefined;
|
|
200
|
+
const match = AUDIT_IDENTIFIER_RE.exec(value);
|
|
201
|
+
return match?.[0] === value ? value : undefined;
|
|
126
202
|
}
|
|
127
203
|
function summarizeNode(node) {
|
|
128
204
|
switch (node.type) {
|
|
129
|
-
case 'agent_task':
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
205
|
+
case 'agent_task': {
|
|
206
|
+
const agent = safeAuditIdentifier(node.agent);
|
|
207
|
+
return agent
|
|
208
|
+
? `@${agent} (${node.mode === 'query' ? 'q' : 'x'})`
|
|
209
|
+
: `agent_task (${node.mode === 'query' ? 'q' : 'x'})`;
|
|
210
|
+
}
|
|
211
|
+
case 'skill_task': {
|
|
212
|
+
const skill = safeAuditIdentifier(node.skill);
|
|
213
|
+
return skill ? `skill ${skill}` : 'skill_task';
|
|
214
|
+
}
|
|
133
215
|
case 'shell_task':
|
|
134
|
-
return
|
|
216
|
+
return 'shell_task';
|
|
135
217
|
case 'branch':
|
|
136
|
-
return
|
|
218
|
+
return 'branch';
|
|
137
219
|
case 'expression':
|
|
138
|
-
return
|
|
220
|
+
return 'expression';
|
|
139
221
|
default:
|
|
140
|
-
return node
|
|
222
|
+
return 'workflow node';
|
|
141
223
|
}
|
|
142
224
|
}
|
|
225
|
+
function getErrorMessage(error) {
|
|
226
|
+
if (error instanceof Error)
|
|
227
|
+
return error.message;
|
|
228
|
+
return typeof error === 'string' ? error : 'Node execution failed';
|
|
229
|
+
}
|
|
230
|
+
function toProcessFailureMetadata(result) {
|
|
231
|
+
return {
|
|
232
|
+
status: result.status,
|
|
233
|
+
timedOut: result.timedOut,
|
|
234
|
+
signal: result.signal,
|
|
235
|
+
errorOrigin: result.errorOrigin,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function appendFailureAudit(run, failure, type, trigger, now = new Date().toISOString()) {
|
|
239
|
+
run.audit = run.audit ?? [];
|
|
240
|
+
run.audit.push({
|
|
241
|
+
exec_id: run.id,
|
|
242
|
+
node_id: failure.failedNode,
|
|
243
|
+
type,
|
|
244
|
+
summary: failure.causeMessage,
|
|
245
|
+
started_at: now,
|
|
246
|
+
ended_at: now,
|
|
247
|
+
duration_ms: 0,
|
|
248
|
+
exit_code: failure.exitCode,
|
|
249
|
+
error: failure.causeMessage,
|
|
250
|
+
trigger,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function assignRunFailure(run, failure, rawError) {
|
|
254
|
+
run.status = 'failed';
|
|
255
|
+
run.error = (0, failure_1.sanitizeLocalRunError)(rawError, failure.causeMessage);
|
|
256
|
+
run.last_failure = failure;
|
|
257
|
+
}
|
|
143
258
|
async function storeNodeOutput(nodeSpec, nodeId, stdout, stateUpdates) {
|
|
144
259
|
if (!nodeSpec.output)
|
|
145
260
|
return;
|
|
@@ -148,12 +263,12 @@ async function storeNodeOutput(nodeSpec, nodeId, stdout, stateUpdates) {
|
|
|
148
263
|
let parsed;
|
|
149
264
|
try {
|
|
150
265
|
parsed = JSON.parse((0, output_format_1.extractJson)(trimmed));
|
|
266
|
+
if (nodeSpec.output_schema) {
|
|
267
|
+
await (0, output_format_1.validateJsonSchema)(parsed, nodeSpec.output_schema);
|
|
268
|
+
}
|
|
151
269
|
}
|
|
152
270
|
catch (e) {
|
|
153
|
-
throw
|
|
154
|
-
}
|
|
155
|
-
if (nodeSpec.output_schema) {
|
|
156
|
-
await (0, output_format_1.validateJsonSchema)(parsed, nodeSpec.output_schema);
|
|
271
|
+
throw (0, failure_1.createWorkflowFailureError)('OUTPUT_INVALID', `Node "${nodeId}" output_format=json parse or validation failed: ${getErrorMessage(e)}`);
|
|
157
272
|
}
|
|
158
273
|
stateUpdates[nodeSpec.output] = parsed;
|
|
159
274
|
}
|
|
@@ -564,6 +679,34 @@ function isProcessAlive(pid) {
|
|
|
564
679
|
function isTerminalStatus(status) {
|
|
565
680
|
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
566
681
|
}
|
|
682
|
+
function isRecentLegacyRetry(run, nowMs) {
|
|
683
|
+
if (run.status !== undefined || run.auto_run)
|
|
684
|
+
return false;
|
|
685
|
+
const lastAudit = run.audit?.at(-1);
|
|
686
|
+
if (lastAudit?.type !== 'retry')
|
|
687
|
+
return false;
|
|
688
|
+
const updatedAt = Date.parse(run.updated_at);
|
|
689
|
+
return Number.isFinite(updatedAt)
|
|
690
|
+
&& nowMs - updatedAt < exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS;
|
|
691
|
+
}
|
|
692
|
+
function normalizeRunStatus(run, nowMs = Date.now()) {
|
|
693
|
+
if (run.status !== undefined)
|
|
694
|
+
return run;
|
|
695
|
+
let status;
|
|
696
|
+
if (run.current_node === '') {
|
|
697
|
+
status = 'pending';
|
|
698
|
+
}
|
|
699
|
+
else if (run.auto_run) {
|
|
700
|
+
status = 'running';
|
|
701
|
+
}
|
|
702
|
+
else if (isRecentLegacyRetry(run, nowMs)) {
|
|
703
|
+
status = 'retrying';
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
status = 'pending';
|
|
707
|
+
}
|
|
708
|
+
return { ...run, status };
|
|
709
|
+
}
|
|
567
710
|
function isRetryableFailure(run, spec) {
|
|
568
711
|
if (run.status !== 'failed')
|
|
569
712
|
return false;
|
|
@@ -575,8 +718,9 @@ function isRetryableFailure(run, spec) {
|
|
|
575
718
|
return node.type !== 'error';
|
|
576
719
|
}
|
|
577
720
|
class RunManager {
|
|
578
|
-
constructor(runsDir = '.crewx/workflow-runs', projectRoot) {
|
|
721
|
+
constructor(runsDir = '.crewx/workflow-runs', projectRoot, deps) {
|
|
579
722
|
this.runsDir = runsDir;
|
|
723
|
+
this.sleep = deps?.sleep ?? sleep;
|
|
580
724
|
const base = projectRoot || process.env.CREWX_WORKSPACE || process.cwd();
|
|
581
725
|
this.resolvedDir = path.resolve(base, this.runsDir);
|
|
582
726
|
this.projectRoot = path.resolve(base);
|
|
@@ -596,7 +740,7 @@ class RunManager {
|
|
|
596
740
|
throw new Error(`Invalid execution ID: "${execId}"`);
|
|
597
741
|
}
|
|
598
742
|
}
|
|
599
|
-
|
|
743
|
+
readRunRaw(execId) {
|
|
600
744
|
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
601
745
|
if (!fs.existsSync(filePath))
|
|
602
746
|
return null;
|
|
@@ -607,16 +751,18 @@ class RunManager {
|
|
|
607
751
|
throw new Error(`Failed to parse execution file ${filePath}: ${e.message}`);
|
|
608
752
|
}
|
|
609
753
|
}
|
|
610
|
-
|
|
754
|
+
readRun(execId) {
|
|
755
|
+
const run = this.readRunRaw(execId);
|
|
756
|
+
return run ? normalizeRunStatus(run) : null;
|
|
757
|
+
}
|
|
758
|
+
isLegacyStatuslessStale(run) {
|
|
611
759
|
if (run.status !== undefined
|
|
612
760
|
|| run.auto_run
|
|
613
761
|
|| run.manual_resume
|
|
614
|
-
|| !run.current_node
|
|
762
|
+
|| !run.current_node
|
|
763
|
+
|| !run.audit?.length) {
|
|
615
764
|
return false;
|
|
616
765
|
}
|
|
617
|
-
const lastAudit = run.audit?.at(-1);
|
|
618
|
-
if (lastAudit?.type !== 'retry')
|
|
619
|
-
return false;
|
|
620
766
|
const updatedAt = Date.parse(run.updated_at);
|
|
621
767
|
if (!Number.isFinite(updatedAt) || Date.now() - updatedAt < exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS) {
|
|
622
768
|
return false;
|
|
@@ -629,16 +775,16 @@ class RunManager {
|
|
|
629
775
|
hasStaleRunner(run) {
|
|
630
776
|
if (run.auto_run)
|
|
631
777
|
return !isProcessAlive(run.auto_run.pid);
|
|
632
|
-
return this.
|
|
778
|
+
return this.isLegacyStatuslessStale(run);
|
|
633
779
|
}
|
|
634
780
|
recoverStaleRunner(execId) {
|
|
635
781
|
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
636
782
|
const lock = new file_lock_1.FileLock(filePath);
|
|
637
783
|
lock.acquire();
|
|
638
784
|
try {
|
|
639
|
-
const run = this.
|
|
785
|
+
const run = this.readRunRaw(execId);
|
|
640
786
|
if (!run || !this.hasStaleRunner(run))
|
|
641
|
-
return run;
|
|
787
|
+
return run ? normalizeRunStatus(run) : null;
|
|
642
788
|
if (isTerminalStatus(run.status)) {
|
|
643
789
|
delete run.auto_run;
|
|
644
790
|
delete run.manual_resume;
|
|
@@ -648,23 +794,18 @@ class RunManager {
|
|
|
648
794
|
const lease = run.auto_run;
|
|
649
795
|
const nodeId = lease?.node_id || run.current_node;
|
|
650
796
|
const now = new Date().toISOString();
|
|
651
|
-
|
|
652
|
-
|
|
797
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
798
|
+
failedNode: nodeId,
|
|
799
|
+
attempt: (0, failure_1.readNodeAttempt)(run, nodeId),
|
|
800
|
+
causeCode: 'INTERRUPTED',
|
|
801
|
+
exitCode: null,
|
|
802
|
+
signal: null,
|
|
803
|
+
errorOrigin: null,
|
|
804
|
+
});
|
|
805
|
+
assignRunFailure(run, failure, exports.WORKFLOW_RUN_INTERRUPTED_ERROR);
|
|
653
806
|
delete run.auto_run;
|
|
654
807
|
delete run.manual_resume;
|
|
655
|
-
run
|
|
656
|
-
run.audit.push({
|
|
657
|
-
exec_id: run.id,
|
|
658
|
-
node_id: nodeId,
|
|
659
|
-
type: 'interrupted',
|
|
660
|
-
summary: exports.WORKFLOW_RUN_INTERRUPTED_ERROR,
|
|
661
|
-
started_at: now,
|
|
662
|
-
ended_at: now,
|
|
663
|
-
duration_ms: 0,
|
|
664
|
-
exit_code: null,
|
|
665
|
-
error: exports.WORKFLOW_RUN_INTERRUPTED_ERROR,
|
|
666
|
-
trigger: 'auto',
|
|
667
|
-
});
|
|
808
|
+
appendFailureAudit(run, failure, 'interrupted', 'auto', now);
|
|
668
809
|
this.saveRun(run);
|
|
669
810
|
return run;
|
|
670
811
|
}
|
|
@@ -674,9 +815,9 @@ class RunManager {
|
|
|
674
815
|
}
|
|
675
816
|
loadRun(execId) {
|
|
676
817
|
this.validateExecId(execId);
|
|
677
|
-
const run = this.
|
|
818
|
+
const run = this.readRunRaw(execId);
|
|
678
819
|
if (!run || !this.hasStaleRunner(run))
|
|
679
|
-
return run;
|
|
820
|
+
return run ? normalizeRunStatus(run) : null;
|
|
680
821
|
return this.recoverStaleRunner(execId);
|
|
681
822
|
}
|
|
682
823
|
saveRun(run) {
|
|
@@ -721,6 +862,32 @@ class RunManager {
|
|
|
721
862
|
lock.release();
|
|
722
863
|
}
|
|
723
864
|
}
|
|
865
|
+
async reserveNodeAttempt(execId, nodeId) {
|
|
866
|
+
let reservedAttempt = 1;
|
|
867
|
+
await this.atomicUpdateAsync(execId, (run) => {
|
|
868
|
+
if (run.status === 'cancelled') {
|
|
869
|
+
reservedAttempt = (0, failure_1.readNodeAttempt)(run, nodeId);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const storedAttempt = run.node_attempts?.[nodeId];
|
|
873
|
+
const previousAttempt = typeof storedAttempt === 'number'
|
|
874
|
+
&& Number.isInteger(storedAttempt)
|
|
875
|
+
&& storedAttempt > 0
|
|
876
|
+
? storedAttempt
|
|
877
|
+
: 0;
|
|
878
|
+
reservedAttempt = previousAttempt + 1;
|
|
879
|
+
run.node_attempts = run.node_attempts ?? {};
|
|
880
|
+
run.node_attempts[nodeId] = reservedAttempt;
|
|
881
|
+
});
|
|
882
|
+
return reservedAttempt;
|
|
883
|
+
}
|
|
884
|
+
clearNodeAttempt(run, nodeId) {
|
|
885
|
+
if (!run.node_attempts)
|
|
886
|
+
return;
|
|
887
|
+
delete run.node_attempts[nodeId];
|
|
888
|
+
if (Object.keys(run.node_attempts).length === 0)
|
|
889
|
+
delete run.node_attempts;
|
|
890
|
+
}
|
|
724
891
|
loadWorkflowYaml(yamlPath) {
|
|
725
892
|
if (!fs.existsSync(yamlPath))
|
|
726
893
|
return null;
|
|
@@ -773,6 +940,7 @@ class RunManager {
|
|
|
773
940
|
completed_nodes: [],
|
|
774
941
|
state,
|
|
775
942
|
initial_state: { ...initialState, ...sets },
|
|
943
|
+
status: 'pending',
|
|
776
944
|
};
|
|
777
945
|
this.saveRun(run);
|
|
778
946
|
return run;
|
|
@@ -804,15 +972,15 @@ class RunManager {
|
|
|
804
972
|
}
|
|
805
973
|
const doc = this.loadWorkflowYaml(snapshot.workflow_file);
|
|
806
974
|
if (!doc) {
|
|
807
|
-
throw
|
|
975
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Failed to load workflow file: ${snapshot.workflow_file}`);
|
|
808
976
|
}
|
|
809
977
|
const spec = doc.workflows[snapshot.workflow_id];
|
|
810
978
|
if (!spec) {
|
|
811
|
-
throw
|
|
979
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Workflow "${snapshot.workflow_id}" not found in ${snapshot.workflow_file}`);
|
|
812
980
|
}
|
|
813
981
|
const nodeSpec = spec.nodes?.[nodeId];
|
|
814
982
|
if (!nodeSpec) {
|
|
815
|
-
throw
|
|
983
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" not found in workflow "${snapshot.workflow_id}"`);
|
|
816
984
|
}
|
|
817
985
|
const prevNode = snapshot.current_node;
|
|
818
986
|
const stateUpdates = {};
|
|
@@ -820,12 +988,20 @@ class RunManager {
|
|
|
820
988
|
let currentNode = nodeId;
|
|
821
989
|
let status;
|
|
822
990
|
let failureMessage;
|
|
991
|
+
let failureCode;
|
|
823
992
|
let nodeTaskId;
|
|
824
993
|
const trigger = opts?.trigger ?? 'manual';
|
|
825
994
|
const startedAt = new Date();
|
|
826
|
-
let nodeStdout = '';
|
|
827
|
-
let nodeStderr = '';
|
|
828
995
|
let nodeExitCode = null;
|
|
996
|
+
let nodeSignal = null;
|
|
997
|
+
let nodeErrorOrigin = null;
|
|
998
|
+
let processFailure;
|
|
999
|
+
const recordProcessResult = (result) => {
|
|
1000
|
+
nodeExitCode = result.status;
|
|
1001
|
+
nodeSignal = result.signal;
|
|
1002
|
+
nodeErrorOrigin = result.errorOrigin;
|
|
1003
|
+
processFailure = toProcessFailureMetadata(result);
|
|
1004
|
+
};
|
|
829
1005
|
const buildAudit = (error) => {
|
|
830
1006
|
const endedAt = new Date();
|
|
831
1007
|
return {
|
|
@@ -841,24 +1017,27 @@ class RunManager {
|
|
|
841
1017
|
trigger,
|
|
842
1018
|
};
|
|
843
1019
|
};
|
|
1020
|
+
const attempt = await this.reserveNodeAttempt(execId, nodeId);
|
|
844
1021
|
try {
|
|
845
1022
|
switch (nodeSpec.type) {
|
|
846
1023
|
case 'agent_task': {
|
|
847
1024
|
if (!nodeSpec.agent) {
|
|
848
|
-
throw
|
|
1025
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" is agent_task but has no agent defined`);
|
|
849
1026
|
}
|
|
850
1027
|
const agentCtx = { ...snapshot.state, state: snapshot.state };
|
|
851
1028
|
const resolvedAgent = interpolateTemplate(nodeSpec.agent, agentCtx).trim();
|
|
852
1029
|
if (!resolvedAgent) {
|
|
853
|
-
throw
|
|
1030
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": assignee not resolved (agent field "${nodeSpec.agent}" interpolated to empty)`);
|
|
854
1031
|
}
|
|
855
1032
|
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, agentCtx) : undefined, this.projectRoot);
|
|
856
|
-
if (!cwdCheck.ok)
|
|
857
|
-
throw
|
|
1033
|
+
if (!cwdCheck.ok) {
|
|
1034
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cwdCheck.error}`);
|
|
1035
|
+
}
|
|
858
1036
|
const nodeCwd = cwdCheck.resolved;
|
|
859
1037
|
const rootCheck = (0, shell_security_1.normalizeCwd)(undefined, this.projectRoot);
|
|
860
|
-
if (!rootCheck.ok)
|
|
861
|
-
throw
|
|
1038
|
+
if (!rootCheck.ok) {
|
|
1039
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${rootCheck.error}`);
|
|
1040
|
+
}
|
|
862
1041
|
const workspaceRoot = rootCheck.resolved;
|
|
863
1042
|
const input = nodeSpec.input
|
|
864
1043
|
? interpolateTemplate(nodeSpec.input, agentCtx)
|
|
@@ -880,12 +1059,15 @@ class RunManager {
|
|
|
880
1059
|
break;
|
|
881
1060
|
}
|
|
882
1061
|
console.log(`Executing: @${resolvedAgent} (${mode} mode)`);
|
|
883
|
-
const crewxCli = process.env.CREWX_CLI || 'npx crewx';
|
|
884
|
-
const cliParts = crewxCli.split(/\s+/);
|
|
885
|
-
const bin = cliParts[0];
|
|
886
|
-
const baseArgs = cliParts.slice(1);
|
|
887
1062
|
const nodeTimeout = Number(process.env[mode === 'q' ? 'CREWX_TIMEOUT_QUERY' : 'CREWX_TIMEOUT_EXECUTE']) || 8 * 3600000;
|
|
888
|
-
const
|
|
1063
|
+
const childInvocation = resolveCrewxChildInvocation({
|
|
1064
|
+
...process.env,
|
|
1065
|
+
CREWX_WORKSPACE: workspaceRoot,
|
|
1066
|
+
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
1067
|
+
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
1068
|
+
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
1069
|
+
}, nodeCwd);
|
|
1070
|
+
const wfArgs = [...childInvocation.argv.slice(1), mode, prompt];
|
|
889
1071
|
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
890
1072
|
wfArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
891
1073
|
}
|
|
@@ -898,33 +1080,27 @@ class RunManager {
|
|
|
898
1080
|
};
|
|
899
1081
|
wfArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
900
1082
|
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
901
|
-
command:
|
|
1083
|
+
command: childInvocation.argv[0],
|
|
1084
|
+
env: childInvocation.env,
|
|
1085
|
+
cwd: nodeCwd,
|
|
902
1086
|
allowShellFallback: true,
|
|
903
1087
|
packageName: 'crewx',
|
|
904
1088
|
});
|
|
905
1089
|
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, wfArgs);
|
|
906
1090
|
const result = await runProcess(invocation.command, invocation.argv, {
|
|
907
1091
|
cwd: nodeCwd,
|
|
908
|
-
env:
|
|
909
|
-
...process.env,
|
|
910
|
-
CREWX_WORKSPACE: workspaceRoot,
|
|
911
|
-
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
912
|
-
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
913
|
-
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
914
|
-
},
|
|
1092
|
+
env: childInvocation.env,
|
|
915
1093
|
timeout: nodeTimeout,
|
|
916
1094
|
shell: invocation.shell ?? false,
|
|
917
1095
|
windowsHide: invocation.windowsHide,
|
|
918
1096
|
});
|
|
919
|
-
|
|
920
|
-
nodeStderr = result.stderr;
|
|
921
|
-
nodeExitCode = result.status;
|
|
922
|
-
if (result.error) {
|
|
923
|
-
throw result.error;
|
|
924
|
-
}
|
|
1097
|
+
recordProcessResult(result);
|
|
925
1098
|
if (result.timedOut) {
|
|
926
1099
|
throw new Error(`Agent execution timed out after ${nodeTimeout}ms`);
|
|
927
1100
|
}
|
|
1101
|
+
if (result.error) {
|
|
1102
|
+
throw result.error;
|
|
1103
|
+
}
|
|
928
1104
|
const taskIdMatch = result.stderr.match(/crewx kill (tsk_\w+)/);
|
|
929
1105
|
nodeTaskId = taskIdMatch?.[1];
|
|
930
1106
|
if (result.status !== 0) {
|
|
@@ -949,7 +1125,7 @@ class RunManager {
|
|
|
949
1125
|
`\n\n## ⚠️ PREVIOUS OUTPUT FAILED JSON.parse: ${lastError.message}\n` +
|
|
950
1126
|
`Return STRICT JSON ONLY. First char "{", last char "}". No prose, no fences.`;
|
|
951
1127
|
const retryPrompt = `@${resolvedAgent} ${retryInput}`;
|
|
952
|
-
const retryArgs = [...
|
|
1128
|
+
const retryArgs = [...childInvocation.argv.slice(1), mode, retryPrompt];
|
|
953
1129
|
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
954
1130
|
retryArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
955
1131
|
}
|
|
@@ -957,24 +1133,16 @@ class RunManager {
|
|
|
957
1133
|
const retryInvocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, retryArgs);
|
|
958
1134
|
const retryResult = await runProcess(retryInvocation.command, retryInvocation.argv, {
|
|
959
1135
|
cwd: nodeCwd,
|
|
960
|
-
env:
|
|
961
|
-
...process.env,
|
|
962
|
-
CREWX_WORKSPACE: workspaceRoot,
|
|
963
|
-
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
964
|
-
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
965
|
-
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
966
|
-
},
|
|
1136
|
+
env: childInvocation.env,
|
|
967
1137
|
timeout: nodeTimeout,
|
|
968
1138
|
shell: retryInvocation.shell ?? false,
|
|
969
1139
|
windowsHide: retryInvocation.windowsHide,
|
|
970
1140
|
});
|
|
971
|
-
|
|
972
|
-
nodeStderr = retryResult.stderr;
|
|
973
|
-
nodeExitCode = retryResult.status;
|
|
974
|
-
if (retryResult.error)
|
|
975
|
-
throw retryResult.error;
|
|
1141
|
+
recordProcessResult(retryResult);
|
|
976
1142
|
if (retryResult.timedOut)
|
|
977
1143
|
throw new Error(`Agent retry timed out after ${nodeTimeout}ms`);
|
|
1144
|
+
if (retryResult.error)
|
|
1145
|
+
throw retryResult.error;
|
|
978
1146
|
if (retryResult.status !== 0) {
|
|
979
1147
|
throw new Error(`Agent retry failed (exit ${retryResult.status}): ${retryResult.stderr.trim()}`);
|
|
980
1148
|
}
|
|
@@ -996,7 +1164,7 @@ class RunManager {
|
|
|
996
1164
|
}
|
|
997
1165
|
}
|
|
998
1166
|
if (parsed === null) {
|
|
999
|
-
throw
|
|
1167
|
+
throw (0, failure_1.createWorkflowFailureError)('OUTPUT_INVALID', `Node "${nodeId}" output_format=json parse failed after ${retryMax + 1} attempt(s): ${lastError.message}`);
|
|
1000
1168
|
}
|
|
1001
1169
|
stateUpdates[nodeSpec.output] = parsed;
|
|
1002
1170
|
console.log(`Result stored in state.${nodeSpec.output} (JSON object)`);
|
|
@@ -1010,10 +1178,10 @@ class RunManager {
|
|
|
1010
1178
|
}
|
|
1011
1179
|
case 'skill_task': {
|
|
1012
1180
|
if (!nodeSpec.skill) {
|
|
1013
|
-
throw
|
|
1181
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" is skill_task but has no skill defined`);
|
|
1014
1182
|
}
|
|
1015
1183
|
if (!shell_security_1.SKILL_NAME_RE.test(nodeSpec.skill)) {
|
|
1016
|
-
throw
|
|
1184
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" has invalid skill name "${nodeSpec.skill}" (must match ^[a-z0-9][a-z0-9-]*$)`);
|
|
1017
1185
|
}
|
|
1018
1186
|
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
1019
1187
|
const skillArgs = (nodeSpec.args ?? []).map((a) => interpolateTemplate(String(a), ctx));
|
|
@@ -1021,11 +1189,13 @@ class RunManager {
|
|
|
1021
1189
|
? interpolateTemplate(String(nodeSpec.stdin), ctx)
|
|
1022
1190
|
: undefined;
|
|
1023
1191
|
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
1024
|
-
if (!cwdCheck.ok)
|
|
1025
|
-
throw
|
|
1192
|
+
if (!cwdCheck.ok) {
|
|
1193
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cwdCheck.error}`);
|
|
1194
|
+
}
|
|
1026
1195
|
const skillRootCheck = (0, shell_security_1.normalizeCwd)(undefined, this.projectRoot);
|
|
1027
|
-
if (!skillRootCheck.ok)
|
|
1028
|
-
throw
|
|
1196
|
+
if (!skillRootCheck.ok) {
|
|
1197
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${skillRootCheck.error}`);
|
|
1198
|
+
}
|
|
1029
1199
|
const skillWorkspaceRoot = skillRootCheck.resolved;
|
|
1030
1200
|
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
1031
1201
|
if (opts?.dryRun) {
|
|
@@ -1033,38 +1203,35 @@ class RunManager {
|
|
|
1033
1203
|
break;
|
|
1034
1204
|
}
|
|
1035
1205
|
console.log(`Executing skill: ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1206
|
+
const childInvocation = resolveCrewxChildInvocation({
|
|
1207
|
+
...process.env,
|
|
1208
|
+
CREWX_WORKSPACE: skillWorkspaceRoot,
|
|
1209
|
+
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
1210
|
+
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
1211
|
+
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
1212
|
+
}, cwdCheck.resolved);
|
|
1213
|
+
const skillArgv = [...childInvocation.argv.slice(1), 'skill', nodeSpec.skill, ...skillArgs];
|
|
1041
1214
|
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
1042
|
-
command:
|
|
1215
|
+
command: childInvocation.argv[0],
|
|
1216
|
+
env: childInvocation.env,
|
|
1217
|
+
cwd: cwdCheck.resolved,
|
|
1043
1218
|
allowShellFallback: true,
|
|
1044
1219
|
packageName: 'crewx',
|
|
1045
1220
|
});
|
|
1046
1221
|
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, skillArgv);
|
|
1047
1222
|
const result = await runProcess(invocation.command, invocation.argv, {
|
|
1048
1223
|
cwd: cwdCheck.resolved,
|
|
1049
|
-
env:
|
|
1050
|
-
...process.env,
|
|
1051
|
-
CREWX_WORKSPACE: skillWorkspaceRoot,
|
|
1052
|
-
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
1053
|
-
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
1054
|
-
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
1055
|
-
},
|
|
1224
|
+
env: childInvocation.env,
|
|
1056
1225
|
timeout,
|
|
1057
1226
|
shell: invocation.shell ?? false,
|
|
1058
1227
|
windowsHide: invocation.windowsHide,
|
|
1059
1228
|
stdin: skillStdin,
|
|
1060
1229
|
});
|
|
1061
|
-
|
|
1062
|
-
nodeStderr = result.stderr;
|
|
1063
|
-
nodeExitCode = result.status;
|
|
1064
|
-
if (result.error)
|
|
1065
|
-
throw result.error;
|
|
1230
|
+
recordProcessResult(result);
|
|
1066
1231
|
if (result.timedOut)
|
|
1067
1232
|
throw new Error(`skill_task "${nodeId}" timed out after ${timeout}ms`);
|
|
1233
|
+
if (result.error)
|
|
1234
|
+
throw result.error;
|
|
1068
1235
|
if (result.status !== 0) {
|
|
1069
1236
|
throw new Error(`skill_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
1070
1237
|
}
|
|
@@ -1078,25 +1245,29 @@ class RunManager {
|
|
|
1078
1245
|
const shellEnvEnabled = process.env.CREWX_WORKFLOW_SHELL === '1';
|
|
1079
1246
|
const shellMetaAllowed = spec.metadata?.shell_task_allowed === true;
|
|
1080
1247
|
if (!shellEnvEnabled || !shellMetaAllowed) {
|
|
1081
|
-
throw
|
|
1248
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', 'shell_task is disabled. Set CREWX_WORKFLOW_SHELL=1 and metadata.shell_task_allowed=true to enable.');
|
|
1082
1249
|
}
|
|
1083
1250
|
const cmdCheck = (0, shell_security_1.validateShellCommand)(nodeSpec.command);
|
|
1084
|
-
if (!cmdCheck.ok)
|
|
1085
|
-
throw
|
|
1251
|
+
if (!cmdCheck.ok) {
|
|
1252
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cmdCheck.error}`);
|
|
1253
|
+
}
|
|
1086
1254
|
const envCheck = (0, shell_security_1.validateEnvKeys)(nodeSpec.env);
|
|
1087
|
-
if (!envCheck.ok)
|
|
1088
|
-
throw
|
|
1255
|
+
if (!envCheck.ok) {
|
|
1256
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${envCheck.error}`);
|
|
1257
|
+
}
|
|
1089
1258
|
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
1090
1259
|
const command = nodeSpec.command.map((c) => interpolateTemplate(c, ctx));
|
|
1091
1260
|
const postCheck = (0, shell_security_1.validateShellCommand)(command);
|
|
1092
|
-
if (!postCheck.ok)
|
|
1093
|
-
throw
|
|
1261
|
+
if (!postCheck.ok) {
|
|
1262
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${postCheck.error}`);
|
|
1263
|
+
}
|
|
1094
1264
|
const interpolatedEnv = nodeSpec.env
|
|
1095
1265
|
? Object.fromEntries(Object.entries(nodeSpec.env).map(([k, v]) => [k, interpolateTemplate(String(v), ctx)]))
|
|
1096
1266
|
: undefined;
|
|
1097
1267
|
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
1098
|
-
if (!cwdCheck.ok)
|
|
1099
|
-
throw
|
|
1268
|
+
if (!cwdCheck.ok) {
|
|
1269
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cwdCheck.error}`);
|
|
1270
|
+
}
|
|
1100
1271
|
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
1101
1272
|
if (opts?.dryRun) {
|
|
1102
1273
|
console.log(`[dry-run] Would execute: ${command.join(' ')}`);
|
|
@@ -1117,13 +1288,11 @@ class RunManager {
|
|
|
1117
1288
|
shell: false,
|
|
1118
1289
|
stdin: shellStdin,
|
|
1119
1290
|
});
|
|
1120
|
-
|
|
1121
|
-
nodeStderr = result.stderr;
|
|
1122
|
-
nodeExitCode = result.status;
|
|
1123
|
-
if (result.error)
|
|
1124
|
-
throw result.error;
|
|
1291
|
+
recordProcessResult(result);
|
|
1125
1292
|
if (result.timedOut)
|
|
1126
1293
|
throw new Error(`shell_task "${nodeId}" timed out after ${timeout}ms`);
|
|
1294
|
+
if (result.error)
|
|
1295
|
+
throw result.error;
|
|
1127
1296
|
if (result.status !== 0) {
|
|
1128
1297
|
throw new Error(`shell_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
1129
1298
|
}
|
|
@@ -1135,13 +1304,13 @@ class RunManager {
|
|
|
1135
1304
|
}
|
|
1136
1305
|
case 'branch': {
|
|
1137
1306
|
if (!nodeSpec.condition) {
|
|
1138
|
-
throw
|
|
1307
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Branch node "${nodeId}" has no condition`);
|
|
1139
1308
|
}
|
|
1140
1309
|
const condResult = String(safeEvaluateBranchCondition(String(nodeSpec.condition), snapshot.state));
|
|
1141
1310
|
const branches = nodeSpec.branches;
|
|
1142
1311
|
const targetNode = branches?.[condResult] ?? nodeSpec.default;
|
|
1143
1312
|
if (!targetNode) {
|
|
1144
|
-
throw
|
|
1313
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Branch "${nodeId}": no matching branch for "${condResult}" and no default`);
|
|
1145
1314
|
}
|
|
1146
1315
|
console.log(`Branch "${nodeId}": condition="${nodeSpec.condition}" → ${condResult} → ${targetNode}`);
|
|
1147
1316
|
nodesToComplete.push(nodeId);
|
|
@@ -1167,6 +1336,7 @@ class RunManager {
|
|
|
1167
1336
|
}
|
|
1168
1337
|
case 'error': {
|
|
1169
1338
|
status = 'failed';
|
|
1339
|
+
failureCode = 'UNKNOWN';
|
|
1170
1340
|
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
1171
1341
|
failureMessage = nodeSpec.message
|
|
1172
1342
|
? interpolateTemplate(nodeSpec.message, ctx)
|
|
@@ -1176,7 +1346,7 @@ class RunManager {
|
|
|
1176
1346
|
}
|
|
1177
1347
|
case 'expression': {
|
|
1178
1348
|
if (!nodeSpec.set || typeof nodeSpec.set !== 'object') {
|
|
1179
|
-
throw
|
|
1349
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Expression node "${nodeId}" requires "set" field`);
|
|
1180
1350
|
}
|
|
1181
1351
|
for (const [key, expr] of Object.entries(nodeSpec.set)) {
|
|
1182
1352
|
const result = safeEvaluate(String(expr), snapshot.state);
|
|
@@ -1207,26 +1377,46 @@ class RunManager {
|
|
|
1207
1377
|
}
|
|
1208
1378
|
}
|
|
1209
1379
|
catch (e) {
|
|
1210
|
-
const errMessage = e
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
const
|
|
1380
|
+
const errMessage = getErrorMessage(e);
|
|
1381
|
+
const causeCode = (0, failure_1.classifyWorkflowFailure)({
|
|
1382
|
+
errorCode: (0, failure_1.getWorkflowFailureCode)(e),
|
|
1383
|
+
process: processFailure,
|
|
1384
|
+
});
|
|
1385
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1386
|
+
failedNode: nodeId,
|
|
1387
|
+
attempt,
|
|
1388
|
+
causeCode,
|
|
1389
|
+
exitCode: causeCode === 'NODE_EXIT_NONZERO' && typeof nodeExitCode === 'number'
|
|
1390
|
+
? nodeExitCode
|
|
1391
|
+
: null,
|
|
1392
|
+
signal: causeCode === 'INTERRUPTED' ? nodeSignal : null,
|
|
1393
|
+
errorOrigin: nodeErrorOrigin,
|
|
1394
|
+
timedOut: processFailure?.timedOut ?? false,
|
|
1395
|
+
});
|
|
1396
|
+
const auditEntry = buildAudit(failure.causeMessage);
|
|
1216
1397
|
await this.atomicUpdateAsync(execId, (run) => {
|
|
1217
1398
|
if (run.status === 'cancelled')
|
|
1218
1399
|
return;
|
|
1219
1400
|
Object.assign(run.state, stateUpdates);
|
|
1220
1401
|
delete run.manual_resume;
|
|
1221
1402
|
run.current_node = nodeId;
|
|
1222
|
-
run
|
|
1223
|
-
run.error = errMessage;
|
|
1403
|
+
assignRunFailure(run, failure, errMessage);
|
|
1224
1404
|
run.audit = run.audit ?? [];
|
|
1225
1405
|
run.audit.push(auditEntry);
|
|
1226
1406
|
});
|
|
1227
1407
|
throw e;
|
|
1228
1408
|
}
|
|
1229
|
-
const
|
|
1409
|
+
const successFailure = failureCode
|
|
1410
|
+
? (0, failure_1.buildNodeFailure)({
|
|
1411
|
+
failedNode: nodeId,
|
|
1412
|
+
attempt,
|
|
1413
|
+
causeCode: failureCode,
|
|
1414
|
+
exitCode: null,
|
|
1415
|
+
signal: null,
|
|
1416
|
+
errorOrigin: null,
|
|
1417
|
+
})
|
|
1418
|
+
: null;
|
|
1419
|
+
const successAudit = buildAudit(successFailure?.causeMessage);
|
|
1230
1420
|
return this.atomicUpdateAsync(execId, (run) => {
|
|
1231
1421
|
if (run.status === 'cancelled')
|
|
1232
1422
|
return;
|
|
@@ -1241,18 +1431,20 @@ class RunManager {
|
|
|
1241
1431
|
run.completed_nodes.push(prevNode);
|
|
1242
1432
|
}
|
|
1243
1433
|
run.current_node = currentNode;
|
|
1244
|
-
if (
|
|
1245
|
-
run
|
|
1246
|
-
if (status !== 'failed') {
|
|
1247
|
-
delete run.error;
|
|
1248
|
-
}
|
|
1434
|
+
if (successFailure) {
|
|
1435
|
+
assignRunFailure(run, successFailure, failureMessage ?? successFailure.causeMessage);
|
|
1249
1436
|
}
|
|
1250
|
-
else
|
|
1251
|
-
run.status = 'running';
|
|
1437
|
+
else {
|
|
1252
1438
|
delete run.error;
|
|
1439
|
+
delete run.last_failure;
|
|
1440
|
+
this.clearNodeAttempt(run, nodeId);
|
|
1441
|
+
if (status) {
|
|
1442
|
+
run.status = status;
|
|
1443
|
+
}
|
|
1444
|
+
else if (run.status !== 'completed') {
|
|
1445
|
+
run.status = 'running';
|
|
1446
|
+
}
|
|
1253
1447
|
}
|
|
1254
|
-
if (failureMessage)
|
|
1255
|
-
run.error = failureMessage;
|
|
1256
1448
|
if (nodeTaskId) {
|
|
1257
1449
|
run.tasks = run.tasks ?? {};
|
|
1258
1450
|
run.tasks[nodeId] = nodeTaskId;
|
|
@@ -1262,13 +1454,19 @@ class RunManager {
|
|
|
1262
1454
|
});
|
|
1263
1455
|
}
|
|
1264
1456
|
async claimAutoRunLease(execId, nodeId, preferredToken) {
|
|
1457
|
+
let claimed = false;
|
|
1265
1458
|
const updated = await this.atomicUpdateAsync(execId, (run) => {
|
|
1459
|
+
if (isTerminalStatus(run.status))
|
|
1460
|
+
return;
|
|
1266
1461
|
const existing = run.auto_run;
|
|
1267
1462
|
const ownedByThisProcess = existing?.pid === process.pid && typeof existing.token === 'string';
|
|
1268
1463
|
if (existing && !ownedByThisProcess && isProcessAlive(existing.pid)) {
|
|
1269
1464
|
throw new Error(`Workflow run "${execId}" is already owned by another runner process`);
|
|
1270
1465
|
}
|
|
1271
1466
|
const token = preferredToken ?? (ownedByThisProcess ? existing.token : (0, sdk_1.generateId)('wfr'));
|
|
1467
|
+
if (!isTerminalStatus(run.status)) {
|
|
1468
|
+
run.status = 'running';
|
|
1469
|
+
}
|
|
1272
1470
|
run.auto_run = {
|
|
1273
1471
|
pid: process.pid,
|
|
1274
1472
|
token,
|
|
@@ -1278,8 +1476,9 @@ class RunManager {
|
|
|
1278
1476
|
: new Date().toISOString(),
|
|
1279
1477
|
};
|
|
1280
1478
|
delete run.manual_resume;
|
|
1479
|
+
claimed = true;
|
|
1281
1480
|
});
|
|
1282
|
-
return updated.auto_run
|
|
1481
|
+
return claimed ? updated.auto_run?.token ?? null : null;
|
|
1283
1482
|
}
|
|
1284
1483
|
async releaseAutoRunLease(execId, token) {
|
|
1285
1484
|
await this.atomicUpdateAsync(execId, (run) => {
|
|
@@ -1287,6 +1486,145 @@ class RunManager {
|
|
|
1287
1486
|
delete run.auto_run;
|
|
1288
1487
|
});
|
|
1289
1488
|
}
|
|
1489
|
+
async failAutoConfiguration(execId, nodeId, reason) {
|
|
1490
|
+
return this.atomicUpdateAsync(execId, (run) => {
|
|
1491
|
+
if (run.status === 'cancelled')
|
|
1492
|
+
return;
|
|
1493
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1494
|
+
failedNode: nodeId,
|
|
1495
|
+
attempt: (0, failure_1.readNodeAttempt)(run, nodeId),
|
|
1496
|
+
causeCode: 'CONFIG_INVALID',
|
|
1497
|
+
exitCode: null,
|
|
1498
|
+
signal: null,
|
|
1499
|
+
errorOrigin: null,
|
|
1500
|
+
});
|
|
1501
|
+
assignRunFailure(run, failure, reason);
|
|
1502
|
+
appendFailureAudit(run, failure, 'config', 'auto');
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
async reserveAutoRetry(execId, nodeId, runnerToken, policy, delayMs) {
|
|
1506
|
+
let reserved = false;
|
|
1507
|
+
const run = await this.atomicUpdateAsync(execId, (draft) => {
|
|
1508
|
+
if (draft.status === 'cancelled')
|
|
1509
|
+
return;
|
|
1510
|
+
if (draft.status !== 'failed')
|
|
1511
|
+
return;
|
|
1512
|
+
if (draft.auto_run?.token !== runnerToken)
|
|
1513
|
+
return;
|
|
1514
|
+
const decision = (0, retry_policy_1.decideRetry)({ policy, run: draft, nodeId });
|
|
1515
|
+
if (!decision.retry || decision.attempt === undefined)
|
|
1516
|
+
return;
|
|
1517
|
+
const now = new Date().toISOString();
|
|
1518
|
+
draft.status = 'retrying';
|
|
1519
|
+
delete draft.error;
|
|
1520
|
+
delete draft.manual_resume;
|
|
1521
|
+
draft.audit = draft.audit ?? [];
|
|
1522
|
+
draft.audit.push({
|
|
1523
|
+
exec_id: draft.id,
|
|
1524
|
+
node_id: nodeId,
|
|
1525
|
+
type: 'retry',
|
|
1526
|
+
summary: `auto retry ${decision.attempt + 1}/${policy.max + 1} in ${delayMs}ms`,
|
|
1527
|
+
started_at: now,
|
|
1528
|
+
ended_at: now,
|
|
1529
|
+
duration_ms: 0,
|
|
1530
|
+
exit_code: null,
|
|
1531
|
+
trigger: 'auto',
|
|
1532
|
+
});
|
|
1533
|
+
reserved = true;
|
|
1534
|
+
});
|
|
1535
|
+
return { run, reserved };
|
|
1536
|
+
}
|
|
1537
|
+
async waitForRetry(execId, runnerToken, delayMs) {
|
|
1538
|
+
const inspect = () => {
|
|
1539
|
+
const run = this.loadRun(execId);
|
|
1540
|
+
if (!run)
|
|
1541
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1542
|
+
if (run.status === 'cancelled') {
|
|
1543
|
+
return { run, outcome: 'cancelled', reason: 'run cancelled during retry delay' };
|
|
1544
|
+
}
|
|
1545
|
+
if (run.auto_run?.token !== runnerToken) {
|
|
1546
|
+
return { run, outcome: 'paused', reason: 'auto-run lease lost during retry delay' };
|
|
1547
|
+
}
|
|
1548
|
+
if (run.status !== 'retrying') {
|
|
1549
|
+
return { run, outcome: 'paused', reason: 'run state changed during retry delay' };
|
|
1550
|
+
}
|
|
1551
|
+
return null;
|
|
1552
|
+
};
|
|
1553
|
+
let remaining = Math.max(0, delayMs);
|
|
1554
|
+
while (remaining > 0) {
|
|
1555
|
+
const slice = Math.min(remaining, RETRY_WAIT_SLICE_MS);
|
|
1556
|
+
await this.sleep(slice);
|
|
1557
|
+
remaining -= slice;
|
|
1558
|
+
const stopped = inspect();
|
|
1559
|
+
if (stopped)
|
|
1560
|
+
return stopped;
|
|
1561
|
+
}
|
|
1562
|
+
const stopped = inspect();
|
|
1563
|
+
if (stopped)
|
|
1564
|
+
return stopped;
|
|
1565
|
+
const run = this.loadRun(execId);
|
|
1566
|
+
if (!run)
|
|
1567
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1568
|
+
return { run, outcome: 'ready' };
|
|
1569
|
+
}
|
|
1570
|
+
async runNodeWithRetry(execId, nodeId, runnerToken, policy) {
|
|
1571
|
+
let token = runnerToken;
|
|
1572
|
+
while (true) {
|
|
1573
|
+
try {
|
|
1574
|
+
const run = await this.executeNode(execId, nodeId, { trigger: 'auto' });
|
|
1575
|
+
return { run, runnerToken: token, outcome: 'success' };
|
|
1576
|
+
}
|
|
1577
|
+
catch (caught) {
|
|
1578
|
+
const run = this.loadRun(execId);
|
|
1579
|
+
if (!run)
|
|
1580
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1581
|
+
if (run.status === 'cancelled') {
|
|
1582
|
+
return { run, runnerToken: token, outcome: 'cancelled', reason: run.error };
|
|
1583
|
+
}
|
|
1584
|
+
if (run.status !== 'failed' || !policy) {
|
|
1585
|
+
return { run, runnerToken: token, outcome: run.status === 'failed' ? 'failed' : 'paused', reason: getErrorMessage(caught) };
|
|
1586
|
+
}
|
|
1587
|
+
const decision = (0, retry_policy_1.decideRetry)({ policy, run, nodeId });
|
|
1588
|
+
if (!decision.retry || decision.attempt === undefined) {
|
|
1589
|
+
return { run, runnerToken: token, outcome: 'failed', reason: getErrorMessage(caught) };
|
|
1590
|
+
}
|
|
1591
|
+
const delayMs = (0, retry_policy_1.computeRetryDelayMs)(policy, decision.attempt);
|
|
1592
|
+
const reservation = await this.reserveAutoRetry(execId, nodeId, token, policy, delayMs);
|
|
1593
|
+
if (!reservation.reserved) {
|
|
1594
|
+
const latest = reservation.run;
|
|
1595
|
+
if (latest.status === 'cancelled') {
|
|
1596
|
+
return { run: latest, runnerToken: token, outcome: 'cancelled', reason: latest.error };
|
|
1597
|
+
}
|
|
1598
|
+
return {
|
|
1599
|
+
run: latest,
|
|
1600
|
+
runnerToken: token,
|
|
1601
|
+
outcome: latest.status === 'failed' ? 'failed' : 'paused',
|
|
1602
|
+
reason: getErrorMessage(caught),
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
const waited = await this.waitForRetry(execId, token, delayMs);
|
|
1606
|
+
if (waited.outcome !== 'ready') {
|
|
1607
|
+
return { run: waited.run, runnerToken: token, outcome: waited.outcome, reason: waited.reason };
|
|
1608
|
+
}
|
|
1609
|
+
const nextToken = await this.claimAutoRunLease(execId, nodeId, token);
|
|
1610
|
+
if (!nextToken) {
|
|
1611
|
+
const latest = this.loadRun(execId);
|
|
1612
|
+
if (!latest)
|
|
1613
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1614
|
+
if (latest.status === 'cancelled') {
|
|
1615
|
+
return { run: latest, runnerToken: token, outcome: 'cancelled', reason: latest.error };
|
|
1616
|
+
}
|
|
1617
|
+
return {
|
|
1618
|
+
run: latest,
|
|
1619
|
+
runnerToken: token,
|
|
1620
|
+
outcome: 'paused',
|
|
1621
|
+
reason: 'run stopped before retry lease could be reclaimed',
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
token = nextToken;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1290
1628
|
async runAuto(execId) {
|
|
1291
1629
|
const leaseKey = `${this.resolvedDir}::${execId}`;
|
|
1292
1630
|
if (inFlightAutoRuns.has(leaseKey)) {
|
|
@@ -1306,11 +1644,12 @@ class RunManager {
|
|
|
1306
1644
|
runnerToken = initial.auto_run.token;
|
|
1307
1645
|
}
|
|
1308
1646
|
const doc = this.loadWorkflowYaml(initial.workflow_file);
|
|
1309
|
-
if (!doc)
|
|
1310
|
-
throw
|
|
1647
|
+
if (!doc) {
|
|
1648
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Failed to load workflow file: ${initial.workflow_file}`);
|
|
1649
|
+
}
|
|
1311
1650
|
const spec = doc.workflows[initial.workflow_id];
|
|
1312
1651
|
if (!spec) {
|
|
1313
|
-
throw
|
|
1652
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Workflow "${initial.workflow_id}" not found in ${initial.workflow_file}`);
|
|
1314
1653
|
}
|
|
1315
1654
|
const executed = [];
|
|
1316
1655
|
if (initial.status === 'completed' || initial.status === 'failed' || initial.status === 'cancelled') {
|
|
@@ -1335,17 +1674,46 @@ class RunManager {
|
|
|
1335
1674
|
if (run.status === 'cancelled')
|
|
1336
1675
|
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1337
1676
|
const nodeId = run.current_node || firstNode;
|
|
1338
|
-
if (!nodeId)
|
|
1339
|
-
|
|
1677
|
+
if (!nodeId) {
|
|
1678
|
+
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1679
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1680
|
+
failedNode: r.current_node || '',
|
|
1681
|
+
attempt: (0, failure_1.readNodeAttempt)(r, r.current_node || ''),
|
|
1682
|
+
causeCode: 'CONFIG_INVALID',
|
|
1683
|
+
exitCode: null,
|
|
1684
|
+
signal: null,
|
|
1685
|
+
errorOrigin: null,
|
|
1686
|
+
});
|
|
1687
|
+
assignRunFailure(r, failure, 'workflow has no nodes');
|
|
1688
|
+
appendFailureAudit(r, failure, 'config', 'auto');
|
|
1689
|
+
});
|
|
1690
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1691
|
+
}
|
|
1340
1692
|
const node = spec.nodes[nodeId];
|
|
1341
1693
|
if (!node) {
|
|
1342
1694
|
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1343
1695
|
r.current_node = nodeId;
|
|
1344
|
-
|
|
1345
|
-
|
|
1696
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1697
|
+
failedNode: nodeId,
|
|
1698
|
+
attempt: (0, failure_1.readNodeAttempt)(r, nodeId),
|
|
1699
|
+
causeCode: 'CONFIG_INVALID',
|
|
1700
|
+
exitCode: null,
|
|
1701
|
+
signal: null,
|
|
1702
|
+
errorOrigin: null,
|
|
1703
|
+
});
|
|
1704
|
+
assignRunFailure(r, failure, `node "${nodeId}" not found in workflow`);
|
|
1705
|
+
appendFailureAudit(r, failure, 'config', 'auto');
|
|
1346
1706
|
});
|
|
1347
1707
|
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1348
1708
|
}
|
|
1709
|
+
const parsedRetryPolicy = (0, retry_policy_1.parseRetryPolicy)(node);
|
|
1710
|
+
if (!parsedRetryPolicy.ok) {
|
|
1711
|
+
run = await this.failAutoConfiguration(execId, nodeId, parsedRetryPolicy.reason);
|
|
1712
|
+
if (run.status === 'cancelled') {
|
|
1713
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1714
|
+
}
|
|
1715
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1716
|
+
}
|
|
1349
1717
|
if (node.type === 'approval') {
|
|
1350
1718
|
if (run.current_node !== nodeId) {
|
|
1351
1719
|
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
@@ -1364,28 +1732,45 @@ class RunManager {
|
|
|
1364
1732
|
}
|
|
1365
1733
|
return { run, outcome: 'paused', reason: `unknown node type "${node.type}" (node "${nodeId}")`, executed };
|
|
1366
1734
|
}
|
|
1367
|
-
|
|
1735
|
+
const claimedToken = await this.claimAutoRunLease(execId, nodeId, runnerToken);
|
|
1736
|
+
if (!claimedToken) {
|
|
1737
|
+
run = this.loadRun(execId);
|
|
1738
|
+
return {
|
|
1739
|
+
run,
|
|
1740
|
+
outcome: run.status === 'cancelled' ? 'cancelled' : 'paused',
|
|
1741
|
+
reason: run.status === 'cancelled' ? run.error : 'run stopped before auto-run lease could be claimed',
|
|
1742
|
+
executed,
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
runnerToken = claimedToken;
|
|
1368
1746
|
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1369
1747
|
const engine = readRunEngineState(r);
|
|
1370
1748
|
engine.visitCounts[nodeId] = (engine.visitCounts[nodeId] ?? 0) + 1;
|
|
1371
1749
|
engine.iterations += 1;
|
|
1372
1750
|
r.state['__engine'] = engine;
|
|
1373
1751
|
if (engine.visitCounts[nodeId] > maxIterations || engine.iterations > maxIterations) {
|
|
1374
|
-
|
|
1375
|
-
|
|
1752
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1753
|
+
failedNode: nodeId,
|
|
1754
|
+
attempt: (0, failure_1.readNodeAttempt)(r, nodeId),
|
|
1755
|
+
causeCode: 'CONFIG_INVALID',
|
|
1756
|
+
exitCode: null,
|
|
1757
|
+
signal: null,
|
|
1758
|
+
errorOrigin: null,
|
|
1759
|
+
});
|
|
1760
|
+
assignRunFailure(r, failure, `auto runner exceeded max_iterations (${maxIterations}); possible loop at "${nodeId}"`);
|
|
1761
|
+
appendFailureAudit(r, failure, 'config', 'auto');
|
|
1376
1762
|
}
|
|
1377
1763
|
});
|
|
1378
1764
|
if (run.status === 'failed') {
|
|
1379
1765
|
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1380
1766
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
run = this.loadRun(execId);
|
|
1387
|
-
return { run, outcome: 'failed', reason: e.message, executed };
|
|
1767
|
+
const nodeResult = await this.runNodeWithRetry(execId, nodeId, runnerToken, parsedRetryPolicy.policy);
|
|
1768
|
+
run = nodeResult.run;
|
|
1769
|
+
runnerToken = nodeResult.runnerToken;
|
|
1770
|
+
if (nodeResult.outcome !== 'success') {
|
|
1771
|
+
return { run, outcome: nodeResult.outcome, reason: nodeResult.reason, executed };
|
|
1388
1772
|
}
|
|
1773
|
+
executed.push(nodeId);
|
|
1389
1774
|
if (run.status === 'completed')
|
|
1390
1775
|
return { run, outcome: 'completed', executed };
|
|
1391
1776
|
if (run.status === 'failed')
|
|
@@ -1418,23 +1803,19 @@ class RunManager {
|
|
|
1418
1803
|
return;
|
|
1419
1804
|
}
|
|
1420
1805
|
const now = new Date();
|
|
1806
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1807
|
+
failedNode: run.current_node || '',
|
|
1808
|
+
attempt: (0, failure_1.readNodeAttempt)(run, run.current_node || ''),
|
|
1809
|
+
causeCode: 'CANCELLED',
|
|
1810
|
+
exitCode: null,
|
|
1811
|
+
signal: null,
|
|
1812
|
+
errorOrigin: null,
|
|
1813
|
+
});
|
|
1814
|
+
assignRunFailure(run, failure, reason);
|
|
1421
1815
|
run.status = 'cancelled';
|
|
1422
|
-
run.error = reason;
|
|
1423
1816
|
delete run.auto_run;
|
|
1424
1817
|
delete run.manual_resume;
|
|
1425
|
-
run
|
|
1426
|
-
run.audit.push({
|
|
1427
|
-
exec_id: run.id,
|
|
1428
|
-
node_id: run.current_node || '',
|
|
1429
|
-
type: 'cancel',
|
|
1430
|
-
summary: reason,
|
|
1431
|
-
started_at: now.toISOString(),
|
|
1432
|
-
ended_at: now.toISOString(),
|
|
1433
|
-
duration_ms: 0,
|
|
1434
|
-
exit_code: null,
|
|
1435
|
-
error: reason,
|
|
1436
|
-
trigger: 'manual',
|
|
1437
|
-
});
|
|
1818
|
+
appendFailureAudit(run, failure, 'cancel', 'manual', now.toISOString());
|
|
1438
1819
|
});
|
|
1439
1820
|
}
|
|
1440
1821
|
reset(execId) {
|
|
@@ -1442,6 +1823,10 @@ class RunManager {
|
|
|
1442
1823
|
run.state = JSON.parse(JSON.stringify(run.initial_state));
|
|
1443
1824
|
run.current_node = '';
|
|
1444
1825
|
run.completed_nodes = [];
|
|
1826
|
+
run.status = 'pending';
|
|
1827
|
+
delete run.error;
|
|
1828
|
+
delete run.last_failure;
|
|
1829
|
+
delete run.node_attempts;
|
|
1445
1830
|
delete run.auto_run;
|
|
1446
1831
|
delete run.manual_resume;
|
|
1447
1832
|
});
|
|
@@ -1463,8 +1848,20 @@ class RunManager {
|
|
|
1463
1848
|
throw new Error(`Run "${execId}" failed at a non-retryable node "${snapshot.current_node}" (error node); use reset to restart`);
|
|
1464
1849
|
}
|
|
1465
1850
|
return this.atomicUpdate(execId, (run) => {
|
|
1851
|
+
if (run.status !== 'failed') {
|
|
1852
|
+
throw new Error(`Run "${execId}" is not failed (status=${run.status ?? 'pending'}); only failed runs can be retried`);
|
|
1853
|
+
}
|
|
1466
1854
|
const failedNode = run.current_node;
|
|
1467
|
-
|
|
1855
|
+
if (failedNode && run.last_failure) {
|
|
1856
|
+
const lastAttempt = run.last_failure.attempt;
|
|
1857
|
+
const storedAttempt = run.node_attempts?.[failedNode];
|
|
1858
|
+
if (typeof lastAttempt === 'number' && Number.isInteger(lastAttempt) && lastAttempt > 0
|
|
1859
|
+
&& (typeof storedAttempt !== 'number' || storedAttempt < lastAttempt)) {
|
|
1860
|
+
run.node_attempts = run.node_attempts ?? {};
|
|
1861
|
+
run.node_attempts[failedNode] = lastAttempt;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
run.status = 'retrying';
|
|
1468
1865
|
delete run.error;
|
|
1469
1866
|
delete run.manual_resume;
|
|
1470
1867
|
run.retry_count = (run.retry_count ?? 0) + 1;
|
|
@@ -1486,7 +1883,7 @@ class RunManager {
|
|
|
1486
1883
|
exec_id: run.id,
|
|
1487
1884
|
node_id: failedNode,
|
|
1488
1885
|
type: 'retry',
|
|
1489
|
-
summary: `manual retry #${run.retry_count}
|
|
1886
|
+
summary: `manual retry #${run.retry_count}`,
|
|
1490
1887
|
started_at: now,
|
|
1491
1888
|
ended_at: now,
|
|
1492
1889
|
duration_ms: 0,
|