@link-assistant/hive-mind 1.69.15 → 1.69.17

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.69.17
4
+
5
+ ### Patch Changes
6
+
7
+ - 46422f1: Increase the default `HIVE_MIND_USAGE_API_CACHE_TTL_MS` from 10 → 13 minutes so the Claude Usage API (`/api/oauth/usage`) is queried less frequently and we stop tripping the upstream "Resets in 3m Xs" rate-limit message. Operators can still override the value via the environment variable. Resolves #1798.
8
+
9
+ ## 1.69.16
10
+
11
+ ### Patch Changes
12
+
13
+ - ca0d938: Fix Telegram work-session completion failing with "Bad Request: can't parse entities" when the discovered Pull request URL contained Markdown-significant characters (`_`, `*`, `` ` ``, `[`). `appendPullRequestLine` (issue #1688) inserted the raw URL into a Markdown message even though the surrounding `Issue:` line was already escaped by `buildTelegramInfoBlock`, so a repo slug like `save_visiogetbb/pull/8` opened an italic entity at byte offset 318 that never closed. The appended `Pull request:` line is now passed through `escapeMarkdown`, and `safeReply`/`safeEditMessageText`/`installTelegramFormattingFallback` now log the offending byte-offset window and the plain-text fallback under `--verbose` so future parse errors point straight to the unescaped character. Resolves #1801.
14
+
3
15
  ## 1.69.15
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.69.15",
3
+ "version": "1.69.17",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -528,12 +528,14 @@ export const getClaudeEnv = (options = {}) => {
528
528
  // Cache TTL configurations (in milliseconds)
529
529
  // The Usage API (Claude limits) has stricter rate limiting than regular APIs
530
530
  // See: https://github.com/link-assistant/hive-mind/issues/1074
531
+ // See: https://github.com/link-assistant/hive-mind/issues/1798
531
532
  export const cacheTtl = {
532
533
  // General API cache TTL (GitHub API, etc.)
533
534
  api: parseIntWithDefault('HIVE_MIND_API_CACHE_TTL_MS', 3 * 60 * 1000), // 3 minutes
534
- // Claude Usage API cache TTL - must be at least 20 minutes to avoid rate limiting
535
- // The API returns null values when called too frequently
536
- usageApi: parseIntWithDefault('HIVE_MIND_USAGE_API_CACHE_TTL_MS', 10 * 60 * 1000), // 10 minutes
535
+ // Claude Usage API cache TTL - increased by 3 minutes (from 10 13) per issue #1798
536
+ // because users still hit "Resets in 3m xs" rate-limit responses. The API
537
+ // returns null values or 429 when called too frequently.
538
+ usageApi: parseIntWithDefault('HIVE_MIND_USAGE_API_CACHE_TTL_MS', 13 * 60 * 1000), // 13 minutes
537
539
  // System metrics cache TTL (RAM, CPU, disk)
538
540
  system: parseIntWithDefault('HIVE_MIND_SYSTEM_CACHE_TTL_MS', 2 * 60 * 1000), // 2 minutes
539
541
  };
@@ -1271,17 +1271,20 @@ export function formatCodexLimitsSection(codexLimits, codexError = null, options
1271
1271
  * Values are loaded from config.lib.mjs which supports environment variable overrides.
1272
1272
  *
1273
1273
  * IMPORTANT: The Claude Usage API has stricter rate limiting than regular APIs.
1274
- * Calling it more frequently than every 20 minutes may result in null values being returned.
1274
+ * Calling it too frequently may return null values or a 429 "Resets in Xm Xs" error.
1275
+ * Default raised from 10 → 13 minutes in issue #1798 (the previous 10-minute TTL still
1276
+ * occasionally tripped a ~3-minute rate-limit window).
1275
1277
  * See: https://github.com/link-assistant/hive-mind/issues/1074
1278
+ * See: https://github.com/link-assistant/hive-mind/issues/1798
1276
1279
  *
1277
1280
  * Configurable via environment variables:
1278
1281
  * - HIVE_MIND_API_CACHE_TTL_MS: General API cache TTL (default: 180000 = 3 minutes)
1279
- * - HIVE_MIND_USAGE_API_CACHE_TTL_MS: Claude Usage API cache TTL (default: 1200000 = 20 minutes)
1282
+ * - HIVE_MIND_USAGE_API_CACHE_TTL_MS: Claude Usage API cache TTL (default: 780000 = 13 minutes)
1280
1283
  * - HIVE_MIND_SYSTEM_CACHE_TTL_MS: System metrics cache TTL (default: 120000 = 2 minutes)
1281
1284
  */
1282
1285
  export const CACHE_TTL = {
1283
1286
  API: cacheTtl.api, // 3 minutes for regular API calls (GitHub)
1284
- USAGE_API: cacheTtl.usageApi, // 20 minutes for Claude Usage API (rate limited)
1287
+ USAGE_API: cacheTtl.usageApi, // 13 minutes for Claude Usage API (rate limited)
1285
1288
  SYSTEM: cacheTtl.system, // 2 minutes for system metrics (RAM, CPU, disk)
1286
1289
  };
1287
1290
 
@@ -1345,9 +1348,10 @@ export function resetLimitCache() {
1345
1348
 
1346
1349
  export async function getCachedClaudeLimits(verbose = false) {
1347
1350
  const cache = getLimitCache();
1348
- // Use USAGE_API TTL (20 minutes) for Claude limits to avoid rate limiting
1349
- // The Claude Usage API returns null values when called too frequently
1351
+ // Use USAGE_API TTL (13 min by default, see issue #1798) for Claude limits to avoid rate limiting.
1352
+ // The Claude Usage API returns null values or 429 errors when called too frequently.
1350
1353
  // See: https://github.com/link-assistant/hive-mind/issues/1074
1354
+ // See: https://github.com/link-assistant/hive-mind/issues/1798
1351
1355
  const cached = cache.get('claude', CACHE_TTL.USAGE_API);
1352
1356
  if (cached) {
1353
1357
  if (verbose) console.log('[VERBOSE] /limits-cache: Using cached Claude limits (TTL: ' + Math.round(CACHE_TTL.USAGE_API / 60000) + ' minutes)');
@@ -1365,8 +1369,9 @@ export async function getCachedClaudeLimits(verbose = false) {
1365
1369
  cache.set('claude', result, CACHE_TTL.USAGE_API);
1366
1370
  } else if (result.error && result.error.includes('Rate limited')) {
1367
1371
  // Cache rate-limit errors to prevent hammering the API
1368
- // Use the same 20-minute TTL as successful responses
1372
+ // Use the same USAGE_API TTL (13 min by default) as successful responses
1369
1373
  // See: https://github.com/link-assistant/hive-mind/issues/1446
1374
+ // See: https://github.com/link-assistant/hive-mind/issues/1798
1370
1375
  cache.set('claude-rate-limited', result, CACHE_TTL.USAGE_API);
1371
1376
  if (verbose) console.log('[VERBOSE] /limits-cache: Cached rate-limit error for ' + Math.round(CACHE_TTL.USAGE_API / 60000) + ' minutes');
1372
1377
  }
@@ -49,11 +49,30 @@ export function buildTelegramFormattingFallbackText(text, options = {}) {
49
49
  return `${getFormattingFallbackWarning(locale)}\n\n${stripTelegramMarkdown(text)}`;
50
50
  }
51
51
 
52
- function logFormattingFailure(scope, error, text, verbose = false) {
52
+ function logFormattingFailure(scope, error, text, verbose = false, fallbackText = null) {
53
53
  const message = error?.description || error?.message || String(error || '');
54
54
  console.error(`[telegram-bot] ${scope}: formatted Telegram message failed: ${message}`);
55
55
  if (verbose) {
56
- console.error(`[telegram-bot] ${scope}: Failing message (${Buffer.byteLength(String(text ?? ''), 'utf-8')} bytes): ${text}`);
56
+ const originalBytes = Buffer.byteLength(String(text ?? ''), 'utf-8');
57
+ console.error(`[telegram-bot] ${scope}: Failing message (${originalBytes} bytes): ${text}`);
58
+ // Issue #1801: when the parser rejects an entity, surface the byte offset
59
+ // from the error along with a small window of the message around it to
60
+ // make pinpointing the offending character trivial on the next iteration.
61
+ const offsetMatch = /byte offset (\d+)/i.exec(message);
62
+ if (offsetMatch) {
63
+ const offset = Number(offsetMatch[1]);
64
+ const buf = Buffer.from(String(text ?? ''), 'utf-8');
65
+ const start = Math.max(0, offset - 32);
66
+ const end = Math.min(buf.length, offset + 32);
67
+ // Decode the window; replacement character is used for bytes that fall
68
+ // mid-codepoint so we still print *something* useful.
69
+ const window = buf.slice(start, end).toString('utf-8');
70
+ console.error(`[telegram-bot] ${scope}: Byte offset ${offset} context [${start}..${end}]: ${JSON.stringify(window)}`);
71
+ }
72
+ if (fallbackText !== null) {
73
+ const fallbackBytes = Buffer.byteLength(String(fallbackText ?? ''), 'utf-8');
74
+ console.error(`[telegram-bot] ${scope}: Fallback message (${fallbackBytes} bytes): ${fallbackText}`);
75
+ }
57
76
  }
58
77
  }
59
78
 
@@ -65,8 +84,8 @@ export async function safeReply(ctx, text, options = {}) {
65
84
  return await ctx.reply(text, firstOptions);
66
85
  } catch (error) {
67
86
  if (!isTelegramFormattingError(error)) throw error;
68
- logFormattingFailure('safeReply', error, text, verbose);
69
87
  const fallbackText = buildTelegramFormattingFallbackText(text, { fallbackLocale });
88
+ logFormattingFailure('safeReply', error, text, verbose, fallbackText);
70
89
  return await ctx.reply(fallbackText, { ...telegramOptions, parse_mode: undefined });
71
90
  }
72
91
  }
@@ -78,8 +97,8 @@ export async function safeEditMessageText(telegram, chatId, messageId, inlineMes
78
97
  return await telegram.editMessageText(chatId, messageId, inlineMessageId, text, firstOptions);
79
98
  } catch (error) {
80
99
  if (!isTelegramFormattingError(error)) throw error;
81
- logFormattingFailure('safeEditMessageText', error, text, verbose);
82
100
  const fallbackText = buildTelegramFormattingFallbackText(text, { fallbackLocale });
101
+ logFormattingFailure('safeEditMessageText', error, text, verbose, fallbackText);
83
102
  return await telegram.editMessageText(chatId, messageId, inlineMessageId, fallbackText, { ...telegramOptions, parse_mode: undefined });
84
103
  }
85
104
  }
@@ -98,9 +117,10 @@ function wrapTelegramMethod(telegram, methodName, textIndex, optionsIndex, defau
98
117
  return await original.apply(this, args);
99
118
  } catch (error) {
100
119
  if (!isTelegramFormattingError(error) || typeof text !== 'string') throw error;
101
- logFormattingFailure(methodName, error, text, verbose || defaults.verbose);
120
+ const fallbackText = buildTelegramFormattingFallbackText(text, { fallbackLocale: fallbackLocale || defaults.fallbackLocale });
121
+ logFormattingFailure(methodName, error, text, verbose || defaults.verbose, fallbackText);
102
122
  const retryArgs = [...args];
103
- retryArgs[textIndex] = buildTelegramFormattingFallbackText(text, { fallbackLocale: fallbackLocale || defaults.fallbackLocale });
123
+ retryArgs[textIndex] = fallbackText;
104
124
  retryArgs[optionsIndex] = { ...telegramOptions, parse_mode: undefined };
105
125
  return await original.apply(this, retryArgs);
106
126
  }
@@ -5,8 +5,8 @@
5
5
  * the args before they are forwarded to /solve, /hive (or /task --split). When
6
6
  * set, the bot:
7
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).
8
+ * shared cached helpers in limits.lib.mjs (USAGE_API TTL: 13 minutes by
9
+ * default — see issue #1798 — to avoid rate limiting).
10
10
  * 2. Embeds a compact "Limits at start" snapshot below the infoBlock so the
11
11
  * starting/executing message shows the user how much budget they had at
12
12
  * the moment the command was queued.
@@ -1,4 +1,5 @@
1
1
  import { t } from './i18n.lib.mjs';
2
+ import { escapeMarkdown } from './telegram-markdown.lib.mjs';
2
3
 
3
4
  const FAILURE_STATUSES = new Set(['failed', 'cancelled', 'canceled', 'error']);
4
5
 
@@ -74,7 +75,7 @@ export function formatExecutingWorkSessionMessage({ sessionName = 'unknown', iso
74
75
  */
75
76
  export function appendPullRequestLine(infoBlock, pullRequestUrl, { locale = null } = {}) {
76
77
  if (!pullRequestUrl || !infoBlock) return infoBlock || '';
77
- if (infoBlock.includes(pullRequestUrl)) return infoBlock;
78
+ if (infoBlock.includes(pullRequestUrl) || infoBlock.includes(escapeMarkdown(pullRequestUrl))) return infoBlock;
78
79
 
79
80
  const lines = infoBlock.split('\n');
80
81
  let lastUrlLineIdx = -1;
@@ -89,7 +90,10 @@ export function appendPullRequestLine(infoBlock, pullRequestUrl, { locale = null
89
90
  lastUrlLineIdx = i;
90
91
  }
91
92
  }
92
- const prLine = `${text(locale, 'telegram.info_pull_request_label', 'Pull request')}: ${pullRequestUrl}`;
93
+ // Issue #1801: escape underscores/asterisks so Markdown parser doesn't open
94
+ // an entity on URLs like .../save_visiogetbb/pull/8 that the Issue: line
95
+ // above already had escaped at buildTelegramInfoBlock time.
96
+ const prLine = `${text(locale, 'telegram.info_pull_request_label', 'Pull request')}: ${escapeMarkdown(pullRequestUrl)}`;
93
97
  if (lastUrlLineIdx === -1) {
94
98
  return `${infoBlock}\n${prLine}`;
95
99
  }