@tt-a1i/hive 2.1.6 → 2.1.8
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 +27 -0
- package/dist/src/server/agent-runtime-contract.d.ts +1 -9
- package/dist/src/server/agent-runtime.js +1 -7
- package/dist/src/server/agent-stdin-dispatcher.d.ts +2 -11
- package/dist/src/server/agent-stdin-dispatcher.js +14 -12
- package/dist/src/server/hive-team-guidance.js +12 -11
- package/dist/src/server/post-start-input-writer.js +1 -1
- package/dist/src/server/report-outbox-store.d.ts +10 -8
- package/dist/src/server/report-outbox-store.js +10 -1
- package/dist/src/server/role-templates.js +8 -4
- package/dist/src/server/routes-remote.js +46 -14
- package/dist/src/server/routes-team.js +1 -1
- package/dist/src/server/routes-workspaces.js +3 -6
- package/dist/src/server/runtime-store-contract.d.ts +2 -2
- package/dist/src/server/runtime-store-helpers.d.ts +4 -0
- package/dist/src/server/runtime-store-helpers.js +34 -11
- package/dist/src/server/runtime-store-worker-mutations.js +12 -6
- package/dist/src/server/runtime-store-workflows.js +7 -1
- package/dist/src/server/team-operations.d.ts +12 -5
- package/dist/src/server/team-operations.js +218 -159
- package/dist/src/shared/remote-bridge-routing.js +3 -1
- package/package.json +1 -1
- package/web/dist/assets/{AddWorkerDialog-DFNkbR_Q.js → AddWorkerDialog-Cd53ifv2.js} +2 -2
- package/web/dist/assets/{AddWorkspaceFlow-BjpjCuRe.js → AddWorkspaceFlow-3sR25gDy.js} +1 -1
- package/web/dist/assets/{FirstRunWizard-BuLgUAny.js → FirstRunWizard-DU8PipxA.js} +1 -1
- package/web/dist/assets/{MarketplaceDrawer-BALuhrnd.js → MarketplaceDrawer-BkNcknv0.js} +1 -1
- package/web/dist/assets/{TaskGraphDrawer-DQTS6JPI.js → TaskGraphDrawer-9mXNimSE.js} +1 -1
- package/web/dist/assets/{WhatsNewDialog-DpgjEIaA.js → WhatsNewDialog-C7awVYub.js} +1 -1
- package/web/dist/assets/WorkerModal-BzgrOKzF.js +1 -0
- package/web/dist/assets/{WorkflowsDrawer-DPWlZs4S.js → WorkflowsDrawer-C4yjTdY7.js} +1 -1
- package/web/dist/assets/{WorkspaceMemoryDrawer-C0vny4zK.js → WorkspaceMemoryDrawer-BlDznlGm.js} +1 -1
- package/web/dist/assets/{WorkspaceTaskDrawer-CJcFZh8C.js → WorkspaceTaskDrawer-CpGGZkp3.js} +1 -1
- package/web/dist/assets/index-BEyvsWk6.js +84 -0
- package/web/dist/assets/index-VHK8pAJq.css +1 -0
- package/web/dist/assets/{search-Bl5O-SfR.js → search-Cv0KnMjw.js} +1 -1
- package/web/dist/assets/{square-terminal-DCDaflg3.js → square-terminal-BZ7EavxU.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/WorkerModal-1y-Xdwv-.js +0 -1
- package/web/dist/assets/index-CEP_bvOg.css +0 -1
- package/web/dist/assets/index-CR1SPGBZ.js +0 -84
|
@@ -17,7 +17,7 @@ export const formatUnknownWorkerError = (workerName, roster) => {
|
|
|
17
17
|
if (roster.length === 0) {
|
|
18
18
|
return [
|
|
19
19
|
`Unknown worker "${workerName}": this workspace currently has no workers.`,
|
|
20
|
-
'
|
|
20
|
+
'Report the empty roster to the user and recommend the worker role to add. If the user or workspace policy has explicitly authorized autonomous staffing, run `team spawn <role> [--name <name>] [--ephemeral]`, then `team list` and retry with the spawned worker name.',
|
|
21
21
|
].join('\n');
|
|
22
22
|
}
|
|
23
23
|
const lines = roster.map((entry) => ` - ${entry.name} (${entry.role}, ${entry.status}, ${entry.pendingTaskCount} pending)`);
|
|
@@ -50,27 +50,55 @@ const buildPendingWarning = (workerName, pendingTaskCount, action) => pendingTas
|
|
|
50
50
|
? `Hive recorded the ${action}, but ${workerName} still has ${pendingTaskCount} open dispatch${pendingTaskCount === 1 ? '' : 'es'}. ` +
|
|
51
51
|
'A worker stays working until every dispatch is closed with `team report --dispatch <id>` or `team cancel --dispatch <id>`.'
|
|
52
52
|
: undefined;
|
|
53
|
-
export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, listOpenWorkspaceDispatches, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, claimQueuedDispatch, reparkClaimedDispatch, reportOutbox, notifyWebhook, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, recordProtocolEvent, getFlags, }) => {
|
|
53
|
+
export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, listOpenWorkspaceDispatches, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, claimQueuedDispatch, reparkClaimedDispatch, reportOutbox, notifyWebhook, runDataMutation, isRuntimeClosing, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, recordProtocolEvent, getFlags, }) => {
|
|
54
54
|
const flags = () => getFlags?.() ?? FEATURE_FLAGS_ALL_OFF;
|
|
55
|
+
const runMutation = runDataMutation ?? ((mutation) => mutation());
|
|
56
|
+
const runtimeClosing = () => isRuntimeClosing?.() === true;
|
|
57
|
+
const drainingReportOutboxIds = new Set();
|
|
55
58
|
// Best-effort redelivery of reports a prior orchestrator outage stranded.
|
|
56
59
|
// Called when a fresh report confirms the orchestrator is reachable and
|
|
57
60
|
// when the orchestrator polls `team list` (its natural post-restart wakeup).
|
|
58
61
|
// An entry is marked delivered only after its PTY write actually resolves,
|
|
59
62
|
// so a still-down orchestrator just leaves the backlog pending.
|
|
60
|
-
const drainReportOutbox = (workspaceId) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
const drainReportOutbox = (workspaceId, targetAgentId = `${workspaceId}:orchestrator`) => {
|
|
64
|
+
if (runtimeClosing())
|
|
65
|
+
return { attempted: 0, firstSyncError: null };
|
|
66
|
+
if (!agentRuntime.getActiveRunByAgentId(workspaceId, targetAgentId)) {
|
|
67
|
+
return { attempted: 0, firstSyncError: null };
|
|
68
|
+
}
|
|
69
|
+
let attempted = 0;
|
|
70
|
+
let firstSyncError = null;
|
|
71
|
+
for (const entry of reportOutbox.listPending(workspaceId, targetAgentId)) {
|
|
72
|
+
if (drainingReportOutboxIds.has(entry.id))
|
|
73
|
+
continue;
|
|
74
|
+
drainingReportOutboxIds.add(entry.id);
|
|
75
|
+
attempted += 1;
|
|
76
|
+
try {
|
|
77
|
+
void agentRuntime
|
|
78
|
+
.deliverSystemMessageToAgent(workspaceId, targetAgentId, entry.payload, {
|
|
79
|
+
requireActiveRun: true,
|
|
80
|
+
})
|
|
81
|
+
.then(() => {
|
|
82
|
+
if (!runtimeClosing())
|
|
83
|
+
reportOutbox.markDelivered(entry.id);
|
|
84
|
+
})
|
|
85
|
+
.catch((error) => {
|
|
86
|
+
if (runtimeClosing())
|
|
87
|
+
return;
|
|
88
|
+
console.error('[hive] swallowed:teamReport.outboxDrain', error);
|
|
89
|
+
})
|
|
90
|
+
.finally(() => {
|
|
91
|
+
drainingReportOutboxIds.delete(entry.id);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
drainingReportOutboxIds.delete(entry.id);
|
|
96
|
+
firstSyncError ??= reportForwardErrorMessage(error);
|
|
97
|
+
if (!runtimeClosing())
|
|
98
|
+
console.error('[hive] swallowed:teamReport.outboxDrain', error);
|
|
99
|
+
}
|
|
73
100
|
}
|
|
101
|
+
return { attempted, firstSyncError };
|
|
74
102
|
};
|
|
75
103
|
const ensureWorkerRun = async (workspaceId, workerId, hivePort) => {
|
|
76
104
|
const activeRun = agentRuntime.getActiveRunByAgentId(workspaceId, workerId);
|
|
@@ -125,6 +153,7 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
125
153
|
return workerId;
|
|
126
154
|
}
|
|
127
155
|
};
|
|
156
|
+
const buildIssuerSystemReminderPayload = (text) => `<hive-system-reminder>\n${escapeHiveEnvelopeText(text)}\n</hive-system-reminder>\n`;
|
|
128
157
|
/** Issuer notification that survives the issuer being down: try the live
|
|
129
158
|
* PTY first; if the issuer has no active run (or the write rejects), park
|
|
130
159
|
* the notice in the report outbox — it drains on the issuer's next
|
|
@@ -132,7 +161,7 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
132
161
|
* (A fire-and-forget writeSystemMessageToAgent silently no-ops without an
|
|
133
162
|
* active run, which is exactly when failures cluster — post-restart.) */
|
|
134
163
|
const notifyIssuerDurably = (workspaceId, issuerAgentId, dispatchId, text) => {
|
|
135
|
-
const payload =
|
|
164
|
+
const payload = buildIssuerSystemReminderPayload(text);
|
|
136
165
|
const park = () => {
|
|
137
166
|
try {
|
|
138
167
|
reportOutbox.enqueue({
|
|
@@ -157,28 +186,30 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
157
186
|
park();
|
|
158
187
|
}
|
|
159
188
|
};
|
|
160
|
-
/** Called
|
|
161
|
-
*
|
|
162
|
-
*
|
|
189
|
+
/** Called inside the worker-delete transaction after stale pending outbox rows
|
|
190
|
+
* for the doomed dispatches are removed, but before dispatch rows are
|
|
191
|
+
* deleted. Open non-workflow dispatches get a durable issuer notice, avoiding
|
|
192
|
+
* a third silent outcome next to delivered/cancelled. */
|
|
163
193
|
const notifyIssuersOfDroppedDispatches = (workspaceId, workerId, reason) => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
item.fromAgentId !== null &&
|
|
169
|
-
item.fromAgentId !== getWorkflowAgentId(workspaceId));
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
console.error('[hive] swallowed:teamDismiss.listOpen', error);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
194
|
+
const open = listOpenWorkspaceDispatches(workspaceId).filter((item) => item.toAgentId === workerId &&
|
|
195
|
+
item.workflowRunId === null &&
|
|
196
|
+
item.fromAgentId !== null &&
|
|
197
|
+
item.fromAgentId !== getWorkflowAgentId(workspaceId));
|
|
175
198
|
const workerName = workerNameOrId(workspaceId, workerId);
|
|
199
|
+
const targetAgentIds = new Set();
|
|
176
200
|
for (const item of open) {
|
|
177
201
|
if (!item.fromAgentId)
|
|
178
202
|
continue;
|
|
179
|
-
|
|
180
|
-
|
|
203
|
+
targetAgentIds.add(item.fromAgentId);
|
|
204
|
+
reportOutbox.enqueue({
|
|
205
|
+
workspaceId,
|
|
206
|
+
targetAgentId: item.fromAgentId,
|
|
207
|
+
dispatchId: item.id,
|
|
208
|
+
payload: buildIssuerSystemReminderPayload(`Dispatch ${item.id} to @${workerName} was DROPPED: ${reason}. ` +
|
|
209
|
+
'It will not run and cannot be reported — re-dispatch the task to another worker if it still matters.'),
|
|
210
|
+
});
|
|
181
211
|
}
|
|
212
|
+
return [...targetAgentIds];
|
|
182
213
|
};
|
|
183
214
|
/** #33: deliver dispatches parked while their worker was stopped. Called on
|
|
184
215
|
* worker run start (lifecycle) and after dispatchTask auto-starts a worker.
|
|
@@ -213,6 +244,8 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
213
244
|
must not destroy parked work. If the row is no longer 'submitted'
|
|
214
245
|
(reported/cancelled meanwhile), the repark loses and we leave it be. */
|
|
215
246
|
const failDispatch = (item, error) => {
|
|
247
|
+
if (runtimeClosing())
|
|
248
|
+
return;
|
|
216
249
|
if (isPromptReadinessTimeoutError(error)) {
|
|
217
250
|
try {
|
|
218
251
|
cancelUndeliveredDispatch(workspaceId, workerId, item.id, reportForwardErrorMessage(error), item.workflowRunId ?? undefined, item.fromAgentId ?? undefined);
|
|
@@ -303,6 +336,8 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
303
336
|
try {
|
|
304
337
|
const writePrompt = agentRuntime.writeSendPrompt(workspaceId, workerId, dispatchId, sender.name, currentWorker.description, text);
|
|
305
338
|
void writePrompt.catch((error) => {
|
|
339
|
+
if (runtimeClosing())
|
|
340
|
+
return;
|
|
306
341
|
// `team send` is intentionally asynchronous (§3.3). A worker that
|
|
307
342
|
// exits during paste-submit did not receive actionable work, so
|
|
308
343
|
// close the open dispatch instead of leaving a fake pending task.
|
|
@@ -344,6 +379,8 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
344
379
|
workspaceStore.markTaskDispatched(workspaceId, workerId);
|
|
345
380
|
pendingMarked = true;
|
|
346
381
|
const cancelDeferredDispatch = (error) => {
|
|
382
|
+
if (runtimeClosing())
|
|
383
|
+
return;
|
|
347
384
|
try {
|
|
348
385
|
cancelUndeliveredDispatch(workspaceId, workerId, dispatchId, reportForwardErrorMessage(error), input.workflowRunId, isWorkflowDispatch ? undefined : fromAgentId);
|
|
349
386
|
}
|
|
@@ -356,6 +393,8 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
356
393
|
};
|
|
357
394
|
void deferUntilPostStartReady
|
|
358
395
|
.then(() => {
|
|
396
|
+
if (runtimeClosing())
|
|
397
|
+
return;
|
|
359
398
|
const current = listOpenWorkspaceDispatches(workspaceId).find((item) => item.id === dispatchId);
|
|
360
399
|
if (!current || current.status !== 'queued')
|
|
361
400
|
return;
|
|
@@ -408,7 +447,7 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
408
447
|
}
|
|
409
448
|
};
|
|
410
449
|
return {
|
|
411
|
-
cancelTask(workspaceId, dispatchId, input) {
|
|
450
|
+
async cancelTask(workspaceId, dispatchId, input) {
|
|
412
451
|
workspaceStore.getAgent(workspaceId, input.fromAgentId);
|
|
413
452
|
const openDispatch = findOpenDispatchById(workspaceId, dispatchId);
|
|
414
453
|
if (!openDispatch) {
|
|
@@ -433,7 +472,9 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
433
472
|
let forwardError = null;
|
|
434
473
|
let forwarded = false;
|
|
435
474
|
try {
|
|
436
|
-
agentRuntime.writeCancelPrompt(workspaceId, dispatch.toAgentId, dispatch.id, input.reason
|
|
475
|
+
await agentRuntime.writeCancelPrompt(workspaceId, dispatch.toAgentId, dispatch.id, input.reason, {
|
|
476
|
+
requireActiveRun: true,
|
|
477
|
+
});
|
|
437
478
|
forwarded = true;
|
|
438
479
|
}
|
|
439
480
|
catch (error) {
|
|
@@ -474,10 +515,21 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
474
515
|
},
|
|
475
516
|
async deliverUserInput(workspaceId, orchestratorId, text) {
|
|
476
517
|
workspaceStore.getAgent(workspaceId, orchestratorId);
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
518
|
+
const messageHandle = insertMessage(createUserInputMessage(workspaceId, orchestratorId, text));
|
|
519
|
+
try {
|
|
520
|
+
await agentRuntime.deliverUserInputToOrchestrator(workspaceId, text, {
|
|
521
|
+
requireActiveRun: true,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
try {
|
|
526
|
+
deleteMessage(messageHandle);
|
|
527
|
+
}
|
|
528
|
+
catch (deleteError) {
|
|
529
|
+
console.error('[hive] swallowed:userInput.rollback', deleteError);
|
|
530
|
+
}
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
481
533
|
},
|
|
482
534
|
statusTask(workspaceId, workerId, input = {}) {
|
|
483
535
|
const text = input.text ?? '';
|
|
@@ -522,146 +574,153 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
522
574
|
if (!openDispatch) {
|
|
523
575
|
throw new ConflictError(formatNoOpenDispatchError(worker.name, input.dispatchId, listOpenWorkspaceDispatches(workspaceId).filter((item) => item.toAgentId === workerId)));
|
|
524
576
|
}
|
|
525
|
-
|
|
577
|
+
let messageHandle;
|
|
578
|
+
let dispatch;
|
|
579
|
+
let reportQueuedBeforeCommit = false;
|
|
580
|
+
const orchestratorId = `${workspaceId}:orchestrator`;
|
|
581
|
+
const payload = buildOrchestratorReportPayload(worker.name, text, artifacts, flags(), openDispatch.id);
|
|
582
|
+
const workflowDispatch = openDispatch.fromAgentId === getWorkflowAgentId(workspaceId);
|
|
583
|
+
const shouldQueueForOrchestrator = input.requireActiveRun === true && !workflowDispatch;
|
|
584
|
+
if (shouldQueueForOrchestrator &&
|
|
585
|
+
agentRuntime.getActiveRunByAgentId(workspaceId, orchestratorId)) {
|
|
586
|
+
drainReportOutbox(workspaceId);
|
|
587
|
+
}
|
|
526
588
|
try {
|
|
527
|
-
|
|
528
|
-
artifacts
|
|
529
|
-
|
|
530
|
-
reportText: text,
|
|
531
|
-
toAgentId: workerId,
|
|
532
|
-
workspaceId,
|
|
533
|
-
});
|
|
534
|
-
if (!dispatch) {
|
|
535
|
-
// Post-race recheck: the dispatch closed between the precheck and
|
|
536
|
-
// the ledger write — re-query so the 409 lists what is still open.
|
|
537
|
-
throw new ConflictError(formatNoOpenDispatchError(worker.name, input.dispatchId, listOpenWorkspaceDispatches(workspaceId).filter((item) => item.toAgentId === workerId)));
|
|
538
|
-
}
|
|
539
|
-
workspaceStore.markTaskReported(workspaceId, workerId);
|
|
540
|
-
recordProtocolEvent?.('report');
|
|
541
|
-
const remainingPendingTaskCount = workspaceStore.getWorker(workspaceId, workerId).pendingTaskCount;
|
|
542
|
-
let forwardError = null;
|
|
543
|
-
let forwarded = false;
|
|
544
|
-
const pendingWarning = buildPendingWarning(worker.name, remainingPendingTaskCount, 'report');
|
|
545
|
-
// Workflow-sourced dispatches: the source is the in-process runner, not a
|
|
546
|
-
// PTY. Resolve its awaiting Promise instead of injecting into orchestrator
|
|
547
|
-
// stdin (which would do nothing — `__workflow__` has no PTY).
|
|
548
|
-
if (dispatch.fromAgentId === getWorkflowAgentId(workspaceId)) {
|
|
549
|
-
try {
|
|
550
|
-
workflowDispatchAwaiter.notifyReport(dispatch.id, {
|
|
551
|
-
artifacts,
|
|
552
|
-
text,
|
|
553
|
-
...(status ? { status } : {}),
|
|
554
|
-
});
|
|
555
|
-
forwarded = true;
|
|
556
|
-
}
|
|
557
|
-
catch (error) {
|
|
558
|
-
forwardError = reportForwardErrorMessage(error);
|
|
559
|
-
console.error('[hive] swallowed:teamReport.workflowForward', error);
|
|
560
|
-
}
|
|
561
|
-
return {
|
|
562
|
-
dispatch,
|
|
563
|
-
forwardError,
|
|
564
|
-
forwarded,
|
|
565
|
-
...(pendingWarning ? { pendingWarning } : {}),
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
// Real worker reported (not a workflow-internal step) — fire the
|
|
569
|
-
// outbound completion webhook. Best-effort; never blocks the report.
|
|
570
|
-
notifyWebhook?.({
|
|
571
|
-
type: 'report_received',
|
|
572
|
-
workspaceId,
|
|
573
|
-
agentId: workerId,
|
|
574
|
-
agentName: worker.name,
|
|
575
|
-
summary: text.slice(0, 280),
|
|
576
|
-
at: Date.now(),
|
|
577
|
-
});
|
|
578
|
-
if (input.requireActiveRun === true) {
|
|
579
|
-
const orchestratorId = `${workspaceId}:orchestrator`;
|
|
580
|
-
// A fresh report proves the orchestrator is reachable — flush any
|
|
581
|
-
// backlog a prior outage stranded first, in arrival order, before
|
|
582
|
-
// this one (both ride the dispatcher's per-agent serial queue).
|
|
583
|
-
drainReportOutbox(workspaceId);
|
|
584
|
-
const payload = buildOrchestratorReportPayload(worker.name, text, artifacts, flags());
|
|
585
|
-
if (agentRuntime.getActiveRunByAgentId(workspaceId, orchestratorId)) {
|
|
586
|
-
try {
|
|
587
|
-
const delivery = agentRuntime.deliverReportToOrchestrator(workspaceId, worker.name, text, artifacts, { requireActiveRun: true });
|
|
588
|
-
forwarded = true;
|
|
589
|
-
// The dispatch is already marked reported. If the PTY dies
|
|
590
|
-
// mid-write the ledger would say "reported" while the
|
|
591
|
-
// orchestrator never received it — persist for redelivery so it
|
|
592
|
-
// isn't silently lost.
|
|
593
|
-
void delivery.catch((error) => {
|
|
594
|
-
console.error('[hive] swallowed:teamReport.forward', error);
|
|
595
|
-
reportOutbox.enqueue({
|
|
596
|
-
workspaceId,
|
|
597
|
-
targetAgentId: orchestratorId,
|
|
598
|
-
dispatchId: dispatch.id,
|
|
599
|
-
payload,
|
|
600
|
-
});
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
catch (error) {
|
|
604
|
-
// TOCTOU: orchestrator vanished between the active-run check and
|
|
605
|
-
// the write. Same fix — queue it.
|
|
606
|
-
console.error('[hive] swallowed:teamReport.forward', error);
|
|
607
|
-
forwardError = reportForwardErrorMessage(error);
|
|
608
|
-
reportOutbox.enqueue({
|
|
609
|
-
workspaceId,
|
|
610
|
-
targetAgentId: orchestratorId,
|
|
611
|
-
dispatchId: dispatch.id,
|
|
612
|
-
payload,
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
else {
|
|
617
|
-
// Orchestrator is down. Queue the report instead of dropping it;
|
|
618
|
-
// the CLI surfaces forwarded:false and the backlog drains on the
|
|
619
|
-
// orchestrator's next `team list` after it restarts.
|
|
620
|
-
forwardError = 'Orchestrator is not running; report queued for delivery.';
|
|
589
|
+
runMutation(() => {
|
|
590
|
+
messageHandle = insertMessage(createReportMessage(workspaceId, workerId, text, status, artifacts));
|
|
591
|
+
if (shouldQueueForOrchestrator) {
|
|
621
592
|
reportOutbox.enqueue({
|
|
622
593
|
workspaceId,
|
|
623
594
|
targetAgentId: orchestratorId,
|
|
624
|
-
dispatchId:
|
|
595
|
+
dispatchId: openDispatch.id,
|
|
625
596
|
payload,
|
|
626
597
|
});
|
|
598
|
+
reportQueuedBeforeCommit = true;
|
|
627
599
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
600
|
+
const nextDispatch = markDispatchReportedByWorker({
|
|
601
|
+
artifacts,
|
|
602
|
+
...(input.dispatchId ? { dispatchId: input.dispatchId } : {}),
|
|
603
|
+
reportText: text,
|
|
604
|
+
toAgentId: workerId,
|
|
605
|
+
workspaceId,
|
|
606
|
+
});
|
|
607
|
+
if (!nextDispatch) {
|
|
608
|
+
// Post-race recheck: the dispatch closed between the precheck and
|
|
609
|
+
// the ledger write — re-query so the 409 lists what is still open.
|
|
610
|
+
throw new ConflictError(formatNoOpenDispatchError(worker.name, input.dispatchId, listOpenWorkspaceDispatches(workspaceId).filter((item) => item.toAgentId === workerId)));
|
|
611
|
+
}
|
|
612
|
+
dispatch = nextDispatch;
|
|
613
|
+
recordProtocolEvent?.('report');
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
if (!runDataMutation) {
|
|
618
|
+
if (reportQueuedBeforeCommit) {
|
|
646
619
|
try {
|
|
647
|
-
|
|
620
|
+
reportOutbox.deletePendingForDispatch(openDispatch.id);
|
|
648
621
|
}
|
|
649
|
-
catch (
|
|
650
|
-
console.error('[hive] swallowed:teamReport.
|
|
622
|
+
catch (deleteOutboxError) {
|
|
623
|
+
console.error('[hive] swallowed:teamReport.outboxRollback', deleteOutboxError);
|
|
651
624
|
}
|
|
625
|
+
}
|
|
626
|
+
if (messageHandle)
|
|
627
|
+
deleteMessage(messageHandle);
|
|
628
|
+
}
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
if (!dispatch)
|
|
632
|
+
throw new Error('Report dispatch was not committed');
|
|
633
|
+
const committedDispatch = dispatch;
|
|
634
|
+
workspaceStore.markTaskReported(workspaceId, workerId);
|
|
635
|
+
const remainingPendingTaskCount = workspaceStore.getWorker(workspaceId, workerId).pendingTaskCount;
|
|
636
|
+
let forwardError = null;
|
|
637
|
+
let forwarded = false;
|
|
638
|
+
const pendingWarning = buildPendingWarning(worker.name, remainingPendingTaskCount, 'report');
|
|
639
|
+
// Workflow-sourced dispatches: the source is the in-process runner, not a
|
|
640
|
+
// PTY. Resolve its awaiting Promise instead of injecting into orchestrator
|
|
641
|
+
// stdin (which would do nothing — `__workflow__` has no PTY).
|
|
642
|
+
if (committedDispatch.fromAgentId === getWorkflowAgentId(workspaceId)) {
|
|
643
|
+
try {
|
|
644
|
+
workflowDispatchAwaiter.notifyReport(committedDispatch.id, {
|
|
645
|
+
artifacts,
|
|
646
|
+
text,
|
|
647
|
+
...(status ? { status } : {}),
|
|
652
648
|
});
|
|
649
|
+
forwarded = true;
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
forwardError = reportForwardErrorMessage(error);
|
|
653
|
+
console.error('[hive] swallowed:teamReport.workflowForward', error);
|
|
653
654
|
}
|
|
654
655
|
return {
|
|
655
|
-
dispatch,
|
|
656
|
+
dispatch: committedDispatch,
|
|
656
657
|
forwardError,
|
|
657
658
|
forwarded,
|
|
658
659
|
...(pendingWarning ? { pendingWarning } : {}),
|
|
659
660
|
};
|
|
660
661
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
662
|
+
// Real worker reported (not a workflow-internal step) — fire the
|
|
663
|
+
// outbound completion webhook. Best-effort; never blocks the report.
|
|
664
|
+
notifyWebhook?.({
|
|
665
|
+
type: 'report_received',
|
|
666
|
+
workspaceId,
|
|
667
|
+
agentId: workerId,
|
|
668
|
+
agentName: worker.name,
|
|
669
|
+
summary: text.slice(0, 280),
|
|
670
|
+
at: Date.now(),
|
|
671
|
+
});
|
|
672
|
+
if (input.requireActiveRun === true) {
|
|
673
|
+
if (agentRuntime.getActiveRunByAgentId(workspaceId, orchestratorId)) {
|
|
674
|
+
const drainResult = drainReportOutbox(workspaceId, orchestratorId);
|
|
675
|
+
forwarded = false;
|
|
676
|
+
forwardError =
|
|
677
|
+
drainResult.firstSyncError ??
|
|
678
|
+
(reportQueuedBeforeCommit
|
|
679
|
+
? 'Report queued for durable delivery; redelivery is in progress.'
|
|
680
|
+
: null);
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
// Orchestrator is down. Queue the report instead of dropping it;
|
|
684
|
+
// the CLI surfaces forwarded:false and the backlog drains on the
|
|
685
|
+
// orchestrator's next `team list` after it restarts.
|
|
686
|
+
forwardError = reportQueuedBeforeCommit
|
|
687
|
+
? 'Orchestrator is not running; report queued for delivery.'
|
|
688
|
+
: 'Orchestrator is not running; report could not be queued for delivery.';
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// M11: if this worker was spawned with `team spawn --ephemeral`, the
|
|
692
|
+
// report that closes its LAST open dispatch triggers auto-dismiss.
|
|
693
|
+
// Gating on remainingPendingTaskCount === 0 keeps the one-shot
|
|
694
|
+
// semantics for the normal single-dispatch case while not deleting
|
|
695
|
+
// still-queued dispatches un-reported when the orchestrator stacked
|
|
696
|
+
// several sends. (If the last dispatch closes via `team cancel`
|
|
697
|
+
// instead, the worker lingers until the orchestrator-exit cascade /
|
|
698
|
+
// boot cleanup collects it.) Deferred via queueMicrotask so the
|
|
699
|
+
// orchestrator's forward write lands BEFORE the worker's PTY is torn
|
|
700
|
+
// down (otherwise the inject + dismiss race). Skipped for workflow
|
|
701
|
+
// dispatches — workflow workers are managed by the runner's own
|
|
702
|
+
// finally block.
|
|
703
|
+
if (worker.ephemeral === true &&
|
|
704
|
+
worker.spawnedBy === 'orchestrator' &&
|
|
705
|
+
remainingPendingTaskCount === 0 &&
|
|
706
|
+
dismissEphemeralWorker) {
|
|
707
|
+
queueMicrotask(() => {
|
|
708
|
+
if (runtimeClosing())
|
|
709
|
+
return;
|
|
710
|
+
try {
|
|
711
|
+
dismissEphemeralWorker(workspaceId, workerId);
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
console.error('[hive] swallowed:teamReport.ephemeralDismiss', error);
|
|
715
|
+
}
|
|
716
|
+
});
|
|
664
717
|
}
|
|
718
|
+
return {
|
|
719
|
+
dispatch: committedDispatch,
|
|
720
|
+
forwardError,
|
|
721
|
+
forwarded,
|
|
722
|
+
...(pendingWarning ? { pendingWarning } : {}),
|
|
723
|
+
};
|
|
665
724
|
},
|
|
666
725
|
};
|
|
667
726
|
};
|
|
@@ -29,7 +29,7 @@ const ALLOWED_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'
|
|
|
29
29
|
// routes-ui refusing tunnel-tagged requests is layer (3).
|
|
30
30
|
const DENIED_HTTP_PATHS = new Set(['/api/ui/session']);
|
|
31
31
|
// HARDEN (trust-root defense-in-depth): the device-pairing TRUST-ROOT actions — begin a pairing,
|
|
32
|
-
// confirm/approve a device, reject a pending one — are desktop-only at the route layer
|
|
32
|
+
// list pending approvals, confirm/approve a device, reject a pending one — are desktop-only at the route layer
|
|
33
33
|
// (routes-remote.gateLocalDesktopOnly). The Authority Model says a phone can NEVER self-approve a
|
|
34
34
|
// new device, so we mirror /api/ui/session's layered defense: layer 1 hard-denies these here on the
|
|
35
35
|
// bridge (a forwarded frame is Reset(StreamRefused) + audited path_denied), layer 3 is the route gate.
|
|
@@ -39,6 +39,8 @@ const DENIED_HTTP_PATHS = new Set(['/api/ui/session']);
|
|
|
39
39
|
const isDeniedPairingPath = (pathname) => {
|
|
40
40
|
if (pathname === '/api/remote/pairings')
|
|
41
41
|
return true; // begin a pairing (POST)
|
|
42
|
+
if (pathname === '/api/remote/pairings/pending')
|
|
43
|
+
return true; // read pending SAS approvals (GET)
|
|
42
44
|
// confirm/reject under /api/remote/pairings/:id — match the action suffix on a pairings path.
|
|
43
45
|
return (pathname.startsWith('/api/remote/pairings/') &&
|
|
44
46
|
(pathname.endsWith('/confirm') || pathname.endsWith('/reject')));
|
package/package.json
CHANGED