@tt-a1i/hive 1.5.0 → 1.7.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 +39 -0
- package/README.en.md +23 -1
- package/README.md +10 -1
- package/dist/src/cli/team.js +19 -2
- package/dist/src/server/agent-run-bootstrap.d.ts +6 -1
- package/dist/src/server/agent-run-bootstrap.js +5 -2
- package/dist/src/server/agent-run-starter.d.ts +6 -7
- package/dist/src/server/agent-run-starter.js +18 -4
- package/dist/src/server/agent-runtime-contract.d.ts +10 -0
- 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 +10 -5
- package/dist/src/server/agent-startup-instructions.d.ts +7 -8
- package/dist/src/server/agent-startup-instructions.js +6 -4
- package/dist/src/server/agent-stdin-dispatcher.d.ts +20 -7
- package/dist/src/server/agent-stdin-dispatcher.js +22 -10
- package/dist/src/server/command-preset-defaults.js +12 -0
- package/dist/src/server/feature-flags.d.ts +42 -0
- package/dist/src/server/feature-flags.js +24 -0
- package/dist/src/server/hive-team-guidance.d.ts +4 -3
- package/dist/src/server/hive-team-guidance.js +17 -16
- package/dist/src/server/post-start-input-writer.js +2 -2
- package/dist/src/server/preset-launch-support.js +2 -1
- package/dist/src/server/recovery-summary.d.ts +5 -6
- package/dist/src/server/recovery-summary.js +3 -2
- 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 +4 -5
- package/dist/src/server/restart-policy.d.ts +5 -1
- package/dist/src/server/restart-policy.js +51 -33
- package/dist/src/server/routes-settings.js +3 -3
- package/dist/src/server/routes-tasks.js +23 -0
- package/dist/src/server/routes-workspaces.js +5 -0
- package/dist/src/server/runtime-restart-policy.d.ts +3 -3
- package/dist/src/server/runtime-restart-policy.js +2 -3
- package/dist/src/server/runtime-store-contract.d.ts +3 -0
- package/dist/src/server/runtime-store-helpers.d.ts +2 -0
- package/dist/src/server/runtime-store-helpers.js +14 -9
- package/dist/src/server/runtime-store-workflows.js +8 -0
- package/dist/src/server/runtime-store.js +1 -0
- package/dist/src/server/session-capture.d.ts +6 -0
- package/dist/src/server/session-capture.js +32 -0
- package/dist/src/server/sqlite-schema-v22.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v22.js +27 -0
- package/dist/src/server/sqlite-schema.d.ts +1 -1
- package/dist/src/server/sqlite-schema.js +19 -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 +6 -7
- package/dist/src/server/tasks-file-watcher.js +3 -2
- package/dist/src/server/tasks-file.d.ts +2 -1
- package/dist/src/server/tasks-file.js +3 -2
- package/dist/src/server/team-authz.d.ts +1 -1
- package/dist/src/server/team-authz.js +1 -0
- package/dist/src/server/team-operations.d.ts +7 -1
- package/dist/src/server/team-operations.js +81 -19
- package/dist/src/server/webhook-notifier.d.ts +34 -0
- package/dist/src/server/webhook-notifier.js +47 -0
- package/dist/src/server/workflow-cli-policy.d.ts +1 -1
- package/dist/src/server/workflow-cli-policy.js +1 -1
- 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-runner.js +12 -2
- package/dist/src/shared/types.d.ts +2 -2
- package/package.json +1 -1
- package/web/dist/assets/{AddWorkerDialog-CcC-7kgG.js → AddWorkerDialog-BRUxpa3f.js} +2 -2
- package/web/dist/assets/{AddWorkspaceDialog-BDpOTfmt.js → AddWorkspaceDialog-D56x5JCb.js} +1 -1
- package/web/dist/assets/{FirstRunWizard-BYX_ocQn.js → FirstRunWizard-BFVaMIsE.js} +1 -1
- package/web/dist/assets/{MarketplaceDrawer-DUxSk7db.js → MarketplaceDrawer-DeEZ35dN.js} +1 -1
- package/web/dist/assets/{WhatsNewDialog-B_RlCXcV.js → WhatsNewDialog-CHkZeINH.js} +1 -1
- package/web/dist/assets/{WorkerModal-D9-7YfZZ.js → WorkerModal-BBCuMLIa.js} +1 -1
- package/web/dist/assets/{WorkspaceTaskDrawer-BCKoF7qc.js → WorkspaceTaskDrawer-CpZHAcj1.js} +1 -1
- package/web/dist/assets/WorkspaceTerminalPanels-7If2mDyp.js +1 -0
- package/web/dist/assets/index-5zh61jMg.css +1 -0
- package/web/dist/assets/index-CxNL0O-C.js +73 -0
- package/web/dist/cli-icons/hermes.png +0 -0
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/WorkspaceTerminalPanels-Dq8y91t2.js +0 -1
- package/web/dist/assets/index-BiOvKIVw.css +0 -1
- package/web/dist/assets/index-DMRUklT3.js +0 -73
|
@@ -2,21 +2,22 @@ import { createAgentRunStore } from './agent-run-store.js';
|
|
|
2
2
|
import { createAgentRuntime } from './agent-runtime.js';
|
|
3
3
|
import { createAgentSessionStore } from './agent-session-store.js';
|
|
4
4
|
import { createDispatchLedgerStore } from './dispatch-ledger-store.js';
|
|
5
|
+
import { readFeatureFlags } from './feature-flags.js';
|
|
5
6
|
import { createMessageLogStore } from './message-log-store.js';
|
|
6
7
|
import { seedOrchestratorLaunchConfig } from './orchestrator-launch.js';
|
|
8
|
+
import { createReportOutboxStore } from './report-outbox-store.js';
|
|
7
9
|
import { openRuntimeDatabase } from './runtime-database.js';
|
|
8
10
|
import { buildRuntimeRestartPolicy } from './runtime-restart-policy.js';
|
|
9
11
|
import { createSettingsStore } from './settings-store.js';
|
|
10
12
|
import { createTasksFileService } from './tasks-file.js';
|
|
11
13
|
import { createTasksFileWatcher } from './tasks-file-watcher.js';
|
|
12
|
-
import { AUTOSTAFF_ENABLED_KEY, readAutostaffEnabled } from './team-autostaff.js';
|
|
13
14
|
import { createTeamOperations } from './team-operations.js';
|
|
14
15
|
import { resolveTerminalInputProfile } from './terminal-input-profile.js';
|
|
15
16
|
import { createUiAuth } from './ui-auth.js';
|
|
17
|
+
import { createWebhookNotifier, WEBHOOK_URL_KEY } from './webhook-notifier.js';
|
|
16
18
|
import { createWorkerOutputTracker } from './worker-output-tracker.js';
|
|
17
19
|
import { readWorkflowCliPolicy, WORKFLOW_CLI_POLICY_KEY } from './workflow-cli-policy.js';
|
|
18
20
|
import { createWorkflowDispatchAwaiter, } from './workflow-dispatch-awaiter.js';
|
|
19
|
-
import { readWorkflowEnabled, WORKFLOW_ENABLED_KEY } from './workflow-feature.js';
|
|
20
21
|
import { createWorkflowRunLogStore } from './workflow-run-log-store.js';
|
|
21
22
|
import { createWorkflowRunStore } from './workflow-run-store.js';
|
|
22
23
|
import { createWorkflowScheduleStore } from './workflow-schedule-store.js';
|
|
@@ -35,6 +36,7 @@ export const createRuntimeStoreServices = (options = {}) => {
|
|
|
35
36
|
const db = openRuntimeDatabase(options.dataDir);
|
|
36
37
|
const messageLogStore = createMessageLogStore(db);
|
|
37
38
|
const dispatchLedgerStore = createDispatchLedgerStore(db);
|
|
39
|
+
const reportOutbox = createReportOutboxStore(db);
|
|
38
40
|
const workflowDispatchAwaiter = createWorkflowDispatchAwaiter();
|
|
39
41
|
const workflowRunStore = createWorkflowRunStore(db);
|
|
40
42
|
const workflowRunLogStore = createWorkflowRunLogStore(db);
|
|
@@ -42,8 +44,10 @@ export const createRuntimeStoreServices = (options = {}) => {
|
|
|
42
44
|
const agentRunStore = createAgentRunStore(db);
|
|
43
45
|
const agentSessionStore = createAgentSessionStore(db);
|
|
44
46
|
const settings = createSettingsStore(db);
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
+
const getFlags = () => readFeatureFlags(settings);
|
|
48
|
+
const webhookNotifier = createWebhookNotifier({
|
|
49
|
+
getUrl: () => settings.getAppState(WEBHOOK_URL_KEY)?.value ?? null,
|
|
50
|
+
});
|
|
47
51
|
const tasksFileService = createTasksFileService();
|
|
48
52
|
const tasksFileWatchCallbacks = new Set();
|
|
49
53
|
const tasksFileWatcher = createTasksFileWatcher({
|
|
@@ -51,8 +55,7 @@ export const createRuntimeStoreServices = (options = {}) => {
|
|
|
51
55
|
notifyTasksUpdated(tasksFileWatchCallbacks, workspaceId, content);
|
|
52
56
|
},
|
|
53
57
|
getWorkflowCliPolicy: () => readWorkflowCliPolicy(settings.getAppState(WORKFLOW_CLI_POLICY_KEY)?.value ?? null),
|
|
54
|
-
|
|
55
|
-
getAutostaffEnabled,
|
|
58
|
+
getFlags,
|
|
56
59
|
});
|
|
57
60
|
const uiAuth = createUiAuth();
|
|
58
61
|
const shellRuntime = createWorkspaceShellRuntime(options.agentManager);
|
|
@@ -71,8 +74,7 @@ export const createRuntimeStoreServices = (options = {}) => {
|
|
|
71
74
|
messageLogStore,
|
|
72
75
|
tasksFileService,
|
|
73
76
|
workspaceStore,
|
|
74
|
-
|
|
75
|
-
getAutostaffEnabled,
|
|
77
|
+
getFlags,
|
|
76
78
|
});
|
|
77
79
|
const workerOutputTracker = options.agentManager
|
|
78
80
|
? createWorkerOutputTracker(options.agentManager.getOutputBus())
|
|
@@ -119,7 +121,7 @@ export const createRuntimeStoreServices = (options = {}) => {
|
|
|
119
121
|
for (const child of children)
|
|
120
122
|
removeWorkerCompletely(workspaceId, child.id);
|
|
121
123
|
}
|
|
122
|
-
}, restartPolicy, (workspaceId, agentId) => workspaceStore.getAgent(workspaceId, agentId),
|
|
124
|
+
}, restartPolicy, (workspaceId, agentId) => workspaceStore.getAgent(workspaceId, agentId), getFlags);
|
|
123
125
|
// Mirrors runtime-store.deleteWorker (stop run → drop launch config → drop
|
|
124
126
|
// dispatches → drop worker row, transactionally). Hoisted `function` so the
|
|
125
127
|
// onAgentExit closure above can reference it; only invoked at runtime, after
|
|
@@ -157,6 +159,8 @@ export const createRuntimeStoreServices = (options = {}) => {
|
|
|
157
159
|
markDispatchCancelled: dispatchLedgerStore.markCancelled,
|
|
158
160
|
markDispatchReportedByWorker: dispatchLedgerStore.markReportedByWorker,
|
|
159
161
|
markDispatchSubmitted: dispatchLedgerStore.markSubmitted,
|
|
162
|
+
reportOutbox,
|
|
163
|
+
notifyWebhook: webhookNotifier.notify,
|
|
160
164
|
workflowDispatchAwaiter,
|
|
161
165
|
workspaceStore,
|
|
162
166
|
dismissEphemeralWorker: (workspaceId, workerId) => removeWorkerCompletely(workspaceId, workerId),
|
|
@@ -176,6 +180,7 @@ export const createRuntimeStoreServices = (options = {}) => {
|
|
|
176
180
|
tasksFileService,
|
|
177
181
|
teamOps,
|
|
178
182
|
uiAuth,
|
|
183
|
+
webhookNotifier,
|
|
179
184
|
workerOutputTracker,
|
|
180
185
|
workflowDispatchAwaiter,
|
|
181
186
|
workflowRunLogStore,
|
|
@@ -78,6 +78,14 @@ export const createRuntimeStoreWorkflowRuntime = (services, store) => {
|
|
|
78
78
|
catch (error) {
|
|
79
79
|
console.error('[hive] workflow.notifyTrigger failed', error);
|
|
80
80
|
}
|
|
81
|
+
// Outbound completion webhook (best-effort) so the user is pinged when a
|
|
82
|
+
// long fan-out finishes without watching the drawer.
|
|
83
|
+
services.webhookNotifier.notify({
|
|
84
|
+
type: 'workflow_finished',
|
|
85
|
+
workspaceId,
|
|
86
|
+
summary: `${finalRecord.name}: ${finalRecord.status}${finalRecord.error ? ` — ${finalRecord.error}` : ''}`,
|
|
87
|
+
at: Date.now(),
|
|
88
|
+
});
|
|
81
89
|
},
|
|
82
90
|
store: {
|
|
83
91
|
addWorkerWithLaunch: (ws, input, launch) => store.addWorkerWithLaunch(ws, input, launch),
|
|
@@ -93,6 +93,7 @@ export const createRuntimeStore = (options = {}) => {
|
|
|
93
93
|
dispatchTask: services.teamOps.dispatchTask,
|
|
94
94
|
dispatchTaskByWorkerName: services.teamOps.dispatchTaskByWorkerName,
|
|
95
95
|
reportTask: services.teamOps.reportTask,
|
|
96
|
+
drainReportOutbox: services.teamOps.drainReportOutbox,
|
|
96
97
|
statusTask: services.teamOps.statusTask,
|
|
97
98
|
listDispatches: services.dispatchLedgerStore.listWorkspaceDispatches,
|
|
98
99
|
listWorkers: (workspaceId) => services.workspaceStore.listWorkers(workspaceId),
|
|
@@ -21,6 +21,7 @@ export interface SessionCaptureSnapshot {
|
|
|
21
21
|
discriminator?: {
|
|
22
22
|
contentIncludes: string | readonly string[];
|
|
23
23
|
};
|
|
24
|
+
getOutput?: () => string | null;
|
|
24
25
|
knownSessionIds: Set<string>;
|
|
25
26
|
env?: Record<string, string>;
|
|
26
27
|
root?: string;
|
|
@@ -65,7 +66,12 @@ export declare const snapshotSessionIdsForCapture: (cwd: string, capture: Sessio
|
|
|
65
66
|
};
|
|
66
67
|
knownSessionIds: Set<string>;
|
|
67
68
|
root: string;
|
|
69
|
+
} | {
|
|
70
|
+
knownSessionIds: Set<string>;
|
|
71
|
+
env?: never;
|
|
72
|
+
root?: never;
|
|
68
73
|
} | undefined;
|
|
69
74
|
export declare const getSessionCaptureEnvironment: (snapshot: SessionCaptureSnapshot | undefined) => Record<string, string>;
|
|
70
75
|
export declare const captureSessionIdForCapture: (cwd: string, capture: SessionIdCaptureConfig, snapshot: SessionCaptureSnapshot, onCapture: (sessionId: string) => void, timeoutMs?: number, intervalMs?: number) => Promise<void>;
|
|
71
76
|
export declare const doesCapturedSessionExist: (cwd: string, capture: SessionIdCaptureConfig, sessionId: string, discriminator?: SessionCaptureSnapshot["discriminator"]) => boolean;
|
|
77
|
+
export declare const captureStdoutRegexSessionId: (pattern: string, getOutput: (() => string | null) | undefined, knownSessionIds: Set<string>, onCapture: (sessionId: string) => void, timeoutMs?: number, intervalMs?: number) => Promise<void>;
|
|
@@ -57,6 +57,11 @@ export const snapshotSessionIdsForCapture = (cwd, capture, discriminator) => {
|
|
|
57
57
|
root: dbPath,
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
+
if (capture.source === 'stdout_regex') {
|
|
61
|
+
return {
|
|
62
|
+
knownSessionIds: new Set(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
60
65
|
return undefined;
|
|
61
66
|
};
|
|
62
67
|
export const getSessionCaptureEnvironment = (snapshot) => snapshot?.env ?? {};
|
|
@@ -73,6 +78,9 @@ export const captureSessionIdForCapture = async (cwd, capture, snapshot, onCaptu
|
|
|
73
78
|
if (capture.source === 'opencode_session_db') {
|
|
74
79
|
await captureOpenCodeSessionId(cwd, snapshot.knownSessionIds, onCapture, timeoutMs, intervalMs, snapshot.root);
|
|
75
80
|
}
|
|
81
|
+
if (capture.source === 'stdout_regex') {
|
|
82
|
+
await captureStdoutRegexSessionId(capture.pattern, snapshot.getOutput, snapshot.knownSessionIds, onCapture, timeoutMs, intervalMs);
|
|
83
|
+
}
|
|
76
84
|
};
|
|
77
85
|
export const doesCapturedSessionExist = (cwd, capture, sessionId, discriminator) => {
|
|
78
86
|
if (capture.source === 'claude_project_jsonl_dir') {
|
|
@@ -89,3 +97,27 @@ export const doesCapturedSessionExist = (cwd, capture, sessionId, discriminator)
|
|
|
89
97
|
}
|
|
90
98
|
return false;
|
|
91
99
|
};
|
|
100
|
+
const compileCaptureRegex = (pattern) => {
|
|
101
|
+
try {
|
|
102
|
+
return new RegExp(pattern, 'u');
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
export const captureStdoutRegexSessionId = async (pattern, getOutput, knownSessionIds, onCapture, timeoutMs = 5000, intervalMs = 100) => {
|
|
109
|
+
const regex = compileCaptureRegex(pattern);
|
|
110
|
+
if (!regex || !getOutput)
|
|
111
|
+
return;
|
|
112
|
+
const deadline = Date.now() + timeoutMs;
|
|
113
|
+
while (Date.now() <= deadline) {
|
|
114
|
+
const output = getOutput();
|
|
115
|
+
const match = output ? regex.exec(output) : null;
|
|
116
|
+
const sessionId = match?.[1];
|
|
117
|
+
if (sessionId && !knownSessionIds.has(sessionId)) {
|
|
118
|
+
onCapture(sessionId);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
122
|
+
}
|
|
123
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const HERMES_SESSION_ID_CAPTURE = {
|
|
2
|
+
pattern: String.raw `Session:\s*([A-Za-z0-9_-]+)`,
|
|
3
|
+
source: 'stdout_regex',
|
|
4
|
+
};
|
|
5
|
+
const HERMES_YOLO_ARGS = ['--yolo'];
|
|
6
|
+
export const applySchemaVersion22 = (db) => {
|
|
7
|
+
const hasCommandPresets = db
|
|
8
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'command_presets'")
|
|
9
|
+
.get();
|
|
10
|
+
if (!hasCommandPresets)
|
|
11
|
+
return;
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
db.prepare(`INSERT INTO command_presets (
|
|
14
|
+
id, display_name, command, args, env, resume_args_template, session_id_capture,
|
|
15
|
+
yolo_args_template, is_builtin, created_at, updated_at
|
|
16
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
17
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
18
|
+
display_name = excluded.display_name,
|
|
19
|
+
command = excluded.command,
|
|
20
|
+
args = excluded.args,
|
|
21
|
+
env = excluded.env,
|
|
22
|
+
resume_args_template = excluded.resume_args_template,
|
|
23
|
+
session_id_capture = excluded.session_id_capture,
|
|
24
|
+
yolo_args_template = excluded.yolo_args_template,
|
|
25
|
+
updated_at = excluded.updated_at
|
|
26
|
+
WHERE command_presets.is_builtin = 1`).run('hermes', 'Hermes', 'hermes', '[]', '{}', '--resume {session_id}', JSON.stringify(HERMES_SESSION_ID_CAPTURE), JSON.stringify(HERMES_YOLO_ARGS), now, now);
|
|
27
|
+
};
|
|
@@ -14,7 +14,8 @@ import { applySchemaVersion18 } from './sqlite-schema-v18.js';
|
|
|
14
14
|
import { applySchemaVersion19 } from './sqlite-schema-v19.js';
|
|
15
15
|
import { applySchemaVersion20 } from './sqlite-schema-v20.js';
|
|
16
16
|
import { applySchemaVersion21 } from './sqlite-schema-v21.js';
|
|
17
|
-
|
|
17
|
+
import { applySchemaVersion22 } from './sqlite-schema-v22.js';
|
|
18
|
+
export const CURRENT_SCHEMA_VERSION = 22;
|
|
18
19
|
// Idempotent column-add helper. SQLite doesn't have `ALTER TABLE … ADD COLUMN
|
|
19
20
|
// IF NOT EXISTS`, so PRAGMA-check first. Safe to call on every init; required
|
|
20
21
|
// for foreign-built DBs where the version-gated migration is skipped.
|
|
@@ -118,6 +119,19 @@ export const initializeRuntimeDatabase = (db) => {
|
|
|
118
119
|
CREATE INDEX IF NOT EXISTS idx_dispatches_open_by_worker
|
|
119
120
|
ON dispatches (workspace_id, to_agent_id, status, sequence);
|
|
120
121
|
|
|
122
|
+
CREATE TABLE IF NOT EXISTS report_outbox (
|
|
123
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
124
|
+
workspace_id TEXT NOT NULL,
|
|
125
|
+
target_agent_id TEXT NOT NULL,
|
|
126
|
+
dispatch_id TEXT NOT NULL UNIQUE,
|
|
127
|
+
payload TEXT NOT NULL,
|
|
128
|
+
created_at INTEGER NOT NULL,
|
|
129
|
+
delivered_at INTEGER
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
CREATE INDEX IF NOT EXISTS idx_report_outbox_pending
|
|
133
|
+
ON report_outbox (workspace_id, target_agent_id, delivered_at, created_at);
|
|
134
|
+
|
|
121
135
|
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
122
136
|
id TEXT PRIMARY KEY,
|
|
123
137
|
workspace_id TEXT NOT NULL,
|
|
@@ -299,4 +313,8 @@ export const initializeRuntimeDatabase = (db) => {
|
|
|
299
313
|
applySchemaVersion21(db);
|
|
300
314
|
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(21, Date.now());
|
|
301
315
|
}
|
|
316
|
+
if (!appliedVersions.has(22)) {
|
|
317
|
+
applySchemaVersion22(db);
|
|
318
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(22, Date.now());
|
|
319
|
+
}
|
|
302
320
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional, read-only task-dependency support for `.hive/tasks.md`.
|
|
3
|
+
*
|
|
4
|
+
* A task line may carry a trailing `[needs: #2, #5]` annotation declaring that
|
|
5
|
+
* it depends on other tasks (referenced by their 1-based position among the
|
|
6
|
+
* GFM task-list items). `team next` uses this to surface the tasks that are
|
|
7
|
+
* currently runnable — not done, and with every dependency already checked off.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately scoped: parsing happens here on the server only and is purely a
|
|
10
|
+
* READ — Hive never auto-dispatches a runnable task and never writes the
|
|
11
|
+
* annotation back into tasks.md (that file stays a human/orchestrator-edited,
|
|
12
|
+
* git-mergeable artifact). The web task renderer is intentionally left
|
|
13
|
+
* untouched, so the annotation just shows as literal text in the UI.
|
|
14
|
+
*/
|
|
15
|
+
export interface ParsedTaskLine {
|
|
16
|
+
/** 1-based position among task-list items — the `#n` that `[needs:]` uses. */
|
|
17
|
+
index: number;
|
|
18
|
+
done: boolean;
|
|
19
|
+
text: string;
|
|
20
|
+
needs: number[];
|
|
21
|
+
}
|
|
22
|
+
export interface RunnableTask {
|
|
23
|
+
index: number;
|
|
24
|
+
text: string;
|
|
25
|
+
}
|
|
26
|
+
export declare const parseTasksWithDeps: (markdown: string) => ParsedTaskLine[];
|
|
27
|
+
/**
|
|
28
|
+
* Tasks that can be worked right now: not yet done, and every `[needs:]`
|
|
29
|
+
* dependency is a task that exists AND is checked off. A dependency on a
|
|
30
|
+
* non-existent `#n`, or on a task that isn't done, withholds the task.
|
|
31
|
+
*/
|
|
32
|
+
export declare const computeRunnableTasks: (markdown: string) => RunnableTask[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const TASK_LINE = /^\s*[-*]\s+\[([ xX])\]\s+(.*)$/;
|
|
2
|
+
const NEEDS = /\[needs:\s*([#\d,\s]+)\]\s*$/i;
|
|
3
|
+
export const parseTasksWithDeps = (markdown) => {
|
|
4
|
+
const tasks = [];
|
|
5
|
+
let index = 0;
|
|
6
|
+
for (const line of markdown.split('\n')) {
|
|
7
|
+
const match = TASK_LINE.exec(line);
|
|
8
|
+
if (!match)
|
|
9
|
+
continue;
|
|
10
|
+
index += 1;
|
|
11
|
+
const done = (match[1] ?? ' ').toLowerCase() === 'x';
|
|
12
|
+
let text = (match[2] ?? '').trim();
|
|
13
|
+
const needs = [];
|
|
14
|
+
const needsMatch = NEEDS.exec(text);
|
|
15
|
+
if (needsMatch) {
|
|
16
|
+
for (const token of (needsMatch[1] ?? '').split(',')) {
|
|
17
|
+
const parsed = Number.parseInt(token.replace('#', '').trim(), 10);
|
|
18
|
+
if (Number.isInteger(parsed) && parsed > 0)
|
|
19
|
+
needs.push(parsed);
|
|
20
|
+
}
|
|
21
|
+
// Drop the trailing annotation from the human-readable text.
|
|
22
|
+
text = text.slice(0, needsMatch.index).trim();
|
|
23
|
+
}
|
|
24
|
+
tasks.push({ index, done, text, needs });
|
|
25
|
+
}
|
|
26
|
+
return tasks;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Tasks that can be worked right now: not yet done, and every `[needs:]`
|
|
30
|
+
* dependency is a task that exists AND is checked off. A dependency on a
|
|
31
|
+
* non-existent `#n`, or on a task that isn't done, withholds the task.
|
|
32
|
+
*/
|
|
33
|
+
export const computeRunnableTasks = (markdown) => {
|
|
34
|
+
const tasks = parseTasksWithDeps(markdown);
|
|
35
|
+
const doneByIndex = new Map(tasks.map((task) => [task.index, task.done]));
|
|
36
|
+
return tasks
|
|
37
|
+
.filter((task) => !task.done)
|
|
38
|
+
.filter((task) => task.needs.every((dep) => doneByIndex.get(dep) === true))
|
|
39
|
+
.map((task) => ({ index: task.index, text: task.text }));
|
|
40
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ChokidarOptions } from 'chokidar';
|
|
2
|
+
import { type FeatureFlags } from './feature-flags.js';
|
|
2
3
|
import type { WorkflowCliPolicy } from './workflow-cli-policy.js';
|
|
3
4
|
/**
|
|
4
5
|
* Watcher configuration. The atomic-save option matters on Windows: VS
|
|
@@ -31,16 +32,14 @@ export interface TasksFileWatcher {
|
|
|
31
32
|
start: (workspaceId: string, workspacePath: string) => Promise<void>;
|
|
32
33
|
stop: (workspaceId: string) => Promise<void>;
|
|
33
34
|
}
|
|
34
|
-
export declare const createTasksFileWatcher: ({ onTasksUpdated, getWorkflowCliPolicy,
|
|
35
|
+
export declare const createTasksFileWatcher: ({ onTasksUpdated, getWorkflowCliPolicy, getFlags, }: {
|
|
35
36
|
onTasksUpdated: (workspaceId: string, content: string) => void;
|
|
36
37
|
/** Lets the freshly-written `.hive/PROTOCOL.md` state the workspace's
|
|
37
38
|
* workflow CLI default + allowlist. Optional: omitted → the doc renders
|
|
38
39
|
* the unrestricted default. */
|
|
39
40
|
getWorkflowCliPolicy?: () => WorkflowCliPolicy;
|
|
40
|
-
/**
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* PROTOCOL.md omits the team-sizing rule. */
|
|
45
|
-
getAutostaffEnabled?: () => boolean;
|
|
41
|
+
/** Resolves the live experimental flags. PROTOCOL.md omits the workflow DSL
|
|
42
|
+
* + `team workflow` commands when `workflowsEnabled` is off, and the
|
|
43
|
+
* team-sizing rule when `autostaffEnabled` is off. Omitted → all off. */
|
|
44
|
+
getFlags?: () => FeatureFlags;
|
|
46
45
|
}) => TasksFileWatcher;
|
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { basename, dirname, normalize } from 'node:path';
|
|
4
4
|
import chokidar from 'chokidar';
|
|
5
|
+
import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
|
|
5
6
|
import { ensureProtocolFile, ensureTasksFile, getTasksFilePath, TASKS_FILE_NAME, } from './tasks-file.js';
|
|
6
7
|
const DEBOUNCE_MS = 100;
|
|
7
8
|
const WATCHER_RETRY_MS = 5000;
|
|
@@ -66,7 +67,7 @@ const isTasksFileEvent = (tasksPath, changedPath) => {
|
|
|
66
67
|
const text = Buffer.isBuffer(changedPath) ? changedPath.toString() : changedPath;
|
|
67
68
|
return normalize(text) === normalize(tasksPath) || basename(text) === TASKS_FILE_NAME;
|
|
68
69
|
};
|
|
69
|
-
export const createTasksFileWatcher = ({ onTasksUpdated, getWorkflowCliPolicy,
|
|
70
|
+
export const createTasksFileWatcher = ({ onTasksUpdated, getWorkflowCliPolicy, getFlags, }) => {
|
|
70
71
|
const watchers = new Map();
|
|
71
72
|
const timers = new Map();
|
|
72
73
|
const retryTimers = new Map();
|
|
@@ -145,7 +146,7 @@ export const createTasksFileWatcher = ({ onTasksUpdated, getWorkflowCliPolicy, g
|
|
|
145
146
|
closed = false;
|
|
146
147
|
await stop(workspaceId);
|
|
147
148
|
ensureTasksFile(workspacePath);
|
|
148
|
-
ensureProtocolFile(workspacePath, getWorkflowCliPolicy?.(),
|
|
149
|
+
ensureProtocolFile(workspacePath, getWorkflowCliPolicy?.(), getFlags?.() ?? FEATURE_FLAGS_ALL_OFF);
|
|
149
150
|
const tasksPath = getTasksFilePath(workspacePath);
|
|
150
151
|
const watcher = chokidar.watch(dirname(tasksPath), buildTasksWatcherOptions(workspacePath));
|
|
151
152
|
const scheduleEmit = (changedPath) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FeatureFlags } from './feature-flags.js';
|
|
1
2
|
import type { WorkflowCliPolicy } from './workflow-cli-policy.js';
|
|
2
3
|
interface TasksFileService {
|
|
3
4
|
readTasks: (workspacePath: string) => string;
|
|
@@ -17,6 +18,6 @@ export declare const ensureTasksFile: (workspacePath: string) => string;
|
|
|
17
18
|
* on every workspace open means a Hive version bump that changes the rules
|
|
18
19
|
* propagates without manual intervention.
|
|
19
20
|
*/
|
|
20
|
-
export declare const ensureProtocolFile: (workspacePath: string, cliPolicy?: WorkflowCliPolicy,
|
|
21
|
+
export declare const ensureProtocolFile: (workspacePath: string, cliPolicy?: WorkflowCliPolicy, flags?: FeatureFlags) => string;
|
|
21
22
|
export declare const createTasksFileService: () => TasksFileService;
|
|
22
23
|
export type { TasksFileService };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
+
import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
|
|
3
4
|
import { buildProtocolDoc } from './hive-team-guidance.js';
|
|
4
5
|
export const HIVE_DIR_NAME = '.hive';
|
|
5
6
|
export const TASKS_FILE_NAME = 'tasks.md';
|
|
@@ -50,10 +51,10 @@ export const ensureTasksFile = (workspacePath) => {
|
|
|
50
51
|
* on every workspace open means a Hive version bump that changes the rules
|
|
51
52
|
* propagates without manual intervention.
|
|
52
53
|
*/
|
|
53
|
-
export const ensureProtocolFile = (workspacePath, cliPolicy,
|
|
54
|
+
export const ensureProtocolFile = (workspacePath, cliPolicy, flags = FEATURE_FLAGS_ALL_OFF) => {
|
|
54
55
|
ensureTasksDir(workspacePath);
|
|
55
56
|
const protocolFilePath = getProtocolFilePath(workspacePath);
|
|
56
|
-
const desired = buildProtocolDoc(cliPolicy,
|
|
57
|
+
const desired = buildProtocolDoc(cliPolicy, flags);
|
|
57
58
|
const current = existsSync(protocolFilePath)
|
|
58
59
|
? runRetryableTasksFileOperation(() => readFileSync(protocolFilePath, 'utf8'))
|
|
59
60
|
: null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentSummary } from '../shared/types.js';
|
|
2
|
-
export type TeamCommand = 'send' | 'list' | 'report' | 'status' | 'cancel' | 'help' | 'spawn' | 'dismiss' | 'workflow';
|
|
2
|
+
export type TeamCommand = 'send' | 'list' | 'next' | 'report' | 'status' | 'cancel' | 'help' | 'spawn' | 'dismiss' | 'workflow';
|
|
3
3
|
export declare const commandAllowedForRole: (role: AgentSummary["role"], command: TeamCommand) => boolean;
|
|
4
4
|
interface AuthenticateInput {
|
|
5
5
|
fromAgentId: string | undefined;
|
|
@@ -2,6 +2,8 @@ import type { TeamListItem } from '../shared/types.js';
|
|
|
2
2
|
import type { AgentRuntime } from './agent-runtime.js';
|
|
3
3
|
import type { DispatchRecord } from './dispatch-ledger-store.js';
|
|
4
4
|
import type { MessageLogHandle, MessageLogRecord } from './message-log-store.js';
|
|
5
|
+
import type { ReportOutboxStore } from './report-outbox-store.js';
|
|
6
|
+
import type { WebhookEvent } from './webhook-notifier.js';
|
|
5
7
|
import type { WorkflowDispatchAwaiter } from './workflow-dispatch-awaiter.js';
|
|
6
8
|
import type { WorkspaceStore } from './workspace-store.js';
|
|
7
9
|
export declare const formatUnknownWorkerError: (workerName: string, roster: readonly TeamListItem[]) => string;
|
|
@@ -35,6 +37,9 @@ export interface TeamOperationsInput {
|
|
|
35
37
|
workspaceId: string;
|
|
36
38
|
}) => DispatchRecord | undefined;
|
|
37
39
|
markDispatchSubmitted: (dispatchId: string) => void;
|
|
40
|
+
reportOutbox: ReportOutboxStore;
|
|
41
|
+
/** Fire an outbound completion webhook (best-effort) when a worker reports. */
|
|
42
|
+
notifyWebhook?: (event: WebhookEvent) => void;
|
|
38
43
|
workflowDispatchAwaiter: WorkflowDispatchAwaiter;
|
|
39
44
|
workspaceStore: WorkspaceStore;
|
|
40
45
|
/** Auto-dismiss an ephemeral orchestrator-spawned worker after its
|
|
@@ -71,13 +76,14 @@ export interface ReportTaskResult {
|
|
|
71
76
|
forwardError: string | null;
|
|
72
77
|
forwarded: boolean;
|
|
73
78
|
}
|
|
74
|
-
export declare const createTeamOperations: ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, }: TeamOperationsInput) => {
|
|
79
|
+
export declare const createTeamOperations: ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, reportOutbox, notifyWebhook, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, }: TeamOperationsInput) => {
|
|
75
80
|
cancelTask(workspaceId: string, dispatchId: string, input: CancelTaskInput): {
|
|
76
81
|
dispatch: DispatchRecord;
|
|
77
82
|
forwardError: string | null;
|
|
78
83
|
forwarded: boolean;
|
|
79
84
|
};
|
|
80
85
|
dispatchTask: (workspaceId: string, workerId: string, text: string, input?: DispatchTaskInput) => Promise<DispatchRecord>;
|
|
86
|
+
drainReportOutbox: (workspaceId: string) => void;
|
|
81
87
|
dispatchTaskByWorkerName(workspaceId: string, workerName: string, text: string, input?: DispatchTaskInput): Promise<DispatchRecord & {
|
|
82
88
|
restartedWorker: boolean;
|
|
83
89
|
}>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildOrchestratorReportPayload } from './agent-stdin-dispatcher.js';
|
|
2
|
+
import { ConflictError } from './http-errors.js';
|
|
2
3
|
import { createReportMessage, createSendMessage, createStatusMessage, createUserInputMessage, } from './runtime-message-builders.js';
|
|
3
4
|
import { getWorkflowAgentId } from './workspace-store-support.js';
|
|
4
5
|
/* Roster snapshot embedded in the 409 the orchestrator sees when it
|
|
@@ -24,7 +25,27 @@ export const formatUnknownWorkerError = (workerName, roster) => {
|
|
|
24
25
|
].join('\n');
|
|
25
26
|
};
|
|
26
27
|
const reportForwardErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
27
|
-
export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, }) => {
|
|
28
|
+
export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, reportOutbox, notifyWebhook, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, }) => {
|
|
29
|
+
// Best-effort redelivery of reports a prior orchestrator outage stranded.
|
|
30
|
+
// Called when a fresh report confirms the orchestrator is reachable and
|
|
31
|
+
// when the orchestrator polls `team list` (its natural post-restart wakeup).
|
|
32
|
+
// An entry is marked delivered only after its PTY write actually resolves,
|
|
33
|
+
// so a still-down orchestrator just leaves the backlog pending.
|
|
34
|
+
const drainReportOutbox = (workspaceId) => {
|
|
35
|
+
const orchestratorId = `${workspaceId}:orchestrator`;
|
|
36
|
+
if (!agentRuntime.getActiveRunByAgentId(workspaceId, orchestratorId))
|
|
37
|
+
return;
|
|
38
|
+
for (const entry of reportOutbox.listPending(workspaceId, orchestratorId)) {
|
|
39
|
+
void agentRuntime
|
|
40
|
+
.deliverSystemMessageToAgent(workspaceId, orchestratorId, entry.payload, {
|
|
41
|
+
requireActiveRun: true,
|
|
42
|
+
})
|
|
43
|
+
.then(() => reportOutbox.markDelivered(entry.id))
|
|
44
|
+
.catch((error) => {
|
|
45
|
+
console.error('[hive] swallowed:teamReport.outboxDrain', error);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
28
49
|
const ensureWorkerRun = async (workspaceId, workerId, hivePort) => {
|
|
29
50
|
if (agentRuntime.getActiveRunByAgentId(workspaceId, workerId)) {
|
|
30
51
|
return;
|
|
@@ -171,6 +192,7 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
171
192
|
return { dispatch, forwardError, forwarded };
|
|
172
193
|
},
|
|
173
194
|
dispatchTask,
|
|
195
|
+
drainReportOutbox,
|
|
174
196
|
async dispatchTaskByWorkerName(workspaceId, workerName, text, input = {}) {
|
|
175
197
|
/* Build the roster once so a missing-name path can surface it without
|
|
176
198
|
a second store call. We deliberately don't go through
|
|
@@ -235,15 +257,6 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
235
257
|
if (!openDispatch) {
|
|
236
258
|
throw new ConflictError(`No open dispatch for worker: ${worker.name}`);
|
|
237
259
|
}
|
|
238
|
-
const isWorkflowDispatch = openDispatch.fromAgentId === getWorkflowAgentId(workspaceId);
|
|
239
|
-
// Pre-check the orchestrator PTY only when the report is heading there.
|
|
240
|
-
// Workflow-sourced dispatches don't need an orchestrator: the runner
|
|
241
|
-
// is in-process and will resolve its awaiter directly.
|
|
242
|
-
if (input.requireActiveRun === true && !isWorkflowDispatch) {
|
|
243
|
-
if (!agentRuntime.getActiveRunByAgentId(workspaceId, `${workspaceId}:orchestrator`)) {
|
|
244
|
-
throw new PtyInactiveError(`No active run for agent: ${workspaceId}:orchestrator`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
260
|
const messageHandle = insertMessage(createReportMessage(workspaceId, workerId, text, status, artifacts));
|
|
248
261
|
try {
|
|
249
262
|
const dispatch = markDispatchReportedByWorker({
|
|
@@ -277,16 +290,65 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
277
290
|
}
|
|
278
291
|
return { dispatch, forwardError, forwarded };
|
|
279
292
|
}
|
|
293
|
+
// Real worker reported (not a workflow-internal step) — fire the
|
|
294
|
+
// outbound completion webhook. Best-effort; never blocks the report.
|
|
295
|
+
notifyWebhook?.({
|
|
296
|
+
type: 'report_received',
|
|
297
|
+
workspaceId,
|
|
298
|
+
agentId: workerId,
|
|
299
|
+
agentName: worker.name,
|
|
300
|
+
summary: text.slice(0, 280),
|
|
301
|
+
at: Date.now(),
|
|
302
|
+
});
|
|
280
303
|
if (input.requireActiveRun === true) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
304
|
+
const orchestratorId = `${workspaceId}:orchestrator`;
|
|
305
|
+
// A fresh report proves the orchestrator is reachable — flush any
|
|
306
|
+
// backlog a prior outage stranded first, in arrival order, before
|
|
307
|
+
// this one (both ride the dispatcher's per-agent serial queue).
|
|
308
|
+
drainReportOutbox(workspaceId);
|
|
309
|
+
const payload = buildOrchestratorReportPayload(worker.name, text, artifacts);
|
|
310
|
+
if (agentRuntime.getActiveRunByAgentId(workspaceId, orchestratorId)) {
|
|
311
|
+
try {
|
|
312
|
+
const delivery = agentRuntime.deliverReportToOrchestrator(workspaceId, worker.name, text, artifacts, { requireActiveRun: true });
|
|
313
|
+
forwarded = true;
|
|
314
|
+
// The dispatch is already marked reported. If the PTY dies
|
|
315
|
+
// mid-write the ledger would say "reported" while the
|
|
316
|
+
// orchestrator never received it — persist for redelivery so it
|
|
317
|
+
// isn't silently lost.
|
|
318
|
+
void delivery.catch((error) => {
|
|
319
|
+
console.error('[hive] swallowed:teamReport.forward', error);
|
|
320
|
+
reportOutbox.enqueue({
|
|
321
|
+
workspaceId,
|
|
322
|
+
targetAgentId: orchestratorId,
|
|
323
|
+
dispatchId: dispatch.id,
|
|
324
|
+
payload,
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
// TOCTOU: orchestrator vanished between the active-run check and
|
|
330
|
+
// the write. Same fix — queue it.
|
|
331
|
+
console.error('[hive] swallowed:teamReport.forward', error);
|
|
332
|
+
forwardError = reportForwardErrorMessage(error);
|
|
333
|
+
reportOutbox.enqueue({
|
|
334
|
+
workspaceId,
|
|
335
|
+
targetAgentId: orchestratorId,
|
|
336
|
+
dispatchId: dispatch.id,
|
|
337
|
+
payload,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
286
340
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
341
|
+
else {
|
|
342
|
+
// Orchestrator is down. Queue the report instead of dropping it;
|
|
343
|
+
// the CLI surfaces forwarded:false and the backlog drains on the
|
|
344
|
+
// orchestrator's next `team list` after it restarts.
|
|
345
|
+
forwardError = 'Orchestrator is not running; report queued for delivery.';
|
|
346
|
+
reportOutbox.enqueue({
|
|
347
|
+
workspaceId,
|
|
348
|
+
targetAgentId: orchestratorId,
|
|
349
|
+
dispatchId: dispatch.id,
|
|
350
|
+
payload,
|
|
351
|
+
});
|
|
290
352
|
}
|
|
291
353
|
}
|
|
292
354
|
// M11: if this worker was spawned with `team spawn --ephemeral`, this
|