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