@link-assistant/hive-mind 1.59.3 → 1.59.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,87 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.59.4
4
+
5
+ ### Patch Changes
6
+
7
+ - b2e0d12: Fix `/terminal_watch` uploading the full session log file when the watch
8
+ completes — addresses issue
9
+ [#1720](https://github.com/link-assistant/hive-mind/issues/1720).
10
+
11
+ Before this fix, `/terminal_watch` finished by calling
12
+ `bot.telegram.sendDocument(chatId, ...)` to attach the `<uuid>.log` file. That
13
+ had two unwanted effects:
14
+ - It duplicated work that the dedicated `/log` command already does.
15
+ - The bare `bot.telegram.sendDocument(chatId, ...)` call did not carry
16
+ `message_thread_id`, so in forum-enabled supergroups the document landed in
17
+ the **General** topic instead of the topic where `/terminal_watch` was
18
+ invoked, and it was not threaded as a reply.
19
+
20
+ `/terminal_watch` now only updates the live "✅ Terminal watch complete"
21
+ message at the end of the session. To download the log, use
22
+ `/log <uuid>` — it correctly replies in the originating topic via
23
+ `ctx.replyWithDocument`, which Telegraf annotates with `message_thread_id`
24
+ automatically.
25
+
26
+ A new regression test (`tests/test-issue-1720-terminal-watch-no-log.mjs`)
27
+ guards both behaviours, and `tests/test-issue-467-terminal-watch.mjs` was
28
+ updated to assert that no document is uploaded by the watcher.
29
+
30
+ - 5c87a38: Fix `hive` to (a) stop forwarding `false` for solve options whose `type` is
31
+ `'string'` but whose `default` is `false`, and (b) exit non-zero when any
32
+ worker fails — issue #1718.
33
+
34
+ Previously, when a user ran `/hive` against several issues, every spawned
35
+ `solve` worker crashed with:
36
+
37
+ ```
38
+ Invalid --working-session-live-progress value: "false". Expected "comment" or "pr".
39
+ ```
40
+
41
+ …and `hive` itself still exited with code `0`, so the Telegram bot rendered a
42
+ green "Work session finished successfully" envelope even though zero PRs had
43
+ been created.
44
+
45
+ Two independent root causes:
46
+ 1. **Auto-forwarder leaked `false` as a string.** In
47
+ [`src/hive.mjs`](./src/hive.mjs), the auto-forward block read:
48
+
49
+ ```js
50
+ } else if ((def.type === 'string' || def.type === 'number') && value !== undefined) {
51
+ args.push(`--${optionName}`, String(value));
52
+ }
53
+ ```
54
+
55
+ For `working-session-live-progress`, `solve.config.lib.mjs` declares
56
+ `type: 'string', default: false`. yargs preserves the boolean `false`
57
+ verbatim, so hive forwarded `--working-session-live-progress false`,
58
+ which `solve` rejects. The fix adds `&& value !== false` to the
59
+ predicate. Other `type:'string'` options whose `default` is `false`
60
+ are now also protected by a single defense-in-depth check.
61
+
62
+ 2. **No non-zero exit on worker failures.** After `monitorWithSentry()`
63
+ resolved, hive returned without consulting `issueQueue.getStats()`. The
64
+ fix queries `finalStats = issueQueue.getStats()` and calls
65
+ `safeExit(1, …)` when `finalStats.failed > 0`, mirroring the exit
66
+ semantics solve already uses. Wrappers like `start-command`, the Telegram
67
+ bot, and CI now correctly observe the failure.
68
+
69
+ `--isolation screen` (R3 of the issue) was already wired through correctly;
70
+ no change required there. The verbose forwarder dump
71
+ (`📋 Command: ${solveCommand} ${args.join(' ')}`) — which is what allowed us
72
+ to diagnose this run in the first place — is preserved.
73
+
74
+ Tests: [`tests/test-issue-1718-hive-passthrough-false.mjs`](./tests/test-issue-1718-hive-passthrough-false.mjs)
75
+ locks the option shape, asserts both fixes are present in `src/hive.mjs`,
76
+ replays the forwarder logic on synthetic argv, and adds a defense-in-depth
77
+ sweep that no `type:'string'` / `default:false` option ever produces
78
+ `--<flag> false`.
79
+
80
+ Documentation: [`docs/case-studies/issue-1718/`](./docs/case-studies/issue-1718/README.md)
81
+ contains the timeline reconstructed from the user's `screen` log, the
82
+ distilled facts, the per-symptom root-cause analysis, the solution plan, and
83
+ notes confirming no upstream report (yargs / start-command) is required.
84
+
3
85
  ## 1.59.3
4
86
 
5
87
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.59.3",
3
+ "version": "1.59.4",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
package/src/hive.mjs CHANGED
@@ -798,8 +798,8 @@ if (isRunningDirectly) {
798
798
  for (const entry of value) {
799
799
  args.push(`--${optionName}`, String(entry));
800
800
  }
801
- } else if ((def.type === 'string' || def.type === 'number') && value !== undefined) {
802
- args.push(`--${optionName}`, String(value));
801
+ } else if ((def.type === 'string' || def.type === 'number') && value !== undefined && value !== false) {
802
+ args.push(`--${optionName}`, String(value)); // Issue #1718: skip false (some string options have default:false)
803
803
  }
804
804
  }
805
805
  // Log the actual command being executed so users can investigate/reproduce
@@ -1483,6 +1483,9 @@ if (isRunningDirectly) {
1483
1483
  await log(` 📁 Full log file: ${absoluteLogPath}`, { level: 'error' });
1484
1484
  await safeExit(1, 'Error occurred');
1485
1485
  }
1486
+
1487
+ const finalStats = issueQueue.getStats(); // Issue #1718: surface worker failures via exit code
1488
+ if (finalStats.failed > 0) await safeExit(1, `${finalStats.failed} task(s) failed (completed: ${finalStats.completed})`);
1486
1489
  } catch (fatalError) {
1487
1490
  // Handle fatal errors during initialization or execution
1488
1491
  console.error('\n❌ Fatal error occurred during hive initialization or execution');
@@ -6,15 +6,12 @@
6
6
  */
7
7
 
8
8
  import fs from 'fs/promises';
9
- import path from 'path';
10
- import { constants as fsConstants } from 'fs';
11
9
  import { extractSessionIdFromText, decideLogDestination, resolveLogPath } from './telegram-log-command.lib.mjs';
12
10
 
13
11
  const DEFAULT_WIDTH = 120;
14
12
  const DEFAULT_HEIGHT = 25;
15
13
  const DEFAULT_INTERVAL_MS = 2500;
16
14
  const DEFAULT_MAX_CHARS = 3400;
17
- const TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
18
15
  const GITHUB_URL_RE = /https:\/\/github\.com\/[^\s"'`<>]+/i;
19
16
  const activeWatches = new Map();
20
17
 
@@ -135,23 +132,6 @@ export function formatTerminalWatchMessage({ sessionId, statusResult = null, log
135
132
  return lines.join('\n');
136
133
  }
137
134
 
138
- async function fileExists(filePath) {
139
- try {
140
- await fs.access(filePath, fsConstants.R_OK);
141
- return true;
142
- } catch {
143
- return false;
144
- }
145
- }
146
-
147
- async function fileSize(filePath) {
148
- try {
149
- return (await fs.stat(filePath)).size;
150
- } catch {
151
- return null;
152
- }
153
- }
154
-
155
135
  async function readLogFile(logPath) {
156
136
  try {
157
137
  return await fs.readFile(logPath, 'utf8');
@@ -182,16 +162,6 @@ export async function resolveTerminalWatchRepository({ sessionInfo = null, statu
182
162
  }
183
163
  }
184
164
 
185
- async function sendLogDocument({ bot, chatId, logPath, sessionId, statusResult }) {
186
- if (!(await fileExists(logPath))) return;
187
- const size = await fileSize(logPath);
188
- if (size !== null && size > TELEGRAM_DOCUMENT_MAX_BYTES) {
189
- await bot.telegram.sendMessage(chatId, `⚠️ Full log for \`${sessionId}\` is ${(size / (1024 * 1024)).toFixed(1)} MB, above Telegram's 50 MB upload limit.`, { parse_mode: 'Markdown' });
190
- return;
191
- }
192
- await bot.telegram.sendDocument(chatId, { source: logPath, filename: path.basename(logPath) }, { caption: `📄 Full log for session \`${sessionId}\`${statusResult?.status ? `\nStatus: \`${statusResult.status}\`` : ''}`, parse_mode: 'Markdown' });
193
- }
194
-
195
165
  async function querySessionStatusWithRetry(querySessionStatus, sessionId, verbose, attempts = 3) {
196
166
  for (let attempt = 1; attempt <= attempts; attempt++) {
197
167
  const statusResult = await querySessionStatus(sessionId, verbose);
@@ -201,7 +171,9 @@ async function querySessionStatusWithRetry(querySessionStatus, sessionId, verbos
201
171
  return null;
202
172
  }
203
173
 
204
- export function watchTerminalLogSession({ bot, chatId, messageId, sessionId, logPath, querySessionStatus, isTerminalSessionStatus, options = {}, repoDescription = null, verbose = false, attachLogOnComplete = true }) {
174
+ // Note: /terminal_watch never uploads the full session log itself (issue #1720).
175
+ // Use /log <uuid> if you want the log file delivered as a document.
176
+ export function watchTerminalLogSession({ bot, chatId, messageId, sessionId, logPath, querySessionStatus, isTerminalSessionStatus, options = {}, repoDescription = null, verbose = false }) {
205
177
  const key = `${chatId}:${messageId}:${sessionId}`;
206
178
  activeWatches.get(key)?.stop();
207
179
 
@@ -225,7 +197,6 @@ export function watchTerminalLogSession({ bot, chatId, messageId, sessionId, log
225
197
  if (completed) {
226
198
  stopped = true;
227
199
  activeWatches.delete(key);
228
- if (attachLogOnComplete) await sendLogDocument({ bot, chatId, logPath, sessionId, statusResult });
229
200
  return;
230
201
  }
231
202
  } catch (error) {
@@ -409,6 +380,5 @@ export const __INTERNAL_FOR_TESTS__ = {
409
380
  DEFAULT_HEIGHT,
410
381
  DEFAULT_INTERVAL_MS,
411
382
  DEFAULT_MAX_CHARS,
412
- TELEGRAM_DOCUMENT_MAX_BYTES,
413
383
  GITHUB_URL_RE,
414
384
  };