@link-assistant/hive-mind 2.15.2 → 2.17.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.
- package/CHANGELOG.md +125 -0
- package/README.hi.md +12 -0
- package/README.md +15 -0
- package/README.ru.md +15 -0
- package/README.zh.md +24 -12
- package/package.json +24 -17
- package/src/agent-snapshot-store.lib.mjs +252 -0
- package/src/agent.lib.mjs +25 -25
- package/src/agent.version-gates.lib.mjs +73 -0
- package/src/bot-lifecycle.lib.mjs +63 -5
- package/src/child-exit.lib.mjs +53 -1
- package/src/claude.session-tokens.lib.mjs +10 -8
- package/src/claude.session-transcript-repair.lib.mjs +65 -34
- package/src/cleanup.mjs +57 -3
- package/src/codex.lib.mjs +11 -1
- package/src/development-log.lib.mjs +22 -7
- package/src/disk-guard.lib.mjs +21 -1
- package/src/formal-ai-version.lib.mjs +10 -6
- package/src/github-error-reporter.lib.mjs +84 -2
- package/src/github.lib.mjs +212 -152
- package/src/instrument.mjs +12 -14
- package/src/instrument.sanitize.lib.mjs +52 -0
- package/src/isolation-runner.lib.mjs +56 -33
- package/src/isolation-runner.parsers.lib.mjs +29 -3
- package/src/isolation-runner.resume.lib.mjs +263 -0
- package/src/log-bounded-read.lib.mjs +411 -0
- package/src/log-sanitize-stream.lib.mjs +267 -0
- package/src/log-sanitize-worker-entry.mjs +31 -0
- package/src/log-sanitize-worker.lib.mjs +186 -0
- package/src/log-upload.lib.mjs +16 -4
- package/src/pull-request-changes.lib.mjs +1 -1
- package/src/session-completion-state.lib.mjs +124 -0
- package/src/session-kill-diagnostics.lib.mjs +117 -12
- package/src/session-kill-policy.lib.mjs +18 -7
- package/src/session-kill-resume.in-place.lib.mjs +136 -0
- package/src/session-kill-resume.lib.mjs +48 -18
- package/src/session-monitor.kill-sections.lib.mjs +8 -0
- package/src/session-monitor.lib.mjs +132 -9
- package/src/session-store.lib.mjs +15 -1
- package/src/solve.clone-errors.lib.mjs +86 -0
- package/src/solve.config.lib.mjs +5 -2
- package/src/solve.repository.lib.mjs +36 -63
- package/src/solve.resource-diagnostics.lib.mjs +105 -5
- package/src/start-command-cli.lib.mjs +60 -0
- package/src/telegram-bot.mjs +31 -91
- package/src/telegram-log-command.lib.mjs +7 -3
- package/src/telegram-overrides-validation.lib.mjs +73 -0
- package/src/telegram-terminal-watch-command.lib.mjs +9 -1
- package/src/working-session-summary.lib.mjs +1 -1
package/src/telegram-bot.mjs
CHANGED
|
@@ -27,10 +27,10 @@ const yargs = getLinoYargsFactory();
|
|
|
27
27
|
const { createYargsConfig: createTelegramYargsConfig } = await import('./telegram.config.lib.mjs');
|
|
28
28
|
const { createYargsConfig: createSolveYargsConfig, detectMalformedFlags } = await import('./solve.config.lib.mjs');
|
|
29
29
|
const { createYargsConfig: createHiveYargsConfig } = await import('./hive.config.lib.mjs');
|
|
30
|
-
const { enhanceUnknownArgumentError } = await import('./option-suggestions.lib.mjs');
|
|
31
30
|
const { validateBranchInArgs } = await import('./solve.branch.lib.mjs');
|
|
32
31
|
const { extractIsolationFromArgs, isValidPerCommandIsolation } = await import('./telegram-isolation.lib.mjs');
|
|
33
32
|
const { mergeArgsWithOverrides } = await import('./args-overrides.lib.mjs'); // issue #2085
|
|
33
|
+
const { validateCommandOverrides } = await import('./telegram-overrides-validation.lib.mjs'); // issue #2198
|
|
34
34
|
const config = createTelegramYargsConfig(yargs(hideBin(process.argv))).parse();
|
|
35
35
|
|
|
36
36
|
// Configuration priority: CLI option > --configuration LINO > .lenv > .env
|
|
@@ -109,96 +109,26 @@ if (ISOLATION_BACKEND) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
// Validate solve overrides early using
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
stderrBuffer.push(chunk);
|
|
128
|
-
return true;
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
// Use .parse() instead of yargs(args).parseSync() to ensure .strict() mode works
|
|
133
|
-
const testYargs = createSolveYargsConfig(yargs());
|
|
134
|
-
// Suppress yargs error output - we'll handle errors ourselves
|
|
135
|
-
testYargs
|
|
136
|
-
.exitProcess(false)
|
|
137
|
-
.showHelpOnFail(false)
|
|
138
|
-
.fail((msg, err) => {
|
|
139
|
-
if (err) throw err;
|
|
140
|
-
throw new Error(msg);
|
|
141
|
-
});
|
|
142
|
-
await testYargs.parse(testArgs);
|
|
143
|
-
// Issue #1482: Validate --base-branch in overrides early
|
|
144
|
-
const overrideBranchError = validateBranchInArgs(solveOverridesForValidation);
|
|
145
|
-
if (overrideBranchError) throw new Error(overrideBranchError);
|
|
146
|
-
console.log('✅ Solve overrides validated successfully');
|
|
147
|
-
} finally {
|
|
148
|
-
// Restore stderr
|
|
149
|
-
process.stderr.write = originalStderrWrite;
|
|
150
|
-
}
|
|
151
|
-
} catch (error) {
|
|
152
|
-
const enhancedError = enhanceUnknownArgumentError(error, createSolveYargsConfig(yargs()));
|
|
153
|
-
console.error(`❌ Invalid solve-overrides: ${enhancedError.message || String(enhancedError)}`);
|
|
154
|
-
console.error(` Overrides: ${solveOverrides.join(' ')}`);
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
// Validate hive overrides early using hive's yargs config
|
|
159
|
-
// Only validate if hive command is enabled
|
|
160
|
-
if (hiveEnabled && hiveOverrides.length > 0) {
|
|
161
|
-
console.log('Validating hive overrides...');
|
|
162
|
-
try {
|
|
163
|
-
const { backend: hiveOverrideIsolation, filteredArgs: hiveOverridesForValidation } = extractIsolationFromArgs(hiveOverrides);
|
|
164
|
-
if (hiveOverrideIsolation && !isValidPerCommandIsolation(hiveOverrideIsolation)) {
|
|
165
|
-
throw new Error(`Invalid --isolation value '${hiveOverrideIsolation}'. Must be: screen, tmux, or docker`);
|
|
166
|
-
}
|
|
167
|
-
// Add a dummy URL as the first argument (required positional for hive)
|
|
168
|
-
const testArgs = ['https://github.com/test/test', ...hiveOverridesForValidation];
|
|
169
|
-
|
|
170
|
-
// Temporarily suppress stderr to avoid yargs error output during validation
|
|
171
|
-
const originalStderrWrite = process.stderr.write;
|
|
172
|
-
const stderrBuffer = [];
|
|
173
|
-
process.stderr.write = chunk => {
|
|
174
|
-
stderrBuffer.push(chunk);
|
|
175
|
-
return true;
|
|
176
|
-
};
|
|
177
|
-
try {
|
|
178
|
-
// Use .parse() instead of yargs(args).parseSync() to ensure .strict() mode works
|
|
179
|
-
const testYargs = createHiveYargsConfig(yargs());
|
|
180
|
-
// Suppress yargs error output - we'll handle errors ourselves
|
|
181
|
-
testYargs
|
|
182
|
-
.exitProcess(false)
|
|
183
|
-
.showHelpOnFail(false)
|
|
184
|
-
.fail((msg, err) => {
|
|
185
|
-
if (err) throw err;
|
|
186
|
-
throw new Error(msg);
|
|
187
|
-
});
|
|
188
|
-
await testYargs.parse(testArgs);
|
|
189
|
-
const overrideBranchError = validateBranchInArgs(hiveOverridesForValidation); // Issue #1482
|
|
190
|
-
if (overrideBranchError) throw new Error(overrideBranchError);
|
|
191
|
-
console.log('✅ Hive overrides validated successfully');
|
|
192
|
-
} finally {
|
|
193
|
-
// Restore stderr
|
|
194
|
-
process.stderr.write = originalStderrWrite;
|
|
195
|
-
}
|
|
196
|
-
} catch (error) {
|
|
197
|
-
const enhancedError = enhanceUnknownArgumentError(error, createHiveYargsConfig(yargs()));
|
|
198
|
-
console.error(`❌ Invalid hive-overrides: ${enhancedError.message || String(enhancedError)}`);
|
|
199
|
-
console.error(` Overrides: ${hiveOverrides.join(' ')}`);
|
|
200
|
-
process.exit(1);
|
|
112
|
+
// Validate solve/hive overrides early, using each command's own yargs config, so
|
|
113
|
+
// a bad flag is rejected at startup instead of when the first session spawns
|
|
114
|
+
// (issue #1209). The shared implementation lives in
|
|
115
|
+
// telegram-overrides-validation.lib.mjs (issue #2198).
|
|
116
|
+
for (const { enabled, label, flag, overrides, createYargsConfig, dummyUrl } of [
|
|
117
|
+
{ enabled: solveEnabled, label: 'Solve', flag: 'solve-overrides', overrides: solveOverrides, createYargsConfig: createSolveYargsConfig, dummyUrl: 'https://github.com/test/test/issues/1' },
|
|
118
|
+
{ enabled: hiveEnabled, label: 'Hive', flag: 'hive-overrides', overrides: hiveOverrides, createYargsConfig: createHiveYargsConfig, dummyUrl: 'https://github.com/test/test' },
|
|
119
|
+
]) {
|
|
120
|
+
if (!enabled || overrides.length === 0) continue;
|
|
121
|
+
|
|
122
|
+
console.log(`Validating ${label.toLowerCase()} overrides...`);
|
|
123
|
+
const result = await validateCommandOverrides({ overrides, createYargsConfig, yargs, dummyUrl });
|
|
124
|
+
if (result.ok) {
|
|
125
|
+
console.log(`✅ ${label} overrides validated successfully`);
|
|
126
|
+
continue;
|
|
201
127
|
}
|
|
128
|
+
|
|
129
|
+
console.error(`❌ Invalid ${flag}: ${result.message}`);
|
|
130
|
+
console.error(` Overrides: ${overrides.join(' ')}`);
|
|
131
|
+
process.exit(1);
|
|
202
132
|
}
|
|
203
133
|
|
|
204
134
|
// Handle dry-run mode - exit after validation WITHOUT loading heavy dependencies
|
|
@@ -1252,7 +1182,17 @@ async function onBotLaunched() {
|
|
|
1252
1182
|
// the monitor resumes watching — and finally reports any that were killed while
|
|
1253
1183
|
// the bot was down. Done before starting the monitor so the first tick already
|
|
1254
1184
|
// sees the resumed sessions.
|
|
1255
|
-
|
|
1185
|
+
// Issue #2189: before replaying the durable store, let `$ --resume-all`
|
|
1186
|
+
// settle the isolation backend's own record — a restart orphans the detached
|
|
1187
|
+
// completion watchers, so an execution that ended while the bot was down has
|
|
1188
|
+
// nobody left to write its exit.
|
|
1189
|
+
await resumeSessionsOnLaunch({
|
|
1190
|
+
resumeTrackedSessions,
|
|
1191
|
+
reconcileIsolationSessions: typeof isolationRunner?.resumeAllIsolationSessions === 'function' ? ({ verbose }) => isolationRunner.resumeAllIsolationSessions({ verbose }) : null,
|
|
1192
|
+
botStartTime: BOT_START_TIME,
|
|
1193
|
+
verbose: VERBOSE,
|
|
1194
|
+
logger: botLogger,
|
|
1195
|
+
});
|
|
1256
1196
|
|
|
1257
1197
|
startSessionMonitoringOnce();
|
|
1258
1198
|
startFormalAiMaintenanceOnce();
|
|
@@ -24,7 +24,8 @@ import path from 'path';
|
|
|
24
24
|
import os from 'os';
|
|
25
25
|
import fs from 'fs/promises';
|
|
26
26
|
import { constants as fsConstants } from 'fs';
|
|
27
|
-
import { sanitizeForPublication
|
|
27
|
+
import { sanitizeForPublication } from './token-sanitization.lib.mjs';
|
|
28
|
+
import { sanitizeLogFileToFileBounded } from './log-sanitize-worker.lib.mjs';
|
|
28
29
|
import { safeReply, safeReplyWithDocument, safeSendDocument } from './telegram-safe-reply.lib.mjs';
|
|
29
30
|
|
|
30
31
|
const UUID_RE = /\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/i;
|
|
@@ -43,8 +44,11 @@ async function prepareSanitizedLogUpload(logPath, caption) {
|
|
|
43
44
|
const sanitizedPath = path.join(tempDir, path.basename(logPath));
|
|
44
45
|
|
|
45
46
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
// Issue #2189: `/log` accepts files up to Telegram's 50 MB document limit,
|
|
48
|
+
// and this used to hold the log twice (raw string + sanitized string) plus a
|
|
49
|
+
// third copy inside the sanitizer. The streaming sanitizer writes the same
|
|
50
|
+
// artifact one block at a time, so a 50 MB log costs the same as a 50 kB one.
|
|
51
|
+
const [, safeCaption] = await Promise.all([sanitizeLogFileToFileBounded({ sourcePath: logPath, destPath: sanitizedPath }), sanitizeForPublication(caption)]);
|
|
48
52
|
return {
|
|
49
53
|
path: sanitizedPath,
|
|
50
54
|
caption: safeCaption,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Early validation of --solve-overrides / --hive-overrides for the Telegram bot.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from src/telegram-bot.mjs, which had grown past the 1350-line
|
|
5
|
+
* warning threshold that scripts/check-file-line-limits.sh enforces (issue
|
|
6
|
+
* #1593, surfaced again by issue #2198). The solve and hive blocks were
|
|
7
|
+
* near-identical copies of each other, so folding them into one parameterised
|
|
8
|
+
* function removes the duplication as well as the lines.
|
|
9
|
+
*
|
|
10
|
+
* Behaviour is unchanged and pinned by tests/test-telegram-bot-dry-run.mjs and
|
|
11
|
+
* tests/test-telegram-bot-configuration-isolation-links-notation.mjs, which
|
|
12
|
+
* assert on the exact console output the caller prints.
|
|
13
|
+
*
|
|
14
|
+
* The function reports rather than exits: the caller owns the messages and the
|
|
15
|
+
* exit code, which keeps this module testable in-process.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { enhanceUnknownArgumentError } from './option-suggestions.lib.mjs';
|
|
19
|
+
import { validateBranchInArgs } from './solve.branch.lib.mjs';
|
|
20
|
+
import { extractIsolationFromArgs, isValidPerCommandIsolation } from './telegram-isolation.lib.mjs';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parses `overrides` through the real yargs config of the target command, so an
|
|
24
|
+
* unknown or malformed flag is rejected at bot startup rather than when the
|
|
25
|
+
* first command spawns a session (issue #1209).
|
|
26
|
+
*
|
|
27
|
+
* @param {object} options
|
|
28
|
+
* @param {string[]} options.overrides Override arguments to validate.
|
|
29
|
+
* @param {Function} options.createYargsConfig The command's yargs config factory.
|
|
30
|
+
* @param {Function} options.yargs A yargs factory (bound to Links Notation).
|
|
31
|
+
* @param {string} options.dummyUrl Stand-in for the command's required positional.
|
|
32
|
+
* @returns {Promise<{ok: true} | {ok: false, message: string}>}
|
|
33
|
+
*/
|
|
34
|
+
export const validateCommandOverrides = async ({ overrides, createYargsConfig, yargs, dummyUrl }) => {
|
|
35
|
+
try {
|
|
36
|
+
const { backend: overrideIsolation, filteredArgs } = extractIsolationFromArgs(overrides);
|
|
37
|
+
if (overrideIsolation && !isValidPerCommandIsolation(overrideIsolation)) {
|
|
38
|
+
throw new Error(`Invalid --isolation value '${overrideIsolation}'. Must be: screen, tmux, or docker`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const testArgs = [dummyUrl, ...filteredArgs];
|
|
42
|
+
|
|
43
|
+
// yargs writes its own diagnostics to stderr before the .fail() handler
|
|
44
|
+
// runs; suppress them so the caller's single message is what the operator
|
|
45
|
+
// sees.
|
|
46
|
+
const originalStderrWrite = process.stderr.write;
|
|
47
|
+
process.stderr.write = () => true;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// .parse() rather than parseSync() so .strict() mode is honoured.
|
|
51
|
+
const testYargs = createYargsConfig(yargs());
|
|
52
|
+
testYargs
|
|
53
|
+
.exitProcess(false)
|
|
54
|
+
.showHelpOnFail(false)
|
|
55
|
+
.fail((msg, err) => {
|
|
56
|
+
if (err) throw err;
|
|
57
|
+
throw new Error(msg);
|
|
58
|
+
});
|
|
59
|
+
await testYargs.parse(testArgs);
|
|
60
|
+
|
|
61
|
+
// Issue #1482: --base-branch inside the overrides is validated too.
|
|
62
|
+
const overrideBranchError = validateBranchInArgs(filteredArgs);
|
|
63
|
+
if (overrideBranchError) throw new Error(overrideBranchError);
|
|
64
|
+
} finally {
|
|
65
|
+
process.stderr.write = originalStderrWrite;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { ok: true };
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const enhancedError = enhanceUnknownArgumentError(error, createYargsConfig(yargs()));
|
|
71
|
+
return { ok: false, message: enhancedError.message || String(enhancedError) };
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import { extractSessionIdFromText, decideLogDestination, resolveLogPath } from './telegram-log-command.lib.mjs';
|
|
10
|
+
import { readLogTailText } from './log-bounded-read.lib.mjs';
|
|
10
11
|
import { parseSessionExitFooter } from './isolation-runner.lib.mjs';
|
|
11
12
|
import { safeReply, safeSendMessage, safeEditMessageText } from './telegram-safe-reply.lib.mjs';
|
|
12
13
|
import { classifyExitStatus, isFailureSessionStatus } from './session-status.lib.mjs';
|
|
@@ -15,6 +16,11 @@ const DEFAULT_WIDTH = 120;
|
|
|
15
16
|
const DEFAULT_HEIGHT = 25;
|
|
16
17
|
const DEFAULT_INTERVAL_MS = 2500;
|
|
17
18
|
const DEFAULT_MAX_CHARS = 3400;
|
|
19
|
+
// Issue #2189: the watch loop re-reads the log every `intervalMs`, but it only
|
|
20
|
+
// ever renders the last `height` lines and looks for the exit footer, both of
|
|
21
|
+
// which live at the very end. Reading the whole file made a poll tick cost
|
|
22
|
+
// O(log size) — a 134 MB session log allocated 134 MB of string per tick.
|
|
23
|
+
const TERMINAL_WATCH_TAIL_BYTES = 256 * 1024;
|
|
18
24
|
const GITHUB_URL_RE = /https:\/\/github\.com\/[^\s"'`<>]+/i;
|
|
19
25
|
const activeWatches = new Map();
|
|
20
26
|
|
|
@@ -141,7 +147,9 @@ export function formatTerminalWatchMessage({ sessionId, statusResult = null, log
|
|
|
141
147
|
|
|
142
148
|
async function readLogFile(logPath) {
|
|
143
149
|
try {
|
|
144
|
-
|
|
150
|
+
const { size } = await fs.stat(logPath);
|
|
151
|
+
if (!size) return '';
|
|
152
|
+
return await readLogTailText(logPath, { maxBytes: TERMINAL_WATCH_TAIL_BYTES });
|
|
145
153
|
} catch (error) {
|
|
146
154
|
if (error?.code === 'ENOENT') return '';
|
|
147
155
|
throw error;
|
|
@@ -64,7 +64,7 @@ export const formatWorkingSessionSummaryMarkdown = text => {
|
|
|
64
64
|
let inFence = false;
|
|
65
65
|
let previousNonEmpty = '';
|
|
66
66
|
|
|
67
|
-
for (let index = 0; index < lines.length;
|
|
67
|
+
for (let index = 0; index < lines.length;) {
|
|
68
68
|
const line = lines[index];
|
|
69
69
|
if (/^\s*(```|~~~)/.test(line)) {
|
|
70
70
|
inFence = !inFence;
|