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