@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 +20 -0
- package/dist/controller/controller.d.ts +5 -0
- package/dist/controller/controller.js +114 -3
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
8476
|
-
|
|
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();
|