@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.
@@ -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
- try {
105
- const { emoji, header, description } = getSessionCommentContent(sessionType, workStartTime);
106
- const startComment = `${emoji} **${header}**\n\n${description}`;
107
- const { ok, commentId, stderr } = await postTrackedComment({ $, owner: global.owner, repo: global.repo, targetNumber: prNumber, body: startComment });
108
- if (ok) {
109
- await log(formatAligned('šŸ’¬', 'Posted:', `${header} comment${commentId ? ` (id=${commentId})` : ''}`, 2));
110
- } else {
111
- await log(`Warning: Could not post work start comment: ${stderr || 'unknown error'}`, { level: 'warning' });
112
- }
113
- } catch (error) {
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
+ }
@@ -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');