@link-assistant/hive-mind 2.11.13 → 2.12.1

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +4 -1
  3. package/src/agent-command.lib.mjs +74 -0
  4. package/src/agent.lib.mjs +59 -34
  5. package/src/agentic-cli-updater.lib.mjs +241 -0
  6. package/src/claude.connection.lib.mjs +209 -0
  7. package/src/claude.lib.mjs +6 -202
  8. package/src/codex.lib.mjs +0 -128
  9. package/src/formal-ai-isolation.lib.mjs +62 -0
  10. package/src/formal-ai-maintenance.lib.mjs +106 -0
  11. package/src/formal-ai-model.lib.mjs +25 -0
  12. package/src/formal-ai-runtime.lib.mjs +10 -0
  13. package/src/formal-ai-sidecar.lib.mjs +565 -0
  14. package/src/formal-ai-updater.lib.mjs +294 -0
  15. package/src/formal-ai-version.lib.mjs +100 -0
  16. package/src/formal-ai.lib.mjs +11 -16
  17. package/src/github-rate-limit.lib.mjs +3 -0
  18. package/src/github-url-parser.lib.mjs +255 -0
  19. package/src/github.lib.mjs +22 -343
  20. package/src/hive.mjs +0 -152
  21. package/src/interactive-mode.lib.mjs +0 -43
  22. package/src/isolation-runner.lib.mjs +44 -173
  23. package/src/limits.lib.mjs +0 -89
  24. package/src/model-args.lib.mjs +32 -0
  25. package/src/models/index.mjs +5 -19
  26. package/src/session-monitor.lib.mjs +14 -172
  27. package/src/solve.auto-merge.lib.mjs +70 -164
  28. package/src/solve.mjs +31 -193
  29. package/src/solve.repository.lib.mjs +0 -83
  30. package/src/solve.results.lib.mjs +2 -92
  31. package/src/solve.session.lib.mjs +52 -19
  32. package/src/solve.tool-uncommitted.lib.mjs +22 -0
  33. package/src/state-lock.lib.mjs +82 -0
  34. package/src/telegram-bot.mjs +17 -65
  35. package/src/telegram-fix-command.lib.mjs +1 -8
  36. package/src/telegram-merge-queue.lib.mjs +3 -155
  37. package/src/telegram-solve-queue.lib.mjs +9 -168
  38. package/src/telegram-task-command.lib.mjs +1 -8
  39. package/src/use-m-bootstrap.lib.mjs +6 -5
  40. package/src/use-with-retry.lib.mjs +128 -2
  41. package/src/working-session-summary.lib.mjs +47 -1
@@ -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
 
@@ -263,7 +254,7 @@ const { createSessionStore } = await import('./session-store.lib.mjs');
263
254
  const { createHeartbeat, resumeSessionsOnLaunch, createShutdownHandler } = await import('./bot-lifecycle.lib.mjs');
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
258
  // Initialize Sentry for error tracking
268
259
  await initializeSentry({
269
260
  debug: VERBOSE,
@@ -275,7 +266,6 @@ await initializeSentry({
275
266
  const { initI18n, t, preloadAllLocales, resolveLocaleFromTelegramCtx } = await import('./i18n.lib.mjs');
276
267
  await initI18n();
277
268
  await preloadAllLocales();
278
-
279
269
  const telegrafModule = await use('telegraf');
280
270
  const { Telegraf } = telegrafModule;
281
271
  const bot = new Telegraf(BOT_TOKEN, {
@@ -301,7 +291,6 @@ botLogger.event('bot_starting', { pid: process.pid, ppid: process.ppid, botStart
301
291
  function isChatAuthorized(chatId) {
302
292
  return _isChatAuthorized(chatId, allowedChats);
303
293
  }
304
-
305
294
  // Topic-level authorization (issue #1100): chat-level auth overrides topic-level
306
295
  function isTopicAuthorized(ctx) {
307
296
  if (isChatAuthorized(ctx.chat?.id)) return true;
@@ -321,7 +310,6 @@ function buildAuthErrorMessage(ctx) {
321
310
  function isOldMessage(ctx) {
322
311
  return _isOldMessage(ctx, BOT_START_TIME, { verbose: VERBOSE });
323
312
  }
324
-
325
313
  async function executeStartScreen(command, args) {
326
314
  return executeStartScreenCommand(command, args, { verbose: VERBOSE });
327
315
  }
@@ -329,7 +317,6 @@ async function executeStartScreen(command, args) {
329
317
  function isForwardedOrReply(ctx) {
330
318
  return _isForwardedOrReply(ctx, { verbose: VERBOSE });
331
319
  }
332
-
333
320
  // Forwarded-only check (issue #1922). Commands that support the reply feature
334
321
  // (e.g. /task, /solve) must still reject forwarded commands without rejecting
335
322
  // genuine user replies, so they use this instead of isForwardedOrReply.
@@ -370,7 +357,6 @@ function validateModelInArgs(args, tool = 'claude') {
370
357
  }
371
358
  return null;
372
359
  }
373
-
374
360
  // Inject --language LOCALE into spawn args if no language flag is already present.
375
361
  // Issue #378: telegram bot resolves the user's effective locale and propagates
376
362
  // it to spawned solve/hive sessions so the AI tool replies in the same language.
@@ -390,7 +376,6 @@ async function getCommandUrlArg(args, createYargsConfig, positionalNames) {
390
376
  if (parsedUrl) return parsedUrl;
391
377
  return args.find(arg => cleanNonPrintableChars(arg).includes('github.com')) || (args[0] && !args[0].startsWith('-') ? args[0] : null);
392
378
  }
393
-
394
379
  async function validateGitHubUrl(args, options = {}) {
395
380
  const { allowedTypes = ['issue', 'pull'], commandName = 'solve', createYargsConfig = null, positionalNames = [], locale = null } = options;
396
381
  const rawUrl = await getCommandUrlArg(args, createYargsConfig, positionalNames);
@@ -416,7 +401,6 @@ async function validateGitHubUrl(args, options = {}) {
416
401
  }
417
402
 
418
403
  const executeAndUpdateMessage = buildExecuteAndUpdateMessage({ resolveIsolation, ISOLATION_BACKEND, isolationRunner, VERBOSE, executeStartScreen, trackSession, untrackSession, AUTO_WATCH_MESSAGE, startAutoTerminalWatchForSession, bot, formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage });
419
-
420
404
  bot.command('help', async ctx => {
421
405
  VERBOSE && console.log('[VERBOSE] /help command received');
422
406
 
@@ -425,7 +409,6 @@ bot.command('help', async ctx => {
425
409
  VERBOSE && console.log('[VERBOSE] /help ignored: old message');
426
410
  return;
427
411
  }
428
-
429
412
  // Ignore forwarded or reply messages
430
413
  if (isForwardedOrReply(ctx)) {
431
414
  VERBOSE && console.log('[VERBOSE] /help ignored: forwarded or reply');
@@ -463,13 +446,11 @@ bot.command('help', async ctx => {
463
446
  authorized,
464
447
  allowTopicHint: topicId ? `TELEGRAM_ALLOWED_TOPICS="(${chatId} ${topicId})"` : '',
465
448
  });
466
-
467
449
  await safeReply(ctx, message, { fallbackLocale: helpLocale });
468
450
  });
469
451
 
470
452
  bot.command('limits', async ctx => {
471
453
  VERBOSE && console.log('[VERBOSE] /limits command received');
472
-
473
454
  // Add breadcrumb for error tracking
474
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 } });
475
456
 
@@ -478,7 +459,6 @@ bot.command('limits', async ctx => {
478
459
  VERBOSE && console.log('[VERBOSE] /limits ignored: old message');
479
460
  return;
480
461
  }
481
-
482
462
  // Ignore forwarded or reply messages
483
463
  if (isForwardedOrReply(ctx)) {
484
464
  VERBOSE && console.log('[VERBOSE] /limits ignored: forwarded or reply');
@@ -493,7 +473,6 @@ bot.command('limits', async ctx => {
493
473
  await ctx.reply(t('telegram.limits_only_in_groups', {}, { locale: userLocale }), { reply_to_message_id: ctx.message.message_id });
494
474
  return;
495
475
  }
496
-
497
476
  if (!isTopicAuthorized(ctx)) {
498
477
  if (VERBOSE) {
499
478
  console.log('[VERBOSE] /limits ignored: not authorized');
@@ -509,7 +488,6 @@ bot.command('limits', async ctx => {
509
488
 
510
489
  // Get all limits using shared cache (3min for API, 2min for system)
511
490
  const limits = await getAllCachedLimits(VERBOSE);
512
-
513
491
  // Format message with usage limits and queue status (issues #1343, #1267)
514
492
  const claudeError = limits.claude.success ? null : limits.claude.error;
515
493
  const codexError = limits.codex.success ? null : limits.codex.error;
@@ -543,7 +521,6 @@ bot.command('version', async ctx => {
543
521
 
544
522
  const { registerLanguageCommand } = await import('./telegram-language-command.lib.mjs');
545
523
  registerLanguageCommand(bot, { VERBOSE, isOldMessage, isForwardedOrReply });
546
-
547
524
  const { registerAcceptInvitesCommand } = await import('./telegram-accept-invitations.lib.mjs');
548
525
  const sharedCommandOpts = { VERBOSE, isOldMessage, isForwarded, isForwardedOrReply, isGroupChat: _isGroupChat, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage, addBreadcrumb, isChatStopped, getStoppedChatRejectMessage };
549
526
  registerAcceptInvitesCommand(bot, sharedCommandOpts);
@@ -565,7 +542,6 @@ async function handleSolveCommand(ctx) {
565
542
  const solveCommandName = getSolveCommandNameFromText(ctx.message?.text) || 'solve';
566
543
  const solveCommandDisplay = `/${solveCommandName}`;
567
544
  VERBOSE && console.log(`[VERBOSE] ${solveCommandDisplay} command received`);
568
-
569
545
  // Add breadcrumb for error tracking
570
546
  await addBreadcrumb({
571
547
  category: 'telegram.command',
@@ -587,7 +563,6 @@ async function handleSolveCommand(ctx) {
587
563
  await ctx.reply(t('telegram.solve_disabled', {}, { locale: solveLocale }));
588
564
  return;
589
565
  }
590
-
591
566
  // Ignore messages sent before bot started
592
567
  if (isOldMessage(ctx)) {
593
568
  if (VERBOSE) {
@@ -605,7 +580,6 @@ async function handleSolveCommand(ctx) {
605
580
  }
606
581
  return;
607
582
  }
608
-
609
583
  if (!_isGroupChat(ctx)) {
610
584
  if (VERBOSE) {
611
585
  console.log(`[VERBOSE] ${solveCommandDisplay} ignored: not a group chat`);
@@ -621,7 +595,6 @@ async function handleSolveCommand(ctx) {
621
595
  await ctx.reply(buildAuthErrorMessage(ctx), { reply_to_message_id: ctx.message.message_id });
622
596
  return;
623
597
  }
624
-
625
598
  // Check if chat is stopped (issue #1081) - reject with same style as queue rejected mode
626
599
  const chatId = ctx.chat.id;
627
600
  if (isChatStopped(chatId)) {
@@ -631,7 +604,6 @@ async function handleSolveCommand(ctx) {
631
604
  }
632
605
 
633
606
  VERBOSE && console.log(`[VERBOSE] ${solveCommandDisplay} passed all checks, executing...`);
634
-
635
607
  const solveToolAlias = getSolveToolAliasFromText(ctx.message.text);
636
608
  let userArgs = parseCommandArgs(ctx.message.text);
637
609
 
@@ -640,7 +612,6 @@ async function handleSolveCommand(ctx) {
640
612
  if (solveSL.handled) return;
641
613
  const solveShowLimits = solveSL.showLimits;
642
614
  userArgs = solveSL.args;
643
-
644
615
  // Check if this is a reply to a message and user didn't provide URL as first argument
645
616
  // In that case, try to extract GitHub URL from the replied message
646
617
  // Issue #1325: Support all options via /solve command when replying (e.g., "/solve --model opus")
@@ -650,7 +621,6 @@ async function handleSolveCommand(ctx) {
650
621
  const commandUrlArg = await getCommandUrlArg(userArgs, createSolveYargsConfig, ['issue-url']);
651
622
  const commandUrlText = commandUrlArg ? cleanNonPrintableChars(commandUrlArg) : '';
652
623
  const commandHasUrl = commandUrlText.includes('github.com') || /^https?:\/\//.test(commandUrlText);
653
-
654
624
  if (isReply && !commandHasUrl) {
655
625
  if (VERBOSE) {
656
626
  console.log('[VERBOSE] /solve is a reply without URL in args, extracting from replied message...');
@@ -659,7 +629,6 @@ async function handleSolveCommand(ctx) {
659
629
 
660
630
  const replyText = message.reply_to_message.text || '';
661
631
  const extraction = _extractGitHubUrl(replyText, { parseGitHubUrl, cleanNonPrintableChars });
662
-
663
632
  if (extraction.error) {
664
633
  // Multiple links found
665
634
  if (VERBOSE) {
@@ -687,7 +656,6 @@ async function handleSolveCommand(ctx) {
687
656
  }
688
657
 
689
658
  userArgs = applySolveToolAlias(userArgs, solveToolAlias);
690
-
691
659
  const { malformed, errors: malformedErrors } = detectMalformedFlags(userArgs);
692
660
  if (malformed.length > 0) {
693
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 });
@@ -727,7 +695,6 @@ async function handleSolveCommand(ctx) {
727
695
  solveTool = args[i].substring('--tool='.length);
728
696
  }
729
697
  }
730
-
731
698
  // Validate model name with helpful error message (before yargs validation)
732
699
  const modelError = validateModelInArgs(args, solveTool);
733
700
  if (modelError) {
@@ -787,7 +754,6 @@ async function handleSolveCommand(ctx) {
787
754
  lockedOptions: solveOverrides.length > 0 ? escapeMarkdown(solveOverrides.join(' ')) : '',
788
755
  });
789
756
  const solveQueue = getSolveQueue({ verbose: VERBOSE });
790
-
791
757
  // Check for duplicate URL in queue (issue #1080)
792
758
  const existingItem = solveQueue.findByUrl(normalizedUrl);
793
759
  if (existingItem) {
@@ -812,7 +778,6 @@ async function handleSolveCommand(ctx) {
812
778
 
813
779
  // Issue #1688: parsed URL context lets the completion message look up linked PRs.
814
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;
815
-
816
781
  const toolQueuedCount = queueStats.queuedByTool[solveTool] || 0; // tool-specific queue count (#1551)
817
782
  // Issue #378: propagate user's effective Telegram locale to the spawned solve session.
818
783
  const argsWithLocale = injectLanguageIfMissing(args, solveLocale);
@@ -820,7 +785,6 @@ async function handleSolveCommand(ctx) {
820
785
  // Issue #594: append "Limits at start" to infoBlock; thread snapshot via sessionInfo.
821
786
  let solveLimitsAtStart = null;
822
787
  if (solveShowLimits) ({ infoBlock, limitsAtStart: solveLimitsAtStart } = await captureStartSnapshotAndAppend({ infoBlock, tool: solveTool, verbose: VERBOSE, limitsLib, commandLabel: '/solve', locale: solveLocale }));
823
-
824
788
  if (check.canStart && check.startReserved) {
825
789
  const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock, locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
826
790
  await executeAndUpdateMessage(ctx, startingMessage, 'solve', argsWithLocale, infoBlock, effectiveSolveIsolation, solveTool, solveUrlContext, { showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart, locale: solveLocale, commandAlias: solveCommandName });
@@ -840,7 +804,6 @@ bot.command(
840
804
  SOLVE_COMMAND_NAMES.map(command => new RegExp(`^${command}$`, 'i')),
841
805
  handleSolveCommand
842
806
  );
843
-
844
807
  // Named handler for /hive command - extracted for reuse by text-based fallback (issue #1207)
845
808
  async function handleHiveCommand(ctx) {
846
809
  if (VERBOSE) {
@@ -859,7 +822,6 @@ async function handleHiveCommand(ctx) {
859
822
  username: ctx.from?.username,
860
823
  },
861
824
  });
862
-
863
825
  const hiveLocale = resolveLocaleFromTelegramCtx(ctx);
864
826
  if (!hiveEnabled) {
865
827
  if (VERBOSE) {
@@ -876,7 +838,6 @@ async function handleHiveCommand(ctx) {
876
838
  }
877
839
  return;
878
840
  }
879
-
880
841
  // Ignore forwarded or reply messages
881
842
  if (isForwardedOrReply(ctx)) {
882
843
  if (VERBOSE) {
@@ -892,7 +853,6 @@ async function handleHiveCommand(ctx) {
892
853
  await ctx.reply(t('telegram.hive_only_in_groups', {}, { locale: hiveLocale }), { reply_to_message_id: ctx.message.message_id });
893
854
  return;
894
855
  }
895
-
896
856
  if (!isTopicAuthorized(ctx)) {
897
857
  if (VERBOSE) {
898
858
  console.log('[VERBOSE] /hive ignored: not authorized');
@@ -908,11 +868,9 @@ async function handleHiveCommand(ctx) {
908
868
  await safeReply(ctx, getStoppedChatRejectMessage(chatId, 'Hive'), { reply_to_message_id: ctx.message.message_id });
909
869
  return;
910
870
  }
911
-
912
871
  VERBOSE && console.log('[VERBOSE] /hive passed all checks, executing...');
913
872
 
914
873
  let userArgs = parseCommandArgs(ctx.message.text);
915
-
916
874
  // Issue #594: see /solve handler.
917
875
  const hiveSL = await handleShowLimitsFlag({ ctx, safeReply, args: userArgs, enabled: SHOW_LIMITS_ENABLED, locale: hiveLocale });
918
876
  if (hiveSL.handled) return;
@@ -935,7 +893,6 @@ async function handleHiveCommand(ctx) {
935
893
  normalizedArgs[0] = `https://github.com/${p.owner}/${p.repo}`;
936
894
  if (VERBOSE) console.log(`[VERBOSE] /hive: Normalized ${p.type} URL to repo URL: ${normalizedArgs[0]}`);
937
895
  } else if (validation.normalizedUrl && validation.normalizedUrl !== userArgs[0]) normalizedArgs[0] = validation.normalizedUrl;
938
-
939
896
  const { backend: hivePerCommandIsolation, filteredArgs: normalizedArgsWithoutIsolation } = extractIsolationFromArgs(normalizedArgs); // issue #1534
940
897
  if (hivePerCommandIsolation && !isValidPerCommandIsolation(hivePerCommandIsolation)) {
941
898
  await safeReply(ctx, t('telegram.invalid_isolation', { value: escapeMarkdown(hivePerCommandIsolation) }, { locale: hiveLocale }), { reply_to_message_id: ctx.message.message_id });
@@ -971,7 +928,6 @@ async function handleHiveCommand(ctx) {
971
928
  await safeReply(ctx, `āŒ ${escapeMarkdown(hiveBranchError)}`, { reply_to_message_id: ctx.message.message_id });
972
929
  return;
973
930
  }
974
-
975
931
  // Validate merged arguments using hive's yargs config
976
932
  try {
977
933
  await parseArgsWithYargs(args, yargs, createHiveYargsConfig);
@@ -994,7 +950,6 @@ async function handleHiveCommand(ctx) {
994
950
  optionsRaw: userOptionsRaw ? escapeMarkdown(userOptionsRaw) : '',
995
951
  lockedOptions: hiveOverrides.length > 0 ? escapeMarkdown(hiveOverrides.join(' ')) : '',
996
952
  });
997
-
998
953
  // Issue #594: see /solve handler.
999
954
  let hiveLimitsAtStart = null;
1000
955
  if (hiveShowLimits) ({ infoBlock, limitsAtStart: hiveLimitsAtStart } = await captureStartSnapshotAndAppend({ infoBlock, tool: hiveTool, verbose: VERBOSE, limitsLib, commandLabel: '/hive', locale: hiveLocale }));
@@ -1004,7 +959,6 @@ async function handleHiveCommand(ctx) {
1004
959
  const hiveArgsWithLocale = injectLanguageIfMissing(args, hiveLocale);
1005
960
  await executeAndUpdateMessage(ctx, startingMessage, 'hive', hiveArgsWithLocale, infoBlock, effectiveHiveIsolation, hiveTool, null, { showLimits: hiveShowLimits, limitsAtStart: hiveLimitsAtStart, locale: hiveLocale });
1006
961
  }
1007
-
1008
962
  bot.command(/^hive$/i, handleHiveCommand);
1009
963
 
1010
964
  const { registerTopCommand } = await import('./telegram-top-command.lib.mjs');
@@ -1014,7 +968,6 @@ registerTopCommand(bot, sharedCommandOpts);
1014
968
  registerStartStopCommands(bot, { ...sharedCommandOpts, getSolveQueue, findRunningSessionByUrl: (url, verbose) => findStoppableSessionByUrl(url, verbose) });
1015
969
  await registerLogCommand(bot, sharedCommandOpts);
1016
970
  await registerTerminalWatchCommand(bot, sharedCommandOpts);
1017
-
1018
971
  // Issue #1745: hidden /tokens command for chat owners (private DMs only,
1019
972
  // undocumented, masked output). Lets operators audit which local tokens are
1020
973
  // live in the bot's environment so they can search for accidental leaks.
@@ -1054,7 +1007,6 @@ registerLeakNotifier(async ({ owner, repo, prNumber, tokenHits = [] }) => {
1054
1007
  }
1055
1008
  }
1056
1009
  });
1057
-
1058
1010
  // Add message listener for verbose debugging
1059
1011
  if (VERBOSE) {
1060
1012
  bot.on('message', (ctx, next) => {
@@ -1102,7 +1054,6 @@ if (VERBOSE) {
1102
1054
  bot.on('message', async (ctx, next) => {
1103
1055
  const text = ctx.message?.text;
1104
1056
  if (!text) return next();
1105
-
1106
1057
  // Extract command from text using the testable filter function
1107
1058
  // Note: We pass null for botUsername here and check it separately with ctx.me
1108
1059
  // which is set by Telegraf after bot initialization
@@ -1116,7 +1067,6 @@ bot.on('message', async (ctx, next) => {
1116
1067
  return next(); // Command is for a different bot or we can't verify
1117
1068
  }
1118
1069
  }
1119
-
1120
1070
  // /subscribe + /unsubscribe (#1688) are intentionally not in the text fallback — Telegraf's bot.command() is sufficient.
1121
1071
  const solveHandlers = Object.fromEntries(SOLVE_COMMAND_NAMES.map(command => [command, handleSolveCommand]));
1122
1072
  const taskHandlers = Object.fromEntries(TASK_COMMAND_NAMES.map(command => [command, handleTaskCommand]));
@@ -1125,13 +1075,11 @@ bot.on('message', async (ctx, next) => {
1125
1075
 
1126
1076
  const handler = handlers[extracted.command];
1127
1077
  if (!handler) return next();
1128
-
1129
1078
  // Log that fallback was triggered - this indicates bot.command() entity matching failed
1130
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).`);
1131
1080
 
1132
1081
  await handler(ctx);
1133
1082
  });
1134
-
1135
1083
  // Add global error handler for uncaught errors in middleware
1136
1084
  bot.catch((error, ctx) => {
1137
1085
  console.error('Unhandled error while processing update', ctx.update.update_id);
@@ -1162,14 +1110,12 @@ bot.catch((error, ctx) => {
1162
1110
  username: ctx.from?.username,
1163
1111
  },
1164
1112
  });
1165
-
1166
1113
  // Try to notify the user about the error with more details
1167
1114
  if (ctx?.reply) {
1168
1115
  const isTelegramParsingError = isTelegramFormattingError(error);
1169
1116
  const isTelegramTextLimitError = isTelegramMessageTooLongError(error);
1170
1117
 
1171
1118
  let errorMessage;
1172
-
1173
1119
  if (isTelegramParsingError || isTelegramTextLimitError) {
1174
1120
  // Issue #1460: Log detailed context for root cause analysis (always logged, not just in verbose mode)
1175
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';
@@ -1216,7 +1162,6 @@ bot.catch((error, ctx) => {
1216
1162
  });
1217
1163
  }
1218
1164
  });
1219
-
1220
1165
  // Track shutdown state to prevent startup messages after shutdown
1221
1166
  let isShuttingDown = false;
1222
1167
 
@@ -1238,12 +1183,12 @@ if (VERBOSE) {
1238
1183
  console.log('[VERBOSE] Bot start time (Unix):', BOT_START_TIME);
1239
1184
  console.log('[VERBOSE] Bot start time (ISO):', new Date(BOT_START_TIME * 1000).toISOString());
1240
1185
  }
1241
-
1242
1186
  // Launch bot with retry logic (issue #1240: handle 409 Conflict with exponential backoff)
1243
1187
  // The launcher handles deleteWebhook + bot.launch() with retry on transient errors.
1244
1188
  // Non-retryable errors (401 Unauthorized) cause immediate exit.
1245
1189
  const launchAbortController = new AbortController();
1246
1190
  let sessionMonitoringTimer = null;
1191
+ let formalAiMaintenance = null;
1247
1192
  let launchAnnouncementShown = false;
1248
1193
 
1249
1194
  function startSessionMonitoringOnce() {
@@ -1252,12 +1197,24 @@ function startSessionMonitoringOnce() {
1252
1197
  // working session when `--on-session-kill=resume` is in effect.
1253
1198
  sessionMonitoringTimer = startSessionMonitoring(bot, VERBOSE, 30000, { isolationRunner });
1254
1199
  }
1200
+ // Issue #2146 (PR #2147 review): stop the Formal AI sidecar once no Formal AI
1201
+ // task holds a lease, then — while the host is idle — update its image (with a
1202
+ // non-destructive memory migration) and refresh the agentic CLIs. Every step is
1203
+ // best-effort and never blocks the bot.
1204
+ function startFormalAiMaintenanceOnce() {
1205
+ if (formalAiMaintenance) return;
1206
+ formalAiMaintenance = startFormalAiMaintenance({
1207
+ verbose: VERBOSE,
1208
+ log: async message => {
1209
+ console.log(message);
1210
+ },
1211
+ });
1212
+ }
1255
1213
 
1256
1214
  // Issue #1927 (requirements #3/#4): a periodic timestamped heartbeat so the "last
1257
1215
  // time the bot was alive" is always discoverable from the log. The heartbeat
1258
1216
  // logic lives in bot-lifecycle.lib.mjs so it can be unit tested.
1259
1217
  const heartbeat = createHeartbeat({ logger: botLogger, getActiveSessionCount });
1260
-
1261
1218
  async function onBotLaunched() {
1262
1219
  if (isShuttingDown || launchAnnouncementShown) return;
1263
1220
  launchAnnouncementShown = true;
@@ -1265,7 +1222,6 @@ async function onBotLaunched() {
1265
1222
  console.log('āœ… SwarmMindBot is now running!');
1266
1223
  console.log('Press Ctrl+C to stop');
1267
1224
  botLogger.event('bot_launched', { pid: process.pid, botStartTime: BOT_START_TIME });
1268
-
1269
1225
  // Issue #1927 (requirements #2/#4): after a restart, reload sessions that were
1270
1226
  // still being tracked when the previous process died and re-register them so
1271
1227
  // the monitor resumes watching — and finally reports any that were killed while
@@ -1274,8 +1230,8 @@ async function onBotLaunched() {
1274
1230
  await resumeSessionsOnLaunch({ resumeTrackedSessions, botStartTime: BOT_START_TIME, verbose: VERBOSE, logger: botLogger });
1275
1231
 
1276
1232
  startSessionMonitoringOnce();
1233
+ startFormalAiMaintenanceOnce();
1277
1234
  heartbeat.start();
1278
-
1279
1235
  if (VERBOSE) {
1280
1236
  console.log('[VERBOSE] Bot launched successfully');
1281
1237
  console.log('[VERBOSE] Polling is active, waiting for messages...');
@@ -1284,7 +1240,6 @@ async function onBotLaunched() {
1284
1240
  try {
1285
1241
  const botInfo = await bot.telegram.getMe();
1286
1242
  const webhookInfo = await bot.telegram.getWebhookInfo();
1287
-
1288
1243
  console.log('[VERBOSE] Bot info:');
1289
1244
  console.log('[VERBOSE] Username: @' + botInfo.username);
1290
1245
  console.log('[VERBOSE] Bot ID:', botInfo.id);
@@ -1309,7 +1264,6 @@ async function onBotLaunched() {
1309
1264
  } catch (err) {
1310
1265
  console.log('[VERBOSE] Could not fetch bot info:', err.message);
1311
1266
  }
1312
-
1313
1267
  console.log('[VERBOSE] Send a message to the bot to test message reception');
1314
1268
  }
1315
1269
  }
@@ -1318,7 +1272,6 @@ async function onBotLaunched() {
1318
1272
  // session map is empty until commands are received, but bot.launch() may stay
1319
1273
  // pending while polling is active.
1320
1274
  startSessionMonitoringOnce();
1321
-
1322
1275
  launchBotWithRetry(
1323
1276
  bot,
1324
1277
  {
@@ -1355,7 +1308,6 @@ const stopSolveQueue = () => {
1355
1308
  /* ignore errors during shutdown */
1356
1309
  }
1357
1310
  };
1358
-
1359
1311
  // Issue #1927: record the shutdown (with a timestamp) so the log shows the bot
1360
1312
  // stopped cleanly — the ABSENCE of this line before the next startup is how a
1361
1313
  // later analysis tells an orderly stop apart from a hard kill. The handler lives
@@ -1372,6 +1324,7 @@ const handleShutdownSignal = createShutdownHandler({
1372
1324
  cleanup: () => {
1373
1325
  launchAbortController.abort();
1374
1326
  if (sessionMonitoringTimer) clearInterval(sessionMonitoringTimer);
1327
+ formalAiMaintenance?.stop();
1375
1328
  heartbeat.stop();
1376
1329
  stopSolveQueue();
1377
1330
  },
@@ -1381,7 +1334,6 @@ process.once('SIGINT', () => {
1381
1334
  console.log('\nšŸ›‘ Received SIGINT (Ctrl+C), stopping bot...');
1382
1335
  handleShutdownSignal('SIGINT');
1383
1336
  });
1384
-
1385
1337
  process.once('SIGTERM', () => {
1386
1338
  console.log('\nšŸ›‘ Received SIGTERM, stopping bot... (Check system logs: journalctl -u <service> or dmesg)');
1387
1339
  handleShutdownSignal('SIGTERM');
@@ -10,6 +10,7 @@
10
10
  import { buildUserMention } from './buildUserMention.lib.mjs';
11
11
  import { validateModelName } from './models/index.mjs';
12
12
  import { parseFixRepository } from './fix.ci-cd.lib.mjs';
13
+ import { getModelFromArgs } from './model-args.lib.mjs';
13
14
  import { escapeMarkdown } from './telegram-markdown.lib.mjs';
14
15
  import { extractIsolationFromArgs, isValidPerCommandIsolation } from './telegram-isolation.lib.mjs';
15
16
  import { mergeArgsWithOverrides } from './args-overrides.lib.mjs';
@@ -47,14 +48,6 @@ export function getFixToolFromArgs(args) {
47
48
  return 'claude';
48
49
  }
49
50
 
50
- function getModelFromArgs(args) {
51
- for (let i = 0; i < args.length; i++) {
52
- if ((args[i] === '--model' || args[i] === '-m') && i + 1 < args.length) return args[i + 1];
53
- if (args[i].startsWith('--model=')) return args[i].substring('--model='.length);
54
- }
55
- return null;
56
- }
57
-
58
51
  function validateFixModel(args) {
59
52
  const model = getModelFromArgs(args);
60
53
  if (!model) return null;