@foxden-app/foxclaw 0.5.27 → 0.5.29
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 +108 -4
- package/dist/i18n.d.ts +2 -2
- package/dist/i18n.js +2 -2
- 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.29 - 2026-06-09
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- 重启自动续接遇到旧 Codex thread 永久 `thread not found` 时,会静默创建 replacement thread 并复用原 Telegram 状态卡继续跑,不再只反复等待旧 thread 恢复。
|
|
9
|
+
- 自动续接提示补充了无上下文兜底指令:如果旧线程上下文不可用,会检查当前工作目录、git 状态、服务日志和运行状态后完成收尾总结。
|
|
10
|
+
|
|
11
|
+
### English
|
|
12
|
+
- Restart auto-resume now silently creates a replacement thread when the old Codex thread remains `thread not found`, reusing the original Telegram status card instead of only waiting for the old thread to recover.
|
|
13
|
+
- The auto-resume prompt now includes a no-context fallback: inspect the current working directory, git status, service logs, and runtime state before finishing the user-facing summary.
|
|
14
|
+
|
|
15
|
+
## 0.5.28 - 2026-06-09
|
|
16
|
+
|
|
17
|
+
### 中文
|
|
18
|
+
- 重启续接恢复失败时不再立刻把 Telegram 状态卡改成“桥接重启、请手动继续”;FoxClaw 会保留 active preview 并在后台短间隔重试几分钟。
|
|
19
|
+
- 这修复了 Codex app-server 刚重启时线程短暂 `thread not found`,导致自动续跑错过窗口、没有最终总结消息的问题。
|
|
20
|
+
|
|
21
|
+
### English
|
|
22
|
+
- 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.
|
|
23
|
+
- 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.
|
|
24
|
+
|
|
5
25
|
## 0.5.27 - 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 });
|
|
@@ -2562,7 +2565,9 @@ export class BridgeSessionCore {
|
|
|
2562
2565
|
}
|
|
2563
2566
|
this.logger.warn('codex.turn_thread_not_found', { scopeId, threadId: binding.threadId });
|
|
2564
2567
|
const replacement = await this.createBinding(scopeId, binding.cwd ?? this.config.defaultCwd);
|
|
2565
|
-
|
|
2568
|
+
if (overrides.notifyMissingThread !== false) {
|
|
2569
|
+
await this.sendMessage(scopeId, t(this.localeForChat(scopeId), 'current_thread_unavailable_continued', { threadId: replacement.threadId }));
|
|
2570
|
+
}
|
|
2566
2571
|
const nextSettings = this.store.getChatSettings(scopeId);
|
|
2567
2572
|
const nextAccess = this.resolveEffectiveAccess(scopeId, nextSettings);
|
|
2568
2573
|
const replacementCwd = replacement.cwd ?? this.config.defaultCwd;
|
|
@@ -7210,8 +7215,104 @@ export class BridgeSessionCore {
|
|
|
7210
7215
|
error: toErrorMeta(error),
|
|
7211
7216
|
});
|
|
7212
7217
|
}
|
|
7213
|
-
|
|
7218
|
+
this.scheduleRestartPreviewRecovery(preview, 0);
|
|
7219
|
+
}
|
|
7220
|
+
}
|
|
7221
|
+
scheduleRestartPreviewRecovery(preview, attempt) {
|
|
7222
|
+
const key = restartPreviewRecoveryKey(preview);
|
|
7223
|
+
if (this.restartPreviewRecoveryTimers.has(key)) {
|
|
7224
|
+
return;
|
|
7225
|
+
}
|
|
7226
|
+
const delayMs = RESTART_PREVIEW_RECOVERY_RETRY_DELAYS_MS[attempt];
|
|
7227
|
+
if (delayMs === undefined) {
|
|
7228
|
+
void this.retireStaleRestartPreview(preview).catch((error) => {
|
|
7229
|
+
this.logger.warn('telegram.preview_stale_retire_failed', {
|
|
7230
|
+
scopeId: preview.scopeId,
|
|
7231
|
+
threadId: preview.threadId,
|
|
7232
|
+
turnId: preview.turnId,
|
|
7233
|
+
error: toErrorMeta(error),
|
|
7234
|
+
});
|
|
7235
|
+
});
|
|
7236
|
+
return;
|
|
7237
|
+
}
|
|
7238
|
+
const timer = setTimeout(() => {
|
|
7239
|
+
this.restartPreviewRecoveryTimers.delete(key);
|
|
7240
|
+
void this.retryRestartPreviewRecovery(preview, attempt + 1).catch((error) => {
|
|
7241
|
+
this.logger.warn('telegram.preview_recovery_retry_failed', {
|
|
7242
|
+
scopeId: preview.scopeId,
|
|
7243
|
+
threadId: preview.threadId,
|
|
7244
|
+
turnId: preview.turnId,
|
|
7245
|
+
attempt: attempt + 1,
|
|
7246
|
+
error: toErrorMeta(error),
|
|
7247
|
+
});
|
|
7248
|
+
this.scheduleRestartPreviewRecovery(preview, attempt + 1);
|
|
7249
|
+
});
|
|
7250
|
+
}, delayMs);
|
|
7251
|
+
timer.unref?.();
|
|
7252
|
+
this.restartPreviewRecoveryTimers.set(key, timer);
|
|
7253
|
+
this.logger.info('telegram.preview_recovery_deferred', {
|
|
7254
|
+
scopeId: preview.scopeId,
|
|
7255
|
+
threadId: preview.threadId,
|
|
7256
|
+
turnId: preview.turnId,
|
|
7257
|
+
attempt: attempt + 1,
|
|
7258
|
+
delayMs,
|
|
7259
|
+
});
|
|
7260
|
+
}
|
|
7261
|
+
async retryRestartPreviewRecovery(preview, attempt) {
|
|
7262
|
+
const current = this.store.listActiveTurnPreviews().find((record) => (record.scopeId === preview.scopeId
|
|
7263
|
+
&& record.turnId === preview.turnId
|
|
7264
|
+
&& record.messageId === preview.messageId));
|
|
7265
|
+
if (!current) {
|
|
7266
|
+
return;
|
|
7267
|
+
}
|
|
7268
|
+
if (!this.ownsScope(current.scopeId)) {
|
|
7269
|
+
return;
|
|
7270
|
+
}
|
|
7271
|
+
if (!this.messaging.canSendToScope(current.scopeId)) {
|
|
7272
|
+
this.store.removeActiveTurnPreview(current.turnId);
|
|
7273
|
+
this.logger.info('telegram.preview_dropped_disabled_channel', {
|
|
7274
|
+
scopeId: current.scopeId,
|
|
7275
|
+
threadId: current.threadId,
|
|
7276
|
+
turnId: current.turnId,
|
|
7277
|
+
});
|
|
7278
|
+
return;
|
|
7279
|
+
}
|
|
7280
|
+
try {
|
|
7281
|
+
if (await this.recoverLiveTurnPreview(current)) {
|
|
7282
|
+
this.logger.info('telegram.preview_recovery_retry_succeeded', {
|
|
7283
|
+
scopeId: current.scopeId,
|
|
7284
|
+
threadId: current.threadId,
|
|
7285
|
+
turnId: current.turnId,
|
|
7286
|
+
attempt,
|
|
7287
|
+
});
|
|
7288
|
+
return;
|
|
7289
|
+
}
|
|
7214
7290
|
}
|
|
7291
|
+
catch (error) {
|
|
7292
|
+
this.logger.warn('telegram.preview_recovery_retry_failed', {
|
|
7293
|
+
scopeId: current.scopeId,
|
|
7294
|
+
threadId: current.threadId,
|
|
7295
|
+
turnId: current.turnId,
|
|
7296
|
+
attempt,
|
|
7297
|
+
error: toErrorMeta(error),
|
|
7298
|
+
});
|
|
7299
|
+
}
|
|
7300
|
+
this.scheduleRestartPreviewRecovery(current, attempt);
|
|
7301
|
+
}
|
|
7302
|
+
async retireStaleRestartPreview(preview) {
|
|
7303
|
+
const current = this.store.listActiveTurnPreviews().find((record) => (record.scopeId === preview.scopeId
|
|
7304
|
+
&& record.turnId === preview.turnId
|
|
7305
|
+
&& record.messageId === preview.messageId));
|
|
7306
|
+
if (!current) {
|
|
7307
|
+
return;
|
|
7308
|
+
}
|
|
7309
|
+
await this.retirePreviewMessage(current.scopeId, current.messageId, t(this.localeForChat(current.scopeId), 'stale_preview_restarted', { threadId: current.threadId }), current.turnId);
|
|
7310
|
+
}
|
|
7311
|
+
clearRestartPreviewRecoveryTimers() {
|
|
7312
|
+
for (const timer of this.restartPreviewRecoveryTimers.values()) {
|
|
7313
|
+
clearTimeout(timer);
|
|
7314
|
+
}
|
|
7315
|
+
this.restartPreviewRecoveryTimers.clear();
|
|
7215
7316
|
}
|
|
7216
7317
|
async recoverLiveTurnPreview(preview) {
|
|
7217
7318
|
if (this.getActiveTurn(preview.scopeId, preview.turnId)) {
|
|
@@ -7226,7 +7327,7 @@ export class BridgeSessionCore {
|
|
|
7226
7327
|
}
|
|
7227
7328
|
const snapshot = await this.app.readThreadSnapshot(preview.threadId);
|
|
7228
7329
|
if (!snapshot) {
|
|
7229
|
-
return false;
|
|
7330
|
+
return preview.isObserved ? false : this.resumeInterruptedTurnAfterRestart(preview, target);
|
|
7230
7331
|
}
|
|
7231
7332
|
const liveTurn = findLiveTurn(snapshot);
|
|
7232
7333
|
if (liveTurn) {
|
|
@@ -7305,7 +7406,7 @@ export class BridgeSessionCore {
|
|
|
7305
7406
|
try {
|
|
7306
7407
|
await this.sendTyping(preview.scopeId);
|
|
7307
7408
|
const turnState = await this.startTurnWithRecovery(preview.scopeId, binding, input, {
|
|
7308
|
-
|
|
7409
|
+
notifyMissingThread: false,
|
|
7309
7410
|
});
|
|
7310
7411
|
if (turnState.collaborationMode === 'plan') {
|
|
7311
7412
|
this.store.setChatCollaborationMode(preview.scopeId, DEFAULT_COLLABORATION_MODE);
|
|
@@ -8660,6 +8761,9 @@ function resolveScopeMessageTarget(scopeId) {
|
|
|
8660
8761
|
return null;
|
|
8661
8762
|
}
|
|
8662
8763
|
}
|
|
8764
|
+
function restartPreviewRecoveryKey(preview) {
|
|
8765
|
+
return `${preview.scopeId}:${preview.turnId}:${preview.messageId}`;
|
|
8766
|
+
}
|
|
8663
8767
|
function seedObservedTurnCursor(turn) {
|
|
8664
8768
|
const agentItems = turn.items.filter((item) => {
|
|
8665
8769
|
const type = item.type.toLowerCase();
|
package/dist/i18n.d.ts
CHANGED
|
@@ -418,7 +418,7 @@ declare const MESSAGES: {
|
|
|
418
418
|
readonly stale_preview_restarted: "The bridge restarted while turn {threadId} was still live. This status card is no longer live. Open the thread in Codex.app or send another message to continue.";
|
|
419
419
|
readonly stale_preview_expired: "This live preview expired after the bridge stopped tracking the turn. If a final reply arrived, see below. Otherwise send the message again.";
|
|
420
420
|
readonly stale_user_input_interrupted: "The bridge found a stale Codex input request for thread {threadId} with no recoverable Telegram question. It interrupted that stuck turn; send the message again to continue.";
|
|
421
|
-
readonly restart_auto_resume_prompt: "FoxClaw restarted while the previous turn was running. Continue the interrupted work from where it left off.";
|
|
421
|
+
readonly restart_auto_resume_prompt: "FoxClaw restarted while the previous turn was running. Continue the interrupted work from where it left off. If the previous thread context is unavailable, inspect the current working directory, git status, service logs, and runtime state, then finish the user-facing summary.";
|
|
422
422
|
readonly streaming_reply_below: "Streaming reply below...";
|
|
423
423
|
readonly interrupt_requested_preview: "Interrupt requested...";
|
|
424
424
|
readonly interrupt_requested_waiting: "Interrupt requested. Waiting for Codex to stop...";
|
|
@@ -1115,7 +1115,7 @@ declare const MESSAGES: {
|
|
|
1115
1115
|
readonly stale_preview_restarted: "桥接在该线程 {threadId} 仍处于活动状态时重启了。这张状态卡已经不再实时更新。你可以去 Codex.app 打开线程,或重新发一条消息继续。";
|
|
1116
1116
|
readonly stale_preview_expired: "之前的实时预览在桥接停止跟踪这轮回复后已经失效。如果下方已有最终回复,请以最终回复为准;否则请重新发送消息。";
|
|
1117
1117
|
readonly stale_user_input_interrupted: "桥接发现线程 {threadId} 有一条已经无法恢复到 Telegram 的 Codex 输入请求,已中断这条卡住的回复。请重新发送消息继续。";
|
|
1118
|
-
readonly restart_auto_resume_prompt: "FoxClaw
|
|
1118
|
+
readonly restart_auto_resume_prompt: "FoxClaw 重启时上一轮回复仍在运行。请从中断处继续完成刚才的工作。如果上一线程上下文不可用,请检查当前工作目录、git 状态、服务日志和运行状态,然后完成面向用户的收尾总结。";
|
|
1119
1119
|
readonly streaming_reply_below: "回复正在下方持续输出...";
|
|
1120
1120
|
readonly interrupt_requested_preview: "已请求中断...";
|
|
1121
1121
|
readonly interrupt_requested_waiting: "已请求中断,正在等待 Codex 停止...";
|
package/dist/i18n.js
CHANGED
|
@@ -416,7 +416,7 @@ const MESSAGES = {
|
|
|
416
416
|
stale_preview_restarted: 'The bridge restarted while turn {threadId} was still live. This status card is no longer live. Open the thread in Codex.app or send another message to continue.',
|
|
417
417
|
stale_preview_expired: 'This live preview expired after the bridge stopped tracking the turn. If a final reply arrived, see below. Otherwise send the message again.',
|
|
418
418
|
stale_user_input_interrupted: 'The bridge found a stale Codex input request for thread {threadId} with no recoverable Telegram question. It interrupted that stuck turn; send the message again to continue.',
|
|
419
|
-
restart_auto_resume_prompt: 'FoxClaw restarted while the previous turn was running. Continue the interrupted work from where it left off.',
|
|
419
|
+
restart_auto_resume_prompt: 'FoxClaw restarted while the previous turn was running. Continue the interrupted work from where it left off. If the previous thread context is unavailable, inspect the current working directory, git status, service logs, and runtime state, then finish the user-facing summary.',
|
|
420
420
|
streaming_reply_below: 'Streaming reply below...',
|
|
421
421
|
interrupt_requested_preview: 'Interrupt requested...',
|
|
422
422
|
interrupt_requested_waiting: 'Interrupt requested. Waiting for Codex to stop...',
|
|
@@ -1113,7 +1113,7 @@ const MESSAGES = {
|
|
|
1113
1113
|
stale_preview_restarted: '桥接在该线程 {threadId} 仍处于活动状态时重启了。这张状态卡已经不再实时更新。你可以去 Codex.app 打开线程,或重新发一条消息继续。',
|
|
1114
1114
|
stale_preview_expired: '之前的实时预览在桥接停止跟踪这轮回复后已经失效。如果下方已有最终回复,请以最终回复为准;否则请重新发送消息。',
|
|
1115
1115
|
stale_user_input_interrupted: '桥接发现线程 {threadId} 有一条已经无法恢复到 Telegram 的 Codex 输入请求,已中断这条卡住的回复。请重新发送消息继续。',
|
|
1116
|
-
restart_auto_resume_prompt: 'FoxClaw
|
|
1116
|
+
restart_auto_resume_prompt: 'FoxClaw 重启时上一轮回复仍在运行。请从中断处继续完成刚才的工作。如果上一线程上下文不可用,请检查当前工作目录、git 状态、服务日志和运行状态,然后完成面向用户的收尾总结。',
|
|
1117
1117
|
streaming_reply_below: '回复正在下方持续输出...',
|
|
1118
1118
|
interrupt_requested_preview: '已请求中断...',
|
|
1119
1119
|
interrupt_requested_waiting: '已请求中断,正在等待 Codex 停止...',
|