@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 +6 -0
- package/package.json +1 -1
- package/src/telegram-bot.mjs +26 -26
- package/src/telegram-isolation.lib.mjs +88 -0
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
package/src/telegram-bot.mjs
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
603
|
-
|
|
604
|
-
VERBOSE && console.log(`[VERBOSE] Using isolation (${
|
|
605
|
-
result = await
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
939
|
-
|
|
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
|
-
|
|
1036
|
-
|
|
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
|
-
|
|
1139
|
-
|
|
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
|
+
}
|