@link-assistant/hive-mind 1.67.2 → 1.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/package.json +1 -1
- package/src/session-monitor.lib.mjs +31 -0
- package/src/start-screen.mjs +20 -0
- package/src/telegram-bot.mjs +34 -48
- package/src/telegram-command-execution.lib.mjs +74 -0
- package/src/telegram-show-limits.lib.mjs +281 -0
- package/src/telegram-solve-queue.lib.mjs +8 -0
- package/src/work-session-formatting.lib.mjs +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.69.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8939a2a: Add experimental `--show-limits` virtual option to hive-telegram-bot's `/solve` and `/hive` commands. When set, the bot embeds a Claude (or Codex) usage snapshot in the executing message and a delta block (start → end, with a parallel-sessions disclaimer) in the completion message. Limits are fetched via the existing 20-minute cached helpers so the upstream usage API isn't rate-limited. The flag is stripped before the args reach `/solve` or `/hive`, and bot administrators can disable it with `TELEGRAM_SHOW_LIMITS=false`. Refs: #594.
|
|
8
|
+
|
|
9
|
+
## 1.68.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- cbc7033: Switch the test runner to folder-based discovery and deprecate `start-screen` in favour of `--isolated screen` (issue #1758).
|
|
14
|
+
- `scripts/run-tests.mjs` now discovers every `*.mjs` / `*.test.mjs` / `*.test.js` file under `tests/` automatically. The hard-coded `LEGACY_DEFAULT_TESTS` allow-list is gone, so new test files no longer need a runner update to be picked up.
|
|
15
|
+
- New markers complement the existing `@hive-mind-test-suite <name>` marker:
|
|
16
|
+
- `@hive-mind-integration` — skip the file in the default suite; opt in via `--suite integration` or `HIVE_MIND_RUN_INTEGRATION=1`.
|
|
17
|
+
- `@hive-mind-test-skip` — exclude helper / fixture modules from every suite.
|
|
18
|
+
- `tests/integration-guard.mjs` exposes `skipUnlessIntegration(import.meta.url)` for token- or network-heavy tests.
|
|
19
|
+
- `src/start-screen.mjs` and `src/telegram-command-execution.lib.mjs::executeStartScreen` print a one-shot deprecation banner to stderr (suppressible with `HIVE_MIND_SUPPRESS_DEPRECATIONS=1`) recommending `--isolated screen`, which is already the default for `hive`/`solve` invocations through the Telegram bot.
|
|
20
|
+
- Adds regression tests `tests/test-issue-1758-runner-discovery.mjs`, `tests/test-issue-1758-start-screen-deprecation.mjs`, and `tests/test-issue-1758-integration-guard.mjs`.
|
|
21
|
+
- Documents the analysis under `docs/case-studies/issue-1758/`.
|
|
22
|
+
|
|
3
23
|
## 1.67.2
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -281,6 +281,36 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// Issue #594: when --show-limits was used at command time, capture an
|
|
285
|
+
// end-of-task limits snapshot and append a delta block to the
|
|
286
|
+
// completion message. The cached helpers respect a 20-min TTL so
|
|
287
|
+
// parallel sessions don't stampede the upstream API.
|
|
288
|
+
const limitsExtraSections = [];
|
|
289
|
+
if (sessionInfo?.showLimits) {
|
|
290
|
+
try {
|
|
291
|
+
const showLimitsLib = await import('./telegram-show-limits.lib.mjs');
|
|
292
|
+
const limitsLib = await import('./limits.lib.mjs');
|
|
293
|
+
const endSnapshot = await showLimitsLib.captureLimitsSnapshot({
|
|
294
|
+
tool: sessionInfo.tool || 'claude',
|
|
295
|
+
verbose,
|
|
296
|
+
limitsLib,
|
|
297
|
+
});
|
|
298
|
+
sessionInfo.limitsAtEnd = endSnapshot;
|
|
299
|
+
const deltaBlock = showLimitsLib.formatLimitsDeltaBlock(sessionInfo.limitsAtStart || null, endSnapshot);
|
|
300
|
+
if (deltaBlock) limitsExtraSections.push(deltaBlock);
|
|
301
|
+
else {
|
|
302
|
+
// Either start snapshot was missing or tool changed — fall back
|
|
303
|
+
// to a plain end-of-task snapshot so the user still sees current state.
|
|
304
|
+
const endBlock = showLimitsLib.formatLimitsSnapshotBlock(endSnapshot, { title: '📊 Limits at end' });
|
|
305
|
+
if (endBlock) limitsExtraSections.push(endBlock);
|
|
306
|
+
}
|
|
307
|
+
} catch (limitsError) {
|
|
308
|
+
if (verbose) {
|
|
309
|
+
console.log(`[VERBOSE] Could not capture end-of-task limits for ${sessionName}: ${limitsError?.message || limitsError}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
284
314
|
const message = formatSessionCompletionMessage({
|
|
285
315
|
sessionName,
|
|
286
316
|
sessionInfo,
|
|
@@ -289,6 +319,7 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
289
319
|
exitCode: finalExitCode,
|
|
290
320
|
infoBlock: sessionInfo?.infoBlock || '',
|
|
291
321
|
pullRequestUrl,
|
|
322
|
+
extraSections: limitsExtraSections,
|
|
292
323
|
});
|
|
293
324
|
|
|
294
325
|
// Update the original reply message if messageId is available, otherwise send new message
|
package/src/start-screen.mjs
CHANGED
|
@@ -19,6 +19,24 @@ const printUsage = (log = console.error) => {
|
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Print a single-line deprecation notice to stderr the first time it is
|
|
24
|
+
* called per process. Suppressed when `HIVE_MIND_SUPPRESS_DEPRECATIONS=1`.
|
|
25
|
+
*
|
|
26
|
+
* Tracked via a module-scope flag (`deprecationWarned`) so a long-running
|
|
27
|
+
* process emits the banner only once even if `main()` is invoked multiple
|
|
28
|
+
* times in tests.
|
|
29
|
+
*
|
|
30
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1758
|
|
31
|
+
*/
|
|
32
|
+
let deprecationWarned = false;
|
|
33
|
+
const printDeprecationBanner = () => {
|
|
34
|
+
if (deprecationWarned) return;
|
|
35
|
+
if (process.env.HIVE_MIND_SUPPRESS_DEPRECATIONS === '1') return;
|
|
36
|
+
deprecationWarned = true;
|
|
37
|
+
console.error('⚠️ start-screen is deprecated; prefer `--isolated screen` (the default in newer hive/solve CLIs). Set HIVE_MIND_SUPPRESS_DEPRECATIONS=1 to silence this warning.');
|
|
38
|
+
};
|
|
39
|
+
|
|
22
40
|
const createStartScreenYargsConfig = yargsInstance =>
|
|
23
41
|
yargsInstance
|
|
24
42
|
.usage(START_SCREEN_USAGE[0])
|
|
@@ -288,6 +306,8 @@ async function createOrEnterScreen(sessionName, command, args, autoTerminate = f
|
|
|
288
306
|
async function main() {
|
|
289
307
|
const args = process.argv.slice(2);
|
|
290
308
|
|
|
309
|
+
printDeprecationBanner();
|
|
310
|
+
|
|
291
311
|
if (args.includes('--help') || args.includes('-h')) {
|
|
292
312
|
printUsage(console.log);
|
|
293
313
|
process.exit(0);
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -30,12 +30,14 @@ const { parseGitHubUrl, validateGitHubEntityExistence } = await import('./github
|
|
|
30
30
|
const { validateModelName, buildModelOptionDescription } = await import('./models/index.mjs');
|
|
31
31
|
const { validateBranchInArgs } = await import('./solve.branch.lib.mjs');
|
|
32
32
|
const { extractIsolationFromArgs, isValidPerCommandIsolation, resolveIsolation, createIsolationAwareQueueCallback } = await import('./telegram-isolation.lib.mjs');
|
|
33
|
-
const
|
|
33
|
+
const limitsLib = await import('./limits.lib.mjs');
|
|
34
|
+
const { formatUsageMessage, formatCodexLimitsSection, getAllCachedLimits } = limitsLib;
|
|
35
|
+
const { handleShowLimitsFlag, captureStartSnapshotAndAppend } = await import('./telegram-show-limits.lib.mjs'); // #594
|
|
34
36
|
const { getVersionInfo, formatVersionMessage } = await import('./version-info.lib.mjs');
|
|
35
37
|
const { escapeMarkdown, escapeMarkdownV2, cleanNonPrintableChars, makeSpecialCharsVisible } = await import('./telegram-markdown.lib.mjs');
|
|
36
38
|
const { getSolveQueue, createQueueExecuteCallback } = await import('./telegram-solve-queue.lib.mjs');
|
|
37
39
|
const { applySolveToolAlias, getFirstParsedPositionalArg, getSolveCommandNameFromText, getSolveToolAliasFromText, moveArgumentToFront, parseArgsWithYargs, parseCommandArgs, SOLVE_COMMAND_NAMES } = await import('./telegram-solve-command.lib.mjs');
|
|
38
|
-
const { executeStartScreen: executeStartScreenCommand } = await import('./telegram-command-execution.lib.mjs');
|
|
40
|
+
const { executeStartScreen: executeStartScreenCommand, buildExecuteAndUpdateMessage } = await import('./telegram-command-execution.lib.mjs');
|
|
39
41
|
const { isChatStopped, getChatStopInfo, getStoppedChatRejectMessage, DEFAULT_STOP_REASON } = await import('./telegram-start-stop-command.lib.mjs');
|
|
40
42
|
const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
|
|
41
43
|
const { safeReply } = await import('./telegram-safe-reply.lib.mjs');
|
|
@@ -110,6 +112,8 @@ const config = yargs(hideBin(process.argv))
|
|
|
110
112
|
default: getenv('TELEGRAM_BOT_VERBOSE', 'false') === 'true',
|
|
111
113
|
})
|
|
112
114
|
.option('autoStartScreenWatchMessage', { type: 'boolean', description: 'Experimental: auto-start separate /terminal_watch messages for public /solve sessions', alias: 'auto-start-screen-watch-message', default: getenv('TELEGRAM_AUTO_START_SCREEN_WATCH_MESSAGE', getenv('TELEGRAM_AUTO_WATCH_MESSAGE', 'false')) === 'true' })
|
|
115
|
+
// Issue #594: bot-owner toggle for --show-limits virtual option in /solve and /hive.
|
|
116
|
+
.option('showLimits', { type: 'boolean', description: 'Experimental: allow /solve and /hive callers to use --show-limits to embed Claude/Codex usage at start, end, and delta in the completion message', alias: 'show-limits', default: getenv('TELEGRAM_SHOW_LIMITS', 'true') !== 'false' })
|
|
113
117
|
.option('isolation', { type: 'string', description: "Isolation backend (screen/tmux/docker). Defaults to 'screen' so Telegram-bot work sessions survive bot restarts; pass --isolation '' (or set TELEGRAM_ISOLATION='') to disable.", default: getenv('TELEGRAM_ISOLATION', 'screen') })
|
|
114
118
|
.help('h')
|
|
115
119
|
.alias('h', 'help')
|
|
@@ -128,6 +132,7 @@ if (config.configuration) {
|
|
|
128
132
|
const BOT_TOKEN = config.token || getenv('TELEGRAM_BOT_TOKEN', '');
|
|
129
133
|
const VERBOSE = config.verbose || getenv('TELEGRAM_BOT_VERBOSE', 'false') === 'true';
|
|
130
134
|
const AUTO_WATCH_MESSAGE = config.autoStartScreenWatchMessage === true;
|
|
135
|
+
const SHOW_LIMITS_ENABLED = config.showLimits === true;
|
|
131
136
|
if (!BOT_TOKEN) {
|
|
132
137
|
console.error('Error: TELEGRAM_BOT_TOKEN not set. Use --token or TELEGRAM_BOT_TOKEN env var.');
|
|
133
138
|
process.exit(1);
|
|
@@ -460,48 +465,7 @@ async function validateGitHubUrl(args, options = {}) {
|
|
|
460
465
|
return { valid: true, parsed, normalizedUrl: url };
|
|
461
466
|
}
|
|
462
467
|
|
|
463
|
-
|
|
464
|
-
const { chat, message_id: msgId } = startingMessage;
|
|
465
|
-
const safeEdit = async text => {
|
|
466
|
-
try {
|
|
467
|
-
await ctx.telegram.editMessageText(chat.id, msgId, undefined, text, { parse_mode: 'Markdown' });
|
|
468
|
-
} catch (e) {
|
|
469
|
-
console.error(`[telegram-bot] Failed to update message for ${commandName}: ${e.message}`);
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
const requesterUserId = ctx.from?.id ?? null; // Issue #1688: suppress duplicate /subscribe DM
|
|
473
|
-
const iso = await resolveIsolation(perCommandIsolation, ISOLATION_BACKEND, isolationRunner, VERBOSE);
|
|
474
|
-
let result, session, sessionInfo;
|
|
475
|
-
if (iso) {
|
|
476
|
-
session = iso.runner.generateSessionId();
|
|
477
|
-
VERBOSE && console.log(`[VERBOSE] Using isolation (${iso.backend}), session: ${session}`);
|
|
478
|
-
result = await iso.runner.executeWithIsolation(commandName, args, { backend: iso.backend, sessionId: session, verbose: VERBOSE });
|
|
479
|
-
if (result.success) {
|
|
480
|
-
sessionInfo = { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, isolationBackend: iso.backend, sessionId: session, tool, infoBlock, urlContext, requesterUserId };
|
|
481
|
-
trackSession(session, sessionInfo, VERBOSE);
|
|
482
|
-
}
|
|
483
|
-
} else {
|
|
484
|
-
result = await executeStartScreen(commandName, args);
|
|
485
|
-
const match = result.success && (result.output.match(/session:\s*(\S+)/i) || result.output.match(/screen -R\s+(\S+)/));
|
|
486
|
-
session = match ? match[1] : 'unknown';
|
|
487
|
-
// Issue #1586: Non-isolation sessions auto-expire after 10 min — screen stays alive via `exec bash` so completion can't be detected reliably; this still blocks duplicate commands in the timeout window.
|
|
488
|
-
if (result.success && session !== 'unknown') {
|
|
489
|
-
sessionInfo = { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, tool, infoBlock, urlContext, requesterUserId };
|
|
490
|
-
trackSession(session, sessionInfo, VERBOSE);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
if (result.warning) return safeEdit(`⚠️ ${result.warning}`);
|
|
494
|
-
if (result.success) {
|
|
495
|
-
await safeEdit(
|
|
496
|
-
formatExecutingWorkSessionMessage({
|
|
497
|
-
sessionName: session,
|
|
498
|
-
isolationBackend: iso?.backend || null,
|
|
499
|
-
infoBlock,
|
|
500
|
-
})
|
|
501
|
-
);
|
|
502
|
-
if (AUTO_WATCH_MESSAGE && commandName === 'solve' && sessionInfo?.isolationBackend) await startAutoTerminalWatchForSession({ bot, ctx, sessionId: session, sessionInfo, verbose: VERBOSE });
|
|
503
|
-
} else await safeEdit(`❌ Error executing ${commandName} command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\`\n\n${infoBlock}`);
|
|
504
|
-
}
|
|
468
|
+
const executeAndUpdateMessage = buildExecuteAndUpdateMessage({ resolveIsolation, ISOLATION_BACKEND, isolationRunner, VERBOSE, executeStartScreen, trackSession, AUTO_WATCH_MESSAGE, startAutoTerminalWatchForSession, bot, formatExecutingWorkSessionMessage });
|
|
505
469
|
|
|
506
470
|
bot.command('help', async ctx => {
|
|
507
471
|
VERBOSE && console.log('[VERBOSE] /help command received');
|
|
@@ -603,6 +567,7 @@ bot.command('help', async ctx => {
|
|
|
603
567
|
message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
|
|
604
568
|
message += '• `--think <level>` - Thinking level (off/low/medium/high/xhigh/max) | `--thinking-budget <num>` - Token budget (0-63999)\n';
|
|
605
569
|
message += '• `--verbose` or `-v` - Verbose output | `--attach-logs` - Attach logs to PR\n';
|
|
570
|
+
if (SHOW_LIMITS_ENABLED) message += '• `--show-limits` - Experimental: embed Claude/Codex usage at start, end and delta (#594)\n';
|
|
606
571
|
message += '\n💡 *Tip:* Many more options available. See full documentation for complete list.\n';
|
|
607
572
|
|
|
608
573
|
if (allowedChats || allowedTopics) {
|
|
@@ -788,6 +753,12 @@ async function handleSolveCommand(ctx) {
|
|
|
788
753
|
const solveToolAlias = getSolveToolAliasFromText(ctx.message.text);
|
|
789
754
|
let userArgs = parseCommandArgs(ctx.message.text);
|
|
790
755
|
|
|
756
|
+
// Issue #594: strip --show-limits from userArgs (hive-telegram-bot virtual option).
|
|
757
|
+
const solveSL = await handleShowLimitsFlag({ ctx, safeReply, args: userArgs, enabled: SHOW_LIMITS_ENABLED });
|
|
758
|
+
if (solveSL.handled) return;
|
|
759
|
+
const solveShowLimits = solveSL.showLimits;
|
|
760
|
+
userArgs = solveSL.args;
|
|
761
|
+
|
|
791
762
|
// Check if this is a reply to a message and user didn't provide URL as first argument
|
|
792
763
|
// In that case, try to extract GitHub URL from the replied message
|
|
793
764
|
// Issue #1325: Support all options via /solve command when replying (e.g., "/solve --model opus")
|
|
@@ -958,11 +929,16 @@ async function handleSolveCommand(ctx) {
|
|
|
958
929
|
const toolQueuedCount = queueStats.queuedByTool[solveTool] || 0; // tool-specific queue count (#1551)
|
|
959
930
|
// Issue #378: propagate user's effective Telegram locale to the spawned solve session.
|
|
960
931
|
const argsWithLocale = injectLanguageIfMissing(args, solveLocale);
|
|
932
|
+
|
|
933
|
+
// Issue #594: append "Limits at start" to infoBlock; thread snapshot via sessionInfo.
|
|
934
|
+
let solveLimitsAtStart = null;
|
|
935
|
+
if (solveShowLimits) ({ infoBlock, limitsAtStart: solveLimitsAtStart } = await captureStartSnapshotAndAppend({ infoBlock, tool: solveTool, verbose: VERBOSE, limitsLib, commandLabel: '/solve' }));
|
|
936
|
+
|
|
961
937
|
if (check.canStart && toolQueuedCount === 0) {
|
|
962
938
|
const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock }), { reply_to_message_id: ctx.message.message_id });
|
|
963
|
-
await executeAndUpdateMessage(ctx, startingMessage, 'solve', argsWithLocale, infoBlock, effectiveSolveIsolation, solveTool, solveUrlContext);
|
|
939
|
+
await executeAndUpdateMessage(ctx, startingMessage, 'solve', argsWithLocale, infoBlock, effectiveSolveIsolation, solveTool, solveUrlContext, { showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart });
|
|
964
940
|
} else {
|
|
965
|
-
const queueItem = solveQueue.enqueue({ url: normalizedUrl, args: argsWithLocale, ctx, requester, infoBlock, tool: solveTool, perCommandIsolation: effectiveSolveIsolation, urlContext: solveUrlContext });
|
|
941
|
+
const queueItem = solveQueue.enqueue({ url: normalizedUrl, args: argsWithLocale, ctx, requester, infoBlock, tool: solveTool, perCommandIsolation: effectiveSolveIsolation, urlContext: solveUrlContext, showLimits: solveShowLimits, limitsAtStart: solveLimitsAtStart });
|
|
966
942
|
let queueMessage = `📋 Solve command queued (${solveTool} queue position #${toolQueuedCount + 1})\n\n${infoBlock}`; // tool-specific position (#1551)
|
|
967
943
|
if (check.reason) queueMessage += `\n\n⏳ Waiting: ${escapeMarkdown(check.reason)}`;
|
|
968
944
|
const queuedMessage = await safeReply(ctx, queueMessage, { reply_to_message_id: ctx.message.message_id });
|
|
@@ -1049,7 +1025,13 @@ async function handleHiveCommand(ctx) {
|
|
|
1049
1025
|
|
|
1050
1026
|
VERBOSE && console.log('[VERBOSE] /hive passed all checks, executing...');
|
|
1051
1027
|
|
|
1052
|
-
|
|
1028
|
+
let userArgs = parseCommandArgs(ctx.message.text);
|
|
1029
|
+
|
|
1030
|
+
// Issue #594: see /solve handler.
|
|
1031
|
+
const hiveSL = await handleShowLimitsFlag({ ctx, safeReply, args: userArgs, enabled: SHOW_LIMITS_ENABLED });
|
|
1032
|
+
if (hiveSL.handled) return;
|
|
1033
|
+
const hiveShowLimits = hiveSL.showLimits;
|
|
1034
|
+
userArgs = hiveSL.args;
|
|
1053
1035
|
|
|
1054
1036
|
// Issue #1102: Allow issues_list/pulls_list URLs and normalize to repo URLs
|
|
1055
1037
|
const validation = await validateGitHubUrl(userArgs, { allowedTypes: ['repo', 'organization', 'user', 'issues_list', 'pulls_list'], commandName: 'hive', createYargsConfig: createHiveYargsConfig, positionalNames: ['github-url'] });
|
|
@@ -1125,10 +1107,14 @@ async function handleHiveCommand(ctx) {
|
|
|
1125
1107
|
infoBlock += `${userOptionsRaw ? '\n' : '\n\n'}🔒 Locked options: ${escapeMarkdown(hiveOverrides.join(' '))}`;
|
|
1126
1108
|
}
|
|
1127
1109
|
|
|
1110
|
+
// Issue #594: see /solve handler.
|
|
1111
|
+
let hiveLimitsAtStart = null;
|
|
1112
|
+
if (hiveShowLimits) ({ infoBlock, limitsAtStart: hiveLimitsAtStart } = await captureStartSnapshotAndAppend({ infoBlock, tool: hiveTool, verbose: VERBOSE, limitsLib, commandLabel: '/hive' }));
|
|
1113
|
+
|
|
1128
1114
|
const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock }), { reply_to_message_id: ctx.message.message_id });
|
|
1129
1115
|
// Issue #378: propagate user's effective Telegram locale to the spawned hive session.
|
|
1130
1116
|
const hiveArgsWithLocale = injectLanguageIfMissing(args, hiveLocale);
|
|
1131
|
-
await executeAndUpdateMessage(ctx, startingMessage, 'hive', hiveArgsWithLocale, infoBlock, effectiveHiveIsolation, hiveTool);
|
|
1117
|
+
await executeAndUpdateMessage(ctx, startingMessage, 'hive', hiveArgsWithLocale, infoBlock, effectiveHiveIsolation, hiveTool, null, { showLimits: hiveShowLimits, limitsAtStart: hiveLimitsAtStart });
|
|
1132
1118
|
}
|
|
1133
1119
|
|
|
1134
1120
|
bot.command(/^hive$/i, handleHiveCommand);
|
|
@@ -4,6 +4,14 @@ import { exec as execCallback } from 'child_process';
|
|
|
4
4
|
|
|
5
5
|
const exec = promisify(execCallback);
|
|
6
6
|
|
|
7
|
+
let deprecationWarned = false;
|
|
8
|
+
function warnStartScreenDeprecated() {
|
|
9
|
+
if (deprecationWarned) return;
|
|
10
|
+
if (process.env.HIVE_MIND_SUPPRESS_DEPRECATIONS === '1') return;
|
|
11
|
+
deprecationWarned = true;
|
|
12
|
+
console.warn('⚠️ executeStartScreen is deprecated; prefer the `--isolated screen` workflow exposed by hive/solve directly. Set HIVE_MIND_SUPPRESS_DEPRECATIONS=1 to silence this warning.');
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
async function findStartScreenCommand() {
|
|
8
16
|
try {
|
|
9
17
|
const { stdout } = await exec('which start-screen');
|
|
@@ -65,9 +73,75 @@ function executeWithCommand(startScreenCmd, command, args, verbose = false) {
|
|
|
65
73
|
});
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Build the executeAndUpdateMessage function used by /solve and /hive in
|
|
78
|
+
* telegram-bot.mjs. The original function captures ~10 module-level closures
|
|
79
|
+
* (resolveIsolation, ISOLATION_BACKEND, isolationRunner, VERBOSE, executeStartScreen,
|
|
80
|
+
* trackSession, AUTO_WATCH_MESSAGE, startAutoTerminalWatchForSession, bot,
|
|
81
|
+
* formatExecutingWorkSessionMessage); the factory pattern lets us extract the
|
|
82
|
+
* function while still keeping all those handles available without making them
|
|
83
|
+
* module-global elsewhere. Splitting this out keeps telegram-bot.mjs under the
|
|
84
|
+
* 1500-line cap (issues #1141, #1730, #594).
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} deps - Dependencies captured at bot startup time.
|
|
87
|
+
* @param {Function} deps.resolveIsolation
|
|
88
|
+
* @param {string|null} deps.ISOLATION_BACKEND
|
|
89
|
+
* @param {Object} deps.isolationRunner
|
|
90
|
+
* @param {boolean} deps.VERBOSE
|
|
91
|
+
* @param {Function} deps.executeStartScreen
|
|
92
|
+
* @param {Function} deps.trackSession
|
|
93
|
+
* @param {boolean} deps.AUTO_WATCH_MESSAGE
|
|
94
|
+
* @param {Function} deps.startAutoTerminalWatchForSession
|
|
95
|
+
* @param {Object} deps.bot
|
|
96
|
+
* @param {Function} deps.formatExecutingWorkSessionMessage
|
|
97
|
+
* @returns {Function} executeAndUpdateMessage(ctx, startingMessage, commandName, args, infoBlock, perCommandIsolation, tool, urlContext, sessionExtras)
|
|
98
|
+
*/
|
|
99
|
+
export function buildExecuteAndUpdateMessage(deps) {
|
|
100
|
+
const { resolveIsolation, ISOLATION_BACKEND, isolationRunner, VERBOSE, executeStartScreen, trackSession, AUTO_WATCH_MESSAGE, startAutoTerminalWatchForSession, bot, formatExecutingWorkSessionMessage } = deps;
|
|
101
|
+
return async function executeAndUpdateMessage(ctx, startingMessage, commandName, args, infoBlock, perCommandIsolation = null, tool = 'claude', urlContext = null, { showLimits = false, limitsAtStart = null } = {}) {
|
|
102
|
+
const { chat, message_id: msgId } = startingMessage;
|
|
103
|
+
const safeEdit = async text => {
|
|
104
|
+
try {
|
|
105
|
+
await ctx.telegram.editMessageText(chat.id, msgId, undefined, text, { parse_mode: 'Markdown' });
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.error(`[telegram-bot] Failed to update message for ${commandName}: ${e.message}`);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const requesterUserId = ctx.from?.id ?? null; // Issue #1688: suppress duplicate /subscribe DM
|
|
111
|
+
const baseSessionInfo = { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, tool, infoBlock, urlContext, requesterUserId, showLimits, limitsAtStart }; // #594: showLimits/limitsAtStart
|
|
112
|
+
const iso = await resolveIsolation(perCommandIsolation, ISOLATION_BACKEND, isolationRunner, VERBOSE);
|
|
113
|
+
let result, session, sessionInfo;
|
|
114
|
+
if (iso) {
|
|
115
|
+
session = iso.runner.generateSessionId();
|
|
116
|
+
VERBOSE && console.log(`[VERBOSE] Using isolation (${iso.backend}), session: ${session}`);
|
|
117
|
+
result = await iso.runner.executeWithIsolation(commandName, args, { backend: iso.backend, sessionId: session, verbose: VERBOSE });
|
|
118
|
+
if (result.success) {
|
|
119
|
+
sessionInfo = { ...baseSessionInfo, isolationBackend: iso.backend, sessionId: session };
|
|
120
|
+
trackSession(session, sessionInfo, VERBOSE);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
result = await executeStartScreen(commandName, args);
|
|
124
|
+
const match = result.success && (result.output.match(/session:\s*(\S+)/i) || result.output.match(/screen -R\s+(\S+)/));
|
|
125
|
+
session = match ? match[1] : 'unknown';
|
|
126
|
+
// Issue #1586: Non-isolation sessions auto-expire after 10 min — screen stays alive via `exec bash` so completion can't be detected reliably; this still blocks duplicate commands in the timeout window.
|
|
127
|
+
if (result.success && session !== 'unknown') {
|
|
128
|
+
sessionInfo = baseSessionInfo;
|
|
129
|
+
trackSession(session, sessionInfo, VERBOSE);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (result.warning) return safeEdit(`⚠️ ${result.warning}`);
|
|
133
|
+
if (result.success) {
|
|
134
|
+
await safeEdit(formatExecutingWorkSessionMessage({ sessionName: session, isolationBackend: iso?.backend || null, infoBlock }));
|
|
135
|
+
if (AUTO_WATCH_MESSAGE && commandName === 'solve' && sessionInfo?.isolationBackend) await startAutoTerminalWatchForSession({ bot, ctx, sessionId: session, sessionInfo, verbose: VERBOSE });
|
|
136
|
+
} else await safeEdit(`❌ Error executing ${commandName} command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\`\n\n${infoBlock}`);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
68
140
|
export async function executeStartScreen(command, args, options = {}) {
|
|
69
141
|
const { verbose = false } = options;
|
|
70
142
|
|
|
143
|
+
warnStartScreenDeprecated();
|
|
144
|
+
|
|
71
145
|
try {
|
|
72
146
|
const whichPath = await findStartScreenCommand();
|
|
73
147
|
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram bot virtual option: --show-limits (experimental).
|
|
3
|
+
*
|
|
4
|
+
* The --show-limits flag is intercepted by hive-telegram-bot and stripped from
|
|
5
|
+
* the args before they are forwarded to /solve, /hive (or /task --split). When
|
|
6
|
+
* set, the bot:
|
|
7
|
+
* 1. Fetches usage limits for the selected tool (Claude or Codex) using the
|
|
8
|
+
* shared cached helpers in limits.lib.mjs (TTL: 20 minutes for the usage
|
|
9
|
+
* API to avoid rate limiting).
|
|
10
|
+
* 2. Embeds a compact "Limits at start" snapshot below the infoBlock so the
|
|
11
|
+
* starting/executing message shows the user how much budget they had at
|
|
12
|
+
* the moment the command was queued.
|
|
13
|
+
* 3. Captures the snapshot in the per-session record so the completion
|
|
14
|
+
* message can render an end-of-task snapshot plus a delta. The delta is
|
|
15
|
+
* not exact — multiple parallel sessions all consume from the same
|
|
16
|
+
* budget — and is reported as such.
|
|
17
|
+
*
|
|
18
|
+
* The flag is purely a Telegram bot concern; downstream commands never see it.
|
|
19
|
+
*
|
|
20
|
+
* @see https://github.com/link-assistant/hive-mind/issues/594
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const SHOW_LIMITS_FLAG = '--show-limits';
|
|
24
|
+
const NO_SHOW_LIMITS_FLAG = '--no-show-limits';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Detect whether the user passed --show-limits (or --no-show-limits to opt out)
|
|
28
|
+
* and return a copy of the args without that flag.
|
|
29
|
+
*
|
|
30
|
+
* Last occurrence wins, matching how yargs resolves repeated boolean flags.
|
|
31
|
+
*
|
|
32
|
+
* @param {string[]} args
|
|
33
|
+
* @returns {{ showLimits: boolean|null, args: string[] }}
|
|
34
|
+
* `showLimits` is `true`/`false` when explicitly set, or `null` when absent.
|
|
35
|
+
*/
|
|
36
|
+
export function extractShowLimitsFlag(args) {
|
|
37
|
+
if (!Array.isArray(args)) return { showLimits: null, args: args || [] };
|
|
38
|
+
let showLimits = null;
|
|
39
|
+
const filtered = [];
|
|
40
|
+
for (const arg of args) {
|
|
41
|
+
if (arg === SHOW_LIMITS_FLAG) {
|
|
42
|
+
showLimits = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (arg === NO_SHOW_LIMITS_FLAG) {
|
|
46
|
+
showLimits = false;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (arg === '--show-limits=true' || arg === '--show-limits=1') {
|
|
50
|
+
showLimits = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (arg === '--show-limits=false' || arg === '--show-limits=0') {
|
|
54
|
+
showLimits = false;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
filtered.push(arg);
|
|
58
|
+
}
|
|
59
|
+
return { showLimits, args: filtered };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Pick a tool key for limits selection. Codex-like tools route to Codex, the
|
|
64
|
+
* rest of the supported tools (claude, opencode, agent, gemini, qwen) route to
|
|
65
|
+
* Claude. This mirrors how solve.mjs selects which CLI to invoke.
|
|
66
|
+
*
|
|
67
|
+
* @param {string|null|undefined} tool
|
|
68
|
+
* @returns {'codex'|'claude'}
|
|
69
|
+
*/
|
|
70
|
+
export function pickLimitsToolKey(tool) {
|
|
71
|
+
return String(tool || '').toLowerCase() === 'codex' ? 'codex' : 'claude';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fetch the cached limits snapshot for the given tool. Returns a normalized
|
|
76
|
+
* shape that captures both the raw `success/usage/error` payload and a
|
|
77
|
+
* `toolKey` so the renderers know which formatter to use.
|
|
78
|
+
*
|
|
79
|
+
* @param {Object} options
|
|
80
|
+
* @param {string} [options.tool='claude']
|
|
81
|
+
* @param {boolean} [options.verbose=false]
|
|
82
|
+
* @param {{ getCachedClaudeLimits: Function, getCachedCodexLimits: Function }} options.limitsLib
|
|
83
|
+
* @returns {Promise<{ toolKey: 'codex'|'claude', success: boolean, usage?: any, error?: string, capturedAt: Date, additionalRateLimits?: any[], credits?: any, planType?: any }>}
|
|
84
|
+
*/
|
|
85
|
+
export async function captureLimitsSnapshot({ tool = 'claude', verbose = false, limitsLib } = {}) {
|
|
86
|
+
if (!limitsLib) throw new Error('captureLimitsSnapshot requires limitsLib');
|
|
87
|
+
const toolKey = pickLimitsToolKey(tool);
|
|
88
|
+
const fetcher = toolKey === 'codex' ? limitsLib.getCachedCodexLimits : limitsLib.getCachedClaudeLimits;
|
|
89
|
+
const result = await fetcher(verbose);
|
|
90
|
+
return {
|
|
91
|
+
toolKey,
|
|
92
|
+
success: !!result?.success,
|
|
93
|
+
usage: result?.usage || null,
|
|
94
|
+
error: result?.success ? null : result?.error || 'Unknown error',
|
|
95
|
+
capturedAt: new Date(),
|
|
96
|
+
additionalRateLimits: result?.additionalRateLimits || null,
|
|
97
|
+
credits: result?.credits || null,
|
|
98
|
+
planType: result?.planType || null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function pct(value) {
|
|
103
|
+
if (value === null || value === undefined) return null;
|
|
104
|
+
const num = Number(value);
|
|
105
|
+
if (!Number.isFinite(num)) return null;
|
|
106
|
+
return Math.floor(num);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatPercentage(value) {
|
|
110
|
+
const p = pct(value);
|
|
111
|
+
return p === null ? 'N/A' : `${p}%`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Render a compact one-line-per-window summary of a Claude snapshot.
|
|
116
|
+
* The compact form is used inside a Markdown code block under the infoBlock.
|
|
117
|
+
*
|
|
118
|
+
* @param {Object} snapshot - Result of captureLimitsSnapshot for tool=claude
|
|
119
|
+
* @returns {string}
|
|
120
|
+
*/
|
|
121
|
+
function formatClaudeSnapshotCompact(snapshot) {
|
|
122
|
+
if (!snapshot) return 'Claude limits: N/A';
|
|
123
|
+
if (!snapshot.success) return `Claude limits: ${snapshot.error || 'unavailable'}`;
|
|
124
|
+
const usage = snapshot.usage || {};
|
|
125
|
+
const lines = [];
|
|
126
|
+
lines.push(`5h session: ${formatPercentage(usage.currentSession?.percentage)}`);
|
|
127
|
+
lines.push(`7d all models: ${formatPercentage(usage.allModels?.percentage)}`);
|
|
128
|
+
if (usage.sonnetOnly && usage.sonnetOnly.percentage !== null && usage.sonnetOnly.percentage !== undefined) {
|
|
129
|
+
lines.push(`7d Sonnet only: ${formatPercentage(usage.sonnetOnly.percentage)}`);
|
|
130
|
+
}
|
|
131
|
+
return lines.join('\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatCodexSnapshotCompact(snapshot) {
|
|
135
|
+
if (!snapshot) return 'Codex limits: N/A';
|
|
136
|
+
if (!snapshot.success) return `Codex limits: ${snapshot.error || 'unavailable'}`;
|
|
137
|
+
const usage = snapshot.usage || {};
|
|
138
|
+
const lines = [];
|
|
139
|
+
lines.push(`5h session: ${formatPercentage(usage.currentSession?.percentage)}`);
|
|
140
|
+
lines.push(`Weekly: ${formatPercentage(usage.allModels?.percentage)}`);
|
|
141
|
+
return lines.join('\n');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Format a snapshot as a fenced code block suitable for prepending/appending
|
|
146
|
+
* inside the info block of a Telegram message.
|
|
147
|
+
*
|
|
148
|
+
* @param {Object} snapshot
|
|
149
|
+
* @param {Object} [options]
|
|
150
|
+
* @param {string} [options.title='📊 Limits at start'] Block title
|
|
151
|
+
* @returns {string}
|
|
152
|
+
*/
|
|
153
|
+
export function formatLimitsSnapshotBlock(snapshot, { title = '📊 Limits at start' } = {}) {
|
|
154
|
+
if (!snapshot) return '';
|
|
155
|
+
const heading = snapshot.toolKey === 'codex' ? 'Codex' : 'Claude';
|
|
156
|
+
const body = snapshot.toolKey === 'codex' ? formatCodexSnapshotCompact(snapshot) : formatClaudeSnapshotCompact(snapshot);
|
|
157
|
+
return `${title} (${heading})\n\`\`\`\n${body}\n\`\`\``;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function deltaFor(startPct, endPct) {
|
|
161
|
+
const s = pct(startPct);
|
|
162
|
+
const e = pct(endPct);
|
|
163
|
+
if (s === null || e === null) return null;
|
|
164
|
+
return e - s;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function formatDeltaValue(delta) {
|
|
168
|
+
if (delta === null || delta === undefined) return 'N/A';
|
|
169
|
+
if (delta === 0) return '±0%';
|
|
170
|
+
const sign = delta > 0 ? '+' : '';
|
|
171
|
+
return `${sign}${delta}%`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Format a "Limits change" block summarizing start, end, and delta for the
|
|
176
|
+
* configured windows. Includes a disclaimer about parallel sessions.
|
|
177
|
+
*
|
|
178
|
+
* @param {Object|null} startSnapshot
|
|
179
|
+
* @param {Object|null} endSnapshot
|
|
180
|
+
* @returns {string}
|
|
181
|
+
*/
|
|
182
|
+
export function formatLimitsDeltaBlock(startSnapshot, endSnapshot) {
|
|
183
|
+
if (!startSnapshot || !endSnapshot) return '';
|
|
184
|
+
if (startSnapshot.toolKey !== endSnapshot.toolKey) return '';
|
|
185
|
+
const heading = startSnapshot.toolKey === 'codex' ? 'Codex' : 'Claude';
|
|
186
|
+
const lines = [];
|
|
187
|
+
|
|
188
|
+
if (!startSnapshot.success && !endSnapshot.success) {
|
|
189
|
+
lines.push(`Start: ${startSnapshot.error || 'unavailable'}`);
|
|
190
|
+
lines.push(`End: ${endSnapshot.error || 'unavailable'}`);
|
|
191
|
+
} else {
|
|
192
|
+
const startUsage = startSnapshot.usage || {};
|
|
193
|
+
const endUsage = endSnapshot.usage || {};
|
|
194
|
+
|
|
195
|
+
const sessionLabel = '5h session';
|
|
196
|
+
lines.push(`${sessionLabel}: ${formatPercentage(startUsage.currentSession?.percentage)} → ${formatPercentage(endUsage.currentSession?.percentage)} (${formatDeltaValue(deltaFor(startUsage.currentSession?.percentage, endUsage.currentSession?.percentage))})`);
|
|
197
|
+
|
|
198
|
+
const allModelsLabel = startSnapshot.toolKey === 'codex' ? 'Weekly' : '7d all models';
|
|
199
|
+
lines.push(`${allModelsLabel}: ${formatPercentage(startUsage.allModels?.percentage)} → ${formatPercentage(endUsage.allModels?.percentage)} (${formatDeltaValue(deltaFor(startUsage.allModels?.percentage, endUsage.allModels?.percentage))})`);
|
|
200
|
+
|
|
201
|
+
if (startSnapshot.toolKey === 'claude' && ((startUsage.sonnetOnly && startUsage.sonnetOnly.percentage !== null && startUsage.sonnetOnly.percentage !== undefined) || (endUsage.sonnetOnly && endUsage.sonnetOnly.percentage !== null && endUsage.sonnetOnly.percentage !== undefined))) {
|
|
202
|
+
lines.push(`7d Sonnet only: ${formatPercentage(startUsage.sonnetOnly?.percentage)} → ${formatPercentage(endUsage.sonnetOnly?.percentage)} (${formatDeltaValue(deltaFor(startUsage.sonnetOnly?.percentage, endUsage.sonnetOnly?.percentage))})`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Note: delta is not precise because multiple parallel tasks may consume
|
|
207
|
+
// from the same Anthropic/OpenAI budget windows during the run.
|
|
208
|
+
lines.push('Note: delta is approximate (parallel sessions share the same budget).');
|
|
209
|
+
|
|
210
|
+
return `📊 Limits change (${heading})\n\`\`\`\n${lines.join('\n')}\n\`\`\``;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Append a free-form section (e.g. limits block) to an existing infoBlock,
|
|
215
|
+
* preserving Markdown structure. A blank line separates sections so the
|
|
216
|
+
* Telegram message stays readable.
|
|
217
|
+
*
|
|
218
|
+
* @param {string} infoBlock
|
|
219
|
+
* @param {string} addition
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
export function appendInfoSection(infoBlock, addition) {
|
|
223
|
+
const base = infoBlock || '';
|
|
224
|
+
const extra = addition || '';
|
|
225
|
+
if (!extra) return base;
|
|
226
|
+
if (!base) return extra;
|
|
227
|
+
return `${base}\n\n${extra}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* High-level helper used by /solve and /hive: parse `--show-limits` out of the
|
|
232
|
+
* raw user args, decide whether the flag is honored (subject to the bot
|
|
233
|
+
* administrator's TELEGRAM_SHOW_LIMITS toggle), and either reply with a
|
|
234
|
+
* rejection message (returning {handled:true}) or hand back the stripped args
|
|
235
|
+
* and a boolean for the caller to thread through.
|
|
236
|
+
*
|
|
237
|
+
* @param {Object} options
|
|
238
|
+
* @param {Object} options.ctx - Telegraf context (used to reply on rejection)
|
|
239
|
+
* @param {Function} options.safeReply - safeReply(ctx, text, opts)
|
|
240
|
+
* @param {string[]} options.args - Raw user args before stripping
|
|
241
|
+
* @param {boolean} options.enabled - Master switch (config.showLimits)
|
|
242
|
+
* @returns {Promise<{ handled: boolean, args: string[], showLimits: boolean }>}
|
|
243
|
+
*/
|
|
244
|
+
export async function handleShowLimitsFlag({ ctx, safeReply, args, enabled }) {
|
|
245
|
+
const { showLimits, args: stripped } = extractShowLimitsFlag(args);
|
|
246
|
+
if (showLimits === true && !enabled) {
|
|
247
|
+
await safeReply(ctx, '❌ `--show-limits` is disabled by the bot administrator.', { reply_to_message_id: ctx.message.message_id });
|
|
248
|
+
return { handled: true, args: stripped, showLimits: false };
|
|
249
|
+
}
|
|
250
|
+
return { handled: false, args: stripped, showLimits: showLimits === true && enabled };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Capture a "limits at start" snapshot for the given tool and append a
|
|
255
|
+
* formatted block to `infoBlock`. Errors are logged via the optional
|
|
256
|
+
* `verbose` flag and silently swallowed so a transient API failure does not
|
|
257
|
+
* abort the user's command.
|
|
258
|
+
*
|
|
259
|
+
* @param {Object} options
|
|
260
|
+
* @param {string} options.infoBlock - Existing infoBlock to append to
|
|
261
|
+
* @param {string} [options.tool='claude']
|
|
262
|
+
* @param {boolean} [options.verbose=false]
|
|
263
|
+
* @param {Object} options.limitsLib - { getCachedClaudeLimits, getCachedCodexLimits }
|
|
264
|
+
* @param {string} [options.commandLabel='command'] - For verbose logging
|
|
265
|
+
* @returns {Promise<{ infoBlock: string, limitsAtStart: Object|null }>}
|
|
266
|
+
*/
|
|
267
|
+
export async function captureStartSnapshotAndAppend({ infoBlock, tool = 'claude', verbose = false, limitsLib, commandLabel = 'command' } = {}) {
|
|
268
|
+
let limitsAtStart = null;
|
|
269
|
+
let nextInfoBlock = infoBlock || '';
|
|
270
|
+
try {
|
|
271
|
+
limitsAtStart = await captureLimitsSnapshot({ tool, verbose, limitsLib });
|
|
272
|
+
const block = formatLimitsSnapshotBlock(limitsAtStart, { title: '📊 Limits at start' });
|
|
273
|
+
if (block) nextInfoBlock = appendInfoSection(nextInfoBlock, block);
|
|
274
|
+
} catch (e) {
|
|
275
|
+
if (verbose) console.log(`[VERBOSE] ${commandLabel} --show-limits snapshot failed: ${e?.message || e}`);
|
|
276
|
+
}
|
|
277
|
+
return { infoBlock: nextInfoBlock, limitsAtStart };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const SHOW_LIMITS_FLAG_NAME = SHOW_LIMITS_FLAG;
|
|
281
|
+
export const NO_SHOW_LIMITS_FLAG_NAME = NO_SHOW_LIMITS_FLAG;
|
|
@@ -48,6 +48,9 @@ class SolveQueueItem {
|
|
|
48
48
|
this.urlContext = options.urlContext || null;
|
|
49
49
|
// Issue #1688: requester user ID for /subscribe duplicate-suppression.
|
|
50
50
|
this.requesterUserId = options.ctx?.from?.id ?? null;
|
|
51
|
+
// Issue #594: --show-limits virtual option (hive-telegram-bot only).
|
|
52
|
+
this.showLimits = options.showLimits === true;
|
|
53
|
+
this.limitsAtStart = options.limitsAtStart || null;
|
|
51
54
|
this.createdAt = new Date();
|
|
52
55
|
this.startedAt = null;
|
|
53
56
|
this.status = QueueItemStatus.QUEUED;
|
|
@@ -1407,6 +1410,11 @@ export function createQueueExecuteCallback(executeStartScreen, trackSessionFn) {
|
|
|
1407
1410
|
// notifying the requester twice via /subscribe.
|
|
1408
1411
|
urlContext: item.urlContext || null,
|
|
1409
1412
|
requesterUserId: item.requesterUserId ?? null,
|
|
1413
|
+
// Issue #594: --show-limits virtual option carries the start-of-task
|
|
1414
|
+
// snapshot from the queueing point through to the completion
|
|
1415
|
+
// handler so it can render an end-of-task delta.
|
|
1416
|
+
showLimits: item.showLimits === true,
|
|
1417
|
+
limitsAtStart: item.limitsAtStart || null,
|
|
1410
1418
|
});
|
|
1411
1419
|
}
|
|
1412
1420
|
}
|
|
@@ -83,7 +83,7 @@ export function appendPullRequestLine(infoBlock, pullRequestUrl) {
|
|
|
83
83
|
return [...before, prLine, ...after].join('\n');
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
export function formatSessionCompletionMessage({ sessionName, sessionInfo, statusResult = null, observedEndTime = new Date(), exitCode = null, infoBlock = '', pullRequestUrl = null } = {}) {
|
|
86
|
+
export function formatSessionCompletionMessage({ sessionName, sessionInfo, statusResult = null, observedEndTime = new Date(), exitCode = null, infoBlock = '', pullRequestUrl = null, extraSections = [] } = {}) {
|
|
87
87
|
const finalExitCode = getSessionCompletionExitCode({ exitCode, statusResult });
|
|
88
88
|
const failed = finalExitCode !== null && finalExitCode !== 0;
|
|
89
89
|
const statusEmoji = failed ? '❌' : '✅';
|
|
@@ -102,5 +102,12 @@ export function formatSessionCompletionMessage({ sessionName, sessionInfo, statu
|
|
|
102
102
|
message += `⏱️ Duration: ${formatSessionDurationSeconds(durationSeconds)}\n`;
|
|
103
103
|
message += `📊 Session: \`${sessionName || 'unknown'}\`${isolationInfo}${details}`;
|
|
104
104
|
|
|
105
|
+
// Issue #594: --show-limits virtual option appends snapshot/delta sections
|
|
106
|
+
// (Markdown code blocks) below the standard completion details.
|
|
107
|
+
const extras = (Array.isArray(extraSections) ? extraSections : []).filter(Boolean);
|
|
108
|
+
if (extras.length > 0) {
|
|
109
|
+
message += `\n\n${extras.join('\n\n')}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
105
112
|
return message;
|
|
106
113
|
}
|