@link-assistant/hive-mind 1.46.6 → 1.46.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.46.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 249cf93: Fix --isolation option not working in /solve and /hive Telegram commands (#1534): extract --isolation from user args before validation, so it's used for execution isolation (via $ CLI from start-command) instead of being forwarded to solve/hive as an unknown argument. Per-command --isolation takes precedence over bot-level ISOLATION_BACKEND setting.
8
+
3
9
  ## 1.46.6
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.46.6",
3
+ "version": "1.46.7",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -40,6 +40,7 @@ const { createYargsConfig: createHiveYargsConfig } = await import('./hive.config
40
40
  const { parseGitHubUrl } = await import('./github.lib.mjs');
41
41
  const { validateModelName, buildModelOptionDescription } = await import('./models/index.mjs');
42
42
  const { validateBranchInArgs } = await import('./solve.branch.lib.mjs');
43
+ const { extractIsolationFromArgs, isValidPerCommandIsolation, resolveIsolation, createIsolationAwareQueueCallback } = await import('./telegram-isolation.lib.mjs');
43
44
  const { formatUsageMessage, getAllCachedLimits } = await import('./limits.lib.mjs');
44
45
  const { getVersionInfo, formatVersionMessage } = await import('./version-info.lib.mjs');
45
46
  const { escapeMarkdown, escapeMarkdownV2, cleanNonPrintableChars, makeSpecialCharsVisible } = await import('./telegram-markdown.lib.mjs');
@@ -586,8 +587,7 @@ async function safeReply(ctx, text, options = {}) {
586
587
  }
587
588
  }
588
589
 
589
- // Execute a command via isolation mode ($ from start-command) or start-screen, then update message
590
- async function executeAndUpdateMessage(ctx, startingMessage, commandName, args, infoBlock) {
590
+ async function executeAndUpdateMessage(ctx, startingMessage, commandName, args, infoBlock, perCommandIsolation = null) {
591
591
  const { chat, message_id: msgId } = startingMessage;
592
592
  const safeEdit = async text => {
593
593
  try {
@@ -596,16 +596,16 @@ async function executeAndUpdateMessage(ctx, startingMessage, commandName, args,
596
596
  console.error(`[telegram-bot] Failed to update message for ${commandName}: ${e.message}`);
597
597
  }
598
598
  };
599
+ const iso = await resolveIsolation(perCommandIsolation, ISOLATION_BACKEND, isolationRunner, VERBOSE);
599
600
  let result,
600
601
  session,
601
602
  extraInfo = '';
602
- if (ISOLATION_BACKEND && isolationRunner) {
603
- const sid = isolationRunner.generateSessionId();
604
- VERBOSE && console.log(`[VERBOSE] Using isolation (${ISOLATION_BACKEND}), session: ${sid}`);
605
- result = await isolationRunner.executeWithIsolation(commandName, args, { backend: ISOLATION_BACKEND, sessionId: sid, verbose: VERBOSE });
606
- session = sid;
607
- extraInfo = `\nšŸ”’ Isolation: \`${ISOLATION_BACKEND}\``;
608
- if (result.success) trackSession(sid, { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, isolationBackend: ISOLATION_BACKEND, sessionId: sid }, VERBOSE);
603
+ if (iso) {
604
+ session = iso.runner.generateSessionId();
605
+ VERBOSE && console.log(`[VERBOSE] Using isolation (${iso.backend}), session: ${session}`);
606
+ result = await iso.runner.executeWithIsolation(commandName, args, { backend: iso.backend, sessionId: session, verbose: VERBOSE });
607
+ extraInfo = `\nšŸ”’ Isolation: \`${iso.backend}\``;
608
+ if (result.success) trackSession(session, { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, isolationBackend: iso.backend, sessionId: session }, VERBOSE);
609
609
  } else {
610
610
  result = await executeStartScreen(commandName, args);
611
611
  const match = result.success && (result.output.match(/session:\s*(\S+)/i) || result.output.match(/screen -R\s+(\S+)/));
@@ -934,9 +934,12 @@ async function handleSolveCommand(ctx) {
934
934
  await safeReply(ctx, errorMsg, { reply_to_message_id: ctx.message.message_id });
935
935
  return;
936
936
  }
937
-
938
- // Merge user args with overrides
939
- const args = mergeArgsWithOverrides(userArgs, solveOverrides);
937
+ const { backend: solvePerCommandIsolation, filteredArgs: userArgsWithoutIsolation } = extractIsolationFromArgs(userArgs); // issue #1534
938
+ if (solvePerCommandIsolation && !isValidPerCommandIsolation(solvePerCommandIsolation)) {
939
+ await safeReply(ctx, `āŒ Invalid --isolation value '${escapeMarkdown(solvePerCommandIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
940
+ return;
941
+ }
942
+ const args = mergeArgsWithOverrides(userArgsWithoutIsolation, solveOverrides);
940
943
 
941
944
  // Determine tool from args (default: claude)
942
945
  let solveTool = 'claude';
@@ -1024,23 +1027,16 @@ async function handleSolveCommand(ctx) {
1024
1027
 
1025
1028
  if (check.canStart && queueStats.queued === 0) {
1026
1029
  const startingMessage = await safeReply(ctx, `šŸš€ Starting solve command...\n\n${infoBlock}`, { reply_to_message_id: ctx.message.message_id });
1027
- await executeAndUpdateMessage(ctx, startingMessage, 'solve', args, infoBlock);
1030
+ await executeAndUpdateMessage(ctx, startingMessage, 'solve', args, infoBlock, solvePerCommandIsolation);
1028
1031
  } else {
1029
- const queueItem = solveQueue.enqueue({ url: normalizedUrl, args, ctx, requester, infoBlock, tool: solveTool });
1032
+ const queueItem = solveQueue.enqueue({ url: normalizedUrl, args, ctx, requester, infoBlock, tool: solveTool, perCommandIsolation: solvePerCommandIsolation });
1030
1033
  let queueMessage = `šŸ“‹ Solve command queued (position #${queueStats.queued + 1})\n\n${infoBlock}`;
1031
1034
  if (check.reason) queueMessage += `\n\nā³ Waiting: ${escapeMarkdown(check.reason)}`;
1032
1035
  const queuedMessage = await safeReply(ctx, queueMessage, { reply_to_message_id: ctx.message.message_id });
1033
1036
  queueItem.messageInfo = { chatId: queuedMessage.chat.id, messageId: queuedMessage.message_id };
1034
1037
  if (!solveQueue.executeCallback) {
1035
- solveQueue.executeCallback =
1036
- ISOLATION_BACKEND && isolationRunner
1037
- ? async item => {
1038
- const sid = isolationRunner.generateSessionId();
1039
- const r = await isolationRunner.executeWithIsolation('solve', item.args, { backend: ISOLATION_BACKEND, sessionId: sid, verbose: VERBOSE });
1040
- if (r.success) trackSession(sid, { chatId: item.ctx?.chat?.id, messageId: item.messageInfo?.messageId, startTime: new Date(), url: item.url, command: 'solve', isolationBackend: ISOLATION_BACKEND, sessionId: sid }, VERBOSE);
1041
- return { ...r, output: r.output || `session: ${sid}` };
1042
- }
1043
- : createQueueExecuteCallback(executeStartScreen, (session, info) => trackSession(session, info, VERBOSE));
1038
+ const _t = (s, i) => trackSession(s, i, VERBOSE);
1039
+ solveQueue.executeCallback = createIsolationAwareQueueCallback(ISOLATION_BACKEND, isolationRunner, _t, createQueueExecuteCallback(executeStartScreen, _t), VERBOSE);
1044
1040
  }
1045
1041
  }
1046
1042
  }
@@ -1135,8 +1131,12 @@ async function handleHiveCommand(ctx) {
1135
1131
  if (VERBOSE) console.log(`[VERBOSE] /hive: Normalized ${p.type} URL to repo URL: ${normalizedArgs[0]}`);
1136
1132
  } else if (validation.normalizedUrl && validation.normalizedUrl !== userArgs[0]) normalizedArgs[0] = validation.normalizedUrl;
1137
1133
 
1138
- // Merge user args with overrides
1139
- const args = mergeArgsWithOverrides(normalizedArgs, hiveOverrides);
1134
+ const { backend: hivePerCommandIsolation, filteredArgs: normalizedArgsWithoutIsolation } = extractIsolationFromArgs(normalizedArgs); // issue #1534
1135
+ if (hivePerCommandIsolation && !isValidPerCommandIsolation(hivePerCommandIsolation)) {
1136
+ await safeReply(ctx, `āŒ Invalid --isolation value '${escapeMarkdown(hivePerCommandIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
1137
+ return;
1138
+ }
1139
+ const args = mergeArgsWithOverrides(normalizedArgsWithoutIsolation, hiveOverrides);
1140
1140
 
1141
1141
  // Determine tool from args (default: claude)
1142
1142
  let hiveTool = 'claude';
@@ -1195,7 +1195,7 @@ async function handleHiveCommand(ctx) {
1195
1195
  }
1196
1196
 
1197
1197
  const startingMessage = await safeReply(ctx, `šŸš€ Starting hive command...\n\n${infoBlock}`, { reply_to_message_id: ctx.message.message_id });
1198
- await executeAndUpdateMessage(ctx, startingMessage, 'hive', args, infoBlock);
1198
+ await executeAndUpdateMessage(ctx, startingMessage, 'hive', args, infoBlock, hivePerCommandIsolation);
1199
1199
  }
1200
1200
 
1201
1201
  bot.command(/^hive$/i, handleHiveCommand);
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Per-command isolation support for Telegram bot commands.
3
+ *
4
+ * Extracts --isolation <backend> from user args in /solve and /hive commands,
5
+ * so it can be used for execution isolation (via $ CLI from start-command)
6
+ * instead of being forwarded to solve/hive as an unknown argument.
7
+ *
8
+ * @see https://github.com/link-assistant/hive-mind/issues/1534
9
+ * @see https://github.com/link-assistant/hive-mind/pull/390
10
+ */
11
+
12
+ const VALID_ISOLATION_BACKENDS = ['screen', 'tmux', 'docker'];
13
+
14
+ /**
15
+ * Extract --isolation <backend> from args array.
16
+ * Returns { backend: string|null, filteredArgs: string[] }.
17
+ * The --isolation flag is a per-command execution option (not a solve/hive option),
18
+ * so it must be stripped before passing args to solve/hive validation and execution.
19
+ */
20
+ export function extractIsolationFromArgs(args) {
21
+ const filteredArgs = [];
22
+ let backend = null;
23
+ for (let i = 0; i < args.length; i++) {
24
+ if (args[i] === '--isolation' && i + 1 < args.length) {
25
+ backend = args[i + 1].trim().toLowerCase();
26
+ i++; // Skip the value
27
+ } else if (args[i].startsWith('--isolation=')) {
28
+ backend = args[i].substring('--isolation='.length).trim().toLowerCase();
29
+ } else {
30
+ filteredArgs.push(args[i]);
31
+ }
32
+ }
33
+ return { backend, filteredArgs };
34
+ }
35
+
36
+ /**
37
+ * Validate an isolation backend value.
38
+ * @param {string} backend
39
+ * @returns {boolean}
40
+ */
41
+ export function isValidPerCommandIsolation(backend) {
42
+ return VALID_ISOLATION_BACKENDS.includes(backend);
43
+ }
44
+
45
+ /**
46
+ * Get the effective isolation backend and runner for a command execution.
47
+ * Per-command isolation takes precedence over bot-level ISOLATION_BACKEND.
48
+ *
49
+ * @param {string|null} perCommandIsolation - Per-command --isolation value from user args
50
+ * @param {string} botIsolationBackend - Bot-level ISOLATION_BACKEND
51
+ * @param {object|null} botIsolationRunner - Bot-level isolation runner module
52
+ * @param {boolean} verbose - Enable verbose logging
53
+ * @returns {Promise<{backend: string, runner: object}|null>}
54
+ */
55
+ export async function resolveIsolation(perCommandIsolation, botIsolationBackend, botIsolationRunner, verbose = false) {
56
+ const effectiveBackend = perCommandIsolation || botIsolationBackend;
57
+ if (!effectiveBackend) return null;
58
+
59
+ let runner = botIsolationRunner;
60
+ if (!runner) {
61
+ try {
62
+ runner = await import('./isolation-runner.lib.mjs');
63
+ if (verbose) console.log('[VERBOSE] Dynamically imported isolation-runner for per-command isolation');
64
+ } catch (e) {
65
+ console.error(`[telegram-bot] Failed to import isolation-runner: ${e.message}`);
66
+ return null;
67
+ }
68
+ }
69
+
70
+ return { backend: effectiveBackend, runner };
71
+ }
72
+
73
+ /**
74
+ * Create a queue execute callback that supports per-command isolation.
75
+ * Falls back to the provided fallback callback when no isolation is active.
76
+ */
77
+ export function createIsolationAwareQueueCallback(botIsolationBackend, botIsolationRunner, trackSession, fallbackCallback, verbose) {
78
+ return async item => {
79
+ const iso = await resolveIsolation(item.perCommandIsolation, botIsolationBackend, botIsolationRunner, verbose);
80
+ if (iso) {
81
+ const sid = iso.runner.generateSessionId();
82
+ const r = await iso.runner.executeWithIsolation(item.command || 'solve', item.args, { backend: iso.backend, sessionId: sid, verbose });
83
+ if (r.success) trackSession(sid, { chatId: item.ctx?.chat?.id, messageId: item.messageInfo?.messageId, startTime: new Date(), url: item.url, command: item.command || 'solve', isolationBackend: iso.backend, sessionId: sid }, verbose);
84
+ return { ...r, output: r.output || `session: ${sid}` };
85
+ }
86
+ return fallbackCallback(item);
87
+ };
88
+ }