@foxden-app/foxclaw 0.5.24 → 0.5.26
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 +22 -0
- package/dist/controller/controller.d.ts +2 -0
- package/dist/controller/controller.js +89 -9
- package/dist/i18n.d.ts +2 -0
- package/dist/i18n.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
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.26 - 2026-06-09
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- 修正重启续接判定:如果 Codex app-server 把被重启打断的旧 turn 标成 completed,但没有任何可转发的 assistant/plan 输出或错误内容,FoxClaw 会把它当作中断任务自动续跑,而不是把 Telegram 状态卡误改成“已完成”。
|
|
9
|
+
- 已经有明确输出或错误结果的 completed turn 仍按完成处理,避免对真实完成的任务重复续跑。
|
|
10
|
+
|
|
11
|
+
### English
|
|
12
|
+
- Fixed restart recovery detection: if the Codex app-server marks a restart-interrupted turn as completed but it has no relayable assistant/plan output or error, FoxClaw now treats it as interrupted and auto-resumes it instead of retiring the Telegram status card as completed.
|
|
13
|
+
- Completed turns that do have output or an error are still treated as finished, avoiding duplicate resume runs for truly completed work.
|
|
14
|
+
|
|
15
|
+
## 0.5.25 - 2026-06-09
|
|
16
|
+
|
|
17
|
+
### 中文
|
|
18
|
+
- FoxClaw 重启后会优先重新接管 Codex app-server 中仍在运行的 live turn,包括 turn id 已变化但线程仍有 live turn 的情况,避免误开第二个续跑任务。
|
|
19
|
+
- 如果重启前记录的活动 turn 已被 app-server 中断且没有完成结果,FoxClaw 会自动在同一线程启动一个“继续中断工作”的新 turn,并复用原 Telegram 状态卡继续更新,不再要求用户手动发送“继续”。
|
|
20
|
+
- 如果旧 turn 已经完成,FoxClaw 只收尾旧状态卡,不会误触发自动续跑。
|
|
21
|
+
|
|
22
|
+
### English
|
|
23
|
+
- After a FoxClaw restart, live Codex app-server turns are reattached first, including cases where the live turn id changed but the thread still has an active turn, avoiding a duplicate resume turn.
|
|
24
|
+
- If the previously tracked active turn was interrupted by the app-server restart and has no completed result, FoxClaw automatically starts a continuation turn in the same thread and reuses the existing Telegram status card.
|
|
25
|
+
- If the old turn already completed, FoxClaw only retires the old status card and does not auto-resume it.
|
|
26
|
+
|
|
5
27
|
## 0.5.24 - 2026-06-09
|
|
6
28
|
|
|
7
29
|
### 中文
|
|
@@ -419,6 +419,8 @@ export declare class BridgeSessionCore {
|
|
|
419
419
|
private syncTurnStream;
|
|
420
420
|
private cleanupStaleTurnPreviews;
|
|
421
421
|
private recoverLiveTurnPreview;
|
|
422
|
+
private attachRecoveredTurnPreview;
|
|
423
|
+
private resumeInterruptedTurnAfterRestart;
|
|
422
424
|
private interruptOrphanWaitingUserInput;
|
|
423
425
|
private cleanupFinishedPreview;
|
|
424
426
|
private cleanupStaleInterruptButton;
|
|
@@ -7229,16 +7229,38 @@ export class BridgeSessionCore {
|
|
|
7229
7229
|
return false;
|
|
7230
7230
|
}
|
|
7231
7231
|
const liveTurn = findLiveTurn(snapshot);
|
|
7232
|
-
if (
|
|
7233
|
-
|
|
7232
|
+
if (liveTurn) {
|
|
7233
|
+
if (liveTurn.turnId === preview.turnId
|
|
7234
|
+
&& snapshot.activeFlags.includes('waitingOnUserInput')
|
|
7235
|
+
&& !this.hasPendingUserInputForTurn(preview.scopeId, preview.turnId)) {
|
|
7236
|
+
return this.interruptOrphanWaitingUserInput(preview);
|
|
7237
|
+
}
|
|
7238
|
+
return this.attachRecoveredTurnPreview(preview, target, snapshot, liveTurn);
|
|
7239
|
+
}
|
|
7240
|
+
const previousTurn = snapshot.turns.find(turn => turn.turnId === preview.turnId) ?? null;
|
|
7241
|
+
if (previousTurn && previousTurn.status !== 'inProgress') {
|
|
7242
|
+
if (!turnHasRelayableOutcome(previousTurn)) {
|
|
7243
|
+
return this.resumeInterruptedTurnAfterRestart(preview, target);
|
|
7244
|
+
}
|
|
7245
|
+
await this.retirePreviewMessage(preview.scopeId, preview.messageId, t(this.localeForChat(preview.scopeId), 'completed_see_reply_below'), preview.turnId);
|
|
7246
|
+
return true;
|
|
7234
7247
|
}
|
|
7235
|
-
if (
|
|
7236
|
-
|
|
7237
|
-
return this.interruptOrphanWaitingUserInput(preview);
|
|
7248
|
+
if (preview.isObserved) {
|
|
7249
|
+
return false;
|
|
7238
7250
|
}
|
|
7251
|
+
return this.resumeInterruptedTurnAfterRestart(preview, target);
|
|
7252
|
+
}
|
|
7253
|
+
async attachRecoveredTurnPreview(preview, target, snapshot, liveTurn) {
|
|
7239
7254
|
await this.stopWatchingScopeThread(preview.scopeId, preview.threadId);
|
|
7240
|
-
const active = this.createActiveTurnState(preview.scopeId, target.chatId, target.chatType, target.topicId, preview.threadId,
|
|
7241
|
-
this.setActiveTurn(preview.scopeId,
|
|
7255
|
+
const active = this.createActiveTurnState(preview.scopeId, target.chatId, target.chatType, target.topicId, preview.threadId, liveTurn.turnId, preview.messageId, preview.isObserved);
|
|
7256
|
+
this.setActiveTurn(preview.scopeId, liveTurn.turnId, active);
|
|
7257
|
+
this.store.saveActiveTurnPreview({
|
|
7258
|
+
turnId: liveTurn.turnId,
|
|
7259
|
+
scopeId: preview.scopeId,
|
|
7260
|
+
threadId: preview.threadId,
|
|
7261
|
+
messageId: preview.messageId,
|
|
7262
|
+
isObserved: preview.isObserved,
|
|
7263
|
+
});
|
|
7242
7264
|
const watcher = {
|
|
7243
7265
|
scopeId: preview.scopeId,
|
|
7244
7266
|
chatId: target.chatId,
|
|
@@ -7248,7 +7270,7 @@ export class BridgeSessionCore {
|
|
|
7248
7270
|
mode: 'app_snapshot',
|
|
7249
7271
|
timer: null,
|
|
7250
7272
|
cursor: seedObservedTurnCursor(liveTurn),
|
|
7251
|
-
activeTurnId:
|
|
7273
|
+
activeTurnId: liveTurn.turnId,
|
|
7252
7274
|
waitingOnApproval: snapshot.activeFlags.includes('waitingOnApproval'),
|
|
7253
7275
|
sessionPath: null,
|
|
7254
7276
|
sessionOffset: -1,
|
|
@@ -7263,11 +7285,59 @@ export class BridgeSessionCore {
|
|
|
7263
7285
|
this.logger.info('telegram.preview_recovered', {
|
|
7264
7286
|
scopeId: preview.scopeId,
|
|
7265
7287
|
threadId: preview.threadId,
|
|
7266
|
-
turnId:
|
|
7288
|
+
turnId: liveTurn.turnId,
|
|
7289
|
+
previousTurnId: preview.turnId,
|
|
7267
7290
|
isObserved: preview.isObserved,
|
|
7268
7291
|
});
|
|
7269
7292
|
return true;
|
|
7270
7293
|
}
|
|
7294
|
+
async resumeInterruptedTurnAfterRestart(preview, target) {
|
|
7295
|
+
const locale = this.localeForChat(preview.scopeId);
|
|
7296
|
+
const binding = {
|
|
7297
|
+
threadId: preview.threadId,
|
|
7298
|
+
cwd: this.store.getBinding(preview.scopeId)?.cwd ?? this.config.defaultCwd,
|
|
7299
|
+
};
|
|
7300
|
+
const input = [{
|
|
7301
|
+
type: 'text',
|
|
7302
|
+
text: t(locale, 'restart_auto_resume_prompt'),
|
|
7303
|
+
text_elements: [],
|
|
7304
|
+
}];
|
|
7305
|
+
try {
|
|
7306
|
+
await this.sendTyping(preview.scopeId);
|
|
7307
|
+
const turnState = await this.startTurnWithRecovery(preview.scopeId, binding, input, {
|
|
7308
|
+
recoverMissingThread: false,
|
|
7309
|
+
});
|
|
7310
|
+
if (turnState.collaborationMode === 'plan') {
|
|
7311
|
+
this.store.setChatCollaborationMode(preview.scopeId, DEFAULT_COLLABORATION_MODE);
|
|
7312
|
+
}
|
|
7313
|
+
await this.registerActiveTurn(preview.scopeId, target.chatId, target.chatType, target.topicId, turnState.threadId, turnState.turnId, preview.messageId, {
|
|
7314
|
+
input,
|
|
7315
|
+
threadId: turnState.threadId,
|
|
7316
|
+
cwd: binding.cwd,
|
|
7317
|
+
chatId: target.chatId,
|
|
7318
|
+
chatType: target.chatType,
|
|
7319
|
+
topicId: target.topicId,
|
|
7320
|
+
collaborationMode: turnState.collaborationMode,
|
|
7321
|
+
failedAuthTargets: new Set(),
|
|
7322
|
+
}, turnState.collaborationMode);
|
|
7323
|
+
this.logger.info('telegram.preview_auto_resumed_after_restart', {
|
|
7324
|
+
scopeId: preview.scopeId,
|
|
7325
|
+
threadId: preview.threadId,
|
|
7326
|
+
previousTurnId: preview.turnId,
|
|
7327
|
+
turnId: turnState.turnId,
|
|
7328
|
+
});
|
|
7329
|
+
return true;
|
|
7330
|
+
}
|
|
7331
|
+
catch (error) {
|
|
7332
|
+
this.logger.warn('telegram.preview_auto_resume_after_restart_failed', {
|
|
7333
|
+
scopeId: preview.scopeId,
|
|
7334
|
+
threadId: preview.threadId,
|
|
7335
|
+
turnId: preview.turnId,
|
|
7336
|
+
error: toErrorMeta(error),
|
|
7337
|
+
});
|
|
7338
|
+
return false;
|
|
7339
|
+
}
|
|
7340
|
+
}
|
|
7271
7341
|
async interruptOrphanWaitingUserInput(preview) {
|
|
7272
7342
|
try {
|
|
7273
7343
|
await this.app.interruptTurn(preview.threadId, preview.turnId);
|
|
@@ -8396,6 +8466,16 @@ function summarizeTurnItems(items) {
|
|
|
8396
8466
|
.map(([type, count]) => `${type}=${count}`)
|
|
8397
8467
|
.join(', ');
|
|
8398
8468
|
}
|
|
8469
|
+
function turnHasRelayableOutcome(turn) {
|
|
8470
|
+
if (turn.error?.trim()) {
|
|
8471
|
+
return true;
|
|
8472
|
+
}
|
|
8473
|
+
return turn.items.some((item) => {
|
|
8474
|
+
const type = item.type.toLowerCase();
|
|
8475
|
+
return ((type === 'agentmessage' || type === 'assistantmessage' || type === 'plan')
|
|
8476
|
+
&& Boolean(item.text?.trim()));
|
|
8477
|
+
});
|
|
8478
|
+
}
|
|
8399
8479
|
function mapGoalNotification(raw) {
|
|
8400
8480
|
if (!raw || typeof raw !== 'object') {
|
|
8401
8481
|
return null;
|
package/dist/i18n.d.ts
CHANGED
|
@@ -418,6 +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
422
|
readonly streaming_reply_below: "Streaming reply below...";
|
|
422
423
|
readonly interrupt_requested_preview: "Interrupt requested...";
|
|
423
424
|
readonly interrupt_requested_waiting: "Interrupt requested. Waiting for Codex to stop...";
|
|
@@ -1114,6 +1115,7 @@ declare const MESSAGES: {
|
|
|
1114
1115
|
readonly stale_preview_restarted: "桥接在该线程 {threadId} 仍处于活动状态时重启了。这张状态卡已经不再实时更新。你可以去 Codex.app 打开线程,或重新发一条消息继续。";
|
|
1115
1116
|
readonly stale_preview_expired: "之前的实时预览在桥接停止跟踪这轮回复后已经失效。如果下方已有最终回复,请以最终回复为准;否则请重新发送消息。";
|
|
1116
1117
|
readonly stale_user_input_interrupted: "桥接发现线程 {threadId} 有一条已经无法恢复到 Telegram 的 Codex 输入请求,已中断这条卡住的回复。请重新发送消息继续。";
|
|
1118
|
+
readonly restart_auto_resume_prompt: "FoxClaw 重启时上一轮回复仍在运行。请从中断处继续完成刚才的工作。";
|
|
1117
1119
|
readonly streaming_reply_below: "回复正在下方持续输出...";
|
|
1118
1120
|
readonly interrupt_requested_preview: "已请求中断...";
|
|
1119
1121
|
readonly interrupt_requested_waiting: "已请求中断,正在等待 Codex 停止...";
|
package/dist/i18n.js
CHANGED
|
@@ -416,6 +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
420
|
streaming_reply_below: 'Streaming reply below...',
|
|
420
421
|
interrupt_requested_preview: 'Interrupt requested...',
|
|
421
422
|
interrupt_requested_waiting: 'Interrupt requested. Waiting for Codex to stop...',
|
|
@@ -1112,6 +1113,7 @@ const MESSAGES = {
|
|
|
1112
1113
|
stale_preview_restarted: '桥接在该线程 {threadId} 仍处于活动状态时重启了。这张状态卡已经不再实时更新。你可以去 Codex.app 打开线程,或重新发一条消息继续。',
|
|
1113
1114
|
stale_preview_expired: '之前的实时预览在桥接停止跟踪这轮回复后已经失效。如果下方已有最终回复,请以最终回复为准;否则请重新发送消息。',
|
|
1114
1115
|
stale_user_input_interrupted: '桥接发现线程 {threadId} 有一条已经无法恢复到 Telegram 的 Codex 输入请求,已中断这条卡住的回复。请重新发送消息继续。',
|
|
1116
|
+
restart_auto_resume_prompt: 'FoxClaw 重启时上一轮回复仍在运行。请从中断处继续完成刚才的工作。',
|
|
1115
1117
|
streaming_reply_below: '回复正在下方持续输出...',
|
|
1116
1118
|
interrupt_requested_preview: '已请求中断...',
|
|
1117
1119
|
interrupt_requested_waiting: '已请求中断,正在等待 Codex 停止...',
|