@tt-a1i/hive 1.4.4 → 1.6.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 +47 -0
- package/README.en.md +21 -0
- package/README.md +16 -0
- package/assets/qq-group.jpg +0 -0
- package/dist/bin/team.cmd +1 -0
- package/dist/src/cli/hive-update.d.ts +45 -17
- package/dist/src/cli/hive-update.js +63 -25
- package/dist/src/cli/hive.d.ts +25 -0
- package/dist/src/cli/hive.js +41 -3
- package/dist/src/cli/team.d.ts +1 -0
- package/dist/src/cli/team.js +216 -3
- package/dist/src/server/agent-command-resolver.js +3 -19
- package/dist/src/server/agent-manager-support.d.ts +2 -2
- package/dist/src/server/agent-manager-support.js +98 -24
- package/dist/src/server/agent-run-starter.d.ts +6 -1
- package/dist/src/server/agent-run-starter.js +9 -2
- package/dist/src/server/agent-run-store.d.ts +1 -1
- package/dist/src/server/agent-runtime-close.d.ts +1 -0
- package/dist/src/server/agent-runtime-close.js +25 -1
- package/dist/src/server/agent-runtime-contract.d.ts +12 -1
- package/dist/src/server/agent-runtime-stop-run.d.ts +1 -1
- package/dist/src/server/agent-runtime-stop-run.js +4 -1
- package/dist/src/server/agent-runtime.d.ts +2 -1
- package/dist/src/server/agent-runtime.js +14 -3
- package/dist/src/server/agent-startup-instructions.d.ts +7 -1
- package/dist/src/server/agent-startup-instructions.js +17 -9
- package/dist/src/server/agent-stdin-dispatcher.d.ts +25 -5
- package/dist/src/server/agent-stdin-dispatcher.js +141 -40
- package/dist/src/server/cron-util.d.ts +7 -0
- package/dist/src/server/cron-util.js +19 -0
- package/dist/src/server/dispatch-ledger-store.d.ts +22 -0
- package/dist/src/server/dispatch-ledger-store.js +51 -3
- package/dist/src/server/env-sync-message.js +9 -9
- package/dist/src/server/feature-flags.d.ts +42 -0
- package/dist/src/server/feature-flags.js +24 -0
- package/dist/src/server/fs-pick-folder.js +4 -0
- package/dist/src/server/fs-sandbox.js +36 -7
- package/dist/src/server/hive-team-guidance.d.ts +12 -6
- package/dist/src/server/hive-team-guidance.js +253 -71
- package/dist/src/server/live-run-registry.d.ts +1 -0
- package/dist/src/server/live-run-registry.js +1 -1
- package/dist/src/server/open-target-commands.js +5 -6
- package/dist/src/server/orchestrator-autostart.d.ts +12 -0
- package/dist/src/server/orchestrator-autostart.js +15 -13
- package/dist/src/server/path-canonicalization.d.ts +3 -0
- package/dist/src/server/path-canonicalization.js +29 -0
- package/dist/src/server/platform-path.d.ts +3 -0
- package/dist/src/server/platform-path.js +13 -0
- package/dist/src/server/post-start-input-writer.d.ts +1 -1
- package/dist/src/server/post-start-input-writer.js +110 -13
- package/dist/src/server/preset-launch-support.d.ts +1 -1
- package/dist/src/server/preset-launch-support.js +33 -2
- package/dist/src/server/recovery-summary.d.ts +5 -1
- package/dist/src/server/recovery-summary.js +18 -17
- package/dist/src/server/report-outbox-store.d.ts +36 -0
- package/dist/src/server/report-outbox-store.js +33 -0
- package/dist/src/server/restart-policy-support.d.ts +5 -1
- package/dist/src/server/restart-policy-support.js +9 -1
- package/dist/src/server/restart-policy.d.ts +6 -2
- package/dist/src/server/restart-policy.js +51 -31
- package/dist/src/server/role-template-store.d.ts +1 -0
- package/dist/src/server/role-template-store.js +11 -1
- package/dist/src/server/route-types.d.ts +43 -0
- package/dist/src/server/routes-runtime.js +2 -1
- package/dist/src/server/routes-settings.js +76 -0
- package/dist/src/server/routes-tasks.js +23 -0
- package/dist/src/server/routes-team.js +211 -1
- package/dist/src/server/routes-workflow-schedules.d.ts +2 -0
- package/dist/src/server/routes-workflow-schedules.js +58 -0
- package/dist/src/server/routes-workflows.d.ts +2 -0
- package/dist/src/server/routes-workflows.js +83 -0
- package/dist/src/server/routes-workspaces.js +5 -0
- package/dist/src/server/routes.js +4 -0
- package/dist/src/server/runtime-restart-policy.d.ts +3 -1
- package/dist/src/server/runtime-restart-policy.js +2 -1
- package/dist/src/server/runtime-store-contract.d.ts +125 -0
- package/dist/src/server/runtime-store-contract.js +1 -0
- package/dist/src/server/runtime-store-helpers.d.ts +11 -0
- package/dist/src/server/runtime-store-helpers.js +106 -2
- package/dist/src/server/runtime-store-workflows.d.ts +6 -0
- package/dist/src/server/runtime-store-workflows.js +108 -0
- package/dist/src/server/runtime-store.d.ts +3 -72
- package/dist/src/server/runtime-store.js +71 -4
- package/dist/src/server/session-capture-codex.d.ts +3 -3
- package/dist/src/server/session-capture-codex.js +9 -7
- package/dist/src/server/session-capture-gemini.d.ts +1 -1
- package/dist/src/server/session-capture-gemini.js +6 -3
- package/dist/src/server/settings-store.d.ts +3 -0
- package/dist/src/server/settings-store.js +1 -0
- package/dist/src/server/sqlite-schema-v19.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v19.js +17 -0
- package/dist/src/server/sqlite-schema-v20.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v20.js +20 -0
- package/dist/src/server/sqlite-schema-v21.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v21.js +20 -0
- package/dist/src/server/sqlite-schema.d.ts +1 -1
- package/dist/src/server/sqlite-schema.js +110 -1
- package/dist/src/server/system-message.d.ts +7 -0
- package/dist/src/server/system-message.js +8 -1
- package/dist/src/server/task-deps.d.ts +32 -0
- package/dist/src/server/task-deps.js +40 -0
- package/dist/src/server/tasks-file-watcher.d.ts +12 -1
- package/dist/src/server/tasks-file-watcher.js +128 -23
- package/dist/src/server/tasks-file.d.ts +3 -1
- package/dist/src/server/tasks-file.js +33 -9
- package/dist/src/server/tasks-websocket-server.js +13 -14
- package/dist/src/server/team-authz.d.ts +1 -1
- package/dist/src/server/team-authz.js +10 -1
- package/dist/src/server/team-autostaff.d.ts +16 -0
- package/dist/src/server/team-autostaff.js +16 -0
- package/dist/src/server/team-list-serializer.d.ts +1 -1
- package/dist/src/server/team-list-serializer.js +3 -1
- package/dist/src/server/team-operations.d.ts +21 -1
- package/dist/src/server/team-operations.js +183 -16
- package/dist/src/server/terminal-protocol.js +9 -3
- package/dist/src/server/terminal-stream-hub.js +16 -10
- package/dist/src/server/terminal-ws-server.js +10 -8
- package/dist/src/server/webhook-notifier.d.ts +34 -0
- package/dist/src/server/webhook-notifier.js +47 -0
- package/dist/src/server/websocket-upgrade-safety.d.ts +10 -0
- package/dist/src/server/websocket-upgrade-safety.js +35 -0
- package/dist/src/server/windows-command-line.d.ts +3 -0
- package/dist/src/server/windows-command-line.js +9 -0
- package/dist/src/server/windows-filename.d.ts +2 -0
- package/dist/src/server/windows-filename.js +33 -0
- package/dist/src/server/workflow-cli-policy.d.ts +60 -0
- package/dist/src/server/workflow-cli-policy.js +110 -0
- package/dist/src/server/workflow-dispatch-awaiter.d.ts +12 -0
- package/dist/src/server/workflow-dispatch-awaiter.js +80 -0
- package/dist/src/server/workflow-feature.d.ts +15 -0
- package/dist/src/server/workflow-feature.js +15 -0
- package/dist/src/server/workflow-http-serializers.d.ts +64 -0
- package/dist/src/server/workflow-http-serializers.js +58 -0
- package/dist/src/server/workflow-output-schema.d.ts +18 -0
- package/dist/src/server/workflow-output-schema.js +41 -0
- package/dist/src/server/workflow-run-log-store.d.ts +19 -0
- package/dist/src/server/workflow-run-log-store.js +45 -0
- package/dist/src/server/workflow-run-store.d.ts +50 -0
- package/dist/src/server/workflow-run-store.js +103 -0
- package/dist/src/server/workflow-runner.d.ts +147 -0
- package/dist/src/server/workflow-runner.js +411 -0
- package/dist/src/server/workflow-schedule-create.d.ts +14 -0
- package/dist/src/server/workflow-schedule-create.js +41 -0
- package/dist/src/server/workflow-schedule-store.d.ts +43 -0
- package/dist/src/server/workflow-schedule-store.js +112 -0
- package/dist/src/server/workflow-scheduler.d.ts +36 -0
- package/dist/src/server/workflow-scheduler.js +97 -0
- package/dist/src/server/workflow-script-loader.d.ts +34 -0
- package/dist/src/server/workflow-script-loader.js +106 -0
- package/dist/src/server/workspace-path-validation.js +16 -4
- package/dist/src/server/workspace-shell-runtime.d.ts +5 -0
- package/dist/src/server/workspace-shell-runtime.js +24 -2
- package/dist/src/server/workspace-store-contract.d.ts +4 -1
- package/dist/src/server/workspace-store-hydration.js +23 -7
- package/dist/src/server/workspace-store-mutations.js +2 -5
- package/dist/src/server/workspace-store-support.d.ts +4 -0
- package/dist/src/server/workspace-store-support.js +13 -1
- package/dist/src/server/workspace-store.js +38 -4
- package/dist/src/shared/types.d.ts +16 -1
- package/package.json +4 -2
- package/web/dist/assets/{AddWorkerDialog-DeZhTQLi.js → AddWorkerDialog-CGbaxu0T.js} +2 -2
- package/web/dist/assets/AddWorkspaceDialog-CNgExu6b.js +1 -0
- package/web/dist/assets/{FirstRunWizard-B5wLcat5.js → FirstRunWizard-DxGApUNc.js} +1 -1
- package/web/dist/assets/{MarketplaceDrawer-BC0eBOEW.js → MarketplaceDrawer-Bk6cpukn.js} +1 -1
- package/web/dist/assets/WhatsNewDialog-CSGzk-2U.js +1 -0
- package/web/dist/assets/WorkerModal-i2F3n3nZ.js +1 -0
- package/web/dist/assets/WorkspaceTaskDrawer-C_Ta_K13.js +1 -0
- package/web/dist/assets/WorkspaceTerminalPanels-VdDxtrQF.js +1 -0
- package/web/dist/assets/index-5zh61jMg.css +1 -0
- package/web/dist/assets/index-CAgGM6nb.js +75 -0
- package/web/dist/assets/path-join-7MR1s7b1.js +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/AddWorkspaceDialog-DDpXNEKf.js +0 -1
- package/web/dist/assets/WorkerModal-BwMHq-Bi.js +0 -1
- package/web/dist/assets/WorkspaceTaskDrawer-CxvT4nqs.js +0 -1
- package/web/dist/assets/WorkspaceTerminalPanels-CvibsPSd.js +0 -1
- package/web/dist/assets/index-BEsTmfrO.css +0 -1
- package/web/dist/assets/index-Ddb7bDN5.js +0 -75
- package/web/dist/assets/path-join-S7qkXQtP.js +0 -1
package/dist/src/cli/team.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { realpathSync } from 'node:fs';
|
|
2
1
|
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { sameFilesystemPath } from '../server/path-canonicalization.js';
|
|
3
3
|
const REQUIRED_ENV_KEYS = [
|
|
4
4
|
'HIVE_PORT',
|
|
5
5
|
'HIVE_PROJECT_ID',
|
|
@@ -9,7 +9,15 @@ const REQUIRED_ENV_KEYS = [
|
|
|
9
9
|
const TEAM_USAGE = [
|
|
10
10
|
'Usage:',
|
|
11
11
|
' team list',
|
|
12
|
+
' team next (tasks in .hive/tasks.md that are unblocked now — those whose [needs: #n] deps are all done)',
|
|
12
13
|
' team send <worker-name> "<task>"',
|
|
14
|
+
' team spawn <role> [--name <name>] [--cli <claude|codex|opencode|gemini>] [--ephemeral]',
|
|
15
|
+
' team dismiss <worker-name>',
|
|
16
|
+
" team workflow run --stdin [--args '<JSON>'] (script from stdin — for multi-line scripts)",
|
|
17
|
+
' team workflow run --inline "<source>" [--args \'<JSON>\']',
|
|
18
|
+
' team workflow stop <run-id>',
|
|
19
|
+
' team workflow show <run-id> (full per-agent transcript for one run)',
|
|
20
|
+
' team workflow schedule --cron "<cron>" --name <n> --stdin (register a recurring run)',
|
|
13
21
|
' team cancel --dispatch <dispatch-id> "<reason>"',
|
|
14
22
|
' team report "<result>" [--dispatch <dispatch-id>] [--artifact <path>]',
|
|
15
23
|
' team report --stdin [--dispatch <dispatch-id>] [--artifact <path>]',
|
|
@@ -35,6 +43,15 @@ const getHiveEnv = () => {
|
|
|
35
43
|
return values;
|
|
36
44
|
};
|
|
37
45
|
const getBaseUrl = (env) => `http://127.0.0.1:${env.HIVE_PORT}`;
|
|
46
|
+
// Read `--flag value` from an argv slice; returns undefined when absent or
|
|
47
|
+
// when the flag is the last token with no following value.
|
|
48
|
+
const readFlag = (args, flag) => {
|
|
49
|
+
const index = args.indexOf(flag);
|
|
50
|
+
if (index === -1)
|
|
51
|
+
return undefined;
|
|
52
|
+
const value = args[index + 1];
|
|
53
|
+
return value && !value.startsWith('--') ? value : undefined;
|
|
54
|
+
};
|
|
38
55
|
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
39
56
|
const describeFetchError = (baseUrl, error) => {
|
|
40
57
|
const cause = error instanceof Error && error.cause instanceof Error ? ` (${error.cause.message})` : '';
|
|
@@ -177,6 +194,20 @@ export const parseCancelArgs = (args) => {
|
|
|
177
194
|
}
|
|
178
195
|
return { dispatchId, reason };
|
|
179
196
|
};
|
|
197
|
+
export const decodeStdinBuffer = (buffer) => {
|
|
198
|
+
if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
|
|
199
|
+
return buffer.subarray(3).toString('utf8');
|
|
200
|
+
}
|
|
201
|
+
if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
|
|
202
|
+
return buffer.subarray(2).toString('utf16le');
|
|
203
|
+
}
|
|
204
|
+
if (buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) {
|
|
205
|
+
const swapped = Buffer.from(buffer.subarray(2));
|
|
206
|
+
swapped.swap16();
|
|
207
|
+
return swapped.toString('utf16le');
|
|
208
|
+
}
|
|
209
|
+
return buffer.toString('utf8');
|
|
210
|
+
};
|
|
180
211
|
export const readStdinToString = async (command = 'report') => {
|
|
181
212
|
if (process.stdin.isTTY) {
|
|
182
213
|
throw new Error(withUsage('--stdin requires piped input, but stdin is a TTY. Did you forget to pipe content in?', command));
|
|
@@ -185,7 +216,7 @@ export const readStdinToString = async (command = 'report') => {
|
|
|
185
216
|
for await (const chunk of process.stdin) {
|
|
186
217
|
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
187
218
|
}
|
|
188
|
-
const content = Buffer.concat(chunks)
|
|
219
|
+
const content = decodeStdinBuffer(Buffer.concat(chunks));
|
|
189
220
|
if (!content.trim()) {
|
|
190
221
|
throw new Error(withUsage('--stdin received empty input', command));
|
|
191
222
|
}
|
|
@@ -213,6 +244,22 @@ export const runTeamCommand = async (argv) => {
|
|
|
213
244
|
console.log(JSON.stringify(await response.json()));
|
|
214
245
|
return;
|
|
215
246
|
}
|
|
247
|
+
if (command === 'next') {
|
|
248
|
+
const env = getHiveEnv();
|
|
249
|
+
const baseUrl = getBaseUrl(env);
|
|
250
|
+
const response = await fetchRuntime(baseUrl, `/api/workspaces/${env.HIVE_PROJECT_ID}/tasks/next`, {
|
|
251
|
+
method: 'GET',
|
|
252
|
+
headers: {
|
|
253
|
+
'x-hive-agent-id': env.HIVE_AGENT_ID,
|
|
254
|
+
'x-hive-agent-token': env.HIVE_AGENT_TOKEN,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
if (!response.ok) {
|
|
258
|
+
await throwHttpError(response);
|
|
259
|
+
}
|
|
260
|
+
console.log(JSON.stringify(await response.json()));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
216
263
|
if (command === 'send') {
|
|
217
264
|
const [workerName, ...taskParts] = args;
|
|
218
265
|
const task = taskParts.join(' ').trim();
|
|
@@ -241,6 +288,172 @@ export const runTeamCommand = async (argv) => {
|
|
|
241
288
|
console.log(JSON.stringify(payload));
|
|
242
289
|
return;
|
|
243
290
|
}
|
|
291
|
+
if (command === 'spawn') {
|
|
292
|
+
const role = args[0];
|
|
293
|
+
if (!role || role.startsWith('--')) {
|
|
294
|
+
throw new Error('Usage: team spawn <role> [--name <name>] [--cli <claude|codex|opencode|gemini>] [--ephemeral]\n' +
|
|
295
|
+
' Default: persistent member (lives until you `team dismiss` it).\n' +
|
|
296
|
+
' --ephemeral: auto-dismiss after the next dispatch report (one-shot worker).');
|
|
297
|
+
}
|
|
298
|
+
const name = readFlag(args, '--name');
|
|
299
|
+
const cli = readFlag(args, '--cli');
|
|
300
|
+
const ephemeral = args.includes('--ephemeral');
|
|
301
|
+
const env = getHiveEnv();
|
|
302
|
+
const response = await postJson(getBaseUrl(env), '/api/team/spawn', {
|
|
303
|
+
project_id: env.HIVE_PROJECT_ID,
|
|
304
|
+
from_agent_id: env.HIVE_AGENT_ID,
|
|
305
|
+
token: env.HIVE_AGENT_TOKEN,
|
|
306
|
+
role,
|
|
307
|
+
...(name ? { name } : {}),
|
|
308
|
+
...(cli ? { cli } : {}),
|
|
309
|
+
...(ephemeral ? { ephemeral: true } : {}),
|
|
310
|
+
});
|
|
311
|
+
console.log(JSON.stringify(await response.json()));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (command === 'dismiss') {
|
|
315
|
+
const workerName = args[0];
|
|
316
|
+
if (!workerName || workerName.startsWith('--')) {
|
|
317
|
+
throw new Error('Usage: team dismiss <worker-name>');
|
|
318
|
+
}
|
|
319
|
+
const env = getHiveEnv();
|
|
320
|
+
const response = await postJson(getBaseUrl(env), '/api/team/dismiss', {
|
|
321
|
+
project_id: env.HIVE_PROJECT_ID,
|
|
322
|
+
from_agent_id: env.HIVE_AGENT_ID,
|
|
323
|
+
token: env.HIVE_AGENT_TOKEN,
|
|
324
|
+
name: workerName,
|
|
325
|
+
});
|
|
326
|
+
console.log(JSON.stringify(await response.json()));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (command === 'workflow') {
|
|
330
|
+
const sub = args[0];
|
|
331
|
+
const rest = args.slice(1);
|
|
332
|
+
if (sub === 'run') {
|
|
333
|
+
const inlineFlag = rest.indexOf('--inline');
|
|
334
|
+
const stdinFlag = rest.includes('--stdin');
|
|
335
|
+
const name = readFlag(rest, '--name');
|
|
336
|
+
// TIER 2 #8 — `--args '<JSON>'` makes the script's `args` global a
|
|
337
|
+
// real value instead of always undefined. Parses lazily so a bad
|
|
338
|
+
// JSON gives a clear local error before the HTTP round-trip.
|
|
339
|
+
const rawArgs = readFlag(rest, '--args');
|
|
340
|
+
let parsedArgs;
|
|
341
|
+
if (rawArgs !== undefined) {
|
|
342
|
+
try {
|
|
343
|
+
parsedArgs = JSON.parse(rawArgs);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
throw new Error(`Usage: team workflow run … --args '<JSON>'\n --args must be valid JSON; got: ${error instanceof Error ? error.message : String(error)}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
let source;
|
|
350
|
+
if (inlineFlag !== -1) {
|
|
351
|
+
const literal = rest[inlineFlag + 1];
|
|
352
|
+
if (!literal)
|
|
353
|
+
throw new Error('Usage: team workflow run --inline "<script-source>"');
|
|
354
|
+
source = literal;
|
|
355
|
+
}
|
|
356
|
+
else if (stdinFlag) {
|
|
357
|
+
source = await readStdinToString('workflow run');
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
throw new Error('Usage: team workflow run --stdin | team workflow run --inline "<script-source>"\n' +
|
|
361
|
+
' Pass workflow source via stdin (POSIX heredoc / `type x.ts |`) or as one inline arg.\n' +
|
|
362
|
+
" Optional: --args '<JSON>' makes the script's `args` global a real value.");
|
|
363
|
+
}
|
|
364
|
+
const env = getHiveEnv();
|
|
365
|
+
const response = await postJson(getBaseUrl(env), '/api/team/workflow/run', {
|
|
366
|
+
project_id: env.HIVE_PROJECT_ID,
|
|
367
|
+
from_agent_id: env.HIVE_AGENT_ID,
|
|
368
|
+
token: env.HIVE_AGENT_TOKEN,
|
|
369
|
+
source,
|
|
370
|
+
...(name ? { name } : {}),
|
|
371
|
+
...(parsedArgs !== undefined ? { args: parsedArgs } : {}),
|
|
372
|
+
});
|
|
373
|
+
console.log(JSON.stringify(await response.json()));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (sub === 'stop') {
|
|
377
|
+
const runId = rest[0];
|
|
378
|
+
if (!runId)
|
|
379
|
+
throw new Error('Usage: team workflow stop <run-id>');
|
|
380
|
+
const env = getHiveEnv();
|
|
381
|
+
const response = await postJson(getBaseUrl(env), '/api/team/workflow/stop', {
|
|
382
|
+
project_id: env.HIVE_PROJECT_ID,
|
|
383
|
+
from_agent_id: env.HIVE_AGENT_ID,
|
|
384
|
+
token: env.HIVE_AGENT_TOKEN,
|
|
385
|
+
run_id: runId,
|
|
386
|
+
});
|
|
387
|
+
console.log(JSON.stringify(await response.json()));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (sub === 'show') {
|
|
391
|
+
const runId = rest[0];
|
|
392
|
+
if (!runId)
|
|
393
|
+
throw new Error('Usage: team workflow show <run-id>');
|
|
394
|
+
const env = getHiveEnv();
|
|
395
|
+
const response = await postJson(getBaseUrl(env), '/api/team/workflow/show', {
|
|
396
|
+
project_id: env.HIVE_PROJECT_ID,
|
|
397
|
+
from_agent_id: env.HIVE_AGENT_ID,
|
|
398
|
+
token: env.HIVE_AGENT_TOKEN,
|
|
399
|
+
run_id: runId,
|
|
400
|
+
});
|
|
401
|
+
console.log(JSON.stringify(await response.json()));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (sub === 'schedule') {
|
|
405
|
+
const usage = 'Usage: team workflow schedule --cron "<5-field cron>" --name <name> --stdin\n' +
|
|
406
|
+
' team workflow schedule --cron "<cron>" --name <name> --inline "<source>" [--args \'<JSON>\']\n' +
|
|
407
|
+
' Registers a recurring run. Source is persisted so cron can fire it with no orchestrator present.';
|
|
408
|
+
const inlineFlag = rest.indexOf('--inline');
|
|
409
|
+
const stdinFlag = rest.includes('--stdin');
|
|
410
|
+
const cron = readFlag(rest, '--cron');
|
|
411
|
+
const name = readFlag(rest, '--name');
|
|
412
|
+
if (!cron || !name)
|
|
413
|
+
throw new Error(usage);
|
|
414
|
+
const rawArgs = readFlag(rest, '--args');
|
|
415
|
+
let parsedArgs;
|
|
416
|
+
if (rawArgs !== undefined) {
|
|
417
|
+
try {
|
|
418
|
+
parsedArgs = JSON.parse(rawArgs);
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
throw new Error(`Usage: team workflow schedule … --args '<JSON>'\n --args must be valid JSON; got: ${error instanceof Error ? error.message : String(error)}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
let source;
|
|
425
|
+
if (inlineFlag !== -1) {
|
|
426
|
+
const literal = rest[inlineFlag + 1];
|
|
427
|
+
if (!literal)
|
|
428
|
+
throw new Error(usage);
|
|
429
|
+
source = literal;
|
|
430
|
+
}
|
|
431
|
+
else if (stdinFlag) {
|
|
432
|
+
source = await readStdinToString('workflow schedule');
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
throw new Error(usage);
|
|
436
|
+
}
|
|
437
|
+
const env = getHiveEnv();
|
|
438
|
+
const response = await postJson(getBaseUrl(env), '/api/team/workflow/schedule', {
|
|
439
|
+
project_id: env.HIVE_PROJECT_ID,
|
|
440
|
+
from_agent_id: env.HIVE_AGENT_ID,
|
|
441
|
+
token: env.HIVE_AGENT_TOKEN,
|
|
442
|
+
source,
|
|
443
|
+
name,
|
|
444
|
+
cron,
|
|
445
|
+
...(parsedArgs !== undefined ? { args: parsedArgs } : {}),
|
|
446
|
+
});
|
|
447
|
+
console.log(JSON.stringify(await response.json()));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
throw new Error('Usage:\n' +
|
|
451
|
+
" team workflow run --stdin [--args '<JSON>'] (read script from stdin)\n" +
|
|
452
|
+
' team workflow run --inline "<source>" [--args ...] (one-arg form)\n' +
|
|
453
|
+
' team workflow stop <run-id> (cancel a running workflow)\n' +
|
|
454
|
+
' team workflow show <run-id> (full per-agent transcript)\n' +
|
|
455
|
+
' team workflow schedule --cron "<cron>" --name <n> --stdin (register a recurring run)');
|
|
456
|
+
}
|
|
244
457
|
if (command === 'cancel') {
|
|
245
458
|
const cancel = parseCancelArgs(args);
|
|
246
459
|
const env = getHiveEnv();
|
|
@@ -294,7 +507,7 @@ export const runTeamCommand = async (argv) => {
|
|
|
294
507
|
throw new Error('Unsupported team command');
|
|
295
508
|
};
|
|
296
509
|
const isMainModule = process.argv[1]
|
|
297
|
-
? fileURLToPath(import.meta.url)
|
|
510
|
+
? sameFilesystemPath(fileURLToPath(import.meta.url), process.argv[1])
|
|
298
511
|
: false;
|
|
299
512
|
if (isMainModule) {
|
|
300
513
|
void runTeamCommand(process.argv.slice(2)).catch((error) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { accessSync, constants } from 'node:fs';
|
|
2
2
|
import { basename, delimiter, extname, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { buildCmdCallCommand } from './windows-command-line.js';
|
|
3
4
|
const hasPathSeparator = (command) => command.includes('/') || command.includes('\\');
|
|
4
5
|
const canExecute = (path, platform = process.platform) => {
|
|
5
6
|
try {
|
|
@@ -54,28 +55,11 @@ const isWindowsBatchFile = (command) => {
|
|
|
54
55
|
const extension = extname(command).toLowerCase();
|
|
55
56
|
return extension === '.cmd' || extension === '.bat';
|
|
56
57
|
};
|
|
57
|
-
/**
|
|
58
|
-
* cmd.exe-style escape: doubles inner `"` (cmd's only literal-quote idiom)
|
|
59
|
-
* and only wraps in `"..."` when the token contains whitespace or a cmd
|
|
60
|
-
* metachar. Plain alnum/path tokens stay unquoted, which is the form
|
|
61
|
-
* cmd.exe parses most predictably.
|
|
62
|
-
*
|
|
63
|
-
* Crucially, we do NOT use the `\"` form that the previous implementation
|
|
64
|
-
* used and that node-pty's `argsToCommandLine` produces: cmd doesn't honor
|
|
65
|
-
* backslash-quote escapes in its own command-line parsing.
|
|
66
|
-
*/
|
|
67
|
-
const escapeCmdToken = (value) => {
|
|
68
|
-
if (value.length === 0)
|
|
69
|
-
return '""';
|
|
70
|
-
const doubled = value.replace(/"/g, '""');
|
|
71
|
-
return /[\s"&<>|^()]/.test(value) ? `"${doubled}"` : doubled;
|
|
72
|
-
};
|
|
73
58
|
const buildWindowsBatchCommandLine = (command, args) => {
|
|
74
|
-
const tokens = [command, ...args].map(escapeCmdToken).join(' ');
|
|
75
59
|
// `call` is cmd's built-in batch invocation; it handles quoted .cmd / .bat
|
|
76
60
|
// paths reliably (this is the same pattern Node.js's child_process uses
|
|
77
61
|
// internally on Windows since the CVE-2024-27980 fix).
|
|
78
|
-
return `/d /s /c
|
|
62
|
+
return `/d /s /c ${buildCmdCallCommand(command, args)}`;
|
|
79
63
|
};
|
|
80
64
|
/**
|
|
81
65
|
* Recognize the exact shape that `createStartupCommandLaunch` produces on
|
|
@@ -93,7 +77,7 @@ const isCmdExeShellLaunch = (resolvedCommand, args) => basename(resolvedCommand)
|
|
|
93
77
|
args.length === 4 &&
|
|
94
78
|
args[0] === '/d' &&
|
|
95
79
|
args[1] === '/s' &&
|
|
96
|
-
args[2] === '/c';
|
|
80
|
+
(args[2] === '/c' || args[2] === '/k');
|
|
97
81
|
export const resolveSpawnCommand = (command, cwd, env, args = [], platform = process.platform) => {
|
|
98
82
|
const resolvedCommand = resolveCommandPath(command, cwd, env, platform);
|
|
99
83
|
if (platform === 'win32' && isWindowsBatchFile(resolvedCommand)) {
|
|
@@ -2,7 +2,7 @@ import type { IPty } from 'node-pty';
|
|
|
2
2
|
import type { AgentRunRecord, AgentRunSnapshot } from './agent-manager.js';
|
|
3
3
|
import type { PtyOutputBus } from './pty-output-bus.js';
|
|
4
4
|
export declare const MAX_RUN_OUTPUT_LENGTH = 1000000;
|
|
5
|
-
type ExecRunner = (cmd: string, args: readonly string[]) => void;
|
|
5
|
+
type ExecRunner = (cmd: string, args: readonly string[], done: (success: boolean) => void) => void;
|
|
6
6
|
/**
|
|
7
7
|
* Windows analogue of POSIX `process.kill(-pgid, SIGKILL)`. node-pty on
|
|
8
8
|
* Windows hands `pty.kill()` to TerminateProcess against the PTY's main
|
|
@@ -28,7 +28,7 @@ type ExecRunner = (cmd: string, args: readonly string[]) => void;
|
|
|
28
28
|
* Exported for unit testing — the `runner` parameter lets tests assert
|
|
29
29
|
* the exact argv without mocking node:child_process.
|
|
30
30
|
*/
|
|
31
|
-
export declare const taskkillProcessTree: (pid: number, platform?: NodeJS.Platform, runner?: ExecRunner) => boolean;
|
|
31
|
+
export declare const taskkillProcessTree: (pid: number, platform?: NodeJS.Platform, runner?: ExecRunner, onFailure?: () => void) => boolean;
|
|
32
32
|
export declare const toAgentRunSnapshot: (run: AgentRunRecord) => AgentRunSnapshot;
|
|
33
33
|
export declare const finishAgentRun: (run: AgentRunRecord, exitCode: number | null, ptyOutputBus: PtyOutputBus) => void;
|
|
34
34
|
export declare const attachAgentPty: (run: AgentRunRecord, pty: IPty, ptyOutputBus: PtyOutputBus) => void;
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
1
|
+
import { execFile, execFileSync } from 'node:child_process';
|
|
2
2
|
export const MAX_RUN_OUTPUT_LENGTH = 1_000_000;
|
|
3
3
|
const FORCE_KILL_DELAY_MS = 750;
|
|
4
|
-
const
|
|
5
|
-
|
|
4
|
+
const TASKKILL_TIMEOUT_MS = 3000;
|
|
5
|
+
const PTY_READ_EOF_EXIT_GRACE_MS = 1000;
|
|
6
|
+
const isPtyReadEofError = (error) => {
|
|
7
|
+
const candidate = error;
|
|
8
|
+
return process.platform !== 'win32' && candidate?.code === 'EIO' && candidate.syscall === 'read';
|
|
9
|
+
};
|
|
10
|
+
const serializePtyInput = (input) => Buffer.isBuffer(input) ? input.toString('latin1') : input;
|
|
11
|
+
const defaultExecRunner = (cmd, args, done) => {
|
|
12
|
+
let settled = false;
|
|
13
|
+
const settle = (success) => {
|
|
14
|
+
if (settled)
|
|
15
|
+
return;
|
|
16
|
+
settled = true;
|
|
17
|
+
done(success);
|
|
18
|
+
};
|
|
19
|
+
const child = execFile(cmd, [...args], { maxBuffer: 64 * 1024, timeout: TASKKILL_TIMEOUT_MS, windowsHide: true }, (error) => settle(!error));
|
|
20
|
+
child.once('error', () => settle(false));
|
|
6
21
|
};
|
|
7
22
|
/**
|
|
8
23
|
* Windows analogue of POSIX `process.kill(-pgid, SIGKILL)`. node-pty on
|
|
@@ -29,11 +44,14 @@ const defaultExecRunner = (cmd, args) => {
|
|
|
29
44
|
* Exported for unit testing — the `runner` parameter lets tests assert
|
|
30
45
|
* the exact argv without mocking node:child_process.
|
|
31
46
|
*/
|
|
32
|
-
export const taskkillProcessTree = (pid, platform = process.platform, runner = defaultExecRunner) => {
|
|
47
|
+
export const taskkillProcessTree = (pid, platform = process.platform, runner = defaultExecRunner, onFailure) => {
|
|
33
48
|
if (platform !== 'win32' || pid <= 0)
|
|
34
49
|
return false;
|
|
35
50
|
try {
|
|
36
|
-
runner('taskkill', ['/pid', String(pid), '/t', '/f'])
|
|
51
|
+
runner('taskkill', ['/pid', String(pid), '/t', '/f'], (success) => {
|
|
52
|
+
if (!success)
|
|
53
|
+
onFailure?.();
|
|
54
|
+
});
|
|
37
55
|
return true;
|
|
38
56
|
}
|
|
39
57
|
catch {
|
|
@@ -61,6 +79,7 @@ export const finishAgentRun = (run, exitCode, ptyOutputBus) => {
|
|
|
61
79
|
export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
62
80
|
let stdinClosed = false;
|
|
63
81
|
let forceKillTimer;
|
|
82
|
+
let ptyReadEofTimer;
|
|
64
83
|
const resolveProcessGroupId = () => {
|
|
65
84
|
if (process.platform === 'win32' || pty.pid <= 0)
|
|
66
85
|
return null;
|
|
@@ -99,26 +118,29 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
99
118
|
ignoreBestEffortGroupKillError(error);
|
|
100
119
|
}
|
|
101
120
|
};
|
|
102
|
-
const
|
|
121
|
+
const killPtyDirect = (signal) => {
|
|
103
122
|
try {
|
|
104
|
-
|
|
105
|
-
// taskkill /pid <pid> /t /f walks the parent's process tree
|
|
106
|
-
// BEFORE terminating it — so we have to run it while the parent
|
|
107
|
-
// is still alive. Calling pty.kill() first (the previous
|
|
108
|
-
// ordering) detaches the children: taskkill /T then fails with
|
|
109
|
-
// "process not found" and the npm-installs / build scripts
|
|
110
|
-
// become orphans. taskkill /f also terminates the parent, so
|
|
111
|
-
// pty.kill() is the fallback for the rare case where taskkill
|
|
112
|
-
// is missing from PATH or refused (e.g. restricted PowerShell).
|
|
113
|
-
if (!taskkillProcessTree(pty.pid))
|
|
114
|
-
pty.kill();
|
|
115
|
-
}
|
|
116
|
-
else
|
|
117
|
-
pty.kill(signal);
|
|
123
|
+
pty.kill(signal);
|
|
118
124
|
}
|
|
119
125
|
catch (error) {
|
|
120
126
|
ignoreMissingProcess(error);
|
|
121
127
|
}
|
|
128
|
+
};
|
|
129
|
+
const killPty = (signal) => {
|
|
130
|
+
if (process.platform === 'win32') {
|
|
131
|
+
// taskkill /pid <pid> /t /f walks the parent's process tree
|
|
132
|
+
// BEFORE terminating it — so we have to run it while the parent
|
|
133
|
+
// is still alive. Calling pty.kill() first (the previous
|
|
134
|
+
// ordering) detaches the children: taskkill /T then fails with
|
|
135
|
+
// "process not found" and the npm-installs / build scripts
|
|
136
|
+
// become orphans. taskkill /f also terminates the parent, so
|
|
137
|
+
// pty.kill() is the fallback for the rare case where taskkill
|
|
138
|
+
// is missing from PATH or refused (e.g. restricted PowerShell).
|
|
139
|
+
if (!taskkillProcessTree(pty.pid, process.platform, defaultExecRunner, () => killPtyDirect()))
|
|
140
|
+
killPtyDirect();
|
|
141
|
+
}
|
|
142
|
+
else
|
|
143
|
+
killPtyDirect(signal);
|
|
122
144
|
killProcessGroup(signal);
|
|
123
145
|
};
|
|
124
146
|
const clearForceKillTimer = () => {
|
|
@@ -127,8 +149,34 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
127
149
|
clearTimeout(forceKillTimer);
|
|
128
150
|
forceKillTimer = undefined;
|
|
129
151
|
};
|
|
152
|
+
const clearPtyReadEofTimer = () => {
|
|
153
|
+
if (!ptyReadEofTimer)
|
|
154
|
+
return;
|
|
155
|
+
clearTimeout(ptyReadEofTimer);
|
|
156
|
+
ptyReadEofTimer = undefined;
|
|
157
|
+
};
|
|
158
|
+
const schedulePtyReadEofExitGuard = (error) => {
|
|
159
|
+
if (ptyReadEofTimer)
|
|
160
|
+
return;
|
|
161
|
+
ptyReadEofTimer = setTimeout(() => {
|
|
162
|
+
ptyReadEofTimer = undefined;
|
|
163
|
+
if (stopped())
|
|
164
|
+
return;
|
|
165
|
+
console.error(`[hive] PTY read EOF without exit for run ${run.runId}`, error);
|
|
166
|
+
finishAgentRun(run, null, ptyOutputBus);
|
|
167
|
+
try {
|
|
168
|
+
killPty('SIGTERM');
|
|
169
|
+
scheduleForceKill();
|
|
170
|
+
}
|
|
171
|
+
catch (killError) {
|
|
172
|
+
ignoreMissingProcess(killError);
|
|
173
|
+
}
|
|
174
|
+
}, PTY_READ_EOF_EXIT_GRACE_MS);
|
|
175
|
+
ptyReadEofTimer.unref?.();
|
|
176
|
+
};
|
|
130
177
|
const cleanupProcessGroup = () => {
|
|
131
178
|
clearForceKillTimer();
|
|
179
|
+
clearPtyReadEofTimer();
|
|
132
180
|
killProcessGroup('SIGKILL');
|
|
133
181
|
};
|
|
134
182
|
const scheduleForceKill = () => {
|
|
@@ -141,11 +189,11 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
141
189
|
// Same ordering as killPty(): tree-kill before terminating the
|
|
142
190
|
// parent, so taskkill /T can still enumerate the process tree.
|
|
143
191
|
// pty.kill() is the fallback for taskkill-missing hosts.
|
|
144
|
-
if (!taskkillProcessTree(pty.pid))
|
|
145
|
-
|
|
192
|
+
if (!taskkillProcessTree(pty.pid, process.platform, defaultExecRunner, () => killPtyDirect()))
|
|
193
|
+
killPtyDirect();
|
|
146
194
|
}
|
|
147
195
|
else
|
|
148
|
-
|
|
196
|
+
killPtyDirect('SIGKILL');
|
|
149
197
|
}
|
|
150
198
|
catch (error) {
|
|
151
199
|
ignoreMissingProcess(error);
|
|
@@ -163,6 +211,9 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
163
211
|
},
|
|
164
212
|
pid: pty.pid,
|
|
165
213
|
resize(cols, rows) {
|
|
214
|
+
if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols <= 0 || rows <= 0) {
|
|
215
|
+
throw new Error(`Invalid terminal size for run: ${run.runId}`);
|
|
216
|
+
}
|
|
166
217
|
pty.resize(cols, rows);
|
|
167
218
|
},
|
|
168
219
|
resume() {
|
|
@@ -173,6 +224,7 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
173
224
|
cleanupProcessGroup();
|
|
174
225
|
return;
|
|
175
226
|
}
|
|
227
|
+
clearPtyReadEofTimer();
|
|
176
228
|
killPty('SIGTERM');
|
|
177
229
|
stdinClosed = true;
|
|
178
230
|
scheduleForceKill();
|
|
@@ -181,7 +233,7 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
181
233
|
if (stdinClosed || run.status === 'exited' || run.status === 'error') {
|
|
182
234
|
throw new Error(`PTY is not active for run: ${run.runId}`);
|
|
183
235
|
}
|
|
184
|
-
pty.write(text);
|
|
236
|
+
pty.write(serializePtyInput(text));
|
|
185
237
|
},
|
|
186
238
|
};
|
|
187
239
|
pty.onData((chunk) => {
|
|
@@ -192,8 +244,30 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
192
244
|
run.output = run.output.slice(-MAX_RUN_OUTPUT_LENGTH);
|
|
193
245
|
ptyOutputBus.publish(run.runId, chunk);
|
|
194
246
|
});
|
|
247
|
+
pty.on?.('error', (error) => {
|
|
248
|
+
if (stopped())
|
|
249
|
+
return;
|
|
250
|
+
if (isPtyReadEofError(error)) {
|
|
251
|
+
// Unix PTYs can surface a closed slave as read/EIO just before
|
|
252
|
+
// node-pty delivers the real onExit event. Treat it as EOF, not
|
|
253
|
+
// as the run's terminal status.
|
|
254
|
+
stdinClosed = true;
|
|
255
|
+
schedulePtyReadEofExitGuard(error);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
console.error(`[hive] PTY error for run ${run.runId}`, error);
|
|
259
|
+
stdinClosed = true;
|
|
260
|
+
finishAgentRun(run, null, ptyOutputBus);
|
|
261
|
+
try {
|
|
262
|
+
killPty('SIGTERM');
|
|
263
|
+
}
|
|
264
|
+
catch (killError) {
|
|
265
|
+
ignoreMissingProcess(killError);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
195
268
|
pty.onExit((event) => {
|
|
196
269
|
stdinClosed = true;
|
|
270
|
+
clearPtyReadEofTimer();
|
|
197
271
|
cleanupProcessGroup();
|
|
198
272
|
finishAgentRun(run, event.exitCode, ptyOutputBus);
|
|
199
273
|
});
|
|
@@ -6,6 +6,7 @@ import type { AgentSessionStorePort } from './agent-runtime-ports.js';
|
|
|
6
6
|
import type { LiveAgentRun } from './agent-runtime-types.js';
|
|
7
7
|
import type { AgentTokenRegistry } from './agent-tokens.js';
|
|
8
8
|
import type { CommandPresetRecord } from './command-preset-store.js';
|
|
9
|
+
import { type FeatureFlags } from './feature-flags.js';
|
|
9
10
|
import type { LiveRunRegistry } from './live-run-registry.js';
|
|
10
11
|
import type { RestartPolicy } from './restart-policy.js';
|
|
11
12
|
interface AgentRunStarterInput {
|
|
@@ -18,6 +19,10 @@ interface AgentRunStarterInput {
|
|
|
18
19
|
getCommandPreset: (id: string) => CommandPresetRecord | undefined;
|
|
19
20
|
getAgent: ((workspaceId: string, agentId: string) => AgentSummary | undefined) | undefined;
|
|
20
21
|
restartPolicy: RestartPolicy;
|
|
22
|
+
/** Resolves the live experimental flags for the orchestrator's startup
|
|
23
|
+
* prompt (`workflowsEnabled` gates the `team workflow` line + authoring
|
|
24
|
+
* rule; `autostaffEnabled` gates the team-sizing rule). */
|
|
25
|
+
getFlags?: () => FeatureFlags;
|
|
21
26
|
}
|
|
22
|
-
export declare const createAgentRunStarter: ({ agentManager, registry, onAgentExit, store, sessionStore, tokenRegistry, getCommandPreset, getAgent, restartPolicy, }: AgentRunStarterInput) => (workspace: WorkspaceSummary, agentId: string, config: AgentLaunchConfigInput, hivePort: string) => Promise<LiveAgentRun>;
|
|
27
|
+
export declare const createAgentRunStarter: ({ agentManager, registry, onAgentExit, store, sessionStore, tokenRegistry, getCommandPreset, getAgent, restartPolicy, getFlags, }: AgentRunStarterInput) => (workspace: WorkspaceSummary, agentId: string, config: AgentLaunchConfigInput, hivePort: string) => Promise<LiveAgentRun>;
|
|
23
28
|
export {};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { buildAgentRunBootstrap, startAgentRunCapture } from './agent-run-bootstrap.js';
|
|
2
2
|
import { handleAgentRunExit } from './agent-run-exit-handler.js';
|
|
3
3
|
import { buildAgentStartupInstructions } from './agent-startup-instructions.js';
|
|
4
|
+
import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
|
|
4
5
|
import { createPostStartInputWriter, isInteractiveAgentCommand } from './post-start-input-writer.js';
|
|
5
|
-
export const createAgentRunStarter = ({ agentManager, registry, onAgentExit, store, sessionStore, tokenRegistry, getCommandPreset, getAgent, restartPolicy, }) => async (workspace, agentId, config, hivePort) => {
|
|
6
|
+
export const createAgentRunStarter = ({ agentManager, registry, onAgentExit, store, sessionStore, tokenRegistry, getCommandPreset, getAgent, restartPolicy, getFlags, }) => async (workspace, agentId, config, hivePort) => {
|
|
6
7
|
if (!agentManager)
|
|
7
8
|
throw new Error('Agent manager is required to start agents');
|
|
8
9
|
const agent = getAgent?.(workspace.id, agentId);
|
|
@@ -99,7 +100,13 @@ export const createAgentRunStarter = ({ agentManager, registry, onAgentExit, sto
|
|
|
99
100
|
!injectedRestartMessage &&
|
|
100
101
|
agent &&
|
|
101
102
|
isInteractiveAgentCommand(startConfig.interactiveCommand ?? startConfig.command)) {
|
|
102
|
-
postStartWriter(run.runId, buildAgentStartupInstructions({
|
|
103
|
+
void postStartWriter(run.runId, buildAgentStartupInstructions({
|
|
104
|
+
agent,
|
|
105
|
+
workspace,
|
|
106
|
+
flags: getFlags?.() ?? FEATURE_FLAGS_ALL_OFF,
|
|
107
|
+
})).catch(() => {
|
|
108
|
+
// The agent may have exited before post-start guidance could be written.
|
|
109
|
+
});
|
|
103
110
|
}
|
|
104
111
|
}
|
|
105
112
|
catch {
|
|
@@ -27,7 +27,7 @@ export declare const createAgentRunStore: (db: Database) => {
|
|
|
27
27
|
runId: string;
|
|
28
28
|
agentId: string;
|
|
29
29
|
pid: number | null;
|
|
30
|
-
status: "
|
|
30
|
+
status: "starting" | "running" | "exited" | "error";
|
|
31
31
|
exitCode: number | null;
|
|
32
32
|
startedAt: number;
|
|
33
33
|
endedAt: number | null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentManager } from './agent-manager.js';
|
|
2
2
|
import type { LiveAgentRun } from './agent-runtime-types.js';
|
|
3
3
|
import type { LiveRunRegistry } from './live-run-registry.js';
|
|
4
|
+
export declare const AGENT_RUNTIME_CLOSE_TIMEOUT_MS = 5000;
|
|
4
5
|
export declare const closeAgentRuntime: (agentManager: AgentManager | undefined, registry: LiveRunRegistry, syncRun: (run: LiveAgentRun) => LiveAgentRun) => Promise<void>;
|
|
@@ -1,10 +1,34 @@
|
|
|
1
|
+
export const AGENT_RUNTIME_CLOSE_TIMEOUT_MS = 5000;
|
|
2
|
+
const waitForExitEntries = async (entries, timeoutMs = AGENT_RUNTIME_CLOSE_TIMEOUT_MS) => {
|
|
3
|
+
if (entries.length === 0)
|
|
4
|
+
return;
|
|
5
|
+
let timer;
|
|
6
|
+
const timeout = new Promise((resolve) => {
|
|
7
|
+
timer = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
8
|
+
timer.unref?.();
|
|
9
|
+
});
|
|
10
|
+
try {
|
|
11
|
+
const result = await Promise.race([
|
|
12
|
+
Promise.all(entries.map((entry) => entry.promise)).then(() => 'done'),
|
|
13
|
+
timeout,
|
|
14
|
+
]);
|
|
15
|
+
if (result === 'timeout') {
|
|
16
|
+
const runIds = entries.map((entry) => entry.runId).join(', ');
|
|
17
|
+
console.error(`[hive] timed out waiting for agent exit during shutdown: ${runIds}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
if (timer)
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
1
25
|
export const closeAgentRuntime = async (agentManager, registry, syncRun) => {
|
|
2
26
|
const runs = registry.list();
|
|
3
27
|
for (const run of runs) {
|
|
4
28
|
syncRun(run);
|
|
5
29
|
agentManager?.stopRun(run.runId);
|
|
6
30
|
}
|
|
7
|
-
await
|
|
31
|
+
await waitForExitEntries(registry.listExitEntries());
|
|
8
32
|
for (const run of registry.list()) {
|
|
9
33
|
agentManager?.removeRun(run.runId);
|
|
10
34
|
registry.remove(run.runId);
|
|
@@ -28,10 +28,21 @@ export interface AgentRuntime {
|
|
|
28
28
|
writeStatusPrompt: (workspaceId: string, workerName: string, workerId: string, text: string, artifacts: string[], input?: {
|
|
29
29
|
requireActiveRun?: boolean;
|
|
30
30
|
}) => void;
|
|
31
|
-
writeSendPrompt: (workspaceId: string, workerId: string, dispatchId: string, fromAgentName: string, workerDescription: string, text: string) => void
|
|
31
|
+
writeSendPrompt: (workspaceId: string, workerId: string, dispatchId: string, fromAgentName: string, workerDescription: string, text: string) => Promise<void>;
|
|
32
32
|
writeCancelPrompt: (workspaceId: string, workerId: string, dispatchId: string, reason: string, input?: {
|
|
33
33
|
requireActiveRun?: boolean;
|
|
34
34
|
}) => void;
|
|
35
35
|
writeUserInputPrompt: (workspaceId: string, text: string) => void;
|
|
36
|
+
writeSystemMessageToAgent: (workspaceId: string, agentId: string, text: string) => void;
|
|
37
|
+
/** Awaitable report delivery — rejects if the orchestrator PTY write fails,
|
|
38
|
+
* so the caller can persist the report to the redelivery outbox. */
|
|
39
|
+
deliverReportToOrchestrator: (workspaceId: string, workerName: string, text: string, artifacts: string[], input?: {
|
|
40
|
+
requireActiveRun?: boolean;
|
|
41
|
+
}) => Promise<void>;
|
|
42
|
+
/** Awaitable opaque delivery — used to drain the report outbox so an entry
|
|
43
|
+
* is marked delivered only after the PTY write resolves. */
|
|
44
|
+
deliverSystemMessageToAgent: (workspaceId: string, agentId: string, text: string, input?: {
|
|
45
|
+
requireActiveRun?: boolean;
|
|
46
|
+
}) => Promise<void>;
|
|
36
47
|
}
|
|
37
48
|
export type { StartAgentOptions };
|