@link-assistant/hive-mind 2.5.6 → 2.6.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.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/codex-rate-limit-windows.lib.mjs +27 -0
- package/src/env-config.lib.mjs +13 -0
- package/src/limits-i18n.lib.mjs +6 -0
- package/src/limits.lib.mjs +38 -10
- package/src/locales/en.lino +14 -0
- package/src/locales/hi.lino +14 -0
- package/src/locales/ru.lino +14 -0
- package/src/locales/zh.lino +14 -0
- package/src/queue-config.lib.mjs +31 -9
- package/src/telegram-bot.mjs +3 -3
- package/src/telegram-rate-limit.lib.mjs +167 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.6.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9ab8794: Make `HIVE_MIND_MIN_START_INTERVAL_MS` authoritative, warn when it is below the recommended 10-minute interval, and deprecate the redundant floor variable.
|
|
8
|
+
|
|
9
|
+
## 2.6.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- c031b7b: Handle weekly-only Codex usage windows and add Telegram Bot API rolling rate-limit telemetry to `/limits`.
|
|
14
|
+
|
|
3
15
|
## 2.5.6
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classify raw Codex windows by duration instead of response position.
|
|
3
|
+
*
|
|
4
|
+
* Historically the primary window was five hours and the secondary window was
|
|
5
|
+
* weekly. Weekly-only accounts can put the weekly value in the primary slot.
|
|
6
|
+
*/
|
|
7
|
+
export function classifyCodexRateLimitWindows(rateLimit) {
|
|
8
|
+
const primary = rateLimit?.primary_window;
|
|
9
|
+
const secondary = rateLimit?.secondary_window;
|
|
10
|
+
const windows = [primary, secondary].filter(Boolean);
|
|
11
|
+
const hasDurationMetadata = windows.some(window => Number.isFinite(Number(window?.limit_window_seconds)));
|
|
12
|
+
|
|
13
|
+
if (!hasDurationMetadata) {
|
|
14
|
+
return { sessionWindow: primary, weeklyWindow: secondary };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const sessionWindow = windows.find(window => {
|
|
18
|
+
const seconds = Number(window?.limit_window_seconds);
|
|
19
|
+
return Number.isFinite(seconds) && seconds > 0 && seconds < 24 * 60 * 60;
|
|
20
|
+
});
|
|
21
|
+
const weeklyWindow = windows.find(window => {
|
|
22
|
+
const seconds = Number(window?.limit_window_seconds);
|
|
23
|
+
return Number.isFinite(seconds) && seconds >= 24 * 60 * 60;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return { sessionWindow, weeklyWindow };
|
|
27
|
+
}
|
package/src/env-config.lib.mjs
CHANGED
|
@@ -16,6 +16,19 @@ function readExplicitEnv(envVar) {
|
|
|
16
16
|
return rawValue === undefined ? null : rawValue;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Emit a warning once when an environment variable was explicitly configured.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} envVar
|
|
23
|
+
* @param {string} warningKey
|
|
24
|
+
* @param {string} message
|
|
25
|
+
*/
|
|
26
|
+
export function warnExplicitEnv(envVar, warningKey, message) {
|
|
27
|
+
if (readExplicitEnv(envVar) !== null) {
|
|
28
|
+
warnOnce(`${warningKey}:${envVar}`, message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
function warnInvalid(envVar, rawValue, type, defaultValue, scope) {
|
|
20
33
|
warnOnce(`invalid:${envVar}`, `[${scope}] ${envVar}=${JSON.stringify(rawValue)} is not a valid ${type}; using default ${defaultValue}.`);
|
|
21
34
|
}
|
package/src/limits-i18n.lib.mjs
CHANGED
|
@@ -85,6 +85,12 @@ const ENGLISH_LIMITS = {
|
|
|
85
85
|
subscription_detail_trial_ends: 'trial ends {{time}}',
|
|
86
86
|
subscription_detail_trial_ends_in: 'trial ends in {{duration}}; {{time}}',
|
|
87
87
|
subscription_status: 'Subscription: {{status}}',
|
|
88
|
+
telegram_api: 'Telegram Bot API (local rolling telemetry)',
|
|
89
|
+
telegram_global_window: '{{used}}/{{limit}} messages in 1s',
|
|
90
|
+
telegram_group_window: '{{used}}/{{limit}} messages in busiest group over 1m',
|
|
91
|
+
telegram_last_rate_limit: 'Last 429: {{method}}',
|
|
92
|
+
telegram_rate_limit_responses: '429 responses since startup: {{count}}',
|
|
93
|
+
telegram_retry_in: 'retry in {{seconds}}s',
|
|
88
94
|
trial_ends: 'Trial ends {{time}}',
|
|
89
95
|
trial_ends_in: 'Trial ends in {{duration}} ({{time}})',
|
|
90
96
|
unavailable: 'unavailable',
|
package/src/limits.lib.mjs
CHANGED
|
@@ -12,9 +12,11 @@ import { promisify } from 'node:util';
|
|
|
12
12
|
import dayjs from 'dayjs';
|
|
13
13
|
import utc from 'dayjs/plugin/utc.js';
|
|
14
14
|
|
|
15
|
+
import { classifyCodexRateLimitWindows } from './codex-rate-limit-windows.lib.mjs';
|
|
15
16
|
import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller. execGhWithRetry adds transient-network retry (#1756).
|
|
16
17
|
import { formatLimitResetsAt, formatLimitResetsIn, formatLocalizedCurrentTime, formatLocalizedRelativeTime, formatLocalizedResetTime, localizeCompactDuration, lt, resolveLimitLocale } from './limits-i18n.lib.mjs';
|
|
17
18
|
import { formatSubscriptionHeading, formatSubscriptionLines, getCachedClaudeSubscription, getCachedCodexSubscription, getClaudeSubscriptionInfo, getCodexSubscriptionInfo } from './limits-subscription.lib.mjs';
|
|
19
|
+
import { getTelegramRateLimits } from './telegram-rate-limit.lib.mjs';
|
|
18
20
|
export { getCachedClaudeSubscription, getCachedCodexSubscription, getClaudeSubscriptionInfo, getCodexSubscriptionInfo };
|
|
19
21
|
// Initialize dayjs plugins
|
|
20
22
|
dayjs.extend(utc);
|
|
@@ -75,6 +77,15 @@ function mapCodexWindow(window) {
|
|
|
75
77
|
};
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
export function mapCodexRateLimitWindows(rateLimit) {
|
|
81
|
+
const { sessionWindow, weeklyWindow } = classifyCodexRateLimitWindows(rateLimit);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
currentSession: mapCodexWindow(sessionWindow),
|
|
85
|
+
allModels: mapCodexWindow(weeklyWindow),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
export async function readCodexAuth(authPath = DEFAULT_CODEX_AUTH_PATH, verbose = false) {
|
|
79
90
|
try {
|
|
80
91
|
const content = await readFile(authPath, 'utf-8');
|
|
@@ -936,8 +947,7 @@ export async function getCodexUsageLimits(verbose = false, authPath = DEFAULT_CO
|
|
|
936
947
|
}
|
|
937
948
|
|
|
938
949
|
const usage = {
|
|
939
|
-
|
|
940
|
-
allModels: mapCodexWindow(data?.rate_limit?.secondary_window),
|
|
950
|
+
...mapCodexRateLimitWindows(data?.rate_limit),
|
|
941
951
|
sonnetOnly: {
|
|
942
952
|
percentage: null,
|
|
943
953
|
resetTime: null,
|
|
@@ -949,8 +959,7 @@ export async function getCodexUsageLimits(verbose = false, authPath = DEFAULT_CO
|
|
|
949
959
|
? data.additional_rate_limits.map(limit => ({
|
|
950
960
|
limitId: limit?.metered_feature || null,
|
|
951
961
|
limitName: limit?.limit_name || limit?.metered_feature || 'additional',
|
|
952
|
-
|
|
953
|
-
allModels: mapCodexWindow(limit?.rate_limit?.secondary_window),
|
|
962
|
+
...mapCodexRateLimitWindows(limit?.rate_limit),
|
|
954
963
|
allowed: limit?.rate_limit?.allowed ?? null,
|
|
955
964
|
limitReached: limit?.rate_limit?.limit_reached ?? null,
|
|
956
965
|
}))
|
|
@@ -1116,6 +1125,24 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
1116
1125
|
sections.push(section);
|
|
1117
1126
|
}
|
|
1118
1127
|
|
|
1128
|
+
const telegramRateLimit = options?.telegramRateLimit || null;
|
|
1129
|
+
if (telegramRateLimit) {
|
|
1130
|
+
let section = `${lt('telegram_api', {}, { locale })}\n`;
|
|
1131
|
+
const global = telegramRateLimit.global;
|
|
1132
|
+
section += `${getProgressBar(global.usedPercentage)} ${global.usedPercentage}% ${lt('used', {}, { locale })}\n`;
|
|
1133
|
+
section += `${lt('telegram_global_window', { used: global.used, limit: global.limit }, { locale })}\n`;
|
|
1134
|
+
const group = telegramRateLimit.busiestGroup;
|
|
1135
|
+
section += `${getProgressBar(group.usedPercentage)} ${group.usedPercentage}% ${lt('used', {}, { locale })}\n`;
|
|
1136
|
+
section += `${lt('telegram_group_window', { used: group.used, limit: group.limit }, { locale })}\n`;
|
|
1137
|
+
section += `${lt('telegram_rate_limit_responses', { count: telegramRateLimit.rateLimitResponses }, { locale })}\n`;
|
|
1138
|
+
if (telegramRateLimit.lastRateLimit) {
|
|
1139
|
+
const retry = telegramRateLimit.lastRateLimit.retryRemainingSeconds;
|
|
1140
|
+
const retryText = retry === null ? '' : `, ${lt('telegram_retry_in', { seconds: retry }, { locale })}`;
|
|
1141
|
+
section += `${lt('telegram_last_rate_limit', { method: telegramRateLimit.lastRateLimit.method }, { locale })}${retryText}\n`;
|
|
1142
|
+
}
|
|
1143
|
+
sections.push(section);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1119
1146
|
const claudeHeading = formatSubscriptionHeading('claude', subscription, { locale });
|
|
1120
1147
|
const useShortClaudeLabels = Boolean(claudeHeading);
|
|
1121
1148
|
const claudeSections = [];
|
|
@@ -1194,7 +1221,7 @@ export function formatCodexLimitsSection(codexLimits, codexError = null, options
|
|
|
1194
1221
|
const sessionSection = formatLimitWindowSection(useTitledLayout ? lt('five_hour_limit_session', {}, { locale }) : lt('codex_5_hour_session', {}, { locale }), usage?.currentSession, 5, DISPLAY_THRESHOLDS.CODEX_5_HOUR_SESSION, { locale });
|
|
1195
1222
|
const weeklySection = formatLimitWindowSection(useTitledLayout ? lt('current_week', {}, { locale }) : lt('current_week_all_models', {}, { locale }), usage?.allModels, 168, DISPLAY_THRESHOLDS.CODEX_WEEKLY, { locale });
|
|
1196
1223
|
|
|
1197
|
-
section +=
|
|
1224
|
+
section += [sessionSection, weeklySection].filter((_, index) => (index === 0 ? hasLimitPercentage(usage?.currentSession) : hasLimitPercentage(usage?.allModels))).join('\n');
|
|
1198
1225
|
|
|
1199
1226
|
const visibleAdditionalRateLimits = additionalRateLimits.filter(limit => hasPositivePercentage(limit.allModels?.percentage));
|
|
1200
1227
|
if (visibleAdditionalRateLimits.length > 0) {
|
|
@@ -1202,9 +1229,10 @@ export function formatCodexLimitsSection(codexLimits, codexError = null, options
|
|
|
1202
1229
|
for (const limit of visibleAdditionalRateLimits) {
|
|
1203
1230
|
const sessionPct = limit.currentSession?.percentage;
|
|
1204
1231
|
const weeklyPct = limit.allModels?.percentage;
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
|
|
1232
|
+
const windowTexts = [];
|
|
1233
|
+
if (sessionPct !== null && sessionPct !== undefined) windowTexts.push(`${lt('session', {}, { locale })} ${Math.floor(sessionPct)}%`);
|
|
1234
|
+
if (weeklyPct !== null && weeklyPct !== undefined) windowTexts.push(`${lt('week', {}, { locale })} ${Math.floor(weeklyPct)}%`);
|
|
1235
|
+
section += `${limit.limitName}: ${windowTexts.join(', ')}\n`;
|
|
1208
1236
|
}
|
|
1209
1237
|
}
|
|
1210
1238
|
|
|
@@ -1409,8 +1437,8 @@ export async function getCachedDiskInfo(verbose = false) {
|
|
|
1409
1437
|
}
|
|
1410
1438
|
|
|
1411
1439
|
export async function getAllCachedLimits(verbose = false) {
|
|
1412
|
-
const [claude, codex, github, memory, cpu, disk, claudeSubscription, codexSubscription] = await Promise.all([getCachedClaudeLimits(verbose), getCachedCodexLimits(verbose), getCachedGitHubLimits(verbose), getCachedMemoryInfo(verbose), getCachedCpuInfo(verbose), getCachedDiskInfo(verbose), getCachedClaudeSubscription(verbose), getCachedCodexSubscription(verbose)]);
|
|
1413
|
-
return { claude, codex, github, memory, cpu, disk, claudeSubscription, codexSubscription };
|
|
1440
|
+
const [claude, codex, github, memory, cpu, disk, claudeSubscription, codexSubscription, telegram] = await Promise.all([getCachedClaudeLimits(verbose), getCachedCodexLimits(verbose), getCachedGitHubLimits(verbose), getCachedMemoryInfo(verbose), getCachedCpuInfo(verbose), getCachedDiskInfo(verbose), getCachedClaudeSubscription(verbose), getCachedCodexSubscription(verbose), getTelegramRateLimits(verbose)]);
|
|
1441
|
+
return { claude, codex, github, memory, cpu, disk, claudeSubscription, codexSubscription, telegram };
|
|
1414
1442
|
}
|
|
1415
1443
|
|
|
1416
1444
|
export default {
|
package/src/locales/en.lino
CHANGED
|
@@ -334,6 +334,20 @@ en
|
|
|
334
334
|
label "Subscription ends {{time}}"
|
|
335
335
|
in "Subscription ends in {{duration}} ({{time}})"
|
|
336
336
|
status "Subscription: {{status}}"
|
|
337
|
+
telegram
|
|
338
|
+
api "Telegram Bot API (local rolling telemetry)"
|
|
339
|
+
global
|
|
340
|
+
window "{{used}}/{{limit}} messages in 1s"
|
|
341
|
+
group
|
|
342
|
+
window "{{used}}/{{limit}} messages in busiest group over 1m"
|
|
343
|
+
last
|
|
344
|
+
rate
|
|
345
|
+
limit "Last 429: {{method}}"
|
|
346
|
+
rate
|
|
347
|
+
limit
|
|
348
|
+
responses "429 responses since startup: {{count}}"
|
|
349
|
+
retry
|
|
350
|
+
in "retry in {{seconds}}s"
|
|
337
351
|
trial
|
|
338
352
|
ends
|
|
339
353
|
label "Trial ends {{time}}"
|
package/src/locales/hi.lino
CHANGED
|
@@ -334,6 +334,20 @@ hi
|
|
|
334
334
|
label "सदस्यता समाप्त होगी {{time}}"
|
|
335
335
|
in "सदस्यता {{duration}} में समाप्त होगी ({{time}})"
|
|
336
336
|
status "सदस्यता: {{status}}"
|
|
337
|
+
telegram
|
|
338
|
+
api "Telegram Bot API (स्थानीय रोलिंग टेलीमेट्री)"
|
|
339
|
+
global
|
|
340
|
+
window "1 सेकंड में {{used}}/{{limit}} संदेश"
|
|
341
|
+
group
|
|
342
|
+
window "सबसे व्यस्त समूह में 1 मिनट में {{used}}/{{limit}} संदेश"
|
|
343
|
+
last
|
|
344
|
+
rate
|
|
345
|
+
limit "अंतिम 429: {{method}}"
|
|
346
|
+
rate
|
|
347
|
+
limit
|
|
348
|
+
responses "स्टार्टअप के बाद 429 प्रतिक्रियाएँ: {{count}}"
|
|
349
|
+
retry
|
|
350
|
+
in "{{seconds}} सेकंड में पुनः प्रयास"
|
|
337
351
|
trial
|
|
338
352
|
ends
|
|
339
353
|
label "ट्रायल समाप्त होगा {{time}}"
|
package/src/locales/ru.lino
CHANGED
|
@@ -334,6 +334,20 @@ ru
|
|
|
334
334
|
label "Подписка заканчивается {{time}}"
|
|
335
335
|
in "Подписка заканчивается через {{duration}} ({{time}})"
|
|
336
336
|
status "Подписка: {{status}}"
|
|
337
|
+
telegram
|
|
338
|
+
api "Telegram Bot API (локальная скользящая статистика)"
|
|
339
|
+
global
|
|
340
|
+
window "{{used}}/{{limit}} сообщений за 1 с"
|
|
341
|
+
group
|
|
342
|
+
window "{{used}}/{{limit}} сообщений в самой активной группе за 1 мин"
|
|
343
|
+
last
|
|
344
|
+
rate
|
|
345
|
+
limit "Последний ответ 429: {{method}}"
|
|
346
|
+
rate
|
|
347
|
+
limit
|
|
348
|
+
responses "Ответов 429 с момента запуска: {{count}}"
|
|
349
|
+
retry
|
|
350
|
+
in "повтор через {{seconds}} с"
|
|
337
351
|
trial
|
|
338
352
|
ends
|
|
339
353
|
label "Пробный период заканчивается {{time}}"
|
package/src/locales/zh.lino
CHANGED
|
@@ -334,6 +334,20 @@ zh
|
|
|
334
334
|
label "订阅结束于 {{time}}"
|
|
335
335
|
in "订阅将在 {{duration}} 后结束 ({{time}})"
|
|
336
336
|
status "订阅: {{status}}"
|
|
337
|
+
telegram
|
|
338
|
+
api "Telegram Bot API(本地滚动遥测)"
|
|
339
|
+
global
|
|
340
|
+
window "1 秒内 {{used}}/{{limit}} 条消息"
|
|
341
|
+
group
|
|
342
|
+
window "最繁忙群组 1 分钟内 {{used}}/{{limit}} 条消息"
|
|
343
|
+
last
|
|
344
|
+
rate
|
|
345
|
+
limit "最近一次 429:{{method}}"
|
|
346
|
+
rate
|
|
347
|
+
limit
|
|
348
|
+
responses "启动后的 429 响应:{{count}}"
|
|
349
|
+
retry
|
|
350
|
+
in "{{seconds}} 秒后重试"
|
|
337
351
|
trial
|
|
338
352
|
ends
|
|
339
353
|
label "试用结束于 {{time}}"
|
package/src/queue-config.lib.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { ensureUseM } from './use-m-bootstrap.lib.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import { parseIntegerEnv, parseNumberEnv, warnExplicitEnv } from './env-config.lib.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Queue Configuration Module
|
|
@@ -183,7 +183,32 @@ const parseFloatWithDefault = (envVar, defaultValue) => parseNumberEnv(envVar, d
|
|
|
183
183
|
const parseIntWithDefault = (envVar, defaultValue) => parseIntegerEnv(envVar, defaultValue, { scope: 'queue-config' });
|
|
184
184
|
|
|
185
185
|
const DEFAULT_MINIMUM_START_INTERVAL_MS = 10 * 60 * 1000;
|
|
186
|
-
const
|
|
186
|
+
const START_INTERVAL_ENV = 'HIVE_MIND_MIN_START_INTERVAL_MS';
|
|
187
|
+
const LEGACY_START_INTERVAL_FLOOR_ENV = 'HIVE_MIND_MIN_START_INTERVAL_FLOOR_MS';
|
|
188
|
+
|
|
189
|
+
function resolveMinimumStartIntervalMs() {
|
|
190
|
+
const hasExplicitInterval = process.env[START_INTERVAL_ENV] !== undefined;
|
|
191
|
+
const hasLegacyFloor = process.env[LEGACY_START_INTERVAL_FLOOR_ENV] !== undefined;
|
|
192
|
+
|
|
193
|
+
if (hasLegacyFloor) {
|
|
194
|
+
warnExplicitEnv(LEGACY_START_INTERVAL_FLOOR_ENV, 'deprecated', `[queue-config] ${LEGACY_START_INTERVAL_FLOOR_ENV} is deprecated; use ${START_INTERVAL_ENV} instead.`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let interval = DEFAULT_MINIMUM_START_INTERVAL_MS;
|
|
198
|
+
if (hasExplicitInterval) {
|
|
199
|
+
interval = parseIntWithDefault(START_INTERVAL_ENV, DEFAULT_MINIMUM_START_INTERVAL_MS);
|
|
200
|
+
} else if (hasLegacyFloor) {
|
|
201
|
+
interval = parseIntWithDefault(LEGACY_START_INTERVAL_FLOOR_ENV, DEFAULT_MINIMUM_START_INTERVAL_MS);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (hasExplicitInterval && interval < DEFAULT_MINIMUM_START_INTERVAL_MS) {
|
|
205
|
+
warnExplicitEnv(START_INTERVAL_ENV, 'recommendation', `[queue-config] ${START_INTERVAL_ENV}=${interval} is below the recommended ${DEFAULT_MINIMUM_START_INTERVAL_MS} ms; short intervals can start a backlog before host metrics settle (#2015).`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return interval;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const minimumStartIntervalMs = resolveMinimumStartIntervalMs();
|
|
187
212
|
|
|
188
213
|
// Parse links notation config from environment variable (if provided)
|
|
189
214
|
const linoConfig = parseQueueConfig(getenv('HIVE_MIND_QUEUE_CONFIG', ''));
|
|
@@ -267,13 +292,10 @@ export const QUEUE_CONFIG = {
|
|
|
267
292
|
// MIN_START_INTERVAL_MS: Minimum global spacing between task startups.
|
|
268
293
|
// Issue #2015: after resource thresholds clear, starting a backlog in a burst
|
|
269
294
|
// can kill the next batch before host metrics have time to settle.
|
|
270
|
-
// Issue #
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
scope: 'queue-config',
|
|
275
|
-
hint: 'Set HIVE_MIND_MIN_START_INTERVAL_FLOOR_MS to lower the minimum.',
|
|
276
|
-
}),
|
|
295
|
+
// Issue #2065: an explicit interval is authoritative. The deprecated floor
|
|
296
|
+
// remains a fallback for existing deployments, while the default remains the
|
|
297
|
+
// safe 10-minute interval introduced for issue #2015.
|
|
298
|
+
MIN_START_INTERVAL_MS: minimumStartIntervalMs,
|
|
277
299
|
CONSUMER_POLL_INTERVAL_MS: parseIntWithDefault('HIVE_MIND_CONSUMER_POLL_INTERVAL_MS', 60000), // 1 minute between queue checks
|
|
278
300
|
MESSAGE_UPDATE_INTERVAL_MS: parseIntWithDefault('HIVE_MIND_MESSAGE_UPDATE_INTERVAL_MS', 60000), // 1 minute between status message updates
|
|
279
301
|
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -329,6 +329,7 @@ const { executeStartScreen: executeStartScreenCommand, buildExecuteAndUpdateMess
|
|
|
329
329
|
const { isChatStopped, getChatStopInfo, getStoppedChatRejectMessage, DEFAULT_STOP_REASON } = await import('./telegram-start-stop-command.lib.mjs');
|
|
330
330
|
const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwarded: _isForwarded, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
|
|
331
331
|
const { installTelegramFormattingFallback, isTelegramFormattingError, isTelegramMessageTooLongError, safeEditMessageText, safeReply, TELEGRAM_TEXT_LIMIT } = await import('./telegram-safe-reply.lib.mjs');
|
|
332
|
+
const { installTelegramRateLimitTracker } = await import('./telegram-rate-limit.lib.mjs');
|
|
332
333
|
const { registerTerminalWatchCommand, startAutoTerminalWatchForSession } = await import('./telegram-terminal-watch-command.lib.mjs');
|
|
333
334
|
const { launchBotWithRetry } = await import('./telegram-bot-launcher.lib.mjs');
|
|
334
335
|
const { trackSession, untrackSession, startSessionMonitoring, hasActiveSessionForUrlAsync, findStoppableSessionByUrl, setSessionStore, setSessionLogger, resumeTrackedSessions, getActiveSessionCount } = await import('./session-monitor.lib.mjs');
|
|
@@ -352,12 +353,11 @@ await preloadAllLocales();
|
|
|
352
353
|
|
|
353
354
|
const telegrafModule = await use('telegraf');
|
|
354
355
|
const { Telegraf } = telegrafModule;
|
|
355
|
-
|
|
356
356
|
const bot = new Telegraf(BOT_TOKEN, {
|
|
357
357
|
handlerTimeout: Infinity, // Remove default 90s timeout; command handlers like /solve spawn long-running processes
|
|
358
358
|
});
|
|
359
359
|
installTelegramFormattingFallback(bot.telegram, { verbose: VERBOSE });
|
|
360
|
-
|
|
360
|
+
installTelegramRateLimitTracker(bot.telegram, { verbose: VERBOSE });
|
|
361
361
|
// Track bot startup time (Unix seconds to match Telegram's message.date format)
|
|
362
362
|
const BOT_START_TIME = Math.floor(Date.now() / 1000);
|
|
363
363
|
|
|
@@ -635,7 +635,7 @@ bot.command('limits', async ctx => {
|
|
|
635
635
|
const claudeSubscription = limits.claudeSubscription?.success ? limits.claudeSubscription.subscription : null;
|
|
636
636
|
const codexSubscription = limits.codexSubscription?.success ? limits.codexSubscription.subscription : null;
|
|
637
637
|
const codexSection = formatCodexLimitsSection(limits.codex.success ? limits.codex : null, codexError, { locale: userLocale, subscription: codexSubscription });
|
|
638
|
-
const message = t('telegram.usage_limits_title', {}, { locale: userLocale }) + '\n\n' + formatUsageMessage(limits.claude.success ? limits.claude.usage : null, limits.disk.success ? limits.disk.diskSpace : null, limits.github.success ? limits.github.githubRateLimit : null, limits.cpu.success ? limits.cpu.cpuLoad : null, limits.memory.success ? limits.memory.memory : null, claudeError, [codexSection, queueStatus], { locale: userLocale, subscription: claudeSubscription });
|
|
638
|
+
const message = t('telegram.usage_limits_title', {}, { locale: userLocale }) + '\n\n' + formatUsageMessage(limits.claude.success ? limits.claude.usage : null, limits.disk.success ? limits.disk.diskSpace : null, limits.github.success ? limits.github.githubRateLimit : null, limits.cpu.success ? limits.cpu.cpuLoad : null, limits.memory.success ? limits.memory.memory : null, claudeError, [codexSection, queueStatus], { locale: userLocale, subscription: claudeSubscription, telegramRateLimit: limits.telegram.telegramRateLimit });
|
|
639
639
|
await safeEditMessageText(ctx.telegram, fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown', fallbackLocale: userLocale, verbose: VERBOSE });
|
|
640
640
|
});
|
|
641
641
|
bot.command('version', async ctx => {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process Telegram Bot API rate-limit telemetry.
|
|
3
|
+
*
|
|
4
|
+
* Telegram does not expose a quota endpoint. The Bot API documents approximate
|
|
5
|
+
* outbound-message limits and reports actual flood control as error 429 with a
|
|
6
|
+
* ResponseParameters.retry_after value. This tracker therefore combines local
|
|
7
|
+
* rolling counters with the last observed server-side throttle.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const TELEGRAM_RATE_LIMITS = Object.freeze({
|
|
11
|
+
GLOBAL_MESSAGES_PER_SECOND: 30,
|
|
12
|
+
GROUP_MESSAGES_PER_MINUTE: 20,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const TRACKER_INSTALLED = Symbol.for('hiveMind.telegramRateLimitTrackerInstalled');
|
|
16
|
+
|
|
17
|
+
function isMessageMethod(method) {
|
|
18
|
+
const name = String(method || '');
|
|
19
|
+
if (/^sendChatAction$/i.test(name)) return false;
|
|
20
|
+
return /^(send|copyMessage$|forwardMessage$|editMessage)/i.test(name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toChatId(payload) {
|
|
24
|
+
const value = payload?.chat_id;
|
|
25
|
+
return value === null || value === undefined ? null : String(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isGroupChatId(chatId) {
|
|
29
|
+
return typeof chatId === 'string' && chatId.startsWith('-');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function percentage(used, limit) {
|
|
33
|
+
return limit > 0 ? Math.min(100, Math.round((used / limit) * 100)) : 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractRateLimitError(error) {
|
|
37
|
+
const response = error?.response || error;
|
|
38
|
+
const code = response?.error_code ?? response?.status ?? error?.code;
|
|
39
|
+
const description = response?.description || error?.description || error?.message || '';
|
|
40
|
+
if (Number(code) !== 429 && !/\b429\b|too many requests/i.test(String(description))) return null;
|
|
41
|
+
|
|
42
|
+
const rawRetryAfter = response?.parameters?.retry_after ?? error?.parameters?.retry_after;
|
|
43
|
+
const retryAfterSeconds = Number(rawRetryAfter);
|
|
44
|
+
return {
|
|
45
|
+
retryAfterSeconds: Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0 ? retryAfterSeconds : null,
|
|
46
|
+
description: String(description),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class TelegramRateLimitTracker {
|
|
51
|
+
constructor({ now = Date.now } = {}) {
|
|
52
|
+
this.now = now;
|
|
53
|
+
this.messageEvents = [];
|
|
54
|
+
this.totalApiRequests = 0;
|
|
55
|
+
this.rateLimitResponses = 0;
|
|
56
|
+
this.lastRateLimit = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
prune(now = this.now()) {
|
|
60
|
+
const oldestRelevant = now - 60_000;
|
|
61
|
+
this.messageEvents = this.messageEvents.filter(event => event.at > oldestRelevant);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
recordRequest(method, payload = {}) {
|
|
65
|
+
this.totalApiRequests++;
|
|
66
|
+
if (!isMessageMethod(method)) return;
|
|
67
|
+
|
|
68
|
+
const now = this.now();
|
|
69
|
+
const chatId = toChatId(payload);
|
|
70
|
+
this.messageEvents.push({ at: now, method: String(method), chatId, isGroup: isGroupChatId(chatId) });
|
|
71
|
+
this.prune(now);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
recordError(error, method, payload = {}) {
|
|
75
|
+
const rateLimit = extractRateLimitError(error);
|
|
76
|
+
if (!rateLimit) return false;
|
|
77
|
+
|
|
78
|
+
const observedAt = this.now();
|
|
79
|
+
this.rateLimitResponses++;
|
|
80
|
+
this.lastRateLimit = {
|
|
81
|
+
...rateLimit,
|
|
82
|
+
method: String(method || 'unknown'),
|
|
83
|
+
chatId: toChatId(payload),
|
|
84
|
+
observedAt,
|
|
85
|
+
retryUntil: rateLimit.retryAfterSeconds === null ? null : observedAt + rateLimit.retryAfterSeconds * 1_000,
|
|
86
|
+
};
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getSnapshot() {
|
|
91
|
+
const now = this.now();
|
|
92
|
+
this.prune(now);
|
|
93
|
+
|
|
94
|
+
const globalUsed = this.messageEvents.filter(event => event.at > now - 1_000).length;
|
|
95
|
+
const groupCounts = new Map();
|
|
96
|
+
for (const event of this.messageEvents) {
|
|
97
|
+
if (!event.isGroup || event.chatId === null) continue;
|
|
98
|
+
groupCounts.set(event.chatId, (groupCounts.get(event.chatId) || 0) + 1);
|
|
99
|
+
}
|
|
100
|
+
let busiestGroupChatId = null;
|
|
101
|
+
let busiestGroupUsed = 0;
|
|
102
|
+
for (const [chatId, count] of groupCounts) {
|
|
103
|
+
if (count > busiestGroupUsed) {
|
|
104
|
+
busiestGroupChatId = chatId;
|
|
105
|
+
busiestGroupUsed = count;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let lastRateLimit = null;
|
|
110
|
+
if (this.lastRateLimit) {
|
|
111
|
+
const retryRemainingSeconds = this.lastRateLimit.retryUntil === null ? null : Math.max(0, Math.ceil((this.lastRateLimit.retryUntil - now) / 1_000));
|
|
112
|
+
lastRateLimit = { ...this.lastRateLimit, retryRemainingSeconds };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
global: {
|
|
117
|
+
used: globalUsed,
|
|
118
|
+
limit: TELEGRAM_RATE_LIMITS.GLOBAL_MESSAGES_PER_SECOND,
|
|
119
|
+
usedPercentage: percentage(globalUsed, TELEGRAM_RATE_LIMITS.GLOBAL_MESSAGES_PER_SECOND),
|
|
120
|
+
},
|
|
121
|
+
busiestGroup: {
|
|
122
|
+
used: busiestGroupUsed,
|
|
123
|
+
limit: TELEGRAM_RATE_LIMITS.GROUP_MESSAGES_PER_MINUTE,
|
|
124
|
+
usedPercentage: percentage(busiestGroupUsed, TELEGRAM_RATE_LIMITS.GROUP_MESSAGES_PER_MINUTE),
|
|
125
|
+
chatId: busiestGroupChatId,
|
|
126
|
+
},
|
|
127
|
+
totalApiRequests: this.totalApiRequests,
|
|
128
|
+
rateLimitResponses: this.rateLimitResponses,
|
|
129
|
+
lastRateLimit,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const defaultTracker = new TelegramRateLimitTracker();
|
|
135
|
+
|
|
136
|
+
export function getTelegramRateLimits(verbose = false) {
|
|
137
|
+
const telegramRateLimit = defaultTracker.getSnapshot();
|
|
138
|
+
if (verbose) console.log('[VERBOSE] /limits Telegram Bot API telemetry:', JSON.stringify(telegramRateLimit, null, 2));
|
|
139
|
+
return { success: true, telegramRateLimit };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function installTelegramRateLimitTracker(telegram, { tracker = defaultTracker, verbose = false } = {}) {
|
|
143
|
+
if (!telegram || telegram[TRACKER_INSTALLED]) return telegram;
|
|
144
|
+
const originalCallApi = telegram.callApi;
|
|
145
|
+
if (typeof originalCallApi !== 'function') return telegram;
|
|
146
|
+
|
|
147
|
+
telegram.callApi = async function trackedCallApi(method, payload = {}, ...rest) {
|
|
148
|
+
tracker.recordRequest(method, payload);
|
|
149
|
+
if (verbose && isMessageMethod(method)) {
|
|
150
|
+
const snapshot = tracker.getSnapshot();
|
|
151
|
+
console.log(`[VERBOSE] Telegram Bot API ${method}: global ${snapshot.global.used}/${snapshot.global.limit} messages/1s; busiest group ${snapshot.busiestGroup.used}/${snapshot.busiestGroup.limit} messages/1m`);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
return await originalCallApi.call(this, method, payload, ...rest);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (tracker.recordError(error, method, payload)) {
|
|
157
|
+
const observed = tracker.getSnapshot().lastRateLimit;
|
|
158
|
+
console.warn(`[telegram-bot] Telegram Bot API rate limit: method=${observed.method} chat=${observed.chatId ?? 'unknown'} retry_after=${observed.retryAfterSeconds ?? 'unknown'}s`);
|
|
159
|
+
if (verbose) console.error('[VERBOSE] Telegram Bot API 429 response:', JSON.stringify(error?.response || { message: error?.message }, null, 2));
|
|
160
|
+
}
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
telegram[TRACKER_INSTALLED] = true;
|
|
166
|
+
return telegram;
|
|
167
|
+
}
|