@crewx/workflow 0.3.22-rc.14 → 0.3.22-rc.140
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 +40 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +266 -9
- package/dist/cli.js.map +1 -1
- package/dist/src/card-state.d.ts +20 -0
- package/dist/src/card-state.d.ts.map +1 -0
- package/dist/src/card-state.js +121 -0
- package/dist/src/card-state.js.map +1 -0
- package/dist/src/engine.d.ts +9 -2
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +68 -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 +120 -0
- package/dist/src/parent-task-sync.d.ts.map +1 -0
- package/dist/src/parent-task-sync.js +307 -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 +35 -2
- package/dist/src/run-manager.d.ts.map +1 -1
- package/dist/src/run-manager.js +1182 -219
- 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/src/workflow-persistence.d.ts +48 -0
- package/dist/src/workflow-persistence.d.ts.map +1 -0
- package/dist/src/workflow-persistence.js +224 -0
- package/dist/src/workflow-persistence.js.map +1 -0
- 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,40 +33,72 @@ 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.WORKFLOW_CANCEL_GRACE_MS = 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_CANCEL_GRACE_MS = 2000;
|
|
59
|
+
const WORKFLOW_CANCEL_POLL_INTERVAL_MS = 100;
|
|
60
|
+
exports.WORKFLOW_RUN_INTERRUPTED_ERROR = 'Workflow run interrupted: runner process exited before completion';
|
|
61
|
+
exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS = 10 * 60000;
|
|
62
|
+
const RETRY_WAIT_SLICE_MS = 1000;
|
|
63
|
+
function sleep(ms) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
setTimeout(resolve, ms);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
52
68
|
const KNOWN_AUTO_NODE_TYPES = new Set([
|
|
53
|
-
'agent_task', 'skill_task', 'shell_task', 'branch', 'expression', 'join', 'end',
|
|
69
|
+
'agent_task', 'skill_task', 'shell_task', 'branch', 'expression', 'join', 'end', 'error',
|
|
54
70
|
]);
|
|
55
|
-
const SELF_ADVANCING_NODE_TYPES = new Set(['branch', 'expression', 'end']);
|
|
56
|
-
function
|
|
71
|
+
const SELF_ADVANCING_NODE_TYPES = new Set(['branch', 'expression', 'end', 'error']);
|
|
72
|
+
function resolveCrewxChildInvocation(env, cwd) {
|
|
73
|
+
const resolution = (0, sdk_1.resolveCrewxExecutable)({ env, cwd });
|
|
74
|
+
if (!resolution.ok) {
|
|
75
|
+
throw (0, failure_1.createWorkflowFailureError)('CLI_UNRESOLVABLE', (0, sdk_1.formatCrewxExecutableFailure)(resolution));
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
argv: resolution.argv,
|
|
79
|
+
env: {
|
|
80
|
+
...env,
|
|
81
|
+
CREWX_CLI_ARGV: JSON.stringify(resolution.argv),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function killProcessGroup(child, signal = 'SIGKILL') {
|
|
57
86
|
try {
|
|
58
87
|
if (process.platform === 'win32') {
|
|
59
88
|
if (child.pid) {
|
|
60
|
-
|
|
89
|
+
const args = ['/pid', String(child.pid), '/T'];
|
|
90
|
+
if (signal === 'SIGKILL')
|
|
91
|
+
args.push('/F');
|
|
92
|
+
cp.spawn('taskkill', args, { stdio: 'ignore' });
|
|
61
93
|
}
|
|
62
94
|
}
|
|
63
95
|
else if (child.pid) {
|
|
64
96
|
try {
|
|
65
|
-
process.kill(-child.pid,
|
|
97
|
+
process.kill(-child.pid, signal);
|
|
66
98
|
}
|
|
67
99
|
catch {
|
|
68
100
|
try {
|
|
69
|
-
child.kill(
|
|
101
|
+
child.kill(signal);
|
|
70
102
|
}
|
|
71
103
|
catch { }
|
|
72
104
|
}
|
|
@@ -76,62 +108,246 @@ function killProcessGroup(child) {
|
|
|
76
108
|
}
|
|
77
109
|
async function runProcess(command, argv, opts) {
|
|
78
110
|
return new Promise((resolve) => {
|
|
111
|
+
const isCancelled = () => {
|
|
112
|
+
try {
|
|
113
|
+
return opts.cancellation?.isCancelled() === true;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
if (isCancelled()) {
|
|
120
|
+
resolve({
|
|
121
|
+
status: null,
|
|
122
|
+
stdout: '',
|
|
123
|
+
stderr: '',
|
|
124
|
+
timedOut: false,
|
|
125
|
+
cancelled: true,
|
|
126
|
+
signal: null,
|
|
127
|
+
errorOrigin: null,
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
79
131
|
let child;
|
|
80
132
|
try {
|
|
81
133
|
child = cp.spawn(command, argv, {
|
|
82
134
|
cwd: opts.cwd,
|
|
83
135
|
env: opts.env,
|
|
84
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
136
|
+
stdio: [opts.stdin !== undefined ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
85
137
|
shell: opts.shell ?? false,
|
|
86
138
|
detached: process.platform !== 'win32',
|
|
87
139
|
windowsHide: opts.windowsHide ?? true,
|
|
88
140
|
});
|
|
89
141
|
}
|
|
90
142
|
catch (err) {
|
|
91
|
-
resolve({
|
|
143
|
+
resolve({
|
|
144
|
+
status: null,
|
|
145
|
+
stdout: '',
|
|
146
|
+
stderr: '',
|
|
147
|
+
timedOut: false,
|
|
148
|
+
signal: null,
|
|
149
|
+
errorOrigin: 'spawn',
|
|
150
|
+
error: err,
|
|
151
|
+
});
|
|
92
152
|
return;
|
|
93
153
|
}
|
|
94
154
|
let stdout = '';
|
|
95
155
|
let stderr = '';
|
|
96
156
|
let settled = false;
|
|
157
|
+
let finishing = false;
|
|
158
|
+
let spawned = false;
|
|
159
|
+
let cancellationRequested = false;
|
|
160
|
+
let postSpawnError;
|
|
161
|
+
let stdinError;
|
|
162
|
+
let timeoutTimer;
|
|
163
|
+
let cancellationEscalationTimer;
|
|
164
|
+
let cancellationPollTimer;
|
|
165
|
+
let activeController;
|
|
166
|
+
const outDecoder = new sdk_1.ConsoleDecoder();
|
|
167
|
+
const errDecoder = new sdk_1.ConsoleDecoder();
|
|
168
|
+
let stdoutDecodeChain = Promise.resolve();
|
|
169
|
+
let stderrDecodeChain = Promise.resolve();
|
|
170
|
+
const cleanup = () => {
|
|
171
|
+
if (timeoutTimer)
|
|
172
|
+
clearTimeout(timeoutTimer);
|
|
173
|
+
if (cancellationEscalationTimer)
|
|
174
|
+
clearTimeout(cancellationEscalationTimer);
|
|
175
|
+
if (cancellationPollTimer)
|
|
176
|
+
clearInterval(cancellationPollTimer);
|
|
177
|
+
if (opts.cancellation
|
|
178
|
+
&& activeController
|
|
179
|
+
&& activeProcessControllers.get(opts.cancellation.key) === activeController) {
|
|
180
|
+
activeProcessControllers.delete(opts.cancellation.key);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
97
183
|
const finish = (result) => {
|
|
98
184
|
if (settled)
|
|
99
185
|
return;
|
|
100
186
|
settled = true;
|
|
101
|
-
|
|
102
|
-
resolve(result);
|
|
187
|
+
cleanup();
|
|
188
|
+
resolve(cancellationRequested ? { ...result, cancelled: true, timedOut: false } : result);
|
|
103
189
|
};
|
|
104
|
-
const
|
|
190
|
+
const requestCancellation = () => {
|
|
191
|
+
if (settled || cancellationRequested)
|
|
192
|
+
return;
|
|
193
|
+
cancellationRequested = true;
|
|
194
|
+
killProcessGroup(child, 'SIGTERM');
|
|
195
|
+
cancellationEscalationTimer = setTimeout(() => {
|
|
196
|
+
if (!settled)
|
|
197
|
+
killProcessGroup(child, 'SIGKILL');
|
|
198
|
+
}, exports.WORKFLOW_CANCEL_GRACE_MS);
|
|
199
|
+
if (typeof cancellationEscalationTimer.unref === 'function') {
|
|
200
|
+
cancellationEscalationTimer.unref();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
if (opts.cancellation) {
|
|
204
|
+
activeController = { cancel: requestCancellation };
|
|
205
|
+
activeProcessControllers.set(opts.cancellation.key, activeController);
|
|
206
|
+
cancellationPollTimer = setInterval(() => {
|
|
207
|
+
if (isCancelled())
|
|
208
|
+
requestCancellation();
|
|
209
|
+
}, WORKFLOW_CANCEL_POLL_INTERVAL_MS);
|
|
210
|
+
if (typeof cancellationPollTimer.unref === 'function')
|
|
211
|
+
cancellationPollTimer.unref();
|
|
212
|
+
}
|
|
213
|
+
timeoutTimer = setTimeout(() => {
|
|
105
214
|
killProcessGroup(child);
|
|
106
|
-
|
|
215
|
+
finishAfterDecode(() => ({
|
|
216
|
+
status: null,
|
|
217
|
+
stdout,
|
|
218
|
+
stderr,
|
|
219
|
+
timedOut: true,
|
|
220
|
+
signal: null,
|
|
221
|
+
errorOrigin: stdinError ? 'stdin' : postSpawnError ? 'post_spawn' : null,
|
|
222
|
+
...(stdinError || postSpawnError ? { error: stdinError ?? postSpawnError } : {}),
|
|
223
|
+
}));
|
|
107
224
|
}, opts.timeout);
|
|
108
|
-
if (typeof
|
|
109
|
-
|
|
110
|
-
child.stdout?.on('data', (d) => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
225
|
+
if (typeof timeoutTimer.unref === 'function')
|
|
226
|
+
timeoutTimer.unref();
|
|
227
|
+
child.stdout?.on('data', (d) => {
|
|
228
|
+
stdoutDecodeChain = stdoutDecodeChain.then(async () => {
|
|
229
|
+
stdout += await outDecoder.write(d);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
child.stderr?.on('data', (d) => {
|
|
233
|
+
stderrDecodeChain = stderrDecodeChain.then(async () => {
|
|
234
|
+
stderr += await errDecoder.write(d);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
child.once('spawn', () => { spawned = true; });
|
|
238
|
+
child.on('error', (err) => {
|
|
239
|
+
if (!spawned) {
|
|
240
|
+
finish({
|
|
241
|
+
status: null,
|
|
242
|
+
stdout,
|
|
243
|
+
stderr,
|
|
244
|
+
timedOut: false,
|
|
245
|
+
signal: null,
|
|
246
|
+
errorOrigin: 'spawn',
|
|
247
|
+
error: err,
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
postSpawnError = postSpawnError ?? err;
|
|
252
|
+
});
|
|
253
|
+
if (opts.stdin !== undefined) {
|
|
254
|
+
child.stdin?.on('error', (err) => {
|
|
255
|
+
stdinError = stdinError ?? err;
|
|
256
|
+
});
|
|
257
|
+
try {
|
|
258
|
+
child.stdin?.end(opts.stdin);
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
stdinError = stdinError ?? err;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const finishAfterDecode = (createResult) => {
|
|
265
|
+
if (settled || finishing)
|
|
266
|
+
return;
|
|
267
|
+
finishing = true;
|
|
268
|
+
void (async () => {
|
|
269
|
+
await Promise.all([stdoutDecodeChain, stderrDecodeChain]);
|
|
270
|
+
const [tailOut, tailErr] = await Promise.all([outDecoder.end(), errDecoder.end()]);
|
|
271
|
+
stdout += tailOut;
|
|
272
|
+
stderr += tailErr;
|
|
273
|
+
finish(createResult());
|
|
274
|
+
})().catch(() => finish(createResult()));
|
|
275
|
+
};
|
|
276
|
+
child.on('close', (code, signal) => finishAfterDecode(() => ({
|
|
277
|
+
status: code,
|
|
278
|
+
stdout,
|
|
279
|
+
stderr,
|
|
280
|
+
timedOut: false,
|
|
281
|
+
signal,
|
|
282
|
+
errorOrigin: stdinError ? 'stdin' : postSpawnError ? 'post_spawn' : null,
|
|
283
|
+
...(stdinError || postSpawnError ? { error: stdinError ?? postSpawnError } : {}),
|
|
284
|
+
})));
|
|
285
|
+
if (isCancelled())
|
|
286
|
+
requestCancellation();
|
|
114
287
|
});
|
|
115
288
|
}
|
|
116
|
-
|
|
117
|
-
|
|
289
|
+
const AUDIT_IDENTIFIER_RE = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
290
|
+
function safeAuditIdentifier(value) {
|
|
291
|
+
if (value === undefined)
|
|
292
|
+
return undefined;
|
|
293
|
+
const match = AUDIT_IDENTIFIER_RE.exec(value);
|
|
294
|
+
return match?.[0] === value ? value : undefined;
|
|
118
295
|
}
|
|
119
296
|
function summarizeNode(node) {
|
|
120
297
|
switch (node.type) {
|
|
121
|
-
case 'agent_task':
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
298
|
+
case 'agent_task': {
|
|
299
|
+
const agent = safeAuditIdentifier(node.agent);
|
|
300
|
+
return agent
|
|
301
|
+
? `@${agent} (${node.mode === 'query' ? 'q' : 'x'})`
|
|
302
|
+
: `agent_task (${node.mode === 'query' ? 'q' : 'x'})`;
|
|
303
|
+
}
|
|
304
|
+
case 'skill_task': {
|
|
305
|
+
const skill = safeAuditIdentifier(node.skill);
|
|
306
|
+
return skill ? `skill ${skill}` : 'skill_task';
|
|
307
|
+
}
|
|
125
308
|
case 'shell_task':
|
|
126
|
-
return
|
|
309
|
+
return 'shell_task';
|
|
127
310
|
case 'branch':
|
|
128
|
-
return
|
|
311
|
+
return 'branch';
|
|
129
312
|
case 'expression':
|
|
130
|
-
return
|
|
313
|
+
return 'expression';
|
|
131
314
|
default:
|
|
132
|
-
return node
|
|
315
|
+
return 'workflow node';
|
|
133
316
|
}
|
|
134
317
|
}
|
|
318
|
+
function getErrorMessage(error) {
|
|
319
|
+
if (error instanceof Error)
|
|
320
|
+
return error.message;
|
|
321
|
+
return typeof error === 'string' ? error : 'Node execution failed';
|
|
322
|
+
}
|
|
323
|
+
function toProcessFailureMetadata(result) {
|
|
324
|
+
return {
|
|
325
|
+
status: result.status,
|
|
326
|
+
timedOut: result.timedOut,
|
|
327
|
+
signal: result.signal,
|
|
328
|
+
errorOrigin: result.errorOrigin,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function appendFailureAudit(run, failure, type, trigger, now = new Date().toISOString()) {
|
|
332
|
+
run.audit = run.audit ?? [];
|
|
333
|
+
run.audit.push({
|
|
334
|
+
exec_id: run.id,
|
|
335
|
+
node_id: failure.failedNode,
|
|
336
|
+
type,
|
|
337
|
+
summary: failure.causeMessage,
|
|
338
|
+
started_at: now,
|
|
339
|
+
ended_at: now,
|
|
340
|
+
duration_ms: 0,
|
|
341
|
+
exit_code: failure.exitCode,
|
|
342
|
+
error: failure.causeMessage,
|
|
343
|
+
trigger,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function assignRunFailure(run, failure, rawError) {
|
|
347
|
+
run.status = 'failed';
|
|
348
|
+
run.error = (0, failure_1.sanitizeLocalRunError)(rawError, failure.causeMessage);
|
|
349
|
+
run.last_failure = failure;
|
|
350
|
+
}
|
|
135
351
|
async function storeNodeOutput(nodeSpec, nodeId, stdout, stateUpdates) {
|
|
136
352
|
if (!nodeSpec.output)
|
|
137
353
|
return;
|
|
@@ -140,12 +356,12 @@ async function storeNodeOutput(nodeSpec, nodeId, stdout, stateUpdates) {
|
|
|
140
356
|
let parsed;
|
|
141
357
|
try {
|
|
142
358
|
parsed = JSON.parse((0, output_format_1.extractJson)(trimmed));
|
|
359
|
+
if (nodeSpec.output_schema) {
|
|
360
|
+
await (0, output_format_1.validateJsonSchema)(parsed, nodeSpec.output_schema);
|
|
361
|
+
}
|
|
143
362
|
}
|
|
144
363
|
catch (e) {
|
|
145
|
-
throw
|
|
146
|
-
}
|
|
147
|
-
if (nodeSpec.output_schema) {
|
|
148
|
-
await (0, output_format_1.validateJsonSchema)(parsed, nodeSpec.output_schema);
|
|
364
|
+
throw (0, failure_1.createWorkflowFailureError)('OUTPUT_INVALID', `Node "${nodeId}" output_format=json parse or validation failed: ${getErrorMessage(e)}`);
|
|
149
365
|
}
|
|
150
366
|
stateUpdates[nodeSpec.output] = parsed;
|
|
151
367
|
}
|
|
@@ -521,12 +737,99 @@ function safeEvaluateBranchCondition(expr, state) {
|
|
|
521
737
|
function validateSafeBranchCondition(expr) {
|
|
522
738
|
safeEvaluateBranchCondition(expr, {});
|
|
523
739
|
}
|
|
740
|
+
function readRunEngineState(run) {
|
|
741
|
+
const raw = run.state['__engine'];
|
|
742
|
+
if (!raw || typeof raw !== 'object')
|
|
743
|
+
return { visitCounts: {}, iterations: 0 };
|
|
744
|
+
const record = raw;
|
|
745
|
+
const rawCounts = record['visitCounts'];
|
|
746
|
+
const visitCounts = {};
|
|
747
|
+
if (rawCounts && typeof rawCounts === 'object') {
|
|
748
|
+
for (const [key, value] of Object.entries(rawCounts)) {
|
|
749
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
750
|
+
visitCounts[key] = value;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const iterations = typeof record['iterations'] === 'number' && Number.isFinite(record['iterations'])
|
|
754
|
+
? record['iterations']
|
|
755
|
+
: 0;
|
|
756
|
+
return { visitCounts, iterations };
|
|
757
|
+
}
|
|
758
|
+
const inFlightAutoRuns = new Set();
|
|
759
|
+
const activeProcessControllers = new Map();
|
|
760
|
+
function processControllerKey(resolvedDir, execId) {
|
|
761
|
+
return `${resolvedDir}::${execId}`;
|
|
762
|
+
}
|
|
763
|
+
function requestActiveProcessCancellation(resolvedDir, execId) {
|
|
764
|
+
activeProcessControllers.get(processControllerKey(resolvedDir, execId))?.cancel();
|
|
765
|
+
}
|
|
766
|
+
function isProcessAlive(pid) {
|
|
767
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
768
|
+
return false;
|
|
769
|
+
if (pid === process.pid)
|
|
770
|
+
return true;
|
|
771
|
+
try {
|
|
772
|
+
process.kill(pid, 0);
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
return error.code === 'EPERM';
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function isTerminalStatus(status) {
|
|
780
|
+
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
781
|
+
}
|
|
782
|
+
function isRecentLegacyRetry(run, nowMs) {
|
|
783
|
+
if (run.status !== undefined || run.auto_run)
|
|
784
|
+
return false;
|
|
785
|
+
const lastAudit = run.audit?.at(-1);
|
|
786
|
+
if (lastAudit?.type !== 'retry')
|
|
787
|
+
return false;
|
|
788
|
+
const updatedAt = Date.parse(run.updated_at);
|
|
789
|
+
return Number.isFinite(updatedAt)
|
|
790
|
+
&& nowMs - updatedAt < exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS;
|
|
791
|
+
}
|
|
792
|
+
function normalizeRunStatus(run, nowMs = Date.now()) {
|
|
793
|
+
if (run.status !== undefined)
|
|
794
|
+
return run;
|
|
795
|
+
let status;
|
|
796
|
+
if (run.current_node === '') {
|
|
797
|
+
status = 'pending';
|
|
798
|
+
}
|
|
799
|
+
else if (run.auto_run) {
|
|
800
|
+
status = 'running';
|
|
801
|
+
}
|
|
802
|
+
else if (isRecentLegacyRetry(run, nowMs)) {
|
|
803
|
+
status = 'retrying';
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
status = 'pending';
|
|
807
|
+
}
|
|
808
|
+
return { ...run, status };
|
|
809
|
+
}
|
|
810
|
+
function isRetryableFailure(run, spec) {
|
|
811
|
+
if (run.status !== 'failed')
|
|
812
|
+
return false;
|
|
813
|
+
if (!run.current_node)
|
|
814
|
+
return false;
|
|
815
|
+
const node = spec.nodes?.[run.current_node];
|
|
816
|
+
if (!node)
|
|
817
|
+
return false;
|
|
818
|
+
return node.type !== 'error';
|
|
819
|
+
}
|
|
524
820
|
class RunManager {
|
|
525
|
-
constructor(runsDir = '.crewx/workflow-runs', projectRoot) {
|
|
821
|
+
constructor(runsDir = '.crewx/workflow-runs', projectRoot, deps) {
|
|
526
822
|
this.runsDir = runsDir;
|
|
823
|
+
this.sleep = deps?.sleep ?? sleep;
|
|
824
|
+
this.onRunPersisted = deps?.onRunPersisted;
|
|
527
825
|
const base = projectRoot || process.env.CREWX_WORKSPACE || process.cwd();
|
|
528
826
|
this.resolvedDir = path.resolve(base, this.runsDir);
|
|
529
827
|
this.projectRoot = path.resolve(base);
|
|
828
|
+
const hasConfig = fs.existsSync(path.join(this.projectRoot, 'crewx.yaml'))
|
|
829
|
+
|| fs.existsSync(path.join(this.projectRoot, 'crewx.yml'));
|
|
830
|
+
if (!hasConfig) {
|
|
831
|
+
console.warn(`[RunManager] projectRoot "${this.projectRoot}" has no crewx.yaml/crewx.yml — agent_task nodes may fail to resolve agents.`);
|
|
832
|
+
}
|
|
530
833
|
}
|
|
531
834
|
ensureRunsDir() {
|
|
532
835
|
if (!fs.existsSync(this.resolvedDir)) {
|
|
@@ -538,8 +841,21 @@ class RunManager {
|
|
|
538
841
|
throw new Error(`Invalid execution ID: "${execId}"`);
|
|
539
842
|
}
|
|
540
843
|
}
|
|
541
|
-
|
|
542
|
-
|
|
844
|
+
isRunCancelled(execId) {
|
|
845
|
+
try {
|
|
846
|
+
return this.readRunRaw(execId)?.status === 'cancelled';
|
|
847
|
+
}
|
|
848
|
+
catch {
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
processCancellation(execId) {
|
|
853
|
+
return {
|
|
854
|
+
key: processControllerKey(this.resolvedDir, execId),
|
|
855
|
+
isCancelled: () => this.isRunCancelled(execId),
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
readRunRaw(execId) {
|
|
543
859
|
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
544
860
|
if (!fs.existsSync(filePath))
|
|
545
861
|
return null;
|
|
@@ -550,11 +866,145 @@ class RunManager {
|
|
|
550
866
|
throw new Error(`Failed to parse execution file ${filePath}: ${e.message}`);
|
|
551
867
|
}
|
|
552
868
|
}
|
|
869
|
+
readRun(execId) {
|
|
870
|
+
this.validateExecId(execId);
|
|
871
|
+
const run = this.readRunRaw(execId);
|
|
872
|
+
return run ? normalizeRunStatus(run) : null;
|
|
873
|
+
}
|
|
874
|
+
isLegacyStatuslessStale(run) {
|
|
875
|
+
if (run.status !== undefined
|
|
876
|
+
|| run.auto_run
|
|
877
|
+
|| run.manual_resume
|
|
878
|
+
|| !run.current_node
|
|
879
|
+
|| !run.audit?.length) {
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
const updatedAt = Date.parse(run.updated_at);
|
|
883
|
+
if (!Number.isFinite(updatedAt) || Date.now() - updatedAt < exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS) {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
const doc = this.loadWorkflowYaml(run.workflow_file);
|
|
887
|
+
const spec = doc?.workflows[run.workflow_id];
|
|
888
|
+
const node = spec?.nodes?.[run.current_node];
|
|
889
|
+
return Boolean(node && KNOWN_AUTO_NODE_TYPES.has(node.type));
|
|
890
|
+
}
|
|
891
|
+
withRunLock(execId, callback) {
|
|
892
|
+
this.validateExecId(execId);
|
|
893
|
+
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
894
|
+
if (!fs.existsSync(filePath)) {
|
|
895
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
896
|
+
}
|
|
897
|
+
const lock = new file_lock_1.FileLock(filePath);
|
|
898
|
+
lock.acquire();
|
|
899
|
+
try {
|
|
900
|
+
const run = this.readRunRaw(execId);
|
|
901
|
+
if (!run)
|
|
902
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
903
|
+
return callback(normalizeRunStatus(run));
|
|
904
|
+
}
|
|
905
|
+
finally {
|
|
906
|
+
lock.release();
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
hasStaleRunner(run) {
|
|
910
|
+
if (run.auto_run)
|
|
911
|
+
return !isProcessAlive(run.auto_run.pid);
|
|
912
|
+
return this.isLegacyStatuslessStale(run);
|
|
913
|
+
}
|
|
914
|
+
recoverStaleRunner(execId) {
|
|
915
|
+
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
916
|
+
const lock = new file_lock_1.FileLock(filePath);
|
|
917
|
+
lock.acquire();
|
|
918
|
+
try {
|
|
919
|
+
const run = this.readRunRaw(execId);
|
|
920
|
+
if (!run || !this.hasStaleRunner(run))
|
|
921
|
+
return run ? normalizeRunStatus(run) : null;
|
|
922
|
+
if (isTerminalStatus(run.status)) {
|
|
923
|
+
delete run.auto_run;
|
|
924
|
+
delete run.manual_resume;
|
|
925
|
+
this.saveRun(run);
|
|
926
|
+
return run;
|
|
927
|
+
}
|
|
928
|
+
const lease = run.auto_run;
|
|
929
|
+
const nodeId = lease?.node_id || run.current_node;
|
|
930
|
+
const now = new Date().toISOString();
|
|
931
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
932
|
+
failedNode: nodeId,
|
|
933
|
+
attempt: (0, failure_1.readNodeAttempt)(run, nodeId),
|
|
934
|
+
causeCode: 'INTERRUPTED',
|
|
935
|
+
exitCode: null,
|
|
936
|
+
signal: null,
|
|
937
|
+
errorOrigin: null,
|
|
938
|
+
});
|
|
939
|
+
assignRunFailure(run, failure, exports.WORKFLOW_RUN_INTERRUPTED_ERROR);
|
|
940
|
+
delete run.auto_run;
|
|
941
|
+
delete run.manual_resume;
|
|
942
|
+
appendFailureAudit(run, failure, 'interrupted', 'auto', now);
|
|
943
|
+
this.saveRun(run);
|
|
944
|
+
return run;
|
|
945
|
+
}
|
|
946
|
+
finally {
|
|
947
|
+
lock.release();
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
loadRun(execId) {
|
|
951
|
+
this.validateExecId(execId);
|
|
952
|
+
const run = this.readRunRaw(execId);
|
|
953
|
+
if (!run || !this.hasStaleRunner(run))
|
|
954
|
+
return run ? normalizeRunStatus(run) : null;
|
|
955
|
+
return this.recoverStaleRunner(execId);
|
|
956
|
+
}
|
|
553
957
|
saveRun(run) {
|
|
554
958
|
this.ensureRunsDir();
|
|
555
959
|
run.updated_at = new Date().toISOString();
|
|
556
960
|
const filePath = path.join(this.resolvedDir, `${run.id}.json`);
|
|
557
|
-
|
|
961
|
+
let existingMode;
|
|
962
|
+
try {
|
|
963
|
+
existingMode = fs.statSync(filePath).mode & 0o7777;
|
|
964
|
+
}
|
|
965
|
+
catch (error) {
|
|
966
|
+
if (error.code !== 'ENOENT')
|
|
967
|
+
throw error;
|
|
968
|
+
}
|
|
969
|
+
const tempPath = path.join(this.resolvedDir, `.${run.id}.tmp-${process.pid}-${(0, sdk_1.generateId)('wfr')}`);
|
|
970
|
+
const serialized = JSON.stringify(run, null, 2) + '\n';
|
|
971
|
+
let tempFd;
|
|
972
|
+
let tempCreated = false;
|
|
973
|
+
try {
|
|
974
|
+
tempFd = fs.openSync(tempPath, 'wx');
|
|
975
|
+
tempCreated = true;
|
|
976
|
+
fs.writeFileSync(tempFd, serialized, 'utf-8');
|
|
977
|
+
if (existingMode !== undefined)
|
|
978
|
+
fs.fchmodSync(tempFd, existingMode);
|
|
979
|
+
fs.closeSync(tempFd);
|
|
980
|
+
tempFd = undefined;
|
|
981
|
+
fs.renameSync(tempPath, filePath);
|
|
982
|
+
tempCreated = false;
|
|
983
|
+
}
|
|
984
|
+
finally {
|
|
985
|
+
if (tempFd !== undefined) {
|
|
986
|
+
try {
|
|
987
|
+
fs.closeSync(tempFd);
|
|
988
|
+
}
|
|
989
|
+
catch {
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (tempCreated) {
|
|
993
|
+
try {
|
|
994
|
+
fs.unlinkSync(tempPath);
|
|
995
|
+
}
|
|
996
|
+
catch {
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (!this.onRunPersisted)
|
|
1001
|
+
return;
|
|
1002
|
+
try {
|
|
1003
|
+
this.onRunPersisted(run);
|
|
1004
|
+
}
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
console.warn(`[RunManager] onRunPersisted failed for ${run.id}: ${getErrorMessage(error)}`);
|
|
1007
|
+
}
|
|
558
1008
|
}
|
|
559
1009
|
atomicUpdate(execId, updater) {
|
|
560
1010
|
this.validateExecId(execId);
|
|
@@ -563,7 +1013,7 @@ class RunManager {
|
|
|
563
1013
|
const lock = new file_lock_1.FileLock(filePath);
|
|
564
1014
|
lock.acquire();
|
|
565
1015
|
try {
|
|
566
|
-
const run = this.
|
|
1016
|
+
const run = this.readRun(execId);
|
|
567
1017
|
if (!run)
|
|
568
1018
|
throw new Error(`Execution not found: ${execId}`);
|
|
569
1019
|
updater(run);
|
|
@@ -581,7 +1031,7 @@ class RunManager {
|
|
|
581
1031
|
const lock = new file_lock_1.FileLock(filePath);
|
|
582
1032
|
await lock.acquireAsync();
|
|
583
1033
|
try {
|
|
584
|
-
const run = this.
|
|
1034
|
+
const run = this.readRun(execId);
|
|
585
1035
|
if (!run)
|
|
586
1036
|
throw new Error(`Execution not found: ${execId}`);
|
|
587
1037
|
updater(run);
|
|
@@ -592,6 +1042,32 @@ class RunManager {
|
|
|
592
1042
|
lock.release();
|
|
593
1043
|
}
|
|
594
1044
|
}
|
|
1045
|
+
async reserveNodeAttempt(execId, nodeId) {
|
|
1046
|
+
let reservedAttempt = 1;
|
|
1047
|
+
await this.atomicUpdateAsync(execId, (run) => {
|
|
1048
|
+
if (run.status === 'cancelled') {
|
|
1049
|
+
reservedAttempt = (0, failure_1.readNodeAttempt)(run, nodeId);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
const storedAttempt = run.node_attempts?.[nodeId];
|
|
1053
|
+
const previousAttempt = typeof storedAttempt === 'number'
|
|
1054
|
+
&& Number.isInteger(storedAttempt)
|
|
1055
|
+
&& storedAttempt > 0
|
|
1056
|
+
? storedAttempt
|
|
1057
|
+
: 0;
|
|
1058
|
+
reservedAttempt = previousAttempt + 1;
|
|
1059
|
+
run.node_attempts = run.node_attempts ?? {};
|
|
1060
|
+
run.node_attempts[nodeId] = reservedAttempt;
|
|
1061
|
+
});
|
|
1062
|
+
return reservedAttempt;
|
|
1063
|
+
}
|
|
1064
|
+
clearNodeAttempt(run, nodeId) {
|
|
1065
|
+
if (!run.node_attempts)
|
|
1066
|
+
return;
|
|
1067
|
+
delete run.node_attempts[nodeId];
|
|
1068
|
+
if (Object.keys(run.node_attempts).length === 0)
|
|
1069
|
+
delete run.node_attempts;
|
|
1070
|
+
}
|
|
595
1071
|
loadWorkflowYaml(yamlPath) {
|
|
596
1072
|
if (!fs.existsSync(yamlPath))
|
|
597
1073
|
return null;
|
|
@@ -601,6 +1077,12 @@ class RunManager {
|
|
|
601
1077
|
return null;
|
|
602
1078
|
return doc;
|
|
603
1079
|
}
|
|
1080
|
+
getNodeType(workflowFile, workflowId, nodeId) {
|
|
1081
|
+
const doc = this.loadWorkflowYaml(workflowFile);
|
|
1082
|
+
const spec = doc?.workflows[workflowId];
|
|
1083
|
+
const node = spec?.nodes?.[nodeId];
|
|
1084
|
+
return typeof node?.type === 'string' ? node.type : null;
|
|
1085
|
+
}
|
|
604
1086
|
start(yamlPath, workflowId, overrides) {
|
|
605
1087
|
const doc = this.loadWorkflowYaml(yamlPath);
|
|
606
1088
|
if (!doc) {
|
|
@@ -638,6 +1120,7 @@ class RunManager {
|
|
|
638
1120
|
completed_nodes: [],
|
|
639
1121
|
state,
|
|
640
1122
|
initial_state: { ...initialState, ...sets },
|
|
1123
|
+
status: 'pending',
|
|
641
1124
|
};
|
|
642
1125
|
this.saveRun(run);
|
|
643
1126
|
return run;
|
|
@@ -656,6 +1139,8 @@ class RunManager {
|
|
|
656
1139
|
}
|
|
657
1140
|
moveNode(execId, nodeId) {
|
|
658
1141
|
return this.atomicUpdate(execId, (run) => {
|
|
1142
|
+
if (run.status === 'cancelled')
|
|
1143
|
+
return;
|
|
659
1144
|
if (run.current_node && !run.completed_nodes.includes(run.current_node)) {
|
|
660
1145
|
run.completed_nodes.push(run.current_node);
|
|
661
1146
|
}
|
|
@@ -667,29 +1152,50 @@ class RunManager {
|
|
|
667
1152
|
if (!snapshot) {
|
|
668
1153
|
throw new Error(`Execution not found: ${execId}`);
|
|
669
1154
|
}
|
|
1155
|
+
if (snapshot.status === 'cancelled') {
|
|
1156
|
+
throw (0, failure_1.createWorkflowFailureError)('CANCELLED', `Workflow run "${execId}" is already cancelled`);
|
|
1157
|
+
}
|
|
670
1158
|
const doc = this.loadWorkflowYaml(snapshot.workflow_file);
|
|
671
1159
|
if (!doc) {
|
|
672
|
-
throw
|
|
1160
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Failed to load workflow file: ${snapshot.workflow_file}`);
|
|
673
1161
|
}
|
|
674
1162
|
const spec = doc.workflows[snapshot.workflow_id];
|
|
675
1163
|
if (!spec) {
|
|
676
|
-
throw
|
|
1164
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Workflow "${snapshot.workflow_id}" not found in ${snapshot.workflow_file}`);
|
|
677
1165
|
}
|
|
678
1166
|
const nodeSpec = spec.nodes?.[nodeId];
|
|
679
1167
|
if (!nodeSpec) {
|
|
680
|
-
throw
|
|
1168
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" not found in workflow "${snapshot.workflow_id}"`);
|
|
681
1169
|
}
|
|
682
1170
|
const prevNode = snapshot.current_node;
|
|
683
1171
|
const stateUpdates = {};
|
|
684
1172
|
const nodesToComplete = [];
|
|
685
1173
|
let currentNode = nodeId;
|
|
686
1174
|
let status;
|
|
1175
|
+
let failureMessage;
|
|
1176
|
+
let failureCode;
|
|
687
1177
|
let nodeTaskId;
|
|
688
1178
|
const trigger = opts?.trigger ?? 'manual';
|
|
689
1179
|
const startedAt = new Date();
|
|
690
|
-
let nodeStdout = '';
|
|
691
|
-
let nodeStderr = '';
|
|
692
1180
|
let nodeExitCode = null;
|
|
1181
|
+
let nodeSignal = null;
|
|
1182
|
+
let nodeErrorOrigin = null;
|
|
1183
|
+
let processFailure;
|
|
1184
|
+
const recordProcessResult = (result) => {
|
|
1185
|
+
nodeExitCode = result.status;
|
|
1186
|
+
nodeSignal = result.signal;
|
|
1187
|
+
nodeErrorOrigin = result.errorOrigin;
|
|
1188
|
+
processFailure = toProcessFailureMetadata(result);
|
|
1189
|
+
};
|
|
1190
|
+
const reserveAgentTaskId = async () => {
|
|
1191
|
+
const taskId = (0, sdk_1.generateId)('tsk');
|
|
1192
|
+
await this.atomicUpdateAsync(execId, (run) => {
|
|
1193
|
+
run.tasks = run.tasks ?? {};
|
|
1194
|
+
run.tasks[nodeId] = taskId;
|
|
1195
|
+
});
|
|
1196
|
+
nodeTaskId = taskId;
|
|
1197
|
+
return taskId;
|
|
1198
|
+
};
|
|
693
1199
|
const buildAudit = (error) => {
|
|
694
1200
|
const endedAt = new Date();
|
|
695
1201
|
return {
|
|
@@ -705,31 +1211,57 @@ class RunManager {
|
|
|
705
1211
|
trigger,
|
|
706
1212
|
};
|
|
707
1213
|
};
|
|
1214
|
+
const attempt = await this.reserveNodeAttempt(execId, nodeId);
|
|
708
1215
|
try {
|
|
709
1216
|
switch (nodeSpec.type) {
|
|
710
1217
|
case 'agent_task': {
|
|
711
1218
|
if (!nodeSpec.agent) {
|
|
712
|
-
throw
|
|
1219
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" is agent_task but has no agent defined`);
|
|
1220
|
+
}
|
|
1221
|
+
const agentCtx = { ...snapshot.state, state: snapshot.state };
|
|
1222
|
+
const resolvedAgent = interpolateTemplate(nodeSpec.agent, agentCtx).trim();
|
|
1223
|
+
if (!resolvedAgent) {
|
|
1224
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": assignee not resolved (agent field "${nodeSpec.agent}" interpolated to empty)`);
|
|
1225
|
+
}
|
|
1226
|
+
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, agentCtx) : undefined, this.projectRoot);
|
|
1227
|
+
if (!cwdCheck.ok) {
|
|
1228
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cwdCheck.error}`);
|
|
1229
|
+
}
|
|
1230
|
+
const nodeCwd = cwdCheck.resolved;
|
|
1231
|
+
const rootCheck = (0, shell_security_1.normalizeCwd)(undefined, this.projectRoot);
|
|
1232
|
+
if (!rootCheck.ok) {
|
|
1233
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${rootCheck.error}`);
|
|
713
1234
|
}
|
|
1235
|
+
const workspaceRoot = rootCheck.resolved;
|
|
714
1236
|
const input = nodeSpec.input
|
|
715
|
-
? interpolateTemplate(nodeSpec.input,
|
|
1237
|
+
? interpolateTemplate(nodeSpec.input, agentCtx)
|
|
716
1238
|
: '';
|
|
1239
|
+
const preamble = process.env.CREWX_WORKFLOW_PREAMBLE === 'off'
|
|
1240
|
+
? ''
|
|
1241
|
+
: (0, output_format_1.buildWorkflowPreamble)({
|
|
1242
|
+
workflowId: snapshot.workflow_id,
|
|
1243
|
+
nodeId,
|
|
1244
|
+
execId: snapshot.id,
|
|
1245
|
+
});
|
|
717
1246
|
const finalInput = nodeSpec.output_format === 'json'
|
|
718
|
-
? input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
719
|
-
: input;
|
|
1247
|
+
? preamble + input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
1248
|
+
: preamble + input;
|
|
720
1249
|
const mode = nodeSpec.mode === 'query' ? 'q' : 'x';
|
|
721
|
-
const prompt = `@${
|
|
1250
|
+
const prompt = `@${resolvedAgent} ${finalInput}`;
|
|
722
1251
|
if (opts?.dryRun) {
|
|
723
|
-
console.log(`[dry-run] Would execute: crewx ${mode} "@${
|
|
1252
|
+
console.log(`[dry-run] Would execute: crewx ${mode} "@${resolvedAgent} ..."`);
|
|
724
1253
|
break;
|
|
725
1254
|
}
|
|
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);
|
|
1255
|
+
console.log(`Executing: @${resolvedAgent} (${mode} mode)`);
|
|
731
1256
|
const nodeTimeout = Number(process.env[mode === 'q' ? 'CREWX_TIMEOUT_QUERY' : 'CREWX_TIMEOUT_EXECUTE']) || 8 * 3600000;
|
|
732
|
-
const
|
|
1257
|
+
const childInvocation = resolveCrewxChildInvocation({
|
|
1258
|
+
...process.env,
|
|
1259
|
+
CREWX_WORKSPACE: workspaceRoot,
|
|
1260
|
+
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
1261
|
+
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
1262
|
+
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
1263
|
+
}, nodeCwd);
|
|
1264
|
+
const wfArgs = [...childInvocation.argv.slice(1), mode, prompt];
|
|
733
1265
|
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
734
1266
|
wfArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
735
1267
|
}
|
|
@@ -742,39 +1274,49 @@ class RunManager {
|
|
|
742
1274
|
};
|
|
743
1275
|
wfArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
744
1276
|
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
745
|
-
command:
|
|
1277
|
+
command: childInvocation.argv[0],
|
|
1278
|
+
env: childInvocation.env,
|
|
1279
|
+
cwd: nodeCwd,
|
|
746
1280
|
allowShellFallback: true,
|
|
747
1281
|
packageName: 'crewx',
|
|
748
1282
|
});
|
|
749
1283
|
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, wfArgs);
|
|
1284
|
+
const taskId = await reserveAgentTaskId();
|
|
750
1285
|
const result = await runProcess(invocation.command, invocation.argv, {
|
|
751
|
-
cwd:
|
|
1286
|
+
cwd: nodeCwd,
|
|
752
1287
|
env: {
|
|
753
|
-
...
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1288
|
+
...childInvocation.env,
|
|
1289
|
+
CREWX_TRACE_ID: taskId,
|
|
1290
|
+
CREWX_PARENT_TASK_ID: '',
|
|
1291
|
+
CREWX_CALLER_AGENT_ID: '',
|
|
757
1292
|
},
|
|
758
1293
|
timeout: nodeTimeout,
|
|
759
1294
|
shell: invocation.shell ?? false,
|
|
760
1295
|
windowsHide: invocation.windowsHide,
|
|
1296
|
+
cancellation: this.processCancellation(execId),
|
|
761
1297
|
});
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
if (result.error) {
|
|
766
|
-
throw result.error;
|
|
1298
|
+
recordProcessResult(result);
|
|
1299
|
+
if (result.cancelled) {
|
|
1300
|
+
throw (0, failure_1.createWorkflowFailureError)('CANCELLED', `Agent execution cancelled for node "${nodeId}"`);
|
|
767
1301
|
}
|
|
768
1302
|
if (result.timedOut) {
|
|
769
1303
|
throw new Error(`Agent execution timed out after ${nodeTimeout}ms`);
|
|
770
1304
|
}
|
|
1305
|
+
if (result.error) {
|
|
1306
|
+
throw result.error;
|
|
1307
|
+
}
|
|
771
1308
|
const taskIdMatch = result.stderr.match(/crewx kill (tsk_\w+)/);
|
|
772
|
-
|
|
1309
|
+
if (taskIdMatch?.[1])
|
|
1310
|
+
nodeTaskId = taskIdMatch[1];
|
|
773
1311
|
if (result.status !== 0) {
|
|
774
1312
|
const stderr = result.stderr.trim();
|
|
775
1313
|
throw new Error(`Agent execution failed (exit ${result.status}): ${stderr}`);
|
|
776
1314
|
}
|
|
777
1315
|
const output = result.stdout.trim();
|
|
1316
|
+
const signal = (0, output_format_1.detectWorkflowSignal)(output);
|
|
1317
|
+
if (signal) {
|
|
1318
|
+
throw new Error(`Node "${nodeId}" agent signaled failure: ${signal.reason ?? '(no reason given)'}`);
|
|
1319
|
+
}
|
|
778
1320
|
if (nodeSpec.output) {
|
|
779
1321
|
if (nodeSpec.output_format === 'json') {
|
|
780
1322
|
const retryMax = nodeSpec.output_retry ?? 1;
|
|
@@ -787,36 +1329,43 @@ class RunManager {
|
|
|
787
1329
|
retryInput = finalInput +
|
|
788
1330
|
`\n\n## ⚠️ PREVIOUS OUTPUT FAILED JSON.parse: ${lastError.message}\n` +
|
|
789
1331
|
`Return STRICT JSON ONLY. First char "{", last char "}". No prose, no fences.`;
|
|
790
|
-
const retryPrompt = `@${
|
|
791
|
-
const retryArgs = [...
|
|
1332
|
+
const retryPrompt = `@${resolvedAgent} ${retryInput}`;
|
|
1333
|
+
const retryArgs = [...childInvocation.argv.slice(1), mode, retryPrompt];
|
|
792
1334
|
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
793
1335
|
retryArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
794
1336
|
}
|
|
795
1337
|
retryArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
796
1338
|
const retryInvocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, retryArgs);
|
|
1339
|
+
const retryTaskId = await reserveAgentTaskId();
|
|
797
1340
|
const retryResult = await runProcess(retryInvocation.command, retryInvocation.argv, {
|
|
798
|
-
cwd:
|
|
1341
|
+
cwd: nodeCwd,
|
|
799
1342
|
env: {
|
|
800
|
-
...
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1343
|
+
...childInvocation.env,
|
|
1344
|
+
CREWX_TRACE_ID: retryTaskId,
|
|
1345
|
+
CREWX_PARENT_TASK_ID: '',
|
|
1346
|
+
CREWX_CALLER_AGENT_ID: '',
|
|
804
1347
|
},
|
|
805
1348
|
timeout: nodeTimeout,
|
|
806
1349
|
shell: retryInvocation.shell ?? false,
|
|
807
1350
|
windowsHide: retryInvocation.windowsHide,
|
|
1351
|
+
cancellation: this.processCancellation(execId),
|
|
808
1352
|
});
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
throw retryResult.error;
|
|
1353
|
+
recordProcessResult(retryResult);
|
|
1354
|
+
if (retryResult.cancelled) {
|
|
1355
|
+
throw (0, failure_1.createWorkflowFailureError)('CANCELLED', `Agent retry cancelled for node "${nodeId}"`);
|
|
1356
|
+
}
|
|
814
1357
|
if (retryResult.timedOut)
|
|
815
1358
|
throw new Error(`Agent retry timed out after ${nodeTimeout}ms`);
|
|
1359
|
+
if (retryResult.error)
|
|
1360
|
+
throw retryResult.error;
|
|
816
1361
|
if (retryResult.status !== 0) {
|
|
817
1362
|
throw new Error(`Agent retry failed (exit ${retryResult.status}): ${retryResult.stderr.trim()}`);
|
|
818
1363
|
}
|
|
819
1364
|
attemptOutput = retryResult.stdout.trim();
|
|
1365
|
+
const retrySignal = (0, output_format_1.detectWorkflowSignal)(attemptOutput);
|
|
1366
|
+
if (retrySignal) {
|
|
1367
|
+
throw new Error(`Node "${nodeId}" agent signaled failure: ${retrySignal.reason ?? '(no reason given)'}`);
|
|
1368
|
+
}
|
|
820
1369
|
}
|
|
821
1370
|
try {
|
|
822
1371
|
parsed = JSON.parse((0, output_format_1.extractJson)(attemptOutput));
|
|
@@ -830,7 +1379,7 @@ class RunManager {
|
|
|
830
1379
|
}
|
|
831
1380
|
}
|
|
832
1381
|
if (parsed === null) {
|
|
833
|
-
throw
|
|
1382
|
+
throw (0, failure_1.createWorkflowFailureError)('OUTPUT_INVALID', `Node "${nodeId}" output_format=json parse failed after ${retryMax + 1} attempt(s): ${lastError.message}`);
|
|
834
1383
|
}
|
|
835
1384
|
stateUpdates[nodeSpec.output] = parsed;
|
|
836
1385
|
console.log(`Result stored in state.${nodeSpec.output} (JSON object)`);
|
|
@@ -844,52 +1393,64 @@ class RunManager {
|
|
|
844
1393
|
}
|
|
845
1394
|
case 'skill_task': {
|
|
846
1395
|
if (!nodeSpec.skill) {
|
|
847
|
-
throw
|
|
1396
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" is skill_task but has no skill defined`);
|
|
848
1397
|
}
|
|
849
1398
|
if (!shell_security_1.SKILL_NAME_RE.test(nodeSpec.skill)) {
|
|
850
|
-
throw
|
|
1399
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" has invalid skill name "${nodeSpec.skill}" (must match ^[a-z0-9][a-z0-9-]*$)`);
|
|
851
1400
|
}
|
|
852
1401
|
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
853
1402
|
const skillArgs = (nodeSpec.args ?? []).map((a) => interpolateTemplate(String(a), ctx));
|
|
1403
|
+
const skillStdin = nodeSpec.stdin !== undefined
|
|
1404
|
+
? interpolateTemplate(String(nodeSpec.stdin), ctx)
|
|
1405
|
+
: undefined;
|
|
854
1406
|
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
855
|
-
if (!cwdCheck.ok)
|
|
856
|
-
throw
|
|
1407
|
+
if (!cwdCheck.ok) {
|
|
1408
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cwdCheck.error}`);
|
|
1409
|
+
}
|
|
1410
|
+
const skillRootCheck = (0, shell_security_1.normalizeCwd)(undefined, this.projectRoot);
|
|
1411
|
+
if (!skillRootCheck.ok) {
|
|
1412
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${skillRootCheck.error}`);
|
|
1413
|
+
}
|
|
1414
|
+
const skillWorkspaceRoot = skillRootCheck.resolved;
|
|
857
1415
|
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
858
1416
|
if (opts?.dryRun) {
|
|
859
1417
|
console.log(`[dry-run] Would execute: crewx skill ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
860
1418
|
break;
|
|
861
1419
|
}
|
|
862
1420
|
console.log(`Executing skill: ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1421
|
+
const childInvocation = resolveCrewxChildInvocation({
|
|
1422
|
+
...process.env,
|
|
1423
|
+
CREWX_WORKSPACE: skillWorkspaceRoot,
|
|
1424
|
+
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
1425
|
+
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
1426
|
+
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
1427
|
+
}, cwdCheck.resolved);
|
|
1428
|
+
const skillArgv = [...childInvocation.argv.slice(1), 'skill', nodeSpec.skill, ...skillArgs];
|
|
868
1429
|
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
869
|
-
command:
|
|
1430
|
+
command: childInvocation.argv[0],
|
|
1431
|
+
env: childInvocation.env,
|
|
1432
|
+
cwd: cwdCheck.resolved,
|
|
870
1433
|
allowShellFallback: true,
|
|
871
1434
|
packageName: 'crewx',
|
|
872
1435
|
});
|
|
873
1436
|
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, skillArgv);
|
|
874
1437
|
const result = await runProcess(invocation.command, invocation.argv, {
|
|
875
1438
|
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
|
-
},
|
|
1439
|
+
env: childInvocation.env,
|
|
882
1440
|
timeout,
|
|
883
1441
|
shell: invocation.shell ?? false,
|
|
884
1442
|
windowsHide: invocation.windowsHide,
|
|
1443
|
+
stdin: skillStdin,
|
|
1444
|
+
cancellation: this.processCancellation(execId),
|
|
885
1445
|
});
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
throw result.error;
|
|
1446
|
+
recordProcessResult(result);
|
|
1447
|
+
if (result.cancelled) {
|
|
1448
|
+
throw (0, failure_1.createWorkflowFailureError)('CANCELLED', `skill_task "${nodeId}" was cancelled`);
|
|
1449
|
+
}
|
|
891
1450
|
if (result.timedOut)
|
|
892
1451
|
throw new Error(`skill_task "${nodeId}" timed out after ${timeout}ms`);
|
|
1452
|
+
if (result.error)
|
|
1453
|
+
throw result.error;
|
|
893
1454
|
if (result.status !== 0) {
|
|
894
1455
|
throw new Error(`skill_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
895
1456
|
}
|
|
@@ -903,25 +1464,29 @@ class RunManager {
|
|
|
903
1464
|
const shellEnvEnabled = process.env.CREWX_WORKFLOW_SHELL === '1';
|
|
904
1465
|
const shellMetaAllowed = spec.metadata?.shell_task_allowed === true;
|
|
905
1466
|
if (!shellEnvEnabled || !shellMetaAllowed) {
|
|
906
|
-
throw
|
|
1467
|
+
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
1468
|
}
|
|
908
1469
|
const cmdCheck = (0, shell_security_1.validateShellCommand)(nodeSpec.command);
|
|
909
|
-
if (!cmdCheck.ok)
|
|
910
|
-
throw
|
|
1470
|
+
if (!cmdCheck.ok) {
|
|
1471
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cmdCheck.error}`);
|
|
1472
|
+
}
|
|
911
1473
|
const envCheck = (0, shell_security_1.validateEnvKeys)(nodeSpec.env);
|
|
912
|
-
if (!envCheck.ok)
|
|
913
|
-
throw
|
|
1474
|
+
if (!envCheck.ok) {
|
|
1475
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${envCheck.error}`);
|
|
1476
|
+
}
|
|
914
1477
|
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
915
1478
|
const command = nodeSpec.command.map((c) => interpolateTemplate(c, ctx));
|
|
916
1479
|
const postCheck = (0, shell_security_1.validateShellCommand)(command);
|
|
917
|
-
if (!postCheck.ok)
|
|
918
|
-
throw
|
|
1480
|
+
if (!postCheck.ok) {
|
|
1481
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${postCheck.error}`);
|
|
1482
|
+
}
|
|
919
1483
|
const interpolatedEnv = nodeSpec.env
|
|
920
1484
|
? Object.fromEntries(Object.entries(nodeSpec.env).map(([k, v]) => [k, interpolateTemplate(String(v), ctx)]))
|
|
921
1485
|
: undefined;
|
|
922
1486
|
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
923
|
-
if (!cwdCheck.ok)
|
|
924
|
-
throw
|
|
1487
|
+
if (!cwdCheck.ok) {
|
|
1488
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cwdCheck.error}`);
|
|
1489
|
+
}
|
|
925
1490
|
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
926
1491
|
if (opts?.dryRun) {
|
|
927
1492
|
console.log(`[dry-run] Would execute: ${command.join(' ')}`);
|
|
@@ -932,19 +1497,25 @@ class RunManager {
|
|
|
932
1497
|
childEnv.CREWX_WORKFLOW_EXEC_ID = snapshot.id;
|
|
933
1498
|
childEnv.CREWX_WORKFLOW_NODE_ID = nodeId;
|
|
934
1499
|
childEnv.CREWX_WORKFLOW_ID = snapshot.workflow_id;
|
|
1500
|
+
const shellStdin = nodeSpec.stdin !== undefined
|
|
1501
|
+
? interpolateTemplate(String(nodeSpec.stdin), ctx)
|
|
1502
|
+
: undefined;
|
|
935
1503
|
const result = await runProcess(command[0], command.slice(1), {
|
|
936
1504
|
cwd: cwdCheck.resolved,
|
|
937
1505
|
env: childEnv,
|
|
938
1506
|
timeout,
|
|
939
1507
|
shell: false,
|
|
1508
|
+
stdin: shellStdin,
|
|
1509
|
+
cancellation: this.processCancellation(execId),
|
|
940
1510
|
});
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
throw result.error;
|
|
1511
|
+
recordProcessResult(result);
|
|
1512
|
+
if (result.cancelled) {
|
|
1513
|
+
throw (0, failure_1.createWorkflowFailureError)('CANCELLED', `shell_task "${nodeId}" was cancelled`);
|
|
1514
|
+
}
|
|
946
1515
|
if (result.timedOut)
|
|
947
1516
|
throw new Error(`shell_task "${nodeId}" timed out after ${timeout}ms`);
|
|
1517
|
+
if (result.error)
|
|
1518
|
+
throw result.error;
|
|
948
1519
|
if (result.status !== 0) {
|
|
949
1520
|
throw new Error(`shell_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
950
1521
|
}
|
|
@@ -956,13 +1527,13 @@ class RunManager {
|
|
|
956
1527
|
}
|
|
957
1528
|
case 'branch': {
|
|
958
1529
|
if (!nodeSpec.condition) {
|
|
959
|
-
throw
|
|
1530
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Branch node "${nodeId}" has no condition`);
|
|
960
1531
|
}
|
|
961
1532
|
const condResult = String(safeEvaluateBranchCondition(String(nodeSpec.condition), snapshot.state));
|
|
962
1533
|
const branches = nodeSpec.branches;
|
|
963
1534
|
const targetNode = branches?.[condResult] ?? nodeSpec.default;
|
|
964
1535
|
if (!targetNode) {
|
|
965
|
-
throw
|
|
1536
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Branch "${nodeId}": no matching branch for "${condResult}" and no default`);
|
|
966
1537
|
}
|
|
967
1538
|
console.log(`Branch "${nodeId}": condition="${nodeSpec.condition}" → ${condResult} → ${targetNode}`);
|
|
968
1539
|
nodesToComplete.push(nodeId);
|
|
@@ -986,9 +1557,19 @@ class RunManager {
|
|
|
986
1557
|
console.log(`End "${nodeId}": workflow execution completed`);
|
|
987
1558
|
break;
|
|
988
1559
|
}
|
|
1560
|
+
case 'error': {
|
|
1561
|
+
status = 'failed';
|
|
1562
|
+
failureCode = 'UNKNOWN';
|
|
1563
|
+
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
1564
|
+
failureMessage = nodeSpec.message
|
|
1565
|
+
? interpolateTemplate(nodeSpec.message, ctx)
|
|
1566
|
+
: `Workflow failed at error node "${nodeId}"`;
|
|
1567
|
+
console.log(`Error "${nodeId}": workflow execution failed — ${failureMessage}`);
|
|
1568
|
+
break;
|
|
1569
|
+
}
|
|
989
1570
|
case 'expression': {
|
|
990
1571
|
if (!nodeSpec.set || typeof nodeSpec.set !== 'object') {
|
|
991
|
-
throw
|
|
1572
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Expression node "${nodeId}" requires "set" field`);
|
|
992
1573
|
}
|
|
993
1574
|
for (const [key, expr] of Object.entries(nodeSpec.set)) {
|
|
994
1575
|
const result = safeEvaluate(String(expr), snapshot.state);
|
|
@@ -1009,30 +1590,61 @@ class RunManager {
|
|
|
1009
1590
|
default:
|
|
1010
1591
|
console.log(`Unknown node type "${nodeSpec.type}" for node "${nodeId}"`);
|
|
1011
1592
|
}
|
|
1593
|
+
if (nodeSpec.fail_when &&
|
|
1594
|
+
!opts?.dryRun &&
|
|
1595
|
+
(nodeSpec.type === 'agent_task' || nodeSpec.type === 'skill_task' || nodeSpec.type === 'shell_task')) {
|
|
1596
|
+
const judged = safeEvaluateBranchCondition(String(nodeSpec.fail_when), { ...snapshot.state, ...stateUpdates });
|
|
1597
|
+
if (judged === true || judged === 'true') {
|
|
1598
|
+
throw new Error(`Node "${nodeId}" judged failed by fail_when: ${nodeSpec.fail_when}`);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1012
1601
|
}
|
|
1013
1602
|
catch (e) {
|
|
1014
|
-
const errMessage = e
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
const
|
|
1603
|
+
const errMessage = getErrorMessage(e);
|
|
1604
|
+
const causeCode = (0, failure_1.classifyWorkflowFailure)({
|
|
1605
|
+
errorCode: (0, failure_1.getWorkflowFailureCode)(e),
|
|
1606
|
+
process: processFailure,
|
|
1607
|
+
});
|
|
1608
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1609
|
+
failedNode: nodeId,
|
|
1610
|
+
attempt,
|
|
1611
|
+
causeCode,
|
|
1612
|
+
exitCode: causeCode === 'NODE_EXIT_NONZERO' && typeof nodeExitCode === 'number'
|
|
1613
|
+
? nodeExitCode
|
|
1614
|
+
: null,
|
|
1615
|
+
signal: causeCode === 'INTERRUPTED' ? nodeSignal : null,
|
|
1616
|
+
errorOrigin: nodeErrorOrigin,
|
|
1617
|
+
timedOut: processFailure?.timedOut ?? false,
|
|
1618
|
+
});
|
|
1619
|
+
const auditEntry = buildAudit(failure.causeMessage);
|
|
1020
1620
|
await this.atomicUpdateAsync(execId, (run) => {
|
|
1021
1621
|
if (run.status === 'cancelled')
|
|
1022
1622
|
return;
|
|
1623
|
+
Object.assign(run.state, stateUpdates);
|
|
1624
|
+
delete run.manual_resume;
|
|
1023
1625
|
run.current_node = nodeId;
|
|
1024
|
-
run
|
|
1025
|
-
run.error = errMessage;
|
|
1626
|
+
assignRunFailure(run, failure, errMessage);
|
|
1026
1627
|
run.audit = run.audit ?? [];
|
|
1027
1628
|
run.audit.push(auditEntry);
|
|
1028
1629
|
});
|
|
1029
1630
|
throw e;
|
|
1030
1631
|
}
|
|
1031
|
-
const
|
|
1632
|
+
const successFailure = failureCode
|
|
1633
|
+
? (0, failure_1.buildNodeFailure)({
|
|
1634
|
+
failedNode: nodeId,
|
|
1635
|
+
attempt,
|
|
1636
|
+
causeCode: failureCode,
|
|
1637
|
+
exitCode: null,
|
|
1638
|
+
signal: null,
|
|
1639
|
+
errorOrigin: null,
|
|
1640
|
+
})
|
|
1641
|
+
: null;
|
|
1642
|
+
const successAudit = buildAudit(successFailure?.causeMessage);
|
|
1032
1643
|
return this.atomicUpdateAsync(execId, (run) => {
|
|
1033
1644
|
if (run.status === 'cancelled')
|
|
1034
1645
|
return;
|
|
1035
1646
|
Object.assign(run.state, stateUpdates);
|
|
1647
|
+
delete run.manual_resume;
|
|
1036
1648
|
for (const node of nodesToComplete) {
|
|
1037
1649
|
if (!run.completed_nodes.includes(node)) {
|
|
1038
1650
|
run.completed_nodes.push(node);
|
|
@@ -1042,8 +1654,20 @@ class RunManager {
|
|
|
1042
1654
|
run.completed_nodes.push(prevNode);
|
|
1043
1655
|
}
|
|
1044
1656
|
run.current_node = currentNode;
|
|
1045
|
-
if (
|
|
1046
|
-
run
|
|
1657
|
+
if (successFailure) {
|
|
1658
|
+
assignRunFailure(run, successFailure, failureMessage ?? successFailure.causeMessage);
|
|
1659
|
+
}
|
|
1660
|
+
else {
|
|
1661
|
+
delete run.error;
|
|
1662
|
+
delete run.last_failure;
|
|
1663
|
+
this.clearNodeAttempt(run, nodeId);
|
|
1664
|
+
if (status) {
|
|
1665
|
+
run.status = status;
|
|
1666
|
+
}
|
|
1667
|
+
else if (run.status !== 'completed') {
|
|
1668
|
+
run.status = 'running';
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1047
1671
|
if (nodeTaskId) {
|
|
1048
1672
|
run.tasks = run.tasks ?? {};
|
|
1049
1673
|
run.tasks[nodeId] = nodeTaskId;
|
|
@@ -1052,122 +1676,458 @@ class RunManager {
|
|
|
1052
1676
|
run.audit.push(successAudit);
|
|
1053
1677
|
});
|
|
1054
1678
|
}
|
|
1055
|
-
async
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1679
|
+
async claimAutoRunLease(execId, nodeId, preferredToken) {
|
|
1680
|
+
let claimed = false;
|
|
1681
|
+
const updated = await this.atomicUpdateAsync(execId, (run) => {
|
|
1682
|
+
if (isTerminalStatus(run.status))
|
|
1683
|
+
return;
|
|
1684
|
+
const existing = run.auto_run;
|
|
1685
|
+
const ownedByThisProcess = existing?.pid === process.pid && typeof existing.token === 'string';
|
|
1686
|
+
if (existing && !ownedByThisProcess && isProcessAlive(existing.pid)) {
|
|
1687
|
+
throw new Error(`Workflow run "${execId}" is already owned by another runner process`);
|
|
1688
|
+
}
|
|
1689
|
+
const token = preferredToken ?? (ownedByThisProcess ? existing.token : (0, sdk_1.generateId)('wfr'));
|
|
1690
|
+
if (!isTerminalStatus(run.status)) {
|
|
1691
|
+
run.status = 'running';
|
|
1692
|
+
}
|
|
1693
|
+
if (!run.current_node)
|
|
1694
|
+
run.current_node = nodeId;
|
|
1695
|
+
run.auto_run = {
|
|
1696
|
+
pid: process.pid,
|
|
1697
|
+
token,
|
|
1698
|
+
node_id: nodeId,
|
|
1699
|
+
started_at: ownedByThisProcess && existing.token === token
|
|
1700
|
+
? existing.started_at
|
|
1701
|
+
: new Date().toISOString(),
|
|
1073
1702
|
};
|
|
1703
|
+
delete run.manual_resume;
|
|
1704
|
+
claimed = true;
|
|
1705
|
+
});
|
|
1706
|
+
return claimed ? updated.auto_run?.token ?? null : null;
|
|
1707
|
+
}
|
|
1708
|
+
async releaseAutoRunLease(execId, token) {
|
|
1709
|
+
await this.atomicUpdateAsync(execId, (run) => {
|
|
1710
|
+
if (run.auto_run?.token === token)
|
|
1711
|
+
delete run.auto_run;
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
async failAutoConfiguration(execId, nodeId, reason) {
|
|
1715
|
+
return this.atomicUpdateAsync(execId, (run) => {
|
|
1716
|
+
if (run.status === 'cancelled')
|
|
1717
|
+
return;
|
|
1718
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1719
|
+
failedNode: nodeId,
|
|
1720
|
+
attempt: (0, failure_1.readNodeAttempt)(run, nodeId),
|
|
1721
|
+
causeCode: 'CONFIG_INVALID',
|
|
1722
|
+
exitCode: null,
|
|
1723
|
+
signal: null,
|
|
1724
|
+
errorOrigin: null,
|
|
1725
|
+
});
|
|
1726
|
+
assignRunFailure(run, failure, reason);
|
|
1727
|
+
appendFailureAudit(run, failure, 'config', 'auto');
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
async reserveAutoRetry(execId, nodeId, runnerToken, policy, delayMs) {
|
|
1731
|
+
let reserved = false;
|
|
1732
|
+
const run = await this.atomicUpdateAsync(execId, (draft) => {
|
|
1733
|
+
if (draft.status === 'cancelled')
|
|
1734
|
+
return;
|
|
1735
|
+
if (draft.status !== 'failed')
|
|
1736
|
+
return;
|
|
1737
|
+
if (draft.auto_run?.token !== runnerToken)
|
|
1738
|
+
return;
|
|
1739
|
+
const decision = (0, retry_policy_1.decideRetry)({ policy, run: draft, nodeId });
|
|
1740
|
+
if (!decision.retry || decision.attempt === undefined)
|
|
1741
|
+
return;
|
|
1742
|
+
const now = new Date().toISOString();
|
|
1743
|
+
draft.status = 'retrying';
|
|
1744
|
+
delete draft.error;
|
|
1745
|
+
delete draft.manual_resume;
|
|
1746
|
+
draft.audit = draft.audit ?? [];
|
|
1747
|
+
draft.audit.push({
|
|
1748
|
+
exec_id: draft.id,
|
|
1749
|
+
node_id: nodeId,
|
|
1750
|
+
type: 'retry',
|
|
1751
|
+
summary: `auto retry ${decision.attempt + 1}/${policy.max + 1} in ${delayMs}ms`,
|
|
1752
|
+
started_at: now,
|
|
1753
|
+
ended_at: now,
|
|
1754
|
+
duration_ms: 0,
|
|
1755
|
+
exit_code: null,
|
|
1756
|
+
trigger: 'auto',
|
|
1757
|
+
});
|
|
1758
|
+
reserved = true;
|
|
1759
|
+
});
|
|
1760
|
+
return { run, reserved };
|
|
1761
|
+
}
|
|
1762
|
+
async waitForRetry(execId, runnerToken, delayMs) {
|
|
1763
|
+
const inspect = () => {
|
|
1764
|
+
const run = this.loadRun(execId);
|
|
1765
|
+
if (!run)
|
|
1766
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1767
|
+
if (run.status === 'cancelled') {
|
|
1768
|
+
return { run, outcome: 'cancelled', reason: 'run cancelled during retry delay' };
|
|
1769
|
+
}
|
|
1770
|
+
if (run.auto_run?.token !== runnerToken) {
|
|
1771
|
+
return { run, outcome: 'paused', reason: 'auto-run lease lost during retry delay' };
|
|
1772
|
+
}
|
|
1773
|
+
if (run.status !== 'retrying') {
|
|
1774
|
+
return { run, outcome: 'paused', reason: 'run state changed during retry delay' };
|
|
1775
|
+
}
|
|
1776
|
+
return null;
|
|
1777
|
+
};
|
|
1778
|
+
let remaining = Math.max(0, delayMs);
|
|
1779
|
+
while (remaining > 0) {
|
|
1780
|
+
const slice = Math.min(remaining, RETRY_WAIT_SLICE_MS);
|
|
1781
|
+
await this.sleep(slice);
|
|
1782
|
+
remaining -= slice;
|
|
1783
|
+
const stopped = inspect();
|
|
1784
|
+
if (stopped)
|
|
1785
|
+
return stopped;
|
|
1074
1786
|
}
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1787
|
+
const stopped = inspect();
|
|
1788
|
+
if (stopped)
|
|
1789
|
+
return stopped;
|
|
1790
|
+
const run = this.loadRun(execId);
|
|
1791
|
+
if (!run)
|
|
1792
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1793
|
+
return { run, outcome: 'ready' };
|
|
1794
|
+
}
|
|
1795
|
+
async runNodeWithRetry(execId, nodeId, runnerToken, policy) {
|
|
1796
|
+
let token = runnerToken;
|
|
1082
1797
|
while (true) {
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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 };
|
|
1798
|
+
try {
|
|
1799
|
+
const run = await this.executeNode(execId, nodeId, { trigger: 'auto' });
|
|
1800
|
+
if (run.status === 'cancelled') {
|
|
1801
|
+
return { run, runnerToken: token, outcome: 'cancelled', reason: run.error };
|
|
1802
|
+
}
|
|
1803
|
+
return { run, runnerToken: token, outcome: 'success' };
|
|
1101
1804
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1805
|
+
catch (caught) {
|
|
1806
|
+
const run = this.loadRun(execId);
|
|
1807
|
+
if (!run)
|
|
1808
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1809
|
+
if (run.status === 'cancelled') {
|
|
1810
|
+
return { run, runnerToken: token, outcome: 'cancelled', reason: run.error };
|
|
1811
|
+
}
|
|
1812
|
+
if (run.status !== 'failed' || !policy) {
|
|
1813
|
+
return { run, runnerToken: token, outcome: run.status === 'failed' ? 'failed' : 'paused', reason: getErrorMessage(caught) };
|
|
1814
|
+
}
|
|
1815
|
+
const decision = (0, retry_policy_1.decideRetry)({ policy, run, nodeId });
|
|
1816
|
+
if (!decision.retry || decision.attempt === undefined) {
|
|
1817
|
+
return { run, runnerToken: token, outcome: 'failed', reason: getErrorMessage(caught) };
|
|
1818
|
+
}
|
|
1819
|
+
const delayMs = (0, retry_policy_1.computeRetryDelayMs)(policy, decision.attempt);
|
|
1820
|
+
const reservation = await this.reserveAutoRetry(execId, nodeId, token, policy, delayMs);
|
|
1821
|
+
if (!reservation.reserved) {
|
|
1822
|
+
const latest = reservation.run;
|
|
1823
|
+
if (latest.status === 'cancelled') {
|
|
1824
|
+
return { run: latest, runnerToken: token, outcome: 'cancelled', reason: latest.error };
|
|
1825
|
+
}
|
|
1826
|
+
return {
|
|
1827
|
+
run: latest,
|
|
1828
|
+
runnerToken: token,
|
|
1829
|
+
outcome: latest.status === 'failed' ? 'failed' : 'paused',
|
|
1830
|
+
reason: getErrorMessage(caught),
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
const waited = await this.waitForRetry(execId, token, delayMs);
|
|
1834
|
+
if (waited.outcome !== 'ready') {
|
|
1835
|
+
return { run: waited.run, runnerToken: token, outcome: waited.outcome, reason: waited.reason };
|
|
1836
|
+
}
|
|
1837
|
+
const nextToken = await this.claimAutoRunLease(execId, nodeId, token);
|
|
1838
|
+
if (!nextToken) {
|
|
1839
|
+
const latest = this.loadRun(execId);
|
|
1840
|
+
if (!latest)
|
|
1841
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1842
|
+
if (latest.status === 'cancelled') {
|
|
1843
|
+
return { run: latest, runnerToken: token, outcome: 'cancelled', reason: latest.error };
|
|
1844
|
+
}
|
|
1845
|
+
return {
|
|
1846
|
+
run: latest,
|
|
1847
|
+
runnerToken: token,
|
|
1848
|
+
outcome: 'paused',
|
|
1849
|
+
reason: 'run stopped before retry lease could be reclaimed',
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
token = nextToken;
|
|
1104
1853
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
async runAuto(execId) {
|
|
1857
|
+
const leaseKey = `${this.resolvedDir}::${execId}`;
|
|
1858
|
+
if (inFlightAutoRuns.has(leaseKey)) {
|
|
1859
|
+
const run = this.loadRun(execId);
|
|
1860
|
+
if (!run)
|
|
1861
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1862
|
+
console.log(`runAuto "${execId}": already in-flight — skipping duplicate auto-run (single-flight guard)`);
|
|
1863
|
+
return { run, outcome: 'paused', reason: 'execution already in-flight (single-flight guard)', executed: [] };
|
|
1864
|
+
}
|
|
1865
|
+
inFlightAutoRuns.add(leaseKey);
|
|
1866
|
+
let runnerToken;
|
|
1867
|
+
try {
|
|
1868
|
+
const initial = this.loadRun(execId);
|
|
1869
|
+
if (!initial)
|
|
1870
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1871
|
+
if (initial.auto_run?.pid === process.pid) {
|
|
1872
|
+
runnerToken = initial.auto_run.token;
|
|
1107
1873
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1874
|
+
const doc = this.loadWorkflowYaml(initial.workflow_file);
|
|
1875
|
+
if (!doc) {
|
|
1876
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Failed to load workflow file: ${initial.workflow_file}`);
|
|
1110
1877
|
}
|
|
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 };
|
|
1878
|
+
const spec = doc.workflows[initial.workflow_id];
|
|
1879
|
+
if (!spec) {
|
|
1880
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Workflow "${initial.workflow_id}" not found in ${initial.workflow_file}`);
|
|
1119
1881
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1882
|
+
const executed = [];
|
|
1883
|
+
if (initial.status === 'completed' || initial.status === 'failed' || initial.status === 'cancelled') {
|
|
1884
|
+
return {
|
|
1885
|
+
run: initial,
|
|
1886
|
+
outcome: initial.status === 'completed' ? 'completed' : initial.status === 'cancelled' ? 'cancelled' : 'failed',
|
|
1887
|
+
reason: `run already ${initial.status}`,
|
|
1888
|
+
executed,
|
|
1889
|
+
};
|
|
1123
1890
|
}
|
|
1124
|
-
|
|
1891
|
+
const firstNode = Object.keys(spec.nodes ?? {})[0];
|
|
1892
|
+
const maxIterations = (typeof spec.max_iterations === 'number' && spec.max_iterations > 0)
|
|
1893
|
+
? spec.max_iterations
|
|
1894
|
+
: 30;
|
|
1895
|
+
let run = initial;
|
|
1896
|
+
while (true) {
|
|
1125
1897
|
run = this.loadRun(execId);
|
|
1126
|
-
|
|
1898
|
+
if (run.status === 'completed')
|
|
1899
|
+
return { run, outcome: 'completed', executed };
|
|
1900
|
+
if (run.status === 'failed')
|
|
1901
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1902
|
+
if (run.status === 'cancelled')
|
|
1903
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1904
|
+
const nodeId = run.current_node || firstNode;
|
|
1905
|
+
if (!nodeId) {
|
|
1906
|
+
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1907
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1908
|
+
failedNode: r.current_node || '',
|
|
1909
|
+
attempt: (0, failure_1.readNodeAttempt)(r, r.current_node || ''),
|
|
1910
|
+
causeCode: 'CONFIG_INVALID',
|
|
1911
|
+
exitCode: null,
|
|
1912
|
+
signal: null,
|
|
1913
|
+
errorOrigin: null,
|
|
1914
|
+
});
|
|
1915
|
+
assignRunFailure(r, failure, 'workflow has no nodes');
|
|
1916
|
+
appendFailureAudit(r, failure, 'config', 'auto');
|
|
1917
|
+
});
|
|
1918
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1919
|
+
}
|
|
1920
|
+
const node = spec.nodes[nodeId];
|
|
1921
|
+
if (!node) {
|
|
1922
|
+
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1923
|
+
r.current_node = nodeId;
|
|
1924
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1925
|
+
failedNode: nodeId,
|
|
1926
|
+
attempt: (0, failure_1.readNodeAttempt)(r, nodeId),
|
|
1927
|
+
causeCode: 'CONFIG_INVALID',
|
|
1928
|
+
exitCode: null,
|
|
1929
|
+
signal: null,
|
|
1930
|
+
errorOrigin: null,
|
|
1931
|
+
});
|
|
1932
|
+
assignRunFailure(r, failure, `node "${nodeId}" not found in workflow`);
|
|
1933
|
+
appendFailureAudit(r, failure, 'config', 'auto');
|
|
1934
|
+
});
|
|
1935
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1936
|
+
}
|
|
1937
|
+
const parsedRetryPolicy = (0, retry_policy_1.parseRetryPolicy)(node);
|
|
1938
|
+
if (!parsedRetryPolicy.ok) {
|
|
1939
|
+
run = await this.failAutoConfiguration(execId, nodeId, parsedRetryPolicy.reason);
|
|
1940
|
+
if (run.status === 'cancelled') {
|
|
1941
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1942
|
+
}
|
|
1943
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1944
|
+
}
|
|
1945
|
+
if (node.type === 'approval') {
|
|
1946
|
+
if (run.current_node !== nodeId) {
|
|
1947
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1948
|
+
}
|
|
1949
|
+
return { run, outcome: 'paused', reason: `approval node "${nodeId}" requires manual decision`, executed };
|
|
1950
|
+
}
|
|
1951
|
+
if (node.type === 'parallel') {
|
|
1952
|
+
if (run.current_node !== nodeId) {
|
|
1953
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1954
|
+
}
|
|
1955
|
+
return { run, outcome: 'paused', reason: `parallel auto-run not implemented (node "${nodeId}")`, executed };
|
|
1956
|
+
}
|
|
1957
|
+
if (!KNOWN_AUTO_NODE_TYPES.has(node.type)) {
|
|
1958
|
+
if (run.current_node !== nodeId) {
|
|
1959
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1960
|
+
}
|
|
1961
|
+
return { run, outcome: 'paused', reason: `unknown node type "${node.type}" (node "${nodeId}")`, executed };
|
|
1962
|
+
}
|
|
1963
|
+
const claimedToken = await this.claimAutoRunLease(execId, nodeId, runnerToken);
|
|
1964
|
+
if (!claimedToken) {
|
|
1965
|
+
run = this.loadRun(execId);
|
|
1966
|
+
return {
|
|
1967
|
+
run,
|
|
1968
|
+
outcome: run.status === 'cancelled' ? 'cancelled' : 'paused',
|
|
1969
|
+
reason: run.status === 'cancelled' ? run.error : 'run stopped before auto-run lease could be claimed',
|
|
1970
|
+
executed,
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
runnerToken = claimedToken;
|
|
1974
|
+
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1975
|
+
if (r.status === 'cancelled')
|
|
1976
|
+
return;
|
|
1977
|
+
const engine = readRunEngineState(r);
|
|
1978
|
+
engine.visitCounts[nodeId] = (engine.visitCounts[nodeId] ?? 0) + 1;
|
|
1979
|
+
engine.iterations += 1;
|
|
1980
|
+
r.state['__engine'] = engine;
|
|
1981
|
+
if (engine.visitCounts[nodeId] > maxIterations || engine.iterations > maxIterations) {
|
|
1982
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1983
|
+
failedNode: nodeId,
|
|
1984
|
+
attempt: (0, failure_1.readNodeAttempt)(r, nodeId),
|
|
1985
|
+
causeCode: 'CONFIG_INVALID',
|
|
1986
|
+
exitCode: null,
|
|
1987
|
+
signal: null,
|
|
1988
|
+
errorOrigin: null,
|
|
1989
|
+
});
|
|
1990
|
+
assignRunFailure(r, failure, `auto runner exceeded max_iterations (${maxIterations}); possible loop at "${nodeId}"`);
|
|
1991
|
+
appendFailureAudit(r, failure, 'config', 'auto');
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1994
|
+
if (run.status === 'cancelled') {
|
|
1995
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1996
|
+
}
|
|
1997
|
+
if (run.status === 'failed') {
|
|
1998
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1999
|
+
}
|
|
2000
|
+
const nodeResult = await this.runNodeWithRetry(execId, nodeId, runnerToken, parsedRetryPolicy.policy);
|
|
2001
|
+
run = nodeResult.run;
|
|
2002
|
+
runnerToken = nodeResult.runnerToken;
|
|
2003
|
+
if (nodeResult.outcome !== 'success') {
|
|
2004
|
+
return { run, outcome: nodeResult.outcome, reason: nodeResult.reason, executed };
|
|
2005
|
+
}
|
|
2006
|
+
executed.push(nodeId);
|
|
2007
|
+
if (run.status === 'completed')
|
|
2008
|
+
return { run, outcome: 'completed', executed };
|
|
2009
|
+
if (run.status === 'failed')
|
|
2010
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
2011
|
+
if (run.status === 'cancelled')
|
|
2012
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
2013
|
+
if (!SELF_ADVANCING_NODE_TYPES.has(node.type)) {
|
|
2014
|
+
const next = node.next;
|
|
2015
|
+
if (!next) {
|
|
2016
|
+
return { run, outcome: 'paused', reason: `node "${nodeId}" has no "next"; stopping`, executed };
|
|
2017
|
+
}
|
|
2018
|
+
run = this.moveNode(execId, next);
|
|
2019
|
+
}
|
|
1127
2020
|
}
|
|
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 };
|
|
2021
|
+
}
|
|
2022
|
+
finally {
|
|
2023
|
+
if (runnerToken) {
|
|
2024
|
+
try {
|
|
2025
|
+
await this.releaseAutoRunLease(execId, runnerToken);
|
|
2026
|
+
}
|
|
2027
|
+
catch {
|
|
1138
2028
|
}
|
|
1139
|
-
run = this.moveNode(execId, next);
|
|
1140
2029
|
}
|
|
2030
|
+
inFlightAutoRuns.delete(leaseKey);
|
|
1141
2031
|
}
|
|
1142
2032
|
}
|
|
1143
2033
|
cancelRun(execId, reason = 'Cancelled by user') {
|
|
1144
|
-
|
|
2034
|
+
const cancelled = this.atomicUpdate(execId, (run) => {
|
|
1145
2035
|
if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {
|
|
1146
2036
|
return;
|
|
1147
2037
|
}
|
|
1148
2038
|
const now = new Date();
|
|
1149
|
-
run.
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
started_at: now.toISOString(),
|
|
1158
|
-
ended_at: now.toISOString(),
|
|
1159
|
-
duration_ms: 0,
|
|
1160
|
-
exit_code: null,
|
|
1161
|
-
error: reason,
|
|
1162
|
-
trigger: 'manual',
|
|
2039
|
+
const activeNodeId = run.auto_run?.node_id || run.current_node || '';
|
|
2040
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
2041
|
+
failedNode: activeNodeId,
|
|
2042
|
+
attempt: (0, failure_1.readNodeAttempt)(run, activeNodeId),
|
|
2043
|
+
causeCode: 'CANCELLED',
|
|
2044
|
+
exitCode: null,
|
|
2045
|
+
signal: null,
|
|
2046
|
+
errorOrigin: null,
|
|
1163
2047
|
});
|
|
2048
|
+
assignRunFailure(run, failure, reason);
|
|
2049
|
+
run.status = 'cancelled';
|
|
2050
|
+
delete run.auto_run;
|
|
2051
|
+
delete run.manual_resume;
|
|
2052
|
+
appendFailureAudit(run, failure, 'cancel', 'manual', now.toISOString());
|
|
1164
2053
|
});
|
|
2054
|
+
if (cancelled.status === 'cancelled') {
|
|
2055
|
+
requestActiveProcessCancellation(this.resolvedDir, execId);
|
|
2056
|
+
}
|
|
2057
|
+
return cancelled;
|
|
1165
2058
|
}
|
|
1166
2059
|
reset(execId) {
|
|
1167
2060
|
return this.atomicUpdate(execId, (run) => {
|
|
1168
2061
|
run.state = JSON.parse(JSON.stringify(run.initial_state));
|
|
1169
2062
|
run.current_node = '';
|
|
1170
2063
|
run.completed_nodes = [];
|
|
2064
|
+
run.status = 'pending';
|
|
2065
|
+
delete run.error;
|
|
2066
|
+
delete run.last_failure;
|
|
2067
|
+
delete run.node_attempts;
|
|
2068
|
+
delete run.auto_run;
|
|
2069
|
+
delete run.manual_resume;
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
retry(execId, options = {}) {
|
|
2073
|
+
const snapshot = this.loadRun(execId);
|
|
2074
|
+
if (!snapshot)
|
|
2075
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
2076
|
+
const doc = this.loadWorkflowYaml(snapshot.workflow_file);
|
|
2077
|
+
if (!doc)
|
|
2078
|
+
throw new Error(`Failed to load workflow file: ${snapshot.workflow_file}`);
|
|
2079
|
+
const spec = doc.workflows[snapshot.workflow_id];
|
|
2080
|
+
if (!spec)
|
|
2081
|
+
throw new Error(`Workflow "${snapshot.workflow_id}" not found`);
|
|
2082
|
+
if (snapshot.status !== 'failed') {
|
|
2083
|
+
throw new Error(`Run "${execId}" is not failed (status=${snapshot.status ?? 'running'}); only failed runs can be retried`);
|
|
2084
|
+
}
|
|
2085
|
+
if (!isRetryableFailure(snapshot, spec)) {
|
|
2086
|
+
throw new Error(`Run "${execId}" failed at a non-retryable node "${snapshot.current_node}" (error node); use reset to restart`);
|
|
2087
|
+
}
|
|
2088
|
+
return this.atomicUpdate(execId, (run) => {
|
|
2089
|
+
if (run.status !== 'failed') {
|
|
2090
|
+
throw new Error(`Run "${execId}" is not failed (status=${run.status ?? 'pending'}); only failed runs can be retried`);
|
|
2091
|
+
}
|
|
2092
|
+
const failedNode = run.current_node;
|
|
2093
|
+
if (failedNode && run.last_failure) {
|
|
2094
|
+
const lastAttempt = run.last_failure.attempt;
|
|
2095
|
+
const storedAttempt = run.node_attempts?.[failedNode];
|
|
2096
|
+
if (typeof lastAttempt === 'number' && Number.isInteger(lastAttempt) && lastAttempt > 0
|
|
2097
|
+
&& (typeof storedAttempt !== 'number' || storedAttempt < lastAttempt)) {
|
|
2098
|
+
run.node_attempts = run.node_attempts ?? {};
|
|
2099
|
+
run.node_attempts[failedNode] = lastAttempt;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
run.status = 'retrying';
|
|
2103
|
+
delete run.error;
|
|
2104
|
+
delete run.manual_resume;
|
|
2105
|
+
run.retry_count = (run.retry_count ?? 0) + 1;
|
|
2106
|
+
run.audit = run.audit ?? [];
|
|
2107
|
+
const now = new Date().toISOString();
|
|
2108
|
+
if (options.autoRun === false) {
|
|
2109
|
+
delete run.auto_run;
|
|
2110
|
+
run.manual_resume = { node_id: failedNode, requested_at: now };
|
|
2111
|
+
}
|
|
2112
|
+
else {
|
|
2113
|
+
run.auto_run = {
|
|
2114
|
+
pid: process.pid,
|
|
2115
|
+
token: (0, sdk_1.generateId)('wfr'),
|
|
2116
|
+
node_id: failedNode,
|
|
2117
|
+
started_at: now,
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
run.audit.push({
|
|
2121
|
+
exec_id: run.id,
|
|
2122
|
+
node_id: failedNode,
|
|
2123
|
+
type: 'retry',
|
|
2124
|
+
summary: `manual retry #${run.retry_count}`,
|
|
2125
|
+
started_at: now,
|
|
2126
|
+
ended_at: now,
|
|
2127
|
+
duration_ms: 0,
|
|
2128
|
+
exit_code: null,
|
|
2129
|
+
trigger: 'manual',
|
|
2130
|
+
});
|
|
1171
2131
|
});
|
|
1172
2132
|
}
|
|
1173
2133
|
listRuns(opts) {
|
|
@@ -1176,7 +2136,10 @@ class RunManager {
|
|
|
1176
2136
|
const runs = [];
|
|
1177
2137
|
for (const file of files) {
|
|
1178
2138
|
try {
|
|
1179
|
-
const
|
|
2139
|
+
const execId = file.slice(0, -'.json'.length);
|
|
2140
|
+
const run = this.loadRun(execId);
|
|
2141
|
+
if (!run)
|
|
2142
|
+
continue;
|
|
1180
2143
|
if (opts?.workflowId && run.workflow_id !== opts.workflowId)
|
|
1181
2144
|
continue;
|
|
1182
2145
|
runs.push(run);
|