@foxden-app/foxclaw 0.5.26 → 0.5.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -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.28 - 2026-06-09
6
+
7
+ ### 中文
8
+ - 重启续接恢复失败时不再立刻把 Telegram 状态卡改成“桥接重启、请手动继续”;FoxClaw 会保留 active preview 并在后台短间隔重试几分钟。
9
+ - 这修复了 Codex app-server 刚重启时线程短暂 `thread not found`,导致自动续跑错过窗口、没有最终总结消息的问题。
10
+
11
+ ### English
12
+ - Restart recovery no longer immediately retires the Telegram status card as "bridge restarted, continue manually" when the first recovery attempt fails. FoxClaw keeps the active preview and retries in the background for several minutes.
13
+ - This fixes cases where the Codex app-server briefly reports `thread not found` right after restart, causing auto-resume to miss its window and never send the final summary.
14
+
15
+ ## 0.5.27 - 2026-06-09
16
+
17
+ ### 中文
18
+ - 重启续接的完成判定进一步收窄:只有 final/final_answer 类 assistant 输出、plan 输出或明确错误才算已有完成结果。
19
+ - 如果旧 turn 只有 commentary 进度消息,例如“正在升级本机服务”,重启后仍会自动续跑,避免状态卡显示“已完成”但没有真正收尾回复。
20
+
21
+ ### English
22
+ - Tightened restart recovery completion detection: only final/final_answer assistant output, plan output, or an explicit error now counts as a completed result.
23
+ - If the previous turn only had commentary progress such as "upgrading the local service", FoxClaw still auto-resumes it after restart instead of retiring the status card as completed without a real final reply.
24
+
5
25
  ## 0.5.26 - 2026-06-09
6
26
 
7
27
  ### 中文
@@ -88,6 +88,7 @@ export declare class BridgeSessionCore {
88
88
  private locks;
89
89
  private approvalTimers;
90
90
  private submittedUserInputTimers;
91
+ private restartPreviewRecoveryTimers;
91
92
  private selfUpdatePollTimer;
92
93
  private proactiveAuthRefreshTimer;
93
94
  private proactiveAuthRefreshInProgress;
@@ -418,6 +419,10 @@ export declare class BridgeSessionCore {
418
419
  private syncTurnStatus;
419
420
  private syncTurnStream;
420
421
  private cleanupStaleTurnPreviews;
422
+ private scheduleRestartPreviewRecovery;
423
+ private retryRestartPreviewRecovery;
424
+ private retireStaleRestartPreview;
425
+ private clearRestartPreviewRecoveryTimers;
421
426
  private recoverLiveTurnPreview;
422
427
  private attachRecoveredTurnPreview;
423
428
  private resumeInterruptedTurnAfterRestart;
@@ -20,6 +20,7 @@ import { applySessionLog, bootstrapSessionLog, splitJsonlChunk, } from './sessio
20
20
  import { renderActiveTurnStatus } from './status.js';
21
21
  import { writeRuntimeStatus } from '../runtime.js';
22
22
  const AUTH_DELETE_REASON_NEEDS_REPAIR = 'needs_repair';
23
+ const RESTART_PREVIEW_RECOVERY_RETRY_DELAYS_MS = [3000, 7000, 15_000, 30_000, 45_000, 60_000, 60_000, 60_000];
23
24
  class UserFacingError extends Error {
24
25
  }
25
26
  const OBSERVED_THREAD_POLL_MS = 1500;
@@ -136,6 +137,7 @@ export class BridgeSessionCore {
136
137
  locks = new Map();
137
138
  approvalTimers = new Map();
138
139
  submittedUserInputTimers = new Map();
140
+ restartPreviewRecoveryTimers = new Map();
139
141
  selfUpdatePollTimer = null;
140
142
  proactiveAuthRefreshTimer = null;
141
143
  proactiveAuthRefreshInProgress = false;
@@ -261,6 +263,7 @@ export class BridgeSessionCore {
261
263
  clearTimeout(timer);
262
264
  }
263
265
  this.submittedUserInputTimers.clear();
266
+ this.clearRestartPreviewRecoveryTimers();
264
267
  this.clearSelfUpdateStatusPoll();
265
268
  this.clearProactiveAuthRefreshTimer();
266
269
  await this.app.stop({ terminateServer: false });
@@ -7210,9 +7213,105 @@ export class BridgeSessionCore {
7210
7213
  error: toErrorMeta(error),
7211
7214
  });
7212
7215
  }
7213
- await this.retirePreviewMessage(preview.scopeId, preview.messageId, t(this.localeForChat(preview.scopeId), 'stale_preview_restarted', { threadId: preview.threadId }), preview.turnId);
7216
+ this.scheduleRestartPreviewRecovery(preview, 0);
7214
7217
  }
7215
7218
  }
7219
+ scheduleRestartPreviewRecovery(preview, attempt) {
7220
+ const key = restartPreviewRecoveryKey(preview);
7221
+ if (this.restartPreviewRecoveryTimers.has(key)) {
7222
+ return;
7223
+ }
7224
+ const delayMs = RESTART_PREVIEW_RECOVERY_RETRY_DELAYS_MS[attempt];
7225
+ if (delayMs === undefined) {
7226
+ void this.retireStaleRestartPreview(preview).catch((error) => {
7227
+ this.logger.warn('telegram.preview_stale_retire_failed', {
7228
+ scopeId: preview.scopeId,
7229
+ threadId: preview.threadId,
7230
+ turnId: preview.turnId,
7231
+ error: toErrorMeta(error),
7232
+ });
7233
+ });
7234
+ return;
7235
+ }
7236
+ const timer = setTimeout(() => {
7237
+ this.restartPreviewRecoveryTimers.delete(key);
7238
+ void this.retryRestartPreviewRecovery(preview, attempt + 1).catch((error) => {
7239
+ this.logger.warn('telegram.preview_recovery_retry_failed', {
7240
+ scopeId: preview.scopeId,
7241
+ threadId: preview.threadId,
7242
+ turnId: preview.turnId,
7243
+ attempt: attempt + 1,
7244
+ error: toErrorMeta(error),
7245
+ });
7246
+ this.scheduleRestartPreviewRecovery(preview, attempt + 1);
7247
+ });
7248
+ }, delayMs);
7249
+ timer.unref?.();
7250
+ this.restartPreviewRecoveryTimers.set(key, timer);
7251
+ this.logger.info('telegram.preview_recovery_deferred', {
7252
+ scopeId: preview.scopeId,
7253
+ threadId: preview.threadId,
7254
+ turnId: preview.turnId,
7255
+ attempt: attempt + 1,
7256
+ delayMs,
7257
+ });
7258
+ }
7259
+ async retryRestartPreviewRecovery(preview, attempt) {
7260
+ const current = this.store.listActiveTurnPreviews().find((record) => (record.scopeId === preview.scopeId
7261
+ && record.turnId === preview.turnId
7262
+ && record.messageId === preview.messageId));
7263
+ if (!current) {
7264
+ return;
7265
+ }
7266
+ if (!this.ownsScope(current.scopeId)) {
7267
+ return;
7268
+ }
7269
+ if (!this.messaging.canSendToScope(current.scopeId)) {
7270
+ this.store.removeActiveTurnPreview(current.turnId);
7271
+ this.logger.info('telegram.preview_dropped_disabled_channel', {
7272
+ scopeId: current.scopeId,
7273
+ threadId: current.threadId,
7274
+ turnId: current.turnId,
7275
+ });
7276
+ return;
7277
+ }
7278
+ try {
7279
+ if (await this.recoverLiveTurnPreview(current)) {
7280
+ this.logger.info('telegram.preview_recovery_retry_succeeded', {
7281
+ scopeId: current.scopeId,
7282
+ threadId: current.threadId,
7283
+ turnId: current.turnId,
7284
+ attempt,
7285
+ });
7286
+ return;
7287
+ }
7288
+ }
7289
+ catch (error) {
7290
+ this.logger.warn('telegram.preview_recovery_retry_failed', {
7291
+ scopeId: current.scopeId,
7292
+ threadId: current.threadId,
7293
+ turnId: current.turnId,
7294
+ attempt,
7295
+ error: toErrorMeta(error),
7296
+ });
7297
+ }
7298
+ this.scheduleRestartPreviewRecovery(current, attempt);
7299
+ }
7300
+ async retireStaleRestartPreview(preview) {
7301
+ const current = this.store.listActiveTurnPreviews().find((record) => (record.scopeId === preview.scopeId
7302
+ && record.turnId === preview.turnId
7303
+ && record.messageId === preview.messageId));
7304
+ if (!current) {
7305
+ return;
7306
+ }
7307
+ await this.retirePreviewMessage(current.scopeId, current.messageId, t(this.localeForChat(current.scopeId), 'stale_preview_restarted', { threadId: current.threadId }), current.turnId);
7308
+ }
7309
+ clearRestartPreviewRecoveryTimers() {
7310
+ for (const timer of this.restartPreviewRecoveryTimers.values()) {
7311
+ clearTimeout(timer);
7312
+ }
7313
+ this.restartPreviewRecoveryTimers.clear();
7314
+ }
7216
7315
  async recoverLiveTurnPreview(preview) {
7217
7316
  if (this.getActiveTurn(preview.scopeId, preview.turnId)) {
7218
7317
  return true;
@@ -8472,8 +8571,17 @@ function turnHasRelayableOutcome(turn) {
8472
8571
  }
8473
8572
  return turn.items.some((item) => {
8474
8573
  const type = item.type.toLowerCase();
8475
- return ((type === 'agentmessage' || type === 'assistantmessage' || type === 'plan')
8476
- && Boolean(item.text?.trim()));
8574
+ if (!item.text?.trim()) {
8575
+ return false;
8576
+ }
8577
+ if (type === 'plan') {
8578
+ return true;
8579
+ }
8580
+ if (type !== 'agentmessage' && type !== 'assistantmessage') {
8581
+ return false;
8582
+ }
8583
+ const phase = item.phase?.toLowerCase() ?? null;
8584
+ return phase === null || phase.startsWith('final');
8477
8585
  });
8478
8586
  }
8479
8587
  function mapGoalNotification(raw) {
@@ -8651,6 +8759,9 @@ function resolveScopeMessageTarget(scopeId) {
8651
8759
  return null;
8652
8760
  }
8653
8761
  }
8762
+ function restartPreviewRecoveryKey(preview) {
8763
+ return `${preview.scopeId}:${preview.turnId}:${preview.messageId}`;
8764
+ }
8654
8765
  function seedObservedTurnCursor(turn) {
8655
8766
  const agentItems = turn.items.filter((item) => {
8656
8767
  const type = item.type.toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.26",
3
+ "version": "0.5.28",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",