@link-assistant/hive-mind 1.56.8 → 1.56.10

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,17 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.56.10
4
+
5
+ ### Patch Changes
6
+
7
+ - e2f9a37: Fix duplicated yargs choice values in Telegram validation errors.
8
+
9
+ ## 1.56.9
10
+
11
+ ### Patch Changes
12
+
13
+ - 94448c3: Fix screen-isolated work-session Telegram updates so executing messages stay compact and completion messages use `$ --status` start/end timestamps and exit codes.
14
+
3
15
  ## 1.56.8
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.56.8",
3
+ "version": "1.56.10",
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
@@ -19,7 +19,8 @@ if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
19
19
  const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
20
20
  globalThis.use = use;
21
21
  const yargsModule = await use('yargs@17.7.2');
22
- const yargs = yargsModule.default || yargsModule;
22
+ const { resolveYargsFactory } = await import('./yargs-factory.lib.mjs');
23
+ const yargs = resolveYargsFactory(yargsModule);
23
24
  const helpersModuleHelp = await use('yargs@17.7.2/helpers');
24
25
  const _helpersHelp = helpersModuleHelp.default || helpersModuleHelp;
25
26
  const hideBinHelp = _helpersHelp.hideBin || (argv => argv.slice(2));
@@ -71,7 +72,8 @@ if (isRunningDirectly) {
71
72
  'loading command-stream'
72
73
  );
73
74
  const yargsModule = await withTimeout(use('yargs@17.7.2'), 30000, 'loading yargs');
74
- const yargs = yargsModule.default || yargsModule;
75
+ const { resolveYargsFactory } = await import('./yargs-factory.lib.mjs');
76
+ const yargs = resolveYargsFactory(yargsModule);
75
77
  const helpersModuleMain = await withTimeout(use('yargs@17.7.2/helpers'), 30000, 'loading yargs helpers');
76
78
  const _helpersMain = helpersModuleMain.default || helpersModuleMain;
77
79
  const hideBin = _helpersMain.hideBin || (argv => argv.slice(2));
@@ -6,6 +6,7 @@ if (typeof globalThis.use === 'undefined') {
6
6
  globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
7
7
  }
8
8
  const use = globalThis.use;
9
+ const { resolveYargsFactory } = await import('./yargs-factory.lib.mjs');
9
10
 
10
11
  // Temporarily unset CI to avoid command-stream trace logs
11
12
  const originalCI = process.env.CI;
@@ -18,7 +19,7 @@ const { $ } = await use('command-stream');
18
19
  const $silent = $({ mirror: false, capture: true });
19
20
 
20
21
  const yargsModule = await use('yargs@17.7.2');
21
- const yargs = yargsModule.default || yargsModule;
22
+ const yargs = resolveYargsFactory(yargsModule);
22
23
  const helpersModule = await use('yargs@17.7.2/helpers');
23
24
  // Node 24 CJS/ESM interop may return the whole module object instead of named exports directly
24
25
  const _helpers = helpersModule.default || helpersModule;
@@ -17,6 +17,9 @@
17
17
 
18
18
  import { promisify } from 'util';
19
19
  import { exec as execCallback } from 'child_process';
20
+ import { formatSessionCompletionMessage, getSessionCompletionExitCode } from './work-session-formatting.lib.mjs';
21
+
22
+ export { formatSessionCompletionMessage, getSessionCompletionExitCode } from './work-session-formatting.lib.mjs';
20
23
 
21
24
  const exec = promisify(execCallback);
22
25
 
@@ -201,6 +204,7 @@ export async function monitorSessions(bot, verbose = false) {
201
204
  for (const { sessionName, sessionInfo } of sessions) {
202
205
  let stillRunning;
203
206
  let exitCode = null;
207
+ let statusResult = null;
204
208
 
205
209
  if (sessionInfo.isolationBackend && sessionInfo.sessionId) {
206
210
  // Isolation mode: use $ --status, with screen -ls only as a fallback
@@ -209,6 +213,7 @@ export async function monitorSessions(bot, verbose = false) {
209
213
  const state = await getIsolationSessionState(sessionName, sessionInfo, { verbose });
210
214
  stillRunning = state.running;
211
215
  exitCode = state.exitCode;
216
+ statusResult = state.statusResult;
212
217
  } else {
213
218
  // Issue #1586: Non-isolation screen sessions cannot reliably detect
214
219
  // completion because start-screen keeps the screen alive via `exec bash`.
@@ -233,21 +238,14 @@ export async function monitorSessions(bot, verbose = false) {
233
238
  console.log(`Session ${sessionName} has finished. Sending notification to chat ${sessionInfo.chatId}`);
234
239
 
235
240
  try {
236
- const endTime = new Date();
237
- const startTime = sessionInfo.startTime instanceof Date ? sessionInfo.startTime : new Date(sessionInfo.startTime);
238
- const duration = Math.round((endTime - startTime) / 1000);
239
- const minutes = Math.floor(duration / 60);
240
- const seconds = duration % 60;
241
-
242
- const statusEmoji = exitCode === null || exitCode === 0 ? '✅' : '❌';
243
- const statusText = exitCode === null || exitCode === 0 ? 'Completed' : `Failed (exit code: ${exitCode})`;
244
- const isolationInfo = sessionInfo.isolationBackend ? `\n🔒 Isolation: ${sessionInfo.isolationBackend}` : '';
245
-
246
- let message = `${statusEmoji} *Work Session ${statusText}*\n\n`;
247
- message += `📊 Session: \`${sessionName}\`\n`;
248
- message += `⏱️ Duration: ${minutes}m ${seconds}s\n`;
249
- message += `🔗 URL: ${sessionInfo.url}${isolationInfo}\n\n`;
250
- message += `The work session has finished. You can now review the results.`;
241
+ const finalExitCode = getSessionCompletionExitCode({ exitCode, statusResult });
242
+ const message = formatSessionCompletionMessage({
243
+ sessionName,
244
+ sessionInfo,
245
+ statusResult,
246
+ observedEndTime: new Date(),
247
+ exitCode: finalExitCode,
248
+ });
251
249
 
252
250
  // Update the original reply message if messageId is available, otherwise send new message
253
251
  if (sessionInfo.messageId) {
@@ -256,7 +254,7 @@ export async function monitorSessions(bot, verbose = false) {
256
254
  await bot.telegram.sendMessage(sessionInfo.chatId, message, { parse_mode: 'Markdown' });
257
255
  }
258
256
 
259
- completeSession(sessionName, exitCode || 0, verbose);
257
+ completeSession(sessionName, finalExitCode || 0, verbose);
260
258
  } catch (error) {
261
259
  console.error(`Failed to send completion notification for ${sessionName}:`, error);
262
260
  completeSession(sessionName, 1, verbose);
@@ -10,6 +10,7 @@
10
10
  import { enhanceErrorMessage, detectMalformedFlags } from './option-suggestions.lib.mjs';
11
11
  import { defaultModels, buildModelOptionDescription, resolveDefaultFallbackModel, resolveRuntimeDefaultModel } from './models/index.mjs';
12
12
  import { validateBranchName } from './solve.branch.lib.mjs';
13
+ import { resolveYargsFactory } from './yargs-factory.lib.mjs';
13
14
 
14
15
  // Re-export for use by telegram-bot.mjs (avoids extra import lines there)
15
16
  export { detectMalformedFlags };
@@ -18,7 +19,7 @@ export { detectMalformedFlags };
18
19
  export const initializeConfig = async use => {
19
20
  // Import yargs with specific version for hideBin support
20
21
  const yargsModule = await use('yargs@17.7.2');
21
- const yargs = yargsModule.default || yargsModule;
22
+ const yargs = resolveYargsFactory(yargsModule);
22
23
  const helpersModule = await use('yargs@17.7.2/helpers');
23
24
  // Node 24 CJS/ESM interop may return the whole module object instead of named exports directly
24
25
  const helpers = helpersModule.default || helpersModule;
@@ -25,13 +25,14 @@ const dotenvxModule = await use('@dotenvx/dotenvx');
25
25
  const dotenvx = dotenvxModule.default || dotenvxModule;
26
26
  const getenvModule = await use('getenv');
27
27
  const getenv = typeof getenvModule === 'function' ? getenvModule : getenvModule.default || getenvModule;
28
+ const { resolveYargsFactory } = await import('./yargs-factory.lib.mjs');
28
29
 
29
30
  // Load .env/.lenv configuration (issue #1318)
30
31
  dotenvx.config({ quiet: true, ignore: ['MISSING_ENV_FILE'] });
31
32
  await loadLenvConfig({ override: true, quiet: true });
32
33
 
33
34
  const yargsModule = await use('yargs@17.7.2');
34
- const yargs = yargsModule.default || yargsModule;
35
+ const yargs = resolveYargsFactory(yargsModule);
35
36
  const helpersModuleBot = await use('yargs@17.7.2/helpers');
36
37
  const _helpersBot = helpersModuleBot.default || helpersModuleBot;
37
38
  const hideBin = _helpersBot.hideBin || (argv => argv.slice(2));
@@ -50,6 +51,7 @@ const { isChatStopped, getChatStopInfo, getStoppedChatRejectMessage, DEFAULT_STO
50
51
  const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
51
52
  const { launchBotWithRetry } = await import('./telegram-bot-launcher.lib.mjs');
52
53
  const { trackSession, startSessionMonitoring, hasActiveSessionForUrlAsync } = await import('./session-monitor.lib.mjs');
54
+ const { formatExecutingWorkSessionMessage } = await import('./work-session-formatting.lib.mjs');
53
55
 
54
56
  const config = yargs(hideBin(process.argv))
55
57
  .usage('Usage: hive-telegram-bot [options]')
@@ -559,14 +561,11 @@ async function executeAndUpdateMessage(ctx, startingMessage, commandName, args,
559
561
  }
560
562
  };
561
563
  const iso = await resolveIsolation(perCommandIsolation, ISOLATION_BACKEND, isolationRunner, VERBOSE);
562
- let result,
563
- session,
564
- extraInfo = '';
564
+ let result, session;
565
565
  if (iso) {
566
566
  session = iso.runner.generateSessionId();
567
567
  VERBOSE && console.log(`[VERBOSE] Using isolation (${iso.backend}), session: ${session}`);
568
568
  result = await iso.runner.executeWithIsolation(commandName, args, { backend: iso.backend, sessionId: session, verbose: VERBOSE });
569
- extraInfo = `\n🔒 Isolation: \`${iso.backend}\``;
570
569
  if (result.success) trackSession(session, { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, isolationBackend: iso.backend, sessionId: session, tool }, VERBOSE);
571
570
  } else {
572
571
  result = await executeStartScreen(commandName, args);
@@ -579,8 +578,16 @@ async function executeAndUpdateMessage(ctx, startingMessage, commandName, args,
579
578
  if (result.success && session !== 'unknown') trackSession(session, { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, tool }, VERBOSE);
580
579
  }
581
580
  if (result.warning) return safeEdit(`⚠️ ${result.warning}`);
582
- if (result.success) await safeEdit(`🔄 ${commandName.charAt(0).toUpperCase() + commandName.slice(1)} command executing...\n\nStatus: \`Executing...\`\n📊 Session: \`${session}\`${extraInfo}\n\n${infoBlock}\n\n🔔 This message will update when the session finishes.`);
583
- else await safeEdit(`❌ Error executing ${commandName} command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\``);
581
+ if (result.success) {
582
+ await safeEdit(
583
+ formatExecutingWorkSessionMessage({
584
+ commandName,
585
+ sessionName: session,
586
+ isolationBackend: iso?.backend || null,
587
+ infoBlock,
588
+ })
589
+ );
590
+ } else await safeEdit(`❌ Error executing ${commandName} command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\``);
584
591
  }
585
592
 
586
593
  bot.command('help', async ctx => {
@@ -20,6 +20,7 @@ export { formatDuration, getRunningAgentProcesses, getRunningClaudeProcesses, ge
20
20
  import { formatDuration, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningProcesses } from './telegram-solve-queue.helpers.lib.mjs';
21
21
  export { QUEUE_CONFIG, THRESHOLD_STRATEGIES } from './queue-config.lib.mjs';
22
22
  import { QUEUE_CONFIG } from './queue-config.lib.mjs';
23
+ import { formatExecutingWorkSessionMessage } from './work-session-formatting.lib.mjs';
23
24
 
24
25
  export const QueueItemStatus = {
25
26
  QUEUED: 'queued',
@@ -1145,8 +1146,12 @@ export class SolveQueue {
1145
1146
  if (result.warning) {
1146
1147
  await item.ctx.telegram.editMessageText(chatId, messageId, undefined, `⚠️ ${result.warning}`, { parse_mode: 'Markdown' });
1147
1148
  } else if (result.success) {
1148
- const isolationInfo = result.isolationBackend ? `\n🔒 Isolation: \`${result.isolationBackend}\`` : '';
1149
- const response = `🔄 Solve command executing...\n\nStatus: \`Executing...\`\n📊 Session: \`${sessionName}\`${isolationInfo}\n\n${item.infoBlock}\n\n🔔 This message will update when the session finishes.`;
1149
+ const response = formatExecutingWorkSessionMessage({
1150
+ commandName: item.command || 'solve',
1151
+ sessionName,
1152
+ isolationBackend: result.isolationBackend,
1153
+ infoBlock: item.infoBlock,
1154
+ });
1150
1155
  await item.ctx.telegram.editMessageText(chatId, messageId, undefined, response, { parse_mode: 'Markdown' });
1151
1156
  } else {
1152
1157
  const response = `❌ Error executing solve command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\``;
@@ -0,0 +1,72 @@
1
+ const FAILURE_STATUSES = new Set(['failed', 'cancelled', 'canceled', 'error']);
2
+
3
+ function capitalizeCommandName(commandName) {
4
+ const normalized = commandName || 'solve';
5
+ return normalized.charAt(0).toUpperCase() + normalized.slice(1);
6
+ }
7
+
8
+ function parseDateValue(value) {
9
+ if (!value) return null;
10
+ const date = value instanceof Date ? value : new Date(value);
11
+ return Number.isNaN(date.getTime()) ? null : date;
12
+ }
13
+
14
+ function normalizeExitCode(value) {
15
+ if (value === null || value === undefined) return null;
16
+ const numeric = Number(value);
17
+ return Number.isFinite(numeric) ? numeric : null;
18
+ }
19
+
20
+ export function getSessionCompletionExitCode({ exitCode = null, statusResult = null } = {}) {
21
+ const explicitExitCode = normalizeExitCode(exitCode);
22
+ if (explicitExitCode !== null) return explicitExitCode;
23
+
24
+ const statusExitCode = normalizeExitCode(statusResult?.exitCode);
25
+ if (statusExitCode !== null) return statusExitCode;
26
+
27
+ const status = String(statusResult?.status || '').toLowerCase();
28
+ if (FAILURE_STATUSES.has(status)) return 1;
29
+
30
+ return null;
31
+ }
32
+
33
+ export function formatSessionDurationSeconds(seconds) {
34
+ const totalSeconds = Math.max(0, Math.round(Number(seconds) || 0));
35
+ const days = Math.floor(totalSeconds / 86400);
36
+ const hours = Math.floor((totalSeconds % 86400) / 3600);
37
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
38
+ const remainingSeconds = totalSeconds % 60;
39
+ const parts = [];
40
+
41
+ if (days > 0) parts.push(`${days}d`);
42
+ if (hours > 0) parts.push(`${hours}h`);
43
+ if (minutes > 0) parts.push(`${minutes}m`);
44
+ if (remainingSeconds > 0 || parts.length === 0) parts.push(`${remainingSeconds}s`);
45
+
46
+ return parts.join(' ');
47
+ }
48
+
49
+ export function formatExecutingWorkSessionMessage({ commandName = 'solve', sessionName = 'unknown', isolationBackend = null, infoBlock = '' } = {}) {
50
+ const isolationInfo = isolationBackend ? `\n🔒 Isolation: \`${isolationBackend}\`` : '';
51
+ const details = infoBlock ? `\n\n${infoBlock}` : '';
52
+ return `⏳ ${capitalizeCommandName(commandName)} command executing...\n\n📊 Session: \`${sessionName}\`${isolationInfo}${details}`;
53
+ }
54
+
55
+ export function formatSessionCompletionMessage({ sessionName, sessionInfo, statusResult = null, observedEndTime = new Date(), exitCode = null } = {}) {
56
+ const finalExitCode = getSessionCompletionExitCode({ exitCode, statusResult });
57
+ const failed = finalExitCode !== null && finalExitCode !== 0;
58
+ const statusEmoji = failed ? '❌' : '✅';
59
+ const statusText = failed ? `Failed (exit code: ${finalExitCode})` : 'Completed';
60
+ const isolationInfo = sessionInfo?.isolationBackend ? `\n🔒 Isolation: ${sessionInfo.isolationBackend}` : '';
61
+ const startTime = parseDateValue(statusResult?.startTime) || parseDateValue(sessionInfo?.startTime) || observedEndTime;
62
+ const endTime = parseDateValue(statusResult?.endTime) || observedEndTime;
63
+ const durationSeconds = Math.max(0, (endTime.getTime() - startTime.getTime()) / 1000);
64
+
65
+ let message = `${statusEmoji} *Work Session ${statusText}*\n\n`;
66
+ message += `📊 Session: \`${sessionName || 'unknown'}\`\n`;
67
+ message += `⏱️ Duration: ${formatSessionDurationSeconds(durationSeconds)}\n`;
68
+ message += `🔗 URL: ${sessionInfo?.url || 'unknown'}${isolationInfo}\n\n`;
69
+ message += 'The work session has finished. You can now review the results.';
70
+
71
+ return message;
72
+ }
@@ -0,0 +1,17 @@
1
+ // Resolve use-m's yargs import to the fresh parser factory.
2
+ //
3
+ // In the use-m yargs@17.7.2 shape, the module object is the factory that
4
+ // creates independent parser instances, while module.default is a singleton
5
+ // wrapper. Reusing the singleton accumulates options and duplicates choice
6
+ // values in validation errors.
7
+ export function resolveYargsFactory(yargsModule) {
8
+ if (typeof yargsModule === 'function' && typeof yargsModule.getInternalMethods === 'function') {
9
+ return yargsModule;
10
+ }
11
+
12
+ const candidate = yargsModule?.default || yargsModule;
13
+ if (typeof candidate !== 'function') {
14
+ throw new TypeError('Unable to resolve yargs factory from imported module');
15
+ }
16
+ return candidate;
17
+ }