@foxden-app/foxclaw 0.5.42 → 0.5.43

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,18 @@
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.43 - 2026-06-18
6
+
7
+ ### 中文
8
+ - Codex 最终答复出现后,FoxClaw 会把本轮所有过程汇报合并到第一条过程消息,并编辑为默认收起的 Telegram RichMessage `details`;最终答复继续独立展示,使历史记录保持清晰的一问一答结构。
9
+ - 折叠摘要显示过程条数和整体起止时间,展开后每段过程汇报保留精确到秒的起止时间及原有 Markdown 富文本效果;普通 Telegram 回合和 CLI 观察回合统一生效。
10
+ - 如果 RichMessage 编辑失败,原过程消息全部保留且不执行删除;超出 Telegram 单条富消息上限时会明确标注未收入归档的条数。
11
+
12
+ ### English
13
+ - 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.
14
+ - 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.
15
+ - 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.
16
+
5
17
  ## 0.5.42 - 2026-06-18
6
18
 
7
19
  ### 中文
@@ -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(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.42",
3
+ "version": "0.5.43",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",