@tt-a1i/hive 1.4.4 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.en.md +21 -0
  3. package/README.md +16 -0
  4. package/assets/qq-group.jpg +0 -0
  5. package/dist/bin/team.cmd +1 -0
  6. package/dist/src/cli/hive-update.d.ts +45 -17
  7. package/dist/src/cli/hive-update.js +63 -25
  8. package/dist/src/cli/hive.d.ts +25 -0
  9. package/dist/src/cli/hive.js +41 -3
  10. package/dist/src/cli/team.d.ts +1 -0
  11. package/dist/src/cli/team.js +216 -3
  12. package/dist/src/server/agent-command-resolver.js +3 -19
  13. package/dist/src/server/agent-manager-support.d.ts +2 -2
  14. package/dist/src/server/agent-manager-support.js +98 -24
  15. package/dist/src/server/agent-run-starter.d.ts +6 -1
  16. package/dist/src/server/agent-run-starter.js +9 -2
  17. package/dist/src/server/agent-run-store.d.ts +1 -1
  18. package/dist/src/server/agent-runtime-close.d.ts +1 -0
  19. package/dist/src/server/agent-runtime-close.js +25 -1
  20. package/dist/src/server/agent-runtime-contract.d.ts +12 -1
  21. package/dist/src/server/agent-runtime-stop-run.d.ts +1 -1
  22. package/dist/src/server/agent-runtime-stop-run.js +4 -1
  23. package/dist/src/server/agent-runtime.d.ts +2 -1
  24. package/dist/src/server/agent-runtime.js +14 -3
  25. package/dist/src/server/agent-startup-instructions.d.ts +7 -1
  26. package/dist/src/server/agent-startup-instructions.js +17 -9
  27. package/dist/src/server/agent-stdin-dispatcher.d.ts +25 -5
  28. package/dist/src/server/agent-stdin-dispatcher.js +141 -40
  29. package/dist/src/server/cron-util.d.ts +7 -0
  30. package/dist/src/server/cron-util.js +19 -0
  31. package/dist/src/server/dispatch-ledger-store.d.ts +22 -0
  32. package/dist/src/server/dispatch-ledger-store.js +51 -3
  33. package/dist/src/server/env-sync-message.js +9 -9
  34. package/dist/src/server/feature-flags.d.ts +42 -0
  35. package/dist/src/server/feature-flags.js +24 -0
  36. package/dist/src/server/fs-pick-folder.js +4 -0
  37. package/dist/src/server/fs-sandbox.js +36 -7
  38. package/dist/src/server/hive-team-guidance.d.ts +12 -6
  39. package/dist/src/server/hive-team-guidance.js +253 -71
  40. package/dist/src/server/live-run-registry.d.ts +1 -0
  41. package/dist/src/server/live-run-registry.js +1 -1
  42. package/dist/src/server/open-target-commands.js +5 -6
  43. package/dist/src/server/orchestrator-autostart.d.ts +12 -0
  44. package/dist/src/server/orchestrator-autostart.js +15 -13
  45. package/dist/src/server/path-canonicalization.d.ts +3 -0
  46. package/dist/src/server/path-canonicalization.js +29 -0
  47. package/dist/src/server/platform-path.d.ts +3 -0
  48. package/dist/src/server/platform-path.js +13 -0
  49. package/dist/src/server/post-start-input-writer.d.ts +1 -1
  50. package/dist/src/server/post-start-input-writer.js +110 -13
  51. package/dist/src/server/preset-launch-support.d.ts +1 -1
  52. package/dist/src/server/preset-launch-support.js +33 -2
  53. package/dist/src/server/recovery-summary.d.ts +5 -1
  54. package/dist/src/server/recovery-summary.js +18 -17
  55. package/dist/src/server/report-outbox-store.d.ts +36 -0
  56. package/dist/src/server/report-outbox-store.js +33 -0
  57. package/dist/src/server/restart-policy-support.d.ts +5 -1
  58. package/dist/src/server/restart-policy-support.js +9 -1
  59. package/dist/src/server/restart-policy.d.ts +6 -2
  60. package/dist/src/server/restart-policy.js +51 -31
  61. package/dist/src/server/role-template-store.d.ts +1 -0
  62. package/dist/src/server/role-template-store.js +11 -1
  63. package/dist/src/server/route-types.d.ts +43 -0
  64. package/dist/src/server/routes-runtime.js +2 -1
  65. package/dist/src/server/routes-settings.js +76 -0
  66. package/dist/src/server/routes-tasks.js +23 -0
  67. package/dist/src/server/routes-team.js +211 -1
  68. package/dist/src/server/routes-workflow-schedules.d.ts +2 -0
  69. package/dist/src/server/routes-workflow-schedules.js +58 -0
  70. package/dist/src/server/routes-workflows.d.ts +2 -0
  71. package/dist/src/server/routes-workflows.js +83 -0
  72. package/dist/src/server/routes-workspaces.js +5 -0
  73. package/dist/src/server/routes.js +4 -0
  74. package/dist/src/server/runtime-restart-policy.d.ts +3 -1
  75. package/dist/src/server/runtime-restart-policy.js +2 -1
  76. package/dist/src/server/runtime-store-contract.d.ts +125 -0
  77. package/dist/src/server/runtime-store-contract.js +1 -0
  78. package/dist/src/server/runtime-store-helpers.d.ts +11 -0
  79. package/dist/src/server/runtime-store-helpers.js +106 -2
  80. package/dist/src/server/runtime-store-workflows.d.ts +6 -0
  81. package/dist/src/server/runtime-store-workflows.js +108 -0
  82. package/dist/src/server/runtime-store.d.ts +3 -72
  83. package/dist/src/server/runtime-store.js +71 -4
  84. package/dist/src/server/session-capture-codex.d.ts +3 -3
  85. package/dist/src/server/session-capture-codex.js +9 -7
  86. package/dist/src/server/session-capture-gemini.d.ts +1 -1
  87. package/dist/src/server/session-capture-gemini.js +6 -3
  88. package/dist/src/server/settings-store.d.ts +3 -0
  89. package/dist/src/server/settings-store.js +1 -0
  90. package/dist/src/server/sqlite-schema-v19.d.ts +2 -0
  91. package/dist/src/server/sqlite-schema-v19.js +17 -0
  92. package/dist/src/server/sqlite-schema-v20.d.ts +2 -0
  93. package/dist/src/server/sqlite-schema-v20.js +20 -0
  94. package/dist/src/server/sqlite-schema-v21.d.ts +2 -0
  95. package/dist/src/server/sqlite-schema-v21.js +20 -0
  96. package/dist/src/server/sqlite-schema.d.ts +1 -1
  97. package/dist/src/server/sqlite-schema.js +110 -1
  98. package/dist/src/server/system-message.d.ts +7 -0
  99. package/dist/src/server/system-message.js +8 -1
  100. package/dist/src/server/task-deps.d.ts +32 -0
  101. package/dist/src/server/task-deps.js +40 -0
  102. package/dist/src/server/tasks-file-watcher.d.ts +12 -1
  103. package/dist/src/server/tasks-file-watcher.js +128 -23
  104. package/dist/src/server/tasks-file.d.ts +3 -1
  105. package/dist/src/server/tasks-file.js +33 -9
  106. package/dist/src/server/tasks-websocket-server.js +13 -14
  107. package/dist/src/server/team-authz.d.ts +1 -1
  108. package/dist/src/server/team-authz.js +10 -1
  109. package/dist/src/server/team-autostaff.d.ts +16 -0
  110. package/dist/src/server/team-autostaff.js +16 -0
  111. package/dist/src/server/team-list-serializer.d.ts +1 -1
  112. package/dist/src/server/team-list-serializer.js +3 -1
  113. package/dist/src/server/team-operations.d.ts +21 -1
  114. package/dist/src/server/team-operations.js +183 -16
  115. package/dist/src/server/terminal-protocol.js +9 -3
  116. package/dist/src/server/terminal-stream-hub.js +16 -10
  117. package/dist/src/server/terminal-ws-server.js +10 -8
  118. package/dist/src/server/webhook-notifier.d.ts +34 -0
  119. package/dist/src/server/webhook-notifier.js +47 -0
  120. package/dist/src/server/websocket-upgrade-safety.d.ts +10 -0
  121. package/dist/src/server/websocket-upgrade-safety.js +35 -0
  122. package/dist/src/server/windows-command-line.d.ts +3 -0
  123. package/dist/src/server/windows-command-line.js +9 -0
  124. package/dist/src/server/windows-filename.d.ts +2 -0
  125. package/dist/src/server/windows-filename.js +33 -0
  126. package/dist/src/server/workflow-cli-policy.d.ts +60 -0
  127. package/dist/src/server/workflow-cli-policy.js +110 -0
  128. package/dist/src/server/workflow-dispatch-awaiter.d.ts +12 -0
  129. package/dist/src/server/workflow-dispatch-awaiter.js +80 -0
  130. package/dist/src/server/workflow-feature.d.ts +15 -0
  131. package/dist/src/server/workflow-feature.js +15 -0
  132. package/dist/src/server/workflow-http-serializers.d.ts +64 -0
  133. package/dist/src/server/workflow-http-serializers.js +58 -0
  134. package/dist/src/server/workflow-output-schema.d.ts +18 -0
  135. package/dist/src/server/workflow-output-schema.js +41 -0
  136. package/dist/src/server/workflow-run-log-store.d.ts +19 -0
  137. package/dist/src/server/workflow-run-log-store.js +45 -0
  138. package/dist/src/server/workflow-run-store.d.ts +50 -0
  139. package/dist/src/server/workflow-run-store.js +103 -0
  140. package/dist/src/server/workflow-runner.d.ts +147 -0
  141. package/dist/src/server/workflow-runner.js +411 -0
  142. package/dist/src/server/workflow-schedule-create.d.ts +14 -0
  143. package/dist/src/server/workflow-schedule-create.js +41 -0
  144. package/dist/src/server/workflow-schedule-store.d.ts +43 -0
  145. package/dist/src/server/workflow-schedule-store.js +112 -0
  146. package/dist/src/server/workflow-scheduler.d.ts +36 -0
  147. package/dist/src/server/workflow-scheduler.js +97 -0
  148. package/dist/src/server/workflow-script-loader.d.ts +34 -0
  149. package/dist/src/server/workflow-script-loader.js +106 -0
  150. package/dist/src/server/workspace-path-validation.js +16 -4
  151. package/dist/src/server/workspace-shell-runtime.d.ts +5 -0
  152. package/dist/src/server/workspace-shell-runtime.js +24 -2
  153. package/dist/src/server/workspace-store-contract.d.ts +4 -1
  154. package/dist/src/server/workspace-store-hydration.js +23 -7
  155. package/dist/src/server/workspace-store-mutations.js +2 -5
  156. package/dist/src/server/workspace-store-support.d.ts +4 -0
  157. package/dist/src/server/workspace-store-support.js +13 -1
  158. package/dist/src/server/workspace-store.js +38 -4
  159. package/dist/src/shared/types.d.ts +16 -1
  160. package/package.json +4 -2
  161. package/web/dist/assets/{AddWorkerDialog-DeZhTQLi.js → AddWorkerDialog-CGbaxu0T.js} +2 -2
  162. package/web/dist/assets/AddWorkspaceDialog-CNgExu6b.js +1 -0
  163. package/web/dist/assets/{FirstRunWizard-B5wLcat5.js → FirstRunWizard-DxGApUNc.js} +1 -1
  164. package/web/dist/assets/{MarketplaceDrawer-BC0eBOEW.js → MarketplaceDrawer-Bk6cpukn.js} +1 -1
  165. package/web/dist/assets/WhatsNewDialog-CSGzk-2U.js +1 -0
  166. package/web/dist/assets/WorkerModal-i2F3n3nZ.js +1 -0
  167. package/web/dist/assets/WorkspaceTaskDrawer-C_Ta_K13.js +1 -0
  168. package/web/dist/assets/WorkspaceTerminalPanels-VdDxtrQF.js +1 -0
  169. package/web/dist/assets/index-5zh61jMg.css +1 -0
  170. package/web/dist/assets/index-CAgGM6nb.js +75 -0
  171. package/web/dist/assets/path-join-7MR1s7b1.js +1 -0
  172. package/web/dist/index.html +2 -2
  173. package/web/dist/sw.js +1 -1
  174. package/web/dist/assets/AddWorkspaceDialog-DDpXNEKf.js +0 -1
  175. package/web/dist/assets/WorkerModal-BwMHq-Bi.js +0 -1
  176. package/web/dist/assets/WorkspaceTaskDrawer-CxvT4nqs.js +0 -1
  177. package/web/dist/assets/WorkspaceTerminalPanels-CvibsPSd.js +0 -1
  178. package/web/dist/assets/index-BEsTmfrO.css +0 -1
  179. package/web/dist/assets/index-Ddb7bDN5.js +0 -75
  180. package/web/dist/assets/path-join-S7qkXQtP.js +0 -1
@@ -7,9 +7,26 @@ const MAX_SUBMIT_AFTER_PASTE_DELAY_MS = 1500;
7
7
  const PASTE_CHARS_PER_DELAY_MS = 4;
8
8
  const PASTE_ACK_CHECK_INTERVAL_MS = 50;
9
9
  const PASTE_ACK_SETTLE_DELAY_MS = 100;
10
+ const CODEX_PASTE_ACK_SETTLE_DELAY_MS = 250;
10
11
  const PASTE_ACK_TIMEOUT_MS = 3000;
12
+ // Codex can take several seconds to render "[Pasted Content ...]" on Windows
13
+ // conpty for large bracketed-paste payloads. Keep Claude's shorter fallback,
14
+ // but give Codex enough time to use the ack-gated path in normal operation.
15
+ const CODEX_PASTE_ACK_TIMEOUT_MS = 10000;
16
+ // Codex only emits "[Pasted Content ...]" for sufficiently large pastes. Short
17
+ // Hive report/status/user-input payloads are usually rendered literally in the
18
+ // input box, so waiting for an acknowledgement there degrades into the 10s
19
+ // timeout before Enter is sent.
20
+ const CODEX_PASTE_ACK_MIN_CHARS = 2000;
21
+ const CODEX_SUBMIT_RETRY_DELAY_MS = 500;
11
22
  const COMMANDS_WITH_BRACKETED_PASTE = new Set(['claude', 'codex', 'opencode']);
23
+ const COMMANDS_WAITING_FOR_PASTE_ACK = new Set(['claude', 'codex']);
24
+ const BRACKETED_PASTE_END = '\u001b[201~';
25
+ const PASTE_ACK_PATTERN = /(?:^|[\r\n])\s*(?:[❯›]\s*)?\[(?:Pasted text #\d+[^\]]*|Pasted Content [\d,]+ chars)\]/u;
26
+ const PASTE_ACK_ONLY_DELTA_PATTERN = /^[\s\r\n]*(?:[❯›]\s*)?\[(?:Pasted text #\d+[^\]]*|Pasted Content [\d,]+ chars)\]\s*(?:[❯›]\s*)?$/u;
12
27
  export const toBracketedPasteSubmission = (text) => `\u001b[200~${text}\u001b[201~`;
28
+ const toError = (error) => (error instanceof Error ? error : new Error(String(error)));
29
+ const createRunInactiveError = (runId) => new Error(`Run became inactive before input was submitted: ${runId}`);
13
30
  const getSubmitAfterPasteDelayMs = (text) => Math.min(MAX_SUBMIT_AFTER_PASTE_DELAY_MS, Math.max(MIN_SUBMIT_AFTER_PASTE_DELAY_MS, Math.ceil(text.length / PASTE_CHARS_PER_DELAY_MS)));
14
31
  export const isInteractiveAgentCommand = (command) => {
15
32
  const brand = normalizeExecutableToken(command);
@@ -22,8 +39,24 @@ export const hasInteractivePromptReady = (output, command = '') => {
22
39
  return (/(?:^|[\r\n])\s*[❯›]\s*/u.test(output) ||
23
40
  (commandName === 'gemini' && hasGeminiPromptReady(output)));
24
41
  };
25
- export const hasBracketedPasteAcknowledgement = (output, baselineLength) => /\[Pasted text #\d+/u.test(output.slice(baselineLength));
26
- const isClaudeCommand = (command) => getCommandName(command) === 'claude';
42
+ export const hasBracketedPasteAcknowledgement = (output, baselineLength) => {
43
+ const outputAfterPaste = output.slice(baselineLength);
44
+ const pasteEndIndex = outputAfterPaste.lastIndexOf(BRACKETED_PASTE_END);
45
+ const acknowledgementOutput = pasteEndIndex === -1
46
+ ? outputAfterPaste
47
+ : outputAfterPaste.slice(pasteEndIndex + BRACKETED_PASTE_END.length);
48
+ return PASTE_ACK_PATTERN.test(acknowledgementOutput);
49
+ };
50
+ const onlyPasteAcknowledgementWasAppended = (before, after) => after.startsWith(before) && PASTE_ACK_ONLY_DELTA_PATTERN.test(after.slice(before.length));
51
+ const shouldWaitForPasteAck = (command, text) => {
52
+ const commandName = getCommandName(command);
53
+ if (commandName === 'codex')
54
+ return text.length >= CODEX_PASTE_ACK_MIN_CHARS;
55
+ return COMMANDS_WAITING_FOR_PASTE_ACK.has(commandName);
56
+ };
57
+ const retriesSubmit = (command) => getCommandName(command) === 'codex';
58
+ const getPasteAckSettleDelayMs = (command) => getCommandName(command) === 'codex' ? CODEX_PASTE_ACK_SETTLE_DELAY_MS : PASTE_ACK_SETTLE_DELAY_MS;
59
+ const getPasteAckTimeoutMs = (command) => getCommandName(command) === 'codex' ? CODEX_PASTE_ACK_TIMEOUT_MS : PASTE_ACK_TIMEOUT_MS;
27
60
  const usesBracketedPaste = (command) => COMMANDS_WITH_BRACKETED_PASTE.has(getCommandName(command));
28
61
  const canTimeoutBeforePromptReady = (command) => getCommandName(command) !== 'gemini';
29
62
  const isWritableRunStatus = (status) => status === undefined || status === 'starting' || status === 'running';
@@ -40,7 +73,7 @@ const writeIfRunWritable = (agentManager, runId, text) => {
40
73
  agentManager.writeInput(runId, text);
41
74
  return true;
42
75
  };
43
- const submitPastedInteractiveInput = (agentManager, runId, text, baselineLength, waitForPasteAck) => {
76
+ const submitPastedInteractiveInput = (agentManager, runId, text, baselineLength, waitForPasteAck, pasteAckSettleDelayMs, pasteAckTimeoutMs, retrySubmit, onDone, onError) => {
44
77
  const pastedAt = Date.now();
45
78
  const minDelay = getSubmitAfterPasteDelayMs(text);
46
79
  let acknowledgedAt = null;
@@ -53,30 +86,73 @@ const submitPastedInteractiveInput = (agentManager, runId, text, baselineLength,
53
86
  return null;
54
87
  }
55
88
  };
56
- const submit = () => {
89
+ const getRunOutputState = () => {
57
90
  try {
58
- writeIfRunWritable(agentManager, runId, '\r');
91
+ const run = agentManager.getRun(runId);
92
+ return { output: run.output, writable: isWritableRunStatus(run.status) };
59
93
  }
60
94
  catch {
95
+ return null;
96
+ }
97
+ };
98
+ const submit = (retryAfterSubmit) => {
99
+ try {
100
+ const outputBeforeSubmit = retryAfterSubmit ? getWritableOutput() : null;
101
+ if (!writeIfRunWritable(agentManager, runId, '\r')) {
102
+ onError(createRunInactiveError(runId));
103
+ return;
104
+ }
105
+ if (!retryAfterSubmit) {
106
+ onDone();
107
+ return;
108
+ }
109
+ setTimeout(() => {
110
+ try {
111
+ const runAfterSubmit = getRunOutputState();
112
+ if (runAfterSubmit === null) {
113
+ onDone();
114
+ return;
115
+ }
116
+ if (outputBeforeSubmit !== null &&
117
+ hasBracketedPasteAcknowledgement(runAfterSubmit.output, baselineLength) &&
118
+ onlyPasteAcknowledgementWasAppended(outputBeforeSubmit, runAfterSubmit.output)) {
119
+ if (!runAfterSubmit.writable || !writeIfRunWritable(agentManager, runId, '\r')) {
120
+ onError(createRunInactiveError(runId));
121
+ return;
122
+ }
123
+ }
124
+ }
125
+ catch (error) {
126
+ onError(toError(error));
127
+ return;
128
+ }
129
+ onDone();
130
+ }, CODEX_SUBMIT_RETRY_DELAY_MS);
131
+ }
132
+ catch (error) {
61
133
  // The PTY may have exited between paste and submit.
134
+ onError(toError(error));
62
135
  }
63
136
  };
64
137
  const trySubmit = () => {
65
138
  if (!waitForPasteAck) {
66
- submit();
139
+ submit(false);
67
140
  return;
68
141
  }
69
142
  const output = getWritableOutput();
70
143
  if (output === null) {
144
+ onError(createRunInactiveError(runId));
71
145
  return;
72
146
  }
73
147
  if (acknowledgedAt === null && hasBracketedPasteAcknowledgement(output, baselineLength)) {
74
148
  acknowledgedAt = Date.now();
75
149
  }
76
150
  const elapsed = Date.now() - pastedAt;
77
- const ackSettled = acknowledgedAt !== null && Date.now() - acknowledgedAt >= PASTE_ACK_SETTLE_DELAY_MS;
78
- if ((ackSettled && elapsed >= minDelay) || elapsed >= PASTE_ACK_TIMEOUT_MS) {
79
- submit();
151
+ const ackSettled = acknowledgedAt !== null && Date.now() - acknowledgedAt >= pasteAckSettleDelayMs;
152
+ const submitAfterAck = ackSettled && elapsed >= minDelay;
153
+ const submitAfterTimeout = elapsed >= pasteAckTimeoutMs;
154
+ if (submitAfterAck || submitAfterTimeout) {
155
+ submit(retrySubmit && submitAfterTimeout);
80
156
  return;
81
157
  }
82
158
  setTimeout(trySubmit, PASTE_ACK_CHECK_INTERVAL_MS);
@@ -86,10 +162,21 @@ const submitPastedInteractiveInput = (agentManager, runId, text, baselineLength,
86
162
  export const createPostStartInputWriter = (agentManager, command) => {
87
163
  if (!isInteractiveAgentCommand(command)) {
88
164
  return (runId, text) => {
89
- writeIfRunWritable(agentManager, runId, `${text}\n`);
165
+ // Synchronous write; an EPIPE/inactive failure still throws synchronously
166
+ // (before the promise is returned), preserving the dispatcher's contract.
167
+ if (!writeIfRunWritable(agentManager, runId, `${text}\n`)) {
168
+ throw createRunInactiveError(runId);
169
+ }
170
+ return Promise.resolve();
90
171
  };
91
172
  }
92
173
  return (runId, text) => {
174
+ let resolveDone;
175
+ let rejectDone;
176
+ const rejectableDone = new Promise((resolve, reject) => {
177
+ resolveDone = resolve;
178
+ rejectDone = reject;
179
+ });
93
180
  const startedAt = Date.now();
94
181
  let isInitialAttempt = true;
95
182
  const tryWrite = () => {
@@ -99,33 +186,43 @@ export const createPostStartInputWriter = (agentManager, command) => {
99
186
  output = isWritableRunStatus(run.status) ? run.output : null;
100
187
  }
101
188
  catch {
189
+ rejectDone(createRunInactiveError(runId));
102
190
  return;
103
191
  }
104
- if (output === null)
192
+ if (output === null) {
193
+ rejectDone(createRunInactiveError(runId));
105
194
  return;
195
+ }
106
196
  if (hasInteractivePromptReady(output, command) ||
107
197
  (canTimeoutBeforePromptReady(command) && Date.now() - startedAt >= READY_TIMEOUT_MS)) {
108
198
  const baselineLength = output.length;
109
199
  const input = usesBracketedPaste(command) ? toBracketedPasteSubmission(text) : text;
110
200
  try {
111
- if (!writeIfRunWritable(agentManager, runId, input))
201
+ if (!writeIfRunWritable(agentManager, runId, input)) {
202
+ rejectDone(createRunInactiveError(runId));
112
203
  return;
204
+ }
113
205
  }
114
206
  catch (error) {
115
207
  if (isInitialAttempt)
116
208
  throw error;
209
+ rejectDone(toError(error));
117
210
  return;
118
211
  }
119
- submitPastedInteractiveInput(agentManager, runId, text, baselineLength, isClaudeCommand(command));
212
+ submitPastedInteractiveInput(agentManager, runId, text, baselineLength, shouldWaitForPasteAck(command, text), getPasteAckSettleDelayMs(command), getPasteAckTimeoutMs(command), retriesSubmit(command), resolveDone, rejectDone);
120
213
  return;
121
214
  }
122
215
  setTimeout(tryWrite, READY_CHECK_INTERVAL_MS);
123
216
  };
124
217
  try {
218
+ // Called outside the Promise executor so an initial-attempt throw
219
+ // propagates synchronously to the caller (preserving the dispatcher's
220
+ // synchronous-throw contract) instead of becoming a rejection.
125
221
  tryWrite();
126
222
  }
127
223
  finally {
128
224
  isInitialAttempt = false;
129
225
  }
226
+ return rejectableDone;
130
227
  };
131
228
  };
@@ -1,6 +1,6 @@
1
1
  import type { AgentLaunchConfigInput } from './agent-run-store.js';
2
2
  import type { CommandPresetRecord } from './command-preset-store.js';
3
3
  import type { SessionCaptureSnapshot } from './session-capture.js';
4
- type BoundPreset = Pick<CommandPresetRecord, 'resumeArgsTemplate' | 'sessionIdCapture' | 'yoloArgsTemplate'>;
4
+ type BoundPreset = Pick<CommandPresetRecord, 'resumeArgsTemplate' | 'sessionIdCapture' | 'yoloArgsTemplate'> & Partial<Pick<CommandPresetRecord, 'command' | 'id'>>;
5
5
  export declare const withPresetResumeArgs: (config: AgentLaunchConfigInput, preset: BoundPreset | null | undefined, lastSessionId: string | undefined, cwd?: string, discriminator?: SessionCaptureSnapshot["discriminator"], onInvalidSessionId?: (sessionId: string) => void) => AgentLaunchConfigInput;
6
6
  export {};
@@ -3,6 +3,36 @@ const appendUniqueArgs = (prefix, args) => {
3
3
  const seen = new Set(prefix);
4
4
  return prefix.concat(args.filter((arg) => !seen.has(arg)));
5
5
  };
6
+ const unquoteExecutable = (command) => command.trim().replace(/^"(.+)"$/, '$1');
7
+ const isNodeExecutable = (command) => {
8
+ if (!command)
9
+ return false;
10
+ return /(?:^|[\\/])node(?:\.exe)?$/iu.test(unquoteExecutable(command));
11
+ };
12
+ const isCodexNpmEntrypoint = (arg) => {
13
+ if (!arg)
14
+ return false;
15
+ const normalized = arg.replace(/\\/gu, '/');
16
+ return /(?:^|\/)@openai\/codex\/bin\/codex\.js$/iu.test(normalized);
17
+ };
18
+ const isCodexPreset = (preset) => preset?.id === 'codex' ||
19
+ preset?.command === 'codex' ||
20
+ preset?.sessionIdCapture?.source === 'codex_session_jsonl_dir';
21
+ const normalizeCodexNodeEntrypoint = (config, preset) => {
22
+ if (!isCodexPreset(preset) || !isNodeExecutable(config.command))
23
+ return config;
24
+ const args = config.args ?? [];
25
+ if (!isCodexNpmEntrypoint(args[0]))
26
+ return config;
27
+ return {
28
+ ...config,
29
+ args: args.slice(1),
30
+ command: preset?.command ?? 'codex',
31
+ interactiveCommand: isNodeExecutable(config.interactiveCommand)
32
+ ? 'codex'
33
+ : (config.interactiveCommand ?? 'codex'),
34
+ };
35
+ };
6
36
  const getEffectiveCapture = (config, preset) => config.sessionIdCapture ?? preset?.sessionIdCapture ?? null;
7
37
  const getEffectiveResumeTemplate = (config, preset) => config.resumeArgsTemplate ?? preset?.resumeArgsTemplate ?? null;
8
38
  const withPresetYoloArgs = (config, preset) => {
@@ -35,7 +65,8 @@ const supportsPresetResume = (capture) => capture?.source === 'claude_project_js
35
65
  capture?.source === 'gemini_session_json_dir' ||
36
66
  capture?.source === 'opencode_session_db';
37
67
  export const withPresetResumeArgs = (config, preset, lastSessionId, cwd, discriminator, onInvalidSessionId) => {
38
- let nextConfig = withPresetYoloArgs(config, preset);
68
+ const launchConfig = normalizeCodexNodeEntrypoint(config, preset);
69
+ let nextConfig = withPresetYoloArgs(launchConfig, preset);
39
70
  const sessionIdCapture = getEffectiveCapture(nextConfig, preset);
40
71
  if (sessionIdCapture && sessionIdCapture !== nextConfig.sessionIdCapture) {
41
72
  nextConfig = { ...nextConfig, sessionIdCapture };
@@ -52,7 +83,7 @@ export const withPresetResumeArgs = (config, preset, lastSessionId, cwd, discrim
52
83
  onInvalidSessionId?.(lastSessionId);
53
84
  return nextConfig;
54
85
  }
55
- const args = config.args ?? [];
86
+ const args = launchConfig.args ?? [];
56
87
  if (hasResumeArgs(args))
57
88
  return nextConfig;
58
89
  const yoloArgs = getPresetYoloArgs(preset);
@@ -1,10 +1,14 @@
1
1
  import type { AgentSummary, WorkspaceSummary } from '../shared/types.js';
2
+ import { type FeatureFlags } from './feature-flags.js';
2
3
  import type { RecoveryMessage } from './message-log-store.js';
3
- export declare const buildRecoverySummary: ({ agent, allTaskMessages, messages, tasksContent, workers, workspace, }: {
4
+ export declare const buildRecoverySummary: ({ agent, allTaskMessages, messages, tasksContent, workers, workspace, flags, }: {
4
5
  agent: AgentSummary;
5
6
  allTaskMessages?: RecoveryMessage[];
6
7
  messages: RecoveryMessage[];
7
8
  tasksContent: string;
8
9
  workers: AgentSummary[];
9
10
  workspace: WorkspaceSummary;
11
+ /** Live experimental flags — keep the recovered handover prompt consistent
12
+ * with what a fresh startup would inject (workflow + team-sizing rules). */
13
+ flags?: FeatureFlags;
10
14
  }) => string;
@@ -1,3 +1,4 @@
1
+ import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
1
2
  import { getHiveTeamRules } from './hive-team-guidance.js';
2
3
  import { wrapSystemMessage } from './system-message.js';
3
4
  import { TASKS_RELATIVE_PATH } from './tasks-file.js';
@@ -6,7 +7,7 @@ const formatUserInputs = (messages) => {
6
7
  const userInputs = messages.filter((message) => message.type === 'user_input');
7
8
  return userInputs.length > 0
8
9
  ? userInputs.slice(-5).map((message) => `- user: ${message.text}`)
9
- : ['- (最近 1 小时没有新的 user_input)'];
10
+ : ['- (no new user_input in the last hour)'];
10
11
  };
11
12
  const formatTaskEvents = (messages, agent) => {
12
13
  const taskEvents = messages.filter((message) => {
@@ -28,7 +29,7 @@ const formatTaskEvents = (messages, agent) => {
28
29
  const status = message.status ? ` [${message.status}]` : '';
29
30
  return `- report <- ${message.from}${status}: ${message.text}`;
30
31
  })
31
- : ['- (最近没有任务事件)'];
32
+ : ['- (no recent task events)'];
32
33
  };
33
34
  const getOpenTaskTargets = (agent, workers) => agent.role === 'orchestrator' ? workers : [agent];
34
35
  const formatOpenTasks = (messages, agent, workers) => {
@@ -53,38 +54,38 @@ const formatOpenTasks = (messages, agent, workers) => {
53
54
  lines.push(`- ${target.name}: ${task.text}`);
54
55
  }
55
56
  if (target.pendingTaskCount > queue.length) {
56
- lines.push(`- ${target.name}: ${target.pendingTaskCount - queue.length} pending 无可恢复详情`);
57
+ lines.push(`- ${target.name}: ${target.pendingTaskCount - queue.length} pending with no recoverable detail`);
57
58
  }
58
59
  }
59
- return lines.length > 0 ? lines : ['- (当前没有未完成任务)'];
60
+ return lines.length > 0 ? lines : ['- (no open tasks right now)'];
60
61
  };
61
62
  const formatWorkers = (workers) => {
62
63
  if (workers.length === 0)
63
- return ['- 当前没有其他 worker'];
64
+ return ['- (no other workers)'];
64
65
  return workers.map((worker) => `- ${worker.name} (${worker.role}, ${worker.status}, pending_task_count: ${worker.pendingTaskCount})`);
65
66
  };
66
- const getTaskSectionTitle = (agent) => agent.role === 'orchestrator' ? '## 你已派出的任务' : '## 最近派给你的任务';
67
- export const buildRecoverySummary = ({ agent, allTaskMessages, messages, tasksContent, workers, workspace, }) => wrapSystemMessage([
68
- `你是 ${workspace.name} ${agent.name}(${agent.role})。`,
69
- '你刚被 Hive 重启了,且无法通过原生 session resume 恢复。下面是接力上下文。',
67
+ const getTaskSectionTitle = (agent) => agent.role === 'orchestrator' ? '## Tasks you dispatched' : '## Tasks recently sent to you';
68
+ export const buildRecoverySummary = ({ agent, allTaskMessages, messages, tasksContent, workers, workspace, flags = FEATURE_FLAGS_ALL_OFF, }) => wrapSystemMessage([
69
+ `You are ${agent.name} (${agent.role}) in workspace ${workspace.name}.`,
70
+ 'Hive just restarted you and could not recover via native session resume. Here is the handover context.',
70
71
  '',
71
- '## 最近 1 小时与 user 的对话',
72
+ '## Conversation with the user in the last hour',
72
73
  ...formatUserInputs(messages),
73
74
  '',
74
75
  getTaskSectionTitle(agent),
75
76
  ...formatTaskEvents(messages, agent),
76
77
  '',
77
- '## 当前未完成任务',
78
+ '## Open tasks',
78
79
  ...formatOpenTasks(allTaskMessages ?? messages, agent, workers),
79
80
  '',
80
- `## 当前 ${TASKS_RELATIVE_PATH} 状态`,
81
- tasksContent.slice(0, TASKS_HEAD_LIMIT) || '()',
81
+ `## Current ${TASKS_RELATIVE_PATH}`,
82
+ tasksContent.slice(0, TASKS_HEAD_LIMIT) || '(empty)',
82
83
  '',
83
- '## 当前活跃 worker',
84
+ '## Active workers',
84
85
  ...formatWorkers(workers),
85
86
  '',
86
- agent.role === 'orchestrator' ? '## Hive worker 派单规则' : '## Hive worker 边界',
87
- ...getHiveTeamRules(agent),
87
+ agent.role === 'orchestrator' ? '## Hive worker dispatch rules' : '## Hive worker boundaries',
88
+ ...getHiveTeamRules(agent, flags),
88
89
  '',
89
- '请基于此继续。如果不确定,问 user',
90
+ 'Continue from here. If unsure, ask the user.',
90
91
  ].join('\n'));
@@ -0,0 +1,36 @@
1
+ import type { Database } from 'better-sqlite3';
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.
9
+ *
10
+ * 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.
13
+ */
14
+ export interface ReportOutboxEntry {
15
+ id: number;
16
+ workspaceId: string;
17
+ targetAgentId: string;
18
+ dispatchId: string;
19
+ payload: string;
20
+ createdAt: number;
21
+ deliveredAt: number | null;
22
+ }
23
+ interface EnqueueInput {
24
+ workspaceId: string;
25
+ targetAgentId: string;
26
+ dispatchId: string;
27
+ payload: string;
28
+ }
29
+ export declare const createReportOutboxStore: (db: Database) => {
30
+ enqueue: (input: EnqueueInput) => void;
31
+ listPending: (workspaceId: string, targetAgentId: string) => ReportOutboxEntry[];
32
+ markDelivered: (id: number) => void;
33
+ pendingCount: (workspaceId: string, targetAgentId: string) => number;
34
+ };
35
+ export type ReportOutboxStore = ReturnType<typeof createReportOutboxStore>;
36
+ export {};
@@ -0,0 +1,33 @@
1
+ const rowToEntry = (row) => ({
2
+ id: row.id,
3
+ workspaceId: row.workspace_id,
4
+ targetAgentId: row.target_agent_id,
5
+ dispatchId: row.dispatch_id,
6
+ payload: row.payload,
7
+ createdAt: row.created_at,
8
+ deliveredAt: row.delivered_at,
9
+ });
10
+ export const createReportOutboxStore = (db) => {
11
+ // INSERT OR IGNORE on the UNIQUE dispatch_id: a dispatch reports once, so a
12
+ // second enqueue for the same dispatch (e.g. a retry path) is a no-op rather
13
+ // than a duplicate the orchestrator would see twice.
14
+ const enqueue = (input) => {
15
+ db.prepare(`INSERT OR IGNORE INTO report_outbox
16
+ (workspace_id, target_agent_id, dispatch_id, payload, created_at)
17
+ VALUES (?, ?, ?, ?, ?)`).run(input.workspaceId, input.targetAgentId, input.dispatchId, input.payload, Date.now());
18
+ };
19
+ const listPending = (workspaceId, targetAgentId) => db
20
+ .prepare(`SELECT id, workspace_id, target_agent_id, dispatch_id, payload, created_at, delivered_at
21
+ FROM report_outbox
22
+ WHERE workspace_id = ? AND target_agent_id = ? AND delivered_at IS NULL
23
+ ORDER BY created_at ASC, id ASC`)
24
+ .all(workspaceId, targetAgentId).map(rowToEntry);
25
+ const markDelivered = (id) => {
26
+ db.prepare(`UPDATE report_outbox SET delivered_at = ? WHERE id = ? AND delivered_at IS NULL`).run(Date.now(), id);
27
+ };
28
+ const pendingCount = (workspaceId, targetAgentId) => db
29
+ .prepare(`SELECT COUNT(*) AS n FROM report_outbox
30
+ WHERE workspace_id = ? AND target_agent_id = ? AND delivered_at IS NULL`)
31
+ .get(workspaceId, targetAgentId).n;
32
+ return { enqueue, listPending, markDelivered, pendingCount };
33
+ };
@@ -1,5 +1,6 @@
1
1
  import type { AgentSummary, WorkspaceSummary } from '../shared/types.js';
2
2
  import type { PersistedAgentRun } from './agent-run-store.js';
3
+ import type { FeatureFlags } from './feature-flags.js';
3
4
  import type { MessageLogHandle, MessageLogRecord, RecoveryMessage } from './message-log-store.js';
4
5
  export interface RestartPolicyInput {
5
6
  deleteMessage: (handle: MessageLogHandle) => void;
@@ -11,6 +12,9 @@ export interface RestartPolicyInput {
11
12
  listAgentRuns: (agentId: string) => PersistedAgentRun[];
12
13
  listMessagesForRecovery: (workspaceId: string, sinceMs: number) => RecoveryMessage[];
13
14
  readTasks: (workspacePath: string) => string;
15
+ /** Resolves the live experimental flags, threaded into the recovery handover
16
+ * prompt so it matches a fresh startup. Optional; omitted → all off. */
17
+ getFlags?: () => FeatureFlags;
14
18
  }
15
19
  export declare const findPreviousRun: (runs: PersistedAgentRun[], currentRunId: string) => PersistedAgentRun | undefined;
16
20
  export declare const writeSystemMessage: ({ deleteMessage, insertMessage, record, runId, text, writeToRun, }: {
@@ -19,5 +23,5 @@ export declare const writeSystemMessage: ({ deleteMessage, insertMessage, record
19
23
  record: MessageLogRecord;
20
24
  runId: string;
21
25
  text: string;
22
- writeToRun: (runId: string, text: string) => void;
26
+ writeToRun: (runId: string, text: string) => Promise<void>;
23
27
  }) => void;
@@ -2,7 +2,15 @@ export const findPreviousRun = (runs, currentRunId) => runs.find((run) => run.ru
2
2
  export const writeSystemMessage = ({ deleteMessage, insertMessage, record, runId, text, writeToRun, }) => {
3
3
  const handle = insertMessage(record);
4
4
  try {
5
- writeToRun(runId, text);
5
+ void writeToRun(runId, text).catch(() => {
6
+ try {
7
+ deleteMessage(handle);
8
+ }
9
+ catch {
10
+ // The runtime may already be closing; the failed post-start write is
11
+ // non-critical once the run is gone.
12
+ }
13
+ });
6
14
  }
7
15
  catch (error) {
8
16
  deleteMessage(handle);
@@ -7,8 +7,12 @@ export interface RestartPolicy {
7
7
  runId: string;
8
8
  startConfig: AgentLaunchConfigInput;
9
9
  workspace: WorkspaceSummary;
10
- writeToRun: (runId: string, text: string) => void;
10
+ writeToRun: (runId: string, text: string) => Promise<void>;
11
11
  }) => boolean;
12
+ /** Record that `runId` was killed at the user's request (Stop button), so a
13
+ * subsequent restart does not inject the crash-recovery handover for a run
14
+ * the user deliberately ended. */
15
+ markUserStopped: (runId: string) => void;
12
16
  }
13
17
  export declare const createNoopRestartPolicy: () => RestartPolicy;
14
- export declare const createRestartPolicy: ({ deleteMessage, getWorkspaceSnapshot, insertMessage, listAgentRuns, listMessagesForRecovery, readTasks, }: RestartPolicyInput) => RestartPolicy;
18
+ export declare const createRestartPolicy: ({ deleteMessage, getWorkspaceSnapshot, insertMessage, listAgentRuns, listMessagesForRecovery, readTasks, getFlags, }: RestartPolicyInput) => RestartPolicy;
@@ -1,3 +1,4 @@
1
+ import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
1
2
  import { buildRecoverySummary } from './recovery-summary.js';
2
3
  import { findPreviousRun, writeSystemMessage, } from './restart-policy-support.js';
3
4
  import { createSystemRecoverySummaryMessage } from './runtime-message-builders.js';
@@ -6,36 +7,55 @@ export const createNoopRestartPolicy = () => ({
6
7
  injectPostStartMessage() {
7
8
  return false;
8
9
  },
10
+ markUserStopped() { },
9
11
  });
10
- export const createRestartPolicy = ({ deleteMessage, getWorkspaceSnapshot, insertMessage, listAgentRuns, listMessagesForRecovery, readTasks, }) => ({
11
- injectPostStartMessage({ agentId, runId, startConfig, workspace, writeToRun }) {
12
- const previousRun = findPreviousRun(listAgentRuns(agentId), runId);
13
- if (!previousRun)
14
- return false;
15
- const snapshot = getWorkspaceSnapshot(workspace.id);
16
- const agent = snapshot.agents.find((item) => item.id === agentId);
17
- if (!agent)
18
- return false;
19
- const workers = snapshot.agents.filter((item) => item.role !== 'orchestrator' && item.id !== agentId);
20
- const tasksContent = readTasks(snapshot.summary.path);
21
- if (startConfig.resumedSessionId)
12
+ export const createRestartPolicy = ({ deleteMessage, getWorkspaceSnapshot, insertMessage, listAgentRuns, listMessagesForRecovery, readTasks, getFlags, }) => {
13
+ // Runs the user killed via the Stop button. A deliberate stop is otherwise
14
+ // byte-identical to a crash (both end status 'error'), so without this a
15
+ // stop+Restart would inject the "could not recover" handover with stale open
16
+ // tasks the user may have meant to abandon. In-process only: after a runtime
17
+ // restart this is empty, which is correct — that case IS a recovery.
18
+ const userStoppedRuns = new Set();
19
+ return {
20
+ markUserStopped(runId) {
21
+ userStoppedRuns.add(runId);
22
+ },
23
+ injectPostStartMessage({ agentId, runId, startConfig, workspace, writeToRun }) {
24
+ const previousRun = findPreviousRun(listAgentRuns(agentId), runId);
25
+ if (!previousRun)
26
+ return false;
27
+ // Consume the marker whether or not we end up skipping, so it never
28
+ // lingers to suppress a later genuine crash on the same run id.
29
+ const wasUserStopped = userStoppedRuns.delete(previousRun.runId);
30
+ const snapshot = getWorkspaceSnapshot(workspace.id);
31
+ const agent = snapshot.agents.find((item) => item.id === agentId);
32
+ if (!agent)
33
+ return false;
34
+ const workers = snapshot.agents.filter((item) => item.role !== 'orchestrator' && item.id !== agentId);
35
+ const tasksContent = readTasks(snapshot.summary.path);
36
+ if (startConfig.resumedSessionId)
37
+ return true;
38
+ // Deliberate stop + restart: start fresh, no crash handover.
39
+ if (wasUserStopped)
40
+ return false;
41
+ const text = buildRecoverySummary({
42
+ agent,
43
+ allTaskMessages: listMessagesForRecovery(workspace.id, 0),
44
+ messages: listMessagesForRecovery(workspace.id, Date.now() - RECOVERY_WINDOW_MS),
45
+ tasksContent,
46
+ workers,
47
+ workspace,
48
+ flags: getFlags?.() ?? FEATURE_FLAGS_ALL_OFF,
49
+ });
50
+ writeSystemMessage({
51
+ deleteMessage,
52
+ insertMessage,
53
+ record: createSystemRecoverySummaryMessage(workspace.id, agentId, text),
54
+ runId,
55
+ text,
56
+ writeToRun,
57
+ });
22
58
  return true;
23
- const text = buildRecoverySummary({
24
- agent,
25
- allTaskMessages: listMessagesForRecovery(workspace.id, 0),
26
- messages: listMessagesForRecovery(workspace.id, Date.now() - RECOVERY_WINDOW_MS),
27
- tasksContent,
28
- workers,
29
- workspace,
30
- });
31
- writeSystemMessage({
32
- deleteMessage,
33
- insertMessage,
34
- record: createSystemRecoverySummaryMessage(workspace.id, agentId, text),
35
- runId,
36
- text,
37
- writeToRun,
38
- });
39
- return true;
40
- },
41
- });
59
+ },
60
+ };
61
+ };
@@ -41,4 +41,5 @@ export declare const createRoleTemplateStore: (db: Database) => {
41
41
  id: string;
42
42
  isBuiltin: boolean;
43
43
  };
44
+ findByName: (name: string) => RoleTemplateRecord | undefined;
44
45
  };
@@ -34,6 +34,16 @@ export const createRoleTemplateStore = (db) => {
34
34
  .all()
35
35
  .map((row) => toRecord(row));
36
36
  };
37
+ /* TIER 2 #4 — case-insensitive lookup by template name. The workflow
38
+ runner uses this to resolve `agent('...', { agentType: 'security-reviewer' })`
39
+ against the workspace's custom role library so workflows can target
40
+ curated roles instead of always re-rolling fresh claude defaults. */
41
+ const findByName = (name) => {
42
+ const needle = name.trim().toLowerCase();
43
+ if (!needle)
44
+ return undefined;
45
+ return list().find((template) => template.name.trim().toLowerCase() === needle);
46
+ };
37
47
  const create = (input) => {
38
48
  const record = { id: randomUUID(), ...input, isBuiltin: false };
39
49
  const now = Date.now();
@@ -62,5 +72,5 @@ export const createRoleTemplateStore = (db) => {
62
72
  throw new ConflictError(`Builtin role template is read-only: ${id}`);
63
73
  db.prepare('DELETE FROM role_templates WHERE id = ?').run(id);
64
74
  };
65
- return { create, list, remove, update };
75
+ return { create, list, remove, update, findByName };
66
76
  };