@tt-a1i/hive 2.1.5 → 2.1.7

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 (41) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/src/server/agent-runtime-contract.d.ts +1 -9
  3. package/dist/src/server/agent-runtime.js +1 -7
  4. package/dist/src/server/agent-startup-instructions.d.ts +1 -0
  5. package/dist/src/server/agent-startup-instructions.js +2 -1
  6. package/dist/src/server/agent-stdin-dispatcher.d.ts +2 -11
  7. package/dist/src/server/agent-stdin-dispatcher.js +14 -12
  8. package/dist/src/server/hive-team-guidance.js +2 -2
  9. package/dist/src/server/post-start-input-writer.js +1 -1
  10. package/dist/src/server/report-outbox-store.d.ts +10 -8
  11. package/dist/src/server/report-outbox-store.js +10 -1
  12. package/dist/src/server/routes-remote.js +46 -14
  13. package/dist/src/server/routes-team.js +1 -1
  14. package/dist/src/server/routes-workspaces.js +3 -6
  15. package/dist/src/server/runtime-store-contract.d.ts +2 -2
  16. package/dist/src/server/runtime-store-helpers.d.ts +4 -0
  17. package/dist/src/server/runtime-store-helpers.js +34 -11
  18. package/dist/src/server/runtime-store-worker-mutations.js +12 -6
  19. package/dist/src/server/runtime-store-workflows.js +7 -1
  20. package/dist/src/server/team-operations.d.ts +12 -5
  21. package/dist/src/server/team-operations.js +217 -158
  22. package/dist/src/shared/remote-bridge-routing.js +3 -1
  23. package/package.json +1 -1
  24. package/web/dist/assets/{AddWorkerDialog-AxzWv2T8.js → AddWorkerDialog-DuUFz6UK.js} +2 -2
  25. package/web/dist/assets/{AddWorkspaceFlow-D-Yh_kk_.js → AddWorkspaceFlow-CFuHJXFe.js} +1 -1
  26. package/web/dist/assets/{FirstRunWizard-nAK4Eprj.js → FirstRunWizard-qA5b90Ru.js} +1 -1
  27. package/web/dist/assets/{MarketplaceDrawer-B8JW-znT.js → MarketplaceDrawer-BdkVJyI0.js} +1 -1
  28. package/web/dist/assets/{TaskGraphDrawer-BQ9Wr41D.js → TaskGraphDrawer-DDhX2p_z.js} +1 -1
  29. package/web/dist/assets/{WhatsNewDialog-D7DwCnTR.js → WhatsNewDialog-Cp_gv_Co.js} +1 -1
  30. package/web/dist/assets/{WorkerModal-tU-XkiFG.js → WorkerModal-BbKQsSSJ.js} +1 -1
  31. package/web/dist/assets/{WorkflowsDrawer-DCa-HN3D.js → WorkflowsDrawer-wk6HSGNl.js} +1 -1
  32. package/web/dist/assets/{WorkspaceMemoryDrawer-4hLlu8BO.js → WorkspaceMemoryDrawer-CSxU_0b1.js} +1 -1
  33. package/web/dist/assets/{WorkspaceTaskDrawer-D3LcLkw_.js → WorkspaceTaskDrawer-Bz3fMad9.js} +1 -1
  34. package/web/dist/assets/index-B3XoRj0C.js +84 -0
  35. package/web/dist/assets/index-CEP_bvOg.css +1 -0
  36. package/web/dist/assets/{search-CcRvD2Q5.js → search-BdM5IXYB.js} +1 -1
  37. package/web/dist/assets/{square-terminal-zBgH6nhI.js → square-terminal-DnmshYpr.js} +1 -1
  38. package/web/dist/index.html +2 -2
  39. package/web/dist/sw.js +1 -1
  40. package/web/dist/assets/index-B8tv0NRK.js +0 -84
  41. package/web/dist/assets/index-CG2TurwT.css +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  All notable user-facing changes will be documented in this file.
4
4
 
5
+ ## 2.1.7 - 2026-06-23
6
+
7
+ Team delivery recovery and remote-access hardening.
8
+
9
+ - Makes worker reports durable when the Orchestrator is offline or restarting,
10
+ so completed work is queued for redelivery instead of being silently lost.
11
+ - Tightens cancel, worker-dismiss, and queued-dispatch recovery paths so stale
12
+ work gets a clear dropped/cancelled outcome instead of leaving agents waiting.
13
+ - Makes user input delivery truthful: if the Orchestrator terminal is offline,
14
+ Hive rejects the input instead of recording a message that was never sent.
15
+ - Standardizes remote-access HTTP responses on snake_case fields and keeps the
16
+ web UI mapping them back into its internal camelCase model.
17
+ - Blocks remote devices from reading or approving desktop-only pairing
18
+ approvals through the tunnel, while preserving normal equal-authority device
19
+ management routes.
20
+
21
+ ## 2.1.6 - 2026-06-21
22
+
23
+ Team UX polish and release visibility.
24
+
25
+ - Adds a global package-update prompt so running Hive can surface a newer npm
26
+ version without relying only on the topbar badge.
27
+ - Makes worker startup handshakes explicit: newly started members now report a
28
+ ready status back to the Orchestrator after receiving Hive's injected startup
29
+ instructions, making failed starts easier to distinguish from idle workers.
30
+ - Removes the Orchestrator pane stop button from the UI so users cannot
31
+ accidentally close the central coordinator from the pane chrome.
32
+ - Reworks the Action Center entry into a dedicated drawer with live workspace
33
+ health, attention, recent activity, and worker selection instead of the old
34
+ compact popover.
35
+
5
36
  ## 2.1.5 - 2026-06-21
6
37
 
7
38
  Ambient memory reliability and background consolidation.
@@ -23,26 +23,18 @@ export interface AgentRuntime {
23
23
  startAgent: (workspace: WorkspaceSummary, agentId: string, input: StartAgentOptions) => Promise<LiveAgentRun>;
24
24
  stopAgentRun: (runId: string) => void;
25
25
  validateAgentToken: AgentTokenRegistry['validate'];
26
- writeReportPrompt: (workspaceId: string, workerName: string, workerId: string, text: string, artifacts: string[], input?: {
27
- requireActiveRun?: boolean;
28
- }) => void;
29
26
  writeStatusPrompt: (workspaceId: string, workerName: string, workerId: string, text: string, artifacts: string[], input?: {
30
27
  requireActiveRun?: boolean;
31
28
  }) => void;
32
29
  writeSendPrompt: (workspaceId: string, workerId: string, dispatchId: string, fromAgentName: string, workerDescription: string, text: string) => Promise<void>;
33
30
  writeCancelPrompt: (workspaceId: string, workerId: string, dispatchId: string, reason: string, input?: {
34
31
  requireActiveRun?: boolean;
35
- }) => void;
32
+ }) => Promise<void>;
36
33
  writeUserInputPrompt: (workspaceId: string, text: string) => void;
37
34
  writeSystemMessageToAgent: (workspaceId: string, agentId: string, text: string) => void;
38
35
  deliverUserInputToOrchestrator: (workspaceId: string, text: string, input?: {
39
36
  requireActiveRun?: boolean;
40
37
  }) => Promise<void>;
41
- /** Awaitable report delivery — rejects if the orchestrator PTY write fails,
42
- * so the caller can persist the report to the redelivery outbox. */
43
- deliverReportToOrchestrator: (workspaceId: string, workerName: string, text: string, artifacts: string[], input?: {
44
- requireActiveRun?: boolean;
45
- }) => Promise<void>;
46
38
  /** Awaitable opaque delivery — used to drain the report outbox so an entry
47
39
  * is marked delivered only after the PTY write resolves. */
48
40
  deliverSystemMessageToAgent: (workspaceId: string, agentId: string, text: string, input?: {
@@ -139,9 +139,6 @@ export const createAgentRuntime = (agentManager, agentRunStore, sessionStore, ge
139
139
  });
140
140
  },
141
141
  validateAgentToken: tokenRegistry.validate,
142
- writeReportPrompt(workspaceId, workerName, _workerId, text, artifacts, input = {}) {
143
- stdinDispatcher.writeReportPrompt(workspaceId, workerName, text, artifacts, input);
144
- },
145
142
  writeStatusPrompt(workspaceId, workerName, _workerId, text, artifacts, input = {}) {
146
143
  stdinDispatcher.writeStatusPrompt(workspaceId, workerName, text, artifacts, input);
147
144
  },
@@ -149,7 +146,7 @@ export const createAgentRuntime = (agentManager, agentRunStore, sessionStore, ge
149
146
  return stdinDispatcher.writeSendPrompt(workspaceId, workerId, dispatchId, fromAgentName, workerDescription, text);
150
147
  },
151
148
  writeCancelPrompt(workspaceId, workerId, dispatchId, reason, input = {}) {
152
- stdinDispatcher.writeCancelPrompt(workspaceId, workerId, dispatchId, reason, input);
149
+ return stdinDispatcher.writeCancelPrompt(workspaceId, workerId, dispatchId, reason, input);
153
150
  },
154
151
  writeUserInputPrompt(workspaceId, text) {
155
152
  stdinDispatcher.writeUserInputPrompt(workspaceId, text);
@@ -160,9 +157,6 @@ export const createAgentRuntime = (agentManager, agentRunStore, sessionStore, ge
160
157
  deliverUserInputToOrchestrator(workspaceId, text, input = {}) {
161
158
  return stdinDispatcher.deliverUserInputToOrchestrator(workspaceId, text, input);
162
159
  },
163
- deliverReportToOrchestrator(workspaceId, workerName, text, artifacts, input = {}) {
164
- return stdinDispatcher.deliverReportToOrchestrator(workspaceId, workerName, text, artifacts, input);
165
- },
166
160
  deliverSystemMessageToAgent(workspaceId, agentId, text, input = {}) {
167
161
  return stdinDispatcher.deliverSystemMessageToAgent(workspaceId, agentId, text, input);
168
162
  },
@@ -8,6 +8,7 @@ export declare const buildAgentLegacyIdentityMarker: ({ agent, workspace, }: {
8
8
  agent: AgentSummary;
9
9
  workspace: WorkspaceSummary;
10
10
  }) => string;
11
+ export declare const WORKER_STARTUP_READY_STATUS = "ready: startup prompt received; Hive CLI channel OK; waiting for dispatch.";
11
12
  export declare const buildAgentStartupInstructions: ({ agent, memoryDigest, workspace, flags, }: {
12
13
  agent: AgentSummary;
13
14
  memoryDigest?: string | null | undefined;
@@ -5,6 +5,7 @@ import { getHiveTeamRules } from './hive-team-guidance.js';
5
5
  import { TASKS_RELATIVE_PATH } from './tasks-file.js';
6
6
  export const buildAgentSessionBindingMarker = ({ agent, workspace, }) => `Hive session binding: workspace_id=${workspace.id}; agent_id=${agent.id}`;
7
7
  export const buildAgentLegacyIdentityMarker = ({ agent, workspace, }) => `You are ${escapeHiveEnvelopeText(agent.name)} (${agent.role}) in workspace ${escapeHiveEnvelopeText(workspace.name)}.`;
8
+ export const WORKER_STARTUP_READY_STATUS = 'ready: startup prompt received; Hive CLI channel OK; waiting for dispatch.';
8
9
  export const buildAgentStartupInstructions = ({ agent, memoryDigest, workspace, flags = FEATURE_FLAGS_ALL_OFF, }) => {
9
10
  const { workflowsEnabled } = flags;
10
11
  const lines = [
@@ -37,7 +38,7 @@ export const buildAgentStartupInstructions = ({ agent, memoryDigest, workspace,
37
38
  lines.push('Available team commands:', '- team status "<finding>" [--artifact <path>] escalate a patrol finding to the Orchestrator', '- team status --stdin [--artifact <path>] same, body read from stdin', '- team recall "<query>" [--limit <n>] [--window <n>] search prior team messages/reports in this workspace', '- team memory show <memory-id> inspect a memory entry and its evidence', '- team memory search "<query>" [--limit <n>] search active workspace memory', '- team --help command syntax only', '', 'You observe only: no dispatches are ever assigned to you, and `team report` is not available to your role.', '', 'Hive sentinel boundaries:', ...getHiveTeamRules(agent, flags));
38
39
  }
39
40
  else {
40
- lines.push('Available team commands:', '- team report "<result>" [--dispatch <id>] [--artifact <path>] report done / failed / blocked', '- team report --stdin [--dispatch <id>] [--artifact <path>] same, body read from stdin (multi-line / quotes / special chars)', '- team status "<state>" [--artifact <path>] progress / standby update at any time; never closes a dispatch', '- team status --stdin [--artifact <path>] same, body read from stdin', '- team recall "<query>" [--limit <n>] [--window <n>] search prior team messages/reports in this workspace', '- team memory show <memory-id> inspect a memory entry and its evidence', '- team memory search "<query>" [--limit <n>] search active workspace memory', '- team memory dream show <dream-run-id> inspect a pending Dream run only when the Orchestrator assigns memory review', '- team --help command syntax only; NOT a way to report', '', 'Syntax notes:', '- The body is the first positional argument; flag order is free: `team report "result" --dispatch X` and `team report --dispatch X "result"` are both valid.', "- Long bodies (multi-line / quotes / shell metacharacters) always go through `--stdin`, piped in via your shell (POSIX: a quoted heredoc `<<'EOF' … EOF` to stop $var / backtick expansion; Windows cmd: `type body.txt |` or `< body.txt`; PowerShell: `Get-Content -Raw -Encoding utf8 body.txt |`). `--stdin` only reads from stdin and relies on no shell syntax of its own.", '- On error the CLI also prints USAGE — fix the arguments against it.', '', 'Hive worker boundaries:', ...getHiveTeamRules(agent, flags));
41
+ lines.push('Available team commands:', '- team report "<result>" [--dispatch <id>] [--artifact <path>] report done / failed / blocked', '- team report --stdin [--dispatch <id>] [--artifact <path>] same, body read from stdin (multi-line / quotes / special chars)', '- team status "<state>" [--artifact <path>] progress / standby update at any time; never closes a dispatch', '- team status --stdin [--artifact <path>] same, body read from stdin', '- team recall "<query>" [--limit <n>] [--window <n>] search prior team messages/reports in this workspace', '- team memory show <memory-id> inspect a memory entry and its evidence', '- team memory search "<query>" [--limit <n>] search active workspace memory', '- team memory dream show <dream-run-id> inspect a pending Dream run only when the Orchestrator assigns memory review', '- team --help command syntax only; NOT a way to report', '', 'Startup handshake:', `- After reading this startup message, immediately run exactly once: \`team status "${WORKER_STARTUP_READY_STATUS}"\`.`, '- This readiness status tells the Orchestrator this member started successfully; it does not close any dispatch.', '- Do not use `team report` for startup readiness. Use `team report` only after you receive an assigned dispatch task.', '', 'Syntax notes:', '- The body is the first positional argument; flag order is free: `team report "result" --dispatch X` and `team report --dispatch X "result"` are both valid.', "- Long bodies (multi-line / quotes / shell metacharacters) always go through `--stdin`, piped in via your shell (POSIX: a quoted heredoc `<<'EOF' … EOF` to stop $var / backtick expansion; Windows cmd: `type body.txt |` or `< body.txt`; PowerShell: `Get-Content -Raw -Encoding utf8 body.txt |`). `--stdin` only reads from stdin and relies on no shell syntax of its own.", '- On error the CLI also prints USAGE — fix the arguments against it.', '', 'Hive worker boundaries:', ...getHiveTeamRules(agent, flags));
41
42
  }
42
43
  lines.push('', '</hive-message>', '');
43
44
  return lines.join('\n');
@@ -16,22 +16,19 @@ interface AgentStdinDispatcherInput {
16
16
  * Optional; omitted → all flags off. */
17
17
  getFlags?: () => FeatureFlags;
18
18
  }
19
- export declare const buildOrchestratorReportPayload: (workerName: string, text: string, artifacts: string[], flags?: FeatureFlags) => string;
19
+ export declare const buildOrchestratorReportPayload: (workerName: string, text: string, artifacts: string[], flags?: FeatureFlags, dispatchId?: string) => string;
20
20
  export declare const buildOrchestratorStatusPayload: (workerName: string, text: string, artifacts: string[], flags?: FeatureFlags) => string;
21
21
  export declare const buildOrchestratorUserInputPayload: (text: string, flags?: FeatureFlags) => string;
22
22
  export declare const buildWorkerDispatchPayload: (fromAgentName: string, workerDescription: string, dispatchId: string, text: string, memoryDigest?: string | null) => string;
23
23
  export declare const buildWorkerCancelPayload: (dispatchId: string, reason: string) => string;
24
24
  export declare const createAgentStdinDispatcher: ({ agentManager, getLaunchConfig, getWorkspaceId, registry, syncRun, memoryInjection, getFlags, }: AgentStdinDispatcherInput) => {
25
- writeReportPrompt(workspaceId: string, workerName: string, text: string, artifacts: string[], input?: {
26
- requireActiveRun?: boolean;
27
- }): void;
28
25
  writeStatusPrompt(workspaceId: string, workerName: string, text: string, artifacts: string[], input?: {
29
26
  requireActiveRun?: boolean;
30
27
  }): void;
31
28
  writeSendPrompt(workspaceId: string, workerId: string, dispatchId: string, fromAgentName: string, workerDescription: string, text: string): Promise<void>;
32
29
  writeCancelPrompt(workspaceId: string, workerId: string, dispatchId: string, reason: string, input?: {
33
30
  requireActiveRun?: boolean;
34
- }): void;
31
+ }): Promise<void>;
35
32
  writeUserInputPrompt(workspaceId: string, text: string): void;
36
33
  deliverUserInputToOrchestrator(workspaceId: string, text: string, input?: {
37
34
  requireActiveRun?: boolean;
@@ -40,12 +37,6 @@ export declare const createAgentStdinDispatcher: ({ agentManager, getLaunchConfi
40
37
  * Used by the workflow runner to notify the triggering orchestrator
41
38
  * when a run finishes (mirrors Claude Code's <task-notification>). */
42
39
  writeSystemMessageToAgent(workspaceId: string, agentId: string, text: string): void;
43
- /** Awaitable report delivery — same payload as writeReportPrompt but the
44
- * promise is returned so the caller can persist the report to the
45
- * redelivery outbox if the orchestrator PTY write rejects. */
46
- deliverReportToOrchestrator(workspaceId: string, workerName: string, text: string, artifacts: string[], input?: {
47
- requireActiveRun?: boolean;
48
- }): Promise<void>;
49
40
  /** Awaitable opaque delivery — used to drain the report outbox so an entry
50
41
  * is marked delivered only after the PTY write actually resolves. */
51
42
  deliverSystemMessageToAgent(workspaceId: string, agentId: string, text: string, input?: {
@@ -4,9 +4,20 @@ import { buildOrchestratorReminderTail, buildWorkerReminderTail } from './hive-t
4
4
  import { PtyInactiveError } from './http-errors.js';
5
5
  import { createPostStartInputWriter } from './post-start-input-writer.js';
6
6
  import { buildDispatchMemoryDigestSafely, logMemoryDigestInjection, rollbackMemoryDigestInjection, } from './team-memory-injection.js';
7
- export const buildOrchestratorReportPayload = (workerName, text, artifacts, flags = FEATURE_FLAGS_ALL_OFF) => {
7
+ export const buildOrchestratorReportPayload = (workerName, text, artifacts, flags = FEATURE_FLAGS_ALL_OFF, dispatchId) => {
8
+ const dispatchAttribute = dispatchId
9
+ ? ` dispatch="${escapeHiveEnvelopeAttribute(dispatchId)}"`
10
+ : '';
8
11
  const lines = [
9
- `<hive-message kind="report" from="@${escapeHiveEnvelopeAttribute(workerName)}">`,
12
+ `<hive-message kind="report" from="@${escapeHiveEnvelopeAttribute(workerName)}"${dispatchAttribute}>`,
13
+ ...(dispatchId
14
+ ? [
15
+ '',
16
+ `dispatch_id: ${escapeHiveEnvelopeText(dispatchId)}`,
17
+ 'If this dispatch_id report was already processed, treat this as a duplicate redelivery and ignore it.',
18
+ '',
19
+ ]
20
+ : []),
10
21
  escapeHiveEnvelopeText(text),
11
22
  ];
12
23
  for (const artifact of artifacts)
@@ -166,9 +177,6 @@ export const createAgentStdinDispatcher = ({ agentManager, getLaunchConfig, getW
166
177
  });
167
178
  };
168
179
  return {
169
- writeReportPrompt(workspaceId, workerName, text, artifacts, input = {}) {
170
- swallowQueuedFailure(writeToActiveAgentRun(workspaceId, `${workspaceId}:orchestrator`, buildOrchestratorReportPayload(workerName, text, artifacts, flags()), input));
171
- },
172
180
  writeStatusPrompt(workspaceId, workerName, text, artifacts, input = {}) {
173
181
  swallowQueuedFailure(writeToActiveAgentRun(workspaceId, `${workspaceId}:orchestrator`, buildOrchestratorStatusPayload(workerName, text, artifacts, flags()), input));
174
182
  },
@@ -206,7 +214,7 @@ export const createAgentStdinDispatcher = ({ agentManager, getLaunchConfig, getW
206
214
  }
207
215
  },
208
216
  writeCancelPrompt(workspaceId, workerId, dispatchId, reason, input = {}) {
209
- swallowQueuedFailure(writeToActiveAgentRun(workspaceId, workerId, buildWorkerCancelPayload(dispatchId, reason), input));
217
+ return writeToActiveAgentRun(workspaceId, workerId, buildWorkerCancelPayload(dispatchId, reason), input);
210
218
  },
211
219
  writeUserInputPrompt(workspaceId, text) {
212
220
  swallowQueuedFailure(writeToActiveAgentRun(workspaceId, `${workspaceId}:orchestrator`, buildOrchestratorUserInputPayload(text, flags())));
@@ -220,12 +228,6 @@ export const createAgentStdinDispatcher = ({ agentManager, getLaunchConfig, getW
220
228
  writeSystemMessageToAgent(workspaceId, agentId, text) {
221
229
  swallowQueuedFailure(writeToActiveAgentRun(workspaceId, agentId, text));
222
230
  },
223
- /** Awaitable report delivery — same payload as writeReportPrompt but the
224
- * promise is returned so the caller can persist the report to the
225
- * redelivery outbox if the orchestrator PTY write rejects. */
226
- deliverReportToOrchestrator(workspaceId, workerName, text, artifacts, input = {}) {
227
- return writeToActiveAgentRun(workspaceId, `${workspaceId}:orchestrator`, buildOrchestratorReportPayload(workerName, text, artifacts, flags()), input);
228
- },
229
231
  /** Awaitable opaque delivery — used to drain the report outbox so an entry
230
232
  * is marked delivered only after the PTY write actually resolves. */
231
233
  deliverSystemMessageToAgent(workspaceId, agentId, text, input = {}) {
@@ -105,7 +105,7 @@ const WORKER_RULES = [
105
105
  'When an assigned task is done, blocked, or has failed, you MUST report to the Orchestrator with `team report`.',
106
106
  'Assume no one is watching your terminal: the user converses with the Orchestrator, not with you. Never stop to wait for terminal input — if you need a decision or are missing information, `team report` the question as blocked and let the Orchestrator decide.',
107
107
  'If you never report, your status stays `working` forever (Hive has no timeout detection) and your work counts as not done.',
108
- '`team status` may be sent at any time (mid-task progress or standby); it NEVER closes a dispatch — only `team report` (or an orchestrator `team cancel`) does.',
108
+ '`team status` may be sent at startup or any time afterward (ready / progress / standby); it NEVER closes a dispatch — only `team report` (or an orchestrator `team cancel`) does.',
109
109
  'Use `team recall "<query>"` when prior team messages or reports may contain useful evidence.',
110
110
  'Use `team memory search "<query>"` to inspect active workspace memory. Include durable findings in `team report`. If the Orchestrator explicitly assigns Dream review, you may run `team memory dream show <dream-run-id>` and propose ops in `team report`; workers cannot apply, add, or forget memory.',
111
111
  '`team --help` only prints command syntax — it is NOT a way to report; its output never reaches the Orchestrator. You still owe a real `team report` / `team status` afterward.',
@@ -341,7 +341,7 @@ export const buildProtocolDoc = (cliPolicy = DEFAULT_WORKFLOW_CLI_POLICY, flags
341
341
  '- `team memory dream show <dream-run-id>` — inspect a pending Dream run only when the orchestrator assigns memory review; propose ops in `team report`, never apply',
342
342
  '- `team report "<result>" --dispatch <id>` — report task outcome',
343
343
  '- `team report --stdin --dispatch <id>` — same, body from stdin (pipe content in via your shell — POSIX heredoc, Windows cmd `type file |`, PowerShell `Get-Content -Raw -Encoding utf8 file |`, or portable stdin redirection `< file`)',
344
- '- `team status "<state>"` — update orchestrator when no dispatch is active',
344
+ '- `team status "<state>"` — send startup readiness, progress, or standby; never closes a dispatch',
345
345
  '',
346
346
  '## `team` CLI — sentinel',
347
347
  '',
@@ -8,7 +8,7 @@ const MIN_SUBMIT_AFTER_PASTE_DELAY_MS = 600;
8
8
  const MAX_SUBMIT_AFTER_PASTE_DELAY_MS = 1500;
9
9
  const PASTE_CHARS_PER_DELAY_MS = 4;
10
10
  const PASTE_ACK_CHECK_INTERVAL_MS = 50;
11
- const PASTE_ACK_SETTLE_DELAY_MS = 100;
11
+ const PASTE_ACK_SETTLE_DELAY_MS = 250;
12
12
  const CODEX_PASTE_ACK_SETTLE_DELAY_MS = 250;
13
13
  const PASTE_ACK_TIMEOUT_MS = 3000;
14
14
  const CLAUDE_HIVE_PASTE_ACK_TIMEOUT_MS = 10000;
@@ -1,15 +1,16 @@
1
1
  import type { Database } from 'better-sqlite3';
2
2
  /**
3
- * Durable redelivery queue for worker reports the orchestrator could not
4
- * receive. `team report` persists the dispatch as reported and forwards the
5
- * report into the orchestrator's stdin best-effort; if that write fails (the
6
- * PTY exited / is mid-restart, or the orchestrator was down at report time)
7
- * the report would otherwise be lost — the ledger says "reported" yet the
8
- * orchestrator waits forever, indistinguishable from a hang.
3
+ * Durable redelivery queue for protocol messages a live agent could not
4
+ * receive. The primary producer is `team report`: it persists the dispatch as
5
+ * reported and forwards the report into the orchestrator's stdin; if that write
6
+ * fails (the PTY exited / is mid-restart, or the orchestrator was down at report
7
+ * time) the report would otherwise be lost — the ledger says "reported" yet the
8
+ * orchestrator waits forever, indistinguishable from a hang. Dispatch-drop
9
+ * notifications use the same queue for their issuer.
9
10
  *
10
11
  * Entries are keyed by dispatch id (UNIQUE) so a report enqueues at most once,
11
- * and drain marks them delivered only after the orchestrator PTY write
12
- * actually resolves, so a failed redelivery stays pending for the next drain.
12
+ * and drain marks them delivered only after the target PTY write actually
13
+ * resolves, so a failed redelivery stays pending for the next drain.
13
14
  */
14
15
  export interface ReportOutboxEntry {
15
16
  id: number;
@@ -27,6 +28,7 @@ interface EnqueueInput {
27
28
  payload: string;
28
29
  }
29
30
  export declare const createReportOutboxStore: (db: Database) => {
31
+ deletePendingForDispatch: (dispatchId: string) => void;
30
32
  enqueue: (input: EnqueueInput) => void;
31
33
  listPending: (workspaceId: string, targetAgentId: string) => ReportOutboxEntry[];
32
34
  markDelivered: (id: number) => void;
@@ -25,9 +25,18 @@ export const createReportOutboxStore = (db) => {
25
25
  const markDelivered = (id) => {
26
26
  db.prepare(`UPDATE report_outbox SET delivered_at = ? WHERE id = ? AND delivered_at IS NULL`).run(Date.now(), id);
27
27
  };
28
+ const deletePendingForDispatch = (dispatchId) => {
29
+ db.prepare('DELETE FROM report_outbox WHERE dispatch_id = ? AND delivered_at IS NULL').run(dispatchId);
30
+ };
28
31
  const pendingCount = (workspaceId, targetAgentId) => db
29
32
  .prepare(`SELECT COUNT(*) AS n FROM report_outbox
30
33
  WHERE workspace_id = ? AND target_agent_id = ? AND delivered_at IS NULL`)
31
34
  .get(workspaceId, targetAgentId).n;
32
- return { enqueue, listPending, markDelivered, pendingCount };
35
+ return {
36
+ deletePendingForDispatch,
37
+ enqueue,
38
+ listPending,
39
+ markDelivered,
40
+ pendingCount,
41
+ };
33
42
  };
@@ -1,5 +1,6 @@
1
1
  import { ForbiddenError } from './http-errors.js';
2
2
  import { REMOTE_DAEMON_TOKEN_KEY, REMOTE_ENABLED_KEY, REMOTE_GATEWAY_URL_KEY, } from './remote-config-keys.js';
3
+ import { HIVE_REMOTE_DEVICE_HEADER } from './remote-loopback-auth.js';
3
4
  import { getRequiredParam, readJsonBody, route, sendJson } from './route-helpers.js';
4
5
  import { requireUiTokenFromRequest } from './ui-auth-helpers.js';
5
6
  // Remote-access device-management + pairing routes (M4). Two gate classes:
@@ -23,8 +24,11 @@ const gateUi = ({ request, store }) => {
23
24
  // check, so even the short-circuit can't admit a tunnel request that slipped a cookie too.
24
25
  const gateLocalDesktopOnly = ({ request, store }) => {
25
26
  if (store.authorizeRemoteTunnelRequest(request)) {
27
+ const rawDeviceId = request.headers[HIVE_REMOTE_DEVICE_HEADER];
26
28
  store.getRemoteAuditStore().enqueue({
27
29
  action: 'reject',
30
+ deviceId: typeof rawDeviceId === 'string' ? rawDeviceId : null,
31
+ endpoint: request.url ?? null,
28
32
  result: 'rejected',
29
33
  rejectReason: 'pairing_confirm_forbidden',
30
34
  });
@@ -38,8 +42,8 @@ const remoteStatus = (store) => {
38
42
  const status = store.getRemoteTunnelStatus();
39
43
  return {
40
44
  enabled: store.settings.getAppState(REMOTE_ENABLED_KEY)?.value === 'true',
41
- loggedIn: isLoggedIn(store),
42
- gatewayUrl: store.settings.getAppState(REMOTE_GATEWAY_URL_KEY)?.value ?? null,
45
+ logged_in: isLoggedIn(store),
46
+ gateway_url: store.settings.getAppState(REMOTE_GATEWAY_URL_KEY)?.value ?? null,
43
47
  connected: status === 'online',
44
48
  // The FULL tunnel state, not just online/offline — the desktop dot was stuck looking "connected or
45
49
  // not" and couldn't show connecting / reconnecting / revoked / logged-out. (connecting+reconnecting
@@ -47,15 +51,41 @@ const remoteStatus = (store) => {
47
51
  connection: status,
48
52
  };
49
53
  };
50
- // RemoteDeviceRecord is already camelCase + metadata-only (no key material). Identity-mapped so the
51
- // response shape stays pinned to the store's invariant-7 projection.
54
+ // HTTP responses stay snake_case even though the store uses camelCase records.
55
+ const toPairingTicketView = (ticket) => ({
56
+ pairing_id: ticket.pairingId,
57
+ qr: ticket.qr,
58
+ code: ticket.code,
59
+ expires_at: ticket.expiresAt,
60
+ });
61
+ const toPendingPairingView = (pending) => ({
62
+ pairing_id: pending.pairingId,
63
+ device_name: pending.deviceName,
64
+ sas: pending.sas,
65
+ expires_at: pending.expiresAt,
66
+ });
52
67
  const toDeviceView = (record) => ({
53
68
  id: record.id,
54
69
  name: record.name,
55
- createdAt: record.createdAt,
56
- lastActive: record.lastActive,
57
- revokedAt: record.revokedAt,
70
+ created_at: record.createdAt,
71
+ last_active: record.lastActive,
72
+ revoked_at: record.revokedAt,
73
+ });
74
+ const toAuditView = (record) => ({
75
+ id: record.id,
76
+ device_id: record.deviceId,
77
+ ts: record.ts,
78
+ workspace_id: record.workspaceId,
79
+ action: record.action,
80
+ endpoint: record.endpoint,
81
+ result: record.result,
82
+ reject_reason: record.rejectReason,
83
+ byte_count: record.byteCount,
84
+ preview: record.preview,
58
85
  });
86
+ const getSearchParams = (url) => new URL(url ?? '', 'http://x').searchParams;
87
+ const hasCompatFlag = (params, snakeName, camelName) => params.has(snakeName) || params.has(camelName);
88
+ const getCompatParam = (params, snakeName, camelName) => params.get(snakeName) ?? params.get(camelName);
59
89
  export const remoteRoutes = [
60
90
  route('GET', '/api/remote/status', (ctx) => {
61
91
  gateUi(ctx);
@@ -83,7 +113,7 @@ export const remoteRoutes = [
83
113
  }
84
114
  try {
85
115
  const ticket = ctx.store.getRemotePairing().beginPairing();
86
- sendJson(ctx.response, 200, ticket);
116
+ sendJson(ctx.response, 200, toPairingTicketView(ticket));
87
117
  }
88
118
  catch {
89
119
  // beginPairing throws if gateway/daemon id vanished between the status read and here.
@@ -94,7 +124,7 @@ export const remoteRoutes = [
94
124
  // HARDEN minor: desktop-only. The confirm dialog backs this; a phone has no use for another
95
125
  // device's in-flight SAS, and exposing it would needlessly widen the secret surface (invariant 7).
96
126
  gateLocalDesktopOnly(ctx);
97
- sendJson(ctx.response, 200, ctx.store.getRemotePairing().listPending());
127
+ sendJson(ctx.response, 200, ctx.store.getRemotePairing().listPending().map(toPendingPairingView));
98
128
  }),
99
129
  route('POST', '/api/remote/pairings/:pairingId/confirm', async (ctx) => {
100
130
  const { params, request, response, store } = ctx;
@@ -139,7 +169,8 @@ export const remoteRoutes = [
139
169
  // ── equal-authority device management ──────────────────────────────────────────────────────────
140
170
  route('GET', '/api/remote/devices', (ctx) => {
141
171
  gateUi(ctx);
142
- const includeRevoked = new URL(ctx.request.url ?? '', 'http://x').searchParams.has('includeRevoked');
172
+ const params = getSearchParams(ctx.request.url);
173
+ const includeRevoked = hasCompatFlag(params, 'include_revoked', 'includeRevoked');
143
174
  sendJson(ctx.response, 200, ctx.store.getRemoteDeviceStore().list(includeRevoked).map(toDeviceView));
144
175
  }),
145
176
  route('POST', '/api/remote/devices/:deviceId/revoke', (ctx) => {
@@ -156,11 +187,12 @@ export const remoteRoutes = [
156
187
  }),
157
188
  route('GET', '/api/remote/audit', (ctx) => {
158
189
  gateUi(ctx);
159
- const url = new URL(ctx.request.url ?? '', 'http://x');
160
- const rawLimit = Number(url.searchParams.get('limit'));
190
+ const params = getSearchParams(ctx.request.url);
191
+ const rawLimit = Number(params.get('limit'));
161
192
  const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 500) : 100;
162
- const deviceId = url.searchParams.get('deviceId');
193
+ const deviceId = getCompatParam(params, 'device_id', 'deviceId');
163
194
  const audit = ctx.store.getRemoteAuditStore();
164
- sendJson(ctx.response, 200, deviceId ? audit.listForDevice(deviceId, limit) : audit.list(limit));
195
+ const records = deviceId ? audit.listForDevice(deviceId, limit) : audit.list(limit);
196
+ sendJson(ctx.response, 200, records.map(toAuditView));
165
197
  }),
166
198
  ];
@@ -306,7 +306,7 @@ export const teamRoutes = [
306
306
  workspaceId: projectId,
307
307
  });
308
308
  requireCommandForRole(agent, 'cancel');
309
- const result = store.cancelTask(projectId, dispatchId, { fromAgentId, reason });
309
+ const result = await store.cancelTask(projectId, dispatchId, { fromAgentId, reason });
310
310
  sendJson(response, 202, {
311
311
  dispatch_id: result.dispatch?.id ?? null,
312
312
  forward_error: result.forwardError,
@@ -109,11 +109,9 @@ export const workspaceRoutes = [
109
109
  workspaceId,
110
110
  });
111
111
  requireCommandForRole(agent, 'list');
112
- // The orchestrator polling `team list` is its natural post-restart wakeup:
113
- // flush any reports a prior outage stranded back into its stdin now that
114
- // it is reachable again. Cheap (indexed, usually empty) and a safe no-op
115
- // for non-orchestrator callers.
116
- store.drainReportOutbox(workspaceId);
112
+ // Polling `team list` is a natural post-restart wakeup: flush any durable
113
+ // notices stranded for the caller now that its PTY is reachable again.
114
+ store.drainReportOutbox(workspaceId, agent.id);
117
115
  sendJson(response, 200, serializeTeamListWithOpenDispatches(store, workspaceId));
118
116
  }),
119
117
  route('POST', '/api/workspaces/:workspaceId/workers', async ({ params, request, response, store }) => {
@@ -198,7 +196,6 @@ export const workspaceRoutes = [
198
196
  }
199
197
  const orchestratorId = getOrchestratorId(workspaceId);
200
198
  if (!store.getActiveRunByAgentId(workspaceId, orchestratorId)) {
201
- store.recordUserInput(workspaceId, orchestratorId, body.text);
202
199
  throw new PtyInactiveError(`No active run for agent: ${orchestratorId}`);
203
200
  }
204
201
  await store.deliverUserInput(workspaceId, orchestratorId, body.text);
@@ -44,9 +44,9 @@ export interface RuntimeStore {
44
44
  reportTask: (workspaceId: string, workerId: string, input?: ReportTaskInput) => ReportTaskResult;
45
45
  /** Flush any reports stranded by a prior orchestrator outage. Safe no-op
46
46
  * when the orchestrator is down or the outbox is empty. */
47
- drainReportOutbox: (workspaceId: string) => void;
47
+ drainReportOutbox: (workspaceId: string, targetAgentId?: string) => void;
48
48
  statusTask: (workspaceId: string, workerId: string, input?: StatusTaskInput) => ReportTaskResult;
49
- cancelTask: (workspaceId: string, dispatchId: string, input: CancelTaskInput) => ReportTaskResult;
49
+ cancelTask: (workspaceId: string, dispatchId: string, input: CancelTaskInput) => Promise<ReportTaskResult>;
50
50
  listDispatches: (workspaceId: string, options?: ListDispatchesOptions) => DispatchRecord[];
51
51
  listOpenDispatches: (workspaceId: string) => DispatchRecord[];
52
52
  listRecentDispatches: (workspaceId: string, limit?: number) => DispatchRecord[];
@@ -10,6 +10,7 @@ import { type RemoteAuditStore } from './remote-audit-store.js';
10
10
  import type { DeviceSessionProvider } from './remote-device-session.js';
11
11
  import { type RemoteDeviceStore } from './remote-device-store.js';
12
12
  import { type RemotePairing } from './remote-pairing.js';
13
+ import { createReportOutboxStore } from './report-outbox-store.js';
13
14
  import { openRuntimeDatabase } from './runtime-database.js';
14
15
  import { createSentinelHeartbeatService } from './sentinel-heartbeat.js';
15
16
  import { createSettingsStore } from './settings-store.js';
@@ -39,6 +40,8 @@ export interface RuntimeStoreServices {
39
40
  agentRuntime: ReturnType<typeof createAgentRuntime>;
40
41
  db: ReturnType<typeof openRuntimeDatabase>;
41
42
  dispatchLedgerStore: ReturnType<typeof createDispatchLedgerStore>;
43
+ isRuntimeClosing: () => boolean;
44
+ markRuntimeClosing: () => void;
42
45
  messageLogStore: ReturnType<typeof createMessageLogStore>;
43
46
  remoteAuditStore: RemoteAuditStore;
44
47
  remoteDeviceSessions: DeviceSessionProvider;
@@ -53,6 +56,7 @@ export interface RuntimeStoreServices {
53
56
  teamMemoryDreamScheduler: ReturnType<typeof createTeamMemoryDreamScheduler>;
54
57
  sentinelHeartbeat: ReturnType<typeof createSentinelHeartbeatService>;
55
58
  protocolEventStats: ReturnType<typeof createProtocolEventStats>;
59
+ reportOutbox: ReturnType<typeof createReportOutboxStore>;
56
60
  teamMemoryDreamStore: ReturnType<typeof createTeamMemoryDreamStore>;
57
61
  teamMemoryDiagnostics: ReturnType<typeof createTeamMemoryDiagnostics>;
58
62
  teamMemoryStore: ReturnType<typeof createTeamMemoryStore>;
@@ -76,6 +76,7 @@ const createWorkspaceUploadStorage = (dataDir) => {
76
76
  };
77
77
  export const createRuntimeStoreServices = (options = {}) => {
78
78
  const db = openRuntimeDatabase(options.dataDir);
79
+ let closing = false;
79
80
  const uploadStorage = createWorkspaceUploadStorage(options.dataDir);
80
81
  const messageLogStore = createMessageLogStore(db);
81
82
  const dispatchLedgerStore = createDispatchLedgerStore(db);
@@ -239,18 +240,24 @@ export const createRuntimeStoreServices = (options = {}) => {
239
240
  // agentRuntime is assigned.
240
241
  function removeWorkerCompletely(workspaceId, workerId) {
241
242
  const activeRun = agentRuntime.getActiveRunByAgentId(workspaceId, workerId);
242
- // Open dispatch rows are about to be hard-deleted — tell their issuers
243
- // first, or parked/in-flight work vanishes silently (review finding).
244
- try {
245
- teamOps.notifyIssuersOfDroppedDispatches(workspaceId, workerId, 'the worker was dismissed');
246
- }
247
- catch (error) {
248
- console.error('[hive] swallowed:removeWorker.notifyDropped', error);
249
- }
243
+ const droppedNoticeTargets = new Set();
250
244
  db.transaction(() => {
245
+ for (const dispatch of dispatchLedgerStore.listWorkspaceDispatches(workspaceId)) {
246
+ if (dispatch.toAgentId === workerId && dispatch.workflowRunId === null) {
247
+ reportOutbox.deletePendingForDispatch(dispatch.id);
248
+ }
249
+ }
250
+ // Open dispatch rows are about to be hard-deleted — queue issuer notices
251
+ // in the same transaction or the delete must not commit.
252
+ for (const targetAgentId of teamOps.notifyIssuersOfDroppedDispatches(workspaceId, workerId, 'the worker was dismissed')) {
253
+ droppedNoticeTargets.add(targetAgentId);
254
+ }
251
255
  dispatchLedgerStore.deleteWorkerDispatches(workspaceId, workerId);
252
256
  workspaceStore.deleteWorker(workspaceId, workerId);
253
257
  })();
258
+ for (const targetAgentId of droppedNoticeTargets) {
259
+ teamOps.drainReportOutbox(workspaceId, targetAgentId);
260
+ }
254
261
  agentRuntime.deleteAgentLaunchConfig(workspaceId, workerId);
255
262
  if (activeRun)
256
263
  agentRuntime.stopAgentRun(activeRun.runId);
@@ -283,6 +290,8 @@ export const createRuntimeStoreServices = (options = {}) => {
283
290
  reparkClaimedDispatch: dispatchLedgerStore.reparkClaimedDispatch,
284
291
  reportOutbox,
285
292
  notifyWebhook: webhookNotifier.notify,
293
+ runDataMutation: (mutation) => db.transaction(mutation)(),
294
+ isRuntimeClosing: () => closing,
286
295
  workflowDispatchAwaiter,
287
296
  workspaceStore,
288
297
  dismissEphemeralWorker: (workspaceId, workerId) => removeWorkerCompletely(workspaceId, workerId),
@@ -296,6 +305,10 @@ export const createRuntimeStoreServices = (options = {}) => {
296
305
  agentRuntime,
297
306
  db,
298
307
  dispatchLedgerStore,
308
+ isRuntimeClosing: () => closing,
309
+ markRuntimeClosing: () => {
310
+ closing = true;
311
+ },
299
312
  messageLogStore,
300
313
  remoteAuditStore,
301
314
  remoteDeviceSessions,
@@ -310,6 +323,7 @@ export const createRuntimeStoreServices = (options = {}) => {
310
323
  teamMemoryDreamScheduler,
311
324
  sentinelHeartbeat,
312
325
  protocolEventStats,
326
+ reportOutbox,
313
327
  teamMemoryDreamStore,
314
328
  teamMemoryDiagnostics,
315
329
  teamMemoryExport,
@@ -345,7 +359,15 @@ export const createRuntimeStoreLifecycle = ({ agentManager, services, }) => {
345
359
  // for orchestrator/workflow pseudo-agents, which never hold queued
346
360
  // worker dispatches addressed to themselves). Wait for post-start
347
361
  // guidance first so startup instructions never race the first task.
348
- const replayQueuedDispatches = () => {
362
+ const deliverPostStartBacklog = () => {
363
+ if (services.isRuntimeClosing())
364
+ return;
365
+ try {
366
+ services.teamOps.drainReportOutbox(workspaceId, agentId);
367
+ }
368
+ catch (error) {
369
+ console.error('[hive] swallowed:startAgent.drainOutbox', error);
370
+ }
349
371
  try {
350
372
  services.teamOps.replayQueuedDispatches(workspaceId, agentId, {
351
373
  createdBeforeMs: run.startedAt,
@@ -356,12 +378,12 @@ export const createRuntimeStoreLifecycle = ({ agentManager, services, }) => {
356
378
  }
357
379
  };
358
380
  if (run.postStartInputReady) {
359
- void run.postStartInputReady.then(replayQueuedDispatches).catch((error) => {
381
+ void run.postStartInputReady.then(deliverPostStartBacklog).catch((error) => {
360
382
  console.error('[hive] swallowed:startAgent.replayAfterPostStart', error);
361
383
  });
362
384
  }
363
385
  else {
364
- replayQueuedDispatches();
386
+ deliverPostStartBacklog();
365
387
  }
366
388
  }
367
389
  return run;
@@ -406,6 +428,7 @@ export const createRuntimeStoreLifecycle = ({ agentManager, services, }) => {
406
428
  };
407
429
  return {
408
430
  close: async () => {
431
+ services.markRuntimeClosing();
409
432
  try {
410
433
  // Fail in-flight workflow awaiters BEFORE their workers vanish, so the
411
434
  // runner's `await agent(...)` rejects with a clear shutdown error