@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 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
- throw error;
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 (this.config.codexAppSyncOnOpen) {
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
- try {
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
- throw error;
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
- let binding;
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: '这轮回复已经结束了',
@@ -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', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Foxden local execution claw for controlling Codex and OpenCode from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",