@crewx/workflow 0.3.22-rc.12 → 0.3.22-rc.121
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +104 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +77 -8
- package/dist/cli.js.map +1 -1
- package/dist/src/engine.d.ts +4 -2
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +26 -1
- package/dist/src/engine.js.map +1 -1
- package/dist/src/mermaid.d.ts.map +1 -1
- package/dist/src/mermaid.js +7 -4
- package/dist/src/mermaid.js.map +1 -1
- package/dist/src/parent-task-sync.d.ts +49 -0
- package/dist/src/parent-task-sync.d.ts.map +1 -0
- package/dist/src/parent-task-sync.js +125 -0
- package/dist/src/parent-task-sync.js.map +1 -0
- package/dist/src/run-manager.d.ts +15 -1
- package/dist/src/run-manager.d.ts.map +1 -1
- package/dist/src/run-manager.js +423 -95
- package/dist/src/run-manager.js.map +1 -1
- package/dist/src/types.d.ts +18 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/output-format.d.ts +12 -0
- package/dist/src/utils/output-format.d.ts.map +1 -1
- package/dist/src/utils/output-format.js +103 -5
- package/dist/src/utils/output-format.js.map +1 -1
- package/dist/workflow-schema.json +14 -2
- package/package.json +14 -4
- package/workflow-schema.json +14 -2
package/dist/src/run-manager.js
CHANGED
|
@@ -33,13 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.RunManager = exports.SKILL_TASK_DEFAULT_TIMEOUT = void 0;
|
|
36
|
+
exports.RunManager = exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS = exports.WORKFLOW_RUN_INTERRUPTED_ERROR = exports.SKILL_TASK_DEFAULT_TIMEOUT = exports.FileLock = void 0;
|
|
37
37
|
exports.parseSetArgs = parseSetArgs;
|
|
38
38
|
exports.readStdin = readStdin;
|
|
39
39
|
exports.interpolateTemplate = interpolateTemplate;
|
|
40
40
|
exports.safeEvaluate = safeEvaluate;
|
|
41
41
|
exports.safeEvaluateBranchCondition = safeEvaluateBranchCondition;
|
|
42
42
|
exports.validateSafeBranchCondition = validateSafeBranchCondition;
|
|
43
|
+
exports.isRetryableFailure = isRetryableFailure;
|
|
43
44
|
const fs = __importStar(require("fs"));
|
|
44
45
|
const path = __importStar(require("path"));
|
|
45
46
|
const yaml = __importStar(require("js-yaml"));
|
|
@@ -48,11 +49,15 @@ const sdk_1 = require("@crewx/sdk");
|
|
|
48
49
|
const file_lock_1 = require("./file-lock");
|
|
49
50
|
const output_format_1 = require("./utils/output-format");
|
|
50
51
|
const shell_security_1 = require("./utils/shell-security");
|
|
52
|
+
var file_lock_2 = require("./file-lock");
|
|
53
|
+
Object.defineProperty(exports, "FileLock", { enumerable: true, get: function () { return file_lock_2.FileLock; } });
|
|
51
54
|
exports.SKILL_TASK_DEFAULT_TIMEOUT = 600000;
|
|
55
|
+
exports.WORKFLOW_RUN_INTERRUPTED_ERROR = 'Workflow run interrupted: runner process exited before completion';
|
|
56
|
+
exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS = 10 * 60000;
|
|
52
57
|
const KNOWN_AUTO_NODE_TYPES = new Set([
|
|
53
|
-
'agent_task', 'skill_task', 'shell_task', 'branch', 'expression', 'join', 'end',
|
|
58
|
+
'agent_task', 'skill_task', 'shell_task', 'branch', 'expression', 'join', 'end', 'error',
|
|
54
59
|
]);
|
|
55
|
-
const SELF_ADVANCING_NODE_TYPES = new Set(['branch', 'expression', 'end']);
|
|
60
|
+
const SELF_ADVANCING_NODE_TYPES = new Set(['branch', 'expression', 'end', 'error']);
|
|
56
61
|
function killProcessGroup(child) {
|
|
57
62
|
try {
|
|
58
63
|
if (process.platform === 'win32') {
|
|
@@ -81,7 +86,7 @@ async function runProcess(command, argv, opts) {
|
|
|
81
86
|
child = cp.spawn(command, argv, {
|
|
82
87
|
cwd: opts.cwd,
|
|
83
88
|
env: opts.env,
|
|
84
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
89
|
+
stdio: [opts.stdin !== undefined ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
85
90
|
shell: opts.shell ?? false,
|
|
86
91
|
detached: process.platform !== 'win32',
|
|
87
92
|
windowsHide: opts.windowsHide ?? true,
|
|
@@ -109,6 +114,9 @@ async function runProcess(command, argv, opts) {
|
|
|
109
114
|
timer.unref();
|
|
110
115
|
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
111
116
|
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
117
|
+
if (opts.stdin !== undefined) {
|
|
118
|
+
child.stdin?.end(opts.stdin);
|
|
119
|
+
}
|
|
112
120
|
child.on('error', (err) => finish({ status: null, stdout, stderr, timedOut: false, error: err }));
|
|
113
121
|
child.on('close', (code) => finish({ status: code, stdout, stderr, timedOut: false }));
|
|
114
122
|
});
|
|
@@ -521,12 +529,62 @@ function safeEvaluateBranchCondition(expr, state) {
|
|
|
521
529
|
function validateSafeBranchCondition(expr) {
|
|
522
530
|
safeEvaluateBranchCondition(expr, {});
|
|
523
531
|
}
|
|
532
|
+
function readRunEngineState(run) {
|
|
533
|
+
const raw = run.state['__engine'];
|
|
534
|
+
if (!raw || typeof raw !== 'object')
|
|
535
|
+
return { visitCounts: {}, iterations: 0 };
|
|
536
|
+
const record = raw;
|
|
537
|
+
const rawCounts = record['visitCounts'];
|
|
538
|
+
const visitCounts = {};
|
|
539
|
+
if (rawCounts && typeof rawCounts === 'object') {
|
|
540
|
+
for (const [key, value] of Object.entries(rawCounts)) {
|
|
541
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
542
|
+
visitCounts[key] = value;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const iterations = typeof record['iterations'] === 'number' && Number.isFinite(record['iterations'])
|
|
546
|
+
? record['iterations']
|
|
547
|
+
: 0;
|
|
548
|
+
return { visitCounts, iterations };
|
|
549
|
+
}
|
|
550
|
+
const inFlightAutoRuns = new Set();
|
|
551
|
+
function isProcessAlive(pid) {
|
|
552
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
553
|
+
return false;
|
|
554
|
+
if (pid === process.pid)
|
|
555
|
+
return true;
|
|
556
|
+
try {
|
|
557
|
+
process.kill(pid, 0);
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
return error.code === 'EPERM';
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function isTerminalStatus(status) {
|
|
565
|
+
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
566
|
+
}
|
|
567
|
+
function isRetryableFailure(run, spec) {
|
|
568
|
+
if (run.status !== 'failed')
|
|
569
|
+
return false;
|
|
570
|
+
if (!run.current_node)
|
|
571
|
+
return false;
|
|
572
|
+
const node = spec.nodes?.[run.current_node];
|
|
573
|
+
if (!node)
|
|
574
|
+
return false;
|
|
575
|
+
return node.type !== 'error';
|
|
576
|
+
}
|
|
524
577
|
class RunManager {
|
|
525
578
|
constructor(runsDir = '.crewx/workflow-runs', projectRoot) {
|
|
526
579
|
this.runsDir = runsDir;
|
|
527
580
|
const base = projectRoot || process.env.CREWX_WORKSPACE || process.cwd();
|
|
528
581
|
this.resolvedDir = path.resolve(base, this.runsDir);
|
|
529
582
|
this.projectRoot = path.resolve(base);
|
|
583
|
+
const hasConfig = fs.existsSync(path.join(this.projectRoot, 'crewx.yaml'))
|
|
584
|
+
|| fs.existsSync(path.join(this.projectRoot, 'crewx.yml'));
|
|
585
|
+
if (!hasConfig) {
|
|
586
|
+
console.warn(`[RunManager] projectRoot "${this.projectRoot}" has no crewx.yaml/crewx.yml — agent_task nodes may fail to resolve agents.`);
|
|
587
|
+
}
|
|
530
588
|
}
|
|
531
589
|
ensureRunsDir() {
|
|
532
590
|
if (!fs.existsSync(this.resolvedDir)) {
|
|
@@ -538,8 +596,7 @@ class RunManager {
|
|
|
538
596
|
throw new Error(`Invalid execution ID: "${execId}"`);
|
|
539
597
|
}
|
|
540
598
|
}
|
|
541
|
-
|
|
542
|
-
this.validateExecId(execId);
|
|
599
|
+
readRun(execId) {
|
|
543
600
|
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
544
601
|
if (!fs.existsSync(filePath))
|
|
545
602
|
return null;
|
|
@@ -550,6 +607,78 @@ class RunManager {
|
|
|
550
607
|
throw new Error(`Failed to parse execution file ${filePath}: ${e.message}`);
|
|
551
608
|
}
|
|
552
609
|
}
|
|
610
|
+
isLegacyRetryStale(run) {
|
|
611
|
+
if (run.status !== undefined
|
|
612
|
+
|| run.auto_run
|
|
613
|
+
|| run.manual_resume
|
|
614
|
+
|| !run.current_node) {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
const lastAudit = run.audit?.at(-1);
|
|
618
|
+
if (lastAudit?.type !== 'retry')
|
|
619
|
+
return false;
|
|
620
|
+
const updatedAt = Date.parse(run.updated_at);
|
|
621
|
+
if (!Number.isFinite(updatedAt) || Date.now() - updatedAt < exports.WORKFLOW_RUN_LEGACY_RECOVERY_GRACE_MS) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
const doc = this.loadWorkflowYaml(run.workflow_file);
|
|
625
|
+
const spec = doc?.workflows[run.workflow_id];
|
|
626
|
+
const node = spec?.nodes?.[run.current_node];
|
|
627
|
+
return Boolean(node && KNOWN_AUTO_NODE_TYPES.has(node.type));
|
|
628
|
+
}
|
|
629
|
+
hasStaleRunner(run) {
|
|
630
|
+
if (run.auto_run)
|
|
631
|
+
return !isProcessAlive(run.auto_run.pid);
|
|
632
|
+
return this.isLegacyRetryStale(run);
|
|
633
|
+
}
|
|
634
|
+
recoverStaleRunner(execId) {
|
|
635
|
+
const filePath = path.join(this.resolvedDir, `${execId}.json`);
|
|
636
|
+
const lock = new file_lock_1.FileLock(filePath);
|
|
637
|
+
lock.acquire();
|
|
638
|
+
try {
|
|
639
|
+
const run = this.readRun(execId);
|
|
640
|
+
if (!run || !this.hasStaleRunner(run))
|
|
641
|
+
return run;
|
|
642
|
+
if (isTerminalStatus(run.status)) {
|
|
643
|
+
delete run.auto_run;
|
|
644
|
+
delete run.manual_resume;
|
|
645
|
+
this.saveRun(run);
|
|
646
|
+
return run;
|
|
647
|
+
}
|
|
648
|
+
const lease = run.auto_run;
|
|
649
|
+
const nodeId = lease?.node_id || run.current_node;
|
|
650
|
+
const now = new Date().toISOString();
|
|
651
|
+
run.status = 'failed';
|
|
652
|
+
run.error = exports.WORKFLOW_RUN_INTERRUPTED_ERROR;
|
|
653
|
+
delete run.auto_run;
|
|
654
|
+
delete run.manual_resume;
|
|
655
|
+
run.audit = run.audit ?? [];
|
|
656
|
+
run.audit.push({
|
|
657
|
+
exec_id: run.id,
|
|
658
|
+
node_id: nodeId,
|
|
659
|
+
type: 'interrupted',
|
|
660
|
+
summary: exports.WORKFLOW_RUN_INTERRUPTED_ERROR,
|
|
661
|
+
started_at: now,
|
|
662
|
+
ended_at: now,
|
|
663
|
+
duration_ms: 0,
|
|
664
|
+
exit_code: null,
|
|
665
|
+
error: exports.WORKFLOW_RUN_INTERRUPTED_ERROR,
|
|
666
|
+
trigger: 'auto',
|
|
667
|
+
});
|
|
668
|
+
this.saveRun(run);
|
|
669
|
+
return run;
|
|
670
|
+
}
|
|
671
|
+
finally {
|
|
672
|
+
lock.release();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
loadRun(execId) {
|
|
676
|
+
this.validateExecId(execId);
|
|
677
|
+
const run = this.readRun(execId);
|
|
678
|
+
if (!run || !this.hasStaleRunner(run))
|
|
679
|
+
return run;
|
|
680
|
+
return this.recoverStaleRunner(execId);
|
|
681
|
+
}
|
|
553
682
|
saveRun(run) {
|
|
554
683
|
this.ensureRunsDir();
|
|
555
684
|
run.updated_at = new Date().toISOString();
|
|
@@ -563,7 +692,7 @@ class RunManager {
|
|
|
563
692
|
const lock = new file_lock_1.FileLock(filePath);
|
|
564
693
|
lock.acquire();
|
|
565
694
|
try {
|
|
566
|
-
const run = this.
|
|
695
|
+
const run = this.readRun(execId);
|
|
567
696
|
if (!run)
|
|
568
697
|
throw new Error(`Execution not found: ${execId}`);
|
|
569
698
|
updater(run);
|
|
@@ -581,7 +710,7 @@ class RunManager {
|
|
|
581
710
|
const lock = new file_lock_1.FileLock(filePath);
|
|
582
711
|
await lock.acquireAsync();
|
|
583
712
|
try {
|
|
584
|
-
const run = this.
|
|
713
|
+
const run = this.readRun(execId);
|
|
585
714
|
if (!run)
|
|
586
715
|
throw new Error(`Execution not found: ${execId}`);
|
|
587
716
|
updater(run);
|
|
@@ -601,6 +730,12 @@ class RunManager {
|
|
|
601
730
|
return null;
|
|
602
731
|
return doc;
|
|
603
732
|
}
|
|
733
|
+
getNodeType(workflowFile, workflowId, nodeId) {
|
|
734
|
+
const doc = this.loadWorkflowYaml(workflowFile);
|
|
735
|
+
const spec = doc?.workflows[workflowId];
|
|
736
|
+
const node = spec?.nodes?.[nodeId];
|
|
737
|
+
return typeof node?.type === 'string' ? node.type : null;
|
|
738
|
+
}
|
|
604
739
|
start(yamlPath, workflowId, overrides) {
|
|
605
740
|
const doc = this.loadWorkflowYaml(yamlPath);
|
|
606
741
|
if (!doc) {
|
|
@@ -684,6 +819,7 @@ class RunManager {
|
|
|
684
819
|
const nodesToComplete = [];
|
|
685
820
|
let currentNode = nodeId;
|
|
686
821
|
let status;
|
|
822
|
+
let failureMessage;
|
|
687
823
|
let nodeTaskId;
|
|
688
824
|
const trigger = opts?.trigger ?? 'manual';
|
|
689
825
|
const startedAt = new Date();
|
|
@@ -711,19 +847,39 @@ class RunManager {
|
|
|
711
847
|
if (!nodeSpec.agent) {
|
|
712
848
|
throw new Error(`Node "${nodeId}" is agent_task but has no agent defined`);
|
|
713
849
|
}
|
|
850
|
+
const agentCtx = { ...snapshot.state, state: snapshot.state };
|
|
851
|
+
const resolvedAgent = interpolateTemplate(nodeSpec.agent, agentCtx).trim();
|
|
852
|
+
if (!resolvedAgent) {
|
|
853
|
+
throw new Error(`Node "${nodeId}": assignee not resolved (agent field "${nodeSpec.agent}" interpolated to empty)`);
|
|
854
|
+
}
|
|
855
|
+
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, agentCtx) : undefined, this.projectRoot);
|
|
856
|
+
if (!cwdCheck.ok)
|
|
857
|
+
throw new Error(`Node "${nodeId}": ${cwdCheck.error}`);
|
|
858
|
+
const nodeCwd = cwdCheck.resolved;
|
|
859
|
+
const rootCheck = (0, shell_security_1.normalizeCwd)(undefined, this.projectRoot);
|
|
860
|
+
if (!rootCheck.ok)
|
|
861
|
+
throw new Error(`Node "${nodeId}": ${rootCheck.error}`);
|
|
862
|
+
const workspaceRoot = rootCheck.resolved;
|
|
714
863
|
const input = nodeSpec.input
|
|
715
|
-
? interpolateTemplate(nodeSpec.input,
|
|
864
|
+
? interpolateTemplate(nodeSpec.input, agentCtx)
|
|
716
865
|
: '';
|
|
866
|
+
const preamble = process.env.CREWX_WORKFLOW_PREAMBLE === 'off'
|
|
867
|
+
? ''
|
|
868
|
+
: (0, output_format_1.buildWorkflowPreamble)({
|
|
869
|
+
workflowId: snapshot.workflow_id,
|
|
870
|
+
nodeId,
|
|
871
|
+
execId: snapshot.id,
|
|
872
|
+
});
|
|
717
873
|
const finalInput = nodeSpec.output_format === 'json'
|
|
718
|
-
? input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
719
|
-
: input;
|
|
874
|
+
? preamble + input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
875
|
+
: preamble + input;
|
|
720
876
|
const mode = nodeSpec.mode === 'query' ? 'q' : 'x';
|
|
721
|
-
const prompt = `@${
|
|
877
|
+
const prompt = `@${resolvedAgent} ${finalInput}`;
|
|
722
878
|
if (opts?.dryRun) {
|
|
723
|
-
console.log(`[dry-run] Would execute: crewx ${mode} "@${
|
|
879
|
+
console.log(`[dry-run] Would execute: crewx ${mode} "@${resolvedAgent} ..."`);
|
|
724
880
|
break;
|
|
725
881
|
}
|
|
726
|
-
console.log(`Executing: @${
|
|
882
|
+
console.log(`Executing: @${resolvedAgent} (${mode} mode)`);
|
|
727
883
|
const crewxCli = process.env.CREWX_CLI || 'npx crewx';
|
|
728
884
|
const cliParts = crewxCli.split(/\s+/);
|
|
729
885
|
const bin = cliParts[0];
|
|
@@ -748,9 +904,10 @@ class RunManager {
|
|
|
748
904
|
});
|
|
749
905
|
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, wfArgs);
|
|
750
906
|
const result = await runProcess(invocation.command, invocation.argv, {
|
|
751
|
-
cwd:
|
|
907
|
+
cwd: nodeCwd,
|
|
752
908
|
env: {
|
|
753
909
|
...process.env,
|
|
910
|
+
CREWX_WORKSPACE: workspaceRoot,
|
|
754
911
|
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
755
912
|
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
756
913
|
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
@@ -775,6 +932,10 @@ class RunManager {
|
|
|
775
932
|
throw new Error(`Agent execution failed (exit ${result.status}): ${stderr}`);
|
|
776
933
|
}
|
|
777
934
|
const output = result.stdout.trim();
|
|
935
|
+
const signal = (0, output_format_1.detectWorkflowSignal)(output);
|
|
936
|
+
if (signal) {
|
|
937
|
+
throw new Error(`Node "${nodeId}" agent signaled failure: ${signal.reason ?? '(no reason given)'}`);
|
|
938
|
+
}
|
|
778
939
|
if (nodeSpec.output) {
|
|
779
940
|
if (nodeSpec.output_format === 'json') {
|
|
780
941
|
const retryMax = nodeSpec.output_retry ?? 1;
|
|
@@ -787,7 +948,7 @@ class RunManager {
|
|
|
787
948
|
retryInput = finalInput +
|
|
788
949
|
`\n\n## ⚠️ PREVIOUS OUTPUT FAILED JSON.parse: ${lastError.message}\n` +
|
|
789
950
|
`Return STRICT JSON ONLY. First char "{", last char "}". No prose, no fences.`;
|
|
790
|
-
const retryPrompt = `@${
|
|
951
|
+
const retryPrompt = `@${resolvedAgent} ${retryInput}`;
|
|
791
952
|
const retryArgs = [...baseArgs, mode, retryPrompt];
|
|
792
953
|
if (process.env.CREWX_WORKFLOW_THREAD === 'on') {
|
|
793
954
|
retryArgs.push(`--thread=workflow:${snapshot.id}`);
|
|
@@ -795,9 +956,10 @@ class RunManager {
|
|
|
795
956
|
retryArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
796
957
|
const retryInvocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, retryArgs);
|
|
797
958
|
const retryResult = await runProcess(retryInvocation.command, retryInvocation.argv, {
|
|
798
|
-
cwd:
|
|
959
|
+
cwd: nodeCwd,
|
|
799
960
|
env: {
|
|
800
961
|
...process.env,
|
|
962
|
+
CREWX_WORKSPACE: workspaceRoot,
|
|
801
963
|
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
802
964
|
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
803
965
|
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
@@ -817,6 +979,10 @@ class RunManager {
|
|
|
817
979
|
throw new Error(`Agent retry failed (exit ${retryResult.status}): ${retryResult.stderr.trim()}`);
|
|
818
980
|
}
|
|
819
981
|
attemptOutput = retryResult.stdout.trim();
|
|
982
|
+
const retrySignal = (0, output_format_1.detectWorkflowSignal)(attemptOutput);
|
|
983
|
+
if (retrySignal) {
|
|
984
|
+
throw new Error(`Node "${nodeId}" agent signaled failure: ${retrySignal.reason ?? '(no reason given)'}`);
|
|
985
|
+
}
|
|
820
986
|
}
|
|
821
987
|
try {
|
|
822
988
|
parsed = JSON.parse((0, output_format_1.extractJson)(attemptOutput));
|
|
@@ -851,9 +1017,16 @@ class RunManager {
|
|
|
851
1017
|
}
|
|
852
1018
|
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
853
1019
|
const skillArgs = (nodeSpec.args ?? []).map((a) => interpolateTemplate(String(a), ctx));
|
|
1020
|
+
const skillStdin = nodeSpec.stdin !== undefined
|
|
1021
|
+
? interpolateTemplate(String(nodeSpec.stdin), ctx)
|
|
1022
|
+
: undefined;
|
|
854
1023
|
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
855
1024
|
if (!cwdCheck.ok)
|
|
856
1025
|
throw new Error(`Node "${nodeId}": ${cwdCheck.error}`);
|
|
1026
|
+
const skillRootCheck = (0, shell_security_1.normalizeCwd)(undefined, this.projectRoot);
|
|
1027
|
+
if (!skillRootCheck.ok)
|
|
1028
|
+
throw new Error(`Node "${nodeId}": ${skillRootCheck.error}`);
|
|
1029
|
+
const skillWorkspaceRoot = skillRootCheck.resolved;
|
|
857
1030
|
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
858
1031
|
if (opts?.dryRun) {
|
|
859
1032
|
console.log(`[dry-run] Would execute: crewx skill ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
@@ -875,6 +1048,7 @@ class RunManager {
|
|
|
875
1048
|
cwd: cwdCheck.resolved,
|
|
876
1049
|
env: {
|
|
877
1050
|
...process.env,
|
|
1051
|
+
CREWX_WORKSPACE: skillWorkspaceRoot,
|
|
878
1052
|
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
879
1053
|
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
880
1054
|
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
@@ -882,6 +1056,7 @@ class RunManager {
|
|
|
882
1056
|
timeout,
|
|
883
1057
|
shell: invocation.shell ?? false,
|
|
884
1058
|
windowsHide: invocation.windowsHide,
|
|
1059
|
+
stdin: skillStdin,
|
|
885
1060
|
});
|
|
886
1061
|
nodeStdout = result.stdout;
|
|
887
1062
|
nodeStderr = result.stderr;
|
|
@@ -932,11 +1107,15 @@ class RunManager {
|
|
|
932
1107
|
childEnv.CREWX_WORKFLOW_EXEC_ID = snapshot.id;
|
|
933
1108
|
childEnv.CREWX_WORKFLOW_NODE_ID = nodeId;
|
|
934
1109
|
childEnv.CREWX_WORKFLOW_ID = snapshot.workflow_id;
|
|
1110
|
+
const shellStdin = nodeSpec.stdin !== undefined
|
|
1111
|
+
? interpolateTemplate(String(nodeSpec.stdin), ctx)
|
|
1112
|
+
: undefined;
|
|
935
1113
|
const result = await runProcess(command[0], command.slice(1), {
|
|
936
1114
|
cwd: cwdCheck.resolved,
|
|
937
1115
|
env: childEnv,
|
|
938
1116
|
timeout,
|
|
939
1117
|
shell: false,
|
|
1118
|
+
stdin: shellStdin,
|
|
940
1119
|
});
|
|
941
1120
|
nodeStdout = result.stdout;
|
|
942
1121
|
nodeStderr = result.stderr;
|
|
@@ -986,6 +1165,15 @@ class RunManager {
|
|
|
986
1165
|
console.log(`End "${nodeId}": workflow execution completed`);
|
|
987
1166
|
break;
|
|
988
1167
|
}
|
|
1168
|
+
case 'error': {
|
|
1169
|
+
status = 'failed';
|
|
1170
|
+
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
1171
|
+
failureMessage = nodeSpec.message
|
|
1172
|
+
? interpolateTemplate(nodeSpec.message, ctx)
|
|
1173
|
+
: `Workflow failed at error node "${nodeId}"`;
|
|
1174
|
+
console.log(`Error "${nodeId}": workflow execution failed — ${failureMessage}`);
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
989
1177
|
case 'expression': {
|
|
990
1178
|
if (!nodeSpec.set || typeof nodeSpec.set !== 'object') {
|
|
991
1179
|
throw new Error(`Expression node "${nodeId}" requires "set" field`);
|
|
@@ -1009,6 +1197,14 @@ class RunManager {
|
|
|
1009
1197
|
default:
|
|
1010
1198
|
console.log(`Unknown node type "${nodeSpec.type}" for node "${nodeId}"`);
|
|
1011
1199
|
}
|
|
1200
|
+
if (nodeSpec.fail_when &&
|
|
1201
|
+
!opts?.dryRun &&
|
|
1202
|
+
(nodeSpec.type === 'agent_task' || nodeSpec.type === 'skill_task' || nodeSpec.type === 'shell_task')) {
|
|
1203
|
+
const judged = safeEvaluateBranchCondition(String(nodeSpec.fail_when), { ...snapshot.state, ...stateUpdates });
|
|
1204
|
+
if (judged === true || judged === 'true') {
|
|
1205
|
+
throw new Error(`Node "${nodeId}" judged failed by fail_when: ${nodeSpec.fail_when}`);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1012
1208
|
}
|
|
1013
1209
|
catch (e) {
|
|
1014
1210
|
const errMessage = e.message;
|
|
@@ -1020,6 +1216,8 @@ class RunManager {
|
|
|
1020
1216
|
await this.atomicUpdateAsync(execId, (run) => {
|
|
1021
1217
|
if (run.status === 'cancelled')
|
|
1022
1218
|
return;
|
|
1219
|
+
Object.assign(run.state, stateUpdates);
|
|
1220
|
+
delete run.manual_resume;
|
|
1023
1221
|
run.current_node = nodeId;
|
|
1024
1222
|
run.status = 'failed';
|
|
1025
1223
|
run.error = errMessage;
|
|
@@ -1033,6 +1231,7 @@ class RunManager {
|
|
|
1033
1231
|
if (run.status === 'cancelled')
|
|
1034
1232
|
return;
|
|
1035
1233
|
Object.assign(run.state, stateUpdates);
|
|
1234
|
+
delete run.manual_resume;
|
|
1036
1235
|
for (const node of nodesToComplete) {
|
|
1037
1236
|
if (!run.completed_nodes.includes(node)) {
|
|
1038
1237
|
run.completed_nodes.push(node);
|
|
@@ -1042,8 +1241,18 @@ class RunManager {
|
|
|
1042
1241
|
run.completed_nodes.push(prevNode);
|
|
1043
1242
|
}
|
|
1044
1243
|
run.current_node = currentNode;
|
|
1045
|
-
if (status)
|
|
1244
|
+
if (status) {
|
|
1046
1245
|
run.status = status;
|
|
1246
|
+
if (status !== 'failed') {
|
|
1247
|
+
delete run.error;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
else if (run.status === 'failed') {
|
|
1251
|
+
run.status = 'running';
|
|
1252
|
+
delete run.error;
|
|
1253
|
+
}
|
|
1254
|
+
if (failureMessage)
|
|
1255
|
+
run.error = failureMessage;
|
|
1047
1256
|
if (nodeTaskId) {
|
|
1048
1257
|
run.tasks = run.tasks ?? {};
|
|
1049
1258
|
run.tasks[nodeId] = nodeTaskId;
|
|
@@ -1052,92 +1261,155 @@ class RunManager {
|
|
|
1052
1261
|
run.audit.push(successAudit);
|
|
1053
1262
|
});
|
|
1054
1263
|
}
|
|
1055
|
-
async
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
outcome: initial.status === 'completed' ? 'completed' : initial.status === 'cancelled' ? 'cancelled' : 'failed',
|
|
1071
|
-
reason: `run already ${initial.status}`,
|
|
1072
|
-
executed,
|
|
1264
|
+
async claimAutoRunLease(execId, nodeId, preferredToken) {
|
|
1265
|
+
const updated = await this.atomicUpdateAsync(execId, (run) => {
|
|
1266
|
+
const existing = run.auto_run;
|
|
1267
|
+
const ownedByThisProcess = existing?.pid === process.pid && typeof existing.token === 'string';
|
|
1268
|
+
if (existing && !ownedByThisProcess && isProcessAlive(existing.pid)) {
|
|
1269
|
+
throw new Error(`Workflow run "${execId}" is already owned by another runner process`);
|
|
1270
|
+
}
|
|
1271
|
+
const token = preferredToken ?? (ownedByThisProcess ? existing.token : (0, sdk_1.generateId)('wfr'));
|
|
1272
|
+
run.auto_run = {
|
|
1273
|
+
pid: process.pid,
|
|
1274
|
+
token,
|
|
1275
|
+
node_id: nodeId,
|
|
1276
|
+
started_at: ownedByThisProcess && existing.token === token
|
|
1277
|
+
? existing.started_at
|
|
1278
|
+
: new Date().toISOString(),
|
|
1073
1279
|
};
|
|
1280
|
+
delete run.manual_resume;
|
|
1281
|
+
});
|
|
1282
|
+
return updated.auto_run.token;
|
|
1283
|
+
}
|
|
1284
|
+
async releaseAutoRunLease(execId, token) {
|
|
1285
|
+
await this.atomicUpdateAsync(execId, (run) => {
|
|
1286
|
+
if (run.auto_run?.token === token)
|
|
1287
|
+
delete run.auto_run;
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
async runAuto(execId) {
|
|
1291
|
+
const leaseKey = `${this.resolvedDir}::${execId}`;
|
|
1292
|
+
if (inFlightAutoRuns.has(leaseKey)) {
|
|
1293
|
+
const run = this.loadRun(execId);
|
|
1294
|
+
if (!run)
|
|
1295
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1296
|
+
console.log(`runAuto "${execId}": already in-flight — skipping duplicate auto-run (single-flight guard)`);
|
|
1297
|
+
return { run, outcome: 'paused', reason: 'execution already in-flight (single-flight guard)', executed: [] };
|
|
1074
1298
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
run = this.loadRun(execId);
|
|
1084
|
-
if (run.status === 'completed')
|
|
1085
|
-
return { run, outcome: 'completed', executed };
|
|
1086
|
-
if (run.status === 'failed')
|
|
1087
|
-
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1088
|
-
if (run.status === 'cancelled')
|
|
1089
|
-
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1090
|
-
const nodeId = run.current_node || firstNode;
|
|
1091
|
-
if (!nodeId)
|
|
1092
|
-
return { run, outcome: 'failed', reason: 'workflow has no nodes', executed };
|
|
1093
|
-
const node = spec.nodes[nodeId];
|
|
1094
|
-
if (!node) {
|
|
1095
|
-
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1096
|
-
r.current_node = nodeId;
|
|
1097
|
-
r.status = 'failed';
|
|
1098
|
-
r.error = `node "${nodeId}" not found in workflow`;
|
|
1099
|
-
});
|
|
1100
|
-
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1101
|
-
}
|
|
1102
|
-
if (node.type === 'approval') {
|
|
1103
|
-
return { run, outcome: 'paused', reason: `approval node "${nodeId}" requires manual decision`, executed };
|
|
1299
|
+
inFlightAutoRuns.add(leaseKey);
|
|
1300
|
+
let runnerToken;
|
|
1301
|
+
try {
|
|
1302
|
+
const initial = this.loadRun(execId);
|
|
1303
|
+
if (!initial)
|
|
1304
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1305
|
+
if (initial.auto_run?.pid === process.pid) {
|
|
1306
|
+
runnerToken = initial.auto_run.token;
|
|
1104
1307
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1308
|
+
const doc = this.loadWorkflowYaml(initial.workflow_file);
|
|
1309
|
+
if (!doc)
|
|
1310
|
+
throw new Error(`Failed to load workflow file: ${initial.workflow_file}`);
|
|
1311
|
+
const spec = doc.workflows[initial.workflow_id];
|
|
1312
|
+
if (!spec) {
|
|
1313
|
+
throw new Error(`Workflow "${initial.workflow_id}" not found in ${initial.workflow_file}`);
|
|
1107
1314
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1315
|
+
const executed = [];
|
|
1316
|
+
if (initial.status === 'completed' || initial.status === 'failed' || initial.status === 'cancelled') {
|
|
1317
|
+
return {
|
|
1318
|
+
run: initial,
|
|
1319
|
+
outcome: initial.status === 'completed' ? 'completed' : initial.status === 'cancelled' ? 'cancelled' : 'failed',
|
|
1320
|
+
reason: `run already ${initial.status}`,
|
|
1321
|
+
executed,
|
|
1322
|
+
};
|
|
1110
1323
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1324
|
+
const firstNode = Object.keys(spec.nodes ?? {})[0];
|
|
1325
|
+
const maxIterations = (typeof spec.max_iterations === 'number' && spec.max_iterations > 0)
|
|
1326
|
+
? spec.max_iterations
|
|
1327
|
+
: 30;
|
|
1328
|
+
let run = initial;
|
|
1329
|
+
while (true) {
|
|
1330
|
+
run = this.loadRun(execId);
|
|
1331
|
+
if (run.status === 'completed')
|
|
1332
|
+
return { run, outcome: 'completed', executed };
|
|
1333
|
+
if (run.status === 'failed')
|
|
1334
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1335
|
+
if (run.status === 'cancelled')
|
|
1336
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1337
|
+
const nodeId = run.current_node || firstNode;
|
|
1338
|
+
if (!nodeId)
|
|
1339
|
+
return { run, outcome: 'failed', reason: 'workflow has no nodes', executed };
|
|
1340
|
+
const node = spec.nodes[nodeId];
|
|
1341
|
+
if (!node) {
|
|
1342
|
+
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1343
|
+
r.current_node = nodeId;
|
|
1344
|
+
r.status = 'failed';
|
|
1345
|
+
r.error = `node "${nodeId}" not found in workflow`;
|
|
1346
|
+
});
|
|
1347
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1348
|
+
}
|
|
1349
|
+
if (node.type === 'approval') {
|
|
1350
|
+
if (run.current_node !== nodeId) {
|
|
1351
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1352
|
+
}
|
|
1353
|
+
return { run, outcome: 'paused', reason: `approval node "${nodeId}" requires manual decision`, executed };
|
|
1354
|
+
}
|
|
1355
|
+
if (node.type === 'parallel') {
|
|
1356
|
+
if (run.current_node !== nodeId) {
|
|
1357
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1358
|
+
}
|
|
1359
|
+
return { run, outcome: 'paused', reason: `parallel auto-run not implemented (node "${nodeId}")`, executed };
|
|
1360
|
+
}
|
|
1361
|
+
if (!KNOWN_AUTO_NODE_TYPES.has(node.type)) {
|
|
1362
|
+
if (run.current_node !== nodeId) {
|
|
1363
|
+
run = await this.atomicUpdateAsync(execId, (r) => { r.current_node = nodeId; });
|
|
1364
|
+
}
|
|
1365
|
+
return { run, outcome: 'paused', reason: `unknown node type "${node.type}" (node "${nodeId}")`, executed };
|
|
1366
|
+
}
|
|
1367
|
+
runnerToken = await this.claimAutoRunLease(execId, nodeId, runnerToken);
|
|
1114
1368
|
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1115
|
-
|
|
1116
|
-
|
|
1369
|
+
const engine = readRunEngineState(r);
|
|
1370
|
+
engine.visitCounts[nodeId] = (engine.visitCounts[nodeId] ?? 0) + 1;
|
|
1371
|
+
engine.iterations += 1;
|
|
1372
|
+
r.state['__engine'] = engine;
|
|
1373
|
+
if (engine.visitCounts[nodeId] > maxIterations || engine.iterations > maxIterations) {
|
|
1374
|
+
r.status = 'failed';
|
|
1375
|
+
r.error = `auto runner exceeded max_iterations (${maxIterations}); possible loop at "${nodeId}"`;
|
|
1376
|
+
}
|
|
1117
1377
|
});
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1378
|
+
if (run.status === 'failed') {
|
|
1379
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1380
|
+
}
|
|
1381
|
+
try {
|
|
1382
|
+
run = await this.executeNode(execId, nodeId, { trigger: 'auto' });
|
|
1383
|
+
executed.push(nodeId);
|
|
1384
|
+
}
|
|
1385
|
+
catch (e) {
|
|
1386
|
+
run = this.loadRun(execId);
|
|
1387
|
+
return { run, outcome: 'failed', reason: e.message, executed };
|
|
1388
|
+
}
|
|
1389
|
+
if (run.status === 'completed')
|
|
1390
|
+
return { run, outcome: 'completed', executed };
|
|
1391
|
+
if (run.status === 'failed')
|
|
1392
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1393
|
+
if (run.status === 'cancelled')
|
|
1394
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1395
|
+
if (!SELF_ADVANCING_NODE_TYPES.has(node.type)) {
|
|
1396
|
+
const next = node.next;
|
|
1397
|
+
if (!next) {
|
|
1398
|
+
return { run, outcome: 'paused', reason: `node "${nodeId}" has no "next"; stopping`, executed };
|
|
1399
|
+
}
|
|
1400
|
+
run = this.moveNode(execId, next);
|
|
1401
|
+
}
|
|
1127
1402
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
if (
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
const next = node.next;
|
|
1136
|
-
if (!next) {
|
|
1137
|
-
return { run, outcome: 'paused', reason: `node "${nodeId}" has no "next"; stopping`, executed };
|
|
1403
|
+
}
|
|
1404
|
+
finally {
|
|
1405
|
+
if (runnerToken) {
|
|
1406
|
+
try {
|
|
1407
|
+
await this.releaseAutoRunLease(execId, runnerToken);
|
|
1408
|
+
}
|
|
1409
|
+
catch {
|
|
1138
1410
|
}
|
|
1139
|
-
run = this.moveNode(execId, next);
|
|
1140
1411
|
}
|
|
1412
|
+
inFlightAutoRuns.delete(leaseKey);
|
|
1141
1413
|
}
|
|
1142
1414
|
}
|
|
1143
1415
|
cancelRun(execId, reason = 'Cancelled by user') {
|
|
@@ -1148,6 +1420,8 @@ class RunManager {
|
|
|
1148
1420
|
const now = new Date();
|
|
1149
1421
|
run.status = 'cancelled';
|
|
1150
1422
|
run.error = reason;
|
|
1423
|
+
delete run.auto_run;
|
|
1424
|
+
delete run.manual_resume;
|
|
1151
1425
|
run.audit = run.audit ?? [];
|
|
1152
1426
|
run.audit.push({
|
|
1153
1427
|
exec_id: run.id,
|
|
@@ -1168,6 +1442,57 @@ class RunManager {
|
|
|
1168
1442
|
run.state = JSON.parse(JSON.stringify(run.initial_state));
|
|
1169
1443
|
run.current_node = '';
|
|
1170
1444
|
run.completed_nodes = [];
|
|
1445
|
+
delete run.auto_run;
|
|
1446
|
+
delete run.manual_resume;
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
retry(execId, options = {}) {
|
|
1450
|
+
const snapshot = this.loadRun(execId);
|
|
1451
|
+
if (!snapshot)
|
|
1452
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1453
|
+
const doc = this.loadWorkflowYaml(snapshot.workflow_file);
|
|
1454
|
+
if (!doc)
|
|
1455
|
+
throw new Error(`Failed to load workflow file: ${snapshot.workflow_file}`);
|
|
1456
|
+
const spec = doc.workflows[snapshot.workflow_id];
|
|
1457
|
+
if (!spec)
|
|
1458
|
+
throw new Error(`Workflow "${snapshot.workflow_id}" not found`);
|
|
1459
|
+
if (snapshot.status !== 'failed') {
|
|
1460
|
+
throw new Error(`Run "${execId}" is not failed (status=${snapshot.status ?? 'running'}); only failed runs can be retried`);
|
|
1461
|
+
}
|
|
1462
|
+
if (!isRetryableFailure(snapshot, spec)) {
|
|
1463
|
+
throw new Error(`Run "${execId}" failed at a non-retryable node "${snapshot.current_node}" (error node); use reset to restart`);
|
|
1464
|
+
}
|
|
1465
|
+
return this.atomicUpdate(execId, (run) => {
|
|
1466
|
+
const failedNode = run.current_node;
|
|
1467
|
+
delete run.status;
|
|
1468
|
+
delete run.error;
|
|
1469
|
+
delete run.manual_resume;
|
|
1470
|
+
run.retry_count = (run.retry_count ?? 0) + 1;
|
|
1471
|
+
run.audit = run.audit ?? [];
|
|
1472
|
+
const now = new Date().toISOString();
|
|
1473
|
+
if (options.autoRun === false) {
|
|
1474
|
+
delete run.auto_run;
|
|
1475
|
+
run.manual_resume = { node_id: failedNode, requested_at: now };
|
|
1476
|
+
}
|
|
1477
|
+
else {
|
|
1478
|
+
run.auto_run = {
|
|
1479
|
+
pid: process.pid,
|
|
1480
|
+
token: (0, sdk_1.generateId)('wfr'),
|
|
1481
|
+
node_id: failedNode,
|
|
1482
|
+
started_at: now,
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
run.audit.push({
|
|
1486
|
+
exec_id: run.id,
|
|
1487
|
+
node_id: failedNode,
|
|
1488
|
+
type: 'retry',
|
|
1489
|
+
summary: `manual retry #${run.retry_count} of node "${failedNode}"`,
|
|
1490
|
+
started_at: now,
|
|
1491
|
+
ended_at: now,
|
|
1492
|
+
duration_ms: 0,
|
|
1493
|
+
exit_code: null,
|
|
1494
|
+
trigger: 'manual',
|
|
1495
|
+
});
|
|
1171
1496
|
});
|
|
1172
1497
|
}
|
|
1173
1498
|
listRuns(opts) {
|
|
@@ -1176,7 +1501,10 @@ class RunManager {
|
|
|
1176
1501
|
const runs = [];
|
|
1177
1502
|
for (const file of files) {
|
|
1178
1503
|
try {
|
|
1179
|
-
const
|
|
1504
|
+
const execId = file.slice(0, -'.json'.length);
|
|
1505
|
+
const run = this.loadRun(execId);
|
|
1506
|
+
if (!run)
|
|
1507
|
+
continue;
|
|
1180
1508
|
if (opts?.workflowId && run.workflow_id !== opts.workflowId)
|
|
1181
1509
|
continue;
|
|
1182
1510
|
runs.push(run);
|