@foxden-app/foxclaw 0.5.42 → 0.5.44
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,28 @@
|
|
|
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.5.44 - 2026-06-18
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- 修复 Codex 真实 `final_answer` phase 在标准化后变成 `finalanswer`、却未被识别为最终答复的问题;该错误会导致 `0.5.43` 的过程汇报折叠逻辑误判“本轮没有最终答复”并直接跳过。
|
|
9
|
+
- 增加真实 phase 分类回归测试,确保最终答复与 commentary 过程汇报正确分离。
|
|
10
|
+
|
|
11
|
+
### English
|
|
12
|
+
- Fixed classification of Codex's real `final_answer` phase, which normalizes to `finalanswer` but was not recognized as a final response; this caused the `0.5.43` progress-collapse path to incorrectly skip turns as having no final answer.
|
|
13
|
+
- Added a regression test for the real phase value so final responses remain distinct from commentary updates.
|
|
14
|
+
|
|
15
|
+
## 0.5.43 - 2026-06-18
|
|
16
|
+
|
|
17
|
+
### 中文
|
|
18
|
+
- Codex 最终答复出现后,FoxClaw 会把本轮所有过程汇报合并到第一条过程消息,并编辑为默认收起的 Telegram RichMessage `details`;最终答复继续独立展示,使历史记录保持清晰的一问一答结构。
|
|
19
|
+
- 折叠摘要显示过程条数和整体起止时间,展开后每段过程汇报保留精确到秒的起止时间及原有 Markdown 富文本效果;普通 Telegram 回合和 CLI 观察回合统一生效。
|
|
20
|
+
- 如果 RichMessage 编辑失败,原过程消息全部保留且不执行删除;超出 Telegram 单条富消息上限时会明确标注未收入归档的条数。
|
|
21
|
+
|
|
22
|
+
### English
|
|
23
|
+
- After a Codex final answer arrives, FoxClaw consolidates all progress updates from the turn into the first progress message and edits it into a collapsed Telegram RichMessage `details` block; the final answer remains separate for a clean question-and-answer history.
|
|
24
|
+
- The collapsed summary shows the update count and overall time range, while each expanded update preserves second-level start/end timestamps and Markdown-rich rendering; this applies to both normal Telegram turns and CLI-observed turns.
|
|
25
|
+
- If RichMessage editing fails, every original progress message is preserved and no deletion is attempted; updates exceeding Telegram's single-rich-message limit are reported explicitly.
|
|
26
|
+
|
|
5
27
|
## 0.5.42 - 2026-06-18
|
|
6
28
|
|
|
7
29
|
### 中文
|
|
@@ -166,7 +166,7 @@ export function classifyAgentOutput(phase, completed) {
|
|
|
166
166
|
return completed ? 'final_answer' : 'commentary';
|
|
167
167
|
}
|
|
168
168
|
const normalized = phase.replace(/[^a-z]/gi, '').toLowerCase();
|
|
169
|
-
if (normalized === 'final' || normalized === 'answer' || normalized === 'response') {
|
|
169
|
+
if (normalized === 'final' || normalized === 'finalanswer' || normalized === 'answer' || normalized === 'response') {
|
|
170
170
|
return 'final_answer';
|
|
171
171
|
}
|
|
172
172
|
return 'commentary';
|
|
@@ -236,6 +236,7 @@ export declare class BridgeSessionCore {
|
|
|
236
236
|
private deleteMessage;
|
|
237
237
|
private sendTyping;
|
|
238
238
|
private sendObservedCliUserMessage;
|
|
239
|
+
private collapseTurnCommentary;
|
|
239
240
|
private cleanupObservedTransientMessages;
|
|
240
241
|
private hasObservedPersistentReply;
|
|
241
242
|
private ensureThreadReady;
|
|
@@ -1855,6 +1855,7 @@ export class BridgeSessionCore {
|
|
|
1855
1855
|
const segment = ensureTurnSegment(active, `${active.turnId}:codex-error`, 'final_answer', 'final_answer', false);
|
|
1856
1856
|
segment.text = text;
|
|
1857
1857
|
segment.completed = true;
|
|
1858
|
+
segment.completedAtMs = Date.now();
|
|
1858
1859
|
await this.queueTurnRender(active, { forceStatus: true, forceStream: true });
|
|
1859
1860
|
}
|
|
1860
1861
|
async finishTerminalErroredActiveTurn(active) {
|
|
@@ -3131,6 +3132,7 @@ export class BridgeSessionCore {
|
|
|
3131
3132
|
let shouldMarkPartialOutput = false;
|
|
3132
3133
|
try {
|
|
3133
3134
|
await this.queueTurnRender(active, { forceStatus: true, forceStream: true });
|
|
3135
|
+
await this.collapseTurnCommentary(active);
|
|
3134
3136
|
const renderedMessages = active.segments.reduce((count, segment) => count + segment.messages.length, 0);
|
|
3135
3137
|
if (renderedMessages === 0) {
|
|
3136
3138
|
const fallbackKey = active.interruptRequested ? 'interrupted' : 'completed';
|
|
@@ -3186,6 +3188,7 @@ export class BridgeSessionCore {
|
|
|
3186
3188
|
}
|
|
3187
3189
|
}
|
|
3188
3190
|
segment.completed = true;
|
|
3191
|
+
segment.completedAtMs = Date.now();
|
|
3189
3192
|
await this.queueTurnRender(active, { forceStream: true, forceStatus: true });
|
|
3190
3193
|
return;
|
|
3191
3194
|
}
|
|
@@ -3421,6 +3424,50 @@ export class BridgeSessionCore {
|
|
|
3421
3424
|
await this.sendHtmlMessage(scopeId, body);
|
|
3422
3425
|
}
|
|
3423
3426
|
}
|
|
3427
|
+
async collapseTurnCommentary(active) {
|
|
3428
|
+
if (active.scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX) || !this.hasObservedPersistentReply(active)) {
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
const segments = active.segments.filter(segment => (segment.outputKind === 'commentary' && Boolean(segment.text.trim()) && segment.messages.length > 0));
|
|
3432
|
+
const messages = segments.flatMap(segment => segment.messages);
|
|
3433
|
+
const firstMessage = messages[0];
|
|
3434
|
+
if (!firstMessage) {
|
|
3435
|
+
return;
|
|
3436
|
+
}
|
|
3437
|
+
const locale = this.localeForChat(active.scopeId);
|
|
3438
|
+
const html = renderCollapsedCommentary(locale, segments);
|
|
3439
|
+
const fallback = locale === 'zh'
|
|
3440
|
+
? `过程汇报(${segments.length} 条,已折叠)`
|
|
3441
|
+
: `Progress updates (${segments.length}, collapsed)`;
|
|
3442
|
+
try {
|
|
3443
|
+
await this.messaging.editRichHtml(active.scopeId, firstMessage.messageId, html, fallback, []);
|
|
3444
|
+
}
|
|
3445
|
+
catch (error) {
|
|
3446
|
+
this.logger.warn('telegram.commentary_collapse_failed', {
|
|
3447
|
+
error: String(error),
|
|
3448
|
+
turnId: active.turnId,
|
|
3449
|
+
messageId: firstMessage.messageId,
|
|
3450
|
+
});
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
for (const message of messages.slice(1)) {
|
|
3454
|
+
try {
|
|
3455
|
+
await this.deleteMessage(active.scopeId, message.messageId);
|
|
3456
|
+
}
|
|
3457
|
+
catch (error) {
|
|
3458
|
+
if (!isTelegramMessageGone(error)) {
|
|
3459
|
+
this.logger.warn('telegram.commentary_collapse_delete_failed', {
|
|
3460
|
+
error: String(error),
|
|
3461
|
+
turnId: active.turnId,
|
|
3462
|
+
messageId: message.messageId,
|
|
3463
|
+
});
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
for (const segment of segments) {
|
|
3468
|
+
segment.messages = [];
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3424
3471
|
async cleanupObservedTransientMessages(active) {
|
|
3425
3472
|
if (!active.isObserved || !this.hasObservedPersistentReply(active)) {
|
|
3426
3473
|
return;
|
|
@@ -8235,11 +8282,50 @@ function ensureTurnSegment(active, itemId, phase, outputKind, isPlan) {
|
|
|
8235
8282
|
isPlan: Boolean(isPlan),
|
|
8236
8283
|
text: '',
|
|
8237
8284
|
completed: false,
|
|
8285
|
+
startedAtMs: Date.now(),
|
|
8286
|
+
completedAtMs: null,
|
|
8238
8287
|
messages: [],
|
|
8239
8288
|
};
|
|
8240
8289
|
active.segments.push(segment);
|
|
8241
8290
|
return segment;
|
|
8242
8291
|
}
|
|
8292
|
+
function renderCollapsedCommentary(locale, segments) {
|
|
8293
|
+
const firstAt = segments[0]?.startedAtMs ?? Date.now();
|
|
8294
|
+
const lastSegment = segments[segments.length - 1];
|
|
8295
|
+
const lastAt = lastSegment?.completedAtMs ?? lastSegment?.startedAtMs ?? firstAt;
|
|
8296
|
+
const range = firstAt === lastAt
|
|
8297
|
+
? formatClockTimestamp(firstAt)
|
|
8298
|
+
: `${formatClockTimestamp(firstAt)} - ${formatClockTimestamp(lastAt)}`;
|
|
8299
|
+
const summary = locale === 'zh'
|
|
8300
|
+
? `过程汇报 · ${segments.length} 条 · ${range}`
|
|
8301
|
+
: `Progress · ${segments.length} updates · ${range}`;
|
|
8302
|
+
const maxBodyLength = TELEGRAM_RICH_MESSAGE_TEXT_LIMIT - summary.length - 1024;
|
|
8303
|
+
const blocks = [];
|
|
8304
|
+
let bodyLength = 0;
|
|
8305
|
+
let omitted = 0;
|
|
8306
|
+
for (const segment of segments) {
|
|
8307
|
+
const endAt = segment.completedAtMs ?? segment.startedAtMs;
|
|
8308
|
+
const timestamp = endAt === segment.startedAtMs
|
|
8309
|
+
? formatClockTimestamp(segment.startedAtMs)
|
|
8310
|
+
: `${formatClockTimestamp(segment.startedAtMs)} - ${formatClockTimestamp(endAt)}`;
|
|
8311
|
+
const block = `<h4>${escapeTelegramHtml(timestamp)}</h4>\n${renderTelegramMarkdownRichHtml(segment.text)}`;
|
|
8312
|
+
if (bodyLength + block.length > maxBodyLength) {
|
|
8313
|
+
omitted += 1;
|
|
8314
|
+
continue;
|
|
8315
|
+
}
|
|
8316
|
+
blocks.push(block);
|
|
8317
|
+
bodyLength += block.length;
|
|
8318
|
+
}
|
|
8319
|
+
if (omitted > 0) {
|
|
8320
|
+
blocks.push(`<p>${escapeTelegramHtml(locale === 'zh' ? `另有 ${omitted} 条过长内容未收入归档` : `${omitted} oversized updates omitted`)}</p>`);
|
|
8321
|
+
}
|
|
8322
|
+
return telegramDetails(summary, blocks.join('\n'));
|
|
8323
|
+
}
|
|
8324
|
+
function formatClockTimestamp(timestampMs) {
|
|
8325
|
+
const date = new Date(timestampMs);
|
|
8326
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
8327
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
8328
|
+
}
|
|
8243
8329
|
function createToolBatchState() {
|
|
8244
8330
|
return {
|
|
8245
8331
|
openCallIds: new Set(),
|