@foxden-app/foxclaw 0.5.58 → 0.5.60

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/.env.example CHANGED
@@ -38,6 +38,9 @@ TELEGRAM_PREVIEW_THROTTLE_MS=800
38
38
  # Set to false if you prefer to keep "read files / edited files / ran command"
39
39
  # detail cards in the chat history.
40
40
  # TELEGRAM_DELETE_TOOL_DETAILS_AFTER_FINAL=true
41
+ # Delete time-sensitive interactive panels such as /auth and /threads after 30 minutes.
42
+ # The timer restarts whenever the panel is refreshed. Set to 0 to disable.
43
+ # TELEGRAM_PANEL_TTL_MS=1800000
41
44
  THREAD_LIST_LIMIT=10
42
45
  CODEX_CLI_BIN=/absolute/path/to/codex
43
46
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
4
4
 
5
+ ## 0.5.60 - 2026-06-21
6
+
7
+ ### 中文
8
+ - 修复 `/auth` 刷新时间测试对 Asia/Shanghai 时区的硬编码,使 Linux、macOS 和 UTC CI runner 都按各自本地时区验证;功能内容与 0.5.59 一并发布。
9
+
10
+ ### English
11
+ - Fixed the `/auth` reset-time test to respect the runner's local timezone across Linux, macOS, and UTC CI environments. This release publishes the feature set introduced in 0.5.59.
12
+
13
+ ## 0.5.59 - 2026-06-21
14
+
15
+ ### 中文
16
+ - `/auth` 富文本表格新增 Quota A、Quota B 的下次刷新时间;精确时间随额度快照持久化并可在节点间共享,旧快照安全显示 `-`。
17
+ - 修复 `/auth` 按钮回调后退化为普通消息的问题:切换、启停、筛选、翻页、修复、刷新和安全同步现在始终使用 RichMessage 编辑。
18
+ - 新增时效面板自动清理:`/auth`、`/threads` 和统一 `/setup` 面板默认空闲 30 分钟后删除,每次交互重新计时;可用 `TELEGRAM_PANEL_TTL_MS` 调整,设为 `0` 可关闭。
19
+
20
+ ### English
21
+ - Added next-reset columns for Quota A and Quota B in the `/auth` RichMessage table. Exact reset timestamps now persist with quota snapshots and can be shared across nodes; legacy snapshots safely show `-`.
22
+ - Fixed `/auth` callback refreshes degrading to plain messages. Switch, toggle, filter, pagination, repair, refresh, and safe-sync actions now consistently edit the panel as a RichMessage.
23
+ - Added automatic cleanup for time-sensitive panels. `/auth`, `/threads`, and unified `/setup` panels are deleted after 30 minutes of inactivity by default, with interaction resetting the timer. Configure `TELEGRAM_PANEL_TTL_MS`, or set it to `0` to disable cleanup.
24
+
5
25
  ## 0.5.58 - 2026-06-21
6
26
 
7
27
  ### 中文
package/dist/config.d.ts CHANGED
@@ -41,6 +41,7 @@ export interface AppConfig {
41
41
  telegramPollIntervalMs: number;
42
42
  telegramPreviewThrottleMs: number;
43
43
  telegramDeleteToolDetailsAfterFinal: boolean;
44
+ telegramPanelTtlMs: number;
44
45
  threadListLimit: number;
45
46
  statusPath: string;
46
47
  logPath: string;
package/dist/config.js CHANGED
@@ -78,6 +78,7 @@ export function loadConfig() {
78
78
  telegramPollIntervalMs: intEnv('TELEGRAM_POLL_INTERVAL_MS', 1200),
79
79
  telegramPreviewThrottleMs: intEnv('TELEGRAM_PREVIEW_THROTTLE_MS', 800),
80
80
  telegramDeleteToolDetailsAfterFinal: boolEnv('TELEGRAM_DELETE_TOOL_DETAILS_AFTER_FINAL', true),
81
+ telegramPanelTtlMs: intEnv('TELEGRAM_PANEL_TTL_MS', 30 * 60_000),
81
82
  threadListLimit: intEnv('THREAD_LIST_LIMIT', 10),
82
83
  statusPath: DEFAULT_STATUS_PATH,
83
84
  logPath: DEFAULT_LOG_PATH,
@@ -96,6 +96,7 @@ export declare class BridgeSessionCore {
96
96
  private proactiveAuthRefreshTimer;
97
97
  private proactiveAuthRefreshInProgress;
98
98
  private proactiveAuthRefreshStatus;
99
+ private stalePanelDeleteTimers;
99
100
  private attachedThreads;
100
101
  private botUsername;
101
102
  private lastError;
@@ -236,6 +237,9 @@ export declare class BridgeSessionCore {
236
237
  private editMessage;
237
238
  private editHtmlMessage;
238
239
  private editRichHtmlMessage;
240
+ private editRichInternalMessage;
241
+ private editAuthPanelMessage;
242
+ private scheduleStalePanelDeletion;
239
243
  private deleteMessage;
240
244
  private sendTyping;
241
245
  private sendObservedCliUserMessage;
@@ -152,6 +152,7 @@ export class BridgeSessionCore {
152
152
  proactiveAuthRefreshTimer = null;
153
153
  proactiveAuthRefreshInProgress = false;
154
154
  proactiveAuthRefreshStatus = null;
155
+ stalePanelDeleteTimers = new Map();
155
156
  attachedThreads = new Set();
156
157
  botUsername = null;
157
158
  lastError = null;
@@ -276,6 +277,10 @@ export class BridgeSessionCore {
276
277
  this.clearRestartPreviewRecoveryTimers();
277
278
  this.clearSelfUpdateStatusPoll();
278
279
  this.clearProactiveAuthRefreshTimer();
280
+ for (const timer of this.stalePanelDeleteTimers.values()) {
281
+ clearTimeout(timer);
282
+ }
283
+ this.stalePanelDeleteTimers.clear();
279
284
  await this.app.stop({ terminateServer: false });
280
285
  this.updateStatus();
281
286
  }
@@ -3431,6 +3436,35 @@ export class BridgeSessionCore {
3431
3436
  await this.editHtmlMessage(scopeId, messageId, fallbackHtml, inlineKeyboard);
3432
3437
  }
3433
3438
  }
3439
+ async editRichInternalMessage(scopeId, messageId, title, text, inlineKeyboard) {
3440
+ if (scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)) {
3441
+ await this.editMessage(scopeId, messageId, text, inlineKeyboard);
3442
+ return;
3443
+ }
3444
+ await this.editRichHtmlMessage(scopeId, messageId, formatRichInternalMessage(title, text), escapeTelegramHtml(text), inlineKeyboard);
3445
+ }
3446
+ async editAuthPanelMessage(scopeId, messageId, text, inlineKeyboard) {
3447
+ await this.editRichInternalMessage(scopeId, messageId, '/auth', text, inlineKeyboard);
3448
+ this.scheduleStalePanelDeletion(scopeId, messageId);
3449
+ }
3450
+ scheduleStalePanelDeletion(scopeId, messageId) {
3451
+ if (this.config.telegramPanelTtlMs <= 0 || parseWeixinBridgeScope(scopeId)) {
3452
+ return;
3453
+ }
3454
+ const key = `${scopeId}:${messageId}`;
3455
+ const existing = this.stalePanelDeleteTimers.get(key);
3456
+ if (existing) {
3457
+ clearTimeout(existing);
3458
+ }
3459
+ const timer = setTimeout(() => {
3460
+ this.stalePanelDeleteTimers.delete(key);
3461
+ void this.deleteMessage(scopeId, messageId).catch(error => {
3462
+ this.logger.warn('telegram.stale_panel_delete_failed', { scopeId, messageId, error: toErrorMeta(error) });
3463
+ });
3464
+ }, this.config.telegramPanelTtlMs);
3465
+ timer.unref();
3466
+ this.stalePanelDeleteTimers.set(key, timer);
3467
+ }
3434
3468
  async deleteMessage(scopeId, messageId) {
3435
3469
  await this.messaging.deleteMessage(scopeId, messageId);
3436
3470
  }
@@ -4859,6 +4893,7 @@ export class BridgeSessionCore {
4859
4893
  const authListMessage = renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null, record);
4860
4894
  const messageId = await this.sendRichInternalMessage(scopeId, '/auth', authListMessage, authChoiceKeyboard(locale, record));
4861
4895
  record.messageId = messageId;
4896
+ this.scheduleStalePanelDeletion(scopeId, messageId);
4862
4897
  }
4863
4898
  async handleAuthSyncCommand(scopeId, locale, args) {
4864
4899
  const action = args[0]?.toLowerCase() ?? 'status';
@@ -5664,12 +5699,12 @@ export class BridgeSessionCore {
5664
5699
  }
5665
5700
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_sync_safe_starting'));
5666
5701
  if (record.messageId !== null) {
5667
- await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_sync_safe_starting'), []);
5702
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_sync_safe_starting'), []);
5668
5703
  }
5669
5704
  const result = await this.runAuthSafeSyncAll();
5670
5705
  if (!result) {
5671
5706
  if (record.messageId !== null) {
5672
- await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_sync_disabled'), authChoiceKeyboard(locale, record));
5707
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_sync_disabled'), authChoiceKeyboard(locale, record));
5673
5708
  }
5674
5709
  return;
5675
5710
  }
@@ -5679,7 +5714,7 @@ export class BridgeSessionCore {
5679
5714
  record.createdAt = Date.now();
5680
5715
  clampCodexAuthListOffset(record);
5681
5716
  if (record.messageId !== null) {
5682
- await this.editMessage(event.scopeId, record.messageId, `${t(locale, 'auth_sync_safe_done', {
5717
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, `${t(locale, 'auth_sync_safe_done', {
5683
5718
  localSynced: result.localSynced,
5684
5719
  localSkipped: result.localSkipped,
5685
5720
  sent: result.sent,
@@ -5695,7 +5730,7 @@ export class BridgeSessionCore {
5695
5730
  }
5696
5731
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_refresh_all_confirm_short'));
5697
5732
  if (record.messageId !== null) {
5698
- await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_refresh_all_confirm_message'), authRefreshAllConfirmKeyboard(locale, record));
5733
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_refresh_all_confirm_message'), authRefreshAllConfirmKeyboard(locale, record));
5699
5734
  }
5700
5735
  return;
5701
5736
  }
@@ -5706,7 +5741,7 @@ export class BridgeSessionCore {
5706
5741
  record.candidates = state.candidates;
5707
5742
  record.createdAt = Date.now();
5708
5743
  if (record.messageId !== null) {
5709
- await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
5744
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
5710
5745
  }
5711
5746
  return;
5712
5747
  }
@@ -5717,12 +5752,12 @@ export class BridgeSessionCore {
5717
5752
  }
5718
5753
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_refresh_all_starting'));
5719
5754
  if (record.messageId !== null) {
5720
- await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_refresh_all_starting'), []);
5755
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_refresh_all_starting'), []);
5721
5756
  }
5722
5757
  const lease = await this.coordinator?.acquireAuthRefreshLease?.('auth refresh all');
5723
5758
  if (lease && !lease.ok) {
5724
5759
  if (record.messageId !== null) {
5725
- await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_refresh_all_lease_failed', { error: lease.reason ?? t(locale, 'unknown') }), authChoiceKeyboard(locale, record));
5760
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_refresh_all_lease_failed', { error: lease.reason ?? t(locale, 'unknown') }), authChoiceKeyboard(locale, record));
5726
5761
  }
5727
5762
  return;
5728
5763
  }
@@ -5738,7 +5773,7 @@ export class BridgeSessionCore {
5738
5773
  record.candidates = state.candidates;
5739
5774
  record.createdAt = Date.now();
5740
5775
  if (record.messageId !== null) {
5741
- await this.editMessage(event.scopeId, record.messageId, `${formatAuthRefreshAllResult(locale, result)}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
5776
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, `${formatAuthRefreshAllResult(locale, result)}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
5742
5777
  }
5743
5778
  return;
5744
5779
  }
@@ -5775,7 +5810,7 @@ export class BridgeSessionCore {
5775
5810
  clampCodexAuthListOffset(record);
5776
5811
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
5777
5812
  if (record.messageId !== null) {
5778
- await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
5813
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
5779
5814
  }
5780
5815
  }
5781
5816
  async handleAuthRepairMenuCallback(event, localId, index, locale) {
@@ -5795,7 +5830,7 @@ export class BridgeSessionCore {
5795
5830
  }
5796
5831
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_repair_actions_short'));
5797
5832
  if (record.messageId !== null) {
5798
- await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_repair_actions_message', { value: formatCodexAuthCandidateDisplayName(candidate.name) }), authRepairKeyboard(locale, record, index));
5833
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_repair_actions_message', { value: formatCodexAuthCandidateDisplayName(candidate.name) }), authRepairKeyboard(locale, record, index));
5799
5834
  }
5800
5835
  }
5801
5836
  async handleAuthRepairActionCallback(event, localId, action, index, locale) {
@@ -5820,7 +5855,7 @@ export class BridgeSessionCore {
5820
5855
  record.createdAt = Date.now();
5821
5856
  clampCodexAuthListOffset(record);
5822
5857
  if (record.messageId !== null) {
5823
- await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
5858
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
5824
5859
  }
5825
5860
  return;
5826
5861
  }
@@ -5831,7 +5866,7 @@ export class BridgeSessionCore {
5831
5866
  if (action === 'login') {
5832
5867
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'login_device_started'));
5833
5868
  if (record.messageId !== null) {
5834
- await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_repair_login_preparing', { value: candidate.name }), []);
5869
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_repair_login_preparing', { value: candidate.name }), []);
5835
5870
  }
5836
5871
  await this.startAuthRepairLogin(event.scopeId, locale, candidate);
5837
5872
  return;
@@ -5843,7 +5878,7 @@ export class BridgeSessionCore {
5843
5878
  record.createdAt = Date.now();
5844
5879
  clampCodexAuthListOffset(record);
5845
5880
  if (record.messageId !== null) {
5846
- await this.editMessage(event.scopeId, record.messageId, `${t(locale, 'auth_candidate_deleted', { value: candidate.name })}${restarted ? `\n${t(locale, 'auth_delete_current_restarted')}` : ''}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
5881
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, `${t(locale, 'auth_candidate_deleted', { value: candidate.name })}${restarted ? `\n${t(locale, 'auth_delete_current_restarted')}` : ''}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
5847
5882
  }
5848
5883
  }
5849
5884
  async handleAuthToggleCallback(event, localId, index, locale) {
@@ -5868,7 +5903,7 @@ export class BridgeSessionCore {
5868
5903
  clampCodexAuthListOffset(record);
5869
5904
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, disabled ? 'auth_candidate_disabled_short' : 'auth_candidate_enabled_short'));
5870
5905
  if (record.messageId !== null) {
5871
- await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
5906
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
5872
5907
  }
5873
5908
  }
5874
5909
  async handleAuthSwitchCallback(event, localId, index, locale) {
@@ -5898,7 +5933,7 @@ export class BridgeSessionCore {
5898
5933
  const switchLabels = await this.readCodexAuthSwitchLabels(candidate);
5899
5934
  const switchingMessage = t(locale, 'auth_switching', this.codexAuthSwitchParams(locale, switchLabels.fromLabel, switchLabels.toLabel));
5900
5935
  if (record.messageId !== null) {
5901
- await this.editMessage(event.scopeId, record.messageId, switchingMessage, []);
5936
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, switchingMessage, []);
5902
5937
  }
5903
5938
  const outcome = await this.switchCodexAuthAndRestart(event.scopeId, locale, candidate, false, false);
5904
5939
  const state = await this.listCodexAuthState();
@@ -5906,7 +5941,7 @@ export class BridgeSessionCore {
5906
5941
  record.createdAt = Date.now();
5907
5942
  clampCodexAuthListOffset(record);
5908
5943
  if (record.messageId !== null) {
5909
- await this.editMessage(event.scopeId, record.messageId, [
5944
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, [
5910
5945
  ...this.formatAuthSwitchValidationLines(locale, outcome),
5911
5946
  renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record),
5912
5947
  ].filter(Boolean).join('\n\n'), authChoiceKeyboard(locale, record));
@@ -6839,6 +6874,7 @@ export class BridgeSessionCore {
6839
6874
  ? buildThreadsKeyboard(locale, threadLikes, binding.threadId)
6840
6875
  : buildThreadListKeyboard(locale, threadLikes, listState, binding.threadId);
6841
6876
  await this.editHtmlMessage(scopeId, event.messageId, text, keyboard);
6877
+ this.scheduleStalePanelDeletion(scopeId, event.messageId);
6842
6878
  }
6843
6879
  let callbackText = t(locale, 'thread_opened');
6844
6880
  if (this.config.codexAppSyncOnOpen) {
@@ -7289,9 +7325,11 @@ export class BridgeSessionCore {
7289
7325
  : buildThreadListKeyboard(locale, forDisplay, presentationState, binding?.threadId ?? null);
7290
7326
  if (messageId !== undefined) {
7291
7327
  await this.editHtmlMessage(scopeId, messageId, text, keyboard);
7328
+ this.scheduleStalePanelDeletion(scopeId, messageId);
7292
7329
  return;
7293
7330
  }
7294
- await this.sendHtmlMessage(scopeId, text, keyboard);
7331
+ const sentMessageId = await this.sendHtmlMessage(scopeId, text, keyboard);
7332
+ this.scheduleStalePanelDeletion(scopeId, sentMessageId);
7295
7333
  }
7296
7334
  async showModelSettingsPanel(scopeId, messageId, locale = this.localeForChat(scopeId)) {
7297
7335
  const models = await this.app.listModels();
@@ -7321,9 +7359,11 @@ export class BridgeSessionCore {
7321
7359
  const keyboard = buildSetupPanelKeyboard(locale, { focus, models, settings, access });
7322
7360
  if (messageId !== undefined) {
7323
7361
  await this.editHtmlMessage(scopeId, messageId, text, keyboard);
7362
+ this.scheduleStalePanelDeletion(scopeId, messageId);
7324
7363
  return;
7325
7364
  }
7326
- await this.sendHtmlMessage(scopeId, text, keyboard);
7365
+ const sentMessageId = await this.sendHtmlMessage(scopeId, text, keyboard);
7366
+ this.scheduleStalePanelDeletion(scopeId, sentMessageId);
7327
7367
  }
7328
7368
  async showAccessSettingsPanel(scopeId, messageId, locale = this.localeForChat(scopeId)) {
7329
7369
  const access = this.resolveEffectiveAccess(scopeId);
@@ -9004,12 +9044,14 @@ function parseRichAuthCandidateRow(line) {
9004
9044
  if (!name) {
9005
9045
  return null;
9006
9046
  }
9007
- const quotas = parts.map(formatRichAuthQuotaCell);
9047
+ const quotas = parts.map(parseRichAuthQuotaCell);
9008
9048
  const statusParts = splitRichAuthStatus(status);
9009
9049
  return {
9010
9050
  index: match[1],
9011
- quotaA: quotas[0] ?? '-',
9012
- quotaB: quotas[1] ?? '-',
9051
+ quotaA: quotas[0]?.value ?? '-',
9052
+ quotaAReset: quotas[0]?.reset ?? '-',
9053
+ quotaB: quotas[1]?.value ?? '-',
9054
+ quotaBReset: quotas[1]?.reset ?? '-',
9013
9055
  name,
9014
9056
  current,
9015
9057
  enabled: statusParts.health === 'disabled' || statusParts.health === '已禁用'
@@ -9021,12 +9063,14 @@ function parseRichAuthCandidateRow(line) {
9021
9063
  function formatRichAuthCandidateTable(rows) {
9022
9064
  return [
9023
9065
  '<table bordered striped>',
9024
- '<tr><th>#</th><th>Quota A</th><th>Quota B</th><th>Auth</th><th>Current</th><th>Plan</th><th>Health</th><th>Last refresh</th><th>Expiry</th><th>Risk</th><th>Command</th></tr>',
9066
+ '<tr><th>#</th><th>Quota A</th><th>A reset</th><th>Quota B</th><th>B reset</th><th>Auth</th><th>Current</th><th>Plan</th><th>Health</th><th>Last refresh</th><th>Expiry</th><th>Risk</th><th>Command</th></tr>',
9025
9067
  ...rows.map(row => [
9026
9068
  '<tr>',
9027
9069
  `<td>${escapeTelegramHtml(row.index)}</td>`,
9028
9070
  `<td>${escapeTelegramHtml(row.quotaA)}</td>`,
9071
+ `<td>${escapeTelegramHtml(row.quotaAReset)}</td>`,
9029
9072
  `<td>${escapeTelegramHtml(row.quotaB)}</td>`,
9073
+ `<td>${escapeTelegramHtml(row.quotaBReset)}</td>`,
9030
9074
  `<td>${formatRichAuthCommandLink(`/auth use ${row.index}`, row.name)}</td>`,
9031
9075
  `<td>${row.current ? 'yes' : '-'}</td>`,
9032
9076
  `<td>${escapeTelegramHtml(row.plan)}</td>`,
@@ -9077,16 +9121,17 @@ function formatRichAuthRisk(health) {
9077
9121
  }
9078
9122
  return '-';
9079
9123
  }
9080
- function formatRichAuthQuotaCell(value) {
9124
+ function parseRichAuthQuotaCell(value) {
9081
9125
  const trimmed = value.trim();
9082
9126
  if (!trimmed || trimmed === '--' || trimmed === '—') {
9083
- return '-';
9127
+ return { value: '-', reset: '-' };
9084
9128
  }
9085
- const [windowLabel, percent] = trimmed.split(':');
9129
+ const [quotaPart, reset = '-'] = trimmed.split('@', 2);
9130
+ const [windowLabel, percent] = quotaPart.split(':');
9086
9131
  if (!windowLabel || percent === undefined) {
9087
- return trimmed;
9132
+ return { value: quotaPart, reset };
9088
9133
  }
9089
- return `${percent}%`;
9134
+ return { value: `${percent}%`, reset };
9090
9135
  }
9091
9136
  function formatRichAuthCommandCell(row) {
9092
9137
  const commands = [formatRichAuthCommandLink(`/auth use ${row.index}`, 'use')];
@@ -11135,8 +11180,10 @@ function authQuotaSnapshotFromRateLimit(snapshot, accountId = null, quotaIdentit
11135
11180
  planType: snapshot.planType,
11136
11181
  primaryWindowDurationMins: snapshot.primary?.windowDurationMins ?? null,
11137
11182
  primaryRemainingPercent: snapshot.primary ? remainingUsagePercent(snapshot.primary.usedPercent) : null,
11183
+ primaryResetsAt: snapshot.primary?.resetsAt ?? null,
11138
11184
  secondaryWindowDurationMins: snapshot.secondary?.windowDurationMins ?? null,
11139
11185
  secondaryRemainingPercent: snapshot.secondary ? remainingUsagePercent(snapshot.secondary.usedPercent) : null,
11186
+ secondaryResetsAt: snapshot.secondary?.resetsAt ?? null,
11140
11187
  };
11141
11188
  }
11142
11189
  function codexAuthQuotaSnapshotFromRecord(record) {
@@ -11147,8 +11194,10 @@ function codexAuthQuotaSnapshotFromRecord(record) {
11147
11194
  planType: record.planType,
11148
11195
  primaryWindowDurationMins: record.primaryWindowDurationMins,
11149
11196
  primaryRemainingPercent: record.primaryRemainingPercent,
11197
+ primaryResetsAt: record.primaryResetsAt,
11150
11198
  secondaryWindowDurationMins: record.secondaryWindowDurationMins,
11151
11199
  secondaryRemainingPercent: record.secondaryRemainingPercent,
11200
+ secondaryResetsAt: record.secondaryResetsAt,
11152
11201
  };
11153
11202
  }
11154
11203
  function mergeCodexAuthQuotaSnapshots(current, incoming) {
@@ -11171,8 +11220,10 @@ function isFiniteCodexAuthQuotaSnapshotRecord(record) {
11171
11220
  && isNullableString(record.planType)
11172
11221
  && isNullableFiniteNumber(record.primaryWindowDurationMins)
11173
11222
  && isNullableFiniteNumber(record.primaryRemainingPercent)
11223
+ && isNullableFiniteNumber(record.primaryResetsAt)
11174
11224
  && isNullableFiniteNumber(record.secondaryWindowDurationMins)
11175
- && isNullableFiniteNumber(record.secondaryRemainingPercent);
11225
+ && isNullableFiniteNumber(record.secondaryRemainingPercent)
11226
+ && isNullableFiniteNumber(record.secondaryResetsAt);
11176
11227
  }
11177
11228
  function remainingUsagePercent(usedPercent) {
11178
11229
  if (!Number.isFinite(usedPercent)) {
@@ -11185,12 +11236,12 @@ function formatAuthQuotaPrefix(locale, snapshot) {
11185
11236
  return '--';
11186
11237
  }
11187
11238
  const windows = [
11188
- [snapshot.primaryWindowDurationMins, snapshot.primaryRemainingPercent, 'primary'],
11189
- [snapshot.secondaryWindowDurationMins, snapshot.secondaryRemainingPercent, 'secondary'],
11239
+ [snapshot.primaryWindowDurationMins, snapshot.primaryRemainingPercent, snapshot.primaryResetsAt, 'primary'],
11240
+ [snapshot.secondaryWindowDurationMins, snapshot.secondaryRemainingPercent, snapshot.secondaryResetsAt, 'secondary'],
11190
11241
  ];
11191
11242
  const values = windows
11192
- .filter(([duration, remaining]) => duration !== null || remaining !== null)
11193
- .map(([duration, remaining, fallback]) => (`${formatCompactRateLimitWindowLabel(locale, duration, fallback)}:${remaining === null ? '--' : formatUsagePercent(remaining)}`));
11243
+ .filter(([duration, remaining, resetsAt]) => duration !== null || remaining !== null || resetsAt !== null)
11244
+ .map(([duration, remaining, resetsAt, fallback]) => (`${formatCompactRateLimitWindowLabel(locale, duration, fallback)}:${remaining === null ? '--' : formatUsagePercent(remaining)}${resetsAt === null ? '' : `@${formatLocalTimestamp(resetsAt)}`}`));
11194
11245
  return values.length > 0 ? values.join('|') : '--';
11195
11246
  }
11196
11247
  function formatAuthQuotaButtonPrefix(snapshot) {
@@ -11216,8 +11267,10 @@ function isCodexAuthQuotaSnapshot(value) {
11216
11267
  && (snapshot.planType === undefined || isNullableString(snapshot.planType))
11217
11268
  && (snapshot.primaryWindowDurationMins === undefined || isNullableFiniteNumber(snapshot.primaryWindowDurationMins))
11218
11269
  && isNullableFiniteNumber(snapshot.primaryRemainingPercent)
11270
+ && (snapshot.primaryResetsAt === undefined || isNullableFiniteNumber(snapshot.primaryResetsAt))
11219
11271
  && (snapshot.secondaryWindowDurationMins === undefined || isNullableFiniteNumber(snapshot.secondaryWindowDurationMins))
11220
- && isNullableFiniteNumber(snapshot.secondaryRemainingPercent);
11272
+ && isNullableFiniteNumber(snapshot.secondaryRemainingPercent)
11273
+ && (snapshot.secondaryResetsAt === undefined || isNullableFiniteNumber(snapshot.secondaryResetsAt));
11221
11274
  }
11222
11275
  function normalizeCodexAuthQuotaSnapshot(snapshot) {
11223
11276
  return {
@@ -11227,8 +11280,10 @@ function normalizeCodexAuthQuotaSnapshot(snapshot) {
11227
11280
  planType: snapshot.planType ?? null,
11228
11281
  primaryWindowDurationMins: snapshot.primaryWindowDurationMins ?? null,
11229
11282
  primaryRemainingPercent: snapshot.primaryRemainingPercent,
11283
+ primaryResetsAt: snapshot.primaryResetsAt ?? null,
11230
11284
  secondaryWindowDurationMins: snapshot.secondaryWindowDurationMins ?? null,
11231
11285
  secondaryRemainingPercent: snapshot.secondaryRemainingPercent,
11286
+ secondaryResetsAt: snapshot.secondaryResetsAt ?? null,
11232
11287
  };
11233
11288
  }
11234
11289
  function chatGptAuthMetadataCompatible(left, right) {
@@ -35,8 +35,10 @@ export interface CodexAuthQuotaSnapshotRecord {
35
35
  planType: string | null;
36
36
  primaryWindowDurationMins: number | null;
37
37
  primaryRemainingPercent: number | null;
38
+ primaryResetsAt: number | null;
38
39
  secondaryWindowDurationMins: number | null;
39
40
  secondaryRemainingPercent: number | null;
41
+ secondaryResetsAt: number | null;
40
42
  updatedAt: number;
41
43
  }
42
44
  export interface CodexAuthPoolStats {
@@ -136,7 +138,7 @@ export declare class BridgeStore {
136
138
  recordCodexAuthCandidateInvalidDelete(name: string, reason?: string | null): void;
137
139
  recordCodexAuthCandidateRemoved(name: string, reason?: string | null): void;
138
140
  getCodexAuthPoolStats(): CodexAuthPoolStats;
139
- setCodexAuthQuotaSnapshot(runtimeId: string, candidateName: string, accountId: string, quotaIdentityId: string, snapshot: Pick<CodexAuthQuotaSnapshotRecord, 'capturedAtMs' | 'planType' | 'primaryWindowDurationMins' | 'primaryRemainingPercent' | 'secondaryWindowDurationMins' | 'secondaryRemainingPercent'>): void;
141
+ setCodexAuthQuotaSnapshot(runtimeId: string, candidateName: string, accountId: string, quotaIdentityId: string, snapshot: Pick<CodexAuthQuotaSnapshotRecord, 'capturedAtMs' | 'planType' | 'primaryWindowDurationMins' | 'primaryRemainingPercent' | 'secondaryWindowDurationMins' | 'secondaryRemainingPercent'> & Partial<Pick<CodexAuthQuotaSnapshotRecord, 'primaryResetsAt' | 'secondaryResetsAt'>>): void;
140
142
  listCodexAuthQuotaSnapshots(quotaIdentityIds: string[]): CodexAuthQuotaSnapshotRecord[];
141
143
  private ensureColumn;
142
144
  }
@@ -191,8 +191,10 @@ export class BridgeStore {
191
191
  plan_type TEXT,
192
192
  primary_window_duration_mins REAL,
193
193
  primary_remaining_percent REAL,
194
+ primary_resets_at INTEGER,
194
195
  secondary_window_duration_mins REAL,
195
196
  secondary_remaining_percent REAL,
197
+ secondary_resets_at INTEGER,
196
198
  updated_at INTEGER NOT NULL,
197
199
  PRIMARY KEY (runtime_id, candidate_name)
198
200
  );
@@ -225,7 +227,9 @@ export class BridgeStore {
225
227
  this.ensureColumn('codex_auth_candidate_runtime', 'state', "TEXT NOT NULL DEFAULT 'active'");
226
228
  this.ensureColumn('codex_auth_quota_snapshots', 'plan_type', 'TEXT');
227
229
  this.ensureColumn('codex_auth_quota_snapshots', 'primary_window_duration_mins', 'REAL');
230
+ this.ensureColumn('codex_auth_quota_snapshots', 'primary_resets_at', 'INTEGER');
228
231
  this.ensureColumn('codex_auth_quota_snapshots', 'secondary_window_duration_mins', 'REAL');
232
+ this.ensureColumn('codex_auth_quota_snapshots', 'secondary_resets_at', 'INTEGER');
229
233
  this.ensureColumn('codex_auth_quota_snapshots', 'quota_identity_id', "TEXT NOT NULL DEFAULT ''");
230
234
  this.ensureColumn('codex_auth_pool_history', 'invalid_delete_count', 'INTEGER NOT NULL DEFAULT 0');
231
235
  this.db.prepare(`
@@ -997,11 +1001,13 @@ export class BridgeStore {
997
1001
  plan_type,
998
1002
  primary_window_duration_mins,
999
1003
  primary_remaining_percent,
1004
+ primary_resets_at,
1000
1005
  secondary_window_duration_mins,
1001
1006
  secondary_remaining_percent,
1007
+ secondary_resets_at,
1002
1008
  updated_at
1003
1009
  )
1004
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1010
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1005
1011
  ON CONFLICT(runtime_id, candidate_name) DO UPDATE SET
1006
1012
  account_id = excluded.account_id,
1007
1013
  quota_identity_id = excluded.quota_identity_id,
@@ -1009,10 +1015,12 @@ export class BridgeStore {
1009
1015
  plan_type = excluded.plan_type,
1010
1016
  primary_window_duration_mins = excluded.primary_window_duration_mins,
1011
1017
  primary_remaining_percent = excluded.primary_remaining_percent,
1018
+ primary_resets_at = excluded.primary_resets_at,
1012
1019
  secondary_window_duration_mins = excluded.secondary_window_duration_mins,
1013
1020
  secondary_remaining_percent = excluded.secondary_remaining_percent,
1021
+ secondary_resets_at = excluded.secondary_resets_at,
1014
1022
  updated_at = excluded.updated_at
1015
- `).run(runtimeId, candidateName, accountId, quotaIdentityId, snapshot.capturedAtMs, snapshot.planType, snapshot.primaryWindowDurationMins, snapshot.primaryRemainingPercent, snapshot.secondaryWindowDurationMins, snapshot.secondaryRemainingPercent, Date.now());
1023
+ `).run(runtimeId, candidateName, accountId, quotaIdentityId, snapshot.capturedAtMs, snapshot.planType, snapshot.primaryWindowDurationMins, snapshot.primaryRemainingPercent, snapshot.primaryResetsAt ?? null, snapshot.secondaryWindowDurationMins, snapshot.secondaryRemainingPercent, snapshot.secondaryResetsAt ?? null, Date.now());
1016
1024
  }
1017
1025
  listCodexAuthQuotaSnapshots(quotaIdentityIds) {
1018
1026
  const uniqueQuotaIdentityIds = [...new Set(quotaIdentityIds.filter(Boolean))];
@@ -1030,8 +1038,10 @@ export class BridgeStore {
1030
1038
  plan_type,
1031
1039
  primary_window_duration_mins,
1032
1040
  primary_remaining_percent,
1041
+ primary_resets_at,
1033
1042
  secondary_window_duration_mins,
1034
1043
  secondary_remaining_percent,
1044
+ secondary_resets_at,
1035
1045
  updated_at
1036
1046
  FROM codex_auth_quota_snapshots
1037
1047
  WHERE quota_identity_id IN (${placeholders})
@@ -1045,8 +1055,10 @@ export class BridgeStore {
1045
1055
  planType: nullableString(row.plan_type),
1046
1056
  primaryWindowDurationMins: nullableNumber(row.primary_window_duration_mins),
1047
1057
  primaryRemainingPercent: nullableNumber(row.primary_remaining_percent),
1058
+ primaryResetsAt: nullableNumber(row.primary_resets_at),
1048
1059
  secondaryWindowDurationMins: nullableNumber(row.secondary_window_duration_mins),
1049
1060
  secondaryRemainingPercent: nullableNumber(row.secondary_remaining_percent),
1061
+ secondaryResetsAt: nullableNumber(row.secondary_resets_at),
1050
1062
  updatedAt: Number(row.updated_at),
1051
1063
  }));
1052
1064
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.58",
3
+ "version": "0.5.60",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",