@link-assistant/hive-mind 2.12.0 ā 2.12.2
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 +14 -0
- package/package.json +4 -1
- package/src/claude.connection.lib.mjs +209 -0
- package/src/claude.lib.mjs +6 -202
- package/src/codex.lib.mjs +0 -128
- package/src/github-rate-limit.lib.mjs +3 -0
- package/src/github-url-parser.lib.mjs +255 -0
- package/src/github.lib.mjs +22 -343
- package/src/hive.mjs +0 -152
- package/src/interactive-mode.lib.mjs +0 -43
- package/src/isolation-runner.lib.mjs +14 -174
- package/src/limits.lib.mjs +0 -89
- package/src/models/index.mjs +0 -15
- package/src/session-monitor.lib.mjs +14 -172
- package/src/solve.auto-merge.lib.mjs +70 -164
- package/src/solve.mjs +31 -193
- package/src/solve.repository.lib.mjs +0 -83
- package/src/solve.results.lib.mjs +0 -90
- package/src/solve.session.lib.mjs +52 -19
- package/src/solve.tool-uncommitted.lib.mjs +22 -0
- package/src/telegram-bot.mjs +0 -66
- package/src/telegram-merge-queue.lib.mjs +3 -155
- package/src/telegram-solve-queue.lib.mjs +9 -168
- package/src/use-m-bootstrap.lib.mjs +6 -5
- package/src/use-with-retry.lib.mjs +128 -2
|
@@ -62,6 +62,48 @@ function getSessionCommentContent(sessionType, timestamp) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Post the tracked comment that marks a work-session start.
|
|
67
|
+
*
|
|
68
|
+
* This is separate from startWorkSession because an in-process continuation
|
|
69
|
+
* after a usage-limit wait is already inside a work session: the PR is already
|
|
70
|
+
* draft, but reviewers still need an explicit boundary for the resumed tool
|
|
71
|
+
* execution.
|
|
72
|
+
*
|
|
73
|
+
* @param {Object} options
|
|
74
|
+
* @param {string} options.owner
|
|
75
|
+
* @param {string} options.repo
|
|
76
|
+
* @param {number} options.prNumber
|
|
77
|
+
* @param {Function} options.$
|
|
78
|
+
* @param {Function} options.log
|
|
79
|
+
* @param {Function} options.formatAligned
|
|
80
|
+
* @param {string} options.sessionType
|
|
81
|
+
* @param {Date} [options.timestamp]
|
|
82
|
+
*/
|
|
83
|
+
export async function postWorkSessionStartComment({ owner, repo, prNumber, $, log, formatAligned, sessionType, timestamp = new Date() }) {
|
|
84
|
+
try {
|
|
85
|
+
const { emoji, header, description } = getSessionCommentContent(sessionType, timestamp);
|
|
86
|
+
const startComment = `${emoji} **${header}**\n\n${description}`;
|
|
87
|
+
const { ok, commentId, stderr } = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: startComment });
|
|
88
|
+
if (ok) {
|
|
89
|
+
await log(formatAligned('š¬', 'Posted:', `${header} comment${commentId ? ` (id=${commentId})` : ''}`, 2));
|
|
90
|
+
} else {
|
|
91
|
+
await log(`Warning: Could not post work start comment: ${stderr || 'unknown error'}`, { level: 'warning' });
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const sentryLib = await import('./sentry.lib.mjs');
|
|
95
|
+
const { reportError } = sentryLib;
|
|
96
|
+
reportError(error, {
|
|
97
|
+
context: 'post_start_comment',
|
|
98
|
+
owner,
|
|
99
|
+
repo,
|
|
100
|
+
prNumber,
|
|
101
|
+
operation: 'create_pr_comment',
|
|
102
|
+
});
|
|
103
|
+
await log('Warning: Could not post work start comment', { level: 'warning' });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
65
107
|
/**
|
|
66
108
|
* Start a work session and post appropriate comment
|
|
67
109
|
* @param {Object} options - Session options
|
|
@@ -101,25 +143,16 @@ export async function startWorkSession({ isContinueMode, prNumber, argv, log, fo
|
|
|
101
143
|
// Post a comment marking the start of work session with appropriate header based on session type.
|
|
102
144
|
// Issue #1625: Use postTrackedComment so the comment ID is registered in-memory and can be
|
|
103
145
|
// excluded from the "did the AI post anything?" check in checkForAiCreatedComments().
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
const sentryLib = await import('./sentry.lib.mjs');
|
|
115
|
-
const { reportError } = sentryLib;
|
|
116
|
-
reportError(error, {
|
|
117
|
-
context: 'post_start_comment',
|
|
118
|
-
prNumber,
|
|
119
|
-
operation: 'create_pr_comment',
|
|
120
|
-
});
|
|
121
|
-
await log('Warning: Could not post work start comment', { level: 'warning' });
|
|
122
|
-
}
|
|
146
|
+
await postWorkSessionStartComment({
|
|
147
|
+
owner: global.owner,
|
|
148
|
+
repo: global.repo,
|
|
149
|
+
prNumber,
|
|
150
|
+
$,
|
|
151
|
+
log,
|
|
152
|
+
formatAligned,
|
|
153
|
+
sessionType,
|
|
154
|
+
timestamp: workStartTime,
|
|
155
|
+
});
|
|
123
156
|
}
|
|
124
157
|
|
|
125
158
|
return workStartTime;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const TOOL_MODULES = {
|
|
2
|
+
agent: './agent.lib.mjs',
|
|
3
|
+
codex: './codex.lib.mjs',
|
|
4
|
+
gemini: './gemini.lib.mjs',
|
|
5
|
+
opencode: './opencode.lib.mjs',
|
|
6
|
+
qwen: './qwen.lib.mjs',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export async function resolveUncommittedChangesTool({ argv, claudeLib, importModule = specifier => import(specifier) }) {
|
|
10
|
+
if (argv.useAgentCommander) {
|
|
11
|
+
const agentCommanderLib = await importModule('./agent-commander.lib.mjs');
|
|
12
|
+
return { agentCommanderLib, checkForUncommittedChanges: agentCommanderLib.checkForUncommittedChanges };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const toolModule = TOOL_MODULES[argv.tool];
|
|
16
|
+
if (toolModule) {
|
|
17
|
+
const module = await importModule(toolModule);
|
|
18
|
+
return { agentCommanderLib: null, checkForUncommittedChanges: module.checkForUncommittedChanges };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { agentCommanderLib: null, checkForUncommittedChanges: claudeLib.checkForUncommittedChanges };
|
|
22
|
+
}
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -13,14 +13,12 @@ if (process.argv.includes('--version')) {
|
|
|
13
13
|
if (typeof use === 'undefined') {
|
|
14
14
|
await ensureUseM();
|
|
15
15
|
}
|
|
16
|
-
|
|
17
16
|
const { lino } = await import('./lino.lib.mjs');
|
|
18
17
|
const { loadLenvConfig } = await import('./lenv-reader.lib.mjs');
|
|
19
18
|
const { getLinoYargsFactory, getenv, hideBin } = await import('./cli-arguments.lib.mjs');
|
|
20
19
|
|
|
21
20
|
const dotenvxModule = await use('@dotenvx/dotenvx');
|
|
22
21
|
const dotenvx = dotenvxModule.default || dotenvxModule;
|
|
23
|
-
|
|
24
22
|
// Load .env/.lenv configuration (issue #1318)
|
|
25
23
|
dotenvx.config({ quiet: true, ignore: ['MISSING_ENV_FILE'] });
|
|
26
24
|
await loadLenvConfig({ override: true, quiet: true });
|
|
@@ -33,14 +31,12 @@ const { enhanceUnknownArgumentError } = await import('./option-suggestions.lib.m
|
|
|
33
31
|
const { validateBranchInArgs } = await import('./solve.branch.lib.mjs');
|
|
34
32
|
const { extractIsolationFromArgs, isValidPerCommandIsolation } = await import('./telegram-isolation.lib.mjs');
|
|
35
33
|
const { mergeArgsWithOverrides } = await import('./args-overrides.lib.mjs'); // issue #2085
|
|
36
|
-
|
|
37
34
|
const config = createTelegramYargsConfig(yargs(hideBin(process.argv))).parse();
|
|
38
35
|
|
|
39
36
|
// Configuration priority: CLI option > --configuration LINO > .lenv > .env
|
|
40
37
|
if (config.configuration) {
|
|
41
38
|
await loadLenvConfig({ configuration: config.configuration, override: true, quiet: true });
|
|
42
39
|
}
|
|
43
|
-
|
|
44
40
|
const BOT_TOKEN = config.token || getenv('TELEGRAM_BOT_TOKEN', '');
|
|
45
41
|
const VERBOSE = config.verbose || getenv('TELEGRAM_BOT_VERBOSE', 'false') === 'true';
|
|
46
42
|
const AUTO_WATCH_MESSAGE = config.autoStartScreenWatchMessage === true;
|
|
@@ -53,7 +49,6 @@ if (!BOT_TOKEN) {
|
|
|
53
49
|
// Resolve final config values (CLI option > environment variable)
|
|
54
50
|
const resolvedAllowedChats = config.allowedChats || getenv('TELEGRAM_ALLOWED_CHATS', '');
|
|
55
51
|
const allowedChats = resolvedAllowedChats ? lino.parseNumericIds(resolvedAllowedChats) : null;
|
|
56
|
-
|
|
57
52
|
// Parse allowed topics (chatId:topicId pairs in Links Notation)
|
|
58
53
|
const resolvedAllowedTopics = config.allowedTopics || getenv('TELEGRAM_ALLOWED_TOPICS', '');
|
|
59
54
|
const allowedTopics = resolvedAllowedTopics ? lino.parseLinks(resolvedAllowedTopics) : null;
|
|
@@ -125,7 +120,6 @@ if (solveEnabled && solveOverrides.length > 0) {
|
|
|
125
120
|
}
|
|
126
121
|
// Add a dummy URL as the first argument (required positional for solve)
|
|
127
122
|
const testArgs = ['https://github.com/test/test/issues/1', ...solveOverridesForValidation];
|
|
128
|
-
|
|
129
123
|
// Temporarily suppress stderr to avoid yargs error output during validation
|
|
130
124
|
const originalStderrWrite = process.stderr.write;
|
|
131
125
|
const stderrBuffer = [];
|
|
@@ -161,7 +155,6 @@ if (solveEnabled && solveOverrides.length > 0) {
|
|
|
161
155
|
process.exit(1);
|
|
162
156
|
}
|
|
163
157
|
}
|
|
164
|
-
|
|
165
158
|
// Validate hive overrides early using hive's yargs config
|
|
166
159
|
// Only validate if hive command is enabled
|
|
167
160
|
if (hiveEnabled && hiveOverrides.length > 0) {
|
|
@@ -181,7 +174,6 @@ if (hiveEnabled && hiveOverrides.length > 0) {
|
|
|
181
174
|
stderrBuffer.push(chunk);
|
|
182
175
|
return true;
|
|
183
176
|
};
|
|
184
|
-
|
|
185
177
|
try {
|
|
186
178
|
// Use .parse() instead of yargs(args).parseSync() to ensure .strict() mode works
|
|
187
179
|
const testYargs = createHiveYargsConfig(yargs());
|
|
@@ -234,7 +226,6 @@ if (config.dryRun) {
|
|
|
234
226
|
console.log('\nš Bot configuration is valid. Exiting without starting the bot.');
|
|
235
227
|
process.exit(0);
|
|
236
228
|
}
|
|
237
|
-
|
|
238
229
|
// === HEAVY DEPENDENCIES LOADED BELOW (skipped in dry-run mode) ===
|
|
239
230
|
// These imports are after dry-run check to speed up config validation. Telegraf can take 3-8s to load on cold start (issue #801).
|
|
240
231
|
|
|
@@ -264,7 +255,6 @@ const { createHeartbeat, resumeSessionsOnLaunch, createShutdownHandler } = await
|
|
|
264
255
|
const { formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage } = await import('./work-session-formatting.lib.mjs');
|
|
265
256
|
const { buildTelegramHelpMessage, buildTelegramInfoBlock, buildSolveQueuedMessage } = await import('./telegram-ui-messages.lib.mjs');
|
|
266
257
|
const { startFormalAiMaintenance } = await import('./formal-ai-maintenance.lib.mjs');
|
|
267
|
-
|
|
268
258
|
// Initialize Sentry for error tracking
|
|
269
259
|
await initializeSentry({
|
|
270
260
|
debug: VERBOSE,
|
|
@@ -276,7 +266,6 @@ await initializeSentry({
|
|
|
276
266
|
const { initI18n, t, preloadAllLocales, resolveLocaleFromTelegramCtx } = await import('./i18n.lib.mjs');
|
|
277
267
|
await initI18n();
|
|
278
268
|
await preloadAllLocales();
|
|
279
|
-
|
|
280
269
|
const telegrafModule = await use('telegraf');
|
|
281
270
|
const { Telegraf } = telegrafModule;
|
|
282
271
|
const bot = new Telegraf(BOT_TOKEN, {
|
|
@@ -302,7 +291,6 @@ botLogger.event('bot_starting', { pid: process.pid, ppid: process.ppid, botStart
|
|
|
302
291
|
function isChatAuthorized(chatId) {
|
|
303
292
|
return _isChatAuthorized(chatId, allowedChats);
|
|
304
293
|
}
|
|
305
|
-
|
|
306
294
|
// Topic-level authorization (issue #1100): chat-level auth overrides topic-level
|
|
307
295
|
function isTopicAuthorized(ctx) {
|
|
308
296
|
if (isChatAuthorized(ctx.chat?.id)) return true;
|
|
@@ -322,7 +310,6 @@ function buildAuthErrorMessage(ctx) {
|
|
|
322
310
|
function isOldMessage(ctx) {
|
|
323
311
|
return _isOldMessage(ctx, BOT_START_TIME, { verbose: VERBOSE });
|
|
324
312
|
}
|
|
325
|
-
|
|
326
313
|
async function executeStartScreen(command, args) {
|
|
327
314
|
return executeStartScreenCommand(command, args, { verbose: VERBOSE });
|
|
328
315
|
}
|
|
@@ -330,7 +317,6 @@ async function executeStartScreen(command, args) {
|
|
|
330
317
|
function isForwardedOrReply(ctx) {
|
|
331
318
|
return _isForwardedOrReply(ctx, { verbose: VERBOSE });
|
|
332
319
|
}
|
|
333
|
-
|
|
334
320
|
// Forwarded-only check (issue #1922). Commands that support the reply feature
|
|
335
321
|
// (e.g. /task, /solve) must still reject forwarded commands without rejecting
|
|
336
322
|
// genuine user replies, so they use this instead of isForwardedOrReply.
|
|
@@ -371,7 +357,6 @@ function validateModelInArgs(args, tool = 'claude') {
|
|
|
371
357
|
}
|
|
372
358
|
return null;
|
|
373
359
|
}
|
|
374
|
-
|
|
375
360
|
// Inject --language LOCALE into spawn args if no language flag is already present.
|
|
376
361
|
// Issue #378: telegram bot resolves the user's effective locale and propagates
|
|
377
362
|
// it to spawned solve/hive sessions so the AI tool replies in the same language.
|
|
@@ -391,7 +376,6 @@ async function getCommandUrlArg(args, createYargsConfig, positionalNames) {
|
|
|
391
376
|
if (parsedUrl) return parsedUrl;
|
|
392
377
|
return args.find(arg => cleanNonPrintableChars(arg).includes('github.com')) || (args[0] && !args[0].startsWith('-') ? args[0] : null);
|
|
393
378
|
}
|
|
394
|
-
|
|
395
379
|
async function validateGitHubUrl(args, options = {}) {
|
|
396
380
|
const { allowedTypes = ['issue', 'pull'], commandName = 'solve', createYargsConfig = null, positionalNames = [], locale = null } = options;
|
|
397
381
|
const rawUrl = await getCommandUrlArg(args, createYargsConfig, positionalNames);
|
|
@@ -417,7 +401,6 @@ async function validateGitHubUrl(args, options = {}) {
|
|
|
417
401
|
}
|
|
418
402
|
|
|
419
403
|
const executeAndUpdateMessage = buildExecuteAndUpdateMessage({ resolveIsolation, ISOLATION_BACKEND, isolationRunner, VERBOSE, executeStartScreen, trackSession, untrackSession, AUTO_WATCH_MESSAGE, startAutoTerminalWatchForSession, bot, formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage });
|
|
420
|
-
|
|
421
404
|
bot.command('help', async ctx => {
|
|
422
405
|
VERBOSE && console.log('[VERBOSE] /help command received');
|
|
423
406
|
|
|
@@ -426,7 +409,6 @@ bot.command('help', async ctx => {
|
|
|
426
409
|
VERBOSE && console.log('[VERBOSE] /help ignored: old message');
|
|
427
410
|
return;
|
|
428
411
|
}
|
|
429
|
-
|
|
430
412
|
// Ignore forwarded or reply messages
|
|
431
413
|
if (isForwardedOrReply(ctx)) {
|
|
432
414
|
VERBOSE && console.log('[VERBOSE] /help ignored: forwarded or reply');
|
|
@@ -464,13 +446,11 @@ bot.command('help', async ctx => {
|
|
|
464
446
|
authorized,
|
|
465
447
|
allowTopicHint: topicId ? `TELEGRAM_ALLOWED_TOPICS="(${chatId} ${topicId})"` : '',
|
|
466
448
|
});
|
|
467
|
-
|
|
468
449
|
await safeReply(ctx, message, { fallbackLocale: helpLocale });
|
|
469
450
|
});
|
|
470
451
|
|
|
471
452
|
bot.command('limits', async ctx => {
|
|
472
453
|
VERBOSE && console.log('[VERBOSE] /limits command received');
|
|
473
|
-
|
|
474
454
|
// Add breadcrumb for error tracking
|
|
475
455
|
await addBreadcrumb({ category: 'telegram.command', message: '/limits command received', level: 'info', data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username } });
|
|
476
456
|
|
|
@@ -479,7 +459,6 @@ bot.command('limits', async ctx => {
|
|
|
479
459
|
VERBOSE && console.log('[VERBOSE] /limits ignored: old message');
|
|
480
460
|
return;
|
|
481
461
|
}
|
|
482
|
-
|
|
483
462
|
// Ignore forwarded or reply messages
|
|
484
463
|
if (isForwardedOrReply(ctx)) {
|
|
485
464
|
VERBOSE && console.log('[VERBOSE] /limits ignored: forwarded or reply');
|
|
@@ -494,7 +473,6 @@ bot.command('limits', async ctx => {
|
|
|
494
473
|
await ctx.reply(t('telegram.limits_only_in_groups', {}, { locale: userLocale }), { reply_to_message_id: ctx.message.message_id });
|
|
495
474
|
return;
|
|
496
475
|
}
|
|
497
|
-
|
|
498
476
|
if (!isTopicAuthorized(ctx)) {
|
|
499
477
|
if (VERBOSE) {
|
|
500
478
|
console.log('[VERBOSE] /limits ignored: not authorized');
|
|
@@ -510,7 +488,6 @@ bot.command('limits', async ctx => {
|
|
|
510
488
|
|
|
511
489
|
// Get all limits using shared cache (3min for API, 2min for system)
|
|
512
490
|
const limits = await getAllCachedLimits(VERBOSE);
|
|
513
|
-
|
|
514
491
|
// Format message with usage limits and queue status (issues #1343, #1267)
|
|
515
492
|
const claudeError = limits.claude.success ? null : limits.claude.error;
|
|
516
493
|
const codexError = limits.codex.success ? null : limits.codex.error;
|
|
@@ -544,7 +521,6 @@ bot.command('version', async ctx => {
|
|
|
544
521
|
|
|
545
522
|
const { registerLanguageCommand } = await import('./telegram-language-command.lib.mjs');
|
|
546
523
|
registerLanguageCommand(bot, { VERBOSE, isOldMessage, isForwardedOrReply });
|
|
547
|
-
|
|
548
524
|
const { registerAcceptInvitesCommand } = await import('./telegram-accept-invitations.lib.mjs');
|
|
549
525
|
const sharedCommandOpts = { VERBOSE, isOldMessage, isForwarded, isForwardedOrReply, isGroupChat: _isGroupChat, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage, addBreadcrumb, isChatStopped, getStoppedChatRejectMessage };
|
|
550
526
|
registerAcceptInvitesCommand(bot, sharedCommandOpts);
|
|
@@ -566,7 +542,6 @@ async function handleSolveCommand(ctx) {
|
|
|
566
542
|
const solveCommandName = getSolveCommandNameFromText(ctx.message?.text) || 'solve';
|
|
567
543
|
const solveCommandDisplay = `/${solveCommandName}`;
|
|
568
544
|
VERBOSE && console.log(`[VERBOSE] ${solveCommandDisplay} command received`);
|
|
569
|
-
|
|
570
545
|
// Add breadcrumb for error tracking
|
|
571
546
|
await addBreadcrumb({
|
|
572
547
|
category: 'telegram.command',
|
|
@@ -588,7 +563,6 @@ async function handleSolveCommand(ctx) {
|
|
|
588
563
|
await ctx.reply(t('telegram.solve_disabled', {}, { locale: solveLocale }));
|
|
589
564
|
return;
|
|
590
565
|
}
|
|
591
|
-
|
|
592
566
|
// Ignore messages sent before bot started
|
|
593
567
|
if (isOldMessage(ctx)) {
|
|
594
568
|
if (VERBOSE) {
|
|
@@ -606,7 +580,6 @@ async function handleSolveCommand(ctx) {
|
|
|
606
580
|
}
|
|
607
581
|
return;
|
|
608
582
|
}
|
|
609
|
-
|
|
610
583
|
if (!_isGroupChat(ctx)) {
|
|
611
584
|
if (VERBOSE) {
|
|
612
585
|
console.log(`[VERBOSE] ${solveCommandDisplay} ignored: not a group chat`);
|
|
@@ -622,7 +595,6 @@ async function handleSolveCommand(ctx) {
|
|
|
622
595
|
await ctx.reply(buildAuthErrorMessage(ctx), { reply_to_message_id: ctx.message.message_id });
|
|
623
596
|
return;
|
|
624
597
|
}
|
|
625
|
-
|
|
626
598
|
// Check if chat is stopped (issue #1081) - reject with same style as queue rejected mode
|
|
627
599
|
const chatId = ctx.chat.id;
|
|
628
600
|
if (isChatStopped(chatId)) {
|
|
@@ -632,7 +604,6 @@ async function handleSolveCommand(ctx) {
|
|
|
632
604
|
}
|
|
633
605
|
|
|
634
606
|
VERBOSE && console.log(`[VERBOSE] ${solveCommandDisplay} passed all checks, executing...`);
|
|
635
|
-
|
|
636
607
|
const solveToolAlias = getSolveToolAliasFromText(ctx.message.text);
|
|
637
608
|
let userArgs = parseCommandArgs(ctx.message.text);
|
|
638
609
|
|
|
@@ -641,7 +612,6 @@ async function handleSolveCommand(ctx) {
|
|
|
641
612
|
if (solveSL.handled) return;
|
|
642
613
|
const solveShowLimits = solveSL.showLimits;
|
|
643
614
|
userArgs = solveSL.args;
|
|
644
|
-
|
|
645
615
|
// Check if this is a reply to a message and user didn't provide URL as first argument
|
|
646
616
|
// In that case, try to extract GitHub URL from the replied message
|
|
647
617
|
// Issue #1325: Support all options via /solve command when replying (e.g., "/solve --model opus")
|
|
@@ -651,7 +621,6 @@ async function handleSolveCommand(ctx) {
|
|
|
651
621
|
const commandUrlArg = await getCommandUrlArg(userArgs, createSolveYargsConfig, ['issue-url']);
|
|
652
622
|
const commandUrlText = commandUrlArg ? cleanNonPrintableChars(commandUrlArg) : '';
|
|
653
623
|
const commandHasUrl = commandUrlText.includes('github.com') || /^https?:\/\//.test(commandUrlText);
|
|
654
|
-
|
|
655
624
|
if (isReply && !commandHasUrl) {
|
|
656
625
|
if (VERBOSE) {
|
|
657
626
|
console.log('[VERBOSE] /solve is a reply without URL in args, extracting from replied message...');
|
|
@@ -660,7 +629,6 @@ async function handleSolveCommand(ctx) {
|
|
|
660
629
|
|
|
661
630
|
const replyText = message.reply_to_message.text || '';
|
|
662
631
|
const extraction = _extractGitHubUrl(replyText, { parseGitHubUrl, cleanNonPrintableChars });
|
|
663
|
-
|
|
664
632
|
if (extraction.error) {
|
|
665
633
|
// Multiple links found
|
|
666
634
|
if (VERBOSE) {
|
|
@@ -688,7 +656,6 @@ async function handleSolveCommand(ctx) {
|
|
|
688
656
|
}
|
|
689
657
|
|
|
690
658
|
userArgs = applySolveToolAlias(userArgs, solveToolAlias);
|
|
691
|
-
|
|
692
659
|
const { malformed, errors: malformedErrors } = detectMalformedFlags(userArgs);
|
|
693
660
|
if (malformed.length > 0) {
|
|
694
661
|
await safeReply(ctx, `ā ${escapeMarkdown(malformedErrors.join('\n'))}\n\n${t('telegram.option_syntax_check', {}, { locale: solveLocale })}`, { reply_to_message_id: ctx.message.message_id });
|
|
@@ -728,7 +695,6 @@ async function handleSolveCommand(ctx) {
|
|
|
728
695
|
solveTool = args[i].substring('--tool='.length);
|
|
729
696
|
}
|
|
730
697
|
}
|
|
731
|
-
|
|
732
698
|
// Validate model name with helpful error message (before yargs validation)
|
|
733
699
|
const modelError = validateModelInArgs(args, solveTool);
|
|
734
700
|
if (modelError) {
|
|
@@ -788,7 +754,6 @@ async function handleSolveCommand(ctx) {
|
|
|
788
754
|
lockedOptions: solveOverrides.length > 0 ? escapeMarkdown(solveOverrides.join(' ')) : '',
|
|
789
755
|
});
|
|
790
756
|
const solveQueue = getSolveQueue({ verbose: VERBOSE });
|
|
791
|
-
|
|
792
757
|
// Check for duplicate URL in queue (issue #1080)
|
|
793
758
|
const existingItem = solveQueue.findByUrl(normalizedUrl);
|
|
794
759
|
if (existingItem) {
|
|
@@ -813,7 +778,6 @@ async function handleSolveCommand(ctx) {
|
|
|
813
778
|
|
|
814
779
|
// Issue #1688: parsed URL context lets the completion message look up linked PRs.
|
|
815
780
|
const solveUrlContext = validation.parsed ? { owner: validation.parsed.owner, repo: validation.parsed.repo, number: validation.parsed.number, type: validation.parsed.type, normalized: validation.parsed.normalized || normalizedUrl } : null;
|
|
816
|
-
|
|
817
781
|
const toolQueuedCount = queueStats.queuedByTool[solveTool] || 0; // tool-specific queue count (#1551)
|
|
818
782
|
// Issue #378: propagate user's effective Telegram locale to the spawned solve session.
|
|
819
783
|
const argsWithLocale = injectLanguageIfMissing(args, solveLocale);
|
|
@@ -821,7 +785,6 @@ async function handleSolveCommand(ctx) {
|
|
|
821
785
|
// Issue #594: append "Limits at start" to infoBlock; thread snapshot via sessionInfo.
|
|
822
786
|
let solveLimitsAtStart = null;
|
|
823
787
|
if (solveShowLimits) ({ infoBlock, limitsAtStart: solveLimitsAtStart } = await captureStartSnapshotAndAppend({ infoBlock, tool: solveTool, verbose: VERBOSE, limitsLib, commandLabel: '/solve', locale: solveLocale }));
|
|
824
|
-
|
|
825
788
|
if (check.canStart && check.startReserved) {
|
|
826
789
|
const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock, locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
|
|
827
790
|
await executeAndUpdateMessage(ctx, startingMessage, 'solve', argsWithLocale, infoBlock, effectiveSolveIsolation, solveTool, solveUrlContext, { showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart, locale: solveLocale, commandAlias: solveCommandName });
|
|
@@ -841,7 +804,6 @@ bot.command(
|
|
|
841
804
|
SOLVE_COMMAND_NAMES.map(command => new RegExp(`^${command}$`, 'i')),
|
|
842
805
|
handleSolveCommand
|
|
843
806
|
);
|
|
844
|
-
|
|
845
807
|
// Named handler for /hive command - extracted for reuse by text-based fallback (issue #1207)
|
|
846
808
|
async function handleHiveCommand(ctx) {
|
|
847
809
|
if (VERBOSE) {
|
|
@@ -860,7 +822,6 @@ async function handleHiveCommand(ctx) {
|
|
|
860
822
|
username: ctx.from?.username,
|
|
861
823
|
},
|
|
862
824
|
});
|
|
863
|
-
|
|
864
825
|
const hiveLocale = resolveLocaleFromTelegramCtx(ctx);
|
|
865
826
|
if (!hiveEnabled) {
|
|
866
827
|
if (VERBOSE) {
|
|
@@ -877,7 +838,6 @@ async function handleHiveCommand(ctx) {
|
|
|
877
838
|
}
|
|
878
839
|
return;
|
|
879
840
|
}
|
|
880
|
-
|
|
881
841
|
// Ignore forwarded or reply messages
|
|
882
842
|
if (isForwardedOrReply(ctx)) {
|
|
883
843
|
if (VERBOSE) {
|
|
@@ -893,7 +853,6 @@ async function handleHiveCommand(ctx) {
|
|
|
893
853
|
await ctx.reply(t('telegram.hive_only_in_groups', {}, { locale: hiveLocale }), { reply_to_message_id: ctx.message.message_id });
|
|
894
854
|
return;
|
|
895
855
|
}
|
|
896
|
-
|
|
897
856
|
if (!isTopicAuthorized(ctx)) {
|
|
898
857
|
if (VERBOSE) {
|
|
899
858
|
console.log('[VERBOSE] /hive ignored: not authorized');
|
|
@@ -909,11 +868,9 @@ async function handleHiveCommand(ctx) {
|
|
|
909
868
|
await safeReply(ctx, getStoppedChatRejectMessage(chatId, 'Hive'), { reply_to_message_id: ctx.message.message_id });
|
|
910
869
|
return;
|
|
911
870
|
}
|
|
912
|
-
|
|
913
871
|
VERBOSE && console.log('[VERBOSE] /hive passed all checks, executing...');
|
|
914
872
|
|
|
915
873
|
let userArgs = parseCommandArgs(ctx.message.text);
|
|
916
|
-
|
|
917
874
|
// Issue #594: see /solve handler.
|
|
918
875
|
const hiveSL = await handleShowLimitsFlag({ ctx, safeReply, args: userArgs, enabled: SHOW_LIMITS_ENABLED, locale: hiveLocale });
|
|
919
876
|
if (hiveSL.handled) return;
|
|
@@ -936,7 +893,6 @@ async function handleHiveCommand(ctx) {
|
|
|
936
893
|
normalizedArgs[0] = `https://github.com/${p.owner}/${p.repo}`;
|
|
937
894
|
if (VERBOSE) console.log(`[VERBOSE] /hive: Normalized ${p.type} URL to repo URL: ${normalizedArgs[0]}`);
|
|
938
895
|
} else if (validation.normalizedUrl && validation.normalizedUrl !== userArgs[0]) normalizedArgs[0] = validation.normalizedUrl;
|
|
939
|
-
|
|
940
896
|
const { backend: hivePerCommandIsolation, filteredArgs: normalizedArgsWithoutIsolation } = extractIsolationFromArgs(normalizedArgs); // issue #1534
|
|
941
897
|
if (hivePerCommandIsolation && !isValidPerCommandIsolation(hivePerCommandIsolation)) {
|
|
942
898
|
await safeReply(ctx, t('telegram.invalid_isolation', { value: escapeMarkdown(hivePerCommandIsolation) }, { locale: hiveLocale }), { reply_to_message_id: ctx.message.message_id });
|
|
@@ -972,7 +928,6 @@ async function handleHiveCommand(ctx) {
|
|
|
972
928
|
await safeReply(ctx, `ā ${escapeMarkdown(hiveBranchError)}`, { reply_to_message_id: ctx.message.message_id });
|
|
973
929
|
return;
|
|
974
930
|
}
|
|
975
|
-
|
|
976
931
|
// Validate merged arguments using hive's yargs config
|
|
977
932
|
try {
|
|
978
933
|
await parseArgsWithYargs(args, yargs, createHiveYargsConfig);
|
|
@@ -995,7 +950,6 @@ async function handleHiveCommand(ctx) {
|
|
|
995
950
|
optionsRaw: userOptionsRaw ? escapeMarkdown(userOptionsRaw) : '',
|
|
996
951
|
lockedOptions: hiveOverrides.length > 0 ? escapeMarkdown(hiveOverrides.join(' ')) : '',
|
|
997
952
|
});
|
|
998
|
-
|
|
999
953
|
// Issue #594: see /solve handler.
|
|
1000
954
|
let hiveLimitsAtStart = null;
|
|
1001
955
|
if (hiveShowLimits) ({ infoBlock, limitsAtStart: hiveLimitsAtStart } = await captureStartSnapshotAndAppend({ infoBlock, tool: hiveTool, verbose: VERBOSE, limitsLib, commandLabel: '/hive', locale: hiveLocale }));
|
|
@@ -1005,7 +959,6 @@ async function handleHiveCommand(ctx) {
|
|
|
1005
959
|
const hiveArgsWithLocale = injectLanguageIfMissing(args, hiveLocale);
|
|
1006
960
|
await executeAndUpdateMessage(ctx, startingMessage, 'hive', hiveArgsWithLocale, infoBlock, effectiveHiveIsolation, hiveTool, null, { showLimits: hiveShowLimits, limitsAtStart: hiveLimitsAtStart, locale: hiveLocale });
|
|
1007
961
|
}
|
|
1008
|
-
|
|
1009
962
|
bot.command(/^hive$/i, handleHiveCommand);
|
|
1010
963
|
|
|
1011
964
|
const { registerTopCommand } = await import('./telegram-top-command.lib.mjs');
|
|
@@ -1015,7 +968,6 @@ registerTopCommand(bot, sharedCommandOpts);
|
|
|
1015
968
|
registerStartStopCommands(bot, { ...sharedCommandOpts, getSolveQueue, findRunningSessionByUrl: (url, verbose) => findStoppableSessionByUrl(url, verbose) });
|
|
1016
969
|
await registerLogCommand(bot, sharedCommandOpts);
|
|
1017
970
|
await registerTerminalWatchCommand(bot, sharedCommandOpts);
|
|
1018
|
-
|
|
1019
971
|
// Issue #1745: hidden /tokens command for chat owners (private DMs only,
|
|
1020
972
|
// undocumented, masked output). Lets operators audit which local tokens are
|
|
1021
973
|
// live in the bot's environment so they can search for accidental leaks.
|
|
@@ -1055,7 +1007,6 @@ registerLeakNotifier(async ({ owner, repo, prNumber, tokenHits = [] }) => {
|
|
|
1055
1007
|
}
|
|
1056
1008
|
}
|
|
1057
1009
|
});
|
|
1058
|
-
|
|
1059
1010
|
// Add message listener for verbose debugging
|
|
1060
1011
|
if (VERBOSE) {
|
|
1061
1012
|
bot.on('message', (ctx, next) => {
|
|
@@ -1103,7 +1054,6 @@ if (VERBOSE) {
|
|
|
1103
1054
|
bot.on('message', async (ctx, next) => {
|
|
1104
1055
|
const text = ctx.message?.text;
|
|
1105
1056
|
if (!text) return next();
|
|
1106
|
-
|
|
1107
1057
|
// Extract command from text using the testable filter function
|
|
1108
1058
|
// Note: We pass null for botUsername here and check it separately with ctx.me
|
|
1109
1059
|
// which is set by Telegraf after bot initialization
|
|
@@ -1117,7 +1067,6 @@ bot.on('message', async (ctx, next) => {
|
|
|
1117
1067
|
return next(); // Command is for a different bot or we can't verify
|
|
1118
1068
|
}
|
|
1119
1069
|
}
|
|
1120
|
-
|
|
1121
1070
|
// /subscribe + /unsubscribe (#1688) are intentionally not in the text fallback ā Telegraf's bot.command() is sufficient.
|
|
1122
1071
|
const solveHandlers = Object.fromEntries(SOLVE_COMMAND_NAMES.map(command => [command, handleSolveCommand]));
|
|
1123
1072
|
const taskHandlers = Object.fromEntries(TASK_COMMAND_NAMES.map(command => [command, handleTaskCommand]));
|
|
@@ -1126,13 +1075,11 @@ bot.on('message', async (ctx, next) => {
|
|
|
1126
1075
|
|
|
1127
1076
|
const handler = handlers[extracted.command];
|
|
1128
1077
|
if (!handler) return next();
|
|
1129
|
-
|
|
1130
1078
|
// Log that fallback was triggered - this indicates bot.command() entity matching failed
|
|
1131
1079
|
console.warn(`[WARNING] Command /${extracted.command} matched by text fallback, not by entity-based bot.command(). ` + `Entities: ${JSON.stringify(ctx.message.entities || [])}. ` + `User: ${ctx.from?.username || ctx.from?.id}. ` + `This may indicate a Telegram client entity issue (issue #1207).`);
|
|
1132
1080
|
|
|
1133
1081
|
await handler(ctx);
|
|
1134
1082
|
});
|
|
1135
|
-
|
|
1136
1083
|
// Add global error handler for uncaught errors in middleware
|
|
1137
1084
|
bot.catch((error, ctx) => {
|
|
1138
1085
|
console.error('Unhandled error while processing update', ctx.update.update_id);
|
|
@@ -1163,14 +1110,12 @@ bot.catch((error, ctx) => {
|
|
|
1163
1110
|
username: ctx.from?.username,
|
|
1164
1111
|
},
|
|
1165
1112
|
});
|
|
1166
|
-
|
|
1167
1113
|
// Try to notify the user about the error with more details
|
|
1168
1114
|
if (ctx?.reply) {
|
|
1169
1115
|
const isTelegramParsingError = isTelegramFormattingError(error);
|
|
1170
1116
|
const isTelegramTextLimitError = isTelegramMessageTooLongError(error);
|
|
1171
1117
|
|
|
1172
1118
|
let errorMessage;
|
|
1173
|
-
|
|
1174
1119
|
if (isTelegramParsingError || isTelegramTextLimitError) {
|
|
1175
1120
|
// Issue #1460: Log detailed context for root cause analysis (always logged, not just in verbose mode)
|
|
1176
1121
|
const userInfo = ctx.from ? { id: ctx.from.id, username: ctx.from.username, first_name: ctx.from.first_name, last_name: ctx.from.last_name } : 'unknown';
|
|
@@ -1217,7 +1162,6 @@ bot.catch((error, ctx) => {
|
|
|
1217
1162
|
});
|
|
1218
1163
|
}
|
|
1219
1164
|
});
|
|
1220
|
-
|
|
1221
1165
|
// Track shutdown state to prevent startup messages after shutdown
|
|
1222
1166
|
let isShuttingDown = false;
|
|
1223
1167
|
|
|
@@ -1239,7 +1183,6 @@ if (VERBOSE) {
|
|
|
1239
1183
|
console.log('[VERBOSE] Bot start time (Unix):', BOT_START_TIME);
|
|
1240
1184
|
console.log('[VERBOSE] Bot start time (ISO):', new Date(BOT_START_TIME * 1000).toISOString());
|
|
1241
1185
|
}
|
|
1242
|
-
|
|
1243
1186
|
// Launch bot with retry logic (issue #1240: handle 409 Conflict with exponential backoff)
|
|
1244
1187
|
// The launcher handles deleteWebhook + bot.launch() with retry on transient errors.
|
|
1245
1188
|
// Non-retryable errors (401 Unauthorized) cause immediate exit.
|
|
@@ -1254,7 +1197,6 @@ function startSessionMonitoringOnce() {
|
|
|
1254
1197
|
// working session when `--on-session-kill=resume` is in effect.
|
|
1255
1198
|
sessionMonitoringTimer = startSessionMonitoring(bot, VERBOSE, 30000, { isolationRunner });
|
|
1256
1199
|
}
|
|
1257
|
-
|
|
1258
1200
|
// Issue #2146 (PR #2147 review): stop the Formal AI sidecar once no Formal AI
|
|
1259
1201
|
// task holds a lease, then ā while the host is idle ā update its image (with a
|
|
1260
1202
|
// non-destructive memory migration) and refresh the agentic CLIs. Every step is
|
|
@@ -1273,7 +1215,6 @@ function startFormalAiMaintenanceOnce() {
|
|
|
1273
1215
|
// time the bot was alive" is always discoverable from the log. The heartbeat
|
|
1274
1216
|
// logic lives in bot-lifecycle.lib.mjs so it can be unit tested.
|
|
1275
1217
|
const heartbeat = createHeartbeat({ logger: botLogger, getActiveSessionCount });
|
|
1276
|
-
|
|
1277
1218
|
async function onBotLaunched() {
|
|
1278
1219
|
if (isShuttingDown || launchAnnouncementShown) return;
|
|
1279
1220
|
launchAnnouncementShown = true;
|
|
@@ -1281,7 +1222,6 @@ async function onBotLaunched() {
|
|
|
1281
1222
|
console.log('ā
SwarmMindBot is now running!');
|
|
1282
1223
|
console.log('Press Ctrl+C to stop');
|
|
1283
1224
|
botLogger.event('bot_launched', { pid: process.pid, botStartTime: BOT_START_TIME });
|
|
1284
|
-
|
|
1285
1225
|
// Issue #1927 (requirements #2/#4): after a restart, reload sessions that were
|
|
1286
1226
|
// still being tracked when the previous process died and re-register them so
|
|
1287
1227
|
// the monitor resumes watching ā and finally reports any that were killed while
|
|
@@ -1292,7 +1232,6 @@ async function onBotLaunched() {
|
|
|
1292
1232
|
startSessionMonitoringOnce();
|
|
1293
1233
|
startFormalAiMaintenanceOnce();
|
|
1294
1234
|
heartbeat.start();
|
|
1295
|
-
|
|
1296
1235
|
if (VERBOSE) {
|
|
1297
1236
|
console.log('[VERBOSE] Bot launched successfully');
|
|
1298
1237
|
console.log('[VERBOSE] Polling is active, waiting for messages...');
|
|
@@ -1301,7 +1240,6 @@ async function onBotLaunched() {
|
|
|
1301
1240
|
try {
|
|
1302
1241
|
const botInfo = await bot.telegram.getMe();
|
|
1303
1242
|
const webhookInfo = await bot.telegram.getWebhookInfo();
|
|
1304
|
-
|
|
1305
1243
|
console.log('[VERBOSE] Bot info:');
|
|
1306
1244
|
console.log('[VERBOSE] Username: @' + botInfo.username);
|
|
1307
1245
|
console.log('[VERBOSE] Bot ID:', botInfo.id);
|
|
@@ -1326,7 +1264,6 @@ async function onBotLaunched() {
|
|
|
1326
1264
|
} catch (err) {
|
|
1327
1265
|
console.log('[VERBOSE] Could not fetch bot info:', err.message);
|
|
1328
1266
|
}
|
|
1329
|
-
|
|
1330
1267
|
console.log('[VERBOSE] Send a message to the bot to test message reception');
|
|
1331
1268
|
}
|
|
1332
1269
|
}
|
|
@@ -1335,7 +1272,6 @@ async function onBotLaunched() {
|
|
|
1335
1272
|
// session map is empty until commands are received, but bot.launch() may stay
|
|
1336
1273
|
// pending while polling is active.
|
|
1337
1274
|
startSessionMonitoringOnce();
|
|
1338
|
-
|
|
1339
1275
|
launchBotWithRetry(
|
|
1340
1276
|
bot,
|
|
1341
1277
|
{
|
|
@@ -1372,7 +1308,6 @@ const stopSolveQueue = () => {
|
|
|
1372
1308
|
/* ignore errors during shutdown */
|
|
1373
1309
|
}
|
|
1374
1310
|
};
|
|
1375
|
-
|
|
1376
1311
|
// Issue #1927: record the shutdown (with a timestamp) so the log shows the bot
|
|
1377
1312
|
// stopped cleanly ā the ABSENCE of this line before the next startup is how a
|
|
1378
1313
|
// later analysis tells an orderly stop apart from a hard kill. The handler lives
|
|
@@ -1399,7 +1334,6 @@ process.once('SIGINT', () => {
|
|
|
1399
1334
|
console.log('\nš Received SIGINT (Ctrl+C), stopping bot...');
|
|
1400
1335
|
handleShutdownSignal('SIGINT');
|
|
1401
1336
|
});
|
|
1402
|
-
|
|
1403
1337
|
process.once('SIGTERM', () => {
|
|
1404
1338
|
console.log('\nš Received SIGTERM, stopping bot... (Check system logs: journalctl -u <service> or dmesg)');
|
|
1405
1339
|
handleShutdownSignal('SIGTERM');
|