@crewx/workflow 0.3.22-rc.19 → 0.3.22-rc.191
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 +1195 -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,30 @@ 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
|
+
let failWhenReason;
|
|
1174
|
+
const recordProcessResult = (result) => {
|
|
1175
|
+
nodeExitCode = result.status;
|
|
1176
|
+
nodeSignal = result.signal;
|
|
1177
|
+
nodeErrorOrigin = result.errorOrigin;
|
|
1178
|
+
processFailure = toProcessFailureMetadata(result);
|
|
1179
|
+
};
|
|
1180
|
+
const reserveAgentTaskId = async () => {
|
|
1181
|
+
const taskId = (0, sdk_1.generateId)('tsk');
|
|
1182
|
+
await this.atomicUpdateAsync(execId, (run) => {
|
|
1183
|
+
run.tasks = run.tasks ?? {};
|
|
1184
|
+
run.tasks[nodeId] = taskId;
|
|
1185
|
+
});
|
|
1186
|
+
nodeTaskId = taskId;
|
|
1187
|
+
return taskId;
|
|
1188
|
+
};
|
|
694
1189
|
const buildAudit = (error) => {
|
|
695
1190
|
const endedAt = new Date();
|
|
696
1191
|
return {
|
|
@@ -706,31 +1201,57 @@ class RunManager {
|
|
|
706
1201
|
trigger,
|
|
707
1202
|
};
|
|
708
1203
|
};
|
|
1204
|
+
const attempt = await this.reserveNodeAttempt(execId, nodeId);
|
|
709
1205
|
try {
|
|
710
1206
|
switch (nodeSpec.type) {
|
|
711
1207
|
case 'agent_task': {
|
|
712
1208
|
if (!nodeSpec.agent) {
|
|
713
|
-
throw
|
|
1209
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" is agent_task but has no agent defined`);
|
|
1210
|
+
}
|
|
1211
|
+
const agentCtx = { ...snapshot.state, state: snapshot.state };
|
|
1212
|
+
const resolvedAgent = interpolateTemplate(nodeSpec.agent, agentCtx).trim();
|
|
1213
|
+
if (!resolvedAgent) {
|
|
1214
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": assignee not resolved (agent field "${nodeSpec.agent}" interpolated to empty)`);
|
|
1215
|
+
}
|
|
1216
|
+
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, agentCtx) : undefined, this.projectRoot);
|
|
1217
|
+
if (!cwdCheck.ok) {
|
|
1218
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cwdCheck.error}`);
|
|
714
1219
|
}
|
|
1220
|
+
const nodeCwd = cwdCheck.resolved;
|
|
1221
|
+
const rootCheck = (0, shell_security_1.normalizeCwd)(undefined, this.projectRoot);
|
|
1222
|
+
if (!rootCheck.ok) {
|
|
1223
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${rootCheck.error}`);
|
|
1224
|
+
}
|
|
1225
|
+
const workspaceRoot = rootCheck.resolved;
|
|
715
1226
|
const input = nodeSpec.input
|
|
716
|
-
? interpolateTemplate(nodeSpec.input,
|
|
1227
|
+
? interpolateTemplate(nodeSpec.input, agentCtx)
|
|
717
1228
|
: '';
|
|
1229
|
+
const preamble = process.env.CREWX_WORKFLOW_PREAMBLE === 'off'
|
|
1230
|
+
? ''
|
|
1231
|
+
: (0, output_format_1.buildWorkflowPreamble)({
|
|
1232
|
+
workflowId: snapshot.workflow_id,
|
|
1233
|
+
nodeId,
|
|
1234
|
+
execId: snapshot.id,
|
|
1235
|
+
});
|
|
718
1236
|
const finalInput = nodeSpec.output_format === 'json'
|
|
719
|
-
? input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
720
|
-
: input;
|
|
1237
|
+
? preamble + input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
1238
|
+
: preamble + input;
|
|
721
1239
|
const mode = nodeSpec.mode === 'query' ? 'q' : 'x';
|
|
722
|
-
const prompt = `@${
|
|
1240
|
+
const prompt = `@${resolvedAgent} ${finalInput}`;
|
|
723
1241
|
if (opts?.dryRun) {
|
|
724
|
-
console.log(`[dry-run] Would execute: crewx ${mode} "@${
|
|
1242
|
+
console.log(`[dry-run] Would execute: crewx ${mode} "@${resolvedAgent} ..."`);
|
|
725
1243
|
break;
|
|
726
1244
|
}
|
|
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);
|
|
1245
|
+
console.log(`Executing: @${resolvedAgent} (${mode} mode)`);
|
|
732
1246
|
const nodeTimeout = Number(process.env[mode === 'q' ? 'CREWX_TIMEOUT_QUERY' : 'CREWX_TIMEOUT_EXECUTE']) || 8 * 3600000;
|
|
733
|
-
const
|
|
1247
|
+
const childInvocation = resolveCrewxChildInvocation({
|
|
1248
|
+
...process.env,
|
|
1249
|
+
CREWX_WORKSPACE: workspaceRoot,
|
|
1250
|
+
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
1251
|
+
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
1252
|
+
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
1253
|
+
}, workspaceRoot);
|
|
1254
|
+
const wfArgs = [...childInvocation.args, mode, prompt];
|
|
734
1255
|
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
735
1256
|
wfArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
736
1257
|
}
|
|
@@ -743,39 +1264,47 @@ class RunManager {
|
|
|
743
1264
|
};
|
|
744
1265
|
wfArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
745
1266
|
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
746
|
-
command:
|
|
1267
|
+
command: childInvocation.command,
|
|
1268
|
+
env: childInvocation.env,
|
|
1269
|
+
cwd: nodeCwd,
|
|
747
1270
|
allowShellFallback: true,
|
|
748
1271
|
packageName: 'crewx',
|
|
749
1272
|
});
|
|
750
1273
|
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, wfArgs);
|
|
1274
|
+
const taskId = await reserveAgentTaskId();
|
|
751
1275
|
const result = await runProcess(invocation.command, invocation.argv, {
|
|
752
|
-
cwd:
|
|
753
|
-
env: {
|
|
754
|
-
...
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
758
|
-
},
|
|
1276
|
+
cwd: nodeCwd,
|
|
1277
|
+
env: (0, sdk_1.withoutCrewxRootContext)({
|
|
1278
|
+
...childInvocation.env,
|
|
1279
|
+
CREWX_TRACE_ID: taskId,
|
|
1280
|
+
}),
|
|
759
1281
|
timeout: nodeTimeout,
|
|
760
1282
|
shell: invocation.shell ?? false,
|
|
761
1283
|
windowsHide: invocation.windowsHide,
|
|
1284
|
+
cancellation: this.processCancellation(execId),
|
|
762
1285
|
});
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
if (result.error) {
|
|
767
|
-
throw result.error;
|
|
1286
|
+
recordProcessResult(result);
|
|
1287
|
+
if (result.cancelled) {
|
|
1288
|
+
throw (0, failure_1.createWorkflowFailureError)('CANCELLED', `Agent execution cancelled for node "${nodeId}"`);
|
|
768
1289
|
}
|
|
769
1290
|
if (result.timedOut) {
|
|
770
1291
|
throw new Error(`Agent execution timed out after ${nodeTimeout}ms`);
|
|
771
1292
|
}
|
|
1293
|
+
if (result.error) {
|
|
1294
|
+
throw result.error;
|
|
1295
|
+
}
|
|
772
1296
|
const taskIdMatch = result.stderr.match(/crewx kill (tsk_\w+)/);
|
|
773
|
-
|
|
1297
|
+
if (taskIdMatch?.[1])
|
|
1298
|
+
nodeTaskId = taskIdMatch[1];
|
|
774
1299
|
if (result.status !== 0) {
|
|
775
1300
|
const stderr = result.stderr.trim();
|
|
776
1301
|
throw new Error(`Agent execution failed (exit ${result.status}): ${stderr}`);
|
|
777
1302
|
}
|
|
778
1303
|
const output = result.stdout.trim();
|
|
1304
|
+
const signal = (0, output_format_1.detectWorkflowSignal)(output);
|
|
1305
|
+
if (signal) {
|
|
1306
|
+
throw new Error(`Node "${nodeId}" agent signaled failure: ${signal.reason ?? '(no reason given)'}`);
|
|
1307
|
+
}
|
|
779
1308
|
if (nodeSpec.output) {
|
|
780
1309
|
if (nodeSpec.output_format === 'json') {
|
|
781
1310
|
const retryMax = nodeSpec.output_retry ?? 1;
|
|
@@ -788,36 +1317,41 @@ class RunManager {
|
|
|
788
1317
|
retryInput = finalInput +
|
|
789
1318
|
`\n\n## ⚠️ PREVIOUS OUTPUT FAILED JSON.parse: ${lastError.message}\n` +
|
|
790
1319
|
`Return STRICT JSON ONLY. First char "{", last char "}". No prose, no fences.`;
|
|
791
|
-
const retryPrompt = `@${
|
|
792
|
-
const retryArgs = [...
|
|
1320
|
+
const retryPrompt = `@${resolvedAgent} ${retryInput}`;
|
|
1321
|
+
const retryArgs = [...childInvocation.args, mode, retryPrompt];
|
|
793
1322
|
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
794
1323
|
retryArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
795
1324
|
}
|
|
796
1325
|
retryArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
797
1326
|
const retryInvocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, retryArgs);
|
|
1327
|
+
const retryTaskId = await reserveAgentTaskId();
|
|
798
1328
|
const retryResult = await runProcess(retryInvocation.command, retryInvocation.argv, {
|
|
799
|
-
cwd:
|
|
800
|
-
env: {
|
|
801
|
-
...
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
805
|
-
},
|
|
1329
|
+
cwd: nodeCwd,
|
|
1330
|
+
env: (0, sdk_1.withoutCrewxRootContext)({
|
|
1331
|
+
...childInvocation.env,
|
|
1332
|
+
CREWX_TRACE_ID: retryTaskId,
|
|
1333
|
+
}),
|
|
806
1334
|
timeout: nodeTimeout,
|
|
807
1335
|
shell: retryInvocation.shell ?? false,
|
|
808
1336
|
windowsHide: retryInvocation.windowsHide,
|
|
1337
|
+
cancellation: this.processCancellation(execId),
|
|
809
1338
|
});
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
throw retryResult.error;
|
|
1339
|
+
recordProcessResult(retryResult);
|
|
1340
|
+
if (retryResult.cancelled) {
|
|
1341
|
+
throw (0, failure_1.createWorkflowFailureError)('CANCELLED', `Agent retry cancelled for node "${nodeId}"`);
|
|
1342
|
+
}
|
|
815
1343
|
if (retryResult.timedOut)
|
|
816
1344
|
throw new Error(`Agent retry timed out after ${nodeTimeout}ms`);
|
|
1345
|
+
if (retryResult.error)
|
|
1346
|
+
throw retryResult.error;
|
|
817
1347
|
if (retryResult.status !== 0) {
|
|
818
1348
|
throw new Error(`Agent retry failed (exit ${retryResult.status}): ${retryResult.stderr.trim()}`);
|
|
819
1349
|
}
|
|
820
1350
|
attemptOutput = retryResult.stdout.trim();
|
|
1351
|
+
const retrySignal = (0, output_format_1.detectWorkflowSignal)(attemptOutput);
|
|
1352
|
+
if (retrySignal) {
|
|
1353
|
+
throw new Error(`Node "${nodeId}" agent signaled failure: ${retrySignal.reason ?? '(no reason given)'}`);
|
|
1354
|
+
}
|
|
821
1355
|
}
|
|
822
1356
|
try {
|
|
823
1357
|
parsed = JSON.parse((0, output_format_1.extractJson)(attemptOutput));
|
|
@@ -831,7 +1365,7 @@ class RunManager {
|
|
|
831
1365
|
}
|
|
832
1366
|
}
|
|
833
1367
|
if (parsed === null) {
|
|
834
|
-
throw
|
|
1368
|
+
throw (0, failure_1.createWorkflowFailureError)('OUTPUT_INVALID', `Node "${nodeId}" output_format=json parse failed after ${retryMax + 1} attempt(s): ${lastError.message}`);
|
|
835
1369
|
}
|
|
836
1370
|
stateUpdates[nodeSpec.output] = parsed;
|
|
837
1371
|
console.log(`Result stored in state.${nodeSpec.output} (JSON object)`);
|
|
@@ -845,52 +1379,64 @@ class RunManager {
|
|
|
845
1379
|
}
|
|
846
1380
|
case 'skill_task': {
|
|
847
1381
|
if (!nodeSpec.skill) {
|
|
848
|
-
throw
|
|
1382
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" is skill_task but has no skill defined`);
|
|
849
1383
|
}
|
|
850
1384
|
if (!shell_security_1.SKILL_NAME_RE.test(nodeSpec.skill)) {
|
|
851
|
-
throw
|
|
1385
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" has invalid skill name "${nodeSpec.skill}" (must match ^[a-z0-9][a-z0-9-]*$)`);
|
|
852
1386
|
}
|
|
853
1387
|
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
854
1388
|
const skillArgs = (nodeSpec.args ?? []).map((a) => interpolateTemplate(String(a), ctx));
|
|
1389
|
+
const skillStdin = nodeSpec.stdin !== undefined
|
|
1390
|
+
? interpolateTemplate(String(nodeSpec.stdin), ctx)
|
|
1391
|
+
: undefined;
|
|
855
1392
|
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
856
|
-
if (!cwdCheck.ok)
|
|
857
|
-
throw
|
|
1393
|
+
if (!cwdCheck.ok) {
|
|
1394
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cwdCheck.error}`);
|
|
1395
|
+
}
|
|
1396
|
+
const skillRootCheck = (0, shell_security_1.normalizeCwd)(undefined, this.projectRoot);
|
|
1397
|
+
if (!skillRootCheck.ok) {
|
|
1398
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${skillRootCheck.error}`);
|
|
1399
|
+
}
|
|
1400
|
+
const skillWorkspaceRoot = skillRootCheck.resolved;
|
|
858
1401
|
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
859
1402
|
if (opts?.dryRun) {
|
|
860
1403
|
console.log(`[dry-run] Would execute: crewx skill ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
861
1404
|
break;
|
|
862
1405
|
}
|
|
863
1406
|
console.log(`Executing skill: ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1407
|
+
const childInvocation = resolveCrewxChildInvocation({
|
|
1408
|
+
...process.env,
|
|
1409
|
+
CREWX_WORKSPACE: skillWorkspaceRoot,
|
|
1410
|
+
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
1411
|
+
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
1412
|
+
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
1413
|
+
}, skillWorkspaceRoot);
|
|
1414
|
+
const skillArgv = [...childInvocation.args, 'skill', nodeSpec.skill, ...skillArgs];
|
|
869
1415
|
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
870
|
-
command:
|
|
1416
|
+
command: childInvocation.command,
|
|
1417
|
+
env: childInvocation.env,
|
|
1418
|
+
cwd: cwdCheck.resolved,
|
|
871
1419
|
allowShellFallback: true,
|
|
872
1420
|
packageName: 'crewx',
|
|
873
1421
|
});
|
|
874
1422
|
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, skillArgv);
|
|
875
1423
|
const result = await runProcess(invocation.command, invocation.argv, {
|
|
876
1424
|
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
|
-
},
|
|
1425
|
+
env: childInvocation.env,
|
|
883
1426
|
timeout,
|
|
884
1427
|
shell: invocation.shell ?? false,
|
|
885
1428
|
windowsHide: invocation.windowsHide,
|
|
1429
|
+
stdin: skillStdin,
|
|
1430
|
+
cancellation: this.processCancellation(execId),
|
|
886
1431
|
});
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
throw result.error;
|
|
1432
|
+
recordProcessResult(result);
|
|
1433
|
+
if (result.cancelled) {
|
|
1434
|
+
throw (0, failure_1.createWorkflowFailureError)('CANCELLED', `skill_task "${nodeId}" was cancelled`);
|
|
1435
|
+
}
|
|
892
1436
|
if (result.timedOut)
|
|
893
1437
|
throw new Error(`skill_task "${nodeId}" timed out after ${timeout}ms`);
|
|
1438
|
+
if (result.error)
|
|
1439
|
+
throw result.error;
|
|
894
1440
|
if (result.status !== 0) {
|
|
895
1441
|
throw new Error(`skill_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
896
1442
|
}
|
|
@@ -904,25 +1450,29 @@ class RunManager {
|
|
|
904
1450
|
const shellEnvEnabled = process.env.CREWX_WORKFLOW_SHELL === '1';
|
|
905
1451
|
const shellMetaAllowed = spec.metadata?.shell_task_allowed === true;
|
|
906
1452
|
if (!shellEnvEnabled || !shellMetaAllowed) {
|
|
907
|
-
throw
|
|
1453
|
+
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
1454
|
}
|
|
909
1455
|
const cmdCheck = (0, shell_security_1.validateShellCommand)(nodeSpec.command);
|
|
910
|
-
if (!cmdCheck.ok)
|
|
911
|
-
throw
|
|
1456
|
+
if (!cmdCheck.ok) {
|
|
1457
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cmdCheck.error}`);
|
|
1458
|
+
}
|
|
912
1459
|
const envCheck = (0, shell_security_1.validateEnvKeys)(nodeSpec.env);
|
|
913
|
-
if (!envCheck.ok)
|
|
914
|
-
throw
|
|
1460
|
+
if (!envCheck.ok) {
|
|
1461
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${envCheck.error}`);
|
|
1462
|
+
}
|
|
915
1463
|
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
916
1464
|
const command = nodeSpec.command.map((c) => interpolateTemplate(c, ctx));
|
|
917
1465
|
const postCheck = (0, shell_security_1.validateShellCommand)(command);
|
|
918
|
-
if (!postCheck.ok)
|
|
919
|
-
throw
|
|
1466
|
+
if (!postCheck.ok) {
|
|
1467
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${postCheck.error}`);
|
|
1468
|
+
}
|
|
920
1469
|
const interpolatedEnv = nodeSpec.env
|
|
921
1470
|
? Object.fromEntries(Object.entries(nodeSpec.env).map(([k, v]) => [k, interpolateTemplate(String(v), ctx)]))
|
|
922
1471
|
: undefined;
|
|
923
1472
|
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
924
|
-
if (!cwdCheck.ok)
|
|
925
|
-
throw
|
|
1473
|
+
if (!cwdCheck.ok) {
|
|
1474
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cwdCheck.error}`);
|
|
1475
|
+
}
|
|
926
1476
|
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
927
1477
|
if (opts?.dryRun) {
|
|
928
1478
|
console.log(`[dry-run] Would execute: ${command.join(' ')}`);
|
|
@@ -933,19 +1483,25 @@ class RunManager {
|
|
|
933
1483
|
childEnv.CREWX_WORKFLOW_EXEC_ID = snapshot.id;
|
|
934
1484
|
childEnv.CREWX_WORKFLOW_NODE_ID = nodeId;
|
|
935
1485
|
childEnv.CREWX_WORKFLOW_ID = snapshot.workflow_id;
|
|
1486
|
+
const shellStdin = nodeSpec.stdin !== undefined
|
|
1487
|
+
? interpolateTemplate(String(nodeSpec.stdin), ctx)
|
|
1488
|
+
: undefined;
|
|
936
1489
|
const result = await runProcess(command[0], command.slice(1), {
|
|
937
1490
|
cwd: cwdCheck.resolved,
|
|
938
1491
|
env: childEnv,
|
|
939
1492
|
timeout,
|
|
940
1493
|
shell: false,
|
|
1494
|
+
stdin: shellStdin,
|
|
1495
|
+
cancellation: this.processCancellation(execId),
|
|
941
1496
|
});
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
throw result.error;
|
|
1497
|
+
recordProcessResult(result);
|
|
1498
|
+
if (result.cancelled) {
|
|
1499
|
+
throw (0, failure_1.createWorkflowFailureError)('CANCELLED', `shell_task "${nodeId}" was cancelled`);
|
|
1500
|
+
}
|
|
947
1501
|
if (result.timedOut)
|
|
948
1502
|
throw new Error(`shell_task "${nodeId}" timed out after ${timeout}ms`);
|
|
1503
|
+
if (result.error)
|
|
1504
|
+
throw result.error;
|
|
949
1505
|
if (result.status !== 0) {
|
|
950
1506
|
throw new Error(`shell_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
951
1507
|
}
|
|
@@ -957,13 +1513,13 @@ class RunManager {
|
|
|
957
1513
|
}
|
|
958
1514
|
case 'branch': {
|
|
959
1515
|
if (!nodeSpec.condition) {
|
|
960
|
-
throw
|
|
1516
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Branch node "${nodeId}" has no condition`);
|
|
961
1517
|
}
|
|
962
1518
|
const condResult = String(safeEvaluateBranchCondition(String(nodeSpec.condition), snapshot.state));
|
|
963
1519
|
const branches = nodeSpec.branches;
|
|
964
1520
|
const targetNode = branches?.[condResult] ?? nodeSpec.default;
|
|
965
1521
|
if (!targetNode) {
|
|
966
|
-
throw
|
|
1522
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Branch "${nodeId}": no matching branch for "${condResult}" and no default`);
|
|
967
1523
|
}
|
|
968
1524
|
console.log(`Branch "${nodeId}": condition="${nodeSpec.condition}" → ${condResult} → ${targetNode}`);
|
|
969
1525
|
nodesToComplete.push(nodeId);
|
|
@@ -989,6 +1545,7 @@ class RunManager {
|
|
|
989
1545
|
}
|
|
990
1546
|
case 'error': {
|
|
991
1547
|
status = 'failed';
|
|
1548
|
+
failureCode = 'UNKNOWN';
|
|
992
1549
|
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
993
1550
|
failureMessage = nodeSpec.message
|
|
994
1551
|
? interpolateTemplate(nodeSpec.message, ctx)
|
|
@@ -998,7 +1555,7 @@ class RunManager {
|
|
|
998
1555
|
}
|
|
999
1556
|
case 'expression': {
|
|
1000
1557
|
if (!nodeSpec.set || typeof nodeSpec.set !== 'object') {
|
|
1001
|
-
throw
|
|
1558
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Expression node "${nodeId}" requires "set" field`);
|
|
1002
1559
|
}
|
|
1003
1560
|
for (const [key, expr] of Object.entries(nodeSpec.set)) {
|
|
1004
1561
|
const result = safeEvaluate(String(expr), snapshot.state);
|
|
@@ -1019,30 +1576,73 @@ class RunManager {
|
|
|
1019
1576
|
default:
|
|
1020
1577
|
console.log(`Unknown node type "${nodeSpec.type}" for node "${nodeId}"`);
|
|
1021
1578
|
}
|
|
1579
|
+
if (nodeSpec.fail_when &&
|
|
1580
|
+
!opts?.dryRun &&
|
|
1581
|
+
(nodeSpec.type === 'agent_task' || nodeSpec.type === 'skill_task' || nodeSpec.type === 'shell_task')) {
|
|
1582
|
+
const judged = safeEvaluateBranchCondition(String(nodeSpec.fail_when), { ...snapshot.state, ...stateUpdates });
|
|
1583
|
+
if (judged === true || judged === 'true') {
|
|
1584
|
+
const output = nodeSpec.output ? stateUpdates[nodeSpec.output] : undefined;
|
|
1585
|
+
if (output && typeof output === 'object' && !Array.isArray(output)) {
|
|
1586
|
+
const reason = output.reason;
|
|
1587
|
+
if (typeof reason === 'string') {
|
|
1588
|
+
const redactedReason = (0, sdk_1.redactTechnicalMessage)(reason);
|
|
1589
|
+
const firstLine = redactedReason
|
|
1590
|
+
?.split(/\r\n|\r|\n/u)
|
|
1591
|
+
.find((line) => line.trim());
|
|
1592
|
+
if (firstLine)
|
|
1593
|
+
failWhenReason = firstLine.trim().slice(0, 180);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
throw new Error(`Node "${nodeId}" judged failed by fail_when: ${nodeSpec.fail_when}`);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1022
1599
|
}
|
|
1023
1600
|
catch (e) {
|
|
1024
|
-
const errMessage = e
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
const
|
|
1601
|
+
const errMessage = getErrorMessage(e);
|
|
1602
|
+
const causeCode = (0, failure_1.classifyWorkflowFailure)({
|
|
1603
|
+
errorCode: (0, failure_1.getWorkflowFailureCode)(e),
|
|
1604
|
+
process: processFailure,
|
|
1605
|
+
});
|
|
1606
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1607
|
+
failedNode: nodeId,
|
|
1608
|
+
attempt,
|
|
1609
|
+
causeCode,
|
|
1610
|
+
exitCode: causeCode === 'NODE_EXIT_NONZERO' && typeof nodeExitCode === 'number'
|
|
1611
|
+
? nodeExitCode
|
|
1612
|
+
: null,
|
|
1613
|
+
signal: causeCode === 'INTERRUPTED' ? nodeSignal : null,
|
|
1614
|
+
errorOrigin: nodeErrorOrigin,
|
|
1615
|
+
timedOut: processFailure?.timedOut ?? false,
|
|
1616
|
+
});
|
|
1617
|
+
const auditEntry = buildAudit(failWhenReason ?? failure.causeMessage);
|
|
1030
1618
|
await this.atomicUpdateAsync(execId, (run) => {
|
|
1031
1619
|
if (run.status === 'cancelled')
|
|
1032
1620
|
return;
|
|
1621
|
+
Object.assign(run.state, stateUpdates);
|
|
1622
|
+
delete run.manual_resume;
|
|
1033
1623
|
run.current_node = nodeId;
|
|
1034
|
-
run
|
|
1035
|
-
run.error = errMessage;
|
|
1624
|
+
assignRunFailure(run, failure, failWhenReason ?? errMessage);
|
|
1036
1625
|
run.audit = run.audit ?? [];
|
|
1037
1626
|
run.audit.push(auditEntry);
|
|
1038
1627
|
});
|
|
1039
1628
|
throw e;
|
|
1040
1629
|
}
|
|
1041
|
-
const
|
|
1630
|
+
const successFailure = failureCode
|
|
1631
|
+
? (0, failure_1.buildNodeFailure)({
|
|
1632
|
+
failedNode: nodeId,
|
|
1633
|
+
attempt,
|
|
1634
|
+
causeCode: failureCode,
|
|
1635
|
+
exitCode: null,
|
|
1636
|
+
signal: null,
|
|
1637
|
+
errorOrigin: null,
|
|
1638
|
+
})
|
|
1639
|
+
: null;
|
|
1640
|
+
const successAudit = buildAudit(successFailure?.causeMessage);
|
|
1042
1641
|
return this.atomicUpdateAsync(execId, (run) => {
|
|
1043
1642
|
if (run.status === 'cancelled')
|
|
1044
1643
|
return;
|
|
1045
1644
|
Object.assign(run.state, stateUpdates);
|
|
1645
|
+
delete run.manual_resume;
|
|
1046
1646
|
for (const node of nodesToComplete) {
|
|
1047
1647
|
if (!run.completed_nodes.includes(node)) {
|
|
1048
1648
|
run.completed_nodes.push(node);
|
|
@@ -1052,10 +1652,20 @@ class RunManager {
|
|
|
1052
1652
|
run.completed_nodes.push(prevNode);
|
|
1053
1653
|
}
|
|
1054
1654
|
run.current_node = currentNode;
|
|
1055
|
-
if (
|
|
1056
|
-
run
|
|
1057
|
-
|
|
1058
|
-
|
|
1655
|
+
if (successFailure) {
|
|
1656
|
+
assignRunFailure(run, successFailure, failureMessage ?? successFailure.causeMessage);
|
|
1657
|
+
}
|
|
1658
|
+
else {
|
|
1659
|
+
delete run.error;
|
|
1660
|
+
delete run.last_failure;
|
|
1661
|
+
this.clearNodeAttempt(run, nodeId);
|
|
1662
|
+
if (status) {
|
|
1663
|
+
run.status = status;
|
|
1664
|
+
}
|
|
1665
|
+
else if (run.status !== 'completed') {
|
|
1666
|
+
run.status = 'running';
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1059
1669
|
if (nodeTaskId) {
|
|
1060
1670
|
run.tasks = run.tasks ?? {};
|
|
1061
1671
|
run.tasks[nodeId] = nodeTaskId;
|
|
@@ -1064,123 +1674,471 @@ class RunManager {
|
|
|
1064
1674
|
run.audit.push(successAudit);
|
|
1065
1675
|
});
|
|
1066
1676
|
}
|
|
1067
|
-
async
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1677
|
+
async claimAutoRunLease(execId, nodeId, preferredToken) {
|
|
1678
|
+
let claimed = false;
|
|
1679
|
+
const updated = await this.atomicUpdateAsync(execId, (run) => {
|
|
1680
|
+
if (isTerminalStatus(run.status))
|
|
1681
|
+
return;
|
|
1682
|
+
const existing = run.auto_run;
|
|
1683
|
+
const ownedByThisProcess = existing?.pid === process.pid && typeof existing.token === 'string';
|
|
1684
|
+
if (existing && !ownedByThisProcess && isProcessAlive(existing.pid)) {
|
|
1685
|
+
throw new Error(`Workflow run "${execId}" is already owned by another runner process`);
|
|
1686
|
+
}
|
|
1687
|
+
const token = preferredToken ?? (ownedByThisProcess ? existing.token : (0, sdk_1.generateId)('wfr'));
|
|
1688
|
+
if (!isTerminalStatus(run.status)) {
|
|
1689
|
+
run.status = 'running';
|
|
1690
|
+
}
|
|
1691
|
+
if (!run.current_node)
|
|
1692
|
+
run.current_node = nodeId;
|
|
1693
|
+
run.auto_run = {
|
|
1694
|
+
pid: process.pid,
|
|
1695
|
+
token,
|
|
1696
|
+
node_id: nodeId,
|
|
1697
|
+
started_at: ownedByThisProcess && existing.token === token
|
|
1698
|
+
? existing.started_at
|
|
1699
|
+
: new Date().toISOString(),
|
|
1085
1700
|
};
|
|
1701
|
+
delete run.manual_resume;
|
|
1702
|
+
claimed = true;
|
|
1703
|
+
});
|
|
1704
|
+
return claimed ? updated.auto_run?.token ?? null : null;
|
|
1705
|
+
}
|
|
1706
|
+
async releaseAutoRunLease(execId, token) {
|
|
1707
|
+
await this.atomicUpdateAsync(execId, (run) => {
|
|
1708
|
+
if (run.auto_run?.token === token)
|
|
1709
|
+
delete run.auto_run;
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
async failAutoConfiguration(execId, nodeId, reason) {
|
|
1713
|
+
return this.atomicUpdateAsync(execId, (run) => {
|
|
1714
|
+
if (run.status === 'cancelled')
|
|
1715
|
+
return;
|
|
1716
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1717
|
+
failedNode: nodeId,
|
|
1718
|
+
attempt: (0, failure_1.readNodeAttempt)(run, nodeId),
|
|
1719
|
+
causeCode: 'CONFIG_INVALID',
|
|
1720
|
+
exitCode: null,
|
|
1721
|
+
signal: null,
|
|
1722
|
+
errorOrigin: null,
|
|
1723
|
+
});
|
|
1724
|
+
assignRunFailure(run, failure, reason);
|
|
1725
|
+
appendFailureAudit(run, failure, 'config', 'auto');
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
async reserveAutoRetry(execId, nodeId, runnerToken, policy, delayMs) {
|
|
1729
|
+
let reserved = false;
|
|
1730
|
+
const run = await this.atomicUpdateAsync(execId, (draft) => {
|
|
1731
|
+
if (draft.status === 'cancelled')
|
|
1732
|
+
return;
|
|
1733
|
+
if (draft.status !== 'failed')
|
|
1734
|
+
return;
|
|
1735
|
+
if (draft.auto_run?.token !== runnerToken)
|
|
1736
|
+
return;
|
|
1737
|
+
const decision = (0, retry_policy_1.decideRetry)({ policy, run: draft, nodeId });
|
|
1738
|
+
if (!decision.retry || decision.attempt === undefined)
|
|
1739
|
+
return;
|
|
1740
|
+
const now = new Date().toISOString();
|
|
1741
|
+
draft.status = 'retrying';
|
|
1742
|
+
delete draft.error;
|
|
1743
|
+
delete draft.manual_resume;
|
|
1744
|
+
draft.audit = draft.audit ?? [];
|
|
1745
|
+
draft.audit.push({
|
|
1746
|
+
exec_id: draft.id,
|
|
1747
|
+
node_id: nodeId,
|
|
1748
|
+
type: 'retry',
|
|
1749
|
+
summary: `auto retry ${decision.attempt + 1}/${policy.max + 1} in ${delayMs}ms`,
|
|
1750
|
+
started_at: now,
|
|
1751
|
+
ended_at: now,
|
|
1752
|
+
duration_ms: 0,
|
|
1753
|
+
exit_code: null,
|
|
1754
|
+
trigger: 'auto',
|
|
1755
|
+
});
|
|
1756
|
+
reserved = true;
|
|
1757
|
+
});
|
|
1758
|
+
return { run, reserved };
|
|
1759
|
+
}
|
|
1760
|
+
async waitForRetry(execId, runnerToken, delayMs) {
|
|
1761
|
+
const inspect = () => {
|
|
1762
|
+
const run = this.loadRun(execId);
|
|
1763
|
+
if (!run)
|
|
1764
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1765
|
+
if (run.status === 'cancelled') {
|
|
1766
|
+
return { run, outcome: 'cancelled', reason: 'run cancelled during retry delay' };
|
|
1767
|
+
}
|
|
1768
|
+
if (run.auto_run?.token !== runnerToken) {
|
|
1769
|
+
return { run, outcome: 'paused', reason: 'auto-run lease lost during retry delay' };
|
|
1770
|
+
}
|
|
1771
|
+
if (run.status !== 'retrying') {
|
|
1772
|
+
return { run, outcome: 'paused', reason: 'run state changed during retry delay' };
|
|
1773
|
+
}
|
|
1774
|
+
return null;
|
|
1775
|
+
};
|
|
1776
|
+
let remaining = Math.max(0, delayMs);
|
|
1777
|
+
while (remaining > 0) {
|
|
1778
|
+
const slice = Math.min(remaining, RETRY_WAIT_SLICE_MS);
|
|
1779
|
+
await this.sleep(slice);
|
|
1780
|
+
remaining -= slice;
|
|
1781
|
+
const stopped = inspect();
|
|
1782
|
+
if (stopped)
|
|
1783
|
+
return stopped;
|
|
1086
1784
|
}
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1785
|
+
const stopped = inspect();
|
|
1786
|
+
if (stopped)
|
|
1787
|
+
return stopped;
|
|
1788
|
+
const run = this.loadRun(execId);
|
|
1789
|
+
if (!run)
|
|
1790
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1791
|
+
return { run, outcome: 'ready' };
|
|
1792
|
+
}
|
|
1793
|
+
async runNodeWithRetry(execId, nodeId, runnerToken, policy) {
|
|
1794
|
+
let token = runnerToken;
|
|
1094
1795
|
while (true) {
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1796
|
+
try {
|
|
1797
|
+
const run = await this.executeNode(execId, nodeId, { trigger: 'auto' });
|
|
1798
|
+
if (run.status === 'cancelled') {
|
|
1799
|
+
return { run, runnerToken: token, outcome: 'cancelled', reason: run.error };
|
|
1800
|
+
}
|
|
1801
|
+
return { run, runnerToken: token, outcome: 'success' };
|
|
1802
|
+
}
|
|
1803
|
+
catch (caught) {
|
|
1804
|
+
const run = this.loadRun(execId);
|
|
1805
|
+
if (!run)
|
|
1806
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1807
|
+
if (run.status === 'cancelled') {
|
|
1808
|
+
return { run, runnerToken: token, outcome: 'cancelled', reason: run.error };
|
|
1809
|
+
}
|
|
1810
|
+
if (run.status !== 'failed' || !policy) {
|
|
1811
|
+
return { run, runnerToken: token, outcome: run.status === 'failed' ? 'failed' : 'paused', reason: getErrorMessage(caught) };
|
|
1812
|
+
}
|
|
1813
|
+
const decision = (0, retry_policy_1.decideRetry)({ policy, run, nodeId });
|
|
1814
|
+
if (!decision.retry || decision.attempt === undefined) {
|
|
1815
|
+
return { run, runnerToken: token, outcome: 'failed', reason: getErrorMessage(caught) };
|
|
1816
|
+
}
|
|
1817
|
+
const delayMs = (0, retry_policy_1.computeRetryDelayMs)(policy, decision.attempt);
|
|
1818
|
+
const reservation = await this.reserveAutoRetry(execId, nodeId, token, policy, delayMs);
|
|
1819
|
+
if (!reservation.reserved) {
|
|
1820
|
+
const latest = reservation.run;
|
|
1821
|
+
if (latest.status === 'cancelled') {
|
|
1822
|
+
return { run: latest, runnerToken: token, outcome: 'cancelled', reason: latest.error };
|
|
1823
|
+
}
|
|
1824
|
+
return {
|
|
1825
|
+
run: latest,
|
|
1826
|
+
runnerToken: token,
|
|
1827
|
+
outcome: latest.status === 'failed' ? 'failed' : 'paused',
|
|
1828
|
+
reason: getErrorMessage(caught),
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
const waited = await this.waitForRetry(execId, token, delayMs);
|
|
1832
|
+
if (waited.outcome !== 'ready') {
|
|
1833
|
+
return { run: waited.run, runnerToken: token, outcome: waited.outcome, reason: waited.reason };
|
|
1834
|
+
}
|
|
1835
|
+
const nextToken = await this.claimAutoRunLease(execId, nodeId, token);
|
|
1836
|
+
if (!nextToken) {
|
|
1837
|
+
const latest = this.loadRun(execId);
|
|
1838
|
+
if (!latest)
|
|
1839
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1840
|
+
if (latest.status === 'cancelled') {
|
|
1841
|
+
return { run: latest, runnerToken: token, outcome: 'cancelled', reason: latest.error };
|
|
1842
|
+
}
|
|
1843
|
+
return {
|
|
1844
|
+
run: latest,
|
|
1845
|
+
runnerToken: token,
|
|
1846
|
+
outcome: 'paused',
|
|
1847
|
+
reason: 'run stopped before retry lease could be reclaimed',
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
token = nextToken;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
async runAuto(execId) {
|
|
1855
|
+
const leaseKey = `${this.resolvedDir}::${execId}`;
|
|
1856
|
+
if (inFlightAutoRuns.has(leaseKey)) {
|
|
1857
|
+
const run = this.loadRun(execId);
|
|
1858
|
+
if (!run)
|
|
1859
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1860
|
+
console.log(`runAuto "${execId}": already in-flight — skipping duplicate auto-run (single-flight guard)`);
|
|
1861
|
+
return { run, outcome: 'paused', reason: 'execution already in-flight (single-flight guard)', executed: [] };
|
|
1862
|
+
}
|
|
1863
|
+
inFlightAutoRuns.add(leaseKey);
|
|
1864
|
+
let runnerToken;
|
|
1865
|
+
try {
|
|
1866
|
+
const initial = this.loadRun(execId);
|
|
1867
|
+
if (!initial)
|
|
1868
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1869
|
+
if (initial.auto_run?.pid === process.pid) {
|
|
1870
|
+
runnerToken = initial.auto_run.token;
|
|
1113
1871
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1872
|
+
const doc = this.loadWorkflowYaml(initial.workflow_file);
|
|
1873
|
+
if (!doc) {
|
|
1874
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Failed to load workflow file: ${initial.workflow_file}`);
|
|
1116
1875
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1876
|
+
const spec = doc.workflows[initial.workflow_id];
|
|
1877
|
+
if (!spec) {
|
|
1878
|
+
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Workflow "${initial.workflow_id}" not found in ${initial.workflow_file}`);
|
|
1119
1879
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1880
|
+
const executed = [];
|
|
1881
|
+
if (initial.status === 'completed' || initial.status === 'failed' || initial.status === 'cancelled') {
|
|
1882
|
+
return {
|
|
1883
|
+
run: initial,
|
|
1884
|
+
outcome: initial.status === 'completed' ? 'completed' : initial.status === 'cancelled' ? 'cancelled' : 'failed',
|
|
1885
|
+
reason: `run already ${initial.status}`,
|
|
1886
|
+
executed,
|
|
1887
|
+
};
|
|
1122
1888
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1889
|
+
const firstNode = Object.keys(spec.nodes ?? {})[0];
|
|
1890
|
+
const maxIterations = (typeof spec.max_iterations === 'number' && spec.max_iterations > 0)
|
|
1891
|
+
? spec.max_iterations
|
|
1892
|
+
: 30;
|
|
1893
|
+
let run = initial;
|
|
1894
|
+
while (true) {
|
|
1895
|
+
run = this.loadRun(execId);
|
|
1896
|
+
if (run.status === 'completed')
|
|
1897
|
+
return { run, outcome: 'completed', executed };
|
|
1898
|
+
if (run.status === 'failed')
|
|
1899
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1900
|
+
if (run.status === 'cancelled')
|
|
1901
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1902
|
+
const nodeId = run.current_node || firstNode;
|
|
1903
|
+
if (!nodeId) {
|
|
1904
|
+
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1905
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1906
|
+
failedNode: r.current_node || '',
|
|
1907
|
+
attempt: (0, failure_1.readNodeAttempt)(r, r.current_node || ''),
|
|
1908
|
+
causeCode: 'CONFIG_INVALID',
|
|
1909
|
+
exitCode: null,
|
|
1910
|
+
signal: null,
|
|
1911
|
+
errorOrigin: null,
|
|
1912
|
+
});
|
|
1913
|
+
assignRunFailure(r, failure, 'workflow has no nodes');
|
|
1914
|
+
appendFailureAudit(r, failure, 'config', 'auto');
|
|
1915
|
+
});
|
|
1916
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1917
|
+
}
|
|
1918
|
+
const node = spec.nodes[nodeId];
|
|
1919
|
+
if (!node) {
|
|
1920
|
+
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1921
|
+
r.current_node = nodeId;
|
|
1922
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1923
|
+
failedNode: nodeId,
|
|
1924
|
+
attempt: (0, failure_1.readNodeAttempt)(r, nodeId),
|
|
1925
|
+
causeCode: 'CONFIG_INVALID',
|
|
1926
|
+
exitCode: null,
|
|
1927
|
+
signal: null,
|
|
1928
|
+
errorOrigin: null,
|
|
1929
|
+
});
|
|
1930
|
+
assignRunFailure(r, failure, `node "${nodeId}" not found in workflow`);
|
|
1931
|
+
appendFailureAudit(r, failure, 'config', 'auto');
|
|
1932
|
+
});
|
|
1933
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1934
|
+
}
|
|
1935
|
+
const parsedRetryPolicy = (0, retry_policy_1.parseRetryPolicy)(node);
|
|
1936
|
+
if (!parsedRetryPolicy.ok) {
|
|
1937
|
+
run = await this.failAutoConfiguration(execId, nodeId, parsedRetryPolicy.reason);
|
|
1938
|
+
if (run.status === 'cancelled') {
|
|
1939
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1940
|
+
}
|
|
1941
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1942
|
+
}
|
|
1943
|
+
if (node.type === 'approval') {
|
|
1944
|
+
if (run.current_node !== nodeId) {
|
|
1945
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1946
|
+
}
|
|
1947
|
+
return { run, outcome: 'paused', reason: `approval node "${nodeId}" requires manual decision`, executed };
|
|
1948
|
+
}
|
|
1949
|
+
if (node.type === 'parallel') {
|
|
1950
|
+
if (run.current_node !== nodeId) {
|
|
1951
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1952
|
+
}
|
|
1953
|
+
return { run, outcome: 'paused', reason: `parallel auto-run not implemented (node "${nodeId}")`, executed };
|
|
1954
|
+
}
|
|
1955
|
+
if (!KNOWN_AUTO_NODE_TYPES.has(node.type)) {
|
|
1956
|
+
if (run.current_node !== nodeId) {
|
|
1957
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1958
|
+
}
|
|
1959
|
+
return { run, outcome: 'paused', reason: `unknown node type "${node.type}" (node "${nodeId}")`, executed };
|
|
1960
|
+
}
|
|
1961
|
+
const claimedToken = await this.claimAutoRunLease(execId, nodeId, runnerToken);
|
|
1962
|
+
if (!claimedToken) {
|
|
1963
|
+
run = this.loadRun(execId);
|
|
1964
|
+
return {
|
|
1965
|
+
run,
|
|
1966
|
+
outcome: run.status === 'cancelled' ? 'cancelled' : 'paused',
|
|
1967
|
+
reason: run.status === 'cancelled' ? run.error : 'run stopped before auto-run lease could be claimed',
|
|
1968
|
+
executed,
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
runnerToken = claimedToken;
|
|
1126
1972
|
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1127
|
-
r.status
|
|
1128
|
-
|
|
1973
|
+
if (r.status === 'cancelled')
|
|
1974
|
+
return;
|
|
1975
|
+
const engine = readRunEngineState(r);
|
|
1976
|
+
engine.visitCounts[nodeId] = (engine.visitCounts[nodeId] ?? 0) + 1;
|
|
1977
|
+
engine.iterations += 1;
|
|
1978
|
+
r.state['__engine'] = engine;
|
|
1979
|
+
if (engine.visitCounts[nodeId] > maxIterations || engine.iterations > maxIterations) {
|
|
1980
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
1981
|
+
failedNode: nodeId,
|
|
1982
|
+
attempt: (0, failure_1.readNodeAttempt)(r, nodeId),
|
|
1983
|
+
causeCode: 'CONFIG_INVALID',
|
|
1984
|
+
exitCode: null,
|
|
1985
|
+
signal: null,
|
|
1986
|
+
errorOrigin: null,
|
|
1987
|
+
});
|
|
1988
|
+
assignRunFailure(r, failure, `auto runner exceeded max_iterations (${maxIterations}); possible loop at "${nodeId}"`);
|
|
1989
|
+
appendFailureAudit(r, failure, 'config', 'auto');
|
|
1990
|
+
}
|
|
1129
1991
|
});
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
run
|
|
1992
|
+
if (run.status === 'cancelled') {
|
|
1993
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1994
|
+
}
|
|
1995
|
+
if (run.status === 'failed') {
|
|
1996
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1997
|
+
}
|
|
1998
|
+
const nodeResult = await this.runNodeWithRetry(execId, nodeId, runnerToken, parsedRetryPolicy.policy);
|
|
1999
|
+
run = nodeResult.run;
|
|
2000
|
+
runnerToken = nodeResult.runnerToken;
|
|
2001
|
+
if (nodeResult.outcome !== 'success') {
|
|
2002
|
+
return { run, outcome: nodeResult.outcome, reason: nodeResult.reason, executed };
|
|
2003
|
+
}
|
|
1134
2004
|
executed.push(nodeId);
|
|
2005
|
+
if (run.status === 'completed')
|
|
2006
|
+
return { run, outcome: 'completed', executed };
|
|
2007
|
+
if (run.status === 'failed')
|
|
2008
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
2009
|
+
if (run.status === 'cancelled')
|
|
2010
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
2011
|
+
if (!SELF_ADVANCING_NODE_TYPES.has(node.type)) {
|
|
2012
|
+
const next = node.next;
|
|
2013
|
+
if (!next) {
|
|
2014
|
+
return { run, outcome: 'paused', reason: `node "${nodeId}" has no "next"; stopping`, executed };
|
|
2015
|
+
}
|
|
2016
|
+
run = this.moveNode(execId, next);
|
|
2017
|
+
}
|
|
1135
2018
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1144
|
-
if (run.status === 'cancelled')
|
|
1145
|
-
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1146
|
-
if (!SELF_ADVANCING_NODE_TYPES.has(node.type)) {
|
|
1147
|
-
const next = node.next;
|
|
1148
|
-
if (!next) {
|
|
1149
|
-
return { run, outcome: 'paused', reason: `node "${nodeId}" has no "next"; stopping`, executed };
|
|
2019
|
+
}
|
|
2020
|
+
finally {
|
|
2021
|
+
if (runnerToken) {
|
|
2022
|
+
try {
|
|
2023
|
+
await this.releaseAutoRunLease(execId, runnerToken);
|
|
2024
|
+
}
|
|
2025
|
+
catch {
|
|
1150
2026
|
}
|
|
1151
|
-
run = this.moveNode(execId, next);
|
|
1152
2027
|
}
|
|
2028
|
+
inFlightAutoRuns.delete(leaseKey);
|
|
1153
2029
|
}
|
|
1154
2030
|
}
|
|
1155
2031
|
cancelRun(execId, reason = 'Cancelled by user') {
|
|
1156
|
-
|
|
2032
|
+
const cancelled = this.atomicUpdate(execId, (run) => {
|
|
1157
2033
|
if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {
|
|
1158
2034
|
return;
|
|
1159
2035
|
}
|
|
1160
2036
|
const now = new Date();
|
|
2037
|
+
const activeNodeId = run.auto_run?.node_id || run.current_node || '';
|
|
2038
|
+
const failure = (0, failure_1.buildNodeFailure)({
|
|
2039
|
+
failedNode: activeNodeId,
|
|
2040
|
+
attempt: (0, failure_1.readNodeAttempt)(run, activeNodeId),
|
|
2041
|
+
causeCode: 'CANCELLED',
|
|
2042
|
+
exitCode: null,
|
|
2043
|
+
signal: null,
|
|
2044
|
+
errorOrigin: null,
|
|
2045
|
+
});
|
|
2046
|
+
assignRunFailure(run, failure, reason);
|
|
1161
2047
|
run.status = 'cancelled';
|
|
1162
|
-
run.
|
|
2048
|
+
delete run.auto_run;
|
|
2049
|
+
delete run.manual_resume;
|
|
2050
|
+
appendFailureAudit(run, failure, 'cancel', 'manual', now.toISOString());
|
|
2051
|
+
});
|
|
2052
|
+
if (cancelled.status === 'cancelled') {
|
|
2053
|
+
requestActiveProcessCancellation(this.resolvedDir, execId);
|
|
2054
|
+
}
|
|
2055
|
+
return cancelled;
|
|
2056
|
+
}
|
|
2057
|
+
reset(execId) {
|
|
2058
|
+
return this.atomicUpdate(execId, (run) => {
|
|
2059
|
+
run.state = JSON.parse(JSON.stringify(run.initial_state));
|
|
2060
|
+
run.current_node = '';
|
|
2061
|
+
run.completed_nodes = [];
|
|
2062
|
+
run.status = 'pending';
|
|
2063
|
+
delete run.error;
|
|
2064
|
+
delete run.last_failure;
|
|
2065
|
+
delete run.node_attempts;
|
|
2066
|
+
delete run.auto_run;
|
|
2067
|
+
delete run.manual_resume;
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
retry(execId, options = {}) {
|
|
2071
|
+
const snapshot = this.loadRun(execId);
|
|
2072
|
+
if (!snapshot)
|
|
2073
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
2074
|
+
const doc = this.loadWorkflowYaml(snapshot.workflow_file);
|
|
2075
|
+
if (!doc)
|
|
2076
|
+
throw new Error(`Failed to load workflow file: ${snapshot.workflow_file}`);
|
|
2077
|
+
const spec = doc.workflows[snapshot.workflow_id];
|
|
2078
|
+
if (!spec)
|
|
2079
|
+
throw new Error(`Workflow "${snapshot.workflow_id}" not found`);
|
|
2080
|
+
if (snapshot.status !== 'failed') {
|
|
2081
|
+
throw new Error(`Run "${execId}" is not failed (status=${snapshot.status ?? 'running'}); only failed runs can be retried`);
|
|
2082
|
+
}
|
|
2083
|
+
if (!isRetryableFailure(snapshot, spec)) {
|
|
2084
|
+
throw new Error(`Run "${execId}" failed at a non-retryable node "${snapshot.current_node}" (error node); use reset to restart`);
|
|
2085
|
+
}
|
|
2086
|
+
return this.atomicUpdate(execId, (run) => {
|
|
2087
|
+
if (run.status !== 'failed') {
|
|
2088
|
+
throw new Error(`Run "${execId}" is not failed (status=${run.status ?? 'pending'}); only failed runs can be retried`);
|
|
2089
|
+
}
|
|
2090
|
+
const failedNode = run.current_node;
|
|
2091
|
+
if (failedNode && run.last_failure) {
|
|
2092
|
+
const lastAttempt = run.last_failure.attempt;
|
|
2093
|
+
const storedAttempt = run.node_attempts?.[failedNode];
|
|
2094
|
+
if (typeof lastAttempt === 'number' && Number.isInteger(lastAttempt) && lastAttempt > 0
|
|
2095
|
+
&& (typeof storedAttempt !== 'number' || storedAttempt < lastAttempt)) {
|
|
2096
|
+
run.node_attempts = run.node_attempts ?? {};
|
|
2097
|
+
run.node_attempts[failedNode] = lastAttempt;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
run.status = 'retrying';
|
|
2101
|
+
delete run.error;
|
|
2102
|
+
delete run.manual_resume;
|
|
2103
|
+
run.retry_count = (run.retry_count ?? 0) + 1;
|
|
1163
2104
|
run.audit = run.audit ?? [];
|
|
2105
|
+
const now = new Date().toISOString();
|
|
2106
|
+
if (options.autoRun === false) {
|
|
2107
|
+
delete run.auto_run;
|
|
2108
|
+
run.manual_resume = { node_id: failedNode, requested_at: now };
|
|
2109
|
+
}
|
|
2110
|
+
else {
|
|
2111
|
+
run.auto_run = {
|
|
2112
|
+
pid: process.pid,
|
|
2113
|
+
token: (0, sdk_1.generateId)('wfr'),
|
|
2114
|
+
node_id: failedNode,
|
|
2115
|
+
started_at: now,
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
1164
2118
|
run.audit.push({
|
|
1165
2119
|
exec_id: run.id,
|
|
1166
|
-
node_id:
|
|
1167
|
-
type: '
|
|
1168
|
-
summary:
|
|
1169
|
-
started_at: now
|
|
1170
|
-
ended_at: now
|
|
2120
|
+
node_id: failedNode,
|
|
2121
|
+
type: 'retry',
|
|
2122
|
+
summary: `manual retry #${run.retry_count}`,
|
|
2123
|
+
started_at: now,
|
|
2124
|
+
ended_at: now,
|
|
1171
2125
|
duration_ms: 0,
|
|
1172
2126
|
exit_code: null,
|
|
1173
|
-
error: reason,
|
|
1174
2127
|
trigger: 'manual',
|
|
1175
2128
|
});
|
|
1176
2129
|
});
|
|
1177
2130
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
}
|
|
2131
|
+
listRunIds() {
|
|
2132
|
+
try {
|
|
2133
|
+
return fs.readdirSync(this.resolvedDir, { withFileTypes: true })
|
|
2134
|
+
.filter((entry) => entry.isFile() && !entry.name.startsWith('.') && entry.name.endsWith('.json'))
|
|
2135
|
+
.map((entry) => entry.name.slice(0, -'.json'.length));
|
|
2136
|
+
}
|
|
2137
|
+
catch (error) {
|
|
2138
|
+
if (error.code === 'ENOENT')
|
|
2139
|
+
return [];
|
|
2140
|
+
throw error;
|
|
2141
|
+
}
|
|
1184
2142
|
}
|
|
1185
2143
|
listRuns(opts) {
|
|
1186
2144
|
this.ensureRunsDir();
|
|
@@ -1188,7 +2146,10 @@ class RunManager {
|
|
|
1188
2146
|
const runs = [];
|
|
1189
2147
|
for (const file of files) {
|
|
1190
2148
|
try {
|
|
1191
|
-
const
|
|
2149
|
+
const execId = file.slice(0, -'.json'.length);
|
|
2150
|
+
const run = this.loadRun(execId);
|
|
2151
|
+
if (!run)
|
|
2152
|
+
continue;
|
|
1192
2153
|
if (opts?.workflowId && run.workflow_id !== opts.workflowId)
|
|
1193
2154
|
continue;
|
|
1194
2155
|
runs.push(run);
|