@tt-a1i/hive 2.0.2 → 2.1.0
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/CHANGELOG.md +33 -0
- package/README.en.md +15 -6
- package/README.md +26 -4
- package/dist/src/cli/hive.d.ts +4 -0
- package/dist/src/cli/hive.js +25 -3
- package/dist/src/cli/team.d.ts +8 -1
- package/dist/src/cli/team.js +111 -11
- package/dist/src/server/action-center-summary.d.ts +193 -0
- package/dist/src/server/action-center-summary.js +188 -0
- package/dist/src/server/agent-command-resolver.d.ts +6 -0
- package/dist/src/server/agent-command-resolver.js +16 -0
- package/dist/src/server/agent-manager.js +11 -1
- package/dist/src/server/agent-run-starter.js +47 -6
- package/dist/src/server/agent-runtime-types.d.ts +4 -0
- package/dist/src/server/agent-startup-instructions.d.ts +4 -0
- package/dist/src/server/agent-startup-instructions.js +35 -9
- package/dist/src/server/agent-stdin-dispatcher.js +17 -9
- package/dist/src/server/diagnostics-support-bundle.d.ts +288 -0
- package/dist/src/server/diagnostics-support-bundle.js +179 -0
- package/dist/src/server/dispatch-ledger-store.d.ts +4 -1
- package/dist/src/server/dispatch-ledger-store.js +46 -6
- package/dist/src/server/hive-envelope-escape.d.ts +2 -0
- package/dist/src/server/hive-envelope-escape.js +2 -0
- package/dist/src/server/hive-team-guidance.d.ts +1 -1
- package/dist/src/server/hive-team-guidance.js +67 -25
- package/dist/src/server/message-log-store.d.ts +1 -1
- package/dist/src/server/post-start-input-writer.js +8 -2
- package/dist/src/server/preset-launch-support.d.ts +2 -0
- package/dist/src/server/preset-launch-support.js +65 -2
- package/dist/src/server/protocol-event-stats.d.ts +39 -0
- package/dist/src/server/protocol-event-stats.js +84 -0
- package/dist/src/server/recovery-summary.js +19 -14
- package/dist/src/server/role-template-store.d.ts +1 -1
- package/dist/src/server/role-templates.d.ts +1 -0
- package/dist/src/server/role-templates.js +43 -29
- package/dist/src/server/routes-action-center.d.ts +2 -0
- package/dist/src/server/routes-action-center.js +37 -0
- package/dist/src/server/routes-diagnostics.d.ts +2 -0
- package/dist/src/server/routes-diagnostics.js +17 -0
- package/dist/src/server/routes-scenarios.d.ts +25 -0
- package/dist/src/server/routes-scenarios.js +89 -0
- package/dist/src/server/routes-settings.js +2 -11
- package/dist/src/server/routes-team-memory.js +52 -0
- package/dist/src/server/routes-team.js +40 -20
- package/dist/src/server/routes-workspace-memory-dreams.js +8 -0
- package/dist/src/server/routes-workspace-uploads.d.ts +2 -0
- package/dist/src/server/routes-workspace-uploads.js +154 -0
- package/dist/src/server/routes-workspaces.js +29 -3
- package/dist/src/server/routes.js +8 -0
- package/dist/src/server/runtime-message-builders.d.ts +0 -1
- package/dist/src/server/runtime-message-builders.js +0 -8
- package/dist/src/server/runtime-store-contract.d.ts +15 -0
- package/dist/src/server/runtime-store-dream.d.ts +14 -1
- package/dist/src/server/runtime-store-dream.js +49 -1
- package/dist/src/server/runtime-store-helpers.d.ts +7 -0
- package/dist/src/server/runtime-store-helpers.js +85 -22
- package/dist/src/server/runtime-store-worker-mutations.d.ts +11 -0
- package/dist/src/server/runtime-store-worker-mutations.js +46 -0
- package/dist/src/server/runtime-store-workflows.js +10 -6
- package/dist/src/server/runtime-store.js +34 -42
- package/dist/src/server/scenario-presets.d.ts +25 -0
- package/dist/src/server/scenario-presets.js +35 -0
- package/dist/src/server/sentinel-heartbeat.d.ts +30 -0
- package/dist/src/server/sentinel-heartbeat.js +145 -0
- package/dist/src/server/spawn-cli-resolver.d.ts +37 -0
- package/dist/src/server/spawn-cli-resolver.js +70 -0
- package/dist/src/server/spawn-worker-defaults.d.ts +13 -0
- package/dist/src/server/spawn-worker-defaults.js +45 -0
- package/dist/src/server/sqlite-schema-v32.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v32.js +17 -0
- package/dist/src/server/sqlite-schema-v33.d.ts +3 -0
- package/dist/src/server/sqlite-schema-v33.js +18 -0
- package/dist/src/server/sqlite-schema-v34.d.ts +11 -0
- package/dist/src/server/sqlite-schema-v34.js +19 -0
- package/dist/src/server/sqlite-schema-v35.d.ts +3 -0
- package/dist/src/server/sqlite-schema-v35.js +23 -0
- package/dist/src/server/sqlite-schema.d.ts +1 -1
- package/dist/src/server/sqlite-schema.js +35 -1
- package/dist/src/server/system-message.d.ts +5 -2
- package/dist/src/server/system-message.js +5 -2
- package/dist/src/server/tasks-file-watcher.d.ts +8 -0
- package/dist/src/server/tasks-file-watcher.js +31 -2
- package/dist/src/server/team-authz.d.ts +9 -1
- package/dist/src/server/team-authz.js +24 -0
- package/dist/src/server/team-list-serializer.d.ts +2 -2
- package/dist/src/server/team-list-serializer.js +2 -1
- package/dist/src/server/team-memory-digest.js +4 -4
- package/dist/src/server/team-memory-dream-applier.js +24 -3
- package/dist/src/server/team-memory-dream-prompt.d.ts +13 -0
- package/dist/src/server/team-memory-dream-prompt.js +91 -0
- package/dist/src/server/team-memory-dream-run-store.d.ts +2 -0
- package/dist/src/server/team-memory-dream-run-store.js +14 -4
- package/dist/src/server/team-memory-dream-runner.d.ts +2 -21
- package/dist/src/server/team-memory-dream-runner.js +3 -148
- package/dist/src/server/team-memory-dream-store.d.ts +1 -1
- package/dist/src/server/team-memory-dream-store.js +1 -1
- package/dist/src/server/team-operations.d.ts +18 -2
- package/dist/src/server/team-operations.js +222 -33
- package/dist/src/server/team-recap.d.ts +10 -0
- package/dist/src/server/team-recap.js +73 -0
- package/dist/src/server/terminal-input-profile.js +88 -9
- package/dist/src/server/upload-limits.d.ts +2 -0
- package/dist/src/server/upload-limits.js +2 -0
- package/dist/src/server/workflow-cli-policy.d.ts +7 -2
- package/dist/src/server/workflow-cli-policy.js +15 -3
- package/dist/src/server/workflow-run-store.d.ts +1 -0
- package/dist/src/server/workflow-run-store.js +11 -1
- package/dist/src/server/workflow-runner.d.ts +4 -1
- package/dist/src/server/workflow-runner.js +418 -118
- package/dist/src/server/workflow-script-loader.d.ts +3 -2
- package/dist/src/server/workflow-script-loader.js +161 -0
- package/dist/src/server/workspace-store-contract.d.ts +2 -0
- package/dist/src/server/workspace-store.d.ts +1 -1
- package/dist/src/server/workspace-store.js +40 -30
- package/dist/src/server/workspace-upload-store.d.ts +40 -0
- package/dist/src/server/workspace-upload-store.js +295 -0
- package/dist/src/shared/scenario-presets.d.ts +32 -0
- package/dist/src/shared/scenario-presets.js +69 -0
- package/dist/src/shared/types.d.ts +12 -1
- package/package.json +1 -1
- package/web/dist/assets/AddWorkerDialog-DBLhwb91.js +2 -0
- package/web/dist/assets/AddWorkspaceFlow-cxvhVAsT.js +1 -0
- package/web/dist/assets/FirstRunWizard-DlEPnWWw.js +1 -0
- package/web/dist/assets/{MarketplaceDrawer-Dd8WIA8T.js → MarketplaceDrawer-CfSiRi8e.js} +11 -11
- package/web/dist/assets/TaskGraphDrawer-C2JufcPs.js +1 -0
- package/web/dist/assets/WhatsNewDialog-vP7buLos.js +1 -0
- package/web/dist/assets/WorkerModal-CSorwcdP.js +1 -0
- package/web/dist/assets/{WorkflowsDrawer-Bjf4olbR.js → WorkflowsDrawer-BXS3w9Uq.js} +1 -1
- package/web/dist/assets/WorkspaceMemoryDrawer-D71ivohr.js +1 -0
- package/web/dist/assets/{WorkspaceTaskDrawer-BIWwISvA.js → WorkspaceTaskDrawer-CGCTSHKa.js} +1 -1
- package/web/dist/assets/index-BcwN8cCw.js +79 -0
- package/web/dist/assets/index-StXTPHls.css +1 -0
- package/web/dist/assets/{search-Bk2HQvO7.js → search-BZw4T67h.js} +1 -1
- package/web/dist/assets/{square-terminal-D93m9hfY.js → square-terminal-B7E57In1.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +1 -1
- package/dist/src/server/env-sync-message.d.ts +0 -9
- package/dist/src/server/env-sync-message.js +0 -29
- package/web/dist/assets/AddWorkerDialog-CbV75qUX.js +0 -2
- package/web/dist/assets/AddWorkspaceFlow-CwV-7wPx.js +0 -1
- package/web/dist/assets/FirstRunWizard-a6PWIK3x.js +0 -1
- package/web/dist/assets/TaskGraphDrawer-Bk5WFIk_.js +0 -1
- package/web/dist/assets/WhatsNewDialog-C2VZaip0.js +0 -1
- package/web/dist/assets/WorkerModal-DucW-9YT.js +0 -1
- package/web/dist/assets/WorkspaceMemoryDrawer-DglCy_5f.js +0 -1
- package/web/dist/assets/index-BAiLYajK.css +0 -1
- package/web/dist/assets/index-BV2k9Dts.js +0 -73
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { cpus } from 'node:os';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
+
import { Worker } from 'node:worker_threads';
|
|
4
5
|
import { assertWindowsSafeFilename } from './windows-filename.js';
|
|
5
6
|
import { resolveWorkflowCli } from './workflow-cli-policy.js';
|
|
6
7
|
import { buildSchemaInstruction, extractJsonBlock } from './workflow-output-schema.js';
|
|
@@ -14,6 +15,152 @@ import { getWorkflowAgentId } from './workspace-store-support.js';
|
|
|
14
15
|
const DEFAULT_MAX_AGENTS_PER_RUN = 1000;
|
|
15
16
|
const DEFAULT_MAX_DURATION_MS = 60 * 60 * 1000;
|
|
16
17
|
const DEFAULT_MAX_CONCURRENT_AGENTS = Math.min(16, Math.max(2, cpus().length - 2));
|
|
18
|
+
const WORKFLOW_VM_WORKER_SOURCE = `
|
|
19
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
20
|
+
const { Script, createContext } = require('node:vm');
|
|
21
|
+
|
|
22
|
+
const BRIDGE_FACTORY = new Script(\`"use strict";
|
|
23
|
+
((hostCall) => {
|
|
24
|
+
const stringify = JSON.stringify;
|
|
25
|
+
const parse = JSON.parse;
|
|
26
|
+
const promiseResolve = Promise.resolve.bind(Promise);
|
|
27
|
+
const promiseThen = Promise.prototype.then;
|
|
28
|
+
return (...args) =>
|
|
29
|
+
promiseThen.call(promiseResolve(hostCall(stringify(args))), (payloadJson) => {
|
|
30
|
+
const payload = parse(payloadJson);
|
|
31
|
+
if (!payload.ok) throw new Error(payload.error || 'Hive workflow host call failed');
|
|
32
|
+
return payload.hasValue ? payload.value : undefined;
|
|
33
|
+
});
|
|
34
|
+
})\`);
|
|
35
|
+
const FLOW_FACTORY = new Script(\`"use strict";
|
|
36
|
+
((catchPerItem) => {
|
|
37
|
+
const promiseAll = Promise.all.bind(Promise);
|
|
38
|
+
const promiseResolve = Promise.resolve.bind(Promise);
|
|
39
|
+
const promiseThen = Promise.prototype.then;
|
|
40
|
+
const promiseCatch = Promise.prototype.catch;
|
|
41
|
+
const parallel = (thunks) =>
|
|
42
|
+
promiseAll(Array.from(thunks).map((thunk) =>
|
|
43
|
+
promiseCatch.call(promiseThen.call(promiseResolve(), () => thunk()), (err) => catchPerItem(err))
|
|
44
|
+
));
|
|
45
|
+
const pipeline = (items, ...stages) =>
|
|
46
|
+
promiseAll(Array.from(items).map((item, index) => {
|
|
47
|
+
let chain = promiseResolve(item);
|
|
48
|
+
for (const stage of stages) {
|
|
49
|
+
chain = promiseThen.call(chain, (prev) => stage(prev, item, index));
|
|
50
|
+
}
|
|
51
|
+
return promiseCatch.call(chain, (err) => catchPerItem(err));
|
|
52
|
+
}));
|
|
53
|
+
return { parallel, pipeline };
|
|
54
|
+
})\`);
|
|
55
|
+
const JSON_PARSE = new Script('JSON.parse');
|
|
56
|
+
|
|
57
|
+
const encodeSuccess = (value) => {
|
|
58
|
+
const payload = { ok: true, hasValue: value !== undefined };
|
|
59
|
+
if (value !== undefined) payload.value = value;
|
|
60
|
+
return JSON.stringify(payload);
|
|
61
|
+
};
|
|
62
|
+
const encodeFailure = (error) =>
|
|
63
|
+
JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
64
|
+
|
|
65
|
+
const createSafeHostCall = (fn) =>
|
|
66
|
+
new Proxy(
|
|
67
|
+
(serializedArgs) => {
|
|
68
|
+
let args;
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(serializedArgs);
|
|
71
|
+
args = Array.isArray(parsed) ? parsed : [];
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return encodeFailure(error);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return Promise.resolve(fn(args)).then(encodeSuccess, encodeFailure);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return encodeFailure(error);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
get: () => undefined,
|
|
83
|
+
getOwnPropertyDescriptor: () => undefined,
|
|
84
|
+
getPrototypeOf: () => null,
|
|
85
|
+
has: () => false,
|
|
86
|
+
ownKeys: () => [],
|
|
87
|
+
set: () => false,
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
let nextCallId = 0;
|
|
92
|
+
const pending = new Map();
|
|
93
|
+
|
|
94
|
+
parentPort.on('message', (message) => {
|
|
95
|
+
if (!message || message.type !== 'hostResponse') return;
|
|
96
|
+
const entry = pending.get(message.id);
|
|
97
|
+
if (!entry) return;
|
|
98
|
+
pending.delete(message.id);
|
|
99
|
+
if (message.ok) entry.resolve(message.value);
|
|
100
|
+
else entry.reject(new Error(message.error || 'Hive workflow host call failed'));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const callHost = (name, args) =>
|
|
104
|
+
new Promise((resolve, reject) => {
|
|
105
|
+
const id = String(++nextCallId);
|
|
106
|
+
pending.set(id, { resolve, reject });
|
|
107
|
+
parentPort.postMessage({ type: 'hostCall', id, name, args });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const cloneIntoVm = (context, value) => {
|
|
111
|
+
if (value === undefined) return undefined;
|
|
112
|
+
const serialized = JSON.stringify(value);
|
|
113
|
+
if (serialized === undefined) return undefined;
|
|
114
|
+
return JSON_PARSE.runInContext(context)(serialized);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const cloneOutOfVm = (value) => {
|
|
118
|
+
if (value === undefined) return undefined;
|
|
119
|
+
const serialized = JSON.stringify(value);
|
|
120
|
+
return serialized === undefined ? undefined : JSON.parse(serialized);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
(async () => {
|
|
124
|
+
try {
|
|
125
|
+
const context = createContext(Object.create(null), {
|
|
126
|
+
codeGeneration: { strings: false, wasm: false },
|
|
127
|
+
});
|
|
128
|
+
const bridgeFactory = BRIDGE_FACTORY.runInContext(context, { timeout: 1000 });
|
|
129
|
+
const bridge = (name) =>
|
|
130
|
+
bridgeFactory(createSafeHostCall((args) => callHost(name, args)));
|
|
131
|
+
const vmAgent = bridge('agent');
|
|
132
|
+
const vmPhase = bridge('phase');
|
|
133
|
+
const vmLog = bridge('log');
|
|
134
|
+
const vmWorkflow = bridge('workflow');
|
|
135
|
+
const vmCatchPerItem = bridge('catchPerItem');
|
|
136
|
+
const { parallel, pipeline } = FLOW_FACTORY.runInContext(context, { timeout: 1000 })(
|
|
137
|
+
vmCatchPerItem
|
|
138
|
+
);
|
|
139
|
+
const fn = new Script(\`"use strict";\\n\${workerData.compiledFunctionSource}\\n; __wf\`, {
|
|
140
|
+
filename: workerData.scriptPath,
|
|
141
|
+
}).runInContext(context, { timeout: 1000 });
|
|
142
|
+
const value = cloneOutOfVm(
|
|
143
|
+
await fn(
|
|
144
|
+
vmAgent,
|
|
145
|
+
parallel,
|
|
146
|
+
pipeline,
|
|
147
|
+
vmPhase,
|
|
148
|
+
vmLog,
|
|
149
|
+
vmWorkflow,
|
|
150
|
+
cloneIntoVm(context, workerData.args)
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
parentPort.postMessage({ type: 'done', ok: true, value });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
parentPort.postMessage({
|
|
156
|
+
type: 'done',
|
|
157
|
+
ok: false,
|
|
158
|
+
error: error instanceof Error ? error.message : String(error),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
})();
|
|
162
|
+
`;
|
|
163
|
+
const errorToMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
17
164
|
const BUILT_IN_WORKER_ROLES = new Set(['coder', 'reviewer', 'tester', 'custom']);
|
|
18
165
|
const isBuiltInWorkerRole = (value) => BUILT_IN_WORKER_ROLES.has(value);
|
|
19
166
|
const buildModelArgs = (_cli, model) => {
|
|
@@ -36,14 +183,21 @@ const toNestedWorkflowFilename = (scriptName) => {
|
|
|
36
183
|
export const createWorkflowRunner = (deps) => {
|
|
37
184
|
const { store, workflowRunStore, awaiter, dispatchPort, resolveWorkspacePath, roleTemplateResolver, logStore, resolveCliLaunchConfig, getWorkflowCliPolicy, } = deps;
|
|
38
185
|
const stoppedRuns = new Set();
|
|
186
|
+
const activeScriptWorkers = new Map();
|
|
39
187
|
// In-memory map: runId → triggering agent. Lost on restart; the spec already
|
|
40
188
|
// doesn't auto-resume interrupted runs, so this is consistent.
|
|
41
189
|
const triggeringAgentByRun = new Map();
|
|
190
|
+
const isRunStopped = (runId) => stoppedRuns.has(runId) || workflowRunStore.getRun(runId)?.status === 'stopped';
|
|
191
|
+
const assertRunActive = (runId) => {
|
|
192
|
+
if (isRunStopped(runId))
|
|
193
|
+
throw new Error('Stopped by user');
|
|
194
|
+
};
|
|
42
195
|
const executeWorkflow = async (run, loaded, args, hivePort) => {
|
|
43
196
|
const workspaceId = run.workspaceId;
|
|
44
197
|
const workflowAgentId = getWorkflowAgentId(workspaceId);
|
|
45
198
|
let stepCounter = 0;
|
|
46
199
|
const spawnedWorkers = [];
|
|
200
|
+
const activeAgentCalls = new Set();
|
|
47
201
|
// Read the CLI policy once per run so a 1000-way fan-out doesn't hit the
|
|
48
202
|
// app_state table per agent() call.
|
|
49
203
|
const cliPolicy = getWorkflowCliPolicy();
|
|
@@ -94,99 +248,123 @@ export const createWorkflowRunner = (deps) => {
|
|
|
94
248
|
workflowRunStore.updateRun(run.id, { phase: currentPhaseTitle });
|
|
95
249
|
};
|
|
96
250
|
const agent = async (prompt, opts = {}) => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
throw new Error(`Workflow agent cap exceeded: ${maxAgents} calls (set meta.maxAgentCalls to raise)`);
|
|
103
|
-
}
|
|
104
|
-
const myStep = ++stepCounter;
|
|
105
|
-
// TIER 2 #4 — resolve agentType. If it's a built-in WorkerRole
|
|
106
|
-
// (coder/reviewer/tester/custom) we honour it directly with the
|
|
107
|
-
// CLI default. Otherwise look up a workspace custom role template
|
|
108
|
-
// by name: on hit we clone its command + args; on miss we throw a
|
|
109
|
-
// clear error (silent fallback to 'coder' would mask typos and
|
|
110
|
-
// make the Hive-distinctive custom-role library invisible).
|
|
111
|
-
const requestedType = opts.agentType ?? 'coder';
|
|
112
|
-
let role;
|
|
113
|
-
let command;
|
|
114
|
-
let templateArgs = [];
|
|
115
|
-
if (typeof requestedType === 'string' && !isBuiltInWorkerRole(requestedType)) {
|
|
116
|
-
const template = roleTemplateResolver.findByName(requestedType);
|
|
117
|
-
if (!template) {
|
|
118
|
-
throw new Error(`Workflow agentType '${requestedType}' is not a built-in role (coder/reviewer/tester/custom) and no matching role template exists in this workspace. ` +
|
|
119
|
-
`Create one via Add Worker → custom role, or use a built-in role.`);
|
|
120
|
-
}
|
|
121
|
-
/* Templates can carry roleType='orchestrator' for the system-level
|
|
122
|
-
Orchestrator template; that's not a valid workflow worker role,
|
|
123
|
-
so we collapse it to 'custom'. Anything else maps through as-is. */
|
|
124
|
-
role = template.roleType === 'orchestrator' ? 'custom' : template.roleType;
|
|
125
|
-
command = resolveWorkflowCli({
|
|
126
|
-
...(opts.cli !== undefined ? { requestedCli: opts.cli } : {}),
|
|
127
|
-
isCustomTemplate: true,
|
|
128
|
-
templateDefaultCommand: template.defaultCommand,
|
|
129
|
-
policy: cliPolicy,
|
|
130
|
-
});
|
|
131
|
-
templateArgs = template.defaultArgs;
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
role = requestedType;
|
|
135
|
-
command = resolveWorkflowCli({
|
|
136
|
-
...(opts.cli !== undefined ? { requestedCli: opts.cli } : {}),
|
|
137
|
-
isCustomTemplate: false,
|
|
138
|
-
policy: cliPolicy,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
const name = opts.label ?? `${requestedType}-${myStep}-${randomUUID()}`;
|
|
142
|
-
const baseLaunchConfig = resolveCliLaunchConfig(command) ?? { command, args: [] };
|
|
143
|
-
/* Model flag goes AFTER the template's own args so an explicit
|
|
144
|
-
opts.model overrides any --model the template baked in. */
|
|
145
|
-
const launchArgs = [
|
|
146
|
-
...(baseLaunchConfig.args ?? []),
|
|
147
|
-
...templateArgs,
|
|
148
|
-
...buildModelArgs(baseLaunchConfig.command, opts.model),
|
|
149
|
-
];
|
|
150
|
-
// TIER 2 #2 — semaphore. Holding the slot across the full
|
|
151
|
-
// dispatch+await means a parallel(100) fan-out gets paced at
|
|
152
|
-
// min(16, cores-2) concurrent PTYs instead of 100 simultaneous
|
|
153
|
-
// process spawns.
|
|
154
|
-
const releaseSlot = await acquireSlot();
|
|
155
|
-
const worker = store.addWorkerWithLaunch(workspaceId, { name, role, ephemeral: true, spawnedBy: 'workflow' }, { ...baseLaunchConfig, args: launchArgs });
|
|
156
|
-
spawnedWorkers.push(worker.id);
|
|
251
|
+
let markAgentCallDone;
|
|
252
|
+
const agentCallDone = new Promise((resolve) => {
|
|
253
|
+
markAgentCallDone = resolve;
|
|
254
|
+
});
|
|
255
|
+
activeAgentCalls.add(agentCallDone);
|
|
157
256
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
workflowRunId: run.id,
|
|
166
|
-
stepIndex: myStep,
|
|
167
|
-
...(currentPhaseTitle ? { phase: currentPhaseTitle } : {}),
|
|
168
|
-
label: opts.label ?? name,
|
|
169
|
-
});
|
|
170
|
-
const report = await awaiter.awaitReport(dispatch.id, opts.timeoutMs);
|
|
171
|
-
// Structured output: hand back the parsed object, or { text } on a
|
|
172
|
-
// parse miss so the script can still branch (treating an absent field
|
|
173
|
-
// as the safe default).
|
|
174
|
-
if (opts.outputSchema) {
|
|
175
|
-
return extractJsonBlock(report.text) ?? { text: report.text };
|
|
257
|
+
assertRunActive(run.id);
|
|
258
|
+
// TIER 2 #11 — hard ceiling so a runaway `while (true) await agent()`
|
|
259
|
+
// can't spawn unbounded PTY subprocesses. The check is BEFORE the
|
|
260
|
+
// step counter increments so the error message names the cap, not
|
|
261
|
+
// the over-cap step.
|
|
262
|
+
if (stepCounter >= maxAgents) {
|
|
263
|
+
throw new Error(`Workflow agent cap exceeded: ${maxAgents} calls (set meta.maxAgentCalls to raise)`);
|
|
176
264
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
265
|
+
const myStep = ++stepCounter;
|
|
266
|
+
// TIER 2 #4 — resolve agentType. If it's a built-in WorkerRole
|
|
267
|
+
// (coder/reviewer/tester/custom) we honour it directly with the
|
|
268
|
+
// CLI default. Otherwise look up a workspace custom role template
|
|
269
|
+
// by name: on hit we clone its command + args; on miss we throw a
|
|
270
|
+
// clear error (silent fallback to 'coder' would mask typos and
|
|
271
|
+
// make the Hive-distinctive custom-role library invisible).
|
|
272
|
+
const requestedType = opts.agentType ?? 'coder';
|
|
273
|
+
let role;
|
|
274
|
+
let command;
|
|
275
|
+
let templateArgs = [];
|
|
276
|
+
if (typeof requestedType === 'string' && !isBuiltInWorkerRole(requestedType)) {
|
|
277
|
+
const template = roleTemplateResolver.findByName(requestedType);
|
|
278
|
+
if (!template) {
|
|
279
|
+
throw new Error(`Workflow agentType '${requestedType}' is not a built-in role (coder/reviewer/tester/custom) and no matching role template exists in this workspace. ` +
|
|
280
|
+
`Use a built-in role or an existing dispatchable role template.`);
|
|
281
|
+
}
|
|
282
|
+
if (template.roleType === 'sentinel') {
|
|
283
|
+
throw new Error(`Workflow agentType '${requestedType}' resolves to a sentinel role, but sentinels are read-only observers and cannot receive workflow dispatches. Use coder, reviewer, tester, custom, or a dispatchable role template.`);
|
|
284
|
+
}
|
|
285
|
+
/* Templates can carry roleType='orchestrator' for the system-level
|
|
286
|
+
Orchestrator template; that's not a valid workflow worker role,
|
|
287
|
+
so we collapse it to 'custom'. Anything else maps through as-is. */
|
|
288
|
+
role = template.roleType === 'orchestrator' ? 'custom' : template.roleType;
|
|
289
|
+
command = resolveWorkflowCli({
|
|
290
|
+
...(opts.cli !== undefined ? { requestedCli: opts.cli } : {}),
|
|
291
|
+
isCustomTemplate: true,
|
|
292
|
+
templateDefaultCommand: template.defaultCommand,
|
|
293
|
+
policy: cliPolicy,
|
|
294
|
+
});
|
|
295
|
+
templateArgs = template.defaultArgs;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
role = requestedType;
|
|
299
|
+
command = resolveWorkflowCli({
|
|
300
|
+
...(opts.cli !== undefined ? { requestedCli: opts.cli } : {}),
|
|
301
|
+
isCustomTemplate: false,
|
|
302
|
+
policy: cliPolicy,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const name = opts.label ?? `${requestedType}-${myStep}-${randomUUID()}`;
|
|
306
|
+
const baseLaunchConfig = resolveCliLaunchConfig(command) ?? { command, args: [] };
|
|
307
|
+
/* Model flag goes AFTER the template's own args so an explicit
|
|
308
|
+
opts.model overrides any --model the template baked in. */
|
|
309
|
+
const launchArgs = [
|
|
310
|
+
...(baseLaunchConfig.args ?? []),
|
|
311
|
+
...templateArgs,
|
|
312
|
+
...buildModelArgs(baseLaunchConfig.command, opts.model),
|
|
313
|
+
];
|
|
314
|
+
// TIER 2 #2 — semaphore. Holding the slot across the full
|
|
315
|
+
// dispatch+await means a parallel(100) fan-out gets paced at
|
|
316
|
+
// min(16, cores-2) concurrent PTYs instead of 100 simultaneous
|
|
317
|
+
// process spawns.
|
|
318
|
+
const releaseSlot = await acquireSlot();
|
|
319
|
+
let worker;
|
|
180
320
|
try {
|
|
181
|
-
|
|
321
|
+
assertRunActive(run.id);
|
|
322
|
+
worker = store.addWorkerWithLaunch(workspaceId, { name, role, ephemeral: true, spawnedBy: 'workflow' }, { ...baseLaunchConfig, args: launchArgs });
|
|
323
|
+
spawnedWorkers.push(worker.id);
|
|
324
|
+
assertRunActive(run.id);
|
|
325
|
+
const liveRun = await store.startAgent(workspaceId, worker.id, { hivePort });
|
|
326
|
+
await liveRun.postStartInputReady;
|
|
327
|
+
assertRunActive(run.id);
|
|
328
|
+
const dispatchPrompt = opts.outputSchema
|
|
329
|
+
? prompt + buildSchemaInstruction(opts.outputSchema)
|
|
330
|
+
: prompt;
|
|
331
|
+
const dispatch = await store.dispatchTaskByWorkerName(workspaceId, name, dispatchPrompt, {
|
|
332
|
+
fromAgentId: workflowAgentId,
|
|
333
|
+
hivePort,
|
|
334
|
+
workflowRunId: run.id,
|
|
335
|
+
stepIndex: myStep,
|
|
336
|
+
...(currentPhaseTitle ? { phase: currentPhaseTitle } : {}),
|
|
337
|
+
label: opts.label ?? name,
|
|
338
|
+
});
|
|
339
|
+
assertRunActive(run.id);
|
|
340
|
+
const report = await awaiter.awaitReport(dispatch.id, opts.timeoutMs);
|
|
341
|
+
assertRunActive(run.id);
|
|
342
|
+
// Structured output: hand back the parsed object, or { text } on a
|
|
343
|
+
// parse miss so the script can still branch (treating an absent field
|
|
344
|
+
// as the safe default).
|
|
345
|
+
if (opts.outputSchema) {
|
|
346
|
+
return extractJsonBlock(report.text) ?? { text: report.text };
|
|
347
|
+
}
|
|
348
|
+
return report.text;
|
|
182
349
|
}
|
|
183
|
-
|
|
184
|
-
|
|
350
|
+
finally {
|
|
351
|
+
if (worker) {
|
|
352
|
+
try {
|
|
353
|
+
store.deleteWorker(workspaceId, worker.id);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
/* idempotent — worker may already be gone via cascade or boot cleanup */
|
|
357
|
+
}
|
|
358
|
+
const idx = spawnedWorkers.indexOf(worker.id);
|
|
359
|
+
if (idx !== -1)
|
|
360
|
+
spawnedWorkers.splice(idx, 1);
|
|
361
|
+
}
|
|
362
|
+
releaseSlot();
|
|
185
363
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
markAgentCallDone();
|
|
367
|
+
activeAgentCalls.delete(agentCallDone);
|
|
190
368
|
}
|
|
191
369
|
};
|
|
192
370
|
// TIER 1 #3 — per-item rejections become null (preserves the
|
|
@@ -197,24 +375,13 @@ export const createWorkflowRunner = (deps) => {
|
|
|
197
375
|
// they're not entirely invisible (TIER 2 #3 will pipe these into the
|
|
198
376
|
// run timeline via log()).
|
|
199
377
|
const catchPerItem = (value) => {
|
|
200
|
-
if (
|
|
378
|
+
if (isRunStopped(run.id))
|
|
201
379
|
throw value;
|
|
202
380
|
console.warn(`[workflow ${loaded.meta.name}] item failed:`, value);
|
|
203
381
|
return null;
|
|
204
382
|
};
|
|
205
|
-
const parallel = async (thunks) => {
|
|
206
|
-
return Promise.all(thunks.map((thunk) => thunk().catch((err) => catchPerItem(err))));
|
|
207
|
-
};
|
|
208
|
-
const pipeline = async (items, ...stages) => {
|
|
209
|
-
return Promise.all(items.map((item, index) => {
|
|
210
|
-
let chain = Promise.resolve(item);
|
|
211
|
-
for (const stage of stages) {
|
|
212
|
-
chain = chain.then((prev) => stage(prev, item, index));
|
|
213
|
-
}
|
|
214
|
-
return chain.catch((err) => catchPerItem(err));
|
|
215
|
-
}));
|
|
216
|
-
};
|
|
217
383
|
const log = (message) => {
|
|
384
|
+
assertRunActive(run.id);
|
|
218
385
|
// TIER 2 #3 — persist + still echo to stdout for server-log
|
|
219
386
|
// visibility. Authors expect `log()` to surface in the Drawer's
|
|
220
387
|
// narrator lane and in the orchestrator's completion reminder.
|
|
@@ -242,6 +409,7 @@ export const createWorkflowRunner = (deps) => {
|
|
|
242
409
|
// sibling-script location.
|
|
243
410
|
const isSyntheticParentPath = run.scriptPath.startsWith('<inline');
|
|
244
411
|
const workflow = async (scriptName, childArgs) => {
|
|
412
|
+
assertRunActive(run.id);
|
|
245
413
|
if (typeof scriptName !== 'string' || !scriptName.trim()) {
|
|
246
414
|
throw new Error('workflow(scriptName): scriptName must be a non-empty string');
|
|
247
415
|
}
|
|
@@ -249,7 +417,7 @@ export const createWorkflowRunner = (deps) => {
|
|
|
249
417
|
const childPath = isSyntheticParentPath
|
|
250
418
|
? join(resolveWorkspacePath(workspaceId), '.hive', 'workflows', filename)
|
|
251
419
|
: join(dirname(run.scriptPath), filename);
|
|
252
|
-
|
|
420
|
+
const child = await runWorkflow({
|
|
253
421
|
workspaceId,
|
|
254
422
|
scriptPath: childPath,
|
|
255
423
|
hivePort,
|
|
@@ -258,7 +426,112 @@ export const createWorkflowRunner = (deps) => {
|
|
|
258
426
|
parentRunId: run.id,
|
|
259
427
|
...(childArgs !== undefined ? { args: childArgs } : {}),
|
|
260
428
|
});
|
|
429
|
+
assertRunActive(run.id);
|
|
430
|
+
return child;
|
|
261
431
|
};
|
|
432
|
+
const runScriptWorker = () => new Promise((resolve, reject) => {
|
|
433
|
+
const worker = new Worker(WORKFLOW_VM_WORKER_SOURCE, {
|
|
434
|
+
eval: true,
|
|
435
|
+
workerData: {
|
|
436
|
+
args,
|
|
437
|
+
compiledFunctionSource: loaded.compiledFunctionSource,
|
|
438
|
+
scriptPath: loaded.scriptPath,
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
activeScriptWorkers.set(run.id, worker);
|
|
442
|
+
let settled = false;
|
|
443
|
+
const activeHostCalls = new Set();
|
|
444
|
+
const settle = (fn) => {
|
|
445
|
+
if (settled)
|
|
446
|
+
return;
|
|
447
|
+
settled = true;
|
|
448
|
+
activeScriptWorkers.delete(run.id);
|
|
449
|
+
void (async () => {
|
|
450
|
+
await Promise.allSettled(activeHostCalls);
|
|
451
|
+
fn();
|
|
452
|
+
void worker.terminate().catch(() => { });
|
|
453
|
+
})();
|
|
454
|
+
};
|
|
455
|
+
const respond = (id, response) => {
|
|
456
|
+
try {
|
|
457
|
+
worker.postMessage({ type: 'hostResponse', id, ...response });
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
/* worker already terminated */
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
worker.on('message', (message) => {
|
|
464
|
+
const record = message;
|
|
465
|
+
if (!record)
|
|
466
|
+
return;
|
|
467
|
+
if (record.type === 'done') {
|
|
468
|
+
settle(() => {
|
|
469
|
+
if (record.ok)
|
|
470
|
+
resolve(record.value);
|
|
471
|
+
else
|
|
472
|
+
reject(new Error(record.error || 'Hive workflow script failed'));
|
|
473
|
+
});
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (record.type !== 'hostCall' || typeof record.id !== 'string')
|
|
477
|
+
return;
|
|
478
|
+
const callId = record.id;
|
|
479
|
+
if (settled) {
|
|
480
|
+
respond(callId, { ok: false, error: 'Stopped by user' });
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const hostCall = (async () => {
|
|
484
|
+
try {
|
|
485
|
+
assertRunActive(run.id);
|
|
486
|
+
const callArgs = Array.isArray(record.args) ? record.args : [];
|
|
487
|
+
let value;
|
|
488
|
+
switch (record.name) {
|
|
489
|
+
case 'agent': {
|
|
490
|
+
const [prompt, opts] = callArgs;
|
|
491
|
+
value = await agent(typeof prompt === 'string' ? prompt : String(prompt), (opts ?? {}));
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
case 'phase': {
|
|
495
|
+
const [title] = callArgs;
|
|
496
|
+
phase(typeof title === 'string' ? title : String(title ?? ''));
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
case 'log': {
|
|
500
|
+
const [message] = callArgs;
|
|
501
|
+
log(typeof message === 'string' ? message : String(message));
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
case 'workflow': {
|
|
505
|
+
const [scriptName, childArgs] = callArgs;
|
|
506
|
+
value = await workflow(typeof scriptName === 'string' ? scriptName : String(scriptName), childArgs);
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case 'catchPerItem': {
|
|
510
|
+
const [item] = callArgs;
|
|
511
|
+
value = catchPerItem(item);
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
default:
|
|
515
|
+
throw new Error(`Unknown workflow host call: ${record.name}`);
|
|
516
|
+
}
|
|
517
|
+
assertRunActive(run.id);
|
|
518
|
+
respond(callId, { ok: true, value });
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
respond(callId, { ok: false, error: errorToMessage(error) });
|
|
522
|
+
}
|
|
523
|
+
})();
|
|
524
|
+
activeHostCalls.add(hostCall);
|
|
525
|
+
void hostCall.finally(() => activeHostCalls.delete(hostCall));
|
|
526
|
+
});
|
|
527
|
+
worker.on('error', (error) => settle(() => reject(error)));
|
|
528
|
+
worker.on('exit', (code) => {
|
|
529
|
+
if (settled)
|
|
530
|
+
return;
|
|
531
|
+
activeScriptWorkers.delete(run.id);
|
|
532
|
+
reject(new Error(`Hive workflow VM worker exited before completion (code ${code})`));
|
|
533
|
+
});
|
|
534
|
+
});
|
|
262
535
|
// TIER 2 #11 — wall-clock budget timer. Triggers stopRun on
|
|
263
536
|
// expiry, which routes through the same path as a user-initiated
|
|
264
537
|
// stop (in-flight awaiters reject; outer catch records 'stopped').
|
|
@@ -272,9 +545,8 @@ export const createWorkflowRunner = (deps) => {
|
|
|
272
545
|
}, maxDurationMs);
|
|
273
546
|
budgetTimer.unref?.();
|
|
274
547
|
try {
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
const returnValue = await fn(agent, parallel, pipeline, phase, log, workflow, args);
|
|
548
|
+
assertRunActive(run.id);
|
|
549
|
+
const returnValue = await runScriptWorker();
|
|
278
550
|
// TIER 1 #2 — if stop was called DURING the run, parallel/pipeline may
|
|
279
551
|
// have caught the cancel rejections (one per in-flight thunk) before
|
|
280
552
|
// the per-item catch could re-throw, e.g. when the user stops AFTER
|
|
@@ -283,8 +555,7 @@ export const createWorkflowRunner = (deps) => {
|
|
|
283
555
|
// degraded result (often a list of nulls), which both lies to the UI
|
|
284
556
|
// and lies to the orchestrator's completion notification. Check the
|
|
285
557
|
// marker after fn returns and record the truth instead.
|
|
286
|
-
if (stoppedRuns.has(run.id)) {
|
|
287
|
-
stoppedRuns.delete(run.id);
|
|
558
|
+
if (stoppedRuns.has(run.id) || workflowRunStore.getRun(run.id)?.status === 'stopped') {
|
|
288
559
|
workflowRunStore.updateRun(run.id, {
|
|
289
560
|
status: 'stopped',
|
|
290
561
|
finishedAt: Date.now(),
|
|
@@ -292,7 +563,6 @@ export const createWorkflowRunner = (deps) => {
|
|
|
292
563
|
});
|
|
293
564
|
}
|
|
294
565
|
else {
|
|
295
|
-
stoppedRuns.delete(run.id);
|
|
296
566
|
// M10: capture the script's return value so the UI can render a single
|
|
297
567
|
// canonical "Result" panel and the orchestrator notification can quote
|
|
298
568
|
// it. `undefined` (no explicit return) stays null on the row.
|
|
@@ -304,17 +574,22 @@ export const createWorkflowRunner = (deps) => {
|
|
|
304
574
|
}
|
|
305
575
|
}
|
|
306
576
|
catch (error) {
|
|
307
|
-
|
|
577
|
+
if (stoppedRuns.has(run.id)) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const wasStopped = workflowRunStore.getRun(run.id)?.status === 'stopped';
|
|
581
|
+
if (wasStopped)
|
|
582
|
+
return;
|
|
308
583
|
const message = error instanceof Error ? error.message : String(error);
|
|
309
584
|
workflowRunStore.updateRun(run.id, {
|
|
310
|
-
status:
|
|
585
|
+
status: 'failed',
|
|
311
586
|
finishedAt: Date.now(),
|
|
312
|
-
error:
|
|
587
|
+
error: message,
|
|
313
588
|
});
|
|
314
|
-
stoppedRuns.delete(run.id);
|
|
315
589
|
}
|
|
316
590
|
finally {
|
|
317
591
|
clearTimeout(budgetTimer);
|
|
592
|
+
await Promise.allSettled(activeAgentCalls);
|
|
318
593
|
// Belt-and-suspenders: dismiss any ephemeral worker still alive. The
|
|
319
594
|
// per-call try/finally should already have cleaned each one up; this is
|
|
320
595
|
// an idempotent safety net for unexpected paths.
|
|
@@ -341,6 +616,7 @@ export const createWorkflowRunner = (deps) => {
|
|
|
341
616
|
}
|
|
342
617
|
}
|
|
343
618
|
}
|
|
619
|
+
stoppedRuns.delete(run.id);
|
|
344
620
|
}
|
|
345
621
|
};
|
|
346
622
|
const buildCreateInput = (input, loaded) => {
|
|
@@ -360,8 +636,17 @@ export const createWorkflowRunner = (deps) => {
|
|
|
360
636
|
if (triggeredByAgentId)
|
|
361
637
|
triggeringAgentByRun.set(runId, triggeredByAgentId);
|
|
362
638
|
};
|
|
639
|
+
const assertParentRunStillRunning = (input) => {
|
|
640
|
+
if (!input.parentRunId)
|
|
641
|
+
return;
|
|
642
|
+
const parent = workflowRunStore.getRun(input.parentRunId);
|
|
643
|
+
if (stoppedRuns.has(input.parentRunId) || parent?.status === 'stopped') {
|
|
644
|
+
throw new Error('Stopped by user');
|
|
645
|
+
}
|
|
646
|
+
};
|
|
363
647
|
const runWorkflow = async (input) => {
|
|
364
648
|
const loaded = await loadWorkflowScriptFile(input.scriptPath);
|
|
649
|
+
assertParentRunStillRunning(input);
|
|
365
650
|
const run = workflowRunStore.createRun(buildCreateInput(input, loaded));
|
|
366
651
|
rememberTrigger(run.id, input.triggeredByAgentId);
|
|
367
652
|
await executeWorkflow(run, loaded, input.args, input.hivePort);
|
|
@@ -372,6 +657,7 @@ export const createWorkflowRunner = (deps) => {
|
|
|
372
657
|
};
|
|
373
658
|
const startWorkflow = async (input) => {
|
|
374
659
|
const loaded = await loadWorkflowScriptFile(input.scriptPath);
|
|
660
|
+
assertParentRunStillRunning(input);
|
|
375
661
|
const run = workflowRunStore.createRun(buildCreateInput(input, loaded));
|
|
376
662
|
rememberTrigger(run.id, input.triggeredByAgentId);
|
|
377
663
|
queueMicrotask(() => {
|
|
@@ -393,24 +679,38 @@ export const createWorkflowRunner = (deps) => {
|
|
|
393
679
|
});
|
|
394
680
|
return run;
|
|
395
681
|
};
|
|
396
|
-
const
|
|
682
|
+
const stopRunAndChildren = (runId, visited) => {
|
|
683
|
+
if (visited.has(runId))
|
|
684
|
+
return false;
|
|
685
|
+
visited.add(runId);
|
|
397
686
|
const current = workflowRunStore.getRun(runId);
|
|
398
687
|
if (!current || current.status !== 'running')
|
|
399
688
|
return false;
|
|
400
689
|
stoppedRuns.add(runId);
|
|
690
|
+
void activeScriptWorkers
|
|
691
|
+
.get(runId)
|
|
692
|
+
?.terminate()
|
|
693
|
+
.catch(() => { });
|
|
401
694
|
// Cancel every open workflow dispatch tied to this run; this rejects the
|
|
402
695
|
// runner's pending `awaitReport` promises, which propagates up the
|
|
403
696
|
// executeWorkflow try → its catch sets status='stopped'.
|
|
404
697
|
for (const dispatchId of dispatchPort.listOpenDispatchIdsForRun(runId)) {
|
|
405
698
|
awaiter.notifyCancel(dispatchId, 'Stopped by user');
|
|
406
699
|
}
|
|
700
|
+
for (const child of workflowRunStore.listChildRuns(runId)) {
|
|
701
|
+
if (child.status === 'running')
|
|
702
|
+
stopRunAndChildren(child.id, visited);
|
|
703
|
+
}
|
|
704
|
+
workflowRunStore.updateRun(runId, {
|
|
705
|
+
status: 'stopped',
|
|
706
|
+
finishedAt: Date.now(),
|
|
707
|
+
error: 'Stopped by user',
|
|
708
|
+
});
|
|
407
709
|
// If the script had no in-flight agent() call when stop was requested,
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
// single-step or no-agent() scripts also reflect 'stopped', mark the row
|
|
411
|
-
// synchronously as a hint to UI — the runner will overwrite to 'stopped'
|
|
412
|
-
// (or 'completed' if it really did finish) when executeWorkflow exits.
|
|
710
|
+
// it may never reject on its own. Persist the stopped state immediately
|
|
711
|
+
// so UI/API truth does not depend on the script reaching a later awaiter.
|
|
413
712
|
return true;
|
|
414
713
|
};
|
|
714
|
+
const stopRun = (runId) => stopRunAndChildren(runId, new Set());
|
|
415
715
|
return { runWorkflow, startWorkflow, startWorkflowInline, stopRun };
|
|
416
716
|
};
|