@foxden-app/foxclaw 0.5.57 → 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
 
@@ -81,7 +84,7 @@ CODEX_CLI_BIN=/absolute/path/to/codex
81
84
  # VOICE_SUMMARY_BUTTON_ENABLED=true
82
85
  # VOICE_SUMMARY_TEXT_LIMIT=180
83
86
  # VOICE_TEXT_LIMIT=2800
84
- # VOICE_TTS_TIMEOUT_MS=60000
87
+ # VOICE_TTS_TIMEOUT_MS=300000
85
88
 
86
89
  # Optional: standard HTTP(S) proxy for Telegram and ChatGPT/Codex backend requests.
87
90
  # Put these in the same env file that `foxclaw start` installs into systemd/launchd.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
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
+
25
+ ## 0.5.58 - 2026-06-21
26
+
27
+ ### 中文
28
+ - 修复重启恢复中的操作明细清理:FoxClaw 现在会把已折叠工具明细的 Telegram message id 持久化到 active turn preview,重启后最终答复完成时仍能按配置删除这些明细消息。
29
+ - 将 `VOICE_TTS_TIMEOUT_MS` 默认值和示例配置改为 300000ms,适配 SoulX 等较慢语音后端;本机配置也已同步为 5 分钟。
30
+
31
+ ### English
32
+ - Fixed operation-detail cleanup after restart recovery. FoxClaw now persists archived tool-detail Telegram message ids with active turn previews, so final-answer cleanup can still delete them after a service restart.
33
+ - Changed the default and example `VOICE_TTS_TIMEOUT_MS` to 300000ms for slower voice backends such as SoulX; the local deployment config has been updated to five minutes as well.
34
+
5
35
  ## 0.5.57 - 2026-06-21
6
36
 
7
37
  ### 中文
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,
@@ -110,7 +111,7 @@ export function loadConfig() {
110
111
  voiceSummaryButtonEnabled: boolEnv('VOICE_SUMMARY_BUTTON_ENABLED', true),
111
112
  voiceSummaryTextLimit: intEnv('VOICE_SUMMARY_TEXT_LIMIT', 180),
112
113
  voiceTextLimit: intEnv('VOICE_TEXT_LIMIT', 2800),
113
- voiceTtsTimeoutMs: intEnv('VOICE_TTS_TIMEOUT_MS', 60_000),
114
+ voiceTtsTimeoutMs: intEnv('VOICE_TTS_TIMEOUT_MS', 300_000),
114
115
  };
115
116
  ensureAppDirs(config);
116
117
  return config;
@@ -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
  }
@@ -2909,8 +2914,9 @@ export class BridgeSessionCore {
2909
2914
  const messageId = await this.sendMessage(record.scopeId, text, keyboard);
2910
2915
  this.store.updatePendingAttachmentBatchMessage(record.batchId, messageId);
2911
2916
  }
2912
- async registerActiveTurn(scopeId, chatId, chatType, topicId, threadId, turnId, previewMessageId, authRetry = null, collaborationMode = DEFAULT_COLLABORATION_MODE, queuedInputId = null) {
2917
+ async registerActiveTurn(scopeId, chatId, chatType, topicId, threadId, turnId, previewMessageId, authRetry = null, collaborationMode = DEFAULT_COLLABORATION_MODE, queuedInputId = null, archivedMessageIds = []) {
2913
2918
  const active = this.createActiveTurnState(scopeId, chatId, chatType, topicId, threadId, turnId, previewMessageId, false, collaborationMode, queuedInputId);
2919
+ active.archivedMessageIds = [...archivedMessageIds];
2914
2920
  active.authRetry = authRetry;
2915
2921
  this.setActiveTurn(scopeId, turnId, active);
2916
2922
  const pendingError = this.pendingTurnErrors.get(turnId);
@@ -2925,6 +2931,7 @@ export class BridgeSessionCore {
2925
2931
  threadId,
2926
2932
  messageId: previewMessageId,
2927
2933
  isObserved: active.isObserved,
2934
+ archivedMessageIds: active.archivedMessageIds,
2928
2935
  });
2929
2936
  }
2930
2937
  this.updateStatus();
@@ -3429,6 +3436,35 @@ export class BridgeSessionCore {
3429
3436
  await this.editHtmlMessage(scopeId, messageId, fallbackHtml, inlineKeyboard);
3430
3437
  }
3431
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
+ }
3432
3468
  async deleteMessage(scopeId, messageId) {
3433
3469
  await this.messaging.deleteMessage(scopeId, messageId);
3434
3470
  }
@@ -4857,6 +4893,7 @@ export class BridgeSessionCore {
4857
4893
  const authListMessage = renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null, record);
4858
4894
  const messageId = await this.sendRichInternalMessage(scopeId, '/auth', authListMessage, authChoiceKeyboard(locale, record));
4859
4895
  record.messageId = messageId;
4896
+ this.scheduleStalePanelDeletion(scopeId, messageId);
4860
4897
  }
4861
4898
  async handleAuthSyncCommand(scopeId, locale, args) {
4862
4899
  const action = args[0]?.toLowerCase() ?? 'status';
@@ -5662,12 +5699,12 @@ export class BridgeSessionCore {
5662
5699
  }
5663
5700
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_sync_safe_starting'));
5664
5701
  if (record.messageId !== null) {
5665
- 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'), []);
5666
5703
  }
5667
5704
  const result = await this.runAuthSafeSyncAll();
5668
5705
  if (!result) {
5669
5706
  if (record.messageId !== null) {
5670
- 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));
5671
5708
  }
5672
5709
  return;
5673
5710
  }
@@ -5677,7 +5714,7 @@ export class BridgeSessionCore {
5677
5714
  record.createdAt = Date.now();
5678
5715
  clampCodexAuthListOffset(record);
5679
5716
  if (record.messageId !== null) {
5680
- 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', {
5681
5718
  localSynced: result.localSynced,
5682
5719
  localSkipped: result.localSkipped,
5683
5720
  sent: result.sent,
@@ -5693,7 +5730,7 @@ export class BridgeSessionCore {
5693
5730
  }
5694
5731
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_refresh_all_confirm_short'));
5695
5732
  if (record.messageId !== null) {
5696
- 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));
5697
5734
  }
5698
5735
  return;
5699
5736
  }
@@ -5704,7 +5741,7 @@ export class BridgeSessionCore {
5704
5741
  record.candidates = state.candidates;
5705
5742
  record.createdAt = Date.now();
5706
5743
  if (record.messageId !== null) {
5707
- 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));
5708
5745
  }
5709
5746
  return;
5710
5747
  }
@@ -5715,12 +5752,12 @@ export class BridgeSessionCore {
5715
5752
  }
5716
5753
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_refresh_all_starting'));
5717
5754
  if (record.messageId !== null) {
5718
- 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'), []);
5719
5756
  }
5720
5757
  const lease = await this.coordinator?.acquireAuthRefreshLease?.('auth refresh all');
5721
5758
  if (lease && !lease.ok) {
5722
5759
  if (record.messageId !== null) {
5723
- 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));
5724
5761
  }
5725
5762
  return;
5726
5763
  }
@@ -5736,7 +5773,7 @@ export class BridgeSessionCore {
5736
5773
  record.candidates = state.candidates;
5737
5774
  record.createdAt = Date.now();
5738
5775
  if (record.messageId !== null) {
5739
- 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));
5740
5777
  }
5741
5778
  return;
5742
5779
  }
@@ -5773,7 +5810,7 @@ export class BridgeSessionCore {
5773
5810
  clampCodexAuthListOffset(record);
5774
5811
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
5775
5812
  if (record.messageId !== null) {
5776
- 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));
5777
5814
  }
5778
5815
  }
5779
5816
  async handleAuthRepairMenuCallback(event, localId, index, locale) {
@@ -5793,7 +5830,7 @@ export class BridgeSessionCore {
5793
5830
  }
5794
5831
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_repair_actions_short'));
5795
5832
  if (record.messageId !== null) {
5796
- 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));
5797
5834
  }
5798
5835
  }
5799
5836
  async handleAuthRepairActionCallback(event, localId, action, index, locale) {
@@ -5818,7 +5855,7 @@ export class BridgeSessionCore {
5818
5855
  record.createdAt = Date.now();
5819
5856
  clampCodexAuthListOffset(record);
5820
5857
  if (record.messageId !== null) {
5821
- 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));
5822
5859
  }
5823
5860
  return;
5824
5861
  }
@@ -5829,7 +5866,7 @@ export class BridgeSessionCore {
5829
5866
  if (action === 'login') {
5830
5867
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'login_device_started'));
5831
5868
  if (record.messageId !== null) {
5832
- 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 }), []);
5833
5870
  }
5834
5871
  await this.startAuthRepairLogin(event.scopeId, locale, candidate);
5835
5872
  return;
@@ -5841,7 +5878,7 @@ export class BridgeSessionCore {
5841
5878
  record.createdAt = Date.now();
5842
5879
  clampCodexAuthListOffset(record);
5843
5880
  if (record.messageId !== null) {
5844
- 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));
5845
5882
  }
5846
5883
  }
5847
5884
  async handleAuthToggleCallback(event, localId, index, locale) {
@@ -5866,7 +5903,7 @@ export class BridgeSessionCore {
5866
5903
  clampCodexAuthListOffset(record);
5867
5904
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, disabled ? 'auth_candidate_disabled_short' : 'auth_candidate_enabled_short'));
5868
5905
  if (record.messageId !== null) {
5869
- 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));
5870
5907
  }
5871
5908
  }
5872
5909
  async handleAuthSwitchCallback(event, localId, index, locale) {
@@ -5896,7 +5933,7 @@ export class BridgeSessionCore {
5896
5933
  const switchLabels = await this.readCodexAuthSwitchLabels(candidate);
5897
5934
  const switchingMessage = t(locale, 'auth_switching', this.codexAuthSwitchParams(locale, switchLabels.fromLabel, switchLabels.toLabel));
5898
5935
  if (record.messageId !== null) {
5899
- await this.editMessage(event.scopeId, record.messageId, switchingMessage, []);
5936
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, switchingMessage, []);
5900
5937
  }
5901
5938
  const outcome = await this.switchCodexAuthAndRestart(event.scopeId, locale, candidate, false, false);
5902
5939
  const state = await this.listCodexAuthState();
@@ -5904,7 +5941,7 @@ export class BridgeSessionCore {
5904
5941
  record.createdAt = Date.now();
5905
5942
  clampCodexAuthListOffset(record);
5906
5943
  if (record.messageId !== null) {
5907
- await this.editMessage(event.scopeId, record.messageId, [
5944
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, [
5908
5945
  ...this.formatAuthSwitchValidationLines(locale, outcome),
5909
5946
  renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record),
5910
5947
  ].filter(Boolean).join('\n\n'), authChoiceKeyboard(locale, record));
@@ -6837,6 +6874,7 @@ export class BridgeSessionCore {
6837
6874
  ? buildThreadsKeyboard(locale, threadLikes, binding.threadId)
6838
6875
  : buildThreadListKeyboard(locale, threadLikes, listState, binding.threadId);
6839
6876
  await this.editHtmlMessage(scopeId, event.messageId, text, keyboard);
6877
+ this.scheduleStalePanelDeletion(scopeId, event.messageId);
6840
6878
  }
6841
6879
  let callbackText = t(locale, 'thread_opened');
6842
6880
  if (this.config.codexAppSyncOnOpen) {
@@ -7287,9 +7325,11 @@ export class BridgeSessionCore {
7287
7325
  : buildThreadListKeyboard(locale, forDisplay, presentationState, binding?.threadId ?? null);
7288
7326
  if (messageId !== undefined) {
7289
7327
  await this.editHtmlMessage(scopeId, messageId, text, keyboard);
7328
+ this.scheduleStalePanelDeletion(scopeId, messageId);
7290
7329
  return;
7291
7330
  }
7292
- await this.sendHtmlMessage(scopeId, text, keyboard);
7331
+ const sentMessageId = await this.sendHtmlMessage(scopeId, text, keyboard);
7332
+ this.scheduleStalePanelDeletion(scopeId, sentMessageId);
7293
7333
  }
7294
7334
  async showModelSettingsPanel(scopeId, messageId, locale = this.localeForChat(scopeId)) {
7295
7335
  const models = await this.app.listModels();
@@ -7319,9 +7359,11 @@ export class BridgeSessionCore {
7319
7359
  const keyboard = buildSetupPanelKeyboard(locale, { focus, models, settings, access });
7320
7360
  if (messageId !== undefined) {
7321
7361
  await this.editHtmlMessage(scopeId, messageId, text, keyboard);
7362
+ this.scheduleStalePanelDeletion(scopeId, messageId);
7322
7363
  return;
7323
7364
  }
7324
- await this.sendHtmlMessage(scopeId, text, keyboard);
7365
+ const sentMessageId = await this.sendHtmlMessage(scopeId, text, keyboard);
7366
+ this.scheduleStalePanelDeletion(scopeId, sentMessageId);
7325
7367
  }
7326
7368
  async showAccessSettingsPanel(scopeId, messageId, locale = this.localeForChat(scopeId)) {
7327
7369
  const access = this.resolveEffectiveAccess(scopeId);
@@ -7758,6 +7800,7 @@ export class BridgeSessionCore {
7758
7800
  async attachRecoveredTurnPreview(preview, target, snapshot, liveTurn) {
7759
7801
  await this.stopWatchingScopeThread(preview.scopeId, preview.threadId);
7760
7802
  const active = this.createActiveTurnState(preview.scopeId, target.chatId, target.chatType, target.topicId, preview.threadId, liveTurn.turnId, preview.messageId, preview.isObserved);
7803
+ active.archivedMessageIds = [...preview.archivedMessageIds];
7761
7804
  this.setActiveTurn(preview.scopeId, liveTurn.turnId, active);
7762
7805
  this.store.saveActiveTurnPreview({
7763
7806
  turnId: liveTurn.turnId,
@@ -7765,6 +7808,7 @@ export class BridgeSessionCore {
7765
7808
  threadId: preview.threadId,
7766
7809
  messageId: preview.messageId,
7767
7810
  isObserved: preview.isObserved,
7811
+ archivedMessageIds: active.archivedMessageIds,
7768
7812
  });
7769
7813
  const watcher = {
7770
7814
  scopeId: preview.scopeId,
@@ -7833,7 +7877,7 @@ export class BridgeSessionCore {
7833
7877
  topicId: target.topicId,
7834
7878
  collaborationMode: turnState.collaborationMode,
7835
7879
  failedAuthTargets: new Set(),
7836
- }, turnState.collaborationMode);
7880
+ }, turnState.collaborationMode, null, preview.archivedMessageIds);
7837
7881
  this.logger.info('telegram.preview_auto_resumed_after_restart', {
7838
7882
  scopeId: preview.scopeId,
7839
7883
  threadId: preview.threadId,
@@ -8042,6 +8086,7 @@ export class BridgeSessionCore {
8042
8086
  threadId: active.threadId,
8043
8087
  messageId,
8044
8088
  isObserved: active.isObserved,
8089
+ archivedMessageIds: active.archivedMessageIds,
8045
8090
  });
8046
8091
  }
8047
8092
  catch (error) {
@@ -8999,12 +9044,14 @@ function parseRichAuthCandidateRow(line) {
8999
9044
  if (!name) {
9000
9045
  return null;
9001
9046
  }
9002
- const quotas = parts.map(formatRichAuthQuotaCell);
9047
+ const quotas = parts.map(parseRichAuthQuotaCell);
9003
9048
  const statusParts = splitRichAuthStatus(status);
9004
9049
  return {
9005
9050
  index: match[1],
9006
- quotaA: quotas[0] ?? '-',
9007
- quotaB: quotas[1] ?? '-',
9051
+ quotaA: quotas[0]?.value ?? '-',
9052
+ quotaAReset: quotas[0]?.reset ?? '-',
9053
+ quotaB: quotas[1]?.value ?? '-',
9054
+ quotaBReset: quotas[1]?.reset ?? '-',
9008
9055
  name,
9009
9056
  current,
9010
9057
  enabled: statusParts.health === 'disabled' || statusParts.health === '已禁用'
@@ -9016,12 +9063,14 @@ function parseRichAuthCandidateRow(line) {
9016
9063
  function formatRichAuthCandidateTable(rows) {
9017
9064
  return [
9018
9065
  '<table bordered striped>',
9019
- '<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>',
9020
9067
  ...rows.map(row => [
9021
9068
  '<tr>',
9022
9069
  `<td>${escapeTelegramHtml(row.index)}</td>`,
9023
9070
  `<td>${escapeTelegramHtml(row.quotaA)}</td>`,
9071
+ `<td>${escapeTelegramHtml(row.quotaAReset)}</td>`,
9024
9072
  `<td>${escapeTelegramHtml(row.quotaB)}</td>`,
9073
+ `<td>${escapeTelegramHtml(row.quotaBReset)}</td>`,
9025
9074
  `<td>${formatRichAuthCommandLink(`/auth use ${row.index}`, row.name)}</td>`,
9026
9075
  `<td>${row.current ? 'yes' : '-'}</td>`,
9027
9076
  `<td>${escapeTelegramHtml(row.plan)}</td>`,
@@ -9072,16 +9121,17 @@ function formatRichAuthRisk(health) {
9072
9121
  }
9073
9122
  return '-';
9074
9123
  }
9075
- function formatRichAuthQuotaCell(value) {
9124
+ function parseRichAuthQuotaCell(value) {
9076
9125
  const trimmed = value.trim();
9077
9126
  if (!trimmed || trimmed === '--' || trimmed === '—') {
9078
- return '-';
9127
+ return { value: '-', reset: '-' };
9079
9128
  }
9080
- const [windowLabel, percent] = trimmed.split(':');
9129
+ const [quotaPart, reset = '-'] = trimmed.split('@', 2);
9130
+ const [windowLabel, percent] = quotaPart.split(':');
9081
9131
  if (!windowLabel || percent === undefined) {
9082
- return trimmed;
9132
+ return { value: quotaPart, reset };
9083
9133
  }
9084
- return `${percent}%`;
9134
+ return { value: `${percent}%`, reset };
9085
9135
  }
9086
9136
  function formatRichAuthCommandCell(row) {
9087
9137
  const commands = [formatRichAuthCommandLink(`/auth use ${row.index}`, 'use')];
@@ -11130,8 +11180,10 @@ function authQuotaSnapshotFromRateLimit(snapshot, accountId = null, quotaIdentit
11130
11180
  planType: snapshot.planType,
11131
11181
  primaryWindowDurationMins: snapshot.primary?.windowDurationMins ?? null,
11132
11182
  primaryRemainingPercent: snapshot.primary ? remainingUsagePercent(snapshot.primary.usedPercent) : null,
11183
+ primaryResetsAt: snapshot.primary?.resetsAt ?? null,
11133
11184
  secondaryWindowDurationMins: snapshot.secondary?.windowDurationMins ?? null,
11134
11185
  secondaryRemainingPercent: snapshot.secondary ? remainingUsagePercent(snapshot.secondary.usedPercent) : null,
11186
+ secondaryResetsAt: snapshot.secondary?.resetsAt ?? null,
11135
11187
  };
11136
11188
  }
11137
11189
  function codexAuthQuotaSnapshotFromRecord(record) {
@@ -11142,8 +11194,10 @@ function codexAuthQuotaSnapshotFromRecord(record) {
11142
11194
  planType: record.planType,
11143
11195
  primaryWindowDurationMins: record.primaryWindowDurationMins,
11144
11196
  primaryRemainingPercent: record.primaryRemainingPercent,
11197
+ primaryResetsAt: record.primaryResetsAt,
11145
11198
  secondaryWindowDurationMins: record.secondaryWindowDurationMins,
11146
11199
  secondaryRemainingPercent: record.secondaryRemainingPercent,
11200
+ secondaryResetsAt: record.secondaryResetsAt,
11147
11201
  };
11148
11202
  }
11149
11203
  function mergeCodexAuthQuotaSnapshots(current, incoming) {
@@ -11166,8 +11220,10 @@ function isFiniteCodexAuthQuotaSnapshotRecord(record) {
11166
11220
  && isNullableString(record.planType)
11167
11221
  && isNullableFiniteNumber(record.primaryWindowDurationMins)
11168
11222
  && isNullableFiniteNumber(record.primaryRemainingPercent)
11223
+ && isNullableFiniteNumber(record.primaryResetsAt)
11169
11224
  && isNullableFiniteNumber(record.secondaryWindowDurationMins)
11170
- && isNullableFiniteNumber(record.secondaryRemainingPercent);
11225
+ && isNullableFiniteNumber(record.secondaryRemainingPercent)
11226
+ && isNullableFiniteNumber(record.secondaryResetsAt);
11171
11227
  }
11172
11228
  function remainingUsagePercent(usedPercent) {
11173
11229
  if (!Number.isFinite(usedPercent)) {
@@ -11180,12 +11236,12 @@ function formatAuthQuotaPrefix(locale, snapshot) {
11180
11236
  return '--';
11181
11237
  }
11182
11238
  const windows = [
11183
- [snapshot.primaryWindowDurationMins, snapshot.primaryRemainingPercent, 'primary'],
11184
- [snapshot.secondaryWindowDurationMins, snapshot.secondaryRemainingPercent, 'secondary'],
11239
+ [snapshot.primaryWindowDurationMins, snapshot.primaryRemainingPercent, snapshot.primaryResetsAt, 'primary'],
11240
+ [snapshot.secondaryWindowDurationMins, snapshot.secondaryRemainingPercent, snapshot.secondaryResetsAt, 'secondary'],
11185
11241
  ];
11186
11242
  const values = windows
11187
- .filter(([duration, remaining]) => duration !== null || remaining !== null)
11188
- .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)}`}`));
11189
11245
  return values.length > 0 ? values.join('|') : '--';
11190
11246
  }
11191
11247
  function formatAuthQuotaButtonPrefix(snapshot) {
@@ -11211,8 +11267,10 @@ function isCodexAuthQuotaSnapshot(value) {
11211
11267
  && (snapshot.planType === undefined || isNullableString(snapshot.planType))
11212
11268
  && (snapshot.primaryWindowDurationMins === undefined || isNullableFiniteNumber(snapshot.primaryWindowDurationMins))
11213
11269
  && isNullableFiniteNumber(snapshot.primaryRemainingPercent)
11270
+ && (snapshot.primaryResetsAt === undefined || isNullableFiniteNumber(snapshot.primaryResetsAt))
11214
11271
  && (snapshot.secondaryWindowDurationMins === undefined || isNullableFiniteNumber(snapshot.secondaryWindowDurationMins))
11215
- && isNullableFiniteNumber(snapshot.secondaryRemainingPercent);
11272
+ && isNullableFiniteNumber(snapshot.secondaryRemainingPercent)
11273
+ && (snapshot.secondaryResetsAt === undefined || isNullableFiniteNumber(snapshot.secondaryResetsAt));
11216
11274
  }
11217
11275
  function normalizeCodexAuthQuotaSnapshot(snapshot) {
11218
11276
  return {
@@ -11222,8 +11280,10 @@ function normalizeCodexAuthQuotaSnapshot(snapshot) {
11222
11280
  planType: snapshot.planType ?? null,
11223
11281
  primaryWindowDurationMins: snapshot.primaryWindowDurationMins ?? null,
11224
11282
  primaryRemainingPercent: snapshot.primaryRemainingPercent,
11283
+ primaryResetsAt: snapshot.primaryResetsAt ?? null,
11225
11284
  secondaryWindowDurationMins: snapshot.secondaryWindowDurationMins ?? null,
11226
11285
  secondaryRemainingPercent: snapshot.secondaryRemainingPercent,
11286
+ secondaryResetsAt: snapshot.secondaryResetsAt ?? null,
11227
11287
  };
11228
11288
  }
11229
11289
  function chatGptAuthMetadataCompatible(left, right) {
@@ -5,6 +5,7 @@ export interface ActiveTurnPreviewRecord {
5
5
  threadId: string;
6
6
  messageId: number;
7
7
  isObserved: boolean;
8
+ archivedMessageIds: number[];
8
9
  createdAt: number;
9
10
  updatedAt: number;
10
11
  }
@@ -34,8 +35,10 @@ export interface CodexAuthQuotaSnapshotRecord {
34
35
  planType: string | null;
35
36
  primaryWindowDurationMins: number | null;
36
37
  primaryRemainingPercent: number | null;
38
+ primaryResetsAt: number | null;
37
39
  secondaryWindowDurationMins: number | null;
38
40
  secondaryRemainingPercent: number | null;
41
+ secondaryResetsAt: number | null;
39
42
  updatedAt: number;
40
43
  }
41
44
  export interface CodexAuthPoolStats {
@@ -82,6 +85,7 @@ export declare class BridgeStore {
82
85
  countPendingApprovals(): number;
83
86
  saveActiveTurnPreview(record: Pick<ActiveTurnPreviewRecord, 'turnId' | 'scopeId' | 'threadId' | 'messageId'> & {
84
87
  isObserved?: boolean;
88
+ archivedMessageIds?: number[];
85
89
  }): void;
86
90
  listActiveTurnPreviews(): ActiveTurnPreviewRecord[];
87
91
  removeActiveTurnPreview(turnId: string): void;
@@ -134,7 +138,7 @@ export declare class BridgeStore {
134
138
  recordCodexAuthCandidateInvalidDelete(name: string, reason?: string | null): void;
135
139
  recordCodexAuthCandidateRemoved(name: string, reason?: string | null): void;
136
140
  getCodexAuthPoolStats(): CodexAuthPoolStats;
137
- 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;
138
142
  listCodexAuthQuotaSnapshots(quotaIdentityIds: string[]): CodexAuthQuotaSnapshotRecord[];
139
143
  private ensureColumn;
140
144
  }
@@ -71,6 +71,7 @@ export class BridgeStore {
71
71
  thread_id TEXT NOT NULL,
72
72
  message_id INTEGER NOT NULL,
73
73
  is_observed INTEGER NOT NULL DEFAULT 0,
74
+ archived_message_ids TEXT NOT NULL DEFAULT '[]',
74
75
  created_at INTEGER NOT NULL,
75
76
  updated_at INTEGER NOT NULL
76
77
  );
@@ -190,8 +191,10 @@ export class BridgeStore {
190
191
  plan_type TEXT,
191
192
  primary_window_duration_mins REAL,
192
193
  primary_remaining_percent REAL,
194
+ primary_resets_at INTEGER,
193
195
  secondary_window_duration_mins REAL,
194
196
  secondary_remaining_percent REAL,
197
+ secondary_resets_at INTEGER,
195
198
  updated_at INTEGER NOT NULL,
196
199
  PRIMARY KEY (runtime_id, candidate_name)
197
200
  );
@@ -219,11 +222,14 @@ export class BridgeStore {
219
222
  this.ensureColumn('pending_user_inputs', 'status', "TEXT NOT NULL DEFAULT 'pending'");
220
223
  this.ensureColumn('pending_user_inputs', 'submitted_at', 'INTEGER');
221
224
  this.ensureColumn('active_turn_previews', 'is_observed', 'INTEGER NOT NULL DEFAULT 0');
225
+ this.ensureColumn('active_turn_previews', 'archived_message_ids', "TEXT NOT NULL DEFAULT '[]'");
222
226
  this.ensureColumn('codex_auth_candidates', 'state', "TEXT NOT NULL DEFAULT 'active'");
223
227
  this.ensureColumn('codex_auth_candidate_runtime', 'state', "TEXT NOT NULL DEFAULT 'active'");
224
228
  this.ensureColumn('codex_auth_quota_snapshots', 'plan_type', 'TEXT');
225
229
  this.ensureColumn('codex_auth_quota_snapshots', 'primary_window_duration_mins', 'REAL');
230
+ this.ensureColumn('codex_auth_quota_snapshots', 'primary_resets_at', 'INTEGER');
226
231
  this.ensureColumn('codex_auth_quota_snapshots', 'secondary_window_duration_mins', 'REAL');
232
+ this.ensureColumn('codex_auth_quota_snapshots', 'secondary_resets_at', 'INTEGER');
227
233
  this.ensureColumn('codex_auth_quota_snapshots', 'quota_identity_id', "TEXT NOT NULL DEFAULT ''");
228
234
  this.ensureColumn('codex_auth_pool_history', 'invalid_delete_count', 'INTEGER NOT NULL DEFAULT 0');
229
235
  this.db.prepare(`
@@ -426,13 +432,13 @@ export class BridgeStore {
426
432
  const now = Date.now();
427
433
  this.db.prepare('DELETE FROM active_turn_previews WHERE turn_id = ? OR scope_id = ?').run(record.turnId, record.scopeId);
428
434
  this.db.prepare(`
429
- INSERT INTO active_turn_previews (turn_id, scope_id, thread_id, message_id, is_observed, created_at, updated_at)
430
- VALUES (?, ?, ?, ?, ?, ?, ?)
431
- `).run(record.turnId, record.scopeId, record.threadId, record.messageId, record.isObserved ? 1 : 0, now, now);
435
+ INSERT INTO active_turn_previews (turn_id, scope_id, thread_id, message_id, is_observed, archived_message_ids, created_at, updated_at)
436
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
437
+ `).run(record.turnId, record.scopeId, record.threadId, record.messageId, record.isObserved ? 1 : 0, JSON.stringify(normalizeMessageIds(record.archivedMessageIds ?? [])), now, now);
432
438
  }
433
439
  listActiveTurnPreviews() {
434
440
  const rows = this.db.prepare(`
435
- SELECT turn_id, scope_id, thread_id, message_id, is_observed, created_at, updated_at
441
+ SELECT turn_id, scope_id, thread_id, message_id, is_observed, archived_message_ids, created_at, updated_at
436
442
  FROM active_turn_previews
437
443
  ORDER BY created_at ASC
438
444
  `).all();
@@ -442,6 +448,7 @@ export class BridgeStore {
442
448
  threadId: String(row.thread_id),
443
449
  messageId: Number(row.message_id),
444
450
  isObserved: Boolean(row.is_observed),
451
+ archivedMessageIds: parseMessageIdArray(row.archived_message_ids),
445
452
  createdAt: Number(row.created_at),
446
453
  updatedAt: Number(row.updated_at),
447
454
  }));
@@ -994,11 +1001,13 @@ export class BridgeStore {
994
1001
  plan_type,
995
1002
  primary_window_duration_mins,
996
1003
  primary_remaining_percent,
1004
+ primary_resets_at,
997
1005
  secondary_window_duration_mins,
998
1006
  secondary_remaining_percent,
1007
+ secondary_resets_at,
999
1008
  updated_at
1000
1009
  )
1001
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1010
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1002
1011
  ON CONFLICT(runtime_id, candidate_name) DO UPDATE SET
1003
1012
  account_id = excluded.account_id,
1004
1013
  quota_identity_id = excluded.quota_identity_id,
@@ -1006,10 +1015,12 @@ export class BridgeStore {
1006
1015
  plan_type = excluded.plan_type,
1007
1016
  primary_window_duration_mins = excluded.primary_window_duration_mins,
1008
1017
  primary_remaining_percent = excluded.primary_remaining_percent,
1018
+ primary_resets_at = excluded.primary_resets_at,
1009
1019
  secondary_window_duration_mins = excluded.secondary_window_duration_mins,
1010
1020
  secondary_remaining_percent = excluded.secondary_remaining_percent,
1021
+ secondary_resets_at = excluded.secondary_resets_at,
1011
1022
  updated_at = excluded.updated_at
1012
- `).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());
1013
1024
  }
1014
1025
  listCodexAuthQuotaSnapshots(quotaIdentityIds) {
1015
1026
  const uniqueQuotaIdentityIds = [...new Set(quotaIdentityIds.filter(Boolean))];
@@ -1027,8 +1038,10 @@ export class BridgeStore {
1027
1038
  plan_type,
1028
1039
  primary_window_duration_mins,
1029
1040
  primary_remaining_percent,
1041
+ primary_resets_at,
1030
1042
  secondary_window_duration_mins,
1031
1043
  secondary_remaining_percent,
1044
+ secondary_resets_at,
1032
1045
  updated_at
1033
1046
  FROM codex_auth_quota_snapshots
1034
1047
  WHERE quota_identity_id IN (${placeholders})
@@ -1042,8 +1055,10 @@ export class BridgeStore {
1042
1055
  planType: nullableString(row.plan_type),
1043
1056
  primaryWindowDurationMins: nullableNumber(row.primary_window_duration_mins),
1044
1057
  primaryRemainingPercent: nullableNumber(row.primary_remaining_percent),
1058
+ primaryResetsAt: nullableNumber(row.primary_resets_at),
1045
1059
  secondaryWindowDurationMins: nullableNumber(row.secondary_window_duration_mins),
1046
1060
  secondaryRemainingPercent: nullableNumber(row.secondary_remaining_percent),
1061
+ secondaryResetsAt: nullableNumber(row.secondary_resets_at),
1047
1062
  updatedAt: Number(row.updated_at),
1048
1063
  }));
1049
1064
  }
@@ -1065,6 +1080,23 @@ function nullableNumber(value) {
1065
1080
  function nullableString(value) {
1066
1081
  return typeof value === 'string' && value.trim() ? value : null;
1067
1082
  }
1083
+ function normalizeMessageIds(values) {
1084
+ return [...new Set(values.filter((value) => Number.isSafeInteger(value) && value > 0))];
1085
+ }
1086
+ function parseMessageIdArray(value) {
1087
+ if (typeof value !== 'string' || !value.trim()) {
1088
+ return [];
1089
+ }
1090
+ try {
1091
+ const parsed = JSON.parse(value);
1092
+ return Array.isArray(parsed)
1093
+ ? normalizeMessageIds(parsed.map((entry) => Number(entry)))
1094
+ : [];
1095
+ }
1096
+ catch {
1097
+ return [];
1098
+ }
1099
+ }
1068
1100
  function normalizeCodexAuthCandidateState(value) {
1069
1101
  return value === 'needs_repair' ? 'needs_repair' : 'active';
1070
1102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.57",
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",