@foxden-app/foxclaw 0.6.5 → 0.6.7
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 +18 -0
- package/dist/controller/controller.d.ts +1 -0
- package/dist/controller/controller.js +37 -26
- package/dist/i18n.d.ts +4 -0
- package/dist/i18n.js +4 -0
- package/dist/telegram/gateway.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
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.6.7 - 2026-08-12
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- 修复从 `/threads` 观察正在由 Codex CLI 执行的线程时错误调用 `thread/resume`、抢占 writer 并报 `already has an active writer` 的问题。观察现在只读绑定线程,并直接尾随 CLI session 文件。
|
|
9
|
+
- 打开被其他 Codex 客户端占用的线程时,仅对明确的 active-writer 冲突降级为只读选中并提示使用 `/watch`;其他错误仍然显式失败。
|
|
10
|
+
|
|
11
|
+
### English
|
|
12
|
+
- Fixed `/threads` Watch attempting `thread/resume` for a thread currently running in Codex CLI, which tried to acquire its writer and failed with `already has an active writer`. Watch now binds read-only and tails the CLI session file directly.
|
|
13
|
+
- Opening a thread owned by another Codex client now falls back to an explicit read-only selection with a `/watch` hint only for the specific active-writer conflict. Other errors still fail visibly.
|
|
14
|
+
|
|
15
|
+
## 0.6.6 - 2026-08-10
|
|
16
|
+
|
|
17
|
+
### 中文
|
|
18
|
+
- Telegram 远端初始化从断网中恢复后会立即刷新桥接状态,使 `/status` 和运行时状态继续显示 bot 用户名,而不是停留在离线启动时的空值。
|
|
19
|
+
|
|
20
|
+
### English
|
|
21
|
+
- Refreshes bridge status as soon as Telegram remote initialization recovers, so `/status` and runtime state show the bot username instead of retaining the empty offline-start value.
|
|
22
|
+
|
|
5
23
|
## 0.6.5 - 2026-08-10
|
|
6
24
|
|
|
7
25
|
### 中文
|
|
@@ -259,6 +259,7 @@ export declare class BridgeSessionCore {
|
|
|
259
259
|
private expireApproval;
|
|
260
260
|
private tryRevealThread;
|
|
261
261
|
private bindCachedThread;
|
|
262
|
+
private bindCachedThreadReadOnly;
|
|
262
263
|
private resumeThreadForScope;
|
|
263
264
|
private setChatAccessPreset;
|
|
264
265
|
private clearAttachedThreadsForScope;
|
|
@@ -174,6 +174,10 @@ export class BridgeSessionCore {
|
|
|
174
174
|
}
|
|
175
175
|
/** Wire Telegram inbound events. Call before {@link startCodexApp}. */
|
|
176
176
|
registerTelegramInboundHandlers() {
|
|
177
|
+
this.bot.on('remoteReady', () => {
|
|
178
|
+
this.botUsername = this.bot.username;
|
|
179
|
+
this.updateStatus();
|
|
180
|
+
});
|
|
177
181
|
this.bot.on('text', (event) => {
|
|
178
182
|
void this.withLock(event.scopeId, async () => this.handleText(event)).catch((error) => {
|
|
179
183
|
void this.handleAsyncError('telegram.text', error, event.scopeId);
|
|
@@ -646,6 +650,7 @@ export class BridgeSessionCore {
|
|
|
646
650
|
this.store.cancelQueuedTurnInputs(scopeId);
|
|
647
651
|
await this.stopWatchingScopeThread(scopeId, thread.threadId);
|
|
648
652
|
let binding;
|
|
653
|
+
let readOnly = false;
|
|
649
654
|
try {
|
|
650
655
|
binding = await this.bindCachedThread(scopeId, thread.threadId);
|
|
651
656
|
}
|
|
@@ -654,7 +659,10 @@ export class BridgeSessionCore {
|
|
|
654
659
|
await this.sendMessage(scopeId, t(locale, 'cached_thread_unavailable'));
|
|
655
660
|
return;
|
|
656
661
|
}
|
|
657
|
-
|
|
662
|
+
if (!isThreadActiveWriterError(error))
|
|
663
|
+
throw error;
|
|
664
|
+
binding = this.bindCachedThreadReadOnly(scopeId, thread);
|
|
665
|
+
readOnly = true;
|
|
658
666
|
}
|
|
659
667
|
const settings = this.store.getChatSettings(scopeId);
|
|
660
668
|
const lines = [
|
|
@@ -664,7 +672,10 @@ export class BridgeSessionCore {
|
|
|
664
672
|
t(locale, 'status_configured_effort', { value: settings?.reasoningEffort ?? t(locale, 'server_default') }),
|
|
665
673
|
t(locale, 'line_cwd', { value: binding.cwd ?? this.config.defaultCwd }),
|
|
666
674
|
];
|
|
667
|
-
if (
|
|
675
|
+
if (readOnly) {
|
|
676
|
+
lines.push(t(locale, 'thread_active_writer_read_only'));
|
|
677
|
+
}
|
|
678
|
+
if (!readOnly && this.config.codexAppSyncOnOpen) {
|
|
668
679
|
const revealError = await this.tryRevealThread(scopeId, binding.threadId, 'open');
|
|
669
680
|
lines.push(revealError ? t(locale, 'codex_sync_failed', { error: revealError }) : t(locale, 'opened_in_codex'));
|
|
670
681
|
}
|
|
@@ -977,16 +988,7 @@ export class BridgeSessionCore {
|
|
|
977
988
|
await this.sendMessage(scopeId, t(locale, 'thread_is_archived_use_unarchive'));
|
|
978
989
|
return;
|
|
979
990
|
}
|
|
980
|
-
|
|
981
|
-
binding = await this.bindCachedThread(scopeId, cached.threadId);
|
|
982
|
-
}
|
|
983
|
-
catch (error) {
|
|
984
|
-
if (isThreadNotFoundError(error)) {
|
|
985
|
-
await this.sendMessage(scopeId, t(locale, 'cached_thread_unavailable'));
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
throw error;
|
|
989
|
-
}
|
|
991
|
+
binding = this.bindCachedThreadReadOnly(scopeId, cached);
|
|
990
992
|
}
|
|
991
993
|
else {
|
|
992
994
|
binding = this.store.getBinding(scopeId);
|
|
@@ -3717,6 +3719,18 @@ export class BridgeSessionCore {
|
|
|
3717
3719
|
const session = await this.resumeThreadForScope(scopeId, { threadId, cwd: null });
|
|
3718
3720
|
return this.storeThreadSession(scopeId, session, 'replace');
|
|
3719
3721
|
}
|
|
3722
|
+
bindCachedThreadReadOnly(scopeId, thread) {
|
|
3723
|
+
const binding = {
|
|
3724
|
+
chatId: scopeId,
|
|
3725
|
+
threadId: thread.threadId,
|
|
3726
|
+
cwd: thread.cwd,
|
|
3727
|
+
updatedAt: Date.now(),
|
|
3728
|
+
};
|
|
3729
|
+
this.store.setBinding(scopeId, binding.threadId, binding.cwd);
|
|
3730
|
+
this.attachedThreads.delete(attachedThreadKey(scopeId, binding.threadId));
|
|
3731
|
+
this.updateStatus();
|
|
3732
|
+
return binding;
|
|
3733
|
+
}
|
|
3720
3734
|
async resumeThreadForScope(scopeId, binding) {
|
|
3721
3735
|
const settings = this.store.getChatSettings(scopeId);
|
|
3722
3736
|
const access = this.resolveEffectiveAccess(scopeId, settings);
|
|
@@ -7003,6 +7017,7 @@ export class BridgeSessionCore {
|
|
|
7003
7017
|
}
|
|
7004
7018
|
await this.stopWatchingScopeThread(scopeId, threadId);
|
|
7005
7019
|
let binding;
|
|
7020
|
+
let readOnly = false;
|
|
7006
7021
|
try {
|
|
7007
7022
|
binding = await this.bindCachedThread(scopeId, threadId);
|
|
7008
7023
|
}
|
|
@@ -7011,7 +7026,10 @@ export class BridgeSessionCore {
|
|
|
7011
7026
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'thread_no_longer_available'));
|
|
7012
7027
|
return;
|
|
7013
7028
|
}
|
|
7014
|
-
|
|
7029
|
+
if (!cached || !isThreadActiveWriterError(error))
|
|
7030
|
+
throw error;
|
|
7031
|
+
binding = this.bindCachedThreadReadOnly(scopeId, cached);
|
|
7032
|
+
readOnly = true;
|
|
7015
7033
|
}
|
|
7016
7034
|
const threads = this.store.listCachedThreads(scopeId);
|
|
7017
7035
|
if (threads.length > 0) {
|
|
@@ -7042,8 +7060,8 @@ export class BridgeSessionCore {
|
|
|
7042
7060
|
await this.editHtmlMessage(scopeId, event.messageId, text, keyboard);
|
|
7043
7061
|
this.scheduleStalePanelDeletion(scopeId, event.messageId);
|
|
7044
7062
|
}
|
|
7045
|
-
let callbackText = t(locale, 'thread_opened');
|
|
7046
|
-
if (this.config.codexAppSyncOnOpen) {
|
|
7063
|
+
let callbackText = readOnly ? t(locale, 'thread_opened_read_only') : t(locale, 'thread_opened');
|
|
7064
|
+
if (!readOnly && this.config.codexAppSyncOnOpen) {
|
|
7047
7065
|
const revealError = await this.tryRevealThread(scopeId, binding.threadId, 'open');
|
|
7048
7066
|
callbackText = revealError ? t(locale, 'opened_sync_failed_short') : t(locale, 'opened_in_codex_short');
|
|
7049
7067
|
}
|
|
@@ -7076,17 +7094,7 @@ export class BridgeSessionCore {
|
|
|
7076
7094
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'thread_is_archived_use_unarchive'));
|
|
7077
7095
|
return;
|
|
7078
7096
|
}
|
|
7079
|
-
|
|
7080
|
-
try {
|
|
7081
|
-
binding = await this.bindCachedThread(scopeId, threadId);
|
|
7082
|
-
}
|
|
7083
|
-
catch (error) {
|
|
7084
|
-
if (isThreadNotFoundError(error)) {
|
|
7085
|
-
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'thread_no_longer_available'));
|
|
7086
|
-
return;
|
|
7087
|
-
}
|
|
7088
|
-
throw error;
|
|
7089
|
-
}
|
|
7097
|
+
const binding = this.bindCachedThreadReadOnly(scopeId, cached);
|
|
7090
7098
|
const target = resolveScopeMessageTarget(scopeId);
|
|
7091
7099
|
if (!target) {
|
|
7092
7100
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'unsupported_action'));
|
|
@@ -11670,6 +11678,9 @@ function formatShortStatusError(error) {
|
|
|
11670
11678
|
function isThreadNotFoundError(error) {
|
|
11671
11679
|
return error instanceof Error && /(thread not found|no rollout found for thread id)/i.test(error.message);
|
|
11672
11680
|
}
|
|
11681
|
+
function isThreadActiveWriterError(error) {
|
|
11682
|
+
return error instanceof Error && /thread\s+\S+\s+already has an active writer/i.test(error.message);
|
|
11683
|
+
}
|
|
11673
11684
|
function isNoActiveTurnToSteerError(error) {
|
|
11674
11685
|
return error instanceof Error && /no active turn to steer/i.test(error.message);
|
|
11675
11686
|
}
|
package/dist/i18n.d.ts
CHANGED
|
@@ -325,6 +325,7 @@ declare const MESSAGES: {
|
|
|
325
325
|
readonly attachment_batch_button_analyze: "Analyze";
|
|
326
326
|
readonly attachment_batch_button_clear: "Clear";
|
|
327
327
|
readonly bound_to_thread: "Bound to thread {threadId}";
|
|
328
|
+
readonly thread_active_writer_read_only: "Another Codex client is currently writing this thread. It is selected read-only; use /watch to mirror live output.";
|
|
328
329
|
readonly line_title: "Title: {value}";
|
|
329
330
|
readonly line_cwd: "CWD: {value}";
|
|
330
331
|
readonly codex_sync_failed: "Codex.app sync failed: {error}";
|
|
@@ -407,6 +408,7 @@ declare const MESSAGES: {
|
|
|
407
408
|
readonly fast_configured: "Fast: {value}";
|
|
408
409
|
readonly thread_no_longer_available: "Thread is no longer available";
|
|
409
410
|
readonly thread_opened: "Thread opened";
|
|
411
|
+
readonly thread_opened_read_only: "Selected read-only; tap Watch for live output";
|
|
410
412
|
readonly opened_sync_failed_short: "Opened, Codex.app sync failed";
|
|
411
413
|
readonly opened_in_codex_short: "Opened in Codex.app";
|
|
412
414
|
readonly turn_already_finished: "Turn already finished";
|
|
@@ -1049,6 +1051,7 @@ declare const MESSAGES: {
|
|
|
1049
1051
|
readonly attachment_batch_button_analyze: "分析";
|
|
1050
1052
|
readonly attachment_batch_button_clear: "清除";
|
|
1051
1053
|
readonly bound_to_thread: "已绑定到线程 {threadId}";
|
|
1054
|
+
readonly thread_active_writer_read_only: "另一个 Codex 客户端正在写入这个线程。现已只读选中;使用 /watch 可同步实时输出。";
|
|
1052
1055
|
readonly line_title: "标题:{value}";
|
|
1053
1056
|
readonly line_cwd: "工作目录:{value}";
|
|
1054
1057
|
readonly codex_sync_failed: "同步到 Codex.app 失败:{error}";
|
|
@@ -1131,6 +1134,7 @@ declare const MESSAGES: {
|
|
|
1131
1134
|
readonly fast_configured: "Fast:{value}";
|
|
1132
1135
|
readonly thread_no_longer_available: "这个线程已经不可用";
|
|
1133
1136
|
readonly thread_opened: "线程已打开";
|
|
1137
|
+
readonly thread_opened_read_only: "已只读选中;点观察可同步输出";
|
|
1134
1138
|
readonly opened_sync_failed_short: "已打开,但同步到 Codex.app 失败";
|
|
1135
1139
|
readonly opened_in_codex_short: "已在 Codex.app 中打开";
|
|
1136
1140
|
readonly turn_already_finished: "这轮回复已经结束了";
|
package/dist/i18n.js
CHANGED
|
@@ -323,6 +323,7 @@ const MESSAGES = {
|
|
|
323
323
|
attachment_batch_button_analyze: 'Analyze',
|
|
324
324
|
attachment_batch_button_clear: 'Clear',
|
|
325
325
|
bound_to_thread: 'Bound to thread {threadId}',
|
|
326
|
+
thread_active_writer_read_only: 'Another Codex client is currently writing this thread. It is selected read-only; use /watch to mirror live output.',
|
|
326
327
|
line_title: 'Title: {value}',
|
|
327
328
|
line_cwd: 'CWD: {value}',
|
|
328
329
|
codex_sync_failed: 'Codex.app sync failed: {error}',
|
|
@@ -405,6 +406,7 @@ const MESSAGES = {
|
|
|
405
406
|
fast_configured: 'Fast: {value}',
|
|
406
407
|
thread_no_longer_available: 'Thread is no longer available',
|
|
407
408
|
thread_opened: 'Thread opened',
|
|
409
|
+
thread_opened_read_only: 'Selected read-only; tap Watch for live output',
|
|
408
410
|
opened_sync_failed_short: 'Opened, Codex.app sync failed',
|
|
409
411
|
opened_in_codex_short: 'Opened in Codex.app',
|
|
410
412
|
turn_already_finished: 'Turn already finished',
|
|
@@ -1047,6 +1049,7 @@ const MESSAGES = {
|
|
|
1047
1049
|
attachment_batch_button_analyze: '分析',
|
|
1048
1050
|
attachment_batch_button_clear: '清除',
|
|
1049
1051
|
bound_to_thread: '已绑定到线程 {threadId}',
|
|
1052
|
+
thread_active_writer_read_only: '另一个 Codex 客户端正在写入这个线程。现已只读选中;使用 /watch 可同步实时输出。',
|
|
1050
1053
|
line_title: '标题:{value}',
|
|
1051
1054
|
line_cwd: '工作目录:{value}',
|
|
1052
1055
|
codex_sync_failed: '同步到 Codex.app 失败:{error}',
|
|
@@ -1129,6 +1132,7 @@ const MESSAGES = {
|
|
|
1129
1132
|
fast_configured: 'Fast:{value}',
|
|
1130
1133
|
thread_no_longer_available: '这个线程已经不可用',
|
|
1131
1134
|
thread_opened: '线程已打开',
|
|
1135
|
+
thread_opened_read_only: '已只读选中;点观察可同步输出',
|
|
1132
1136
|
opened_sync_failed_short: '已打开,但同步到 Codex.app 失败',
|
|
1133
1137
|
opened_in_codex_short: '已在 Codex.app 中打开',
|
|
1134
1138
|
turn_already_finished: '这轮回复已经结束了',
|
package/dist/telegram/gateway.js
CHANGED
|
@@ -246,6 +246,9 @@ export class TelegramGateway extends EventEmitter {
|
|
|
246
246
|
await this.resolveBotIdentity(true);
|
|
247
247
|
await this.registerCommands();
|
|
248
248
|
remoteInitialized = true;
|
|
249
|
+
this.emit('remoteReady');
|
|
250
|
+
if (!this.running)
|
|
251
|
+
return;
|
|
249
252
|
}
|
|
250
253
|
const offset = this.store.getTelegramOffset(this.botKey) + 1;
|
|
251
254
|
const result = await callTelegramApi(this.botToken, 'getUpdates', {
|