@crewx/workflow 0.3.22-rc.192 → 0.3.22-rc.194
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/dist/cli.d.ts +1 -1
- package/dist/cli.js +17 -1190
- package/dist/src/card-state.d.ts +1 -1
- package/dist/src/card-state.js +1 -121
- package/dist/src/engine.d.ts +1 -1
- package/dist/src/engine.js +1 -379
- package/dist/src/failure.d.ts +1 -1
- package/dist/src/failure.js +1 -122
- package/dist/src/file-lock.d.ts +1 -1
- package/dist/src/file-lock.js +1 -97
- package/dist/src/list-view.d.ts +1 -1
- package/dist/src/list-view.js +3 -188
- package/dist/src/mermaid.d.ts +1 -1
- package/dist/src/mermaid.js +2 -133
- package/dist/src/parent-task-sync.d.ts +1 -1
- package/dist/src/parent-task-sync.js +1 -445
- package/dist/src/retry-policy.d.ts +1 -1
- package/dist/src/retry-policy.js +1 -140
- package/dist/src/run-manager.d.ts +1 -1
- package/dist/src/run-manager.js +4 -2165
- package/dist/src/types.d.ts +1 -1
- package/dist/src/types.js +1 -5
- package/dist/src/utils/output-format.d.ts +1 -1
- package/dist/src/utils/output-format.js +12 -206
- package/dist/src/utils/shell-security.d.ts +1 -1
- package/dist/src/utils/shell-security.js +1 -138
- package/dist/src/workflow-persistence.d.ts +1 -1
- package/dist/src/workflow-persistence.js +1 -224
- package/package.json +4 -4
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/src/card-state.d.ts.map +0 -1
- package/dist/src/card-state.js.map +0 -1
- package/dist/src/engine.d.ts.map +0 -1
- package/dist/src/engine.js.map +0 -1
- package/dist/src/failure.d.ts.map +0 -1
- package/dist/src/failure.js.map +0 -1
- package/dist/src/file-lock.d.ts.map +0 -1
- package/dist/src/file-lock.js.map +0 -1
- package/dist/src/list-view.d.ts.map +0 -1
- package/dist/src/list-view.js.map +0 -1
- package/dist/src/mermaid.d.ts.map +0 -1
- package/dist/src/mermaid.js.map +0 -1
- package/dist/src/parent-task-sync.d.ts.map +0 -1
- package/dist/src/parent-task-sync.js.map +0 -1
- package/dist/src/retry-policy.d.ts.map +0 -1
- package/dist/src/retry-policy.js.map +0 -1
- package/dist/src/run-manager.d.ts.map +0 -1
- package/dist/src/run-manager.js.map +0 -1
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/types.js.map +0 -1
- package/dist/src/utils/output-format.d.ts.map +0 -1
- package/dist/src/utils/output-format.js.map +0 -1
- package/dist/src/utils/shell-security.d.ts.map +0 -1
- package/dist/src/utils/shell-security.js.map +0 -1
- package/dist/src/workflow-persistence.d.ts.map +0 -1
- package/dist/src/workflow-persistence.js.map +0 -1
package/dist/src/run-manager.js
CHANGED
|
@@ -1,2165 +1,4 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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
|
-
exports.parseSetArgs = parseSetArgs;
|
|
38
|
-
exports.readStdin = readStdin;
|
|
39
|
-
exports.interpolateTemplate = interpolateTemplate;
|
|
40
|
-
exports.safeEvaluate = safeEvaluate;
|
|
41
|
-
exports.safeEvaluateBranchCondition = safeEvaluateBranchCondition;
|
|
42
|
-
exports.validateSafeBranchCondition = validateSafeBranchCondition;
|
|
43
|
-
exports.normalizeRunStatus = normalizeRunStatus;
|
|
44
|
-
exports.isRetryableFailure = isRetryableFailure;
|
|
45
|
-
const fs = __importStar(require("fs"));
|
|
46
|
-
const path = __importStar(require("path"));
|
|
47
|
-
const yaml = __importStar(require("js-yaml"));
|
|
48
|
-
const cp = __importStar(require("child_process"));
|
|
49
|
-
const sdk_1 = require("@crewx/sdk");
|
|
50
|
-
const failure_1 = require("./failure");
|
|
51
|
-
const file_lock_1 = require("./file-lock");
|
|
52
|
-
const output_format_1 = require("./utils/output-format");
|
|
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; } });
|
|
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
|
-
}
|
|
68
|
-
const KNOWN_AUTO_NODE_TYPES = new Set([
|
|
69
|
-
'agent_task', 'skill_task', 'shell_task', 'branch', 'expression', 'join', 'end', 'error',
|
|
70
|
-
]);
|
|
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 (0, sdk_1.createCrewxChildInvocation)(resolution, { baseEnv: env });
|
|
78
|
-
}
|
|
79
|
-
async function killProcessGroup(child, signal = 'SIGKILL') {
|
|
80
|
-
if (!child.pid)
|
|
81
|
-
return;
|
|
82
|
-
try {
|
|
83
|
-
const result = await (0, sdk_1.terminateProcessTree)(child.pid, { signal });
|
|
84
|
-
if (result.status === 'failed' || result.status === 'not_found') {
|
|
85
|
-
try {
|
|
86
|
-
child.kill(signal);
|
|
87
|
-
}
|
|
88
|
-
catch { }
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
try {
|
|
93
|
-
child.kill(signal);
|
|
94
|
-
}
|
|
95
|
-
catch { }
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
async function runProcess(command, argv, opts) {
|
|
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
|
-
}
|
|
120
|
-
let child;
|
|
121
|
-
try {
|
|
122
|
-
child = cp.spawn(command, argv, {
|
|
123
|
-
cwd: opts.cwd,
|
|
124
|
-
env: opts.env,
|
|
125
|
-
stdio: [opts.stdin !== undefined ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
126
|
-
shell: opts.shell ?? false,
|
|
127
|
-
detached: process.platform !== 'win32',
|
|
128
|
-
windowsHide: opts.windowsHide ?? true,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
catch (err) {
|
|
132
|
-
resolve({
|
|
133
|
-
status: null,
|
|
134
|
-
stdout: '',
|
|
135
|
-
stderr: '',
|
|
136
|
-
timedOut: false,
|
|
137
|
-
signal: null,
|
|
138
|
-
errorOrigin: 'spawn',
|
|
139
|
-
error: err,
|
|
140
|
-
});
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
let stdout = '';
|
|
144
|
-
let stderr = '';
|
|
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
|
-
};
|
|
172
|
-
const finish = (result) => {
|
|
173
|
-
if (settled)
|
|
174
|
-
return;
|
|
175
|
-
settled = true;
|
|
176
|
-
cleanup();
|
|
177
|
-
resolve(cancellationRequested ? { ...result, cancelled: true, timedOut: false } : result);
|
|
178
|
-
};
|
|
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
|
-
}));
|
|
213
|
-
}, opts.timeout);
|
|
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();
|
|
276
|
-
});
|
|
277
|
-
}
|
|
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;
|
|
284
|
-
}
|
|
285
|
-
function summarizeNode(node) {
|
|
286
|
-
switch (node.type) {
|
|
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
|
-
}
|
|
297
|
-
case 'shell_task':
|
|
298
|
-
return 'shell_task';
|
|
299
|
-
case 'branch':
|
|
300
|
-
return 'branch';
|
|
301
|
-
case 'expression':
|
|
302
|
-
return 'expression';
|
|
303
|
-
default:
|
|
304
|
-
return 'workflow node';
|
|
305
|
-
}
|
|
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
|
-
}
|
|
340
|
-
async function storeNodeOutput(nodeSpec, nodeId, stdout, stateUpdates) {
|
|
341
|
-
if (!nodeSpec.output)
|
|
342
|
-
return;
|
|
343
|
-
const trimmed = stdout.trim();
|
|
344
|
-
if (nodeSpec.output_format === 'json') {
|
|
345
|
-
let parsed;
|
|
346
|
-
try {
|
|
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
|
-
}
|
|
351
|
-
}
|
|
352
|
-
catch (e) {
|
|
353
|
-
throw (0, failure_1.createWorkflowFailureError)('OUTPUT_INVALID', `Node "${nodeId}" output_format=json parse or validation failed: ${getErrorMessage(e)}`);
|
|
354
|
-
}
|
|
355
|
-
stateUpdates[nodeSpec.output] = parsed;
|
|
356
|
-
}
|
|
357
|
-
else {
|
|
358
|
-
stateUpdates[nodeSpec.output] = trimmed;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
function parseValue(raw) {
|
|
362
|
-
try {
|
|
363
|
-
return JSON.parse(raw);
|
|
364
|
-
}
|
|
365
|
-
catch {
|
|
366
|
-
return raw;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
function parseSetArgs(args) {
|
|
370
|
-
const sets = {};
|
|
371
|
-
for (let i = 0; i < args.length; i++) {
|
|
372
|
-
const arg = args[i];
|
|
373
|
-
if (arg === '--set' && i + 1 < args.length) {
|
|
374
|
-
const pair = args[i + 1];
|
|
375
|
-
const eqIdx = pair.indexOf('=');
|
|
376
|
-
if (eqIdx !== -1) {
|
|
377
|
-
const key = pair.substring(0, eqIdx);
|
|
378
|
-
const value = pair.substring(eqIdx + 1);
|
|
379
|
-
sets[key] = parseValue(value);
|
|
380
|
-
}
|
|
381
|
-
i++;
|
|
382
|
-
}
|
|
383
|
-
else if (arg.startsWith('--set=')) {
|
|
384
|
-
const rest = arg.substring('--set='.length);
|
|
385
|
-
const eqIdx = rest.indexOf('=');
|
|
386
|
-
if (eqIdx !== -1) {
|
|
387
|
-
const key = rest.substring(0, eqIdx);
|
|
388
|
-
const value = rest.substring(eqIdx + 1);
|
|
389
|
-
sets[key] = parseValue(value);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
return sets;
|
|
394
|
-
}
|
|
395
|
-
async function readStdin() {
|
|
396
|
-
const chunks = [];
|
|
397
|
-
for await (const chunk of process.stdin) {
|
|
398
|
-
chunks.push(chunk);
|
|
399
|
-
}
|
|
400
|
-
return Buffer.concat(chunks).toString('utf-8').trimEnd();
|
|
401
|
-
}
|
|
402
|
-
function interpolateTemplate(template, state) {
|
|
403
|
-
return template.replace(/\{\{([\w.]+)\}\}/g, (_, expr) => {
|
|
404
|
-
const parts = expr.split('.');
|
|
405
|
-
let value = state[parts[0]];
|
|
406
|
-
for (let i = 1; i < parts.length && value != null; i++) {
|
|
407
|
-
value = value[parts[i]];
|
|
408
|
-
}
|
|
409
|
-
if (value === null || value === undefined)
|
|
410
|
-
return '';
|
|
411
|
-
if (typeof value === 'object')
|
|
412
|
-
return JSON.stringify(value, null, 2);
|
|
413
|
-
return String(value);
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
const FORBIDDEN_IDENTS = new Set([
|
|
417
|
-
'require', 'import', 'process', 'global', 'globalThis',
|
|
418
|
-
'Function', 'eval', '__proto__', 'constructor', 'prototype',
|
|
419
|
-
]);
|
|
420
|
-
const FORBIDDEN_STATE_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
421
|
-
function tokenize(expr) {
|
|
422
|
-
const tokens = [];
|
|
423
|
-
let i = 0;
|
|
424
|
-
while (i < expr.length) {
|
|
425
|
-
const ch = expr[i];
|
|
426
|
-
if (/\s/.test(ch)) {
|
|
427
|
-
i++;
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
if (/\d/.test(ch)) {
|
|
431
|
-
let num = '';
|
|
432
|
-
while (i < expr.length && /[\d.]/.test(expr[i])) {
|
|
433
|
-
num += expr[i];
|
|
434
|
-
i++;
|
|
435
|
-
}
|
|
436
|
-
tokens.push({ type: 'NUMBER', value: Number(num) });
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
if (ch === "'" || ch === '"') {
|
|
440
|
-
const quote = ch;
|
|
441
|
-
i++;
|
|
442
|
-
let str = '';
|
|
443
|
-
while (i < expr.length && expr[i] !== quote) {
|
|
444
|
-
if (expr[i] === '\\') {
|
|
445
|
-
i++;
|
|
446
|
-
str += expr[i] ?? '';
|
|
447
|
-
}
|
|
448
|
-
else {
|
|
449
|
-
str += expr[i];
|
|
450
|
-
}
|
|
451
|
-
i++;
|
|
452
|
-
}
|
|
453
|
-
if (i >= expr.length)
|
|
454
|
-
throw new Error('Unterminated string literal');
|
|
455
|
-
i++;
|
|
456
|
-
tokens.push({ type: 'STRING', value: str });
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
if (/[a-zA-Z_]/.test(ch)) {
|
|
460
|
-
let ident = '';
|
|
461
|
-
while (i < expr.length && /\w/.test(expr[i])) {
|
|
462
|
-
ident += expr[i];
|
|
463
|
-
i++;
|
|
464
|
-
}
|
|
465
|
-
if (ident === 'true' || ident === 'false') {
|
|
466
|
-
tokens.push({ type: 'BOOLEAN', value: ident === 'true' });
|
|
467
|
-
}
|
|
468
|
-
else {
|
|
469
|
-
if (FORBIDDEN_IDENTS.has(ident)) {
|
|
470
|
-
throw new Error(`Forbidden identifier in expression: "${ident}"`);
|
|
471
|
-
}
|
|
472
|
-
tokens.push({ type: 'IDENT', value: ident });
|
|
473
|
-
}
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
if (i + 1 < expr.length) {
|
|
477
|
-
const two = expr.substring(i, i + 2);
|
|
478
|
-
const twoCharMap = {
|
|
479
|
-
'>=': 'GTE', '<=': 'LTE', '==': 'EQ', '!=': 'NEQ', '&&': 'AND', '||': 'OR',
|
|
480
|
-
};
|
|
481
|
-
if (twoCharMap[two]) {
|
|
482
|
-
tokens.push({ type: twoCharMap[two], value: two });
|
|
483
|
-
i += 2;
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
const singleCharMap = {
|
|
488
|
-
'+': 'PLUS', '-': 'MINUS', '*': 'STAR', '/': 'SLASH', '%': 'PERCENT',
|
|
489
|
-
'>': 'GT', '<': 'LT', '!': 'NOT',
|
|
490
|
-
'?': 'QUESTION', ':': 'COLON',
|
|
491
|
-
'(': 'LPAREN', ')': 'RPAREN',
|
|
492
|
-
};
|
|
493
|
-
if (singleCharMap[ch]) {
|
|
494
|
-
tokens.push({ type: singleCharMap[ch], value: ch });
|
|
495
|
-
i++;
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
498
|
-
if (ch === '.' || ch === '[' || ch === ']') {
|
|
499
|
-
throw new Error(`Forbidden character in expression: "${ch}" (property access not allowed)`);
|
|
500
|
-
}
|
|
501
|
-
throw new Error(`Unexpected character in expression: "${ch}"`);
|
|
502
|
-
}
|
|
503
|
-
tokens.push({ type: 'EOF', value: '' });
|
|
504
|
-
return tokens;
|
|
505
|
-
}
|
|
506
|
-
class ExprParser {
|
|
507
|
-
constructor(tokens, state) {
|
|
508
|
-
this.tokens = tokens;
|
|
509
|
-
this.state = state;
|
|
510
|
-
this.pos = 0;
|
|
511
|
-
}
|
|
512
|
-
peek() { return this.tokens[this.pos]; }
|
|
513
|
-
advance() { return this.tokens[this.pos++]; }
|
|
514
|
-
expect(type) {
|
|
515
|
-
const t = this.advance();
|
|
516
|
-
if (t.type !== type)
|
|
517
|
-
throw new Error(`Expected ${type}, got ${t.type}`);
|
|
518
|
-
return t;
|
|
519
|
-
}
|
|
520
|
-
parse() {
|
|
521
|
-
const result = this.parseTernary();
|
|
522
|
-
if (this.peek().type !== 'EOF') {
|
|
523
|
-
throw new Error(`Unexpected token after expression: ${this.peek().type}`);
|
|
524
|
-
}
|
|
525
|
-
return result;
|
|
526
|
-
}
|
|
527
|
-
parseTernary() {
|
|
528
|
-
const cond = this.parseOr();
|
|
529
|
-
if (this.peek().type === 'QUESTION') {
|
|
530
|
-
this.advance();
|
|
531
|
-
const thenVal = this.parseTernary();
|
|
532
|
-
this.expect('COLON');
|
|
533
|
-
const elseVal = this.parseTernary();
|
|
534
|
-
return cond ? thenVal : elseVal;
|
|
535
|
-
}
|
|
536
|
-
return cond;
|
|
537
|
-
}
|
|
538
|
-
parseOr() {
|
|
539
|
-
let left = this.parseAnd();
|
|
540
|
-
while (this.peek().type === 'OR') {
|
|
541
|
-
this.advance();
|
|
542
|
-
const right = this.parseAnd();
|
|
543
|
-
left = left || right;
|
|
544
|
-
}
|
|
545
|
-
return left;
|
|
546
|
-
}
|
|
547
|
-
parseAnd() {
|
|
548
|
-
let left = this.parseEquality();
|
|
549
|
-
while (this.peek().type === 'AND') {
|
|
550
|
-
this.advance();
|
|
551
|
-
const right = this.parseEquality();
|
|
552
|
-
left = left && right;
|
|
553
|
-
}
|
|
554
|
-
return left;
|
|
555
|
-
}
|
|
556
|
-
parseEquality() {
|
|
557
|
-
let left = this.parseComparison();
|
|
558
|
-
while (this.peek().type === 'EQ' || this.peek().type === 'NEQ') {
|
|
559
|
-
const op = this.advance().type;
|
|
560
|
-
const right = this.parseComparison();
|
|
561
|
-
left = op === 'EQ' ? left == right : left != right;
|
|
562
|
-
}
|
|
563
|
-
return left;
|
|
564
|
-
}
|
|
565
|
-
parseComparison() {
|
|
566
|
-
let left = this.parseAdditive();
|
|
567
|
-
while (['GT', 'LT', 'GTE', 'LTE'].includes(this.peek().type)) {
|
|
568
|
-
const op = this.advance().type;
|
|
569
|
-
const right = this.parseAdditive();
|
|
570
|
-
switch (op) {
|
|
571
|
-
case 'GT':
|
|
572
|
-
left = left > right;
|
|
573
|
-
break;
|
|
574
|
-
case 'LT':
|
|
575
|
-
left = left < right;
|
|
576
|
-
break;
|
|
577
|
-
case 'GTE':
|
|
578
|
-
left = left >= right;
|
|
579
|
-
break;
|
|
580
|
-
case 'LTE':
|
|
581
|
-
left = left <= right;
|
|
582
|
-
break;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
return left;
|
|
586
|
-
}
|
|
587
|
-
parseAdditive() {
|
|
588
|
-
let left = this.parseMultiplicative();
|
|
589
|
-
while (this.peek().type === 'PLUS' || this.peek().type === 'MINUS') {
|
|
590
|
-
const op = this.advance().type;
|
|
591
|
-
const right = this.parseMultiplicative();
|
|
592
|
-
if (op === 'PLUS') {
|
|
593
|
-
if (typeof left === 'string' || typeof right === 'string') {
|
|
594
|
-
left = String(left) + String(right);
|
|
595
|
-
}
|
|
596
|
-
else {
|
|
597
|
-
left = left + right;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
else {
|
|
601
|
-
left = left - right;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
return left;
|
|
605
|
-
}
|
|
606
|
-
parseMultiplicative() {
|
|
607
|
-
let left = this.parseUnary();
|
|
608
|
-
while (['STAR', 'SLASH', 'PERCENT'].includes(this.peek().type)) {
|
|
609
|
-
const op = this.advance().type;
|
|
610
|
-
const right = this.parseUnary();
|
|
611
|
-
switch (op) {
|
|
612
|
-
case 'STAR':
|
|
613
|
-
left = left * right;
|
|
614
|
-
break;
|
|
615
|
-
case 'SLASH':
|
|
616
|
-
left = left / right;
|
|
617
|
-
break;
|
|
618
|
-
case 'PERCENT':
|
|
619
|
-
left = left % right;
|
|
620
|
-
break;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
return left;
|
|
624
|
-
}
|
|
625
|
-
parseUnary() {
|
|
626
|
-
if (this.peek().type === 'NOT') {
|
|
627
|
-
this.advance();
|
|
628
|
-
return !this.parseUnary();
|
|
629
|
-
}
|
|
630
|
-
if (this.peek().type === 'MINUS') {
|
|
631
|
-
this.advance();
|
|
632
|
-
return -this.parseUnary();
|
|
633
|
-
}
|
|
634
|
-
return this.parsePrimary();
|
|
635
|
-
}
|
|
636
|
-
parsePrimary() {
|
|
637
|
-
const token = this.peek();
|
|
638
|
-
switch (token.type) {
|
|
639
|
-
case 'NUMBER':
|
|
640
|
-
this.advance();
|
|
641
|
-
return token.value;
|
|
642
|
-
case 'STRING':
|
|
643
|
-
this.advance();
|
|
644
|
-
return token.value;
|
|
645
|
-
case 'BOOLEAN':
|
|
646
|
-
this.advance();
|
|
647
|
-
return token.value;
|
|
648
|
-
case 'IDENT': {
|
|
649
|
-
this.advance();
|
|
650
|
-
const key = token.value;
|
|
651
|
-
const val = this.state[key];
|
|
652
|
-
if (val === undefined || val === null)
|
|
653
|
-
return 0;
|
|
654
|
-
if (typeof val === 'boolean' || typeof val === 'number')
|
|
655
|
-
return val;
|
|
656
|
-
const strVal = String(val);
|
|
657
|
-
if (strVal === '')
|
|
658
|
-
return '';
|
|
659
|
-
const num = Number(strVal);
|
|
660
|
-
return isNaN(num) ? strVal : num;
|
|
661
|
-
}
|
|
662
|
-
case 'LPAREN': {
|
|
663
|
-
this.advance();
|
|
664
|
-
const result = this.parseTernary();
|
|
665
|
-
this.expect('RPAREN');
|
|
666
|
-
return result;
|
|
667
|
-
}
|
|
668
|
-
default:
|
|
669
|
-
throw new Error(`Unexpected token: ${token.type}`);
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
function safeEvaluate(expr, state) {
|
|
674
|
-
const tokens = tokenize(expr);
|
|
675
|
-
const parser = new ExprParser(tokens, state);
|
|
676
|
-
return parser.parse();
|
|
677
|
-
}
|
|
678
|
-
function getStatePathValue(state, pathExpr) {
|
|
679
|
-
const parts = pathExpr.split('.');
|
|
680
|
-
let value = state;
|
|
681
|
-
for (const part of parts) {
|
|
682
|
-
if (FORBIDDEN_STATE_PATH_SEGMENTS.has(part)) {
|
|
683
|
-
throw new Error(`Forbidden state path segment in expression: "${part}"`);
|
|
684
|
-
}
|
|
685
|
-
if (!part || value === null || value === undefined || typeof value !== 'object') {
|
|
686
|
-
return undefined;
|
|
687
|
-
}
|
|
688
|
-
value = value[part];
|
|
689
|
-
}
|
|
690
|
-
return value;
|
|
691
|
-
}
|
|
692
|
-
function unescapeQuotedLiteral(value) {
|
|
693
|
-
return value
|
|
694
|
-
.replace(/\\'/g, "'")
|
|
695
|
-
.replace(/\\"/g, '"')
|
|
696
|
-
.replace(/\\\\/g, '\\');
|
|
697
|
-
}
|
|
698
|
-
function normalizeBranchCondition(expr, state) {
|
|
699
|
-
const scope = {};
|
|
700
|
-
let counter = 0;
|
|
701
|
-
const bind = (value) => {
|
|
702
|
-
const key = `__state_${counter++}`;
|
|
703
|
-
scope[key] = value;
|
|
704
|
-
return key;
|
|
705
|
-
};
|
|
706
|
-
let normalized = expr
|
|
707
|
-
.replace(/!==/g, '!=')
|
|
708
|
-
.replace(/===/g, '==');
|
|
709
|
-
const replaceIncludes = (pattern) => {
|
|
710
|
-
normalized = normalized.replace(pattern, (_match, statePath, literal) => {
|
|
711
|
-
const value = getStatePathValue(state, statePath);
|
|
712
|
-
return bind(String(value ?? '').includes(unescapeQuotedLiteral(literal)));
|
|
713
|
-
});
|
|
714
|
-
};
|
|
715
|
-
replaceIncludes(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\.includes\(\s*'((?:\\.|[^'\\])*)'\s*\)/g);
|
|
716
|
-
replaceIncludes(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\.includes\(\s*"((?:\\.|[^"\\])*)"\s*\)/g);
|
|
717
|
-
normalized = normalized.replace(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)/g, (_match, statePath) => bind(getStatePathValue(state, statePath)));
|
|
718
|
-
normalized = normalized.replace(/\bNumber\(\s*([A-Za-z_]\w*)\s*\)/g, '$1');
|
|
719
|
-
normalized = normalized.replace(/\bparseInt\(\s*([A-Za-z_]\w*(?:\s*\|\|\s*(?:'[^']*'|"[^"]*"))?)\s*\)/g, '($1)');
|
|
720
|
-
return { expr: normalized, scope };
|
|
721
|
-
}
|
|
722
|
-
function safeEvaluateBranchCondition(expr, state) {
|
|
723
|
-
const normalized = normalizeBranchCondition(expr, state);
|
|
724
|
-
return safeEvaluate(normalized.expr, { ...state, ...normalized.scope });
|
|
725
|
-
}
|
|
726
|
-
function validateSafeBranchCondition(expr) {
|
|
727
|
-
safeEvaluateBranchCondition(expr, {});
|
|
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
|
-
}
|
|
809
|
-
class RunManager {
|
|
810
|
-
constructor(runsDir = '.crewx/workflow-runs', projectRoot, deps) {
|
|
811
|
-
this.runsDir = runsDir;
|
|
812
|
-
this.sleep = deps?.sleep ?? sleep;
|
|
813
|
-
this.onRunPersisted = deps?.onRunPersisted;
|
|
814
|
-
const base = projectRoot || process.env.CREWX_WORKSPACE || process.cwd();
|
|
815
|
-
this.resolvedDir = path.resolve(base, this.runsDir);
|
|
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
|
-
}
|
|
822
|
-
}
|
|
823
|
-
ensureRunsDir() {
|
|
824
|
-
if (!fs.existsSync(this.resolvedDir)) {
|
|
825
|
-
fs.mkdirSync(this.resolvedDir, { recursive: true });
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
validateExecId(execId) {
|
|
829
|
-
if (/[/\\]|\.\./.test(execId)) {
|
|
830
|
-
throw new Error(`Invalid execution ID: "${execId}"`);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
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) {
|
|
848
|
-
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
849
|
-
if (!fs.existsSync(filePath))
|
|
850
|
-
return null;
|
|
851
|
-
try {
|
|
852
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
853
|
-
}
|
|
854
|
-
catch (e) {
|
|
855
|
-
throw new Error(`Failed to parse execution file ${filePath}: ${e.message}`);
|
|
856
|
-
}
|
|
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
|
-
}
|
|
946
|
-
saveRun(run) {
|
|
947
|
-
this.ensureRunsDir();
|
|
948
|
-
run.updated_at = new Date().toISOString();
|
|
949
|
-
const filePath = path.join(this.resolvedDir, `${run.id}.json`);
|
|
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
|
-
}
|
|
997
|
-
}
|
|
998
|
-
atomicUpdate(execId, updater) {
|
|
999
|
-
this.validateExecId(execId);
|
|
1000
|
-
this.ensureRunsDir();
|
|
1001
|
-
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
1002
|
-
const lock = new file_lock_1.FileLock(filePath);
|
|
1003
|
-
lock.acquire();
|
|
1004
|
-
try {
|
|
1005
|
-
const run = this.readRun(execId);
|
|
1006
|
-
if (!run)
|
|
1007
|
-
throw new Error(`Execution not found: ${execId}`);
|
|
1008
|
-
updater(run);
|
|
1009
|
-
this.saveRun(run);
|
|
1010
|
-
return run;
|
|
1011
|
-
}
|
|
1012
|
-
finally {
|
|
1013
|
-
lock.release();
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
async atomicUpdateAsync(execId, updater) {
|
|
1017
|
-
this.validateExecId(execId);
|
|
1018
|
-
this.ensureRunsDir();
|
|
1019
|
-
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
1020
|
-
const lock = new file_lock_1.FileLock(filePath);
|
|
1021
|
-
await lock.acquireAsync();
|
|
1022
|
-
try {
|
|
1023
|
-
const run = this.readRun(execId);
|
|
1024
|
-
if (!run)
|
|
1025
|
-
throw new Error(`Execution not found: ${execId}`);
|
|
1026
|
-
updater(run);
|
|
1027
|
-
this.saveRun(run);
|
|
1028
|
-
return run;
|
|
1029
|
-
}
|
|
1030
|
-
finally {
|
|
1031
|
-
lock.release();
|
|
1032
|
-
}
|
|
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
|
-
}
|
|
1060
|
-
loadWorkflowYaml(yamlPath) {
|
|
1061
|
-
if (!fs.existsSync(yamlPath))
|
|
1062
|
-
return null;
|
|
1063
|
-
const content = fs.readFileSync(yamlPath, 'utf-8');
|
|
1064
|
-
const doc = yaml.load(content);
|
|
1065
|
-
if (!doc || !doc.workflows || typeof doc.workflows !== 'object')
|
|
1066
|
-
return null;
|
|
1067
|
-
return doc;
|
|
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
|
-
}
|
|
1075
|
-
start(yamlPath, workflowId, overrides) {
|
|
1076
|
-
const doc = this.loadWorkflowYaml(yamlPath);
|
|
1077
|
-
if (!doc) {
|
|
1078
|
-
throw new Error(`Failed to load workflow: ${yamlPath}`);
|
|
1079
|
-
}
|
|
1080
|
-
const workflowEntries = Object.entries(doc.workflows);
|
|
1081
|
-
if (workflowEntries.length === 0) {
|
|
1082
|
-
throw new Error(`No workflows found in ${yamlPath}`);
|
|
1083
|
-
}
|
|
1084
|
-
let resolvedId;
|
|
1085
|
-
let spec;
|
|
1086
|
-
if (workflowId) {
|
|
1087
|
-
if (!doc.workflows[workflowId]) {
|
|
1088
|
-
throw new Error(`Workflow "${workflowId}" not found in ${yamlPath}`);
|
|
1089
|
-
}
|
|
1090
|
-
resolvedId = workflowId;
|
|
1091
|
-
spec = doc.workflows[workflowId];
|
|
1092
|
-
}
|
|
1093
|
-
else {
|
|
1094
|
-
[resolvedId, spec] = workflowEntries[0];
|
|
1095
|
-
}
|
|
1096
|
-
const execId = (0, sdk_1.generateId)('wfr');
|
|
1097
|
-
const initialState = spec.state
|
|
1098
|
-
? JSON.parse(JSON.stringify(spec.state))
|
|
1099
|
-
: {};
|
|
1100
|
-
const sets = overrides ?? {};
|
|
1101
|
-
const state = { ...initialState, ...sets };
|
|
1102
|
-
const run = {
|
|
1103
|
-
id: execId,
|
|
1104
|
-
workflow_id: resolvedId,
|
|
1105
|
-
workflow_file: yamlPath,
|
|
1106
|
-
created_at: new Date().toISOString(),
|
|
1107
|
-
updated_at: new Date().toISOString(),
|
|
1108
|
-
current_node: '',
|
|
1109
|
-
completed_nodes: [],
|
|
1110
|
-
state,
|
|
1111
|
-
initial_state: { ...initialState, ...sets },
|
|
1112
|
-
status: 'pending',
|
|
1113
|
-
};
|
|
1114
|
-
this.saveRun(run);
|
|
1115
|
-
return run;
|
|
1116
|
-
}
|
|
1117
|
-
getState(execId) {
|
|
1118
|
-
const run = this.loadRun(execId);
|
|
1119
|
-
if (!run) {
|
|
1120
|
-
throw new Error(`Execution not found: ${execId}`);
|
|
1121
|
-
}
|
|
1122
|
-
return run.state;
|
|
1123
|
-
}
|
|
1124
|
-
setState(execId, updates) {
|
|
1125
|
-
this.atomicUpdate(execId, (run) => {
|
|
1126
|
-
Object.assign(run.state, updates);
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
moveNode(execId, nodeId) {
|
|
1130
|
-
return this.atomicUpdate(execId, (run) => {
|
|
1131
|
-
if (run.status === 'cancelled')
|
|
1132
|
-
return;
|
|
1133
|
-
if (run.current_node && !run.completed_nodes.includes(run.current_node)) {
|
|
1134
|
-
run.completed_nodes.push(run.current_node);
|
|
1135
|
-
}
|
|
1136
|
-
run.current_node = nodeId;
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
async executeNode(execId, nodeId, opts) {
|
|
1140
|
-
const snapshot = this.loadRun(execId);
|
|
1141
|
-
if (!snapshot) {
|
|
1142
|
-
throw new Error(`Execution not found: ${execId}`);
|
|
1143
|
-
}
|
|
1144
|
-
if (snapshot.status === 'cancelled') {
|
|
1145
|
-
throw (0, failure_1.createWorkflowFailureError)('CANCELLED', `Workflow run "${execId}" is already cancelled`);
|
|
1146
|
-
}
|
|
1147
|
-
const doc = this.loadWorkflowYaml(snapshot.workflow_file);
|
|
1148
|
-
if (!doc) {
|
|
1149
|
-
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Failed to load workflow file: ${snapshot.workflow_file}`);
|
|
1150
|
-
}
|
|
1151
|
-
const spec = doc.workflows[snapshot.workflow_id];
|
|
1152
|
-
if (!spec) {
|
|
1153
|
-
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Workflow "${snapshot.workflow_id}" not found in ${snapshot.workflow_file}`);
|
|
1154
|
-
}
|
|
1155
|
-
const nodeSpec = spec.nodes?.[nodeId];
|
|
1156
|
-
if (!nodeSpec) {
|
|
1157
|
-
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" not found in workflow "${snapshot.workflow_id}"`);
|
|
1158
|
-
}
|
|
1159
|
-
const prevNode = snapshot.current_node;
|
|
1160
|
-
const stateUpdates = {};
|
|
1161
|
-
const nodesToComplete = [];
|
|
1162
|
-
let currentNode = nodeId;
|
|
1163
|
-
let status;
|
|
1164
|
-
let failureMessage;
|
|
1165
|
-
let failureCode;
|
|
1166
|
-
let nodeTaskId;
|
|
1167
|
-
const trigger = opts?.trigger ?? 'manual';
|
|
1168
|
-
const startedAt = new Date();
|
|
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
|
-
};
|
|
1189
|
-
const buildAudit = (error) => {
|
|
1190
|
-
const endedAt = new Date();
|
|
1191
|
-
return {
|
|
1192
|
-
exec_id: snapshot.id,
|
|
1193
|
-
node_id: nodeId,
|
|
1194
|
-
type: nodeSpec.type,
|
|
1195
|
-
summary: summarizeNode(nodeSpec),
|
|
1196
|
-
started_at: startedAt.toISOString(),
|
|
1197
|
-
ended_at: endedAt.toISOString(),
|
|
1198
|
-
duration_ms: endedAt.getTime() - startedAt.getTime(),
|
|
1199
|
-
exit_code: nodeExitCode,
|
|
1200
|
-
...(error ? { error } : {}),
|
|
1201
|
-
trigger,
|
|
1202
|
-
};
|
|
1203
|
-
};
|
|
1204
|
-
const attempt = await this.reserveNodeAttempt(execId, nodeId);
|
|
1205
|
-
try {
|
|
1206
|
-
switch (nodeSpec.type) {
|
|
1207
|
-
case 'agent_task': {
|
|
1208
|
-
if (!nodeSpec.agent) {
|
|
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}`);
|
|
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;
|
|
1226
|
-
const input = nodeSpec.input
|
|
1227
|
-
? interpolateTemplate(nodeSpec.input, agentCtx)
|
|
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
|
-
});
|
|
1236
|
-
const finalInput = nodeSpec.output_format === 'json'
|
|
1237
|
-
? preamble + input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
1238
|
-
: preamble + input;
|
|
1239
|
-
const mode = nodeSpec.mode === 'query' ? 'q' : 'x';
|
|
1240
|
-
const prompt = `@${resolvedAgent} ${finalInput}`;
|
|
1241
|
-
if (opts?.dryRun) {
|
|
1242
|
-
console.log(`[dry-run] Would execute: crewx ${mode} "@${resolvedAgent} ..."`);
|
|
1243
|
-
break;
|
|
1244
|
-
}
|
|
1245
|
-
console.log(`Executing: @${resolvedAgent} (${mode} mode)`);
|
|
1246
|
-
const nodeTimeout = Number(process.env[mode === 'q' ? 'CREWX_TIMEOUT_QUERY' : 'CREWX_TIMEOUT_EXECUTE']) || 8 * 3600000;
|
|
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];
|
|
1255
|
-
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
1256
|
-
wfArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
1257
|
-
}
|
|
1258
|
-
const wfMetadata = {
|
|
1259
|
-
workflow: {
|
|
1260
|
-
exec_id: snapshot.id,
|
|
1261
|
-
node_id: nodeId,
|
|
1262
|
-
workflow_id: snapshot.workflow_id,
|
|
1263
|
-
},
|
|
1264
|
-
};
|
|
1265
|
-
wfArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
1266
|
-
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
1267
|
-
command: childInvocation.command,
|
|
1268
|
-
env: childInvocation.env,
|
|
1269
|
-
cwd: nodeCwd,
|
|
1270
|
-
allowShellFallback: true,
|
|
1271
|
-
packageName: 'crewx',
|
|
1272
|
-
});
|
|
1273
|
-
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, wfArgs);
|
|
1274
|
-
const taskId = await reserveAgentTaskId();
|
|
1275
|
-
const result = await runProcess(invocation.command, invocation.argv, {
|
|
1276
|
-
cwd: nodeCwd,
|
|
1277
|
-
env: (0, sdk_1.withoutCrewxRootContext)({
|
|
1278
|
-
...childInvocation.env,
|
|
1279
|
-
CREWX_TRACE_ID: taskId,
|
|
1280
|
-
}),
|
|
1281
|
-
timeout: nodeTimeout,
|
|
1282
|
-
shell: invocation.shell ?? false,
|
|
1283
|
-
windowsHide: invocation.windowsHide,
|
|
1284
|
-
cancellation: this.processCancellation(execId),
|
|
1285
|
-
});
|
|
1286
|
-
recordProcessResult(result);
|
|
1287
|
-
if (result.cancelled) {
|
|
1288
|
-
throw (0, failure_1.createWorkflowFailureError)('CANCELLED', `Agent execution cancelled for node "${nodeId}"`);
|
|
1289
|
-
}
|
|
1290
|
-
if (result.timedOut) {
|
|
1291
|
-
throw new Error(`Agent execution timed out after ${nodeTimeout}ms`);
|
|
1292
|
-
}
|
|
1293
|
-
if (result.error) {
|
|
1294
|
-
throw result.error;
|
|
1295
|
-
}
|
|
1296
|
-
const taskIdMatch = result.stderr.match(/crewx kill (tsk_\w+)/);
|
|
1297
|
-
if (taskIdMatch?.[1])
|
|
1298
|
-
nodeTaskId = taskIdMatch[1];
|
|
1299
|
-
if (result.status !== 0) {
|
|
1300
|
-
const stderr = result.stderr.trim();
|
|
1301
|
-
throw new Error(`Agent execution failed (exit ${result.status}): ${stderr}`);
|
|
1302
|
-
}
|
|
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
|
-
}
|
|
1308
|
-
if (nodeSpec.output) {
|
|
1309
|
-
if (nodeSpec.output_format === 'json') {
|
|
1310
|
-
const retryMax = nodeSpec.output_retry ?? 1;
|
|
1311
|
-
let parsed = null;
|
|
1312
|
-
let lastError = null;
|
|
1313
|
-
let retryInput = finalInput;
|
|
1314
|
-
for (let attempt = 0; attempt <= retryMax; attempt++) {
|
|
1315
|
-
let attemptOutput = output;
|
|
1316
|
-
if (attempt > 0) {
|
|
1317
|
-
retryInput = finalInput +
|
|
1318
|
-
`\n\n## ⚠️ PREVIOUS OUTPUT FAILED JSON.parse: ${lastError.message}\n` +
|
|
1319
|
-
`Return STRICT JSON ONLY. First char "{", last char "}". No prose, no fences.`;
|
|
1320
|
-
const retryPrompt = `@${resolvedAgent} ${retryInput}`;
|
|
1321
|
-
const retryArgs = [...childInvocation.args, mode, retryPrompt];
|
|
1322
|
-
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
1323
|
-
retryArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
1324
|
-
}
|
|
1325
|
-
retryArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
1326
|
-
const retryInvocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, retryArgs);
|
|
1327
|
-
const retryTaskId = await reserveAgentTaskId();
|
|
1328
|
-
const retryResult = await runProcess(retryInvocation.command, retryInvocation.argv, {
|
|
1329
|
-
cwd: nodeCwd,
|
|
1330
|
-
env: (0, sdk_1.withoutCrewxRootContext)({
|
|
1331
|
-
...childInvocation.env,
|
|
1332
|
-
CREWX_TRACE_ID: retryTaskId,
|
|
1333
|
-
}),
|
|
1334
|
-
timeout: nodeTimeout,
|
|
1335
|
-
shell: retryInvocation.shell ?? false,
|
|
1336
|
-
windowsHide: retryInvocation.windowsHide,
|
|
1337
|
-
cancellation: this.processCancellation(execId),
|
|
1338
|
-
});
|
|
1339
|
-
recordProcessResult(retryResult);
|
|
1340
|
-
if (retryResult.cancelled) {
|
|
1341
|
-
throw (0, failure_1.createWorkflowFailureError)('CANCELLED', `Agent retry cancelled for node "${nodeId}"`);
|
|
1342
|
-
}
|
|
1343
|
-
if (retryResult.timedOut)
|
|
1344
|
-
throw new Error(`Agent retry timed out after ${nodeTimeout}ms`);
|
|
1345
|
-
if (retryResult.error)
|
|
1346
|
-
throw retryResult.error;
|
|
1347
|
-
if (retryResult.status !== 0) {
|
|
1348
|
-
throw new Error(`Agent retry failed (exit ${retryResult.status}): ${retryResult.stderr.trim()}`);
|
|
1349
|
-
}
|
|
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
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
try {
|
|
1357
|
-
parsed = JSON.parse((0, output_format_1.extractJson)(attemptOutput));
|
|
1358
|
-
if (nodeSpec.output_schema) {
|
|
1359
|
-
await (0, output_format_1.validateJsonSchema)(parsed, nodeSpec.output_schema);
|
|
1360
|
-
}
|
|
1361
|
-
break;
|
|
1362
|
-
}
|
|
1363
|
-
catch (e) {
|
|
1364
|
-
lastError = e;
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
if (parsed === null) {
|
|
1368
|
-
throw (0, failure_1.createWorkflowFailureError)('OUTPUT_INVALID', `Node "${nodeId}" output_format=json parse failed after ${retryMax + 1} attempt(s): ${lastError.message}`);
|
|
1369
|
-
}
|
|
1370
|
-
stateUpdates[nodeSpec.output] = parsed;
|
|
1371
|
-
console.log(`Result stored in state.${nodeSpec.output} (JSON object)`);
|
|
1372
|
-
}
|
|
1373
|
-
else {
|
|
1374
|
-
stateUpdates[nodeSpec.output] = output;
|
|
1375
|
-
console.log(`Result stored in state.${nodeSpec.output} (${output.length} chars)`);
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
break;
|
|
1379
|
-
}
|
|
1380
|
-
case 'skill_task': {
|
|
1381
|
-
if (!nodeSpec.skill) {
|
|
1382
|
-
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" is skill_task but has no skill defined`);
|
|
1383
|
-
}
|
|
1384
|
-
if (!shell_security_1.SKILL_NAME_RE.test(nodeSpec.skill)) {
|
|
1385
|
-
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}" has invalid skill name "${nodeSpec.skill}" (must match ^[a-z0-9][a-z0-9-]*$)`);
|
|
1386
|
-
}
|
|
1387
|
-
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
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;
|
|
1392
|
-
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
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;
|
|
1401
|
-
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
1402
|
-
if (opts?.dryRun) {
|
|
1403
|
-
console.log(`[dry-run] Would execute: crewx skill ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
1404
|
-
break;
|
|
1405
|
-
}
|
|
1406
|
-
console.log(`Executing skill: ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
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];
|
|
1415
|
-
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
1416
|
-
command: childInvocation.command,
|
|
1417
|
-
env: childInvocation.env,
|
|
1418
|
-
cwd: cwdCheck.resolved,
|
|
1419
|
-
allowShellFallback: true,
|
|
1420
|
-
packageName: 'crewx',
|
|
1421
|
-
});
|
|
1422
|
-
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, skillArgv);
|
|
1423
|
-
const result = await runProcess(invocation.command, invocation.argv, {
|
|
1424
|
-
cwd: cwdCheck.resolved,
|
|
1425
|
-
env: childInvocation.env,
|
|
1426
|
-
timeout,
|
|
1427
|
-
shell: invocation.shell ?? false,
|
|
1428
|
-
windowsHide: invocation.windowsHide,
|
|
1429
|
-
stdin: skillStdin,
|
|
1430
|
-
cancellation: this.processCancellation(execId),
|
|
1431
|
-
});
|
|
1432
|
-
recordProcessResult(result);
|
|
1433
|
-
if (result.cancelled) {
|
|
1434
|
-
throw (0, failure_1.createWorkflowFailureError)('CANCELLED', `skill_task "${nodeId}" was cancelled`);
|
|
1435
|
-
}
|
|
1436
|
-
if (result.timedOut)
|
|
1437
|
-
throw new Error(`skill_task "${nodeId}" timed out after ${timeout}ms`);
|
|
1438
|
-
if (result.error)
|
|
1439
|
-
throw result.error;
|
|
1440
|
-
if (result.status !== 0) {
|
|
1441
|
-
throw new Error(`skill_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
1442
|
-
}
|
|
1443
|
-
await storeNodeOutput(nodeSpec, nodeId, result.stdout, stateUpdates);
|
|
1444
|
-
if (nodeSpec.output) {
|
|
1445
|
-
console.log(`Result stored in state.${nodeSpec.output}`);
|
|
1446
|
-
}
|
|
1447
|
-
break;
|
|
1448
|
-
}
|
|
1449
|
-
case 'shell_task': {
|
|
1450
|
-
const shellEnvEnabled = process.env.CREWX_WORKFLOW_SHELL === '1';
|
|
1451
|
-
const shellMetaAllowed = spec.metadata?.shell_task_allowed === true;
|
|
1452
|
-
if (!shellEnvEnabled || !shellMetaAllowed) {
|
|
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.');
|
|
1454
|
-
}
|
|
1455
|
-
const cmdCheck = (0, shell_security_1.validateShellCommand)(nodeSpec.command);
|
|
1456
|
-
if (!cmdCheck.ok) {
|
|
1457
|
-
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cmdCheck.error}`);
|
|
1458
|
-
}
|
|
1459
|
-
const envCheck = (0, shell_security_1.validateEnvKeys)(nodeSpec.env);
|
|
1460
|
-
if (!envCheck.ok) {
|
|
1461
|
-
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${envCheck.error}`);
|
|
1462
|
-
}
|
|
1463
|
-
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
1464
|
-
const command = nodeSpec.command.map((c) => interpolateTemplate(c, ctx));
|
|
1465
|
-
const postCheck = (0, shell_security_1.validateShellCommand)(command);
|
|
1466
|
-
if (!postCheck.ok) {
|
|
1467
|
-
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${postCheck.error}`);
|
|
1468
|
-
}
|
|
1469
|
-
const interpolatedEnv = nodeSpec.env
|
|
1470
|
-
? Object.fromEntries(Object.entries(nodeSpec.env).map(([k, v]) => [k, interpolateTemplate(String(v), ctx)]))
|
|
1471
|
-
: undefined;
|
|
1472
|
-
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
1473
|
-
if (!cwdCheck.ok) {
|
|
1474
|
-
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Node "${nodeId}": ${cwdCheck.error}`);
|
|
1475
|
-
}
|
|
1476
|
-
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
1477
|
-
if (opts?.dryRun) {
|
|
1478
|
-
console.log(`[dry-run] Would execute: ${command.join(' ')}`);
|
|
1479
|
-
break;
|
|
1480
|
-
}
|
|
1481
|
-
console.log(`Executing shell_task: ${command[0]} (${command.length - 1} args)`);
|
|
1482
|
-
const childEnv = (0, shell_security_1.buildShellEnv)(interpolatedEnv, process.env);
|
|
1483
|
-
childEnv.CREWX_WORKFLOW_EXEC_ID = snapshot.id;
|
|
1484
|
-
childEnv.CREWX_WORKFLOW_NODE_ID = nodeId;
|
|
1485
|
-
childEnv.CREWX_WORKFLOW_ID = snapshot.workflow_id;
|
|
1486
|
-
const shellStdin = nodeSpec.stdin !== undefined
|
|
1487
|
-
? interpolateTemplate(String(nodeSpec.stdin), ctx)
|
|
1488
|
-
: undefined;
|
|
1489
|
-
const result = await runProcess(command[0], command.slice(1), {
|
|
1490
|
-
cwd: cwdCheck.resolved,
|
|
1491
|
-
env: childEnv,
|
|
1492
|
-
timeout,
|
|
1493
|
-
shell: false,
|
|
1494
|
-
stdin: shellStdin,
|
|
1495
|
-
cancellation: this.processCancellation(execId),
|
|
1496
|
-
});
|
|
1497
|
-
recordProcessResult(result);
|
|
1498
|
-
if (result.cancelled) {
|
|
1499
|
-
throw (0, failure_1.createWorkflowFailureError)('CANCELLED', `shell_task "${nodeId}" was cancelled`);
|
|
1500
|
-
}
|
|
1501
|
-
if (result.timedOut)
|
|
1502
|
-
throw new Error(`shell_task "${nodeId}" timed out after ${timeout}ms`);
|
|
1503
|
-
if (result.error)
|
|
1504
|
-
throw result.error;
|
|
1505
|
-
if (result.status !== 0) {
|
|
1506
|
-
throw new Error(`shell_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
1507
|
-
}
|
|
1508
|
-
await storeNodeOutput(nodeSpec, nodeId, result.stdout, stateUpdates);
|
|
1509
|
-
if (nodeSpec.output) {
|
|
1510
|
-
console.log(`Result stored in state.${nodeSpec.output}`);
|
|
1511
|
-
}
|
|
1512
|
-
break;
|
|
1513
|
-
}
|
|
1514
|
-
case 'branch': {
|
|
1515
|
-
if (!nodeSpec.condition) {
|
|
1516
|
-
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Branch node "${nodeId}" has no condition`);
|
|
1517
|
-
}
|
|
1518
|
-
const condResult = String(safeEvaluateBranchCondition(String(nodeSpec.condition), snapshot.state));
|
|
1519
|
-
const branches = nodeSpec.branches;
|
|
1520
|
-
const targetNode = branches?.[condResult] ?? nodeSpec.default;
|
|
1521
|
-
if (!targetNode) {
|
|
1522
|
-
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Branch "${nodeId}": no matching branch for "${condResult}" and no default`);
|
|
1523
|
-
}
|
|
1524
|
-
console.log(`Branch "${nodeId}": condition="${nodeSpec.condition}" → ${condResult} → ${targetNode}`);
|
|
1525
|
-
nodesToComplete.push(nodeId);
|
|
1526
|
-
currentNode = targetNode;
|
|
1527
|
-
break;
|
|
1528
|
-
}
|
|
1529
|
-
case 'parallel': {
|
|
1530
|
-
const branches = nodeSpec.branches;
|
|
1531
|
-
if (branches && Array.isArray(branches)) {
|
|
1532
|
-
console.log(`Parallel "${nodeId}": branches = [${branches.join(', ')}]`);
|
|
1533
|
-
console.log(` Enter each branch sequentially with: workflow run node <exec-id> <branch-node>`);
|
|
1534
|
-
}
|
|
1535
|
-
break;
|
|
1536
|
-
}
|
|
1537
|
-
case 'join': {
|
|
1538
|
-
console.log(`Join "${nodeId}": completed_nodes = [${snapshot.completed_nodes.join(', ')}]`);
|
|
1539
|
-
break;
|
|
1540
|
-
}
|
|
1541
|
-
case 'end': {
|
|
1542
|
-
status = 'completed';
|
|
1543
|
-
console.log(`End "${nodeId}": workflow execution completed`);
|
|
1544
|
-
break;
|
|
1545
|
-
}
|
|
1546
|
-
case 'error': {
|
|
1547
|
-
status = 'failed';
|
|
1548
|
-
failureCode = 'UNKNOWN';
|
|
1549
|
-
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
1550
|
-
failureMessage = nodeSpec.message
|
|
1551
|
-
? interpolateTemplate(nodeSpec.message, ctx)
|
|
1552
|
-
: `Workflow failed at error node "${nodeId}"`;
|
|
1553
|
-
console.log(`Error "${nodeId}": workflow execution failed — ${failureMessage}`);
|
|
1554
|
-
break;
|
|
1555
|
-
}
|
|
1556
|
-
case 'expression': {
|
|
1557
|
-
if (!nodeSpec.set || typeof nodeSpec.set !== 'object') {
|
|
1558
|
-
throw (0, failure_1.createWorkflowFailureError)('CONFIG_INVALID', `Expression node "${nodeId}" requires "set" field`);
|
|
1559
|
-
}
|
|
1560
|
-
for (const [key, expr] of Object.entries(nodeSpec.set)) {
|
|
1561
|
-
const result = safeEvaluate(String(expr), snapshot.state);
|
|
1562
|
-
stateUpdates[key] = String(result);
|
|
1563
|
-
console.log(` ${key} = ${result}`);
|
|
1564
|
-
}
|
|
1565
|
-
console.log(`Expression "${nodeId}": set ${Object.keys(nodeSpec.set).join(', ')}`);
|
|
1566
|
-
if (nodeSpec.next) {
|
|
1567
|
-
nodesToComplete.push(nodeId);
|
|
1568
|
-
currentNode = nodeSpec.next;
|
|
1569
|
-
}
|
|
1570
|
-
break;
|
|
1571
|
-
}
|
|
1572
|
-
case 'approval': {
|
|
1573
|
-
console.log(`Approval "${nodeId}": manual intervention required (not auto-executed)`);
|
|
1574
|
-
break;
|
|
1575
|
-
}
|
|
1576
|
-
default:
|
|
1577
|
-
console.log(`Unknown node type "${nodeSpec.type}" for node "${nodeId}"`);
|
|
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
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
catch (e) {
|
|
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);
|
|
1618
|
-
await this.atomicUpdateAsync(execId, (run) => {
|
|
1619
|
-
if (run.status === 'cancelled')
|
|
1620
|
-
return;
|
|
1621
|
-
Object.assign(run.state, stateUpdates);
|
|
1622
|
-
delete run.manual_resume;
|
|
1623
|
-
run.current_node = nodeId;
|
|
1624
|
-
assignRunFailure(run, failure, failWhenReason ?? errMessage);
|
|
1625
|
-
run.audit = run.audit ?? [];
|
|
1626
|
-
run.audit.push(auditEntry);
|
|
1627
|
-
});
|
|
1628
|
-
throw e;
|
|
1629
|
-
}
|
|
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);
|
|
1641
|
-
return this.atomicUpdateAsync(execId, (run) => {
|
|
1642
|
-
if (run.status === 'cancelled')
|
|
1643
|
-
return;
|
|
1644
|
-
Object.assign(run.state, stateUpdates);
|
|
1645
|
-
delete run.manual_resume;
|
|
1646
|
-
for (const node of nodesToComplete) {
|
|
1647
|
-
if (!run.completed_nodes.includes(node)) {
|
|
1648
|
-
run.completed_nodes.push(node);
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
if (prevNode && prevNode !== nodeId && !run.completed_nodes.includes(prevNode)) {
|
|
1652
|
-
run.completed_nodes.push(prevNode);
|
|
1653
|
-
}
|
|
1654
|
-
run.current_node = currentNode;
|
|
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
|
-
}
|
|
1669
|
-
if (nodeTaskId) {
|
|
1670
|
-
run.tasks = run.tasks ?? {};
|
|
1671
|
-
run.tasks[nodeId] = nodeTaskId;
|
|
1672
|
-
}
|
|
1673
|
-
run.audit = run.audit ?? [];
|
|
1674
|
-
run.audit.push(successAudit);
|
|
1675
|
-
});
|
|
1676
|
-
}
|
|
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(),
|
|
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;
|
|
1784
|
-
}
|
|
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;
|
|
1795
|
-
while (true) {
|
|
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;
|
|
1871
|
-
}
|
|
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}`);
|
|
1875
|
-
}
|
|
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}`);
|
|
1879
|
-
}
|
|
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
|
-
};
|
|
1888
|
-
}
|
|
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;
|
|
1972
|
-
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
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
|
-
}
|
|
1991
|
-
});
|
|
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
|
-
}
|
|
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
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
finally {
|
|
2021
|
-
if (runnerToken) {
|
|
2022
|
-
try {
|
|
2023
|
-
await this.releaseAutoRunLease(execId, runnerToken);
|
|
2024
|
-
}
|
|
2025
|
-
catch {
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
inFlightAutoRuns.delete(leaseKey);
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
cancelRun(execId, reason = 'Cancelled by user') {
|
|
2032
|
-
const cancelled = this.atomicUpdate(execId, (run) => {
|
|
2033
|
-
if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {
|
|
2034
|
-
return;
|
|
2035
|
-
}
|
|
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);
|
|
2047
|
-
run.status = 'cancelled';
|
|
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;
|
|
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
|
-
}
|
|
2118
|
-
run.audit.push({
|
|
2119
|
-
exec_id: run.id,
|
|
2120
|
-
node_id: failedNode,
|
|
2121
|
-
type: 'retry',
|
|
2122
|
-
summary: `manual retry #${run.retry_count}`,
|
|
2123
|
-
started_at: now,
|
|
2124
|
-
ended_at: now,
|
|
2125
|
-
duration_ms: 0,
|
|
2126
|
-
exit_code: null,
|
|
2127
|
-
trigger: 'manual',
|
|
2128
|
-
});
|
|
2129
|
-
});
|
|
2130
|
-
}
|
|
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
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
listRuns(opts) {
|
|
2144
|
-
this.ensureRunsDir();
|
|
2145
|
-
const files = fs.readdirSync(this.resolvedDir).filter(f => f.endsWith('.json'));
|
|
2146
|
-
const runs = [];
|
|
2147
|
-
for (const file of files) {
|
|
2148
|
-
try {
|
|
2149
|
-
const execId = file.slice(0, -'.json'.length);
|
|
2150
|
-
const run = this.loadRun(execId);
|
|
2151
|
-
if (!run)
|
|
2152
|
-
continue;
|
|
2153
|
-
if (opts?.workflowId && run.workflow_id !== opts.workflowId)
|
|
2154
|
-
continue;
|
|
2155
|
-
runs.push(run);
|
|
2156
|
-
}
|
|
2157
|
-
catch {
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
runs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
2161
|
-
return runs;
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
|
-
exports.RunManager = RunManager;
|
|
2165
|
-
//# sourceMappingURL=run-manager.js.map
|
|
1
|
+
"use strict";var xe=exports&&exports.__createBinding||(Object.create?(function(a,e,t,o){if(o===void 0)o=t;var r=Object.getOwnPropertyDescriptor(e,t);if(!r||("get"in r?!e.__esModule:r.writable||r.configurable)){r={enumerable:true,get:function(){return e[t]}}}Object.defineProperty(a,o,r)}):(function(a,e,t,o){if(o===void 0)o=t;a[o]=e[t]}));var Ie=exports&&exports.__setModuleDefault||(Object.create?(function(a,e){Object.defineProperty(a,"default",{enumerable:true,value:e})}):function(a,e){a["default"]=e});var ie=exports&&exports.__importStar||(function(){var a=function(e){a=Object.getOwnPropertyNames||function(t){var o=[];for(var r in t)if(Object.prototype.hasOwnProperty.call(t,r))o[o.length]=r;return o};return a(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null){for(var o=a(e),r=0;r<o.length;r++)if(o[r]!=="default")xe(t,e,o[r])}Ie(t,e);return t}})();Object.defineProperty(exports,"__esModule",{value:true});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;exports.parseSetArgs=He;exports.readStdin=Ze;exports.interpolateTemplate=b;exports.safeEvaluate=Ee;exports.safeEvaluateBranchCondition=de;exports.validateSafeBranchCondition=st;exports.normalizeRunStatus=ee;exports.isRetryableFailure=Ue;const R=ie(require("fs"));const W=ie(require("path"));const Me=ie(require("js-yaml"));const Ge=ie(require("child_process"));const N=require("@crewx/sdk");const d=require("./failure");const le=require("./file-lock");const K=require("./utils/output-format");const U=require("./utils/shell-security");const ce=require("./retry-policy");var Ke=require("./file-lock");Object.defineProperty(exports,"FileLock",{enumerable:true,get:function(){return Ke.FileLock}});exports.SKILL_TASK_DEFAULT_TIMEOUT=6e5;exports.WORKFLOW_CANCEL_GRACE_MS=2e3;const Ve=100;exports.WORKFLOW_RUN_INTERRUPTED_ERROR="Workflow run interrupted: runner process exited before completion";exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS=10*6e4;const qe=1e3;function ze(a){return new Promise(e=>{setTimeout(e,a)})}const Ae=new Set(["agent_task","skill_task","shell_task","branch","expression","join","end","error"]);const Xe=new Set(["branch","expression","end","error"]);function Se(a,e){const t=(0,N.resolveCrewxExecutable)({env:a,cwd:e});if(!t.ok){throw(0,d.createWorkflowFailureError)("CLI_UNRESOLVABLE",(0,N.formatCrewxExecutableFailure)(t))}return(0,N.createCrewxChildInvocation)(t,{baseEnv:a})}async function ye(a,e="SIGKILL"){if(!a.pid)return;try{const t=await(0,N.terminateProcessTree)(a.pid,{signal:e});if(t.status==="failed"||t.status==="not_found"){try{a.kill(e)}catch{}}}catch{try{a.kill(e)}catch{}}}async function ue(a,e,t){return new Promise(o=>{const r=()=>{try{return t.cancellation?.isCancelled()===true}catch{return false}};if(r()){o({status:null,stdout:"",stderr:"",timedOut:false,cancelled:true,signal:null,errorOrigin:null});return}let s;try{s=Ge.spawn(a,e,{cwd:t.cwd,env:t.env,stdio:[t.stdin!==void 0?"pipe":"ignore","pipe","pipe"],shell:t.shell??false,detached:process.platform!=="win32",windowsHide:t.windowsHide??true})}catch(g){o({status:null,stdout:"",stderr:"",timedOut:false,signal:null,errorOrigin:"spawn",error:g});return}let i="";let n="";let c=false;let p=false;let l=false;let f=false;let m;let _;let S;let C;let w;let k;const F=new N.ConsoleDecoder;const H=new N.ConsoleDecoder;let X=Promise.resolve();let q=Promise.resolve();const Z=()=>{if(S)clearTimeout(S);if(C)clearTimeout(C);if(w)clearInterval(w);if(t.cancellation&&k&&fe.get(t.cancellation.key)===k){fe.delete(t.cancellation.key)}};const I=g=>{if(c)return;c=true;Z();o(f?{...g,cancelled:true,timedOut:false}:g)};const B=()=>{if(c||f)return;f=true;void ye(s,"SIGTERM");C=setTimeout(()=>{if(!c)void ye(s,"SIGKILL")},exports.WORKFLOW_CANCEL_GRACE_MS);if(typeof C.unref==="function"){C.unref()}};if(t.cancellation){k={cancel:B};fe.set(t.cancellation.key,k);w=setInterval(()=>{if(r())B()},Ve);if(typeof w.unref==="function")w.unref()}S=setTimeout(()=>{void ye(s);Q(()=>({status:null,stdout:i,stderr:n,timedOut:true,signal:null,errorOrigin:_?"stdin":m?"post_spawn":null,..._||m?{error:_??m}:{}}))},t.timeout);if(typeof S.unref==="function")S.unref();s.stdout?.on("data",g=>{X=X.then(async()=>{i+=await F.write(g)})});s.stderr?.on("data",g=>{q=q.then(async()=>{n+=await H.write(g)})});s.once("spawn",()=>{l=true});s.on("error",g=>{if(!l){I({status:null,stdout:i,stderr:n,timedOut:false,signal:null,errorOrigin:"spawn",error:g});return}m=m??g});if(t.stdin!==void 0){s.stdin?.on("error",g=>{_=_??g});try{s.stdin?.end(t.stdin)}catch(g){_=_??g}}const Q=g=>{if(c||p)return;p=true;void(async()=>{await Promise.all([X,q]);const[M,pe]=await Promise.all([F.end(),H.end()]);i+=M;n+=pe;I(g())})().catch(()=>I(g()))};s.on("close",(g,M)=>Q(()=>({status:g,stdout:i,stderr:n,timedOut:false,signal:M,errorOrigin:_?"stdin":m?"post_spawn":null,..._||m?{error:_??m}:{}})));if(r())B()})}const Be=/^[A-Za-z0-9_.-]{1,128}$/;function be(a){if(a===void 0)return void 0;const e=Be.exec(a);return e?.[0]===a?a:void 0}function Je(a){switch(a.type){case"agent_task":{const e=be(a.agent);return e?`@${e} (${a.mode==="query"?"q":"x"})`:`agent_task (${a.mode==="query"?"q":"x"})`}case"skill_task":{const e=be(a.skill);return e?`skill ${e}`:"skill_task"}case"shell_task":return"shell_task";case"branch":return"branch";case"expression":return"expression";default:return"workflow node"}}function J(a){if(a instanceof Error)return a.message;return typeof a==="string"?a:"Node execution failed"}function Ye(a){return{status:a.status,timedOut:a.timedOut,signal:a.signal,errorOrigin:a.errorOrigin}}function Y(a,e,t,o,r=new Date().toISOString()){a.audit=a.audit??[];a.audit.push({exec_id:a.id,node_id:e.failedNode,type:t,summary:e.causeMessage,started_at:r,ended_at:r,duration_ms:0,exit_code:e.exitCode,error:e.causeMessage,trigger:o})}function V(a,e,t){a.status="failed";a.error=(0,d.sanitizeLocalRunError)(t,e.causeMessage);a.last_failure=e}async function Le(a,e,t,o){if(!a.output)return;const r=t.trim();if(a.output_format==="json"){let s;try{s=JSON.parse((0,K.extractJson)(r));if(a.output_schema){await(0,K.validateJsonSchema)(s,a.output_schema)}}catch(i){throw(0,d.createWorkflowFailureError)("OUTPUT_INVALID",`Node "${e}" output_format=json parse or validation failed: ${J(i)}`)}o[a.output]=s}else{o[a.output]=r}}function We(a){try{return JSON.parse(a)}catch{return a}}function He(a){const e={};for(let t=0;t<a.length;t++){const o=a[t];if(o==="--set"&&t+1<a.length){const r=a[t+1];const s=r.indexOf("=");if(s!==-1){const i=r.substring(0,s);const n=r.substring(s+1);e[i]=We(n)}t++}else if(o.startsWith("--set=")){const r=o.substring("--set=".length);const s=r.indexOf("=");if(s!==-1){const i=r.substring(0,s);const n=r.substring(s+1);e[i]=We(n)}}}return e}async function Ze(){const a=[];for await(const e of process.stdin){a.push(e)}return Buffer.concat(a).toString("utf-8").trimEnd()}function b(a,e){return a.replace(/\{\{([\w.]+)\}\}/g,(t,o)=>{const r=o.split(".");let s=e[r[0]];for(let i=1;i<r.length&&s!=null;i++){s=s[r[i]]}if(s===null||s===void 0)return"";if(typeof s==="object")return JSON.stringify(s,null,2);return String(s)})}const Qe=new Set(["require","import","process","global","globalThis","Function","eval","__proto__","constructor","prototype"]);const et=new Set(["__proto__","constructor","prototype"]);function tt(a){const e=[];let t=0;while(t<a.length){const o=a[t];if(/\s/.test(o)){t++;continue}if(/\d/.test(o)){let s="";while(t<a.length&&/[\d.]/.test(a[t])){s+=a[t];t++}e.push({type:"NUMBER",value:Number(s)});continue}if(o==="'"||o==='"'){const s=o;t++;let i="";while(t<a.length&&a[t]!==s){if(a[t]==="\\"){t++;i+=a[t]??""}else{i+=a[t]}t++}if(t>=a.length)throw new Error("Unterminated string literal");t++;e.push({type:"STRING",value:i});continue}if(/[a-zA-Z_]/.test(o)){let s="";while(t<a.length&&/\w/.test(a[t])){s+=a[t];t++}if(s==="true"||s==="false"){e.push({type:"BOOLEAN",value:s==="true"})}else{if(Qe.has(s)){throw new Error(`Forbidden identifier in expression: "${s}"`)}e.push({type:"IDENT",value:s})}continue}if(t+1<a.length){const s=a.substring(t,t+2);const i={">=":"GTE","<=":"LTE","==":"EQ","!=":"NEQ","&&":"AND","||":"OR"};if(i[s]){e.push({type:i[s],value:s});t+=2;continue}}const r={"+":"PLUS","-":"MINUS","*":"STAR","/":"SLASH","%":"PERCENT",">":"GT","<":"LT","!":"NOT","?":"QUESTION",":":"COLON","(":"LPAREN",")":"RPAREN"};if(r[o]){e.push({type:r[o],value:o});t++;continue}if(o==="."||o==="["||o==="]"){throw new Error(`Forbidden character in expression: "${o}" (property access not allowed)`)}throw new Error(`Unexpected character in expression: "${o}"`)}e.push({type:"EOF",value:""});return e}class rt{constructor(e,t){this.tokens=e;this.state=t;this.pos=0}peek(){return this.tokens[this.pos]}advance(){return this.tokens[this.pos++]}expect(e){const t=this.advance();if(t.type!==e)throw new Error(`Expected ${e}, got ${t.type}`);return t}parse(){const e=this.parseTernary();if(this.peek().type!=="EOF"){throw new Error(`Unexpected token after expression: ${this.peek().type}`)}return e}parseTernary(){const e=this.parseOr();if(this.peek().type==="QUESTION"){this.advance();const t=this.parseTernary();this.expect("COLON");const o=this.parseTernary();return e?t:o}return e}parseOr(){let e=this.parseAnd();while(this.peek().type==="OR"){this.advance();const t=this.parseAnd();e=e||t}return e}parseAnd(){let e=this.parseEquality();while(this.peek().type==="AND"){this.advance();const t=this.parseEquality();e=e&&t}return e}parseEquality(){let e=this.parseComparison();while(this.peek().type==="EQ"||this.peek().type==="NEQ"){const t=this.advance().type;const o=this.parseComparison();e=t==="EQ"?e==o:e!=o}return e}parseComparison(){let e=this.parseAdditive();while(["GT","LT","GTE","LTE"].includes(this.peek().type)){const t=this.advance().type;const o=this.parseAdditive();switch(t){case"GT":e=e>o;break;case"LT":e=e<o;break;case"GTE":e=e>=o;break;case"LTE":e=e<=o;break}}return e}parseAdditive(){let e=this.parseMultiplicative();while(this.peek().type==="PLUS"||this.peek().type==="MINUS"){const t=this.advance().type;const o=this.parseMultiplicative();if(t==="PLUS"){if(typeof e==="string"||typeof o==="string"){e=String(e)+String(o)}else{e=e+o}}else{e=e-o}}return e}parseMultiplicative(){let e=this.parseUnary();while(["STAR","SLASH","PERCENT"].includes(this.peek().type)){const t=this.advance().type;const o=this.parseUnary();switch(t){case"STAR":e=e*o;break;case"SLASH":e=e/o;break;case"PERCENT":e=e%o;break}}return e}parseUnary(){if(this.peek().type==="NOT"){this.advance();return!this.parseUnary()}if(this.peek().type==="MINUS"){this.advance();return-this.parseUnary()}return this.parsePrimary()}parsePrimary(){const e=this.peek();switch(e.type){case"NUMBER":this.advance();return e.value;case"STRING":this.advance();return e.value;case"BOOLEAN":this.advance();return e.value;case"IDENT":{this.advance();const t=e.value;const o=this.state[t];if(o===void 0||o===null)return 0;if(typeof o==="boolean"||typeof o==="number")return o;const r=String(o);if(r==="")return"";const s=Number(r);return isNaN(s)?r:s}case"LPAREN":{this.advance();const t=this.parseTernary();this.expect("RPAREN");return t}default:throw new Error(`Unexpected token: ${e.type}`)}}}function Ee(a,e){const t=tt(a);const o=new rt(t,e);return o.parse()}function Fe(a,e){const t=e.split(".");let o=a;for(const r of t){if(et.has(r)){throw new Error(`Forbidden state path segment in expression: "${r}"`)}if(!r||o===null||o===void 0||typeof o!=="object"){return void 0}o=o[r]}return o}function ot(a){return a.replace(/\\'/g,"'").replace(/\\"/g,'"').replace(/\\\\/g,"\\")}function nt(a,e){const t={};let o=0;const r=n=>{const c=`__state_${o++}`;t[c]=n;return c};let s=a.replace(/!==/g,"!=").replace(/===/g,"==");const i=n=>{s=s.replace(n,(c,p,l)=>{const f=Fe(e,p);return r(String(f??"").includes(ot(l)))})};i(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\.includes\(\s*'((?:\\.|[^'\\])*)'\s*\)/g);i(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\.includes\(\s*"((?:\\.|[^"\\])*)"\s*\)/g);s=s.replace(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)/g,(n,c)=>r(Fe(e,c)));s=s.replace(/\bNumber\(\s*([A-Za-z_]\w*)\s*\)/g,"$1");s=s.replace(/\bparseInt\(\s*([A-Za-z_]\w*(?:\s*\|\|\s*(?:'[^']*'|"[^"]*"))?)\s*\)/g,"($1)");return{expr:s,scope:t}}function de(a,e){const t=nt(a,e);return Ee(t.expr,{...e,...t.scope})}function st(a){de(a,{})}function at(a){const e=a.state["__engine"];if(!e||typeof e!=="object")return{visitCounts:{},iterations:0};const t=e;const o=t["visitCounts"];const r={};if(o&&typeof o==="object"){for(const[i,n]of Object.entries(o)){if(typeof n==="number"&&Number.isFinite(n))r[i]=n}}const s=typeof t["iterations"]==="number"&&Number.isFinite(t["iterations"])?t["iterations"]:0;return{visitCounts:r,iterations:s}}const ke=new Set;const fe=new Map;function Te(a,e){return`${a}::${e}`}function it(a,e){fe.get(Te(a,e))?.cancel()}function De(a){if(!Number.isInteger(a)||a<=0)return false;if(a===process.pid)return true;try{process.kill(a,0);return true}catch(e){return e.code==="EPERM"}}function Re(a){return a==="completed"||a==="failed"||a==="cancelled"}function lt(a,e){if(a.status!==void 0||a.auto_run)return false;const t=a.audit?.at(-1);if(t?.type!=="retry")return false;const o=Date.parse(a.updated_at);return Number.isFinite(o)&&e-o<exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS}function ee(a,e=Date.now()){if(a.status!==void 0)return a;let t;if(a.current_node===""){t="pending"}else if(a.auto_run){t="running"}else if(lt(a,e)){t="retrying"}else{t="pending"}return{...a,status:t}}function Ue(a,e){if(a.status!=="failed")return false;if(!a.current_node)return false;const t=e.nodes?.[a.current_node];if(!t)return false;return t.type!=="error"}class ct{constructor(e=".crewx/workflow-runs",t,o){this.runsDir=e;this.sleep=o?.sleep??ze;this.onRunPersisted=o?.onRunPersisted;const r=t||process.env.CREWX_WORKSPACE||process.cwd();this.resolvedDir=W.resolve(r,this.runsDir);this.projectRoot=W.resolve(r);const s=R.existsSync(W.join(this.projectRoot,"crewx.yaml"))||R.existsSync(W.join(this.projectRoot,"crewx.yml"));if(!s){console.warn(`[RunManager] projectRoot "${this.projectRoot}" has no crewx.yaml/crewx.yml \u2014 agent_task nodes may fail to resolve agents.`)}}ensureRunsDir(){if(!R.existsSync(this.resolvedDir)){R.mkdirSync(this.resolvedDir,{recursive:true})}}validateExecId(e){if(/[/\\]|\.\./.test(e)){throw new Error(`Invalid execution ID: "${e}"`)}}isRunCancelled(e){try{return this.readRunRaw(e)?.status==="cancelled"}catch{return false}}processCancellation(e){return{key:Te(this.resolvedDir,e),isCancelled:()=>this.isRunCancelled(e)}}readRunRaw(e){const t=W.join(this.resolvedDir,`${e}.json`);if(!R.existsSync(t))return null;try{return JSON.parse(R.readFileSync(t,"utf-8"))}catch(o){throw new Error(`Failed to parse execution file ${t}: ${o.message}`)}}readRun(e){this.validateExecId(e);const t=this.readRunRaw(e);return t?ee(t):null}isLegacyStatuslessStale(e){if(e.status!==void 0||e.auto_run||e.manual_resume||!e.current_node||!e.audit?.length){return false}const t=Date.parse(e.updated_at);if(!Number.isFinite(t)||Date.now()-t<exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS){return false}const o=this.loadWorkflowYaml(e.workflow_file);const r=o?.workflows[e.workflow_id];const s=r?.nodes?.[e.current_node];return Boolean(s&&Ae.has(s.type))}withRunLock(e,t){this.validateExecId(e);const o=W.join(this.resolvedDir,`${e}.json`);if(!R.existsSync(o)){throw new Error(`Execution not found: ${e}`)}const r=new le.FileLock(o);r.acquire();try{const s=this.readRunRaw(e);if(!s)throw new Error(`Execution not found: ${e}`);return t(ee(s))}finally{r.release()}}hasStaleRunner(e){if(e.auto_run)return!De(e.auto_run.pid);return this.isLegacyStatuslessStale(e)}recoverStaleRunner(e){const t=W.join(this.resolvedDir,`${e}.json`);const o=new le.FileLock(t);o.acquire();try{const r=this.readRunRaw(e);if(!r||!this.hasStaleRunner(r))return r?ee(r):null;if(Re(r.status)){delete r.auto_run;delete r.manual_resume;this.saveRun(r);return r}const s=r.auto_run;const i=s?.node_id||r.current_node;const n=new Date().toISOString();const c=(0,d.buildNodeFailure)({failedNode:i,attempt:(0,d.readNodeAttempt)(r,i),causeCode:"INTERRUPTED",exitCode:null,signal:null,errorOrigin:null});V(r,c,exports.WORKFLOW_RUN_INTERRUPTED_ERROR);delete r.auto_run;delete r.manual_resume;Y(r,c,"interrupted","auto",n);this.saveRun(r);return r}finally{o.release()}}loadRun(e){this.validateExecId(e);const t=this.readRunRaw(e);if(!t||!this.hasStaleRunner(t))return t?ee(t):null;return this.recoverStaleRunner(e)}saveRun(e){this.ensureRunsDir();e.updated_at=new Date().toISOString();const t=W.join(this.resolvedDir,`${e.id}.json`);let o;try{o=R.statSync(t).mode&4095}catch(c){if(c.code!=="ENOENT")throw c}const r=W.join(this.resolvedDir,`.${e.id}.tmp-${process.pid}-${(0,N.generateId)("wfr")}`);const s=JSON.stringify(e,null,2)+"\n";let i;let n=false;try{i=R.openSync(r,"wx");n=true;R.writeFileSync(i,s,"utf-8");if(o!==void 0)R.fchmodSync(i,o);R.closeSync(i);i=void 0;R.renameSync(r,t);n=false}finally{if(i!==void 0){try{R.closeSync(i)}catch{}}if(n){try{R.unlinkSync(r)}catch{}}}if(!this.onRunPersisted)return;try{this.onRunPersisted(e)}catch(c){console.warn(`[RunManager] onRunPersisted failed for ${e.id}: ${J(c)}`)}}atomicUpdate(e,t){this.validateExecId(e);this.ensureRunsDir();const o=W.join(this.resolvedDir,`${e}.json`);const r=new le.FileLock(o);r.acquire();try{const s=this.readRun(e);if(!s)throw new Error(`Execution not found: ${e}`);t(s);this.saveRun(s);return s}finally{r.release()}}async atomicUpdateAsync(e,t){this.validateExecId(e);this.ensureRunsDir();const o=W.join(this.resolvedDir,`${e}.json`);const r=new le.FileLock(o);await r.acquireAsync();try{const s=this.readRun(e);if(!s)throw new Error(`Execution not found: ${e}`);t(s);this.saveRun(s);return s}finally{r.release()}}async reserveNodeAttempt(e,t){let o=1;await this.atomicUpdateAsync(e,r=>{if(r.status==="cancelled"){o=(0,d.readNodeAttempt)(r,t);return}const s=r.node_attempts?.[t];const i=typeof s==="number"&&Number.isInteger(s)&&s>0?s:0;o=i+1;r.node_attempts=r.node_attempts??{};r.node_attempts[t]=o});return o}clearNodeAttempt(e,t){if(!e.node_attempts)return;delete e.node_attempts[t];if(Object.keys(e.node_attempts).length===0)delete e.node_attempts}loadWorkflowYaml(e){if(!R.existsSync(e))return null;const t=R.readFileSync(e,"utf-8");const o=Me.load(t);if(!o||!o.workflows||typeof o.workflows!=="object")return null;return o}getNodeType(e,t,o){const r=this.loadWorkflowYaml(e);const s=r?.workflows[t];const i=s?.nodes?.[o];return typeof i?.type==="string"?i.type:null}start(e,t,o){const r=this.loadWorkflowYaml(e);if(!r){throw new Error(`Failed to load workflow: ${e}`)}const s=Object.entries(r.workflows);if(s.length===0){throw new Error(`No workflows found in ${e}`)}let i;let n;if(t){if(!r.workflows[t]){throw new Error(`Workflow "${t}" not found in ${e}`)}i=t;n=r.workflows[t]}else{[i,n]=s[0]}const c=(0,N.generateId)("wfr");const p=n.state?JSON.parse(JSON.stringify(n.state)):{};const l=o??{};const f={...p,...l};const m={id:c,workflow_id:i,workflow_file:e,created_at:new Date().toISOString(),updated_at:new Date().toISOString(),current_node:"",completed_nodes:[],state:f,initial_state:{...p,...l},status:"pending"};this.saveRun(m);return m}getState(e){const t=this.loadRun(e);if(!t){throw new Error(`Execution not found: ${e}`)}return t.state}setState(e,t){this.atomicUpdate(e,o=>{Object.assign(o.state,t)})}moveNode(e,t){return this.atomicUpdate(e,o=>{if(o.status==="cancelled")return;if(o.current_node&&!o.completed_nodes.includes(o.current_node)){o.completed_nodes.push(o.current_node)}o.current_node=t})}async executeNode(e,t,o){const r=this.loadRun(e);if(!r){throw new Error(`Execution not found: ${e}`)}if(r.status==="cancelled"){throw(0,d.createWorkflowFailureError)("CANCELLED",`Workflow run "${e}" is already cancelled`)}const s=this.loadWorkflowYaml(r.workflow_file);if(!s){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Failed to load workflow file: ${r.workflow_file}`)}const i=s.workflows[r.workflow_id];if(!i){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Workflow "${r.workflow_id}" not found in ${r.workflow_file}`)}const n=i.nodes?.[t];if(!n){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}" not found in workflow "${r.workflow_id}"`)}const c=r.current_node;const p={};const l=[];let f=t;let m;let _;let S;let C;const w=o?.trigger??"manual";const k=new Date;let F=null;let H=null;let X=null;let q;let Z;const I=u=>{F=u.status;H=u.signal;X=u.errorOrigin;q=Ye(u)};const B=async()=>{const u=(0,N.generateId)("tsk");await this.atomicUpdateAsync(e,h=>{h.tasks=h.tasks??{};h.tasks[t]=u});C=u;return u};const Q=u=>{const h=new Date;return{exec_id:r.id,node_id:t,type:n.type,summary:Je(n),started_at:k.toISOString(),ended_at:h.toISOString(),duration_ms:h.getTime()-k.getTime(),exit_code:F,...u?{error:u}:{},trigger:w}};const g=await this.reserveNodeAttempt(e,t);try{switch(n.type){case"agent_task":{if(!n.agent){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}" is agent_task but has no agent defined`)}const u={...r.state,state:r.state};const h=b(n.agent,u).trim();if(!h){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": assignee not resolved (agent field "${n.agent}" interpolated to empty)`)}const E=(0,U.normalizeCwd)(n.cwd?b(n.cwd,u):void 0,this.projectRoot);if(!E.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${E.error}`)}const $=E.resolved;const v=(0,U.normalizeCwd)(void 0,this.projectRoot);if(!v.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${v.error}`)}const y=v.resolved;const G=n.input?b(n.input,u):"";const j=process.env.CREWX_WORKFLOW_PREAMBLE==="off"?"":(0,K.buildWorkflowPreamble)({workflowId:r.workflow_id,nodeId:t,execId:r.id});const P=n.output_format==="json"?j+G+(0,K.buildJsonOutputSuffix)(n.output_schema,n.output_strict):j+G;const T=n.mode==="query"?"q":"x";const L=`@${h} ${P}`;if(o?.dryRun){console.log(`[dry-run] Would execute: crewx ${T} "@${h} ..."`);break}console.log(`Executing: @${h} (${T} mode)`);const A=Number(process.env[T==="q"?"CREWX_TIMEOUT_QUERY":"CREWX_TIMEOUT_EXECUTE"])||8*36e5;const O=Se({...process.env,CREWX_WORKSPACE:y,CREWX_WORKFLOW_EXEC_ID:r.id,CREWX_WORKFLOW_NODE_ID:t,CREWX_WORKFLOW_ID:r.workflow_id},y);const z=[...O.args,T,L];if(process.env.CREWX_WORKFLOW_THREAD==="on"){z.push(`--thread=workflow:${r.id}`)}const te={workflow:{exec_id:r.id,node_id:t,workflow_id:r.workflow_id}};z.push("--metadata",JSON.stringify(te));const Ne=(0,N.resolveWindowsSpawnProgram)({command:O.command,env:O.env,cwd:$,allowShellFallback:true,packageName:"crewx"});const re=(0,N.materializeWindowsSpawnProgram)(Ne,z);const je=await B();const D=await ue(re.command,re.argv,{cwd:$,env:(0,N.withoutCrewxRootContext)({...O.env,CREWX_TRACE_ID:je}),timeout:A,shell:re.shell??false,windowsHide:re.windowsHide,cancellation:this.processCancellation(e)});I(D);if(D.cancelled){throw(0,d.createWorkflowFailureError)("CANCELLED",`Agent execution cancelled for node "${t}"`)}if(D.timedOut){throw new Error(`Agent execution timed out after ${A}ms`)}if(D.error){throw D.error}const Oe=D.stderr.match(/crewx kill (tsk_\w+)/);if(Oe?.[1])C=Oe[1];if(D.status!==0){const ne=D.stderr.trim();throw new Error(`Agent execution failed (exit ${D.status}): ${ne}`)}const oe=D.stdout.trim();const ve=(0,K.detectWorkflowSignal)(oe);if(ve){throw new Error(`Node "${t}" agent signaled failure: ${ve.reason??"(no reason given)"}`)}if(n.output){if(n.output_format==="json"){const ne=n.output_retry??1;let se=null;let we=null;let Ce=P;for(let he=0;he<=ne;he++){let _e=oe;if(he>0){Ce=P+`
|
|
2
|
+
|
|
3
|
+
## \u26A0\uFE0F PREVIOUS OUTPUT FAILED JSON.parse: ${we.message}
|
|
4
|
+
Return STRICT JSON ONLY. First char "{", last char "}". No prose, no fences.`;const me=`@${h} ${Ce}`;const ge=[...O.args,T,me];if(process.env.CREWX_WORKFLOW_THREAD==="on"){ge.push(`--thread=workflow:${r.id}`)}ge.push("--metadata",JSON.stringify(te));const ae=(0,N.materializeWindowsSpawnProgram)(Ne,ge);const Pe=await B();const x=await ue(ae.command,ae.argv,{cwd:$,env:(0,N.withoutCrewxRootContext)({...O.env,CREWX_TRACE_ID:Pe}),timeout:A,shell:ae.shell??false,windowsHide:ae.windowsHide,cancellation:this.processCancellation(e)});I(x);if(x.cancelled){throw(0,d.createWorkflowFailureError)("CANCELLED",`Agent retry cancelled for node "${t}"`)}if(x.timedOut)throw new Error(`Agent retry timed out after ${A}ms`);if(x.error)throw x.error;if(x.status!==0){throw new Error(`Agent retry failed (exit ${x.status}): ${x.stderr.trim()}`)}_e=x.stdout.trim();const $e=(0,K.detectWorkflowSignal)(_e);if($e){throw new Error(`Node "${t}" agent signaled failure: ${$e.reason??"(no reason given)"}`)}}try{se=JSON.parse((0,K.extractJson)(_e));if(n.output_schema){await(0,K.validateJsonSchema)(se,n.output_schema)}break}catch(me){we=me}}if(se===null){throw(0,d.createWorkflowFailureError)("OUTPUT_INVALID",`Node "${t}" output_format=json parse failed after ${ne+1} attempt(s): ${we.message}`)}p[n.output]=se;console.log(`Result stored in state.${n.output} (JSON object)`)}else{p[n.output]=oe;console.log(`Result stored in state.${n.output} (${oe.length} chars)`)}}break}case"skill_task":{if(!n.skill){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}" is skill_task but has no skill defined`)}if(!U.SKILL_NAME_RE.test(n.skill)){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}" has invalid skill name "${n.skill}" (must match ^[a-z0-9][a-z0-9-]*$)`)}const u={...r.state,state:r.state};const h=(n.args??[]).map(O=>b(String(O),u));const E=n.stdin!==void 0?b(String(n.stdin),u):void 0;const $=(0,U.normalizeCwd)(n.cwd?b(n.cwd,u):void 0,this.projectRoot);if(!$.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${$.error}`)}const v=(0,U.normalizeCwd)(void 0,this.projectRoot);if(!v.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${v.error}`)}const y=v.resolved;const G=n.timeout??exports.SKILL_TASK_DEFAULT_TIMEOUT;if(o?.dryRun){console.log(`[dry-run] Would execute: crewx skill ${n.skill} ${h.join(" ")}`.trim());break}console.log(`Executing skill: ${n.skill} ${h.join(" ")}`.trim());const j=Se({...process.env,CREWX_WORKSPACE:y,CREWX_WORKFLOW_EXEC_ID:r.id,CREWX_WORKFLOW_NODE_ID:t,CREWX_WORKFLOW_ID:r.workflow_id},y);const P=[...j.args,"skill",n.skill,...h];const T=(0,N.resolveWindowsSpawnProgram)({command:j.command,env:j.env,cwd:$.resolved,allowShellFallback:true,packageName:"crewx"});const L=(0,N.materializeWindowsSpawnProgram)(T,P);const A=await ue(L.command,L.argv,{cwd:$.resolved,env:j.env,timeout:G,shell:L.shell??false,windowsHide:L.windowsHide,stdin:E,cancellation:this.processCancellation(e)});I(A);if(A.cancelled){throw(0,d.createWorkflowFailureError)("CANCELLED",`skill_task "${t}" was cancelled`)}if(A.timedOut)throw new Error(`skill_task "${t}" timed out after ${G}ms`);if(A.error)throw A.error;if(A.status!==0){throw new Error(`skill_task "${t}" failed (exit ${A.status}): ${A.stderr.trim()}`)}await Le(n,t,A.stdout,p);if(n.output){console.log(`Result stored in state.${n.output}`)}break}case"shell_task":{const u=process.env.CREWX_WORKFLOW_SHELL==="1";const h=i.metadata?.shell_task_allowed===true;if(!u||!h){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID","shell_task is disabled. Set CREWX_WORKFLOW_SHELL=1 and metadata.shell_task_allowed=true to enable.")}const E=(0,U.validateShellCommand)(n.command);if(!E.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${E.error}`)}const $=(0,U.validateEnvKeys)(n.env);if(!$.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${$.error}`)}const v={...r.state,state:r.state};const y=n.command.map(z=>b(z,v));const G=(0,U.validateShellCommand)(y);if(!G.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${G.error}`)}const j=n.env?Object.fromEntries(Object.entries(n.env).map(([z,te])=>[z,b(String(te),v)])):void 0;const P=(0,U.normalizeCwd)(n.cwd?b(n.cwd,v):void 0,this.projectRoot);if(!P.ok){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Node "${t}": ${P.error}`)}const T=n.timeout??exports.SKILL_TASK_DEFAULT_TIMEOUT;if(o?.dryRun){console.log(`[dry-run] Would execute: ${y.join(" ")}`);break}console.log(`Executing shell_task: ${y[0]} (${y.length-1} args)`);const L=(0,U.buildShellEnv)(j,process.env);L.CREWX_WORKFLOW_EXEC_ID=r.id;L.CREWX_WORKFLOW_NODE_ID=t;L.CREWX_WORKFLOW_ID=r.workflow_id;const A=n.stdin!==void 0?b(String(n.stdin),v):void 0;const O=await ue(y[0],y.slice(1),{cwd:P.resolved,env:L,timeout:T,shell:false,stdin:A,cancellation:this.processCancellation(e)});I(O);if(O.cancelled){throw(0,d.createWorkflowFailureError)("CANCELLED",`shell_task "${t}" was cancelled`)}if(O.timedOut)throw new Error(`shell_task "${t}" timed out after ${T}ms`);if(O.error)throw O.error;if(O.status!==0){throw new Error(`shell_task "${t}" failed (exit ${O.status}): ${O.stderr.trim()}`)}await Le(n,t,O.stdout,p);if(n.output){console.log(`Result stored in state.${n.output}`)}break}case"branch":{if(!n.condition){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Branch node "${t}" has no condition`)}const u=String(de(String(n.condition),r.state));const h=n.branches;const E=h?.[u]??n.default;if(!E){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Branch "${t}": no matching branch for "${u}" and no default`)}console.log(`Branch "${t}": condition="${n.condition}" \u2192 ${u} \u2192 ${E}`);l.push(t);f=E;break}case"parallel":{const u=n.branches;if(u&&Array.isArray(u)){console.log(`Parallel "${t}": branches = [${u.join(", ")}]`);console.log(` Enter each branch sequentially with: workflow run node <exec-id> <branch-node>`)}break}case"join":{console.log(`Join "${t}": completed_nodes = [${r.completed_nodes.join(", ")}]`);break}case"end":{m="completed";console.log(`End "${t}": workflow execution completed`);break}case"error":{m="failed";S="UNKNOWN";const u={...r.state,state:r.state};_=n.message?b(n.message,u):`Workflow failed at error node "${t}"`;console.log(`Error "${t}": workflow execution failed \u2014 ${_}`);break}case"expression":{if(!n.set||typeof n.set!=="object"){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Expression node "${t}" requires "set" field`)}for(const[u,h]of Object.entries(n.set)){const E=Ee(String(h),r.state);p[u]=String(E);console.log(` ${u} = ${E}`)}console.log(`Expression "${t}": set ${Object.keys(n.set).join(", ")}`);if(n.next){l.push(t);f=n.next}break}case"approval":{console.log(`Approval "${t}": manual intervention required (not auto-executed)`);break}default:console.log(`Unknown node type "${n.type}" for node "${t}"`)}if(n.fail_when&&!o?.dryRun&&(n.type==="agent_task"||n.type==="skill_task"||n.type==="shell_task")){const u=de(String(n.fail_when),{...r.state,...p});if(u===true||u==="true"){const h=n.output?p[n.output]:void 0;if(h&&typeof h==="object"&&!Array.isArray(h)){const E=h.reason;if(typeof E==="string"){const $=(0,N.redactTechnicalMessage)(E);const v=$?.split(/\r\n|\r|\n/u).find(y=>y.trim());if(v)Z=v.trim().slice(0,180)}}throw new Error(`Node "${t}" judged failed by fail_when: ${n.fail_when}`)}}}catch(u){const h=J(u);const E=(0,d.classifyWorkflowFailure)({errorCode:(0,d.getWorkflowFailureCode)(u),process:q});const $=(0,d.buildNodeFailure)({failedNode:t,attempt:g,causeCode:E,exitCode:E==="NODE_EXIT_NONZERO"&&typeof F==="number"?F:null,signal:E==="INTERRUPTED"?H:null,errorOrigin:X,timedOut:q?.timedOut??false});const v=Q(Z??$.causeMessage);await this.atomicUpdateAsync(e,y=>{if(y.status==="cancelled")return;Object.assign(y.state,p);delete y.manual_resume;y.current_node=t;V(y,$,Z??h);y.audit=y.audit??[];y.audit.push(v)});throw u}const M=S?(0,d.buildNodeFailure)({failedNode:t,attempt:g,causeCode:S,exitCode:null,signal:null,errorOrigin:null}):null;const pe=Q(M?.causeMessage);return this.atomicUpdateAsync(e,u=>{if(u.status==="cancelled")return;Object.assign(u.state,p);delete u.manual_resume;for(const h of l){if(!u.completed_nodes.includes(h)){u.completed_nodes.push(h)}}if(c&&c!==t&&!u.completed_nodes.includes(c)){u.completed_nodes.push(c)}u.current_node=f;if(M){V(u,M,_??M.causeMessage)}else{delete u.error;delete u.last_failure;this.clearNodeAttempt(u,t);if(m){u.status=m}else if(u.status!=="completed"){u.status="running"}}if(C){u.tasks=u.tasks??{};u.tasks[t]=C}u.audit=u.audit??[];u.audit.push(pe)})}async claimAutoRunLease(e,t,o){let r=false;const s=await this.atomicUpdateAsync(e,i=>{if(Re(i.status))return;const n=i.auto_run;const c=n?.pid===process.pid&&typeof n.token==="string";if(n&&!c&&De(n.pid)){throw new Error(`Workflow run "${e}" is already owned by another runner process`)}const p=o??(c?n.token:(0,N.generateId)("wfr"));if(!Re(i.status)){i.status="running"}if(!i.current_node)i.current_node=t;i.auto_run={pid:process.pid,token:p,node_id:t,started_at:c&&n.token===p?n.started_at:new Date().toISOString()};delete i.manual_resume;r=true});return r?s.auto_run?.token??null:null}async releaseAutoRunLease(e,t){await this.atomicUpdateAsync(e,o=>{if(o.auto_run?.token===t)delete o.auto_run})}async failAutoConfiguration(e,t,o){return this.atomicUpdateAsync(e,r=>{if(r.status==="cancelled")return;const s=(0,d.buildNodeFailure)({failedNode:t,attempt:(0,d.readNodeAttempt)(r,t),causeCode:"CONFIG_INVALID",exitCode:null,signal:null,errorOrigin:null});V(r,s,o);Y(r,s,"config","auto")})}async reserveAutoRetry(e,t,o,r,s){let i=false;const n=await this.atomicUpdateAsync(e,c=>{if(c.status==="cancelled")return;if(c.status!=="failed")return;if(c.auto_run?.token!==o)return;const p=(0,ce.decideRetry)({policy:r,run:c,nodeId:t});if(!p.retry||p.attempt===void 0)return;const l=new Date().toISOString();c.status="retrying";delete c.error;delete c.manual_resume;c.audit=c.audit??[];c.audit.push({exec_id:c.id,node_id:t,type:"retry",summary:`auto retry ${p.attempt+1}/${r.max+1} in ${s}ms`,started_at:l,ended_at:l,duration_ms:0,exit_code:null,trigger:"auto"});i=true});return{run:n,reserved:i}}async waitForRetry(e,t,o){const r=()=>{const c=this.loadRun(e);if(!c)throw new Error(`Execution not found: ${e}`);if(c.status==="cancelled"){return{run:c,outcome:"cancelled",reason:"run cancelled during retry delay"}}if(c.auto_run?.token!==t){return{run:c,outcome:"paused",reason:"auto-run lease lost during retry delay"}}if(c.status!=="retrying"){return{run:c,outcome:"paused",reason:"run state changed during retry delay"}}return null};let s=Math.max(0,o);while(s>0){const c=Math.min(s,qe);await this.sleep(c);s-=c;const p=r();if(p)return p}const i=r();if(i)return i;const n=this.loadRun(e);if(!n)throw new Error(`Execution not found: ${e}`);return{run:n,outcome:"ready"}}async runNodeWithRetry(e,t,o,r){let s=o;while(true){try{const i=await this.executeNode(e,t,{trigger:"auto"});if(i.status==="cancelled"){return{run:i,runnerToken:s,outcome:"cancelled",reason:i.error}}return{run:i,runnerToken:s,outcome:"success"}}catch(i){const n=this.loadRun(e);if(!n)throw new Error(`Execution not found: ${e}`);if(n.status==="cancelled"){return{run:n,runnerToken:s,outcome:"cancelled",reason:n.error}}if(n.status!=="failed"||!r){return{run:n,runnerToken:s,outcome:n.status==="failed"?"failed":"paused",reason:J(i)}}const c=(0,ce.decideRetry)({policy:r,run:n,nodeId:t});if(!c.retry||c.attempt===void 0){return{run:n,runnerToken:s,outcome:"failed",reason:J(i)}}const p=(0,ce.computeRetryDelayMs)(r,c.attempt);const l=await this.reserveAutoRetry(e,t,s,r,p);if(!l.reserved){const _=l.run;if(_.status==="cancelled"){return{run:_,runnerToken:s,outcome:"cancelled",reason:_.error}}return{run:_,runnerToken:s,outcome:_.status==="failed"?"failed":"paused",reason:J(i)}}const f=await this.waitForRetry(e,s,p);if(f.outcome!=="ready"){return{run:f.run,runnerToken:s,outcome:f.outcome,reason:f.reason}}const m=await this.claimAutoRunLease(e,t,s);if(!m){const _=this.loadRun(e);if(!_)throw new Error(`Execution not found: ${e}`);if(_.status==="cancelled"){return{run:_,runnerToken:s,outcome:"cancelled",reason:_.error}}return{run:_,runnerToken:s,outcome:"paused",reason:"run stopped before retry lease could be reclaimed"}}s=m}}}async runAuto(e){const t=`${this.resolvedDir}::${e}`;if(ke.has(t)){const r=this.loadRun(e);if(!r)throw new Error(`Execution not found: ${e}`);console.log(`runAuto "${e}": already in-flight \u2014 skipping duplicate auto-run (single-flight guard)`);return{run:r,outcome:"paused",reason:"execution already in-flight (single-flight guard)",executed:[]}}ke.add(t);let o;try{const r=this.loadRun(e);if(!r)throw new Error(`Execution not found: ${e}`);if(r.auto_run?.pid===process.pid){o=r.auto_run.token}const s=this.loadWorkflowYaml(r.workflow_file);if(!s){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Failed to load workflow file: ${r.workflow_file}`)}const i=s.workflows[r.workflow_id];if(!i){throw(0,d.createWorkflowFailureError)("CONFIG_INVALID",`Workflow "${r.workflow_id}" not found in ${r.workflow_file}`)}const n=[];if(r.status==="completed"||r.status==="failed"||r.status==="cancelled"){return{run:r,outcome:r.status==="completed"?"completed":r.status==="cancelled"?"cancelled":"failed",reason:`run already ${r.status}`,executed:n}}const c=Object.keys(i.nodes??{})[0];const p=typeof i.max_iterations==="number"&&i.max_iterations>0?i.max_iterations:30;let l=r;while(true){l=this.loadRun(e);if(l.status==="completed")return{run:l,outcome:"completed",executed:n};if(l.status==="failed")return{run:l,outcome:"failed",reason:l.error,executed:n};if(l.status==="cancelled")return{run:l,outcome:"cancelled",reason:l.error,executed:n};const f=l.current_node||c;if(!f){l=await this.atomicUpdateAsync(e,w=>{const k=(0,d.buildNodeFailure)({failedNode:w.current_node||"",attempt:(0,d.readNodeAttempt)(w,w.current_node||""),causeCode:"CONFIG_INVALID",exitCode:null,signal:null,errorOrigin:null});V(w,k,"workflow has no nodes");Y(w,k,"config","auto")});return{run:l,outcome:"failed",reason:l.error,executed:n}}const m=i.nodes[f];if(!m){l=await this.atomicUpdateAsync(e,w=>{w.current_node=f;const k=(0,d.buildNodeFailure)({failedNode:f,attempt:(0,d.readNodeAttempt)(w,f),causeCode:"CONFIG_INVALID",exitCode:null,signal:null,errorOrigin:null});V(w,k,`node "${f}" not found in workflow`);Y(w,k,"config","auto")});return{run:l,outcome:"failed",reason:l.error,executed:n}}const _=(0,ce.parseRetryPolicy)(m);if(!_.ok){l=await this.failAutoConfiguration(e,f,_.reason);if(l.status==="cancelled"){return{run:l,outcome:"cancelled",reason:l.error,executed:n}}return{run:l,outcome:"failed",reason:l.error,executed:n}}if(m.type==="approval"){if(l.current_node!==f){l=await this.atomicUpdateAsync(e,w=>{w.current_node=f})}return{run:l,outcome:"paused",reason:`approval node "${f}" requires manual decision`,executed:n}}if(m.type==="parallel"){if(l.current_node!==f){l=await this.atomicUpdateAsync(e,w=>{w.current_node=f})}return{run:l,outcome:"paused",reason:`parallel auto-run not implemented (node "${f}")`,executed:n}}if(!Ae.has(m.type)){if(l.current_node!==f){l=await this.atomicUpdateAsync(e,w=>{w.current_node=f})}return{run:l,outcome:"paused",reason:`unknown node type "${m.type}" (node "${f}")`,executed:n}}const S=await this.claimAutoRunLease(e,f,o);if(!S){l=this.loadRun(e);return{run:l,outcome:l.status==="cancelled"?"cancelled":"paused",reason:l.status==="cancelled"?l.error:"run stopped before auto-run lease could be claimed",executed:n}}o=S;l=await this.atomicUpdateAsync(e,w=>{if(w.status==="cancelled")return;const k=at(w);k.visitCounts[f]=(k.visitCounts[f]??0)+1;k.iterations+=1;w.state["__engine"]=k;if(k.visitCounts[f]>p||k.iterations>p){const F=(0,d.buildNodeFailure)({failedNode:f,attempt:(0,d.readNodeAttempt)(w,f),causeCode:"CONFIG_INVALID",exitCode:null,signal:null,errorOrigin:null});V(w,F,`auto runner exceeded max_iterations (${p}); possible loop at "${f}"`);Y(w,F,"config","auto")}});if(l.status==="cancelled"){return{run:l,outcome:"cancelled",reason:l.error,executed:n}}if(l.status==="failed"){return{run:l,outcome:"failed",reason:l.error,executed:n}}const C=await this.runNodeWithRetry(e,f,o,_.policy);l=C.run;o=C.runnerToken;if(C.outcome!=="success"){return{run:l,outcome:C.outcome,reason:C.reason,executed:n}}n.push(f);if(l.status==="completed")return{run:l,outcome:"completed",executed:n};if(l.status==="failed")return{run:l,outcome:"failed",reason:l.error,executed:n};if(l.status==="cancelled")return{run:l,outcome:"cancelled",reason:l.error,executed:n};if(!Xe.has(m.type)){const w=m.next;if(!w){return{run:l,outcome:"paused",reason:`node "${f}" has no "next"; stopping`,executed:n}}l=this.moveNode(e,w)}}}finally{if(o){try{await this.releaseAutoRunLease(e,o)}catch{}}ke.delete(t)}}cancelRun(e,t="Cancelled by user"){const o=this.atomicUpdate(e,r=>{if(r.status==="completed"||r.status==="failed"||r.status==="cancelled"){return}const s=new Date;const i=r.auto_run?.node_id||r.current_node||"";const n=(0,d.buildNodeFailure)({failedNode:i,attempt:(0,d.readNodeAttempt)(r,i),causeCode:"CANCELLED",exitCode:null,signal:null,errorOrigin:null});V(r,n,t);r.status="cancelled";delete r.auto_run;delete r.manual_resume;Y(r,n,"cancel","manual",s.toISOString())});if(o.status==="cancelled"){it(this.resolvedDir,e)}return o}reset(e){return this.atomicUpdate(e,t=>{t.state=JSON.parse(JSON.stringify(t.initial_state));t.current_node="";t.completed_nodes=[];t.status="pending";delete t.error;delete t.last_failure;delete t.node_attempts;delete t.auto_run;delete t.manual_resume})}retry(e,t={}){const o=this.loadRun(e);if(!o)throw new Error(`Execution not found: ${e}`);const r=this.loadWorkflowYaml(o.workflow_file);if(!r)throw new Error(`Failed to load workflow file: ${o.workflow_file}`);const s=r.workflows[o.workflow_id];if(!s)throw new Error(`Workflow "${o.workflow_id}" not found`);if(o.status!=="failed"){throw new Error(`Run "${e}" is not failed (status=${o.status??"running"}); only failed runs can be retried`)}if(!Ue(o,s)){throw new Error(`Run "${e}" failed at a non-retryable node "${o.current_node}" (error node); use reset to restart`)}return this.atomicUpdate(e,i=>{if(i.status!=="failed"){throw new Error(`Run "${e}" is not failed (status=${i.status??"pending"}); only failed runs can be retried`)}const n=i.current_node;if(n&&i.last_failure){const p=i.last_failure.attempt;const l=i.node_attempts?.[n];if(typeof p==="number"&&Number.isInteger(p)&&p>0&&(typeof l!=="number"||l<p)){i.node_attempts=i.node_attempts??{};i.node_attempts[n]=p}}i.status="retrying";delete i.error;delete i.manual_resume;i.retry_count=(i.retry_count??0)+1;i.audit=i.audit??[];const c=new Date().toISOString();if(t.autoRun===false){delete i.auto_run;i.manual_resume={node_id:n,requested_at:c}}else{i.auto_run={pid:process.pid,token:(0,N.generateId)("wfr"),node_id:n,started_at:c}}i.audit.push({exec_id:i.id,node_id:n,type:"retry",summary:`manual retry #${i.retry_count}`,started_at:c,ended_at:c,duration_ms:0,exit_code:null,trigger:"manual"})})}listRunIds(){try{return R.readdirSync(this.resolvedDir,{withFileTypes:true}).filter(e=>e.isFile()&&!e.name.startsWith(".")&&e.name.endsWith(".json")).map(e=>e.name.slice(0,-".json".length))}catch(e){if(e.code==="ENOENT")return[];throw e}}listRuns(e){this.ensureRunsDir();const t=R.readdirSync(this.resolvedDir).filter(r=>r.endsWith(".json"));const o=[];for(const r of t){try{const s=r.slice(0,-".json".length);const i=this.loadRun(s);if(!i)continue;if(e?.workflowId&&i.workflow_id!==e.workflowId)continue;o.push(i)}catch{}}o.sort((r,s)=>new Date(s.created_at).getTime()-new Date(r.created_at).getTime());return o}}exports.RunManager=ct;
|