@foxden-app/foxclaw 0.6.7 → 0.6.9
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,26 @@
|
|
|
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.9 - 2026-08-15
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- 补齐 Codex CLI 0.147.0 会话日志的其他观察兼容:新的 `item_completed` / `UserMessage` 会继续把 CLI 侧补充输入镜像到 Telegram,保持原有 `codex-cli-user` 体验。
|
|
9
|
+
- 识别新的 `custom_tool_call`、`function_call` 及其 output 记录,严格按 `call_id` 跨轮询分片配对,让长时间工具调用恢复“正在运行/已运行”状态;未知或孤立 output 保持静默,不猜测配对。`Reasoning`、`ContextCompaction`、`Extension` 等内部记录仍不会污染聊天。
|
|
10
|
+
|
|
11
|
+
### English
|
|
12
|
+
- Completed the remaining Codex CLI 0.147.0 session-log observation compatibility. New `item_completed` / `UserMessage` records continue mirroring CLI-side follow-up input to Telegram with the existing `codex-cli-user` experience.
|
|
13
|
+
- Recognizes new `custom_tool_call`, `function_call`, and matching output records across polling chunks, pairing them strictly by `call_id` so long-running tools regain Running/Completed status. Unknown or orphan outputs stay silent, while internal `Reasoning`, `ContextCompaction`, and `Extension` records remain chat-invisible.
|
|
14
|
+
|
|
15
|
+
## 0.6.8 - 2026-08-15
|
|
16
|
+
|
|
17
|
+
### 中文
|
|
18
|
+
- 修复只读观察 Codex CLI 0.147.0 会话时只显示“正在思考...”和“已完成。”、遗漏实际回复的问题。会话日志观察器现在识别新的 `item_completed` / `AgentMessage` 结构,并沿用 Codex 消息 ID,避免同一正文与 `response_item` 镜像重复发送。
|
|
19
|
+
- 识别新日志中的 `turn_aborted` 和 `turn_interrupted`,让被中断的外部 CLI 轮次明确结束为“已中断”,不再残留为进行中状态。
|
|
20
|
+
|
|
21
|
+
### English
|
|
22
|
+
- Fixed read-only observation of Codex CLI 0.147.0 sessions showing only “Thinking...” and “Completed.” while dropping the actual response. The session-log observer now recognizes the new `item_completed` / `AgentMessage` shape and preserves Codex message IDs without duplicating the mirrored `response_item`.
|
|
23
|
+
- Recognizes `turn_aborted` and `turn_interrupted` in new session logs so interrupted external CLI turns finish explicitly instead of remaining active.
|
|
24
|
+
|
|
5
25
|
## 0.6.7 - 2026-08-12
|
|
6
26
|
|
|
7
27
|
### 中文
|
|
@@ -2,6 +2,7 @@ import { type TurnActivityEvent } from './activity.js';
|
|
|
2
2
|
export interface SessionLogCursor {
|
|
3
3
|
activeTurnId: string | null;
|
|
4
4
|
nextMessageIndex: number;
|
|
5
|
+
pendingToolCalls?: SessionToolCall[];
|
|
5
6
|
}
|
|
6
7
|
export interface SessionLogBootstrap {
|
|
7
8
|
cursor: SessionLogCursor;
|
|
@@ -17,6 +18,12 @@ export interface SplitJsonlChunk {
|
|
|
17
18
|
lines: string[];
|
|
18
19
|
remainder: string;
|
|
19
20
|
}
|
|
21
|
+
interface SessionToolCall {
|
|
22
|
+
callId: string;
|
|
23
|
+
kind: 'custom' | 'function';
|
|
24
|
+
name: string;
|
|
25
|
+
}
|
|
20
26
|
export declare function splitJsonlChunk(remainder: string, chunk: string): SplitJsonlChunk;
|
|
21
27
|
export declare function bootstrapSessionLog(lines: string[]): SessionLogBootstrap;
|
|
22
28
|
export declare function applySessionLog(lines: string[], cursor: SessionLogCursor): SessionLogDiff;
|
|
29
|
+
export {};
|
|
@@ -13,34 +13,30 @@ export function splitJsonlChunk(remainder, chunk) {
|
|
|
13
13
|
}
|
|
14
14
|
export function bootstrapSessionLog(lines) {
|
|
15
15
|
const records = parseRecords(lines);
|
|
16
|
-
let
|
|
17
|
-
let nextMessageIndex = 0;
|
|
16
|
+
let state = { activeTurnId: null, nextMessageIndex: 0 };
|
|
18
17
|
let events = [];
|
|
19
18
|
for (const record of records) {
|
|
20
|
-
const next = applySessionRecord(record,
|
|
19
|
+
const next = applySessionRecord(record, state);
|
|
21
20
|
if (next.startedTurnId) {
|
|
22
|
-
|
|
23
|
-
nextMessageIndex = 0;
|
|
21
|
+
state = next.cursor;
|
|
24
22
|
events = [];
|
|
25
23
|
continue;
|
|
26
24
|
}
|
|
27
|
-
if (!activeTurnId) {
|
|
25
|
+
if (!state.activeTurnId) {
|
|
28
26
|
continue;
|
|
29
27
|
}
|
|
30
28
|
if (next.turnCompleted) {
|
|
31
|
-
|
|
32
|
-
nextMessageIndex = 0;
|
|
29
|
+
state = next.cursor;
|
|
33
30
|
events = [];
|
|
34
31
|
continue;
|
|
35
32
|
}
|
|
36
33
|
events.push(...next.events);
|
|
37
|
-
|
|
38
|
-
nextMessageIndex = next.cursor.nextMessageIndex;
|
|
34
|
+
state = next.cursor;
|
|
39
35
|
}
|
|
40
36
|
return {
|
|
41
|
-
cursor:
|
|
37
|
+
cursor: state,
|
|
42
38
|
events,
|
|
43
|
-
startedTurnId: activeTurnId,
|
|
39
|
+
startedTurnId: state.activeTurnId,
|
|
44
40
|
};
|
|
45
41
|
}
|
|
46
42
|
export function applySessionLog(lines, cursor) {
|
|
@@ -51,10 +47,7 @@ export function applySessionLog(lines, cursor) {
|
|
|
51
47
|
for (const record of records) {
|
|
52
48
|
const next = applySessionRecord(record, state);
|
|
53
49
|
if (next.startedTurnId) {
|
|
54
|
-
state =
|
|
55
|
-
activeTurnId: next.startedTurnId,
|
|
56
|
-
nextMessageIndex: 0,
|
|
57
|
-
};
|
|
50
|
+
state = next.cursor;
|
|
58
51
|
startedTurnIds.push(next.startedTurnId);
|
|
59
52
|
continue;
|
|
60
53
|
}
|
|
@@ -104,6 +97,35 @@ function applySessionRecord(record, cursor) {
|
|
|
104
97
|
if (type === 'event_msg' && payload?.type === 'agent_message' && typeof payload.message === 'string') {
|
|
105
98
|
return createSessionTextEvents(activeTurnId, cursor, payload.message, typeof payload.phase === 'string' ? payload.phase : null, false);
|
|
106
99
|
}
|
|
100
|
+
if (type === 'event_msg'
|
|
101
|
+
&& payload?.type === 'item_completed'
|
|
102
|
+
&& payload?.turn_id === activeTurnId
|
|
103
|
+
&& normalizeSessionItemType(payload?.item) === 'agentmessage') {
|
|
104
|
+
const text = extractSessionItemText(payload.item);
|
|
105
|
+
if (text === null) {
|
|
106
|
+
return { cursor, events: [], startedTurnId: null, turnCompleted: false };
|
|
107
|
+
}
|
|
108
|
+
return createSessionTextEvents(activeTurnId, cursor, text, typeof payload.item.phase === 'string' ? payload.item.phase : null, false, false, typeof payload.item.id === 'string' ? payload.item.id : null);
|
|
109
|
+
}
|
|
110
|
+
if (type === 'event_msg'
|
|
111
|
+
&& payload?.type === 'item_completed'
|
|
112
|
+
&& payload?.turn_id === activeTurnId
|
|
113
|
+
&& normalizeSessionItemType(payload?.item) === 'usermessage') {
|
|
114
|
+
const text = extractSessionItemText(payload.item)?.trim() ?? '';
|
|
115
|
+
if (!text) {
|
|
116
|
+
return { cursor, events: [], startedTurnId: null, turnCompleted: false };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
cursor,
|
|
120
|
+
events: [{
|
|
121
|
+
kind: 'user_message',
|
|
122
|
+
turnId: activeTurnId,
|
|
123
|
+
text,
|
|
124
|
+
}],
|
|
125
|
+
startedTurnId: null,
|
|
126
|
+
turnCompleted: false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
107
129
|
if (type === 'response_item' && payload?.type === 'plan' && typeof payload.text === 'string') {
|
|
108
130
|
return createSessionTextEvents(activeTurnId, cursor, payload.text, 'commentary', true, true);
|
|
109
131
|
}
|
|
@@ -140,6 +162,18 @@ function applySessionRecord(record, cursor) {
|
|
|
140
162
|
turnCompleted: false,
|
|
141
163
|
};
|
|
142
164
|
}
|
|
165
|
+
if (type === 'response_item' && payload?.type === 'custom_tool_call') {
|
|
166
|
+
return createSessionToolStart(activeTurnId, cursor, payload, 'custom');
|
|
167
|
+
}
|
|
168
|
+
if (type === 'response_item' && payload?.type === 'function_call') {
|
|
169
|
+
return createSessionToolStart(activeTurnId, cursor, payload, 'function');
|
|
170
|
+
}
|
|
171
|
+
if (type === 'response_item' && payload?.type === 'custom_tool_call_output') {
|
|
172
|
+
return createSessionToolEnd(activeTurnId, cursor, payload, 'custom');
|
|
173
|
+
}
|
|
174
|
+
if (type === 'response_item' && payload?.type === 'function_call_output') {
|
|
175
|
+
return createSessionToolEnd(activeTurnId, cursor, payload, 'function');
|
|
176
|
+
}
|
|
143
177
|
if (type === 'event_msg' && payload?.type === 'exec_command_end' && payload?.turn_id === activeTurnId) {
|
|
144
178
|
const exec = createExecEndEvent(payload);
|
|
145
179
|
if (!exec) {
|
|
@@ -169,6 +203,20 @@ function applySessionRecord(record, cursor) {
|
|
|
169
203
|
turnCompleted: true,
|
|
170
204
|
};
|
|
171
205
|
}
|
|
206
|
+
if (type === 'event_msg'
|
|
207
|
+
&& (payload?.type === 'turn_aborted' || payload?.type === 'turn_interrupted')
|
|
208
|
+
&& payload?.turn_id === activeTurnId) {
|
|
209
|
+
return {
|
|
210
|
+
cursor: { activeTurnId: null, nextMessageIndex: 0 },
|
|
211
|
+
events: [{
|
|
212
|
+
kind: 'turn_completed',
|
|
213
|
+
turnId: activeTurnId,
|
|
214
|
+
state: 'interrupted',
|
|
215
|
+
}],
|
|
216
|
+
startedTurnId: null,
|
|
217
|
+
turnCompleted: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
172
220
|
return {
|
|
173
221
|
cursor,
|
|
174
222
|
events: [],
|
|
@@ -179,12 +227,13 @@ function applySessionRecord(record, cursor) {
|
|
|
179
227
|
function buildSessionItemId(turnId, index) {
|
|
180
228
|
return `${turnId}:session:${index}`;
|
|
181
229
|
}
|
|
182
|
-
function createSessionTextEvents(activeTurnId, cursor, text, phase, forceCommentary, isPlan = false) {
|
|
183
|
-
const itemId = buildSessionItemId(activeTurnId, cursor.nextMessageIndex + 1);
|
|
230
|
+
function createSessionTextEvents(activeTurnId, cursor, text, phase, forceCommentary, isPlan = false, sourceItemId = null) {
|
|
231
|
+
const itemId = sourceItemId ?? buildSessionItemId(activeTurnId, cursor.nextMessageIndex + 1);
|
|
184
232
|
const outputKind = forceCommentary ? 'commentary' : classifyAgentOutput(phase, true);
|
|
185
233
|
const streamOutputKind = forceCommentary ? 'commentary' : classifyAgentOutput(phase, false);
|
|
186
234
|
return {
|
|
187
235
|
cursor: {
|
|
236
|
+
...cursor,
|
|
188
237
|
activeTurnId,
|
|
189
238
|
nextMessageIndex: cursor.nextMessageIndex + 1,
|
|
190
239
|
},
|
|
@@ -219,6 +268,80 @@ function createSessionTextEvents(activeTurnId, cursor, text, phase, forceComment
|
|
|
219
268
|
turnCompleted: false,
|
|
220
269
|
};
|
|
221
270
|
}
|
|
271
|
+
function createSessionToolStart(turnId, cursor, payload, kind) {
|
|
272
|
+
const callId = typeof payload?.call_id === 'string' ? payload.call_id : null;
|
|
273
|
+
const name = typeof payload?.name === 'string' ? payload.name.trim() : '';
|
|
274
|
+
if (!callId || !name) {
|
|
275
|
+
return { cursor, events: [], startedTurnId: null, turnCompleted: false };
|
|
276
|
+
}
|
|
277
|
+
const pendingToolCalls = [
|
|
278
|
+
...(cursor.pendingToolCalls ?? []).filter(call => call.callId !== callId),
|
|
279
|
+
{ callId, kind, name },
|
|
280
|
+
];
|
|
281
|
+
const exec = createSessionToolEvent(turnId, callId, name);
|
|
282
|
+
return {
|
|
283
|
+
cursor: { ...cursor, pendingToolCalls },
|
|
284
|
+
events: [{
|
|
285
|
+
kind: 'tool_started',
|
|
286
|
+
turnId,
|
|
287
|
+
exec,
|
|
288
|
+
state: inferToolActivityState(exec),
|
|
289
|
+
}],
|
|
290
|
+
startedTurnId: null,
|
|
291
|
+
turnCompleted: false,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function createSessionToolEnd(turnId, cursor, payload, kind) {
|
|
295
|
+
const callId = typeof payload?.call_id === 'string' ? payload.call_id : null;
|
|
296
|
+
const pending = callId
|
|
297
|
+
? (cursor.pendingToolCalls ?? []).find(call => call.callId === callId && call.kind === kind)
|
|
298
|
+
: null;
|
|
299
|
+
if (!pending) {
|
|
300
|
+
return { cursor, events: [], startedTurnId: null, turnCompleted: false };
|
|
301
|
+
}
|
|
302
|
+
const exec = createSessionToolEvent(turnId, pending.callId, pending.name);
|
|
303
|
+
return {
|
|
304
|
+
cursor: {
|
|
305
|
+
...cursor,
|
|
306
|
+
pendingToolCalls: (cursor.pendingToolCalls ?? []).filter(call => call !== pending),
|
|
307
|
+
},
|
|
308
|
+
events: [{
|
|
309
|
+
kind: 'tool_completed',
|
|
310
|
+
turnId,
|
|
311
|
+
exec,
|
|
312
|
+
state: inferToolActivityState(exec),
|
|
313
|
+
}],
|
|
314
|
+
startedTurnId: null,
|
|
315
|
+
turnCompleted: false,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function createSessionToolEvent(turnId, callId, name) {
|
|
319
|
+
return {
|
|
320
|
+
callId,
|
|
321
|
+
turnId,
|
|
322
|
+
command: [`Tool ${name}`],
|
|
323
|
+
cwd: null,
|
|
324
|
+
parsedCmd: [],
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function normalizeSessionItemType(item) {
|
|
328
|
+
if (typeof item?.type !== 'string') {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
return item.type.replace(/[^a-z]/gi, '').toLowerCase();
|
|
332
|
+
}
|
|
333
|
+
function extractSessionItemText(item) {
|
|
334
|
+
if (typeof item?.text === 'string') {
|
|
335
|
+
return item.text;
|
|
336
|
+
}
|
|
337
|
+
if (!Array.isArray(item?.content)) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const parts = item.content
|
|
341
|
+
.map((entry) => typeof entry?.text === 'string' ? entry.text : '')
|
|
342
|
+
.filter(Boolean);
|
|
343
|
+
return parts.length > 0 ? parts.join('') : null;
|
|
344
|
+
}
|
|
222
345
|
function createExecStartEvent(turnId, payload) {
|
|
223
346
|
const callId = typeof payload.call_id === 'string' ? payload.call_id : null;
|
|
224
347
|
if (!callId) {
|