@crewx/workflow 0.3.22-rc.13 → 0.3.22-rc.130
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/SKILL.md +38 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +78 -9
- package/dist/cli.js.map +1 -1
- package/dist/src/engine.d.ts +4 -2
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +57 -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/mermaid.d.ts.map +1 -1
- package/dist/src/mermaid.js +7 -4
- package/dist/src/mermaid.js.map +1 -1
- package/dist/src/parent-task-sync.d.ts +49 -0
- package/dist/src/parent-task-sync.d.ts.map +1 -0
- package/dist/src/parent-task-sync.js +132 -0
- package/dist/src/parent-task-sync.js.map +1 -0
- 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 +28 -2
- package/dist/src/run-manager.d.ts.map +1 -1
- package/dist/src/run-manager.js +935 -210
- package/dist/src/run-manager.js.map +1 -1
- package/dist/src/types.d.ts +39 -4
- 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/src/utils/output-format.d.ts +12 -0
- package/dist/src/utils/output-format.d.ts.map +1 -1
- package/dist/src/utils/output-format.js +103 -5
- package/dist/src/utils/output-format.js.map +1 -1
- package/dist/workflow-schema.json +34 -5
- package/package.json +14 -4
- package/workflow-schema.json +34 -5
package/dist/src/run-manager.js
CHANGED
|
@@ -33,26 +33,53 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.RunManager = exports.SKILL_TASK_DEFAULT_TIMEOUT = void 0;
|
|
36
|
+
exports.RunManager = exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS = exports.WORKFLOW_RUN_INTERRUPTED_ERROR = exports.SKILL_TASK_DEFAULT_TIMEOUT = exports.FileLock = void 0;
|
|
37
37
|
exports.parseSetArgs = parseSetArgs;
|
|
38
38
|
exports.readStdin = readStdin;
|
|
39
39
|
exports.interpolateTemplate = interpolateTemplate;
|
|
40
40
|
exports.safeEvaluate = safeEvaluate;
|
|
41
41
|
exports.safeEvaluateBranchCondition = safeEvaluateBranchCondition;
|
|
42
42
|
exports.validateSafeBranchCondition = validateSafeBranchCondition;
|
|
43
|
+
exports.normalizeRunStatus = normalizeRunStatus;
|
|
44
|
+
exports.isRetryableFailure = isRetryableFailure;
|
|
43
45
|
const fs = __importStar(require("fs"));
|
|
44
46
|
const path = __importStar(require("path"));
|
|
45
47
|
const yaml = __importStar(require("js-yaml"));
|
|
46
48
|
const cp = __importStar(require("child_process"));
|
|
47
49
|
const sdk_1 = require("@crewx/sdk");
|
|
50
|
+
const failure_1 = require("./failure");
|
|
48
51
|
const file_lock_1 = require("./file-lock");
|
|
49
52
|
const output_format_1 = require("./utils/output-format");
|
|
50
53
|
const shell_security_1 = require("./utils/shell-security");
|
|
54
|
+
const retry_policy_1 = require("./retry-policy");
|
|
55
|
+
var file_lock_2 = require("./file-lock");
|
|
56
|
+
Object.defineProperty(exports, "FileLock", { enumerable: true, get: function () { return file_lock_2.FileLock; } });
|
|
51
57
|
exports.SKILL_TASK_DEFAULT_TIMEOUT = 600000;
|
|
58
|
+
exports.WORKFLOW_RUN_INTERRUPTED_ERROR = 'Workflow run interrupted: runner process exited before completion';
|
|
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
|
+
}
|
|
52
66
|
const KNOWN_AUTO_NODE_TYPES = new Set([
|
|
53
|
-
'agent_task', 'skill_task', 'shell_task', 'branch', 'expression', 'join', 'end',
|
|
67
|
+
'agent_task', 'skill_task', 'shell_task', 'branch', 'expression', 'join', 'end', 'error',
|
|
54
68
|
]);
|
|
55
|
-
const SELF_ADVANCING_NODE_TYPES = new Set(['branch', 'expression', 'end']);
|
|
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
|
+
}
|
|
56
83
|
function killProcessGroup(child) {
|
|
57
84
|
try {
|
|
58
85
|
if (process.platform === 'win32') {
|
|
@@ -81,19 +108,30 @@ async function runProcess(command, argv, opts) {
|
|
|
81
108
|
child = cp.spawn(command, argv, {
|
|
82
109
|
cwd: opts.cwd,
|
|
83
110
|
env: opts.env,
|
|
84
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
111
|
+
stdio: [opts.stdin !== undefined ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
85
112
|
shell: opts.shell ?? false,
|
|
86
113
|
detached: process.platform !== 'win32',
|
|
87
114
|
windowsHide: opts.windowsHide ?? true,
|
|
88
115
|
});
|
|
89
116
|
}
|
|
90
117
|
catch (err) {
|
|
91
|
-
resolve({
|
|
118
|
+
resolve({
|
|
119
|
+
status: null,
|
|
120
|
+
stdout: '',
|
|
121
|
+
stderr: '',
|
|
122
|
+
timedOut: false,
|
|
123
|
+
signal: null,
|
|
124
|
+
errorOrigin: 'spawn',
|
|
125
|
+
error: err,
|
|
126
|
+
});
|
|
92
127
|
return;
|
|
93
128
|
}
|
|
94
129
|
let stdout = '';
|
|
95
130
|
let stderr = '';
|
|
96
131
|
let settled = false;
|
|
132
|
+
let spawned = false;
|
|
133
|
+
let postSpawnError;
|
|
134
|
+
let stdinError;
|
|
97
135
|
const finish = (result) => {
|
|
98
136
|
if (settled)
|
|
99
137
|
return;
|
|
@@ -103,35 +141,120 @@ async function runProcess(command, argv, opts) {
|
|
|
103
141
|
};
|
|
104
142
|
const timer = setTimeout(() => {
|
|
105
143
|
killProcessGroup(child);
|
|
106
|
-
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
|
+
});
|
|
107
153
|
}, opts.timeout);
|
|
108
154
|
if (typeof timer.unref === 'function')
|
|
109
155
|
timer.unref();
|
|
110
156
|
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
111
157
|
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
112
|
-
child.
|
|
113
|
-
child.on('
|
|
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
|
+
});
|
|
174
|
+
if (opts.stdin !== undefined) {
|
|
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
|
+
}
|
|
184
|
+
}
|
|
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
|
+
}));
|
|
114
194
|
});
|
|
115
195
|
}
|
|
116
|
-
|
|
117
|
-
|
|
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;
|
|
118
202
|
}
|
|
119
203
|
function summarizeNode(node) {
|
|
120
204
|
switch (node.type) {
|
|
121
|
-
case 'agent_task':
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
}
|
|
125
215
|
case 'shell_task':
|
|
126
|
-
return
|
|
216
|
+
return 'shell_task';
|
|
127
217
|
case 'branch':
|
|
128
|
-
return
|
|
218
|
+
return 'branch';
|
|
129
219
|
case 'expression':
|
|
130
|
-
return
|
|
220
|
+
return 'expression';
|
|
131
221
|
default:
|
|
132
|
-
return node
|
|
222
|
+
return 'workflow node';
|
|
133
223
|
}
|
|
134
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
|
+
}
|
|
135
258
|
async function storeNodeOutput(nodeSpec, nodeId, stdout, stateUpdates) {
|
|
136
259
|
if (!nodeSpec.output)
|
|
137
260
|
return;
|
|
@@ -140,12 +263,12 @@ async function storeNodeOutput(nodeSpec, nodeId, stdout, stateUpdates) {
|
|
|
140
263
|
let parsed;
|
|
141
264
|
try {
|
|
142
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
|
+
}
|
|
143
269
|
}
|
|
144
270
|
catch (e) {
|
|
145
|
-
throw
|
|
146
|
-
}
|
|
147
|
-
if (nodeSpec.output_schema) {
|
|
148
|
-
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)}`);
|
|
149
272
|
}
|
|
150
273
|
stateUpdates[nodeSpec.output] = parsed;
|
|
151
274
|
}
|
|
@@ -521,12 +644,91 @@ function safeEvaluateBranchCondition(expr, state) {
|
|
|
521
644
|
function validateSafeBranchCondition(expr) {
|
|
522
645
|
safeEvaluateBranchCondition(expr, {});
|
|
523
646
|
}
|
|
647
|
+
function readRunEngineState(run) {
|
|
648
|
+
const raw = run.state['__engine'];
|
|
649
|
+
if (!raw || typeof raw !== 'object')
|
|
650
|
+
return { visitCounts: {}, iterations: 0 };
|
|
651
|
+
const record = raw;
|
|
652
|
+
const rawCounts = record['visitCounts'];
|
|
653
|
+
const visitCounts = {};
|
|
654
|
+
if (rawCounts && typeof rawCounts === 'object') {
|
|
655
|
+
for (const [key, value] of Object.entries(rawCounts)) {
|
|
656
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
657
|
+
visitCounts[key] = value;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const iterations = typeof record['iterations'] === 'number' && Number.isFinite(record['iterations'])
|
|
661
|
+
? record['iterations']
|
|
662
|
+
: 0;
|
|
663
|
+
return { visitCounts, iterations };
|
|
664
|
+
}
|
|
665
|
+
const inFlightAutoRuns = new Set();
|
|
666
|
+
function isProcessAlive(pid) {
|
|
667
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
668
|
+
return false;
|
|
669
|
+
if (pid === process.pid)
|
|
670
|
+
return true;
|
|
671
|
+
try {
|
|
672
|
+
process.kill(pid, 0);
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
return error.code === 'EPERM';
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function isTerminalStatus(status) {
|
|
680
|
+
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
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
|
+
}
|
|
710
|
+
function isRetryableFailure(run, spec) {
|
|
711
|
+
if (run.status !== 'failed')
|
|
712
|
+
return false;
|
|
713
|
+
if (!run.current_node)
|
|
714
|
+
return false;
|
|
715
|
+
const node = spec.nodes?.[run.current_node];
|
|
716
|
+
if (!node)
|
|
717
|
+
return false;
|
|
718
|
+
return node.type !== 'error';
|
|
719
|
+
}
|
|
524
720
|
class RunManager {
|
|
525
|
-
constructor(runsDir = '.crewx/workflow-runs', projectRoot) {
|
|
721
|
+
constructor(runsDir = '.crewx/workflow-runs', projectRoot, deps) {
|
|
526
722
|
this.runsDir = runsDir;
|
|
723
|
+
this.sleep = deps?.sleep ?? sleep;
|
|
527
724
|
const base = projectRoot || process.env.CREWX_WORKSPACE || process.cwd();
|
|
528
725
|
this.resolvedDir = path.resolve(base, this.runsDir);
|
|
529
726
|
this.projectRoot = path.resolve(base);
|
|
727
|
+
const hasConfig = fs.existsSync(path.join(this.projectRoot, 'crewx.yaml'))
|
|
728
|
+
|| fs.existsSync(path.join(this.projectRoot, 'crewx.yml'));
|
|
729
|
+
if (!hasConfig) {
|
|
730
|
+
console.warn(`[RunManager] projectRoot "${this.projectRoot}" has no crewx.yaml/crewx.yml — agent_task nodes may fail to resolve agents.`);
|
|
731
|
+
}
|
|
530
732
|
}
|
|
531
733
|
ensureRunsDir() {
|
|
532
734
|
if (!fs.existsSync(this.resolvedDir)) {
|
|
@@ -538,8 +740,7 @@ class RunManager {
|
|
|
538
740
|
throw new Error(`Invalid execution ID: "${execId}"`);
|
|
539
741
|
}
|
|
540
742
|
}
|
|
541
|
-
|
|
542
|
-
this.validateExecId(execId);
|
|
743
|
+
readRunRaw(execId) {
|
|
543
744
|
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
544
745
|
if (!fs.existsSync(filePath))
|
|
545
746
|
return null;
|
|
@@ -550,6 +751,75 @@ class RunManager {
|
|
|
550
751
|
throw new Error(`Failed to parse execution file ${filePath}: ${e.message}`);
|
|
551
752
|
}
|
|
552
753
|
}
|
|
754
|
+
readRun(execId) {
|
|
755
|
+
const run = this.readRunRaw(execId);
|
|
756
|
+
return run ? normalizeRunStatus(run) : null;
|
|
757
|
+
}
|
|
758
|
+
isLegacyStatuslessStale(run) {
|
|
759
|
+
if (run.status !== undefined
|
|
760
|
+
|| run.auto_run
|
|
761
|
+
|| run.manual_resume
|
|
762
|
+
|| !run.current_node
|
|
763
|
+
|| !run.audit?.length) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
const updatedAt = Date.parse(run.updated_at);
|
|
767
|
+
if (!Number.isFinite(updatedAt) || Date.now() - updatedAt < exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS) {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
const doc = this.loadWorkflowYaml(run.workflow_file);
|
|
771
|
+
const spec = doc?.workflows[run.workflow_id];
|
|
772
|
+
const node = spec?.nodes?.[run.current_node];
|
|
773
|
+
return Boolean(node && KNOWN_AUTO_NODE_TYPES.has(node.type));
|
|
774
|
+
}
|
|
775
|
+
hasStaleRunner(run) {
|
|
776
|
+
if (run.auto_run)
|
|
777
|
+
return !isProcessAlive(run.auto_run.pid);
|
|
778
|
+
return this.isLegacyStatuslessStale(run);
|
|
779
|
+
}
|
|
780
|
+
recoverStaleRunner(execId) {
|
|
781
|
+
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
782
|
+
const lock = new file_lock_1.FileLock(filePath);
|
|
783
|
+
lock.acquire();
|
|
784
|
+
try {
|
|
785
|
+
const run = this.readRunRaw(execId);
|
|
786
|
+
if (!run || !this.hasStaleRunner(run))
|
|
787
|
+
return run ? normalizeRunStatus(run) : null;
|
|
788
|
+
if (isTerminalStatus(run.status)) {
|
|
789
|
+
delete run.auto_run;
|
|
790
|
+
delete run.manual_resume;
|
|
791
|
+
this.saveRun(run);
|
|
792
|
+
return run;
|
|
793
|
+
}
|
|
794
|
+
const lease = run.auto_run;
|
|
795
|
+
const nodeId = lease?.node_id || run.current_node;
|
|
796
|
+
const now = new Date().toISOString();
|
|
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);
|
|
806
|
+
delete run.auto_run;
|
|
807
|
+
delete run.manual_resume;
|
|
808
|
+
appendFailureAudit(run, failure, 'interrupted', 'auto', now);
|
|
809
|
+
this.saveRun(run);
|
|
810
|
+
return run;
|
|
811
|
+
}
|
|
812
|
+
finally {
|
|
813
|
+
lock.release();
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
loadRun(execId) {
|
|
817
|
+
this.validateExecId(execId);
|
|
818
|
+
const run = this.readRunRaw(execId);
|
|
819
|
+
if (!run || !this.hasStaleRunner(run))
|
|
820
|
+
return run ? normalizeRunStatus(run) : null;
|
|
821
|
+
return this.recoverStaleRunner(execId);
|
|
822
|
+
}
|
|
553
823
|
saveRun(run) {
|
|
554
824
|
this.ensureRunsDir();
|
|
555
825
|
run.updated_at = new Date().toISOString();
|
|
@@ -563,7 +833,7 @@ class RunManager {
|
|
|
563
833
|
const lock = new file_lock_1.FileLock(filePath);
|
|
564
834
|
lock.acquire();
|
|
565
835
|
try {
|
|
566
|
-
const run = this.
|
|
836
|
+
const run = this.readRun(execId);
|
|
567
837
|
if (!run)
|
|
568
838
|
throw new Error(`Execution not found: ${execId}`);
|
|
569
839
|
updater(run);
|
|
@@ -581,7 +851,7 @@ class RunManager {
|
|
|
581
851
|
const lock = new file_lock_1.FileLock(filePath);
|
|
582
852
|
await lock.acquireAsync();
|
|
583
853
|
try {
|
|
584
|
-
const run = this.
|
|
854
|
+
const run = this.readRun(execId);
|
|
585
855
|
if (!run)
|
|
586
856
|
throw new Error(`Execution not found: ${execId}`);
|
|
587
857
|
updater(run);
|
|
@@ -592,6 +862,32 @@ class RunManager {
|
|
|
592
862
|
lock.release();
|
|
593
863
|
}
|
|
594
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
|
+
}
|
|
595
891
|
loadWorkflowYaml(yamlPath) {
|
|
596
892
|
if (!fs.existsSync(yamlPath))
|
|
597
893
|
return null;
|
|
@@ -601,6 +897,12 @@ class RunManager {
|
|
|
601
897
|
return null;
|
|
602
898
|
return doc;
|
|
603
899
|
}
|
|
900
|
+
getNodeType(workflowFile, workflowId, nodeId) {
|
|
901
|
+
const doc = this.loadWorkflowYaml(workflowFile);
|
|
902
|
+
const spec = doc?.workflows[workflowId];
|
|
903
|
+
const node = spec?.nodes?.[nodeId];
|
|
904
|
+
return typeof node?.type === 'string' ? node.type : null;
|
|
905
|
+
}
|
|
604
906
|
start(yamlPath, workflowId, overrides) {
|
|
605
907
|
const doc = this.loadWorkflowYaml(yamlPath);
|
|
606
908
|
if (!doc) {
|
|
@@ -638,6 +940,7 @@ class RunManager {
|
|
|
638
940
|
completed_nodes: [],
|
|
639
941
|
state,
|
|
640
942
|
initial_state: { ...initialState, ...sets },
|
|
943
|
+
status: 'pending',
|
|
641
944
|
};
|
|
642
945
|
this.saveRun(run);
|
|
643
946
|
return run;
|
|
@@ -669,27 +972,36 @@ class RunManager {
|
|
|
669
972
|
}
|
|
670
973
|
const doc = this.loadWorkflowYaml(snapshot.workflow_file);
|
|
671
974
|
if (!doc) {
|
|
672
|
-
throw
|
|
975
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Failed to load workflow file: ${snapshot.workflow_file}`);
|
|
673
976
|
}
|
|
674
977
|
const spec = doc.workflows[snapshot.workflow_id];
|
|
675
978
|
if (!spec) {
|
|
676
|
-
throw
|
|
979
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Workflow "${snapshot.workflow_id}" not found in ${snapshot.workflow_file}`);
|
|
677
980
|
}
|
|
678
981
|
const nodeSpec = spec.nodes?.[nodeId];
|
|
679
982
|
if (!nodeSpec) {
|
|
680
|
-
throw
|
|
983
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" not found in workflow "${snapshot.workflow_id}"`);
|
|
681
984
|
}
|
|
682
985
|
const prevNode = snapshot.current_node;
|
|
683
986
|
const stateUpdates = {};
|
|
684
987
|
const nodesToComplete = [];
|
|
685
988
|
let currentNode = nodeId;
|
|
686
989
|
let status;
|
|
990
|
+
let failureMessage;
|
|
991
|
+
let failureCode;
|
|
687
992
|
let nodeTaskId;
|
|
688
993
|
const trigger = opts?.trigger ?? 'manual';
|
|
689
994
|
const startedAt = new Date();
|
|
690
|
-
let nodeStdout = '';
|
|
691
|
-
let nodeStderr = '';
|
|
692
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
|
+
};
|
|
693
1005
|
const buildAudit = (error) => {
|
|
694
1006
|
const endedAt = new Date();
|
|
695
1007
|
return {
|
|
@@ -705,31 +1017,57 @@ class RunManager {
|
|
|
705
1017
|
trigger,
|
|
706
1018
|
};
|
|
707
1019
|
};
|
|
1020
|
+
const attempt = await this.reserveNodeAttempt(execId, nodeId);
|
|
708
1021
|
try {
|
|
709
1022
|
switch (nodeSpec.type) {
|
|
710
1023
|
case 'agent_task': {
|
|
711
1024
|
if (!nodeSpec.agent) {
|
|
712
|
-
throw
|
|
1025
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" is agent_task but has no agent defined`);
|
|
1026
|
+
}
|
|
1027
|
+
const agentCtx = { ...snapshot.state, state: snapshot.state };
|
|
1028
|
+
const resolvedAgent = interpolateTemplate(nodeSpec.agent, agentCtx).trim();
|
|
1029
|
+
if (!resolvedAgent) {
|
|
1030
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": assignee not resolved (agent field "${nodeSpec.agent}" interpolated to empty)`);
|
|
713
1031
|
}
|
|
1032
|
+
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, agentCtx) : undefined, this.projectRoot);
|
|
1033
|
+
if (!cwdCheck.ok) {
|
|
1034
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cwdCheck.error}`);
|
|
1035
|
+
}
|
|
1036
|
+
const nodeCwd = cwdCheck.resolved;
|
|
1037
|
+
const rootCheck = (0, shell_security_1.normalizeCwd)(undefined, this.projectRoot);
|
|
1038
|
+
if (!rootCheck.ok) {
|
|
1039
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${rootCheck.error}`);
|
|
1040
|
+
}
|
|
1041
|
+
const workspaceRoot = rootCheck.resolved;
|
|
714
1042
|
const input = nodeSpec.input
|
|
715
|
-
? interpolateTemplate(nodeSpec.input,
|
|
1043
|
+
? interpolateTemplate(nodeSpec.input, agentCtx)
|
|
716
1044
|
: '';
|
|
1045
|
+
const preamble = process.env.CREWX_WORKFLOW_PREAMBLE === 'off'
|
|
1046
|
+
? ''
|
|
1047
|
+
: (0, output_format_1.buildWorkflowPreamble)({
|
|
1048
|
+
workflowId: snapshot.workflow_id,
|
|
1049
|
+
nodeId,
|
|
1050
|
+
execId: snapshot.id,
|
|
1051
|
+
});
|
|
717
1052
|
const finalInput = nodeSpec.output_format === 'json'
|
|
718
|
-
? input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
719
|
-
: input;
|
|
1053
|
+
? preamble + input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
1054
|
+
: preamble + input;
|
|
720
1055
|
const mode = nodeSpec.mode === 'query' ? 'q' : 'x';
|
|
721
|
-
const prompt = `@${
|
|
1056
|
+
const prompt = `@${resolvedAgent} ${finalInput}`;
|
|
722
1057
|
if (opts?.dryRun) {
|
|
723
|
-
console.log(`[dry-run] Would execute: crewx ${mode} "@${
|
|
1058
|
+
console.log(`[dry-run] Would execute: crewx ${mode} "@${resolvedAgent} ..."`);
|
|
724
1059
|
break;
|
|
725
1060
|
}
|
|
726
|
-
console.log(`Executing: @${
|
|
727
|
-
const crewxCli = process.env.CREWX_CLI || 'npx crewx';
|
|
728
|
-
const cliParts = crewxCli.split(/\s+/);
|
|
729
|
-
const bin = cliParts[0];
|
|
730
|
-
const baseArgs = cliParts.slice(1);
|
|
1061
|
+
console.log(`Executing: @${resolvedAgent} (${mode} mode)`);
|
|
731
1062
|
const nodeTimeout = Number(process.env[mode === 'q' ? 'CREWX_TIMEOUT_QUERY' : 'CREWX_TIMEOUT_EXECUTE']) || 8 * 3600000;
|
|
732
|
-
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];
|
|
733
1071
|
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
734
1072
|
wfArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
735
1073
|
}
|
|
@@ -742,32 +1080,27 @@ class RunManager {
|
|
|
742
1080
|
};
|
|
743
1081
|
wfArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
744
1082
|
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
745
|
-
command:
|
|
1083
|
+
command: childInvocation.argv[0],
|
|
1084
|
+
env: childInvocation.env,
|
|
1085
|
+
cwd: nodeCwd,
|
|
746
1086
|
allowShellFallback: true,
|
|
747
1087
|
packageName: 'crewx',
|
|
748
1088
|
});
|
|
749
1089
|
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, wfArgs);
|
|
750
1090
|
const result = await runProcess(invocation.command, invocation.argv, {
|
|
751
|
-
cwd:
|
|
752
|
-
env:
|
|
753
|
-
...process.env,
|
|
754
|
-
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
755
|
-
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
756
|
-
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
757
|
-
},
|
|
1091
|
+
cwd: nodeCwd,
|
|
1092
|
+
env: childInvocation.env,
|
|
758
1093
|
timeout: nodeTimeout,
|
|
759
1094
|
shell: invocation.shell ?? false,
|
|
760
1095
|
windowsHide: invocation.windowsHide,
|
|
761
1096
|
});
|
|
762
|
-
|
|
763
|
-
nodeStderr = result.stderr;
|
|
764
|
-
nodeExitCode = result.status;
|
|
765
|
-
if (result.error) {
|
|
766
|
-
throw result.error;
|
|
767
|
-
}
|
|
1097
|
+
recordProcessResult(result);
|
|
768
1098
|
if (result.timedOut) {
|
|
769
1099
|
throw new Error(`Agent execution timed out after ${nodeTimeout}ms`);
|
|
770
1100
|
}
|
|
1101
|
+
if (result.error) {
|
|
1102
|
+
throw result.error;
|
|
1103
|
+
}
|
|
771
1104
|
const taskIdMatch = result.stderr.match(/crewx kill (tsk_\w+)/);
|
|
772
1105
|
nodeTaskId = taskIdMatch?.[1];
|
|
773
1106
|
if (result.status !== 0) {
|
|
@@ -775,6 +1108,10 @@ class RunManager {
|
|
|
775
1108
|
throw new Error(`Agent execution failed (exit ${result.status}): ${stderr}`);
|
|
776
1109
|
}
|
|
777
1110
|
const output = result.stdout.trim();
|
|
1111
|
+
const signal = (0, output_format_1.detectWorkflowSignal)(output);
|
|
1112
|
+
if (signal) {
|
|
1113
|
+
throw new Error(`Node "${nodeId}" agent signaled failure: ${signal.reason ?? '(no reason given)'}`);
|
|
1114
|
+
}
|
|
778
1115
|
if (nodeSpec.output) {
|
|
779
1116
|
if (nodeSpec.output_format === 'json') {
|
|
780
1117
|
const retryMax = nodeSpec.output_retry ?? 1;
|
|
@@ -787,36 +1124,33 @@ class RunManager {
|
|
|
787
1124
|
retryInput = finalInput +
|
|
788
1125
|
`\n\n## ⚠️ PREVIOUS OUTPUT FAILED JSON.parse: ${lastError.message}\n` +
|
|
789
1126
|
`Return STRICT JSON ONLY. First char "{", last char "}". No prose, no fences.`;
|
|
790
|
-
const retryPrompt = `@${
|
|
791
|
-
const retryArgs = [...
|
|
1127
|
+
const retryPrompt = `@${resolvedAgent} ${retryInput}`;
|
|
1128
|
+
const retryArgs = [...childInvocation.argv.slice(1), mode, retryPrompt];
|
|
792
1129
|
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
793
1130
|
retryArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
794
1131
|
}
|
|
795
1132
|
retryArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
796
1133
|
const retryInvocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, retryArgs);
|
|
797
1134
|
const retryResult = await runProcess(retryInvocation.command, retryInvocation.argv, {
|
|
798
|
-
cwd:
|
|
799
|
-
env:
|
|
800
|
-
...process.env,
|
|
801
|
-
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
802
|
-
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
803
|
-
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
804
|
-
},
|
|
1135
|
+
cwd: nodeCwd,
|
|
1136
|
+
env: childInvocation.env,
|
|
805
1137
|
timeout: nodeTimeout,
|
|
806
1138
|
shell: retryInvocation.shell ?? false,
|
|
807
1139
|
windowsHide: retryInvocation.windowsHide,
|
|
808
1140
|
});
|
|
809
|
-
|
|
810
|
-
nodeStderr = retryResult.stderr;
|
|
811
|
-
nodeExitCode = retryResult.status;
|
|
812
|
-
if (retryResult.error)
|
|
813
|
-
throw retryResult.error;
|
|
1141
|
+
recordProcessResult(retryResult);
|
|
814
1142
|
if (retryResult.timedOut)
|
|
815
1143
|
throw new Error(`Agent retry timed out after ${nodeTimeout}ms`);
|
|
1144
|
+
if (retryResult.error)
|
|
1145
|
+
throw retryResult.error;
|
|
816
1146
|
if (retryResult.status !== 0) {
|
|
817
1147
|
throw new Error(`Agent retry failed (exit ${retryResult.status}): ${retryResult.stderr.trim()}`);
|
|
818
1148
|
}
|
|
819
1149
|
attemptOutput = retryResult.stdout.trim();
|
|
1150
|
+
const retrySignal = (0, output_format_1.detectWorkflowSignal)(attemptOutput);
|
|
1151
|
+
if (retrySignal) {
|
|
1152
|
+
throw new Error(`Node "${nodeId}" agent signaled failure: ${retrySignal.reason ?? '(no reason given)'}`);
|
|
1153
|
+
}
|
|
820
1154
|
}
|
|
821
1155
|
try {
|
|
822
1156
|
parsed = JSON.parse((0, output_format_1.extractJson)(attemptOutput));
|
|
@@ -830,7 +1164,7 @@ class RunManager {
|
|
|
830
1164
|
}
|
|
831
1165
|
}
|
|
832
1166
|
if (parsed === null) {
|
|
833
|
-
throw
|
|
1167
|
+
throw (0, failure_1.createWorkflowFailureError)('OUTPUT_INVALID', `Node "${nodeId}" output_format=json parse failed after ${retryMax + 1} attempt(s): ${lastError.message}`);
|
|
834
1168
|
}
|
|
835
1169
|
stateUpdates[nodeSpec.output] = parsed;
|
|
836
1170
|
console.log(`Result stored in state.${nodeSpec.output} (JSON object)`);
|
|
@@ -844,52 +1178,60 @@ class RunManager {
|
|
|
844
1178
|
}
|
|
845
1179
|
case 'skill_task': {
|
|
846
1180
|
if (!nodeSpec.skill) {
|
|
847
|
-
throw
|
|
1181
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" is skill_task but has no skill defined`);
|
|
848
1182
|
}
|
|
849
1183
|
if (!shell_security_1.SKILL_NAME_RE.test(nodeSpec.skill)) {
|
|
850
|
-
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-]*$)`);
|
|
851
1185
|
}
|
|
852
1186
|
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
853
1187
|
const skillArgs = (nodeSpec.args ?? []).map((a) => interpolateTemplate(String(a), ctx));
|
|
1188
|
+
const skillStdin = nodeSpec.stdin !== undefined
|
|
1189
|
+
? interpolateTemplate(String(nodeSpec.stdin), ctx)
|
|
1190
|
+
: undefined;
|
|
854
1191
|
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
855
|
-
if (!cwdCheck.ok)
|
|
856
|
-
throw
|
|
1192
|
+
if (!cwdCheck.ok) {
|
|
1193
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cwdCheck.error}`);
|
|
1194
|
+
}
|
|
1195
|
+
const skillRootCheck = (0, shell_security_1.normalizeCwd)(undefined, this.projectRoot);
|
|
1196
|
+
if (!skillRootCheck.ok) {
|
|
1197
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${skillRootCheck.error}`);
|
|
1198
|
+
}
|
|
1199
|
+
const skillWorkspaceRoot = skillRootCheck.resolved;
|
|
857
1200
|
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
858
1201
|
if (opts?.dryRun) {
|
|
859
1202
|
console.log(`[dry-run] Would execute: crewx skill ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
860
1203
|
break;
|
|
861
1204
|
}
|
|
862
1205
|
console.log(`Executing skill: ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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];
|
|
868
1214
|
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
869
|
-
command:
|
|
1215
|
+
command: childInvocation.argv[0],
|
|
1216
|
+
env: childInvocation.env,
|
|
1217
|
+
cwd: cwdCheck.resolved,
|
|
870
1218
|
allowShellFallback: true,
|
|
871
1219
|
packageName: 'crewx',
|
|
872
1220
|
});
|
|
873
1221
|
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, skillArgv);
|
|
874
1222
|
const result = await runProcess(invocation.command, invocation.argv, {
|
|
875
1223
|
cwd: cwdCheck.resolved,
|
|
876
|
-
env:
|
|
877
|
-
...process.env,
|
|
878
|
-
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
879
|
-
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
880
|
-
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
881
|
-
},
|
|
1224
|
+
env: childInvocation.env,
|
|
882
1225
|
timeout,
|
|
883
1226
|
shell: invocation.shell ?? false,
|
|
884
1227
|
windowsHide: invocation.windowsHide,
|
|
1228
|
+
stdin: skillStdin,
|
|
885
1229
|
});
|
|
886
|
-
|
|
887
|
-
nodeStderr = result.stderr;
|
|
888
|
-
nodeExitCode = result.status;
|
|
889
|
-
if (result.error)
|
|
890
|
-
throw result.error;
|
|
1230
|
+
recordProcessResult(result);
|
|
891
1231
|
if (result.timedOut)
|
|
892
1232
|
throw new Error(`skill_task "${nodeId}" timed out after ${timeout}ms`);
|
|
1233
|
+
if (result.error)
|
|
1234
|
+
throw result.error;
|
|
893
1235
|
if (result.status !== 0) {
|
|
894
1236
|
throw new Error(`skill_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
895
1237
|
}
|
|
@@ -903,25 +1245,29 @@ class RunManager {
|
|
|
903
1245
|
const shellEnvEnabled = process.env.CREWX_WORKFLOW_SHELL === '1';
|
|
904
1246
|
const shellMetaAllowed = spec.metadata?.shell_task_allowed === true;
|
|
905
1247
|
if (!shellEnvEnabled || !shellMetaAllowed) {
|
|
906
|
-
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.');
|
|
907
1249
|
}
|
|
908
1250
|
const cmdCheck = (0, shell_security_1.validateShellCommand)(nodeSpec.command);
|
|
909
|
-
if (!cmdCheck.ok)
|
|
910
|
-
throw
|
|
1251
|
+
if (!cmdCheck.ok) {
|
|
1252
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cmdCheck.error}`);
|
|
1253
|
+
}
|
|
911
1254
|
const envCheck = (0, shell_security_1.validateEnvKeys)(nodeSpec.env);
|
|
912
|
-
if (!envCheck.ok)
|
|
913
|
-
throw
|
|
1255
|
+
if (!envCheck.ok) {
|
|
1256
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${envCheck.error}`);
|
|
1257
|
+
}
|
|
914
1258
|
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
915
1259
|
const command = nodeSpec.command.map((c) => interpolateTemplate(c, ctx));
|
|
916
1260
|
const postCheck = (0, shell_security_1.validateShellCommand)(command);
|
|
917
|
-
if (!postCheck.ok)
|
|
918
|
-
throw
|
|
1261
|
+
if (!postCheck.ok) {
|
|
1262
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${postCheck.error}`);
|
|
1263
|
+
}
|
|
919
1264
|
const interpolatedEnv = nodeSpec.env
|
|
920
1265
|
? Object.fromEntries(Object.entries(nodeSpec.env).map(([k, v]) => [k, interpolateTemplate(String(v), ctx)]))
|
|
921
1266
|
: undefined;
|
|
922
1267
|
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
923
|
-
if (!cwdCheck.ok)
|
|
924
|
-
throw
|
|
1268
|
+
if (!cwdCheck.ok) {
|
|
1269
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cwdCheck.error}`);
|
|
1270
|
+
}
|
|
925
1271
|
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
926
1272
|
if (opts?.dryRun) {
|
|
927
1273
|
console.log(`[dry-run] Would execute: ${command.join(' ')}`);
|
|
@@ -932,19 +1278,21 @@ class RunManager {
|
|
|
932
1278
|
childEnv.CREWX_WORKFLOW_EXEC_ID = snapshot.id;
|
|
933
1279
|
childEnv.CREWX_WORKFLOW_NODE_ID = nodeId;
|
|
934
1280
|
childEnv.CREWX_WORKFLOW_ID = snapshot.workflow_id;
|
|
1281
|
+
const shellStdin = nodeSpec.stdin !== undefined
|
|
1282
|
+
? interpolateTemplate(String(nodeSpec.stdin), ctx)
|
|
1283
|
+
: undefined;
|
|
935
1284
|
const result = await runProcess(command[0], command.slice(1), {
|
|
936
1285
|
cwd: cwdCheck.resolved,
|
|
937
1286
|
env: childEnv,
|
|
938
1287
|
timeout,
|
|
939
1288
|
shell: false,
|
|
1289
|
+
stdin: shellStdin,
|
|
940
1290
|
});
|
|
941
|
-
|
|
942
|
-
nodeStderr = result.stderr;
|
|
943
|
-
nodeExitCode = result.status;
|
|
944
|
-
if (result.error)
|
|
945
|
-
throw result.error;
|
|
1291
|
+
recordProcessResult(result);
|
|
946
1292
|
if (result.timedOut)
|
|
947
1293
|
throw new Error(`shell_task "${nodeId}" timed out after ${timeout}ms`);
|
|
1294
|
+
if (result.error)
|
|
1295
|
+
throw result.error;
|
|
948
1296
|
if (result.status !== 0) {
|
|
949
1297
|
throw new Error(`shell_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
950
1298
|
}
|
|
@@ -956,13 +1304,13 @@ class RunManager {
|
|
|
956
1304
|
}
|
|
957
1305
|
case 'branch': {
|
|
958
1306
|
if (!nodeSpec.condition) {
|
|
959
|
-
throw
|
|
1307
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Branch node "${nodeId}" has no condition`);
|
|
960
1308
|
}
|
|
961
1309
|
const condResult = String(safeEvaluateBranchCondition(String(nodeSpec.condition), snapshot.state));
|
|
962
1310
|
const branches = nodeSpec.branches;
|
|
963
1311
|
const targetNode = branches?.[condResult] ?? nodeSpec.default;
|
|
964
1312
|
if (!targetNode) {
|
|
965
|
-
throw
|
|
1313
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Branch "${nodeId}": no matching branch for "${condResult}" and no default`);
|
|
966
1314
|
}
|
|
967
1315
|
console.log(`Branch "${nodeId}": condition="${nodeSpec.condition}" → ${condResult} → ${targetNode}`);
|
|
968
1316
|
nodesToComplete.push(nodeId);
|
|
@@ -986,9 +1334,19 @@ class RunManager {
|
|
|
986
1334
|
console.log(`End "${nodeId}": workflow execution completed`);
|
|
987
1335
|
break;
|
|
988
1336
|
}
|
|
1337
|
+
case 'error': {
|
|
1338
|
+
status = 'failed';
|
|
1339
|
+
failureCode = 'UNKNOWN';
|
|
1340
|
+
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
1341
|
+
failureMessage = nodeSpec.message
|
|
1342
|
+
? interpolateTemplate(nodeSpec.message, ctx)
|
|
1343
|
+
: `Workflow failed at error node "${nodeId}"`;
|
|
1344
|
+
console.log(`Error "${nodeId}": workflow execution failed — ${failureMessage}`);
|
|
1345
|
+
break;
|
|
1346
|
+
}
|
|
989
1347
|
case 'expression': {
|
|
990
1348
|
if (!nodeSpec.set || typeof nodeSpec.set !== 'object') {
|
|
991
|
-
throw
|
|
1349
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Expression node "${nodeId}" requires "set" field`);
|
|
992
1350
|
}
|
|
993
1351
|
for (const [key, expr] of Object.entries(nodeSpec.set)) {
|
|
994
1352
|
const result = safeEvaluate(String(expr), snapshot.state);
|
|
@@ -1009,30 +1367,61 @@ class RunManager {
|
|
|
1009
1367
|
default:
|
|
1010
1368
|
console.log(`Unknown node type "${nodeSpec.type}" for node "${nodeId}"`);
|
|
1011
1369
|
}
|
|
1370
|
+
if (nodeSpec.fail_when &&
|
|
1371
|
+
!opts?.dryRun &&
|
|
1372
|
+
(nodeSpec.type === 'agent_task' || nodeSpec.type === 'skill_task' || nodeSpec.type === 'shell_task')) {
|
|
1373
|
+
const judged = safeEvaluateBranchCondition(String(nodeSpec.fail_when), { ...snapshot.state, ...stateUpdates });
|
|
1374
|
+
if (judged === true || judged === 'true') {
|
|
1375
|
+
throw new Error(`Node "${nodeId}" judged failed by fail_when: ${nodeSpec.fail_when}`);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1012
1378
|
}
|
|
1013
1379
|
catch (e) {
|
|
1014
|
-
const errMessage = e
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
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);
|
|
1020
1397
|
await this.atomicUpdateAsync(execId, (run) => {
|
|
1021
1398
|
if (run.status === 'cancelled')
|
|
1022
1399
|
return;
|
|
1400
|
+
Object.assign(run.state, stateUpdates);
|
|
1401
|
+
delete run.manual_resume;
|
|
1023
1402
|
run.current_node = nodeId;
|
|
1024
|
-
run
|
|
1025
|
-
run.error = errMessage;
|
|
1403
|
+
assignRunFailure(run, failure, errMessage);
|
|
1026
1404
|
run.audit = run.audit ?? [];
|
|
1027
1405
|
run.audit.push(auditEntry);
|
|
1028
1406
|
});
|
|
1029
1407
|
throw e;
|
|
1030
1408
|
}
|
|
1031
|
-
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);
|
|
1032
1420
|
return this.atomicUpdateAsync(execId, (run) => {
|
|
1033
1421
|
if (run.status === 'cancelled')
|
|
1034
1422
|
return;
|
|
1035
1423
|
Object.assign(run.state, stateUpdates);
|
|
1424
|
+
delete run.manual_resume;
|
|
1036
1425
|
for (const node of nodesToComplete) {
|
|
1037
1426
|
if (!run.completed_nodes.includes(node)) {
|
|
1038
1427
|
run.completed_nodes.push(node);
|
|
@@ -1042,8 +1431,20 @@ class RunManager {
|
|
|
1042
1431
|
run.completed_nodes.push(prevNode);
|
|
1043
1432
|
}
|
|
1044
1433
|
run.current_node = currentNode;
|
|
1045
|
-
if (
|
|
1046
|
-
run
|
|
1434
|
+
if (successFailure) {
|
|
1435
|
+
assignRunFailure(run, successFailure, failureMessage ?? successFailure.causeMessage);
|
|
1436
|
+
}
|
|
1437
|
+
else {
|
|
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
|
+
}
|
|
1447
|
+
}
|
|
1047
1448
|
if (nodeTaskId) {
|
|
1048
1449
|
run.tasks = run.tasks ?? {};
|
|
1049
1450
|
run.tasks[nodeId] = nodeTaskId;
|
|
@@ -1052,92 +1453,348 @@ class RunManager {
|
|
|
1052
1453
|
run.audit.push(successAudit);
|
|
1053
1454
|
});
|
|
1054
1455
|
}
|
|
1055
|
-
async
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1456
|
+
async claimAutoRunLease(execId, nodeId, preferredToken) {
|
|
1457
|
+
let claimed = false;
|
|
1458
|
+
const updated = await this.atomicUpdateAsync(execId, (run) => {
|
|
1459
|
+
if (isTerminalStatus(run.status))
|
|
1460
|
+
return;
|
|
1461
|
+
const existing = run.auto_run;
|
|
1462
|
+
const ownedByThisProcess = existing?.pid === process.pid && typeof existing.token === 'string';
|
|
1463
|
+
if (existing && !ownedByThisProcess && isProcessAlive(existing.pid)) {
|
|
1464
|
+
throw new Error(`Workflow run "${execId}" is already owned by another runner process`);
|
|
1465
|
+
}
|
|
1466
|
+
const token = preferredToken ?? (ownedByThisProcess ? existing.token : (0, sdk_1.generateId)('wfr'));
|
|
1467
|
+
if (!isTerminalStatus(run.status)) {
|
|
1468
|
+
run.status = 'running';
|
|
1469
|
+
}
|
|
1470
|
+
run.auto_run = {
|
|
1471
|
+
pid: process.pid,
|
|
1472
|
+
token,
|
|
1473
|
+
node_id: nodeId,
|
|
1474
|
+
started_at: ownedByThisProcess && existing.token === token
|
|
1475
|
+
? existing.started_at
|
|
1476
|
+
: new Date().toISOString(),
|
|
1073
1477
|
};
|
|
1478
|
+
delete run.manual_resume;
|
|
1479
|
+
claimed = true;
|
|
1480
|
+
});
|
|
1481
|
+
return claimed ? updated.auto_run?.token ?? null : null;
|
|
1482
|
+
}
|
|
1483
|
+
async releaseAutoRunLease(execId, token) {
|
|
1484
|
+
await this.atomicUpdateAsync(execId, (run) => {
|
|
1485
|
+
if (run.auto_run?.token === token)
|
|
1486
|
+
delete run.auto_run;
|
|
1487
|
+
});
|
|
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;
|
|
1074
1561
|
}
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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;
|
|
1082
1572
|
while (true) {
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
return { run, outcome: '
|
|
1086
|
-
if (run.status === 'failed')
|
|
1087
|
-
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1088
|
-
if (run.status === 'cancelled')
|
|
1089
|
-
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1090
|
-
const nodeId = run.current_node || firstNode;
|
|
1091
|
-
if (!nodeId)
|
|
1092
|
-
return { run, outcome: 'failed', reason: 'workflow has no nodes', executed };
|
|
1093
|
-
const node = spec.nodes[nodeId];
|
|
1094
|
-
if (!node) {
|
|
1095
|
-
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1096
|
-
r.current_node = nodeId;
|
|
1097
|
-
r.status = 'failed';
|
|
1098
|
-
r.error = `node "${nodeId}" not found in workflow`;
|
|
1099
|
-
});
|
|
1100
|
-
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1573
|
+
try {
|
|
1574
|
+
const run = await this.executeNode(execId, nodeId, { trigger: 'auto' });
|
|
1575
|
+
return { run, runnerToken: token, outcome: 'success' };
|
|
1101
1576
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
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;
|
|
1104
1625
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
async runAuto(execId) {
|
|
1629
|
+
const leaseKey = `${this.resolvedDir}::${execId}`;
|
|
1630
|
+
if (inFlightAutoRuns.has(leaseKey)) {
|
|
1631
|
+
const run = this.loadRun(execId);
|
|
1632
|
+
if (!run)
|
|
1633
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1634
|
+
console.log(`runAuto "${execId}": already in-flight — skipping duplicate auto-run (single-flight guard)`);
|
|
1635
|
+
return { run, outcome: 'paused', reason: 'execution already in-flight (single-flight guard)', executed: [] };
|
|
1636
|
+
}
|
|
1637
|
+
inFlightAutoRuns.add(leaseKey);
|
|
1638
|
+
let runnerToken;
|
|
1639
|
+
try {
|
|
1640
|
+
const initial = this.loadRun(execId);
|
|
1641
|
+
if (!initial)
|
|
1642
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1643
|
+
if (initial.auto_run?.pid === process.pid) {
|
|
1644
|
+
runnerToken = initial.auto_run.token;
|
|
1107
1645
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1646
|
+
const doc = this.loadWorkflowYaml(initial.workflow_file);
|
|
1647
|
+
if (!doc) {
|
|
1648
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Failed to load workflow file: ${initial.workflow_file}`);
|
|
1110
1649
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1115
|
-
r.status = 'failed';
|
|
1116
|
-
r.error = `auto runner exceeded max_iterations (${maxIterations}); possible loop at "${nodeId}"`;
|
|
1117
|
-
});
|
|
1118
|
-
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1650
|
+
const spec = doc.workflows[initial.workflow_id];
|
|
1651
|
+
if (!spec) {
|
|
1652
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Workflow "${initial.workflow_id}" not found in ${initial.workflow_file}`);
|
|
1119
1653
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1654
|
+
const executed = [];
|
|
1655
|
+
if (initial.status === 'completed' || initial.status === 'failed' || initial.status === 'cancelled') {
|
|
1656
|
+
return {
|
|
1657
|
+
run: initial,
|
|
1658
|
+
outcome: initial.status === 'completed' ? 'completed' : initial.status === 'cancelled' ? 'cancelled' : 'failed',
|
|
1659
|
+
reason: `run already ${initial.status}`,
|
|
1660
|
+
executed,
|
|
1661
|
+
};
|
|
1123
1662
|
}
|
|
1124
|
-
|
|
1663
|
+
const firstNode = Object.keys(spec.nodes ?? {})[0];
|
|
1664
|
+
const maxIterations = (typeof spec.max_iterations === 'number' && spec.max_iterations > 0)
|
|
1665
|
+
? spec.max_iterations
|
|
1666
|
+
: 30;
|
|
1667
|
+
let run = initial;
|
|
1668
|
+
while (true) {
|
|
1125
1669
|
run = this.loadRun(execId);
|
|
1126
|
-
|
|
1670
|
+
if (run.status === 'completed')
|
|
1671
|
+
return { run, outcome: 'completed', executed };
|
|
1672
|
+
if (run.status === 'failed')
|
|
1673
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1674
|
+
if (run.status === 'cancelled')
|
|
1675
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1676
|
+
const nodeId = run.current_node || firstNode;
|
|
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
|
+
}
|
|
1692
|
+
const node = spec.nodes[nodeId];
|
|
1693
|
+
if (!node) {
|
|
1694
|
+
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1695
|
+
r.current_node = nodeId;
|
|
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');
|
|
1706
|
+
});
|
|
1707
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
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
|
+
}
|
|
1717
|
+
if (node.type === 'approval') {
|
|
1718
|
+
if (run.current_node !== nodeId) {
|
|
1719
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1720
|
+
}
|
|
1721
|
+
return { run, outcome: 'paused', reason: `approval node "${nodeId}" requires manual decision`, executed };
|
|
1722
|
+
}
|
|
1723
|
+
if (node.type === 'parallel') {
|
|
1724
|
+
if (run.current_node !== nodeId) {
|
|
1725
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1726
|
+
}
|
|
1727
|
+
return { run, outcome: 'paused', reason: `parallel auto-run not implemented (node "${nodeId}")`, executed };
|
|
1728
|
+
}
|
|
1729
|
+
if (!KNOWN_AUTO_NODE_TYPES.has(node.type)) {
|
|
1730
|
+
if (run.current_node !== nodeId) {
|
|
1731
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1732
|
+
}
|
|
1733
|
+
return { run, outcome: 'paused', reason: `unknown node type "${node.type}" (node "${nodeId}")`, executed };
|
|
1734
|
+
}
|
|
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;
|
|
1746
|
+
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1747
|
+
const engine = readRunEngineState(r);
|
|
1748
|
+
engine.visitCounts[nodeId] = (engine.visitCounts[nodeId] ?? 0) + 1;
|
|
1749
|
+
engine.iterations += 1;
|
|
1750
|
+
r.state['__engine'] = engine;
|
|
1751
|
+
if (engine.visitCounts[nodeId] > maxIterations || engine.iterations > maxIterations) {
|
|
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');
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
if (run.status === 'failed') {
|
|
1765
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1766
|
+
}
|
|
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 };
|
|
1772
|
+
}
|
|
1773
|
+
executed.push(nodeId);
|
|
1774
|
+
if (run.status === 'completed')
|
|
1775
|
+
return { run, outcome: 'completed', executed };
|
|
1776
|
+
if (run.status === 'failed')
|
|
1777
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1778
|
+
if (run.status === 'cancelled')
|
|
1779
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1780
|
+
if (!SELF_ADVANCING_NODE_TYPES.has(node.type)) {
|
|
1781
|
+
const next = node.next;
|
|
1782
|
+
if (!next) {
|
|
1783
|
+
return { run, outcome: 'paused', reason: `node "${nodeId}" has no "next"; stopping`, executed };
|
|
1784
|
+
}
|
|
1785
|
+
run = this.moveNode(execId, next);
|
|
1786
|
+
}
|
|
1127
1787
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
if (
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
const next = node.next;
|
|
1136
|
-
if (!next) {
|
|
1137
|
-
return { run, outcome: 'paused', reason: `node "${nodeId}" has no "next"; stopping`, executed };
|
|
1788
|
+
}
|
|
1789
|
+
finally {
|
|
1790
|
+
if (runnerToken) {
|
|
1791
|
+
try {
|
|
1792
|
+
await this.releaseAutoRunLease(execId, runnerToken);
|
|
1793
|
+
}
|
|
1794
|
+
catch {
|
|
1138
1795
|
}
|
|
1139
|
-
run = this.moveNode(execId, next);
|
|
1140
1796
|
}
|
|
1797
|
+
inFlightAutoRuns.delete(leaseKey);
|
|
1141
1798
|
}
|
|
1142
1799
|
}
|
|
1143
1800
|
cancelRun(execId, reason = 'Cancelled by user') {
|
|
@@ -1146,21 +1803,19 @@ class RunManager {
|
|
|
1146
1803
|
return;
|
|
1147
1804
|
}
|
|
1148
1805
|
const now = new Date();
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
summary: reason,
|
|
1157
|
-
started_at: now.toISOString(),
|
|
1158
|
-
ended_at: now.toISOString(),
|
|
1159
|
-
duration_ms: 0,
|
|
1160
|
-
exit_code: null,
|
|
1161
|
-
error: reason,
|
|
1162
|
-
trigger: 'manual',
|
|
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,
|
|
1163
1813
|
});
|
|
1814
|
+
assignRunFailure(run, failure, reason);
|
|
1815
|
+
run.status = 'cancelled';
|
|
1816
|
+
delete run.auto_run;
|
|
1817
|
+
delete run.manual_resume;
|
|
1818
|
+
appendFailureAudit(run, failure, 'cancel', 'manual', now.toISOString());
|
|
1164
1819
|
});
|
|
1165
1820
|
}
|
|
1166
1821
|
reset(execId) {
|
|
@@ -1168,6 +1823,73 @@ class RunManager {
|
|
|
1168
1823
|
run.state = JSON.parse(JSON.stringify(run.initial_state));
|
|
1169
1824
|
run.current_node = '';
|
|
1170
1825
|
run.completed_nodes = [];
|
|
1826
|
+
run.status = 'pending';
|
|
1827
|
+
delete run.error;
|
|
1828
|
+
delete run.last_failure;
|
|
1829
|
+
delete run.node_attempts;
|
|
1830
|
+
delete run.auto_run;
|
|
1831
|
+
delete run.manual_resume;
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
retry(execId, options = {}) {
|
|
1835
|
+
const snapshot = this.loadRun(execId);
|
|
1836
|
+
if (!snapshot)
|
|
1837
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1838
|
+
const doc = this.loadWorkflowYaml(snapshot.workflow_file);
|
|
1839
|
+
if (!doc)
|
|
1840
|
+
throw new Error(`Failed to load workflow file: ${snapshot.workflow_file}`);
|
|
1841
|
+
const spec = doc.workflows[snapshot.workflow_id];
|
|
1842
|
+
if (!spec)
|
|
1843
|
+
throw new Error(`Workflow "${snapshot.workflow_id}" not found`);
|
|
1844
|
+
if (snapshot.status !== 'failed') {
|
|
1845
|
+
throw new Error(`Run "${execId}" is not failed (status=${snapshot.status ?? 'running'}); only failed runs can be retried`);
|
|
1846
|
+
}
|
|
1847
|
+
if (!isRetryableFailure(snapshot, spec)) {
|
|
1848
|
+
throw new Error(`Run "${execId}" failed at a non-retryable node "${snapshot.current_node}" (error node); use reset to restart`);
|
|
1849
|
+
}
|
|
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
|
+
}
|
|
1854
|
+
const failedNode = run.current_node;
|
|
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';
|
|
1865
|
+
delete run.error;
|
|
1866
|
+
delete run.manual_resume;
|
|
1867
|
+
run.retry_count = (run.retry_count ?? 0) + 1;
|
|
1868
|
+
run.audit = run.audit ?? [];
|
|
1869
|
+
const now = new Date().toISOString();
|
|
1870
|
+
if (options.autoRun === false) {
|
|
1871
|
+
delete run.auto_run;
|
|
1872
|
+
run.manual_resume = { node_id: failedNode, requested_at: now };
|
|
1873
|
+
}
|
|
1874
|
+
else {
|
|
1875
|
+
run.auto_run = {
|
|
1876
|
+
pid: process.pid,
|
|
1877
|
+
token: (0, sdk_1.generateId)('wfr'),
|
|
1878
|
+
node_id: failedNode,
|
|
1879
|
+
started_at: now,
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
run.audit.push({
|
|
1883
|
+
exec_id: run.id,
|
|
1884
|
+
node_id: failedNode,
|
|
1885
|
+
type: 'retry',
|
|
1886
|
+
summary: `manual retry #${run.retry_count}`,
|
|
1887
|
+
started_at: now,
|
|
1888
|
+
ended_at: now,
|
|
1889
|
+
duration_ms: 0,
|
|
1890
|
+
exit_code: null,
|
|
1891
|
+
trigger: 'manual',
|
|
1892
|
+
});
|
|
1171
1893
|
});
|
|
1172
1894
|
}
|
|
1173
1895
|
listRuns(opts) {
|
|
@@ -1176,7 +1898,10 @@ class RunManager {
|
|
|
1176
1898
|
const runs = [];
|
|
1177
1899
|
for (const file of files) {
|
|
1178
1900
|
try {
|
|
1179
|
-
const
|
|
1901
|
+
const execId = file.slice(0, -'.json'.length);
|
|
1902
|
+
const run = this.loadRun(execId);
|
|
1903
|
+
if (!run)
|
|
1904
|
+
continue;
|
|
1180
1905
|
if (opts?.workflowId && run.workflow_id !== opts.workflowId)
|
|
1181
1906
|
continue;
|
|
1182
1907
|
runs.push(run);
|