@foxden-app/foxclaw 0.7.1 → 0.7.2

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,16 @@
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.7.2 - 2026-08-30
6
+
7
+ ### 中文
8
+ - 为 Codex CLI 0.151.0 及以上版本接入原生跨客户端线程队列。在 Telegram `/watch` 观察电脑端 CLI 会话时,直接发送文字或使用 `/queue <消息>` 会把下一轮任务排入同一线程,由桌面 CLI 在当前轮结束后继续执行。
9
+ - 保持观察模式的 writer 安全边界:FoxClaw 不会 `resume`、中断或接管外部 CLI,`/steer` 仍明确拒绝;旧版 Codex 缺少队列接口时会提示升级,不会改写 session 文件或报告虚假成功。
10
+
11
+ ### English
12
+ - Added the native cross-client thread queue available in Codex CLI 0.151.0 and later. While Telegram `/watch` observes a desktop CLI session, plain text or `/queue <message>` now queues the next task on the same thread for the desktop CLI to continue after its current turn.
13
+ - Preserved the writer-safety boundary of watch mode: FoxClaw does not resume, interrupt, or take over the external CLI, and `/steer` remains explicitly unavailable. Older Codex versions receive an upgrade notice instead of session-file mutation or false success.
14
+
5
15
  ## 0.7.1 - 2026-08-30
6
16
 
7
17
  ### 中文
@@ -98,6 +98,9 @@ export declare class CodexAppClient extends EventEmitter {
98
98
  steerTurn(threadId: string, expectedTurnId: string, input: TurnInput[]): Promise<{
99
99
  turnId: string;
100
100
  }>;
101
+ queueThreadInput(threadId: string, clientUserMessageId: string, input: TurnInput[]): Promise<{
102
+ queuedSubmissionId: string;
103
+ }>;
101
104
  forkThread(options: {
102
105
  threadId: string;
103
106
  cwd: string | null;
@@ -186,6 +186,14 @@ export class CodexAppClient extends EventEmitter {
186
186
  const result = await this.request('turn/steer', { threadId, expectedTurnId, input });
187
187
  return { turnId: String(result?.turnId ?? expectedTurnId) };
188
188
  }
189
+ async queueThreadInput(threadId, clientUserMessageId, input) {
190
+ const result = await this.request('thread/queue/add', { threadId, clientUserMessageId, input });
191
+ const queuedSubmissionId = result?.queuedSubmission?.id;
192
+ if (typeof queuedSubmissionId !== 'string' || !queuedSubmissionId) {
193
+ throw new Error('thread/queue/add returned no queued submission id');
194
+ }
195
+ return { queuedSubmissionId };
196
+ }
189
197
  async forkThread(options) {
190
198
  const params = {
191
199
  threadId: options.threadId,
@@ -289,6 +289,7 @@ export declare class BridgeSessionCore {
289
289
  private handleTakeoverCommand;
290
290
  private handleQueueCommand;
291
291
  private handleActiveTurnInboundMessage;
292
+ private queueObservedThreadMessage;
292
293
  private queuePromptAfterActiveTurn;
293
294
  private steerActiveTurn;
294
295
  private enqueuePreparedTurnInput;
@@ -359,6 +359,9 @@ export class BridgeSessionCore {
359
359
  await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
360
360
  return;
361
361
  }
362
+ if (await this.queueObservedThreadMessage(event, locale, event.text.trim())) {
363
+ return;
364
+ }
362
365
  await this.startBoundTurnFromEvent(event, locale, event.text.trim());
363
366
  return;
364
367
  }
@@ -417,6 +420,9 @@ export class BridgeSessionCore {
417
420
  await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
418
421
  return;
419
422
  }
423
+ if (await this.queueObservedThreadMessage(event, locale, decision.text)) {
424
+ return;
425
+ }
420
426
  await this.startBoundTurnFromEvent(event, locale, decision.text);
421
427
  }
422
428
  async handleCommand(event, locale, name, args) {
@@ -4269,12 +4275,15 @@ export class BridgeSessionCore {
4269
4275
  return;
4270
4276
  }
4271
4277
  const active = this.findActiveTurn(scopeId);
4272
- if (!active) {
4273
- await this.startBoundTurnFromEvent(event, locale, nextPrompt);
4278
+ const observedThreadId = active?.isObserved
4279
+ ? active.threadId
4280
+ : this.observedThreadWatchers.get(scopeId)?.threadId ?? null;
4281
+ if (observedThreadId) {
4282
+ await this.queueObservedThreadMessage(event, locale, nextPrompt, observedThreadId);
4274
4283
  return;
4275
4284
  }
4276
- if (active.isObserved) {
4277
- await this.sendMessage(scopeId, t(locale, 'watch_read_only_active'));
4285
+ if (!active) {
4286
+ await this.startBoundTurnFromEvent(event, locale, nextPrompt);
4278
4287
  return;
4279
4288
  }
4280
4289
  await this.queuePromptAfterActiveTurn(event, locale, nextPrompt);
@@ -4285,7 +4294,7 @@ export class BridgeSessionCore {
4285
4294
  return;
4286
4295
  }
4287
4296
  if (active.isObserved) {
4288
- await this.sendMessage(event.scopeId, t(locale, 'watch_read_only_active'));
4297
+ await this.queueObservedThreadMessage(event, locale, text, active.threadId);
4289
4298
  return;
4290
4299
  }
4291
4300
  const settings = this.store.getChatSettings(event.scopeId);
@@ -4311,6 +4320,34 @@ export class BridgeSessionCore {
4311
4320
  await this.startBoundTurnFromEvent(event, locale, text);
4312
4321
  }
4313
4322
  }
4323
+ async queueObservedThreadMessage(event, locale, text, threadId = this.observedThreadWatchers.get(event.scopeId)?.threadId ?? null) {
4324
+ if (!threadId) {
4325
+ return false;
4326
+ }
4327
+ const input = await this.buildTurnInput({
4328
+ threadId,
4329
+ cwd: this.store.getBinding(event.scopeId)?.cwd ?? this.config.defaultCwd,
4330
+ }, { ...event, text }, locale);
4331
+ const clientUserMessageId = `foxclaw:${event.scopeId}:${event.messageId}`;
4332
+ try {
4333
+ const queued = await this.app.queueThreadInput(threadId, clientUserMessageId, input);
4334
+ await this.sendMessage(event.scopeId, t(locale, 'watch_prompt_queued', {
4335
+ id: shortId(queued.queuedSubmissionId),
4336
+ }));
4337
+ }
4338
+ catch (error) {
4339
+ if (!isThreadQueueUnsupportedError(error)) {
4340
+ throw error;
4341
+ }
4342
+ this.logger.warn('codex.observed_thread_queue_unsupported', {
4343
+ scopeId: event.scopeId,
4344
+ threadId,
4345
+ error: toErrorMeta(error),
4346
+ });
4347
+ await this.sendMessage(event.scopeId, t(locale, 'watch_queue_unsupported'));
4348
+ }
4349
+ return true;
4350
+ }
4314
4351
  async queuePromptAfterActiveTurn(event, locale, text) {
4315
4352
  const active = this.findActiveTurn(event.scopeId);
4316
4353
  if (!active) {
@@ -5601,6 +5638,10 @@ export class BridgeSessionCore {
5601
5638
  await this.sendMessage(scopeId, t(locale, 'no_active_turn'));
5602
5639
  return;
5603
5640
  }
5641
+ if (active.isObserved) {
5642
+ await this.sendMessage(scopeId, t(locale, 'watch_read_only_active'));
5643
+ return;
5644
+ }
5604
5645
  await this.app.steerTurn(active.threadId, active.turnId, [{
5605
5646
  type: 'text',
5606
5647
  text,
@@ -11811,6 +11852,12 @@ function isThreadActiveWriterError(error) {
11811
11852
  function isNoActiveTurnToSteerError(error) {
11812
11853
  return error instanceof Error && /no active turn to steer/i.test(error.message);
11813
11854
  }
11855
+ function isThreadQueueUnsupportedError(error) {
11856
+ const message = formatUserError(error).toLowerCase();
11857
+ return message.includes('method not found')
11858
+ || message.includes('unknown method')
11859
+ || message.includes('thread/queue/add') && message.includes('not supported');
11860
+ }
11814
11861
  function isThreadNewCwdCreateConfirmation(text) {
11815
11862
  return /^(y|yes|ok|okay|confirm|create|mkdir|确定|确认|创建|新建|好)$/i.test(text.trim());
11816
11863
  }
package/dist/i18n.d.ts CHANGED
@@ -309,7 +309,9 @@ declare const MESSAGES: {
309
309
  readonly watch_already_enabled: "Already watching thread {threadId}.";
310
310
  readonly watch_started_active: "Watching thread {threadId}. Live Codex updates from the current turn will appear here.";
311
311
  readonly watch_started_idle: "Watching thread {threadId}. I will mirror the next live turn from Codex CLI here.";
312
- readonly watch_read_only_active: "This chat is watching that turn read-only. Use approval commands if needed, or /unwatch before sending a new prompt.";
312
+ readonly watch_read_only_active: "This chat is watching that turn read-only, so this operation cannot take ownership. Send plain text or use /queue to queue the next prompt, or /unwatch to start separately.";
313
+ readonly watch_prompt_queued: "Queued for the watched Codex CLI session ({id}). The CLI will continue with it after the current turn.";
314
+ readonly watch_queue_unsupported: "This Codex version does not support cross-client session queues. Update Codex to 0.151.0 or later.";
313
315
  readonly watch_not_enabled: "This chat is not watching any thread.";
314
316
  readonly watch_stopped: "Stopped watching thread {threadId}.";
315
317
  readonly queued_prompt_set: "Queued #{position} ({id}). I will send it after the current turn finishes.";
@@ -1035,7 +1037,9 @@ declare const MESSAGES: {
1035
1037
  readonly watch_already_enabled: "已经在观察线程 {threadId}。";
1036
1038
  readonly watch_started_active: "已开始观察线程 {threadId}。当前这轮 Codex 更新会同步到这里。";
1037
1039
  readonly watch_started_idle: "已开始观察线程 {threadId}。之后 CLI 里这条线程一旦有新 live turn,我会同步到这里。";
1038
- readonly watch_read_only_active: "当前聊天正在只读观察这轮。需要审批时可用审批命令;要发送新 prompt 请先 /unwatch。";
1040
+ readonly watch_read_only_active: "当前聊天正在只读观察这轮,不能接管这项操作。直接发送文字或使用 /queue 可排入下一轮;要另行启动请先 /unwatch。";
1041
+ readonly watch_prompt_queued: "已排入正在观察的 Codex CLI 会话({id})。当前轮结束后,CLI 会自动接着执行。";
1042
+ readonly watch_queue_unsupported: "当前 Codex 不支持跨客户端会话队列,请升级到 Codex 0.151.0 或更高版本。";
1039
1043
  readonly watch_not_enabled: "当前没有正在观察的线程。";
1040
1044
  readonly watch_stopped: "已停止观察线程 {threadId}。";
1041
1045
  readonly queued_prompt_set: "已排队 #{position}({id})。当前回复结束后我会自动发送。";
package/dist/i18n.js CHANGED
@@ -307,7 +307,9 @@ const MESSAGES = {
307
307
  watch_already_enabled: 'Already watching thread {threadId}.',
308
308
  watch_started_active: 'Watching thread {threadId}. Live Codex updates from the current turn will appear here.',
309
309
  watch_started_idle: 'Watching thread {threadId}. I will mirror the next live turn from Codex CLI here.',
310
- watch_read_only_active: 'This chat is watching that turn read-only. Use approval commands if needed, or /unwatch before sending a new prompt.',
310
+ watch_read_only_active: 'This chat is watching that turn read-only, so this operation cannot take ownership. Send plain text or use /queue to queue the next prompt, or /unwatch to start separately.',
311
+ watch_prompt_queued: 'Queued for the watched Codex CLI session ({id}). The CLI will continue with it after the current turn.',
312
+ watch_queue_unsupported: 'This Codex version does not support cross-client session queues. Update Codex to 0.151.0 or later.',
311
313
  watch_not_enabled: 'This chat is not watching any thread.',
312
314
  watch_stopped: 'Stopped watching thread {threadId}.',
313
315
  queued_prompt_set: 'Queued #{position} ({id}). I will send it after the current turn finishes.',
@@ -1033,7 +1035,9 @@ const MESSAGES = {
1033
1035
  watch_already_enabled: '已经在观察线程 {threadId}。',
1034
1036
  watch_started_active: '已开始观察线程 {threadId}。当前这轮 Codex 更新会同步到这里。',
1035
1037
  watch_started_idle: '已开始观察线程 {threadId}。之后 CLI 里这条线程一旦有新 live turn,我会同步到这里。',
1036
- watch_read_only_active: '当前聊天正在只读观察这轮。需要审批时可用审批命令;要发送新 prompt 请先 /unwatch。',
1038
+ watch_read_only_active: '当前聊天正在只读观察这轮,不能接管这项操作。直接发送文字或使用 /queue 可排入下一轮;要另行启动请先 /unwatch。',
1039
+ watch_prompt_queued: '已排入正在观察的 Codex CLI 会话({id})。当前轮结束后,CLI 会自动接着执行。',
1040
+ watch_queue_unsupported: '当前 Codex 不支持跨客户端会话队列,请升级到 Codex 0.151.0 或更高版本。',
1037
1041
  watch_not_enabled: '当前没有正在观察的线程。',
1038
1042
  watch_stopped: '已停止观察线程 {threadId}。',
1039
1043
  queued_prompt_set: '已排队 #{position}({id})。当前回复结束后我会自动发送。',
@@ -353,7 +353,9 @@ Usage:
353
353
  - `/watch <n>`: watch item n from the latest `/threads` list.
354
354
  - `/unwatch`: stop watching.
355
355
 
356
- Watch mode mirrors live turn progress and approval requests. The watching chat is read-only for normal prompts during the observed turn. Send `/unwatch` before starting a new prompt from that chat, or wait for the turn to finish.
356
+ Watch mode mirrors live turn progress and approval requests. It still does not take ownership of the CLI writer, and `/steer` cannot modify the in-flight turn. With Codex CLI 0.151.0 or later, however, plain text and `/queue <message>` use Codex's cross-client queue for the same thread. The desktop CLI automatically continues with those messages, in order, after the current turn finishes.
357
+
358
+ Older Codex versions do not expose this queue API. FoxClaw reports the required upgrade instead of falling back to writer takeover or session-file mutation. Use `/unwatch` first only when you want Telegram to start a separate turn itself.
357
359
 
358
360
  ## 6. Codex Login And Auth Rotation
359
361
 
@@ -353,7 +353,9 @@ FoxClaw 的聊天是“绑定线程”的。你在手机上打开某个 Codex
353
353
  - `/watch <n>`:观察最近一次 `/threads` 列表里的第 n 个线程。
354
354
  - `/unwatch`:停止观察。
355
355
 
356
- 观察模式会同步 live turn 进展和审批请求。观察中的聊天默认是只读的,普通 prompt 不会直接插入正在观察的 turn;需要发新任务时先 `/unwatch`,或等当前 turn 完成。
356
+ 观察模式会同步 live turn 进展和审批请求。观察中的聊天仍然不接管 CLI writer,也不能用 `/steer` 修改正在执行的 turn;但从 Codex CLI 0.151.0 开始,直接发送文字或使用 `/queue <消息>` 会通过 Codex 的跨客户端队列排入同一线程,由桌面 CLI 在当前 turn 结束后自动继续执行。连续发送多条会按顺序排队。
357
+
358
+ 旧版 Codex 没有这个队列接口,FoxClaw 会明确提示升级,不会退回到抢占 writer 或改写 session 文件。要让 Telegram 自己另行启动 turn,仍需先 `/unwatch`。
357
359
 
358
360
  ## 6. Codex 登录和 auth 轮转
359
361
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
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",