@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 +10 -0
- package/dist/codex_app/client.d.ts +3 -0
- package/dist/codex_app/client.js +8 -0
- package/dist/controller/controller.d.ts +1 -0
- package/dist/controller/controller.js +52 -5
- package/dist/i18n.d.ts +6 -2
- package/dist/i18n.js +6 -2
- package/docs/user-manual.md +3 -1
- package/docs/zh/user-manual.md +3 -1
- package/package.json +1 -1
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;
|
package/dist/codex_app/client.js
CHANGED
|
@@ -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
|
-
|
|
4273
|
-
|
|
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
|
|
4277
|
-
await this.
|
|
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.
|
|
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.
|
|
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: "
|
|
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.
|
|
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: '
|
|
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})。当前回复结束后我会自动发送。',
|
package/docs/user-manual.md
CHANGED
|
@@ -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.
|
|
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
|
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -353,7 +353,9 @@ FoxClaw 的聊天是“绑定线程”的。你在手机上打开某个 Codex
|
|
|
353
353
|
- `/watch <n>`:观察最近一次 `/threads` 列表里的第 n 个线程。
|
|
354
354
|
- `/unwatch`:停止观察。
|
|
355
355
|
|
|
356
|
-
观察模式会同步 live 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
|
|