@tt-a1i/hive 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.en.md +15 -6
  3. package/README.md +26 -4
  4. package/dist/src/cli/hive.d.ts +4 -0
  5. package/dist/src/cli/hive.js +25 -3
  6. package/dist/src/cli/team.d.ts +8 -1
  7. package/dist/src/cli/team.js +111 -11
  8. package/dist/src/server/action-center-summary.d.ts +193 -0
  9. package/dist/src/server/action-center-summary.js +188 -0
  10. package/dist/src/server/agent-command-resolver.d.ts +6 -0
  11. package/dist/src/server/agent-command-resolver.js +16 -0
  12. package/dist/src/server/agent-manager.js +11 -1
  13. package/dist/src/server/agent-run-starter.js +47 -6
  14. package/dist/src/server/agent-runtime-types.d.ts +4 -0
  15. package/dist/src/server/agent-startup-instructions.d.ts +4 -0
  16. package/dist/src/server/agent-startup-instructions.js +35 -9
  17. package/dist/src/server/agent-stdin-dispatcher.js +17 -9
  18. package/dist/src/server/diagnostics-support-bundle.d.ts +288 -0
  19. package/dist/src/server/diagnostics-support-bundle.js +179 -0
  20. package/dist/src/server/dispatch-ledger-store.d.ts +4 -1
  21. package/dist/src/server/dispatch-ledger-store.js +46 -6
  22. package/dist/src/server/hive-envelope-escape.d.ts +2 -0
  23. package/dist/src/server/hive-envelope-escape.js +2 -0
  24. package/dist/src/server/hive-team-guidance.d.ts +1 -1
  25. package/dist/src/server/hive-team-guidance.js +67 -25
  26. package/dist/src/server/message-log-store.d.ts +1 -1
  27. package/dist/src/server/post-start-input-writer.js +8 -2
  28. package/dist/src/server/preset-launch-support.d.ts +2 -0
  29. package/dist/src/server/preset-launch-support.js +65 -2
  30. package/dist/src/server/protocol-event-stats.d.ts +39 -0
  31. package/dist/src/server/protocol-event-stats.js +84 -0
  32. package/dist/src/server/recovery-summary.js +19 -14
  33. package/dist/src/server/role-template-store.d.ts +1 -1
  34. package/dist/src/server/role-templates.d.ts +1 -0
  35. package/dist/src/server/role-templates.js +43 -29
  36. package/dist/src/server/routes-action-center.d.ts +2 -0
  37. package/dist/src/server/routes-action-center.js +37 -0
  38. package/dist/src/server/routes-diagnostics.d.ts +2 -0
  39. package/dist/src/server/routes-diagnostics.js +17 -0
  40. package/dist/src/server/routes-scenarios.d.ts +25 -0
  41. package/dist/src/server/routes-scenarios.js +89 -0
  42. package/dist/src/server/routes-settings.js +2 -11
  43. package/dist/src/server/routes-team-memory.js +52 -0
  44. package/dist/src/server/routes-team.js +40 -20
  45. package/dist/src/server/routes-workspace-memory-dreams.js +8 -0
  46. package/dist/src/server/routes-workspace-uploads.d.ts +2 -0
  47. package/dist/src/server/routes-workspace-uploads.js +154 -0
  48. package/dist/src/server/routes-workspaces.js +29 -3
  49. package/dist/src/server/routes.js +8 -0
  50. package/dist/src/server/runtime-message-builders.d.ts +0 -1
  51. package/dist/src/server/runtime-message-builders.js +0 -8
  52. package/dist/src/server/runtime-store-contract.d.ts +15 -0
  53. package/dist/src/server/runtime-store-dream.d.ts +14 -1
  54. package/dist/src/server/runtime-store-dream.js +49 -1
  55. package/dist/src/server/runtime-store-helpers.d.ts +7 -0
  56. package/dist/src/server/runtime-store-helpers.js +85 -22
  57. package/dist/src/server/runtime-store-worker-mutations.d.ts +11 -0
  58. package/dist/src/server/runtime-store-worker-mutations.js +46 -0
  59. package/dist/src/server/runtime-store-workflows.js +10 -6
  60. package/dist/src/server/runtime-store.js +34 -42
  61. package/dist/src/server/scenario-presets.d.ts +25 -0
  62. package/dist/src/server/scenario-presets.js +35 -0
  63. package/dist/src/server/sentinel-heartbeat.d.ts +30 -0
  64. package/dist/src/server/sentinel-heartbeat.js +145 -0
  65. package/dist/src/server/spawn-cli-resolver.d.ts +37 -0
  66. package/dist/src/server/spawn-cli-resolver.js +70 -0
  67. package/dist/src/server/spawn-worker-defaults.d.ts +13 -0
  68. package/dist/src/server/spawn-worker-defaults.js +45 -0
  69. package/dist/src/server/sqlite-schema-v32.d.ts +2 -0
  70. package/dist/src/server/sqlite-schema-v32.js +17 -0
  71. package/dist/src/server/sqlite-schema-v33.d.ts +3 -0
  72. package/dist/src/server/sqlite-schema-v33.js +18 -0
  73. package/dist/src/server/sqlite-schema-v34.d.ts +11 -0
  74. package/dist/src/server/sqlite-schema-v34.js +19 -0
  75. package/dist/src/server/sqlite-schema-v35.d.ts +3 -0
  76. package/dist/src/server/sqlite-schema-v35.js +23 -0
  77. package/dist/src/server/sqlite-schema.d.ts +1 -1
  78. package/dist/src/server/sqlite-schema.js +35 -1
  79. package/dist/src/server/system-message.d.ts +5 -2
  80. package/dist/src/server/system-message.js +5 -2
  81. package/dist/src/server/tasks-file-watcher.d.ts +8 -0
  82. package/dist/src/server/tasks-file-watcher.js +31 -2
  83. package/dist/src/server/team-authz.d.ts +9 -1
  84. package/dist/src/server/team-authz.js +24 -0
  85. package/dist/src/server/team-list-serializer.d.ts +2 -2
  86. package/dist/src/server/team-list-serializer.js +2 -1
  87. package/dist/src/server/team-memory-digest.js +4 -4
  88. package/dist/src/server/team-memory-dream-applier.js +24 -3
  89. package/dist/src/server/team-memory-dream-prompt.d.ts +13 -0
  90. package/dist/src/server/team-memory-dream-prompt.js +91 -0
  91. package/dist/src/server/team-memory-dream-run-store.d.ts +2 -0
  92. package/dist/src/server/team-memory-dream-run-store.js +14 -4
  93. package/dist/src/server/team-memory-dream-runner.d.ts +2 -21
  94. package/dist/src/server/team-memory-dream-runner.js +3 -148
  95. package/dist/src/server/team-memory-dream-store.d.ts +1 -1
  96. package/dist/src/server/team-memory-dream-store.js +1 -1
  97. package/dist/src/server/team-operations.d.ts +18 -2
  98. package/dist/src/server/team-operations.js +222 -33
  99. package/dist/src/server/team-recap.d.ts +10 -0
  100. package/dist/src/server/team-recap.js +73 -0
  101. package/dist/src/server/terminal-input-profile.js +88 -9
  102. package/dist/src/server/upload-limits.d.ts +2 -0
  103. package/dist/src/server/upload-limits.js +2 -0
  104. package/dist/src/server/workflow-cli-policy.d.ts +7 -2
  105. package/dist/src/server/workflow-cli-policy.js +15 -3
  106. package/dist/src/server/workflow-run-store.d.ts +1 -0
  107. package/dist/src/server/workflow-run-store.js +11 -1
  108. package/dist/src/server/workflow-runner.d.ts +4 -1
  109. package/dist/src/server/workflow-runner.js +418 -118
  110. package/dist/src/server/workflow-script-loader.d.ts +3 -2
  111. package/dist/src/server/workflow-script-loader.js +161 -0
  112. package/dist/src/server/workspace-store-contract.d.ts +2 -0
  113. package/dist/src/server/workspace-store.d.ts +1 -1
  114. package/dist/src/server/workspace-store.js +40 -30
  115. package/dist/src/server/workspace-upload-store.d.ts +40 -0
  116. package/dist/src/server/workspace-upload-store.js +295 -0
  117. package/dist/src/shared/scenario-presets.d.ts +32 -0
  118. package/dist/src/shared/scenario-presets.js +69 -0
  119. package/dist/src/shared/types.d.ts +12 -1
  120. package/package.json +1 -1
  121. package/web/dist/assets/AddWorkerDialog-DBLhwb91.js +2 -0
  122. package/web/dist/assets/AddWorkspaceFlow-cxvhVAsT.js +1 -0
  123. package/web/dist/assets/FirstRunWizard-DlEPnWWw.js +1 -0
  124. package/web/dist/assets/{MarketplaceDrawer-Dd8WIA8T.js → MarketplaceDrawer-CfSiRi8e.js} +11 -11
  125. package/web/dist/assets/TaskGraphDrawer-C2JufcPs.js +1 -0
  126. package/web/dist/assets/WhatsNewDialog-vP7buLos.js +1 -0
  127. package/web/dist/assets/WorkerModal-CSorwcdP.js +1 -0
  128. package/web/dist/assets/{WorkflowsDrawer-Bjf4olbR.js → WorkflowsDrawer-BXS3w9Uq.js} +1 -1
  129. package/web/dist/assets/WorkspaceMemoryDrawer-D71ivohr.js +1 -0
  130. package/web/dist/assets/{WorkspaceTaskDrawer-BIWwISvA.js → WorkspaceTaskDrawer-CGCTSHKa.js} +1 -1
  131. package/web/dist/assets/index-BcwN8cCw.js +79 -0
  132. package/web/dist/assets/index-StXTPHls.css +1 -0
  133. package/web/dist/assets/{search-Bk2HQvO7.js → search-BZw4T67h.js} +1 -1
  134. package/web/dist/assets/{square-terminal-D93m9hfY.js → square-terminal-B7E57In1.js} +1 -1
  135. package/web/dist/index.html +2 -2
  136. package/web/dist/sw.js +1 -1
  137. package/dist/src/server/env-sync-message.d.ts +0 -9
  138. package/dist/src/server/env-sync-message.js +0 -29
  139. package/web/dist/assets/AddWorkerDialog-CbV75qUX.js +0 -2
  140. package/web/dist/assets/AddWorkspaceFlow-CwV-7wPx.js +0 -1
  141. package/web/dist/assets/FirstRunWizard-a6PWIK3x.js +0 -1
  142. package/web/dist/assets/TaskGraphDrawer-Bk5WFIk_.js +0 -1
  143. package/web/dist/assets/WhatsNewDialog-C2VZaip0.js +0 -1
  144. package/web/dist/assets/WorkerModal-DucW-9YT.js +0 -1
  145. package/web/dist/assets/WorkspaceMemoryDrawer-DglCy_5f.js +0 -1
  146. package/web/dist/assets/index-BAiLYajK.css +0 -1
  147. package/web/dist/assets/index-BV2k9Dts.js +0 -73
@@ -1,6 +1,9 @@
1
1
  import { buildOrchestratorReportPayload } from './agent-stdin-dispatcher.js';
2
+ import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
3
+ import { escapeHiveEnvelopeText } from './hive-envelope-escape.js';
2
4
  import { ConflictError } from './http-errors.js';
3
5
  import { createReportMessage, createSendMessage, createStatusMessage, createUserInputMessage, } from './runtime-message-builders.js';
6
+ import { assertWorkerDispatchable } from './team-authz.js';
4
7
  import { getWorkflowAgentId } from './workspace-store-support.js';
5
8
  /* Roster snapshot embedded in the 409 the orchestrator sees when it
6
9
  dispatches to a missing name. Format prioritizes the orchestrator's
@@ -14,7 +17,7 @@ export const formatUnknownWorkerError = (workerName, roster) => {
14
17
  if (roster.length === 0) {
15
18
  return [
16
19
  `Unknown worker "${workerName}": this workspace currently has no workers.`,
17
- 'Ask the user to add a worker in the Hive UI (Add Worker), then run `team list` and retry.',
20
+ 'Run `team spawn <role> [--name <name>] [--ephemeral]`, then `team list` and retry with the spawned worker name. Do not ask the user to add workers unless a real role decision is missing.',
18
21
  ].join('\n');
19
22
  }
20
23
  const lines = roster.map((entry) => ` - ${entry.name} (${entry.role}, ${entry.status}, ${entry.pendingTaskCount} pending)`);
@@ -24,12 +27,30 @@ export const formatUnknownWorkerError = (workerName, roster) => {
24
27
  'Retry with `team send "<one of the names above>" "<task>"`, or run `team list` to refresh the roster.',
25
28
  ].join('\n');
26
29
  };
30
+ /* Worker-facing 409 for `team report`. The same generic "no open dispatch"
31
+ used to cover two very different states: (a) the worker passed a --dispatch
32
+ id that is wrong or already closed while OTHER dispatches are still open —
33
+ recoverable in one retry if we list the real ids; (b) the worker truly has
34
+ nothing open — the right move is `team status`, not a report retry loop. */
35
+ export const formatNoOpenDispatchError = (workerName, requestedDispatchId, openDispatches) => {
36
+ if (requestedDispatchId !== undefined && openDispatches.length > 0) {
37
+ const lines = openDispatches.map((dispatch) => ` - ${dispatch.id} (${dispatch.status}): ${dispatch.text.slice(0, 60)}${dispatch.text.length > 60 ? '…' : ''}`);
38
+ return [
39
+ `Dispatch ${requestedDispatchId} is not open for worker ${workerName}. Your open dispatches:`,
40
+ ...lines,
41
+ 'Re-run `team report --dispatch <one of the ids above>`, or omit --dispatch to close the oldest.',
42
+ ].join('\n');
43
+ }
44
+ return (`No open dispatch for worker: ${workerName}. Nothing is awaiting your report — ` +
45
+ 'if you have progress or standby info to share, use `team status "<state>"` instead.');
46
+ };
27
47
  const reportForwardErrorMessage = (error) => error instanceof Error ? error.message : String(error);
28
48
  const buildPendingWarning = (workerName, pendingTaskCount, action) => pendingTaskCount > 0
29
49
  ? `Hive recorded the ${action}, but ${workerName} still has ${pendingTaskCount} open dispatch${pendingTaskCount === 1 ? '' : 'es'}. ` +
30
50
  'A worker stays working until every dispatch is closed with `team report --dispatch <id>` or `team cancel --dispatch <id>`.'
31
51
  : undefined;
32
- export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, reportOutbox, notifyWebhook, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, }) => {
52
+ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, listOpenWorkspaceDispatches, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, claimQueuedDispatch, reparkClaimedDispatch, reportOutbox, notifyWebhook, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, recordProtocolEvent, getFlags, }) => {
53
+ const flags = () => getFlags?.() ?? FEATURE_FLAGS_ALL_OFF;
33
54
  // Best-effort redelivery of reports a prior orchestrator outage stranded.
34
55
  // Called when a fresh report confirms the orchestrator is reachable and
35
56
  // when the orchestrator polls `team list` (its natural post-restart wakeup).
@@ -71,7 +92,10 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
71
92
  throw error;
72
93
  }
73
94
  };
74
- const cancelUndeliveredDispatch = (workspaceId, workerId, dispatchId, reason, workflowRunId) => {
95
+ const cancelUndeliveredDispatch = (workspaceId, workerId, dispatchId, reason, workflowRunId,
96
+ /** Non-workflow issuer to notify — without this the orchestrator already
97
+ * holds an ok:true for a dispatch that silently died (audit #34). */
98
+ notifyAgentId) => {
75
99
  const cancelled = markDispatchCancelled({ dispatchId, reason, workspaceId });
76
100
  if (!cancelled)
77
101
  return;
@@ -84,11 +108,141 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
84
108
  if (workflowRunId !== undefined) {
85
109
  workflowDispatchAwaiter.notifyCancel(dispatchId, reason);
86
110
  }
111
+ if (notifyAgentId !== undefined) {
112
+ notifyIssuerDurably(workspaceId, notifyAgentId, dispatchId, `Dispatch ${dispatchId} to @${workerNameOrId(workspaceId, workerId)} was CANCELLED: delivery failed (${reason}). ` +
113
+ 'The worker likely exited before receiving it — re-send after the worker is started, or dispatch to another worker.');
114
+ }
115
+ };
116
+ const workerNameOrId = (workspaceId, workerId) => {
117
+ try {
118
+ return workspaceStore.getWorker(workspaceId, workerId).name;
119
+ }
120
+ catch {
121
+ // Worker row already gone (e.g. dismissed) — fall back to the id.
122
+ return workerId;
123
+ }
124
+ };
125
+ /** Issuer notification that survives the issuer being down: try the live
126
+ * PTY first; if the issuer has no active run (or the write rejects), park
127
+ * the notice in the report outbox — it drains on the issuer's next
128
+ * `team list` / start, the same redelivery path worker reports use.
129
+ * (A fire-and-forget writeSystemMessageToAgent silently no-ops without an
130
+ * active run, which is exactly when failures cluster — post-restart.) */
131
+ const notifyIssuerDurably = (workspaceId, issuerAgentId, dispatchId, text) => {
132
+ const payload = `<hive-system-reminder>\n${escapeHiveEnvelopeText(text)}\n</hive-system-reminder>\n`;
133
+ const park = () => {
134
+ try {
135
+ reportOutbox.enqueue({
136
+ workspaceId,
137
+ targetAgentId: issuerAgentId,
138
+ dispatchId,
139
+ payload,
140
+ });
141
+ }
142
+ catch (error) {
143
+ console.error('[hive] swallowed:teamDispatch.notifyOutbox', error);
144
+ }
145
+ };
146
+ try {
147
+ agentRuntime
148
+ .deliverSystemMessageToAgent(workspaceId, issuerAgentId, payload, {
149
+ requireActiveRun: true,
150
+ })
151
+ .catch(park);
152
+ }
153
+ catch {
154
+ park();
155
+ }
156
+ };
157
+ /** Called before a worker (and its dispatch rows) are deleted: open
158
+ * non-workflow dispatches would otherwise vanish without the issuer ever
159
+ * hearing — a third silent outcome next to delivered/cancelled. */
160
+ const notifyIssuersOfDroppedDispatches = (workspaceId, workerId, reason) => {
161
+ let open;
162
+ try {
163
+ open = listOpenWorkspaceDispatches(workspaceId).filter((item) => item.toAgentId === workerId &&
164
+ item.workflowRunId === null &&
165
+ item.fromAgentId !== null &&
166
+ item.fromAgentId !== getWorkflowAgentId(workspaceId));
167
+ }
168
+ catch (error) {
169
+ console.error('[hive] swallowed:teamDismiss.listOpen', error);
170
+ return;
171
+ }
172
+ const workerName = workerNameOrId(workspaceId, workerId);
173
+ for (const item of open) {
174
+ if (!item.fromAgentId)
175
+ continue;
176
+ notifyIssuerDurably(workspaceId, item.fromAgentId, item.id, `Dispatch ${item.id} to @${workerName} was DROPPED: ${reason}. ` +
177
+ 'It will not run and cannot be reported — re-dispatch the task to another worker if it still matters.');
178
+ }
179
+ };
180
+ /** #33: deliver dispatches parked while their worker was stopped. Called on
181
+ * worker run start (lifecycle) and after dispatchTask auto-starts a worker.
182
+ * `claimQueuedDispatch` makes each delivery single-shot under races (UI
183
+ * start vs auto-start send); failures compensate via
184
+ * cancelUndeliveredDispatch and notify the issuer. Workflow dispatches are
185
+ * excluded — the runner owns their lifecycle and its awaiters must not see
186
+ * a late delivery after the run already ended. */
187
+ const replayQueuedDispatches = (workspaceId, workerId, options = {}) => {
188
+ if (!agentRuntime.getActiveRunByAgentId(workspaceId, workerId))
189
+ return;
190
+ const queued = listOpenWorkspaceDispatches(workspaceId).filter((item) => item.toAgentId === workerId &&
191
+ item.status === 'queued' &&
192
+ item.workflowRunId === null &&
193
+ item.fromAgentId !== getWorkflowAgentId(workspaceId) &&
194
+ item.id !== options.excludeDispatchId);
195
+ if (queued.length === 0)
196
+ return;
197
+ let worker;
198
+ try {
199
+ worker = workspaceStore.getWorker(workspaceId, workerId);
200
+ }
201
+ catch {
202
+ return;
203
+ }
204
+ /* A replay write only fails when the fresh run is already dead
205
+ (PtyInactive) — nothing reached a live CLI, so the dispatch goes BACK
206
+ to parked for the next start instead of being cancelled: one bad start
207
+ must not destroy parked work. If the row is no longer 'submitted'
208
+ (reported/cancelled meanwhile), the repark loses and we leave it be. */
209
+ const failDispatch = (dispatchId, error) => {
210
+ try {
211
+ if (!reparkClaimedDispatch(dispatchId)) {
212
+ console.error('[hive] swallowed:teamReplay.reparkLost', dispatchId);
213
+ }
214
+ }
215
+ catch (reparkError) {
216
+ console.error('[hive] swallowed:teamReplay.repark', reparkError);
217
+ }
218
+ console.error('[hive] swallowed:teamReplay.writePrompt', error);
219
+ };
220
+ for (const item of queued) {
221
+ if (!claimQueuedDispatch(item.id))
222
+ continue;
223
+ let senderName = 'Orchestrator';
224
+ if (item.fromAgentId) {
225
+ try {
226
+ senderName = workspaceStore.getAgent(workspaceId, item.fromAgentId).name;
227
+ }
228
+ catch {
229
+ // Issuer gone; the default label is still actionable for the worker.
230
+ }
231
+ }
232
+ try {
233
+ const writePrompt = agentRuntime.writeSendPrompt(workspaceId, workerId, item.id, senderName, worker.description, item.text);
234
+ void writePrompt.catch((error) => failDispatch(item.id, error));
235
+ }
236
+ catch (error) {
237
+ failDispatch(item.id, error);
238
+ }
239
+ }
87
240
  };
88
241
  const dispatchTask = async (workspaceId, workerId, text, input = {}) => {
89
242
  const fromAgentId = input.fromAgentId;
90
243
  const sender = fromAgentId ? workspaceStore.getAgent(workspaceId, fromAgentId) : undefined;
91
244
  const worker = workspaceStore.getWorker(workspaceId, workerId);
245
+ assertWorkerDispatchable(worker);
92
246
  const message = createSendMessage(workspaceId, workerId, text, input.fromAgentId);
93
247
  const messageHandle = insertMessage(message);
94
248
  let dispatch;
@@ -110,30 +264,57 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
110
264
  if (input.label !== undefined)
111
265
  dispatchInput.label = input.label;
112
266
  dispatch = createDispatch(dispatchInput);
267
+ recordProtocolEvent?.('send');
113
268
  const dispatchId = dispatch.id;
114
269
  if (fromAgentId && sender) {
115
270
  const shouldAutoStartWorker = input.autoStartWorker !== false;
116
271
  const hadActiveRun = Boolean(agentRuntime.getActiveRunByAgentId(workspaceId, workerId));
117
272
  if (!hadActiveRun && shouldAutoStartWorker) {
118
273
  await ensureWorkerRun(workspaceId, workerId, input.hivePort ?? '');
274
+ // The fresh run may also have OLDER parked dispatches waiting —
275
+ // deliver them ahead of this one. Claims keep delivery single-shot;
276
+ // the current dispatch is claimed by this same path just below.
277
+ replayQueuedDispatches(workspaceId, workerId, { excludeDispatchId: dispatchId });
119
278
  }
120
279
  if (!shouldAutoStartWorker && !agentRuntime.getActiveRunByAgentId(workspaceId, workerId)) {
121
280
  workspaceStore.markTaskDispatched(workspaceId, workerId);
122
281
  pendingMarked = true;
123
- return dispatch;
282
+ // Parked for a stopped worker: replayQueuedDispatches delivers it on
283
+ // the next worker start. Tag the record so the route/CLI can say so.
284
+ return Object.assign(dispatch, { queuedForStoppedWorker: true });
124
285
  }
125
286
  const isWorkflowDispatch = input.workflowRunId !== undefined || input.fromAgentId === getWorkflowAgentId(workspaceId);
126
- markDispatchSubmitted(dispatchId);
287
+ // Revalidate the worker before claiming: it may have been deleted
288
+ // while ensureWorkerRun was awaited (R2 hardening) — a vanished worker
289
+ // must not leave a claimed-but-undeliverable dispatch behind.
290
+ workspaceStore.getWorker(workspaceId, workerId);
291
+ // Claim (queued → submitted) instead of a blind mark: a UI-initiated
292
+ // start can replay-deliver this dispatch while ensureWorkerRun above
293
+ // was awaited — losing the claim means delivery is already owned.
294
+ const claimed = claimQueuedDispatch(dispatchId);
127
295
  workspaceStore.markTaskDispatched(workspaceId, workerId);
128
296
  pendingMarked = true;
129
- try {
130
- const writePrompt = agentRuntime.writeSendPrompt(workspaceId, workerId, dispatchId, sender.name, worker.description, text);
131
- void writePrompt.catch((error) => {
132
- // `team send` is intentionally asynchronous (§3.3). A worker that
133
- // exits during paste-submit did not receive actionable work, so
134
- // close the open dispatch instead of leaving a fake pending task.
297
+ if (claimed)
298
+ try {
299
+ const writePrompt = agentRuntime.writeSendPrompt(workspaceId, workerId, dispatchId, sender.name, worker.description, text);
300
+ void writePrompt.catch((error) => {
301
+ // `team send` is intentionally asynchronous (§3.3). A worker that
302
+ // exits during paste-submit did not receive actionable work, so
303
+ // close the open dispatch instead of leaving a fake pending task.
304
+ try {
305
+ cancelUndeliveredDispatch(workspaceId, workerId, dispatchId, reportForwardErrorMessage(error), input.workflowRunId, isWorkflowDispatch ? undefined : fromAgentId);
306
+ }
307
+ catch (cancelError) {
308
+ if (!isWorkflowDispatch)
309
+ console.error('[hive] swallowed:teamDispatch.cancelUndelivered', cancelError);
310
+ }
311
+ if (!isWorkflowDispatch)
312
+ console.error('[hive] swallowed:teamDispatch.writePrompt', error);
313
+ });
314
+ }
315
+ catch (error) {
135
316
  try {
136
- cancelUndeliveredDispatch(workspaceId, workerId, dispatchId, reportForwardErrorMessage(error), input.workflowRunId);
317
+ cancelUndeliveredDispatch(workspaceId, workerId, dispatchId, reportForwardErrorMessage(error), input.workflowRunId, isWorkflowDispatch ? undefined : fromAgentId);
137
318
  }
138
319
  catch (cancelError) {
139
320
  if (!isWorkflowDispatch)
@@ -141,19 +322,7 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
141
322
  }
142
323
  if (!isWorkflowDispatch)
143
324
  console.error('[hive] swallowed:teamDispatch.writePrompt', error);
144
- });
145
- }
146
- catch (error) {
147
- try {
148
- cancelUndeliveredDispatch(workspaceId, workerId, dispatchId, reportForwardErrorMessage(error), input.workflowRunId);
149
325
  }
150
- catch (cancelError) {
151
- if (!isWorkflowDispatch)
152
- console.error('[hive] swallowed:teamDispatch.cancelUndelivered', cancelError);
153
- }
154
- if (!isWorkflowDispatch)
155
- console.error('[hive] swallowed:teamDispatch.writePrompt', error);
156
- }
157
326
  }
158
327
  else {
159
328
  workspaceStore.markTaskDispatched(workspaceId, workerId);
@@ -193,6 +362,13 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
193
362
  throw new ConflictError(`No open dispatch: ${dispatchId}`);
194
363
  }
195
364
  workspaceStore.markTaskCancelled(workspaceId, dispatch.toAgentId);
365
+ recordProtocolEvent?.('cancel');
366
+ // A workflow-owned dispatch cancelled from outside the runner (e.g. an
367
+ // orchestrator that got hold of the id) must still resolve the runner's
368
+ // awaiter — otherwise the run wedges until its step timeout.
369
+ if (dispatch.workflowRunId !== null) {
370
+ workflowDispatchAwaiter.notifyCancel(dispatch.id, input.reason);
371
+ }
196
372
  let forwardError = null;
197
373
  let forwarded = false;
198
374
  try {
@@ -207,6 +383,8 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
207
383
  },
208
384
  dispatchTask,
209
385
  drainReportOutbox,
386
+ replayQueuedDispatches,
387
+ notifyIssuersOfDroppedDispatches,
210
388
  async dispatchTaskByWorkerName(workspaceId, workerName, text, input = {}) {
211
389
  /* Build the roster once so a missing-name path can surface it without
212
390
  a second store call. We deliberately don't go through
@@ -238,6 +416,7 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
238
416
  const artifacts = input.artifacts ?? [];
239
417
  const worker = workspaceStore.getWorker(workspaceId, workerId);
240
418
  const messageHandle = insertMessage(createStatusMessage(workspaceId, workerId, text, artifacts));
419
+ recordProtocolEvent?.('status');
241
420
  try {
242
421
  let forwardError = null;
243
422
  let forwarded = false;
@@ -273,7 +452,7 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
273
452
  const worker = workspaceStore.getWorker(workspaceId, workerId);
274
453
  const openDispatch = findOpenDispatch(workspaceId, workerId, input.dispatchId);
275
454
  if (!openDispatch) {
276
- throw new ConflictError(`No open dispatch for worker: ${worker.name}`);
455
+ throw new ConflictError(formatNoOpenDispatchError(worker.name, input.dispatchId, listOpenWorkspaceDispatches(workspaceId).filter((item) => item.toAgentId === workerId)));
277
456
  }
278
457
  const messageHandle = insertMessage(createReportMessage(workspaceId, workerId, text, status, artifacts));
279
458
  try {
@@ -285,9 +464,12 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
285
464
  workspaceId,
286
465
  });
287
466
  if (!dispatch) {
288
- throw new ConflictError(`No open dispatch for worker: ${worker.name}`);
467
+ // Post-race recheck: the dispatch closed between the precheck and
468
+ // the ledger write — re-query so the 409 lists what is still open.
469
+ throw new ConflictError(formatNoOpenDispatchError(worker.name, input.dispatchId, listOpenWorkspaceDispatches(workspaceId).filter((item) => item.toAgentId === workerId)));
289
470
  }
290
471
  workspaceStore.markTaskReported(workspaceId, workerId);
472
+ recordProtocolEvent?.('report');
291
473
  const remainingPendingTaskCount = workspaceStore.getWorker(workspaceId, workerId).pendingTaskCount;
292
474
  let forwardError = null;
293
475
  let forwarded = false;
@@ -331,7 +513,7 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
331
513
  // backlog a prior outage stranded first, in arrival order, before
332
514
  // this one (both ride the dispatcher's per-agent serial queue).
333
515
  drainReportOutbox(workspaceId);
334
- const payload = buildOrchestratorReportPayload(worker.name, text, artifacts);
516
+ const payload = buildOrchestratorReportPayload(worker.name, text, artifacts, flags());
335
517
  if (agentRuntime.getActiveRunByAgentId(workspaceId, orchestratorId)) {
336
518
  try {
337
519
  const delivery = agentRuntime.deliverReportToOrchestrator(workspaceId, worker.name, text, artifacts, { requireActiveRun: true });
@@ -376,14 +558,21 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
376
558
  });
377
559
  }
378
560
  }
379
- // M11: if this worker was spawned with `team spawn --ephemeral`, this
380
- // first successful report is its trigger to auto-dismiss. Deferred via
381
- // queueMicrotask so the orchestrator's forward write lands BEFORE the
382
- // worker's PTY is torn down (otherwise the inject + dismiss race).
383
- // Skipped for workflow dispatches workflow workers are managed by
384
- // the runner's own finally block.
561
+ // M11: if this worker was spawned with `team spawn --ephemeral`, the
562
+ // report that closes its LAST open dispatch triggers auto-dismiss.
563
+ // Gating on remainingPendingTaskCount === 0 keeps the one-shot
564
+ // semantics for the normal single-dispatch case while not deleting
565
+ // still-queued dispatches un-reported when the orchestrator stacked
566
+ // several sends. (If the last dispatch closes via `team cancel`
567
+ // instead, the worker lingers until the orchestrator-exit cascade /
568
+ // boot cleanup collects it.) Deferred via queueMicrotask so the
569
+ // orchestrator's forward write lands BEFORE the worker's PTY is torn
570
+ // down (otherwise the inject + dismiss race). Skipped for workflow
571
+ // dispatches — workflow workers are managed by the runner's own
572
+ // finally block.
385
573
  if (worker.ephemeral === true &&
386
574
  worker.spawnedBy === 'orchestrator' &&
575
+ remainingPendingTaskCount === 0 &&
387
576
  dismissEphemeralWorker) {
388
577
  queueMicrotask(() => {
389
578
  try {
@@ -0,0 +1,10 @@
1
+ import type { TeamListItem } from '../shared/types.js';
2
+ import type { DispatchRecord } from './dispatch-ledger-store.js';
3
+ export interface TeamRecapInput {
4
+ /** Recent dispatches, newest first (listRecentWorkspaceDispatches order). */
5
+ dispatches: DispatchRecord[];
6
+ now: number;
7
+ workers: TeamListItem[];
8
+ workspaceName: string;
9
+ }
10
+ export declare const buildTeamRecapMarkdown: (input: TeamRecapInput) => string;
@@ -0,0 +1,73 @@
1
+ // Privacy note — recap vs. support bundle: dispatch label/phase/reportText are
2
+ // workflow-authored free text (the first-party guidance even puts file paths
3
+ // in labels). The shareable support bundle therefore reduces them to presence
4
+ // flags (see action-center-summary.ts `includeTextEvidence === false`). The
5
+ // recap is the opposite case on purpose: the user explicitly clicks "Copy
6
+ // recap" to share their own team's progress, so the verbatim text IS the
7
+ // product. Do not "sanitize" this output to match the bundle.
8
+ const REPORT_PREVIEW_MAX = 200;
9
+ /** Markdown table cells cannot contain raw pipes or newlines. */
10
+ const tableCell = (text) => text.replace(/\s+/gu, ' ').trim().replace(/\|/gu, '\\|');
11
+ const truncate = (text, max) => {
12
+ const normalized = text.replace(/\s+/gu, ' ').trim();
13
+ return normalized.length > max ? `${normalized.slice(0, max)}...` : normalized;
14
+ };
15
+ const dispatchTimestamp = (dispatch) => dispatch.reportedAt ?? dispatch.submittedAt ?? dispatch.deliveredAt ?? dispatch.createdAt;
16
+ const formatAge = (timestamp, now) => {
17
+ const seconds = Math.max(0, Math.floor((now - timestamp) / 1000));
18
+ if (seconds < 60)
19
+ return `${seconds}s`;
20
+ if (seconds < 3600)
21
+ return `${Math.floor(seconds / 60)}m`;
22
+ if (seconds < 86400)
23
+ return `${Math.floor(seconds / 3600)}h`;
24
+ return `${Math.floor(seconds / 86400)}d`;
25
+ };
26
+ export const buildTeamRecapMarkdown = (input) => {
27
+ const workersById = new Map(input.workers.map((worker) => [worker.id, worker]));
28
+ const workerName = (agentId) => workersById.get(agentId)?.name ?? agentId;
29
+ const lines = [];
30
+ lines.push(`# Team recap — ${input.workspaceName}`);
31
+ lines.push('');
32
+ lines.push(`Generated at ${new Date(input.now).toISOString()}`);
33
+ lines.push('');
34
+ lines.push('## Team');
35
+ lines.push('');
36
+ if (input.workers.length === 0) {
37
+ lines.push('_No team members._');
38
+ }
39
+ else {
40
+ for (const worker of input.workers) {
41
+ lines.push(`- **${worker.name}** — ${worker.role} · ${worker.status}`);
42
+ }
43
+ }
44
+ lines.push('');
45
+ lines.push('## Recent dispatches');
46
+ lines.push('');
47
+ if (input.dispatches.length === 0) {
48
+ lines.push('_No dispatches yet._');
49
+ }
50
+ else {
51
+ lines.push('| Label | Status | Worker | Age |');
52
+ lines.push('| --- | --- | --- | --- |');
53
+ for (const dispatch of input.dispatches) {
54
+ const label = dispatch.label ?? dispatch.phase ?? truncate(dispatch.text, 60);
55
+ const age = formatAge(dispatchTimestamp(dispatch), input.now);
56
+ lines.push(`| ${tableCell(label)} | ${dispatch.status} | ${tableCell(workerName(dispatch.toAgentId))} | ${age} |`);
57
+ }
58
+ }
59
+ lines.push('');
60
+ const reported = input.dispatches.filter((dispatch) => dispatch.status === 'reported' && dispatch.reportText);
61
+ if (reported.length > 0) {
62
+ lines.push('## Reports');
63
+ lines.push('');
64
+ for (const dispatch of reported) {
65
+ const heading = dispatch.label ?? dispatch.phase ?? workerName(dispatch.toAgentId);
66
+ lines.push(`### ${truncate(heading, 80)}`);
67
+ lines.push('');
68
+ lines.push(truncate(dispatch.reportText ?? '', REPORT_PREVIEW_MAX));
69
+ lines.push('');
70
+ }
71
+ }
72
+ return `${lines.join('\n').trimEnd()}\n`;
73
+ };
@@ -1,10 +1,91 @@
1
- import { normalizeExecutableToken } from './startup-command-parser.js';
1
+ import { getStartupCommandExecutable, normalizeExecutableToken } from './startup-command-parser.js';
2
+ const PROFILE_BY_BRAND = {
3
+ codex: 'codex',
4
+ grok: 'grok',
5
+ opencode: 'opencode',
6
+ };
7
+ // Shells whose spawn shape wraps the real command line in their final
8
+ // argument (`cmd /d /s /c <line>`, `zsh -lic <line>`). Legacy launch configs
9
+ // persisted before interactiveCommand existed only have this shape, so the
10
+ // profile must see through it or those rows degrade to 'default' forever.
11
+ const SHELL_WRAPPERS = new Set(['bash', 'cmd', 'fish', 'ksh', 'powershell', 'pwsh', 'sh', 'zsh']);
12
+ const NPX_WRAPPERS = new Set(['bunx', 'npx', 'pnpx']);
2
13
  const isCodexNpmEntrypoint = (arg) => {
3
14
  if (!arg)
4
15
  return false;
5
16
  const normalized = arg.replace(/\\/gu, '/');
6
17
  return /(?:^|\/)@openai\/codex\/bin\/codex\.js$/iu.test(normalized);
7
18
  };
19
+ const brandFromToken = (token) => normalizeExecutableToken(token);
20
+ const stripNpmScope = (pkg) => pkg.startsWith('@') ? (pkg.split('/').at(-1) ?? pkg) : pkg;
21
+ const tokenizeCommandLine = (line) => line.match(/"[^"]*"|'[^']*'|[^\s'"]+/gu)?.map((token) => token.replace(/^["']|["']$/gu, '')) ?? [];
22
+ /**
23
+ * Walk a token list and resolve the CLI brand of the command that will
24
+ * actually own the terminal, seeing through shell wrappers (skipping their
25
+ * flag tokens), npx-style runners, and `node <entrypoint>` launches.
26
+ */
27
+ const profileFromTokens = (tokens) => {
28
+ let index = 0;
29
+ while (index < tokens.length) {
30
+ const executable = brandFromToken(tokens[index]);
31
+ if (!executable)
32
+ return undefined;
33
+ const direct = PROFILE_BY_BRAND[executable];
34
+ if (direct)
35
+ return direct;
36
+ if (SHELL_WRAPPERS.has(executable)) {
37
+ index += 1;
38
+ while (index < tokens.length && /^[-/]/u.test(tokens[index] ?? ''))
39
+ index += 1;
40
+ continue;
41
+ }
42
+ if (NPX_WRAPPERS.has(executable)) {
43
+ index += 1;
44
+ while (index < tokens.length && tokens[index]?.startsWith('-'))
45
+ index += 1;
46
+ const pkg = tokens[index];
47
+ if (!pkg)
48
+ return undefined;
49
+ const brand = brandFromToken(stripNpmScope(pkg));
50
+ return brand ? PROFILE_BY_BRAND[brand] : undefined;
51
+ }
52
+ if (executable === 'node') {
53
+ return tokens.slice(index + 1).some(isCodexNpmEntrypoint) ? 'codex' : undefined;
54
+ }
55
+ return undefined;
56
+ }
57
+ return undefined;
58
+ };
59
+ const profileFromCommandText = (text) => {
60
+ if (!text?.trim())
61
+ return undefined;
62
+ // Fast paths: the first (possibly quoted) token, and the whole text as one
63
+ // token — the latter covers unquoted Windows paths with spaces such as
64
+ // `C:\Program Files\nodejs\opencode.cmd`.
65
+ for (const candidate of [getStartupCommandExecutable(text), text]) {
66
+ const brand = brandFromToken(candidate ?? undefined);
67
+ const profile = brand ? PROFILE_BY_BRAND[brand] : undefined;
68
+ if (profile)
69
+ return profile;
70
+ }
71
+ return profileFromTokens(tokenizeCommandLine(text));
72
+ };
73
+ const profileFromSpawnArgs = (config) => {
74
+ const executable = brandFromToken(config.command);
75
+ if (!executable || !config.args?.length)
76
+ return undefined;
77
+ if (SHELL_WRAPPERS.has(executable)) {
78
+ // The wrapped command line rides in the shell's final argument.
79
+ return profileFromCommandText(config.args.at(-1));
80
+ }
81
+ if (NPX_WRAPPERS.has(executable)) {
82
+ return profileFromTokens([executable, ...config.args]);
83
+ }
84
+ if (executable === 'node') {
85
+ return config.args.some(isCodexNpmEntrypoint) ? 'codex' : undefined;
86
+ }
87
+ return undefined;
88
+ };
8
89
  export const resolveTerminalInputProfile = (config) => {
9
90
  if (!config)
10
91
  return 'default';
@@ -16,12 +97,10 @@ export const resolveTerminalInputProfile = (config) => {
16
97
  return 'opencode';
17
98
  if (config.sessionIdCapture?.source === 'codex_session_jsonl_dir')
18
99
  return 'codex';
19
- const executable = normalizeExecutableToken(config.interactiveCommand) ?? normalizeExecutableToken(config.command);
20
- if (executable === 'codex')
21
- return 'codex';
22
- if (executable === 'grok')
23
- return 'grok';
24
- if (isCodexNpmEntrypoint(config.args?.[0]))
25
- return 'codex';
26
- return executable === 'opencode' ? 'opencode' : 'default';
100
+ if (config.sessionIdCapture?.source === 'opencode_session_db')
101
+ return 'opencode';
102
+ return (profileFromCommandText(config.interactiveCommand) ??
103
+ profileFromCommandText(config.command) ??
104
+ profileFromSpawnArgs(config) ??
105
+ 'default');
27
106
  };
@@ -0,0 +1,2 @@
1
+ export declare const WORKSPACE_UPLOAD_MAX_BYTES: number;
2
+ export declare const WORKSPACE_UPLOAD_JSON_BODY_LIMIT_BYTES: number;
@@ -0,0 +1,2 @@
1
+ export const WORKSPACE_UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
2
+ export const WORKSPACE_UPLOAD_JSON_BODY_LIMIT_BYTES = 140 * 1024 * 1024;
@@ -12,8 +12,13 @@
12
12
  * unrestricted and defaults to `claude` — i.e. exactly the old behavior, so
13
13
  * upgrading without configuring anything changes nothing.
14
14
  */
15
- /** Canonical CLI set — mirrors the built-in command preset ids. The allowlist is a subset of these. */
16
- export declare const CANONICAL_WORKFLOW_CLIS: readonly ["claude", "codex", "opencode", "gemini", "hermes", "qwen", "agy", "cursor", "grok"];
15
+ /**
16
+ * Canonical workflow-capable CLIs. This is intentionally narrower than every
17
+ * built-in command preset: editor/assistant presets that do not support the
18
+ * Hive worker report loop should not become workflow agent targets merely by
19
+ * existing in settings.
20
+ */
21
+ export declare const CANONICAL_WORKFLOW_CLIS: readonly ["claude", "codex", "opencode", "gemini", "hermes", "qwen", "agy"];
17
22
  export type WorkflowCli = (typeof CANONICAL_WORKFLOW_CLIS)[number];
18
23
  export declare const WORKFLOW_CLI_POLICY_KEY = "workflow.cli-policy";
19
24
  export interface WorkflowCliPolicy {
@@ -12,9 +12,21 @@
12
12
  * unrestricted and defaults to `claude` — i.e. exactly the old behavior, so
13
13
  * upgrading without configuring anything changes nothing.
14
14
  */
15
- import { BUILTIN_COMMAND_PRESET_IDS } from './command-preset-defaults.js';
16
- /** Canonical CLI set — mirrors the built-in command preset ids. The allowlist is a subset of these. */
17
- export const CANONICAL_WORKFLOW_CLIS = BUILTIN_COMMAND_PRESET_IDS;
15
+ /**
16
+ * Canonical workflow-capable CLIs. This is intentionally narrower than every
17
+ * built-in command preset: editor/assistant presets that do not support the
18
+ * Hive worker report loop should not become workflow agent targets merely by
19
+ * existing in settings.
20
+ */
21
+ export const CANONICAL_WORKFLOW_CLIS = [
22
+ 'claude',
23
+ 'codex',
24
+ 'opencode',
25
+ 'gemini',
26
+ 'hermes',
27
+ 'qwen',
28
+ 'agy',
29
+ ];
18
30
  export const WORKFLOW_CLI_POLICY_KEY = 'workflow.cli-policy';
19
31
  export const DEFAULT_WORKFLOW_CLI_POLICY = {
20
32
  default: 'claude',
@@ -45,6 +45,7 @@ export declare const createWorkflowRunStore: (db: Database) => {
45
45
  updateRun: (id: string, input: UpdateRunInput) => void;
46
46
  getRun: (id: string) => WorkflowRunRecord | undefined;
47
47
  listWorkspaceRuns: (workspaceId: string) => WorkflowRunRecord[];
48
+ listChildRuns: (parentRunId: string) => WorkflowRunRecord[];
48
49
  markUnfinishedRunsInterrupted: () => void;
49
50
  };
50
51
  export {};